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.
@@ -0,0 +1,75 @@
1
+ import { type ToolRun } from "./tools.js";
2
+ /** A skill in the lock: its name and the tag it is at. */
3
+ export interface LockedSkill {
4
+ name: string;
5
+ tag: string | null;
6
+ }
7
+ /**
8
+ * Where the repo's skills lock is: the manifest's `paths.lock`, or the default.
9
+ * @param root - The repo.
10
+ * @returns The path, relative to the repo.
11
+ * @example lockPath(root) // ".agents/skills.lock.json"
12
+ */
13
+ export declare function lockPath(root: string): string;
14
+ /**
15
+ * Every skill in the repo's lock, by name.
16
+ * @param root - The repo.
17
+ * @returns The lock's entries.
18
+ * @example Object.keys(lockedSkills(root)) // ["close-pr", "code-audit"]
19
+ */
20
+ export declare function lockedSkills(root: string): Record<string, unknown>;
21
+ /**
22
+ * Runs `skills-manager update <name>` for each skill, stopping at the first that fails.
23
+ * @param root - The repo.
24
+ * @param names - The skills.
25
+ * @returns Null if every one worked, or the command that failed, to retry it, and what it printed.
26
+ * @example await updateSkills(root, ["close-pr", "pr-review-comments"]) // null
27
+ */
28
+ export declare function updateSkills(root: string, names: string[]): Promise<{
29
+ command: string;
30
+ run: ToolRun;
31
+ } | null>;
32
+ /**
33
+ * The repo's own skills among some names: a folder in the skills folder that the lock does
34
+ * not know, which `skills-manager` replaces only when allowed.
35
+ * @param root - The repo.
36
+ * @param names - The skills about to be installed.
37
+ * @returns The names that are the repo's own.
38
+ * @example ownSkills(root, ["planning", "code-audit"]) // ["planning"]
39
+ */
40
+ export declare function ownSkills(root: string, names: string[]): string[];
41
+ /**
42
+ * The skills of a catalog in the repo's lock.
43
+ * @param root - The repo.
44
+ * @param catalog - The catalog, as owner/name.
45
+ * @returns Name and tag of each one.
46
+ * @example catalogSkills(root, "ramon-sg/issue-flow") // [{ name: "close-pr", tag: "v0.2.0" }]
47
+ */
48
+ export declare function catalogSkills(root: string, catalog: string): LockedSkill[];
49
+ /**
50
+ * The catalogs in the repo's skills manifest, with the ref each one is pinned to.
51
+ * @param root - The repo.
52
+ * @returns Each catalog and its ref (null if it follows the latest release).
53
+ * @example manifestCatalogs(root) // [{ catalog: "ramon-sg/issue-flow", ref: "v0.2.0" }]
54
+ */
55
+ export declare function manifestCatalogs(root: string): {
56
+ catalog: string;
57
+ ref: string | null;
58
+ }[];
59
+ /**
60
+ * Pins a catalog of the manifest to a tag, so `skills-manager update` moves its skills there.
61
+ * @param root - The repo.
62
+ * @param catalog - The catalog.
63
+ * @param tag - The tag, like v0.2.0.
64
+ * @returns Whether the manifest has the catalog.
65
+ * @example pinCatalog(root, "ramon-sg/issue-flow", "v0.2.0") // true
66
+ */
67
+ export declare function pinCatalog(root: string, catalog: string, tag: string): boolean;
68
+ /**
69
+ * The `skills-manager` to run: the version the repo pins in package.json, so the lock is
70
+ * written by the same version every time, or the latest 1.x.
71
+ * @param root - The repo.
72
+ * @returns The npx spec.
73
+ * @example skillsManagerSpec(root) // "@ramon-sg/skills-manager@1.1.0"
74
+ */
75
+ export declare function skillsManagerSpec(root: string): string;
@@ -0,0 +1,116 @@
1
+ /**
2
+ * @file What agent-kitchen reads of the files of `skills-manager`: which skills of a catalog
3
+ * the lock has and at which tag (the lock is where the manifest's `paths.lock` says), the
4
+ * catalog's source in the manifest, which `skills-manager` to run (the one the repo pins,
5
+ * or the latest 1.x), the repo's own skills (folders the lock does not know), and updating
6
+ * skills one by one, because `skills-manager update` takes a single name.
7
+ */
8
+ import { existsSync, readdirSync } from "node:fs";
9
+ import { join } from "node:path";
10
+ import { dependencyVersion, isObject, readJson, writeJson } from "./repo.js";
11
+ import { SKILLS_LOCK, SKILLS_MANAGER, SKILLS_MANIFEST } from "./piece.js";
12
+ import { runNpx } from "./tools.js";
13
+ /** Where `skills-manager` puts the skills when the manifest does not say. */
14
+ const SKILLS_DIR = ".agents/skills";
15
+ /**
16
+ * Where the repo's skills lock is: the manifest's `paths.lock`, or the default.
17
+ * @param root - The repo.
18
+ * @returns The path, relative to the repo.
19
+ * @example lockPath(root) // ".agents/skills.lock.json"
20
+ */
21
+ export function lockPath(root) {
22
+ const paths = readJson(root, SKILLS_MANIFEST)?.paths;
23
+ return isObject(paths) && typeof paths.lock === "string" && paths.lock !== "" ? paths.lock : SKILLS_LOCK;
24
+ }
25
+ /**
26
+ * Every skill in the repo's lock, by name.
27
+ * @param root - The repo.
28
+ * @returns The lock's entries.
29
+ * @example Object.keys(lockedSkills(root)) // ["close-pr", "code-audit"]
30
+ */
31
+ export function lockedSkills(root) {
32
+ const lock = readJson(root, lockPath(root));
33
+ return isObject(lock?.skills) ? lock.skills : {};
34
+ }
35
+ /**
36
+ * Runs `skills-manager update <name>` for each skill, stopping at the first that fails.
37
+ * @param root - The repo.
38
+ * @param names - The skills.
39
+ * @returns Null if every one worked, or the command that failed, to retry it, and what it printed.
40
+ * @example await updateSkills(root, ["close-pr", "pr-review-comments"]) // null
41
+ */
42
+ export async function updateSkills(root, names) {
43
+ const spec = skillsManagerSpec(root);
44
+ for (const name of names) {
45
+ const run = await runNpx(root, [spec, "update", name]);
46
+ if (!run.ok)
47
+ return { command: `npx ${spec} update ${name}`, run };
48
+ }
49
+ return null;
50
+ }
51
+ /**
52
+ * The repo's own skills among some names: a folder in the skills folder that the lock does
53
+ * not know, which `skills-manager` replaces only when allowed.
54
+ * @param root - The repo.
55
+ * @param names - The skills about to be installed.
56
+ * @returns The names that are the repo's own.
57
+ * @example ownSkills(root, ["planning", "code-audit"]) // ["planning"]
58
+ */
59
+ export function ownSkills(root, names) {
60
+ const paths = readJson(root, SKILLS_MANIFEST)?.paths;
61
+ const dir = join(root, isObject(paths) && typeof paths.skillsDir === "string" && paths.skillsDir !== "" ? paths.skillsDir : SKILLS_DIR);
62
+ const folders = existsSync(dir) ? readdirSync(dir) : [];
63
+ const locked = lockedSkills(root);
64
+ return [...new Set(names)].filter((name) => folders.includes(name) && locked[name] === undefined);
65
+ }
66
+ /**
67
+ * The skills of a catalog in the repo's lock.
68
+ * @param root - The repo.
69
+ * @param catalog - The catalog, as owner/name.
70
+ * @returns Name and tag of each one.
71
+ * @example catalogSkills(root, "ramon-sg/issue-flow") // [{ name: "close-pr", tag: "v0.2.0" }]
72
+ */
73
+ export function catalogSkills(root, catalog) {
74
+ const skills = lockedSkills(root);
75
+ return Object.entries(skills)
76
+ .filter(([, entry]) => isObject(entry) && entry.source === `catalog:${catalog}`)
77
+ .map(([name, entry]) => ({ name, tag: isObject(entry) && typeof entry.tag === "string" ? entry.tag : null }));
78
+ }
79
+ /**
80
+ * The catalogs in the repo's skills manifest, with the ref each one is pinned to.
81
+ * @param root - The repo.
82
+ * @returns Each catalog and its ref (null if it follows the latest release).
83
+ * @example manifestCatalogs(root) // [{ catalog: "ramon-sg/issue-flow", ref: "v0.2.0" }]
84
+ */
85
+ export function manifestCatalogs(root) {
86
+ const manifest = readJson(root, SKILLS_MANIFEST);
87
+ const sources = Array.isArray(manifest?.sources) ? manifest.sources.filter(isObject) : [];
88
+ return sources.flatMap((source) => (typeof source.catalog === "string" ? [{ catalog: source.catalog, ref: typeof source.ref === "string" ? source.ref : null }] : []));
89
+ }
90
+ /**
91
+ * Pins a catalog of the manifest to a tag, so `skills-manager update` moves its skills there.
92
+ * @param root - The repo.
93
+ * @param catalog - The catalog.
94
+ * @param tag - The tag, like v0.2.0.
95
+ * @returns Whether the manifest has the catalog.
96
+ * @example pinCatalog(root, "ramon-sg/issue-flow", "v0.2.0") // true
97
+ */
98
+ export function pinCatalog(root, catalog, tag) {
99
+ const manifest = readJson(root, SKILLS_MANIFEST);
100
+ const sources = Array.isArray(manifest?.sources) ? manifest.sources : [];
101
+ if (manifest === null || !sources.some((source) => isObject(source) && source.catalog === catalog))
102
+ return false;
103
+ const moved = sources.map((source) => (isObject(source) && source.catalog === catalog ? { ...source, ref: tag } : source));
104
+ writeJson(root, SKILLS_MANIFEST, { ...manifest, sources: moved });
105
+ return true;
106
+ }
107
+ /**
108
+ * The `skills-manager` to run: the version the repo pins in package.json, so the lock is
109
+ * written by the same version every time, or the latest 1.x.
110
+ * @param root - The repo.
111
+ * @returns The npx spec.
112
+ * @example skillsManagerSpec(root) // "@ramon-sg/skills-manager@1.1.0"
113
+ */
114
+ export function skillsManagerSpec(root) {
115
+ return `${SKILLS_MANAGER}@${dependencyVersion(readJson(root, "package.json"), SKILLS_MANAGER) ?? "1"}`;
116
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Shows the tools' output as it comes instead of only when they fail.
3
+ * @param value - True for `--verbose`.
4
+ * @example setVerbose(true)
5
+ */
6
+ export declare function setVerbose(value: boolean): void;
7
+ /**
8
+ * Compares two release versions.
9
+ * @param left - A version like 0.2.0.
10
+ * @param right - Another.
11
+ * @returns Negative if left is older, 0 if equal, positive if newer.
12
+ * @example compareVersions("0.10.0", "0.9.1") // 1
13
+ */
14
+ export declare function compareVersions(left: string, right: string): number;
15
+ /**
16
+ * Whether a version is a release like 1.2.3.
17
+ * @param version - The text.
18
+ * @returns True for a release version.
19
+ * @example isRelease("1.2.3") // true
20
+ */
21
+ export declare function isRelease(version: string): boolean;
22
+ /**
23
+ * The latest version of a package on npm.
24
+ * @param name - The package.
25
+ * @returns The version, or null if npm cannot tell.
26
+ * @example await npmLatest("@ramon-sg/claude-cloud") // "1.0.0"
27
+ */
28
+ export declare function npmLatest(name: string): Promise<string | null>;
29
+ /**
30
+ * The highest `vX.Y.Z` tag of a GitHub repo.
31
+ * @param repo - The repo, as owner/name.
32
+ * @returns The version without the `v`, or null if it has none or git cannot reach it.
33
+ * @example await latestTag("ramon-sg/agent-skills") // "0.1.0"
34
+ */
35
+ export declare function latestTag(repo: string): Promise<string | null>;
36
+ /**
37
+ * Downloads a package into npx's cache without running it, once per package, so its step
38
+ * starts at once. A failed download is ignored: the step itself will say what failed.
39
+ * @param root - The repo.
40
+ * @param spec - The package, as npx takes it.
41
+ * @returns When it is done.
42
+ * @example void download(root, "@ramon-sg/claude-cloud@1.0.0")
43
+ */
44
+ export declare function download(root: string, spec: string): Promise<void>;
45
+ /** How a tool ended, and what it printed if it was kept. */
46
+ export interface ToolRun {
47
+ ok: boolean;
48
+ output: string;
49
+ }
50
+ /**
51
+ * Runs `npx --yes <args>` in the repo: quietly, or with its output on the terminal with
52
+ * `--verbose`. It must not ask anything: the person answered before the steps ran.
53
+ * @param root - The repo.
54
+ * @param args - The package and its arguments.
55
+ * @returns Whether it exited 0, and what it printed.
56
+ * @example await runNpx(root, ["@ramon-sg/claude-cloud@1.0.0", "init"])
57
+ */
58
+ export declare function runNpx(root: string, args: string[]): Promise<ToolRun>;
59
+ /**
60
+ * The end of a failed tool's output, indented, to show under its step.
61
+ * @param output - What it printed.
62
+ * @returns Its last lines, or an empty text.
63
+ * @example failedOutput("npm error 404\n") // " npm error 404\n"
64
+ */
65
+ export declare function failedOutput(output: string): string;
66
+ /**
67
+ * Runs `npx --yes <args>` in the repo and returns what it printed on stdout.
68
+ * @param root - The repo.
69
+ * @param args - The package and its arguments.
70
+ * @param lookup - True to give up after the lookups' time limit, for a read that only informs.
71
+ * @returns Its stdout and exit code, or null if npx could not start.
72
+ * @example await captureNpx(root, ["@ramon-sg/issue-flow@0.2.0", "doctor", "--json"])
73
+ */
74
+ export declare function captureNpx(root: string, args: string[], lookup?: boolean): Promise<{
75
+ out: string;
76
+ status: number | null;
77
+ } | null>;
package/dist/tools.js ADDED
@@ -0,0 +1,175 @@
1
+ /**
2
+ * @file How agent-kitchen reaches the other tools and their releases: `npx` to run them (always
3
+ * by their scoped name or GitHub spec, because the unscoped names on npm belong to other
4
+ * people), `npm view` for the latest version on npm, and `git ls-remote` for the latest
5
+ * `vX.Y.Z` tag on GitHub. Everything is asynchronous so the lookups run at the same time,
6
+ * and each lookup runs once per process. A lookup that fails, or takes over 20 seconds,
7
+ * gives null: the piece is shown, not offered. git never asks for credentials here.
8
+ *
9
+ * The tools run quietly: their output is kept and shown only when they fail, so the person
10
+ * reads one line per step. `--verbose` shows it as it comes.
11
+ */
12
+ import { spawn } from "node:child_process";
13
+ /** A release version. */
14
+ const VERSION = /^(\d+)\.(\d+)\.(\d+)$/;
15
+ /** How long a lookup of the latest version may take before it counts as unknown. */
16
+ const LOOKUP_TIMEOUT_MS = 20_000;
17
+ /** A release tag in `git ls-remote --tags --refs` output. */
18
+ const TAG_REF = /refs\/tags\/v(\d+\.\d+\.\d+)$/;
19
+ /** How many lines of a failed tool's output are shown. */
20
+ const FAILED_LINES = 25;
21
+ /** Whether the tools' output goes straight to the terminal. */
22
+ let verbose = false;
23
+ /** Lookups already started, by what they look up. */
24
+ const lookups = new Map();
25
+ /** Downloads already started, by package. */
26
+ const downloads = new Map();
27
+ /**
28
+ * Shows the tools' output as it comes instead of only when they fail.
29
+ * @param value - True for `--verbose`.
30
+ * @example setVerbose(true)
31
+ */
32
+ export function setVerbose(value) {
33
+ verbose = value;
34
+ }
35
+ /**
36
+ * Compares two release versions.
37
+ * @param left - A version like 0.2.0.
38
+ * @param right - Another.
39
+ * @returns Negative if left is older, 0 if equal, positive if newer.
40
+ * @example compareVersions("0.10.0", "0.9.1") // 1
41
+ */
42
+ export function compareVersions(left, right) {
43
+ const parts = (version) => (VERSION.exec(version) ?? []).slice(1).map(Number);
44
+ const [a, b] = [parts(left), parts(right)];
45
+ for (let index = 0; index < 3; index += 1) {
46
+ const diff = (a[index] ?? 0) - (b[index] ?? 0);
47
+ if (diff !== 0)
48
+ return diff;
49
+ }
50
+ return 0;
51
+ }
52
+ /**
53
+ * Whether a version is a release like 1.2.3.
54
+ * @param version - The text.
55
+ * @returns True for a release version.
56
+ * @example isRelease("1.2.3") // true
57
+ */
58
+ export function isRelease(version) {
59
+ return VERSION.test(version);
60
+ }
61
+ /**
62
+ * Runs a program and collects what it prints.
63
+ * @param program - The program.
64
+ * @param args - Its arguments.
65
+ * @param options - The folder, a time limit, extra variables, and whether its output goes to the terminal.
66
+ * @returns How it ended; status null if it could not start or ran out of time.
67
+ */
68
+ function run(program, args, options = {}) {
69
+ return new Promise((resolve) => {
70
+ const child = spawn(program, args, { cwd: options.cwd, env: { ...process.env, ...options.env }, stdio: options.inherit === true ? "inherit" : ["ignore", "pipe", "pipe"] });
71
+ let out = "";
72
+ let err = "";
73
+ child.stdout?.on("data", (chunk) => (out += chunk.toString()));
74
+ child.stderr?.on("data", (chunk) => (err += chunk.toString()));
75
+ const timer = options.timeout === undefined ? undefined : setTimeout(() => child.kill(), options.timeout);
76
+ child.on("error", () => resolve({ status: null, out, err }));
77
+ child.on("close", (code, signal) => {
78
+ if (timer !== undefined)
79
+ clearTimeout(timer);
80
+ resolve({ status: signal === null ? code : null, out, err });
81
+ });
82
+ });
83
+ }
84
+ /**
85
+ * Runs a lookup once: later calls get the same promise.
86
+ * @param key - What it looks up.
87
+ * @param lookup - How.
88
+ * @returns Its result.
89
+ */
90
+ function once(key, lookup) {
91
+ const known = lookups.get(key);
92
+ if (known !== undefined)
93
+ return known;
94
+ const started = lookup();
95
+ lookups.set(key, started);
96
+ return started;
97
+ }
98
+ /**
99
+ * The latest version of a package on npm.
100
+ * @param name - The package.
101
+ * @returns The version, or null if npm cannot tell.
102
+ * @example await npmLatest("@ramon-sg/claude-cloud") // "1.0.0"
103
+ */
104
+ export function npmLatest(name) {
105
+ return once(`npm:${name}`, async () => {
106
+ const result = await run("npm", ["view", name, "version", "--fetch-retries=0"], { timeout: LOOKUP_TIMEOUT_MS });
107
+ const version = result.status === 0 ? result.out.trim() : "";
108
+ return isRelease(version) ? version : null;
109
+ });
110
+ }
111
+ /**
112
+ * The highest `vX.Y.Z` tag of a GitHub repo.
113
+ * @param repo - The repo, as owner/name.
114
+ * @returns The version without the `v`, or null if it has none or git cannot reach it.
115
+ * @example await latestTag("ramon-sg/agent-skills") // "0.1.0"
116
+ */
117
+ export function latestTag(repo) {
118
+ return once(`tag:${repo}`, async () => {
119
+ const result = await run("git", ["ls-remote", "--tags", "--refs", `https://github.com/${repo}.git`], { timeout: LOOKUP_TIMEOUT_MS, env: { GIT_TERMINAL_PROMPT: "0" } });
120
+ if (result.status !== 0)
121
+ return null;
122
+ const versions = result.out.split("\n").flatMap((line) => TAG_REF.exec(line.trim())?.slice(1, 2) ?? []);
123
+ return versions.sort(compareVersions).at(-1) ?? null;
124
+ });
125
+ }
126
+ /**
127
+ * Downloads a package into npx's cache without running it, once per package, so its step
128
+ * starts at once. A failed download is ignored: the step itself will say what failed.
129
+ * @param root - The repo.
130
+ * @param spec - The package, as npx takes it.
131
+ * @returns When it is done.
132
+ * @example void download(root, "@ramon-sg/claude-cloud@1.0.0")
133
+ */
134
+ export function download(root, spec) {
135
+ const known = downloads.get(spec);
136
+ if (known !== undefined)
137
+ return known;
138
+ const started = run("npx", ["--yes", "--package", spec, "--", "node", "-e", ""], { cwd: root }).then(() => undefined);
139
+ downloads.set(spec, started);
140
+ return started;
141
+ }
142
+ /**
143
+ * Runs `npx --yes <args>` in the repo: quietly, or with its output on the terminal with
144
+ * `--verbose`. It must not ask anything: the person answered before the steps ran.
145
+ * @param root - The repo.
146
+ * @param args - The package and its arguments.
147
+ * @returns Whether it exited 0, and what it printed.
148
+ * @example await runNpx(root, ["@ramon-sg/claude-cloud@1.0.0", "init"])
149
+ */
150
+ export async function runNpx(root, args) {
151
+ const result = await run("npx", ["--yes", ...args], { cwd: root, inherit: verbose });
152
+ return { ok: result.status === 0, output: `${result.out}${result.err}` };
153
+ }
154
+ /**
155
+ * The end of a failed tool's output, indented, to show under its step.
156
+ * @param output - What it printed.
157
+ * @returns Its last lines, or an empty text.
158
+ * @example failedOutput("npm error 404\n") // " npm error 404\n"
159
+ */
160
+ export function failedOutput(output) {
161
+ const lines = output.trimEnd().split("\n").filter((line) => line.trim() !== "");
162
+ return lines.slice(-FAILED_LINES).map((line) => ` ${line}\n`).join("");
163
+ }
164
+ /**
165
+ * Runs `npx --yes <args>` in the repo and returns what it printed on stdout.
166
+ * @param root - The repo.
167
+ * @param args - The package and its arguments.
168
+ * @param lookup - True to give up after the lookups' time limit, for a read that only informs.
169
+ * @returns Its stdout and exit code, or null if npx could not start.
170
+ * @example await captureNpx(root, ["@ramon-sg/issue-flow@0.2.0", "doctor", "--json"])
171
+ */
172
+ export async function captureNpx(root, args, lookup = false) {
173
+ const result = await run("npx", ["--yes", ...args], { cwd: root, ...(lookup ? { timeout: LOOKUP_TIMEOUT_MS } : {}) });
174
+ return result.status === null && result.out === "" ? null : { out: result.out, status: result.status };
175
+ }
package/package.json CHANGED
@@ -1,6 +1,22 @@
1
1
  {
2
2
  "name": "agent-kitchen",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.3.0",
4
+ "description": "One command to install, update and check a repo's agent tooling: the issue flow and its skills, the general skills, Claude cloud sessions and claude-repo-guard.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": { "agent-kitchen": "dist/cli.js" },
8
+ "files": ["dist"],
9
+ "engines": { "node": ">=20" },
10
+ "publishConfig": { "access": "public", "provenance": true },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/ramon-sg/agent-kitchen.git"
14
+ },
15
+ "scripts": {
16
+ "build": "tsc",
17
+ "test": "tsc && node --test test/*.test.mjs",
18
+ "prepublishOnly": "npm test"
19
+ },
20
+ "dependencies": { "@clack/prompts": "1.8.1" },
21
+ "devDependencies": { "@types/node": "^24.0.0", "typescript": "^5.9.3" }
22
+ }