skillspub 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js ADDED
@@ -0,0 +1,1562 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs } from 'node:util';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { defaultHome } from "./core.js";
7
+ import { ExplainError, explainVisibility, } from "./explain.js";
8
+ import { inspectHarness, inspectHarnesses, planHarnessOperation, } from "./harnesses/registry.js";
9
+ import { applyTargetMigration, loadTargets, planTargetMigration, applyDoctorRepairs, doctorGlobalInventory, doctorProjectInventory, scanGlobalInventory, scanProjectInventory, readStateFile, } from "./inventory.js";
10
+ import { filterRows, projectRows, readViewState, untagged, viewTargets, } from "./view.js";
11
+ import { sharedAdd, sharedDescribe, sharedFind, planSharedAdd, planSharedRemove, planSharedUpdate, sharedRemove, sharedRemoveCascade, sharedRefresh, sharedOutdated, sharedUpdate, } from "./shared.js";
12
+ import { sourceMirrorState, verifySourceMutation, } from "./source-verification.js";
13
+ import { addBundleMembers, addPresetSelectors, addResourceTags, applyCatalogMutation, createBundle, createPreset, expandSelector, listBundles, listPresets, listTags, readState, removeBundleMembers, removePresetSelectors, removeResourceTags, showBundle, planCatalogMutation, showPreset, tagsForResource, } from "./catalog.js";
14
+ import { activatePreset, applyActivationPlan, applyPresetReconcile, deactivatePreset, deletePreset, planActivation, planMirrorAction, planPresetReconcile, remainingDrift, } from "./reconcile.js";
15
+ const USAGE = `SkillsPub — multi-agent skills on/off manager (disk is the source of truth)
16
+
17
+ skillspub ls [--target T] [--tag T] skill × Target matrix (+ untagged/deadlink hints)
18
+ skillspub on|off <selector> <target...> [--yes] update Skill Target relationships
19
+ skillspub status <skill> per-Target state of one skill
20
+ skillspub explain <selector> [--harness H] [--want visible|hidden]
21
+ skillspub mirror sync|overwrite|remove|convert <target-id> <slot> [--yes]
22
+ skillspub bundle ls|show|create|add|rm ...
23
+ skillspub tag add|rm|ls ... manage global resource Tags
24
+ skillspub preset create|add|rm|ls|show|activate|deactivate|reconcile|delete ...
25
+ skillspub shared find|describe|refresh|outdated|add|update|remove ... manage the Shared Target via skills@1.5.21
26
+ skillspub scan explicitly scan Global Skill Target inventory
27
+ skillspub doctor [--repair --yes] diagnose; explicitly confirm safe repairs
28
+ skillspub project <path> scan|doctor|explain|shared|preset|mirror|harnesses ... operate on exact Project Skill Targets
29
+ skillspub targets list resolved Skill Targets
30
+ skillspub harnesses [name inspect|setup|reconcile [--yes]] inspect or configure a Harness
31
+ skillspub project <path> harnesses pi migrate [--yes] migrate a stale Project Pi Target
32
+ skillspub migrate targets [--yes] preview or migrate runtimes.json to targets.json
33
+ skillspub tui [--project [path]] interactive full-screen skill browser
34
+ (--project: project-scope view, cwd when path omitted)
35
+ `;
36
+ export function shouldRunTui(command, stdinIsTty, stdoutIsTty) {
37
+ return command === 'tui' ||
38
+ (command === undefined && stdinIsTty && stdoutIsTty);
39
+ }
40
+ const CELL = { on: 'on', off: 'off', deadlink: '!' };
41
+ class CliError extends Error {
42
+ code;
43
+ exitCode;
44
+ details;
45
+ constructor(code, message, exitCode, details) {
46
+ super(message);
47
+ this.code = code;
48
+ this.exitCode = exitCode;
49
+ this.details = details;
50
+ }
51
+ }
52
+ function writeJson(document) {
53
+ process.stdout.write(`${JSON.stringify(document)}\n`);
54
+ }
55
+ function errorInfo(error, mutation = false) {
56
+ if (error instanceof CliError)
57
+ return { code: error.code, exitCode: error.exitCode, details: error.details ?? {} };
58
+ const err = error;
59
+ if (err.code?.startsWith('ERR_PARSE_ARGS') || err.message.startsWith('usage:'))
60
+ return { code: 'usage_error', exitCode: 2, details: {} };
61
+ if (err.code && ['preflight_error', 'concurrent_modification', 'io_error', 'apply_failed']
62
+ .includes(err.code))
63
+ return { code: err.code, exitCode: 1, details: err.details ?? {} };
64
+ if (err.code && /^(EACCES|EEXIST|EIO|ENOENT|ENOSPC|EPERM|EROFS)$/.test(err.code))
65
+ return { code: 'io_error', exitCode: 1, details: { errno: err.code } };
66
+ if (mutation)
67
+ return { code: 'preflight_error', exitCode: 1, details: {} };
68
+ return { code: 'runtime_error', exitCode: 1, details: {} };
69
+ }
70
+ const MUTATING_COMMANDS = new Set(['on', 'off', 'mirror', 'migrate']);
71
+ const MUTATING_ACTIONS = {
72
+ harnesses: new Set(['setup', 'reconcile', 'migrate']),
73
+ bundle: new Set(['create', 'add', 'rm']),
74
+ tag: new Set(['add', 'rm']),
75
+ preset: new Set(['create', 'add', 'rm', 'activate', 'deactivate', 'reconcile', 'delete']),
76
+ shared: new Set(['add', 'update', 'remove']),
77
+ };
78
+ function isMutationCommand(command, args) {
79
+ if (command === 'project') {
80
+ const [, projectCommand, ...projectArgs] = args;
81
+ return isMutationCommand(projectCommand, projectArgs);
82
+ }
83
+ if (command === 'doctor')
84
+ return args.includes('--repair');
85
+ if (command && MUTATING_COMMANDS.has(command))
86
+ return true;
87
+ const actions = MUTATING_ACTIONS[command ?? ''];
88
+ return actions?.has(args[command === 'harnesses' ? 1 : 0] ?? '') ?? false;
89
+ }
90
+ function printExplanation(explanation) {
91
+ const scope = explanation.scope.projectPath
92
+ ? `Project ${explanation.scope.projectPath}`
93
+ : 'Global';
94
+ console.log(`${explanation.resource.name} (${explanation.resource.realPath})`);
95
+ console.log(`Scope: ${scope}`);
96
+ for (const harness of explanation.harnesses) {
97
+ console.log(`\n${harness.name}: ${harness.effectiveVisibility}${harness.detected ? '' : ' (not detected)'}`);
98
+ console.log(` support: ${harness.support}`);
99
+ console.log(` Shared: ${harness.sharedConsumption.status} — ${harness.sharedConsumption.detail}`);
100
+ console.log(` isolation: ${harness.isolation.status} — ${harness.isolation.detail}`);
101
+ for (const evidence of harness.evidence)
102
+ console.log(` evidence: ${evidence.verifiedVersion} ${evidence.url} — ${evidence.detail}`);
103
+ for (const reason of harness.reasons)
104
+ console.log(` reason: ${reason.message}`);
105
+ for (const warning of harness.warnings)
106
+ console.log(` warning: ${warning.message}`);
107
+ for (const conflict of harness.conflicts)
108
+ console.log(` conflict: ${conflict.message}`);
109
+ for (const root of harness.roots) {
110
+ console.log(` ${root.consumption}: ${root.path} — ${root.reason}`);
111
+ for (const relationship of root.relationships)
112
+ console.log(` ${relationship.activation} ${relationship.form} ${relationship.path}${relationship.selected ? ' (selected)' : ''}`);
113
+ }
114
+ if (harness.plan) {
115
+ console.log(` wanted: ${explanation.wanted}; executable: ${harness.plan.executable}`);
116
+ for (const step of harness.plan.steps)
117
+ console.log(` step: ${step.operation} ${step.targetId}/${step.slot}`);
118
+ for (const blocker of harness.plan.blockers)
119
+ console.log(` blocker: ${blocker.message}`);
120
+ }
121
+ }
122
+ }
123
+ function cmdExplain(home, args, projectPath, json = false) {
124
+ const parsed = parseArgs({
125
+ args,
126
+ allowPositionals: true,
127
+ options: {
128
+ harness: { type: 'string' },
129
+ want: { type: 'string' },
130
+ },
131
+ });
132
+ if (parsed.positionals.length !== 1)
133
+ throw new Error('usage: skillspub explain <selector> [--harness H] [--want visible|hidden]');
134
+ if (parsed.values.want !== undefined &&
135
+ parsed.values.want !== 'visible' && parsed.values.want !== 'hidden')
136
+ throw new Error('usage: --want must be visible or hidden');
137
+ try {
138
+ const result = explainVisibility(home, parsed.positionals[0], {
139
+ projectPath,
140
+ harness: parsed.values.harness,
141
+ want: parsed.values.want,
142
+ });
143
+ if (!json)
144
+ printExplanation(result);
145
+ return result;
146
+ }
147
+ catch (error) {
148
+ if (!(error instanceof ExplainError))
149
+ throw error;
150
+ throw new CliError(error.code, error.message, error.code === 'unknown_harness' ? 2 : 1, error.details);
151
+ }
152
+ }
153
+ function pad(s, n) {
154
+ return s + ' '.repeat(Math.max(0, n - s.length));
155
+ }
156
+ function relationships(row, target) {
157
+ return row.relationships.filter((relationship) => relationship.target === target);
158
+ }
159
+ function matrixCell(row, target) {
160
+ const states = [...new Set(relationships(row, target).map(({ info }) => CELL[info.presence]))];
161
+ return states.join('/') || '·';
162
+ }
163
+ function printMatrix(rows, targetNames) {
164
+ const w = Math.max(5, ...rows.map((r) => r.displayName.length)) + 2;
165
+ console.log(pad('skill', w) + targetNames.map((a) => pad(a, 9)).join(''));
166
+ for (const r of rows) {
167
+ console.log(pad(r.displayName, w) +
168
+ targetNames.map((target) => pad(matrixCell(r, target), 9)).join(''));
169
+ }
170
+ }
171
+ function matchingInstances(rows, selector) {
172
+ if (selector.startsWith('skill:')) {
173
+ const resourceId = selector.slice('skill:'.length);
174
+ return rows.filter((row) => row.realPath === resourceId);
175
+ }
176
+ return rows.filter((row) => row.relationships.some((relationship) => relationship.name === selector));
177
+ }
178
+ function variantLocation(row) {
179
+ return row.realPath
180
+ ?? `${row.relationships[0].info.path} -> ${row.relationships[0].info.target ?? '?'}`;
181
+ }
182
+ function refuseAmbiguousName(rows, name) {
183
+ const matches = matchingInstances(rows, name);
184
+ if (matches.length < 2)
185
+ return;
186
+ const details = {
187
+ name,
188
+ matches: matches.map((row) => ({
189
+ name: row.displayName,
190
+ selector: `skill:${row.realPath ?? variantLocation(row)}`,
191
+ location: variantLocation(row),
192
+ })),
193
+ };
194
+ throw new CliError('ambiguous_selector', `skill name "${name}" is ambiguous:\n${matches
195
+ .map((row) => ` - ${row.displayName}: ${variantLocation(row)}`)
196
+ .join('\n')}\nUse skillspub tui to select a specific variant.`, 1, details);
197
+ }
198
+ function cmdLs(home, args, json = false) {
199
+ const { values } = parseArgs({
200
+ args,
201
+ options: { target: { type: 'string' }, agent: { type: 'string' }, tag: { type: 'string' } },
202
+ });
203
+ if (values.target && values.agent)
204
+ throw new Error('usage: skillspub ls [--target T] [--tag T]');
205
+ const warnings = values.agent ? ['--agent is deprecated; use --target'] : [];
206
+ if (!json && warnings.length > 0)
207
+ console.error(`warning: ${warnings[0]}`);
208
+ const target = values.target ?? values.agent;
209
+ const report = scanGlobalInventory(home, undefined, { persist: false });
210
+ const targets = viewTargets(report);
211
+ if (target && !targets.some((candidate) => candidate.name === target))
212
+ throw new CliError('unknown_target', `unknown target: ${target}`, 1, { target });
213
+ const { tags } = readViewState(home);
214
+ let rows = filterRows(projectRows(report), { target, tag: values.tag }, tags);
215
+ if (values.tag && !target) {
216
+ try {
217
+ const selected = new Set(expandSelector(home, `tag:${values.tag}`, report).resourceIds);
218
+ rows = rows.filter((row) => row.realPath && selected.has(row.realPath));
219
+ }
220
+ catch (error) {
221
+ if (error.message !== `unknown Tag: ${values.tag}`)
222
+ throw error;
223
+ rows = [];
224
+ }
225
+ }
226
+ const cols = target ? [target] : targets.map((candidate) => candidate.name);
227
+ const deadlinks = rows.flatMap((row) => row.relationships
228
+ .filter(({ info }) => info.presence === 'deadlink')
229
+ .map(({ target, name, info }) => `${target}/${name} -> ${info.target ?? '?'}`));
230
+ const untaggedRows = untagged(rows, tags);
231
+ const data = {
232
+ targets: cols,
233
+ rows: rows.map((row) => ({
234
+ name: row.displayName,
235
+ resourceId: row.realPath ?? row.relationships[0].info.path,
236
+ relationships: row.relationships.map(({ target, name, info }) => ({
237
+ target,
238
+ slot: name,
239
+ presence: info.presence,
240
+ form: info.form,
241
+ })),
242
+ })),
243
+ deadlinks,
244
+ untagged: untaggedRows,
245
+ warnings,
246
+ };
247
+ if (json)
248
+ return data;
249
+ if (rows.length === 0) {
250
+ console.log('no skills found');
251
+ return;
252
+ }
253
+ if (values.tag && !target) {
254
+ for (const row of data.rows) {
255
+ const relationships = row.relationships
256
+ .map(({ target, slot, presence }) => `global:${target}/${slot}:${presence}`)
257
+ .join(', ');
258
+ console.log(`${row.name}\tskill:${row.resourceId}\t${relationships}`);
259
+ }
260
+ return;
261
+ }
262
+ printMatrix(rows, cols);
263
+ if (deadlinks.length > 0)
264
+ console.log(`\n死链 (doctor 清理): ${deadlinks.join(', ')}`);
265
+ if (untaggedRows.length > 0)
266
+ console.log(`未分类 (skillspub tag add): ${untaggedRows.join(', ')}`);
267
+ }
268
+ function activationPlanData(plan, operation = 'activation') {
269
+ return {
270
+ operation,
271
+ targets: plan.targets.map((target) => ({
272
+ targetId: target.targetId,
273
+ targetKey: target.targetKey,
274
+ slot: target.slot,
275
+ resourceId: target.resourceId,
276
+ from: target.from,
277
+ intent: target.intent,
278
+ to: target.to,
279
+ ...(target.destination ? { destination: target.destination } : {}),
280
+ ...(target.createForm ? { createForm: target.createForm } : {}),
281
+ ...(target.mirrorAction ? { mirrorAction: target.mirrorAction } : {}),
282
+ ...(target.syncMirror ? { syncMirror: true } : {}),
283
+ ...(target.remove ? { remove: true } : {}),
284
+ })),
285
+ staleResourceIds: plan.staleResourceIds,
286
+ };
287
+ }
288
+ function activationApplyError(home, plan, error, projectPath) {
289
+ const cause = error;
290
+ if (cause.code === 'concurrent_modification')
291
+ throw new CliError('concurrent_modification', cause.message, 1, { partialEffects: 'none', remainingDrift: [] });
292
+ let driftRemaining = [];
293
+ let scanError;
294
+ try {
295
+ const report = projectPath
296
+ ? scanProjectInventory(home, projectPath)
297
+ : scanGlobalInventory(home);
298
+ driftRemaining = remainingDrift(plan, report);
299
+ }
300
+ catch (scanFailure) {
301
+ scanError = scanFailure.message;
302
+ }
303
+ const drift = scanError ? `could not rescan: ${scanError}` : driftRemaining.join(', ') || 'none';
304
+ throw new CliError('apply_failed', `${cause.message}\nRemaining drift: ${drift}`, 1, {
305
+ partialEffects: driftRemaining.length > 0 ? 'present' : 'unknown',
306
+ remainingDrift: driftRemaining,
307
+ ...(scanError ? { scanError } : {}),
308
+ });
309
+ }
310
+ function cmdOnOff(home, on, args, json = false) {
311
+ const [skill, ...names] = args;
312
+ if (!skill || names.length === 0)
313
+ throw new Error(`usage: skillspub ${on ? 'on' : 'off'} <skill> <target...>`);
314
+ if (!skill.startsWith('bundle:') && !skill.startsWith('tag:') && !skill.startsWith('skill:')) {
315
+ const rows = projectRows(scanGlobalInventory(home, undefined, { persist: false }));
316
+ refuseAmbiguousName(rows, skill);
317
+ }
318
+ const confirmed = names.includes('--yes');
319
+ const targets = names.filter((name) => name !== '--yes');
320
+ const plan = planActivation(home, skill, targets, on ? 'on' : 'off');
321
+ const planData = activationPlanData(plan);
322
+ if (json && !confirmed)
323
+ return { applied: false, plan: planData };
324
+ if (!json) {
325
+ console.log('Plan:');
326
+ if (plan.targets.length === 0)
327
+ console.log(' no current Target Slots');
328
+ else
329
+ for (const target of plan.targets) {
330
+ const intent = target.intent === target.to
331
+ ? ''
332
+ : ` (Base intent ${target.intent}; claimed ${target.to})`;
333
+ let mirror = '';
334
+ if (target.createForm === 'mirror')
335
+ mirror = ' (create Mirror)';
336
+ else if (target.syncMirror)
337
+ mirror = ' (synchronize Mirror)';
338
+ console.log(` ${target.targetId}/${target.slot}\t${target.from} -> ${target.to}${intent}${mirror}`);
339
+ }
340
+ }
341
+ if (!confirmed && plan.targets.some((target) => target.from === 'missing' && target.to === 'on')) {
342
+ throw new Error('creating missing Relationships requires --yes');
343
+ }
344
+ try {
345
+ applyActivationPlan(home, plan);
346
+ }
347
+ catch (error) {
348
+ activationApplyError(home, plan, error);
349
+ }
350
+ const report = scanGlobalInventory(home);
351
+ if (json)
352
+ return {
353
+ applied: true,
354
+ plan: planData,
355
+ result: { verified: true },
356
+ remainingDrift: remainingDrift(plan, report),
357
+ };
358
+ }
359
+ function cmdMirror(home, args, projectPath, json = false) {
360
+ const [action, targetId, slot, ...rest] = args;
361
+ if (!['sync', 'overwrite', 'remove', 'convert'].includes(action ?? '') || !targetId || !slot ||
362
+ rest.some((arg) => arg !== '--yes'))
363
+ throw new Error('usage: skillspub mirror sync|overwrite|remove|convert <target-id> <slot> [--yes]');
364
+ const scope = projectPath ? { projectPath } : {};
365
+ const plan = planMirrorAction(home, targetId, slot, action, scope);
366
+ const planData = activationPlanData(plan, `mirror.${action}`);
367
+ const confirmed = rest.includes('--yes');
368
+ if (json && !confirmed)
369
+ return { applied: false, plan: planData };
370
+ if (!json) {
371
+ const target = plan.targets[0];
372
+ console.log(`Mirror plan: ${action} ${target?.targetId}/${target?.slot}`);
373
+ }
374
+ if (!confirmed)
375
+ return;
376
+ try {
377
+ applyActivationPlan(home, plan);
378
+ }
379
+ catch (error) {
380
+ activationApplyError(home, plan, error, projectPath);
381
+ }
382
+ const report = projectPath
383
+ ? scanProjectInventory(home, projectPath)
384
+ : scanGlobalInventory(home);
385
+ if (json)
386
+ return {
387
+ applied: true,
388
+ plan: planData,
389
+ result: { verified: true },
390
+ remainingDrift: remainingDrift(plan, report),
391
+ };
392
+ }
393
+ function cmdStatus(home, args, json = false) {
394
+ const [skill] = args;
395
+ if (!skill || args.length !== 1)
396
+ throw new Error('usage: skillspub status <skill>');
397
+ const report = scanGlobalInventory(home, undefined, { persist: false });
398
+ const targets = viewTargets(report);
399
+ const matches = matchingInstances(projectRows(report), skill);
400
+ if (matches.length === 0) {
401
+ if (json)
402
+ throw new CliError('resource_not_found', `${skill} not found in any Target`, 1, { selector: skill });
403
+ console.error(`warning: ${skill} not found in any Target`);
404
+ process.exitCode = 1;
405
+ return;
406
+ }
407
+ refuseAmbiguousName(matches, skill);
408
+ if (json) {
409
+ const instance = matches[0];
410
+ return {
411
+ name: skill,
412
+ resourceId: instance.realPath ?? variantLocation(instance),
413
+ targets: targets.map((target) => ({
414
+ target: target.name,
415
+ relationships: relationships(instance, target.name).map(({ name, info }) => ({
416
+ slot: name,
417
+ presence: info.presence,
418
+ form: info.form,
419
+ path: info.path,
420
+ ...(info.target ? { linkTarget: info.target } : {}),
421
+ })),
422
+ })),
423
+ };
424
+ }
425
+ for (const [index, instance] of matches.entries()) {
426
+ if (matches.length > 1) {
427
+ if (index > 0)
428
+ console.log('');
429
+ console.log(`${instance.displayName}\t${variantLocation(instance)}`);
430
+ }
431
+ for (const target of targets) {
432
+ const found = relationships(instance, target.name);
433
+ if (found.length === 0) {
434
+ console.log(`${target.name}\t—`);
435
+ continue;
436
+ }
437
+ for (const { name, info } of found) {
438
+ const extra = info.target ? ` -> ${info.target}` : '';
439
+ console.log(`${target.name}\t${CELL[info.presence]}\t${info.path}${extra}${name === skill ? '' : ` (${name})`}`);
440
+ }
441
+ }
442
+ }
443
+ }
444
+ function cmdTargets(home, args, json = false) {
445
+ if (args.length > 0)
446
+ throw new Error('usage: skillspub targets');
447
+ const targets = loadTargets(home);
448
+ if (json)
449
+ return targets;
450
+ for (const target of targets) {
451
+ console.log([
452
+ target.key,
453
+ target.kind,
454
+ target.discoveryRoot,
455
+ target.parkingRoot,
456
+ ].join('\t'));
457
+ }
458
+ }
459
+ function printHarnesses(title, harnesses) {
460
+ console.log(title);
461
+ if (harnesses.length === 0) {
462
+ console.log(' none');
463
+ return;
464
+ }
465
+ for (const harness of harnesses) {
466
+ console.log(`${harness.key}\t${harness.support}\tShared ${harness.sharedConsumption.status}\tIsolation ${harness.isolation.status}\tLink ${harness.link.supported ? 'supported' : 'unsupported'}${harness.mirror ? `\tMirror ${harness.mirror.supported ? 'supported' : 'unsupported'}` : ''}`);
467
+ console.log(` Shared: ${harness.sharedConsumption.detail}`);
468
+ console.log(` Isolation: ${harness.isolation.detail}`);
469
+ for (const target of harness.targets)
470
+ console.log(` target\t${target.scope}\t${target.discoveryRoot}`);
471
+ for (const evidence of harness.evidence)
472
+ console.log(` evidence\tv${evidence.verifiedVersion}\t${evidence.url}`);
473
+ }
474
+ }
475
+ function cmdHarnesses(home, args, projectPath, json = false) {
476
+ const targets = loadTargets(home);
477
+ const selectedProject = projectPath ? fs.realpathSync(projectPath) : undefined;
478
+ if (args.length === 0) {
479
+ const report = inspectHarnesses(home, targets, selectedProject);
480
+ if (json)
481
+ return report;
482
+ printHarnesses('Detected Harnesses:', report.detected);
483
+ printHarnesses('Available Harnesses:', report.available);
484
+ return;
485
+ }
486
+ const [harness, action, ...rest] = args;
487
+ if (!harness || !['inspect', 'setup', 'reconcile', 'migrate'].includes(action ?? '') ||
488
+ rest.some((arg) => arg !== '--yes') || (action === 'inspect' && rest.length > 0) ||
489
+ (action === 'migrate' && (!selectedProject || harness !== 'pi')))
490
+ throw new Error('usage: skillspub harnesses [name inspect|setup|reconcile [--yes]] (Pi migrate is Project-only)');
491
+ if (action === 'inspect') {
492
+ const inspection = inspectHarness(harness, home, targets, selectedProject);
493
+ if (json)
494
+ return inspection;
495
+ printHarnesses(`${harness} Harness:`, [inspection]);
496
+ return;
497
+ }
498
+ const plan = planHarnessOperation(harness, action, home, targets, selectedProject);
499
+ const stableLine = (line) => line.replace(/\d{10,}-[0-9a-f]{8}-[0-9a-f-]{27}/gi, '<recovery-id>');
500
+ const stableData = (value) => {
501
+ try {
502
+ return JSON.parse(JSON.stringify(value, (_key, entry) => typeof entry === 'string' ? stableLine(entry) : entry));
503
+ }
504
+ catch (error) {
505
+ throw new Error('failed to stabilize Harness operation data', { cause: error });
506
+ }
507
+ };
508
+ const planData = {
509
+ operation: `harness.${action}`,
510
+ harness,
511
+ ...(selectedProject ? { projectPath: selectedProject } : {}),
512
+ title: plan.title,
513
+ steps: plan.lines.map(stableLine),
514
+ ...(plan.relationshipImpact
515
+ ? { relationshipImpact: stableData(plan.relationshipImpact) }
516
+ : {}),
517
+ ...(plan.recovery?.length ? { recovery: plan.recovery.map(stableLine) } : {}),
518
+ };
519
+ const confirmed = rest.includes('--yes');
520
+ if (json && !confirmed)
521
+ return { applied: false, plan: planData };
522
+ if (!json) {
523
+ console.log(plan.title);
524
+ for (const line of plan.lines)
525
+ console.log(` ${line}`);
526
+ if (plan.recovery?.length) {
527
+ console.log('Recovery before confirmation:');
528
+ for (const line of plan.recovery)
529
+ console.log(` ${line}`);
530
+ }
531
+ }
532
+ if (!confirmed)
533
+ return;
534
+ let inspection;
535
+ try {
536
+ plan.apply();
537
+ inspection = plan.verify();
538
+ }
539
+ catch (error) {
540
+ const cause = error;
541
+ throw new CliError(cause.code === 'concurrent_modification' ? 'concurrent_modification' : 'apply_failed', cause.message, 1, {
542
+ partialEffects: cause.partialEffects ?? (cause.code === 'concurrent_modification' ? 'none' : 'unknown'),
543
+ recovery: plan.recovery ?? [],
544
+ });
545
+ }
546
+ const result = plan.result?.(inspection) ?? { inspection, recovery: plan.recovery ?? [] };
547
+ if (!json) {
548
+ if (plan.recovery?.length) {
549
+ console.log('Manual recovery:');
550
+ for (const line of plan.recovery)
551
+ console.log(` ${line}`);
552
+ }
553
+ if ('drift' in result) {
554
+ console.log(`Actual: ${result.actual.unlinkedRelationships} unlinked, ` +
555
+ `${result.actual.retainedRelationships} retained, ` +
556
+ `${result.actual.preservedSourceResources} source resources preserved`);
557
+ console.log(`Desired: ${result.desired.unlinkedRelationships} unlinked, ` +
558
+ `${result.desired.retainedRelationships} retained, ` +
559
+ `${result.desired.preservedSourceResources} source resources preserved`);
560
+ console.log(`Drift: ${result.drift.relationships.length} Relationship effects; ` +
561
+ `isolation ${result.drift.isolation ? 'yes' : 'no'}`);
562
+ console.log(`Isolation: ${result.isolation.status} — ${result.isolation.detail}`);
563
+ if (result.sharedConsumption)
564
+ console.log(`Shared consumption: ${result.sharedConsumption.status} — ${result.sharedConsumption.detail}`);
565
+ if (result.effectiveVisibility)
566
+ console.log(`Effective Visibility: ${result.effectiveVisibility.status} — ${result.effectiveVisibility.detail}`);
567
+ console.log('Relationship effects:');
568
+ for (const effect of result.relationshipEffects) {
569
+ console.log(` ${effect.scope}/${effect.targetKey} (${effect.targetId})\t` +
570
+ `resource=${effect.resourceId}\tform=${effect.form}\tActivation=${effect.activation}\t` +
571
+ `source=${effect.sourcePath}\ttarget=${effect.targetPath}\t` +
572
+ `action=${effect.plannedAction}\toutcome=${effect.outcome}`);
573
+ }
574
+ console.log(`Recovery evidence: config backup ${result.recovery.configBackupPreserved ? 'preserved' : 'missing'}; ` +
575
+ `${result.recovery.stateBackupPreserved === undefined ? '' : `state backup ${result.recovery.stateBackupPreserved ? 'preserved' : 'missing'}; `}` +
576
+ `affected-Link manifest ${result.recovery.manifestPreserved ? 'preserved' : 'missing'}; ` +
577
+ `${result.recovery.manifestPath}`);
578
+ }
579
+ console.log(`${inspection.name} ${action} verified.`);
580
+ return;
581
+ }
582
+ return {
583
+ applied: true,
584
+ plan: planData,
585
+ result: stableData(result),
586
+ remainingDrift: 'drift' in result ? result.drift.relationships : [],
587
+ };
588
+ }
589
+ function printTargetMigration(home) {
590
+ const plan = planTargetMigration(home);
591
+ if (plan.status === 'already-migrated') {
592
+ console.log('Target registry already migrated.');
593
+ return plan;
594
+ }
595
+ console.log('Target migration plan:');
596
+ console.log(` legacy\t${plan.legacyFile}`);
597
+ console.log('Legacy Target Definition overrides:');
598
+ if (plan.overrides.length === 0)
599
+ console.log(' none');
600
+ else
601
+ for (const override of plan.overrides) {
602
+ console.log(` ${override.disabled ? 'disabled' : 'override'}\t${override.key}`);
603
+ for (const field of ['discoveryRoot', 'parkingRoot', 'projectPath', 'lockFile'])
604
+ if (override[field] !== undefined)
605
+ console.log(` ${field}\t${override[field]}`);
606
+ }
607
+ console.log('New built-in Target Definitions:');
608
+ if (plan.introducedDefinitions.length === 0)
609
+ console.log(' none');
610
+ else
611
+ for (const definition of plan.introducedDefinitions) {
612
+ console.log(` introduced\t${definition.key}`);
613
+ console.log(` global discovery\t${definition.discoveryRoot}`);
614
+ console.log(` global parking\t${definition.parkingRoot}`);
615
+ console.log(` project\t${definition.projectPath}`);
616
+ if (definition.relationship)
617
+ console.log(` capabilities\t${definition.relationship.support}, link ${definition.relationship.link}`);
618
+ }
619
+ if (plan.genericTargets.length === 0)
620
+ console.log(' no Generic Targets');
621
+ else
622
+ for (const target of plan.genericTargets)
623
+ console.log(` generic\t${target.key}`);
624
+ console.log(` write\t${plan.targetFile}`);
625
+ console.log(` backup\t${plan.backupFile}`);
626
+ return plan;
627
+ }
628
+ function cmdMigrate(home, args, json = false) {
629
+ const [subject, ...rest] = args;
630
+ if (subject !== 'targets' || rest.some((arg) => arg !== '--yes'))
631
+ throw new Error('usage: skillspub migrate targets [--yes]');
632
+ const plan = json ? planTargetMigration(home) : printTargetMigration(home);
633
+ const planData = { operation: 'migrate.targets', ...plan };
634
+ const confirmed = rest.includes('--yes');
635
+ if (json && !confirmed)
636
+ return { applied: false, plan: planData };
637
+ if (plan.status === 'already-migrated') {
638
+ if (json)
639
+ return {
640
+ applied: confirmed,
641
+ plan: planData,
642
+ result: { status: 'already-migrated', targets: loadTargets(home) },
643
+ remainingDrift: [],
644
+ };
645
+ return;
646
+ }
647
+ if (!confirmed)
648
+ return;
649
+ try {
650
+ applyTargetMigration(home, plan);
651
+ }
652
+ catch (error) {
653
+ const cause = error;
654
+ throw new CliError(cause.code === 'concurrent_modification' ? 'concurrent_modification' : 'apply_failed', cause.message, 1, { partialEffects: cause.code === 'concurrent_modification' ? 'none' : 'unknown' });
655
+ }
656
+ if (!json) {
657
+ console.log(`Migrated Target registry: ${plan.targetFile}`);
658
+ return;
659
+ }
660
+ return {
661
+ applied: true,
662
+ plan: planData,
663
+ result: { status: 'migrated', targets: loadTargets(home) },
664
+ remainingDrift: [],
665
+ };
666
+ }
667
+ function prepareGlobalMutation(home) {
668
+ scanGlobalInventory(home);
669
+ }
670
+ function cmdTag(home, args, json = false) {
671
+ const confirmed = json && args.includes('--yes');
672
+ const [action, resource, ...names] = json ? args.filter((arg) => arg !== '--yes') : args;
673
+ switch (action) {
674
+ case 'add': {
675
+ if (!resource || names.length === 0)
676
+ throw new Error('usage: skillspub tag add <resource> <tag...>');
677
+ if (json)
678
+ return runCatalogMutation(home, {
679
+ operation: 'tag.add', resource, names,
680
+ }, confirmed);
681
+ prepareGlobalMutation(home);
682
+ const added = addResourceTags(home, resource, names);
683
+ console.log(`added ${added} tag${added === 1 ? '' : 's'} to ${resource}`);
684
+ break;
685
+ }
686
+ case 'rm': {
687
+ if (!resource)
688
+ throw new Error('usage: skillspub tag rm <resource> [<tag...>]');
689
+ if (json)
690
+ return runCatalogMutation(home, {
691
+ operation: 'tag.rm', resource, names,
692
+ }, confirmed);
693
+ prepareGlobalMutation(home);
694
+ const removed = removeResourceTags(home, resource, names);
695
+ console.log(`removed ${removed} tag${removed === 1 ? '' : 's'} from ${resource}`);
696
+ break;
697
+ }
698
+ case 'ls': {
699
+ const { values } = parseArgs({
700
+ args: args.slice(1),
701
+ options: { skill: { type: 'string' } },
702
+ });
703
+ if (values.skill) {
704
+ const resourceTags = tagsForResource(home, values.skill);
705
+ if (json)
706
+ return resourceTags;
707
+ console.log(`${resourceTags.name ?? resourceTags.id}\tskill:${resourceTags.id}${resourceTags.stale ? '\tstale' : ''}`);
708
+ if (resourceTags.tags.length === 0)
709
+ console.log(' no tags');
710
+ else
711
+ for (const tag of resourceTags.tags)
712
+ console.log(` ${tag}`);
713
+ }
714
+ else {
715
+ const tags = listTags(home);
716
+ if (json)
717
+ return tags;
718
+ if (tags.length === 0)
719
+ console.log('no tags found');
720
+ else
721
+ for (const tag of tags)
722
+ console.log(`${tag.name}\t${tag.resources}`);
723
+ }
724
+ break;
725
+ }
726
+ default:
727
+ throw new Error('usage: skillspub tag add|rm|ls ...');
728
+ }
729
+ }
730
+ function printPresetPlan(plan) {
731
+ console.log('Plan:');
732
+ if (plan.targets.length === 0)
733
+ console.log(' no Target Slot changes');
734
+ else
735
+ for (const target of plan.targets) {
736
+ const intent = target.intent === target.to
737
+ ? ''
738
+ : ` (Base intent ${target.intent}; claimed ${target.to})`;
739
+ console.log(` ${target.targetId}/${target.slot}\t${target.from} -> ${target.to}${intent}`);
740
+ }
741
+ if (plan.staleResourceIds.length > 0) {
742
+ console.log('Stale selectors:');
743
+ for (const id of plan.staleResourceIds)
744
+ console.log(` - skill:${id}`);
745
+ }
746
+ }
747
+ function presetPlanData(plan, operation) {
748
+ return {
749
+ ...activationPlanData(plan, operation),
750
+ claims: plan.claims,
751
+ lastClaims: plan.lastClaims,
752
+ presetActivations: plan.presetActivations,
753
+ baseIntentDefaults: plan.baseIntentDefaults,
754
+ };
755
+ }
756
+ function runPresetPlan(home, plan, scope, operation, json = false, confirmed = false) {
757
+ const planData = presetPlanData(plan, operation);
758
+ if (json && !confirmed)
759
+ return { applied: false, plan: planData };
760
+ if (!json)
761
+ printPresetPlan(plan);
762
+ try {
763
+ applyPresetReconcile(home, plan, scope);
764
+ }
765
+ catch (error) {
766
+ activationApplyError(home, plan, error, scope.projectPath);
767
+ }
768
+ const report = scope.projectPath
769
+ ? scanProjectInventory(home, scope.projectPath)
770
+ : scanGlobalInventory(home);
771
+ if (json)
772
+ return {
773
+ applied: true,
774
+ plan: planData,
775
+ result: { verified: true },
776
+ remainingDrift: remainingDrift(plan, report),
777
+ };
778
+ }
779
+ function presetDeletePlanData(home, name, projectPath) {
780
+ const selectors = showPreset(home, name);
781
+ const globalState = readState(home);
782
+ const globalTargets = globalState.presetActivations?.[name] ?? [];
783
+ const deactivations = [];
784
+ if (globalTargets.length > 0)
785
+ deactivations.push(presetPlanData(deactivatePreset(home, name, globalTargets), 'preset.deactivate'));
786
+ if (projectPath) {
787
+ const realProject = fs.realpathSync(projectPath);
788
+ const projectState = readStateFile(path.join(realProject, '.skillspub', 'state.json'));
789
+ const projectTargets = projectState.presetActivations?.[name] ?? [];
790
+ if (projectTargets.length > 0)
791
+ deactivations.push(presetPlanData(deactivatePreset(home, name, projectTargets, { projectPath: realProject }), 'preset.deactivate'));
792
+ }
793
+ return {
794
+ operation: 'preset.delete',
795
+ name,
796
+ selectors,
797
+ deactivations,
798
+ ...(projectPath ? { projectPath: fs.realpathSync(projectPath) } : {}),
799
+ };
800
+ }
801
+ function cmdPreset(home, args, projectPath, json = false) {
802
+ const scope = projectPath ? { projectPath } : {};
803
+ const confirmed = json && args.includes('--yes');
804
+ const [action, name, ...rest] = json ? args.filter((arg) => arg !== '--yes') : args;
805
+ switch (action) {
806
+ case 'ls': {
807
+ if (name)
808
+ throw new Error('usage: skillspub preset ls');
809
+ const presets = listPresets(home);
810
+ if (json)
811
+ return presets;
812
+ if (presets.length === 0)
813
+ console.log('no presets found');
814
+ else
815
+ for (const preset of presets)
816
+ console.log(`${preset.name}\t${preset.selectors}`);
817
+ break;
818
+ }
819
+ case 'show': {
820
+ if (!name || rest.length > 0)
821
+ throw new Error('usage: skillspub preset show <name>');
822
+ const selectors = showPreset(home, name);
823
+ if (json)
824
+ return { name, selectors };
825
+ console.log(name);
826
+ if (selectors.length === 0)
827
+ console.log(' no selectors');
828
+ else
829
+ for (const item of selectors)
830
+ console.log(` ${item.selector}`);
831
+ break;
832
+ }
833
+ case 'create': {
834
+ if (!name)
835
+ throw new Error('usage: skillspub preset create <name> [<selector...>]');
836
+ if (json)
837
+ return runCatalogMutation(home, {
838
+ operation: 'preset.create', name, selectors: rest,
839
+ }, confirmed);
840
+ prepareGlobalMutation(home);
841
+ const count = createPreset(home, name, rest);
842
+ console.log(`created preset ${name} with ${count} selector${count === 1 ? '' : 's'}`);
843
+ break;
844
+ }
845
+ case 'add': {
846
+ if (!name || rest.length === 0)
847
+ throw new Error('usage: skillspub preset add <name> <selector...>');
848
+ if (json)
849
+ return runCatalogMutation(home, {
850
+ operation: 'preset.add', name, selectors: rest,
851
+ }, confirmed);
852
+ prepareGlobalMutation(home);
853
+ const added = addPresetSelectors(home, name, rest);
854
+ console.log(`added ${added} selector${added === 1 ? '' : 's'} to ${name}`);
855
+ break;
856
+ }
857
+ case 'rm': {
858
+ if (!name)
859
+ throw new Error('usage: skillspub preset rm <name> [<selector...>]');
860
+ if (json)
861
+ return runCatalogMutation(home, {
862
+ operation: 'preset.rm', name, selectors: rest,
863
+ }, confirmed);
864
+ prepareGlobalMutation(home);
865
+ const removed = removePresetSelectors(home, name, rest);
866
+ console.log(removed === undefined
867
+ ? `removed preset ${name}`
868
+ : `removed ${removed} selector${removed === 1 ? '' : 's'} from ${name}`);
869
+ break;
870
+ }
871
+ case 'activate': {
872
+ if (!name || rest.length === 0)
873
+ throw new Error('usage: skillspub preset activate <name> <target...>');
874
+ return runPresetPlan(home, activatePreset(home, name, rest, scope), scope, 'preset.activate', json, confirmed);
875
+ }
876
+ case 'deactivate': {
877
+ if (!name || rest.length === 0)
878
+ throw new Error('usage: skillspub preset deactivate <name> <target...>');
879
+ return runPresetPlan(home, deactivatePreset(home, name, rest, scope), scope, 'preset.deactivate', json, confirmed);
880
+ }
881
+ case 'reconcile': {
882
+ const presetName = name;
883
+ const targets = rest;
884
+ return runPresetPlan(home, planPresetReconcile(home, presetName, targets.length > 0 ? targets : undefined, scope), scope, 'preset.reconcile', json, confirmed);
885
+ }
886
+ case 'delete': {
887
+ if (!name || rest.some((arg) => arg !== '--yes'))
888
+ throw new Error('usage: skillspub preset delete <name> [--yes]');
889
+ if (json) {
890
+ const plan = presetDeletePlanData(home, name, projectPath);
891
+ if (!confirmed)
892
+ return { applied: false, plan };
893
+ try {
894
+ deletePreset(home, name, { yes: true, projectPath });
895
+ }
896
+ catch (error) {
897
+ const cause = error;
898
+ throw new CliError(cause.code === 'concurrent_modification' ? 'concurrent_modification' : 'apply_failed', cause.message, 1, { partialEffects: cause.code === 'concurrent_modification' ? 'none' : 'unknown' });
899
+ }
900
+ return {
901
+ applied: true,
902
+ plan,
903
+ result: { deleted: true },
904
+ remainingDrift: [],
905
+ };
906
+ }
907
+ deletePreset(home, name, { yes: args.includes('--yes'), projectPath });
908
+ console.log(`deleted preset ${name}`);
909
+ break;
910
+ }
911
+ default:
912
+ throw new Error('usage: skillspub preset ls|show|create|add|rm|activate|deactivate|reconcile|delete ...');
913
+ }
914
+ }
915
+ function runCatalogMutation(home, mutation, confirmed) {
916
+ const plan = planCatalogMutation(home, scanGlobalInventory(home, undefined, { persist: false }), mutation);
917
+ if (!confirmed)
918
+ return { applied: false, plan };
919
+ const result = applyCatalogMutation(home, plan);
920
+ return { applied: true, plan, result, remainingDrift: [] };
921
+ }
922
+ function cmdBundle(home, args, json = false) {
923
+ const confirmed = json && args.includes('--yes');
924
+ const [action, name, ...selectors] = json ? args.filter((arg) => arg !== '--yes') : args;
925
+ switch (action) {
926
+ case 'ls': {
927
+ if (name)
928
+ throw new Error('usage: skillspub bundle ls');
929
+ const bundles = listBundles(home);
930
+ if (json)
931
+ return bundles;
932
+ if (bundles.length === 0)
933
+ console.log('no bundles found');
934
+ else
935
+ for (const bundle of bundles)
936
+ console.log(`${bundle.name}\t${bundle.members}`);
937
+ break;
938
+ }
939
+ case 'show': {
940
+ if (!name || selectors.length > 0)
941
+ throw new Error('usage: skillspub bundle show <name>');
942
+ const members = showBundle(home, name);
943
+ if (json)
944
+ return { name, members };
945
+ console.log(name);
946
+ if (members.length === 0)
947
+ console.log(' no members');
948
+ else
949
+ for (const member of members) {
950
+ console.log(` ${member.name ?? member.id}\tskill:${member.id}${member.stale ? '\tstale' : ''}`);
951
+ }
952
+ break;
953
+ }
954
+ case 'create': {
955
+ if (!name)
956
+ throw new Error('usage: skillspub bundle create <name> [<skill>...]');
957
+ if (json)
958
+ return runCatalogMutation(home, {
959
+ operation: 'bundle.create', name, selectors,
960
+ }, confirmed);
961
+ prepareGlobalMutation(home);
962
+ const count = createBundle(home, name, selectors);
963
+ console.log(`created bundle ${name} with ${count} member${count === 1 ? '' : 's'}`);
964
+ break;
965
+ }
966
+ case 'add': {
967
+ if (!name || selectors.length === 0)
968
+ throw new Error('usage: skillspub bundle add <name> <skill...>');
969
+ if (json)
970
+ return runCatalogMutation(home, {
971
+ operation: 'bundle.add', name, selectors,
972
+ }, confirmed);
973
+ prepareGlobalMutation(home);
974
+ const added = addBundleMembers(home, name, selectors);
975
+ console.log(`added ${added} member${added === 1 ? '' : 's'} to ${name}`);
976
+ break;
977
+ }
978
+ case 'rm': {
979
+ if (!name)
980
+ throw new Error('usage: skillspub bundle rm <name> [<skill>...]');
981
+ if (json)
982
+ return runCatalogMutation(home, {
983
+ operation: 'bundle.rm', name, selectors,
984
+ }, confirmed);
985
+ prepareGlobalMutation(home);
986
+ const removed = removeBundleMembers(home, name, selectors);
987
+ console.log(removed === undefined
988
+ ? `removed bundle ${name}`
989
+ : `removed ${removed} member${removed === 1 ? '' : 's'} from ${name}`);
990
+ break;
991
+ }
992
+ default:
993
+ throw new Error('usage: skillspub bundle ls|show|create|add|rm ...');
994
+ }
995
+ }
996
+ function printSharedPlan(plan) {
997
+ let label = 'Add';
998
+ if ('items' in plan)
999
+ label = 'Update';
1000
+ else if (plan.replacement)
1001
+ label = 'Replace';
1002
+ console.log(`Source ${label} plan:`);
1003
+ console.log(`Scope: ${plan.scope?.kind === 'project' ? 'exact Project' : 'Global'} — ${plan.scope?.path}`);
1004
+ console.log(JSON.stringify(plan, null, 2));
1005
+ console.log('Confirm with --yes after reviewing this immutable plan.');
1006
+ }
1007
+ function sharedSourceVerification(home, plan, result, projectPath, outcome = 'succeeded') {
1008
+ const truth = verifySourceMutation(home, plan, result, projectPath);
1009
+ let relationshipEffects;
1010
+ if ('items' in plan)
1011
+ relationshipEffects = plan.items.filter((item) => item.included).flatMap((item) => item.relationshipEffects);
1012
+ else if ('dependencies' in plan)
1013
+ relationshipEffects = plan.dependencies;
1014
+ else
1015
+ relationshipEffects = plan.relationshipEffects ?? [];
1016
+ const source = { outcome, provenance: truth.provenance, resource: truth.resource };
1017
+ if ('items' in plan) {
1018
+ const outcomes = new Map(result.items.map((item) => [item.name, item.outcome]));
1019
+ source.items = plan.items.map((item) => ({
1020
+ name: item.name,
1021
+ outcome: outcomes.get(item.name) ?? 'skipped',
1022
+ provenance: item.expectedFinalTruth.source,
1023
+ }));
1024
+ }
1025
+ const mirrorState = sourceMirrorState(truth);
1026
+ return {
1027
+ actualRelationships: truth.relationships,
1028
+ actual: truth.actual,
1029
+ desired: truth.desired,
1030
+ drift: truth.drift,
1031
+ source,
1032
+ updateAvailability: truth.updateAvailability,
1033
+ relationshipEffects,
1034
+ mirrorState,
1035
+ recovery: plan.recovery,
1036
+ nextLoadEffectiveVisibility: truth.effectiveVisibility,
1037
+ runningHarnessReloaded: false,
1038
+ };
1039
+ }
1040
+ function printSourceVerification(finalTruth) {
1041
+ console.log('Final truth:');
1042
+ console.log(JSON.stringify(finalTruth, null, 2));
1043
+ }
1044
+ function sharedFailureWithPlan(home, error, plan, projectPath) {
1045
+ const failure = error;
1046
+ const details = failure.details ?? {};
1047
+ const actual = typeof details.actual === 'string' ? details.actual : 'rescan unavailable';
1048
+ const drift = Array.isArray(details.remainingDrift) ? details.remainingDrift : [];
1049
+ const result = 'items' in plan
1050
+ ? {
1051
+ actual,
1052
+ drift,
1053
+ items: Array.isArray(details.items)
1054
+ ? details.items
1055
+ : plan.items.map((item) => ({
1056
+ ...item,
1057
+ outcome: item.included ? 'failed' : 'skipped',
1058
+ ...(item.included ? { reason: failure.message } : {}),
1059
+ })),
1060
+ }
1061
+ : { actual, drift };
1062
+ const partial = details.partialEffects !== undefined && details.partialEffects !== 'none-detected' ||
1063
+ Array.isArray(details.completedWork) && details.completedWork.length > 0;
1064
+ failure.details = {
1065
+ ...details,
1066
+ plan,
1067
+ finalTruth: sharedSourceVerification(home, plan, result, projectPath, partial ? 'partial' : 'failed'),
1068
+ };
1069
+ return failure;
1070
+ }
1071
+ function cmdShared(home, args, projectPath, json = false) {
1072
+ const confirmed = args.includes('--yes');
1073
+ const [action, ...rest] = args.filter((arg) => arg !== '--yes');
1074
+ if (confirmed && action !== 'add' && action !== 'update' && action !== 'remove')
1075
+ throw new Error(`usage: skillspub shared ${action ?? 'find|describe|refresh|outdated|add|update|remove'} ...`);
1076
+ switch (action) {
1077
+ case 'find':
1078
+ return sharedFind(home, rest, projectPath, !json);
1079
+ case 'describe':
1080
+ if (rest.length !== 1 || rest[0].startsWith('-'))
1081
+ throw new Error('usage: skillspub shared describe <source>');
1082
+ return sharedDescribe(home, rest[0], projectPath, !json);
1083
+ case 'refresh':
1084
+ case 'outdated': {
1085
+ if (rest.length > 0)
1086
+ throw new Error(`usage: skillspub shared ${action}`);
1087
+ const result = action === 'refresh'
1088
+ ? sharedRefresh(home, projectPath)
1089
+ : sharedOutdated(home, projectPath);
1090
+ if (!json)
1091
+ for (const entry of result.entries)
1092
+ console.log([
1093
+ entry.status,
1094
+ entry.name,
1095
+ entry.source,
1096
+ entry.checkedAt ?? '-',
1097
+ entry.error ?? '',
1098
+ ].join('\t'));
1099
+ return result;
1100
+ }
1101
+ case 'add': {
1102
+ const { values, positionals } = parseArgs({
1103
+ args: rest,
1104
+ options: {
1105
+ skill: { type: 'string' },
1106
+ replace: { type: 'boolean' },
1107
+ },
1108
+ allowPositionals: true,
1109
+ strict: true,
1110
+ });
1111
+ if (positionals.length !== 1 || !values.skill)
1112
+ throw new Error('usage: skillspub shared add <source> --skill <name> [--replace] [--yes]');
1113
+ const plan = planSharedAdd(home, positionals[0], values.skill, Boolean(values.replace), projectPath);
1114
+ if (plan.replacement && !values.replace) {
1115
+ if (!json) {
1116
+ console.log(`Replace: ${plan.replacement.from} -> ${plan.replacement.to}`);
1117
+ throw new Error('source replacement requires --replace');
1118
+ }
1119
+ throw new CliError('preflight_error', 'source replacement requires --replace', 1, { plan, requiredOption: '--replace' });
1120
+ }
1121
+ if (!confirmed) {
1122
+ if (json)
1123
+ return { applied: false, plan };
1124
+ printSharedPlan(plan);
1125
+ break;
1126
+ }
1127
+ let result;
1128
+ try {
1129
+ result = sharedAdd(home, positionals[0], values.skill, Boolean(values.replace), projectPath, plan, json);
1130
+ }
1131
+ catch (error) {
1132
+ throw sharedFailureWithPlan(home, error, plan, projectPath);
1133
+ }
1134
+ const finalTruth = sharedSourceVerification(home, plan, result, projectPath);
1135
+ if (json)
1136
+ return { applied: true, plan, result, finalTruth, remainingDrift: result.drift };
1137
+ console.log(`Actual: ${result.actual}`);
1138
+ console.log(`Remaining drift: ${result.drift.join(', ') || 'none'}`);
1139
+ printSourceVerification(finalTruth);
1140
+ break;
1141
+ }
1142
+ case 'update': {
1143
+ if (rest.some((arg) => arg.startsWith('-')))
1144
+ throw new Error('usage: skillspub shared update [<managed-name>...] [--yes]');
1145
+ const plan = planSharedUpdate(home, rest, projectPath);
1146
+ if (!confirmed) {
1147
+ if (json)
1148
+ return { applied: false, plan };
1149
+ printSharedPlan(plan);
1150
+ break;
1151
+ }
1152
+ let result;
1153
+ try {
1154
+ result = sharedUpdate(home, rest, projectPath, plan, json);
1155
+ }
1156
+ catch (error) {
1157
+ throw sharedFailureWithPlan(home, error, plan, projectPath);
1158
+ }
1159
+ if (!json)
1160
+ for (const item of result.items)
1161
+ console.log(`${item.name}: ${item.outcome}${item.reason ? ` (${item.reason})` : ''}`);
1162
+ const failed = result.items.filter(({ outcome }) => outcome === 'failed');
1163
+ if (failed.length > 0)
1164
+ throw Object.assign(new Error(`skills update failed (${failed.map(({ name, reason }) => `${name}: ${reason}`).join(', ')})\nActual: ${result.actual}\nRemaining drift: ${result.drift.join(', ') || 'none'}`), {
1165
+ code: 'apply_failed',
1166
+ details: {
1167
+ actual: result.actual,
1168
+ remainingDrift: result.drift,
1169
+ partialEffects: result.items.some(({ outcome }) => outcome === 'updated') ? 'present' : 'none-detected',
1170
+ stage: 'upstream',
1171
+ plan,
1172
+ items: result.items,
1173
+ finalTruth: sharedSourceVerification(home, plan, result, projectPath, result.items.some(({ outcome }) => outcome === 'updated') ? 'partial' : 'failed'),
1174
+ },
1175
+ });
1176
+ if (!json) {
1177
+ console.log(`Actual: ${result.actual}`);
1178
+ console.log(`Remaining drift: ${result.drift.join(', ') || 'none'}`);
1179
+ printSourceVerification(sharedSourceVerification(home, plan, result, projectPath));
1180
+ break;
1181
+ }
1182
+ return {
1183
+ applied: true,
1184
+ plan,
1185
+ result,
1186
+ finalTruth: sharedSourceVerification(home, plan, result, projectPath),
1187
+ remainingDrift: result.drift,
1188
+ };
1189
+ }
1190
+ case 'remove': {
1191
+ const { values, positionals } = parseArgs({
1192
+ args: rest,
1193
+ options: {
1194
+ yes: { type: 'boolean' },
1195
+ cascade: { type: 'boolean' },
1196
+ },
1197
+ allowPositionals: true,
1198
+ strict: true,
1199
+ });
1200
+ if (positionals.length !== 1)
1201
+ throw new Error('usage: skillspub shared remove <managed-name> [--cascade|--yes]');
1202
+ const cascadeRequested = Boolean(values.cascade);
1203
+ const yes = confirmed;
1204
+ const sourceConfirmed = yes && !cascadeRequested;
1205
+ const preview = planSharedRemove(home, positionals, projectPath);
1206
+ if (!json) {
1207
+ console.log('Removal plan:');
1208
+ console.log(` Source: ${preview.source.provenance} ${preview.source.path}`);
1209
+ if (preview.dependencies.length === 0)
1210
+ console.log(' no scanned dependent Relationships');
1211
+ else
1212
+ for (const dependency of preview.dependencies)
1213
+ console.log(` - ${dependency.targetId}/${dependency.slot}: ${dependency.form}/${dependency.activation} ` +
1214
+ `source=${dependency.source} target=${dependency.path} action=${dependency.plannedAction} ` +
1215
+ `fingerprint=${dependency.fingerprint}`);
1216
+ for (const blocker of preview.blockers)
1217
+ console.log(`Blocker: ${blocker}`);
1218
+ for (const warning of preview.warnings)
1219
+ console.log(`Warning: ${warning}`);
1220
+ console.log(`Recovery manifest: ${preview.recovery.manifest}`);
1221
+ }
1222
+ if (!json && preview.blockers.length > 0)
1223
+ throw new Error(preview.blockers.join('; '));
1224
+ if (!cascadeRequested && !sourceConfirmed) {
1225
+ if (json)
1226
+ return { applied: false, phase: 'preview', plan: preview };
1227
+ throw new Error('confirm the complete Relationship cascade with --cascade --yes');
1228
+ }
1229
+ if (cascadeRequested && !yes) {
1230
+ if (json)
1231
+ return { applied: false, phase: 'cascade-preview', plan: preview, requiredOption: '--yes' };
1232
+ throw new Error('Relationship cascade requires --cascade --yes');
1233
+ }
1234
+ if (cascadeRequested) {
1235
+ let result;
1236
+ try {
1237
+ result = sharedRemoveCascade(home, positionals, preview, projectPath);
1238
+ }
1239
+ catch (error) {
1240
+ throw sharedFailureWithPlan(home, error, preview, projectPath);
1241
+ }
1242
+ if (json)
1243
+ return {
1244
+ applied: true,
1245
+ phase: 'cascade',
1246
+ plan: preview,
1247
+ result,
1248
+ finalTruth: sharedSourceVerification(home, preview, result, projectPath, 'partial'),
1249
+ nextConfirmation: 'source-deletion',
1250
+ };
1251
+ console.log('Relationship cascade complete.');
1252
+ console.log(`Completed work: ${result.completedWork?.join(', ') || 'no dependent Relationships'}`);
1253
+ console.log(`Recovery manifest: ${result.recoveryManifest}`);
1254
+ printSourceVerification(sharedSourceVerification(home, preview, result, projectPath, 'partial'));
1255
+ console.log(`Confirm source deletion separately with: skillspub shared remove ${preview.source.name} --yes`);
1256
+ break;
1257
+ }
1258
+ let result;
1259
+ try {
1260
+ result = sharedRemove(home, positionals, {
1261
+ sourceConfirmed: true,
1262
+ projectPath,
1263
+ expected: preview,
1264
+ nonInteractive: json,
1265
+ });
1266
+ }
1267
+ catch (error) {
1268
+ throw sharedFailureWithPlan(home, error, preview, projectPath);
1269
+ }
1270
+ if (json)
1271
+ return {
1272
+ applied: true,
1273
+ phase: 'source',
1274
+ plan: preview,
1275
+ result,
1276
+ finalTruth: sharedSourceVerification(home, preview, result, projectPath),
1277
+ remainingDrift: result.drift,
1278
+ };
1279
+ console.log(`Actual: ${result.actual}`);
1280
+ console.log(`Completed work: ${result.completedWork?.join(', ')}`);
1281
+ console.log(`Remaining drift: ${result.drift.join(', ') || 'none'}`);
1282
+ printSourceVerification(sharedSourceVerification(home, preview, result, projectPath));
1283
+ break;
1284
+ }
1285
+ default:
1286
+ throw new Error('usage: skillspub shared find|describe|refresh|outdated|add|update|remove ...');
1287
+ }
1288
+ }
1289
+ function printDoctor(report) {
1290
+ console.log(report.scope === 'project'
1291
+ ? `Project Doctor: ${report.projectPath}`
1292
+ : 'Global Doctor');
1293
+ console.log('Structural anomalies:');
1294
+ const findings = report.findings.filter(({ category }) => category === 'structural');
1295
+ if (findings.length === 0)
1296
+ console.log(' none');
1297
+ else
1298
+ for (const finding of findings)
1299
+ console.log(` - ${finding.message}`);
1300
+ console.log('Safe repair plan:');
1301
+ if (report.repairs.length === 0)
1302
+ console.log(' none');
1303
+ for (const repair of report.repairs) {
1304
+ let action = 'remove broken link';
1305
+ if (repair.kind === 'retarget-link')
1306
+ action = 'retarget Link';
1307
+ else if (repair.kind === 'migrate-legacy-off')
1308
+ action = 'migrate legacy OFF';
1309
+ console.log(` - ${action}: ${repair.path}: ${repair.from}${repair.to ? ` -> ${repair.to}` : ''}`);
1310
+ }
1311
+ }
1312
+ function cmdDoctor(home, args, projectPath, json = false) {
1313
+ const { values } = parseArgs({
1314
+ args,
1315
+ options: {
1316
+ repair: { type: 'boolean' },
1317
+ yes: { type: 'boolean' },
1318
+ },
1319
+ strict: true,
1320
+ });
1321
+ if (values.yes && !values.repair)
1322
+ throw new Error('usage: skillspub doctor [--repair --yes]');
1323
+ const diagnose = () => projectPath
1324
+ ? doctorProjectInventory(home, projectPath)
1325
+ : doctorGlobalInventory(home);
1326
+ const report = diagnose();
1327
+ if (json && !values.repair)
1328
+ return report;
1329
+ const plan = {
1330
+ operation: 'doctor.repair',
1331
+ scope: report.scope,
1332
+ ...(report.projectPath ? { projectPath: report.projectPath } : {}),
1333
+ repairs: report.repairs,
1334
+ };
1335
+ if (json && !values.yes)
1336
+ return { applied: false, plan };
1337
+ if (!json)
1338
+ printDoctor(report);
1339
+ if (!values.repair || report.repairs.length === 0) {
1340
+ if (json)
1341
+ return {
1342
+ applied: Boolean(values.yes),
1343
+ plan,
1344
+ result: { completed: [], report },
1345
+ remainingDrift: [],
1346
+ };
1347
+ return;
1348
+ }
1349
+ if (!values.yes)
1350
+ throw new Error('repairs require confirmation; rerun with --repair --yes');
1351
+ const result = applyDoctorRepairs(report.repairs);
1352
+ const remaining = diagnose();
1353
+ if (json) {
1354
+ if (result.failed)
1355
+ throw new CliError('partial_apply', `repair failed: ${result.failed.repair.path}: ${result.failed.error}`, 1, { completed: result.completed, failed: result.failed, remaining });
1356
+ return {
1357
+ applied: true,
1358
+ plan,
1359
+ result: { completed: result.completed, report: remaining },
1360
+ remainingDrift: remaining.repairs.map(({ path }) => path),
1361
+ };
1362
+ }
1363
+ console.log(`Applied repairs: ${result.completed.length}`);
1364
+ if (result.failed) {
1365
+ console.error(`Repair failed: ${result.failed.repair.path}: ${result.failed.error}`);
1366
+ printDoctor(remaining);
1367
+ throw new Error('repair stopped; remaining anomalies are shown above');
1368
+ }
1369
+ printDoctor(remaining);
1370
+ }
1371
+ function printHarnessDrift(home, targets) {
1372
+ const harnesses = inspectHarnesses(home, targets);
1373
+ const drift = [...harnesses.detected, ...harnesses.available]
1374
+ .filter((harness) => harness.isolation.status === 'drift');
1375
+ console.log('Harness drift:');
1376
+ if (drift.length === 0)
1377
+ console.log(' none');
1378
+ else
1379
+ for (const harness of drift)
1380
+ console.log(` - ${harness.name} Shared isolation: ${harness.isolation.detail}`);
1381
+ }
1382
+ function printScan(report) {
1383
+ console.log(report.scope === 'project'
1384
+ ? `Project scan: ${report.projectPath}`
1385
+ : 'Global scan');
1386
+ console.log('Target roots:');
1387
+ for (const target of report.targets) {
1388
+ const source = target.scope === 'global' ? 'Global' : target.sourceDirectory;
1389
+ const access = target.writable ? 'writable' : `read-only from ${source}`;
1390
+ console.log(` - ${target.id} [${access}] ${target.discoveryRoot} | OFF ${target.parkingRoot}`);
1391
+ }
1392
+ console.log('Relationships:');
1393
+ if (report.relationships.length === 0)
1394
+ console.log(' none');
1395
+ for (const relationship of report.relationships) {
1396
+ const target = report.targets.find(({ id }) => id === relationship.targetId);
1397
+ const source = target?.scope === 'global' ? 'Global' : target?.sourceDirectory;
1398
+ const access = relationship.readOnly ? `read-only from ${source}` : 'writable';
1399
+ const linkTarget = relationship.target ? ` -> ${relationship.target}` : '';
1400
+ console.log(` - ${relationship.name} @ ${relationship.targetId}: ` +
1401
+ `${relationship.activation} ${relationship.form} [${access}] ${relationship.path}${linkTarget}`);
1402
+ }
1403
+ console.log(`Missing relationships: ${report.missing.length}`);
1404
+ const sections = [
1405
+ ['Structural anomalies', 'structural'],
1406
+ ['Uncategorized metadata', 'metadata'],
1407
+ ['External changes', 'change'],
1408
+ ];
1409
+ for (const [title, category] of sections) {
1410
+ console.log(`${title}:`);
1411
+ const findings = report.findings.filter((finding) => finding.category === category);
1412
+ if (findings.length === 0)
1413
+ console.log(' none');
1414
+ else
1415
+ for (const finding of findings)
1416
+ console.log(` - ${finding.message}`);
1417
+ }
1418
+ }
1419
+ async function main(args = process.argv.slice(2), stdinIsTty = Boolean(process.stdin.isTTY), stdoutIsTty = Boolean(process.stdout.isTTY)) {
1420
+ const json = args.includes('--json');
1421
+ const [cmd, ...rest] = args.filter((arg) => arg !== '--json');
1422
+ const mutation = isMutationCommand(cmd, rest);
1423
+ const home = defaultHome({ migrate: false });
1424
+ try {
1425
+ let data;
1426
+ if (json && cmd === 'tui')
1427
+ throw new CliError('usage_error', 'skillspub tui does not support --json', 2);
1428
+ if (json && rest.includes('--yes') && !mutation)
1429
+ throw new CliError('usage_error', '--yes is only valid for mutating commands', 2);
1430
+ if (!json && shouldRunTui(cmd, stdinIsTty, stdoutIsTty)) {
1431
+ let projectPath;
1432
+ const args = [...rest];
1433
+ while (args.length > 0) {
1434
+ const arg = args.shift();
1435
+ if (arg === '--project')
1436
+ projectPath = args.shift() ?? process.cwd();
1437
+ else
1438
+ throw new Error('usage: skillspub tui [--project [path]]');
1439
+ }
1440
+ await (await import("./tui.js")).runTui(home, { projectPath });
1441
+ }
1442
+ else
1443
+ switch (cmd) {
1444
+ case 'ls':
1445
+ data = cmdLs(home, rest, json);
1446
+ break;
1447
+ case 'on':
1448
+ data = cmdOnOff(home, true, rest, json);
1449
+ break;
1450
+ case 'off':
1451
+ data = cmdOnOff(home, false, rest, json);
1452
+ break;
1453
+ case 'status':
1454
+ data = cmdStatus(home, rest, json);
1455
+ break;
1456
+ case 'explain':
1457
+ data = cmdExplain(home, rest, undefined, json);
1458
+ break;
1459
+ case 'mirror':
1460
+ data = cmdMirror(home, rest, undefined, json);
1461
+ break;
1462
+ case 'bundle':
1463
+ data = cmdBundle(home, rest, json);
1464
+ break;
1465
+ case 'tag':
1466
+ data = cmdTag(home, rest, json);
1467
+ break;
1468
+ case 'preset':
1469
+ data = cmdPreset(home, rest, undefined, json);
1470
+ break;
1471
+ case 'shared':
1472
+ data = cmdShared(home, rest, undefined, json);
1473
+ break;
1474
+ case 'scan': {
1475
+ if (rest.length > 0)
1476
+ throw new Error('usage: skillspub scan');
1477
+ const report = scanGlobalInventory(home, undefined, { persist: false });
1478
+ if (json)
1479
+ data = { inventory: report, harnesses: inspectHarnesses(home, report.targets) };
1480
+ else {
1481
+ printScan(report);
1482
+ printHarnessDrift(home, report.targets);
1483
+ }
1484
+ break;
1485
+ }
1486
+ case 'doctor':
1487
+ data = cmdDoctor(home, rest, undefined, json);
1488
+ break;
1489
+ case 'project': {
1490
+ const [projectPath, projectCommand, ...projectArgs] = rest;
1491
+ if (!projectPath)
1492
+ throw new Error('usage: skillspub project <path> scan|doctor|explain|shared|preset|mirror|harnesses');
1493
+ if (projectCommand === 'scan' && projectArgs.length === 0) {
1494
+ const report = scanProjectInventory(home, projectPath, undefined, { persist: false });
1495
+ if (json)
1496
+ data = report;
1497
+ else
1498
+ printScan(report);
1499
+ }
1500
+ else if (projectCommand === 'doctor')
1501
+ data = cmdDoctor(home, projectArgs, projectPath, json);
1502
+ else if (projectCommand === 'explain')
1503
+ data = cmdExplain(home, projectArgs, projectPath, json);
1504
+ else if (projectCommand === 'shared')
1505
+ data = cmdShared(home, projectArgs, projectPath, json);
1506
+ else if (projectCommand === 'preset')
1507
+ data = cmdPreset(home, projectArgs, projectPath, json);
1508
+ else if (projectCommand === 'mirror')
1509
+ data = cmdMirror(home, projectArgs, projectPath, json);
1510
+ else if (projectCommand === 'harnesses')
1511
+ data = cmdHarnesses(home, projectArgs, projectPath, json);
1512
+ else
1513
+ throw new Error('usage: skillspub project <path> scan|doctor|explain|shared|preset|mirror|harnesses');
1514
+ break;
1515
+ }
1516
+ case 'targets':
1517
+ data = cmdTargets(home, rest, json);
1518
+ break;
1519
+ case 'harnesses':
1520
+ data = cmdHarnesses(home, rest, undefined, json);
1521
+ break;
1522
+ case 'migrate':
1523
+ data = cmdMigrate(home, rest, json);
1524
+ break;
1525
+ default:
1526
+ if (json)
1527
+ throw new Error(`usage: ${cmd ? `unknown command ${cmd}` : 'skillspub <command>'}`);
1528
+ process.stderr.write(USAGE);
1529
+ process.exitCode = cmd === undefined ? 0 : 2;
1530
+ return;
1531
+ }
1532
+ if (json)
1533
+ writeJson({ schemaVersion: 1, ok: true, data });
1534
+ }
1535
+ catch (err) {
1536
+ if (json) {
1537
+ const { code, exitCode, details } = errorInfo(err, mutation);
1538
+ writeJson({
1539
+ schemaVersion: 1,
1540
+ ok: false,
1541
+ error: {
1542
+ code,
1543
+ message: err.message,
1544
+ details,
1545
+ },
1546
+ });
1547
+ process.exitCode = exitCode;
1548
+ }
1549
+ else {
1550
+ console.error(`skillspub: ${err.message}`);
1551
+ const details = err.details;
1552
+ if (details?.finalTruth) {
1553
+ console.error('Final truth:');
1554
+ console.error(JSON.stringify(details.finalTruth, null, 2));
1555
+ }
1556
+ process.exitCode = errorInfo(err, mutation).exitCode;
1557
+ }
1558
+ }
1559
+ }
1560
+ if (process.argv[1] &&
1561
+ fs.realpathSync(process.argv[1]) === fileURLToPath(import.meta.url))
1562
+ await main();