claude-code-autoconfig 1.0.200 → 1.0.201
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/.claude/hooks/terminal-title.directive.md +19 -11
- package/.claude/hooks/terminal-title.js +59 -14
- package/.claude/settings.json +9 -9
- package/CHANGELOG.md +4 -3
- package/bin/cli.js +1042 -1014
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -1,1014 +1,1042 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const readline = require('readline');
|
|
6
|
-
const { execSync, spawn } = require('child_process');
|
|
7
|
-
const { formatUpdateSummary } = require('./update-summary.js');
|
|
8
|
-
|
|
9
|
-
const cwd = process.cwd();
|
|
10
|
-
const packageDir = path.dirname(__dirname);
|
|
11
|
-
|
|
12
|
-
// Cleanup any stray 'nul' file immediately on startup (Windows /dev/null artifact)
|
|
13
|
-
function cleanupNulFile() {
|
|
14
|
-
const nulFile = path.join(cwd, 'nul');
|
|
15
|
-
if (fs.existsSync(nulFile)) {
|
|
16
|
-
try {
|
|
17
|
-
fs.unlinkSync(nulFile);
|
|
18
|
-
} catch (e) {
|
|
19
|
-
// Ignore - file might be locked
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
cleanupNulFile();
|
|
24
|
-
|
|
25
|
-
// Reserved Windows device names - never create files with these names
|
|
26
|
-
const WINDOWS_RESERVED = ['CON', 'PRN', 'AUX', 'NUL', 'COM1', 'COM2', 'COM3', 'COM4',
|
|
27
|
-
'COM5', 'COM6', 'COM7', 'COM8', 'COM9', 'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5',
|
|
28
|
-
'LPT6', 'LPT7', 'LPT8', 'LPT9'];
|
|
29
|
-
|
|
30
|
-
// Files/folders installed by autoconfig - don't backup these
|
|
31
|
-
const AUTOCONFIG_FILES = ['commands', 'docs', 'agents', 'migration', 'hooks', 'scripts', 'sounds', 'rules', 'feedback', 'settings.json', 'settings.local.json', '.mcp.json', '.autoconfig-version', '.autoconfig-plugins.json'];
|
|
32
|
-
|
|
33
|
-
function isReservedName(name) {
|
|
34
|
-
const baseName = name.replace(/\.[^.]*$/, '').toUpperCase();
|
|
35
|
-
return WINDOWS_RESERVED.includes(baseName);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function hasUserContent(claudeDir) {
|
|
39
|
-
// Check if .claude/ has any files beyond what autoconfig installs
|
|
40
|
-
if (!fs.existsSync(claudeDir)) return false;
|
|
41
|
-
|
|
42
|
-
const entries = fs.readdirSync(claudeDir);
|
|
43
|
-
for (const entry of entries) {
|
|
44
|
-
if (!AUTOCONFIG_FILES.includes(entry)) {
|
|
45
|
-
// Found something that's not from autoconfig
|
|
46
|
-
return true;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
return false;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function formatTimestamp() {
|
|
53
|
-
const now = new Date();
|
|
54
|
-
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
|
|
55
|
-
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
56
|
-
const month = months[now.getMonth()];
|
|
57
|
-
const day = now.getDate();
|
|
58
|
-
const year = now.getFullYear();
|
|
59
|
-
const hour = now.getHours();
|
|
60
|
-
const min = String(now.getMinutes()).padStart(2, '0');
|
|
61
|
-
const ampm = hour >= 12 ? 'pm' : 'am';
|
|
62
|
-
const hour12 = hour % 12 || 12;
|
|
63
|
-
|
|
64
|
-
return `${month}-${day}-${year}_${hour12}-${min}${ampm}`;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// --pull-updates: Copy new update files from package to user's project
|
|
68
|
-
function parseAppliedUpdates(filePath) {
|
|
69
|
-
if (!fs.existsSync(filePath)) return [];
|
|
70
|
-
const content = fs.readFileSync(filePath, 'utf8');
|
|
71
|
-
const match = content.match(/<!-- @applied\r?\n([\s\S]*?)-->/);
|
|
72
|
-
if (!match) return [];
|
|
73
|
-
|
|
74
|
-
return match[1].trim().split('\n')
|
|
75
|
-
.filter(line => line.trim())
|
|
76
|
-
.map(line => {
|
|
77
|
-
const idMatch = line.match(/^(\d{3})/);
|
|
78
|
-
return idMatch ? parseInt(idMatch[1], 10) : 0;
|
|
79
|
-
})
|
|
80
|
-
.filter(id => id > 0);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function getHighestAppliedId(appliedIds) {
|
|
84
|
-
return appliedIds.length > 0 ? Math.max(...appliedIds) : 0;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function pullUpdates() {
|
|
88
|
-
console.log('\x1b[36m%s\x1b[0m', '🔄 Checking for updates...');
|
|
89
|
-
console.log();
|
|
90
|
-
|
|
91
|
-
const userCmdPath = path.join(cwd, '.claude', 'commands', 'autoconfig-update.md');
|
|
92
|
-
const packageCmdPath = path.join(packageDir, '.claude', 'commands', 'autoconfig-update.md');
|
|
93
|
-
const packageUpdatesDir = path.join(packageDir, '.claude', 'updates');
|
|
94
|
-
const userUpdatesDir = path.join(cwd, '.claude', 'updates');
|
|
95
|
-
|
|
96
|
-
// Ensure .claude/commands/ exists
|
|
97
|
-
fs.mkdirSync(path.join(cwd, '.claude', 'commands'), { recursive: true });
|
|
98
|
-
|
|
99
|
-
// Refresh autoconfig-update.md (preserve user's @applied block)
|
|
100
|
-
if (fs.existsSync(packageCmdPath)) {
|
|
101
|
-
if (fs.existsSync(userCmdPath)) {
|
|
102
|
-
const userContent = fs.readFileSync(userCmdPath, 'utf8');
|
|
103
|
-
const packageContent = fs.readFileSync(packageCmdPath, 'utf8');
|
|
104
|
-
const userApplied = userContent.match(/<!-- @applied[\s\S]*?-->/);
|
|
105
|
-
if (userApplied) {
|
|
106
|
-
const merged = packageContent.replace(/<!-- @applied[\s\S]*?-->/, userApplied[0]);
|
|
107
|
-
fs.writeFileSync(userCmdPath, merged);
|
|
108
|
-
} else {
|
|
109
|
-
fs.copyFileSync(packageCmdPath, userCmdPath);
|
|
110
|
-
}
|
|
111
|
-
} else {
|
|
112
|
-
fs.copyFileSync(packageCmdPath, userCmdPath);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Check for available updates in package
|
|
117
|
-
if (!fs.existsSync(packageUpdatesDir)) {
|
|
118
|
-
console.log('\x1b[32m%s\x1b[0m', '✅ Already up to date');
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const appliedIds = parseAppliedUpdates(userCmdPath);
|
|
123
|
-
const highestApplied = getHighestAppliedId(appliedIds);
|
|
124
|
-
|
|
125
|
-
const updateFiles = fs.readdirSync(packageUpdatesDir).filter(f => f.endsWith('.md'));
|
|
126
|
-
const newUpdates = updateFiles.filter(file => {
|
|
127
|
-
const match = file.match(/^(\d{3})-/);
|
|
128
|
-
if (!match) return false;
|
|
129
|
-
return parseInt(match[1], 10) > highestApplied;
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
if (newUpdates.length === 0) {
|
|
133
|
-
console.log('\x1b[32m%s\x1b[0m', '✅ Already up to date');
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Copy new update files
|
|
138
|
-
fs.mkdirSync(userUpdatesDir, { recursive: true });
|
|
139
|
-
for (const file of newUpdates) {
|
|
140
|
-
fs.copyFileSync(
|
|
141
|
-
path.join(packageUpdatesDir, file),
|
|
142
|
-
path.join(userUpdatesDir, file)
|
|
143
|
-
);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
console.log('\x1b[32m%s\x1b[0m', `✅ Copied ${newUpdates.length} new update${newUpdates.length > 1 ? 's' : ''} to .claude/updates/`);
|
|
147
|
-
console.log();
|
|
148
|
-
console.log('Run \x1b[36mclaude /autoconfig-update\x1b[0m to review and install updates.');
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
if (process.argv.includes('--pull-updates')) {
|
|
152
|
-
pullUpdates();
|
|
153
|
-
process.exit(0);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// ============================================================================
|
|
157
|
-
// Settings merge helpers (shared by the upgrade path and the plugin installer)
|
|
158
|
-
// ============================================================================
|
|
159
|
-
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
// -
|
|
164
|
-
//
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
if (!userSettings.
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
if (!
|
|
310
|
-
return
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
function
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
if (manifest.
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
console.log('
|
|
447
|
-
console.log();
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
process.
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
console.log();
|
|
500
|
-
console.log('\x1b[
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
console.log('\x1b[
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
if (
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
for (const entry of
|
|
572
|
-
const
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
const
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
const
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
const
|
|
657
|
-
|
|
658
|
-
const
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
const
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
)
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
//
|
|
778
|
-
|
|
779
|
-
const
|
|
780
|
-
|
|
781
|
-
const
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
const
|
|
810
|
-
const
|
|
811
|
-
if (fs.existsSync(
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
//
|
|
880
|
-
//
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
if
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
//
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
console.log();
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
console.log();
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
console.log(
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
});
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const readline = require('readline');
|
|
6
|
+
const { execSync, spawn } = require('child_process');
|
|
7
|
+
const { formatUpdateSummary } = require('./update-summary.js');
|
|
8
|
+
|
|
9
|
+
const cwd = process.cwd();
|
|
10
|
+
const packageDir = path.dirname(__dirname);
|
|
11
|
+
|
|
12
|
+
// Cleanup any stray 'nul' file immediately on startup (Windows /dev/null artifact)
|
|
13
|
+
function cleanupNulFile() {
|
|
14
|
+
const nulFile = path.join(cwd, 'nul');
|
|
15
|
+
if (fs.existsSync(nulFile)) {
|
|
16
|
+
try {
|
|
17
|
+
fs.unlinkSync(nulFile);
|
|
18
|
+
} catch (e) {
|
|
19
|
+
// Ignore - file might be locked
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
cleanupNulFile();
|
|
24
|
+
|
|
25
|
+
// Reserved Windows device names - never create files with these names
|
|
26
|
+
const WINDOWS_RESERVED = ['CON', 'PRN', 'AUX', 'NUL', 'COM1', 'COM2', 'COM3', 'COM4',
|
|
27
|
+
'COM5', 'COM6', 'COM7', 'COM8', 'COM9', 'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5',
|
|
28
|
+
'LPT6', 'LPT7', 'LPT8', 'LPT9'];
|
|
29
|
+
|
|
30
|
+
// Files/folders installed by autoconfig - don't backup these
|
|
31
|
+
const AUTOCONFIG_FILES = ['commands', 'docs', 'agents', 'migration', 'hooks', 'scripts', 'sounds', 'rules', 'feedback', 'settings.json', 'settings.local.json', '.mcp.json', '.autoconfig-version', '.autoconfig-plugins.json'];
|
|
32
|
+
|
|
33
|
+
function isReservedName(name) {
|
|
34
|
+
const baseName = name.replace(/\.[^.]*$/, '').toUpperCase();
|
|
35
|
+
return WINDOWS_RESERVED.includes(baseName);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function hasUserContent(claudeDir) {
|
|
39
|
+
// Check if .claude/ has any files beyond what autoconfig installs
|
|
40
|
+
if (!fs.existsSync(claudeDir)) return false;
|
|
41
|
+
|
|
42
|
+
const entries = fs.readdirSync(claudeDir);
|
|
43
|
+
for (const entry of entries) {
|
|
44
|
+
if (!AUTOCONFIG_FILES.includes(entry)) {
|
|
45
|
+
// Found something that's not from autoconfig
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function formatTimestamp() {
|
|
53
|
+
const now = new Date();
|
|
54
|
+
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
|
|
55
|
+
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
56
|
+
const month = months[now.getMonth()];
|
|
57
|
+
const day = now.getDate();
|
|
58
|
+
const year = now.getFullYear();
|
|
59
|
+
const hour = now.getHours();
|
|
60
|
+
const min = String(now.getMinutes()).padStart(2, '0');
|
|
61
|
+
const ampm = hour >= 12 ? 'pm' : 'am';
|
|
62
|
+
const hour12 = hour % 12 || 12;
|
|
63
|
+
|
|
64
|
+
return `${month}-${day}-${year}_${hour12}-${min}${ampm}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// --pull-updates: Copy new update files from package to user's project
|
|
68
|
+
function parseAppliedUpdates(filePath) {
|
|
69
|
+
if (!fs.existsSync(filePath)) return [];
|
|
70
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
71
|
+
const match = content.match(/<!-- @applied\r?\n([\s\S]*?)-->/);
|
|
72
|
+
if (!match) return [];
|
|
73
|
+
|
|
74
|
+
return match[1].trim().split('\n')
|
|
75
|
+
.filter(line => line.trim())
|
|
76
|
+
.map(line => {
|
|
77
|
+
const idMatch = line.match(/^(\d{3})/);
|
|
78
|
+
return idMatch ? parseInt(idMatch[1], 10) : 0;
|
|
79
|
+
})
|
|
80
|
+
.filter(id => id > 0);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function getHighestAppliedId(appliedIds) {
|
|
84
|
+
return appliedIds.length > 0 ? Math.max(...appliedIds) : 0;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function pullUpdates() {
|
|
88
|
+
console.log('\x1b[36m%s\x1b[0m', '🔄 Checking for updates...');
|
|
89
|
+
console.log();
|
|
90
|
+
|
|
91
|
+
const userCmdPath = path.join(cwd, '.claude', 'commands', 'autoconfig-update.md');
|
|
92
|
+
const packageCmdPath = path.join(packageDir, '.claude', 'commands', 'autoconfig-update.md');
|
|
93
|
+
const packageUpdatesDir = path.join(packageDir, '.claude', 'updates');
|
|
94
|
+
const userUpdatesDir = path.join(cwd, '.claude', 'updates');
|
|
95
|
+
|
|
96
|
+
// Ensure .claude/commands/ exists
|
|
97
|
+
fs.mkdirSync(path.join(cwd, '.claude', 'commands'), { recursive: true });
|
|
98
|
+
|
|
99
|
+
// Refresh autoconfig-update.md (preserve user's @applied block)
|
|
100
|
+
if (fs.existsSync(packageCmdPath)) {
|
|
101
|
+
if (fs.existsSync(userCmdPath)) {
|
|
102
|
+
const userContent = fs.readFileSync(userCmdPath, 'utf8');
|
|
103
|
+
const packageContent = fs.readFileSync(packageCmdPath, 'utf8');
|
|
104
|
+
const userApplied = userContent.match(/<!-- @applied[\s\S]*?-->/);
|
|
105
|
+
if (userApplied) {
|
|
106
|
+
const merged = packageContent.replace(/<!-- @applied[\s\S]*?-->/, userApplied[0]);
|
|
107
|
+
fs.writeFileSync(userCmdPath, merged);
|
|
108
|
+
} else {
|
|
109
|
+
fs.copyFileSync(packageCmdPath, userCmdPath);
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
fs.copyFileSync(packageCmdPath, userCmdPath);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Check for available updates in package
|
|
117
|
+
if (!fs.existsSync(packageUpdatesDir)) {
|
|
118
|
+
console.log('\x1b[32m%s\x1b[0m', '✅ Already up to date');
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const appliedIds = parseAppliedUpdates(userCmdPath);
|
|
123
|
+
const highestApplied = getHighestAppliedId(appliedIds);
|
|
124
|
+
|
|
125
|
+
const updateFiles = fs.readdirSync(packageUpdatesDir).filter(f => f.endsWith('.md'));
|
|
126
|
+
const newUpdates = updateFiles.filter(file => {
|
|
127
|
+
const match = file.match(/^(\d{3})-/);
|
|
128
|
+
if (!match) return false;
|
|
129
|
+
return parseInt(match[1], 10) > highestApplied;
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
if (newUpdates.length === 0) {
|
|
133
|
+
console.log('\x1b[32m%s\x1b[0m', '✅ Already up to date');
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Copy new update files
|
|
138
|
+
fs.mkdirSync(userUpdatesDir, { recursive: true });
|
|
139
|
+
for (const file of newUpdates) {
|
|
140
|
+
fs.copyFileSync(
|
|
141
|
+
path.join(packageUpdatesDir, file),
|
|
142
|
+
path.join(userUpdatesDir, file)
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
console.log('\x1b[32m%s\x1b[0m', `✅ Copied ${newUpdates.length} new update${newUpdates.length > 1 ? 's' : ''} to .claude/updates/`);
|
|
147
|
+
console.log();
|
|
148
|
+
console.log('Run \x1b[36mclaude /autoconfig-update\x1b[0m to review and install updates.');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (process.argv.includes('--pull-updates')) {
|
|
152
|
+
pullUpdates();
|
|
153
|
+
process.exit(0);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ============================================================================
|
|
157
|
+
// Settings merge helpers (shared by the upgrade path and the plugin installer)
|
|
158
|
+
// ============================================================================
|
|
159
|
+
|
|
160
|
+
// Rewrite legacy cwd-relative hook commands ("node .claude/hooks/X.js") to the
|
|
161
|
+
// ${CLAUDE_PROJECT_DIR:-.}-anchored form IN PLACE, mutating the given settings object.
|
|
162
|
+
// Two reasons, both from the 2026-07-08 stuck-title postmortem:
|
|
163
|
+
// 1. cwd-relative commands go MODULE_NOT_FOUND the moment a session cd's away from the
|
|
164
|
+
// project root — every matching tool call then spews a red hook error until cwd returns.
|
|
165
|
+
// 2. mergeSettingsInto dedups by EXACT command string, so without this rewrite an upgrade
|
|
166
|
+
// would ADD the anchored template entry alongside the user's old relative one -> the
|
|
167
|
+
// hook runs twice per event.
|
|
168
|
+
// Only known autoconfig-managed hook filenames are rewritten; user-authored commands are
|
|
169
|
+
// never touched.
|
|
170
|
+
function migrateLegacyHookCommands(userSettings) {
|
|
171
|
+
if (!userSettings || !userSettings.hooks) return;
|
|
172
|
+
const LEGACY = /^node \.claude\/hooks\/([\w.-]+\.js)$/;
|
|
173
|
+
for (const matchers of Object.values(userSettings.hooks)) {
|
|
174
|
+
if (!Array.isArray(matchers)) continue;
|
|
175
|
+
for (const matcher of matchers) {
|
|
176
|
+
for (const hook of matcher.hooks || []) {
|
|
177
|
+
const m = typeof hook.command === 'string' && hook.command.match(LEGACY);
|
|
178
|
+
if (m) hook.command = 'node "${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/' + m[1] + '"';
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Additively fold a settings fragment (hooks / env / permissions) into an existing
|
|
185
|
+
// settings object, mutating and returning it.
|
|
186
|
+
// - hooks: add hook commands that don't already exist (dedup by command string, per event)
|
|
187
|
+
// - env: add keys the user hasn't set (never overwrite an existing value)
|
|
188
|
+
// - permissions.allow/deny: add missing rules (and migrate deprecated :* syntax)
|
|
189
|
+
function mergeSettingsInto(userSettings, fragment) {
|
|
190
|
+
if (fragment.hooks) {
|
|
191
|
+
if (!userSettings.hooks) userSettings.hooks = {};
|
|
192
|
+
for (const [event, matchers] of Object.entries(fragment.hooks)) {
|
|
193
|
+
if (!userSettings.hooks[event]) {
|
|
194
|
+
userSettings.hooks[event] = matchers;
|
|
195
|
+
} else {
|
|
196
|
+
// Add any hook commands that don't already exist
|
|
197
|
+
for (const matcher of matchers) {
|
|
198
|
+
for (const hook of matcher.hooks || []) {
|
|
199
|
+
const exists = userSettings.hooks[event].some(m =>
|
|
200
|
+
(m.hooks || []).some(h => h.command === hook.command)
|
|
201
|
+
);
|
|
202
|
+
if (!exists) {
|
|
203
|
+
const existingMatcher = userSettings.hooks[event].find(m => m.matcher === matcher.matcher);
|
|
204
|
+
if (existingMatcher) {
|
|
205
|
+
existingMatcher.hooks = existingMatcher.hooks || [];
|
|
206
|
+
existingMatcher.hooks.push(hook);
|
|
207
|
+
} else {
|
|
208
|
+
userSettings.hooks[event].push(matcher);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (fragment.env) {
|
|
218
|
+
if (!userSettings.env) userSettings.env = {};
|
|
219
|
+
for (const [key, value] of Object.entries(fragment.env)) {
|
|
220
|
+
if (!(key in userSettings.env)) userSettings.env[key] = value;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (fragment.permissions) {
|
|
225
|
+
if (!userSettings.permissions) userSettings.permissions = {};
|
|
226
|
+
for (const key of ['allow', 'deny']) {
|
|
227
|
+
if (!fragment.permissions[key]) continue;
|
|
228
|
+
if (!userSettings.permissions[key]) {
|
|
229
|
+
userSettings.permissions[key] = fragment.permissions[key];
|
|
230
|
+
} else {
|
|
231
|
+
// Migrate deprecated :* syntax to space-* in existing entries
|
|
232
|
+
userSettings.permissions[key] = userSettings.permissions[key].map(rule =>
|
|
233
|
+
rule.replace(/^(Bash\([^)]*):(\*\))$/, '$1 $2')
|
|
234
|
+
);
|
|
235
|
+
for (const rule of fragment.permissions[key]) {
|
|
236
|
+
if (!userSettings.permissions[key].includes(rule)) {
|
|
237
|
+
userSettings.permissions[key].push(rule);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return userSettings;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Inverse of mergeSettingsInto: strip a fragment's contributions back out. Only removes
|
|
248
|
+
// what the fragment added (dedup-safe), leaving the user's own entries untouched.
|
|
249
|
+
function unmergeSettingsFrom(userSettings, fragment) {
|
|
250
|
+
if (fragment.hooks && userSettings.hooks) {
|
|
251
|
+
for (const [event, matchers] of Object.entries(fragment.hooks)) {
|
|
252
|
+
if (!userSettings.hooks[event]) continue;
|
|
253
|
+
const commands = new Set();
|
|
254
|
+
for (const matcher of matchers) {
|
|
255
|
+
for (const hook of matcher.hooks || []) {
|
|
256
|
+
if (hook.command) commands.add(hook.command);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
userSettings.hooks[event] = userSettings.hooks[event]
|
|
260
|
+
.map(m => {
|
|
261
|
+
if (m.hooks) m.hooks = m.hooks.filter(h => !commands.has(h.command));
|
|
262
|
+
return m;
|
|
263
|
+
})
|
|
264
|
+
.filter(m => (m.hooks || []).length > 0);
|
|
265
|
+
if (userSettings.hooks[event].length === 0) delete userSettings.hooks[event];
|
|
266
|
+
}
|
|
267
|
+
if (Object.keys(userSettings.hooks).length === 0) delete userSettings.hooks;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (fragment.env && userSettings.env) {
|
|
271
|
+
for (const [key, value] of Object.entries(fragment.env)) {
|
|
272
|
+
if (userSettings.env[key] === value) delete userSettings.env[key];
|
|
273
|
+
}
|
|
274
|
+
if (Object.keys(userSettings.env).length === 0) delete userSettings.env;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (fragment.permissions && userSettings.permissions) {
|
|
278
|
+
for (const key of ['allow', 'deny']) {
|
|
279
|
+
if (!fragment.permissions[key] || !userSettings.permissions[key]) continue;
|
|
280
|
+
const remove = new Set(fragment.permissions[key]);
|
|
281
|
+
userSettings.permissions[key] = userSettings.permissions[key].filter(r => !remove.has(r));
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return userSettings;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ============================================================================
|
|
289
|
+
// Plugin system: install / remove / list drop-in add-on plugins.
|
|
290
|
+
//
|
|
291
|
+
// A plugin is a folder containing a plugin.json manifest:
|
|
292
|
+
// {
|
|
293
|
+
// "name": "terminal-title",
|
|
294
|
+
// "version": "1.0.0",
|
|
295
|
+
// "description": "...",
|
|
296
|
+
// "files": [ { "from": "hooks/x.js", "to": "hooks/x.js" } ], // "to" is relative to <project>/.claude
|
|
297
|
+
// "settings": { "env": {...}, "hooks": {...}, "permissions": {...} } // folded into .claude/settings.json
|
|
298
|
+
// }
|
|
299
|
+
//
|
|
300
|
+
// Installed plugins are tracked in .claude/.autoconfig-plugins.json so `plugin remove`
|
|
301
|
+
// cleanly undoes both the copied files and the settings contributions. The free core
|
|
302
|
+
// ships only this generic loader — paid/closed plugins live and are delivered separately.
|
|
303
|
+
// ============================================================================
|
|
304
|
+
|
|
305
|
+
const PLUGINS_LEDGER = '.autoconfig-plugins.json';
|
|
306
|
+
|
|
307
|
+
function readPluginsLedger(claudeDir) {
|
|
308
|
+
const p = path.join(claudeDir, PLUGINS_LEDGER);
|
|
309
|
+
if (!fs.existsSync(p)) return {};
|
|
310
|
+
try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return {}; }
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function writePluginsLedger(claudeDir, ledger) {
|
|
314
|
+
fs.mkdirSync(claudeDir, { recursive: true });
|
|
315
|
+
fs.writeFileSync(path.join(claudeDir, PLUGINS_LEDGER), JSON.stringify(ledger, null, 2));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function loadManifest(pluginDir) {
|
|
319
|
+
const manifestPath = path.join(pluginDir, 'plugin.json');
|
|
320
|
+
if (!fs.existsSync(manifestPath)) throw new Error(`no plugin.json found in ${pluginDir}`);
|
|
321
|
+
let manifest;
|
|
322
|
+
try {
|
|
323
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
324
|
+
} catch (e) {
|
|
325
|
+
throw new Error(`plugin.json is not valid JSON: ${e.message}`);
|
|
326
|
+
}
|
|
327
|
+
if (!manifest.name || typeof manifest.name !== 'string') {
|
|
328
|
+
throw new Error('plugin.json must declare a string "name"');
|
|
329
|
+
}
|
|
330
|
+
if (manifest.files && !Array.isArray(manifest.files)) {
|
|
331
|
+
throw new Error('plugin.json "files" must be an array');
|
|
332
|
+
}
|
|
333
|
+
if (!manifest.files) manifest.files = [];
|
|
334
|
+
return manifest;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function pluginAdd(pluginArg, claudeDir) {
|
|
338
|
+
const pluginDir = path.resolve(cwd, pluginArg);
|
|
339
|
+
const manifest = loadManifest(pluginDir);
|
|
340
|
+
console.log('\x1b[36m%s\x1b[0m', `📦 Installing plugin: ${manifest.name}${manifest.version ? ' v' + manifest.version : ''}`);
|
|
341
|
+
|
|
342
|
+
// 1. Copy declared files into <project>/.claude/<to>
|
|
343
|
+
const installedFiles = [];
|
|
344
|
+
for (const file of manifest.files) {
|
|
345
|
+
if (!file || !file.from || !file.to) throw new Error('each "files" entry must have "from" and "to"');
|
|
346
|
+
const src = path.resolve(pluginDir, file.from);
|
|
347
|
+
if (!fs.existsSync(src)) throw new Error(`plugin file not found: ${file.from}`);
|
|
348
|
+
const dest = path.join(claudeDir, file.to);
|
|
349
|
+
if (isReservedName(path.basename(dest))) throw new Error(`refusing to write reserved filename: ${file.to}`);
|
|
350
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
351
|
+
fs.copyFileSync(src, dest);
|
|
352
|
+
installedFiles.push(file.to);
|
|
353
|
+
console.log('\x1b[90m%s\x1b[0m', ` + .claude/${file.to}`);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// 2. Fold the settings fragment into .claude/settings.json (clone first to avoid aliasing the ledger copy)
|
|
357
|
+
if (manifest.settings) {
|
|
358
|
+
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
359
|
+
let userSettings = {};
|
|
360
|
+
if (fs.existsSync(settingsPath)) {
|
|
361
|
+
try { userSettings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); } catch { userSettings = {}; }
|
|
362
|
+
}
|
|
363
|
+
mergeSettingsInto(userSettings, JSON.parse(JSON.stringify(manifest.settings)));
|
|
364
|
+
fs.mkdirSync(claudeDir, { recursive: true });
|
|
365
|
+
fs.writeFileSync(settingsPath, JSON.stringify(userSettings, null, 2));
|
|
366
|
+
console.log('\x1b[90m%s\x1b[0m', ' ✎ merged settings.json (hooks / env / permissions)');
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// 3. Record in the ledger so removal can cleanly undo everything
|
|
370
|
+
const ledger = readPluginsLedger(claudeDir);
|
|
371
|
+
ledger[manifest.name] = {
|
|
372
|
+
version: manifest.version || null,
|
|
373
|
+
files: installedFiles,
|
|
374
|
+
settings: manifest.settings || null,
|
|
375
|
+
installedAt: new Date().toISOString()
|
|
376
|
+
};
|
|
377
|
+
writePluginsLedger(claudeDir, ledger);
|
|
378
|
+
console.log('\x1b[32m%s\x1b[0m', `✅ Installed ${manifest.name}`);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function pluginRemove(name, claudeDir) {
|
|
382
|
+
const ledger = readPluginsLedger(claudeDir);
|
|
383
|
+
const entry = ledger[name];
|
|
384
|
+
if (!entry) throw new Error(`plugin "${name}" is not installed`);
|
|
385
|
+
console.log('\x1b[36m%s\x1b[0m', `🗑 Removing plugin: ${name}`);
|
|
386
|
+
|
|
387
|
+
// 1. Delete the files the plugin installed
|
|
388
|
+
for (const rel of entry.files || []) {
|
|
389
|
+
const p = path.join(claudeDir, rel);
|
|
390
|
+
if (fs.existsSync(p)) {
|
|
391
|
+
fs.rmSync(p, { force: true });
|
|
392
|
+
console.log('\x1b[90m%s\x1b[0m', ` - .claude/${rel}`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// 2. Revert the settings contributions
|
|
397
|
+
if (entry.settings) {
|
|
398
|
+
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
399
|
+
if (fs.existsSync(settingsPath)) {
|
|
400
|
+
try {
|
|
401
|
+
const userSettings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
402
|
+
unmergeSettingsFrom(userSettings, entry.settings);
|
|
403
|
+
fs.writeFileSync(settingsPath, JSON.stringify(userSettings, null, 2));
|
|
404
|
+
console.log('\x1b[90m%s\x1b[0m', ' ✎ reverted settings.json contributions');
|
|
405
|
+
} catch { /* leave settings intact if unparsable */ }
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// 3. Drop it from the ledger
|
|
410
|
+
delete ledger[name];
|
|
411
|
+
writePluginsLedger(claudeDir, ledger);
|
|
412
|
+
console.log('\x1b[32m%s\x1b[0m', `✅ Removed ${name}`);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function pluginList(claudeDir) {
|
|
416
|
+
const ledger = readPluginsLedger(claudeDir);
|
|
417
|
+
const names = Object.keys(ledger);
|
|
418
|
+
if (names.length === 0) {
|
|
419
|
+
console.log('\x1b[90m%s\x1b[0m', 'No plugins installed.');
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
console.log('\x1b[36m%s\x1b[0m', 'Installed plugins:');
|
|
423
|
+
for (const name of names) {
|
|
424
|
+
const e = ledger[name];
|
|
425
|
+
const n = (e.files || []).length;
|
|
426
|
+
console.log(` • ${name}${e.version ? ' v' + e.version : ''} (${n} file${n === 1 ? '' : 's'})`);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function runPluginCommand(argv) {
|
|
431
|
+
const sub = argv[3];
|
|
432
|
+
const arg = argv[4];
|
|
433
|
+
const claudeDir = path.join(cwd, '.claude');
|
|
434
|
+
try {
|
|
435
|
+
if (sub === 'add' || sub === 'install') {
|
|
436
|
+
if (!arg) throw new Error('usage: claude-code-autoconfig plugin add <path-to-plugin-dir>');
|
|
437
|
+
pluginAdd(arg, claudeDir);
|
|
438
|
+
} else if (sub === 'remove' || sub === 'rm' || sub === 'uninstall') {
|
|
439
|
+
if (!arg) throw new Error('usage: claude-code-autoconfig plugin remove <name>');
|
|
440
|
+
pluginRemove(arg, claudeDir);
|
|
441
|
+
} else if (sub === 'list' || sub === 'ls') {
|
|
442
|
+
pluginList(claudeDir);
|
|
443
|
+
} else {
|
|
444
|
+
console.log('Usage:');
|
|
445
|
+
console.log(' claude-code-autoconfig plugin add <dir> Install a plugin from a folder');
|
|
446
|
+
console.log(' claude-code-autoconfig plugin remove <name> Uninstall a plugin');
|
|
447
|
+
console.log(' claude-code-autoconfig plugin list List installed plugins');
|
|
448
|
+
process.exit(sub ? 1 : 0);
|
|
449
|
+
}
|
|
450
|
+
} catch (err) {
|
|
451
|
+
console.log('\x1b[31m%s\x1b[0m', `❌ ${err.message}`);
|
|
452
|
+
process.exit(1);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (process.argv[2] === 'plugin') {
|
|
457
|
+
runPluginCommand(process.argv);
|
|
458
|
+
process.exit(0);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const forceMode = process.argv.includes('--force');
|
|
462
|
+
|
|
463
|
+
// Detect if running inside Claude Code session.
|
|
464
|
+
// CLAUDECODE=1 alone is not reliable: env vars inherit into every descendant
|
|
465
|
+
// process (e.g. VS Code launched from a Claude session, and any terminal it
|
|
466
|
+
// spawns). A genuine in-agent run has piped stdio (isTTY falsy), while a human
|
|
467
|
+
// terminal has a real TTY — so require both signals before blocking.
|
|
468
|
+
const insideClaude = process.env.CLAUDECODE === '1' && !process.stdout.isTTY;
|
|
469
|
+
|
|
470
|
+
console.log('\x1b[36m%s\x1b[0m', '🚀 Claude Code Autoconfig');
|
|
471
|
+
console.log();
|
|
472
|
+
|
|
473
|
+
// Block early if running inside Claude Code (unless --bootstrap)
|
|
474
|
+
if (insideClaude && !process.argv.includes('--bootstrap')) {
|
|
475
|
+
console.log('\x1b[31m%s\x1b[0m', '● The tool needs to be run from a regular terminal, not from within Claude Code.');
|
|
476
|
+
console.log();
|
|
477
|
+
console.log(' Open a separate terminal window and run:');
|
|
478
|
+
console.log();
|
|
479
|
+
console.log(' \x1b[36mnpx claude-code-autoconfig@latest\x1b[0m');
|
|
480
|
+
console.log();
|
|
481
|
+
process.exit(0);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Step 1: Check if Claude Code is installed
|
|
485
|
+
function isClaudeInstalled() {
|
|
486
|
+
try {
|
|
487
|
+
execSync('claude --version', { stdio: 'ignore' });
|
|
488
|
+
return true;
|
|
489
|
+
} catch {
|
|
490
|
+
return false;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function installClaude() {
|
|
495
|
+
console.log('\x1b[33m%s\x1b[0m', '⚠️ Claude Code not found. Installing...');
|
|
496
|
+
console.log();
|
|
497
|
+
try {
|
|
498
|
+
execSync('npm install -g @anthropic-ai/claude-code', { stdio: 'inherit' });
|
|
499
|
+
console.log();
|
|
500
|
+
console.log('\x1b[32m%s\x1b[0m', '✅ Claude Code installed');
|
|
501
|
+
return true;
|
|
502
|
+
} catch (err) {
|
|
503
|
+
console.log('\x1b[31m%s\x1b[0m', '❌ Failed to install Claude Code');
|
|
504
|
+
console.log(' Install manually: npm install -g @anthropic-ai/claude-code');
|
|
505
|
+
return false;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (!isClaudeInstalled()) {
|
|
510
|
+
if (!installClaude()) {
|
|
511
|
+
process.exit(1);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
console.log('\x1b[32m%s\x1b[0m', '✅ Claude Code detected');
|
|
516
|
+
|
|
517
|
+
// Step 2: Backup existing .claude/ if it has user content
|
|
518
|
+
const claudeDest = path.join(cwd, '.claude');
|
|
519
|
+
const SKIP_BACKUP = ['migration']; // Don't backup the migration folder itself
|
|
520
|
+
let migrationPath = null;
|
|
521
|
+
|
|
522
|
+
// Diagnostic: log pre-install state
|
|
523
|
+
console.log();
|
|
524
|
+
console.log('\x1b[90m%s\x1b[0m', '── Pre-install state ──');
|
|
525
|
+
console.log('\x1b[90m%s\x1b[0m', ` Working dir: ${cwd}`);
|
|
526
|
+
const claudeMdExists = fs.existsSync(path.join(cwd, 'CLAUDE.md'));
|
|
527
|
+
console.log('\x1b[90m%s\x1b[0m', ` CLAUDE.md: ${claudeMdExists ? 'exists' : 'not found'}`);
|
|
528
|
+
if (claudeMdExists) {
|
|
529
|
+
const claudeMdContent = fs.readFileSync(path.join(cwd, 'CLAUDE.md'), 'utf8');
|
|
530
|
+
const hasMarker = claudeMdContent.includes('AUTO-GENERATED BY /autoconfig');
|
|
531
|
+
console.log('\x1b[90m%s\x1b[0m', ` CLAUDE.md autoconfig marker: ${hasMarker ? 'yes' : 'no'}`);
|
|
532
|
+
}
|
|
533
|
+
if (fs.existsSync(claudeDest)) {
|
|
534
|
+
const entries = fs.readdirSync(claudeDest);
|
|
535
|
+
console.log('\x1b[90m%s\x1b[0m', ` .claude/ exists: yes (${entries.length} entries)`);
|
|
536
|
+
for (const e of entries) {
|
|
537
|
+
const isAutoconfig = AUTOCONFIG_FILES.includes(e);
|
|
538
|
+
console.log('\x1b[90m%s\x1b[0m', ` ${isAutoconfig ? '·' : '▸'} ${e}${isAutoconfig ? '' : ' (user content)'}`);
|
|
539
|
+
}
|
|
540
|
+
const docsHtml = path.join(claudeDest, 'docs', 'autoconfig.docs.html');
|
|
541
|
+
console.log('\x1b[90m%s\x1b[0m', ` autoconfig.docs.html: ${fs.existsSync(docsHtml) ? 'exists' : 'not found'}`);
|
|
542
|
+
} else {
|
|
543
|
+
console.log('\x1b[90m%s\x1b[0m', ' .claude/ exists: no');
|
|
544
|
+
}
|
|
545
|
+
console.log('\x1b[90m%s\x1b[0m', '───────────────────────');
|
|
546
|
+
console.log();
|
|
547
|
+
|
|
548
|
+
function copyDirForBackup(src, dest) {
|
|
549
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
550
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
551
|
+
|
|
552
|
+
for (const entry of entries) {
|
|
553
|
+
if (SKIP_BACKUP.includes(entry.name)) continue;
|
|
554
|
+
if (AUTOCONFIG_FILES.includes(entry.name)) continue; // Skip autoconfig-installed files
|
|
555
|
+
if (isReservedName(entry.name)) continue;
|
|
556
|
+
|
|
557
|
+
const srcPath = path.join(src, entry.name);
|
|
558
|
+
const destPath = path.join(dest, entry.name);
|
|
559
|
+
|
|
560
|
+
if (entry.isDirectory()) {
|
|
561
|
+
copyDirForBackup(srcPath, destPath);
|
|
562
|
+
} else {
|
|
563
|
+
fs.copyFileSync(srcPath, destPath);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function collectFiles(dir, prefix = '') {
|
|
569
|
+
const files = [];
|
|
570
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
571
|
+
for (const entry of entries) {
|
|
572
|
+
const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
573
|
+
if (entry.isDirectory()) {
|
|
574
|
+
files.push(...collectFiles(path.join(dir, entry.name), relPath));
|
|
575
|
+
} else {
|
|
576
|
+
files.push(relPath);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return files;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
if (fs.existsSync(claudeDest) && hasUserContent(claudeDest)) {
|
|
583
|
+
const userEntries = fs.readdirSync(claudeDest).filter(e =>
|
|
584
|
+
e !== 'migration' && !AUTOCONFIG_FILES.includes(e)
|
|
585
|
+
);
|
|
586
|
+
console.log('\x1b[90m%s\x1b[0m', ` Backup triggered by user content: ${userEntries.join(', ')}`);
|
|
587
|
+
|
|
588
|
+
const timestamp = formatTimestamp();
|
|
589
|
+
const migrationDir = path.join(claudeDest, 'migration');
|
|
590
|
+
migrationPath = path.join(migrationDir, timestamp);
|
|
591
|
+
|
|
592
|
+
fs.mkdirSync(migrationPath, { recursive: true });
|
|
593
|
+
|
|
594
|
+
// Copy user files to backup (excluding autoconfig-installed files)
|
|
595
|
+
for (const entry of userEntries) {
|
|
596
|
+
const srcPath = path.join(claudeDest, entry);
|
|
597
|
+
const destPath = path.join(migrationPath, entry);
|
|
598
|
+
|
|
599
|
+
if (fs.statSync(srcPath).isDirectory()) {
|
|
600
|
+
copyDirForBackup(srcPath, destPath);
|
|
601
|
+
} else {
|
|
602
|
+
fs.copyFileSync(srcPath, destPath);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// Collect backed up files for metadata
|
|
607
|
+
const backedUpFiles = collectFiles(migrationPath);
|
|
608
|
+
|
|
609
|
+
if (backedUpFiles.length > 0) {
|
|
610
|
+
// Write latest.json for the guide
|
|
611
|
+
fs.writeFileSync(path.join(migrationDir, 'latest.json'), JSON.stringify({
|
|
612
|
+
timestamp: timestamp,
|
|
613
|
+
backedUpFiles: backedUpFiles
|
|
614
|
+
}, null, 2));
|
|
615
|
+
|
|
616
|
+
// Create README inside the dated backup folder
|
|
617
|
+
const backupReadme = `# Migration Backup: ${timestamp}
|
|
618
|
+
|
|
619
|
+
This folder contains a backup of your previous .claude/ configuration.
|
|
620
|
+
|
|
621
|
+
## Why This Backup Exists
|
|
622
|
+
|
|
623
|
+
You ran \`npx claude-code-autoconfig\` on a project that already had Claude Code configured.
|
|
624
|
+
Your previous files were backed up here before the new configuration was applied.
|
|
625
|
+
|
|
626
|
+
## Backed Up Files
|
|
627
|
+
|
|
628
|
+
${backedUpFiles.map(f => `- ${f}`).join('\n')}
|
|
629
|
+
|
|
630
|
+
## Restoring Files
|
|
631
|
+
|
|
632
|
+
To restore any file, copy it from this folder back to \`.claude/\`.
|
|
633
|
+
|
|
634
|
+
For example:
|
|
635
|
+
\`\`\`bash
|
|
636
|
+
cp .claude/migration/${timestamp}/settings.json .claude/settings.json
|
|
637
|
+
\`\`\`
|
|
638
|
+
`;
|
|
639
|
+
fs.writeFileSync(path.join(migrationPath, 'README.md'), backupReadme);
|
|
640
|
+
|
|
641
|
+
console.log('\x1b[33m%s\x1b[0m', `⚠️ Backed up existing config to .claude/migration/${timestamp}/`);
|
|
642
|
+
} else {
|
|
643
|
+
// No user files to backup, remove the empty migration folder
|
|
644
|
+
fs.rmdirSync(migrationPath, { recursive: true });
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Read previous installed version (before copying overwrites it)
|
|
649
|
+
const versionFile = path.join(claudeDest, '.autoconfig-version');
|
|
650
|
+
const previousVersion = fs.existsSync(versionFile)
|
|
651
|
+
? fs.readFileSync(versionFile, 'utf8').trim()
|
|
652
|
+
: null;
|
|
653
|
+
const currentVersion = require(path.join(packageDir, 'package.json')).version;
|
|
654
|
+
|
|
655
|
+
// Detect upgrade vs fresh install (must run BEFORE copying files)
|
|
656
|
+
const isUpgrade = (() => {
|
|
657
|
+
// Indicator 1: CLAUDE.md has autoconfig marker
|
|
658
|
+
const claudeMdPath = path.join(cwd, 'CLAUDE.md');
|
|
659
|
+
if (fs.existsSync(claudeMdPath)) {
|
|
660
|
+
const content = fs.readFileSync(claudeMdPath, 'utf8');
|
|
661
|
+
if (content.includes('AUTO-GENERATED BY /autoconfig')) {
|
|
662
|
+
console.log('\x1b[90m%s\x1b[0m', ' Upgrade detected: CLAUDE.md has autoconfig marker');
|
|
663
|
+
return true;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
// Indicator 2: docs HTML exists (unique autoconfig artifact)
|
|
667
|
+
const docsPath = path.join(claudeDest, 'docs', 'autoconfig.docs.html');
|
|
668
|
+
if (fs.existsSync(docsPath)) {
|
|
669
|
+
console.log('\x1b[90m%s\x1b[0m', ' Upgrade detected: autoconfig.docs.html exists');
|
|
670
|
+
return true;
|
|
671
|
+
}
|
|
672
|
+
console.log('\x1b[90m%s\x1b[0m', ' Install type: fresh (no previous autoconfig found)');
|
|
673
|
+
return false;
|
|
674
|
+
})();
|
|
675
|
+
|
|
676
|
+
// Step 3: Copy minimal bootstrap (commands/, docs/, agents/, feedback/, hooks/)
|
|
677
|
+
const commandsSrc = path.join(packageDir, '.claude', 'commands');
|
|
678
|
+
const docsSrc = path.join(packageDir, '.claude', 'docs');
|
|
679
|
+
const agentsSrc = path.join(packageDir, '.claude', 'agents');
|
|
680
|
+
const feedbackSrc = path.join(packageDir, '.claude', 'feedback');
|
|
681
|
+
const hooksSrc = path.join(packageDir, '.claude', 'hooks');
|
|
682
|
+
const scriptsSrc = path.join(packageDir, '.claude', 'scripts');
|
|
683
|
+
|
|
684
|
+
// Files that exist in the dev repo but should never be installed to user projects
|
|
685
|
+
const DEV_ONLY_FILES = ['deploy-to-npmjs.md'];
|
|
686
|
+
|
|
687
|
+
function copyDir(src, dest) {
|
|
688
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
689
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
690
|
+
|
|
691
|
+
for (const entry of entries) {
|
|
692
|
+
if (isReservedName(entry.name)) continue;
|
|
693
|
+
if (DEV_ONLY_FILES.includes(entry.name)) continue;
|
|
694
|
+
|
|
695
|
+
const srcPath = path.join(src, entry.name);
|
|
696
|
+
const destPath = path.join(dest, entry.name);
|
|
697
|
+
|
|
698
|
+
if (entry.isDirectory()) {
|
|
699
|
+
copyDir(srcPath, destPath);
|
|
700
|
+
} else {
|
|
701
|
+
fs.copyFileSync(srcPath, destPath);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function copyDirIfMissing(src, dest) {
|
|
707
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
708
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
709
|
+
for (const entry of entries) {
|
|
710
|
+
if (isReservedName(entry.name)) continue;
|
|
711
|
+
if (DEV_ONLY_FILES.includes(entry.name)) continue;
|
|
712
|
+
const srcPath = path.join(src, entry.name);
|
|
713
|
+
const destPath = path.join(dest, entry.name);
|
|
714
|
+
if (entry.isDirectory()) {
|
|
715
|
+
copyDirIfMissing(srcPath, destPath);
|
|
716
|
+
} else if (!fs.existsSync(destPath)) {
|
|
717
|
+
fs.copyFileSync(srcPath, destPath);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// Parse @version from command file content
|
|
723
|
+
function parseCommandVersion(content) {
|
|
724
|
+
const match = content.match(/<!-- @version (\d+) -->/);
|
|
725
|
+
return match ? parseInt(match[1], 10) : 0;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// Track what commands are new/updated for summary
|
|
729
|
+
const commandsDest = path.join(claudeDest, 'commands');
|
|
730
|
+
const existingCommandContents = new Map();
|
|
731
|
+
if (fs.existsSync(commandsDest)) {
|
|
732
|
+
for (const f of fs.readdirSync(commandsDest).filter(f => f.endsWith('.md'))) {
|
|
733
|
+
existingCommandContents.set(f, fs.readFileSync(path.join(commandsDest, f), 'utf8'));
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// Copy commands (required for /autoconfig to work)
|
|
738
|
+
// Preserve user's saved @screenshotDir in gls.md across upgrades
|
|
739
|
+
const glsDest = path.join(claudeDest, 'commands', 'gls.md');
|
|
740
|
+
let savedScreenshotDir = null;
|
|
741
|
+
if (fs.existsSync(glsDest)) {
|
|
742
|
+
const firstLine = fs.readFileSync(glsDest, 'utf8').split(/\r?\n/)[0];
|
|
743
|
+
const match = firstLine.match(/<!-- @screenshotDir (.+?) -->/);
|
|
744
|
+
if (match) savedScreenshotDir = match[1].trim();
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
if (fs.existsSync(commandsSrc)) {
|
|
748
|
+
copyDir(commandsSrc, path.join(claudeDest, 'commands'));
|
|
749
|
+
} else {
|
|
750
|
+
console.log('\x1b[31m%s\x1b[0m', '❌ Error: commands directory not found');
|
|
751
|
+
process.exit(1);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// Detect new and updated commands (with version tracking)
|
|
755
|
+
const newCommands = [];
|
|
756
|
+
const updatedCommands = []; // { file, oldVersion, newVersion }
|
|
757
|
+
for (const f of fs.readdirSync(commandsDest).filter(f => f.endsWith('.md') && !DEV_ONLY_FILES.includes(f))) {
|
|
758
|
+
const newContent = fs.readFileSync(path.join(commandsDest, f), 'utf8');
|
|
759
|
+
if (!existingCommandContents.has(f)) {
|
|
760
|
+
newCommands.push({ file: f, version: parseCommandVersion(newContent) });
|
|
761
|
+
} else if (newContent !== existingCommandContents.get(f)) {
|
|
762
|
+
const oldVersion = parseCommandVersion(existingCommandContents.get(f));
|
|
763
|
+
const newVersion = parseCommandVersion(newContent);
|
|
764
|
+
updatedCommands.push({ file: f, oldVersion, newVersion });
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// Restore saved screenshot dir after commands overwrite
|
|
769
|
+
if (savedScreenshotDir && fs.existsSync(glsDest)) {
|
|
770
|
+
const content = fs.readFileSync(glsDest, 'utf8');
|
|
771
|
+
fs.writeFileSync(glsDest, content.replace(
|
|
772
|
+
/<!-- @screenshotDir\s*-->/,
|
|
773
|
+
`<!-- @screenshotDir ${savedScreenshotDir} -->`
|
|
774
|
+
));
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// Copy docs (only .html files — skip internal planning docs)
|
|
778
|
+
if (fs.existsSync(docsSrc)) {
|
|
779
|
+
const docsDestDir = path.join(claudeDest, 'docs');
|
|
780
|
+
fs.mkdirSync(docsDestDir, { recursive: true });
|
|
781
|
+
for (const file of fs.readdirSync(docsSrc)) {
|
|
782
|
+
if (file.endsWith('.html')) {
|
|
783
|
+
fs.copyFileSync(path.join(docsSrc, file), path.join(docsDestDir, file));
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// Copy agents if exists
|
|
789
|
+
if (fs.existsSync(agentsSrc)) {
|
|
790
|
+
copyDir(agentsSrc, path.join(claudeDest, 'agents'));
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// Copy feedback template (preserve user customizations unless --force)
|
|
794
|
+
if (fs.existsSync(feedbackSrc)) {
|
|
795
|
+
const copyFn = forceMode ? copyDir : copyDirIfMissing;
|
|
796
|
+
copyFn(feedbackSrc, path.join(claudeDest, 'feedback'));
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// Copy hooks directory. Genuinely user-authorable hooks are preserved on upgrade
|
|
800
|
+
// (copyDirIfMissing), BUT the cca-managed title-hook files are ALWAYS refreshed so bug-fixes
|
|
801
|
+
// reach existing installs — without this, copyDirIfMissing leaves stale hooks in place forever
|
|
802
|
+
// (same always-overwrite rationale as scripts/ below). --force already overwrites everything.
|
|
803
|
+
const MANAGED_HOOKS = ['terminal-title.js', 'terminal-title.directive.md', 'arcade-beeps.js'];
|
|
804
|
+
if (fs.existsSync(hooksSrc)) {
|
|
805
|
+
const copyFn = forceMode ? copyDir : copyDirIfMissing;
|
|
806
|
+
copyFn(hooksSrc, path.join(claudeDest, 'hooks'));
|
|
807
|
+
if (!forceMode) {
|
|
808
|
+
const hooksDestDir = path.join(claudeDest, 'hooks');
|
|
809
|
+
for (const name of MANAGED_HOOKS) {
|
|
810
|
+
const src = path.join(hooksSrc, name);
|
|
811
|
+
if (fs.existsSync(src)) fs.copyFileSync(src, path.join(hooksDestDir, name));
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// Copy scripts directory (always overwrite — these are utility scripts, not user-customizable)
|
|
817
|
+
if (fs.existsSync(scriptsSrc)) {
|
|
818
|
+
copyDir(scriptsSrc, path.join(claudeDest, 'scripts'));
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// Copy sounds directory (binary status-cue assets for arcade-beeps; always overwrite)
|
|
822
|
+
const soundsSrc = path.join(packageDir, '.claude', 'sounds');
|
|
823
|
+
if (fs.existsSync(soundsSrc)) {
|
|
824
|
+
copyDir(soundsSrc, path.join(claudeDest, 'sounds'));
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// Note: updates directory is no longer copied to user projects.
|
|
828
|
+
// Update files are only used by --pull-updates (for /autoconfig-update).
|
|
829
|
+
// On fresh install, all updates are pre-marked as applied and the content
|
|
830
|
+
// is already baked into /autoconfig itself, so the files are unnecessary.
|
|
831
|
+
|
|
832
|
+
// Copy settings.json — fresh install gets full copy, upgrades get hooks + permissions merged
|
|
833
|
+
const settingsSrc = path.join(packageDir, '.claude', 'settings.json');
|
|
834
|
+
const settingsDest = path.join(claudeDest, 'settings.json');
|
|
835
|
+
if (fs.existsSync(settingsSrc)) {
|
|
836
|
+
if (forceMode || !fs.existsSync(settingsDest)) {
|
|
837
|
+
fs.copyFileSync(settingsSrc, settingsDest);
|
|
838
|
+
} else {
|
|
839
|
+
// Merge hooks and permissions from package into existing settings
|
|
840
|
+
try {
|
|
841
|
+
const pkgSettings = JSON.parse(fs.readFileSync(settingsSrc, 'utf8'));
|
|
842
|
+
const userSettings = JSON.parse(fs.readFileSync(settingsDest, 'utf8'));
|
|
843
|
+
|
|
844
|
+
// Upgrade legacy relative hook commands FIRST so the anchored template entries below
|
|
845
|
+
// dedupe against them instead of doubling up (see migrateLegacyHookCommands).
|
|
846
|
+
migrateLegacyHookCommands(userSettings);
|
|
847
|
+
|
|
848
|
+
// Additively fold package hooks/env/permissions into the user's settings
|
|
849
|
+
// (shared with the plugin installer — see mergeSettingsInto).
|
|
850
|
+
mergeSettingsInto(userSettings, pkgSettings);
|
|
851
|
+
|
|
852
|
+
fs.writeFileSync(settingsDest, JSON.stringify(userSettings, null, 2));
|
|
853
|
+
} catch (err) {
|
|
854
|
+
// If merge fails, don't break the install
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
console.log('\x1b[32m%s\x1b[0m', '✅ Prepared /autoconfig command');
|
|
860
|
+
|
|
861
|
+
// Show what was installed/updated
|
|
862
|
+
if (isUpgrade && (newCommands.length > 0 || updatedCommands.length > 0)) {
|
|
863
|
+
console.log();
|
|
864
|
+
for (const { file, version } of newCommands) {
|
|
865
|
+
const name = file.replace('.md', '');
|
|
866
|
+
const ver = version > 0 ? ` v${version}` : '';
|
|
867
|
+
console.log('\x1b[36m%s\x1b[0m', ` + /${name}${ver} (new)`);
|
|
868
|
+
}
|
|
869
|
+
for (const { file, oldVersion, newVersion } of updatedCommands) {
|
|
870
|
+
if (oldVersion > 0 && newVersion > 0 && oldVersion === newVersion) continue;
|
|
871
|
+
const name = file.replace('.md', '');
|
|
872
|
+
const ver = (oldVersion > 0 && newVersion > 0) ? ` (v${oldVersion} → v${newVersion})` : ' (updated)';
|
|
873
|
+
console.log('\x1b[33m%s\x1b[0m', ` ↑ /${name}${ver}`);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
|
|
878
|
+
// Pre-mark all bundled updates as applied when the @applied block is empty.
|
|
879
|
+
// On fresh installs, /autoconfig handles their content (e.g., debug methodology in MEMORY.md).
|
|
880
|
+
// On upgrades from pre-update-system versions, these updates are already baked in.
|
|
881
|
+
// The regex only matches an empty @applied block, so this is safe to run unconditionally.
|
|
882
|
+
{
|
|
883
|
+
const userCmdPath = path.join(claudeDest, 'commands', 'autoconfig-update.md');
|
|
884
|
+
const packageUpdatesDir = path.join(packageDir, '.claude', 'updates');
|
|
885
|
+
if (fs.existsSync(userCmdPath) && fs.existsSync(packageUpdatesDir)) {
|
|
886
|
+
const updateFiles = fs.readdirSync(packageUpdatesDir)
|
|
887
|
+
.filter(f => f.endsWith('.md') && /^\d{3}-/.test(f))
|
|
888
|
+
.sort();
|
|
889
|
+
if (updateFiles.length > 0) {
|
|
890
|
+
const appliedLines = updateFiles.map(file => {
|
|
891
|
+
const id = file.match(/^(\d{3})-/)[1];
|
|
892
|
+
const content = fs.readFileSync(path.join(packageUpdatesDir, file), 'utf8');
|
|
893
|
+
const titleMatch = content.match(/<!-- @title (.+?) -->/);
|
|
894
|
+
const title = titleMatch ? titleMatch[1] : file.replace(/^\d{3}-/, '').replace(/\.md$/, '');
|
|
895
|
+
return `${id} - ${title}`;
|
|
896
|
+
});
|
|
897
|
+
const cmdContent = fs.readFileSync(userCmdPath, 'utf8');
|
|
898
|
+
const updated = cmdContent.replace(
|
|
899
|
+
/<!-- @applied\r?\n-->/,
|
|
900
|
+
`<!-- @applied\n${appliedLines.join('\n')}\n-->`
|
|
901
|
+
);
|
|
902
|
+
fs.writeFileSync(userCmdPath, updated);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// Clean up updates directory — updates are tracked in the @applied block,
|
|
908
|
+
// so the .md files don't need to stay in the user's project
|
|
909
|
+
const userUpdatesDir = path.join(claudeDest, 'updates');
|
|
910
|
+
if (fs.existsSync(userUpdatesDir)) {
|
|
911
|
+
fs.rmSync(userUpdatesDir, { recursive: true });
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
// Migrate FEEDBACK.md content to CLAUDE.md Discoveries section (one-time, on upgrade)
|
|
915
|
+
if (isUpgrade) {
|
|
916
|
+
const claudeMdPath = path.join(cwd, 'CLAUDE.md');
|
|
917
|
+
const feedbackPath = path.join(claudeDest, 'feedback', 'FEEDBACK.md');
|
|
918
|
+
|
|
919
|
+
if (fs.existsSync(claudeMdPath) && fs.existsSync(feedbackPath)) {
|
|
920
|
+
const feedbackContent = fs.readFileSync(feedbackPath, 'utf8');
|
|
921
|
+
|
|
922
|
+
// Extract custom content (everything after the first --- separator following the header)
|
|
923
|
+
const feedbackLines = feedbackContent.split(/\r?\n/);
|
|
924
|
+
let firstSeparatorIdx = -1;
|
|
925
|
+
for (let i = 0; i < feedbackLines.length; i++) {
|
|
926
|
+
if (feedbackLines[i].trim() === '---') {
|
|
927
|
+
firstSeparatorIdx = i;
|
|
928
|
+
break;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
if (firstSeparatorIdx >= 0) {
|
|
933
|
+
const customContent = feedbackLines.slice(firstSeparatorIdx + 1).join('\n').trim();
|
|
934
|
+
|
|
935
|
+
// Only migrate if there's custom content and it hasn't already been migrated
|
|
936
|
+
const claudeMdContent = fs.readFileSync(claudeMdPath, 'utf8');
|
|
937
|
+
const hasDiscoveries = claudeMdContent.includes('## Discoveries');
|
|
938
|
+
|
|
939
|
+
if (customContent.length > 0 && !hasDiscoveries) {
|
|
940
|
+
// Add Discoveries section to CLAUDE.md
|
|
941
|
+
const discoveriesSection = `\n\n## Discoveries\n<!-- Claude: append project-specific learnings, gotchas, and context below. This section persists across /autoconfig runs. -->\n\n${customContent}\n`;
|
|
942
|
+
fs.writeFileSync(claudeMdPath, claudeMdContent + discoveriesSection);
|
|
943
|
+
|
|
944
|
+
// Reset FEEDBACK.md to clean template
|
|
945
|
+
const cleanTemplate = `<!-- @description Human-authored corrections and guidance for Claude. Reserved for team feedback only — Claude must not write here. This directory persists across /autoconfig runs. -->\n\n# Team Feedback\n\n**This file is for human-authored corrections and guidance only.**\nClaude reads this file but must never write to it. When Claude discovers project context, gotchas, or learnings, it should append to the \`## Discoveries\` section in CLAUDE.md instead.\n\n---\n\n`;
|
|
946
|
+
fs.writeFileSync(feedbackPath, cleanTemplate);
|
|
947
|
+
|
|
948
|
+
// Count migrated sections
|
|
949
|
+
const sectionCount = (customContent.match(/^## /gm) || []).length || 1;
|
|
950
|
+
console.log('\x1b[36m%s\x1b[0m', ` 📋 Migrated ${sectionCount} section${sectionCount > 1 ? 's' : ''} from FEEDBACK.md → CLAUDE.md Discoveries`);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// Write current version marker
|
|
957
|
+
fs.writeFileSync(versionFile, currentVersion);
|
|
958
|
+
|
|
959
|
+
const launchCommand = isUpgrade ? '/autoconfig-update' : '/autoconfig';
|
|
960
|
+
|
|
961
|
+
// --bootstrap: copy files only, exit silently (used by /autoconfig inside Claude)
|
|
962
|
+
const bootstrapMode = process.argv.includes('--bootstrap');
|
|
963
|
+
if (bootstrapMode) {
|
|
964
|
+
process.exit(0);
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// Step 4: Show "READY" message
|
|
968
|
+
console.log();
|
|
969
|
+
if (isUpgrade) {
|
|
970
|
+
console.log('\x1b[33m╔════════════════════════════════════════════╗\x1b[0m');
|
|
971
|
+
console.log('\x1b[33m║ ║\x1b[0m');
|
|
972
|
+
console.log('\x1b[33m║\x1b[0m \x1b[33;1mREADY TO UPDATE\x1b[0m \x1b[33m║\x1b[0m');
|
|
973
|
+
console.log('\x1b[33m║ ║\x1b[0m');
|
|
974
|
+
console.log('\x1b[33m║\x1b[0m \x1b[36mPress ENTER to launch Claude and\x1b[0m \x1b[33m║\x1b[0m');
|
|
975
|
+
console.log('\x1b[33m║\x1b[0m \x1b[36mauto-run /autoconfig-update\x1b[0m \x1b[33m║\x1b[0m');
|
|
976
|
+
console.log('\x1b[33m║ ║\x1b[0m');
|
|
977
|
+
console.log('\x1b[33m╚════════════════════════════════════════════╝\x1b[0m');
|
|
978
|
+
} else {
|
|
979
|
+
console.log('\x1b[33m╔════════════════════════════════════════════╗\x1b[0m');
|
|
980
|
+
console.log('\x1b[33m║ ║\x1b[0m');
|
|
981
|
+
console.log('\x1b[33m║\x1b[0m \x1b[33;1mREADY TO CONFIGURE\x1b[0m \x1b[33m║\x1b[0m');
|
|
982
|
+
console.log('\x1b[33m║ ║\x1b[0m');
|
|
983
|
+
console.log('\x1b[33m║\x1b[0m \x1b[36mPress ENTER to launch Claude and\x1b[0m \x1b[33m║\x1b[0m');
|
|
984
|
+
console.log('\x1b[33m║\x1b[0m \x1b[36mauto-run /autoconfig\x1b[0m \x1b[33m║\x1b[0m');
|
|
985
|
+
console.log('\x1b[33m║ ║\x1b[0m');
|
|
986
|
+
console.log('\x1b[33m╚════════════════════════════════════════════╝\x1b[0m');
|
|
987
|
+
}
|
|
988
|
+
// Show what changed on the upgrade path so a re-run never looks like "nothing came down":
|
|
989
|
+
// grouped features/fixes since the installed version, or a single confirmation line when
|
|
990
|
+
// already on the latest. Rendered here so it lands right before the ENTER prompt.
|
|
991
|
+
// Logic lives in update-summary.js (pure + unit-tested).
|
|
992
|
+
if (isUpgrade) {
|
|
993
|
+
const changelogPath = path.join(packageDir, 'CHANGELOG.md');
|
|
994
|
+
const changelogText = fs.existsSync(changelogPath) ? fs.readFileSync(changelogPath, 'utf8') : '';
|
|
995
|
+
console.log();
|
|
996
|
+
for (const seg of formatUpdateSummary(previousVersion, currentVersion, changelogText)) {
|
|
997
|
+
if (seg.kind === 'latest') console.log(`\x1b[32m ✓ ${seg.text}\x1b[0m`);
|
|
998
|
+
else if (seg.kind === 'heading') console.log(`\x1b[36m ${seg.text}\x1b[0m`);
|
|
999
|
+
else if (seg.kind === 'group') console.log(`\x1b[33m ${seg.text}:\x1b[0m`);
|
|
1000
|
+
else if (seg.kind === 'item') console.log(`\x1b[90m • ${seg.text}\x1b[0m`);
|
|
1001
|
+
else if (seg.kind === 'more') console.log(`\x1b[90m ${seg.text}\x1b[0m`);
|
|
1002
|
+
}
|
|
1003
|
+
console.log();
|
|
1004
|
+
}
|
|
1005
|
+
if (!isUpgrade) {
|
|
1006
|
+
console.log('\x1b[90m%s\x1b[0m', "You'll need to approve a few file prompts to complete the installation.");
|
|
1007
|
+
console.log();
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
// Step 5: Wait for Enter, then launch Claude Code
|
|
1011
|
+
const rl = readline.createInterface({
|
|
1012
|
+
input: process.stdin,
|
|
1013
|
+
output: process.stdout
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
rl.question('\x1b[90mPress ENTER to continue...\x1b[0m', () => {
|
|
1017
|
+
rl.close();
|
|
1018
|
+
|
|
1019
|
+
console.log();
|
|
1020
|
+
console.log('\x1b[36m%s\x1b[0m', `🚀 Launching Claude Code with ${launchCommand}...`);
|
|
1021
|
+
console.log();
|
|
1022
|
+
console.log('\x1b[90m%s\x1b[0m', ' Heads up: Claude Code can take 30+ seconds to initialize.');
|
|
1023
|
+
console.log('\x1b[90m%s\x1b[0m', ' Please be patient while it loads.');
|
|
1024
|
+
console.log();
|
|
1025
|
+
|
|
1026
|
+
// Spawn claude with the appropriate command
|
|
1027
|
+
const claude = spawn('claude', [launchCommand], {
|
|
1028
|
+
cwd: cwd,
|
|
1029
|
+
stdio: 'inherit',
|
|
1030
|
+
shell: true
|
|
1031
|
+
});
|
|
1032
|
+
|
|
1033
|
+
claude.on('error', (err) => {
|
|
1034
|
+
console.log('\x1b[31m%s\x1b[0m', '❌ Failed to launch Claude Code');
|
|
1035
|
+
console.log(` Run "claude" manually, then run ${launchCommand}`);
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1038
|
+
// Cleanup when Claude exits
|
|
1039
|
+
claude.on('close', () => {
|
|
1040
|
+
cleanupNulFile();
|
|
1041
|
+
});
|
|
1042
|
+
});
|