agent-kitchen 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +77 -2
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +105 -0
- package/dist/commands/doctor.d.ts +16 -0
- package/dist/commands/doctor.js +22 -0
- package/dist/commands/sync.d.ts +23 -0
- package/dist/commands/sync.js +111 -0
- package/dist/menu.d.ts +21 -0
- package/dist/menu.js +31 -0
- package/dist/output.d.ts +29 -0
- package/dist/output.js +31 -0
- package/dist/piece.d.ts +49 -0
- package/dist/piece.js +14 -0
- package/dist/pieces/cloud.d.ts +17 -0
- package/dist/pieces/cloud.js +93 -0
- package/dist/pieces/guard.d.ts +40 -0
- package/dist/pieces/guard.js +173 -0
- package/dist/pieces/issue-flow.d.ts +32 -0
- package/dist/pieces/issue-flow.js +147 -0
- package/dist/pieces/skills.d.ts +17 -0
- package/dist/pieces/skills.js +155 -0
- package/dist/repo.d.ts +48 -0
- package/dist/repo.js +117 -0
- package/dist/skills-files.d.ts +62 -0
- package/dist/skills-files.js +93 -0
- package/dist/tools.d.ts +48 -0
- package/dist/tools.js +85 -0
- package/package.json +20 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ramón Soto
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,78 @@
|
|
|
1
|
-
#
|
|
1
|
+
# agent-kitchen
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Un solo comando para instalar, actualizar y revisar lo que un repo usa para trabajar con agentes: el flujo de issues con sus skills, las skills generales, las sesiones de Claude en la nube y las barandas de seguridad.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npx agent-kitchen # muestra qué hay, qué falta y qué está atrasado, y pregunta
|
|
7
|
+
npx agent-kitchen doctor # revisa que todo funcione, sin cambiar nada
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Al arrancar revisa si npm tiene una versión más nueva de sí mismo. Si la hay, corre esa con los mismos argumentos, así que `npx` nunca usa una copia vieja de su caché.
|
|
11
|
+
|
|
12
|
+
## En simple
|
|
13
|
+
|
|
14
|
+
- Muestra cada pieza con la versión que tiene el repo y la última que existe.
|
|
15
|
+
- Ofrece instalar lo que falta y actualizar lo que está atrasado, en un menú de casillas. Todo viene marcado; desmarcas lo que no quieres.
|
|
16
|
+
- 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
|
+
- Correrlo de nuevo no rompe nada: si todo está al día, lo dice y termina.
|
|
18
|
+
|
|
19
|
+
## Las piezas
|
|
20
|
+
|
|
21
|
+
| Pieza | Qué es | Versión del repo | Última | Instalar | Actualizar |
|
|
22
|
+
|---|---|---|---|---|---|
|
|
23
|
+
| `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
|
+
| `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 | `install` en esa tag | `install` en la tag nueva |
|
|
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 | El menú de `skills-manager` | `skills-manager update` |
|
|
27
|
+
|
|
28
|
+
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
|
+
|
|
30
|
+
### Cómo se conectan
|
|
31
|
+
|
|
32
|
+
- **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
|
+
- **Skills nuevas:** si un catálogo trae skills que el repo no tiene, aparecen en el menú sin marcar. Al marcarlas se abre el menú de `skills-manager` para elegir.
|
|
34
|
+
- **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
|
+
- **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
|
+
|
|
37
|
+
## Uso
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
npx agent-kitchen # menú, en una terminal
|
|
41
|
+
npx agent-kitchen --dry-run # solo muestra qué ofrecería
|
|
42
|
+
npx agent-kitchen --yes # hace lo sugerido sin preguntar
|
|
43
|
+
npx agent-kitchen --skip claude-cloud # deja fuera una pieza (o varias, con comas)
|
|
44
|
+
npx agent-kitchen doctor # revisa cada pieza con su propio doctor
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
- **Sin terminal y sin `--yes`** (por ejemplo, desde un agente), solo muestra lo que ofrecería.
|
|
48
|
+
- **Corre en el checkout principal,** no en un worktree, y desde tu terminal. Dentro de una sesión de Claude, el sandbox de `claude-repo-guard` no deja escribir `.claude/settings.json`.
|
|
49
|
+
- **Al terminar,** corre el `install` de tu gestor de paquetes, revisa el diff y haz commit.
|
|
50
|
+
|
|
51
|
+
**`doctor`** corre el doctor de cada herramienta en la versión del repo: `issue-flow doctor --json`, `skills-manager check` y `claude-repo-guard doctor --json`. Además revisa que las skills del flujo estén en la tag de su CLI, que `.claude/cloud.json` fije una versión exacta y, en macOS, `weakerNetworkIsolation`. Lo que no está instalado o tiene una versión nueva solo se anota con `·`. Sale con 1 si algo falla (`✗`).
|
|
52
|
+
|
|
53
|
+
## Sumar una pieza
|
|
54
|
+
|
|
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`.
|
|
56
|
+
|
|
57
|
+
## Desarrollo
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
npm install
|
|
61
|
+
npm test # compila y corre los tests
|
|
62
|
+
```
|
|
63
|
+
|
|
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. El menú no se prueba renderizado: se prueba `menuOptions` y se corrió a mano en una terminal.
|
|
65
|
+
|
|
66
|
+
## Publicar una versión
|
|
67
|
+
|
|
68
|
+
Una versión es una release `vX.Y.Z` en GitHub, con el mismo número que `package.json`. Al publicarla, GitHub Actions corre los tests y publica en npm con procedencia (`.github/workflows/publish.yml`).
|
|
69
|
+
|
|
70
|
+
La primera versión va a mano, porque npm solo deja configurar la publicación desde GitHub en un paquete que ya existe:
|
|
71
|
+
|
|
72
|
+
1. `npm login` y, desde `main`, `npm publish --provenance=false`.
|
|
73
|
+
2. En npmjs.com, en el paquete, Settings → Trusted publishing: GitHub Actions, repo `ramon-sg/agent-kitchen`, workflow `publish.yml`.
|
|
74
|
+
3. Pushear la tag de esa versión sin crear una release: una release volvería a publicar la misma versión y fallaría.
|
|
75
|
+
|
|
76
|
+
## Licencia
|
|
77
|
+
|
|
78
|
+
MIT
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @file Entry point of `agent-kitchen`: parses the arguments and runs the command. Before
|
|
4
|
+
* that, if npm has a newer agent-kitchen than the one running (npx may run an old copy from
|
|
5
|
+
* its cache), it runs that one instead with the same arguments. Usage errors exit 2 with
|
|
6
|
+
* the usage text; errors of the repo exit 1 with their message.
|
|
7
|
+
*/
|
|
8
|
+
import { spawnSync } from "node:child_process";
|
|
9
|
+
import { readFileSync } from "node:fs";
|
|
10
|
+
import { parseArgs } from "node:util";
|
|
11
|
+
import { doctor } from "./commands/doctor.js";
|
|
12
|
+
import { pieceNames, sync } from "./commands/sync.js";
|
|
13
|
+
import { KitError, processOutput, UsageError } from "./output.js";
|
|
14
|
+
import { compareVersions, isRelease, npmLatest } from "./tools.js";
|
|
15
|
+
/** This package on npm. */
|
|
16
|
+
const PACKAGE = "agent-kitchen";
|
|
17
|
+
/** Set in the run that a newer agent-kitchen was handed to, so it never hands over again. */
|
|
18
|
+
const HANDED_OVER = "AGENT_KITCHEN_HANDED_OVER";
|
|
19
|
+
/** Usage text. */
|
|
20
|
+
const USAGE = `Usage:
|
|
21
|
+
agent-kitchen [--yes | --dry-run] [--skip <piece>,...]
|
|
22
|
+
agent-kitchen doctor
|
|
23
|
+
|
|
24
|
+
agent-kitchen looks at each piece of the repo's agent tooling (${pieceNames().join(", ")}),
|
|
25
|
+
shows what the repo has and the latest of each, and offers to install what is missing and
|
|
26
|
+
update what is behind: in a terminal, a menu where you untick what you do not want. --yes
|
|
27
|
+
runs what it suggests without asking; without a terminal it only shows it. Run it in the
|
|
28
|
+
main checkout, from your terminal.
|
|
29
|
+
|
|
30
|
+
doctor runs each piece's own checks at the repo's version, and changes nothing.`;
|
|
31
|
+
/**
|
|
32
|
+
* This package's version.
|
|
33
|
+
* @returns The version in its package.json.
|
|
34
|
+
*/
|
|
35
|
+
function ownVersion() {
|
|
36
|
+
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
37
|
+
return pkg.version ?? "unknown";
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Runs the latest agent-kitchen with the same arguments, if npm has one newer than this.
|
|
41
|
+
* @param argv - The arguments.
|
|
42
|
+
* @returns Its exit code, or null if this one is the latest or npm cannot tell.
|
|
43
|
+
*/
|
|
44
|
+
function handOver(argv) {
|
|
45
|
+
const own = ownVersion();
|
|
46
|
+
if (process.env[HANDED_OVER] !== undefined || !isRelease(own))
|
|
47
|
+
return null;
|
|
48
|
+
const latest = npmLatest(PACKAGE);
|
|
49
|
+
if (latest === null || compareVersions(latest, own) <= 0)
|
|
50
|
+
return null;
|
|
51
|
+
process.stderr.write(`agent-kitchen: running ${latest}, the latest (this is ${own}).\n`);
|
|
52
|
+
const result = spawnSync("npx", ["--yes", `${PACKAGE}@${latest}`, ...argv], { stdio: "inherit", env: { ...process.env, [HANDED_OVER]: own } });
|
|
53
|
+
return result.status ?? 1;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Runs the CLI.
|
|
57
|
+
* @param argv - The arguments, without node and the script.
|
|
58
|
+
* @returns The exit code.
|
|
59
|
+
* @throws UsageError for unknown arguments; KitError from the command.
|
|
60
|
+
*/
|
|
61
|
+
async function main(argv) {
|
|
62
|
+
const out = processOutput();
|
|
63
|
+
const { values, positionals } = parseArgs({
|
|
64
|
+
args: argv,
|
|
65
|
+
allowPositionals: true,
|
|
66
|
+
options: { yes: { type: "boolean" }, "dry-run": { type: "boolean" }, skip: { type: "string" }, help: { type: "boolean", short: "h" }, version: { type: "boolean" } },
|
|
67
|
+
});
|
|
68
|
+
if (values.help === true) {
|
|
69
|
+
out.stdout(`${USAGE}\n`);
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
if (values.version === true) {
|
|
73
|
+
out.stdout(`${ownVersion()}\n`);
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
const handed = handOver(argv);
|
|
77
|
+
if (handed !== null)
|
|
78
|
+
return handed;
|
|
79
|
+
const [command, ...rest] = positionals;
|
|
80
|
+
if (rest.length > 0 || (command !== undefined && command !== "doctor"))
|
|
81
|
+
throw new UsageError(`unknown command ${positionals.join(" ")}.`);
|
|
82
|
+
if (command === "doctor") {
|
|
83
|
+
if (values.yes !== undefined || values["dry-run"] !== undefined || values.skip !== undefined)
|
|
84
|
+
throw new UsageError("doctor takes no flags.");
|
|
85
|
+
return doctor(process.cwd(), out);
|
|
86
|
+
}
|
|
87
|
+
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);
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
process.exitCode = await main(process.argv.slice(2));
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
if (error instanceof UsageError || (error instanceof TypeError && "code" in error && String(error.code).startsWith("ERR_PARSE_ARGS"))) {
|
|
95
|
+
process.stderr.write(`agent-kitchen: ${error.message}\n\n${USAGE}\n`);
|
|
96
|
+
process.exitCode = 2;
|
|
97
|
+
}
|
|
98
|
+
else if (error instanceof KitError) {
|
|
99
|
+
process.stderr.write(`agent-kitchen: ${error.message}\n`);
|
|
100
|
+
process.exitCode = 1;
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file `agent-kitchen doctor`: whether each piece the repo has works, with the doctor of each
|
|
3
|
+
* tool at the repo's own version: the issue flow (and its skills at its tag),
|
|
4
|
+
* `skills-manager check`, the version `.claude/cloud.json` pins, and claude-repo-guard. It
|
|
5
|
+
* changes nothing. A piece the repo does not have, or a newer release, is only noted.
|
|
6
|
+
*/
|
|
7
|
+
import type { Output } from "../output.js";
|
|
8
|
+
/**
|
|
9
|
+
* Checks the repo; see the file's header.
|
|
10
|
+
* @param cwd - Where it runs.
|
|
11
|
+
* @param out - Where to write.
|
|
12
|
+
* @returns 0 if nothing failed, 1 if something did.
|
|
13
|
+
* @throws KitError outside a checkout or with a broken file.
|
|
14
|
+
* @example doctor(process.cwd(), out)
|
|
15
|
+
*/
|
|
16
|
+
export declare function doctor(cwd: string, out: Output): number;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { cloudChecks } from "../pieces/cloud.js";
|
|
2
|
+
import { guardChecks } from "../pieces/guard.js";
|
|
3
|
+
import { issueFlowChecks } from "../pieces/issue-flow.js";
|
|
4
|
+
import { skillsChecks } from "../pieces/skills.js";
|
|
5
|
+
import { mainCheckout } from "../repo.js";
|
|
6
|
+
/** The mark of each check's status. */
|
|
7
|
+
const MARKS = { ok: "✓", fail: "✗", info: "·" };
|
|
8
|
+
/**
|
|
9
|
+
* Checks the repo; see the file's header.
|
|
10
|
+
* @param cwd - Where it runs.
|
|
11
|
+
* @param out - Where to write.
|
|
12
|
+
* @returns 0 if nothing failed, 1 if something did.
|
|
13
|
+
* @throws KitError outside a checkout or with a broken file.
|
|
14
|
+
* @example doctor(process.cwd(), out)
|
|
15
|
+
*/
|
|
16
|
+
export function doctor(cwd, out) {
|
|
17
|
+
const root = mainCheckout(cwd, "agent-kitchen doctor");
|
|
18
|
+
const checks = [...issueFlowChecks(root), ...skillsChecks(root), ...cloudChecks(root), ...guardChecks(root)];
|
|
19
|
+
for (const check of checks)
|
|
20
|
+
out.stdout(`${MARKS[check.status]} ${check.name}: ${check.detail}\n`);
|
|
21
|
+
return checks.some((check) => check.status === "fail") ? 1 : 0;
|
|
22
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type Output } from "../output.js";
|
|
2
|
+
/** Options of the command. */
|
|
3
|
+
export interface SyncOptions {
|
|
4
|
+
cwd: string;
|
|
5
|
+
yes: boolean;
|
|
6
|
+
dryRun: boolean;
|
|
7
|
+
skip: string[];
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The names `--skip` takes.
|
|
11
|
+
* @returns The pieces' names.
|
|
12
|
+
* @example pieceNames() // ["issue-flow", "claude-cloud", "claude-repo-guard", "skills"]
|
|
13
|
+
*/
|
|
14
|
+
export declare function pieceNames(): string[];
|
|
15
|
+
/**
|
|
16
|
+
* Runs the command; see the file's header.
|
|
17
|
+
* @param options - The options.
|
|
18
|
+
* @param out - Where to write and ask.
|
|
19
|
+
* @returns 0, or 1 if a step failed or the person cancelled.
|
|
20
|
+
* @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
|
+
*/
|
|
23
|
+
export declare function sync(options: SyncOptions, out: Output): Promise<number>;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file `agent-kitchen [--yes] [--dry-run] [--skip <piece>,...]`: the one command. It looks at
|
|
3
|
+
* each piece (the issue flow, claude-cloud, claude-repo-guard, the skills), shows what the
|
|
4
|
+
* repo has and the latest of each, and offers the steps that install what is missing or
|
|
5
|
+
* update what is behind. In a terminal the person unticks what they do not want; with
|
|
6
|
+
* `--yes` it runs the ticked ones; without a terminal it only shows them. The steps run in
|
|
7
|
+
* order (the flow first, the skills last, since they follow the flow's version) and one that
|
|
8
|
+
* fails does not stop the rest.
|
|
9
|
+
*/
|
|
10
|
+
import { askSteps } from "../menu.js";
|
|
11
|
+
import { KitError, UsageError, warn } from "../output.js";
|
|
12
|
+
import { cloudStatus, NAME as CLOUD } from "../pieces/cloud.js";
|
|
13
|
+
import { guardStatus, NAME as GUARD } from "../pieces/guard.js";
|
|
14
|
+
import { issueFlowStatus, NAME as FLOW } from "../pieces/issue-flow.js";
|
|
15
|
+
import { NAME as SKILLS, skillsStatus } from "../pieces/skills.js";
|
|
16
|
+
import { mainCheckout, packageManager } from "../repo.js";
|
|
17
|
+
/** 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
|
+
];
|
|
24
|
+
/**
|
|
25
|
+
* The names `--skip` takes.
|
|
26
|
+
* @returns The pieces' names.
|
|
27
|
+
* @example pieceNames() // ["issue-flow", "claude-cloud", "claude-repo-guard", "skills"]
|
|
28
|
+
*/
|
|
29
|
+
export function pieceNames() {
|
|
30
|
+
return PIECES.map((piece) => piece.name);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Prints what the repo has of each piece.
|
|
34
|
+
* @param statuses - The pieces' statuses.
|
|
35
|
+
* @param out - Where to write.
|
|
36
|
+
*/
|
|
37
|
+
function printStatus(statuses, out) {
|
|
38
|
+
const width = Math.max(...statuses.map((status) => status.name.length));
|
|
39
|
+
out.stdout("This repo has:\n");
|
|
40
|
+
for (const status of statuses)
|
|
41
|
+
out.stdout(` ${status.name.padEnd(width)} ${status.state} · ${status.summary}\n`);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The steps to run: the ticked ones with `--yes`, the picked ones in a terminal, or none.
|
|
45
|
+
* @param steps - The steps offered.
|
|
46
|
+
* @param options - The options.
|
|
47
|
+
* @param out - Where to write.
|
|
48
|
+
* @returns The steps, or null if nothing should run.
|
|
49
|
+
*/
|
|
50
|
+
async function chooseSteps(steps, options, out) {
|
|
51
|
+
if (options.yes)
|
|
52
|
+
return steps.filter((step) => step.checked);
|
|
53
|
+
if (options.dryRun || !out.interactive) {
|
|
54
|
+
out.stdout(`It would offer${options.dryRun ? "" : " (run it in a terminal to pick, or with --yes)"}:\n${steps.map((step) => ` [${step.checked ? "x" : " "}] ${step.label}\n`).join("")}`);
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const picked = await askSteps(steps);
|
|
58
|
+
if (picked === null)
|
|
59
|
+
warn(out, "cancelled; nothing changed.");
|
|
60
|
+
return picked;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Runs the command; see the file's header.
|
|
64
|
+
* @param options - The options.
|
|
65
|
+
* @param out - Where to write and ask.
|
|
66
|
+
* @returns 0, or 1 if a step failed or the person cancelled.
|
|
67
|
+
* @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)
|
|
69
|
+
*/
|
|
70
|
+
export async function sync(options, out) {
|
|
71
|
+
const unknown = options.skip.find((name) => !pieceNames().includes(name));
|
|
72
|
+
if (unknown !== undefined)
|
|
73
|
+
throw new UsageError(`unknown piece ${unknown}; there are ${pieceNames().join(", ")}.`);
|
|
74
|
+
if (options.yes && options.dryRun)
|
|
75
|
+
throw new UsageError("use --yes or --dry-run, not both.");
|
|
76
|
+
const root = mainCheckout(options.cwd, "agent-kitchen");
|
|
77
|
+
const statuses = [];
|
|
78
|
+
for (const piece of PIECES.filter((item) => !options.skip.includes(item.name)))
|
|
79
|
+
statuses.push(piece.status(root, statuses));
|
|
80
|
+
printStatus(statuses, out);
|
|
81
|
+
const steps = statuses.flatMap((status) => status.steps);
|
|
82
|
+
if (steps.length === 0) {
|
|
83
|
+
out.stdout("Nothing to install or update.\n");
|
|
84
|
+
return 0;
|
|
85
|
+
}
|
|
86
|
+
const chosen = await chooseSteps(steps, options, out);
|
|
87
|
+
if (chosen === null)
|
|
88
|
+
return options.dryRun || !out.interactive ? 0 : 1;
|
|
89
|
+
if (chosen.length === 0) {
|
|
90
|
+
out.stdout("Nothing picked; nothing changed.\n");
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
93
|
+
const failed = [];
|
|
94
|
+
for (const step of chosen) {
|
|
95
|
+
out.stdout(`\n→ ${step.label}\n`);
|
|
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)
|
|
107
|
+
failed.push(step.label);
|
|
108
|
+
}
|
|
109
|
+
out.stdout(`\n${failed.length === 0 ? "Done." : `Done, but these failed: ${failed.join("; ")}.`} Next: run '${packageManager(root)} install', review the diff and commit it.\n`);
|
|
110
|
+
return failed.length === 0 ? 0 : 1;
|
|
111
|
+
}
|
package/dist/menu.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Step } from "./piece.js";
|
|
2
|
+
/**
|
|
3
|
+
* The menu's options: each step's index and label, which names its piece.
|
|
4
|
+
* @param steps - The steps.
|
|
5
|
+
* @returns The options and the ones ticked at the start.
|
|
6
|
+
* @example menuOptions(steps).initialValues // [0, 1]
|
|
7
|
+
*/
|
|
8
|
+
export declare function menuOptions(steps: Step[]): {
|
|
9
|
+
options: {
|
|
10
|
+
value: number;
|
|
11
|
+
label: string;
|
|
12
|
+
}[];
|
|
13
|
+
initialValues: number[];
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Asks which steps to run.
|
|
17
|
+
* @param steps - The steps.
|
|
18
|
+
* @returns The picked ones, in their order, or null if the person cancelled.
|
|
19
|
+
* @example await askSteps(steps)
|
|
20
|
+
*/
|
|
21
|
+
export declare function askSteps(steps: Step[]): Promise<Step[] | null>;
|
package/dist/menu.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The checkbox menu of the steps (`@clack/prompts`): one line per step, the ones a
|
|
3
|
+
* piece suggests already ticked; space toggles, enter runs (the library shows the keys). It
|
|
4
|
+
* only runs with a terminal.
|
|
5
|
+
*/
|
|
6
|
+
import { isCancel, multiselect } from "@clack/prompts";
|
|
7
|
+
/**
|
|
8
|
+
* The menu's options: each step's index and label, which names its piece.
|
|
9
|
+
* @param steps - The steps.
|
|
10
|
+
* @returns The options and the ones ticked at the start.
|
|
11
|
+
* @example menuOptions(steps).initialValues // [0, 1]
|
|
12
|
+
*/
|
|
13
|
+
export function menuOptions(steps) {
|
|
14
|
+
return {
|
|
15
|
+
options: steps.map((step, index) => ({ value: index, label: step.label })),
|
|
16
|
+
initialValues: steps.flatMap((step, index) => (step.checked ? [index] : [])),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Asks which steps to run.
|
|
21
|
+
* @param steps - The steps.
|
|
22
|
+
* @returns The picked ones, in their order, or null if the person cancelled.
|
|
23
|
+
* @example await askSteps(steps)
|
|
24
|
+
*/
|
|
25
|
+
export async function askSteps(steps) {
|
|
26
|
+
const { options, initialValues } = menuOptions(steps);
|
|
27
|
+
const picked = await multiselect({ message: "What to do?", options, initialValues, required: false });
|
|
28
|
+
if (isCancel(picked))
|
|
29
|
+
return null;
|
|
30
|
+
return steps.filter((_, index) => picked.includes(index));
|
|
31
|
+
}
|
package/dist/output.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Where commands write, and the errors that end them. Commands get an `Output`
|
|
3
|
+
* instead of writing to `process` directly, so the entry point decides about the terminal.
|
|
4
|
+
*/
|
|
5
|
+
/** Usage error: exits 2 and prints the usage text. */
|
|
6
|
+
export declare class UsageError extends Error {
|
|
7
|
+
}
|
|
8
|
+
/** An error of the repo or of a file in it: exits 1 with the message. */
|
|
9
|
+
export declare class KitError extends Error {
|
|
10
|
+
}
|
|
11
|
+
/** Where a command writes, and whether someone can answer a question. */
|
|
12
|
+
export interface Output {
|
|
13
|
+
stdout: (text: string) => void;
|
|
14
|
+
stderr: (text: string) => void;
|
|
15
|
+
interactive: boolean;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The process's output; interactive when both stdin and stdout are a terminal.
|
|
19
|
+
* @returns The output.
|
|
20
|
+
* @example processOutput().interactive
|
|
21
|
+
*/
|
|
22
|
+
export declare function processOutput(): Output;
|
|
23
|
+
/**
|
|
24
|
+
* Prints a warning on stderr with the program's name.
|
|
25
|
+
* @param out - Where to write.
|
|
26
|
+
* @param text - The warning.
|
|
27
|
+
* @example warn(out, "claude-cloud init failed")
|
|
28
|
+
*/
|
|
29
|
+
export declare function warn(out: Output, text: string): void;
|
package/dist/output.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Where commands write, and the errors that end them. Commands get an `Output`
|
|
3
|
+
* instead of writing to `process` directly, so the entry point decides about the terminal.
|
|
4
|
+
*/
|
|
5
|
+
/** Usage error: exits 2 and prints the usage text. */
|
|
6
|
+
export class UsageError extends Error {
|
|
7
|
+
}
|
|
8
|
+
/** An error of the repo or of a file in it: exits 1 with the message. */
|
|
9
|
+
export class KitError extends Error {
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The process's output; interactive when both stdin and stdout are a terminal.
|
|
13
|
+
* @returns The output.
|
|
14
|
+
* @example processOutput().interactive
|
|
15
|
+
*/
|
|
16
|
+
export function processOutput() {
|
|
17
|
+
return {
|
|
18
|
+
stdout: (text) => process.stdout.write(text),
|
|
19
|
+
stderr: (text) => process.stderr.write(text),
|
|
20
|
+
interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Prints a warning on stderr with the program's name.
|
|
25
|
+
* @param out - Where to write.
|
|
26
|
+
* @param text - The warning.
|
|
27
|
+
* @example warn(out, "claude-cloud init failed")
|
|
28
|
+
*/
|
|
29
|
+
export function warn(out, text) {
|
|
30
|
+
out.stderr(`agent-kitchen: ${text}\n`);
|
|
31
|
+
}
|
package/dist/piece.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file What agent-kitchen knows of each piece it manages, and the names they share. A piece
|
|
3
|
+
* (the issue flow, claude-cloud, claude-repo-guard, the skills) tells what the repo has and
|
|
4
|
+
* the steps that would install or update it; agent-kitchen shows them, lets the person pick,
|
|
5
|
+
* and runs the picked ones in order. Every step runs the piece's own tool, which owns its
|
|
6
|
+
* files: agent-kitchen only writes the few lines that join two pieces.
|
|
7
|
+
*/
|
|
8
|
+
import type { Output } from "./output.js";
|
|
9
|
+
/** The issue flow on npm. */
|
|
10
|
+
export declare const FLOW_PACKAGE = "@ramon-sg/issue-flow";
|
|
11
|
+
/** The GitHub repo of the issue flow, which is also the catalog of its skills. */
|
|
12
|
+
export declare const FLOW_CATALOG = "ramon-sg/issue-flow";
|
|
13
|
+
/** The catalogs of general skills, offered next to the flow's. */
|
|
14
|
+
export declare const GENERAL_CATALOGS: string[];
|
|
15
|
+
/** `skills-manager` when the repo does not pin it. */
|
|
16
|
+
export declare const SKILLS_MANAGER = "@ramon-sg/skills-manager";
|
|
17
|
+
/** Where `skills-manager` keeps the repo's skills manifest. */
|
|
18
|
+
export declare const SKILLS_MANIFEST = ".agents/skills.json";
|
|
19
|
+
/** Where `skills-manager` keeps the repo's skills lock. */
|
|
20
|
+
export declare const SKILLS_LOCK = ".agents/skills.lock.json";
|
|
21
|
+
/** The issue flow's config. */
|
|
22
|
+
export declare const FLOW_CONFIG = ".agents/flow.json";
|
|
23
|
+
/** One step a piece offers: what it does, and how it runs. */
|
|
24
|
+
export interface Step {
|
|
25
|
+
piece: string;
|
|
26
|
+
label: string;
|
|
27
|
+
checked: boolean;
|
|
28
|
+
run: (context: Context) => boolean;
|
|
29
|
+
}
|
|
30
|
+
/** What a piece found in the repo: whether it has it, how it is, and the steps offered. */
|
|
31
|
+
export interface PieceStatus {
|
|
32
|
+
name: string;
|
|
33
|
+
summary: string;
|
|
34
|
+
installed: boolean;
|
|
35
|
+
state: string;
|
|
36
|
+
steps: Step[];
|
|
37
|
+
}
|
|
38
|
+
/** What a step gets when it runs. */
|
|
39
|
+
export interface Context {
|
|
40
|
+
root: string;
|
|
41
|
+
yes: boolean;
|
|
42
|
+
out: Output;
|
|
43
|
+
}
|
|
44
|
+
/** One check of `agent-kitchen doctor`: passed, failed, or only worth knowing. */
|
|
45
|
+
export interface Check {
|
|
46
|
+
name: string;
|
|
47
|
+
status: "ok" | "fail" | "info";
|
|
48
|
+
detail: string;
|
|
49
|
+
}
|
package/dist/piece.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** The issue flow on npm. */
|
|
2
|
+
export const FLOW_PACKAGE = "@ramon-sg/issue-flow";
|
|
3
|
+
/** The GitHub repo of the issue flow, which is also the catalog of its skills. */
|
|
4
|
+
export const FLOW_CATALOG = "ramon-sg/issue-flow";
|
|
5
|
+
/** The catalogs of general skills, offered next to the flow's. */
|
|
6
|
+
export const GENERAL_CATALOGS = ["ramon-sg/agent-skills"];
|
|
7
|
+
/** `skills-manager` when the repo does not pin it. */
|
|
8
|
+
export const SKILLS_MANAGER = "@ramon-sg/skills-manager";
|
|
9
|
+
/** Where `skills-manager` keeps the repo's skills manifest. */
|
|
10
|
+
export const SKILLS_MANIFEST = ".agents/skills.json";
|
|
11
|
+
/** Where `skills-manager` keeps the repo's skills lock. */
|
|
12
|
+
export const SKILLS_LOCK = ".agents/skills.lock.json";
|
|
13
|
+
/** The issue flow's config. */
|
|
14
|
+
export const FLOW_CONFIG = ".agents/flow.json";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Check, PieceStatus } from "../piece.js";
|
|
2
|
+
/** The piece's name, as `--skip` takes it. */
|
|
3
|
+
export declare const NAME = "claude-cloud";
|
|
4
|
+
/**
|
|
5
|
+
* What the repo has of claude-cloud, and the step it needs.
|
|
6
|
+
* @param root - The repo.
|
|
7
|
+
* @returns The piece's status.
|
|
8
|
+
* @example cloudStatus(root).state // "1.0.0, the latest"
|
|
9
|
+
*/
|
|
10
|
+
export declare function cloudStatus(root: string): PieceStatus;
|
|
11
|
+
/**
|
|
12
|
+
* Whether `.claude/cloud.json` pins a version, and whether a newer one exists.
|
|
13
|
+
* @param root - The repo.
|
|
14
|
+
* @returns The checks.
|
|
15
|
+
* @example cloudChecks(root)[0].status // "ok"
|
|
16
|
+
*/
|
|
17
|
+
export declare function cloudChecks(root: string): Check[];
|