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