agent-kitchen 0.2.0 → 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.
@@ -2,12 +2,16 @@
2
2
  * @file What agent-kitchen reads of the files of `skills-manager`: which skills of a catalog
3
3
  * the lock has and at which tag (the lock is where the manifest's `paths.lock` says), the
4
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.
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
7
  */
8
+ import { existsSync, readdirSync } from "node:fs";
9
+ import { join } from "node:path";
8
10
  import { dependencyVersion, isObject, readJson, writeJson } from "./repo.js";
9
11
  import { SKILLS_LOCK, SKILLS_MANAGER, SKILLS_MANIFEST } from "./piece.js";
10
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";
11
15
  /**
12
16
  * Where the repo's skills lock is: the manifest's `paths.lock`, or the default.
13
17
  * @param root - The repo.
@@ -32,13 +36,32 @@ export function lockedSkills(root) {
32
36
  * Runs `skills-manager update <name>` for each skill, stopping at the first that fails.
33
37
  * @param root - The repo.
34
38
  * @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
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
37
41
  */
38
- export function updateSkills(root, names) {
42
+ export async function updateSkills(root, names) {
39
43
  const spec = skillsManagerSpec(root);
40
- const failed = names.find((name) => !runNpx(root, [spec, "update", name]));
41
- return failed === undefined ? null : `npx ${spec} update ${failed}`;
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);
42
65
  }
43
66
  /**
44
67
  * The skills of a catalog in the repo's lock.
package/dist/tools.d.ts CHANGED
@@ -1,3 +1,9 @@
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;
1
7
  /**
2
8
  * Compares two release versions.
3
9
  * @param left - A version like 0.2.0.
@@ -14,35 +20,58 @@ export declare function compareVersions(left: string, right: string): number;
14
20
  */
15
21
  export declare function isRelease(version: string): boolean;
16
22
  /**
17
- * Runs `npx --yes <args>` in the repo, with its output and questions on this terminal.
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.
18
39
  * @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"])
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")
22
43
  */
23
- export declare function runNpx(root: string, args: string[]): boolean;
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
+ }
24
50
  /**
25
- * Runs `npx --yes <args>` in the repo and returns what it printed.
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.
26
53
  * @param root - The repo.
27
54
  * @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"])
55
+ * @returns Whether it exited 0, and what it printed.
56
+ * @example await runNpx(root, ["@ramon-sg/claude-cloud@1.0.0", "init"])
30
57
  */
31
- export declare function captureNpx(root: string, args: string[]): {
32
- out: string;
33
- status: number | null;
34
- } | null;
58
+ export declare function runNpx(root: string, args: string[]): Promise<ToolRun>;
35
59
  /**
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"
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"
40
64
  */
41
- export declare function npmLatest(name: string): string | null;
65
+ export declare function failedOutput(output: string): string;
42
66
  /**
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"
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"])
47
73
  */
48
- export declare function latestTag(repo: string): string | null;
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 CHANGED
@@ -2,16 +2,36 @@
2
2
  * @file How agent-kitchen reaches the other tools and their releases: `npx` to run them (always
3
3
  * by their scoped name or GitHub spec, because the unscoped names on npm belong to other
4
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.
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.
7
11
  */
8
- import { spawnSync } from "node:child_process";
12
+ import { spawn } from "node:child_process";
9
13
  /** A release version. */
10
14
  const VERSION = /^(\d+)\.(\d+)\.(\d+)$/;
11
15
  /** How long a lookup of the latest version may take before it counts as unknown. */
12
16
  const LOOKUP_TIMEOUT_MS = 20_000;
13
17
  /** A release tag in `git ls-remote --tags --refs` output. */
14
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
+ }
15
35
  /**
16
36
  * Compares two release versions.
17
37
  * @param left - A version like 0.2.0.
@@ -39,47 +59,117 @@ export function isRelease(version) {
39
59
  return VERSION.test(version);
40
60
  }
41
61
  /**
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"])
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.
47
67
  */
48
- export function runNpx(root, args) {
49
- return spawnSync("npx", ["--yes", ...args], { cwd: root, stdio: "inherit" }).status === 0;
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
+ });
50
83
  }
51
84
  /**
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"])
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.
57
89
  */
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;
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;
61
97
  }
62
98
  /**
63
99
  * The latest version of a package on npm.
64
100
  * @param name - The package.
65
101
  * @returns The version, or null if npm cannot tell.
66
- * @example npmLatest("@ramon-sg/claude-cloud") // "1.0.0"
102
+ * @example await npmLatest("@ramon-sg/claude-cloud") // "1.0.0"
67
103
  */
68
104
  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;
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
+ });
72
110
  }
73
111
  /**
74
112
  * The highest `vX.Y.Z` tag of a GitHub repo.
75
113
  * @param repo - The repo, as owner/name.
76
114
  * @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"
115
+ * @example await latestTag("ramon-sg/agent-skills") // "0.1.0"
78
116
  */
79
117
  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;
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 };
85
175
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-kitchen",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
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
5
  "license": "MIT",
6
6
  "type": "module",