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.
- package/README.md +6 -2
- package/bin/gtask.js +24 -0
- package/package.json +1 -1
- package/src/commands/global-reinstall.js +243 -0
- package/src/commands/global-uninstall.js +229 -0
- package/src/commands/init.js +224 -0
- package/src/commands/uninstall.js +8 -1
- package/src/data/releases.json +15 -0
- package/src/utils/global-registry.js +230 -0
- package/src/utils/paths.js +146 -0
- package/templates/commands/create-task-list-for-issue.template.md +216 -0
- package/templates/commands/create-task-list.template.md +280 -82
- package/templates/commands/menu-issues-list.template.md +288 -0
package/src/commands/init.js
CHANGED
|
@@ -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.'));
|
package/src/data/releases.json
CHANGED
|
@@ -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
|
+
}
|