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/dist/menu.js
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @file The
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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.
|
|
5
6
|
*/
|
|
6
|
-
import {
|
|
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
|
+
}
|
|
7
19
|
/**
|
|
8
20
|
* The menu's options: each step's index and label, which names its piece.
|
|
9
21
|
* @param steps - The steps.
|
|
@@ -16,6 +28,36 @@ export function menuOptions(steps) {
|
|
|
16
28
|
initialValues: steps.flatMap((step, index) => (step.checked ? [index] : [])),
|
|
17
29
|
};
|
|
18
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
|
+
}
|
|
19
61
|
/**
|
|
20
62
|
* Asks which steps to run.
|
|
21
63
|
* @param steps - The steps.
|
|
@@ -23,9 +65,36 @@ export function menuOptions(steps) {
|
|
|
23
65
|
* @example await askSteps(steps)
|
|
24
66
|
*/
|
|
25
67
|
export async function askSteps(steps) {
|
|
68
|
+
spanishKeys();
|
|
26
69
|
const { options, initialValues } = menuOptions(steps);
|
|
27
|
-
const picked = await multiselect({ message: "
|
|
70
|
+
const picked = await multiselect({ message: "¿Qué pasos hago?", options, initialValues, required: false });
|
|
28
71
|
if (isCancel(picked))
|
|
29
72
|
return null;
|
|
30
73
|
return steps.filter((_, index) => picked.includes(index));
|
|
31
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/piece.d.ts
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
* and runs the picked ones in order. Every step runs the piece's own tool, which owns its
|
|
6
6
|
* files: agent-kitchen only writes the few lines that join two pieces.
|
|
7
7
|
*/
|
|
8
|
-
import type { Output } from "./output.js";
|
|
9
8
|
/** The issue flow on npm. */
|
|
10
9
|
export declare const FLOW_PACKAGE = "@ramon-sg/issue-flow";
|
|
11
10
|
/** The GitHub repo of the issue flow, which is also the catalog of its skills. */
|
|
@@ -20,12 +19,29 @@ export declare const SKILLS_MANIFEST = ".agents/skills.json";
|
|
|
20
19
|
export declare const SKILLS_LOCK = ".agents/skills.lock.json";
|
|
21
20
|
/** The issue flow's config. */
|
|
22
21
|
export declare const FLOW_CONFIG = ".agents/flow.json";
|
|
23
|
-
/**
|
|
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. */
|
|
24
38
|
export interface Step {
|
|
25
39
|
piece: string;
|
|
26
40
|
label: string;
|
|
27
41
|
checked: boolean;
|
|
28
|
-
|
|
42
|
+
downloads: string[];
|
|
43
|
+
skills?: SkillPick;
|
|
44
|
+
run: (context: Context) => Promise<boolean>;
|
|
29
45
|
}
|
|
30
46
|
/** What a piece found in the repo: whether it has it, how it is, and the steps offered. */
|
|
31
47
|
export interface PieceStatus {
|
|
@@ -35,11 +51,15 @@ export interface PieceStatus {
|
|
|
35
51
|
state: string;
|
|
36
52
|
steps: Step[];
|
|
37
53
|
}
|
|
38
|
-
/**
|
|
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
|
+
*/
|
|
39
59
|
export interface Context {
|
|
40
60
|
root: string;
|
|
41
|
-
|
|
42
|
-
|
|
61
|
+
note: (text: string) => void;
|
|
62
|
+
fail: (text: string, output?: string) => void;
|
|
43
63
|
}
|
|
44
64
|
/** One check of `agent-kitchen doctor`: passed, failed, or only worth knowing. */
|
|
45
65
|
export interface Check {
|
package/dist/piece.js
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
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
|
+
*/
|
|
1
8
|
/** The issue flow on npm. */
|
|
2
9
|
export const FLOW_PACKAGE = "@ramon-sg/issue-flow";
|
|
3
10
|
/** The GitHub repo of the issue flow, which is also the catalog of its skills. */
|
package/dist/pieces/cloud.d.ts
CHANGED
|
@@ -1,17 +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
|
+
*/
|
|
1
8
|
import type { Check, PieceStatus } from "../piece.js";
|
|
2
9
|
/** The piece's name, as `--skip` takes it. */
|
|
3
10
|
export declare const NAME = "claude-cloud";
|
|
11
|
+
/** The package on npm. */
|
|
12
|
+
export declare const PACKAGE = "@ramon-sg/claude-cloud";
|
|
4
13
|
/**
|
|
5
14
|
* What the repo has of claude-cloud, and the step it needs.
|
|
6
15
|
* @param root - The repo.
|
|
7
16
|
* @returns The piece's status.
|
|
8
|
-
* @example cloudStatus(root).state // "1.0.0,
|
|
17
|
+
* @example (await cloudStatus(root)).state // "1.0.0, al día"
|
|
9
18
|
*/
|
|
10
|
-
export declare function cloudStatus(root: string): PieceStatus
|
|
19
|
+
export declare function cloudStatus(root: string): Promise<PieceStatus>;
|
|
11
20
|
/**
|
|
12
21
|
* Whether `.claude/cloud.json` pins a version, and whether a newer one exists.
|
|
13
22
|
* @param root - The repo.
|
|
14
23
|
* @returns The checks.
|
|
15
|
-
* @example cloudChecks(root)[0].status // "ok"
|
|
24
|
+
* @example (await cloudChecks(root))[0].status // "ok"
|
|
16
25
|
*/
|
|
17
|
-
export declare function cloudChecks(root: string): Check[]
|
|
26
|
+
export declare function cloudChecks(root: string): Promise<Check[]>;
|
package/dist/pieces/cloud.js
CHANGED
|
@@ -1,17 +1,9 @@
|
|
|
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 { warn } from "../output.js";
|
|
9
1
|
import { readJson, writeJson } from "../repo.js";
|
|
10
2
|
import { compareVersions, isRelease, npmLatest, runNpx } from "../tools.js";
|
|
11
3
|
/** The piece's name, as `--skip` takes it. */
|
|
12
4
|
export const NAME = "claude-cloud";
|
|
13
5
|
/** The package on npm. */
|
|
14
|
-
const PACKAGE = "@ramon-sg/claude-cloud";
|
|
6
|
+
export const PACKAGE = "@ramon-sg/claude-cloud";
|
|
15
7
|
/** Its config in the repo, which pins its version. */
|
|
16
8
|
const CONFIG = ".claude/cloud.json";
|
|
17
9
|
/**
|
|
@@ -35,11 +27,13 @@ function stepFor(config, latest) {
|
|
|
35
27
|
return {
|
|
36
28
|
piece: NAME,
|
|
37
29
|
checked: true,
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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)
|
|
41
35
|
return true;
|
|
42
|
-
|
|
36
|
+
context.fail(`claude-cloud init falló; corre 'npx ${spec} init'.`, run.output);
|
|
43
37
|
return false;
|
|
44
38
|
},
|
|
45
39
|
};
|
|
@@ -49,8 +43,9 @@ function stepFor(config, latest) {
|
|
|
49
43
|
return {
|
|
50
44
|
piece: NAME,
|
|
51
45
|
checked: true,
|
|
52
|
-
|
|
53
|
-
|
|
46
|
+
downloads: [],
|
|
47
|
+
label: `Actualizar claude-cloud ${version ?? "?"} → ${latest} (la versión que fija ${CONFIG})`,
|
|
48
|
+
run: async (context) => {
|
|
54
49
|
const current = readJson(context.root, CONFIG) ?? {};
|
|
55
50
|
const schema = typeof current.$schema === "string" ? current.$schema.replace(/@ramon-sg\/claude-cloud@[^/]+\//, `${PACKAGE}@${latest}/`) : undefined;
|
|
56
51
|
writeJson(context.root, CONFIG, { ...current, ...(schema === undefined ? {} : { $schema: schema }), version: latest });
|
|
@@ -62,32 +57,32 @@ function stepFor(config, latest) {
|
|
|
62
57
|
* What the repo has of claude-cloud, and the step it needs.
|
|
63
58
|
* @param root - The repo.
|
|
64
59
|
* @returns The piece's status.
|
|
65
|
-
* @example cloudStatus(root).state // "1.0.0,
|
|
60
|
+
* @example (await cloudStatus(root)).state // "1.0.0, al día"
|
|
66
61
|
*/
|
|
67
|
-
export function cloudStatus(root) {
|
|
62
|
+
export async function cloudStatus(root) {
|
|
68
63
|
const config = readJson(root, CONFIG);
|
|
69
|
-
const latest = npmLatest(PACKAGE);
|
|
64
|
+
const latest = await npmLatest(PACKAGE);
|
|
70
65
|
const version = pinned(config);
|
|
71
66
|
const step = stepFor(config, latest);
|
|
72
|
-
const known = latest === null ? " (npm
|
|
73
|
-
const state = config === null ? `
|
|
74
|
-
return { name: NAME, summary: "
|
|
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] };
|
|
75
70
|
}
|
|
76
71
|
/**
|
|
77
72
|
* Whether `.claude/cloud.json` pins a version, and whether a newer one exists.
|
|
78
73
|
* @param root - The repo.
|
|
79
74
|
* @returns The checks.
|
|
80
|
-
* @example cloudChecks(root)[0].status // "ok"
|
|
75
|
+
* @example (await cloudChecks(root))[0].status // "ok"
|
|
81
76
|
*/
|
|
82
|
-
export function cloudChecks(root) {
|
|
77
|
+
export async function cloudChecks(root) {
|
|
83
78
|
const config = readJson(root, CONFIG);
|
|
84
79
|
if (config === null)
|
|
85
|
-
return [{ name: NAME, status: "info", detail: "
|
|
80
|
+
return [{ name: NAME, status: "info", detail: "no instalado" }];
|
|
86
81
|
const version = pinned(config);
|
|
82
|
+
const latest = await npmLatest(PACKAGE);
|
|
87
83
|
if (version === null)
|
|
88
|
-
return [{ name: NAME, status: "fail", detail: `${CONFIG}
|
|
89
|
-
const latest = npmLatest(PACKAGE);
|
|
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"}"` }];
|
|
90
85
|
if (latest !== null && compareVersions(latest, version) > 0)
|
|
91
|
-
return [{ name: NAME, status: "info", detail: `${version}, ${latest}
|
|
86
|
+
return [{ name: NAME, status: "info", detail: `${version}, hay ${latest}: corre npx agent-kitchen` }];
|
|
92
87
|
return [{ name: NAME, status: "ok", detail: version }];
|
|
93
88
|
}
|
package/dist/pieces/guard.d.ts
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
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
|
+
* Its command that installs or updates is `apply` since 0.5.0 and `install` before, so the
|
|
12
|
+
* command follows the version being run. If it fails, its own output is shown; the hint about
|
|
13
|
+
* Claude's sandbox is added only when that output is a permission error, which is how the
|
|
14
|
+
* sandbox's block on `.claude/settings.json` shows up.
|
|
15
|
+
*/
|
|
1
16
|
import { type Check, type PieceStatus } from "../piece.js";
|
|
2
17
|
/** The piece's name, as `--skip` takes it. */
|
|
3
18
|
export declare const NAME = "claude-repo-guard";
|
|
19
|
+
/** Its GitHub repo. */
|
|
20
|
+
export declare const REPO = "ramon-sg/claude-repo-guard";
|
|
4
21
|
/** What the auto mode is told about the issue flow. */
|
|
5
22
|
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.";
|
|
6
23
|
/** The option that lets `gh` verify TLS inside the macOS sandbox. */
|
|
@@ -12,12 +29,27 @@ export declare const WEAKER_NETWORK = "weakerNetworkIsolation";
|
|
|
12
29
|
* @example guardSpec("0.2.0") // "github:ramon-sg/claude-repo-guard#v0.2.0"
|
|
13
30
|
*/
|
|
14
31
|
export declare function guardSpec(version: string | null): string;
|
|
32
|
+
/**
|
|
33
|
+
* The guard's command that installs or updates, at a version.
|
|
34
|
+
* @param version - The version, or null for the default branch.
|
|
35
|
+
* @returns `apply` from 0.5.0 on and on the default branch, `install` before.
|
|
36
|
+
* @example guardCommand("0.4.1") // "install"
|
|
37
|
+
*/
|
|
38
|
+
export declare function guardCommand(version: string | null): string;
|
|
39
|
+
/**
|
|
40
|
+
* What to say when the guard did not install.
|
|
41
|
+
* @param version - The version it ran.
|
|
42
|
+
* @param output - What it printed.
|
|
43
|
+
* @returns One line: the command to run by hand, and the sandbox hint only if the output is a permission error.
|
|
44
|
+
* @example guardFailure("0.5.1", "Error: EPERM: operation not permitted") // "…sandbox…"
|
|
45
|
+
*/
|
|
46
|
+
export declare function guardFailure(version: string | null, output: string): string;
|
|
15
47
|
/**
|
|
16
48
|
* The guard's options with what the flow needs added; what the repo set stays.
|
|
17
49
|
* @param root - The repo.
|
|
18
50
|
* @param platform - The operating system (`process.platform`).
|
|
19
51
|
* @returns The new options and what changed.
|
|
20
|
-
* @example guardOptions(root, "darwin").changes // ["
|
|
52
|
+
* @example guardOptions(root, "darwin").changes // ["weakerNetworkIsolation activado, …"]
|
|
21
53
|
*/
|
|
22
54
|
export declare function guardOptions(root: string, platform: string): {
|
|
23
55
|
options: Record<string, unknown>;
|
|
@@ -28,13 +60,13 @@ export declare function guardOptions(root: string, platform: string): {
|
|
|
28
60
|
* @param root - The repo.
|
|
29
61
|
* @param flowComing - Whether an earlier step of this run installs the issue flow.
|
|
30
62
|
* @returns The piece's status.
|
|
31
|
-
* @example guardStatus(root, false).state // "
|
|
63
|
+
* @example (await guardStatus(root, false)).state // "no instalado"
|
|
32
64
|
*/
|
|
33
|
-
export declare function guardStatus(root: string, flowComing: boolean): PieceStatus
|
|
65
|
+
export declare function guardStatus(root: string, flowComing: boolean): Promise<PieceStatus>;
|
|
34
66
|
/**
|
|
35
67
|
* The guard's own doctor at the repo's version and, on macOS, the option `gh` needs.
|
|
36
68
|
* @param root - The repo.
|
|
37
69
|
* @returns The checks.
|
|
38
|
-
* @example guardChecks(root).map((check) => check.status) // ["ok"]
|
|
70
|
+
* @example (await guardChecks(root)).map((check) => check.status) // ["ok"]
|
|
39
71
|
*/
|
|
40
|
-
export declare function guardChecks(root: string): Check[]
|
|
72
|
+
export declare function guardChecks(root: string): Promise<Check[]>;
|
package/dist/pieces/guard.js
CHANGED
|
@@ -7,15 +7,19 @@
|
|
|
7
7
|
* macOS `weakerNetworkIsolation` lets `gh` work inside the sandbox (the guard's own default
|
|
8
8
|
* is false, so false counts as missing; the step can be unticked). When the flow is
|
|
9
9
|
* installed in the same run, the guard gets those options in that run too.
|
|
10
|
+
*
|
|
11
|
+
* Its command that installs or updates is `apply` since 0.5.0 and `install` before, so the
|
|
12
|
+
* command follows the version being run. If it fails, its own output is shown; the hint about
|
|
13
|
+
* Claude's sandbox is added only when that output is a permission error, which is how the
|
|
14
|
+
* sandbox's block on `.claude/settings.json` shows up.
|
|
10
15
|
*/
|
|
11
|
-
import { warn } from "../output.js";
|
|
12
16
|
import { FLOW_CONFIG } from "../piece.js";
|
|
13
17
|
import { readJson, writeJson } from "../repo.js";
|
|
14
18
|
import { captureNpx, compareVersions, isRelease, latestTag, runNpx } from "../tools.js";
|
|
15
19
|
/** The piece's name, as `--skip` takes it. */
|
|
16
20
|
export const NAME = "claude-repo-guard";
|
|
17
21
|
/** Its GitHub repo. */
|
|
18
|
-
const REPO = "ramon-sg/claude-repo-guard";
|
|
22
|
+
export const REPO = "ramon-sg/claude-repo-guard";
|
|
19
23
|
/** Its options in the repo. */
|
|
20
24
|
const CONFIG = ".claude/repo-guard.config.json";
|
|
21
25
|
/** What its install records, with its version. */
|
|
@@ -33,12 +37,38 @@ export const WEAKER_NETWORK = "weakerNetworkIsolation";
|
|
|
33
37
|
export function guardSpec(version) {
|
|
34
38
|
return version === null ? `github:${REPO}` : `github:${REPO}#v${version}`;
|
|
35
39
|
}
|
|
40
|
+
/** The first version whose install command is `apply`. */
|
|
41
|
+
const APPLY_SINCE = "0.5.0";
|
|
42
|
+
/** What a write blocked by a sandbox or by the file's permissions prints. */
|
|
43
|
+
const PERMISSION_ERROR = /\b(EPERM|EACCES|EROFS)\b|operation not permitted|permission denied|read-only file system/i;
|
|
44
|
+
/**
|
|
45
|
+
* The guard's command that installs or updates, at a version.
|
|
46
|
+
* @param version - The version, or null for the default branch.
|
|
47
|
+
* @returns `apply` from 0.5.0 on and on the default branch, `install` before.
|
|
48
|
+
* @example guardCommand("0.4.1") // "install"
|
|
49
|
+
*/
|
|
50
|
+
export function guardCommand(version) {
|
|
51
|
+
return version === null || compareVersions(version, APPLY_SINCE) >= 0 ? "apply" : "install";
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* What to say when the guard did not install.
|
|
55
|
+
* @param version - The version it ran.
|
|
56
|
+
* @param output - What it printed.
|
|
57
|
+
* @returns One line: the command to run by hand, and the sandbox hint only if the output is a permission error.
|
|
58
|
+
* @example guardFailure("0.5.1", "Error: EPERM: operation not permitted") // "…sandbox…"
|
|
59
|
+
*/
|
|
60
|
+
export function guardFailure(version, output) {
|
|
61
|
+
const command = `npx ${guardSpec(version)} ${guardCommand(version)}`;
|
|
62
|
+
if (PERMISSION_ERROR.test(output))
|
|
63
|
+
return `claude-repo-guard no se instaló: no tuvo permiso para escribir en el repo. Si corres agent-kitchen dentro de una sesión de Claude, su sandbox bloquea .claude/settings.json; corre '${command}' desde tu terminal.`;
|
|
64
|
+
return `claude-repo-guard no se instaló; su error está abajo. Para reintentar a mano: '${command}'.`;
|
|
65
|
+
}
|
|
36
66
|
/**
|
|
37
67
|
* The guard's options with what the flow needs added; what the repo set stays.
|
|
38
68
|
* @param root - The repo.
|
|
39
69
|
* @param platform - The operating system (`process.platform`).
|
|
40
70
|
* @returns The new options and what changed.
|
|
41
|
-
* @example guardOptions(root, "darwin").changes // ["
|
|
71
|
+
* @example guardOptions(root, "darwin").changes // ["weakerNetworkIsolation activado, …"]
|
|
42
72
|
*/
|
|
43
73
|
export function guardOptions(root, platform) {
|
|
44
74
|
const options = { ...(readJson(root, CONFIG) ?? {}) };
|
|
@@ -47,16 +77,16 @@ export function guardOptions(root, platform) {
|
|
|
47
77
|
const environment = Array.isArray(options.environment) ? options.environment.map(String) : [];
|
|
48
78
|
if (flow !== null && !environment.includes(GUARD_ENVIRONMENT)) {
|
|
49
79
|
options.environment = [...environment, GUARD_ENVIRONMENT];
|
|
50
|
-
changes.push(
|
|
80
|
+
changes.push("le explica el flujo de issues al modo auto");
|
|
51
81
|
}
|
|
52
82
|
const baseBranch = typeof flow?.baseBranch === "string" ? flow.baseBranch : null;
|
|
53
83
|
if (baseBranch !== null && (options.integrationBranch === undefined || options.integrationBranch === "")) {
|
|
54
84
|
options.integrationBranch = baseBranch;
|
|
55
|
-
changes.push(
|
|
85
|
+
changes.push(`rama de integración ${baseBranch}`);
|
|
56
86
|
}
|
|
57
87
|
if (platform === "darwin" && options[WEAKER_NETWORK] !== true) {
|
|
58
88
|
options[WEAKER_NETWORK] = true;
|
|
59
|
-
changes.push(`${
|
|
89
|
+
changes.push(`${WEAKER_NETWORK} activado, para que gh funcione en el sandbox de macOS`);
|
|
60
90
|
}
|
|
61
91
|
return { options, changes };
|
|
62
92
|
}
|
|
@@ -66,15 +96,16 @@ export function guardOptions(root, platform) {
|
|
|
66
96
|
* @param version - The version, or null for the default branch.
|
|
67
97
|
* @returns Whether it installed.
|
|
68
98
|
*/
|
|
69
|
-
function install(context, version) {
|
|
99
|
+
async function install(context, version) {
|
|
70
100
|
const { options, changes } = guardOptions(context.root, process.platform);
|
|
71
101
|
if (changes.length > 0)
|
|
72
102
|
writeJson(context.root, CONFIG, options);
|
|
73
103
|
for (const change of changes)
|
|
74
|
-
context.
|
|
75
|
-
|
|
104
|
+
context.note(change);
|
|
105
|
+
const run = await runNpx(context.root, [guardSpec(version), guardCommand(version), "--yes"]);
|
|
106
|
+
if (run.ok)
|
|
76
107
|
return true;
|
|
77
|
-
|
|
108
|
+
context.fail(guardFailure(version, run.output), run.output);
|
|
78
109
|
return false;
|
|
79
110
|
}
|
|
80
111
|
/**
|
|
@@ -84,20 +115,12 @@ function install(context, version) {
|
|
|
84
115
|
* @param version - The guard's version, or null.
|
|
85
116
|
* @returns Whether it worked, or true if nothing was missing.
|
|
86
117
|
*/
|
|
87
|
-
function addOptions(context, version) {
|
|
118
|
+
async function addOptions(context, version) {
|
|
88
119
|
if (guardOptions(context.root, process.platform).changes.length > 0)
|
|
89
120
|
return install(context, version);
|
|
90
|
-
context.
|
|
121
|
+
context.note("sus opciones ya tienen lo que necesita el flujo");
|
|
91
122
|
return true;
|
|
92
123
|
}
|
|
93
|
-
/**
|
|
94
|
-
* Says how to retry an install that failed.
|
|
95
|
-
* @param out - Where to write.
|
|
96
|
-
* @param version - The version it tried.
|
|
97
|
-
*/
|
|
98
|
-
function failed(out, version) {
|
|
99
|
-
warn(out, `claude-repo-guard did not install; run 'npx ${guardSpec(version)} install' from your terminal (inside a Claude session its sandbox blocks .claude/settings.json).`);
|
|
100
|
-
}
|
|
101
124
|
/**
|
|
102
125
|
* The version its manifest records.
|
|
103
126
|
* @param root - The repo.
|
|
@@ -118,11 +141,11 @@ function installedVersion(root) {
|
|
|
118
141
|
*/
|
|
119
142
|
function stepFor(root, installed, latest, flowComing) {
|
|
120
143
|
if (installed === undefined)
|
|
121
|
-
return { piece: NAME, checked: true, label: `claude-repo-guard ${latest ?? "(main)"}: auto
|
|
144
|
+
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) };
|
|
122
145
|
if (latest !== null && (installed === null || compareVersions(latest, installed) > 0))
|
|
123
|
-
return { piece: NAME, checked: true, label: `claude-repo-guard ${installed ?? "?"} → ${latest}`, run: (context) => install(context, latest) };
|
|
146
|
+
return { piece: NAME, checked: true, downloads: [guardSpec(latest)], label: `Actualizar claude-repo-guard ${installed ?? "?"} → ${latest}`, run: (context) => install(context, latest) };
|
|
124
147
|
if (flowComing || guardOptions(root, process.platform).changes.length > 0)
|
|
125
|
-
return { piece: NAME, checked: true, label: "claude-repo-guard
|
|
148
|
+
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) };
|
|
126
149
|
return null;
|
|
127
150
|
}
|
|
128
151
|
/**
|
|
@@ -130,30 +153,30 @@ function stepFor(root, installed, latest, flowComing) {
|
|
|
130
153
|
* @param root - The repo.
|
|
131
154
|
* @param flowComing - Whether an earlier step of this run installs the issue flow.
|
|
132
155
|
* @returns The piece's status.
|
|
133
|
-
* @example guardStatus(root, false).state // "
|
|
156
|
+
* @example (await guardStatus(root, false)).state // "no instalado"
|
|
134
157
|
*/
|
|
135
|
-
export function guardStatus(root, flowComing) {
|
|
158
|
+
export async function guardStatus(root, flowComing) {
|
|
136
159
|
const installed = installedVersion(root);
|
|
137
|
-
const latest = latestTag(REPO);
|
|
138
|
-
const known = latest === null ? " (GitHub
|
|
139
|
-
const state = installed === undefined ? `
|
|
160
|
+
const latest = await latestTag(REPO);
|
|
161
|
+
const known = latest === null ? " (GitHub no tiene una versión)" : "";
|
|
162
|
+
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}`;
|
|
140
163
|
const step = stepFor(root, installed, latest, flowComing);
|
|
141
|
-
return { name: NAME, summary: "
|
|
164
|
+
return { name: NAME, summary: "barandas: modo auto con sandbox", installed: installed !== undefined, state, steps: step === null ? [] : [step] };
|
|
142
165
|
}
|
|
143
166
|
/**
|
|
144
167
|
* The guard's own doctor at the repo's version and, on macOS, the option `gh` needs.
|
|
145
168
|
* @param root - The repo.
|
|
146
169
|
* @returns The checks.
|
|
147
|
-
* @example guardChecks(root).map((check) => check.status) // ["ok"]
|
|
170
|
+
* @example (await guardChecks(root)).map((check) => check.status) // ["ok"]
|
|
148
171
|
*/
|
|
149
|
-
export function guardChecks(root) {
|
|
172
|
+
export async function guardChecks(root) {
|
|
150
173
|
const installed = installedVersion(root);
|
|
151
174
|
if (installed === undefined)
|
|
152
|
-
return [{ name: NAME, status: "info", detail: "
|
|
175
|
+
return [{ name: NAME, status: "info", detail: "no instalado (Claude se abre con tus propios permisos)" }];
|
|
153
176
|
const checks = [];
|
|
154
177
|
if (process.platform === "darwin" && readJson(root, CONFIG)?.[WEAKER_NETWORK] !== true)
|
|
155
|
-
checks.push({ name: `${NAME}: ${WEAKER_NETWORK}`, status: "fail", detail: `
|
|
156
|
-
const result = captureNpx(root, [guardSpec(installed), "doctor", "--json"]);
|
|
178
|
+
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` });
|
|
179
|
+
const result = await captureNpx(root, [guardSpec(installed), "doctor", "--json"]);
|
|
157
180
|
let found;
|
|
158
181
|
try {
|
|
159
182
|
found = JSON.parse(result?.out ?? "");
|
|
@@ -162,10 +185,10 @@ export function guardChecks(root) {
|
|
|
162
185
|
found = null;
|
|
163
186
|
}
|
|
164
187
|
if (!Array.isArray(found))
|
|
165
|
-
return [...checks, { name: NAME, status: "fail", detail: `
|
|
188
|
+
return [...checks, { name: NAME, status: "fail", detail: `no se pudo correr su doctor; corre 'npx ${guardSpec(installed)} doctor'` }];
|
|
166
189
|
const bad = found.filter((check) => typeof check !== "object" || check === null || check.ok !== true);
|
|
167
190
|
if (bad.length === 0)
|
|
168
|
-
return [...checks, { name: NAME, status: "ok", detail: `${installed ?? "
|
|
191
|
+
return [...checks, { name: NAME, status: "ok", detail: `${installed ?? "versión desconocida"}, su doctor pasa` }];
|
|
169
192
|
return [...checks, ...bad.map((check) => {
|
|
170
193
|
const entry = typeof check === "object" && check !== null ? check : { name: "?", detail: String(check) };
|
|
171
194
|
return { name: `${NAME}: ${String(entry.name)}`, status: "fail", detail: String(entry.detail) };
|
|
@@ -1,3 +1,10 @@
|
|
|
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
|
+
*/
|
|
1
8
|
import { type Check, type PieceStatus } from "../piece.js";
|
|
2
9
|
/** The piece's name, as `--skip` takes it. */
|
|
3
10
|
export declare const NAME = "issue-flow";
|
|
@@ -20,13 +27,13 @@ export declare function flowVersion(root: string): string | null;
|
|
|
20
27
|
* What the repo has of the issue flow, and the step it needs.
|
|
21
28
|
* @param root - The repo.
|
|
22
29
|
* @returns The piece's status.
|
|
23
|
-
* @example issueFlowStatus(root).state // "0.1.0, 0.2.0
|
|
30
|
+
* @example (await issueFlowStatus(root)).state // "0.1.0, hay 0.2.0"
|
|
24
31
|
*/
|
|
25
|
-
export declare function issueFlowStatus(root: string): PieceStatus
|
|
32
|
+
export declare function issueFlowStatus(root: string): Promise<PieceStatus>;
|
|
26
33
|
/**
|
|
27
34
|
* The flow's own doctor at the repo's version, and whether its skills are at that tag.
|
|
28
35
|
* @param root - The repo.
|
|
29
36
|
* @returns The checks.
|
|
30
|
-
* @example issueFlowChecks(root)[0].name // "issue-flow: .agents/flow.json"
|
|
37
|
+
* @example (await issueFlowChecks(root))[0].name // "issue-flow: .agents/flow.json"
|
|
31
38
|
*/
|
|
32
|
-
export declare function issueFlowChecks(root: string): Check[]
|
|
39
|
+
export declare function issueFlowChecks(root: string): Promise<Check[]>;
|