@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 +14 -1
- package/dist/index.js +248 -56
- 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/style.d.ts +5 -0
- package/dist/style.js +20 -0
- package/dist/sync/syncGuidance.d.ts +33 -0
- package/dist/sync/syncGuidance.js +139 -0
- package/package.json +9 -7
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
|
@@ -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
|
-
|
|
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(
|
|
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(
|
|
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("--
|
|
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 =
|
|
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(
|
|
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(
|
|
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
|
-
|
|
119
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
486
|
+
console.error(style.yellow(`\nUnresolved imports (${unresolvedImports.length}):`));
|
|
379
487
|
for (const unresolvedImport of unresolvedImports) {
|
|
380
|
-
console.error(
|
|
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 === "
|
|
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 '
|
|
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,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
|
package/dist/style.d.ts
ADDED
|
@@ -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
|
|
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.
|
|
32
|
-
"@lifesavertech/archguard-core": "2.0
|
|
33
|
-
"@lifesavertech/archguard-rules-react": "2.0
|
|
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"
|