claude-cli-advanced-starter-pack 1.8.3 → 1.8.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.
@@ -28,6 +28,8 @@ import {
28
28
  generateMergeExplanation,
29
29
  formatMergeOptions,
30
30
  } from '../utils/smart-merge.js';
31
+ import { registerProject } from '../utils/global-registry.js';
32
+ import { replacePlaceholders } from '../utils/template-engine.js';
31
33
 
32
34
  const __filename = fileURLToPath(import.meta.url);
33
35
  const __dirname = dirname(__filename);
@@ -169,6 +171,18 @@ const AVAILABLE_COMMANDS = [
169
171
  category: 'GitHub',
170
172
  selected: true,
171
173
  },
174
+ {
175
+ name: 'menu-issues-list',
176
+ description: 'Mobile-friendly menu of open GitHub issues',
177
+ category: 'GitHub',
178
+ selected: true,
179
+ },
180
+ {
181
+ name: 'create-task-list-for-issue',
182
+ description: 'Start working on a GitHub issue by number',
183
+ category: 'GitHub',
184
+ selected: true,
185
+ },
172
186
  {
173
187
  name: 'phase-dev-plan',
174
188
  description: 'Create phased development plans (95%+ success rate)',
@@ -1410,10 +1424,206 @@ function generateSettingsLocalJson() {
1410
1424
  }, null, 2);
1411
1425
  }
1412
1426
 
1427
+ /**
1428
+ * Run dev mode - rapid template testing workflow
1429
+ * Loads existing tech-stack.json, processes templates, overwrites commands
1430
+ */
1431
+ async function runDevMode(options = {}) {
1432
+ const cwd = process.cwd();
1433
+ const projectName = basename(cwd);
1434
+ const claudeDir = join(cwd, '.claude');
1435
+ const commandsDir = join(claudeDir, 'commands');
1436
+ const hooksDir = join(claudeDir, 'hooks');
1437
+ const configDir = join(claudeDir, 'config');
1438
+ const techStackPath = join(configDir, 'tech-stack.json');
1439
+
1440
+ console.log(chalk.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
1441
+ console.log(chalk.magenta.bold(' 🔧 DEV MODE - Template Testing'));
1442
+ console.log(chalk.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
1443
+ console.log('');
1444
+ console.log(chalk.cyan(` Project: ${chalk.bold(projectName)}`));
1445
+ console.log(chalk.cyan(` Location: ${cwd}`));
1446
+ console.log('');
1447
+
1448
+ // Load existing tech-stack.json
1449
+ let techStack = {};
1450
+ if (existsSync(techStackPath)) {
1451
+ try {
1452
+ techStack = JSON.parse(readFileSync(techStackPath, 'utf8'));
1453
+ console.log(chalk.green(' ✓ Loaded existing tech-stack.json'));
1454
+ } catch (err) {
1455
+ console.log(chalk.yellow(` ⚠ Could not parse tech-stack.json: ${err.message}`));
1456
+ }
1457
+ } else {
1458
+ console.log(chalk.yellow(' ⚠ No tech-stack.json found - templates will have unprocessed placeholders'));
1459
+ }
1460
+
1461
+ // Ensure directories exist
1462
+ if (!existsSync(commandsDir)) {
1463
+ mkdirSync(commandsDir, { recursive: true });
1464
+ console.log(chalk.green(' ✓ Created .claude/commands/'));
1465
+ }
1466
+ if (!existsSync(hooksDir)) {
1467
+ mkdirSync(hooksDir, { recursive: true });
1468
+ console.log(chalk.green(' ✓ Created .claude/hooks/'));
1469
+ }
1470
+
1471
+ // Identify custom commands (no matching template) to preserve
1472
+ const templatesDir = join(__dirname, '..', '..', 'templates', 'commands');
1473
+ const hooksTemplatesDir = join(__dirname, '..', '..', 'templates', 'hooks');
1474
+
1475
+ const templateCommandNames = existsSync(templatesDir)
1476
+ ? readdirSync(templatesDir).filter(f => f.endsWith('.template.md')).map(f => f.replace('.template.md', ''))
1477
+ : [];
1478
+ const templateHookNames = existsSync(hooksTemplatesDir)
1479
+ ? readdirSync(hooksTemplatesDir).filter(f => f.endsWith('.template.js')).map(f => f.replace('.template.js', ''))
1480
+ : [];
1481
+
1482
+ // Find existing custom commands (those without matching templates)
1483
+ const existingCommands = existsSync(commandsDir)
1484
+ ? readdirSync(commandsDir).filter(f => f.endsWith('.md')).map(f => f.replace('.md', ''))
1485
+ : [];
1486
+ const customCommands = existingCommands.filter(cmd =>
1487
+ !templateCommandNames.includes(cmd) &&
1488
+ cmd !== 'menu' &&
1489
+ cmd !== 'INDEX' &&
1490
+ cmd !== 'README'
1491
+ );
1492
+
1493
+ if (customCommands.length > 0) {
1494
+ console.log(chalk.blue(` 📌 Preserving ${customCommands.length} custom command(s):`));
1495
+ for (const cmd of customCommands) {
1496
+ console.log(chalk.dim(` • /${cmd}`));
1497
+ }
1498
+ console.log('');
1499
+ }
1500
+
1501
+ console.log(chalk.bold('Processing and deploying templates...\n'));
1502
+
1503
+ const spinner = ora('Processing templates...').start();
1504
+ const deployed = { commands: [], hooks: [], preserved: customCommands };
1505
+ const failed = [];
1506
+
1507
+ // Get all command templates
1508
+ if (existsSync(templatesDir)) {
1509
+ const templateFiles = readdirSync(templatesDir).filter(f => f.endsWith('.template.md'));
1510
+
1511
+ for (const templateFile of templateFiles) {
1512
+ const cmdName = templateFile.replace('.template.md', '');
1513
+ const templatePath = join(templatesDir, templateFile);
1514
+ const outputPath = join(commandsDir, `${cmdName}.md`);
1515
+
1516
+ try {
1517
+ let content = readFileSync(templatePath, 'utf8');
1518
+
1519
+ // Process template with tech-stack values
1520
+ const { content: processed, warnings } = replacePlaceholders(content, techStack, {
1521
+ preserveUnknown: false,
1522
+ warnOnMissing: false,
1523
+ });
1524
+
1525
+ writeFileSync(outputPath, processed, 'utf8');
1526
+ deployed.commands.push(cmdName);
1527
+ } catch (err) {
1528
+ failed.push({ name: cmdName, type: 'command', error: err.message });
1529
+ }
1530
+ }
1531
+ }
1532
+
1533
+ // Also process hook templates
1534
+ if (existsSync(hooksTemplatesDir)) {
1535
+ const hookFiles = readdirSync(hooksTemplatesDir).filter(f => f.endsWith('.template.js'));
1536
+
1537
+ for (const hookFile of hookFiles) {
1538
+ const hookName = hookFile.replace('.template.js', '');
1539
+ const templatePath = join(hooksTemplatesDir, hookFile);
1540
+ const outputPath = join(hooksDir, `${hookName}.js`);
1541
+
1542
+ try {
1543
+ let content = readFileSync(templatePath, 'utf8');
1544
+
1545
+ // Process template with tech-stack values
1546
+ const { content: processed } = replacePlaceholders(content, techStack, {
1547
+ preserveUnknown: false,
1548
+ warnOnMissing: false,
1549
+ });
1550
+
1551
+ writeFileSync(outputPath, processed, 'utf8');
1552
+ deployed.hooks.push(hookName);
1553
+ } catch (err) {
1554
+ failed.push({ name: hookName, type: 'hook', error: err.message });
1555
+ }
1556
+ }
1557
+ }
1558
+
1559
+ // Generate menu command from scratch (uses COMMAND_TEMPLATES)
1560
+ const menuTemplate = COMMAND_TEMPLATES['menu'];
1561
+ if (menuTemplate) {
1562
+ const installedAgents = existsSync(join(claudeDir, 'agents'))
1563
+ ? readdirSync(join(claudeDir, 'agents')).filter(f => f.endsWith('.md')).map(f => f.replace('.md', ''))
1564
+ : [];
1565
+ const installedSkills = existsSync(join(claudeDir, 'skills'))
1566
+ ? readdirSync(join(claudeDir, 'skills')).filter(f => !f.startsWith('.'))
1567
+ : [];
1568
+ const installedHooks = existsSync(hooksDir)
1569
+ ? readdirSync(hooksDir).filter(f => f.endsWith('.js')).map(f => f.replace('.js', ''))
1570
+ : [];
1571
+
1572
+ const menuContent = generateMenuCommand(projectName, deployed.commands, installedAgents, installedSkills, installedHooks);
1573
+ writeFileSync(join(commandsDir, 'menu.md'), menuContent, 'utf8');
1574
+ deployed.commands.push('menu');
1575
+ }
1576
+
1577
+ // Generate INDEX.md
1578
+ const indexContent = generateIndexFile(deployed.commands, projectName);
1579
+ writeFileSync(join(commandsDir, 'INDEX.md'), indexContent, 'utf8');
1580
+
1581
+ // Generate README.md
1582
+ const readmeContent = generateReadmeFile(deployed.commands, projectName);
1583
+ writeFileSync(join(commandsDir, 'README.md'), readmeContent, 'utf8');
1584
+
1585
+ spinner.stop();
1586
+
1587
+ // Summary
1588
+ console.log('');
1589
+ console.log(chalk.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
1590
+ console.log(chalk.green.bold(' ✓ DEV MODE: Templates Deployed'));
1591
+ console.log(chalk.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
1592
+ console.log('');
1593
+ console.log(chalk.cyan(` Commands: ${deployed.commands.length} deployed`));
1594
+ console.log(chalk.cyan(` Hooks: ${deployed.hooks.length} deployed`));
1595
+ if (deployed.preserved && deployed.preserved.length > 0) {
1596
+ console.log(chalk.blue(` Custom: ${deployed.preserved.length} preserved`));
1597
+ }
1598
+ if (failed.length > 0) {
1599
+ console.log(chalk.yellow(` Failed: ${failed.length}`));
1600
+ for (const f of failed) {
1601
+ console.log(chalk.red(` • ${f.type}/${f.name}: ${f.error}`));
1602
+ }
1603
+ }
1604
+ console.log('');
1605
+ console.log(chalk.dim(' tech-stack.json: Preserved'));
1606
+ console.log(chalk.dim(' settings.json: Preserved'));
1607
+ if (deployed.preserved && deployed.preserved.length > 0) {
1608
+ console.log(chalk.dim(` Custom commands: ${deployed.preserved.join(', ')}`));
1609
+ }
1610
+ console.log('');
1611
+ console.log(chalk.yellow.bold(' ⚠ Restart Claude Code CLI to use new commands'));
1612
+ console.log('');
1613
+
1614
+ return { deployed, failed };
1615
+ }
1616
+
1413
1617
  /**
1414
1618
  * Run the init wizard
1415
1619
  */
1416
1620
  export async function runInit(options = {}) {
1621
+ // DEV MODE: Fast path for template testing
1622
+ if (options.dev) {
1623
+ showHeader('Claude CLI Advanced Starter Pack - DEV MODE');
1624
+ return runDevMode(options);
1625
+ }
1626
+
1417
1627
  showHeader('Claude CLI Advanced Starter Pack - Project Setup');
1418
1628
 
1419
1629
  const cwd = process.cwd();
@@ -2416,6 +2626,20 @@ export async function runInit(options = {}) {
2416
2626
  writeFileSync(ccaspStatePath, JSON.stringify(ccaspState, null, 2), 'utf8');
2417
2627
  console.log(chalk.green(` ✓ Updated ccasp-state.json (v${currentVersion})`));
2418
2628
 
2629
+ // Register project in global registry (unless --no-register flag is set)
2630
+ if (!options.noRegister) {
2631
+ const isNewProject = registerProject(cwd, {
2632
+ name: projectName,
2633
+ version: currentVersion,
2634
+ features: selectedFeatures
2635
+ });
2636
+ if (isNewProject) {
2637
+ console.log(chalk.green(` ✓ Registered project in global CCASP registry`));
2638
+ } else {
2639
+ console.log(chalk.dim(` ○ Updated project in global CCASP registry`));
2640
+ }
2641
+ }
2642
+
2419
2643
  // Show next steps
2420
2644
  console.log(chalk.bold('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'));
2421
2645
  console.log(chalk.bold('Next Steps:\n'));
@@ -8,6 +8,7 @@ import chalk from 'chalk';
8
8
  import { existsSync, readFileSync, readdirSync, unlinkSync, rmdirSync, copyFileSync, statSync } from 'fs';
9
9
  import { join, basename, dirname } from 'path';
10
10
  import { createInterface } from 'readline';
11
+ import { unregisterProject } from '../utils/global-registry.js';
11
12
 
12
13
  /**
13
14
  * Prompt user for confirmation
@@ -396,8 +397,14 @@ export async function runUninstall(options = {}) {
396
397
  }
397
398
  }
398
399
 
400
+ // Remove from global registry
401
+ const wasRegistered = unregisterProject(projectDir);
402
+ if (wasRegistered) {
403
+ console.log(chalk.green(' ✓ Removed from global CCASP registry'));
404
+ }
405
+
399
406
  // Done
400
- console.log(chalk.green.bold(' ✓ CCASP uninstalled successfully!\n'));
407
+ console.log(chalk.green.bold('\n ✓ CCASP uninstalled successfully!\n'));
401
408
 
402
409
  if (backups.size > 0) {
403
410
  console.log(chalk.dim(' Your backed-up files have been restored.'));
@@ -1,5 +1,20 @@
1
1
  {
2
2
  "releases": [
3
+ {
4
+ "version": "1.8.4",
5
+ "date": "2026-01-31",
6
+ "summary": "Release notes pending",
7
+ "highlights": [],
8
+ "newFeatures": {
9
+ "commands": [],
10
+ "agents": [],
11
+ "skills": [],
12
+ "hooks": [],
13
+ "other": []
14
+ },
15
+ "breaking": [],
16
+ "deprecated": []
17
+ },
3
18
  {
4
19
  "version": "1.5.0",
5
20
  "date": "2026-01-30",
@@ -0,0 +1,230 @@
1
+ /**
2
+ * Global Project Registry
3
+ * Tracks all projects configured with CCASP across the system
4
+ * Location: ~/.claude/ccasp-registry.json
5
+ */
6
+
7
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
8
+ import { join } from 'path';
9
+ import {
10
+ globalClaudePath,
11
+ getHomeDir,
12
+ toForwardSlashes,
13
+ pathsEqual,
14
+ getPathSegment
15
+ } from './paths.js';
16
+
17
+ /**
18
+ * Get the path to the global registry file
19
+ * @returns {string} Path to ~/.claude/ccasp-registry.json
20
+ */
21
+ export function getRegistryPath() {
22
+ return globalClaudePath('ccasp-registry.json');
23
+ }
24
+
25
+ /**
26
+ * Get the global .claude directory path
27
+ * @returns {string} Path to ~/.claude/
28
+ */
29
+ export function getGlobalClaudeDir() {
30
+ return join(getHomeDir(), '.claude');
31
+ }
32
+
33
+ /**
34
+ * Ensure the global .claude directory exists
35
+ */
36
+ function ensureGlobalDir() {
37
+ const globalDir = getGlobalClaudeDir();
38
+ if (!existsSync(globalDir)) {
39
+ mkdirSync(globalDir, { recursive: true });
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Load the global registry
45
+ * @returns {Object} Registry object with projects array and metadata
46
+ */
47
+ export function loadRegistry() {
48
+ const registryPath = getRegistryPath();
49
+
50
+ if (!existsSync(registryPath)) {
51
+ return {
52
+ version: '1.0.0',
53
+ projects: [],
54
+ lastModified: null
55
+ };
56
+ }
57
+
58
+ try {
59
+ const content = readFileSync(registryPath, 'utf-8');
60
+ return JSON.parse(content);
61
+ } catch (err) {
62
+ console.error(`Warning: Could not parse registry file: ${err.message}`);
63
+ return {
64
+ version: '1.0.0',
65
+ projects: [],
66
+ lastModified: null
67
+ };
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Save the global registry
73
+ * @param {Object} registry - Registry object to save
74
+ */
75
+ export function saveRegistry(registry) {
76
+ ensureGlobalDir();
77
+ const registryPath = getRegistryPath();
78
+
79
+ registry.lastModified = new Date().toISOString();
80
+
81
+ writeFileSync(registryPath, JSON.stringify(registry, null, 2), 'utf-8');
82
+ }
83
+
84
+ /**
85
+ * Register a project in the global registry
86
+ * @param {string} projectPath - Absolute path to the project
87
+ * @param {Object} metadata - Optional metadata (name, version, features)
88
+ * @returns {boolean} True if newly registered, false if already existed
89
+ */
90
+ export function registerProject(projectPath, metadata = {}) {
91
+ const registry = loadRegistry();
92
+
93
+ // Check if already registered (using cross-platform path comparison)
94
+ const existingIndex = registry.projects.findIndex(
95
+ p => pathsEqual(p.path, projectPath)
96
+ );
97
+
98
+ const projectEntry = {
99
+ path: projectPath,
100
+ name: metadata.name || getPathSegment(projectPath),
101
+ registeredAt: new Date().toISOString(),
102
+ lastInitAt: new Date().toISOString(),
103
+ ccaspVersion: metadata.version || 'unknown',
104
+ features: metadata.features || []
105
+ };
106
+
107
+ if (existingIndex >= 0) {
108
+ // Update existing entry
109
+ registry.projects[existingIndex] = {
110
+ ...registry.projects[existingIndex],
111
+ ...projectEntry,
112
+ registeredAt: registry.projects[existingIndex].registeredAt // Keep original registration date
113
+ };
114
+ saveRegistry(registry);
115
+ return false;
116
+ }
117
+
118
+ // Add new entry
119
+ registry.projects.push(projectEntry);
120
+ saveRegistry(registry);
121
+ return true;
122
+ }
123
+
124
+ /**
125
+ * Unregister a project from the global registry
126
+ * @param {string} projectPath - Absolute path to the project
127
+ * @returns {boolean} True if removed, false if not found
128
+ */
129
+ export function unregisterProject(projectPath) {
130
+ const registry = loadRegistry();
131
+
132
+ const initialLength = registry.projects.length;
133
+ registry.projects = registry.projects.filter(
134
+ p => !pathsEqual(p.path, projectPath)
135
+ );
136
+
137
+ if (registry.projects.length < initialLength) {
138
+ saveRegistry(registry);
139
+ return true;
140
+ }
141
+
142
+ return false;
143
+ }
144
+
145
+ /**
146
+ * Get all registered projects
147
+ * @param {Object} options - Filter options
148
+ * @param {boolean} options.existsOnly - Only return projects that still exist on disk
149
+ * @returns {Array} Array of project entries
150
+ */
151
+ export function getRegisteredProjects(options = {}) {
152
+ const registry = loadRegistry();
153
+ let projects = registry.projects;
154
+
155
+ if (options.existsOnly) {
156
+ projects = projects.filter(p => existsSync(p.path));
157
+ }
158
+
159
+ return projects;
160
+ }
161
+
162
+ /**
163
+ * Check if a project is registered
164
+ * @param {string} projectPath - Absolute path to the project
165
+ * @returns {boolean} True if registered
166
+ */
167
+ export function isProjectRegistered(projectPath) {
168
+ const registry = loadRegistry();
169
+
170
+ return registry.projects.some(
171
+ p => pathsEqual(p.path, projectPath)
172
+ );
173
+ }
174
+
175
+ /**
176
+ * Get registry statistics
177
+ * @returns {Object} Stats about registered projects
178
+ */
179
+ export function getRegistryStats() {
180
+ const registry = loadRegistry();
181
+ const projects = registry.projects;
182
+
183
+ let existingCount = 0;
184
+ let missingCount = 0;
185
+
186
+ for (const project of projects) {
187
+ if (existsSync(project.path)) {
188
+ existingCount++;
189
+ } else {
190
+ missingCount++;
191
+ }
192
+ }
193
+
194
+ return {
195
+ total: projects.length,
196
+ existing: existingCount,
197
+ missing: missingCount,
198
+ lastModified: registry.lastModified
199
+ };
200
+ }
201
+
202
+ /**
203
+ * Clean up registry by removing projects that no longer exist
204
+ * @returns {number} Number of projects removed
205
+ */
206
+ export function cleanupRegistry() {
207
+ const registry = loadRegistry();
208
+ const initialLength = registry.projects.length;
209
+
210
+ registry.projects = registry.projects.filter(p => existsSync(p.path));
211
+
212
+ const removed = initialLength - registry.projects.length;
213
+ if (removed > 0) {
214
+ saveRegistry(registry);
215
+ }
216
+
217
+ return removed;
218
+ }
219
+
220
+ /**
221
+ * Clear the entire registry
222
+ */
223
+ export function clearRegistry() {
224
+ const registry = {
225
+ version: '1.0.0',
226
+ projects: [],
227
+ lastModified: new Date().toISOString()
228
+ };
229
+ saveRegistry(registry);
230
+ }
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Cross-Platform Path Utilities
3
+ *
4
+ * Centralized path handling for Windows/macOS/Linux compatibility.
5
+ * All path operations should use these utilities to ensure consistency.
6
+ */
7
+
8
+ import { join, normalize, sep } from 'path';
9
+ import { homedir } from 'os';
10
+
11
+ /**
12
+ * Normalize a path to use forward slashes (for comparison and storage)
13
+ * @param {string} filePath - Path to normalize
14
+ * @returns {string} Path with forward slashes
15
+ */
16
+ export function toForwardSlashes(filePath) {
17
+ if (!filePath) return filePath;
18
+ return filePath.replace(/\\/g, '/');
19
+ }
20
+
21
+ /**
22
+ * Normalize a path to use the platform's native separator
23
+ * @param {string} filePath - Path to normalize
24
+ * @returns {string} Path with native separators
25
+ */
26
+ export function toNativePath(filePath) {
27
+ if (!filePath) return filePath;
28
+ return filePath.replace(/[/\\]/g, sep);
29
+ }
30
+
31
+ /**
32
+ * Compare two paths for equality (cross-platform safe)
33
+ * @param {string} path1 - First path
34
+ * @param {string} path2 - Second path
35
+ * @returns {boolean} True if paths are equivalent
36
+ */
37
+ export function pathsEqual(path1, path2) {
38
+ if (!path1 || !path2) return path1 === path2;
39
+ const norm1 = toForwardSlashes(path1).replace(/\/$/, '').toLowerCase();
40
+ const norm2 = toForwardSlashes(path2).replace(/\/$/, '').toLowerCase();
41
+ return norm1 === norm2;
42
+ }
43
+
44
+ /**
45
+ * Get the last segment of a path (cross-platform safe)
46
+ * @param {string} filePath - Path to extract from
47
+ * @returns {string} Last segment of the path
48
+ */
49
+ export function getPathSegment(filePath) {
50
+ if (!filePath) return '';
51
+ return filePath.split(/[/\\]/).pop() || '';
52
+ }
53
+
54
+ /**
55
+ * Build a .claude-relative path string for use in config/templates
56
+ * Always uses forward slashes for consistency in JSON/templates
57
+ * @param {...string} segments - Path segments after .claude/
58
+ * @returns {string} Path string like ".claude/hooks/file.js"
59
+ */
60
+ export function claudeRelativePath(...segments) {
61
+ return '.claude/' + segments.join('/');
62
+ }
63
+
64
+ /**
65
+ * Build an absolute path to a .claude directory item
66
+ * @param {string} baseDir - Project root directory
67
+ * @param {...string} segments - Path segments after .claude/
68
+ * @returns {string} Absolute path using native separators
69
+ */
70
+ export function claudeAbsolutePath(baseDir, ...segments) {
71
+ return join(baseDir, '.claude', ...segments);
72
+ }
73
+
74
+ /**
75
+ * Get the user's home directory (cross-platform)
76
+ * @returns {string} Home directory path
77
+ */
78
+ export function getHomeDir() {
79
+ return process.env.HOME || process.env.USERPROFILE || homedir();
80
+ }
81
+
82
+ /**
83
+ * Build a path relative to user's home directory
84
+ * @param {...string} segments - Path segments after home
85
+ * @returns {string} Absolute path using native separators
86
+ */
87
+ export function homeRelativePath(...segments) {
88
+ return join(getHomeDir(), ...segments);
89
+ }
90
+
91
+ /**
92
+ * Build a path to global .claude directory
93
+ * @param {...string} segments - Path segments after ~/.claude/
94
+ * @returns {string} Absolute path using native separators
95
+ */
96
+ export function globalClaudePath(...segments) {
97
+ return join(getHomeDir(), '.claude', ...segments);
98
+ }
99
+
100
+ /**
101
+ * Check if running on Windows
102
+ * @returns {boolean} True if Windows
103
+ */
104
+ export function isWindows() {
105
+ return process.platform === 'win32';
106
+ }
107
+
108
+ /**
109
+ * Get the appropriate command existence checker
110
+ * @returns {string} 'where' on Windows, 'which' on Unix
111
+ */
112
+ export function getWhichCommand() {
113
+ return isWindows() ? 'where' : 'which';
114
+ }
115
+
116
+ /**
117
+ * Normalize line endings to Unix style (LF)
118
+ * @param {string} content - Content to normalize
119
+ * @returns {string} Content with Unix line endings
120
+ */
121
+ export function normalizeLineEndings(content) {
122
+ if (!content) return content;
123
+ return content.replace(/\r\n/g, '\n');
124
+ }
125
+
126
+ /**
127
+ * Build a Node.js command that works cross-platform
128
+ * Uses forward slashes which Node.js handles on all platforms
129
+ * @param {string} scriptPath - Path to the script (relative to project root)
130
+ * @returns {string} Command string like "node .claude/hooks/script.js"
131
+ */
132
+ export function nodeCommand(scriptPath) {
133
+ // Node.js handles forward slashes on all platforms
134
+ const normalizedPath = toForwardSlashes(scriptPath);
135
+ return `node ${normalizedPath}`;
136
+ }
137
+
138
+ /**
139
+ * Ensure a path uses consistent separators for storage/comparison
140
+ * Stores with forward slashes, converts to native when needed for fs operations
141
+ * @param {string} filePath - Path to store
142
+ * @returns {string} Normalized path for storage
143
+ */
144
+ export function storagePath(filePath) {
145
+ return toForwardSlashes(filePath);
146
+ }