@lidtop/loadout 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/dist/cli.js +4 -45
- package/dist/external.d.ts +4 -0
- package/dist/external.js +40 -20
- package/dist/interactive.d.ts +13 -7
- package/dist/interactive.js +77 -34
- package/dist/picker.d.ts +22 -0
- package/dist/picker.js +211 -207
- package/dist/prepare.d.ts +10 -0
- package/dist/prepare.js +92 -0
- package/dist/retry.d.ts +1 -1
- package/dist/retry.js +3 -1
- package/dist/review.d.ts +35 -0
- package/dist/review.js +346 -0
- package/dist/setup.d.ts +9 -0
- package/dist/setup.js +120 -0
- package/dist/terminal.d.ts +1 -0
- package/dist/terminal.js +20 -8
- package/kits/dont-commit/instructions.md +1 -0
- package/kits/dont-commit/kit.yaml +6 -0
- package/kits/write-kit/skills/loadout-write-kit/SKILL.md +3 -1
- package/package.json +1 -1
package/dist/picker.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { createPrompt, useEffect, useKeypress, useState, isEnterKey, isSpaceKey, } from '@inquirer/core';
|
|
2
2
|
import { stripVTControlCharacters, styleText } from 'node:util';
|
|
3
3
|
import stringWidth from 'string-width';
|
|
4
|
+
import path from 'node:path';
|
|
4
5
|
import { resolveKits, reasons } from './resolve.js';
|
|
5
6
|
import { kitSource } from './schema.js';
|
|
6
7
|
import { availableUpdates, hasUpdate } from './updates.js';
|
|
7
8
|
import { prepareInput } from './terminal.js';
|
|
8
9
|
import { providerDescriptions } from './curated.js';
|
|
9
|
-
const accent = (value) => styleText('cyan', value);
|
|
10
10
|
const muted = (value) => styleText('dim', value);
|
|
11
11
|
const bold = (value) => styleText('bold', value);
|
|
12
|
+
const scopeColor = (target, value) => styleText(target.global ? 'magenta' : 'cyan', value);
|
|
12
13
|
const clean = (value) => stripVTControlCharacters(value).replace(/[\x00-\x1f\x7f]/g, ' ');
|
|
13
14
|
function fit(value, width) {
|
|
14
15
|
const text = clean(value);
|
|
@@ -22,7 +23,7 @@ function fit(value, width) {
|
|
|
22
23
|
}
|
|
23
24
|
return width > 0 ? `${result}…` : '';
|
|
24
25
|
}
|
|
25
|
-
function wordmark(width, rows) {
|
|
26
|
+
function wordmark(width, rows, accent) {
|
|
26
27
|
if (width < 43 || rows < 20)
|
|
27
28
|
return ['', ` ${accent(bold('LOADOUT'))}`, ''];
|
|
28
29
|
return [
|
|
@@ -44,20 +45,33 @@ const providerPrefixes = {
|
|
|
44
45
|
'mattpocock/skills': 'matt-pocock-',
|
|
45
46
|
'anthropics/skills': 'anthropic-',
|
|
46
47
|
};
|
|
48
|
+
const initialView = (target) => ({
|
|
49
|
+
section: target.global ||
|
|
50
|
+
![...(target.catalog?.kits.values() ?? [])].some((kit) => !providerFor(kit))
|
|
51
|
+
? 'Browse'
|
|
52
|
+
: 'Kits',
|
|
53
|
+
provider: undefined,
|
|
54
|
+
providerList: { query: '', active: 0 },
|
|
55
|
+
query: '',
|
|
56
|
+
active: 0,
|
|
57
|
+
});
|
|
47
58
|
const emptyState = () => ({
|
|
48
59
|
schemaVersion: 1,
|
|
49
60
|
selected: [],
|
|
50
61
|
answers: {},
|
|
51
62
|
});
|
|
52
63
|
const renderPicker = createPrompt((config, done) => {
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
-
const [
|
|
56
|
-
const [
|
|
57
|
-
|
|
58
|
-
const [
|
|
64
|
+
const snapshot = config.session?.snapshot;
|
|
65
|
+
const targets = config.targets;
|
|
66
|
+
const [views, setViews] = useState(snapshot?.views ?? targets.map(initialView));
|
|
67
|
+
const [targetIndex, setTargetIndex] = useState(snapshot?.targetIndex ?? config.initial ?? 0);
|
|
68
|
+
const [visited, setVisited] = useState(snapshot?.visited ?? [config.initial ?? 0]);
|
|
69
|
+
const [selections, setSelections] = useState(snapshot?.selections ??
|
|
70
|
+
config.targets.map((target) => (target.state?.selected ?? []).filter((id) => target.catalog?.kits.has(id) &&
|
|
71
|
+
target.catalog.kits.get(id)?.ready !== false)));
|
|
59
72
|
const [notice, setNotice] = useState('');
|
|
60
73
|
const target = targets[targetIndex];
|
|
74
|
+
const accent = (value) => scopeColor(target, value);
|
|
61
75
|
const sections = target.global
|
|
62
76
|
? ['Browse', 'Installed']
|
|
63
77
|
: repositorySections;
|
|
@@ -66,17 +80,18 @@ const renderPicker = createPrompt((config, done) => {
|
|
|
66
80
|
kits: new Map(),
|
|
67
81
|
};
|
|
68
82
|
const selected = selections[targetIndex];
|
|
69
|
-
const
|
|
70
|
-
JSON.stringify([...(
|
|
83
|
+
const pendingTargets = targets.filter((item, index) => JSON.stringify([...selections[index]].sort()) !==
|
|
84
|
+
JSON.stringify([...(item.state?.selected ?? [])].sort()));
|
|
71
85
|
const setSelected = (value) => setSelections(selections.map((ids, index) => (index === targetIndex ? value : ids)));
|
|
72
|
-
const [section, setSection] = useState(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const [
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
86
|
+
const [section, setSection] = useState(snapshot?.section ?? views[targetIndex].section);
|
|
87
|
+
const [provider, setProvider] = useState(snapshot?.provider);
|
|
88
|
+
const [providerList, setProviderList] = useState(snapshot?.providerList ?? { query: '', active: 0 });
|
|
89
|
+
const [query, setQuery] = useState(snapshot?.query ?? '');
|
|
90
|
+
const [active, setActive] = useState(snapshot?.active ?? 0);
|
|
91
|
+
useEffect((rl) => {
|
|
92
|
+
if (snapshot?.query)
|
|
93
|
+
rl.write(snapshot.query);
|
|
94
|
+
}, []);
|
|
80
95
|
const [finished, setFinished] = useState(false);
|
|
81
96
|
const [size, setSize] = useState({
|
|
82
97
|
columns: process.stdout.columns || 80,
|
|
@@ -176,10 +191,16 @@ const renderPicker = createPrompt((config, done) => {
|
|
|
176
191
|
.sort((a, b) => Number(a.ready === false) - Number(b.ready === false))
|
|
177
192
|
.map((kit) => ({ id: kit.id, description: kit.description, kit }));
|
|
178
193
|
}
|
|
194
|
+
if (provider)
|
|
195
|
+
entries.push({
|
|
196
|
+
id: '@back',
|
|
197
|
+
action: 'back',
|
|
198
|
+
description: 'Keeps your selections',
|
|
199
|
+
});
|
|
179
200
|
entries.push({
|
|
180
|
-
id: '@
|
|
181
|
-
action: '
|
|
182
|
-
description: '
|
|
201
|
+
id: '@review',
|
|
202
|
+
action: 'review',
|
|
203
|
+
description: '',
|
|
183
204
|
});
|
|
184
205
|
const cursor = Math.min(active, Math.max(0, entries.length - 1));
|
|
185
206
|
const focused = entries[cursor];
|
|
@@ -193,72 +214,77 @@ const renderPicker = createPrompt((config, done) => {
|
|
|
193
214
|
rl.clearLine(0);
|
|
194
215
|
rl.write(query);
|
|
195
216
|
};
|
|
196
|
-
const
|
|
197
|
-
setScopeFocused(false);
|
|
198
|
-
setTargetIndex(index);
|
|
199
|
-
setSection(targets[index].global ||
|
|
200
|
-
![...targets[index].catalog.kits.values()].some((kit) => !providerFor(kit))
|
|
201
|
-
? 'Browse'
|
|
202
|
-
: 'Kits');
|
|
203
|
-
setVisited([...new Set([...visited, index])]);
|
|
217
|
+
const backToProviders = () => {
|
|
204
218
|
setProvider(undefined);
|
|
205
|
-
|
|
206
|
-
|
|
219
|
+
setQuery(providerList.query);
|
|
220
|
+
setActive(providerList.active);
|
|
221
|
+
rl.clearLine(0);
|
|
222
|
+
rl.write(providerList.query);
|
|
207
223
|
};
|
|
208
|
-
|
|
209
|
-
if (
|
|
210
|
-
switchTarget(pendingSwitch);
|
|
211
|
-
setPendingSwitch(undefined);
|
|
212
|
-
}
|
|
213
|
-
else if (key.name === 'n' ||
|
|
214
|
-
key.name === 'escape' ||
|
|
215
|
-
isEnterKey(key)) {
|
|
216
|
-
setPendingSwitch(undefined);
|
|
224
|
+
const switchTarget = (index) => {
|
|
225
|
+
if (index === targetIndex) {
|
|
217
226
|
restoreInput();
|
|
227
|
+
return;
|
|
218
228
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
const other = targets[next];
|
|
225
|
-
if (other.error)
|
|
226
|
-
setNotice(other.error);
|
|
227
|
-
else if (!other.catalog)
|
|
228
|
-
setNotice('Cannot load this location.');
|
|
229
|
-
else if (hasSelectionChanges) {
|
|
230
|
-
setPendingSwitch(next);
|
|
229
|
+
const next = targets[index];
|
|
230
|
+
if (next.error || !next.catalog) {
|
|
231
|
+
setNotice(next.error ?? 'Cannot load this location.');
|
|
232
|
+
restoreInput();
|
|
233
|
+
return;
|
|
231
234
|
}
|
|
232
|
-
|
|
233
|
-
|
|
235
|
+
setViews(views.map((view, index) => index === targetIndex
|
|
236
|
+
? { section, provider, providerList, query, active: cursor }
|
|
237
|
+
: view));
|
|
238
|
+
const view = views[index];
|
|
239
|
+
setTargetIndex(index);
|
|
240
|
+
setSection(view.section);
|
|
241
|
+
setProvider(view.provider);
|
|
242
|
+
setProviderList(view.providerList);
|
|
243
|
+
setQuery(view.query);
|
|
244
|
+
setActive(view.active);
|
|
245
|
+
setVisited([...new Set([...visited, index])]);
|
|
246
|
+
setNotice('');
|
|
234
247
|
rl.clearLine(0);
|
|
248
|
+
rl.write(view.query);
|
|
249
|
+
};
|
|
250
|
+
if (key.name === 'tab') {
|
|
251
|
+
switchTarget((targetIndex + (key.shift ? -1 : 1) + targets.length) %
|
|
252
|
+
targets.length);
|
|
235
253
|
return;
|
|
236
254
|
}
|
|
237
|
-
if (['
|
|
238
|
-
const index =
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
const count = sections.length + (targets.length > 1 ? 1 : 0);
|
|
242
|
-
const backwards = key.name === 'left' || (key.name === 'tab' && key.shift);
|
|
255
|
+
if (['left', 'right'].includes(key.name)) {
|
|
256
|
+
const index = sections.indexOf(section);
|
|
257
|
+
const count = sections.length;
|
|
258
|
+
const backwards = key.name === 'left';
|
|
243
259
|
const next = (index + (backwards ? -1 : 1) + count) % count;
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
setProvider(undefined);
|
|
248
|
-
reset();
|
|
249
|
-
}
|
|
250
|
-
else
|
|
251
|
-
restoreInput();
|
|
260
|
+
setSection(sections[next]);
|
|
261
|
+
setProvider(undefined);
|
|
262
|
+
reset();
|
|
252
263
|
}
|
|
253
264
|
else if (key.name === 'up' || key.name === 'down') {
|
|
254
|
-
setScopeFocused(false);
|
|
255
265
|
if (entries.length)
|
|
256
266
|
setActive((cursor + (key.name === 'up' ? -1 : 1) + entries.length) %
|
|
257
267
|
entries.length);
|
|
258
268
|
restoreInput();
|
|
259
269
|
}
|
|
260
270
|
else if (isEnterKey(key) || isSpaceKey(key)) {
|
|
261
|
-
if (focused?.action === '
|
|
271
|
+
if (focused?.action === 'back') {
|
|
272
|
+
backToProviders();
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
else if (focused?.action === 'review') {
|
|
276
|
+
if (config.session)
|
|
277
|
+
config.session.snapshot = {
|
|
278
|
+
targetIndex,
|
|
279
|
+
visited,
|
|
280
|
+
selections,
|
|
281
|
+
views,
|
|
282
|
+
section,
|
|
283
|
+
provider,
|
|
284
|
+
providerList,
|
|
285
|
+
query,
|
|
286
|
+
active: cursor,
|
|
287
|
+
};
|
|
262
288
|
setFinished(true);
|
|
263
289
|
done(targets.flatMap((item, index) => {
|
|
264
290
|
if (!item.catalog || !visited.includes(index))
|
|
@@ -273,6 +299,7 @@ const renderPicker = createPrompt((config, done) => {
|
|
|
273
299
|
}));
|
|
274
300
|
}
|
|
275
301
|
else if (browsingProviders && focused) {
|
|
302
|
+
setProviderList({ query, active: cursor });
|
|
276
303
|
setProvider(focused.id);
|
|
277
304
|
setActive(0);
|
|
278
305
|
}
|
|
@@ -286,124 +313,115 @@ const renderPicker = createPrompt((config, done) => {
|
|
|
286
313
|
rl.write(query);
|
|
287
314
|
}
|
|
288
315
|
else if (key.name === 'escape') {
|
|
289
|
-
if (
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
}
|
|
293
|
-
else {
|
|
294
|
-
if (!query && provider)
|
|
295
|
-
setProvider(undefined);
|
|
316
|
+
if (!query && provider)
|
|
317
|
+
backToProviders();
|
|
318
|
+
else
|
|
296
319
|
reset();
|
|
297
|
-
}
|
|
298
320
|
}
|
|
299
321
|
else if (!['left', 'right', 'home', 'end'].includes(key.name)) {
|
|
300
|
-
setScopeFocused(false);
|
|
301
322
|
setQuery(clean(rl.line));
|
|
302
323
|
setActive(0);
|
|
303
324
|
}
|
|
304
325
|
});
|
|
305
|
-
const
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
...scopeLines,
|
|
330
|
-
'',
|
|
331
|
-
` ${bold(fit(`Switch to ${targets[pendingSwitch].label}?`, width - 2))}`,
|
|
332
|
-
...selectionWarning,
|
|
333
|
-
'',
|
|
334
|
-
` ${accent('[Enter/Esc]')} Stay ${accent('[y]')} Switch`,
|
|
335
|
-
'\u001b[?25l',
|
|
336
|
-
].join('\n');
|
|
326
|
+
const scopeLabels = targets.map((item, index) => `[${index === targetIndex ? '●' : '○'} ${clean(item.label)}${pendingTargets.includes(item) ? '*' : ''}${item.error || !item.catalog ? ' !' : ''}]`);
|
|
327
|
+
const baseWidth = scopeLabels.reduce((sum, label) => sum + stringWidth(label), 0) +
|
|
328
|
+
(targets.length - 1) * 2;
|
|
329
|
+
const repositoryIndex = targets.findIndex((item) => !item.global);
|
|
330
|
+
const nameWidth = width - 4 - baseWidth - 3;
|
|
331
|
+
if (repositoryIndex >= 0 && nameWidth >= 3) {
|
|
332
|
+
const repository = targets[repositoryIndex];
|
|
333
|
+
const name = fit(path.basename(repository.root) || repository.root, nameWidth);
|
|
334
|
+
scopeLabels[repositoryIndex] = scopeLabels[repositoryIndex].replace(clean(repository.label), () => `${clean(repository.label)} · ${name}`);
|
|
335
|
+
}
|
|
336
|
+
const segments = targets.map((item, index) => {
|
|
337
|
+
const label = scopeLabels[index];
|
|
338
|
+
const colored = scopeColor(item, label);
|
|
339
|
+
return index === targetIndex
|
|
340
|
+
? bold(colored)
|
|
341
|
+
: pendingTargets.includes(item)
|
|
342
|
+
? colored
|
|
343
|
+
: muted(colored);
|
|
344
|
+
});
|
|
345
|
+
const selectorWidth = segments.reduce((sum, item) => sum + stringWidth(item), 0) +
|
|
346
|
+
(segments.length - 1) * 2;
|
|
347
|
+
const scopeLines = selectorWidth <= width - 4
|
|
348
|
+
? [` ${segments.join(' ')}`]
|
|
349
|
+
: segments.map((item) => ` ${item}`);
|
|
337
350
|
if (finished)
|
|
338
351
|
return [
|
|
339
|
-
...
|
|
352
|
+
...wordmark(width, height, accent),
|
|
340
353
|
` ${accent('✓')} ${selected.length} selected${requiredCount ? muted(` · ${requiredCount} required`) : ''}\n`,
|
|
341
354
|
].join('\n');
|
|
342
355
|
const updates = availableUpdates(catalog).length;
|
|
343
356
|
const counts = `${selected.length} selected${requiredCount ? ` · ${requiredCount} required` : ''}`;
|
|
357
|
+
const tabLabel = (name) => name === 'Browse' && provider ? `Browse › ${clean(provider)}` : name;
|
|
358
|
+
const inlineProvider = !!provider && sections.map(tabLabel).join(' ').length + 2 <= width - 2;
|
|
344
359
|
const tabs = width >= 34
|
|
345
|
-
? ` ${sections
|
|
346
|
-
|
|
360
|
+
? ` ${sections
|
|
361
|
+
.map((name) => {
|
|
362
|
+
const label = inlineProvider ? tabLabel(name) : name;
|
|
363
|
+
return name === section
|
|
364
|
+
? accent(bold(`[${label}]`))
|
|
365
|
+
: muted(label);
|
|
366
|
+
})
|
|
367
|
+
.join(' ')}`
|
|
368
|
+
: ` ${accent(bold(`[${section}]`))}`;
|
|
347
369
|
const filter = ` ${accent('/')} ${query ? `${fit(query, width - 6)}${accent('▏')}` : muted(browsingProviders ? 'Search providers or kits' : 'Search kits')}`;
|
|
348
|
-
const
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
:
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
`esc ${provider ? 'back' : 'clear'}`,
|
|
361
|
-
];
|
|
362
|
-
const helpLines = [];
|
|
363
|
-
const separator = width >= 100 ? ' ' : ' · ';
|
|
370
|
+
const escapeHint = query ? 'Esc clear' : provider ? 'Esc back' : '';
|
|
371
|
+
const hints = [
|
|
372
|
+
...(width >= 60 ? ['↑↓ move'] : []),
|
|
373
|
+
focused?.action
|
|
374
|
+
? 'Enter select'
|
|
375
|
+
: browsingProviders
|
|
376
|
+
? 'Space open'
|
|
377
|
+
: 'Space toggle',
|
|
378
|
+
targets.length > 1 ? 'Tab switch scope' : '←→ tabs',
|
|
379
|
+
escapeHint,
|
|
380
|
+
].filter(Boolean);
|
|
381
|
+
let helpText = '';
|
|
364
382
|
for (const hint of hints) {
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
helpLines[helpLines.length - 1] = combined;
|
|
369
|
-
else
|
|
370
|
-
helpLines.push(fit(hint, width - 2));
|
|
371
|
-
}
|
|
372
|
-
const help = helpLines.map((line) => ` ${muted(line)}`);
|
|
373
|
-
if (scopeFocused && other) {
|
|
374
|
-
const action = other.error || !other.catalog
|
|
375
|
-
? `${other.label} is unavailable`
|
|
376
|
-
: `Switch to ${other.label}`;
|
|
377
|
-
return [
|
|
378
|
-
...header,
|
|
379
|
-
...scopeLines,
|
|
380
|
-
'',
|
|
381
|
-
tabs,
|
|
382
|
-
rule,
|
|
383
|
-
` ${bold(fit(action, width - 2))}`,
|
|
384
|
-
...(other.error ? [` ${fit(other.error, width - 2)}`] : []),
|
|
385
|
-
` ${muted(fit(other.root, width - 2))}`,
|
|
386
|
-
rule,
|
|
387
|
-
...help,
|
|
388
|
-
'\u001b[?25l',
|
|
389
|
-
].join('\n');
|
|
383
|
+
const combined = helpText ? `${helpText} · ${hint}` : hint;
|
|
384
|
+
if (stringWidth(combined) <= width - 2)
|
|
385
|
+
helpText = combined;
|
|
390
386
|
}
|
|
387
|
+
const help = [` ${muted(helpText)}`];
|
|
391
388
|
const beforeList = [
|
|
392
|
-
...header,
|
|
393
389
|
...scopeLines,
|
|
394
|
-
...(scopeLines.length && detailed ? [''] : []),
|
|
395
390
|
tabs,
|
|
396
|
-
...(provider
|
|
397
|
-
|
|
398
|
-
|
|
391
|
+
...(provider && !inlineProvider
|
|
392
|
+
? [` ${muted(fit(provider, width - 2))}`]
|
|
393
|
+
: []),
|
|
399
394
|
filter,
|
|
400
|
-
...(detailed ? [''] : []),
|
|
401
395
|
];
|
|
402
396
|
const summary = fit(`${counts}${updates ? ` · ${updates} updates` : ''}`, width - 6);
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
const
|
|
406
|
-
const
|
|
397
|
+
const actions = entries.filter((row) => row.action);
|
|
398
|
+
const kit = focused?.kit;
|
|
399
|
+
const why = kit ? reasons(catalog, selected, kit.id) : [];
|
|
400
|
+
const detail = notice ||
|
|
401
|
+
(kit?.ready === false
|
|
402
|
+
? 'Edit this kit, then set ready: true in kit.yaml'
|
|
403
|
+
: kit && willUninstall(kit.id)
|
|
404
|
+
? 'Uninstall on apply. Select again to keep.'
|
|
405
|
+
: why.length
|
|
406
|
+
? `Required by ${why.map(displayName).join(', ')}${selected.includes(kit.id) ? ' · also selected' : ' · space to keep explicitly'}`
|
|
407
|
+
: kit?.requires.length
|
|
408
|
+
? `Requires ${kit.requires.map(displayName).join(', ')}`
|
|
409
|
+
: kit && hasUpdate(kit)
|
|
410
|
+
? `Catalog update · ${kit.pinned.ref.slice(0, 8)} → ${kit.external.ref.slice(0, 8)}`
|
|
411
|
+
: '');
|
|
412
|
+
// Keep the contextual row allocated so focus changes never move the footer.
|
|
413
|
+
const footerHeight = 3 + actions.length + help.length;
|
|
414
|
+
// Prefer useful list rows over decorative branding on short terminals.
|
|
415
|
+
const header = wordmark(width, height, accent);
|
|
416
|
+
const headerBudget = height - beforeList.length - footerHeight - (height >= 20 ? 8 : 4);
|
|
417
|
+
if (header.length <= headerBudget)
|
|
418
|
+
beforeList.unshift(...header);
|
|
419
|
+
else if (headerBudget >= 1)
|
|
420
|
+
beforeList.unshift(` ${accent(bold('LOADOUT'))}`);
|
|
421
|
+
const spacing = height - beforeList.length - footerHeight >= 8 ? 1 : 0;
|
|
422
|
+
if (spacing)
|
|
423
|
+
beforeList.push('');
|
|
424
|
+
const pageSize = Math.max(1, Math.min(8, Math.floor((height - beforeList.length - footerHeight) / 2)));
|
|
407
425
|
const listEntries = entries.filter((row) => !row.action);
|
|
408
426
|
const listCursor = Math.min(cursor, Math.max(0, listEntries.length - 1));
|
|
409
427
|
const start = Math.max(0, Math.min(listCursor - Math.floor(pageSize / 2), listEntries.length - pageSize));
|
|
@@ -436,28 +454,24 @@ const renderPicker = createPrompt((config, done) => {
|
|
|
436
454
|
? 'Will uninstall'
|
|
437
455
|
: hasUpdate(row.kit)
|
|
438
456
|
? 'update'
|
|
439
|
-
:
|
|
440
|
-
? '
|
|
441
|
-
:
|
|
442
|
-
? '
|
|
443
|
-
:
|
|
444
|
-
? 'saved'
|
|
445
|
-
: '';
|
|
457
|
+
: required
|
|
458
|
+
? 'required'
|
|
459
|
+
: row.kit.pinned && !explicit
|
|
460
|
+
? 'saved'
|
|
461
|
+
: '';
|
|
446
462
|
const marker = !row.kit
|
|
447
463
|
? accent('▸')
|
|
448
464
|
: explicit
|
|
449
465
|
? accent('●')
|
|
450
466
|
: required
|
|
451
467
|
? styleText('yellow', '◆')
|
|
452
|
-
: muted('○');
|
|
468
|
+
: muted(accent('○'));
|
|
453
469
|
const badge = required || willUninstall(row.id) || (row.kit && hasUpdate(row.kit))
|
|
454
470
|
? styleText('yellow', label)
|
|
455
471
|
: muted(label);
|
|
456
472
|
const name = fit(displayName(row.id), width - label.length - 9);
|
|
457
473
|
const gap = ' '.repeat(Math.max(2, width - stringWidth(name) - label.length - 6));
|
|
458
|
-
lines.push(` ${focus ? accent('›') : ' '} ${marker} ${row.kit?.ready === false ? muted(name) : focus ? bold(name) : name}${gap}${badge}`);
|
|
459
|
-
if (detailed)
|
|
460
|
-
lines.push(` ${muted(fit(row.kit && section === 'Installed' ? `${kitSource(row.kit)} · ${row.description}` : row.description, width - 6))}`);
|
|
474
|
+
lines.push(` ${focus ? accent('›') : ' '} ${marker} ${row.kit?.ready === false ? muted(name) : focus ? bold(name) : name}${gap}${badge}`, ` ${muted(fit(row.kit && section === 'Installed' ? `${kitSource(row.kit)} · ${row.description}` : row.description, width - 6))}`);
|
|
461
475
|
}
|
|
462
476
|
if (!page.length) {
|
|
463
477
|
const empty = !catalog.kits.size
|
|
@@ -465,48 +479,38 @@ const renderPicker = createPrompt((config, done) => {
|
|
|
465
479
|
: query
|
|
466
480
|
? 'No matching kits. Esc to clear.'
|
|
467
481
|
: section === 'Kits'
|
|
468
|
-
? 'No repository kits.
|
|
482
|
+
? 'No repository kits. ←→ to browse.'
|
|
469
483
|
: section === 'Installed'
|
|
470
|
-
? 'No kits installed.
|
|
484
|
+
? 'No kits installed. ←→ to browse.'
|
|
471
485
|
: 'No external providers configured.';
|
|
472
486
|
lines.push(` ${muted(fit(empty, width - 2))}`);
|
|
473
487
|
}
|
|
474
|
-
const kit = focused?.kit;
|
|
475
|
-
const why = kit ? reasons(catalog, selected, kit.id) : [];
|
|
476
|
-
const detail = focused?.action
|
|
477
|
-
? focused.description
|
|
478
|
-
: browsingProviders
|
|
479
|
-
? [
|
|
480
|
-
focused?.selectedCount ? `${focused.selectedCount} selected` : '',
|
|
481
|
-
focused?.downloadedCount
|
|
482
|
-
? `${focused.downloadedCount} downloaded`
|
|
483
|
-
: '',
|
|
484
|
-
]
|
|
485
|
-
.filter(Boolean)
|
|
486
|
-
.join(' · ') || 'Choose a provider to explore its kits'
|
|
487
|
-
: kit?.ready === false
|
|
488
|
-
? 'Edit this kit, then set ready: true in kit.yaml'
|
|
489
|
-
: kit && willUninstall(kit.id)
|
|
490
|
-
? 'Uninstall on apply. Select again to keep.'
|
|
491
|
-
: kit && hasUpdate(kit)
|
|
492
|
-
? `Catalog update · ${kit.pinned.ref.slice(0, 8)} → ${kit.external.ref.slice(0, 8)}`
|
|
493
|
-
: why.length
|
|
494
|
-
? `Required by ${why.map(displayName).join(', ')}${selected.includes(kit.id) ? ' · also selected' : ' · space to keep explicitly'}`
|
|
495
|
-
: kit?.requires.length
|
|
496
|
-
? `Requires ${kit.requires.map(displayName).join(', ')}`
|
|
497
|
-
: kit?.external
|
|
498
|
-
? `Includes ${(kit.pinned ?? kit.external).skills.map((p) => p.split('/').at(-1)).join(', ')}`
|
|
499
|
-
: '';
|
|
500
488
|
const pagination = listEntries.length > page.length
|
|
501
489
|
? `${start + 1}–${start + page.length} of ${listEntries.length}`
|
|
502
490
|
: '';
|
|
491
|
+
const status = fit([summary, pagination].filter(Boolean).join(' · '), width - 6);
|
|
492
|
+
const bottomRule = ` ${muted('─'.repeat(Math.max(1, width - stringWidth(status) - 6)))} ${scopeColor(target, status)} ${muted('─')}`;
|
|
503
493
|
return [
|
|
504
494
|
...beforeList,
|
|
505
495
|
...lines,
|
|
496
|
+
` ${muted(fit(detail, width - 2))}`,
|
|
506
497
|
bottomRule,
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
498
|
+
...actions.map((row) => {
|
|
499
|
+
const label = row.action === 'back'
|
|
500
|
+
? width >= 25
|
|
501
|
+
? 'Back to providers'
|
|
502
|
+
: 'Back'
|
|
503
|
+
: width >= 22
|
|
504
|
+
? 'Review changes'
|
|
505
|
+
: 'Review';
|
|
506
|
+
const button = fit(`[ ${label} ]`, width - 4);
|
|
507
|
+
const focus = row.id === focused?.id;
|
|
508
|
+
const helperWidth = width - stringWidth(button) - 6;
|
|
509
|
+
const helper = row.description && helperWidth > 0
|
|
510
|
+
? ` ${muted(fit(row.description, helperWidth))}`
|
|
511
|
+
: '';
|
|
512
|
+
return ` ${focus ? accent('›') : ' '} ${focus ? accent(bold(button)) : button}${helper}`;
|
|
513
|
+
}),
|
|
510
514
|
...help,
|
|
511
515
|
'\u001b[?25l',
|
|
512
516
|
].join('\n');
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type PromptContext } from './interactive.js';
|
|
2
|
+
import { type ReviewTarget, type PreparedTarget } from './review.js';
|
|
3
|
+
type Progress = (scope: number, message: string) => void;
|
|
4
|
+
type Retry = (scope: number, id: string, error: Error) => Promise<boolean>;
|
|
5
|
+
type Config = {
|
|
6
|
+
selections: ReviewTarget[];
|
|
7
|
+
run: (progress: Progress, retry: Retry, signal: AbortSignal) => Promise<PreparedTarget[]>;
|
|
8
|
+
};
|
|
9
|
+
export declare function prepareScreen(config: Config, context?: PromptContext): Promise<PreparedTarget[]>;
|
|
10
|
+
export {};
|