@pieai/pro-gov 0.3.11 → 0.3.12
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
CHANGED
|
@@ -45,6 +45,8 @@ pro-gov assets discover --target .
|
|
|
45
45
|
pro-gov assets recommend --target .
|
|
46
46
|
pro-gov portfolio check --config /path/to/portfolio.json
|
|
47
47
|
pro-gov portfolio plan --config /path/to/portfolio.json --target web-app --json
|
|
48
|
+
pro-gov portfolio assets-check --config /path/to/portfolio.json --json
|
|
49
|
+
pro-gov portfolio doctor --config /path/to/portfolio.json --json
|
|
48
50
|
pro-gov lens inspect --target .
|
|
49
51
|
pro-gov lens report --target . --out .pro-gov/lens-report.md
|
|
50
52
|
pro-gov init --profile engineering-runtime --dry-run
|
|
@@ -63,6 +65,8 @@ What these commands do:
|
|
|
63
65
|
| `assets recommend` | Suggests relevant asset bundles with reasons. | No |
|
|
64
66
|
| `portfolio check` | Validates an external portfolio manifest owned by the user or organization. | No |
|
|
65
67
|
| `portfolio plan` | Builds dry-run asset plans for manifest targets, using packaged public assets unless a full checkout is supplied. | No |
|
|
68
|
+
| `portfolio assets-check` | Checks the integrity of each target's current managed asset lock and symlinks. | No |
|
|
69
|
+
| `portfolio doctor` | Runs the offline fleet gate: package, target CLI, bundle, asset, symlink, Git-state evidence, and optional host-tooling checks. | No |
|
|
66
70
|
| `lens inspect` | Produces ProjectLens-style local evidence. | No |
|
|
67
71
|
| `lens report` | Writes the requested report file. | Only the explicit output |
|
|
68
72
|
| `init --dry-run` | Shows starter/profile files that would be needed. | No |
|
|
@@ -98,7 +102,10 @@ User-scoped skills, such as a personal loop library, are not installed into
|
|
|
98
102
|
project targets; link them once under the user's skill roots instead.
|
|
99
103
|
|
|
100
104
|
The plan is the safety gate. `apply` may update managed targets described by the
|
|
101
|
-
plan; it must not overwrite an unrelated unmanaged file.
|
|
105
|
+
plan; it must not overwrite an unrelated unmanaged file. When an asset leaves a
|
|
106
|
+
bundle, a plan may remove its old symlink only when the previous lock proves
|
|
107
|
+
ownership and both plan-time and apply-time checks confirm that the path is
|
|
108
|
+
still the same managed symlink.
|
|
102
109
|
|
|
103
110
|
These checkout-only workflows depend on a maintainer-local `agent-assets/`
|
|
104
111
|
registry when local-only assets are being used. The public repository and npm
|
|
@@ -111,7 +118,7 @@ the private-source and public-copy hashes recorded during promotion.
|
|
|
111
118
|
- `pro-gov` distributes starter, profile, integration, and adoption assets.
|
|
112
119
|
- `pro-gov assets discover|recommend` provides read-only project evidence and
|
|
113
120
|
deterministic recommendations.
|
|
114
|
-
- `pro-gov portfolio check|plan` reads an external portfolio manifest. Real
|
|
121
|
+
- `pro-gov portfolio check|plan|assets-check|doctor` reads an external portfolio manifest. Real
|
|
115
122
|
downstream project lists belong in the user's control repository, not in this
|
|
116
123
|
public package. The manifest does not require a private headquarters repo:
|
|
117
124
|
npm users can omit `controlPlane` and `executionEngine`, and PGS will use the
|
|
@@ -127,6 +134,8 @@ the private-source and public-copy hashes recorded during promotion.
|
|
|
127
134
|
use `--dry-run` and a deliberate migration so local truth is never overwritten.
|
|
128
135
|
- Superpowers, Compound Engineering, and Ponytail are external tools, not
|
|
129
136
|
bundled runtime dependencies.
|
|
137
|
+
- Optional `hostTooling` entries let `portfolio doctor` verify required Codex or
|
|
138
|
+
Claude Code plugins. PGS does not install or upgrade those plugins.
|
|
130
139
|
|
|
131
140
|
## Recommended Companion Tools
|
|
132
141
|
|
|
@@ -153,6 +153,10 @@ Managed symlinks are relative by default. A target project's normal
|
|
|
153
153
|
public package to know a maintainer's private registry. Maintainers can add
|
|
154
154
|
`--strict-registry`, or run `pro-gov portfolio assets-check --config
|
|
155
155
|
/path/to/portfolio.json`, when they need central private-registry validation.
|
|
156
|
+
Use `pro-gov portfolio doctor --config /path/to/portfolio.json` as the final
|
|
157
|
+
offline fleet gate when package versions, target-local routers and hooks,
|
|
158
|
+
declared bundles, locks, managed symlinks, and optional host plugins all need to
|
|
159
|
+
be checked together. The command reports dirty Git state but does not edit it.
|
|
156
160
|
|
|
157
161
|
When a maintainer promotes a private asset into `public-agent-assets/`, the
|
|
158
162
|
public registry must record the private-source hash and the public-copy hash.
|
|
@@ -125,4 +125,7 @@ install PGS packages
|
|
|
125
125
|
|
|
126
126
|
PGS does not automatically install, enable, update, or remove these external
|
|
127
127
|
plugins in another person's AI host. It documents the recommended boundary and
|
|
128
|
-
lets each project or user adopt tools deliberately.
|
|
128
|
+
lets each project or user adopt tools deliberately. A user-owned portfolio may
|
|
129
|
+
declare required Codex or Claude Code plugin ids in `hostTooling`; `portfolio
|
|
130
|
+
doctor` then verifies installed/enabled state and reports observed versions.
|
|
131
|
+
The host's native plugin manager remains the installation and update authority.
|
package/cli-guide.md
CHANGED
|
@@ -18,6 +18,8 @@ pro-gov assets discover --target .
|
|
|
18
18
|
pro-gov assets recommend --target .
|
|
19
19
|
pro-gov portfolio check --config /path/to/portfolio.json
|
|
20
20
|
pro-gov portfolio plan --config /path/to/portfolio.json --target web-app --json
|
|
21
|
+
pro-gov portfolio assets-check --config /path/to/portfolio.json --json
|
|
22
|
+
pro-gov portfolio doctor --config /path/to/portfolio.json --json
|
|
21
23
|
pro-gov lens inspect --target .
|
|
22
24
|
pro-gov lens report --target . --out .pro-gov/lens-report.md
|
|
23
25
|
pro-gov lens audit init --target /path/to/project --out audits/project/2026-07-01
|
|
@@ -46,6 +48,8 @@ Full upstream-checkout commands:
|
|
|
46
48
|
pro-gov assets list --json
|
|
47
49
|
pro-gov portfolio check --config /path/to/control-repo/.pro-gov/portfolio.json --json
|
|
48
50
|
pro-gov portfolio plan --config /path/to/control-repo/.pro-gov/portfolio.json --target web-app --json
|
|
51
|
+
pro-gov portfolio assets-check --config /path/to/control-repo/.pro-gov/portfolio.json --json
|
|
52
|
+
pro-gov portfolio doctor --config /path/to/control-repo/.pro-gov/portfolio.json --json
|
|
49
53
|
pro-gov assets plan --bundle base-governance --target . --out .pro-gov/asset-plan.json
|
|
50
54
|
pro-gov assets plan --bundle project-lens --target /path/to/project --host codex --placement manual --out /tmp/project-lens-plan.json
|
|
51
55
|
pro-gov assets apply --plan .pro-gov/asset-plan.json
|
|
@@ -72,6 +76,27 @@ public assets packaged with `@pieai/pro-gov`. Add `executionEngine.path` only
|
|
|
72
76
|
when a full local checkout should provide a private asset registry for strict
|
|
73
77
|
maintainer checks.
|
|
74
78
|
|
|
79
|
+
`portfolio assets-check` verifies that the currently recorded lock and managed
|
|
80
|
+
links are healthy. `portfolio doctor` additionally compares each target with the
|
|
81
|
+
current portfolio `assetBundles`, registry hashes, package versions, target-local
|
|
82
|
+
router/hook checks, and optional `hostTooling` requirements. The doctor is
|
|
83
|
+
offline and read-only by default. A dirty product worktree is reported as
|
|
84
|
+
evidence but is not itself a governance failure.
|
|
85
|
+
|
|
86
|
+
Third-party update discovery is deliberately separate and low frequency:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
pro-gov assets npx update --plan
|
|
90
|
+
codex plugin marketplace upgrade
|
|
91
|
+
codex plugin list --json
|
|
92
|
+
claude plugin update <plugin>
|
|
93
|
+
claude plugin list --json
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The native tools own fetching and installation. PGS reviews resulting asset
|
|
97
|
+
changes, regenerates plans, and verifies the fleet; it does not implement a
|
|
98
|
+
second plugin marketplace or silently accept upstream changes.
|
|
99
|
+
|
|
75
100
|
`lens audit init` creates a raw-first audit package for Project Lens plus
|
|
76
101
|
Ponytail reviews. For a "read-only project audit", the target repository remains
|
|
77
102
|
read-only, but the audit package is still the required output record. `lens
|
package/dist/cli.js
CHANGED
|
@@ -79,7 +79,14 @@ function createNpxSkillsMaintenancePlan(options) {
|
|
|
79
79
|
cpSync(options.npxRoot, tempRoot, { recursive: true, dereference: false });
|
|
80
80
|
const command2 = buildNpxCommand(options);
|
|
81
81
|
const runner = options.runner ?? defaultRunner;
|
|
82
|
-
const
|
|
82
|
+
const timeoutMs = options.timeoutMs ?? 3e5;
|
|
83
|
+
const result = runner({ command: command2, cwd: tempRoot, timeoutMs });
|
|
84
|
+
if (result.timedOut) {
|
|
85
|
+
throw new Error(`npx skills ${options.operation} timed out after ${timeoutMs}ms`);
|
|
86
|
+
}
|
|
87
|
+
if (result.status === null && result.signal) {
|
|
88
|
+
throw new Error(`npx skills ${options.operation} terminated by ${result.signal}`);
|
|
89
|
+
}
|
|
83
90
|
if (result.status !== 0) {
|
|
84
91
|
throw new Error(`npx skills ${options.operation} failed with exit code ${result.status}`);
|
|
85
92
|
}
|
|
@@ -128,12 +135,18 @@ function buildNpxCommand(options) {
|
|
|
128
135
|
if (options.skill) command2.push(options.skill);
|
|
129
136
|
return command2;
|
|
130
137
|
}
|
|
131
|
-
function defaultRunner({ command: command2, cwd }) {
|
|
132
|
-
const result = spawnSync(command2[0] ?? "npx", command2.slice(1), {
|
|
138
|
+
function defaultRunner({ command: command2, cwd, timeoutMs }) {
|
|
139
|
+
const result = spawnSync(command2[0] ?? "npx", command2.slice(1), {
|
|
140
|
+
cwd,
|
|
141
|
+
encoding: "utf8",
|
|
142
|
+
timeout: timeoutMs
|
|
143
|
+
});
|
|
133
144
|
return {
|
|
134
145
|
status: result.status,
|
|
135
146
|
stdout: result.stdout,
|
|
136
|
-
stderr: result.stderr
|
|
147
|
+
stderr: result.stderr,
|
|
148
|
+
signal: result.signal,
|
|
149
|
+
timedOut: result.error?.code === "ETIMEDOUT"
|
|
137
150
|
};
|
|
138
151
|
}
|
|
139
152
|
function snapshotFiles(root) {
|
|
@@ -536,8 +549,229 @@ function resolveSafePath(root, sourcePath) {
|
|
|
536
549
|
}
|
|
537
550
|
|
|
538
551
|
// src/asset-targets/apply.ts
|
|
539
|
-
import {
|
|
540
|
-
|
|
552
|
+
import {
|
|
553
|
+
existsSync as existsSync8,
|
|
554
|
+
lstatSync as lstatSync3,
|
|
555
|
+
mkdirSync as mkdirSync2,
|
|
556
|
+
readlinkSync as readlinkSync2,
|
|
557
|
+
realpathSync,
|
|
558
|
+
symlinkSync,
|
|
559
|
+
unlinkSync,
|
|
560
|
+
writeFileSync
|
|
561
|
+
} from "node:fs";
|
|
562
|
+
import { dirname as dirname4, join as join8, relative as relative4, resolve as resolve2 } from "node:path";
|
|
563
|
+
|
|
564
|
+
// src/asset-targets/install-plan.ts
|
|
565
|
+
import { existsSync as existsSync7, lstatSync as lstatSync2, readFileSync as readFileSync4, readlinkSync } from "node:fs";
|
|
566
|
+
import { basename, dirname as dirname3, join as join7, resolve } from "node:path";
|
|
567
|
+
function createAssetInstallPlan(options) {
|
|
568
|
+
const placement = options.placement ?? "registry";
|
|
569
|
+
const assetsById = new Map(options.registry.assets.map((asset) => [asset.id, asset]));
|
|
570
|
+
const bundlesById = new Map(options.bundles.map((bundle) => [bundle.id, bundle]));
|
|
571
|
+
const assetIds = resolveBundleAssetIds(options.bundleIds, bundlesById);
|
|
572
|
+
const assets = assetIds.map((assetId) => {
|
|
573
|
+
const asset = assetsById.get(assetId);
|
|
574
|
+
if (!asset) throw new Error(`Unknown asset id in bundle: ${assetId}`);
|
|
575
|
+
if (asset.kind === "skill" && !asset.hosts.includes(options.host)) {
|
|
576
|
+
throw new Error(`Asset ${assetId} does not support host ${options.host}`);
|
|
577
|
+
}
|
|
578
|
+
return asset;
|
|
579
|
+
});
|
|
580
|
+
const lockEntries = createAgentAssetLockEntries(options.registry, options.agentAssetsDir, assetIds);
|
|
581
|
+
const managedEntries = readManagedEntries(options.targetDir);
|
|
582
|
+
const managedTargets = new Set(managedEntries.map((entry) => entry.targetPath));
|
|
583
|
+
const assetActions = assets.map(
|
|
584
|
+
(asset) => createAssetAction(asset, options.agentAssetsDir, options.targetDir, options.host, placement, managedTargets)
|
|
585
|
+
);
|
|
586
|
+
const manifest = {
|
|
587
|
+
schemaVersion: 1,
|
|
588
|
+
host: options.host,
|
|
589
|
+
placement,
|
|
590
|
+
bundleIds: [...options.bundleIds],
|
|
591
|
+
assetIds
|
|
592
|
+
};
|
|
593
|
+
const lockfile = {
|
|
594
|
+
schemaVersion: 1,
|
|
595
|
+
host: options.host,
|
|
596
|
+
placement,
|
|
597
|
+
bundleIds: [...options.bundleIds],
|
|
598
|
+
assets: lockEntries.map((entry) => {
|
|
599
|
+
const action = assetActions.find((candidate) => "assetId" in candidate && candidate.assetId === entry.id);
|
|
600
|
+
return {
|
|
601
|
+
...entry,
|
|
602
|
+
targetPath: action && "targetPath" in action ? action.targetPath : ""
|
|
603
|
+
};
|
|
604
|
+
})
|
|
605
|
+
};
|
|
606
|
+
const writeActions = [
|
|
607
|
+
{
|
|
608
|
+
type: "write-file",
|
|
609
|
+
targetPath: ".pro-gov/assets.json",
|
|
610
|
+
content: `${JSON.stringify(manifest, null, 2)}
|
|
611
|
+
`
|
|
612
|
+
},
|
|
613
|
+
{
|
|
614
|
+
type: "write-file",
|
|
615
|
+
targetPath: ".pro-gov/assets.lock.json",
|
|
616
|
+
content: `${JSON.stringify(lockfile, null, 2)}
|
|
617
|
+
`
|
|
618
|
+
}
|
|
619
|
+
];
|
|
620
|
+
const expectedTargetPaths = new Set(
|
|
621
|
+
assetActions.filter((action) => "assetId" in action).map((action) => action.targetPath)
|
|
622
|
+
);
|
|
623
|
+
const removalActions = createRemovalActions(
|
|
624
|
+
options.targetDir,
|
|
625
|
+
options.agentAssetsDir,
|
|
626
|
+
managedEntries,
|
|
627
|
+
expectedTargetPaths
|
|
628
|
+
);
|
|
629
|
+
return {
|
|
630
|
+
schemaVersion: 1,
|
|
631
|
+
dryRun: true,
|
|
632
|
+
targetDir: options.targetDir,
|
|
633
|
+
host: options.host,
|
|
634
|
+
placement,
|
|
635
|
+
bundleIds: [...options.bundleIds],
|
|
636
|
+
assetIds,
|
|
637
|
+
actions: [
|
|
638
|
+
...createDirectoryActions([...assetActions, ...writeActions]),
|
|
639
|
+
...removalActions,
|
|
640
|
+
...assetActions,
|
|
641
|
+
...writeActions
|
|
642
|
+
]
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
function resolveBundleAssetIds(bundleIds, bundlesById) {
|
|
646
|
+
const ids = /* @__PURE__ */ new Set();
|
|
647
|
+
for (const bundleId of bundleIds) {
|
|
648
|
+
const bundle = bundlesById.get(bundleId);
|
|
649
|
+
if (!bundle) throw new Error(`Unknown bundle id: ${bundleId}`);
|
|
650
|
+
for (const assetId of bundle.assets) {
|
|
651
|
+
ids.add(assetId);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
return [...ids].sort();
|
|
655
|
+
}
|
|
656
|
+
function createAssetAction(asset, agentAssetsDir, targetDir, host, placement, managedTargets) {
|
|
657
|
+
if (asset.kind === "skill" && asset.defaultScope === "user") {
|
|
658
|
+
throw new Error(
|
|
659
|
+
`User-scoped asset ${asset.id} must be linked at the user level, not installed into a project target.`
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
const sourcePath = join7(agentAssetsDir, asset.sourcePath);
|
|
663
|
+
const targetPath = resolveHostTargetPath(asset, host, placement);
|
|
664
|
+
const targetAbsolutePath = join7(targetDir, targetPath);
|
|
665
|
+
const targetExists = pathExistsEvenIfDanglingSymlink2(targetAbsolutePath);
|
|
666
|
+
if (targetExists) {
|
|
667
|
+
const stats = lstatSync2(targetAbsolutePath);
|
|
668
|
+
if (stats.isSymbolicLink() && managedTargets.has(targetPath)) {
|
|
669
|
+
return {
|
|
670
|
+
type: "update-symlink",
|
|
671
|
+
assetId: asset.id,
|
|
672
|
+
sourcePath,
|
|
673
|
+
targetPath
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
throw new Error(`Refusing to overwrite unmanaged target: ${targetPath}`);
|
|
677
|
+
}
|
|
678
|
+
return {
|
|
679
|
+
type: "symlink",
|
|
680
|
+
assetId: asset.id,
|
|
681
|
+
sourcePath,
|
|
682
|
+
targetPath
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
function resolveHostTargetPath(asset, host, placement) {
|
|
686
|
+
if (asset.kind === "skill") {
|
|
687
|
+
const effectivePlacement = resolveSkillPlacement(asset, placement);
|
|
688
|
+
if (host === "claude-code") {
|
|
689
|
+
if (placement === "manual") {
|
|
690
|
+
throw new Error("Manual skill placement is only supported for .agents hosts");
|
|
691
|
+
}
|
|
692
|
+
return `.claude/skills/${basename(asset.sourcePath)}`;
|
|
693
|
+
}
|
|
694
|
+
if (effectivePlacement === "manual") {
|
|
695
|
+
return `.agents/manual-skills/${basename(asset.sourcePath)}`;
|
|
696
|
+
}
|
|
697
|
+
return `.agents/skills/${basename(asset.sourcePath)}`;
|
|
698
|
+
}
|
|
699
|
+
if (asset.kind === "rule") {
|
|
700
|
+
return `.pro-gov/agent-assets/rules/${basename(asset.sourcePath)}`;
|
|
701
|
+
}
|
|
702
|
+
return `.pro-gov/agent-assets/commands/${basename(asset.sourcePath)}`;
|
|
703
|
+
}
|
|
704
|
+
function resolveSkillPlacement(asset, placement) {
|
|
705
|
+
if (placement !== "registry") return placement;
|
|
706
|
+
return asset.defaultPlacement ?? "auto";
|
|
707
|
+
}
|
|
708
|
+
function createDirectoryActions(actions) {
|
|
709
|
+
const directories = /* @__PURE__ */ new Set();
|
|
710
|
+
for (const action of actions) {
|
|
711
|
+
if (action.type === "create-dir") continue;
|
|
712
|
+
const directory = dirname3(action.targetPath);
|
|
713
|
+
if (directory !== ".") directories.add(directory);
|
|
714
|
+
}
|
|
715
|
+
return [...directories].sort().map((targetPath) => ({ type: "create-dir", targetPath }));
|
|
716
|
+
}
|
|
717
|
+
function readManagedEntries(targetDir) {
|
|
718
|
+
const lockfilePath = join7(targetDir, ".pro-gov/assets.lock.json");
|
|
719
|
+
if (!existsSync7(lockfilePath)) return [];
|
|
720
|
+
try {
|
|
721
|
+
const lockfile = JSON.parse(readFileSync4(lockfilePath, "utf8"));
|
|
722
|
+
return (lockfile.assets ?? []).filter(
|
|
723
|
+
(entry) => typeof entry.id === "string" && typeof entry.sourcePath === "string" && typeof entry.targetPath === "string"
|
|
724
|
+
);
|
|
725
|
+
} catch {
|
|
726
|
+
return [];
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
function createRemovalActions(targetDir, agentAssetsDir, managedEntries, expectedTargetPaths) {
|
|
730
|
+
const actions = [];
|
|
731
|
+
for (const entry of managedEntries) {
|
|
732
|
+
if (expectedTargetPaths.has(entry.targetPath)) continue;
|
|
733
|
+
if (!isManagedAssetTargetPath(entry.targetPath)) {
|
|
734
|
+
throw new Error(`Refusing to remove managed asset outside supported roots: ${entry.targetPath}`);
|
|
735
|
+
}
|
|
736
|
+
const targetAbsolutePath = join7(targetDir, entry.targetPath);
|
|
737
|
+
if (!pathExistsEvenIfDanglingSymlink2(targetAbsolutePath)) continue;
|
|
738
|
+
const stats = lstatSync2(targetAbsolutePath);
|
|
739
|
+
if (!stats.isSymbolicLink()) {
|
|
740
|
+
throw new Error(`Refusing to remove path that is no longer a managed symlink: ${entry.targetPath}`);
|
|
741
|
+
}
|
|
742
|
+
const expectedSourcePath = join7(agentAssetsDir, entry.sourcePath);
|
|
743
|
+
const actualSourcePath = resolve(dirname3(targetAbsolutePath), readlinkSync(targetAbsolutePath));
|
|
744
|
+
if (actualSourcePath !== resolve(expectedSourcePath)) {
|
|
745
|
+
throw new Error(`Refusing to remove managed symlink with changed target: ${entry.targetPath}`);
|
|
746
|
+
}
|
|
747
|
+
actions.push({
|
|
748
|
+
type: "remove-symlink",
|
|
749
|
+
assetId: entry.id,
|
|
750
|
+
expectedSourcePath,
|
|
751
|
+
targetPath: entry.targetPath
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
return actions.sort((a, b) => a.targetPath.localeCompare(b.targetPath));
|
|
755
|
+
}
|
|
756
|
+
function isManagedAssetTargetPath(path) {
|
|
757
|
+
return [
|
|
758
|
+
".agents/skills/",
|
|
759
|
+
".agents/manual-skills/",
|
|
760
|
+
".claude/skills/",
|
|
761
|
+
".pro-gov/agent-assets/rules/",
|
|
762
|
+
".pro-gov/agent-assets/commands/"
|
|
763
|
+
].some((prefix) => path.startsWith(prefix));
|
|
764
|
+
}
|
|
765
|
+
function pathExistsEvenIfDanglingSymlink2(path) {
|
|
766
|
+
try {
|
|
767
|
+
lstatSync2(path);
|
|
768
|
+
return true;
|
|
769
|
+
} catch {
|
|
770
|
+
return false;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// src/asset-targets/apply.ts
|
|
541
775
|
function applyAssetInstallPlan(plan) {
|
|
542
776
|
const appliedActions = [];
|
|
543
777
|
for (const action of plan.actions) {
|
|
@@ -547,52 +781,71 @@ function applyAssetInstallPlan(plan) {
|
|
|
547
781
|
return { appliedActions };
|
|
548
782
|
}
|
|
549
783
|
function applyAction(targetDir, action) {
|
|
550
|
-
const targetAbsolutePath =
|
|
784
|
+
const targetAbsolutePath = join8(targetDir, action.targetPath);
|
|
785
|
+
if (action.type === "remove-symlink") {
|
|
786
|
+
removeManagedSymlink(targetAbsolutePath, action);
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
551
789
|
if (action.type === "create-dir") {
|
|
552
790
|
mkdirSync2(targetAbsolutePath, { recursive: true });
|
|
553
791
|
return;
|
|
554
792
|
}
|
|
555
793
|
if (action.type === "write-file") {
|
|
556
|
-
mkdirSync2(
|
|
794
|
+
mkdirSync2(dirname4(targetAbsolutePath), { recursive: true });
|
|
557
795
|
writeFileSync(targetAbsolutePath, action.content);
|
|
558
796
|
return;
|
|
559
797
|
}
|
|
560
|
-
mkdirSync2(
|
|
561
|
-
const sourceAbsolutePath =
|
|
562
|
-
const symlinkTarget = relative4(realpathSync(
|
|
798
|
+
mkdirSync2(dirname4(targetAbsolutePath), { recursive: true });
|
|
799
|
+
const sourceAbsolutePath = resolve2(action.sourcePath);
|
|
800
|
+
const symlinkTarget = relative4(realpathSync(dirname4(targetAbsolutePath)), realpathSync(sourceAbsolutePath)) || ".";
|
|
563
801
|
if (action.type === "symlink") {
|
|
564
|
-
if (
|
|
802
|
+
if (pathExistsEvenIfDanglingSymlink3(targetAbsolutePath)) {
|
|
565
803
|
throw new Error(`Refusing to overwrite unmanaged target: ${action.targetPath}`);
|
|
566
804
|
}
|
|
567
805
|
symlinkSync(symlinkTarget, targetAbsolutePath);
|
|
568
806
|
return;
|
|
569
807
|
}
|
|
570
|
-
if (!
|
|
808
|
+
if (!pathExistsEvenIfDanglingSymlink3(targetAbsolutePath)) {
|
|
571
809
|
symlinkSync(symlinkTarget, targetAbsolutePath);
|
|
572
810
|
return;
|
|
573
811
|
}
|
|
574
|
-
const stats =
|
|
812
|
+
const stats = lstatSync3(targetAbsolutePath);
|
|
575
813
|
if (!stats.isSymbolicLink()) {
|
|
576
814
|
throw new Error(`Refusing to overwrite unmanaged target: ${action.targetPath}`);
|
|
577
815
|
}
|
|
578
816
|
unlinkSync(targetAbsolutePath);
|
|
579
817
|
symlinkSync(symlinkTarget, targetAbsolutePath);
|
|
580
818
|
}
|
|
581
|
-
function
|
|
819
|
+
function removeManagedSymlink(targetAbsolutePath, action) {
|
|
820
|
+
if (!isManagedAssetTargetPath(action.targetPath)) {
|
|
821
|
+
throw new Error(`Refusing to remove managed asset outside supported roots: ${action.targetPath}`);
|
|
822
|
+
}
|
|
823
|
+
if (!pathExistsEvenIfDanglingSymlink3(targetAbsolutePath)) return;
|
|
824
|
+
const stats = lstatSync3(targetAbsolutePath);
|
|
825
|
+
if (!stats.isSymbolicLink()) {
|
|
826
|
+
throw new Error(`Refusing to remove path that is no longer a managed symlink: ${action.targetPath}`);
|
|
827
|
+
}
|
|
828
|
+
const actualSourcePath = resolve2(dirname4(targetAbsolutePath), readlinkSync2(targetAbsolutePath));
|
|
829
|
+
if (actualSourcePath !== resolve2(action.expectedSourcePath)) {
|
|
830
|
+
throw new Error(`Refusing to remove managed symlink with changed target: ${action.targetPath}`);
|
|
831
|
+
}
|
|
832
|
+
unlinkSync(targetAbsolutePath);
|
|
833
|
+
}
|
|
834
|
+
function pathExistsEvenIfDanglingSymlink3(path) {
|
|
582
835
|
try {
|
|
583
|
-
|
|
836
|
+
lstatSync3(path);
|
|
584
837
|
return true;
|
|
585
838
|
} catch {
|
|
586
|
-
return
|
|
839
|
+
return existsSync8(path);
|
|
587
840
|
}
|
|
588
841
|
}
|
|
589
842
|
|
|
590
843
|
// src/asset-targets/check.ts
|
|
591
|
-
import { existsSync as
|
|
592
|
-
import { basename, join as
|
|
844
|
+
import { existsSync as existsSync9, lstatSync as lstatSync4, readFileSync as readFileSync5 } from "node:fs";
|
|
845
|
+
import { basename as basename2, join as join9 } from "node:path";
|
|
593
846
|
function checkInstalledAssets(options) {
|
|
594
|
-
const lockfilePath =
|
|
595
|
-
if (!
|
|
847
|
+
const lockfilePath = join9(options.targetDir, ".pro-gov/assets.lock.json");
|
|
848
|
+
if (!existsSync9(lockfilePath)) {
|
|
596
849
|
return {
|
|
597
850
|
targetDir: options.targetDir,
|
|
598
851
|
issues: [
|
|
@@ -604,12 +857,12 @@ function checkInstalledAssets(options) {
|
|
|
604
857
|
};
|
|
605
858
|
}
|
|
606
859
|
const registryById = new Map(options.registry.assets.map((asset) => [asset.id, asset]));
|
|
607
|
-
const lockfile = JSON.parse(
|
|
860
|
+
const lockfile = JSON.parse(readFileSync5(lockfilePath, "utf8"));
|
|
608
861
|
const issues = [];
|
|
609
862
|
const strictRegistry = options.strictRegistry ?? false;
|
|
610
863
|
for (const entry of lockfile.assets ?? []) {
|
|
611
864
|
const asset = registryById.get(entry.id);
|
|
612
|
-
const targetAbsolutePath =
|
|
865
|
+
const targetAbsolutePath = join9(options.targetDir, entry.targetPath);
|
|
613
866
|
if (!asset && strictRegistry) {
|
|
614
867
|
issues.push({
|
|
615
868
|
type: "unknown-asset",
|
|
@@ -636,7 +889,7 @@ function checkInstalledAssets(options) {
|
|
|
636
889
|
issues.push(placementDriftIssue);
|
|
637
890
|
}
|
|
638
891
|
}
|
|
639
|
-
if (!
|
|
892
|
+
if (!pathExistsEvenIfDanglingSymlink4(targetAbsolutePath)) {
|
|
640
893
|
issues.push({
|
|
641
894
|
type: "missing-target",
|
|
642
895
|
id: entry.id,
|
|
@@ -645,7 +898,7 @@ function checkInstalledAssets(options) {
|
|
|
645
898
|
});
|
|
646
899
|
continue;
|
|
647
900
|
}
|
|
648
|
-
const targetStats =
|
|
901
|
+
const targetStats = lstatSync4(targetAbsolutePath);
|
|
649
902
|
if (!targetStats.isSymbolicLink()) {
|
|
650
903
|
issues.push({
|
|
651
904
|
type: "unmanaged-conflict",
|
|
@@ -655,7 +908,7 @@ function checkInstalledAssets(options) {
|
|
|
655
908
|
});
|
|
656
909
|
continue;
|
|
657
910
|
}
|
|
658
|
-
if (!
|
|
911
|
+
if (!existsSync9(targetAbsolutePath)) {
|
|
659
912
|
issues.push({
|
|
660
913
|
type: "dangling-symlink",
|
|
661
914
|
id: entry.id,
|
|
@@ -675,8 +928,8 @@ function checkInstalledAssets(options) {
|
|
|
675
928
|
});
|
|
676
929
|
}
|
|
677
930
|
if (!asset || !strictRegistry) continue;
|
|
678
|
-
const sourceAbsolutePath =
|
|
679
|
-
if (!
|
|
931
|
+
const sourceAbsolutePath = join9(options.agentAssetsDir, asset.sourcePath);
|
|
932
|
+
if (!existsSync9(sourceAbsolutePath)) {
|
|
680
933
|
issues.push({
|
|
681
934
|
type: "missing-source",
|
|
682
935
|
id: entry.id,
|
|
@@ -714,10 +967,10 @@ function checkDuplicateSkillPlacements(targetDir, registry) {
|
|
|
714
967
|
const issues = [];
|
|
715
968
|
for (const asset of registry.assets) {
|
|
716
969
|
if (asset.kind !== "skill") continue;
|
|
717
|
-
const skillName =
|
|
970
|
+
const skillName = basename2(asset.sourcePath);
|
|
718
971
|
const autoPath = `.agents/skills/${skillName}`;
|
|
719
972
|
const manualPath = `.agents/manual-skills/${skillName}`;
|
|
720
|
-
if (
|
|
973
|
+
if (pathExistsEvenIfDanglingSymlink4(join9(targetDir, autoPath)) && pathExistsEvenIfDanglingSymlink4(join9(targetDir, manualPath))) {
|
|
721
974
|
issues.push({
|
|
722
975
|
type: "duplicate-skill-placement",
|
|
723
976
|
id: asset.id,
|
|
@@ -757,7 +1010,7 @@ function expectedSkillTargetPrefixes(host) {
|
|
|
757
1010
|
return void 0;
|
|
758
1011
|
}
|
|
759
1012
|
function expectedRegistrySkillTargetPath(host, sourcePath, placement) {
|
|
760
|
-
const skillName =
|
|
1013
|
+
const skillName = basename2(sourcePath);
|
|
761
1014
|
if (host === "claude-code") {
|
|
762
1015
|
return placement === "manual" ? void 0 : `.claude/skills/${skillName}`;
|
|
763
1016
|
}
|
|
@@ -766,163 +1019,6 @@ function expectedRegistrySkillTargetPath(host, sourcePath, placement) {
|
|
|
766
1019
|
}
|
|
767
1020
|
return void 0;
|
|
768
1021
|
}
|
|
769
|
-
function pathExistsEvenIfDanglingSymlink3(path) {
|
|
770
|
-
try {
|
|
771
|
-
lstatSync3(path);
|
|
772
|
-
return true;
|
|
773
|
-
} catch {
|
|
774
|
-
return false;
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
// src/asset-targets/install-plan.ts
|
|
779
|
-
import { existsSync as existsSync9, lstatSync as lstatSync4, readFileSync as readFileSync5 } from "node:fs";
|
|
780
|
-
import { basename as basename2, dirname as dirname4, join as join9 } from "node:path";
|
|
781
|
-
function createAssetInstallPlan(options) {
|
|
782
|
-
const placement = options.placement ?? "registry";
|
|
783
|
-
const assetsById = new Map(options.registry.assets.map((asset) => [asset.id, asset]));
|
|
784
|
-
const bundlesById = new Map(options.bundles.map((bundle) => [bundle.id, bundle]));
|
|
785
|
-
const assetIds = resolveBundleAssetIds(options.bundleIds, bundlesById);
|
|
786
|
-
const assets = assetIds.map((assetId) => {
|
|
787
|
-
const asset = assetsById.get(assetId);
|
|
788
|
-
if (!asset) throw new Error(`Unknown asset id in bundle: ${assetId}`);
|
|
789
|
-
if (asset.kind === "skill" && !asset.hosts.includes(options.host)) {
|
|
790
|
-
throw new Error(`Asset ${assetId} does not support host ${options.host}`);
|
|
791
|
-
}
|
|
792
|
-
return asset;
|
|
793
|
-
});
|
|
794
|
-
const lockEntries = createAgentAssetLockEntries(options.registry, options.agentAssetsDir, assetIds);
|
|
795
|
-
const managedTargets = readManagedTargets(options.targetDir);
|
|
796
|
-
const assetActions = assets.map(
|
|
797
|
-
(asset) => createAssetAction(asset, options.agentAssetsDir, options.targetDir, options.host, placement, managedTargets)
|
|
798
|
-
);
|
|
799
|
-
const manifest = {
|
|
800
|
-
schemaVersion: 1,
|
|
801
|
-
host: options.host,
|
|
802
|
-
placement,
|
|
803
|
-
bundleIds: [...options.bundleIds],
|
|
804
|
-
assetIds
|
|
805
|
-
};
|
|
806
|
-
const lockfile = {
|
|
807
|
-
schemaVersion: 1,
|
|
808
|
-
host: options.host,
|
|
809
|
-
placement,
|
|
810
|
-
bundleIds: [...options.bundleIds],
|
|
811
|
-
assets: lockEntries.map((entry) => {
|
|
812
|
-
const action = assetActions.find((candidate) => "assetId" in candidate && candidate.assetId === entry.id);
|
|
813
|
-
return {
|
|
814
|
-
...entry,
|
|
815
|
-
targetPath: action && "targetPath" in action ? action.targetPath : ""
|
|
816
|
-
};
|
|
817
|
-
})
|
|
818
|
-
};
|
|
819
|
-
const writeActions = [
|
|
820
|
-
{
|
|
821
|
-
type: "write-file",
|
|
822
|
-
targetPath: ".pro-gov/assets.json",
|
|
823
|
-
content: `${JSON.stringify(manifest, null, 2)}
|
|
824
|
-
`
|
|
825
|
-
},
|
|
826
|
-
{
|
|
827
|
-
type: "write-file",
|
|
828
|
-
targetPath: ".pro-gov/assets.lock.json",
|
|
829
|
-
content: `${JSON.stringify(lockfile, null, 2)}
|
|
830
|
-
`
|
|
831
|
-
}
|
|
832
|
-
];
|
|
833
|
-
return {
|
|
834
|
-
schemaVersion: 1,
|
|
835
|
-
dryRun: true,
|
|
836
|
-
targetDir: options.targetDir,
|
|
837
|
-
host: options.host,
|
|
838
|
-
placement,
|
|
839
|
-
bundleIds: [...options.bundleIds],
|
|
840
|
-
assetIds,
|
|
841
|
-
actions: [...createDirectoryActions([...assetActions, ...writeActions]), ...assetActions, ...writeActions]
|
|
842
|
-
};
|
|
843
|
-
}
|
|
844
|
-
function resolveBundleAssetIds(bundleIds, bundlesById) {
|
|
845
|
-
const ids = /* @__PURE__ */ new Set();
|
|
846
|
-
for (const bundleId of bundleIds) {
|
|
847
|
-
const bundle = bundlesById.get(bundleId);
|
|
848
|
-
if (!bundle) throw new Error(`Unknown bundle id: ${bundleId}`);
|
|
849
|
-
for (const assetId of bundle.assets) {
|
|
850
|
-
ids.add(assetId);
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
return [...ids].sort();
|
|
854
|
-
}
|
|
855
|
-
function createAssetAction(asset, agentAssetsDir, targetDir, host, placement, managedTargets) {
|
|
856
|
-
if (asset.kind === "skill" && asset.defaultScope === "user") {
|
|
857
|
-
throw new Error(
|
|
858
|
-
`User-scoped asset ${asset.id} must be linked at the user level, not installed into a project target.`
|
|
859
|
-
);
|
|
860
|
-
}
|
|
861
|
-
const sourcePath = join9(agentAssetsDir, asset.sourcePath);
|
|
862
|
-
const targetPath = resolveHostTargetPath(asset, host, placement);
|
|
863
|
-
const targetAbsolutePath = join9(targetDir, targetPath);
|
|
864
|
-
const targetExists = pathExistsEvenIfDanglingSymlink4(targetAbsolutePath);
|
|
865
|
-
if (targetExists) {
|
|
866
|
-
const stats = lstatSync4(targetAbsolutePath);
|
|
867
|
-
if (stats.isSymbolicLink() && managedTargets.has(targetPath)) {
|
|
868
|
-
return {
|
|
869
|
-
type: "update-symlink",
|
|
870
|
-
assetId: asset.id,
|
|
871
|
-
sourcePath,
|
|
872
|
-
targetPath
|
|
873
|
-
};
|
|
874
|
-
}
|
|
875
|
-
throw new Error(`Refusing to overwrite unmanaged target: ${targetPath}`);
|
|
876
|
-
}
|
|
877
|
-
return {
|
|
878
|
-
type: "symlink",
|
|
879
|
-
assetId: asset.id,
|
|
880
|
-
sourcePath,
|
|
881
|
-
targetPath
|
|
882
|
-
};
|
|
883
|
-
}
|
|
884
|
-
function resolveHostTargetPath(asset, host, placement) {
|
|
885
|
-
if (asset.kind === "skill") {
|
|
886
|
-
const effectivePlacement = resolveSkillPlacement(asset, placement);
|
|
887
|
-
if (host === "claude-code") {
|
|
888
|
-
if (placement === "manual") {
|
|
889
|
-
throw new Error("Manual skill placement is only supported for .agents hosts");
|
|
890
|
-
}
|
|
891
|
-
return `.claude/skills/${basename2(asset.sourcePath)}`;
|
|
892
|
-
}
|
|
893
|
-
if (effectivePlacement === "manual") {
|
|
894
|
-
return `.agents/manual-skills/${basename2(asset.sourcePath)}`;
|
|
895
|
-
}
|
|
896
|
-
return `.agents/skills/${basename2(asset.sourcePath)}`;
|
|
897
|
-
}
|
|
898
|
-
if (asset.kind === "rule") {
|
|
899
|
-
return `.pro-gov/agent-assets/rules/${basename2(asset.sourcePath)}`;
|
|
900
|
-
}
|
|
901
|
-
return `.pro-gov/agent-assets/commands/${basename2(asset.sourcePath)}`;
|
|
902
|
-
}
|
|
903
|
-
function resolveSkillPlacement(asset, placement) {
|
|
904
|
-
if (placement !== "registry") return placement;
|
|
905
|
-
return asset.defaultPlacement ?? "auto";
|
|
906
|
-
}
|
|
907
|
-
function createDirectoryActions(actions) {
|
|
908
|
-
const directories = /* @__PURE__ */ new Set();
|
|
909
|
-
for (const action of actions) {
|
|
910
|
-
if (action.type === "create-dir") continue;
|
|
911
|
-
const directory = dirname4(action.targetPath);
|
|
912
|
-
if (directory !== ".") directories.add(directory);
|
|
913
|
-
}
|
|
914
|
-
return [...directories].sort().map((targetPath) => ({ type: "create-dir", targetPath }));
|
|
915
|
-
}
|
|
916
|
-
function readManagedTargets(targetDir) {
|
|
917
|
-
const lockfilePath = join9(targetDir, ".pro-gov/assets.lock.json");
|
|
918
|
-
if (!existsSync9(lockfilePath)) return /* @__PURE__ */ new Set();
|
|
919
|
-
try {
|
|
920
|
-
const lockfile = JSON.parse(readFileSync5(lockfilePath, "utf8"));
|
|
921
|
-
return new Set((lockfile.assets ?? []).map((asset) => asset.targetPath).filter(Boolean));
|
|
922
|
-
} catch {
|
|
923
|
-
return /* @__PURE__ */ new Set();
|
|
924
|
-
}
|
|
925
|
-
}
|
|
926
1022
|
function pathExistsEvenIfDanglingSymlink4(path) {
|
|
927
1023
|
try {
|
|
928
1024
|
lstatSync4(path);
|
|
@@ -1642,7 +1738,7 @@ function isEngineeringRuntimeProject(root) {
|
|
|
1642
1738
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
1643
1739
|
import { existsSync as existsSync13, mkdirSync as mkdirSync4, readFileSync as readFileSync10, writeFileSync as writeFileSync3 } from "node:fs";
|
|
1644
1740
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
1645
|
-
import { basename as basename3, join as join13, resolve as
|
|
1741
|
+
import { basename as basename3, join as join13, resolve as resolve3 } from "node:path";
|
|
1646
1742
|
|
|
1647
1743
|
// src/host-hooks/host-hook-runner.ts
|
|
1648
1744
|
import { closeSync, openSync, readFileSync as readFileSync9, readSync, statSync as statSync3 } from "node:fs";
|
|
@@ -1967,7 +2063,7 @@ function defaultDebugDir() {
|
|
|
1967
2063
|
});
|
|
1968
2064
|
const value = gitPath.status === 0 ? gitPath.stdout.trim() : "";
|
|
1969
2065
|
if (value) {
|
|
1970
|
-
return
|
|
2066
|
+
return resolve3(process.cwd(), value);
|
|
1971
2067
|
}
|
|
1972
2068
|
return join13(tmpdir2(), "pro-gov-hook-debug", basename3(process.cwd()));
|
|
1973
2069
|
}
|
|
@@ -1983,8 +2079,8 @@ function readPackageVersion() {
|
|
|
1983
2079
|
}
|
|
1984
2080
|
function resolvePackageJsonPath() {
|
|
1985
2081
|
const candidates = [
|
|
1986
|
-
|
|
1987
|
-
|
|
2082
|
+
resolve3(process.cwd(), "packages/pro-gov/package.json"),
|
|
2083
|
+
resolve3(process.cwd(), "node_modules/@pieai/pro-gov/package.json")
|
|
1988
2084
|
];
|
|
1989
2085
|
return candidates.find((candidate) => existsSync13(candidate));
|
|
1990
2086
|
}
|
|
@@ -2691,12 +2787,12 @@ function printUsage2() {
|
|
|
2691
2787
|
}
|
|
2692
2788
|
|
|
2693
2789
|
// src/commands/portfolio.ts
|
|
2694
|
-
import { existsSync as
|
|
2695
|
-
import { join as
|
|
2790
|
+
import { existsSync as existsSync20 } from "node:fs";
|
|
2791
|
+
import { join as join19 } from "node:path";
|
|
2696
2792
|
|
|
2697
2793
|
// src/portfolio/manifest.ts
|
|
2698
2794
|
import { existsSync as existsSync17, readFileSync as readFileSync14 } from "node:fs";
|
|
2699
|
-
import { dirname as dirname10, isAbsolute as isAbsolute3, resolve as
|
|
2795
|
+
import { dirname as dirname10, isAbsolute as isAbsolute3, resolve as resolve4 } from "node:path";
|
|
2700
2796
|
function loadPortfolioManifest(configPath) {
|
|
2701
2797
|
let parsed;
|
|
2702
2798
|
try {
|
|
@@ -2712,7 +2808,7 @@ function loadPortfolioManifest(configPath) {
|
|
|
2712
2808
|
]
|
|
2713
2809
|
};
|
|
2714
2810
|
}
|
|
2715
|
-
const normalized = resolveManifestPaths(parsed, dirname10(
|
|
2811
|
+
const normalized = resolveManifestPaths(parsed, dirname10(resolve4(configPath)));
|
|
2716
2812
|
const issues = validatePortfolioManifest(normalized);
|
|
2717
2813
|
return {
|
|
2718
2814
|
configPath,
|
|
@@ -2726,7 +2822,7 @@ function resolveManifestPaths(value, configDir) {
|
|
|
2726
2822
|
if (!isRecord2(endpoint) || typeof endpoint.path !== "string" || isAbsolute3(endpoint.path)) {
|
|
2727
2823
|
return endpoint;
|
|
2728
2824
|
}
|
|
2729
|
-
return { ...endpoint, path:
|
|
2825
|
+
return { ...endpoint, path: resolve4(configDir, endpoint.path) };
|
|
2730
2826
|
};
|
|
2731
2827
|
return {
|
|
2732
2828
|
...value,
|
|
@@ -2760,9 +2856,15 @@ function validatePortfolioManifest(value) {
|
|
|
2760
2856
|
message: "Portfolio manifest portfolioId must be a non-empty string."
|
|
2761
2857
|
});
|
|
2762
2858
|
}
|
|
2763
|
-
validateAllowedFields(
|
|
2859
|
+
validateAllowedFields(
|
|
2860
|
+
value,
|
|
2861
|
+
"root",
|
|
2862
|
+
["schemaVersion", "portfolioId", "controlPlane", "executionEngine", "hostTooling", "targets"],
|
|
2863
|
+
issues
|
|
2864
|
+
);
|
|
2764
2865
|
validateEndpoint(value.controlPlane, "controlPlane", issues);
|
|
2765
2866
|
validateEndpoint(value.executionEngine, "executionEngine", issues);
|
|
2867
|
+
validateHostTooling(value.hostTooling, issues);
|
|
2766
2868
|
if (!Array.isArray(value.targets)) {
|
|
2767
2869
|
issues.push({
|
|
2768
2870
|
type: "invalid-field",
|
|
@@ -2874,19 +2976,462 @@ function validateOptionalStringArray(value, id, field, issues) {
|
|
|
2874
2976
|
});
|
|
2875
2977
|
}
|
|
2876
2978
|
}
|
|
2979
|
+
function validateHostTooling(value, issues) {
|
|
2980
|
+
if (value === void 0) return;
|
|
2981
|
+
if (!Array.isArray(value)) {
|
|
2982
|
+
issues.push({
|
|
2983
|
+
type: "invalid-field",
|
|
2984
|
+
field: "hostTooling",
|
|
2985
|
+
message: "Portfolio hostTooling must be an array."
|
|
2986
|
+
});
|
|
2987
|
+
return;
|
|
2988
|
+
}
|
|
2989
|
+
const seenHosts = /* @__PURE__ */ new Set();
|
|
2990
|
+
for (const entry of value) {
|
|
2991
|
+
if (!isRecord2(entry)) {
|
|
2992
|
+
issues.push({
|
|
2993
|
+
type: "invalid-field",
|
|
2994
|
+
field: "hostTooling",
|
|
2995
|
+
message: "Portfolio hostTooling entry must be an object."
|
|
2996
|
+
});
|
|
2997
|
+
continue;
|
|
2998
|
+
}
|
|
2999
|
+
validateAllowedFields(entry, "hostTooling", ["host", "plugins"], issues);
|
|
3000
|
+
if (entry.host !== "codex" && entry.host !== "claude-code") {
|
|
3001
|
+
issues.push({
|
|
3002
|
+
type: "invalid-field",
|
|
3003
|
+
field: "hostTooling.host",
|
|
3004
|
+
message: "Portfolio hostTooling host must be codex or claude-code."
|
|
3005
|
+
});
|
|
3006
|
+
} else if (seenHosts.has(entry.host)) {
|
|
3007
|
+
issues.push({
|
|
3008
|
+
type: "invalid-field",
|
|
3009
|
+
field: "hostTooling",
|
|
3010
|
+
message: `Duplicate portfolio hostTooling entry: ${entry.host}`
|
|
3011
|
+
});
|
|
3012
|
+
} else {
|
|
3013
|
+
seenHosts.add(entry.host);
|
|
3014
|
+
}
|
|
3015
|
+
if (!Array.isArray(entry.plugins) || !entry.plugins.every((plugin) => typeof plugin === "string" && plugin.length > 0)) {
|
|
3016
|
+
issues.push({
|
|
3017
|
+
type: "invalid-field",
|
|
3018
|
+
field: "hostTooling.plugins",
|
|
3019
|
+
message: "Portfolio hostTooling plugins must be an array of non-empty strings."
|
|
3020
|
+
});
|
|
3021
|
+
}
|
|
3022
|
+
}
|
|
3023
|
+
}
|
|
2877
3024
|
function isRecord2(value) {
|
|
2878
3025
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2879
3026
|
}
|
|
2880
3027
|
|
|
3028
|
+
// src/portfolio/doctor.ts
|
|
3029
|
+
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
3030
|
+
import { existsSync as existsSync19, readFileSync as readFileSync16 } from "node:fs";
|
|
3031
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
3032
|
+
import { dirname as dirname11, join as join18 } from "node:path";
|
|
3033
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
3034
|
+
|
|
3035
|
+
// src/host-tooling/inventory.ts
|
|
3036
|
+
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
3037
|
+
function inspectHostTooling(requirements, runner = defaultRunner2) {
|
|
3038
|
+
const hosts = [];
|
|
3039
|
+
const issues = [];
|
|
3040
|
+
for (const requirement of requirements) {
|
|
3041
|
+
const command2 = requirement.host === "codex" ? ["codex", "plugin", "list", "--json"] : ["claude", "plugin", "list", "--json"];
|
|
3042
|
+
const result = runner({ host: requirement.host, command: command2 });
|
|
3043
|
+
if (result.status !== 0) {
|
|
3044
|
+
issues.push({
|
|
3045
|
+
type: "host-tooling-command-failed",
|
|
3046
|
+
host: requirement.host,
|
|
3047
|
+
message: `Unable to inspect ${requirement.host} plugins: ${result.stderr || `exit ${result.status}`}`
|
|
3048
|
+
});
|
|
3049
|
+
hosts.push({ host: requirement.host, plugins: [] });
|
|
3050
|
+
continue;
|
|
3051
|
+
}
|
|
3052
|
+
let plugins;
|
|
3053
|
+
try {
|
|
3054
|
+
plugins = parseHostPlugins(requirement.host, JSON.parse(result.stdout));
|
|
3055
|
+
} catch (error) {
|
|
3056
|
+
issues.push({
|
|
3057
|
+
type: "host-tooling-command-failed",
|
|
3058
|
+
host: requirement.host,
|
|
3059
|
+
message: `Unable to parse ${requirement.host} plugin inventory: ${error instanceof Error ? error.message : String(error)}`
|
|
3060
|
+
});
|
|
3061
|
+
hosts.push({ host: requirement.host, plugins: [] });
|
|
3062
|
+
continue;
|
|
3063
|
+
}
|
|
3064
|
+
const byId = new Map(plugins.map((plugin) => [plugin.id, plugin]));
|
|
3065
|
+
hosts.push({
|
|
3066
|
+
host: requirement.host,
|
|
3067
|
+
plugins: requirement.plugins.flatMap((pluginId) => {
|
|
3068
|
+
const plugin = byId.get(pluginId);
|
|
3069
|
+
return plugin ? [plugin] : [];
|
|
3070
|
+
})
|
|
3071
|
+
});
|
|
3072
|
+
for (const pluginId of requirement.plugins) {
|
|
3073
|
+
const plugin = byId.get(pluginId);
|
|
3074
|
+
if (!plugin) {
|
|
3075
|
+
issues.push({
|
|
3076
|
+
type: "host-tooling-missing",
|
|
3077
|
+
host: requirement.host,
|
|
3078
|
+
pluginId,
|
|
3079
|
+
message: `Required ${requirement.host} plugin is missing: ${pluginId}`
|
|
3080
|
+
});
|
|
3081
|
+
} else if (!plugin.enabled) {
|
|
3082
|
+
issues.push({
|
|
3083
|
+
type: "host-tooling-disabled",
|
|
3084
|
+
host: requirement.host,
|
|
3085
|
+
pluginId,
|
|
3086
|
+
message: `Required ${requirement.host} plugin is disabled: ${pluginId}`
|
|
3087
|
+
});
|
|
3088
|
+
}
|
|
3089
|
+
}
|
|
3090
|
+
}
|
|
3091
|
+
return { hosts, issues };
|
|
3092
|
+
}
|
|
3093
|
+
function parseHostPlugins(host, value) {
|
|
3094
|
+
const entries = host === "codex" ? isRecord3(value) && Array.isArray(value.installed) ? value.installed : void 0 : Array.isArray(value) ? value : void 0;
|
|
3095
|
+
if (!entries) throw new Error("expected a plugin array");
|
|
3096
|
+
return entries.flatMap((entry) => {
|
|
3097
|
+
if (!isRecord3(entry)) return [];
|
|
3098
|
+
const id = host === "codex" ? entry.pluginId : entry.id;
|
|
3099
|
+
if (typeof id !== "string") return [];
|
|
3100
|
+
return [{
|
|
3101
|
+
id,
|
|
3102
|
+
version: typeof entry.version === "string" ? entry.version : void 0,
|
|
3103
|
+
enabled: entry.enabled === true
|
|
3104
|
+
}];
|
|
3105
|
+
});
|
|
3106
|
+
}
|
|
3107
|
+
function defaultRunner2({ command: command2 }) {
|
|
3108
|
+
const result = spawnSync5(command2[0] ?? "", command2.slice(1), {
|
|
3109
|
+
encoding: "utf8",
|
|
3110
|
+
timeout: 1e4
|
|
3111
|
+
});
|
|
3112
|
+
return {
|
|
3113
|
+
status: result.status,
|
|
3114
|
+
stdout: result.stdout,
|
|
3115
|
+
stderr: result.stderr || result.error?.message || ""
|
|
3116
|
+
};
|
|
3117
|
+
}
|
|
3118
|
+
function isRecord3(value) {
|
|
3119
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3120
|
+
}
|
|
3121
|
+
|
|
3122
|
+
// src/portfolio/asset-state.ts
|
|
3123
|
+
import { existsSync as existsSync18, lstatSync as lstatSync5, readFileSync as readFileSync15 } from "node:fs";
|
|
3124
|
+
import { join as join17 } from "node:path";
|
|
3125
|
+
function comparePortfolioAssetState(options) {
|
|
3126
|
+
const expectedManifest = readPlanDocument(options.expectedPlan, ".pro-gov/assets.json");
|
|
3127
|
+
const expectedLock = readPlanDocument(options.expectedPlan, ".pro-gov/assets.lock.json");
|
|
3128
|
+
const currentManifest = readJsonFile(join17(options.targetDir, ".pro-gov/assets.json"));
|
|
3129
|
+
const currentLock = readJsonFile(join17(options.targetDir, ".pro-gov/assets.lock.json"));
|
|
3130
|
+
const issues = [];
|
|
3131
|
+
if (!sameStrings(currentManifest?.bundleIds, expectedManifest?.bundleIds)) {
|
|
3132
|
+
issues.push({
|
|
3133
|
+
type: "bundle-drift",
|
|
3134
|
+
message: "Target asset bundles do not match the portfolio manifest."
|
|
3135
|
+
});
|
|
3136
|
+
}
|
|
3137
|
+
if (!sameStrings(currentManifest?.assetIds, expectedManifest?.assetIds)) {
|
|
3138
|
+
issues.push({
|
|
3139
|
+
type: "asset-set-drift",
|
|
3140
|
+
message: "Target asset ids do not match the current bundle definitions."
|
|
3141
|
+
});
|
|
3142
|
+
}
|
|
3143
|
+
if (!sameLock(currentLock, expectedLock)) {
|
|
3144
|
+
issues.push({
|
|
3145
|
+
type: "asset-lock-drift",
|
|
3146
|
+
message: "Target asset lock does not match the current registry, placement, and content hashes."
|
|
3147
|
+
});
|
|
3148
|
+
}
|
|
3149
|
+
const expectedTargets = new Set((expectedLock?.assets ?? []).map((entry) => entry.targetPath));
|
|
3150
|
+
for (const entry of currentLock?.assets ?? []) {
|
|
3151
|
+
if (expectedTargets.has(entry.targetPath)) continue;
|
|
3152
|
+
const targetAbsolutePath = join17(options.targetDir, entry.targetPath);
|
|
3153
|
+
if (!pathIsSymlink(targetAbsolutePath)) continue;
|
|
3154
|
+
issues.push({
|
|
3155
|
+
type: "orphaned-managed-symlink",
|
|
3156
|
+
targetPath: entry.targetPath,
|
|
3157
|
+
message: `Previously managed symlink is absent from the expected bundle state: ${entry.targetPath}`
|
|
3158
|
+
});
|
|
3159
|
+
}
|
|
3160
|
+
return { issues };
|
|
3161
|
+
}
|
|
3162
|
+
function readPlanDocument(plan, targetPath) {
|
|
3163
|
+
const action = plan.actions.find(
|
|
3164
|
+
(candidate) => candidate.type === "write-file" && candidate.targetPath === targetPath
|
|
3165
|
+
);
|
|
3166
|
+
if (!action || action.type !== "write-file") return void 0;
|
|
3167
|
+
try {
|
|
3168
|
+
return JSON.parse(action.content);
|
|
3169
|
+
} catch {
|
|
3170
|
+
return void 0;
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3173
|
+
function readJsonFile(path) {
|
|
3174
|
+
if (!existsSync18(path)) return void 0;
|
|
3175
|
+
try {
|
|
3176
|
+
return JSON.parse(readFileSync15(path, "utf8"));
|
|
3177
|
+
} catch {
|
|
3178
|
+
return void 0;
|
|
3179
|
+
}
|
|
3180
|
+
}
|
|
3181
|
+
function sameStrings(left, right) {
|
|
3182
|
+
return JSON.stringify([...left ?? []].sort()) === JSON.stringify([...right ?? []].sort());
|
|
3183
|
+
}
|
|
3184
|
+
function sameLock(left, right) {
|
|
3185
|
+
return JSON.stringify(normalizeLock(left)) === JSON.stringify(normalizeLock(right));
|
|
3186
|
+
}
|
|
3187
|
+
function normalizeLock(lock) {
|
|
3188
|
+
return {
|
|
3189
|
+
host: lock?.host,
|
|
3190
|
+
placement: lock?.placement,
|
|
3191
|
+
bundleIds: [...lock?.bundleIds ?? []].sort(),
|
|
3192
|
+
assets: [...lock?.assets ?? []].sort((a, b) => a.id.localeCompare(b.id))
|
|
3193
|
+
};
|
|
3194
|
+
}
|
|
3195
|
+
function pathIsSymlink(path) {
|
|
3196
|
+
try {
|
|
3197
|
+
return lstatSync5(path).isSymbolicLink();
|
|
3198
|
+
} catch {
|
|
3199
|
+
return false;
|
|
3200
|
+
}
|
|
3201
|
+
}
|
|
3202
|
+
|
|
3203
|
+
// src/portfolio/doctor.ts
|
|
3204
|
+
function inspectPortfolio(options) {
|
|
3205
|
+
const expectedPackageVersions = getExpectedPackageVersions();
|
|
3206
|
+
const hostTooling = inspectHostTooling(options.manifest.hostTooling ?? []);
|
|
3207
|
+
const targets = options.targets.map((target) => inspectTarget({
|
|
3208
|
+
target,
|
|
3209
|
+
agentAssetsDir: options.agentAssetsDir,
|
|
3210
|
+
registry: options.registry,
|
|
3211
|
+
bundles: options.bundles,
|
|
3212
|
+
expectedPackageVersions
|
|
3213
|
+
}));
|
|
3214
|
+
return {
|
|
3215
|
+
ok: hostTooling.issues.length === 0 && targets.every((target) => target.issues.length === 0),
|
|
3216
|
+
portfolioId: options.manifest.portfolioId,
|
|
3217
|
+
expectedPackageVersions,
|
|
3218
|
+
hostTooling,
|
|
3219
|
+
targets
|
|
3220
|
+
};
|
|
3221
|
+
}
|
|
3222
|
+
function inspectTarget(options) {
|
|
3223
|
+
const { target } = options;
|
|
3224
|
+
const issues = [];
|
|
3225
|
+
const packageJson = readJson2(join18(target.path, "package.json"));
|
|
3226
|
+
const packages = {};
|
|
3227
|
+
for (const packageName of ["@pieai/pro-gov", "@pieai/doc-gov"]) {
|
|
3228
|
+
const declared = packageJson?.devDependencies?.[packageName] ?? packageJson?.dependencies?.[packageName];
|
|
3229
|
+
const installedPackage = readJson2(join18(target.path, "node_modules", packageName, "package.json"));
|
|
3230
|
+
const installed = installedPackage?.version;
|
|
3231
|
+
const expected = options.expectedPackageVersions[packageName];
|
|
3232
|
+
packages[packageName] = { declared, installed, expected };
|
|
3233
|
+
if (!declared) {
|
|
3234
|
+
issues.push({
|
|
3235
|
+
type: "package-declaration-missing",
|
|
3236
|
+
packageName,
|
|
3237
|
+
message: `Target does not declare ${packageName}.`
|
|
3238
|
+
});
|
|
3239
|
+
}
|
|
3240
|
+
if (!installed || expected && installed !== expected) {
|
|
3241
|
+
issues.push({
|
|
3242
|
+
type: "package-version-drift",
|
|
3243
|
+
packageName,
|
|
3244
|
+
message: `Target ${packageName} installed version is ${installed ?? "missing"}; expected ${expected ?? "unknown"}.`
|
|
3245
|
+
});
|
|
3246
|
+
}
|
|
3247
|
+
}
|
|
3248
|
+
const checks = runTargetChecks(target);
|
|
3249
|
+
for (const check of checks) {
|
|
3250
|
+
if (check.status === 0) continue;
|
|
3251
|
+
issues.push({
|
|
3252
|
+
type: "target-check-failed",
|
|
3253
|
+
check: check.name,
|
|
3254
|
+
message: `Target check failed: ${check.name} (${check.status ?? "unavailable"}).`
|
|
3255
|
+
});
|
|
3256
|
+
}
|
|
3257
|
+
const assetCheck = checkInstalledAssets({
|
|
3258
|
+
targetDir: target.path,
|
|
3259
|
+
agentAssetsDir: options.agentAssetsDir,
|
|
3260
|
+
registry: options.registry,
|
|
3261
|
+
strictRegistry: true
|
|
3262
|
+
});
|
|
3263
|
+
issues.push(...assetCheck.issues.map((issue) => ({
|
|
3264
|
+
type: issue.type,
|
|
3265
|
+
targetPath: issue.targetPath,
|
|
3266
|
+
message: issue.message
|
|
3267
|
+
})));
|
|
3268
|
+
try {
|
|
3269
|
+
const expectedPlan = createAssetInstallPlan({
|
|
3270
|
+
targetDir: target.path,
|
|
3271
|
+
agentAssetsDir: options.agentAssetsDir,
|
|
3272
|
+
registry: options.registry,
|
|
3273
|
+
bundles: options.bundles,
|
|
3274
|
+
bundleIds: target.assetBundles ?? [],
|
|
3275
|
+
host: "codex"
|
|
3276
|
+
});
|
|
3277
|
+
issues.push(...comparePortfolioAssetState({ targetDir: target.path, expectedPlan }).issues);
|
|
3278
|
+
} catch (error) {
|
|
3279
|
+
issues.push({
|
|
3280
|
+
type: "asset-lock-drift",
|
|
3281
|
+
message: error instanceof Error ? error.message : String(error)
|
|
3282
|
+
});
|
|
3283
|
+
if (!existsSync19(join18(target.path, ".pro-gov/assets.json"))) {
|
|
3284
|
+
issues.push({ type: "bundle-drift", message: "Target asset manifest is missing." });
|
|
3285
|
+
}
|
|
3286
|
+
}
|
|
3287
|
+
return {
|
|
3288
|
+
id: target.id,
|
|
3289
|
+
path: target.path,
|
|
3290
|
+
profile: target.profile,
|
|
3291
|
+
packages,
|
|
3292
|
+
git: inspectGit(target.path),
|
|
3293
|
+
checks,
|
|
3294
|
+
issues: deduplicateIssues(issues)
|
|
3295
|
+
};
|
|
3296
|
+
}
|
|
3297
|
+
function runTargetChecks(target) {
|
|
3298
|
+
const proGovCli = join18(target.path, "node_modules/@pieai/pro-gov/dist/cli.js");
|
|
3299
|
+
const docGovCli = join18(target.path, "node_modules/@pieai/doc-gov/dist/cli.js");
|
|
3300
|
+
const commands = [
|
|
3301
|
+
{
|
|
3302
|
+
name: "pro-gov doctor",
|
|
3303
|
+
cli: proGovCli,
|
|
3304
|
+
args: target.profile === "engineering-runtime" ? ["doctor", "--strict-hooks"] : ["doctor"]
|
|
3305
|
+
},
|
|
3306
|
+
{ name: "doc-gov router-check", cli: docGovCli, args: ["router-check"] },
|
|
3307
|
+
{ name: "doc-gov scan --check", cli: docGovCli, args: ["scan", "--check"] }
|
|
3308
|
+
];
|
|
3309
|
+
return commands.map((command2) => {
|
|
3310
|
+
if (!existsSync19(command2.cli)) return { name: command2.name, status: null };
|
|
3311
|
+
const result = spawnSync6(process.execPath, [command2.cli, ...command2.args], {
|
|
3312
|
+
cwd: target.path,
|
|
3313
|
+
encoding: "utf8",
|
|
3314
|
+
timeout: 3e4
|
|
3315
|
+
});
|
|
3316
|
+
return { name: command2.name, status: result.status };
|
|
3317
|
+
});
|
|
3318
|
+
}
|
|
3319
|
+
function inspectGit(path) {
|
|
3320
|
+
const inside = spawnSync6("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
3321
|
+
cwd: path,
|
|
3322
|
+
encoding: "utf8"
|
|
3323
|
+
});
|
|
3324
|
+
if (inside.status !== 0) return { isRepository: false, dirty: false };
|
|
3325
|
+
const status = spawnSync6("git", ["status", "--porcelain"], { cwd: path, encoding: "utf8" });
|
|
3326
|
+
const branch = spawnSync6("git", ["branch", "--show-current"], { cwd: path, encoding: "utf8" });
|
|
3327
|
+
return {
|
|
3328
|
+
isRepository: true,
|
|
3329
|
+
dirty: status.stdout.trim().length > 0,
|
|
3330
|
+
branch: branch.stdout.trim() || void 0
|
|
3331
|
+
};
|
|
3332
|
+
}
|
|
3333
|
+
function getExpectedPackageVersions() {
|
|
3334
|
+
const proGovPackage = readJson2(findOwnPackageJson());
|
|
3335
|
+
let docGovVersion;
|
|
3336
|
+
try {
|
|
3337
|
+
const require2 = createRequire2(import.meta.url);
|
|
3338
|
+
const docGovPackage = readJson2(require2.resolve("@pieai/doc-gov/package.json"));
|
|
3339
|
+
docGovVersion = docGovPackage?.version;
|
|
3340
|
+
} catch {
|
|
3341
|
+
docGovVersion = void 0;
|
|
3342
|
+
}
|
|
3343
|
+
return {
|
|
3344
|
+
"@pieai/pro-gov": proGovPackage?.version,
|
|
3345
|
+
"@pieai/doc-gov": docGovVersion
|
|
3346
|
+
};
|
|
3347
|
+
}
|
|
3348
|
+
function findOwnPackageJson() {
|
|
3349
|
+
let current = dirname11(fileURLToPath3(import.meta.url));
|
|
3350
|
+
for (let depth = 0; depth < 5; depth += 1) {
|
|
3351
|
+
const candidate = join18(current, "package.json");
|
|
3352
|
+
if (existsSync19(candidate)) return candidate;
|
|
3353
|
+
current = dirname11(current);
|
|
3354
|
+
}
|
|
3355
|
+
return "";
|
|
3356
|
+
}
|
|
3357
|
+
function readJson2(path) {
|
|
3358
|
+
if (!path || !existsSync19(path)) return void 0;
|
|
3359
|
+
try {
|
|
3360
|
+
return JSON.parse(readFileSync16(path, "utf8"));
|
|
3361
|
+
} catch {
|
|
3362
|
+
return void 0;
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3365
|
+
function deduplicateIssues(issues) {
|
|
3366
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3367
|
+
return issues.filter((issue) => {
|
|
3368
|
+
const key = `${issue.type}\0${issue.packageName ?? ""}\0${issue.targetPath ?? ""}\0${issue.check ?? ""}\0${issue.message}`;
|
|
3369
|
+
if (seen.has(key)) return false;
|
|
3370
|
+
seen.add(key);
|
|
3371
|
+
return true;
|
|
3372
|
+
});
|
|
3373
|
+
}
|
|
3374
|
+
|
|
2881
3375
|
// src/commands/portfolio.ts
|
|
2882
3376
|
function runPortfolio(args) {
|
|
2883
3377
|
const [subcommand2, ...rest] = args;
|
|
2884
3378
|
if (subcommand2 === "check") return runPortfolioCheck(rest);
|
|
2885
3379
|
if (subcommand2 === "plan") return runPortfolioPlan(rest);
|
|
2886
3380
|
if (subcommand2 === "assets-check") return runPortfolioAssetsCheck(rest);
|
|
3381
|
+
if (subcommand2 === "doctor") return runPortfolioDoctor(rest);
|
|
2887
3382
|
printUsage3();
|
|
2888
3383
|
return 1;
|
|
2889
3384
|
}
|
|
3385
|
+
function runPortfolioDoctor(args) {
|
|
3386
|
+
const options = parsePortfolioOptions(args);
|
|
3387
|
+
if (!options.ok) {
|
|
3388
|
+
console.error(options.error);
|
|
3389
|
+
printUsage3();
|
|
3390
|
+
return 1;
|
|
3391
|
+
}
|
|
3392
|
+
const loaded = loadPortfolioManifest(options.value.configPath);
|
|
3393
|
+
if (loaded.issues.length > 0 || !loaded.manifest) {
|
|
3394
|
+
if (options.value.json) {
|
|
3395
|
+
console.log(JSON.stringify({ ok: false, configPath: loaded.configPath, issues: loaded.issues, targets: [] }, null, 2));
|
|
3396
|
+
} else {
|
|
3397
|
+
for (const issue of loaded.issues) console.error(`${issue.type}: ${issue.message}`);
|
|
3398
|
+
}
|
|
3399
|
+
return 1;
|
|
3400
|
+
}
|
|
3401
|
+
const targets = getDefaultPortfolioTargets(loaded.manifest).filter(
|
|
3402
|
+
(target) => !options.value.targetId || options.value.targetId === "all" || target.id === options.value.targetId
|
|
3403
|
+
);
|
|
3404
|
+
if (targets.length === 0) {
|
|
3405
|
+
console.error(`Unknown portfolio target: ${options.value.targetId}`);
|
|
3406
|
+
return 1;
|
|
3407
|
+
}
|
|
3408
|
+
const loadedAssets = loadAgentAssetRegistry({
|
|
3409
|
+
agentAssetsDir: findPortfolioAgentAssetsDir(loaded.manifest)
|
|
3410
|
+
});
|
|
3411
|
+
if (loadedAssets.issues.length > 0) {
|
|
3412
|
+
for (const issue of loadedAssets.issues) console.error(`${issue.type}: ${issue.message}`);
|
|
3413
|
+
return 1;
|
|
3414
|
+
}
|
|
3415
|
+
const result = inspectPortfolio({
|
|
3416
|
+
manifest: loaded.manifest,
|
|
3417
|
+
targets,
|
|
3418
|
+
agentAssetsDir: loadedAssets.agentAssetsDir,
|
|
3419
|
+
registry: loadedAssets.registry,
|
|
3420
|
+
bundles: loadAgentAssetBundles(loadedAssets.agentAssetsDir)
|
|
3421
|
+
});
|
|
3422
|
+
const output = { configPath: loaded.configPath, ...result };
|
|
3423
|
+
if (options.value.json) {
|
|
3424
|
+
console.log(JSON.stringify(output, null, 2));
|
|
3425
|
+
} else if (result.ok) {
|
|
3426
|
+
console.log(`portfolio doctor passed (${targets.length} targets)`);
|
|
3427
|
+
} else {
|
|
3428
|
+
for (const issue of result.hostTooling.issues) console.log(`${issue.host} ${issue.type}: ${issue.message}`);
|
|
3429
|
+
for (const target of result.targets) {
|
|
3430
|
+
for (const issue of target.issues) console.log(`${target.id} ${issue.type}: ${issue.message}`);
|
|
3431
|
+
}
|
|
3432
|
+
}
|
|
3433
|
+
return result.ok ? 0 : 1;
|
|
3434
|
+
}
|
|
2890
3435
|
function runPortfolioCheck(args) {
|
|
2891
3436
|
const options = parsePortfolioOptions(args);
|
|
2892
3437
|
if (!options.ok) {
|
|
@@ -3129,19 +3674,20 @@ function isHost2(value) {
|
|
|
3129
3674
|
return value === "codex" || value === "claude-code" || value === "gemini-cli" || value === "antigravity";
|
|
3130
3675
|
}
|
|
3131
3676
|
function findPortfolioAgentAssetsDir(manifest) {
|
|
3132
|
-
const agentAssetsDir = manifest?.executionEngine?.path ?
|
|
3133
|
-
return agentAssetsDir &&
|
|
3677
|
+
const agentAssetsDir = manifest?.executionEngine?.path ? join19(manifest.executionEngine.path, "agent-assets") : void 0;
|
|
3678
|
+
return agentAssetsDir && existsSync20(join19(agentAssetsDir, "registry.json")) ? agentAssetsDir : void 0;
|
|
3134
3679
|
}
|
|
3135
3680
|
function printUsage3() {
|
|
3136
3681
|
console.error("Usage:");
|
|
3137
3682
|
console.error(" pro-gov portfolio check --config <path> [--json]");
|
|
3138
3683
|
console.error(" pro-gov portfolio plan --config <path> [--target <id|all>] [--host codex|claude-code|gemini-cli|antigravity] [--json]");
|
|
3139
3684
|
console.error(" pro-gov portfolio assets-check --config <path> [--target <id|all>] [--json]");
|
|
3685
|
+
console.error(" pro-gov portfolio doctor --config <path> [--target <id|all>] [--json]");
|
|
3140
3686
|
}
|
|
3141
3687
|
|
|
3142
3688
|
// src/commands/sync.ts
|
|
3143
|
-
import { existsSync as
|
|
3144
|
-
import { join as
|
|
3689
|
+
import { existsSync as existsSync21, readFileSync as readFileSync17 } from "node:fs";
|
|
3690
|
+
import { join as join20 } from "node:path";
|
|
3145
3691
|
function runSync(args) {
|
|
3146
3692
|
if (!args.includes("--check")) {
|
|
3147
3693
|
console.error("pro-gov sync is read-only and requires --check.");
|
|
@@ -3168,16 +3714,16 @@ function runSync(args) {
|
|
|
3168
3714
|
console.log("pro-gov sync check");
|
|
3169
3715
|
console.log(`profile: ${profile}`);
|
|
3170
3716
|
for (const file of planStarterFiles(profile)) {
|
|
3171
|
-
const targetPath =
|
|
3172
|
-
if (!
|
|
3717
|
+
const targetPath = join20(process.cwd(), file.targetPath);
|
|
3718
|
+
if (!existsSync21(targetPath)) {
|
|
3173
3719
|
if (file.ownership === "optional-guardrail") continue;
|
|
3174
3720
|
console.log(`missing: ${file.targetPath}`);
|
|
3175
3721
|
differences += 1;
|
|
3176
3722
|
continue;
|
|
3177
3723
|
}
|
|
3178
3724
|
if (file.ownership === "project-local-seed") continue;
|
|
3179
|
-
const source =
|
|
3180
|
-
const target =
|
|
3725
|
+
const source = readFileSync17(file.absoluteSourcePath, "utf8");
|
|
3726
|
+
const target = readFileSync17(targetPath, "utf8");
|
|
3181
3727
|
if (source !== target) {
|
|
3182
3728
|
console.log(`different: ${file.targetPath}`);
|
|
3183
3729
|
differences += 1;
|
|
@@ -3192,7 +3738,7 @@ function runSync(args) {
|
|
|
3192
3738
|
}
|
|
3193
3739
|
function inferInstalledProfile(root) {
|
|
3194
3740
|
const installed = ["engineering-runtime", "doc-only"].filter(
|
|
3195
|
-
(profile) =>
|
|
3741
|
+
(profile) => existsSync21(join20(root, `docs/governance/agents-routing/${profile}-v0.9.md`))
|
|
3196
3742
|
);
|
|
3197
3743
|
return installed.length === 1 ? installed[0] : void 0;
|
|
3198
3744
|
}
|
|
@@ -3215,6 +3761,7 @@ var COMMANDS = [
|
|
|
3215
3761
|
"portfolio check --config <path> [--json]",
|
|
3216
3762
|
"portfolio plan --config <path> [--target <id|all>] [--json]",
|
|
3217
3763
|
"portfolio assets-check --config <path> [--target <id|all>] [--json]",
|
|
3764
|
+
"portfolio doctor --config <path> [--target <id|all>] [--json]",
|
|
3218
3765
|
"lens scan [--target <path>] [--json]",
|
|
3219
3766
|
"lens inspect [--target <path>] [--format text|json]",
|
|
3220
3767
|
"lens report --target <path> --out <path>",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pieai/pro-gov",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.12",
|
|
4
4
|
"description": "Project-level distribution kit for Project Governance System.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-agents",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"access": "public"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@pieai/doc-gov": "^0.3.
|
|
38
|
+
"@pieai/doc-gov": "^0.3.12"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@types/node": "24.13.2",
|