@lifesavertech/archguard-cli 2.0.2 → 2.2.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.d.ts CHANGED
@@ -5,7 +5,12 @@ interface CheckOptions {
5
5
  baselinePath?: string | null;
6
6
  updateBaselinePath?: string | null;
7
7
  projectRoot?: string | null;
8
+ configPath?: string | null;
8
9
  verbose?: boolean;
10
+ diff?: boolean;
11
+ diffBase?: string | null;
12
+ /** Test hook: explicit changed files for diff mode without git. */
13
+ changedFiles?: string[];
9
14
  }
10
15
  interface CheckSummary {
11
16
  mode: "baseline" | "check" | "update-baseline";
@@ -14,6 +19,11 @@ interface CheckSummary {
14
19
  violations: Violation[];
15
20
  suppressedViolationCount: number;
16
21
  baselinePath?: string | null;
22
+ diff?: {
23
+ baseRef: string;
24
+ changedFileCount: number;
25
+ affectedFileCount: number;
26
+ };
17
27
  }
18
28
  interface SourceFileAnalysis {
19
29
  filePath: string;
@@ -21,7 +31,10 @@ interface SourceFileAnalysis {
21
31
  ast: RuleContext["ast"];
22
32
  imports: ImportInfo[];
23
33
  }
24
- export declare function initializeArchguard(preset?: string): void;
34
+ export declare function initializeArchguard(options?: {
35
+ preset?: string;
36
+ detect?: boolean;
37
+ }): void;
25
38
  export declare function detectReactTypeScript(cwd: string): boolean;
26
39
  export declare function runChecks(options?: CheckOptions): CheckSummary;
27
40
  export declare function findTypeScriptFiles(dir: string): string[];
package/dist/index.js CHANGED
@@ -34,9 +34,6 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  return result;
35
35
  };
36
36
  })();
37
- var __importDefault = (this && this.__importDefault) || function (mod) {
38
- return (mod && mod.__esModule) ? mod : { "default": mod };
39
- };
40
37
  Object.defineProperty(exports, "__esModule", { value: true });
41
38
  exports.initializeArchguard = initializeArchguard;
42
39
  exports.detectReactTypeScript = detectReactTypeScript;
@@ -47,26 +44,40 @@ exports.resolveImportPath = resolveImportPath;
47
44
  const commander_1 = require("commander");
48
45
  const fs = __importStar(require("fs"));
49
46
  const path = __importStar(require("path"));
50
- const chalk_1 = __importDefault(require("chalk"));
51
47
  const archguard_core_1 = require("@lifesavertech/archguard-core");
52
48
  const archguard_rules_react_1 = require("@lifesavertech/archguard-rules-react");
53
- const archguard_guidance_1 = require("@lifesavertech/archguard-guidance");
54
49
  const presets_1 = require("./presets");
50
+ const detectStructure_1 = require("./init/detectStructure");
51
+ const generateConfig_1 = require("./init/generateConfig");
52
+ const syncGuidance_1 = require("./sync/syncGuidance");
53
+ const style = __importStar(require("./style"));
54
+ const MIN_NODE_MAJOR = 20;
55
+ const nodeMajor = Number(process.versions.node.split(".")[0] ?? "0");
56
+ if (nodeMajor < MIN_NODE_MAJOR) {
57
+ console.error(`Error: ArchGuard requires Node.js >=${MIN_NODE_MAJOR}. Current: ${process.versions.node}`);
58
+ process.exit(1);
59
+ }
55
60
  const program = new commander_1.Command();
56
61
  const DEFAULT_BASELINE_PATH = ".archguard-baseline.json";
57
62
  const SARIF_SCHEMA_URL = "https://json.schemastore.org/sarif-2.1.0.json";
58
- program.name("archguard").description("Architecture guard for React + TypeScript projects").version("2.0.1");
63
+ function readCliVersion() {
64
+ const packageJsonPath = path.join(__dirname, "..", "package.json");
65
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
66
+ return packageJson.version ?? "0.0.0";
67
+ }
68
+ program.name("archguard").description("Architecture guard for React + TypeScript projects").version(readCliVersion());
59
69
  program
60
70
  .command("init")
61
71
  .description("Initialize archguard in the current project")
62
72
  .option("--preset <preset>", `Architecture preset (${(0, presets_1.formatPresetList)()})`)
73
+ .option("--no-detect", "Use preset only; skip repository structure detection")
63
74
  .action((options) => {
64
75
  try {
65
- initializeArchguard(options.preset);
66
- console.log(chalk_1.default.green("✓ archguard initialized successfully"));
76
+ initializeArchguard({ preset: options.preset, detect: options.detect });
77
+ console.log(style.green("✓ archguard initialized successfully"));
67
78
  }
68
79
  catch (error) {
69
- console.error(chalk_1.default.red(`✗ Error: ${error instanceof Error ? error.message : String(error)}`));
80
+ console.error(style.red(`✗ Error: ${error instanceof Error ? error.message : String(error)}`));
70
81
  process.exit(1);
71
82
  }
72
83
  });
@@ -74,16 +85,21 @@ program
74
85
  .command("check")
75
86
  .description("Check project against architecture rules")
76
87
  .option("--baseline [path]", "Allow violations already captured in a baseline file")
77
- .option("--format <format>", "Output format (text, json, sarif)", "text")
88
+ .option("--config <path>", "Path to architecture.yaml (default: discover from scan root)")
89
+ .option("--diff", "Only report violations in changed files and their direct import neighbors")
90
+ .option("--diff-base <ref>", "Git ref for --diff (default: origin/main, main, or HEAD~1)")
91
+ .option("--format <format>", "Output format (text, json, sarif, github, agent)", "text")
92
+ .option("--json", "Emit machine-readable JSON (alias for --format json)")
78
93
  .option("--root <path>", "Project subdirectory to scan (for monorepos)")
79
94
  .option("--update-baseline [path]", "Write the current violations to a baseline file and exit")
80
95
  .option("--verbose", "Log unresolved imports and scan diagnostics")
81
96
  .action((options) => {
82
97
  try {
83
98
  const baselinePath = normalizeOptionalPath(options.baseline);
84
- const format = normalizeOutputFormat(options.format);
99
+ const format = resolveOutputFormat(options);
85
100
  const updateBaselinePath = normalizeOptionalPath(options.updateBaseline);
86
101
  const projectRoot = normalizeOptionalString(options.root);
102
+ const configPath = normalizeOptionalString(options.config);
87
103
  if (baselinePath && updateBaselinePath) {
88
104
  throw new Error("Use either --baseline or --update-baseline, not both.");
89
105
  }
@@ -91,7 +107,10 @@ program
91
107
  baselinePath,
92
108
  updateBaselinePath,
93
109
  projectRoot,
110
+ configPath,
94
111
  verbose: Boolean(options.verbose),
112
+ diff: Boolean(options.diff),
113
+ diffBase: normalizeOptionalString(options.diffBase),
95
114
  });
96
115
  printCheckSummary(result, format);
97
116
  if (!result.passed) {
@@ -99,14 +118,52 @@ program
99
118
  }
100
119
  }
101
120
  catch (error) {
102
- console.error(chalk_1.default.red(`✗ Error: ${error instanceof Error ? error.message : String(error)}`));
121
+ console.error(style.red(`✗ Error: ${error instanceof Error ? error.message : String(error)}`));
122
+ process.exit(1);
123
+ }
124
+ });
125
+ program
126
+ .command("sync")
127
+ .description("Regenerate AGENTS.md and .archguard guidance from architecture.yaml")
128
+ .option("--check", "Exit with error if guidance files are out of date")
129
+ .option("--config <path>", "Path to architecture.yaml (default: discover from cwd)")
130
+ .option("--force", "Overwrite existing agent rule files (AGENTS.md, .cursor/rules, copilot instructions)")
131
+ .option("--targets <list>", "Comma-separated targets: editor,agents,cursor,copilot", "editor,agents,cursor,copilot")
132
+ .option("--update-agents", "Overwrite AGENTS.md even when it already exists")
133
+ .action((options) => {
134
+ try {
135
+ const result = (0, syncGuidance_1.runGuidanceSync)(process.cwd(), {
136
+ configPath: normalizeOptionalString(options.config),
137
+ updateAgents: Boolean(options.updateAgents),
138
+ checkOnly: Boolean(options.check),
139
+ targets: (0, syncGuidance_1.parseGuidanceTargets)(options.targets),
140
+ force: Boolean(options.force),
141
+ });
142
+ if (options.check) {
143
+ if (result.updated.length > 0) {
144
+ console.error(style.red(`✗ Guidance is stale. Run 'archguard sync' to update:\n${result.updated.map((file) => ` - ${file}`).join("\n")}`));
145
+ process.exit(1);
146
+ }
147
+ console.log(style.green("✓ Guidance is up to date"));
148
+ return;
149
+ }
150
+ if (result.updated.length === 0) {
151
+ console.log(style.green("✓ Guidance already up to date"));
152
+ return;
153
+ }
154
+ for (const file of result.updated) {
155
+ console.log(style.green(`✓ Updated ${path.relative(process.cwd(), file) || file}`));
156
+ }
157
+ }
158
+ catch (error) {
159
+ console.error(style.red(`✗ Error: ${error instanceof Error ? error.message : String(error)}`));
103
160
  process.exit(1);
104
161
  }
105
162
  });
106
163
  if (require.main === module) {
107
164
  program.parse();
108
165
  }
109
- function initializeArchguard(preset) {
166
+ function initializeArchguard(options = {}) {
110
167
  const cwd = process.cwd();
111
168
  if (!detectReactTypeScript(cwd)) {
112
169
  throw new Error("React + TypeScript project not detected");
@@ -115,21 +172,22 @@ function initializeArchguard(preset) {
115
172
  if (fs.existsSync(architectureYamlPath)) {
116
173
  throw new Error("architecture.yaml already exists");
117
174
  }
118
- fs.writeFileSync(architectureYamlPath, (0, presets_1.getPresetConfig)(preset));
119
- const guidanceDir = path.join(cwd, ".archguard");
120
- if (!fs.existsSync(guidanceDir)) {
121
- fs.mkdirSync(guidanceDir, { recursive: true });
122
- }
123
- const editorGuidePath = path.join(guidanceDir, "ARCHITECTURE_RULES.md");
175
+ const configContents = resolveInitConfig(cwd, options);
176
+ fs.writeFileSync(architectureYamlPath, configContents);
124
177
  const architecture = (0, archguard_core_1.loadArchitectureConfig)(architectureYamlPath);
125
- const editorGuide = (0, archguard_guidance_1.generateEditorGuide)(architecture);
126
- fs.writeFileSync(editorGuidePath, editorGuide);
127
- const agentsPath = path.join(cwd, "AGENTS.md");
128
- if (!fs.existsSync(agentsPath)) {
129
- fs.writeFileSync(agentsPath, (0, archguard_guidance_1.generateAgentsGuide)(architecture));
130
- }
178
+ (0, syncGuidance_1.runGuidanceSync)(cwd, { configPath: architectureYamlPath, updateAgents: false });
131
179
  installPreCommitHook(cwd);
132
180
  }
181
+ function resolveInitConfig(cwd, options) {
182
+ if (options.detect === false) {
183
+ return (0, presets_1.getPresetConfig)(options.preset);
184
+ }
185
+ const detected = (0, detectStructure_1.detectStructure)(cwd);
186
+ if (detected) {
187
+ return (0, generateConfig_1.generateConfigFromStructure)(detected, options.preset);
188
+ }
189
+ return (0, presets_1.getPresetConfig)(options.preset);
190
+ }
133
191
  function detectReactTypeScript(cwd) {
134
192
  const packageJsonPath = path.join(cwd, "package.json");
135
193
  if (!fs.existsSync(packageJsonPath)) {
@@ -144,9 +202,16 @@ function detectReactTypeScript(cwd) {
144
202
  function runChecks(options = {}) {
145
203
  const cwd = process.cwd();
146
204
  const scanRoot = resolveScanRoot(cwd, options.projectRoot);
147
- const { violations: allViolations, scanSummary } = collectViolations(scanRoot, {
205
+ const applyDiffFilter = Boolean(options.diff) && !options.updateBaselinePath;
206
+ const { violations: rawViolations, scanSummary, diffMeta } = collectViolations(scanRoot, {
148
207
  verbose: options.verbose,
208
+ configPath: options.configPath,
209
+ diff: applyDiffFilter,
210
+ diffBase: options.diffBase,
211
+ changedFiles: options.changedFiles,
212
+ cwd,
149
213
  });
214
+ const allViolations = (0, archguard_core_1.enrichViolations)(rawViolations);
150
215
  if (options.verbose && scanSummary.unresolvedImports.length > 0) {
151
216
  printUnresolvedImports(scanSummary.unresolvedImports, scanRoot);
152
217
  }
@@ -161,44 +226,45 @@ function runChecks(options = {}) {
161
226
  baselinePath: options.updateBaselinePath,
162
227
  };
163
228
  }
229
+ const withDiff = (summary) => diffMeta ? { ...summary, diff: diffMeta } : summary;
164
230
  if (options.baselinePath) {
165
231
  const baselineViolations = loadBaseline(options.baselinePath);
166
232
  const { newViolations, suppressedViolationCount } = filterViolationsAgainstBaseline(allViolations, baselineViolations, scanRoot);
167
233
  if (newViolations.length > 0) {
168
- return {
234
+ return withDiff({
169
235
  mode: "baseline",
170
236
  passed: false,
171
237
  violationCount: newViolations.length,
172
238
  violations: newViolations,
173
239
  suppressedViolationCount,
174
240
  baselinePath: options.baselinePath,
175
- };
241
+ });
176
242
  }
177
- return {
243
+ return withDiff({
178
244
  mode: "baseline",
179
245
  passed: true,
180
246
  violationCount: 0,
181
247
  violations: [],
182
248
  suppressedViolationCount,
183
249
  baselinePath: options.baselinePath,
184
- };
250
+ });
185
251
  }
186
252
  if (allViolations.length > 0) {
187
- return {
253
+ return withDiff({
188
254
  mode: "check",
189
255
  passed: false,
190
256
  violationCount: allViolations.length,
191
257
  violations: allViolations,
192
258
  suppressedViolationCount: 0,
193
- };
259
+ });
194
260
  }
195
- return {
261
+ return withDiff({
196
262
  mode: "check",
197
263
  passed: true,
198
264
  violationCount: 0,
199
265
  violations: [],
200
266
  suppressedViolationCount: 0,
201
- };
267
+ });
202
268
  }
203
269
  function findTypeScriptFiles(dir) {
204
270
  const files = [];
@@ -270,7 +336,7 @@ function installPreCommitHook(cwd) {
270
336
  }
271
337
  const preCommitPath = path.join(gitHooksDir, "pre-commit");
272
338
  if (fs.existsSync(preCommitPath)) {
273
- console.log(chalk_1.default.yellow("! Existing pre-commit hook detected. ArchGuard did not overwrite it; add the check command manually."));
339
+ console.log(style.yellow("! Existing pre-commit hook detected. ArchGuard did not overwrite it; add the check command manually."));
274
340
  return;
275
341
  }
276
342
  // Use the installed CLI name so hooks keep working after npm install or global linking.
@@ -281,19 +347,32 @@ npx archguard check || exit 1
281
347
  fs.writeFileSync(preCommitPath, preCommitHook);
282
348
  fs.chmodSync(preCommitPath, 0o755);
283
349
  }
284
- function collectViolations(scanRoot, options = {}) {
285
- const configPath = (0, archguard_core_1.findArchitectureConfig)(scanRoot);
286
- if (!configPath) {
287
- throw new Error("architecture.yaml not found. Run 'archguard init' first.");
350
+ function assertLayerCoverage(sourceFiles, architecture, scanRoot) {
351
+ const layerRulesEnabled = (0, archguard_core_1.isLayerBoundaryEnforcementEnabled)(architecture.rules) ||
352
+ Boolean(architecture.rules.noCircularLayerDeps);
353
+ if (!layerRulesEnabled || sourceFiles.length === 0) {
354
+ return;
288
355
  }
356
+ const matchedFiles = (0, archguard_core_1.countFilesMatchingLayers)(sourceFiles, architecture, scanRoot);
357
+ if (matchedFiles > 0) {
358
+ return;
359
+ }
360
+ throw new Error(`No scanned files matched any configured layer — boundary rules enforced nothing. ` +
361
+ `Map your folders with patterns.layers or layers.*.paths in architecture.yaml ` +
362
+ `or align folder names with layer keys. Scanned ${sourceFiles.length} file(s) under ${scanRoot}.`);
363
+ }
364
+ function collectViolations(scanRoot, options = {}) {
365
+ const configPath = resolveConfigPath(scanRoot, options.configPath);
289
366
  const architecture = (0, archguard_core_1.loadArchitectureConfig)(configPath);
290
367
  const engine = new archguard_core_1.RuleEngine();
291
368
  const unresolvedImports = [];
292
369
  engine.registerRule((0, archguard_rules_react_1.createLayerImportBoundaryRule)());
370
+ engine.registerRule((0, archguard_rules_react_1.createBarrelBoundaryBypassRule)());
293
371
  engine.registerRule((0, archguard_rules_react_1.createBusinessLogicInComponentsRule)());
294
372
  engine.registerRule((0, archguard_rules_react_1.createDataFetchingInUIRule)());
295
373
  engine.registerRule((0, archguard_rules_react_1.createCircularDependencyRule)());
296
- const sourceFiles = findTypeScriptFiles(scanRoot);
374
+ const sourceFiles = findTypeScriptFiles(scanRoot).filter((filePath) => !(0, archguard_core_1.shouldIgnorePath)(filePath, architecture, scanRoot));
375
+ assertLayerCoverage(sourceFiles, architecture, scanRoot);
297
376
  const importResolver = (0, archguard_core_1.createImportResolver)(scanRoot);
298
377
  const sourceFileAnalyses = analyzeSourceFiles(sourceFiles, importResolver, unresolvedImports);
299
378
  const importGraph = buildImportGraph(sourceFileAnalyses);
@@ -312,19 +391,40 @@ function collectViolations(scanRoot, options = {}) {
312
391
  allViolations.push(...result.violations);
313
392
  }
314
393
  if (options.verbose) {
315
- console.error(chalk_1.default.gray(`ArchGuard scanned ${sourceFiles.length} file(s) from ${path.relative(scanRoot, scanRoot) || "."} using ${normalizeViolationPath(configPath, scanRoot)}`));
394
+ console.error(style.gray(`ArchGuard scanned ${sourceFiles.length} file(s) from ${path.relative(scanRoot, scanRoot) || "."} using ${normalizeViolationPath(configPath, scanRoot)}`));
395
+ }
396
+ let violations = sortViolations(allViolations.map((violation) => ({
397
+ ...violation,
398
+ file: normalizeViolationPath(violation.file, scanRoot),
399
+ })));
400
+ let diffMeta;
401
+ if (options.diff) {
402
+ const { changedFiles, affectedFiles } = (0, archguard_core_1.resolveDiffAffectedFiles)({
403
+ cwd: options.cwd ?? process.cwd(),
404
+ scanRoot,
405
+ baseRef: options.diffBase ?? undefined,
406
+ changedFiles: options.changedFiles,
407
+ }, importGraph);
408
+ const normalizedAffected = new Set([...affectedFiles].map((filePath) => normalizeViolationPath(filePath, scanRoot)));
409
+ violations = (0, archguard_core_1.filterViolationsForDiff)(violations, normalizedAffected);
410
+ diffMeta = {
411
+ baseRef: options.diffBase ?? "auto",
412
+ changedFileCount: changedFiles.length,
413
+ affectedFileCount: normalizedAffected.size,
414
+ };
415
+ if (options.verbose) {
416
+ console.error(style.gray(`Diff mode: ${changedFiles.length} changed file(s), ${normalizedAffected.size} affected file(s)`));
417
+ }
316
418
  }
317
419
  return {
318
- violations: sortViolations(allViolations.map((violation) => ({
319
- ...violation,
320
- file: normalizeViolationPath(violation.file, scanRoot),
321
- }))),
420
+ violations,
322
421
  scanSummary: { unresolvedImports },
422
+ diffMeta,
323
423
  };
324
424
  }
325
425
  function printViolations(violations) {
326
426
  for (const violation of violations) {
327
- console.log(chalk_1.default.yellow(`${violation.file}:${violation.line}:${violation.column}`) +
427
+ console.log(style.yellow(`${violation.file}:${violation.line}:${violation.column}`) +
328
428
  ` - ${violation.message} (${violation.rule})`);
329
429
  }
330
430
  }
@@ -333,29 +433,37 @@ function printCheckSummary(summary, format) {
333
433
  console.log(JSON.stringify(buildJsonSummary(summary), null, 2));
334
434
  return;
335
435
  }
436
+ if (format === "agent") {
437
+ console.log(JSON.stringify(buildAgentSummary(summary), null, 2));
438
+ return;
439
+ }
336
440
  if (format === "sarif") {
337
441
  console.log(JSON.stringify(buildSarifReport(summary), null, 2));
338
442
  return;
339
443
  }
444
+ if (format === "github") {
445
+ printGithubAnnotations(summary);
446
+ return;
447
+ }
340
448
  if (summary.mode === "update-baseline") {
341
- console.log(chalk_1.default.green(`✓ Wrote ${summary.violations.length} violation(s) to baseline ${summary.baselinePath}`));
449
+ console.log(style.green(`✓ Wrote ${summary.violations.length} violation(s) to baseline ${summary.baselinePath}`));
342
450
  return;
343
451
  }
344
452
  if (summary.mode === "baseline" && !summary.passed) {
345
- console.log(chalk_1.default.red(`\n✗ Found ${summary.violationCount} new violation(s) (${summary.suppressedViolationCount} matched baseline):\n`));
453
+ console.log(style.red(`\n✗ Found ${summary.violationCount} new violation(s) (${summary.suppressedViolationCount} matched baseline):\n`));
346
454
  printViolations(summary.violations);
347
455
  return;
348
456
  }
349
457
  if (summary.mode === "baseline" && summary.suppressedViolationCount > 0) {
350
- console.log(chalk_1.default.green(`✓ No new violations. ${summary.suppressedViolationCount} violation(s) matched baseline ${summary.baselinePath}`));
458
+ console.log(style.green(`✓ No new violations. ${summary.suppressedViolationCount} violation(s) matched baseline ${summary.baselinePath}`));
351
459
  return;
352
460
  }
353
461
  if (!summary.passed) {
354
- console.log(chalk_1.default.red(`\n✗ Found ${summary.violationCount} violation(s):\n`));
462
+ console.log(style.red(`\n✗ Found ${summary.violationCount} violation(s):\n`));
355
463
  printViolations(summary.violations);
356
464
  return;
357
465
  }
358
- console.log(chalk_1.default.green("✓ All checks passed"));
466
+ console.log(style.green("✓ All checks passed"));
359
467
  }
360
468
  function normalizeOptionalString(optionValue) {
361
469
  if (optionValue === undefined) {
@@ -375,9 +483,9 @@ function resolveScanRoot(cwd, projectRoot) {
375
483
  return scanRoot;
376
484
  }
377
485
  function printUnresolvedImports(unresolvedImports, scanRoot) {
378
- console.error(chalk_1.default.yellow(`\nUnresolved imports (${unresolvedImports.length}):`));
486
+ console.error(style.yellow(`\nUnresolved imports (${unresolvedImports.length}):`));
379
487
  for (const unresolvedImport of unresolvedImports) {
380
- console.error(chalk_1.default.yellow(` ${normalizeViolationPath(unresolvedImport.file, scanRoot)} -> ${unresolvedImport.importPath}`));
488
+ console.error(style.yellow(` ${normalizeViolationPath(unresolvedImport.file, scanRoot)} -> ${unresolvedImport.importPath}`));
381
489
  }
382
490
  }
383
491
  function normalizeOptionalPath(optionValue) {
@@ -392,13 +500,40 @@ function normalizeOptionalPath(optionValue) {
392
500
  }
393
501
  function normalizeOutputFormat(format) {
394
502
  const normalizedFormat = (format ?? "text").trim().toLowerCase();
395
- if (normalizedFormat === "json" || normalizedFormat === "sarif" || normalizedFormat === "text") {
503
+ if (normalizedFormat === "agent" ||
504
+ normalizedFormat === "github" ||
505
+ normalizedFormat === "json" ||
506
+ normalizedFormat === "sarif" ||
507
+ normalizedFormat === "text") {
396
508
  return normalizedFormat;
397
509
  }
398
- throw new Error(`Unsupported output format: ${format}. Expected 'text', 'json', or 'sarif'.`);
510
+ throw new Error(`Unsupported output format: ${format}. Expected 'text', 'json', 'sarif', 'github', or 'agent'.`);
511
+ }
512
+ function resolveOutputFormat(options) {
513
+ if (options.json) {
514
+ return "json";
515
+ }
516
+ return normalizeOutputFormat(options.format);
517
+ }
518
+ function resolveConfigPath(scanRoot, configPath) {
519
+ if (configPath) {
520
+ const resolvedPath = path.isAbsolute(configPath)
521
+ ? configPath
522
+ : path.resolve(scanRoot, configPath);
523
+ if (!fs.existsSync(resolvedPath)) {
524
+ throw new Error(`Architecture config not found: ${configPath}`);
525
+ }
526
+ return resolvedPath;
527
+ }
528
+ const discoveredPath = (0, archguard_core_1.findArchitectureConfig)(scanRoot);
529
+ if (!discoveredPath) {
530
+ throw new Error("architecture.yaml not found. Run 'archguard init' first.");
531
+ }
532
+ return discoveredPath;
399
533
  }
400
534
  function buildJsonSummary(summary) {
401
535
  return {
536
+ schemaVersion: 2,
402
537
  status: summary.passed ? "passed" : "failed",
403
538
  mode: summary.mode,
404
539
  passed: summary.passed,
@@ -406,8 +541,60 @@ function buildJsonSummary(summary) {
406
541
  suppressedViolationCount: summary.suppressedViolationCount,
407
542
  baselinePath: summary.baselinePath ?? null,
408
543
  violations: summary.violations,
544
+ diff: summary.diff,
409
545
  };
410
546
  }
547
+ function buildAgentSummary(summary) {
548
+ const violations = summary.violations.map((violation) => ({
549
+ id: violation.id ?? (0, archguard_core_1.createViolationId)(violation),
550
+ file: violation.file,
551
+ line: violation.line,
552
+ column: violation.column,
553
+ rule: violation.rule,
554
+ message: truncateAgentText(violation.message, 240),
555
+ fix: violation.fix ? truncateAgentText(violation.fix, 240) : undefined,
556
+ context: violation.context,
557
+ }));
558
+ return {
559
+ passed: summary.passed,
560
+ violationCount: summary.violationCount,
561
+ summary: summary.passed
562
+ ? "All architecture checks passed."
563
+ : `Found ${summary.violationCount} violation(s). Fix layer imports or update architecture.yaml.`,
564
+ violations,
565
+ };
566
+ }
567
+ function truncateAgentText(value, maxLength) {
568
+ if (value.length <= maxLength) {
569
+ return value;
570
+ }
571
+ return `${value.slice(0, maxLength - 3)}...`;
572
+ }
573
+ function printGithubAnnotations(summary) {
574
+ if (summary.mode === "update-baseline") {
575
+ console.log(`::notice title=ArchGuard::Wrote ${summary.violations.length} violation(s) to baseline ${summary.baselinePath}`);
576
+ return;
577
+ }
578
+ if (summary.passed) {
579
+ if (summary.mode === "baseline" && summary.suppressedViolationCount > 0) {
580
+ console.log(`::notice title=ArchGuard::No new violations (${summary.suppressedViolationCount} matched baseline).`);
581
+ return;
582
+ }
583
+ console.log("::notice title=ArchGuard::All checks passed");
584
+ return;
585
+ }
586
+ for (const violation of summary.violations) {
587
+ const title = violation.rule;
588
+ const message = escapeGithubMessage(`${violation.message} (${violation.rule})`);
589
+ console.log(`::error file=${violation.file},line=${violation.line},col=${violation.column},title=${escapeGithubMessage(title)}::${message}`);
590
+ }
591
+ }
592
+ function escapeGithubMessage(value) {
593
+ return value
594
+ .replace(/%/g, "%25")
595
+ .replace(/\r/g, "%0D")
596
+ .replace(/\n/g, "%0A");
597
+ }
411
598
  function buildSarifReport(summary) {
412
599
  const ruleDescriptions = new Map([
413
600
  ["layer-import-boundary", "Import violates a configured layer boundary."],
@@ -557,5 +744,10 @@ function isViolation(value) {
557
744
  typeof violation.line === "number" &&
558
745
  typeof violation.column === "number" &&
559
746
  typeof violation.message === "string" &&
560
- typeof violation.rule === "string");
747
+ typeof violation.rule === "string" &&
748
+ (violation.id === undefined || typeof violation.id === "string") &&
749
+ (violation.severity === undefined ||
750
+ violation.severity === "error" ||
751
+ violation.severity === "warning") &&
752
+ (violation.fix === undefined || typeof violation.fix === "string"));
561
753
  }
@@ -0,0 +1,12 @@
1
+ /** Scan repository folders to infer layer layout for smart init. */
2
+ export interface DetectedLayer {
3
+ name: string;
4
+ include: string[];
5
+ }
6
+ export interface DetectedStructure {
7
+ sourceRoot: string;
8
+ layers: DetectedLayer[];
9
+ uiLayerNames: string[];
10
+ style: "custom" | "fsd" | "layered";
11
+ }
12
+ export declare function detectStructure(projectRoot: string): DetectedStructure | null;
@@ -0,0 +1,163 @@
1
+ "use strict";
2
+ /** Scan repository folders to infer layer layout for smart init. */
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ exports.detectStructure = detectStructure;
38
+ const fs = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
40
+ const SOURCE_ROOT_CANDIDATES = ["src", "app"];
41
+ const FOLDER_LAYER_MAP = {
42
+ app: "app",
43
+ common: "shared",
44
+ components: "ui",
45
+ database: "db",
46
+ data: "db",
47
+ db: "db",
48
+ domain: "domain",
49
+ entities: "entities",
50
+ features: "features",
51
+ hooks: "features",
52
+ lib: "shared",
53
+ libs: "shared",
54
+ models: "domain",
55
+ modules: "features",
56
+ pages: "ui",
57
+ repositories: "db",
58
+ services: "domain",
59
+ shared: "shared",
60
+ ui: "ui",
61
+ utils: "shared",
62
+ views: "ui",
63
+ widgets: "widgets",
64
+ };
65
+ const UI_LAYER_CANDIDATES = new Set(["ui", "app", "pages", "widgets"]);
66
+ function detectStructure(projectRoot) {
67
+ const sourceRoot = findSourceRoot(projectRoot);
68
+ if (!sourceRoot) {
69
+ return null;
70
+ }
71
+ const layerFolders = collectLayerFolders(projectRoot, sourceRoot);
72
+ if (layerFolders.length < 2) {
73
+ return null;
74
+ }
75
+ const grouped = groupFoldersByLayer(layerFolders);
76
+ if (Object.keys(grouped).length < 2) {
77
+ return null;
78
+ }
79
+ const layers = Object.entries(grouped).map(([name, folders]) => ({
80
+ name,
81
+ include: folders.map((folder) => `${folder}/**`),
82
+ }));
83
+ const style = inferStyle(layers.map((layer) => layer.name));
84
+ const uiLayerNames = layers
85
+ .map((layer) => layer.name)
86
+ .filter((name) => UI_LAYER_CANDIDATES.has(name));
87
+ return {
88
+ sourceRoot,
89
+ layers,
90
+ uiLayerNames,
91
+ style,
92
+ };
93
+ }
94
+ function findSourceRoot(projectRoot) {
95
+ for (const candidate of SOURCE_ROOT_CANDIDATES) {
96
+ const candidatePath = path.join(projectRoot, candidate);
97
+ if (fs.existsSync(candidatePath) && fs.statSync(candidatePath).isDirectory()) {
98
+ return candidate;
99
+ }
100
+ }
101
+ return null;
102
+ }
103
+ function collectLayerFolders(projectRoot, sourceRoot) {
104
+ const sourcePath = path.join(projectRoot, sourceRoot);
105
+ const entries = fs.readdirSync(sourcePath, { withFileTypes: true });
106
+ const layerFolders = [];
107
+ for (const entry of entries) {
108
+ if (!entry.isDirectory()) {
109
+ continue;
110
+ }
111
+ const folderName = entry.name;
112
+ if (!FOLDER_LAYER_MAP[folderName]) {
113
+ continue;
114
+ }
115
+ const folderPath = path.join(sourcePath, folderName);
116
+ if (containsTypeScriptFiles(folderPath)) {
117
+ layerFolders.push(`${sourceRoot}/${folderName}`);
118
+ }
119
+ }
120
+ return layerFolders;
121
+ }
122
+ function containsTypeScriptFiles(dir) {
123
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
124
+ for (const entry of entries) {
125
+ const fullPath = path.join(dir, entry.name);
126
+ if (entry.isDirectory()) {
127
+ if (containsTypeScriptFiles(fullPath)) {
128
+ return true;
129
+ }
130
+ continue;
131
+ }
132
+ if (entry.isFile() && (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx"))) {
133
+ return true;
134
+ }
135
+ }
136
+ return false;
137
+ }
138
+ function groupFoldersByLayer(folders) {
139
+ const grouped = {};
140
+ for (const folder of folders) {
141
+ const segment = folder.split("/").pop();
142
+ if (!segment) {
143
+ continue;
144
+ }
145
+ const layerName = FOLDER_LAYER_MAP[segment];
146
+ if (!layerName) {
147
+ continue;
148
+ }
149
+ grouped[layerName] ?? (grouped[layerName] = []);
150
+ grouped[layerName].push(folder);
151
+ }
152
+ return grouped;
153
+ }
154
+ function inferStyle(layerNames) {
155
+ const names = new Set(layerNames);
156
+ if (names.has("entities") && names.has("features") && (names.has("pages") || names.has("app"))) {
157
+ return "fsd";
158
+ }
159
+ if (names.has("domain") && names.has("features") && names.has("ui")) {
160
+ return "layered";
161
+ }
162
+ return "custom";
163
+ }
@@ -0,0 +1,3 @@
1
+ /** Build architecture.yaml from detected structure and optional preset rules. */
2
+ import { DetectedStructure } from "./detectStructure";
3
+ export declare function generateConfigFromStructure(structure: DetectedStructure, preset?: string): string;
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ /** Build architecture.yaml from detected structure and optional preset rules. */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.generateConfigFromStructure = generateConfigFromStructure;
5
+ const PRESET_RULES = {
6
+ "boundaries-only": {
7
+ enforceLayerBoundaries: true,
8
+ noCircularLayerDeps: true,
9
+ noBusinessLogicInComponents: false,
10
+ noDataFetchingInUI: false,
11
+ },
12
+ "react-layered": {
13
+ enforceLayerBoundaries: true,
14
+ noCircularLayerDeps: true,
15
+ noBusinessLogicInComponents: true,
16
+ noDataFetchingInUI: true,
17
+ uiLayers: ["ui"],
18
+ },
19
+ "feature-sliced-react": {
20
+ enforceLayerBoundaries: true,
21
+ noCircularLayerDeps: true,
22
+ noBusinessLogicInComponents: true,
23
+ noDataFetchingInUI: true,
24
+ uiLayers: ["app", "pages"],
25
+ },
26
+ };
27
+ function generateConfigFromStructure(structure, preset) {
28
+ const layerNames = structure.layers.map((layer) => layer.name);
29
+ const rules = resolveRules(structure, preset);
30
+ const allowedImports = buildAllowedImports(layerNames, structure.style);
31
+ const lines = [
32
+ "patterns:",
33
+ ` root: ${structure.sourceRoot}`,
34
+ " ignore:",
35
+ ' - "**/*.test.{ts,tsx}"',
36
+ ' - "**/*.spec.{ts,tsx}"',
37
+ ' - "**/__tests__/**"',
38
+ " layers:",
39
+ ];
40
+ for (const layer of structure.layers) {
41
+ lines.push(` ${layer.name}:`);
42
+ lines.push(" include:");
43
+ for (const include of layer.include) {
44
+ lines.push(` - ${include}`);
45
+ }
46
+ }
47
+ lines.push("");
48
+ lines.push("layers:");
49
+ for (const layerName of layerNames) {
50
+ const imports = allowedImports[layerName] ?? [];
51
+ lines.push(` ${layerName}:`);
52
+ if (imports.length > 0) {
53
+ lines.push(` allowedImports: [${imports.join(", ")}]`);
54
+ }
55
+ else {
56
+ lines.push(" allowedImports: []");
57
+ }
58
+ }
59
+ if (rules.uiLayers && rules.uiLayers.length > 0) {
60
+ lines.push("");
61
+ lines.push(`uiLayers: [${rules.uiLayers.join(", ")}]`);
62
+ }
63
+ else if (structure.uiLayerNames.length > 0 && rules.noBusinessLogicInComponents) {
64
+ lines.push("");
65
+ lines.push(`uiLayers: [${structure.uiLayerNames.join(", ")}]`);
66
+ }
67
+ lines.push("");
68
+ lines.push("rules:");
69
+ lines.push(` enforceLayerBoundaries: ${rules.enforceLayerBoundaries}`);
70
+ lines.push(` noCircularLayerDeps: ${rules.noCircularLayerDeps}`);
71
+ lines.push(` noBusinessLogicInComponents: ${rules.noBusinessLogicInComponents}`);
72
+ lines.push(` noDataFetchingInUI: ${rules.noDataFetchingInUI}`);
73
+ lines.push("");
74
+ return lines.join("\n");
75
+ }
76
+ function resolveRules(structure, preset) {
77
+ if (preset && preset in PRESET_RULES) {
78
+ return PRESET_RULES[preset];
79
+ }
80
+ if (structure.style === "fsd") {
81
+ return PRESET_RULES["feature-sliced-react"];
82
+ }
83
+ if (structure.style === "layered") {
84
+ return PRESET_RULES["react-layered"];
85
+ }
86
+ return PRESET_RULES["boundaries-only"];
87
+ }
88
+ function buildAllowedImports(layerNames, style) {
89
+ const names = new Set(layerNames);
90
+ if (style === "fsd") {
91
+ return {
92
+ app: pickExisting(names, ["pages", "features", "entities", "shared"]),
93
+ pages: pickExisting(names, ["features", "entities", "shared"]),
94
+ features: pickExisting(names, ["entities", "shared"]),
95
+ entities: pickExisting(names, ["shared"]),
96
+ shared: [],
97
+ widgets: pickExisting(names, ["entities", "shared", "features"]),
98
+ ui: pickExisting(names, ["features", "shared"]),
99
+ db: [],
100
+ domain: [],
101
+ };
102
+ }
103
+ if (style === "layered") {
104
+ return {
105
+ domain: [],
106
+ features: pickExisting(names, ["domain", "shared"]),
107
+ ui: pickExisting(names, ["features", "shared"]),
108
+ shared: [],
109
+ db: [],
110
+ app: pickExisting(names, ["features", "shared"]),
111
+ pages: pickExisting(names, ["features", "shared"]),
112
+ };
113
+ }
114
+ const imports = {};
115
+ const lowerLayers = new Set(["domain", "db", "data", "entities", "shared"]);
116
+ for (const layerName of layerNames) {
117
+ if (lowerLayers.has(layerName)) {
118
+ imports[layerName] = layerName === "shared" ? [] : pickExisting(names, ["shared"]);
119
+ continue;
120
+ }
121
+ imports[layerName] = pickExisting(names, ["features", "domain", "db", "shared", "entities"]);
122
+ }
123
+ return imports;
124
+ }
125
+ function pickExisting(available, candidates) {
126
+ return candidates.filter((candidate) => available.has(candidate));
127
+ }
@@ -15,8 +15,17 @@ const BOUNDARIES_ONLY_CONFIG = `layers:
15
15
  shared:
16
16
  allowedImports: []
17
17
 
18
+ # Map custom folder layouts with paths (relative to project root):
19
+ # ui:
20
+ # paths: [src/components, src/pages]
21
+ # allowedImports: [features, db]
22
+ # db:
23
+ # paths: [src/db]
24
+ # allowedImports: []
25
+
18
26
  rules:
19
27
  enforceLayerBoundaries: true
28
+ noBarrelBoundaryBypass: true
20
29
  noCircularLayerDeps: true
21
30
  noBusinessLogicInComponents: false
22
31
  noDataFetchingInUI: false
@@ -35,6 +44,7 @@ uiLayers: [ui]
35
44
 
36
45
  rules:
37
46
  enforceLayerBoundaries: true
47
+ noBarrelBoundaryBypass: true
38
48
  noCircularLayerDeps: true
39
49
  noBusinessLogicInComponents: true
40
50
  noDataFetchingInUI: true
@@ -55,6 +65,7 @@ uiLayers: [app, pages]
55
65
 
56
66
  rules:
57
67
  enforceLayerBoundaries: true
68
+ noBarrelBoundaryBypass: true
58
69
  noCircularLayerDeps: true
59
70
  noBusinessLogicInComponents: true
60
71
  noDataFetchingInUI: true
@@ -0,0 +1,5 @@
1
+ /** Terminal styling via Node built-ins (no ESM-only deps in CJS output). */
2
+ export declare function green(text: string): string;
3
+ export declare function red(text: string): string;
4
+ export declare function yellow(text: string): string;
5
+ export declare function gray(text: string): string;
package/dist/style.js ADDED
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ /** Terminal styling via Node built-ins (no ESM-only deps in CJS output). */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.green = green;
5
+ exports.red = red;
6
+ exports.yellow = yellow;
7
+ exports.gray = gray;
8
+ const node_util_1 = require("node:util");
9
+ function green(text) {
10
+ return (0, node_util_1.styleText)("green", text);
11
+ }
12
+ function red(text) {
13
+ return (0, node_util_1.styleText)("red", text);
14
+ }
15
+ function yellow(text) {
16
+ return (0, node_util_1.styleText)("yellow", text);
17
+ }
18
+ function gray(text) {
19
+ return (0, node_util_1.styleText)("gray", text);
20
+ }
@@ -0,0 +1,33 @@
1
+ /** Regenerate guidance files from architecture.yaml. */
2
+ import { ArchitectureConfig } from "@lifesavertech/archguard-core";
3
+ export type GuidanceTarget = "agents" | "copilot" | "cursor" | "editor";
4
+ export declare const DEFAULT_GUIDANCE_TARGETS: GuidanceTarget[];
5
+ export interface SyncTarget {
6
+ path: string;
7
+ content: string;
8
+ name: GuidanceTarget;
9
+ }
10
+ export interface SyncPlan {
11
+ configPath: string;
12
+ architecture: ArchitectureConfig;
13
+ targets: SyncTarget[];
14
+ }
15
+ export interface SyncResult {
16
+ configPath: string;
17
+ updated: string[];
18
+ unchanged: string[];
19
+ }
20
+ export declare function parseGuidanceTargets(value: string | undefined): GuidanceTarget[];
21
+ export declare function planGuidanceSync(cwd: string, options?: {
22
+ configPath?: string | null;
23
+ updateAgents?: boolean;
24
+ targets?: GuidanceTarget[];
25
+ force?: boolean;
26
+ }): SyncPlan;
27
+ export declare function runGuidanceSync(cwd: string, options?: {
28
+ configPath?: string | null;
29
+ updateAgents?: boolean;
30
+ checkOnly?: boolean;
31
+ targets?: GuidanceTarget[];
32
+ force?: boolean;
33
+ }): SyncResult;
@@ -0,0 +1,139 @@
1
+ "use strict";
2
+ /** Regenerate guidance files from architecture.yaml. */
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ exports.DEFAULT_GUIDANCE_TARGETS = void 0;
38
+ exports.parseGuidanceTargets = parseGuidanceTargets;
39
+ exports.planGuidanceSync = planGuidanceSync;
40
+ exports.runGuidanceSync = runGuidanceSync;
41
+ const fs = __importStar(require("fs"));
42
+ const path = __importStar(require("path"));
43
+ const archguard_core_1 = require("@lifesavertech/archguard-core");
44
+ const archguard_guidance_1 = require("@lifesavertech/archguard-guidance");
45
+ exports.DEFAULT_GUIDANCE_TARGETS = ["editor", "agents", "cursor", "copilot"];
46
+ function parseGuidanceTargets(value) {
47
+ if (!value || value.trim().length === 0) {
48
+ return [...exports.DEFAULT_GUIDANCE_TARGETS];
49
+ }
50
+ const allowed = new Set(exports.DEFAULT_GUIDANCE_TARGETS);
51
+ const parsed = value
52
+ .split(",")
53
+ .map((part) => part.trim())
54
+ .filter(Boolean);
55
+ for (const target of parsed) {
56
+ if (!allowed.has(target)) {
57
+ throw new Error(`Unknown sync target "${target}". Use: ${exports.DEFAULT_GUIDANCE_TARGETS.join(", ")}`);
58
+ }
59
+ }
60
+ return parsed;
61
+ }
62
+ function planGuidanceSync(cwd, options = {}) {
63
+ const configPath = resolveConfigPath(cwd, options.configPath);
64
+ const architecture = (0, archguard_core_1.loadArchitectureConfig)(configPath);
65
+ const selectedTargets = options.targets ?? exports.DEFAULT_GUIDANCE_TARGETS;
66
+ const targets = [];
67
+ if (selectedTargets.includes("editor")) {
68
+ targets.push({
69
+ name: "editor",
70
+ path: path.join(cwd, ".archguard", "ARCHITECTURE_RULES.md"),
71
+ content: `${(0, archguard_guidance_1.generateEditorGuide)(architecture)}\n`,
72
+ });
73
+ }
74
+ const agentsPath = path.join(cwd, "AGENTS.md");
75
+ if (selectedTargets.includes("agents") && (options.force || options.updateAgents || !fs.existsSync(agentsPath))) {
76
+ targets.push({
77
+ name: "agents",
78
+ path: agentsPath,
79
+ content: `${(0, archguard_guidance_1.generateAgentsGuide)(architecture)}\n`,
80
+ });
81
+ }
82
+ const cursorPath = path.join(cwd, ".cursor", "rules", "archguard.mdc");
83
+ if (selectedTargets.includes("cursor") && (options.force || !fs.existsSync(cursorPath))) {
84
+ targets.push({
85
+ name: "cursor",
86
+ path: cursorPath,
87
+ content: `${(0, archguard_guidance_1.generateCursorRules)(architecture)}\n`,
88
+ });
89
+ }
90
+ const copilotPath = path.join(cwd, ".github", "copilot-instructions.md");
91
+ if (selectedTargets.includes("copilot") && (options.force || !fs.existsSync(copilotPath))) {
92
+ targets.push({
93
+ name: "copilot",
94
+ path: copilotPath,
95
+ content: `${(0, archguard_guidance_1.generateCopilotInstructions)(architecture)}\n`,
96
+ });
97
+ }
98
+ return { configPath, architecture, targets };
99
+ }
100
+ function runGuidanceSync(cwd, options = {}) {
101
+ const plan = planGuidanceSync(cwd, options);
102
+ const updated = [];
103
+ const unchanged = [];
104
+ for (const target of plan.targets) {
105
+ const current = fs.existsSync(target.path) ? fs.readFileSync(target.path, "utf-8") : null;
106
+ if (current === target.content) {
107
+ unchanged.push(target.path);
108
+ continue;
109
+ }
110
+ if (options.checkOnly) {
111
+ updated.push(target.path);
112
+ continue;
113
+ }
114
+ fs.mkdirSync(path.dirname(target.path), { recursive: true });
115
+ fs.writeFileSync(target.path, target.content);
116
+ updated.push(target.path);
117
+ }
118
+ return {
119
+ configPath: plan.configPath,
120
+ updated,
121
+ unchanged,
122
+ };
123
+ }
124
+ function resolveConfigPath(cwd, configPath) {
125
+ if (configPath) {
126
+ const resolvedPath = path.isAbsolute(configPath)
127
+ ? configPath
128
+ : path.resolve(cwd, configPath);
129
+ if (!fs.existsSync(resolvedPath)) {
130
+ throw new Error(`Architecture config not found: ${configPath}`);
131
+ }
132
+ return resolvedPath;
133
+ }
134
+ const discoveredPath = (0, archguard_core_1.findArchitectureConfig)(cwd);
135
+ if (!discoveredPath) {
136
+ throw new Error("architecture.yaml not found. Run 'archguard init' first.");
137
+ }
138
+ return discoveredPath;
139
+ }
package/package.json CHANGED
@@ -1,12 +1,15 @@
1
1
  {
2
2
  "name": "@lifesavertech/archguard-cli",
3
- "version": "2.0.2",
3
+ "version": "2.2.0",
4
4
  "description": "Command-line interface for ArchGuard",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
5
7
  "bin": {
6
8
  "archguard": "dist/index.js"
7
9
  },
8
10
  "files": [
9
- "dist"
11
+ "dist",
12
+ "package.json"
10
13
  ],
11
14
  "engines": {
12
15
  "node": ">=20"
@@ -28,11 +31,10 @@
28
31
  "test": "npm run build:test && node --test dist-test/index.test.js dist-test/presets/presets.test.js"
29
32
  },
30
33
  "dependencies": {
31
- "@lifesavertech/archguard-guidance": "2.0.1",
32
- "@lifesavertech/archguard-core": "2.0.1",
33
- "@lifesavertech/archguard-rules-react": "2.0.1",
34
- "commander": "^11.0.0",
35
- "chalk": "^5.3.0"
34
+ "@lifesavertech/archguard-guidance": "2.0.2",
35
+ "@lifesavertech/archguard-core": "2.2.0",
36
+ "@lifesavertech/archguard-rules-react": "2.2.0",
37
+ "commander": "^11.0.0"
36
38
  },
37
39
  "devDependencies": {
38
40
  "@types/node": "^20.0.0"