get-shit-done-cc 1.6.3 → 1.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/bin/install.js +161 -7
  2. package/package.json +1 -1
package/bin/install.js CHANGED
@@ -151,6 +151,96 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix) {
151
151
  }
152
152
  }
153
153
 
154
+ /**
155
+ * Clean up orphaned files from previous GSD versions
156
+ */
157
+ function cleanupOrphanedFiles(claudeDir) {
158
+ const orphanedFiles = [
159
+ 'hooks/gsd-notify.sh', // Removed in v1.6.x
160
+ ];
161
+
162
+ for (const relPath of orphanedFiles) {
163
+ const fullPath = path.join(claudeDir, relPath);
164
+ if (fs.existsSync(fullPath)) {
165
+ fs.unlinkSync(fullPath);
166
+ console.log(` ${green}✓${reset} Removed orphaned ${relPath}`);
167
+ }
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Clean up orphaned hook registrations from settings.json
173
+ */
174
+ function cleanupOrphanedHooks(settings) {
175
+ const orphanedHookPatterns = [
176
+ 'gsd-notify.sh', // Removed in v1.6.x
177
+ ];
178
+
179
+ let cleaned = false;
180
+
181
+ // Check all hook event types (Stop, SessionStart, etc.)
182
+ if (settings.hooks) {
183
+ for (const eventType of Object.keys(settings.hooks)) {
184
+ const hookEntries = settings.hooks[eventType];
185
+ if (Array.isArray(hookEntries)) {
186
+ // Filter out entries that contain orphaned hooks
187
+ const filtered = hookEntries.filter(entry => {
188
+ if (entry.hooks && Array.isArray(entry.hooks)) {
189
+ // Check if any hook in this entry matches orphaned patterns
190
+ const hasOrphaned = entry.hooks.some(h =>
191
+ h.command && orphanedHookPatterns.some(pattern => h.command.includes(pattern))
192
+ );
193
+ if (hasOrphaned) {
194
+ cleaned = true;
195
+ return false; // Remove this entry
196
+ }
197
+ }
198
+ return true; // Keep this entry
199
+ });
200
+ settings.hooks[eventType] = filtered;
201
+ }
202
+ }
203
+ }
204
+
205
+ if (cleaned) {
206
+ console.log(` ${green}✓${reset} Removed orphaned hook registrations`);
207
+ }
208
+
209
+ return settings;
210
+ }
211
+
212
+ /**
213
+ * Verify a directory exists and contains files
214
+ */
215
+ function verifyInstalled(dirPath, description) {
216
+ if (!fs.existsSync(dirPath)) {
217
+ console.error(` ${yellow}✗${reset} Failed to install ${description}: directory not created`);
218
+ return false;
219
+ }
220
+ try {
221
+ const entries = fs.readdirSync(dirPath);
222
+ if (entries.length === 0) {
223
+ console.error(` ${yellow}✗${reset} Failed to install ${description}: directory is empty`);
224
+ return false;
225
+ }
226
+ } catch (e) {
227
+ console.error(` ${yellow}✗${reset} Failed to install ${description}: ${e.message}`);
228
+ return false;
229
+ }
230
+ return true;
231
+ }
232
+
233
+ /**
234
+ * Verify a file exists
235
+ */
236
+ function verifyFileInstalled(filePath, description) {
237
+ if (!fs.existsSync(filePath)) {
238
+ console.error(` ${yellow}✗${reset} Failed to install ${description}: file not created`);
239
+ return false;
240
+ }
241
+ return true;
242
+ }
243
+
154
244
  /**
155
245
  * Install to the specified directory
156
246
  */
@@ -175,6 +265,12 @@ function install(isGlobal) {
175
265
 
176
266
  console.log(` Installing to ${cyan}${locationLabel}${reset}\n`);
177
267
 
268
+ // Track installation failures
269
+ const failures = [];
270
+
271
+ // Clean up orphaned files from previous versions
272
+ cleanupOrphanedFiles(claudeDir);
273
+
178
274
  // Create commands directory
179
275
  const commandsDir = path.join(claudeDir, 'commands');
180
276
  fs.mkdirSync(commandsDir, { recursive: true });
@@ -183,13 +279,21 @@ function install(isGlobal) {
183
279
  const gsdSrc = path.join(src, 'commands', 'gsd');
184
280
  const gsdDest = path.join(commandsDir, 'gsd');
185
281
  copyWithPathReplacement(gsdSrc, gsdDest, pathPrefix);
186
- console.log(` ${green}✓${reset} Installed commands/gsd`);
282
+ if (verifyInstalled(gsdDest, 'commands/gsd')) {
283
+ console.log(` ${green}✓${reset} Installed commands/gsd`);
284
+ } else {
285
+ failures.push('commands/gsd');
286
+ }
187
287
 
188
288
  // Copy get-shit-done skill with path replacement
189
289
  const skillSrc = path.join(src, 'get-shit-done');
190
290
  const skillDest = path.join(claudeDir, 'get-shit-done');
191
291
  copyWithPathReplacement(skillSrc, skillDest, pathPrefix);
192
- console.log(` ${green}✓${reset} Installed get-shit-done`);
292
+ if (verifyInstalled(skillDest, 'get-shit-done')) {
293
+ console.log(` ${green}✓${reset} Installed get-shit-done`);
294
+ } else {
295
+ failures.push('get-shit-done');
296
+ }
193
297
 
194
298
  // Copy agents to ~/.claude/agents (subagents must be at root level)
195
299
  // Only delete gsd-*.md files to preserve user's custom agents
@@ -216,7 +320,11 @@ function install(isGlobal) {
216
320
  fs.writeFileSync(path.join(agentsDest, entry.name), content);
217
321
  }
218
322
  }
219
- console.log(` ${green}✓${reset} Installed agents`);
323
+ if (verifyInstalled(agentsDest, 'agents')) {
324
+ console.log(` ${green}✓${reset} Installed agents`);
325
+ } else {
326
+ failures.push('agents');
327
+ }
220
328
  }
221
329
 
222
330
  // Copy CHANGELOG.md
@@ -224,13 +332,21 @@ function install(isGlobal) {
224
332
  const changelogDest = path.join(claudeDir, 'get-shit-done', 'CHANGELOG.md');
225
333
  if (fs.existsSync(changelogSrc)) {
226
334
  fs.copyFileSync(changelogSrc, changelogDest);
227
- console.log(` ${green}✓${reset} Installed CHANGELOG.md`);
335
+ if (verifyFileInstalled(changelogDest, 'CHANGELOG.md')) {
336
+ console.log(` ${green}✓${reset} Installed CHANGELOG.md`);
337
+ } else {
338
+ failures.push('CHANGELOG.md');
339
+ }
228
340
  }
229
341
 
230
342
  // Write VERSION file for whats-new command
231
343
  const versionDest = path.join(claudeDir, 'get-shit-done', 'VERSION');
232
344
  fs.writeFileSync(versionDest, pkg.version);
233
- console.log(` ${green}✓${reset} Wrote VERSION (${pkg.version})`);
345
+ if (verifyFileInstalled(versionDest, 'VERSION')) {
346
+ console.log(` ${green}✓${reset} Wrote VERSION (${pkg.version})`);
347
+ } else {
348
+ failures.push('VERSION');
349
+ }
234
350
 
235
351
  // Copy hooks
236
352
  const hooksSrc = path.join(src, 'hooks');
@@ -243,12 +359,23 @@ function install(isGlobal) {
243
359
  const destFile = path.join(hooksDest, entry);
244
360
  fs.copyFileSync(srcFile, destFile);
245
361
  }
246
- console.log(` ${green}✓${reset} Installed hooks`);
362
+ if (verifyInstalled(hooksDest, 'hooks')) {
363
+ console.log(` ${green}✓${reset} Installed hooks`);
364
+ } else {
365
+ failures.push('hooks');
366
+ }
367
+ }
368
+
369
+ // If critical components failed, exit with error
370
+ if (failures.length > 0) {
371
+ console.error(`\n ${yellow}Installation incomplete!${reset} Failed: ${failures.join(', ')}`);
372
+ console.error(` Try running directly: node ~/.npm/_npx/*/node_modules/get-shit-done-cc/bin/install.js --global\n`);
373
+ process.exit(1);
247
374
  }
248
375
 
249
376
  // Configure statusline and hooks in settings.json
250
377
  const settingsPath = path.join(claudeDir, 'settings.json');
251
- const settings = readSettings(settingsPath);
378
+ const settings = cleanupOrphanedHooks(readSettings(settingsPath));
252
379
  const statuslineCommand = isGlobal
253
380
  ? 'node "$HOME/.claude/hooks/statusline.js"'
254
381
  : 'node .claude/hooks/statusline.js';
@@ -364,11 +491,37 @@ function handleStatusline(settings, isInteractive, callback) {
364
491
  * Prompt for install location
365
492
  */
366
493
  function promptLocation() {
494
+ // Check if stdin is a TTY - if not, fall back to global install
495
+ // This handles npx execution in environments like WSL2 where stdin may not be properly connected
496
+ if (!process.stdin.isTTY) {
497
+ console.log(` ${yellow}Non-interactive terminal detected, defaulting to global install${reset}\n`);
498
+ const { settingsPath, settings, statuslineCommand } = install(true);
499
+ handleStatusline(settings, false, (shouldInstallStatusline) => {
500
+ finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline);
501
+ });
502
+ return;
503
+ }
504
+
367
505
  const rl = readline.createInterface({
368
506
  input: process.stdin,
369
507
  output: process.stdout
370
508
  });
371
509
 
510
+ // Track whether we've processed the answer to prevent double-execution
511
+ let answered = false;
512
+
513
+ // Handle readline close event to detect premature stdin closure
514
+ rl.on('close', () => {
515
+ if (!answered) {
516
+ answered = true;
517
+ console.log(`\n ${yellow}Input stream closed, defaulting to global install${reset}\n`);
518
+ const { settingsPath, settings, statuslineCommand } = install(true);
519
+ handleStatusline(settings, false, (shouldInstallStatusline) => {
520
+ finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline);
521
+ });
522
+ }
523
+ });
524
+
372
525
  const configDir = expandTilde(explicitConfigDir) || expandTilde(process.env.CLAUDE_CONFIG_DIR);
373
526
  const globalPath = configDir || path.join(os.homedir(), '.claude');
374
527
  const globalLabel = globalPath.replace(os.homedir(), '~');
@@ -380,6 +533,7 @@ function promptLocation() {
380
533
  `);
381
534
 
382
535
  rl.question(` Choice ${dim}[1]${reset}: `, (answer) => {
536
+ answered = true;
383
537
  rl.close();
384
538
  const choice = answer.trim() || '1';
385
539
  const isGlobal = choice !== '2';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "get-shit-done-cc",
3
- "version": "1.6.3",
3
+ "version": "1.6.4",
4
4
  "description": "A meta-prompting, context engineering and spec-driven development system for Claude Code by TÂCHES.",
5
5
  "bin": {
6
6
  "get-shit-done-cc": "bin/install.js"