@markdy/cli 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +242 -4
  2. package/package.json +5 -5
package/dist/index.js CHANGED
@@ -13,10 +13,21 @@ import {
13
13
  repairMarkdyCode,
14
14
  getIntelliCodeCompletions,
15
15
  predictNextLineSuggestion,
16
- getArchitectureSuggestions
16
+ getArchitectureSuggestions,
17
+ recommendArchitecturePattern,
18
+ synthesizeCustomRecipe,
19
+ getArchitectureRecipe,
20
+ listArchitectureRecipes,
21
+ verifyDiagramQuality,
22
+ analyzeC4Model,
23
+ filterC4Hierarchy,
24
+ generateC4Storyboard,
25
+ exportC4LevelViews,
26
+ detectArchitectureDrift,
27
+ autoHealArchitectureDrift
17
28
  } from "@markdy/core";
18
29
  import { createRequire } from "module";
19
- import { basename, dirname, extname, join, resolve, sep } from "path";
30
+ import { basename, dirname, extname, join, relative, resolve, sep } from "path";
20
31
  import { fileURLToPath, pathToFileURL } from "url";
21
32
  import { createServer } from "http";
22
33
  import { readdir, readFile, stat, writeFile } from "fs/promises";
@@ -69,6 +80,18 @@ async function runCli(argv, io = defaultIo(), runtime = defaultRuntime()) {
69
80
  return aiCommand(parsed, io, runtime);
70
81
  case "suggest":
71
82
  return suggestCommand(parsed, io);
83
+ case "guide":
84
+ return guideCommand(parsed, io);
85
+ case "recipe":
86
+ return recipeCommand(parsed, io);
87
+ case "verify":
88
+ case "doctor":
89
+ return verifyCommand(parsed, io);
90
+ case "drift":
91
+ case "sync":
92
+ return driftCommand(parsed, io);
93
+ case "c4":
94
+ return c4Command(parsed, io);
72
95
  case "check":
73
96
  return checkCommand(parsed, io);
74
97
  case "check-all":
@@ -275,6 +298,113 @@ async function explainCommand(parsed, io) {
275
298
  }
276
299
  return { exitCode: 0 };
277
300
  }
301
+ async function guideCommand(parsed, io) {
302
+ const query = parsed.positionals.join(" ").trim();
303
+ const jsonMode = hasFlag(parsed, "json");
304
+ const synthesizeMode = hasFlag(parsed, "synthesize");
305
+ if (!query) {
306
+ const recipes = listArchitectureRecipes();
307
+ if (jsonMode) {
308
+ io.stdout(JSON.stringify(recipes, null, 2));
309
+ } else {
310
+ io.stdout("Markdy Architecture Recipe Catalog:\n");
311
+ for (const r of recipes) {
312
+ io.stdout(` \u2022 [${r.id}] ${r.name} (${r.category}) - ${r.description}`);
313
+ }
314
+ io.stdout('\nRun `markdy guide "<query>"` or `markdy recipe <id>` to view full blueprint code.');
315
+ }
316
+ return { exitCode: 0 };
317
+ }
318
+ if (synthesizeMode) {
319
+ const synthesized = synthesizeCustomRecipe(query);
320
+ if (jsonMode) {
321
+ io.stdout(JSON.stringify(synthesized, null, 2));
322
+ } else {
323
+ io.stdout(`
324
+ \u26A1 Dynamic Architecture Synthesis for: "${query}"`);
325
+ io.stdout(`Pattern: ${synthesized.inferredPattern}`);
326
+ io.stdout(`Components: ${synthesized.detectedComponents.map((c) => `${c.id} (${c.kind})`).join(", ")}`);
327
+ io.stdout(`Rationale: ${synthesized.rationale}
328
+ `);
329
+ io.stdout("Synthesized MarkdyScript:\n");
330
+ io.stdout(synthesized.markdyScript);
331
+ }
332
+ return { exitCode: 0 };
333
+ }
334
+ const recommendations = recommendArchitecturePattern(query);
335
+ if (jsonMode) {
336
+ io.stdout(JSON.stringify(recommendations, null, 2));
337
+ return { exitCode: 0 };
338
+ }
339
+ const top = recommendations[0];
340
+ io.stdout(`
341
+ \u2728 Recommended Architecture Pattern: ${top.recipe.name} (${top.recipe.id})`);
342
+ io.stdout(`Category: ${top.recipe.category} | Layout: ${top.recipe.recommendedLayout}`);
343
+ io.stdout(`Rationale: ${top.rationale}
344
+ `);
345
+ io.stdout("Highlights:");
346
+ for (const hl of top.recipe.highlights) {
347
+ io.stdout(` - ${hl}`);
348
+ }
349
+ io.stdout("\nCanonical Markdy Blueprint:\n");
350
+ io.stdout(top.recipe.code);
351
+ return { exitCode: 0 };
352
+ }
353
+ async function recipeCommand(parsed, io) {
354
+ const recipeId = parsed.positionals[0];
355
+ const jsonMode = hasFlag(parsed, "json");
356
+ if (!recipeId) {
357
+ return guideCommand(parsed, io);
358
+ }
359
+ const recipe = getArchitectureRecipe(recipeId);
360
+ if (!recipe) {
361
+ io.stderr(`markdy recipe: no recipe found for '${recipeId}'. Run 'markdy guide' to list available recipes.`);
362
+ return { exitCode: 1 };
363
+ }
364
+ if (jsonMode) {
365
+ io.stdout(JSON.stringify(recipe, null, 2));
366
+ } else {
367
+ io.stdout(recipe.code);
368
+ }
369
+ return { exitCode: 0 };
370
+ }
371
+ async function verifyCommand(parsed, io) {
372
+ const file = parsed.positionals[0];
373
+ if (!file) {
374
+ io.stderr("markdy verify: expected a .markdy input file");
375
+ return { exitCode: 1 };
376
+ }
377
+ const qualityFlag = getStringFlag(parsed, "quality")?.toLowerCase();
378
+ const strict = hasFlag(parsed, "strict") || qualityFlag === "strict" || qualityFlag === "showcase";
379
+ const profile = strict ? "showcase" : "standard";
380
+ const jsonMode = hasFlag(parsed, "json");
381
+ const scene = await loadSceneFromFile(file);
382
+ const report = verifyDiagramQuality(scene.ast, { profile });
383
+ if (jsonMode) {
384
+ io.stdout(JSON.stringify(report, null, 2));
385
+ return { exitCode: report.passed ? 0 : 1 };
386
+ }
387
+ io.stdout("\n\u{1F50D} Markdy 9-Point Quality Gate & Viewport Verification");
388
+ io.stdout(`File: ${scene.filePath}`);
389
+ io.stdout(`Profile: ${report.qualityProfile.toUpperCase()} | SHA-256 Receipt: ${report.sha256Receipt}`);
390
+ io.stdout(`Status: ${report.passed ? "\u2705 PASSED" : "\u274C FAILED"} (Errors: ${report.errorCount}, Warnings: ${report.warningCount})
391
+ `);
392
+ io.stdout("Checks:");
393
+ for (const check of report.checks) {
394
+ const symbol = check.status === "pass" ? "\u2713" : check.status === "warn" ? "\u26A0" : "\u2717";
395
+ io.stdout(` ${symbol} [${check.id}] ${check.name}: ${check.message}`);
396
+ }
397
+ io.stdout("\nViewport Compliance:");
398
+ for (const [vp, compliant] of Object.entries(report.viewportCompliance)) {
399
+ io.stdout(` \u2022 ${vp}: ${compliant ? "\u2713 PASS" : "\u2717 OVERFLOW"}`);
400
+ }
401
+ io.stdout("\nMetrics:");
402
+ io.stdout(` Nodes: ${report.metrics.nodeCount} | Edges: ${report.metrics.edgeCount} | Story Beats: ${report.metrics.beatCount}`);
403
+ io.stdout(` Estimated Dimensions: ${report.metrics.estimatedWidth}\xD7${report.metrics.estimatedHeight} (Aspect Ratio: ${report.metrics.aspectRatio})`);
404
+ io.stdout(` Code Provenance Anchors: ${report.metrics.provenanceAnchorCount} | Vector Symbols: ${report.metrics.symbolCount}
405
+ `);
406
+ return { exitCode: report.passed ? 0 : 1 };
407
+ }
278
408
  async function newCommand(parsed, io) {
279
409
  const target = parsed.positionals[0] ?? "scene.markdy";
280
410
  const force = hasFlag(parsed, "force");
@@ -310,6 +440,10 @@ async function importCommand(parsed, io) {
310
440
  let markdyCode;
311
441
  if (formatFlag === "mermaid" || ext === ".mmd" || ext === ".mermaid") {
312
442
  markdyCode = compat.transpileMermaidToMarkdy(content, title).code;
443
+ } else if (formatFlag === "d2" || ext === ".d2") {
444
+ markdyCode = compat.transpileD2ToMarkdy(content).markdyScript;
445
+ } else if (formatFlag === "plantuml" || formatFlag === "puml" || ext === ".puml" || ext === ".plantuml" || content.includes("@startuml")) {
446
+ markdyCode = compat.transpilePlantUmlToMarkdy(content).markdyScript;
313
447
  } else if (formatFlag === "compose" || (ext === ".yml" || ext === ".yaml") && (inputFile.includes("compose") || content.includes("services:"))) {
314
448
  markdyCode = compat.transpileDockerComposeToMarkdy(content, title);
315
449
  } else if (formatFlag === "k8s" || content.includes("apiVersion:") && content.includes("kind:")) {
@@ -355,6 +489,105 @@ async function diffCommand(parsed, io) {
355
489
  io.stdout(diffResult.summaryMarkdown);
356
490
  return { exitCode: 0 };
357
491
  }
492
+ async function collectRepoFiles(dir, baseDir = dir) {
493
+ const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
494
+ const files = [];
495
+ for (const entry of entries) {
496
+ if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist" || entry.name === "build") {
497
+ continue;
498
+ }
499
+ const full = join(dir, entry.name);
500
+ if (entry.isDirectory()) {
501
+ files.push(...await collectRepoFiles(full, baseDir));
502
+ } else if (entry.isFile()) {
503
+ files.push(relative(baseDir, full).replace(/\\/g, "/"));
504
+ }
505
+ }
506
+ return files;
507
+ }
508
+ async function driftCommand(parsed, io) {
509
+ const file = parsed.positionals[0];
510
+ if (!file) {
511
+ io.stderr("markdy drift: expected a .markdy input file (e.g. markdy drift system.markdy)");
512
+ return { exitCode: 1 };
513
+ }
514
+ const scene = await loadSceneFromFile(file).catch((err) => {
515
+ io.stderr(`markdy drift: ${describeError(err)}`);
516
+ return null;
517
+ });
518
+ if (!scene) return { exitCode: 1 };
519
+ const repoRoot = resolve(getStringFlag(parsed, "repo") || process.cwd());
520
+ const repoFiles = await collectRepoFiles(repoRoot);
521
+ const report = detectArchitectureDrift(scene.ast, repoFiles);
522
+ const fixMode = hasFlag(parsed, "fix");
523
+ if (fixMode) {
524
+ const healed = autoHealArchitectureDrift(scene.ast, report, repoFiles);
525
+ await writeFile(scene.filePath, healed.healedMarkdyScript, "utf8");
526
+ if (hasFlag(parsed, "json")) {
527
+ io.stdout(JSON.stringify({ report, healed }, null, 2));
528
+ } else {
529
+ io.stdout(report.summaryMarkdown);
530
+ io.stdout(`
531
+ \u2705 Auto-healed ${healed.healedAnchorCount} broken anchor(s) and incorporated ${healed.addedServiceCount} orphan service(s) into ${file}`);
532
+ for (const m of healed.healedMappings) {
533
+ io.stdout(` \u2022 ${m.nodeId}: \`${m.oldPath}\` \u2192 \`${m.newPath}\``);
534
+ }
535
+ }
536
+ return { exitCode: 0 };
537
+ }
538
+ if (hasFlag(parsed, "json")) {
539
+ io.stdout(JSON.stringify(report, null, 2));
540
+ return { exitCode: report.isSynchronized ? 0 : 1 };
541
+ }
542
+ io.stdout(report.summaryMarkdown);
543
+ if (report.healingMarkdySnippet) {
544
+ io.stdout("\n\u2728 Suggested MarkdyScript Additions (Pass `--fix` to auto-apply):\n```markdy\n" + report.healingMarkdySnippet + "\n```\n");
545
+ }
546
+ return { exitCode: report.isSynchronized ? 0 : 1 };
547
+ }
548
+ async function c4Command(parsed, io) {
549
+ const file = parsed.positionals[0];
550
+ if (!file) {
551
+ io.stderr("markdy c4: expected a .markdy input file (e.g. markdy c4 system.markdy)");
552
+ return { exitCode: 1 };
553
+ }
554
+ const scene = await loadSceneFromFile(file).catch((err) => {
555
+ io.stderr(`markdy c4: ${describeError(err)}`);
556
+ return null;
557
+ });
558
+ if (!scene) return { exitCode: 1 };
559
+ if (hasFlag(parsed, "storyboard")) {
560
+ const storyboard = generateC4Storyboard(scene.ast);
561
+ io.stdout(storyboard);
562
+ return { exitCode: 0 };
563
+ }
564
+ if (hasFlag(parsed, "export-views")) {
565
+ const views = exportC4LevelViews(scene.ast);
566
+ const targetDir = getStringFlag(parsed, "out") || dirname(resolve(file));
567
+ const base = basename(file, extname(file));
568
+ for (const [lvl, exportData] of Object.entries(views)) {
569
+ const outPath = join(targetDir, `${base}-L${exportData.levelNumber}-${lvl}.markdy`);
570
+ await writeFile(outPath, exportData.markdyScript, "utf8");
571
+ io.stdout(` \u2713 Exported C4 L${exportData.levelNumber} [${lvl.toUpperCase()}]: ${outPath} (${exportData.nodeCount} nodes, ${exportData.edgeCount} flows)`);
572
+ }
573
+ io.stdout(`
574
+ \u{1F4E6} Successfully exported 4 C4 level blueprints to ${targetDir}`);
575
+ return { exitCode: 0 };
576
+ }
577
+ const levelFlag = getStringFlag(parsed, "level");
578
+ if (levelFlag) {
579
+ const { visibleNodeIds } = filterC4Hierarchy(scene.ast, levelFlag);
580
+ io.stdout(`C4 Level [${levelFlag.toUpperCase()}]: ${visibleNodeIds.length} nodes visible (${visibleNodeIds.join(", ")})`);
581
+ return { exitCode: 0 };
582
+ }
583
+ const report = analyzeC4Model(scene.ast);
584
+ if (hasFlag(parsed, "json")) {
585
+ io.stdout(JSON.stringify(report, null, 2));
586
+ return { exitCode: 0 };
587
+ }
588
+ io.stdout(report.summaryMarkdown);
589
+ return { exitCode: 0 };
590
+ }
358
591
  async function shareCommand(parsed, io) {
359
592
  const file = parsed.positionals[0];
360
593
  if (!file) {
@@ -1255,8 +1488,13 @@ function helpText() {
1255
1488
  " markdy lint <file-or-dir> [--strict] [--arch-rules]",
1256
1489
  " markdy fmt <file-or-dir> [--write | --check]",
1257
1490
  " markdy render <file.markdy> [--out file.html] [--port 4242] [--no-open]",
1491
+ " markdy verify <file.markdy> [--quality standard|strict|showcase] [--json]",
1492
+ " markdy guide [scenario-query] [--synthesize] [--json]",
1493
+ " markdy recipe <recipe-id> [--json]",
1494
+ " markdy c4 <file.markdy> [--level context|container|component|code] [--storyboard] [--export-views] [--out <dir>] [--json]",
1495
+ " markdy drift <file.markdy> [--repo <dir>] [--fix] [--json]",
1258
1496
  " markdy explain <file.markdy> [--json]",
1259
- " markdy import <file> [--from compose|k8s|terraform|mermaid] [--out scene.markdy]",
1497
+ " markdy import <file> [--from compose|k8s|terraform|mermaid|d2|plantuml] [--out scene.markdy]",
1260
1498
  " markdy diff <before.markdy> <after.markdy> [--evolution]",
1261
1499
  " markdy share <file.markdy>",
1262
1500
  " markdy suggest <file.markdy> [--line <n>] [--col <n>] [--json]",
@@ -1322,7 +1560,7 @@ function parseArgv(argv) {
1322
1560
  return { command, positionals, flags };
1323
1561
  }
1324
1562
  function expectsValue(flag) {
1325
- return flag === "out" || flag === "port" || flag === "config" || flag === "dist" || flag === "from" || flag === "line" || flag === "col";
1563
+ return flag === "out" || flag === "port" || flag === "config" || flag === "dist" || flag === "from" || flag === "line" || flag === "col" || flag === "quality" || flag === "level" || flag === "repo" || flag === "theme" || flag === "format";
1326
1564
  }
1327
1565
  function hasFlag(parsed, name) {
1328
1566
  return parsed.flags.get(name) === true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markdy/cli",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "First-party CLI for diagram-native MarkdyScript diagrams: lint, format, explain, render, and preview.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -43,10 +43,10 @@
43
43
  "registry": "https://registry.npmjs.org"
44
44
  },
45
45
  "dependencies": {
46
- "@markdy/compat": "1.2.0",
47
- "@markdy/renderer-dom": "1.2.0",
48
- "@markdy/core": "1.2.0",
49
- "@markdy/stdlib-systems": "1.2.0"
46
+ "@markdy/compat": "1.3.0",
47
+ "@markdy/stdlib-systems": "1.3.0",
48
+ "@markdy/core": "1.3.0",
49
+ "@markdy/renderer-dom": "1.3.0"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@types/node": "^25.9.5",