@heart-of-gold/toolkit 0.1.49 → 0.1.50

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.
Files changed (2) hide show
  1. package/package.json +3 -2
  2. package/scripts/release.mjs +193 -0
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@heart-of-gold/toolkit",
3
- "version": "0.1.49",
3
+ "version": "0.1.50",
4
4
  "type": "module",
5
- "description": "Cross-platform installer for Heart of Gold skills \u2014 works with Codex, OpenCode, Pi, Claude Code, and more",
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,193 @@
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 { readFileSync, writeFileSync } from "node:fs";
22
+ import { execSync } from "node:child_process";
23
+ import { dirname, join } from "node:path";
24
+ import { fileURLToPath } from "node:url";
25
+
26
+ const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
27
+ const ROOT_PKG = join(ROOT, "package.json");
28
+ const MARKETPLACE = join(ROOT, ".claude-plugin/marketplace.json");
29
+ const PLUGINS = ["guide", "deep-thought", "marvin", "babel-fish", "quellis"];
30
+
31
+ const readJson = (p) => JSON.parse(readFileSync(p, "utf8"));
32
+ const writeJson = (p, d) => writeFileSync(p, JSON.stringify(d, null, 2) + "\n");
33
+ const sh = (cmd, opts = {}) => execSync(cmd, { cwd: ROOT, stdio: "inherit", ...opts });
34
+ const shRead = (cmd) => execSync(cmd, { cwd: ROOT, encoding: "utf8" });
35
+
36
+ function bump(version, kind) {
37
+ const [maj, min, pat] = version.split(".").map(Number);
38
+ if (kind === "patch") return `${maj}.${min}.${pat + 1}`;
39
+ if (kind === "minor") return `${maj}.${min + 1}.0`;
40
+ if (kind === "major") return `${maj + 1}.0.0`;
41
+ throw new Error(`Unknown bump kind: ${kind} (use patch, minor, or major)`);
42
+ }
43
+
44
+ function help() {
45
+ console.log(`One-shot release tool. Bumps a plugin (plugin.json + marketplace.json) and
46
+ the root package.json in lockstep, runs prepublish checks, commits, and —
47
+ only with --ship — pushes and publishes to NPM.
48
+
49
+ Usage:
50
+ node scripts/release.mjs <plugin|root>:<patch|minor|major> [flags]
51
+
52
+ Examples:
53
+ node scripts/release.mjs marvin:patch # bump + commit, no push/publish
54
+ node scripts/release.mjs marvin:minor --ship -m "new skill" # bump + commit + push + npm publish
55
+ node scripts/release.mjs root:patch # root only (CLI fix; no plugin)
56
+ node scripts/release.mjs --dry-run marvin:patch # preview, mutate nothing
57
+
58
+ Flags:
59
+ --ship after commit, run \`git push\` and \`npm publish --access public\`
60
+ --dry-run print the plan, change no files
61
+ -m, --message TEXT description after the standard "Release X.Y.Z — " prefix
62
+ -h, --help this text
63
+
64
+ Plugins: ${PLUGINS.join(" | ")} | root`);
65
+ }
66
+
67
+ function parse(argv) {
68
+ const out = { target: null, kind: null, dryRun: false, ship: false, message: "" };
69
+ for (let i = 0; i < argv.length; i++) {
70
+ const a = argv[i];
71
+ if (a === "--help" || a === "-h") { help(); process.exit(0); }
72
+ else if (a === "--dry-run") out.dryRun = true;
73
+ else if (a === "--ship") out.ship = true;
74
+ else if (a === "--message" || a === "-m") out.message = argv[++i] ?? "";
75
+ else if (a.includes(":")) [out.target, out.kind] = a.split(":");
76
+ else throw new Error(`Unknown arg: ${a}`);
77
+ }
78
+ if (!out.target || !out.kind) { help(); process.exit(1); }
79
+ if (out.target !== "root" && !PLUGINS.includes(out.target)) {
80
+ throw new Error(`Unknown plugin: ${out.target}. Known: ${PLUGINS.join(", ")}, root`);
81
+ }
82
+ if (!["patch", "minor", "major"].includes(out.kind)) {
83
+ throw new Error(`Unknown bump kind: ${out.kind}. Use patch | minor | major`);
84
+ }
85
+ if (out.target === "root" && out.kind !== "patch") {
86
+ throw new Error("Root bumps are reserved for patch releases (plugin changes drive minor/major).");
87
+ }
88
+ return out;
89
+ }
90
+
91
+ function ensureNoDrift(target, marketplace) {
92
+ if (target === "root") return;
93
+ const pluginPath = join(ROOT, `plugins/${target}/.claude-plugin/plugin.json`);
94
+ const pluginVersion = readJson(pluginPath).version;
95
+ const marketplaceVersion = marketplace.plugins.find((p) => p.name === target).version;
96
+ if (pluginVersion !== marketplaceVersion) {
97
+ throw new Error(
98
+ `Version drift on ${target}: plugin.json=${pluginVersion}, marketplace.json=${marketplaceVersion}. ` +
99
+ `Reconcile both files before releasing.`,
100
+ );
101
+ }
102
+ }
103
+
104
+ function ensureGitClean(allowedRelativePaths) {
105
+ const status = shRead("git status --porcelain").split("\n").filter(Boolean);
106
+ const offending = status.filter((line) => {
107
+ const file = line.slice(3);
108
+ return !allowedRelativePaths.some((a) => file === a);
109
+ });
110
+ if (offending.length) {
111
+ throw new Error(
112
+ `Working tree has changes outside the release scope. Commit or stash them first:\n${offending.join("\n")}`,
113
+ );
114
+ }
115
+ }
116
+
117
+ function main() {
118
+ const { target, kind, dryRun, ship, message } = parse(process.argv.slice(2));
119
+
120
+ const rootPkg = readJson(ROOT_PKG);
121
+ const marketplace = readJson(MARKETPLACE);
122
+ ensureNoDrift(target, marketplace);
123
+
124
+ const oldRoot = rootPkg.version;
125
+ const newRoot = bump(oldRoot, "patch");
126
+ let pluginPlan = null;
127
+ if (target !== "root") {
128
+ const pluginPath = join(ROOT, `plugins/${target}/.claude-plugin/plugin.json`);
129
+ const pluginJson = readJson(pluginPath);
130
+ const oldPlugin = pluginJson.version;
131
+ const newPlugin = bump(oldPlugin, kind);
132
+ pluginPlan = { name: target, pluginPath, pluginJson, oldPlugin, newPlugin };
133
+ }
134
+
135
+ console.log("\nRelease plan");
136
+ console.log("------------");
137
+ if (pluginPlan) console.log(` plugin ${pluginPlan.name}: ${pluginPlan.oldPlugin} → ${pluginPlan.newPlugin}`);
138
+ console.log(` root @heart-of-gold/toolkit: ${oldRoot} → ${newRoot}`);
139
+ console.log(` prepublish checks: will run`);
140
+ console.log(` commit: yes`);
141
+ console.log(` push: ${ship ? "yes" : "no"}`);
142
+ console.log(` npm publish: ${ship ? "yes" : "no"}`);
143
+ if (dryRun) {
144
+ console.log("\n[dry-run] Nothing changed.");
145
+ return;
146
+ }
147
+
148
+ const allowed = [
149
+ "package.json",
150
+ ".claude-plugin/marketplace.json",
151
+ pluginPlan ? `plugins/${pluginPlan.name}/.claude-plugin/plugin.json` : null,
152
+ ].filter(Boolean);
153
+ ensureGitClean(allowed);
154
+
155
+ if (pluginPlan) {
156
+ pluginPlan.pluginJson.version = pluginPlan.newPlugin;
157
+ writeJson(pluginPlan.pluginPath, pluginPlan.pluginJson);
158
+ const idx = marketplace.plugins.findIndex((p) => p.name === pluginPlan.name);
159
+ marketplace.plugins[idx].version = pluginPlan.newPlugin;
160
+ writeJson(MARKETPLACE, marketplace);
161
+ }
162
+ rootPkg.version = newRoot;
163
+ writeJson(ROOT_PKG, rootPkg);
164
+
165
+ console.log("\nRunning prepublish checks…");
166
+ sh("npm run check:publish-safety && npm run check:compat");
167
+
168
+ const subject = pluginPlan
169
+ ? `Release ${newRoot} — bump ${pluginPlan.name} to ${pluginPlan.newPlugin}${message ? `: ${message}` : ""}`
170
+ : `Release ${newRoot} — root patch${message ? `: ${message}` : ""}`;
171
+ sh(`git add ${allowed.map((p) => `"${p}"`).join(" ")}`);
172
+ sh(`git commit -m ${JSON.stringify(subject)}`);
173
+ console.log(`\nCommit created: ${subject}`);
174
+
175
+ if (!ship) {
176
+ console.log("\nNot shipping. To finish:");
177
+ console.log(" git push && npm publish --access public");
178
+ return;
179
+ }
180
+
181
+ console.log("\nPushing to origin…");
182
+ sh("git push");
183
+ console.log("\nPublishing to NPM…");
184
+ sh("npm publish --access public");
185
+ console.log(`\nReleased @heart-of-gold/toolkit@${newRoot}.`);
186
+ }
187
+
188
+ try {
189
+ main();
190
+ } catch (err) {
191
+ console.error(`\nrelease: ${err.message}\n`);
192
+ process.exit(1);
193
+ }