@lidtop/loadout 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 +28 -0
- package/dist/adoption.d.ts +13 -0
- package/dist/adoption.js +169 -0
- package/dist/bundled.d.ts +1 -0
- package/dist/bundled.js +3 -0
- package/dist/catalog.d.ts +3 -0
- package/dist/catalog.js +149 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +293 -0
- package/dist/curated.d.ts +3 -0
- package/dist/curated.js +98 -0
- package/dist/external.d.ts +51 -0
- package/dist/external.js +285 -0
- package/dist/fs.d.ts +6 -0
- package/dist/fs.js +60 -0
- package/dist/init.d.ts +1 -0
- package/dist/init.js +51 -0
- package/dist/interactive.d.ts +10 -0
- package/dist/interactive.js +82 -0
- package/dist/output-path.d.ts +1 -0
- package/dist/output-path.js +15 -0
- package/dist/picker.d.ts +29 -0
- package/dist/picker.js +569 -0
- package/dist/render.d.ts +14 -0
- package/dist/render.js +64 -0
- package/dist/resolve.d.ts +6 -0
- package/dist/resolve.js +80 -0
- package/dist/retry.d.ts +4 -0
- package/dist/retry.js +26 -0
- package/dist/schema.d.ts +111 -0
- package/dist/schema.js +139 -0
- package/dist/storage.d.ts +27 -0
- package/dist/storage.js +298 -0
- package/dist/targets.d.ts +11 -0
- package/dist/targets.js +31 -0
- package/dist/terminal.d.ts +1 -0
- package/dist/terminal.js +41 -0
- package/dist/updates.d.ts +4 -0
- package/dist/updates.js +30 -0
- package/kits/greenfield/instructions.md +3 -0
- package/kits/greenfield/kit.yaml +6 -0
- package/kits/write-kit/kit.yaml +6 -0
- package/kits/write-kit/skills/loadout-write-kit/SKILL.md +55 -0
- package/package.json +64 -0
package/dist/picker.js
ADDED
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
import { createPrompt, useEffect, useKeypress, useState, isEnterKey, isSpaceKey, } from '@inquirer/core';
|
|
2
|
+
import { stripVTControlCharacters, styleText } from 'node:util';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import stringWidth from 'string-width';
|
|
5
|
+
import { resolveKits, reasons } from './resolve.js';
|
|
6
|
+
import { kitSource } from './schema.js';
|
|
7
|
+
import { availableUpdates, hasUpdate } from './updates.js';
|
|
8
|
+
import { prepareInput } from './terminal.js';
|
|
9
|
+
import { providerDescriptions } from './curated.js';
|
|
10
|
+
const accent = (value) => styleText('cyan', value);
|
|
11
|
+
const muted = (value) => styleText('dim', value);
|
|
12
|
+
const bold = (value) => styleText('bold', value);
|
|
13
|
+
const clean = (value) => stripVTControlCharacters(value).replace(/[\x00-\x1f\x7f]/g, ' ');
|
|
14
|
+
function fit(value, width) {
|
|
15
|
+
const text = clean(value);
|
|
16
|
+
if (stringWidth(text) <= width)
|
|
17
|
+
return text;
|
|
18
|
+
let result = '';
|
|
19
|
+
for (const character of text) {
|
|
20
|
+
if (stringWidth(result + character) > width - 1)
|
|
21
|
+
break;
|
|
22
|
+
result += character;
|
|
23
|
+
}
|
|
24
|
+
return width > 0 ? `${result}…` : '';
|
|
25
|
+
}
|
|
26
|
+
function wordmark(width, rows) {
|
|
27
|
+
if (width < 43 || rows < 20)
|
|
28
|
+
return ['', ` ${accent(bold('LOADOUT'))}`, ''];
|
|
29
|
+
return [
|
|
30
|
+
'',
|
|
31
|
+
accent(' █ █▀▀█ █▀▀█ █▀▀▄ █▀▀█ █ █ ▀▀█▀▀'),
|
|
32
|
+
accent(' █ █ █ █▄▄█ █ █ █ █ █ █ █'),
|
|
33
|
+
accent(' █▄▄█ █▄▄█ █ █ █▄▄▀ █▄▄█ ▀▄▄▀ █'),
|
|
34
|
+
'',
|
|
35
|
+
];
|
|
36
|
+
}
|
|
37
|
+
const repositorySections = ['Kits', 'Browse', 'Installed'];
|
|
38
|
+
const providerFor = (kit) => kit.origin === 'bundled' ? 'loadout' : kit.external?.repo;
|
|
39
|
+
const providerPrefixes = {
|
|
40
|
+
loadout: 'loadout-',
|
|
41
|
+
'mattpocock/skills': 'matt-pocock-',
|
|
42
|
+
'anthropics/skills': 'anthropic-',
|
|
43
|
+
};
|
|
44
|
+
const emptyState = () => ({
|
|
45
|
+
schemaVersion: 1,
|
|
46
|
+
selected: [],
|
|
47
|
+
answers: {},
|
|
48
|
+
});
|
|
49
|
+
const renderPicker = createPrompt((config, done) => {
|
|
50
|
+
const [targets, setTargets] = useState(config.targets);
|
|
51
|
+
const [targetIndex, setTargetIndex] = useState(config.initial ?? 0);
|
|
52
|
+
const [visited, setVisited] = useState([config.initial ?? 0]);
|
|
53
|
+
const [selections, setSelections] = useState(config.targets.map((target) => (target.state?.selected ?? []).filter((id) => target.catalog?.kits.has(id) &&
|
|
54
|
+
target.catalog.kits.get(id)?.ready !== false)));
|
|
55
|
+
const [pending, setPending] = useState(undefined);
|
|
56
|
+
const [pendingSwitch, setPendingSwitch] = useState(undefined);
|
|
57
|
+
const [notice, setNotice] = useState('');
|
|
58
|
+
const target = targets[targetIndex];
|
|
59
|
+
const sections = target.global
|
|
60
|
+
? ['Browse', 'Installed']
|
|
61
|
+
: repositorySections;
|
|
62
|
+
const catalog = target.catalog ?? {
|
|
63
|
+
root: target.root,
|
|
64
|
+
kits: new Map(),
|
|
65
|
+
};
|
|
66
|
+
const selected = selections[targetIndex];
|
|
67
|
+
const hasSelectionChanges = JSON.stringify([...selected].sort()) !==
|
|
68
|
+
JSON.stringify([...(target.state?.selected ?? [])].sort());
|
|
69
|
+
const setSelected = (value) => setSelections(selections.map((ids, index) => (index === targetIndex ? value : ids)));
|
|
70
|
+
const [section, setSection] = useState(target.global ? 'Browse' : 'Kits');
|
|
71
|
+
const [scopeFocused, setScopeFocused] = useState(false);
|
|
72
|
+
const [provider, setProvider] = useState(undefined);
|
|
73
|
+
const [query, setQuery] = useState('');
|
|
74
|
+
const [active, setActive] = useState(0);
|
|
75
|
+
const [finished, setFinished] = useState(false);
|
|
76
|
+
const [size, setSize] = useState({
|
|
77
|
+
columns: process.stdout.columns || 80,
|
|
78
|
+
rows: process.stdout.rows || 24,
|
|
79
|
+
});
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
const resize = () => setSize({
|
|
82
|
+
columns: process.stdout.columns || 80,
|
|
83
|
+
rows: process.stdout.rows || 24,
|
|
84
|
+
});
|
|
85
|
+
process.stdout.on('resize', resize);
|
|
86
|
+
return () => {
|
|
87
|
+
process.stdout.off('resize', resize);
|
|
88
|
+
};
|
|
89
|
+
}, []);
|
|
90
|
+
const width = Math.max(12, (config.columns ?? size.columns) - 2);
|
|
91
|
+
const height = config.rows ?? size.rows;
|
|
92
|
+
const enabled = resolveKits(catalog, selected);
|
|
93
|
+
const enabledSet = new Set(enabled);
|
|
94
|
+
const installedSet = new Set(resolveKits(catalog, (target.state?.selected ?? []).filter((id) => catalog.kits.has(id) && catalog.kits.get(id)?.ready !== false)));
|
|
95
|
+
const willUninstall = (id) => installedSet.has(id) && !enabledSet.has(id);
|
|
96
|
+
const requiredCount = enabled.length - selected.length;
|
|
97
|
+
const all = [...catalog.kits.values()];
|
|
98
|
+
const matches = (kit) => `${kit.id} ${kit.description} ${kitSource(kit)} ${kit.external?.repo ?? ''} ${kit.external?.skills.join(' ') ?? ''}`
|
|
99
|
+
.toLowerCase()
|
|
100
|
+
.includes(query.toLowerCase());
|
|
101
|
+
const browsingProviders = section === 'Browse' && !provider;
|
|
102
|
+
const displayName = (id) => {
|
|
103
|
+
const kit = catalog.kits.get(id);
|
|
104
|
+
if (section !== 'Browse' ||
|
|
105
|
+
!provider ||
|
|
106
|
+
!kit ||
|
|
107
|
+
providerFor(kit) !== provider)
|
|
108
|
+
return id;
|
|
109
|
+
const prefix = providerPrefixes[provider];
|
|
110
|
+
return prefix && id.startsWith(prefix) && id.length > prefix.length
|
|
111
|
+
? id.slice(prefix.length)
|
|
112
|
+
: id;
|
|
113
|
+
};
|
|
114
|
+
let entries;
|
|
115
|
+
if (browsingProviders) {
|
|
116
|
+
const providers = new Map();
|
|
117
|
+
for (const kit of all) {
|
|
118
|
+
const name = providerFor(kit);
|
|
119
|
+
if (!name)
|
|
120
|
+
continue;
|
|
121
|
+
const kits = providers.get(name) ?? [];
|
|
122
|
+
kits.push(kit);
|
|
123
|
+
providers.set(name, kits);
|
|
124
|
+
}
|
|
125
|
+
entries = [...providers]
|
|
126
|
+
.sort(([a], [b]) => a === 'loadout' ? -1 : b === 'loadout' ? 1 : a.localeCompare(b))
|
|
127
|
+
.flatMap(([id, kits]) => {
|
|
128
|
+
const matching = kits.filter(matches);
|
|
129
|
+
if (!matching.length)
|
|
130
|
+
return [];
|
|
131
|
+
const count = kits.filter((kit) => enabledSet.has(kit.id)).length;
|
|
132
|
+
const origins = new Set(kits.map((kit) => kit.origin));
|
|
133
|
+
const description = id === 'loadout'
|
|
134
|
+
? 'Included with Loadout'
|
|
135
|
+
: origins.has('curated') && providerDescriptions[id]
|
|
136
|
+
? providerDescriptions[id]
|
|
137
|
+
: origins.size > 1
|
|
138
|
+
? 'Curated and repository sources'
|
|
139
|
+
: origins.has('curated')
|
|
140
|
+
? 'Curated kits'
|
|
141
|
+
: 'Repository sources';
|
|
142
|
+
return [
|
|
143
|
+
{
|
|
144
|
+
id,
|
|
145
|
+
count: matching.length,
|
|
146
|
+
description,
|
|
147
|
+
selectedCount: count,
|
|
148
|
+
downloadedCount: kits.filter((kit) => !!kit.pinned).length,
|
|
149
|
+
},
|
|
150
|
+
];
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
entries = all
|
|
155
|
+
.filter((kit) => {
|
|
156
|
+
if (!matches(kit))
|
|
157
|
+
return false;
|
|
158
|
+
if (section === 'Kits')
|
|
159
|
+
return !providerFor(kit);
|
|
160
|
+
if (section === 'Installed')
|
|
161
|
+
return (!!providerFor(kit) &&
|
|
162
|
+
(!!kit.pinned ||
|
|
163
|
+
enabledSet.has(kit.id) ||
|
|
164
|
+
installedSet.has(kit.id)));
|
|
165
|
+
return providerFor(kit) === provider;
|
|
166
|
+
})
|
|
167
|
+
.sort((a, b) => Number(a.ready === false) - Number(b.ready === false))
|
|
168
|
+
.map((kit) => ({ id: kit.id, description: kit.description, kit }));
|
|
169
|
+
}
|
|
170
|
+
entries.push({
|
|
171
|
+
id: '@continue',
|
|
172
|
+
action: 'continue',
|
|
173
|
+
description: 'Review selections and apply',
|
|
174
|
+
});
|
|
175
|
+
const cursor = Math.min(active, Math.max(0, entries.length - 1));
|
|
176
|
+
const focused = entries[cursor];
|
|
177
|
+
useKeypress((key, rl) => {
|
|
178
|
+
const reset = () => {
|
|
179
|
+
setQuery('');
|
|
180
|
+
setActive(0);
|
|
181
|
+
rl.clearLine(0);
|
|
182
|
+
};
|
|
183
|
+
const restoreInput = () => {
|
|
184
|
+
rl.clearLine(0);
|
|
185
|
+
rl.write(query);
|
|
186
|
+
};
|
|
187
|
+
const switchTarget = (index) => {
|
|
188
|
+
setScopeFocused(false);
|
|
189
|
+
setTargetIndex(index);
|
|
190
|
+
setSection(targets[index].global ? 'Browse' : 'Kits');
|
|
191
|
+
setVisited([...new Set([...visited, index])]);
|
|
192
|
+
setProvider(undefined);
|
|
193
|
+
setNotice('');
|
|
194
|
+
reset();
|
|
195
|
+
};
|
|
196
|
+
if (pendingSwitch !== undefined) {
|
|
197
|
+
if (key.name === 'y') {
|
|
198
|
+
switchTarget(pendingSwitch);
|
|
199
|
+
setPendingSwitch(undefined);
|
|
200
|
+
}
|
|
201
|
+
else if (key.name === 'n' ||
|
|
202
|
+
key.name === 'escape' ||
|
|
203
|
+
isEnterKey(key)) {
|
|
204
|
+
setPendingSwitch(undefined);
|
|
205
|
+
restoreInput();
|
|
206
|
+
}
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (pending !== undefined) {
|
|
210
|
+
if (key.name === 'y' && config.initialize) {
|
|
211
|
+
try {
|
|
212
|
+
const initialized = config.initialize(targets[pending]);
|
|
213
|
+
if (!initialized.catalog)
|
|
214
|
+
throw new Error(initialized.error ??
|
|
215
|
+
'Initialization did not produce a catalog.');
|
|
216
|
+
setTargets(targets.map((item, index) => index === pending ? initialized : item));
|
|
217
|
+
setSelections(selections.map((ids, index) => index === pending ? (initialized.state?.selected ?? []) : ids));
|
|
218
|
+
switchTarget(pending);
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
setNotice(error.message);
|
|
222
|
+
}
|
|
223
|
+
setPending(undefined);
|
|
224
|
+
}
|
|
225
|
+
else if (key.name === 'n' ||
|
|
226
|
+
key.name === 'escape' ||
|
|
227
|
+
isEnterKey(key)) {
|
|
228
|
+
setPending(undefined);
|
|
229
|
+
}
|
|
230
|
+
restoreInput();
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (scopeFocused && (isEnterKey(key) || isSpaceKey(key))) {
|
|
234
|
+
setScopeFocused(false);
|
|
235
|
+
const next = (targetIndex + 1) % targets.length;
|
|
236
|
+
const other = targets[next];
|
|
237
|
+
if (other.error)
|
|
238
|
+
setNotice(other.error);
|
|
239
|
+
else if (!other.catalog) {
|
|
240
|
+
setPending(next);
|
|
241
|
+
reset();
|
|
242
|
+
}
|
|
243
|
+
else if (hasSelectionChanges) {
|
|
244
|
+
setPendingSwitch(next);
|
|
245
|
+
}
|
|
246
|
+
else
|
|
247
|
+
switchTarget(next);
|
|
248
|
+
rl.clearLine(0);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (['tab', 'left', 'right'].includes(key.name)) {
|
|
252
|
+
const index = scopeFocused
|
|
253
|
+
? sections.length
|
|
254
|
+
: sections.indexOf(section);
|
|
255
|
+
const count = sections.length + (targets.length > 1 ? 1 : 0);
|
|
256
|
+
const backwards = key.name === 'left' || (key.name === 'tab' && key.shift);
|
|
257
|
+
const next = (index + (backwards ? -1 : 1) + count) % count;
|
|
258
|
+
setScopeFocused(next === sections.length);
|
|
259
|
+
if (next !== sections.length) {
|
|
260
|
+
setSection(sections[next]);
|
|
261
|
+
setProvider(undefined);
|
|
262
|
+
reset();
|
|
263
|
+
}
|
|
264
|
+
else
|
|
265
|
+
restoreInput();
|
|
266
|
+
}
|
|
267
|
+
else if (key.name === 'up' || key.name === 'down') {
|
|
268
|
+
setScopeFocused(false);
|
|
269
|
+
if (entries.length)
|
|
270
|
+
setActive((cursor + (key.name === 'up' ? -1 : 1) + entries.length) %
|
|
271
|
+
entries.length);
|
|
272
|
+
restoreInput();
|
|
273
|
+
}
|
|
274
|
+
else if (isEnterKey(key) || isSpaceKey(key)) {
|
|
275
|
+
if (focused?.action === 'continue') {
|
|
276
|
+
setFinished(true);
|
|
277
|
+
done(targets.flatMap((item, index) => {
|
|
278
|
+
if (!item.catalog || !visited.includes(index))
|
|
279
|
+
return [];
|
|
280
|
+
const ids = [...selections[index]].sort();
|
|
281
|
+
const saved = item.state ?? emptyState();
|
|
282
|
+
if (index !== targetIndex &&
|
|
283
|
+
JSON.stringify(ids) ===
|
|
284
|
+
JSON.stringify([...saved.selected].sort()))
|
|
285
|
+
return [];
|
|
286
|
+
return [{ target: item, state: { ...saved, selected: ids } }];
|
|
287
|
+
}));
|
|
288
|
+
}
|
|
289
|
+
else if (browsingProviders && focused) {
|
|
290
|
+
setProvider(focused.id);
|
|
291
|
+
setActive(0);
|
|
292
|
+
}
|
|
293
|
+
else if (focused?.kit && focused.kit.ready !== false) {
|
|
294
|
+
setSelected(selected.includes(focused.id)
|
|
295
|
+
? selected.filter((id) => id !== focused.id)
|
|
296
|
+
: [...selected, focused.id]);
|
|
297
|
+
setActive(cursor);
|
|
298
|
+
}
|
|
299
|
+
rl.clearLine(0);
|
|
300
|
+
rl.write(query);
|
|
301
|
+
}
|
|
302
|
+
else if (key.name === 'escape') {
|
|
303
|
+
if (scopeFocused) {
|
|
304
|
+
setScopeFocused(false);
|
|
305
|
+
restoreInput();
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
if (!query && provider)
|
|
309
|
+
setProvider(undefined);
|
|
310
|
+
reset();
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
else if (!['left', 'right', 'home', 'end'].includes(key.name)) {
|
|
314
|
+
setScopeFocused(false);
|
|
315
|
+
setQuery(clean(rl.line));
|
|
316
|
+
setActive(0);
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
const header = wordmark(width, height);
|
|
320
|
+
const other = targets.length > 1
|
|
321
|
+
? targets[(targetIndex + 1) % targets.length]
|
|
322
|
+
: undefined;
|
|
323
|
+
const switchLabel = other
|
|
324
|
+
? `[ Go to ${other.label}${other.error ? ' !' : ''} ]`
|
|
325
|
+
: '';
|
|
326
|
+
const scopeLabel = width >= 60 ? `${target.label} · ${target.root}` : target.label;
|
|
327
|
+
const scopeText = fit(scopeLabel, width - switchLabel.length - 4);
|
|
328
|
+
const scopeLines = targets.length > 1 || target.global
|
|
329
|
+
? [
|
|
330
|
+
` ${bold(scopeText)}${' '.repeat(Math.max(2, width - stringWidth(scopeText) - switchLabel.length - 2))}${scopeFocused ? accent(bold(switchLabel)) : muted(switchLabel)}`,
|
|
331
|
+
...(width < 60 ? [` ${muted(fit(target.root, width - 2))}`] : []),
|
|
332
|
+
]
|
|
333
|
+
: [];
|
|
334
|
+
const selectionWarning = hasSelectionChanges
|
|
335
|
+
? [
|
|
336
|
+
` ${fit(`Unapplied changes in ${target.label}.`, width - 2)}`,
|
|
337
|
+
' Selections stay in this session.',
|
|
338
|
+
]
|
|
339
|
+
: [];
|
|
340
|
+
if (pendingSwitch !== undefined)
|
|
341
|
+
return [
|
|
342
|
+
...header,
|
|
343
|
+
...scopeLines,
|
|
344
|
+
'',
|
|
345
|
+
` ${bold(fit(`Switch to ${targets[pendingSwitch].label}?`, width - 2))}`,
|
|
346
|
+
...selectionWarning,
|
|
347
|
+
'',
|
|
348
|
+
` ${accent('[Enter/Esc]')} Stay ${accent('[y]')} Switch`,
|
|
349
|
+
'\u001b[?25l',
|
|
350
|
+
].join('\n');
|
|
351
|
+
if (pending !== undefined) {
|
|
352
|
+
const destination = targets[pending];
|
|
353
|
+
const pathWidth = width - 9;
|
|
354
|
+
const catalogPath = path.join(destination.root, '.loadout');
|
|
355
|
+
const displayPath = stringWidth(clean(catalogPath)) <= pathWidth
|
|
356
|
+
? clean(catalogPath)
|
|
357
|
+
: destination.global
|
|
358
|
+
? '~/.loadout'
|
|
359
|
+
: `${fit(destination.root, pathWidth - 9)}/.loadout`;
|
|
360
|
+
return [
|
|
361
|
+
...header,
|
|
362
|
+
...scopeLines,
|
|
363
|
+
'',
|
|
364
|
+
` ${bold(fit(`Set up ${destination.label} and switch?`, width - 2))}`,
|
|
365
|
+
` ${fit(`Create ${displayPath}`, width - 2)}`,
|
|
366
|
+
...selectionWarning,
|
|
367
|
+
'',
|
|
368
|
+
` ${accent('[Enter/Esc]')} Cancel ${accent('[y]')} Set up`,
|
|
369
|
+
'\u001b[?25l',
|
|
370
|
+
].join('\n');
|
|
371
|
+
}
|
|
372
|
+
if (finished)
|
|
373
|
+
return [
|
|
374
|
+
...header,
|
|
375
|
+
` ${accent('✓')} ${selected.length} selected${requiredCount ? muted(` · ${requiredCount} required`) : ''}\n`,
|
|
376
|
+
].join('\n');
|
|
377
|
+
const updates = availableUpdates(catalog).length;
|
|
378
|
+
const counts = `${selected.length} selected${requiredCount ? ` · ${requiredCount} required` : ''}`;
|
|
379
|
+
const tabs = width >= 34
|
|
380
|
+
? ` ${sections.map((name) => (name === section && !scopeFocused ? accent(bold(`[${name}]`)) : muted(name))).join(' ')}`
|
|
381
|
+
: ` ${scopeFocused ? muted(section) : accent(bold(`[${section}]`))} ${muted('←→/tab')}`;
|
|
382
|
+
const filter = ` ${accent('/')} ${query ? `${fit(query, width - 6)}${accent('▏')}` : muted(browsingProviders ? 'Search providers or kits' : 'Search kits')}`;
|
|
383
|
+
const rule = ` ${muted('─'.repeat(width - 2))}`;
|
|
384
|
+
const detailed = height >= 20;
|
|
385
|
+
const hints = scopeFocused
|
|
386
|
+
? [
|
|
387
|
+
`space/enter ${other?.error ? 'details' : other?.catalog ? 'switch' : 'setup'}`,
|
|
388
|
+
'←→/tab move',
|
|
389
|
+
'esc back',
|
|
390
|
+
]
|
|
391
|
+
: [
|
|
392
|
+
'↑↓ move',
|
|
393
|
+
`space/enter ${focused?.action ? 'select' : browsingProviders ? 'open' : 'toggle'}`,
|
|
394
|
+
'←→/tab switch',
|
|
395
|
+
`esc ${provider ? 'back' : 'clear'}`,
|
|
396
|
+
];
|
|
397
|
+
const helpLines = [];
|
|
398
|
+
const separator = width >= 100 ? ' ' : ' · ';
|
|
399
|
+
for (const hint of hints) {
|
|
400
|
+
const previous = helpLines.at(-1);
|
|
401
|
+
const combined = previous ? `${previous}${separator}${hint}` : hint;
|
|
402
|
+
if (previous && stringWidth(combined) <= width - 2)
|
|
403
|
+
helpLines[helpLines.length - 1] = combined;
|
|
404
|
+
else
|
|
405
|
+
helpLines.push(fit(hint, width - 2));
|
|
406
|
+
}
|
|
407
|
+
const help = helpLines.map((line) => ` ${muted(line)}`);
|
|
408
|
+
if (scopeFocused && other) {
|
|
409
|
+
const action = other.error
|
|
410
|
+
? `${other.label} is unavailable`
|
|
411
|
+
: `${other.catalog ? 'Switch to' : 'Set up'} ${other.label}`;
|
|
412
|
+
return [
|
|
413
|
+
...header,
|
|
414
|
+
...scopeLines,
|
|
415
|
+
'',
|
|
416
|
+
tabs,
|
|
417
|
+
rule,
|
|
418
|
+
` ${bold(fit(action, width - 2))}`,
|
|
419
|
+
...(other.error ? [` ${fit(other.error, width - 2)}`] : []),
|
|
420
|
+
` ${muted(fit(other.root, width - 2))}`,
|
|
421
|
+
rule,
|
|
422
|
+
...help,
|
|
423
|
+
'\u001b[?25l',
|
|
424
|
+
].join('\n');
|
|
425
|
+
}
|
|
426
|
+
const beforeList = [
|
|
427
|
+
...header,
|
|
428
|
+
...scopeLines,
|
|
429
|
+
...(scopeLines.length && detailed ? [''] : []),
|
|
430
|
+
tabs,
|
|
431
|
+
...(provider ? [` ${muted(fit(provider, width - 2))}`] : []),
|
|
432
|
+
...(detailed ? [''] : []),
|
|
433
|
+
rule,
|
|
434
|
+
filter,
|
|
435
|
+
...(detailed ? [''] : []),
|
|
436
|
+
];
|
|
437
|
+
const summary = fit(`${counts}${updates ? ` · ${updates} updates` : ''}`, width - 6);
|
|
438
|
+
const bottomRule = ` ${muted(`${'─'.repeat(Math.max(1, width - stringWidth(summary) - 6))} ${summary} ─`)}`;
|
|
439
|
+
// Border, detail, spacing, keyboard help, and cursor-control line.
|
|
440
|
+
const footerHeight = 5 + help.length;
|
|
441
|
+
const pageSize = Math.max(1, Math.min(8, Math.floor((height - beforeList.length - footerHeight) / (detailed ? 2 : 1))));
|
|
442
|
+
const listEntries = entries.filter((row) => !row.action);
|
|
443
|
+
const listCursor = Math.min(cursor, Math.max(0, listEntries.length - 1));
|
|
444
|
+
const start = Math.max(0, Math.min(listCursor - Math.floor(pageSize / 2), listEntries.length - pageSize));
|
|
445
|
+
const page = listEntries.slice(start, start + pageSize);
|
|
446
|
+
const lines = [];
|
|
447
|
+
for (const row of page) {
|
|
448
|
+
const focus = row.id === focused?.id;
|
|
449
|
+
const explicit = selected.includes(row.id);
|
|
450
|
+
const required = enabledSet.has(row.id) && !explicit;
|
|
451
|
+
const providerStatus = [
|
|
452
|
+
row.selectedCount
|
|
453
|
+
? `${row.selectedCount} ${width >= 76 ? 'selected' : 'sel'}`
|
|
454
|
+
: '',
|
|
455
|
+
row.downloadedCount
|
|
456
|
+
? `${row.downloadedCount} ${width >= 76 ? 'downloaded' : 'dl'}`
|
|
457
|
+
: '',
|
|
458
|
+
]
|
|
459
|
+
.filter(Boolean)
|
|
460
|
+
.join(' · ');
|
|
461
|
+
const label = !row.kit
|
|
462
|
+
? [
|
|
463
|
+
width >= 76 || !providerStatus ? `${row.count} kits` : '',
|
|
464
|
+
providerStatus,
|
|
465
|
+
]
|
|
466
|
+
.filter(Boolean)
|
|
467
|
+
.join(' · ')
|
|
468
|
+
: row.kit.ready === false
|
|
469
|
+
? 'Needs setup'
|
|
470
|
+
: willUninstall(row.id)
|
|
471
|
+
? 'Will uninstall'
|
|
472
|
+
: hasUpdate(row.kit)
|
|
473
|
+
? 'update'
|
|
474
|
+
: explicit
|
|
475
|
+
? 'selected'
|
|
476
|
+
: required
|
|
477
|
+
? 'required'
|
|
478
|
+
: row.kit.pinned
|
|
479
|
+
? 'saved'
|
|
480
|
+
: '';
|
|
481
|
+
const marker = !row.kit
|
|
482
|
+
? accent('▸')
|
|
483
|
+
: explicit
|
|
484
|
+
? accent('●')
|
|
485
|
+
: required
|
|
486
|
+
? styleText('yellow', '◆')
|
|
487
|
+
: muted('○');
|
|
488
|
+
const badge = required || willUninstall(row.id) || (row.kit && hasUpdate(row.kit))
|
|
489
|
+
? styleText('yellow', label)
|
|
490
|
+
: muted(label);
|
|
491
|
+
const name = fit(displayName(row.id), width - label.length - 9);
|
|
492
|
+
const gap = ' '.repeat(Math.max(2, width - stringWidth(name) - label.length - 6));
|
|
493
|
+
lines.push(` ${focus ? accent('›') : ' '} ${marker} ${row.kit?.ready === false ? muted(name) : focus ? bold(name) : name}${gap}${badge}`);
|
|
494
|
+
if (detailed)
|
|
495
|
+
lines.push(` ${muted(fit(row.kit && section === 'Installed' ? `${kitSource(row.kit)} · ${row.description}` : row.description, width - 6))}`);
|
|
496
|
+
}
|
|
497
|
+
if (!page.length) {
|
|
498
|
+
const empty = !catalog.kits.size
|
|
499
|
+
? 'No kits in this catalog.'
|
|
500
|
+
: query
|
|
501
|
+
? 'No matching kits. Esc to clear.'
|
|
502
|
+
: section === 'Kits'
|
|
503
|
+
? 'No repository kits. Tab to browse.'
|
|
504
|
+
: section === 'Installed'
|
|
505
|
+
? 'No kits installed. Tab to browse.'
|
|
506
|
+
: 'No external providers configured.';
|
|
507
|
+
lines.push(` ${muted(fit(empty, width - 2))}`);
|
|
508
|
+
}
|
|
509
|
+
const kit = focused?.kit;
|
|
510
|
+
const why = kit ? reasons(catalog, selected, kit.id) : [];
|
|
511
|
+
const detail = focused?.action
|
|
512
|
+
? focused.description
|
|
513
|
+
: browsingProviders
|
|
514
|
+
? [
|
|
515
|
+
focused?.selectedCount ? `${focused.selectedCount} selected` : '',
|
|
516
|
+
focused?.downloadedCount
|
|
517
|
+
? `${focused.downloadedCount} downloaded`
|
|
518
|
+
: '',
|
|
519
|
+
]
|
|
520
|
+
.filter(Boolean)
|
|
521
|
+
.join(' · ') || 'Choose a provider to explore its kits'
|
|
522
|
+
: kit?.ready === false
|
|
523
|
+
? 'Edit this kit, then set ready: true in kit.yaml'
|
|
524
|
+
: kit && willUninstall(kit.id)
|
|
525
|
+
? 'Uninstall on apply. Select again to keep.'
|
|
526
|
+
: kit && hasUpdate(kit)
|
|
527
|
+
? `Catalog update · ${kit.pinned.ref.slice(0, 8)} → ${kit.external.ref.slice(0, 8)}`
|
|
528
|
+
: why.length
|
|
529
|
+
? `Required by ${why.map(displayName).join(', ')}${selected.includes(kit.id) ? ' · also selected' : ' · space to keep explicitly'}`
|
|
530
|
+
: kit?.requires.length
|
|
531
|
+
? `Requires ${kit.requires.map(displayName).join(', ')}`
|
|
532
|
+
: kit?.external
|
|
533
|
+
? `Includes ${(kit.pinned ?? kit.external).skills.map((p) => p.split('/').at(-1)).join(', ')}`
|
|
534
|
+
: '';
|
|
535
|
+
const pagination = listEntries.length > page.length
|
|
536
|
+
? `${start + 1}–${start + page.length} of ${listEntries.length}`
|
|
537
|
+
: '';
|
|
538
|
+
return [
|
|
539
|
+
...beforeList,
|
|
540
|
+
...lines,
|
|
541
|
+
bottomRule,
|
|
542
|
+
` ${focused?.action ? accent('›') : ' '} ${focused?.action ? accent(bold('[ Continue ]')) : '[ Continue ]'}`,
|
|
543
|
+
` ${muted(fit(notice || [pagination, detail].filter(Boolean).join(' · '), width - 2))}`,
|
|
544
|
+
'',
|
|
545
|
+
...help,
|
|
546
|
+
'\u001b[?25l',
|
|
547
|
+
].join('\n');
|
|
548
|
+
});
|
|
549
|
+
export function targetPicker(config, context) {
|
|
550
|
+
prepareInput(context?.input ?? process.stdin);
|
|
551
|
+
return renderPicker(config, context);
|
|
552
|
+
}
|
|
553
|
+
// Single-location entry point for consumers that only need selected IDs.
|
|
554
|
+
export async function kitPicker(config, context) {
|
|
555
|
+
const result = await targetPicker({
|
|
556
|
+
targets: [
|
|
557
|
+
{
|
|
558
|
+
label: 'Repository',
|
|
559
|
+
root: config.catalog.root,
|
|
560
|
+
global: false,
|
|
561
|
+
catalog: config.catalog,
|
|
562
|
+
state: { ...emptyState(), selected: config.selected },
|
|
563
|
+
},
|
|
564
|
+
],
|
|
565
|
+
columns: config.columns,
|
|
566
|
+
rows: config.rows,
|
|
567
|
+
}, context);
|
|
568
|
+
return result[0].state.selected;
|
|
569
|
+
}
|
package/dist/render.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type Catalog, type State } from './schema.js';
|
|
2
|
+
export type FileContent = {
|
|
3
|
+
content: Buffer;
|
|
4
|
+
mode: number;
|
|
5
|
+
};
|
|
6
|
+
export type Rendered = {
|
|
7
|
+
files: Map<string, FileContent>;
|
|
8
|
+
skillRoots: Set<string>;
|
|
9
|
+
external?: {
|
|
10
|
+
before?: Buffer;
|
|
11
|
+
content: Buffer;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
export declare function render(catalog: Catalog, state: State): Rendered;
|
package/dist/render.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { agents } from './schema.js';
|
|
4
|
+
import { safePath, walk, portableMode } from './fs.js';
|
|
5
|
+
import { resolveKits } from './resolve.js';
|
|
6
|
+
export function render(catalog, state) {
|
|
7
|
+
const files = new Map();
|
|
8
|
+
const skillRoots = new Set();
|
|
9
|
+
const sections = new Map();
|
|
10
|
+
for (const id of resolveKits(catalog, state.selected)) {
|
|
11
|
+
const kit = catalog.kits.get(id);
|
|
12
|
+
for (const output of kit.outputs) {
|
|
13
|
+
if (output.when &&
|
|
14
|
+
state.answers[id]?.[output.when.answer] !== output.when.equals)
|
|
15
|
+
continue;
|
|
16
|
+
const source = safePath(kit.directory, output.source);
|
|
17
|
+
if (output.type === 'instructions') {
|
|
18
|
+
const section = fs.readFileSync(source, 'utf8').trim();
|
|
19
|
+
sections.set(output.scope, [
|
|
20
|
+
...(sections.get(output.scope) ?? []),
|
|
21
|
+
section,
|
|
22
|
+
]);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
for (const agent of agents) {
|
|
26
|
+
const destination = `${agent === 'codex' ? '.agents' : '.claude'}/skills/${path.basename(source)}`;
|
|
27
|
+
if (skillRoots.has(destination))
|
|
28
|
+
throw new Error(`Output collision: multiple skills target ${destination}`);
|
|
29
|
+
skillRoots.add(destination);
|
|
30
|
+
for (const file of walk(source)) {
|
|
31
|
+
const src = safePath(source, file);
|
|
32
|
+
files.set(`${destination}/${file}`, {
|
|
33
|
+
content: fs.readFileSync(src),
|
|
34
|
+
mode: portableMode(fs.statSync(src).mode & 0o111 ? 0o755 : 0o644),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
for (const [scope, content] of sections) {
|
|
42
|
+
const instructions = catalog.global
|
|
43
|
+
? '.codex/AGENTS.md'
|
|
44
|
+
: path.posix.join(scope, 'AGENTS.md');
|
|
45
|
+
const claude = catalog.global
|
|
46
|
+
? '.claude/CLAUDE.md'
|
|
47
|
+
: path.posix.join(scope, 'CLAUDE.md');
|
|
48
|
+
if (files.has(instructions) || files.has(claude))
|
|
49
|
+
throw new Error(`Output collision: ${scope}`);
|
|
50
|
+
files.set(instructions, {
|
|
51
|
+
content: Buffer.from(`${content.join('\n\n')}\n`),
|
|
52
|
+
mode: 0o644,
|
|
53
|
+
});
|
|
54
|
+
// Imports resolve beside CLAUDE.md, including within nested scopes.
|
|
55
|
+
files.set(claude, {
|
|
56
|
+
content: Buffer.from(catalog.global ? '@../.codex/AGENTS.md\n' : '@AGENTS.md\n'),
|
|
57
|
+
mode: 0o644,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
files: new Map([...files].sort(([a], [b]) => a.localeCompare(b))),
|
|
62
|
+
skillRoots,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type Answer, type Catalog, type Question, type State } from './schema.js';
|
|
2
|
+
export declare function resolveKits(catalog: Catalog, selected: string[]): string[];
|
|
3
|
+
export declare function reasons(catalog: Catalog, selected: string[], target: string): string[];
|
|
4
|
+
export declare function disableKits(catalog: Catalog, selected: string[], target: string, cascade: boolean): string[];
|
|
5
|
+
export declare function configure(catalog: Catalog, state: State, ask?: (kit: string, key: string, question: Question, saved: Answer | undefined) => Promise<Answer>): Promise<State>;
|
|
6
|
+
export declare function setAnswers(catalog: Catalog, state: State, values: string[]): void;
|