@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.
- package/dist/index.js +242 -4
- 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.
|
|
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.
|
|
47
|
-
"@markdy/
|
|
48
|
-
"@markdy/core": "1.
|
|
49
|
-
"@markdy/
|
|
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",
|