@ijfw/install 1.3.1 → 1.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/CHANGELOG.md +19 -1
- package/dist/ijfw.js +1414 -149
- package/dist/install.js +43 -11
- package/dist/uninstall.js +40 -0
- package/package.json +1 -1
package/dist/ijfw.js
CHANGED
|
@@ -267,6 +267,17 @@ import { spawnSync as spawnSync3 } from "node:child_process";
|
|
|
267
267
|
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
268
268
|
import { join as join2 } from "node:path";
|
|
269
269
|
import { tmpdir } from "node:os";
|
|
270
|
+
function formatMessages(results, severity12) {
|
|
271
|
+
const out = [];
|
|
272
|
+
for (const file of results) {
|
|
273
|
+
for (const msg of file.messages || []) {
|
|
274
|
+
if (severity12 === "error" && msg.severity !== 2) continue;
|
|
275
|
+
if (severity12 === "warning" && msg.severity !== 1) continue;
|
|
276
|
+
out.push(`${file.filePath}:${msg.line}:${msg.column} ${msg.severity === 2 ? "error" : "warning"} ${msg.ruleId} -- ${msg.message}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return out;
|
|
280
|
+
}
|
|
270
281
|
async function run3(ctx) {
|
|
271
282
|
const t0 = Date.now();
|
|
272
283
|
const eslintVer = ctx.versions["eslint"] || "latest";
|
|
@@ -292,6 +303,7 @@ async function run3(ctx) {
|
|
|
292
303
|
import security from '${join2(tmpDir, "node_modules", "eslint-plugin-security", "index.js").replace(/\\/g, "/")}';
|
|
293
304
|
export default [
|
|
294
305
|
{
|
|
306
|
+
linterOptions: { reportUnusedDisableDirectives: 'off' },
|
|
295
307
|
files: ['installer/src/**/*.js', 'mcp-server/src/**/*.js'],
|
|
296
308
|
plugins: { security },
|
|
297
309
|
rules: ${JSON.stringify(RULES)},
|
|
@@ -303,13 +315,20 @@ export default [
|
|
|
303
315
|
const eslintBin = join2(tmpDir, "node_modules", ".bin", "eslint");
|
|
304
316
|
const res = spawnSync3(
|
|
305
317
|
eslintBin,
|
|
306
|
-
["--config", configPath, "installer/src/**/*.js", "mcp-server/src/**/*.js"],
|
|
318
|
+
["--no-config-lookup", "--config", configPath, "--format", "json", "installer/src/**/*.js", "mcp-server/src/**/*.js"],
|
|
307
319
|
{ encoding: "utf8", cwd: ctx.repoRoot, timeout: 6e4 }
|
|
308
320
|
);
|
|
309
321
|
const durationMs = Date.now() - t0;
|
|
310
322
|
const output = (res.stdout || "") + (res.stderr || "");
|
|
311
|
-
|
|
312
|
-
|
|
323
|
+
let results = [];
|
|
324
|
+
try {
|
|
325
|
+
results = JSON.parse(res.stdout || "[]");
|
|
326
|
+
} catch {
|
|
327
|
+
results = [];
|
|
328
|
+
}
|
|
329
|
+
const errorCount = results.reduce((sum, file) => sum + (file.errorCount || 0), 0);
|
|
330
|
+
const warningCount = results.reduce((sum, file) => sum + (file.warningCount || 0), 0);
|
|
331
|
+
if (res.status === 0 && errorCount === 0 && warningCount === 0) {
|
|
313
332
|
return {
|
|
314
333
|
name: "eslint-security",
|
|
315
334
|
status: "PASS",
|
|
@@ -318,20 +337,20 @@ export default [
|
|
|
318
337
|
durationMs
|
|
319
338
|
};
|
|
320
339
|
}
|
|
321
|
-
if (res.status ===
|
|
340
|
+
if (errorCount > 0 || res.status === 2) {
|
|
322
341
|
return {
|
|
323
342
|
name: "eslint-security",
|
|
324
|
-
status: "
|
|
325
|
-
message:
|
|
326
|
-
details:
|
|
343
|
+
status: "FAIL",
|
|
344
|
+
message: `eslint-security: ${errorCount || "unknown"} security error(s) found`,
|
|
345
|
+
details: (formatMessages(results, "error").length ? formatMessages(results, "error") : output.split("\n").filter(Boolean)).slice(0, 30),
|
|
327
346
|
durationMs
|
|
328
347
|
};
|
|
329
348
|
}
|
|
330
349
|
return {
|
|
331
350
|
name: "eslint-security",
|
|
332
|
-
status: "
|
|
333
|
-
message:
|
|
334
|
-
details:
|
|
351
|
+
status: "WARN",
|
|
352
|
+
message: `eslint-security: ${warningCount} advisory warning(s)`,
|
|
353
|
+
details: (formatMessages(results, "warning").length ? formatMessages(results, "warning") : output.split("\n").filter(Boolean)).slice(0, 30),
|
|
335
354
|
durationMs
|
|
336
355
|
};
|
|
337
356
|
} finally {
|
|
@@ -350,11 +369,7 @@ var init_eslint_security = __esm({
|
|
|
350
369
|
"src/preflight/gates/eslint-security.js"() {
|
|
351
370
|
RULES = {
|
|
352
371
|
"security/detect-eval-with-expression": "error",
|
|
353
|
-
"security/detect-non-literal-fs-filename": "warn",
|
|
354
|
-
"security/detect-non-literal-regexp": "warn",
|
|
355
372
|
"security/detect-non-literal-require": "warn",
|
|
356
|
-
"security/detect-object-injection": "warn",
|
|
357
|
-
"security/detect-possible-timing-attacks": "warn",
|
|
358
373
|
"security/detect-pseudoRandomBytes": "error",
|
|
359
374
|
"security/detect-unsafe-regex": "error"
|
|
360
375
|
};
|
|
@@ -367,15 +382,15 @@ var init_eslint_security = __esm({
|
|
|
367
382
|
// src/preflight/gates/psscriptanalyzer.js
|
|
368
383
|
var psscriptanalyzer_exports = {};
|
|
369
384
|
__export(psscriptanalyzer_exports, {
|
|
385
|
+
fallbackAnalyzePowerShellText: () => fallbackAnalyzePowerShellText,
|
|
370
386
|
name: () => name4,
|
|
371
387
|
parallel: () => parallel4,
|
|
372
388
|
run: () => run4,
|
|
373
389
|
severity: () => severity4
|
|
374
390
|
});
|
|
375
391
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
376
|
-
import { readdirSync as readdirSync2, statSync as statSync2 } from "node:fs";
|
|
392
|
+
import { readFileSync, readdirSync as readdirSync2, statSync as statSync2 } from "node:fs";
|
|
377
393
|
import { join as join3 } from "node:path";
|
|
378
|
-
import { platform } from "node:os";
|
|
379
394
|
function findPs1Files(dir, acc = []) {
|
|
380
395
|
let entries;
|
|
381
396
|
try {
|
|
@@ -397,6 +412,109 @@ function findPs1Files(dir, acc = []) {
|
|
|
397
412
|
}
|
|
398
413
|
return acc;
|
|
399
414
|
}
|
|
415
|
+
function stripPowerShellComments(source) {
|
|
416
|
+
let out = "";
|
|
417
|
+
let i = 0;
|
|
418
|
+
let inSingle = false;
|
|
419
|
+
let inDouble = false;
|
|
420
|
+
let inBlockComment = false;
|
|
421
|
+
while (i < source.length) {
|
|
422
|
+
const ch = source[i];
|
|
423
|
+
const next = source[i + 1];
|
|
424
|
+
if (inBlockComment) {
|
|
425
|
+
if (ch === "#" && next === ">") {
|
|
426
|
+
inBlockComment = false;
|
|
427
|
+
i += 2;
|
|
428
|
+
} else {
|
|
429
|
+
if (ch === "\n") out += "\n";
|
|
430
|
+
i += 1;
|
|
431
|
+
}
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
if (!inSingle && !inDouble && ch === "<" && next === "#") {
|
|
435
|
+
inBlockComment = true;
|
|
436
|
+
i += 2;
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
if (!inSingle && !inDouble && ch === "#") {
|
|
440
|
+
while (i < source.length && source[i] !== "\n") i += 1;
|
|
441
|
+
out += "\n";
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
out += ch;
|
|
445
|
+
if (!inDouble && ch === "'") inSingle = !inSingle;
|
|
446
|
+
else if (!inSingle && ch === '"' && source[i - 1] !== "`") inDouble = !inDouble;
|
|
447
|
+
i += 1;
|
|
448
|
+
}
|
|
449
|
+
return out;
|
|
450
|
+
}
|
|
451
|
+
function bracketIssues(source, file) {
|
|
452
|
+
const pairs = { "(": ")", "[": "]", "{": "}" };
|
|
453
|
+
const closers = new Set(Object.values(pairs));
|
|
454
|
+
const stack = [];
|
|
455
|
+
let inSingle = false;
|
|
456
|
+
let inDouble = false;
|
|
457
|
+
for (let i = 0; i < source.length; i++) {
|
|
458
|
+
const ch = source[i];
|
|
459
|
+
if (!inDouble && ch === "'") {
|
|
460
|
+
inSingle = !inSingle;
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
if (!inSingle && ch === '"' && source[i - 1] !== "`") {
|
|
464
|
+
inDouble = !inDouble;
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
if (inSingle || inDouble) continue;
|
|
468
|
+
if (pairs[ch]) stack.push({ ch, index: i });
|
|
469
|
+
else if (closers.has(ch)) {
|
|
470
|
+
const open = stack.pop();
|
|
471
|
+
if (!open || pairs[open.ch] !== ch) return [`${file}: unbalanced bracket near offset ${i}`];
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
if (inSingle || inDouble) return [`${file}: unterminated string literal`];
|
|
475
|
+
if (stack.length > 0) return [`${file}: unclosed bracket near offset ${stack[stack.length - 1].index}`];
|
|
476
|
+
return [];
|
|
477
|
+
}
|
|
478
|
+
function fallbackAnalyzePowerShellText(source, file = "<inline>") {
|
|
479
|
+
const stripped = stripPowerShellComments(source);
|
|
480
|
+
const issues = bracketIssues(stripped, file);
|
|
481
|
+
const banned = [
|
|
482
|
+
[/\bInvoke-Expression\b|\biex\b/i, "Invoke-Expression/iex dynamic execution"],
|
|
483
|
+
[/\bSet-ExecutionPolicy\b[\s\S]{0,80}\bBypass\b/i, "Set-ExecutionPolicy Bypass"],
|
|
484
|
+
[/\bStart-Process\b[\s\S]{0,160}\b-Verb\s+RunAs\b/i, "Start-Process -Verb RunAs elevation"],
|
|
485
|
+
[/\bNew-Object\s+Net\.WebClient\b|\bDownloadString\s*\(/i, "legacy WebClient/DownloadString network execution"]
|
|
486
|
+
];
|
|
487
|
+
for (const [re2, label] of banned) {
|
|
488
|
+
if (re2.test(stripped)) issues.push(`${file}: banned PowerShell pattern: ${label}`);
|
|
489
|
+
}
|
|
490
|
+
return issues;
|
|
491
|
+
}
|
|
492
|
+
function runFallback(files, t0, reason) {
|
|
493
|
+
const issues = [];
|
|
494
|
+
for (const file of files) {
|
|
495
|
+
try {
|
|
496
|
+
issues.push(...fallbackAnalyzePowerShellText(readFileSync(file, "utf8"), file));
|
|
497
|
+
} catch (e) {
|
|
498
|
+
issues.push(`${file}: could not read script (${e.message || e})`);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
if (issues.length > 0) {
|
|
502
|
+
return {
|
|
503
|
+
name: "psscriptanalyzer",
|
|
504
|
+
status: "FAIL",
|
|
505
|
+
message: `PowerShell fallback found issues in ${files.length} script(s)`,
|
|
506
|
+
details: [reason, ...issues].slice(0, 20),
|
|
507
|
+
durationMs: Date.now() - t0
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
return {
|
|
511
|
+
name: "psscriptanalyzer",
|
|
512
|
+
status: "PASS",
|
|
513
|
+
message: `${files.length} PowerShell script(s) clean (static fallback; ${reason})`,
|
|
514
|
+
details: [],
|
|
515
|
+
durationMs: Date.now() - t0
|
|
516
|
+
};
|
|
517
|
+
}
|
|
400
518
|
async function run4(ctx) {
|
|
401
519
|
const t0 = Date.now();
|
|
402
520
|
const files = findPs1Files(ctx.repoRoot);
|
|
@@ -411,14 +529,15 @@ async function run4(ctx) {
|
|
|
411
529
|
}
|
|
412
530
|
const which = spawnSync4("pwsh", ["--version"], { encoding: "utf8" });
|
|
413
531
|
if (which.status === null || which.error) {
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
532
|
+
return runFallback(files, t0, "pwsh unavailable");
|
|
533
|
+
}
|
|
534
|
+
const moduleCheck = spawnSync4(
|
|
535
|
+
"pwsh",
|
|
536
|
+
["-NoProfile", "-NonInteractive", "-Command", "if (Get-Module -ListAvailable -Name PSScriptAnalyzer) { exit 0 } else { exit 3 }"],
|
|
537
|
+
{ encoding: "utf8", cwd: ctx.repoRoot, timeout: 1e4 }
|
|
538
|
+
);
|
|
539
|
+
if (moduleCheck.status !== 0) {
|
|
540
|
+
return runFallback(files, t0, "PSScriptAnalyzer module unavailable");
|
|
422
541
|
}
|
|
423
542
|
const script = `
|
|
424
543
|
$files = @(${files.map((f) => `'${f.replace(/'/g, "''")}'`).join(",")})
|
|
@@ -450,10 +569,9 @@ if ($found) { exit 1 } else { exit 0 }
|
|
|
450
569
|
};
|
|
451
570
|
}
|
|
452
571
|
const lines = ((res.stdout || "") + (res.stderr || "")).split("\n").filter(Boolean);
|
|
453
|
-
const isWin = platform() === "win32";
|
|
454
572
|
return {
|
|
455
573
|
name: "psscriptanalyzer",
|
|
456
|
-
status:
|
|
574
|
+
status: "FAIL",
|
|
457
575
|
message: `PSScriptAnalyzer found issues in ${files.length} script(s)`,
|
|
458
576
|
details: lines.slice(0, 20),
|
|
459
577
|
durationMs
|
|
@@ -568,6 +686,996 @@ var init_gitleaks = __esm({
|
|
|
568
686
|
}
|
|
569
687
|
});
|
|
570
688
|
|
|
689
|
+
// ../mcp-server/src/gate-result-schema.js
|
|
690
|
+
function makeGateId(gate) {
|
|
691
|
+
if (typeof gate !== "string" || !GATE_NAME_PATTERN.test(gate)) {
|
|
692
|
+
throw new TypeError(
|
|
693
|
+
`makeGateId: invalid gate name "${gate}" \u2014 must match ${GATE_NAME_PATTERN}`
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
const safe = gate.replace(/:/g, "-");
|
|
697
|
+
const ts = Date.now();
|
|
698
|
+
const rand4 = Math.floor(Math.random() * 65536).toString(16).padStart(4, "0");
|
|
699
|
+
return `${safe}-${ts}-${rand4}`;
|
|
700
|
+
}
|
|
701
|
+
function isString(v2) {
|
|
702
|
+
return typeof v2 === "string";
|
|
703
|
+
}
|
|
704
|
+
function isNonNullObject(v2) {
|
|
705
|
+
return v2 !== null && typeof v2 === "object" && !Array.isArray(v2);
|
|
706
|
+
}
|
|
707
|
+
function validateGateResult(obj) {
|
|
708
|
+
const errors = [];
|
|
709
|
+
if (!isNonNullObject(obj)) {
|
|
710
|
+
return { valid: false, errors: ["root: must be an object"] };
|
|
711
|
+
}
|
|
712
|
+
if (obj.schema_version !== SCHEMA_VERSION) {
|
|
713
|
+
errors.push(
|
|
714
|
+
`schema_version: must equal "${SCHEMA_VERSION}", got ${JSON.stringify(obj.schema_version)}`
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
if (!isString(obj.gate)) {
|
|
718
|
+
errors.push("gate: must be a string");
|
|
719
|
+
} else if (!GATE_NAME_PATTERN.test(obj.gate)) {
|
|
720
|
+
errors.push(
|
|
721
|
+
`gate: "${obj.gate}" does not match ${GATE_NAME_PATTERN}`
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
if (!VALID_STATUSES.includes(obj.status)) {
|
|
725
|
+
errors.push(
|
|
726
|
+
`status: must be one of ${VALID_STATUSES.join("|")}, got ${JSON.stringify(obj.status)}`
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
if (!VALID_PROJECT_TYPES.includes(obj.project_type)) {
|
|
730
|
+
errors.push(
|
|
731
|
+
`project_type: must be one of ${VALID_PROJECT_TYPES.join("|")}, got ${JSON.stringify(obj.project_type)}`
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
if (!Array.isArray(obj.lenses)) {
|
|
735
|
+
errors.push("lenses: must be an array (empty for single-model gates)");
|
|
736
|
+
} else {
|
|
737
|
+
obj.lenses.forEach((lens, i) => {
|
|
738
|
+
if (!isNonNullObject(lens)) {
|
|
739
|
+
errors.push(`lenses[${i}]: must be an object`);
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
if (!isString(lens.model)) errors.push(`lenses[${i}].model: must be a string`);
|
|
743
|
+
if (!VALID_STATUSES.includes(lens.verdict)) {
|
|
744
|
+
errors.push(
|
|
745
|
+
`lenses[${i}].verdict: must be one of ${VALID_STATUSES.join("|")}`
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
if (typeof lens.confidence !== "number" || lens.confidence < 0 || lens.confidence > 1) {
|
|
749
|
+
errors.push(`lenses[${i}].confidence: must be number in [0,1]`);
|
|
750
|
+
}
|
|
751
|
+
if (!isString(lens.summary)) errors.push(`lenses[${i}].summary: must be a string`);
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
if (!Array.isArray(obj.affected_artifacts)) {
|
|
755
|
+
errors.push("affected_artifacts: must be an array");
|
|
756
|
+
} else {
|
|
757
|
+
obj.affected_artifacts.forEach((a, i) => {
|
|
758
|
+
if (!isNonNullObject(a)) {
|
|
759
|
+
errors.push(`affected_artifacts[${i}]: must be an object`);
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
if (!VALID_ARTIFACT_TYPES.includes(a.type)) {
|
|
763
|
+
errors.push(
|
|
764
|
+
`affected_artifacts[${i}].type: must be one of ${VALID_ARTIFACT_TYPES.join("|")}`
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
if (!isString(a.ref)) errors.push(`affected_artifacts[${i}].ref: must be a string`);
|
|
768
|
+
if (!isString(a.role)) errors.push(`affected_artifacts[${i}].role: must be a string`);
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
if (!isNonNullObject(obj.accounting)) {
|
|
772
|
+
errors.push("accounting: must be an object");
|
|
773
|
+
} else {
|
|
774
|
+
const a = obj.accounting;
|
|
775
|
+
if (typeof a.duration_ms !== "number" || a.duration_ms < 0) {
|
|
776
|
+
errors.push("accounting.duration_ms: must be a non-negative number");
|
|
777
|
+
}
|
|
778
|
+
if (typeof a.lenses_invoked !== "number" || a.lenses_invoked < 0 || !Number.isInteger(a.lenses_invoked)) {
|
|
779
|
+
errors.push("accounting.lenses_invoked: must be a non-negative integer");
|
|
780
|
+
}
|
|
781
|
+
if (a.cost_usd !== null && (typeof a.cost_usd !== "number" || a.cost_usd < 0)) {
|
|
782
|
+
errors.push("accounting.cost_usd: must be null or non-negative number");
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
if (!Array.isArray(obj.remediation)) {
|
|
786
|
+
errors.push("remediation: must be an array (may be empty)");
|
|
787
|
+
} else {
|
|
788
|
+
obj.remediation.forEach((r, i) => {
|
|
789
|
+
if (!isNonNullObject(r)) {
|
|
790
|
+
errors.push(`remediation[${i}]: must be an object`);
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
if (!isString(r.action)) errors.push(`remediation[${i}].action: must be a string`);
|
|
794
|
+
if (!isString(r.target)) errors.push(`remediation[${i}].target: must be a string`);
|
|
795
|
+
if (!isString(r.agent_recommended)) {
|
|
796
|
+
errors.push(`remediation[${i}].agent_recommended: must be a string`);
|
|
797
|
+
}
|
|
798
|
+
if (typeof r.confidence !== "number" || r.confidence < 0 || r.confidence > 1) {
|
|
799
|
+
errors.push(`remediation[${i}].confidence: must be number in [0,1]`);
|
|
800
|
+
}
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
if (obj.receipts_ref !== null && !isString(obj.receipts_ref)) {
|
|
804
|
+
errors.push("receipts_ref: must be a string or null");
|
|
805
|
+
}
|
|
806
|
+
if (obj.supersedes !== null && !isString(obj.supersedes)) {
|
|
807
|
+
errors.push("supersedes: must be a string or null");
|
|
808
|
+
}
|
|
809
|
+
if (!isString(obj.gate_id) || obj.gate_id.length === 0) {
|
|
810
|
+
errors.push("gate_id: must be a non-empty string");
|
|
811
|
+
} else if (isString(obj.gate)) {
|
|
812
|
+
const safeGate = obj.gate.replace(/:/g, "-");
|
|
813
|
+
if (!obj.gate_id.startsWith(safeGate + "-")) {
|
|
814
|
+
errors.push(
|
|
815
|
+
`gate_id: expected to start with "${safeGate}-" (colon-collapsed gate name)`
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
if (!isString(obj.emitted_at) || !ISO8601_PATTERN.test(obj.emitted_at)) {
|
|
820
|
+
errors.push("emitted_at: must be ISO-8601 string");
|
|
821
|
+
}
|
|
822
|
+
return { valid: errors.length === 0, errors };
|
|
823
|
+
}
|
|
824
|
+
function formatGateResult(obj) {
|
|
825
|
+
const json = JSON.stringify(obj, null, 2);
|
|
826
|
+
return "```gate-result\n" + json + "\n```";
|
|
827
|
+
}
|
|
828
|
+
var SCHEMA_VERSION, GATE_NAME_PATTERN, VALID_STATUSES, VALID_PROJECT_TYPES, VALID_ARTIFACT_TYPES, ISO8601_PATTERN;
|
|
829
|
+
var init_gate_result_schema = __esm({
|
|
830
|
+
"../mcp-server/src/gate-result-schema.js"() {
|
|
831
|
+
SCHEMA_VERSION = "1.0";
|
|
832
|
+
GATE_NAME_PATTERN = /^[a-z][a-z0-9-]*(:[a-z][a-z0-9-]*)?$/;
|
|
833
|
+
VALID_STATUSES = Object.freeze([
|
|
834
|
+
"PASS",
|
|
835
|
+
"CONDITIONAL",
|
|
836
|
+
"WARN",
|
|
837
|
+
"FLAG",
|
|
838
|
+
"FAIL"
|
|
839
|
+
]);
|
|
840
|
+
VALID_PROJECT_TYPES = Object.freeze([
|
|
841
|
+
"software",
|
|
842
|
+
"book",
|
|
843
|
+
"content",
|
|
844
|
+
"business",
|
|
845
|
+
"design",
|
|
846
|
+
"mixed",
|
|
847
|
+
"unknown"
|
|
848
|
+
]);
|
|
849
|
+
VALID_ARTIFACT_TYPES = Object.freeze([
|
|
850
|
+
"file",
|
|
851
|
+
"chapter",
|
|
852
|
+
"section",
|
|
853
|
+
"asset",
|
|
854
|
+
"persona",
|
|
855
|
+
"decision",
|
|
856
|
+
"component"
|
|
857
|
+
]);
|
|
858
|
+
ISO8601_PATTERN = // eslint-disable-next-line security/detect-unsafe-regex -- fixed-length anchored ISO 8601 shape; optional fractional + tz are non-overlapping
|
|
859
|
+
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})$/;
|
|
860
|
+
}
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
// ../mcp-server/src/scan-resume.js
|
|
864
|
+
import { existsSync, mkdirSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2, renameSync, unlinkSync, copyFileSync } from "fs";
|
|
865
|
+
import { join as join4 } from "path";
|
|
866
|
+
function statePath(projectRoot) {
|
|
867
|
+
return join4(String(projectRoot), ".ijfw", STATE_FILE);
|
|
868
|
+
}
|
|
869
|
+
function loadScanState(projectRoot) {
|
|
870
|
+
const path = statePath(projectRoot);
|
|
871
|
+
if (!existsSync(path)) return null;
|
|
872
|
+
try {
|
|
873
|
+
const raw = readFileSync2(path, "utf8");
|
|
874
|
+
const parsed = JSON.parse(raw);
|
|
875
|
+
if (parsed && typeof parsed === "object") return parsed;
|
|
876
|
+
} catch {
|
|
877
|
+
}
|
|
878
|
+
return null;
|
|
879
|
+
}
|
|
880
|
+
function writeScanState(projectRoot, state) {
|
|
881
|
+
const dir = join4(String(projectRoot), ".ijfw");
|
|
882
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
883
|
+
const finalPath = statePath(projectRoot);
|
|
884
|
+
const tmpPath = `${finalPath}.tmp.${process.pid}.${Date.now()}`;
|
|
885
|
+
const safe = {
|
|
886
|
+
scan_id: String(state.scan_id || ""),
|
|
887
|
+
started_at: String(state.started_at || (/* @__PURE__ */ new Date()).toISOString()),
|
|
888
|
+
last_path_walked: String(state.last_path_walked || ""),
|
|
889
|
+
files_scanned: Number.isFinite(state.files_scanned) ? state.files_scanned : 0,
|
|
890
|
+
total_estimate: Number.isFinite(state.total_estimate) ? state.total_estimate : 0,
|
|
891
|
+
attempts: Number.isFinite(state.attempts) ? state.attempts : 1,
|
|
892
|
+
incomplete: state.incomplete !== false,
|
|
893
|
+
session_id: state.session_id || null
|
|
894
|
+
};
|
|
895
|
+
if (state.partial && typeof state.partial === "object") {
|
|
896
|
+
safe.partial = state.partial;
|
|
897
|
+
}
|
|
898
|
+
writeFileSync2(tmpPath, JSON.stringify(safe, null, 2) + "\n", "utf8");
|
|
899
|
+
try {
|
|
900
|
+
renameSync(tmpPath, finalPath);
|
|
901
|
+
} catch (err) {
|
|
902
|
+
if (!err || err.code !== "EXDEV") throw err;
|
|
903
|
+
try {
|
|
904
|
+
copyFileSync(tmpPath, finalPath);
|
|
905
|
+
} finally {
|
|
906
|
+
try {
|
|
907
|
+
unlinkSync(tmpPath);
|
|
908
|
+
} catch {
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
return finalPath;
|
|
913
|
+
}
|
|
914
|
+
function lockPath(projectRoot) {
|
|
915
|
+
return join4(String(projectRoot), ".ijfw", LOCK_FILE);
|
|
916
|
+
}
|
|
917
|
+
function isPidAlive(pid) {
|
|
918
|
+
if (!Number.isFinite(pid) || pid <= 0) return false;
|
|
919
|
+
try {
|
|
920
|
+
process.kill(pid, 0);
|
|
921
|
+
return true;
|
|
922
|
+
} catch (err) {
|
|
923
|
+
if (err && err.code === "EPERM") return true;
|
|
924
|
+
return false;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
function reclaimIfStale(lp) {
|
|
928
|
+
if (!existsSync(lp)) return;
|
|
929
|
+
let raw;
|
|
930
|
+
try {
|
|
931
|
+
raw = readFileSync2(lp, "utf8");
|
|
932
|
+
} catch {
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
const lines = String(raw).split(/\r?\n/);
|
|
936
|
+
const pid = Number(lines[0]);
|
|
937
|
+
const ts = Number(lines[1]);
|
|
938
|
+
const ageOk = Number.isFinite(ts) && Date.now() - ts <= LOCK_STALE_MS;
|
|
939
|
+
if (isPidAlive(pid) && ageOk) return;
|
|
940
|
+
try {
|
|
941
|
+
unlinkSync(lp);
|
|
942
|
+
} catch {
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
function acquireScanLock(projectRoot) {
|
|
946
|
+
const dir = join4(String(projectRoot), ".ijfw");
|
|
947
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
948
|
+
const lp = lockPath(projectRoot);
|
|
949
|
+
reclaimIfStale(lp);
|
|
950
|
+
const payload = String(process.pid) + "\n" + String(Date.now()) + "\n";
|
|
951
|
+
try {
|
|
952
|
+
writeFileSync2(lp, payload, { encoding: "utf8", flag: "wx" });
|
|
953
|
+
} catch (err) {
|
|
954
|
+
if (err && err.code === "EEXIST") return null;
|
|
955
|
+
throw err;
|
|
956
|
+
}
|
|
957
|
+
let released = false;
|
|
958
|
+
return {
|
|
959
|
+
released: () => {
|
|
960
|
+
if (released) return;
|
|
961
|
+
released = true;
|
|
962
|
+
try {
|
|
963
|
+
unlinkSync(lp);
|
|
964
|
+
} catch {
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
function shouldResume(state) {
|
|
970
|
+
if (!state || typeof state !== "object") return false;
|
|
971
|
+
if (state.incomplete !== true) return false;
|
|
972
|
+
if (!state.started_at || typeof state.started_at !== "string") return false;
|
|
973
|
+
const startedMs = Date.parse(state.started_at);
|
|
974
|
+
if (!Number.isFinite(startedMs)) return false;
|
|
975
|
+
const ageMs = Date.now() - startedMs;
|
|
976
|
+
if (ageMs > STALENESS_MS) return false;
|
|
977
|
+
const attempts = Number.isFinite(state.attempts) ? state.attempts : 0;
|
|
978
|
+
if (attempts >= ATTEMPT_CAP) return false;
|
|
979
|
+
return true;
|
|
980
|
+
}
|
|
981
|
+
function clearScanState(projectRoot) {
|
|
982
|
+
const path = statePath(projectRoot);
|
|
983
|
+
if (existsSync(path)) {
|
|
984
|
+
try {
|
|
985
|
+
unlinkSync(path);
|
|
986
|
+
} catch {
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
var STATE_FILE, LOCK_FILE, STALENESS_MS, ATTEMPT_CAP, LOCK_STALE_MS;
|
|
991
|
+
var init_scan_resume = __esm({
|
|
992
|
+
"../mcp-server/src/scan-resume.js"() {
|
|
993
|
+
STATE_FILE = "scan-state.json";
|
|
994
|
+
LOCK_FILE = "scan-state.json.lock";
|
|
995
|
+
STALENESS_MS = 24 * 60 * 60 * 1e3;
|
|
996
|
+
ATTEMPT_CAP = 3;
|
|
997
|
+
LOCK_STALE_MS = 60 * 1e3;
|
|
998
|
+
}
|
|
999
|
+
});
|
|
1000
|
+
|
|
1001
|
+
// ../mcp-server/src/project-type-detector.js
|
|
1002
|
+
import {
|
|
1003
|
+
readFileSync as readFileSync3,
|
|
1004
|
+
writeFileSync as writeFileSync3,
|
|
1005
|
+
existsSync as existsSync2,
|
|
1006
|
+
readdirSync as readdirSync3,
|
|
1007
|
+
statSync as statSync3,
|
|
1008
|
+
renameSync as renameSync2,
|
|
1009
|
+
mkdirSync as mkdirSync2,
|
|
1010
|
+
unlinkSync as unlinkSync2,
|
|
1011
|
+
realpathSync,
|
|
1012
|
+
copyFileSync as copyFileSync2
|
|
1013
|
+
} from "fs";
|
|
1014
|
+
import { join as join5, extname, isAbsolute, resolve as pathResolve, dirname } from "path";
|
|
1015
|
+
import { fileURLToPath } from "url";
|
|
1016
|
+
import { createHash } from "crypto";
|
|
1017
|
+
function detect(projectRoot, options = {}) {
|
|
1018
|
+
const root = String(projectRoot || process.cwd());
|
|
1019
|
+
const c9Available = options.c9Available === false ? false : options.c9Available === true ? true : isC9AvailableSync();
|
|
1020
|
+
const maxFiles = Number.isFinite(options.maxFiles) && options.maxFiles > 0 ? options.maxFiles : MAX_FILES;
|
|
1021
|
+
const signals = [];
|
|
1022
|
+
const fallbackReason = c9Available ? null : "c9_unavailable";
|
|
1023
|
+
if (options.explicitType && DOMAINS.includes(String(options.explicitType))) {
|
|
1024
|
+
signals.push({ kind: "user_declaration", weight: 1, value: options.explicitType });
|
|
1025
|
+
return finalize({
|
|
1026
|
+
primary: options.explicitType,
|
|
1027
|
+
secondary: [],
|
|
1028
|
+
score: 1,
|
|
1029
|
+
signals,
|
|
1030
|
+
scanIncomplete: false,
|
|
1031
|
+
fallbackReason,
|
|
1032
|
+
treeHash: "",
|
|
1033
|
+
branchHash: branchHash(root)
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
const fmAgents = readFrontmatterType(join5(root, "AGENTS.md"));
|
|
1037
|
+
if (fmAgents && DOMAINS.includes(fmAgents)) {
|
|
1038
|
+
signals.push({ kind: "agents_md_frontmatter", weight: 0.9, value: fmAgents });
|
|
1039
|
+
}
|
|
1040
|
+
const fmBrief = readFrontmatterType(join5(root, ".ijfw", "memory", "brief.md"));
|
|
1041
|
+
if (fmBrief && DOMAINS.includes(fmBrief)) {
|
|
1042
|
+
signals.push({ kind: "brief_md_frontmatter", weight: 0.8, value: fmBrief });
|
|
1043
|
+
}
|
|
1044
|
+
const timeBudgetMs = resolveTimeBudgetMs(options);
|
|
1045
|
+
const walk = walkProject(root, { maxFiles, maxDepth: MAX_DEPTH, options, timeBudgetMs });
|
|
1046
|
+
const treeHash = fileTreeHash(walk.fingerprint);
|
|
1047
|
+
if (walk.manifestsFound.length > 0) {
|
|
1048
|
+
signals.push({
|
|
1049
|
+
kind: "manifest",
|
|
1050
|
+
weight: 0.9,
|
|
1051
|
+
manifests: walk.manifestsFound.slice(0, 6)
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
for (const d of walk.dirHits.book) signals.push({ kind: "dir_book", weight: 0.4, name: d });
|
|
1055
|
+
for (const d of walk.dirHits.content) signals.push({ kind: "dir_content", weight: 0.4, name: d });
|
|
1056
|
+
for (const d of walk.dirHits.business) signals.push({ kind: "dir_business", weight: 0.4, name: d });
|
|
1057
|
+
for (const d of walk.dirHits.design) signals.push({ kind: "dir_design", weight: 0.4, name: d });
|
|
1058
|
+
const totals = walk.extTotals;
|
|
1059
|
+
const totalClassified = Object.values(totals).reduce((a, b2) => a + b2, 0);
|
|
1060
|
+
if (totalClassified > 0) {
|
|
1061
|
+
for (const [domain, count] of Object.entries(totals)) {
|
|
1062
|
+
const ratio = count / totalClassified;
|
|
1063
|
+
if (ratio >= 0.05) {
|
|
1064
|
+
signals.push({
|
|
1065
|
+
kind: "file_extension_ratio",
|
|
1066
|
+
weight: 0.7,
|
|
1067
|
+
domain,
|
|
1068
|
+
ratio: Number(ratio.toFixed(3)),
|
|
1069
|
+
count
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
for (const hit of walk.patternHits) {
|
|
1075
|
+
signals.push({ kind: "filename_pattern", weight: hit.weight, domain: hit.domain, name: hit.name });
|
|
1076
|
+
}
|
|
1077
|
+
const scoreboard = scoreSignals(signals);
|
|
1078
|
+
const ranked = rankDomains(scoreboard);
|
|
1079
|
+
let primary;
|
|
1080
|
+
let secondary = [];
|
|
1081
|
+
let confidence;
|
|
1082
|
+
if (ranked.length === 0) {
|
|
1083
|
+
primary = "unknown";
|
|
1084
|
+
confidence = 0;
|
|
1085
|
+
} else {
|
|
1086
|
+
primary = ranked[0].domain;
|
|
1087
|
+
confidence = ranked[0].score;
|
|
1088
|
+
secondary = ranked.slice(1).filter((r) => r.score >= 0.4 && r.domain !== primary).map((r) => r.domain);
|
|
1089
|
+
if (ranked.length >= 2 && ranked[0].score >= 0.55 && ranked[1].score >= 0.5 && ranked[1].score / ranked[0].score >= 0.75) {
|
|
1090
|
+
const topTwo = [ranked[0].domain, ranked[1].domain];
|
|
1091
|
+
secondary = topTwo;
|
|
1092
|
+
primary = "mixed";
|
|
1093
|
+
confidence = Math.min(0.85, (ranked[0].score + ranked[1].score) / 2);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
const highTrust = signals.some(
|
|
1097
|
+
(s) => s.kind === "user_declaration" || s.kind === "agents_md_frontmatter" || s.kind === "brief_md_frontmatter"
|
|
1098
|
+
);
|
|
1099
|
+
if (!c9Available && !highTrust && confidence > 0.7) confidence = 0.7;
|
|
1100
|
+
const scanIncomplete = walk.incomplete;
|
|
1101
|
+
if (scanIncomplete) {
|
|
1102
|
+
const lock = acquireScanLock(root);
|
|
1103
|
+
if (lock) {
|
|
1104
|
+
try {
|
|
1105
|
+
const prior = loadScanState(root) || {};
|
|
1106
|
+
writeScanState(root, {
|
|
1107
|
+
scan_id: prior.scan_id || newScanId(),
|
|
1108
|
+
started_at: prior.started_at || (/* @__PURE__ */ new Date()).toISOString(),
|
|
1109
|
+
last_path_walked: walk.lastPathWalked,
|
|
1110
|
+
files_scanned: walk.filesScanned,
|
|
1111
|
+
total_estimate: walk.totalEstimate,
|
|
1112
|
+
attempts: (prior.attempts || 0) + 1,
|
|
1113
|
+
incomplete: true,
|
|
1114
|
+
session_id: options.sessionId || null,
|
|
1115
|
+
partial: snapshotPartial(walk)
|
|
1116
|
+
});
|
|
1117
|
+
} catch {
|
|
1118
|
+
} finally {
|
|
1119
|
+
lock.released();
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
} else {
|
|
1123
|
+
try {
|
|
1124
|
+
clearScanState(root);
|
|
1125
|
+
} catch {
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
return finalize({
|
|
1129
|
+
primary,
|
|
1130
|
+
secondary,
|
|
1131
|
+
score: confidence,
|
|
1132
|
+
signals,
|
|
1133
|
+
scanIncomplete,
|
|
1134
|
+
fallbackReason,
|
|
1135
|
+
treeHash,
|
|
1136
|
+
branchHash: branchHash(root)
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
function finalize({ primary, secondary, score, signals, scanIncomplete, fallbackReason, treeHash, branchHash: bh }) {
|
|
1140
|
+
const confidence = Number(Math.max(0, Math.min(1, score)).toFixed(3));
|
|
1141
|
+
const out = {
|
|
1142
|
+
type: primary,
|
|
1143
|
+
// single-label alias for hoist
|
|
1144
|
+
primary_type: primary,
|
|
1145
|
+
secondary_types: Array.isArray(secondary) ? secondary : [],
|
|
1146
|
+
confidence,
|
|
1147
|
+
scan_incomplete: !!scanIncomplete,
|
|
1148
|
+
detected_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1149
|
+
signals,
|
|
1150
|
+
fallback_reason: fallbackReason,
|
|
1151
|
+
file_tree_hash: treeHash || "",
|
|
1152
|
+
branch_hash: bh || ""
|
|
1153
|
+
};
|
|
1154
|
+
return out;
|
|
1155
|
+
}
|
|
1156
|
+
function readFrontmatterType(path) {
|
|
1157
|
+
if (!existsSync2(path)) return null;
|
|
1158
|
+
let src;
|
|
1159
|
+
try {
|
|
1160
|
+
src = readFileSync3(path, "utf8");
|
|
1161
|
+
} catch {
|
|
1162
|
+
return null;
|
|
1163
|
+
}
|
|
1164
|
+
if (!src.startsWith("---\n")) return null;
|
|
1165
|
+
const after = src.slice(4);
|
|
1166
|
+
const closeIdx = after.search(/\n---\s*(?:\r?\n|$)/);
|
|
1167
|
+
if (closeIdx < 0) return null;
|
|
1168
|
+
const fm = after.slice(0, closeIdx);
|
|
1169
|
+
for (const ln of fm.split(/\r?\n/)) {
|
|
1170
|
+
const m2 = ln.match(/^type\s*:\s*(\S+)\s*$/);
|
|
1171
|
+
if (m2) {
|
|
1172
|
+
const v2 = m2[1].replace(/^["']|["']$/g, "");
|
|
1173
|
+
return v2;
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
return null;
|
|
1177
|
+
}
|
|
1178
|
+
function walkProject(root, { maxFiles, maxDepth, options, timeBudgetMs }) {
|
|
1179
|
+
const out = {
|
|
1180
|
+
filesScanned: 0,
|
|
1181
|
+
totalEstimate: 0,
|
|
1182
|
+
incomplete: false,
|
|
1183
|
+
lastPathWalked: "",
|
|
1184
|
+
fingerprint: [],
|
|
1185
|
+
manifestsFound: [],
|
|
1186
|
+
dirHits: { book: [], content: [], business: [], design: [] },
|
|
1187
|
+
extTotals: {},
|
|
1188
|
+
patternHits: []
|
|
1189
|
+
};
|
|
1190
|
+
let resumeFrom = null;
|
|
1191
|
+
let priorState = null;
|
|
1192
|
+
if (options.resume !== false) {
|
|
1193
|
+
const state = loadScanState(root);
|
|
1194
|
+
if (state && shouldResume(state)) {
|
|
1195
|
+
resumeFrom = state.last_path_walked || null;
|
|
1196
|
+
priorState = state;
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
if (priorState && priorState.partial && typeof priorState.partial === "object") {
|
|
1200
|
+
const p = priorState.partial;
|
|
1201
|
+
out.filesScanned = Number.isFinite(p.files_scanned) ? p.files_scanned : 0;
|
|
1202
|
+
out.totalEstimate = Number.isFinite(p.total_estimate) ? p.total_estimate : out.filesScanned;
|
|
1203
|
+
if (Array.isArray(p.fingerprint)) out.fingerprint = p.fingerprint.slice(0, 4096);
|
|
1204
|
+
if (Array.isArray(p.manifestsFound)) out.manifestsFound = p.manifestsFound.slice();
|
|
1205
|
+
if (p.dirHits && typeof p.dirHits === "object") {
|
|
1206
|
+
for (const k2 of ["book", "content", "business", "design"]) {
|
|
1207
|
+
if (Array.isArray(p.dirHits[k2])) out.dirHits[k2] = p.dirHits[k2].slice();
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
if (p.extTotals && typeof p.extTotals === "object") out.extTotals = { ...p.extTotals };
|
|
1211
|
+
if (Array.isArray(p.patternHits)) out.patternHits = p.patternHits.slice();
|
|
1212
|
+
}
|
|
1213
|
+
let resumed = !resumeFrom;
|
|
1214
|
+
const visitedDirs = /* @__PURE__ */ new Set();
|
|
1215
|
+
try {
|
|
1216
|
+
visitedDirs.add(realpathSync.native(root));
|
|
1217
|
+
} catch {
|
|
1218
|
+
}
|
|
1219
|
+
const startedAt = Date.now();
|
|
1220
|
+
const budget = Number.isFinite(timeBudgetMs) && timeBudgetMs > 0 ? timeBudgetMs : DEFAULT_TIME_BUDGET_MS;
|
|
1221
|
+
let entriesSinceTimeCheck = 0;
|
|
1222
|
+
const stack = [{ path: root, depth: 0 }];
|
|
1223
|
+
while (stack.length > 0) {
|
|
1224
|
+
const { path, depth } = stack.pop();
|
|
1225
|
+
if (depth > maxDepth) continue;
|
|
1226
|
+
let entries;
|
|
1227
|
+
try {
|
|
1228
|
+
entries = readdirSync3(path, { withFileTypes: true });
|
|
1229
|
+
} catch {
|
|
1230
|
+
continue;
|
|
1231
|
+
}
|
|
1232
|
+
entries.sort((a, b2) => a.name < b2.name ? -1 : a.name > b2.name ? 1 : 0);
|
|
1233
|
+
for (const entry of entries) {
|
|
1234
|
+
const childPath = join5(path, entry.name);
|
|
1235
|
+
if (!resumed) {
|
|
1236
|
+
if (childPath === resumeFrom) resumed = true;
|
|
1237
|
+
continue;
|
|
1238
|
+
}
|
|
1239
|
+
out.lastPathWalked = childPath;
|
|
1240
|
+
entriesSinceTimeCheck += 1;
|
|
1241
|
+
if (entriesSinceTimeCheck >= TIME_BUDGET_CHECK_EVERY) {
|
|
1242
|
+
entriesSinceTimeCheck = 0;
|
|
1243
|
+
if (Date.now() - startedAt > budget) {
|
|
1244
|
+
out.incomplete = true;
|
|
1245
|
+
return out;
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
if (entry.isDirectory()) {
|
|
1249
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
1250
|
+
try {
|
|
1251
|
+
const real = realpathSync.native(childPath);
|
|
1252
|
+
if (visitedDirs.has(real)) continue;
|
|
1253
|
+
visitedDirs.add(real);
|
|
1254
|
+
} catch {
|
|
1255
|
+
}
|
|
1256
|
+
recordDirHit(out, entry.name, depth);
|
|
1257
|
+
stack.push({ path: childPath, depth: depth + 1 });
|
|
1258
|
+
continue;
|
|
1259
|
+
}
|
|
1260
|
+
if (!entry.isFile()) continue;
|
|
1261
|
+
out.filesScanned += 1;
|
|
1262
|
+
out.totalEstimate = Math.max(out.totalEstimate, out.filesScanned);
|
|
1263
|
+
if (out.fingerprint.length < 4096) {
|
|
1264
|
+
out.fingerprint.push(childPath.slice(root.length + 1));
|
|
1265
|
+
}
|
|
1266
|
+
if (depth <= 2 && SOFTWARE_MANIFESTS.includes(entry.name)) {
|
|
1267
|
+
out.manifestsFound.push(entry.name);
|
|
1268
|
+
}
|
|
1269
|
+
for (const p of FILENAME_PATTERNS) {
|
|
1270
|
+
if (p.re.test(entry.name)) {
|
|
1271
|
+
out.patternHits.push({ name: entry.name, domain: p.domain, weight: p.weight });
|
|
1272
|
+
break;
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
const ext = extname(entry.name).toLowerCase();
|
|
1276
|
+
const dom = EXT_DOMAIN[ext];
|
|
1277
|
+
if (dom) {
|
|
1278
|
+
out.extTotals[dom] = (out.extTotals[dom] || 0) + 1;
|
|
1279
|
+
}
|
|
1280
|
+
if (out.filesScanned % CHECKPOINT_EVERY === 0) {
|
|
1281
|
+
try {
|
|
1282
|
+
writeScanState(root, {
|
|
1283
|
+
scan_id: newScanId(),
|
|
1284
|
+
started_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1285
|
+
last_path_walked: childPath,
|
|
1286
|
+
files_scanned: out.filesScanned,
|
|
1287
|
+
total_estimate: out.totalEstimate,
|
|
1288
|
+
attempts: 1,
|
|
1289
|
+
incomplete: true,
|
|
1290
|
+
partial: snapshotPartial(out)
|
|
1291
|
+
});
|
|
1292
|
+
} catch {
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
if (out.filesScanned >= maxFiles) {
|
|
1296
|
+
out.incomplete = true;
|
|
1297
|
+
return out;
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
return out;
|
|
1302
|
+
}
|
|
1303
|
+
function snapshotPartial(out) {
|
|
1304
|
+
return {
|
|
1305
|
+
files_scanned: out.filesScanned,
|
|
1306
|
+
total_estimate: out.totalEstimate,
|
|
1307
|
+
fingerprint: out.fingerprint.slice(0, 4096),
|
|
1308
|
+
manifestsFound: out.manifestsFound.slice(0, 32),
|
|
1309
|
+
dirHits: {
|
|
1310
|
+
book: out.dirHits.book.slice(0, 32),
|
|
1311
|
+
content: out.dirHits.content.slice(0, 32),
|
|
1312
|
+
business: out.dirHits.business.slice(0, 32),
|
|
1313
|
+
design: out.dirHits.design.slice(0, 32)
|
|
1314
|
+
},
|
|
1315
|
+
extTotals: { ...out.extTotals },
|
|
1316
|
+
patternHits: out.patternHits.slice(0, 64)
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
function resolveTimeBudgetMs(options) {
|
|
1320
|
+
if (Number.isFinite(options.timeBudgetMs) && options.timeBudgetMs > 0) {
|
|
1321
|
+
return options.timeBudgetMs;
|
|
1322
|
+
}
|
|
1323
|
+
const env = process.env.IJFW_DETECT_TIME_BUDGET_MS;
|
|
1324
|
+
if (env) {
|
|
1325
|
+
const n = Number(env);
|
|
1326
|
+
if (Number.isFinite(n) && n > 0) return n;
|
|
1327
|
+
}
|
|
1328
|
+
return DEFAULT_TIME_BUDGET_MS;
|
|
1329
|
+
}
|
|
1330
|
+
function recordDirHit(out, name12, depth) {
|
|
1331
|
+
if (depth > 2) return;
|
|
1332
|
+
const lower = name12.toLowerCase();
|
|
1333
|
+
if (BOOK_DIRS.includes(lower)) out.dirHits.book.push(lower);
|
|
1334
|
+
if (CONTENT_DIRS.includes(lower)) out.dirHits.content.push(lower);
|
|
1335
|
+
if (BUSINESS_DIRS.includes(lower)) out.dirHits.business.push(lower);
|
|
1336
|
+
if (DESIGN_DIRS.includes(lower)) out.dirHits.design.push(lower);
|
|
1337
|
+
}
|
|
1338
|
+
function scoreSignals(signals) {
|
|
1339
|
+
const board = {
|
|
1340
|
+
software: 0,
|
|
1341
|
+
book: 0,
|
|
1342
|
+
content: 0,
|
|
1343
|
+
business: 0,
|
|
1344
|
+
design: 0,
|
|
1345
|
+
mixed: 0,
|
|
1346
|
+
unknown: 0
|
|
1347
|
+
};
|
|
1348
|
+
const patternBudget = { software: 0.8, book: 0.8, content: 0.8, business: 0.8, design: 0.8, mixed: 0.8, unknown: 0.8 };
|
|
1349
|
+
const dirBudget = { software: 0.6, book: 0.6, content: 0.6, business: 0.6, design: 0.6, mixed: 0.6, unknown: 0.6 };
|
|
1350
|
+
for (const s of signals) {
|
|
1351
|
+
if (s.kind === "user_declaration" && s.value) board[s.value] += 1;
|
|
1352
|
+
else if (s.kind === "agents_md_frontmatter" && s.value) board[s.value] += 0.9;
|
|
1353
|
+
else if (s.kind === "brief_md_frontmatter" && s.value) board[s.value] += 0.8;
|
|
1354
|
+
else if (s.kind === "manifest") board.software += 0.9;
|
|
1355
|
+
else if (s.kind === "dir_book") {
|
|
1356
|
+
const add = Math.min(0.4, dirBudget.book);
|
|
1357
|
+
board.book += add;
|
|
1358
|
+
dirBudget.book -= add;
|
|
1359
|
+
} else if (s.kind === "dir_content") {
|
|
1360
|
+
const add = Math.min(0.4, dirBudget.content);
|
|
1361
|
+
board.content += add;
|
|
1362
|
+
dirBudget.content -= add;
|
|
1363
|
+
} else if (s.kind === "dir_business") {
|
|
1364
|
+
const add = Math.min(0.4, dirBudget.business);
|
|
1365
|
+
board.business += add;
|
|
1366
|
+
dirBudget.business -= add;
|
|
1367
|
+
} else if (s.kind === "dir_design") {
|
|
1368
|
+
const add = Math.min(0.4, dirBudget.design);
|
|
1369
|
+
board.design += add;
|
|
1370
|
+
dirBudget.design -= add;
|
|
1371
|
+
} else if (s.kind === "file_extension_ratio") {
|
|
1372
|
+
const m2 = s.ratio || 0;
|
|
1373
|
+
board[s.domain] = (board[s.domain] || 0) + 0.7 * m2;
|
|
1374
|
+
} else if (s.kind === "filename_pattern") {
|
|
1375
|
+
const add = Math.min(s.weight, patternBudget[s.domain] || 0);
|
|
1376
|
+
if (add > 0) {
|
|
1377
|
+
board[s.domain] = (board[s.domain] || 0) + add;
|
|
1378
|
+
patternBudget[s.domain] -= add;
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
return board;
|
|
1383
|
+
}
|
|
1384
|
+
function rankDomains(board) {
|
|
1385
|
+
const arr = Object.entries(board).filter(([d]) => d !== "mixed" && d !== "unknown").map(([domain, raw]) => ({ domain, raw }));
|
|
1386
|
+
if (arr.length === 0) return [];
|
|
1387
|
+
const maxRaw = arr.reduce((m2, e) => Math.max(m2, e.raw), 0);
|
|
1388
|
+
if (maxRaw <= 0) return [];
|
|
1389
|
+
for (const e of arr) {
|
|
1390
|
+
e.score = anchor(e.raw, maxRaw);
|
|
1391
|
+
}
|
|
1392
|
+
arr.sort((a, b2) => b2.score - a.score);
|
|
1393
|
+
return arr;
|
|
1394
|
+
}
|
|
1395
|
+
function anchor(raw, maxRaw) {
|
|
1396
|
+
if (raw <= 0) return 0;
|
|
1397
|
+
const top = Math.min(0.95, 0.4 + 0.55 * Math.tanh(raw));
|
|
1398
|
+
return Number((top * (raw / maxRaw)).toFixed(3));
|
|
1399
|
+
}
|
|
1400
|
+
function fileTreeHash(paths) {
|
|
1401
|
+
if (!paths || paths.length === 0) return "";
|
|
1402
|
+
const h = createHash("sha256");
|
|
1403
|
+
for (const p of paths) h.update(p + "\n");
|
|
1404
|
+
return h.digest("hex").slice(0, 16);
|
|
1405
|
+
}
|
|
1406
|
+
function branchHash(root) {
|
|
1407
|
+
try {
|
|
1408
|
+
const dotGit = join5(root, ".git");
|
|
1409
|
+
if (!existsSync2(dotGit)) return "";
|
|
1410
|
+
let headPath = null;
|
|
1411
|
+
let st;
|
|
1412
|
+
try {
|
|
1413
|
+
st = statSync3(dotGit);
|
|
1414
|
+
} catch {
|
|
1415
|
+
return "";
|
|
1416
|
+
}
|
|
1417
|
+
if (st.isDirectory()) {
|
|
1418
|
+
headPath = join5(dotGit, "HEAD");
|
|
1419
|
+
} else if (st.isFile()) {
|
|
1420
|
+
const ptr = readFileSync3(dotGit, "utf8");
|
|
1421
|
+
const m3 = ptr.match(/^gitdir:\s*(.+?)\s*$/m);
|
|
1422
|
+
if (!m3) return "";
|
|
1423
|
+
const target = m3[1];
|
|
1424
|
+
const gitDir = isAbsolute(target) ? target : pathResolve(root, target);
|
|
1425
|
+
headPath = join5(gitDir, "HEAD");
|
|
1426
|
+
} else {
|
|
1427
|
+
return "";
|
|
1428
|
+
}
|
|
1429
|
+
if (!headPath || !existsSync2(headPath)) return "";
|
|
1430
|
+
const head = readFileSync3(headPath, "utf8").trim();
|
|
1431
|
+
const m2 = head.match(/^ref:\s*(.+)$/);
|
|
1432
|
+
const branch = m2 ? m2[1] : head;
|
|
1433
|
+
return createHash("sha256").update(branch).digest("hex").slice(0, 16);
|
|
1434
|
+
} catch {
|
|
1435
|
+
}
|
|
1436
|
+
return "";
|
|
1437
|
+
}
|
|
1438
|
+
function isC9AvailableSync() {
|
|
1439
|
+
if (_c9AvailableCache !== null) return _c9AvailableCache;
|
|
1440
|
+
try {
|
|
1441
|
+
const here = fileURLToPath(import.meta.url);
|
|
1442
|
+
const fts5Path = join5(dirname(here), "compute", "fts5.js");
|
|
1443
|
+
_c9AvailableCache = existsSync2(fts5Path);
|
|
1444
|
+
} catch {
|
|
1445
|
+
_c9AvailableCache = false;
|
|
1446
|
+
}
|
|
1447
|
+
return _c9AvailableCache;
|
|
1448
|
+
}
|
|
1449
|
+
function newScanId() {
|
|
1450
|
+
return createHash("sha256").update(String(process.pid) + ":" + String(Date.now()) + ":" + Math.random()).digest("hex").slice(0, 12);
|
|
1451
|
+
}
|
|
1452
|
+
var DOMAINS, MAX_FILES, MAX_DEPTH, CHECKPOINT_EVERY, DEFAULT_TIME_BUDGET_MS, TIME_BUDGET_CHECK_EVERY, SKIP_DIRS, EXT_DOMAIN, SOFTWARE_MANIFESTS, BOOK_DIRS, CONTENT_DIRS, BUSINESS_DIRS, DESIGN_DIRS, FILENAME_PATTERNS, _c9AvailableCache;
|
|
1453
|
+
var init_project_type_detector = __esm({
|
|
1454
|
+
"../mcp-server/src/project-type-detector.js"() {
|
|
1455
|
+
init_scan_resume();
|
|
1456
|
+
DOMAINS = ["software", "book", "content", "business", "design", "mixed", "unknown"];
|
|
1457
|
+
MAX_FILES = 2e5;
|
|
1458
|
+
MAX_DEPTH = 12;
|
|
1459
|
+
CHECKPOINT_EVERY = 500;
|
|
1460
|
+
DEFAULT_TIME_BUDGET_MS = 5e3;
|
|
1461
|
+
TIME_BUDGET_CHECK_EVERY = 1e3;
|
|
1462
|
+
SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
1463
|
+
".git",
|
|
1464
|
+
".hg",
|
|
1465
|
+
".svn",
|
|
1466
|
+
".ijfw",
|
|
1467
|
+
".planning",
|
|
1468
|
+
".cache",
|
|
1469
|
+
"node_modules",
|
|
1470
|
+
"dist",
|
|
1471
|
+
"build",
|
|
1472
|
+
"out",
|
|
1473
|
+
"target",
|
|
1474
|
+
".next",
|
|
1475
|
+
"__pycache__",
|
|
1476
|
+
".venv",
|
|
1477
|
+
"venv",
|
|
1478
|
+
"env",
|
|
1479
|
+
".pytest_cache",
|
|
1480
|
+
".mypy_cache",
|
|
1481
|
+
".tox",
|
|
1482
|
+
".gradle",
|
|
1483
|
+
".idea",
|
|
1484
|
+
".vscode",
|
|
1485
|
+
"vendor",
|
|
1486
|
+
"bower_components"
|
|
1487
|
+
]);
|
|
1488
|
+
EXT_DOMAIN = {
|
|
1489
|
+
// software (heavy)
|
|
1490
|
+
".js": "software",
|
|
1491
|
+
".jsx": "software",
|
|
1492
|
+
".ts": "software",
|
|
1493
|
+
".tsx": "software",
|
|
1494
|
+
".mjs": "software",
|
|
1495
|
+
".cjs": "software",
|
|
1496
|
+
".py": "software",
|
|
1497
|
+
".rs": "software",
|
|
1498
|
+
".go": "software",
|
|
1499
|
+
".java": "software",
|
|
1500
|
+
".kt": "software",
|
|
1501
|
+
".scala": "software",
|
|
1502
|
+
".rb": "software",
|
|
1503
|
+
".php": "software",
|
|
1504
|
+
".c": "software",
|
|
1505
|
+
".cc": "software",
|
|
1506
|
+
".cpp": "software",
|
|
1507
|
+
".h": "software",
|
|
1508
|
+
".hpp": "software",
|
|
1509
|
+
".hh": "software",
|
|
1510
|
+
".swift": "software",
|
|
1511
|
+
".m": "software",
|
|
1512
|
+
".mm": "software",
|
|
1513
|
+
".cs": "software",
|
|
1514
|
+
".fs": "software",
|
|
1515
|
+
".lua": "software",
|
|
1516
|
+
".dart": "software",
|
|
1517
|
+
".zig": "software",
|
|
1518
|
+
// book / long-form prose
|
|
1519
|
+
".tex": "book",
|
|
1520
|
+
".bib": "book",
|
|
1521
|
+
".latex": "book",
|
|
1522
|
+
".epub": "book",
|
|
1523
|
+
".mobi": "book",
|
|
1524
|
+
// content / blog / docs / marketing
|
|
1525
|
+
".mdx": "content",
|
|
1526
|
+
".markdown": "content",
|
|
1527
|
+
".rst": "content",
|
|
1528
|
+
// design / assets
|
|
1529
|
+
".fig": "design",
|
|
1530
|
+
".sketch": "design",
|
|
1531
|
+
".xd": "design",
|
|
1532
|
+
".ai": "design",
|
|
1533
|
+
".psd": "design",
|
|
1534
|
+
".indd": "design",
|
|
1535
|
+
".svg": "design",
|
|
1536
|
+
".afdesign": "design",
|
|
1537
|
+
".afphoto": "design",
|
|
1538
|
+
// business / ops
|
|
1539
|
+
".xlsx": "business",
|
|
1540
|
+
".xls": "business",
|
|
1541
|
+
".csv": "business",
|
|
1542
|
+
".numbers": "business",
|
|
1543
|
+
".ods": "business",
|
|
1544
|
+
".pptx": "business",
|
|
1545
|
+
".ppt": "business",
|
|
1546
|
+
".key": "business",
|
|
1547
|
+
".docx": "business",
|
|
1548
|
+
".doc": "business"
|
|
1549
|
+
};
|
|
1550
|
+
SOFTWARE_MANIFESTS = [
|
|
1551
|
+
"package.json",
|
|
1552
|
+
"Cargo.toml",
|
|
1553
|
+
"pyproject.toml",
|
|
1554
|
+
"setup.py",
|
|
1555
|
+
"Gemfile",
|
|
1556
|
+
"go.mod",
|
|
1557
|
+
"pom.xml",
|
|
1558
|
+
"build.gradle",
|
|
1559
|
+
"build.gradle.kts",
|
|
1560
|
+
"composer.json",
|
|
1561
|
+
"Package.swift",
|
|
1562
|
+
"mix.exs",
|
|
1563
|
+
"rebar.config",
|
|
1564
|
+
"pubspec.yaml",
|
|
1565
|
+
"CMakeLists.txt",
|
|
1566
|
+
"Makefile"
|
|
1567
|
+
];
|
|
1568
|
+
BOOK_DIRS = ["manuscripts", "manuscript", "drafts", "draft", "chapters", "book"];
|
|
1569
|
+
CONTENT_DIRS = ["content", "posts", "articles", "blog", "newsletter", "social"];
|
|
1570
|
+
BUSINESS_DIRS = ["strategy", "financials", "finance", "ops", "runbooks", "sop", "sops", "ops-runbooks"];
|
|
1571
|
+
DESIGN_DIRS = ["designs", "design", "assets", "mockups", "wireframes", "figma"];
|
|
1572
|
+
FILENAME_PATTERNS = [
|
|
1573
|
+
{ re: /^chapter[-_]?\d+/i, domain: "book", weight: 0.4 },
|
|
1574
|
+
{ re: /^ch\d+/i, domain: "book", weight: 0.3 },
|
|
1575
|
+
{ re: /^brand[-_]voice/i, domain: "content", weight: 0.4 },
|
|
1576
|
+
{ re: /^seo[-_]/i, domain: "content", weight: 0.2 },
|
|
1577
|
+
{ re: /^post[-_]/i, domain: "content", weight: 0.2 },
|
|
1578
|
+
{ re: /^figma[-_]export/i, domain: "design", weight: 0.4 },
|
|
1579
|
+
{ re: /^wireframe/i, domain: "design", weight: 0.3 }
|
|
1580
|
+
];
|
|
1581
|
+
_c9AvailableCache = null;
|
|
1582
|
+
}
|
|
1583
|
+
});
|
|
1584
|
+
|
|
1585
|
+
// ../mcp-server/src/gate-result.js
|
|
1586
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
1587
|
+
import { basename, dirname as dirname2, join as join6 } from "node:path";
|
|
1588
|
+
async function emitGateResult(gateOpts, context = {}) {
|
|
1589
|
+
if (gateOpts === null || typeof gateOpts !== "object") {
|
|
1590
|
+
throw new TypeError("emitGateResult: gateOpts must be an object");
|
|
1591
|
+
}
|
|
1592
|
+
const projectType = typeof context.project_type === "string" && context.project_type.length > 0 ? context.project_type : await resolveProjectType(context.projectRoot);
|
|
1593
|
+
const result = {
|
|
1594
|
+
schema_version: SCHEMA_VERSION,
|
|
1595
|
+
gate_id: makeGateId(gateOpts.gate),
|
|
1596
|
+
gate: gateOpts.gate,
|
|
1597
|
+
status: gateOpts.status,
|
|
1598
|
+
project_type: projectType,
|
|
1599
|
+
lenses: Array.isArray(gateOpts.lenses) ? gateOpts.lenses : [],
|
|
1600
|
+
affected_artifacts: Array.isArray(gateOpts.affected_artifacts) ? gateOpts.affected_artifacts : [],
|
|
1601
|
+
accounting: gateOpts.accounting,
|
|
1602
|
+
remediation: Array.isArray(gateOpts.remediation) ? gateOpts.remediation : [],
|
|
1603
|
+
receipts_ref: gateOpts.receipts_ref === void 0 ? null : gateOpts.receipts_ref,
|
|
1604
|
+
supersedes: gateOpts.supersedes === void 0 ? null : gateOpts.supersedes,
|
|
1605
|
+
emitted_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1606
|
+
};
|
|
1607
|
+
const { valid, errors } = validateGateResult(result);
|
|
1608
|
+
if (!valid) {
|
|
1609
|
+
throw new Error(
|
|
1610
|
+
`emitGateResult: invalid gate-result \u2014 ${errors.join("; ")}`
|
|
1611
|
+
);
|
|
1612
|
+
}
|
|
1613
|
+
makeReceipt(result, {
|
|
1614
|
+
projectRoot: typeof context.projectRoot === "string" && context.projectRoot.length > 0 ? context.projectRoot : process.cwd()
|
|
1615
|
+
}).catch(() => {
|
|
1616
|
+
});
|
|
1617
|
+
return formatGateResult(result);
|
|
1618
|
+
}
|
|
1619
|
+
async function makeReceipt(gateResult, opts = {}) {
|
|
1620
|
+
try {
|
|
1621
|
+
if (!gateResult || typeof gateResult !== "object") return;
|
|
1622
|
+
const gateId = typeof gateResult.gate_id === "string" ? gateResult.gate_id : null;
|
|
1623
|
+
if (!gateId) return;
|
|
1624
|
+
if (!RECEIPT_GATE_ID_PATTERN.test(gateId)) {
|
|
1625
|
+
try {
|
|
1626
|
+
process.stderr.write(
|
|
1627
|
+
`ijfw: makeReceipt rejected unsafe gate_id "${gateId}"
|
|
1628
|
+
`
|
|
1629
|
+
);
|
|
1630
|
+
} catch {
|
|
1631
|
+
}
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1634
|
+
const safeId = basename(gateId);
|
|
1635
|
+
const root = typeof opts.projectRoot === "string" && opts.projectRoot.length > 0 ? opts.projectRoot : process.cwd();
|
|
1636
|
+
const receiptPath = join6(
|
|
1637
|
+
root,
|
|
1638
|
+
".ijfw",
|
|
1639
|
+
"memory",
|
|
1640
|
+
"gate-receipts",
|
|
1641
|
+
`${safeId}.json`
|
|
1642
|
+
);
|
|
1643
|
+
await mkdir(dirname2(receiptPath), { recursive: true });
|
|
1644
|
+
const body = JSON.stringify(gateResult, null, 2) + "\n";
|
|
1645
|
+
await writeFile(receiptPath, body, "utf8");
|
|
1646
|
+
} catch (err) {
|
|
1647
|
+
const msg = err && err.message ? err.message : String(err);
|
|
1648
|
+
try {
|
|
1649
|
+
process.stderr.write(`ijfw: gate-result receipt write failed: ${msg}
|
|
1650
|
+
`);
|
|
1651
|
+
} catch {
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
async function resolveProjectType(projectRoot) {
|
|
1656
|
+
try {
|
|
1657
|
+
const root = typeof projectRoot === "string" && projectRoot.length > 0 ? projectRoot : process.cwd();
|
|
1658
|
+
const detected = detect(root);
|
|
1659
|
+
if (detected && typeof detected.primary_type === "string" && detected.primary_type.length > 0) {
|
|
1660
|
+
return detected.primary_type;
|
|
1661
|
+
}
|
|
1662
|
+
if (detected && typeof detected.type === "string" && detected.type.length > 0) {
|
|
1663
|
+
return detected.type;
|
|
1664
|
+
}
|
|
1665
|
+
return "unknown";
|
|
1666
|
+
} catch {
|
|
1667
|
+
return "unknown";
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
var RECEIPT_GATE_ID_PATTERN;
|
|
1671
|
+
var init_gate_result = __esm({
|
|
1672
|
+
"../mcp-server/src/gate-result.js"() {
|
|
1673
|
+
init_gate_result_schema();
|
|
1674
|
+
init_project_type_detector();
|
|
1675
|
+
RECEIPT_GATE_ID_PATTERN = /^[a-z][a-z0-9-]+$/;
|
|
1676
|
+
}
|
|
1677
|
+
});
|
|
1678
|
+
|
|
571
1679
|
// src/preflight/gates/audit-ci.js
|
|
572
1680
|
var audit_ci_exports = {};
|
|
573
1681
|
__export(audit_ci_exports, {
|
|
@@ -577,51 +1685,105 @@ __export(audit_ci_exports, {
|
|
|
577
1685
|
severity: () => severity7
|
|
578
1686
|
});
|
|
579
1687
|
import { spawnSync as spawnSync7 } from "node:child_process";
|
|
580
|
-
import { join as
|
|
1688
|
+
import { join as join7 } from "node:path";
|
|
1689
|
+
function parseAuditReport(output) {
|
|
1690
|
+
const start = output.indexOf("{");
|
|
1691
|
+
if (start === -1) return null;
|
|
1692
|
+
try {
|
|
1693
|
+
return JSON.parse(output.slice(start));
|
|
1694
|
+
} catch {
|
|
1695
|
+
return null;
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
function highCriticalCount(report) {
|
|
1699
|
+
const vulns = report?.metadata?.vulnerabilities || {};
|
|
1700
|
+
return Number(vulns.high || 0) + Number(vulns.critical || 0);
|
|
1701
|
+
}
|
|
1702
|
+
function vulnerableNames(report) {
|
|
1703
|
+
const out = [];
|
|
1704
|
+
for (const [name12, vuln] of Object.entries(report?.vulnerabilities || {})) {
|
|
1705
|
+
const severity12 = String(vuln?.severity || "").toLowerCase();
|
|
1706
|
+
if (severity12 === "high" || severity12 === "critical") out.push(`${name12}: ${severity12}`);
|
|
1707
|
+
}
|
|
1708
|
+
return out;
|
|
1709
|
+
}
|
|
581
1710
|
async function run7(ctx) {
|
|
582
1711
|
const t0 = Date.now();
|
|
583
|
-
const ver = ctx.versions["audit-ci"] || "latest";
|
|
584
|
-
const configPath = join4(ctx.repoRoot, ".audit-ci.jsonc");
|
|
585
1712
|
const packageDirs = ["installer", "mcp-server"];
|
|
586
1713
|
const runs = packageDirs.map((dir) => {
|
|
587
1714
|
const res = spawnSync7(
|
|
588
|
-
"
|
|
589
|
-
["
|
|
1715
|
+
"npm",
|
|
1716
|
+
["audit", "--audit-level=high", "--json"],
|
|
590
1717
|
{
|
|
591
1718
|
encoding: "utf8",
|
|
592
|
-
cwd:
|
|
1719
|
+
cwd: join7(ctx.repoRoot, dir),
|
|
593
1720
|
timeout: 6e4
|
|
594
1721
|
}
|
|
595
1722
|
);
|
|
596
|
-
|
|
1723
|
+
const output = (res.stdout || "") + (res.stderr || "");
|
|
1724
|
+
const report = parseAuditReport(output);
|
|
1725
|
+
const highCritical = highCriticalCount(report);
|
|
1726
|
+
return { dir, status: res.status, output, report, highCritical };
|
|
597
1727
|
});
|
|
598
1728
|
const durationMs = Date.now() - t0;
|
|
599
|
-
const failed = runs.filter((r) => r.
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
1729
|
+
const failed = runs.filter((r) => !r.report || r.highCritical > 0);
|
|
1730
|
+
const status = failed.length === 0 ? "PASS" : "FAIL";
|
|
1731
|
+
const message = status === "PASS" ? "audit-ci: no high/critical vulnerabilities in installer or mcp-server" : "audit-ci: high or critical vulnerabilities found";
|
|
1732
|
+
let details;
|
|
1733
|
+
if (status === "PASS") {
|
|
1734
|
+
details = runs.map((r) => `${r.dir}: pass`);
|
|
1735
|
+
} else {
|
|
1736
|
+
const lines = [];
|
|
1737
|
+
for (const r of failed) {
|
|
1738
|
+
if (!r.report) {
|
|
1739
|
+
lines.push(`${r.dir}: audit report unavailable`);
|
|
1740
|
+
lines.push(...r.output.split("\n").filter(Boolean).slice(0, 10));
|
|
1741
|
+
continue;
|
|
1742
|
+
}
|
|
1743
|
+
lines.push(`${r.dir}: ${r.highCritical} high/critical advisory item(s)`);
|
|
1744
|
+
lines.push(...vulnerableNames(r.report).slice(0, 10));
|
|
1745
|
+
}
|
|
1746
|
+
details = lines.slice(0, 20);
|
|
608
1747
|
}
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
1748
|
+
try {
|
|
1749
|
+
const block = await emitGateResult(
|
|
1750
|
+
{
|
|
1751
|
+
gate: "preflight:audit-ci",
|
|
1752
|
+
status,
|
|
1753
|
+
lenses: [],
|
|
1754
|
+
affected_artifacts: [],
|
|
1755
|
+
accounting: {
|
|
1756
|
+
duration_ms: durationMs,
|
|
1757
|
+
lenses_invoked: 0,
|
|
1758
|
+
cost_usd: null
|
|
1759
|
+
},
|
|
1760
|
+
remediation: []
|
|
1761
|
+
},
|
|
1762
|
+
ctx && ctx.repoRoot ? { projectRoot: ctx.repoRoot } : {}
|
|
1763
|
+
);
|
|
1764
|
+
if (typeof block === "string" && block.length > 0) {
|
|
1765
|
+
details = [...details, block];
|
|
1766
|
+
}
|
|
1767
|
+
} catch (err) {
|
|
1768
|
+
const msg = err && err.message ? err.message : String(err);
|
|
1769
|
+
try {
|
|
1770
|
+
process.stderr.write(`ijfw: preflight:audit-ci gate-result emit failed: ${msg}
|
|
1771
|
+
`);
|
|
1772
|
+
} catch {
|
|
1773
|
+
}
|
|
613
1774
|
}
|
|
614
1775
|
return {
|
|
615
1776
|
name: "audit-ci",
|
|
616
|
-
status
|
|
617
|
-
message
|
|
618
|
-
details
|
|
1777
|
+
status,
|
|
1778
|
+
message,
|
|
1779
|
+
details,
|
|
619
1780
|
durationMs
|
|
620
1781
|
};
|
|
621
1782
|
}
|
|
622
1783
|
var name7, severity7, parallel7;
|
|
623
1784
|
var init_audit_ci = __esm({
|
|
624
1785
|
"src/preflight/gates/audit-ci.js"() {
|
|
1786
|
+
init_gate_result();
|
|
625
1787
|
name7 = "audit-ci";
|
|
626
1788
|
severity7 = "blocking";
|
|
627
1789
|
parallel7 = true;
|
|
@@ -683,7 +1845,7 @@ __export(license_check_exports, {
|
|
|
683
1845
|
severity: () => severity9
|
|
684
1846
|
});
|
|
685
1847
|
import { spawnSync as spawnSync9 } from "node:child_process";
|
|
686
|
-
import { join as
|
|
1848
|
+
import { join as join8 } from "node:path";
|
|
687
1849
|
async function run9(ctx) {
|
|
688
1850
|
const t0 = Date.now();
|
|
689
1851
|
const ver = ctx.versions["license-checker"] || "latest";
|
|
@@ -692,7 +1854,7 @@ async function run9(ctx) {
|
|
|
692
1854
|
["--yes", `license-checker@${ver}`, "--onlyAllow", ALLOWED, "--production"],
|
|
693
1855
|
{
|
|
694
1856
|
encoding: "utf8",
|
|
695
|
-
cwd:
|
|
1857
|
+
cwd: join8(ctx.repoRoot, "installer"),
|
|
696
1858
|
timeout: 3e4
|
|
697
1859
|
}
|
|
698
1860
|
);
|
|
@@ -735,12 +1897,12 @@ __export(pack_smoke_exports, {
|
|
|
735
1897
|
severity: () => severity10
|
|
736
1898
|
});
|
|
737
1899
|
import { spawnSync as spawnSync10 } from "node:child_process";
|
|
738
|
-
import { mkdtempSync as mkdtempSync2, rmSync as rmSync2, mkdirSync, writeFileSync as
|
|
739
|
-
import { join as
|
|
1900
|
+
import { mkdtempSync as mkdtempSync2, rmSync as rmSync2, mkdirSync as mkdirSync3, writeFileSync as writeFileSync4, readdirSync as readdirSync4, existsSync as existsSync3 } from "node:fs";
|
|
1901
|
+
import { join as join9, resolve } from "node:path";
|
|
740
1902
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
741
1903
|
async function run10(ctx) {
|
|
742
1904
|
const t0 = Date.now();
|
|
743
|
-
const installerDir =
|
|
1905
|
+
const installerDir = join9(ctx.repoRoot, "installer");
|
|
744
1906
|
const build = spawnSync10("npm", ["run", "build"], {
|
|
745
1907
|
encoding: "utf8",
|
|
746
1908
|
cwd: installerDir,
|
|
@@ -782,13 +1944,13 @@ async function run10(ctx) {
|
|
|
782
1944
|
};
|
|
783
1945
|
}
|
|
784
1946
|
const tarballPath = resolve(installerDir, tarball);
|
|
785
|
-
const tmpRoot = mkdtempSync2(
|
|
786
|
-
const fakeHome =
|
|
787
|
-
const installDir =
|
|
788
|
-
|
|
789
|
-
|
|
1947
|
+
const tmpRoot = mkdtempSync2(join9(tmpdir2(), "ijfw-pack-smoke-"));
|
|
1948
|
+
const fakeHome = join9(tmpRoot, "home");
|
|
1949
|
+
const installDir = join9(tmpRoot, "install");
|
|
1950
|
+
mkdirSync3(fakeHome, { recursive: true });
|
|
1951
|
+
mkdirSync3(installDir, { recursive: true });
|
|
790
1952
|
try {
|
|
791
|
-
|
|
1953
|
+
writeFileSync4(join9(installDir, "package.json"), JSON.stringify({ name: "smoke-test", version: "1.0.0", type: "module" }));
|
|
792
1954
|
const install = spawnSync10("npm", ["install", "--no-save", tarballPath], {
|
|
793
1955
|
encoding: "utf8",
|
|
794
1956
|
cwd: installDir,
|
|
@@ -806,25 +1968,25 @@ async function run10(ctx) {
|
|
|
806
1968
|
};
|
|
807
1969
|
}
|
|
808
1970
|
const binCandidates = [
|
|
809
|
-
|
|
810
|
-
|
|
1971
|
+
join9(installDir, "node_modules", ".bin", "ijfw"),
|
|
1972
|
+
join9(installDir, "node_modules", ".bin", "ijfw-install")
|
|
811
1973
|
];
|
|
812
1974
|
let binPath = null;
|
|
813
1975
|
for (const c2 of binCandidates) {
|
|
814
|
-
if (
|
|
1976
|
+
if (existsSync3(c2)) {
|
|
815
1977
|
binPath = c2;
|
|
816
1978
|
break;
|
|
817
1979
|
}
|
|
818
1980
|
}
|
|
819
1981
|
if (!binPath) {
|
|
820
|
-
const binDir =
|
|
1982
|
+
const binDir = join9(installDir, "node_modules", ".bin");
|
|
821
1983
|
let entries = [];
|
|
822
1984
|
try {
|
|
823
|
-
entries =
|
|
1985
|
+
entries = readdirSync4(binDir);
|
|
824
1986
|
} catch {
|
|
825
1987
|
}
|
|
826
1988
|
const found = entries.find((e) => e.startsWith("ijfw"));
|
|
827
|
-
if (found) binPath =
|
|
1989
|
+
if (found) binPath = join9(binDir, found);
|
|
828
1990
|
}
|
|
829
1991
|
if (!binPath) {
|
|
830
1992
|
return {
|
|
@@ -887,12 +2049,12 @@ __export(upgrade_smoke_exports, {
|
|
|
887
2049
|
severity: () => severity11
|
|
888
2050
|
});
|
|
889
2051
|
import { spawnSync as spawnSync11 } from "node:child_process";
|
|
890
|
-
import { mkdtempSync as mkdtempSync3, rmSync as rmSync3, mkdirSync as
|
|
891
|
-
import { join as
|
|
2052
|
+
import { mkdtempSync as mkdtempSync3, rmSync as rmSync3, mkdirSync as mkdirSync4, writeFileSync as writeFileSync5, readFileSync as readFileSync4, existsSync as existsSync4 } from "node:fs";
|
|
2053
|
+
import { join as join10, resolve as resolve2 } from "node:path";
|
|
892
2054
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
893
2055
|
async function run11(ctx) {
|
|
894
2056
|
const t0 = Date.now();
|
|
895
|
-
const installerDir =
|
|
2057
|
+
const installerDir = join10(ctx.repoRoot, "installer");
|
|
896
2058
|
const build = spawnSync11("npm", ["run", "build"], {
|
|
897
2059
|
encoding: "utf8",
|
|
898
2060
|
cwd: installerDir,
|
|
@@ -925,15 +2087,15 @@ async function run11(ctx) {
|
|
|
925
2087
|
}
|
|
926
2088
|
const tarball = pack.stdout.trim();
|
|
927
2089
|
const tarballPath = resolve2(installerDir, tarball);
|
|
928
|
-
const tmpRoot = mkdtempSync3(
|
|
929
|
-
const fakeHome =
|
|
930
|
-
const installDir =
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
const claudeDir =
|
|
934
|
-
|
|
2090
|
+
const tmpRoot = mkdtempSync3(join10(tmpdir3(), "ijfw-upgrade-smoke-"));
|
|
2091
|
+
const fakeHome = join10(tmpRoot, "home");
|
|
2092
|
+
const installDir = join10(tmpRoot, "install");
|
|
2093
|
+
mkdirSync4(fakeHome, { recursive: true });
|
|
2094
|
+
mkdirSync4(installDir, { recursive: true });
|
|
2095
|
+
const claudeDir = join10(fakeHome, ".claude");
|
|
2096
|
+
mkdirSync4(claudeDir, { recursive: true });
|
|
935
2097
|
try {
|
|
936
|
-
|
|
2098
|
+
writeFileSync5(join10(installDir, "package.json"), JSON.stringify({ name: "upgrade-smoke", version: "1.0.0", type: "module" }));
|
|
937
2099
|
const install = spawnSync11("npm", ["install", "--no-save", tarballPath], {
|
|
938
2100
|
encoding: "utf8",
|
|
939
2101
|
cwd: installDir,
|
|
@@ -951,12 +2113,12 @@ async function run11(ctx) {
|
|
|
951
2113
|
};
|
|
952
2114
|
}
|
|
953
2115
|
const binCandidates = [
|
|
954
|
-
|
|
955
|
-
|
|
2116
|
+
join10(installDir, "node_modules", ".bin", "ijfw-install"),
|
|
2117
|
+
join10(installDir, "node_modules", ".bin", "ijfw")
|
|
956
2118
|
];
|
|
957
2119
|
let installerBin = null;
|
|
958
2120
|
for (const c2 of binCandidates) {
|
|
959
|
-
if (
|
|
2121
|
+
if (existsSync4(c2)) {
|
|
960
2122
|
installerBin = c2;
|
|
961
2123
|
break;
|
|
962
2124
|
}
|
|
@@ -970,11 +2132,11 @@ async function run11(ctx) {
|
|
|
970
2132
|
durationMs: Date.now() - t0
|
|
971
2133
|
};
|
|
972
2134
|
}
|
|
973
|
-
const settingsPath =
|
|
974
|
-
if (
|
|
2135
|
+
const settingsPath = join10(claudeDir, "settings.json");
|
|
2136
|
+
if (existsSync4(settingsPath)) {
|
|
975
2137
|
let settings;
|
|
976
2138
|
try {
|
|
977
|
-
settings = JSON.parse(
|
|
2139
|
+
settings = JSON.parse(readFileSync4(settingsPath, "utf8"));
|
|
978
2140
|
} catch (e) {
|
|
979
2141
|
return {
|
|
980
2142
|
name: "upgrade-smoke",
|
|
@@ -995,9 +2157,9 @@ async function run11(ctx) {
|
|
|
995
2157
|
};
|
|
996
2158
|
}
|
|
997
2159
|
}
|
|
998
|
-
const marketplaceSrc =
|
|
999
|
-
if (
|
|
1000
|
-
const src =
|
|
2160
|
+
const marketplaceSrc = join10(installerDir, "src", "marketplace.js");
|
|
2161
|
+
if (existsSync4(marketplaceSrc)) {
|
|
2162
|
+
const src = readFileSync4(marketplaceSrc, "utf8");
|
|
1001
2163
|
const registersCorrectKey = src.includes("'ijfw@ijfw'") || src.includes('"ijfw@ijfw"');
|
|
1002
2164
|
const registersWrongKey = /enabledPlugins\[['"]ijfw-core@ijfw['"]\]\s*=\s*true/.test(src);
|
|
1003
2165
|
if (!registersCorrectKey) {
|
|
@@ -1052,9 +2214,9 @@ var preflight_exports = {};
|
|
|
1052
2214
|
__export(preflight_exports, {
|
|
1053
2215
|
runPreflightCommand: () => runPreflightCommand
|
|
1054
2216
|
});
|
|
1055
|
-
import { readFileSync as
|
|
1056
|
-
import { join as
|
|
1057
|
-
import { fileURLToPath } from "node:url";
|
|
2217
|
+
import { readFileSync as readFileSync5, existsSync as existsSync5 } from "node:fs";
|
|
2218
|
+
import { join as join11, dirname as dirname3, resolve as resolve3 } from "node:path";
|
|
2219
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1058
2220
|
function printHelp() {
|
|
1059
2221
|
console.log(`
|
|
1060
2222
|
ijfw preflight -- 11-gate quality pipeline
|
|
@@ -1090,13 +2252,13 @@ SLO
|
|
|
1090
2252
|
}
|
|
1091
2253
|
function loadVersions(repoRoot2) {
|
|
1092
2254
|
const candidates = [
|
|
1093
|
-
|
|
1094
|
-
|
|
2255
|
+
join11(repoRoot2, "preflight-versions.json"),
|
|
2256
|
+
join11(repoRoot2, ".ijfw", "preflight-versions.json")
|
|
1095
2257
|
];
|
|
1096
2258
|
for (const f of candidates) {
|
|
1097
|
-
if (
|
|
2259
|
+
if (existsSync5(f)) {
|
|
1098
2260
|
try {
|
|
1099
|
-
return JSON.parse(
|
|
2261
|
+
return JSON.parse(readFileSync5(f, "utf8"));
|
|
1100
2262
|
} catch {
|
|
1101
2263
|
}
|
|
1102
2264
|
}
|
|
@@ -1155,7 +2317,7 @@ async function runPreflightCommand(argv, repoRoot2) {
|
|
|
1155
2317
|
function defaultRepoRoot() {
|
|
1156
2318
|
let dir = __dirname;
|
|
1157
2319
|
for (let i = 0; i < 8; i++) {
|
|
1158
|
-
if (
|
|
2320
|
+
if (existsSync5(join11(dir, "package.json")) && existsSync5(join11(dir, "mcp-server"))) return dir;
|
|
1159
2321
|
const next = resolve3(dir, "..");
|
|
1160
2322
|
if (next === dir) break;
|
|
1161
2323
|
dir = next;
|
|
@@ -1166,8 +2328,8 @@ var __dirname;
|
|
|
1166
2328
|
var init_preflight = __esm({
|
|
1167
2329
|
async "src/preflight.js"() {
|
|
1168
2330
|
init_runner();
|
|
1169
|
-
__dirname =
|
|
1170
|
-
if (process.argv[1] && resolve3(process.argv[1]) ===
|
|
2331
|
+
__dirname = dirname3(fileURLToPath2(import.meta.url));
|
|
2332
|
+
if (process.argv[1] && resolve3(process.argv[1]) === fileURLToPath2(import.meta.url)) {
|
|
1171
2333
|
await runPreflightCommand(process.argv, defaultRepoRoot());
|
|
1172
2334
|
}
|
|
1173
2335
|
}
|
|
@@ -2433,20 +3595,78 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
2433
3595
|
});
|
|
2434
3596
|
|
|
2435
3597
|
// src/ijfw.js
|
|
2436
|
-
import { dirname as
|
|
2437
|
-
import { fileURLToPath as
|
|
2438
|
-
import { existsSync as
|
|
2439
|
-
import { homedir, platform
|
|
3598
|
+
import { dirname as dirname4, join as join12, resolve as resolve4, basename as basename2 } from "node:path";
|
|
3599
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
3600
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync5, copyFileSync as copyFileSync3, readdirSync as readdirSync5, rmSync as rmSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "node:fs";
|
|
3601
|
+
import { homedir, platform } from "node:os";
|
|
2440
3602
|
import { spawnSync as spawnSync12 } from "node:child_process";
|
|
2441
|
-
var __dirname2 =
|
|
3603
|
+
var __dirname2 = dirname4(fileURLToPath3(import.meta.url));
|
|
2442
3604
|
function repoRoot() {
|
|
2443
3605
|
let dir = __dirname2;
|
|
2444
3606
|
for (let i = 0; i < 6; i++) {
|
|
2445
|
-
if (
|
|
3607
|
+
if (existsSync6(join12(dir, "package.json")) && existsSync6(join12(dir, ".git"))) return dir;
|
|
2446
3608
|
dir = resolve4(dir, "..");
|
|
2447
3609
|
}
|
|
2448
3610
|
return process.cwd();
|
|
2449
3611
|
}
|
|
3612
|
+
function findInternalAsset(...rel) {
|
|
3613
|
+
const root = repoRoot();
|
|
3614
|
+
const ijfwHome = join12(homedir(), ".ijfw");
|
|
3615
|
+
const candidates = [join12(root, ...rel), join12(ijfwHome, ...rel)];
|
|
3616
|
+
return candidates.find((p) => existsSync6(p)) || null;
|
|
3617
|
+
}
|
|
3618
|
+
function readDashboardPort() {
|
|
3619
|
+
const portFile = join12(homedir(), ".ijfw", "dashboard.port");
|
|
3620
|
+
try {
|
|
3621
|
+
const port = Number.parseInt(readFileSync6(portFile, "utf8").trim(), 10);
|
|
3622
|
+
return Number.isFinite(port) ? port : 37891;
|
|
3623
|
+
} catch {
|
|
3624
|
+
return 37891;
|
|
3625
|
+
}
|
|
3626
|
+
}
|
|
3627
|
+
function openBrowser(url) {
|
|
3628
|
+
if (process.env.CI || process.env.NO_OPEN) return;
|
|
3629
|
+
const r = platform() === "darwin" ? spawnSync12("open", [url], { stdio: "ignore" }) : platform() === "win32" ? spawnSync12("cmd", ["/c", "start", "", url], { stdio: "ignore", shell: false }) : spawnSync12("xdg-open", [url], { stdio: "ignore" });
|
|
3630
|
+
return r.status ?? 0;
|
|
3631
|
+
}
|
|
3632
|
+
var ORCHESTRATOR_COMMANDS = /* @__PURE__ */ new Set([
|
|
3633
|
+
"update",
|
|
3634
|
+
"statusline",
|
|
3635
|
+
"config",
|
|
3636
|
+
"insight",
|
|
3637
|
+
"blackboard",
|
|
3638
|
+
"team",
|
|
3639
|
+
"swarm",
|
|
3640
|
+
"codex",
|
|
3641
|
+
"recover",
|
|
3642
|
+
"memory",
|
|
3643
|
+
"cross",
|
|
3644
|
+
"status",
|
|
3645
|
+
"demo",
|
|
3646
|
+
"import",
|
|
3647
|
+
"receipt",
|
|
3648
|
+
"--purge-receipts",
|
|
3649
|
+
"workflow",
|
|
3650
|
+
"handoff",
|
|
3651
|
+
"compress",
|
|
3652
|
+
"consolidate",
|
|
3653
|
+
"cross-audit",
|
|
3654
|
+
"cross-critique",
|
|
3655
|
+
"cross-research",
|
|
3656
|
+
"ijfw-audit",
|
|
3657
|
+
"ijfw-execute",
|
|
3658
|
+
"ijfw-help",
|
|
3659
|
+
"ijfw-plan",
|
|
3660
|
+
"ijfw-ship",
|
|
3661
|
+
"ijfw-verify",
|
|
3662
|
+
"memory-audit",
|
|
3663
|
+
"memory-consent",
|
|
3664
|
+
"memory-why",
|
|
3665
|
+
"metrics",
|
|
3666
|
+
"mode",
|
|
3667
|
+
"override",
|
|
3668
|
+
"extension"
|
|
3669
|
+
]);
|
|
2450
3670
|
function printHelp2() {
|
|
2451
3671
|
console.log(`
|
|
2452
3672
|
ijfw -- the AI efficiency layer
|
|
@@ -2460,7 +3680,13 @@ COMMANDS
|
|
|
2460
3680
|
help Open the full IJFW guide (terminal, or --browser for rendered)
|
|
2461
3681
|
preflight Run 11-gate quality pipeline before publishing
|
|
2462
3682
|
dashboard Start / stop / check the local observability dashboard
|
|
2463
|
-
design Manage
|
|
3683
|
+
design Manage live previews and durable design intelligence
|
|
3684
|
+
blackboard Coordinate project-local swarm state and artifact claims
|
|
3685
|
+
codex Check and sync Codex-native IJFW surfaces
|
|
3686
|
+
team Assemble project agents, charter, and workflow manifest
|
|
3687
|
+
swarm Plan, prepare, and track artifact-aware parallel work
|
|
3688
|
+
recover Show latest checkpoint and next recovery step
|
|
3689
|
+
cross Run Trident audit/research/critique, e.g. ijfw cross audit README.md
|
|
2464
3690
|
doctor Diagnose IJFW installation health
|
|
2465
3691
|
|
|
2466
3692
|
--help, -h Show this help
|
|
@@ -2475,10 +3701,10 @@ function doctorCheck(cmd, args) {
|
|
|
2475
3701
|
}
|
|
2476
3702
|
function findCli() {
|
|
2477
3703
|
const candidates = [
|
|
2478
|
-
|
|
2479
|
-
|
|
3704
|
+
join12(repoRoot(), "mcp-server", "src", "cross-orchestrator-cli.js"),
|
|
3705
|
+
join12(homedir(), ".ijfw", "mcp-server", "src", "cross-orchestrator-cli.js")
|
|
2480
3706
|
];
|
|
2481
|
-
return candidates.find((p) =>
|
|
3707
|
+
return candidates.find((p) => existsSync6(p)) || null;
|
|
2482
3708
|
}
|
|
2483
3709
|
function delegateToCli(argTail) {
|
|
2484
3710
|
const cli = findCli();
|
|
@@ -2497,8 +3723,8 @@ async function main() {
|
|
|
2497
3723
|
const verbose = argv.slice(3).includes("--verbose");
|
|
2498
3724
|
if (delegateToCli(argv.slice(2))) return;
|
|
2499
3725
|
try {
|
|
2500
|
-
const pkgPath =
|
|
2501
|
-
const pkg = JSON.parse(
|
|
3726
|
+
const pkgPath = join12(__dirname2, "..", "package.json");
|
|
3727
|
+
const pkg = JSON.parse(readFileSync6(pkgPath, "utf8"));
|
|
2502
3728
|
console.log(`@ijfw/install@${pkg.version || "unknown"}`);
|
|
2503
3729
|
if (verbose) {
|
|
2504
3730
|
console.log(" (full --verbose details require a completed install: run ijfw install)");
|
|
@@ -2508,11 +3734,14 @@ async function main() {
|
|
|
2508
3734
|
}
|
|
2509
3735
|
process.exit(0);
|
|
2510
3736
|
}
|
|
2511
|
-
if (sub
|
|
3737
|
+
if (ORCHESTRATOR_COMMANDS.has(sub)) {
|
|
2512
3738
|
if (delegateToCli(argv.slice(2))) return;
|
|
2513
3739
|
console.error(`'ijfw ${sub}' requires a completed IJFW install. Run: ijfw install`);
|
|
2514
3740
|
process.exit(1);
|
|
2515
3741
|
}
|
|
3742
|
+
if (sub === "doctor" && findCli()) {
|
|
3743
|
+
if (delegateToCli(argv.slice(2))) return;
|
|
3744
|
+
}
|
|
2516
3745
|
switch (sub) {
|
|
2517
3746
|
case "install": {
|
|
2518
3747
|
const installBin = resolve4(__dirname2, "..", "dist", "install.js");
|
|
@@ -2533,19 +3762,13 @@ async function main() {
|
|
|
2533
3762
|
}
|
|
2534
3763
|
case "dashboard": {
|
|
2535
3764
|
const dashSub = argv[3];
|
|
2536
|
-
const root = repoRoot();
|
|
2537
|
-
const ijfwHome = join9(homedir(), ".ijfw");
|
|
2538
|
-
const findInTree = (...rel) => {
|
|
2539
|
-
const candidates = [join9(root, ...rel), join9(ijfwHome, ...rel)];
|
|
2540
|
-
return candidates.find((p) => existsSync4(p)) || null;
|
|
2541
|
-
};
|
|
2542
3765
|
if (dashSub === "start" || dashSub === "stop" || dashSub === "status") {
|
|
2543
|
-
const dashBin =
|
|
3766
|
+
const dashBin = findInternalAsset("mcp-server", "bin", "ijfw-dashboard");
|
|
2544
3767
|
if (dashBin) {
|
|
2545
3768
|
const r = spawnSync12("node", [dashBin, dashSub, ...argv.slice(4)], { stdio: "inherit" });
|
|
2546
3769
|
process.exit(r.status ?? 0);
|
|
2547
3770
|
} else {
|
|
2548
|
-
const serverJs =
|
|
3771
|
+
const serverJs = findInternalAsset("mcp-server", "src", "dashboard-server.js");
|
|
2549
3772
|
if (dashSub === "start" && serverJs) {
|
|
2550
3773
|
const { spawn } = await import("node:child_process");
|
|
2551
3774
|
const child = spawn(process.execPath, [serverJs, "start", "--daemon"], {
|
|
@@ -2560,7 +3783,7 @@ async function main() {
|
|
|
2560
3783
|
process.exit(1);
|
|
2561
3784
|
}
|
|
2562
3785
|
} else if (dashSub === "render" || !dashSub) {
|
|
2563
|
-
const binJs =
|
|
3786
|
+
const binJs = findInternalAsset("scripts", "dashboard", "bin.js");
|
|
2564
3787
|
if (binJs) {
|
|
2565
3788
|
const r = spawnSync12("node", [binJs, ...argv.slice(dashSub ? 4 : 3)], { stdio: "inherit" });
|
|
2566
3789
|
process.exit(r.status ?? 0);
|
|
@@ -2576,30 +3799,72 @@ async function main() {
|
|
|
2576
3799
|
}
|
|
2577
3800
|
case "design": {
|
|
2578
3801
|
const designSub = argv[3];
|
|
2579
|
-
const
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
3802
|
+
const durableDesign = ["init", "plan", "audit", "critique", "polish", "normalize", "bolder", "quieter", "handoff"];
|
|
3803
|
+
if (durableDesign.includes(designSub)) {
|
|
3804
|
+
if (delegateToCli(argv.slice(2))) return;
|
|
3805
|
+
console.error(`'ijfw design ${designSub}' requires a completed IJFW install. Run: ijfw install`);
|
|
3806
|
+
process.exit(1);
|
|
3807
|
+
}
|
|
3808
|
+
const contentDir = join12(homedir(), ".ijfw", "design-companion", "content");
|
|
3809
|
+
mkdirSync5(contentDir, { recursive: true });
|
|
3810
|
+
if (designSub === "start" || designSub === "open") {
|
|
3811
|
+
const dashBin = findInternalAsset("mcp-server", "bin", "ijfw-dashboard");
|
|
3812
|
+
if (!dashBin) {
|
|
3813
|
+
console.error("[ijfw] Design companion server not found. Run `ijfw-install` to deploy ~/.ijfw/, or run from the IJFW repo root.");
|
|
2585
3814
|
process.exit(1);
|
|
2586
3815
|
}
|
|
2587
|
-
const
|
|
2588
|
-
|
|
2589
|
-
|
|
3816
|
+
const noOpen = argv.slice(4).includes("--no-open");
|
|
3817
|
+
const r = spawnSync12("node", [dashBin, "start", "--no-open"], { stdio: designSub === "start" ? "inherit" : "ignore" });
|
|
3818
|
+
if ((r.status ?? 1) !== 0) process.exit(r.status ?? 1);
|
|
3819
|
+
const url = `http://localhost:${readDashboardPort()}/design`;
|
|
3820
|
+
if (!noOpen) openBrowser(url);
|
|
3821
|
+
console.log(`Design companion running at ${url}`);
|
|
3822
|
+
} else if (designSub === "status") {
|
|
3823
|
+
const dashBin = findInternalAsset("mcp-server", "bin", "ijfw-dashboard");
|
|
3824
|
+
if (!dashBin) {
|
|
3825
|
+
console.error("[ijfw] Design companion server not found. Run `ijfw-install` to deploy ~/.ijfw/, or run from the IJFW repo root.");
|
|
2590
3826
|
process.exit(1);
|
|
2591
3827
|
}
|
|
2592
|
-
const
|
|
2593
|
-
|
|
2594
|
-
|
|
3828
|
+
const r = spawnSync12("node", [dashBin, "status"], { stdio: "inherit" });
|
|
3829
|
+
if ((r.status ?? 1) === 0) console.log(`Design companion URL: http://localhost:${readDashboardPort()}/design`);
|
|
3830
|
+
process.exit(r.status ?? 0);
|
|
3831
|
+
} else if (designSub === "stop") {
|
|
3832
|
+
const dashBin = findInternalAsset("mcp-server", "bin", "ijfw-dashboard");
|
|
3833
|
+
if (!dashBin) {
|
|
3834
|
+
console.error("[ijfw] Design companion server not found. Run `ijfw-install` to deploy ~/.ijfw/, or run from the IJFW repo root.");
|
|
3835
|
+
process.exit(1);
|
|
3836
|
+
}
|
|
3837
|
+
const r = spawnSync12("node", [dashBin, "stop"], { stdio: "inherit" });
|
|
3838
|
+
process.exit(r.status ?? 0);
|
|
3839
|
+
} else if (designSub === "push") {
|
|
3840
|
+
const filePaths = argv.slice(4);
|
|
3841
|
+
if (filePaths.length === 0) {
|
|
3842
|
+
console.error("Usage: ijfw design push <file.html> [more.html ...]");
|
|
3843
|
+
process.exit(1);
|
|
3844
|
+
}
|
|
3845
|
+
for (const filePath of filePaths) {
|
|
3846
|
+
const abs = resolve4(filePath);
|
|
3847
|
+
if (!abs.toLowerCase().endsWith(".html")) {
|
|
3848
|
+
console.error("Design companion accepts standalone .html files.");
|
|
3849
|
+
process.exit(1);
|
|
3850
|
+
}
|
|
3851
|
+
if (!existsSync6(abs)) {
|
|
3852
|
+
console.error(`File not found: ${abs}`);
|
|
3853
|
+
process.exit(1);
|
|
3854
|
+
}
|
|
3855
|
+
const dest = join12(contentDir, basename2(abs));
|
|
3856
|
+
copyFileSync3(abs, dest);
|
|
3857
|
+
console.log(`Design pushed: ${dest}`);
|
|
3858
|
+
}
|
|
3859
|
+
console.log(`Preview: http://localhost:${readDashboardPort()}/design`);
|
|
2595
3860
|
} else if (designSub === "clear") {
|
|
2596
|
-
const files =
|
|
2597
|
-
for (const f of files) rmSync4(
|
|
3861
|
+
const files = readdirSync5(contentDir);
|
|
3862
|
+
for (const f of files) rmSync4(join12(contentDir, f), { force: true });
|
|
2598
3863
|
console.log("Design companion content cleared.");
|
|
2599
3864
|
} else {
|
|
2600
|
-
console.log("ijfw design -- Manage
|
|
3865
|
+
console.log("ijfw design -- Manage live preview and durable design intelligence.");
|
|
2601
3866
|
console.log("");
|
|
2602
|
-
console.log("Usage: ijfw design push <file.html> |
|
|
3867
|
+
console.log("Usage: ijfw design start [--no-open] | open | status | stop | push <file.html> [more.html ...] | clear | init|plan|audit|critique|polish|normalize|bolder|quieter|handoff");
|
|
2603
3868
|
process.exit(1);
|
|
2604
3869
|
}
|
|
2605
3870
|
break;
|
|
@@ -2607,26 +3872,26 @@ async function main() {
|
|
|
2607
3872
|
case "help": {
|
|
2608
3873
|
const wantsBrowser = argv.slice(3).includes("--browser");
|
|
2609
3874
|
const candidates = [
|
|
2610
|
-
|
|
3875
|
+
join12(repoRoot(), "docs", "GUIDE.md"),
|
|
2611
3876
|
resolve4(__dirname2, "..", "docs", "GUIDE.md"),
|
|
2612
|
-
|
|
3877
|
+
join12(homedir(), ".ijfw", "docs", "GUIDE.md")
|
|
2613
3878
|
];
|
|
2614
|
-
const guidePath = candidates.find((p) =>
|
|
3879
|
+
const guidePath = candidates.find((p) => existsSync6(p));
|
|
2615
3880
|
if (!guidePath) {
|
|
2616
3881
|
console.error("[ijfw] Guide not found. Run `ijfw install` to fetch the full guide, or visit https://gitlab.com/therealseandonahoe/ijfw/-/blob/main/docs/GUIDE.md");
|
|
2617
3882
|
process.exit(1);
|
|
2618
3883
|
}
|
|
2619
3884
|
if (wantsBrowser) {
|
|
2620
3885
|
const { marked } = await Promise.resolve().then(() => (init_marked_esm(), marked_esm_exports));
|
|
2621
|
-
const assetsSrc =
|
|
2622
|
-
const outDir =
|
|
2623
|
-
|
|
2624
|
-
if (
|
|
2625
|
-
for (const f of
|
|
2626
|
-
|
|
3886
|
+
const assetsSrc = join12(dirname4(guidePath), "guide", "assets");
|
|
3887
|
+
const outDir = join12(homedir(), ".ijfw", "guide");
|
|
3888
|
+
mkdirSync5(join12(outDir, "assets"), { recursive: true });
|
|
3889
|
+
if (existsSync6(assetsSrc)) {
|
|
3890
|
+
for (const f of readdirSync5(assetsSrc)) {
|
|
3891
|
+
copyFileSync3(join12(assetsSrc, f), join12(outDir, "assets", f));
|
|
2627
3892
|
}
|
|
2628
3893
|
}
|
|
2629
|
-
const md =
|
|
3894
|
+
const md = readFileSync6(guidePath, "utf8").replace(/\(guide\/assets\//g, "(assets/");
|
|
2630
3895
|
const rendered = marked.parse(md, { gfm: true, breaks: false });
|
|
2631
3896
|
const html = `<!doctype html>
|
|
2632
3897
|
<html lang="en"><head>
|
|
@@ -2643,9 +3908,9 @@ async function main() {
|
|
|
2643
3908
|
table{display:table;width:100%}
|
|
2644
3909
|
</style>
|
|
2645
3910
|
</head><body><div class="wrap markdown-body">${rendered}</div></body></html>`;
|
|
2646
|
-
const outHtml =
|
|
2647
|
-
|
|
2648
|
-
const opener =
|
|
3911
|
+
const outHtml = join12(outDir, "index.html");
|
|
3912
|
+
writeFileSync6(outHtml, html);
|
|
3913
|
+
const opener = platform() === "darwin" ? "open" : platform() === "win32" ? "start" : "xdg-open";
|
|
2649
3914
|
spawnSync12(opener, [outHtml], { stdio: "ignore", detached: true });
|
|
2650
3915
|
console.log(`[ijfw] Guide opened in your browser.`);
|
|
2651
3916
|
console.log(` Local copy: ${outHtml}`);
|
|
@@ -2655,10 +3920,10 @@ async function main() {
|
|
|
2655
3920
|
if (hasLess) {
|
|
2656
3921
|
const lessRes = spawnSync12("less", ["-R", guidePath], { stdio: "inherit" });
|
|
2657
3922
|
if (lessRes.status !== 0 && lessRes.status !== null) {
|
|
2658
|
-
process.stdout.write(
|
|
3923
|
+
process.stdout.write(readFileSync6(guidePath, "utf8"));
|
|
2659
3924
|
}
|
|
2660
3925
|
} else {
|
|
2661
|
-
process.stdout.write(
|
|
3926
|
+
process.stdout.write(readFileSync6(guidePath, "utf8"));
|
|
2662
3927
|
}
|
|
2663
3928
|
process.exit(0);
|
|
2664
3929
|
break;
|