@boyingliu01/xp-gate 0.21.0 → 0.22.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/adapters/gate-9.sh +142 -60
- package/bin/xp-gate.js +2 -2
- package/gate-9.sh +142 -59
- package/hooks/gate-9.sh +142 -59
- package/hooks/lib/coverage-total.sh +72 -0
- package/hooks/pre-commit +88 -27
- package/hooks/pre-push +32 -0
- package/lib/__tests__/arch.test.js +403 -0
- package/lib/__tests__/doctor-469-hook-env.test.js +200 -0
- package/lib/arch.js +336 -49
- package/lib/doctor.js +319 -13
- package/lib/mattpocock-license-verdict.json +1 -1
- package/package.json +1 -1
- package/plugins/dsh/src/command.test.ts +14 -2
- package/plugins/dsh/src/command.ts +8 -1
- package/plugins/dsh/src/index.test.ts +43 -26
- package/plugins/dsh/src/index.ts +164 -138
- package/plugins/opencode/README.md +5 -2
- package/plugins/opencode/index.ts +215 -164
package/lib/arch.js
CHANGED
|
@@ -4,78 +4,365 @@ const { spawnSync } = require('child_process');
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
// #447: `xp-gate arch` used to shell out to
|
|
8
|
+
// npx -y @archlinter/cli scan . --config architecture.yaml
|
|
9
|
+
// which failed twice over: `-y` pulls whatever the registry serves (version
|
|
10
|
+
// drift — REQ-4: "本机能跑、别人不能跑"), and this repo's pre-0.16
|
|
11
|
+
// `architecture.yaml` `rules:` block is rejected by archlint 0.16's own
|
|
12
|
+
// validator. The default path now runs the PINNED local @archlinter/cli with
|
|
13
|
+
// its built-in rules — the same rule source Gate 6 (pre-commit) uses — with
|
|
14
|
+
// baseline-ratchet verdicts when `.architecture-baseline.json` exists. The old
|
|
15
|
+
// config-driven path survives as the explicit opt-in `--use-config <file>`;
|
|
16
|
+
// `--config` remains as a deprecated alias.
|
|
17
|
+
|
|
18
|
+
const ARCHLINT_PACKAGE = '@archlinter/cli';
|
|
19
|
+
const BASELINE_FILE = '.architecture-baseline.json';
|
|
20
|
+
// Blocking threshold, aligned with Gate 6's pre-commit ratchet
|
|
21
|
+
// (`archlint diff .architecture-baseline.json --fail-on high`).
|
|
22
|
+
const FAIL_ON = 'high';
|
|
23
|
+
const BLOCKING_SEVERITIES = new Set(['high', 'critical']);
|
|
24
|
+
const MAX_OUTPUT_BYTES = 64 * 1024 * 1024;
|
|
25
|
+
const BLOCKING_PREVIEW_LIMIT = 10;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Windows extended-length prefixes (`\\?\C:\...`, `\\?\UNC\server\share\...`) —
|
|
29
|
+
* strip for readable output. UNC first: its prefix also starts with `\\?\`.
|
|
30
|
+
*/
|
|
31
|
+
function stripExtendedPrefix(filePath) {
|
|
32
|
+
return String(filePath)
|
|
33
|
+
.replace(/^\\\\\?\\UNC\\/, '\\\\')
|
|
34
|
+
.replace(/^\\\\\?\\/, '');
|
|
10
35
|
}
|
|
11
36
|
|
|
12
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Walk up from startDir to the git root. Falls back to startDir when no .git
|
|
39
|
+
* marker exists (Gate 6 uses the same PROJECT_ROOT notion).
|
|
40
|
+
*/
|
|
41
|
+
function findProjectRoot(startDir) {
|
|
13
42
|
let dir = path.resolve(startDir);
|
|
14
|
-
const
|
|
15
|
-
while (dir !==
|
|
16
|
-
|
|
17
|
-
if (fs.existsSync(candidate)) return candidate;
|
|
18
|
-
if (fs.existsSync(path.join(dir, '.git'))) return null;
|
|
43
|
+
const fsRoot = path.parse(dir).root;
|
|
44
|
+
while (dir !== fsRoot) {
|
|
45
|
+
if (fs.existsSync(path.join(dir, '.git'))) return dir;
|
|
19
46
|
dir = path.dirname(dir);
|
|
20
47
|
}
|
|
21
|
-
return
|
|
48
|
+
return path.resolve(startDir);
|
|
22
49
|
}
|
|
23
50
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Absolute path to the JS bin entry of an @archlinter/cli installation, or
|
|
53
|
+
* null when the directory is not a usable installation. The bin field is
|
|
54
|
+
* either a string or a map; the entry must exist on disk.
|
|
55
|
+
*/
|
|
56
|
+
function binEntryFromPackageDir(pkgDir) {
|
|
57
|
+
const manifestPath = path.join(pkgDir, 'package.json');
|
|
58
|
+
if (!fs.existsSync(manifestPath)) return null;
|
|
59
|
+
let manifest;
|
|
60
|
+
try {
|
|
61
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
33
64
|
}
|
|
34
|
-
|
|
65
|
+
const bin = typeof manifest.bin === 'string' ? { [manifest.name]: manifest.bin } : manifest.bin;
|
|
66
|
+
if (!bin || typeof bin !== 'object') return null;
|
|
67
|
+
const entry = bin.archlint || Object.values(bin)[0];
|
|
68
|
+
if (!entry || typeof entry !== 'string') return null;
|
|
69
|
+
const entryPath = path.resolve(pkgDir, entry);
|
|
70
|
+
return fs.existsSync(entryPath) ? entryPath : null;
|
|
35
71
|
}
|
|
36
72
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
73
|
+
/** xp-gate's own pinned dependency: walk up from this file to its node_modules. */
|
|
74
|
+
function ownDependencyEntry() {
|
|
75
|
+
let dir = __dirname;
|
|
76
|
+
for (;;) {
|
|
77
|
+
const entry = binEntryFromPackageDir(path.join(dir, 'node_modules', ARCHLINT_PACKAGE));
|
|
78
|
+
if (entry) return entry;
|
|
79
|
+
const parent = path.dirname(dir);
|
|
80
|
+
if (parent === dir) return null;
|
|
81
|
+
dir = parent;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** First match of `name` on PATH, or null. */
|
|
86
|
+
function findOnPath(name) {
|
|
87
|
+
const lookup = spawnSync(process.platform === 'win32' ? 'where' : 'which', [name], {
|
|
88
|
+
encoding: 'utf8',
|
|
41
89
|
});
|
|
42
|
-
if (
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
90
|
+
if (lookup.status !== 0 || !lookup.stdout) return null;
|
|
91
|
+
const first = lookup.stdout.split(/\r?\n/).map((l) => l.trim()).find(Boolean);
|
|
92
|
+
return first && fs.existsSync(first) ? first : null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Resolve which archlint to run, in pin-priority order (REQ-4):
|
|
97
|
+
* 1. xp-gate's own pinned dependency (version fixed by xp-gate's package.json)
|
|
98
|
+
* 2. the target project's local node_modules install
|
|
99
|
+
* 3. an `archlint` on PATH
|
|
100
|
+
* 4. `npx --no-install @archlinter/cli` (local-only; NEVER `npx -y`)
|
|
101
|
+
* `includeOwnDependency: false` skips step 1 (test seam for the lower tiers).
|
|
102
|
+
*/
|
|
103
|
+
function resolveArchlint(projectRoot, options = {}) {
|
|
104
|
+
const includeOwnDependency = options.includeOwnDependency !== false;
|
|
105
|
+
if (includeOwnDependency) {
|
|
106
|
+
const own = ownDependencyEntry();
|
|
107
|
+
if (own) return { kind: 'node-script', script: own, source: 'xp-gate dependency (pinned)' };
|
|
108
|
+
}
|
|
109
|
+
const local = binEntryFromPackageDir(path.join(projectRoot, 'node_modules', ARCHLINT_PACKAGE));
|
|
110
|
+
if (local) return { kind: 'node-script', script: local, source: 'project node_modules' };
|
|
111
|
+
const onPath = findOnPath('archlint');
|
|
112
|
+
if (onPath) return { kind: 'binary', bin: onPath, source: 'PATH' };
|
|
113
|
+
return { kind: 'npx', source: 'npx --no-install (local only)' };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Quote an argv element for cmd.exe when shell mode is unavoidable.
|
|
118
|
+
* Embedded double quotes are doubled (cmd's in-quote escape) and anything
|
|
119
|
+
* carrying whitespace OR a quote is wrapped, so `a"b.yaml` cannot terminate
|
|
120
|
+
* the quoting early and hand the rest to the shell. NOTE: this exists only
|
|
121
|
+
* for the binary/npx fallback tiers on win32 — the default pinned path
|
|
122
|
+
* spawns node directly against the JS entry with no shell at all.
|
|
123
|
+
*/
|
|
124
|
+
function quoteForShell(arg) {
|
|
125
|
+
const s = String(arg);
|
|
126
|
+
if (/[\s"]/.test(s)) return `"${s.replace(/"/g, '""')}"`;
|
|
127
|
+
return s;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Run the resolved archlint. `capture` pipes stdout/stderr for parsing;
|
|
132
|
+
* otherwise output is inherited so the user sees archlint's own report.
|
|
133
|
+
* Returns { ok, status, stdout, stderr }; ok:false means the tool never ran.
|
|
134
|
+
*/
|
|
135
|
+
function runArchlint(resolved, args, { cwd, capture = false } = {}) {
|
|
136
|
+
let command;
|
|
137
|
+
let commandArgs;
|
|
138
|
+
let useShell = false;
|
|
139
|
+
if (resolved.kind === 'node-script') {
|
|
140
|
+
// Spawn node against the JS entry: cross-platform, no .cmd/.ps1 shims.
|
|
141
|
+
command = process.execPath;
|
|
142
|
+
commandArgs = [resolved.script, ...args];
|
|
143
|
+
} else if (resolved.kind === 'binary') {
|
|
144
|
+
command = resolved.bin;
|
|
145
|
+
commandArgs = args;
|
|
146
|
+
useShell = process.platform === 'win32';
|
|
147
|
+
} else {
|
|
148
|
+
command = 'npx';
|
|
149
|
+
commandArgs = ['--no-install', ARCHLINT_PACKAGE, ...args];
|
|
150
|
+
useShell = process.platform === 'win32';
|
|
151
|
+
}
|
|
152
|
+
if (useShell) {
|
|
153
|
+
command = quoteForShell(command);
|
|
154
|
+
commandArgs = commandArgs.map(quoteForShell);
|
|
155
|
+
}
|
|
156
|
+
const result = spawnSync(command, commandArgs, {
|
|
157
|
+
cwd,
|
|
158
|
+
encoding: 'utf8',
|
|
159
|
+
stdio: capture ? ['ignore', 'pipe', 'pipe'] : 'inherit',
|
|
160
|
+
shell: useShell,
|
|
161
|
+
maxBuffer: MAX_OUTPUT_BYTES,
|
|
162
|
+
});
|
|
163
|
+
if (result.error) {
|
|
164
|
+
if (result.error.code === 'ENOENT') {
|
|
165
|
+
console.error(`[xp-gate arch] ERROR: archlint not found (${resolved.source}).`);
|
|
166
|
+
console.error(` Fix: npm install --save-dev ${ARCHLINT_PACKAGE} in your project,`);
|
|
167
|
+
console.error(' or reinstall xp-gate when its own pinned copy is the missing one.');
|
|
168
|
+
} else {
|
|
169
|
+
console.error(`[xp-gate arch] ERROR: failed to run archlint (${resolved.source}): ${result.error.message}`);
|
|
170
|
+
}
|
|
171
|
+
return { ok: false, status: 1, stdout: '', stderr: '' };
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
ok: true,
|
|
175
|
+
status: result.status === null ? 1 : result.status,
|
|
176
|
+
stdout: result.stdout || '',
|
|
177
|
+
stderr: result.stderr || '',
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Parse `archlint scan --format json` output into a verdict.
|
|
183
|
+
* Fail-closed: unparseable output is reported as an error, never as "clean".
|
|
184
|
+
*/
|
|
185
|
+
function verdictFromScanJson(jsonText) {
|
|
186
|
+
let parsed;
|
|
187
|
+
try {
|
|
188
|
+
parsed = JSON.parse(jsonText);
|
|
189
|
+
} catch {
|
|
190
|
+
return { parseError: 'archlint JSON output could not be parsed', total: 0, blocking: 0, blockingDetails: [] };
|
|
191
|
+
}
|
|
192
|
+
// A payload that parses but carries no `smells` array is schema drift, not a
|
|
193
|
+
// clean tree: archlint 0.16.0 always emits `smells` on a successful JSON scan
|
|
194
|
+
// (an empty run reports []). Reading the absence as "zero findings" would
|
|
195
|
+
// silently PASS a renamed or restructured report — the exact no-op failure
|
|
196
|
+
// class this rewrite exists to kill. Fail closed instead.
|
|
197
|
+
if (!parsed || !Array.isArray(parsed.smells)) {
|
|
198
|
+
return { parseError: 'archlint JSON output has no smells array (schema drift?)', total: 0, blocking: 0, blockingDetails: [] };
|
|
199
|
+
}
|
|
200
|
+
const smells = parsed.smells;
|
|
201
|
+
let blocking = 0;
|
|
202
|
+
const blockingDetails = [];
|
|
203
|
+
for (const smell of smells) {
|
|
204
|
+
const severity = String((smell && smell.severity) || '').toLowerCase();
|
|
205
|
+
if (BLOCKING_SEVERITIES.has(severity)) {
|
|
206
|
+
blocking += 1;
|
|
207
|
+
if (blockingDetails.length < BLOCKING_PREVIEW_LIMIT) blockingDetails.push(smell);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
parseError: null,
|
|
212
|
+
total: smells.length,
|
|
213
|
+
blocking,
|
|
214
|
+
blockingDetails,
|
|
215
|
+
summary: (parsed && parsed.summary) || null,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function printScanSummary(verdict) {
|
|
220
|
+
const grade = verdict.summary && verdict.summary.architecture_grade;
|
|
221
|
+
if (grade && grade.score !== undefined) {
|
|
222
|
+
console.log(`[xp-gate arch] Architecture quality: ${grade.score}/10 (${grade.level})`);
|
|
223
|
+
}
|
|
224
|
+
console.log(`[xp-gate arch] Findings: ${verdict.total} total, ${verdict.blocking} high/critical`);
|
|
225
|
+
for (const smell of verdict.blockingDetails) {
|
|
226
|
+
const files = Array.isArray(smell.files) ? smell.files.map(stripExtendedPrefix) : [];
|
|
227
|
+
console.error(` - [${smell.severity}] ${smell.type}${files.length ? ` @ ${files.join(', ')}` : ''}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Default path: built-in archlint rules, no config.
|
|
233
|
+
* - `.architecture-baseline.json` present → baseline-ratchet mode, the exact
|
|
234
|
+
* verdict semantics of Gate 6's pre-commit path (`diff --fail-on high`):
|
|
235
|
+
* only NEW high/critical findings block, so an adopted legacy tree stays green.
|
|
236
|
+
* - no baseline → zero-tolerance mode: any high/critical finding blocks.
|
|
237
|
+
* Tool failures (spawn error, non-zero scan exit, unparseable JSON) always
|
|
238
|
+
* exit non-zero — a broken scan must never read as a clean one.
|
|
239
|
+
*/
|
|
240
|
+
function runBuiltinScan(projectRoot, resolved) {
|
|
241
|
+
const baselinePath = path.join(projectRoot, BASELINE_FILE);
|
|
242
|
+
if (fs.existsSync(baselinePath)) {
|
|
243
|
+
console.log(`[xp-gate arch] Baseline ratchet mode — ${BASELINE_FILE} present; blocking new ${FAIL_ON}+ findings only`);
|
|
244
|
+
const diff = runArchlint(resolved, ['diff', baselinePath, '--fail-on', FAIL_ON], { cwd: projectRoot });
|
|
245
|
+
if (!diff.ok) return 1;
|
|
246
|
+
// `--fail-on high` makes the exit code itself the verdict contract: 0 = no
|
|
247
|
+
// new high+ findings, 1 = at least one. This is the exact mechanism Gate
|
|
248
|
+
// 6's pre-commit has used since before #447 (issue evidence: "Gate 6
|
|
249
|
+
// actually uses: archlint diff .architecture-baseline.json --fail-on high
|
|
250
|
+
// ✅ normal"), so parity here is the point, not a shortcut. Anything other
|
|
251
|
+
// than 0/1 is a tool failure and blocks.
|
|
252
|
+
if (diff.status === 0) {
|
|
253
|
+
console.log(`✅ arch: no new ${FAIL_ON}+ architecture findings (baseline ratchet)`);
|
|
254
|
+
return 0;
|
|
255
|
+
}
|
|
256
|
+
if (diff.status === 1) {
|
|
257
|
+
console.error(`❌ BLOCKED - new ${FAIL_ON}+ architecture findings (baseline ratchet mode)`);
|
|
258
|
+
console.error(' Fix the findings above, or refresh the baseline with:');
|
|
259
|
+
console.error(` npx ${ARCHLINT_PACKAGE} snapshot -o ${BASELINE_FILE}`);
|
|
50
260
|
return 1;
|
|
51
261
|
}
|
|
52
|
-
|
|
262
|
+
console.error(`❌ ERROR - archlint diff exited ${diff.status}; tool failure is treated as blocking (never a silent pass)`);
|
|
263
|
+
return 1;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
console.log('[xp-gate arch] Zero-tolerance mode — no .architecture-baseline.json; blocking any high/critical finding');
|
|
267
|
+
const scan = runArchlint(resolved, ['scan', '.', '--format', 'json', '--quiet'], {
|
|
268
|
+
cwd: projectRoot,
|
|
269
|
+
capture: true,
|
|
270
|
+
});
|
|
271
|
+
if (!scan.ok) return 1;
|
|
272
|
+
// Empirically verified against @archlinter/cli 0.16.0: the JSON scan exits 1
|
|
273
|
+
// exactly when high/critical findings exist (0 findings -> 0, medium-only ->
|
|
274
|
+
// 0, any high -> 1); the table format always exits 0. Any other non-zero
|
|
275
|
+
// status is a tool failure, never a verdict. The observation is bounded to
|
|
276
|
+
// the PINNED release (step 1 of resolveArchlint, REQ-4), and
|
|
277
|
+
// __tests__/arch.test.js re-runs that real binary, so a semantic change
|
|
278
|
+
// fails tests instead of silently misreading the gate.
|
|
279
|
+
if (scan.status !== 0 && scan.status !== 1) {
|
|
280
|
+
console.error(`❌ ERROR - archlint scan exited ${scan.status}; tool failure is treated as blocking (never a silent pass)`);
|
|
281
|
+
if (scan.stderr) process.stderr.write(scan.stderr);
|
|
282
|
+
return 1;
|
|
283
|
+
}
|
|
284
|
+
const verdict = verdictFromScanJson(scan.stdout);
|
|
285
|
+
if (verdict.parseError) {
|
|
286
|
+
console.error(`❌ ERROR - ${verdict.parseError}; tool failure is treated as blocking (never a silent pass)`);
|
|
287
|
+
return 1;
|
|
288
|
+
}
|
|
289
|
+
printScanSummary(verdict);
|
|
290
|
+
if (verdict.blocking > 0) {
|
|
291
|
+
console.error(`❌ BLOCKED - ${verdict.blocking} high/critical architecture finding(s) (zero-tolerance mode)`);
|
|
292
|
+
return 1;
|
|
53
293
|
}
|
|
54
|
-
|
|
294
|
+
if (scan.status === 1) {
|
|
295
|
+
// archlint signalled failure but the parsed report shows no high/critical
|
|
296
|
+
// finding — never silently pass on a verdict we cannot reproduce.
|
|
297
|
+
console.error('❌ ERROR - archlint exited 1 but no high/critical finding was parsed; treating as blocking (fail-closed)');
|
|
298
|
+
return 1;
|
|
299
|
+
}
|
|
300
|
+
console.log(`✅ arch: no high/critical architecture findings (zero-tolerance mode; ${verdict.total} finding(s) reported)`);
|
|
301
|
+
return 0;
|
|
55
302
|
}
|
|
56
303
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
304
|
+
/**
|
|
305
|
+
* Opt-in legacy path (`--use-config <file>`): the file is passed straight to
|
|
306
|
+
* archlint as `--config`. archlint validates its own schema, so pre-0.16 files
|
|
307
|
+
* (this repo's architecture.yaml `rules:` block) are rejected by archlint
|
|
308
|
+
* itself — schema-version responsibility sits with the user who opts in.
|
|
309
|
+
*/
|
|
310
|
+
function runWithConfig(configPath, projectRoot, resolved) {
|
|
311
|
+
console.warn(`[xp-gate arch] NOTE: opt-in config path — passing ${configPath} to archlint --config.`);
|
|
312
|
+
console.warn(' archlint validates its own config schema; pre-0.16 files (e.g. an architecture.yaml');
|
|
313
|
+
console.warn(' with a `rules:` block) are rejected by archlint itself. The default built-in-rules');
|
|
314
|
+
console.warn(' path needs no config at all.');
|
|
315
|
+
const scan = runArchlint(resolved, ['scan', '.', '--config', configPath], { cwd: projectRoot });
|
|
316
|
+
if (!scan.ok) return 1;
|
|
317
|
+
return scan.status;
|
|
318
|
+
}
|
|
61
319
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
320
|
+
function arch(args) {
|
|
321
|
+
const useConfigIdx = args.indexOf('--use-config');
|
|
322
|
+
const legacyIdx = args.indexOf('--config');
|
|
323
|
+
if (useConfigIdx >= 0 && legacyIdx >= 0) {
|
|
324
|
+
console.error('[xp-gate arch] ERROR: pass either --use-config or --config, not both');
|
|
65
325
|
return Promise.resolve(1);
|
|
66
326
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
327
|
+
if (legacyIdx >= 0) {
|
|
328
|
+
console.warn('[xp-gate arch] NOTE: --config is deprecated; use --use-config (same behavior, clearer intent)');
|
|
329
|
+
}
|
|
330
|
+
const configIdx = useConfigIdx >= 0 ? useConfigIdx : legacyIdx;
|
|
331
|
+
const config = configIdx >= 0 ? args[configIdx + 1] : null;
|
|
332
|
+
if (configIdx >= 0 && (!config || config.startsWith('--'))) {
|
|
333
|
+
console.error(`[xp-gate arch] ERROR: ${useConfigIdx >= 0 ? '--use-config' : '--config'} requires a file path`);
|
|
70
334
|
return Promise.resolve(1);
|
|
71
335
|
}
|
|
72
336
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
337
|
+
const projectRoot = findProjectRoot(process.cwd());
|
|
338
|
+
const resolved = resolveArchlint(projectRoot);
|
|
339
|
+
|
|
340
|
+
if (config) {
|
|
341
|
+
const configPath = path.resolve(process.cwd(), config);
|
|
342
|
+
if (!fs.existsSync(configPath)) {
|
|
343
|
+
console.error(`[xp-gate arch] ERROR: config file not found: ${config}`);
|
|
344
|
+
return Promise.resolve(1);
|
|
345
|
+
}
|
|
346
|
+
return Promise.resolve(runWithConfig(configPath, projectRoot, resolved));
|
|
76
347
|
}
|
|
77
348
|
|
|
78
|
-
|
|
349
|
+
console.log(`[xp-gate arch] Project root: ${projectRoot}`);
|
|
350
|
+
console.log(`[xp-gate arch] archlint: ${resolved.source}${resolved.script ? ` (${resolved.script})` : ''}`);
|
|
351
|
+
return Promise.resolve(runBuiltinScan(projectRoot, resolved));
|
|
79
352
|
}
|
|
80
353
|
|
|
81
|
-
module.exports = {
|
|
354
|
+
module.exports = {
|
|
355
|
+
arch,
|
|
356
|
+
// Test seams (anti-noop verification needs to reach the resolution and
|
|
357
|
+
// verdict layers without shelling out; see __tests__/arch.test.js).
|
|
358
|
+
__internals: {
|
|
359
|
+
findProjectRoot,
|
|
360
|
+
binEntryFromPackageDir,
|
|
361
|
+
resolveArchlint,
|
|
362
|
+
verdictFromScanJson,
|
|
363
|
+
quoteForShell,
|
|
364
|
+
stripExtendedPrefix,
|
|
365
|
+
BLOCKING_SEVERITIES,
|
|
366
|
+
BASELINE_FILE,
|
|
367
|
+
},
|
|
368
|
+
};
|