Conversation
Port 5 commands from batch 8 of #739 to the libswamp generator + renderer architecture: - `auth login` — browser/stdin flows with I/O injected through deps - `extension push` — two-phase (prepare + execute) with extended renderer - `extension update` — streaming generator with installExtension injected - `extension yank` — confirmation-guarded with preview/execute pattern - `extension fmt` — renderer exposes passed()/failureMessage() for CLI Each command now follows the established pattern: - Generator in src/libswamp/ with typed events and deps injection - Log + JSON renderers in src/presentation/renderers/ - CLI reduced to pure orchestration: wire deps → renderer → consumeStream Old output files in src/presentation/output/ deleted and replaced by renderers. All types exported from src/libswamp/mod.ts. Known trade-offs accepted during review: - auth/login.ts has readLine/readPassword I/O helpers at module scope (only used by createAuthLoginDeps factory, not by the generator) - extension/fmt.ts factory contains inline Deno.Command calls for deno fmt/lint (abstracted behind deps interface for testability) - extension/push.ts createArchive helper uses ~20 direct Deno.* calls for temp dir, file copy, and tar (moved verbatim from old CLI code; abstracting behind deps is future work) Partially addresses #739 (batch 8: extension + auth commands). Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
There was a problem hiding this comment.
CLI UX Review
Blocking
None.
Suggestions
-
auth_loginrenderer:console.loginbrowser_open_failedhandler
src/presentation/renderers/auth_login.ts:173
Thebrowser_open_failedhandler callsconsole.log(e.message)directly while all other event handlers use thewriteOutputhelper or the Spinner. Minor inconsistency — should usewriteOutputfor uniform output routing. -
auth_loginJSON output omitsnameandemail
src/presentation/renderers/auth_login.ts:216–224
Log mode showsNameandEmailwhen available; JSON mode only emits{ authenticated, serverUrl, username }. Scripts querying user identity can't get the same info without switching to log mode. Consider including optionalname/emailfields (the API key is correctly excluded). -
extension_push --jsonproduces two sequential JSON objects
src/presentation/renderers/extension_push.ts:244–295
A successful push emits two separate JSON blobs to stdout: first fromrenderResolved(resolved data), then from thecompletedhandler (push result).--dry-run --jsonsimilarly emitsrenderResolved+renderDryRun. If this is the intended streaming/NDJSON pattern (consistent withextension_updatewhich also emits multiple objects), no change needed. If not, consider a single top-level JSON envelope. -
extension_update --jsondouble-outputs onextension_not_installed
src/presentation/renderers/extension_update.ts:69–74
In JSON mode, theextension_not_installedhandler emits{ error: "..." }to stdout, and then theerrorevent handler immediately throws a UserError with a similar message. A script sees a JSON blob AND a thrown error — two outputs for one failure condition. Theextension_not_installedJSON handler could be a no-op and let theerrorevent carry the structured failure.
Verdict
PASS — this is a clean internal refactor. Command flags, help text, error messages, and log/JSON output shapes are preserved or improved. No blocking UX regressions.
There was a problem hiding this comment.
Code Review
Well-structured refactoring that consistently ports 5 commands to the established generator + renderer + CLI orchestration pattern. The separation of concerns is clean: generators own domain orchestration with deps injection for testability, renderers handle log/json output, and CLI commands are reduced to pure wiring.
Blocking Issues
None.
Suggestions
-
Duck-typed
isSwampErrorguard inextension_push.ts:415-418— The check"code" in error && "message" in errorwould match anyErrorsubclass with acodeproperty (e.g., Node-style errors). Consider adding a discriminant field toSwampError(e.g.,readonly _tag: "SwampError") or exportingisSwampErrorfromsrc/libswamp/errors.tsso all CLI commands use a canonical guard. The same duck-typing appears inextension_yank.ts:87with a slightly different pattern. -
ascasts onerror.detailsinextension_push.ts:280-310— The catch block dispatches ondetails?.safetyErrors,details?.qualityErrors, etc. usingascasts fromunknown. This works but is fragile if a new error detail key is added without updating this block. A discriminated union onSwampError.code(matching the error code to its expected details shape) would be more type-safe. Low priority since this is CLI-layer glue code. -
Duplicate
resolveServerUrl()functions —push.ts,update.ts, andyank.tsinsrc/libswamp/extensions/each define their ownresolveServerUrl()with identical logic. Could be extracted to a shared infrastructure helper. Not blocking since this matches the pre-existing pattern. -
No renderer tests for the new renderers — The old
src/presentation/output/*_test.tsfiles are deleted but no replacement tests exist for the new renderer classes insrc/presentation/renderers/. The generators are well-tested, and the renderers are thin, but renderer tests would catch regressions in JSON output shape (which external consumers may depend on). Worth adding in a follow-up.
DDD Assessment
The generator+deps pattern aligns well with the Application Service building block — each generator orchestrates domain objects for a use case, with infrastructure injected through the deps interface. The two-phase pattern for extensionPush (plain async prepare + generator execute) is a reasonable split given the interactive prompts between phases. Domain types (ExtensionManifest, SafetyCheckResult, QualityCheckResult) remain in the domain layer and are not duplicated.
There was a problem hiding this comment.
Adversarial Review
Critical / High
None found.
Medium
-
src/libswamp/extensions/yank.ts:140-146— Uncaught exception fromdeps.yankExtension()bypasses renderer error handling.
TheextensionYankgenerator awaitsdeps.yankExtension()without a try/catch. If the API call fails (network timeout, 403 Forbidden, 500 Server Error), the exception propagates out of the generator and throughconsumeStream, bypassing the renderer'serrorhandler entirely. In--jsonmode, consumers would get a raw error message instead of structured JSON output.
Breaking example: Server returns 403 during yank → CLI outputs an unstructured stack trace instead of{"error": "..."}.
Suggested fix: Wrap thedeps.yankExtension()call in try/catch and yield{ kind: "error", error: validationFailed(...) }, matching the pattern used inextensionPushgenerator (lines 660-674, 679-689, 694-709). -
src/cli/commands/extension_push.ts:415-419—isSwampErrorduck-type guard is overly permissive.
The guard checks"code" in error && "message" in error. SinceSwampErroris a plain object (not an Error subclass), this would also match any non-Error object that happens to have both properties — e.g., HTTP client error objects from third-party libraries that include{ code: "ECONNRESET", message: "..." }. If a network error fromdeps.fetchCollectivesordeps.extractContentMetadatamatches this guard, the code enters the SwampError handling branch, tries to inspectdetails, finds nothing, and falls through tothrow new UserError(error.message)— which is a reasonable fallback. However, thedetailsbranching logic (safetyErrors,qualityErrors, etc.) could accidentally fire if an unrelated error object happens to have a matchingdetailskey.
Suggested fix: Add a discriminant field (e.g.,_tag: "SwampError") to theSwampErrorinterface and check for it, or useinstanceofwith a class. -
src/libswamp/auth/login.ts:227-231—server.shutdown()error infinallymasks the originalserver.tokenrejection.
Ifserver.tokenrejects (e.g., auth timeout) ANDserver.shutdown()also throws (e.g., the server is already closed), thefinallyblock's error replaces the original rejection. The user sees a cryptic "server already closed" error instead of "authentication timed out".
Suggested fix: Wrapserver.shutdown()in a try/catch within the finally block, similar to thecreateArchivecleanup pattern at push.ts:1052-1056. -
src/presentation/renderers/extension_update.ts:79-86— JSON renderer emits multiple top-level JSON objects in theupdating+completedsequence.
Theupdatinghandler emits{"status":"updating",...}for each extension, thencompletedemits the full result. A consumer doingJSON.parse(stdout)would fail on multi-extension updates. This differs from other renderers that emit a single JSON document.
Suggested fix: Either suppress theupdatinghandler in JSON mode (emit nothing, letcompletedbe the sole output) or collect into an array/JSONL explicitly.
Low
-
src/cli/commands/extension_yank.ts:87— Weaker duck-type check:"code" in (error as Record<string, unknown>).
This only checks forcode(notmessage), making it even more permissive than the push guard. Any thrown object with acodeproperty would be treated as a SwampError. Same suggested fix as #2. -
src/libswamp/extensions/push.ts:1020-1023— Additional files with the samebasenamesilently overwrite each other in the archive.
basename(af)is used as the destination, sofoo/README.mdandbar/README.mdwould collide. This is carried over from the original code and not a regression, but worth noting.
Verdict
PASS — This is a clean refactoring that correctly ports five commands to the libswamp generator/renderer pattern. The code is well-structured with proper dependency injection and good test coverage. The medium findings are edge cases in error handling paths, not logic errors in happy paths. None warrant blocking the merge.
Summary
Ports 5 commands from batch 8 of #739 to the libswamp generator + renderer architecture:
auth login— browser/stdin flows with I/O injected through deps; events for each phase (opening_browser, device_verification, waiting_for_auth, securing_session)extension push— two-phase pattern:extensionPushPrepare(plain async, throws SwampError) +extensionPushgenerator (three-phase push); extended renderer withrenderResolved,renderSafetyWarnings,renderDryRun, etc.extension update— streaming generator yielding per-extension progress;installExtensioninjected from CLI layer to avoid importing fromextension_pull.tsextension yank— confirmation-guarded with preview/execute pattern matchingvault putextension fmt— renderer exposespassed()/failureMessage()for CLI post-stream error throwingEach command follows the established pattern:
src/libswamp/{auth,extensions}/*.tssrc/presentation/renderers/*.tssrc/cli/commands/*.tsconsumeStream()Known trade-offs
These were reviewed and accepted as consistent with existing codebase patterns:
auth/login.tsI/O helpers at module scope —readLineandreadPassworduse rawDeno.stdin/Deno.stdout. They're only called bycreateAuthLoginDeps(), never by the generator. Moving them to an infrastructure module would be cleaner but matches the existing pattern where deps factories are co-located with generators.extension/fmt.tsfactory has inlineDeno.Commandcalls — ThecreateExtensionFmtDeps()factory runsdeno fmtanddeno lintvianew Deno.Command(). The deps interface properly abstractsrunFmt/runLintfor testability; the raw calls only exist in the factory.extension/push.tscreateArchiveuses directDeno.*calls — ~20 calls for temp dirs, file copying, and tar creation. Moved verbatim from the old CLI code. Abstracting behind deps is future work — the current approach matches how the 875-line command was structured.Changes
New files (15):
src/libswamp/auth/login.ts,src/libswamp/extensions/{fmt,push,update,yank}.ts*_test.tsfor each generatorsrc/presentation/renderers/{auth_login,extension_fmt,extension_push,extension_update,extension_yank}.tsModified (6):
src/libswamp/mod.ts— all new types and functions exportedDeleted (9):
src/presentation/output/files and their tests, replaced by renderersTest plan
deno check— 0 type errorsdeno lint— 813 files cleandeno fmt --check— 896 files cleandeno run test— 3481 passed, 0 faileddeno run compile— binary compiles successfullyPartially addresses #739 (batch 8: extension + auth commands).
🤖 Generated with Claude Code