agent-kitchen 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,8 +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
+ * `init` runs with `--yes`: the person answered agent-kitchen's questions before the steps.
6
7
  */
7
- import { warn } from "../output.js";
8
8
  import { FLOW_CATALOG, FLOW_PACKAGE } from "../piece.js";
9
9
  import { dependencyVersion, readJson } from "../repo.js";
10
10
  import { catalogSkills, manifestCatalogs, pinCatalog, updateSkills } from "../skills-files.js";
@@ -12,7 +12,7 @@ import { captureNpx, compareVersions, isRelease, npmLatest, runNpx } from "../to
12
12
  /** The piece's name, as `--skip` takes it. */
13
13
  export const NAME = "issue-flow";
14
14
  /** How the catalog's ref is named when it is off. */
15
- const SKILLS_CATALOG_LABEL = "the catalog";
15
+ const SKILLS_CATALOG_LABEL = "el catálogo";
16
16
  /** The first version whose `doctor` prints `--json`. */
17
17
  const JSON_DOCTOR = "0.2.0";
18
18
  /**
@@ -43,9 +43,9 @@ export function flowVersion(root) {
43
43
  */
44
44
  function skillsOff(root, version) {
45
45
  const tag = `v${version}`;
46
- const off = catalogSkills(root, FLOW_CATALOG).filter((skill) => skill.tag !== tag).map((skill) => `${skill.name} at ${skill.tag ?? "no tag"}`);
46
+ const off = catalogSkills(root, FLOW_CATALOG).filter((skill) => skill.tag !== tag).map((skill) => `${skill.name} en ${skill.tag ?? "sin tag"}`);
47
47
  const source = manifestCatalogs(root).find((item) => item.catalog === FLOW_CATALOG);
48
- return source !== undefined && source.ref !== tag ? [...off, `${SKILLS_CATALOG_LABEL} at ${source.ref ?? "the latest"}`] : off;
48
+ return source !== undefined && source.ref !== tag ? [...off, `${SKILLS_CATALOG_LABEL} en ${source.ref ?? "la última"}`] : off;
49
49
  }
50
50
  /**
51
51
  * Moves the flow's skills to a version's tag: the catalog's ref in the manifest, and then
@@ -54,13 +54,16 @@ function skillsOff(root, version) {
54
54
  * @param version - The flow's version.
55
55
  * @returns Whether it worked, or true if the repo has none of the flow's skills.
56
56
  */
57
- function moveSkills(context, version) {
57
+ async function moveSkills(context, version) {
58
58
  if (!pinCatalog(context.root, FLOW_CATALOG, `v${version}`))
59
59
  return true;
60
- const failed = updateSkills(context.root, catalogSkills(context.root, FLOW_CATALOG).map((skill) => skill.name));
61
- if (failed === null)
60
+ const names = catalogSkills(context.root, FLOW_CATALOG).map((skill) => skill.name);
61
+ const failed = await updateSkills(context.root, names);
62
+ if (failed === null) {
63
+ context.note(`skills del flujo en v${version}: ${names.join(", ")}`);
62
64
  return true;
63
- warn(context.out, `skills-manager did not update the flow's skills; run '${failed}' and then agent-kitchen again.`);
65
+ }
66
+ context.fail(`skills-manager no actualizó las skills del flujo; corre '${failed.command}' y luego agent-kitchen otra vez.`, failed.run.output);
64
67
  return false;
65
68
  }
66
69
  /**
@@ -70,10 +73,11 @@ function moveSkills(context, version) {
70
73
  * @param args - The command and its flags.
71
74
  * @returns Whether it exited 0.
72
75
  */
73
- function runFlow(context, version, args) {
74
- if (runNpx(context.root, [`${FLOW_PACKAGE}@${version}`, ...args]))
76
+ async function runFlow(context, version, args) {
77
+ const run = await runNpx(context.root, [`${FLOW_PACKAGE}@${version}`, ...args]);
78
+ if (run.ok)
75
79
  return true;
76
- warn(context.out, `issue-flow ${args[0] ?? ""} failed; run 'npx ${FLOW_PACKAGE}@${version} ${args.join(" ")}'.`);
80
+ context.fail(`issue-flow ${args[0] ?? ""} falló; corre 'npx ${FLOW_PACKAGE}@${version} ${args.join(" ")}'.`, run.output);
77
81
  return false;
78
82
  }
79
83
  /**
@@ -85,47 +89,47 @@ function runFlow(context, version, args) {
85
89
  */
86
90
  function stepFor(installed, latest, root) {
87
91
  if (installed === null && latest !== null)
88
- return { piece: NAME, checked: true, label: `issue-flow ${latest}: .agents/flow.json with its recipes, the hooks and scripts`, run: (context) => runFlow(context, latest, ["init", ...(context.yes ? ["--yes"] : [])]) };
92
+ 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"]) };
89
93
  if (installed !== null && latest !== null && compareVersions(latest, installed) > 0)
90
- return { piece: NAME, checked: true, label: `issue-flow ${installed} → ${latest}: the CLI, its hooks and its skills`, run: (context) => runFlow(context, latest, ["upgrade"]) && moveSkills(context, latest) };
94
+ 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)) };
91
95
  if (installed !== null && skillsOff(root, installed).length > 0)
92
- return { piece: NAME, checked: true, label: `issue-flow's skills → v${installed}, the CLI's version`, run: (context) => moveSkills(context, installed) };
96
+ 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) };
93
97
  return null;
94
98
  }
95
99
  /**
96
100
  * What the repo has of the issue flow, and the step it needs.
97
101
  * @param root - The repo.
98
102
  * @returns The piece's status.
99
- * @example issueFlowStatus(root).state // "0.1.0, 0.2.0 available"
103
+ * @example (await issueFlowStatus(root)).state // "0.1.0, hay 0.2.0"
100
104
  */
101
- export function issueFlowStatus(root) {
102
- const summary = "the issue flow: branch, worktree and draft PR per issue, and its skills";
105
+ export async function issueFlowStatus(root) {
106
+ const summary = "rama, worktree y PR en draft por issue";
103
107
  const spec = flowSpec(root);
104
108
  if (spec !== null && !isRelease(spec))
105
- return { name: NAME, summary, installed: true, state: `package.json has ${spec}, not an exact release; agent-kitchen leaves it`, steps: [] };
109
+ return { name: NAME, summary, installed: true, state: `package.json tiene ${spec}, no una versión exacta; no se toca`, steps: [] };
106
110
  const installed = flowVersion(root);
107
- const latest = npmLatest(FLOW_PACKAGE);
111
+ const latest = await npmLatest(FLOW_PACKAGE);
108
112
  const step = stepFor(installed, latest, root);
109
- const known = latest === null ? " (npm cannot tell the latest)" : "";
110
- const state = installed === null ? `not installed${known}` : latest !== null && compareVersions(latest, installed) > 0 ? `${installed}, ${latest} available` : `${installed}${known}`;
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}`;
111
115
  return { name: NAME, summary, installed: installed !== null, state, steps: step === null ? [] : [step] };
112
116
  }
113
117
  /**
114
118
  * The flow's own doctor at the repo's version, and whether its skills are at that tag.
115
119
  * @param root - The repo.
116
120
  * @returns The checks.
117
- * @example issueFlowChecks(root)[0].name // "issue-flow: .agents/flow.json"
121
+ * @example (await issueFlowChecks(root))[0].name // "issue-flow: .agents/flow.json"
118
122
  */
119
- export function issueFlowChecks(root) {
123
+ export async function issueFlowChecks(root) {
120
124
  const spec = flowSpec(root);
121
125
  if (spec === null)
122
- return [{ name: NAME, status: "info", detail: "not installed" }];
126
+ return [{ name: NAME, status: "info", detail: "no instalado" }];
123
127
  const installed = flowVersion(root);
124
128
  if (installed === null)
125
- return [{ name: NAME, status: "info", detail: `package.json has ${spec}, not an exact release` }];
129
+ return [{ name: NAME, status: "info", detail: `package.json tiene ${spec}, no una versión exacta` }];
126
130
  if (compareVersions(installed, JSON_DOCTOR) < 0)
127
- return [{ name: NAME, status: "info", detail: `${installed} predates its doctor --json: run npx agent-kitchen to update it` }];
128
- const result = captureNpx(root, [`${FLOW_PACKAGE}@${installed}`, "doctor", "--json"]);
131
+ return [{ name: NAME, status: "info", detail: `${installed} no tiene doctor --json: corre npx agent-kitchen para actualizarlo` }];
132
+ const result = await captureNpx(root, [`${FLOW_PACKAGE}@${installed}`, "doctor", "--json"]);
129
133
  let found;
130
134
  try {
131
135
  found = JSON.parse(result?.out ?? "");
@@ -135,13 +139,13 @@ export function issueFlowChecks(root) {
135
139
  }
136
140
  const own = Array.isArray(found)
137
141
  ? found.map((check) => ({ name: `${NAME}: ${String(check.name)}`, status: check.ok === true ? "ok" : "fail", detail: String(check.detail) }))
138
- : [{ name: NAME, status: "fail", detail: `could not run its doctor; run 'npx ${FLOW_PACKAGE}@${installed} doctor'` }];
142
+ : [{ name: NAME, status: "fail", detail: `no se pudo correr su doctor; corre 'npx ${FLOW_PACKAGE}@${installed} doctor'` }];
139
143
  const off = skillsOff(root, installed);
140
144
  const count = catalogSkills(root, FLOW_CATALOG).length;
141
145
  const skills = count === 0 && off.length === 0
142
- ? { name: `${NAME}: skills`, status: "info", detail: "none installed" }
146
+ ? { name: `${NAME}: skills`, status: "info", detail: "ninguna instalada" }
143
147
  : off.length === 0
144
- ? { name: `${NAME}: skills`, status: "ok", detail: `${count} at v${installed}` }
145
- : { name: `${NAME}: skills`, status: "fail", detail: `${off.join(", ")}, the CLI is ${installed}: run npx agent-kitchen` };
148
+ ? { name: `${NAME}: skills`, status: "ok", detail: `${count} en v${installed}` }
149
+ : { name: `${NAME}: skills`, status: "fail", detail: `${off.join(", ")}; issue-flow está en ${installed}: corre npx agent-kitchen` };
146
150
  return [...own, skills];
147
151
  }
@@ -1,17 +1,57 @@
1
- import { type Check, type PieceStatus } from "../piece.js";
1
+ /**
2
+ * @file The skills, through `skills-manager`: the flow's catalog (pinned to the flow's
3
+ * version, because its skills call the CLI) and the general catalogs (each skill at its
4
+ * own tag). agent-kitchen reads what the catalogs offer (`--candidates`), lets the person
5
+ * pick before any step runs, and then installs the pick with `--select`, so
6
+ * `skills-manager` asks nothing. The first time every skill comes ticked. Later it offers
7
+ * to move the general skills to their catalog's latest tag, and, unticked, the skills a
8
+ * catalog has that the repo does not take yet. The flow's skills move with the flow
9
+ * (pieces/issue-flow.ts), never here.
10
+ *
11
+ * A skill the repo already has as its own (a folder the lock does not know) is replaced
12
+ * only if the person allows it (`--replace`); `skills-manager` keeps its `project.md`.
13
+ */
14
+ import { type Candidate, type Check, type PieceStatus, type SkillPick } from "../piece.js";
2
15
  /** The piece's name, as `--skip` takes it. */
3
16
  export declare const NAME = "skills";
17
+ /**
18
+ * Every skill some candidates bring: their own and what they require, all the way down,
19
+ * as `skills-manager` follows it.
20
+ * @param candidates - Every candidate offered.
21
+ * @param ids - The candidates picked.
22
+ * @returns The skills' names.
23
+ * @example skillsOf(candidates, ["catalog:ramon-sg/issue-flow/close-pr"]) // ["close-pr", "pr-review-comments", "github-issues"]
24
+ */
25
+ export declare function skillsOf(candidates: Candidate[], ids: string[]): string[];
26
+ /**
27
+ * The repo's own skills a pick would replace: each picked skill and what it requires.
28
+ * @param root - The repo.
29
+ * @param pick - The pick.
30
+ * @returns Their names.
31
+ * @example ownInPick(root, step.skills) // ["planning"]
32
+ */
33
+ export declare function ownInPick(root: string, pick: SkillPick): string[];
34
+ /**
35
+ * Drops from a pick the skills that would replace one of the repo's own (or that require
36
+ * one), for when the person does not allow replacing them.
37
+ * @param pick - The pick.
38
+ * @param own - The repo's own skills that stay.
39
+ * @returns The pick without them.
40
+ * @example keepOwn(pick, ["planning"]).picked
41
+ */
42
+ export declare function keepOwn(pick: SkillPick, own: string[]): SkillPick;
4
43
  /**
5
44
  * What the repo has of the skills, and the steps it can take.
6
45
  * @param root - The repo.
46
+ * @param withFlow - Whether this run may install the flow (it is not left out with `--skip`).
7
47
  * @returns The piece's status.
8
- * @example skillsStatus(root).state // "6 installed"
48
+ * @example (await skillsStatus(root, true)).state // "6 instaladas"
9
49
  */
10
- export declare function skillsStatus(root: string): PieceStatus;
50
+ export declare function skillsStatus(root: string, withFlow: boolean): Promise<PieceStatus>;
11
51
  /**
12
52
  * Whether `skills-manager check` passes: disk, lock and manifest agree.
13
53
  * @param root - The repo.
14
54
  * @returns The checks.
15
- * @example skillsChecks(root)[0].status // "ok"
55
+ * @example (await skillsChecks(root))[0].status // "ok"
16
56
  */
17
- export declare function skillsChecks(root: string): Check[];
57
+ export declare function skillsChecks(root: string): Promise<Check[]>;
@@ -1,20 +1,24 @@
1
1
  /**
2
2
  * @file The skills, through `skills-manager`: the flow's catalog (pinned to the flow's
3
3
  * version, because its skills call the CLI) and the general catalogs (each skill at its
4
- * own tag). The first time it opens the `skills-manager` menu with every catalog. Later it
5
- * offers to move the general skills to their catalog's latest tag, and, unticked, to pick
6
- * the skills a catalog has that the repo does not take yet. The flow's skills move with the
7
- * flow (pieces/issue-flow.ts), never here.
4
+ * own tag). agent-kitchen reads what the catalogs offer (`--candidates`), lets the person
5
+ * pick before any step runs, and then installs the pick with `--select`, so
6
+ * `skills-manager` asks nothing. The first time every skill comes ticked. Later it offers
7
+ * to move the general skills to their catalog's latest tag, and, unticked, the skills a
8
+ * catalog has that the repo does not take yet. The flow's skills move with the flow
9
+ * (pieces/issue-flow.ts), never here.
10
+ *
11
+ * A skill the repo already has as its own (a folder the lock does not know) is replaced
12
+ * only if the person allows it (`--replace`); `skills-manager` keeps its `project.md`.
8
13
  */
9
- import { warn } from "../output.js";
10
- import { FLOW_CATALOG, GENERAL_CATALOGS, SKILLS_MANIFEST } from "../piece.js";
11
- import { readJson } from "../repo.js";
12
- import { catalogSkills, lockedSkills, manifestCatalogs, pinCatalog, skillsManagerSpec, updateSkills } from "../skills-files.js";
13
- import { captureNpx, compareVersions, latestTag, runNpx } from "../tools.js";
14
+ import { FLOW_CATALOG, FLOW_PACKAGE, GENERAL_CATALOGS, SKILLS_MANIFEST } from "../piece.js";
15
+ import { isObject, readJson } from "../repo.js";
16
+ import { catalogSkills, lockedSkills, manifestCatalogs, ownSkills, pinCatalog, skillsManagerSpec, updateSkills } from "../skills-files.js";
17
+ import { captureNpx, compareVersions, latestTag, npmLatest, runNpx } from "../tools.js";
14
18
  import { flowVersion } from "./issue-flow.js";
15
19
  /** The piece's name, as `--skip` takes it. */
16
20
  export const NAME = "skills";
17
- /** The skills picked without a terminal: the flow's, and the audit `close-pr` uses. */
21
+ /** The skills picked when the catalogs cannot be read: the flow's, and the audit `close-pr` uses. */
18
22
  const DEFAULT_SKILLS = [
19
23
  ...["github-issues", "start-issue", "close-pr", "pr-review-comments", "pr-worktree"].map((name) => `catalog:${FLOW_CATALOG}/${name}`),
20
24
  "catalog:ramon-sg/agent-skills/code-audit",
@@ -22,47 +26,131 @@ const DEFAULT_SKILLS = [
22
26
  /** A release tag. */
23
27
  const RELEASE_TAG = /^v\d+\.\d+\.\d+$/;
24
28
  /**
25
- * The `--catalog` flags: the flow's at the repo's flow version, if the repo has the flow,
26
- * and the general ones.
27
- * @param root - The repo.
29
+ * The `--catalog` flags: the flow's at a version, if there is one, and the general ones.
30
+ * @param flow - The flow's version, or null.
28
31
  * @returns The flags.
29
32
  */
30
- function catalogArgs(root) {
31
- const flow = flowVersion(root);
33
+ function catalogArgs(flow) {
32
34
  const catalogs = [...(flow === null ? [] : [`${FLOW_CATALOG}#v${flow}`]), ...GENERAL_CATALOGS];
33
35
  return catalogs.flatMap((catalog) => ["--catalog", catalog]);
34
36
  }
35
37
  /**
36
- * The default skills the manifest does not take yet, of the catalogs offered.
38
+ * Reads one candidate of `--candidates`.
39
+ * @param value - An item of its output.
40
+ * @returns The candidate, or null if it is not one.
41
+ */
42
+ function toCandidate(value) {
43
+ if (!isObject(value) || typeof value.id !== "string" || typeof value.name !== "string")
44
+ return null;
45
+ const names = (field) => (Array.isArray(field) ? field.map(String) : []);
46
+ return {
47
+ id: value.id,
48
+ name: value.name,
49
+ catalog: typeof value.catalog === "string" ? value.catalog : "",
50
+ description: typeof value.description === "string" ? value.description : "",
51
+ skills: names(value.skills),
52
+ requires: names(value.requires),
53
+ };
54
+ }
55
+ /**
56
+ * Every skill some candidates bring: their own and what they require, all the way down,
57
+ * as `skills-manager` follows it.
58
+ * @param candidates - Every candidate offered.
59
+ * @param ids - The candidates picked.
60
+ * @returns The skills' names.
61
+ * @example skillsOf(candidates, ["catalog:ramon-sg/issue-flow/close-pr"]) // ["close-pr", "pr-review-comments", "github-issues"]
62
+ */
63
+ export function skillsOf(candidates, ids) {
64
+ const byName = new Map(candidates.map((candidate) => [candidate.name, candidate]));
65
+ const found = new Set();
66
+ const pending = candidates.filter((candidate) => ids.includes(candidate.id)).flatMap((candidate) => [...candidate.skills, ...candidate.requires]);
67
+ while (pending.length > 0) {
68
+ const name = pending.pop();
69
+ if (name === undefined || found.has(name))
70
+ continue;
71
+ found.add(name);
72
+ pending.push(...(byName.get(name)?.requires ?? []));
73
+ }
74
+ return [...found];
75
+ }
76
+ /**
77
+ * The catalog of a candidate's id (`catalog:<owner>/<repo>/<name>`).
78
+ * @param id - The id.
79
+ * @returns The catalog, as owner/repo.
80
+ */
81
+ function catalogOf(id) {
82
+ return id.replace(/^catalog:/, "").split("/").slice(0, 2).join("/");
83
+ }
84
+ /**
85
+ * What the catalogs offer that the manifest does not take yet, as `skills-manager` lists it.
86
+ * @param root - The repo.
87
+ * @param flow - The flow's version whose catalog to read, or null.
88
+ * @returns The candidates, or null if `skills-manager` could not tell.
89
+ */
90
+ async function readCandidates(root, flow) {
91
+ const command = readJson(root, SKILLS_MANIFEST) === null ? "init" : "add";
92
+ const result = await captureNpx(root, [skillsManagerSpec(root), command, "--candidates", ...catalogArgs(flow)], true);
93
+ let found;
94
+ try {
95
+ found = JSON.parse(result?.out ?? "");
96
+ }
97
+ catch {
98
+ return null;
99
+ }
100
+ if (!Array.isArray(found))
101
+ return null;
102
+ return found.flatMap((item) => toCandidate(item) ?? []);
103
+ }
104
+ /**
105
+ * The repo's own skills a pick would replace: each picked skill and what it requires.
37
106
  * @param root - The repo.
38
- * @returns Their ids.
107
+ * @param pick - The pick.
108
+ * @returns Their names.
109
+ * @example ownInPick(root, step.skills) // ["planning"]
110
+ */
111
+ export function ownInPick(root, pick) {
112
+ return ownSkills(root, skillsOf(pick.candidates, pick.picked));
113
+ }
114
+ /**
115
+ * Drops from a pick the skills that would replace one of the repo's own (or that require
116
+ * one), for when the person does not allow replacing them.
117
+ * @param pick - The pick.
118
+ * @param own - The repo's own skills that stay.
119
+ * @returns The pick without them.
120
+ * @example keepOwn(pick, ["planning"]).picked
39
121
  */
40
- function missingDefaults(root) {
41
- const manifest = readJson(root, SKILLS_MANIFEST);
42
- const sources = Array.isArray(manifest?.sources) ? manifest.sources : [];
43
- const taken = new Set(sources.flatMap((source) => (typeof source.catalog === "string" && Array.isArray(source.skills) ? source.skills.map((name) => `catalog:${String(source.catalog)}/${String(name)}`) : [])));
44
- const flow = flowVersion(root) !== null;
45
- return DEFAULT_SKILLS.filter((id) => !taken.has(id) && (flow || !id.startsWith(`catalog:${FLOW_CATALOG}/`)));
122
+ export function keepOwn(pick, own) {
123
+ const picked = pick.picked.filter((id) => !skillsOf(pick.candidates, [id]).some((name) => own.includes(name)));
124
+ return { ...pick, picked, replace: [] };
46
125
  }
47
126
  /**
48
- * Opens the `skills-manager` menu with the catalogs; without a terminal or with `--yes`,
49
- * picks the default skills the repo does not have yet.
127
+ * Installs the picked skills with `--select`, replacing the own ones the person allowed.
50
128
  * @param context - Where it runs.
51
- * @returns Whether `skills-manager` worked, or true if there was nothing to pick.
129
+ * @param pick - What to install.
130
+ * @returns Whether `skills-manager` worked, or true if nothing was picked.
52
131
  */
53
- function pick(context) {
54
- const spec = skillsManagerSpec(context.root);
55
- const args = [spec, readJson(context.root, SKILLS_MANIFEST) === null ? "init" : "add", ...catalogArgs(context.root)];
56
- const choose = context.yes || !context.out.interactive;
57
- const missing = choose ? missingDefaults(context.root) : [];
58
- if (choose && missing.length === 0) {
59
- context.out.stdout(` the default skills are already in ${SKILLS_MANIFEST}\n`);
132
+ async function install(context, pick) {
133
+ const flow = flowVersion(context.root);
134
+ const picked = flow === null ? pick.picked.filter((id) => catalogOf(id) !== FLOW_CATALOG) : pick.picked;
135
+ if (picked.length < pick.picked.length)
136
+ context.note("sin las skills del flujo, porque issue-flow no está instalado");
137
+ if (picked.length === 0) {
138
+ context.note("ninguna skill que instalar");
60
139
  return true;
61
140
  }
62
- if (runNpx(context.root, choose ? [...args, "--select", missing.join(",")] : args))
63
- return true;
64
- warn(context.out, `skills-manager did not install the skills; run 'npx ${args.join(" ")}'.`);
65
- return false;
141
+ const spec = skillsManagerSpec(context.root);
142
+ const command = readJson(context.root, SKILLS_MANIFEST) === null ? "init" : "add";
143
+ const args = [spec, command, ...catalogArgs(flow), "--select", picked.join(","), ...(pick.replace.length === 0 ? [] : ["--replace", pick.replace.join(",")])];
144
+ const run = await runNpx(context.root, args);
145
+ if (!run.ok) {
146
+ context.fail(`skills-manager no instaló las skills; corre 'npx ${args.join(" ")}'.`, run.output);
147
+ return false;
148
+ }
149
+ const names = pick.candidates.filter((candidate) => picked.includes(candidate.id)).map((candidate) => candidate.name);
150
+ context.note(names.length === 0 ? `${picked.length} skills` : names.join(", "));
151
+ if (pick.replace.length > 0)
152
+ context.note(`reemplazadas (con su project.md): ${pick.replace.join(", ")}`);
153
+ return true;
66
154
  }
67
155
  /**
68
156
  * The step that moves a general catalog's skills to its latest tag, if they are behind. A
@@ -72,10 +160,10 @@ function pick(context) {
72
160
  * @param ref - The ref the manifest pins it to, or null.
73
161
  * @returns The step, or null.
74
162
  */
75
- function updateStep(root, catalog, ref) {
163
+ async function updateStep(root, catalog, ref) {
76
164
  if (ref !== null && !RELEASE_TAG.test(ref))
77
165
  return null;
78
- const latest = latestTag(catalog);
166
+ const latest = await latestTag(catalog);
79
167
  if (latest === null)
80
168
  return null;
81
169
  const behind = catalogSkills(root, catalog).filter((skill) => skill.tag !== null && RELEASE_TAG.test(skill.tag) && compareVersions(latest, skill.tag.replace(/^v/, "")) > 0);
@@ -85,71 +173,79 @@ function updateStep(root, catalog, ref) {
85
173
  return {
86
174
  piece: NAME,
87
175
  checked: true,
88
- label: `${catalog}: ${behind.map((skill) => skill.name).join(", ")} ${from} → v${latest}`,
89
- run: (context) => {
176
+ downloads: [skillsManagerSpec(root)],
177
+ label: `Actualizar skills de ${catalog} ${from} → v${latest}: ${behind.map((skill) => skill.name).join(", ")}`,
178
+ run: async (context) => {
90
179
  if (ref !== null)
91
180
  pinCatalog(context.root, catalog, `v${latest}`);
92
- const failed = updateSkills(context.root, behind.map((skill) => skill.name));
181
+ const failed = await updateSkills(context.root, behind.map((skill) => skill.name));
93
182
  if (failed === null)
94
183
  return true;
95
- warn(context.out, `skills-manager did not update ${catalog}; run '${failed}' and then agent-kitchen again.`);
184
+ context.fail(`skills-manager no actualizó ${catalog}; corre '${failed.command}' y luego agent-kitchen otra vez.`, failed.run.output);
96
185
  return false;
97
186
  },
98
187
  };
99
188
  }
100
189
  /**
101
- * The skills the catalogs have and the repo does not take, as `skills-manager` lists them,
102
- * without those already in the lock (it lists the ones that came through `requires`).
190
+ * The flow's version whose catalog to offer: the repo's, or the latest when the repo does
191
+ * not have the flow yet (it is installed before the skills), unless the flow is left out.
103
192
  * @param root - The repo.
104
- * @returns Their names, or null if `skills-manager` could not tell.
193
+ * @param withFlow - Whether this run may install the flow.
194
+ * @returns The version, or null if there is none.
105
195
  */
106
- function newSkills(root) {
107
- const result = captureNpx(root, [skillsManagerSpec(root), "add", "--candidates", ...catalogArgs(root)]);
108
- let found;
109
- try {
110
- found = JSON.parse(result?.out ?? "");
111
- }
112
- catch {
113
- return null;
114
- }
115
- if (!Array.isArray(found))
116
- return null;
117
- const locked = lockedSkills(root);
118
- return found.map((candidate) => String(candidate.name)).filter((name) => locked[name] === undefined);
196
+ async function offeredFlow(root, withFlow) {
197
+ return flowVersion(root) ?? (withFlow ? await npmLatest(FLOW_PACKAGE) : null);
119
198
  }
120
199
  /**
121
200
  * What the repo has of the skills, and the steps it can take.
122
201
  * @param root - The repo.
202
+ * @param withFlow - Whether this run may install the flow (it is not left out with `--skip`).
123
203
  * @returns The piece's status.
124
- * @example skillsStatus(root).state // "6 installed"
204
+ * @example (await skillsStatus(root, true)).state // "6 instaladas"
125
205
  */
126
- export function skillsStatus(root) {
127
- const summary = "skills: the flow's, planning, code and test audits";
206
+ export async function skillsStatus(root, withFlow) {
207
+ const summary = "skills del flujo, planes y auditorías";
208
+ const flow = await offeredFlow(root, withFlow);
209
+ const candidatesRead = readCandidates(root, flow);
128
210
  if (readJson(root, SKILLS_MANIFEST) === null) {
129
- const label = `skills: pick them from ${[FLOW_CATALOG, ...GENERAL_CATALOGS].join(" and ")} (without a terminal, the flow's and code-audit)`;
130
- return { name: NAME, summary, installed: false, state: "not installed", steps: [{ piece: NAME, checked: true, label, run: pick }] };
211
+ const candidates = await candidatesRead;
212
+ const fallback = DEFAULT_SKILLS.filter((id) => flow !== null || catalogOf(id) !== FLOW_CATALOG).map((id) => {
213
+ const name = id.split("/").at(-1) ?? id;
214
+ return { id, name, catalog: catalogOf(id), description: "", skills: [name], requires: [] };
215
+ });
216
+ const offered = candidates ?? fallback;
217
+ const catalogs = [...new Set(offered.map((candidate) => candidate.catalog))];
218
+ const label = `Instalar ${offered.length} skills de ${catalogs.join(" y ")}`;
219
+ const skills = { candidates: offered, picked: offered.map((candidate) => candidate.id), replace: [] };
220
+ const state = candidates === null ? "no instaladas (no se pudieron leer los catálogos)" : "no instaladas";
221
+ return { name: NAME, summary, installed: false, state, steps: [{ piece: NAME, checked: true, downloads: [], label, skills, run: (context) => install(context, skills) }] };
131
222
  }
132
223
  const general = manifestCatalogs(root).filter((source) => source.catalog !== FLOW_CATALOG);
133
- const steps = general.flatMap((source) => updateStep(root, source.catalog, source.ref) ?? []);
134
- const fresh = newSkills(root);
135
- if (fresh !== null && fresh.length > 0)
136
- steps.push({ piece: NAME, checked: false, label: `skills: ${fresh.length} more to pick (${fresh.join(", ")})`, run: pick });
224
+ const steps = (await Promise.all(general.map((source) => updateStep(root, source.catalog, source.ref)))).flatMap((step) => step ?? []);
225
+ const locked = lockedSkills(root);
226
+ const candidates = await candidatesRead;
227
+ const fresh = (candidates ?? []).filter((candidate) => locked[candidate.name] === undefined);
228
+ if (fresh.length > 0) {
229
+ const skills = { candidates: fresh, picked: [], replace: [] };
230
+ steps.push({ piece: NAME, checked: false, downloads: [skillsManagerSpec(root)], label: `Agregar skills nuevas (${fresh.length}): ${fresh.map((candidate) => candidate.name).join(", ")}`, skills, run: (context) => install(context, skills) });
231
+ }
137
232
  const count = manifestCatalogs(root).reduce((total, source) => total + catalogSkills(root, source.catalog).length, 0);
138
233
  const behind = steps.filter((step) => step.checked).length;
139
- return { name: NAME, summary, installed: true, state: `${count} installed${behind > 0 ? ", some behind" : ""}`, steps };
234
+ const unread = candidates === null ? " (no se pudieron leer los catálogos)" : "";
235
+ return { name: NAME, summary, installed: true, state: `${count} instaladas${behind > 0 ? ", algunas atrasadas" : ", al día"}${unread}`, steps };
140
236
  }
141
237
  /**
142
238
  * Whether `skills-manager check` passes: disk, lock and manifest agree.
143
239
  * @param root - The repo.
144
240
  * @returns The checks.
145
- * @example skillsChecks(root)[0].status // "ok"
241
+ * @example (await skillsChecks(root))[0].status // "ok"
146
242
  */
147
- export function skillsChecks(root) {
243
+ export async function skillsChecks(root) {
148
244
  if (readJson(root, SKILLS_MANIFEST) === null)
149
- return [{ name: NAME, status: "info", detail: "not installed" }];
245
+ return [{ name: NAME, status: "info", detail: "no instaladas" }];
150
246
  const spec = skillsManagerSpec(root);
151
- const result = captureNpx(root, [spec, "check"]);
247
+ const result = await captureNpx(root, [spec, "check"]);
152
248
  if (result?.status === 0)
153
- return [{ name: NAME, status: "ok", detail: "skills-manager check passes" }];
154
- return [{ name: NAME, status: "fail", detail: `disk, lock and manifest disagree: run 'npx ${spec} check'` }];
249
+ return [{ name: NAME, status: "ok", detail: "skills-manager check pasa" }];
250
+ return [{ name: NAME, status: "fail", detail: `el disco, el lock y el manifiesto no coinciden: corre 'npx ${spec} check'` }];
155
251
  }
package/dist/repo.js CHANGED
@@ -38,10 +38,10 @@ export function mainCheckout(cwd, command) {
38
38
  const common = git(cwd, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
39
39
  const top = git(cwd, ["rev-parse", "--show-toplevel"]);
40
40
  if (common === null || top === null)
41
- throw new KitError(`run ${command} inside a checkout of the repo.`);
41
+ throw new KitError(`corre ${command} dentro del repo.`);
42
42
  const main = realpathSync(dirname(common));
43
43
  if (realpathSync(top) !== main)
44
- throw new KitError(`run ${command} in the main checkout, ${main}, not in a worktree.`);
44
+ throw new KitError(`corre ${command} en la copia principal, ${main}, no en un worktree.`);
45
45
  return main;
46
46
  }
47
47
  /**
@@ -61,10 +61,10 @@ export function readJson(root, path) {
61
61
  value = JSON.parse(readFileSync(file, "utf8"));
62
62
  }
63
63
  catch (error) {
64
- throw new KitError(`${path} is not valid JSON (${error instanceof Error ? error.message : String(error)}); fix it and run again.`);
64
+ throw new KitError(`${path} no es JSON válido (${error instanceof Error ? error.message : String(error)}); arréglalo y corre otra vez.`);
65
65
  }
66
66
  if (!isObject(value))
67
- throw new KitError(`${path} is not a JSON object; fix it and run again.`);
67
+ throw new KitError(`${path} no es un objeto JSON; arréglalo y corre otra vez.`);
68
68
  return value;
69
69
  }
70
70
  /**
@@ -1,3 +1,4 @@
1
+ import { type ToolRun } from "./tools.js";
1
2
  /** A skill in the lock: its name and the tag it is at. */
2
3
  export interface LockedSkill {
3
4
  name: string;
@@ -21,10 +22,22 @@ export declare function lockedSkills(root: string): Record<string, unknown>;
21
22
  * Runs `skills-manager update <name>` for each skill, stopping at the first that fails.
22
23
  * @param root - The repo.
23
24
  * @param names - The skills.
24
- * @returns The command that failed, to retry it, or null if every one worked.
25
- * @example updateSkills(root, ["close-pr", "pr-review-comments"]) // null
25
+ * @returns Null if every one worked, or the command that failed, to retry it, and what it printed.
26
+ * @example await updateSkills(root, ["close-pr", "pr-review-comments"]) // null
26
27
  */
27
- export declare function updateSkills(root: string, names: string[]): string | null;
28
+ export declare function updateSkills(root: string, names: string[]): Promise<{
29
+ command: string;
30
+ run: ToolRun;
31
+ } | null>;
32
+ /**
33
+ * The repo's own skills among some names: a folder in the skills folder that the lock does
34
+ * not know, which `skills-manager` replaces only when allowed.
35
+ * @param root - The repo.
36
+ * @param names - The skills about to be installed.
37
+ * @returns The names that are the repo's own.
38
+ * @example ownSkills(root, ["planning", "code-audit"]) // ["planning"]
39
+ */
40
+ export declare function ownSkills(root: string, names: string[]): string[];
28
41
  /**
29
42
  * The skills of a catalog in the repo's lock.
30
43
  * @param root - The repo.