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,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
- });