@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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Loadout contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Loadout
|
|
2
|
+
|
|
3
|
+
Share a collection of agent skills and instructions with your team. Everyone in the repository picks the kits they want, keeping the full collection available without adding it all to their agent's context.
|
|
4
|
+
|
|
5
|
+
Kit definitions live in the repository. Each developer's selections and generated files stay local and ignored by Git. Loadout configures both Codex and Claude Code.
|
|
6
|
+
|
|
7
|
+
## Use it
|
|
8
|
+
|
|
9
|
+
Requires Node.js **22.13+**. In a repository using Loadout:
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npm install -g @lidtop/loadout
|
|
13
|
+
loadout
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Pick your kits, select **Continue**, and confirm. Run `loadout` again to change your selection.
|
|
17
|
+
|
|
18
|
+
## Add it to your repository
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
loadout init
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Add kits under `.loadout/kits/` and commit the collection for your team. Enable **Browse → loadout → write-kit** for a short guide your agent can use to create kits.
|
|
25
|
+
|
|
26
|
+
`loadout --help` lists the commands.
|
|
27
|
+
|
|
28
|
+
[MIT](LICENSE) · External kits retain their upstream licenses.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { FileContent, Rendered } from './render.js';
|
|
2
|
+
import type { Change } from './storage.js';
|
|
3
|
+
export declare function prepareAdoption(root: string, rendered: Rendered, owned: Record<string, {
|
|
4
|
+
hash: string;
|
|
5
|
+
mode: number;
|
|
6
|
+
}>, global: boolean, allow: boolean): {
|
|
7
|
+
files: Map<string, FileContent>;
|
|
8
|
+
originals: Map<string, FileContent>;
|
|
9
|
+
released: Set<string>;
|
|
10
|
+
adopted: string[];
|
|
11
|
+
beforeOutputs: Change[];
|
|
12
|
+
afterOutputs: Change[];
|
|
13
|
+
};
|
package/dist/adoption.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { exists, json, readOptional, safePath, walk, portableMode, } from './fs.js';
|
|
6
|
+
import { isOutput } from './output-path.js';
|
|
7
|
+
import { parse } from './schema.js';
|
|
8
|
+
const manifestPath = '.loadout/adopted.json';
|
|
9
|
+
const digest = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
|
10
|
+
const manifestSchema = z
|
|
11
|
+
.object({
|
|
12
|
+
schemaVersion: z.literal(1),
|
|
13
|
+
files: z.record(z.string(), z
|
|
14
|
+
.object({
|
|
15
|
+
hash: z.string().regex(/^[a-f0-9]{64}$/),
|
|
16
|
+
mode: z.number().int().min(0).max(0o777),
|
|
17
|
+
})
|
|
18
|
+
.strict()),
|
|
19
|
+
})
|
|
20
|
+
.strict();
|
|
21
|
+
function read(root, file) {
|
|
22
|
+
const content = readOptional(root, file);
|
|
23
|
+
return content === undefined
|
|
24
|
+
? undefined
|
|
25
|
+
: {
|
|
26
|
+
content,
|
|
27
|
+
mode: portableMode(fs.statSync(safePath(root, file)).mode),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function compose(original, generated, file) {
|
|
31
|
+
const base = original.toString('utf8');
|
|
32
|
+
if (!Buffer.from(base).equals(original) || original.includes(0))
|
|
33
|
+
throw new Error(`Cannot preserve ${file}: expected UTF-8 instructions.`);
|
|
34
|
+
if (path.posix.basename(file) === 'CLAUDE.md') {
|
|
35
|
+
const normalize = (line) => line.trim().replace(/^@\.\//, '@');
|
|
36
|
+
if (base
|
|
37
|
+
.split(/\r?\n/)
|
|
38
|
+
.some((line) => normalize(line) === normalize(generated.toString())))
|
|
39
|
+
return original;
|
|
40
|
+
}
|
|
41
|
+
const crlf = (base.match(/\r\n/g) ?? []).length;
|
|
42
|
+
const newline = crlf > (base.match(/(?<!\r)\n/g) ?? []).length ? '\r\n' : '\n';
|
|
43
|
+
return Buffer.concat([
|
|
44
|
+
original,
|
|
45
|
+
Buffer.from(base ? (base.endsWith('\n') ? newline : newline + newline) : ''),
|
|
46
|
+
Buffer.from(generated.toString('utf8').replace(/\r?\n/g, newline)),
|
|
47
|
+
]);
|
|
48
|
+
}
|
|
49
|
+
// This only prepares bytes. Baselines, outputs, and ownership are committed by
|
|
50
|
+
// the same preview/recheck/rollback transaction as every other Loadout file.
|
|
51
|
+
export function prepareAdoption(root, rendered, owned, global, allow) {
|
|
52
|
+
const beforeManifest = read(root, manifestPath);
|
|
53
|
+
const manifest = beforeManifest
|
|
54
|
+
? parse(manifestSchema, JSON.parse(beforeManifest.content.toString()), manifestPath)
|
|
55
|
+
: {
|
|
56
|
+
schemaVersion: 1,
|
|
57
|
+
files: {},
|
|
58
|
+
};
|
|
59
|
+
const originals = new Map();
|
|
60
|
+
const blobs = new Map();
|
|
61
|
+
for (const [file, entry] of Object.entries(manifest.files)) {
|
|
62
|
+
if (!isOutput(file, global) || !Object.hasOwn(owned, file))
|
|
63
|
+
throw new Error(`Unexpected adopted output: ${file}`);
|
|
64
|
+
const blobPath = `.loadout/adopted/${entry.hash}`;
|
|
65
|
+
const blob = read(root, blobPath);
|
|
66
|
+
if (!blob || digest(blob.content) !== entry.hash)
|
|
67
|
+
throw new Error(`Original content is missing or changed for ${file}. Restore its .loadout/adopted backup before applying.`);
|
|
68
|
+
blobs.set(blobPath, blob);
|
|
69
|
+
originals.set(file, { content: blob.content, mode: entry.mode });
|
|
70
|
+
}
|
|
71
|
+
const adopted = [];
|
|
72
|
+
if (allow) {
|
|
73
|
+
for (const skill of rendered.skillRoots) {
|
|
74
|
+
if (!exists(safePath(root, skill)) ||
|
|
75
|
+
Object.keys(owned).some((file) => file.startsWith(`${skill}/`)))
|
|
76
|
+
continue;
|
|
77
|
+
const actual = walk(safePath(root, skill))
|
|
78
|
+
.map((file) => `${skill}/${file}`)
|
|
79
|
+
.sort();
|
|
80
|
+
const expected = [...rendered.files.keys()]
|
|
81
|
+
.filter((file) => file.startsWith(`${skill}/`))
|
|
82
|
+
.sort();
|
|
83
|
+
if (JSON.stringify(actual) !== JSON.stringify(expected) ||
|
|
84
|
+
actual.some((file) => {
|
|
85
|
+
const current = read(root, file);
|
|
86
|
+
const next = rendered.files.get(file);
|
|
87
|
+
return (!current.content.equals(next.content) || current.mode !== next.mode);
|
|
88
|
+
}))
|
|
89
|
+
throw new Error(`Existing skill differs: ${skill}. Rename or move that folder, or deselect the kit, before continuing.`);
|
|
90
|
+
}
|
|
91
|
+
for (const file of rendered.files.keys()) {
|
|
92
|
+
if (Object.hasOwn(owned, file))
|
|
93
|
+
continue;
|
|
94
|
+
const original = read(root, file);
|
|
95
|
+
if (original) {
|
|
96
|
+
const expected = rendered.files.get(file);
|
|
97
|
+
if ([...rendered.skillRoots].some((skill) => file.startsWith(`${skill}/`)) &&
|
|
98
|
+
(!original.content.equals(expected.content) ||
|
|
99
|
+
original.mode !== expected.mode))
|
|
100
|
+
throw new Error(`Existing skill differs: ${file}. Rename or move its folder, or deselect the kit, before continuing.`);
|
|
101
|
+
originals.set(file, original);
|
|
102
|
+
adopted.push(file);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const files = new Map(rendered.files);
|
|
107
|
+
const released = new Set();
|
|
108
|
+
const nextManifest = {
|
|
109
|
+
schemaVersion: 1,
|
|
110
|
+
files: {},
|
|
111
|
+
};
|
|
112
|
+
const nextBlobs = new Map();
|
|
113
|
+
for (const [file, original] of originals) {
|
|
114
|
+
const next = files.get(file);
|
|
115
|
+
if (!next) {
|
|
116
|
+
files.set(file, original);
|
|
117
|
+
released.add(file);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (['AGENTS.md', 'CLAUDE.md'].includes(path.posix.basename(file)) &&
|
|
121
|
+
!/^\.(agents|claude)\/skills\//.test(file))
|
|
122
|
+
files.set(file, {
|
|
123
|
+
...next,
|
|
124
|
+
content: compose(original.content, next.content, file),
|
|
125
|
+
});
|
|
126
|
+
const hash = digest(original.content);
|
|
127
|
+
nextManifest.files[file] = { hash, mode: original.mode };
|
|
128
|
+
const blobPath = `.loadout/adopted/${hash}`;
|
|
129
|
+
const existing = blobs.get(blobPath) ?? read(root, blobPath);
|
|
130
|
+
if (existing && !existing.content.equals(original.content))
|
|
131
|
+
throw new Error(`Original backup changed: ${blobPath}`);
|
|
132
|
+
nextBlobs.set(blobPath, existing ?? { content: original.content, mode: portableMode(0o600) });
|
|
133
|
+
}
|
|
134
|
+
const beforeOutputs = [];
|
|
135
|
+
const afterOutputs = [];
|
|
136
|
+
for (const [file, after] of nextBlobs) {
|
|
137
|
+
const before = blobs.get(file) ?? read(root, file);
|
|
138
|
+
beforeOutputs.push({
|
|
139
|
+
path: file,
|
|
140
|
+
before,
|
|
141
|
+
after,
|
|
142
|
+
kind: before ? 'unchanged' : 'create',
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
for (const [file, before] of blobs)
|
|
146
|
+
if (!nextBlobs.has(file))
|
|
147
|
+
afterOutputs.push({ path: file, before, kind: 'delete' });
|
|
148
|
+
if (beforeManifest || Object.keys(nextManifest.files).length) {
|
|
149
|
+
const after = Object.keys(nextManifest.files).length
|
|
150
|
+
? {
|
|
151
|
+
content: json(nextManifest),
|
|
152
|
+
mode: beforeManifest?.mode ?? portableMode(0o600),
|
|
153
|
+
}
|
|
154
|
+
: undefined;
|
|
155
|
+
afterOutputs.push({
|
|
156
|
+
path: manifestPath,
|
|
157
|
+
before: beforeManifest,
|
|
158
|
+
after,
|
|
159
|
+
kind: !after
|
|
160
|
+
? 'delete'
|
|
161
|
+
: !beforeManifest
|
|
162
|
+
? 'create'
|
|
163
|
+
: after.content.equals(beforeManifest.content)
|
|
164
|
+
? 'unchanged'
|
|
165
|
+
: 'update',
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return { files, originals, released, adopted, beforeOutputs, afterOutputs };
|
|
169
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const bundledRoot: string;
|
package/dist/bundled.js
ADDED
package/dist/catalog.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { parse as yaml } from 'yaml';
|
|
5
|
+
import { configSchema, kitSchema, parse, validAnswer, idSchema, } from './schema.js';
|
|
6
|
+
import { exists, safePath, walk } from './fs.js';
|
|
7
|
+
import { curatedKits } from './curated.js';
|
|
8
|
+
import { resolveKits } from './resolve.js';
|
|
9
|
+
import { bundledRoot } from './bundled.js';
|
|
10
|
+
export function discover(cwd) {
|
|
11
|
+
let root = fs.realpathSync(cwd);
|
|
12
|
+
const initial = root;
|
|
13
|
+
const home = fs.realpathSync(os.homedir());
|
|
14
|
+
while (true) {
|
|
15
|
+
// A home catalog is global, never an implicit catalog for child projects.
|
|
16
|
+
if (root === home && root !== initial)
|
|
17
|
+
break;
|
|
18
|
+
if (exists(path.join(root, '.loadout/config.yaml')))
|
|
19
|
+
return root;
|
|
20
|
+
if (exists(path.join(root, '.git')) || path.dirname(root) === root)
|
|
21
|
+
break;
|
|
22
|
+
root = path.dirname(root);
|
|
23
|
+
}
|
|
24
|
+
throw new Error('No .loadout/config.yaml found in this repository. Run loadout init at the repository root.');
|
|
25
|
+
}
|
|
26
|
+
function readYaml(root, relative) {
|
|
27
|
+
try {
|
|
28
|
+
return yaml(fs.readFileSync(safePath(root, relative), 'utf8'));
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
throw new Error(`${relative}: ${error.message}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function validateKit(kit, root, global) {
|
|
35
|
+
if (kit.ready === false)
|
|
36
|
+
return;
|
|
37
|
+
const dir = kit.directory;
|
|
38
|
+
for (const [key, q] of Object.entries(kit.questions)) {
|
|
39
|
+
if (q.default !== undefined && !validAnswer(q, q.default))
|
|
40
|
+
throw new Error(`${kit.id}.${key}: invalid default`);
|
|
41
|
+
if (q.type === 'choice' && new Set(q.choices).size !== q.choices.length)
|
|
42
|
+
throw new Error(`${kit.id}.${key}: duplicate choices`);
|
|
43
|
+
}
|
|
44
|
+
for (const output of kit.outputs) {
|
|
45
|
+
const source = safePath(dir, output.source);
|
|
46
|
+
if (!exists(source))
|
|
47
|
+
throw new Error(`${kit.id}: missing source ${output.source}`);
|
|
48
|
+
if (output.when) {
|
|
49
|
+
const q = kit.questions[output.when.answer];
|
|
50
|
+
if (!q || !validAnswer(q, output.when.equals))
|
|
51
|
+
throw new Error(`${kit.id}: invalid condition on ${output.when.answer}`);
|
|
52
|
+
}
|
|
53
|
+
if (output.type === 'instructions') {
|
|
54
|
+
if (global && output.scope !== '.')
|
|
55
|
+
throw new Error(`${kit.id}: global instructions must use scope: .`);
|
|
56
|
+
if (!fs.statSync(source).isFile())
|
|
57
|
+
throw new Error(`${kit.id}: instructions source must be a file`);
|
|
58
|
+
if (output.scope !== '.') {
|
|
59
|
+
const scope = safePath(root, output.scope);
|
|
60
|
+
if (!exists(scope) || !fs.statSync(scope).isDirectory())
|
|
61
|
+
throw new Error(`${kit.id}: scope directory does not exist: ${output.scope}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
if (!fs.statSync(source).isDirectory())
|
|
66
|
+
throw new Error(`${kit.id}: skill source must be a directory`);
|
|
67
|
+
const name = path.basename(source);
|
|
68
|
+
parse(idSchema, name, `${kit.id}: skill directory name`);
|
|
69
|
+
const files = walk(source);
|
|
70
|
+
if (!files.includes('SKILL.md'))
|
|
71
|
+
throw new Error(`${kit.id}: skill requires SKILL.md`);
|
|
72
|
+
const skill = fs.readFileSync(path.join(source, 'SKILL.md'), 'utf8');
|
|
73
|
+
const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(skill);
|
|
74
|
+
if (!frontmatter)
|
|
75
|
+
throw new Error(`${kit.id}: SKILL.md needs YAML frontmatter`);
|
|
76
|
+
const meta = yaml(frontmatter[1]);
|
|
77
|
+
if (!meta ||
|
|
78
|
+
meta.name !== name ||
|
|
79
|
+
typeof meta.description !== 'string' ||
|
|
80
|
+
!meta.description.trim())
|
|
81
|
+
throw new Error(`${kit.id}: SKILL.md needs name: ${name} and a description`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export function loadCatalog(root, global = path.resolve(root) === fs.realpathSync(os.homedir())) {
|
|
86
|
+
const config = parse(configSchema, readYaml(root, '.loadout/config.yaml'), '.loadout/config.yaml');
|
|
87
|
+
const directory = safePath(root, '.loadout/kits');
|
|
88
|
+
const kits = new Map();
|
|
89
|
+
for (const folder of exists(directory)
|
|
90
|
+
? fs.readdirSync(directory).sort()
|
|
91
|
+
: []) {
|
|
92
|
+
const dir = safePath(directory, folder);
|
|
93
|
+
if (!fs.statSync(dir).isDirectory())
|
|
94
|
+
continue;
|
|
95
|
+
const manifest = `.loadout/kits/${folder}/kit.yaml`;
|
|
96
|
+
const kit = {
|
|
97
|
+
...parse(kitSchema, readYaml(root, manifest), manifest),
|
|
98
|
+
directory: dir,
|
|
99
|
+
};
|
|
100
|
+
if (kits.has(kit.id))
|
|
101
|
+
throw new Error(`Duplicate kit ID: ${kit.id}`);
|
|
102
|
+
validateKit(kit, root, global);
|
|
103
|
+
kits.set(kit.id, kit);
|
|
104
|
+
}
|
|
105
|
+
for (const [definitions, origin] of [
|
|
106
|
+
[config.curated ? curatedKits : [], 'curated'],
|
|
107
|
+
[config.externalKits, 'external'],
|
|
108
|
+
]) {
|
|
109
|
+
for (const definition of definitions) {
|
|
110
|
+
if (origin === 'curated' &&
|
|
111
|
+
config.externalKits.some((kit) => kit.id === definition.id))
|
|
112
|
+
continue;
|
|
113
|
+
if (kits.has(definition.id))
|
|
114
|
+
throw new Error(`Duplicate kit ID: ${definition.id}. Local and external kits must have distinct IDs.`);
|
|
115
|
+
kits.set(definition.id, {
|
|
116
|
+
schemaVersion: 1,
|
|
117
|
+
id: definition.id,
|
|
118
|
+
description: definition.description,
|
|
119
|
+
requires: [],
|
|
120
|
+
questions: {},
|
|
121
|
+
outputs: [],
|
|
122
|
+
directory: root,
|
|
123
|
+
external: definition.source,
|
|
124
|
+
origin,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (config.curated) {
|
|
129
|
+
for (const folder of fs.readdirSync(bundledRoot).sort()) {
|
|
130
|
+
const definition = parse(kitSchema, readYaml(bundledRoot, `${folder}/kit.yaml`), `loadout/${folder}`);
|
|
131
|
+
const kit = {
|
|
132
|
+
...definition,
|
|
133
|
+
id: `loadout-${definition.id}`,
|
|
134
|
+
requires: definition.requires.map((id) => `loadout-${id}`),
|
|
135
|
+
directory: safePath(bundledRoot, folder),
|
|
136
|
+
origin: 'bundled',
|
|
137
|
+
};
|
|
138
|
+
if (kits.has(kit.id))
|
|
139
|
+
throw new Error(`Duplicate kit ID: ${kit.id}`);
|
|
140
|
+
validateKit(kit, root, global);
|
|
141
|
+
kits.set(kit.id, kit);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const catalog = { root, kits, global };
|
|
145
|
+
resolveKits(catalog, [...kits.values()]
|
|
146
|
+
.filter((kit) => kit.ready !== false)
|
|
147
|
+
.map((kit) => kit.id));
|
|
148
|
+
return catalog;
|
|
149
|
+
}
|
package/dist/cli.d.ts
ADDED