@lifesavertech/archguard-cli 2.0.3 → 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
@@ -46,8 +46,10 @@ const fs = __importStar(require("fs"));
46
46
  const path = __importStar(require("path"));
47
47
  const archguard_core_1 = require("@lifesavertech/archguard-core");
48
48
  const archguard_rules_react_1 = require("@lifesavertech/archguard-rules-react");
49
- const archguard_guidance_1 = require("@lifesavertech/archguard-guidance");
50
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");
51
53
  const style = __importStar(require("./style"));
52
54
  const MIN_NODE_MAJOR = 20;
53
55
  const nodeMajor = Number(process.versions.node.split(".")[0] ?? "0");
@@ -68,9 +70,10 @@ program
68
70
  .command("init")
69
71
  .description("Initialize archguard in the current project")
70
72
  .option("--preset <preset>", `Architecture preset (${(0, presets_1.formatPresetList)()})`)
73
+ .option("--no-detect", "Use preset only; skip repository structure detection")
71
74
  .action((options) => {
72
75
  try {
73
- initializeArchguard(options.preset);
76
+ initializeArchguard({ preset: options.preset, detect: options.detect });
74
77
  console.log(style.green("✓ archguard initialized successfully"));
75
78
  }
76
79
  catch (error) {
@@ -82,16 +85,21 @@ program
82
85
  .command("check")
83
86
  .description("Check project against architecture rules")
84
87
  .option("--baseline [path]", "Allow violations already captured in a baseline file")
85
- .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)")
86
93
  .option("--root <path>", "Project subdirectory to scan (for monorepos)")
87
94
  .option("--update-baseline [path]", "Write the current violations to a baseline file and exit")
88
95
  .option("--verbose", "Log unresolved imports and scan diagnostics")
89
96
  .action((options) => {
90
97
  try {
91
98
  const baselinePath = normalizeOptionalPath(options.baseline);
92
- const format = normalizeOutputFormat(options.format);
99
+ const format = resolveOutputFormat(options);
93
100
  const updateBaselinePath = normalizeOptionalPath(options.updateBaseline);
94
101
  const projectRoot = normalizeOptionalString(options.root);
102
+ const configPath = normalizeOptionalString(options.config);
95
103
  if (baselinePath && updateBaselinePath) {
96
104
  throw new Error("Use either --baseline or --update-baseline, not both.");
97
105
  }
@@ -99,7 +107,10 @@ program
99
107
  baselinePath,
100
108
  updateBaselinePath,
101
109
  projectRoot,
110
+ configPath,
102
111
  verbose: Boolean(options.verbose),
112
+ diff: Boolean(options.diff),
113
+ diffBase: normalizeOptionalString(options.diffBase),
103
114
  });
104
115
  printCheckSummary(result, format);
105
116
  if (!result.passed) {
@@ -111,10 +122,48 @@ program
111
122
  process.exit(1);
112
123
  }
113
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)}`));
160
+ process.exit(1);
161
+ }
162
+ });
114
163
  if (require.main === module) {
115
164
  program.parse();
116
165
  }
117
- function initializeArchguard(preset) {
166
+ function initializeArchguard(options = {}) {
118
167
  const cwd = process.cwd();
119
168
  if (!detectReactTypeScript(cwd)) {
120
169
  throw new Error("React + TypeScript project not detected");
@@ -123,21 +172,22 @@ function initializeArchguard(preset) {
123
172
  if (fs.existsSync(architectureYamlPath)) {
124
173
  throw new Error("architecture.yaml already exists");
125
174
  }
126
- fs.writeFileSync(architectureYamlPath, (0, presets_1.getPresetConfig)(preset));
127
- const guidanceDir = path.join(cwd, ".archguard");
128
- if (!fs.existsSync(guidanceDir)) {
129
- fs.mkdirSync(guidanceDir, { recursive: true });
130
- }
131
- const editorGuidePath = path.join(guidanceDir, "ARCHITECTURE_RULES.md");
175
+ const configContents = resolveInitConfig(cwd, options);
176
+ fs.writeFileSync(architectureYamlPath, configContents);
132
177
  const architecture = (0, archguard_core_1.loadArchitectureConfig)(architectureYamlPath);
133
- const editorGuide = (0, archguard_guidance_1.generateEditorGuide)(architecture);
134
- fs.writeFileSync(editorGuidePath, editorGuide);
135
- const agentsPath = path.join(cwd, "AGENTS.md");
136
- if (!fs.existsSync(agentsPath)) {
137
- fs.writeFileSync(agentsPath, (0, archguard_guidance_1.generateAgentsGuide)(architecture));
138
- }
178
+ (0, syncGuidance_1.runGuidanceSync)(cwd, { configPath: architectureYamlPath, updateAgents: false });
139
179
  installPreCommitHook(cwd);
140
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
+ }
141
191
  function detectReactTypeScript(cwd) {
142
192
  const packageJsonPath = path.join(cwd, "package.json");
143
193
  if (!fs.existsSync(packageJsonPath)) {
@@ -152,9 +202,16 @@ function detectReactTypeScript(cwd) {
152
202
  function runChecks(options = {}) {
153
203
  const cwd = process.cwd();
154
204
  const scanRoot = resolveScanRoot(cwd, options.projectRoot);
155
- const { violations: allViolations, scanSummary } = collectViolations(scanRoot, {
205
+ const applyDiffFilter = Boolean(options.diff) && !options.updateBaselinePath;
206
+ const { violations: rawViolations, scanSummary, diffMeta } = collectViolations(scanRoot, {
156
207
  verbose: options.verbose,
208
+ configPath: options.configPath,
209
+ diff: applyDiffFilter,
210
+ diffBase: options.diffBase,
211
+ changedFiles: options.changedFiles,
212
+ cwd,
157
213
  });
214
+ const allViolations = (0, archguard_core_1.enrichViolations)(rawViolations);
158
215
  if (options.verbose && scanSummary.unresolvedImports.length > 0) {
159
216
  printUnresolvedImports(scanSummary.unresolvedImports, scanRoot);
160
217
  }
@@ -169,44 +226,45 @@ function runChecks(options = {}) {
169
226
  baselinePath: options.updateBaselinePath,
170
227
  };
171
228
  }
229
+ const withDiff = (summary) => diffMeta ? { ...summary, diff: diffMeta } : summary;
172
230
  if (options.baselinePath) {
173
231
  const baselineViolations = loadBaseline(options.baselinePath);
174
232
  const { newViolations, suppressedViolationCount } = filterViolationsAgainstBaseline(allViolations, baselineViolations, scanRoot);
175
233
  if (newViolations.length > 0) {
176
- return {
234
+ return withDiff({
177
235
  mode: "baseline",
178
236
  passed: false,
179
237
  violationCount: newViolations.length,
180
238
  violations: newViolations,
181
239
  suppressedViolationCount,
182
240
  baselinePath: options.baselinePath,
183
- };
241
+ });
184
242
  }
185
- return {
243
+ return withDiff({
186
244
  mode: "baseline",
187
245
  passed: true,
188
246
  violationCount: 0,
189
247
  violations: [],
190
248
  suppressedViolationCount,
191
249
  baselinePath: options.baselinePath,
192
- };
250
+ });
193
251
  }
194
252
  if (allViolations.length > 0) {
195
- return {
253
+ return withDiff({
196
254
  mode: "check",
197
255
  passed: false,
198
256
  violationCount: allViolations.length,
199
257
  violations: allViolations,
200
258
  suppressedViolationCount: 0,
201
- };
259
+ });
202
260
  }
203
- return {
261
+ return withDiff({
204
262
  mode: "check",
205
263
  passed: true,
206
264
  violationCount: 0,
207
265
  violations: [],
208
266
  suppressedViolationCount: 0,
209
- };
267
+ });
210
268
  }
211
269
  function findTypeScriptFiles(dir) {
212
270
  const files = [];
@@ -289,19 +347,32 @@ npx archguard check || exit 1
289
347
  fs.writeFileSync(preCommitPath, preCommitHook);
290
348
  fs.chmodSync(preCommitPath, 0o755);
291
349
  }
292
- function collectViolations(scanRoot, options = {}) {
293
- const configPath = (0, archguard_core_1.findArchitectureConfig)(scanRoot);
294
- if (!configPath) {
295
- 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;
355
+ }
356
+ const matchedFiles = (0, archguard_core_1.countFilesMatchingLayers)(sourceFiles, architecture, scanRoot);
357
+ if (matchedFiles > 0) {
358
+ return;
296
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);
297
366
  const architecture = (0, archguard_core_1.loadArchitectureConfig)(configPath);
298
367
  const engine = new archguard_core_1.RuleEngine();
299
368
  const unresolvedImports = [];
300
369
  engine.registerRule((0, archguard_rules_react_1.createLayerImportBoundaryRule)());
370
+ engine.registerRule((0, archguard_rules_react_1.createBarrelBoundaryBypassRule)());
301
371
  engine.registerRule((0, archguard_rules_react_1.createBusinessLogicInComponentsRule)());
302
372
  engine.registerRule((0, archguard_rules_react_1.createDataFetchingInUIRule)());
303
373
  engine.registerRule((0, archguard_rules_react_1.createCircularDependencyRule)());
304
- const sourceFiles = findTypeScriptFiles(scanRoot);
374
+ const sourceFiles = findTypeScriptFiles(scanRoot).filter((filePath) => !(0, archguard_core_1.shouldIgnorePath)(filePath, architecture, scanRoot));
375
+ assertLayerCoverage(sourceFiles, architecture, scanRoot);
305
376
  const importResolver = (0, archguard_core_1.createImportResolver)(scanRoot);
306
377
  const sourceFileAnalyses = analyzeSourceFiles(sourceFiles, importResolver, unresolvedImports);
307
378
  const importGraph = buildImportGraph(sourceFileAnalyses);
@@ -322,12 +393,33 @@ function collectViolations(scanRoot, options = {}) {
322
393
  if (options.verbose) {
323
394
  console.error(style.gray(`ArchGuard scanned ${sourceFiles.length} file(s) from ${path.relative(scanRoot, scanRoot) || "."} using ${normalizeViolationPath(configPath, scanRoot)}`));
324
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
+ }
418
+ }
325
419
  return {
326
- violations: sortViolations(allViolations.map((violation) => ({
327
- ...violation,
328
- file: normalizeViolationPath(violation.file, scanRoot),
329
- }))),
420
+ violations,
330
421
  scanSummary: { unresolvedImports },
422
+ diffMeta,
331
423
  };
332
424
  }
333
425
  function printViolations(violations) {
@@ -341,10 +433,18 @@ function printCheckSummary(summary, format) {
341
433
  console.log(JSON.stringify(buildJsonSummary(summary), null, 2));
342
434
  return;
343
435
  }
436
+ if (format === "agent") {
437
+ console.log(JSON.stringify(buildAgentSummary(summary), null, 2));
438
+ return;
439
+ }
344
440
  if (format === "sarif") {
345
441
  console.log(JSON.stringify(buildSarifReport(summary), null, 2));
346
442
  return;
347
443
  }
444
+ if (format === "github") {
445
+ printGithubAnnotations(summary);
446
+ return;
447
+ }
348
448
  if (summary.mode === "update-baseline") {
349
449
  console.log(style.green(`✓ Wrote ${summary.violations.length} violation(s) to baseline ${summary.baselinePath}`));
350
450
  return;
@@ -400,13 +500,40 @@ function normalizeOptionalPath(optionValue) {
400
500
  }
401
501
  function normalizeOutputFormat(format) {
402
502
  const normalizedFormat = (format ?? "text").trim().toLowerCase();
403
- if (normalizedFormat === "json" || normalizedFormat === "sarif" || normalizedFormat === "text") {
503
+ if (normalizedFormat === "agent" ||
504
+ normalizedFormat === "github" ||
505
+ normalizedFormat === "json" ||
506
+ normalizedFormat === "sarif" ||
507
+ normalizedFormat === "text") {
404
508
  return normalizedFormat;
405
509
  }
406
- 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;
407
533
  }
408
534
  function buildJsonSummary(summary) {
409
535
  return {
536
+ schemaVersion: 2,
410
537
  status: summary.passed ? "passed" : "failed",
411
538
  mode: summary.mode,
412
539
  passed: summary.passed,
@@ -414,8 +541,60 @@ function buildJsonSummary(summary) {
414
541
  suppressedViolationCount: summary.suppressedViolationCount,
415
542
  baselinePath: summary.baselinePath ?? null,
416
543
  violations: summary.violations,
544
+ diff: summary.diff,
417
545
  };
418
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
+ }
419
598
  function buildSarifReport(summary) {
420
599
  const ruleDescriptions = new Map([
421
600
  ["layer-import-boundary", "Import violates a configured layer boundary."],
@@ -565,5 +744,10 @@ function isViolation(value) {
565
744
  typeof violation.line === "number" &&
566
745
  typeof violation.column === "number" &&
567
746
  typeof violation.message === "string" &&
568
- 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"));
569
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,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,7 +1,9 @@
1
1
  {
2
2
  "name": "@lifesavertech/archguard-cli",
3
- "version": "2.0.3",
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
  },
@@ -29,9 +31,9 @@
29
31
  "test": "npm run build:test && node --test dist-test/index.test.js dist-test/presets/presets.test.js"
30
32
  },
31
33
  "dependencies": {
32
- "@lifesavertech/archguard-guidance": "2.0.1",
33
- "@lifesavertech/archguard-core": "2.0.1",
34
- "@lifesavertech/archguard-rules-react": "2.0.1",
34
+ "@lifesavertech/archguard-guidance": "2.0.2",
35
+ "@lifesavertech/archguard-core": "2.2.0",
36
+ "@lifesavertech/archguard-rules-react": "2.2.0",
35
37
  "commander": "^11.0.0"
36
38
  },
37
39
  "devDependencies": {