Files
getcargohq__cargo-skills/.github/scripts/sync-cookbooks.ts
Auree Aubert d91ab11e45 Point the CDK skill at cdk add, and stop documenting a template flag (#121)
The cookbook guidance said there was no copy tool and the agent had to place
the code itself. `cdk add cookbook/<slug>` is that tool now, so the skill says
what it does, where it writes, and — the part an agent gets wrong — that the
cookbook's own procedure has to be picked up at its Adapt section, because its
opening steps assume you found the folder on GitHub and still have to place it.

The command reference documented `--template <blank|full>` and
`--list-templates` for `cdk init`. Neither flag exists: the scaffold never
varies and a worked example comes from a cookbook. It also had no row for `add`
or `cookbook` at all, which is the reference an agent reads to decide what to
run. Fixed, along with the two recipes and the example that invoked
`--template full`.

Refreshes the cookbook snapshot, which had gone stale at two cookbooks while
the catalog grew to three — `crm-enrichment` was missing from the menu.

Regenerates llms.txt and the skill's metadata snapshot, both CI-gated.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 22:12:45 -07:00

173 lines
7.8 KiB
TypeScript

/**
* Keeps cargo-cdk aware of the cookbooks in getcargohq/gtm-skills: the skills
* that carry a worked cookbook, the same jobs as the one-off skills there
* as a deployed pipeline an agent adapts into a project.
*
* WHY THIS EXISTS
*
* `cargo-cdk/SKILL.md` used to tell the agent to go read a menu on GitHub,
* mid-task, which an agent may simply not do. When it does not, it authors from
* scratch an outcome that was already written. The menu has to be a local file.
*
* gtm-skills publishes ONE machine-readable catalog.json, generated and gated
* in that repo, so this script parses no markdown: it fetches one URL (or reads
* the sibling checkout), snapshots it, and renders the reference. Same shape as
* sync-trigger-slugs.ts: a committed snapshot, prose generated from it, and
* `--check` in CI so the two cannot drift. CI never reaches the network.
*
* node .github/scripts/sync-cookbooks.ts # regenerate the reference
* node .github/scripts/sync-cookbooks.ts --check # CI: fail if stale
* node .github/scripts/sync-cookbooks.ts --refresh # re-pull from ../gtm-skills
* (or GTM_SKILLS_ROOT, a dir or the
* catalog file), else GitHub
*
* Requires Node >= 22.18 (run as .ts via native type-stripping).
*/
import { readFileSync, writeFileSync, existsSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
const snapshotPath = join(repoRoot, ".github/data/cookbooks.json");
const referencePath = join(repoRoot, "cargo-cdk/references/cookbooks.md");
const REPO = "getcargohq/gtm-skills";
const CATALOG_URL = `https://raw.githubusercontent.com/${REPO}/main/catalog.json`;
interface Variation { id: string; when: string; trade: string; }
interface Cookbook {
slug: string;
kind: "cookbook";
outcome: string;
state: string;
chain: number | null;
requires: string[];
hasSkill: boolean;
variations: Variation[];
}
async function refresh(): Promise<Cookbook[]> {
// GTM_SKILLS_ROOT may name the sibling checkout (a directory) or the
// catalog file itself; either resolves to the file.
const configured = resolve(repoRoot, process.env.GTM_SKILLS_ROOT ?? "../gtm-skills");
const local = configured.endsWith(".json") ? configured : join(configured, "catalog.json");
let raw: string;
if (existsSync(local)) {
console.log(`reading ${local}`);
raw = readFileSync(local, "utf8");
} else {
console.log(`fetching ${CATALOG_URL}`);
const res = await fetch(CATALOG_URL);
if (!res.ok) throw new Error(`${CATALOG_URL}: ${res.status}`);
raw = await res.text();
}
const catalog = JSON.parse(raw) as {
skills: Array<{
name: string; kind: string; job: string; state?: string; chain?: number | null;
canChange?: Array<{ id: string; when: string; cost: string }>;
}>;
pending?: Array<{ name: string; job: string; state?: string }>;
};
const out: Cookbook[] = [];
for (const s of catalog.skills) {
if (s.kind !== "cookbook") continue;
out.push({
slug: s.name, kind: "cookbook", outcome: s.job, state: s.state ?? "to-be-approved",
chain: s.chain ?? null, requires: [], hasSkill: true,
variations: (s.canChange ?? []).map((v) => ({ id: v.id, when: v.when, trade: v.cost })),
});
}
for (const p of catalog.pending ?? []) {
out.push({ slug: p.name, kind: "cookbook", outcome: p.job, state: p.state ?? "to-be-approved", chain: null, requires: [], hasSkill: false, variations: [] });
}
return out.sort((a, b) => a.slug.localeCompare(b.slug));
}
function render(examples: Cookbook[]): string {
const withSkill = examples.filter((b) => b.hasSkill);
const L: string[] = [];
L.push("<!-- Generated by .github/scripts/sync-cookbooks.ts from gtm-skills' catalog.json. Do not edit by hand. -->");
L.push("");
L.push("# Cookbooks: worked CDK examples, before you author one from scratch");
L.push("");
L.push(`Skills in [\`${REPO}\`](https://github.com/${REPO}) that are cookbooks: worked CDK resources, the same`);
L.push("jobs as the one-off skills there, as a deployed pipeline that keeps producing the result.");
L.push("");
L.push("**Every folder is self-contained** (its own models, connectors and folders; no shared");
L.push("foundation, no requires graph). `cdk add` copies one into the project; the agent then");
L.push("reconciles it with what is already declared (an existing accounts model, an existing CRM");
L.push("connector), adapts it in place, plans, deploys on a yes, and walks its `Done when`. The");
L.push("code is a worked example, not a template to fill in — and not something to regenerate");
L.push("from the skill's prose.");
L.push("");
L.push("```sh");
L.push("cargo-ai cdk add cookbook/<slug> # inside a CDK project: this is the copy step");
L.push("cargo-ai cdk init <dir> --cookbook <slug> # no project yet: scaffold and install together");
L.push(`npx skills add ${REPO}/<slug> # the procedure on its own, without the CDK resources`);
L.push("```");
L.push("");
L.push("After either `cdk` command the files are in `infra/<slug>/` and `.claude/skills/<slug>/`.");
L.push("Start the skill at its Adapt section: its earlier steps assume you found the folder in");
L.push("gtm-skills and still have to place it.");
L.push("");
L.push("## With a skill");
L.push("");
L.push("| Skill | Deploys | State |");
L.push("| --- | --- | --- |");
for (const b of withSkill) L.push(`| \`${b.slug}\` | ${b.outcome} | ${b.state} |`);
const withVariations = withSkill.filter((b) => b.variations.length);
if (withVariations.length) {
L.push("");
L.push("## When one does not fit as written");
L.push("");
L.push("Declared adaptations, not forks. Reach for one before concluding a skill is the wrong start.");
L.push("");
for (const b of withVariations) {
L.push(`**\`${b.slug}\`**`);
L.push("");
for (const v of b.variations) L.push(`- \`${v.id}\` — ${v.when}. Costs: ${v.trade}.`);
L.push("");
}
}
L.push("## Routing");
L.push("");
L.push("**One-off versus standing is the whole test.** A user who wants a list today wants a one-off");
L.push("skill (or `cargo-gtm`, when this pack is installed); a user who wants a pipeline that keeps");
L.push("producing it wants one of these. The same words describe both, so listen for whether the");
L.push("result is meant to keep arriving.");
L.push("");
L.push("**Never `cargo-ai cdk init --force` into a directory that is not empty.** It replaces the");
L.push("project's `package.json` and reverts adapted code while `cargo.state.json` survives, so the");
L.push("next plan diffs a live workspace against code nobody wrote. Run `cdk add cookbook/<slug>`");
L.push("in the project that is already there; it skips every file it would otherwise overwrite.");
L.push("");
return L.join("\n");
}
const examples: Cookbook[] = process.argv.includes("--refresh")
? await refresh()
: JSON.parse(readFileSync(snapshotPath, "utf8"));
if (process.argv.includes("--refresh")) {
writeFileSync(snapshotPath, JSON.stringify(examples, null, 2) + "\n");
console.log(`snapshot: ${examples.length} cookbooks -> ${snapshotPath}`);
}
const rendered = render(examples);
if (process.argv.includes("--check")) {
const current = existsSync(referencePath)
? readFileSync(referencePath, "utf8")
: "";
if (current !== rendered) {
console.error(
"cargo-cdk/references/cookbooks.md is stale.\n Run: node .github/scripts/sync-cookbooks.ts",
);
process.exit(1);
}
console.log(
`ok: cookbook menu matches the snapshot (${examples.length} cookbooks)`,
);
} else {
writeFileSync(referencePath, rendered);
console.log(`wrote ${referencePath} (${examples.length} cookbooks)`);
}