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.
- package/README.md +12 -8
- package/dist/cli.js +33 -18
- package/dist/commands/doctor.d.ts +2 -2
- package/dist/commands/doctor.js +3 -3
- package/dist/commands/sync.d.ts +2 -1
- package/dist/commands/sync.js +183 -51
- package/dist/menu.d.ts +36 -1
- package/dist/menu.js +74 -5
- package/dist/piece.d.ts +26 -6
- package/dist/piece.js +7 -0
- package/dist/pieces/cloud.d.ts +13 -4
- package/dist/pieces/cloud.js +22 -27
- package/dist/pieces/guard.d.ts +17 -5
- package/dist/pieces/guard.js +28 -36
- package/dist/pieces/issue-flow.d.ts +11 -4
- package/dist/pieces/issue-flow.js +35 -31
- package/dist/pieces/skills.d.ts +45 -5
- package/dist/pieces/skills.js +171 -75
- package/dist/repo.js +4 -4
- package/dist/skills-files.d.ts +16 -3
- package/dist/skills-files.js +30 -7
- package/dist/tools.d.ts +51 -22
- package/dist/tools.js +118 -28
- package/package.json +1 -1
|
@@ -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 = "
|
|
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}
|
|
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}
|
|
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
|
|
61
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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}:
|
|
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}
|
|
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: `
|
|
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
|
|
103
|
+
* @example (await issueFlowStatus(root)).state // "0.1.0, hay 0.2.0"
|
|
100
104
|
*/
|
|
101
|
-
export function issueFlowStatus(root) {
|
|
102
|
-
const summary = "
|
|
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
|
|
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
|
|
110
|
-
const state = installed === null ? `
|
|
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: "
|
|
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
|
|
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}
|
|
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: `
|
|
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: "
|
|
146
|
+
? { name: `${NAME}: skills`, status: "info", detail: "ninguna instalada" }
|
|
143
147
|
: off.length === 0
|
|
144
|
-
? { name: `${NAME}: skills`, status: "ok", detail: `${count}
|
|
145
|
-
: { name: `${NAME}: skills`, status: "fail", detail: `${off.join(", ")}
|
|
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
|
}
|
package/dist/pieces/skills.d.ts
CHANGED
|
@@ -1,17 +1,57 @@
|
|
|
1
|
-
|
|
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
|
|
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[]>;
|
package/dist/pieces/skills.js
CHANGED
|
@@ -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).
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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 {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
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
|
|
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
|
|
26
|
-
*
|
|
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(
|
|
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
|
-
*
|
|
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
|
-
* @
|
|
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
|
|
41
|
-
const
|
|
42
|
-
|
|
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
|
-
*
|
|
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
|
-
* @
|
|
129
|
+
* @param pick - What to install.
|
|
130
|
+
* @returns Whether `skills-manager` worked, or true if nothing was picked.
|
|
52
131
|
*/
|
|
53
|
-
function
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (
|
|
59
|
-
context.
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
89
|
-
|
|
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
|
-
|
|
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
|
|
102
|
-
*
|
|
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
|
-
* @
|
|
193
|
+
* @param withFlow - Whether this run may install the flow.
|
|
194
|
+
* @returns The version, or null if there is none.
|
|
105
195
|
*/
|
|
106
|
-
function
|
|
107
|
-
|
|
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
|
|
204
|
+
* @example (await skillsStatus(root, true)).state // "6 instaladas"
|
|
125
205
|
*/
|
|
126
|
-
export function skillsStatus(root) {
|
|
127
|
-
const summary = "skills
|
|
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
|
|
130
|
-
|
|
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.
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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: "
|
|
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
|
|
154
|
-
return [{ name: NAME, status: "fail", detail: `
|
|
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(`
|
|
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(`
|
|
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}
|
|
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}
|
|
67
|
+
throw new KitError(`${path} no es un objeto JSON; arréglalo y corre otra vez.`);
|
|
68
68
|
return value;
|
|
69
69
|
}
|
|
70
70
|
/**
|
package/dist/skills-files.d.ts
CHANGED
|
@@ -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
|
|
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[]):
|
|
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.
|