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.
Files changed (155) hide show
  1. package/README.md +32 -9
  2. package/package.json +13 -4
  3. package/src/cli/command-registry.ts +66 -62
  4. package/src/cli/commands/continue-command.ts +75 -1
  5. package/src/cli/commands/install-skills-command.ts +35 -0
  6. package/src/cli/commands/new-command.ts +62 -0
  7. package/src/cli/index.ts +19 -44
  8. package/src/cli/utils/long-arg-guard.ts +28 -0
  9. package/src/cli/utils/prompt-file.ts +26 -0
  10. package/src/infrastructure/cli-parser.ts +77 -0
  11. package/src/infrastructure/io.ts +9 -0
  12. package/src/services/skills-cli-driver.ts +58 -0
  13. package/src/services/skills-service.ts +52 -0
  14. package/.env +0 -2
  15. package/.github/workflows/manual-release.yml +0 -34
  16. package/.github/workflows/release.yml +0 -198
  17. package/.github/workflows/test.yml +0 -29
  18. package/.opencode/commands/opsx-apply.md +0 -152
  19. package/.opencode/commands/opsx-archive.md +0 -157
  20. package/.opencode/commands/opsx-explore.md +0 -169
  21. package/.opencode/commands/opsx-propose.md +0 -104
  22. package/.opencode/commands/opsx-sync.md +0 -140
  23. package/.opencode/commands/search-intelligently.md +0 -9
  24. package/.opencode/opencode.json +0 -20
  25. package/.opencode/skills/intellisearch/.version +0 -1
  26. package/.opencode/skills/intellisearch/SKILL.md +0 -84
  27. package/.opencode/skills/intellisearch/references/brave-search.md +0 -67
  28. package/.opencode/skills/intellisearch/references/ddg-search.md +0 -79
  29. package/.opencode/skills/intellisearch/references/deepwiki-tools.md +0 -246
  30. package/.opencode/skills/intellisearch/references/examples.md +0 -97
  31. package/.opencode/skills/intellisearch/references/gh-cli.md +0 -143
  32. package/.opencode/skills/intellisearch/references/google-search.md +0 -40
  33. package/.opencode/skills/intellisearch/references/search-workflow.md +0 -143
  34. package/.opencode/skills/intellisearch/references/workflow.md +0 -115
  35. package/.opencode/skills/openspec-apply-change/SKILL.md +0 -159
  36. package/.opencode/skills/openspec-archive-change/SKILL.md +0 -117
  37. package/.opencode/skills/openspec-explore/SKILL.md +0 -287
  38. package/.opencode/skills/openspec-propose/SKILL.md +0 -111
  39. package/.opencode/skills/openspec-sync-specs/SKILL.md +0 -147
  40. package/.serena/project.yml +0 -133
  41. package/AGENTS.md +0 -136
  42. package/CHANGELOG.md +0 -3
  43. package/bun.lock +0 -105
  44. package/docs/INSTALL.md +0 -185
  45. package/docs/PLAYWRIGHT_CLI_API.md +0 -306
  46. package/docs/python-release-for-reference.md +0 -175
  47. package/examples/sample_json_export.json +0 -32
  48. package/examples/sample_markdown_export.md +0 -221
  49. package/examples/usage_examples.sh +0 -222
  50. package/install.ps1 +0 -264
  51. package/install.sh +0 -275
  52. package/openspec/MAESTRO_MIGRATION.md +0 -180
  53. package/openspec/changes/archive/2026-06-08-command-spec-conformance/.openspec.yaml +0 -2
  54. package/openspec/changes/archive/2026-06-08-command-spec-conformance/design.md +0 -194
  55. package/openspec/changes/archive/2026-06-08-command-spec-conformance/proposal.md +0 -52
  56. package/openspec/changes/archive/2026-06-08-command-spec-conformance/specs/multi-profile-conversations/spec.md +0 -133
  57. package/openspec/changes/archive/2026-06-08-command-spec-conformance/tasks.md +0 -95
  58. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/.openspec.yaml +0 -2
  59. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/design.md +0 -28
  60. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/proposal.md +0 -22
  61. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/specs/interactive-prompt-loop/spec.md +0 -35
  62. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/tasks.md +0 -28
  63. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/.openspec.yaml +0 -2
  64. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/design.md +0 -323
  65. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/proposal.md +0 -122
  66. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/specs/no-capability-changes/spec.md +0 -29
  67. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/tasks.md +0 -172
  68. package/openspec/changes/archive/2026-06-09-v2-install-migration/.openspec.yaml +0 -2
  69. package/openspec/changes/archive/2026-06-09-v2-install-migration/design.md +0 -241
  70. package/openspec/changes/archive/2026-06-09-v2-install-migration/proposal.md +0 -41
  71. package/openspec/changes/archive/2026-06-09-v2-install-migration/specs/v2-installer/spec.md +0 -205
  72. package/openspec/changes/archive/2026-06-09-v2-install-migration/tasks.md +0 -97
  73. package/openspec/changes/commander-cli-parser/.openspec.yaml +0 -2
  74. package/openspec/changes/commander-cli-parser/design.md +0 -107
  75. package/openspec/changes/commander-cli-parser/proposal.md +0 -31
  76. package/openspec/changes/commander-cli-parser/specs/cli/spec.md +0 -48
  77. package/openspec/changes/commander-cli-parser/tasks.md +0 -33
  78. package/openspec/changes/cross-platform-build-and-ci/.openspec.yaml +0 -2
  79. package/openspec/changes/cross-platform-build-and-ci/design.md +0 -213
  80. package/openspec/changes/cross-platform-build-and-ci/proposal.md +0 -50
  81. package/openspec/changes/cross-platform-build-and-ci/specs/path-and-file-mediation/spec.md +0 -335
  82. package/openspec/changes/cross-platform-build-and-ci/specs/release-pipeline/spec.md +0 -95
  83. package/openspec/changes/cross-platform-build-and-ci/tasks.md +0 -164
  84. package/openspec/config.yaml +0 -20
  85. package/openspec/specs/auth/spec.md +0 -394
  86. package/openspec/specs/cli/spec.md +0 -108
  87. package/openspec/specs/commands/spec.md +0 -409
  88. package/openspec/specs/configuration/spec.md +0 -130
  89. package/openspec/specs/conversations/spec.md +0 -139
  90. package/openspec/specs/domain-model/spec.md +0 -128
  91. package/openspec/specs/formatters/spec.md +0 -158
  92. package/openspec/specs/install-browser/spec.md +0 -146
  93. package/openspec/specs/interactive-prompt-loop/spec.md +0 -41
  94. package/openspec/specs/logger/spec.md +0 -71
  95. package/openspec/specs/mediator/spec.md +0 -72
  96. package/openspec/specs/multi-profile-conversations/spec.md +0 -145
  97. package/openspec/specs/path-utils/spec.md +0 -101
  98. package/openspec/specs/profiles/spec.md +0 -94
  99. package/openspec/specs/storage/spec.md +0 -198
  100. package/openspec/specs/testing/spec.md +0 -237
  101. package/openspec/specs/v2-installer/spec.md +0 -205
  102. package/openspec/specs/validators/spec.md +0 -65
  103. package/scripts/build.ts +0 -88
  104. package/scripts/clean-build.ps1 +0 -4
  105. package/scripts/clean-build.sh +0 -5
  106. package/scripts/install-browser.ps1 +0 -5
  107. package/scripts/install-browser.sh +0 -6
  108. package/scripts/lint-path-mediation.ps1 +0 -65
  109. package/scripts/lint-path-mediation.sh +0 -40
  110. package/tests/cli/auth-command.test.ts +0 -220
  111. package/tests/cli/command-registry.test.ts +0 -67
  112. package/tests/cli/continue-command.test.ts +0 -100
  113. package/tests/cli/delete-command.test.ts +0 -128
  114. package/tests/cli/export-all-command.test.ts +0 -375
  115. package/tests/cli/install-browser-command.test.ts +0 -48
  116. package/tests/cli/list-command.test.ts +0 -179
  117. package/tests/cli/new-command.test.ts +0 -139
  118. package/tests/cli/status-command.test.ts +0 -106
  119. package/tests/core/query-handlers.test.ts +0 -187
  120. package/tests/fixtures/auth-fixtures.ts +0 -98
  121. package/tests/fixtures/chat-fixtures.ts +0 -87
  122. package/tests/infrastructure/cli-table.test.ts +0 -107
  123. package/tests/infrastructure/formatters.test.ts +0 -202
  124. package/tests/infrastructure/io.test.ts +0 -197
  125. package/tests/infrastructure/logger.test.ts +0 -74
  126. package/tests/infrastructure/storage.test.ts +0 -335
  127. package/tests/infrastructure/validators.test.ts +0 -67
  128. package/tests/integration/commands/auth.test.ts +0 -186
  129. package/tests/integration/commands/continue.test.ts +0 -130
  130. package/tests/integration/commands/delete.test.ts +0 -140
  131. package/tests/integration/commands/export.test.ts +0 -266
  132. package/tests/integration/commands/fetch.test.ts +0 -238
  133. package/tests/integration/commands/list.test.ts +0 -281
  134. package/tests/integration/commands/profile.test.ts +0 -258
  135. package/tests/integration/commands/status.test.ts +0 -223
  136. package/tests/integration/installer-script-shape.test.ts +0 -67
  137. package/tests/parity/compare-outputs.ts +0 -359
  138. package/tests/parity/test-commands-parity.ps1 +0 -82
  139. package/tests/parity/test-commands-parity.sh +0 -61
  140. package/tests/services/auth-service.test.ts +0 -315
  141. package/tests/services/cookie-monitor.test.ts +0 -229
  142. package/tests/services/cookie-storage-service.test.ts +0 -230
  143. package/tests/services/gemini-client-wrapper.test.ts +0 -688
  144. package/tests/services/install-browser-service.test.ts +0 -83
  145. package/tests/services/playwright-cli-driver.test.ts +0 -468
  146. package/tests/services/profile-auth-manager.test.ts +0 -334
  147. package/tests/services/profile-service.test.ts +0 -224
  148. package/tests/setup.ts +0 -95
  149. package/tests/smoke/smoke.test.ts +0 -52
  150. package/tests/unit/config.test.ts +0 -159
  151. package/tests/unit/errors.test.ts +0 -119
  152. package/tests/unit/formatters.test.ts +0 -316
  153. package/tests/unit/mediator.test.ts +0 -152
  154. package/tests/unit/path-utils.test.ts +0 -289
  155. package/tsconfig.json +0 -16
@@ -1,335 +0,0 @@
1
- import { describe, test, expect, beforeEach, afterEach } from "bun:test";
2
- import { mkdirSync, rmSync, existsSync } from "node:fs";
3
- import { join } from "node:path";
4
- import { tmpdir } from "node:os";
5
- import { CookieStorage, ProfileManager } from "../../src/infrastructure/storage.ts";
6
- import type { Cookie } from "../../src/core/types.ts";
7
-
8
- const TEST_DIR = join(tmpdir(), "gemiterm-test-storage");
9
-
10
- beforeEach(() => {
11
- process.env.GEMITERM_CONFIG_DIR = TEST_DIR;
12
- mkdirSync(TEST_DIR, { recursive: true });
13
- });
14
-
15
- afterEach(() => {
16
- rmSync(TEST_DIR, { recursive: true, force: true });
17
- delete process.env.GEMITERM_CONFIG_DIR;
18
- });
19
-
20
- function makeValidCookies(): Cookie[] {
21
- const farFuture = Math.floor(Date.now() / 1000) + 365 * 24 * 60 * 60;
22
- return [
23
- {
24
- name: "__Secure-1PSID",
25
- value: "test-psid-value",
26
- domain: ".google.com",
27
- path: "/",
28
- expires: farFuture,
29
- httpOnly: true,
30
- secure: true,
31
- sameSite: "Lax",
32
- },
33
- {
34
- name: "__Secure-1PSIDTS",
35
- value: "test-psidts-value",
36
- domain: ".google.com",
37
- path: "/",
38
- expires: farFuture,
39
- httpOnly: true,
40
- secure: true,
41
- sameSite: "Lax",
42
- },
43
- ];
44
- }
45
-
46
- function makeExpiredCookies(): Cookie[] {
47
- const past = Math.floor(Date.now() / 1000) - 30 * 24 * 60 * 60;
48
- return [
49
- {
50
- name: "__Secure-1PSID",
51
- value: "expired-psid",
52
- domain: ".google.com",
53
- path: "/",
54
- expires: past,
55
- httpOnly: true,
56
- secure: true,
57
- sameSite: "Lax",
58
- },
59
- {
60
- name: "__Secure-1PSIDTS",
61
- value: "expired-psidts",
62
- domain: ".google.com",
63
- path: "/",
64
- expires: past,
65
- httpOnly: true,
66
- secure: true,
67
- sameSite: "Lax",
68
- },
69
- ];
70
- }
71
-
72
- describe("CookieStorage", () => {
73
- let storage: CookieStorage;
74
-
75
- beforeEach(() => {
76
- storage = new CookieStorage();
77
- });
78
-
79
- test("saves and loads cookies for a profile", () => {
80
- const cookies = makeValidCookies();
81
- storage.save("test-profile", cookies);
82
-
83
- const loaded = storage.load("test-profile");
84
- expect(loaded).toHaveLength(2);
85
- expect(loaded[0].name).toBe("__Secure-1PSID");
86
- expect(loaded[1].name).toBe("__Secure-1PSIDTS");
87
- });
88
-
89
- test("throws when loading non-existent profile", () => {
90
- expect(() => storage.load("nonexistent")).toThrow("No storage state found");
91
- });
92
-
93
- test("deletes profile directory", () => {
94
- storage.save("to-delete", makeValidCookies());
95
- expect(existsSync(join(TEST_DIR, "profiles", "to-delete"))).toBe(true);
96
-
97
- storage.delete("to-delete");
98
- expect(existsSync(join(TEST_DIR, "profiles", "to-delete"))).toBe(false);
99
- });
100
-
101
- test("delete is a no-op for non-existent profile", () => {
102
- expect(() => storage.delete("nonexistent")).not.toThrow();
103
- });
104
-
105
- test("list returns empty array when no profiles exist", () => {
106
- expect(storage.list()).toEqual([]);
107
- });
108
-
109
- test("list returns saved profile names", () => {
110
- storage.save("alpha", makeValidCookies());
111
- storage.save("beta", makeValidCookies());
112
-
113
- const names = storage.list();
114
- expect(names).toContain("alpha");
115
- expect(names).toContain("beta");
116
- });
117
-
118
- test("overwrites existing cookies on save", () => {
119
- storage.save("overwrite", makeValidCookies());
120
- const newCookies = [
121
- { ...makeValidCookies()[0], value: "new-value" },
122
- { ...makeValidCookies()[1], value: "new-ts-value" },
123
- ];
124
- storage.save("overwrite", newCookies);
125
-
126
- const loaded = storage.load("overwrite");
127
- expect(loaded[0].value).toBe("new-value");
128
- });
129
- });
130
-
131
- describe("ProfileManager", () => {
132
- let manager: ProfileManager;
133
-
134
- beforeEach(() => {
135
- manager = new ProfileManager();
136
- });
137
-
138
- test("create creates profile directory", () => {
139
- manager.create("new-profile");
140
- expect(existsSync(join(TEST_DIR, "profiles", "new-profile"))).toBe(true);
141
- });
142
-
143
- test("create sets first profile as default", () => {
144
- manager.create("first");
145
- expect(manager.getDefault()).toBe("first");
146
- });
147
-
148
- test("create does not change default if profiles already exist", () => {
149
- manager.create("first");
150
- manager.create("second");
151
- expect(manager.getDefault()).toBe("first");
152
- });
153
-
154
- test("create throws if profile already exists", () => {
155
- manager.create("dup");
156
- expect(() => manager.create("dup")).toThrow("already exists");
157
- });
158
-
159
- test("delete removes profile directory and resets default", () => {
160
- manager.create("p1");
161
- manager.create("p2");
162
- manager.setDefault("p1");
163
-
164
- manager.delete("p1");
165
- expect(manager.getDefault()).toBe("p2");
166
- expect(manager.list()).not.toContain("p1");
167
- });
168
-
169
- test("delete resets default to remaining profile", () => {
170
- manager.create("solo");
171
- manager.delete("solo");
172
- expect(manager.list()).toEqual([]);
173
- });
174
-
175
- test("rename moves profile directory", () => {
176
- manager.create("old-name");
177
- manager.rename("old-name", "new-name");
178
-
179
- expect(manager.list()).toContain("new-name");
180
- expect(manager.list()).not.toContain("old-name");
181
- expect(existsSync(join(TEST_DIR, "profiles", "new-name"))).toBe(true);
182
- expect(existsSync(join(TEST_DIR, "profiles", "old-name"))).toBe(false);
183
- });
184
-
185
- test("rename updates default if renamed profile was default", () => {
186
- manager.create("default-profile");
187
- manager.rename("default-profile", "renamed");
188
-
189
- expect(manager.getDefault()).toBe("renamed");
190
- });
191
-
192
- test("rename throws if source does not exist", () => {
193
- expect(() => manager.rename("nope", "dest")).toThrow("does not exist");
194
- });
195
-
196
- test("rename throws if destination already exists", () => {
197
- manager.create("a");
198
- manager.create("b");
199
- expect(() => manager.rename("a", "b")).toThrow("already exists");
200
- });
201
-
202
- test("setDefault throws if profile does not exist", () => {
203
- expect(() => manager.setDefault("ghost")).toThrow("does not exist");
204
- });
205
-
206
- test("setDefault sets the default profile name", () => {
207
- manager.create("p1");
208
- manager.create("p2");
209
- manager.setDefault("p2");
210
- expect(manager.getDefault()).toBe("p2");
211
- });
212
-
213
- test("getStatus returns correct data for existing valid profile", () => {
214
- const storage = new CookieStorage();
215
- const mgr = new ProfileManager(storage);
216
- storage.save("active", makeValidCookies());
217
-
218
- const status = mgr.getStatus("active");
219
- expect(status.exists).toBe(true);
220
- expect(status.isActive).toBe(true);
221
- expect(status.expiresAt).not.toBeNull();
222
- expect(status.isDefault).toBe(false);
223
- });
224
-
225
- test("getStatus returns inactive for expired cookies", () => {
226
- const storage = new CookieStorage();
227
- const mgr = new ProfileManager(storage);
228
- storage.save("expired", makeExpiredCookies());
229
-
230
- const status = mgr.getStatus("expired");
231
- expect(status.exists).toBe(true);
232
- expect(status.isActive).toBe(false);
233
- });
234
-
235
- test("getStatus returns active for session cookies (expires: -1)", () => {
236
- const storage = new CookieStorage();
237
- const mgr = new ProfileManager(storage);
238
- const sessionCookies: Cookie[] = [
239
- {
240
- name: "__Secure-1PSID",
241
- value: "psid",
242
- domain: ".google.com",
243
- path: "/",
244
- expires: -1,
245
- httpOnly: true,
246
- secure: true,
247
- sameSite: "Lax",
248
- },
249
- {
250
- name: "__Secure-1PSIDTS",
251
- value: "psidts",
252
- domain: ".google.com",
253
- path: "/",
254
- expires: -1,
255
- httpOnly: true,
256
- secure: true,
257
- sameSite: "Lax",
258
- },
259
- ];
260
- storage.save("session", sessionCookies);
261
-
262
- const status = mgr.getStatus("session");
263
- expect(status.exists).toBe(true);
264
- expect(status.isActive).toBe(true);
265
- expect(status.expiresAt).toBeNull();
266
- });
267
-
268
- test("getStatus returns not exists for missing profile", () => {
269
- const status = manager.getStatus("missing");
270
- expect(status.exists).toBe(false);
271
- expect(status.isActive).toBe(false);
272
- });
273
-
274
- test("getStatus marks the default profile", () => {
275
- manager.create("def");
276
- const status = manager.getStatus("def");
277
- expect(status.isDefault).toBe(true);
278
- });
279
-
280
- test("getAllStatuses returns all profile statuses", () => {
281
- const storage = new CookieStorage();
282
- const mgr = new ProfileManager(storage);
283
- storage.save("active", makeValidCookies());
284
- storage.save("expired", makeExpiredCookies());
285
-
286
- const statuses = mgr.getAllStatuses();
287
- expect(statuses).toHaveLength(2);
288
- const active = statuses.find((s) => s.name === "active")!;
289
- const expired = statuses.find((s) => s.name === "expired")!;
290
- expect(active.isActive).toBe(true);
291
- expect(expired.isActive).toBe(false);
292
- });
293
-
294
- test("hasValidCookies returns true for fresh cookies", () => {
295
- const storage = new CookieStorage();
296
- const mgr = new ProfileManager(storage);
297
- storage.save("fresh", makeValidCookies());
298
-
299
- expect(mgr.hasValidCookies("fresh")).toBe(true);
300
- });
301
-
302
- test("hasValidCookies returns false for expired cookies", () => {
303
- const storage = new CookieStorage();
304
- const mgr = new ProfileManager(storage);
305
- storage.save("stale", makeExpiredCookies());
306
-
307
- expect(mgr.hasValidCookies("stale")).toBe(false);
308
- });
309
-
310
- test("hasValidCookies returns false for missing profile", () => {
311
- expect(manager.hasValidCookies("nope")).toBe(false);
312
- });
313
-
314
- test("loadCookiesForApi returns cookie values", () => {
315
- const storage = new CookieStorage();
316
- const mgr = new ProfileManager(storage);
317
- storage.save("api-test", makeValidCookies());
318
-
319
- const result = mgr.loadCookiesForApi("api-test");
320
- expect(result.secure1psid).toBe("test-psid-value");
321
- expect(result.secure1psidts).toBe("test-psidts-value");
322
- });
323
-
324
- test("loadCookiesForApi throws for expired cookies", () => {
325
- const storage = new CookieStorage();
326
- const mgr = new ProfileManager(storage);
327
- storage.save("expired-api", makeExpiredCookies());
328
-
329
- expect(() => mgr.loadCookiesForApi("expired-api")).toThrow("expired");
330
- });
331
-
332
- test("loadCookiesForApi throws for missing profile", () => {
333
- expect(() => manager.loadCookiesForApi("ghost")).toThrow("No storage state found");
334
- });
335
- });
@@ -1,67 +0,0 @@
1
- import { describe, test, expect } from "bun:test";
2
- import {
3
- validateConversationId,
4
- parseIsoDate,
5
- validateProfileName,
6
- } from "../../src/infrastructure/validators.ts";
7
- import { GemitermError } from "../../src/core/errors.ts";
8
-
9
- describe("validateConversationId", () => {
10
- test("does not throw for a valid non-empty id", () => {
11
- expect(() => validateConversationId("abc123")).not.toThrow();
12
- });
13
-
14
- test("throws for empty string", () => {
15
- expect(() => validateConversationId("")).toThrow(GemitermError);
16
- expect(() => validateConversationId("")).toThrow("Conversation ID must not be empty");
17
- });
18
-
19
- test("throws for whitespace-only string", () => {
20
- expect(() => validateConversationId(" ")).toThrow(GemitermError);
21
- });
22
- });
23
-
24
- describe("parseIsoDate", () => {
25
- test("returns timestamp for valid ISO date", () => {
26
- const ts = parseIsoDate("2025-01-15T10:30:00.000Z", "createdAt");
27
- expect(typeof ts).toBe("number");
28
- expect(ts).toBe(new Date("2025-01-15T10:30:00.000Z").getTime());
29
- });
30
-
31
- test("throws for invalid date string", () => {
32
- expect(() => parseIsoDate("not-a-date", "field")).toThrow(GemitermError);
33
- expect(() => parseIsoDate("not-a-date", "field")).toThrow("Invalid ISO date for 'field'");
34
- });
35
-
36
- test("includes field name in error message", () => {
37
- expect(() => parseIsoDate("bad", "updatedAt")).toThrow("'updatedAt'");
38
- });
39
- });
40
-
41
- describe("validateProfileName", () => {
42
- test("does not throw for valid names", () => {
43
- expect(() => validateProfileName("default")).not.toThrow();
44
- expect(() => validateProfileName("my-profile")).not.toThrow();
45
- expect(() => validateProfileName("profile_2")).not.toThrow();
46
- expect(() => validateProfileName("ABC123")).not.toThrow();
47
- });
48
-
49
- test("throws for empty string", () => {
50
- expect(() => validateProfileName("")).toThrow(GemitermError);
51
- expect(() => validateProfileName("")).toThrow("Profile name must not be empty");
52
- });
53
-
54
- test("throws for whitespace-only string", () => {
55
- expect(() => validateProfileName(" ")).toThrow(GemitermError);
56
- });
57
-
58
- test("throws for name with spaces", () => {
59
- expect(() => validateProfileName("my profile")).toThrow(GemitermError);
60
- });
61
-
62
- test("throws for name with special characters", () => {
63
- expect(() => validateProfileName("profile@bad")).toThrow(GemitermError);
64
- expect(() => validateProfileName("pro/ject")).toThrow(GemitermError);
65
- expect(() => validateProfileName("pro.file")).toThrow(GemitermError);
66
- });
67
- });
@@ -1,186 +0,0 @@
1
- import { describe, test, expect, mock, beforeEach, afterEach, spyOn } from "bun:test";
2
- import { AuthCommand } from "../../../src/cli/commands/auth-command.ts";
3
- import { Mediator } from "../../../src/core/mediator.ts";
4
- import type { CliCommandContext } from "../../../src/cli/command-registry.ts";
5
- import { setupTestConfig, teardownTestConfig } from "../../setup.ts";
6
- import { createMockCookies, mockProfileDir } from "../../fixtures/auth-fixtures.ts";
7
- import * as configModule from "../../../src/infrastructure/config.ts";
8
-
9
- describe("auth command integration", () => {
10
- let command: AuthCommand;
11
- let context: CliCommandContext;
12
- let logSpy: ReturnType<typeof spyOn>;
13
- let originalEnv: Record<string, string | undefined>;
14
-
15
- beforeEach(() => {
16
- command = new AuthCommand();
17
- context = { verbose: false, mediator: new Mediator() };
18
- logSpy = spyOn(console, "log").mockImplementation(() => {});
19
- originalEnv = {
20
- GEMITERM_CONFIG_DIR: process.env.GEMITERM_CONFIG_DIR,
21
- };
22
- const configDir = setupTestConfig("auth-integration");
23
- spyOn(configModule, "getConfigDir").mockReturnValue(configDir);
24
- spyOn(configModule, "ensureConfigDir").mockImplementation(() => {});
25
- spyOn(configModule, "getDefaultProfileName").mockReturnValue("default");
26
- });
27
-
28
- afterEach(() => {
29
- mock.restore();
30
- logSpy.mockRestore();
31
- teardownTestConfig(originalEnv);
32
- });
33
-
34
- describe("command metadata", () => {
35
- test("has correct name and description", () => {
36
- expect(command.name).toBe("auth");
37
- expect(command.description).toBe("Authenticate with Google Gemini");
38
- });
39
- });
40
-
41
- describe("auth flow with mock browser", () => {
42
- test("creates first profile and authenticates when no profiles exist", async () => {
43
- spyOn(configModule, "listProfiles").mockReturnValue([]);
44
- const authSpy = spyOn(command as any, "authenticateWithProfile").mockResolvedValue(undefined);
45
-
46
- await command.execute([], context);
47
-
48
- expect(authSpy).toHaveBeenCalledWith(
49
- expect.anything(),
50
- "default",
51
- expect.anything(),
52
- true,
53
- );
54
- });
55
-
56
- test("authenticates with the only profile when one exists", async () => {
57
- spyOn(configModule, "listProfiles").mockReturnValue(["solo"]);
58
- const authSpy = spyOn(command as any, "authenticateWithProfile").mockResolvedValue(undefined);
59
-
60
- await command.execute([], context);
61
-
62
- expect(authSpy).toHaveBeenCalledWith(
63
- expect.anything(),
64
- "solo",
65
- expect.anything(),
66
- false,
67
- );
68
- });
69
-
70
- test("propagates authentication errors", async () => {
71
- spyOn(configModule, "listProfiles").mockReturnValue(["default"]);
72
- spyOn(command as any, "authenticateWithProfile").mockRejectedValue(
73
- new Error("Browser launch failed"),
74
- );
75
-
76
- await expect(command.execute([], context)).rejects.toThrow("Browser launch failed");
77
- });
78
- });
79
-
80
- describe("profile selection menu", () => {
81
- test("displays profile menu when multiple profiles exist", async () => {
82
- const profiles = ["work", "personal", "dev"];
83
-
84
- spyOn(configModule, "listProfiles").mockReturnValue(profiles);
85
- const menuSpy = spyOn(command as any, "showProfileMenu").mockResolvedValue(null);
86
-
87
- await command.execute([], context);
88
-
89
- expect(menuSpy).toHaveBeenCalledWith(profiles, expect.anything());
90
- });
91
-
92
- test("menu shows all profile options", async () => {
93
- const profiles = ["alpha", "beta"];
94
- spyOn(command as any, "promptInput").mockResolvedValue("X");
95
-
96
- const mockProfileManager = {
97
- getStatus: mock((name: string) => ({
98
- name,
99
- exists: true,
100
- isActive: true,
101
- expiresAt: null,
102
- isDefault: name === "alpha",
103
- })),
104
- create: mock(() => {}),
105
- delete: mock(() => {}),
106
- rename: mock(() => {}),
107
- setDefault: mock(() => {}),
108
- };
109
-
110
- await (command as any).showProfileMenu(profiles, mockProfileManager);
111
-
112
- const output = logSpy.mock.calls.map((c) => c[0]).join("\n");
113
- expect(output).toContain("[A]");
114
- expect(output).toContain("Add new profile");
115
- expect(output).toContain("[D]");
116
- expect(output).toContain("Delete profile");
117
- expect(output).toContain("[S]");
118
- expect(output).toContain("Set default");
119
- expect(output).toContain("[R]");
120
- expect(output).toContain("Rename profile");
121
- expect(output).toContain("[X]");
122
- expect(output).toContain("Exit");
123
- });
124
-
125
- test("selecting a profile from menu triggers authentication", async () => {
126
- spyOn(configModule, "listProfiles").mockReturnValue(["p1", "p2"]);
127
- spyOn(command as any, "showProfileMenu").mockResolvedValue({
128
- type: "auth",
129
- profileName: "p2",
130
- });
131
- const authSpy = spyOn(command as any, "authenticateWithProfile").mockResolvedValue(undefined);
132
-
133
- await command.execute([], context);
134
-
135
- expect(authSpy).toHaveBeenCalledWith(
136
- expect.anything(),
137
- "p2",
138
- expect.anything(),
139
- false,
140
- );
141
- });
142
-
143
- test("exiting menu continues with default profile", async () => {
144
- spyOn(configModule, "listProfiles").mockReturnValue(["p1", "p2"]);
145
- spyOn(command as any, "showProfileMenu").mockResolvedValue(null);
146
-
147
- await command.execute([], context);
148
-
149
- const output = logSpy.mock.calls.map((c) => c[0]).join("\n");
150
- expect(output).toContain("Continuing with current default");
151
- });
152
-
153
- test("adding profile from menu triggers auth with new name", async () => {
154
- const profiles = ["existing"];
155
- const newProfileName = "brand-new";
156
-
157
- spyOn(configModule, "listProfiles").mockReturnValue(profiles);
158
-
159
- const mockProfileManager = {
160
- create: mock(() => {}),
161
- getStatus: mock(() => ({
162
- name: "existing",
163
- exists: true,
164
- isActive: true,
165
- expiresAt: null,
166
- isDefault: true,
167
- })),
168
- delete: mock(() => {}),
169
- rename: mock(() => {}),
170
- setDefault: mock(() => {}),
171
- };
172
-
173
- spyOn(command as any, "promptInput").mockImplementation(
174
- (prompt: string) => {
175
- if (prompt.includes("Select")) return Promise.resolve("A");
176
- if (prompt.includes("Enter profile name")) return Promise.resolve(newProfileName);
177
- return Promise.resolve("");
178
- },
179
- );
180
-
181
- const result = await (command as any).showProfileMenu(profiles, mockProfileManager);
182
-
183
- expect(result).toEqual({ type: "auth", profileName: newProfileName });
184
- });
185
- });
186
- });