@lifesavertech/archguard-cli 2.0.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/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # @lifesavertech/archguard-cli
2
+
3
+ Command-line interface for ArchGuard — architecture gate for AI-assisted React PRs.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -D @lifesavertech/archguard-cli
9
+ npx archguard --version
10
+ ```
11
+
12
+ Until published to npm, use the [GitHub Action](https://github.com/lindseystead/ArchGuard/blob/main/docs/github-action.md) or [install from source](https://github.com/lindseystead/ArchGuard#install-from-this-repository).
13
+
14
+ > The legacy scope `@archguard/cli` on npm is an unrelated package. Do not use it.
15
+
16
+ ## Usage
17
+
18
+ ```bash
19
+ archguard init # boundaries-only (v2 default)
20
+ archguard init --preset react-layered # boundaries + UI heuristics
21
+ archguard init --preset feature-sliced-react
22
+ archguard check
23
+ archguard check --root apps/web --verbose
24
+ archguard check --baseline
25
+ archguard check --format json
26
+ ```
27
+
28
+ ## Presets
29
+
30
+ | Preset | UI heuristics | Layers |
31
+ | --- | --- | --- |
32
+ | _(default)_ / `boundaries-only` | Off | domain, features, ui, shared |
33
+ | `react-layered` | On | domain, features, ui, shared |
34
+ | `feature-sliced-react` | On | FSD: app, pages, features, entities, shared |
35
+
36
+ Preset definitions live in `src/presets/`.
37
+
38
+ ## Documentation
39
+
40
+ | Doc | Purpose |
41
+ | --- | --- |
42
+ | [ADOPTION.md](https://github.com/lindseystead/ArchGuard/blob/main/docs/ADOPTION.md) | Setup guide |
43
+ | [architecture.example.yaml](https://github.com/lindseystead/ArchGuard/blob/main/docs/architecture.example.yaml) | Config reference |
44
+ | [ENGINEERING_PLAN.md](https://github.com/lindseystead/ArchGuard/blob/main/docs/ENGINEERING_PLAN.md) | v2 roadmap |
45
+
46
+ Full documentation: [github.com/lindseystead/ArchGuard](https://github.com/lindseystead/ArchGuard)
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+ /** CLI entry point. */
3
+ import { ImportInfo, RuleContext, ImportGraph, Violation } from "@lifesavertech/archguard-core";
4
+ interface CheckOptions {
5
+ baselinePath?: string | null;
6
+ updateBaselinePath?: string | null;
7
+ projectRoot?: string | null;
8
+ verbose?: boolean;
9
+ }
10
+ interface CheckSummary {
11
+ mode: "baseline" | "check" | "update-baseline";
12
+ passed: boolean;
13
+ violationCount: number;
14
+ violations: Violation[];
15
+ suppressedViolationCount: number;
16
+ baselinePath?: string | null;
17
+ }
18
+ interface SourceFileAnalysis {
19
+ filePath: string;
20
+ sourceCode: string;
21
+ ast: RuleContext["ast"];
22
+ imports: ImportInfo[];
23
+ }
24
+ export declare function initializeArchguard(preset?: string): void;
25
+ export declare function detectReactTypeScript(cwd: string): boolean;
26
+ export declare function runChecks(options?: CheckOptions): CheckSummary;
27
+ export declare function findTypeScriptFiles(dir: string): string[];
28
+ export declare function buildImportGraph(sourceFileAnalyses: SourceFileAnalysis[]): ImportGraph;
29
+ export declare function resolveImportPath(importPath: string, fromFile: string, projectRoot?: string): string | null;
30
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,561 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /** CLI entry point. */
4
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
5
+ if (k2 === undefined) k2 = k;
6
+ var desc = Object.getOwnPropertyDescriptor(m, k);
7
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
8
+ desc = { enumerable: true, get: function() { return m[k]; } };
9
+ }
10
+ Object.defineProperty(o, k2, desc);
11
+ }) : (function(o, m, k, k2) {
12
+ if (k2 === undefined) k2 = k;
13
+ o[k2] = m[k];
14
+ }));
15
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
16
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
17
+ }) : function(o, v) {
18
+ o["default"] = v;
19
+ });
20
+ var __importStar = (this && this.__importStar) || (function () {
21
+ var ownKeys = function(o) {
22
+ ownKeys = Object.getOwnPropertyNames || function (o) {
23
+ var ar = [];
24
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
25
+ return ar;
26
+ };
27
+ return ownKeys(o);
28
+ };
29
+ return function (mod) {
30
+ if (mod && mod.__esModule) return mod;
31
+ var result = {};
32
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
33
+ __setModuleDefault(result, mod);
34
+ return result;
35
+ };
36
+ })();
37
+ var __importDefault = (this && this.__importDefault) || function (mod) {
38
+ return (mod && mod.__esModule) ? mod : { "default": mod };
39
+ };
40
+ Object.defineProperty(exports, "__esModule", { value: true });
41
+ exports.initializeArchguard = initializeArchguard;
42
+ exports.detectReactTypeScript = detectReactTypeScript;
43
+ exports.runChecks = runChecks;
44
+ exports.findTypeScriptFiles = findTypeScriptFiles;
45
+ exports.buildImportGraph = buildImportGraph;
46
+ exports.resolveImportPath = resolveImportPath;
47
+ const commander_1 = require("commander");
48
+ const fs = __importStar(require("fs"));
49
+ const path = __importStar(require("path"));
50
+ const chalk_1 = __importDefault(require("chalk"));
51
+ const archguard_core_1 = require("@lifesavertech/archguard-core");
52
+ const archguard_rules_react_1 = require("@lifesavertech/archguard-rules-react");
53
+ const archguard_guidance_1 = require("@lifesavertech/archguard-guidance");
54
+ const presets_1 = require("./presets");
55
+ const program = new commander_1.Command();
56
+ const DEFAULT_BASELINE_PATH = ".archguard-baseline.json";
57
+ const SARIF_SCHEMA_URL = "https://json.schemastore.org/sarif-2.1.0.json";
58
+ program.name("archguard").description("Architecture guard for React + TypeScript projects").version("2.0.0");
59
+ program
60
+ .command("init")
61
+ .description("Initialize archguard in the current project")
62
+ .option("--preset <preset>", `Architecture preset (${(0, presets_1.formatPresetList)()})`)
63
+ .action((options) => {
64
+ try {
65
+ initializeArchguard(options.preset);
66
+ console.log(chalk_1.default.green("✓ archguard initialized successfully"));
67
+ }
68
+ catch (error) {
69
+ console.error(chalk_1.default.red(`✗ Error: ${error instanceof Error ? error.message : String(error)}`));
70
+ process.exit(1);
71
+ }
72
+ });
73
+ program
74
+ .command("check")
75
+ .description("Check project against architecture rules")
76
+ .option("--baseline [path]", "Allow violations already captured in a baseline file")
77
+ .option("--format <format>", "Output format (text, json, sarif)", "text")
78
+ .option("--root <path>", "Project subdirectory to scan (for monorepos)")
79
+ .option("--update-baseline [path]", "Write the current violations to a baseline file and exit")
80
+ .option("--verbose", "Log unresolved imports and scan diagnostics")
81
+ .action((options) => {
82
+ try {
83
+ const baselinePath = normalizeOptionalPath(options.baseline);
84
+ const format = normalizeOutputFormat(options.format);
85
+ const updateBaselinePath = normalizeOptionalPath(options.updateBaseline);
86
+ const projectRoot = normalizeOptionalString(options.root);
87
+ if (baselinePath && updateBaselinePath) {
88
+ throw new Error("Use either --baseline or --update-baseline, not both.");
89
+ }
90
+ const result = runChecks({
91
+ baselinePath,
92
+ updateBaselinePath,
93
+ projectRoot,
94
+ verbose: Boolean(options.verbose),
95
+ });
96
+ printCheckSummary(result, format);
97
+ if (!result.passed) {
98
+ process.exit(1);
99
+ }
100
+ }
101
+ catch (error) {
102
+ console.error(chalk_1.default.red(`✗ Error: ${error instanceof Error ? error.message : String(error)}`));
103
+ process.exit(1);
104
+ }
105
+ });
106
+ if (require.main === module) {
107
+ program.parse();
108
+ }
109
+ function initializeArchguard(preset) {
110
+ const cwd = process.cwd();
111
+ if (!detectReactTypeScript(cwd)) {
112
+ throw new Error("React + TypeScript project not detected");
113
+ }
114
+ const architectureYamlPath = path.join(cwd, "architecture.yaml");
115
+ if (fs.existsSync(architectureYamlPath)) {
116
+ throw new Error("architecture.yaml already exists");
117
+ }
118
+ fs.writeFileSync(architectureYamlPath, (0, presets_1.getPresetConfig)(preset));
119
+ const guidanceDir = path.join(cwd, ".archguard");
120
+ if (!fs.existsSync(guidanceDir)) {
121
+ fs.mkdirSync(guidanceDir, { recursive: true });
122
+ }
123
+ const editorGuidePath = path.join(guidanceDir, "ARCHITECTURE_RULES.md");
124
+ const architecture = (0, archguard_core_1.loadArchitectureConfig)(architectureYamlPath);
125
+ const editorGuide = (0, archguard_guidance_1.generateEditorGuide)(architecture);
126
+ fs.writeFileSync(editorGuidePath, editorGuide);
127
+ const agentsPath = path.join(cwd, "AGENTS.md");
128
+ if (!fs.existsSync(agentsPath)) {
129
+ fs.writeFileSync(agentsPath, (0, archguard_guidance_1.generateAgentsGuide)(architecture));
130
+ }
131
+ installPreCommitHook(cwd);
132
+ }
133
+ function detectReactTypeScript(cwd) {
134
+ const packageJsonPath = path.join(cwd, "package.json");
135
+ if (!fs.existsSync(packageJsonPath)) {
136
+ return false;
137
+ }
138
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
139
+ const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
140
+ const hasReact = deps.react || deps["react-dom"];
141
+ const hasTypeScript = deps.typescript || fs.existsSync(path.join(cwd, "tsconfig.json"));
142
+ return !!(hasReact && hasTypeScript);
143
+ }
144
+ function runChecks(options = {}) {
145
+ const cwd = process.cwd();
146
+ const scanRoot = resolveScanRoot(cwd, options.projectRoot);
147
+ const { violations: allViolations, scanSummary } = collectViolations(scanRoot, {
148
+ verbose: options.verbose,
149
+ });
150
+ if (options.verbose && scanSummary.unresolvedImports.length > 0) {
151
+ printUnresolvedImports(scanSummary.unresolvedImports, scanRoot);
152
+ }
153
+ if (options.updateBaselinePath) {
154
+ writeBaseline(options.updateBaselinePath, allViolations, scanRoot);
155
+ return {
156
+ mode: "update-baseline",
157
+ passed: true,
158
+ violationCount: allViolations.length,
159
+ violations: allViolations,
160
+ suppressedViolationCount: 0,
161
+ baselinePath: options.updateBaselinePath,
162
+ };
163
+ }
164
+ if (options.baselinePath) {
165
+ const baselineViolations = loadBaseline(options.baselinePath);
166
+ const { newViolations, suppressedViolationCount } = filterViolationsAgainstBaseline(allViolations, baselineViolations, scanRoot);
167
+ if (newViolations.length > 0) {
168
+ return {
169
+ mode: "baseline",
170
+ passed: false,
171
+ violationCount: newViolations.length,
172
+ violations: newViolations,
173
+ suppressedViolationCount,
174
+ baselinePath: options.baselinePath,
175
+ };
176
+ }
177
+ return {
178
+ mode: "baseline",
179
+ passed: true,
180
+ violationCount: 0,
181
+ violations: [],
182
+ suppressedViolationCount,
183
+ baselinePath: options.baselinePath,
184
+ };
185
+ }
186
+ if (allViolations.length > 0) {
187
+ return {
188
+ mode: "check",
189
+ passed: false,
190
+ violationCount: allViolations.length,
191
+ violations: allViolations,
192
+ suppressedViolationCount: 0,
193
+ };
194
+ }
195
+ return {
196
+ mode: "check",
197
+ passed: true,
198
+ violationCount: 0,
199
+ violations: [],
200
+ suppressedViolationCount: 0,
201
+ };
202
+ }
203
+ function findTypeScriptFiles(dir) {
204
+ const files = [];
205
+ const ignoreDirs = ["node_modules", "dist", "build", ".git", ".archguard"];
206
+ function walk(currentDir) {
207
+ const entries = fs.readdirSync(currentDir, { withFileTypes: true });
208
+ for (const entry of entries) {
209
+ const fullPath = path.join(currentDir, entry.name);
210
+ if (entry.isDirectory()) {
211
+ if (!ignoreDirs.includes(entry.name)) {
212
+ walk(fullPath);
213
+ }
214
+ }
215
+ else if (entry.isFile() && (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx"))) {
216
+ files.push(fullPath);
217
+ }
218
+ }
219
+ }
220
+ walk(dir);
221
+ return files;
222
+ }
223
+ function buildImportGraph(sourceFileAnalyses) {
224
+ const graph = {};
225
+ const fileSet = new Set(sourceFileAnalyses.map((analysis) => analysis.filePath));
226
+ for (const analysis of sourceFileAnalyses) {
227
+ const resolvedImports = [];
228
+ for (const importedModule of analysis.imports) {
229
+ if (importedModule.resolvedPath && fileSet.has(importedModule.resolvedPath)) {
230
+ resolvedImports.push(importedModule.resolvedPath);
231
+ }
232
+ }
233
+ graph[analysis.filePath] = resolvedImports;
234
+ }
235
+ return graph;
236
+ }
237
+ function analyzeSourceFiles(sourceFiles, importResolver, unresolvedImports) {
238
+ return sourceFiles.map((filePath) => {
239
+ const sourceCode = fs.readFileSync(filePath, "utf-8");
240
+ const ast = (0, archguard_core_1.parseTypeScript)(sourceCode, filePath);
241
+ const imports = (0, archguard_core_1.extractImports)(ast).map((importInfo) => {
242
+ const resolvedPath = importResolver.resolve(importInfo.from, filePath);
243
+ if (!resolvedPath &&
244
+ (importInfo.from.startsWith(".") || importInfo.from.startsWith("@") || importInfo.from.startsWith("/"))) {
245
+ unresolvedImports.push({
246
+ file: filePath,
247
+ importPath: importInfo.from,
248
+ });
249
+ }
250
+ return {
251
+ ...importInfo,
252
+ resolvedPath,
253
+ };
254
+ });
255
+ return {
256
+ filePath,
257
+ sourceCode,
258
+ ast,
259
+ imports,
260
+ };
261
+ });
262
+ }
263
+ function resolveImportPath(importPath, fromFile, projectRoot = process.cwd()) {
264
+ return (0, archguard_core_1.createImportResolver)(projectRoot).resolve(importPath, fromFile);
265
+ }
266
+ function installPreCommitHook(cwd) {
267
+ const gitHooksDir = path.join(cwd, ".git", "hooks");
268
+ if (!fs.existsSync(gitHooksDir)) {
269
+ return;
270
+ }
271
+ const preCommitPath = path.join(gitHooksDir, "pre-commit");
272
+ if (fs.existsSync(preCommitPath)) {
273
+ console.log(chalk_1.default.yellow("! Existing pre-commit hook detected. ArchGuard did not overwrite it; add the check command manually."));
274
+ return;
275
+ }
276
+ // Use the installed CLI name so hooks keep working after npm install or global linking.
277
+ const preCommitHook = `#!/bin/sh
278
+ # archguard pre-commit hook
279
+ npx archguard check || exit 1
280
+ `;
281
+ fs.writeFileSync(preCommitPath, preCommitHook);
282
+ fs.chmodSync(preCommitPath, 0o755);
283
+ }
284
+ function collectViolations(scanRoot, options = {}) {
285
+ const configPath = (0, archguard_core_1.findArchitectureConfig)(scanRoot);
286
+ if (!configPath) {
287
+ throw new Error("architecture.yaml not found. Run 'archguard init' first.");
288
+ }
289
+ const architecture = (0, archguard_core_1.loadArchitectureConfig)(configPath);
290
+ const engine = new archguard_core_1.RuleEngine();
291
+ const unresolvedImports = [];
292
+ engine.registerRule((0, archguard_rules_react_1.createLayerImportBoundaryRule)());
293
+ engine.registerRule((0, archguard_rules_react_1.createBusinessLogicInComponentsRule)());
294
+ engine.registerRule((0, archguard_rules_react_1.createDataFetchingInUIRule)());
295
+ engine.registerRule((0, archguard_rules_react_1.createCircularDependencyRule)());
296
+ const sourceFiles = findTypeScriptFiles(scanRoot);
297
+ const importResolver = (0, archguard_core_1.createImportResolver)(scanRoot);
298
+ const sourceFileAnalyses = analyzeSourceFiles(sourceFiles, importResolver, unresolvedImports);
299
+ const importGraph = buildImportGraph(sourceFileAnalyses);
300
+ const allViolations = [];
301
+ for (const analysis of sourceFileAnalyses) {
302
+ const context = {
303
+ filePath: analysis.filePath,
304
+ sourceCode: analysis.sourceCode,
305
+ ast: analysis.ast,
306
+ imports: analysis.imports,
307
+ architecture,
308
+ importGraph,
309
+ projectRoot: scanRoot,
310
+ };
311
+ const result = engine.executeRules(context);
312
+ allViolations.push(...result.violations);
313
+ }
314
+ if (options.verbose) {
315
+ console.error(chalk_1.default.gray(`ArchGuard scanned ${sourceFiles.length} file(s) from ${path.relative(scanRoot, scanRoot) || "."} using ${normalizeViolationPath(configPath, scanRoot)}`));
316
+ }
317
+ return {
318
+ violations: sortViolations(allViolations.map((violation) => ({
319
+ ...violation,
320
+ file: normalizeViolationPath(violation.file, scanRoot),
321
+ }))),
322
+ scanSummary: { unresolvedImports },
323
+ };
324
+ }
325
+ function printViolations(violations) {
326
+ for (const violation of violations) {
327
+ console.log(chalk_1.default.yellow(`${violation.file}:${violation.line}:${violation.column}`) +
328
+ ` - ${violation.message} (${violation.rule})`);
329
+ }
330
+ }
331
+ function printCheckSummary(summary, format) {
332
+ if (format === "json") {
333
+ console.log(JSON.stringify(buildJsonSummary(summary), null, 2));
334
+ return;
335
+ }
336
+ if (format === "sarif") {
337
+ console.log(JSON.stringify(buildSarifReport(summary), null, 2));
338
+ return;
339
+ }
340
+ if (summary.mode === "update-baseline") {
341
+ console.log(chalk_1.default.green(`✓ Wrote ${summary.violations.length} violation(s) to baseline ${summary.baselinePath}`));
342
+ return;
343
+ }
344
+ if (summary.mode === "baseline" && !summary.passed) {
345
+ console.log(chalk_1.default.red(`\n✗ Found ${summary.violationCount} new violation(s) (${summary.suppressedViolationCount} matched baseline):\n`));
346
+ printViolations(summary.violations);
347
+ return;
348
+ }
349
+ if (summary.mode === "baseline" && summary.suppressedViolationCount > 0) {
350
+ console.log(chalk_1.default.green(`✓ No new violations. ${summary.suppressedViolationCount} violation(s) matched baseline ${summary.baselinePath}`));
351
+ return;
352
+ }
353
+ if (!summary.passed) {
354
+ console.log(chalk_1.default.red(`\n✗ Found ${summary.violationCount} violation(s):\n`));
355
+ printViolations(summary.violations);
356
+ return;
357
+ }
358
+ console.log(chalk_1.default.green("✓ All checks passed"));
359
+ }
360
+ function normalizeOptionalString(optionValue) {
361
+ if (optionValue === undefined) {
362
+ return null;
363
+ }
364
+ const trimmedValue = optionValue.trim();
365
+ return trimmedValue.length > 0 ? trimmedValue : null;
366
+ }
367
+ function resolveScanRoot(cwd, projectRoot) {
368
+ const scanRoot = projectRoot ? path.resolve(cwd, projectRoot) : cwd;
369
+ if (!fs.existsSync(scanRoot)) {
370
+ throw new Error(`Scan root not found: ${projectRoot ?? "."}`);
371
+ }
372
+ if (!fs.statSync(scanRoot).isDirectory()) {
373
+ throw new Error(`Scan root is not a directory: ${projectRoot}`);
374
+ }
375
+ return scanRoot;
376
+ }
377
+ function printUnresolvedImports(unresolvedImports, scanRoot) {
378
+ console.error(chalk_1.default.yellow(`\nUnresolved imports (${unresolvedImports.length}):`));
379
+ for (const unresolvedImport of unresolvedImports) {
380
+ console.error(chalk_1.default.yellow(` ${normalizeViolationPath(unresolvedImport.file, scanRoot)} -> ${unresolvedImport.importPath}`));
381
+ }
382
+ }
383
+ function normalizeOptionalPath(optionValue) {
384
+ if (optionValue === undefined || optionValue === false) {
385
+ return null;
386
+ }
387
+ if (optionValue === true) {
388
+ return DEFAULT_BASELINE_PATH;
389
+ }
390
+ const trimmedValue = optionValue.trim();
391
+ return trimmedValue.length > 0 ? trimmedValue : DEFAULT_BASELINE_PATH;
392
+ }
393
+ function normalizeOutputFormat(format) {
394
+ const normalizedFormat = (format ?? "text").trim().toLowerCase();
395
+ if (normalizedFormat === "json" || normalizedFormat === "sarif" || normalizedFormat === "text") {
396
+ return normalizedFormat;
397
+ }
398
+ throw new Error(`Unsupported output format: ${format}. Expected 'text', 'json', or 'sarif'.`);
399
+ }
400
+ function buildJsonSummary(summary) {
401
+ return {
402
+ status: summary.passed ? "passed" : "failed",
403
+ mode: summary.mode,
404
+ passed: summary.passed,
405
+ violationCount: summary.violationCount,
406
+ suppressedViolationCount: summary.suppressedViolationCount,
407
+ baselinePath: summary.baselinePath ?? null,
408
+ violations: summary.violations,
409
+ };
410
+ }
411
+ function buildSarifReport(summary) {
412
+ const ruleDescriptions = new Map([
413
+ ["layer-import-boundary", "Import violates a configured layer boundary."],
414
+ ["no-business-logic-in-components", "Component contains logic that should be moved out of the UI layer."],
415
+ ["no-data-fetching-in-ui", "Component performs data fetching that should be moved out of the UI layer."],
416
+ ["no-circular-layer-deps", "Dependency graph contains a circular reference."],
417
+ ]);
418
+ const ruleIds = Array.from(new Set(summary.violations.map((violation) => violation.rule))).sort();
419
+ return {
420
+ $schema: SARIF_SCHEMA_URL,
421
+ version: "2.1.0",
422
+ runs: [
423
+ {
424
+ tool: {
425
+ driver: {
426
+ name: "archguard",
427
+ informationUri: "https://github.com/lindseystead/ArchGuard",
428
+ rules: ruleIds.map((ruleId) => ({
429
+ id: ruleId,
430
+ shortDescription: {
431
+ text: ruleDescriptions.get(ruleId) ?? "ArchGuard architectural violation.",
432
+ },
433
+ })),
434
+ },
435
+ },
436
+ invocations: [
437
+ {
438
+ executionSuccessful: true,
439
+ properties: {
440
+ baselinePath: summary.baselinePath ?? null,
441
+ mode: summary.mode,
442
+ suppressedViolationCount: summary.suppressedViolationCount,
443
+ violationCount: summary.violationCount,
444
+ },
445
+ },
446
+ ],
447
+ results: summary.violations.map((violation) => ({
448
+ ruleId: violation.rule,
449
+ level: "error",
450
+ message: { text: violation.message },
451
+ locations: [
452
+ {
453
+ physicalLocation: {
454
+ artifactLocation: {
455
+ uri: normalizeViolationPath(violation.file, process.cwd()),
456
+ },
457
+ region: {
458
+ startColumn: violation.column,
459
+ startLine: violation.line,
460
+ },
461
+ },
462
+ },
463
+ ],
464
+ })),
465
+ },
466
+ ],
467
+ };
468
+ }
469
+ function writeBaseline(baselinePath, violations, cwd) {
470
+ const resolvedPath = resolveProjectPath(cwd, baselinePath);
471
+ fs.mkdirSync(path.dirname(resolvedPath), { recursive: true });
472
+ const baseline = {
473
+ version: 1,
474
+ generatedAt: new Date().toISOString(),
475
+ violations: violations.map((violation) => ({
476
+ ...violation,
477
+ file: normalizeViolationPath(violation.file, cwd),
478
+ })),
479
+ };
480
+ fs.writeFileSync(resolvedPath, `${JSON.stringify(baseline, null, 2)}\n`);
481
+ }
482
+ function loadBaseline(baselinePath) {
483
+ const resolvedPath = resolveProjectPath(process.cwd(), baselinePath);
484
+ if (!fs.existsSync(resolvedPath)) {
485
+ throw new Error(`Baseline file not found: ${baselinePath}`);
486
+ }
487
+ const rawContent = fs.readFileSync(resolvedPath, "utf-8");
488
+ const parsed = JSON.parse(rawContent);
489
+ if (!Array.isArray(parsed.violations)) {
490
+ throw new Error(`Invalid baseline file: ${baselinePath}`);
491
+ }
492
+ return sortViolations(parsed.violations.map((violation) => {
493
+ if (!isViolation(violation)) {
494
+ throw new Error(`Invalid baseline file: ${baselinePath}`);
495
+ }
496
+ return violation;
497
+ }));
498
+ }
499
+ function filterViolationsAgainstBaseline(currentViolations, baselineViolations, cwd) {
500
+ const baselineCounts = new Map();
501
+ for (const violation of baselineViolations) {
502
+ const fingerprint = getViolationFingerprint(violation, cwd);
503
+ baselineCounts.set(fingerprint, (baselineCounts.get(fingerprint) ?? 0) + 1);
504
+ }
505
+ const newViolations = [];
506
+ let suppressedViolationCount = 0;
507
+ for (const violation of currentViolations) {
508
+ const fingerprint = getViolationFingerprint(violation, cwd);
509
+ const remainingMatches = baselineCounts.get(fingerprint) ?? 0;
510
+ if (remainingMatches > 0) {
511
+ baselineCounts.set(fingerprint, remainingMatches - 1);
512
+ suppressedViolationCount += 1;
513
+ continue;
514
+ }
515
+ newViolations.push(violation);
516
+ }
517
+ return { newViolations, suppressedViolationCount };
518
+ }
519
+ function sortViolations(violations) {
520
+ return [...violations].sort((left, right) => {
521
+ if (left.file !== right.file) {
522
+ return left.file.localeCompare(right.file);
523
+ }
524
+ if (left.rule !== right.rule) {
525
+ return left.rule.localeCompare(right.rule);
526
+ }
527
+ if (left.message !== right.message) {
528
+ return left.message.localeCompare(right.message);
529
+ }
530
+ if (left.line !== right.line) {
531
+ return left.line - right.line;
532
+ }
533
+ return left.column - right.column;
534
+ });
535
+ }
536
+ function normalizeViolationPath(filePath, cwd) {
537
+ const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath);
538
+ const relativePath = path.relative(cwd, absolutePath) || path.basename(absolutePath);
539
+ return relativePath.replace(/\\/g, "/");
540
+ }
541
+ function getViolationFingerprint(violation, cwd) {
542
+ return [
543
+ normalizeViolationPath(violation.file, cwd),
544
+ violation.rule,
545
+ violation.message,
546
+ ].join("::");
547
+ }
548
+ function resolveProjectPath(cwd, filePath) {
549
+ return path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath);
550
+ }
551
+ function isViolation(value) {
552
+ if (!value || typeof value !== "object") {
553
+ return false;
554
+ }
555
+ const violation = value;
556
+ return (typeof violation.file === "string" &&
557
+ typeof violation.line === "number" &&
558
+ typeof violation.column === "number" &&
559
+ typeof violation.message === "string" &&
560
+ typeof violation.rule === "string");
561
+ }
@@ -0,0 +1,10 @@
1
+ /** Architecture.yaml presets for `archguard init`. */
2
+ export declare const PRESET_NAMES: readonly ["boundaries-only", "react-layered", "feature-sliced-react"];
3
+ export type PresetName = (typeof PRESET_NAMES)[number];
4
+ /** Human-readable preset list for CLI errors and help text. */
5
+ export declare function formatPresetList(): string;
6
+ /**
7
+ * Resolve preset name to architecture.yaml contents.
8
+ * Omit preset for boundaries-only (v2 default).
9
+ */
10
+ export declare function getPresetConfig(preset?: string): string;
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ /** Architecture.yaml presets for `archguard init`. */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.PRESET_NAMES = void 0;
5
+ exports.formatPresetList = formatPresetList;
6
+ exports.getPresetConfig = getPresetConfig;
7
+ exports.PRESET_NAMES = ["boundaries-only", "react-layered", "feature-sliced-react"];
8
+ const BOUNDARIES_ONLY_CONFIG = `layers:
9
+ domain:
10
+ allowedImports: []
11
+ features:
12
+ allowedImports: [domain, shared]
13
+ ui:
14
+ allowedImports: [features, shared]
15
+ shared:
16
+ allowedImports: []
17
+
18
+ rules:
19
+ enforceLayerBoundaries: true
20
+ noCircularLayerDeps: true
21
+ noBusinessLogicInComponents: false
22
+ noDataFetchingInUI: false
23
+ `;
24
+ const REACT_LAYERED_CONFIG = `layers:
25
+ domain:
26
+ allowedImports: []
27
+ features:
28
+ allowedImports: [domain, shared]
29
+ ui:
30
+ allowedImports: [features, shared]
31
+ shared:
32
+ allowedImports: []
33
+
34
+ uiLayers: [ui]
35
+
36
+ rules:
37
+ enforceLayerBoundaries: true
38
+ noCircularLayerDeps: true
39
+ noBusinessLogicInComponents: true
40
+ noDataFetchingInUI: true
41
+ `;
42
+ const FEATURE_SLICED_REACT_CONFIG = `layers:
43
+ app:
44
+ allowedImports: [pages, features, entities, shared]
45
+ pages:
46
+ allowedImports: [features, entities, shared]
47
+ features:
48
+ allowedImports: [entities, shared]
49
+ entities:
50
+ allowedImports: [shared]
51
+ shared:
52
+ allowedImports: []
53
+
54
+ uiLayers: [app, pages]
55
+
56
+ rules:
57
+ enforceLayerBoundaries: true
58
+ noCircularLayerDeps: true
59
+ noBusinessLogicInComponents: true
60
+ noDataFetchingInUI: true
61
+ `;
62
+ /** Human-readable preset list for CLI errors and help text. */
63
+ function formatPresetList() {
64
+ return exports.PRESET_NAMES.join(", ");
65
+ }
66
+ /**
67
+ * Resolve preset name to architecture.yaml contents.
68
+ * Omit preset for boundaries-only (v2 default).
69
+ */
70
+ function getPresetConfig(preset) {
71
+ if (!preset || preset === "boundaries-only") {
72
+ return BOUNDARIES_ONLY_CONFIG;
73
+ }
74
+ switch (preset) {
75
+ case "react-layered":
76
+ return REACT_LAYERED_CONFIG;
77
+ case "feature-sliced-react":
78
+ return FEATURE_SLICED_REACT_CONFIG;
79
+ default:
80
+ throw new Error(`Unknown preset: ${preset}. Available: ${formatPresetList()}`);
81
+ }
82
+ }
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@lifesavertech/archguard-cli",
3
+ "version": "2.0.0",
4
+ "description": "Command-line interface for ArchGuard",
5
+ "bin": {
6
+ "archguard": "./dist/index.js"
7
+ },
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "engines": {
12
+ "node": ">=20"
13
+ },
14
+ "keywords": [
15
+ "archguard",
16
+ "architecture",
17
+ "react",
18
+ "typescript",
19
+ "cli"
20
+ ],
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "scripts": {
25
+ "build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc -b --force && node -e \"require('fs').chmodSync('dist/index.js', 0o755)\"",
26
+ "prepack": "npm run build",
27
+ "build:test": "node -e \"require('fs').rmSync('dist-test',{recursive:true,force:true})\" && tsc -p tsconfig.test.json",
28
+ "test": "npm run build:test && node --test dist-test/index.test.js dist-test/presets/presets.test.js"
29
+ },
30
+ "dependencies": {
31
+ "@lifesavertech/archguard-guidance": "2.0.0",
32
+ "@lifesavertech/archguard-core": "2.0.0",
33
+ "@lifesavertech/archguard-rules-react": "2.0.0",
34
+ "commander": "^11.0.0",
35
+ "chalk": "^5.3.0"
36
+ },
37
+ "devDependencies": {
38
+ "@types/node": "^20.0.0"
39
+ },
40
+ "author": "Lindsey Stead",
41
+ "homepage": "https://github.com/lindseystead/ArchGuard#readme",
42
+ "bugs": {
43
+ "url": "https://github.com/lindseystead/ArchGuard/issues"
44
+ },
45
+ "license": "MIT",
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "https://github.com/lindseystead/ArchGuard.git",
49
+ "directory": "packages/cli"
50
+ }
51
+ }