@heart-of-gold/toolkit 0.1.49 → 0.1.51
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/package.json +3 -2
- package/scripts/release.mjs +261 -0
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heart-of-gold/toolkit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.51",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Cross-platform installer for Heart of Gold skills
|
|
5
|
+
"description": "Cross-platform installer for Heart of Gold skills — works with Codex, OpenCode, Pi, Claude Code, and more",
|
|
6
6
|
"bin": {
|
|
7
7
|
"heart-of-gold": "src/index.ts"
|
|
8
8
|
},
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"check:compat": "python3 scripts/check-harness-compatibility.py",
|
|
13
13
|
"test:pi-guided": "node --test tests/pi-guided-workflows.test.mjs",
|
|
14
14
|
"test:visualize": "node --test tests/visualize-smart-render.test.mjs",
|
|
15
|
+
"release": "node scripts/release.mjs",
|
|
15
16
|
"prepublishOnly": "npm run check:publish-safety && npm run check:compat"
|
|
16
17
|
},
|
|
17
18
|
"dependencies": {
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// One-shot release tool. Bumps a plugin (plugin.json + marketplace.json) and
|
|
3
|
+
// the root package.json in lockstep, runs the prepublish checks, commits, and
|
|
4
|
+
// — only with --ship — pushes and publishes to NPM.
|
|
5
|
+
//
|
|
6
|
+
// Usage:
|
|
7
|
+
// node scripts/release.mjs <plugin|root>:<patch|minor|major> [flags]
|
|
8
|
+
//
|
|
9
|
+
// Examples:
|
|
10
|
+
// node scripts/release.mjs marvin:patch # bump + commit, no push/publish
|
|
11
|
+
// node scripts/release.mjs marvin:minor --ship -m "new skill" # bump + commit + push + npm publish
|
|
12
|
+
// node scripts/release.mjs root:patch # root only (CLI fix; no plugin)
|
|
13
|
+
// node scripts/release.mjs --dry-run marvin:patch # preview, mutate nothing
|
|
14
|
+
//
|
|
15
|
+
// Flags:
|
|
16
|
+
// --ship after commit, run `git push` and `npm publish --access public`
|
|
17
|
+
// --dry-run print the plan, change no files
|
|
18
|
+
// -m, --message TEXT description after the standard "Release X.Y.Z — " prefix
|
|
19
|
+
// -h, --help this text
|
|
20
|
+
|
|
21
|
+
import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
22
|
+
import { execSync } from "node:child_process";
|
|
23
|
+
import { dirname, join } from "node:path";
|
|
24
|
+
import { createInterface } from "node:readline";
|
|
25
|
+
import { fileURLToPath } from "node:url";
|
|
26
|
+
|
|
27
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
28
|
+
const ROOT_PKG = join(ROOT, "package.json");
|
|
29
|
+
const MARKETPLACE = join(ROOT, ".claude-plugin/marketplace.json");
|
|
30
|
+
const PLUGINS = ["guide", "deep-thought", "marvin", "babel-fish", "quellis"];
|
|
31
|
+
|
|
32
|
+
const readJson = (p) => JSON.parse(readFileSync(p, "utf8"));
|
|
33
|
+
const writeJson = (p, d) => writeFileSync(p, JSON.stringify(d, null, 2) + "\n");
|
|
34
|
+
const sh = (cmd, opts = {}) => execSync(cmd, { cwd: ROOT, stdio: "inherit", ...opts });
|
|
35
|
+
const shRead = (cmd) => execSync(cmd, { cwd: ROOT, encoding: "utf8" });
|
|
36
|
+
|
|
37
|
+
function bump(version, kind) {
|
|
38
|
+
const [maj, min, pat] = version.split(".").map(Number);
|
|
39
|
+
if (kind === "patch") return `${maj}.${min}.${pat + 1}`;
|
|
40
|
+
if (kind === "minor") return `${maj}.${min + 1}.0`;
|
|
41
|
+
if (kind === "major") return `${maj + 1}.0.0`;
|
|
42
|
+
throw new Error(`Unknown bump kind: ${kind} (use patch, minor, or major)`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function help() {
|
|
46
|
+
console.log(`One-shot release tool. Bumps a plugin (plugin.json + marketplace.json) and
|
|
47
|
+
the root package.json in lockstep, runs prepublish checks, commits, and —
|
|
48
|
+
only with --ship — pushes and publishes to NPM.
|
|
49
|
+
|
|
50
|
+
Usage:
|
|
51
|
+
node scripts/release.mjs <plugin|root>:<patch|minor|major> [flags]
|
|
52
|
+
|
|
53
|
+
Examples:
|
|
54
|
+
node scripts/release.mjs marvin:patch # bump + commit, no push/publish
|
|
55
|
+
node scripts/release.mjs marvin:minor --ship -m "new skill" # bump + commit + push + npm publish
|
|
56
|
+
node scripts/release.mjs root:patch # root only (CLI fix; no plugin)
|
|
57
|
+
node scripts/release.mjs --dry-run marvin:patch # preview, mutate nothing
|
|
58
|
+
|
|
59
|
+
Flags:
|
|
60
|
+
--ship after commit, run \`git push\` and \`npm publish --access public\`
|
|
61
|
+
--dry-run print the plan, change no files
|
|
62
|
+
--add-pi-skills skip the prompt and add every detected new skill to pi.skills
|
|
63
|
+
--no-pi-skills skip the pi.skills detection entirely
|
|
64
|
+
-m, --message TEXT description after the standard "Release X.Y.Z — " prefix
|
|
65
|
+
-h, --help this text
|
|
66
|
+
|
|
67
|
+
Plugins: ${PLUGINS.join(" | ")} | root`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function prompt(question) {
|
|
71
|
+
return new Promise((resolve) => {
|
|
72
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
73
|
+
rl.question(question, (answer) => {
|
|
74
|
+
rl.close();
|
|
75
|
+
resolve(answer.trim().toLowerCase());
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function detectMissingPiSkills(rootPkg) {
|
|
81
|
+
const exported = new Set(rootPkg.pi?.skills ?? []);
|
|
82
|
+
const found = [];
|
|
83
|
+
for (const plugin of PLUGINS) {
|
|
84
|
+
const skillsDir = join(ROOT, "plugins", plugin, "skills");
|
|
85
|
+
if (!existsSync(skillsDir)) continue;
|
|
86
|
+
for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
|
|
87
|
+
if (!entry.isDirectory()) continue;
|
|
88
|
+
if (!existsSync(join(skillsDir, entry.name, "SKILL.md"))) continue;
|
|
89
|
+
found.push(`./plugins/${plugin}/skills/${entry.name}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return found.filter((path) => !exported.has(path));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function addPiSkills(rootPkg, missing) {
|
|
96
|
+
// Insert each path at the end of its plugin's contiguous block — preserves
|
|
97
|
+
// existing per-plugin ordering (which isn't always alphabetical) and just
|
|
98
|
+
// appends new entries within each plugin's section.
|
|
99
|
+
const skills = [...(rootPkg.pi?.skills ?? [])];
|
|
100
|
+
for (const path of missing) {
|
|
101
|
+
const pluginPrefix = path.split("/").slice(0, 3).join("/") + "/";
|
|
102
|
+
let insertAt = skills.length;
|
|
103
|
+
for (let i = skills.length - 1; i >= 0; i--) {
|
|
104
|
+
if (skills[i].startsWith(pluginPrefix)) {
|
|
105
|
+
insertAt = i + 1;
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
skills.splice(insertAt, 0, path);
|
|
110
|
+
}
|
|
111
|
+
rootPkg.pi = rootPkg.pi ?? {};
|
|
112
|
+
rootPkg.pi.skills = skills;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function parse(argv) {
|
|
116
|
+
const out = {
|
|
117
|
+
target: null,
|
|
118
|
+
kind: null,
|
|
119
|
+
dryRun: false,
|
|
120
|
+
ship: false,
|
|
121
|
+
message: "",
|
|
122
|
+
piSkills: "prompt",
|
|
123
|
+
};
|
|
124
|
+
for (let i = 0; i < argv.length; i++) {
|
|
125
|
+
const a = argv[i];
|
|
126
|
+
if (a === "--help" || a === "-h") { help(); process.exit(0); }
|
|
127
|
+
else if (a === "--dry-run") out.dryRun = true;
|
|
128
|
+
else if (a === "--ship") out.ship = true;
|
|
129
|
+
else if (a === "--add-pi-skills") out.piSkills = "auto";
|
|
130
|
+
else if (a === "--no-pi-skills") out.piSkills = "skip";
|
|
131
|
+
else if (a === "--message" || a === "-m") out.message = argv[++i] ?? "";
|
|
132
|
+
else if (a.includes(":")) [out.target, out.kind] = a.split(":");
|
|
133
|
+
else throw new Error(`Unknown arg: ${a}`);
|
|
134
|
+
}
|
|
135
|
+
if (!out.target || !out.kind) { help(); process.exit(1); }
|
|
136
|
+
if (out.target !== "root" && !PLUGINS.includes(out.target)) {
|
|
137
|
+
throw new Error(`Unknown plugin: ${out.target}. Known: ${PLUGINS.join(", ")}, root`);
|
|
138
|
+
}
|
|
139
|
+
if (!["patch", "minor", "major"].includes(out.kind)) {
|
|
140
|
+
throw new Error(`Unknown bump kind: ${out.kind}. Use patch | minor | major`);
|
|
141
|
+
}
|
|
142
|
+
if (out.target === "root" && out.kind !== "patch") {
|
|
143
|
+
throw new Error("Root bumps are reserved for patch releases (plugin changes drive minor/major).");
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function ensureNoDrift(target, marketplace) {
|
|
149
|
+
if (target === "root") return;
|
|
150
|
+
const pluginPath = join(ROOT, `plugins/${target}/.claude-plugin/plugin.json`);
|
|
151
|
+
const pluginVersion = readJson(pluginPath).version;
|
|
152
|
+
const marketplaceVersion = marketplace.plugins.find((p) => p.name === target).version;
|
|
153
|
+
if (pluginVersion !== marketplaceVersion) {
|
|
154
|
+
throw new Error(
|
|
155
|
+
`Version drift on ${target}: plugin.json=${pluginVersion}, marketplace.json=${marketplaceVersion}. ` +
|
|
156
|
+
`Reconcile both files before releasing.`,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function ensureGitClean(allowedRelativePaths) {
|
|
162
|
+
const status = shRead("git status --porcelain").split("\n").filter(Boolean);
|
|
163
|
+
const offending = status.filter((line) => {
|
|
164
|
+
const file = line.slice(3);
|
|
165
|
+
return !allowedRelativePaths.some((a) => file === a);
|
|
166
|
+
});
|
|
167
|
+
if (offending.length) {
|
|
168
|
+
throw new Error(
|
|
169
|
+
`Working tree has changes outside the release scope. Commit or stash them first:\n${offending.join("\n")}`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function main() {
|
|
175
|
+
const { target, kind, dryRun, ship, message, piSkills } = parse(process.argv.slice(2));
|
|
176
|
+
|
|
177
|
+
const rootPkg = readJson(ROOT_PKG);
|
|
178
|
+
const marketplace = readJson(MARKETPLACE);
|
|
179
|
+
ensureNoDrift(target, marketplace);
|
|
180
|
+
|
|
181
|
+
const oldRoot = rootPkg.version;
|
|
182
|
+
const newRoot = bump(oldRoot, "patch");
|
|
183
|
+
let pluginPlan = null;
|
|
184
|
+
if (target !== "root") {
|
|
185
|
+
const pluginPath = join(ROOT, `plugins/${target}/.claude-plugin/plugin.json`);
|
|
186
|
+
const pluginJson = readJson(pluginPath);
|
|
187
|
+
const oldPlugin = pluginJson.version;
|
|
188
|
+
const newPlugin = bump(oldPlugin, kind);
|
|
189
|
+
pluginPlan = { name: target, pluginPath, pluginJson, oldPlugin, newPlugin };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const missingPi = piSkills === "skip" ? [] : detectMissingPiSkills(rootPkg);
|
|
193
|
+
|
|
194
|
+
console.log("\nRelease plan");
|
|
195
|
+
console.log("------------");
|
|
196
|
+
if (pluginPlan) console.log(` plugin ${pluginPlan.name}: ${pluginPlan.oldPlugin} → ${pluginPlan.newPlugin}`);
|
|
197
|
+
console.log(` root @heart-of-gold/toolkit: ${oldRoot} → ${newRoot}`);
|
|
198
|
+
if (missingPi.length > 0) {
|
|
199
|
+
console.log(` pi.skills: ${missingPi.length} new skill${missingPi.length === 1 ? "" : "s"} detected`);
|
|
200
|
+
for (const p of missingPi) console.log(` + ${p}`);
|
|
201
|
+
}
|
|
202
|
+
console.log(` prepublish checks: will run`);
|
|
203
|
+
console.log(` commit: yes`);
|
|
204
|
+
console.log(` push: ${ship ? "yes" : "no"}`);
|
|
205
|
+
console.log(` npm publish: ${ship ? "yes" : "no"}`);
|
|
206
|
+
if (dryRun) {
|
|
207
|
+
console.log("\n[dry-run] Nothing changed.");
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
let addPi = piSkills === "auto" && missingPi.length > 0;
|
|
212
|
+
if (missingPi.length > 0 && piSkills === "prompt") {
|
|
213
|
+
const answer = await prompt(`\nAdd ${missingPi.length} new skill${missingPi.length === 1 ? "" : "s"} to pi.skills? [Y/n] `);
|
|
214
|
+
addPi = answer === "" || answer === "y" || answer === "yes";
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const allowed = [
|
|
218
|
+
"package.json",
|
|
219
|
+
".claude-plugin/marketplace.json",
|
|
220
|
+
pluginPlan ? `plugins/${pluginPlan.name}/.claude-plugin/plugin.json` : null,
|
|
221
|
+
].filter(Boolean);
|
|
222
|
+
ensureGitClean(allowed);
|
|
223
|
+
|
|
224
|
+
if (pluginPlan) {
|
|
225
|
+
pluginPlan.pluginJson.version = pluginPlan.newPlugin;
|
|
226
|
+
writeJson(pluginPlan.pluginPath, pluginPlan.pluginJson);
|
|
227
|
+
const idx = marketplace.plugins.findIndex((p) => p.name === pluginPlan.name);
|
|
228
|
+
marketplace.plugins[idx].version = pluginPlan.newPlugin;
|
|
229
|
+
writeJson(MARKETPLACE, marketplace);
|
|
230
|
+
}
|
|
231
|
+
rootPkg.version = newRoot;
|
|
232
|
+
if (addPi) addPiSkills(rootPkg, missingPi);
|
|
233
|
+
writeJson(ROOT_PKG, rootPkg);
|
|
234
|
+
|
|
235
|
+
console.log("\nRunning prepublish checks…");
|
|
236
|
+
sh("npm run check:publish-safety && npm run check:compat");
|
|
237
|
+
|
|
238
|
+
const subject = pluginPlan
|
|
239
|
+
? `Release ${newRoot} — bump ${pluginPlan.name} to ${pluginPlan.newPlugin}${message ? `: ${message}` : ""}`
|
|
240
|
+
: `Release ${newRoot} — root patch${message ? `: ${message}` : ""}`;
|
|
241
|
+
sh(`git add ${allowed.map((p) => `"${p}"`).join(" ")}`);
|
|
242
|
+
sh(`git commit -m ${JSON.stringify(subject)}`);
|
|
243
|
+
console.log(`\nCommit created: ${subject}`);
|
|
244
|
+
|
|
245
|
+
if (!ship) {
|
|
246
|
+
console.log("\nNot shipping. To finish:");
|
|
247
|
+
console.log(" git push && npm publish --access public");
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
console.log("\nPushing to origin…");
|
|
252
|
+
sh("git push");
|
|
253
|
+
console.log("\nPublishing to NPM…");
|
|
254
|
+
sh("npm publish --access public");
|
|
255
|
+
console.log(`\nReleased @heart-of-gold/toolkit@${newRoot}.`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
main().catch((err) => {
|
|
259
|
+
console.error(`\nrelease: ${err.message}\n`);
|
|
260
|
+
process.exit(1);
|
|
261
|
+
});
|