@polderlabs/bizar 3.16.0 → 3.19.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/cli/{plan-templates.mjs → artifact-templates.mjs} +26 -26
- package/cli/{plan.mjs → artifact.mjs} +134 -134
- package/cli/{plan.test.mjs → artifact.test.mjs} +108 -108
- package/cli/banner.mjs +1 -1
- package/cli/bin.mjs +9 -33
- package/cli/install.mjs +1 -1
- package/cli/prompts.mjs +2 -2
- package/config/AGENTS.md +9 -51
- package/config/agents/baldr.md +2 -24
- package/config/agents/browser-harness.md +80 -0
- package/config/agents/forseti.md +1 -23
- package/config/agents/frigg.md +3 -24
- package/config/agents/heimdall.md +2 -24
- package/config/agents/hermod.md +1 -23
- package/config/agents/mimir.md +1 -23
- package/config/agents/odin.md +1 -25
- package/config/agents/quick.md +1 -18
- package/config/agents/semble-search.md +1 -1
- package/config/agents/thor.md +2 -29
- package/config/agents/tyr.md +2 -29
- package/config/agents/vidarr.md +1 -28
- package/config/agents/vor.md +6 -28
- package/config/opencode.json.template +2 -16
- package/config/rules/uncertainty.md +1 -1
- package/config/skills/bizar/SKILL.md +1 -1
- package/package.json +1 -1
- package/cli/graph-build-from-cache.mjs +0 -124
- package/cli/graph.mjs +0 -642
- package/cli/graph.test.mjs +0 -218
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* bizar
|
|
2
|
+
* bizar artifact <subcommand>
|
|
3
3
|
*
|
|
4
4
|
* Subcommands:
|
|
5
|
-
* new <slug> Create a new
|
|
5
|
+
* new <slug> Create a new artifact
|
|
6
6
|
* Flags: --template <name> | --template <path>
|
|
7
|
-
* open <slug> Open an existing
|
|
8
|
-
* list List all
|
|
9
|
-
* delete <slug> Delete a
|
|
10
|
-
* export <slug> Export
|
|
11
|
-
* templates List available
|
|
12
|
-
* template save <name> <
|
|
7
|
+
* open <slug> Open an existing artifact in the browser
|
|
8
|
+
* list List all artifacts
|
|
9
|
+
* delete <slug> Delete a artifact (with confirmation)
|
|
10
|
+
* export <slug> Export artifact.mdx to stdout
|
|
11
|
+
* templates List available artifact templates
|
|
12
|
+
* template save <name> <artifact-slug> Save a artifact as a library template
|
|
13
13
|
* template delete <name> Delete a user-added library template
|
|
14
14
|
* help Show this help
|
|
15
15
|
*
|
|
@@ -44,12 +44,12 @@ import {
|
|
|
44
44
|
buildVars,
|
|
45
45
|
saveTemplate as saveTemplateToLibrary,
|
|
46
46
|
deleteTemplate as deleteLibraryTemplate,
|
|
47
|
-
} from './
|
|
47
|
+
} from './artifact-templates.mjs';
|
|
48
48
|
|
|
49
49
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
50
50
|
const PROJECT_ROOT = resolve(__dirname, '..');
|
|
51
|
-
const TEMPLATES_DIR = join(PROJECT_ROOT, 'templates', '
|
|
52
|
-
const PLANS_DIR = join(PROJECT_ROOT, '
|
|
51
|
+
const TEMPLATES_DIR = join(PROJECT_ROOT, 'templates', 'artifact');
|
|
52
|
+
const PLANS_DIR = join(PROJECT_ROOT, 'artifacts');
|
|
53
53
|
const MAX_REQUEST_BODY_BYTES = 5 * 1024 * 1024;
|
|
54
54
|
|
|
55
55
|
// ─── Flag parsing ────────────────────────────────────────────────────────────
|
|
@@ -165,14 +165,14 @@ function readPlanFile(slug, filename) {
|
|
|
165
165
|
|
|
166
166
|
/**
|
|
167
167
|
* The current canvas schema. v2 introduces elements, connections, viewport,
|
|
168
|
-
* and threaded comments. The single source of truth for new
|
|
168
|
+
* and threaded comments. The single source of truth for new artifacts.
|
|
169
169
|
*/
|
|
170
170
|
const CANVAS_SCHEMA_VERSION = 2;
|
|
171
171
|
|
|
172
172
|
function emptyCanvas(title) {
|
|
173
173
|
return {
|
|
174
174
|
schemaVersion: CANVAS_SCHEMA_VERSION,
|
|
175
|
-
title: title || 'Untitled
|
|
175
|
+
title: title || 'Untitled artifact',
|
|
176
176
|
elements: [],
|
|
177
177
|
connections: [],
|
|
178
178
|
comments: [],
|
|
@@ -181,7 +181,7 @@ function emptyCanvas(title) {
|
|
|
181
181
|
}
|
|
182
182
|
|
|
183
183
|
function readCanvasFile(planDir) {
|
|
184
|
-
const path = join(planDir, '
|
|
184
|
+
const path = join(planDir, 'artifact.json');
|
|
185
185
|
if (!existsSync(path)) return null;
|
|
186
186
|
try {
|
|
187
187
|
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
@@ -191,13 +191,13 @@ function readCanvasFile(planDir) {
|
|
|
191
191
|
}
|
|
192
192
|
|
|
193
193
|
function writeCanvasFile(planDir, canvas) {
|
|
194
|
-
atomicWriteJson(join(planDir, '
|
|
194
|
+
atomicWriteJson(join(planDir, 'artifact.json'), canvas);
|
|
195
195
|
}
|
|
196
196
|
|
|
197
197
|
/**
|
|
198
|
-
* Migration shim: if a v1
|
|
198
|
+
* Migration shim: if a v1 artifact (artifact.mdx) exists but no artifact.json, build
|
|
199
199
|
* a v2 canvas with the mdx content as a single "text" element on the
|
|
200
|
-
* canvas. Idempotent — won't overwrite an existing
|
|
200
|
+
* canvas. Idempotent — won't overwrite an existing artifact.json.
|
|
201
201
|
*
|
|
202
202
|
* Returns the resulting canvas.
|
|
203
203
|
*/
|
|
@@ -211,12 +211,12 @@ function loadOrMigrateCanvas(planDir, fallbackTitle) {
|
|
|
211
211
|
if (!existing.viewport || typeof existing.viewport !== 'object') {
|
|
212
212
|
existing.viewport = { x: 0, y: 0, zoom: 1 };
|
|
213
213
|
}
|
|
214
|
-
if (typeof existing.title !== 'string') existing.title = fallbackTitle || 'Untitled
|
|
214
|
+
if (typeof existing.title !== 'string') existing.title = fallbackTitle || 'Untitled artifact';
|
|
215
215
|
if (existing.schemaVersion !== CANVAS_SCHEMA_VERSION) existing.schemaVersion = CANVAS_SCHEMA_VERSION;
|
|
216
216
|
return existing;
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
-
const mdxPath = join(planDir, '
|
|
219
|
+
const mdxPath = join(planDir, 'artifact.mdx');
|
|
220
220
|
if (existsSync(mdxPath)) {
|
|
221
221
|
const mdx = readFileSync(mdxPath, 'utf-8');
|
|
222
222
|
// If the mdx is empty or just whitespace, return a blank canvas.
|
|
@@ -242,7 +242,7 @@ function loadOrMigrateCanvas(planDir, fallbackTitle) {
|
|
|
242
242
|
return canvas;
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
-
// No
|
|
245
|
+
// No artifact.json and no artifact.mdx — start fresh.
|
|
246
246
|
const c = emptyCanvas(fallbackTitle);
|
|
247
247
|
writeCanvasFile(planDir, c);
|
|
248
248
|
return c;
|
|
@@ -252,7 +252,7 @@ function loadOrMigrateCanvas(planDir, fallbackTitle) {
|
|
|
252
252
|
* Convert a v2 canvas state to a derived markdown document.
|
|
253
253
|
* Used by:
|
|
254
254
|
* - the /api/<slug>/markdown-export endpoint
|
|
255
|
-
* - the `bizar
|
|
255
|
+
* - the `bizar artifact export` subcommand (for backwards compat)
|
|
256
256
|
*
|
|
257
257
|
* Strategy: emit each element as a section, in a stable order (top-to-bottom
|
|
258
258
|
* by y, then left-to-right by x). Connections are not represented in the
|
|
@@ -260,7 +260,7 @@ function loadOrMigrateCanvas(planDir, fallbackTitle) {
|
|
|
260
260
|
*/
|
|
261
261
|
function canvasToMarkdown(canvas) {
|
|
262
262
|
const lines = [];
|
|
263
|
-
const title = (canvas && canvas.title) || 'Untitled
|
|
263
|
+
const title = (canvas && canvas.title) || 'Untitled artifact';
|
|
264
264
|
lines.push('# ' + title);
|
|
265
265
|
lines.push('');
|
|
266
266
|
lines.push('*Exported from canvas on ' + new Date().toISOString() + '*');
|
|
@@ -373,8 +373,8 @@ function makeReplyId() {
|
|
|
373
373
|
return 'r_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
|
|
374
374
|
}
|
|
375
375
|
|
|
376
|
-
/** Best-effort read of
|
|
377
|
-
* invalid JSON. Used by the canvas endpoints to surface the
|
|
376
|
+
/** Best-effort read of artifacts/<slug>/meta.json. Returns null on missing or
|
|
377
|
+
* invalid JSON. Used by the canvas endpoints to surface the artifact title. */
|
|
378
378
|
function readPlanMeta(planDir) {
|
|
379
379
|
const metaPath = join(planDir, 'meta.json');
|
|
380
380
|
if (!existsSync(metaPath)) return null;
|
|
@@ -388,8 +388,8 @@ function readPlanMeta(planDir) {
|
|
|
388
388
|
// ─── new <slug> flow ─────────────────────────────────────────────────────────
|
|
389
389
|
|
|
390
390
|
/**
|
|
391
|
-
* Resolve the content for a new
|
|
392
|
-
* - If template is the string "blank" or null → use
|
|
391
|
+
* Resolve the content for a new artifact from the template system.
|
|
392
|
+
* - If template is the string "blank" or null → use artifact.mdx.template (the v1 default)
|
|
393
393
|
* - If template is a built-in name (feature-design, etc.) → use that template
|
|
394
394
|
* - If template is an absolute path to a .mdx file → use that file
|
|
395
395
|
* - Otherwise → throw with a helpful error
|
|
@@ -398,7 +398,7 @@ function readPlanMeta(planDir) {
|
|
|
398
398
|
*/
|
|
399
399
|
async function resolveTemplateContent(template, vars) {
|
|
400
400
|
if (template == null || template === '' || template === 'blank') {
|
|
401
|
-
const tpl = await readTemplate('
|
|
401
|
+
const tpl = await readTemplate('artifact.mdx.template');
|
|
402
402
|
return { content: replaceTemplate(tpl, vars), templateName: 'blank', source: 'built-in' };
|
|
403
403
|
}
|
|
404
404
|
|
|
@@ -429,14 +429,14 @@ async function resolveTemplateContent(template, vars) {
|
|
|
429
429
|
|
|
430
430
|
if (tpl.content == null) {
|
|
431
431
|
// "blank" via getTemplate — same as the default branch
|
|
432
|
-
const blankTpl = await readTemplate('
|
|
432
|
+
const blankTpl = await readTemplate('artifact.mdx.template');
|
|
433
433
|
return { content: replaceTemplate(blankTpl, vars), templateName: 'blank', source: 'built-in' };
|
|
434
434
|
}
|
|
435
435
|
|
|
436
436
|
return { content: substitute(tpl.content, vars), templateName: tpl.name, source: tpl.source };
|
|
437
437
|
}
|
|
438
438
|
|
|
439
|
-
async function
|
|
439
|
+
async function createArtifact(slug, { template = null } = {}) {
|
|
440
440
|
const planDir = join(PLANS_DIR, slug);
|
|
441
441
|
|
|
442
442
|
// Step 1: validate
|
|
@@ -444,8 +444,8 @@ async function createPlan(slug, { template = null } = {}) {
|
|
|
444
444
|
|
|
445
445
|
// Step 2: check existence
|
|
446
446
|
if (existsSync(planDir)) {
|
|
447
|
-
console.error(` ✗
|
|
448
|
-
console.error(` Use "bizar
|
|
447
|
+
console.error(` ✗ Artifact "${slug}" already exists at ${planDir}`);
|
|
448
|
+
console.error(` Use "bizar artifact open ${slug}" to open it.`);
|
|
449
449
|
return false;
|
|
450
450
|
}
|
|
451
451
|
|
|
@@ -464,7 +464,7 @@ async function createPlan(slug, { template = null } = {}) {
|
|
|
464
464
|
lastEdited: now,
|
|
465
465
|
};
|
|
466
466
|
|
|
467
|
-
// Step 5:
|
|
467
|
+
// Step 5: artifact.mdx (from template if --template was given)
|
|
468
468
|
let mdxContent;
|
|
469
469
|
let templateName = 'blank';
|
|
470
470
|
try {
|
|
@@ -473,11 +473,11 @@ async function createPlan(slug, { template = null } = {}) {
|
|
|
473
473
|
templateName = resolved.templateName;
|
|
474
474
|
} catch (err) {
|
|
475
475
|
console.error(` ✗ ${err.message}`);
|
|
476
|
-
// Clean up the empty
|
|
476
|
+
// Clean up the empty artifact directory we just created
|
|
477
477
|
rmSync(planDir, { recursive: true, force: true });
|
|
478
478
|
return false;
|
|
479
479
|
}
|
|
480
|
-
writePlanFile(slug, '
|
|
480
|
+
writePlanFile(slug, 'artifact.mdx', mdxContent);
|
|
481
481
|
|
|
482
482
|
// Step 6: meta.json
|
|
483
483
|
const metaTemplate = await readTemplate('meta.json.template');
|
|
@@ -487,10 +487,10 @@ async function createPlan(slug, { template = null } = {}) {
|
|
|
487
487
|
// Step 7: comments.json (stored as empty array for comments)
|
|
488
488
|
writePlanFile(slug, 'comments.json', JSON.stringify([], null, 2));
|
|
489
489
|
|
|
490
|
-
// Step 8:
|
|
490
|
+
// Step 8: artifact.html (regenerate from template)
|
|
491
491
|
await regenerateHtml(slug);
|
|
492
492
|
|
|
493
|
-
console.log(` ✓ Created
|
|
493
|
+
console.log(` ✓ Created artifact "${title}" (slug: ${slug})`);
|
|
494
494
|
if (templateName !== 'blank') {
|
|
495
495
|
console.log(` Template: ${templateName}`);
|
|
496
496
|
}
|
|
@@ -506,23 +506,23 @@ export async function regenerateHtml(slug) {
|
|
|
506
506
|
if (!existsSync(planDir)) return;
|
|
507
507
|
|
|
508
508
|
// Prefer the v2 canvas template. Fall back to the v1 template (kept for
|
|
509
|
-
// backwards compat with
|
|
509
|
+
// backwards compat with artifacts that don't have artifact.json).
|
|
510
510
|
let htmlTemplate;
|
|
511
|
-
let templateName = '
|
|
511
|
+
let templateName = 'artifact.html.template';
|
|
512
512
|
try {
|
|
513
|
-
htmlTemplate = await readTemplate('
|
|
514
|
-
templateName = '
|
|
513
|
+
htmlTemplate = await readTemplate('artifact.canvas.template');
|
|
514
|
+
templateName = 'artifact.canvas.template';
|
|
515
515
|
} catch {
|
|
516
516
|
try {
|
|
517
|
-
htmlTemplate = await readTemplate('
|
|
518
|
-
templateName = '
|
|
517
|
+
htmlTemplate = await readTemplate('artifact.html.template');
|
|
518
|
+
templateName = 'artifact.html.template';
|
|
519
519
|
} catch {
|
|
520
520
|
// No HTML template yet — Tyr is working on it in parallel
|
|
521
521
|
return;
|
|
522
522
|
}
|
|
523
523
|
}
|
|
524
524
|
|
|
525
|
-
const planMdx = readFileSync(join(planDir, '
|
|
525
|
+
const planMdx = readFileSync(join(planDir, 'artifact.mdx'), 'utf-8');
|
|
526
526
|
const metaJson = readFileSync(join(planDir, 'meta.json'), 'utf-8');
|
|
527
527
|
const commentsJson = readFileSync(join(planDir, 'comments.json'), 'utf-8');
|
|
528
528
|
|
|
@@ -559,18 +559,18 @@ export async function regenerateHtml(slug) {
|
|
|
559
559
|
};
|
|
560
560
|
|
|
561
561
|
const htmlContent = replaceTemplate(htmlTemplate, vars);
|
|
562
|
-
atomicWriteText(join(planDir, '
|
|
562
|
+
atomicWriteText(join(planDir, 'artifact.html'), htmlContent);
|
|
563
563
|
}
|
|
564
564
|
|
|
565
565
|
// ─── open <slug> flow ────────────────────────────────────────────────────────
|
|
566
566
|
|
|
567
|
-
async function
|
|
567
|
+
async function openArtifact(slug) {
|
|
568
568
|
if (!validateSlug(slug)) return false;
|
|
569
569
|
|
|
570
570
|
const planDir = join(PLANS_DIR, slug);
|
|
571
571
|
if (!existsSync(planDir)) {
|
|
572
|
-
console.error(` ✗
|
|
573
|
-
console.error(` Use "bizar
|
|
572
|
+
console.error(` ✗ Artifact "${slug}" not found at ${planDir}`);
|
|
573
|
+
console.error(` Use "bizar artifact new ${slug}" to create it.`);
|
|
574
574
|
return false;
|
|
575
575
|
}
|
|
576
576
|
|
|
@@ -581,7 +581,7 @@ async function openPlan(slug) {
|
|
|
581
581
|
const { port, close } = await startServer(slug, planDir);
|
|
582
582
|
|
|
583
583
|
const url = `http://localhost:${port}/${slug}/`;
|
|
584
|
-
console.log(` ✓ Opening
|
|
584
|
+
console.log(` ✓ Opening artifact "${slug}" at ${url}`);
|
|
585
585
|
openBrowser(url);
|
|
586
586
|
|
|
587
587
|
// Keep process alive until Ctrl-C
|
|
@@ -594,7 +594,7 @@ async function openPlan(slug) {
|
|
|
594
594
|
|
|
595
595
|
async function listPlans() {
|
|
596
596
|
if (!existsSync(PLANS_DIR)) {
|
|
597
|
-
console.log(' No
|
|
597
|
+
console.log(' No artifacts found. Run `bizar artifact new <slug>` to create one.');
|
|
598
598
|
return true;
|
|
599
599
|
}
|
|
600
600
|
|
|
@@ -603,34 +603,34 @@ async function listPlans() {
|
|
|
603
603
|
});
|
|
604
604
|
|
|
605
605
|
if (dirs.length === 0) {
|
|
606
|
-
console.log(' No
|
|
606
|
+
console.log(' No artifacts found. Run `bizar artifact new <slug>` to create one.');
|
|
607
607
|
return true;
|
|
608
608
|
}
|
|
609
609
|
|
|
610
610
|
// Read meta for each
|
|
611
|
-
const
|
|
611
|
+
const artifacts = [];
|
|
612
612
|
for (const dir of dirs) {
|
|
613
613
|
try {
|
|
614
614
|
const meta = JSON.parse(readFileSync(join(PLANS_DIR, dir, 'meta.json'), 'utf-8'));
|
|
615
|
-
|
|
615
|
+
artifacts.push(meta);
|
|
616
616
|
} catch {
|
|
617
617
|
// Skip invalid meta.json
|
|
618
618
|
}
|
|
619
619
|
}
|
|
620
620
|
|
|
621
621
|
// Sort by lastEdited newest first
|
|
622
|
-
|
|
622
|
+
artifacts.sort((a, b) => new Date(b.lastEdited) - new Date(a.lastEdited));
|
|
623
623
|
|
|
624
624
|
// Print table
|
|
625
625
|
console.log(' Slug Title Status Last Edited Author');
|
|
626
626
|
console.log(' ────────────── ────────────────────── ──────── ──────────────────────── ───────────────');
|
|
627
627
|
|
|
628
|
-
for (const
|
|
629
|
-
const slug = (
|
|
630
|
-
const title = (
|
|
631
|
-
const status = (
|
|
632
|
-
const lastEdited = (
|
|
633
|
-
const author = (
|
|
628
|
+
for (const artifact of artifacts) {
|
|
629
|
+
const slug = (artifact.slug || '').padEnd(15);
|
|
630
|
+
const title = (artifact.title || '').substring(0, 23).padEnd(23);
|
|
631
|
+
const status = (artifact.status || 'draft').padEnd(9);
|
|
632
|
+
const lastEdited = (artifact.lastEdited || '').substring(0, 27).padEnd(27);
|
|
633
|
+
const author = (artifact.author || 'unknown').substring(0, 20);
|
|
634
634
|
console.log(` ${slug} ${title} ${status} ${lastEdited} ${author}`);
|
|
635
635
|
}
|
|
636
636
|
console.log();
|
|
@@ -640,12 +640,12 @@ async function listPlans() {
|
|
|
640
640
|
|
|
641
641
|
// ─── delete <slug> flow ─────────────────────────────────────────────────────
|
|
642
642
|
|
|
643
|
-
async function
|
|
643
|
+
async function deleteArtifact(slug) {
|
|
644
644
|
if (!validateSlug(slug)) return false;
|
|
645
645
|
|
|
646
646
|
const planDir = join(PLANS_DIR, slug);
|
|
647
647
|
if (!existsSync(planDir)) {
|
|
648
|
-
console.error(` ✗
|
|
648
|
+
console.error(` ✗ Artifact "${slug}" not found.`);
|
|
649
649
|
return false;
|
|
650
650
|
}
|
|
651
651
|
|
|
@@ -656,11 +656,11 @@ async function deletePlan(slug) {
|
|
|
656
656
|
} catch { /* use slug as fallback */ }
|
|
657
657
|
|
|
658
658
|
// Read confirm from stdin
|
|
659
|
-
const answer = await question(` Delete
|
|
659
|
+
const answer = await question(` Delete artifact "${title}" (slug: ${slug})? This is irreversible. [y/N] `);
|
|
660
660
|
|
|
661
661
|
if (answer.trim().toLowerCase() === 'y') {
|
|
662
662
|
rmSync(planDir, { recursive: true });
|
|
663
|
-
console.log(` ✓ Deleted
|
|
663
|
+
console.log(` ✓ Deleted artifact "${slug}".`);
|
|
664
664
|
return true;
|
|
665
665
|
} else {
|
|
666
666
|
console.log(' Cancelled.');
|
|
@@ -670,18 +670,18 @@ async function deletePlan(slug) {
|
|
|
670
670
|
|
|
671
671
|
// ─── export <slug> flow ──────────────────────────────────────────────────────
|
|
672
672
|
|
|
673
|
-
async function
|
|
673
|
+
async function exportArtifact(slug) {
|
|
674
674
|
if (!validateSlug(slug)) return false;
|
|
675
675
|
|
|
676
676
|
const planDir = join(PLANS_DIR, slug);
|
|
677
677
|
if (!existsSync(planDir)) {
|
|
678
|
-
console.error(` ✗
|
|
678
|
+
console.error(` ✗ Artifact "${slug}" not found.`);
|
|
679
679
|
return false;
|
|
680
680
|
}
|
|
681
681
|
|
|
682
|
-
// Prefer the v2 canvas-derived markdown when
|
|
683
|
-
// to the raw mdx for v1
|
|
684
|
-
const canvasFile = join(planDir, '
|
|
682
|
+
// Prefer the v2 canvas-derived markdown when artifact.json exists. Fall back
|
|
683
|
+
// to the raw mdx for v1 artifacts.
|
|
684
|
+
const canvasFile = join(planDir, 'artifact.json');
|
|
685
685
|
if (existsSync(canvasFile)) {
|
|
686
686
|
try {
|
|
687
687
|
const canvas = JSON.parse(readFileSync(canvasFile, 'utf-8'));
|
|
@@ -692,9 +692,9 @@ async function exportPlan(slug) {
|
|
|
692
692
|
}
|
|
693
693
|
}
|
|
694
694
|
|
|
695
|
-
const planFile = join(planDir, '
|
|
695
|
+
const planFile = join(planDir, 'artifact.mdx');
|
|
696
696
|
if (!existsSync(planFile)) {
|
|
697
|
-
console.error(` ✗
|
|
697
|
+
console.error(` ✗ Artifact "${slug}" not found.`);
|
|
698
698
|
return false;
|
|
699
699
|
}
|
|
700
700
|
|
|
@@ -707,22 +707,22 @@ async function exportPlan(slug) {
|
|
|
707
707
|
|
|
708
708
|
function showHelp() {
|
|
709
709
|
console.log(`
|
|
710
|
-
bizar
|
|
710
|
+
bizar artifact <subcommand> [options]
|
|
711
711
|
|
|
712
712
|
Subcommands:
|
|
713
|
-
new <slug> [--template <name>] Create a new
|
|
714
|
-
open <slug> Open an existing
|
|
715
|
-
list List all
|
|
716
|
-
delete <slug> Delete a
|
|
717
|
-
export <slug> Export
|
|
718
|
-
templates List available
|
|
719
|
-
template save <name> <
|
|
713
|
+
new <slug> [--template <name>] Create a new artifact (default: blank template)
|
|
714
|
+
open <slug> Open an existing artifact in the browser
|
|
715
|
+
list List all artifacts
|
|
716
|
+
delete <slug> Delete a artifact (with confirmation)
|
|
717
|
+
export <slug> Export artifact.mdx to stdout
|
|
718
|
+
templates List available artifact templates
|
|
719
|
+
template save <name> <artifact-slug> Save a artifact as a library template
|
|
720
720
|
template delete <name> Delete a user-added library template
|
|
721
721
|
help Show this help
|
|
722
722
|
|
|
723
|
-
Plans are stored in
|
|
724
|
-
-
|
|
725
|
-
-
|
|
723
|
+
Plans are stored in artifacts/<slug>/ as:
|
|
724
|
+
- artifact.mdx source content (in git)
|
|
725
|
+
- artifact.html viewer/editor (gitignored)
|
|
726
726
|
- comments.json comments array (gitignored)
|
|
727
727
|
- meta.json metadata (in git)
|
|
728
728
|
|
|
@@ -733,13 +733,13 @@ function showHelp() {
|
|
|
733
733
|
decision-record Architecture Decision Record (ADR)
|
|
734
734
|
|
|
735
735
|
Examples:
|
|
736
|
-
bizar
|
|
737
|
-
bizar
|
|
738
|
-
bizar
|
|
739
|
-
bizar
|
|
740
|
-
bizar
|
|
741
|
-
bizar
|
|
742
|
-
bizar
|
|
736
|
+
bizar artifact new my-feature
|
|
737
|
+
bizar artifact new auth-v2 --template feature-design
|
|
738
|
+
bizar artifact new oops --template bug-investigation
|
|
739
|
+
bizar artifact templates
|
|
740
|
+
bizar artifact open my-feature
|
|
741
|
+
bizar artifact list
|
|
742
|
+
bizar artifact export my-feature > my-feature.mdx
|
|
743
743
|
`);
|
|
744
744
|
}
|
|
745
745
|
|
|
@@ -982,7 +982,7 @@ function renderCommentCountHtml(count) {
|
|
|
982
982
|
/** Parse an HTTP request body. Supports:
|
|
983
983
|
* - application/x-www-form-urlencoded (htmx default for <form>)
|
|
984
984
|
* - application/json (legacy / direct API)
|
|
985
|
-
* - text/plain (raw MDX for
|
|
985
|
+
* - text/plain (raw MDX for artifact save)
|
|
986
986
|
* - anything else: returns raw string
|
|
987
987
|
*/
|
|
988
988
|
function readRequestBody(req) {
|
|
@@ -1113,11 +1113,11 @@ async function handleRequest(req, res, slug, planDir, serverPort) {
|
|
|
1113
1113
|
|
|
1114
1114
|
try {
|
|
1115
1115
|
if (pathname === `/${slug}/` || pathname === `/${slug}` || pathname === '/') {
|
|
1116
|
-
// Serve
|
|
1117
|
-
const htmlPath = join(planDir, '
|
|
1116
|
+
// Serve artifact.html
|
|
1117
|
+
const htmlPath = join(planDir, 'artifact.html');
|
|
1118
1118
|
if (!existsSync(htmlPath)) {
|
|
1119
1119
|
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
1120
|
-
res.end('
|
|
1120
|
+
res.end('artifact.html not found. Run `bizar artifact new ${slug}` first.');
|
|
1121
1121
|
return;
|
|
1122
1122
|
}
|
|
1123
1123
|
const html = readFileSync(htmlPath, 'utf-8');
|
|
@@ -1126,12 +1126,12 @@ async function handleRequest(req, res, slug, planDir, serverPort) {
|
|
|
1126
1126
|
return;
|
|
1127
1127
|
}
|
|
1128
1128
|
|
|
1129
|
-
// ── Self-hosted htmx — served from templates/
|
|
1129
|
+
// ── Self-hosted htmx — served from templates/artifact/htmx.min.js ────────────
|
|
1130
1130
|
if (pathname === '/htmx.min.js' && req.method === 'GET') {
|
|
1131
1131
|
const htmxPath = join(TEMPLATES_DIR, 'htmx.min.js');
|
|
1132
1132
|
if (!existsSync(htmxPath)) {
|
|
1133
1133
|
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
1134
|
-
res.end('htmx.min.js not found in templates/
|
|
1134
|
+
res.end('htmx.min.js not found in templates/artifact/');
|
|
1135
1135
|
return;
|
|
1136
1136
|
}
|
|
1137
1137
|
const buf = readFileSync(htmxPath);
|
|
@@ -1144,14 +1144,14 @@ async function handleRequest(req, res, slug, planDir, serverPort) {
|
|
|
1144
1144
|
}
|
|
1145
1145
|
|
|
1146
1146
|
// ── New RESTful, slug-scoped routes (preferred for htmx) ─────────────────
|
|
1147
|
-
// GET /api/<slug>/
|
|
1148
|
-
// PUT /api/<slug>/
|
|
1147
|
+
// GET /api/<slug>/artifact → MDX text/plain
|
|
1148
|
+
// PUT /api/<slug>/artifact → save MDX, returns empty 200
|
|
1149
1149
|
// GET /api/<slug>/comments → JSON (default) or HTML (?format=html or ?sectionId=)
|
|
1150
1150
|
// POST /api/<slug>/comments → add a comment, returns the new <li> HTML
|
|
1151
1151
|
// PUT /api/<slug>/comments → replace the whole comments array (JSON)
|
|
1152
1152
|
// GET /api/<slug>/count → comment count for a section (?sectionId=)
|
|
1153
1153
|
// We validate the slug in the URL against the bound slug so the server can't
|
|
1154
|
-
// be tricked into serving data for a different
|
|
1154
|
+
// be tricked into serving data for a different artifact.
|
|
1155
1155
|
|
|
1156
1156
|
if (pathname.startsWith('/api/') && pathname.split('/').length >= 4) {
|
|
1157
1157
|
const parts = pathname.split('/').filter(Boolean); // ['api', '<urlSlug>', '<resource>']
|
|
@@ -1160,7 +1160,7 @@ async function handleRequest(req, res, slug, planDir, serverPort) {
|
|
|
1160
1160
|
|
|
1161
1161
|
if (urlSlug !== slug) {
|
|
1162
1162
|
res.writeHead(403, { 'Content-Type': 'text/plain' });
|
|
1163
|
-
res.end(`Forbidden: this server is bound to
|
|
1163
|
+
res.end(`Forbidden: this server is bound to artifact "${slug}"`);
|
|
1164
1164
|
return;
|
|
1165
1165
|
}
|
|
1166
1166
|
|
|
@@ -1238,7 +1238,7 @@ async function handleRequest(req, res, slug, planDir, serverPort) {
|
|
|
1238
1238
|
if (!next.viewport || typeof next.viewport !== 'object') {
|
|
1239
1239
|
next.viewport = { x: 0, y: 0, zoom: 1 };
|
|
1240
1240
|
}
|
|
1241
|
-
if (typeof next.title !== 'string') next.title = 'Untitled
|
|
1241
|
+
if (typeof next.title !== 'string') next.title = 'Untitled artifact';
|
|
1242
1242
|
next.schemaVersion = CANVAS_SCHEMA_VERSION;
|
|
1243
1243
|
writeCanvasFile(planDir, next);
|
|
1244
1244
|
bumpLastEdited(planDir);
|
|
@@ -1503,7 +1503,7 @@ async function handleRequest(req, res, slug, planDir, serverPort) {
|
|
|
1503
1503
|
}
|
|
1504
1504
|
|
|
1505
1505
|
// POST /api/<slug>/comments
|
|
1506
|
-
// v2 path (canvas): JSON body with text + x/y/elementId, writes to
|
|
1506
|
+
// v2 path (canvas): JSON body with text + x/y/elementId, writes to artifact.json
|
|
1507
1507
|
// v1 path (legacy): body has sectionId (form OR JSON), writes to comments.json
|
|
1508
1508
|
// v1 returns HTML <li>; v2 returns JSON.
|
|
1509
1509
|
// We dispatch on body shape: if sectionId is present, it's v1.
|
|
@@ -1654,23 +1654,23 @@ async function handleRequest(req, res, slug, planDir, serverPort) {
|
|
|
1654
1654
|
}
|
|
1655
1655
|
|
|
1656
1656
|
// ── v1 routes (legacy, kept for backwards compat) ───────────────
|
|
1657
|
-
// GET /api/<slug>/
|
|
1658
|
-
// PUT /api/<slug>/
|
|
1657
|
+
// GET /api/<slug>/artifact → MDX text/plain
|
|
1658
|
+
// PUT /api/<slug>/artifact → save MDX
|
|
1659
1659
|
// GET /api/<slug>/comments → JSON (default) or HTML (?format=html)
|
|
1660
1660
|
// POST /api/<slug>/comments → add a v1 comment (form data with sectionId)
|
|
1661
1661
|
// PUT /api/<slug>/comments → replace the whole v1 array (JSON)
|
|
1662
1662
|
// GET /api/<slug>/count → comment count for a section
|
|
1663
1663
|
|
|
1664
|
-
// GET /api/<slug>/
|
|
1665
|
-
if (resource === '
|
|
1666
|
-
const mdx = readFileSync(join(planDir, '
|
|
1664
|
+
// GET /api/<slug>/artifact
|
|
1665
|
+
if (resource === 'artifact' && req.method === 'GET') {
|
|
1666
|
+
const mdx = readFileSync(join(planDir, 'artifact.mdx'), 'utf-8');
|
|
1667
1667
|
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
1668
1668
|
res.end(mdx);
|
|
1669
1669
|
return;
|
|
1670
1670
|
}
|
|
1671
1671
|
|
|
1672
|
-
// PUT /api/<slug>/
|
|
1673
|
-
if (resource === '
|
|
1672
|
+
// PUT /api/<slug>/artifact
|
|
1673
|
+
if (resource === 'artifact' && req.method === 'PUT') {
|
|
1674
1674
|
const parsed = await readRequestBody(req);
|
|
1675
1675
|
const content = parsed.kind === 'form' ? (parsed.data.content || '') : parsed.data;
|
|
1676
1676
|
if (typeof content !== 'string') {
|
|
@@ -1679,7 +1679,7 @@ async function handleRequest(req, res, slug, planDir, serverPort) {
|
|
|
1679
1679
|
return;
|
|
1680
1680
|
}
|
|
1681
1681
|
try {
|
|
1682
|
-
atomicWriteText(join(planDir, '
|
|
1682
|
+
atomicWriteText(join(planDir, 'artifact.mdx'), content);
|
|
1683
1683
|
bumpLastEdited(planDir);
|
|
1684
1684
|
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
1685
1685
|
res.end('saved');
|
|
@@ -1713,7 +1713,7 @@ async function handleRequest(req, res, slug, planDir, serverPort) {
|
|
|
1713
1713
|
|
|
1714
1714
|
// (v1 POST /api/<slug>/comments is now handled by the merged v2/v1
|
|
1715
1715
|
// handler above, which dispatches on body shape — sectionId → v1
|
|
1716
|
-
// writes to comments.json; otherwise v2 writes to
|
|
1716
|
+
// writes to comments.json; otherwise v2 writes to artifact.json.)
|
|
1717
1717
|
|
|
1718
1718
|
// PUT /api/<slug>/comments → v1: replace the whole array (JSON body)
|
|
1719
1719
|
if (resource === 'comments' && req.method === 'PUT') {
|
|
@@ -1753,19 +1753,19 @@ async function handleRequest(req, res, slug, planDir, serverPort) {
|
|
|
1753
1753
|
// the /api/<slug>/comments routes that didn't match a v2 discriminator.)
|
|
1754
1754
|
}
|
|
1755
1755
|
|
|
1756
|
-
if (pathname === '/api/
|
|
1757
|
-
const mdx = readFileSync(join(planDir, '
|
|
1756
|
+
if (pathname === '/api/artifact' && req.method === 'GET') {
|
|
1757
|
+
const mdx = readFileSync(join(planDir, 'artifact.mdx'), 'utf-8');
|
|
1758
1758
|
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
1759
1759
|
res.end(mdx);
|
|
1760
1760
|
return;
|
|
1761
1761
|
}
|
|
1762
1762
|
|
|
1763
|
-
if (pathname === '/api/
|
|
1763
|
+
if (pathname === '/api/artifact' && req.method === 'PUT') {
|
|
1764
1764
|
let body = '';
|
|
1765
1765
|
req.on('data', (chunk) => { body += chunk; });
|
|
1766
1766
|
req.on('end', () => {
|
|
1767
1767
|
try {
|
|
1768
|
-
atomicWriteText(join(planDir, '
|
|
1768
|
+
atomicWriteText(join(planDir, 'artifact.mdx'), body);
|
|
1769
1769
|
// Update lastEdited in meta.json
|
|
1770
1770
|
const meta = JSON.parse(readFileSync(join(planDir, 'meta.json'), 'utf-8'));
|
|
1771
1771
|
meta.lastEdited = new Date().toISOString();
|
|
@@ -1837,13 +1837,13 @@ async function handleRequest(req, res, slug, planDir, serverPort) {
|
|
|
1837
1837
|
|
|
1838
1838
|
if (pathname === '/api/regenerate' && req.method === 'POST') {
|
|
1839
1839
|
try {
|
|
1840
|
-
// Read current
|
|
1841
|
-
const planMdx = readFileSync(join(planDir, '
|
|
1840
|
+
// Read current artifact.mdx and regenerate HTML
|
|
1841
|
+
const planMdx = readFileSync(join(planDir, 'artifact.mdx'), 'utf-8');
|
|
1842
1842
|
const metaJson = readFileSync(join(planDir, 'meta.json'), 'utf-8');
|
|
1843
1843
|
const commentsJson = readFileSync(join(planDir, 'comments.json'), 'utf-8');
|
|
1844
1844
|
const meta = JSON.parse(metaJson);
|
|
1845
1845
|
|
|
1846
|
-
const planHtmlTemplate = readFileSync(join(TEMPLATES_DIR, '
|
|
1846
|
+
const planHtmlTemplate = readFileSync(join(TEMPLATES_DIR, 'artifact.html.template'), 'utf-8');
|
|
1847
1847
|
const planJson = JSON.stringify(planMdx);
|
|
1848
1848
|
|
|
1849
1849
|
const vars = {
|
|
@@ -1862,7 +1862,7 @@ async function handleRequest(req, res, slug, planDir, serverPort) {
|
|
|
1862
1862
|
for (const [key, value] of Object.entries(vars)) {
|
|
1863
1863
|
htmlContent = htmlContent.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), value);
|
|
1864
1864
|
}
|
|
1865
|
-
atomicWriteText(join(planDir, '
|
|
1865
|
+
atomicWriteText(join(planDir, 'artifact.html'), htmlContent);
|
|
1866
1866
|
|
|
1867
1867
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1868
1868
|
res.end(JSON.stringify({ ok: true }));
|
|
@@ -1960,11 +1960,11 @@ function waitForSignal(closeFn) {
|
|
|
1960
1960
|
* delete <slug>
|
|
1961
1961
|
* export <slug>
|
|
1962
1962
|
* templates
|
|
1963
|
-
* template save <name> <
|
|
1963
|
+
* template save <name> <artifact-slug>
|
|
1964
1964
|
* template delete <name>
|
|
1965
1965
|
* help
|
|
1966
1966
|
*/
|
|
1967
|
-
export async function
|
|
1967
|
+
export async function runArtifact(argsOrPositional, legacyFlags) {
|
|
1968
1968
|
// Normalize the input — accept either a flat argv array (current bin.mjs
|
|
1969
1969
|
// style) or an already-parsed { positional, flags } object (test style).
|
|
1970
1970
|
let positional;
|
|
@@ -1981,14 +1981,14 @@ export async function runPlan(argsOrPositional, legacyFlags) {
|
|
|
1981
1981
|
flags = legacyFlags || {};
|
|
1982
1982
|
}
|
|
1983
1983
|
|
|
1984
|
-
// Special-case: "template save <name> <
|
|
1984
|
+
// Special-case: "template save <name> <artifact-slug>" / "template delete <name>"
|
|
1985
1985
|
if (positional[0] === 'template') {
|
|
1986
1986
|
const action = positional[1];
|
|
1987
1987
|
if (action === 'save') {
|
|
1988
1988
|
const name = positional[2];
|
|
1989
1989
|
const planSlug = positional[3];
|
|
1990
1990
|
if (!name || !planSlug) {
|
|
1991
|
-
console.error(' ✗ Usage: bizar
|
|
1991
|
+
console.error(' ✗ Usage: bizar artifact template save <name> <artifact-slug>');
|
|
1992
1992
|
return false;
|
|
1993
1993
|
}
|
|
1994
1994
|
try {
|
|
@@ -2003,7 +2003,7 @@ export async function runPlan(argsOrPositional, legacyFlags) {
|
|
|
2003
2003
|
if (action === 'delete' || action === 'rm') {
|
|
2004
2004
|
const name = positional[2];
|
|
2005
2005
|
if (!name) {
|
|
2006
|
-
console.error(' ✗ Usage: bizar
|
|
2006
|
+
console.error(' ✗ Usage: bizar artifact template delete <name>');
|
|
2007
2007
|
return false;
|
|
2008
2008
|
}
|
|
2009
2009
|
try {
|
|
@@ -2020,7 +2020,7 @@ export async function runPlan(argsOrPositional, legacyFlags) {
|
|
|
2020
2020
|
return true;
|
|
2021
2021
|
}
|
|
2022
2022
|
console.error(` ✗ Unknown template action: "${action}"`);
|
|
2023
|
-
console.error(' Use: save <name> <
|
|
2023
|
+
console.error(' Use: save <name> <artifact-slug>, delete <name>, or list');
|
|
2024
2024
|
return false;
|
|
2025
2025
|
}
|
|
2026
2026
|
|
|
@@ -2029,21 +2029,21 @@ export async function runPlan(argsOrPositional, legacyFlags) {
|
|
|
2029
2029
|
switch (subcommand) {
|
|
2030
2030
|
case 'new': {
|
|
2031
2031
|
if (!slug) {
|
|
2032
|
-
console.error(' ✗ Usage: bizar
|
|
2032
|
+
console.error(' ✗ Usage: bizar artifact new <slug> [--template <name>]');
|
|
2033
2033
|
return false;
|
|
2034
2034
|
}
|
|
2035
|
-
const created = await
|
|
2035
|
+
const created = await createArtifact(slug, { template: flags.template || null });
|
|
2036
2036
|
if (!created) return false;
|
|
2037
2037
|
// Start server and open browser
|
|
2038
|
-
return await
|
|
2038
|
+
return await openArtifact(slug);
|
|
2039
2039
|
}
|
|
2040
2040
|
|
|
2041
2041
|
case 'open': {
|
|
2042
2042
|
if (!slug) {
|
|
2043
|
-
console.error(' ✗ Usage: bizar
|
|
2043
|
+
console.error(' ✗ Usage: bizar artifact open <slug>');
|
|
2044
2044
|
return false;
|
|
2045
2045
|
}
|
|
2046
|
-
return await
|
|
2046
|
+
return await openArtifact(slug);
|
|
2047
2047
|
}
|
|
2048
2048
|
|
|
2049
2049
|
case 'list': {
|
|
@@ -2052,18 +2052,18 @@ export async function runPlan(argsOrPositional, legacyFlags) {
|
|
|
2052
2052
|
|
|
2053
2053
|
case 'delete': {
|
|
2054
2054
|
if (!slug) {
|
|
2055
|
-
console.error(' ✗ Usage: bizar
|
|
2055
|
+
console.error(' ✗ Usage: bizar artifact delete <slug>');
|
|
2056
2056
|
return false;
|
|
2057
2057
|
}
|
|
2058
|
-
return await
|
|
2058
|
+
return await deleteArtifact(slug);
|
|
2059
2059
|
}
|
|
2060
2060
|
|
|
2061
2061
|
case 'export': {
|
|
2062
2062
|
if (!slug) {
|
|
2063
|
-
console.error(' ✗ Usage: bizar
|
|
2063
|
+
console.error(' ✗ Usage: bizar artifact export <slug>');
|
|
2064
2064
|
return false;
|
|
2065
2065
|
}
|
|
2066
|
-
return await
|
|
2066
|
+
return await exportArtifact(slug);
|
|
2067
2067
|
}
|
|
2068
2068
|
|
|
2069
2069
|
case 'templates': {
|
|
@@ -2079,14 +2079,14 @@ export async function runPlan(argsOrPositional, legacyFlags) {
|
|
|
2079
2079
|
|
|
2080
2080
|
default: {
|
|
2081
2081
|
console.error(` ✗ Unknown subcommand: "${subcommand}"`);
|
|
2082
|
-
console.error(` Run "bizar
|
|
2082
|
+
console.error(` Run "bizar artifact help" for usage.`);
|
|
2083
2083
|
return false;
|
|
2084
2084
|
}
|
|
2085
2085
|
}
|
|
2086
2086
|
}
|
|
2087
2087
|
|
|
2088
2088
|
// Default export for CLI entrypoint
|
|
2089
|
-
export default
|
|
2089
|
+
export default runArtifact;
|
|
2090
2090
|
|
|
2091
2091
|
// Named exports for the v2 canvas helpers (used by tests and the AI tool).
|
|
2092
2092
|
// These are the public surface of the canvas subsystem.
|