pi-gsd 2.0.4 → 2.0.6

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 (46) hide show
  1. package/.gsd/harnesses/pi/get-shit-done/workflows/add-phase.md +1 -1
  2. package/.gsd/harnesses/pi/get-shit-done/workflows/add-tests.md +1 -1
  3. package/.gsd/harnesses/pi/get-shit-done/workflows/add-todo.md +1 -1
  4. package/.gsd/harnesses/pi/get-shit-done/workflows/audit-milestone.md +1 -1
  5. package/.gsd/harnesses/pi/get-shit-done/workflows/autonomous.md +6 -6
  6. package/.gsd/harnesses/pi/get-shit-done/workflows/check-todos.md +1 -1
  7. package/.gsd/harnesses/pi/get-shit-done/workflows/complete-milestone.md +2 -2
  8. package/.gsd/harnesses/pi/get-shit-done/workflows/discuss-phase-assumptions.md +1 -1
  9. package/.gsd/harnesses/pi/get-shit-done/workflows/discuss-phase.md +1 -1
  10. package/.gsd/harnesses/pi/get-shit-done/workflows/do.md +1 -1
  11. package/.gsd/harnesses/pi/get-shit-done/workflows/execute-milestone.md +38 -38
  12. package/.gsd/harnesses/pi/get-shit-done/workflows/execute-plan.md +1 -1
  13. package/.gsd/harnesses/pi/get-shit-done/workflows/insert-phase.md +1 -1
  14. package/.gsd/harnesses/pi/get-shit-done/workflows/list-workspaces.md +1 -1
  15. package/.gsd/harnesses/pi/get-shit-done/workflows/manager.md +2 -2
  16. package/.gsd/harnesses/pi/get-shit-done/workflows/map-codebase.md +1 -1
  17. package/.gsd/harnesses/pi/get-shit-done/workflows/milestone-summary.md +1 -1
  18. package/.gsd/harnesses/pi/get-shit-done/workflows/new-milestone.md +1 -1
  19. package/.gsd/harnesses/pi/get-shit-done/workflows/new-project.md +1 -1
  20. package/.gsd/harnesses/pi/get-shit-done/workflows/new-workspace.md +1 -1
  21. package/.gsd/harnesses/pi/get-shit-done/workflows/pause-work.md +1 -1
  22. package/.gsd/harnesses/pi/get-shit-done/workflows/plan-milestone.md +8 -8
  23. package/.gsd/harnesses/pi/get-shit-done/workflows/progress.md +1 -1
  24. package/.gsd/harnesses/pi/get-shit-done/workflows/quick.md +1 -1
  25. package/.gsd/harnesses/pi/get-shit-done/workflows/remove-phase.md +1 -1
  26. package/.gsd/harnesses/pi/get-shit-done/workflows/remove-workspace.md +1 -1
  27. package/.gsd/harnesses/pi/get-shit-done/workflows/research-phase.md +1 -1
  28. package/.gsd/harnesses/pi/get-shit-done/workflows/resume-project.md +1 -1
  29. package/.gsd/harnesses/pi/get-shit-done/workflows/review.md +1 -1
  30. package/.gsd/harnesses/pi/get-shit-done/workflows/settings.md +1 -1
  31. package/.gsd/harnesses/pi/get-shit-done/workflows/ship.md +1 -1
  32. package/.gsd/harnesses/pi/get-shit-done/workflows/ui-phase.md +1 -1
  33. package/.gsd/harnesses/pi/get-shit-done/workflows/ui-review.md +1 -1
  34. package/.gsd/harnesses/pi/get-shit-done/workflows/validate-phase.md +1 -1
  35. package/.gsd/harnesses/pi/get-shit-done/workflows/verify-phase.md +1 -1
  36. package/.gsd/harnesses/pi/get-shit-done/workflows/verify-work.md +1 -1
  37. package/README.md +60 -60
  38. package/dist/pi-gsd-hooks.js +4 -4
  39. package/dist/pi-gsd-tools.js +2 -2
  40. package/package.json +1 -1
  41. package/prompts/gsd-discuss-phase.md +1 -1
  42. package/prompts/gsd-execute-phase.md +1 -1
  43. package/prompts/gsd-plan-phase.md +1 -1
  44. package/prompts/gsd-quick.md +1 -1
  45. package/prompts/gsd-verify-work.md +1 -1
  46. package/scripts/postinstall.js +323 -323
@@ -7,7 +7,7 @@
7
7
  * \`.gsd/harnesses/pi/\` into the consumer project's \`.pi/gsd/\`
8
8
  * and installs the \`pi-gsd-hooks.ts\` extension into \`.pi/extensions/\`.
9
9
  *
10
- * Safe to re-run files are skipped if already present (unless GSD_FORCE=1).
10
+ * Safe to re-run - files are skipped if already present (unless GSD_FORCE=1).
11
11
  */
12
12
 
13
13
  const fs = require("fs");
@@ -16,8 +16,8 @@ const path = require("path");
16
16
  // ─── Constants ────────────────────────────────────────────────────────────────
17
17
 
18
18
  const FORCE =
19
- process.env.GSD_FORCE_REINSTALL === "1" ||
20
- process.argv.includes("--force-reinstall");
19
+ process.env.GSD_FORCE_REINSTALL === "1" ||
20
+ process.argv.includes("--force-reinstall");
21
21
 
22
22
  /**
23
23
  * Directory that contains this package's files.
@@ -60,32 +60,32 @@ const HARNESSES = [{ src: "pi", dest: ".pi", hooks: true, subdir: "gsd" }];
60
60
  * @returns {{ copied: number, skipped: number }}
61
61
  */
62
62
  function copyDir(src, dest, overwrite) {
63
- let copied = 0;
64
- let skipped = 0;
65
-
66
- if (!fs.existsSync(src)) return { copied, skipped };
67
-
68
- fs.mkdirSync(dest, { recursive: true });
69
-
70
- for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
71
- const srcEntry = path.join(src, entry.name);
72
- const destEntry = path.join(dest, entry.name);
73
-
74
- if (entry.isDirectory()) {
75
- const sub = copyDir(srcEntry, destEntry, overwrite);
76
- copied += sub.copied;
77
- skipped += sub.skipped;
78
- } else if (entry.isFile()) {
79
- if (!overwrite && fs.existsSync(destEntry)) {
80
- skipped++;
81
- } else {
82
- fs.copyFileSync(srcEntry, destEntry);
83
- copied++;
84
- }
85
- }
86
- }
87
-
88
- return { copied, skipped };
63
+ let copied = 0;
64
+ let skipped = 0;
65
+
66
+ if (!fs.existsSync(src)) return { copied, skipped };
67
+
68
+ fs.mkdirSync(dest, { recursive: true });
69
+
70
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
71
+ const srcEntry = path.join(src, entry.name);
72
+ const destEntry = path.join(dest, entry.name);
73
+
74
+ if (entry.isDirectory()) {
75
+ const sub = copyDir(srcEntry, destEntry, overwrite);
76
+ copied += sub.copied;
77
+ skipped += sub.skipped;
78
+ } else if (entry.isFile()) {
79
+ if (!overwrite && fs.existsSync(destEntry)) {
80
+ skipped++;
81
+ } else {
82
+ fs.copyFileSync(srcEntry, destEntry);
83
+ copied++;
84
+ }
85
+ }
86
+ }
87
+
88
+ return { copied, skipped };
89
89
  }
90
90
 
91
91
  /**
@@ -96,203 +96,203 @@ function copyDir(src, dest, overwrite) {
96
96
  * @param {string} msg
97
97
  */
98
98
  function log(level, msg) {
99
- const isTTY = process.stdout.isTTY;
100
- const colours = {
101
- ok: isTTY ? "\x1b[32m✓\x1b[0m" : "✓",
102
- skip: isTTY ? "\x1b[33m–\x1b[0m" : "–",
103
- warn: isTTY ? "\x1b[33m⚠\x1b[0m" : "⚠",
104
- err: isTTY ? "\x1b[31m✗\x1b[0m" : "✗",
105
- };
106
- console.log(` ${colours[level] || " "} ${msg}`);
99
+ const isTTY = process.stdout.isTTY;
100
+ const colours = {
101
+ ok: isTTY ? "\x1b[32m✓\x1b[0m" : "✓",
102
+ skip: isTTY ? "\x1b[33m–\x1b[0m" : "–",
103
+ warn: isTTY ? "\x1b[33m⚠\x1b[0m" : "⚠",
104
+ err: isTTY ? "\x1b[31m✗\x1b[0m" : "✗",
105
+ };
106
+ console.log(` ${colours[level] || " "} ${msg}`);
107
107
  }
108
108
 
109
109
  // ─── Main ─────────────────────────────────────────────────────────────────────
110
110
 
111
111
  function main() {
112
- // Skip when running inside the package's own development tree
113
- // (i.e. when INIT_CWD === the package directory itself).
114
- if (path.resolve(PROJECT_ROOT) === path.resolve(PKG_DIR)) {
115
- log(
116
- "skip",
117
- "Running inside package source tree - skipping harness install.",
118
- );
119
- return;
120
- }
121
-
122
- // Skip when explicitly opted out
123
- if (process.env.GSD_SKIP_INSTALL === "1") {
124
- log("skip", "GSD_SKIP_INSTALL=1 - skipping harness install.");
125
- return;
126
- }
127
-
128
- const harnessesRoot = path.join(PKG_DIR, ".gsd", "harnesses");
129
- const hooksRoot = path.join(PKG_DIR, ".gsd", "hooks");
130
-
131
- console.log("");
132
- console.log(" GSD - installing harness files into your project…");
133
- if (FORCE)
134
- console.log(" (force-reinstall mode: existing files will be overwritten)");
135
- console.log("");
136
-
137
- let totalCopied = 0;
138
- let totalSkipped = 0;
139
- let installed = 0;
140
-
141
- for (const harness of HARNESSES) {
142
- const srcHarness = path.join(harnessesRoot, harness.src);
143
- const destHarness = path.join(PROJECT_ROOT, harness.dest);
144
-
145
- // ── get-shit-done/ content ──────────────────────────────────────────────
146
- const srcGsd = path.join(srcHarness, harness.subdir);
147
- const destGsd = path.join(destHarness, harness.subdir);
148
-
149
- if (!fs.existsSync(srcHarness)) {
150
- log(
151
- "skip",
152
- `${harness.dest}/${harness.subdir} (source absent - skipped)`,
153
- );
154
- continue;
155
- }
156
-
157
- const { copied, skipped } = copyDir(srcGsd, destGsd, FORCE);
158
- totalCopied += copied;
159
- totalSkipped += skipped;
160
-
161
- if (copied > 0 || skipped === 0) {
162
- log(
163
- "ok",
164
- `${harness.dest}/${harness.subdir} (${copied} file${copied === 1 ? "" : "s"} installed)`,
165
- );
166
- } else {
167
- log(
168
- "skip",
169
- `${harness.dest}/${harness.subdir} (already up-to-date, ${skipped} file${skipped === 1 ? "" : "s"} skipped)`,
170
- );
171
- }
172
-
173
- // ── gsd-file-manifest.json ──────────────────────────────────────────────
174
- const manifestSrc = path.join(srcHarness, "gsd-file-manifest.json");
175
- const manifestDest = path.join(destHarness, "gsd-file-manifest.json");
176
-
177
- if (fs.existsSync(manifestSrc)) {
178
- if (!FORCE && fs.existsSync(manifestDest)) {
179
- totalSkipped++;
180
- } else {
181
- fs.mkdirSync(destHarness, { recursive: true });
182
- fs.copyFileSync(manifestSrc, manifestDest);
183
- totalCopied++;
184
- }
185
- }
186
-
187
- // ── hooks/ (platform-selective) ─────────────────────────────────────────
188
- if (harness.hooks && fs.existsSync(hooksRoot)) {
189
- const destHooks = path.join(destHarness, "hooks");
190
- const h = copyDir(hooksRoot, destHooks, FORCE);
191
- totalCopied += h.copied;
192
- totalSkipped += h.skipped;
193
-
194
- if (h.copied > 0) {
195
- log(
196
- "ok",
197
- `${harness.dest}/hooks (${h.copied} hook${h.copied === 1 ? "" : "s"} installed)`,
198
- );
199
- }
200
- }
201
-
202
- // ── skills/ (opencode only - present in .gsd/harnesses/opencode/skills) ─
203
- const srcSkills = path.join(srcHarness, "skills");
204
- const destSkills = path.join(destHarness, "skills");
205
-
206
- if (fs.existsSync(srcSkills)) {
207
- const s = copyDir(srcSkills, destSkills, FORCE);
208
- totalCopied += s.copied;
209
- totalSkipped += s.skipped;
210
-
211
- if (s.copied > 0) {
212
- log(
213
- "ok",
214
- `${harness.dest}/skills (${s.copied} skill file${s.copied === 1 ? "" : "s"} installed)`,
215
- );
216
- }
217
- }
218
-
219
- installed++;
220
- }
221
-
222
- // ── Pi extension served from npm package via pi.extensions in package.json ─────
223
- // No local copying needed. Cleanup stale files from old install approach.
224
- const extDir = path.join(PROJECT_ROOT, ".pi", "extensions");
225
- const staleExts = ["gsd-hooks.ts", "pi-gsd-hooks.ts"];
226
- for (const name of staleExts) {
227
- const stale = path.join(extDir, name);
228
- if (fs.existsSync(stale)) {
229
- fs.rmSync(stale);
230
- log("ok", `.pi/extensions/${name} (removed extension now served from package)`);
231
- }
232
- }
233
- const settingsFile2 = path.join(PROJECT_ROOT, ".pi", "settings.json");
234
- if (fs.existsSync(settingsFile2)) {
235
- try {
236
- const settings2 = JSON.parse(fs.readFileSync(settingsFile2, "utf8"));
237
- if (Array.isArray(settings2.extensions)) {
238
- const cleaned2 = settings2.extensions.filter((e) => !staleExts.some((n) => e.endsWith(n)));
239
- if (cleaned2.length !== settings2.extensions.length) {
240
- settings2.extensions = cleaned2;
241
- fs.writeFileSync(settingsFile2, JSON.stringify(settings2, null, "\t"), "utf8");
242
- log("ok", ".pi/settings.json (removed stale extension entries)");
243
- }
244
- }
245
- } catch { /* ignore */ }
246
- }
247
- // ── Pi prompt templates cleanup stale local copies ───────────────────────
248
- // Prompts are served directly from the npm package (user scope).
249
- // Local copies in .pi/prompts/ cause collision warnings on every pi update.
250
- // Remove any gsd-*.md files previously installed there.
251
- // Also remove the old gsd-hooks.ts if present (renamed to pi-gsd-hooks.ts).
252
- const promptsDest = path.join(PROJECT_ROOT, ".pi", "prompts");
253
- if (fs.existsSync(promptsDest)) {
254
- const stale = fs
255
- .readdirSync(promptsDest)
256
- .filter((f) => f.startsWith("gsd-") && f.endsWith(".md"));
257
- if (stale.length > 0) {
258
- for (const f of stale) fs.rmSync(path.join(promptsDest, f));
259
- log(
260
- "ok",
261
- `.pi/prompts (removed ${stale.length} stale local gsd-*.md served from package instead)`,
262
- );
263
- }
264
- }
265
- const oldExt = path.join(PROJECT_ROOT, ".pi", "extensions", "gsd-hooks.ts");
266
- if (fs.existsSync(oldExt)) {
267
- fs.rmSync(oldExt);
268
- log(
269
- "ok",
270
- ".pi/extensions/gsd-hooks.ts (removed renamed to pi-gsd-hooks.ts)",
271
- );
272
- }
273
-
274
- console.log("");
275
-
276
- if (installed === 0) {
277
- log("warn", "No harness source directories found inside the package.");
278
- log(
279
- "warn",
280
- "The package may be incomplete. Try: npm install --force get-shit-done-cc",
281
- );
282
- console.log("");
283
- return;
284
- }
285
-
286
- console.log(` GSD v${getPackageVersion()} installed successfully.`);
287
- console.log(
288
- ` ${totalCopied} file${totalCopied === 1 ? "" : "s"} copied, ${totalSkipped} skipped.`,
289
- );
290
- console.log("");
291
- console.log(" Next steps:");
292
- console.log(" Run /gsd-new-project to initialise a project.");
293
- console.log("");
294
- console.log(" Docs: https://github.com/fulgidus/pi-gsd#readme");
295
- console.log("");
112
+ // Skip when running inside the package's own development tree
113
+ // (i.e. when INIT_CWD === the package directory itself).
114
+ if (path.resolve(PROJECT_ROOT) === path.resolve(PKG_DIR)) {
115
+ log(
116
+ "skip",
117
+ "Running inside package source tree - skipping harness install.",
118
+ );
119
+ return;
120
+ }
121
+
122
+ // Skip when explicitly opted out
123
+ if (process.env.GSD_SKIP_INSTALL === "1") {
124
+ log("skip", "GSD_SKIP_INSTALL=1 - skipping harness install.");
125
+ return;
126
+ }
127
+
128
+ const harnessesRoot = path.join(PKG_DIR, ".gsd", "harnesses");
129
+ const hooksRoot = path.join(PKG_DIR, ".gsd", "hooks");
130
+
131
+ console.log("");
132
+ console.log(" GSD - installing harness files into your project…");
133
+ if (FORCE)
134
+ console.log(" (force-reinstall mode: existing files will be overwritten)");
135
+ console.log("");
136
+
137
+ let totalCopied = 0;
138
+ let totalSkipped = 0;
139
+ let installed = 0;
140
+
141
+ for (const harness of HARNESSES) {
142
+ const srcHarness = path.join(harnessesRoot, harness.src);
143
+ const destHarness = path.join(PROJECT_ROOT, harness.dest);
144
+
145
+ // ── get-shit-done/ content ──────────────────────────────────────────────
146
+ const srcGsd = path.join(srcHarness, harness.subdir);
147
+ const destGsd = path.join(destHarness, harness.subdir);
148
+
149
+ if (!fs.existsSync(srcHarness)) {
150
+ log(
151
+ "skip",
152
+ `${harness.dest}/${harness.subdir} (source absent - skipped)`,
153
+ );
154
+ continue;
155
+ }
156
+
157
+ const { copied, skipped } = copyDir(srcGsd, destGsd, FORCE);
158
+ totalCopied += copied;
159
+ totalSkipped += skipped;
160
+
161
+ if (copied > 0 || skipped === 0) {
162
+ log(
163
+ "ok",
164
+ `${harness.dest}/${harness.subdir} (${copied} file${copied === 1 ? "" : "s"} installed)`,
165
+ );
166
+ } else {
167
+ log(
168
+ "skip",
169
+ `${harness.dest}/${harness.subdir} (already up-to-date, ${skipped} file${skipped === 1 ? "" : "s"} skipped)`,
170
+ );
171
+ }
172
+
173
+ // ── gsd-file-manifest.json ──────────────────────────────────────────────
174
+ const manifestSrc = path.join(srcHarness, "gsd-file-manifest.json");
175
+ const manifestDest = path.join(destHarness, "gsd-file-manifest.json");
176
+
177
+ if (fs.existsSync(manifestSrc)) {
178
+ if (!FORCE && fs.existsSync(manifestDest)) {
179
+ totalSkipped++;
180
+ } else {
181
+ fs.mkdirSync(destHarness, { recursive: true });
182
+ fs.copyFileSync(manifestSrc, manifestDest);
183
+ totalCopied++;
184
+ }
185
+ }
186
+
187
+ // ── hooks/ (platform-selective) ─────────────────────────────────────────
188
+ if (harness.hooks && fs.existsSync(hooksRoot)) {
189
+ const destHooks = path.join(destHarness, "hooks");
190
+ const h = copyDir(hooksRoot, destHooks, FORCE);
191
+ totalCopied += h.copied;
192
+ totalSkipped += h.skipped;
193
+
194
+ if (h.copied > 0) {
195
+ log(
196
+ "ok",
197
+ `${harness.dest}/hooks (${h.copied} hook${h.copied === 1 ? "" : "s"} installed)`,
198
+ );
199
+ }
200
+ }
201
+
202
+ // ── skills/ (opencode only - present in .gsd/harnesses/opencode/skills) ─
203
+ const srcSkills = path.join(srcHarness, "skills");
204
+ const destSkills = path.join(destHarness, "skills");
205
+
206
+ if (fs.existsSync(srcSkills)) {
207
+ const s = copyDir(srcSkills, destSkills, FORCE);
208
+ totalCopied += s.copied;
209
+ totalSkipped += s.skipped;
210
+
211
+ if (s.copied > 0) {
212
+ log(
213
+ "ok",
214
+ `${harness.dest}/skills (${s.copied} skill file${s.copied === 1 ? "" : "s"} installed)`,
215
+ );
216
+ }
217
+ }
218
+
219
+ installed++;
220
+ }
221
+
222
+ // ── Pi extension - served from npm package via pi.extensions in package.json ─────
223
+ // No local copying needed. Cleanup stale files from old install approach.
224
+ const extDir = path.join(PROJECT_ROOT, ".pi", "extensions");
225
+ const staleExts = ["gsd-hooks.ts", "pi-gsd-hooks.ts"];
226
+ for (const name of staleExts) {
227
+ const stale = path.join(extDir, name);
228
+ if (fs.existsSync(stale)) {
229
+ fs.rmSync(stale);
230
+ log("ok", `.pi/extensions/${name} (removed - extension now served from package)`);
231
+ }
232
+ }
233
+ const settingsFile2 = path.join(PROJECT_ROOT, ".pi", "settings.json");
234
+ if (fs.existsSync(settingsFile2)) {
235
+ try {
236
+ const settings2 = JSON.parse(fs.readFileSync(settingsFile2, "utf8"));
237
+ if (Array.isArray(settings2.extensions)) {
238
+ const cleaned2 = settings2.extensions.filter((e) => !staleExts.some((n) => e.endsWith(n)));
239
+ if (cleaned2.length !== settings2.extensions.length) {
240
+ settings2.extensions = cleaned2;
241
+ fs.writeFileSync(settingsFile2, JSON.stringify(settings2, null, "\t"), "utf8");
242
+ log("ok", ".pi/settings.json (removed stale extension entries)");
243
+ }
244
+ }
245
+ } catch { /* ignore */ }
246
+ }
247
+ // ── Pi prompt templates - cleanup stale local copies ───────────────────────
248
+ // Prompts are served directly from the npm package (user scope).
249
+ // Local copies in .pi/prompts/ cause collision warnings on every pi update.
250
+ // Remove any gsd-*.md files previously installed there.
251
+ // Also remove the old gsd-hooks.ts if present (renamed to pi-gsd-hooks.ts).
252
+ const promptsDest = path.join(PROJECT_ROOT, ".pi", "prompts");
253
+ if (fs.existsSync(promptsDest)) {
254
+ const stale = fs
255
+ .readdirSync(promptsDest)
256
+ .filter((f) => f.startsWith("gsd-") && f.endsWith(".md"));
257
+ if (stale.length > 0) {
258
+ for (const f of stale) fs.rmSync(path.join(promptsDest, f));
259
+ log(
260
+ "ok",
261
+ `.pi/prompts (removed ${stale.length} stale local gsd-*.md - served from package instead)`,
262
+ );
263
+ }
264
+ }
265
+ const oldExt = path.join(PROJECT_ROOT, ".pi", "extensions", "gsd-hooks.ts");
266
+ if (fs.existsSync(oldExt)) {
267
+ fs.rmSync(oldExt);
268
+ log(
269
+ "ok",
270
+ ".pi/extensions/gsd-hooks.ts (removed - renamed to pi-gsd-hooks.ts)",
271
+ );
272
+ }
273
+
274
+ console.log("");
275
+
276
+ if (installed === 0) {
277
+ log("warn", "No harness source directories found inside the package.");
278
+ log(
279
+ "warn",
280
+ "The package may be incomplete. Try: npm install --force get-shit-done-cc",
281
+ );
282
+ console.log("");
283
+ return;
284
+ }
285
+
286
+ console.log(` GSD v${getPackageVersion()} installed successfully.`);
287
+ console.log(
288
+ ` ${totalCopied} file${totalCopied === 1 ? "" : "s"} copied, ${totalSkipped} skipped.`,
289
+ );
290
+ console.log("");
291
+ console.log(" Next steps:");
292
+ console.log(" Run /gsd-new-project to initialise a project.");
293
+ console.log("");
294
+ console.log(" Docs: https://github.com/fulgidus/pi-gsd#readme");
295
+ console.log("");
296
296
  }
297
297
 
298
298
  /**
@@ -302,14 +302,14 @@ function main() {
302
302
  * @returns {string}
303
303
  */
304
304
  function getPackageVersion() {
305
- try {
306
- const pkg = JSON.parse(
307
- fs.readFileSync(path.join(PKG_DIR, "package.json"), "utf8"),
308
- );
309
- return pkg.version || "unknown";
310
- } catch {
311
- return "unknown";
312
- }
305
+ try {
306
+ const pkg = JSON.parse(
307
+ fs.readFileSync(path.join(PKG_DIR, "package.json"), "utf8"),
308
+ );
309
+ return pkg.version || "unknown";
310
+ } catch {
311
+ return "unknown";
312
+ }
313
313
  }
314
314
 
315
315
  /**
@@ -327,100 +327,100 @@ function getPackageVersion() {
327
327
  * @param {function} callback Called with (copied: boolean)
328
328
  */
329
329
  function installPiExtension(projectRoot, pkgDir, force, callback) {
330
- const piDir = path.join(projectRoot, ".pi");
331
- const extDir = path.join(piDir, "extensions");
332
- const extDest = path.join(extDir, "pi-gsd-hooks.ts");
333
- const extSrc = path.join(pkgDir, ".gsd", "extensions", "pi-gsd-hooks.ts");
334
-
335
- if (!fs.existsSync(extSrc)) {
336
- log("warn", ".pi/extensions/pi-gsd-hooks.ts (source absent - skipped)");
337
- callback(false);
338
- return;
339
- }
340
-
341
- // Always update the extension it is owned by pi-gsd, not the user.
342
- // Compare pi-gsd-extension-version comments to detect staleness.
343
- const extractVersion = (file) => {
344
- try {
345
- const match = fs
346
- .readFileSync(file, "utf8")
347
- .match(/pi-gsd-extension-version:\s*([\d.]+)/);
348
- return match ? match[1] : null;
349
- } catch {
350
- return null;
351
- }
352
- };
353
- const srcVersion = extractVersion(extSrc);
354
- const destVersion = fs.existsSync(extDest) ? extractVersion(extDest) : null;
355
- const needsUpdate = force || !destVersion || destVersion !== srcVersion;
356
-
357
- if (!needsUpdate) {
358
- log("skip", `.pi/extensions/pi-gsd-hooks.ts (up-to-date v${destVersion})`);
359
- callback(false);
360
- } else {
361
- try {
362
- fs.mkdirSync(extDir, { recursive: true });
363
- fs.copyFileSync(extSrc, extDest);
364
- log(
365
- "ok",
366
- ".pi/extensions/pi-gsd-hooks.ts (GSD lifecycle extension installed)",
367
- );
368
- callback(true);
369
- } catch (e) {
370
- log(
371
- "warn",
372
- ".pi/extensions/pi-gsd-hooks.ts (install failed: " + e.message + ")",
373
- );
374
- callback(false);
375
- return;
376
- }
377
- }
378
-
379
- // Update .pi/settings.json to include the extension path in the extensions array.
380
- // The file is already auto-discovered from .pi/extensions/, but explicit registration
381
- // is added as a belt-and-suspenders measure.
382
- const settingsFile = path.join(piDir, "settings.json");
383
- try {
384
- let settings = {};
385
- if (fs.existsSync(settingsFile)) {
386
- try {
387
- settings = JSON.parse(fs.readFileSync(settingsFile, "utf8"));
388
- } catch {
389
- // Unreadable settings - start fresh object
390
- }
391
- }
392
-
393
- const extensions = Array.isArray(settings.extensions)
394
- ? settings.extensions
395
- : [];
396
-
397
- // Remove stale gsd-hooks.ts entry if present (renamed to pi-gsd-hooks.ts)
398
- const oldExtPath = path.join(extDir, "gsd-hooks.ts");
399
- const cleaned = extensions.filter((e) => e !== oldExtPath);
400
-
401
- // Avoid duplicate entries
402
- if (!cleaned.includes(extDest)) {
403
- settings.extensions = [...cleaned, extDest];
404
- fs.mkdirSync(piDir, { recursive: true });
405
- fs.writeFileSync(
406
- settingsFile,
407
- JSON.stringify(settings, null, "\t"),
408
- "utf8",
409
- );
410
- log("ok", ".pi/settings.json (extensions array updated)");
411
- } else if (cleaned.length !== extensions.length) {
412
- // Removed stale entry but extDest already present
413
- settings.extensions = cleaned;
414
- fs.writeFileSync(
415
- settingsFile,
416
- JSON.stringify(settings, null, "\t"),
417
- "utf8",
418
- );
419
- log("ok", ".pi/settings.json (removed stale gsd-hooks.ts entry)");
420
- }
421
- } catch (e) {
422
- log("warn", ".pi/settings.json (could not update: " + e.message + ")");
423
- }
330
+ const piDir = path.join(projectRoot, ".pi");
331
+ const extDir = path.join(piDir, "extensions");
332
+ const extDest = path.join(extDir, "pi-gsd-hooks.ts");
333
+ const extSrc = path.join(pkgDir, ".gsd", "extensions", "pi-gsd-hooks.ts");
334
+
335
+ if (!fs.existsSync(extSrc)) {
336
+ log("warn", ".pi/extensions/pi-gsd-hooks.ts (source absent - skipped)");
337
+ callback(false);
338
+ return;
339
+ }
340
+
341
+ // Always update the extension - it is owned by pi-gsd, not the user.
342
+ // Compare pi-gsd-extension-version comments to detect staleness.
343
+ const extractVersion = (file) => {
344
+ try {
345
+ const match = fs
346
+ .readFileSync(file, "utf8")
347
+ .match(/pi-gsd-extension-version:\s*([\d.]+)/);
348
+ return match ? match[1] : null;
349
+ } catch {
350
+ return null;
351
+ }
352
+ };
353
+ const srcVersion = extractVersion(extSrc);
354
+ const destVersion = fs.existsSync(extDest) ? extractVersion(extDest) : null;
355
+ const needsUpdate = force || !destVersion || destVersion !== srcVersion;
356
+
357
+ if (!needsUpdate) {
358
+ log("skip", `.pi/extensions/pi-gsd-hooks.ts (up-to-date v${destVersion})`);
359
+ callback(false);
360
+ } else {
361
+ try {
362
+ fs.mkdirSync(extDir, { recursive: true });
363
+ fs.copyFileSync(extSrc, extDest);
364
+ log(
365
+ "ok",
366
+ ".pi/extensions/pi-gsd-hooks.ts (GSD lifecycle extension installed)",
367
+ );
368
+ callback(true);
369
+ } catch (e) {
370
+ log(
371
+ "warn",
372
+ ".pi/extensions/pi-gsd-hooks.ts (install failed: " + e.message + ")",
373
+ );
374
+ callback(false);
375
+ return;
376
+ }
377
+ }
378
+
379
+ // Update .pi/settings.json to include the extension path in the extensions array.
380
+ // The file is already auto-discovered from .pi/extensions/, but explicit registration
381
+ // is added as a belt-and-suspenders measure.
382
+ const settingsFile = path.join(piDir, "settings.json");
383
+ try {
384
+ let settings = {};
385
+ if (fs.existsSync(settingsFile)) {
386
+ try {
387
+ settings = JSON.parse(fs.readFileSync(settingsFile, "utf8"));
388
+ } catch {
389
+ // Unreadable settings - start fresh object
390
+ }
391
+ }
392
+
393
+ const extensions = Array.isArray(settings.extensions)
394
+ ? settings.extensions
395
+ : [];
396
+
397
+ // Remove stale gsd-hooks.ts entry if present (renamed to pi-gsd-hooks.ts)
398
+ const oldExtPath = path.join(extDir, "gsd-hooks.ts");
399
+ const cleaned = extensions.filter((e) => e !== oldExtPath);
400
+
401
+ // Avoid duplicate entries
402
+ if (!cleaned.includes(extDest)) {
403
+ settings.extensions = [...cleaned, extDest];
404
+ fs.mkdirSync(piDir, { recursive: true });
405
+ fs.writeFileSync(
406
+ settingsFile,
407
+ JSON.stringify(settings, null, "\t"),
408
+ "utf8",
409
+ );
410
+ log("ok", ".pi/settings.json (extensions array updated)");
411
+ } else if (cleaned.length !== extensions.length) {
412
+ // Removed stale entry but extDest already present
413
+ settings.extensions = cleaned;
414
+ fs.writeFileSync(
415
+ settingsFile,
416
+ JSON.stringify(settings, null, "\t"),
417
+ "utf8",
418
+ );
419
+ log("ok", ".pi/settings.json (removed stale gsd-hooks.ts entry)");
420
+ }
421
+ } catch (e) {
422
+ log("warn", ".pi/settings.json (could not update: " + e.message + ")");
423
+ }
424
424
  }
425
425
 
426
426
  main();