@pieai/pro-gov 0.3.3 → 0.3.4

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/dist/cli.js CHANGED
@@ -30,6 +30,7 @@ function isValidProfile(profile) {
30
30
  function listFiles(dir) {
31
31
  const files = [];
32
32
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
33
+ if (isPlatformMetadata(entry.name)) continue;
33
34
  const absolutePath = join(dir, entry.name);
34
35
  if (entry.isDirectory()) {
35
36
  files.push(...listFiles(absolutePath));
@@ -42,27 +43,1194 @@ function listFiles(dir) {
42
43
  function toUnixPath(path) {
43
44
  return path.replaceAll("\\", "/");
44
45
  }
46
+ function isPlatformMetadata(name) {
47
+ return name === ".DS_Store" || name === "Thumbs.db" || name.startsWith("._");
48
+ }
49
+
50
+ // src/commands/assets.ts
51
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync2 } from "node:fs";
52
+ import { dirname as dirname5 } from "node:path";
53
+
54
+ // src/asset-bundles/bundles.ts
55
+ import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync } from "node:fs";
56
+ import { join as join2 } from "node:path";
57
+ function loadAgentAssetBundles(agentAssetsDir) {
58
+ const bundlesDir = join2(agentAssetsDir, "bundles");
59
+ if (!existsSync2(bundlesDir)) return [];
60
+ return readdirSync2(bundlesDir, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => {
61
+ const bundlePath = join2(bundlesDir, entry.name);
62
+ return JSON.parse(readFileSync(bundlePath, "utf8"));
63
+ }).sort((a, b) => a.id.localeCompare(b.id));
64
+ }
65
+
66
+ // src/asset-npx/maintenance.ts
67
+ import { createHash } from "node:crypto";
68
+ import { spawnSync } from "node:child_process";
69
+ import { cpSync, existsSync as existsSync3, mkdirSync, mkdtempSync, readdirSync as readdirSync3, readFileSync as readFileSync2, statSync } from "node:fs";
70
+ import { join as join3, relative as relative2 } from "node:path";
71
+ import { tmpdir } from "node:os";
72
+ function createNpxSkillsMaintenancePlan(options) {
73
+ assertNativeNpxRoot(options.npxRoot);
74
+ if (options.operation === "add" && !options.source) {
75
+ throw new Error("npx skills add requires a source.");
76
+ }
77
+ const before = snapshotFiles(options.npxRoot);
78
+ const tempRoot = mkdtempSync(join3(tmpdir(), "pro-gov-npx-skills-"));
79
+ cpSync(options.npxRoot, tempRoot, { recursive: true, dereference: false });
80
+ const command2 = buildNpxCommand(options);
81
+ const runner = options.runner ?? defaultRunner;
82
+ const result = runner({ command: command2, cwd: tempRoot });
83
+ if (result.status !== 0) {
84
+ throw new Error(`npx skills ${options.operation} failed with exit code ${result.status}`);
85
+ }
86
+ if (options.operation === "update") {
87
+ assertNoReportedPartialUpdateFailure(result.stdout, result.stderr);
88
+ }
89
+ const after = snapshotFiles(tempRoot);
90
+ const changes = diffSnapshots(before, after);
91
+ return {
92
+ schemaVersion: 1,
93
+ operation: options.operation,
94
+ npxRoot: options.npxRoot,
95
+ tempRoot,
96
+ command: command2,
97
+ stdout: result.stdout,
98
+ stderr: result.stderr,
99
+ exitCode: result.status ?? 0,
100
+ changes,
101
+ summary: summarizeChanges(changes),
102
+ appliedToRealRoot: false
103
+ };
104
+ }
105
+ function assertNoReportedPartialUpdateFailure(stdout, stderr) {
106
+ const output = `${stdout}
107
+ ${stderr}`.replace(/\u001B\[[0-?]*[ -/]*[@-~]/g, "");
108
+ const failure = output.match(/Failed to update\s+\d+\s+skill\(s\)/i);
109
+ if (failure) {
110
+ throw new Error(`npx skills update reported a partial failure: ${failure[0]}`);
111
+ }
112
+ }
113
+ function assertNativeNpxRoot(npxRoot) {
114
+ if (!existsSync3(join3(npxRoot, "skills-lock.json"))) {
115
+ throw new Error(`npx skills root is missing skills-lock.json: ${npxRoot}`);
116
+ }
117
+ if (!existsSync3(join3(npxRoot, ".agents/skills"))) {
118
+ throw new Error(`npx skills root is missing .agents/skills: ${npxRoot}`);
119
+ }
120
+ }
121
+ function buildNpxCommand(options) {
122
+ if (options.operation === "add") {
123
+ const command3 = ["npx", "--yes", "skills", "add", options.source ?? ""];
124
+ if (options.skill) command3.push("--skill", options.skill);
125
+ return command3;
126
+ }
127
+ const command2 = ["npx", "--yes", "skills", "update", "-p", "-y"];
128
+ if (options.skill) command2.push(options.skill);
129
+ return command2;
130
+ }
131
+ function defaultRunner({ command: command2, cwd }) {
132
+ const result = spawnSync(command2[0] ?? "npx", command2.slice(1), { cwd, encoding: "utf8" });
133
+ return {
134
+ status: result.status,
135
+ stdout: result.stdout,
136
+ stderr: result.stderr
137
+ };
138
+ }
139
+ function snapshotFiles(root) {
140
+ const snapshot = /* @__PURE__ */ new Map();
141
+ for (const filePath of listFiles2(root)) {
142
+ const relativePath = toUnixPath2(relative2(root, filePath));
143
+ snapshot.set(relativePath, hashFile(filePath));
144
+ }
145
+ return snapshot;
146
+ }
147
+ function listFiles2(root) {
148
+ const files = [];
149
+ collectFiles(root, root, files);
150
+ return files.sort();
151
+ }
152
+ function collectFiles(root, current, files) {
153
+ mkdirSync(root, { recursive: true });
154
+ for (const entry of readdirSync3(current, { withFileTypes: true })) {
155
+ const entryPath = join3(current, entry.name);
156
+ if (entry.isDirectory()) {
157
+ collectFiles(root, entryPath, files);
158
+ } else if (entry.isFile()) {
159
+ files.push(entryPath);
160
+ }
161
+ }
162
+ }
163
+ function hashFile(path) {
164
+ const hash = createHash("sha256");
165
+ const stats = statSync(path);
166
+ hash.update(String(stats.size));
167
+ hash.update("\0");
168
+ hash.update(readFileSync2(path));
169
+ return hash.digest("hex");
170
+ }
171
+ function diffSnapshots(before, after) {
172
+ const changes = [];
173
+ for (const [path, hash] of after) {
174
+ if (!before.has(path)) {
175
+ changes.push({ type: "added", path });
176
+ } else if (before.get(path) !== hash) {
177
+ changes.push({ type: "modified", path });
178
+ }
179
+ }
180
+ for (const path of before.keys()) {
181
+ if (!after.has(path)) changes.push({ type: "deleted", path });
182
+ }
183
+ return changes.sort((a, b) => a.path.localeCompare(b.path) || a.type.localeCompare(b.type));
184
+ }
185
+ function summarizeChanges(changes) {
186
+ const counts = /* @__PURE__ */ new Map();
187
+ for (const change of changes) counts.set(change.type, (counts.get(change.type) ?? 0) + 1);
188
+ return `added=${counts.get("added") ?? 0} modified=${counts.get("modified") ?? 0} deleted=${counts.get("deleted") ?? 0}`;
189
+ }
190
+ function toUnixPath2(path) {
191
+ return path.replaceAll("\\", "/");
192
+ }
193
+
194
+ // src/asset-registry/loader.ts
195
+ import { createHash as createHash2 } from "node:crypto";
196
+ import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync3, statSync as statSync2 } from "node:fs";
197
+ import { dirname as dirname2, join as join5, relative as relative3 } from "node:path";
198
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
199
+
200
+ // src/asset-registry/registry.ts
201
+ import { existsSync as existsSync4, lstatSync } from "node:fs";
202
+ import { isAbsolute, join as join4, posix } from "node:path";
203
+ var supportedFamilies = /* @__PURE__ */ new Set([
204
+ "pie-skills",
205
+ "dokobot",
206
+ "npx-skills",
207
+ "pie-rules",
208
+ "pie-commands"
209
+ ]);
210
+ var supportedKinds = /* @__PURE__ */ new Set(["skill", "rule", "command"]);
211
+ var supportedVisibilities = /* @__PURE__ */ new Set([
212
+ "public",
213
+ "private",
214
+ "third-party"
215
+ ]);
216
+ var supportedSourceKinds = /* @__PURE__ */ new Set(["local", "local-pack", "npx"]);
217
+ var supportedHosts = /* @__PURE__ */ new Set([
218
+ "codex",
219
+ "claude-code",
220
+ "gemini-cli",
221
+ "antigravity"
222
+ ]);
223
+ function validateAssetRegistry(registry, options = {}) {
224
+ const issues = [];
225
+ const seenIds = /* @__PURE__ */ new Set();
226
+ for (const asset of registry.assets) {
227
+ if (seenIds.has(asset.id)) {
228
+ issues.push({
229
+ type: "duplicate-id",
230
+ id: asset.id,
231
+ message: `Duplicate asset id: ${asset.id}`
232
+ });
233
+ }
234
+ seenIds.add(asset.id);
235
+ if (!supportedFamilies.has(asset.family)) {
236
+ issues.push({
237
+ type: "unsupported-enum",
238
+ id: asset.id,
239
+ message: `Unsupported asset family: ${asset.family}`
240
+ });
241
+ }
242
+ if (!supportedKinds.has(asset.kind)) {
243
+ issues.push({
244
+ type: "unsupported-enum",
245
+ id: asset.id,
246
+ message: `Unsupported asset kind: ${asset.kind}`
247
+ });
248
+ }
249
+ if (!supportedVisibilities.has(asset.visibility)) {
250
+ issues.push({
251
+ type: "unsupported-enum",
252
+ id: asset.id,
253
+ message: `Unsupported asset visibility: ${asset.visibility}`
254
+ });
255
+ }
256
+ if (!supportedSourceKinds.has(asset.sourceKind)) {
257
+ issues.push({
258
+ type: "unsupported-enum",
259
+ id: asset.id,
260
+ message: `Unsupported asset source kind: ${asset.sourceKind}`
261
+ });
262
+ }
263
+ for (const host of asset.hosts) {
264
+ if (!supportedHosts.has(host)) {
265
+ issues.push({
266
+ type: "unsupported-host",
267
+ id: asset.id,
268
+ message: `Unsupported asset host: ${host}`
269
+ });
270
+ }
271
+ }
272
+ if (!isSafeRegistrySourcePath(asset.sourcePath)) {
273
+ issues.push({
274
+ type: "unsafe-source-path",
275
+ id: asset.id,
276
+ path: asset.sourcePath,
277
+ message: `Asset source path escapes agent-assets: ${asset.sourcePath}`
278
+ });
279
+ }
280
+ if (asset.visibility !== "public" && asset.publishable) {
281
+ issues.push({
282
+ type: "non-public-publishable",
283
+ id: asset.id,
284
+ message: `Only public assets may be publishable: ${asset.id}`
285
+ });
286
+ }
287
+ if (options.agentAssetsDir && isSafeRegistrySourcePath(asset.sourcePath)) {
288
+ const sourceAbsolutePath = join4(options.agentAssetsDir, normalizeRegistrySourcePath(asset.sourcePath));
289
+ if (!existsSync4(sourceAbsolutePath)) {
290
+ issues.push({
291
+ type: "missing-source-path",
292
+ id: asset.id,
293
+ path: asset.sourcePath,
294
+ message: `Asset source path does not exist: ${asset.sourcePath}`
295
+ });
296
+ } else if (asset.kind === "skill" && !existsSync4(join4(sourceAbsolutePath, "SKILL.md"))) {
297
+ issues.push({
298
+ type: "missing-skill-file",
299
+ id: asset.id,
300
+ path: asset.sourcePath,
301
+ message: `Skill asset is missing SKILL.md: ${asset.sourcePath}`
302
+ });
303
+ }
304
+ }
305
+ }
306
+ if (options.agentAssetsDir) {
307
+ const npxCompatibilityLayer = join4(options.agentAssetsDir, "skills/npx-skills/skills");
308
+ if (pathExistsEvenIfDanglingSymlink(npxCompatibilityLayer)) {
309
+ issues.push({
310
+ type: "internal-npx-compatibility-layer",
311
+ id: "npx-skills",
312
+ path: "skills/npx-skills/skills",
313
+ message: "Do not create an internal npx compatibility symlink layer."
314
+ });
315
+ }
316
+ }
317
+ return issues;
318
+ }
319
+ function isSafeRegistrySourcePath(sourcePath) {
320
+ if (!sourcePath || isAbsolute(sourcePath) || sourcePath.startsWith("/")) return false;
321
+ const normalized = normalizeRegistrySourcePath(sourcePath);
322
+ if (normalized === "." || normalized.startsWith("../") || normalized === "..") return false;
323
+ return !normalized.split("/").includes("..");
324
+ }
325
+ function normalizeRegistrySourcePath(sourcePath) {
326
+ return posix.normalize(sourcePath.replaceAll("\\", "/"));
327
+ }
328
+ function pathExistsEvenIfDanglingSymlink(path) {
329
+ try {
330
+ lstatSync(path);
331
+ return true;
332
+ } catch {
333
+ return false;
334
+ }
335
+ }
336
+
337
+ // src/asset-registry/loader.ts
338
+ function loadAgentAssetRegistry(options = {}) {
339
+ const agentAssetsDir = options.agentAssetsDir ?? findDefaultAgentAssetsDir();
340
+ const registryPath = join5(agentAssetsDir, "registry.json");
341
+ if (!existsSync5(registryPath)) {
342
+ return {
343
+ registry: { schemaVersion: 1, assets: [] },
344
+ agentAssetsDir,
345
+ registryPath,
346
+ issues: []
347
+ };
348
+ }
349
+ const registry = JSON.parse(readFileSync3(registryPath, "utf8"));
350
+ return {
351
+ registry,
352
+ agentAssetsDir,
353
+ registryPath,
354
+ issues: validateAssetRegistry(registry, { agentAssetsDir })
355
+ };
356
+ }
357
+ function createAgentAssetLockEntries(registry, agentAssetsDir, assetIds) {
358
+ const wantedIds = assetIds ? new Set(assetIds) : void 0;
359
+ return registry.assets.filter((asset) => !wantedIds || wantedIds.has(asset.id)).map((asset) => ({
360
+ id: asset.id,
361
+ sourcePath: asset.sourcePath,
362
+ contentHash: hashAgentAssetContent(asset, agentAssetsDir)
363
+ })).sort((a, b) => a.id.localeCompare(b.id));
364
+ }
365
+ function hashAgentAssetContent(asset, agentAssetsDir) {
366
+ const sourceAbsolutePath = join5(agentAssetsDir, asset.sourcePath);
367
+ const hash = createHash2("sha256");
368
+ for (const filePath of listFiles3(sourceAbsolutePath)) {
369
+ const relativePath = toUnixPath3(relative3(sourceAbsolutePath, filePath));
370
+ hash.update(relativePath);
371
+ hash.update("\0");
372
+ hash.update(readFileSync3(filePath));
373
+ hash.update("\0");
374
+ }
375
+ return `sha256:${hash.digest("hex")}`;
376
+ }
377
+ function findDefaultAgentAssetsDir() {
378
+ const packageRoot2 = findPackageRoot(dirname2(fileURLToPath2(import.meta.url)));
379
+ const repoRoot = join5(packageRoot2, "..", "..");
380
+ const candidates = [join5(packageRoot2, "assets/agent-assets"), join5(repoRoot, "agent-assets")];
381
+ return candidates.find((candidate) => existsSync5(join5(candidate, "registry.json"))) ?? candidates[0];
382
+ }
383
+ function findPackageRoot(startDir) {
384
+ let current = startDir;
385
+ while (current !== dirname2(current)) {
386
+ const packageJsonPath = join5(current, "package.json");
387
+ if (existsSync5(packageJsonPath)) {
388
+ try {
389
+ const packageJson = JSON.parse(readFileSync3(packageJsonPath, "utf8"));
390
+ if (packageJson.name === "@pieai/pro-gov") return current;
391
+ } catch {
392
+ }
393
+ }
394
+ current = dirname2(current);
395
+ }
396
+ return startDir;
397
+ }
398
+ function listFiles3(absolutePath) {
399
+ const stats = statSync2(absolutePath);
400
+ if (stats.isFile()) return [absolutePath];
401
+ const files = [];
402
+ for (const entry of readdirSync4(absolutePath, { withFileTypes: true })) {
403
+ const entryPath = join5(absolutePath, entry.name);
404
+ if (entry.isDirectory()) {
405
+ files.push(...listFiles3(entryPath));
406
+ } else if (entry.isFile()) {
407
+ files.push(entryPath);
408
+ }
409
+ }
410
+ return files.sort();
411
+ }
412
+ function toUnixPath3(path) {
413
+ return path.replaceAll("\\", "/");
414
+ }
415
+
416
+ // src/asset-targets/apply.ts
417
+ import { existsSync as existsSync6, lstatSync as lstatSync2, mkdirSync as mkdirSync2, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
418
+ import { dirname as dirname3, join as join6, resolve } from "node:path";
419
+ function applyAssetInstallPlan(plan) {
420
+ const appliedActions = [];
421
+ for (const action of plan.actions) {
422
+ applyAction(plan.targetDir, action);
423
+ appliedActions.push(action.type);
424
+ }
425
+ return { appliedActions };
426
+ }
427
+ function applyAction(targetDir, action) {
428
+ const targetAbsolutePath = join6(targetDir, action.targetPath);
429
+ if (action.type === "create-dir") {
430
+ mkdirSync2(targetAbsolutePath, { recursive: true });
431
+ return;
432
+ }
433
+ if (action.type === "write-file") {
434
+ mkdirSync2(dirname3(targetAbsolutePath), { recursive: true });
435
+ writeFileSync(targetAbsolutePath, action.content);
436
+ return;
437
+ }
438
+ mkdirSync2(dirname3(targetAbsolutePath), { recursive: true });
439
+ const sourceAbsolutePath = resolve(action.sourcePath);
440
+ if (action.type === "symlink") {
441
+ if (pathExistsEvenIfDanglingSymlink2(targetAbsolutePath)) {
442
+ throw new Error(`Refusing to overwrite unmanaged target: ${action.targetPath}`);
443
+ }
444
+ symlinkSync(sourceAbsolutePath, targetAbsolutePath);
445
+ return;
446
+ }
447
+ if (!pathExistsEvenIfDanglingSymlink2(targetAbsolutePath)) {
448
+ symlinkSync(sourceAbsolutePath, targetAbsolutePath);
449
+ return;
450
+ }
451
+ const stats = lstatSync2(targetAbsolutePath);
452
+ if (!stats.isSymbolicLink()) {
453
+ throw new Error(`Refusing to overwrite unmanaged target: ${action.targetPath}`);
454
+ }
455
+ unlinkSync(targetAbsolutePath);
456
+ symlinkSync(sourceAbsolutePath, targetAbsolutePath);
457
+ }
458
+ function pathExistsEvenIfDanglingSymlink2(path) {
459
+ try {
460
+ lstatSync2(path);
461
+ return true;
462
+ } catch {
463
+ return existsSync6(path);
464
+ }
465
+ }
466
+
467
+ // src/asset-targets/check.ts
468
+ import { existsSync as existsSync7, lstatSync as lstatSync3, readFileSync as readFileSync4 } from "node:fs";
469
+ import { join as join7 } from "node:path";
470
+ function checkInstalledAssets(options) {
471
+ const lockfilePath = join7(options.targetDir, ".pro-gov/assets.lock.json");
472
+ if (!existsSync7(lockfilePath)) {
473
+ return {
474
+ targetDir: options.targetDir,
475
+ issues: [
476
+ {
477
+ type: "missing-lock",
478
+ message: "Missing .pro-gov/assets.lock.json"
479
+ }
480
+ ]
481
+ };
482
+ }
483
+ const registryById = new Map(options.registry.assets.map((asset) => [asset.id, asset]));
484
+ const lockfile = JSON.parse(readFileSync4(lockfilePath, "utf8"));
485
+ const issues = [];
486
+ for (const entry of lockfile.assets ?? []) {
487
+ const asset = registryById.get(entry.id);
488
+ const targetAbsolutePath = join7(options.targetDir, entry.targetPath);
489
+ const sourceAbsolutePath = join7(options.agentAssetsDir, entry.sourcePath);
490
+ if (!asset) {
491
+ issues.push({
492
+ type: "unknown-asset",
493
+ id: entry.id,
494
+ targetPath: entry.targetPath,
495
+ message: `Lockfile references unknown asset: ${entry.id}`
496
+ });
497
+ continue;
498
+ }
499
+ const hostFolderIssue = checkHostFolder(lockfile.host, asset.kind, entry.targetPath, entry.id);
500
+ if (hostFolderIssue) {
501
+ issues.push(hostFolderIssue);
502
+ }
503
+ if (!pathExistsEvenIfDanglingSymlink3(targetAbsolutePath)) {
504
+ issues.push({
505
+ type: "missing-target",
506
+ id: entry.id,
507
+ targetPath: entry.targetPath,
508
+ message: `Managed target is missing: ${entry.targetPath}`
509
+ });
510
+ continue;
511
+ }
512
+ const targetStats = lstatSync3(targetAbsolutePath);
513
+ if (!targetStats.isSymbolicLink()) {
514
+ issues.push({
515
+ type: "unmanaged-conflict",
516
+ id: entry.id,
517
+ targetPath: entry.targetPath,
518
+ message: `Managed target is not a symlink: ${entry.targetPath}`
519
+ });
520
+ continue;
521
+ }
522
+ if (!existsSync7(targetAbsolutePath)) {
523
+ issues.push({
524
+ type: "dangling-symlink",
525
+ id: entry.id,
526
+ targetPath: entry.targetPath,
527
+ message: `Managed symlink is dangling: ${entry.targetPath}`
528
+ });
529
+ continue;
530
+ }
531
+ if (!existsSync7(sourceAbsolutePath)) {
532
+ issues.push({
533
+ type: "missing-source",
534
+ id: entry.id,
535
+ targetPath: entry.targetPath,
536
+ message: `Managed asset source is missing: ${entry.sourcePath}`
537
+ });
538
+ continue;
539
+ }
540
+ const currentHash = hashAgentAssetContent(asset, options.agentAssetsDir);
541
+ if (currentHash !== entry.contentHash) {
542
+ issues.push({
543
+ type: "hash-drift",
544
+ id: entry.id,
545
+ targetPath: entry.targetPath,
546
+ message: `Managed asset hash drifted: ${entry.id}`
547
+ });
548
+ }
549
+ }
550
+ return { targetDir: options.targetDir, issues };
551
+ }
552
+ function checkHostFolder(host, kind, targetPath, id) {
553
+ if (kind !== "skill") return void 0;
554
+ const expectedPrefix = expectedSkillTargetPrefix(host);
555
+ if (!expectedPrefix) {
556
+ return {
557
+ type: "unsupported-host-folder",
558
+ id,
559
+ targetPath,
560
+ message: `Lockfile host is unsupported for managed skill target: ${host ?? "missing"}`
561
+ };
562
+ }
563
+ if (!targetPath.startsWith(expectedPrefix)) {
564
+ return {
565
+ type: "unsupported-host-folder",
566
+ id,
567
+ targetPath,
568
+ message: `Managed skill target ${targetPath} does not match host ${host}; expected ${expectedPrefix}`
569
+ };
570
+ }
571
+ return void 0;
572
+ }
573
+ function expectedSkillTargetPrefix(host) {
574
+ if (host === "claude-code") return ".claude/skills/";
575
+ if (host === "codex" || host === "gemini-cli" || host === "antigravity") {
576
+ return ".agents/skills/";
577
+ }
578
+ return void 0;
579
+ }
580
+ function pathExistsEvenIfDanglingSymlink3(path) {
581
+ try {
582
+ lstatSync3(path);
583
+ return true;
584
+ } catch {
585
+ return false;
586
+ }
587
+ }
588
+
589
+ // src/asset-targets/install-plan.ts
590
+ import { existsSync as existsSync8, lstatSync as lstatSync4, readFileSync as readFileSync5 } from "node:fs";
591
+ import { basename, dirname as dirname4, join as join8 } from "node:path";
592
+ function createAssetInstallPlan(options) {
593
+ const assetsById = new Map(options.registry.assets.map((asset) => [asset.id, asset]));
594
+ const bundlesById = new Map(options.bundles.map((bundle) => [bundle.id, bundle]));
595
+ const assetIds = resolveBundleAssetIds(options.bundleIds, bundlesById);
596
+ const assets = assetIds.map((assetId) => {
597
+ const asset = assetsById.get(assetId);
598
+ if (!asset) throw new Error(`Unknown asset id in bundle: ${assetId}`);
599
+ if (asset.kind === "skill" && !asset.hosts.includes(options.host)) {
600
+ throw new Error(`Asset ${assetId} does not support host ${options.host}`);
601
+ }
602
+ return asset;
603
+ });
604
+ const lockEntries = createAgentAssetLockEntries(options.registry, options.agentAssetsDir, assetIds);
605
+ const managedTargets = readManagedTargets(options.targetDir);
606
+ const assetActions = assets.map(
607
+ (asset) => createAssetAction(asset, options.agentAssetsDir, options.targetDir, options.host, managedTargets)
608
+ );
609
+ const manifest = {
610
+ schemaVersion: 1,
611
+ host: options.host,
612
+ bundleIds: [...options.bundleIds],
613
+ assetIds
614
+ };
615
+ const lockfile = {
616
+ schemaVersion: 1,
617
+ host: options.host,
618
+ bundleIds: [...options.bundleIds],
619
+ assets: lockEntries.map((entry) => {
620
+ const action = assetActions.find((candidate) => "assetId" in candidate && candidate.assetId === entry.id);
621
+ return {
622
+ ...entry,
623
+ targetPath: action && "targetPath" in action ? action.targetPath : ""
624
+ };
625
+ })
626
+ };
627
+ const writeActions = [
628
+ {
629
+ type: "write-file",
630
+ targetPath: ".pro-gov/assets.json",
631
+ content: `${JSON.stringify(manifest, null, 2)}
632
+ `
633
+ },
634
+ {
635
+ type: "write-file",
636
+ targetPath: ".pro-gov/assets.lock.json",
637
+ content: `${JSON.stringify(lockfile, null, 2)}
638
+ `
639
+ }
640
+ ];
641
+ return {
642
+ schemaVersion: 1,
643
+ dryRun: true,
644
+ targetDir: options.targetDir,
645
+ host: options.host,
646
+ bundleIds: [...options.bundleIds],
647
+ assetIds,
648
+ actions: [...createDirectoryActions([...assetActions, ...writeActions]), ...assetActions, ...writeActions]
649
+ };
650
+ }
651
+ function resolveBundleAssetIds(bundleIds, bundlesById) {
652
+ const ids = /* @__PURE__ */ new Set();
653
+ for (const bundleId of bundleIds) {
654
+ const bundle = bundlesById.get(bundleId);
655
+ if (!bundle) throw new Error(`Unknown bundle id: ${bundleId}`);
656
+ for (const assetId of bundle.assets) {
657
+ ids.add(assetId);
658
+ }
659
+ }
660
+ return [...ids].sort();
661
+ }
662
+ function createAssetAction(asset, agentAssetsDir, targetDir, host, managedTargets) {
663
+ const sourcePath = join8(agentAssetsDir, asset.sourcePath);
664
+ const targetPath = resolveHostTargetPath(asset, host);
665
+ const targetAbsolutePath = join8(targetDir, targetPath);
666
+ const targetExists = pathExistsEvenIfDanglingSymlink4(targetAbsolutePath);
667
+ if (targetExists) {
668
+ const stats = lstatSync4(targetAbsolutePath);
669
+ if (stats.isSymbolicLink() && managedTargets.has(targetPath)) {
670
+ return {
671
+ type: "update-symlink",
672
+ assetId: asset.id,
673
+ sourcePath,
674
+ targetPath
675
+ };
676
+ }
677
+ throw new Error(`Refusing to overwrite unmanaged target: ${targetPath}`);
678
+ }
679
+ return {
680
+ type: "symlink",
681
+ assetId: asset.id,
682
+ sourcePath,
683
+ targetPath
684
+ };
685
+ }
686
+ function resolveHostTargetPath(asset, host) {
687
+ if (asset.kind === "skill") {
688
+ if (host === "claude-code") {
689
+ return `.claude/skills/${basename(asset.sourcePath)}`;
690
+ }
691
+ return `.agents/skills/${basename(asset.sourcePath)}`;
692
+ }
693
+ if (asset.kind === "rule") {
694
+ return `.pro-gov/agent-assets/rules/${basename(asset.sourcePath)}`;
695
+ }
696
+ return `.pro-gov/agent-assets/commands/${basename(asset.sourcePath)}`;
697
+ }
698
+ function createDirectoryActions(actions) {
699
+ const directories = /* @__PURE__ */ new Set();
700
+ for (const action of actions) {
701
+ if (action.type === "create-dir") continue;
702
+ const directory = dirname4(action.targetPath);
703
+ if (directory !== ".") directories.add(directory);
704
+ }
705
+ return [...directories].sort().map((targetPath) => ({ type: "create-dir", targetPath }));
706
+ }
707
+ function readManagedTargets(targetDir) {
708
+ const lockfilePath = join8(targetDir, ".pro-gov/assets.lock.json");
709
+ if (!existsSync8(lockfilePath)) return /* @__PURE__ */ new Set();
710
+ try {
711
+ const lockfile = JSON.parse(readFileSync5(lockfilePath, "utf8"));
712
+ return new Set((lockfile.assets ?? []).map((asset) => asset.targetPath).filter(Boolean));
713
+ } catch {
714
+ return /* @__PURE__ */ new Set();
715
+ }
716
+ }
717
+ function pathExistsEvenIfDanglingSymlink4(path) {
718
+ try {
719
+ lstatSync4(path);
720
+ return true;
721
+ } catch {
722
+ return false;
723
+ }
724
+ }
725
+
726
+ // src/asset-targets/recommend.ts
727
+ import { existsSync as existsSync9, readdirSync as readdirSync5, readFileSync as readFileSync6 } from "node:fs";
728
+ import { join as join9 } from "node:path";
729
+ var frontendPackages = /* @__PURE__ */ new Set([
730
+ "@vitejs/plugin-react",
731
+ "astro",
732
+ "next",
733
+ "nuxt",
734
+ "react",
735
+ "svelte",
736
+ "tailwindcss",
737
+ "vite",
738
+ "vue"
739
+ ]);
740
+ var agentEntryCandidates = ["AGENTS.md", "CLAUDE.md", ".gemini/settings.json", "GEMINI.md"];
741
+ function discoverTargetSignals(targetDir) {
742
+ const packageJson = readJson(join9(targetDir, "package.json"));
743
+ const dependencyNames = packageJson ? Object.keys({ ...packageJson.dependencies, ...packageJson.devDependencies }) : [];
744
+ const frontendSignals = dependencyNames.filter((name) => frontendPackages.has(name)).sort();
745
+ const hasAgentEntry = agentEntryCandidates.some((file) => existsSync9(join9(targetDir, file)));
746
+ const researchSignals = [
747
+ existsSync9(join9(targetDir, "docs/research")) ? "docs/research" : "",
748
+ existsSync9(join9(targetDir, "research")) ? "research" : "",
749
+ hasBookChildDirectory(targetDir, "research") ? "books/*/research" : "",
750
+ textFileIncludes(join9(targetDir, "README.md"), ["research", "\u8C03\u7814"]) ? "README research" : ""
751
+ ].filter(Boolean);
752
+ const writingSignals = [
753
+ existsSync9(join9(targetDir, "chapters")) ? "chapters" : "",
754
+ existsSync9(join9(targetDir, "src/chapters")) ? "src/chapters" : "",
755
+ hasBookChildDirectory(targetDir, "chapters") ? "books/*/chapters" : "",
756
+ textFileIncludes(join9(targetDir, "AGENTS.md"), ["writing mode", "novel chapter", "book content"]) ? "AGENTS writing" : ""
757
+ ].filter(Boolean);
758
+ return {
759
+ targetDir,
760
+ hasPackageJson: Boolean(packageJson),
761
+ hasAgentEntry,
762
+ frontendSignals,
763
+ researchSignals,
764
+ writingSignals
765
+ };
766
+ }
767
+ function recommendBundlesForTarget(targetDir) {
768
+ const signals = discoverTargetSignals(targetDir);
769
+ const recommendations = [
770
+ {
771
+ bundleId: "base-governance",
772
+ confidence: signals.hasAgentEntry || signals.hasPackageJson ? "high" : "medium",
773
+ reasons: [
774
+ signals.hasAgentEntry ? "agent entry file present" : "",
775
+ signals.hasPackageJson ? "package.json present" : "",
776
+ !signals.hasAgentEntry && !signals.hasPackageJson ? "default project governance baseline" : ""
777
+ ].filter(Boolean)
778
+ }
779
+ ];
780
+ if (signals.frontendSignals.length > 0) {
781
+ recommendations.push({
782
+ bundleId: "frontend-app",
783
+ confidence: "high",
784
+ reasons: signals.frontendSignals.map((signal) => `frontend dependency: ${signal}`)
785
+ });
786
+ }
787
+ if (signals.researchSignals.length > 0) {
788
+ recommendations.push({
789
+ bundleId: "research-docs",
790
+ confidence: "high",
791
+ reasons: signals.researchSignals
792
+ });
793
+ }
794
+ if (signals.writingSignals.length > 0) {
795
+ recommendations.push({
796
+ bundleId: "novel-writing",
797
+ confidence: "high",
798
+ reasons: signals.writingSignals
799
+ });
800
+ }
801
+ return recommendations;
802
+ }
803
+ function readJson(path) {
804
+ if (!existsSync9(path)) return void 0;
805
+ try {
806
+ return JSON.parse(readFileSync6(path, "utf8"));
807
+ } catch {
808
+ return void 0;
809
+ }
810
+ }
811
+ function textFileIncludes(path, needles) {
812
+ if (!existsSync9(path)) return false;
813
+ const contents = readFileSync6(path, "utf8").toLowerCase();
814
+ return needles.some((needle) => contents.includes(needle.toLowerCase()));
815
+ }
816
+ function hasBookChildDirectory(targetDir, childName) {
817
+ const booksDir = join9(targetDir, "books");
818
+ if (!existsSync9(booksDir)) return false;
819
+ try {
820
+ return readdirSync5(booksDir, { withFileTypes: true }).some(
821
+ (entry) => entry.isDirectory() && existsSync9(join9(booksDir, entry.name, childName))
822
+ );
823
+ } catch {
824
+ return false;
825
+ }
826
+ }
45
827
 
46
828
  // src/commands/assets.ts
47
829
  function runAssets(args) {
48
- const [subcommand2] = args;
49
- if (subcommand2 !== "list") {
50
- console.error("Usage: pro-gov assets list");
830
+ const [subcommand2, ...rest] = args;
831
+ if (subcommand2 === "list") {
832
+ return runAssetsList(rest);
833
+ }
834
+ if (subcommand2 === "recommend") {
835
+ return runAssetsRecommend(rest);
836
+ }
837
+ if (subcommand2 === "discover") {
838
+ return runAssetsDiscover(rest);
839
+ }
840
+ if (subcommand2 === "plan") {
841
+ return runAssetsPlan(rest);
842
+ }
843
+ if (subcommand2 === "apply") {
844
+ return runAssetsApply(rest);
845
+ }
846
+ if (subcommand2 === "check") {
847
+ return runAssetsCheck(rest);
848
+ }
849
+ if (subcommand2 === "npx") {
850
+ return runAssetsNpx(rest);
851
+ }
852
+ printUsage();
853
+ return 1;
854
+ }
855
+ function runAssetsNpx(args) {
856
+ const [operation, ...rest] = args;
857
+ if (!operation || operation === "--help" || operation === "-h" || rest.includes("--help")) {
858
+ printNpxUsage();
859
+ return 0;
860
+ }
861
+ if (operation !== "add" && operation !== "update") {
862
+ printNpxUsage();
863
+ return 1;
864
+ }
865
+ const options = parseNpxOptions(operation, rest);
866
+ if (!options.ok) {
867
+ console.error(options.error);
868
+ printNpxUsage();
869
+ return 1;
870
+ }
871
+ try {
872
+ const loaded = loadAgentAssetRegistry();
873
+ const plan = createNpxSkillsMaintenancePlan({
874
+ operation,
875
+ npxRoot: options.value.npxRoot ?? `${loaded.agentAssetsDir}/skills/npx-skills`,
876
+ source: options.value.source,
877
+ skill: options.value.skill
878
+ });
879
+ console.log(JSON.stringify(plan, null, 2));
880
+ return 0;
881
+ } catch (error) {
882
+ console.error(error instanceof Error ? error.message : String(error));
883
+ return 1;
884
+ }
885
+ }
886
+ function runAssetsApply(args) {
887
+ const options = parseApplyOptions(args);
888
+ if (!options.ok) {
889
+ console.error(options.error);
890
+ printUsage();
891
+ return 1;
892
+ }
893
+ try {
894
+ const plan = JSON.parse(readFileSync7(options.value.planPath, "utf8"));
895
+ const result = applyAssetInstallPlan(plan);
896
+ console.log(`applied-actions: ${result.appliedActions.length}`);
897
+ return 0;
898
+ } catch (error) {
899
+ console.error(error instanceof Error ? error.message : String(error));
900
+ return 1;
901
+ }
902
+ }
903
+ function runAssetsCheck(args) {
904
+ const options = parseTargetJsonOptions(args);
905
+ if (!options.ok) {
906
+ console.error(options.error);
907
+ printUsage();
908
+ return 1;
909
+ }
910
+ const loaded = loadAgentAssetRegistry();
911
+ const result = checkInstalledAssets({
912
+ targetDir: options.value.targetDir,
913
+ agentAssetsDir: loaded.agentAssetsDir,
914
+ registry: loaded.registry
915
+ });
916
+ if (options.value.json) {
917
+ console.log(JSON.stringify(result, null, 2));
918
+ } else if (result.issues.length === 0) {
919
+ console.log("assets check passed");
920
+ } else {
921
+ for (const issue of result.issues) {
922
+ console.log(`${issue.type}: ${issue.message}`);
923
+ }
924
+ }
925
+ return result.issues.length === 0 ? 0 : 1;
926
+ }
927
+ function runAssetsList(args) {
928
+ const options = parseListOptions(args);
929
+ if (!options.ok) {
930
+ console.error(options.error);
931
+ printUsage();
51
932
  return 1;
52
933
  }
934
+ if (options.value.registryMode) {
935
+ return listRegistryAssets(options.value);
936
+ }
53
937
  for (const asset of listAssets()) {
54
938
  console.log(asset.path);
55
939
  }
56
940
  return 0;
57
941
  }
942
+ function runAssetsRecommend(args) {
943
+ const options = parseTargetJsonOptions(args);
944
+ if (!options.ok) {
945
+ console.error(options.error);
946
+ printUsage();
947
+ return 1;
948
+ }
949
+ const recommendations = recommendBundlesForTarget(options.value.targetDir);
950
+ if (options.value.json) {
951
+ console.log(JSON.stringify({ targetDir: options.value.targetDir, recommendations }, null, 2));
952
+ } else {
953
+ for (const recommendation of recommendations) {
954
+ console.log(
955
+ `${recommendation.bundleId} ${recommendation.confidence} ${recommendation.reasons.join("; ")}`
956
+ );
957
+ }
958
+ }
959
+ return 0;
960
+ }
961
+ function runAssetsDiscover(args) {
962
+ const options = parseTargetJsonOptions(args);
963
+ if (!options.ok) {
964
+ console.error(options.error);
965
+ printUsage();
966
+ return 1;
967
+ }
968
+ const signals = discoverTargetSignals(options.value.targetDir);
969
+ if (options.value.json) {
970
+ console.log(JSON.stringify(signals, null, 2));
971
+ } else {
972
+ console.log(`target: ${signals.targetDir}`);
973
+ console.log(`package-json: ${signals.hasPackageJson ? "yes" : "no"}`);
974
+ console.log(`agent-entry: ${signals.hasAgentEntry ? "yes" : "no"}`);
975
+ console.log(`frontend: ${signals.frontendSignals.join(", ") || "none"}`);
976
+ console.log(`research: ${signals.researchSignals.join(", ") || "none"}`);
977
+ console.log(`writing: ${signals.writingSignals.join(", ") || "none"}`);
978
+ }
979
+ return 0;
980
+ }
981
+ function runAssetsPlan(args) {
982
+ const options = parsePlanOptions(args);
983
+ if (!options.ok) {
984
+ console.error(options.error);
985
+ printUsage();
986
+ return 1;
987
+ }
988
+ const loaded = loadAgentAssetRegistry();
989
+ if (loaded.issues.length > 0) {
990
+ for (const issue of loaded.issues) {
991
+ console.error(`${issue.type}: ${issue.message}`);
992
+ }
993
+ return 1;
994
+ }
995
+ try {
996
+ const plan = createAssetInstallPlan({
997
+ targetDir: options.value.targetDir,
998
+ agentAssetsDir: loaded.agentAssetsDir,
999
+ registry: loaded.registry,
1000
+ bundles: loadAgentAssetBundles(loaded.agentAssetsDir),
1001
+ bundleIds: options.value.bundleIds,
1002
+ host: options.value.host
1003
+ });
1004
+ if (options.value.json) {
1005
+ console.log(JSON.stringify(plan, null, 2));
1006
+ } else {
1007
+ console.log(`target: ${plan.targetDir}`);
1008
+ console.log(`host: ${plan.host}`);
1009
+ console.log(`bundles: ${plan.bundleIds.join(", ")}`);
1010
+ console.log(`assets: ${plan.assetIds.length}`);
1011
+ console.log(`actions: ${plan.actions.length}`);
1012
+ console.log("dry-run: true");
1013
+ }
1014
+ if (options.value.outPath) {
1015
+ mkdirSync3(dirname5(options.value.outPath), { recursive: true });
1016
+ writeFileSync2(options.value.outPath, `${JSON.stringify(plan, null, 2)}
1017
+ `);
1018
+ if (!options.value.json) {
1019
+ console.log(`plan: ${options.value.outPath}`);
1020
+ }
1021
+ }
1022
+ return 0;
1023
+ } catch (error) {
1024
+ console.error(error instanceof Error ? error.message : String(error));
1025
+ return 1;
1026
+ }
1027
+ }
1028
+ function parseListOptions(args) {
1029
+ const options = {
1030
+ json: false,
1031
+ registryMode: false,
1032
+ visibility: "all"
1033
+ };
1034
+ for (let index = 0; index < args.length; index += 1) {
1035
+ const arg = args[index];
1036
+ if (arg === "--json") {
1037
+ options.json = true;
1038
+ options.registryMode = true;
1039
+ } else if (arg === "--visibility") {
1040
+ const visibility = args[index + 1];
1041
+ if (!isVisibilityFilter(visibility)) {
1042
+ return { ok: false, error: "Expected --visibility public|private|third-party|all" };
1043
+ }
1044
+ options.visibility = visibility;
1045
+ options.registryMode = true;
1046
+ index += 1;
1047
+ } else if (arg === "--registry") {
1048
+ options.registryMode = true;
1049
+ } else {
1050
+ return { ok: false, error: `Unknown assets list option: ${arg}` };
1051
+ }
1052
+ }
1053
+ return { ok: true, value: options };
1054
+ }
1055
+ function parseTargetJsonOptions(args) {
1056
+ const options = {
1057
+ targetDir: process.cwd(),
1058
+ json: false
1059
+ };
1060
+ for (let index = 0; index < args.length; index += 1) {
1061
+ const arg = args[index];
1062
+ if (arg === "--target") {
1063
+ const targetDir = args[index + 1];
1064
+ if (!targetDir) return { ok: false, error: "Expected --target <path>" };
1065
+ options.targetDir = targetDir;
1066
+ index += 1;
1067
+ } else if (arg === "--json") {
1068
+ options.json = true;
1069
+ } else {
1070
+ return { ok: false, error: `Unknown assets target option: ${arg}` };
1071
+ }
1072
+ }
1073
+ return { ok: true, value: options };
1074
+ }
1075
+ function parsePlanOptions(args) {
1076
+ const options = {
1077
+ targetDir: process.cwd(),
1078
+ json: false,
1079
+ bundleIds: [],
1080
+ host: "codex"
1081
+ };
1082
+ for (let index = 0; index < args.length; index += 1) {
1083
+ const arg = args[index];
1084
+ if (arg === "--target") {
1085
+ const targetDir = args[index + 1];
1086
+ if (!targetDir) return { ok: false, error: "Expected --target <path>" };
1087
+ options.targetDir = targetDir;
1088
+ index += 1;
1089
+ } else if (arg === "--bundle") {
1090
+ const bundleId = args[index + 1];
1091
+ if (!bundleId) return { ok: false, error: "Expected --bundle <bundle-id>" };
1092
+ options.bundleIds.push(bundleId);
1093
+ index += 1;
1094
+ } else if (arg === "--host") {
1095
+ const host = args[index + 1];
1096
+ if (!isHost(host)) {
1097
+ return {
1098
+ ok: false,
1099
+ error: "Expected --host codex|claude-code|gemini-cli|antigravity"
1100
+ };
1101
+ }
1102
+ options.host = host;
1103
+ index += 1;
1104
+ } else if (arg === "--out") {
1105
+ const outPath = args[index + 1];
1106
+ if (!outPath) return { ok: false, error: "Expected --out <path>" };
1107
+ options.outPath = outPath;
1108
+ index += 1;
1109
+ } else if (arg === "--json") {
1110
+ options.json = true;
1111
+ } else {
1112
+ return { ok: false, error: `Unknown assets plan option: ${arg}` };
1113
+ }
1114
+ }
1115
+ if (options.bundleIds.length === 0) {
1116
+ return { ok: false, error: "Expected at least one --bundle <bundle-id>" };
1117
+ }
1118
+ return { ok: true, value: options };
1119
+ }
1120
+ function parseApplyOptions(args) {
1121
+ let planPath = "";
1122
+ for (let index = 0; index < args.length; index += 1) {
1123
+ const arg = args[index];
1124
+ if (arg === "--plan") {
1125
+ const value = args[index + 1];
1126
+ if (!value) return { ok: false, error: "Expected --plan <path>" };
1127
+ planPath = value;
1128
+ index += 1;
1129
+ } else {
1130
+ return { ok: false, error: `Unknown assets apply option: ${arg}` };
1131
+ }
1132
+ }
1133
+ if (!planPath) return { ok: false, error: "Expected --plan <path>" };
1134
+ return { ok: true, value: { planPath } };
1135
+ }
1136
+ function parseNpxOptions(operation, args) {
1137
+ const options = {};
1138
+ const positional = [];
1139
+ let hasPlan = false;
1140
+ for (let index = 0; index < args.length; index += 1) {
1141
+ const arg = args[index];
1142
+ if (arg === "--plan") {
1143
+ hasPlan = true;
1144
+ } else if (arg === "--root") {
1145
+ const npxRoot = args[index + 1];
1146
+ if (!npxRoot) return { ok: false, error: "Expected --root <path>" };
1147
+ options.npxRoot = npxRoot;
1148
+ index += 1;
1149
+ } else if (arg === "--skill") {
1150
+ const skill = args[index + 1];
1151
+ if (!skill) return { ok: false, error: "Expected --skill <name>" };
1152
+ options.skill = skill;
1153
+ index += 1;
1154
+ } else if (arg.startsWith("-")) {
1155
+ return { ok: false, error: `Unknown assets npx option: ${arg}` };
1156
+ } else {
1157
+ positional.push(arg);
1158
+ }
1159
+ }
1160
+ if (!hasPlan) return { ok: false, error: "Expected --plan. Direct npx writes are not supported." };
1161
+ if (operation === "add") {
1162
+ const [source] = positional;
1163
+ if (!source) return { ok: false, error: "Expected pro-gov assets npx add <source> --plan" };
1164
+ options.source = source;
1165
+ } else if (positional.length > 0) {
1166
+ return { ok: false, error: `Unexpected assets npx update argument: ${positional[0]}` };
1167
+ }
1168
+ return { ok: true, value: options };
1169
+ }
1170
+ function listRegistryAssets(options) {
1171
+ const loaded = loadAgentAssetRegistry();
1172
+ if (loaded.issues.length > 0) {
1173
+ for (const issue of loaded.issues) {
1174
+ console.error(`${issue.type}: ${issue.message}`);
1175
+ }
1176
+ return 1;
1177
+ }
1178
+ const assets = filterAssetsByVisibility(loaded.registry.assets, options.visibility);
1179
+ if (options.json) {
1180
+ console.log(
1181
+ JSON.stringify(
1182
+ {
1183
+ count: assets.length,
1184
+ registryPath: loaded.registryPath,
1185
+ assets
1186
+ },
1187
+ null,
1188
+ 2
1189
+ )
1190
+ );
1191
+ } else {
1192
+ for (const asset of assets) {
1193
+ console.log(asset.id);
1194
+ }
1195
+ }
1196
+ return 0;
1197
+ }
1198
+ function filterAssetsByVisibility(assets, visibility) {
1199
+ if (visibility === "all") return [...assets];
1200
+ return assets.filter((asset) => asset.visibility === visibility);
1201
+ }
1202
+ function isVisibilityFilter(value) {
1203
+ return value === "public" || value === "private" || value === "third-party" || value === "all";
1204
+ }
1205
+ function isHost(value) {
1206
+ return value === "codex" || value === "claude-code" || value === "gemini-cli" || value === "antigravity";
1207
+ }
1208
+ function printUsage() {
1209
+ console.error("Usage:");
1210
+ console.error(" pro-gov assets list [--registry] [--json] [--visibility public|private|third-party|all]");
1211
+ console.error(" pro-gov assets discover [--target <path>] [--json]");
1212
+ console.error(" pro-gov assets recommend [--target <path>] [--json]");
1213
+ console.error(" pro-gov assets plan --bundle <bundle-id> [--bundle <bundle-id>] [--target <path>] [--host codex|claude-code|gemini-cli|antigravity] [--out <path>] [--json]");
1214
+ console.error(" pro-gov assets apply --plan <path>");
1215
+ console.error(" pro-gov assets check [--target <path>] [--json]");
1216
+ console.error(" pro-gov assets npx add <source> [--skill <name>] --plan [--root <path>]");
1217
+ console.error(" pro-gov assets npx update [--skill <name>] --plan [--root <path>]");
1218
+ }
1219
+ function printNpxUsage() {
1220
+ console.log("Usage: pro-gov assets npx add <source> [--skill <name>] --plan [--root <path>]");
1221
+ console.log("Usage: pro-gov assets npx update [--skill <name>] --plan [--root <path>]");
1222
+ console.log("");
1223
+ console.log("Runs npx skills only in a temporary copy and prints a reviewable plan.");
1224
+ }
58
1225
 
59
1226
  // src/commands/doctor.ts
60
- import { spawnSync } from "node:child_process";
61
- import { existsSync as existsSync2 } from "node:fs";
1227
+ import { spawnSync as spawnSync2 } from "node:child_process";
1228
+ import { existsSync as existsSync10 } from "node:fs";
62
1229
  import { createRequire } from "node:module";
63
- import { dirname as dirname2, join as join2 } from "node:path";
1230
+ import { dirname as dirname6, join as join10 } from "node:path";
64
1231
  var REQUIRED_ASSETS = [
65
1232
  "starter/AGENTS.template.md",
1233
+ "starter/.gemini/settings.json",
66
1234
  "starter/docs/governance/ssot-v0.9.md",
67
1235
  "starter/docs/governance/agents-routing/engineering-runtime-v0.9.md",
68
1236
  "starter/docs/governance/agents-routing/doc-only-v0.9.md",
@@ -86,7 +1254,7 @@ function runDoctor(_args) {
86
1254
  return missing.length > 0 ? 1 : 0;
87
1255
  }
88
1256
  function checkDocGov() {
89
- const fromPath = spawnSync("doc-gov", ["--help"], {
1257
+ const fromPath = spawnSync2("doc-gov", ["--help"], {
90
1258
  encoding: "utf8",
91
1259
  stdio: "ignore"
92
1260
  });
@@ -97,7 +1265,7 @@ function checkDocGov() {
97
1265
  if (!dependencyCli) {
98
1266
  return "doc-gov: not found; install @pieai/doc-gov beside @pieai/pro-gov for validation.";
99
1267
  }
100
- const fromDependency = spawnSync(process.execPath, [dependencyCli, "--help"], {
1268
+ const fromDependency = spawnSync2(process.execPath, [dependencyCli, "--help"], {
101
1269
  encoding: "utf8",
102
1270
  stdio: "ignore"
103
1271
  });
@@ -110,8 +1278,8 @@ function resolveDocGovDependencyCli() {
110
1278
  try {
111
1279
  const require2 = createRequire(import.meta.url);
112
1280
  const packageJsonPath = require2.resolve("@pieai/doc-gov/package.json");
113
- const cliPath = join2(dirname2(packageJsonPath), "dist/cli.js");
114
- return existsSync2(cliPath) ? cliPath : null;
1281
+ const cliPath = join10(dirname6(packageJsonPath), "dist/cli.js");
1282
+ return existsSync10(cliPath) ? cliPath : null;
115
1283
  } catch {
116
1284
  return null;
117
1285
  }
@@ -176,9 +1344,256 @@ function readFlag(args, flag) {
176
1344
  return value;
177
1345
  }
178
1346
 
1347
+ // src/commands/lens.ts
1348
+ import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync3 } from "node:fs";
1349
+ import { dirname as dirname7 } from "node:path";
1350
+
1351
+ // src/lens/report.ts
1352
+ function formatProjectLensInspection(report) {
1353
+ return [
1354
+ `target: ${report.targetDir}`,
1355
+ `ai-entry-files: ${formatList(report.aiEntryFiles)}`,
1356
+ `ai-config-files: ${formatList(report.aiConfigFiles)}`,
1357
+ `package-scripts: ${formatList(report.packageJson?.scripts ?? [])}`,
1358
+ `dependencies: ${formatList(report.packageJson?.dependencies ?? [])}`,
1359
+ `dev-dependencies: ${formatList(report.packageJson?.devDependencies ?? [])}`,
1360
+ `docs-directory: ${report.docs.hasDocsDirectory ? "yes" : "no"}`,
1361
+ `markdown-files: ${report.docs.markdownFileCount}`,
1362
+ `governance-files: ${formatList(report.docs.governanceFiles)}`,
1363
+ `git: ${report.git.available ? "available" : "unavailable"}`,
1364
+ `git-branch: ${report.git.branch ?? "unknown"}`,
1365
+ `git-head: ${report.git.head ?? "unknown"}`,
1366
+ `large-files: ${report.largeFiles.length}`
1367
+ ].join("\n");
1368
+ }
1369
+ function renderProjectLensMarkdownReport(report) {
1370
+ return `${[
1371
+ "# Project Lens Evidence Report",
1372
+ "",
1373
+ `- Target: \`${report.targetDir}\``,
1374
+ `- Generated: ${(/* @__PURE__ */ new Date()).toISOString()}`,
1375
+ "- Scope: local read-only evidence for AI-assisted project review",
1376
+ "",
1377
+ "## AI Entry Files",
1378
+ "",
1379
+ bulletList(report.aiEntryFiles),
1380
+ "",
1381
+ "## AI Config Adapters",
1382
+ "",
1383
+ bulletList(report.aiConfigFiles),
1384
+ "",
1385
+ "## Package",
1386
+ "",
1387
+ `- Scripts: ${formatList(report.packageJson?.scripts ?? [])}`,
1388
+ `- Dependencies: ${formatList(report.packageJson?.dependencies ?? [])}`,
1389
+ `- Dev dependencies: ${formatList(report.packageJson?.devDependencies ?? [])}`,
1390
+ "",
1391
+ "## Docs",
1392
+ "",
1393
+ `- Docs directory: ${report.docs.hasDocsDirectory ? "yes" : "no"}`,
1394
+ `- Markdown files: ${report.docs.markdownFileCount}`,
1395
+ `- Governance files: ${formatList(report.docs.governanceFiles)}`,
1396
+ "",
1397
+ "## Git",
1398
+ "",
1399
+ `- Available: ${report.git.available ? "yes" : "no"}`,
1400
+ `- Branch: ${report.git.branch ?? "unknown"}`,
1401
+ `- Head: ${report.git.head ?? "unknown"}`,
1402
+ "",
1403
+ "## Large Files",
1404
+ "",
1405
+ report.largeFiles.length === 0 ? "- none" : report.largeFiles.map((file) => `- \`${file.path}\` (${file.bytes} bytes)`).join("\n"),
1406
+ "",
1407
+ "## Review Notes",
1408
+ "",
1409
+ "- This report is evidence only; it does not replace human or AI judgement.",
1410
+ "- Use the ProjectLens skills for interpretation, tradeoff analysis, and recommendations."
1411
+ ].join("\n")}
1412
+ `;
1413
+ }
1414
+ function formatList(values) {
1415
+ return values.length === 0 ? "none" : values.join(", ");
1416
+ }
1417
+ function bulletList(values) {
1418
+ if (values.length === 0) return "- none";
1419
+ return values.map((value) => `- \`${value}\``).join("\n");
1420
+ }
1421
+
1422
+ // src/lens/scan.ts
1423
+ import { spawnSync as spawnSync3 } from "node:child_process";
1424
+ import { existsSync as existsSync11, readdirSync as readdirSync6, readFileSync as readFileSync8, statSync as statSync3 } from "node:fs";
1425
+ import { join as join11, relative as relative4 } from "node:path";
1426
+ var ignoredDirectories = /* @__PURE__ */ new Set([
1427
+ ".git",
1428
+ ".next",
1429
+ ".turbo",
1430
+ "dist",
1431
+ "node_modules",
1432
+ "coverage"
1433
+ ]);
1434
+ function scanProjectLensTarget(targetDir, options = {}) {
1435
+ const largeFileBytes = options.largeFileBytes ?? 5e4;
1436
+ const files = listProjectFiles(targetDir);
1437
+ const markdownFiles = files.filter((file) => file.endsWith(".md"));
1438
+ const packageJson = readPackageJson(targetDir);
1439
+ return {
1440
+ targetDir,
1441
+ aiEntryFiles: ["AGENTS.md", "CLAUDE.md", "GEMINI.md"].filter(
1442
+ (file) => existsSync11(join11(targetDir, file))
1443
+ ),
1444
+ aiConfigFiles: [".gemini/settings.json"].filter((file) => existsSync11(join11(targetDir, file))),
1445
+ packageJson,
1446
+ docs: {
1447
+ hasDocsDirectory: existsSync11(join11(targetDir, "docs")),
1448
+ markdownFileCount: markdownFiles.length,
1449
+ governanceFiles: markdownFiles.filter((file) => file.startsWith("docs/governance/") || file.startsWith("docs/policy/")).sort()
1450
+ },
1451
+ git: readGitState(targetDir),
1452
+ largeFiles: files.map((file) => ({ path: file, bytes: statSync3(join11(targetDir, file)).size })).filter((file) => file.bytes >= largeFileBytes).sort((a, b) => b.bytes - a.bytes || a.path.localeCompare(b.path)).slice(0, 25)
1453
+ };
1454
+ }
1455
+ function readPackageJson(targetDir) {
1456
+ const packageJsonPath = join11(targetDir, "package.json");
1457
+ if (!existsSync11(packageJsonPath)) return void 0;
1458
+ try {
1459
+ const packageJson = JSON.parse(readFileSync8(packageJsonPath, "utf8"));
1460
+ return {
1461
+ scripts: Object.keys(packageJson.scripts ?? {}).sort(),
1462
+ dependencies: Object.keys(packageJson.dependencies ?? {}).sort(),
1463
+ devDependencies: Object.keys(packageJson.devDependencies ?? {}).sort()
1464
+ };
1465
+ } catch {
1466
+ return void 0;
1467
+ }
1468
+ }
1469
+ function readGitState(targetDir) {
1470
+ const branch = runGit(targetDir, ["rev-parse", "--abbrev-ref", "HEAD"]);
1471
+ if (!branch.ok) return { available: false };
1472
+ const head = runGit(targetDir, ["log", "-1", "--format=%H %s"]);
1473
+ const status = runGit(targetDir, ["status", "-sb"]);
1474
+ return {
1475
+ available: true,
1476
+ branch: branch.stdout,
1477
+ head: head.ok ? head.stdout : void 0,
1478
+ statusShort: status.ok ? status.stdout : void 0
1479
+ };
1480
+ }
1481
+ function runGit(targetDir, args) {
1482
+ const result = spawnSync3("git", ["-C", targetDir, ...args], {
1483
+ encoding: "utf8"
1484
+ });
1485
+ if (result.status !== 0) return { ok: false };
1486
+ return { ok: true, stdout: result.stdout.trim() };
1487
+ }
1488
+ function listProjectFiles(targetDir) {
1489
+ const files = [];
1490
+ collectFiles2(targetDir, targetDir, files);
1491
+ return files.sort();
1492
+ }
1493
+ function collectFiles2(rootDir, currentDir, files) {
1494
+ if (!existsSync11(currentDir)) return;
1495
+ for (const entry of readdirSync6(currentDir, { withFileTypes: true })) {
1496
+ if (entry.isDirectory()) {
1497
+ if (ignoredDirectories.has(entry.name)) continue;
1498
+ collectFiles2(rootDir, join11(currentDir, entry.name), files);
1499
+ } else if (entry.isFile()) {
1500
+ files.push(toUnixPath4(relative4(rootDir, join11(currentDir, entry.name))));
1501
+ }
1502
+ }
1503
+ }
1504
+ function toUnixPath4(path) {
1505
+ return path.replaceAll("\\", "/");
1506
+ }
1507
+
1508
+ // src/commands/lens.ts
1509
+ function runLens(args) {
1510
+ const [subcommand2, ...rest] = args;
1511
+ if (subcommand2 === "scan" || subcommand2 === "inspect") {
1512
+ return runLensInspect(rest, subcommand2);
1513
+ }
1514
+ if (subcommand2 === "report") {
1515
+ return runLensReport(rest);
1516
+ }
1517
+ printUsage2();
1518
+ return 1;
1519
+ }
1520
+ function runLensInspect(args, subcommand2) {
1521
+ const options = parseLensOptions(args, subcommand2);
1522
+ if (!options.ok) {
1523
+ console.error(options.error);
1524
+ printUsage2();
1525
+ return 1;
1526
+ }
1527
+ const report = scanProjectLensTarget(options.value.targetDir);
1528
+ if (options.value.json || options.value.format === "json") {
1529
+ console.log(JSON.stringify(report, null, 2));
1530
+ } else {
1531
+ console.log(formatProjectLensInspection(report));
1532
+ }
1533
+ return 0;
1534
+ }
1535
+ function runLensReport(args) {
1536
+ const options = parseLensOptions(args, "report");
1537
+ if (!options.ok) {
1538
+ console.error(options.error);
1539
+ printUsage2();
1540
+ return 1;
1541
+ }
1542
+ if (!options.value.outPath) {
1543
+ console.error("Expected --out <path>");
1544
+ printUsage2();
1545
+ return 1;
1546
+ }
1547
+ const report = scanProjectLensTarget(options.value.targetDir);
1548
+ const markdown = renderProjectLensMarkdownReport(report);
1549
+ mkdirSync4(dirname7(options.value.outPath), { recursive: true });
1550
+ writeFileSync3(options.value.outPath, markdown);
1551
+ console.log(`report: ${options.value.outPath}`);
1552
+ return 0;
1553
+ }
1554
+ function parseLensOptions(args, subcommand2) {
1555
+ const options = {
1556
+ targetDir: process.cwd(),
1557
+ json: false,
1558
+ format: "text"
1559
+ };
1560
+ for (let index = 0; index < args.length; index += 1) {
1561
+ const arg = args[index];
1562
+ if (arg === "--target") {
1563
+ const targetDir = args[index + 1];
1564
+ if (!targetDir) return { ok: false, error: "Expected --target <path>" };
1565
+ options.targetDir = targetDir;
1566
+ index += 1;
1567
+ } else if (arg === "--json") {
1568
+ options.json = true;
1569
+ options.format = "json";
1570
+ } else if (arg === "--format") {
1571
+ const format = args[index + 1];
1572
+ if (format !== "text" && format !== "json") {
1573
+ return { ok: false, error: "Expected --format text|json" };
1574
+ }
1575
+ options.format = format;
1576
+ index += 1;
1577
+ } else if (arg === "--out") {
1578
+ const outPath = args[index + 1];
1579
+ if (!outPath) return { ok: false, error: "Expected --out <path>" };
1580
+ options.outPath = outPath;
1581
+ index += 1;
1582
+ } else {
1583
+ return { ok: false, error: `Unknown lens ${subcommand2} option: ${arg}` };
1584
+ }
1585
+ }
1586
+ return { ok: true, value: options };
1587
+ }
1588
+ function printUsage2() {
1589
+ console.error("Usage: pro-gov lens scan [--target <path>] [--json]");
1590
+ console.error("Usage: pro-gov lens inspect [--target <path>] [--format text|json]");
1591
+ console.error("Usage: pro-gov lens report --target <path> --out <path>");
1592
+ }
1593
+
179
1594
  // src/commands/sync.ts
180
- import { existsSync as existsSync3, readFileSync } from "node:fs";
181
- import { join as join3 } from "node:path";
1595
+ import { existsSync as existsSync12, readFileSync as readFileSync9 } from "node:fs";
1596
+ import { join as join12 } from "node:path";
182
1597
  function runSync(args) {
183
1598
  if (!args.includes("--check")) {
184
1599
  console.error("pro-gov sync requires --check in this first read-only release.");
@@ -187,14 +1602,14 @@ function runSync(args) {
187
1602
  let differences = 0;
188
1603
  console.log("pro-gov sync check");
189
1604
  for (const file of planStarterFiles()) {
190
- const targetPath = join3(process.cwd(), file.targetPath);
191
- if (!existsSync3(targetPath)) {
1605
+ const targetPath = join12(process.cwd(), file.targetPath);
1606
+ if (!existsSync12(targetPath)) {
192
1607
  console.log(`missing: ${file.targetPath}`);
193
1608
  differences += 1;
194
1609
  continue;
195
1610
  }
196
- const source = readFileSync(file.absoluteSourcePath, "utf8");
197
- const target = readFileSync(targetPath, "utf8");
1611
+ const source = readFileSync9(file.absoluteSourcePath, "utf8");
1612
+ const target = readFileSync9(targetPath, "utf8");
198
1613
  if (source !== target) {
199
1614
  console.log(`different: ${file.targetPath}`);
200
1615
  differences += 1;
@@ -210,7 +1625,16 @@ function runSync(args) {
210
1625
 
211
1626
  // src/cli.ts
212
1627
  var COMMANDS = [
213
- "assets list",
1628
+ "assets list [--json] [--visibility public|private|third-party|all]",
1629
+ "assets discover [--target <path>] [--json]",
1630
+ "assets recommend [--target <path>] [--json]",
1631
+ "assets plan --bundle <bundle-id> [--target <path>] [--json]",
1632
+ "assets apply --plan <path>",
1633
+ "assets check [--target <path>] [--json]",
1634
+ "assets npx add|update ... --plan",
1635
+ "lens scan [--target <path>] [--json]",
1636
+ "lens inspect [--target <path>] [--format text|json]",
1637
+ "lens report --target <path> --out <path>",
214
1638
  "init --profile <engineering-runtime|doc-only> --dry-run",
215
1639
  "sync --check",
216
1640
  "doctor"
@@ -219,8 +1643,10 @@ var [command, subcommand] = process.argv.slice(2);
219
1643
  if (!command || command === "--help" || command === "-h") {
220
1644
  printHelp();
221
1645
  process.exitCode = command ? 0 : 1;
222
- } else if (command === "assets" && subcommand === "list") {
1646
+ } else if (command === "assets") {
223
1647
  process.exitCode = runAssets(process.argv.slice(3));
1648
+ } else if (command === "lens") {
1649
+ process.exitCode = runLens(process.argv.slice(3));
224
1650
  } else if (command === "init") {
225
1651
  process.exitCode = runInit(process.argv.slice(3));
226
1652
  } else if (command === "sync") {