agent-kitchen 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +81 -2
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +120 -0
- package/dist/commands/doctor.d.ts +16 -0
- package/dist/commands/doctor.js +22 -0
- package/dist/commands/sync.d.ts +24 -0
- package/dist/commands/sync.js +243 -0
- package/dist/menu.d.ts +56 -0
- package/dist/menu.js +100 -0
- package/dist/output.d.ts +29 -0
- package/dist/output.js +31 -0
- package/dist/piece.d.ts +69 -0
- package/dist/piece.js +21 -0
- package/dist/pieces/cloud.d.ts +26 -0
- package/dist/pieces/cloud.js +88 -0
- package/dist/pieces/guard.d.ts +52 -0
- package/dist/pieces/guard.js +165 -0
- package/dist/pieces/issue-flow.d.ts +39 -0
- package/dist/pieces/issue-flow.js +151 -0
- package/dist/pieces/skills.d.ts +57 -0
- package/dist/pieces/skills.js +251 -0
- package/dist/repo.d.ts +48 -0
- package/dist/repo.js +117 -0
- package/dist/skills-files.d.ts +75 -0
- package/dist/skills-files.js +116 -0
- package/dist/tools.d.ts +77 -0
- package/dist/tools.js +175 -0
- package/package.json +20 -4
package/dist/menu.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The questions, in Spanish (`@clack/prompts`), all asked before any step runs: the
|
|
3
|
+
* quick path (do everything suggested, pick, or leave), the checkbox menu of the steps, the
|
|
4
|
+
* skills of a step grouped by catalog with their description, and whether to replace the
|
|
5
|
+
* repo's own skills. They only run with a terminal.
|
|
6
|
+
*/
|
|
7
|
+
import { styleText } from "node:util";
|
|
8
|
+
import { confirm, groupMultiselect, isCancel, MULTISELECT_INSTRUCTIONS, multiselect, select, SELECT_INSTRUCTIONS } from "@clack/prompts";
|
|
9
|
+
/** Width a skill's description is cut to in the menu. */
|
|
10
|
+
const DESCRIPTION_WIDTH = 70;
|
|
11
|
+
/**
|
|
12
|
+
* Puts the keys' help of the menus in Spanish. The library exports those lines as arrays,
|
|
13
|
+
* so they are replaced in place.
|
|
14
|
+
*/
|
|
15
|
+
function spanishKeys() {
|
|
16
|
+
MULTISELECT_INSTRUCTIONS.splice(0, MULTISELECT_INSTRUCTIONS.length, `${styleText("dim", "↑/↓")} moverse`, `${styleText("dim", "Espacio:")} marcar`, `${styleText("dim", "Enter:")} seguir`);
|
|
17
|
+
SELECT_INSTRUCTIONS.splice(0, SELECT_INSTRUCTIONS.length, `${styleText("dim", "↑/↓")} moverse`, `${styleText("dim", "Enter:")} elegir`);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The menu's options: each step's index and label, which names its piece.
|
|
21
|
+
* @param steps - The steps.
|
|
22
|
+
* @returns The options and the ones ticked at the start.
|
|
23
|
+
* @example menuOptions(steps).initialValues // [0, 1]
|
|
24
|
+
*/
|
|
25
|
+
export function menuOptions(steps) {
|
|
26
|
+
return {
|
|
27
|
+
options: steps.map((step, index) => ({ value: index, label: step.label })),
|
|
28
|
+
initialValues: steps.flatMap((step, index) => (step.checked ? [index] : [])),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The skills menu's options: grouped by catalog, each with its description cut short.
|
|
33
|
+
* @param candidates - What the catalogs offer.
|
|
34
|
+
* @returns The groups.
|
|
35
|
+
* @example Object.keys(skillOptions(candidates)) // ["ramon-sg/issue-flow", "ramon-sg/agent-skills"]
|
|
36
|
+
*/
|
|
37
|
+
export function skillOptions(candidates) {
|
|
38
|
+
const groups = {};
|
|
39
|
+
for (const candidate of candidates) {
|
|
40
|
+
const hint = candidate.description.length > DESCRIPTION_WIDTH ? `${candidate.description.slice(0, DESCRIPTION_WIDTH - 1)}…` : candidate.description;
|
|
41
|
+
const group = candidate.catalog === "" ? "skills" : candidate.catalog;
|
|
42
|
+
groups[group] = [...(groups[group] ?? []), { value: candidate.id, label: candidate.name, ...(hint === "" ? {} : { hint }) }];
|
|
43
|
+
}
|
|
44
|
+
return groups;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Asks what to do: everything suggested, pick step by step, or leave.
|
|
48
|
+
* @param count - How many steps are suggested; with none, the option to do them is not shown.
|
|
49
|
+
* @returns The answer; "leave" if the person cancelled.
|
|
50
|
+
* @example await askPath(4) // "all"
|
|
51
|
+
*/
|
|
52
|
+
export async function askPath(count) {
|
|
53
|
+
spanishKeys();
|
|
54
|
+
const all = { value: "all", label: `Todo lo sugerido (${count} ${count === 1 ? "paso" : "pasos"})`, hint: "recomendado" };
|
|
55
|
+
const answer = await select({
|
|
56
|
+
message: "¿Qué hago?",
|
|
57
|
+
options: [...(count === 0 ? [] : [all]), { value: "pick", label: "Elegir qué hacer" }, { value: "leave", label: "Salir sin cambiar nada" }],
|
|
58
|
+
});
|
|
59
|
+
return isCancel(answer) ? "leave" : answer;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Asks which steps to run.
|
|
63
|
+
* @param steps - The steps.
|
|
64
|
+
* @returns The picked ones, in their order, or null if the person cancelled.
|
|
65
|
+
* @example await askSteps(steps)
|
|
66
|
+
*/
|
|
67
|
+
export async function askSteps(steps) {
|
|
68
|
+
spanishKeys();
|
|
69
|
+
const { options, initialValues } = menuOptions(steps);
|
|
70
|
+
const picked = await multiselect({ message: "¿Qué pasos hago?", options, initialValues, required: false });
|
|
71
|
+
if (isCancel(picked))
|
|
72
|
+
return null;
|
|
73
|
+
return steps.filter((_, index) => picked.includes(index));
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Asks which skills to install.
|
|
77
|
+
* @param candidates - What the catalogs offer.
|
|
78
|
+
* @param ticked - The ids ticked at the start.
|
|
79
|
+
* @returns The picked ids, or null if the person cancelled.
|
|
80
|
+
* @example await askSkills(candidates, ["catalog:ramon-sg/agent-skills/planning"])
|
|
81
|
+
*/
|
|
82
|
+
export async function askSkills(candidates, ticked) {
|
|
83
|
+
spanishKeys();
|
|
84
|
+
const picked = await groupMultiselect({ message: "¿Qué skills instalo?", options: skillOptions(candidates), initialValues: ticked, required: false });
|
|
85
|
+
return isCancel(picked) ? null : picked;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Asks whether the catalog's skills replace the repo's own ones with the same name.
|
|
89
|
+
* @param own - Their names.
|
|
90
|
+
* @returns True to replace them, false to keep them, or null if the person cancelled.
|
|
91
|
+
* @example await askReplace(["planning"])
|
|
92
|
+
*/
|
|
93
|
+
export async function askReplace(own) {
|
|
94
|
+
const answer = await confirm({
|
|
95
|
+
message: `El repo ya tiene su propia versión de: ${own.join(", ")}. ¿Las reemplazo por las del catálogo? Su project.md se conserva.`,
|
|
96
|
+
active: "Sí, reemplazar",
|
|
97
|
+
inactive: "No, dejarlas fuera",
|
|
98
|
+
});
|
|
99
|
+
return isCancel(answer) ? null : answer;
|
|
100
|
+
}
|
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,69 @@
|
|
|
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
|
+
/** The issue flow on npm. */
|
|
9
|
+
export declare const FLOW_PACKAGE = "@ramon-sg/issue-flow";
|
|
10
|
+
/** The GitHub repo of the issue flow, which is also the catalog of its skills. */
|
|
11
|
+
export declare const FLOW_CATALOG = "ramon-sg/issue-flow";
|
|
12
|
+
/** The catalogs of general skills, offered next to the flow's. */
|
|
13
|
+
export declare const GENERAL_CATALOGS: string[];
|
|
14
|
+
/** `skills-manager` when the repo does not pin it. */
|
|
15
|
+
export declare const SKILLS_MANAGER = "@ramon-sg/skills-manager";
|
|
16
|
+
/** Where `skills-manager` keeps the repo's skills manifest. */
|
|
17
|
+
export declare const SKILLS_MANIFEST = ".agents/skills.json";
|
|
18
|
+
/** Where `skills-manager` keeps the repo's skills lock. */
|
|
19
|
+
export declare const SKILLS_LOCK = ".agents/skills.lock.json";
|
|
20
|
+
/** The issue flow's config. */
|
|
21
|
+
export declare const FLOW_CONFIG = ".agents/flow.json";
|
|
22
|
+
/** A skill a catalog offers, as `skills-manager --candidates` lists it. */
|
|
23
|
+
export interface Candidate {
|
|
24
|
+
id: string;
|
|
25
|
+
name: string;
|
|
26
|
+
catalog: string;
|
|
27
|
+
description: string;
|
|
28
|
+
skills: string[];
|
|
29
|
+
requires: string[];
|
|
30
|
+
}
|
|
31
|
+
/** The skills a step installs: what it offers, and what the person picked and lets it replace. */
|
|
32
|
+
export interface SkillPick {
|
|
33
|
+
candidates: Candidate[];
|
|
34
|
+
picked: string[];
|
|
35
|
+
replace: string[];
|
|
36
|
+
}
|
|
37
|
+
/** One step a piece offers: what it does, what it downloads, and how it runs. */
|
|
38
|
+
export interface Step {
|
|
39
|
+
piece: string;
|
|
40
|
+
label: string;
|
|
41
|
+
checked: boolean;
|
|
42
|
+
downloads: string[];
|
|
43
|
+
skills?: SkillPick;
|
|
44
|
+
run: (context: Context) => Promise<boolean>;
|
|
45
|
+
}
|
|
46
|
+
/** What a piece found in the repo: whether it has it, how it is, and the steps offered. */
|
|
47
|
+
export interface PieceStatus {
|
|
48
|
+
name: string;
|
|
49
|
+
summary: string;
|
|
50
|
+
installed: boolean;
|
|
51
|
+
state: string;
|
|
52
|
+
steps: Step[];
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* What a step gets when it runs: the repo, and where to say what it did (`note`, one short
|
|
56
|
+
* line under the step) or what went wrong (`fail`, with the command to run by hand and the
|
|
57
|
+
* tool's output).
|
|
58
|
+
*/
|
|
59
|
+
export interface Context {
|
|
60
|
+
root: string;
|
|
61
|
+
note: (text: string) => void;
|
|
62
|
+
fail: (text: string, output?: string) => void;
|
|
63
|
+
}
|
|
64
|
+
/** One check of `agent-kitchen doctor`: passed, failed, or only worth knowing. */
|
|
65
|
+
export interface Check {
|
|
66
|
+
name: string;
|
|
67
|
+
status: "ok" | "fail" | "info";
|
|
68
|
+
detail: string;
|
|
69
|
+
}
|
package/dist/piece.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
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
|
+
/** The issue flow on npm. */
|
|
9
|
+
export const FLOW_PACKAGE = "@ramon-sg/issue-flow";
|
|
10
|
+
/** The GitHub repo of the issue flow, which is also the catalog of its skills. */
|
|
11
|
+
export const FLOW_CATALOG = "ramon-sg/issue-flow";
|
|
12
|
+
/** The catalogs of general skills, offered next to the flow's. */
|
|
13
|
+
export const GENERAL_CATALOGS = ["ramon-sg/agent-skills"];
|
|
14
|
+
/** `skills-manager` when the repo does not pin it. */
|
|
15
|
+
export const SKILLS_MANAGER = "@ramon-sg/skills-manager";
|
|
16
|
+
/** Where `skills-manager` keeps the repo's skills manifest. */
|
|
17
|
+
export const SKILLS_MANIFEST = ".agents/skills.json";
|
|
18
|
+
/** Where `skills-manager` keeps the repo's skills lock. */
|
|
19
|
+
export const SKILLS_LOCK = ".agents/skills.lock.json";
|
|
20
|
+
/** The issue flow's config. */
|
|
21
|
+
export const FLOW_CONFIG = ".agents/flow.json";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Claude cloud sessions (`@ramon-sg/claude-cloud`). The repo's version is the one
|
|
3
|
+
* `.claude/cloud.json` pins: the cloud machine runs exactly that one. Installing runs its
|
|
4
|
+
* `init`; updating only moves that version (and a `$schema` pinned to a version), which is
|
|
5
|
+
* how `claude-cloud` updates. Outside
|
|
6
|
+
* the cloud its hook does nothing, so a repo nobody opens in the cloud loses nothing.
|
|
7
|
+
*/
|
|
8
|
+
import type { Check, PieceStatus } from "../piece.js";
|
|
9
|
+
/** The piece's name, as `--skip` takes it. */
|
|
10
|
+
export declare const NAME = "claude-cloud";
|
|
11
|
+
/** The package on npm. */
|
|
12
|
+
export declare const PACKAGE = "@ramon-sg/claude-cloud";
|
|
13
|
+
/**
|
|
14
|
+
* What the repo has of claude-cloud, and the step it needs.
|
|
15
|
+
* @param root - The repo.
|
|
16
|
+
* @returns The piece's status.
|
|
17
|
+
* @example (await cloudStatus(root)).state // "1.0.0, al día"
|
|
18
|
+
*/
|
|
19
|
+
export declare function cloudStatus(root: string): Promise<PieceStatus>;
|
|
20
|
+
/**
|
|
21
|
+
* Whether `.claude/cloud.json` pins a version, and whether a newer one exists.
|
|
22
|
+
* @param root - The repo.
|
|
23
|
+
* @returns The checks.
|
|
24
|
+
* @example (await cloudChecks(root))[0].status // "ok"
|
|
25
|
+
*/
|
|
26
|
+
export declare function cloudChecks(root: string): Promise<Check[]>;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { readJson, writeJson } from "../repo.js";
|
|
2
|
+
import { compareVersions, isRelease, npmLatest, runNpx } from "../tools.js";
|
|
3
|
+
/** The piece's name, as `--skip` takes it. */
|
|
4
|
+
export const NAME = "claude-cloud";
|
|
5
|
+
/** The package on npm. */
|
|
6
|
+
export const PACKAGE = "@ramon-sg/claude-cloud";
|
|
7
|
+
/** Its config in the repo, which pins its version. */
|
|
8
|
+
const CONFIG = ".claude/cloud.json";
|
|
9
|
+
/**
|
|
10
|
+
* The version `.claude/cloud.json` pins.
|
|
11
|
+
* @param config - The config, or null.
|
|
12
|
+
* @returns The version, or null if it has none.
|
|
13
|
+
*/
|
|
14
|
+
function pinned(config) {
|
|
15
|
+
return typeof config?.version === "string" && isRelease(config.version) ? config.version : null;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The step the repo needs, if any.
|
|
19
|
+
* @param config - The config, or null.
|
|
20
|
+
* @param latest - The latest on npm, or null.
|
|
21
|
+
* @returns The step, or null.
|
|
22
|
+
*/
|
|
23
|
+
function stepFor(config, latest) {
|
|
24
|
+
const version = pinned(config);
|
|
25
|
+
if (config === null) {
|
|
26
|
+
const spec = `${PACKAGE}@${latest ?? "1"}`;
|
|
27
|
+
return {
|
|
28
|
+
piece: NAME,
|
|
29
|
+
checked: true,
|
|
30
|
+
downloads: [spec],
|
|
31
|
+
label: `Instalar claude-cloud ${latest ?? "1.x"}: prepara las sesiones en la nube de Claude`,
|
|
32
|
+
run: async (context) => {
|
|
33
|
+
const run = await runNpx(context.root, [spec, "init"]);
|
|
34
|
+
if (run.ok)
|
|
35
|
+
return true;
|
|
36
|
+
context.fail(`claude-cloud init falló; corre 'npx ${spec} init'.`, run.output);
|
|
37
|
+
return false;
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
if (latest === null || (version !== null && compareVersions(latest, version) <= 0))
|
|
42
|
+
return null;
|
|
43
|
+
return {
|
|
44
|
+
piece: NAME,
|
|
45
|
+
checked: true,
|
|
46
|
+
downloads: [],
|
|
47
|
+
label: `Actualizar claude-cloud ${version ?? "?"} → ${latest} (la versión que fija ${CONFIG})`,
|
|
48
|
+
run: async (context) => {
|
|
49
|
+
const current = readJson(context.root, CONFIG) ?? {};
|
|
50
|
+
const schema = typeof current.$schema === "string" ? current.$schema.replace(/@ramon-sg\/claude-cloud@[^/]+\//, `${PACKAGE}@${latest}/`) : undefined;
|
|
51
|
+
writeJson(context.root, CONFIG, { ...current, ...(schema === undefined ? {} : { $schema: schema }), version: latest });
|
|
52
|
+
return true;
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* What the repo has of claude-cloud, and the step it needs.
|
|
58
|
+
* @param root - The repo.
|
|
59
|
+
* @returns The piece's status.
|
|
60
|
+
* @example (await cloudStatus(root)).state // "1.0.0, al día"
|
|
61
|
+
*/
|
|
62
|
+
export async function cloudStatus(root) {
|
|
63
|
+
const config = readJson(root, CONFIG);
|
|
64
|
+
const latest = await npmLatest(PACKAGE);
|
|
65
|
+
const version = pinned(config);
|
|
66
|
+
const step = stepFor(config, latest);
|
|
67
|
+
const known = latest === null ? " (npm no responde)" : "";
|
|
68
|
+
const state = config === null ? `no instalado${known}` : version === null ? `${CONFIG} no fija versión` : step === null ? `${version}, al día${known}` : `${version}, hay ${latest ?? "?"}`;
|
|
69
|
+
return { name: NAME, summary: "sesiones en la nube de Claude", installed: config !== null, state, steps: step === null ? [] : [step] };
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Whether `.claude/cloud.json` pins a version, and whether a newer one exists.
|
|
73
|
+
* @param root - The repo.
|
|
74
|
+
* @returns The checks.
|
|
75
|
+
* @example (await cloudChecks(root))[0].status // "ok"
|
|
76
|
+
*/
|
|
77
|
+
export async function cloudChecks(root) {
|
|
78
|
+
const config = readJson(root, CONFIG);
|
|
79
|
+
if (config === null)
|
|
80
|
+
return [{ name: NAME, status: "info", detail: "no instalado" }];
|
|
81
|
+
const version = pinned(config);
|
|
82
|
+
const latest = await npmLatest(PACKAGE);
|
|
83
|
+
if (version === null)
|
|
84
|
+
return [{ name: NAME, status: "fail", detail: `${CONFIG} no fija una versión exacta: pon en "version" la que se usa, como "${latest ?? "1.0.0"}"` }];
|
|
85
|
+
if (latest !== null && compareVersions(latest, version) > 0)
|
|
86
|
+
return [{ name: NAME, status: "info", detail: `${version}, hay ${latest}: corre npx agent-kitchen` }];
|
|
87
|
+
return [{ name: NAME, status: "ok", detail: version }];
|
|
88
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The guardrails (`claude-repo-guard`): auto mode with the sandbox, installed in the
|
|
3
|
+
* repo. It is not on npm, so it runs from GitHub at its `vX.Y.Z` tag; the repo's version is
|
|
4
|
+
* the one its manifest records. Before installing or updating, agent-kitchen writes in its
|
|
5
|
+
* options what the issue flow needs, if the repo has the flow: the auto mode is told that
|
|
6
|
+
* the flow's writes are normal, the flow's base branch becomes `integrationBranch`, and on
|
|
7
|
+
* macOS `weakerNetworkIsolation` lets `gh` work inside the sandbox (the guard's own default
|
|
8
|
+
* is false, so false counts as missing; the step can be unticked). When the flow is
|
|
9
|
+
* installed in the same run, the guard gets those options in that run too.
|
|
10
|
+
*/
|
|
11
|
+
import { type Check, type PieceStatus } from "../piece.js";
|
|
12
|
+
/** The piece's name, as `--skip` takes it. */
|
|
13
|
+
export declare const NAME = "claude-repo-guard";
|
|
14
|
+
/** Its GitHub repo. */
|
|
15
|
+
export declare const REPO = "ramon-sg/claude-repo-guard";
|
|
16
|
+
/** What the auto mode is told about the issue flow. */
|
|
17
|
+
export declare const GUARD_ENVIRONMENT = "issue-flow is this repo's issue workflow: making the branch and worktree of an issue, its empty start commit, pushing it, opening its draft PR, assigning the issue and adding its \"Slug de rama\" block are normal steps of the work, not risky actions.";
|
|
18
|
+
/** The option that lets `gh` verify TLS inside the macOS sandbox. */
|
|
19
|
+
export declare const WEAKER_NETWORK = "weakerNetworkIsolation";
|
|
20
|
+
/**
|
|
21
|
+
* The npx spec of a version.
|
|
22
|
+
* @param version - The version, or null for the default branch.
|
|
23
|
+
* @returns The spec.
|
|
24
|
+
* @example guardSpec("0.2.0") // "github:ramon-sg/claude-repo-guard#v0.2.0"
|
|
25
|
+
*/
|
|
26
|
+
export declare function guardSpec(version: string | null): string;
|
|
27
|
+
/**
|
|
28
|
+
* The guard's options with what the flow needs added; what the repo set stays.
|
|
29
|
+
* @param root - The repo.
|
|
30
|
+
* @param platform - The operating system (`process.platform`).
|
|
31
|
+
* @returns The new options and what changed.
|
|
32
|
+
* @example guardOptions(root, "darwin").changes // ["weakerNetworkIsolation activado, …"]
|
|
33
|
+
*/
|
|
34
|
+
export declare function guardOptions(root: string, platform: string): {
|
|
35
|
+
options: Record<string, unknown>;
|
|
36
|
+
changes: string[];
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* What the repo has of claude-repo-guard, and the step it needs.
|
|
40
|
+
* @param root - The repo.
|
|
41
|
+
* @param flowComing - Whether an earlier step of this run installs the issue flow.
|
|
42
|
+
* @returns The piece's status.
|
|
43
|
+
* @example (await guardStatus(root, false)).state // "no instalado"
|
|
44
|
+
*/
|
|
45
|
+
export declare function guardStatus(root: string, flowComing: boolean): Promise<PieceStatus>;
|
|
46
|
+
/**
|
|
47
|
+
* The guard's own doctor at the repo's version and, on macOS, the option `gh` needs.
|
|
48
|
+
* @param root - The repo.
|
|
49
|
+
* @returns The checks.
|
|
50
|
+
* @example (await guardChecks(root)).map((check) => check.status) // ["ok"]
|
|
51
|
+
*/
|
|
52
|
+
export declare function guardChecks(root: string): Promise<Check[]>;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The guardrails (`claude-repo-guard`): auto mode with the sandbox, installed in the
|
|
3
|
+
* repo. It is not on npm, so it runs from GitHub at its `vX.Y.Z` tag; the repo's version is
|
|
4
|
+
* the one its manifest records. Before installing or updating, agent-kitchen writes in its
|
|
5
|
+
* options what the issue flow needs, if the repo has the flow: the auto mode is told that
|
|
6
|
+
* the flow's writes are normal, the flow's base branch becomes `integrationBranch`, and on
|
|
7
|
+
* macOS `weakerNetworkIsolation` lets `gh` work inside the sandbox (the guard's own default
|
|
8
|
+
* is false, so false counts as missing; the step can be unticked). When the flow is
|
|
9
|
+
* installed in the same run, the guard gets those options in that run too.
|
|
10
|
+
*/
|
|
11
|
+
import { FLOW_CONFIG } from "../piece.js";
|
|
12
|
+
import { readJson, writeJson } from "../repo.js";
|
|
13
|
+
import { captureNpx, compareVersions, isRelease, latestTag, runNpx } from "../tools.js";
|
|
14
|
+
/** The piece's name, as `--skip` takes it. */
|
|
15
|
+
export const NAME = "claude-repo-guard";
|
|
16
|
+
/** Its GitHub repo. */
|
|
17
|
+
export const REPO = "ramon-sg/claude-repo-guard";
|
|
18
|
+
/** Its options in the repo. */
|
|
19
|
+
const CONFIG = ".claude/repo-guard.config.json";
|
|
20
|
+
/** What its install records, with its version. */
|
|
21
|
+
const MANIFEST = ".claude/repo-guard.manifest.json";
|
|
22
|
+
/** What the auto mode is told about the issue flow. */
|
|
23
|
+
export const GUARD_ENVIRONMENT = "issue-flow is this repo's issue workflow: making the branch and worktree of an issue, its empty start commit, pushing it, opening its draft PR, assigning the issue and adding its \"Slug de rama\" block are normal steps of the work, not risky actions.";
|
|
24
|
+
/** The option that lets `gh` verify TLS inside the macOS sandbox. */
|
|
25
|
+
export const WEAKER_NETWORK = "weakerNetworkIsolation";
|
|
26
|
+
/**
|
|
27
|
+
* The npx spec of a version.
|
|
28
|
+
* @param version - The version, or null for the default branch.
|
|
29
|
+
* @returns The spec.
|
|
30
|
+
* @example guardSpec("0.2.0") // "github:ramon-sg/claude-repo-guard#v0.2.0"
|
|
31
|
+
*/
|
|
32
|
+
export function guardSpec(version) {
|
|
33
|
+
return version === null ? `github:${REPO}` : `github:${REPO}#v${version}`;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The guard's options with what the flow needs added; what the repo set stays.
|
|
37
|
+
* @param root - The repo.
|
|
38
|
+
* @param platform - The operating system (`process.platform`).
|
|
39
|
+
* @returns The new options and what changed.
|
|
40
|
+
* @example guardOptions(root, "darwin").changes // ["weakerNetworkIsolation activado, …"]
|
|
41
|
+
*/
|
|
42
|
+
export function guardOptions(root, platform) {
|
|
43
|
+
const options = { ...(readJson(root, CONFIG) ?? {}) };
|
|
44
|
+
const changes = [];
|
|
45
|
+
const flow = readJson(root, FLOW_CONFIG);
|
|
46
|
+
const environment = Array.isArray(options.environment) ? options.environment.map(String) : [];
|
|
47
|
+
if (flow !== null && !environment.includes(GUARD_ENVIRONMENT)) {
|
|
48
|
+
options.environment = [...environment, GUARD_ENVIRONMENT];
|
|
49
|
+
changes.push("le explica el flujo de issues al modo auto");
|
|
50
|
+
}
|
|
51
|
+
const baseBranch = typeof flow?.baseBranch === "string" ? flow.baseBranch : null;
|
|
52
|
+
if (baseBranch !== null && (options.integrationBranch === undefined || options.integrationBranch === "")) {
|
|
53
|
+
options.integrationBranch = baseBranch;
|
|
54
|
+
changes.push(`rama de integración ${baseBranch}`);
|
|
55
|
+
}
|
|
56
|
+
if (platform === "darwin" && options[WEAKER_NETWORK] !== true) {
|
|
57
|
+
options[WEAKER_NETWORK] = true;
|
|
58
|
+
changes.push(`${WEAKER_NETWORK} activado, para que gh funcione en el sandbox de macOS`);
|
|
59
|
+
}
|
|
60
|
+
return { options, changes };
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Writes the flow's options and installs the guard at a version.
|
|
64
|
+
* @param context - Where it runs.
|
|
65
|
+
* @param version - The version, or null for the default branch.
|
|
66
|
+
* @returns Whether it installed.
|
|
67
|
+
*/
|
|
68
|
+
async function install(context, version) {
|
|
69
|
+
const { options, changes } = guardOptions(context.root, process.platform);
|
|
70
|
+
if (changes.length > 0)
|
|
71
|
+
writeJson(context.root, CONFIG, options);
|
|
72
|
+
for (const change of changes)
|
|
73
|
+
context.note(change);
|
|
74
|
+
const run = await runNpx(context.root, [guardSpec(version), "install", "--yes"]);
|
|
75
|
+
if (run.ok)
|
|
76
|
+
return true;
|
|
77
|
+
context.fail(`claude-repo-guard no se instaló; corre 'npx ${guardSpec(version)} install' desde tu terminal (dentro de una sesión de Claude su sandbox bloquea .claude/settings.json).`, run.output);
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Writes the flow's options and reinstalls the guard at its version, if they are missing
|
|
82
|
+
* by now (the flow may have been installed earlier in this run).
|
|
83
|
+
* @param context - Where it runs.
|
|
84
|
+
* @param version - The guard's version, or null.
|
|
85
|
+
* @returns Whether it worked, or true if nothing was missing.
|
|
86
|
+
*/
|
|
87
|
+
async function addOptions(context, version) {
|
|
88
|
+
if (guardOptions(context.root, process.platform).changes.length > 0)
|
|
89
|
+
return install(context, version);
|
|
90
|
+
context.note("sus opciones ya tienen lo que necesita el flujo");
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The version its manifest records.
|
|
95
|
+
* @param root - The repo.
|
|
96
|
+
* @returns The version, null if it records none, or undefined if it is not installed.
|
|
97
|
+
*/
|
|
98
|
+
function installedVersion(root) {
|
|
99
|
+
const manifest = readJson(root, MANIFEST);
|
|
100
|
+
if (manifest === null)
|
|
101
|
+
return undefined;
|
|
102
|
+
return typeof manifest.version === "string" && isRelease(manifest.version) ? manifest.version : null;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* The step the repo needs, if any.
|
|
106
|
+
* @param root - The repo.
|
|
107
|
+
* @param installed - The repo's version, null if unknown, undefined if not installed.
|
|
108
|
+
* @param latest - The latest tag, or null.
|
|
109
|
+
* @returns The step, or null.
|
|
110
|
+
*/
|
|
111
|
+
function stepFor(root, installed, latest, flowComing) {
|
|
112
|
+
if (installed === undefined)
|
|
113
|
+
return { piece: NAME, checked: true, downloads: [guardSpec(latest)], label: `Instalar claude-repo-guard ${latest ?? "(main)"}: modo auto con sandbox, y un documento de qué protege`, run: (context) => install(context, latest) };
|
|
114
|
+
if (latest !== null && (installed === null || compareVersions(latest, installed) > 0))
|
|
115
|
+
return { piece: NAME, checked: true, downloads: [guardSpec(latest)], label: `Actualizar claude-repo-guard ${installed ?? "?"} → ${latest}`, run: (context) => install(context, latest) };
|
|
116
|
+
if (flowComing || guardOptions(root, process.platform).changes.length > 0)
|
|
117
|
+
return { piece: NAME, checked: true, downloads: [guardSpec(installed)], label: "Ajustar claude-repo-guard a lo que necesita el flujo de issues", run: (context) => addOptions(context, installed) };
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* What the repo has of claude-repo-guard, and the step it needs.
|
|
122
|
+
* @param root - The repo.
|
|
123
|
+
* @param flowComing - Whether an earlier step of this run installs the issue flow.
|
|
124
|
+
* @returns The piece's status.
|
|
125
|
+
* @example (await guardStatus(root, false)).state // "no instalado"
|
|
126
|
+
*/
|
|
127
|
+
export async function guardStatus(root, flowComing) {
|
|
128
|
+
const installed = installedVersion(root);
|
|
129
|
+
const latest = await latestTag(REPO);
|
|
130
|
+
const known = latest === null ? " (GitHub no tiene una versión)" : "";
|
|
131
|
+
const state = installed === undefined ? `no instalado${known}` : latest !== null && (installed === null || compareVersions(latest, installed) > 0) ? `${installed ?? "versión desconocida"}, hay ${latest}` : `${installed ?? "versión desconocida"}, al día${known}`;
|
|
132
|
+
const step = stepFor(root, installed, latest, flowComing);
|
|
133
|
+
return { name: NAME, summary: "barandas: modo auto con sandbox", installed: installed !== undefined, state, steps: step === null ? [] : [step] };
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* The guard's own doctor at the repo's version and, on macOS, the option `gh` needs.
|
|
137
|
+
* @param root - The repo.
|
|
138
|
+
* @returns The checks.
|
|
139
|
+
* @example (await guardChecks(root)).map((check) => check.status) // ["ok"]
|
|
140
|
+
*/
|
|
141
|
+
export async function guardChecks(root) {
|
|
142
|
+
const installed = installedVersion(root);
|
|
143
|
+
if (installed === undefined)
|
|
144
|
+
return [{ name: NAME, status: "info", detail: "no instalado (Claude se abre con tus propios permisos)" }];
|
|
145
|
+
const checks = [];
|
|
146
|
+
if (process.platform === "darwin" && readJson(root, CONFIG)?.[WEAKER_NETWORK] !== true)
|
|
147
|
+
checks.push({ name: `${NAME}: ${WEAKER_NETWORK}`, status: "fail", detail: `no está activado en ${CONFIG}, así que gh falla en el sandbox de macOS: corre npx agent-kitchen` });
|
|
148
|
+
const result = await captureNpx(root, [guardSpec(installed), "doctor", "--json"]);
|
|
149
|
+
let found;
|
|
150
|
+
try {
|
|
151
|
+
found = JSON.parse(result?.out ?? "");
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
found = null;
|
|
155
|
+
}
|
|
156
|
+
if (!Array.isArray(found))
|
|
157
|
+
return [...checks, { name: NAME, status: "fail", detail: `no se pudo correr su doctor; corre 'npx ${guardSpec(installed)} doctor'` }];
|
|
158
|
+
const bad = found.filter((check) => typeof check !== "object" || check === null || check.ok !== true);
|
|
159
|
+
if (bad.length === 0)
|
|
160
|
+
return [...checks, { name: NAME, status: "ok", detail: `${installed ?? "versión desconocida"}, su doctor pasa` }];
|
|
161
|
+
return [...checks, ...bad.map((check) => {
|
|
162
|
+
const entry = typeof check === "object" && check !== null ? check : { name: "?", detail: String(check) };
|
|
163
|
+
return { name: `${NAME}: ${String(entry.name)}`, status: "fail", detail: String(entry.detail) };
|
|
164
|
+
})];
|
|
165
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The issue flow (`@ramon-sg/issue-flow`): its CLI, hooks and config, and its skills.
|
|
3
|
+
* The repo's version is the one in package.json. Installing runs its `init` at the latest
|
|
4
|
+
* version; updating runs its `upgrade` at the new one and then moves its skills to the same
|
|
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.
|
|
7
|
+
*/
|
|
8
|
+
import { type Check, type PieceStatus } from "../piece.js";
|
|
9
|
+
/** The piece's name, as `--skip` takes it. */
|
|
10
|
+
export declare const NAME = "issue-flow";
|
|
11
|
+
/**
|
|
12
|
+
* The issue flow's version in the repo, as package.json has it: an exact release when the
|
|
13
|
+
* flow set it up, or anything else (a range, a `file:` link) someone wrote by hand.
|
|
14
|
+
* @param root - The repo.
|
|
15
|
+
* @returns The version in package.json, or null.
|
|
16
|
+
* @example flowVersion(root) // "0.2.0"
|
|
17
|
+
*/
|
|
18
|
+
export declare function flowSpec(root: string): string | null;
|
|
19
|
+
/**
|
|
20
|
+
* The issue flow's release in the repo: what package.json pins, if it is an exact release.
|
|
21
|
+
* @param root - The repo.
|
|
22
|
+
* @returns The version, or null if there is none or it is not a release.
|
|
23
|
+
* @example flowVersion(root) // "0.2.0"
|
|
24
|
+
*/
|
|
25
|
+
export declare function flowVersion(root: string): string | null;
|
|
26
|
+
/**
|
|
27
|
+
* What the repo has of the issue flow, and the step it needs.
|
|
28
|
+
* @param root - The repo.
|
|
29
|
+
* @returns The piece's status.
|
|
30
|
+
* @example (await issueFlowStatus(root)).state // "0.1.0, hay 0.2.0"
|
|
31
|
+
*/
|
|
32
|
+
export declare function issueFlowStatus(root: string): Promise<PieceStatus>;
|
|
33
|
+
/**
|
|
34
|
+
* The flow's own doctor at the repo's version, and whether its skills are at that tag.
|
|
35
|
+
* @param root - The repo.
|
|
36
|
+
* @returns The checks.
|
|
37
|
+
* @example (await issueFlowChecks(root))[0].name // "issue-flow: .agents/flow.json"
|
|
38
|
+
*/
|
|
39
|
+
export declare function issueFlowChecks(root: string): Promise<Check[]>;
|