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,375 +0,0 @@
1
- import { describe, test, expect, mock, beforeEach, afterEach, spyOn } from "bun:test";
2
- import { ExportAllCommand } from "../../src/cli/commands/export-all-command.ts";
3
- import { Mediator } from "../../src/core/mediator.ts";
4
- import type { CliCommandContext } from "../../src/cli/command-registry.ts";
5
- import { QUERY_TYPES } from "../../src/core/query-handlers.ts";
6
- import { mkdirSync, readFileSync, rmSync, existsSync } from "node:fs";
7
- import { join } from "node:path";
8
- import { tmpdir } from "node:os";
9
-
10
- const SAMPLE_CHATS = [
11
- { id: "abc123", title: "Python tips", isPinned: true, timestamp: 1717000000000 },
12
- { id: "def456", title: "Bun setup", isPinned: false, timestamp: 1717100000000 },
13
- ];
14
-
15
- const SAMPLE_MESSAGES = [
16
- { role: "user" as const, content: "Hello" },
17
- { role: "model" as const, content: "Hi there!" },
18
- ];
19
-
20
- const ANSI_RE = /\x1b\[[0-9;]*m/g;
21
-
22
- function stripAnsi(value: string): string {
23
- return value.replace(ANSI_RE, "");
24
- }
25
-
26
- function capturedLog(spy: ReturnType<typeof spyOn>): string {
27
- return stripAnsi(spy.mock.calls.map((c) => c[0]).join("\n"));
28
- }
29
-
30
- function capturedStdout(spy: ReturnType<typeof spyOn>): string {
31
- return stripAnsi(spy.mock.calls.map((c) => c[0]).join(""));
32
- }
33
-
34
- describe("ExportAllCommand", () => {
35
- let command: ExportAllCommand;
36
- let mediator: Mediator;
37
- let context: CliCommandContext;
38
- let logSpy: ReturnType<typeof spyOn>;
39
- let writeSpy: ReturnType<typeof spyOn>;
40
- let tempDir: string;
41
-
42
- beforeEach(() => {
43
- command = new ExportAllCommand();
44
- mediator = new Mediator();
45
- tempDir = join(tmpdir(), `export-all-test-${Date.now()}`);
46
- mkdirSync(tempDir, { recursive: true });
47
- context = { verbose: false, mediator };
48
- logSpy = spyOn(console, "log").mockImplementation(() => {});
49
- writeSpy = spyOn(process.stdout, "write").mockImplementation(() => true);
50
- });
51
-
52
- afterEach(() => {
53
- mock.restore();
54
- logSpy.mockRestore();
55
- writeSpy.mockRestore();
56
- try {
57
- rmSync(tempDir, { recursive: true, force: true });
58
- } catch {}
59
- });
60
-
61
- test("has correct name and description", () => {
62
- expect(command.name).toBe("export-all");
63
- expect(command.description).toBe("Export all conversations to files");
64
- });
65
-
66
- test("shows help with --help flag", async () => {
67
- await command.execute(["--help"], context);
68
- const output = capturedLog(logSpy);
69
- expect(output).toContain("Usage: gemiterm export-all");
70
- expect(output).toContain("--output-dir");
71
- expect(output).toContain("--since");
72
- expect(output).toContain("--all-profiles");
73
- });
74
-
75
- test("exports all chats and creates index.md", async () => {
76
- const listHandler = {
77
- queryType: QUERY_TYPES.LIST_CHATS,
78
- handle: mock(async () => ({ chats: SAMPLE_CHATS })),
79
- };
80
- const fetchHandler = {
81
- queryType: QUERY_TYPES.FETCH_CHAT,
82
- handle: mock(async () => ({ messages: SAMPLE_MESSAGES })),
83
- };
84
- mediator.registerQueryHandler(listHandler as any);
85
- mediator.registerQueryHandler(fetchHandler as any);
86
-
87
- await command.execute(["--output-dir", tempDir], context);
88
-
89
- const indexPath = join(tempDir, "index.md");
90
- expect(existsSync(indexPath)).toBeTrue();
91
-
92
- const indexContent = readFileSync(indexPath, "utf-8");
93
- expect(indexContent).toContain("Exported Conversations");
94
- expect(indexContent).toContain("Python tips");
95
- expect(indexContent).toContain("Bun setup");
96
- });
97
-
98
- test("creates individual export files for each chat", async () => {
99
- const listHandler = {
100
- queryType: QUERY_TYPES.LIST_CHATS,
101
- handle: mock(async () => ({ chats: SAMPLE_CHATS })),
102
- };
103
- const fetchHandler = {
104
- queryType: QUERY_TYPES.FETCH_CHAT,
105
- handle: mock(async () => ({ messages: SAMPLE_MESSAGES })),
106
- };
107
- mediator.registerQueryHandler(listHandler as any);
108
- mediator.registerQueryHandler(fetchHandler as any);
109
-
110
- await command.execute(["--output-dir", tempDir], context);
111
-
112
- const files = readFileSync(join(tempDir, "index.md"), "utf-8");
113
- const mdFiles = files.match(/\.md\)/g);
114
- expect(mdFiles).toHaveLength(2);
115
- });
116
-
117
- test("shows no conversations message when empty", async () => {
118
- const listHandler = {
119
- queryType: QUERY_TYPES.LIST_CHATS,
120
- handle: mock(async () => ({ chats: [] })),
121
- };
122
- mediator.registerQueryHandler(listHandler as any);
123
-
124
- await command.execute(["--output-dir", tempDir], context);
125
-
126
- const output = capturedLog(logSpy);
127
- expect(output).toContain("No conversations found");
128
- });
129
-
130
- test("passes allProfiles flag to query", async () => {
131
- const listHandler = {
132
- queryType: QUERY_TYPES.LIST_CHATS,
133
- handle: mock(async () => ({ chats: SAMPLE_CHATS })),
134
- };
135
- const fetchHandler = {
136
- queryType: QUERY_TYPES.FETCH_CHAT,
137
- handle: mock(async () => ({ messages: SAMPLE_MESSAGES })),
138
- };
139
- mediator.registerQueryHandler(listHandler as any);
140
- mediator.registerQueryHandler(fetchHandler as any);
141
-
142
- await command.execute(["--all-profiles", "--output-dir", tempDir], context);
143
-
144
- expect(listHandler.handle).toHaveBeenCalledWith(
145
- expect.objectContaining({
146
- payload: expect.objectContaining({ allProfiles: true }),
147
- }),
148
- );
149
- });
150
-
151
- test("filters by --since date", async () => {
152
- const listHandler = {
153
- queryType: QUERY_TYPES.LIST_CHATS,
154
- handle: mock(async () => ({ chats: SAMPLE_CHATS })),
155
- };
156
- const fetchHandler = {
157
- queryType: QUERY_TYPES.FETCH_CHAT,
158
- handle: mock(async () => ({ messages: SAMPLE_MESSAGES })),
159
- };
160
- mediator.registerQueryHandler(listHandler as any);
161
- mediator.registerQueryHandler(fetchHandler as any);
162
-
163
- await command.execute(["--since", "2024-01-01", "--output-dir", tempDir], context);
164
-
165
- const indexContent = readFileSync(join(tempDir, "index.md"), "utf-8");
166
- const mdFiles = indexContent.match(/\.md\)/g);
167
- expect(mdFiles).toHaveLength(2);
168
- });
169
-
170
- test("reports failed exports in summary and index", async () => {
171
- const listHandler = {
172
- queryType: QUERY_TYPES.LIST_CHATS,
173
- handle: mock(async () => ({ chats: SAMPLE_CHATS })),
174
- };
175
- let callCount = 0;
176
- const fetchHandler = {
177
- queryType: QUERY_TYPES.FETCH_CHAT,
178
- handle: mock(async () => {
179
- callCount++;
180
- if (callCount === 1) return { messages: SAMPLE_MESSAGES };
181
- throw new Error("Network error");
182
- }),
183
- };
184
- mediator.registerQueryHandler(listHandler as any);
185
- mediator.registerQueryHandler(fetchHandler as any);
186
-
187
- await command.execute(["--output-dir", tempDir], context);
188
-
189
- const output = capturedLog(logSpy);
190
- expect(output).toMatch(/Exported:\s+1/);
191
- expect(output).toMatch(/Failed:\s+1/);
192
-
193
- const indexContent = readFileSync(join(tempDir, "index.md"), "utf-8");
194
- expect(indexContent).toContain("Failed Exports");
195
- expect(indexContent).toContain("Network error");
196
- });
197
-
198
- test("shows progress output during export", async () => {
199
- const listHandler = {
200
- queryType: QUERY_TYPES.LIST_CHATS,
201
- handle: mock(async () => ({ chats: SAMPLE_CHATS })),
202
- };
203
- const fetchHandler = {
204
- queryType: QUERY_TYPES.FETCH_CHAT,
205
- handle: mock(async () => ({ messages: SAMPLE_MESSAGES })),
206
- };
207
- mediator.registerQueryHandler(listHandler as any);
208
- mediator.registerQueryHandler(fetchHandler as any);
209
-
210
- await command.execute(["--output-dir", tempDir], context);
211
-
212
- const progressOutput = capturedStdout(writeSpy);
213
- expect(progressOutput).toContain("[1/2]");
214
- expect(progressOutput).toContain("[2/2]");
215
- });
216
-
217
- test("uses --include-metadata in index", async () => {
218
- const listHandler = {
219
- queryType: QUERY_TYPES.LIST_CHATS,
220
- handle: mock(async () => ({ chats: SAMPLE_CHATS })),
221
- };
222
- const fetchHandler = {
223
- queryType: QUERY_TYPES.FETCH_CHAT,
224
- handle: mock(async () => ({ messages: SAMPLE_MESSAGES })),
225
- };
226
- mediator.registerQueryHandler(listHandler as any);
227
- mediator.registerQueryHandler(fetchHandler as any);
228
-
229
- await command.execute(["--include-metadata", "--output-dir", tempDir], context);
230
-
231
- const indexContent = readFileSync(join(tempDir, "index.md"), "utf-8");
232
- expect(indexContent).toContain("Successful:");
233
- expect(indexContent).toContain("Failed:");
234
- });
235
-
236
- test("defaults output directory to ./exports", async () => {
237
- const listHandler = {
238
- queryType: QUERY_TYPES.LIST_CHATS,
239
- handle: mock(async () => ({ chats: [] })),
240
- };
241
- mediator.registerQueryHandler(listHandler as any);
242
-
243
- await command.execute([], context);
244
- const output = capturedLog(logSpy);
245
- expect(output).toContain("No conversations found");
246
- });
247
-
248
- test("handles all exports failing", async () => {
249
- const listHandler = {
250
- queryType: QUERY_TYPES.LIST_CHATS,
251
- handle: mock(async () => ({ chats: SAMPLE_CHATS })),
252
- };
253
- const fetchHandler = {
254
- queryType: QUERY_TYPES.FETCH_CHAT,
255
- handle: mock(async () => {
256
- throw new Error("boom");
257
- }),
258
- };
259
- mediator.registerQueryHandler(listHandler as any);
260
- mediator.registerQueryHandler(fetchHandler as any);
261
-
262
- await command.execute(["--output-dir", tempDir], context);
263
-
264
- const output = capturedLog(logSpy);
265
- expect(output).toMatch(/Exported:\s+0/);
266
- expect(output).toMatch(/Failed:\s+2/);
267
- expect(output).toMatch(/Index:/);
268
-
269
- const indexContent = readFileSync(join(tempDir, "index.md"), "utf-8");
270
- expect(indexContent).toContain("Failed Exports");
271
- expect(indexContent).toContain("Python tips");
272
- expect(indexContent).toContain("Bun setup");
273
- expect(indexContent).toContain("boom");
274
- });
275
-
276
- test("preserves non-Error throw values in failure output", async () => {
277
- const listHandler = {
278
- queryType: QUERY_TYPES.LIST_CHATS,
279
- handle: mock(async () => ({ chats: [SAMPLE_CHATS[0]] })),
280
- };
281
- const fetchHandler = {
282
- queryType: QUERY_TYPES.FETCH_CHAT,
283
- handle: mock(async () => {
284
- throw "string-failure";
285
- }),
286
- };
287
- mediator.registerQueryHandler(listHandler as any);
288
- mediator.registerQueryHandler(fetchHandler as any);
289
-
290
- await command.execute(["--output-dir", tempDir], context);
291
-
292
- const indexContent = readFileSync(join(tempDir, "index.md"), "utf-8");
293
- expect(indexContent).toContain("string-failure");
294
- });
295
-
296
- test("creates the output directory when it does not exist", async () => {
297
- const nested = join(tempDir, "deep", "nested", "out");
298
- const listHandler = {
299
- queryType: QUERY_TYPES.LIST_CHATS,
300
- handle: mock(async () => ({ chats: SAMPLE_CHATS })),
301
- };
302
- const fetchHandler = {
303
- queryType: QUERY_TYPES.FETCH_CHAT,
304
- handle: mock(async () => ({ messages: SAMPLE_MESSAGES })),
305
- };
306
- mediator.registerQueryHandler(listHandler as any);
307
- mediator.registerQueryHandler(fetchHandler as any);
308
-
309
- await command.execute(["--output-dir", nested], context);
310
-
311
- expect(existsSync(nested)).toBeTrue();
312
- expect(existsSync(join(nested, "index.md"))).toBeTrue();
313
- });
314
-
315
- test("accepts -a short form for --all-profiles", async () => {
316
- const listHandler = {
317
- queryType: QUERY_TYPES.LIST_CHATS,
318
- handle: mock(async () => ({ chats: SAMPLE_CHATS })),
319
- };
320
- const fetchHandler = {
321
- queryType: QUERY_TYPES.FETCH_CHAT,
322
- handle: mock(async () => ({ messages: SAMPLE_MESSAGES })),
323
- };
324
- mediator.registerQueryHandler(listHandler as any);
325
- mediator.registerQueryHandler(fetchHandler as any);
326
-
327
- await command.execute(["-a", "--output-dir", tempDir], context);
328
-
329
- expect(listHandler.handle).toHaveBeenCalledWith(
330
- expect.objectContaining({
331
- payload: expect.objectContaining({ allProfiles: true }),
332
- }),
333
- );
334
- });
335
-
336
- test("ignores invalid --since date and exports all chats", async () => {
337
- const listHandler = {
338
- queryType: QUERY_TYPES.LIST_CHATS,
339
- handle: mock(async () => ({ chats: SAMPLE_CHATS })),
340
- };
341
- const fetchHandler = {
342
- queryType: QUERY_TYPES.FETCH_CHAT,
343
- handle: mock(async () => ({ messages: SAMPLE_MESSAGES })),
344
- };
345
- mediator.registerQueryHandler(listHandler as any);
346
- mediator.registerQueryHandler(fetchHandler as any);
347
-
348
- await command.execute(["--since", "not-a-date", "--output-dir", tempDir], context);
349
-
350
- const indexContent = readFileSync(join(tempDir, "index.md"), "utf-8");
351
- expect(indexContent).toContain("Python tips");
352
- expect(indexContent).toContain("Bun setup");
353
- });
354
-
355
- test("filters out chats older than --since date", async () => {
356
- const oldChat = { id: "old1", title: "Old chat", isPinned: false, timestamp: 1577836800000 };
357
- const newChat = { id: "new1", title: "New chat", isPinned: false, timestamp: 1717200000000 };
358
- const listHandler = {
359
- queryType: QUERY_TYPES.LIST_CHATS,
360
- handle: mock(async () => ({ chats: [oldChat, newChat] })),
361
- };
362
- const fetchHandler = {
363
- queryType: QUERY_TYPES.FETCH_CHAT,
364
- handle: mock(async () => ({ messages: SAMPLE_MESSAGES })),
365
- };
366
- mediator.registerQueryHandler(listHandler as any);
367
- mediator.registerQueryHandler(fetchHandler as any);
368
-
369
- await command.execute(["--since", "2024-01-01", "--output-dir", tempDir], context);
370
-
371
- const indexContent = readFileSync(join(tempDir, "index.md"), "utf-8");
372
- expect(indexContent).toContain("New chat");
373
- expect(indexContent).not.toContain("Old chat");
374
- });
375
- });
@@ -1,48 +0,0 @@
1
- import { describe, test, expect, beforeEach, mock, spyOn } from "bun:test";
2
- import { InstallBrowserCommand } from "../../src/cli/commands/install-browser-command.ts";
3
- import { InstallBrowserService, InstallBrowserError } from "../../src/services/install-browser-service.ts";
4
-
5
- describe("InstallBrowserCommand", () => {
6
- let command: InstallBrowserCommand;
7
-
8
- beforeEach(() => {
9
- command = new InstallBrowserCommand();
10
- });
11
-
12
- test("has correct name and description", () => {
13
- expect(command.name).toBe("install-browser");
14
- expect(command.description).toContain("Chromium");
15
- });
16
-
17
- test("implements CliCommand interface", () => {
18
- expect(typeof command.execute).toBe("function");
19
- });
20
-
21
- test("exits with code 1 on InstallBrowserError", async () => {
22
- const exitSpy = spyOn(process, "exit").mockImplementation(() => {
23
- throw new Error("process.exit");
24
- });
25
-
26
- const service = new InstallBrowserService();
27
- spyOn(service, "install").mockImplementation(async () => {
28
- throw new InstallBrowserError("install failed");
29
- });
30
-
31
- const spy = spyOn(InstallBrowserService.prototype, "install").mockImplementation(async () => {
32
- throw new InstallBrowserError("install failed");
33
- });
34
-
35
- await expect(command.execute([], { verbose: false })).rejects.toThrow("process.exit");
36
-
37
- expect(exitSpy).toHaveBeenCalledWith(1);
38
- spy.mockRestore();
39
- exitSpy.mockRestore();
40
- });
41
-
42
- test("succeeds when install completes", async () => {
43
- const spy = spyOn(InstallBrowserService.prototype, "install").mockResolvedValue(undefined);
44
-
45
- await expect(command.execute([], { verbose: false })).resolves.toBeUndefined();
46
- spy.mockRestore();
47
- });
48
- });
@@ -1,179 +0,0 @@
1
- import { describe, test, expect, mock, beforeEach, afterEach, spyOn } from "bun:test";
2
- import { ListCommand } from "../../src/cli/commands/list-command.ts";
3
- import { Mediator } from "../../src/core/mediator.ts";
4
- import type { CliCommandContext } from "../../src/cli/command-registry.ts";
5
- import { QUERY_TYPES } from "../../src/core/query-handlers.ts";
6
- import type { ListChatsQueryResult } from "../../src/core/query-handlers.ts";
7
-
8
- const SAMPLE_CHATS = [
9
- { id: "abc123", title: "Python tips", isPinned: true, timestamp: 1717000000000 },
10
- { id: "def456", title: "Bun setup", isPinned: false, timestamp: 1717100000000 },
11
- { id: "ghi789", title: "Alpha test", isPinned: false, timestamp: 1716900000000 },
12
- ];
13
-
14
- describe("ListCommand", () => {
15
- let command: ListCommand;
16
- let mediator: Mediator;
17
- let context: CliCommandContext;
18
- let logSpy: ReturnType<typeof spyOn>;
19
-
20
- beforeEach(() => {
21
- command = new ListCommand();
22
- mediator = new Mediator();
23
- context = { verbose: false, mediator };
24
- logSpy = spyOn(console, "log").mockImplementation(() => {});
25
- });
26
-
27
- afterEach(() => {
28
- mock.restore();
29
- logSpy.mockRestore();
30
- });
31
-
32
- test("has correct name and description", () => {
33
- expect(command.name).toBe("list");
34
- expect(command.description).toBe("List conversations");
35
- });
36
-
37
- test("shows help with --help flag", async () => {
38
- await command.execute(["--help"], context);
39
- const output = logSpy.mock.calls.map((c) => c[0]).join("\n");
40
- expect(output).toContain("Usage: gemiterm list");
41
- expect(output).toContain("--limit");
42
- expect(output).toContain("--format");
43
- });
44
-
45
- test("sends list-chats query and displays results", async () => {
46
- const mockHandler = {
47
- queryType: QUERY_TYPES.LIST_CHATS,
48
- handle: mock(async () => ({ chats: SAMPLE_CHATS })),
49
- };
50
- mediator.registerQueryHandler(mockHandler as any);
51
-
52
- await command.execute([], context);
53
-
54
- const output = logSpy.mock.calls.map((c) => c[0]).join("\n");
55
- expect(output).toContain("abc123");
56
- expect(output).toContain("Python tips");
57
- expect(output).toContain("Total: 3 conversations");
58
- });
59
-
60
- test("applies limit", async () => {
61
- const mockHandler = {
62
- queryType: QUERY_TYPES.LIST_CHATS,
63
- handle: mock(async () => ({ chats: SAMPLE_CHATS })),
64
- };
65
- mediator.registerQueryHandler(mockHandler as any);
66
-
67
- await command.execute(["--limit", "1"], context);
68
-
69
- const output = logSpy.mock.calls.map((c) => c[0]).join("\n");
70
- expect(output).toContain("Total: 1 conversation");
71
- });
72
-
73
- test("applies search filter", async () => {
74
- const mockHandler = {
75
- queryType: QUERY_TYPES.LIST_CHATS,
76
- handle: mock(async () => {
77
- const filtered = SAMPLE_CHATS.filter((c) => c.title.toLowerCase().includes("bun"));
78
- return { chats: filtered };
79
- }),
80
- };
81
- mediator.registerQueryHandler(mockHandler as any);
82
-
83
- await command.execute(["--search", "Bun"], context);
84
-
85
- expect(mockHandler.handle).toHaveBeenCalledWith(
86
- expect.objectContaining({
87
- payload: expect.objectContaining({ search: "Bun" }),
88
- }),
89
- );
90
- });
91
-
92
- test("applies --all flag", async () => {
93
- const mockHandler = {
94
- queryType: QUERY_TYPES.LIST_CHATS,
95
- handle: mock(async () => ({ chats: SAMPLE_CHATS })),
96
- };
97
- mediator.registerQueryHandler(mockHandler as any);
98
-
99
- await command.execute(["--all"], context);
100
-
101
- const output = logSpy.mock.calls.map((c) => c[0]).join("\n");
102
- expect(output).toContain("Total: 3 conversations");
103
- });
104
-
105
- test("applies sort by alpha", async () => {
106
- const mockHandler = {
107
- queryType: QUERY_TYPES.LIST_CHATS,
108
- handle: mock(async () => ({ chats: SAMPLE_CHATS })),
109
- };
110
- mediator.registerQueryHandler(mockHandler as any);
111
-
112
- await command.execute(["--sort", "alpha"], context);
113
-
114
- const output = logSpy.mock.calls.map((c) => c[0]).join("\n");
115
- const alphaIdx = output.indexOf("Alpha test");
116
- const bunIdx = output.indexOf("Bun setup");
117
- const pythonIdx = output.indexOf("Python tips");
118
- expect(alphaIdx).toBeLessThan(bunIdx);
119
- expect(bunIdx).toBeLessThan(pythonIdx);
120
- });
121
-
122
- test("outputs JSON format", async () => {
123
- const mockHandler = {
124
- queryType: QUERY_TYPES.LIST_CHATS,
125
- handle: mock(async () => ({ chats: SAMPLE_CHATS })),
126
- };
127
- mediator.registerQueryHandler(mockHandler as any);
128
-
129
- await command.execute(["--format", "json"], context);
130
-
131
- const output = logSpy.mock.calls.map((c) => c[0]).join("\n");
132
- const parsed = JSON.parse(output);
133
- expect(parsed).toHaveProperty("chats");
134
- expect(parsed.chats).toHaveLength(3);
135
- });
136
-
137
- test("shows no conversations message when empty", async () => {
138
- const mockHandler = {
139
- queryType: QUERY_TYPES.LIST_CHATS,
140
- handle: mock(async () => ({ chats: [] })),
141
- };
142
- mediator.registerQueryHandler(mockHandler as any);
143
-
144
- await command.execute([], context);
145
-
146
- const output = logSpy.mock.calls.map((c) => c[0]).join("\n");
147
- expect(output).toContain("No conversations found");
148
- });
149
-
150
- test("applies --all-profiles flag in query", async () => {
151
- const mockHandler = {
152
- queryType: QUERY_TYPES.LIST_CHATS,
153
- handle: mock(async () => ({ chats: [] })),
154
- };
155
- mediator.registerQueryHandler(mockHandler as any);
156
-
157
- await command.execute(["--all-profiles"], context);
158
-
159
- expect(mockHandler.handle).toHaveBeenCalledWith(
160
- expect.objectContaining({
161
- payload: expect.objectContaining({ allProfiles: true }),
162
- }),
163
- );
164
- });
165
-
166
- test("applies --offset flag", async () => {
167
- const mockHandler = {
168
- queryType: QUERY_TYPES.LIST_CHATS,
169
- handle: mock(async () => ({ chats: SAMPLE_CHATS })),
170
- };
171
- mediator.registerQueryHandler(mockHandler as any);
172
-
173
- await command.execute(["--offset", "2", "--limit", "1"], context);
174
-
175
- const output = logSpy.mock.calls.map((c) => c[0]).join("\n");
176
- expect(output).toContain("ghi789");
177
- expect(output).toContain("Total: 1 conversation");
178
- });
179
- });