@lidtop/loadout 0.3.2 → 0.4.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/README.md +4 -6
- package/dist/cli.js +0 -0
- package/dist/picker.js +23 -7
- package/dist/schema.d.ts +3 -1
- package/dist/schema.js +2 -1
- package/dist/storage.d.ts +3 -1
- package/dist/storage.js +34 -7
- package/dist/targets.d.ts +1 -0
- package/dist/targets.js +7 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Choose agent skills and instructions for yourself or your team. Loadout configures Codex and Claude Code.
|
|
4
4
|
|
|
5
|
+
[Watch the demo](demo.mp4)
|
|
6
|
+
|
|
5
7
|
Private kits and selections live in `.loadout-personal/`, ignored by Git. Add reusable kits to `~/.loadout/kits/` and enable them per repository.
|
|
6
8
|
|
|
7
9
|
## Use it
|
|
@@ -13,13 +15,9 @@ npm install -g @lidtop/loadout
|
|
|
13
15
|
loadout
|
|
14
16
|
```
|
|
15
17
|
|
|
16
|
-
Pick your kits
|
|
17
|
-
|
|
18
|
-
The destination selector above the tabs shows **Repository** in cyan (this repository only) and **Global** in magenta (your user configuration across repositories). Active controls and selection markers follow the destination's color. Press **Tab** to switch destinations instantly, or **Shift+Tab** to cycle backward. **←/→** switches between the Kits, Browse, and Installed sections. Each destination keeps its own selections, provider, search, and cursor position, ready to browse as soon as you switch. A **\*** marks destinations with unapplied selections; **Review changes** includes the session's changes across both. Descriptions appear beneath every provider and kit, with dependency or update notes when needed.
|
|
19
|
-
|
|
20
|
-
Press **Esc** from review, questions, or downloads to return to your kits. Inside file details, **Esc** returns to the previous view. Your kit choices, completed answers, and picker position are preserved; completed downloads are reused during the session and file plans are rebuilt before review. In review, **↑/↓** scroll and **Tab** moves between actions. Press **Ctrl+C** to cancel setup, or **Esc twice quickly** from the picker. Nothing is applied until you choose **Apply changes**.
|
|
18
|
+
Pick your kits, select **Review changes**, then **Apply changes**. Run `loadout` again to change your selection.
|
|
21
19
|
|
|
22
|
-
|
|
20
|
+
Choose **Repository** for the current project or **Global** for your user configuration across repositories.
|
|
23
21
|
|
|
24
22
|
## Share kits with your team
|
|
25
23
|
|
package/dist/cli.js
CHANGED
|
File without changes
|
package/dist/picker.js
CHANGED
|
@@ -11,6 +11,11 @@ const muted = (value) => styleText('dim', value);
|
|
|
11
11
|
const bold = (value) => styleText('bold', value);
|
|
12
12
|
const scopeColor = (target, value) => styleText(target.global ? 'magenta' : 'cyan', value);
|
|
13
13
|
const clean = (value) => stripVTControlCharacters(value).replace(/[\x00-\x1f\x7f]/g, ' ');
|
|
14
|
+
function installationTime(value) {
|
|
15
|
+
const date = new Date(value);
|
|
16
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
17
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
18
|
+
}
|
|
14
19
|
function fit(value, width) {
|
|
15
20
|
const text = clean(value);
|
|
16
21
|
if (stringWidth(text) <= width)
|
|
@@ -182,13 +187,16 @@ const renderPicker = createPrompt((config, done) => {
|
|
|
182
187
|
if (section === 'Kits')
|
|
183
188
|
return !providerFor(kit);
|
|
184
189
|
if (section === 'Installed')
|
|
185
|
-
return (
|
|
186
|
-
(!!kit.pinned ||
|
|
187
|
-
enabledSet.has(kit.id) ||
|
|
188
|
-
installedSet.has(kit.id)));
|
|
190
|
+
return installedSet.has(kit.id);
|
|
189
191
|
return providerFor(kit) === provider;
|
|
190
192
|
})
|
|
191
|
-
.sort((a, b) =>
|
|
193
|
+
.sort((a, b) => {
|
|
194
|
+
if (section === 'Installed') {
|
|
195
|
+
const time = (id) => target.installedAt?.[id] ? Date.parse(target.installedAt[id]) : 0;
|
|
196
|
+
return time(b.id) - time(a.id);
|
|
197
|
+
}
|
|
198
|
+
return Number(a.ready === false) - Number(b.ready === false);
|
|
199
|
+
})
|
|
192
200
|
.map((kit) => ({ id: kit.id, description: kit.description, kit }));
|
|
193
201
|
}
|
|
194
202
|
if (provider)
|
|
@@ -352,7 +360,7 @@ const renderPicker = createPrompt((config, done) => {
|
|
|
352
360
|
...wordmark(width, height, accent),
|
|
353
361
|
` ${accent('✓')} ${selected.length} selected${requiredCount ? muted(` · ${requiredCount} required`) : ''}\n`,
|
|
354
362
|
].join('\n');
|
|
355
|
-
const updates = availableUpdates(catalog).length;
|
|
363
|
+
const updates = availableUpdates(catalog, selected).length;
|
|
356
364
|
const counts = `${selected.length} selected${requiredCount ? ` · ${requiredCount} required` : ''}`;
|
|
357
365
|
const tabLabel = (name) => name === 'Browse' && provider ? `Browse › ${clean(provider)}` : name;
|
|
358
366
|
const inlineProvider = !!provider && sections.map(tabLabel).join(' ').length + 2 <= width - 2;
|
|
@@ -421,7 +429,8 @@ const renderPicker = createPrompt((config, done) => {
|
|
|
421
429
|
const spacing = height - beforeList.length - footerHeight >= 8 ? 1 : 0;
|
|
422
430
|
if (spacing)
|
|
423
431
|
beforeList.push('');
|
|
424
|
-
const pageSize = Math.max(1, Math.min(8, Math.floor((height - beforeList.length - footerHeight) /
|
|
432
|
+
const pageSize = Math.max(1, Math.min(8, Math.floor((height - beforeList.length - footerHeight) /
|
|
433
|
+
(section === 'Installed' ? 3 : 2))));
|
|
425
434
|
const listEntries = entries.filter((row) => !row.action);
|
|
426
435
|
const listCursor = Math.min(cursor, Math.max(0, listEntries.length - 1));
|
|
427
436
|
const start = Math.max(0, Math.min(listCursor - Math.floor(pageSize / 2), listEntries.length - pageSize));
|
|
@@ -472,6 +481,13 @@ const renderPicker = createPrompt((config, done) => {
|
|
|
472
481
|
const name = fit(displayName(row.id), width - label.length - 9);
|
|
473
482
|
const gap = ' '.repeat(Math.max(2, width - stringWidth(name) - label.length - 6));
|
|
474
483
|
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))}`);
|
|
484
|
+
if (section === 'Installed' && row.kit) {
|
|
485
|
+
const timestamp = target.installedAt?.[row.id];
|
|
486
|
+
const installed = timestamp
|
|
487
|
+
? `Installed ${installationTime(timestamp)}`
|
|
488
|
+
: 'Installed';
|
|
489
|
+
lines.push(` ${muted(fit(installed, width - 6))}`);
|
|
490
|
+
}
|
|
475
491
|
}
|
|
476
492
|
if (!page.length) {
|
|
477
493
|
const empty = !catalog.kits.size
|
package/dist/schema.d.ts
CHANGED
|
@@ -83,8 +83,9 @@ export declare const stateSchema: z.ZodObject<{
|
|
|
83
83
|
selected: z.ZodArray<z.ZodString>;
|
|
84
84
|
answers: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodBoolean, z.ZodString]>>>;
|
|
85
85
|
}, z.core.$strict>;
|
|
86
|
-
export declare const
|
|
86
|
+
export declare const generatedSchema: z.ZodObject<{
|
|
87
87
|
schemaVersion: z.ZodLiteral<1>;
|
|
88
|
+
installedAt: z.ZodRecord<z.ZodString, z.ZodISODateTime>;
|
|
88
89
|
files: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
89
90
|
hash: z.ZodString;
|
|
90
91
|
mode: z.ZodUnion<readonly [z.ZodLiteral<420>, z.ZodLiteral<493>]>;
|
|
@@ -93,6 +94,7 @@ export declare const ownedSchema: z.ZodObject<{
|
|
|
93
94
|
export type Question = z.infer<typeof question>;
|
|
94
95
|
export type Answer = boolean | string;
|
|
95
96
|
export type State = z.infer<typeof stateSchema>;
|
|
97
|
+
export type Generated = z.infer<typeof generatedSchema>;
|
|
96
98
|
export type Kit = z.infer<typeof kitSchema> & {
|
|
97
99
|
directory: string;
|
|
98
100
|
external?: ExternalSource;
|
package/dist/schema.js
CHANGED
|
@@ -111,9 +111,10 @@ export const stateSchema = z
|
|
|
111
111
|
answers: z.record(idSchema, z.record(idSchema, z.union([z.boolean(), z.string()]))),
|
|
112
112
|
})
|
|
113
113
|
.strict();
|
|
114
|
-
export const
|
|
114
|
+
export const generatedSchema = z
|
|
115
115
|
.object({
|
|
116
116
|
schemaVersion: z.literal(1),
|
|
117
|
+
installedAt: z.record(idSchema, z.iso.datetime()),
|
|
117
118
|
files: z.record(z.string(), z
|
|
118
119
|
.object({
|
|
119
120
|
hash: z.string().regex(/^[a-f0-9]{64}$/),
|
package/dist/storage.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type IgnoreTarget } from './ignore.js';
|
|
2
|
-
import { type Catalog, type State } from './schema.js';
|
|
2
|
+
import { type Catalog, type State, type Generated } from './schema.js';
|
|
3
3
|
import { type FileContent, type Rendered } from './render.js';
|
|
4
4
|
export type Change = {
|
|
5
5
|
path: string;
|
|
@@ -10,6 +10,7 @@ export type Change = {
|
|
|
10
10
|
export type Plan = {
|
|
11
11
|
root: string;
|
|
12
12
|
changes: Change[];
|
|
13
|
+
installing?: string[];
|
|
13
14
|
adopted?: string[];
|
|
14
15
|
skippedInstructions?: {
|
|
15
16
|
paths: string[];
|
|
@@ -27,6 +28,7 @@ export type Plan = {
|
|
|
27
28
|
};
|
|
28
29
|
};
|
|
29
30
|
export declare function loadState(catalog: Catalog): State;
|
|
31
|
+
export declare function loadGenerated(root: string): Generated;
|
|
30
32
|
type ExcludePlan = {
|
|
31
33
|
target: IgnoreTarget;
|
|
32
34
|
paths: string[];
|
package/dist/storage.js
CHANGED
|
@@ -6,7 +6,8 @@ import path from 'node:path';
|
|
|
6
6
|
import { createHash, randomUUID } from 'node:crypto';
|
|
7
7
|
import { execFileSync } from 'node:child_process';
|
|
8
8
|
import { exists, json, readOptional, safePath, walk, portableMode, } from './fs.js';
|
|
9
|
-
import { parse,
|
|
9
|
+
import { parse, generatedSchema, stateSchema, } from './schema.js';
|
|
10
|
+
import { resolveKits } from './resolve.js';
|
|
10
11
|
const hash = (content) => createHash('sha256').update(content).digest('hex');
|
|
11
12
|
export function loadState(catalog) {
|
|
12
13
|
const raw = readOptional(catalog.root, '.loadout-personal/local.json');
|
|
@@ -14,6 +15,12 @@ export function loadState(catalog) {
|
|
|
14
15
|
? parse(stateSchema, JSON.parse(raw.toString()), '.loadout-personal/local.json')
|
|
15
16
|
: { schemaVersion: 1, selected: [], answers: {} };
|
|
16
17
|
}
|
|
18
|
+
export function loadGenerated(root) {
|
|
19
|
+
const raw = readOptional(root, '.loadout-personal/generated.json');
|
|
20
|
+
return raw
|
|
21
|
+
? parse(generatedSchema, JSON.parse(raw.toString()), '.loadout-personal/generated.json')
|
|
22
|
+
: { schemaVersion: 1, installedAt: {}, files: {} };
|
|
23
|
+
}
|
|
17
24
|
function snapshot(root, relative) {
|
|
18
25
|
const content = readOptional(root, relative);
|
|
19
26
|
return content === undefined
|
|
@@ -71,10 +78,12 @@ function trackedFiles(root) {
|
|
|
71
78
|
}
|
|
72
79
|
export function plan(catalog, state, rendered, options = {}) {
|
|
73
80
|
const root = catalog.root;
|
|
74
|
-
const
|
|
75
|
-
const owned =
|
|
76
|
-
|
|
77
|
-
|
|
81
|
+
const generated = loadGenerated(root);
|
|
82
|
+
const owned = generated.files;
|
|
83
|
+
const enabled = resolveKits(catalog, state.selected).sort();
|
|
84
|
+
const installing = enabled.filter((id) => !Object.hasOwn(generated.installedAt, id));
|
|
85
|
+
const plannedAt = new Date().toISOString();
|
|
86
|
+
const installedAt = Object.fromEntries(enabled.map((id) => [id, generated.installedAt[id] ?? plannedAt]));
|
|
78
87
|
const tracked = trackedFiles(root);
|
|
79
88
|
const skippedInstructions = [];
|
|
80
89
|
const retainedKits = new Set(rendered.skillKits);
|
|
@@ -149,7 +158,10 @@ export function plan(catalog, state, rendered, options = {}) {
|
|
|
149
158
|
.map(([p, f]) => [p, { hash: hash(f.content), mode: f.mode }]));
|
|
150
159
|
const metadata = new Map([
|
|
151
160
|
['.loadout-personal/local.json', json(state)],
|
|
152
|
-
[
|
|
161
|
+
[
|
|
162
|
+
'.loadout-personal/generated.json',
|
|
163
|
+
json({ schemaVersion: 1, installedAt, files }),
|
|
164
|
+
],
|
|
153
165
|
]);
|
|
154
166
|
if (rendered.external) {
|
|
155
167
|
const current = readOptional(root, '.loadout-personal/external.json');
|
|
@@ -175,6 +187,7 @@ export function plan(catalog, state, rendered, options = {}) {
|
|
|
175
187
|
return {
|
|
176
188
|
root,
|
|
177
189
|
changes,
|
|
190
|
+
installing,
|
|
178
191
|
adopted: adoption.adopted,
|
|
179
192
|
skippedInstructions,
|
|
180
193
|
kitsWithoutOutputs,
|
|
@@ -321,9 +334,23 @@ export function applyAll(plans) {
|
|
|
321
334
|
for (const { root, change } of writes)
|
|
322
335
|
if (!equal(snapshot(root, change.path), change.before))
|
|
323
336
|
throw new Error(`File changed since preview: ${change.path}. Run the command again.`);
|
|
324
|
-
|
|
337
|
+
const installedAt = new Date().toISOString();
|
|
338
|
+
for (const { root, change: planned } of writes) {
|
|
339
|
+
let change = planned;
|
|
325
340
|
if (change.kind === 'unchanged')
|
|
326
341
|
continue;
|
|
342
|
+
const installing = plans.find((plan) => plan.root === root)?.installing;
|
|
343
|
+
if (change.path === '.loadout-personal/generated.json' &&
|
|
344
|
+
change.after &&
|
|
345
|
+
installing?.length) {
|
|
346
|
+
const generated = parse(generatedSchema, JSON.parse(change.after.content.toString()), change.path);
|
|
347
|
+
for (const id of installing)
|
|
348
|
+
generated.installedAt[id] = installedAt;
|
|
349
|
+
change = {
|
|
350
|
+
...change,
|
|
351
|
+
after: { ...change.after, content: json(generated) },
|
|
352
|
+
};
|
|
353
|
+
}
|
|
327
354
|
if (change.after)
|
|
328
355
|
writeAtomic(root, change.path, change.after);
|
|
329
356
|
else
|
package/dist/targets.d.ts
CHANGED
package/dist/targets.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { loadCatalog } from './catalog.js';
|
|
2
2
|
import { readExternal } from './external.js';
|
|
3
|
-
import { loadState } from './storage.js';
|
|
3
|
+
import { loadGenerated, loadState } from './storage.js';
|
|
4
4
|
export function loadTarget(root, global) {
|
|
5
5
|
const target = {
|
|
6
6
|
root,
|
|
@@ -15,7 +15,12 @@ export function loadTarget(root, global) {
|
|
|
15
15
|
if (kit?.external)
|
|
16
16
|
kit.pinned = snapshot.source;
|
|
17
17
|
}
|
|
18
|
-
return {
|
|
18
|
+
return {
|
|
19
|
+
...target,
|
|
20
|
+
catalog,
|
|
21
|
+
state: loadState(catalog),
|
|
22
|
+
installedAt: loadGenerated(root).installedAt,
|
|
23
|
+
};
|
|
19
24
|
}
|
|
20
25
|
catch (error) {
|
|
21
26
|
return { ...target, error: error.message };
|