agent-kitchen 0.2.0 → 0.3.1
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 +13 -9
- 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 +37 -5
- package/dist/pieces/guard.js +59 -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 +21 -7
package/README.md
CHANGED
|
@@ -11,8 +11,10 @@ Al arrancar revisa si npm tiene una versión más nueva de sí mismo. Si la hay,
|
|
|
11
11
|
|
|
12
12
|
## En simple
|
|
13
13
|
|
|
14
|
-
- Muestra cada pieza con la versión que tiene el repo y
|
|
15
|
-
-
|
|
14
|
+
- Muestra cada pieza con la versión que tiene el repo y si está al día.
|
|
15
|
+
- 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.
|
|
16
|
+
- Cada paso es una línea con ✓ o ✗. La salida de cada herramienta solo aparece si falla, o con `--verbose`.
|
|
17
|
+
- Es rápido: las versiones se consultan todas a la vez y las herramientas se descargan mientras respondes.
|
|
16
18
|
- 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í.
|
|
17
19
|
- Correrlo de nuevo no rompe nada: si todo está al día, lo dice y termina.
|
|
18
20
|
|
|
@@ -22,24 +24,26 @@ Al arrancar revisa si npm tiene una versión más nueva de sí mismo. Si la hay,
|
|
|
22
24
|
|---|---|---|---|---|---|
|
|
23
25
|
| `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 |
|
|
24
26
|
| `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` |
|
|
25
|
-
| `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 | `
|
|
26
|
-
| `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 |
|
|
27
|
+
| `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 |
|
|
28
|
+
| `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` |
|
|
27
29
|
|
|
28
30
|
Si npm o GitHub no responden, la pieza se muestra pero no se ofrece actualizarla. Si un paso falla, los demás siguen y el comando dice cómo reintentarlo.
|
|
29
31
|
|
|
30
32
|
### Cómo se conectan
|
|
31
33
|
|
|
32
34
|
- **Las skills del flujo van con su CLI,** porque la llaman: se instalan en la tag de la versión del flujo y se mueven con él. Las generales llevan cada una su propia versión.
|
|
33
|
-
- **
|
|
35
|
+
- **Qué skills:** la primera vez vienen todas las de los catálogos. Con "Elegir" aparece un menú agrupado por catálogo, con la descripción de cada una. Si un catálogo trae skills que el repo no tiene, se ofrecen como un paso sin marcar.
|
|
36
|
+
- **Skills propias del repo:** si el repo ya tiene una carpeta con el nombre de una skill del catálogo, pregunta una vez si reemplazarlas. Al reemplazar se conserva su `project.md`. Con `--yes` no se reemplazan: se dejan fuera y lo dice.
|
|
34
37
|
- **Seguridad y flujo:** si el repo tiene el flujo, antes de instalar `claude-repo-guard` se escribe en sus opciones que abrir el PR en draft, asignar la issue y agregar el "Slug de rama" son parte del trabajo normal. También se escribe la rama base del flujo como `integrationBranch`. En macOS se agrega `weakerNetworkIsolation`, que `gh` necesita dentro del sandbox. Si `claude-repo-guard` ya estaba al día pero le faltan esas opciones, se ofrecen como un paso.
|
|
35
38
|
- **La nube no molesta en local:** el hook de sesión solo actúa en la nube. Un repo que nunca se abre ahí no pierde nada.
|
|
36
39
|
|
|
37
40
|
## Uso
|
|
38
41
|
|
|
39
42
|
```sh
|
|
40
|
-
npx agent-kitchen #
|
|
41
|
-
npx agent-kitchen --dry-run # solo muestra qué
|
|
43
|
+
npx agent-kitchen # pregunta, en una terminal (Enter: todo lo sugerido)
|
|
44
|
+
npx agent-kitchen --dry-run # solo muestra qué haría
|
|
42
45
|
npx agent-kitchen --yes # hace lo sugerido sin preguntar
|
|
46
|
+
npx agent-kitchen --verbose # muestra la salida de cada herramienta
|
|
43
47
|
npx agent-kitchen --skip claude-cloud # deja fuera una pieza (o varias, con comas)
|
|
44
48
|
npx agent-kitchen doctor # revisa cada pieza con su propio doctor
|
|
45
49
|
```
|
|
@@ -52,7 +56,7 @@ npx agent-kitchen doctor # revisa cada pieza con su propio docto
|
|
|
52
56
|
|
|
53
57
|
## Sumar una pieza
|
|
54
58
|
|
|
55
|
-
Una pieza es un archivo en `src/pieces/` con dos funciones: `status(root)` dice qué tiene el repo y qué pasos ofrece, y `checks(root)` da sus revisiones para `doctor`. Se registra en `PIECES` de `src/commands/sync.ts` y en `src/commands/doctor.ts`. Cada paso corre la herramienta de la pieza, que es dueña de sus archivos. Un catálogo de skills nuevo es una línea en `GENERAL_CATALOGS` de `src/piece.ts`.
|
|
59
|
+
Una pieza es un archivo en `src/pieces/` con dos funciones asíncronas: `status(root)` dice qué tiene el repo y qué pasos ofrece, y `checks(root)` da sus revisiones para `doctor`. Un paso dice qué paquetes descargar antes (`downloads`) y nunca pregunta nada: las preguntas van antes, en `src/menu.ts`. Se registra en `PIECES` de `src/commands/sync.ts` y en `src/commands/doctor.ts`. Cada paso corre la herramienta de la pieza, que es dueña de sus archivos. Un catálogo de skills nuevo es una línea en `GENERAL_CATALOGS` de `src/piece.ts`.
|
|
56
60
|
|
|
57
61
|
## Desarrollo
|
|
58
62
|
|
|
@@ -61,7 +65,7 @@ npm install
|
|
|
61
65
|
npm test # compila y corre los tests
|
|
62
66
|
```
|
|
63
67
|
|
|
64
|
-
Los tests corren el CLI compilado sobre repos reales. `npx` y `npm` son falsos (`test/fake-tools.mjs`) y registran cada llamada. Las tags de GitHub salen de repos locales a los que git llega reescribiendo `https://github.com/`, así que `git ls-remote` es el real.
|
|
68
|
+
Los tests corren el CLI compilado sobre repos reales. `npx` y `npm` son falsos (`test/fake-tools.mjs`) y registran cada llamada. Las tags de GitHub salen de repos locales a los que git llega reescribiendo `https://github.com/`, así que `git ls-remote` es el real. Los menús no se prueban renderizados: se prueban `menuOptions` y `skillOptions`, y se corrieron en una terminal con `expect`. Con `@clack/prompts` 1.8.1, una ruedita mostrada antes de una pregunta deja a la segunda pregunta sin teclado; por eso la revisión inicial es una línea de texto.
|
|
65
69
|
|
|
66
70
|
## Publicar una versión
|
|
67
71
|
|
package/dist/cli.js
CHANGED
|
@@ -11,23 +11,27 @@ import { parseArgs } from "node:util";
|
|
|
11
11
|
import { doctor } from "./commands/doctor.js";
|
|
12
12
|
import { pieceNames, sync } from "./commands/sync.js";
|
|
13
13
|
import { KitError, processOutput, UsageError } from "./output.js";
|
|
14
|
-
import {
|
|
14
|
+
import { FLOW_PACKAGE, GENERAL_CATALOGS } from "./piece.js";
|
|
15
|
+
import { PACKAGE as CLOUD_PACKAGE } from "./pieces/cloud.js";
|
|
16
|
+
import { REPO as GUARD_REPO } from "./pieces/guard.js";
|
|
17
|
+
import { compareVersions, isRelease, latestTag, npmLatest } from "./tools.js";
|
|
15
18
|
/** This package on npm. */
|
|
16
19
|
const PACKAGE = "agent-kitchen";
|
|
17
20
|
/** Set in the run that a newer agent-kitchen was handed to, so it never hands over again. */
|
|
18
21
|
const HANDED_OVER = "AGENT_KITCHEN_HANDED_OVER";
|
|
19
22
|
/** Usage text. */
|
|
20
|
-
const USAGE = `
|
|
21
|
-
agent-kitchen [--yes | --dry-run] [--skip <
|
|
23
|
+
const USAGE = `Uso:
|
|
24
|
+
agent-kitchen [--yes | --dry-run] [--verbose] [--skip <pieza>,...]
|
|
22
25
|
agent-kitchen doctor
|
|
23
26
|
|
|
24
|
-
agent-kitchen
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
agent-kitchen revisa cada pieza de las herramientas de agentes del repo (${pieceNames().join(", ")}),
|
|
28
|
+
muestra qué tiene el repo y la última versión de cada una, y ofrece instalar lo que falta y
|
|
29
|
+
actualizar lo que está atrasado. En una terminal pregunta una vez: hacer todo lo sugerido,
|
|
30
|
+
o elegir los pasos y las skills. --yes hace lo sugerido sin preguntar; sin terminal solo lo
|
|
31
|
+
muestra. --verbose muestra la salida de cada herramienta. Córrelo en la copia principal del
|
|
32
|
+
repo, desde tu terminal.
|
|
29
33
|
|
|
30
|
-
doctor
|
|
34
|
+
doctor corre las revisiones de cada pieza en la versión del repo, sin cambiar nada.`;
|
|
31
35
|
/**
|
|
32
36
|
* This package's version.
|
|
33
37
|
* @returns The version in its package.json.
|
|
@@ -36,19 +40,29 @@ function ownVersion() {
|
|
|
36
40
|
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
37
41
|
return pkg.version ?? "unknown";
|
|
38
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Starts the lookups of the latest versions, so they run while this one checks its own.
|
|
45
|
+
*/
|
|
46
|
+
function startLookups() {
|
|
47
|
+
void npmLatest(FLOW_PACKAGE);
|
|
48
|
+
void npmLatest(CLOUD_PACKAGE);
|
|
49
|
+
void latestTag(GUARD_REPO);
|
|
50
|
+
for (const catalog of GENERAL_CATALOGS)
|
|
51
|
+
void latestTag(catalog);
|
|
52
|
+
}
|
|
39
53
|
/**
|
|
40
54
|
* Runs the latest agent-kitchen with the same arguments, if npm has one newer than this.
|
|
41
55
|
* @param argv - The arguments.
|
|
42
56
|
* @returns Its exit code, or null if this one is the latest or npm cannot tell.
|
|
43
57
|
*/
|
|
44
|
-
function handOver(argv) {
|
|
58
|
+
async function handOver(argv) {
|
|
45
59
|
const own = ownVersion();
|
|
46
60
|
if (process.env[HANDED_OVER] !== undefined || !isRelease(own))
|
|
47
61
|
return null;
|
|
48
|
-
const latest = npmLatest(PACKAGE);
|
|
62
|
+
const latest = await npmLatest(PACKAGE);
|
|
49
63
|
if (latest === null || compareVersions(latest, own) <= 0)
|
|
50
64
|
return null;
|
|
51
|
-
process.stderr.write(`agent-kitchen:
|
|
65
|
+
process.stderr.write(`agent-kitchen: uso ${latest}, la última (esta es ${own}).\n`);
|
|
52
66
|
const result = spawnSync("npx", ["--yes", `${PACKAGE}@${latest}`, ...argv], { stdio: "inherit", env: { ...process.env, [HANDED_OVER]: own } });
|
|
53
67
|
return result.status ?? 1;
|
|
54
68
|
}
|
|
@@ -63,7 +77,7 @@ async function main(argv) {
|
|
|
63
77
|
const { values, positionals } = parseArgs({
|
|
64
78
|
args: argv,
|
|
65
79
|
allowPositionals: true,
|
|
66
|
-
options: { yes: { type: "boolean" }, "dry-run": { type: "boolean" }, skip: { type: "string" }, help: { type: "boolean", short: "h" }, version: { type: "boolean" } },
|
|
80
|
+
options: { yes: { type: "boolean" }, "dry-run": { type: "boolean" }, verbose: { type: "boolean", short: "v" }, skip: { type: "string" }, help: { type: "boolean", short: "h" }, version: { type: "boolean" } },
|
|
67
81
|
});
|
|
68
82
|
if (values.help === true) {
|
|
69
83
|
out.stdout(`${USAGE}\n`);
|
|
@@ -73,19 +87,20 @@ async function main(argv) {
|
|
|
73
87
|
out.stdout(`${ownVersion()}\n`);
|
|
74
88
|
return 0;
|
|
75
89
|
}
|
|
76
|
-
|
|
90
|
+
startLookups();
|
|
91
|
+
const handed = await handOver(argv);
|
|
77
92
|
if (handed !== null)
|
|
78
93
|
return handed;
|
|
79
94
|
const [command, ...rest] = positionals;
|
|
80
95
|
if (rest.length > 0 || (command !== undefined && command !== "doctor"))
|
|
81
|
-
throw new UsageError(`
|
|
96
|
+
throw new UsageError(`no existe el comando ${positionals.join(" ")}.`);
|
|
82
97
|
if (command === "doctor") {
|
|
83
|
-
if (values.yes !== undefined || values["dry-run"] !== undefined || values.skip !== undefined)
|
|
84
|
-
throw new UsageError("doctor
|
|
98
|
+
if (values.yes !== undefined || values["dry-run"] !== undefined || values.skip !== undefined || values.verbose !== undefined)
|
|
99
|
+
throw new UsageError("doctor no lleva opciones.");
|
|
85
100
|
return doctor(process.cwd(), out);
|
|
86
101
|
}
|
|
87
102
|
const skip = (values.skip ?? "").split(",").map((name) => name.trim()).filter((name) => name !== "");
|
|
88
|
-
return sync({ cwd: process.cwd(), yes: values.yes ?? false, dryRun: values["dry-run"] ?? false, skip }, out);
|
|
103
|
+
return sync({ cwd: process.cwd(), yes: values.yes ?? false, dryRun: values["dry-run"] ?? false, verbose: values.verbose ?? false, skip }, out);
|
|
89
104
|
}
|
|
90
105
|
try {
|
|
91
106
|
process.exitCode = await main(process.argv.slice(2));
|
|
@@ -11,6 +11,6 @@ import type { Output } from "../output.js";
|
|
|
11
11
|
* @param out - Where to write.
|
|
12
12
|
* @returns 0 if nothing failed, 1 if something did.
|
|
13
13
|
* @throws KitError outside a checkout or with a broken file.
|
|
14
|
-
* @example doctor(process.cwd(), out)
|
|
14
|
+
* @example await doctor(process.cwd(), out)
|
|
15
15
|
*/
|
|
16
|
-
export declare function doctor(cwd: string, out: Output): number
|
|
16
|
+
export declare function doctor(cwd: string, out: Output): Promise<number>;
|
package/dist/commands/doctor.js
CHANGED
|
@@ -11,11 +11,11 @@ const MARKS = { ok: "✓", fail: "✗", info: "·" };
|
|
|
11
11
|
* @param out - Where to write.
|
|
12
12
|
* @returns 0 if nothing failed, 1 if something did.
|
|
13
13
|
* @throws KitError outside a checkout or with a broken file.
|
|
14
|
-
* @example doctor(process.cwd(), out)
|
|
14
|
+
* @example await doctor(process.cwd(), out)
|
|
15
15
|
*/
|
|
16
|
-
export function doctor(cwd, out) {
|
|
16
|
+
export async function doctor(cwd, out) {
|
|
17
17
|
const root = mainCheckout(cwd, "agent-kitchen doctor");
|
|
18
|
-
const checks = [
|
|
18
|
+
const checks = (await Promise.all([issueFlowChecks(root), skillsChecks(root), cloudChecks(root), guardChecks(root)])).flat();
|
|
19
19
|
for (const check of checks)
|
|
20
20
|
out.stdout(`${MARKS[check.status]} ${check.name}: ${check.detail}\n`);
|
|
21
21
|
return checks.some((check) => check.status === "fail") ? 1 : 0;
|
package/dist/commands/sync.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export interface SyncOptions {
|
|
|
4
4
|
cwd: string;
|
|
5
5
|
yes: boolean;
|
|
6
6
|
dryRun: boolean;
|
|
7
|
+
verbose: boolean;
|
|
7
8
|
skip: string[];
|
|
8
9
|
}
|
|
9
10
|
/**
|
|
@@ -18,6 +19,6 @@ export declare function pieceNames(): string[];
|
|
|
18
19
|
* @param out - Where to write and ask.
|
|
19
20
|
* @returns 0, or 1 if a step failed or the person cancelled.
|
|
20
21
|
* @throws KitError outside the main checkout or with a broken file; UsageError for an unknown piece.
|
|
21
|
-
* @example await sync({ cwd, yes: false, dryRun: true, skip: [] }, out)
|
|
22
|
+
* @example await sync({ cwd, yes: false, dryRun: true, verbose: false, skip: [] }, out)
|
|
22
23
|
*/
|
|
23
24
|
export declare function sync(options: SyncOptions, out: Output): Promise<number>;
|
package/dist/commands/sync.js
CHANGED
|
@@ -1,33 +1,48 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @file `agent-kitchen [--yes] [--dry-run] [--skip <piece>,...]`: the one command.
|
|
3
|
-
* each piece (the issue flow, claude-cloud, claude-repo-guard, the skills)
|
|
4
|
-
*
|
|
5
|
-
* update what is behind. In a terminal
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
2
|
+
* @file `agent-kitchen [--yes] [--dry-run] [--verbose] [--skip <piece>,...]`: the one command.
|
|
3
|
+
* It looks at each piece (the issue flow, claude-cloud, claude-repo-guard, the skills) with
|
|
4
|
+
* every lookup at once, shows what the repo has, and offers the steps that install what is
|
|
5
|
+
* missing or update what is behind. In a terminal it asks first, all in one go: everything
|
|
6
|
+
* suggested, or pick the steps and the skills; whether to replace the repo's own skills.
|
|
7
|
+
* While the person answers, the tools download. Then the steps run in order (the flow
|
|
8
|
+
* first, the skills last, since they follow the flow's version), one line each, with the
|
|
9
|
+
* tools' output only when one fails; one that fails does not stop the rest. With `--yes`
|
|
10
|
+
* it does what is suggested without asking (and keeps the repo's own skills); without a
|
|
11
|
+
* terminal it only shows the steps.
|
|
9
12
|
*/
|
|
10
|
-
import {
|
|
13
|
+
import { spinner } from "@clack/prompts";
|
|
14
|
+
import { askPath, askReplace, askSkills, askSteps } from "../menu.js";
|
|
11
15
|
import { KitError, UsageError, warn } from "../output.js";
|
|
12
16
|
import { cloudStatus, NAME as CLOUD } from "../pieces/cloud.js";
|
|
13
17
|
import { guardStatus, NAME as GUARD } from "../pieces/guard.js";
|
|
14
18
|
import { issueFlowStatus, NAME as FLOW } from "../pieces/issue-flow.js";
|
|
15
|
-
import { NAME as SKILLS, skillsStatus } from "../pieces/skills.js";
|
|
19
|
+
import { keepOwn, NAME as SKILLS, ownInPick, skillsStatus } from "../pieces/skills.js";
|
|
16
20
|
import { mainCheckout, packageManager } from "../repo.js";
|
|
21
|
+
import { download, failedOutput, setVerbose } from "../tools.js";
|
|
17
22
|
/** Every piece, in the order its steps run. */
|
|
18
|
-
const PIECES = [
|
|
19
|
-
{ name: FLOW, status: issueFlowStatus },
|
|
20
|
-
{ name: CLOUD, status: cloudStatus },
|
|
21
|
-
{ name: GUARD, status: (root, before) => guardStatus(root, before.some((status) => status.name === FLOW && !status.installed && status.steps.length > 0)) },
|
|
22
|
-
{ name: SKILLS, status: skillsStatus },
|
|
23
|
-
];
|
|
23
|
+
const PIECES = [FLOW, CLOUD, GUARD, SKILLS];
|
|
24
24
|
/**
|
|
25
25
|
* The names `--skip` takes.
|
|
26
26
|
* @returns The pieces' names.
|
|
27
27
|
* @example pieceNames() // ["issue-flow", "claude-cloud", "claude-repo-guard", "skills"]
|
|
28
28
|
*/
|
|
29
29
|
export function pieceNames() {
|
|
30
|
-
return PIECES
|
|
30
|
+
return [...PIECES];
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Each piece's status, with every lookup at once. The guard's step depends on whether the
|
|
34
|
+
* flow is about to be installed, so it waits for the flow's status.
|
|
35
|
+
* @param root - The repo.
|
|
36
|
+
* @param skip - The pieces left out.
|
|
37
|
+
* @returns The statuses, in the pieces' order.
|
|
38
|
+
*/
|
|
39
|
+
async function statusesOf(root, skip) {
|
|
40
|
+
const flow = skip.includes(FLOW) ? null : issueFlowStatus(root);
|
|
41
|
+
const cloud = skip.includes(CLOUD) ? null : cloudStatus(root);
|
|
42
|
+
const skills = skip.includes(SKILLS) ? null : skillsStatus(root, !skip.includes(FLOW));
|
|
43
|
+
const guard = skip.includes(GUARD) ? null : (flow ?? Promise.resolve(null)).then((status) => guardStatus(root, status !== null && !status.installed && status.steps.length > 0));
|
|
44
|
+
const all = await Promise.all([flow, cloud, guard, skills]);
|
|
45
|
+
return all.flatMap((status) => status ?? []);
|
|
31
46
|
}
|
|
32
47
|
/**
|
|
33
48
|
* Prints what the repo has of each piece.
|
|
@@ -36,28 +51,145 @@ export function pieceNames() {
|
|
|
36
51
|
*/
|
|
37
52
|
function printStatus(statuses, out) {
|
|
38
53
|
const width = Math.max(...statuses.map((status) => status.name.length));
|
|
39
|
-
|
|
54
|
+
const stateWidth = Math.max(...statuses.map((status) => status.state.length));
|
|
55
|
+
out.stdout("\nEste repo tiene:\n");
|
|
40
56
|
for (const status of statuses)
|
|
41
|
-
out.stdout(` ${status.name.padEnd(width)} ${status.state} · ${status.summary}\n`);
|
|
57
|
+
out.stdout(` ${status.name.padEnd(width)} ${status.state.padEnd(stateWidth)} · ${status.summary}\n`);
|
|
58
|
+
out.stdout("\n");
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Leaves the repo's own skills out of a step's pick, saying so.
|
|
62
|
+
* @param root - The repo.
|
|
63
|
+
* @param step - A step that installs skills.
|
|
64
|
+
* @param out - Where to write.
|
|
65
|
+
*/
|
|
66
|
+
function keepOwnSkills(root, step, out) {
|
|
67
|
+
if (step.skills === undefined)
|
|
68
|
+
return;
|
|
69
|
+
const own = ownInPick(root, step.skills);
|
|
70
|
+
if (own.length === 0)
|
|
71
|
+
return;
|
|
72
|
+
Object.assign(step.skills, keepOwn(step.skills, own));
|
|
73
|
+
out.stdout(`Se dejan fuera las skills que el repo ya tiene como propias: ${own.join(", ")}. Para reemplazarlas, corre agent-kitchen en una terminal.\n`);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Asks, for a step that installs skills, which ones, and whether to replace the repo's own.
|
|
77
|
+
* @param root - The repo.
|
|
78
|
+
* @param step - The step.
|
|
79
|
+
* @param chooseSkills - Whether to show the skills menu, or keep the step's pick.
|
|
80
|
+
* @returns False if the person cancelled.
|
|
81
|
+
*/
|
|
82
|
+
async function askAboutSkills(root, step, chooseSkills) {
|
|
83
|
+
const pick = step.skills;
|
|
84
|
+
if (pick === undefined)
|
|
85
|
+
return true;
|
|
86
|
+
if (chooseSkills) {
|
|
87
|
+
const picked = await askSkills(pick.candidates, pick.picked.length > 0 ? pick.picked : pick.candidates.map((candidate) => candidate.id));
|
|
88
|
+
if (picked === null)
|
|
89
|
+
return false;
|
|
90
|
+
pick.picked = picked;
|
|
91
|
+
}
|
|
92
|
+
const own = ownInPick(root, pick);
|
|
93
|
+
if (own.length === 0)
|
|
94
|
+
return true;
|
|
95
|
+
const replace = await askReplace(own);
|
|
96
|
+
if (replace === null)
|
|
97
|
+
return false;
|
|
98
|
+
if (replace)
|
|
99
|
+
pick.replace = own;
|
|
100
|
+
else
|
|
101
|
+
Object.assign(pick, keepOwn(pick, own));
|
|
102
|
+
return true;
|
|
42
103
|
}
|
|
43
104
|
/**
|
|
44
|
-
* The steps to run: the
|
|
105
|
+
* The steps to run: the suggested ones with `--yes`, what the person picks in a terminal,
|
|
106
|
+
* or none (only shown) with `--dry-run` or without a terminal.
|
|
107
|
+
* @param root - The repo.
|
|
45
108
|
* @param steps - The steps offered.
|
|
46
109
|
* @param options - The options.
|
|
47
110
|
* @param out - Where to write.
|
|
48
|
-
* @returns The steps, or null if nothing should run.
|
|
111
|
+
* @returns The steps, "leave" if the person chose to leave, or null if nothing should run (only shown, or cancelled).
|
|
49
112
|
*/
|
|
50
|
-
async function chooseSteps(steps, options, out) {
|
|
51
|
-
|
|
52
|
-
|
|
113
|
+
async function chooseSteps(root, steps, options, out) {
|
|
114
|
+
const suggested = steps.filter((step) => step.checked);
|
|
115
|
+
if (options.yes) {
|
|
116
|
+
if (suggested.length === 0)
|
|
117
|
+
out.stdout(`Todo al día. Opcional, para elegir en una terminal: ${steps.map((step) => step.label).join("; ")}.\n`);
|
|
118
|
+
for (const step of suggested)
|
|
119
|
+
keepOwnSkills(root, step, out);
|
|
120
|
+
return suggested;
|
|
121
|
+
}
|
|
53
122
|
if (options.dryRun || !out.interactive) {
|
|
54
|
-
|
|
123
|
+
const hint = options.dryRun ? "" : " (para hacerlos, corre agent-kitchen en una terminal, o con --yes)";
|
|
124
|
+
out.stdout(`Pasos sugeridos${hint}:\n${steps.map((step) => ` [${step.checked ? "x" : " "}] ${step.label}\n`).join("")}`);
|
|
55
125
|
return null;
|
|
56
126
|
}
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
127
|
+
for (const step of suggested)
|
|
128
|
+
for (const spec of step.downloads)
|
|
129
|
+
void download(root, spec);
|
|
130
|
+
const path = await askPath(suggested.length);
|
|
131
|
+
if (path === "leave")
|
|
132
|
+
return "leave";
|
|
133
|
+
const chosen = path === "all" ? suggested : await askSteps(steps);
|
|
134
|
+
if (chosen === null)
|
|
135
|
+
return null;
|
|
136
|
+
for (const step of chosen) {
|
|
137
|
+
if (!(await askAboutSkills(root, step, path === "pick")))
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
return chosen;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* How progress is shown: a spinner in a terminal (unless the tools print with
|
|
144
|
+
* `--verbose`), plain lines otherwise. Only for the steps, after every question: a spinner
|
|
145
|
+
* shown before a question leaves the second question deaf to the keyboard (@clack/prompts
|
|
146
|
+
* 1.8.1).
|
|
147
|
+
* @param out - Where to write.
|
|
148
|
+
* @param verbose - Whether the tools print on the terminal.
|
|
149
|
+
* @returns The progress.
|
|
150
|
+
*/
|
|
151
|
+
function progressFor(out, verbose) {
|
|
152
|
+
if (out.interactive && !verbose) {
|
|
153
|
+
const spin = spinner();
|
|
154
|
+
return { start: (text) => spin.start(text), stop: (text, ok) => (ok ? spin.stop(text) : spin.error(text)) };
|
|
155
|
+
}
|
|
156
|
+
return { start: (text) => out.stdout(`→ ${text}\n`), stop: (text, ok) => out.stdout(`${ok ? "✓" : "✗"} ${text}\n`) };
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Runs one step and shows how it went: its notes, and what to do if it failed.
|
|
160
|
+
* @param root - The repo.
|
|
161
|
+
* @param step - The step.
|
|
162
|
+
* @param progress - How progress is shown.
|
|
163
|
+
* @param out - Where to write.
|
|
164
|
+
* @returns Whether it worked.
|
|
165
|
+
* @throws What the step throws that is not a KitError.
|
|
166
|
+
*/
|
|
167
|
+
async function runStep(root, step, progress, out) {
|
|
168
|
+
const notes = [];
|
|
169
|
+
const failures = [];
|
|
170
|
+
const context = {
|
|
171
|
+
root,
|
|
172
|
+
note: (text) => notes.push(text),
|
|
173
|
+
fail: (text, output) => failures.push(` ${text}\n${output === undefined ? "" : failedOutput(output)}`),
|
|
174
|
+
};
|
|
175
|
+
progress.start(step.label);
|
|
176
|
+
await Promise.all(step.downloads.map((spec) => download(root, spec)));
|
|
177
|
+
let ok;
|
|
178
|
+
try {
|
|
179
|
+
ok = await step.run(context);
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
if (!(error instanceof KitError))
|
|
183
|
+
throw error;
|
|
184
|
+
failures.push(` ${error.message}\n`);
|
|
185
|
+
ok = false;
|
|
186
|
+
}
|
|
187
|
+
progress.stop(step.label, ok);
|
|
188
|
+
for (const note of notes)
|
|
189
|
+
out.stdout(` · ${note}\n`);
|
|
190
|
+
for (const failure of failures)
|
|
191
|
+
out.stderr(failure);
|
|
192
|
+
return ok;
|
|
61
193
|
}
|
|
62
194
|
/**
|
|
63
195
|
* Runs the command; see the file's header.
|
|
@@ -65,47 +197,47 @@ async function chooseSteps(steps, options, out) {
|
|
|
65
197
|
* @param out - Where to write and ask.
|
|
66
198
|
* @returns 0, or 1 if a step failed or the person cancelled.
|
|
67
199
|
* @throws KitError outside the main checkout or with a broken file; UsageError for an unknown piece.
|
|
68
|
-
* @example await sync({ cwd, yes: false, dryRun: true, skip: [] }, out)
|
|
200
|
+
* @example await sync({ cwd, yes: false, dryRun: true, verbose: false, skip: [] }, out)
|
|
69
201
|
*/
|
|
70
202
|
export async function sync(options, out) {
|
|
71
203
|
const unknown = options.skip.find((name) => !pieceNames().includes(name));
|
|
72
204
|
if (unknown !== undefined)
|
|
73
|
-
throw new UsageError(`
|
|
205
|
+
throw new UsageError(`no existe la pieza ${unknown}; son ${pieceNames().join(", ")}.`);
|
|
74
206
|
if (options.yes && options.dryRun)
|
|
75
|
-
throw new UsageError("
|
|
207
|
+
throw new UsageError("usa --yes o --dry-run, no los dos.");
|
|
76
208
|
const root = mainCheckout(options.cwd, "agent-kitchen");
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
209
|
+
setVerbose(options.verbose);
|
|
210
|
+
if (out.interactive)
|
|
211
|
+
out.stdout("Revisando el repo y las últimas versiones…\n");
|
|
212
|
+
const statuses = await statusesOf(root, options.skip);
|
|
80
213
|
printStatus(statuses, out);
|
|
81
214
|
const steps = statuses.flatMap((status) => status.steps);
|
|
82
215
|
if (steps.length === 0) {
|
|
83
|
-
out.stdout("
|
|
216
|
+
out.stdout("Todo al día: no hay nada que instalar ni actualizar.\n");
|
|
84
217
|
return 0;
|
|
85
218
|
}
|
|
86
|
-
const chosen = await chooseSteps(steps, options, out);
|
|
87
|
-
if (chosen ===
|
|
219
|
+
const chosen = await chooseSteps(root, steps, options, out);
|
|
220
|
+
if (chosen === "leave") {
|
|
221
|
+
out.stdout("No se cambió nada.\n");
|
|
222
|
+
return 0;
|
|
223
|
+
}
|
|
224
|
+
if (chosen === null) {
|
|
225
|
+
if (!options.dryRun && out.interactive)
|
|
226
|
+
warn(out, "no se cambió nada.");
|
|
88
227
|
return options.dryRun || !out.interactive ? 0 : 1;
|
|
228
|
+
}
|
|
89
229
|
if (chosen.length === 0) {
|
|
90
|
-
|
|
230
|
+
if (!options.yes)
|
|
231
|
+
out.stdout("No elegiste nada; no se cambió nada.\n");
|
|
91
232
|
return 0;
|
|
92
233
|
}
|
|
234
|
+
const progress = progressFor(out, options.verbose);
|
|
93
235
|
const failed = [];
|
|
94
236
|
for (const step of chosen) {
|
|
95
|
-
|
|
96
|
-
let ok;
|
|
97
|
-
try {
|
|
98
|
-
ok = step.run({ root, yes: options.yes || !out.interactive, out });
|
|
99
|
-
}
|
|
100
|
-
catch (error) {
|
|
101
|
-
if (!(error instanceof KitError))
|
|
102
|
-
throw error;
|
|
103
|
-
warn(out, error.message);
|
|
104
|
-
ok = false;
|
|
105
|
-
}
|
|
106
|
-
if (!ok)
|
|
237
|
+
if (!(await runStep(root, step, progress, out)))
|
|
107
238
|
failed.push(step.label);
|
|
108
239
|
}
|
|
109
|
-
|
|
240
|
+
const next = `Falta: corre '${packageManager(root)} install', revisa el diff y haz commit.`;
|
|
241
|
+
out.stdout(`\n${failed.length === 0 ? "Listo." : `Terminó, pero fallaron: ${failed.join("; ")}.`} ${next}\n`);
|
|
110
242
|
return failed.length === 0 ? 0 : 1;
|
|
111
243
|
}
|
package/dist/menu.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import type { Step } from "./piece.js";
|
|
1
|
+
import type { Candidate, Step } from "./piece.js";
|
|
2
|
+
/** What the person wants after seeing the table. */
|
|
3
|
+
export type Path = "all" | "pick" | "leave";
|
|
2
4
|
/**
|
|
3
5
|
* The menu's options: each step's index and label, which names its piece.
|
|
4
6
|
* @param steps - The steps.
|
|
@@ -12,6 +14,24 @@ export declare function menuOptions(steps: Step[]): {
|
|
|
12
14
|
}[];
|
|
13
15
|
initialValues: number[];
|
|
14
16
|
};
|
|
17
|
+
/**
|
|
18
|
+
* The skills menu's options: grouped by catalog, each with its description cut short.
|
|
19
|
+
* @param candidates - What the catalogs offer.
|
|
20
|
+
* @returns The groups.
|
|
21
|
+
* @example Object.keys(skillOptions(candidates)) // ["ramon-sg/issue-flow", "ramon-sg/agent-skills"]
|
|
22
|
+
*/
|
|
23
|
+
export declare function skillOptions(candidates: Candidate[]): Record<string, {
|
|
24
|
+
value: string;
|
|
25
|
+
label: string;
|
|
26
|
+
hint?: string;
|
|
27
|
+
}[]>;
|
|
28
|
+
/**
|
|
29
|
+
* Asks what to do: everything suggested, pick step by step, or leave.
|
|
30
|
+
* @param count - How many steps are suggested; with none, the option to do them is not shown.
|
|
31
|
+
* @returns The answer; "leave" if the person cancelled.
|
|
32
|
+
* @example await askPath(4) // "all"
|
|
33
|
+
*/
|
|
34
|
+
export declare function askPath(count: number): Promise<Path>;
|
|
15
35
|
/**
|
|
16
36
|
* Asks which steps to run.
|
|
17
37
|
* @param steps - The steps.
|
|
@@ -19,3 +39,18 @@ export declare function menuOptions(steps: Step[]): {
|
|
|
19
39
|
* @example await askSteps(steps)
|
|
20
40
|
*/
|
|
21
41
|
export declare function askSteps(steps: Step[]): Promise<Step[] | null>;
|
|
42
|
+
/**
|
|
43
|
+
* Asks which skills to install.
|
|
44
|
+
* @param candidates - What the catalogs offer.
|
|
45
|
+
* @param ticked - The ids ticked at the start.
|
|
46
|
+
* @returns The picked ids, or null if the person cancelled.
|
|
47
|
+
* @example await askSkills(candidates, ["catalog:ramon-sg/agent-skills/planning"])
|
|
48
|
+
*/
|
|
49
|
+
export declare function askSkills(candidates: Candidate[], ticked: string[]): Promise<string[] | null>;
|
|
50
|
+
/**
|
|
51
|
+
* Asks whether the catalog's skills replace the repo's own ones with the same name.
|
|
52
|
+
* @param own - Their names.
|
|
53
|
+
* @returns True to replace them, false to keep them, or null if the person cancelled.
|
|
54
|
+
* @example await askReplace(["planning"])
|
|
55
|
+
*/
|
|
56
|
+
export declare function askReplace(own: string[]): Promise<boolean | null>;
|