pi-gsd 1.2.2 → 1.3.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.
Files changed (58) hide show
  1. package/.gsd/extensions/gsd-hooks.ts +469 -211
  2. package/README.md +2 -2
  3. package/package.json +6 -2
  4. package/prompts/gsd-add-backlog.md +6 -0
  5. package/prompts/gsd-add-phase.md +6 -0
  6. package/prompts/gsd-add-tests.md +6 -0
  7. package/prompts/gsd-add-todo.md +6 -0
  8. package/prompts/gsd-audit-milestone.md +6 -0
  9. package/prompts/gsd-audit-uat.md +6 -0
  10. package/prompts/gsd-autonomous.md +6 -0
  11. package/prompts/gsd-check-todos.md +6 -0
  12. package/prompts/gsd-cleanup.md +6 -0
  13. package/prompts/gsd-complete-milestone.md +6 -0
  14. package/prompts/gsd-debug.md +6 -0
  15. package/prompts/gsd-discuss-phase.md +6 -0
  16. package/prompts/gsd-do.md +6 -0
  17. package/prompts/gsd-execute-phase.md +6 -0
  18. package/prompts/gsd-fast.md +6 -0
  19. package/prompts/gsd-forensics.md +6 -0
  20. package/prompts/gsd-insert-phase.md +6 -0
  21. package/prompts/gsd-join-discord.md +6 -0
  22. package/prompts/gsd-list-phase-assumptions.md +6 -0
  23. package/prompts/gsd-list-workspaces.md +6 -0
  24. package/prompts/gsd-manager.md +6 -0
  25. package/prompts/gsd-map-codebase.md +6 -0
  26. package/prompts/gsd-milestone-summary.md +6 -0
  27. package/prompts/gsd-new-milestone.md +6 -0
  28. package/prompts/gsd-new-project.md +6 -0
  29. package/prompts/gsd-new-workspace.md +6 -0
  30. package/prompts/gsd-next.md +6 -0
  31. package/prompts/gsd-note.md +6 -0
  32. package/prompts/gsd-pause-work.md +6 -0
  33. package/prompts/gsd-plan-milestone-gaps.md +6 -0
  34. package/prompts/gsd-plan-phase.md +6 -0
  35. package/prompts/gsd-plant-seed.md +6 -0
  36. package/prompts/gsd-pr-branch.md +6 -0
  37. package/prompts/gsd-profile-user.md +6 -0
  38. package/prompts/gsd-quick.md +6 -0
  39. package/prompts/gsd-reapply-patches.md +6 -0
  40. package/prompts/gsd-remove-phase.md +6 -0
  41. package/prompts/gsd-remove-workspace.md +6 -0
  42. package/prompts/gsd-research-phase.md +6 -0
  43. package/prompts/gsd-resume-work.md +6 -0
  44. package/prompts/gsd-review-backlog.md +6 -0
  45. package/prompts/gsd-review.md +6 -0
  46. package/prompts/gsd-session-report.md +6 -0
  47. package/prompts/gsd-set-profile.md +6 -0
  48. package/prompts/gsd-settings.md +6 -0
  49. package/prompts/gsd-setup-pi.md +6 -0
  50. package/prompts/gsd-ship.md +6 -0
  51. package/prompts/gsd-thread.md +6 -0
  52. package/prompts/gsd-ui-phase.md +6 -0
  53. package/prompts/gsd-ui-review.md +6 -0
  54. package/prompts/gsd-update.md +6 -0
  55. package/prompts/gsd-validate-phase.md +6 -0
  56. package/prompts/gsd-verify-work.md +6 -0
  57. package/prompts/gsd-workstreams.md +6 -0
  58. package/scripts/postinstall.js +265 -250
@@ -3,7 +3,7 @@
3
3
  * postinstall.js - GSD harness installer
4
4
  *
5
5
  * Runs automatically after `npm install pi-gsd`.
6
- * Copies the pi harness from this package's
6
+ * Copies the pi harness from this package's
7
7
  * \`.gsd/harnesses/pi/\` into the consumer project's \`.pi/gsd/\`
8
8
  * and installs the \`gsd-hooks.ts\` extension into \`.pi/extensions/\`.
9
9
  *
@@ -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.
@@ -40,9 +40,7 @@ const PROJECT_ROOT = process.env.INIT_CWD || process.cwd();
40
40
  * dest - directory in the consumer project root
41
41
  * hooks - whether this platform supports GSD hooks (copied from .gsd/hooks/)
42
42
  */
43
- const HARNESSES = [
44
- { src: "pi", dest: ".pi", hooks: true, subdir: "gsd" },
45
- ];
43
+ const HARNESSES = [{ src: "pi", dest: ".pi", hooks: true, subdir: "gsd" }];
46
44
 
47
45
  /**
48
46
  * Subdirectory name used inside each harness's dest folder for
@@ -62,32 +60,32 @@ const HARNESSES = [
62
60
  * @returns {{ copied: number, skipped: number }}
63
61
  */
64
62
  function copyDir(src, dest, overwrite) {
65
- let copied = 0;
66
- let skipped = 0;
67
-
68
- if (!fs.existsSync(src)) return { copied, skipped };
69
-
70
- fs.mkdirSync(dest, { recursive: true });
71
-
72
- for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
73
- const srcEntry = path.join(src, entry.name);
74
- const destEntry = path.join(dest, entry.name);
75
-
76
- if (entry.isDirectory()) {
77
- const sub = copyDir(srcEntry, destEntry, overwrite);
78
- copied += sub.copied;
79
- skipped += sub.skipped;
80
- } else if (entry.isFile()) {
81
- if (!overwrite && fs.existsSync(destEntry)) {
82
- skipped++;
83
- } else {
84
- fs.copyFileSync(srcEntry, destEntry);
85
- copied++;
86
- }
87
- }
88
- }
89
-
90
- 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 };
91
89
  }
92
90
 
93
91
  /**
@@ -98,156 +96,173 @@ function copyDir(src, dest, overwrite) {
98
96
  * @param {string} msg
99
97
  */
100
98
  function log(level, msg) {
101
- const isTTY = process.stdout.isTTY;
102
- const colours = {
103
- ok: isTTY ? "\x1b[32m✓\x1b[0m" : "✓",
104
- skip: isTTY ? "\x1b[33m–\x1b[0m" : "–",
105
- warn: isTTY ? "\x1b[33m⚠\x1b[0m" : "⚠",
106
- err: isTTY ? "\x1b[31m✗\x1b[0m" : "✗",
107
- };
108
- 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}`);
109
107
  }
110
108
 
111
109
  // ─── Main ─────────────────────────────────────────────────────────────────────
112
110
 
113
111
  function main() {
114
- // Skip when running inside the package's own development tree
115
- // (i.e. when INIT_CWD === the package directory itself).
116
- if (path.resolve(PROJECT_ROOT) === path.resolve(PKG_DIR)) {
117
- log(
118
- "skip",
119
- "Running inside package source tree - skipping harness install.",
120
- );
121
- return;
122
- }
123
-
124
- // Skip when explicitly opted out
125
- if (process.env.GSD_SKIP_INSTALL === "1") {
126
- log("skip", "GSD_SKIP_INSTALL=1 - skipping harness install.");
127
- return;
128
- }
129
-
130
- const harnessesRoot = path.join(PKG_DIR, ".gsd", "harnesses");
131
- const hooksRoot = path.join(PKG_DIR, ".gsd", "hooks");
132
-
133
- console.log("");
134
- console.log(" GSD - installing harness files into your project…");
135
- if (FORCE)
136
- console.log(" (force-reinstall mode: existing files will be overwritten)");
137
- console.log("");
138
-
139
- let totalCopied = 0;
140
- let totalSkipped = 0;
141
- let installed = 0;
142
-
143
- for (const harness of HARNESSES) {
144
- const srcHarness = path.join(harnessesRoot, harness.src);
145
- const destHarness = path.join(PROJECT_ROOT, harness.dest);
146
-
147
- // ── get-shit-done/ content ──────────────────────────────────────────────
148
- const srcGsd = path.join(srcHarness, harness.subdir);
149
- const destGsd = path.join(destHarness, harness.subdir);
150
-
151
- if (!fs.existsSync(srcHarness)) {
152
- log("skip", `${harness.dest}/${harness.subdir} (source absent - skipped)`);
153
- continue;
154
- }
155
-
156
- const { copied, skipped } = copyDir(srcGsd, destGsd, FORCE);
157
- totalCopied += copied;
158
- totalSkipped += skipped;
159
-
160
- if (copied > 0 || skipped === 0) {
161
- log(
162
- "ok",
163
- `${harness.dest}/${harness.subdir} (${copied} file${copied === 1 ? "" : "s"} installed)`,
164
- );
165
- } else {
166
- log(
167
- "skip",
168
- `${harness.dest}/${harness.subdir} (already up-to-date, ${skipped} file${skipped === 1 ? "" : "s"} skipped)`,
169
- );
170
- }
171
-
172
- // ── gsd-file-manifest.json ──────────────────────────────────────────────
173
- const manifestSrc = path.join(srcHarness, "gsd-file-manifest.json");
174
- const manifestDest = path.join(destHarness, "gsd-file-manifest.json");
175
-
176
- if (fs.existsSync(manifestSrc)) {
177
- if (!FORCE && fs.existsSync(manifestDest)) {
178
- totalSkipped++;
179
- } else {
180
- fs.mkdirSync(destHarness, { recursive: true });
181
- fs.copyFileSync(manifestSrc, manifestDest);
182
- totalCopied++;
183
- }
184
- }
185
-
186
- // ── hooks/ (platform-selective) ─────────────────────────────────────────
187
- if (harness.hooks && fs.existsSync(hooksRoot)) {
188
- const destHooks = path.join(destHarness, "hooks");
189
- const h = copyDir(hooksRoot, destHooks, FORCE);
190
- totalCopied += h.copied;
191
- totalSkipped += h.skipped;
192
-
193
- if (h.copied > 0) {
194
- log(
195
- "ok",
196
- `${harness.dest}/hooks (${h.copied} hook${h.copied === 1 ? "" : "s"} installed)`,
197
- );
198
- }
199
- }
200
-
201
- // ── skills/ (opencode only - present in .gsd/harnesses/opencode/skills) ─
202
- const srcSkills = path.join(srcHarness, "skills");
203
- const destSkills = path.join(destHarness, "skills");
204
-
205
- if (fs.existsSync(srcSkills)) {
206
- const s = copyDir(srcSkills, destSkills, FORCE);
207
- totalCopied += s.copied;
208
- totalSkipped += s.skipped;
209
-
210
- if (s.copied > 0) {
211
- log(
212
- "ok",
213
- `${harness.dest}/skills (${s.copied} skill file${s.copied === 1 ? "" : "s"} installed)`,
214
- );
215
- }
216
- }
217
-
218
- installed++;
219
- }
220
-
221
- // ── Pi extension (.pi/extensions/gsd-hooks.ts) ─────────────────────────────
222
- // Install the GSD pi lifecycle extension (session_start, tool_call, tool_result hooks).
223
- // The extension is auto-discovered by pi from .pi/extensions/ - no manual wiring needed.
224
- installPiExtension(PROJECT_ROOT, PKG_DIR, FORCE, (copied) => {
225
- if (copied) totalCopied++;
226
- else totalSkipped++;
227
- });
228
-
229
- console.log("");
230
-
231
- if (installed === 0) {
232
- log("warn", "No harness source directories found inside the package.");
233
- log(
234
- "warn",
235
- "The package may be incomplete. Try: npm install --force get-shit-done-cc",
236
- );
237
- console.log("");
238
- return;
239
- }
240
-
241
- console.log(` GSD v${getPackageVersion()} installed successfully.`);
242
- console.log(
243
- ` ${totalCopied} file${totalCopied === 1 ? "" : "s"} copied, ${totalSkipped} skipped.`,
244
- );
245
- console.log("");
246
- console.log(" Next steps:");
247
- console.log(" Run /gsd-new-project to initialise a project.");
248
- console.log("");
249
- console.log(" Docs: https://github.com/fulgidus/pi-gsd#readme");
250
- 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 (.pi/extensions/gsd-hooks.ts) ─────────────────────────────
223
+ // Install the GSD pi lifecycle extension (session_start, tool_call, tool_result hooks).
224
+ // The extension is auto-discovered by pi from .pi/extensions/ - no manual wiring needed.
225
+ installPiExtension(PROJECT_ROOT, PKG_DIR, FORCE, (copied) => {
226
+ if (copied) totalCopied++;
227
+ else totalSkipped++;
228
+ });
229
+
230
+ // ── Pi prompt templates (.pi/prompts/gsd-*.md) ──────────────────────────────
231
+ const promptsSrc = path.join(PKG_DIR, "prompts");
232
+ const promptsDest = path.join(PROJECT_ROOT, ".pi", "prompts");
233
+ if (fs.existsSync(promptsSrc)) {
234
+ const p = copyDir(promptsSrc, promptsDest, FORCE);
235
+ totalCopied += p.copied;
236
+ totalSkipped += p.skipped;
237
+ if (p.copied > 0) {
238
+ log("ok", `.pi/prompts (${p.copied} template${p.copied === 1 ? "" : "s"} installed)`);
239
+ } else {
240
+ log("skip", `.pi/prompts (already up-to-date)`);
241
+ }
242
+ }
243
+
244
+ console.log("");
245
+
246
+ if (installed === 0) {
247
+ log("warn", "No harness source directories found inside the package.");
248
+ log(
249
+ "warn",
250
+ "The package may be incomplete. Try: npm install --force get-shit-done-cc",
251
+ );
252
+ console.log("");
253
+ return;
254
+ }
255
+
256
+ console.log(` GSD v${getPackageVersion()} installed successfully.`);
257
+ console.log(
258
+ ` ${totalCopied} file${totalCopied === 1 ? "" : "s"} copied, ${totalSkipped} skipped.`,
259
+ );
260
+ console.log("");
261
+ console.log(" Next steps:");
262
+ console.log(" Run /gsd-new-project to initialise a project.");
263
+ console.log("");
264
+ console.log(" Docs: https://github.com/fulgidus/pi-gsd#readme");
265
+ console.log("");
251
266
  }
252
267
 
253
268
  /**
@@ -257,14 +272,14 @@ function main() {
257
272
  * @returns {string}
258
273
  */
259
274
  function getPackageVersion() {
260
- try {
261
- const pkg = JSON.parse(
262
- fs.readFileSync(path.join(PKG_DIR, "package.json"), "utf8"),
263
- );
264
- return pkg.version || "unknown";
265
- } catch {
266
- return "unknown";
267
- }
275
+ try {
276
+ const pkg = JSON.parse(
277
+ fs.readFileSync(path.join(PKG_DIR, "package.json"), "utf8"),
278
+ );
279
+ return pkg.version || "unknown";
280
+ } catch {
281
+ return "unknown";
282
+ }
268
283
  }
269
284
 
270
285
  /**
@@ -282,71 +297,71 @@ function getPackageVersion() {
282
297
  * @param {function} callback Called with (copied: boolean)
283
298
  */
284
299
  function installPiExtension(projectRoot, pkgDir, force, callback) {
285
- const piDir = path.join(projectRoot, ".pi");
286
- const extDir = path.join(piDir, "extensions");
287
- const extDest = path.join(extDir, "gsd-hooks.ts");
288
- const extSrc = path.join(pkgDir, ".gsd", "extensions", "gsd-hooks.ts");
289
-
290
- if (!fs.existsSync(extSrc)) {
291
- log("warn", ".pi/extensions/gsd-hooks.ts (source absent - skipped)");
292
- callback(false);
293
- return;
294
- }
295
-
296
- if (!force && fs.existsSync(extDest)) {
297
- log("skip", ".pi/extensions/gsd-hooks.ts (already exists)");
298
- callback(false);
299
- } else {
300
- try {
301
- fs.mkdirSync(extDir, { recursive: true });
302
- fs.copyFileSync(extSrc, extDest);
303
- log(
304
- "ok",
305
- ".pi/extensions/gsd-hooks.ts (GSD lifecycle extension installed)",
306
- );
307
- callback(true);
308
- } catch (e) {
309
- log(
310
- "warn",
311
- ".pi/extensions/gsd-hooks.ts (install failed: " + e.message + ")",
312
- );
313
- callback(false);
314
- return;
315
- }
316
- }
317
-
318
- // Update .pi/settings.json to include the extension path in the extensions array.
319
- // The file is already auto-discovered from .pi/extensions/, but explicit registration
320
- // is added as a belt-and-suspenders measure.
321
- const settingsFile = path.join(piDir, "settings.json");
322
- try {
323
- let settings = {};
324
- if (fs.existsSync(settingsFile)) {
325
- try {
326
- settings = JSON.parse(fs.readFileSync(settingsFile, "utf8"));
327
- } catch {
328
- // Unreadable settings - start fresh object
329
- }
330
- }
331
-
332
- const extensions = Array.isArray(settings.extensions)
333
- ? settings.extensions
334
- : [];
335
-
336
- // Avoid duplicate entries
337
- if (!extensions.includes(extDest)) {
338
- settings.extensions = [...extensions, extDest];
339
- fs.mkdirSync(piDir, { recursive: true });
340
- fs.writeFileSync(
341
- settingsFile,
342
- JSON.stringify(settings, null, "\t"),
343
- "utf8",
344
- );
345
- log("ok", ".pi/settings.json (extensions array updated)");
346
- }
347
- } catch (e) {
348
- log("warn", ".pi/settings.json (could not update: " + e.message + ")");
349
- }
300
+ const piDir = path.join(projectRoot, ".pi");
301
+ const extDir = path.join(piDir, "extensions");
302
+ const extDest = path.join(extDir, "gsd-hooks.ts");
303
+ const extSrc = path.join(pkgDir, ".gsd", "extensions", "gsd-hooks.ts");
304
+
305
+ if (!fs.existsSync(extSrc)) {
306
+ log("warn", ".pi/extensions/gsd-hooks.ts (source absent - skipped)");
307
+ callback(false);
308
+ return;
309
+ }
310
+
311
+ if (!force && fs.existsSync(extDest)) {
312
+ log("skip", ".pi/extensions/gsd-hooks.ts (already exists)");
313
+ callback(false);
314
+ } else {
315
+ try {
316
+ fs.mkdirSync(extDir, { recursive: true });
317
+ fs.copyFileSync(extSrc, extDest);
318
+ log(
319
+ "ok",
320
+ ".pi/extensions/gsd-hooks.ts (GSD lifecycle extension installed)",
321
+ );
322
+ callback(true);
323
+ } catch (e) {
324
+ log(
325
+ "warn",
326
+ ".pi/extensions/gsd-hooks.ts (install failed: " + e.message + ")",
327
+ );
328
+ callback(false);
329
+ return;
330
+ }
331
+ }
332
+
333
+ // Update .pi/settings.json to include the extension path in the extensions array.
334
+ // The file is already auto-discovered from .pi/extensions/, but explicit registration
335
+ // is added as a belt-and-suspenders measure.
336
+ const settingsFile = path.join(piDir, "settings.json");
337
+ try {
338
+ let settings = {};
339
+ if (fs.existsSync(settingsFile)) {
340
+ try {
341
+ settings = JSON.parse(fs.readFileSync(settingsFile, "utf8"));
342
+ } catch {
343
+ // Unreadable settings - start fresh object
344
+ }
345
+ }
346
+
347
+ const extensions = Array.isArray(settings.extensions)
348
+ ? settings.extensions
349
+ : [];
350
+
351
+ // Avoid duplicate entries
352
+ if (!extensions.includes(extDest)) {
353
+ settings.extensions = [...extensions, extDest];
354
+ fs.mkdirSync(piDir, { recursive: true });
355
+ fs.writeFileSync(
356
+ settingsFile,
357
+ JSON.stringify(settings, null, "\t"),
358
+ "utf8",
359
+ );
360
+ log("ok", ".pi/settings.json (extensions array updated)");
361
+ }
362
+ } catch (e) {
363
+ log("warn", ".pi/settings.json (could not update: " + e.message + ")");
364
+ }
350
365
  }
351
366
 
352
367
  main();