gemiterm 2.0.0 → 2.1.0

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 (150) 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/install-skills-command.ts +35 -0
  5. package/src/cli/index.ts +19 -44
  6. package/src/infrastructure/cli-parser.ts +77 -0
  7. package/src/services/skills-cli-driver.ts +58 -0
  8. package/src/services/skills-service.ts +52 -0
  9. package/.env +0 -2
  10. package/.github/workflows/manual-release.yml +0 -34
  11. package/.github/workflows/release.yml +0 -198
  12. package/.github/workflows/test.yml +0 -29
  13. package/.opencode/commands/opsx-apply.md +0 -152
  14. package/.opencode/commands/opsx-archive.md +0 -157
  15. package/.opencode/commands/opsx-explore.md +0 -169
  16. package/.opencode/commands/opsx-propose.md +0 -104
  17. package/.opencode/commands/opsx-sync.md +0 -140
  18. package/.opencode/commands/search-intelligently.md +0 -9
  19. package/.opencode/opencode.json +0 -20
  20. package/.opencode/skills/intellisearch/.version +0 -1
  21. package/.opencode/skills/intellisearch/SKILL.md +0 -84
  22. package/.opencode/skills/intellisearch/references/brave-search.md +0 -67
  23. package/.opencode/skills/intellisearch/references/ddg-search.md +0 -79
  24. package/.opencode/skills/intellisearch/references/deepwiki-tools.md +0 -246
  25. package/.opencode/skills/intellisearch/references/examples.md +0 -97
  26. package/.opencode/skills/intellisearch/references/gh-cli.md +0 -143
  27. package/.opencode/skills/intellisearch/references/google-search.md +0 -40
  28. package/.opencode/skills/intellisearch/references/search-workflow.md +0 -143
  29. package/.opencode/skills/intellisearch/references/workflow.md +0 -115
  30. package/.opencode/skills/openspec-apply-change/SKILL.md +0 -159
  31. package/.opencode/skills/openspec-archive-change/SKILL.md +0 -117
  32. package/.opencode/skills/openspec-explore/SKILL.md +0 -287
  33. package/.opencode/skills/openspec-propose/SKILL.md +0 -111
  34. package/.opencode/skills/openspec-sync-specs/SKILL.md +0 -147
  35. package/.serena/project.yml +0 -133
  36. package/AGENTS.md +0 -136
  37. package/CHANGELOG.md +0 -3
  38. package/bun.lock +0 -105
  39. package/docs/INSTALL.md +0 -185
  40. package/docs/PLAYWRIGHT_CLI_API.md +0 -306
  41. package/docs/python-release-for-reference.md +0 -175
  42. package/examples/sample_json_export.json +0 -32
  43. package/examples/sample_markdown_export.md +0 -221
  44. package/examples/usage_examples.sh +0 -222
  45. package/install.ps1 +0 -264
  46. package/install.sh +0 -275
  47. package/openspec/MAESTRO_MIGRATION.md +0 -180
  48. package/openspec/changes/archive/2026-06-08-command-spec-conformance/.openspec.yaml +0 -2
  49. package/openspec/changes/archive/2026-06-08-command-spec-conformance/design.md +0 -194
  50. package/openspec/changes/archive/2026-06-08-command-spec-conformance/proposal.md +0 -52
  51. package/openspec/changes/archive/2026-06-08-command-spec-conformance/specs/multi-profile-conversations/spec.md +0 -133
  52. package/openspec/changes/archive/2026-06-08-command-spec-conformance/tasks.md +0 -95
  53. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/.openspec.yaml +0 -2
  54. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/design.md +0 -28
  55. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/proposal.md +0 -22
  56. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/specs/interactive-prompt-loop/spec.md +0 -35
  57. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/tasks.md +0 -28
  58. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/.openspec.yaml +0 -2
  59. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/design.md +0 -323
  60. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/proposal.md +0 -122
  61. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/specs/no-capability-changes/spec.md +0 -29
  62. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/tasks.md +0 -172
  63. package/openspec/changes/archive/2026-06-09-v2-install-migration/.openspec.yaml +0 -2
  64. package/openspec/changes/archive/2026-06-09-v2-install-migration/design.md +0 -241
  65. package/openspec/changes/archive/2026-06-09-v2-install-migration/proposal.md +0 -41
  66. package/openspec/changes/archive/2026-06-09-v2-install-migration/specs/v2-installer/spec.md +0 -205
  67. package/openspec/changes/archive/2026-06-09-v2-install-migration/tasks.md +0 -97
  68. package/openspec/changes/commander-cli-parser/.openspec.yaml +0 -2
  69. package/openspec/changes/commander-cli-parser/design.md +0 -107
  70. package/openspec/changes/commander-cli-parser/proposal.md +0 -31
  71. package/openspec/changes/commander-cli-parser/specs/cli/spec.md +0 -48
  72. package/openspec/changes/commander-cli-parser/tasks.md +0 -33
  73. package/openspec/changes/cross-platform-build-and-ci/.openspec.yaml +0 -2
  74. package/openspec/changes/cross-platform-build-and-ci/design.md +0 -213
  75. package/openspec/changes/cross-platform-build-and-ci/proposal.md +0 -50
  76. package/openspec/changes/cross-platform-build-and-ci/specs/path-and-file-mediation/spec.md +0 -335
  77. package/openspec/changes/cross-platform-build-and-ci/specs/release-pipeline/spec.md +0 -95
  78. package/openspec/changes/cross-platform-build-and-ci/tasks.md +0 -164
  79. package/openspec/config.yaml +0 -20
  80. package/openspec/specs/auth/spec.md +0 -394
  81. package/openspec/specs/cli/spec.md +0 -108
  82. package/openspec/specs/commands/spec.md +0 -409
  83. package/openspec/specs/configuration/spec.md +0 -130
  84. package/openspec/specs/conversations/spec.md +0 -139
  85. package/openspec/specs/domain-model/spec.md +0 -128
  86. package/openspec/specs/formatters/spec.md +0 -158
  87. package/openspec/specs/install-browser/spec.md +0 -146
  88. package/openspec/specs/interactive-prompt-loop/spec.md +0 -41
  89. package/openspec/specs/logger/spec.md +0 -71
  90. package/openspec/specs/mediator/spec.md +0 -72
  91. package/openspec/specs/multi-profile-conversations/spec.md +0 -145
  92. package/openspec/specs/path-utils/spec.md +0 -101
  93. package/openspec/specs/profiles/spec.md +0 -94
  94. package/openspec/specs/storage/spec.md +0 -198
  95. package/openspec/specs/testing/spec.md +0 -237
  96. package/openspec/specs/v2-installer/spec.md +0 -205
  97. package/openspec/specs/validators/spec.md +0 -65
  98. package/scripts/build.ts +0 -88
  99. package/scripts/clean-build.ps1 +0 -4
  100. package/scripts/clean-build.sh +0 -5
  101. package/scripts/install-browser.ps1 +0 -5
  102. package/scripts/install-browser.sh +0 -6
  103. package/scripts/lint-path-mediation.ps1 +0 -65
  104. package/scripts/lint-path-mediation.sh +0 -40
  105. package/tests/cli/auth-command.test.ts +0 -220
  106. package/tests/cli/command-registry.test.ts +0 -67
  107. package/tests/cli/continue-command.test.ts +0 -100
  108. package/tests/cli/delete-command.test.ts +0 -128
  109. package/tests/cli/export-all-command.test.ts +0 -375
  110. package/tests/cli/install-browser-command.test.ts +0 -48
  111. package/tests/cli/list-command.test.ts +0 -179
  112. package/tests/cli/new-command.test.ts +0 -139
  113. package/tests/cli/status-command.test.ts +0 -106
  114. package/tests/core/query-handlers.test.ts +0 -187
  115. package/tests/fixtures/auth-fixtures.ts +0 -98
  116. package/tests/fixtures/chat-fixtures.ts +0 -87
  117. package/tests/infrastructure/cli-table.test.ts +0 -107
  118. package/tests/infrastructure/formatters.test.ts +0 -202
  119. package/tests/infrastructure/io.test.ts +0 -197
  120. package/tests/infrastructure/logger.test.ts +0 -74
  121. package/tests/infrastructure/storage.test.ts +0 -335
  122. package/tests/infrastructure/validators.test.ts +0 -67
  123. package/tests/integration/commands/auth.test.ts +0 -186
  124. package/tests/integration/commands/continue.test.ts +0 -130
  125. package/tests/integration/commands/delete.test.ts +0 -140
  126. package/tests/integration/commands/export.test.ts +0 -266
  127. package/tests/integration/commands/fetch.test.ts +0 -238
  128. package/tests/integration/commands/list.test.ts +0 -281
  129. package/tests/integration/commands/profile.test.ts +0 -258
  130. package/tests/integration/commands/status.test.ts +0 -223
  131. package/tests/integration/installer-script-shape.test.ts +0 -67
  132. package/tests/parity/compare-outputs.ts +0 -359
  133. package/tests/parity/test-commands-parity.ps1 +0 -82
  134. package/tests/parity/test-commands-parity.sh +0 -61
  135. package/tests/services/auth-service.test.ts +0 -315
  136. package/tests/services/cookie-monitor.test.ts +0 -229
  137. package/tests/services/cookie-storage-service.test.ts +0 -230
  138. package/tests/services/gemini-client-wrapper.test.ts +0 -688
  139. package/tests/services/install-browser-service.test.ts +0 -83
  140. package/tests/services/playwright-cli-driver.test.ts +0 -468
  141. package/tests/services/profile-auth-manager.test.ts +0 -334
  142. package/tests/services/profile-service.test.ts +0 -224
  143. package/tests/setup.ts +0 -95
  144. package/tests/smoke/smoke.test.ts +0 -52
  145. package/tests/unit/config.test.ts +0 -159
  146. package/tests/unit/errors.test.ts +0 -119
  147. package/tests/unit/formatters.test.ts +0 -316
  148. package/tests/unit/mediator.test.ts +0 -152
  149. package/tests/unit/path-utils.test.ts +0 -289
  150. 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
- });