@lidtop/loadout 0.2.0 → 0.3.1
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/bundled.d.ts +1 -0
- package/dist/bundled.js +8 -0
- package/dist/cli.js +7 -2
- package/dist/interactive.d.ts +7 -2
- package/dist/interactive.js +66 -25
- package/dist/picker.d.ts +22 -0
- package/dist/picker.js +217 -210
- package/dist/schema.js +2 -1
- package/kits/claude-cli/instructions.md +2 -0
- package/kits/claude-cli/kit.yaml +19 -0
- package/kits/claude-cli/skills/loadout-claude-cli/SKILL.md +7 -0
- package/kits/codex-cli/instructions.md +2 -0
- package/kits/codex-cli/kit.yaml +19 -0
- package/kits/codex-cli/skills/loadout-codex-cli/SKILL.md +7 -0
- package/kits/dont-commit/instructions.md +1 -0
- package/kits/dont-commit/kit.yaml +6 -0
- package/kits/opencode-cli/instructions.md +2 -0
- package/kits/opencode-cli/kit.yaml +19 -0
- package/kits/opencode-cli/skills/loadout-opencode-cli/SKILL.md +7 -0
- package/kits/write-kit/skills/loadout-write-kit/SKILL.md +3 -1
- package/package.json +1 -1
- package/dist/locations.d.ts +0 -1
- package/dist/locations.js +0 -4
package/README.md
CHANGED
|
@@ -13,7 +13,11 @@ npm install -g @lidtop/loadout
|
|
|
13
13
|
loadout
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
Pick your kits, select **
|
|
16
|
+
Pick your kits, select **Review changes**, and confirm. Inside a provider, choose **Back to providers** to keep browsing with your selections preserved. Run `loadout` again to change your selection.
|
|
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** at a setup question or review prompt to return to the picker with your selections and position preserved. Press **Esc twice quickly** or **Ctrl+C** to cancel setup.
|
|
17
21
|
|
|
18
22
|
Conflicting project instructions are skipped with a warning; the kit’s skills still install.
|
|
19
23
|
|
package/dist/bundled.d.ts
CHANGED
package/dist/bundled.js
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
1
|
import { fileURLToPath } from 'node:url';
|
|
2
2
|
// Shared by source runs and the published dist/ entry point.
|
|
3
3
|
export const bundledRoot = fileURLToPath(new URL('../kits/', import.meta.url));
|
|
4
|
+
const agentCliKits = new Set([
|
|
5
|
+
'loadout-claude-cli',
|
|
6
|
+
'loadout-codex-cli',
|
|
7
|
+
'loadout-opencode-cli',
|
|
8
|
+
]);
|
|
9
|
+
export function bundledProvider(id) {
|
|
10
|
+
return agentCliKits.has(id) ? 'loadout-agent-clis' : 'loadout';
|
|
11
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -134,7 +134,9 @@ async function generate(catalog, state, opts) {
|
|
|
134
134
|
}
|
|
135
135
|
async function setup() {
|
|
136
136
|
const { targets, initial } = interactiveTargets();
|
|
137
|
-
|
|
137
|
+
await interactive(targets, initial, { review: reviewSelections });
|
|
138
|
+
}
|
|
139
|
+
async function reviewSelections(configured) {
|
|
138
140
|
const offline = program.opts().offline;
|
|
139
141
|
const plans = [];
|
|
140
142
|
for (const { target, state } of configured) {
|
|
@@ -165,7 +167,10 @@ async function setup() {
|
|
|
165
167
|
}
|
|
166
168
|
if (!plans.some(hasChanges))
|
|
167
169
|
return;
|
|
168
|
-
|
|
170
|
+
const scopes = configured
|
|
171
|
+
.filter((_, index) => hasChanges(plans[index]))
|
|
172
|
+
.map(({ target }) => target.label);
|
|
173
|
+
if (await confirmApply(undefined, scopes)) {
|
|
169
174
|
applyAll(plans);
|
|
170
175
|
console.log('\nYour loadout is ready.');
|
|
171
176
|
for (const result of plans) {
|
package/dist/interactive.d.ts
CHANGED
|
@@ -2,9 +2,14 @@ import { checkbox, confirm, select } from '@inquirer/prompts';
|
|
|
2
2
|
import { type Catalog, type State } from './schema.js';
|
|
3
3
|
import { type TargetSelection } from './picker.js';
|
|
4
4
|
import { type Target } from './targets.js';
|
|
5
|
-
|
|
5
|
+
type PromptContext = NonNullable<Parameters<typeof confirm>[1]>;
|
|
6
|
+
export declare function interactive(targets: Target[], initial?: number, options?: {
|
|
7
|
+
context?: PromptContext;
|
|
8
|
+
review?: (selections: TargetSelection[], context?: PromptContext) => Promise<void>;
|
|
9
|
+
}): Promise<TargetSelection[]>;
|
|
6
10
|
export declare function configureSelection(target: Target, state: State, context?: Parameters<typeof confirm>[1]): Promise<State>;
|
|
7
|
-
export declare function confirmApply(): Promise<boolean>;
|
|
11
|
+
export declare function confirmApply(context?: PromptContext, scopes?: string[]): Promise<boolean>;
|
|
8
12
|
export declare function selectUpdates(catalog: Catalog, state: State, offline?: boolean, context?: Parameters<typeof checkbox>[1]): Promise<string[]>;
|
|
9
13
|
export declare function confirmRetry(id: string, error: Error, context?: Parameters<typeof select>[1]): Promise<boolean>;
|
|
10
14
|
export declare function confirmAdoption(paths: string[], context?: Parameters<typeof confirm>[1]): Promise<boolean>;
|
|
15
|
+
export {};
|
package/dist/interactive.js
CHANGED
|
@@ -1,21 +1,57 @@
|
|
|
1
1
|
import { checkbox, confirm, select } from '@inquirer/prompts';
|
|
2
2
|
import { configure } from './resolve.js';
|
|
3
3
|
import { validAnswer } from './schema.js';
|
|
4
|
-
import { targetPicker } from './picker.js';
|
|
4
|
+
import { targetPicker, } from './picker.js';
|
|
5
5
|
import { availableUpdates, updateDescription } from './updates.js';
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
import { prepareInput } from './terminal.js';
|
|
7
|
+
class BackToPicker extends Error {
|
|
8
|
+
}
|
|
9
|
+
async function backPrompt(prompt, context = {}) {
|
|
10
|
+
const input = context.input ?? process.stdin;
|
|
11
|
+
prepareInput(input);
|
|
12
|
+
const controller = new AbortController();
|
|
13
|
+
const back = (_text, key) => {
|
|
14
|
+
if (key.name === 'escape')
|
|
15
|
+
controller.abort(new BackToPicker());
|
|
16
|
+
};
|
|
17
|
+
input.on('keypress', back);
|
|
18
|
+
try {
|
|
19
|
+
return await prompt({
|
|
20
|
+
...context,
|
|
21
|
+
signal: context.signal
|
|
22
|
+
? AbortSignal.any([context.signal, controller.signal])
|
|
23
|
+
: controller.signal,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (error instanceof Error && error.cause instanceof BackToPicker)
|
|
28
|
+
throw error.cause;
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
input.off('keypress', back);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export async function interactive(targets, initial = 0, options = {}) {
|
|
36
|
+
if (!options.context && (!process.stdin.isTTY || !process.stdout.isTTY))
|
|
8
37
|
throw new Error('Interactive setup needs a terminal. Use loadout enable <kit>, then loadout apply.');
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
initial,
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
38
|
+
const session = {};
|
|
39
|
+
for (;;) {
|
|
40
|
+
const chosen = await targetPicker({ targets, initial, session }, options.context);
|
|
41
|
+
try {
|
|
42
|
+
const configured = [];
|
|
43
|
+
for (const { target, state } of chosen) {
|
|
44
|
+
const value = await configureSelection(target, state, options.context);
|
|
45
|
+
configured.push({ target, state: value });
|
|
46
|
+
}
|
|
47
|
+
await options.review?.(configured, options.context);
|
|
48
|
+
return configured;
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (!(error instanceof BackToPicker))
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
17
54
|
}
|
|
18
|
-
return configured;
|
|
19
55
|
}
|
|
20
56
|
export async function configureSelection(target, state, context) {
|
|
21
57
|
const change = new Map();
|
|
@@ -23,10 +59,10 @@ export async function configureSelection(target, state, context) {
|
|
|
23
59
|
if (!change.has(kit)) {
|
|
24
60
|
const hasSaved = Object.entries(target.catalog.kits.get(kit).questions).some(([name, q]) => validAnswer(q, state.answers[kit]?.[name]));
|
|
25
61
|
change.set(kit, hasSaved
|
|
26
|
-
? await confirm({
|
|
62
|
+
? await backPrompt((context) => confirm({
|
|
27
63
|
message: `${target.label} · ${kit} · Change selection?`,
|
|
28
64
|
default: false,
|
|
29
|
-
}, context)
|
|
65
|
+
}, context), context)
|
|
30
66
|
: true);
|
|
31
67
|
}
|
|
32
68
|
const saved = state.answers[kit]?.[key];
|
|
@@ -34,16 +70,21 @@ export async function configureSelection(target, state, context) {
|
|
|
34
70
|
return saved;
|
|
35
71
|
const message = `${target.label} · ${kit} · ${question.message}`;
|
|
36
72
|
if (question.type === 'boolean')
|
|
37
|
-
return confirm({ message, default: typeof value === 'boolean' ? value : false }, context);
|
|
38
|
-
return select({
|
|
73
|
+
return backPrompt((context) => confirm({ message, default: typeof value === 'boolean' ? value : false }, context), context);
|
|
74
|
+
return backPrompt((context) => select({
|
|
39
75
|
message,
|
|
40
76
|
choices: question.choices.map((v) => ({ name: v, value: v })),
|
|
41
77
|
default: typeof value === 'string' ? value : undefined,
|
|
42
|
-
}, context);
|
|
78
|
+
}, context), context);
|
|
43
79
|
});
|
|
44
80
|
}
|
|
45
|
-
export async function confirmApply() {
|
|
46
|
-
return confirm({
|
|
81
|
+
export async function confirmApply(context, scopes = []) {
|
|
82
|
+
return backPrompt((context) => confirm({
|
|
83
|
+
message: scopes.length
|
|
84
|
+
? `Apply changes to ${scopes.join(' and ')}?`
|
|
85
|
+
: 'Apply changes?',
|
|
86
|
+
default: true,
|
|
87
|
+
}, context), context);
|
|
47
88
|
}
|
|
48
89
|
export async function selectUpdates(catalog, state, offline = false, context) {
|
|
49
90
|
const updates = availableUpdates(catalog, state.selected);
|
|
@@ -53,7 +94,7 @@ export async function selectUpdates(catalog, state, offline = false, context) {
|
|
|
53
94
|
console.log(`${updates.length} catalog update(s) available. Run loadout online to review them; keeping saved versions.`);
|
|
54
95
|
return [];
|
|
55
96
|
}
|
|
56
|
-
return checkbox({
|
|
97
|
+
return backPrompt((context) => checkbox({
|
|
57
98
|
message: 'Catalog updates available · choose kits to update (Enter skips)',
|
|
58
99
|
choices: updates.map((kit) => ({
|
|
59
100
|
name: kit.id,
|
|
@@ -61,20 +102,20 @@ export async function selectUpdates(catalog, state, offline = false, context) {
|
|
|
61
102
|
description: updateDescription(kit),
|
|
62
103
|
})),
|
|
63
104
|
required: false,
|
|
64
|
-
}, context);
|
|
105
|
+
}, context), context);
|
|
65
106
|
}
|
|
66
107
|
export async function confirmRetry(id, error, context) {
|
|
67
|
-
return select({
|
|
108
|
+
return backPrompt((context) => select({
|
|
68
109
|
message: `${id}: ${error.message}`,
|
|
69
110
|
choices: [
|
|
70
111
|
{ name: 'Retry download', value: true },
|
|
71
112
|
{ name: 'Cancel', value: false },
|
|
72
113
|
],
|
|
73
|
-
}, context);
|
|
114
|
+
}, context), context);
|
|
74
115
|
}
|
|
75
116
|
export async function confirmAdoption(paths, context) {
|
|
76
|
-
return confirm({
|
|
117
|
+
return backPrompt((context) => confirm({
|
|
77
118
|
message: `Keep existing content and let Loadout manage ${paths.join(', ')}? Originals will be restored when disabled.`,
|
|
78
119
|
default: true,
|
|
79
|
-
}, context);
|
|
120
|
+
}, context), context);
|
|
80
121
|
}
|
package/dist/picker.d.ts
CHANGED
|
@@ -6,11 +6,32 @@ export type PickerConfig = {
|
|
|
6
6
|
columns?: number;
|
|
7
7
|
rows?: number;
|
|
8
8
|
};
|
|
9
|
+
declare const repositorySections: readonly ['Kits', 'Browse', 'Installed'];
|
|
10
|
+
type Section = (typeof repositorySections)[number];
|
|
9
11
|
export type TargetPickerConfig = {
|
|
10
12
|
targets: Target[];
|
|
11
13
|
initial?: number;
|
|
12
14
|
columns?: number;
|
|
13
15
|
rows?: number;
|
|
16
|
+
session?: PickerSession;
|
|
17
|
+
};
|
|
18
|
+
type PickerView = {
|
|
19
|
+
section: Section;
|
|
20
|
+
provider: string | undefined;
|
|
21
|
+
providerList: {
|
|
22
|
+
query: string;
|
|
23
|
+
active: number;
|
|
24
|
+
};
|
|
25
|
+
query: string;
|
|
26
|
+
active: number;
|
|
27
|
+
};
|
|
28
|
+
export type PickerSession = {
|
|
29
|
+
snapshot?: PickerView & {
|
|
30
|
+
targetIndex: number;
|
|
31
|
+
visited: number[];
|
|
32
|
+
selections: string[][];
|
|
33
|
+
views: PickerView[];
|
|
34
|
+
};
|
|
14
35
|
};
|
|
15
36
|
export type TargetSelection = {
|
|
16
37
|
target: Target;
|
|
@@ -21,6 +42,7 @@ declare const renderPicker: import("@inquirer/type").Prompt<TargetSelection[], {
|
|
|
21
42
|
initial?: number;
|
|
22
43
|
columns?: number;
|
|
23
44
|
rows?: number;
|
|
45
|
+
session?: PickerSession;
|
|
24
46
|
} & TargetPickerConfig>;
|
|
25
47
|
export declare function targetPicker(config: TargetPickerConfig, context?: Parameters<typeof renderPicker>[1]): Promise<TargetSelection[]>;
|
|
26
48
|
export declare function kitPicker(config: PickerConfig, context?: Parameters<typeof targetPicker>[1]): Promise<string[]>;
|
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 [
|
|
@@ -35,28 +36,42 @@ function wordmark(width, rows) {
|
|
|
35
36
|
}
|
|
36
37
|
const repositorySections = ['Kits', 'Browse', 'Installed'];
|
|
37
38
|
const providerFor = (kit) => kit.origin === 'bundled'
|
|
38
|
-
?
|
|
39
|
+
? kitSource(kit)
|
|
39
40
|
: (kit.external?.repo ??
|
|
40
41
|
(kit.origin === 'personal' ? 'Personal' : undefined));
|
|
41
42
|
const providerPrefixes = {
|
|
42
43
|
loadout: 'loadout-',
|
|
44
|
+
'loadout-agent-clis': 'loadout-',
|
|
43
45
|
'mattpocock/skills': 'matt-pocock-',
|
|
44
46
|
'anthropics/skills': 'anthropic-',
|
|
45
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
|
+
});
|
|
46
58
|
const emptyState = () => ({
|
|
47
59
|
schemaVersion: 1,
|
|
48
60
|
selected: [],
|
|
49
61
|
answers: {},
|
|
50
62
|
});
|
|
51
63
|
const renderPicker = createPrompt((config, done) => {
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
const [
|
|
55
|
-
const [
|
|
56
|
-
|
|
57
|
-
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)));
|
|
58
72
|
const [notice, setNotice] = useState('');
|
|
59
73
|
const target = targets[targetIndex];
|
|
74
|
+
const accent = (value) => scopeColor(target, value);
|
|
60
75
|
const sections = target.global
|
|
61
76
|
? ['Browse', 'Installed']
|
|
62
77
|
: repositorySections;
|
|
@@ -65,17 +80,18 @@ const renderPicker = createPrompt((config, done) => {
|
|
|
65
80
|
kits: new Map(),
|
|
66
81
|
};
|
|
67
82
|
const selected = selections[targetIndex];
|
|
68
|
-
const
|
|
69
|
-
JSON.stringify([...(
|
|
83
|
+
const pendingTargets = targets.filter((item, index) => JSON.stringify([...selections[index]].sort()) !==
|
|
84
|
+
JSON.stringify([...(item.state?.selected ?? [])].sort()));
|
|
70
85
|
const setSelected = (value) => setSelections(selections.map((ids, index) => (index === targetIndex ? value : ids)));
|
|
71
|
-
const [section, setSection] = useState(
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const [
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
+
}, []);
|
|
79
95
|
const [finished, setFinished] = useState(false);
|
|
80
96
|
const [size, setSize] = useState({
|
|
81
97
|
columns: process.stdout.columns || 80,
|
|
@@ -134,8 +150,10 @@ const renderPicker = createPrompt((config, done) => {
|
|
|
134
150
|
return [];
|
|
135
151
|
const count = kits.filter((kit) => enabledSet.has(kit.id)).length;
|
|
136
152
|
const origins = new Set(kits.map((kit) => kit.origin));
|
|
137
|
-
const description =
|
|
138
|
-
?
|
|
153
|
+
const description = origins.has('bundled')
|
|
154
|
+
? id === 'loadout-agent-clis'
|
|
155
|
+
? 'Delegate tasks through agent CLI harnesses'
|
|
156
|
+
: 'Included with Loadout'
|
|
139
157
|
: id === 'Personal'
|
|
140
158
|
? 'Your personal kits'
|
|
141
159
|
: origins.has('curated') && providerDescriptions[id]
|
|
@@ -173,10 +191,16 @@ const renderPicker = createPrompt((config, done) => {
|
|
|
173
191
|
.sort((a, b) => Number(a.ready === false) - Number(b.ready === false))
|
|
174
192
|
.map((kit) => ({ id: kit.id, description: kit.description, kit }));
|
|
175
193
|
}
|
|
194
|
+
if (provider)
|
|
195
|
+
entries.push({
|
|
196
|
+
id: '@back',
|
|
197
|
+
action: 'back',
|
|
198
|
+
description: 'Keeps your selections',
|
|
199
|
+
});
|
|
176
200
|
entries.push({
|
|
177
|
-
id: '@
|
|
178
|
-
action: '
|
|
179
|
-
description: '
|
|
201
|
+
id: '@review',
|
|
202
|
+
action: 'review',
|
|
203
|
+
description: '',
|
|
180
204
|
});
|
|
181
205
|
const cursor = Math.min(active, Math.max(0, entries.length - 1));
|
|
182
206
|
const focused = entries[cursor];
|
|
@@ -190,72 +214,77 @@ const renderPicker = createPrompt((config, done) => {
|
|
|
190
214
|
rl.clearLine(0);
|
|
191
215
|
rl.write(query);
|
|
192
216
|
};
|
|
193
|
-
const
|
|
194
|
-
setScopeFocused(false);
|
|
195
|
-
setTargetIndex(index);
|
|
196
|
-
setSection(targets[index].global ||
|
|
197
|
-
![...targets[index].catalog.kits.values()].some((kit) => !providerFor(kit))
|
|
198
|
-
? 'Browse'
|
|
199
|
-
: 'Kits');
|
|
200
|
-
setVisited([...new Set([...visited, index])]);
|
|
217
|
+
const backToProviders = () => {
|
|
201
218
|
setProvider(undefined);
|
|
202
|
-
|
|
203
|
-
|
|
219
|
+
setQuery(providerList.query);
|
|
220
|
+
setActive(providerList.active);
|
|
221
|
+
rl.clearLine(0);
|
|
222
|
+
rl.write(providerList.query);
|
|
204
223
|
};
|
|
205
|
-
|
|
206
|
-
if (
|
|
207
|
-
switchTarget(pendingSwitch);
|
|
208
|
-
setPendingSwitch(undefined);
|
|
209
|
-
}
|
|
210
|
-
else if (key.name === 'n' ||
|
|
211
|
-
key.name === 'escape' ||
|
|
212
|
-
isEnterKey(key)) {
|
|
213
|
-
setPendingSwitch(undefined);
|
|
224
|
+
const switchTarget = (index) => {
|
|
225
|
+
if (index === targetIndex) {
|
|
214
226
|
restoreInput();
|
|
227
|
+
return;
|
|
215
228
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const other = targets[next];
|
|
222
|
-
if (other.error)
|
|
223
|
-
setNotice(other.error);
|
|
224
|
-
else if (!other.catalog)
|
|
225
|
-
setNotice('Cannot load this location.');
|
|
226
|
-
else if (hasSelectionChanges) {
|
|
227
|
-
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;
|
|
228
234
|
}
|
|
229
|
-
|
|
230
|
-
|
|
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('');
|
|
231
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);
|
|
232
253
|
return;
|
|
233
254
|
}
|
|
234
|
-
if (['
|
|
235
|
-
const index =
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
const count = sections.length + (targets.length > 1 ? 1 : 0);
|
|
239
|
-
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';
|
|
240
259
|
const next = (index + (backwards ? -1 : 1) + count) % count;
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
setProvider(undefined);
|
|
245
|
-
reset();
|
|
246
|
-
}
|
|
247
|
-
else
|
|
248
|
-
restoreInput();
|
|
260
|
+
setSection(sections[next]);
|
|
261
|
+
setProvider(undefined);
|
|
262
|
+
reset();
|
|
249
263
|
}
|
|
250
264
|
else if (key.name === 'up' || key.name === 'down') {
|
|
251
|
-
setScopeFocused(false);
|
|
252
265
|
if (entries.length)
|
|
253
266
|
setActive((cursor + (key.name === 'up' ? -1 : 1) + entries.length) %
|
|
254
267
|
entries.length);
|
|
255
268
|
restoreInput();
|
|
256
269
|
}
|
|
257
270
|
else if (isEnterKey(key) || isSpaceKey(key)) {
|
|
258
|
-
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
|
+
};
|
|
259
288
|
setFinished(true);
|
|
260
289
|
done(targets.flatMap((item, index) => {
|
|
261
290
|
if (!item.catalog || !visited.includes(index))
|
|
@@ -270,6 +299,7 @@ const renderPicker = createPrompt((config, done) => {
|
|
|
270
299
|
}));
|
|
271
300
|
}
|
|
272
301
|
else if (browsingProviders && focused) {
|
|
302
|
+
setProviderList({ query, active: cursor });
|
|
273
303
|
setProvider(focused.id);
|
|
274
304
|
setActive(0);
|
|
275
305
|
}
|
|
@@ -283,124 +313,115 @@ const renderPicker = createPrompt((config, done) => {
|
|
|
283
313
|
rl.write(query);
|
|
284
314
|
}
|
|
285
315
|
else if (key.name === 'escape') {
|
|
286
|
-
if (
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
}
|
|
290
|
-
else {
|
|
291
|
-
if (!query && provider)
|
|
292
|
-
setProvider(undefined);
|
|
316
|
+
if (!query && provider)
|
|
317
|
+
backToProviders();
|
|
318
|
+
else
|
|
293
319
|
reset();
|
|
294
|
-
}
|
|
295
320
|
}
|
|
296
321
|
else if (!['left', 'right', 'home', 'end'].includes(key.name)) {
|
|
297
|
-
setScopeFocused(false);
|
|
298
322
|
setQuery(clean(rl.line));
|
|
299
323
|
setActive(0);
|
|
300
324
|
}
|
|
301
325
|
});
|
|
302
|
-
const
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
...scopeLines,
|
|
327
|
-
'',
|
|
328
|
-
` ${bold(fit(`Switch to ${targets[pendingSwitch].label}?`, width - 2))}`,
|
|
329
|
-
...selectionWarning,
|
|
330
|
-
'',
|
|
331
|
-
` ${accent('[Enter/Esc]')} Stay ${accent('[y]')} Switch`,
|
|
332
|
-
'\u001b[?25l',
|
|
333
|
-
].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}`);
|
|
334
350
|
if (finished)
|
|
335
351
|
return [
|
|
336
|
-
...
|
|
352
|
+
...wordmark(width, height, accent),
|
|
337
353
|
` ${accent('✓')} ${selected.length} selected${requiredCount ? muted(` · ${requiredCount} required`) : ''}\n`,
|
|
338
354
|
].join('\n');
|
|
339
355
|
const updates = availableUpdates(catalog).length;
|
|
340
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;
|
|
341
359
|
const tabs = width >= 34
|
|
342
|
-
? ` ${sections
|
|
343
|
-
|
|
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}]`))}`;
|
|
344
369
|
const filter = ` ${accent('/')} ${query ? `${fit(query, width - 6)}${accent('▏')}` : muted(browsingProviders ? 'Search providers or kits' : 'Search kits')}`;
|
|
345
|
-
const
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
:
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
`esc ${provider ? 'back' : 'clear'}`,
|
|
358
|
-
];
|
|
359
|
-
const helpLines = [];
|
|
360
|
-
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 = '';
|
|
361
382
|
for (const hint of hints) {
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
helpLines[helpLines.length - 1] = combined;
|
|
366
|
-
else
|
|
367
|
-
helpLines.push(fit(hint, width - 2));
|
|
368
|
-
}
|
|
369
|
-
const help = helpLines.map((line) => ` ${muted(line)}`);
|
|
370
|
-
if (scopeFocused && other) {
|
|
371
|
-
const action = other.error || !other.catalog
|
|
372
|
-
? `${other.label} is unavailable`
|
|
373
|
-
: `Switch to ${other.label}`;
|
|
374
|
-
return [
|
|
375
|
-
...header,
|
|
376
|
-
...scopeLines,
|
|
377
|
-
'',
|
|
378
|
-
tabs,
|
|
379
|
-
rule,
|
|
380
|
-
` ${bold(fit(action, width - 2))}`,
|
|
381
|
-
...(other.error ? [` ${fit(other.error, width - 2)}`] : []),
|
|
382
|
-
` ${muted(fit(other.root, width - 2))}`,
|
|
383
|
-
rule,
|
|
384
|
-
...help,
|
|
385
|
-
'\u001b[?25l',
|
|
386
|
-
].join('\n');
|
|
383
|
+
const combined = helpText ? `${helpText} · ${hint}` : hint;
|
|
384
|
+
if (stringWidth(combined) <= width - 2)
|
|
385
|
+
helpText = combined;
|
|
387
386
|
}
|
|
387
|
+
const help = [` ${muted(helpText)}`];
|
|
388
388
|
const beforeList = [
|
|
389
|
-
...header,
|
|
390
389
|
...scopeLines,
|
|
391
|
-
...(scopeLines.length && detailed ? [''] : []),
|
|
392
390
|
tabs,
|
|
393
|
-
...(provider
|
|
394
|
-
|
|
395
|
-
|
|
391
|
+
...(provider && !inlineProvider
|
|
392
|
+
? [` ${muted(fit(provider, width - 2))}`]
|
|
393
|
+
: []),
|
|
396
394
|
filter,
|
|
397
|
-
...(detailed ? [''] : []),
|
|
398
395
|
];
|
|
399
396
|
const summary = fit(`${counts}${updates ? ` · ${updates} updates` : ''}`, width - 6);
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
const
|
|
403
|
-
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)));
|
|
404
425
|
const listEntries = entries.filter((row) => !row.action);
|
|
405
426
|
const listCursor = Math.min(cursor, Math.max(0, listEntries.length - 1));
|
|
406
427
|
const start = Math.max(0, Math.min(listCursor - Math.floor(pageSize / 2), listEntries.length - pageSize));
|
|
@@ -433,28 +454,24 @@ const renderPicker = createPrompt((config, done) => {
|
|
|
433
454
|
? 'Will uninstall'
|
|
434
455
|
: hasUpdate(row.kit)
|
|
435
456
|
? 'update'
|
|
436
|
-
:
|
|
437
|
-
? '
|
|
438
|
-
:
|
|
439
|
-
? '
|
|
440
|
-
:
|
|
441
|
-
? 'saved'
|
|
442
|
-
: '';
|
|
457
|
+
: required
|
|
458
|
+
? 'required'
|
|
459
|
+
: row.kit.pinned && !explicit
|
|
460
|
+
? 'saved'
|
|
461
|
+
: '';
|
|
443
462
|
const marker = !row.kit
|
|
444
463
|
? accent('▸')
|
|
445
464
|
: explicit
|
|
446
465
|
? accent('●')
|
|
447
466
|
: required
|
|
448
467
|
? styleText('yellow', '◆')
|
|
449
|
-
: muted('○');
|
|
468
|
+
: muted(accent('○'));
|
|
450
469
|
const badge = required || willUninstall(row.id) || (row.kit && hasUpdate(row.kit))
|
|
451
470
|
? styleText('yellow', label)
|
|
452
471
|
: muted(label);
|
|
453
472
|
const name = fit(displayName(row.id), width - label.length - 9);
|
|
454
473
|
const gap = ' '.repeat(Math.max(2, width - stringWidth(name) - label.length - 6));
|
|
455
|
-
lines.push(` ${focus ? accent('›') : ' '} ${marker} ${row.kit?.ready === false ? muted(name) : focus ? bold(name) : name}${gap}${badge}`);
|
|
456
|
-
if (detailed)
|
|
457
|
-
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))}`);
|
|
458
475
|
}
|
|
459
476
|
if (!page.length) {
|
|
460
477
|
const empty = !catalog.kits.size
|
|
@@ -462,48 +479,38 @@ const renderPicker = createPrompt((config, done) => {
|
|
|
462
479
|
: query
|
|
463
480
|
? 'No matching kits. Esc to clear.'
|
|
464
481
|
: section === 'Kits'
|
|
465
|
-
? 'No repository kits.
|
|
482
|
+
? 'No repository kits. ←→ to browse.'
|
|
466
483
|
: section === 'Installed'
|
|
467
|
-
? 'No kits installed.
|
|
484
|
+
? 'No kits installed. ←→ to browse.'
|
|
468
485
|
: 'No external providers configured.';
|
|
469
486
|
lines.push(` ${muted(fit(empty, width - 2))}`);
|
|
470
487
|
}
|
|
471
|
-
const kit = focused?.kit;
|
|
472
|
-
const why = kit ? reasons(catalog, selected, kit.id) : [];
|
|
473
|
-
const detail = focused?.action
|
|
474
|
-
? focused.description
|
|
475
|
-
: browsingProviders
|
|
476
|
-
? [
|
|
477
|
-
focused?.selectedCount ? `${focused.selectedCount} selected` : '',
|
|
478
|
-
focused?.downloadedCount
|
|
479
|
-
? `${focused.downloadedCount} downloaded`
|
|
480
|
-
: '',
|
|
481
|
-
]
|
|
482
|
-
.filter(Boolean)
|
|
483
|
-
.join(' · ') || 'Choose a provider to explore its kits'
|
|
484
|
-
: kit?.ready === false
|
|
485
|
-
? 'Edit this kit, then set ready: true in kit.yaml'
|
|
486
|
-
: kit && willUninstall(kit.id)
|
|
487
|
-
? 'Uninstall on apply. Select again to keep.'
|
|
488
|
-
: kit && hasUpdate(kit)
|
|
489
|
-
? `Catalog update · ${kit.pinned.ref.slice(0, 8)} → ${kit.external.ref.slice(0, 8)}`
|
|
490
|
-
: why.length
|
|
491
|
-
? `Required by ${why.map(displayName).join(', ')}${selected.includes(kit.id) ? ' · also selected' : ' · space to keep explicitly'}`
|
|
492
|
-
: kit?.requires.length
|
|
493
|
-
? `Requires ${kit.requires.map(displayName).join(', ')}`
|
|
494
|
-
: kit?.external
|
|
495
|
-
? `Includes ${(kit.pinned ?? kit.external).skills.map((p) => p.split('/').at(-1)).join(', ')}`
|
|
496
|
-
: '';
|
|
497
488
|
const pagination = listEntries.length > page.length
|
|
498
489
|
? `${start + 1}–${start + page.length} of ${listEntries.length}`
|
|
499
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('─')}`;
|
|
500
493
|
return [
|
|
501
494
|
...beforeList,
|
|
502
495
|
...lines,
|
|
496
|
+
` ${muted(fit(detail, width - 2))}`,
|
|
503
497
|
bottomRule,
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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
|
+
}),
|
|
507
514
|
...help,
|
|
508
515
|
'\u001b[?25l',
|
|
509
516
|
].join('\n');
|
package/dist/schema.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { bundledProvider } from './bundled.js';
|
|
2
3
|
export const idSchema = z
|
|
3
4
|
.string()
|
|
4
5
|
.regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, 'Use lowercase words separated by hyphens');
|
|
@@ -134,7 +135,7 @@ export function parse(schema, value, label) {
|
|
|
134
135
|
}
|
|
135
136
|
export function kitSource(kit) {
|
|
136
137
|
return kit.origin === 'bundled'
|
|
137
|
-
?
|
|
138
|
+
? bundledProvider(kit.id)
|
|
138
139
|
: ((kit.pinned ?? kit.external)?.repo ??
|
|
139
140
|
(kit.origin === 'personal' ? 'Personal' : 'Repository'));
|
|
140
141
|
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
Use `--model opus` by default for Claude CLI; use `--model fable` only when asked. Start with `claude -p --model opus --output-format json "PROMPT"` and capture `session_id` from the response.
|
|
2
|
+
For follow-ups, run `claude -p --resume SESSION_ID --model opus --output-format json "FOLLOW-UP"` in the same working directory; substitute `fable` in both commands when requested.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
schemaVersion: 1
|
|
2
|
+
id: claude-cli
|
|
3
|
+
description: Run non-interactive Claude CLI conversations
|
|
4
|
+
questions:
|
|
5
|
+
placement:
|
|
6
|
+
type: choice
|
|
7
|
+
message: Add Claude CLI guidance to context or as a skill?
|
|
8
|
+
choices: [context, skill]
|
|
9
|
+
outputs:
|
|
10
|
+
- type: instructions
|
|
11
|
+
source: instructions.md
|
|
12
|
+
when:
|
|
13
|
+
answer: placement
|
|
14
|
+
equals: context
|
|
15
|
+
- type: skill
|
|
16
|
+
source: skills/loadout-claude-cli
|
|
17
|
+
when:
|
|
18
|
+
answer: placement
|
|
19
|
+
equals: skill
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: loadout-claude-cli
|
|
3
|
+
description: Run non-interactive Claude CLI prompts and follow-up conversations. Use when asked to delegate a task to Claude CLI or continue a Claude CLI session.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Use `--model opus` by default for Claude CLI; use `--model fable` only when asked. Start with `claude -p --model opus --output-format json "PROMPT"` and capture `session_id` from the response.
|
|
7
|
+
For follow-ups, run `claude -p --resume SESSION_ID --model opus --output-format json "FOLLOW-UP"` in the same working directory; substitute `fable` in both commands when requested.
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
Use `--model gpt-5.6-sol` by default; use `--model gpt-6-astra` only when asked. Start with `codex exec --model gpt-5.6-sol --json "PROMPT"` and capture `thread_id` from the output.
|
|
2
|
+
For follow-ups, run `codex exec resume --model gpt-5.6-sol --json SESSION_ID "FOLLOW-UP"` in the same working directory; substitute `gpt-6-astra` in both commands when requested.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
schemaVersion: 1
|
|
2
|
+
id: codex-cli
|
|
3
|
+
description: Run non-interactive Codex CLI conversations
|
|
4
|
+
questions:
|
|
5
|
+
placement:
|
|
6
|
+
type: choice
|
|
7
|
+
message: Add Codex CLI guidance to context or as a skill?
|
|
8
|
+
choices: [context, skill]
|
|
9
|
+
outputs:
|
|
10
|
+
- type: instructions
|
|
11
|
+
source: instructions.md
|
|
12
|
+
when:
|
|
13
|
+
answer: placement
|
|
14
|
+
equals: context
|
|
15
|
+
- type: skill
|
|
16
|
+
source: skills/loadout-codex-cli
|
|
17
|
+
when:
|
|
18
|
+
answer: placement
|
|
19
|
+
equals: skill
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: loadout-codex-cli
|
|
3
|
+
description: Run non-interactive Codex CLI prompts and follow-up conversations. Use when asked to delegate a task to Codex CLI or continue a Codex CLI session.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Use `--model gpt-5.6-sol` by default; use `--model gpt-6-astra` only when asked. Start with `codex exec --model gpt-5.6-sol --json "PROMPT"` and capture `thread_id` from the output.
|
|
7
|
+
For follow-ups, run `codex exec resume --model gpt-5.6-sol --json SESSION_ID "FOLLOW-UP"` in the same working directory; substitute `gpt-6-astra` in both commands when requested.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Do not commit changes or perform Git operations. Leave Git to the user.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
schemaVersion: 1
|
|
2
|
+
id: opencode-cli
|
|
3
|
+
description: Run non-interactive OpenCode CLI conversations
|
|
4
|
+
questions:
|
|
5
|
+
placement:
|
|
6
|
+
type: choice
|
|
7
|
+
message: Add OpenCode CLI guidance to context or as a skill?
|
|
8
|
+
choices: [context, skill]
|
|
9
|
+
outputs:
|
|
10
|
+
- type: instructions
|
|
11
|
+
source: instructions.md
|
|
12
|
+
when:
|
|
13
|
+
answer: placement
|
|
14
|
+
equals: context
|
|
15
|
+
- type: skill
|
|
16
|
+
source: skills/loadout-opencode-cli
|
|
17
|
+
when:
|
|
18
|
+
answer: placement
|
|
19
|
+
equals: skill
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: loadout-opencode-cli
|
|
3
|
+
description: Run non-interactive OpenCode CLI prompts and follow-up conversations. Use when asked to delegate a task to OpenCode CLI or continue an OpenCode CLI session.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Start with `opencode run --format json "PROMPT"` using the configured default model, and capture `sessionID` from the output.
|
|
7
|
+
For follow-ups, run `opencode run --session SESSION_ID --format json "FOLLOW-UP"` in the same working directory.
|
|
@@ -5,6 +5,8 @@ description: Create or edit private or shared Loadout kits.
|
|
|
5
5
|
|
|
6
6
|
Use `.loadout-personal/kits/` for private kits, `.loadout/kits/` for shared kits, or `~/.loadout/kits/` for kits you can enable in any repository. Adapt this example; drop unused questions, outputs, and files.
|
|
7
7
|
|
|
8
|
+
State each requirement once as a direct instruction, including its exceptions. Preserve its exact trigger and scope. Omit introductions, summaries, explanations of familiar concepts, and lists of actions a rule already covers. Do not add unrequested rules or steps.
|
|
9
|
+
|
|
8
10
|
`.loadout-personal/kits/review/kit.yaml`:
|
|
9
11
|
|
|
10
12
|
```yaml
|
|
@@ -52,4 +54,4 @@ loadout enable review --answer review.diagrams=true --answer review.detail=detai
|
|
|
52
54
|
loadout disable review
|
|
53
55
|
```
|
|
54
56
|
|
|
55
|
-
Drop `--dry-run --diff` to apply, or select via `loadout` → Browse → Personal →
|
|
57
|
+
Drop `--dry-run --diff` to apply, or select via `loadout` → Browse → Personal → Review changes.
|
package/package.json
CHANGED
package/dist/locations.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function stateDirectory(root: string): string;
|
package/dist/locations.js
DELETED