agent-kitchen 0.4.0 → 0.6.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 +15 -4
- package/dist/cli.js +10 -5
- package/dist/commands/doctor.d.ts +1 -8
- package/dist/commands/doctor.js +23 -6
- package/dist/commands/sync.d.ts +22 -2
- package/dist/commands/sync.js +220 -29
- package/dist/decisions.d.ts +54 -0
- package/dist/decisions.js +91 -0
- package/dist/git-finish.d.ts +53 -0
- package/dist/git-finish.js +253 -0
- package/dist/menu.d.ts +33 -0
- package/dist/menu.js +46 -0
- package/dist/piece.d.ts +6 -5
- package/dist/piece.js +6 -5
- package/dist/pieces/issue-flow.d.ts +12 -4
- package/dist/pieces/issue-flow.js +205 -26
- package/dist/pieces/skills-manager.d.ts +31 -0
- package/dist/pieces/skills-manager.js +127 -0
- package/dist/repo.d.ts +20 -0
- package/dist/repo.js +51 -0
- package/dist/skills-files.d.ts +5 -3
- package/dist/skills-files.js +15 -9
- package/dist/tools.d.ts +33 -2
- package/dist/tools.js +52 -5
- package/package.json +1 -1
|
@@ -1,20 +1,42 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file The issue flow (`@ramon-sg/issue-flow`): its CLI, hooks and config, and its skills.
|
|
3
|
-
* The repo's version is the one in package.json.
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* The repo's version is the one in package.json. From 0.3.0, installing and updating run its
|
|
4
|
+
* `apply` at the latest version, and reading its `--json` tells what it kept as it was; at
|
|
5
|
+
* the repo's version, `apply` also repairs hooks or a `$schema` its doctor finds behind (a
|
|
6
|
+
* version raised by hand or by Dependabot). Before 0.3.0, installing runs `init` and
|
|
7
|
+
* updating runs `upgrade`. After updating or repairing, and after installing with `apply`,
|
|
8
|
+
* the flow's skills move to the same tag, because they call the CLI. If only the skills are
|
|
9
|
+
* off, they move alone. From 0.2.6 it also offers `labels sync`, after the install or
|
|
10
|
+
* update, when GitHub lacks some of the labels the flow expects. From 0.3.0, whose `apply`
|
|
11
|
+
* keeps the repo's github-rest, it also raises an exact github-rest below the floor of the
|
|
12
|
+
* range that version of the flow declares on npm, as the flow itself reads it.
|
|
13
|
+
* `apply` and `init` run with `--yes`: the person answered agent-kitchen's questions before
|
|
14
|
+
* the steps.
|
|
7
15
|
*/
|
|
8
|
-
import { FLOW_CATALOG, FLOW_PACKAGE } from "../piece.js";
|
|
9
|
-
import { dependencyVersion, readJson } from "../repo.js";
|
|
16
|
+
import { FLOW_CATALOG, FLOW_CONFIG, FLOW_PACKAGE } from "../piece.js";
|
|
17
|
+
import { dependencyVersion, isObject, readJson, setDependency } from "../repo.js";
|
|
10
18
|
import { catalogSkills, manifestCatalogs, pinCatalog, updateSkills } from "../skills-files.js";
|
|
11
|
-
import { captureNpx, compareVersions, isRelease, npmLatest, runNpx } from "../tools.js";
|
|
19
|
+
import { captureNpx, compareVersions, isRelease, npmDependencies, npmLatest, runNpx } from "../tools.js";
|
|
12
20
|
/** The piece's name, as `--skip` takes it. */
|
|
13
21
|
export const NAME = "issue-flow";
|
|
14
22
|
/** How the catalog's ref is named when it is off. */
|
|
15
23
|
const SKILLS_CATALOG_LABEL = "el catálogo";
|
|
16
24
|
/** The first version whose `doctor` prints `--json`. */
|
|
17
25
|
const JSON_DOCTOR = "0.2.0";
|
|
26
|
+
/** The first version with `labels sync`. */
|
|
27
|
+
const LABELS_SINCE = "0.2.6";
|
|
28
|
+
/** The first version whose `apply` installs, updates and repairs, instead of `init` and `upgrade`. */
|
|
29
|
+
const APPLY_SINCE = "0.3.0";
|
|
30
|
+
/** The client of GitHub's REST API that the flow's skills call. */
|
|
31
|
+
const GITHUB_REST = "@ramon-sg/github-rest";
|
|
32
|
+
/**
|
|
33
|
+
* Whether a version installs and updates with `apply`.
|
|
34
|
+
* @param version - The flow's version.
|
|
35
|
+
* @returns True from 0.3.0 on.
|
|
36
|
+
*/
|
|
37
|
+
function usesApply(version) {
|
|
38
|
+
return compareVersions(version, APPLY_SINCE) >= 0;
|
|
39
|
+
}
|
|
18
40
|
/**
|
|
19
41
|
* The issue flow's version in the repo, as package.json has it: an exact release when the
|
|
20
42
|
* flow set it up, or anything else (a range, a `file:` link) someone wrote by hand.
|
|
@@ -80,18 +102,168 @@ async function runFlow(context, version, args) {
|
|
|
80
102
|
context.fail(`issue-flow ${args[0] ?? ""} falló; corre 'npx ${FLOW_PACKAGE}@${version} ${args.join(" ")}'.`, run.output);
|
|
81
103
|
return false;
|
|
82
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* Runs the flow's `apply` at a version and reads its `--json`: what it kept as it was goes as
|
|
107
|
+
* a note. Output that is not its JSON counts as a failure, since it does not tell what it wrote.
|
|
108
|
+
* @param context - Where it runs.
|
|
109
|
+
* @param version - The version, 0.3.0 or later.
|
|
110
|
+
* @returns Whether it worked.
|
|
111
|
+
*/
|
|
112
|
+
async function applyFlow(context, version) {
|
|
113
|
+
const retry = `npx ${FLOW_PACKAGE}@${version} apply --yes`;
|
|
114
|
+
const result = await captureNpx(context.root, [`${FLOW_PACKAGE}@${version}`, "apply", "--yes", "--json"]);
|
|
115
|
+
const output = `${result?.out ?? ""}${result?.err ?? ""}`;
|
|
116
|
+
if (result === null || result.status !== 0) {
|
|
117
|
+
context.fail(`issue-flow apply falló; corre '${retry}'.`, output);
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
let report;
|
|
121
|
+
try {
|
|
122
|
+
report = JSON.parse(result.out);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
report = null;
|
|
126
|
+
}
|
|
127
|
+
if (!isObject(report) || !Array.isArray(report.kept)) {
|
|
128
|
+
context.fail(`issue-flow apply no dijo qué hizo; corre '${retry}'.`, output);
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
for (const line of report.kept)
|
|
132
|
+
context.note(String(line));
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* The flow's own `doctor --json` at a version.
|
|
137
|
+
* @param root - The repo.
|
|
138
|
+
* @param version - The version, 0.2.0 or later.
|
|
139
|
+
* @param lookup - True to give up after the lookups' time limit, as the status does.
|
|
140
|
+
* @returns Its checks, or null if it could not run or its output is not a list.
|
|
141
|
+
*/
|
|
142
|
+
async function flowDoctor(root, version, lookup = false) {
|
|
143
|
+
const result = await captureNpx(root, [`${FLOW_PACKAGE}@${version}`, "doctor", "--json"], lookup);
|
|
144
|
+
try {
|
|
145
|
+
const found = JSON.parse(result?.out ?? "");
|
|
146
|
+
return Array.isArray(found) ? found : null;
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Whether the flow's doctor finds what `apply` repairs: a hook missing or behind, or the
|
|
154
|
+
* config's `$schema` behind. A config that is invalid fails the same check but `apply` cannot
|
|
155
|
+
* fix it, so the `$schema` is told by its detail; and a hook newer than the repo's version
|
|
156
|
+
* makes `apply` refuse, so it does not count.
|
|
157
|
+
* @param checks - The doctor's checks.
|
|
158
|
+
* @param version - The repo's version.
|
|
159
|
+
* @returns True if `apply` would repair them.
|
|
160
|
+
*/
|
|
161
|
+
function needsRepair(checks, version) {
|
|
162
|
+
const hookBehind = (detail) => {
|
|
163
|
+
const found = /^at (\d+\.\d+\.\d+)/.exec(detail)?.[1];
|
|
164
|
+
return found === undefined || compareVersions(found, version) < 0;
|
|
165
|
+
};
|
|
166
|
+
return checks.some((check) => check.ok === false && (String(check.name).endsWith(" hook") ? hookBehind(String(check.detail)) : check.name === FLOW_CONFIG && String(check.detail).includes("$schema")));
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* The labels the flow expects that GitHub lacks, as `labels sync --dry-run --json` of a
|
|
170
|
+
* version tells (a label that only differs in color or description does not count).
|
|
171
|
+
* @param root - The repo.
|
|
172
|
+
* @param version - The flow's version.
|
|
173
|
+
* @returns Their names, none for a version without `labels sync`, or null if it could not tell.
|
|
174
|
+
* @example await missingLabels(root, "0.2.6") // ["spike"]
|
|
175
|
+
*/
|
|
176
|
+
async function missingLabels(root, version) {
|
|
177
|
+
if (compareVersions(version, LABELS_SINCE) < 0)
|
|
178
|
+
return [];
|
|
179
|
+
const result = await captureNpx(root, [`${FLOW_PACKAGE}@${version}`, "labels", "sync", "--dry-run", "--json"], true);
|
|
180
|
+
let found;
|
|
181
|
+
try {
|
|
182
|
+
found = JSON.parse(result?.out ?? "");
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
if (!isObject(found) || !Array.isArray(found.labels))
|
|
188
|
+
return null;
|
|
189
|
+
return found.labels.filter((label) => isObject(label) && label.status === "to-create").map((label) => String(label.name));
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* The step that makes in GitHub the labels the flow lacks, with its own command. It only
|
|
193
|
+
* runs if the repo has that version of the flow by then: not when its install or update
|
|
194
|
+
* was left out or failed.
|
|
195
|
+
* @param version - The flow's version.
|
|
196
|
+
* @param missing - The labels.
|
|
197
|
+
* @returns The step.
|
|
198
|
+
*/
|
|
199
|
+
function labelsStep(version, missing) {
|
|
200
|
+
return { piece: NAME, checked: true, downloads: [`${FLOW_PACKAGE}@${version}`], label: `Crear en GitHub los labels que faltan: ${missing.join(", ")}`, run: async (context) => {
|
|
201
|
+
if (flowVersion(context.root) !== version) {
|
|
202
|
+
context.note(`no se crearon: el repo no tiene issue-flow ${version}`);
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
return runFlow(context, version, ["labels", "sync"]);
|
|
206
|
+
} };
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* The lowest github-rest a version of the flow works with: the floor of the range it declares
|
|
210
|
+
* on npm (`^0.3.0` gives 0.3.0), as the flow reads it from its own package.json.
|
|
211
|
+
* @param version - The flow's version.
|
|
212
|
+
* @returns The version, or null before 0.3.0 (its `upgrade` raises github-rest) or if npm cannot tell.
|
|
213
|
+
* @example await restMinimum("0.3.0") // "0.3.0"
|
|
214
|
+
*/
|
|
215
|
+
async function restMinimum(version) {
|
|
216
|
+
if (!usesApply(version))
|
|
217
|
+
return null;
|
|
218
|
+
const range = (await npmDependencies(`${FLOW_PACKAGE}@${version}`))?.[GITHUB_REST]?.replace(/^[\^~]/, "");
|
|
219
|
+
return range !== undefined && isRelease(range) ? range : null;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* The step that raises github-rest to a version of the flow's minimum, if package.json has an
|
|
223
|
+
* exact version below it. A missing one gets none: `apply` adds it at the minimum. A range is
|
|
224
|
+
* the repo's own choice. At run time it reads package.json again, and raises nothing unless
|
|
225
|
+
* the repo has that version of the flow by then.
|
|
226
|
+
* @param root - The repo.
|
|
227
|
+
* @param version - The flow's version the repo ends at.
|
|
228
|
+
* @param minimum - The lowest github-rest it works with.
|
|
229
|
+
* @returns The step, or null.
|
|
230
|
+
*/
|
|
231
|
+
function restStep(root, version, minimum) {
|
|
232
|
+
const below = (rest) => rest !== null && isRelease(rest) && compareVersions(rest, minimum) < 0;
|
|
233
|
+
const current = dependencyVersion(readJson(root, "package.json"), GITHUB_REST);
|
|
234
|
+
if (!below(current))
|
|
235
|
+
return null;
|
|
236
|
+
return { piece: NAME, checked: true, downloads: [], label: `Subir github-rest ${current} → ${minimum}, el mínimo de issue-flow ${version}`, run: async (context) => {
|
|
237
|
+
if (flowVersion(context.root) !== version)
|
|
238
|
+
context.note(`no se subió: el repo no tiene issue-flow ${version}`);
|
|
239
|
+
else if (!below(dependencyVersion(readJson(context.root, "package.json"), GITHUB_REST)))
|
|
240
|
+
context.note("package.json ya tenía una versión que sirve");
|
|
241
|
+
else {
|
|
242
|
+
setDependency(context.root, GITHUB_REST, minimum);
|
|
243
|
+
context.note(`${GITHUB_REST} ${minimum} en package.json`);
|
|
244
|
+
}
|
|
245
|
+
return true;
|
|
246
|
+
} };
|
|
247
|
+
}
|
|
83
248
|
/**
|
|
84
249
|
* The step the repo needs, if any.
|
|
85
250
|
* @param installed - The repo's version, or null.
|
|
86
251
|
* @param latest - The latest on npm, or null.
|
|
87
252
|
* @param root - The repo.
|
|
253
|
+
* @param repair - Whether the flow's doctor finds what `apply` repairs, at the repo's version.
|
|
88
254
|
* @returns The step, or null.
|
|
89
255
|
*/
|
|
90
|
-
function stepFor(installed, latest, root) {
|
|
91
|
-
if (installed === null && latest !== null)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
256
|
+
function stepFor(installed, latest, root, repair) {
|
|
257
|
+
if (installed === null && latest !== null) {
|
|
258
|
+
const run = usesApply(latest) ? async (context) => (await applyFlow(context, latest)) && (await moveSkills(context, latest)) : (context) => runFlow(context, latest, ["init", "--yes"]);
|
|
259
|
+
return { piece: NAME, checked: true, downloads: [`${FLOW_PACKAGE}@${latest}`], label: `Instalar issue-flow ${latest}: config del flujo, hooks y scripts`, run };
|
|
260
|
+
}
|
|
261
|
+
if (installed !== null && latest !== null && compareVersions(latest, installed) > 0) {
|
|
262
|
+
const update = (context) => (usesApply(latest) ? applyFlow(context, latest) : runFlow(context, latest, ["upgrade"]));
|
|
263
|
+
return { piece: NAME, checked: true, downloads: [`${FLOW_PACKAGE}@${latest}`], label: `Actualizar issue-flow ${installed} → ${latest}, con sus hooks y sus skills`, run: async (context) => (await update(context)) && (await moveSkills(context, latest)) };
|
|
264
|
+
}
|
|
265
|
+
if (installed !== null && repair)
|
|
266
|
+
return { piece: NAME, checked: true, downloads: [`${FLOW_PACKAGE}@${installed}`], label: `Reparar issue-flow ${installed}: hooks y $schema`, run: async (context) => (await applyFlow(context, installed)) && (skillsOff(context.root, installed).length === 0 || (await moveSkills(context, installed))) };
|
|
95
267
|
if (installed !== null && skillsOff(root, installed).length > 0)
|
|
96
268
|
return { piece: NAME, checked: true, downloads: [], label: `Poner las skills del flujo en v${installed}, la versión de issue-flow`, run: (context) => moveSkills(context, installed) };
|
|
97
269
|
return null;
|
|
@@ -109,10 +281,15 @@ export async function issueFlowStatus(root) {
|
|
|
109
281
|
return { name: NAME, summary, installed: true, state: `package.json tiene ${spec}, no una versión exacta; no se toca`, steps: [] };
|
|
110
282
|
const installed = flowVersion(root);
|
|
111
283
|
const latest = await npmLatest(FLOW_PACKAGE);
|
|
112
|
-
const
|
|
113
|
-
const
|
|
114
|
-
const
|
|
115
|
-
|
|
284
|
+
const behind = installed !== null && latest !== null && compareVersions(latest, installed) > 0;
|
|
285
|
+
const target = installed === null || behind ? latest : installed;
|
|
286
|
+
const [missing, checks, minimum] = await Promise.all([target === null ? [] : missingLabels(root, target), installed !== null && !behind && usesApply(installed) ? flowDoctor(root, installed, true) : null, target === null ? null : restMinimum(target)]);
|
|
287
|
+
const step = stepFor(installed, latest, root, installed !== null && checks !== null && needsRepair(checks, installed));
|
|
288
|
+
const rest = target === null || minimum === null ? null : restStep(root, target, minimum);
|
|
289
|
+
const known = `${latest === null ? " (npm no responde)" : ""}${missing === null ? " (no se pudieron leer los labels de GitHub)" : ""}`;
|
|
290
|
+
const state = installed === null ? `no instalado${known}` : behind ? `${installed}, hay ${latest}${known}` : `${installed}, al día${known}`;
|
|
291
|
+
const steps = [...(step === null ? [] : [step]), ...(rest === null ? [] : [rest]), ...(target !== null && missing !== null && missing.length > 0 ? [labelsStep(target, missing)] : [])];
|
|
292
|
+
return { name: NAME, summary, installed: installed !== null, state, steps };
|
|
116
293
|
}
|
|
117
294
|
/**
|
|
118
295
|
* The flow's own doctor at the repo's version, and whether its skills are at that tag.
|
|
@@ -129,15 +306,9 @@ export async function issueFlowChecks(root) {
|
|
|
129
306
|
return [{ name: NAME, status: "info", detail: `package.json tiene ${spec}, no una versión exacta` }];
|
|
130
307
|
if (compareVersions(installed, JSON_DOCTOR) < 0)
|
|
131
308
|
return [{ name: NAME, status: "info", detail: `${installed} no tiene doctor --json: corre npx agent-kitchen apply para actualizarlo` }];
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
found = JSON.parse(result?.out ?? "");
|
|
136
|
-
}
|
|
137
|
-
catch {
|
|
138
|
-
found = null;
|
|
139
|
-
}
|
|
140
|
-
const own = Array.isArray(found)
|
|
309
|
+
const labelsRead = missingLabels(root, installed);
|
|
310
|
+
const found = await flowDoctor(root, installed);
|
|
311
|
+
const own = found !== null
|
|
141
312
|
? found.map((check) => ({ name: `${NAME}: ${String(check.name)}`, status: check.ok === true ? "ok" : "fail", detail: String(check.detail) }))
|
|
142
313
|
: [{ name: NAME, status: "fail", detail: `no se pudo correr su doctor; corre 'npx ${FLOW_PACKAGE}@${installed} doctor'` }];
|
|
143
314
|
const off = skillsOff(root, installed);
|
|
@@ -147,5 +318,13 @@ export async function issueFlowChecks(root) {
|
|
|
147
318
|
: off.length === 0
|
|
148
319
|
? { name: `${NAME}: skills`, status: "ok", detail: `${count} en v${installed}` }
|
|
149
320
|
: { name: `${NAME}: skills`, status: "fail", detail: `${off.join(", ")}; issue-flow está en ${installed}: corre npx agent-kitchen apply` };
|
|
150
|
-
|
|
321
|
+
const missing = await labelsRead;
|
|
322
|
+
const labels = compareVersions(installed, LABELS_SINCE) < 0
|
|
323
|
+
? []
|
|
324
|
+
: missing === null
|
|
325
|
+
? [{ name: `${NAME}: labels`, status: "info", detail: "no se pudieron leer de GitHub" }]
|
|
326
|
+
: missing.length > 0
|
|
327
|
+
? [{ name: `${NAME}: labels`, status: "info", detail: `faltan ${missing.join(", ")} en GitHub; corre npx agent-kitchen apply` }]
|
|
328
|
+
: [{ name: `${NAME}: labels`, status: "ok", detail: "GitHub tiene los que espera el flujo" }];
|
|
329
|
+
return [...own, skills, ...labels];
|
|
151
330
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The version of `skills-manager` in the repo's package.json. agent-kitchen runs it to
|
|
3
|
+
* install and move the skills, so agent-kitchen is the one that knows the oldest version it
|
|
4
|
+
* works with: it adds it when missing and raises an exact version below it, never lowers
|
|
5
|
+
* one. A range or `workspace:` is the repo's own choice: it is left alone, with a warning if
|
|
6
|
+
* the version it runs is too old. Only in a repo that uses skills and has package.json, or
|
|
7
|
+
* gets one in this run from the flow's install: then the step makes package.json first, and
|
|
8
|
+
* the flow's `apply`, which never adds skills-manager, adds its own packages to it.
|
|
9
|
+
*/
|
|
10
|
+
import { type Check, type PieceStatus } from "../piece.js";
|
|
11
|
+
/** The piece's name, as `--skip` takes it. */
|
|
12
|
+
export declare const NAME = "skills-manager";
|
|
13
|
+
/** The oldest `skills-manager` agent-kitchen works with: the first with `--replace`. */
|
|
14
|
+
export declare const MIN_VERSION = "1.2.0";
|
|
15
|
+
/**
|
|
16
|
+
* What the repo has of skills-manager, and the step that raises it, if any. The step reads
|
|
17
|
+
* package.json again when it runs.
|
|
18
|
+
* @param root - The repo.
|
|
19
|
+
* @param withSkills - Whether this run may install skills.
|
|
20
|
+
* @param flowComing - Whether this run installs the flow, which makes package.json if missing.
|
|
21
|
+
* @returns The piece's status, or null where the piece does not apply.
|
|
22
|
+
* @example skillsManagerStatus(root, true, false)?.state // "1.1.0, bajo 1.2.0"
|
|
23
|
+
*/
|
|
24
|
+
export declare function skillsManagerStatus(root: string, withSkills: boolean, flowComing: boolean): PieceStatus | null;
|
|
25
|
+
/**
|
|
26
|
+
* Whether the version of skills-manager the repo runs is the one agent-kitchen needs.
|
|
27
|
+
* @param root - The repo.
|
|
28
|
+
* @returns The checks; none where the piece does not apply.
|
|
29
|
+
* @example skillsManagerChecks(root)[0]?.status // "ok"
|
|
30
|
+
*/
|
|
31
|
+
export declare function skillsManagerChecks(root: string): Check[];
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The version of `skills-manager` in the repo's package.json. agent-kitchen runs it to
|
|
3
|
+
* install and move the skills, so agent-kitchen is the one that knows the oldest version it
|
|
4
|
+
* works with: it adds it when missing and raises an exact version below it, never lowers
|
|
5
|
+
* one. A range or `workspace:` is the repo's own choice: it is left alone, with a warning if
|
|
6
|
+
* the version it runs is too old. Only in a repo that uses skills and has package.json, or
|
|
7
|
+
* gets one in this run from the flow's install: then the step makes package.json first, and
|
|
8
|
+
* the flow's `apply`, which never adds skills-manager, adds its own packages to it.
|
|
9
|
+
*/
|
|
10
|
+
import { SKILLS_MANAGER, SKILLS_MANIFEST } from "../piece.js";
|
|
11
|
+
import { dependencyVersion, installedVersion, readJson, setDependency, writeJson } from "../repo.js";
|
|
12
|
+
import { compareVersions, isRelease } from "../tools.js";
|
|
13
|
+
/** The piece's name, as `--skip` takes it. */
|
|
14
|
+
export const NAME = "skills-manager";
|
|
15
|
+
/** The oldest `skills-manager` agent-kitchen works with: the first with `--replace`. */
|
|
16
|
+
export const MIN_VERSION = "1.2.0";
|
|
17
|
+
/**
|
|
18
|
+
* What package.json has of skills-manager and, for a range, the version that runs: the one
|
|
19
|
+
* in node_modules, or the first x.y.z of the range.
|
|
20
|
+
* @param root - The repo.
|
|
21
|
+
* @returns What it has.
|
|
22
|
+
*/
|
|
23
|
+
function pinned(root) {
|
|
24
|
+
const spec = dependencyVersion(readJson(root, "package.json"), SKILLS_MANAGER);
|
|
25
|
+
if (spec === null)
|
|
26
|
+
return { kind: "missing" };
|
|
27
|
+
if (isRelease(spec))
|
|
28
|
+
return { kind: "exact", version: spec };
|
|
29
|
+
const floor = spec.startsWith("workspace:") ? null : (/\d+\.\d+\.\d+/.exec(spec)?.[0] ?? null);
|
|
30
|
+
return { kind: "other", spec, runs: installedVersion(root, SKILLS_MANAGER) ?? floor };
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Whether a version is older than the one agent-kitchen needs.
|
|
34
|
+
* @param version - The version.
|
|
35
|
+
* @returns True if it is below the minimum.
|
|
36
|
+
*/
|
|
37
|
+
function below(version) {
|
|
38
|
+
return compareVersions(version, MIN_VERSION) < 0;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Whether the piece applies: the repo uses skills, or this run may install them, and it has
|
|
42
|
+
* package.json or the flow's install is about to make one.
|
|
43
|
+
* @param root - The repo.
|
|
44
|
+
* @param withSkills - Whether this run may install skills (`skills` is not left out).
|
|
45
|
+
* @param flowComing - Whether this run installs the flow.
|
|
46
|
+
* @returns True if it applies.
|
|
47
|
+
*/
|
|
48
|
+
function applies(root, withSkills, flowComing) {
|
|
49
|
+
return (flowComing || readJson(root, "package.json") !== null) && (withSkills || readJson(root, SKILLS_MANIFEST) !== null);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* What the repo has of skills-manager, and the step that raises it, if any. The step reads
|
|
53
|
+
* package.json again when it runs.
|
|
54
|
+
* @param root - The repo.
|
|
55
|
+
* @param withSkills - Whether this run may install skills.
|
|
56
|
+
* @param flowComing - Whether this run installs the flow, which makes package.json if missing.
|
|
57
|
+
* @returns The piece's status, or null where the piece does not apply.
|
|
58
|
+
* @example skillsManagerStatus(root, true, false)?.state // "1.1.0, bajo 1.2.0"
|
|
59
|
+
*/
|
|
60
|
+
export function skillsManagerStatus(root, withSkills, flowComing) {
|
|
61
|
+
if (!applies(root, withSkills, flowComing))
|
|
62
|
+
return null;
|
|
63
|
+
const summary = "el CLI de las skills, que corre agent-kitchen";
|
|
64
|
+
if (readJson(root, "package.json") === null) {
|
|
65
|
+
const make = {
|
|
66
|
+
piece: NAME,
|
|
67
|
+
checked: true,
|
|
68
|
+
downloads: [`${SKILLS_MANAGER}@${MIN_VERSION}`],
|
|
69
|
+
label: `Crear package.json con skills-manager ${MIN_VERSION}`,
|
|
70
|
+
run: async (context) => {
|
|
71
|
+
if (readJson(context.root, "package.json") === null)
|
|
72
|
+
writeJson(context.root, "package.json", { private: true, devDependencies: { [SKILLS_MANAGER]: MIN_VERSION } });
|
|
73
|
+
else if (pinned(context.root).kind === "missing")
|
|
74
|
+
setDependency(context.root, SKILLS_MANAGER, MIN_VERSION);
|
|
75
|
+
context.note(`${SKILLS_MANAGER} ${MIN_VERSION} en package.json`);
|
|
76
|
+
return true;
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
return { name: NAME, summary, installed: false, state: "no hay package.json", steps: [make] };
|
|
80
|
+
}
|
|
81
|
+
const now = pinned(root);
|
|
82
|
+
if (now.kind === "other") {
|
|
83
|
+
const old = now.runs !== null && below(now.runs) ? `; corre ${now.runs}, bajo ${MIN_VERSION}` : "";
|
|
84
|
+
return { name: NAME, summary, installed: true, state: `package.json tiene ${now.spec}, no una versión exacta; no se toca${old}`, steps: [] };
|
|
85
|
+
}
|
|
86
|
+
if (now.kind === "exact" && !below(now.version))
|
|
87
|
+
return { name: NAME, summary, installed: true, state: `${now.version}, al día`, steps: [] };
|
|
88
|
+
const label = now.kind === "missing" ? `Agregar skills-manager ${MIN_VERSION} a package.json` : `Subir skills-manager ${now.version} → ${MIN_VERSION}, el mínimo de agent-kitchen`;
|
|
89
|
+
const state = now.kind === "missing" ? "no está en package.json" : `${now.version}, bajo ${MIN_VERSION}`;
|
|
90
|
+
const step = {
|
|
91
|
+
piece: NAME,
|
|
92
|
+
checked: true,
|
|
93
|
+
downloads: [`${SKILLS_MANAGER}@${MIN_VERSION}`],
|
|
94
|
+
label,
|
|
95
|
+
run: async (context) => {
|
|
96
|
+
const current = pinned(context.root);
|
|
97
|
+
if (current.kind === "missing" || (current.kind === "exact" && below(current.version))) {
|
|
98
|
+
setDependency(context.root, SKILLS_MANAGER, MIN_VERSION);
|
|
99
|
+
context.note(`${SKILLS_MANAGER} ${MIN_VERSION} en package.json`);
|
|
100
|
+
}
|
|
101
|
+
else
|
|
102
|
+
context.note("package.json ya tenía una versión que sirve");
|
|
103
|
+
return true;
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
return { name: NAME, summary, installed: now.kind !== "missing", state, steps: [step] };
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Whether the version of skills-manager the repo runs is the one agent-kitchen needs.
|
|
110
|
+
* @param root - The repo.
|
|
111
|
+
* @returns The checks; none where the piece does not apply.
|
|
112
|
+
* @example skillsManagerChecks(root)[0]?.status // "ok"
|
|
113
|
+
*/
|
|
114
|
+
export function skillsManagerChecks(root) {
|
|
115
|
+
if (!applies(root, false, false))
|
|
116
|
+
return [];
|
|
117
|
+
const now = pinned(root);
|
|
118
|
+
if (now.kind === "missing")
|
|
119
|
+
return [{ name: NAME, status: "info", detail: `no está en package.json; corre npx agent-kitchen apply para fijar ${MIN_VERSION}` }];
|
|
120
|
+
if (now.kind === "exact")
|
|
121
|
+
return below(now.version)
|
|
122
|
+
? [{ name: NAME, status: "fail", detail: `${now.version}, bajo ${MIN_VERSION}, el mínimo de agent-kitchen: corre npx agent-kitchen apply` }]
|
|
123
|
+
: [{ name: NAME, status: "ok", detail: `${now.version} (mínimo ${MIN_VERSION})` }];
|
|
124
|
+
if (now.runs !== null && below(now.runs))
|
|
125
|
+
return [{ name: NAME, status: "fail", detail: `${now.spec} corre ${now.runs}, bajo ${MIN_VERSION}, el mínimo de agent-kitchen: sube el rango` }];
|
|
126
|
+
return [{ name: NAME, status: "ok", detail: `${now.spec}${now.runs === null ? "" : `, corre ${now.runs}`}` }];
|
|
127
|
+
}
|
package/dist/repo.d.ts
CHANGED
|
@@ -46,3 +46,23 @@ export declare function dependencyVersion(pkg: Record<string, unknown> | null, n
|
|
|
46
46
|
* @example packageManager(root) // "pnpm"
|
|
47
47
|
*/
|
|
48
48
|
export declare function packageManager(root: string): string;
|
|
49
|
+
/**
|
|
50
|
+
* The version of a package installed in the repo's `node_modules`.
|
|
51
|
+
* @param root - The repo.
|
|
52
|
+
* @param name - The package.
|
|
53
|
+
* @returns Its version, or null if it is not installed.
|
|
54
|
+
* @example installedVersion(root, "@ramon-sg/skills-manager") // "1.3.0"
|
|
55
|
+
*/
|
|
56
|
+
export declare function installedVersion(root: string, name: string): string | null;
|
|
57
|
+
/**
|
|
58
|
+
* Sets the version of a dependency in package.json, changing nothing else: the file keeps
|
|
59
|
+
* its indentation, line endings and key order. It changes the entry `dependencyVersion`
|
|
60
|
+
* reads (`devDependencies` first, then `dependencies`); a new one goes into
|
|
61
|
+
* `devDependencies`, in alphabetical order, as npm adds it.
|
|
62
|
+
* @param root - The repo.
|
|
63
|
+
* @param name - The package.
|
|
64
|
+
* @param version - The version.
|
|
65
|
+
* @throws KitError if package.json is missing or not a JSON object.
|
|
66
|
+
* @example setDependency(root, "@ramon-sg/skills-manager", "1.2.0")
|
|
67
|
+
*/
|
|
68
|
+
export declare function setDependency(root: string, name: string, version: string): void;
|
package/dist/repo.js
CHANGED
|
@@ -115,3 +115,54 @@ export function packageManager(root) {
|
|
|
115
115
|
return field;
|
|
116
116
|
return LOCKFILES.find(([file]) => existsSync(join(root, file)))?.[1] ?? "npm";
|
|
117
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* The version of a package installed in the repo's `node_modules`.
|
|
120
|
+
* @param root - The repo.
|
|
121
|
+
* @param name - The package.
|
|
122
|
+
* @returns Its version, or null if it is not installed.
|
|
123
|
+
* @example installedVersion(root, "@ramon-sg/skills-manager") // "1.3.0"
|
|
124
|
+
*/
|
|
125
|
+
export function installedVersion(root, name) {
|
|
126
|
+
const file = join(root, "node_modules", name, "package.json");
|
|
127
|
+
if (!existsSync(file))
|
|
128
|
+
return null;
|
|
129
|
+
try {
|
|
130
|
+
const pkg = JSON.parse(readFileSync(file, "utf8"));
|
|
131
|
+
return isObject(pkg) && typeof pkg.version === "string" ? pkg.version : null;
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Sets the version of a dependency in package.json, changing nothing else: the file keeps
|
|
139
|
+
* its indentation, line endings and key order. It changes the entry `dependencyVersion`
|
|
140
|
+
* reads (`devDependencies` first, then `dependencies`); a new one goes into
|
|
141
|
+
* `devDependencies`, in alphabetical order, as npm adds it.
|
|
142
|
+
* @param root - The repo.
|
|
143
|
+
* @param name - The package.
|
|
144
|
+
* @param version - The version.
|
|
145
|
+
* @throws KitError if package.json is missing or not a JSON object.
|
|
146
|
+
* @example setDependency(root, "@ramon-sg/skills-manager", "1.2.0")
|
|
147
|
+
*/
|
|
148
|
+
export function setDependency(root, name, version) {
|
|
149
|
+
const pkg = readJson(root, "package.json");
|
|
150
|
+
if (pkg === null)
|
|
151
|
+
throw new KitError("no hay package.json.");
|
|
152
|
+
const text = readFileSync(join(root, "package.json"), "utf8");
|
|
153
|
+
const indent = /^([ \t]+)"/m.exec(text)?.[1] ?? " ";
|
|
154
|
+
const eol = text.includes("\r\n") ? "\r\n" : "\n";
|
|
155
|
+
const [deps, dev] = [pkg.dependencies, pkg.devDependencies];
|
|
156
|
+
if (isObject(dev) && typeof dev[name] === "string")
|
|
157
|
+
dev[name] = version;
|
|
158
|
+
else if (isObject(deps) && typeof deps[name] === "string")
|
|
159
|
+
deps[name] = version;
|
|
160
|
+
else {
|
|
161
|
+
const entries = Object.entries(isObject(dev) ? dev : {});
|
|
162
|
+
const at = entries.findIndex(([key]) => key > name);
|
|
163
|
+
entries.splice(at === -1 ? entries.length : at, 0, [name, version]);
|
|
164
|
+
pkg.devDependencies = Object.fromEntries(entries);
|
|
165
|
+
}
|
|
166
|
+
const json = JSON.stringify(pkg, null, indent).replace(/\n/g, eol);
|
|
167
|
+
writeFileSync(join(root, "package.json"), /\r?\n$/.test(text) || text === "" ? `${json}${eol}` : json);
|
|
168
|
+
}
|
package/dist/skills-files.d.ts
CHANGED
|
@@ -66,10 +66,12 @@ export declare function manifestCatalogs(root: string): {
|
|
|
66
66
|
*/
|
|
67
67
|
export declare function pinCatalog(root: string, catalog: string, tag: string): boolean;
|
|
68
68
|
/**
|
|
69
|
-
* The `skills-manager` to run
|
|
70
|
-
*
|
|
69
|
+
* The `skills-manager` to run, so the lock is written by the same version every time: the
|
|
70
|
+
* exact version package.json pins (the one its install leaves, even before it runs), else
|
|
71
|
+
* the one in node_modules, else the range package.json has (npx cannot take `workspace:`),
|
|
72
|
+
* else the latest 1.x.
|
|
71
73
|
* @param root - The repo.
|
|
72
74
|
* @returns The npx spec.
|
|
73
|
-
* @example skillsManagerSpec(root) // "@ramon-sg/skills-manager@1.
|
|
75
|
+
* @example skillsManagerSpec(root) // "@ramon-sg/skills-manager@1.2.0"
|
|
74
76
|
*/
|
|
75
77
|
export declare function skillsManagerSpec(root: string): string;
|
package/dist/skills-files.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file What agent-kitchen reads of the files of `skills-manager`: which skills of a catalog
|
|
3
3
|
* the lock has and at which tag (the lock is where the manifest's `paths.lock` says), the
|
|
4
|
-
* catalog's source in the manifest, which `skills-manager` to run (the one the repo pins
|
|
5
|
-
* or the latest 1.x), the repo's own skills (folders the lock does not know),
|
|
6
|
-
* skills one by one, because `skills-manager update` takes a single name.
|
|
4
|
+
* catalog's source in the manifest, which `skills-manager` to run (the one the repo pins or
|
|
5
|
+
* has installed, or the latest 1.x), the repo's own skills (folders the lock does not know),
|
|
6
|
+
* and updating skills one by one, because `skills-manager update` takes a single name.
|
|
7
7
|
*/
|
|
8
8
|
import { existsSync, readdirSync } from "node:fs";
|
|
9
9
|
import { join } from "node:path";
|
|
10
|
-
import { dependencyVersion, isObject, readJson, writeJson } from "./repo.js";
|
|
10
|
+
import { dependencyVersion, installedVersion, isObject, readJson, writeJson } from "./repo.js";
|
|
11
11
|
import { SKILLS_LOCK, SKILLS_MANAGER, SKILLS_MANIFEST } from "./piece.js";
|
|
12
|
-
import { runNpx } from "./tools.js";
|
|
12
|
+
import { isRelease, runNpx } from "./tools.js";
|
|
13
13
|
/** Where `skills-manager` puts the skills when the manifest does not say. */
|
|
14
14
|
const SKILLS_DIR = ".agents/skills";
|
|
15
15
|
/**
|
|
@@ -105,12 +105,18 @@ export function pinCatalog(root, catalog, tag) {
|
|
|
105
105
|
return true;
|
|
106
106
|
}
|
|
107
107
|
/**
|
|
108
|
-
* The `skills-manager` to run
|
|
109
|
-
*
|
|
108
|
+
* The `skills-manager` to run, so the lock is written by the same version every time: the
|
|
109
|
+
* exact version package.json pins (the one its install leaves, even before it runs), else
|
|
110
|
+
* the one in node_modules, else the range package.json has (npx cannot take `workspace:`),
|
|
111
|
+
* else the latest 1.x.
|
|
110
112
|
* @param root - The repo.
|
|
111
113
|
* @returns The npx spec.
|
|
112
|
-
* @example skillsManagerSpec(root) // "@ramon-sg/skills-manager@1.
|
|
114
|
+
* @example skillsManagerSpec(root) // "@ramon-sg/skills-manager@1.2.0"
|
|
113
115
|
*/
|
|
114
116
|
export function skillsManagerSpec(root) {
|
|
115
|
-
|
|
117
|
+
const pinned = dependencyVersion(readJson(root, "package.json"), SKILLS_MANAGER);
|
|
118
|
+
if (pinned !== null && isRelease(pinned))
|
|
119
|
+
return `${SKILLS_MANAGER}@${pinned}`;
|
|
120
|
+
const range = pinned === null || pinned.startsWith("workspace:") ? null : pinned;
|
|
121
|
+
return `${SKILLS_MANAGER}@${installedVersion(root, SKILLS_MANAGER) ?? range ?? "1"}`;
|
|
116
122
|
}
|
package/dist/tools.d.ts
CHANGED
|
@@ -26,6 +26,13 @@ export declare function isRelease(version: string): boolean;
|
|
|
26
26
|
* @example await npmLatest("@ramon-sg/claude-cloud") // "1.0.0"
|
|
27
27
|
*/
|
|
28
28
|
export declare function npmLatest(name: string): Promise<string | null>;
|
|
29
|
+
/**
|
|
30
|
+
* The dependencies a version of a package declares on npm.
|
|
31
|
+
* @param spec - The package and its version, as `name@x.y.z`.
|
|
32
|
+
* @returns Each dependency's range, or null if npm cannot tell.
|
|
33
|
+
* @example await npmDependencies("@ramon-sg/issue-flow@0.3.0") // { "@ramon-sg/github-rest": "^0.3.0" }
|
|
34
|
+
*/
|
|
35
|
+
export declare function npmDependencies(spec: string): Promise<Record<string, string> | null>;
|
|
29
36
|
/**
|
|
30
37
|
* The highest `vX.Y.Z` tag of a GitHub repo.
|
|
31
38
|
* @param repo - The repo, as owner/name.
|
|
@@ -64,14 +71,38 @@ export declare function runNpx(root: string, args: string[]): Promise<ToolRun>;
|
|
|
64
71
|
*/
|
|
65
72
|
export declare function failedOutput(output: string): string;
|
|
66
73
|
/**
|
|
67
|
-
* Runs `npx --yes <args>` in the repo and returns what it printed
|
|
74
|
+
* Runs `npx --yes <args>` in the repo and returns what it printed.
|
|
68
75
|
* @param root - The repo.
|
|
69
76
|
* @param args - The package and its arguments.
|
|
70
77
|
* @param lookup - True to give up after the lookups' time limit, for a read that only informs.
|
|
71
|
-
* @returns Its stdout and exit code, or null if npx could not start.
|
|
78
|
+
* @returns Its stdout, stderr and exit code, or null if npx could not start.
|
|
72
79
|
* @example await captureNpx(root, ["@ramon-sg/issue-flow@0.2.0", "doctor", "--json"])
|
|
73
80
|
*/
|
|
74
81
|
export declare function captureNpx(root: string, args: string[], lookup?: boolean): Promise<{
|
|
75
82
|
out: string;
|
|
83
|
+
err: string;
|
|
76
84
|
status: number | null;
|
|
77
85
|
} | null>;
|
|
86
|
+
/**
|
|
87
|
+
* Runs the repo's package manager's install: quietly, or with its output on the terminal
|
|
88
|
+
* with `--verbose`.
|
|
89
|
+
* @param root - The repo.
|
|
90
|
+
* @param manager - `npm`, `pnpm`, `bun` or `yarn`.
|
|
91
|
+
* @returns Whether it worked and, if it ran quietly, what it printed.
|
|
92
|
+
* @example await runInstall(root, "pnpm")
|
|
93
|
+
*/
|
|
94
|
+
export declare function runInstall(root: string, manager: string): Promise<ToolRun>;
|
|
95
|
+
/**
|
|
96
|
+
* Runs `npx --yes <args>` in the repo without some variables of the environment, and returns
|
|
97
|
+
* what it printed.
|
|
98
|
+
* @param root - The repo.
|
|
99
|
+
* @param args - The package and its arguments.
|
|
100
|
+
* @param unset - The variables left out.
|
|
101
|
+
* @returns Its stdout, stderr and exit code.
|
|
102
|
+
* @example await captureNpxWithout(root, ["@ramon-sg/github-rest@0.3.0", "pr", "create"], ["GH_REPO"])
|
|
103
|
+
*/
|
|
104
|
+
export declare function captureNpxWithout(root: string, args: string[], unset: string[]): Promise<{
|
|
105
|
+
out: string;
|
|
106
|
+
err: string;
|
|
107
|
+
status: number | null;
|
|
108
|
+
}>;
|