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/LICENSE +21 -0
- package/README.md +86 -0
- package/dist/catalog.js +352 -0
- package/dist/cli.js +1562 -0
- package/dist/core.js +31 -0
- package/dist/explain.js +307 -0
- package/dist/harnesses/claude.js +95 -0
- package/dist/harnesses/grok.js +746 -0
- package/dist/harnesses/pi.js +1380 -0
- package/dist/harnesses/registry.js +30 -0
- package/dist/harnesses/target.js +5 -0
- package/dist/harnesses/types.js +1 -0
- package/dist/inventory.js +1437 -0
- package/dist/npx-skills.js +273 -0
- package/dist/reconcile.js +1014 -0
- package/dist/shared.js +1411 -0
- package/dist/source-verification.js +297 -0
- package/dist/targets/shared.js +13 -0
- package/dist/tui.js +2373 -0
- package/dist/view.js +321 -0
- package/package.json +51 -0
package/dist/tui.js
ADDED
|
@@ -0,0 +1,2373 @@
|
|
|
1
|
+
import { createElement as h, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { Box, Text, render, useApp, useInput, useStdout } from 'ink';
|
|
7
|
+
import wrapAnsi from 'wrap-ansi';
|
|
8
|
+
import { defaultHome, } from "./core.js";
|
|
9
|
+
import { searchRows, skillDetail, sortRows, harnessStatusBadge, projectTuiSnapshot, tuiSnapshot, tuiSnapshotInventory, } from "./view.js";
|
|
10
|
+
import { applyActivationPlan, planActivation, planLink, planMirrorAction, planToggle, planUnlink, } from "./reconcile.js";
|
|
11
|
+
import { scanGlobalInventory, scanProjectInventory, } from "./inventory.js";
|
|
12
|
+
import { addPresetSelectors, addResourceTags, createPreset, removePresetSelectors, removeResourceTags, } from "./catalog.js";
|
|
13
|
+
import { explainVisibility, explainVisibilityFromInventory, } from "./explain.js";
|
|
14
|
+
import { planSharedAdd, planSharedRemove, planSharedUpdate, sharedAdd, sharedFind, sharedRemove, sharedRemoveCascade, sharedRefresh, sharedOperationLockPath, sharedUpdate, } from "./shared.js";
|
|
15
|
+
import { normalizeNpxSkillsName, } from "./npx-skills.js";
|
|
16
|
+
import { catalogCandidateTruth, relationshipStatusText, sourceDesiredTruth, sourceInventoryRows, sourceMirrorState, sourceRelationships, verifySourceMutation, } from "./source-verification.js";
|
|
17
|
+
/** Below this width the passive summary column is hidden. */
|
|
18
|
+
const WIDE_MIN = 80;
|
|
19
|
+
const MANAGED_SUPPORT_EXPLANATION = 'Managed support: verified Adapter can control and explain this Harness; it does not mean optional setup/reconcile was applied.';
|
|
20
|
+
const SOURCE_REFRESH_CHILD = '--skillspub-source-refresh-child';
|
|
21
|
+
if (process.argv[2] === SOURCE_REFRESH_CHILD) {
|
|
22
|
+
let response;
|
|
23
|
+
try {
|
|
24
|
+
const home = JSON.parse(process.argv[3] ?? '');
|
|
25
|
+
response = { result: { availability: sharedRefresh(home), snapshot: tuiSnapshot(home) } };
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
response = { error: error.message };
|
|
29
|
+
process.exitCode = 1;
|
|
30
|
+
}
|
|
31
|
+
process.stdout.write(JSON.stringify(response));
|
|
32
|
+
}
|
|
33
|
+
function removeOwnedOperationLock(lock, pid) {
|
|
34
|
+
try {
|
|
35
|
+
if (fs.readFileSync(lock, 'utf8') === `${pid}\n`)
|
|
36
|
+
fs.rmSync(lock, { force: true });
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
if (error.code !== 'ENOENT')
|
|
40
|
+
process.emitWarning(`Could not remove Source refresh lock: ${error.message}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function startSourceRefresh(home) {
|
|
44
|
+
const operationLock = sharedOperationLockPath(home);
|
|
45
|
+
let child;
|
|
46
|
+
try {
|
|
47
|
+
child = spawn(process.execPath, [
|
|
48
|
+
fileURLToPath(import.meta.url),
|
|
49
|
+
SOURCE_REFRESH_CHILD,
|
|
50
|
+
JSON.stringify(home),
|
|
51
|
+
], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
throw new Error(`Source refresh failed to start: ${error.message}`);
|
|
55
|
+
}
|
|
56
|
+
if (!child.pid)
|
|
57
|
+
throw new Error('Source refresh failed to start');
|
|
58
|
+
const childPid = child.pid;
|
|
59
|
+
const result = new Promise((resolve, reject) => {
|
|
60
|
+
let stdout = '';
|
|
61
|
+
let stderr = '';
|
|
62
|
+
child.stdout?.setEncoding('utf8').on('data', (chunk) => { stdout += chunk; });
|
|
63
|
+
child.stderr?.setEncoding('utf8').on('data', (chunk) => { stderr += chunk; });
|
|
64
|
+
child.once('error', reject);
|
|
65
|
+
child.once('close', (code) => {
|
|
66
|
+
try {
|
|
67
|
+
const response = JSON.parse(stdout);
|
|
68
|
+
if (response.result)
|
|
69
|
+
resolve(response.result);
|
|
70
|
+
else
|
|
71
|
+
reject(new Error((response.error ?? stderr.trim()) || `Source refresh exited with code ${code}`));
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
reject(new Error(stderr.trim() || error.message));
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
return { child, childPid, result, operationLock };
|
|
79
|
+
}
|
|
80
|
+
/** Existing relationships of one target, in inventory order (absent skills excluded). */
|
|
81
|
+
function entriesFor(rows, targetName) {
|
|
82
|
+
return rows.flatMap((row) => row.relationships
|
|
83
|
+
.filter((relationship) => relationship.target === targetName)
|
|
84
|
+
.map((relationship) => ({
|
|
85
|
+
row,
|
|
86
|
+
relationship,
|
|
87
|
+
key: relationship.info.path,
|
|
88
|
+
})));
|
|
89
|
+
}
|
|
90
|
+
/** Fixed-width status column so skill names align; deadlink gets '!' (red + target suffix carry the rest). */
|
|
91
|
+
function statusText(info) {
|
|
92
|
+
const form = info.mirrored ? 'mirror' : info.linked ? 'link' : 'local';
|
|
93
|
+
const base = `${info.underOff ? '[ OFF ]' : '[ ON ]'} ${form}`;
|
|
94
|
+
const text = info.presence === 'deadlink' ? `${base}!` : info.diverged ? `${base}!` : base;
|
|
95
|
+
return text.padEnd('[ OFF ] local'.length);
|
|
96
|
+
}
|
|
97
|
+
/** Mark identity that survives on/off moves: configDir-relative path with the off-parking prefix stripped. */
|
|
98
|
+
function markKey(configDir, realPath) {
|
|
99
|
+
return path.relative(configDir, realPath).replace(/^\.skillspub-off\//, '');
|
|
100
|
+
}
|
|
101
|
+
function statusColor(info) {
|
|
102
|
+
if (info.presence === 'deadlink')
|
|
103
|
+
return 'red';
|
|
104
|
+
return info.presence === 'on' ? 'green' : 'yellow';
|
|
105
|
+
}
|
|
106
|
+
function updateStatusText(row) {
|
|
107
|
+
const update = row.updateAvailability;
|
|
108
|
+
if (!update)
|
|
109
|
+
return undefined;
|
|
110
|
+
if (update.status === 'check-failed' && update.error?.includes('installer lock lacks'))
|
|
111
|
+
return 'not checkable';
|
|
112
|
+
if (update.status === 'check-failed' && /ETIMEDOUT|timed out/i.test(update.error ?? ''))
|
|
113
|
+
return 'check timeout';
|
|
114
|
+
return update.status;
|
|
115
|
+
}
|
|
116
|
+
function updateText(row) {
|
|
117
|
+
const status = updateStatusText(row);
|
|
118
|
+
if (!status)
|
|
119
|
+
return undefined;
|
|
120
|
+
return `${status}${row.updateAvailability?.checkedAt ? ` @ ${row.updateAvailability.checkedAt}` : ''}`;
|
|
121
|
+
}
|
|
122
|
+
function updateColor(row) {
|
|
123
|
+
const status = row.updateAvailability?.status;
|
|
124
|
+
if (status === 'current')
|
|
125
|
+
return 'green';
|
|
126
|
+
if (status === 'available' || updateStatusText(row) === 'not checkable')
|
|
127
|
+
return 'yellow';
|
|
128
|
+
if (status === 'check-failed' || status === 'upstream-missing')
|
|
129
|
+
return 'red';
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
function statusSortValue(info) {
|
|
133
|
+
if (!info)
|
|
134
|
+
return '3:missing';
|
|
135
|
+
const rank = info.presence === 'on' ? '0' : info.presence === 'off' ? '1' : '2';
|
|
136
|
+
return `${rank}:${info.mirrored ? 'mirror' : info.linked ? 'link' : 'local'}`;
|
|
137
|
+
}
|
|
138
|
+
function sortLabel(sort) {
|
|
139
|
+
return sort[0].toUpperCase() + sort.slice(1);
|
|
140
|
+
}
|
|
141
|
+
function inheritedOn(info) {
|
|
142
|
+
return Boolean(info?.readOnly && info.presence === 'on');
|
|
143
|
+
}
|
|
144
|
+
/** Visible window [start, start+height) that keeps `selected` on screen. */
|
|
145
|
+
function windowStart(length, selected, height) {
|
|
146
|
+
return Math.max(0, Math.min(selected - Math.floor(height / 2), length - height));
|
|
147
|
+
}
|
|
148
|
+
function ListColumn({ title, focused, width, flexGrow, children, }) {
|
|
149
|
+
return h(Box, {
|
|
150
|
+
flexDirection: 'column',
|
|
151
|
+
width,
|
|
152
|
+
flexGrow,
|
|
153
|
+
flexShrink: width === undefined ? 1 : 0,
|
|
154
|
+
borderStyle: 'single',
|
|
155
|
+
borderColor: focused ? 'cyan' : 'gray',
|
|
156
|
+
}, h(Text, { bold: focused, color: focused ? 'cyan' : undefined }, ` ${title}`), children);
|
|
157
|
+
}
|
|
158
|
+
function RowLine({ active, focused, marked, wrap = 'truncate-end', children, }) {
|
|
159
|
+
return h(Text, { inverse: active && focused, bold: (active && !focused) || marked, wrap }, `${active ? '›' : marked ? '●' : ' '} `, children);
|
|
160
|
+
}
|
|
161
|
+
export function HarnessBadge({ harness, compact = false, }) {
|
|
162
|
+
const badge = harnessStatusBadge(harness);
|
|
163
|
+
const text = compact && harness.support === 'discoverable' ? '[discover]' : badge.text;
|
|
164
|
+
return h(Text, {
|
|
165
|
+
color: badge.tone === 'success' ? 'green' : badge.tone === 'danger' ? 'red' : badge.tone === 'warning' ? 'yellow' : undefined,
|
|
166
|
+
dimColor: badge.tone === 'muted',
|
|
167
|
+
}, ` ${text}`);
|
|
168
|
+
}
|
|
169
|
+
function HarnessRow({ harness }) {
|
|
170
|
+
return h(Box, { width: '100%' }, h(Box, { flexGrow: 1, flexShrink: 1 }, h(Text, { dimColor: true, wrap: 'truncate-end' }, ` ${harness.name}`)), h(Box, { flexShrink: 0 }, h(HarnessBadge, { harness, compact: true })));
|
|
171
|
+
}
|
|
172
|
+
function TargetList({ targets, harnesses, pendingTargetKeys, selected, focused, maxWidth, height, }) {
|
|
173
|
+
const start = windowStart(targets.length, selected, height);
|
|
174
|
+
const detected = new Map(harnesses.detected.map((harness) => [harness.key, harness]));
|
|
175
|
+
const pendingKeys = new Set(pendingTargetKeys);
|
|
176
|
+
const pending = harnesses.detected.filter(({ key }) => pendingKeys.has(key));
|
|
177
|
+
const rowText = (name, harness) => ` ${name}${harness ? ` ${harnessStatusBadge(harness).text}` : ''}`;
|
|
178
|
+
const rows = [
|
|
179
|
+
...targets.map(({ name }) => rowText(name, detected.get(name))),
|
|
180
|
+
...(pending.length === 0 ? [] : [' Pending migration']),
|
|
181
|
+
...(harnesses.available.length === 0 ? [] : [' Available']),
|
|
182
|
+
...[...pending, ...harnesses.available].map((harness) => rowText(harness.name, harness)),
|
|
183
|
+
];
|
|
184
|
+
const width = Math.min(maxWidth, 32, Math.max(18, ...rows.map((row) => row.length + 2)));
|
|
185
|
+
return h(ListColumn, { title: 'Targets', focused, width }, ...targets.slice(start, start + height).map((target, index) => {
|
|
186
|
+
const harness = detected.get(target.name);
|
|
187
|
+
return h(RowLine, { key: `target:${target.name}`, active: start + index === selected, focused }, target.name, harness ? h(HarnessBadge, { harness }) : null);
|
|
188
|
+
}), ...(pending.length === 0
|
|
189
|
+
? []
|
|
190
|
+
: [
|
|
191
|
+
h(Text, { key: 'pending-migration', dimColor: true, wrap: 'truncate-end' }, ' Pending migration'),
|
|
192
|
+
...pending.map((harness) => h(HarnessRow, { key: `pending:${harness.key}`, harness })),
|
|
193
|
+
]), ...(harnesses.available.length === 0
|
|
194
|
+
? []
|
|
195
|
+
: [
|
|
196
|
+
h(Text, { key: 'available', dimColor: true, wrap: 'truncate-end' }, ' Available'),
|
|
197
|
+
...harnesses.available.map((harness) => h(HarnessRow, { key: `available:${harness.key}`, harness })),
|
|
198
|
+
]));
|
|
199
|
+
}
|
|
200
|
+
function RelationshipList({ entries, selected, focused, height, marks, showScope, }) {
|
|
201
|
+
const start = windowStart(entries.length, selected, height);
|
|
202
|
+
return h(ListColumn, { title: 'Relationships', focused, flexGrow: 1 }, ...entries.slice(start, start + height).map((entry, index) => {
|
|
203
|
+
const info = entry.relationship.info;
|
|
204
|
+
const active = start + index === selected;
|
|
205
|
+
return h(RowLine, { key: entry.key, active, focused, marked: marks?.has(entry.row.id) }, h(Text, { color: statusColor(info) }, statusText(info)), ' ', entry.relationship.name === entry.row.name
|
|
206
|
+
? entry.row.displayName
|
|
207
|
+
: `${entry.relationship.name} → ${entry.row.displayName}`, info.presence === 'deadlink' && info.target
|
|
208
|
+
? h(Text, { dimColor: true }, ` -> ${info.target}`)
|
|
209
|
+
: null, showScope && entry.relationship.scope && entry.relationship.scope !== 'project'
|
|
210
|
+
? h(Text, { dimColor: true }, ` ·${entry.relationship.scope}`)
|
|
211
|
+
: null, updateText(entry.row)
|
|
212
|
+
? h(Text, { color: updateColor(entry.row) }, ` · ${updateText(entry.row)}`)
|
|
213
|
+
: null);
|
|
214
|
+
}), entries.length === 0
|
|
215
|
+
? h(Text, { dimColor: true }, ' no skills for this target')
|
|
216
|
+
: null);
|
|
217
|
+
}
|
|
218
|
+
/** Skill tab: every live skill instance/variant, one row each. */
|
|
219
|
+
function InstanceList({ rows, selected, focused, width, height, marks, }) {
|
|
220
|
+
const start = windowStart(rows.length, selected, height);
|
|
221
|
+
return h(ListColumn, { title: 'Skills', focused, width }, ...rows.slice(start, start + height).map((row, index) => h(RowLine, {
|
|
222
|
+
key: row.id,
|
|
223
|
+
active: start + index === selected,
|
|
224
|
+
focused,
|
|
225
|
+
marked: marks?.has(row.id),
|
|
226
|
+
wrap: 'wrap',
|
|
227
|
+
}, row.displayName, updateText(row)
|
|
228
|
+
? h(Text, { color: updateColor(row) }, ` · ${updateText(row)}`)
|
|
229
|
+
: null)), rows.length === 0
|
|
230
|
+
? h(Text, { dimColor: true }, ' no skills on disk')
|
|
231
|
+
: null);
|
|
232
|
+
}
|
|
233
|
+
/** Skill tab: every target in registry order with its state for the selected instance. */
|
|
234
|
+
function TargetStatusList({ targets, row, selected, focused, width, height, }) {
|
|
235
|
+
const start = windowStart(targets.length, selected, height);
|
|
236
|
+
return h(ListColumn, { title: 'Targets', focused, width }, ...targets.slice(start, start + height).map((target, index) => {
|
|
237
|
+
const info = row?.targets[target.name];
|
|
238
|
+
return h(RowLine, { key: target.name, active: start + index === selected, focused }, `${target.name} `, info
|
|
239
|
+
? h(Text, { color: statusColor(info) }, statusText(info))
|
|
240
|
+
: h(Text, { dimColor: true }, 'missing'));
|
|
241
|
+
}));
|
|
242
|
+
}
|
|
243
|
+
function TargetInfoPanel({ target, harness, width, }) {
|
|
244
|
+
const rows = [
|
|
245
|
+
h(Text, { key: 'target-name', bold: true, wrap: 'wrap' }, ` ${target.name}`),
|
|
246
|
+
h(Text, { key: 'target-gap' }, ''),
|
|
247
|
+
h(Text, { key: 'type' }, ' ', h(Text, { bold: true }, 'Type:'), ' Skill Target'),
|
|
248
|
+
h(Text, { key: 'path', wrap: 'wrap' }, ' ', h(Text, { bold: true }, 'Path:'), ` ${target.dir}`),
|
|
249
|
+
];
|
|
250
|
+
if (harness) {
|
|
251
|
+
rows.push(h(Text, { key: 'harness-gap' }, ''), h(Text, { key: 'harness' }, ' ', h(Text, { bold: true, color: 'cyan' }, 'Harness:'), ` ${harness.name}`), h(Text, { key: 'detected' }, ' ', h(Text, { bold: true }, 'Detected:'), ` ${harness.detected ? 'yes' : 'no'}`), h(Text, { key: 'support' }, ' ', h(Text, { bold: true }, 'Adapter support:'), ` ${harness.support}`), h(Text, { key: 'shared' }, ' ', h(Text, { bold: true }, 'Shared consumption:'), ` ${harness.sharedConsumption.status}`), h(Text, { key: 'isolation' }, ' ', h(Text, { bold: true }, 'Isolation:'), ` ${harness.isolation.status}`), ...(harness.support === 'managed'
|
|
252
|
+
? [h(Text, { key: 'support-explanation', wrap: 'wrap' }, ` ${MANAGED_SUPPORT_EXPLANATION}`)]
|
|
253
|
+
: []), h(Text, { key: 'link' }, ' ', h(Text, { bold: true }, 'Link:'), ` ${harness.link.supported ? 'supported' : 'unsupported'}`), ...(harness.mirror
|
|
254
|
+
? [h(Text, { key: 'mirror' }, ' ', h(Text, { bold: true }, 'Mirror:'), ` ${harness.mirror.supported ? 'supported' : 'unsupported'}`)]
|
|
255
|
+
: []));
|
|
256
|
+
}
|
|
257
|
+
return h(ListColumn, { title: 'Info', focused: false, width }, ...rows);
|
|
258
|
+
}
|
|
259
|
+
function InfoPanel({ row, info, membership, visibility, width, }) {
|
|
260
|
+
const inner = Math.max(8, width - 2); // column borders
|
|
261
|
+
/** OSC 8 terminal hyperlink: every wrapped line maps to the full URL, so
|
|
262
|
+
* cmd+click never opens a truncated first-line fragment. */
|
|
263
|
+
const osc8 = (href, text) => `\x1b]8;;${href}\x07${text}\x1b]8;;\x07`;
|
|
264
|
+
/** Pre-wrap a `Label: value` row; continuation lines align with the label, and
|
|
265
|
+
* unbroken strings (paths) hard-wrap inside the panel instead of overflowing it. */
|
|
266
|
+
const labeled = (label, value, color, href) => {
|
|
267
|
+
const text = `${label}: ${value && value.length > 0 ? value : '—'}`;
|
|
268
|
+
const lines = wrapAnsi(text, Math.max(4, inner - 1), {
|
|
269
|
+
wordWrap: true,
|
|
270
|
+
trim: true,
|
|
271
|
+
hard: true,
|
|
272
|
+
}).split('\n');
|
|
273
|
+
const linkify = (part) => href ? osc8(href, part) : part;
|
|
274
|
+
return lines.map((part, index) => {
|
|
275
|
+
if (index === 0 && part.startsWith(`${label}:`)) {
|
|
276
|
+
return h(Text, { key: label }, ' ', h(Text, { bold: true, color }, `${label}:`), linkify(part.slice(label.length + 1)));
|
|
277
|
+
}
|
|
278
|
+
return h(Text, { key: `${label}-${index}` }, ` ${linkify(part)}`);
|
|
279
|
+
});
|
|
280
|
+
};
|
|
281
|
+
return h(ListColumn, { title: 'Info', focused: false, width }, row
|
|
282
|
+
? [
|
|
283
|
+
h(Text, { key: 'name', bold: true, wrap: 'wrap' }, ` ${row.displayName}`),
|
|
284
|
+
h(Text, { key: 'gap-top' }, ''),
|
|
285
|
+
...labeled('Description', row.description),
|
|
286
|
+
...labeled('Source', row.provenance.sourceUrl
|
|
287
|
+
? row.sourceLabel.replace(/^(?:git\+)?https?:\/\//, '')
|
|
288
|
+
: row.sourceLabel, undefined, row.provenance.sourceUrl),
|
|
289
|
+
...labeled('Path', row.realPath ?? (info ? `${info.path}${info.target ? ` -> ${info.target}` : ''}` : undefined)),
|
|
290
|
+
h(Text, { key: 'gap-cat' }, ''),
|
|
291
|
+
...labeled('Bundles', membership?.bundles.join(', '), 'cyan'),
|
|
292
|
+
...labeled('Tags', membership?.tags.join(', '), 'green'),
|
|
293
|
+
...labeled('Presets', membership?.presets.join(', '), 'magenta'),
|
|
294
|
+
h(Text, { key: 'gap-update' }, ''),
|
|
295
|
+
...labeled('Update availability', updateStatusText(row), updateColor(row)),
|
|
296
|
+
...labeled('Checked at', row.updateAvailability?.checkedAt),
|
|
297
|
+
...(row.updateAvailability?.error
|
|
298
|
+
? labeled('Update error', row.updateAvailability.error, 'red')
|
|
299
|
+
: []),
|
|
300
|
+
h(Text, { key: 'gap-visibility' }, ''),
|
|
301
|
+
h(Text, { key: 'visibility', bold: true, color: 'cyan' }, ' Effective Visibility'),
|
|
302
|
+
...(visibility?.harnesses.map((harness) => h(Text, { key: `visibility-${harness.key}`, wrap: 'wrap' }, ` ${harness.name}: ${harness.effectiveVisibility}${harness.detected ? '' : ' · not-detected'}`)) ?? []),
|
|
303
|
+
]
|
|
304
|
+
: h(Text, { dimColor: true }, ' nothing selected'));
|
|
305
|
+
}
|
|
306
|
+
function BatchActivationModal({ confirm }) {
|
|
307
|
+
const lines = confirm.plans.flatMap((plan) => plan.targets.map((target) => ` ${target.targetId}/${target.slot} ${target.from} -> ${target.to}`));
|
|
308
|
+
return h(Box, { flexGrow: 1, flexDirection: 'column', borderStyle: 'round', borderColor: 'yellow', paddingX: 1, justifyContent: 'center' }, h(Text, { bold: true }, `Batch ${confirm.intent} @ ${confirm.targetName}?`), ...confirm.errors.map((error, index) => h(Text, { key: `err-${index}`, color: 'red' }, ` ${error}`)), ...lines.slice(0, 12).map((line, index) => h(Text, { key: `line-${index}` }, line)), lines.length > 12 ? h(Text, { dimColor: true }, ` … ${lines.length - 12} more`) : null, h(Text, { color: 'yellow' }, ' y confirm n/esc cancel '));
|
|
309
|
+
}
|
|
310
|
+
function knownTagNames(tags) {
|
|
311
|
+
return [...new Set(Object.values(tags).flat())].sort((a, b) => a.localeCompare(b));
|
|
312
|
+
}
|
|
313
|
+
function ManageModal({ row, catalog, bundles, manage, }) {
|
|
314
|
+
const assigned = new Set(catalog.tags[row.id] ?? []);
|
|
315
|
+
const tagNames = knownTagNames(catalog.tags);
|
|
316
|
+
const selector = `skill:${row.id}`;
|
|
317
|
+
const presetNames = Object.keys(catalog.presets).sort((a, b) => a.localeCompare(b));
|
|
318
|
+
const active = (section, index) => manage.section === section && manage.index === index;
|
|
319
|
+
const marker = (on) => (on ? '›' : ' ');
|
|
320
|
+
const sectionTitle = (title, on) => h(Text, { bold: on, color: on ? 'cyan' : undefined }, ` ${title}`);
|
|
321
|
+
return h(Box, { flexGrow: 1, flexDirection: 'column', borderStyle: 'round', borderColor: 'cyan', paddingX: 1 }, h(Text, { bold: true, wrap: 'truncate-end' }, ` Manage: ${row.displayName}`), h(Text, { dimColor: true, wrap: 'truncate-end' }, ` Bundles: ${bundles.length > 0 ? bundles.join(', ') : '—'}`), h(Text, null, ''), sectionTitle('Tags', manage.section === 'tags'), ...tagNames.map((tag, index) => h(Text, { key: `tag-${tag}`, inverse: active('tags', index) }, `${marker(active('tags', index))} [${assigned.has(tag) ? 'x' : ' '}] ${tag}`)), h(Text, { key: 'tag-add', inverse: active('tags', tagNames.length) }, `${marker(active('tags', tagNames.length))} + add tag`), h(Text, null, ''), sectionTitle('Presets', manage.section === 'presets'), ...presetNames.map((name, index) => {
|
|
322
|
+
const member = (catalog.presets[name]?.selectors ?? []).includes(selector);
|
|
323
|
+
return h(Text, { key: `preset-${name}`, inverse: active('presets', index) }, `${marker(active('presets', index))} [${member ? 'x' : ' '}] ${name}`);
|
|
324
|
+
}), h(Text, { key: 'preset-add', inverse: active('presets', presetNames.length) }, `${marker(active('presets', presetNames.length))} + new preset`), manage.input
|
|
325
|
+
? h(Text, null, ` ${manage.input.kind === 'tag' ? 'tag' : 'preset'} name: ${manage.input.value}`)
|
|
326
|
+
: null);
|
|
327
|
+
}
|
|
328
|
+
function ConfirmationModal({ confirmation, }) {
|
|
329
|
+
return h(Box, {
|
|
330
|
+
flexGrow: 1,
|
|
331
|
+
flexDirection: 'column',
|
|
332
|
+
borderStyle: 'round',
|
|
333
|
+
borderColor: 'yellow',
|
|
334
|
+
paddingX: 1,
|
|
335
|
+
justifyContent: 'center',
|
|
336
|
+
}, h(Text, { bold: true }, `${confirmation.kind === 'link' ? 'Link' : confirmation.kind === 'unlink' ? 'Unlink' : confirmation.kind.replace('mirror-', 'Mirror ')} relationship?`), h(Text, { wrap: 'wrap' }, ` ${confirmation.source} → ${confirmation.destination}`), h(Text, { color: 'yellow' }, ' y confirm n/esc cancel '));
|
|
337
|
+
}
|
|
338
|
+
function detailLines(content, width) {
|
|
339
|
+
return wrapAnsi(content, width, { hard: true, trim: false, wordWrap: false }).split('\n');
|
|
340
|
+
}
|
|
341
|
+
function DetailModal({ row, lines, scroll, height, }) {
|
|
342
|
+
// header + footer + modal chrome/title leave this many content rows
|
|
343
|
+
const viewHeight = Math.max(1, height - 8);
|
|
344
|
+
const visible = lines.slice(scroll, scroll + viewHeight);
|
|
345
|
+
return h(Box, {
|
|
346
|
+
flexGrow: 1,
|
|
347
|
+
flexDirection: 'column',
|
|
348
|
+
borderStyle: 'round',
|
|
349
|
+
borderColor: 'cyan',
|
|
350
|
+
paddingX: 1,
|
|
351
|
+
overflow: 'hidden',
|
|
352
|
+
}, h(Text, { bold: true, wrap: 'truncate-end' }, `SKILL.md — ${row.displayName} [${Math.min(scroll + 1, lines.length)}/${lines.length}]`), ...visible.map((line, index) => h(Text, { key: scroll + index, wrap: 'truncate-end' }, line || ' ')));
|
|
353
|
+
}
|
|
354
|
+
function mutableSourceRelationship(row) {
|
|
355
|
+
return sourceRelationships(row).find(({ info }) => info.form === 'local' && !info.readOnly && info.scope === 'global');
|
|
356
|
+
}
|
|
357
|
+
function sameSourceIntent(original, fresh) {
|
|
358
|
+
const originalEffects = (original.relationshipEffects ?? []).map((effect) => ({
|
|
359
|
+
...effect,
|
|
360
|
+
plannedAction: 'preserve-intent',
|
|
361
|
+
}));
|
|
362
|
+
const freshEffects = (fresh.relationshipEffects ?? []).map((effect) => ({
|
|
363
|
+
...effect,
|
|
364
|
+
plannedAction: 'preserve-intent',
|
|
365
|
+
}));
|
|
366
|
+
const sourceStillExpected = fresh.replacement
|
|
367
|
+
? fresh.replacement.from === original.replacement?.from
|
|
368
|
+
: fresh.currentSource === original.source || (!fresh.currentSource && !original.currentSource);
|
|
369
|
+
return fresh.targetId === original.targetId &&
|
|
370
|
+
fresh.candidate?.identity === original.candidate?.identity &&
|
|
371
|
+
JSON.stringify(fresh.slots) === JSON.stringify(original.slots) &&
|
|
372
|
+
JSON.stringify(fresh.scope) === JSON.stringify(original.scope) &&
|
|
373
|
+
JSON.stringify(fresh.intentPreservation) === JSON.stringify(original.intentPreservation) &&
|
|
374
|
+
JSON.stringify(freshEffects) === JSON.stringify(originalEffects) &&
|
|
375
|
+
(fresh.blockers?.length ?? 0) === 0 &&
|
|
376
|
+
sourceStillExpected;
|
|
377
|
+
}
|
|
378
|
+
function candidateId(candidate) {
|
|
379
|
+
return `${candidate.source}\0${candidate.name}`;
|
|
380
|
+
}
|
|
381
|
+
function sourceDetailLines(candidate, row) {
|
|
382
|
+
if (candidate)
|
|
383
|
+
return [
|
|
384
|
+
`Identity: ${candidate.source}@${candidate.name}`,
|
|
385
|
+
`Source: ${candidate.source}`,
|
|
386
|
+
`Skill path/name: ${candidate.name}`,
|
|
387
|
+
`Destination Slot: ${normalizeNpxSkillsName(candidate.name)}`,
|
|
388
|
+
`Installs: ${candidate.installs ?? 'unknown'}`,
|
|
389
|
+
`Detail: ${candidate.detailUrl}`,
|
|
390
|
+
];
|
|
391
|
+
if (!row)
|
|
392
|
+
return ['No resource selected.'];
|
|
393
|
+
const relationships = sourceRelationships(row);
|
|
394
|
+
return [
|
|
395
|
+
`Identity: ${row.realPath ?? row.id}`,
|
|
396
|
+
`Name: ${row.name}`,
|
|
397
|
+
`Provenance: ${row.sourceLabel}`,
|
|
398
|
+
`Real path: ${row.realPath ?? 'unresolved'}`,
|
|
399
|
+
`Update availability: ${updateStatusText(row) ?? 'unknown'}`,
|
|
400
|
+
...(row.updateAvailability?.error ? [`Update check: ${row.updateAvailability.error}`] : []),
|
|
401
|
+
...relationships.map((relationship) => `${relationship.readOnly ? 'Read-only inherited' : relationship.info.form} ${relationship.scope ?? 'global'}: ${relationship.info.path}`),
|
|
402
|
+
];
|
|
403
|
+
}
|
|
404
|
+
function SourceDetail({ title, lines, bordered = false, wrap = 'truncate-end', }) {
|
|
405
|
+
return h(Box, {
|
|
406
|
+
flexDirection: 'column',
|
|
407
|
+
flexGrow: 1,
|
|
408
|
+
...(bordered ? { borderStyle: 'round', borderColor: 'cyan', paddingX: 1 } : {}),
|
|
409
|
+
}, h(Text, { bold: true }, title), ...lines.map((line, index) => h(Text, { key: index, wrap }, line)));
|
|
410
|
+
}
|
|
411
|
+
function relationshipEffectLines(effects) {
|
|
412
|
+
const lines = [];
|
|
413
|
+
let group = '';
|
|
414
|
+
for (const effect of effects) {
|
|
415
|
+
const nextGroup = `${effect.scope}\0${effect.targetId}`;
|
|
416
|
+
if (nextGroup !== group) {
|
|
417
|
+
lines.push(`Scope ${effect.scope} · Skill Target ${effect.targetKey} (${effect.targetId})`);
|
|
418
|
+
group = nextGroup;
|
|
419
|
+
}
|
|
420
|
+
lines.push(`${effect.plannedAction}: ${effect.form}/${effect.activation} ` +
|
|
421
|
+
`source=${effect.sourcePath} target=${effect.targetPath}`);
|
|
422
|
+
}
|
|
423
|
+
return lines;
|
|
424
|
+
}
|
|
425
|
+
function sourceOperationHint(operation) {
|
|
426
|
+
switch (operation.phase) {
|
|
427
|
+
case 'preview': {
|
|
428
|
+
const blocked = operation.kind === 'update'
|
|
429
|
+
? operation.plan.blockers.length > 0 || !operation.plan.items.some(({ included }) => included)
|
|
430
|
+
: (operation.plan.blockers?.length ?? 0) > 0;
|
|
431
|
+
return blocked
|
|
432
|
+
? ' ↑↓/j/k scroll l full log/evidence blocked — esc cancel '
|
|
433
|
+
: ' ↑↓/j/k scroll l full log/evidence enter continue esc cancel ';
|
|
434
|
+
}
|
|
435
|
+
case 'confirm':
|
|
436
|
+
return operation.kind === 'remove'
|
|
437
|
+
? ' ↑↓/j/k scroll enter confirm cascade esc cancel '
|
|
438
|
+
: ' ↑↓/j/k scroll enter confirm esc cancel ';
|
|
439
|
+
case 'source-confirm':
|
|
440
|
+
return ' ↑↓/j/k scroll l log/evidence enter confirm source deletion esc keep source ';
|
|
441
|
+
case 'run':
|
|
442
|
+
return ' running — scope switching and unrelated mutations disabled ';
|
|
443
|
+
case 'verify':
|
|
444
|
+
return ' ↑↓/j/k scroll l full log/evidence enter acknowledge Verify truth ';
|
|
445
|
+
case 'failed':
|
|
446
|
+
return ' ↑↓/j/k scroll l full log/evidence t retry esc acknowledge remaining Drift ';
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
function sourceOperationLines(operation) {
|
|
450
|
+
const timeline = `Timeline: ${operation.steps.map(({ name, status }) => `${name} ${status}`).join(' → ')}`;
|
|
451
|
+
let states = 'States: queued · running · failed · skipped';
|
|
452
|
+
if (operation.phase === 'preview' || operation.phase === 'confirm')
|
|
453
|
+
states = 'States: queued';
|
|
454
|
+
else if (operation.phase === 'run')
|
|
455
|
+
states = 'States: queued · running';
|
|
456
|
+
else if (operation.phase === 'verify')
|
|
457
|
+
states = 'States: queued · running · succeeded';
|
|
458
|
+
if (operation.kind === 'remove') {
|
|
459
|
+
const plan = operation.plan;
|
|
460
|
+
const scope = plan.scope.kind === 'project' ? 'exact Project' : 'Global';
|
|
461
|
+
const dependencies = plan.dependencies;
|
|
462
|
+
if (operation.phase === 'preview')
|
|
463
|
+
return {
|
|
464
|
+
title: 'Source Remove plan',
|
|
465
|
+
lines: [
|
|
466
|
+
states,
|
|
467
|
+
timeline,
|
|
468
|
+
`Scope: ${scope} — ${plan.scope.path}`,
|
|
469
|
+
`Source: ${plan.source.provenance} name=${plan.source.name}`,
|
|
470
|
+
`Shared Slot: ${plan.source.slot}`,
|
|
471
|
+
`Source path: ${plan.source.path}`,
|
|
472
|
+
`Source fingerprint: ${plan.source.fingerprint}`,
|
|
473
|
+
`Source Adapter: ${plan.sourceAdapter.package}`,
|
|
474
|
+
`Permissions: source=${plan.preconditions.permissions.source} state=${plan.preconditions.permissions.state} lock=${plan.preconditions.permissions.lock}`,
|
|
475
|
+
...plan.preconditions.permissions.dependencies.map(({ path: dependencyPath, status }) => `Dependency permission: ${status} ${dependencyPath}`),
|
|
476
|
+
...plan.selection.included.map(({ identity, reason }) => `Included: ${identity} — ${reason}`),
|
|
477
|
+
...plan.selection.excluded.map(({ identity, reason }) => `Excluded: ${identity} — ${reason}`),
|
|
478
|
+
`Relationship effects: ${dependencies.length}`,
|
|
479
|
+
...dependencies.map((dependency) => `${dependency.targetKey} (${dependency.targetId}) ${dependency.form}/${dependency.activation} ` +
|
|
480
|
+
`source=${dependency.source} target=${dependency.path} action=${dependency.plannedAction} ` +
|
|
481
|
+
`fingerprint=${dependency.fingerprint}`),
|
|
482
|
+
`Blockers: ${plan.blockers.join('; ') || 'none'}`,
|
|
483
|
+
...plan.warnings.map((warning) => `Warning: ${warning}`),
|
|
484
|
+
`Recovery: manifest=${plan.recovery.manifest}; ${plan.recovery.evidence.join(', ')}`,
|
|
485
|
+
`Current Actual: ${plan.currentTruth.actual}; Desired=${plan.currentTruth.desired}; Drift=${plan.currentTruth.drift}`,
|
|
486
|
+
`Expected final truth: ${plan.expectedFinalTruth.actual}; Desired=${plan.expectedFinalTruth.desired}; Drift=${plan.expectedFinalTruth.drift}`,
|
|
487
|
+
],
|
|
488
|
+
};
|
|
489
|
+
if (operation.phase === 'confirm')
|
|
490
|
+
return {
|
|
491
|
+
title: 'Source removal — cascade confirmation',
|
|
492
|
+
lines: [
|
|
493
|
+
'Confirm complete Relationship cascade.',
|
|
494
|
+
`Source remains: ${plan.source.path}`,
|
|
495
|
+
`Dependencies to delete: ${dependencies.length}`,
|
|
496
|
+
...dependencies.map(({ targetId, slot, form, activation, path: dependencyPath }) => `${targetId}/${slot} ${form}/${activation} ${dependencyPath}`),
|
|
497
|
+
'Vercel skills source deletion will require a separate confirmation.',
|
|
498
|
+
'Scope switching and unrelated mutations are disabled until confirmation ends.',
|
|
499
|
+
states,
|
|
500
|
+
timeline,
|
|
501
|
+
],
|
|
502
|
+
};
|
|
503
|
+
if (operation.phase === 'source-confirm') {
|
|
504
|
+
const truth = operation.truth;
|
|
505
|
+
return {
|
|
506
|
+
title: 'Source removal — source confirmation',
|
|
507
|
+
lines: [
|
|
508
|
+
'Relationship cascade succeeded.',
|
|
509
|
+
`Confirm Vercel skills source deletion: ${plan.source.name}`,
|
|
510
|
+
'Only this proven managed name will be passed; remove --all is forbidden.',
|
|
511
|
+
'Source outcome: partial',
|
|
512
|
+
`Provenance: ${truth?.provenance ?? plan.source.provenance}`,
|
|
513
|
+
...(truth?.relationships ?? []).map((relationship) => `Actual Relationship: ${relationship}`),
|
|
514
|
+
`Actual: ${truth?.actual ?? operation.result?.actual ?? 'rescan unavailable'}`,
|
|
515
|
+
`Desired: ${truth?.desired ?? 'removed'}`,
|
|
516
|
+
`Drift: ${truth?.drift ?? (operation.result?.drift.join(', ') || 'source deletion remains')}`,
|
|
517
|
+
`Mirror state: ${truth ? sourceMirrorState(truth) : 'unknown'}`,
|
|
518
|
+
`Relationship effects: ${dependencies.length} planned; completed=${operation.result?.completedWork?.join(', ') || 'no dependent Relationships'}`,
|
|
519
|
+
`Update availability: ${truth?.updateAvailability ?? 'unknown'}`,
|
|
520
|
+
`Recovery paths: manifest=${operation.result?.recoveryManifest ?? plan.recovery.manifest}`,
|
|
521
|
+
`next-load Effective Visibility: ${truth?.effectiveVisibility ?? 'unknown'}`,
|
|
522
|
+
'running Harness not reloaded',
|
|
523
|
+
'Launch-dependent consumption: unknown',
|
|
524
|
+
states,
|
|
525
|
+
timeline,
|
|
526
|
+
],
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
if (operation.phase === 'run')
|
|
530
|
+
return {
|
|
531
|
+
title: 'Source removal — running',
|
|
532
|
+
lines: [
|
|
533
|
+
states,
|
|
534
|
+
timeline,
|
|
535
|
+
`Stage: ${operation.runStep}`,
|
|
536
|
+
'Scope switching and unrelated mutations are disabled while this plan runs.',
|
|
537
|
+
`Recovery manifest: ${plan.recovery.manifest}`,
|
|
538
|
+
],
|
|
539
|
+
};
|
|
540
|
+
const succeeded = operation.phase === 'verify';
|
|
541
|
+
const truth = operation.truth;
|
|
542
|
+
return {
|
|
543
|
+
title: succeeded ? 'Source operation — Verify truth' : 'Source operation — failed',
|
|
544
|
+
lines: [
|
|
545
|
+
states,
|
|
546
|
+
timeline,
|
|
547
|
+
...operation.steps.map(({ name, status }) => `Step: ${name} ${status}`),
|
|
548
|
+
...(succeeded ? [] : [
|
|
549
|
+
`Error: ${operation.error?.split('\n')[0]}`,
|
|
550
|
+
'Raw log: press l for full output',
|
|
551
|
+
]),
|
|
552
|
+
`Source outcome: ${operation.outcome ?? (succeeded ? 'succeeded' : 'failed')}`,
|
|
553
|
+
`Provenance: ${truth?.provenance ?? plan.source.provenance}`,
|
|
554
|
+
`Resource: ${truth?.resource ?? 'missing'}`,
|
|
555
|
+
...(truth?.relationships ?? []).map((relationship) => `Actual Relationship: ${relationship}`),
|
|
556
|
+
`Actual: ${truth?.actual ?? operation.result?.actual ?? 'rescan unavailable'}`,
|
|
557
|
+
`Desired: ${truth?.desired ?? 'removed'}`,
|
|
558
|
+
`Drift: ${truth?.drift ?? (operation.result?.drift.join(', ') || 'see error')}`,
|
|
559
|
+
`Mirror state: ${truth ? sourceMirrorState(truth) : 'unknown'}`,
|
|
560
|
+
`Relationship effects: ${dependencies.length} planned; completed=${operation.result?.completedWork?.join(', ') || 'unknown'}`,
|
|
561
|
+
`Update availability: ${truth?.updateAvailability ?? 'unknown'}`,
|
|
562
|
+
`next-load Effective Visibility: ${truth?.effectiveVisibility ?? 'unknown'}`,
|
|
563
|
+
'running Harness not reloaded',
|
|
564
|
+
'Launch-dependent consumption: unknown',
|
|
565
|
+
`Recovery paths: lock=${plan.target.lockFile}; manifest=${plan.recovery.manifest}`,
|
|
566
|
+
succeeded ? 'Enter acknowledge Verify truth' : 't retry · Esc acknowledge remaining Drift',
|
|
567
|
+
],
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
if (operation.kind === 'update') {
|
|
571
|
+
const plan = operation.plan;
|
|
572
|
+
const included = plan.items.filter(({ included }) => included);
|
|
573
|
+
const excluded = plan.items.filter(({ included }) => !included);
|
|
574
|
+
const effects = included.flatMap(({ relationshipEffects }) => relationshipEffects);
|
|
575
|
+
const itemLines = operation.result?.items.map(({ name, outcome, reason, actual, drift }) => `${name}: ${outcome}${reason ? ` (${reason})` : ''}; ` +
|
|
576
|
+
`Actual=${actual ?? 'not run'}; Drift=${drift?.join(', ') || 'none'}`) ?? plan.items.flatMap(({ name, included: selected, reason, desired, temporaryVisibility, currentTruth, intentPreservation, expectedFinalTruth, }) => [
|
|
577
|
+
`${name}: ${selected ? 'included' : `excluded (${reason})`}`,
|
|
578
|
+
` Current=${currentTruth.actual}; hash=${currentTruth.hash}; source=${currentTruth.source}`,
|
|
579
|
+
` Desired=${desired}; Preset claims=${intentPreservation.presetClaims.join(', ') || 'none'}; temporary visibility=${temporaryVisibility ? 'required' : 'no'}`,
|
|
580
|
+
` Expected=${expectedFinalTruth.actual}; Drift=${expectedFinalTruth.drift}; Source=${expectedFinalTruth.source}; Relationships=${expectedFinalTruth.relationships}`,
|
|
581
|
+
]);
|
|
582
|
+
const selectionExclusions = (operation.selectionExclusions ?? []).map(({ name, identity, reason }) => `${name}: excluded (${reason}); Identity=${identity}`);
|
|
583
|
+
if (operation.phase === 'preview')
|
|
584
|
+
return {
|
|
585
|
+
title: `Source Update plan — ${included.length} included, ${excluded.length + selectionExclusions.length} excluded`,
|
|
586
|
+
lines: [
|
|
587
|
+
states,
|
|
588
|
+
timeline,
|
|
589
|
+
`Scope: ${plan.scope.kind === 'project' ? 'exact Project' : 'Global'} — ${plan.scope.path}`,
|
|
590
|
+
`Confirmed identity-stable set: ${included.map(({ name }) => name).join(', ') || 'none'}`,
|
|
591
|
+
`Source Adapter: ${plan.sourceAdapter.package}; update owner=${plan.sourceAdapter.updateOwner}`,
|
|
592
|
+
...itemLines,
|
|
593
|
+
...selectionExclusions,
|
|
594
|
+
`Relationship effects: ${effects.length}`,
|
|
595
|
+
...relationshipEffectLines(effects),
|
|
596
|
+
`Mirrors: ${effects.some(({ plannedAction }) => plannedAction === 'mirror-sync') ? 'mirror-sync Drift; explicit reconcile required; diverged Mirrors untouched' : 'none'}`,
|
|
597
|
+
`next-load Effective Visibility consequences:`,
|
|
598
|
+
...(operation.visibilityConsequences ?? ['unknown until final rescan']),
|
|
599
|
+
`Hashes: lock=${plan.preconditions.lock.hash} policy=${plan.preconditions.policy.hash}`,
|
|
600
|
+
`Lock ownership: ${plan.preconditions.lock.owner}`,
|
|
601
|
+
`Permissions: target=${plan.preconditions.permissions.target} lock=${plan.preconditions.permissions.lock}`,
|
|
602
|
+
`Blockers: ${plan.blockers.join('; ') || 'none'}`,
|
|
603
|
+
`Recovery: lock=${plan.recovery.operationLock}; ${plan.recovery.evidence.join(', ')}`,
|
|
604
|
+
],
|
|
605
|
+
};
|
|
606
|
+
if (operation.phase === 'confirm')
|
|
607
|
+
return {
|
|
608
|
+
title: 'Source Update — confirmation',
|
|
609
|
+
lines: [
|
|
610
|
+
`Confirm exact named set: ${included.map(({ name }) => name).join(', ')}`,
|
|
611
|
+
`Scope: ${plan.scope.kind === 'project' ? 'exact Project' : 'Global'} — ${plan.scope.path}`,
|
|
612
|
+
'Desired Activation will be restored after every success or failure.',
|
|
613
|
+
'Links consume refreshed source directly; Mirrors remain explicit mirror-sync Drift.',
|
|
614
|
+
'Vercel skills owns any upstream prompt.',
|
|
615
|
+
'Scope switching and unrelated mutations are disabled until confirmation ends.',
|
|
616
|
+
states,
|
|
617
|
+
timeline,
|
|
618
|
+
'Enter confirm · Esc cancel',
|
|
619
|
+
],
|
|
620
|
+
};
|
|
621
|
+
if (operation.phase === 'run')
|
|
622
|
+
return {
|
|
623
|
+
title: 'Source Update — running',
|
|
624
|
+
lines: [
|
|
625
|
+
states,
|
|
626
|
+
timeline,
|
|
627
|
+
'One selected-scope operation lock protects the confirmed set.',
|
|
628
|
+
'Per-item upstream updates continue after failures.',
|
|
629
|
+
'Scope switching and unrelated mutations are disabled while this plan runs.',
|
|
630
|
+
`Artifact: ${plan.target.lockFile}`,
|
|
631
|
+
],
|
|
632
|
+
};
|
|
633
|
+
const failed = operation.result?.items.filter(({ outcome }) => outcome === 'failed').length ?? 0;
|
|
634
|
+
return {
|
|
635
|
+
title: operation.phase === 'verify' ? 'Source Update — Verify truth' : 'Source Update — failed',
|
|
636
|
+
lines: [
|
|
637
|
+
states,
|
|
638
|
+
timeline,
|
|
639
|
+
...(operation.error ? [
|
|
640
|
+
`Error: ${operation.error.split('\n')[0]}`,
|
|
641
|
+
'Raw log: press l for full output',
|
|
642
|
+
] : []),
|
|
643
|
+
...itemLines,
|
|
644
|
+
`Source outcome: ${operation.outcome ?? (failed > 0 ? 'failed' : 'succeeded')}`,
|
|
645
|
+
`Provenance: ${operation.truth?.provenance ?? 'Source unknown'}`,
|
|
646
|
+
...(operation.truth?.relationships ?? []).map((relationship) => `Actual Relationship: ${relationship}`),
|
|
647
|
+
`Actual: ${operation.truth?.actual ?? operation.result?.actual ?? 'rescan unavailable'}`,
|
|
648
|
+
`Desired: ${operation.truth?.desired ?? 'preserved per item'}`,
|
|
649
|
+
`Drift: ${operation.truth?.drift ?? (operation.result?.drift.join(', ') || 'none')}`,
|
|
650
|
+
`Mirror state: ${operation.truth ? sourceMirrorState(operation.truth) : 'unknown'}`,
|
|
651
|
+
`Relationship effects: ${effects.length} planned`,
|
|
652
|
+
`Update availability: ${operation.truth?.updateAvailability ?? 'unknown'}`,
|
|
653
|
+
`next-load Effective Visibility: ${operation.truth?.effectiveVisibility ?? 'unknown'}`,
|
|
654
|
+
'running Harness not reloaded',
|
|
655
|
+
'Launch-dependent consumption: unknown',
|
|
656
|
+
`Recovery paths: operation lock=${plan.recovery.operationLock}; source lock=${plan.target.lockFile}; ${plan.recovery.evidence.join(', ')}`,
|
|
657
|
+
failed > 0 ? 't retry failed items · Esc acknowledge remaining Drift' : 'Enter acknowledge Verify truth',
|
|
658
|
+
],
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
const plan = operation.plan;
|
|
662
|
+
const scope = plan.scope?.kind === 'project' ? 'exact Project' : 'Global';
|
|
663
|
+
const effects = plan.relationshipEffects ?? [];
|
|
664
|
+
if (operation.phase === 'preview')
|
|
665
|
+
return {
|
|
666
|
+
title: `Source ${plan.replacement ? 'Replace' : 'Add'} plan`,
|
|
667
|
+
lines: [
|
|
668
|
+
states,
|
|
669
|
+
timeline,
|
|
670
|
+
`Scope: ${scope} — ${plan.scope?.path}`,
|
|
671
|
+
`Candidate: ${operation.candidate.source}@${operation.candidate.name}`,
|
|
672
|
+
`Provenance: ${operation.candidate.source}`,
|
|
673
|
+
`Shared Slot: ${plan.candidate?.normalizedSlot}`,
|
|
674
|
+
`Source Adapter: ${plan.sourceAdapter?.package}`,
|
|
675
|
+
`Ownership: SkillsPub scope/identity/Slot/Relationships; Vercel skills security audit + Proceed`,
|
|
676
|
+
...(plan.replacement ? [`Replace: ${plan.replacement.from} → ${plan.replacement.to}`] : []),
|
|
677
|
+
`Current Actual: ${plan.currentTruth?.actual}; Desired=${plan.currentTruth?.desired}; Drift=${plan.currentTruth?.drift}`,
|
|
678
|
+
`Relationship effects: ${effects.map(({ plannedAction }) => plannedAction).join(', ')}`,
|
|
679
|
+
...relationshipEffectLines(effects),
|
|
680
|
+
`Hashes: source=${plan.preconditions?.sourceEntry.hash} lock=${plan.preconditions?.lock.hash} policy=${plan.preconditions?.policy.hash}`,
|
|
681
|
+
`Permissions: target=${plan.preconditions?.permissions.target} lock=${plan.preconditions?.permissions.lock}`,
|
|
682
|
+
`Lock ownership: ${plan.preconditions?.lock.owner}`,
|
|
683
|
+
`Blockers: ${plan.blockers?.join('; ') || 'none'}`,
|
|
684
|
+
`Preserve Slot intent: ${plan.intentPreservation?.baseIntent}`,
|
|
685
|
+
`Tags: ${plan.intentPreservation?.tags.join(', ') || 'none'}`,
|
|
686
|
+
`Bundles: ${plan.intentPreservation?.bundles.join(', ') || 'none'}`,
|
|
687
|
+
`Preset claims: ${plan.intentPreservation?.presetClaims.join(', ') || 'none'}`,
|
|
688
|
+
`Preset selectors: ${plan.intentPreservation?.presetSelectors.join(', ') || 'none'}`,
|
|
689
|
+
`Recovery: ${plan.recovery?.evidence.join(', ')}`,
|
|
690
|
+
`Expected final truth: ${plan.expectedFinalTruth?.actual}; Desired=${plan.expectedFinalTruth?.desired}; Drift=${plan.expectedFinalTruth?.drift}`,
|
|
691
|
+
],
|
|
692
|
+
};
|
|
693
|
+
if (operation.phase === 'confirm')
|
|
694
|
+
return {
|
|
695
|
+
title: 'Source operation — confirmation',
|
|
696
|
+
lines: [
|
|
697
|
+
'Confirm SkillsPub intent.',
|
|
698
|
+
'Scope · identity · Slot · Relationships',
|
|
699
|
+
`Candidate: ${operation.candidate.source}@${operation.candidate.name}`,
|
|
700
|
+
`Scope: ${scope} — ${plan.scope?.path}`,
|
|
701
|
+
`Slot: ${plan.candidate?.normalizedSlot}`,
|
|
702
|
+
`Relationships: ${effects.length}; no Harness-specific Link or Mirror will be created`,
|
|
703
|
+
'Vercel skills owns security audit and final Proceed.',
|
|
704
|
+
'Scope switching and unrelated mutations are disabled until confirmation ends.',
|
|
705
|
+
states,
|
|
706
|
+
timeline,
|
|
707
|
+
'Enter confirm · Esc cancel',
|
|
708
|
+
],
|
|
709
|
+
};
|
|
710
|
+
if (operation.phase === 'run')
|
|
711
|
+
return {
|
|
712
|
+
title: 'Source operation — running',
|
|
713
|
+
lines: [
|
|
714
|
+
states,
|
|
715
|
+
timeline,
|
|
716
|
+
'Upstream ownership handoff: Vercel skills security audit and Proceed',
|
|
717
|
+
'Scope switching and unrelated mutations are disabled while this plan runs.',
|
|
718
|
+
`Artifact: ${plan.target?.lockFile}`,
|
|
719
|
+
'Final filesystem rescan queued',
|
|
720
|
+
],
|
|
721
|
+
};
|
|
722
|
+
const succeeded = operation.phase === 'verify';
|
|
723
|
+
const truth = operation.truth;
|
|
724
|
+
return {
|
|
725
|
+
title: succeeded ? 'Source operation — Verify truth' : 'Source operation — failed',
|
|
726
|
+
lines: [
|
|
727
|
+
states,
|
|
728
|
+
timeline,
|
|
729
|
+
...(succeeded ? [] : [
|
|
730
|
+
...(/new preview required/i.test(operation.error ?? '') ? ['New preview required.'] : []),
|
|
731
|
+
`Error: ${operation.error?.split('\n')[0]}`,
|
|
732
|
+
'Raw log: press l for full output',
|
|
733
|
+
]),
|
|
734
|
+
`Source outcome: ${operation.outcome ?? (succeeded ? 'succeeded' : 'failed')}`,
|
|
735
|
+
`Provenance: ${truth?.provenance ?? 'Source unknown'}`,
|
|
736
|
+
`Resource: ${truth?.resource ?? 'missing'}`,
|
|
737
|
+
`Slot: ${truth?.slot ?? plan.candidate?.normalizedSlot}`,
|
|
738
|
+
`Relationships: ${truth?.relationships.length ?? 0}`,
|
|
739
|
+
...(truth?.relationships ?? []).map((relationship) => `Actual Relationship: ${relationship}`),
|
|
740
|
+
`Actual: ${truth?.actual ?? operation.result?.actual ?? 'rescan unavailable'}`,
|
|
741
|
+
`Desired: ${truth?.desired ?? 'unknown'}`,
|
|
742
|
+
`Drift: ${truth?.drift ?? (operation.result?.drift.join(', ') || (succeeded ? 'none' : 'see error'))}`,
|
|
743
|
+
`Mirror state: ${truth ? sourceMirrorState(truth) : 'unknown'}`,
|
|
744
|
+
`Relationship effects: ${effects.length} planned`,
|
|
745
|
+
`Update availability: ${truth?.updateAvailability ?? 'unknown'}`,
|
|
746
|
+
`next-load Effective Visibility: ${truth?.effectiveVisibility ?? 'unknown'}`,
|
|
747
|
+
'running Harness not reloaded',
|
|
748
|
+
'Launch-dependent consumption: unknown',
|
|
749
|
+
`Recovery paths: lock=${plan.target?.lockFile}; operation lock=${plan.recovery?.operationLock}; ${plan.recovery?.evidence.join(', ')}`,
|
|
750
|
+
succeeded ? 'Enter acknowledge Verify truth' : 't retry · Esc acknowledge remaining Drift · a new preview required if intent changed',
|
|
751
|
+
],
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
function sourceEmptyLines(surface) {
|
|
755
|
+
const identityLines = [
|
|
756
|
+
' Source = Shared remote lifecycle: find/add/replace/update/remove',
|
|
757
|
+
' (Target & Skill manage installed Relationships).',
|
|
758
|
+
' Global is the default/recommended scope.',
|
|
759
|
+
' exact Project Source copies are explicit CLI-only:',
|
|
760
|
+
' skillspub project <path> shared …',
|
|
761
|
+
];
|
|
762
|
+
if (surface === 'catalog')
|
|
763
|
+
return [
|
|
764
|
+
' No remote candidates loaded.',
|
|
765
|
+
' Press / to search with the pinned Vercel skills Source Adapter.',
|
|
766
|
+
' Nothing is fetched automatically.',
|
|
767
|
+
...identityLines,
|
|
768
|
+
' Current scope: Global (default/recommended).',
|
|
769
|
+
' exact Project data is not modified.',
|
|
770
|
+
];
|
|
771
|
+
return [
|
|
772
|
+
' No Shared resources in the Global scope.',
|
|
773
|
+
...identityLines,
|
|
774
|
+
' Current scope: Global (default/recommended).',
|
|
775
|
+
' exact Project data is not modified.',
|
|
776
|
+
];
|
|
777
|
+
}
|
|
778
|
+
function SourceWorkspace({ scopePath, surface, candidates, candidateIndex, candidateTruth, inventory, inventoryIndex, marks, visibility, desired, drift, operation, width, height, }) {
|
|
779
|
+
const candidate = surface === 'catalog' ? candidates[candidateIndex] : undefined;
|
|
780
|
+
const row = surface === 'inventory' ? inventory[inventoryIndex] : undefined;
|
|
781
|
+
const relationships = sourceRelationships(row);
|
|
782
|
+
const idleDetail = sourceDetailLines(surface === 'catalog' ? candidate : undefined, surface === 'inventory' ? row : undefined);
|
|
783
|
+
const activeDetail = operation ? sourceOperationLines(operation) : undefined;
|
|
784
|
+
const operationPage = Math.max(3, height - 11);
|
|
785
|
+
const operationScroll = operation?.scroll ?? 0;
|
|
786
|
+
const detail = activeDetail
|
|
787
|
+
? activeDetail.lines.slice(operationScroll, operationScroll + operationPage)
|
|
788
|
+
: idleDetail;
|
|
789
|
+
const detailTitle = activeDetail
|
|
790
|
+
? `${activeDetail.title} [${Math.min(operationScroll + 1, activeDetail.lines.length)}/${activeDetail.lines.length}]`
|
|
791
|
+
: surface === 'catalog' ? 'Selected candidate' : 'Selected resource';
|
|
792
|
+
const idleActual = row
|
|
793
|
+
? relationships.map(({ info }) => relationshipStatusText(info)).join(', ')
|
|
794
|
+
: candidateTruth?.actual ?? (candidate ? 'not installed' : 'none selected');
|
|
795
|
+
const idleEffective = row
|
|
796
|
+
? [...new Set(visibility?.harnesses.map(({ effectiveVisibility }) => effectiveVisibility) ?? ['unknown'])].join('/')
|
|
797
|
+
: candidateTruth?.effectiveVisibility ?? 'not applicable';
|
|
798
|
+
const addCurrentTruth = operation?.kind === 'add' ? operation.plan.currentTruth : undefined;
|
|
799
|
+
const actual = operation?.truth?.actual ?? addCurrentTruth?.actual ?? idleActual;
|
|
800
|
+
const displayedDesired = operation?.truth?.desired ?? addCurrentTruth?.desired ?? candidateTruth?.desired ?? desired;
|
|
801
|
+
const displayedDrift = operation?.truth?.drift ?? addCurrentTruth?.drift ?? candidateTruth?.drift ?? drift;
|
|
802
|
+
const displayedUpdate = operation?.truth?.updateAvailability ?? row?.updateAvailability?.status ?? candidateTruth?.updateAvailability ?? 'unknown';
|
|
803
|
+
const effective = operation?.truth?.effectiveVisibility ?? idleEffective;
|
|
804
|
+
const displayedRelationships = operation?.truth?.relationships.length ?? (row ? relationships.length : candidateTruth?.relationships ?? 0);
|
|
805
|
+
const selectedIndex = surface === 'catalog' ? candidateIndex : inventoryIndex;
|
|
806
|
+
const list = surface === 'catalog'
|
|
807
|
+
? candidates.map((item, index) => h(RowLine, {
|
|
808
|
+
key: candidateId(item),
|
|
809
|
+
active: index === candidateIndex,
|
|
810
|
+
focused: true,
|
|
811
|
+
}, `${item.source}@${item.name}${item.installs ? ` ${item.installs}` : ''}`))
|
|
812
|
+
: inventory.map((item, index) => {
|
|
813
|
+
const rel = sourceRelationships(item)[0];
|
|
814
|
+
const inherited = rel?.readOnly ? ` [inherited ${rel.scope}: ${path.dirname(rel.info.path)}]` : '';
|
|
815
|
+
return h(RowLine, {
|
|
816
|
+
key: item.id,
|
|
817
|
+
active: index === inventoryIndex,
|
|
818
|
+
focused: true,
|
|
819
|
+
marked: marks.has(item.id),
|
|
820
|
+
}, `${item.displayName} ${item.sourceLabel}${inherited}`);
|
|
821
|
+
});
|
|
822
|
+
return h(Box, { height, flexDirection: 'column' }, h(Text, { wrap: 'wrap' }, `Scope: Global (default/recommended) Path: ${scopePath}`), h(Text, { color: 'cyan', wrap: 'wrap' }, 'Discover → Inspect & plan → Confirm ownership → Run & maintain → Verify truth'), h(Box, { flexGrow: 1, overflow: 'hidden' }, h(ListColumn, { title: surface === 'catalog' ? 'Catalog' : 'Inventory', focused: true, flexGrow: 1 }, ...(list.length > 0
|
|
823
|
+
? list.slice(windowStart(list.length, selectedIndex, Math.max(1, height - 8)), windowStart(list.length, selectedIndex, Math.max(1, height - 8)) + Math.max(1, height - 8))
|
|
824
|
+
: sourceEmptyLines(surface).map((line, index) => h(Text, { key: `empty-${index}`, dimColor: true, wrap: 'wrap' }, line)))), width >= WIDE_MIN
|
|
825
|
+
? h(Box, { width: Math.max(34, Math.floor(width * 0.42)), paddingX: 1 }, h(SourceDetail, { title: detailTitle, lines: detail }))
|
|
826
|
+
: null), width < WIDE_MIN
|
|
827
|
+
? h(SourceDetail, {
|
|
828
|
+
title: detailTitle,
|
|
829
|
+
lines: detail,
|
|
830
|
+
})
|
|
831
|
+
: null, h(Text, { wrap: 'wrap' }, `Selected: ${idleDetail[0] ?? 'none'} Actual: ${actual} Desired: ${displayedDesired}`), h(Text, { wrap: 'wrap' }, `Drift: ${displayedDrift} Update: ${displayedUpdate} Relationship: ${displayedRelationships} Effective Visibility: ${effective}`));
|
|
832
|
+
}
|
|
833
|
+
const fallbackVisibilityInventories = new WeakMap();
|
|
834
|
+
function resolveVisibility(home, row, projectPath, harness, want, snapshot) {
|
|
835
|
+
if (!row?.realPath)
|
|
836
|
+
return undefined;
|
|
837
|
+
try {
|
|
838
|
+
if (!snapshot)
|
|
839
|
+
return explainVisibility(home, `skill:${row.id}`, { projectPath, harness, want });
|
|
840
|
+
let report = tuiSnapshotInventory(snapshot) ?? fallbackVisibilityInventories.get(snapshot);
|
|
841
|
+
if (!report) {
|
|
842
|
+
report = snapshot.project
|
|
843
|
+
? scanProjectInventory(home, snapshot.project, undefined, { persist: false })
|
|
844
|
+
: scanGlobalInventory(home, undefined, { persist: false });
|
|
845
|
+
fallbackVisibilityInventories.set(snapshot, report);
|
|
846
|
+
}
|
|
847
|
+
return explainVisibilityFromInventory(home, report, `skill:${row.id}`, { projectPath, harness, want }, snapshot.harnesses);
|
|
848
|
+
}
|
|
849
|
+
catch {
|
|
850
|
+
return undefined;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
function explanationLines(explanation) {
|
|
854
|
+
const harness = explanation?.harnesses[0];
|
|
855
|
+
if (!harness)
|
|
856
|
+
return ['Explain unavailable; refresh Inventory and try again.'];
|
|
857
|
+
const lines = [
|
|
858
|
+
`Result: ${harness.effectiveVisibility}${harness.detected ? '' : ' · not-detected'}`,
|
|
859
|
+
`Detected: ${harness.detected ? 'yes' : 'no'}`,
|
|
860
|
+
`Adapter support: ${harness.support}`,
|
|
861
|
+
`Shared consumption: ${harness.sharedConsumption.status} — ${harness.sharedConsumption.detail}`,
|
|
862
|
+
`Isolation: ${harness.isolation.status} — ${harness.isolation.detail}`,
|
|
863
|
+
'',
|
|
864
|
+
'Evidence:',
|
|
865
|
+
...harness.evidence.map((evidence) => ` ${evidence.verifiedVersion} — ${evidence.detail} — ${evidence.url}`),
|
|
866
|
+
...harness.reasons.map((reason) => `reason: ${reason.message}`),
|
|
867
|
+
...harness.warnings.map((warning) => `warning: ${warning.message}`),
|
|
868
|
+
...harness.conflicts.map((conflict) => `conflict: ${conflict.message}`),
|
|
869
|
+
];
|
|
870
|
+
if (harness.plan) {
|
|
871
|
+
lines.push('', `Wanted: ${explanation.wanted}`, `Executable: ${harness.plan.executable ? 'yes' : 'no'}`, ...harness.plan.steps.flatMap((step) => [
|
|
872
|
+
`step: ${step.operation} ${step.targetId}/${step.slot} (${step.from} -> ${step.to}${step.form ? `, ${step.form}` : ''})`,
|
|
873
|
+
...step.preconditions.map((condition) => ` precondition: ${condition.message}`),
|
|
874
|
+
]), ...harness.plan.blockers.map((blocker) => `blocker: ${blocker.message}`));
|
|
875
|
+
}
|
|
876
|
+
lines.push('', 'Roots:');
|
|
877
|
+
for (const root of harness.roots) {
|
|
878
|
+
lines.push(`${root.consumption} ${root.scope}/${root.kind}: ${root.path}`, ` reason: ${root.reason}`, ...root.relationships.map((relationship) => ` ${relationship.activation} ${relationship.form}${relationship.selected ? ' selected' : ''}: ${relationship.path}`));
|
|
879
|
+
}
|
|
880
|
+
return lines;
|
|
881
|
+
}
|
|
882
|
+
function ExplainModal({ row, harnessName, harnessIndex, harnessCount, lines, scroll, height, }) {
|
|
883
|
+
const viewHeight = Math.max(1, height - 8);
|
|
884
|
+
return h(Box, {
|
|
885
|
+
flexGrow: 1,
|
|
886
|
+
flexDirection: 'column',
|
|
887
|
+
borderStyle: 'round',
|
|
888
|
+
borderColor: 'cyan',
|
|
889
|
+
paddingX: 1,
|
|
890
|
+
overflow: 'hidden',
|
|
891
|
+
}, h(Text, { bold: true, wrap: 'truncate-end' }, `Explain — ${row.name} — ${harnessName} [${harnessIndex + 1}/${harnessCount}]`), ...lines.slice(scroll, scroll + viewHeight).map((line, index) => h(Text, { key: scroll + index, wrap: 'truncate-end' }, line || ' ')));
|
|
892
|
+
}
|
|
893
|
+
export function App({ home, projectPath }) {
|
|
894
|
+
const { exit } = useApp();
|
|
895
|
+
const { stdout } = useStdout();
|
|
896
|
+
const readSize = () => ({
|
|
897
|
+
width: stdout.columns ?? 100,
|
|
898
|
+
height: stdout.rows ?? 30,
|
|
899
|
+
});
|
|
900
|
+
const [size, setSize] = useState(readSize);
|
|
901
|
+
useEffect(() => {
|
|
902
|
+
const resize = () => setSize(readSize());
|
|
903
|
+
stdout.on('resize', resize);
|
|
904
|
+
return () => {
|
|
905
|
+
stdout.off('resize', resize);
|
|
906
|
+
};
|
|
907
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
908
|
+
}, [stdout]);
|
|
909
|
+
const { width, height } = size;
|
|
910
|
+
const takeSnapshot = () => projectPath ? projectTuiSnapshot(home, projectPath) : tuiSnapshot(home);
|
|
911
|
+
const [snapshot, setSnapshot] = useState(takeSnapshot);
|
|
912
|
+
// Global-only: Source operates exclusively on the Global Shared Target,
|
|
913
|
+
// even when the TUI has an exact Project context (issue #145). exact
|
|
914
|
+
// Project Source copies remain available through the explicit CLI only.
|
|
915
|
+
const sourceTakeSnapshot = () => tuiSnapshot(home);
|
|
916
|
+
const [sourceSnapshot, setSourceSnapshot] = useState(snapshot);
|
|
917
|
+
const [sourceSurface, setSourceSurface] = useState('catalog');
|
|
918
|
+
const [sourceCandidates, setSourceCandidates] = useState([]);
|
|
919
|
+
const [sourceCandidateId, setSourceCandidateId] = useState();
|
|
920
|
+
const [sourceResourceId, setSourceResourceId] = useState();
|
|
921
|
+
const [sourceMarks, setSourceMarks] = useState(new Set());
|
|
922
|
+
const [sourceDetailOpen, setSourceDetailOpen] = useState(false);
|
|
923
|
+
const [sourceLogOpen, setSourceLogOpen] = useState(false);
|
|
924
|
+
const [sourceLogScroll, setSourceLogScroll] = useState(0);
|
|
925
|
+
const [sourceOperation, setSourceOperation] = useState();
|
|
926
|
+
const [latestSourceOperation, setLatestSourceOperation] = useState();
|
|
927
|
+
const activeSourceRefresh = useRef(undefined);
|
|
928
|
+
const mounted = useRef(true);
|
|
929
|
+
useEffect(() => () => {
|
|
930
|
+
mounted.current = false;
|
|
931
|
+
const refresh = activeSourceRefresh.current;
|
|
932
|
+
if (refresh) {
|
|
933
|
+
const cleanup = () => removeOwnedOperationLock(refresh.operationLock, refresh.childPid);
|
|
934
|
+
refresh.child.once('close', cleanup);
|
|
935
|
+
if (!refresh.child.kill())
|
|
936
|
+
cleanup();
|
|
937
|
+
}
|
|
938
|
+
}, []);
|
|
939
|
+
const planScope = projectPath ? { projectPath } : {};
|
|
940
|
+
const assertSourceRefreshIdle = () => {
|
|
941
|
+
if (activeSourceRefresh.current)
|
|
942
|
+
throw new Error('Source refresh in progress; mutations are disabled');
|
|
943
|
+
};
|
|
944
|
+
const prepareMutation = () => {
|
|
945
|
+
assertSourceRefreshIdle();
|
|
946
|
+
if (projectPath)
|
|
947
|
+
scanProjectInventory(home, projectPath);
|
|
948
|
+
else
|
|
949
|
+
scanGlobalInventory(home);
|
|
950
|
+
};
|
|
951
|
+
const [tab, setTab] = useState('target');
|
|
952
|
+
const [focusColumn, setFocusColumn] = useState(0);
|
|
953
|
+
const [targetIndex, setTargetIndex] = useState(0);
|
|
954
|
+
const [relationshipKey, setRelationshipKey] = useState();
|
|
955
|
+
// Skill-tab selection is tracked by instance id so tab switches keep identity.
|
|
956
|
+
const [instanceId, setInstanceId] = useState();
|
|
957
|
+
const [instanceTargetIndex, setInstanceTargetIndex] = useState(0);
|
|
958
|
+
const [query, setQuery] = useState('');
|
|
959
|
+
const [searching, setSearching] = useState(false);
|
|
960
|
+
const [sort, setSort] = useState('name');
|
|
961
|
+
const [modal, setModal] = useState(null);
|
|
962
|
+
const [explainModal, setExplainModal] = useState(null);
|
|
963
|
+
const [targetInfoOpen, setTargetInfoOpen] = useState(false);
|
|
964
|
+
const [confirmation, setConfirmation] = useState(null);
|
|
965
|
+
const [manage, setManage] = useState(null);
|
|
966
|
+
const [batch, setBatch] = useState(null);
|
|
967
|
+
const [batchConfirm, setBatchConfirm] = useState(null);
|
|
968
|
+
const [batchTag, setBatchTag] = useState(null);
|
|
969
|
+
const [feedback, setFeedback] = useState('');
|
|
970
|
+
const targets = snapshot.targets;
|
|
971
|
+
const target = targets[Math.min(targetIndex, Math.max(0, targets.length - 1))];
|
|
972
|
+
const targetHarness = target
|
|
973
|
+
? [...snapshot.harnesses.detected, ...snapshot.harnesses.available]
|
|
974
|
+
.find((harness) => harness.key === target.name)
|
|
975
|
+
: undefined;
|
|
976
|
+
const instTarget = Math.min(instanceTargetIndex, Math.max(0, targets.length - 1));
|
|
977
|
+
const instanceTarget = targets[instTarget];
|
|
978
|
+
const rows = useMemo(() => sortRows(searchRows(snapshot.rows, query), sort, (row) => statusSortValue(row.targets[(tab === 'target' ? target : instanceTarget)?.name ?? ''])), [snapshot.rows, query, sort, tab, target, instanceTarget]);
|
|
979
|
+
const entries = useMemo(() => (target ? entriesFor(rows, target.name) : []), [rows, target]);
|
|
980
|
+
const sourceInventory = useMemo(() => sourceInventoryRows(sourceSnapshot), [sourceSnapshot]);
|
|
981
|
+
const sourceCandidateFound = sourceCandidates.findIndex((candidate) => candidateId(candidate) === sourceCandidateId);
|
|
982
|
+
const sourceCandidateIndex = sourceCandidateFound < 0 ? 0 : sourceCandidateFound;
|
|
983
|
+
const sourceResourceFound = sourceInventory.findIndex(({ id }) => id === sourceResourceId);
|
|
984
|
+
const sourceResourceIndex = sourceResourceFound < 0
|
|
985
|
+
? sourceResourceId === '' ? -1 : 0
|
|
986
|
+
: sourceResourceFound;
|
|
987
|
+
const sourceCandidate = sourceCandidates[sourceCandidateIndex];
|
|
988
|
+
const sourceResource = sourceInventory[sourceResourceIndex];
|
|
989
|
+
const relationshipFound = entries.findIndex((entry) => entry.key === relationshipKey);
|
|
990
|
+
const relationshipIndex = relationshipFound === -1 ? 0 : relationshipFound;
|
|
991
|
+
const entry = entries[relationshipIndex];
|
|
992
|
+
const found = rows.findIndex((row) => row.id === instanceId);
|
|
993
|
+
const instanceIndex = found === -1 ? 0 : found; // deterministic fallback: first row
|
|
994
|
+
const instance = rows[instanceIndex];
|
|
995
|
+
const wide = width >= WIDE_MIN;
|
|
996
|
+
const markedIds = batch
|
|
997
|
+
? new Set(rows
|
|
998
|
+
.filter((row) => row.realPath && batch.marks.has(markKey(home.configDir, row.realPath)))
|
|
999
|
+
.map((row) => row.id))
|
|
1000
|
+
: undefined;
|
|
1001
|
+
const bodyHeight = Math.max(3, height - 2);
|
|
1002
|
+
const listHeight = Math.max(1, bodyHeight - 3);
|
|
1003
|
+
const targetStatusWidth = Math.max(28, Math.min(40, Math.max(0, ...targets.map((target) => target.name.length)) + 24));
|
|
1004
|
+
// Info is capped (its text wraps); name lists flex with what remains (long names win).
|
|
1005
|
+
const infoWidth = wide ? Math.max(28, Math.min(48, Math.floor(width * 0.28))) : 0;
|
|
1006
|
+
const instanceWidth = Math.max(10, width - targetStatusWidth - infoWidth);
|
|
1007
|
+
const modalContent = modal
|
|
1008
|
+
? (skillDetail(home, modal.row.id, projectPath)?.content ?? 'SKILL.md unavailable')
|
|
1009
|
+
: '';
|
|
1010
|
+
const modalLines = modal ? detailLines(modalContent, Math.max(1, width - 8)) : [];
|
|
1011
|
+
const modalPage = Math.max(1, height - 6);
|
|
1012
|
+
const sourceDetailContent = sourceDetailLines(sourceSurface === 'catalog' ? sourceCandidate : undefined, sourceSurface === 'inventory' ? sourceResource : undefined);
|
|
1013
|
+
const sourceEvidenceOperation = sourceOperation ?? latestSourceOperation;
|
|
1014
|
+
const sourceLogLines = sourceEvidenceOperation
|
|
1015
|
+
? detailLines([
|
|
1016
|
+
...(sourceOperation ? [] : [
|
|
1017
|
+
sourceOperationLines(sourceEvidenceOperation).title,
|
|
1018
|
+
...sourceOperationLines(sourceEvidenceOperation).lines,
|
|
1019
|
+
'',
|
|
1020
|
+
]),
|
|
1021
|
+
sourceEvidenceOperation.log ?? 'No upstream output captured.',
|
|
1022
|
+
'',
|
|
1023
|
+
`Operation lock: ${sourceEvidenceOperation.plan.recovery?.operationLock}`,
|
|
1024
|
+
`Evidence: ${sourceEvidenceOperation.plan.recovery?.evidence.join(', ')}`,
|
|
1025
|
+
...(sourceEvidenceOperation.kind === 'remove'
|
|
1026
|
+
? sourceEvidenceOperation.plan.dependencies.map((dependency) => `Dependency: ${dependency.targetId}/${dependency.slot} ${dependency.form}/${dependency.activation} ` +
|
|
1027
|
+
`${dependency.path} fingerprint=${dependency.fingerprint} action=${dependency.plannedAction}`)
|
|
1028
|
+
: []),
|
|
1029
|
+
].join('\n'), Math.max(1, width - 8))
|
|
1030
|
+
: [];
|
|
1031
|
+
const selectedRow = tab === 'target' ? entry?.row : instance;
|
|
1032
|
+
const selectedTarget = tab === 'target' ? target : targets[instTarget];
|
|
1033
|
+
const selectedInfo = tab === 'target'
|
|
1034
|
+
? entry?.relationship.info
|
|
1035
|
+
: selectedRow?.targets[selectedTarget?.name ?? ''];
|
|
1036
|
+
const selectedRel = tab === 'target'
|
|
1037
|
+
? entry?.relationship
|
|
1038
|
+
: selectedRow?.relationships.find((relationship) => relationship.target === selectedTarget?.name &&
|
|
1039
|
+
relationship.info.path === selectedInfo?.path);
|
|
1040
|
+
const visibility = useMemo(() => resolveVisibility(home, selectedRow, projectPath, undefined, undefined, snapshot), [home, projectPath, selectedRow, snapshot]);
|
|
1041
|
+
const activeSourceResource = sourceSurface === 'inventory' ? sourceResource : undefined;
|
|
1042
|
+
const sourceVisibility = useMemo(() => resolveVisibility(home, activeSourceResource, undefined, undefined, undefined, sourceSnapshot), [home, activeSourceResource, sourceSnapshot]);
|
|
1043
|
+
const sourceTruth = useMemo(() => sourceDesiredTruth(home, activeSourceResource, 'global', ''), [home, activeSourceResource, sourceSnapshot]);
|
|
1044
|
+
const sourceCandidateTruth = useMemo(() => sourceSurface === 'catalog'
|
|
1045
|
+
? catalogCandidateTruth(home, sourceSnapshot, sourceCandidate, 'global', '')
|
|
1046
|
+
: undefined, [home, sourceSurface, sourceSnapshot, sourceCandidate]);
|
|
1047
|
+
const sourceTarget = sourceSnapshot.targets.find(({ name }) => name === 'shared');
|
|
1048
|
+
const sourceRelationship = sourceRelationships(activeSourceResource).find((relationship) => !relationship.readOnly && relationship.info.form === 'local');
|
|
1049
|
+
const sourceRemovable = Boolean(activeSourceResource && sourceRelationship && activeSourceResource.sourceLabel !== 'Source unknown');
|
|
1050
|
+
const sourcePath = sourceTarget?.dir ?? home.configDir;
|
|
1051
|
+
const explained = useMemo(() => resolveVisibility(home, explainModal?.row, projectPath, explainModal?.harness, explainModal?.want), [home, projectPath, explainModal?.row, explainModal?.harness, explainModal?.want]);
|
|
1052
|
+
const explainedHarness = explained?.harnesses[0];
|
|
1053
|
+
const explainHarnesses = visibility?.harnesses ?? [];
|
|
1054
|
+
const explainHarnessIndex = Math.max(0, explainHarnesses.findIndex(({ key }) => key === explainModal?.harness));
|
|
1055
|
+
const explainDetailLines = detailLines(explanationLines(explained).join('\n'), Math.max(1, width - 8));
|
|
1056
|
+
const selectedCell = selectedRow && selectedTarget;
|
|
1057
|
+
const actionable = focusColumn === 1 && selectedCell;
|
|
1058
|
+
const manageRow = manage ? rows.find((candidate) => candidate.id === manage.rowId) : undefined;
|
|
1059
|
+
const membership = useMemo(() => {
|
|
1060
|
+
if (!selectedRow)
|
|
1061
|
+
return undefined;
|
|
1062
|
+
const { bundles, tags, presets } = snapshot.catalog;
|
|
1063
|
+
const unambiguousName = rows.filter((row) => row.name === selectedRow.name).length === 1;
|
|
1064
|
+
return {
|
|
1065
|
+
bundles: Object.entries(bundles)
|
|
1066
|
+
.filter(([, members]) => members.includes(selectedRow.id) ||
|
|
1067
|
+
(unambiguousName && members.includes(selectedRow.name)))
|
|
1068
|
+
.map(([name]) => name)
|
|
1069
|
+
.sort((a, b) => a.localeCompare(b)),
|
|
1070
|
+
tags: tags[selectedRow.id] ?? [],
|
|
1071
|
+
// Definition membership, same semantics as Bundles/Tags and the manage modal —
|
|
1072
|
+
// active claims are a separate concept (they only exist for activated Presets).
|
|
1073
|
+
presets: Object.entries(presets)
|
|
1074
|
+
.filter(([, preset]) => preset.selectors.includes(`skill:${selectedRow.id}`))
|
|
1075
|
+
.map(([name]) => name)
|
|
1076
|
+
.sort((a, b) => a.localeCompare(b)),
|
|
1077
|
+
};
|
|
1078
|
+
}, [selectedRow, snapshot.catalog, rows]);
|
|
1079
|
+
/** Re-read disk, then re-anchor selection: mutation moves entries, so locate
|
|
1080
|
+
* the fresh row/relationship by (targetId, slot) or stable row id. */
|
|
1081
|
+
const refresh = (keep) => {
|
|
1082
|
+
const next = takeSnapshot();
|
|
1083
|
+
setSnapshot(next);
|
|
1084
|
+
if (!keep)
|
|
1085
|
+
return;
|
|
1086
|
+
const row = keep.targetId === undefined
|
|
1087
|
+
? keep.rowId === undefined
|
|
1088
|
+
? undefined
|
|
1089
|
+
: next.rows.find((candidate) => candidate.id === keep.rowId)
|
|
1090
|
+
: next.rows.find((candidate) => candidate.relationships.some((rel) => rel.targetId === keep.targetId && rel.slot === keep.slot));
|
|
1091
|
+
if (row)
|
|
1092
|
+
setInstanceId(row.id);
|
|
1093
|
+
const rel = keep.targetId === undefined
|
|
1094
|
+
? keep.target
|
|
1095
|
+
? row?.relationships.find((candidate) => candidate.target === keep.target)
|
|
1096
|
+
: undefined
|
|
1097
|
+
: row?.relationships.find((candidate) => candidate.targetId === keep.targetId && candidate.slot === keep.slot);
|
|
1098
|
+
if (rel)
|
|
1099
|
+
setRelationshipKey(rel.info.path);
|
|
1100
|
+
return next;
|
|
1101
|
+
};
|
|
1102
|
+
const sourceSteps = (operation, status = 'queued') => {
|
|
1103
|
+
if (operation === 'shared.remove')
|
|
1104
|
+
return [
|
|
1105
|
+
{ name: 'plan recheck', status },
|
|
1106
|
+
{ name: 'Relationship cascade', status },
|
|
1107
|
+
{ name: 'recovery manifest', status },
|
|
1108
|
+
{ name: 'Vercel skills remove', status },
|
|
1109
|
+
{ name: 'provenance verification', status },
|
|
1110
|
+
{ name: 'final rescan', status },
|
|
1111
|
+
];
|
|
1112
|
+
const command = operation === 'shared.update' ? 'update' : 'add';
|
|
1113
|
+
return [
|
|
1114
|
+
{ name: 'plan recheck', status },
|
|
1115
|
+
{ name: 'upstream ownership handoff', status },
|
|
1116
|
+
{ name: `Vercel skills ${command}`, status },
|
|
1117
|
+
{ name: 'Desired state preservation', status },
|
|
1118
|
+
{ name: command === 'update' ? 'per-item verification' : 'provenance verification', status },
|
|
1119
|
+
{ name: 'final rescan', status },
|
|
1120
|
+
];
|
|
1121
|
+
};
|
|
1122
|
+
const applySourceOperation = (operation) => {
|
|
1123
|
+
try {
|
|
1124
|
+
assertSourceRefreshIdle();
|
|
1125
|
+
if (operation.kind === 'remove') {
|
|
1126
|
+
const plan = operation.plan;
|
|
1127
|
+
const result = operation.runStep === 'source'
|
|
1128
|
+
? sharedRemove(home, [plan.source.name], {
|
|
1129
|
+
sourceConfirmed: true,
|
|
1130
|
+
expected: plan,
|
|
1131
|
+
})
|
|
1132
|
+
: sharedRemoveCascade(home, [plan.source.name], plan);
|
|
1133
|
+
const finalSnapshot = sourceTakeSnapshot();
|
|
1134
|
+
const truth = verifySourceMutation(home, plan, result);
|
|
1135
|
+
setSourceSnapshot(finalSnapshot);
|
|
1136
|
+
if (operation.runStep !== 'source') {
|
|
1137
|
+
const steps = sourceSteps(plan.operation);
|
|
1138
|
+
for (const step of steps.slice(0, 3))
|
|
1139
|
+
step.status = 'succeeded';
|
|
1140
|
+
setSourceOperation({
|
|
1141
|
+
...operation,
|
|
1142
|
+
phase: 'source-confirm',
|
|
1143
|
+
result,
|
|
1144
|
+
truth,
|
|
1145
|
+
outcome: 'partial',
|
|
1146
|
+
retry: false,
|
|
1147
|
+
scroll: 0,
|
|
1148
|
+
steps,
|
|
1149
|
+
log: 'Known Relationship cascade completed; source preserved pending confirmation.',
|
|
1150
|
+
});
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
setSourceOperation({
|
|
1154
|
+
...operation,
|
|
1155
|
+
phase: 'verify',
|
|
1156
|
+
result,
|
|
1157
|
+
truth,
|
|
1158
|
+
outcome: 'succeeded',
|
|
1159
|
+
retry: false,
|
|
1160
|
+
scroll: 0,
|
|
1161
|
+
steps: sourceSteps(plan.operation, 'succeeded'),
|
|
1162
|
+
log: 'Pinned Vercel skills remove completed; final filesystem rescan succeeded.',
|
|
1163
|
+
});
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
if (operation.kind === 'update') {
|
|
1167
|
+
const retryNames = operation.retry && operation.result
|
|
1168
|
+
? operation.result.items.filter(({ outcome }) => outcome === 'failed').map(({ name }) => name)
|
|
1169
|
+
: operation.plan.items.filter(({ reason }) => reason !== 'unmarked').map(({ name }) => name);
|
|
1170
|
+
const applyPlan = operation.retry ? planSharedUpdate(home, retryNames) : operation.plan;
|
|
1171
|
+
if (operation.retry) {
|
|
1172
|
+
const original = new Map(operation.plan.items.map((item) => [item.name, item]));
|
|
1173
|
+
const changed = applyPlan.items.some((item) => {
|
|
1174
|
+
const expected = original.get(item.name);
|
|
1175
|
+
return !expected || !item.included || item.identity !== expected.identity;
|
|
1176
|
+
});
|
|
1177
|
+
if (changed)
|
|
1178
|
+
throw new Error('Source update intent changed; new preview required.');
|
|
1179
|
+
}
|
|
1180
|
+
const attempt = sharedUpdate(home, retryNames, undefined, applyPlan);
|
|
1181
|
+
const result = operation.retry && operation.result
|
|
1182
|
+
? {
|
|
1183
|
+
actual: attempt.actual,
|
|
1184
|
+
drift: [...new Set([...operation.result.drift, ...attempt.drift])],
|
|
1185
|
+
items: [
|
|
1186
|
+
...operation.result.items.filter(({ outcome }) => outcome !== 'failed'),
|
|
1187
|
+
...attempt.items,
|
|
1188
|
+
],
|
|
1189
|
+
}
|
|
1190
|
+
: attempt;
|
|
1191
|
+
const finalSnapshot = sourceTakeSnapshot();
|
|
1192
|
+
setSourceSnapshot(finalSnapshot);
|
|
1193
|
+
const failed = result.items.filter(({ outcome }) => outcome === 'failed');
|
|
1194
|
+
const truth = verifySourceMutation(home, operation.plan, result);
|
|
1195
|
+
let outcome = 'succeeded';
|
|
1196
|
+
if (failed.length > 0)
|
|
1197
|
+
outcome = result.items.some((item) => item.outcome === 'updated') ? 'partial' : 'failed';
|
|
1198
|
+
setSourceOperation({
|
|
1199
|
+
...operation,
|
|
1200
|
+
phase: failed.length > 0 ? 'failed' : 'verify',
|
|
1201
|
+
plan: operation.plan,
|
|
1202
|
+
result,
|
|
1203
|
+
truth,
|
|
1204
|
+
outcome,
|
|
1205
|
+
retry: false,
|
|
1206
|
+
scroll: 0,
|
|
1207
|
+
steps: sourceSteps(operation.plan.operation, 'succeeded').map((step) => failed.length > 0 && step.name === 'Vercel skills update'
|
|
1208
|
+
? { ...step, status: 'failed' }
|
|
1209
|
+
: step),
|
|
1210
|
+
error: failed.length > 0
|
|
1211
|
+
? `${failed.length} update${failed.length === 1 ? '' : 's'} failed`
|
|
1212
|
+
: undefined,
|
|
1213
|
+
log: [
|
|
1214
|
+
...result.items.flatMap(({ name, log }) => log ? [`[${name}]`, log] : []),
|
|
1215
|
+
'Final filesystem rescan succeeded.',
|
|
1216
|
+
].join('\n'),
|
|
1217
|
+
});
|
|
1218
|
+
const completed = new Set(result.items
|
|
1219
|
+
.filter(({ outcome }) => outcome === 'updated')
|
|
1220
|
+
.map(({ slot }) => slot));
|
|
1221
|
+
if (completed.size > 0)
|
|
1222
|
+
setSourceMarks((marks) => new Set([...marks].filter((id) => {
|
|
1223
|
+
const row = sourceInventory.find(({ id: resourceId }) => resourceId === id);
|
|
1224
|
+
return !row?.updateAvailability || !completed.has(row.updateAvailability.slot);
|
|
1225
|
+
})));
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
let plan = operation.plan;
|
|
1229
|
+
if (operation.retry) {
|
|
1230
|
+
const fresh = planSharedAdd(home, operation.candidate.source, operation.candidate.name, Boolean(operation.plan.replacement));
|
|
1231
|
+
if (!sameSourceIntent(operation.plan, fresh))
|
|
1232
|
+
throw new Error('Source intent changed; new preview required.');
|
|
1233
|
+
plan = fresh;
|
|
1234
|
+
}
|
|
1235
|
+
const result = sharedAdd(home, operation.candidate.source, operation.candidate.name, Boolean(plan.replace), undefined, plan);
|
|
1236
|
+
const finalSnapshot = sourceTakeSnapshot();
|
|
1237
|
+
const truth = verifySourceMutation(home, plan, result);
|
|
1238
|
+
setSourceSnapshot(finalSnapshot);
|
|
1239
|
+
setSourceOperation({
|
|
1240
|
+
...operation,
|
|
1241
|
+
phase: 'verify',
|
|
1242
|
+
plan,
|
|
1243
|
+
result,
|
|
1244
|
+
truth,
|
|
1245
|
+
outcome: 'succeeded',
|
|
1246
|
+
retry: false,
|
|
1247
|
+
scroll: 0,
|
|
1248
|
+
steps: sourceSteps(plan.operation, 'succeeded'),
|
|
1249
|
+
log: 'Pinned Vercel skills handoff completed; final filesystem rescan succeeded.',
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1252
|
+
catch (error) {
|
|
1253
|
+
const failure = error;
|
|
1254
|
+
const message = failure.message;
|
|
1255
|
+
const preflightFailure = failure.code === 'concurrent_modification' ||
|
|
1256
|
+
/changed after preview|new preview required|operation already in progress/i.test(message);
|
|
1257
|
+
const failureStage = preflightFailure ? 'preflight' : failure.details?.stage ?? 'preflight';
|
|
1258
|
+
let finalSnapshot;
|
|
1259
|
+
try {
|
|
1260
|
+
finalSnapshot = sourceTakeSnapshot();
|
|
1261
|
+
setSourceSnapshot(finalSnapshot);
|
|
1262
|
+
}
|
|
1263
|
+
catch {
|
|
1264
|
+
// The operation error remains primary; failure truth reports the unavailable rescan.
|
|
1265
|
+
}
|
|
1266
|
+
const displayError = preflightFailure ? `${message} New preview required.` : message;
|
|
1267
|
+
if (operation.kind === 'update') {
|
|
1268
|
+
const steps = sourceSteps(operation.plan.operation);
|
|
1269
|
+
steps[0].status = 'failed';
|
|
1270
|
+
for (const step of steps.slice(1, -1))
|
|
1271
|
+
step.status = 'skipped';
|
|
1272
|
+
steps.at(-1).status = finalSnapshot ? 'succeeded' : 'failed';
|
|
1273
|
+
const result = operation.result ?? {
|
|
1274
|
+
actual: finalSnapshot ? 'final filesystem rescan completed' : 'rescan unavailable',
|
|
1275
|
+
drift: [],
|
|
1276
|
+
items: operation.plan.items.map((item) => ({
|
|
1277
|
+
...item,
|
|
1278
|
+
outcome: item.included ? 'failed' : 'skipped',
|
|
1279
|
+
...(item.included ? { reason: displayError } : {}),
|
|
1280
|
+
})),
|
|
1281
|
+
};
|
|
1282
|
+
const truth = finalSnapshot
|
|
1283
|
+
? verifySourceMutation(home, operation.plan, result)
|
|
1284
|
+
: undefined;
|
|
1285
|
+
setSourceOperation({
|
|
1286
|
+
...operation,
|
|
1287
|
+
phase: 'failed',
|
|
1288
|
+
result,
|
|
1289
|
+
truth,
|
|
1290
|
+
outcome: result.items.some(({ outcome }) => outcome === 'updated') ? 'partial' : 'failed',
|
|
1291
|
+
retry: false,
|
|
1292
|
+
scroll: 0,
|
|
1293
|
+
error: displayError,
|
|
1294
|
+
log: message,
|
|
1295
|
+
steps,
|
|
1296
|
+
});
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1299
|
+
const plan = operation.plan;
|
|
1300
|
+
const truth = finalSnapshot
|
|
1301
|
+
? verifySourceMutation(home, plan, { actual: 'rescan unavailable', drift: [] })
|
|
1302
|
+
: undefined;
|
|
1303
|
+
const steps = sourceSteps(plan.operation);
|
|
1304
|
+
if (operation.kind === 'remove') {
|
|
1305
|
+
if (operation.runStep === 'source') {
|
|
1306
|
+
for (const step of steps.slice(0, 3))
|
|
1307
|
+
step.status = 'succeeded';
|
|
1308
|
+
if (steps[3])
|
|
1309
|
+
steps[3].status = 'failed';
|
|
1310
|
+
if (steps[4])
|
|
1311
|
+
steps[4].status = 'skipped';
|
|
1312
|
+
}
|
|
1313
|
+
else if (failureStage === 'preflight') {
|
|
1314
|
+
if (steps[0])
|
|
1315
|
+
steps[0].status = 'failed';
|
|
1316
|
+
for (const step of steps.slice(1, 5))
|
|
1317
|
+
step.status = 'skipped';
|
|
1318
|
+
}
|
|
1319
|
+
else {
|
|
1320
|
+
if (steps[0])
|
|
1321
|
+
steps[0].status = 'succeeded';
|
|
1322
|
+
if (steps[1])
|
|
1323
|
+
steps[1].status = 'failed';
|
|
1324
|
+
for (const step of steps.slice(2, 5))
|
|
1325
|
+
step.status = 'skipped';
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
else if (failureStage === 'preflight') {
|
|
1329
|
+
if (steps[0])
|
|
1330
|
+
steps[0].status = 'failed';
|
|
1331
|
+
for (const step of steps.slice(1, 5))
|
|
1332
|
+
step.status = 'skipped';
|
|
1333
|
+
}
|
|
1334
|
+
else if (failureStage === 'upstream') {
|
|
1335
|
+
for (const step of steps.slice(0, 2))
|
|
1336
|
+
step.status = 'succeeded';
|
|
1337
|
+
if (steps[2])
|
|
1338
|
+
steps[2].status = 'failed';
|
|
1339
|
+
if (steps[3])
|
|
1340
|
+
steps[3].status = 'succeeded';
|
|
1341
|
+
if (steps[4])
|
|
1342
|
+
steps[4].status = 'skipped';
|
|
1343
|
+
}
|
|
1344
|
+
else {
|
|
1345
|
+
for (const step of steps.slice(0, 4))
|
|
1346
|
+
step.status = 'succeeded';
|
|
1347
|
+
if (steps[4])
|
|
1348
|
+
steps[4].status = 'failed';
|
|
1349
|
+
}
|
|
1350
|
+
if (steps[5])
|
|
1351
|
+
steps[5].status = finalSnapshot ? 'succeeded' : 'failed';
|
|
1352
|
+
const partial = operation.kind === 'remove' && operation.runStep === 'source' ||
|
|
1353
|
+
failure.details?.partialEffects === 'present' ||
|
|
1354
|
+
(failure.details?.completedWork?.length ?? 0) > 0;
|
|
1355
|
+
setSourceOperation({
|
|
1356
|
+
...operation,
|
|
1357
|
+
phase: 'failed',
|
|
1358
|
+
truth,
|
|
1359
|
+
outcome: partial ? 'partial' : 'failed',
|
|
1360
|
+
retry: false,
|
|
1361
|
+
scroll: 0,
|
|
1362
|
+
error: displayError,
|
|
1363
|
+
log: message,
|
|
1364
|
+
steps,
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
1367
|
+
};
|
|
1368
|
+
useEffect(() => {
|
|
1369
|
+
if (sourceOperation?.phase !== 'run')
|
|
1370
|
+
return;
|
|
1371
|
+
const pending = sourceOperation;
|
|
1372
|
+
const timer = setTimeout(() => applySourceOperation(pending), 0);
|
|
1373
|
+
return () => clearTimeout(timer);
|
|
1374
|
+
// The run phase owns one immutable operation; later state transitions must not restart it.
|
|
1375
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1376
|
+
}, [sourceOperation?.phase]);
|
|
1377
|
+
useEffect(() => {
|
|
1378
|
+
if (sourceOperation?.phase === 'source-confirm' || sourceOperation?.phase === 'verify' || sourceOperation?.phase === 'failed')
|
|
1379
|
+
setLatestSourceOperation(sourceOperation);
|
|
1380
|
+
}, [sourceOperation]);
|
|
1381
|
+
const beginSourceUpdate = (candidates, considered = candidates) => {
|
|
1382
|
+
if (candidates.length === 0) {
|
|
1383
|
+
setFeedback('Batch update: mark at least one Inventory resource');
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1386
|
+
const mutable = considered.filter((row) => row.updateAvailability && mutableSourceRelationship(row));
|
|
1387
|
+
const selectedIds = new Set(candidates.map(({ id }) => id));
|
|
1388
|
+
const selected = mutable.filter(({ id }) => selectedIds.has(id));
|
|
1389
|
+
const selectionExclusions = considered.flatMap((row) => {
|
|
1390
|
+
if (mutable.some(({ id }) => id === row.id))
|
|
1391
|
+
return [];
|
|
1392
|
+
const marked = selectedIds.has(row.id);
|
|
1393
|
+
const inherited = sourceRelationships(row).some(({ info }) => info.readOnly);
|
|
1394
|
+
let reason = 'unmarked';
|
|
1395
|
+
if (marked)
|
|
1396
|
+
reason = inherited ? 'inherited read-only' : 'unknown Vercel ownership';
|
|
1397
|
+
return [{ identity: row.id, name: row.name, reason }];
|
|
1398
|
+
});
|
|
1399
|
+
if (selected.length === 0) {
|
|
1400
|
+
setFeedback('Update unavailable: marked resources are inherited or lack proven scope-local Vercel ownership');
|
|
1401
|
+
return;
|
|
1402
|
+
}
|
|
1403
|
+
try {
|
|
1404
|
+
const plan = planSharedUpdate(home, selected.map((row) => row.updateAvailability.slot), undefined, mutable.map((row) => row.updateAvailability.slot));
|
|
1405
|
+
if (!plan.items.some(({ included }) => included)) {
|
|
1406
|
+
setFeedback('Update unavailable: no selected resource has an identity-matching available update');
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
const visibilityConsequences = selected.map((row) => {
|
|
1410
|
+
const states = resolveVisibility(home, row, undefined, undefined, undefined, sourceSnapshot)?.harnesses.map((harness) => `${harness.name}=${harness.effectiveVisibility}`) ?? ['unknown'];
|
|
1411
|
+
return `${row.name}: Relationships remain in place; ${states.join(', ')} on next load; Mirror content may require explicit reconcile.`;
|
|
1412
|
+
});
|
|
1413
|
+
setSourceOperation({
|
|
1414
|
+
kind: 'update',
|
|
1415
|
+
phase: 'preview',
|
|
1416
|
+
plan,
|
|
1417
|
+
selectionExclusions,
|
|
1418
|
+
visibilityConsequences,
|
|
1419
|
+
steps: sourceSteps(plan.operation),
|
|
1420
|
+
scroll: 0,
|
|
1421
|
+
});
|
|
1422
|
+
}
|
|
1423
|
+
catch (error) {
|
|
1424
|
+
setFeedback(error.message);
|
|
1425
|
+
}
|
|
1426
|
+
};
|
|
1427
|
+
useInput((input, key) => {
|
|
1428
|
+
if (sourceLogOpen) {
|
|
1429
|
+
if (key.escape) {
|
|
1430
|
+
setSourceLogOpen(false);
|
|
1431
|
+
setSourceLogScroll(0);
|
|
1432
|
+
}
|
|
1433
|
+
else if (key.downArrow || input === 'j') {
|
|
1434
|
+
setSourceLogScroll((value) => Math.min(Math.max(0, sourceLogLines.length - 1), value + 1));
|
|
1435
|
+
}
|
|
1436
|
+
else if (key.upArrow || input === 'k') {
|
|
1437
|
+
setSourceLogScroll((value) => Math.max(0, value - 1));
|
|
1438
|
+
}
|
|
1439
|
+
return;
|
|
1440
|
+
}
|
|
1441
|
+
if (sourceOperation) {
|
|
1442
|
+
if (input === 'l' && sourceOperation.phase !== 'run') {
|
|
1443
|
+
setSourceLogOpen(true);
|
|
1444
|
+
setSourceLogScroll(0);
|
|
1445
|
+
return;
|
|
1446
|
+
}
|
|
1447
|
+
const operationLines = sourceOperationLines(sourceOperation).lines;
|
|
1448
|
+
if (sourceOperation.phase !== 'run' && (key.downArrow || input === 'j')) {
|
|
1449
|
+
setSourceOperation({
|
|
1450
|
+
...sourceOperation,
|
|
1451
|
+
scroll: Math.min(Math.max(0, operationLines.length - 1), sourceOperation.scroll + 1),
|
|
1452
|
+
});
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
if (sourceOperation.phase !== 'run' && (key.upArrow || input === 'k')) {
|
|
1456
|
+
setSourceOperation({ ...sourceOperation, scroll: Math.max(0, sourceOperation.scroll - 1) });
|
|
1457
|
+
return;
|
|
1458
|
+
}
|
|
1459
|
+
if (sourceOperation.phase === 'preview') {
|
|
1460
|
+
const blocked = sourceOperation.kind === 'update'
|
|
1461
|
+
? sourceOperation.plan.blockers.length > 0 ||
|
|
1462
|
+
!sourceOperation.plan.items.some(({ included }) => included)
|
|
1463
|
+
: (sourceOperation.plan.blockers?.length ?? 0) > 0;
|
|
1464
|
+
if (key.escape)
|
|
1465
|
+
setSourceOperation(undefined);
|
|
1466
|
+
else if (key.return && !blocked)
|
|
1467
|
+
setSourceOperation({ ...sourceOperation, phase: 'confirm', scroll: 0 });
|
|
1468
|
+
return;
|
|
1469
|
+
}
|
|
1470
|
+
if (sourceOperation.phase === 'confirm') {
|
|
1471
|
+
if (key.escape)
|
|
1472
|
+
setSourceOperation(undefined);
|
|
1473
|
+
else if (key.return)
|
|
1474
|
+
setSourceOperation({
|
|
1475
|
+
...sourceOperation,
|
|
1476
|
+
phase: 'run',
|
|
1477
|
+
runStep: sourceOperation.plan.operation === 'shared.remove' ? 'cascade' : undefined,
|
|
1478
|
+
retry: false,
|
|
1479
|
+
scroll: 0,
|
|
1480
|
+
steps: sourceSteps(sourceOperation.plan.operation).map((step, index) => ({
|
|
1481
|
+
...step,
|
|
1482
|
+
status: index === 0 ? 'running' : 'queued',
|
|
1483
|
+
})),
|
|
1484
|
+
});
|
|
1485
|
+
return;
|
|
1486
|
+
}
|
|
1487
|
+
if (sourceOperation.phase === 'source-confirm') {
|
|
1488
|
+
if (key.escape) {
|
|
1489
|
+
setSourceSnapshot(sourceTakeSnapshot());
|
|
1490
|
+
setFeedback('Source preserved; Relationship cascade remains complete.');
|
|
1491
|
+
setSourceOperation(undefined);
|
|
1492
|
+
}
|
|
1493
|
+
else if (key.return)
|
|
1494
|
+
setSourceOperation({
|
|
1495
|
+
...sourceOperation,
|
|
1496
|
+
phase: 'run',
|
|
1497
|
+
runStep: 'source',
|
|
1498
|
+
retry: false,
|
|
1499
|
+
scroll: 0,
|
|
1500
|
+
steps: sourceOperation.steps.map((step, index) => ({
|
|
1501
|
+
...step,
|
|
1502
|
+
status: index === 3 ? 'running' : step.status,
|
|
1503
|
+
})),
|
|
1504
|
+
});
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
1507
|
+
if (sourceOperation.phase === 'verify') {
|
|
1508
|
+
if (key.return || key.escape)
|
|
1509
|
+
setSourceOperation(undefined);
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
if (sourceOperation.phase === 'failed' && input === 't') {
|
|
1513
|
+
setSourceOperation({
|
|
1514
|
+
...sourceOperation,
|
|
1515
|
+
phase: 'run',
|
|
1516
|
+
retry: true,
|
|
1517
|
+
scroll: 0,
|
|
1518
|
+
steps: sourceSteps(sourceOperation.plan.operation).map((step, index) => ({
|
|
1519
|
+
...step,
|
|
1520
|
+
status: index === 0 ? 'running' : 'queued',
|
|
1521
|
+
})),
|
|
1522
|
+
});
|
|
1523
|
+
}
|
|
1524
|
+
else if (sourceOperation.phase === 'failed' && key.escape)
|
|
1525
|
+
setSourceOperation(undefined);
|
|
1526
|
+
return;
|
|
1527
|
+
}
|
|
1528
|
+
if (sourceDetailOpen) {
|
|
1529
|
+
if (key.escape)
|
|
1530
|
+
setSourceDetailOpen(false);
|
|
1531
|
+
return;
|
|
1532
|
+
}
|
|
1533
|
+
if (targetInfoOpen) {
|
|
1534
|
+
if (key.escape)
|
|
1535
|
+
setTargetInfoOpen(false);
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1538
|
+
if (explainModal) {
|
|
1539
|
+
if (key.escape)
|
|
1540
|
+
return setExplainModal(null);
|
|
1541
|
+
if (key.tab && explainHarnesses.length > 0) {
|
|
1542
|
+
const next = explainHarnesses[(explainHarnessIndex + 1) % explainHarnesses.length];
|
|
1543
|
+
if (next)
|
|
1544
|
+
return setExplainModal({ ...explainModal, harness: next.key, scroll: 0 });
|
|
1545
|
+
}
|
|
1546
|
+
if (input === 'v')
|
|
1547
|
+
return setExplainModal({ ...explainModal, want: 'visible', scroll: 0 });
|
|
1548
|
+
if (input === 'h')
|
|
1549
|
+
return setExplainModal({ ...explainModal, want: 'hidden', scroll: 0 });
|
|
1550
|
+
if (input === 'd') {
|
|
1551
|
+
const { want: _, ...diagnosis } = explainModal;
|
|
1552
|
+
return setExplainModal({ ...diagnosis, scroll: 0 });
|
|
1553
|
+
}
|
|
1554
|
+
if (key.downArrow || input === 'j')
|
|
1555
|
+
return setExplainModal({ ...explainModal, scroll: Math.min(Math.max(0, explainDetailLines.length - 1), explainModal.scroll + 1) });
|
|
1556
|
+
if (key.upArrow || input === 'k')
|
|
1557
|
+
return setExplainModal({ ...explainModal, scroll: Math.max(0, explainModal.scroll - 1) });
|
|
1558
|
+
if (key.pageDown || (key.ctrl && input === 'd'))
|
|
1559
|
+
return setExplainModal({ ...explainModal, scroll: Math.min(Math.max(0, explainDetailLines.length - 1), explainModal.scroll + modalPage) });
|
|
1560
|
+
if (key.pageUp || (key.ctrl && input === 'u'))
|
|
1561
|
+
return setExplainModal({ ...explainModal, scroll: Math.max(0, explainModal.scroll - modalPage) });
|
|
1562
|
+
return;
|
|
1563
|
+
}
|
|
1564
|
+
if (modal) {
|
|
1565
|
+
if (key.escape)
|
|
1566
|
+
return setModal(null);
|
|
1567
|
+
if (key.downArrow || input === 'j')
|
|
1568
|
+
return setModal({ ...modal, scroll: Math.min(modalLines.length - 1, modal.scroll + 1) });
|
|
1569
|
+
if (key.upArrow || input === 'k')
|
|
1570
|
+
return setModal({ ...modal, scroll: Math.max(0, modal.scroll - 1) });
|
|
1571
|
+
if (key.pageDown || (key.ctrl && input === 'd'))
|
|
1572
|
+
return setModal({ ...modal, scroll: Math.min(modalLines.length - 1, modal.scroll + modalPage) });
|
|
1573
|
+
if (key.pageUp || (key.ctrl && input === 'u'))
|
|
1574
|
+
return setModal({ ...modal, scroll: Math.max(0, modal.scroll - modalPage) });
|
|
1575
|
+
return;
|
|
1576
|
+
}
|
|
1577
|
+
if (manage) {
|
|
1578
|
+
const current = rows.find((candidate) => candidate.id === manage.rowId);
|
|
1579
|
+
if (!current)
|
|
1580
|
+
return setManage(null);
|
|
1581
|
+
const row = current;
|
|
1582
|
+
const selector = `skill:${row.id}`;
|
|
1583
|
+
const assignedTags = snapshot.catalog.tags[row.id] ?? [];
|
|
1584
|
+
const tagNames = knownTagNames(snapshot.catalog.tags);
|
|
1585
|
+
const presetNames = Object.keys(snapshot.catalog.presets)
|
|
1586
|
+
.sort((a, b) => a.localeCompare(b));
|
|
1587
|
+
const rowCount = (manage.section === 'tags' ? tagNames.length : presetNames.length) + 1;
|
|
1588
|
+
const draft = manage.input;
|
|
1589
|
+
if (draft) {
|
|
1590
|
+
if (key.escape)
|
|
1591
|
+
return setManage({ ...manage, input: undefined });
|
|
1592
|
+
if (key.return) {
|
|
1593
|
+
const value = draft.value.trim();
|
|
1594
|
+
if (value) {
|
|
1595
|
+
try {
|
|
1596
|
+
prepareMutation();
|
|
1597
|
+
if (draft.kind === 'tag') {
|
|
1598
|
+
addResourceTags(home, selector, [value]);
|
|
1599
|
+
setFeedback(`Tagged ${row.name}: ${value}`);
|
|
1600
|
+
}
|
|
1601
|
+
else {
|
|
1602
|
+
createPreset(home, value, [selector]);
|
|
1603
|
+
setFeedback(`Created preset ${value} with ${row.name}`);
|
|
1604
|
+
}
|
|
1605
|
+
refresh({ rowId: row.id });
|
|
1606
|
+
}
|
|
1607
|
+
catch (err) {
|
|
1608
|
+
setFeedback(err.message);
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
return setManage({ ...manage, input: undefined });
|
|
1612
|
+
}
|
|
1613
|
+
if (key.backspace || key.delete || input === '\x7f')
|
|
1614
|
+
return setManage({ ...manage, input: { ...draft, value: draft.value.slice(0, -1) } });
|
|
1615
|
+
if (input && !key.ctrl && !key.meta)
|
|
1616
|
+
return setManage({ ...manage, input: { ...draft, value: draft.value + input } });
|
|
1617
|
+
return;
|
|
1618
|
+
}
|
|
1619
|
+
if (key.escape)
|
|
1620
|
+
return setManage(null);
|
|
1621
|
+
if (key.tab)
|
|
1622
|
+
return setManage({
|
|
1623
|
+
...manage,
|
|
1624
|
+
section: manage.section === 'tags' ? 'presets' : 'tags',
|
|
1625
|
+
index: 0,
|
|
1626
|
+
});
|
|
1627
|
+
if (key.downArrow || input === 'j')
|
|
1628
|
+
return setManage({ ...manage, index: Math.min(rowCount - 1, manage.index + 1) });
|
|
1629
|
+
if (key.upArrow || input === 'k')
|
|
1630
|
+
return setManage({ ...manage, index: Math.max(0, manage.index - 1) });
|
|
1631
|
+
const onActionRow = manage.index === rowCount - 1;
|
|
1632
|
+
if (manage.section === 'tags') {
|
|
1633
|
+
if ((key.return || input === 'a') && onActionRow)
|
|
1634
|
+
return setManage({ ...manage, input: { kind: 'tag', value: '' } });
|
|
1635
|
+
const tag = tagNames[manage.index];
|
|
1636
|
+
if (!onActionRow && tag !== undefined && (input === ' ' || input === 'x')) {
|
|
1637
|
+
const assigned = assignedTags.includes(tag);
|
|
1638
|
+
if (input === 'x' && !assigned)
|
|
1639
|
+
return;
|
|
1640
|
+
try {
|
|
1641
|
+
prepareMutation();
|
|
1642
|
+
if (assigned)
|
|
1643
|
+
removeResourceTags(home, selector, [tag]);
|
|
1644
|
+
else
|
|
1645
|
+
addResourceTags(home, selector, [tag]);
|
|
1646
|
+
refresh({ rowId: row.id });
|
|
1647
|
+
setFeedback(assigned
|
|
1648
|
+
? `Removed tag ${tag} from ${row.name}`
|
|
1649
|
+
: `Tagged ${row.name}: ${tag}`);
|
|
1650
|
+
}
|
|
1651
|
+
catch (err) {
|
|
1652
|
+
setFeedback(err.message);
|
|
1653
|
+
}
|
|
1654
|
+
return setManage({ ...manage, index: Math.min(manage.index, Math.max(0, tagNames.length - 1)) });
|
|
1655
|
+
}
|
|
1656
|
+
return;
|
|
1657
|
+
}
|
|
1658
|
+
if ((key.return || input === 'a') && onActionRow)
|
|
1659
|
+
return setManage({ ...manage, input: { kind: 'preset', value: '' } });
|
|
1660
|
+
if (input === ' ' && !onActionRow) {
|
|
1661
|
+
const name = presetNames[manage.index];
|
|
1662
|
+
if (name !== undefined) {
|
|
1663
|
+
try {
|
|
1664
|
+
prepareMutation();
|
|
1665
|
+
const member = (snapshot.catalog.presets[name]?.selectors ?? []).includes(selector);
|
|
1666
|
+
if (member)
|
|
1667
|
+
removePresetSelectors(home, name, [selector]);
|
|
1668
|
+
else
|
|
1669
|
+
addPresetSelectors(home, name, [selector]);
|
|
1670
|
+
refresh({ rowId: row.id });
|
|
1671
|
+
setFeedback(member
|
|
1672
|
+
? `Removed ${row.name} from preset ${name}`
|
|
1673
|
+
: `Added ${row.name} to preset ${name}`);
|
|
1674
|
+
}
|
|
1675
|
+
catch (err) {
|
|
1676
|
+
setFeedback(err.message);
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
return;
|
|
1680
|
+
}
|
|
1681
|
+
return;
|
|
1682
|
+
}
|
|
1683
|
+
if (searching) {
|
|
1684
|
+
if (key.escape) {
|
|
1685
|
+
setQuery('');
|
|
1686
|
+
return setSearching(false);
|
|
1687
|
+
}
|
|
1688
|
+
if (key.return) {
|
|
1689
|
+
if (tab === 'source') {
|
|
1690
|
+
try {
|
|
1691
|
+
const result = sharedFind(home, query.trim().split(/\s+/).filter(Boolean), undefined, false);
|
|
1692
|
+
setSourceCandidates(result.candidates);
|
|
1693
|
+
setSourceCandidateId(result.candidates[0]
|
|
1694
|
+
? candidateId(result.candidates[0])
|
|
1695
|
+
: undefined);
|
|
1696
|
+
setFeedback(result.raw ? 'Source returned unstructured output' : `${result.candidates.length} candidates`);
|
|
1697
|
+
}
|
|
1698
|
+
catch (error) {
|
|
1699
|
+
setFeedback(error.message);
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
return setSearching(false);
|
|
1703
|
+
}
|
|
1704
|
+
if (key.backspace || key.delete || input === '\x7f')
|
|
1705
|
+
return setQuery((value) => value.slice(0, -1));
|
|
1706
|
+
if (input && !key.ctrl && !key.meta)
|
|
1707
|
+
return setQuery((value) => value + input);
|
|
1708
|
+
return;
|
|
1709
|
+
}
|
|
1710
|
+
if (batchConfirm) {
|
|
1711
|
+
if (input === 'y') {
|
|
1712
|
+
try {
|
|
1713
|
+
prepareMutation();
|
|
1714
|
+
}
|
|
1715
|
+
catch (err) {
|
|
1716
|
+
setFeedback(err.message);
|
|
1717
|
+
return setBatchConfirm(null);
|
|
1718
|
+
}
|
|
1719
|
+
let applied = 0;
|
|
1720
|
+
const failures = [];
|
|
1721
|
+
for (const plan of batchConfirm.plans) {
|
|
1722
|
+
try {
|
|
1723
|
+
applyActivationPlan(home, plan);
|
|
1724
|
+
applied++;
|
|
1725
|
+
}
|
|
1726
|
+
catch (err) {
|
|
1727
|
+
failures.push(err.message);
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
refresh({
|
|
1731
|
+
rowId: selectedRow?.id,
|
|
1732
|
+
target: selectedTarget?.name,
|
|
1733
|
+
});
|
|
1734
|
+
setFeedback(`Batch ${batchConfirm.intent} @ ${batchConfirm.targetName}: ${applied} applied` +
|
|
1735
|
+
(failures.length > 0 ? `, ${failures.length} failed` : ''));
|
|
1736
|
+
return setBatchConfirm(null);
|
|
1737
|
+
}
|
|
1738
|
+
if (input === 'n' || key.escape)
|
|
1739
|
+
return setBatchConfirm(null);
|
|
1740
|
+
return;
|
|
1741
|
+
}
|
|
1742
|
+
if (batchTag) {
|
|
1743
|
+
if (key.escape)
|
|
1744
|
+
return setBatchTag(null);
|
|
1745
|
+
if (key.return) {
|
|
1746
|
+
const value = batchTag.value.trim();
|
|
1747
|
+
if (value && batch) {
|
|
1748
|
+
const markedRows = rows.filter((row) => row.realPath && batch.marks.has(markKey(home.configDir, row.realPath)));
|
|
1749
|
+
try {
|
|
1750
|
+
prepareMutation();
|
|
1751
|
+
}
|
|
1752
|
+
catch (err) {
|
|
1753
|
+
setFeedback(err.message);
|
|
1754
|
+
return setBatchTag(null);
|
|
1755
|
+
}
|
|
1756
|
+
let count = 0;
|
|
1757
|
+
const failures = [];
|
|
1758
|
+
for (const row of markedRows) {
|
|
1759
|
+
try {
|
|
1760
|
+
if (batchTag.action === 'add')
|
|
1761
|
+
addResourceTags(home, `skill:${row.id}`, [value]);
|
|
1762
|
+
else
|
|
1763
|
+
removeResourceTags(home, `skill:${row.id}`, [value]);
|
|
1764
|
+
count++;
|
|
1765
|
+
}
|
|
1766
|
+
catch (err) {
|
|
1767
|
+
failures.push(err.message);
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
refresh();
|
|
1771
|
+
setFeedback(`${batchTag.action === 'add' ? 'Tagged' : 'Untagged'} ${count} skills: ${value}` +
|
|
1772
|
+
(failures.length > 0 ? ` (${failures.length} failed)` : ''));
|
|
1773
|
+
}
|
|
1774
|
+
return setBatchTag(null);
|
|
1775
|
+
}
|
|
1776
|
+
if (key.backspace || key.delete || input === '\x7f')
|
|
1777
|
+
return setBatchTag({ ...batchTag, value: batchTag.value.slice(0, -1) });
|
|
1778
|
+
if (input && !key.ctrl && !key.meta)
|
|
1779
|
+
return setBatchTag({ ...batchTag, value: batchTag.value + input });
|
|
1780
|
+
return;
|
|
1781
|
+
}
|
|
1782
|
+
if (confirmation) {
|
|
1783
|
+
if (input === 'y') {
|
|
1784
|
+
const neighbor = tab === 'target'
|
|
1785
|
+
? entries[relationshipIndex + 1] ?? entries[relationshipIndex - 1]
|
|
1786
|
+
: undefined;
|
|
1787
|
+
try {
|
|
1788
|
+
prepareMutation();
|
|
1789
|
+
if (confirmation.kind === 'link' || confirmation.kind === 'mirror-create') {
|
|
1790
|
+
applyActivationPlan(home, planLink(home, confirmation.row.id, confirmation.target.name, planScope));
|
|
1791
|
+
}
|
|
1792
|
+
else if (confirmation.kind === 'unlink') {
|
|
1793
|
+
applyActivationPlan(home, planUnlink(home, confirmation.targetId, confirmation.slot, planScope));
|
|
1794
|
+
}
|
|
1795
|
+
else {
|
|
1796
|
+
applyActivationPlan(home, planMirrorAction(home, confirmation.targetId, confirmation.slot, confirmation.kind.replace('mirror-', ''), planScope));
|
|
1797
|
+
}
|
|
1798
|
+
refresh({ rowId: confirmation.row.id });
|
|
1799
|
+
if (neighbor)
|
|
1800
|
+
setRelationshipKey(neighbor.key);
|
|
1801
|
+
setFeedback(`${confirmation.kind === 'link' || confirmation.kind === 'mirror-create' ? 'Linked' : confirmation.kind === 'unlink' ? 'Unlinked' : confirmation.kind.replace('mirror-', 'Mirror ')} ${confirmation.row.name} @ ${confirmation.target.name}`);
|
|
1802
|
+
}
|
|
1803
|
+
catch (err) {
|
|
1804
|
+
setFeedback(err.message);
|
|
1805
|
+
}
|
|
1806
|
+
return setConfirmation(null);
|
|
1807
|
+
}
|
|
1808
|
+
if (input === 'n' || key.escape)
|
|
1809
|
+
return setConfirmation(null);
|
|
1810
|
+
return;
|
|
1811
|
+
}
|
|
1812
|
+
if (input === '1' || input === '2' || input === '3') {
|
|
1813
|
+
setBatch(null);
|
|
1814
|
+
setQuery('');
|
|
1815
|
+
if (input === '3') {
|
|
1816
|
+
// Source is Global-only (issue #145); pressing 3 refreshes the Global snapshot.
|
|
1817
|
+
try {
|
|
1818
|
+
setSourceSnapshot(sourceTakeSnapshot());
|
|
1819
|
+
}
|
|
1820
|
+
catch (error) {
|
|
1821
|
+
setFeedback(error.message);
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
setTab(input === '1' ? 'target' : input === '2' ? 'skill' : 'source');
|
|
1825
|
+
return;
|
|
1826
|
+
}
|
|
1827
|
+
if (tab === 'source') {
|
|
1828
|
+
if (input === 'q' || (key.ctrl && input === 'c'))
|
|
1829
|
+
return exit();
|
|
1830
|
+
if (input === 'l' && latestSourceOperation) {
|
|
1831
|
+
setSourceLogOpen(true);
|
|
1832
|
+
setSourceLogScroll(0);
|
|
1833
|
+
return;
|
|
1834
|
+
}
|
|
1835
|
+
if (input === 'p') {
|
|
1836
|
+
// Global-only: `p` never switches scope or mutates; it points to the
|
|
1837
|
+
// explicit exact-Project Source CLI (issue #145).
|
|
1838
|
+
setFeedback('exact Project Source is explicit CLI-only: skillspub project <path> shared …');
|
|
1839
|
+
return;
|
|
1840
|
+
}
|
|
1841
|
+
if (key.tab) {
|
|
1842
|
+
setSourceSurface((value) => value === 'catalog' ? 'inventory' : 'catalog');
|
|
1843
|
+
return;
|
|
1844
|
+
}
|
|
1845
|
+
if (input === '/') {
|
|
1846
|
+
setSourceSurface('catalog');
|
|
1847
|
+
setQuery('');
|
|
1848
|
+
setSearching(true);
|
|
1849
|
+
return;
|
|
1850
|
+
}
|
|
1851
|
+
if (input === 'a' && sourceSurface === 'catalog' && sourceCandidate) {
|
|
1852
|
+
try {
|
|
1853
|
+
const initial = planSharedAdd(home, sourceCandidate.source, sourceCandidate.name, false);
|
|
1854
|
+
const plan = initial.replacement
|
|
1855
|
+
? planSharedAdd(home, sourceCandidate.source, sourceCandidate.name, true)
|
|
1856
|
+
: initial;
|
|
1857
|
+
setSourceOperation({
|
|
1858
|
+
kind: 'add',
|
|
1859
|
+
phase: 'preview',
|
|
1860
|
+
plan,
|
|
1861
|
+
candidate: sourceCandidate,
|
|
1862
|
+
steps: sourceSteps(plan.operation),
|
|
1863
|
+
scroll: 0,
|
|
1864
|
+
});
|
|
1865
|
+
}
|
|
1866
|
+
catch (error) {
|
|
1867
|
+
setFeedback(error.message);
|
|
1868
|
+
}
|
|
1869
|
+
return;
|
|
1870
|
+
}
|
|
1871
|
+
if (input === 'r') {
|
|
1872
|
+
if (activeSourceRefresh.current) {
|
|
1873
|
+
setFeedback('Source refresh already in progress');
|
|
1874
|
+
return;
|
|
1875
|
+
}
|
|
1876
|
+
const resourceIdAtRefreshStart = sourceResource?.id;
|
|
1877
|
+
let refresh;
|
|
1878
|
+
try {
|
|
1879
|
+
refresh = startSourceRefresh(home);
|
|
1880
|
+
}
|
|
1881
|
+
catch (error) {
|
|
1882
|
+
setFeedback(error.message);
|
|
1883
|
+
return;
|
|
1884
|
+
}
|
|
1885
|
+
activeSourceRefresh.current = refresh;
|
|
1886
|
+
setFeedback('Refreshing Source update availability…');
|
|
1887
|
+
void refresh.result
|
|
1888
|
+
.then(({ availability, snapshot: next }) => {
|
|
1889
|
+
if (!mounted.current)
|
|
1890
|
+
return;
|
|
1891
|
+
const validIds = new Set(sourceInventoryRows(next).map(({ id }) => id));
|
|
1892
|
+
setSourceSnapshot(next);
|
|
1893
|
+
setSourceResourceId((current) => [current, resourceIdAtRefreshStart].find((id) => id && validIds.has(id)) ?? '');
|
|
1894
|
+
setSourceMarks((marks) => new Set([...marks].filter((id) => validIds.has(id))));
|
|
1895
|
+
setFeedback(`Refreshed ${availability.entries.length} managed resources`);
|
|
1896
|
+
})
|
|
1897
|
+
.catch((error) => {
|
|
1898
|
+
if (mounted.current)
|
|
1899
|
+
setFeedback(error.message);
|
|
1900
|
+
})
|
|
1901
|
+
.finally(() => {
|
|
1902
|
+
if (activeSourceRefresh.current === refresh)
|
|
1903
|
+
activeSourceRefresh.current = undefined;
|
|
1904
|
+
});
|
|
1905
|
+
return;
|
|
1906
|
+
}
|
|
1907
|
+
if (input === 'u' && sourceSurface === 'inventory' &&
|
|
1908
|
+
sourceResource?.updateAvailability?.status === 'available' &&
|
|
1909
|
+
mutableSourceRelationship(sourceResource)) {
|
|
1910
|
+
beginSourceUpdate([sourceResource]);
|
|
1911
|
+
return;
|
|
1912
|
+
}
|
|
1913
|
+
if (input === 'b' && sourceSurface === 'inventory') {
|
|
1914
|
+
beginSourceUpdate(sourceInventory.filter(({ id }) => sourceMarks.has(id)), sourceInventory);
|
|
1915
|
+
return;
|
|
1916
|
+
}
|
|
1917
|
+
if (key.downArrow || input === 'j') {
|
|
1918
|
+
if (sourceSurface === 'catalog') {
|
|
1919
|
+
const next = sourceCandidates[Math.min(sourceCandidates.length - 1, sourceCandidateIndex + 1)];
|
|
1920
|
+
if (next)
|
|
1921
|
+
setSourceCandidateId(candidateId(next));
|
|
1922
|
+
}
|
|
1923
|
+
else {
|
|
1924
|
+
const next = sourceInventory[Math.min(sourceInventory.length - 1, sourceResourceIndex + 1)];
|
|
1925
|
+
if (next)
|
|
1926
|
+
setSourceResourceId(next.id);
|
|
1927
|
+
}
|
|
1928
|
+
return;
|
|
1929
|
+
}
|
|
1930
|
+
if (key.upArrow || input === 'k') {
|
|
1931
|
+
if (sourceSurface === 'catalog') {
|
|
1932
|
+
const next = sourceCandidates[Math.max(0, sourceCandidateIndex - 1)];
|
|
1933
|
+
if (next)
|
|
1934
|
+
setSourceCandidateId(candidateId(next));
|
|
1935
|
+
}
|
|
1936
|
+
else {
|
|
1937
|
+
const next = sourceInventory[Math.max(0, sourceResourceIndex - 1)];
|
|
1938
|
+
if (next)
|
|
1939
|
+
setSourceResourceId(next.id);
|
|
1940
|
+
}
|
|
1941
|
+
return;
|
|
1942
|
+
}
|
|
1943
|
+
if (input === 'd' && sourceSurface === 'inventory' && sourceResource) {
|
|
1944
|
+
if (!sourceRemovable)
|
|
1945
|
+
return setFeedback('Remove unavailable: select a proven Vercel-managed local Shared source');
|
|
1946
|
+
try {
|
|
1947
|
+
const plan = planSharedRemove(home, [sourceResource.name]);
|
|
1948
|
+
setSourceOperation({
|
|
1949
|
+
kind: 'remove',
|
|
1950
|
+
phase: 'preview',
|
|
1951
|
+
plan,
|
|
1952
|
+
steps: sourceSteps(plan.operation),
|
|
1953
|
+
scroll: 0,
|
|
1954
|
+
});
|
|
1955
|
+
}
|
|
1956
|
+
catch (error) {
|
|
1957
|
+
setFeedback(error.message);
|
|
1958
|
+
}
|
|
1959
|
+
return;
|
|
1960
|
+
}
|
|
1961
|
+
if (input === ' ' && sourceSurface === 'inventory' && sourceResource) {
|
|
1962
|
+
const marks = new Set(sourceMarks);
|
|
1963
|
+
if (marks.has(sourceResource.id))
|
|
1964
|
+
marks.delete(sourceResource.id);
|
|
1965
|
+
else
|
|
1966
|
+
marks.add(sourceResource.id);
|
|
1967
|
+
setSourceMarks(marks);
|
|
1968
|
+
return;
|
|
1969
|
+
}
|
|
1970
|
+
if (key.return && (sourceCandidate || sourceResource)) {
|
|
1971
|
+
setSourceDetailOpen(true);
|
|
1972
|
+
return;
|
|
1973
|
+
}
|
|
1974
|
+
return;
|
|
1975
|
+
}
|
|
1976
|
+
if (batch) {
|
|
1977
|
+
if (input === 'v' || key.escape)
|
|
1978
|
+
return setBatch(null);
|
|
1979
|
+
if (key.tab) {
|
|
1980
|
+
setBatch({ marks: new Set() });
|
|
1981
|
+
return setTab((value) => (value === 'target' ? 'skill' : 'target'));
|
|
1982
|
+
}
|
|
1983
|
+
if (input === ' ' && selectedRow?.realPath) {
|
|
1984
|
+
const marks = new Set(batch.marks);
|
|
1985
|
+
const key = markKey(home.configDir, selectedRow.realPath);
|
|
1986
|
+
if (marks.has(key))
|
|
1987
|
+
marks.delete(key);
|
|
1988
|
+
else
|
|
1989
|
+
marks.add(key);
|
|
1990
|
+
return setBatch({ marks });
|
|
1991
|
+
}
|
|
1992
|
+
if (input === ' ')
|
|
1993
|
+
return setFeedback('cannot mark a broken relationship');
|
|
1994
|
+
if (input === 'o' || input === 'O') {
|
|
1995
|
+
const intent = input === 'o' ? 'on' : 'off';
|
|
1996
|
+
const target = selectedTarget;
|
|
1997
|
+
const markedRows = rows.filter((row) => row.realPath && batch.marks.has(markKey(home.configDir, row.realPath)));
|
|
1998
|
+
if (!target || markedRows.length === 0)
|
|
1999
|
+
return setFeedback('Batch: mark at least one skill with a directory');
|
|
2000
|
+
const plans = [];
|
|
2001
|
+
const errors = [];
|
|
2002
|
+
for (const row of markedRows) {
|
|
2003
|
+
if (intent === 'on' && inheritedOn(row.targets[target.name]))
|
|
2004
|
+
continue;
|
|
2005
|
+
if (intent === 'off' && (!row.targets[target.name] || inheritedOn(row.targets[target.name])))
|
|
2006
|
+
continue;
|
|
2007
|
+
try {
|
|
2008
|
+
plans.push(planActivation(home, `skill:${row.id}`, [target.name], intent, planScope));
|
|
2009
|
+
}
|
|
2010
|
+
catch (err) {
|
|
2011
|
+
errors.push(`${row.name}: ${err.message}`);
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
if (plans.length === 0 && errors.length === 0)
|
|
2015
|
+
return setFeedback(`Batch ${intent}: skipped already-effective entries`);
|
|
2016
|
+
return setBatchConfirm({ intent, targetName: target.name, plans, errors });
|
|
2017
|
+
}
|
|
2018
|
+
if (input === 't' || input === 'T')
|
|
2019
|
+
return setBatchTag({ action: input === 't' ? 'add' : 'rm', value: '' });
|
|
2020
|
+
if (input === 'i' || input === 'o' || input === 'c' || input === 'm')
|
|
2021
|
+
return setFeedback('exit batch mode first (v)');
|
|
2022
|
+
}
|
|
2023
|
+
if (input === 'v')
|
|
2024
|
+
return setBatch({ marks: new Set() });
|
|
2025
|
+
if (input === 'm' && selectedRow?.realPath)
|
|
2026
|
+
return setManage({ rowId: selectedRow.id, section: 'tags', index: 0 });
|
|
2027
|
+
if (input === 'e') {
|
|
2028
|
+
const harness = visibility?.harnesses[0];
|
|
2029
|
+
if (!selectedRow?.realPath || !harness)
|
|
2030
|
+
return setFeedback('Explain unavailable: select an installed Skill resource');
|
|
2031
|
+
return setExplainModal({ row: selectedRow, harness: harness.key, scroll: 0 });
|
|
2032
|
+
}
|
|
2033
|
+
if (input === 'q' || (key.ctrl && input === 'c'))
|
|
2034
|
+
return exit();
|
|
2035
|
+
if (input === '/')
|
|
2036
|
+
return setSearching(true);
|
|
2037
|
+
if (input === 's')
|
|
2038
|
+
return setSort((value) => value === 'name' ? 'status' : value === 'status' ? 'source' : 'name');
|
|
2039
|
+
if (input === 'R') {
|
|
2040
|
+
const currentTarget = target?.name;
|
|
2041
|
+
const currentInstanceTarget = instanceTarget?.name;
|
|
2042
|
+
const next = takeSnapshot();
|
|
2043
|
+
setSnapshot(next);
|
|
2044
|
+
setTargetIndex(Math.max(0, next.targets.findIndex(({ name }) => name === currentTarget)));
|
|
2045
|
+
setInstanceTargetIndex(Math.max(0, next.targets.findIndex(({ name }) => name === currentInstanceTarget)));
|
|
2046
|
+
return;
|
|
2047
|
+
}
|
|
2048
|
+
if (key.tab)
|
|
2049
|
+
return setTab((value) => (value === 'target' ? 'skill' : 'target'));
|
|
2050
|
+
if (key.rightArrow || input === 'l')
|
|
2051
|
+
return setFocusColumn(1);
|
|
2052
|
+
if (key.leftArrow || input === 'h')
|
|
2053
|
+
return setFocusColumn(0);
|
|
2054
|
+
if (key.downArrow || input === 'j') {
|
|
2055
|
+
if (tab === 'target') {
|
|
2056
|
+
if (focusColumn === 0)
|
|
2057
|
+
setTargetIndex((value) => Math.min(Math.max(0, targets.length - 1), value + 1));
|
|
2058
|
+
else {
|
|
2059
|
+
const next = entries[Math.min(entries.length - 1, relationshipIndex + 1)];
|
|
2060
|
+
if (next)
|
|
2061
|
+
setRelationshipKey(next.key);
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
else if (focusColumn === 0) {
|
|
2065
|
+
const next = rows[Math.min(rows.length - 1, instanceIndex + 1)];
|
|
2066
|
+
if (next)
|
|
2067
|
+
setInstanceId(next.id);
|
|
2068
|
+
}
|
|
2069
|
+
else {
|
|
2070
|
+
setInstanceTargetIndex((value) => Math.min(Math.max(0, targets.length - 1), value + 1));
|
|
2071
|
+
}
|
|
2072
|
+
return;
|
|
2073
|
+
}
|
|
2074
|
+
if (key.upArrow || input === 'k') {
|
|
2075
|
+
if (tab === 'target') {
|
|
2076
|
+
if (focusColumn === 0)
|
|
2077
|
+
setTargetIndex((value) => Math.max(0, value - 1));
|
|
2078
|
+
else {
|
|
2079
|
+
const next = entries[Math.max(0, relationshipIndex - 1)];
|
|
2080
|
+
if (next)
|
|
2081
|
+
setRelationshipKey(next.key);
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
else if (focusColumn === 0) {
|
|
2085
|
+
const next = rows[Math.max(0, instanceIndex - 1)];
|
|
2086
|
+
if (next)
|
|
2087
|
+
setInstanceId(next.id);
|
|
2088
|
+
}
|
|
2089
|
+
else {
|
|
2090
|
+
setInstanceTargetIndex((value) => Math.max(0, value - 1));
|
|
2091
|
+
}
|
|
2092
|
+
return;
|
|
2093
|
+
}
|
|
2094
|
+
if (input === ' ' && selectedCell) {
|
|
2095
|
+
if (inheritedOn(selectedInfo))
|
|
2096
|
+
return setFeedback(`read-only: inherited from ${selectedRel?.scope}`);
|
|
2097
|
+
const canEnable = Boolean(projectPath) && selectedRow.realPath &&
|
|
2098
|
+
(!selectedInfo || selectedInfo.readOnly);
|
|
2099
|
+
if (!selectedInfo && !canEnable)
|
|
2100
|
+
return;
|
|
2101
|
+
if (!selectedRow.realPath && !selectedInfo)
|
|
2102
|
+
return setFeedback('cannot enable a broken relationship');
|
|
2103
|
+
try {
|
|
2104
|
+
prepareMutation();
|
|
2105
|
+
if (selectedInfo && !selectedInfo.readOnly && selectedRel) {
|
|
2106
|
+
applyActivationPlan(home, planToggle(home, selectedRel.targetId, selectedRel.slot, planScope));
|
|
2107
|
+
refresh({ targetId: selectedRel.targetId, slot: selectedRel.slot, rowId: selectedRow.id });
|
|
2108
|
+
setFeedback(`${selectedRow.name} @ ${selectedTarget.name}: ${selectedInfo.underOff ? 'on' : 'off'}`);
|
|
2109
|
+
}
|
|
2110
|
+
else if (canEnable) {
|
|
2111
|
+
const plan = planActivation(home, `skill:${selectedRow.id}`, [selectedTarget.name], 'on', planScope);
|
|
2112
|
+
if (plan.targets[0]?.createForm === 'mirror') {
|
|
2113
|
+
return setConfirmation({
|
|
2114
|
+
kind: 'mirror-create',
|
|
2115
|
+
row: selectedRow,
|
|
2116
|
+
target: selectedTarget,
|
|
2117
|
+
targetId: '',
|
|
2118
|
+
slot: '',
|
|
2119
|
+
source: selectedRow.realPath ?? '',
|
|
2120
|
+
destination: path.join(selectedTarget.dir, selectedRow.name),
|
|
2121
|
+
});
|
|
2122
|
+
}
|
|
2123
|
+
applyActivationPlan(home, plan);
|
|
2124
|
+
refresh({ rowId: selectedRow.id, target: selectedTarget.name });
|
|
2125
|
+
setFeedback(`${selectedRow.name} @ ${selectedTarget.name}: on`);
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
catch (err) {
|
|
2129
|
+
setFeedback(err.message);
|
|
2130
|
+
}
|
|
2131
|
+
return;
|
|
2132
|
+
}
|
|
2133
|
+
if (input === 'i' && actionable && !selectedInfo && !projectPath) {
|
|
2134
|
+
if (!selectedRow.realPath)
|
|
2135
|
+
return setFeedback('Link unavailable: selected skill has no directory');
|
|
2136
|
+
try {
|
|
2137
|
+
const plan = planLink(home, selectedRow.id, selectedTarget.name, planScope);
|
|
2138
|
+
return setConfirmation({
|
|
2139
|
+
kind: plan.targets[0]?.createForm === 'mirror' ? 'mirror-create' : 'link',
|
|
2140
|
+
row: selectedRow,
|
|
2141
|
+
target: selectedTarget,
|
|
2142
|
+
targetId: '',
|
|
2143
|
+
slot: '',
|
|
2144
|
+
source: selectedRow.realPath,
|
|
2145
|
+
destination: path.join(selectedTarget.dir, selectedRow.name),
|
|
2146
|
+
});
|
|
2147
|
+
}
|
|
2148
|
+
catch (err) {
|
|
2149
|
+
return setFeedback(err.message);
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
if (actionable && selectedInfo?.mirrored && selectedRel &&
|
|
2153
|
+
(input === 'S' || input === 'o' || input === 'c' || input === 'u' || input === 'x')) {
|
|
2154
|
+
if (selectedRel.readOnly)
|
|
2155
|
+
return setFeedback(`read-only: inherited from ${selectedRel.scope}`);
|
|
2156
|
+
const kind = input === 'S'
|
|
2157
|
+
? 'mirror-sync'
|
|
2158
|
+
: input === 'o'
|
|
2159
|
+
? 'mirror-overwrite'
|
|
2160
|
+
: input === 'c'
|
|
2161
|
+
? 'mirror-convert'
|
|
2162
|
+
: 'mirror-remove';
|
|
2163
|
+
return setConfirmation({
|
|
2164
|
+
kind,
|
|
2165
|
+
row: selectedRow,
|
|
2166
|
+
target: selectedTarget,
|
|
2167
|
+
info: selectedInfo,
|
|
2168
|
+
targetId: selectedRel.targetId,
|
|
2169
|
+
slot: selectedRel.slot,
|
|
2170
|
+
source: selectedInfo.path,
|
|
2171
|
+
destination: selectedRow.realPath ?? '?',
|
|
2172
|
+
});
|
|
2173
|
+
}
|
|
2174
|
+
if ((input === 'u' || input === 'x') && actionable && selectedInfo?.linked && selectedRel) {
|
|
2175
|
+
if (selectedRel.readOnly)
|
|
2176
|
+
return setFeedback(`read-only: inherited from ${selectedRel.scope}`);
|
|
2177
|
+
return setConfirmation({
|
|
2178
|
+
kind: 'unlink',
|
|
2179
|
+
row: selectedRow,
|
|
2180
|
+
target: selectedTarget,
|
|
2181
|
+
info: selectedInfo,
|
|
2182
|
+
targetId: selectedRel.targetId,
|
|
2183
|
+
slot: selectedRel.slot,
|
|
2184
|
+
source: selectedInfo.path,
|
|
2185
|
+
destination: selectedInfo.target ?? '?',
|
|
2186
|
+
});
|
|
2187
|
+
}
|
|
2188
|
+
if (key.return) {
|
|
2189
|
+
if (tab === 'target' && focusColumn === 0 && target)
|
|
2190
|
+
return setTargetInfoOpen(true);
|
|
2191
|
+
const row = tab === 'target' ? entry?.row : instance;
|
|
2192
|
+
if (row)
|
|
2193
|
+
setModal({ row, scroll: 0 });
|
|
2194
|
+
}
|
|
2195
|
+
});
|
|
2196
|
+
const columnName = tab === 'source'
|
|
2197
|
+
? sourceSurface
|
|
2198
|
+
: tab === 'target'
|
|
2199
|
+
? focusColumn === 0
|
|
2200
|
+
? 'targets'
|
|
2201
|
+
: 'relationships'
|
|
2202
|
+
: focusColumn === 0
|
|
2203
|
+
? 'skills'
|
|
2204
|
+
: 'targets';
|
|
2205
|
+
const actionHint = selectedCell
|
|
2206
|
+
? inheritedOn(selectedInfo)
|
|
2207
|
+
? ''
|
|
2208
|
+
: selectedInfo && !selectedInfo.readOnly
|
|
2209
|
+
? ` space ${selectedInfo.underOff ? 'on' : 'off'}${actionable
|
|
2210
|
+
? selectedInfo.mirrored
|
|
2211
|
+
? ' S sync o overwrite c convert u remove'
|
|
2212
|
+
: selectedInfo.linked
|
|
2213
|
+
? ' u unlink'
|
|
2214
|
+
: ''
|
|
2215
|
+
: ''}`
|
|
2216
|
+
: selectedRow.realPath
|
|
2217
|
+
? projectPath ? ' space on' : actionable ? ' i link' : ''
|
|
2218
|
+
: ''
|
|
2219
|
+
: '';
|
|
2220
|
+
return h(Box, { flexDirection: 'column', width, height }, h(Text, null, h(Text, { inverse: tab === 'target' }, ' 1 Target '), ' ', h(Text, { inverse: tab === 'skill' }, ' 2 Skill '), ' ', h(Text, { inverse: tab === 'source' }, ' 3 Source '), snapshot.project && tab !== 'source' ? h(Text, { color: 'cyan' }, ` Project: ${snapshot.project}`) : null, tab === 'source'
|
|
2221
|
+
? ` ${sourceSurface === 'catalog' ? 'Catalog' : 'Inventory'}`
|
|
2222
|
+
: ` Sort: ${sortLabel(sort)}${query ? ` Search: ${query}` : ''}`), sourceLogOpen && sourceEvidenceOperation
|
|
2223
|
+
? h(Box, { height: bodyHeight, paddingLeft: 2, paddingRight: 2, paddingTop: 1 }, h(SourceDetail, {
|
|
2224
|
+
title: sourceOperation
|
|
2225
|
+
? 'Full operation log / evidence — Source operation log & evidence'
|
|
2226
|
+
: 'Latest Source operation transcript',
|
|
2227
|
+
lines: sourceLogLines.slice(sourceLogScroll, sourceLogScroll + Math.max(1, bodyHeight - 4)),
|
|
2228
|
+
bordered: true,
|
|
2229
|
+
}))
|
|
2230
|
+
: sourceDetailOpen
|
|
2231
|
+
? h(Box, { height: bodyHeight, paddingLeft: 2, paddingRight: 2, paddingTop: 1 }, h(SourceDetail, {
|
|
2232
|
+
title: sourceSurface === 'catalog' ? 'Candidate detail' : 'Resource detail',
|
|
2233
|
+
lines: sourceDetailContent,
|
|
2234
|
+
bordered: true,
|
|
2235
|
+
}))
|
|
2236
|
+
: targetInfoOpen && target
|
|
2237
|
+
? h(Box, { height: bodyHeight, paddingLeft: 2, paddingRight: 2, paddingTop: 1 }, h(TargetInfoPanel, {
|
|
2238
|
+
target,
|
|
2239
|
+
harness: targetHarness,
|
|
2240
|
+
width: Math.max(12, width - 4),
|
|
2241
|
+
}))
|
|
2242
|
+
: explainModal
|
|
2243
|
+
? h(Box, { height: bodyHeight, paddingLeft: 2, paddingRight: 2, paddingTop: 1 }, h(ExplainModal, {
|
|
2244
|
+
row: explainModal.row,
|
|
2245
|
+
harnessName: explainedHarness?.name ?? explainModal.harness,
|
|
2246
|
+
harnessIndex: explainHarnessIndex,
|
|
2247
|
+
harnessCount: Math.max(1, explainHarnesses.length),
|
|
2248
|
+
lines: explainDetailLines,
|
|
2249
|
+
scroll: explainModal.scroll,
|
|
2250
|
+
height,
|
|
2251
|
+
}))
|
|
2252
|
+
: modal
|
|
2253
|
+
? h(Box, { height: bodyHeight, paddingLeft: 2, paddingRight: 2, paddingTop: 1 }, h(DetailModal, { row: modal.row, lines: modalLines, scroll: modal.scroll, height }))
|
|
2254
|
+
: manage && manageRow
|
|
2255
|
+
? h(Box, { height: bodyHeight, paddingLeft: 2, paddingRight: 2, paddingTop: 1 }, h(ManageModal, {
|
|
2256
|
+
row: manageRow,
|
|
2257
|
+
catalog: snapshot.catalog,
|
|
2258
|
+
bundles: membership?.bundles ?? [],
|
|
2259
|
+
manage,
|
|
2260
|
+
}))
|
|
2261
|
+
: batchConfirm
|
|
2262
|
+
? h(Box, { height: bodyHeight, paddingLeft: 2, paddingRight: 2, paddingTop: 1 }, h(BatchActivationModal, { confirm: batchConfirm }))
|
|
2263
|
+
: confirmation
|
|
2264
|
+
? h(Box, { height: bodyHeight, paddingLeft: 2, paddingRight: 2, paddingTop: 1 }, h(ConfirmationModal, { confirmation }))
|
|
2265
|
+
: tab === 'source'
|
|
2266
|
+
? h(SourceWorkspace, {
|
|
2267
|
+
scopePath: sourcePath,
|
|
2268
|
+
surface: sourceSurface,
|
|
2269
|
+
candidates: sourceCandidates,
|
|
2270
|
+
candidateIndex: sourceCandidateIndex,
|
|
2271
|
+
candidateTruth: sourceCandidateTruth,
|
|
2272
|
+
inventory: sourceInventory,
|
|
2273
|
+
inventoryIndex: sourceResourceIndex,
|
|
2274
|
+
marks: sourceMarks,
|
|
2275
|
+
visibility: sourceVisibility,
|
|
2276
|
+
desired: sourceTruth.desired,
|
|
2277
|
+
drift: sourceTruth.drift,
|
|
2278
|
+
operation: sourceOperation,
|
|
2279
|
+
width,
|
|
2280
|
+
height: bodyHeight,
|
|
2281
|
+
})
|
|
2282
|
+
: tab === 'target'
|
|
2283
|
+
? h(Box, { height: bodyHeight }, h(TargetList, {
|
|
2284
|
+
targets,
|
|
2285
|
+
harnesses: snapshot.harnesses,
|
|
2286
|
+
pendingTargetKeys: snapshot.pendingTargetKeys,
|
|
2287
|
+
selected: targetIndex,
|
|
2288
|
+
focused: focusColumn === 0,
|
|
2289
|
+
maxWidth: Math.max(10, Math.floor(width / 2) + 2),
|
|
2290
|
+
height: listHeight,
|
|
2291
|
+
}), h(RelationshipList, {
|
|
2292
|
+
entries,
|
|
2293
|
+
selected: relationshipIndex,
|
|
2294
|
+
focused: focusColumn === 1,
|
|
2295
|
+
height: listHeight,
|
|
2296
|
+
marks: markedIds,
|
|
2297
|
+
showScope: snapshot.project !== undefined,
|
|
2298
|
+
}), wide
|
|
2299
|
+
? focusColumn === 0
|
|
2300
|
+
? h(TargetInfoPanel, {
|
|
2301
|
+
target,
|
|
2302
|
+
harness: targetHarness,
|
|
2303
|
+
width: infoWidth,
|
|
2304
|
+
})
|
|
2305
|
+
: h(InfoPanel, {
|
|
2306
|
+
row: entry?.row,
|
|
2307
|
+
info: entry?.relationship.info,
|
|
2308
|
+
membership,
|
|
2309
|
+
visibility,
|
|
2310
|
+
width: infoWidth,
|
|
2311
|
+
})
|
|
2312
|
+
: null)
|
|
2313
|
+
: h(Box, { height: bodyHeight }, h(InstanceList, {
|
|
2314
|
+
rows,
|
|
2315
|
+
selected: instanceIndex,
|
|
2316
|
+
focused: focusColumn === 0,
|
|
2317
|
+
width: instanceWidth,
|
|
2318
|
+
height: listHeight,
|
|
2319
|
+
marks: markedIds,
|
|
2320
|
+
}), wide
|
|
2321
|
+
? h(InfoPanel, {
|
|
2322
|
+
row: instance,
|
|
2323
|
+
info: instance?.targets[targets[instTarget]?.name ?? ''],
|
|
2324
|
+
membership,
|
|
2325
|
+
visibility,
|
|
2326
|
+
width: infoWidth,
|
|
2327
|
+
})
|
|
2328
|
+
: null, h(TargetStatusList, {
|
|
2329
|
+
targets,
|
|
2330
|
+
row: instance,
|
|
2331
|
+
selected: instTarget,
|
|
2332
|
+
focused: focusColumn === 1,
|
|
2333
|
+
width: targetStatusWidth,
|
|
2334
|
+
height: listHeight,
|
|
2335
|
+
})), h(Text, { inverse: true, wrap: 'truncate-end' }, sourceLogOpen
|
|
2336
|
+
? ' ↑↓/j/k scroll esc close log '
|
|
2337
|
+
: sourceDetailOpen
|
|
2338
|
+
? ' esc close '
|
|
2339
|
+
: targetInfoOpen
|
|
2340
|
+
? ' esc close '
|
|
2341
|
+
: explainModal
|
|
2342
|
+
? ' tab Harness v visible h hidden d diagnosis ↑↓/j/k scroll PgUp/PgDn page esc close '
|
|
2343
|
+
: modal
|
|
2344
|
+
? ' ↑↓/jk scroll PgUp/PgDn page esc close '
|
|
2345
|
+
: manage
|
|
2346
|
+
? ` ${feedback}${feedback ? ' ' : ''}j/k move tab section space toggle a add x rm tag esc close `
|
|
2347
|
+
: batchConfirm
|
|
2348
|
+
? ' y confirm n/esc cancel '
|
|
2349
|
+
: batchTag
|
|
2350
|
+
? ` tag ${batchTag.action === 'add' ? 'add' : 'rm'}: ${batchTag.value}`
|
|
2351
|
+
: batch
|
|
2352
|
+
? ` ${feedback}${feedback ? ' ' : ''}${batch.marks.size} marked v/esc exit space mark o on O off t tag T untag `
|
|
2353
|
+
: confirmation
|
|
2354
|
+
? ' y confirm n/esc cancel '
|
|
2355
|
+
: searching
|
|
2356
|
+
? ` search: ${query || '…'} enter ${tab === 'source' ? 'search Source' : 'apply'} esc clear `
|
|
2357
|
+
: tab === 'source'
|
|
2358
|
+
? sourceOperation
|
|
2359
|
+
? sourceOperationHint(sourceOperation)
|
|
2360
|
+
: ` ${feedback}${feedback ? ' ' : ''}source:${columnName}${latestSourceOperation ? ' l latest transcript' : ''} tab Catalog/Inventory / search ↑↓/jk enter detail${sourceSurface === 'catalog' && sourceCandidate ? ' a add/replace' : ''} r refresh${sourceSurface === 'inventory' ? ` space mark (${sourceMarks.size})${sourceResource?.updateAvailability?.status === 'available' && mutableSourceRelationship(sourceResource) ? ' u update' : ''}${sourceMarks.size > 0 ? ' b batch update' : ''}${sourceRemovable ? ' d remove' : ''}` : ''} 1/2 matrices q `
|
|
2361
|
+
: ` ${feedback}${feedback ? ' ' : ''}${tab}:${columnName} ←→/hl ↑↓/jk${selectedRow?.realPath ? ' e explain' : ''}${actionHint} enter ${tab === 'target' && focusColumn === 0 ? 'details' : 'SKILL.md'} m manage / search s sort:${sortLabel(sort)} R refresh tab 1/2/3 workspace q `));
|
|
2362
|
+
}
|
|
2363
|
+
export async function runTui(home = defaultHome({ migrate: false }), options = {}) {
|
|
2364
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
2365
|
+
throw new Error('skillspub tui requires an interactive terminal');
|
|
2366
|
+
}
|
|
2367
|
+
const app = render(h(App, { home, projectPath: options.projectPath }), {
|
|
2368
|
+
exitOnCtrlC: false,
|
|
2369
|
+
patchConsole: false,
|
|
2370
|
+
alternateScreen: true,
|
|
2371
|
+
});
|
|
2372
|
+
await app.waitUntilExit();
|
|
2373
|
+
}
|