@tekyzinc/gsd-t 2.22.0 → 2.24.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.
- package/CHANGELOG.md +99 -0
- package/README.md +14 -3
- package/bin/gsd-t.js +1381 -1300
- package/commands/gsd-t-complete-milestone.md +12 -12
- package/commands/gsd-t-debug.md +4 -4
- package/commands/gsd-t-discuss.md +7 -9
- package/commands/gsd-t-execute.md +5 -5
- package/commands/gsd-t-feature.md +2 -2
- package/commands/gsd-t-impact.md +9 -3
- package/commands/gsd-t-init.md +12 -12
- package/commands/gsd-t-integrate.md +5 -5
- package/commands/gsd-t-milestone.md +3 -3
- package/commands/gsd-t-partition.md +4 -4
- package/commands/gsd-t-plan.md +6 -6
- package/commands/gsd-t-project.md +3 -3
- package/commands/gsd-t-promote-debt.md +3 -3
- package/commands/gsd-t-qa.md +63 -0
- package/commands/gsd-t-quick.md +4 -4
- package/commands/gsd-t-scan.md +3 -3
- package/commands/gsd-t-test-sync.md +9 -9
- package/commands/gsd-t-verify.md +6 -6
- package/commands/gsd-t-wave.md +193 -137
- package/docs/GSD-T-README.md +12 -0
- package/docs/architecture.md +134 -14
- package/docs/infrastructure.md +33 -11
- package/docs/requirements.md +41 -11
- package/docs/workflows.md +86 -33
- package/package.json +4 -3
- package/scripts/gsd-t-fetch-version.js +25 -0
- package/scripts/gsd-t-heartbeat.js +180 -201
- package/scripts/gsd-t-update-check.js +79 -0
- package/scripts/npm-update-check.js +42 -27
- package/templates/CLAUDE-global.md +10 -3
package/bin/gsd-t.js
CHANGED
|
@@ -1,1300 +1,1381 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* GSD-T CLI Installer
|
|
5
|
-
*
|
|
6
|
-
* Usage:
|
|
7
|
-
* npx @tekyzinc/gsd-t install — Install commands + global CLAUDE.md
|
|
8
|
-
* npx @tekyzinc/gsd-t update — Update commands + global CLAUDE.md (preserves customizations)
|
|
9
|
-
* npx @tekyzinc/gsd-t update-all — Update globally + all registered project CLAUDE.md files
|
|
10
|
-
* npx @tekyzinc/gsd-t init [name] — Initialize a new project with GSD-T structure (auto-registers)
|
|
11
|
-
* npx @tekyzinc/gsd-t register — Register current directory as a GSD-T project
|
|
12
|
-
* npx @tekyzinc/gsd-t status — Show what's installed and check for updates
|
|
13
|
-
* npx @tekyzinc/gsd-t uninstall — Remove GSD-T commands (leaves project files alone)
|
|
14
|
-
* npx @tekyzinc/gsd-t doctor — Diagnose common issues
|
|
15
|
-
* npx @tekyzinc/gsd-t changelog — Open changelog in the browser
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
const fs = require("fs");
|
|
19
|
-
const path = require("path");
|
|
20
|
-
const os = require("os");
|
|
21
|
-
const { execFileSync, spawn: cpSpawn } = require("child_process");
|
|
22
|
-
|
|
23
|
-
// ─── Configuration ───────────────────────────────────────────────────────────
|
|
24
|
-
|
|
25
|
-
const CLAUDE_DIR = path.join(os.homedir(), ".claude");
|
|
26
|
-
const COMMANDS_DIR = path.join(CLAUDE_DIR, "commands");
|
|
27
|
-
const SCRIPTS_DIR = path.join(CLAUDE_DIR, "scripts");
|
|
28
|
-
const GLOBAL_CLAUDE_MD = path.join(CLAUDE_DIR, "CLAUDE.md");
|
|
29
|
-
const SETTINGS_JSON = path.join(CLAUDE_DIR, "settings.json");
|
|
30
|
-
const VERSION_FILE = path.join(CLAUDE_DIR, ".gsd-t-version");
|
|
31
|
-
const PROJECTS_FILE = path.join(CLAUDE_DIR, ".gsd-t-projects");
|
|
32
|
-
const UPDATE_CHECK_FILE = path.join(CLAUDE_DIR, ".gsd-t-update-check");
|
|
33
|
-
|
|
34
|
-
// Where our package files live (relative to this script)
|
|
35
|
-
const PKG_ROOT = path.resolve(__dirname, "..");
|
|
36
|
-
const PKG_COMMANDS = path.join(PKG_ROOT, "commands");
|
|
37
|
-
const PKG_SCRIPTS = path.join(PKG_ROOT, "scripts");
|
|
38
|
-
const PKG_TEMPLATES = path.join(PKG_ROOT, "templates");
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
"",
|
|
48
|
-
"",
|
|
49
|
-
"
|
|
50
|
-
"",
|
|
51
|
-
"
|
|
52
|
-
"",
|
|
53
|
-
"
|
|
54
|
-
"-
|
|
55
|
-
"-
|
|
56
|
-
"-
|
|
57
|
-
"-
|
|
58
|
-
"-
|
|
59
|
-
"-
|
|
60
|
-
"-
|
|
61
|
-
"
|
|
62
|
-
"",
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
const
|
|
72
|
-
const
|
|
73
|
-
const
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
return
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
fs.
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
function
|
|
291
|
-
return getCommandFiles().filter((f) => f.startsWith("gsd-t-"));
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
function
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
return
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
if (
|
|
419
|
-
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
const
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
if (
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
function
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
const
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
log(
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
const
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
const
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
log("");
|
|
967
|
-
|
|
968
|
-
log(
|
|
969
|
-
log(
|
|
970
|
-
log(`
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
const
|
|
1014
|
-
if (
|
|
1015
|
-
success(`
|
|
1016
|
-
} else
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
info(
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
if (
|
|
1069
|
-
success("
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
info("
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
let issues = 0;
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
//
|
|
1148
|
-
const
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
const
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
}
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
log(
|
|
1228
|
-
log(` ${
|
|
1229
|
-
log(` ${
|
|
1230
|
-
log(` ${CYAN}update-
|
|
1231
|
-
log(` ${
|
|
1232
|
-
log(` ${
|
|
1233
|
-
log(` ${
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
}
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* GSD-T CLI Installer
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* npx @tekyzinc/gsd-t install — Install commands + global CLAUDE.md
|
|
8
|
+
* npx @tekyzinc/gsd-t update — Update commands + global CLAUDE.md (preserves customizations)
|
|
9
|
+
* npx @tekyzinc/gsd-t update-all — Update globally + all registered project CLAUDE.md files
|
|
10
|
+
* npx @tekyzinc/gsd-t init [name] — Initialize a new project with GSD-T structure (auto-registers)
|
|
11
|
+
* npx @tekyzinc/gsd-t register — Register current directory as a GSD-T project
|
|
12
|
+
* npx @tekyzinc/gsd-t status — Show what's installed and check for updates
|
|
13
|
+
* npx @tekyzinc/gsd-t uninstall — Remove GSD-T commands (leaves project files alone)
|
|
14
|
+
* npx @tekyzinc/gsd-t doctor — Diagnose common issues
|
|
15
|
+
* npx @tekyzinc/gsd-t changelog — Open changelog in the browser
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const fs = require("fs");
|
|
19
|
+
const path = require("path");
|
|
20
|
+
const os = require("os");
|
|
21
|
+
const { execFileSync, spawn: cpSpawn } = require("child_process");
|
|
22
|
+
|
|
23
|
+
// ─── Configuration ───────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
const CLAUDE_DIR = path.join(os.homedir(), ".claude");
|
|
26
|
+
const COMMANDS_DIR = path.join(CLAUDE_DIR, "commands");
|
|
27
|
+
const SCRIPTS_DIR = path.join(CLAUDE_DIR, "scripts");
|
|
28
|
+
const GLOBAL_CLAUDE_MD = path.join(CLAUDE_DIR, "CLAUDE.md");
|
|
29
|
+
const SETTINGS_JSON = path.join(CLAUDE_DIR, "settings.json");
|
|
30
|
+
const VERSION_FILE = path.join(CLAUDE_DIR, ".gsd-t-version");
|
|
31
|
+
const PROJECTS_FILE = path.join(CLAUDE_DIR, ".gsd-t-projects");
|
|
32
|
+
const UPDATE_CHECK_FILE = path.join(CLAUDE_DIR, ".gsd-t-update-check");
|
|
33
|
+
|
|
34
|
+
// Where our package files live (relative to this script)
|
|
35
|
+
const PKG_ROOT = path.resolve(__dirname, "..");
|
|
36
|
+
const PKG_COMMANDS = path.join(PKG_ROOT, "commands");
|
|
37
|
+
const PKG_SCRIPTS = path.join(PKG_ROOT, "scripts");
|
|
38
|
+
const PKG_TEMPLATES = path.join(PKG_ROOT, "templates");
|
|
39
|
+
|
|
40
|
+
// Read our version from package.json
|
|
41
|
+
const PKG_VERSION = require(path.join(PKG_ROOT, "package.json")).version;
|
|
42
|
+
const CHANGELOG_URL = "https://github.com/Tekyz-Inc/get-stuff-done-teams/blob/main/CHANGELOG.md";
|
|
43
|
+
|
|
44
|
+
// Destructive Action Guard — injected into project CLAUDE.md files by doUpdateAll
|
|
45
|
+
const GUARD_SECTION = [
|
|
46
|
+
"",
|
|
47
|
+
"",
|
|
48
|
+
"# Destructive Action Guard (MANDATORY)",
|
|
49
|
+
"",
|
|
50
|
+
"**NEVER perform destructive or structural changes without explicit user approval.** This applies at ALL autonomy levels.",
|
|
51
|
+
"",
|
|
52
|
+
"Before any of these actions, STOP and ask the user:",
|
|
53
|
+
"- DROP TABLE, DROP COLUMN, DROP INDEX, TRUNCATE, DELETE without WHERE",
|
|
54
|
+
"- Renaming or removing database tables or columns",
|
|
55
|
+
"- Schema migrations that lose data or break existing queries",
|
|
56
|
+
"- Replacing an existing architecture pattern (e.g., normalized → denormalized)",
|
|
57
|
+
"- Removing or replacing existing files/modules that contain working functionality",
|
|
58
|
+
"- Changing ORM models in ways that conflict with the existing database schema",
|
|
59
|
+
"- Removing API endpoints or changing response shapes that existing clients depend on",
|
|
60
|
+
"- Any change that would require other parts of the system to be rewritten",
|
|
61
|
+
"",
|
|
62
|
+
'**Rule: "Adapt new code to existing structures, not the other way around."**',
|
|
63
|
+
"",
|
|
64
|
+
].join("\n");
|
|
65
|
+
|
|
66
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
const BOLD = "\x1b[1m";
|
|
69
|
+
const GREEN = "\x1b[32m";
|
|
70
|
+
const YELLOW = "\x1b[33m";
|
|
71
|
+
const RED = "\x1b[31m";
|
|
72
|
+
const CYAN = "\x1b[36m";
|
|
73
|
+
const DIM = "\x1b[2m";
|
|
74
|
+
const RESET = "\x1b[0m";
|
|
75
|
+
|
|
76
|
+
function log(msg) {
|
|
77
|
+
console.log(msg);
|
|
78
|
+
}
|
|
79
|
+
function success(msg) {
|
|
80
|
+
console.log(`${GREEN} ✓${RESET} ${msg}`);
|
|
81
|
+
}
|
|
82
|
+
function warn(msg) {
|
|
83
|
+
console.log(`${YELLOW} ⚠${RESET} ${msg}`);
|
|
84
|
+
}
|
|
85
|
+
function error(msg) {
|
|
86
|
+
console.log(`${RED} ✗${RESET} ${msg}`);
|
|
87
|
+
}
|
|
88
|
+
function info(msg) {
|
|
89
|
+
console.log(`${CYAN} ℹ${RESET} ${msg}`);
|
|
90
|
+
}
|
|
91
|
+
function heading(msg) {
|
|
92
|
+
console.log(`\n${BOLD}${msg}${RESET}`);
|
|
93
|
+
}
|
|
94
|
+
function link(text, url) {
|
|
95
|
+
return `\x1b]8;;${url}\x07${text}\x1b]8;;\x07`;
|
|
96
|
+
}
|
|
97
|
+
function versionLink(ver) {
|
|
98
|
+
return link(`v${ver || PKG_VERSION}`, CHANGELOG_URL);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function ensureDir(dir) {
|
|
102
|
+
if (hasSymlinkInPath(dir)) {
|
|
103
|
+
warn(`Refusing to use path with symlinked component: ${dir}`);
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
if (!fs.existsSync(dir)) {
|
|
107
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
if (isSymlink(dir)) {
|
|
111
|
+
warn(`Refusing to use symlinked directory: ${dir}`);
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function isSymlink(filePath) {
|
|
118
|
+
try {
|
|
119
|
+
return fs.lstatSync(filePath).isSymbolicLink();
|
|
120
|
+
} catch {
|
|
121
|
+
return false; // File doesn't exist yet — safe to write
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function hasSymlinkInPath(targetPath) {
|
|
126
|
+
const resolved = path.resolve(targetPath);
|
|
127
|
+
let current = path.dirname(resolved);
|
|
128
|
+
const root = path.parse(resolved).root;
|
|
129
|
+
while (current !== root) {
|
|
130
|
+
if (isSymlink(current)) return true;
|
|
131
|
+
const parent = path.dirname(current);
|
|
132
|
+
if (parent === current) break;
|
|
133
|
+
current = parent;
|
|
134
|
+
}
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function validateProjectName(name) {
|
|
139
|
+
return /^[a-zA-Z0-9][a-zA-Z0-9._\- ]{0,100}$/.test(name);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function applyTokens(content, projectName, date) {
|
|
143
|
+
return content.replace(/\{Project Name\}/g, projectName).replace(/\{Date\}/g, date);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function normalizeEol(str) {
|
|
147
|
+
return str.replace(/\r\n/g, "\n");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function validateVersion(ver) {
|
|
151
|
+
return /^\d+\.\d+\.\d+(-[a-zA-Z0-9.]+)?$/.test(ver);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function validateProjectPath(p) {
|
|
155
|
+
try {
|
|
156
|
+
if (!path.isAbsolute(p) || !fs.existsSync(p)) return false;
|
|
157
|
+
const stat = fs.statSync(p);
|
|
158
|
+
if (!stat.isDirectory()) return false;
|
|
159
|
+
// On Unix, verify directory is owned by current user (defense-in-depth)
|
|
160
|
+
if (typeof process.getuid === "function" && stat.uid !== process.getuid()) return false;
|
|
161
|
+
return true;
|
|
162
|
+
} catch {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function copyFile(src, dest, label) {
|
|
168
|
+
if (isSymlink(dest)) {
|
|
169
|
+
warn(`Skipping symlink target: ${dest}`);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
fs.copyFileSync(src, dest);
|
|
174
|
+
success(label || path.basename(dest));
|
|
175
|
+
} catch (e) {
|
|
176
|
+
error(`Failed to copy ${label || path.basename(dest)}: ${e.message}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function hasPlaywright(projectDir) {
|
|
181
|
+
const configs = ["playwright.config.ts", "playwright.config.js", "playwright.config.mjs"];
|
|
182
|
+
return configs.some((f) => fs.existsSync(path.join(projectDir, f)));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function readProjectDeps(projectDir) {
|
|
186
|
+
const pkgPath = path.join(projectDir, "package.json");
|
|
187
|
+
if (!fs.existsSync(pkgPath)) return [];
|
|
188
|
+
try {
|
|
189
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
190
|
+
return Object.keys(pkg.dependencies || {}).concat(Object.keys(pkg.devDependencies || {}));
|
|
191
|
+
} catch { return []; }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function readPyContent(projectDir, filename) {
|
|
195
|
+
const fp = path.join(projectDir, filename);
|
|
196
|
+
if (!fs.existsSync(fp)) return "";
|
|
197
|
+
try { return fs.readFileSync(fp, "utf8"); } catch { return ""; }
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function hasSwagger(projectDir) {
|
|
201
|
+
const specFiles = ["swagger.json", "swagger.yaml", "swagger.yml", "openapi.json", "openapi.yaml", "openapi.yml"];
|
|
202
|
+
if (specFiles.some((f) => fs.existsSync(path.join(projectDir, f)))) return true;
|
|
203
|
+
|
|
204
|
+
const swaggerPkgs = ["swagger-jsdoc", "swagger-ui-express", "@fastify/swagger", "@nestjs/swagger", "swagger-ui", "express-openapi-validator"];
|
|
205
|
+
if (swaggerPkgs.some((p) => readProjectDeps(projectDir).includes(p))) return true;
|
|
206
|
+
|
|
207
|
+
for (const f of ["requirements.txt", "pyproject.toml"]) {
|
|
208
|
+
if (readPyContent(projectDir, f).includes("fastapi")) return true;
|
|
209
|
+
}
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function hasApi(projectDir) {
|
|
214
|
+
const apiFrameworks = ["express", "fastify", "hono", "koa", "hapi", "@nestjs/core", "next"];
|
|
215
|
+
if (apiFrameworks.some((p) => readProjectDeps(projectDir).includes(p))) return true;
|
|
216
|
+
|
|
217
|
+
for (const f of ["requirements.txt", "pyproject.toml"]) {
|
|
218
|
+
const content = readPyContent(projectDir, f);
|
|
219
|
+
if (content.includes("fastapi") || content.includes("flask") || content.includes("django")) return true;
|
|
220
|
+
}
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function getInstalledVersion() {
|
|
225
|
+
try {
|
|
226
|
+
return fs.readFileSync(VERSION_FILE, "utf8").trim();
|
|
227
|
+
} catch {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function saveInstalledVersion() {
|
|
233
|
+
if (isSymlink(VERSION_FILE)) {
|
|
234
|
+
warn("Skipping version write — target is a symlink");
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
try {
|
|
238
|
+
fs.writeFileSync(VERSION_FILE, PKG_VERSION);
|
|
239
|
+
} catch (e) {
|
|
240
|
+
error(`Failed to save version file: ${e.message}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function getRegisteredProjects() {
|
|
245
|
+
try {
|
|
246
|
+
const content = fs.readFileSync(PROJECTS_FILE, "utf8").trim();
|
|
247
|
+
if (!content) return [];
|
|
248
|
+
const lines = content.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
|
|
249
|
+
return lines.filter((p) => {
|
|
250
|
+
if (!validateProjectPath(p)) {
|
|
251
|
+
warn(`Skipping invalid project path: ${p}`);
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
return true;
|
|
255
|
+
});
|
|
256
|
+
} catch {
|
|
257
|
+
return [];
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function registerProject(projectDir) {
|
|
262
|
+
const resolved = path.resolve(projectDir);
|
|
263
|
+
const projects = getRegisteredProjects();
|
|
264
|
+
if (projects.includes(resolved)) return false;
|
|
265
|
+
if (isSymlink(PROJECTS_FILE)) {
|
|
266
|
+
warn("Skipping project registration — target is a symlink");
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
try {
|
|
270
|
+
projects.push(resolved);
|
|
271
|
+
fs.writeFileSync(PROJECTS_FILE, projects.join("\n") + "\n");
|
|
272
|
+
return true;
|
|
273
|
+
} catch (e) {
|
|
274
|
+
error(`Failed to register project: ${e.message}`);
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function getCommandFiles() {
|
|
280
|
+
// All .md files in our commands/ directory (gsd-t-* plus utilities like branch, checkin, Claude-md)
|
|
281
|
+
return fs
|
|
282
|
+
.readdirSync(PKG_COMMANDS)
|
|
283
|
+
.filter((f) => f.endsWith(".md"));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function getGsdtCommands() {
|
|
287
|
+
return getCommandFiles().filter((f) => f.startsWith("gsd-t-"));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function getUtilityCommands() {
|
|
291
|
+
return getCommandFiles().filter((f) => !f.startsWith("gsd-t-"));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function getInstalledCommands() {
|
|
295
|
+
try {
|
|
296
|
+
const ourCommands = getCommandFiles();
|
|
297
|
+
return fs
|
|
298
|
+
.readdirSync(COMMANDS_DIR)
|
|
299
|
+
.filter((f) => ourCommands.includes(f));
|
|
300
|
+
} catch {
|
|
301
|
+
return [];
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ─── Heartbeat ──────────────────────────────────────────────────────────────
|
|
306
|
+
|
|
307
|
+
const HEARTBEAT_SCRIPT = "gsd-t-heartbeat.js";
|
|
308
|
+
const HEARTBEAT_HOOKS = [
|
|
309
|
+
"SessionStart", "PostToolUse", "SubagentStart", "SubagentStop",
|
|
310
|
+
"TaskCompleted", "TeammateIdle", "Notification", "Stop", "SessionEnd"
|
|
311
|
+
];
|
|
312
|
+
|
|
313
|
+
function installHeartbeat() {
|
|
314
|
+
ensureDir(SCRIPTS_DIR);
|
|
315
|
+
|
|
316
|
+
// Copy heartbeat script
|
|
317
|
+
const src = path.join(PKG_SCRIPTS, HEARTBEAT_SCRIPT);
|
|
318
|
+
const dest = path.join(SCRIPTS_DIR, HEARTBEAT_SCRIPT);
|
|
319
|
+
|
|
320
|
+
if (!fs.existsSync(src)) {
|
|
321
|
+
warn("Heartbeat script not found in package — skipping");
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const srcContent = fs.readFileSync(src, "utf8");
|
|
326
|
+
const destContent = fs.existsSync(dest) ? fs.readFileSync(dest, "utf8") : "";
|
|
327
|
+
|
|
328
|
+
if (normalizeEol(srcContent) !== normalizeEol(destContent)) {
|
|
329
|
+
copyFile(src, dest, HEARTBEAT_SCRIPT);
|
|
330
|
+
} else {
|
|
331
|
+
info("Heartbeat script unchanged");
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Configure hooks in settings.json
|
|
335
|
+
const hooksAdded = configureHeartbeatHooks(dest);
|
|
336
|
+
if (hooksAdded > 0) {
|
|
337
|
+
success(`${hooksAdded} heartbeat hooks configured in settings.json`);
|
|
338
|
+
} else {
|
|
339
|
+
info("Heartbeat hooks already configured");
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function configureHeartbeatHooks(scriptPath) {
|
|
344
|
+
const parsed = readSettingsJson();
|
|
345
|
+
if (parsed === null && fs.existsSync(SETTINGS_JSON)) {
|
|
346
|
+
warn("settings.json has invalid JSON — cannot configure hooks");
|
|
347
|
+
return 0;
|
|
348
|
+
}
|
|
349
|
+
const settings = parsed || {};
|
|
350
|
+
|
|
351
|
+
if (!settings.hooks) settings.hooks = {};
|
|
352
|
+
const cmd = `node "${scriptPath.replace(/\\/g, "\\\\")}"`;
|
|
353
|
+
let added = 0;
|
|
354
|
+
|
|
355
|
+
for (const event of HEARTBEAT_HOOKS) {
|
|
356
|
+
if (addHeartbeatHook(settings.hooks, event, cmd)) added++;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (added > 0 && !isSymlink(SETTINGS_JSON)) {
|
|
360
|
+
fs.writeFileSync(SETTINGS_JSON, JSON.stringify(settings, null, 2));
|
|
361
|
+
} else if (added > 0) {
|
|
362
|
+
warn("Skipping settings.json write — target is a symlink");
|
|
363
|
+
}
|
|
364
|
+
return added;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function addHeartbeatHook(hooks, event, cmd) {
|
|
368
|
+
if (!hooks[event]) hooks[event] = [];
|
|
369
|
+
const hasHeartbeat = hooks[event].some((entry) =>
|
|
370
|
+
entry.hooks && entry.hooks.some((h) => h.command && h.command.includes(HEARTBEAT_SCRIPT))
|
|
371
|
+
);
|
|
372
|
+
if (hasHeartbeat) return false;
|
|
373
|
+
hooks[event].push({ matcher: "", hooks: [{ type: "command", command: cmd, async: true }] });
|
|
374
|
+
return true;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ─── Update Check Hook ──────────────────────────────────────────────────────
|
|
378
|
+
|
|
379
|
+
const UPDATE_CHECK_SCRIPT = "gsd-t-update-check.js";
|
|
380
|
+
|
|
381
|
+
function installUpdateCheck() {
|
|
382
|
+
ensureDir(SCRIPTS_DIR);
|
|
383
|
+
|
|
384
|
+
// Copy update check script
|
|
385
|
+
const src = path.join(PKG_SCRIPTS, UPDATE_CHECK_SCRIPT);
|
|
386
|
+
const dest = path.join(SCRIPTS_DIR, UPDATE_CHECK_SCRIPT);
|
|
387
|
+
|
|
388
|
+
if (!fs.existsSync(src)) {
|
|
389
|
+
warn("Update check script not found in package — skipping");
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const srcContent = fs.readFileSync(src, "utf8");
|
|
394
|
+
const destContent = fs.existsSync(dest) ? fs.readFileSync(dest, "utf8") : "";
|
|
395
|
+
|
|
396
|
+
if (normalizeEol(srcContent) !== normalizeEol(destContent)) {
|
|
397
|
+
copyFile(src, dest, UPDATE_CHECK_SCRIPT);
|
|
398
|
+
} else {
|
|
399
|
+
info("Update check script unchanged");
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Configure SessionStart hook in settings.json
|
|
403
|
+
configureUpdateCheckHook(dest);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function configureUpdateCheckHook(scriptPath) {
|
|
407
|
+
let settings = {};
|
|
408
|
+
if (fs.existsSync(SETTINGS_JSON)) {
|
|
409
|
+
try {
|
|
410
|
+
settings = JSON.parse(fs.readFileSync(SETTINGS_JSON, "utf8"));
|
|
411
|
+
} catch {
|
|
412
|
+
warn("settings.json has invalid JSON — cannot configure update check hook");
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (!settings.hooks) settings.hooks = {};
|
|
418
|
+
if (!settings.hooks.SessionStart) settings.hooks.SessionStart = [];
|
|
419
|
+
|
|
420
|
+
const cmd = `node "${scriptPath.replace(/\\/g, "\\\\")}"`;
|
|
421
|
+
|
|
422
|
+
// Check if update check hook already exists
|
|
423
|
+
const hasUpdateCheck = settings.hooks.SessionStart.some((entry) =>
|
|
424
|
+
entry.hooks && entry.hooks.some((h) => h.command && h.command.includes(UPDATE_CHECK_SCRIPT))
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
if (hasUpdateCheck) {
|
|
428
|
+
// Fix matcher if it's not empty string (bug fix — "startup" doesn't match all sessions)
|
|
429
|
+
let fixed = false;
|
|
430
|
+
for (const entry of settings.hooks.SessionStart) {
|
|
431
|
+
if (entry.hooks && entry.hooks.some((h) => h.command && h.command.includes(UPDATE_CHECK_SCRIPT))) {
|
|
432
|
+
if (entry.matcher !== "") {
|
|
433
|
+
entry.matcher = "";
|
|
434
|
+
fixed = true;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
if (fixed) {
|
|
439
|
+
if (!isSymlink(SETTINGS_JSON)) {
|
|
440
|
+
fs.writeFileSync(SETTINGS_JSON, JSON.stringify(settings, null, 2));
|
|
441
|
+
}
|
|
442
|
+
success("Fixed update check hook matcher");
|
|
443
|
+
} else {
|
|
444
|
+
info("Update check hook already configured");
|
|
445
|
+
}
|
|
446
|
+
} else {
|
|
447
|
+
// Add new hook — synchronous (not async) so output is available before Claude responds
|
|
448
|
+
settings.hooks.SessionStart.unshift({
|
|
449
|
+
matcher: "",
|
|
450
|
+
hooks: [{ type: "command", command: cmd }],
|
|
451
|
+
});
|
|
452
|
+
if (!isSymlink(SETTINGS_JSON)) {
|
|
453
|
+
fs.writeFileSync(SETTINGS_JSON, JSON.stringify(settings, null, 2));
|
|
454
|
+
}
|
|
455
|
+
success("Update check hook configured");
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// ─── Commands ────────────────────────────────────────────────────────────────
|
|
460
|
+
|
|
461
|
+
function installCommands(isUpdate) {
|
|
462
|
+
heading("Slash Commands");
|
|
463
|
+
const commandFiles = getCommandFiles();
|
|
464
|
+
const gsdtCommands = getGsdtCommands();
|
|
465
|
+
const utilityCommands = getUtilityCommands();
|
|
466
|
+
let installed = 0, skipped = 0;
|
|
467
|
+
|
|
468
|
+
for (const file of commandFiles) {
|
|
469
|
+
const src = path.join(PKG_COMMANDS, file);
|
|
470
|
+
const dest = path.join(COMMANDS_DIR, file);
|
|
471
|
+
if (isUpdate && fs.existsSync(dest)) {
|
|
472
|
+
if (normalizeEol(fs.readFileSync(src, "utf8")) === normalizeEol(fs.readFileSync(dest, "utf8"))) {
|
|
473
|
+
skipped++;
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
copyFile(src, dest, file);
|
|
478
|
+
installed++;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (skipped > 0) info(`${skipped} commands unchanged`);
|
|
482
|
+
success(`${gsdtCommands.length} GSD-T commands + ${utilityCommands.length} utilities ${isUpdate ? "updated" : "installed"} → ~/.claude/commands/`);
|
|
483
|
+
return { gsdtCommands, utilityCommands };
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function installGlobalClaudeMd(isUpdate) {
|
|
487
|
+
heading("Global CLAUDE.md");
|
|
488
|
+
const globalSrc = path.join(PKG_TEMPLATES, "CLAUDE-global.md");
|
|
489
|
+
|
|
490
|
+
if (!fs.existsSync(GLOBAL_CLAUDE_MD)) {
|
|
491
|
+
copyFile(globalSrc, GLOBAL_CLAUDE_MD, "CLAUDE.md installed → ~/.claude/CLAUDE.md");
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const existing = fs.readFileSync(GLOBAL_CLAUDE_MD, "utf8");
|
|
496
|
+
if (existing.includes("GSD-T: Contract-Driven Development")) {
|
|
497
|
+
updateExistingGlobalClaudeMd(globalSrc, existing, isUpdate);
|
|
498
|
+
} else {
|
|
499
|
+
appendGsdtToClaudeMd(globalSrc);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function updateExistingGlobalClaudeMd(globalSrc, existing, isUpdate) {
|
|
504
|
+
if (!isUpdate) {
|
|
505
|
+
info("CLAUDE.md already contains GSD-T config — skipping");
|
|
506
|
+
info("Run 'gsd-t update' to overwrite with latest version");
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
const template = fs.readFileSync(globalSrc, "utf8");
|
|
510
|
+
if (normalizeEol(existing) === normalizeEol(template)) {
|
|
511
|
+
copyFile(globalSrc, GLOBAL_CLAUDE_MD, "CLAUDE.md updated (no customizations detected)");
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
const backupPath = GLOBAL_CLAUDE_MD + ".backup-" + Date.now();
|
|
515
|
+
if (!isSymlink(backupPath)) fs.copyFileSync(GLOBAL_CLAUDE_MD, backupPath);
|
|
516
|
+
else warn("Skipping backup — target is a symlink");
|
|
517
|
+
copyFile(globalSrc, GLOBAL_CLAUDE_MD, "CLAUDE.md updated");
|
|
518
|
+
warn(`Previous version backed up to ${path.basename(backupPath)}`);
|
|
519
|
+
info("Review the backup if you had custom additions to merge back in.");
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function appendGsdtToClaudeMd(globalSrc) {
|
|
523
|
+
if (isSymlink(GLOBAL_CLAUDE_MD)) { warn("Skipping CLAUDE.md append — target is a symlink"); return; }
|
|
524
|
+
const gsdtContent = fs.readFileSync(globalSrc, "utf8");
|
|
525
|
+
const separator = "\n\n# ─── GSD-T Section (added by installer) ───\n\n";
|
|
526
|
+
fs.appendFileSync(GLOBAL_CLAUDE_MD, separator + gsdtContent);
|
|
527
|
+
success("GSD-T config appended to existing CLAUDE.md");
|
|
528
|
+
info("Your existing content was preserved.");
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function doInstall(opts = {}) {
|
|
532
|
+
const isUpdate = opts.update || false;
|
|
533
|
+
heading(`${isUpdate ? "Updating" : "Installing"} GSD-T ${versionLink()}`);
|
|
534
|
+
log("");
|
|
535
|
+
|
|
536
|
+
if (ensureDir(COMMANDS_DIR)) success("Created ~/.claude/commands/");
|
|
537
|
+
|
|
538
|
+
const { gsdtCommands, utilityCommands } = installCommands(isUpdate);
|
|
539
|
+
installGlobalClaudeMd(isUpdate);
|
|
540
|
+
|
|
541
|
+
heading("Heartbeat (Real-time Events)");
|
|
542
|
+
installHeartbeat();
|
|
543
|
+
|
|
544
|
+
heading("Update Check (Session Start)");
|
|
545
|
+
installUpdateCheck();
|
|
546
|
+
saveInstalledVersion();
|
|
547
|
+
|
|
548
|
+
showInstallSummary(gsdtCommands.length, utilityCommands.length);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function showInstallSummary(gsdtCount, utilCount) {
|
|
552
|
+
heading("Installation Complete!");
|
|
553
|
+
log("");
|
|
554
|
+
log(` Commands: ${gsdtCount} GSD-T + ${utilCount} utility commands in ~/.claude/commands/`);
|
|
555
|
+
log(` Config: ~/.claude/CLAUDE.md`);
|
|
556
|
+
log(` Version: ${versionLink()}`);
|
|
557
|
+
log("");
|
|
558
|
+
log(`${BOLD}Quick Start:${RESET}`);
|
|
559
|
+
log(` ${DIM}$${RESET} cd your-project`);
|
|
560
|
+
log(` ${DIM}$${RESET} claude`);
|
|
561
|
+
log(` ${DIM}>${RESET} /user:gsd-t-init my-project`);
|
|
562
|
+
log(` ${DIM}>${RESET} /user:gsd-t-milestone "First Feature"`);
|
|
563
|
+
log(` ${DIM}>${RESET} /user:gsd-t-wave`);
|
|
564
|
+
log("");
|
|
565
|
+
log(`${BOLD}Other commands:${RESET}`);
|
|
566
|
+
log(` ${DIM}$${RESET} npx @tekyzinc/gsd-t status ${DIM}— check installation${RESET}`);
|
|
567
|
+
log(` ${DIM}$${RESET} npx @tekyzinc/gsd-t update ${DIM}— update to latest${RESET}`);
|
|
568
|
+
log(` ${DIM}$${RESET} npx @tekyzinc/gsd-t init myapp ${DIM}— scaffold a new project${RESET}`);
|
|
569
|
+
log(` ${DIM}$${RESET} npx @tekyzinc/gsd-t doctor ${DIM}— diagnose issues${RESET}`);
|
|
570
|
+
log("");
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function doUpdate() {
|
|
574
|
+
const installedVersion = getInstalledVersion();
|
|
575
|
+
|
|
576
|
+
if (installedVersion === PKG_VERSION) {
|
|
577
|
+
heading(`GSD-T ${versionLink()}`);
|
|
578
|
+
info("Already up to date!");
|
|
579
|
+
log("");
|
|
580
|
+
log(" To force a reinstall, run:");
|
|
581
|
+
log(` ${DIM}$${RESET} npx @tekyzinc/gsd-t install`);
|
|
582
|
+
log("");
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
if (installedVersion) {
|
|
587
|
+
heading(`Updating GSD-T: ${versionLink(installedVersion)} → ${versionLink()}`);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
doInstall({ update: true });
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function initClaudeMd(projectDir, projectName, today) {
|
|
594
|
+
const claudeMdPath = path.join(projectDir, "CLAUDE.md");
|
|
595
|
+
if (isSymlink(claudeMdPath)) {
|
|
596
|
+
warn("Skipping CLAUDE.md — target is a symlink");
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
try {
|
|
600
|
+
const template = fs.readFileSync(path.join(PKG_TEMPLATES, "CLAUDE-project.md"), "utf8");
|
|
601
|
+
const content = applyTokens(template, projectName, today);
|
|
602
|
+
fs.writeFileSync(claudeMdPath, content, { flag: "wx" });
|
|
603
|
+
success("CLAUDE.md created");
|
|
604
|
+
} catch (e) {
|
|
605
|
+
if (e.code === "EEXIST") {
|
|
606
|
+
const content = fs.readFileSync(claudeMdPath, "utf8");
|
|
607
|
+
if (content.includes("GSD-T Workflow")) {
|
|
608
|
+
info("CLAUDE.md already contains GSD-T section — skipping");
|
|
609
|
+
} else {
|
|
610
|
+
warn("CLAUDE.md exists but doesn't reference GSD-T");
|
|
611
|
+
info("Run /user:gsd-t-init inside Claude Code to add GSD-T section");
|
|
612
|
+
}
|
|
613
|
+
} else { throw e; }
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function initDocs(projectDir, projectName, today) {
|
|
618
|
+
const docsDir = path.join(projectDir, "docs");
|
|
619
|
+
ensureDir(docsDir);
|
|
620
|
+
|
|
621
|
+
const docTemplates = ["requirements.md", "architecture.md", "workflows.md", "infrastructure.md"];
|
|
622
|
+
for (const file of docTemplates) {
|
|
623
|
+
const destPath = path.join(docsDir, file);
|
|
624
|
+
if (isSymlink(destPath)) {
|
|
625
|
+
warn(`Skipping docs/${file} — target is a symlink`);
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
try {
|
|
629
|
+
const template = fs.readFileSync(path.join(PKG_TEMPLATES, file), "utf8");
|
|
630
|
+
const content = applyTokens(template, projectName, today);
|
|
631
|
+
fs.writeFileSync(destPath, content, { flag: "wx" });
|
|
632
|
+
success(`docs/${file}`);
|
|
633
|
+
} catch (e) {
|
|
634
|
+
if (e.code === "EEXIST") { info(`docs/${file} already exists — skipping`); }
|
|
635
|
+
else { throw e; }
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function initGsdtDir(projectDir, projectName, today) {
|
|
641
|
+
const gsdtDir = path.join(projectDir, ".gsd-t");
|
|
642
|
+
const contractsDir = path.join(gsdtDir, "contracts");
|
|
643
|
+
const domainsDir = path.join(gsdtDir, "domains");
|
|
644
|
+
|
|
645
|
+
ensureDir(contractsDir);
|
|
646
|
+
ensureDir(domainsDir);
|
|
647
|
+
|
|
648
|
+
for (const dir of [contractsDir, domainsDir]) {
|
|
649
|
+
const gitkeep = path.join(dir, ".gitkeep");
|
|
650
|
+
if (isSymlink(gitkeep)) continue;
|
|
651
|
+
try { fs.writeFileSync(gitkeep, "", { flag: "wx" }); }
|
|
652
|
+
catch (e) { if (e.code !== "EEXIST") throw e; }
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
writeTemplateFile("progress.md", path.join(gsdtDir, "progress.md"), ".gsd-t/progress.md", projectName, today);
|
|
656
|
+
writeTemplateFile("backlog.md", path.join(gsdtDir, "backlog.md"), ".gsd-t/backlog.md", projectName, today);
|
|
657
|
+
writeTemplateFile("backlog-settings.md", path.join(gsdtDir, "backlog-settings.md"), ".gsd-t/backlog-settings.md", projectName, today);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function writeTemplateFile(templateName, destPath, label, projectName, today) {
|
|
661
|
+
if (isSymlink(destPath)) { warn(`Skipping ${label} — target is a symlink`); return; }
|
|
662
|
+
try {
|
|
663
|
+
const template = fs.readFileSync(path.join(PKG_TEMPLATES, templateName), "utf8");
|
|
664
|
+
const content = projectName ? applyTokens(template, projectName, today) : template;
|
|
665
|
+
fs.writeFileSync(destPath, content, { flag: "wx" });
|
|
666
|
+
success(label);
|
|
667
|
+
} catch (e) {
|
|
668
|
+
if (e.code === "EEXIST") { info(`${label} already exists — skipping`); }
|
|
669
|
+
else { throw e; }
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function doInit(projectName) {
|
|
674
|
+
if (!projectName) projectName = path.basename(process.cwd());
|
|
675
|
+
|
|
676
|
+
if (!validateProjectName(projectName)) {
|
|
677
|
+
error(`Invalid project name: "${projectName}"`);
|
|
678
|
+
info("Project names must start with a letter or number and contain only letters, numbers, dots, hyphens, underscores, or spaces (max 101 chars)");
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
heading(`Initializing GSD-T project: ${projectName}`);
|
|
683
|
+
log("");
|
|
684
|
+
|
|
685
|
+
const projectDir = process.cwd();
|
|
686
|
+
const today = new Date().toISOString().split("T")[0];
|
|
687
|
+
|
|
688
|
+
initClaudeMd(projectDir, projectName, today);
|
|
689
|
+
initDocs(projectDir, projectName, today);
|
|
690
|
+
initGsdtDir(projectDir, projectName, today);
|
|
691
|
+
|
|
692
|
+
if (registerProject(projectDir)) success("Registered in ~/.claude/.gsd-t-projects");
|
|
693
|
+
|
|
694
|
+
showInitTree(projectDir);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function showInitTree(projectDir) {
|
|
698
|
+
heading("Project Initialized!");
|
|
699
|
+
log("");
|
|
700
|
+
log(` ${projectDir}/`);
|
|
701
|
+
log(" ├── CLAUDE.md");
|
|
702
|
+
log(" ├── docs/");
|
|
703
|
+
log(" │ ├── requirements.md");
|
|
704
|
+
log(" │ ├── architecture.md");
|
|
705
|
+
log(" │ ├── workflows.md");
|
|
706
|
+
log(" │ └── infrastructure.md");
|
|
707
|
+
log(" └── .gsd-t/");
|
|
708
|
+
log(" ├── progress.md");
|
|
709
|
+
log(" ├── backlog.md");
|
|
710
|
+
log(" ├── backlog-settings.md");
|
|
711
|
+
log(" ├── contracts/");
|
|
712
|
+
log(" └── domains/");
|
|
713
|
+
log("");
|
|
714
|
+
log(`${BOLD}Next steps:${RESET}`);
|
|
715
|
+
log(` 1. Edit CLAUDE.md — add project overview and tech stack`);
|
|
716
|
+
log(` 2. Start Claude Code: ${DIM}claude${RESET}`);
|
|
717
|
+
log(` 3. Run: ${DIM}/user:gsd-t-populate${RESET} ${DIM}(if existing codebase)${RESET}`);
|
|
718
|
+
log(` Or: ${DIM}/user:gsd-t-project${RESET} ${DIM}(if new project)${RESET}`);
|
|
719
|
+
log("");
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function doStatus() {
|
|
723
|
+
heading("GSD-T Status");
|
|
724
|
+
log("");
|
|
725
|
+
if (!showStatusVersion()) return;
|
|
726
|
+
showStatusCommands();
|
|
727
|
+
showStatusConfig();
|
|
728
|
+
showStatusTeams();
|
|
729
|
+
showStatusProject();
|
|
730
|
+
log("");
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function showStatusVersion() {
|
|
734
|
+
const installedVersion = getInstalledVersion();
|
|
735
|
+
if (installedVersion) {
|
|
736
|
+
success(`Installed version: ${versionLink(installedVersion)}`);
|
|
737
|
+
if (installedVersion !== PKG_VERSION) {
|
|
738
|
+
warn(`Latest version: ${versionLink()}`);
|
|
739
|
+
info(`Run 'npx @tekyzinc/gsd-t update' to update`);
|
|
740
|
+
} else {
|
|
741
|
+
success(`Up to date (latest: ${versionLink()})`);
|
|
742
|
+
}
|
|
743
|
+
return true;
|
|
744
|
+
}
|
|
745
|
+
error("GSD-T not installed");
|
|
746
|
+
info("Run 'npx @tekyzinc/gsd-t install' to install");
|
|
747
|
+
return false;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function showStatusCommands() {
|
|
751
|
+
heading("Slash Commands");
|
|
752
|
+
const expected = getCommandFiles();
|
|
753
|
+
const installed = getInstalledCommands();
|
|
754
|
+
const missing = expected.filter((f) => !installed.includes(f));
|
|
755
|
+
const extra = installed.filter((f) => !expected.includes(f));
|
|
756
|
+
const present = expected.filter((f) => installed.includes(f));
|
|
757
|
+
log(` ${present.length}/${expected.length} commands installed (${getGsdtCommands().length} GSD-T + ${getUtilityCommands().length} utilities)`);
|
|
758
|
+
if (missing.length > 0) warn(`Missing: ${missing.join(", ")}`);
|
|
759
|
+
if (extra.length > 0) info(`Custom commands found: ${extra.join(", ")}`);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function showStatusConfig() {
|
|
763
|
+
heading("Global Config");
|
|
764
|
+
if (fs.existsSync(GLOBAL_CLAUDE_MD)) {
|
|
765
|
+
const content = fs.readFileSync(GLOBAL_CLAUDE_MD, "utf8");
|
|
766
|
+
if (content.includes("GSD-T: Contract-Driven Development")) {
|
|
767
|
+
success("~/.claude/CLAUDE.md contains GSD-T config");
|
|
768
|
+
} else {
|
|
769
|
+
warn("~/.claude/CLAUDE.md exists but doesn't contain GSD-T section");
|
|
770
|
+
}
|
|
771
|
+
} else {
|
|
772
|
+
error("~/.claude/CLAUDE.md not found");
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function showStatusTeams() {
|
|
777
|
+
heading("Agent Teams");
|
|
778
|
+
if (!fs.existsSync(SETTINGS_JSON)) {
|
|
779
|
+
info("No settings.json found (Claude Code will use defaults)");
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
const settings = readSettingsJson();
|
|
783
|
+
if (settings === null) {
|
|
784
|
+
warn("settings.json exists but couldn't be parsed");
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
const teamsEnabled = settings?.env?.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS === "1";
|
|
788
|
+
if (teamsEnabled) {
|
|
789
|
+
success("Agent Teams enabled in settings.json");
|
|
790
|
+
} else {
|
|
791
|
+
info("Agent Teams not enabled (optional — solo mode works fine)");
|
|
792
|
+
info('Add "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1" to env in settings.json');
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function showStatusProject() {
|
|
797
|
+
heading("Current Project");
|
|
798
|
+
const cwd = process.cwd();
|
|
799
|
+
const hasGsdT = fs.existsSync(path.join(cwd, ".gsd-t"));
|
|
800
|
+
const hasClaudeMd = fs.existsSync(path.join(cwd, "CLAUDE.md"));
|
|
801
|
+
|
|
802
|
+
if (hasGsdT) {
|
|
803
|
+
success(`.gsd-t/ found in ${cwd}`);
|
|
804
|
+
const progressPath = path.join(cwd, ".gsd-t", "progress.md");
|
|
805
|
+
if (fs.existsSync(progressPath)) {
|
|
806
|
+
const progress = fs.readFileSync(progressPath, "utf8");
|
|
807
|
+
const statusMatch = progress.match(/## Status:\s*(.+)/);
|
|
808
|
+
const milestoneMatch = progress.match(/## Project:\s*(.+)/);
|
|
809
|
+
if (milestoneMatch) info(`Project: ${milestoneMatch[1]}`);
|
|
810
|
+
if (statusMatch) info(`Status: ${statusMatch[1]}`);
|
|
811
|
+
}
|
|
812
|
+
} else if (hasClaudeMd) {
|
|
813
|
+
info("CLAUDE.md found but no .gsd-t/ directory");
|
|
814
|
+
info("Run /user:gsd-t-init inside Claude Code to set up");
|
|
815
|
+
} else {
|
|
816
|
+
info("Not in a GSD-T project directory");
|
|
817
|
+
info(`Run 'npx @tekyzinc/gsd-t init' to set up this directory`);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function doUninstall() {
|
|
822
|
+
heading("Uninstalling GSD-T");
|
|
823
|
+
log("");
|
|
824
|
+
|
|
825
|
+
removeInstalledCommands();
|
|
826
|
+
removeVersionFile();
|
|
827
|
+
|
|
828
|
+
warn("~/.claude/CLAUDE.md was NOT removed (may contain your customizations)");
|
|
829
|
+
info("Remove manually if desired: delete the GSD-T section from ~/.claude/CLAUDE.md");
|
|
830
|
+
info("Project files (.gsd-t/, docs/, CLAUDE.md) were NOT removed");
|
|
831
|
+
|
|
832
|
+
heading("Uninstall Complete");
|
|
833
|
+
log("");
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function removeInstalledCommands() {
|
|
837
|
+
const commands = getInstalledCommands();
|
|
838
|
+
let removed = 0;
|
|
839
|
+
for (const file of commands) {
|
|
840
|
+
const fp = path.join(COMMANDS_DIR, file);
|
|
841
|
+
if (isSymlink(fp)) { warn(`Skipping symlink: ${file}`); continue; }
|
|
842
|
+
try { fs.unlinkSync(fp); removed++; }
|
|
843
|
+
catch (e) { error(`Failed to remove ${file}: ${e.message}`); }
|
|
844
|
+
}
|
|
845
|
+
if (removed > 0) success(`Removed ${removed} slash commands from ~/.claude/commands/`);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
function removeVersionFile() {
|
|
849
|
+
try {
|
|
850
|
+
if (fs.existsSync(VERSION_FILE) && !isSymlink(VERSION_FILE)) fs.unlinkSync(VERSION_FILE);
|
|
851
|
+
} catch (e) {
|
|
852
|
+
error(`Failed to remove version file: ${e.message}`);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
function updateProjectClaudeMd(claudeMd, projectName) {
|
|
857
|
+
const content = fs.readFileSync(claudeMd, "utf8");
|
|
858
|
+
if (content.includes("Destructive Action Guard")) return false;
|
|
859
|
+
|
|
860
|
+
const newContent = insertGuardSection(content);
|
|
861
|
+
if (isSymlink(claudeMd)) { warn(`${projectName} — skipping CLAUDE.md write (symlink)`); return false; }
|
|
862
|
+
try {
|
|
863
|
+
fs.writeFileSync(claudeMd, newContent);
|
|
864
|
+
success(`${projectName} — added Destructive Action Guard`);
|
|
865
|
+
return true;
|
|
866
|
+
} catch (e) {
|
|
867
|
+
error(`${projectName} — failed to update CLAUDE.md: ${e.message}`);
|
|
868
|
+
return false;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
function insertGuardSection(content) {
|
|
873
|
+
const preCommitMatch = content.match(/\n(#{1,3} Pre-Commit Gate)/);
|
|
874
|
+
if (preCommitMatch) return content.replace("\n" + preCommitMatch[1], GUARD_SECTION + "\n" + preCommitMatch[1]);
|
|
875
|
+
const dontDoMatch = content.match(/\n(#{1,3} Don't Do These Things)/);
|
|
876
|
+
if (dontDoMatch) return content.replace("\n" + dontDoMatch[1], GUARD_SECTION + "\n" + dontDoMatch[1]);
|
|
877
|
+
return content + GUARD_SECTION;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
function createProjectChangelog(projectDir, projectName) {
|
|
881
|
+
const changelogPath = path.join(projectDir, "CHANGELOG.md");
|
|
882
|
+
if (isSymlink(changelogPath)) return false;
|
|
883
|
+
try {
|
|
884
|
+
const today = new Date().toISOString().split("T")[0];
|
|
885
|
+
const changelogContent = [
|
|
886
|
+
"# Changelog",
|
|
887
|
+
"",
|
|
888
|
+
"All notable changes to this project are documented here.",
|
|
889
|
+
"",
|
|
890
|
+
`## [0.1.0] - ${today}`,
|
|
891
|
+
"",
|
|
892
|
+
"### Added",
|
|
893
|
+
"- Initial changelog created by GSD-T",
|
|
894
|
+
"",
|
|
895
|
+
].join("\n");
|
|
896
|
+
fs.writeFileSync(changelogPath, changelogContent, { flag: "wx" });
|
|
897
|
+
success(`${projectName} — created CHANGELOG.md`);
|
|
898
|
+
return true;
|
|
899
|
+
} catch (e) {
|
|
900
|
+
if (e.code !== "EEXIST") throw e;
|
|
901
|
+
return false;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
function checkProjectHealth(projects) {
|
|
906
|
+
heading("Project Health");
|
|
907
|
+
const playwrightMissing = [];
|
|
908
|
+
const swaggerMissing = [];
|
|
909
|
+
|
|
910
|
+
for (const projectDir of projects) {
|
|
911
|
+
if (!fs.existsSync(projectDir)) continue;
|
|
912
|
+
const name = path.basename(projectDir);
|
|
913
|
+
if (!hasPlaywright(projectDir)) playwrightMissing.push(name);
|
|
914
|
+
if (hasApi(projectDir) && !hasSwagger(projectDir)) swaggerMissing.push(name);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
if (playwrightMissing.length === 0 && swaggerMissing.length === 0) {
|
|
918
|
+
success("All projects have Playwright and Swagger configured");
|
|
919
|
+
} else {
|
|
920
|
+
if (playwrightMissing.length > 0) {
|
|
921
|
+
warn(`Playwright missing: ${playwrightMissing.join(", ")}`);
|
|
922
|
+
info("Playwright will be auto-installed when you run a GSD-T command in each project");
|
|
923
|
+
}
|
|
924
|
+
if (swaggerMissing.length > 0) {
|
|
925
|
+
warn(`Swagger/OpenAPI missing (API detected): ${swaggerMissing.join(", ")}`);
|
|
926
|
+
info("Swagger will be auto-configured when an API endpoint is created or modified");
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
return { playwrightMissing, swaggerMissing };
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
function doUpdateAll() {
|
|
933
|
+
updateGlobalCommands();
|
|
934
|
+
heading("Updating registered projects...");
|
|
935
|
+
log("");
|
|
936
|
+
|
|
937
|
+
const projects = getRegisteredProjects();
|
|
938
|
+
if (projects.length === 0) { showNoProjectsHint(); return; }
|
|
939
|
+
|
|
940
|
+
const counts = { updated: 0, skipped: 0, missing: 0, errors: 0 };
|
|
941
|
+
for (const projectDir of projects) {
|
|
942
|
+
try {
|
|
943
|
+
updateSingleProject(projectDir, counts);
|
|
944
|
+
} catch (e) {
|
|
945
|
+
warn(`${path.basename(projectDir)} — error: ${e.message || e}`);
|
|
946
|
+
counts.errors++;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
const { playwrightMissing, swaggerMissing } = checkProjectHealth(projects);
|
|
951
|
+
showUpdateAllSummary(projects.length, counts, playwrightMissing, swaggerMissing);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function updateGlobalCommands() {
|
|
955
|
+
if (getInstalledVersion() !== PKG_VERSION) {
|
|
956
|
+
doInstall({ update: true });
|
|
957
|
+
} else {
|
|
958
|
+
heading(`GSD-T ${versionLink()}`);
|
|
959
|
+
success("Global commands already up to date");
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
function showNoProjectsHint() {
|
|
964
|
+
info("No projects registered");
|
|
965
|
+
log("");
|
|
966
|
+
log(" Projects are registered automatically when you run:");
|
|
967
|
+
log(` ${DIM}$${RESET} npx @tekyzinc/gsd-t init`);
|
|
968
|
+
log("");
|
|
969
|
+
log(" Or register an existing project manually:");
|
|
970
|
+
log(` ${DIM}$${RESET} npx @tekyzinc/gsd-t register`);
|
|
971
|
+
log("");
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
function updateSingleProject(projectDir, counts) {
|
|
975
|
+
const projectName = path.basename(projectDir);
|
|
976
|
+
const claudeMd = path.join(projectDir, "CLAUDE.md");
|
|
977
|
+
|
|
978
|
+
if (!fs.existsSync(projectDir)) {
|
|
979
|
+
warn(`${projectName} — directory not found (${projectDir})`);
|
|
980
|
+
counts.missing++;
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
if (!fs.existsSync(claudeMd)) {
|
|
984
|
+
warn(`${projectName} — no CLAUDE.md found`);
|
|
985
|
+
counts.skipped++;
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
const guardAdded = updateProjectClaudeMd(claudeMd, projectName);
|
|
989
|
+
const changelogCreated = createProjectChangelog(projectDir, projectName);
|
|
990
|
+
if (guardAdded || changelogCreated) {
|
|
991
|
+
counts.updated++;
|
|
992
|
+
} else {
|
|
993
|
+
info(`${projectName} — already up to date`);
|
|
994
|
+
counts.skipped++;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
function showUpdateAllSummary(total, counts, playwrightMissing, swaggerMissing) {
|
|
999
|
+
log("");
|
|
1000
|
+
heading("Update All Complete");
|
|
1001
|
+
log(` Projects registered: ${total}`);
|
|
1002
|
+
log(` Updated: ${counts.updated}`);
|
|
1003
|
+
log(` Already current: ${counts.skipped}`);
|
|
1004
|
+
if (counts.missing > 0) log(` Not found: ${counts.missing}`);
|
|
1005
|
+
if (counts.errors > 0) log(` Errors: ${counts.errors}`);
|
|
1006
|
+
if (playwrightMissing.length > 0) log(` Missing Playwright: ${playwrightMissing.length}`);
|
|
1007
|
+
if (swaggerMissing.length > 0) log(` Missing Swagger: ${swaggerMissing.length}`);
|
|
1008
|
+
log("");
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
function checkDoctorEnvironment() {
|
|
1012
|
+
let issues = 0;
|
|
1013
|
+
const nodeVersion = parseInt(process.version.slice(1));
|
|
1014
|
+
if (nodeVersion >= 16) {
|
|
1015
|
+
success(`Node.js ${process.version}`);
|
|
1016
|
+
} else {
|
|
1017
|
+
error(`Node.js ${process.version} — requires >= 16`);
|
|
1018
|
+
issues++;
|
|
1019
|
+
}
|
|
1020
|
+
try {
|
|
1021
|
+
const claudeVersion = execFileSync("claude", ["--version"], { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
1022
|
+
success(`Claude Code: ${claudeVersion}`);
|
|
1023
|
+
} catch {
|
|
1024
|
+
warn("Claude Code CLI not found in PATH");
|
|
1025
|
+
info("Install with: npm install -g @anthropic-ai/claude-code");
|
|
1026
|
+
issues++;
|
|
1027
|
+
}
|
|
1028
|
+
if (fs.existsSync(CLAUDE_DIR)) {
|
|
1029
|
+
success("~/.claude/ directory exists");
|
|
1030
|
+
} else {
|
|
1031
|
+
error("~/.claude/ directory not found");
|
|
1032
|
+
info("Run 'npx @tekyzinc/gsd-t install' to create it");
|
|
1033
|
+
issues++;
|
|
1034
|
+
}
|
|
1035
|
+
return issues;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
function checkDoctorInstallation() {
|
|
1039
|
+
let issues = 0;
|
|
1040
|
+
const installed = getInstalledCommands();
|
|
1041
|
+
const expected = getCommandFiles();
|
|
1042
|
+
if (installed.length === expected.length) {
|
|
1043
|
+
success(`All ${expected.length} commands installed`);
|
|
1044
|
+
} else if (installed.length > 0) {
|
|
1045
|
+
warn(`${installed.length}/${expected.length} commands installed`);
|
|
1046
|
+
info(`Missing: ${expected.filter((f) => !installed.includes(f)).join(", ")}`);
|
|
1047
|
+
issues++;
|
|
1048
|
+
} else {
|
|
1049
|
+
error("No GSD-T commands installed");
|
|
1050
|
+
issues++;
|
|
1051
|
+
}
|
|
1052
|
+
issues += checkDoctorClaudeMd();
|
|
1053
|
+
issues += checkDoctorSettings();
|
|
1054
|
+
issues += checkDoctorEncoding(installed);
|
|
1055
|
+
return issues;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
function checkDoctorClaudeMd() {
|
|
1059
|
+
if (!fs.existsSync(GLOBAL_CLAUDE_MD)) { error("No global CLAUDE.md"); return 1; }
|
|
1060
|
+
const content = fs.readFileSync(GLOBAL_CLAUDE_MD, "utf8");
|
|
1061
|
+
if (content.includes("GSD-T")) { success("CLAUDE.md contains GSD-T config"); return 0; }
|
|
1062
|
+
warn("CLAUDE.md exists but missing GSD-T section");
|
|
1063
|
+
return 1;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
function checkDoctorSettings() {
|
|
1067
|
+
if (!fs.existsSync(SETTINGS_JSON)) { info("No settings.json (not required)"); return 0; }
|
|
1068
|
+
if (readSettingsJson() !== null) {
|
|
1069
|
+
success("settings.json is valid JSON");
|
|
1070
|
+
return 0;
|
|
1071
|
+
}
|
|
1072
|
+
error("settings.json has invalid JSON");
|
|
1073
|
+
return 1;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
function checkDoctorEncoding(installed) {
|
|
1077
|
+
let bad = 0;
|
|
1078
|
+
for (const file of installed) {
|
|
1079
|
+
const content = fs.readFileSync(path.join(COMMANDS_DIR, file), "utf8");
|
|
1080
|
+
if (content.includes("\u00e2\u20ac") || content.includes("\u00c3")) bad++;
|
|
1081
|
+
}
|
|
1082
|
+
if (bad > 0) {
|
|
1083
|
+
error(`${bad} command files have encoding issues (corrupted characters)`);
|
|
1084
|
+
info("Run 'npx @tekyzinc/gsd-t update' to replace with clean versions");
|
|
1085
|
+
return 1;
|
|
1086
|
+
}
|
|
1087
|
+
if (installed.length > 0) success("No encoding issues in command files");
|
|
1088
|
+
return 0;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
function checkDoctorProject() {
|
|
1092
|
+
let issues = 0;
|
|
1093
|
+
const cwd = process.cwd();
|
|
1094
|
+
if (hasPlaywright(cwd)) {
|
|
1095
|
+
success("Playwright configured");
|
|
1096
|
+
} else {
|
|
1097
|
+
warn("Playwright not configured in this project");
|
|
1098
|
+
info("Will be auto-installed when you run a GSD-T testing command");
|
|
1099
|
+
issues++;
|
|
1100
|
+
}
|
|
1101
|
+
if (hasApi(cwd)) {
|
|
1102
|
+
if (hasSwagger(cwd)) {
|
|
1103
|
+
success("Swagger/OpenAPI configured");
|
|
1104
|
+
} else {
|
|
1105
|
+
warn("API framework detected but no Swagger/OpenAPI spec found");
|
|
1106
|
+
info("Will be auto-configured when an API endpoint is created or modified");
|
|
1107
|
+
issues++;
|
|
1108
|
+
}
|
|
1109
|
+
} else {
|
|
1110
|
+
info("No API framework detected (Swagger check skipped)");
|
|
1111
|
+
}
|
|
1112
|
+
return issues;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
function doDoctor() {
|
|
1116
|
+
heading("GSD-T Doctor");
|
|
1117
|
+
log("");
|
|
1118
|
+
let issues = 0;
|
|
1119
|
+
issues += checkDoctorEnvironment();
|
|
1120
|
+
issues += checkDoctorInstallation();
|
|
1121
|
+
issues += checkDoctorProject();
|
|
1122
|
+
log("");
|
|
1123
|
+
if (issues === 0) {
|
|
1124
|
+
log(`${GREEN}${BOLD} All checks passed!${RESET}`);
|
|
1125
|
+
} else {
|
|
1126
|
+
log(`${YELLOW}${BOLD} ${issues} issue${issues > 1 ? "s" : ""} found${RESET}`);
|
|
1127
|
+
}
|
|
1128
|
+
log("");
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
function doRegister() {
|
|
1132
|
+
const projectDir = process.cwd();
|
|
1133
|
+
const gsdtDir = path.join(projectDir, ".gsd-t");
|
|
1134
|
+
|
|
1135
|
+
if (!fs.existsSync(gsdtDir)) {
|
|
1136
|
+
error("Not a GSD-T project (no .gsd-t/ directory found)");
|
|
1137
|
+
info("Run 'npx @tekyzinc/gsd-t init' to initialize this project first");
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
if (registerProject(projectDir)) {
|
|
1142
|
+
success(`Registered: ${projectDir}`);
|
|
1143
|
+
} else {
|
|
1144
|
+
info("Already registered");
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
// Show all registered projects
|
|
1148
|
+
const projects = getRegisteredProjects();
|
|
1149
|
+
log("");
|
|
1150
|
+
heading("Registered Projects");
|
|
1151
|
+
for (const p of projects) {
|
|
1152
|
+
const exists = fs.existsSync(p);
|
|
1153
|
+
if (exists) {
|
|
1154
|
+
log(` ${GREEN}✓${RESET} ${p}`);
|
|
1155
|
+
} else {
|
|
1156
|
+
log(` ${RED}✗${RESET} ${p} ${DIM}(not found)${RESET}`);
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
log("");
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
function isNewerVersion(latest, current) {
|
|
1163
|
+
const l = latest.split(".").map(Number);
|
|
1164
|
+
const c = current.split(".").map(Number);
|
|
1165
|
+
for (let i = 0; i < 3; i++) {
|
|
1166
|
+
if ((l[i] || 0) > (c[i] || 0)) return true;
|
|
1167
|
+
if ((l[i] || 0) < (c[i] || 0)) return false;
|
|
1168
|
+
}
|
|
1169
|
+
return false;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
function checkForUpdates(command) {
|
|
1173
|
+
const skipCommands = ["install", "update", "update-all", "--version", "-v"];
|
|
1174
|
+
if (skipCommands.includes(command)) return;
|
|
1175
|
+
|
|
1176
|
+
const cached = readUpdateCache();
|
|
1177
|
+
|
|
1178
|
+
if (cached && cached.latest && validateVersion(cached.latest) && isNewerVersion(cached.latest, PKG_VERSION)) {
|
|
1179
|
+
showUpdateNotice(cached.latest);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
if (!cached) {
|
|
1183
|
+
fetchVersionSync();
|
|
1184
|
+
} else if ((Date.now() - cached.timestamp) > 3600000) {
|
|
1185
|
+
refreshVersionAsync();
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
function readSettingsJson() {
|
|
1190
|
+
if (!fs.existsSync(SETTINGS_JSON)) return null;
|
|
1191
|
+
try { return JSON.parse(fs.readFileSync(SETTINGS_JSON, "utf8")); }
|
|
1192
|
+
catch { return null; }
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
function readUpdateCache() {
|
|
1196
|
+
try {
|
|
1197
|
+
if (fs.existsSync(UPDATE_CHECK_FILE)) {
|
|
1198
|
+
return JSON.parse(fs.readFileSync(UPDATE_CHECK_FILE, "utf8"));
|
|
1199
|
+
}
|
|
1200
|
+
} catch { /* ignore corrupt cache */ }
|
|
1201
|
+
return null;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
function fetchVersionSync() {
|
|
1205
|
+
try {
|
|
1206
|
+
const fetchScriptPath = path.join(__dirname, "..", "scripts", "gsd-t-fetch-version.js");
|
|
1207
|
+
const result = execFileSync(
|
|
1208
|
+
process.execPath, [fetchScriptPath],
|
|
1209
|
+
{ timeout: 8000, encoding: "utf8" }
|
|
1210
|
+
).trim();
|
|
1211
|
+
if (result && validateVersion(result) && !isSymlink(UPDATE_CHECK_FILE)) {
|
|
1212
|
+
fs.writeFileSync(UPDATE_CHECK_FILE, JSON.stringify({ latest: result, timestamp: Date.now() }));
|
|
1213
|
+
if (isNewerVersion(result, PKG_VERSION)) showUpdateNotice(result);
|
|
1214
|
+
}
|
|
1215
|
+
} catch { /* timeout or network error — skip */ }
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
function refreshVersionAsync() {
|
|
1219
|
+
const updateScript = path.join(__dirname, "..", "scripts", "npm-update-check.js");
|
|
1220
|
+
const child = cpSpawn(process.execPath, [updateScript, UPDATE_CHECK_FILE], {
|
|
1221
|
+
detached: true, stdio: "ignore",
|
|
1222
|
+
});
|
|
1223
|
+
child.unref();
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
function showUpdateNotice(latest) {
|
|
1227
|
+
log("");
|
|
1228
|
+
log(` ${YELLOW}╭──────────────────────────────────────────────╮${RESET}`);
|
|
1229
|
+
log(` ${YELLOW}│${RESET} Update available: ${DIM}${PKG_VERSION}${RESET} → ${GREEN}${latest}${RESET} ${YELLOW}│${RESET}`);
|
|
1230
|
+
log(` ${YELLOW}│${RESET} Run: ${CYAN}npm update -g @tekyzinc/gsd-t${RESET} ${YELLOW}│${RESET}`);
|
|
1231
|
+
log(` ${YELLOW}│${RESET} Then: ${CYAN}gsd-t update-all${RESET} ${YELLOW}│${RESET}`);
|
|
1232
|
+
log(` ${YELLOW}│${RESET} Changelog: ${CYAN}gsd-t changelog${RESET} ${YELLOW}│${RESET}`);
|
|
1233
|
+
log(` ${YELLOW}╰──────────────────────────────────────────────╯${RESET}`);
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
function doChangelog() {
|
|
1237
|
+
try {
|
|
1238
|
+
if (process.platform === "win32") {
|
|
1239
|
+
// SAFETY: CHANGELOG_URL is a hardcoded constant (line 43). If it ever becomes
|
|
1240
|
+
// dynamic/user-provided, this cmd.exe call would need URL validation to prevent injection.
|
|
1241
|
+
execFileSync("cmd", ["/c", "start", "", CHANGELOG_URL], { stdio: "ignore" });
|
|
1242
|
+
} else {
|
|
1243
|
+
const openCmd = process.platform === "darwin" ? "open" : "xdg-open";
|
|
1244
|
+
execFileSync(openCmd, [CHANGELOG_URL], { stdio: "ignore" });
|
|
1245
|
+
}
|
|
1246
|
+
success(`Opened changelog in browser`);
|
|
1247
|
+
} catch {
|
|
1248
|
+
// Fallback: print the URL
|
|
1249
|
+
log(`\n ${CHANGELOG_URL}\n`);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
function showHelp() {
|
|
1254
|
+
log(`\n${BOLD}GSD-T${RESET} — Contract-Driven Development for Claude Code\n`);
|
|
1255
|
+
log(`${BOLD}Usage:${RESET} npx @tekyzinc/gsd-t ${CYAN}<command>${RESET} [options]\n`);
|
|
1256
|
+
log(`${BOLD}Commands:${RESET}`);
|
|
1257
|
+
log(` ${CYAN}install${RESET} Install slash commands + global CLAUDE.md`);
|
|
1258
|
+
log(` ${CYAN}update${RESET} Update global commands + CLAUDE.md`);
|
|
1259
|
+
log(` ${CYAN}update-all${RESET} Update globally + all registered project CLAUDE.md files`);
|
|
1260
|
+
log(` ${CYAN}init${RESET} [name] Scaffold GSD-T project (auto-registers)`);
|
|
1261
|
+
log(` ${CYAN}register${RESET} Register current directory as a GSD-T project`);
|
|
1262
|
+
log(` ${CYAN}status${RESET} Show installation status + check for updates`);
|
|
1263
|
+
log(` ${CYAN}uninstall${RESET} Remove GSD-T commands (keeps project files)`);
|
|
1264
|
+
log(` ${CYAN}doctor${RESET} Diagnose common issues`);
|
|
1265
|
+
log(` ${CYAN}changelog${RESET} Open changelog in the browser`);
|
|
1266
|
+
log(` ${CYAN}help${RESET} Show this help\n`);
|
|
1267
|
+
log(`${BOLD}Examples:${RESET}`);
|
|
1268
|
+
log(` ${DIM}$${RESET} npx @tekyzinc/gsd-t install`);
|
|
1269
|
+
log(` ${DIM}$${RESET} npx @tekyzinc/gsd-t init my-saas-app`);
|
|
1270
|
+
log(` ${DIM}$${RESET} npx @tekyzinc/gsd-t update\n`);
|
|
1271
|
+
log(`${BOLD}After installing, use in Claude Code:${RESET}`);
|
|
1272
|
+
log(` ${DIM}>${RESET} /user:gsd-t-project "Build a task management app"`);
|
|
1273
|
+
log(` ${DIM}>${RESET} /user:gsd-t-wave\n`);
|
|
1274
|
+
log(`${DIM}Docs: https://github.com/Tekyz-Inc/get-stuff-done-teams${RESET}\n`);
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
// ─── Exports (for testing) ───────────────────────────────────────────────────
|
|
1278
|
+
|
|
1279
|
+
module.exports = {
|
|
1280
|
+
validateProjectName,
|
|
1281
|
+
applyTokens,
|
|
1282
|
+
normalizeEol,
|
|
1283
|
+
validateVersion,
|
|
1284
|
+
validateProjectPath,
|
|
1285
|
+
isSymlink,
|
|
1286
|
+
hasSymlinkInPath,
|
|
1287
|
+
isNewerVersion,
|
|
1288
|
+
ensureDir,
|
|
1289
|
+
copyFile,
|
|
1290
|
+
hasPlaywright,
|
|
1291
|
+
hasSwagger,
|
|
1292
|
+
hasApi,
|
|
1293
|
+
readProjectDeps,
|
|
1294
|
+
readPyContent,
|
|
1295
|
+
getCommandFiles,
|
|
1296
|
+
getGsdtCommands,
|
|
1297
|
+
getUtilityCommands,
|
|
1298
|
+
getInstalledCommands,
|
|
1299
|
+
getInstalledVersion,
|
|
1300
|
+
getRegisteredProjects,
|
|
1301
|
+
updateSingleProject,
|
|
1302
|
+
updateGlobalCommands,
|
|
1303
|
+
showNoProjectsHint,
|
|
1304
|
+
showUpdateAllSummary,
|
|
1305
|
+
showStatusVersion,
|
|
1306
|
+
showStatusCommands,
|
|
1307
|
+
showStatusConfig,
|
|
1308
|
+
showStatusTeams,
|
|
1309
|
+
showStatusProject,
|
|
1310
|
+
showInstallSummary,
|
|
1311
|
+
showInitTree,
|
|
1312
|
+
writeTemplateFile,
|
|
1313
|
+
insertGuardSection,
|
|
1314
|
+
addHeartbeatHook,
|
|
1315
|
+
removeInstalledCommands,
|
|
1316
|
+
removeVersionFile,
|
|
1317
|
+
checkDoctorClaudeMd,
|
|
1318
|
+
checkDoctorSettings,
|
|
1319
|
+
checkDoctorEncoding,
|
|
1320
|
+
updateExistingGlobalClaudeMd,
|
|
1321
|
+
appendGsdtToClaudeMd,
|
|
1322
|
+
readSettingsJson,
|
|
1323
|
+
readUpdateCache,
|
|
1324
|
+
fetchVersionSync,
|
|
1325
|
+
refreshVersionAsync,
|
|
1326
|
+
PKG_VERSION,
|
|
1327
|
+
PKG_ROOT,
|
|
1328
|
+
PKG_COMMANDS,
|
|
1329
|
+
};
|
|
1330
|
+
|
|
1331
|
+
// ─── Main ────────────────────────────────────────────────────────────────────
|
|
1332
|
+
|
|
1333
|
+
if (require.main === module) {
|
|
1334
|
+
const args = process.argv.slice(2);
|
|
1335
|
+
const command = args[0] || "help";
|
|
1336
|
+
|
|
1337
|
+
switch (command) {
|
|
1338
|
+
case "install":
|
|
1339
|
+
doInstall();
|
|
1340
|
+
break;
|
|
1341
|
+
case "update":
|
|
1342
|
+
doUpdate();
|
|
1343
|
+
break;
|
|
1344
|
+
case "update-all":
|
|
1345
|
+
doUpdateAll();
|
|
1346
|
+
break;
|
|
1347
|
+
case "init":
|
|
1348
|
+
doInit(args[1]);
|
|
1349
|
+
break;
|
|
1350
|
+
case "register":
|
|
1351
|
+
doRegister();
|
|
1352
|
+
break;
|
|
1353
|
+
case "status":
|
|
1354
|
+
doStatus();
|
|
1355
|
+
break;
|
|
1356
|
+
case "uninstall":
|
|
1357
|
+
doUninstall();
|
|
1358
|
+
break;
|
|
1359
|
+
case "doctor":
|
|
1360
|
+
doDoctor();
|
|
1361
|
+
break;
|
|
1362
|
+
case "changelog":
|
|
1363
|
+
doChangelog();
|
|
1364
|
+
break;
|
|
1365
|
+
case "help":
|
|
1366
|
+
case "--help":
|
|
1367
|
+
case "-h":
|
|
1368
|
+
showHelp();
|
|
1369
|
+
break;
|
|
1370
|
+
case "--version":
|
|
1371
|
+
case "-v":
|
|
1372
|
+
log(PKG_VERSION);
|
|
1373
|
+
break;
|
|
1374
|
+
default:
|
|
1375
|
+
error(`Unknown command: ${command}`);
|
|
1376
|
+
showHelp();
|
|
1377
|
+
process.exit(1);
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
checkForUpdates(command);
|
|
1381
|
+
}
|