agent-kitchen 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/repo.d.ts ADDED
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Whether a value is a plain JSON object.
3
+ * @param value - The value.
4
+ * @returns True for an object that is not an array nor null.
5
+ * @example isObject({}) // true
6
+ */
7
+ export declare function isObject(value: unknown): value is Record<string, unknown>;
8
+ /**
9
+ * The main checkout of the repo a folder is in.
10
+ * @param cwd - The folder.
11
+ * @param command - The command that runs, for the error.
12
+ * @returns Its root, with symlinks resolved.
13
+ * @throws KitError outside a git checkout or inside a worktree.
14
+ * @example mainCheckout(process.cwd(), "agent-kitchen") // "/code/shop"
15
+ */
16
+ export declare function mainCheckout(cwd: string, command: string): string;
17
+ /**
18
+ * Reads a JSON object of the repo.
19
+ * @param root - The repo.
20
+ * @param path - The file, relative to the repo.
21
+ * @returns The object, or null if the file does not exist.
22
+ * @throws KitError if it is not valid JSON or not an object: it is never overwritten then.
23
+ * @example readJson(root, ".claude/cloud.json")
24
+ */
25
+ export declare function readJson(root: string, path: string): Record<string, unknown> | null;
26
+ /**
27
+ * Writes a JSON file of the repo, indented by two spaces, creating its folder.
28
+ * @param root - The repo.
29
+ * @param path - The file, relative to the repo.
30
+ * @param value - What to write.
31
+ * @example writeJson(root, ".claude/cloud.json", { version: "1.1.0" })
32
+ */
33
+ export declare function writeJson(root: string, path: string, value: unknown): void;
34
+ /**
35
+ * The version of a dependency in `package.json`, development or not.
36
+ * @param pkg - The package.json, or null.
37
+ * @param name - The package.
38
+ * @returns The version, or null if it is not there.
39
+ * @example dependencyVersion(pkg, "@ramon-sg/issue-flow") // "0.2.0"
40
+ */
41
+ export declare function dependencyVersion(pkg: Record<string, unknown> | null, name: string): string | null;
42
+ /**
43
+ * The repo's package manager: the `packageManager` field, then its lockfile, then npm.
44
+ * @param root - The repo.
45
+ * @returns `npm`, `pnpm`, `bun` or `yarn`.
46
+ * @example packageManager(root) // "pnpm"
47
+ */
48
+ export declare function packageManager(root: string): string;
package/dist/repo.js ADDED
@@ -0,0 +1,117 @@
1
+ /**
2
+ * @file The repo agent-kitchen works on: its main checkout (never a worktree, whose files are
3
+ * not the ones the team commits from), the JSON files the pieces keep in it, and its
4
+ * package manager.
5
+ */
6
+ import { spawnSync } from "node:child_process";
7
+ import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
8
+ import { dirname, join } from "node:path";
9
+ import { KitError } from "./output.js";
10
+ /**
11
+ * Whether a value is a plain JSON object.
12
+ * @param value - The value.
13
+ * @returns True for an object that is not an array nor null.
14
+ * @example isObject({}) // true
15
+ */
16
+ export function isObject(value) {
17
+ return typeof value === "object" && value !== null && !Array.isArray(value);
18
+ }
19
+ /**
20
+ * Runs git in a folder.
21
+ * @param cwd - The folder.
22
+ * @param args - The arguments.
23
+ * @returns Its trimmed stdout, or null if it failed.
24
+ */
25
+ function git(cwd, args) {
26
+ const result = spawnSync("git", args, { cwd, encoding: "utf8" });
27
+ return result.status === 0 ? result.stdout.trim() : null;
28
+ }
29
+ /**
30
+ * The main checkout of the repo a folder is in.
31
+ * @param cwd - The folder.
32
+ * @param command - The command that runs, for the error.
33
+ * @returns Its root, with symlinks resolved.
34
+ * @throws KitError outside a git checkout or inside a worktree.
35
+ * @example mainCheckout(process.cwd(), "agent-kitchen") // "/code/shop"
36
+ */
37
+ export function mainCheckout(cwd, command) {
38
+ const common = git(cwd, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
39
+ const top = git(cwd, ["rev-parse", "--show-toplevel"]);
40
+ if (common === null || top === null)
41
+ throw new KitError(`run ${command} inside a checkout of the repo.`);
42
+ const main = realpathSync(dirname(common));
43
+ if (realpathSync(top) !== main)
44
+ throw new KitError(`run ${command} in the main checkout, ${main}, not in a worktree.`);
45
+ return main;
46
+ }
47
+ /**
48
+ * Reads a JSON object of the repo.
49
+ * @param root - The repo.
50
+ * @param path - The file, relative to the repo.
51
+ * @returns The object, or null if the file does not exist.
52
+ * @throws KitError if it is not valid JSON or not an object: it is never overwritten then.
53
+ * @example readJson(root, ".claude/cloud.json")
54
+ */
55
+ export function readJson(root, path) {
56
+ const file = join(root, path);
57
+ if (!existsSync(file))
58
+ return null;
59
+ let value;
60
+ try {
61
+ value = JSON.parse(readFileSync(file, "utf8"));
62
+ }
63
+ catch (error) {
64
+ throw new KitError(`${path} is not valid JSON (${error instanceof Error ? error.message : String(error)}); fix it and run again.`);
65
+ }
66
+ if (!isObject(value))
67
+ throw new KitError(`${path} is not a JSON object; fix it and run again.`);
68
+ return value;
69
+ }
70
+ /**
71
+ * Writes a JSON file of the repo, indented by two spaces, creating its folder.
72
+ * @param root - The repo.
73
+ * @param path - The file, relative to the repo.
74
+ * @param value - What to write.
75
+ * @example writeJson(root, ".claude/cloud.json", { version: "1.1.0" })
76
+ */
77
+ export function writeJson(root, path, value) {
78
+ const file = join(root, path);
79
+ mkdirSync(dirname(file), { recursive: true });
80
+ writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
81
+ }
82
+ /**
83
+ * The version of a dependency in `package.json`, development or not.
84
+ * @param pkg - The package.json, or null.
85
+ * @param name - The package.
86
+ * @returns The version, or null if it is not there.
87
+ * @example dependencyVersion(pkg, "@ramon-sg/issue-flow") // "0.2.0"
88
+ */
89
+ export function dependencyVersion(pkg, name) {
90
+ for (const field of ["devDependencies", "dependencies"]) {
91
+ const deps = pkg?.[field];
92
+ if (isObject(deps) && typeof deps[name] === "string")
93
+ return deps[name];
94
+ }
95
+ return null;
96
+ }
97
+ /** The lockfile of each package manager, in the order they are looked for. */
98
+ const LOCKFILES = [
99
+ ["bun.lock", "bun"],
100
+ ["bun.lockb", "bun"],
101
+ ["pnpm-lock.yaml", "pnpm"],
102
+ ["yarn.lock", "yarn"],
103
+ ["package-lock.json", "npm"],
104
+ ];
105
+ /**
106
+ * The repo's package manager: the `packageManager` field, then its lockfile, then npm.
107
+ * @param root - The repo.
108
+ * @returns `npm`, `pnpm`, `bun` or `yarn`.
109
+ * @example packageManager(root) // "pnpm"
110
+ */
111
+ export function packageManager(root) {
112
+ const pkg = readJson(root, "package.json");
113
+ const field = typeof pkg?.packageManager === "string" ? pkg.packageManager.split("@")[0] : undefined;
114
+ if (field !== undefined && field !== "")
115
+ return field;
116
+ return LOCKFILES.find(([file]) => existsSync(join(root, file)))?.[1] ?? "npm";
117
+ }
@@ -0,0 +1,62 @@
1
+ /** A skill in the lock: its name and the tag it is at. */
2
+ export interface LockedSkill {
3
+ name: string;
4
+ tag: string | null;
5
+ }
6
+ /**
7
+ * Where the repo's skills lock is: the manifest's `paths.lock`, or the default.
8
+ * @param root - The repo.
9
+ * @returns The path, relative to the repo.
10
+ * @example lockPath(root) // ".agents/skills.lock.json"
11
+ */
12
+ export declare function lockPath(root: string): string;
13
+ /**
14
+ * Every skill in the repo's lock, by name.
15
+ * @param root - The repo.
16
+ * @returns The lock's entries.
17
+ * @example Object.keys(lockedSkills(root)) // ["close-pr", "code-audit"]
18
+ */
19
+ export declare function lockedSkills(root: string): Record<string, unknown>;
20
+ /**
21
+ * Runs `skills-manager update <name>` for each skill, stopping at the first that fails.
22
+ * @param root - The repo.
23
+ * @param names - The skills.
24
+ * @returns The command that failed, to retry it, or null if every one worked.
25
+ * @example updateSkills(root, ["close-pr", "pr-review-comments"]) // null
26
+ */
27
+ export declare function updateSkills(root: string, names: string[]): string | null;
28
+ /**
29
+ * The skills of a catalog in the repo's lock.
30
+ * @param root - The repo.
31
+ * @param catalog - The catalog, as owner/name.
32
+ * @returns Name and tag of each one.
33
+ * @example catalogSkills(root, "ramon-sg/issue-flow") // [{ name: "close-pr", tag: "v0.2.0" }]
34
+ */
35
+ export declare function catalogSkills(root: string, catalog: string): LockedSkill[];
36
+ /**
37
+ * The catalogs in the repo's skills manifest, with the ref each one is pinned to.
38
+ * @param root - The repo.
39
+ * @returns Each catalog and its ref (null if it follows the latest release).
40
+ * @example manifestCatalogs(root) // [{ catalog: "ramon-sg/issue-flow", ref: "v0.2.0" }]
41
+ */
42
+ export declare function manifestCatalogs(root: string): {
43
+ catalog: string;
44
+ ref: string | null;
45
+ }[];
46
+ /**
47
+ * Pins a catalog of the manifest to a tag, so `skills-manager update` moves its skills there.
48
+ * @param root - The repo.
49
+ * @param catalog - The catalog.
50
+ * @param tag - The tag, like v0.2.0.
51
+ * @returns Whether the manifest has the catalog.
52
+ * @example pinCatalog(root, "ramon-sg/issue-flow", "v0.2.0") // true
53
+ */
54
+ export declare function pinCatalog(root: string, catalog: string, tag: string): boolean;
55
+ /**
56
+ * The `skills-manager` to run: the version the repo pins in package.json, so the lock is
57
+ * written by the same version every time, or the latest 1.x.
58
+ * @param root - The repo.
59
+ * @returns The npx spec.
60
+ * @example skillsManagerSpec(root) // "@ramon-sg/skills-manager@1.1.0"
61
+ */
62
+ export declare function skillsManagerSpec(root: string): string;
@@ -0,0 +1,93 @@
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), and updating skills one by one, because `skills-manager update`
6
+ * takes a single name.
7
+ */
8
+ import { dependencyVersion, isObject, readJson, writeJson } from "./repo.js";
9
+ import { SKILLS_LOCK, SKILLS_MANAGER, SKILLS_MANIFEST } from "./piece.js";
10
+ import { runNpx } from "./tools.js";
11
+ /**
12
+ * Where the repo's skills lock is: the manifest's `paths.lock`, or the default.
13
+ * @param root - The repo.
14
+ * @returns The path, relative to the repo.
15
+ * @example lockPath(root) // ".agents/skills.lock.json"
16
+ */
17
+ export function lockPath(root) {
18
+ const paths = readJson(root, SKILLS_MANIFEST)?.paths;
19
+ return isObject(paths) && typeof paths.lock === "string" && paths.lock !== "" ? paths.lock : SKILLS_LOCK;
20
+ }
21
+ /**
22
+ * Every skill in the repo's lock, by name.
23
+ * @param root - The repo.
24
+ * @returns The lock's entries.
25
+ * @example Object.keys(lockedSkills(root)) // ["close-pr", "code-audit"]
26
+ */
27
+ export function lockedSkills(root) {
28
+ const lock = readJson(root, lockPath(root));
29
+ return isObject(lock?.skills) ? lock.skills : {};
30
+ }
31
+ /**
32
+ * Runs `skills-manager update <name>` for each skill, stopping at the first that fails.
33
+ * @param root - The repo.
34
+ * @param names - The skills.
35
+ * @returns The command that failed, to retry it, or null if every one worked.
36
+ * @example updateSkills(root, ["close-pr", "pr-review-comments"]) // null
37
+ */
38
+ export function updateSkills(root, names) {
39
+ const spec = skillsManagerSpec(root);
40
+ const failed = names.find((name) => !runNpx(root, [spec, "update", name]));
41
+ return failed === undefined ? null : `npx ${spec} update ${failed}`;
42
+ }
43
+ /**
44
+ * The skills of a catalog in the repo's lock.
45
+ * @param root - The repo.
46
+ * @param catalog - The catalog, as owner/name.
47
+ * @returns Name and tag of each one.
48
+ * @example catalogSkills(root, "ramon-sg/issue-flow") // [{ name: "close-pr", tag: "v0.2.0" }]
49
+ */
50
+ export function catalogSkills(root, catalog) {
51
+ const skills = lockedSkills(root);
52
+ return Object.entries(skills)
53
+ .filter(([, entry]) => isObject(entry) && entry.source === `catalog:${catalog}`)
54
+ .map(([name, entry]) => ({ name, tag: isObject(entry) && typeof entry.tag === "string" ? entry.tag : null }));
55
+ }
56
+ /**
57
+ * The catalogs in the repo's skills manifest, with the ref each one is pinned to.
58
+ * @param root - The repo.
59
+ * @returns Each catalog and its ref (null if it follows the latest release).
60
+ * @example manifestCatalogs(root) // [{ catalog: "ramon-sg/issue-flow", ref: "v0.2.0" }]
61
+ */
62
+ export function manifestCatalogs(root) {
63
+ const manifest = readJson(root, SKILLS_MANIFEST);
64
+ const sources = Array.isArray(manifest?.sources) ? manifest.sources.filter(isObject) : [];
65
+ return sources.flatMap((source) => (typeof source.catalog === "string" ? [{ catalog: source.catalog, ref: typeof source.ref === "string" ? source.ref : null }] : []));
66
+ }
67
+ /**
68
+ * Pins a catalog of the manifest to a tag, so `skills-manager update` moves its skills there.
69
+ * @param root - The repo.
70
+ * @param catalog - The catalog.
71
+ * @param tag - The tag, like v0.2.0.
72
+ * @returns Whether the manifest has the catalog.
73
+ * @example pinCatalog(root, "ramon-sg/issue-flow", "v0.2.0") // true
74
+ */
75
+ export function pinCatalog(root, catalog, tag) {
76
+ const manifest = readJson(root, SKILLS_MANIFEST);
77
+ const sources = Array.isArray(manifest?.sources) ? manifest.sources : [];
78
+ if (manifest === null || !sources.some((source) => isObject(source) && source.catalog === catalog))
79
+ return false;
80
+ const moved = sources.map((source) => (isObject(source) && source.catalog === catalog ? { ...source, ref: tag } : source));
81
+ writeJson(root, SKILLS_MANIFEST, { ...manifest, sources: moved });
82
+ return true;
83
+ }
84
+ /**
85
+ * The `skills-manager` to run: the version the repo pins in package.json, so the lock is
86
+ * written by the same version every time, or the latest 1.x.
87
+ * @param root - The repo.
88
+ * @returns The npx spec.
89
+ * @example skillsManagerSpec(root) // "@ramon-sg/skills-manager@1.1.0"
90
+ */
91
+ export function skillsManagerSpec(root) {
92
+ return `${SKILLS_MANAGER}@${dependencyVersion(readJson(root, "package.json"), SKILLS_MANAGER) ?? "1"}`;
93
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Compares two release versions.
3
+ * @param left - A version like 0.2.0.
4
+ * @param right - Another.
5
+ * @returns Negative if left is older, 0 if equal, positive if newer.
6
+ * @example compareVersions("0.10.0", "0.9.1") // 1
7
+ */
8
+ export declare function compareVersions(left: string, right: string): number;
9
+ /**
10
+ * Whether a version is a release like 1.2.3.
11
+ * @param version - The text.
12
+ * @returns True for a release version.
13
+ * @example isRelease("1.2.3") // true
14
+ */
15
+ export declare function isRelease(version: string): boolean;
16
+ /**
17
+ * Runs `npx --yes <args>` in the repo, with its output and questions on this terminal.
18
+ * @param root - The repo.
19
+ * @param args - The package and its arguments.
20
+ * @returns Whether it exited 0.
21
+ * @example runNpx(root, ["@ramon-sg/claude-cloud@1.0.0", "init"])
22
+ */
23
+ export declare function runNpx(root: string, args: string[]): boolean;
24
+ /**
25
+ * Runs `npx --yes <args>` in the repo and returns what it printed.
26
+ * @param root - The repo.
27
+ * @param args - The package and its arguments.
28
+ * @returns Its stdout and exit code, or null if npx could not start.
29
+ * @example captureNpx(root, ["@ramon-sg/issue-flow@0.2.0", "doctor", "--json"])
30
+ */
31
+ export declare function captureNpx(root: string, args: string[]): {
32
+ out: string;
33
+ status: number | null;
34
+ } | null;
35
+ /**
36
+ * The latest version of a package on npm.
37
+ * @param name - The package.
38
+ * @returns The version, or null if npm cannot tell.
39
+ * @example npmLatest("@ramon-sg/claude-cloud") // "1.0.0"
40
+ */
41
+ export declare function npmLatest(name: string): string | null;
42
+ /**
43
+ * The highest `vX.Y.Z` tag of a GitHub repo.
44
+ * @param repo - The repo, as owner/name.
45
+ * @returns The version without the `v`, or null if it has none or git cannot reach it.
46
+ * @example latestTag("ramon-sg/agent-skills") // "0.1.0"
47
+ */
48
+ export declare function latestTag(repo: string): string | null;
package/dist/tools.js ADDED
@@ -0,0 +1,85 @@
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. A lookup that fails, or takes over 20 seconds, gives null: the
6
+ * piece is shown, not offered. git never asks for credentials here.
7
+ */
8
+ import { spawnSync } from "node:child_process";
9
+ /** A release version. */
10
+ const VERSION = /^(\d+)\.(\d+)\.(\d+)$/;
11
+ /** How long a lookup of the latest version may take before it counts as unknown. */
12
+ const LOOKUP_TIMEOUT_MS = 20_000;
13
+ /** A release tag in `git ls-remote --tags --refs` output. */
14
+ const TAG_REF = /refs\/tags\/v(\d+\.\d+\.\d+)$/;
15
+ /**
16
+ * Compares two release versions.
17
+ * @param left - A version like 0.2.0.
18
+ * @param right - Another.
19
+ * @returns Negative if left is older, 0 if equal, positive if newer.
20
+ * @example compareVersions("0.10.0", "0.9.1") // 1
21
+ */
22
+ export function compareVersions(left, right) {
23
+ const parts = (version) => (VERSION.exec(version) ?? []).slice(1).map(Number);
24
+ const [a, b] = [parts(left), parts(right)];
25
+ for (let index = 0; index < 3; index += 1) {
26
+ const diff = (a[index] ?? 0) - (b[index] ?? 0);
27
+ if (diff !== 0)
28
+ return diff;
29
+ }
30
+ return 0;
31
+ }
32
+ /**
33
+ * Whether a version is a release like 1.2.3.
34
+ * @param version - The text.
35
+ * @returns True for a release version.
36
+ * @example isRelease("1.2.3") // true
37
+ */
38
+ export function isRelease(version) {
39
+ return VERSION.test(version);
40
+ }
41
+ /**
42
+ * Runs `npx --yes <args>` in the repo, with its output and questions on this terminal.
43
+ * @param root - The repo.
44
+ * @param args - The package and its arguments.
45
+ * @returns Whether it exited 0.
46
+ * @example runNpx(root, ["@ramon-sg/claude-cloud@1.0.0", "init"])
47
+ */
48
+ export function runNpx(root, args) {
49
+ return spawnSync("npx", ["--yes", ...args], { cwd: root, stdio: "inherit" }).status === 0;
50
+ }
51
+ /**
52
+ * Runs `npx --yes <args>` in the repo and returns what it printed.
53
+ * @param root - The repo.
54
+ * @param args - The package and its arguments.
55
+ * @returns Its stdout and exit code, or null if npx could not start.
56
+ * @example captureNpx(root, ["@ramon-sg/issue-flow@0.2.0", "doctor", "--json"])
57
+ */
58
+ export function captureNpx(root, args) {
59
+ const result = spawnSync("npx", ["--yes", ...args], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] });
60
+ return result.error === undefined ? { out: result.stdout, status: result.status } : null;
61
+ }
62
+ /**
63
+ * The latest version of a package on npm.
64
+ * @param name - The package.
65
+ * @returns The version, or null if npm cannot tell.
66
+ * @example npmLatest("@ramon-sg/claude-cloud") // "1.0.0"
67
+ */
68
+ export function npmLatest(name) {
69
+ const result = spawnSync("npm", ["view", name, "version", "--fetch-retries=0"], { encoding: "utf8", timeout: LOOKUP_TIMEOUT_MS });
70
+ const version = result.status === 0 ? result.stdout.trim() : "";
71
+ return isRelease(version) ? version : null;
72
+ }
73
+ /**
74
+ * The highest `vX.Y.Z` tag of a GitHub repo.
75
+ * @param repo - The repo, as owner/name.
76
+ * @returns The version without the `v`, or null if it has none or git cannot reach it.
77
+ * @example latestTag("ramon-sg/agent-skills") // "0.1.0"
78
+ */
79
+ export function latestTag(repo) {
80
+ const result = spawnSync("git", ["ls-remote", "--tags", "--refs", `https://github.com/${repo}.git`], { encoding: "utf8", timeout: LOOKUP_TIMEOUT_MS, env: { ...process.env, GIT_TERMINAL_PROMPT: "0" } });
81
+ if (result.status !== 0)
82
+ return null;
83
+ const versions = result.stdout.split("\n").flatMap((line) => TAG_REF.exec(line.trim())?.slice(1, 2) ?? []);
84
+ return versions.sort(compareVersions).at(-1) ?? null;
85
+ }
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.2.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
+ }