agent-cloud-kit 0.0.0-stage → 1.0.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +229 -2
- package/dist/ca.js +30 -0
- package/dist/cli.js +418 -0
- package/dist/config.js +197 -0
- package/dist/constants.js +62 -0
- package/dist/detect.js +265 -0
- package/dist/docker.js +95 -0
- package/dist/doctor.js +200 -0
- package/dist/init.js +136 -0
- package/dist/installers.js +463 -0
- package/dist/launcher.js +140 -0
- package/dist/manual.js +211 -0
- package/dist/prepare/detect.js +57 -0
- package/dist/prepare/index.js +492 -0
- package/dist/prepare/prompts.js +43 -0
- package/dist/prepare/questions.js +70 -0
- package/dist/run.js +113 -0
- package/dist/session.js +245 -0
- package/dist/setup.js +277 -0
- package/dist/skill.js +148 -0
- package/dist/state.js +147 -0
- package/dist/steps.js +46 -0
- package/package.json +46 -4
- package/schema.json +72 -0
- package/skills/cloud-ready/SKILL.md +179 -0
- package/skills/cloud-ready/reference/compat.md +110 -0
- package/skills/cloud-ready/reference/knowledge.md +56 -0
package/dist/skill.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The `cloud-ready` skill that `init` copies into the repo, and the link Claude Code needs
|
|
3
|
+
* to discover it.
|
|
4
|
+
*/
|
|
5
|
+
import { cpSync, existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { dirname, join } from "node:path";
|
|
7
|
+
/** Repo-relative directory of the `cloud-ready` skill. */
|
|
8
|
+
export const SKILL_DIR = ".agents/skills/cloud-ready";
|
|
9
|
+
/** Directory of the skill inside this package. */
|
|
10
|
+
const PACKAGE_SKILL_DIR = "skills/cloud-ready";
|
|
11
|
+
/** Repo-relative folder of the skills that Claude Code discovers. */
|
|
12
|
+
const CLAUDE_SKILLS_DIR = ".claude/skills";
|
|
13
|
+
/** Repo-relative folder of the skills that skills-manager installs. */
|
|
14
|
+
const AGENTS_SKILLS_DIR = ".agents/skills";
|
|
15
|
+
/** Repo-relative path of the link to the skill that Claude Code discovers. */
|
|
16
|
+
const SKILL_LINK = `${CLAUDE_SKILLS_DIR}/cloud-ready`;
|
|
17
|
+
/** Target of `SKILL_LINK`, relative to the folder that holds it. */
|
|
18
|
+
const SKILL_LINK_TARGET = `../../${SKILL_DIR}`;
|
|
19
|
+
/** Matches the frontmatter at the start of a skill file; group 1 is its body. */
|
|
20
|
+
const FRONTMATTER = /^---\r?\n([\s\S]*?)\r?\n---/;
|
|
21
|
+
/** Matches the `metadata:` block of the frontmatter, up to the next top-level key or the end. */
|
|
22
|
+
const METADATA_BLOCK = /^metadata:[ \t]*\n((?:[ \t]+.*(?:\n|$))*)/m;
|
|
23
|
+
/** Matches an indented `version:` line inside the metadata block; the value may be quoted. */
|
|
24
|
+
const VERSION_LINE = /^\s+version:\s*"?([^"\n]+)"?$/m;
|
|
25
|
+
/**
|
|
26
|
+
* Reads `metadata.version` from the frontmatter of the installed skill.
|
|
27
|
+
*
|
|
28
|
+
* @param repo - Repo root.
|
|
29
|
+
* @returns The version, or undefined when the skill or its version is missing.
|
|
30
|
+
* @example
|
|
31
|
+
* skillVersion("/home/user/app"); // "1.0.0"
|
|
32
|
+
*/
|
|
33
|
+
export function skillVersion(repo) {
|
|
34
|
+
const file = join(repo, SKILL_DIR, "SKILL.md");
|
|
35
|
+
if (!existsSync(file))
|
|
36
|
+
return undefined;
|
|
37
|
+
const frontmatter = FRONTMATTER.exec(readFileSync(file, "utf8"))?.[1] ?? "";
|
|
38
|
+
const block = METADATA_BLOCK.exec(frontmatter)?.[1] ?? "";
|
|
39
|
+
return VERSION_LINE.exec(block)?.[1]?.trim();
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Sets `metadata.version` in the frontmatter of a skill file, adding the block when it is missing.
|
|
43
|
+
*
|
|
44
|
+
* @param text - Content of SKILL.md.
|
|
45
|
+
* @param version - Version to write.
|
|
46
|
+
* @returns The new content; unchanged when the file has no frontmatter.
|
|
47
|
+
* @example
|
|
48
|
+
* withVersion('---\nname: x\nmetadata:\n version: "0.0.0"\n---\n', "1.0.0"); // version "1.0.0"
|
|
49
|
+
*/
|
|
50
|
+
function withVersion(text, version) {
|
|
51
|
+
const match = FRONTMATTER.exec(text);
|
|
52
|
+
if (!match)
|
|
53
|
+
return text;
|
|
54
|
+
const frontmatter = match[1] ?? "";
|
|
55
|
+
const line = ` version: "${version}"`;
|
|
56
|
+
const block = METADATA_BLOCK.exec(frontmatter);
|
|
57
|
+
let updated;
|
|
58
|
+
if (!block) {
|
|
59
|
+
updated = `${frontmatter}\nmetadata:\n${line}`;
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
const body = block[1] ?? "";
|
|
63
|
+
const newBody = VERSION_LINE.test(body) ? body.replace(VERSION_LINE, line) : `${line}\n${body}`;
|
|
64
|
+
const head = block[0].slice(0, block[0].length - body.length);
|
|
65
|
+
updated = frontmatter.slice(0, block.index) + head + newBody + frontmatter.slice(block.index + block[0].length);
|
|
66
|
+
}
|
|
67
|
+
return `---\n${updated}\n---${text.slice(match[0].length)}`;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Copies the `cloud-ready` skill of this package into the repo, with `metadata.version` set to the
|
|
71
|
+
* package version. Leaves it untouched when it is already at that version; otherwise replaces the
|
|
72
|
+
* whole folder atomically (copy to a temp folder, then rename).
|
|
73
|
+
*
|
|
74
|
+
* @param repo - Repo root.
|
|
75
|
+
* @param packageRoot - Root of this package, which holds `skills/cloud-ready`.
|
|
76
|
+
* @param packageVersion - Version of this package.
|
|
77
|
+
* @returns A line describing the outcome.
|
|
78
|
+
* @throws When the package has no skill or the copy fails (the installed skill is kept).
|
|
79
|
+
* @example
|
|
80
|
+
* installSkill("/home/user/app", "/usr/lib/node_modules/agent-cloud-kit", "1.0.0");
|
|
81
|
+
* // "installed .agents/skills/cloud-ready 1.0.0"
|
|
82
|
+
*/
|
|
83
|
+
export function installSkill(repo, packageRoot, packageVersion) {
|
|
84
|
+
const current = skillVersion(repo);
|
|
85
|
+
if (current === packageVersion)
|
|
86
|
+
return `${SKILL_DIR} already at ${packageVersion}`;
|
|
87
|
+
const source = join(packageRoot, PACKAGE_SKILL_DIR);
|
|
88
|
+
if (!existsSync(join(source, "SKILL.md")))
|
|
89
|
+
throw new Error(`${PACKAGE_SKILL_DIR}/SKILL.md not found in ${packageRoot}`);
|
|
90
|
+
const target = join(repo, SKILL_DIR);
|
|
91
|
+
const temp = `${target}.${process.pid}.tmp`;
|
|
92
|
+
const old = `${target}.${process.pid}.old`;
|
|
93
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
94
|
+
try {
|
|
95
|
+
rmSync(temp, { recursive: true, force: true });
|
|
96
|
+
cpSync(source, temp, { recursive: true });
|
|
97
|
+
const skillFile = join(temp, "SKILL.md");
|
|
98
|
+
writeFileSync(skillFile, withVersion(readFileSync(skillFile, "utf8"), packageVersion));
|
|
99
|
+
if (existsSync(target))
|
|
100
|
+
renameSync(target, old);
|
|
101
|
+
renameSync(temp, target);
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
rmSync(temp, { recursive: true, force: true });
|
|
105
|
+
if (existsSync(old) && !existsSync(target))
|
|
106
|
+
renameSync(old, target);
|
|
107
|
+
throw err;
|
|
108
|
+
}
|
|
109
|
+
rmSync(old, { recursive: true, force: true });
|
|
110
|
+
return current === undefined ? `installed ${SKILL_DIR} ${packageVersion}` : `updated ${SKILL_DIR} from ${current} to ${packageVersion}`;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Tells whether a path exists, counting a broken symlink as existing.
|
|
114
|
+
*
|
|
115
|
+
* @param path - Absolute path.
|
|
116
|
+
* @returns True when something is there.
|
|
117
|
+
*/
|
|
118
|
+
function present(path) {
|
|
119
|
+
try {
|
|
120
|
+
lstatSync(path);
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Links the skill where Claude Code discovers it: Claude Code does not read `.agents/skills/`, so
|
|
129
|
+
* this creates the relative symlink `.claude/skills/cloud-ready -> ../../.agents/skills/cloud-ready`.
|
|
130
|
+
* Does nothing when `.claude/skills` already resolves to `.agents/skills` or the entry exists.
|
|
131
|
+
*
|
|
132
|
+
* @param repo - Repo root.
|
|
133
|
+
* @returns A line describing what was done, or undefined when nothing was needed.
|
|
134
|
+
* @example
|
|
135
|
+
* linkSkill("/home/user/app"); // "linked .claude/skills/cloud-ready to .agents/skills/cloud-ready"
|
|
136
|
+
*/
|
|
137
|
+
export function linkSkill(repo) {
|
|
138
|
+
const claudeSkills = join(repo, CLAUDE_SKILLS_DIR);
|
|
139
|
+
const agentsSkills = join(repo, AGENTS_SKILLS_DIR);
|
|
140
|
+
if (existsSync(claudeSkills) && existsSync(agentsSkills) && realpathSync(claudeSkills) === realpathSync(agentsSkills))
|
|
141
|
+
return undefined;
|
|
142
|
+
const link = join(repo, SKILL_LINK);
|
|
143
|
+
if (present(link))
|
|
144
|
+
return undefined;
|
|
145
|
+
mkdirSync(claudeSkills, { recursive: true });
|
|
146
|
+
symlinkSync(SKILL_LINK_TARGET, link);
|
|
147
|
+
return `linked ${SKILL_LINK} to ${SKILL_DIR}`;
|
|
148
|
+
}
|
package/dist/state.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Machine fingerprint and the per-repo state file that records what setup last did.
|
|
3
|
+
*/
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { basename, join } from "node:path";
|
|
7
|
+
import { TOOLS } from "./detect.js";
|
|
8
|
+
/** Directory, inside the tools dir, that holds one state file per repo. */
|
|
9
|
+
const STATE_DIR = "state";
|
|
10
|
+
/** Prefix of every fingerprint. */
|
|
11
|
+
const FINGERPRINT_PREFIX = "sha256-";
|
|
12
|
+
/** Hash algorithm used for fingerprints. */
|
|
13
|
+
const HASH_ALGORITHM = "sha256";
|
|
14
|
+
/** Indentation of the state file. */
|
|
15
|
+
const JSON_INDENT = 2;
|
|
16
|
+
/**
|
|
17
|
+
* Serializes a value as JSON with object keys sorted recursively; arrays keep their order.
|
|
18
|
+
*
|
|
19
|
+
* @param value - Any JSON-compatible value.
|
|
20
|
+
* @returns A canonical JSON string.
|
|
21
|
+
*/
|
|
22
|
+
function stableJson(value) {
|
|
23
|
+
if (Array.isArray(value))
|
|
24
|
+
return `[${value.map(stableJson).join(",")}]`;
|
|
25
|
+
if (typeof value === "object" && value !== null) {
|
|
26
|
+
const entries = Object.entries(value)
|
|
27
|
+
.filter(([, v]) => v !== undefined)
|
|
28
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
29
|
+
.map(([k, v]) => `${JSON.stringify(k)}:${stableJson(v)}`);
|
|
30
|
+
return `{${entries.join(",")}}`;
|
|
31
|
+
}
|
|
32
|
+
return JSON.stringify(value) ?? "null";
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Hashes everything that decides how the machine is built, so a change asks for a rebuild.
|
|
36
|
+
* `session`, `requiredEnv` and `notes` are left out: changing them does not need a rebuild.
|
|
37
|
+
*
|
|
38
|
+
* @param config - Repo config.
|
|
39
|
+
* @param versions - Pinned tool versions.
|
|
40
|
+
* @param images - Docker images to pull.
|
|
41
|
+
* @param packageVersion - Version of this package.
|
|
42
|
+
* @returns `sha256-<hex>`.
|
|
43
|
+
* @example
|
|
44
|
+
* fingerprint(config, { node: "20.1.0" }, ["postgres:16"], "1.0.0"); // "sha256-ab12..."
|
|
45
|
+
*/
|
|
46
|
+
export function fingerprint(config, versions, images, packageVersion) {
|
|
47
|
+
const { version, aptRepos, apt, docker, setup } = config;
|
|
48
|
+
const json = stableJson({ version, aptRepos, apt, docker, setup, versions, images, packageVersion });
|
|
49
|
+
return FINGERPRINT_PREFIX + createHash(HASH_ALGORITHM).update(json).digest("hex");
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Locates the state file of a repo.
|
|
53
|
+
*
|
|
54
|
+
* @param toolsDir - Tools directory.
|
|
55
|
+
* @param repo - Repo root path.
|
|
56
|
+
* @returns `<toolsDir>/state/<basename(repo)>.json`.
|
|
57
|
+
* @example
|
|
58
|
+
* statePath("/opt/agent-cloud-kit", "/home/user/app"); // "/opt/agent-cloud-kit/state/app.json"
|
|
59
|
+
*/
|
|
60
|
+
export function statePath(toolsDir, repo) {
|
|
61
|
+
return join(toolsDir, STATE_DIR, `${basename(repo)}.json`);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Writes the state file atomically (temp file in the same dir, then rename).
|
|
65
|
+
*
|
|
66
|
+
* @param toolsDir - Tools directory.
|
|
67
|
+
* @param repo - Repo root path.
|
|
68
|
+
* @param state - State to record.
|
|
69
|
+
* @example
|
|
70
|
+
* writeState("/opt/agent-cloud-kit", "/home/user/app", state);
|
|
71
|
+
*/
|
|
72
|
+
export function writeState(toolsDir, repo, state) {
|
|
73
|
+
const file = statePath(toolsDir, repo);
|
|
74
|
+
mkdirSync(join(toolsDir, STATE_DIR), { recursive: true });
|
|
75
|
+
const temp = `${file}.${process.pid}.tmp`;
|
|
76
|
+
writeFileSync(temp, `${JSON.stringify(state, null, JSON_INDENT)}\n`);
|
|
77
|
+
renameSync(temp, file);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Tells whether a value is a plain JSON object.
|
|
81
|
+
*
|
|
82
|
+
* @param value - Any parsed JSON value.
|
|
83
|
+
* @returns True for non-null, non-array objects.
|
|
84
|
+
*/
|
|
85
|
+
function isRecord(value) {
|
|
86
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Tells whether a parsed value has the shape of a `StepResult`.
|
|
90
|
+
*
|
|
91
|
+
* @param value - Any parsed JSON value.
|
|
92
|
+
* @returns True for a string `name`, a boolean `ok`, a finite numeric `ms` and an optional string `error`.
|
|
93
|
+
*/
|
|
94
|
+
function isStepResult(value) {
|
|
95
|
+
if (!isRecord(value))
|
|
96
|
+
return false;
|
|
97
|
+
return (typeof value.name === "string" &&
|
|
98
|
+
typeof value.ok === "boolean" &&
|
|
99
|
+
typeof value.ms === "number" &&
|
|
100
|
+
Number.isFinite(value.ms) &&
|
|
101
|
+
(value.error === undefined || typeof value.error === "string"));
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Tells whether a parsed value is a map of known tools to version strings.
|
|
105
|
+
*
|
|
106
|
+
* @param value - Any parsed JSON value.
|
|
107
|
+
* @returns True when every key is a tool and every value a string.
|
|
108
|
+
*/
|
|
109
|
+
function isVersionMap(value) {
|
|
110
|
+
if (!isRecord(value))
|
|
111
|
+
return false;
|
|
112
|
+
return Object.entries(value).every(([k, v]) => TOOLS.includes(k) && typeof v === "string");
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Tells whether a parsed value has the shape of a `MachineState`; a missing `resolved` is accepted.
|
|
116
|
+
*
|
|
117
|
+
* @param value - Any parsed JSON value.
|
|
118
|
+
* @returns True when all required fields are present with the right types.
|
|
119
|
+
*/
|
|
120
|
+
function isMachineState(value) {
|
|
121
|
+
if (!isRecord(value))
|
|
122
|
+
return false;
|
|
123
|
+
return (typeof value.fingerprint === "string" &&
|
|
124
|
+
typeof value.packageVersion === "string" &&
|
|
125
|
+
typeof value.finishedAt === "string" &&
|
|
126
|
+
Array.isArray(value.steps) &&
|
|
127
|
+
value.steps.every(isStepResult) &&
|
|
128
|
+
(value.resolved === undefined || isVersionMap(value.resolved)));
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Reads a repo's state file; never throws because it runs at session start.
|
|
132
|
+
*
|
|
133
|
+
* @param toolsDir - Tools directory.
|
|
134
|
+
* @param repo - Repo root path.
|
|
135
|
+
* @returns The state, or undefined when the file is missing, unreadable, invalid or incomplete.
|
|
136
|
+
* @example
|
|
137
|
+
* readState("/opt/agent-cloud-kit", "/home/user/app"); // undefined before the first setup
|
|
138
|
+
*/
|
|
139
|
+
export function readState(toolsDir, repo) {
|
|
140
|
+
try {
|
|
141
|
+
const parsed = JSON.parse(readFileSync(statePath(toolsDir, repo), "utf8"));
|
|
142
|
+
return isMachineState(parsed) ? { ...parsed, resolved: parsed.resolved ?? {} } : undefined;
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
}
|
package/dist/steps.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Step results: runs one unit of work and reports its outcome without ever throwing.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Runs a step, timing it and converting any failure into a result.
|
|
6
|
+
*
|
|
7
|
+
* @param name - Name reported in the result.
|
|
8
|
+
* @param fn - Work to run.
|
|
9
|
+
* @returns The outcome; this promise never rejects.
|
|
10
|
+
* @example
|
|
11
|
+
* await step("pull images", async () => pull()); // { name: "pull images", ok: true, ms: 1200 }
|
|
12
|
+
*/
|
|
13
|
+
export async function step(name, fn) {
|
|
14
|
+
const start = Date.now();
|
|
15
|
+
try {
|
|
16
|
+
await fn();
|
|
17
|
+
return { name, ok: true, ms: Date.now() - start };
|
|
18
|
+
}
|
|
19
|
+
catch (err) {
|
|
20
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
21
|
+
return { name, ok: false, ms: Date.now() - start, error };
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** Prefix of the name given to a lane whose function rejected. */
|
|
25
|
+
const LANE_NAME_PREFIX = "lane ";
|
|
26
|
+
/**
|
|
27
|
+
* Runs lanes of steps concurrently and joins their results in lane order.
|
|
28
|
+
*
|
|
29
|
+
* @param laneFns - Lane functions, each resolving to the results of its own steps.
|
|
30
|
+
* @returns Results ordered by lane, then by position within the lane; never rejects.
|
|
31
|
+
* @example
|
|
32
|
+
* await lanes(async () => [await step("a", fa)], async () => [await step("b", fb)]);
|
|
33
|
+
*/
|
|
34
|
+
export async function lanes(...laneFns) {
|
|
35
|
+
const groups = await Promise.all(laneFns.map(async (laneFn, index) => {
|
|
36
|
+
const start = Date.now();
|
|
37
|
+
try {
|
|
38
|
+
return await laneFn();
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
42
|
+
return [{ name: `${LANE_NAME_PREFIX}${index}`, ok: false, ms: Date.now() - start, error }];
|
|
43
|
+
}
|
|
44
|
+
}));
|
|
45
|
+
return groups.flat();
|
|
46
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,48 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-cloud-kit",
|
|
3
|
-
"version": "0.0.0
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0-rc.0",
|
|
4
|
+
"description": "Zero-dependency CLI that prepares Claude Code cloud sessions.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/ramon-sg/agent-cloud-kit.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/ramon-sg/agent-cloud-kit#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/ramon-sg/agent-cloud-kit/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"cloud",
|
|
17
|
+
"sandbox",
|
|
18
|
+
"setup",
|
|
19
|
+
"session-start",
|
|
20
|
+
"cli"
|
|
21
|
+
],
|
|
22
|
+
"bin": {
|
|
23
|
+
"agent-cloud-kit": "dist/cli.js"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"schema.json",
|
|
28
|
+
"skills",
|
|
29
|
+
"README.md",
|
|
30
|
+
"LICENSE"
|
|
31
|
+
],
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=20"
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public",
|
|
37
|
+
"provenance": true
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsc",
|
|
41
|
+
"typecheck": "tsc -p tsconfig.test.json",
|
|
42
|
+
"test": "node --test 'test/**/*.test.ts'"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/node": "20.19.43",
|
|
46
|
+
"typescript": "7.0.2"
|
|
47
|
+
}
|
|
48
|
+
}
|
package/schema.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"title": "agent-cloud-kit config",
|
|
4
|
+
"description": "Contents of .claude/cloud.json for agent-cloud-kit.",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"required": ["version"],
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"properties": {
|
|
9
|
+
"$schema": { "type": "string", "description": "Path or URL of this schema." },
|
|
10
|
+
"version": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"pattern": "^\\d+\\.\\d+\\.\\d+(-[0-9A-Za-z.-]+)?(\\+[0-9A-Za-z.-]+)?$",
|
|
13
|
+
"description": "Exact agent-cloud-kit version to run (no ranges)."
|
|
14
|
+
},
|
|
15
|
+
"aptRepos": {
|
|
16
|
+
"type": "array",
|
|
17
|
+
"items": { "type": "string" },
|
|
18
|
+
"default": [],
|
|
19
|
+
"description": "HTTPS URLs of setup scripts that add APT repositories (piped to bash)."
|
|
20
|
+
},
|
|
21
|
+
"apt": {
|
|
22
|
+
"type": "array",
|
|
23
|
+
"items": { "type": "string" },
|
|
24
|
+
"default": [],
|
|
25
|
+
"description": "APT packages to install."
|
|
26
|
+
},
|
|
27
|
+
"docker": {
|
|
28
|
+
"type": "object",
|
|
29
|
+
"additionalProperties": false,
|
|
30
|
+
"default": { "pull": "auto" },
|
|
31
|
+
"properties": {
|
|
32
|
+
"pull": {
|
|
33
|
+
"description": "\"auto\" detects images, false disables pulling, or list images explicitly.",
|
|
34
|
+
"oneOf": [
|
|
35
|
+
{ "const": "auto" },
|
|
36
|
+
{ "const": false },
|
|
37
|
+
{ "type": "array", "items": { "type": "string" } }
|
|
38
|
+
],
|
|
39
|
+
"default": "auto"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"setup": {
|
|
44
|
+
"type": "array",
|
|
45
|
+
"items": { "type": "string" },
|
|
46
|
+
"default": [],
|
|
47
|
+
"description": "Shell commands run once when the environment is set up."
|
|
48
|
+
},
|
|
49
|
+
"session": {
|
|
50
|
+
"type": "array",
|
|
51
|
+
"items": { "type": "string" },
|
|
52
|
+
"default": [],
|
|
53
|
+
"description": "Shell commands run at the start of every session."
|
|
54
|
+
},
|
|
55
|
+
"requiredEnv": {
|
|
56
|
+
"type": "array",
|
|
57
|
+
"items": { "type": "string" },
|
|
58
|
+
"default": [],
|
|
59
|
+
"description": "Environment variables that must be set; a warning is shown when missing."
|
|
60
|
+
},
|
|
61
|
+
"domains": {
|
|
62
|
+
"type": "array",
|
|
63
|
+
"items": { "type": "string", "pattern": "^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)+$" },
|
|
64
|
+
"default": [],
|
|
65
|
+
"description": "Extra hosts the cloud environment must allow; doctor probes them and the manual lists them."
|
|
66
|
+
},
|
|
67
|
+
"notes": {
|
|
68
|
+
"type": "string",
|
|
69
|
+
"description": "What does and does not run in the cloud: free text, or a path, relative to the repo, of a text file inside it whose contents are used instead. The session prints it at start and the manual shows it."
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cloud-ready
|
|
3
|
+
description: Prepara o verifica un repo Node/TypeScript para las sesiones en la nube de Claude Code (claude.ai/code). Usa `configure` en local, con el usuario, para escribir `.claude/cloud.json` y decidir qué corre en la nube; usa `verify` dentro de una sesión en la nube para comprobar la preparación, arreglarla y reportar en el PR.
|
|
4
|
+
argument-hint: "configure [respuestas] | verify"
|
|
5
|
+
metadata:
|
|
6
|
+
version: "1.0.0"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# cloud-ready
|
|
10
|
+
|
|
11
|
+
Deja el repo listo para la nube de Claude Code y lo comprueba en la nube real. No impone una
|
|
12
|
+
forma de testear: se respeta lo que el repo ya hace. Un repo sin tests solo necesita la máquina
|
|
13
|
+
preparada.
|
|
14
|
+
|
|
15
|
+
- **Modo:** el primer argumento. `configure` corre en local con el usuario; `verify`, en la nube.
|
|
16
|
+
Sin argumento: `verify` si `CLAUDE_CODE_REMOTE=true`, si no `configure`.
|
|
17
|
+
- **Antes de empezar:** lee `reference/knowledge.md` (lo que se sabe de la nube) y, si el repo
|
|
18
|
+
tiene tests, `reference/compat.md` (cómo hacerlos compatibles).
|
|
19
|
+
- **El archivo de `notes`:** el que diga `notes` en `.claude/cloud.json`; si no hay, usa
|
|
20
|
+
`.claude/cloud-notes.md` y pon `"notes": ".claude/cloud-notes.md"` en la config. Se muestra al
|
|
21
|
+
empezar cada sesión en la nube y se copia en `docs/agent-cloud.md`: que sea corto.
|
|
22
|
+
- **La CLI:** `agent-cloud-kit <comando>`. Si no está en el `PATH`, usa `npx -y agent-cloud-kit <comando>`.
|
|
23
|
+
|
|
24
|
+
## Archivos de la preparación
|
|
25
|
+
|
|
26
|
+
Son los únicos que `verify` puede cambiar:
|
|
27
|
+
|
|
28
|
+
- `.claude/cloud.json`
|
|
29
|
+
- el archivo de `notes`
|
|
30
|
+
- `docs/agent-cloud.md` (lo regenera `agent-cloud-kit manual`; a mano solo la sección "Hallazgos")
|
|
31
|
+
- `.agents/skills/test-audit/project.md` (solo su sección "Nube", y solo si el archivo existe)
|
|
32
|
+
|
|
33
|
+
## `configure` (local, con el usuario)
|
|
34
|
+
|
|
35
|
+
Si `CLAUDE_CODE_REMOTE=true`, detente: este modo necesita al usuario en una terminal local.
|
|
36
|
+
Las respuestas que traiga el argumento (variables secretas, dominios extra) son el punto de
|
|
37
|
+
partida: no las vuelvas a preguntar.
|
|
38
|
+
|
|
39
|
+
1. **Lee qué usa el repo:** `package.json` (scripts, `packageManager`, `engines`), el lockfile,
|
|
40
|
+
`.tool-versions`, `mise.toml`, `.nvmrc`, archivos de Docker compose, `Dockerfile`,
|
|
41
|
+
`.env.example`, la CI (`.github/workflows/`) y el README. Lee `.claude/cloud.json` si existe.
|
|
42
|
+
2. **Escribe la preparación en `.claude/cloud.json`** (no toques `version` ni `$schema`):
|
|
43
|
+
- `setup`: la instalación de dependencias con el gestor del repo y el lockfile congelado
|
|
44
|
+
(`npm ci`, `pnpm install --frozen-lockfile`, `bun install --frozen-lockfile`…), más lo que
|
|
45
|
+
haga falta una sola vez (p. ej. navegadores de Playwright). Node y los gestores fijados en
|
|
46
|
+
`.tool-versions`, `mise.toml`, `.nvmrc` o `packageManager` los instala el paquete solo: no
|
|
47
|
+
los agregues. Cada paso tiene 180 s y el setup entero debe quedar bajo ~5 min.
|
|
48
|
+
- `apt` y `aptRepos`: solo lo que el repo necesite y la imagen no traiga.
|
|
49
|
+
- `session`: comandos rápidos de cada sesión (2 min como máximo cada uno, sin depender de la red).
|
|
50
|
+
- `domains`: los hosts que el setup o los tests necesitan y que no están en la lista por
|
|
51
|
+
defecto de gestores de paquetes.
|
|
52
|
+
- `requiredEnv`: solo los nombres de las variables secretas que de verdad hacen falta en la nube.
|
|
53
|
+
- `notes`: la ruta del archivo de `notes`.
|
|
54
|
+
3. **Busca los tests del repo:** scripts `test*` y `e2e*` de `package.json`, configuración de
|
|
55
|
+
runners (`vitest.config.*`, `jest.config.*`, `playwright.config.*`, `.mocharc*`),
|
|
56
|
+
Testcontainers en las dependencias, compose de tests y los jobs de la CI que corren tests.
|
|
57
|
+
4. **Sin tests:** díselo al usuario y no hagas nada más con tests. Anota en `notes` que el repo
|
|
58
|
+
no tiene tests y que en la nube solo se prepara la máquina. Sigue en el paso 7.
|
|
59
|
+
5. **Con tests:** agrúpalos (p. ej. unitarios, con base de datos, de navegador) y, para cada
|
|
60
|
+
grupo, di si correría en la nube tal cual. Si no, propón, en este orden
|
|
61
|
+
(`reference/compat.md`):
|
|
62
|
+
1. Adaptar la preparación: `setup`, `session`, `domains`, `requiredEnv`.
|
|
63
|
+
2. Solo si eso no basta, una variante para la nube: un script aparte (p. ej. `cloud:services`)
|
|
64
|
+
o una rama por `CLAUDE_CODE_REMOTE` en un script existente.
|
|
65
|
+
3. Si nada de eso alcanza, déjalo fuera de la nube y explica por qué.
|
|
66
|
+
|
|
67
|
+
Muestra cada propuesta con el cambio exacto (archivo y contenido).
|
|
68
|
+
6. **Pregunta y aplica solo lo aprobado.** Un cambio fuera de la config (scripts, compose,
|
|
69
|
+
configuración de tests) necesita un sí explícito del usuario. Nunca cambies lo que un test
|
|
70
|
+
comprueba ni crees scripts de tests nuevos sin aprobación.
|
|
71
|
+
7. **Escribe el archivo de `notes`:** qué correr en la nube (el comando tal como lo corre el
|
|
72
|
+
repo, lo que hay que levantar antes, cuánto tarda y si va en segundo plano), qué no y por qué.
|
|
73
|
+
Por ejemplo:
|
|
74
|
+
|
|
75
|
+
```markdown
|
|
76
|
+
En la nube: `npm test` (~1 min). Antes de `npm run test:db`, `docker compose up -d db`
|
|
77
|
+
(~3 min, en segundo plano). No corre `npm run test:e2e:prod`: pega a una API externa que el
|
|
78
|
+
entorno no permite.
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
8. **Si existe `.agents/skills/test-audit/project.md`:** escribe o actualiza su sección "Nube"
|
|
82
|
+
con el mismo resumen; no toques el resto del archivo.
|
|
83
|
+
9. **Corre `agent-cloud-kit manual`** y muéstrale al usuario la sección "Configurar el entorno en
|
|
84
|
+
claude.ai" de `docs/agent-cloud.md` (setup script, dominios, variables).
|
|
85
|
+
10. **No hagas commit:** `agent-cloud-kit prepare` lo hace al salir. Si te invocaron fuera de
|
|
86
|
+
`prepare`, pregunta al usuario.
|
|
87
|
+
11. **Termina diciéndole al usuario que salga:** dile al usuario, con estas palabras o parecidas,
|
|
88
|
+
"configure terminó: sal de claude con `/exit` y `prepare` sigue (commit, push y PR)". Si te
|
|
89
|
+
invocaron fuera de `prepare`, omite este paso.
|
|
90
|
+
|
|
91
|
+
## `verify` (en la nube)
|
|
92
|
+
|
|
93
|
+
Solo con `CLAUDE_CODE_REMOTE=true`; si no, detente y dilo. El prompt trae la rama de `prepare`
|
|
94
|
+
(`chore/cloud-ready-…`) y puede traer el número del PR. La sesión trabaja en su propia rama
|
|
95
|
+
`claude/…`: no la cambies; al final empujas a la rama de `prepare`.
|
|
96
|
+
|
|
97
|
+
1. **Contexto:** lee `.claude/cloud.json`, el archivo de `notes` y `docs/agent-cloud.md`. Anota
|
|
98
|
+
las líneas `agent-cloud-kit:` de falla que mostró el inicio de la sesión ("did not run",
|
|
99
|
+
pasos fallidos, "another configuration", variables faltantes).
|
|
100
|
+
2. **Corre `agent-cloud-kit doctor`** y guarda la salida. Si `agent-cloud-kit` no está en el
|
|
101
|
+
`PATH`, el setup no corrió: usa `npx -y agent-cloud-kit doctor` y repórtalo.
|
|
102
|
+
3. **Comprueba la preparación:** dependencias instaladas (p. ej. `node_modules` existe), las
|
|
103
|
+
versiones fijadas (`node -v` y los demás gestores contra `.tool-versions`, `mise.toml`,
|
|
104
|
+
`.nvmrc` o `packageManager`), las variables de `requiredEnv` presentes (solo nombres; nunca
|
|
105
|
+
imprimas valores) y Docker, si el repo lo usa.
|
|
106
|
+
4. **Tests, solo si las `notes` dicen qué correr en la nube.** Córrelos tal como el repo los
|
|
107
|
+
corre, con lo que haya que levantar antes. Las corridas largas van en segundo plano, con la
|
|
108
|
+
salida a un log que sigues hasta que terminen. Si las `notes` no dicen qué correr, no corras
|
|
109
|
+
tests y dilo en el reporte.
|
|
110
|
+
5. **Si algo falla,** diagnostica con `reference/knowledge.md` y `reference/compat.md`, y
|
|
111
|
+
clasifica:
|
|
112
|
+
- **Se arregla en la preparación:** cambia solo los archivos de la preparación y vuelve a
|
|
113
|
+
comprobar. Un cambio de `setup` no se re-ejecuta en esta sesión (el setup ya corrió): corre
|
|
114
|
+
el comando a mano para probarlo y anota que la máquina guardada se rehará en una sesión nueva.
|
|
115
|
+
- **Solo se cambia en claude.ai** (dominios de la red, variables, entorno por defecto con
|
|
116
|
+
`/remote-env`, rehacer la máquina editando el setup script, el Claude GitHub App): no lo
|
|
117
|
+
intentes; deja la instrucción exacta para el reporte y deja de intentar arreglos.
|
|
118
|
+
- **Requiere cambiar código, tests o scripts:** no lo cambies; deja el cambio propuesto
|
|
119
|
+
(archivo y diff) para el comentario del PR.
|
|
120
|
+
|
|
121
|
+
Hasta **3 intentos** de arreglo en total. Tras el tercero, detente y reporta lo que queda.
|
|
122
|
+
6. **Hallazgos:** ver "Hallazgos", abajo.
|
|
123
|
+
7. **Corre `agent-cloud-kit manual`.**
|
|
124
|
+
8. **Antes del commit,** revisa `git diff --name-only HEAD` y `git status --porcelain`. Todo lo
|
|
125
|
+
que no sea un archivo de la preparación se revierte con `git checkout -- <archivo>` (si está
|
|
126
|
+
versionado) o se borra (si es nuevo y lo creaste tú). Un cambio necesario al código, tests o
|
|
127
|
+
scripts queda como propuesta en el comentario del PR.
|
|
128
|
+
9. **Commit y push:** agrega solo los archivos de la preparación que cambiaron, por nombre
|
|
129
|
+
(`git add -- .claude/cloud.json <archivo de notes> docs/agent-cloud.md`, más
|
|
130
|
+
`.agents/skills/test-audit/project.md` si lo tocaste); si hay algo en stage,
|
|
131
|
+
`git commit -m "chore(cloud): verify cloud-ready"`. Luego
|
|
132
|
+
`git push origin HEAD:<rama de prepare>`. Si el push se rechaza, `git fetch origin <rama de
|
|
133
|
+
prepare>`, `git rebase FETCH_HEAD` (el clon es parcial: `origin/<rama>` puede no existir) y
|
|
134
|
+
reintenta una vez.
|
|
135
|
+
10. **Comentario en el PR,** por REST con `gh api` (`gh pr …` usa GraphQL y está bloqueado;
|
|
136
|
+
`gh auth status` dice que el token es inválido, pero `gh api` funciona):
|
|
137
|
+
- `owner` y `repo`: de `git remote get-url origin` (las dos últimas partes de la ruta, sin `.git`).
|
|
138
|
+
- Número del PR: el del prompt; si no viene,
|
|
139
|
+
`gh api "repos/{owner}/{repo}/pulls?head={owner}:{rama de prepare}&state=open" --jq '.[0].number'`.
|
|
140
|
+
- Comentario: escribe el cuerpo en un archivo temporal y
|
|
141
|
+
`gh api repos/{owner}/{repo}/issues/{n}/comments -f body="$(cat <archivo>)"`.
|
|
142
|
+
- Si no hay PR, el resultado queda solo en `docs/agent-cloud.md` y en tu resumen final.
|
|
143
|
+
- No saques el PR de draft: lo decide el usuario.
|
|
144
|
+
11. **Resumen final** en la sesión, con lo mismo que el comentario.
|
|
145
|
+
|
|
146
|
+
### El comentario del PR
|
|
147
|
+
|
|
148
|
+
```markdown
|
|
149
|
+
## cloud-ready verify
|
|
150
|
+
|
|
151
|
+
**Comprobado:** `agent-cloud-kit doctor` (salida 0 / qué falló), dependencias, versiones, variables.
|
|
152
|
+
**Corrió:** cada comando, su resultado y cuánto tardó.
|
|
153
|
+
**No corrió:** qué y por qué.
|
|
154
|
+
**Cambiar en claude.ai:** la instrucción exacta (dónde y qué), o "nada".
|
|
155
|
+
**Cambios propuestos al código o los tests:** archivo y diff, o "ninguno".
|
|
156
|
+
**Arreglado en la preparación:** archivos y por qué. Intentos: n de 3.
|
|
157
|
+
**Hallazgos:** los nuevos, o "ninguno".
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Hallazgos
|
|
161
|
+
|
|
162
|
+
En ambos modos: todo hecho nuevo sobre la nube (algo que no está en `reference/knowledge.md`,
|
|
163
|
+
o que lo contradice) va a la sección `## Hallazgos` de `docs/agent-cloud.md`, fuera de las
|
|
164
|
+
marcas de `agent-cloud-kit` (`manual` la conserva). Una línea por hallazgo, marcada
|
|
165
|
+
"proponer al paquete":
|
|
166
|
+
|
|
167
|
+
```markdown
|
|
168
|
+
- (proponer al paquete) `docker compose` necesita `--wait` para que la base acepte conexiones.
|
|
169
|
+
Visto en `verify`, 2026-11-02, con `docker compose up -d db`.
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
No edites `reference/knowledge.md` del repo: se reemplaza al actualizar el paquete.
|
|
173
|
+
|
|
174
|
+
## Límites
|
|
175
|
+
|
|
176
|
+
- `verify` nunca cambia código, tests ni scripts; `configure`, solo con aprobación del usuario.
|
|
177
|
+
- Nunca imprimas ni escribas el valor de una variable secreta; solo su nombre.
|
|
178
|
+
- Nada de `gh pr` ni `gh issue` en la nube: usa `gh api` (REST).
|
|
179
|
+
- No configures claude.ai: da la instrucción exacta y que la haga el usuario.
|