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,107 +0,0 @@
1
- import { describe, test, expect } from "bun:test";
2
- import { renderTable, type ColumnDef } from "../../src/infrastructure/cli-table.ts";
3
-
4
- interface Row {
5
- name: string;
6
- age: number;
7
- }
8
-
9
- describe("renderTable", () => {
10
- test("returns dim emptyMessage for empty rows", () => {
11
- const out = renderTable<Row>({
12
- columns: [
13
- { header: "Name", width: 10, cell: (r) => r.name },
14
- { header: "Age", width: 5, cell: (r) => String(r.age) },
15
- ],
16
- rows: [],
17
- emptyMessage: "Nothing here",
18
- });
19
- expect(out).toContain("Nothing here");
20
- expect(out).not.toContain("Name");
21
- });
22
-
23
- test("renders header row with column names", () => {
24
- const out = renderTable<Row>({
25
- columns: [
26
- { header: "Name", width: 10, cell: (r) => r.name },
27
- { header: "Age", width: 5, cell: (r) => String(r.age) },
28
- ],
29
- rows: [{ name: "Alice", age: 30 }],
30
- });
31
- expect(out).toContain("Name");
32
- expect(out).toContain("Age");
33
- });
34
-
35
- test("renders data rows with cell values", () => {
36
- const out = renderTable<Row>({
37
- columns: [
38
- { header: "Name", width: 10, cell: (r) => r.name },
39
- { header: "Age", width: 5, cell: (r) => String(r.age) },
40
- ],
41
- rows: [
42
- { name: "Alice", age: 30 },
43
- { name: "Bob", age: 25 },
44
- ],
45
- });
46
- expect(out).toContain("Alice");
47
- expect(out).toContain("30");
48
- expect(out).toContain("Bob");
49
- expect(out).toContain("25");
50
- });
51
-
52
- test("includes Unicode box-drawing borders", () => {
53
- const out = renderTable<Row>({
54
- columns: [
55
- { header: "Name", width: 10, cell: (r) => r.name },
56
- { header: "Age", width: 5, cell: (r) => String(r.age) },
57
- ],
58
- rows: [{ name: "Alice", age: 30 }],
59
- });
60
- expect(out).toMatch(/[\u2500-\u257f]/);
61
- });
62
-
63
- test("appends footer when provided", () => {
64
- const out = renderTable<Row>({
65
- columns: [
66
- { header: "Name", width: 10, cell: (r) => r.name },
67
- { header: "Age", width: 5, cell: (r) => String(r.age) },
68
- ],
69
- rows: [{ name: "Alice", age: 30 }],
70
- footer: "Total: 1",
71
- });
72
- expect(out).toContain("Total: 1");
73
- });
74
-
75
- test("does not crash with zero columns", () => {
76
- const out = renderTable<Row>({
77
- columns: [],
78
- rows: [{ name: "Alice", age: 30 }],
79
- });
80
- expect(typeof out).toBe("string");
81
- });
82
-
83
- test("handles zero rows with no emptyMessage", () => {
84
- const out = renderTable<Row>({
85
- columns: [
86
- { header: "Name", width: 10, cell: (r) => r.name },
87
- { header: "Age", width: 5, cell: (r) => String(r.age) },
88
- ],
89
- rows: [],
90
- });
91
- expect(out).toContain("Name");
92
- });
93
-
94
- test("respects optional PROFILE-style spread-in column", () => {
95
- const includeExtra = true;
96
- const columns: ColumnDef<Row & { extra?: string }>[] = [
97
- { header: "Name", width: 10, cell: (r) => r.name },
98
- ...(includeExtra ? [{ header: "Extra", width: 8, cell: (r: Row & { extra?: string }) => r.extra ?? "" }] : []),
99
- ];
100
- const out = renderTable({
101
- columns,
102
- rows: [{ name: "Alice", age: 30, extra: "yes" }],
103
- });
104
- expect(out).toContain("Extra");
105
- expect(out).toContain("yes");
106
- });
107
- });
@@ -1,202 +0,0 @@
1
- import { describe, test, expect } from "bun:test";
2
- import {
3
- formatChatAsMarkdown,
4
- formatChatAsJson,
5
- formatProfileTable,
6
- formatChatList,
7
- } from "../../src/infrastructure/formatters.ts";
8
- import type { Message, ChatInfo, ProfileStatus } from "../../src/core/types.ts";
9
-
10
- function makeMessages(): Message[] {
11
- return [
12
- { role: "user", content: "Hello, Gemini!" },
13
- { role: "model", content: "Hi there! How can I help you today?" },
14
- { role: "user", content: "Tell me about TypeScript." },
15
- { role: "model", content: "TypeScript is a typed superset of JavaScript." },
16
- ];
17
- }
18
-
19
- function makeChats(): ChatInfo[] {
20
- return [
21
- { id: "abc123", title: "Chat about TypeScript", isPinned: false, timestamp: Date.now() },
22
- { id: "def456", title: "Pinned conversation", isPinned: true, timestamp: Date.now() - 86400000 },
23
- ];
24
- }
25
-
26
- function makeStatuses(): ProfileStatus[] {
27
- return [
28
- {
29
- name: "default",
30
- exists: true,
31
- isActive: true,
32
- expiresAt: new Date(Date.now() + 7 * 86400000).toISOString(),
33
- isDefault: true,
34
- },
35
- {
36
- name: "expired-profile",
37
- exists: true,
38
- isActive: false,
39
- expiresAt: new Date(Date.now() - 86400000).toISOString(),
40
- isDefault: false,
41
- },
42
- {
43
- name: "missing-cookies",
44
- exists: true,
45
- isActive: false,
46
- expiresAt: null,
47
- isDefault: false,
48
- },
49
- ];
50
- }
51
-
52
- describe("formatChatAsMarkdown", () => {
53
- test("renders messages with proper markdown headers", () => {
54
- const messages = makeMessages();
55
- const result = formatChatAsMarkdown(messages, "Test Chat");
56
-
57
- expect(result).toContain("# Test Chat");
58
- expect(result).toContain("**You:**");
59
- expect(result).toContain("**Gemini:**");
60
- expect(result).toContain("Hello, Gemini!");
61
- expect(result).toContain("TypeScript is a typed superset of JavaScript.");
62
- });
63
-
64
- test("includes separator lines between messages", () => {
65
- const result = formatChatAsMarkdown(makeMessages(), "Title");
66
- const separatorCount = (result.match(/^---$/gm) ?? []).length;
67
- expect(separatorCount).toBe(4);
68
- });
69
-
70
- test("includes metadata when includeMetadata is true and conversationId provided", () => {
71
- const result = formatChatAsMarkdown(
72
- makeMessages(),
73
- "Meta Chat",
74
- "conv-xyz",
75
- true,
76
- );
77
-
78
- expect(result).toContain("> Conversation ID: conv-xyz");
79
- expect(result).toContain("> Messages: 4");
80
- expect(result).toContain("> Exported:");
81
- });
82
-
83
- test("omits metadata when includeMetadata is false", () => {
84
- const result = formatChatAsMarkdown(
85
- makeMessages(),
86
- "No Meta",
87
- "conv-xyz",
88
- false,
89
- );
90
-
91
- expect(result).not.toContain("> Conversation ID:");
92
- expect(result).not.toContain("> Messages:");
93
- });
94
-
95
- test("handles empty messages array", () => {
96
- const result = formatChatAsMarkdown([], "Empty Chat");
97
- expect(result).toContain("# Empty Chat");
98
- expect(result).not.toContain("**You:**");
99
- expect(result).not.toContain("**Gemini:**");
100
- });
101
- });
102
-
103
- describe("formatChatAsJson", () => {
104
- test("returns valid JSON with conversationId and messages", () => {
105
- const messages = makeMessages();
106
- const result = formatChatAsJson(messages, "conv-123");
107
-
108
- const parsed = JSON.parse(result);
109
- expect(parsed.conversationId).toBe("conv-123");
110
- expect(parsed.messages).toHaveLength(4);
111
- expect(parsed.messages[0].role).toBe("user");
112
- expect(parsed.messages[0].content).toBe("Hello, Gemini!");
113
- });
114
-
115
- test("handles empty messages array", () => {
116
- const result = formatChatAsJson([], "conv-empty");
117
- const parsed = JSON.parse(result);
118
- expect(parsed.conversationId).toBe("conv-empty");
119
- expect(parsed.messages).toEqual([]);
120
- });
121
- });
122
-
123
- describe("formatProfileTable", () => {
124
- test("renders profile statuses in table format", () => {
125
- const statuses = makeStatuses();
126
- const result = formatProfileTable(statuses);
127
-
128
- expect(result).toContain("NAME");
129
- expect(result).toContain("ACTIVE");
130
- expect(result).toContain("EXPIRES");
131
- expect(result).toContain("DEFAULT");
132
- expect(result).toContain("default");
133
- expect(result).toContain("expired-profile");
134
- });
135
-
136
- test("shows active checkmark for active profile", () => {
137
- const statuses = makeStatuses();
138
- const result = formatProfileTable(statuses);
139
- expect(result).toContain("\u2713");
140
- });
141
-
142
- test("shows inactive X for inactive profile", () => {
143
- const statuses = makeStatuses();
144
- const result = formatProfileTable(statuses);
145
- expect(result).toContain("\u2717");
146
- });
147
-
148
- test("shows default marker note", () => {
149
- const result = formatProfileTable(makeStatuses());
150
- expect(result).toContain("default profile");
151
- });
152
-
153
- test("returns message when no profiles exist", () => {
154
- const result = formatProfileTable([]);
155
- expect(result).toContain("No profiles found");
156
- });
157
-
158
- test("shows N/A for missing expiry date", () => {
159
- const statuses: ProfileStatus[] = [
160
- {
161
- name: "no-expiry",
162
- exists: true,
163
- isActive: true,
164
- expiresAt: null,
165
- isDefault: false,
166
- },
167
- ];
168
- const result = formatProfileTable(statuses);
169
- expect(result).toContain("no-expiry");
170
- });
171
- });
172
-
173
- describe("formatChatList", () => {
174
- test("renders chat list in table format", () => {
175
- const chats = makeChats();
176
- const result = formatChatList(chats);
177
-
178
- expect(result).toContain("ID");
179
- expect(result).toContain("TITLE");
180
- expect(result).toContain("DATE");
181
- expect(result).toContain("abc123");
182
- expect(result).toContain("Chat about TypeScript");
183
- });
184
-
185
- test("shows total count footer", () => {
186
- const result = formatChatList(makeChats());
187
- expect(result).toContain("Total: 2 conversations");
188
- });
189
-
190
- test("shows singular when only one chat", () => {
191
- const chats: ChatInfo[] = [
192
- { id: "solo", title: "Solo chat", isPinned: false, timestamp: Date.now() },
193
- ];
194
- const result = formatChatList(chats);
195
- expect(result).toContain("Total: 1 conversation");
196
- });
197
-
198
- test("returns message when no chats exist", () => {
199
- const result = formatChatList([]);
200
- expect(result).toContain("No conversations found");
201
- });
202
- });
@@ -1,197 +0,0 @@
1
- import { describe, test, expect, beforeEach, afterEach } from "bun:test";
2
- import { join } from "node:path";
3
- import { tmpdir } from "node:os";
4
- import {
5
- ensureDir,
6
- existsFile,
7
- readTextFile,
8
- safeReadTextFile,
9
- writeTextFile,
10
- readJsonFile,
11
- writeJsonFile,
12
- removeDir,
13
- renameDir,
14
- isDirectory,
15
- listSubdirectories,
16
- IOError,
17
- } from "../../src/infrastructure/io.ts";
18
-
19
- const TEST_ROOT = join(tmpdir(), `gemiterm-io-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
20
-
21
- beforeEach(() => {
22
- ensureDir(TEST_ROOT);
23
- });
24
-
25
- afterEach(() => {
26
- try {
27
- removeDir(TEST_ROOT);
28
- } catch {
29
- // ignore
30
- }
31
- });
32
-
33
- describe("IOError", () => {
34
- test("has correct name and message", () => {
35
- const err = new IOError("boom");
36
- expect(err.name).toBe("IOError");
37
- expect(err.message).toBe("boom");
38
- expect(err.cause).toBeUndefined();
39
- });
40
-
41
- test("preserves cause", () => {
42
- const cause = new Error("underlying");
43
- const err = new IOError("wrapper", cause);
44
- expect(err.cause).toBe(cause);
45
- });
46
- });
47
-
48
- describe("ensureDir", () => {
49
- test("creates a directory that does not exist", () => {
50
- const dir = join(TEST_ROOT, "new-dir");
51
- expect(existsFile(dir)).toBe(false);
52
- ensureDir(dir);
53
- expect(existsFile(dir)).toBe(true);
54
- expect(isDirectory(dir)).toBe(true);
55
- });
56
-
57
- test("creates nested directories recursively", () => {
58
- const dir = join(TEST_ROOT, "a", "b", "c");
59
- expect(existsFile(dir)).toBe(false);
60
- ensureDir(dir);
61
- expect(isDirectory(dir)).toBe(true);
62
- expect(isDirectory(join(TEST_ROOT, "a", "b"))).toBe(true);
63
- expect(isDirectory(join(TEST_ROOT, "a"))).toBe(true);
64
- });
65
-
66
- test("is a no-op on an existing directory", () => {
67
- ensureDir(TEST_ROOT);
68
- expect(() => ensureDir(TEST_ROOT)).not.toThrow();
69
- });
70
- });
71
-
72
- describe("existsFile", () => {
73
- test("returns true for an existing file", () => {
74
- const file = join(TEST_ROOT, "marker.txt");
75
- ensureDir(TEST_ROOT);
76
- writeTextFile(file, "x");
77
- expect(existsFile(file)).toBe(true);
78
- });
79
-
80
- test("returns false for a missing file", () => {
81
- expect(existsFile(join(TEST_ROOT, "nope.txt"))).toBe(false);
82
- });
83
- });
84
-
85
- describe("readTextFile", () => {
86
- test("reads an existing file as UTF-8", () => {
87
- const file = join(TEST_ROOT, "read.txt");
88
- writeTextFile(file, "hello");
89
- expect(readTextFile(file)).toBe("hello");
90
- });
91
-
92
- test("throws IOError on a missing file", () => {
93
- expect(() => readTextFile(join(TEST_ROOT, "nope.txt"))).toThrow(IOError);
94
- });
95
- });
96
-
97
- describe("safeReadTextFile", () => {
98
- test("returns the content of an existing file", () => {
99
- const file = join(TEST_ROOT, "safe.txt");
100
- writeTextFile(file, "content");
101
- expect(safeReadTextFile(file)).toBe("content");
102
- });
103
-
104
- test("returns the empty string on a missing file", () => {
105
- expect(safeReadTextFile(join(TEST_ROOT, "nope.txt"))).toBe("");
106
- });
107
- });
108
-
109
- describe("writeTextFile", () => {
110
- test("writes a new file", () => {
111
- const file = join(TEST_ROOT, "out.txt");
112
- writeTextFile(file, "data");
113
- expect(existsFile(file)).toBe(true);
114
- expect(readTextFile(file)).toBe("data");
115
- });
116
-
117
- test("creates nested directories that do not exist", () => {
118
- const file = join(TEST_ROOT, "nested", "deep", "out.txt");
119
- writeTextFile(file, "data");
120
- expect(readTextFile(file)).toBe("data");
121
- });
122
- });
123
-
124
- describe("readJsonFile and writeJsonFile", () => {
125
- test("round-trips a JSON object", () => {
126
- const file = join(TEST_ROOT, "obj.json");
127
- const original = { a: 1, b: ["x", "y"], c: { nested: true } };
128
- writeJsonFile(file, original);
129
- const read = readJsonFile<typeof original>(file);
130
- expect(read).toEqual(original);
131
- });
132
-
133
- test("readJsonFile throws on missing file", () => {
134
- expect(() => readJsonFile(join(TEST_ROOT, "missing.json"))).toThrow(IOError);
135
- });
136
- });
137
-
138
- describe("removeDir", () => {
139
- test("removes a directory recursively", () => {
140
- const dir = join(TEST_ROOT, "rm");
141
- ensureDir(dir);
142
- writeTextFile(join(dir, "f.txt"), "x");
143
- expect(existsFile(dir)).toBe(true);
144
- removeDir(dir);
145
- expect(existsFile(dir)).toBe(false);
146
- });
147
-
148
- test("is a no-op on a missing path", () => {
149
- expect(() => removeDir(join(TEST_ROOT, "nope"))).not.toThrow();
150
- });
151
- });
152
-
153
- describe("renameDir", () => {
154
- test("renames a directory", () => {
155
- const src = join(TEST_ROOT, "src");
156
- const dest = join(TEST_ROOT, "dest");
157
- ensureDir(src);
158
- expect(isDirectory(src)).toBe(true);
159
- renameDir(src, dest);
160
- expect(isDirectory(src)).toBe(false);
161
- expect(isDirectory(dest)).toBe(true);
162
- });
163
-
164
- test("throws when source is missing", () => {
165
- expect(() => renameDir(join(TEST_ROOT, "nope"), join(TEST_ROOT, "dest"))).toThrow(IOError);
166
- });
167
- });
168
-
169
- describe("isDirectory", () => {
170
- test("returns true for a directory", () => {
171
- expect(isDirectory(TEST_ROOT)).toBe(true);
172
- });
173
-
174
- test("returns false for a file", () => {
175
- const file = join(TEST_ROOT, "f.txt");
176
- writeTextFile(file, "x");
177
- expect(isDirectory(file)).toBe(false);
178
- });
179
-
180
- test("returns false for a missing path", () => {
181
- expect(isDirectory(join(TEST_ROOT, "nope"))).toBe(false);
182
- });
183
- });
184
-
185
- describe("listSubdirectories", () => {
186
- test("returns only subdirectory names", () => {
187
- ensureDir(join(TEST_ROOT, "a"));
188
- ensureDir(join(TEST_ROOT, "b"));
189
- writeTextFile(join(TEST_ROOT, "not-a-dir.txt"), "x");
190
- const subs = listSubdirectories(TEST_ROOT).sort();
191
- expect(subs).toEqual(["a", "b"]);
192
- });
193
-
194
- test("returns an empty array for a missing path", () => {
195
- expect(listSubdirectories(join(TEST_ROOT, "nope"))).toEqual([]);
196
- });
197
- });
@@ -1,74 +0,0 @@
1
- import { describe, test, expect, beforeEach, afterEach, spyOn } from "bun:test";
2
- import { Logger } from "../../src/infrastructure/logger.ts";
3
-
4
- describe("Logger", () => {
5
- let stderrSpy: ReturnType<typeof spyOn>;
6
-
7
- beforeEach(() => {
8
- stderrSpy = spyOn(process.stderr, "write").mockImplementation(() => true);
9
- Logger.setVerbose(false);
10
- });
11
-
12
- afterEach(() => {
13
- stderrSpy.mockRestore();
14
- });
15
-
16
- test("info writes to stderr with correct format", () => {
17
- const logger = new Logger("test-module");
18
- logger.info("hello world");
19
-
20
- expect(stderrSpy).toHaveBeenCalledTimes(1);
21
- const output = stderrSpy.mock.calls[0][0] as string;
22
- expect(output).toMatch(/^\[\d{4}-\d{2}-\d{2}T/);
23
- expect(output).toContain("[INFO] [test-module] hello world");
24
- });
25
-
26
- test("warn writes to stderr with WARN level", () => {
27
- const logger = new Logger("cli");
28
- logger.warn("something happened");
29
-
30
- const output = stderrSpy.mock.calls[0][0] as string;
31
- expect(output).toContain("[WARN] [cli] something happened");
32
- });
33
-
34
- test("error writes to stderr with ERROR level", () => {
35
- const logger = new Logger("auth");
36
- logger.error("failed");
37
-
38
- const output = stderrSpy.mock.calls[0][0] as string;
39
- expect(output).toContain("[ERROR] [auth] failed");
40
- });
41
-
42
- test("debug is silent when verbose is off", () => {
43
- const logger = new Logger("test");
44
- logger.debug("should not appear");
45
-
46
- expect(stderrSpy).toHaveBeenCalledTimes(0);
47
- });
48
-
49
- test("debug writes to stderr when verbose is on", () => {
50
- Logger.setVerbose(true);
51
- const logger = new Logger("test");
52
- logger.debug("verbose message");
53
-
54
- expect(stderrSpy).toHaveBeenCalledTimes(1);
55
- const output = stderrSpy.mock.calls[0][0] as string;
56
- expect(output).toContain("[DEBUG] [test] verbose message");
57
- });
58
-
59
- test("extra args are appended to message", () => {
60
- const logger = new Logger("svc");
61
- logger.info("result", 42, "extra");
62
-
63
- const output = stderrSpy.mock.calls[0][0] as string;
64
- expect(output).toContain("[svc] result 42 extra");
65
- });
66
-
67
- test("isVerbose returns current verbose state", () => {
68
- expect(Logger.isVerbose()).toBe(false);
69
- Logger.setVerbose(true);
70
- expect(Logger.isVerbose()).toBe(true);
71
- Logger.setVerbose(false);
72
- expect(Logger.isVerbose()).toBe(false);
73
- });
74
- });