Add CLI logout, status, ctx commands and update docs#34
Conversation
Implement deferred CLI commands: interactive auth login setup, CSV/JSON import into spreadsheet tabs, and tab data export. Update schema registry, VitePress docs, and skill files. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Design browser-based OAuth login for CLI with JWT session storage in ~/.wafflebase/, workspace context switching, and auth code exchange pattern for secure token handling. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Add Bearer token extraction as a fallback in JwtStrategy so CLI clients can authenticate via Authorization: Bearer <token> header in addition to the wafflebase_session cookie. Cookie still takes priority. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
In-memory store for two short-lived token types used in the CLI login flow: OAuth state tokens (CSRF protection, 5-min TTL) and authorization codes (60-sec TTL). Both are single-use and cleaned up on each create. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Support CLI OAuth login by passing a state token through the GitHub OAuth flow. When mode=cli&port=<port> query params are present, GitHubAuthGuard stores state in CliAuthStore and injects it into Passport's authenticate options. On callback, if the state maps to a CLI session, the backend creates a short-lived auth code and redirects to http://127.0.0.1:<port>/callback?code=<code>. The new POST /auth/cli/exchange endpoint lets the CLI exchange that code for JWT tokens. Co-Authored-By: Claude Opus 4.6 <[email protected]>
When no wafflebase_refresh cookie is present, the endpoint now reads
refreshToken from the request body. If the token came from the body,
it returns JSON { accessToken, refreshToken } instead of setting
cookies, allowing CLI clients to rotate tokens without browser cookies.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Changes default config location from ~/.config/wafflebase/config.yaml to ~/.wafflebase/config.yaml to consolidate all wafflebase state under a single home directory. Adds best-effort migration that copies the old file on first use. Exports getConfigPath() and migrateConfigIfNeeded() for use by the upcoming session store module. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Session store reads/writes ~/.wafflebase/session.json with 0600 permissions. Supports load, save, clear, expiry check, and JWT exp claim decoding. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Add JWT session as a fallback auth method in the CLI. The auth resolution order is: flags/env API key → session JWT → config profile API key → none. The HTTP client now auto-refreshes expired JWT tokens on 401 responses, persisting new tokens to the session file. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Starts local HTTP server, opens browser for GitHub OAuth, exchanges auth code for JWT tokens, fetches user info and workspaces, saves session to ~/.wafflebase/session.json. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Implements Tasks 9 and 10: logout clears the session file, status displays the current auth state (user, server, workspace, expiry), and ctx list/switch lets users view and change the active workspace with exact-ID, exact-name, and ID-prefix matching. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Remove old interactive auth login command. Add login, logout, status, ctx.list, and ctx.switch entries to schema registry. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Replace auth login with login/logout/status/ctx commands in VitePress docs and design doc. Update config path to ~/.wafflebase/. Co-Authored-By: Claude Opus 4.6 <[email protected]>
- Backend: JWT Bearer, CLI auth store, GitHub guard - CLI: config, session, auth, login/logout/status/ctx - Docs and schema registry updated Co-Authored-By: Claude Opus 4.6 <[email protected]>
📝 WalkthroughWalkthroughAdds browser-based GitHub OAuth for the CLI with a local callback server and one-time codes, persistent JWT sessions at ~/.wafflebase/session.json, CLI commands (login, logout, status, ctx, import, export), backend CLI OAuth support (CliAuthStore, GitHubAuthGuard, /auth/cli/exchange), and automatic token refresh in the HTTP client. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant CLI
participant LocalServer as Local Callback Server
participant GitHubOAuth as GitHub OAuth
participant Backend
User->>CLI: wafflebase login
CLI->>LocalServer: start server (random port)
CLI->>Backend: GET /auth/github?mode=cli&port=<port>
Backend->>Backend: GitHubAuthGuard -> CliAuthStore.createState
Backend-->>User: redirect to GitHub (includes state)
User->>GitHubOAuth: authorize
GitHubOAuth->>Backend: callback /auth/github/callback?state=<state>
Backend->>Backend: CliAuthStore.consumeState -> create one-time code
Backend-->>LocalServer: redirect to http://localhost:<port>/callback?code=<code>
LocalServer-->>CLI: return code
CLI->>Backend: POST /auth/cli/exchange { code }
Backend->>Backend: CliAuthStore.consumeCode -> issue JWTs
Backend-->>CLI: { accessToken, refreshToken }
CLI->>Backend: GET /auth/me (Bearer)
Backend-->>CLI: user info
CLI->>Backend: GET /workspaces
Backend-->>CLI: workspace list
CLI->>CLI: save session (~/.wafflebase/session.json)
sequenceDiagram
participant CLI
participant HttpClient as HTTP Client (auto-refresh)
participant Backend
CLI->>HttpClient: API request (uses session.accessToken)
HttpClient->>Backend: request with Authorization: Bearer accessToken
Backend-->>HttpClient: 401 Unauthorized (expired)
HttpClient->>Backend: POST /auth/refresh { refreshToken }
Backend-->>HttpClient: { accessToken, refreshToken }
HttpClient->>HttpClient: update session + saveSession
HttpClient->>Backend: retry original request (new token)
Backend-->>HttpClient: 200 OK
HttpClient-->>CLI: response
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
📝 Coding Plan
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. Comment |
Verification: verify:selfResult: ✅ PASS in 100.0s
Verification: verify:integrationResult: ✅ PASS |
Production API is more useful as the default than localhost for most users. Extracted DEFAULT_SERVER constant to keep all references in sync. Co-Authored-By: Claude Opus 4.6 <[email protected]>
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (12)
packages/cli/test/csv-parse.test.ts (1)
4-33: Good test coverage for parseCsv.The tests cover key scenarios including quoted fields, escaped quotes, and CRLF handling.
Consider adding a test for quoted fields containing newlines, as this is a common CSV edge case:
it('handles quoted fields with embedded newlines', () => { expect(parseCsv('"line1\nline2",b')).toEqual([['line1\nline2', 'b']]); });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/cli/test/csv-parse.test.ts` around lines 4 - 33, Add a test to cover quoted fields that contain embedded newlines for parseCsv: create an it block that calls parseCsv with a CSV string where a field is quoted and includes a newline (e.g. "\"line1\nline2\",b") and assert the parsed output contains that field with the newline preserved (['line1\nline2','b']). Place this new test alongside the other parseCsv tests so parseCsv is exercised for embedded-newline quoted fields.packages/cli/src/util/csv-parse.ts (1)
68-77: Consider logging a warning for invalid cell references.
parseStartRefsilently falls back to{row: 1, col: 1}when the input is invalid. While this provides a safe default, users may not realize their--startargument was malformed. Consider returning the fallback with a console warning, or documenting this behavior clearly.💡 Optional: Add warning for invalid input
export function parseStartRef(ref: string): { row: number; col: number } { const match = ref.match(/^([A-Z]+)(\d+)$/i); - if (!match) return { row: 1, col: 1 }; + if (!match) { + console.warn(`Invalid cell reference "${ref}", defaulting to A1`); + return { row: 1, col: 1 }; + } const letters = match[1].toUpperCase();🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/cli/src/util/csv-parse.ts` around lines 68 - 77, The parseStartRef function currently silently returns the default {row: 1, col: 1} for invalid refs; update parseStartRef to emit a clear warning (e.g., console.warn) when the input ref fails the regex so users know their --start argument was malformed, include the original ref value in the message for debugging, and then continue returning the existing fallback; keep the function name parseStartRef and its return shape unchanged.packages/cli/test/config.test.ts (1)
81-125: Consider cleaning up temp directories after tests.The migration tests create temporary directories in
tmpdir()but don't clean them up. While this is typically fine for CI environments that reset between runs, accumulated test artifacts could clutter local development environments over time.♻️ Proposed cleanup using afterEach
+import { rmSync } from 'node:fs'; + describe('migrateConfigIfNeeded', () => { + const createdDirs: string[] = []; + + afterEach(() => { + for (const dir of createdDirs) { + rmSync(dir, { recursive: true, force: true }); + } + createdDirs.length = 0; + }); + it('copies config from old path to new path when new path does not exist', () => { const base = join(tmpdir(), `wfb-migrate-test-${Date.now()}`); + createdDirs.push(base); // ... rest of test🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/cli/test/config.test.ts` around lines 81 - 125, Tests under the migrateConfigIfNeeded describe block leave temporary directories in the OS tmpdir; add cleanup by storing each test's base temp directory in a scoped variable and adding an afterEach hook that recursively removes that base (using fs.rmSync or fs.rmdirSync with { recursive: true } or try/catch around unlink) so tests do not leave artifacts; reference the existing migrateConfigIfNeeded tests and the tmpdir-based base variables (e.g., base, oldDir, newDir) and ensure the afterEach is tolerant if the directory was never created.packages/cli/src/commands/ctx.ts (1)
10-14: Truncated IDs may cause visual ambiguity.The workspace ID is truncated to 8 characters for display (
ws.id.slice(0, 8)). If workspace IDs are UUIDs, this is typically sufficient for uniqueness, but theswitchcommand accepts the full ID, exact name, or prefix match. Users might try to copy-paste the truncated ID shown inlist, which would then trigger prefix matching rather than exact ID match.This works correctly due to the prefix-match fallback, but consider showing the truncation explicitly (e.g.,
abc12345...) to signal it's abbreviated.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/cli/src/commands/ctx.ts` around lines 10 - 14, The displayed workspace ID is being truncated with ws.id.slice(0, 8) which can mislead users into thinking that shown text is the full ID; update the rendering in the mapping that builds the list (the .map((ws) => { ... }) block) to explicitly indicate truncation when you shorten an ID (e.g., show `${ws.id.slice(0,8)}...` only when ws.id.length > 8), leaving the active marker logic (marker = ws.id === activeId ? '*' : ' ') unchanged so users know the ID is abbreviated.packages/cli/src/config/session.ts (1)
59-68: Consider atomic write to prevent session file corruption.The current implementation writes directly to the session file. If the process is interrupted during
writeFileSync, the file could be left in a corrupted or partial state. This is a common issue with credential files that could leave users unable to authenticate.A safer pattern is to write to a temporary file first, then atomically rename it to the target path.
♻️ Proposed atomic write pattern
+import { renameSync } from 'node:fs'; + export function saveSession( session: Session, path: string = getSessionPath(), ): void { const dir = dirname(path); mkdirSync(dir, { recursive: true }); - writeFileSync(path, JSON.stringify(session, null, 2), { mode: 0o600 }); - // Explicitly chmod in case the file already existed with different permissions - chmodSync(path, 0o600); + const tempPath = `${path}.tmp`; + writeFileSync(tempPath, JSON.stringify(session, null, 2), { mode: 0o600 }); + renameSync(tempPath, path); + // Explicitly chmod in case rename preserved different permissions + chmodSync(path, 0o600); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/cli/src/config/session.ts` around lines 59 - 68, The saveSession function currently writes directly to the target path and can corrupt the session file if interrupted; change it to perform an atomic write by writing JSON to a temporary file in the same directory (use dirname(getSessionPath()) or dirname(path)), fs.writeFileSync the temp file with mode 0o600, fs.renameSync the temp file to the target path to atomically replace it, then optionally chmodSync the final path to 0o600; also ensure mkdirSync(dir, { recursive: true }) runs before creating the temp and clean up the temp file with unlinkSync on error to avoid leaving cruft.packages/cli/src/commands/import.ts (1)
51-55: Inconsistent header handling for JSON array-of-objects.When
--no-headeris passed for CSV, the first row is treated as data. However, for JSON array-of-objects, a header row is always synthesized from object keys (line 55), ignoring thelocalOpts.headerflag. This could be intentional (objects inherently define their structure), but may surprise users expecting consistent behavior.Consider documenting this behavior or applying the header flag consistently.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/cli/src/commands/import.ts` around lines 51 - 55, The JSON array-of-objects branch always synthesizes a header row (vars objs, keys, rows) which ignores the CSV header flag; update the logic in import.ts where parsed is an object array so it checks localOpts.header and only prepends keys when header is true, otherwise convert all objects to value rows (e.g., map each object to keys from the first object but do not add the keys row) so behavior matches the --no-header option; alternatively add a clear comment documenting the intentional difference if you prefer to keep current behavior.docs/tasks/active/20260312-cli-todo.md (1)
61-68: Consider archiving this task file.All phases and verification items are marked complete. Based on learnings, completed tasks should be archived with
pnpm tasks:archive && pnpm tasks:indexafterpnpm verify:fastpasses.Note: The PR description mentions some manual E2E items are not all completed. If those need tracking, consider adding them to this file before archiving, or create a follow-up task.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/tasks/active/20260312-cli-todo.md` around lines 61 - 68, This task file has all verification items checked and should be archived: first ensure the suite passes locally by running pnpm verify:fast, then archive the task with pnpm tasks:archive && pnpm tasks:index; before archiving, if there are outstanding manual E2E checks referenced in the PR description, add them as checklist items to this task file (or create a follow-up task) so those items remain tracked prior to running the archive/index commands.packages/backend/src/auth/github.strategy.ts (1)
23-30: Consider type-safe access to__cliStateToken.The
(req as any).__cliStateTokencast works but loses type safety. SinceGitHubAuthGuardsets this property, consider augmenting the ExpressRequesttype to include it.🔧 Add type declaration for `__cliStateToken`
Create or extend a declaration file (e.g.,
packages/backend/src/types/express.d.ts):declare global { namespace Express { interface Request { __cliStateToken?: string; } } } export {};Then update the access:
- const cliState = (req as any).__cliStateToken as string | undefined; + const cliState = req.__cliStateToken;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/backend/src/auth/github.strategy.ts` around lines 23 - 30, Add a proper type declaration for Express.Request to include the optional __cliStateToken (e.g., create an ambient declaration that extends Express.Request with __cliStateToken?: string and export {}), then remove the unsafe cast in authenticate and access the property as req.__cliStateToken; update authenticate(req: Request, ...) to use that typed property (referencing the authenticate method and GitHubAuthGuard usage) so the code is type-safe without (req as any).packages/backend/src/auth/cli-auth.store.ts (1)
66-70: Consider rate-limiting or bounding the maps for production resilience.The
cleanup()method runs on everycreateState/createCodecall, iterating all entries. Under high load or a DoS attack, the maps could grow faster than cleanup removes entries. For production, consider:
- A maximum size limit with oldest-entry eviction
- Periodic cleanup on a timer rather than synchronous iteration
This is a nice-to-have for CLI auth which typically has low concurrency.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/backend/src/auth/cli-auth.store.ts` around lines 66 - 70, The cleanup() method currently iterates the entire states and codes Maps on every createState/createCode call which can cause unbounded growth under load; change the strategy by (1) introducing configurable MAX_STATES and MAX_CODES constants and evicting oldest entries when insertion would exceed those limits (use Map insertion order or maintain a small FIFO/linked list to identify oldest) inside createState/createCode rather than full cleanup, and (2) move full-scan expiry removal to a periodic async timer (setInterval) that runs off the hot path and expose a stop/teardown to clear the timer; update references to cleanup(), createState, createCode, states and codes to implement these limits and the periodic cleaner.docs/tasks/active/20260315-cli-oauth-login-todo.md (1)
519-524: Minor: Add language specifier to fenced code block.The output example block lacks a language specifier. While this doesn't affect functionality, it improves documentation consistency and syntax highlighting.
📝 Suggested fix
Output format: -``` +```text Logged in as hackerwins ([email protected]) Server: http://localhost:3000 Workspace: hackerwins's Workspace (e98ff707-...) Session: valid (expires 2026-03-15T10:00:00Z)</details> <details> <summary>🤖 Prompt for AI Agents</summary>Verify each finding against the current code and only fix it if needed.
In
@docs/tasks/active/20260315-cli-oauth-login-todo.mdaround lines 519 - 524,
The fenced code block showing the CLI login output is missing a language
specifier; update the triple-backtick fence for the example output in the
docs/tasks/active/20260315-cli-oauth-login-todo.md file to include a language
(e.g., add "text" after), so the block becomestext to enable consistent
syntax highlighting for the Logged in as... Server... Workspace... Session
output example.</details> </blockquote></details> <details> <summary>packages/backend/src/auth/auth.controller.ts (2)</summary><blockquote> `52-62`: **Consider underscore-prefix for unused parameters.** The `void` statements work to suppress unused variable warnings, but underscore-prefixing is more idiomatic in TypeScript: ```typescript async githubAuth( `@Query`('mode') _mode: string | undefined, `@Query`('port') _port: string | undefined, `@Req`() _req: Request, ) { // Guard handles CLI mode detection and injects state token } ``` This makes intent clearer at declaration rather than requiring statements in the body. <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against the current code and only fix it if needed. In `@packages/backend/src/auth/auth.controller.ts` around lines 52 - 62, Rename the unused parameters in the async githubAuth method to use an underscore prefix (e.g., change mode, port, req to _mode, _port, _req) and remove the trailing void statements in the function body; update the function signature in auth.controller.ts (githubAuth) accordingly so the intent is clear at declaration and the body no longer contains the unused-variable suppressions. ``` </details> --- `132-138`: **Consider adding a comment to clarify the dual-source token logic.** The cookie-priority logic is correct but non-obvious to future readers. A brief comment would help: ```diff + // Support both cookie-based (web) and body-based (CLI) refresh flows. + // Cookie takes priority if present; CLI clients send token in body. const cookieToken = req.cookies?.[REFRESH_COOKIE_NAME]; const bodyToken = typeof req.body?.refreshToken === 'string' ? req.body.refreshToken : undefined; const fromBody = !cookieToken && !!bodyToken; const refreshToken = cookieToken ?? bodyToken; ``` As per coding guidelines: "Write clear comments explaining the 'why' behind complex logic, not just the 'what'". <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against the current code and only fix it if needed. In `@packages/backend/src/auth/auth.controller.ts` around lines 132 - 138, Add a brief clarifying comment above the dual-source token logic that explains why cookieToken takes precedence over req.body.refreshToken and what fromBody represents; specifically annotate the block using the symbols REFRESH_COOKIE_NAME, cookieToken, bodyToken, fromBody, and refreshToken to state that cookies are preferred for refresh tokens for security/consistency and fromBody is true only when no cookie is present and a string body token exists. Ensure the comment explains the intended precedence and purpose (not just restating the code). ``` </details> </blockquote></details> </blockquote></details> <details> <summary>🤖 Prompt for all review comments with AI agents</summary>Verify each finding against the current code and only fix it if needed.
Inline comments:
In@packages/backend/src/auth/auth.controller.ts:
- Around line 86-98: When a stateToken is present but
this.cliAuthStore.consumeState(stateToken) returns undefined the code silently
falls through to the web flow; change the logic so that when stateToken is
provided and consumeState returns undefined you throw a BadRequestException
(e.g., "Invalid or expired CLI state") to surface the error to the CLI flow;
also simplify the branch for state.mode === 'cli' by removing the redundant port
range check (since GitHubAuthGuard already validates port) and proceed to
generate the code via this.cliAuthStore.createCode(user.id) and redirect to the
127.0.0.1:{port}/callback URL as before.In
@packages/cli/package.json:
- Line 23: Add an "engines" field to the CLI package manifest to require Node.js
=20 so users cannot install with incompatible runtimes; update
packages/cli/package.json by inserting an "engines" object (e.g., "node":
">=20") at the top-level of the JSON to enforce the minimum Node version and
prevent runtime errors from the "open" dependency v11.0.0.In
@packages/cli/src/client/http-client.ts:
- Around line 70-77: The token refresh path updates in-memory tokens but skips
persistence when loadSession() returns null, causing silent loss; update the
refresh logic (around loadSession/saveSession in http-client.ts) to handle a
missing session file by either creating and saving a new session object with the
refreshed accessToken/refreshToken/expiresAt (use decodeJwtExpiry) or by logging
a warning and returning false to signal persistence failure; ensure you
reference loadSession(), saveSession(), and decodeJwtExpiry() and that the
function's return value reflects whether persistence succeeded.In
@packages/cli/src/commands/export.ts:
- Around line 9-14: The detectFormat function currently casts any formatFlag to
'csv'|'json' without validation, so invalid values (e.g., 'xml') silently become
treated as JSON; update detectFormat to validate formatFlag explicitly: check if
formatFlag equals 'csv' or 'json' (case-insensitive if desired) and return it
only then, otherwise fall back to extension-based detection or throw/return a
default and surface an error. Locate the detectFormat function and the
formatFlag parameter to implement this guard and ensure callers (e.g., where fmt
is compared to 'csv') receive only valid values.In
@packages/cli/src/commands/import.ts:
- Around line 9-14: The function detectFormat currently unsafely casts the
user-provided formatFlag to 'csv'|'json'; update detectFormat to explicitly
validate formatFlag (check if formatFlag === 'csv' or formatFlag === 'json')
before returning it, otherwise ignore it (or throw a clear error) and fall back
to extname(file) detection; reference the detectFormat function and the
formatFlag parameter and ensure any error message mentions the invalid format
and accepted values so callers/users know to pass 'csv' or 'json'.In
@packages/cli/src/commands/login.ts:
- Around line 90-98: The workspace fetch currently fails silently and leaves
workspaces empty; wrap the fetch call used to populate workspaces (the wsRes
fetch to${server}/workspacesusing tokens.accessToken) in a try/catch and,
when wsRes.ok is false or an exception is thrown, emit a clear warning (e.g.,
console.warn or the CLI logger) that includes the HTTP status and response text
or the caught error message so users know the fetch failed rather than
legitimately having no workspaces; keep the existing behavior of defaulting
workspaces to [] after logging the warning.
Nitpick comments:
In@docs/tasks/active/20260312-cli-todo.md:
- Around line 61-68: This task file has all verification items checked and
should be archived: first ensure the suite passes locally by running pnpm
verify:fast, then archive the task with pnpm tasks:archive && pnpm tasks:index;
before archiving, if there are outstanding manual E2E checks referenced in the
PR description, add them as checklist items to this task file (or create a
follow-up task) so those items remain tracked prior to running the archive/index
commands.In
@docs/tasks/active/20260315-cli-oauth-login-todo.md:
- Around line 519-524: The fenced code block showing the CLI login output is
missing a language specifier; update the triple-backtick fence for the example
output in the docs/tasks/active/20260315-cli-oauth-login-todo.md file to include
a language (e.g., add "text" after), so the block becomestext to enable
consistent syntax highlighting for the Logged in as... Server... Workspace...
Session output example.In
@packages/backend/src/auth/auth.controller.ts:
- Around line 52-62: Rename the unused parameters in the async githubAuth method
to use an underscore prefix (e.g., change mode, port, req to _mode, _port, _req)
and remove the trailing void statements in the function body; update the
function signature in auth.controller.ts (githubAuth) accordingly so the intent
is clear at declaration and the body no longer contains the unused-variable
suppressions.- Around line 132-138: Add a brief clarifying comment above the dual-source
token logic that explains why cookieToken takes precedence over
req.body.refreshToken and what fromBody represents; specifically annotate the
block using the symbols REFRESH_COOKIE_NAME, cookieToken, bodyToken, fromBody,
and refreshToken to state that cookies are preferred for refresh tokens for
security/consistency and fromBody is true only when no cookie is present and a
string body token exists. Ensure the comment explains the intended precedence
and purpose (not just restating the code).In
@packages/backend/src/auth/cli-auth.store.ts:
- Around line 66-70: The cleanup() method currently iterates the entire states
and codes Maps on every createState/createCode call which can cause unbounded
growth under load; change the strategy by (1) introducing configurable
MAX_STATES and MAX_CODES constants and evicting oldest entries when insertion
would exceed those limits (use Map insertion order or maintain a small
FIFO/linked list to identify oldest) inside createState/createCode rather than
full cleanup, and (2) move full-scan expiry removal to a periodic async timer
(setInterval) that runs off the hot path and expose a stop/teardown to clear the
timer; update references to cleanup(), createState, createCode, states and codes
to implement these limits and the periodic cleaner.In
@packages/backend/src/auth/github.strategy.ts:
- Around line 23-30: Add a proper type declaration for Express.Request to
include the optional __cliStateToken (e.g., create an ambient declaration that
extends Express.Request with __cliStateToken?: string and export {}), then
remove the unsafe cast in authenticate and access the property as
req.__cliStateToken; update authenticate(req: Request, ...) to use that typed
property (referencing the authenticate method and GitHubAuthGuard usage) so the
code is type-safe without (req as any).In
@packages/cli/src/commands/ctx.ts:
- Around line 10-14: The displayed workspace ID is being truncated with
ws.id.slice(0, 8) which can mislead users into thinking that shown text is the
full ID; update the rendering in the mapping that builds the list (the .map((ws)
=> { ... }) block) to explicitly indicate truncation when you shorten an ID
(e.g., show${ws.id.slice(0,8)}...only when ws.id.length > 8), leaving the
active marker logic (marker = ws.id === activeId ? '*' : ' ') unchanged so users
know the ID is abbreviated.In
@packages/cli/src/commands/import.ts:
- Around line 51-55: The JSON array-of-objects branch always synthesizes a
header row (vars objs, keys, rows) which ignores the CSV header flag; update the
logic in import.ts where parsed is an object array so it checks localOpts.header
and only prepends keys when header is true, otherwise convert all objects to
value rows (e.g., map each object to keys from the first object but do not add
the keys row) so behavior matches the --no-header option; alternatively add a
clear comment documenting the intentional difference if you prefer to keep
current behavior.In
@packages/cli/src/config/session.ts:
- Around line 59-68: The saveSession function currently writes directly to the
target path and can corrupt the session file if interrupted; change it to
perform an atomic write by writing JSON to a temporary file in the same
directory (use dirname(getSessionPath()) or dirname(path)), fs.writeFileSync the
temp file with mode 0o600, fs.renameSync the temp file to the target path to
atomically replace it, then optionally chmodSync the final path to 0o600; also
ensure mkdirSync(dir, { recursive: true }) runs before creating the temp and
clean up the temp file with unlinkSync on error to avoid leaving cruft.In
@packages/cli/src/util/csv-parse.ts:
- Around line 68-77: The parseStartRef function currently silently returns the
default {row: 1, col: 1} for invalid refs; update parseStartRef to emit a clear
warning (e.g., console.warn) when the input ref fails the regex so users know
their --start argument was malformed, include the original ref value in the
message for debugging, and then continue returning the existing fallback; keep
the function name parseStartRef and its return shape unchanged.In
@packages/cli/test/config.test.ts:
- Around line 81-125: Tests under the migrateConfigIfNeeded describe block leave
temporary directories in the OS tmpdir; add cleanup by storing each test's base
temp directory in a scoped variable and adding an afterEach hook that
recursively removes that base (using fs.rmSync or fs.rmdirSync with { recursive:
true } or try/catch around unlink) so tests do not leave artifacts; reference
the existing migrateConfigIfNeeded tests and the tmpdir-based base variables
(e.g., base, oldDir, newDir) and ensure the afterEach is tolerant if the
directory was never created.In
@packages/cli/test/csv-parse.test.ts:
- Around line 4-33: Add a test to cover quoted fields that contain embedded
newlines for parseCsv: create an it block that calls parseCsv with a CSV string
where a field is quoted and includes a newline (e.g. ""line1\nline2",b") and
assert the parsed output contains that field with the newline preserved
(['line1\nline2','b']). Place this new test alongside the other parseCsv tests
so parseCsv is exercised for embedded-newline quoted fields.</details> --- <details> <summary>ℹ️ Review info</summary> <details> <summary>⚙️ Run configuration</summary> **Configuration used**: defaults **Review profile**: CHILL **Plan**: Pro **Run ID**: `6a6f76f5-b94c-462f-a82e-c48497bcc05f` </details> <details> <summary>📥 Commits</summary> Reviewing files that changed from the base of the PR and between 865e7c142f86194722a9abe2fe381643695411a5 and 66d2a34090b4c6838b079c089fa3ae3a2626f21b. </details> <details> <summary>⛔ Files ignored due to path filters (1)</summary> * `pnpm-lock.yaml` is excluded by `!**/pnpm-lock.yaml` </details> <details> <summary>📒 Files selected for processing (33)</summary> * `docs/design/README.md` * `docs/design/cli-oauth-login.md` * `docs/design/rest-api-and-cli.md` * `docs/tasks/active/20260312-cli-todo.md` * `docs/tasks/active/20260315-cli-oauth-login-todo.md` * `packages/backend/src/auth/auth.controller.spec.ts` * `packages/backend/src/auth/auth.controller.ts` * `packages/backend/src/auth/auth.module.ts` * `packages/backend/src/auth/cli-auth.store.spec.ts` * `packages/backend/src/auth/cli-auth.store.ts` * `packages/backend/src/auth/github-auth.guard.ts` * `packages/backend/src/auth/github.strategy.ts` * `packages/backend/src/auth/jwt.strategy.ts` * `packages/cli/package.json` * `packages/cli/skills/SKILL.md` * `packages/cli/skills/import-export.md` * `packages/cli/src/bin.ts` * `packages/cli/src/client/http-client.ts` * `packages/cli/src/commands/ctx.ts` * `packages/cli/src/commands/export.ts` * `packages/cli/src/commands/import.ts` * `packages/cli/src/commands/login.ts` * `packages/cli/src/commands/logout.ts` * `packages/cli/src/commands/status.ts` * `packages/cli/src/config/config.ts` * `packages/cli/src/config/session.ts` * `packages/cli/src/schema/registry.ts` * `packages/cli/src/util/csv-parse.ts` * `packages/cli/test/config.test.ts` * `packages/cli/test/csv-parse.test.ts` * `packages/cli/test/ctx.test.ts` * `packages/cli/test/session.test.ts` * `packages/docs/api/cli.md` </details> </details> <!-- This is an auto-generated comment by CodeRabbit for review status -->
| // 7. Get workspace list | ||
| const wsRes = await fetch(`${server}/workspaces`, { | ||
| headers: { Authorization: `Bearer ${tokens.accessToken}` }, | ||
| }); | ||
|
|
||
| let workspaces: WorkspaceInfo[] = []; | ||
| if (wsRes.ok) { | ||
| workspaces = (await wsRes.json()) as WorkspaceInfo[]; | ||
| } |
There was a problem hiding this comment.
Silent failure on workspace fetch may confuse users.
If the /workspaces endpoint returns an error (e.g., 500, network issue), the code silently proceeds with an empty array, leading to "No workspaces found." The user may not realize the fetch failed vs. genuinely having no workspaces.
Consider logging a warning when the workspace fetch fails so users can troubleshoot.
🛠️ Suggested improvement
// 7. Get workspace list
const wsRes = await fetch(`${server}/workspaces`, {
headers: { Authorization: `Bearer ${tokens.accessToken}` },
});
let workspaces: WorkspaceInfo[] = [];
if (wsRes.ok) {
workspaces = (await wsRes.json()) as WorkspaceInfo[];
+ } else {
+ console.error('Warning: Failed to fetch workspaces. You may need to set workspace manually.');
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // 7. Get workspace list | |
| const wsRes = await fetch(`${server}/workspaces`, { | |
| headers: { Authorization: `Bearer ${tokens.accessToken}` }, | |
| }); | |
| let workspaces: WorkspaceInfo[] = []; | |
| if (wsRes.ok) { | |
| workspaces = (await wsRes.json()) as WorkspaceInfo[]; | |
| } | |
| // 7. Get workspace list | |
| const wsRes = await fetch(`${server}/workspaces`, { | |
| headers: { Authorization: `Bearer ${tokens.accessToken}` }, | |
| }); | |
| let workspaces: WorkspaceInfo[] = []; | |
| if (wsRes.ok) { | |
| workspaces = (await wsRes.json()) as WorkspaceInfo[]; | |
| } else { | |
| console.error('Warning: Failed to fetch workspaces. You may need to set workspace manually.'); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/cli/src/commands/login.ts` around lines 90 - 98, The workspace fetch
currently fails silently and leaves workspaces empty; wrap the fetch call used
to populate workspaces (the wsRes fetch to `${server}/workspaces` using
tokens.accessToken) in a try/catch and, when wsRes.ok is false or an exception
is thrown, emit a clear warning (e.g., console.warn or the CLI logger) that
includes the HTTP status and response text or the caught error message so users
know the fetch failed rather than legitimately having no workspaces; keep the
existing behavior of defaulting workspaces to [] after logging the warning.
verify:entropy flagged bare backtick filenames like `session.json` as broken file references. Replaced with prose to avoid false positives from the doc-staleness checker. Co-Authored-By: Claude Opus 4.6 <[email protected]>
verify:entropy (doc staleness, dead code, dependency freshness) and builds were only caught in CI. The pre-push hook closes this gap locally. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Browser visual/interaction tests require Docker Chromium and are covered by CI. Pre-push now runs builds, chunk budgets, and entropy checks without browser lane. Co-Authored-By: Claude Opus 4.6 <[email protected]>
verify:self = local (fast + builds + chunks + entropy) verify:ci = CI-only (browser + integration, needs Docker) verify:full = verify:self + verify:ci Pre-push hook runs verify:self. Browser and integration tests move to verify:ci, run only in CI. Co-Authored-By: Claude Opus 4.6 <[email protected]>
- Return error when CLI OAuth state is expired/invalid instead of silently falling through to the web flow - Add format validation for --format flag in export/import commands - Warn when workspace fetch fails during login - Add comment explaining session-missing case in token refresh - Add engines field (node>=20) to CLI package.json for open@11 Co-Authored-By: Claude Opus 4.6 <[email protected]>
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
docs/design/harness-engineering.md (1)
185-206:⚠️ Potential issue | 🟡 MinorUpdate the earlier
verify:selfreferences in this doc too.This section moves browser coverage into
verify:ci, but the summary at Lines 20-23 and the Phase 18a / top-level status sections still say browser lanes are part ofverify:self. Leaving both versions in one document makes the lane contract hard to trust.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/design/harness-engineering.md` around lines 185 - 206, Update the earlier references that still claim browser lanes belong to `pnpm verify:self`: change the summary and any Phase 18a / top-level status text that lists browser coverage under `verify:self` so it no longer mentions browser visual/interaction or Chromium; instead ensure `pnpm verify:self` is described only as the fast runner/builds/chunk budgets/entropy step (matching the table entry) and update `pnpm verify:ci` descriptions to explicitly state it now includes browser visual/interaction and Chromium coverage and produces the CI summary report.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/design/cli-oauth-login.md`:
- Line 36: The markdown lint MD040 warnings are caused by unlabeled fenced code
blocks in docs/design/cli-oauth-login.md; update each backtick fence to include
an appropriate language identifier (e.g., ```text or ```bash) for the blocks
that show the filesystem snippet (starts with " ~/.wafflebase/"), the CLI
examples (blocks containing "wafflebase login", "wafflebase logout", "wafflebase
ctx list", etc.), and the workspace/API-key examples (blocks showing "* e98ff707
hackerwins's Workspace" and the numbered steps). Ensure every opening ``` is
followed by the correct identifier (text or bash) as indicated in the proposed
fix so all occurrences mentioned (lines with the filesystem snippet, CLI
commands, workspace list, and numbered steps) are labeled.
- Line 55: The example JSON contains a real-looking email ("email":
"[email protected]"); replace it with an anonymized placeholder such as
"email": "[email protected]" (or "email": "[email protected]") in the
docs/design/cli-oauth-login.md example so public docs do not expose personal
identifiers.
In `@packages/cli/src/client/http-client.ts`:
- Around line 97-126: The API-key methods (listApiKeys, createApiKey,
revokeApiKey) bypass the centralized JWT refresh/retry logic in request(),
causing failures on expired sessions; either route those methods to call
request() instead of raw fetch or implement a new requestAbsolute() that
duplicates request()'s 401->refreshSession()->retry logic and use it from those
functions; update listApiKeys, createApiKey, revokeApiKey to call the unified
helper (or requestAbsolute) so they inherit the same auto-refresh-and-retry
behavior and return the same { ok, status, data } shape as request().
In `@packages/cli/src/commands/export.ts`:
- Around line 38-40: The current export error turns every non-OK response into a
generic `HTTP ${res.status}`, losing backend error details; update the error
handling after the getClient(...).getCells(docId, localOpts.tab,
localOpts.range) call to parse the response body (e.g., await res.json() or
await res.text()) and, if the parsed body contains an error message (e.g.,
body.error?.message or body.message), include that message in the thrown Error
instead of or alongside `HTTP ${res.status}` so failed exports show the API's
descriptive error.
In `@packages/cli/src/commands/import.ts`:
- Around line 36-70: The CSV path currently ignores the --no-header /
localOpts.header option; update the import action so the CSV parsing respects
localOpts.header by wiring it into parseCsv (or by post-processing the parsed
rows): call parseCsv(raw, { header: localOpts.header }) if parseCsv supports
options, or after rows = parseCsv(raw) treat the first row as a header row only
when localOpts.header === true (and leave it as data when localOpts.header ===
false). Modify the code around parseCsv and use localOpts.header (and the
--no-header option name) to decide whether the first row is header or regular
data.
In `@packages/cli/src/commands/login.ts`:
- Around line 91-136: Before saving the new session, don't overwrite an existing
session's workspace state when the /workspaces fetch fails: call loadSession()
(or retrieve the current saved session) before performing the fetch and, if
wsRes.ok is false, reuse existingSession.workspaces and
existingSession.activeWorkspace instead of leaving workspaces = [] and
activeWorkspace = ''. Update the logic around workspaces, activeWorkspace, and
the final Session object (used by saveSession) so that only a successful
wsRes.json() replaces the saved workspace list; otherwise preserve the previous
values.
In `@packages/cli/src/config/config.ts`:
- Around line 42-44: Replace the console.log migration notices printed during
config resolution with stderr output so they don't pollute machine-readable CLI
stdout; locate the console.log calls in the config resolution/migration code
(the statements that print "[wafflebase] Config migrated from ${oldPath} to
${newPath}" and the similar one around lines 61-63) and change them to write to
stderr (e.g., use console.error or process.stderr.write) so normal CLI stdout
remains untouched for JSON/pipe consumers.
In `@scripts/verify-ci.mjs`:
- Around line 114-126: The current loop short-circuits remaining lanes when the
`failed` flag is true, causing subsequent lanes in `LANES` (e.g.,
`verify:integration`) to be skipped; remove the early-skip logic so every lane
runs regardless of previous failures. Concretely, delete the `if (failed) { ...
continue; }` block (the code that pushes `skipReport` and calls
`writeLaneReport`) and let the normal lane execution flow run for each item in
`LANES`; continue to set `failed` when a lane actually fails and still
push/report each lane's real result into `results` using `writeLaneReport` so
reports reflect actual run outcomes. Ensure `results`, `failed`, `LANES`, and
`writeLaneReport` are used as before but without preemptive skipping.
---
Outside diff comments:
In `@docs/design/harness-engineering.md`:
- Around line 185-206: Update the earlier references that still claim browser
lanes belong to `pnpm verify:self`: change the summary and any Phase 18a /
top-level status text that lists browser coverage under `verify:self` so it no
longer mentions browser visual/interaction or Chromium; instead ensure `pnpm
verify:self` is described only as the fast runner/builds/chunk budgets/entropy
step (matching the table entry) and update `pnpm verify:ci` descriptions to
explicitly state it now includes browser visual/interaction and Chromium
coverage and produces the CI summary report.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 2b302abf-7e39-4a6e-a039-3f2afca8bff7
📒 Files selected for processing (16)
.githooks/pre-pushdocs/design/cli-oauth-login.mddocs/design/harness-engineering.mdpackage.jsonpackages/backend/src/auth/auth.controller.tspackages/cli/package.jsonpackages/cli/src/client/http-client.tspackages/cli/src/commands/export.tspackages/cli/src/commands/import.tspackages/cli/src/commands/login.tspackages/cli/src/config/config.tspackages/cli/src/schema/registry.tspackages/cli/test/config.test.tspackages/docs/api/cli.mdscripts/verify-ci.mjsscripts/verify-self.mjs
💤 Files with no reviewable changes (1)
- scripts/verify-self.mjs
| // Auto-refresh on 401 for JWT auth (one attempt only) | ||
| if ( | ||
| res.status === 401 && | ||
| this.config.authMode === 'jwt' && | ||
| this.config.refreshToken | ||
| ) { | ||
| const refreshed = await this.refreshSession(); | ||
| if (refreshed) { | ||
| // Retry the original request with new token | ||
| const retryRes = await fetch(url, { | ||
| method, | ||
| headers: this.headers, | ||
| body: body ? JSON.stringify(body) : undefined, | ||
| }); | ||
| const retryData = (await retryRes.json().catch(() => null)) as T; | ||
| return { ok: retryRes.ok, status: retryRes.status, data: retryData }; | ||
| } | ||
|
|
||
| // Refresh failed — return a clear error | ||
| return { | ||
| ok: false, | ||
| status: 401, | ||
| data: { | ||
| error: { | ||
| code: 'SESSION_EXPIRED', | ||
| message: 'Session expired. Run `wafflebase login`.', | ||
| }, | ||
| } as T, | ||
| }; | ||
| } |
There was a problem hiding this comment.
Reuse the JWT refresh path for API-key endpoints too.
Only request() gets the new 401 refresh/retry logic. listApiKeys(), createApiKey(), and revokeApiKey() still use raw fetch, so the same expired session refreshes for document/tab/cell commands but fails for API-key management. Please funnel those methods through the same helper, or add a requestAbsolute() variant with the same retry behavior.
Also applies to: 179-203
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/cli/src/client/http-client.ts` around lines 97 - 126, The API-key
methods (listApiKeys, createApiKey, revokeApiKey) bypass the centralized JWT
refresh/retry logic in request(), causing failures on expired sessions; either
route those methods to call request() instead of raw fetch or implement a new
requestAbsolute() that duplicates request()'s 401->refreshSession()->retry logic
and use it from those functions; update listApiKeys, createApiKey, revokeApiKey
to call the unified helper (or requestAbsolute) so they inherit the same
auto-refresh-and-retry behavior and return the same { ok, status, data } shape
as request().
| .option('--no-header', 'First row is NOT a header (CSV)') | ||
| .option('--start <ref>', 'Top-left cell to start import', 'A1') | ||
| .action(async function (this: Command, docId: string, file: string) { | ||
| const opts = getGlobalOpts(this); | ||
| const localOpts = this.opts<{ | ||
| tab: string; | ||
| format?: string; | ||
| header: boolean; | ||
| start: string; | ||
| }>(); | ||
|
|
||
| try { | ||
| const fmt = detectFormat(file, localOpts.format); | ||
| const raw = readInput(file); | ||
| let rows: string[][]; | ||
|
|
||
| if (fmt === 'json') { | ||
| const parsed = JSON.parse(raw); | ||
| // Expect either array of arrays or array of objects | ||
| if (Array.isArray(parsed) && parsed.length > 0) { | ||
| if (Array.isArray(parsed[0])) { | ||
| rows = parsed as string[][]; | ||
| } else if (typeof parsed[0] === 'object') { | ||
| // Array of objects → header row + value rows | ||
| const objs = parsed as Record<string, unknown>[]; | ||
| const keys = Object.keys(objs[0]); | ||
| rows = [keys, ...objs.map((o) => keys.map((k) => String(o[k] ?? '')))]; | ||
| } else { | ||
| throw new Error('JSON must be an array of arrays or array of objects'); | ||
| } | ||
| } else { | ||
| throw new Error('JSON must be a non-empty array'); | ||
| } | ||
| } else { | ||
| rows = parseCsv(raw); |
There was a problem hiding this comment.
--no-header is declared but never applied.
The command parses header but CSV import ignores it, so --no-header currently has no effect. This creates a behavior mismatch with docs/schema and will confuse users.
Proposed fix (wire the option in CSV path)
- } else {
- rows = parseCsv(raw);
- }
+ } else {
+ const parsedRows = parseCsv(raw);
+ rows = localOpts.header ? parsedRows.slice(1) : parsedRows;
+ }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/cli/src/commands/import.ts` around lines 36 - 70, The CSV path
currently ignores the --no-header / localOpts.header option; update the import
action so the CSV parsing respects localOpts.header by wiring it into parseCsv
(or by post-processing the parsed rows): call parseCsv(raw, { header:
localOpts.header }) if parseCsv supports options, or after rows = parseCsv(raw)
treat the first row as a header row only when localOpts.header === true (and
leave it as data when localOpts.header === false). Modify the code around
parseCsv and use localOpts.header (and the --no-header option name) to decide
whether the first row is header or regular data.
- login: use global --server via optsWithGlobals() instead of shadowed local option that Commander couldn't resolve - export/import: rename --format to --file-format to avoid collision with the global output --format option - session: add WAFFLEBASE_SESSION env override for test isolation - login callback page: add centered card layout with system font Co-Authored-By: Claude Opus 4.6 <[email protected]>
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (3)
packages/cli/src/commands/import.ts (1)
69-71:⚠️ Potential issue | 🟠 Major
--no-headeroption is still not applied for CSV imports.The
localOpts.headerflag is parsed (line 43) but the CSV branch at line 70 ignores it. When--no-headeris passed,headerisfalse, meaning the first row should be treated as data, not a header. Currently all rows are passed unchanged.Per the context in
csv-parse.ts,buildCellMapexpects preprocessed data. The header handling should happen here before callingbuildCellMap.🐛 Proposed fix
} else { - rows = parseCsv(raw); + const parsed = parseCsv(raw); + // When header is true (default), first row is a header and included in data + // When --no-header is passed (header=false), all rows are data + rows = parsed; } + + // If header mode is on and we have CSV data, the first row is already + // part of the data array. No stripping needed since buildCellMap + // imports all rows including headers as cell values.Note: If the intent is to skip the header row when importing (not import column names as cell values), the logic would be:
} else { - rows = parseCsv(raw); + const parsed = parseCsv(raw); + // Skip header row when header=true (default), keep all rows when --no-header + rows = localOpts.header ? parsed.slice(1) : parsed; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/cli/src/commands/import.ts` around lines 69 - 71, The CSV branch ignores the localOpts.header flag so --no-header isn’t applied; in the import flow (the block that calls parseCsv and later buildCellMap) detect localOpts.header (or localOpts.header === false) and, when header is false, do not treat the first parsed row as a header — either pass the parsed rows unchanged as data rows or explicitly prepend synthetic headers if buildCellMap expects headers; update the code around parseCsv(...) and before buildCellMap(...) to remove/retain the first row accordingly (refer to parseCsv, rows and buildCellMap identifiers) so the first CSV row is treated as data when --no-header is set.packages/cli/src/commands/export.ts (1)
38-40:⚠️ Potential issue | 🟡 MinorPreserve the backend's error message on failed exports.
The error at line 40 reduces every non-OK response to
HTTP ${res.status}, hiding useful details like missing tabs or invalid ranges. As per coding guidelines: "Use meaningful error messages that help with debugging."🔧 Proposed fix
- if (!res.ok) throw new Error(`HTTP ${res.status}`); + if (!res.ok) { + const body = res.data as { error?: { message?: string } } | null; + throw new Error(body?.error?.message ?? `HTTP ${res.status}`); + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/cli/src/commands/export.ts` around lines 38 - 40, The current export error replaces any non-OK response with a generic "HTTP ${res.status}", losing backend diagnostics; update the handling after calling getClient(...).getCells(docId, localOpts.tab, localOpts.range) to read the response body (e.g., await res.text() or await res.json() depending on content-type) and include that message along with the status in the thrown Error so callers see the backend error (reference the res variable returned from getCells and the surrounding try block where the throw happens).packages/cli/src/commands/login.ts (1)
96-101:⚠️ Potential issue | 🟠 MajorWorkspace fetch failure still overwrites session with empty workspace state.
While a warning was added at line 100, a transient
/workspacesfailure (e.g., HTTP 500) still results in savingworkspaces: []andactiveWorkspace: ''at lines 132-133. Subsequent workspace-scoped commands will fail or use empty workspace IDs.Consider preserving the existing session's workspace state on fetch failure, or failing the login entirely rather than leaving the user in a broken state.
🔧 Proposed fix - preserve existing workspace state
let workspaces: WorkspaceInfo[] = []; + let activeWorkspace = ''; if (wsRes.ok) { workspaces = (await wsRes.json()) as WorkspaceInfo[]; } else { console.warn(`Warning: failed to fetch workspaces (HTTP ${wsRes.status})`); + // Preserve existing workspace state if available + if (existing) { + workspaces = existing.workspaces; + activeWorkspace = existing.activeWorkspace; + console.warn('Preserving previous workspace context.'); + } } // 8. Select workspace - let activeWorkspace = ''; - if (workspaces.length === 0) { + if (workspaces.length === 0 && !activeWorkspace) { console.log('No workspaces found.'); } else if (workspaces.length === 1) {Also applies to: 125-136
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/cli/src/commands/login.ts` around lines 96 - 101, The current login flow overwrites the session with an empty workspace list when wsRes.ok is false; instead, when the /workspaces fetch fails (wsRes.ok === false) preserve the existing workspace state or abort the login: do not assign workspaces = [] or activeWorkspace = '' on error—keep the previous session.workspaces and session.activeWorkspace (or load them from the session store) and only update/sync workspaces when wsRes.ok is true, or throw/return an error to stop the save flow so the code path that writes the session (the block that persists workspaces and activeWorkspace) does not run with an empty list. Ensure you update the logic around the workspaces variable, wsRes handling, and the session save to reflect this behavior.
🧹 Nitpick comments (2)
packages/cli/test/config.test.ts (2)
15-16: Add a positiveWAFFLEBASE_SESSIONresolution test (and precedence check).This suite now covers the missing-session path, but not the critical edge case where a real session file exists and should participate in
resolveConfigprecedence. Please add one test that uses a valid session file and asserts ordering versus flags/env/config.As per coding guidelines:
**/*.{test,spec}.{js,ts,jsx,tsx}: Write tests for critical business logic and edge cases.Also applies to: 23-30
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/cli/test/config.test.ts` around lines 15 - 16, Add a positive test case to packages/cli/test/config.test.ts that creates a valid WAFFLEBASE_SESSION file and verifies resolveConfig reads it and that its values take the correct precedence versus CLI flags, environment variables, and config file; specifically, call resolveConfig with conflicting inputs (e.g., set a flag, set process.env.WAFFLEBASE_SESSION to point to the temp session file, and provide a config object/file) and assert the resolved config matches the session file's values, then clean up the temp session file and restore process.env (remove WAFFLEBASE_SESSION) after the test.
85-126: Use per-test temp dirs with explicit cleanup to avoid fixture leakage.These tests create directories/files in
tmpdir()but never remove them. Over time this pollutes CI/dev machines and can cause rare collisions with timestamp-based naming. PrefermkdtempSync+rmSync(..., { recursive: true, force: true })in teardown/finally.Suggested refactor
-import { mkdirSync, writeFileSync, existsSync, readFileSync } from 'node:fs'; +import { mkdirSync, writeFileSync, existsSync, readFileSync, mkdtempSync, rmSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; describe('migrateConfigIfNeeded', () => { it('copies config from old path to new path when new path does not exist', () => { - const base = join(tmpdir(), `wfb-migrate-test-${Date.now()}`); + const base = mkdtempSync(join(tmpdir(), 'wfb-migrate-test-')); const oldDir = join(base, 'old'); const newDir = join(base, 'new'); const oldPath = join(oldDir, 'config.yaml'); const newPath = join(newDir, 'config.yaml'); - mkdirSync(oldDir, { recursive: true }); - writeFileSync(oldPath, 'profiles:\n default:\n server: https://old.example.com\n'); - - migrateConfigIfNeeded(newPath, oldPath); - - expect(existsSync(newPath)).toBe(true); - expect(readFileSync(newPath, 'utf-8')).toContain('https://old.example.com'); + try { + mkdirSync(oldDir, { recursive: true }); + writeFileSync(oldPath, 'profiles:\n default:\n server: https://old.example.com\n'); + migrateConfigIfNeeded(newPath, oldPath); + expect(existsSync(newPath)).toBe(true); + expect(readFileSync(newPath, 'utf-8')).toContain('https://old.example.com'); + } finally { + rmSync(base, { recursive: true, force: true }); + } }); });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/cli/test/config.test.ts` around lines 85 - 126, These tests leak temp files by using join(tmpdir(), `...${Date.now()}`) without cleanup; change each test to create a per-test temp directory with mkdtempSync(path.join(tmpdir(), 'wfb-migrate-XXXXXX')), use that dir for oldDir/newDir and call migrateConfigIfNeeded(newPath, oldPath) as before, and ensure cleanup by calling rmSync(base, { recursive: true, force: true }) in a finally block inside the test or in an afterEach teardown; reference migrateConfigIfNeeded, mkdtempSync, rmSync, tmpdir and the base/oldDir/newDir/newPath variables when making the edits.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/docs/api/cli.md`:
- Around line 338-343: The example JSON in the CLI docs shows an inconsistent
server URL (the "url" field contains "http://localhost:3000") while the doc's
default server is documented elsewhere as "https://wafflebase-api.yorkie.dev";
update the example in the packages/docs/api/cli.md example block (the JSON
object containing "dry_run", "method", "url", "body") to either use the
documented default URL or add a short comment/annotation next to the example
stating that "http://localhost:3000" is only an override/local example so
readers aren't confused by the mismatch.
---
Duplicate comments:
In `@packages/cli/src/commands/export.ts`:
- Around line 38-40: The current export error replaces any non-OK response with
a generic "HTTP ${res.status}", losing backend diagnostics; update the handling
after calling getClient(...).getCells(docId, localOpts.tab, localOpts.range) to
read the response body (e.g., await res.text() or await res.json() depending on
content-type) and include that message along with the status in the thrown Error
so callers see the backend error (reference the res variable returned from
getCells and the surrounding try block where the throw happens).
In `@packages/cli/src/commands/import.ts`:
- Around line 69-71: The CSV branch ignores the localOpts.header flag so
--no-header isn’t applied; in the import flow (the block that calls parseCsv and
later buildCellMap) detect localOpts.header (or localOpts.header === false) and,
when header is false, do not treat the first parsed row as a header — either
pass the parsed rows unchanged as data rows or explicitly prepend synthetic
headers if buildCellMap expects headers; update the code around parseCsv(...)
and before buildCellMap(...) to remove/retain the first row accordingly (refer
to parseCsv, rows and buildCellMap identifiers) so the first CSV row is treated
as data when --no-header is set.
In `@packages/cli/src/commands/login.ts`:
- Around line 96-101: The current login flow overwrites the session with an
empty workspace list when wsRes.ok is false; instead, when the /workspaces fetch
fails (wsRes.ok === false) preserve the existing workspace state or abort the
login: do not assign workspaces = [] or activeWorkspace = '' on error—keep the
previous session.workspaces and session.activeWorkspace (or load them from the
session store) and only update/sync workspaces when wsRes.ok is true, or
throw/return an error to stop the save flow so the code path that writes the
session (the block that persists workspaces and activeWorkspace) does not run
with an empty list. Ensure you update the logic around the workspaces variable,
wsRes handling, and the session save to reflect this behavior.
---
Nitpick comments:
In `@packages/cli/test/config.test.ts`:
- Around line 15-16: Add a positive test case to
packages/cli/test/config.test.ts that creates a valid WAFFLEBASE_SESSION file
and verifies resolveConfig reads it and that its values take the correct
precedence versus CLI flags, environment variables, and config file;
specifically, call resolveConfig with conflicting inputs (e.g., set a flag, set
process.env.WAFFLEBASE_SESSION to point to the temp session file, and provide a
config object/file) and assert the resolved config matches the session file's
values, then clean up the temp session file and restore process.env (remove
WAFFLEBASE_SESSION) after the test.
- Around line 85-126: These tests leak temp files by using join(tmpdir(),
`...${Date.now()}`) without cleanup; change each test to create a per-test temp
directory with mkdtempSync(path.join(tmpdir(), 'wfb-migrate-XXXXXX')), use that
dir for oldDir/newDir and call migrateConfigIfNeeded(newPath, oldPath) as
before, and ensure cleanup by calling rmSync(base, { recursive: true, force:
true }) in a finally block inside the test or in an afterEach teardown;
reference migrateConfigIfNeeded, mkdtempSync, rmSync, tmpdir and the
base/oldDir/newDir/newPath variables when making the edits.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: a9594d8b-77a2-4fcc-a532-e255e13cf076
📒 Files selected for processing (7)
packages/cli/src/commands/export.tspackages/cli/src/commands/import.tspackages/cli/src/commands/login.tspackages/cli/src/config/session.tspackages/cli/src/schema/registry.tspackages/cli/test/config.test.tspackages/docs/api/cli.md
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/cli/src/config/session.ts
- export: preserve backend error message on failure - import: remove unused --no-header flag (spreadsheets import all rows) - login: fail with error when workspace fetch fails instead of saving empty workspace state - config: use stderr for migration notice to not break JSON output - docs: add language identifiers to code blocks, anonymize example email in session schema Co-Authored-By: Claude Opus 4.6 <[email protected]>
Move cli and cli-oauth-login task files to archive after completion in recent PRs (#34, #35). Update task index accordingly. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Summary
logout,status, andctx list/ctx switchcommands for workspace managementauth logincommand with top-levellogin/logout/status/ctxcommands in bin.ts and schema registry~/.wafflebase/)Test plan
pnpm verify:fastpasses (103 backend, 1018 sheet, 59 CLI, 51 frontend tests)wafflebase login→ browser OAuth → session storedwafflebase ctx list/wafflebase ctx switchwafflebase logoutclears session🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Documentation
Tests
Chores