gemiterm 2.0.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -9
- package/package.json +13 -4
- package/src/cli/command-registry.ts +66 -62
- package/src/cli/commands/continue-command.ts +75 -1
- package/src/cli/commands/install-skills-command.ts +35 -0
- package/src/cli/commands/new-command.ts +62 -0
- package/src/cli/index.ts +19 -44
- package/src/cli/utils/long-arg-guard.ts +28 -0
- package/src/cli/utils/prompt-file.ts +26 -0
- package/src/infrastructure/cli-parser.ts +77 -0
- package/src/infrastructure/io.ts +9 -0
- package/src/services/skills-cli-driver.ts +58 -0
- package/src/services/skills-service.ts +52 -0
- package/.env +0 -2
- package/.github/workflows/manual-release.yml +0 -34
- package/.github/workflows/release.yml +0 -198
- package/.github/workflows/test.yml +0 -29
- package/.opencode/commands/opsx-apply.md +0 -152
- package/.opencode/commands/opsx-archive.md +0 -157
- package/.opencode/commands/opsx-explore.md +0 -169
- package/.opencode/commands/opsx-propose.md +0 -104
- package/.opencode/commands/opsx-sync.md +0 -140
- package/.opencode/commands/search-intelligently.md +0 -9
- package/.opencode/opencode.json +0 -20
- package/.opencode/skills/intellisearch/.version +0 -1
- package/.opencode/skills/intellisearch/SKILL.md +0 -84
- package/.opencode/skills/intellisearch/references/brave-search.md +0 -67
- package/.opencode/skills/intellisearch/references/ddg-search.md +0 -79
- package/.opencode/skills/intellisearch/references/deepwiki-tools.md +0 -246
- package/.opencode/skills/intellisearch/references/examples.md +0 -97
- package/.opencode/skills/intellisearch/references/gh-cli.md +0 -143
- package/.opencode/skills/intellisearch/references/google-search.md +0 -40
- package/.opencode/skills/intellisearch/references/search-workflow.md +0 -143
- package/.opencode/skills/intellisearch/references/workflow.md +0 -115
- package/.opencode/skills/openspec-apply-change/SKILL.md +0 -159
- package/.opencode/skills/openspec-archive-change/SKILL.md +0 -117
- package/.opencode/skills/openspec-explore/SKILL.md +0 -287
- package/.opencode/skills/openspec-propose/SKILL.md +0 -111
- package/.opencode/skills/openspec-sync-specs/SKILL.md +0 -147
- package/.serena/project.yml +0 -133
- package/AGENTS.md +0 -136
- package/CHANGELOG.md +0 -3
- package/bun.lock +0 -105
- package/docs/INSTALL.md +0 -185
- package/docs/PLAYWRIGHT_CLI_API.md +0 -306
- package/docs/python-release-for-reference.md +0 -175
- package/examples/sample_json_export.json +0 -32
- package/examples/sample_markdown_export.md +0 -221
- package/examples/usage_examples.sh +0 -222
- package/install.ps1 +0 -264
- package/install.sh +0 -275
- package/openspec/MAESTRO_MIGRATION.md +0 -180
- package/openspec/changes/archive/2026-06-08-command-spec-conformance/.openspec.yaml +0 -2
- package/openspec/changes/archive/2026-06-08-command-spec-conformance/design.md +0 -194
- package/openspec/changes/archive/2026-06-08-command-spec-conformance/proposal.md +0 -52
- package/openspec/changes/archive/2026-06-08-command-spec-conformance/specs/multi-profile-conversations/spec.md +0 -133
- package/openspec/changes/archive/2026-06-08-command-spec-conformance/tasks.md +0 -95
- package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/.openspec.yaml +0 -2
- package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/design.md +0 -28
- package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/proposal.md +0 -22
- package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/specs/interactive-prompt-loop/spec.md +0 -35
- package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/tasks.md +0 -28
- package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/.openspec.yaml +0 -2
- package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/design.md +0 -323
- package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/proposal.md +0 -122
- package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/specs/no-capability-changes/spec.md +0 -29
- package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/tasks.md +0 -172
- package/openspec/changes/archive/2026-06-09-v2-install-migration/.openspec.yaml +0 -2
- package/openspec/changes/archive/2026-06-09-v2-install-migration/design.md +0 -241
- package/openspec/changes/archive/2026-06-09-v2-install-migration/proposal.md +0 -41
- package/openspec/changes/archive/2026-06-09-v2-install-migration/specs/v2-installer/spec.md +0 -205
- package/openspec/changes/archive/2026-06-09-v2-install-migration/tasks.md +0 -97
- package/openspec/changes/commander-cli-parser/.openspec.yaml +0 -2
- package/openspec/changes/commander-cli-parser/design.md +0 -107
- package/openspec/changes/commander-cli-parser/proposal.md +0 -31
- package/openspec/changes/commander-cli-parser/specs/cli/spec.md +0 -48
- package/openspec/changes/commander-cli-parser/tasks.md +0 -33
- package/openspec/changes/cross-platform-build-and-ci/.openspec.yaml +0 -2
- package/openspec/changes/cross-platform-build-and-ci/design.md +0 -213
- package/openspec/changes/cross-platform-build-and-ci/proposal.md +0 -50
- package/openspec/changes/cross-platform-build-and-ci/specs/path-and-file-mediation/spec.md +0 -335
- package/openspec/changes/cross-platform-build-and-ci/specs/release-pipeline/spec.md +0 -95
- package/openspec/changes/cross-platform-build-and-ci/tasks.md +0 -164
- package/openspec/config.yaml +0 -20
- package/openspec/specs/auth/spec.md +0 -394
- package/openspec/specs/cli/spec.md +0 -108
- package/openspec/specs/commands/spec.md +0 -409
- package/openspec/specs/configuration/spec.md +0 -130
- package/openspec/specs/conversations/spec.md +0 -139
- package/openspec/specs/domain-model/spec.md +0 -128
- package/openspec/specs/formatters/spec.md +0 -158
- package/openspec/specs/install-browser/spec.md +0 -146
- package/openspec/specs/interactive-prompt-loop/spec.md +0 -41
- package/openspec/specs/logger/spec.md +0 -71
- package/openspec/specs/mediator/spec.md +0 -72
- package/openspec/specs/multi-profile-conversations/spec.md +0 -145
- package/openspec/specs/path-utils/spec.md +0 -101
- package/openspec/specs/profiles/spec.md +0 -94
- package/openspec/specs/storage/spec.md +0 -198
- package/openspec/specs/testing/spec.md +0 -237
- package/openspec/specs/v2-installer/spec.md +0 -205
- package/openspec/specs/validators/spec.md +0 -65
- package/scripts/build.ts +0 -88
- package/scripts/clean-build.ps1 +0 -4
- package/scripts/clean-build.sh +0 -5
- package/scripts/install-browser.ps1 +0 -5
- package/scripts/install-browser.sh +0 -6
- package/scripts/lint-path-mediation.ps1 +0 -65
- package/scripts/lint-path-mediation.sh +0 -40
- package/tests/cli/auth-command.test.ts +0 -220
- package/tests/cli/command-registry.test.ts +0 -67
- package/tests/cli/continue-command.test.ts +0 -100
- package/tests/cli/delete-command.test.ts +0 -128
- package/tests/cli/export-all-command.test.ts +0 -375
- package/tests/cli/install-browser-command.test.ts +0 -48
- package/tests/cli/list-command.test.ts +0 -179
- package/tests/cli/new-command.test.ts +0 -139
- package/tests/cli/status-command.test.ts +0 -106
- package/tests/core/query-handlers.test.ts +0 -187
- package/tests/fixtures/auth-fixtures.ts +0 -98
- package/tests/fixtures/chat-fixtures.ts +0 -87
- package/tests/infrastructure/cli-table.test.ts +0 -107
- package/tests/infrastructure/formatters.test.ts +0 -202
- package/tests/infrastructure/io.test.ts +0 -197
- package/tests/infrastructure/logger.test.ts +0 -74
- package/tests/infrastructure/storage.test.ts +0 -335
- package/tests/infrastructure/validators.test.ts +0 -67
- package/tests/integration/commands/auth.test.ts +0 -186
- package/tests/integration/commands/continue.test.ts +0 -130
- package/tests/integration/commands/delete.test.ts +0 -140
- package/tests/integration/commands/export.test.ts +0 -266
- package/tests/integration/commands/fetch.test.ts +0 -238
- package/tests/integration/commands/list.test.ts +0 -281
- package/tests/integration/commands/profile.test.ts +0 -258
- package/tests/integration/commands/status.test.ts +0 -223
- package/tests/integration/installer-script-shape.test.ts +0 -67
- package/tests/parity/compare-outputs.ts +0 -359
- package/tests/parity/test-commands-parity.ps1 +0 -82
- package/tests/parity/test-commands-parity.sh +0 -61
- package/tests/services/auth-service.test.ts +0 -315
- package/tests/services/cookie-monitor.test.ts +0 -229
- package/tests/services/cookie-storage-service.test.ts +0 -230
- package/tests/services/gemini-client-wrapper.test.ts +0 -688
- package/tests/services/install-browser-service.test.ts +0 -83
- package/tests/services/playwright-cli-driver.test.ts +0 -468
- package/tests/services/profile-auth-manager.test.ts +0 -334
- package/tests/services/profile-service.test.ts +0 -224
- package/tests/setup.ts +0 -95
- package/tests/smoke/smoke.test.ts +0 -52
- package/tests/unit/config.test.ts +0 -159
- package/tests/unit/errors.test.ts +0 -119
- package/tests/unit/formatters.test.ts +0 -316
- package/tests/unit/mediator.test.ts +0 -152
- package/tests/unit/path-utils.test.ts +0 -289
- package/tsconfig.json +0 -16
|
@@ -1,394 +0,0 @@
|
|
|
1
|
-
## Purpose
|
|
2
|
-
|
|
3
|
-
The interactive authentication capability for `gemiterm`. It owns the end-to-end login flow (launching a headed Chromium via `playwright-cli`, polling a running browser session for Google's sign-out indicator, capturing the `__Secure-1PSID` and `__Secure-1PSIDTS` cookies, persisting them to the profile storage, and closing the browser). It also owns the multi-profile management CLI surface (`auth` command) and the `playwright-cli` driver abstraction that auto-detects the local install (direct binary or `bunx @playwright/cli`).
|
|
4
|
-
|
|
5
|
-
## Requirements
|
|
6
|
-
|
|
7
|
-
### Requirement: AuthService.authenticate orchestrates the full login flow
|
|
8
|
-
The `AuthService.authenticate(profileName?)` method MUST be the single entry point for the login flow. When called, it MUST resolve the profile name (using the provided name, or the configured default), validate it, log an info-level "Starting authentication" message, and then in order: print a one-shot headed-browser notification, launch the browser via `PlaywrightCliDriver.openHeaded("https://gemini.google.com/app", profileName, profileName)`, wait up to 300000 ms (5 minutes) for the cookie monitor to report auth cookies, save the captured cookies via the cookie storage, compute the session expiry from `__Secure-1PSIDTS.expires`, and print a confirmation message containing the cookie count and the expiration date (when known). The browser session MUST always be closed in a `finally` block, even if the wait or save step throws.
|
|
9
|
-
|
|
10
|
-
#### Scenario: Successful authentication returns cookies and expiry
|
|
11
|
-
- **WHEN** `authenticate("test-profile")` is called and the cookie monitor reports both `__Secure-1PSID` and `__Secure-1PSIDTS` before the timeout
|
|
12
|
-
- **THEN** the method resolves with an `AuthResult` whose `cookies` array has length 2, whose `expiresAt` is a `Date` derived from `__Secure-1PSIDTS.expires * 1000`, the driver `openHeaded` is called exactly once with the Gemini app URL, the cookie monitor `start` is called exactly once, the cookie storage `save` is called exactly once, and the driver `closeSession` is called exactly once with the profile name
|
|
13
|
-
|
|
14
|
-
#### Scenario: Driver is opened with the Gemini app URL and the profile as session
|
|
15
|
-
- **WHEN** `launchBrowser("my-profile")` is called via the public flow
|
|
16
|
-
- **THEN** the driver `openHeaded` is invoked with `"https://gemini.google.com/app"` as the URL, `"my-profile"` as the profile, and `"my-profile"` as the session identifier
|
|
17
|
-
|
|
18
|
-
#### Scenario: Browser is closed in a finally block on error
|
|
19
|
-
- **WHEN** `authenticate("test-profile")` is called and `waitForLogin` throws (e.g. timeout)
|
|
20
|
-
- **THEN** the driver `closeSession` is still invoked with the profile name (verified by `expect(driver.closeSession).toHaveBeenCalledWith("test-profile")`)
|
|
21
|
-
|
|
22
|
-
#### Scenario: Invalid profile name is rejected
|
|
23
|
-
- **WHEN** `authenticate("bad name!")` is called
|
|
24
|
-
- **THEN** the method rejects with an error whose message contains `invalid characters`
|
|
25
|
-
|
|
26
|
-
#### Scenario: Authentication timeout is surfaced as AuthServiceTimeoutError
|
|
27
|
-
- **WHEN** `waitForLogin` is called and the cookie monitor never invokes its callback before the timeout fires
|
|
28
|
-
- **THEN** the method rejects with an `AuthServiceTimeoutError` whose message contains the configured timeout in milliseconds and the substring `No auth cookies detected`, and the cookie monitor `stop` has been called
|
|
29
|
-
|
|
30
|
-
### Requirement: AuthService prints headed-browser notification (no Enter-block)
|
|
31
|
-
The `AuthService.notifyUser(profileName)` method MUST print a one-shot UI message to the console before launching the browser. The message MUST contain the substring `Opening headed browser`, the Gemini app URL `https://gemini.google.com/app`, and the substring `auto-detect`. The method MUST NOT block on an Enter key press — once the messages are printed, the call MUST return and the caller MUST proceed directly to launch the browser. The single-line "Press Enter to launch browser" prompt described in earlier design notes is not implemented; the current flow runs the launch immediately after the notification.
|
|
32
|
-
|
|
33
|
-
#### Scenario: Notification includes URL and auto-detect hint
|
|
34
|
-
- **WHEN** `notifyUser("default")` is called
|
|
35
|
-
- **THEN** the combined console output contains the substrings `Opening headed browser`, `https://gemini.google.com/app`, and `auto-detect`
|
|
36
|
-
|
|
37
|
-
#### Scenario: Notification does not block waiting for input
|
|
38
|
-
- **WHEN** the auth flow calls `notifyUser` followed by `launchBrowser`
|
|
39
|
-
- **THEN** `launchBrowser` runs without the user pressing Enter (no `readline` or stdin reader is involved in the path)
|
|
40
|
-
|
|
41
|
-
### Requirement: AuthService prints a confirmation with cookie count and expiry
|
|
42
|
-
The `AuthService.confirmAuthSuccess(cookieCount, expiresAt, cookies)` method MUST print a success block to the console. The block MUST contain the substrings `Login auto-detected`, `Authentication successful`, and `<cookieCount> cookies`. When `expiresAt` is a `Date`, the block MUST additionally contain the substring `Session expires` followed by the localized date. The block MUST also report the presence of `__Secure-1PSID` in the supplied cookies with a `✅` (present) or `❌` (absent) marker.
|
|
43
|
-
|
|
44
|
-
#### Scenario: Success block with expiry
|
|
45
|
-
- **WHEN** `confirmAuthSuccess(2, new Date("2026-12-31T00:00:00Z"), makeAuthCookies())` is called
|
|
46
|
-
- **THEN** the combined output contains `Login auto-detected`, `Authentication successful`, `2 cookies`, `Session expires`, `__Secure-1PSID`, and the `✅` marker
|
|
47
|
-
|
|
48
|
-
#### Scenario: Success block without expiry
|
|
49
|
-
- **WHEN** `confirmAuthSuccess(1, null, [])` is called
|
|
50
|
-
- **THEN** the combined output contains `Authentication successful` and `1 cookies` but does NOT contain the substring `Session expires`
|
|
51
|
-
|
|
52
|
-
#### Scenario: Success block with missing __Secure-1PSID
|
|
53
|
-
- **WHEN** `confirmAuthSuccess(0, null, [])` is called
|
|
54
|
-
- **THEN** the combined output contains `__Secure-1PSID` and the `❌` marker
|
|
55
|
-
|
|
56
|
-
### Requirement: CookieMonitor polls every 2 seconds using a sign-out-link JS probe
|
|
57
|
-
The `CookieMonitor.start(session, onCookiesFound, timeoutMs?)` method MUST begin polling the browser session identified by `session` for the presence of the auth cookies. The poll cadence MUST be a 2-second interval (`POLL_INTERVAL_MS = 2000`). On each tick the monitor MUST run a JavaScript probe that detects a logged-in state by checking for the presence of `a[href^="https://accounts.google.com/SignOutOptions"]`; the probe expression MUST be exactly `document.querySelector('a[href^="https://accounts.google.com/SignOutOptions"]') !== null`. Only when the probe returns `true` (matched as a trimmed string `"true"`) MUST the monitor then read the cookie list and require BOTH `__Secure-1PSID` and `__Secure-1PSIDTS` to be present before invoking the `onCookiesFound` callback. The monitor MUST also accept a `timeoutMs` parameter (default 300000 ms / 5 minutes) and MUST apply `.unref()` to the timeout handle so it does not block CLI process exit.
|
|
58
|
-
|
|
59
|
-
#### Scenario: Monitor fires callback on first tick where probe and cookies are both ready
|
|
60
|
-
- **WHEN** `start("sess1", cb, 10000)` is called and every 2-second tick returns `true` from the probe and returns both required cookies from `cookieList`
|
|
61
|
-
- **THEN** `cb` is invoked exactly once with the auth cookies, `isRunning` is `false`, and the interval has been cleared
|
|
62
|
-
|
|
63
|
-
#### Scenario: Monitor does not call driver immediately on start
|
|
64
|
-
- **WHEN** `start("sess1", cb, 10000)` is called
|
|
65
|
-
- **THEN** `driver.evalJs` is NOT called synchronously — the first eval call is scheduled by the interval and only happens after the 2-second delay
|
|
66
|
-
|
|
67
|
-
#### Scenario: Monitor does not call callback when probe returns false
|
|
68
|
-
- **WHEN** `start("sess1", cb, 10000)` is called and every tick returns `false` from the probe
|
|
69
|
-
- **THEN** `cb` is never invoked and `isRunning` is `false` after `stop()`
|
|
70
|
-
|
|
71
|
-
#### Scenario: Monitor swallows repeated eval throws
|
|
72
|
-
- **WHEN** `start("sess1", cb, 10000)` is called and every tick's `evalJs` rejects
|
|
73
|
-
- **THEN** `start` resolves successfully, `cb` is never invoked, and the monitor continues running (does not reject)
|
|
74
|
-
|
|
75
|
-
#### Scenario: Timeout handle is unref'd so CLI can exit
|
|
76
|
-
- **WHEN** `start("sess1", cb, 10000)` is called
|
|
77
|
-
- **THEN** the `setTimeout` handle used for the hard timeout has `unref()` called on it (the monitor must not keep the event loop alive past the timeout)
|
|
78
|
-
|
|
79
|
-
### Requirement: CookieMonitor exposes checkLoggedIn and checkCookies helpers
|
|
80
|
-
The `CookieMonitor.checkLoggedIn(session)` method MUST return `true` when the sign-out-link probe evaluates to the string `"true"` (with surrounding whitespace allowed) and MUST return `false` for any other return value, including probe errors. The `CookieMonitor.checkCookies(session)` method MUST call `driver.cookieList(session)`, filter the result to entries whose `name` is `__Secure-1PSID` or `__Secure-1PSIDTS`, and return the filtered list when both required cookies are present; otherwise it MUST return an empty array. Both helpers MUST swallow driver errors and return `false` / `[]` respectively.
|
|
81
|
-
|
|
82
|
-
#### Scenario: checkLoggedIn returns true on probe success
|
|
83
|
-
- **WHEN** `driver.evalJs` returns the string `"true"`
|
|
84
|
-
- **THEN** `checkLoggedIn("sess1")` resolves with `true`
|
|
85
|
-
|
|
86
|
-
#### Scenario: checkLoggedIn returns false on whitespace-padded true
|
|
87
|
-
- **WHEN** `driver.evalJs` returns the string `" true \n"`
|
|
88
|
-
- **THEN** `checkLoggedIn("sess1")` resolves with `true` (whitespace is trimmed)
|
|
89
|
-
|
|
90
|
-
#### Scenario: checkLoggedIn returns false on probe throw
|
|
91
|
-
- **WHEN** `driver.evalJs` rejects
|
|
92
|
-
- **THEN** `checkLoggedIn("sess1")` resolves with `false` (does not throw)
|
|
93
|
-
|
|
94
|
-
#### Scenario: checkCookies returns required cookies when both present
|
|
95
|
-
- **WHEN** `driver.cookieList` returns an array containing both `__Secure-1PSID` and `__Secure-1PSIDTS`
|
|
96
|
-
- **THEN** `checkCookies("sess1")` resolves with a length-2 array containing both required cookie names
|
|
97
|
-
|
|
98
|
-
#### Scenario: checkCookies returns empty when only one required cookie present
|
|
99
|
-
- **WHEN** `driver.cookieList` returns only `__Secure-1PSID`
|
|
100
|
-
- **THEN** `checkCookies("sess1")` resolves with `[]`
|
|
101
|
-
|
|
102
|
-
### Requirement: CookieMonitor.stop is idempotent and clears the interval
|
|
103
|
-
The `CookieMonitor.stop()` method MUST clear the active polling interval and timeout handle, set the internal `_stopped` flag, and log a `Cookie monitor stopped` info message. Subsequent calls to `stop()` MUST be no-ops (verified by the `stop is idempotent` test). After `stop()` is called, `isRunning` MUST be `false`.
|
|
104
|
-
|
|
105
|
-
#### Scenario: stop prevents further polling
|
|
106
|
-
- **WHEN** `start` is called and then `stop` is called before the first tick fires
|
|
107
|
-
- **THEN** the callback is never invoked and `isRunning` is `false`
|
|
108
|
-
|
|
109
|
-
#### Scenario: stop is idempotent
|
|
110
|
-
- **WHEN** `stop()` is called three consecutive times on a monitor that was never started
|
|
111
|
-
- **THEN** none of the calls throw and `isRunning` is `false`
|
|
112
|
-
|
|
113
|
-
### Requirement: CookieStorageService loads and validates per-profile cookies
|
|
114
|
-
The `CookieStorageService.loadCookiesForProfile(profileName)` method MUST return a `LoadedCookies` object with two fields: `secure_1psid: string` and `secure_1psidts: string | null`. The method MUST read cookies from the underlying `CookieStorage.load(profileName)`, build a name-to-value map, and extract the values for `__Secure-1PSID` and `__Secure-1PSIDTS`. If `__Secure-1PSID` is missing, the method MUST throw an `Error` whose message contains `Missing required cookie __Secure-1PSID` and mentions the profile name and `gemiterm auth`. The `secure_1psidts` field MUST be `null` when the cookie is absent.
|
|
115
|
-
|
|
116
|
-
#### Scenario: Load returns both cookie values
|
|
117
|
-
- **WHEN** the profile's storage contains both `__Secure-1PSID` and `__Secure-1PSIDTS`
|
|
118
|
-
- **THEN** `loadCookiesForProfile("default")` returns `{ secure_1psid: "<psid value>", secure_1psidts: "<psidts value>" }`
|
|
119
|
-
|
|
120
|
-
#### Scenario: Load returns null for missing 1PSIDTS
|
|
121
|
-
- **WHEN** the profile's storage contains only `__Secure-1PSID`
|
|
122
|
-
- **THEN** `loadCookiesForProfile("default")` returns `{ secure_1psid: "<psid value>", secure_1psidts: null }`
|
|
123
|
-
|
|
124
|
-
#### Scenario: Load throws when __Secure-1PSID is missing
|
|
125
|
-
- **WHEN** the profile's storage contains only `__Secure-1PSIDTS`
|
|
126
|
-
- **THEN** `loadCookiesForProfile("default")` throws an error whose message contains `Missing required cookie`
|
|
127
|
-
|
|
128
|
-
### Requirement: CookieStorageService validates and computes cookie freshness
|
|
129
|
-
The `CookieStorageService.validateCookies(cookies)` method MUST return `true` when the cookie array contains entries for BOTH `__Secure-1PSID` and `__Secure-1PSIDTS`, and MUST return `false` for any other case (including empty arrays). The `CookieStorageService.checkCookieFreshness(cookies)` method MUST return `true` only when `__Secure-1PSIDTS.expires` is a positive number AND the resulting expiry is more than 7 days (604800000 ms) in the future; it MUST return `false` for expired cookies, cookies expiring within 7 days, and cookies with no expiry timestamp.
|
|
130
|
-
|
|
131
|
-
#### Scenario: validateCookies returns true when both required cookies are present
|
|
132
|
-
- **WHEN** `validateCookies` is called with an array containing both `__Secure-1PSID` and `__Secure-1PSIDTS`
|
|
133
|
-
- **THEN** it returns `true`
|
|
134
|
-
|
|
135
|
-
#### Scenario: validateCookies returns false when 1PSIDTS is missing
|
|
136
|
-
- **WHEN** `validateCookies` is called with an array containing only `__Secure-1PSID`
|
|
137
|
-
- **THEN** it returns `false`
|
|
138
|
-
|
|
139
|
-
#### Scenario: checkCookieFreshness returns true for cookies expiring far in the future
|
|
140
|
-
- **WHEN** `checkCookieFreshness` is called with cookies whose `__Secure-1PSIDTS.expires` is more than 7 days from now
|
|
141
|
-
- **THEN** it returns `true`
|
|
142
|
-
|
|
143
|
-
#### Scenario: checkCookieFreshness returns false for cookies expiring within 7 days
|
|
144
|
-
- **WHEN** `checkCookieFreshness` is called with cookies whose `__Secure-1PSIDTS.expires` is 3 days from now
|
|
145
|
-
- **THEN** it returns `false`
|
|
146
|
-
|
|
147
|
-
#### Scenario: checkCookieFreshness returns false for expired cookies
|
|
148
|
-
- **WHEN** `checkCookieFreshness` is called with cookies whose `__Secure-1PSIDTS.expires` is in the past
|
|
149
|
-
- **THEN** it returns `false`
|
|
150
|
-
|
|
151
|
-
### Requirement: CookieStorageService computes cookie expiry
|
|
152
|
-
The `CookieStorageService.getCookieExpiry(cookies)` method MUST return a `Date` derived from `__Secure-1PSIDTS.expires * 1000` (Unix seconds to JS ms) when the cookie has a positive `expires` value. The method MUST return `null` when the cookie array is empty or when no cookie has a positive `expires` value.
|
|
153
|
-
|
|
154
|
-
#### Scenario: getCookieExpiry returns Date for valid expiry
|
|
155
|
-
- **WHEN** `getCookieExpiry` is called with cookies that include `__Secure-1PSIDTS` having a positive `expires`
|
|
156
|
-
- **THEN** it returns a `Date` whose time is greater than the current time
|
|
157
|
-
|
|
158
|
-
#### Scenario: getCookieExpiry returns null when 1PSIDTS has no positive expires
|
|
159
|
-
- **WHEN** `getCookieExpiry` is called with cookies that include `__Secure-1PSIDTS` having `expires <= 0`
|
|
160
|
-
- **THEN** it returns `null`
|
|
161
|
-
|
|
162
|
-
#### Scenario: getCookieExpiry returns null for empty cookies
|
|
163
|
-
- **WHEN** `getCookieExpiry([])` is called
|
|
164
|
-
- **THEN** it returns `null`
|
|
165
|
-
|
|
166
|
-
### Requirement: ProfileAuthManager.ensureAuthenticated returns cookies or throws
|
|
167
|
-
The `ProfileAuthManager.ensureAuthenticated(profileName?)` method MUST resolve the profile name (provided value, or the configured default), validate it, and check that the profile has valid cookies. If `profileManager.hasValidCookies(name)` returns `false`, the method MUST throw an `AuthenticationError` whose message contains `No valid session for profile '<name>'` and the substring `gemiterm login`. If the cookies are valid, the method MUST return the result of `cookieStorageService.loadCookiesForProfile(name)`.
|
|
168
|
-
|
|
169
|
-
#### Scenario: Returns cookies for a profile with valid session
|
|
170
|
-
- **WHEN** `ensureAuthenticated("default")` is called and the default profile has fresh cookies
|
|
171
|
-
- **THEN** it returns a `LoadedCookies` object whose `secure_1psid` and `secure_1psidts` match the stored values
|
|
172
|
-
|
|
173
|
-
#### Scenario: Throws AuthenticationError when no valid cookies
|
|
174
|
-
- **WHEN** `ensureAuthenticated("default")` is called and the profile has no cookies
|
|
175
|
-
- **THEN** it throws an `AuthenticationError` whose message contains `No valid session`
|
|
176
|
-
|
|
177
|
-
#### Scenario: Throws AuthenticationError with expired cookies
|
|
178
|
-
- **WHEN** `ensureAuthenticated("default")` is called and the profile's cookies are expired
|
|
179
|
-
- **THEN** it throws an `AuthenticationError` whose message contains `No valid session`
|
|
180
|
-
|
|
181
|
-
#### Scenario: Uses default profile when none specified
|
|
182
|
-
- **WHEN** `ensureAuthenticated()` is called with no argument and the configured default profile has valid cookies
|
|
183
|
-
- **THEN** it returns the loaded cookies for the default profile
|
|
184
|
-
|
|
185
|
-
#### Scenario: Throws on invalid profile name
|
|
186
|
-
- **WHEN** `ensureAuthenticated("bad name!")` is called
|
|
187
|
-
- **THEN** it throws an error whose message contains `invalid characters`
|
|
188
|
-
|
|
189
|
-
### Requirement: ProfileAuthManager.getActiveProfiles filters to valid sessions
|
|
190
|
-
The `ProfileAuthManager.getActiveProfiles()` method MUST return the names of all profiles that have valid cookies. The method MUST return `string[]`, MUST be empty when no profiles exist, and MUST NOT include profiles whose cookies are expired or invalid.
|
|
191
|
-
|
|
192
|
-
#### Scenario: Returns only profiles with valid cookies
|
|
193
|
-
- **WHEN** two profiles exist, one with fresh cookies and one with expired cookies
|
|
194
|
-
- **THEN** `getActiveProfiles()` returns a list containing the fresh profile and not the expired one
|
|
195
|
-
|
|
196
|
-
#### Scenario: Returns empty array when no profiles have valid cookies
|
|
197
|
-
- **WHEN** profiles exist but all have expired cookies
|
|
198
|
-
- **THEN** `getActiveProfiles()` returns `[]`
|
|
199
|
-
|
|
200
|
-
#### Scenario: Returns empty array when no profiles exist
|
|
201
|
-
- **WHEN** no profiles are configured
|
|
202
|
-
- **THEN** `getActiveProfiles()` returns `[]`
|
|
203
|
-
|
|
204
|
-
### Requirement: ProfileAuthManager.findProfileForConversation returns the profile that owns the conversation
|
|
205
|
-
|
|
206
|
-
The `ProfileAuthManager.findProfileForConversation(conversationId)` method MUST iterate over all profiles in `profileManager.list()` order and, for each, call `geminiClient.profileHasConversation(name, conversationId)` to check whether the conversation appears in that profile's chat list. It MUST return the name of the first profile whose helper returns `true`. When no profile owns the conversation, the method MUST return `null`. The `conversationId` argument MUST be passed to the lookup helper; it MUST NOT be ignored. The method MUST NOT throw on a missing or unknown conversation id.
|
|
207
|
-
|
|
208
|
-
#### Scenario: Returns the profile that owns the conversation
|
|
209
|
-
- **WHEN** `findProfileForConversation("conv-123")` is called and conversation `conv-123` exists in profile `work` but not in profile `personal`
|
|
210
|
-
- **THEN** the method returns the string `"work"`
|
|
211
|
-
|
|
212
|
-
#### Scenario: Returns null when conversation is not in any profile
|
|
213
|
-
- **WHEN** `findProfileForConversation("conv-456")` is called and conversation `conv-456` does not exist in any profile's chat list
|
|
214
|
-
- **THEN** the method returns `null`
|
|
215
|
-
|
|
216
|
-
#### Scenario: Returns null when no profiles exist
|
|
217
|
-
- **WHEN** `findProfileForConversation("conv-789")` is called and no profiles are configured
|
|
218
|
-
- **THEN** the method returns `null`
|
|
219
|
-
|
|
220
|
-
#### Scenario: Returns first profile in list order when multiple profiles report ownership
|
|
221
|
-
- **WHEN** `findProfileForConversation("conv-shared")` is called and conversation `conv-shared` exists in both profile `profile1` and profile `profile3`, and `profileManager.list()` returns `["profile1", "profile2", "profile3"]` in that order
|
|
222
|
-
- **THEN** the method returns the string `"profile1"` (the first profile in list order that reports ownership)
|
|
223
|
-
|
|
224
|
-
### Requirement: PlaywrightCliDriver auto-detects between direct and bunx strategies
|
|
225
|
-
The `PlaywrightCliDriver` constructor MUST default the underlying `PlaywrightRunner` to a `BunPlaywrightRunner` with strategy `"direct"` (invoking the `playwright-cli` binary directly). The first time `runCli` is needed, the driver MUST probe the available install: it MUST first try `playwright-cli --version`; if that exits with code 0, the strategy is `direct`. If the direct probe fails, the driver MUST try `bunx @playwright/cli --version`; on success, the strategy is `bunx`. If both probes fail, the driver MUST emit a warning and `runCli` MUST continue to throw `PlaywrightCliError` on any call. The probe MUST time out after 5000 ms (`PROBE_TIMEOUT_MS`) per attempt.
|
|
226
|
-
|
|
227
|
-
#### Scenario: Strategy exposes the runner's resolved strategy
|
|
228
|
-
- **WHEN** the driver is constructed with a runner of strategy `"direct"`
|
|
229
|
-
- **THEN** `driver.strategy` returns `"direct"`; the same holds for strategy `"bunx"`
|
|
230
|
-
|
|
231
|
-
#### Scenario: Driver picks up the direct runner when probe succeeds
|
|
232
|
-
- **WHEN** the constructor is given no runner and the live probe of `playwright-cli --version` returns exit code 0
|
|
233
|
-
- **THEN** `driver.strategy` is `"direct"`
|
|
234
|
-
|
|
235
|
-
### Requirement: PlaywrightCliDriver exposes the standard browser-driver methods
|
|
236
|
-
The `PlaywrightCliDriver` class MUST expose the following async methods, each of which MUST construct its argument array with `withSession(session, [...])` (which prepends `-s=<session>`) for the session-scoped methods:
|
|
237
|
-
- `runCli(args)` returns the trimmed `stdout` string on success, or throws `PlaywrightCliError(command, exitCode, stderr)` on non-zero exit codes.
|
|
238
|
-
- `openHeaded(url, profile, session?)` builds the args `["open", "--browser=chromium", "--headed", "--persistent", "--profile=<profileDir>", url]`, prepending `-s=<session>` when supplied, and resolves the profile dir through the injected `profileDirResolver` (defaulting to `getProfileDir(profile)`).
|
|
239
|
-
- `evalJs(session, expression)` invokes `playwright-cli eval <expression> --raw` and returns the raw output string.
|
|
240
|
-
- `cookieList(session)` invokes `playwright-cli cookie-list --json` and returns a parsed `Cookie[]` (the JSON-array path is preferred; a non-JSON, non-array response yields `[]`).
|
|
241
|
-
- `stateSave(session, path)` and `stateLoad(session, path)` invoke `playwright-cli state-save <path>` and `state-load <path>` respectively.
|
|
242
|
-
- `closeSession(session)` invokes `playwright-cli close`; if the error is a `PlaywrightCliError` whose message contains `not found` (case-insensitive), the call resolves successfully instead of throwing.
|
|
243
|
-
- `closeAll()` invokes `playwright-cli close-all` with no session flag.
|
|
244
|
-
|
|
245
|
-
#### Scenario: runCli resolves with stdout on success
|
|
246
|
-
- **WHEN** `runCli(["--version"])` is called and the underlying runner returns exit code 0 with stdout `v1.2.3`
|
|
247
|
-
- **THEN** the method resolves with the string `v1.2.3`
|
|
248
|
-
|
|
249
|
-
#### Scenario: runCli throws PlaywrightCliError on non-zero exit
|
|
250
|
-
- **WHEN** `runCli(["nonexistent-command"])` is called and the underlying runner returns exit code 1
|
|
251
|
-
- **THEN** the method rejects with a `PlaywrightCliError` whose message contains the command name and the stderr text
|
|
252
|
-
|
|
253
|
-
#### Scenario: openHeaded builds expected args with the session flag
|
|
254
|
-
- **WHEN** `openHeaded("https://gemini.google.com/app", "p1", "s1")` is called
|
|
255
|
-
- **THEN** the runner is invoked once with args beginning with `-s=s1`, containing `open`, `https://gemini.google.com/app`, `--browser=chromium`, `--headed`, `--persistent`, and `--profile=<dir>/p1` as the last few flags
|
|
256
|
-
|
|
257
|
-
#### Scenario: evalJs includes --raw and returns stdout
|
|
258
|
-
- **WHEN** `evalJs("test-session", "() => document.title")` is called
|
|
259
|
-
- **THEN** the runner is invoked with `["-s=test-session", "eval", "() => document.title", "--raw"]` and the result is the runner's stdout
|
|
260
|
-
|
|
261
|
-
#### Scenario: cookieList parses JSON array output
|
|
262
|
-
- **WHEN** `cookieList("sess1")` is called and the runner returns a JSON array of cookie objects
|
|
263
|
-
- **THEN** the method resolves with a `Cookie[]` whose length and fields match the input
|
|
264
|
-
|
|
265
|
-
#### Scenario: cookieList returns empty array for non-JSON output
|
|
266
|
-
- **WHEN** `cookieList("sess1")` is called and the runner returns the string `not-json`
|
|
267
|
-
- **THEN** the method resolves with `[]`
|
|
268
|
-
|
|
269
|
-
#### Scenario: closeSession swallows "not found" errors
|
|
270
|
-
- **WHEN** `closeSession("sess1")` is called and the runner returns exit code 1 with stderr `session not found`
|
|
271
|
-
- **THEN** the method resolves successfully (does not throw)
|
|
272
|
-
|
|
273
|
-
#### Scenario: closeSession propagates other PlaywrightCliError failures
|
|
274
|
-
- **WHEN** `closeSession("sess1")` is called and the runner returns exit code 1 with stderr `something else failed`
|
|
275
|
-
- **THEN** the method rejects with a `PlaywrightCliError`
|
|
276
|
-
|
|
277
|
-
#### Scenario: closeAll runs without a session flag
|
|
278
|
-
- **WHEN** `closeAll()` is called
|
|
279
|
-
- **THEN** the runner is invoked exactly once with args equal to `["close-all"]` and no `-s=` flag
|
|
280
|
-
|
|
281
|
-
### Requirement: AuthCommand is registered as the "auth" command
|
|
282
|
-
The `AuthCommand` class MUST be registered with the CLI as the command named `"auth"`. Its `description` field MUST be the string `"Authenticate with Google Gemini"`. The `name` and `description` properties are part of the public `CliCommand` contract.
|
|
283
|
-
|
|
284
|
-
#### Scenario: AuthCommand metadata
|
|
285
|
-
- **WHEN** an `AuthCommand` instance is constructed
|
|
286
|
-
- **THEN** `command.name === "auth"` and `command.description === "Authenticate with Google Gemini"`
|
|
287
|
-
|
|
288
|
-
### Requirement: AuthCommand shows usage on --help
|
|
289
|
-
When the `auth` command is invoked with `--help` or `-h` as any argument, the command MUST print a usage block to the console containing the substring `gemiterm auth`, the description `Authenticate with Google Gemini.`, and the option row ` -h, --help Show this help message`. The command MUST NOT proceed to the authentication flow when help is requested.
|
|
290
|
-
|
|
291
|
-
#### Scenario: --help shows usage and skips auth
|
|
292
|
-
- **WHEN** the user invokes `gemiterm auth --help`
|
|
293
|
-
- **THEN** the console output contains the substrings `gemiterm auth`, `Authenticate with Google Gemini.`, and `-h, --help`, and the auth flow is not entered (no driver or monitor interactions)
|
|
294
|
-
|
|
295
|
-
#### Scenario: -h shows usage and skips auth
|
|
296
|
-
- **WHEN** the user invokes `gemiterm auth -h`
|
|
297
|
-
- **THEN** the same usage block is printed and the auth flow is not entered
|
|
298
|
-
|
|
299
|
-
### Requirement: AuthCommand auto-creates the default profile on first run
|
|
300
|
-
When `AuthCommand.execute` is called and `listProfiles()` returns zero profiles, the command MUST create the default profile (using `getDefaultProfileName()`) and call `authenticateWithProfile` with `createFirst=true`. When exactly one profile exists, the command MUST authenticate that profile with `createFirst=false`.
|
|
301
|
-
|
|
302
|
-
#### Scenario: First run authenticates the default profile and creates it
|
|
303
|
-
- **WHEN** `auth` is invoked and no profiles exist
|
|
304
|
-
- **THEN** `authenticateWithProfile` is called with the default profile name and `createFirst=true`
|
|
305
|
-
|
|
306
|
-
#### Scenario: Single-profile setup authenticates that profile without creating
|
|
307
|
-
- **WHEN** `auth` is invoked and only one profile exists
|
|
308
|
-
- **THEN** `authenticateWithProfile` is called with that profile's name and `createFirst=false`
|
|
309
|
-
|
|
310
|
-
### Requirement: AuthCommand shows the multi-profile management menu when 2+ profiles exist
|
|
311
|
-
When `AuthCommand.execute` is called and `listProfiles()` returns 2 or more profile names, the command MUST render a `Profile Management` section that includes:
|
|
312
|
-
- A profile table rendered by `formatProfileTable` containing the statuses of the existing profiles.
|
|
313
|
-
- A list of options, each formatted as ` [X] <label>`, with keys and labels: `[A] Add new profile`, `[D] Delete profile`, `[S] Set default`, `[R] Rename profile`, `[X] Exit and continue with current default`.
|
|
314
|
-
|
|
315
|
-
The command MUST prompt the user with the text `Select an option` and route the response (case-insensitive, trimmed) to the matching handler. Any unrecognized key (or `X`) MUST be treated as `Exit` and the command MUST print the substring `Continuing with current default profile.` and return without invoking the auth flow.
|
|
316
|
-
|
|
317
|
-
#### Scenario: Multi-profile run opens the menu
|
|
318
|
-
- **WHEN** `auth` is invoked and multiple profiles exist
|
|
319
|
-
- **THEN** `showProfileMenu` is called with the list of profile names and a `ProfileManager` instance
|
|
320
|
-
|
|
321
|
-
#### Scenario: Menu renders all five options
|
|
322
|
-
- **WHEN** the menu is rendered for any 2+ profile setup
|
|
323
|
-
- **THEN** the combined output contains the substrings `[A] Add new profile`, `[D] Delete profile`, `[S] Set default`, `[R] Rename profile`, and `[X] Exit`
|
|
324
|
-
|
|
325
|
-
#### Scenario: Unknown option exits to default
|
|
326
|
-
- **WHEN** the user enters `Z` (or any non-matching key) at the menu prompt
|
|
327
|
-
- **THEN** the command prints `Continuing with current default profile.` and returns without calling `authenticateWithProfile`
|
|
328
|
-
|
|
329
|
-
#### Scenario: X option exits to default
|
|
330
|
-
- **WHEN** the user enters `X` at the menu prompt
|
|
331
|
-
- **THEN** the command prints `Continuing with current default profile.` and returns without calling `authenticateWithProfile`
|
|
332
|
-
|
|
333
|
-
### Requirement: AuthCommand "Add new profile" menu option
|
|
334
|
-
When the user selects `A` at the menu prompt, the command MUST prompt for a profile name, validate it via `validateProfileName`, reject duplicates with a `GemitermError` whose message contains `already exists`, create the profile with `profileManager.create(name)`, and return the new profile name to the caller (which triggers `authenticateWithProfile` for that name).
|
|
335
|
-
|
|
336
|
-
#### Scenario: Adding a new profile triggers auth with the new name
|
|
337
|
-
- **WHEN** the user selects `A` and enters a fresh profile name
|
|
338
|
-
- **THEN** the new profile is created and `authenticateWithProfile` is called with the new name
|
|
339
|
-
|
|
340
|
-
#### Scenario: Duplicate profile name is rejected
|
|
341
|
-
- **WHEN** the user selects `A` and enters a name that already exists
|
|
342
|
-
- **THEN** the command throws a `GemitermError` whose message contains `already exists`
|
|
343
|
-
|
|
344
|
-
#### Scenario: Invalid profile name is rejected
|
|
345
|
-
- **WHEN** the user selects `A` and enters a name with invalid characters (e.g. `bad name!!`)
|
|
346
|
-
- **THEN** the command throws an error whose message contains `invalid characters`
|
|
347
|
-
|
|
348
|
-
### Requirement: AuthCommand "Delete profile" menu option
|
|
349
|
-
When the user selects `D` at the menu prompt, the command MUST prompt for the profile name to delete, reject unknown profiles with a `GemitermError` whose message contains `does not exist`, then prompt `Delete profile '<name>'? [y/N]` and only delete the profile (via `profileManager.delete`) when the answer starts with `y` (case-insensitive). Any other answer MUST cancel the delete (printing `Cancelled.`) and return without invoking the auth flow.
|
|
350
|
-
|
|
351
|
-
#### Scenario: Deleting an existing profile removes it
|
|
352
|
-
- **WHEN** the user selects `D`, enters an existing profile name, and confirms with a `y`-prefixed answer
|
|
353
|
-
- **THEN** `profileManager.delete(name)` is called and the command returns without authenticating
|
|
354
|
-
|
|
355
|
-
#### Scenario: Deleting a non-existent profile throws
|
|
356
|
-
- **WHEN** the user selects `D` and enters a profile name not in `listProfiles()`
|
|
357
|
-
- **THEN** the command throws a `GemitermError` whose message contains `does not exist`
|
|
358
|
-
|
|
359
|
-
#### Scenario: Cancelling the delete confirmation preserves the profile
|
|
360
|
-
- **WHEN** the user selects `D`, enters an existing profile name, and answers the confirmation with `n` (or anything not starting with `y`)
|
|
361
|
-
- **THEN** the delete is not performed and the command returns without authenticating
|
|
362
|
-
|
|
363
|
-
### Requirement: AuthCommand "Set default" menu option
|
|
364
|
-
When the user selects `S` at the menu prompt, the command MUST prompt for a profile name, reject unknown profiles with a `GemitermError` whose message contains `does not exist`, then call BOTH `profileManager.setDefault(name)` and `setDefaultProfileName(name)`, print the substring `Default profile set to '<name>'.`, and return without invoking the auth flow.
|
|
365
|
-
|
|
366
|
-
#### Scenario: Setting default updates both the manager and the global marker
|
|
367
|
-
- **WHEN** the user selects `S` and enters a valid existing profile name
|
|
368
|
-
- **THEN** `profileManager.setDefault(name)` and `setDefaultProfileName(name)` are both called with the same name
|
|
369
|
-
|
|
370
|
-
#### Scenario: Setting default for unknown profile throws
|
|
371
|
-
- **WHEN** the user selects `S` and enters a profile name not in `listProfiles()`
|
|
372
|
-
- **THEN** the command throws a `GemitermError` whose message contains `does not exist`
|
|
373
|
-
|
|
374
|
-
### Requirement: AuthCommand "Rename profile" menu option
|
|
375
|
-
When the user selects `R` at the menu prompt, the command MUST prompt for the current profile name (rejecting unknown names with a `GemitermError` whose message contains `does not exist`), then prompt for the new name, validate the new name via `validateProfileName`, call `profileManager.rename(oldName, newName)`, and return the new name to the caller (which triggers `authenticateWithProfile` for the new name).
|
|
376
|
-
|
|
377
|
-
#### Scenario: Renaming a profile triggers auth with the new name
|
|
378
|
-
- **WHEN** the user selects `R` and enters a valid existing old name and a valid new name
|
|
379
|
-
- **THEN** `profileManager.rename(oldName, newName)` is called and `authenticateWithProfile` is called with the new name
|
|
380
|
-
|
|
381
|
-
#### Scenario: Renaming with invalid new name throws
|
|
382
|
-
- **WHEN** the user selects `R` and enters a new name that fails `validateProfileName`
|
|
383
|
-
- **THEN** the command throws an error whose message contains `invalid characters`
|
|
384
|
-
|
|
385
|
-
#### Scenario: Renaming an unknown profile throws
|
|
386
|
-
- **WHEN** the user selects `R` and enters an old name not in `listProfiles()`
|
|
387
|
-
- **THEN** the command throws a `GemitermError` whose message contains `does not exist`
|
|
388
|
-
|
|
389
|
-
### Requirement: AuthCommand propagates authentication errors
|
|
390
|
-
When `AuthCommand.authenticateWithProfile` (or its delegates) throws, the command MUST propagate the error to the caller so the process exits non-zero. The error message is the original error's message (e.g. `Browser launch failed` from the integration test fixture).
|
|
391
|
-
|
|
392
|
-
#### Scenario: Auth flow errors are propagated
|
|
393
|
-
- **WHEN** the auth flow rejects with an error whose message is `Browser launch failed`
|
|
394
|
-
- **THEN** `execute` rejects with an error whose message contains `Browser launch failed`
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
## Purpose
|
|
2
|
-
|
|
3
|
-
The CLI entry point for the `gemiterm` application. It provides the executable shebang, parses global flags, registers the 11 top-level commands, prints help and version output, and dispatches subcommand execution to a `CommandRegistry`. The CLI is the only public face of the application and is the process entry point launched by the user.
|
|
4
|
-
|
|
5
|
-
## Requirements
|
|
6
|
-
|
|
7
|
-
### Requirement: Bun Shebang Entry Point
|
|
8
|
-
The CLI source file MUST begin with the shebang line `#!/usr/bin/env bun` so the file is executable directly by the Bun runtime when invoked as a script or compiled binary. The shebang MUST appear on the first line of `src/cli/index.ts`.
|
|
9
|
-
|
|
10
|
-
#### Scenario: Inspecting the shebang line
|
|
11
|
-
- **WHEN** the file `src/cli/index.ts` is read
|
|
12
|
-
- **THEN** the first line is exactly `#!/usr/bin/env bun`
|
|
13
|
-
|
|
14
|
-
### Requirement: Global Flags Parsing
|
|
15
|
-
The system MUST parse global flags from the raw process arguments and expose a `GlobalFlags` object with three boolean fields: `verbose`, `version`, and `help`. The parser MUST recognize `--verbose` and its short alias `-v` as setting `verbose=true`, MUST recognize `--version` as setting `version=true`, and MUST recognize `--help` and its short alias `-h` as setting `help=true` (only when no subcommand has been seen yet). All non-flag arguments MUST be returned as `remaining` args.
|
|
16
|
-
|
|
17
|
-
#### Scenario: Verbose flag sets verbose
|
|
18
|
-
- **WHEN** the user invokes `gemiterm --verbose list`
|
|
19
|
-
- **THEN** `parseGlobalFlags` returns `flags.verbose === true` and `remaining === ["list"]`
|
|
20
|
-
|
|
21
|
-
#### Scenario: Verbose short alias works
|
|
22
|
-
- **WHEN** the user invokes `gemiterm -v list`
|
|
23
|
-
- **THEN** `flags.verbose === true` and `remaining === ["list"]`
|
|
24
|
-
|
|
25
|
-
#### Scenario: Version flag sets version
|
|
26
|
-
- **WHEN** the user invokes `gemiterm --version`
|
|
27
|
-
- **THEN** `flags.version === true` and `remaining === []`
|
|
28
|
-
|
|
29
|
-
#### Scenario: Help flag sets help
|
|
30
|
-
- **WHEN** the user invokes `gemiterm --help`
|
|
31
|
-
- **THEN** `flags.help === true` and `remaining === []`
|
|
32
|
-
|
|
33
|
-
#### Scenario: Help short alias works
|
|
34
|
-
- **WHEN** the user invokes `gemiterm -h`
|
|
35
|
-
- **THEN** `flags.help === true` and `remaining === []`
|
|
36
|
-
|
|
37
|
-
### Requirement: Verbose Flag Enables Debug Logging
|
|
38
|
-
When the global `--verbose` flag is set, the system MUST call `Logger.setVerbose(true)` so that `debug()` output from every `Logger` instance is emitted.
|
|
39
|
-
|
|
40
|
-
#### Scenario: Verbose flag enables debug
|
|
41
|
-
- **WHEN** the user runs `gemiterm --verbose <command>`
|
|
42
|
-
- **THEN** `Logger.setVerbose(true)` is invoked before the command handler runs
|
|
43
|
-
|
|
44
|
-
### Requirement: Version Output From Package Metadata
|
|
45
|
-
When the `--version` global flag is set, the system MUST print `gemiterm v<version>` to stdout and exit the process with code 0, where `<version>` is read from the `version` field of the `package.json` at the project root.
|
|
46
|
-
|
|
47
|
-
#### Scenario: Version output
|
|
48
|
-
- **WHEN** the user invokes `gemiterm --version`
|
|
49
|
-
- **THEN** the process prints a line of the form `gemiterm v2.0.0` to stdout and exits with code 0
|
|
50
|
-
|
|
51
|
-
### Requirement: No-Args Behavior Prints Help
|
|
52
|
-
When the user invokes `gemiterm` with no arguments (after global flag parsing), the system MUST print the help screen via `showHelp(registry)` and exit the process with code 0.
|
|
53
|
-
|
|
54
|
-
#### Scenario: No arguments prints help
|
|
55
|
-
- **WHEN** the user invokes `gemiterm` with no arguments
|
|
56
|
-
- **THEN** `showHelp(registry)` is called and the process exits with code 0
|
|
57
|
-
|
|
58
|
-
### Requirement: Unknown Subcommand Suggestion
|
|
59
|
-
When the user invokes a subcommand that is not registered with the `CommandRegistry`, the system MUST print an `Unknown command` message to stderr. If at least one command is registered, the system MUST also print a `Did you mean one of: <names>?` suggestion line listing the registered command names. If no commands are registered, the system MUST print a fallback message directing the user to `--help`. In both cases the system MUST exit with code 1.
|
|
60
|
-
|
|
61
|
-
#### Scenario: Unknown subcommand with registered names
|
|
62
|
-
- **WHEN** the user invokes `gemiterm not-a-command` and the registry contains registered names
|
|
63
|
-
- **THEN** stderr receives `Unknown command: 'not-a-command'` followed by a line starting with `Did you mean one of:` and the process exits with code 1
|
|
64
|
-
|
|
65
|
-
#### Scenario: Unknown subcommand with empty registry
|
|
66
|
-
- **WHEN** the user invokes `gemiterm not-a-command` and the registry is empty
|
|
67
|
-
- **THEN** stderr receives `Unknown command: 'not-a-command'` and a fallback suggesting `--help`, and the process exits with code 1
|
|
68
|
-
|
|
69
|
-
### Requirement: CommandRegistry Public Surface
|
|
70
|
-
The `CommandRegistry` class MUST expose a `register(commandName, handler)` method that throws if the name is already registered, a `getHandler(commandName)` method that returns the handler or `undefined`, a `has(commandName)` boolean check, a `getRegisteredNames()` method returning all registered names, and a `registerAllCommands()` method that wires up the full default command set.
|
|
71
|
-
|
|
72
|
-
#### Scenario: Register a new command
|
|
73
|
-
- **WHEN** `register("x", handler)` is called on a registry that does not yet contain `"x"`
|
|
74
|
-
- **THEN** `has("x")` returns `true` and `getHandler("x")` returns the registered handler
|
|
75
|
-
|
|
76
|
-
#### Scenario: Re-registering the same name throws
|
|
77
|
-
- **WHEN** `register("x", handler)` is called twice with the same name
|
|
78
|
-
- **THEN** the second call throws an error whose message mentions the duplicate name
|
|
79
|
-
|
|
80
|
-
#### Scenario: getHandler for missing name returns undefined
|
|
81
|
-
- **WHEN** `getHandler("missing")` is called on a registry that does not contain it
|
|
82
|
-
- **THEN** the result is `undefined`
|
|
83
|
-
|
|
84
|
-
#### Scenario: getRegisteredNames returns the registered keys
|
|
85
|
-
- **WHEN** commands `"a"`, `"b"`, and `"c"` are registered
|
|
86
|
-
- **THEN** `getRegisteredNames()` returns a list containing all three names
|
|
87
|
-
|
|
88
|
-
### Requirement: Default Command Set Registration
|
|
89
|
-
`CommandRegistry.registerAllCommands()` MUST explicitly register exactly 11 commands: `auth`, `profile`, `status`, `list`, `fetch`, `continue`, `new`, `delete`, `export`, `export-all`, and `install-browser`. Each registration MUST be performed by direct, hand-written `register()` calls — the system MUST NOT use any auto-discovery or glob-based mechanism to populate the registry.
|
|
90
|
-
|
|
91
|
-
#### Scenario: After registerAllCommands the registry has 11 entries
|
|
92
|
-
- **WHEN** a fresh `CommandRegistry` has `registerAllCommands()` invoked
|
|
93
|
-
- **THEN** `getRegisteredNames()` returns exactly the names `auth`, `profile`, `status`, `list`, `fetch`, `continue`, `new`, `delete`, `export`, `export-all`, and `install-browser`
|
|
94
|
-
|
|
95
|
-
#### Scenario: Each registered name resolves to a handler
|
|
96
|
-
- **WHEN** `registerAllCommands()` has been called
|
|
97
|
-
- **THEN** `getHandler(name)` returns a non-`undefined` `CliCommand` for each of the 11 names
|
|
98
|
-
|
|
99
|
-
### Requirement: Help Output
|
|
100
|
-
The system MUST expose a `showHelp(registry?)` function in `src/cli/commands/help.ts` that, when a registry is provided, prints the registered command names with their descriptions, then prints the three global options (`--version`, `--help, -h`, `--verbose, -v`) with their descriptions, and finally prints a hint to run `gemiterm <command> --help` for more information.
|
|
101
|
-
|
|
102
|
-
#### Scenario: Help with a registry
|
|
103
|
-
- **WHEN** `showHelp(registry)` is called with a registry containing 11 commands
|
|
104
|
-
- **THEN** the output contains the application name `GemiTerm`, a `Commands:` section listing all 11 names and their descriptions, a `Global Options:` section listing the three global flags, and the hint about `<command> --help`
|
|
105
|
-
|
|
106
|
-
#### Scenario: Help with no registry
|
|
107
|
-
- **WHEN** `showHelp()` is called with no arguments
|
|
108
|
-
- **THEN** the output still includes the application name, the `Global Options:` section, and the command-specific help hint (the `Commands:` section is empty)
|