Skip to content

[DX-928] Reduce Duplication #153

Merged
umair-ably merged 7 commits intomainfrom
DX-792-refactor
Mar 6, 2026
Merged

[DX-928] Reduce Duplication #153
umair-ably merged 7 commits intomainfrom
DX-792-refactor

Conversation

@umair-ably
Copy link
Contributor

@umair-ably umair-ably commented Mar 6, 2026

Repo wide refactor to identify areas of duplication and steer back towards KISS and DRY principles

Summary by CodeRabbit

  • Bug Fixes

    • Improved centralized error handling across commands for clearer error messaging.
  • Refactor

    • Consolidated timestamp formatting utilities for consistent message timestamp display across the CLI.
    • Standardized command flag handling for duration and rewind options across multiple commands.
    • Unified status change logging and connection state management for improved feedback during operations.

- Deletes stale files
- Audits and updates docs for accuracy and relevance
- Improves test coverage
@vercel
Copy link

vercel bot commented Mar 6, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cli-web-cli Ready Ready Preview, Comment Mar 6, 2026 2:51pm

Request Review

@umair-ably umair-ably requested a review from Copilot March 6, 2026 11:22
@coderabbitai
Copy link

coderabbitai bot commented Mar 6, 2026

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 3cd47ced-1dcb-4d07-8baa-00181b1f2688

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

This PR introduces centralized utilities and base command patterns to reduce duplication across the CLI. It exports reusable flags (durationFlag, rewindFlag) and utility functions (formatMessageTimestamp, formatPresenceAction, buildHistoryParams, interpolateMessage), implements centralized error handling and configuration helpers, abstracts room status handling, and establishes new SpacesBaseCommand and StatsBaseCommand classes to consolidate repeated patterns in subscribe and stats commands.

Changes

Cohort / File(s) Summary
Core Flags & Utilities
src/flags.ts, src/utils/output.ts, src/utils/history.ts, src/utils/message.ts
Added durationFlag and rewindFlag exports; introduced formatMessageTimestamp() and formatPresenceAction() utilities for consistent timestamp and presence action rendering; added buildHistoryParams() helper to consolidate history parameter construction and validation.
Base Command Enhancements
src/base-command.ts, src/chat-base-command.ts
Added protected helpers: parseJsonFlag() for JSON flag parsing, handleCommandError() for centralized error handling, and configureRewind() for rewind setup. Removed _chatRealtimeClient field and added setupRoomStatusHandler() for unified room status change handling and logging.
Spaces Base Command
src/spaces-base-command.ts
Introduced new protected async methods: waitForConnection(), initializeSpace(), and waitForCursorsChannelAttachment() to centralize space initialization, connection management, and cursors channel readiness logic across space commands.
Stats Base Command
src/stats-base-command.ts
New abstract base class StatsBaseCommand providing unified framework for stats polling with static statsFlags, abstract methods fetchStats() and getStatsLabel(), and protected methods for live/one-time stats retrieval and display management.
Channels Commands
src/commands/channels/history.ts, src/commands/channels/occupancy/subscribe.ts, src/commands/channels/presence/enter.ts, src/commands/channels/presence/subscribe.ts, src/commands/channels/publish.ts, src/commands/channels/subscribe.ts
Refactored to use buildHistoryParams(), durationFlag, rewindFlag, formatMessageTimestamp(), configureRewind(), and handleCommandError(); removed local interpolateMessage() in favor of shared utility.
Logs Commands
src/commands/logs/channel-lifecycle/subscribe.ts, src/commands/logs/connection-lifecycle/history.ts, src/commands/logs/connection-lifecycle/subscribe.ts, src/commands/logs/history.ts, src/commands/logs/push/history.ts, src/commands/logs/push/subscribe.ts, src/commands/logs/subscribe.ts
Updated to use buildHistoryParams(), durationFlag, rewindFlag, formatMessageTimestamp(), configureRewind(), and centralized handleCommandError() for consistent error handling and timestamp formatting.
Rooms Commands
src/commands/rooms/list.ts, src/commands/rooms/messages/history.ts, src/commands/rooms/messages/reactions/remove.ts, src/commands/rooms/messages/reactions/send.ts, src/commands/rooms/messages/reactions/subscribe.ts, src/commands/rooms/messages/send.ts, src/commands/rooms/messages/subscribe.ts, src/commands/rooms/occupancy/get.ts, src/commands/rooms/occupancy/subscribe.ts, src/commands/rooms/presence/enter.ts, src/commands/rooms/presence/subscribe.ts, src/commands/rooms/reactions/send.ts, src/commands/rooms/reactions/subscribe.ts, src/commands/rooms/typing/keystroke.ts, src/commands/rooms/typing/subscribe.ts
Consolidated to use durationFlag, formatMessageTimestamp(), formatPresenceAction(), parseJsonFlag(), setupRoomStatusHandler(), and handleCommandError(); removed inline status handling and error handling branches in favor of centralized helpers.
Spaces Commands
src/commands/spaces/cursors/get-all.ts, src/commands/spaces/cursors/set.ts, src/commands/spaces/cursors/subscribe.ts, src/commands/spaces/locations/get-all.ts, src/commands/spaces/locations/set.ts, src/commands/spaces/locations/subscribe.ts, src/commands/spaces/locks/acquire.ts, src/commands/spaces/locks/get-all.ts, src/commands/spaces/locks/get.ts, src/commands/spaces/locks/subscribe.ts, src/commands/spaces/members/enter.ts, src/commands/spaces/members/subscribe.ts
Refactored to use initializeSpace(), waitForCursorsChannelAttachment(), durationFlag, parseJsonFlag(), handleCommandError(); replaced manual client/space setup with unified initialization flow; added non-null assertions for space access.
Stats Commands
src/commands/stats/account.ts, src/commands/stats/app.ts
Migrated from ControlBaseCommand to StatsBaseCommand; simplified flag handling to use StatsBaseCommand.statsFlags; introduced protected fetchStats() and getStatsLabel() methods; removed extensive polling and display logic in favor of base class framework.
Test Updates
test/e2e/rooms/rooms-e2e.test.ts, test/unit/commands/logs/connection-lifecycle/subscribe.test.ts
Updated test expectations: changed subscription message assertion and adjusted expected parameter shape for channel initialization.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 Hops of joy through refactored code,
Flags now shared on a lighter load,
Errors centralized, status abstracted clean,
The finest utilities ever seen!
Room handlers sing in harmony.
✨🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'DX-928 Reduce Duplication' directly reflects the main objective of this pull request, which is a repository-wide refactor to eliminate code duplication and apply DRY (Don't Repeat Yourself) principles.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch DX-792-refactor

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR is a codebase-wide refactor that eliminates duplication by extracting common patterns into shared utilities and base-class methods, in line with KISS and DRY principles.

Changes:

  • Extracted shared flags (durationFlag, rewindFlag) and utility functions (buildHistoryParams, interpolateMessage, formatMessageTimestamp, formatPresenceAction) to reduce inline repetition across dozens of commands.
  • Added base-class helper methods (handleCommandError, configureRewind, parseJsonFlag, setupRoomStatusHandler, initializeSpace, waitForConnection, waitForCursorsChannelAttachment) to consolidate repeated patterns in command catch blocks and setup flows.
  • Introduced StatsBaseCommand abstract class to unify the duplicate stats polling/display logic previously duplicated between stats/app.ts and stats/account.ts.

Reviewed changes

Copilot reviewed 53 out of 53 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/flags.ts Added shared durationFlag and rewindFlag exports
src/utils/output.ts Added formatMessageTimestamp and formatPresenceAction helpers
src/utils/message.ts New file: extracted interpolateMessage from command classes
src/utils/history.ts New file: extracted buildHistoryParams from history commands
src/base-command.ts Added handleCommandError, configureRewind, parseJsonFlag helpers
src/chat-base-command.ts Added setupRoomStatusHandler helper; removed _chatRealtimeClient field
src/spaces-base-command.ts Added waitForConnection, initializeSpace, waitForCursorsChannelAttachment helpers
src/stats-base-command.ts New file: abstract base for stats commands
src/commands/stats/app.ts Refactored to extend StatsBaseCommand
src/commands/stats/account.ts Refactored to extend StatsBaseCommand
src/commands/spaces/** Replaced inline setup/cleanup with shared helpers
src/commands/rooms/** Replaced inline status handling and error blocks with helpers
src/commands/channels/** Replaced inline rewind/error/timestamp logic with helpers
src/commands/logs/** Replaced inline history params and error handling with helpers
test/unit/commands/logs/connection-lifecycle/subscribe.test.ts Updated expectation from undefined to {} for channel options
test/e2e/rooms/rooms-e2e.test.ts Updated expected ready signal from "Connected to room:" to "Subscribed to room:"
AGENTS.md Updated documentation to reference new shared flags and helpers

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/commands/spaces/cursors/get-all.ts (1)

168-188: ⚠️ Potential issue | 🟠 Major

Keep the live-update fallback reachable.

The new stabilization wait rejects before the getAll() fallback try/catch runs. If the connection stays non-connected for 5 seconds, the command now exits with an error instead of returning the cursor updates already collected in cursorMap. Move the wait inside the try { ... getAll() ... } catch { ... } block, or downgrade the timeout to a warning and continue with the live snapshot.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/spaces/cursors/get-all.ts` around lines 168 - 188, The
connection-stabilization Promise currently lives outside the try/catch so a
timeout rejection escapes the getAll() fallback; move the wait (the block that
checks this.realtimeClient!.connection.state and awaits the Promise with
setTimeout/once) inside the same try that calls this.space!.cursors.getAll() so
any timeout rejection is caught and the catch block can return the existing
cursorMap/live snapshot, or alternatively change the timeout rejection to a
non-throwing warning that logs and continues to call
this.space!.cursors.getAll(); touch the symbols
this.realtimeClient!.connection.state, the stabilization Promise,
this.space!.cursors.getAll(), and cursorMap when making the change.
src/commands/spaces/locations/set.ts (1)

144-155: ⚠️ Potential issue | 🟡 Minor

E2E error handling outputs misleading success status.

When an error occurs in the E2E code path, the catch block still outputs success: true, which masks the actual failure. This could make debugging E2E test failures difficult.

🛡️ Suggested fix to output accurate status
       } catch {
         // If an error occurs in E2E mode, just exit cleanly after showing what we can
         if (this.shouldOutputJson(flags)) {
           this.log(
             this.formatJsonOutput(
-              { success: true, location, spaceName },
+              { success: false, error: "Failed to set location", location, spaceName },
               flags,
             ),
           );
         }
         // Don't call this.error() in E2E mode as it sets exit code to 1
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/spaces/locations/set.ts` around lines 144 - 155, The catch block
currently swallows the thrown error and emits a JSON payload with success: true;
update the catch to capture the error (e.g., catch (err)) and when
this.shouldOutputJson(flags) is true call this.formatJsonOutput with success:
false and include the error details (err.message and/or err.stack) alongside
location and spaceName so E2E consumers see the real failure status, but still
avoid calling this.error() to keep exit code behavior unchanged.
🧹 Nitpick comments (11)
src/utils/output.ts (1)

27-34: Consider whether timestamp 0 should be treated as valid.

The condition !timestamp && timestamp !== 0 explicitly allows 0 as a valid timestamp, which would produce 1970-01-01T00:00:00.000Z. While this is technically correct behavior, a Unix timestamp of 0 is almost never a valid message timestamp in practice and could mask bugs where 0 is passed erroneously.

If 0 is not expected as a valid input, simplifying to if (!timestamp) would treat 0 the same as null/undefined and fall back to the current time.

💡 Optional simplification
 export function formatMessageTimestamp(
   timestamp: number | Date | undefined | null,
 ): string {
-  if (!timestamp && timestamp !== 0) return new Date().toISOString();
+  if (!timestamp) return new Date().toISOString();
   return timestamp instanceof Date
     ? timestamp.toISOString()
     : new Date(timestamp).toISOString();
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/output.ts` around lines 27 - 34, The formatMessageTimestamp
function currently treats 0 as a valid timestamp; update formatMessageTimestamp
so a timestamp value of 0 is treated like null/undefined (i.e., fall back to
current time) by simplifying the check to if (!timestamp) and keep the rest of
the logic (Date instance -> toISOString, otherwise new
Date(timestamp).toISOString()); update any tests or callers that rely on 0 being
preserved.
src/commands/rooms/typing/keystroke.ts (1)

93-106: Consider extracting common status handling logic.

This command maintains custom onStatusChange handling because it needs to trigger keystroke() on attach. However, the status logging and Failed handling (lines 100-106, 172-179) duplicates logic from setupRoomStatusHandler.

Consider enhancing setupRoomStatusHandler to accept an optional onAttached callback, which would allow reuse while preserving the custom keystroke behavior:

this.setupRoomStatusHandler(room, flags, {
  roomName,
  onAttached: async () => {
    // Custom keystroke logic here
  }
});

This would further reduce duplication across room commands that need custom attach behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/rooms/typing/keystroke.ts` around lines 93 - 106, Extract the
duplicated status-change and Failed handling by extending setupRoomStatusHandler
to accept an options object with an optional onAttached callback; move the
existing logging and Failed->reason logic from this command's
room.onStatusChange handler into setupRoomStatusHandler and, in this file, call
setupRoomStatusHandler(room, flags, { roomName, onAttached: async () => { await
this.keystroke(...) } }) so the custom keystroke behavior runs on attach while
all status logging and error-reason extraction remains centralized in
setupRoomStatusHandler; update references to
onStatusChange/room.error/RoomStatus.Failed accordingly.
src/commands/spaces/locks/subscribe.ts (1)

62-66: Move or rename the post-init progress message.

After Line 62 returns, the command is already connected and has entered the space, so Connecting to space... is no longer true. Emit that progress line before initializeSpace() or change it to the next real step to keep the CLI output accurate.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/spaces/locks/subscribe.ts` around lines 62 - 66, The progress
message "Connecting to space..." is emitted after await
this.initializeSpace(...) so it reports a step that's already completed; either
move the log call to before the await to show the actual connection step, or
change the message after initializeSpace to reflect the new state (e.g.,
"Connected to space" or "Entered space")—update the call site that invokes
this.log(progress(resource(...))) in subscribe.ts near initializeSpace, keeping
the guard this.shouldOutputJson(flags) unchanged.
src/commands/spaces/locks/get.ts (1)

39-45: Let initializeSpace() own the enter step too.

You're still duplicating one of the helper's responsibilities here. Passing enterSpace: true keeps the lock command on the same initialization path as the other space commands and avoids the flow drifting later.

♻️ Suggested simplification
-      await this.initializeSpace(flags, spaceName, {
-        enterSpace: false,
-        setupConnectionLogging: false,
-      });
-
-      await this.space!.enter();
+      await this.initializeSpace(flags, spaceName, {
+        enterSpace: true,
+        setupConnectionLogging: false,
+      });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/spaces/locks/get.ts` around lines 39 - 45, The code calls
this.space!.enter() after initializeSpace even though initializeSpace supports
an enterSpace option; remove the explicit enter call and call initializeSpace
with enterSpace: true (e.g., await this.initializeSpace(flags, spaceName, {
enterSpace: true, setupConnectionLogging: false })) so the helper owns entering
the space and the flow matches other space commands (drop the
this.space!.enter() line).
src/commands/rooms/presence/enter.ts (1)

68-78: Quote stripping before JSON parsing may indicate upstream issue.

The manual trimming and quote removal before parseJsonFlag suggests shell quoting artifacts are reaching the command. This workaround handles it, but consider whether parseJsonFlag should handle these cases consistently, or if the issue is in how the shell passes arguments.

If this pattern is needed in multiple commands, consider moving the quote-stripping logic into parseJsonFlag itself for consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/rooms/presence/enter.ts` around lines 68 - 78, The code is
pre-stripping surrounding quotes from rawData before calling parseJsonFlag; move
this normalization into parseJsonFlag so all callers (including this
PresenceData flow) get consistent handling—update parseJsonFlag to trim
whitespace and remove a single pair of matching surrounding single or double
quotes before attempting JSON parsing, then remove the duplicate quote-stripping
block in enter.ts (the rawData/trimmed handling and slice logic) and continue to
call parseJsonFlag(trimmed, "data", flags) (or pass rawData unchanged) so
PresenceData parsing uses the centralized behavior.
src/commands/spaces/members/enter.ts (1)

276-285: Consider using centralized error handling for consistency.

Other commands in this PR use handleCommandError for catch blocks. While the inline approach works, consider aligning with the centralized pattern for consistency.

♻️ Optional: Use centralized error handler
     } catch (error) {
-      const errorMsg = `Error: ${error instanceof Error ? error.message : String(error)}`;
-      this.logCliEvent(flags, "error", "unhandledError", errorMsg, {
-        error: errorMsg,
-      });
-      if (this.shouldOutputJson(flags)) {
-        this.jsonError({ error: errorMsg, success: false }, flags);
-      } else {
-        this.error(errorMsg);
-      }
+      this.handleCommandError(error, flags, "member", { spaceName });
     } finally {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/spaces/members/enter.ts` around lines 276 - 285, The catch block
in enter.ts currently builds errorMsg, logs via this.logCliEvent and branches on
this.shouldOutputJson/this.jsonError/this.error; replace that inline handling
with a call to the centralized handler used elsewhere: handleCommandError. In
the catch for the command's main function, pass the command instance (this), the
parsed flags object (flags), and the caught error, and include the same
contextual metadata you were passing to logCliEvent (e.g., { event:
"unhandledError", error: ... } or similar) so logs and JSON output remain
consistent; remove the inline this.logCliEvent/this.jsonError/this.error calls
and rely on handleCommandError to perform consistent logging and output. Ensure
you import/resolve handleCommandError the same way other commands do.
src/commands/spaces/cursors/get-all.ts (1)

47-58: Extract the enter/poll sequence into SpacesBaseCommand.

This same 5-second enter-and-poll flow now exists here and in src/commands/spaces/locations/get-all.ts. Pulling it behind a shared helper would better match the PR’s DRY goal and make future connection-state fixes one-place changes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/spaces/cursors/get-all.ts` around lines 47 - 58, The
enter-and-poll connection sequence (the initializeSpace call with
enterSpace:false, the optional progress log via shouldOutputJson, and awaiting
this.space!.enter() plus the 5s poll) is duplicated and should be extracted into
a shared helper on SpacesBaseCommand (e.g., ensureSpaceConnected or
enterAndPollSpace). Add a new method on SpacesBaseCommand that accepts flags and
spaceName, performs initializeSpace(..., { enterSpace:false,
setupConnectionLogging:false }), emits the same progress log when
shouldOutputJson(flags) is false, calls await this.space!.enter(), and runs the
existing 5-second poll/connection-state logic; then replace the duplicated block
in get-all.ts (and the same block in locations/get-all.ts) with a call to that
new helper.
src/stats-base-command.ts (2)

143-150: Signal handlers don't terminate the process.

The cleanup function clears the interval and logs a message but doesn't call process.exit(). The command relies on the never-resolving Promise at line 172 to keep running. After SIGINT/SIGTERM, the interval is cleared but the process may hang until Node.js detects no more work. Consider explicitly exiting after cleanup.

♻️ Suggested fix to exit cleanly
       const cleanup = () => {
         if (this.pollInterval) {
           clearInterval(this.pollInterval);
           this.pollInterval = undefined;
         }

         this.log("\nUnsubscribed from live stats");
+        process.exit(0);
       };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/stats-base-command.ts` around lines 143 - 150, The cleanup callback for
live stats (cleanup) clears this.pollInterval and logs but does not terminate
the process; update cleanup (the function referenced by the SIGINT/SIGTERM
handlers) to call process.exit(0) (or process.exit(1) on error) after clearing
the interval and logging so the command exits immediately; ensure you reference
this.pollInterval and the same cleanup function used by the signal handlers so
no dangling Promise keeps the process alive.

61-66: Avoid mutating the input flags object.

Directly modifying flags.unit mutates the input parameter, which can lead to unexpected behavior if the flags object is used elsewhere. Consider using a local variable instead.

♻️ Suggested fix using a local variable
   protected async runStats(
     flags: BaseFlags,
     controlApi: ControlApi,
   ): Promise<void> {
+    let unit = flags.unit as string;
     if (flags.live && flags.unit !== "minute") {
       this.warn(
         "Live stats only support minute intervals. Using minute interval.",
       );
-      flags.unit = "minute";
+      unit = "minute";
     }

     await this.showAuthInfoIfNeeded(flags);

     this.statsDisplay = new StatsDisplay({
       intervalSeconds: flags.interval as number,
       json: this.shouldOutputJson(flags),
       live: flags.live as boolean,
       startTime: flags.live ? new Date() : undefined,
-      unit: flags.unit as "day" | "hour" | "minute" | "month",
+      unit: unit as "day" | "hour" | "minute" | "month",
       ...this.getStatsDisplayOptions(),
     });

Note: You'll also need to pass unit to the fetch methods or store it as instance state if used elsewhere.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/stats-base-command.ts` around lines 61 - 66, Avoid mutating the incoming
flags object: read flags.unit into a local variable (e.g., let unit =
flags.unit), and if (flags.live && unit !== "minute") set unit = "minute" and
call this.warn(...). Replace any later uses of flags.unit with this local unit
(or assign it to instance state like this.unit) and ensure any fetch methods
receive this unit value as an argument rather than reading flags.unit directly.
src/commands/spaces/cursors/subscribe.ts (1)

168-170: Consider removing the empty finally block.

The finally block only contains a comment stating cleanup is handled elsewhere. Since it performs no actual work, it can be safely removed to reduce noise.

♻️ Suggested cleanup
     } catch (error) {
       this.handleCommandError(error, flags, "cursor", { spaceName });
-    } finally {
-      // Cleanup is now handled by base class finally() method
     }
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/spaces/cursors/subscribe.ts` around lines 168 - 170, Remove the
empty finally block in the async block inside the subscribe command (the finally
that only contains the comment "Cleanup is now handled by base class finally()
method"); edit the function/method where this try...finally appears (in
src/commands/spaces/cursors/subscribe.ts, inside the subscribe handler/execute
method) and delete the entire finally clause so only the try (and any catch)
remain, leaving cleanup to the base class.
src/commands/spaces/locations/set.ts (1)

273-275: Consider removing the empty finally block.

Similar to the cursors/subscribe.ts file, this finally block only contains a comment and performs no work.

♻️ Suggested cleanup
     } catch (error) {
       const errorMsg = `Error: ${error instanceof Error ? error.message : String(error)}`;
       this.logCliEvent(flags, "location", "fatalError", errorMsg, {
         error: errorMsg,
       });
       if (this.shouldOutputJson(flags)) {
         this.jsonError({ error: errorMsg, success: false }, flags);
       } else {
         this.error(errorMsg);
       }
-    } finally {
-      // Cleanup is now handled by base class finally() method
     }
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/spaces/locations/set.ts` around lines 273 - 275, Remove the
empty finally block in src/commands/spaces/locations/set.ts (the try...finally
that only contains the comment "// Cleanup is now handled by base class
finally() method"); delete the entire finally { ... } block so there is no no-op
finally, or if you want to preserve the note move that comment into the
surrounding function/method's header or above the try so behavior is documented
without an empty block.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/chat-base-command.ts`:
- Around line 91-135: The handler in setupRoomStatusHandler currently suppresses
error output for RoomStatus.Failed when shouldOutputJson(flags) is true; emit a
JSON-friendly error instead of doing nothing: after you call logCliEvent for the
Failed case (or inside the statusChange.current === RoomStatus.Failed branch),
when shouldOutputJson(flags) is true call this.error(...) or an appropriate JSON
error emission path with the same message used for the human output (e.g.,
`Failed to attach to room ${options.roomName}: ${reasonMsg || "Unknown error"}`)
so JSON consumers receive the failure notification; reference
setupRoomStatusHandler, RoomStatus.Failed, shouldOutputJson, logCliEvent and
this.error to locate where to add the JSON-specific emission.

In `@src/commands/spaces/locations/get-all.ts`:
- Around line 123-124: The code incorrectly destructures the result of
locations.getAll() as `{ items: locationsFromSpace }`; change it to assign the
raw result to locationsFromSpace (e.g., `const locationsFromSpace = await
this.space!.locations.getAll()`), then normalize that value before use to
support both array and object shapes (check Array.isArray(locationsFromSpace)
and, if an object keyed by connectionId, extract/flatten its values) so
downstream checks and iteration work correctly; update any logic using
locationsFromSpace accordingly (see the locations.getAll call and how
subscribe.ts handles the returned shape for guidance).

In `@src/spaces-base-command.ts`:
- Around line 148-156: The waitForConnection() function has a race where it
checks connection.state === "connected" before registering the connection event
handlers, so a fast transition can be missed; fix by registering the same
listeners currently added later (the "connected" and error/timeout handlers used
around the listener registration block) at the start of waitForConnection()
(before checking connection.state), then immediately check connection.state and
handle the already-connected case by calling this.logCliEvent(flags,
"connection", "connected", "Realtime connection established.") and returning;
ensure you still remove/cleanup those listeners on resolve/reject to avoid
leaks.

---

Outside diff comments:
In `@src/commands/spaces/cursors/get-all.ts`:
- Around line 168-188: The connection-stabilization Promise currently lives
outside the try/catch so a timeout rejection escapes the getAll() fallback; move
the wait (the block that checks this.realtimeClient!.connection.state and awaits
the Promise with setTimeout/once) inside the same try that calls
this.space!.cursors.getAll() so any timeout rejection is caught and the catch
block can return the existing cursorMap/live snapshot, or alternatively change
the timeout rejection to a non-throwing warning that logs and continues to call
this.space!.cursors.getAll(); touch the symbols
this.realtimeClient!.connection.state, the stabilization Promise,
this.space!.cursors.getAll(), and cursorMap when making the change.

In `@src/commands/spaces/locations/set.ts`:
- Around line 144-155: The catch block currently swallows the thrown error and
emits a JSON payload with success: true; update the catch to capture the error
(e.g., catch (err)) and when this.shouldOutputJson(flags) is true call
this.formatJsonOutput with success: false and include the error details
(err.message and/or err.stack) alongside location and spaceName so E2E consumers
see the real failure status, but still avoid calling this.error() to keep exit
code behavior unchanged.

---

Nitpick comments:
In `@src/commands/rooms/presence/enter.ts`:
- Around line 68-78: The code is pre-stripping surrounding quotes from rawData
before calling parseJsonFlag; move this normalization into parseJsonFlag so all
callers (including this PresenceData flow) get consistent handling—update
parseJsonFlag to trim whitespace and remove a single pair of matching
surrounding single or double quotes before attempting JSON parsing, then remove
the duplicate quote-stripping block in enter.ts (the rawData/trimmed handling
and slice logic) and continue to call parseJsonFlag(trimmed, "data", flags) (or
pass rawData unchanged) so PresenceData parsing uses the centralized behavior.

In `@src/commands/rooms/typing/keystroke.ts`:
- Around line 93-106: Extract the duplicated status-change and Failed handling
by extending setupRoomStatusHandler to accept an options object with an optional
onAttached callback; move the existing logging and Failed->reason logic from
this command's room.onStatusChange handler into setupRoomStatusHandler and, in
this file, call setupRoomStatusHandler(room, flags, { roomName, onAttached:
async () => { await this.keystroke(...) } }) so the custom keystroke behavior
runs on attach while all status logging and error-reason extraction remains
centralized in setupRoomStatusHandler; update references to
onStatusChange/room.error/RoomStatus.Failed accordingly.

In `@src/commands/spaces/cursors/get-all.ts`:
- Around line 47-58: The enter-and-poll connection sequence (the initializeSpace
call with enterSpace:false, the optional progress log via shouldOutputJson, and
awaiting this.space!.enter() plus the 5s poll) is duplicated and should be
extracted into a shared helper on SpacesBaseCommand (e.g., ensureSpaceConnected
or enterAndPollSpace). Add a new method on SpacesBaseCommand that accepts flags
and spaceName, performs initializeSpace(..., { enterSpace:false,
setupConnectionLogging:false }), emits the same progress log when
shouldOutputJson(flags) is false, calls await this.space!.enter(), and runs the
existing 5-second poll/connection-state logic; then replace the duplicated block
in get-all.ts (and the same block in locations/get-all.ts) with a call to that
new helper.

In `@src/commands/spaces/cursors/subscribe.ts`:
- Around line 168-170: Remove the empty finally block in the async block inside
the subscribe command (the finally that only contains the comment "Cleanup is
now handled by base class finally() method"); edit the function/method where
this try...finally appears (in src/commands/spaces/cursors/subscribe.ts, inside
the subscribe handler/execute method) and delete the entire finally clause so
only the try (and any catch) remain, leaving cleanup to the base class.

In `@src/commands/spaces/locations/set.ts`:
- Around line 273-275: Remove the empty finally block in
src/commands/spaces/locations/set.ts (the try...finally that only contains the
comment "// Cleanup is now handled by base class finally() method"); delete the
entire finally { ... } block so there is no no-op finally, or if you want to
preserve the note move that comment into the surrounding function/method's
header or above the try so behavior is documented without an empty block.

In `@src/commands/spaces/locks/get.ts`:
- Around line 39-45: The code calls this.space!.enter() after initializeSpace
even though initializeSpace supports an enterSpace option; remove the explicit
enter call and call initializeSpace with enterSpace: true (e.g., await
this.initializeSpace(flags, spaceName, { enterSpace: true,
setupConnectionLogging: false })) so the helper owns entering the space and the
flow matches other space commands (drop the this.space!.enter() line).

In `@src/commands/spaces/locks/subscribe.ts`:
- Around line 62-66: The progress message "Connecting to space..." is emitted
after await this.initializeSpace(...) so it reports a step that's already
completed; either move the log call to before the await to show the actual
connection step, or change the message after initializeSpace to reflect the new
state (e.g., "Connected to space" or "Entered space")—update the call site that
invokes this.log(progress(resource(...))) in subscribe.ts near initializeSpace,
keeping the guard this.shouldOutputJson(flags) unchanged.

In `@src/commands/spaces/members/enter.ts`:
- Around line 276-285: The catch block in enter.ts currently builds errorMsg,
logs via this.logCliEvent and branches on
this.shouldOutputJson/this.jsonError/this.error; replace that inline handling
with a call to the centralized handler used elsewhere: handleCommandError. In
the catch for the command's main function, pass the command instance (this), the
parsed flags object (flags), and the caught error, and include the same
contextual metadata you were passing to logCliEvent (e.g., { event:
"unhandledError", error: ... } or similar) so logs and JSON output remain
consistent; remove the inline this.logCliEvent/this.jsonError/this.error calls
and rely on handleCommandError to perform consistent logging and output. Ensure
you import/resolve handleCommandError the same way other commands do.

In `@src/stats-base-command.ts`:
- Around line 143-150: The cleanup callback for live stats (cleanup) clears
this.pollInterval and logs but does not terminate the process; update cleanup
(the function referenced by the SIGINT/SIGTERM handlers) to call process.exit(0)
(or process.exit(1) on error) after clearing the interval and logging so the
command exits immediately; ensure you reference this.pollInterval and the same
cleanup function used by the signal handlers so no dangling Promise keeps the
process alive.
- Around line 61-66: Avoid mutating the incoming flags object: read flags.unit
into a local variable (e.g., let unit = flags.unit), and if (flags.live && unit
!== "minute") set unit = "minute" and call this.warn(...). Replace any later
uses of flags.unit with this local unit (or assign it to instance state like
this.unit) and ensure any fetch methods receive this unit value as an argument
rather than reading flags.unit directly.

In `@src/utils/output.ts`:
- Around line 27-34: The formatMessageTimestamp function currently treats 0 as a
valid timestamp; update formatMessageTimestamp so a timestamp value of 0 is
treated like null/undefined (i.e., fall back to current time) by simplifying the
check to if (!timestamp) and keep the rest of the logic (Date instance ->
toISOString, otherwise new Date(timestamp).toISOString()); update any tests or
callers that rely on 0 being preserved.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 3788981e-8864-4c67-a9f5-f79790308154

📥 Commits

Reviewing files that changed from the base of the PR and between 362c05d and 9693562.

📒 Files selected for processing (53)
  • AGENTS.md
  • src/base-command.ts
  • src/chat-base-command.ts
  • src/commands/channels/history.ts
  • src/commands/channels/occupancy/subscribe.ts
  • src/commands/channels/presence/enter.ts
  • src/commands/channels/presence/subscribe.ts
  • src/commands/channels/publish.ts
  • src/commands/channels/subscribe.ts
  • src/commands/logs/channel-lifecycle/subscribe.ts
  • src/commands/logs/connection-lifecycle/history.ts
  • src/commands/logs/connection-lifecycle/subscribe.ts
  • src/commands/logs/history.ts
  • src/commands/logs/push/history.ts
  • src/commands/logs/push/subscribe.ts
  • src/commands/logs/subscribe.ts
  • src/commands/rooms/list.ts
  • src/commands/rooms/messages/history.ts
  • src/commands/rooms/messages/reactions/remove.ts
  • src/commands/rooms/messages/reactions/send.ts
  • src/commands/rooms/messages/reactions/subscribe.ts
  • src/commands/rooms/messages/send.ts
  • src/commands/rooms/messages/subscribe.ts
  • src/commands/rooms/occupancy/get.ts
  • src/commands/rooms/occupancy/subscribe.ts
  • src/commands/rooms/presence/enter.ts
  • src/commands/rooms/presence/subscribe.ts
  • src/commands/rooms/reactions/send.ts
  • src/commands/rooms/reactions/subscribe.ts
  • src/commands/rooms/typing/keystroke.ts
  • src/commands/rooms/typing/subscribe.ts
  • src/commands/spaces/cursors/get-all.ts
  • src/commands/spaces/cursors/set.ts
  • src/commands/spaces/cursors/subscribe.ts
  • src/commands/spaces/locations/get-all.ts
  • src/commands/spaces/locations/set.ts
  • src/commands/spaces/locations/subscribe.ts
  • src/commands/spaces/locks/acquire.ts
  • src/commands/spaces/locks/get-all.ts
  • src/commands/spaces/locks/get.ts
  • src/commands/spaces/locks/subscribe.ts
  • src/commands/spaces/members/enter.ts
  • src/commands/spaces/members/subscribe.ts
  • src/commands/stats/account.ts
  • src/commands/stats/app.ts
  • src/flags.ts
  • src/spaces-base-command.ts
  • src/stats-base-command.ts
  • src/utils/history.ts
  • src/utils/message.ts
  • src/utils/output.ts
  • test/e2e/rooms/rooms-e2e.test.ts
  • test/unit/commands/logs/connection-lifecycle/subscribe.test.ts

@umair-ably umair-ably force-pushed the DX-928-repo-update branch from 1c42c11 to 2d57732 Compare March 6, 2026 13:22
@umair-ably umair-ably requested a review from sacOO7 March 6, 2026 13:35
Base automatically changed from DX-928-repo-update to main March 6, 2026 13:36
Copy link
Contributor

@sacOO7 sacOO7 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, seems we have addressed duplicate validation snippets, error handling and other parts of the code.
I will still try to cross-check

  1. code patterns, snippets, or logic that are duplicated or very similar across the codebase.
  2. Determine whether these can be refactored into reusable utilities, shared helpers, or extracted into common methods within the same class hierarchy or parent classes.
  3. Focus especially on areas such as validation logic, error handling, data transformations, conditional patterns, and repeated service or repository interactions.

Copy link
Contributor

@sacOO7 sacOO7 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for addressing feedback : )
LGTM !

@umair-ably umair-ably merged commit cef3e9b into main Mar 6, 2026
10 checks passed
@umair-ably umair-ably deleted the DX-792-refactor branch March 6, 2026 15:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants