Skip to content

refactor: port extension and auth commands to libswamp pattern#828

Merged
stack72 merged 1 commit intomainfrom
default
Mar 22, 2026
Merged

refactor: port extension and auth commands to libswamp pattern#828
stack72 merged 1 commit intomainfrom
default

Conversation

@stack72
Copy link
Copy Markdown
Contributor

@stack72 stack72 commented Mar 22, 2026

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) + extensionPush generator (three-phase push); extended renderer with renderResolved, renderSafetyWarnings, renderDryRun, etc.
  • extension update — streaming generator yielding per-extension progress; installExtension injected from CLI layer to avoid importing from extension_pull.ts
  • extension yank — confirmation-guarded with preview/execute pattern matching vault put
  • extension fmt — renderer exposes passed()/failureMessage() for CLI post-stream error throwing

Each command follows the established pattern:

Layer Location Responsibility
Generator src/libswamp/{auth,extensions}/*.ts Domain orchestration via deps injection
Renderer src/presentation/renderers/*.ts Log + JSON output matching old shapes
CLI src/cli/commands/*.ts Pure orchestration: wire deps → renderer → consumeStream()

Known trade-offs

These were reviewed and accepted as consistent with existing codebase patterns:

  1. auth/login.ts I/O helpers at module scopereadLine and readPassword use raw Deno.stdin/Deno.stdout. They're only called by createAuthLoginDeps(), 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.

  2. extension/fmt.ts factory has inline Deno.Command calls — The createExtensionFmtDeps() factory runs deno fmt and deno lint via new Deno.Command(). The deps interface properly abstracts runFmt/runLint for testability; the raw calls only exist in the factory.

  3. extension/push.ts createArchive uses direct Deno.* 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):

  • Generators: src/libswamp/auth/login.ts, src/libswamp/extensions/{fmt,push,update,yank}.ts
  • Tests: *_test.ts for each generator
  • Renderers: src/presentation/renderers/{auth_login,extension_fmt,extension_push,extension_update,extension_yank}.ts

Modified (6):

  • CLI commands refactored to pure orchestration
  • src/libswamp/mod.ts — all new types and functions exported

Deleted (9):

  • Old src/presentation/output/ files and their tests, replaced by renderers

Test plan

  • deno check — 0 type errors
  • deno lint — 813 files clean
  • deno fmt --check — 896 files clean
  • deno run test — 3481 passed, 0 failed
  • deno run compile — binary compiles successfully
  • License headers verified on all new files

Partially addresses #739 (batch 8: extension + auth commands).

🤖 Generated with Claude Code

github-actions[bot]

This comment was marked as outdated.

github-actions[bot]

This comment was marked as outdated.

github-actions[bot]

This comment was marked as outdated.

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]>
Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

CLI UX Review

Blocking

None.

Suggestions

  1. auth_login renderer: console.log in browser_open_failed handler
    src/presentation/renderers/auth_login.ts:173
    The browser_open_failed handler calls console.log(e.message) directly while all other event handlers use the writeOutput helper or the Spinner. Minor inconsistency — should use writeOutput for uniform output routing.

  2. auth_login JSON output omits name and email
    src/presentation/renderers/auth_login.ts:216–224
    Log mode shows Name and Email when 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 optional name/email fields (the API key is correctly excluded).

  3. extension_push --json produces two sequential JSON objects
    src/presentation/renderers/extension_push.ts:244–295
    A successful push emits two separate JSON blobs to stdout: first from renderResolved (resolved data), then from the completed handler (push result). --dry-run --json similarly emits renderResolved + renderDryRun. If this is the intended streaming/NDJSON pattern (consistent with extension_update which also emits multiple objects), no change needed. If not, consider a single top-level JSON envelope.

  4. extension_update --json double-outputs on extension_not_installed
    src/presentation/renderers/extension_update.ts:69–74
    In JSON mode, the extension_not_installed handler emits { error: "..." } to stdout, and then the error event 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. The extension_not_installed JSON handler could be a no-op and let the error event 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.

Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

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

  1. Duck-typed isSwampError guard in extension_push.ts:415-418 — The check "code" in error && "message" in error would match any Error subclass with a code property (e.g., Node-style errors). Consider adding a discriminant field to SwampError (e.g., readonly _tag: "SwampError") or exporting isSwampError from src/libswamp/errors.ts so all CLI commands use a canonical guard. The same duck-typing appears in extension_yank.ts:87 with a slightly different pattern.

  2. as casts on error.details in extension_push.ts:280-310 — The catch block dispatches on details?.safetyErrors, details?.qualityErrors, etc. using as casts from unknown. This works but is fragile if a new error detail key is added without updating this block. A discriminated union on SwampError.code (matching the error code to its expected details shape) would be more type-safe. Low priority since this is CLI-layer glue code.

  3. Duplicate resolveServerUrl() functionspush.ts, update.ts, and yank.ts in src/libswamp/extensions/ each define their own resolveServerUrl() with identical logic. Could be extracted to a shared infrastructure helper. Not blocking since this matches the pre-existing pattern.

  4. No renderer tests for the new renderers — The old src/presentation/output/*_test.ts files are deleted but no replacement tests exist for the new renderer classes in src/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.

Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

Adversarial Review

Critical / High

None found.

Medium

  1. src/libswamp/extensions/yank.ts:140-146 — Uncaught exception from deps.yankExtension() bypasses renderer error handling.
    The extensionYank generator awaits deps.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 through consumeStream, bypassing the renderer's error handler entirely. In --json mode, 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 the deps.yankExtension() call in try/catch and yield { kind: "error", error: validationFailed(...) }, matching the pattern used in extensionPush generator (lines 660-674, 679-689, 694-709).

  2. src/cli/commands/extension_push.ts:415-419isSwampError duck-type guard is overly permissive.
    The guard checks "code" in error && "message" in error. Since SwampError is 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 from deps.fetchCollectives or deps.extractContentMetadata matches this guard, the code enters the SwampError handling branch, tries to inspect details, finds nothing, and falls through to throw new UserError(error.message) — which is a reasonable fallback. However, the details branching logic (safetyErrors, qualityErrors, etc.) could accidentally fire if an unrelated error object happens to have a matching details key.
    Suggested fix: Add a discriminant field (e.g., _tag: "SwampError") to the SwampError interface and check for it, or use instanceof with a class.

  3. src/libswamp/auth/login.ts:227-231server.shutdown() error in finally masks the original server.token rejection.
    If server.token rejects (e.g., auth timeout) AND server.shutdown() also throws (e.g., the server is already closed), the finally block's error replaces the original rejection. The user sees a cryptic "server already closed" error instead of "authentication timed out".
    Suggested fix: Wrap server.shutdown() in a try/catch within the finally block, similar to the createArchive cleanup pattern at push.ts:1052-1056.

  4. src/presentation/renderers/extension_update.ts:79-86 — JSON renderer emits multiple top-level JSON objects in the updating + completed sequence.
    The updating handler emits {"status":"updating",...} for each extension, then completed emits the full result. A consumer doing JSON.parse(stdout) would fail on multi-extension updates. This differs from other renderers that emit a single JSON document.
    Suggested fix: Either suppress the updating handler in JSON mode (emit nothing, let completed be the sole output) or collect into an array/JSONL explicitly.

Low

  1. src/cli/commands/extension_yank.ts:87 — Weaker duck-type check: "code" in (error as Record<string, unknown>).
    This only checks for code (not message), making it even more permissive than the push guard. Any thrown object with a code property would be treated as a SwampError. Same suggested fix as #2.

  2. src/libswamp/extensions/push.ts:1020-1023 — Additional files with the same basename silently overwrite each other in the archive.
    basename(af) is used as the destination, so foo/README.md and bar/README.md would 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.

@stack72 stack72 merged commit f36011e into main Mar 22, 2026
9 checks passed
@stack72 stack72 deleted the default branch March 22, 2026 22:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant