@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.
@@ -1,15 +1,15 @@
1
1
  /**
2
- * bizar plan <subcommand>
2
+ * bizar artifact <subcommand>
3
3
  *
4
4
  * Subcommands:
5
- * new <slug> Create a new plan
5
+ * new <slug> Create a new artifact
6
6
  * Flags: --template <name> | --template <path>
7
- * open <slug> Open an existing plan in the browser
8
- * list List all plans
9
- * delete <slug> Delete a plan (with confirmation)
10
- * export <slug> Export plan.mdx to stdout
11
- * templates List available plan templates
12
- * template save <name> <plan-slug> Save a plan as a library template
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 './plan-templates.mjs';
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', 'plan');
52
- const PLANS_DIR = join(PROJECT_ROOT, 'plans');
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 plans.
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 plan',
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, 'plan.json');
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, 'plan.json'), canvas);
194
+ atomicWriteJson(join(planDir, 'artifact.json'), canvas);
195
195
  }
196
196
 
197
197
  /**
198
- * Migration shim: if a v1 plan (plan.mdx) exists but no plan.json, build
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 plan.json.
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 plan';
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, 'plan.mdx');
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 plan.json and no plan.mdx — start fresh.
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 plan export` subcommand (for backwards compat)
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 plan';
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 plans/<slug>/meta.json. Returns null on missing or
377
- * invalid JSON. Used by the canvas endpoints to surface the plan title. */
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 plan from the template system.
392
- * - If template is the string "blank" or null → use plan.mdx.template (the v1 default)
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('plan.mdx.template');
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('plan.mdx.template');
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 createPlan(slug, { template = null } = {}) {
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(` ✗ Plan "${slug}" already exists at ${planDir}`);
448
- console.error(` Use "bizar plan open ${slug}" to open it.`);
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: plan.mdx (from template if --template was given)
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 plan directory we just created
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, 'plan.mdx', mdxContent);
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: plan.html (regenerate from template)
490
+ // Step 8: artifact.html (regenerate from template)
491
491
  await regenerateHtml(slug);
492
492
 
493
- console.log(` ✓ Created plan "${title}" (slug: ${slug})`);
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 plans that don't have plan.json).
509
+ // backwards compat with artifacts that don't have artifact.json).
510
510
  let htmlTemplate;
511
- let templateName = 'plan.html.template';
511
+ let templateName = 'artifact.html.template';
512
512
  try {
513
- htmlTemplate = await readTemplate('plan.canvas.template');
514
- templateName = 'plan.canvas.template';
513
+ htmlTemplate = await readTemplate('artifact.canvas.template');
514
+ templateName = 'artifact.canvas.template';
515
515
  } catch {
516
516
  try {
517
- htmlTemplate = await readTemplate('plan.html.template');
518
- templateName = 'plan.html.template';
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, 'plan.mdx'), 'utf-8');
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, 'plan.html'), htmlContent);
562
+ atomicWriteText(join(planDir, 'artifact.html'), htmlContent);
563
563
  }
564
564
 
565
565
  // ─── open <slug> flow ────────────────────────────────────────────────────────
566
566
 
567
- async function openPlan(slug) {
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(` ✗ Plan "${slug}" not found at ${planDir}`);
573
- console.error(` Use "bizar plan new ${slug}" to create it.`);
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 plan "${slug}" at ${url}`);
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 plans found. Run `bizar plan new <slug>` to create one.');
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 plans found. Run `bizar plan new <slug>` to create one.');
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 plans = [];
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
- plans.push(meta);
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
- plans.sort((a, b) => new Date(b.lastEdited) - new Date(a.lastEdited));
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 plan of plans) {
629
- const slug = (plan.slug || '').padEnd(15);
630
- const title = (plan.title || '').substring(0, 23).padEnd(23);
631
- const status = (plan.status || 'draft').padEnd(9);
632
- const lastEdited = (plan.lastEdited || '').substring(0, 27).padEnd(27);
633
- const author = (plan.author || 'unknown').substring(0, 20);
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 deletePlan(slug) {
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(` ✗ Plan "${slug}" not found.`);
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 plan "${title}" (slug: ${slug})? This is irreversible. [y/N] `);
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 plan "${slug}".`);
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 exportPlan(slug) {
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(` ✗ Plan "${slug}" not found.`);
678
+ console.error(` ✗ Artifact "${slug}" not found.`);
679
679
  return false;
680
680
  }
681
681
 
682
- // Prefer the v2 canvas-derived markdown when plan.json exists. Fall back
683
- // to the raw mdx for v1 plans.
684
- const canvasFile = join(planDir, 'plan.json');
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, 'plan.mdx');
695
+ const planFile = join(planDir, 'artifact.mdx');
696
696
  if (!existsSync(planFile)) {
697
- console.error(` ✗ Plan "${slug}" not found.`);
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 plan <subcommand> [options]
710
+ bizar artifact <subcommand> [options]
711
711
 
712
712
  Subcommands:
713
- new <slug> [--template <name>] Create a new plan (default: blank template)
714
- open <slug> Open an existing plan in the browser
715
- list List all plans
716
- delete <slug> Delete a plan (with confirmation)
717
- export <slug> Export plan.mdx to stdout
718
- templates List available plan templates
719
- template save <name> <plan-slug> Save a plan as a library template
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 plans/<slug>/ as:
724
- - plan.mdx source content (in git)
725
- - plan.html viewer/editor (gitignored)
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 plan new my-feature
737
- bizar plan new auth-v2 --template feature-design
738
- bizar plan new oops --template bug-investigation
739
- bizar plan templates
740
- bizar plan open my-feature
741
- bizar plan list
742
- bizar plan export my-feature > my-feature.mdx
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 plan save)
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 plan.html
1117
- const htmlPath = join(planDir, 'plan.html');
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('plan.html not found. Run `bizar plan new ${slug}` first.');
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/plan/htmx.min.js ────────────
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/plan/');
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>/plan → MDX text/plain
1148
- // PUT /api/<slug>/plan → save MDX, returns empty 200
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 plan.
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 plan "${slug}"`);
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 plan';
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 plan.json
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>/plan → MDX text/plain
1658
- // PUT /api/<slug>/plan → save MDX
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>/plan
1665
- if (resource === 'plan' && req.method === 'GET') {
1666
- const mdx = readFileSync(join(planDir, 'plan.mdx'), 'utf-8');
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>/plan
1673
- if (resource === 'plan' && req.method === 'PUT') {
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, 'plan.mdx'), content);
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 plan.json.)
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/plan' && req.method === 'GET') {
1757
- const mdx = readFileSync(join(planDir, 'plan.mdx'), 'utf-8');
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/plan' && req.method === 'PUT') {
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, 'plan.mdx'), body);
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 plan.mdx and regenerate HTML
1841
- const planMdx = readFileSync(join(planDir, 'plan.mdx'), 'utf-8');
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, 'plan.html.template'), 'utf-8');
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, 'plan.html'), htmlContent);
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> <plan-slug>
1963
+ * template save <name> <artifact-slug>
1964
1964
  * template delete <name>
1965
1965
  * help
1966
1966
  */
1967
- export async function runPlan(argsOrPositional, legacyFlags) {
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> <plan-slug>" / "template delete <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 plan template save <name> <plan-slug>');
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 plan template delete <name>');
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> <plan-slug>, delete <name>, or list');
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 plan new <slug> [--template <name>]');
2032
+ console.error(' ✗ Usage: bizar artifact new <slug> [--template <name>]');
2033
2033
  return false;
2034
2034
  }
2035
- const created = await createPlan(slug, { template: flags.template || null });
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 openPlan(slug);
2038
+ return await openArtifact(slug);
2039
2039
  }
2040
2040
 
2041
2041
  case 'open': {
2042
2042
  if (!slug) {
2043
- console.error(' ✗ Usage: bizar plan open <slug>');
2043
+ console.error(' ✗ Usage: bizar artifact open <slug>');
2044
2044
  return false;
2045
2045
  }
2046
- return await openPlan(slug);
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 plan delete <slug>');
2055
+ console.error(' ✗ Usage: bizar artifact delete <slug>');
2056
2056
  return false;
2057
2057
  }
2058
- return await deletePlan(slug);
2058
+ return await deleteArtifact(slug);
2059
2059
  }
2060
2060
 
2061
2061
  case 'export': {
2062
2062
  if (!slug) {
2063
- console.error(' ✗ Usage: bizar plan export <slug>');
2063
+ console.error(' ✗ Usage: bizar artifact export <slug>');
2064
2064
  return false;
2065
2065
  }
2066
- return await exportPlan(slug);
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 plan help" for usage.`);
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 runPlan;
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.