agent-kitchen 0.4.0 → 0.5.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 CHANGED
@@ -17,14 +17,15 @@ Al arrancar revisa si npm tiene una versión más nueva de sí mismo. Si la hay,
17
17
  - Pregunta una vez: **todo lo sugerido** (Enter), **elegir** los pasos y las skills, o **salir**. Todas las preguntas van antes de instalar; después corre solo.
18
18
  - Cada paso es una línea con ✓ o ✗. La salida de cada herramienta solo aparece si falla, o con `--verbose`.
19
19
  - Es rápido: las versiones se consultan todas a la vez y las herramientas se descargan mientras respondes.
20
- - Cada pieza la instala su propia herramienta. `agent-kitchen` solo sabe cuáles hay, en qué versión están y cómo se conectan entre sí. La excepción es una línea de `AGENTS.md`, que escribe él mismo.
20
+ - Cada pieza la instala su propia herramienta. `agent-kitchen` solo sabe cuáles hay, en qué versión están y cómo se conectan entre sí. Las excepciones son una línea de `AGENTS.md` y la versión de `skills-manager` en `package.json`, que escribe él mismo.
21
21
  - Correrlo de nuevo no rompe nada: si todo está al día, lo dice y termina.
22
22
 
23
23
  ## Las piezas
24
24
 
25
25
  | Pieza | Qué es | Versión del repo | Última | Instalar | Actualizar |
26
26
  |---|---|---|---|---|---|
27
- | `issue-flow` | Flujo de issues ([`@ramon-sg/issue-flow`](https://github.com/ramon-sg/issue-flow)): rama, worktree y PR en draft por issue, y sus 5 skills | `package.json` | npm | `issue-flow init` | `issue-flow upgrade` en la versión nueva, y sus skills a la misma tag |
27
+ | `skills-manager` | La versión de [`@ramon-sg/skills-manager`](https://github.com/ramon-sg/skills-manager) que corre `agent-kitchen`: como mínimo 1.2.0. Si falta la agrega y si es menor la sube; nunca la baja. Un rango o `workspace:` no se toca, y avisa si la versión que corre queda bajo el mínimo. Solo en repos con `package.json` que usan skills | `package.json` | — | La agrega `agent-kitchen` | La sube `agent-kitchen` |
28
+ | `issue-flow` | Flujo de issues ([`@ramon-sg/issue-flow`](https://github.com/ramon-sg/issue-flow)): rama, worktree y PR en draft por issue, y sus 5 skills | `package.json` | npm | `issue-flow init` | `issue-flow upgrade` en la versión nueva, y sus skills a la misma tag. Desde 0.2.6, si a GitHub le faltan labels del flujo (`spike`, `P1`–`P4`…), `issue-flow labels sync` los crea |
28
29
  | `claude-cloud` | Sesiones en la nube ([`@ramon-sg/claude-cloud`](https://github.com/ramon-sg/claude-cloud)): herramientas, Docker y el clone completo | `.claude/cloud.json` | npm | `claude-cloud init` | Cambia la versión que fija `.claude/cloud.json` |
29
30
  | `claude-repo-guard` | Barandas ([`claude-repo-guard`](https://github.com/ramon-sg/claude-repo-guard)): modo auto con sandbox, escribe solo en su worktree | `.claude/repo-guard.manifest.json` | Tags `vX.Y.Z` de GitHub | `apply` en esa tag (`install` antes de 0.5.0) | `apply` en la tag nueva |
30
31
  | `skills` | Skills con [`skills-manager`](https://github.com/ramon-sg/skills-manager): las del flujo y las de [`agent-skills`](https://github.com/ramon-sg/agent-skills) (planes, auditoría de código y de tests) | `.agents/skills.lock.json` | Tags del catálogo | `skills-manager init --select` con las elegidas | `skills-manager update` |
@@ -57,7 +58,7 @@ npx agent-kitchen doctor # revisa cada pieza con su propio
57
58
  - **Corre en el checkout principal,** no en un worktree, y desde tu terminal. Dentro de una sesión de Claude, el sandbox de `claude-repo-guard` no deja escribir `.claude/settings.json`.
58
59
  - **Al terminar,** corre el `install` de tu gestor de paquetes, revisa el diff y haz commit.
59
60
 
60
- **`doctor`** corre el doctor de cada herramienta en la versión del repo: `issue-flow doctor --json`, `skills-manager check` y `claude-repo-guard doctor --json`. Además revisa que las skills del flujo estén en la tag de su CLI, que `.claude/cloud.json` fije una versión exacta y, en macOS, `weakerNetworkIsolation`. En un repo con `package.json`, también la línea de `AGENTS.md` y, si hay `CLAUDE.md`, su `@AGENTS.md`. Lo que no está instalado o tiene una versión nueva solo se anota con `·`. Sale con 1 si algo falla (`✗`).
61
+ **`doctor`** corre el doctor de cada herramienta en la versión del repo: `issue-flow doctor --json`, `skills-manager check` y `claude-repo-guard doctor --json`. Además revisa que `skills-manager` esté al menos en 1.2.0, que las skills del flujo estén en la tag de su CLI, qué labels del flujo le faltan a GitHub (con `·`), que `.claude/cloud.json` fije una versión exacta y, en macOS, `weakerNetworkIsolation`. En un repo con `package.json`, también la línea de `AGENTS.md` y, si hay `CLAUDE.md`, su `@AGENTS.md`. Lo que no está instalado o tiene una versión nueva solo se anota con `·`. Sale con 1 si algo falla (`✗`).
61
62
 
62
63
  ## Sumar una pieza
63
64
 
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @file `agent-kitchen doctor`: whether each piece the repo has works, with the doctor of each
3
- * tool at the repo's own version: the issue flow (and its skills at its tag),
3
+ * tool at the repo's own version: the version of skills-manager agent-kitchen needs, the
4
+ * issue flow (and its skills at its tag),
4
5
  * `skills-manager check`, the version `.claude/cloud.json` pins, and claude-repo-guard. It
5
6
  * changes nothing. A piece the repo does not have, or a newer release, is only noted, and so is
6
7
  * a missing rule in AGENTS.md.
@@ -3,6 +3,7 @@ import { cloudChecks } from "../pieces/cloud.js";
3
3
  import { guardChecks } from "../pieces/guard.js";
4
4
  import { issueFlowChecks } from "../pieces/issue-flow.js";
5
5
  import { skillsChecks } from "../pieces/skills.js";
6
+ import { skillsManagerChecks } from "../pieces/skills-manager.js";
6
7
  import { mainCheckout } from "../repo.js";
7
8
  /** The mark of each check's status. */
8
9
  const MARKS = { ok: "✓", fail: "✗", info: "·" };
@@ -16,7 +17,7 @@ const MARKS = { ok: "✓", fail: "✗", info: "·" };
16
17
  */
17
18
  export async function doctor(cwd, out) {
18
19
  const root = mainCheckout(cwd, "agent-kitchen doctor");
19
- const checks = (await Promise.all([issueFlowChecks(root), skillsChecks(root), cloudChecks(root), guardChecks(root), agentsMdChecks(root)])).flat();
20
+ const checks = (await Promise.all([skillsManagerChecks(root), issueFlowChecks(root), skillsChecks(root), cloudChecks(root), guardChecks(root), agentsMdChecks(root)])).flat();
20
21
  for (const check of checks)
21
22
  out.stdout(`${MARKS[check.status]} ${check.name}: ${check.detail}\n`);
22
23
  return checks.some((check) => check.status === "fail") ? 1 : 0;
@@ -10,7 +10,7 @@ export interface SyncOptions {
10
10
  /**
11
11
  * The names `--skip` takes.
12
12
  * @returns The pieces' names.
13
- * @example pieceNames() // ["issue-flow", "claude-cloud", "claude-repo-guard", "skills", "agents-md"]
13
+ * @example pieceNames() // ["skills-manager", "issue-flow", "claude-cloud", "claude-repo-guard", "skills", "agents-md"]
14
14
  */
15
15
  export declare function pieceNames(): string[];
16
16
  /**
@@ -1,13 +1,14 @@
1
1
  /**
2
2
  * @file `agent-kitchen apply [--yes] [--dry-run] [--verbose] [--skip <piece>,...]`: the one command
3
3
  * that changes the repo (and, for now, also the bare `agent-kitchen`).
4
- * It looks at each piece (the issue flow, claude-cloud, claude-repo-guard, the skills, the
5
- * rule in AGENTS.md) with
4
+ * It looks at each piece (the version of skills-manager, the issue flow, claude-cloud,
5
+ * claude-repo-guard, the skills, the rule in AGENTS.md) with
6
6
  * every lookup at once, shows what the repo has, and offers the steps that install what is
7
7
  * missing or update what is behind. In a terminal it asks first, all in one go: everything
8
8
  * suggested, or pick the steps and the skills; whether to replace the repo's own skills.
9
- * While the person answers, the tools download. Then the steps run in order (the flow
10
- * first, the skills last, since they follow the flow's version), one line each, with the
9
+ * While the person answers, the tools download. Then the steps run in order (skills-manager
10
+ * first, since the flow moves its skills with it; the skills last, since they follow the
11
+ * flow's version), one line each, with the
11
12
  * tools' output only when one fails; one that fails does not stop the rest. With `--yes`
12
13
  * it does what is suggested without asking (and keeps the repo's own skills); without a
13
14
  * terminal it only shows the steps.
@@ -20,14 +21,15 @@ import { guardStatus, NAME as GUARD } from "../pieces/guard.js";
20
21
  import { issueFlowStatus, NAME as FLOW } from "../pieces/issue-flow.js";
21
22
  import { keepOwn, NAME as SKILLS, ownInPick, skillsStatus } from "../pieces/skills.js";
22
23
  import { agentsMdStatus, NAME as AGENTS_MD } from "../pieces/agents-md.js";
24
+ import { NAME as SKILLS_MANAGER, skillsManagerStatus } from "../pieces/skills-manager.js";
23
25
  import { mainCheckout, packageManager } from "../repo.js";
24
26
  import { download, failedOutput, setVerbose } from "../tools.js";
25
27
  /** Every piece, in the order its steps run. */
26
- const PIECES = [FLOW, CLOUD, GUARD, SKILLS, AGENTS_MD];
28
+ const PIECES = [SKILLS_MANAGER, FLOW, CLOUD, GUARD, SKILLS, AGENTS_MD];
27
29
  /**
28
30
  * The names `--skip` takes.
29
31
  * @returns The pieces' names.
30
- * @example pieceNames() // ["issue-flow", "claude-cloud", "claude-repo-guard", "skills", "agents-md"]
32
+ * @example pieceNames() // ["skills-manager", "issue-flow", "claude-cloud", "claude-repo-guard", "skills", "agents-md"]
31
33
  */
32
34
  export function pieceNames() {
33
35
  return [...PIECES];
@@ -41,13 +43,14 @@ export function pieceNames() {
41
43
  * @returns The statuses, in the pieces' order.
42
44
  */
43
45
  async function statusesOf(root, skip) {
46
+ const skillsManager = skip.includes(SKILLS_MANAGER) ? null : skillsManagerStatus(root, !skip.includes(SKILLS));
44
47
  const flow = skip.includes(FLOW) ? null : issueFlowStatus(root);
45
48
  const cloud = skip.includes(CLOUD) ? null : cloudStatus(root);
46
49
  const skills = skip.includes(SKILLS) ? null : skillsStatus(root, !skip.includes(FLOW));
47
50
  const flowComing = (flow ?? Promise.resolve(null)).then((status) => status !== null && !status.installed && status.steps.length > 0);
48
51
  const guard = skip.includes(GUARD) ? null : flowComing.then((coming) => guardStatus(root, coming));
49
52
  const agentsMd = skip.includes(AGENTS_MD) ? null : flowComing.then((coming) => agentsMdStatus(root, coming));
50
- const all = await Promise.all([flow, cloud, guard, skills, agentsMd]);
53
+ const all = await Promise.all([skillsManager, flow, cloud, guard, skills, agentsMd]);
51
54
  return all.flatMap((status) => status ?? []);
52
55
  }
53
56
  /**
package/dist/piece.d.ts CHANGED
@@ -1,10 +1,11 @@
1
1
  /**
2
2
  * @file What agent-kitchen knows of each piece it manages, and the names they share. A piece
3
- * (the issue flow, claude-cloud, claude-repo-guard, the skills, the rule in AGENTS.md) tells
4
- * what the repo has and the steps that would install or update it; agent-kitchen shows them,
5
- * lets the person pick, and runs the picked ones in order. Every step runs the piece's own
6
- * tool, which owns its files: agent-kitchen only writes the few lines that join two pieces,
7
- * and the one line of AGENTS.md, which has no tool.
3
+ * (the version of skills-manager, the issue flow, claude-cloud, claude-repo-guard, the
4
+ * skills, the rule in AGENTS.md) tells what the repo has and the steps that would install or
5
+ * update it; agent-kitchen shows them, lets the person pick, and runs the picked ones in
6
+ * order. Every step runs the piece's own tool, which owns its files: agent-kitchen only
7
+ * writes the few lines that join two pieces, the one line of AGENTS.md, which has no tool,
8
+ * and the version of skills-manager in package.json, which agent-kitchen is the one to run.
8
9
  */
9
10
  /** The issue flow on npm. */
10
11
  export declare const FLOW_PACKAGE = "@ramon-sg/issue-flow";
package/dist/piece.js CHANGED
@@ -1,10 +1,11 @@
1
1
  /**
2
2
  * @file What agent-kitchen knows of each piece it manages, and the names they share. A piece
3
- * (the issue flow, claude-cloud, claude-repo-guard, the skills, the rule in AGENTS.md) tells
4
- * what the repo has and the steps that would install or update it; agent-kitchen shows them,
5
- * lets the person pick, and runs the picked ones in order. Every step runs the piece's own
6
- * tool, which owns its files: agent-kitchen only writes the few lines that join two pieces,
7
- * and the one line of AGENTS.md, which has no tool.
3
+ * (the version of skills-manager, the issue flow, claude-cloud, claude-repo-guard, the
4
+ * skills, the rule in AGENTS.md) tells what the repo has and the steps that would install or
5
+ * update it; agent-kitchen shows them, lets the person pick, and runs the picked ones in
6
+ * order. Every step runs the piece's own tool, which owns its files: agent-kitchen only
7
+ * writes the few lines that join two pieces, the one line of AGENTS.md, which has no tool,
8
+ * and the version of skills-manager in package.json, which agent-kitchen is the one to run.
8
9
  */
9
10
  /** The issue flow on npm. */
10
11
  export const FLOW_PACKAGE = "@ramon-sg/issue-flow";
@@ -3,6 +3,8 @@
3
3
  * The repo's version is the one in package.json. Installing runs its `init` at the latest
4
4
  * version; updating runs its `upgrade` at the new one and then moves its skills to the same
5
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.
6
8
  * `init` runs with `--yes`: the person answered agent-kitchen's questions before the steps.
7
9
  */
8
10
  import { type Check, type PieceStatus } from "../piece.js";
@@ -3,10 +3,12 @@
3
3
  * The repo's version is the one in package.json. Installing runs its `init` at the latest
4
4
  * version; updating runs its `upgrade` at the new one and then moves its skills to the same
5
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.
6
8
  * `init` runs with `--yes`: the person answered agent-kitchen's questions before the steps.
7
9
  */
8
10
  import { FLOW_CATALOG, FLOW_PACKAGE } from "../piece.js";
9
- import { dependencyVersion, readJson } from "../repo.js";
11
+ import { dependencyVersion, isObject, readJson } from "../repo.js";
10
12
  import { catalogSkills, manifestCatalogs, pinCatalog, updateSkills } from "../skills-files.js";
11
13
  import { captureNpx, compareVersions, isRelease, npmLatest, runNpx } from "../tools.js";
12
14
  /** The piece's name, as `--skip` takes it. */
@@ -15,6 +17,8 @@ export const NAME = "issue-flow";
15
17
  const SKILLS_CATALOG_LABEL = "el catálogo";
16
18
  /** The first version whose `doctor` prints `--json`. */
17
19
  const JSON_DOCTOR = "0.2.0";
20
+ /** The first version with `labels sync`. */
21
+ const LABELS_SINCE = "0.2.6";
18
22
  /**
19
23
  * The issue flow's version in the repo, as package.json has it: an exact release when the
20
24
  * flow set it up, or anything else (a range, a `file:` link) someone wrote by hand.
@@ -80,6 +84,46 @@ async function runFlow(context, version, args) {
80
84
  context.fail(`issue-flow ${args[0] ?? ""} falló; corre 'npx ${FLOW_PACKAGE}@${version} ${args.join(" ")}'.`, run.output);
81
85
  return false;
82
86
  }
87
+ /**
88
+ * The labels the flow expects that GitHub lacks, as `labels sync --dry-run --json` of a
89
+ * version tells (a label that only differs in color or description does not count).
90
+ * @param root - The repo.
91
+ * @param version - The flow's version.
92
+ * @returns Their names, none for a version without `labels sync`, or null if it could not tell.
93
+ * @example await missingLabels(root, "0.2.6") // ["spike"]
94
+ */
95
+ async function missingLabels(root, version) {
96
+ if (compareVersions(version, LABELS_SINCE) < 0)
97
+ return [];
98
+ const result = await captureNpx(root, [`${FLOW_PACKAGE}@${version}`, "labels", "sync", "--dry-run", "--json"], true);
99
+ let found;
100
+ try {
101
+ found = JSON.parse(result?.out ?? "");
102
+ }
103
+ catch {
104
+ return null;
105
+ }
106
+ if (!isObject(found) || !Array.isArray(found.labels))
107
+ return null;
108
+ return found.labels.filter((label) => isObject(label) && label.status === "to-create").map((label) => String(label.name));
109
+ }
110
+ /**
111
+ * The step that makes in GitHub the labels the flow lacks, with its own command. It only
112
+ * runs if the repo has that version of the flow by then: not when its install or update
113
+ * was left out or failed.
114
+ * @param version - The flow's version.
115
+ * @param missing - The labels.
116
+ * @returns The step.
117
+ */
118
+ function labelsStep(version, missing) {
119
+ return { piece: NAME, checked: true, downloads: [`${FLOW_PACKAGE}@${version}`], label: `Crear en GitHub los labels que faltan: ${missing.join(", ")}`, run: async (context) => {
120
+ if (flowVersion(context.root) !== version) {
121
+ context.note(`no se crearon: el repo no tiene issue-flow ${version}`);
122
+ return true;
123
+ }
124
+ return runFlow(context, version, ["labels", "sync"]);
125
+ } };
126
+ }
83
127
  /**
84
128
  * The step the repo needs, if any.
85
129
  * @param installed - The repo's version, or null.
@@ -110,9 +154,13 @@ export async function issueFlowStatus(root) {
110
154
  const installed = flowVersion(root);
111
155
  const latest = await npmLatest(FLOW_PACKAGE);
112
156
  const step = stepFor(installed, latest, root);
113
- const known = latest === null ? " (npm no responde)" : "";
114
- const state = installed === null ? `no instalado${known}` : latest !== null && compareVersions(latest, installed) > 0 ? `${installed}, hay ${latest}` : `${installed}, al día${known}`;
115
- return { name: NAME, summary, installed: installed !== null, state, steps: step === null ? [] : [step] };
157
+ const behind = installed !== null && latest !== null && compareVersions(latest, installed) > 0;
158
+ const target = installed === null || behind ? latest : installed;
159
+ const missing = target === null ? [] : await missingLabels(root, target);
160
+ const known = `${latest === null ? " (npm no responde)" : ""}${missing === null ? " (no se pudieron leer los labels de GitHub)" : ""}`;
161
+ 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)] : [])];
163
+ return { name: NAME, summary, installed: installed !== null, state, steps };
116
164
  }
117
165
  /**
118
166
  * The flow's own doctor at the repo's version, and whether its skills are at that tag.
@@ -129,6 +177,7 @@ export async function issueFlowChecks(root) {
129
177
  return [{ name: NAME, status: "info", detail: `package.json tiene ${spec}, no una versión exacta` }];
130
178
  if (compareVersions(installed, JSON_DOCTOR) < 0)
131
179
  return [{ name: NAME, status: "info", detail: `${installed} no tiene doctor --json: corre npx agent-kitchen apply para actualizarlo` }];
180
+ const labelsRead = missingLabels(root, installed);
132
181
  const result = await captureNpx(root, [`${FLOW_PACKAGE}@${installed}`, "doctor", "--json"]);
133
182
  let found;
134
183
  try {
@@ -147,5 +196,13 @@ export async function issueFlowChecks(root) {
147
196
  : off.length === 0
148
197
  ? { name: `${NAME}: skills`, status: "ok", detail: `${count} en v${installed}` }
149
198
  : { name: `${NAME}: skills`, status: "fail", detail: `${off.join(", ")}; issue-flow está en ${installed}: corre npx agent-kitchen apply` };
150
- return [...own, skills];
199
+ const missing = await labelsRead;
200
+ const labels = compareVersions(installed, LABELS_SINCE) < 0
201
+ ? []
202
+ : missing === null
203
+ ? [{ name: `${NAME}: labels`, status: "info", detail: "no se pudieron leer de GitHub" }]
204
+ : missing.length > 0
205
+ ? [{ name: `${NAME}: labels`, status: "info", detail: `faltan ${missing.join(", ")} en GitHub; corre npx agent-kitchen apply` }]
206
+ : [{ name: `${NAME}: labels`, status: "ok", detail: "GitHub tiene los que espera el flujo" }];
207
+ return [...own, skills, ...labels];
151
208
  }
@@ -0,0 +1,28 @@
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 with package.json that uses skills.
7
+ */
8
+ import { type Check, type PieceStatus } from "../piece.js";
9
+ /** The piece's name, as `--skip` takes it. */
10
+ export declare const NAME = "skills-manager";
11
+ /** The oldest `skills-manager` agent-kitchen works with: the first with `--replace`. */
12
+ export declare const MIN_VERSION = "1.2.0";
13
+ /**
14
+ * What the repo has of skills-manager, and the step that raises it, if any. The step reads
15
+ * package.json again when it runs.
16
+ * @param root - The repo.
17
+ * @param withSkills - Whether this run may install skills.
18
+ * @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"
20
+ */
21
+ export declare function skillsManagerStatus(root: string, withSkills: boolean): PieceStatus | null;
22
+ /**
23
+ * Whether the version of skills-manager the repo runs is the one agent-kitchen needs.
24
+ * @param root - The repo.
25
+ * @returns The checks; none where the piece does not apply.
26
+ * @example skillsManagerChecks(root)[0]?.status // "ok"
27
+ */
28
+ export declare function skillsManagerChecks(root: string): Check[];
@@ -0,0 +1,106 @@
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 with package.json that uses skills.
7
+ */
8
+ import { SKILLS_MANAGER, SKILLS_MANIFEST } from "../piece.js";
9
+ import { dependencyVersion, installedVersion, readJson, setDependency } from "../repo.js";
10
+ import { compareVersions, isRelease } from "../tools.js";
11
+ /** The piece's name, as `--skip` takes it. */
12
+ export const NAME = "skills-manager";
13
+ /** The oldest `skills-manager` agent-kitchen works with: the first with `--replace`. */
14
+ export const MIN_VERSION = "1.2.0";
15
+ /**
16
+ * What package.json has of skills-manager and, for a range, the version that runs: the one
17
+ * in node_modules, or the first x.y.z of the range.
18
+ * @param root - The repo.
19
+ * @returns What it has.
20
+ */
21
+ function pinned(root) {
22
+ const spec = dependencyVersion(readJson(root, "package.json"), SKILLS_MANAGER);
23
+ if (spec === null)
24
+ return { kind: "missing" };
25
+ if (isRelease(spec))
26
+ return { kind: "exact", version: spec };
27
+ const floor = spec.startsWith("workspace:") ? null : (/\d+\.\d+\.\d+/.exec(spec)?.[0] ?? null);
28
+ return { kind: "other", spec, runs: installedVersion(root, SKILLS_MANAGER) ?? floor };
29
+ }
30
+ /**
31
+ * Whether a version is older than the one agent-kitchen needs.
32
+ * @param version - The version.
33
+ * @returns True if it is below the minimum.
34
+ */
35
+ function below(version) {
36
+ return compareVersions(version, MIN_VERSION) < 0;
37
+ }
38
+ /**
39
+ * Whether the piece applies: the repo has package.json and uses skills, or this run may
40
+ * install them.
41
+ * @param root - The repo.
42
+ * @param withSkills - Whether this run may install skills (`skills` is not left out).
43
+ * @returns True if it applies.
44
+ */
45
+ function applies(root, withSkills) {
46
+ return readJson(root, "package.json") !== null && (withSkills || readJson(root, SKILLS_MANIFEST) !== null);
47
+ }
48
+ /**
49
+ * What the repo has of skills-manager, and the step that raises it, if any. The step reads
50
+ * package.json again when it runs.
51
+ * @param root - The repo.
52
+ * @param withSkills - Whether this run may install skills.
53
+ * @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"
55
+ */
56
+ export function skillsManagerStatus(root, withSkills) {
57
+ if (!applies(root, withSkills))
58
+ return null;
59
+ const summary = "el CLI de las skills, que corre agent-kitchen";
60
+ const now = pinned(root);
61
+ if (now.kind === "other") {
62
+ const old = now.runs !== null && below(now.runs) ? `; corre ${now.runs}, bajo ${MIN_VERSION}` : "";
63
+ return { name: NAME, summary, installed: true, state: `package.json tiene ${now.spec}, no una versión exacta; no se toca${old}`, steps: [] };
64
+ }
65
+ if (now.kind === "exact" && !below(now.version))
66
+ return { name: NAME, summary, installed: true, state: `${now.version}, al día`, steps: [] };
67
+ 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`;
68
+ const state = now.kind === "missing" ? "no está en package.json" : `${now.version}, bajo ${MIN_VERSION}`;
69
+ const step = {
70
+ piece: NAME,
71
+ checked: true,
72
+ downloads: [`${SKILLS_MANAGER}@${MIN_VERSION}`],
73
+ label,
74
+ run: async (context) => {
75
+ const current = pinned(context.root);
76
+ if (current.kind === "missing" || (current.kind === "exact" && below(current.version))) {
77
+ setDependency(context.root, SKILLS_MANAGER, MIN_VERSION);
78
+ context.note(`${SKILLS_MANAGER} ${MIN_VERSION} en package.json`);
79
+ }
80
+ else
81
+ context.note("package.json ya tenía una versión que sirve");
82
+ return true;
83
+ },
84
+ };
85
+ return { name: NAME, summary, installed: now.kind !== "missing", state, steps: [step] };
86
+ }
87
+ /**
88
+ * Whether the version of skills-manager the repo runs is the one agent-kitchen needs.
89
+ * @param root - The repo.
90
+ * @returns The checks; none where the piece does not apply.
91
+ * @example skillsManagerChecks(root)[0]?.status // "ok"
92
+ */
93
+ export function skillsManagerChecks(root) {
94
+ if (!applies(root, false))
95
+ return [];
96
+ const now = pinned(root);
97
+ if (now.kind === "missing")
98
+ return [{ name: NAME, status: "info", detail: `no está en package.json; corre npx agent-kitchen apply para fijar ${MIN_VERSION}` }];
99
+ if (now.kind === "exact")
100
+ return below(now.version)
101
+ ? [{ name: NAME, status: "fail", detail: `${now.version}, bajo ${MIN_VERSION}, el mínimo de agent-kitchen: corre npx agent-kitchen apply` }]
102
+ : [{ name: NAME, status: "ok", detail: `${now.version} (mínimo ${MIN_VERSION})` }];
103
+ if (now.runs !== null && below(now.runs))
104
+ return [{ name: NAME, status: "fail", detail: `${now.spec} corre ${now.runs}, bajo ${MIN_VERSION}, el mínimo de agent-kitchen: sube el rango` }];
105
+ return [{ name: NAME, status: "ok", detail: `${now.spec}${now.runs === null ? "" : `, corre ${now.runs}`}` }];
106
+ }
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
+ }
@@ -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: the version the repo pins in package.json, so the lock is
70
- * written by the same version every time, or the latest 1.x.
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.1.0"
75
+ * @example skillsManagerSpec(root) // "@ramon-sg/skills-manager@1.2.0"
74
76
  */
75
77
  export declare function skillsManagerSpec(root: string): string;
@@ -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), and updating
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: the version the repo pins in package.json, so the lock is
109
- * written by the same version every time, or the latest 1.x.
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.1.0"
114
+ * @example skillsManagerSpec(root) // "@ramon-sg/skills-manager@1.2.0"
113
115
  */
114
116
  export function skillsManagerSpec(root) {
115
- return `${SKILLS_MANAGER}@${dependencyVersion(readJson(root, "package.json"), SKILLS_MANAGER) ?? "1"}`;
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-kitchen",
3
- "version": "0.4.0",
3
+ "version": "0.5.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",