@assuredloop/orca-bot-kit 0.1.0 → 0.3.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 +548 -3
- package/SETUP.md +275 -0
- package/package.json +32 -13
- package/rules/evidence.md +15 -0
- package/rules/finishing.md +23 -0
- package/rules/limits.md +18 -0
- package/rules/mail.md +17 -0
- package/rules/profiles.md +9 -0
- package/rules/review.md +14 -0
- package/rules/scope.md +22 -0
- package/rules/simple.md +14 -0
- package/rules/talk.md +13 -0
- package/rules/tests-first.md +17 -0
- package/rules/the-ask.md +19 -0
- package/skills/obk-arch/LICENSES.md +52 -0
- package/skills/obk-arch/NOTICE.md +242 -0
- package/skills/obk-arch/SKILL.md +598 -0
- package/skills/obk-bot-building/LICENSES.md +21 -0
- package/skills/obk-bot-building/NOTICE.md +113 -0
- package/skills/obk-bot-building/SKILL.md +312 -0
- package/skills/obk-debugging/LICENSES.md +67 -0
- package/skills/obk-debugging/NOTICE.md +115 -0
- package/skills/obk-debugging/SKILL.md +434 -0
- package/skills/obk-decision-memo/LICENSES.md +49 -0
- package/skills/obk-decision-memo/NOTICE.md +181 -0
- package/skills/obk-decision-memo/SKILL.md +241 -0
- package/skills/obk-finops/LICENSES.md +19 -0
- package/skills/obk-finops/NOTICE.md +77 -0
- package/skills/obk-finops/SKILL.md +148 -0
- package/skills/obk-fleet-review/LICENSES.md +52 -0
- package/skills/obk-fleet-review/NOTICE.md +104 -0
- package/skills/obk-fleet-review/SKILL.md +256 -0
- package/skills/obk-grilling/LICENSES.md +42 -0
- package/skills/obk-grilling/NOTICE.md +111 -0
- package/skills/obk-grilling/SKILL.md +221 -0
- package/skills/obk-grooming/LICENSES.md +52 -0
- package/skills/obk-grooming/NOTICE.md +111 -0
- package/skills/obk-grooming/SKILL.md +222 -0
- package/skills/obk-handoff/LICENSES.md +45 -0
- package/skills/obk-handoff/NOTICE.md +51 -0
- package/skills/obk-handoff/SKILL.md +119 -0
- package/skills/obk-personal-facilitation/LICENSES.md +53 -0
- package/skills/obk-personal-facilitation/NOTICE.md +62 -0
- package/skills/obk-personal-facilitation/SKILL.md +86 -0
- package/skills/obk-recall/LICENSES.md +42 -0
- package/skills/obk-recall/NOTICE.md +53 -0
- package/skills/obk-recall/SKILL.md +129 -0
- package/skills/obk-researching/LICENSES.md +67 -0
- package/skills/obk-researching/NOTICE.md +170 -0
- package/skills/obk-researching/SKILL.md +269 -0
- package/skills/obk-reviewing/LICENSES.md +66 -0
- package/skills/obk-reviewing/NOTICE.md +58 -0
- package/skills/obk-reviewing/SKILL.md +411 -0
- package/skills/obk-tdd/LICENSES.md +68 -0
- package/skills/obk-tdd/NOTICE.md +54 -0
- package/skills/obk-tdd/SKILL.md +547 -0
- package/skills/obk-tdd/mutation-tools.md +80 -0
- package/skills/obk-teach/LICENSES.md +42 -0
- package/skills/obk-teach/NOTICE.md +65 -0
- package/skills/obk-teach/SKILL.md +106 -0
- package/skills/obk-why/LICENSES.md +42 -0
- package/skills/obk-why/NOTICE.md +74 -0
- package/skills/obk-why/SKILL.md +180 -0
- package/skills/obk-writing/LICENSES.md +272 -0
- package/skills/obk-writing/NOTICE.md +161 -0
- package/skills/obk-writing/SKILL.md +254 -0
- package/src/book.js +268 -0
- package/src/bot.js +451 -0
- package/src/cli.js +1169 -0
- package/src/conversations.js +150 -0
- package/src/groom.js +159 -0
- package/src/health.js +309 -0
- package/src/hooks.js +257 -0
- package/src/init.js +314 -0
- package/src/launch.js +299 -0
- package/src/message.js +402 -0
- package/src/orca.js +362 -0
- package/src/pause.js +63 -0
- package/src/record.js +209 -0
- package/src/restart.js +193 -0
- package/src/retire.js +92 -0
- package/src/roster.js +71 -0
- package/src/rules.js +408 -0
- package/src/skills.js +630 -0
- package/src/sources.js +371 -0
- package/src/up.js +500 -0
- package/src/usage.js +401 -0
- package/bin/okb.js +0 -4
- package/src/index.js +0 -11
package/src/sources.js
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
// Skills from an online repo, at the version the user pinned (PRD 6.7, ADR 0014).
|
|
2
|
+
//
|
|
3
|
+
// A source is a git repository the user names in `<bots>/skills.yaml`. The kit
|
|
4
|
+
// clones it into `<bots>.skill-sources/<name>`, a sibling of the bots folder and
|
|
5
|
+
// never inside it, so the user's repo holds no copy of anybody else's work.
|
|
6
|
+
//
|
|
7
|
+
// The one thing this must never do is move a clone on its own. `fetch` gets
|
|
8
|
+
// what is missing and leaves what is there exactly where it is — even when the
|
|
9
|
+
// ref is a branch the origin has carried on past. `update` is the user asking,
|
|
10
|
+
// and it is the only thing that moves a clone. So the version a bot runs
|
|
11
|
+
// changes when they say so and at no other time.
|
|
12
|
+
//
|
|
13
|
+
// What they asked for is `ref` and what they got is `sha`, both in their own
|
|
14
|
+
// file, so the difference is theirs to see.
|
|
15
|
+
|
|
16
|
+
import { spawnSync } from 'node:child_process';
|
|
17
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import { parseDocument } from 'yaml';
|
|
20
|
+
|
|
21
|
+
import { changesExactly, requireBotsFolder, YAML_OUT } from './bot.js';
|
|
22
|
+
import { ownCli, shellWord } from './launch.js';
|
|
23
|
+
|
|
24
|
+
/** The user's file that lists where skills come from. */
|
|
25
|
+
const SKILLS_YAML = 'skills.yaml';
|
|
26
|
+
|
|
27
|
+
/** Where the clones go: beside the bots folder, named after it. */
|
|
28
|
+
export const sourcesDir = (bots) => `${bots}.skill-sources`;
|
|
29
|
+
|
|
30
|
+
/** Where one source's clone goes. */
|
|
31
|
+
export const cloneDir = (bots, name) => path.join(sourcesDir(bots), name);
|
|
32
|
+
|
|
33
|
+
/** A source's name, which is also the folder it is cloned into and the prefix a bot names it by. */
|
|
34
|
+
const NAME = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
35
|
+
|
|
36
|
+
/** The prefix issue #35 gave the kit's own shelf, and so not a name a source may take. */
|
|
37
|
+
const TAKEN = ['kit'];
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* What looks like something that runs. Not a scan and not a gate: the user
|
|
41
|
+
* takes the risk for a third-party skill (PRD 6.7), and what the kit owes them
|
|
42
|
+
* is knowing there is a risk to take.
|
|
43
|
+
*/
|
|
44
|
+
const RUNS = /\.(sh|bash|zsh|py|rb|pl|js|mjs|cjs|ts)$/i;
|
|
45
|
+
const RUNS_DIR = /^(scripts?|hooks?|bin)$/i;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Write one source into the user's own `skills.yaml`, and change nothing else
|
|
49
|
+
* in it. Returns the source as it was written.
|
|
50
|
+
*
|
|
51
|
+
* What this accepts is what `readSources` accepts, and for the same reason the
|
|
52
|
+
* bot commands check a name before writing it: an entry the next command
|
|
53
|
+
* refuses to read is worse than a refusal here, because the user finds it two
|
|
54
|
+
* commands later with nothing to say which one wrote it.
|
|
55
|
+
*
|
|
56
|
+
* It does not clone. Pinning a version and going to the network are two
|
|
57
|
+
* different decisions, and `skills fetch` is the one that goes.
|
|
58
|
+
*/
|
|
59
|
+
export function addSource(bots, { name, repo, ref, path: inside }) {
|
|
60
|
+
requireBotsFolder(bots);
|
|
61
|
+
|
|
62
|
+
if (!NAME.test(name)) {
|
|
63
|
+
throw new Error(`${name} cannot be a source's name: a name is lower-case letters, digits and single hyphens, such as someones-skills.`);
|
|
64
|
+
}
|
|
65
|
+
if (TAKEN.includes(name)) {
|
|
66
|
+
throw new Error(`${name} is the kit's own shelf, which a bot names kit:<name>, so a source cannot be called that. Call it something else.`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const file = path.join(bots, SKILLS_YAML);
|
|
70
|
+
// Read through the same reader every other command uses, so a file this one
|
|
71
|
+
// would add to is a file they can all still read afterwards.
|
|
72
|
+
if (readSources(file).some((source) => source.name === name)) {
|
|
73
|
+
throw new Error(`${file} already lists a source called ${name}, and source add never writes over one.`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const source = { name, repo, ref, ...(inside === undefined ? {} : { path: inside }) };
|
|
77
|
+
const was = existsSync(file) ? readFileSync(file, 'utf8') : '';
|
|
78
|
+
const doc = parseDocument(was);
|
|
79
|
+
const listed = doc.get('sources', true);
|
|
80
|
+
if (listed?.items === undefined) {
|
|
81
|
+
// No list to add to: an empty `sources:`, or no sources key at all.
|
|
82
|
+
doc.set('sources', [source]);
|
|
83
|
+
} else {
|
|
84
|
+
// A list written inline becomes a block list from here on, which is the one
|
|
85
|
+
// shape a source with four fields in it reads well in.
|
|
86
|
+
listed.flow = false;
|
|
87
|
+
doc.addIn(['sources'], source);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const text = doc.toString(YAML_OUT);
|
|
91
|
+
if (!changesExactly(was, text, (had) => ({ ...had, sources: [...(had.sources ?? []), source] }))) {
|
|
92
|
+
throw new Error(`${file} cannot have a source added to it without changing something else in it, so nothing was written. Add ${name} to its sources list by hand.`);
|
|
93
|
+
}
|
|
94
|
+
writeFileSync(file, text);
|
|
95
|
+
return source;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Fetch what is missing, or move what the user asks to move.
|
|
100
|
+
* Returns one entry per source: `{ name, state, ref, sha }`, or `{ name, trouble }`.
|
|
101
|
+
*
|
|
102
|
+
* `moving` is the whole difference between the two commands. Without it a
|
|
103
|
+
* source already cloned is reported as it stands and nothing reaches the
|
|
104
|
+
* network; with it the ref is resolved again and the clone follows.
|
|
105
|
+
*/
|
|
106
|
+
export function fetchSources(bots, { source: onlySource, moving = false } = {}) {
|
|
107
|
+
const file = path.join(bots, SKILLS_YAML);
|
|
108
|
+
const sources = readSources(file);
|
|
109
|
+
|
|
110
|
+
if (onlySource !== undefined && !sources.some((source) => source.name === onlySource)) {
|
|
111
|
+
throw new Error(`${file} lists no source called ${onlySource}. The sources it lists are: ${sources.map((one) => one.name).join(', ') || 'none'}.`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const wanted = onlySource === undefined ? sources : sources.filter((source) => source.name === onlySource);
|
|
115
|
+
const report = [];
|
|
116
|
+
const got = new Map();
|
|
117
|
+
|
|
118
|
+
for (const source of wanted) {
|
|
119
|
+
try {
|
|
120
|
+
const at = bring(bots, source, moving);
|
|
121
|
+
got.set(source.name, at.sha);
|
|
122
|
+
report.push({ name: source.name, state: at.state, ref: source.ref, sha: at.sha, ...(at.runs ? { runs: true } : {}) });
|
|
123
|
+
} catch (error) {
|
|
124
|
+
report.push({ name: source.name, trouble: error.message });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
writeShas(file, got);
|
|
129
|
+
return report;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Put one source's clone where it belongs, and say what that took.
|
|
134
|
+
*
|
|
135
|
+
* Three things this has to get right, because each is a way of running a
|
|
136
|
+
* version nobody chose:
|
|
137
|
+
*
|
|
138
|
+
* - A directory under the sources folder means a source at the version that was
|
|
139
|
+
* asked for, and nothing else. The clone and the checkout happen out of the
|
|
140
|
+
* way and are moved into place once both have worked, so a checkout that
|
|
141
|
+
* fails leaves nothing for the next run to mistake for a source.
|
|
142
|
+
* - A clone that has gone missing comes back at the commit the file records,
|
|
143
|
+
* not at whatever its ref points to today. Restoring is not moving.
|
|
144
|
+
* - A clone is this source's only while it came from the repository the file
|
|
145
|
+
* names. Point a source somewhere else and the old clone is not an answer.
|
|
146
|
+
*/
|
|
147
|
+
function bring(bots, source, moving) {
|
|
148
|
+
const dir = cloneDir(bots, source.name);
|
|
149
|
+
|
|
150
|
+
if (existsSync(dir)) {
|
|
151
|
+
const wrong = wrongClone(bots, source);
|
|
152
|
+
if (wrong !== undefined) {
|
|
153
|
+
if (!moving) throw new Error(wrong);
|
|
154
|
+
rmSync(dir, { recursive: true, force: true });
|
|
155
|
+
return clone(bots, source, source.ref, false);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!moving) return { state: 'there', sha: headOf(dir), runs: carriesScripts(skillsIn(bots, source)) };
|
|
159
|
+
|
|
160
|
+
const was = headOf(dir);
|
|
161
|
+
git(['fetch', '--quiet', '--tags', '--force', 'origin'], dir, `${source.name}: ${source.repo} could not be fetched from`);
|
|
162
|
+
checkout(dir, source, source.ref, false);
|
|
163
|
+
const now = headOf(dir);
|
|
164
|
+
return { state: now === was ? 'there' : 'moved', sha: now, runs: carriesScripts(skillsIn(bots, source)) };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Nothing there. A fetch puts back the version the file records; only the
|
|
168
|
+
// asking takes what the ref names now.
|
|
169
|
+
const recorded = typeof source.sha === 'string' && source.sha.trim() !== '';
|
|
170
|
+
return clone(bots, source, !moving && recorded ? source.sha.trim() : source.ref, !moving && recorded);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Clone a source and check it out out of the way, and put it in place only once
|
|
175
|
+
* both have worked. `<name>.fetching` is where it happens; a run that cannot
|
|
176
|
+
* finish leaves that behind and not a source.
|
|
177
|
+
*/
|
|
178
|
+
function clone(bots, source, rev, recorded) {
|
|
179
|
+
mkdirSync(sourcesDir(bots), { recursive: true });
|
|
180
|
+
const dir = cloneDir(bots, source.name);
|
|
181
|
+
const fetching = `${dir}.fetching`;
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
rmSync(fetching, { recursive: true, force: true });
|
|
185
|
+
// `--`: a repo value from skills.yaml, which may be someone else's, is a
|
|
186
|
+
// repository and never one of git's own options, whatever it begins with.
|
|
187
|
+
git(['clone', '--quiet', '--', source.repo, fetching], undefined, `${source.name}: ${source.repo} could not be cloned`);
|
|
188
|
+
checkout(fetching, source, rev, recorded);
|
|
189
|
+
renameSync(fetching, dir);
|
|
190
|
+
} finally {
|
|
191
|
+
rmSync(fetching, { recursive: true, force: true });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return { state: 'cloned', sha: headOf(dir), runs: carriesScripts(skillsIn(bots, source)) };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Why the clone beside the bots folder is not this source's, or undefined when
|
|
199
|
+
* it is. A clone is a source's only while it came from the repository the file
|
|
200
|
+
* names; fetch and build both ask here, so neither takes what the other refuses.
|
|
201
|
+
*/
|
|
202
|
+
export function wrongClone(bots, source) {
|
|
203
|
+
const from = originOf(cloneDir(bots, source.name));
|
|
204
|
+
if (from === source.repo) return undefined;
|
|
205
|
+
return `${source.name}: the clone beside your bots folder came from ${from}, and skills.yaml now names ${source.repo}. Run ${shellWord(ownCli())} skills update --source ${source.name} to take it from there instead.`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Where a clone came from, as git itself has it written down. */
|
|
209
|
+
function originOf(dir) {
|
|
210
|
+
const found = spawnSync('git', ['config', '--get', 'remote.origin.url'], { cwd: dir, encoding: 'utf8' });
|
|
211
|
+
return found.status === 0 ? found.stdout.trim() : undefined;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Check a clone out at `rev`. A branch, a tag and a sha all go the same way in,
|
|
216
|
+
* which is why the kit does not ask which kind it was given.
|
|
217
|
+
*
|
|
218
|
+
* The origin's idea of a branch is the one that counts. A fetch moves
|
|
219
|
+
* `origin/main` and leaves the local `main` where it was, so checking out the
|
|
220
|
+
* bare name would quietly keep the old commit and report a move that never
|
|
221
|
+
* happened. A tag or a sha has no `origin/` form and falls through to itself.
|
|
222
|
+
*/
|
|
223
|
+
function checkout(dir, source, rev, recorded) {
|
|
224
|
+
const at = [`refs/remotes/origin/${rev}`, rev]
|
|
225
|
+
.map((candidate) => resolve(dir, candidate))
|
|
226
|
+
.find((sha) => sha !== undefined);
|
|
227
|
+
|
|
228
|
+
if (at === undefined) {
|
|
229
|
+
throw new Error(recorded
|
|
230
|
+
? `${source.name}: ${source.repo} no longer has the commit skills.yaml records for it (${rev.slice(0, 7)}), so the version you pinned cannot be put back. Run ${shellWord(ownCli())} skills update --source ${source.name} to take what ${source.ref} names now, or point it at a version the repo still has.`
|
|
231
|
+
: `${source.name}: ${source.repo} has nothing called ${rev}`);
|
|
232
|
+
}
|
|
233
|
+
git(['-c', 'advice.detachedHead=false', 'checkout', '--quiet', at], dir, `${source.name}: could not go to ${rev}`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** The commit a name stands for in this clone, or nothing when it stands for none. */
|
|
237
|
+
function resolve(dir, rev) {
|
|
238
|
+
const found = spawnSync('git', ['rev-parse', '--verify', '--quiet', `${rev}^{commit}`], { cwd: dir, encoding: 'utf8' });
|
|
239
|
+
return found.status === 0 && found.stdout.trim() !== '' ? found.stdout.trim() : undefined;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** What a clone is at now. */
|
|
243
|
+
const headOf = (dir) => git(['rev-parse', 'HEAD'], dir, 'could not read what the clone is at');
|
|
244
|
+
|
|
245
|
+
/** Run git, and turn a refusal into a sentence the user can act on. */
|
|
246
|
+
function git(args, cwd, trouble) {
|
|
247
|
+
const result = spawnSync('git', args, { cwd, encoding: 'utf8' });
|
|
248
|
+
if (result.error) throw new Error(`could not run git: ${result.error.message}`);
|
|
249
|
+
if (result.status !== 0) throw new Error(`${trouble}: ${(result.stderr || '').trim().split('\n').at(-1) ?? ''}`);
|
|
250
|
+
return result.stdout.trim();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Where a source's skills sit inside its clone: its root, or the subfolder it names. */
|
|
254
|
+
export const skillsIn = (bots, source) => path.join(cloneDir(bots, source.name), source.path ?? '');
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Whether anything under a source's skills runs. One answer for the whole
|
|
258
|
+
* source, because one line is what the user is owed, however much is in there.
|
|
259
|
+
*/
|
|
260
|
+
function carriesScripts(dir) {
|
|
261
|
+
let found = false;
|
|
262
|
+
const look = (at, depth) => {
|
|
263
|
+
if (found || depth > 4) return;
|
|
264
|
+
for (const entry of readdirSync(at, { withFileTypes: true })) {
|
|
265
|
+
if (found) return;
|
|
266
|
+
if (entry.isDirectory()) {
|
|
267
|
+
if (RUNS_DIR.test(entry.name)) found = true;
|
|
268
|
+
else look(path.join(at, entry.name), depth + 1);
|
|
269
|
+
} else if (RUNS.test(entry.name)) {
|
|
270
|
+
found = true;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
// The skills themselves, and not the repository around them: every clone
|
|
277
|
+
// carries a `.git/hooks`, and a question about what a source runs that
|
|
278
|
+
// answers yes for all of them answers nothing.
|
|
279
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
280
|
+
if (!entry.isDirectory() || !existsSync(path.join(dir, entry.name, 'SKILL.md'))) continue;
|
|
281
|
+
look(path.join(dir, entry.name), 1);
|
|
282
|
+
}
|
|
283
|
+
} catch {
|
|
284
|
+
// A source whose skills cannot be walked is not a source to make claims
|
|
285
|
+
// about. Saying nothing is the honest answer.
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
return found;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* The sources the user listed, checked far enough that the kit knows what it is
|
|
293
|
+
* being asked for. A file it cannot read is refused whole rather than half
|
|
294
|
+
* followed, and nothing is written to it.
|
|
295
|
+
*/
|
|
296
|
+
export function readSources(file) {
|
|
297
|
+
if (!existsSync(file)) return [];
|
|
298
|
+
|
|
299
|
+
const doc = parseDocument(readFileSync(file, 'utf8'));
|
|
300
|
+
if (doc.errors.length > 0) {
|
|
301
|
+
throw new Error(`${file} is not readable as YAML: ${doc.errors[0].message.split('\n')[0]}`);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const listed = doc.toJS()?.sources;
|
|
305
|
+
if (listed === undefined || listed === null) return [];
|
|
306
|
+
if (!Array.isArray(listed)) {
|
|
307
|
+
throw new Error(`${file} has a sources entry that is not a list of sources.`);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const names = new Set();
|
|
311
|
+
return listed.map((source) => {
|
|
312
|
+
if (source === null || typeof source !== 'object' || Array.isArray(source)) {
|
|
313
|
+
throw new Error(`${file} has a source that is not a mapping: a source has a name, a repo and a ref.`);
|
|
314
|
+
}
|
|
315
|
+
const { name, repo, ref } = source;
|
|
316
|
+
if (typeof name !== 'string' || !NAME.test(name)) {
|
|
317
|
+
throw new Error(`${file} has a source called ${JSON.stringify(name)}, and a source is named in lower-case letters, digits and single hyphens.`);
|
|
318
|
+
}
|
|
319
|
+
if (TAKEN.includes(name)) {
|
|
320
|
+
throw new Error(`${file} has a source called ${name}, and that name is the kit's own shelf: a bot names one of the kit's skills kit:<name>. Call it something else.`);
|
|
321
|
+
}
|
|
322
|
+
if (names.has(name)) throw new Error(`${file} lists ${name} more than once.`);
|
|
323
|
+
names.add(name);
|
|
324
|
+
|
|
325
|
+
if (typeof repo !== 'string' || repo.trim() === '') {
|
|
326
|
+
throw new Error(`${file}: the source ${name} has no repo to clone.`);
|
|
327
|
+
}
|
|
328
|
+
if (typeof ref !== 'string' || ref.trim() === '') {
|
|
329
|
+
throw new Error(`${file}: the source ${name} has no ref, so there is no version to pin it at.`);
|
|
330
|
+
}
|
|
331
|
+
if (source.path !== undefined && typeof source.path !== 'string') {
|
|
332
|
+
throw new Error(`${file}: the source ${name} has a path that is not a subfolder inside the repo.`);
|
|
333
|
+
}
|
|
334
|
+
return source;
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Write each source's resolved sha back into the user's own file, and change
|
|
340
|
+
* nothing else in it. The file is theirs: the comments, the order and every
|
|
341
|
+
* value they wrote come back as they wrote them, and a file the kit cannot
|
|
342
|
+
* edit that narrowly is left alone and said so.
|
|
343
|
+
*/
|
|
344
|
+
function writeShas(file, got) {
|
|
345
|
+
if (got.size === 0) return;
|
|
346
|
+
|
|
347
|
+
const source = readFileSync(file, 'utf8');
|
|
348
|
+
const doc = parseDocument(source);
|
|
349
|
+
const listed = doc.get('sources', true);
|
|
350
|
+
let changed = false;
|
|
351
|
+
|
|
352
|
+
for (const entry of listed?.items ?? []) {
|
|
353
|
+
const name = entry?.get?.('name');
|
|
354
|
+
if (!got.has(name) || entry.get('sha') === got.get(name)) continue;
|
|
355
|
+
entry.set('sha', got.get(name));
|
|
356
|
+
changed = true;
|
|
357
|
+
}
|
|
358
|
+
if (!changed) return;
|
|
359
|
+
|
|
360
|
+
const text = doc.toString(YAML_OUT);
|
|
361
|
+
if (!changesExactly(source, text, (was) => ({
|
|
362
|
+
...was,
|
|
363
|
+
sources: was.sources.map((one) => (got.has(one.name) ? { ...one, sha: got.get(one.name) } : one)),
|
|
364
|
+
}))) {
|
|
365
|
+
throw new Error(`${file} cannot have the versions written into it without changing something else in it, so nothing was written. Add a sha to each source by hand, or let the kit rewrite the file.`);
|
|
366
|
+
}
|
|
367
|
+
writeFileSync(file, text);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** Whether a bots folder has a clone of this source yet. */
|
|
371
|
+
export const isCloned = (bots, name) => statSync(cloneDir(bots, name), { throwIfNoEntry: false })?.isDirectory() === true;
|