@pieai/pro-gov 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/assets/portfolio-dashboard/app.js +3 -3
- package/cli-guide.md +13 -9
- package/dist/cli.js +562 -163
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -2584,6 +2584,9 @@ function formatProjectLensInspection(report) {
|
|
|
2584
2584
|
`ai-host-ssot: ${report.hostSsot.compliant ? "compliant" : "non-compliant"}`,
|
|
2585
2585
|
`claude-entry-link: ${formatLink(report.hostSsot.claudeEntry)}`,
|
|
2586
2586
|
`claude-skills-link: ${formatLink(report.hostSsot.claudeSkills)}`,
|
|
2587
|
+
`user-host-ssot: ${report.userHostSsot.compliant ? "compliant" : "non-compliant"}`,
|
|
2588
|
+
`verification: ${report.verification.status} (missing: ${formatList(report.verification.missing)})`,
|
|
2589
|
+
`redundancy: ${report.redundancy.status}`,
|
|
2587
2590
|
`package-scripts: ${formatList(report.packageJson?.scripts ?? [])}`,
|
|
2588
2591
|
`dependencies: ${formatList(report.packageJson?.dependencies ?? [])}`,
|
|
2589
2592
|
`dev-dependencies: ${formatList(report.packageJson?.devDependencies ?? [])}`,
|
|
@@ -2624,6 +2627,20 @@ function renderProjectLensMarkdownReport(report) {
|
|
|
2624
2627
|
`- Canonical skills: \`${report.hostSsot.canonicalSkills.path}\` (${report.hostSsot.canonicalSkills.status})`,
|
|
2625
2628
|
`- Claude skills: ${formatLink(report.hostSsot.claudeSkills)}`,
|
|
2626
2629
|
`- Issues: ${formatList(report.hostSsot.issues)}`,
|
|
2630
|
+
`- User skills SSOT: ${report.userHostSsot.compliant ? "compliant" : "non-compliant"}`,
|
|
2631
|
+
`- User SSOT issues: ${formatList(report.userHostSsot.issues)}`,
|
|
2632
|
+
"",
|
|
2633
|
+
"## Verification Gates",
|
|
2634
|
+
"",
|
|
2635
|
+
`- Status: ${report.verification.status}`,
|
|
2636
|
+
`- Required scripts: ${formatList(report.verification.requiredScripts)}`,
|
|
2637
|
+
`- Missing scripts: ${formatList(report.verification.missing)}`,
|
|
2638
|
+
"",
|
|
2639
|
+
"## Redundancy Evidence",
|
|
2640
|
+
"",
|
|
2641
|
+
`- Status: ${report.redundancy.status}`,
|
|
2642
|
+
`- Legacy directories: ${formatList(report.redundancy.legacyDirectories.map((entry) => entry.path))}`,
|
|
2643
|
+
`- Playwright caches: ${formatList(report.redundancy.playwrightCaches.filter((entry) => entry.exists).map((entry) => `${entry.path} (${entry.bytes} bytes)`))}`,
|
|
2627
2644
|
"",
|
|
2628
2645
|
"## Package",
|
|
2629
2646
|
"",
|
|
@@ -2669,8 +2686,9 @@ function formatLink(link) {
|
|
|
2669
2686
|
|
|
2670
2687
|
// src/lens/scan.ts
|
|
2671
2688
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
2672
|
-
import { existsSync as
|
|
2673
|
-
import {
|
|
2689
|
+
import { existsSync as existsSync18, readdirSync as readdirSync8, readFileSync as readFileSync12, statSync as statSync4 } from "node:fs";
|
|
2690
|
+
import { homedir as homedir2 } from "node:os";
|
|
2691
|
+
import { join as join20, relative as relative7 } from "node:path";
|
|
2674
2692
|
|
|
2675
2693
|
// src/host-ssot.ts
|
|
2676
2694
|
import {
|
|
@@ -2795,6 +2813,117 @@ function safeLstat2(path) {
|
|
|
2795
2813
|
}
|
|
2796
2814
|
}
|
|
2797
2815
|
|
|
2816
|
+
// src/portfolio/redundancy.ts
|
|
2817
|
+
import { existsSync as existsSync16, readdirSync as readdirSync7, statSync as statSync3 } from "node:fs";
|
|
2818
|
+
import { homedir } from "node:os";
|
|
2819
|
+
import { join as join18 } from "node:path";
|
|
2820
|
+
var DEFAULT_CACHE_THRESHOLD_BYTES = 1e9;
|
|
2821
|
+
var MAX_CACHE_ENTRIES = 2e4;
|
|
2822
|
+
function inspectProjectRedundancy(root, options = {}) {
|
|
2823
|
+
const legacyDirectories = inspectLegacyDirectories(root);
|
|
2824
|
+
const homeDir = options.homeDir ?? homedir();
|
|
2825
|
+
const cachePaths = getPlaywrightCachePaths(homeDir, options.playwrightBrowsersPath ?? process.env.PLAYWRIGHT_BROWSERS_PATH);
|
|
2826
|
+
const playwrightCaches = cachePaths.map((path) => inspectPlaywrightCache(path));
|
|
2827
|
+
const cacheThresholdBytes = options.cacheThresholdBytes ?? DEFAULT_CACHE_THRESHOLD_BYTES;
|
|
2828
|
+
const status = legacyDirectories.length > 0 || playwrightCaches.some((cache) => cache.exists && (cache.bytes >= cacheThresholdBytes || cache.truncated)) ? "attention" : "clean";
|
|
2829
|
+
return { status, legacyDirectories, playwrightCaches };
|
|
2830
|
+
}
|
|
2831
|
+
function inspectLegacyDirectories(root) {
|
|
2832
|
+
const relativePath = ".agent";
|
|
2833
|
+
const path = join18(root, relativePath);
|
|
2834
|
+
if (!existsSync16(path)) return [];
|
|
2835
|
+
const stats = collectDirectoryStats(path);
|
|
2836
|
+
return [{
|
|
2837
|
+
path: relativePath,
|
|
2838
|
+
kind: "legacy-ai-directory",
|
|
2839
|
+
fileCount: stats.fileCount,
|
|
2840
|
+
bytes: stats.bytes,
|
|
2841
|
+
reason: "\u9879\u76EE\u7EA7\u65E7 AI \u5BBF\u4E3B\u76EE\u5F55\uFF1B\u5E94\u4E0E .agents/skills \u7684 SSOT \u9010\u9879\u6BD4\u5BF9\u540E\u518D\u51B3\u5B9A\u662F\u5426\u8FC1\u79FB\uFF0C\u626B\u63CF\u5668\u4E0D\u81EA\u52A8\u5220\u9664\u3002"
|
|
2842
|
+
}];
|
|
2843
|
+
}
|
|
2844
|
+
function getPlaywrightCachePaths(homeDir, configuredPath) {
|
|
2845
|
+
const candidates = [
|
|
2846
|
+
configuredPath && configuredPath !== "0" ? configuredPath : void 0,
|
|
2847
|
+
join18(homeDir, "Library/Caches/ms-playwright"),
|
|
2848
|
+
join18(homeDir, ".cache/ms-playwright"),
|
|
2849
|
+
join18(homeDir, "AppData/Local/ms-playwright")
|
|
2850
|
+
].filter((path) => Boolean(path));
|
|
2851
|
+
return [...new Set(candidates)];
|
|
2852
|
+
}
|
|
2853
|
+
function inspectPlaywrightCache(path) {
|
|
2854
|
+
if (!existsSync16(path)) {
|
|
2855
|
+
return { path, exists: false, fileCount: 0, bytes: 0, revisionCount: 0, truncated: false };
|
|
2856
|
+
}
|
|
2857
|
+
const stats = collectDirectoryStats(path);
|
|
2858
|
+
let revisionCount = 0;
|
|
2859
|
+
try {
|
|
2860
|
+
revisionCount = readdirSync7(path, { withFileTypes: true }).filter((entry) => entry.isDirectory()).length;
|
|
2861
|
+
} catch {
|
|
2862
|
+
revisionCount = 0;
|
|
2863
|
+
}
|
|
2864
|
+
return { path, exists: true, fileCount: stats.fileCount, bytes: stats.bytes, revisionCount, truncated: stats.truncated };
|
|
2865
|
+
}
|
|
2866
|
+
function collectDirectoryStats(root) {
|
|
2867
|
+
let fileCount = 0;
|
|
2868
|
+
let bytes = 0;
|
|
2869
|
+
let truncated = false;
|
|
2870
|
+
const pending = [root];
|
|
2871
|
+
while (pending.length > 0) {
|
|
2872
|
+
const current = pending.pop();
|
|
2873
|
+
if (!current) continue;
|
|
2874
|
+
let entries;
|
|
2875
|
+
try {
|
|
2876
|
+
entries = readdirSync7(current, { withFileTypes: true });
|
|
2877
|
+
} catch {
|
|
2878
|
+
continue;
|
|
2879
|
+
}
|
|
2880
|
+
for (const entry of entries) {
|
|
2881
|
+
if (fileCount >= MAX_CACHE_ENTRIES) {
|
|
2882
|
+
truncated = true;
|
|
2883
|
+
break;
|
|
2884
|
+
}
|
|
2885
|
+
const path = join18(current, entry.name);
|
|
2886
|
+
if (entry.isDirectory()) {
|
|
2887
|
+
pending.push(path);
|
|
2888
|
+
} else if (entry.isFile()) {
|
|
2889
|
+
fileCount += 1;
|
|
2890
|
+
try {
|
|
2891
|
+
bytes += statSync3(path).size;
|
|
2892
|
+
} catch {
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
2895
|
+
}
|
|
2896
|
+
if (truncated) break;
|
|
2897
|
+
}
|
|
2898
|
+
return { fileCount, bytes, truncated };
|
|
2899
|
+
}
|
|
2900
|
+
|
|
2901
|
+
// src/portfolio/verification.ts
|
|
2902
|
+
import { existsSync as existsSync17, readFileSync as readFileSync11 } from "node:fs";
|
|
2903
|
+
import { join as join19 } from "node:path";
|
|
2904
|
+
var REQUIRED_PROJECT_SCRIPTS = ["typecheck", "lint", "format:check", "verify"];
|
|
2905
|
+
function inspectProjectVerification(root) {
|
|
2906
|
+
const packageJson = readPackageJson(join19(root, "package.json"));
|
|
2907
|
+
const scripts = Object.fromEntries(
|
|
2908
|
+
REQUIRED_PROJECT_SCRIPTS.map((name) => [name, typeof packageJson?.scripts?.[name] === "string" ? packageJson.scripts[name] : void 0])
|
|
2909
|
+
);
|
|
2910
|
+
const missing = REQUIRED_PROJECT_SCRIPTS.filter((name) => typeof scripts[name] !== "string" || scripts[name]?.trim().length === 0);
|
|
2911
|
+
return {
|
|
2912
|
+
status: packageJson ? missing.length === 0 ? "compliant" : "attention" : "unknown",
|
|
2913
|
+
requiredScripts: REQUIRED_PROJECT_SCRIPTS,
|
|
2914
|
+
scripts,
|
|
2915
|
+
missing
|
|
2916
|
+
};
|
|
2917
|
+
}
|
|
2918
|
+
function readPackageJson(path) {
|
|
2919
|
+
if (!existsSync17(path)) return void 0;
|
|
2920
|
+
try {
|
|
2921
|
+
return JSON.parse(readFileSync11(path, "utf8"));
|
|
2922
|
+
} catch {
|
|
2923
|
+
return void 0;
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
|
|
2798
2927
|
// src/lens/scan.ts
|
|
2799
2928
|
var ignoredDirectories = /* @__PURE__ */ new Set([
|
|
2800
2929
|
".git",
|
|
@@ -2811,7 +2940,7 @@ function scanProjectLensTarget(targetDir, options = {}) {
|
|
|
2811
2940
|
const markdownFiles = files.filter(
|
|
2812
2941
|
(file) => file.startsWith("docs/") && file.endsWith(".md")
|
|
2813
2942
|
);
|
|
2814
|
-
const packageJson =
|
|
2943
|
+
const packageJson = readPackageJson2(targetDir);
|
|
2815
2944
|
return {
|
|
2816
2945
|
targetDir,
|
|
2817
2946
|
scanScope: {
|
|
@@ -2821,25 +2950,31 @@ function scanProjectLensTarget(targetDir, options = {}) {
|
|
|
2821
2950
|
excludedFileCount: candidateFiles.length - files.length
|
|
2822
2951
|
},
|
|
2823
2952
|
aiEntryFiles: ["AGENTS.md", "CLAUDE.md"].filter(
|
|
2824
|
-
(file) =>
|
|
2953
|
+
(file) => existsSync18(join20(targetDir, file))
|
|
2825
2954
|
),
|
|
2826
2955
|
aiConfigFiles: [],
|
|
2827
2956
|
hostSsot: inspectProjectHostSsot(targetDir),
|
|
2957
|
+
userHostSsot: inspectUserSkillsSsot(options.homeDir ?? process.env.HOME ?? homedir2()),
|
|
2958
|
+
verification: inspectProjectVerification(targetDir),
|
|
2959
|
+
redundancy: inspectProjectRedundancy(targetDir, {
|
|
2960
|
+
homeDir: options.homeDir,
|
|
2961
|
+
playwrightBrowsersPath: options.playwrightBrowsersPath
|
|
2962
|
+
}),
|
|
2828
2963
|
packageJson,
|
|
2829
2964
|
docs: {
|
|
2830
|
-
hasDocsDirectory:
|
|
2965
|
+
hasDocsDirectory: existsSync18(join20(targetDir, "docs")),
|
|
2831
2966
|
markdownFileCount: markdownFiles.length,
|
|
2832
2967
|
governanceFiles: markdownFiles.filter((file) => file.startsWith("docs/governance/") || file.startsWith("docs/policy/")).sort()
|
|
2833
2968
|
},
|
|
2834
2969
|
git: readGitState(targetDir),
|
|
2835
|
-
largeFiles: files.map((file) => ({ path: file, bytes:
|
|
2970
|
+
largeFiles: files.map((file) => ({ path: file, bytes: statSync4(join20(targetDir, file)).size })).filter((file) => file.bytes >= largeFileBytes).sort((a, b) => b.bytes - a.bytes || a.path.localeCompare(b.path)).slice(0, 25)
|
|
2836
2971
|
};
|
|
2837
2972
|
}
|
|
2838
|
-
function
|
|
2839
|
-
const packageJsonPath =
|
|
2840
|
-
if (!
|
|
2973
|
+
function readPackageJson2(targetDir) {
|
|
2974
|
+
const packageJsonPath = join20(targetDir, "package.json");
|
|
2975
|
+
if (!existsSync18(packageJsonPath)) return void 0;
|
|
2841
2976
|
try {
|
|
2842
|
-
const packageJson = JSON.parse(
|
|
2977
|
+
const packageJson = JSON.parse(readFileSync12(packageJsonPath, "utf8"));
|
|
2843
2978
|
return {
|
|
2844
2979
|
scripts: Object.keys(packageJson.scripts ?? {}).sort(),
|
|
2845
2980
|
dependencies: Object.keys(packageJson.dependencies ?? {}).sort(),
|
|
@@ -2877,7 +3012,7 @@ function listProjectFiles(targetDir) {
|
|
|
2877
3012
|
"-z"
|
|
2878
3013
|
]);
|
|
2879
3014
|
if (gitFiles.ok) {
|
|
2880
|
-
return gitFiles.stdout.split("\0").filter(Boolean).map(toUnixPath4).filter((file) =>
|
|
3015
|
+
return gitFiles.stdout.split("\0").filter(Boolean).map(toUnixPath4).filter((file) => existsSync18(join20(targetDir, file))).sort();
|
|
2881
3016
|
}
|
|
2882
3017
|
const files = [];
|
|
2883
3018
|
collectFiles2(targetDir, targetDir, files);
|
|
@@ -2895,13 +3030,13 @@ function isFirstPartyEvidenceFile(file) {
|
|
|
2895
3030
|
return !excludedEvidencePrefixes.some((prefix) => file.startsWith(prefix));
|
|
2896
3031
|
}
|
|
2897
3032
|
function collectFiles2(rootDir, currentDir, files) {
|
|
2898
|
-
if (!
|
|
2899
|
-
for (const entry of
|
|
3033
|
+
if (!existsSync18(currentDir)) return;
|
|
3034
|
+
for (const entry of readdirSync8(currentDir, { withFileTypes: true })) {
|
|
2900
3035
|
if (entry.isDirectory()) {
|
|
2901
3036
|
if (ignoredDirectories.has(entry.name)) continue;
|
|
2902
|
-
collectFiles2(rootDir,
|
|
3037
|
+
collectFiles2(rootDir, join20(currentDir, entry.name), files);
|
|
2903
3038
|
} else if (entry.isFile()) {
|
|
2904
|
-
files.push(toUnixPath4(relative7(rootDir,
|
|
3039
|
+
files.push(toUnixPath4(relative7(rootDir, join20(currentDir, entry.name))));
|
|
2905
3040
|
}
|
|
2906
3041
|
}
|
|
2907
3042
|
}
|
|
@@ -3057,16 +3192,16 @@ function printUsage3() {
|
|
|
3057
3192
|
}
|
|
3058
3193
|
|
|
3059
3194
|
// src/commands/portfolio.ts
|
|
3060
|
-
import { existsSync as
|
|
3061
|
-
import { join as
|
|
3195
|
+
import { existsSync as existsSync24, readFileSync as readFileSync18 } from "node:fs";
|
|
3196
|
+
import { join as join25 } from "node:path";
|
|
3062
3197
|
|
|
3063
3198
|
// src/portfolio/manifest.ts
|
|
3064
|
-
import { existsSync as
|
|
3199
|
+
import { existsSync as existsSync19, readFileSync as readFileSync13 } from "node:fs";
|
|
3065
3200
|
import { dirname as dirname11, isAbsolute as isAbsolute4, resolve as resolve4 } from "node:path";
|
|
3066
3201
|
function loadPortfolioManifest(configPath) {
|
|
3067
3202
|
let parsed;
|
|
3068
3203
|
try {
|
|
3069
|
-
parsed = JSON.parse(
|
|
3204
|
+
parsed = JSON.parse(readFileSync13(configPath, "utf8"));
|
|
3070
3205
|
} catch (error) {
|
|
3071
3206
|
return {
|
|
3072
3207
|
configPath,
|
|
@@ -3235,7 +3370,7 @@ function validateEndpoint(value, field, issues, technologyCatalog) {
|
|
|
3235
3370
|
});
|
|
3236
3371
|
return;
|
|
3237
3372
|
}
|
|
3238
|
-
if (!
|
|
3373
|
+
if (!existsSync19(value.path)) {
|
|
3239
3374
|
issues.push({
|
|
3240
3375
|
type: "missing-path",
|
|
3241
3376
|
id: typeof value.id === "string" ? value.id : void 0,
|
|
@@ -3264,7 +3399,7 @@ function validateTechnologyGovernance(value, issues) {
|
|
|
3264
3399
|
issues.push({ type: "invalid-field", field: "technologyGovernance", message: "Portfolio technologyGovernance must be an object." });
|
|
3265
3400
|
return { technologies, projectTypes };
|
|
3266
3401
|
}
|
|
3267
|
-
validateAllowedFields(value, "technologyGovernance", ["strategySource", "technologies", "projectTypes"], issues);
|
|
3402
|
+
validateAllowedFields(value, "technologyGovernance", ["strategySource", "versionPolicy", "technologies", "projectTypes"], issues);
|
|
3268
3403
|
if (value.strategySource !== void 0 && (typeof value.strategySource !== "string" || value.strategySource.length === 0)) {
|
|
3269
3404
|
issues.push({ type: "invalid-field", field: "technologyGovernance.strategySource", message: "Technology strategySource must be a non-empty string." });
|
|
3270
3405
|
}
|
|
@@ -3327,8 +3462,102 @@ function validateTechnologyGovernance(value, issues) {
|
|
|
3327
3462
|
if (!technologies.has(technology)) issues.push({ type: "invalid-field", field: "technologyGovernance.projectTypes", message: `Project type ${projectType.id} references unknown technology: ${technology}` });
|
|
3328
3463
|
}
|
|
3329
3464
|
}
|
|
3465
|
+
validateVersionPolicy(value.versionPolicy, projectTypes, issues);
|
|
3330
3466
|
return { technologies, projectTypes };
|
|
3331
3467
|
}
|
|
3468
|
+
function validateVersionPolicy(value, projectTypes, issues) {
|
|
3469
|
+
if (value === void 0) return;
|
|
3470
|
+
if (!isRecord(value)) {
|
|
3471
|
+
issues.push({ type: "invalid-field", field: "technologyGovernance.versionPolicy", message: "Technology versionPolicy must be an object." });
|
|
3472
|
+
return;
|
|
3473
|
+
}
|
|
3474
|
+
validateAllowedFields(value, "versionPolicy", ["schemaVersion", "packageManager", "runtime", "runtimes", "packages"], issues);
|
|
3475
|
+
if (value.schemaVersion !== 1) {
|
|
3476
|
+
issues.push({ type: "invalid-field", field: "technologyGovernance.versionPolicy.schemaVersion", message: "Technology versionPolicy schemaVersion must be 1." });
|
|
3477
|
+
}
|
|
3478
|
+
validateVersionRequirement(value.packageManager, "technologyGovernance.versionPolicy.packageManager", issues);
|
|
3479
|
+
validateVersionRequirement(value.runtime, "technologyGovernance.versionPolicy.runtime", issues);
|
|
3480
|
+
validateRuntimeRequirements(value.runtimes, projectTypes, issues);
|
|
3481
|
+
if (!Array.isArray(value.packages)) {
|
|
3482
|
+
issues.push({ type: "invalid-field", field: "technologyGovernance.versionPolicy.packages", message: "Technology versionPolicy packages must be an array." });
|
|
3483
|
+
return;
|
|
3484
|
+
}
|
|
3485
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3486
|
+
for (const entry of value.packages) {
|
|
3487
|
+
if (!isRecord(entry)) {
|
|
3488
|
+
issues.push({ type: "invalid-field", field: "technologyGovernance.versionPolicy.packages", message: "Version policy package entries must be objects." });
|
|
3489
|
+
continue;
|
|
3490
|
+
}
|
|
3491
|
+
validateAllowedFields(entry, "versionPolicy.package", ["name", "version", "appliesTo"], issues);
|
|
3492
|
+
const name = typeof entry.name === "string" ? entry.name : "";
|
|
3493
|
+
if (!name || seen.has(name)) {
|
|
3494
|
+
issues.push({ type: "invalid-field", field: "technologyGovernance.versionPolicy.packages.name", message: `Version policy package name must be non-empty and unique: ${String(entry.name)}` });
|
|
3495
|
+
} else seen.add(name);
|
|
3496
|
+
if (typeof entry.version !== "string" || !isExactVersion(entry.version)) {
|
|
3497
|
+
issues.push({ type: "invalid-field", field: "technologyGovernance.versionPolicy.packages.version", message: `Version policy package version must be exact semver: ${String(entry.version)}` });
|
|
3498
|
+
}
|
|
3499
|
+
if (entry.appliesTo !== void 0) {
|
|
3500
|
+
validateOptionalStringArray(entry.appliesTo, name, "technologyGovernance.versionPolicy.packages.appliesTo", issues);
|
|
3501
|
+
if (Array.isArray(entry.appliesTo)) {
|
|
3502
|
+
for (const projectType of entry.appliesTo) {
|
|
3503
|
+
if (typeof projectType === "string" && !projectTypes.has(projectType)) {
|
|
3504
|
+
issues.push({ type: "invalid-field", field: "technologyGovernance.versionPolicy.packages.appliesTo", message: `Version policy references unknown project type: ${projectType}` });
|
|
3505
|
+
}
|
|
3506
|
+
}
|
|
3507
|
+
}
|
|
3508
|
+
}
|
|
3509
|
+
}
|
|
3510
|
+
}
|
|
3511
|
+
function validateRuntimeRequirements(value, projectTypes, issues) {
|
|
3512
|
+
if (value === void 0) return;
|
|
3513
|
+
const field = "technologyGovernance.versionPolicy.runtimes";
|
|
3514
|
+
if (!Array.isArray(value)) {
|
|
3515
|
+
issues.push({ type: "invalid-field", field, message: "Technology versionPolicy runtimes must be an array." });
|
|
3516
|
+
return;
|
|
3517
|
+
}
|
|
3518
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3519
|
+
for (const entry of value) {
|
|
3520
|
+
if (!isRecord(entry)) {
|
|
3521
|
+
issues.push({ type: "invalid-field", field, message: "Version policy runtime entries must be objects." });
|
|
3522
|
+
continue;
|
|
3523
|
+
}
|
|
3524
|
+
validateAllowedFields(entry, "versionPolicy.runtimeEntry", ["name", "version", "appliesTo"], issues);
|
|
3525
|
+
const name = typeof entry.name === "string" ? entry.name : "";
|
|
3526
|
+
if (!name || seen.has(name)) {
|
|
3527
|
+
issues.push({ type: "invalid-field", field: `${field}.name`, message: `Version policy runtime name must be non-empty and unique: ${String(entry.name)}` });
|
|
3528
|
+
} else seen.add(name);
|
|
3529
|
+
if (typeof entry.version !== "string" || !isExactVersion(entry.version)) {
|
|
3530
|
+
issues.push({ type: "invalid-field", field: `${field}.version`, message: `Version policy runtime version must be exact semver: ${String(entry.version)}` });
|
|
3531
|
+
}
|
|
3532
|
+
if (entry.appliesTo !== void 0) {
|
|
3533
|
+
validateOptionalStringArray(entry.appliesTo, name, `${field}.appliesTo`, issues);
|
|
3534
|
+
if (Array.isArray(entry.appliesTo)) {
|
|
3535
|
+
for (const projectType of entry.appliesTo) {
|
|
3536
|
+
if (typeof projectType === "string" && !projectTypes.has(projectType)) {
|
|
3537
|
+
issues.push({ type: "invalid-field", field: `${field}.appliesTo`, message: `Version policy references unknown project type: ${projectType}` });
|
|
3538
|
+
}
|
|
3539
|
+
}
|
|
3540
|
+
}
|
|
3541
|
+
}
|
|
3542
|
+
}
|
|
3543
|
+
}
|
|
3544
|
+
function validateVersionRequirement(value, field, issues) {
|
|
3545
|
+
if (value === void 0) return;
|
|
3546
|
+
if (!isRecord(value)) {
|
|
3547
|
+
issues.push({ type: "invalid-field", field, message: "Version requirement must be an object." });
|
|
3548
|
+
return;
|
|
3549
|
+
}
|
|
3550
|
+
validateAllowedFields(value, field, ["name", "version"], issues);
|
|
3551
|
+
if (typeof value.name !== "string" || value.name.length === 0) {
|
|
3552
|
+
issues.push({ type: "invalid-field", field: `${field}.name`, message: "Version requirement name must be non-empty." });
|
|
3553
|
+
}
|
|
3554
|
+
if (typeof value.version !== "string" || !isExactVersion(value.version)) {
|
|
3555
|
+
issues.push({ type: "invalid-field", field: `${field}.version`, message: `Version requirement version must be exact semver: ${String(value.version)}` });
|
|
3556
|
+
}
|
|
3557
|
+
}
|
|
3558
|
+
function isExactVersion(value) {
|
|
3559
|
+
return /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(value);
|
|
3560
|
+
}
|
|
3332
3561
|
function isRepositoryRelativePath(value) {
|
|
3333
3562
|
if (value.length === 0 || isAbsolute4(value)) return false;
|
|
3334
3563
|
const segments = value.replaceAll("\\", "/").split("/");
|
|
@@ -3502,11 +3731,11 @@ function isRecord(value) {
|
|
|
3502
3731
|
}
|
|
3503
3732
|
|
|
3504
3733
|
// src/portfolio/doctor.ts
|
|
3505
|
-
import { spawnSync as
|
|
3506
|
-
import { existsSync as
|
|
3734
|
+
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
3735
|
+
import { existsSync as existsSync22, readFileSync as readFileSync16 } from "node:fs";
|
|
3507
3736
|
import { createRequire as createRequire2 } from "node:module";
|
|
3508
|
-
import { homedir } from "node:os";
|
|
3509
|
-
import { dirname as dirname12, join as
|
|
3737
|
+
import { homedir as homedir3 } from "node:os";
|
|
3738
|
+
import { dirname as dirname12, join as join23 } from "node:path";
|
|
3510
3739
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
3511
3740
|
|
|
3512
3741
|
// src/host-tooling/inventory.ts
|
|
@@ -3597,13 +3826,13 @@ function isRecord2(value) {
|
|
|
3597
3826
|
}
|
|
3598
3827
|
|
|
3599
3828
|
// src/portfolio/asset-state.ts
|
|
3600
|
-
import { existsSync as
|
|
3601
|
-
import { join as
|
|
3829
|
+
import { existsSync as existsSync20, lstatSync as lstatSync7, readFileSync as readFileSync14 } from "node:fs";
|
|
3830
|
+
import { join as join21 } from "node:path";
|
|
3602
3831
|
function comparePortfolioAssetState(options) {
|
|
3603
3832
|
const expectedManifest = readPlanDocument(options.expectedPlan, ".pro-gov/assets.json");
|
|
3604
3833
|
const expectedLock = readPlanDocument(options.expectedPlan, ".pro-gov/assets.lock.json");
|
|
3605
|
-
const currentManifest = readJsonFile(
|
|
3606
|
-
const currentLock = readJsonFile(
|
|
3834
|
+
const currentManifest = readJsonFile(join21(options.targetDir, ".pro-gov/assets.json"));
|
|
3835
|
+
const currentLock = readJsonFile(join21(options.targetDir, ".pro-gov/assets.lock.json"));
|
|
3607
3836
|
const issues = [];
|
|
3608
3837
|
if (!sameStrings(currentManifest?.bundleIds, expectedManifest?.bundleIds)) {
|
|
3609
3838
|
issues.push({
|
|
@@ -3627,7 +3856,7 @@ function comparePortfolioAssetState(options) {
|
|
|
3627
3856
|
for (const entry of currentLock?.assets ?? []) {
|
|
3628
3857
|
if (expectedTargets.has(entry.targetPath)) continue;
|
|
3629
3858
|
if (options.expectedPlan.actions.some((action) => action.type === "adopt-symlink" && action.assetId === entry.id && action.legacyTargetPath === entry.targetPath)) continue;
|
|
3630
|
-
const targetAbsolutePath =
|
|
3859
|
+
const targetAbsolutePath = join21(options.targetDir, entry.targetPath);
|
|
3631
3860
|
if (!pathIsSymlink(targetAbsolutePath)) continue;
|
|
3632
3861
|
issues.push({
|
|
3633
3862
|
type: "orphaned-managed-symlink",
|
|
@@ -3649,9 +3878,9 @@ function readPlanDocument(plan, targetPath) {
|
|
|
3649
3878
|
}
|
|
3650
3879
|
}
|
|
3651
3880
|
function readJsonFile(path) {
|
|
3652
|
-
if (!
|
|
3881
|
+
if (!existsSync20(path)) return void 0;
|
|
3653
3882
|
try {
|
|
3654
|
-
return JSON.parse(
|
|
3883
|
+
return JSON.parse(readFileSync14(path, "utf8"));
|
|
3655
3884
|
} catch {
|
|
3656
3885
|
return void 0;
|
|
3657
3886
|
}
|
|
@@ -3678,6 +3907,95 @@ function pathIsSymlink(path) {
|
|
|
3678
3907
|
}
|
|
3679
3908
|
}
|
|
3680
3909
|
|
|
3910
|
+
// src/portfolio/version-policy.ts
|
|
3911
|
+
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
3912
|
+
import { existsSync as existsSync21, readFileSync as readFileSync15 } from "node:fs";
|
|
3913
|
+
import { join as join22 } from "node:path";
|
|
3914
|
+
function inspectVersionPolicy(root, policy, projectType) {
|
|
3915
|
+
if (!policy) return { status: "compliant", packages: [], runtimes: [], attentionCount: 0 };
|
|
3916
|
+
const packageJson = readJson2(join22(root, "package.json"));
|
|
3917
|
+
const packageManager = policy.packageManager ? inspectPackageManager(packageJson, policy.packageManager.name, policy.packageManager.version) : void 0;
|
|
3918
|
+
const runtime = policy.runtime ? inspectRuntime(policy.runtime.name, policy.runtime.version) : void 0;
|
|
3919
|
+
const runtimes = (policy.runtimes ?? []).map((requirement) => {
|
|
3920
|
+
if (requirement.appliesTo && (!projectType || !requirement.appliesTo.includes(projectType))) {
|
|
3921
|
+
return { name: requirement.name, expected: requirement.version, status: "not-applicable" };
|
|
3922
|
+
}
|
|
3923
|
+
return inspectRuntime(requirement.name, requirement.version);
|
|
3924
|
+
});
|
|
3925
|
+
const packages = policy.packages.map((requirement) => {
|
|
3926
|
+
if (requirement.appliesTo && (!projectType || !requirement.appliesTo.includes(projectType))) {
|
|
3927
|
+
return { name: requirement.name, expected: requirement.version, status: "not-applicable" };
|
|
3928
|
+
}
|
|
3929
|
+
const declared = findDeclaredVersion(packageJson, requirement.name);
|
|
3930
|
+
const installed = readInstalledVersion(root, requirement.name);
|
|
3931
|
+
const status = declared === void 0 ? requirement.appliesTo ? "missing" : "not-applicable" : declared !== requirement.version ? "drift" : installed !== requirement.version ? installed === void 0 ? "missing" : "drift" : "compliant";
|
|
3932
|
+
return {
|
|
3933
|
+
name: requirement.name,
|
|
3934
|
+
expected: requirement.version,
|
|
3935
|
+
declared,
|
|
3936
|
+
installed,
|
|
3937
|
+
actual: installed,
|
|
3938
|
+
status
|
|
3939
|
+
};
|
|
3940
|
+
});
|
|
3941
|
+
const all = [packageManager, runtime, ...runtimes, ...packages].filter((item) => item !== void 0);
|
|
3942
|
+
const attentionCount = all.filter((item) => item.status !== "compliant" && item.status !== "not-applicable").length;
|
|
3943
|
+
return {
|
|
3944
|
+
status: attentionCount === 0 ? "compliant" : "attention",
|
|
3945
|
+
packageManager,
|
|
3946
|
+
runtime,
|
|
3947
|
+
runtimes,
|
|
3948
|
+
packages,
|
|
3949
|
+
attentionCount
|
|
3950
|
+
};
|
|
3951
|
+
}
|
|
3952
|
+
function inspectPackageManager(packageJson, expectedName, expectedVersion) {
|
|
3953
|
+
const declared = typeof packageJson?.packageManager === "string" ? packageJson.packageManager : void 0;
|
|
3954
|
+
const expected = `${expectedName}@${expectedVersion}`;
|
|
3955
|
+
return {
|
|
3956
|
+
name: expectedName,
|
|
3957
|
+
expected: expectedVersion,
|
|
3958
|
+
actual: declared,
|
|
3959
|
+
declared,
|
|
3960
|
+
status: declared === void 0 ? "missing" : declared === expected ? "compliant" : "drift"
|
|
3961
|
+
};
|
|
3962
|
+
}
|
|
3963
|
+
function inspectRuntime(expectedName, expectedVersion) {
|
|
3964
|
+
const actual = readRuntimeVersion(expectedName);
|
|
3965
|
+
return {
|
|
3966
|
+
name: expectedName,
|
|
3967
|
+
expected: expectedVersion,
|
|
3968
|
+
actual,
|
|
3969
|
+
status: actual === void 0 ? "missing" : actual === expectedVersion ? "compliant" : "drift"
|
|
3970
|
+
};
|
|
3971
|
+
}
|
|
3972
|
+
function readRuntimeVersion(name) {
|
|
3973
|
+
if (name === "node") return process.versions.node;
|
|
3974
|
+
if (name !== "deno") return void 0;
|
|
3975
|
+
const result = spawnSync5("deno", ["--version"], { encoding: "utf8" });
|
|
3976
|
+
if (result.status !== 0) return void 0;
|
|
3977
|
+
return /^deno\s+(\d+\.\d+\.\d+)/m.exec(result.stdout)?.[1];
|
|
3978
|
+
}
|
|
3979
|
+
function findDeclaredVersion(packageJson, name) {
|
|
3980
|
+
for (const section of ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]) {
|
|
3981
|
+
const value = packageJson?.[section]?.[name];
|
|
3982
|
+
if (typeof value === "string") return value;
|
|
3983
|
+
}
|
|
3984
|
+
return void 0;
|
|
3985
|
+
}
|
|
3986
|
+
function readInstalledVersion(root, name) {
|
|
3987
|
+
const packageJson = readJson2(join22(root, "node_modules", name, "package.json"));
|
|
3988
|
+
return typeof packageJson?.version === "string" ? packageJson.version : void 0;
|
|
3989
|
+
}
|
|
3990
|
+
function readJson2(path) {
|
|
3991
|
+
if (!existsSync21(path)) return void 0;
|
|
3992
|
+
try {
|
|
3993
|
+
return JSON.parse(readFileSync15(path, "utf8"));
|
|
3994
|
+
} catch {
|
|
3995
|
+
return void 0;
|
|
3996
|
+
}
|
|
3997
|
+
}
|
|
3998
|
+
|
|
3681
3999
|
// src/portfolio/doctor.ts
|
|
3682
4000
|
function inspectPortfolio(options) {
|
|
3683
4001
|
const expectedPackageVersions = getExpectedPackageVersions();
|
|
@@ -3687,14 +4005,15 @@ function inspectPortfolio(options) {
|
|
|
3687
4005
|
agentAssetsDir: options.agentAssetsDir,
|
|
3688
4006
|
registry: options.registry,
|
|
3689
4007
|
bundles: options.bundles,
|
|
3690
|
-
expectedPackageVersions
|
|
4008
|
+
expectedPackageVersions,
|
|
4009
|
+
versionPolicy: options.manifest.technologyGovernance?.versionPolicy
|
|
3691
4010
|
}));
|
|
3692
4011
|
return {
|
|
3693
4012
|
ok: hostTooling.issues.length === 0 && targets.every((target) => target.issues.length === 0),
|
|
3694
4013
|
portfolioId: options.manifest.portfolioId,
|
|
3695
4014
|
expectedPackageVersions,
|
|
3696
4015
|
hostTooling,
|
|
3697
|
-
hostSsot: { userSkills: inspectUserSkillsSsot(options.homeDir ??
|
|
4016
|
+
hostSsot: { userSkills: inspectUserSkillsSsot(options.homeDir ?? homedir3()) },
|
|
3698
4017
|
targets
|
|
3699
4018
|
};
|
|
3700
4019
|
}
|
|
@@ -3702,11 +4021,11 @@ function inspectTarget(options) {
|
|
|
3702
4021
|
const { target } = options;
|
|
3703
4022
|
const hostSsot = inspectProjectHostSsot(target.path);
|
|
3704
4023
|
const issues = [];
|
|
3705
|
-
const packageJson =
|
|
4024
|
+
const packageJson = readJson3(join23(target.path, "package.json"));
|
|
3706
4025
|
const packages = {};
|
|
3707
4026
|
for (const packageName of ["@pieai/pro-gov", "@pieai/doc-gov"]) {
|
|
3708
4027
|
const declared = packageJson?.devDependencies?.[packageName] ?? packageJson?.dependencies?.[packageName];
|
|
3709
|
-
const installedPackage =
|
|
4028
|
+
const installedPackage = readJson3(join23(target.path, "node_modules", packageName, "package.json"));
|
|
3710
4029
|
const installed = installedPackage?.version;
|
|
3711
4030
|
const expected = options.expectedPackageVersions[packageName];
|
|
3712
4031
|
packages[packageName] = { declared, installed, expected };
|
|
@@ -3726,6 +4045,8 @@ function inspectTarget(options) {
|
|
|
3726
4045
|
}
|
|
3727
4046
|
}
|
|
3728
4047
|
const checks = runTargetChecks(target);
|
|
4048
|
+
const versions = inspectVersionPolicy(target.path, options.versionPolicy, target.projectType);
|
|
4049
|
+
const verification = inspectProjectVerification(target.path);
|
|
3729
4050
|
for (const check of checks) {
|
|
3730
4051
|
if (check.status === 0) continue;
|
|
3731
4052
|
issues.push({
|
|
@@ -3760,7 +4081,7 @@ function inspectTarget(options) {
|
|
|
3760
4081
|
type: "asset-lock-drift",
|
|
3761
4082
|
message: error instanceof Error ? error.message : String(error)
|
|
3762
4083
|
});
|
|
3763
|
-
if (!
|
|
4084
|
+
if (!existsSync22(join23(target.path, ".pro-gov/assets.json"))) {
|
|
3764
4085
|
issues.push({ type: "bundle-drift", message: "Target asset manifest is missing." });
|
|
3765
4086
|
}
|
|
3766
4087
|
}
|
|
@@ -3772,19 +4093,21 @@ function inspectTarget(options) {
|
|
|
3772
4093
|
git: inspectGit(target.path),
|
|
3773
4094
|
checks,
|
|
3774
4095
|
hostSsot,
|
|
4096
|
+
versions,
|
|
4097
|
+
verification,
|
|
3775
4098
|
issues: deduplicateIssues(issues)
|
|
3776
4099
|
};
|
|
3777
4100
|
}
|
|
3778
4101
|
function readTargetAssetHost(targetDir) {
|
|
3779
|
-
const lockfile =
|
|
4102
|
+
const lockfile = readJson3(join23(targetDir, ".pro-gov/assets.lock.json"));
|
|
3780
4103
|
return isAssetRegistryHost(lockfile?.host) ? lockfile.host : void 0;
|
|
3781
4104
|
}
|
|
3782
4105
|
function isAssetRegistryHost(value) {
|
|
3783
4106
|
return value === "codex" || value === "claude-code" || value === "gemini-cli" || value === "antigravity";
|
|
3784
4107
|
}
|
|
3785
4108
|
function runTargetChecks(target) {
|
|
3786
|
-
const proGovCli =
|
|
3787
|
-
const docGovCli =
|
|
4109
|
+
const proGovCli = join23(target.path, "node_modules/@pieai/pro-gov/dist/cli.js");
|
|
4110
|
+
const docGovCli = join23(target.path, "node_modules/@pieai/doc-gov/dist/cli.js");
|
|
3788
4111
|
const commands = [
|
|
3789
4112
|
{
|
|
3790
4113
|
name: "pro-gov doctor",
|
|
@@ -3795,8 +4118,8 @@ function runTargetChecks(target) {
|
|
|
3795
4118
|
{ name: "doc-gov scan --check", cli: docGovCli, args: ["scan", "--check"] }
|
|
3796
4119
|
];
|
|
3797
4120
|
return commands.map((command2) => {
|
|
3798
|
-
if (!
|
|
3799
|
-
const result =
|
|
4121
|
+
if (!existsSync22(command2.cli)) return { name: command2.name, status: null };
|
|
4122
|
+
const result = spawnSync6(process.execPath, [command2.cli, ...command2.args], {
|
|
3800
4123
|
cwd: target.path,
|
|
3801
4124
|
encoding: "utf8",
|
|
3802
4125
|
timeout: 3e4
|
|
@@ -3805,13 +4128,13 @@ function runTargetChecks(target) {
|
|
|
3805
4128
|
});
|
|
3806
4129
|
}
|
|
3807
4130
|
function inspectGit(path) {
|
|
3808
|
-
const inside =
|
|
4131
|
+
const inside = spawnSync6("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
3809
4132
|
cwd: path,
|
|
3810
4133
|
encoding: "utf8"
|
|
3811
4134
|
});
|
|
3812
4135
|
if (inside.status !== 0) return { isRepository: false, dirty: false };
|
|
3813
|
-
const status =
|
|
3814
|
-
const branch =
|
|
4136
|
+
const status = spawnSync6("git", ["status", "--porcelain"], { cwd: path, encoding: "utf8" });
|
|
4137
|
+
const branch = spawnSync6("git", ["branch", "--show-current"], { cwd: path, encoding: "utf8" });
|
|
3815
4138
|
return {
|
|
3816
4139
|
isRepository: true,
|
|
3817
4140
|
dirty: status.stdout.trim().length > 0,
|
|
@@ -3819,11 +4142,11 @@ function inspectGit(path) {
|
|
|
3819
4142
|
};
|
|
3820
4143
|
}
|
|
3821
4144
|
function getExpectedPackageVersions() {
|
|
3822
|
-
const proGovPackage =
|
|
4145
|
+
const proGovPackage = readJson3(findOwnPackageJson());
|
|
3823
4146
|
let docGovVersion;
|
|
3824
4147
|
try {
|
|
3825
4148
|
const require2 = createRequire2(import.meta.url);
|
|
3826
|
-
const docGovPackage =
|
|
4149
|
+
const docGovPackage = readJson3(require2.resolve("@pieai/doc-gov/package.json"));
|
|
3827
4150
|
docGovVersion = docGovPackage?.version;
|
|
3828
4151
|
} catch {
|
|
3829
4152
|
docGovVersion = void 0;
|
|
@@ -3836,16 +4159,16 @@ function getExpectedPackageVersions() {
|
|
|
3836
4159
|
function findOwnPackageJson() {
|
|
3837
4160
|
let current = dirname12(fileURLToPath3(import.meta.url));
|
|
3838
4161
|
for (let depth = 0; depth < 5; depth += 1) {
|
|
3839
|
-
const candidate =
|
|
3840
|
-
if (
|
|
4162
|
+
const candidate = join23(current, "package.json");
|
|
4163
|
+
if (existsSync22(candidate)) return candidate;
|
|
3841
4164
|
current = dirname12(current);
|
|
3842
4165
|
}
|
|
3843
4166
|
return "";
|
|
3844
4167
|
}
|
|
3845
|
-
function
|
|
3846
|
-
if (!path || !
|
|
4168
|
+
function readJson3(path) {
|
|
4169
|
+
if (!path || !existsSync22(path)) return void 0;
|
|
3847
4170
|
try {
|
|
3848
|
-
return JSON.parse(
|
|
4171
|
+
return JSON.parse(readFileSync16(path, "utf8"));
|
|
3849
4172
|
} catch {
|
|
3850
4173
|
return void 0;
|
|
3851
4174
|
}
|
|
@@ -3864,27 +4187,31 @@ function deduplicateIssues(issues) {
|
|
|
3864
4187
|
import { execFileSync } from "node:child_process";
|
|
3865
4188
|
import {
|
|
3866
4189
|
cpSync as cpSync2,
|
|
3867
|
-
existsSync as
|
|
4190
|
+
existsSync as existsSync23,
|
|
3868
4191
|
lstatSync as lstatSync8,
|
|
3869
4192
|
mkdirSync as mkdirSync8,
|
|
3870
|
-
readFileSync as
|
|
3871
|
-
readdirSync as
|
|
4193
|
+
readFileSync as readFileSync17,
|
|
4194
|
+
readdirSync as readdirSync9,
|
|
3872
4195
|
realpathSync as realpathSync4,
|
|
3873
|
-
statSync as
|
|
4196
|
+
statSync as statSync5,
|
|
3874
4197
|
writeFileSync as writeFileSync7
|
|
3875
4198
|
} from "node:fs";
|
|
3876
|
-
import { homedir as
|
|
3877
|
-
import { dirname as dirname13, join as
|
|
4199
|
+
import { homedir as homedir4 } from "node:os";
|
|
4200
|
+
import { dirname as dirname13, join as join24, relative as relative8, resolve as resolve5, sep } from "node:path";
|
|
3878
4201
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
3879
4202
|
var CURRENT_ROUTER_VERSION = "1.1";
|
|
3880
4203
|
function inspectPortfolioAiHealth(options) {
|
|
3881
|
-
const
|
|
3882
|
-
const
|
|
3883
|
-
|
|
4204
|
+
const allEndpoints = collectEndpoints(options.manifest);
|
|
4205
|
+
const endpoints = options.targetId && options.targetId !== "all" ? allEndpoints.filter(({ endpoint }) => endpoint.id === options.targetId) : allEndpoints;
|
|
4206
|
+
if (options.targetId && options.targetId !== "all" && endpoints.length === 0) {
|
|
4207
|
+
throw new Error(`Unknown portfolio target: ${options.targetId}`);
|
|
4208
|
+
}
|
|
4209
|
+
const secretsRoot = options.secretsRoot ?? join24(dirname13(options.manifest.controlPlane?.path ?? allEndpoints[0]?.endpoint.path ?? process.cwd()), ".secrets");
|
|
4210
|
+
const homeDir = options.homeDir ?? process.env.HOME ?? homedir4();
|
|
3884
4211
|
const grokVersion = commandVersion("grok");
|
|
3885
4212
|
const executionEngineRoot = options.manifest.executionEngine?.path;
|
|
3886
4213
|
const skillRegistry = inspectSkillRegistry(executionEngineRoot);
|
|
3887
|
-
const expectedPackageVersion = packageVersion(
|
|
4214
|
+
const expectedPackageVersion = packageVersion(join24(executionEngineRoot ?? "", "packages/pro-gov/package.json"));
|
|
3888
4215
|
const repositories = endpoints.map(({ endpoint, role }) => inspectRepository(
|
|
3889
4216
|
endpoint,
|
|
3890
4217
|
role,
|
|
@@ -3897,20 +4224,27 @@ function inspectPortfolioAiHealth(options) {
|
|
|
3897
4224
|
const summary = { healthy: 0, attention: 0, unhealthy: 0 };
|
|
3898
4225
|
for (const repository of repositories) summary[repository.status] += 1;
|
|
3899
4226
|
return {
|
|
3900
|
-
schemaVersion:
|
|
4227
|
+
schemaVersion: 5,
|
|
3901
4228
|
portfolioId: options.manifest.portfolioId,
|
|
3902
4229
|
generatedAt: options.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
4230
|
+
coverage: {
|
|
4231
|
+
mode: options.targetId && options.targetId !== "all" ? "single" : "all",
|
|
4232
|
+
targetId: options.targetId && options.targetId !== "all" ? options.targetId : void 0,
|
|
4233
|
+
coveredRepositoryIds: repositories.map((repository) => repository.id),
|
|
4234
|
+
totalRepositoryCount: allEndpoints.length
|
|
4235
|
+
},
|
|
3903
4236
|
privacy: "Names, paths, counts, scopes, safe health states, and configuration origins only. Secret values, environment values, MCP commands, arguments, URLs, headers, MCP environment maps, and raw process environments are never retained or rendered.",
|
|
3904
4237
|
secretsRoot: inspectSecretsRoot(secretsRoot),
|
|
3905
4238
|
hostEnvironment: inspectHostEnvironment(
|
|
3906
4239
|
homeDir,
|
|
3907
4240
|
grokVersion,
|
|
3908
|
-
|
|
4241
|
+
allEndpoints.map(({ endpoint }) => endpoint.path),
|
|
3909
4242
|
options.manifest.specialistChecks?.devspace
|
|
3910
4243
|
),
|
|
3911
4244
|
skillRegistry,
|
|
3912
4245
|
technologyGovernance: {
|
|
3913
4246
|
strategySource: options.manifest.technologyGovernance?.strategySource,
|
|
4247
|
+
versionPolicy: options.manifest.technologyGovernance?.versionPolicy,
|
|
3914
4248
|
projectTypes: options.manifest.technologyGovernance?.projectTypes.length ?? 0,
|
|
3915
4249
|
technologies: options.manifest.technologyGovernance?.technologies.length ?? 0
|
|
3916
4250
|
},
|
|
@@ -3918,19 +4252,44 @@ function inspectPortfolioAiHealth(options) {
|
|
|
3918
4252
|
repositories
|
|
3919
4253
|
};
|
|
3920
4254
|
}
|
|
4255
|
+
function mergePortfolioAiHealthReport(existing, latest, allRepositoryIds) {
|
|
4256
|
+
const repositoriesById = /* @__PURE__ */ new Map();
|
|
4257
|
+
for (const repository of existing?.repositories ?? []) repositoriesById.set(repository.id, repository);
|
|
4258
|
+
for (const repository of latest.repositories) repositoriesById.set(repository.id, repository);
|
|
4259
|
+
const repositories = allRepositoryIds.map((id) => repositoriesById.get(id)).filter((repository) => repository !== void 0);
|
|
4260
|
+
const coveredIds = /* @__PURE__ */ new Set([
|
|
4261
|
+
...existing?.coverage?.coveredRepositoryIds ?? [],
|
|
4262
|
+
...latest.coverage.coveredRepositoryIds
|
|
4263
|
+
]);
|
|
4264
|
+
const coveredRepositoryIds = allRepositoryIds.filter((id) => coveredIds.has(id));
|
|
4265
|
+
const summary = { healthy: 0, attention: 0, unhealthy: 0 };
|
|
4266
|
+
for (const repository of repositories) summary[repository.status] += 1;
|
|
4267
|
+
const complete = coveredRepositoryIds.length === allRepositoryIds.length;
|
|
4268
|
+
return {
|
|
4269
|
+
...latest,
|
|
4270
|
+
repositories,
|
|
4271
|
+
summary,
|
|
4272
|
+
coverage: {
|
|
4273
|
+
mode: complete ? "all" : "single",
|
|
4274
|
+
targetId: complete ? void 0 : latest.coverage.targetId,
|
|
4275
|
+
coveredRepositoryIds,
|
|
4276
|
+
totalRepositoryCount: allRepositoryIds.length
|
|
4277
|
+
}
|
|
4278
|
+
};
|
|
4279
|
+
}
|
|
3921
4280
|
function writePortfolioAiHealthReport(report, outDir) {
|
|
3922
4281
|
mkdirSync8(outDir, { recursive: true });
|
|
3923
4282
|
const dashboardAssets = findDashboardAssets();
|
|
3924
4283
|
for (const file of ["index.html", "app.js", "app.css"]) {
|
|
3925
|
-
const source =
|
|
3926
|
-
if (!
|
|
3927
|
-
cpSync2(source,
|
|
4284
|
+
const source = join24(dashboardAssets, file);
|
|
4285
|
+
if (!existsSync23(source)) throw new Error(`Portfolio dashboard asset is missing: ${source}`);
|
|
4286
|
+
cpSync2(source, join24(outDir, file));
|
|
3928
4287
|
}
|
|
3929
|
-
const jsonPath =
|
|
3930
|
-
const htmlPath =
|
|
4288
|
+
const jsonPath = join24(outDir, "portfolio-ai-health.json");
|
|
4289
|
+
const htmlPath = join24(outDir, "index.html");
|
|
3931
4290
|
writeFileSync7(jsonPath, `${JSON.stringify(report, null, 2)}
|
|
3932
4291
|
`);
|
|
3933
|
-
writeFileSync7(
|
|
4292
|
+
writeFileSync7(join24(outDir, "data.js"), `window.__PORTFOLIO_AI_HEALTH__ = ${safeJavaScriptJson(report)};
|
|
3934
4293
|
`);
|
|
3935
4294
|
return { jsonPath, htmlPath };
|
|
3936
4295
|
}
|
|
@@ -3957,15 +4316,18 @@ function inspectRepository(endpoint, role, secretsRoot, homeDir, expectedPackage
|
|
|
3957
4316
|
const hooks = inspectHooks(root);
|
|
3958
4317
|
const docs = inspectDocs(root, role === "execution-engine" ? void 0 : expectedPackageVersion);
|
|
3959
4318
|
const mcp = {
|
|
3960
|
-
codexProject: tomlMcpNames(
|
|
3961
|
-
claudeCodeProjectShared: jsonObjectKeys(
|
|
4319
|
+
codexProject: tomlMcpNames(join24(root, ".codex/config.toml")),
|
|
4320
|
+
claudeCodeProjectShared: jsonObjectKeys(join24(root, ".mcp.json"), "mcpServers"),
|
|
3962
4321
|
claudeCodeProjectLocal: claudeProjectLocalMcpNames(homeDir, root),
|
|
3963
|
-
grokProject: tomlMcpNames(
|
|
4322
|
+
grokProject: tomlMcpNames(join24(root, ".grok/config.toml")),
|
|
3964
4323
|
grokEffective: grokInspection.effectiveMcp,
|
|
3965
4324
|
grokInspection: grokInspection.inspection
|
|
3966
4325
|
};
|
|
3967
4326
|
const secrets = inspectRepositorySecrets(root, endpoint.id, secretsRoot, git.isRepository, endpoint.environmentPolicy);
|
|
3968
4327
|
const projectModel = inspectProjectModel(root, endpoint, technologyGovernance);
|
|
4328
|
+
const versions = inspectVersionPolicy(root, technologyGovernance?.versionPolicy, endpoint.projectType);
|
|
4329
|
+
const verification = inspectProjectVerification(root);
|
|
4330
|
+
const redundancy = inspectProjectRedundancy(root, { homeDir });
|
|
3969
4331
|
const recommendations = [];
|
|
3970
4332
|
if (!git.isRepository) recommendations.push("\u8BE5\u8DEF\u5F84\u4E0D\u662F Git \u4ED3\u5E93\uFF1B\u786E\u8BA4\u6E05\u5355\u8DEF\u5F84\u662F\u5426\u6B63\u786E\u3002");
|
|
3971
4333
|
if (git.unmergedBranches.length > 0) recommendations.push(`\u6709 ${git.unmergedBranches.length} \u6761\u5206\u652F\u5C1A\u672A\u5408\u5165\u5F53\u524D HEAD\uFF1A${git.unmergedBranches.join(", ")}\u3002`);
|
|
@@ -4002,12 +4364,15 @@ function inspectRepository(endpoint, role, secretsRoot, homeDir, expectedPackage
|
|
|
4002
4364
|
if (missingBaseline.length > 0) recommendations.push(`\u6280\u672F\u57FA\u7EBF\u7F3A\u5C11\u53EF\u9A8C\u8BC1\u4FE1\u53F7\uFF1A${missingBaseline.map((technology) => technology.label).join("\u3001")}\u3002`);
|
|
4003
4365
|
const missingSelected = projectModel.optionalCapabilities.filter((technology) => technology.selected && !technology.detected);
|
|
4004
4366
|
if (missingSelected.length > 0) recommendations.push(`\u5DF2\u9009\u80FD\u529B\u7F3A\u5C11\u53EF\u9A8C\u8BC1\u4FE1\u53F7\uFF1A${missingSelected.map((technology) => technology.label).join("\u3001")}\u3002`);
|
|
4367
|
+
if (versions.status === "attention") recommendations.push(`\u6280\u672F\u7248\u672C\u7B56\u7565\u6709 ${versions.attentionCount} \u9879\u6F02\u79FB\u6216\u7F3A\u5931\uFF1B\u58F0\u660E\u7248\u672C\u4E0E\u5DF2\u5B89\u88C5\u7248\u672C\u5FC5\u987B\u7CBE\u786E\u5BF9\u9F50\u3002`);
|
|
4368
|
+
if (verification.status === "attention") recommendations.push(`\u9A8C\u8BC1\u811A\u672C\u7F3A\u5931\uFF1A${verification.missing.join("\u3001")}\uFF1BPGS \u8981\u6C42 typecheck\u3001lint\u3001format:check\u3001verify \u53EF\u53D1\u73B0\u3002`);
|
|
4369
|
+
if (redundancy.status === "attention") recommendations.push("\u53D1\u73B0\u65E7 AI \u76EE\u5F55\u6216\u5927\u578B Playwright \u7F13\u5B58\uFF1B\u4EC5\u63D0\u4F9B\u8BC1\u636E\uFF0C\u786E\u8BA4\u5F52\u5C5E\u540E\u518D\u7531\u4EBA\u5DE5\u6E05\u7406\u3002");
|
|
4005
4370
|
return {
|
|
4006
4371
|
id: endpoint.id,
|
|
4007
4372
|
role,
|
|
4008
4373
|
path: root,
|
|
4009
4374
|
profile: "profile" in endpoint ? endpoint.profile : void 0,
|
|
4010
|
-
status: deriveStatus(role, entries, git, hooks, skills, hostSsot, secrets, docs, projectModel, Boolean(technologyGovernance)),
|
|
4375
|
+
status: deriveStatus(role, entries, git, hooks, skills, hostSsot, secrets, docs, projectModel, Boolean(technologyGovernance), versions, verification, redundancy),
|
|
4011
4376
|
recommendations,
|
|
4012
4377
|
git,
|
|
4013
4378
|
entries,
|
|
@@ -4017,10 +4382,13 @@ function inspectRepository(endpoint, role, secretsRoot, homeDir, expectedPackage
|
|
|
4017
4382
|
hostSsot,
|
|
4018
4383
|
secrets,
|
|
4019
4384
|
docs,
|
|
4020
|
-
projectModel
|
|
4385
|
+
projectModel,
|
|
4386
|
+
versions,
|
|
4387
|
+
verification,
|
|
4388
|
+
redundancy
|
|
4021
4389
|
};
|
|
4022
4390
|
}
|
|
4023
|
-
function deriveStatus(role, entries, git, hooks, skills, hostSsot, secrets, docs, projectModel, technologyGovernanceConfigured) {
|
|
4391
|
+
function deriveStatus(role, entries, git, hooks, skills, hostSsot, secrets, docs, projectModel, technologyGovernanceConfigured, versions, verification, redundancy) {
|
|
4024
4392
|
if (!git.isRepository || entries.agents === "missing" || entries.claude === "dangling-symlink" || entries.gemini === "dangling-symlink" || skills.canonical.some((item) => item.kind === "dangling-symlink") || secrets.repositoryEnvFiles.some((file) => file.tracked && !file.template && !file.fixture) || hasUnsafeCentralSecretPermissions(secrets)) return "unhealthy";
|
|
4025
4393
|
const missingBaseline = projectModel.baseline.some((technology) => !technology.detected && !hasBaselineException(projectModel, technology.id));
|
|
4026
4394
|
const missingSelected = projectModel.optionalCapabilities.some((technology) => technology.selected && !technology.detected);
|
|
@@ -4029,7 +4397,7 @@ function deriveStatus(role, entries, git, hooks, skills, hostSsot, secrets, docs
|
|
|
4029
4397
|
const packageVersionNeedsReview = docs.packages.expected !== void 0 && !docs.packages.aligned;
|
|
4030
4398
|
const routerVersionNeedsReview = !docs.routerAligned;
|
|
4031
4399
|
const targetManifestMissing = role === "target" && !docs.manifest;
|
|
4032
|
-
if (entries.agents !== "pgs-router" || entries.claude !== "agents-symlink" || hasWorkflowReminderHooks(hooks) || skills.claudeCompatibility === "duplicate-directory" || skills.claudeCompatibility === "dangling-symlink" || !hostSsot.compliant || git.branches.length > 1 || git.worktrees.length > 1 || git.dirtyPaths.length > 0 || (git.ahead ?? 0) > 0 || secretMaterializationNeedsReview || technologyGovernanceConfigured && !projectModel.projectType || missingBaseline || missingSelected || packageVersionNeedsReview || routerVersionNeedsReview || targetManifestMissing) return "attention";
|
|
4400
|
+
if (entries.agents !== "pgs-router" || entries.claude !== "agents-symlink" || hasWorkflowReminderHooks(hooks) || skills.claudeCompatibility === "duplicate-directory" || skills.claudeCompatibility === "dangling-symlink" || !hostSsot.compliant || git.branches.length > 1 || git.worktrees.length > 1 || git.dirtyPaths.length > 0 || (git.ahead ?? 0) > 0 || secretMaterializationNeedsReview || technologyGovernanceConfigured && !projectModel.projectType || missingBaseline || missingSelected || packageVersionNeedsReview || routerVersionNeedsReview || targetManifestMissing || versions.status === "attention" || verification.status === "attention" || redundancy.legacyDirectories.length > 0) return "attention";
|
|
4033
4401
|
return "healthy";
|
|
4034
4402
|
}
|
|
4035
4403
|
function inspectProjectModel(root, endpoint, governance) {
|
|
@@ -4039,7 +4407,7 @@ function inspectProjectModel(root, endpoint, governance) {
|
|
|
4039
4407
|
const detection = (id) => {
|
|
4040
4408
|
const technology = technologyById.get(id);
|
|
4041
4409
|
const packageMatch = technology?.packages?.some((name) => packages.has(name)) ?? false;
|
|
4042
|
-
const fileMatch = technology?.files?.some((path) =>
|
|
4410
|
+
const fileMatch = technology?.files?.some((path) => existsSync23(join24(root, path))) ?? false;
|
|
4043
4411
|
return { id, label: technology?.label ?? id, detected: packageMatch || fileMatch };
|
|
4044
4412
|
};
|
|
4045
4413
|
const selected = new Set(endpoint.capabilities ?? []);
|
|
@@ -4062,10 +4430,10 @@ function collectPackageNames(root) {
|
|
|
4062
4430
|
try {
|
|
4063
4431
|
files = splitLines(execFileSync("git", ["ls-files", "*package.json"], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }));
|
|
4064
4432
|
} catch {
|
|
4065
|
-
if (
|
|
4433
|
+
if (existsSync23(join24(root, "package.json"))) files = ["package.json"];
|
|
4066
4434
|
}
|
|
4067
4435
|
for (const file of files) {
|
|
4068
|
-
const packageJson =
|
|
4436
|
+
const packageJson = readJson4(join24(root, file));
|
|
4069
4437
|
if (!isRecord3(packageJson)) continue;
|
|
4070
4438
|
for (const section of ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]) {
|
|
4071
4439
|
const dependencies = packageJson[section];
|
|
@@ -4103,9 +4471,9 @@ function inspectGit2(root) {
|
|
|
4103
4471
|
};
|
|
4104
4472
|
}
|
|
4105
4473
|
function inspectEntries(root) {
|
|
4106
|
-
const agentsPath =
|
|
4107
|
-
const agents = !
|
|
4108
|
-
const claudePath =
|
|
4474
|
+
const agentsPath = join24(root, "AGENTS.md");
|
|
4475
|
+
const agents = !existsSync23(agentsPath) ? "missing" : safeRead(agentsPath).includes("PGS-ROUTER:BEGIN") ? "pgs-router" : "custom";
|
|
4476
|
+
const claudePath = join24(root, "CLAUDE.md");
|
|
4109
4477
|
let claude = "missing";
|
|
4110
4478
|
if (pathLexists(claudePath)) {
|
|
4111
4479
|
const info = lstatSync8(claudePath);
|
|
@@ -4123,7 +4491,7 @@ function inspectEntries(root) {
|
|
|
4123
4491
|
return { agents, claude, gemini: inspectOptionalEntry(root, "GEMINI.md", agentsPath) };
|
|
4124
4492
|
}
|
|
4125
4493
|
function inspectOptionalEntry(root, filename, agentsPath) {
|
|
4126
|
-
const path =
|
|
4494
|
+
const path = join24(root, filename);
|
|
4127
4495
|
if (!pathLexists(path)) return "missing";
|
|
4128
4496
|
const info = lstatSync8(path);
|
|
4129
4497
|
if (info.isSymbolicLink()) {
|
|
@@ -4137,7 +4505,7 @@ function inspectOptionalEntry(root, filename, agentsPath) {
|
|
|
4137
4505
|
return /AGENTS\.md/.test(content) && content.length < 2e3 ? "thin-adapter" : "custom";
|
|
4138
4506
|
}
|
|
4139
4507
|
function inspectSkills(root, grokInspection) {
|
|
4140
|
-
const lock =
|
|
4508
|
+
const lock = readJson4(join24(root, ".pro-gov/assets.lock.json"));
|
|
4141
4509
|
const managed = /* @__PURE__ */ new Set();
|
|
4142
4510
|
const bundleIds = stringArray(isRecord3(lock) ? lock.bundleIds : void 0);
|
|
4143
4511
|
if (isRecord3(lock) && Array.isArray(lock.assets)) {
|
|
@@ -4147,9 +4515,9 @@ function inspectSkills(root, grokInspection) {
|
|
|
4147
4515
|
if (match) managed.add(match[1]);
|
|
4148
4516
|
}
|
|
4149
4517
|
}
|
|
4150
|
-
const skillRoot =
|
|
4518
|
+
const skillRoot = join24(root, ".agents/skills");
|
|
4151
4519
|
const canonical = pathLexists(skillRoot) && safeIsDirectory(skillRoot) ? safeReadDir(skillRoot).filter((name) => !name.startsWith(".")).map((name) => {
|
|
4152
|
-
const path =
|
|
4520
|
+
const path = join24(skillRoot, name);
|
|
4153
4521
|
const stat = lstatSync8(path);
|
|
4154
4522
|
let kind = stat.isSymbolicLink() ? "symlink" : stat.isDirectory() ? "directory" : "file";
|
|
4155
4523
|
if (stat.isSymbolicLink()) {
|
|
@@ -4175,20 +4543,20 @@ function inspectSkills(root, grokInspection) {
|
|
|
4175
4543
|
},
|
|
4176
4544
|
hosts: {
|
|
4177
4545
|
codexProject: canonical.filter((item) => item.kind !== "dangling-symlink").length,
|
|
4178
|
-
claudeCodeProject: inspectSkillRoot(
|
|
4179
|
-
grokNativeProject: inspectSkillRoot(
|
|
4546
|
+
claudeCodeProject: inspectSkillRoot(join24(root, ".claude/skills")).names.length,
|
|
4547
|
+
grokNativeProject: inspectSkillRoot(join24(root, ".grok/skills")).names.length,
|
|
4180
4548
|
grokEffective: grokInspection.skills
|
|
4181
4549
|
}
|
|
4182
4550
|
};
|
|
4183
4551
|
}
|
|
4184
4552
|
function inspectClaudeSkillRoot(root) {
|
|
4185
|
-
const path =
|
|
4553
|
+
const path = join24(root, ".claude/skills");
|
|
4186
4554
|
if (!pathLexists(path)) return "missing";
|
|
4187
4555
|
const stat = lstatSync8(path);
|
|
4188
4556
|
if (stat.isSymbolicLink()) {
|
|
4189
4557
|
try {
|
|
4190
4558
|
const target = realpathSync4(path);
|
|
4191
|
-
return target === realpathSync4(
|
|
4559
|
+
return target === realpathSync4(join24(root, ".agents/skills")) ? "shared-root" : "other";
|
|
4192
4560
|
} catch {
|
|
4193
4561
|
return "dangling-symlink";
|
|
4194
4562
|
}
|
|
@@ -4196,7 +4564,7 @@ function inspectClaudeSkillRoot(root) {
|
|
|
4196
4564
|
return stat.isDirectory() ? "duplicate-directory" : "other";
|
|
4197
4565
|
}
|
|
4198
4566
|
function inspectRepositorySecrets(root, id, secretsRoot, isRepository, environmentPolicy) {
|
|
4199
|
-
const centralPath =
|
|
4567
|
+
const centralPath = join24(secretsRoot, id);
|
|
4200
4568
|
const centralRealPath = safeRealpath(centralPath);
|
|
4201
4569
|
const localOnlyReasons = new Map(
|
|
4202
4570
|
(environmentPolicy?.localOnly ?? []).map((entry) => [entry.path, entry.reason])
|
|
@@ -4208,16 +4576,16 @@ function inspectRepositorySecrets(root, id, secretsRoot, isRepository, environme
|
|
|
4208
4576
|
tracked: isRepository ? gitTracks(root, path) : false,
|
|
4209
4577
|
template: isEnvironmentTemplate(path),
|
|
4210
4578
|
fixture: isEnvironmentFixture(path),
|
|
4211
|
-
symlink: lstatSync8(
|
|
4212
|
-
centralized: pointsInside(
|
|
4579
|
+
symlink: lstatSync8(join24(root, path)).isSymbolicLink(),
|
|
4580
|
+
centralized: pointsInside(join24(root, path), centralRealPath),
|
|
4213
4581
|
localOnly: localOnlyReason !== void 0,
|
|
4214
4582
|
...localOnlyReason !== void 0 ? { localOnlyReason } : {}
|
|
4215
4583
|
};
|
|
4216
4584
|
});
|
|
4217
4585
|
return {
|
|
4218
|
-
centralDirectory:
|
|
4219
|
-
centralMode:
|
|
4220
|
-
centralFiles:
|
|
4586
|
+
centralDirectory: existsSync23(centralPath) ? "present" : "absent",
|
|
4587
|
+
centralMode: existsSync23(centralPath) ? modeString(statSync5(centralPath).mode) : void 0,
|
|
4588
|
+
centralFiles: existsSync23(centralPath) ? collectCentralSecretFiles(centralPath) : [],
|
|
4221
4589
|
repositoryEnvFiles: envFiles
|
|
4222
4590
|
};
|
|
4223
4591
|
}
|
|
@@ -4226,11 +4594,11 @@ function collectEnvironmentFiles(root, current = root, depth = 0) {
|
|
|
4226
4594
|
if (depth > 5) return [];
|
|
4227
4595
|
const found = [];
|
|
4228
4596
|
try {
|
|
4229
|
-
for (const entry of
|
|
4597
|
+
for (const entry of readdirSync9(current, { withFileTypes: true })) {
|
|
4230
4598
|
if (entry.isDirectory()) {
|
|
4231
|
-
if (!SKIP_ENV_DIRECTORIES.has(entry.name)) found.push(...collectEnvironmentFiles(root,
|
|
4599
|
+
if (!SKIP_ENV_DIRECTORIES.has(entry.name)) found.push(...collectEnvironmentFiles(root, join24(current, entry.name), depth + 1));
|
|
4232
4600
|
} else if (isEnvironmentFilename(entry.name) && !isProviderGeneratedEnvironmentFile(entry.name)) {
|
|
4233
|
-
found.push(relative8(root,
|
|
4601
|
+
found.push(relative8(root, join24(current, entry.name)));
|
|
4234
4602
|
}
|
|
4235
4603
|
}
|
|
4236
4604
|
} catch {
|
|
@@ -4242,8 +4610,8 @@ function collectCentralSecretFiles(root, current = root, depth = 0) {
|
|
|
4242
4610
|
if (depth > 3) return [];
|
|
4243
4611
|
const found = [];
|
|
4244
4612
|
try {
|
|
4245
|
-
for (const entry of
|
|
4246
|
-
const path =
|
|
4613
|
+
for (const entry of readdirSync9(current, { withFileTypes: true })) {
|
|
4614
|
+
const path = join24(current, entry.name);
|
|
4247
4615
|
if (entry.isDirectory()) found.push(...collectCentralSecretFiles(root, path, depth + 1));
|
|
4248
4616
|
else found.push({ path: relative8(root, path), mode: modeString(lstatSync8(path).mode) });
|
|
4249
4617
|
}
|
|
@@ -4286,12 +4654,12 @@ function hasUnsafeCentralSecretPermissions(secrets) {
|
|
|
4286
4654
|
return secrets.centralDirectory === "present" && (secrets.centralMode !== "700" || secrets.centralFiles.some((file) => file.mode !== "600"));
|
|
4287
4655
|
}
|
|
4288
4656
|
function inspectSecretsRoot(path) {
|
|
4289
|
-
return
|
|
4657
|
+
return existsSync23(path) ? { path, exists: true, mode: modeString(statSync5(path).mode) } : { path, exists: false };
|
|
4290
4658
|
}
|
|
4291
4659
|
function inspectHostEnvironment(homeDir, grokVersion, repositoryPaths, devspaceSettings) {
|
|
4292
|
-
const codexConfig =
|
|
4293
|
-
const claudeConfig =
|
|
4294
|
-
const grokConfig =
|
|
4660
|
+
const codexConfig = join24(homeDir, ".codex/config.toml");
|
|
4661
|
+
const claudeConfig = join24(homeDir, ".claude.json");
|
|
4662
|
+
const grokConfig = join24(homeDir, ".grok/config.toml");
|
|
4295
4663
|
const hostEnvironment = {
|
|
4296
4664
|
mcp: {
|
|
4297
4665
|
codexUser: { path: codexConfig, names: tomlMcpNames(codexConfig) },
|
|
@@ -4299,11 +4667,11 @@ function inspectHostEnvironment(homeDir, grokVersion, repositoryPaths, devspaceS
|
|
|
4299
4667
|
grokUser: { path: grokConfig, names: tomlMcpNames(grokConfig) }
|
|
4300
4668
|
},
|
|
4301
4669
|
skills: {
|
|
4302
|
-
codexUser: inspectSkillRoot(
|
|
4303
|
-
claudeCodeUser: inspectSkillRoot(
|
|
4304
|
-
grokUser: inspectSkillRoot(
|
|
4305
|
-
grokAgentsCompatibility: inspectSkillRoot(
|
|
4306
|
-
grokClaudeCompatibility: inspectSkillRoot(
|
|
4670
|
+
codexUser: inspectSkillRoot(join24(homeDir, ".agents/skills")),
|
|
4671
|
+
claudeCodeUser: inspectSkillRoot(join24(homeDir, ".claude/skills")),
|
|
4672
|
+
grokUser: inspectSkillRoot(join24(homeDir, ".grok/skills")),
|
|
4673
|
+
grokAgentsCompatibility: inspectSkillRoot(join24(homeDir, ".agents/skills")),
|
|
4674
|
+
grokClaudeCompatibility: inspectSkillRoot(join24(homeDir, ".claude/skills")),
|
|
4307
4675
|
ssot: inspectUserSkillsSsot(homeDir)
|
|
4308
4676
|
},
|
|
4309
4677
|
grok: {
|
|
@@ -4323,9 +4691,9 @@ function inspectHostEnvironment(homeDir, grokVersion, repositoryPaths, devspaceS
|
|
|
4323
4691
|
}
|
|
4324
4692
|
function inspectDevSpaceHealth(options) {
|
|
4325
4693
|
const run = options.run ?? runDevSpaceCommand;
|
|
4326
|
-
const configDirectory =
|
|
4327
|
-
const configPath =
|
|
4328
|
-
const authPath =
|
|
4694
|
+
const configDirectory = join24(options.homeDir, ".devspace");
|
|
4695
|
+
const configPath = join24(configDirectory, "config.json");
|
|
4696
|
+
const authPath = join24(configDirectory, "auth.json");
|
|
4329
4697
|
const installedResult = run("devspace", ["--version"], 3e3);
|
|
4330
4698
|
const installedVersion = installedResult.ok ? installedResult.stdout.match(/\b\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?\b/)?.[0] : void 0;
|
|
4331
4699
|
const latestResult = run("npm", ["view", "@waishnav/devspace", "version", "--registry=https://registry.npmjs.org/"], 5e3);
|
|
@@ -4335,14 +4703,14 @@ function inspectDevSpaceHealth(options) {
|
|
|
4335
4703
|
const processEnvironment = pid ? run("ps", ["eww", "-p", pid, "-o", "command="], 3e3) : void 0;
|
|
4336
4704
|
const processToolMode = processEnvironment?.ok ? processEnvironment.stdout.match(/(?:^|\s)DEVSPACE_TOOL_MODE=(minimal|full|codex)(?:\s|$)/)?.[1] : void 0;
|
|
4337
4705
|
const doctor = !installedResult.ok ? "unavailable" : run("devspace", ["doctor"], 1e4).ok ? "ok" : "failed";
|
|
4338
|
-
const configValue =
|
|
4706
|
+
const configValue = readJson4(configPath);
|
|
4339
4707
|
const configExists = isRecord3(configValue);
|
|
4340
4708
|
const allowedRoots = configExists && Array.isArray(configValue.allowedRoots) ? configValue.allowedRoots.filter((value) => typeof value === "string") : [];
|
|
4341
4709
|
const portfolioCoverage = !configExists || allowedRoots.length === 0 ? "unknown" : options.repositoryPaths.every((repositoryPath) => allowedRoots.some((root) => isPathInside(repositoryPath, root))) ? "complete" : "partial";
|
|
4342
4710
|
const bind = configExists && typeof configValue.host === "string" ? isLoopbackHost(configValue.host) ? "loopback" : "non-loopback" : "unknown";
|
|
4343
|
-
const directoryMode =
|
|
4344
|
-
const fileMode =
|
|
4345
|
-
const authMode =
|
|
4711
|
+
const directoryMode = existsSync23(configDirectory) ? modeString(statSync5(configDirectory).mode) : void 0;
|
|
4712
|
+
const fileMode = existsSync23(configPath) ? modeString(statSync5(configPath).mode) : void 0;
|
|
4713
|
+
const authMode = existsSync23(authPath) ? modeString(statSync5(authPath).mode) : void 0;
|
|
4346
4714
|
const update = installedVersion && latestVersion ? installedVersion === latestVersion ? "current" : "available" : "unknown";
|
|
4347
4715
|
const recommendations = [];
|
|
4348
4716
|
let status = "healthy";
|
|
@@ -4356,7 +4724,7 @@ function inspectDevSpaceHealth(options) {
|
|
|
4356
4724
|
};
|
|
4357
4725
|
if (!installedResult.ok) unhealthy("\u672C\u673A\u672A\u53D1\u73B0 DevSpace\uFF1B\u65E0\u6CD5\u4F7F\u7528\u5BBF\u4E3B\u5DE5\u4F5C\u533A\u670D\u52A1\u3002");
|
|
4358
4726
|
if (!configExists) unhealthy("\u7F3A\u5C11 ~/.devspace/config.json\u3002");
|
|
4359
|
-
if (!
|
|
4727
|
+
if (!existsSync23(authPath)) unhealthy("\u7F3A\u5C11 ~/.devspace/auth.json\u3002");
|
|
4360
4728
|
if (directoryMode && directoryMode !== "700") unhealthy(`~/.devspace \u76EE\u5F55\u6743\u9650\u4E3A ${directoryMode}\uFF0C\u5E94\u6536\u7D27\u4E3A 700\u3002`);
|
|
4361
4729
|
if (fileMode && fileMode !== "600") unhealthy(`DevSpace \u914D\u7F6E\u6587\u4EF6\u6743\u9650\u4E3A ${fileMode}\uFF0C\u5E94\u4E3A 600\u3002`);
|
|
4362
4730
|
if (authMode && authMode !== "600") unhealthy(`DevSpace \u8BA4\u8BC1\u6587\u4EF6\u6743\u9650\u4E3A ${authMode}\uFF0C\u5E94\u4E3A 600\u3002`);
|
|
@@ -4382,7 +4750,7 @@ function inspectDevSpaceHealth(options) {
|
|
|
4382
4750
|
exists: configExists,
|
|
4383
4751
|
...directoryMode ? { directoryMode } : {},
|
|
4384
4752
|
...fileMode ? { fileMode } : {},
|
|
4385
|
-
authExists:
|
|
4753
|
+
authExists: existsSync23(authPath),
|
|
4386
4754
|
...authMode ? { authMode } : {},
|
|
4387
4755
|
bind,
|
|
4388
4756
|
portValid: configExists && typeof configValue.port === "number" && Number.isInteger(configValue.port) && configValue.port > 0 && configValue.port <= 65535,
|
|
@@ -4416,12 +4784,12 @@ function isPathInside(path, root) {
|
|
|
4416
4784
|
}
|
|
4417
4785
|
function inspectSkillRoot(path) {
|
|
4418
4786
|
const exists = pathLexists(path) && safeIsDirectory(path);
|
|
4419
|
-
const names = exists ? safeReadDir(path).filter((name) => !name.startsWith(".") &&
|
|
4787
|
+
const names = exists ? safeReadDir(path).filter((name) => !name.startsWith(".") && existsSync23(join24(path, name, "SKILL.md"))) : [];
|
|
4420
4788
|
return { path, exists, names };
|
|
4421
4789
|
}
|
|
4422
4790
|
function claudeProjectLocalMcpNames(homeDir, root) {
|
|
4423
4791
|
if (!homeDir) return [];
|
|
4424
|
-
const value =
|
|
4792
|
+
const value = readJson4(join24(homeDir, ".claude.json"));
|
|
4425
4793
|
if (!isRecord3(value) || !isRecord3(value.projects)) return [];
|
|
4426
4794
|
const candidates = new Set([resolve5(root), safeRealpath(root)].filter((path) => Boolean(path)));
|
|
4427
4795
|
const names = /* @__PURE__ */ new Set();
|
|
@@ -4457,13 +4825,13 @@ function inspectGrokProject(root, homeDir, grokVersion) {
|
|
|
4457
4825
|
const value = JSON.parse(execFileSync("grok", ["inspect", "--json"], {
|
|
4458
4826
|
cwd: root,
|
|
4459
4827
|
encoding: "utf8",
|
|
4460
|
-
env: { ...process.env, HOME: homeDir, GROK_HOME:
|
|
4828
|
+
env: { ...process.env, HOME: homeDir, GROK_HOME: join24(homeDir, ".grok") },
|
|
4461
4829
|
maxBuffer: 10 * 1024 * 1024,
|
|
4462
4830
|
stdio: ["ignore", "pipe", "ignore"],
|
|
4463
4831
|
timeout: 8e3
|
|
4464
4832
|
}));
|
|
4465
4833
|
if (!isRecord3(value)) return empty("failed");
|
|
4466
|
-
const userClaudeNames = new Set(jsonObjectKeys(
|
|
4834
|
+
const userClaudeNames = new Set(jsonObjectKeys(join24(homeDir, ".claude.json"), "mcpServers"));
|
|
4467
4835
|
const localClaudeNames = new Set(claudeProjectLocalMcpNames(homeDir, root));
|
|
4468
4836
|
const effectiveMcp = Array.isArray(value.mcpServers) ? value.mcpServers.flatMap((item) => {
|
|
4469
4837
|
if (!isRecord3(item) || typeof item.name !== "string") return [];
|
|
@@ -4521,9 +4889,9 @@ function inferGrokMcpScope(name, sourceType, sourcePath, root, homeDir, userClau
|
|
|
4521
4889
|
}
|
|
4522
4890
|
const resolvedSource = safeRealpath(sourcePath) ?? resolve5(sourcePath);
|
|
4523
4891
|
const resolvedRoot = safeRealpath(root) ?? resolve5(root);
|
|
4524
|
-
if (resolvedSource ===
|
|
4892
|
+
if (resolvedSource === join24(resolvedRoot, ".mcp.json")) return "project-shared";
|
|
4525
4893
|
if (resolvedSource.startsWith(resolvedRoot + sep)) return "project";
|
|
4526
|
-
if (homeDir && resolvedSource ===
|
|
4894
|
+
if (homeDir && resolvedSource === join24(resolve5(homeDir), ".claude.json")) {
|
|
4527
4895
|
if (localClaudeNames.has(name)) return "project-local";
|
|
4528
4896
|
if (userClaudeNames.has(name)) return "user";
|
|
4529
4897
|
}
|
|
@@ -4558,7 +4926,7 @@ function inspectHooks(root) {
|
|
|
4558
4926
|
{ host: "codex", path: ".codex/hooks.json" }
|
|
4559
4927
|
];
|
|
4560
4928
|
return configs.map((config) => {
|
|
4561
|
-
const value =
|
|
4929
|
+
const value = readJson4(join24(root, config.path));
|
|
4562
4930
|
const counts = /* @__PURE__ */ new Map();
|
|
4563
4931
|
collectHookEvents(value, counts);
|
|
4564
4932
|
return {
|
|
@@ -4579,18 +4947,18 @@ function collectHookEvents(value, counts) {
|
|
|
4579
4947
|
}
|
|
4580
4948
|
}
|
|
4581
4949
|
function inspectDocs(root, expected) {
|
|
4582
|
-
const packageJson =
|
|
4950
|
+
const packageJson = readJson4(join24(root, "package.json"));
|
|
4583
4951
|
const dependencies = isRecord3(packageJson) ? { ...recordOrEmpty(packageJson.dependencies), ...recordOrEmpty(packageJson.devDependencies) } : {};
|
|
4584
4952
|
const docGov = dependencyVersion(dependencies["@pieai/doc-gov"]);
|
|
4585
4953
|
const proGov = dependencyVersion(dependencies["@pieai/pro-gov"]);
|
|
4586
|
-
const routerMatch = safeRead(
|
|
4954
|
+
const routerMatch = safeRead(join24(root, "AGENTS.md")).match(/PGS-ROUTER:BEGIN\s+v([0-9.]+)/);
|
|
4587
4955
|
const declared = [docGov, proGov].filter((value) => Boolean(value));
|
|
4588
4956
|
return {
|
|
4589
4957
|
routerVersion: routerMatch?.[1],
|
|
4590
4958
|
expectedRouterVersion: CURRENT_ROUTER_VERSION,
|
|
4591
4959
|
routerAligned: routerMatch?.[1] === CURRENT_ROUTER_VERSION,
|
|
4592
|
-
manifest:
|
|
4593
|
-
currentWork:
|
|
4960
|
+
manifest: existsSync23(join24(root, "docs/governance/MANIFEST.yml")),
|
|
4961
|
+
currentWork: existsSync23(join24(root, "docs/reference/execution/current-work.md")),
|
|
4594
4962
|
packages: {
|
|
4595
4963
|
expected,
|
|
4596
4964
|
docGov,
|
|
@@ -4605,7 +4973,7 @@ function dependencyVersion(value) {
|
|
|
4605
4973
|
return match?.[1];
|
|
4606
4974
|
}
|
|
4607
4975
|
function packageVersion(path) {
|
|
4608
|
-
const value =
|
|
4976
|
+
const value = readJson4(path);
|
|
4609
4977
|
return isRecord3(value) && typeof value.version === "string" ? value.version : void 0;
|
|
4610
4978
|
}
|
|
4611
4979
|
function recordOrEmpty(value) {
|
|
@@ -4613,20 +4981,20 @@ function recordOrEmpty(value) {
|
|
|
4613
4981
|
}
|
|
4614
4982
|
function inspectSkillRegistry(executionEngineRoot) {
|
|
4615
4983
|
if (!executionEngineRoot) return { source: 0, registered: 0, bundled: 0, bundles: 0 };
|
|
4616
|
-
const agentAssetsRoot =
|
|
4617
|
-
const registry =
|
|
4984
|
+
const agentAssetsRoot = join24(executionEngineRoot, "agent-assets");
|
|
4985
|
+
const registry = readJson4(join24(agentAssetsRoot, "registry.json"));
|
|
4618
4986
|
const assets = isRecord3(registry) && Array.isArray(registry.assets) ? registry.assets : [];
|
|
4619
4987
|
const registeredSkills = assets.filter((asset) => isRecord3(asset) && asset.kind === "skill");
|
|
4620
|
-
const bundleRoot =
|
|
4988
|
+
const bundleRoot = join24(agentAssetsRoot, "bundles");
|
|
4621
4989
|
const bundleFiles = safeReadDir(bundleRoot).filter((file) => file.endsWith(".json"));
|
|
4622
4990
|
const bundledIds = /* @__PURE__ */ new Set();
|
|
4623
4991
|
for (const file of bundleFiles) {
|
|
4624
|
-
const bundle =
|
|
4992
|
+
const bundle = readJson4(join24(bundleRoot, file));
|
|
4625
4993
|
if (!isRecord3(bundle) || !Array.isArray(bundle.assets)) continue;
|
|
4626
4994
|
for (const id of bundle.assets) if (typeof id === "string") bundledIds.add(id);
|
|
4627
4995
|
}
|
|
4628
|
-
const sourceRoots = [
|
|
4629
|
-
const source = sourceRoots.reduce((count, root) => count + safeReadDir(root).filter((name) =>
|
|
4996
|
+
const sourceRoots = [join24(agentAssetsRoot, "skills/pie-skills"), join24(agentAssetsRoot, "skills/npx-skills/.agents/skills")];
|
|
4997
|
+
const source = sourceRoots.reduce((count, root) => count + safeReadDir(root).filter((name) => existsSync23(join24(root, name, "SKILL.md"))).length, 0);
|
|
4630
4998
|
return {
|
|
4631
4999
|
source,
|
|
4632
5000
|
registered: registeredSkills.length,
|
|
@@ -4635,12 +5003,12 @@ function inspectSkillRegistry(executionEngineRoot) {
|
|
|
4635
5003
|
};
|
|
4636
5004
|
}
|
|
4637
5005
|
function jsonObjectKeys(path, key) {
|
|
4638
|
-
const value =
|
|
5006
|
+
const value = readJson4(path);
|
|
4639
5007
|
if (!isRecord3(value) || !isRecord3(value[key])) return [];
|
|
4640
5008
|
return Object.keys(value[key]).sort();
|
|
4641
5009
|
}
|
|
4642
5010
|
function tomlMcpNames(path) {
|
|
4643
|
-
if (!
|
|
5011
|
+
if (!existsSync23(path)) return [];
|
|
4644
5012
|
const names = /* @__PURE__ */ new Set();
|
|
4645
5013
|
for (const line of safeRead(path).split(/\r?\n/)) {
|
|
4646
5014
|
const match = line.match(/^\s*\[mcp_servers\.(?:"([^"]+)"|([^\.\]]+))\]\s*$/);
|
|
@@ -4649,30 +5017,30 @@ function tomlMcpNames(path) {
|
|
|
4649
5017
|
}
|
|
4650
5018
|
return [...names].sort();
|
|
4651
5019
|
}
|
|
4652
|
-
function
|
|
5020
|
+
function readJson4(path) {
|
|
4653
5021
|
try {
|
|
4654
|
-
return JSON.parse(
|
|
5022
|
+
return JSON.parse(readFileSync17(path, "utf8"));
|
|
4655
5023
|
} catch {
|
|
4656
5024
|
return void 0;
|
|
4657
5025
|
}
|
|
4658
5026
|
}
|
|
4659
5027
|
function safeRead(path) {
|
|
4660
5028
|
try {
|
|
4661
|
-
return
|
|
5029
|
+
return readFileSync17(path, "utf8");
|
|
4662
5030
|
} catch {
|
|
4663
5031
|
return "";
|
|
4664
5032
|
}
|
|
4665
5033
|
}
|
|
4666
5034
|
function safeReadDir(path) {
|
|
4667
5035
|
try {
|
|
4668
|
-
return
|
|
5036
|
+
return readdirSync9(path).sort();
|
|
4669
5037
|
} catch {
|
|
4670
5038
|
return [];
|
|
4671
5039
|
}
|
|
4672
5040
|
}
|
|
4673
5041
|
function safeIsDirectory(path) {
|
|
4674
5042
|
try {
|
|
4675
|
-
return
|
|
5043
|
+
return statSync5(path).isDirectory();
|
|
4676
5044
|
} catch {
|
|
4677
5045
|
return false;
|
|
4678
5046
|
}
|
|
@@ -4709,14 +5077,14 @@ function findDashboardAssets() {
|
|
|
4709
5077
|
const packageRoot2 = dirname13(dirname13(fileURLToPath4(import.meta.url)));
|
|
4710
5078
|
const candidates = [
|
|
4711
5079
|
process.env.PGS_DASHBOARD_ASSETS_DIR,
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
5080
|
+
join24(packageRoot2, ".dashboard-build"),
|
|
5081
|
+
join24(packageRoot2, "assets/portfolio-dashboard"),
|
|
5082
|
+
join24(process.cwd(), ".dashboard-build"),
|
|
5083
|
+
join24(process.cwd(), "assets/portfolio-dashboard"),
|
|
5084
|
+
join24(process.cwd(), "packages/pro-gov/.dashboard-build"),
|
|
5085
|
+
join24(process.cwd(), "packages/pro-gov/assets/portfolio-dashboard")
|
|
4718
5086
|
].filter((value) => Boolean(value));
|
|
4719
|
-
const match = candidates.find((path) =>
|
|
5087
|
+
const match = candidates.find((path) => existsSync23(join24(path, "index.html")));
|
|
4720
5088
|
if (!match) throw new Error("Portfolio dashboard assets were not built. Run pnpm --filter @pieai/pro-gov build.");
|
|
4721
5089
|
return match;
|
|
4722
5090
|
}
|
|
@@ -4752,10 +5120,23 @@ function runPortfolioAiHealth(args) {
|
|
|
4752
5120
|
for (const issue of loaded.issues) console.error(`${issue.type}: ${issue.message}`);
|
|
4753
5121
|
return 1;
|
|
4754
5122
|
}
|
|
4755
|
-
const
|
|
5123
|
+
const targetId = options.value.targetId && options.value.targetId !== "all" ? options.value.targetId : void 0;
|
|
5124
|
+
if (targetId && !loaded.manifest.targets.some((target) => target.id === targetId)) {
|
|
5125
|
+
console.error(`Unknown portfolio target: ${targetId}`);
|
|
5126
|
+
return 1;
|
|
5127
|
+
}
|
|
5128
|
+
const latest = inspectPortfolioAiHealth({
|
|
4756
5129
|
manifest: loaded.manifest,
|
|
4757
|
-
secretsRoot: options.value.secretsRoot
|
|
5130
|
+
secretsRoot: options.value.secretsRoot,
|
|
5131
|
+
targetId
|
|
4758
5132
|
});
|
|
5133
|
+
const allRepositoryIds = [
|
|
5134
|
+
loaded.manifest.controlPlane?.id,
|
|
5135
|
+
loaded.manifest.executionEngine?.id,
|
|
5136
|
+
...loaded.manifest.targets.map((target) => target.id)
|
|
5137
|
+
].filter((id) => Boolean(id));
|
|
5138
|
+
const existing = targetId ? readExistingAiHealthReport(options.value.outDir, loaded.manifest.portfolioId) : void 0;
|
|
5139
|
+
const report = targetId ? mergePortfolioAiHealthReport(existing, latest, allRepositoryIds) : latest;
|
|
4759
5140
|
const written = writePortfolioAiHealthReport(report, options.value.outDir);
|
|
4760
5141
|
if (options.value.json) {
|
|
4761
5142
|
console.log(JSON.stringify({ ok: true, ...written, summary: report.summary }, null, 2));
|
|
@@ -4814,6 +5195,12 @@ function runPortfolioDoctor(args) {
|
|
|
4814
5195
|
for (const warning of target.hostSsot.issues) {
|
|
4815
5196
|
console.log(`${target.id} host-ssot-warning: ${warning}`);
|
|
4816
5197
|
}
|
|
5198
|
+
if (target.verification.status === "attention") {
|
|
5199
|
+
console.log(`${target.id} verification-warning: missing ${target.verification.missing.join(", ")}`);
|
|
5200
|
+
}
|
|
5201
|
+
if (target.versions.status === "attention") {
|
|
5202
|
+
console.log(`${target.id} version-policy-warning: ${target.versions.attentionCount} drift(s)`);
|
|
5203
|
+
}
|
|
4817
5204
|
}
|
|
4818
5205
|
if (result.ok) {
|
|
4819
5206
|
console.log(`portfolio doctor passed (${targets.length} targets)`);
|
|
@@ -5078,8 +5465,8 @@ function isHost2(value) {
|
|
|
5078
5465
|
return value === "codex" || value === "claude-code" || value === "gemini-cli" || value === "antigravity";
|
|
5079
5466
|
}
|
|
5080
5467
|
function findPortfolioAgentAssetsDir(manifest) {
|
|
5081
|
-
const agentAssetsDir = manifest?.executionEngine?.path ?
|
|
5082
|
-
return agentAssetsDir &&
|
|
5468
|
+
const agentAssetsDir = manifest?.executionEngine?.path ? join25(manifest.executionEngine.path, "agent-assets") : void 0;
|
|
5469
|
+
return agentAssetsDir && existsSync24(join25(agentAssetsDir, "registry.json")) ? agentAssetsDir : void 0;
|
|
5083
5470
|
}
|
|
5084
5471
|
function printUsage4() {
|
|
5085
5472
|
console.error("Usage:");
|
|
@@ -5087,12 +5474,24 @@ function printUsage4() {
|
|
|
5087
5474
|
console.error(" pro-gov portfolio plan --config <path> [--target <id|all>] [--host codex|claude-code|gemini-cli|antigravity] [--json]");
|
|
5088
5475
|
console.error(" pro-gov portfolio assets-check --config <path> [--target <id|all>] [--json]");
|
|
5089
5476
|
console.error(" pro-gov portfolio doctor --config <path> [--target <id|all>] [--json]");
|
|
5090
|
-
console.error(" pro-gov portfolio ai-health --config <path> --out <directory> [--secrets-root <directory>] [--json]");
|
|
5477
|
+
console.error(" pro-gov portfolio ai-health --config <path> --out <directory> [--target <id|all>] [--secrets-root <directory>] [--json]");
|
|
5478
|
+
}
|
|
5479
|
+
function readExistingAiHealthReport(outDir, portfolioId) {
|
|
5480
|
+
if (!outDir) return void 0;
|
|
5481
|
+
const path = join25(outDir, "portfolio-ai-health.json");
|
|
5482
|
+
if (!existsSync24(path)) return void 0;
|
|
5483
|
+
try {
|
|
5484
|
+
const value = JSON.parse(readFileSync18(path, "utf8"));
|
|
5485
|
+
if (!value || typeof value !== "object" || value.portfolioId !== portfolioId || !Array.isArray(value.repositories)) return void 0;
|
|
5486
|
+
return value;
|
|
5487
|
+
} catch {
|
|
5488
|
+
return void 0;
|
|
5489
|
+
}
|
|
5091
5490
|
}
|
|
5092
5491
|
|
|
5093
5492
|
// src/commands/sync.ts
|
|
5094
|
-
import { existsSync as
|
|
5095
|
-
import { join as
|
|
5493
|
+
import { existsSync as existsSync25, lstatSync as lstatSync9, readFileSync as readFileSync19, readlinkSync as readlinkSync4 } from "node:fs";
|
|
5494
|
+
import { join as join26 } from "node:path";
|
|
5096
5495
|
function runSync(args) {
|
|
5097
5496
|
const check = args.includes("--check");
|
|
5098
5497
|
if (!check) {
|
|
@@ -5120,7 +5519,7 @@ function runSync(args) {
|
|
|
5120
5519
|
console.log("pro-gov sync check");
|
|
5121
5520
|
console.log(`profile: ${profile}`);
|
|
5122
5521
|
for (const file of planStarterFiles(profile)) {
|
|
5123
|
-
const targetPath =
|
|
5522
|
+
const targetPath = join26(process.cwd(), file.targetPath);
|
|
5124
5523
|
const stat = safeLstat3(targetPath);
|
|
5125
5524
|
if (!stat) {
|
|
5126
5525
|
if (file.ownership === "optional-guardrail") continue;
|
|
@@ -5144,8 +5543,8 @@ function runSync(args) {
|
|
|
5144
5543
|
}
|
|
5145
5544
|
continue;
|
|
5146
5545
|
}
|
|
5147
|
-
const source =
|
|
5148
|
-
const target =
|
|
5546
|
+
const source = readFileSync19(file.absoluteSourcePath, "utf8");
|
|
5547
|
+
const target = readFileSync19(targetPath, "utf8");
|
|
5149
5548
|
if (!matchesExpectedContent(file.targetPath, source, target)) {
|
|
5150
5549
|
console.log(`different: ${file.targetPath}`);
|
|
5151
5550
|
differences += 1;
|
|
@@ -5179,7 +5578,7 @@ function normalizeMarkdownTableCell(cell) {
|
|
|
5179
5578
|
}
|
|
5180
5579
|
function inferInstalledProfile(root) {
|
|
5181
5580
|
const installed = ["engineering-runtime", "doc-only"].filter(
|
|
5182
|
-
(profile) =>
|
|
5581
|
+
(profile) => existsSync25(join26(root, `docs/governance/agents-routing/${profile}-v1.1.md`))
|
|
5183
5582
|
);
|
|
5184
5583
|
return installed.length === 1 ? installed[0] : void 0;
|
|
5185
5584
|
}
|