@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 +14 -1
- package/dist/index.js +222 -38
- package/dist/init/detectStructure.d.ts +12 -0
- package/dist/init/detectStructure.js +163 -0
- package/dist/init/generateConfig.d.ts +3 -0
- package/dist/init/generateConfig.js +127 -0
- package/dist/presets/index.js +11 -0
- package/dist/sync/syncGuidance.d.ts +33 -0
- package/dist/sync/syncGuidance.js +139 -0
- package/package.json +6 -4
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(
|
|
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("--
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
127
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
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
|
|
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 === "
|
|
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 '
|
|
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,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
|
+
}
|
package/dist/presets/index.js
CHANGED
|
@@ -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
|
+
"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.
|
|
33
|
-
"@lifesavertech/archguard-core": "2.0
|
|
34
|
-
"@lifesavertech/archguard-rules-react": "2.0
|
|
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": {
|