agent-kitchen 0.5.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.
@@ -1,16 +1,22 @@
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. Installing runs its `init` at the latest
4
- * version; updating runs its `upgrade` at the new one and then moves its skills to the same
5
- * tag, because the skills call the CLI. If only the skills are off, it moves them alone.
6
- * From 0.2.6 it also offers `labels sync`, after the install or update, when GitHub lacks
7
- * some of the labels the flow expects.
8
- * `init` runs with `--yes`: the person answered agent-kitchen's questions before the steps.
9
- */
10
- import { FLOW_CATALOG, FLOW_PACKAGE } from "../piece.js";
11
- import { dependencyVersion, isObject, readJson } from "../repo.js";
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.
15
+ */
16
+ import { FLOW_CATALOG, FLOW_CONFIG, FLOW_PACKAGE } from "../piece.js";
17
+ import { dependencyVersion, isObject, readJson, setDependency } from "../repo.js";
12
18
  import { catalogSkills, manifestCatalogs, pinCatalog, updateSkills } from "../skills-files.js";
13
- import { captureNpx, compareVersions, isRelease, npmLatest, runNpx } from "../tools.js";
19
+ import { captureNpx, compareVersions, isRelease, npmDependencies, npmLatest, runNpx } from "../tools.js";
14
20
  /** The piece's name, as `--skip` takes it. */
15
21
  export const NAME = "issue-flow";
16
22
  /** How the catalog's ref is named when it is off. */
@@ -19,6 +25,18 @@ const SKILLS_CATALOG_LABEL = "el catálogo";
19
25
  const JSON_DOCTOR = "0.2.0";
20
26
  /** The first version with `labels sync`. */
21
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
+ }
22
40
  /**
23
41
  * The issue flow's version in the repo, as package.json has it: an exact release when the
24
42
  * flow set it up, or anything else (a range, a `file:` link) someone wrote by hand.
@@ -84,6 +102,69 @@ async function runFlow(context, version, args) {
84
102
  context.fail(`issue-flow ${args[0] ?? ""} falló; corre 'npx ${FLOW_PACKAGE}@${version} ${args.join(" ")}'.`, run.output);
85
103
  return false;
86
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
+ }
87
168
  /**
88
169
  * The labels the flow expects that GitHub lacks, as `labels sync --dry-run --json` of a
89
170
  * version tells (a label that only differs in color or description does not count).
@@ -124,18 +205,65 @@ function labelsStep(version, missing) {
124
205
  return runFlow(context, version, ["labels", "sync"]);
125
206
  } };
126
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
+ }
127
248
  /**
128
249
  * The step the repo needs, if any.
129
250
  * @param installed - The repo's version, or null.
130
251
  * @param latest - The latest on npm, or null.
131
252
  * @param root - The repo.
253
+ * @param repair - Whether the flow's doctor finds what `apply` repairs, at the repo's version.
132
254
  * @returns The step, or null.
133
255
  */
134
- function stepFor(installed, latest, root) {
135
- if (installed === null && latest !== null)
136
- return { piece: NAME, checked: true, downloads: [`${FLOW_PACKAGE}@${latest}`], label: `Instalar issue-flow ${latest}: config del flujo, hooks y scripts`, run: (context) => runFlow(context, latest, ["init", "--yes"]) };
137
- if (installed !== null && latest !== null && compareVersions(latest, installed) > 0)
138
- 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 runFlow(context, latest, ["upgrade"])) && (await moveSkills(context, latest)) };
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))) };
139
267
  if (installed !== null && skillsOff(root, installed).length > 0)
140
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) };
141
269
  return null;
@@ -153,13 +281,14 @@ export async function issueFlowStatus(root) {
153
281
  return { name: NAME, summary, installed: true, state: `package.json tiene ${spec}, no una versión exacta; no se toca`, steps: [] };
154
282
  const installed = flowVersion(root);
155
283
  const latest = await npmLatest(FLOW_PACKAGE);
156
- const step = stepFor(installed, latest, root);
157
284
  const behind = installed !== null && latest !== null && compareVersions(latest, installed) > 0;
158
285
  const target = installed === null || behind ? latest : installed;
159
- const missing = target === null ? [] : await missingLabels(root, target);
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);
160
289
  const known = `${latest === null ? " (npm no responde)" : ""}${missing === null ? " (no se pudieron leer los labels de GitHub)" : ""}`;
161
290
  const state = installed === null ? `no instalado${known}` : behind ? `${installed}, hay ${latest}${known}` : `${installed}, al día${known}`;
162
- const steps = [...(step === null ? [] : [step]), ...(target !== null && missing !== null && missing.length > 0 ? [labelsStep(target, missing)] : [])];
291
+ const steps = [...(step === null ? [] : [step]), ...(rest === null ? [] : [rest]), ...(target !== null && missing !== null && missing.length > 0 ? [labelsStep(target, missing)] : [])];
163
292
  return { name: NAME, summary, installed: installed !== null, state, steps };
164
293
  }
165
294
  /**
@@ -178,15 +307,8 @@ export async function issueFlowChecks(root) {
178
307
  if (compareVersions(installed, JSON_DOCTOR) < 0)
179
308
  return [{ name: NAME, status: "info", detail: `${installed} no tiene doctor --json: corre npx agent-kitchen apply para actualizarlo` }];
180
309
  const labelsRead = missingLabels(root, installed);
181
- const result = await captureNpx(root, [`${FLOW_PACKAGE}@${installed}`, "doctor", "--json"]);
182
- let found;
183
- try {
184
- found = JSON.parse(result?.out ?? "");
185
- }
186
- catch {
187
- found = null;
188
- }
189
- const own = Array.isArray(found)
310
+ const found = await flowDoctor(root, installed);
311
+ const own = found !== null
190
312
  ? found.map((check) => ({ name: `${NAME}: ${String(check.name)}`, status: check.ok === true ? "ok" : "fail", detail: String(check.detail) }))
191
313
  : [{ name: NAME, status: "fail", detail: `no se pudo correr su doctor; corre 'npx ${FLOW_PACKAGE}@${installed} doctor'` }];
192
314
  const off = skillsOff(root, installed);
@@ -3,7 +3,9 @@
3
3
  * install and move the skills, so agent-kitchen is the one that knows the oldest version it
4
4
  * works with: it adds it when missing and raises an exact version below it, never lowers
5
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 with package.json that uses skills.
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.
7
9
  */
8
10
  import { type Check, type PieceStatus } from "../piece.js";
9
11
  /** The piece's name, as `--skip` takes it. */
@@ -15,10 +17,11 @@ export declare const MIN_VERSION = "1.2.0";
15
17
  * package.json again when it runs.
16
18
  * @param root - The repo.
17
19
  * @param withSkills - Whether this run may install skills.
20
+ * @param flowComing - Whether this run installs the flow, which makes package.json if missing.
18
21
  * @returns The piece's status, or null where the piece does not apply.
19
- * @example skillsManagerStatus(root, true)?.state // "1.1.0, bajo 1.2.0"
22
+ * @example skillsManagerStatus(root, true, false)?.state // "1.1.0, bajo 1.2.0"
20
23
  */
21
- export declare function skillsManagerStatus(root: string, withSkills: boolean): PieceStatus | null;
24
+ export declare function skillsManagerStatus(root: string, withSkills: boolean, flowComing: boolean): PieceStatus | null;
22
25
  /**
23
26
  * Whether the version of skills-manager the repo runs is the one agent-kitchen needs.
24
27
  * @param root - The repo.
@@ -3,10 +3,12 @@
3
3
  * install and move the skills, so agent-kitchen is the one that knows the oldest version it
4
4
  * works with: it adds it when missing and raises an exact version below it, never lowers
5
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 with package.json that uses skills.
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.
7
9
  */
8
10
  import { SKILLS_MANAGER, SKILLS_MANIFEST } from "../piece.js";
9
- import { dependencyVersion, installedVersion, readJson, setDependency } from "../repo.js";
11
+ import { dependencyVersion, installedVersion, readJson, setDependency, writeJson } from "../repo.js";
10
12
  import { compareVersions, isRelease } from "../tools.js";
11
13
  /** The piece's name, as `--skip` takes it. */
12
14
  export const NAME = "skills-manager";
@@ -36,27 +38,46 @@ function below(version) {
36
38
  return compareVersions(version, MIN_VERSION) < 0;
37
39
  }
38
40
  /**
39
- * Whether the piece applies: the repo has package.json and uses skills, or this run may
40
- * install them.
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.
41
43
  * @param root - The repo.
42
44
  * @param withSkills - Whether this run may install skills (`skills` is not left out).
45
+ * @param flowComing - Whether this run installs the flow.
43
46
  * @returns True if it applies.
44
47
  */
45
- function applies(root, withSkills) {
46
- return readJson(root, "package.json") !== null && (withSkills || readJson(root, SKILLS_MANIFEST) !== null);
48
+ function applies(root, withSkills, flowComing) {
49
+ return (flowComing || readJson(root, "package.json") !== null) && (withSkills || readJson(root, SKILLS_MANIFEST) !== null);
47
50
  }
48
51
  /**
49
52
  * What the repo has of skills-manager, and the step that raises it, if any. The step reads
50
53
  * package.json again when it runs.
51
54
  * @param root - The repo.
52
55
  * @param withSkills - Whether this run may install skills.
56
+ * @param flowComing - Whether this run installs the flow, which makes package.json if missing.
53
57
  * @returns The piece's status, or null where the piece does not apply.
54
- * @example skillsManagerStatus(root, true)?.state // "1.1.0, bajo 1.2.0"
58
+ * @example skillsManagerStatus(root, true, false)?.state // "1.1.0, bajo 1.2.0"
55
59
  */
56
- export function skillsManagerStatus(root, withSkills) {
57
- if (!applies(root, withSkills))
60
+ export function skillsManagerStatus(root, withSkills, flowComing) {
61
+ if (!applies(root, withSkills, flowComing))
58
62
  return null;
59
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
+ }
60
81
  const now = pinned(root);
61
82
  if (now.kind === "other") {
62
83
  const old = now.runs !== null && below(now.runs) ? `; corre ${now.runs}, bajo ${MIN_VERSION}` : "";
@@ -91,7 +112,7 @@ export function skillsManagerStatus(root, withSkills) {
91
112
  * @example skillsManagerChecks(root)[0]?.status // "ok"
92
113
  */
93
114
  export function skillsManagerChecks(root) {
94
- if (!applies(root, false))
115
+ if (!applies(root, false, false))
95
116
  return [];
96
117
  const now = pinned(root);
97
118
  if (now.kind === "missing")
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 on stdout.
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
+ }>;
package/dist/tools.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @file How agent-kitchen reaches the other tools and their releases: `npx` to run them (always
3
3
  * by their scoped name or GitHub spec, because the unscoped names on npm belong to other
4
- * people), `npm view` for the latest version on npm, and `git ls-remote` for the latest
4
+ * people), `npm view` for the latest version on npm and a version's dependencies, and `git ls-remote` for the latest
5
5
  * `vX.Y.Z` tag on GitHub. Everything is asynchronous so the lookups run at the same time,
6
6
  * and each lookup runs once per process. A lookup that fails, or takes over 20 seconds,
7
7
  * gives null: the piece is shown, not offered. git never asks for credentials here.
@@ -67,7 +67,10 @@ export function isRelease(version) {
67
67
  */
68
68
  function run(program, args, options = {}) {
69
69
  return new Promise((resolve) => {
70
- const child = spawn(program, args, { cwd: options.cwd, env: { ...process.env, ...options.env }, stdio: options.inherit === true ? "inherit" : ["ignore", "pipe", "pipe"] });
70
+ const env = { ...process.env, ...options.env };
71
+ for (const name of options.unset ?? [])
72
+ delete env[name];
73
+ const child = spawn(program, args, { cwd: options.cwd, env, stdio: options.inherit === true ? "inherit" : ["ignore", "pipe", "pipe"] });
71
74
  let out = "";
72
75
  let err = "";
73
76
  child.stdout?.on("data", (chunk) => (out += chunk.toString()));
@@ -108,6 +111,26 @@ export function npmLatest(name) {
108
111
  return isRelease(version) ? version : null;
109
112
  });
110
113
  }
114
+ /**
115
+ * The dependencies a version of a package declares on npm.
116
+ * @param spec - The package and its version, as `name@x.y.z`.
117
+ * @returns Each dependency's range, or null if npm cannot tell.
118
+ * @example await npmDependencies("@ramon-sg/issue-flow@0.3.0") // { "@ramon-sg/github-rest": "^0.3.0" }
119
+ */
120
+ export function npmDependencies(spec) {
121
+ return once(`npm-deps:${spec}`, async () => {
122
+ const result = await run("npm", ["view", spec, "dependencies", "--json", "--fetch-retries=0"], { timeout: LOOKUP_TIMEOUT_MS });
123
+ if (result.status !== 0)
124
+ return null;
125
+ try {
126
+ const found = JSON.parse(result.out);
127
+ return typeof found === "object" && found !== null && !Array.isArray(found) ? found : null;
128
+ }
129
+ catch {
130
+ return null;
131
+ }
132
+ });
133
+ }
111
134
  /**
112
135
  * The highest `vX.Y.Z` tag of a GitHub repo.
113
136
  * @param repo - The repo, as owner/name.
@@ -162,14 +185,38 @@ export function failedOutput(output) {
162
185
  return lines.slice(-FAILED_LINES).map((line) => ` ${line}\n`).join("");
163
186
  }
164
187
  /**
165
- * Runs `npx --yes <args>` in the repo and returns what it printed on stdout.
188
+ * Runs `npx --yes <args>` in the repo and returns what it printed.
166
189
  * @param root - The repo.
167
190
  * @param args - The package and its arguments.
168
191
  * @param lookup - True to give up after the lookups' time limit, for a read that only informs.
169
- * @returns Its stdout and exit code, or null if npx could not start.
192
+ * @returns Its stdout, stderr and exit code, or null if npx could not start.
170
193
  * @example await captureNpx(root, ["@ramon-sg/issue-flow@0.2.0", "doctor", "--json"])
171
194
  */
172
195
  export async function captureNpx(root, args, lookup = false) {
173
196
  const result = await run("npx", ["--yes", ...args], { cwd: root, ...(lookup ? { timeout: LOOKUP_TIMEOUT_MS } : {}) });
174
- return result.status === null && result.out === "" ? null : { out: result.out, status: result.status };
197
+ return result.status === null && result.out === "" ? null : { out: result.out, err: result.err, status: result.status };
198
+ }
199
+ /**
200
+ * Runs the repo's package manager's install: quietly, or with its output on the terminal
201
+ * with `--verbose`.
202
+ * @param root - The repo.
203
+ * @param manager - `npm`, `pnpm`, `bun` or `yarn`.
204
+ * @returns Whether it worked and, if it ran quietly, what it printed.
205
+ * @example await runInstall(root, "pnpm")
206
+ */
207
+ export async function runInstall(root, manager) {
208
+ const result = await run(manager, ["install"], { cwd: root, inherit: verbose });
209
+ return { ok: result.status === 0, output: `${result.out}${result.err}` };
210
+ }
211
+ /**
212
+ * Runs `npx --yes <args>` in the repo without some variables of the environment, and returns
213
+ * what it printed.
214
+ * @param root - The repo.
215
+ * @param args - The package and its arguments.
216
+ * @param unset - The variables left out.
217
+ * @returns Its stdout, stderr and exit code.
218
+ * @example await captureNpxWithout(root, ["@ramon-sg/github-rest@0.3.0", "pr", "create"], ["GH_REPO"])
219
+ */
220
+ export async function captureNpxWithout(root, args, unset) {
221
+ return run("npx", ["--yes", ...args], { cwd: root, unset });
175
222
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-kitchen",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "One command to install, update and check a repo's agent tooling: the issue flow and its skills, the general skills, Claude cloud sessions, claude-repo-guard and the installed-versions rule in AGENTS.md.",
5
5
  "license": "MIT",
6
6
  "type": "module",