@dependency-maritime/cli 0.1.0-beta.1 → 0.1.0-beta.3

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/README.md CHANGED
@@ -127,3 +127,16 @@ This project uses **npm** for package management. Please ensure you lock files a
127
127
  ## 📝 License
128
128
 
129
129
  Distributed under the MIT License.
130
+
131
+ ### Render existing graph evidence
132
+
133
+ With Graphviz `dot` installed, render without performing a second analysis:
134
+
135
+ ```bash
136
+ maritime graph --input .maritime --output docs/images/dependency-graph.svg
137
+ ```
138
+
139
+ `.maritime/dependency-graph.json` is canonical evidence; SVG and DOT outputs are derived
140
+ presentations. The composite Action's reproducible committed-SVG contract is limited to its pinned
141
+ Ubuntu Graphviz path; other runners must provide and pin `dot` themselves because layout can vary
142
+ between Graphviz versions.
@@ -0,0 +1 @@
1
+ export declare function runGraphCommand(args: string[]): Promise<number>;
@@ -0,0 +1,6 @@
1
+ import type { MaritimeCruiseResult } from '../../schema/dependency-cruiser';
2
+ declare function externalPackageName(value: string): string | undefined;
3
+ /** Pure, deterministic conversion of a validated dependency-cruiser result to Graphviz DOT. */
4
+ export declare function renderDependencyGraphToDot(graph: MaritimeCruiseResult): string;
5
+ export declare function inferGraphvizFormat(outputPath: string): 'svg' | 'dot';
6
+ export { externalPackageName };
@@ -0,0 +1,4 @@
1
+ export declare function normalizeGraphvizSvg(svg: string): string;
2
+ export declare function renderDotWithGraphviz(dot: string, outputPath: string, options?: {
3
+ executable?: string;
4
+ }): Promise<void>;
@@ -1,7 +1,10 @@
1
1
  import { runAnalyzeCommand } from './commands/analyze';
2
2
  import { runValidateCommand } from './commands/validate';
3
+ import { runGraphCommand } from './commands/graph';
3
4
  import { validateArtifacts } from './validate/validate';
4
- export { runAnalyzeCommand, runValidateCommand, validateArtifacts };
5
+ export { runAnalyzeCommand, runValidateCommand, runGraphCommand, validateArtifacts };
6
+ export { renderDependencyGraphToDot, inferGraphvizFormat } from './graph/render-dot';
7
+ export { renderDotWithGraphviz, normalizeGraphvizSvg } from './graph/render-graphviz';
5
8
  export * from '../schema/manifest';
6
9
  export { readDependencyGraph, resolveDepcruiseConfig, generateDependencyGraph, runEslintComplexityScan, countLinesOfCode, writeOutputFiles } from './analyze/adapters';
7
10
  export { calculateMetrics, isSupportedTypeScriptFile, calculateInstability, calculateScore, calculateHealthScore } from './analyze/calculate-metrics';
@@ -1,7 +1,10 @@
1
1
  import { runAnalyzeCommand } from './commands/analyze';
2
2
  import { runValidateCommand } from './commands/validate';
3
+ import { runGraphCommand } from './commands/graph';
3
4
  import { validateArtifacts } from './validate/validate';
4
- export { runAnalyzeCommand, runValidateCommand, validateArtifacts };
5
+ export { runAnalyzeCommand, runValidateCommand, runGraphCommand, validateArtifacts };
6
+ export { renderDependencyGraphToDot, inferGraphvizFormat } from './graph/render-dot';
7
+ export { renderDotWithGraphviz, normalizeGraphvizSvg } from './graph/render-graphviz';
5
8
  export * from '../schema/manifest';
6
9
  export { readDependencyGraph, resolveDepcruiseConfig, generateDependencyGraph, runEslintComplexityScan, countLinesOfCode, writeOutputFiles } from './analyze/adapters';
7
10
  export { calculateMetrics, isSupportedTypeScriptFile, calculateInstability, calculateScore, calculateHealthScore } from './analyze/calculate-metrics';
package/dist/cli/index.js CHANGED
@@ -960,8 +960,8 @@ async function validateArtifacts(options = {}) {
960
960
  const artifactDirRelative = options.artifactDir ?? ".maritime";
961
961
  const artifactDir = path5.resolve(workingDir, artifactDirRelative);
962
962
  try {
963
- const stat2 = await fsPromises2.stat(artifactDir);
964
- if (!stat2.isDirectory()) {
963
+ const stat3 = await fsPromises2.stat(artifactDir);
964
+ if (!stat3.isDirectory()) {
965
965
  throw new ValidationError(`Artifact path is not a directory: ${artifactDirRelative}`);
966
966
  }
967
967
  } catch (err) {
@@ -1125,6 +1125,215 @@ Exit Codes:
1125
1125
  }
1126
1126
  }
1127
1127
 
1128
+ // src/cli/commands/graph.ts
1129
+ import { parseArgs as parseArgs3 } from "node:util";
1130
+ import * as fs4 from "node:fs/promises";
1131
+ import * as path9 from "node:path";
1132
+
1133
+ // src/cli/graph/render-dot.ts
1134
+ import * as path7 from "node:path";
1135
+ var dotQuote = (value) => JSON.stringify(value);
1136
+ var normalizedPath = (value) => value.replaceAll("\\", "/").replace(/^\.\//, "");
1137
+ function externalPackageName(value) {
1138
+ const normalized = normalizedPath(value);
1139
+ const marker = "node_modules/";
1140
+ const markerIndex = normalized.lastIndexOf(marker);
1141
+ const packagePath = markerIndex >= 0 ? normalized.slice(markerIndex + marker.length) : normalized;
1142
+ const parts = packagePath.split("/").filter(Boolean);
1143
+ if (markerIndex < 0 && (packagePath.startsWith(".") || packagePath.startsWith("/") || parts.length === 0)) {
1144
+ return void 0;
1145
+ }
1146
+ return parts[0]?.startsWith("@") && parts[1] ? `${parts[0]}/${parts[1]}` : parts[0];
1147
+ }
1148
+ function isExternalPath(value) {
1149
+ return normalizedPath(value).split("/").includes("node_modules");
1150
+ }
1151
+ function addLocalModule(root, source) {
1152
+ const parts = normalizedPath(source).split("/").filter((part) => part && part !== ".");
1153
+ const name = parts.pop();
1154
+ if (!name) return;
1155
+ let directory = root;
1156
+ for (const part of parts) {
1157
+ let child = directory.directories.get(part);
1158
+ if (!child) {
1159
+ child = { directories: /* @__PURE__ */ new Map(), files: [] };
1160
+ directory.directories.set(part, child);
1161
+ }
1162
+ directory = child;
1163
+ }
1164
+ directory.files.push({ source: normalizedPath(source), name });
1165
+ }
1166
+ function renderDirectory(directory, segments, indent) {
1167
+ const lines = [];
1168
+ for (const [name, child] of [...directory.directories].sort(([a], [b]) => a.localeCompare(b))) {
1169
+ const childSegments = [...segments, name];
1170
+ lines.push(`${indent}subgraph ${dotQuote(`cluster:${childSegments.join("/")}`)} {`);
1171
+ lines.push(`${indent} label=${dotQuote(name)};`);
1172
+ lines.push(`${indent} color="#94a3b8";`);
1173
+ lines.push(...renderDirectory(child, childSegments, `${indent} `));
1174
+ lines.push(`${indent}}`);
1175
+ }
1176
+ for (const file of [...directory.files].sort((a, b) => a.name.localeCompare(b.name) || a.source.localeCompare(b.source))) {
1177
+ lines.push(`${indent}${dotQuote(`local:${file.source}`)} [label=${dotQuote(file.name)}, shape="box"];`);
1178
+ }
1179
+ return lines;
1180
+ }
1181
+ function edgeAttributes(dependency) {
1182
+ const attributes = [];
1183
+ const dependencyTypes = [...dependency.dependencyTypes].sort();
1184
+ if (dependencyTypes.length > 0) attributes.push(`label=${dotQuote(dependencyTypes.join(", "))}`);
1185
+ if (dependency.typeOnly || dependency.preCompilationOnly) attributes.push('style="dashed"');
1186
+ if (dependency.circular) attributes.push('color="#d97706"', 'penwidth="2"');
1187
+ if (!dependency.valid) {
1188
+ if (dependency.circular) attributes.push('xlabel="invalid"', 'fontcolor="#dc2626"');
1189
+ else attributes.push('color="#dc2626"', 'penwidth="2"');
1190
+ }
1191
+ return attributes.length > 0 ? ` [${attributes.join(", ")}]` : "";
1192
+ }
1193
+ function renderDependencyGraphToDot(graph) {
1194
+ const root = { directories: /* @__PURE__ */ new Map(), files: [] };
1195
+ const localSources = /* @__PURE__ */ new Set();
1196
+ const externalPackages = /* @__PURE__ */ new Set();
1197
+ for (const module of graph.modules) {
1198
+ const source = normalizedPath(module.source);
1199
+ if (isExternalPath(source) || module.coreModule) continue;
1200
+ localSources.add(source);
1201
+ addLocalModule(root, source);
1202
+ }
1203
+ const edges = [];
1204
+ for (const module of [...graph.modules].sort((a, b) => a.source.localeCompare(b.source))) {
1205
+ const source = normalizedPath(module.source);
1206
+ if (!localSources.has(source)) continue;
1207
+ for (const dependency of module.dependencies) {
1208
+ const resolved = normalizedPath(dependency.resolved);
1209
+ let target;
1210
+ if (localSources.has(resolved)) {
1211
+ target = `local:${resolved}`;
1212
+ } else if (!dependency.coreModule) {
1213
+ const looksExternal = isExternalPath(resolved) || dependency.dependencyTypes.includes("npm");
1214
+ const packageName = looksExternal ? externalPackageName(isExternalPath(resolved) ? resolved : dependency.module) : void 0;
1215
+ if (packageName) {
1216
+ externalPackages.add(packageName);
1217
+ target = `external:${packageName}`;
1218
+ }
1219
+ }
1220
+ if (target) edges.push({ from: `local:${source}`, to: target, dependency });
1221
+ }
1222
+ }
1223
+ edges.sort((a, b) => a.from.localeCompare(b.from) || a.to.localeCompare(b.to) || JSON.stringify(a.dependency).localeCompare(JSON.stringify(b.dependency)));
1224
+ const lines = [
1225
+ 'digraph "dependency-graph" {',
1226
+ ' graph [compound="true", rankdir="LR", fontname="Helvetica"];',
1227
+ ' node [fontname="Helvetica", fontsize="10"];',
1228
+ ' edge [fontname="Helvetica", fontsize="8"];',
1229
+ ...renderDirectory(root, [], " ")
1230
+ ];
1231
+ if (externalPackages.size > 0) {
1232
+ lines.push(' subgraph "cluster:external-packages" {', ' label="External packages";', ' style="dashed";', ' color="#64748b";');
1233
+ for (const packageName of [...externalPackages].sort()) {
1234
+ lines.push(` ${dotQuote(`external:${packageName}`)} [label=${dotQuote(packageName)}, shape="component", style="filled", fillcolor="#e2e8f0"];`);
1235
+ }
1236
+ lines.push(" }");
1237
+ }
1238
+ for (const edge of edges) {
1239
+ lines.push(` ${dotQuote(edge.from)} -> ${dotQuote(edge.to)}${edgeAttributes(edge.dependency)};`);
1240
+ }
1241
+ lines.push("}", "");
1242
+ return lines.join("\n");
1243
+ }
1244
+ function inferGraphvizFormat(outputPath) {
1245
+ const extension = path7.extname(outputPath).toLowerCase();
1246
+ if (extension === ".svg") return "svg";
1247
+ if (extension === ".dot") return "dot";
1248
+ throw new Error(`Unsupported graph output format "${extension || "(none)"}". Use an .svg or .dot output path.`);
1249
+ }
1250
+
1251
+ // src/cli/graph/render-graphviz.ts
1252
+ import { spawn } from "node:child_process";
1253
+ import * as fs3 from "node:fs/promises";
1254
+ import * as path8 from "node:path";
1255
+ function normalizeGraphvizSvg(svg) {
1256
+ return svg.replace(/<!-- Generated by graphviz version .*? -->/gi, "<!-- Generated by Graphviz -->");
1257
+ }
1258
+ async function renderDotWithGraphviz(dot, outputPath, options = {}) {
1259
+ const executable = options.executable ?? "dot";
1260
+ const svg = await new Promise((resolve7, reject) => {
1261
+ const child = spawn(executable, ["-Tsvg"], { stdio: ["pipe", "pipe", "pipe"] });
1262
+ const stdout = [];
1263
+ const stderr = [];
1264
+ child.stdout.on("data", (chunk) => stdout.push(chunk));
1265
+ child.stderr.on("data", (chunk) => stderr.push(chunk));
1266
+ child.on("error", (error) => {
1267
+ if (error.code === "ENOENT") {
1268
+ reject(new Error(`Graphviz executable "${executable}" was not found. Install Graphviz and ensure "dot" is available on PATH.`));
1269
+ } else {
1270
+ reject(new Error(`Unable to start Graphviz: ${error.message}`));
1271
+ }
1272
+ });
1273
+ child.on("close", (code) => {
1274
+ if (code === 0) resolve7(Buffer.concat(stdout).toString("utf8"));
1275
+ else reject(new Error(`Graphviz exited with code ${code}: ${Buffer.concat(stderr).toString("utf8").trim()}`));
1276
+ });
1277
+ child.stdin.end(dot);
1278
+ });
1279
+ await fs3.mkdir(path8.dirname(outputPath), { recursive: true });
1280
+ await fs3.writeFile(outputPath, normalizeGraphvizSvg(svg));
1281
+ }
1282
+
1283
+ // src/cli/commands/graph.ts
1284
+ async function runGraphCommand(args) {
1285
+ let values;
1286
+ try {
1287
+ values = parseArgs3({ args, options: {
1288
+ input: { type: "string", short: "i" },
1289
+ output: { type: "string", short: "o" },
1290
+ cwd: { type: "string" },
1291
+ help: { type: "boolean", short: "h" }
1292
+ } }).values;
1293
+ } catch (error) {
1294
+ console.error(`Error parsing arguments: ${error instanceof Error ? error.message : String(error)}`);
1295
+ return 2;
1296
+ }
1297
+ if (values.help) {
1298
+ console.log(`
1299
+ Usage: maritime graph --input <artifact-directory-or-graph.json> --output <graph.svg|graph.dot>
1300
+
1301
+ Renders existing canonical graph evidence without running dependency analysis.
1302
+
1303
+ Options:
1304
+ -i, --input <path> .maritime directory or dependency-graph.json (default: .maritime)
1305
+ -o, --output <path> SVG or DOT output path (required)
1306
+ --cwd <dir> Working directory root for resolution
1307
+ -h, --help Show help message
1308
+ `);
1309
+ return 0;
1310
+ }
1311
+ if (!values.output) {
1312
+ console.error("Error: --output is required.");
1313
+ return 2;
1314
+ }
1315
+ const cwd = path9.resolve(values.cwd ?? process.cwd());
1316
+ const input = path9.resolve(cwd, values.input ?? ".maritime");
1317
+ const output = path9.resolve(cwd, values.output);
1318
+ try {
1319
+ const stat3 = await fs4.stat(input);
1320
+ const graphPath = stat3.isDirectory() ? path9.resolve(input, (await validateArtifacts({ artifactDir: input, cwd })).manifest.artifacts.graph) : input;
1321
+ const parsed = JSON.parse(await fs4.readFile(graphPath, "utf8"));
1322
+ const result = CruiseResultSchema.safeParse(parsed);
1323
+ if (!result.success) throw new Error(`Invalid Maritime dependency graph: ${result.error.message}`);
1324
+ const dot = renderDependencyGraphToDot(result.data);
1325
+ const format = inferGraphvizFormat(output);
1326
+ await fs4.mkdir(path9.dirname(output), { recursive: true });
1327
+ if (format === "dot") await fs4.writeFile(output, dot);
1328
+ else await renderDotWithGraphviz(dot, output);
1329
+ console.log(`\u2705 Dependency graph rendered from ${graphPath} to ${output}`);
1330
+ return 0;
1331
+ } catch (error) {
1332
+ console.error(`Error rendering dependency graph: ${error instanceof Error ? error.message : String(error)}`);
1333
+ return 2;
1334
+ }
1335
+ }
1336
+
1128
1337
  // src/cli/index.ts
1129
1338
  async function analyzeProject(options) {
1130
1339
  const args = [];
@@ -1181,13 +1390,18 @@ export {
1181
1390
  countLinesOfCode,
1182
1391
  detectEslintConfig,
1183
1392
  generateDependencyGraph,
1393
+ inferGraphvizFormat,
1184
1394
  isSupportedTypeScriptFile,
1395
+ normalizeGraphvizSvg,
1185
1396
  parseEslintComplexityReport,
1186
1397
  readDependencyGraph,
1398
+ renderDependencyGraphToDot,
1399
+ renderDotWithGraphviz,
1187
1400
  renderMarkdownReport,
1188
1401
  resolveDepcruiseConfig,
1189
1402
  runAnalyzeCommand,
1190
1403
  runEslintComplexityScan,
1404
+ runGraphCommand,
1191
1405
  runValidateCommand,
1192
1406
  validateArtifacts,
1193
1407
  validateEslintEnvironment,
package/dist/cli/main.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli/main.ts
4
- import { parseArgs as parseArgs3 } from "node:util";
4
+ import { parseArgs as parseArgs4 } from "node:util";
5
5
 
6
6
  // src/cli/commands/analyze.ts
7
7
  import { parseArgs } from "node:util";
@@ -965,8 +965,8 @@ async function validateArtifacts(options = {}) {
965
965
  const artifactDirRelative = options.artifactDir ?? ".maritime";
966
966
  const artifactDir = path5.resolve(workingDir, artifactDirRelative);
967
967
  try {
968
- const stat2 = await fsPromises2.stat(artifactDir);
969
- if (!stat2.isDirectory()) {
968
+ const stat3 = await fsPromises2.stat(artifactDir);
969
+ if (!stat3.isDirectory()) {
970
970
  throw new ValidationError(`Artifact path is not a directory: ${artifactDirRelative}`);
971
971
  }
972
972
  } catch (err) {
@@ -1130,11 +1130,220 @@ Exit Codes:
1130
1130
  }
1131
1131
  }
1132
1132
 
1133
+ // src/cli/commands/graph.ts
1134
+ import { parseArgs as parseArgs3 } from "node:util";
1135
+ import * as fs4 from "node:fs/promises";
1136
+ import * as path9 from "node:path";
1137
+
1138
+ // src/cli/graph/render-dot.ts
1139
+ import * as path7 from "node:path";
1140
+ var dotQuote = (value) => JSON.stringify(value);
1141
+ var normalizedPath = (value) => value.replaceAll("\\", "/").replace(/^\.\//, "");
1142
+ function externalPackageName(value) {
1143
+ const normalized = normalizedPath(value);
1144
+ const marker = "node_modules/";
1145
+ const markerIndex = normalized.lastIndexOf(marker);
1146
+ const packagePath = markerIndex >= 0 ? normalized.slice(markerIndex + marker.length) : normalized;
1147
+ const parts = packagePath.split("/").filter(Boolean);
1148
+ if (markerIndex < 0 && (packagePath.startsWith(".") || packagePath.startsWith("/") || parts.length === 0)) {
1149
+ return void 0;
1150
+ }
1151
+ return parts[0]?.startsWith("@") && parts[1] ? `${parts[0]}/${parts[1]}` : parts[0];
1152
+ }
1153
+ function isExternalPath(value) {
1154
+ return normalizedPath(value).split("/").includes("node_modules");
1155
+ }
1156
+ function addLocalModule(root, source) {
1157
+ const parts = normalizedPath(source).split("/").filter((part) => part && part !== ".");
1158
+ const name = parts.pop();
1159
+ if (!name) return;
1160
+ let directory = root;
1161
+ for (const part of parts) {
1162
+ let child = directory.directories.get(part);
1163
+ if (!child) {
1164
+ child = { directories: /* @__PURE__ */ new Map(), files: [] };
1165
+ directory.directories.set(part, child);
1166
+ }
1167
+ directory = child;
1168
+ }
1169
+ directory.files.push({ source: normalizedPath(source), name });
1170
+ }
1171
+ function renderDirectory(directory, segments, indent) {
1172
+ const lines = [];
1173
+ for (const [name, child] of [...directory.directories].sort(([a], [b]) => a.localeCompare(b))) {
1174
+ const childSegments = [...segments, name];
1175
+ lines.push(`${indent}subgraph ${dotQuote(`cluster:${childSegments.join("/")}`)} {`);
1176
+ lines.push(`${indent} label=${dotQuote(name)};`);
1177
+ lines.push(`${indent} color="#94a3b8";`);
1178
+ lines.push(...renderDirectory(child, childSegments, `${indent} `));
1179
+ lines.push(`${indent}}`);
1180
+ }
1181
+ for (const file of [...directory.files].sort((a, b) => a.name.localeCompare(b.name) || a.source.localeCompare(b.source))) {
1182
+ lines.push(`${indent}${dotQuote(`local:${file.source}`)} [label=${dotQuote(file.name)}, shape="box"];`);
1183
+ }
1184
+ return lines;
1185
+ }
1186
+ function edgeAttributes(dependency) {
1187
+ const attributes = [];
1188
+ const dependencyTypes = [...dependency.dependencyTypes].sort();
1189
+ if (dependencyTypes.length > 0) attributes.push(`label=${dotQuote(dependencyTypes.join(", "))}`);
1190
+ if (dependency.typeOnly || dependency.preCompilationOnly) attributes.push('style="dashed"');
1191
+ if (dependency.circular) attributes.push('color="#d97706"', 'penwidth="2"');
1192
+ if (!dependency.valid) {
1193
+ if (dependency.circular) attributes.push('xlabel="invalid"', 'fontcolor="#dc2626"');
1194
+ else attributes.push('color="#dc2626"', 'penwidth="2"');
1195
+ }
1196
+ return attributes.length > 0 ? ` [${attributes.join(", ")}]` : "";
1197
+ }
1198
+ function renderDependencyGraphToDot(graph) {
1199
+ const root = { directories: /* @__PURE__ */ new Map(), files: [] };
1200
+ const localSources = /* @__PURE__ */ new Set();
1201
+ const externalPackages = /* @__PURE__ */ new Set();
1202
+ for (const module of graph.modules) {
1203
+ const source = normalizedPath(module.source);
1204
+ if (isExternalPath(source) || module.coreModule) continue;
1205
+ localSources.add(source);
1206
+ addLocalModule(root, source);
1207
+ }
1208
+ const edges = [];
1209
+ for (const module of [...graph.modules].sort((a, b) => a.source.localeCompare(b.source))) {
1210
+ const source = normalizedPath(module.source);
1211
+ if (!localSources.has(source)) continue;
1212
+ for (const dependency of module.dependencies) {
1213
+ const resolved = normalizedPath(dependency.resolved);
1214
+ let target;
1215
+ if (localSources.has(resolved)) {
1216
+ target = `local:${resolved}`;
1217
+ } else if (!dependency.coreModule) {
1218
+ const looksExternal = isExternalPath(resolved) || dependency.dependencyTypes.includes("npm");
1219
+ const packageName = looksExternal ? externalPackageName(isExternalPath(resolved) ? resolved : dependency.module) : void 0;
1220
+ if (packageName) {
1221
+ externalPackages.add(packageName);
1222
+ target = `external:${packageName}`;
1223
+ }
1224
+ }
1225
+ if (target) edges.push({ from: `local:${source}`, to: target, dependency });
1226
+ }
1227
+ }
1228
+ edges.sort((a, b) => a.from.localeCompare(b.from) || a.to.localeCompare(b.to) || JSON.stringify(a.dependency).localeCompare(JSON.stringify(b.dependency)));
1229
+ const lines = [
1230
+ 'digraph "dependency-graph" {',
1231
+ ' graph [compound="true", rankdir="LR", fontname="Helvetica"];',
1232
+ ' node [fontname="Helvetica", fontsize="10"];',
1233
+ ' edge [fontname="Helvetica", fontsize="8"];',
1234
+ ...renderDirectory(root, [], " ")
1235
+ ];
1236
+ if (externalPackages.size > 0) {
1237
+ lines.push(' subgraph "cluster:external-packages" {', ' label="External packages";', ' style="dashed";', ' color="#64748b";');
1238
+ for (const packageName of [...externalPackages].sort()) {
1239
+ lines.push(` ${dotQuote(`external:${packageName}`)} [label=${dotQuote(packageName)}, shape="component", style="filled", fillcolor="#e2e8f0"];`);
1240
+ }
1241
+ lines.push(" }");
1242
+ }
1243
+ for (const edge of edges) {
1244
+ lines.push(` ${dotQuote(edge.from)} -> ${dotQuote(edge.to)}${edgeAttributes(edge.dependency)};`);
1245
+ }
1246
+ lines.push("}", "");
1247
+ return lines.join("\n");
1248
+ }
1249
+ function inferGraphvizFormat(outputPath) {
1250
+ const extension = path7.extname(outputPath).toLowerCase();
1251
+ if (extension === ".svg") return "svg";
1252
+ if (extension === ".dot") return "dot";
1253
+ throw new Error(`Unsupported graph output format "${extension || "(none)"}". Use an .svg or .dot output path.`);
1254
+ }
1255
+
1256
+ // src/cli/graph/render-graphviz.ts
1257
+ import { spawn } from "node:child_process";
1258
+ import * as fs3 from "node:fs/promises";
1259
+ import * as path8 from "node:path";
1260
+ function normalizeGraphvizSvg(svg) {
1261
+ return svg.replace(/<!-- Generated by graphviz version .*? -->/gi, "<!-- Generated by Graphviz -->");
1262
+ }
1263
+ async function renderDotWithGraphviz(dot, outputPath, options = {}) {
1264
+ const executable = options.executable ?? "dot";
1265
+ const svg = await new Promise((resolve7, reject) => {
1266
+ const child = spawn(executable, ["-Tsvg"], { stdio: ["pipe", "pipe", "pipe"] });
1267
+ const stdout = [];
1268
+ const stderr = [];
1269
+ child.stdout.on("data", (chunk) => stdout.push(chunk));
1270
+ child.stderr.on("data", (chunk) => stderr.push(chunk));
1271
+ child.on("error", (error) => {
1272
+ if (error.code === "ENOENT") {
1273
+ reject(new Error(`Graphviz executable "${executable}" was not found. Install Graphviz and ensure "dot" is available on PATH.`));
1274
+ } else {
1275
+ reject(new Error(`Unable to start Graphviz: ${error.message}`));
1276
+ }
1277
+ });
1278
+ child.on("close", (code) => {
1279
+ if (code === 0) resolve7(Buffer.concat(stdout).toString("utf8"));
1280
+ else reject(new Error(`Graphviz exited with code ${code}: ${Buffer.concat(stderr).toString("utf8").trim()}`));
1281
+ });
1282
+ child.stdin.end(dot);
1283
+ });
1284
+ await fs3.mkdir(path8.dirname(outputPath), { recursive: true });
1285
+ await fs3.writeFile(outputPath, normalizeGraphvizSvg(svg));
1286
+ }
1287
+
1288
+ // src/cli/commands/graph.ts
1289
+ async function runGraphCommand(args) {
1290
+ let values;
1291
+ try {
1292
+ values = parseArgs3({ args, options: {
1293
+ input: { type: "string", short: "i" },
1294
+ output: { type: "string", short: "o" },
1295
+ cwd: { type: "string" },
1296
+ help: { type: "boolean", short: "h" }
1297
+ } }).values;
1298
+ } catch (error) {
1299
+ console.error(`Error parsing arguments: ${error instanceof Error ? error.message : String(error)}`);
1300
+ return 2;
1301
+ }
1302
+ if (values.help) {
1303
+ console.log(`
1304
+ Usage: maritime graph --input <artifact-directory-or-graph.json> --output <graph.svg|graph.dot>
1305
+
1306
+ Renders existing canonical graph evidence without running dependency analysis.
1307
+
1308
+ Options:
1309
+ -i, --input <path> .maritime directory or dependency-graph.json (default: .maritime)
1310
+ -o, --output <path> SVG or DOT output path (required)
1311
+ --cwd <dir> Working directory root for resolution
1312
+ -h, --help Show help message
1313
+ `);
1314
+ return 0;
1315
+ }
1316
+ if (!values.output) {
1317
+ console.error("Error: --output is required.");
1318
+ return 2;
1319
+ }
1320
+ const cwd = path9.resolve(values.cwd ?? process.cwd());
1321
+ const input = path9.resolve(cwd, values.input ?? ".maritime");
1322
+ const output = path9.resolve(cwd, values.output);
1323
+ try {
1324
+ const stat3 = await fs4.stat(input);
1325
+ const graphPath = stat3.isDirectory() ? path9.resolve(input, (await validateArtifacts({ artifactDir: input, cwd })).manifest.artifacts.graph) : input;
1326
+ const parsed = JSON.parse(await fs4.readFile(graphPath, "utf8"));
1327
+ const result = CruiseResultSchema.safeParse(parsed);
1328
+ if (!result.success) throw new Error(`Invalid Maritime dependency graph: ${result.error.message}`);
1329
+ const dot = renderDependencyGraphToDot(result.data);
1330
+ const format = inferGraphvizFormat(output);
1331
+ await fs4.mkdir(path9.dirname(output), { recursive: true });
1332
+ if (format === "dot") await fs4.writeFile(output, dot);
1333
+ else await renderDotWithGraphviz(dot, output);
1334
+ console.log(`\u2705 Dependency graph rendered from ${graphPath} to ${output}`);
1335
+ return 0;
1336
+ } catch (error) {
1337
+ console.error(`Error rendering dependency graph: ${error instanceof Error ? error.message : String(error)}`);
1338
+ return 2;
1339
+ }
1340
+ }
1341
+
1133
1342
  // src/cli/main.ts
1134
1343
  import { fileURLToPath as fileURLToPath2 } from "node:url";
1135
- import * as fs3 from "node:fs";
1344
+ import * as fs5 from "node:fs";
1136
1345
  async function main() {
1137
- const { positionals } = parseArgs3({
1346
+ const { positionals } = parseArgs4({
1138
1347
  args: process.argv.slice(2),
1139
1348
  allowPositionals: true,
1140
1349
  strict: false
@@ -1148,9 +1357,12 @@ async function main() {
1148
1357
  const args = process.argv.slice(3);
1149
1358
  const exitCode = await runValidateCommand(args);
1150
1359
  process.exit(exitCode);
1360
+ } else if (command === "graph") {
1361
+ const exitCode = await runGraphCommand(process.argv.slice(3));
1362
+ process.exit(exitCode);
1151
1363
  } else {
1152
1364
  console.error(`Unknown command: ${command || "(none)"}`);
1153
- console.error("Available commands: analyze, validate");
1365
+ console.error("Available commands: analyze, validate, graph");
1154
1366
  process.exit(1);
1155
1367
  }
1156
1368
  }
@@ -1159,7 +1371,7 @@ if (process.argv[1]) {
1159
1371
  try {
1160
1372
  const currentPath = fileURLToPath2(import.meta.url);
1161
1373
  const execPath = process.argv[1];
1162
- isMain = currentPath === execPath || currentPath === fs3.realpathSync(execPath);
1374
+ isMain = currentPath === execPath || currentPath === fs5.realpathSync(execPath);
1163
1375
  } catch {
1164
1376
  isMain = false;
1165
1377
  }
@@ -178,3 +178,7 @@ export declare const CruiseResultSchema: z.ZodObject<{
178
178
  optionsUsed: z.ZodUnknown;
179
179
  }, z.core.$loose>;
180
180
  }, z.core.$loose>;
181
+ /** Canonical graph types guaranteed after Maritime schema validation. */
182
+ export type MaritimeDependency = z.infer<typeof DependencySchema>;
183
+ export type MaritimeModule = z.infer<typeof ModuleSchema>;
184
+ export type MaritimeCruiseResult = z.infer<typeof CruiseResultSchema>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dependency-maritime/cli",
3
- "version": "0.1.0-beta.1",
3
+ "version": "0.1.0-beta.3",
4
4
  "type": "module",
5
5
  "main": "./dist/cli/index.js",
6
6
  "types": "./dist/cli/index.d.ts",
@@ -39,13 +39,12 @@
39
39
  "lint": "eslint .",
40
40
  "depcruise": "depcruise src --config config/.dependency-cruiser.cjs",
41
41
  "check:arch": "depcruise src --config config/.dependency-cruiser.cjs",
42
- "generate:dot": "depcruise-fmt -T dot .maritime/dependency-graph.json > config/dependency-graph.dot",
43
- "generate:graph": "dot -T svg config/dependency-graph.dot > docs/images/dependency-graph.svg && dot -T png config/dependency-graph.dot > docs/images/dependency-graph.png",
42
+ "generate:graph": "node dist/cli/main.js graph --input .maritime --output docs/images/dependency-graph.svg",
44
43
  "preview": "vite preview",
45
44
  "test": "vitest",
46
45
  "test:unit": "vitest",
47
46
  "test:bench": "vitest bench -c vitest.bench.config.ts",
48
- "test:cli-package": "vitest run tests/cli-pack-smoke.test.ts",
47
+ "test:cli-package": "vitest run tests/cli-pack-smoke.test.ts tests/action-ref-resolution.test.ts",
49
48
  "test:e2e": "playwright test",
50
49
  "test:screenshots": "playwright test -c config/playwright.screenshots.config.ts"
51
50
  },