azcodr 2.2.0 → 2.4.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/.agents/scripts/agent_guard.js +63 -0
- package/.agents/skills/lets-build/SKILL.md +1 -1
- package/.github/workflows/ci.yml +21 -0
- package/.github/workflows/publish.yml +7 -0
- package/CODE_OF_CONDUCT.md +122 -0
- package/CONTRIBUTING.md +85 -0
- package/README.md +73 -3
- package/SECURITY.md +32 -0
- package/data/memory.template +36 -0
- package/docs/rules/clean_code.md +13 -0
- package/docs/tipping-points.md +37 -0
- package/lib/agent-guard-command.d.ts +28 -0
- package/lib/agent-guard-command.d.ts.map +1 -0
- package/lib/agent-guard-command.js +103 -0
- package/lib/agent-guard-command.js.map +1 -0
- package/lib/agent-guard-file.d.ts +32 -0
- package/lib/agent-guard-file.d.ts.map +1 -0
- package/lib/agent-guard-file.js +77 -0
- package/lib/agent-guard-file.js.map +1 -0
- package/lib/agent-guard-tdd.d.ts +34 -0
- package/lib/agent-guard-tdd.d.ts.map +1 -0
- package/lib/agent-guard-tdd.js +77 -0
- package/lib/agent-guard-tdd.js.map +1 -0
- package/lib/agent-guard.d.ts +38 -0
- package/lib/agent-guard.d.ts.map +1 -0
- package/lib/agent-guard.js +80 -0
- package/lib/agent-guard.js.map +1 -0
- package/lib/boundaries.d.ts +41 -0
- package/lib/boundaries.d.ts.map +1 -0
- package/lib/boundaries.js +158 -0
- package/lib/boundaries.js.map +1 -0
- package/lib/cli-boundaries.d.ts +18 -0
- package/lib/cli-boundaries.d.ts.map +1 -0
- package/lib/cli-boundaries.js +46 -0
- package/lib/cli-boundaries.js.map +1 -0
- package/lib/cli-parse.d.ts +5 -0
- package/lib/cli-parse.d.ts.map +1 -1
- package/lib/cli-parse.js +35 -5
- package/lib/cli-parse.js.map +1 -1
- package/lib/cli.d.ts +17 -0
- package/lib/cli.d.ts.map +1 -1
- package/lib/cli.js +47 -5
- package/lib/cli.js.map +1 -1
- package/lib/guards.d.ts +6 -0
- package/lib/guards.d.ts.map +1 -1
- package/lib/guards.js +10 -1
- package/lib/guards.js.map +1 -1
- package/lib/index.d.ts +15 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +12 -0
- package/lib/index.js.map +1 -1
- package/lib/repo.js +4 -4
- package/lib/repo.js.map +1 -1
- package/lib/scaffold.d.ts +2 -7
- package/lib/scaffold.d.ts.map +1 -1
- package/lib/scaffold.js +7 -10
- package/lib/scaffold.js.map +1 -1
- package/lib/validate.d.ts +33 -0
- package/lib/validate.d.ts.map +1 -0
- package/lib/validate.js +24 -0
- package/lib/validate.js.map +1 -0
- package/memory.md +24 -0
- package/package.json +10 -2
- package/scripts/validate/links.js +0 -1
- package/scripts/validate/root.js +1 -1
- package/src/agent-guard-command.ts +118 -0
- package/src/agent-guard-file.ts +107 -0
- package/src/agent-guard-tdd.ts +96 -0
- package/src/agent-guard.ts +123 -0
- package/src/boundaries.ts +198 -0
- package/src/cli-boundaries.ts +67 -0
- package/src/cli-parse.ts +40 -4
- package/src/cli.ts +57 -15
- package/src/guards.ts +11 -1
- package/src/index.ts +40 -0
- package/src/repo.ts +4 -4
- package/src/scaffold.ts +7 -10
- package/src/validate.ts +47 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { inspectCommand } from './agent-guard-command.js';
|
|
3
|
+
import type { CommandViolation } from './agent-guard-command.js';
|
|
4
|
+
import { inspectFileWrite } from './agent-guard-file.js';
|
|
5
|
+
import type { FileCheckResult } from './agent-guard-file.js';
|
|
6
|
+
import {
|
|
7
|
+
inspectTddRequirement,
|
|
8
|
+
readTddState,
|
|
9
|
+
writeTddState
|
|
10
|
+
} from './agent-guard-tdd.js';
|
|
11
|
+
import type { SessionTddState } from './agent-guard-tdd.js';
|
|
12
|
+
|
|
13
|
+
export interface GuardDecision {
|
|
14
|
+
allowed: boolean;
|
|
15
|
+
reason?: string;
|
|
16
|
+
source?: 'command' | 'file' | 'tdd';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ToolEnvelope {
|
|
20
|
+
tool_name?: string;
|
|
21
|
+
tool_input?: Record<string, unknown>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface GuardOptions {
|
|
25
|
+
workspaceRoot?: string;
|
|
26
|
+
enforceTestFirst?: boolean;
|
|
27
|
+
maxFileLines?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function parseEnvelope(raw: string): ToolEnvelope | null {
|
|
31
|
+
try {
|
|
32
|
+
const trimmed = raw.trim();
|
|
33
|
+
if (!trimmed.startsWith('{')) return null;
|
|
34
|
+
return JSON.parse(trimmed) as ToolEnvelope;
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function extractKey(input: Record<string, unknown>, keys: readonly string[]): string | undefined {
|
|
41
|
+
for (const k of keys) {
|
|
42
|
+
if (typeof input[k] === 'string') return input[k];
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function extractFilePath(input: Record<string, unknown>): string | undefined {
|
|
48
|
+
return extractKey(input, ['TargetFile', 'file_path', 'path', 'filePath']);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function extractCommand(input: Record<string, unknown>): string | undefined {
|
|
52
|
+
return extractKey(input, ['CommandLine', 'command', 'cmd']);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function inspectPreCommand(cmd: string): GuardDecision {
|
|
56
|
+
const violation = inspectCommand(cmd);
|
|
57
|
+
if (violation.blocked) {
|
|
58
|
+
return { allowed: false, reason: violation.reason, source: 'command' };
|
|
59
|
+
}
|
|
60
|
+
return { allowed: true };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function inspectPreFile(
|
|
64
|
+
filePath: string,
|
|
65
|
+
input: Record<string, unknown>,
|
|
66
|
+
options: GuardOptions
|
|
67
|
+
): GuardDecision {
|
|
68
|
+
const incoming = extractKey(input, ['CodeContent', 'content']);
|
|
69
|
+
const replacement = extractKey(input, ['ReplacementContent', 'replacement']);
|
|
70
|
+
const target = extractKey(input, ['TargetContent', 'target']);
|
|
71
|
+
|
|
72
|
+
const fileDecision = inspectFileWrite({
|
|
73
|
+
filePath,
|
|
74
|
+
incomingContent: incoming,
|
|
75
|
+
replacementContent: replacement,
|
|
76
|
+
targetContent: target,
|
|
77
|
+
maxLines: options.maxFileLines
|
|
78
|
+
});
|
|
79
|
+
if (fileDecision.blocked) {
|
|
80
|
+
return { allowed: false, reason: fileDecision.reason, source: 'file' };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const root = options.workspaceRoot ?? process.cwd();
|
|
84
|
+
const statePath = path.join(root, '.agents', '.session-state.json');
|
|
85
|
+
const state = readTddState(statePath);
|
|
86
|
+
const tddDecision = inspectTddRequirement(filePath, state, options.enforceTestFirst);
|
|
87
|
+
if (tddDecision.blocked) {
|
|
88
|
+
return { allowed: false, reason: tddDecision.reason, source: 'tdd' };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return { allowed: true };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Inspects a tool-use request before execution (PreToolUse hook).
|
|
96
|
+
*
|
|
97
|
+
* @param rawInput - Raw command string, JSON envelope, or argv slice.
|
|
98
|
+
* @param options - Guard configuration options.
|
|
99
|
+
* @returns Guard decision indicating whether tool execution is permitted.
|
|
100
|
+
*/
|
|
101
|
+
export function inspectPreTool(rawInput: string, options: GuardOptions = {}): GuardDecision {
|
|
102
|
+
const env = parseEnvelope(rawInput);
|
|
103
|
+
|
|
104
|
+
if (env && env.tool_input) {
|
|
105
|
+
const cmd = extractCommand(env.tool_input);
|
|
106
|
+
if (cmd) return inspectPreCommand(cmd);
|
|
107
|
+
|
|
108
|
+
const targetFile = extractFilePath(env.tool_input);
|
|
109
|
+
if (targetFile) return inspectPreFile(targetFile, env.tool_input, options);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return inspectPreCommand(rawInput);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export {
|
|
116
|
+
inspectCommand,
|
|
117
|
+
inspectFileWrite,
|
|
118
|
+
inspectTddRequirement,
|
|
119
|
+
readTddState,
|
|
120
|
+
writeTddState
|
|
121
|
+
};
|
|
122
|
+
export type { CommandViolation, FileCheckResult, SessionTddState };
|
|
123
|
+
export default { inspectPreTool, inspectCommand, inspectFileWrite, inspectTddRequirement };
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export interface BoundaryRule {
|
|
5
|
+
fromLayer: string;
|
|
6
|
+
disallowImportsFrom: readonly string[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface BoundaryViolation {
|
|
10
|
+
file: string;
|
|
11
|
+
importedFile: string;
|
|
12
|
+
fromLayer: string;
|
|
13
|
+
toLayer: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface BoundaryReport {
|
|
17
|
+
moduleCount: number;
|
|
18
|
+
cycles: string[][];
|
|
19
|
+
violations: BoundaryViolation[];
|
|
20
|
+
ok: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const DEFAULT_BOUNDARY_RULES: readonly BoundaryRule[] = [
|
|
24
|
+
{
|
|
25
|
+
fromLayer: 'domain',
|
|
26
|
+
disallowImportsFrom: ['infrastructure', 'infra', 'adapters', 'cli', 'transport', 'controllers']
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
fromLayer: 'core',
|
|
30
|
+
disallowImportsFrom: ['infrastructure', 'infra', 'adapters', 'presentation', 'ui']
|
|
31
|
+
}
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
function isSkippedDir(name: string): boolean {
|
|
35
|
+
return name.startsWith('.') || name === 'node_modules' || name === 'lib';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isEligibleSource(name: string): boolean {
|
|
39
|
+
const isScript = name.endsWith('.ts') || name.endsWith('.js');
|
|
40
|
+
if (!isScript || name.endsWith('.d.ts')) return false;
|
|
41
|
+
return !name.includes('.test.') && !name.includes('.spec.');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function findSourceFiles(dir: string): string[] {
|
|
45
|
+
if (!fs.existsSync(dir)) return [];
|
|
46
|
+
const results: string[] = [];
|
|
47
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
48
|
+
|
|
49
|
+
for (const entry of entries) {
|
|
50
|
+
if (isSkippedDir(entry.name)) continue;
|
|
51
|
+
const full = path.join(dir, entry.name);
|
|
52
|
+
if (entry.isDirectory()) {
|
|
53
|
+
results.push(...findSourceFiles(full));
|
|
54
|
+
} else if (isEligibleSource(entry.name)) {
|
|
55
|
+
results.push(path.resolve(full));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return results.sort();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function resolveImportTarget(sourceFile: string, specifier: string): string | null {
|
|
63
|
+
const dir = path.dirname(sourceFile);
|
|
64
|
+
const candidate = path.resolve(dir, specifier);
|
|
65
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
66
|
+
|
|
67
|
+
if (specifier.endsWith('.js')) {
|
|
68
|
+
const tsCandidate = path.resolve(dir, specifier.slice(0, -3) + '.ts');
|
|
69
|
+
if (fs.existsSync(tsCandidate)) return tsCandidate;
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function extractLocalImports(filePath: string): string[] {
|
|
75
|
+
if (!fs.existsSync(filePath)) return [];
|
|
76
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
77
|
+
const imports: string[] = [];
|
|
78
|
+
const regex = /(?:import|export)\s+(?:(?:[\s\S]*?from\s+)|)['"](\.[^'"]+)['"]|import\s*\(\s*['"](\.[^'"]+)['"]\s*\)/g;
|
|
79
|
+
|
|
80
|
+
for (const match of content.matchAll(regex)) {
|
|
81
|
+
const spec = match[1] ?? match[2];
|
|
82
|
+
if (spec) {
|
|
83
|
+
const resolved = resolveImportTarget(filePath, spec);
|
|
84
|
+
if (resolved && !imports.includes(resolved)) {
|
|
85
|
+
imports.push(resolved);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return imports.sort();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function buildDependencyGraph(files: readonly string[]): Map<string, string[]> {
|
|
94
|
+
const graph = new Map<string, string[]>();
|
|
95
|
+
for (const file of files) {
|
|
96
|
+
graph.set(file, extractLocalImports(file));
|
|
97
|
+
}
|
|
98
|
+
return graph;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function detectDependencyCycles(graph: Map<string, string[]>): string[][] {
|
|
102
|
+
const cycles: string[][] = [];
|
|
103
|
+
const visited = new Set<string>();
|
|
104
|
+
const inStack = new Set<string>();
|
|
105
|
+
const pathStack: string[] = [];
|
|
106
|
+
|
|
107
|
+
function dfs(node: string): void {
|
|
108
|
+
visited.add(node);
|
|
109
|
+
inStack.add(node);
|
|
110
|
+
pathStack.push(node);
|
|
111
|
+
|
|
112
|
+
for (const neighbor of graph.get(node) ?? []) {
|
|
113
|
+
if (!visited.has(neighbor)) {
|
|
114
|
+
dfs(neighbor);
|
|
115
|
+
} else if (inStack.has(neighbor)) {
|
|
116
|
+
const startIdx = pathStack.indexOf(neighbor);
|
|
117
|
+
if (startIdx !== -1) {
|
|
118
|
+
cycles.push([...pathStack.slice(startIdx), neighbor]);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
inStack.delete(node);
|
|
124
|
+
pathStack.pop();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
for (const node of graph.keys()) {
|
|
128
|
+
if (!visited.has(node)) dfs(node);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return cycles;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function hasLayerSegment(pathStr: string, segment: string): boolean {
|
|
135
|
+
return new RegExp(`(^|[/\\\\])${segment}([/\\\\]|$)`, 'i').test(pathStr);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function detectBoundaryViolations(
|
|
139
|
+
graph: Map<string, string[]>,
|
|
140
|
+
rules: readonly BoundaryRule[] = DEFAULT_BOUNDARY_RULES
|
|
141
|
+
): BoundaryViolation[] {
|
|
142
|
+
const violations: BoundaryViolation[] = [];
|
|
143
|
+
|
|
144
|
+
for (const [file, imports] of graph.entries()) {
|
|
145
|
+
for (const rule of rules) {
|
|
146
|
+
if (hasLayerSegment(file, rule.fromLayer)) {
|
|
147
|
+
for (const imported of imports) {
|
|
148
|
+
for (const disallowed of rule.disallowImportsFrom) {
|
|
149
|
+
if (hasLayerSegment(imported, disallowed)) {
|
|
150
|
+
violations.push({
|
|
151
|
+
file,
|
|
152
|
+
importedFile: imported,
|
|
153
|
+
fromLayer: rule.fromLayer,
|
|
154
|
+
toLayer: disallowed
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return violations;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Inspects a source tree for architectural boundary violations and circular dependency cycles.
|
|
168
|
+
*
|
|
169
|
+
* @param sourceDir - Directory containing source code (e.g. src/).
|
|
170
|
+
* @param rules - Architectural boundary rules.
|
|
171
|
+
* @returns Report summarizing cycle and boundary findings.
|
|
172
|
+
*/
|
|
173
|
+
export function inspectBoundaries(
|
|
174
|
+
sourceDir: string,
|
|
175
|
+
rules: readonly BoundaryRule[] = DEFAULT_BOUNDARY_RULES
|
|
176
|
+
): BoundaryReport {
|
|
177
|
+
const files = findSourceFiles(sourceDir);
|
|
178
|
+
const graph = buildDependencyGraph(files);
|
|
179
|
+
const cycles = detectDependencyCycles(graph);
|
|
180
|
+
const violations = detectBoundaryViolations(graph, rules);
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
moduleCount: files.length,
|
|
184
|
+
cycles,
|
|
185
|
+
violations,
|
|
186
|
+
ok: cycles.length === 0 && violations.length === 0
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export default {
|
|
191
|
+
DEFAULT_BOUNDARY_RULES,
|
|
192
|
+
findSourceFiles,
|
|
193
|
+
extractLocalImports,
|
|
194
|
+
buildDependencyGraph,
|
|
195
|
+
detectDependencyCycles,
|
|
196
|
+
detectBoundaryViolations,
|
|
197
|
+
inspectBoundaries
|
|
198
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import { inspectBoundaries } from './boundaries.js';
|
|
4
|
+
import type { BoundaryReport } from './boundaries.js';
|
|
5
|
+
import type { CliParsedOptions } from './cli-parse.js';
|
|
6
|
+
|
|
7
|
+
export interface BoundaryIo {
|
|
8
|
+
out: (msg: string) => void;
|
|
9
|
+
err: (msg: string) => void;
|
|
10
|
+
exit: (code: number) => void | number;
|
|
11
|
+
cwd: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function resolveBoundaryTarget(targetDir: string | null, cwd: string): string {
|
|
15
|
+
if (targetDir) {
|
|
16
|
+
return path.resolve(cwd, targetDir);
|
|
17
|
+
}
|
|
18
|
+
const defaultSrc = path.join(cwd, 'src');
|
|
19
|
+
return fs.existsSync(defaultSrc) ? defaultSrc : cwd;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function printViolations(report: BoundaryReport, out: (msg: string) => void): void {
|
|
23
|
+
const total = report.cycles.length + report.violations.length;
|
|
24
|
+
out(`🚨 ARCHITECTURAL VIOLATIONS DETECTED (${total}):`);
|
|
25
|
+
for (const cycle of report.cycles) {
|
|
26
|
+
out(` - Circular Dependency: ${cycle.join(' -> ')}`);
|
|
27
|
+
}
|
|
28
|
+
for (const v of report.violations) {
|
|
29
|
+
out(` - Layer Boundary Breach: ${v.file} illegally imports ${v.importedFile} (${v.fromLayer} -> ${v.toLayer})`);
|
|
30
|
+
}
|
|
31
|
+
out(' Action Required: Invert dependencies via Ports or extract shared modules.\n');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function formatBoundaryReport(
|
|
35
|
+
targetDir: string,
|
|
36
|
+
report: BoundaryReport,
|
|
37
|
+
out: (msg: string) => void
|
|
38
|
+
): void {
|
|
39
|
+
out(`\n🔍 Inspecting Architectural Boundaries in: ${targetDir}`);
|
|
40
|
+
out('--------------------------------------------------------------');
|
|
41
|
+
if (report.ok) {
|
|
42
|
+
out(`✅ Analyzed ${report.moduleCount} modules: 0 circular cycles, 0 boundary violations.`);
|
|
43
|
+
out('🎉 SUCCESS: Architecture is clean and drift-free! (0 violations)\n');
|
|
44
|
+
} else {
|
|
45
|
+
printViolations(report, out);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function handleBoundariesCommand(
|
|
50
|
+
parsed: CliParsedOptions,
|
|
51
|
+
io: BoundaryIo
|
|
52
|
+
): Promise<void | number> {
|
|
53
|
+
const targetDir = resolveBoundaryTarget(parsed.targetDir, io.cwd);
|
|
54
|
+
const report = inspectBoundaries(targetDir);
|
|
55
|
+
|
|
56
|
+
if (!parsed.silent) {
|
|
57
|
+
formatBoundaryReport(targetDir, report, io.out);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return io.exit(report.ok ? 0 : 1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export default {
|
|
64
|
+
resolveBoundaryTarget,
|
|
65
|
+
formatBoundaryReport,
|
|
66
|
+
handleBoundariesCommand
|
|
67
|
+
};
|
package/src/cli-parse.ts
CHANGED
|
@@ -5,7 +5,10 @@
|
|
|
5
5
|
* raise the complexity of the parser itself.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
export type CliCommand = 'scaffold' | 'check' | 'boundaries';
|
|
9
|
+
|
|
8
10
|
export interface CliParsedOptions {
|
|
11
|
+
command: CliCommand;
|
|
9
12
|
targetDir: string | null;
|
|
10
13
|
force: boolean;
|
|
11
14
|
noGit: boolean;
|
|
@@ -18,9 +21,19 @@ export interface CliParsedOptions {
|
|
|
18
21
|
export type BooleanFlagKey = 'force' | 'noGit' | 'dryRun' | 'silent';
|
|
19
22
|
|
|
20
23
|
export function emptyOptions(): Omit<CliParsedOptions, 'terminal' | 'message'> {
|
|
21
|
-
return {
|
|
24
|
+
return {
|
|
25
|
+
command: 'scaffold',
|
|
26
|
+
targetDir: null,
|
|
27
|
+
force: false,
|
|
28
|
+
noGit: false,
|
|
29
|
+
dryRun: false,
|
|
30
|
+
silent: false
|
|
31
|
+
};
|
|
22
32
|
}
|
|
23
33
|
|
|
34
|
+
export const CHECK_COMMANDS = new Set(['check', 'validate', 'audit']);
|
|
35
|
+
export const BOUNDARY_COMMANDS = new Set(['boundaries', 'boundary', 'cycles']);
|
|
36
|
+
|
|
24
37
|
export const BOOLEAN_FLAGS = new Map<string, BooleanFlagKey>([
|
|
25
38
|
['-f', 'force'],
|
|
26
39
|
['--force', 'force'],
|
|
@@ -36,12 +49,34 @@ export interface ArgOutcome {
|
|
|
36
49
|
message?: string;
|
|
37
50
|
}
|
|
38
51
|
|
|
52
|
+
function checkSpecialFlag(arg: string): 'help' | 'version' | null {
|
|
53
|
+
if (arg === '-h' || arg === '--help') return 'help';
|
|
54
|
+
if (arg === '-v' || arg === '--version') return 'version';
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function checkCommandKeyword(
|
|
59
|
+
arg: string,
|
|
60
|
+
state: Omit<CliParsedOptions, 'terminal' | 'message'>
|
|
61
|
+
): boolean {
|
|
62
|
+
if (state.targetDir !== null || state.command !== 'scaffold') return false;
|
|
63
|
+
if (CHECK_COMMANDS.has(arg)) {
|
|
64
|
+
state.command = 'check';
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
if (BOUNDARY_COMMANDS.has(arg)) {
|
|
68
|
+
state.command = 'boundaries';
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
|
|
39
74
|
export function applyArg(
|
|
40
75
|
state: Omit<CliParsedOptions, 'terminal' | 'message'>,
|
|
41
76
|
arg: string
|
|
42
77
|
): ArgOutcome {
|
|
43
|
-
|
|
44
|
-
if (
|
|
78
|
+
const special = checkSpecialFlag(arg);
|
|
79
|
+
if (special !== null) return { terminal: special };
|
|
45
80
|
const flag = BOOLEAN_FLAGS.get(arg);
|
|
46
81
|
if (flag !== undefined) {
|
|
47
82
|
state[flag] = true;
|
|
@@ -53,6 +88,7 @@ export function applyArg(
|
|
|
53
88
|
message: `Unknown argument '${arg}'. Run 'npx azcodr --help' for available options.`
|
|
54
89
|
};
|
|
55
90
|
}
|
|
91
|
+
if (checkCommandKeyword(arg, state)) return { terminal: null };
|
|
56
92
|
if (!state.targetDir) {
|
|
57
93
|
state.targetDir = arg;
|
|
58
94
|
return { terminal: null };
|
|
@@ -74,4 +110,4 @@ export function parseArgs(rawArgs: readonly string[]): CliParsedOptions {
|
|
|
74
110
|
return { ...state, terminal: null, message: undefined };
|
|
75
111
|
}
|
|
76
112
|
|
|
77
|
-
export default { emptyOptions, BOOLEAN_FLAGS, applyArg, parseArgs };
|
|
113
|
+
export default { emptyOptions, BOOLEAN_FLAGS, CHECK_COMMANDS, applyArg, parseArgs };
|
package/src/cli.ts
CHANGED
|
@@ -4,6 +4,8 @@ import process from 'node:process';
|
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { scaffold, getTemplateDir } from './scaffold.js';
|
|
6
6
|
import type { ScaffoldOptions, ScaffoldResult } from './scaffold.js';
|
|
7
|
+
import { validate, createSilentReporter } from './validate.js';
|
|
8
|
+
import { handleBoundariesCommand } from './cli-boundaries.js';
|
|
7
9
|
import { parseArgs } from './cli-parse.js';
|
|
8
10
|
import type { CliParsedOptions } from './cli-parse.js';
|
|
9
11
|
import { askQuestion, resolveTargetDir, ensureWritableTarget } from './cli-target.js';
|
|
@@ -26,9 +28,13 @@ Enterprise Multi-Tenant Architecture & Agentic Engineering Starter Template
|
|
|
26
28
|
|
|
27
29
|
Usage:
|
|
28
30
|
npx azcodr [directory] [options]
|
|
31
|
+
npx azcodr check [directory] [options]
|
|
32
|
+
npx azcodr boundaries [directory] [options]
|
|
29
33
|
|
|
30
34
|
Commands:
|
|
31
|
-
[directory]
|
|
35
|
+
[directory] Scaffold azcodr template into directory (default: current directory)
|
|
36
|
+
check [directory] Validate architecture and governance rules (alias: validate, audit)
|
|
37
|
+
boundaries [directory] Inspect dependency cycles and layer boundaries (alias: boundary, cycles)
|
|
32
38
|
|
|
33
39
|
Options:
|
|
34
40
|
-d, --dry-run Simulate scaffolding without modifying filesystem
|
|
@@ -40,8 +46,10 @@ Options:
|
|
|
40
46
|
|
|
41
47
|
Examples:
|
|
42
48
|
npx azcodr my-project
|
|
49
|
+
npx azcodr check
|
|
50
|
+
npx azcodr boundaries ./src
|
|
51
|
+
npx azcodr check ./existing-repo
|
|
43
52
|
npx azcodr . --dry-run
|
|
44
|
-
npx azcodr . --force
|
|
45
53
|
`);
|
|
46
54
|
}
|
|
47
55
|
|
|
@@ -58,6 +66,8 @@ export interface CliIo {
|
|
|
58
66
|
cwd?: string;
|
|
59
67
|
templateDir?: string;
|
|
60
68
|
scaffold?: (options?: ScaffoldOptions) => ScaffoldResult;
|
|
69
|
+
validate?: (workspaceRoot: string, reporter?: any) => { errors: number; warnings: number } | Promise<{ errors: number; warnings: number }>;
|
|
70
|
+
boundaries?: (parsed: CliParsedOptions, io: any) => Promise<void | number>;
|
|
61
71
|
}
|
|
62
72
|
|
|
63
73
|
export interface NormalizedCliIo {
|
|
@@ -69,20 +79,30 @@ export interface NormalizedCliIo {
|
|
|
69
79
|
cwd: string;
|
|
70
80
|
templateDir: string;
|
|
71
81
|
scaffoldFn: (options?: ScaffoldOptions) => ScaffoldResult;
|
|
82
|
+
validateFn: (workspaceRoot: string, reporter?: any) => { errors: number; warnings: number } | Promise<{ errors: number; warnings: number }>;
|
|
83
|
+
boundariesFn: (parsed: CliParsedOptions, io: any) => Promise<void | number>;
|
|
72
84
|
}
|
|
73
85
|
|
|
74
|
-
function
|
|
75
|
-
|
|
76
|
-
out
|
|
77
|
-
err
|
|
78
|
-
exit
|
|
79
|
-
stdin
|
|
80
|
-
stdout
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
+
function defaultStreams(io: CliIo) {
|
|
87
|
+
return {
|
|
88
|
+
out: io.out ?? console.log,
|
|
89
|
+
err: io.err ?? console.error,
|
|
90
|
+
exit: io.exit ?? process.exit,
|
|
91
|
+
stdin: io.stdin ?? process.stdin,
|
|
92
|
+
stdout: io.stdout ?? process.stdout
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function normalizeIo(io: CliIo): NormalizedCliIo {
|
|
97
|
+
const streams = defaultStreams(io);
|
|
98
|
+
return {
|
|
99
|
+
...streams,
|
|
100
|
+
cwd: io.cwd ?? process.cwd(),
|
|
101
|
+
templateDir: io.templateDir ?? getTemplateDir(),
|
|
102
|
+
scaffoldFn: io.scaffold ?? scaffold,
|
|
103
|
+
validateFn: io.validate ?? validate,
|
|
104
|
+
boundariesFn: io.boundaries ?? handleBoundariesCommand
|
|
105
|
+
};
|
|
86
106
|
}
|
|
87
107
|
|
|
88
108
|
function outBanner(fullIo: NormalizedCliIo): void {
|
|
@@ -103,6 +123,22 @@ function handleTerminal(parsed: CliParsedOptions, io: NormalizedCliIo): void | n
|
|
|
103
123
|
return exit(1);
|
|
104
124
|
}
|
|
105
125
|
|
|
126
|
+
async function handleCheckCommand(
|
|
127
|
+
parsed: CliParsedOptions,
|
|
128
|
+
fullIo: NormalizedCliIo
|
|
129
|
+
): Promise<void | number> {
|
|
130
|
+
const checkDir = parsed.targetDir ? path.resolve(fullIo.cwd, parsed.targetDir) : fullIo.cwd;
|
|
131
|
+
const reporter = parsed.silent ? createSilentReporter() : undefined;
|
|
132
|
+
const result = await fullIo.validateFn(checkDir, reporter);
|
|
133
|
+
if (result.errors !== 0) return fullIo.exit(1);
|
|
134
|
+
|
|
135
|
+
const srcDir = path.join(checkDir, 'src');
|
|
136
|
+
if (fs.existsSync(srcDir)) {
|
|
137
|
+
return fullIo.boundariesFn({ ...parsed, targetDir: srcDir }, fullIo);
|
|
138
|
+
}
|
|
139
|
+
return fullIo.exit(0);
|
|
140
|
+
}
|
|
141
|
+
|
|
106
142
|
function reportDryRun(result: ScaffoldResult, out: (msg: string) => void): void {
|
|
107
143
|
for (const action of result.actions) {
|
|
108
144
|
out(` [preview] ${action}`);
|
|
@@ -199,10 +235,16 @@ export async function runCli(
|
|
|
199
235
|
io: CliIo = {}
|
|
200
236
|
): Promise<void | number> {
|
|
201
237
|
const fullIo = normalizeIo(io);
|
|
202
|
-
|
|
203
238
|
const parsed = parseArgs(rawArgs);
|
|
204
239
|
if (parsed.terminal !== null) return handleTerminal(parsed, fullIo);
|
|
205
240
|
|
|
241
|
+
if (parsed.command === 'check') {
|
|
242
|
+
return handleCheckCommand(parsed, fullIo);
|
|
243
|
+
}
|
|
244
|
+
if (parsed.command === 'boundaries') {
|
|
245
|
+
return fullIo.boundariesFn(parsed, fullIo);
|
|
246
|
+
}
|
|
247
|
+
|
|
206
248
|
if (!parsed.silent) {
|
|
207
249
|
outBanner(fullIo);
|
|
208
250
|
}
|
package/src/guards.ts
CHANGED
|
@@ -2,8 +2,18 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import process from 'node:process';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
5
6
|
import { ScaffoldError, ERROR_CODES } from './errors.js';
|
|
6
|
-
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Resolves the root filesystem directory of the azcodr template files.
|
|
10
|
+
*
|
|
11
|
+
* @returns Absolute filesystem path to the template root directory.
|
|
12
|
+
*/
|
|
13
|
+
export function getTemplateDir(): string {
|
|
14
|
+
const currentDir = import.meta.dirname ?? path.dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
return path.resolve(currentDir, '..');
|
|
16
|
+
}
|
|
7
17
|
|
|
8
18
|
/**
|
|
9
19
|
* Options for configuring protected target verification.
|
package/src/index.ts
CHANGED
|
@@ -39,6 +39,46 @@ export {
|
|
|
39
39
|
ERROR_CODES
|
|
40
40
|
} from './scaffold.js';
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Standalone architecture validation engine.
|
|
44
|
+
*/
|
|
45
|
+
export { validate, createSilentReporter } from './validate.js';
|
|
46
|
+
export type { ValidationResult, ValidationReporter } from './validate.js';
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Agent-runtime architectural enforcement and safety guards.
|
|
50
|
+
*/
|
|
51
|
+
export {
|
|
52
|
+
inspectPreTool,
|
|
53
|
+
inspectCommand,
|
|
54
|
+
inspectFileWrite,
|
|
55
|
+
inspectTddRequirement,
|
|
56
|
+
readTddState,
|
|
57
|
+
writeTddState
|
|
58
|
+
} from './agent-guard.js';
|
|
59
|
+
export type {
|
|
60
|
+
GuardDecision,
|
|
61
|
+
GuardOptions,
|
|
62
|
+
ToolEnvelope,
|
|
63
|
+
CommandViolation,
|
|
64
|
+
FileCheckResult,
|
|
65
|
+
SessionTddState
|
|
66
|
+
} from './agent-guard.js';
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Architectural boundary enforcement and dependency cycle prevention.
|
|
70
|
+
*/
|
|
71
|
+
export {
|
|
72
|
+
inspectBoundaries,
|
|
73
|
+
detectDependencyCycles,
|
|
74
|
+
detectBoundaryViolations,
|
|
75
|
+
findSourceFiles,
|
|
76
|
+
extractLocalImports,
|
|
77
|
+
buildDependencyGraph,
|
|
78
|
+
DEFAULT_BOUNDARY_RULES
|
|
79
|
+
} from './boundaries.js';
|
|
80
|
+
export type { BoundaryRule, BoundaryViolation, BoundaryReport } from './boundaries.js';
|
|
81
|
+
|
|
42
82
|
/**
|
|
43
83
|
* Type definitions and option interfaces for scaffolding operations.
|
|
44
84
|
*/
|
package/src/repo.ts
CHANGED
|
@@ -59,10 +59,10 @@ function commitWithFallbackIdentity(targetDir: string): void {
|
|
|
59
59
|
...options,
|
|
60
60
|
env: {
|
|
61
61
|
...process.env,
|
|
62
|
-
GIT_AUTHOR_NAME: '
|
|
63
|
-
GIT_AUTHOR_EMAIL: '
|
|
64
|
-
GIT_COMMITTER_NAME: '
|
|
65
|
-
GIT_COMMITTER_EMAIL: '
|
|
62
|
+
GIT_AUTHOR_NAME: 'azcodr[bot]',
|
|
63
|
+
GIT_AUTHOR_EMAIL: 'bot@azcodr.internal',
|
|
64
|
+
GIT_COMMITTER_NAME: 'azcodr[bot]',
|
|
65
|
+
GIT_COMMITTER_EMAIL: 'bot@azcodr.internal'
|
|
66
66
|
}
|
|
67
67
|
});
|
|
68
68
|
}
|