@polderlabs/bizar 4.5.2 → 4.7.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 (123) hide show
  1. package/bizar-dash/dist/assets/main-DHZmbnxQ.js +361 -0
  2. package/bizar-dash/dist/assets/main-DHZmbnxQ.js.map +1 -0
  3. package/bizar-dash/dist/assets/main-DX_Jh8Wc.css +1 -0
  4. package/bizar-dash/dist/assets/{mobile-lbH6szyX.js → mobile-BK8-ythT.js} +18 -18
  5. package/bizar-dash/dist/assets/mobile-BK8-ythT.js.map +1 -0
  6. package/bizar-dash/dist/assets/{mobile-BRhoDOUz.js → mobile-Chvf9u_B.js} +1 -1
  7. package/bizar-dash/dist/assets/{mobile-BRhoDOUz.js.map → mobile-Chvf9u_B.js.map} +1 -1
  8. package/bizar-dash/dist/index.html +3 -3
  9. package/bizar-dash/dist/mobile.html +2 -2
  10. package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  11. package/bizar-dash/skills/publishing/SKILL.md +146 -0
  12. package/bizar-dash/src/server/api.mjs +8 -0
  13. package/bizar-dash/src/server/backup-store.mjs +525 -0
  14. package/bizar-dash/src/server/digest-store.mjs +558 -0
  15. package/bizar-dash/src/server/lib/rate-limit.mjs +122 -0
  16. package/bizar-dash/src/server/logger.mjs +71 -0
  17. package/bizar-dash/src/server/memory-lightrag.mjs +45 -6
  18. package/bizar-dash/src/server/metrics.mjs +193 -0
  19. package/bizar-dash/src/server/routes/backup.mjs +112 -0
  20. package/bizar-dash/src/server/routes/chat.mjs +20 -3
  21. package/bizar-dash/src/server/routes/digests.mjs +82 -0
  22. package/bizar-dash/src/server/routes/lightrag.mjs +3 -2
  23. package/bizar-dash/src/server/routes/memory.mjs +5 -4
  24. package/bizar-dash/src/server/routes/misc.mjs +2 -1
  25. package/bizar-dash/src/server/routes/overview.mjs +2 -1
  26. package/bizar-dash/src/server/routes-v2/events.mjs +14 -0
  27. package/bizar-dash/src/server/schedules-runner.mjs +126 -0
  28. package/bizar-dash/src/server/server.mjs +79 -0
  29. package/bizar-dash/src/web/App.tsx +8 -1
  30. package/bizar-dash/src/web/components/BackupRestore.tsx +330 -0
  31. package/bizar-dash/src/web/components/SearchModal.tsx +6 -3
  32. package/bizar-dash/src/web/components/VirtualList.tsx +53 -0
  33. package/bizar-dash/src/web/components/chat/ChatThread.tsx +17 -11
  34. package/bizar-dash/src/web/components/chat/Composer.tsx +2 -0
  35. package/bizar-dash/src/web/hooks/useI18n.ts +13 -0
  36. package/bizar-dash/src/web/lib/i18n.ts +25 -0
  37. package/bizar-dash/src/web/locales/en.json +52 -0
  38. package/bizar-dash/src/web/main.tsx +5 -0
  39. package/bizar-dash/src/web/styles/main.css +70 -8
  40. package/bizar-dash/src/web/views/Activity.tsx +35 -18
  41. package/bizar-dash/src/web/views/Agents.tsx +57 -42
  42. package/bizar-dash/src/web/views/Artifacts.tsx +38 -25
  43. package/bizar-dash/src/web/views/Chat.tsx +8 -0
  44. package/bizar-dash/src/web/views/History.tsx +94 -76
  45. package/bizar-dash/src/web/views/MiniMaxUsage.tsx +21 -4
  46. package/bizar-dash/src/web/views/Mods.tsx +30 -17
  47. package/bizar-dash/src/web/views/Overview.tsx +19 -9
  48. package/bizar-dash/src/web/views/Providers.tsx +16 -16
  49. package/bizar-dash/src/web/views/Schedules.tsx +33 -15
  50. package/bizar-dash/src/web/views/Settings.tsx +97 -1745
  51. package/bizar-dash/src/web/views/Skills.tsx +4 -1
  52. package/bizar-dash/src/web/views/Tasks.tsx +11 -2
  53. package/bizar-dash/src/web/views/memory/ConfigPanel.tsx +8 -2
  54. package/bizar-dash/src/web/views/memory/LightragPanel.tsx +3 -0
  55. package/bizar-dash/src/web/views/memory/ObsidianPanel.tsx +12 -4
  56. package/bizar-dash/src/web/views/memory/SemanticSearchPanel.tsx +3 -0
  57. package/bizar-dash/src/web/views/settings/ActivitySection.tsx +205 -0
  58. package/bizar-dash/src/web/views/settings/AgentSection.tsx +294 -0
  59. package/bizar-dash/src/web/views/settings/AuthSection.tsx +159 -0
  60. package/bizar-dash/src/web/views/settings/BackupSection.tsx +16 -0
  61. package/bizar-dash/src/web/views/settings/EnvVarsSection.tsx +16 -0
  62. package/bizar-dash/src/web/views/settings/GeneralSection.tsx +105 -0
  63. package/bizar-dash/src/web/views/settings/HeadroomSection.tsx +39 -0
  64. package/bizar-dash/src/web/views/settings/MemorySection.tsx +16 -0
  65. package/bizar-dash/src/web/views/settings/NetworkSection.tsx +87 -0
  66. package/bizar-dash/src/web/views/settings/NotificationsSection.tsx +34 -0
  67. package/bizar-dash/src/web/views/settings/ProvidersSection.tsx +16 -0
  68. package/bizar-dash/src/web/views/settings/SkillsSection.tsx +16 -0
  69. package/bizar-dash/src/web/views/settings/SystemLlmSection.tsx +81 -0
  70. package/bizar-dash/src/web/views/settings/ThemeSection.tsx +168 -0
  71. package/bizar-dash/src/web/views/settings/UpdatesSection.tsx +256 -0
  72. package/bizar-dash/tests/a11y.test.tsx +206 -0
  73. package/bizar-dash/tests/backup-restore.test.mjs +217 -0
  74. package/bizar-dash/tests/backup-restore.test.tsx +123 -0
  75. package/bizar-dash/tests/backup-store.test.mjs +300 -0
  76. package/bizar-dash/tests/cli-bugfixes.test.mjs +4 -4
  77. package/bizar-dash/tests/cli-error-visibility.test.mjs +153 -0
  78. package/bizar-dash/tests/cli-refactor.test.mjs +184 -0
  79. package/bizar-dash/tests/components/Button.test.tsx +41 -0
  80. package/bizar-dash/tests/components/Card.test.tsx +42 -0
  81. package/bizar-dash/tests/components/Modal.test.tsx +104 -0
  82. package/bizar-dash/tests/components/Spinner.test.tsx +32 -0
  83. package/bizar-dash/tests/components/StatusBadge.test.tsx +35 -0
  84. package/bizar-dash/tests/components/Toast.test.tsx +108 -0
  85. package/bizar-dash/tests/digest-generation.test.mjs +191 -0
  86. package/bizar-dash/tests/digest-store.test.mjs +264 -0
  87. package/bizar-dash/tests/hooks/useModal.test.tsx +84 -0
  88. package/bizar-dash/tests/hooks/useToast.test.tsx +50 -0
  89. package/bizar-dash/tests/lib/i18n.test.ts +46 -0
  90. package/bizar-dash/tests/lib/utils.test.ts +194 -0
  91. package/bizar-dash/tests/logger.test.mjs +207 -0
  92. package/bizar-dash/tests/metrics.test.mjs +183 -0
  93. package/bizar-dash/tests/rate-limit.test.mjs +298 -0
  94. package/bizar-dash/tests/server-bugfixes.test.mjs +2 -2
  95. package/bizar-dash/tests/setup.ts +7 -0
  96. package/bizar-dash/vitest.config.ts +13 -0
  97. package/cli/artifact-cli.mjs +605 -0
  98. package/cli/artifact-render.mjs +621 -0
  99. package/cli/artifact-server.mjs +847 -0
  100. package/cli/artifact.mjs +38 -2096
  101. package/cli/bg.mjs +5 -13
  102. package/cli/bin.mjs +221 -1350
  103. package/cli/commands/artifact.mjs +20 -0
  104. package/cli/commands/dash.mjs +160 -0
  105. package/cli/commands/headroom.mjs +204 -0
  106. package/cli/commands/install.mjs +169 -0
  107. package/cli/commands/memory.mjs +25 -0
  108. package/cli/commands/minimax.mjs +285 -0
  109. package/cli/commands/mod.mjs +185 -0
  110. package/cli/commands/service.mjs +65 -0
  111. package/cli/commands/usage.mjs +109 -0
  112. package/cli/commands/util.mjs +459 -0
  113. package/cli/digest.mjs +149 -0
  114. package/cli/doctor.mjs +1 -13
  115. package/cli/provision.mjs +2 -13
  116. package/cli/service-controller.mjs +1 -11
  117. package/cli/service.mjs +1 -11
  118. package/cli/utils.mjs +41 -1
  119. package/package.json +6 -1
  120. package/bizar-dash/dist/assets/main-B4OfGAwz.js +0 -361
  121. package/bizar-dash/dist/assets/main-B4OfGAwz.js.map +0 -1
  122. package/bizar-dash/dist/assets/main-DAlLdW8I.css +0 -1
  123. package/bizar-dash/dist/assets/mobile-lbH6szyX.js.map +0 -1
@@ -0,0 +1,300 @@
1
+ /**
2
+ * tests/backup-store.test.mjs
3
+ *
4
+ * v4.8.0 — Tests for the backup-store module.
5
+ */
6
+ import { describe, it, beforeEach, afterEach } from 'node:test';
7
+ import assert from 'node:assert';
8
+ import { tmpdir } from 'node:os';
9
+ import { join } from 'node:path';
10
+ import {
11
+ mkdirSync,
12
+ rmSync,
13
+ writeFileSync,
14
+ existsSync,
15
+ readFileSync,
16
+ readdirSync,
17
+ } from 'node:fs';
18
+
19
+ const BACKUP_STORE = await import('../src/server/backup-store.mjs').then((m) => m);
20
+
21
+ describe('backup-store', () => {
22
+ let backupRoot;
23
+ let configDir;
24
+ let opencodeDir;
25
+ let memoryDir;
26
+
27
+ beforeEach(() => {
28
+ const id = `bizar-backup-test-${Date.now()}-${Math.random().toString(36).slice(2)}`;
29
+ backupRoot = join(tmpdir(), id, 'backups');
30
+ configDir = join(tmpdir(), id, 'config', 'bizar');
31
+ opencodeDir = join(tmpdir(), id, 'config', 'opencode');
32
+ memoryDir = join(tmpdir(), id, 'local', 'share', 'bizar', 'memory');
33
+
34
+ mkdirSync(configDir, { recursive: true });
35
+ mkdirSync(opencodeDir, { recursive: true });
36
+ mkdirSync(memoryDir, { recursive: true });
37
+ mkdirSync(backupRoot, { recursive: true });
38
+
39
+ // Create some test files
40
+ writeFileSync(join(configDir, 'settings.json'), JSON.stringify({ theme: { mode: 'dark' } }), { mode: 0o600 });
41
+ writeFileSync(join(opencodeDir, 'opencode.json'), JSON.stringify({ models: [] }), { mode: 0o600 });
42
+ writeFileSync(join(memoryDir, 'test.md'), '# Test memory', { mode: 0o600 });
43
+ });
44
+
45
+ afterEach(() => {
46
+ try { rmSync(backupRoot, { recursive: true, force: true }); } catch { /* ignore */ }
47
+ try { rmSync(join(tmpdir(), `bizar-backup-test-${Date.now()}-${Math.random().toString(36).slice(2)}`), { recursive: true, force: true }); } catch { /* ignore */ }
48
+ });
49
+
50
+ // ── createBackup ─────────────────────────────────────────────────────────────
51
+
52
+ describe('createBackup', () => {
53
+ it('creates a timestamped backup directory', async () => {
54
+ const result = await BACKUP_STORE.createBackup({ outDir: backupRoot });
55
+ assert.strictEqual(result.ok, true);
56
+ assert.ok(result.path.includes('bizar-'));
57
+ assert.ok(existsSync(result.path));
58
+ });
59
+
60
+ it('writes a manifest.json with version, createdAt, and paths', async () => {
61
+ const result = await BACKUP_STORE.createBackup({ outDir: backupRoot });
62
+ const manifestPath = join(result.path, 'manifest.json');
63
+ assert.ok(existsSync(manifestPath));
64
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
65
+ assert.ok(manifest.version);
66
+ assert.ok(manifest.createdAt);
67
+ assert.ok(Array.isArray(manifest.paths));
68
+ });
69
+
70
+ it('copies existing files to the backup', async () => {
71
+ const result = await BACKUP_STORE.createBackup({ outDir: backupRoot });
72
+ const configBackup = join(result.path, 'config');
73
+ assert.ok(existsSync(configBackup));
74
+ const settingsBackup = join(configBackup, 'settings.json');
75
+ assert.ok(existsSync(settingsBackup));
76
+ });
77
+
78
+ it('skips non-existent optional paths', async () => {
79
+ const result = await BACKUP_STORE.createBackup({ outDir: backupRoot });
80
+ const manifestPath = join(result.path, 'manifest.json');
81
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
82
+ // Optional paths that don't exist should be in skipped list
83
+ assert.ok(manifest.skipped);
84
+ });
85
+
86
+ it('respects custom label in name and manifest', async () => {
87
+ const result = await BACKUP_STORE.createBackup({ outDir: backupRoot, label: 'test-label' });
88
+ assert.ok(result.path.includes('test-label'));
89
+ const manifest = JSON.parse(readFileSync(join(result.path, 'manifest.json'), 'utf8'));
90
+ assert.strictEqual(manifest.label, 'test-label');
91
+ });
92
+
93
+ it('returns sizeBytes and durationMs', async () => {
94
+ const result = await BACKUP_STORE.createBackup({ outDir: backupRoot });
95
+ assert.ok(typeof result.sizeBytes === 'number');
96
+ assert.ok(result.sizeBytes >= 0);
97
+ assert.ok(typeof result.durationMs === 'number');
98
+ assert.ok(result.durationMs >= 0);
99
+ });
100
+ });
101
+
102
+ // ── listBackups ──────────────────────────────────────────────────────────────
103
+
104
+ describe('listBackups', () => {
105
+ it('returns empty array when no backups exist', async () => {
106
+ const emptyDir = join(tmpdir(), `bizar-empty-${Date.now()}-${Math.random().toString(36).slice(2)}`);
107
+ mkdirSync(emptyDir, { recursive: true });
108
+ try {
109
+ const backups = await BACKUP_STORE.listBackups({ inDir: emptyDir });
110
+ assert.strictEqual(backups.length, 0);
111
+ } finally {
112
+ rmSync(emptyDir, { recursive: true, force: true });
113
+ }
114
+ });
115
+
116
+ it('returns all backups sorted newest-first', async () => {
117
+ await BACKUP_STORE.createBackup({ outDir: backupRoot });
118
+ await BACKUP_STORE.createBackup({ outDir: backupRoot, label: 'second' });
119
+ const backups = await BACKUP_STORE.listBackups({ inDir: backupRoot });
120
+ assert.strictEqual(backups.length, 2);
121
+ assert.strictEqual(backups[0].manifest?.label, 'second');
122
+ assert.strictEqual(backups[1].manifest?.label, null);
123
+ });
124
+
125
+ it('includes createdAt, sizeBytes, sizeFormatted, and manifest', async () => {
126
+ await BACKUP_STORE.createBackup({ outDir: backupRoot });
127
+ const backups = await BACKUP_STORE.listBackups({ inDir: backupRoot });
128
+ assert.ok(backups[0].createdAt);
129
+ assert.ok(typeof backups[0].sizeBytes === 'number');
130
+ assert.ok(typeof backups[0].sizeFormatted === 'string');
131
+ assert.ok(backups[0].manifest);
132
+ });
133
+ });
134
+
135
+ // ── restoreBackup (dryRun) ───────────────────────────────────────────────────
136
+
137
+ describe('restoreBackup (dryRun)', () => {
138
+ it('does not modify any files when dryRun is true', async () => {
139
+ const result = await BACKUP_STORE.createBackup({ outDir: backupRoot });
140
+ const settingsFile = join(configDir, 'settings.json');
141
+ const originalContent = readFileSync(settingsFile, 'utf8');
142
+ // Modify the settings file
143
+ writeFileSync(settingsFile, JSON.stringify({ theme: { mode: 'light' } }), { mode: 0o600 });
144
+ const restoreResult = await BACKUP_STORE.restoreBackup({
145
+ backupPath: result.path,
146
+ dryRun: true,
147
+ conflictStrategy: 'overwrite',
148
+ });
149
+ assert.strictEqual(restoreResult.ok, true);
150
+ // File should still have the modified content
151
+ assert.strictEqual(readFileSync(settingsFile, 'utf8'), JSON.stringify({ theme: { mode: 'light' } }));
152
+ });
153
+
154
+ it('reports what would be restored in dryRun mode', async () => {
155
+ const result = await BACKUP_STORE.createBackup({ outDir: backupRoot });
156
+ const restoreResult = await BACKUP_STORE.restoreBackup({
157
+ backupPath: result.path,
158
+ dryRun: true,
159
+ conflictStrategy: 'merge',
160
+ });
161
+ assert.ok(restoreResult.restored.length > 0);
162
+ });
163
+ });
164
+
165
+ // ── restoreBackup (overwrite) ────────────────────────────────────────────────
166
+
167
+ describe('restoreBackup (overwrite)', () => {
168
+ it('reports overwritten entries when conflictStrategy is overwrite (dryRun)', async () => {
169
+ const result = await BACKUP_STORE.createBackup({ outDir: backupRoot });
170
+ // dryRun=true means no actual overwrite happens, but we can verify
171
+ // the restored list includes the backed-up entries
172
+ const restoreResult = await BACKUP_STORE.restoreBackup({
173
+ backupPath: result.path,
174
+ dryRun: true,
175
+ conflictStrategy: 'overwrite',
176
+ });
177
+ assert.strictEqual(restoreResult.ok, true);
178
+ // In dryRun mode, entries that would be overwritten are listed in restored
179
+ assert.ok(restoreResult.restored.length > 0);
180
+ });
181
+
182
+ it('overwrites a manually-created backup correctly', async () => {
183
+ // Create a manual backup structure with project-state label (which is in PROJECT_BACKUP_PATHS)
184
+ const testBackupDir = join(backupRoot, 'bizar-test-overwrite-manual');
185
+ const testProjectRoot = join(tmpdir(), `backup-overwrite-test-${Date.now()}`);
186
+ mkdirSync(testBackupDir, { recursive: true });
187
+ mkdirSync(testProjectRoot, { recursive: true });
188
+ mkdirSync(join(testBackupDir, 'project-state'), { recursive: true });
189
+
190
+ // Write original content to backup
191
+ const originalContent = { theme: { mode: 'dark' } };
192
+ writeFileSync(join(testBackupDir, 'project-state', 'state.json'), JSON.stringify(originalContent), { mode: 0o600 });
193
+ // Use new manifest format with {label, src} objects
194
+ writeFileSync(
195
+ join(testBackupDir, 'manifest.json'),
196
+ JSON.stringify({
197
+ version: '1.0.0',
198
+ createdAt: new Date().toISOString(),
199
+ paths: [{ label: 'project-state', src: '.bizar/' }],
200
+ }),
201
+ { mode: 0o600 },
202
+ );
203
+
204
+ // Restore with overwrite — project-state is in PROJECT_BACKUP_PATHS, so it will restore
205
+ const restoreResult = await BACKUP_STORE.restoreBackup({
206
+ backupPath: testBackupDir,
207
+ dryRun: false,
208
+ conflictStrategy: 'overwrite',
209
+ projectRoot: testProjectRoot,
210
+ });
211
+ // The entry should be restored (it's in PROJECT_BACKUP_PATHS)
212
+ assert.ok(restoreResult.ok);
213
+
214
+ // Cleanup
215
+ rmSync(testProjectRoot, { recursive: true, force: true });
216
+ });
217
+ });
218
+
219
+ // ── verifyBackup ─────────────────────────────────────────────────────────────
220
+
221
+ describe('verifyBackup', () => {
222
+ it('returns ok=true for a valid backup', async () => {
223
+ const result = await BACKUP_STORE.createBackup({ outDir: backupRoot });
224
+ const verifyResult = await BACKUP_STORE.verifyBackup({ backupPath: result.path });
225
+ assert.strictEqual(verifyResult.ok, true);
226
+ assert.strictEqual(verifyResult.issues.length, 0);
227
+ });
228
+
229
+ it('returns ok=false when manifest is missing', async () => {
230
+ const backupDir = join(backupRoot, 'bizar-test-no-manifest');
231
+ mkdirSync(backupDir, { recursive: true });
232
+ writeFileSync(join(backupDir, 'somefile.txt'), 'test', { mode: 0o600 });
233
+ const verifyResult = await BACKUP_STORE.verifyBackup({ backupPath: backupDir });
234
+ assert.strictEqual(verifyResult.ok, false);
235
+ assert.ok(verifyResult.issues.length > 0);
236
+ });
237
+
238
+ it('detects missing backup entries', async () => {
239
+ const result = await BACKUP_STORE.createBackup({ outDir: backupRoot });
240
+ // Remove a backed-up directory
241
+ rmSync(join(result.path, 'config'), { recursive: true, force: true });
242
+ const verifyResult = await BACKUP_STORE.verifyBackup({ backupPath: result.path });
243
+ assert.strictEqual(verifyResult.ok, false);
244
+ assert.ok(verifyResult.issues.some((i) => i.includes('config')));
245
+ });
246
+
247
+ it('returns ok=false when backup directory does not exist', async () => {
248
+ const verifyResult = await BACKUP_STORE.verifyBackup({
249
+ backupPath: join(tmpdir(), 'nonexistent-backup-path'),
250
+ });
251
+ assert.strictEqual(verifyResult.ok, false);
252
+ });
253
+ });
254
+
255
+ // ── deleteBackup ─────────────────────────────────────────────────────────────
256
+
257
+ describe('deleteBackup', () => {
258
+ it('removes the backup directory', async () => {
259
+ const result = await BACKUP_STORE.createBackup({ outDir: backupRoot });
260
+ assert.ok(existsSync(result.path));
261
+ const deleteResult = await BACKUP_STORE.deleteBackup({ backupPath: result.path });
262
+ assert.strictEqual(deleteResult.ok, true);
263
+ assert.ok(!existsSync(result.path));
264
+ });
265
+
266
+ it('returns ok=false when backup does not exist', async () => {
267
+ const deleteResult = await BACKUP_STORE.deleteBackup({
268
+ backupPath: join(tmpdir(), 'nonexistent-backup-path'),
269
+ });
270
+ assert.strictEqual(deleteResult.ok, false);
271
+ assert.ok(deleteResult.error);
272
+ });
273
+ });
274
+
275
+ // ── getManifest ──────────────────────────────────────────────────────────────
276
+
277
+ describe('getManifest', () => {
278
+ it('returns manifest when backup exists', async () => {
279
+ const result = await BACKUP_STORE.createBackup({ outDir: backupRoot });
280
+ const manifestResult = await BACKUP_STORE.getManifest({ backupPath: result.path });
281
+ assert.strictEqual(manifestResult.ok, true);
282
+ assert.ok(manifestResult.manifest);
283
+ assert.ok(manifestResult.manifest.version);
284
+ });
285
+
286
+ it('returns ok=false when manifest is missing', async () => {
287
+ const backupDir = join(backupRoot, 'bizar-no-manifest');
288
+ mkdirSync(backupDir, { recursive: true });
289
+ const manifestResult = await BACKUP_STORE.getManifest({ backupPath: backupDir });
290
+ assert.strictEqual(manifestResult.ok, false);
291
+ });
292
+
293
+ it('returns ok=false when backup does not exist', async () => {
294
+ const manifestResult = await BACKUP_STORE.getManifest({
295
+ backupPath: join(tmpdir(), 'nonexistent'),
296
+ });
297
+ assert.strictEqual(manifestResult.ok, false);
298
+ });
299
+ });
300
+ });
@@ -20,7 +20,7 @@ const REPO = join(process.cwd());
20
20
  describe('B1 — parseWithModsFlag', () => {
21
21
  it('exits with code 2 when --with-mods is the last arg', async () => {
22
22
  // Import fresh each time to avoid module-level side effects
23
- const { parseWithModsFlag } = await import('../../cli/bin.mjs');
23
+ const { parseWithModsFlag } = await import('../../cli/commands/install.mjs');
24
24
  let exitCode = null;
25
25
  const origExit = process.exit;
26
26
  // Mock process.exit to capture the code without terminating the test
@@ -36,7 +36,7 @@ describe('B1 — parseWithModsFlag', () => {
36
36
  });
37
37
 
38
38
  it('exits with code 2 when next arg is another flag', async () => {
39
- const { parseWithModsFlag } = await import('../../cli/bin.mjs');
39
+ const { parseWithModsFlag } = await import('../../cli/commands/install.mjs');
40
40
  let exitCode = null;
41
41
  const origExit = process.exit;
42
42
  try {
@@ -51,7 +51,7 @@ describe('B1 — parseWithModsFlag', () => {
51
51
  });
52
52
 
53
53
  it('returns null when --with-mods is not present', async () => {
54
- const { parseWithModsFlag } = await import('../../cli/bin.mjs');
54
+ const { parseWithModsFlag } = await import('../../cli/commands/install.mjs');
55
55
  let exitCode = null;
56
56
  const origExit = process.exit;
57
57
  try {
@@ -65,7 +65,7 @@ describe('B1 — parseWithModsFlag', () => {
65
65
  });
66
66
 
67
67
  it('returns array of mod ids when value is provided', async () => {
68
- const { parseWithModsFlag } = await import('../../cli/bin.mjs');
68
+ const { parseWithModsFlag } = await import('../../cli/commands/install.mjs');
69
69
  let exitCode = null;
70
70
  const origExit = process.exit;
71
71
  try {
@@ -0,0 +1,153 @@
1
+ /**
2
+ * bizar-dash/tests/cli-error-visibility.test.mjs
3
+ *
4
+ * v4.7.1 — CLI silent error visibility fixes.
5
+ *
6
+ * Verifies:
7
+ * 1. importCommand() failure produces a visible error message
8
+ * 2. `bizar help` shows help text (not "Unknown command")
9
+ * 3. `bizar --help` shows help text
10
+ * 4. `bizar` (no command) shows help text
11
+ *
12
+ * Run with: node --test bizar-dash/tests/cli-error-visibility.test.mjs
13
+ */
14
+ import { test, describe } from 'node:test';
15
+ import assert from 'node:assert/strict';
16
+ import { spawnSync } from 'node:child_process';
17
+ import { resolve, dirname } from 'node:path';
18
+ import { fileURLToPath } from 'node:url';
19
+
20
+ const __dirname = dirname(fileURLToPath(import.meta.url));
21
+
22
+ // __dirname = .../BizarHarness/bizar-dash/tests → .../BizarHarness/cli
23
+ const CLI_ROOT = resolve(__dirname, '..', '..', 'cli');
24
+
25
+ // ── helpers ───────────────────────────────────────────────────────────────────
26
+
27
+ /**
28
+ * Run the CLI as a subprocess and capture stdout + stderr.
29
+ * Uses spawnSync for reliable synchronous capture.
30
+ * Returns { code, stdout, stderr }.
31
+ */
32
+ function runBizar(args, env = {}) {
33
+ const binPath = resolve(CLI_ROOT, 'bin.mjs');
34
+ const result = spawnSync('node', [binPath, ...args], {
35
+ env: { ...process.env, BIZAR_SKIP_INSTALL: '1', ...env },
36
+ timeout: 10000,
37
+ encoding: 'utf8',
38
+ });
39
+ return {
40
+ code: result.status,
41
+ stdout: result.stdout || '',
42
+ stderr: result.stderr || '',
43
+ };
44
+ }
45
+
46
+ // ── importCommand() failure ───────────────────────────────────────────────────
47
+
48
+ describe('importCommand() failure produces visible error', () => {
49
+ test('non-existent command prints "Unknown command", not silent exit', () => {
50
+ const { code, stdout, stderr } = runBizar(['this-command-does-not-exist-xyz']);
51
+ const combined = stdout + stderr;
52
+ // Should exit with error code
53
+ assert.notStrictEqual(code, 0, `should exit non-zero, got ${code}`);
54
+ // Should NOT be silent
55
+ assert.ok(combined.length > 0, `should produce output, not silent. stdout=${stdout.length}, stderr=${stderr.length}`);
56
+ // Should mention the unknown command
57
+ assert.ok(
58
+ combined.includes('Unknown command') || combined.includes('this-command-does-not-exist-xyz'),
59
+ `output should mention Unknown command or the command name, got: ${combined.slice(0, 200)}`,
60
+ );
61
+ });
62
+
63
+ test('broken command module logs error to stderr (not silent)', () => {
64
+ // The bin.mjs importCommand catch now logs to console.error.
65
+ // We verify stderr is non-empty when a command fails to load.
66
+ const { stderr } = runBizar(['this-command-does-not-exist-xyz']);
67
+ assert.ok(stderr.length > 0, `stderr should not be empty when command fails. stderr=${JSON.stringify(stderr.slice(0, 100))}`);
68
+ });
69
+ });
70
+
71
+ // ── `bizar help` alias ─────────────────────────────────────────────────────────
72
+
73
+ describe('`bizar help` alias', () => {
74
+ test('`bizar help` shows help text, not "Unknown command"', () => {
75
+ const { code, stdout, stderr } = runBizar(['help']);
76
+ const combined = stdout + stderr;
77
+ assert.strictEqual(code, 0, `should exit with 0, got ${code}. stderr: ${stderr.slice(0, 300)}`);
78
+ assert.ok(
79
+ combined.includes('Usage:') && combined.includes('bizar'),
80
+ `should show help usage, got: ${combined.slice(0, 300)}`,
81
+ );
82
+ assert.ok(
83
+ !combined.includes('Unknown command'),
84
+ `should NOT say "Unknown command", got: ${combined.slice(0, 300)}`,
85
+ );
86
+ });
87
+ });
88
+
89
+ // ── `bizar --help` ─────────────────────────────────────────────────────────────
90
+
91
+ describe('`bizar --help`', () => {
92
+ test('`bizar --help` shows help text', () => {
93
+ const { code, stdout, stderr } = runBizar(['--help']);
94
+ const combined = stdout + stderr;
95
+ assert.strictEqual(code, 0, `should exit with 0, got ${code}. stderr: ${stderr.slice(0, 300)}`);
96
+ assert.ok(
97
+ combined.includes('Usage:') && combined.includes('bizar'),
98
+ `should show help usage, got: ${combined.slice(0, 300)}`,
99
+ );
100
+ });
101
+
102
+ test('`bizar -h` shows help text', () => {
103
+ const { code, stdout, stderr } = runBizar(['-h']);
104
+ const combined = stdout + stderr;
105
+ assert.strictEqual(code, 0, `should exit with 0, got ${code}. stderr: ${stderr.slice(0, 300)}`);
106
+ assert.ok(
107
+ combined.includes('Usage:') && combined.includes('bizar'),
108
+ `should show help usage, got: ${combined.slice(0, 300)}`,
109
+ );
110
+ });
111
+ });
112
+
113
+ // ── `bizar` (no command) ───────────────────────────────────────────────────────
114
+
115
+ describe('`bizar` with no command', () => {
116
+ test('`bizar` with no args shows help text', () => {
117
+ const { code, stdout, stderr } = runBizar([]);
118
+ const combined = stdout + stderr;
119
+ assert.strictEqual(code, 0, `should exit with 0, got ${code}. stderr: ${stderr.slice(0, 300)}`);
120
+ assert.ok(
121
+ combined.includes('Usage:') && combined.includes('bizar'),
122
+ `should show help usage, got: ${combined.slice(0, 300)}`,
123
+ );
124
+ assert.ok(
125
+ !combined.includes('Unknown command'),
126
+ `should NOT say "Unknown command", got: ${combined.slice(0, 300)}`,
127
+ );
128
+ });
129
+ });
130
+
131
+ // ── Defensive null checks on import ───────────────────────────────────────────
132
+
133
+ describe('command module null-check produces user-visible error', () => {
134
+ test('bin.mjs has null checks after importCommand in all case branches', async () => {
135
+ const binSrc = await import('fs').then((fs) =>
136
+ fs.promises.readFile(resolve(CLI_ROOT, 'bin.mjs'), 'utf8'),
137
+ );
138
+ // Count `if (!mod)` checks that follow `importCommand`
139
+ const nullCheckCount = (binSrc.match(/if \(!mod\)/g) || []).length;
140
+ // We have 9 case groups that call importCommand: install/update, service, dash,
141
+ // minimax, headroom, mod, artifact, memory, usage, and util (utility commands)
142
+ assert.ok(
143
+ nullCheckCount >= 9,
144
+ `Expected at least 9 null checks after importCommand, found ${nullCheckCount}`,
145
+ );
146
+ // Also verify each has a user-visible error message
147
+ const errorMsgCount = (binSrc.match(/Could not load .+ command module/g) || []).length;
148
+ assert.ok(
149
+ errorMsgCount >= 9,
150
+ `Expected at least 9 user-visible error messages for failed loads, found ${errorMsgCount}`,
151
+ );
152
+ });
153
+ });
@@ -0,0 +1,184 @@
1
+ /**
2
+ * bizar-dash/tests/cli-refactor.test.mjs
3
+ *
4
+ * v4.6 — CLI refactor tests.
5
+ *
6
+ * Verifies:
7
+ * 1. cli/bin.mjs imports from cli/commands/*.mjs correctly
8
+ * 2. Each command module exports a `run` function
9
+ * 3. cli/utils.mjs exports `which()` and `bizarConfigDir()`
10
+ * 4. cli/artifact-cli.mjs, cli/artifact-server.mjs, cli/artifact-render.mjs
11
+ * exist and export the right things
12
+ * 5. cli/artifact.mjs is a backward-compatible re-export shell
13
+ */
14
+ import { test, describe } from 'node:test';
15
+ import assert from 'node:assert/strict';
16
+ import { resolve, dirname } from 'node:path';
17
+ import { fileURLToPath } from 'node:url';
18
+ import { readdirSync } from 'node:fs';
19
+
20
+ const __dirname = dirname(fileURLToPath(import.meta.url));
21
+ const CLI_ROOT = resolve(__dirname, '..', '..', 'cli');
22
+ const COMMANDS_DIR = resolve(CLI_ROOT, 'commands');
23
+
24
+ // ── utils.mjs ────────────────────────────────────────────────────────────────
25
+
26
+ describe('cli/utils.mjs exports', () => {
27
+ test('exports which()', async () => {
28
+ const { which } = await import(`${CLI_ROOT}/utils.mjs`);
29
+ assert.equal(typeof which, 'function');
30
+ });
31
+
32
+ test('exports whichPath()', async () => {
33
+ const { whichPath } = await import(`${CLI_ROOT}/utils.mjs`);
34
+ assert.equal(typeof whichPath, 'function');
35
+ });
36
+
37
+ test('exports bizarConfigDir()', async () => {
38
+ const { bizarConfigDir } = await import(`${CLI_ROOT}/utils.mjs`);
39
+ assert.equal(typeof bizarConfigDir, 'function');
40
+ });
41
+
42
+ test('bizarConfigDir() returns a non-empty string', async () => {
43
+ const { bizarConfigDir } = await import(`${CLI_ROOT}/utils.mjs`);
44
+ const dir = bizarConfigDir();
45
+ assert.equal(typeof dir, 'string');
46
+ assert.ok(dir.length > 0);
47
+ assert.ok(dir.includes('bizar'));
48
+ });
49
+
50
+ test('which() returns boolean', async () => {
51
+ const { which } = await import(`${CLI_ROOT}/utils.mjs`);
52
+ const result = which('node');
53
+ assert.equal(typeof result, 'boolean');
54
+ });
55
+
56
+ test('which() returns false for nonexistent command', async () => {
57
+ const { which } = await import(`${CLI_ROOT}/utils.mjs`);
58
+ const result = which('this-command-does-not-exist-xyz');
59
+ assert.equal(result, false);
60
+ });
61
+
62
+ test('whichPath() returns null for nonexistent command', async () => {
63
+ const { whichPath } = await import(`${CLI_ROOT}/utils.mjs`);
64
+ const result = whichPath('this-command-does-not-exist-xyz');
65
+ assert.equal(result, null);
66
+ });
67
+ });
68
+
69
+ // ── Command modules ────────────────────────────────────────────────────────────
70
+
71
+ const COMMAND_FILES = [
72
+ 'install',
73
+ 'service',
74
+ 'dash',
75
+ 'minimax',
76
+ 'headroom',
77
+ 'mod',
78
+ 'artifact',
79
+ 'memory',
80
+ 'usage',
81
+ 'util',
82
+ ];
83
+
84
+ describe('cli/commands/*.mjs exports', () => {
85
+ for (const name of COMMAND_FILES) {
86
+ test(`${name}.mjs exports run()`, async () => {
87
+ const mod = await import(`${COMMANDS_DIR}/${name}.mjs`);
88
+ assert.equal(typeof mod.run, 'function', `${name}.mjs should export run()`);
89
+ });
90
+
91
+ // Note: calling run() directly would execute the command (network calls, etc.)
92
+ // which is not appropriate in a module structure test. The run() signature
93
+ // is validated via TypeScript / node --check instead.
94
+ }
95
+ });
96
+
97
+ // ── Artifact split ────────────────────────────────────────────────────────────
98
+
99
+ describe('artifact module split', () => {
100
+ test('artifact-cli.mjs exists and exports runArtifact', async () => {
101
+ const mod = await import(`${CLI_ROOT}/artifact-cli.mjs`);
102
+ assert.equal(typeof mod.runArtifact, 'function');
103
+ assert.equal(typeof mod.default, 'function');
104
+ });
105
+
106
+ test('artifact-cli.mjs exports showHelp', async () => {
107
+ const mod = await import(`${CLI_ROOT}/artifact-cli.mjs`);
108
+ assert.equal(typeof mod.showHelp, 'function');
109
+ });
110
+
111
+ test('artifact-cli.mjs exports regenerateHtml', async () => {
112
+ const mod = await import(`${CLI_ROOT}/artifact-cli.mjs`);
113
+ assert.equal(typeof mod.regenerateHtml, 'function');
114
+ });
115
+
116
+ test('artifact-server.mjs exists and exports startServer', async () => {
117
+ const mod = await import(`${CLI_ROOT}/artifact-server.mjs`);
118
+ assert.equal(typeof mod.startServer, 'function');
119
+ });
120
+
121
+ test('artifact-render.mjs exports CANVAS_SCHEMA_VERSION', async () => {
122
+ const mod = await import(`${CLI_ROOT}/artifact-render.mjs`);
123
+ assert.equal(mod.CANVAS_SCHEMA_VERSION, 2);
124
+ });
125
+
126
+ test('artifact-render.mjs exports canvas helpers', async () => {
127
+ const mod = await import(`${CLI_ROOT}/artifact-render.mjs`);
128
+ assert.equal(typeof mod.emptyCanvas, 'function');
129
+ assert.equal(typeof mod.readCanvasFile, 'function');
130
+ assert.equal(typeof mod.writeCanvasFile, 'function');
131
+ assert.equal(typeof mod.loadOrMigrateCanvas, 'function');
132
+ assert.equal(typeof mod.canvasToMarkdown, 'function');
133
+ });
134
+
135
+ test('artifact-render.mjs exports ID generators', async () => {
136
+ const mod = await import(`${CLI_ROOT}/artifact-render.mjs`);
137
+ assert.equal(typeof mod.makeElementId, 'function');
138
+ assert.equal(typeof mod.makeConnectionId, 'function');
139
+ assert.equal(typeof mod.makeCommentId, 'function');
140
+ assert.equal(typeof mod.makeReplyId, 'function');
141
+ });
142
+
143
+ test('artifact-render.mjs exports HTML renderers', async () => {
144
+ const mod = await import(`${CLI_ROOT}/artifact-render.mjs`);
145
+ assert.equal(typeof mod.renderElementHTML, 'function');
146
+ assert.equal(typeof mod.renderConnectionHTML, 'function');
147
+ assert.equal(typeof mod.renderCommentPinHTML, 'function');
148
+ assert.equal(typeof mod.escapeHtml, 'function');
149
+ assert.equal(typeof mod.formatDate, 'function');
150
+ });
151
+
152
+ test('artifact.mjs is a re-export shell (backward compat)', async () => {
153
+ const mod = await import(`${CLI_ROOT}/artifact.mjs`);
154
+ // Should re-export runArtifact
155
+ assert.equal(typeof mod.runArtifact, 'function');
156
+ // Should re-export runPlan as alias for runArtifact (backward compat)
157
+ assert.equal(typeof mod.runPlan, 'function');
158
+ // Should re-export startServer
159
+ assert.equal(typeof mod.startServer, 'function');
160
+ // Should re-export canvas helpers
161
+ assert.equal(typeof mod.emptyCanvas, 'function');
162
+ assert.equal(typeof mod.canvasToMarkdown, 'function');
163
+ // Should re-export render helpers
164
+ assert.equal(typeof mod.renderElementHTML, 'function');
165
+ assert.equal(typeof mod.escapeHtml, 'function');
166
+ // Should re-export regenerateHtml
167
+ assert.equal(typeof mod.regenerateHtml, 'function');
168
+ });
169
+ });
170
+
171
+ // ── bin.mjs structure ─────────────────────────────────────────────────────────
172
+
173
+ describe('cli/bin.mjs structure', () => {
174
+ test('bin.mjs is a valid ESM module', async () => {
175
+ // Just verify it can be parsed as ESM — don't run main()
176
+ const { readFileSync } = await import('node:fs');
177
+ const src = readFileSync(`${CLI_ROOT}/bin.mjs`, 'utf8');
178
+ assert.ok(src.includes('#!/usr/bin/env node'));
179
+ assert.ok(src.includes('export'));
180
+ assert.ok(src.includes('import'));
181
+ });
182
+ });
183
+
184
+ console.log(' cli-refactor.test.mjs loaded — run with: node --test bizar-dash/tests/cli-refactor.test.mjs');