@polderlabs/bizar 4.5.2 → 4.7.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.
- package/bizar-dash/dist/assets/main-DGGq-iZI.js +361 -0
- package/bizar-dash/dist/assets/main-DGGq-iZI.js.map +1 -0
- package/bizar-dash/dist/assets/{mobile-lbH6szyX.js → mobile-CWqPoGaT.js} +18 -18
- package/bizar-dash/dist/assets/mobile-CWqPoGaT.js.map +1 -0
- package/bizar-dash/dist/assets/{mobile-BRhoDOUz.js → mobile-i4Uv9eW8.js} +1 -1
- package/bizar-dash/dist/assets/{mobile-BRhoDOUz.js.map → mobile-i4Uv9eW8.js.map} +1 -1
- package/bizar-dash/dist/index.html +2 -2
- package/bizar-dash/dist/mobile.html +2 -2
- package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/bizar-dash/src/server/logger.mjs +71 -0
- package/bizar-dash/src/server/memory-lightrag.mjs +45 -6
- package/bizar-dash/src/server/metrics.mjs +193 -0
- package/bizar-dash/src/server/routes/chat.mjs +6 -3
- package/bizar-dash/src/server/routes/lightrag.mjs +3 -2
- package/bizar-dash/src/server/routes/memory.mjs +5 -4
- package/bizar-dash/src/server/routes/misc.mjs +2 -1
- package/bizar-dash/src/server/routes/overview.mjs +2 -1
- package/bizar-dash/src/server/server.mjs +62 -0
- package/bizar-dash/src/web/components/SearchModal.tsx +3 -2
- package/bizar-dash/src/web/components/VirtualList.tsx +53 -0
- package/bizar-dash/src/web/components/chat/ChatThread.tsx +17 -11
- package/bizar-dash/src/web/hooks/useI18n.ts +13 -0
- package/bizar-dash/src/web/lib/i18n.ts +25 -0
- package/bizar-dash/src/web/locales/en.json +52 -0
- package/bizar-dash/src/web/main.tsx +5 -0
- package/bizar-dash/src/web/views/Activity.tsx +24 -17
- package/bizar-dash/src/web/views/History.tsx +90 -75
- package/bizar-dash/src/web/views/Overview.tsx +11 -8
- package/bizar-dash/src/web/views/Settings.tsx +7 -1
- package/bizar-dash/tests/cli-bugfixes.test.mjs +4 -4
- package/bizar-dash/tests/cli-refactor.test.mjs +184 -0
- package/bizar-dash/tests/components/Button.test.tsx +41 -0
- package/bizar-dash/tests/components/Card.test.tsx +42 -0
- package/bizar-dash/tests/components/Modal.test.tsx +104 -0
- package/bizar-dash/tests/components/Spinner.test.tsx +32 -0
- package/bizar-dash/tests/components/StatusBadge.test.tsx +35 -0
- package/bizar-dash/tests/components/Toast.test.tsx +108 -0
- package/bizar-dash/tests/hooks/useModal.test.tsx +84 -0
- package/bizar-dash/tests/hooks/useToast.test.tsx +50 -0
- package/bizar-dash/tests/lib/i18n.test.ts +46 -0
- package/bizar-dash/tests/lib/utils.test.ts +194 -0
- package/bizar-dash/tests/logger.test.mjs +207 -0
- package/bizar-dash/tests/metrics.test.mjs +183 -0
- package/bizar-dash/tests/server-bugfixes.test.mjs +2 -2
- package/bizar-dash/tests/setup.ts +7 -0
- package/bizar-dash/vitest.config.ts +13 -0
- package/cli/artifact-cli.mjs +605 -0
- package/cli/artifact-render.mjs +621 -0
- package/cli/artifact-server.mjs +847 -0
- package/cli/artifact.mjs +38 -2096
- package/cli/bg.mjs +5 -13
- package/cli/bin.mjs +149 -1361
- package/cli/commands/artifact.mjs +20 -0
- package/cli/commands/dash.mjs +160 -0
- package/cli/commands/headroom.mjs +204 -0
- package/cli/commands/install.mjs +169 -0
- package/cli/commands/memory.mjs +25 -0
- package/cli/commands/minimax.mjs +265 -0
- package/cli/commands/mod.mjs +185 -0
- package/cli/commands/service.mjs +65 -0
- package/cli/commands/usage.mjs +109 -0
- package/cli/commands/util.mjs +306 -0
- package/cli/doctor.mjs +1 -13
- package/cli/provision.mjs +2 -13
- package/cli/service-controller.mjs +1 -11
- package/cli/service.mjs +1 -11
- package/cli/utils.mjs +41 -1
- package/package.json +6 -1
- package/bizar-dash/dist/assets/main-B4OfGAwz.js +0 -361
- package/bizar-dash/dist/assets/main-B4OfGAwz.js.map +0 -1
- package/bizar-dash/dist/assets/mobile-lbH6szyX.js.map +0 -1
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli/artifact-cli.mjs
|
|
3
|
+
*
|
|
4
|
+
* Artifact CLI dispatch and flag parsing.
|
|
5
|
+
* Extracted from artifact.mjs to separate CLI from server and rendering logic.
|
|
6
|
+
*/
|
|
7
|
+
import {
|
|
8
|
+
readFileSync,
|
|
9
|
+
existsSync,
|
|
10
|
+
mkdirSync,
|
|
11
|
+
rmSync,
|
|
12
|
+
} from 'node:fs';
|
|
13
|
+
import { join, resolve, dirname, basename, isAbsolute } from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
import { spawn } from 'node:child_process';
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
getTemplate,
|
|
19
|
+
getTemplateNames,
|
|
20
|
+
listTemplates,
|
|
21
|
+
printTemplates,
|
|
22
|
+
substitute,
|
|
23
|
+
buildVars,
|
|
24
|
+
saveTemplate as saveTemplateToLibrary,
|
|
25
|
+
deleteTemplate as deleteLibraryTemplate,
|
|
26
|
+
} from './artifact-templates.mjs';
|
|
27
|
+
|
|
28
|
+
import {
|
|
29
|
+
CANVAS_SCHEMA_VERSION,
|
|
30
|
+
emptyCanvas,
|
|
31
|
+
readCanvasFile,
|
|
32
|
+
writeCanvasFile,
|
|
33
|
+
loadOrMigrateCanvas,
|
|
34
|
+
canvasToMarkdown,
|
|
35
|
+
makeElementId,
|
|
36
|
+
makeConnectionId,
|
|
37
|
+
makeCommentId,
|
|
38
|
+
makeReplyId,
|
|
39
|
+
readPlanMeta,
|
|
40
|
+
renderElementHTML,
|
|
41
|
+
renderConnectionHTML,
|
|
42
|
+
renderCommentPinHTML,
|
|
43
|
+
renderCommentThreadHTML,
|
|
44
|
+
renderReplyHTML,
|
|
45
|
+
escapeHtml,
|
|
46
|
+
formatDate,
|
|
47
|
+
replaceTemplate,
|
|
48
|
+
readTemplate,
|
|
49
|
+
atomicWriteText,
|
|
50
|
+
atomicWriteJson,
|
|
51
|
+
openBrowser,
|
|
52
|
+
} from './artifact-render.mjs';
|
|
53
|
+
|
|
54
|
+
import {
|
|
55
|
+
startServer,
|
|
56
|
+
} from './artifact-server.mjs';
|
|
57
|
+
|
|
58
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
59
|
+
const PROJECT_ROOT = resolve(__dirname, '..');
|
|
60
|
+
const TEMPLATES_DIR = join(PROJECT_ROOT, 'templates', 'artifact');
|
|
61
|
+
const PLANS_DIR = join(PROJECT_ROOT, 'artifacts');
|
|
62
|
+
const MAX_REQUEST_BODY_BYTES = 5 * 1024 * 1024;
|
|
63
|
+
|
|
64
|
+
// ─── Slug validation ─────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
const SLUG_REGEX = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
67
|
+
|
|
68
|
+
function validateSlug(slug) {
|
|
69
|
+
if (!slug || !SLUG_REGEX.test(slug)) {
|
|
70
|
+
console.error(
|
|
71
|
+
` ✗ Invalid slug "${slug}". Slug must be lowercase, may contain hyphens,\n` +
|
|
72
|
+
` must start with an alphanumeric character, and be 1–64 characters.`
|
|
73
|
+
);
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ─── Title case ──────────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
function titleCase(str) {
|
|
82
|
+
return str
|
|
83
|
+
.split('-')
|
|
84
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
85
|
+
.join(' ');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ─── Template resolution ─────────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Resolve the content for a new artifact from the template system.
|
|
92
|
+
*/
|
|
93
|
+
async function resolveTemplateContent(template, vars) {
|
|
94
|
+
if (template == null || template === '' || template === 'blank') {
|
|
95
|
+
const tpl = await readTemplate('artifact.mdx.template');
|
|
96
|
+
return { content: replaceTemplate(tpl, vars), templateName: 'blank', source: 'built-in' };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Absolute path to a user file
|
|
100
|
+
if (isAbsolute(template) || template.endsWith('.mdx') || template.startsWith('.')) {
|
|
101
|
+
let p = template;
|
|
102
|
+
if (!isAbsolute(p)) p = resolve(p);
|
|
103
|
+
if (existsSync(p)) {
|
|
104
|
+
const fileContent = readFileSync(p, 'utf-8');
|
|
105
|
+
return {
|
|
106
|
+
content: substitute(fileContent, vars),
|
|
107
|
+
templateName: basename(p).replace(/\.mdx$/, ''),
|
|
108
|
+
source: 'file',
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Built-in name
|
|
114
|
+
const tpl = getTemplate(template);
|
|
115
|
+
if (!tpl) {
|
|
116
|
+
const available = getTemplateNames().join(', ');
|
|
117
|
+
throw new Error(
|
|
118
|
+
`Unknown template "${template}".\n` +
|
|
119
|
+
` Built-in templates: ${available}.\n` +
|
|
120
|
+
` Or pass an absolute path to a .mdx file.`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (tpl.content == null) {
|
|
125
|
+
const blankTpl = await readTemplate('artifact.mdx.template');
|
|
126
|
+
return { content: replaceTemplate(blankTpl, vars), templateName: 'blank', source: 'built-in' };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return { content: substitute(tpl.content, vars), templateName: tpl.name, source: tpl.source };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ─── Artifact operations ───────────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
async function createArtifact(slug, { template = null } = {}) {
|
|
135
|
+
const planDir = join(PLANS_DIR, slug);
|
|
136
|
+
|
|
137
|
+
// Step 1: validate
|
|
138
|
+
if (!validateSlug(slug)) return false;
|
|
139
|
+
|
|
140
|
+
// Step 2: check existence
|
|
141
|
+
if (existsSync(planDir)) {
|
|
142
|
+
console.error(` ✗ Artifact "${slug}" already exists at ${planDir}`);
|
|
143
|
+
console.error(` Use "bizar artifact open ${slug}" to open it.`);
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Step 3: create directory
|
|
148
|
+
mkdirSync(planDir, { recursive: true });
|
|
149
|
+
|
|
150
|
+
// Step 4: generate values
|
|
151
|
+
const now = new Date().toISOString();
|
|
152
|
+
const title = titleCase(slug);
|
|
153
|
+
const author = process.env.USER || 'unknown';
|
|
154
|
+
const vars = {
|
|
155
|
+
title,
|
|
156
|
+
slug,
|
|
157
|
+
author,
|
|
158
|
+
created: now,
|
|
159
|
+
lastEdited: now,
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// Step 5: artifact.mdx (from template if --template was given)
|
|
163
|
+
let mdxContent;
|
|
164
|
+
let templateName = 'blank';
|
|
165
|
+
try {
|
|
166
|
+
const resolved = await resolveTemplateContent(template, vars);
|
|
167
|
+
mdxContent = resolved.content;
|
|
168
|
+
templateName = resolved.templateName;
|
|
169
|
+
} catch (err) {
|
|
170
|
+
console.error(` ✗ ${err.message}`);
|
|
171
|
+
rmSync(planDir, { recursive: true, force: true });
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
writePlanFile(slug, 'artifact.mdx', mdxContent);
|
|
175
|
+
|
|
176
|
+
// Step 6: meta.json
|
|
177
|
+
const metaTemplate = await readTemplate('meta.json.template');
|
|
178
|
+
const metaContent = replaceTemplate(metaTemplate, vars);
|
|
179
|
+
writePlanFile(slug, 'meta.json', metaContent);
|
|
180
|
+
|
|
181
|
+
// Step 7: comments.json (stored as empty array for comments)
|
|
182
|
+
writePlanFile(slug, 'comments.json', JSON.stringify([], null, 2));
|
|
183
|
+
|
|
184
|
+
// Step 8: artifact.html (regenerate from template)
|
|
185
|
+
await regenerateHtml(slug);
|
|
186
|
+
|
|
187
|
+
console.log(` ✓ Created artifact "${title}" (slug: ${slug})`);
|
|
188
|
+
if (templateName !== 'blank') {
|
|
189
|
+
console.log(` Template: ${templateName}`);
|
|
190
|
+
}
|
|
191
|
+
console.log(` → ${planDir}`);
|
|
192
|
+
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export async function regenerateHtml(slug) {
|
|
197
|
+
const planDir = join(PLANS_DIR, slug);
|
|
198
|
+
if (!existsSync(planDir)) return;
|
|
199
|
+
|
|
200
|
+
// Prefer the v2 canvas template. Fall back to the v1 template.
|
|
201
|
+
let htmlTemplate;
|
|
202
|
+
let templateName = 'artifact.html.template';
|
|
203
|
+
try {
|
|
204
|
+
htmlTemplate = await readTemplate('artifact.canvas.template');
|
|
205
|
+
templateName = 'artifact.canvas.template';
|
|
206
|
+
} catch {
|
|
207
|
+
try {
|
|
208
|
+
htmlTemplate = await readTemplate('artifact.html.template');
|
|
209
|
+
templateName = 'artifact.html.template';
|
|
210
|
+
} catch {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const planMdx = readFileSync(join(planDir, 'artifact.mdx'), 'utf-8');
|
|
216
|
+
const metaJson = readFileSync(join(planDir, 'meta.json'), 'utf-8');
|
|
217
|
+
const commentsJson = readFileSync(join(planDir, 'comments.json'), 'utf-8');
|
|
218
|
+
const meta = JSON.parse(metaJson);
|
|
219
|
+
const planJson = JSON.stringify(planMdx);
|
|
220
|
+
|
|
221
|
+
let canvasJson = 'null';
|
|
222
|
+
try {
|
|
223
|
+
const canvas = loadOrMigrateCanvas(planDir, meta.title || slug);
|
|
224
|
+
canvasJson = JSON.stringify(canvas);
|
|
225
|
+
} catch {
|
|
226
|
+
// ignore
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const vars = {
|
|
230
|
+
title: meta.title || slug,
|
|
231
|
+
slug: meta.slug || slug,
|
|
232
|
+
status: meta.status || 'draft',
|
|
233
|
+
created: meta.created || new Date().toISOString(),
|
|
234
|
+
lastEdited: meta.lastEdited || new Date().toISOString(),
|
|
235
|
+
author: meta.author || 'unknown',
|
|
236
|
+
planJson,
|
|
237
|
+
commentsJson,
|
|
238
|
+
metaJson,
|
|
239
|
+
canvasJson,
|
|
240
|
+
templateName,
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const htmlContent = replaceTemplate(htmlTemplate, vars);
|
|
244
|
+
atomicWriteText(join(planDir, 'artifact.html'), htmlContent);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function openArtifact(slug) {
|
|
248
|
+
if (!validateSlug(slug)) return false;
|
|
249
|
+
|
|
250
|
+
const planDir = join(PLANS_DIR, slug);
|
|
251
|
+
if (!existsSync(planDir)) {
|
|
252
|
+
console.error(` ✗ Artifact "${slug}" not found at ${planDir}`);
|
|
253
|
+
console.error(` Use "bizar artifact new ${slug}" to create it.`);
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Regenerate HTML to bake in current state
|
|
258
|
+
await regenerateHtml(slug);
|
|
259
|
+
|
|
260
|
+
// Start server
|
|
261
|
+
const { port, close } = await startServer(slug, planDir);
|
|
262
|
+
|
|
263
|
+
const url = `http://localhost:${port}/${slug}/`;
|
|
264
|
+
console.log(` ✓ Opening artifact "${slug}" at ${url}`);
|
|
265
|
+
openBrowser(url);
|
|
266
|
+
|
|
267
|
+
// Keep process alive until Ctrl-C
|
|
268
|
+
await waitForSignal(close);
|
|
269
|
+
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async function listPlans() {
|
|
274
|
+
if (!existsSync(PLANS_DIR)) {
|
|
275
|
+
console.log(' No artifacts found. Run `bizar artifact new <slug>` to create one.');
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const { readdirSync } = await import('node:fs');
|
|
280
|
+
const dirs = readdirSync(PLANS_DIR).filter((d) => {
|
|
281
|
+
return existsSync(join(PLANS_DIR, d, 'meta.json'));
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
if (dirs.length === 0) {
|
|
285
|
+
console.log(' No artifacts found. Run `bizar artifact new <slug>` to create one.');
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Read meta for each
|
|
290
|
+
const artifacts = [];
|
|
291
|
+
for (const dir of dirs) {
|
|
292
|
+
try {
|
|
293
|
+
const meta = JSON.parse(readFileSync(join(PLANS_DIR, dir, 'meta.json'), 'utf-8'));
|
|
294
|
+
artifacts.push(meta);
|
|
295
|
+
} catch {
|
|
296
|
+
// Skip invalid meta.json
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Sort by lastEdited newest first
|
|
301
|
+
artifacts.sort((a, b) => new Date(b.lastEdited) - new Date(a.lastEdited));
|
|
302
|
+
|
|
303
|
+
// Print table
|
|
304
|
+
console.log(' Slug Title Status Last Edited Author');
|
|
305
|
+
console.log(' ────────────── ────────────────────── ──────── ──────────────────────── ───────────────');
|
|
306
|
+
|
|
307
|
+
for (const artifact of artifacts) {
|
|
308
|
+
const slug = (artifact.slug || '').padEnd(15);
|
|
309
|
+
const title = (artifact.title || '').substring(0, 23).padEnd(23);
|
|
310
|
+
const status = (artifact.status || 'draft').padEnd(9);
|
|
311
|
+
const lastEdited = (artifact.lastEdited || '').substring(0, 27).padEnd(27);
|
|
312
|
+
const author = (artifact.author || 'unknown').substring(0, 20);
|
|
313
|
+
console.log(` ${slug} ${title} ${status} ${lastEdited} ${author}`);
|
|
314
|
+
}
|
|
315
|
+
console.log();
|
|
316
|
+
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async function deleteArtifact(slug) {
|
|
321
|
+
if (!validateSlug(slug)) return false;
|
|
322
|
+
|
|
323
|
+
const planDir = join(PLANS_DIR, slug);
|
|
324
|
+
if (!existsSync(planDir)) {
|
|
325
|
+
console.error(` ✗ Artifact "${slug}" not found.`);
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
let title = slug;
|
|
330
|
+
try {
|
|
331
|
+
const meta = JSON.parse(readFileSync(join(planDir, 'meta.json'), 'utf-8'));
|
|
332
|
+
title = meta.title || slug;
|
|
333
|
+
} catch { /* use slug as fallback */ }
|
|
334
|
+
|
|
335
|
+
// Read confirm from stdin
|
|
336
|
+
const answer = await question(` Delete artifact "${title}" (slug: ${slug})? This is irreversible. [y/N] `);
|
|
337
|
+
|
|
338
|
+
if (answer.trim().toLowerCase() === 'y') {
|
|
339
|
+
rmSync(planDir, { recursive: true });
|
|
340
|
+
console.log(` ✓ Deleted artifact "${slug}".`);
|
|
341
|
+
return true;
|
|
342
|
+
} else {
|
|
343
|
+
console.log(' Cancelled.');
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async function exportArtifact(slug) {
|
|
349
|
+
if (!validateSlug(slug)) return false;
|
|
350
|
+
|
|
351
|
+
const planDir = join(PLANS_DIR, slug);
|
|
352
|
+
if (!existsSync(planDir)) {
|
|
353
|
+
console.error(` ✗ Artifact "${slug}" not found.`);
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Prefer the v2 canvas-derived markdown when artifact.json exists.
|
|
358
|
+
const canvasFile = join(planDir, 'artifact.json');
|
|
359
|
+
if (existsSync(canvasFile)) {
|
|
360
|
+
try {
|
|
361
|
+
const canvas = JSON.parse(readFileSync(canvasFile, 'utf-8'));
|
|
362
|
+
process.stdout.write(canvasToMarkdown(canvas));
|
|
363
|
+
return true;
|
|
364
|
+
} catch {
|
|
365
|
+
// fall through to mdx export
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const planFile = join(planDir, 'artifact.mdx');
|
|
370
|
+
if (!existsSync(planFile)) {
|
|
371
|
+
console.error(` ✗ Artifact "${slug}" not found.`);
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const content = readFileSync(planFile, 'utf-8');
|
|
376
|
+
process.stdout.write(content);
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
381
|
+
|
|
382
|
+
function question(prompt) {
|
|
383
|
+
return new Promise((resolve) => {
|
|
384
|
+
process.stdout.write(prompt);
|
|
385
|
+
process.stdin.once('data', (data) => resolve(data.toString()));
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function waitForSignal(closeFn) {
|
|
390
|
+
return new Promise((resolve) => {
|
|
391
|
+
const finish = async () => {
|
|
392
|
+
await closeFn();
|
|
393
|
+
process.off('SIGINT', finish);
|
|
394
|
+
process.off('SIGTERM', finish);
|
|
395
|
+
resolve();
|
|
396
|
+
};
|
|
397
|
+
process.on('SIGINT', finish);
|
|
398
|
+
process.on('SIGTERM', finish);
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function writePlanFile(slug, filename, content) {
|
|
403
|
+
const dir = join(PLANS_DIR, slug);
|
|
404
|
+
mkdirSync(dir, { recursive: true });
|
|
405
|
+
atomicWriteText(join(dir, filename), content);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// ─── Help ────────────────────────────────────────────────────────────────────
|
|
409
|
+
|
|
410
|
+
export function showHelp() {
|
|
411
|
+
console.log(`
|
|
412
|
+
bizar artifact <subcommand> [options]
|
|
413
|
+
|
|
414
|
+
Subcommands:
|
|
415
|
+
new <slug> [--template <name>] Create a new artifact (default: blank template)
|
|
416
|
+
open <slug> Open an existing artifact in the browser
|
|
417
|
+
list List all artifacts
|
|
418
|
+
delete <slug> Delete a artifact (with confirmation)
|
|
419
|
+
export <slug> Export artifact.mdx to stdout
|
|
420
|
+
templates List available artifact templates
|
|
421
|
+
template save <name> <artifact-slug> Save a artifact as a library template
|
|
422
|
+
template delete <name> Delete a user-added library template
|
|
423
|
+
help Show this help
|
|
424
|
+
|
|
425
|
+
Plans are stored in artifacts/<slug>/ as:
|
|
426
|
+
- artifact.mdx source content (in git)
|
|
427
|
+
- artifact.html viewer/editor (gitignored)
|
|
428
|
+
- comments.json comments array (gitignored)
|
|
429
|
+
- meta.json metadata (in git)
|
|
430
|
+
|
|
431
|
+
Built-in templates:
|
|
432
|
+
blank Empty starter (the v1 default)
|
|
433
|
+
feature-design For designing a new feature
|
|
434
|
+
bug-investigation For investigating a bug
|
|
435
|
+
decision-record Architecture Decision Record (ADR)
|
|
436
|
+
|
|
437
|
+
Examples:
|
|
438
|
+
bizar artifact new my-feature
|
|
439
|
+
bizar artifact new auth-v2 --template feature-design
|
|
440
|
+
bizar artifact new oops --template bug-investigation
|
|
441
|
+
bizar artifact templates
|
|
442
|
+
bizar artifact open my-feature
|
|
443
|
+
bizar artifact list
|
|
444
|
+
bizar artifact export my-feature > my-feature.mdx
|
|
445
|
+
`);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// ─── Main dispatcher ──────────────────────────────────────────────────────────
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Parse a string of CLI args into { positional, flags }.
|
|
452
|
+
* Supports --flag value and --flag=value styles. Booleans (no value)
|
|
453
|
+
* are stored as true.
|
|
454
|
+
*/
|
|
455
|
+
function parseArgs(argv) {
|
|
456
|
+
const positional = [];
|
|
457
|
+
const flags = {};
|
|
458
|
+
for (let i = 0; i < argv.length; i++) {
|
|
459
|
+
const a = argv[i];
|
|
460
|
+
if (a.startsWith('--')) {
|
|
461
|
+
const eq = a.indexOf('=');
|
|
462
|
+
if (eq !== -1) {
|
|
463
|
+
flags[a.slice(2, eq)] = a.slice(eq + 1);
|
|
464
|
+
} else {
|
|
465
|
+
const key = a.slice(2);
|
|
466
|
+
const next = argv[i + 1];
|
|
467
|
+
if (next != null && !next.startsWith('--')) {
|
|
468
|
+
flags[key] = next;
|
|
469
|
+
i++;
|
|
470
|
+
} else {
|
|
471
|
+
flags[key] = true;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
} else {
|
|
475
|
+
positional.push(a);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return { positional, flags };
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Main entry point for `bizar artifact`.
|
|
483
|
+
* Subcommands: new, open, list, delete, export, templates,
|
|
484
|
+
* template save <name> <artifact-slug>, template delete <name>, help
|
|
485
|
+
*/
|
|
486
|
+
export async function runArtifact(argsOrPositional, legacyFlags) {
|
|
487
|
+
let positional;
|
|
488
|
+
let flags;
|
|
489
|
+
if (Array.isArray(argsOrPositional)) {
|
|
490
|
+
const parsed = parseArgs(argsOrPositional);
|
|
491
|
+
positional = parsed.positional;
|
|
492
|
+
flags = { ...(legacyFlags || {}), ...parsed.flags };
|
|
493
|
+
} else if (argsOrPositional && Array.isArray(argsOrPositional.positional)) {
|
|
494
|
+
positional = argsOrPositional.positional;
|
|
495
|
+
flags = { ...(legacyFlags || {}), ...(argsOrPositional.flags || {}) };
|
|
496
|
+
} else {
|
|
497
|
+
positional = [];
|
|
498
|
+
flags = legacyFlags || {};
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Special-case: "template save <name> <artifact-slug>" / "template delete <name>"
|
|
502
|
+
if (positional[0] === 'template') {
|
|
503
|
+
const action = positional[1];
|
|
504
|
+
if (action === 'save') {
|
|
505
|
+
const name = positional[2];
|
|
506
|
+
const planSlug = positional[3];
|
|
507
|
+
if (!name || !planSlug) {
|
|
508
|
+
console.error(' ✗ Usage: bizar artifact template save <name> <artifact-slug>');
|
|
509
|
+
return false;
|
|
510
|
+
}
|
|
511
|
+
try {
|
|
512
|
+
const target = saveTemplateToLibrary(name, planSlug);
|
|
513
|
+
console.log(` ✓ Saved template "${name}" → ${target}`);
|
|
514
|
+
return true;
|
|
515
|
+
} catch (err) {
|
|
516
|
+
console.error(` ✗ ${err.message}`);
|
|
517
|
+
return false;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
if (action === 'delete' || action === 'rm') {
|
|
521
|
+
const name = positional[2];
|
|
522
|
+
if (!name) {
|
|
523
|
+
console.error(' ✗ Usage: bizar artifact template delete <name>');
|
|
524
|
+
return false;
|
|
525
|
+
}
|
|
526
|
+
try {
|
|
527
|
+
const removed = deleteLibraryTemplate(name);
|
|
528
|
+
console.log(` ✓ Removed template "${name}" from library (${removed})`);
|
|
529
|
+
return true;
|
|
530
|
+
} catch (err) {
|
|
531
|
+
console.error(` ✗ ${err.message}`);
|
|
532
|
+
return false;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
if (action === 'list' || action === undefined) {
|
|
536
|
+
printTemplates();
|
|
537
|
+
return true;
|
|
538
|
+
}
|
|
539
|
+
console.error(` ✗ Unknown template action: "${action}"`);
|
|
540
|
+
console.error(' Use: save <name> <artifact-slug>, delete <name>, or list');
|
|
541
|
+
return false;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const [subcommand, slug] = positional;
|
|
545
|
+
|
|
546
|
+
switch (subcommand) {
|
|
547
|
+
case 'new': {
|
|
548
|
+
if (!slug) {
|
|
549
|
+
console.error(' ✗ Usage: bizar artifact new <slug> [--template <name>]');
|
|
550
|
+
return false;
|
|
551
|
+
}
|
|
552
|
+
const created = await createArtifact(slug, { template: flags.template || null });
|
|
553
|
+
if (!created) return false;
|
|
554
|
+
return await openArtifact(slug);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
case 'open': {
|
|
558
|
+
if (!slug) {
|
|
559
|
+
console.error(' ✗ Usage: bizar artifact open <slug>');
|
|
560
|
+
return false;
|
|
561
|
+
}
|
|
562
|
+
return await openArtifact(slug);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
case 'list': {
|
|
566
|
+
return await listPlans();
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
case 'delete': {
|
|
570
|
+
if (!slug) {
|
|
571
|
+
console.error(' ✗ Usage: bizar artifact delete <slug>');
|
|
572
|
+
return false;
|
|
573
|
+
}
|
|
574
|
+
return await deleteArtifact(slug);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
case 'export': {
|
|
578
|
+
if (!slug) {
|
|
579
|
+
console.error(' ✗ Usage: bizar artifact export <slug>');
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
return await exportArtifact(slug);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
case 'templates': {
|
|
586
|
+
printTemplates();
|
|
587
|
+
return true;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
case 'help':
|
|
591
|
+
case undefined: {
|
|
592
|
+
showHelp();
|
|
593
|
+
return true;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
default: {
|
|
597
|
+
console.error(` ✗ Unknown subcommand: "${subcommand}"`);
|
|
598
|
+
console.error(` Run "bizar artifact help" for usage.`);
|
|
599
|
+
return false;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Default export for CLI entrypoint
|
|
605
|
+
export default runArtifact;
|