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