claude-devkit-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/devkit.js +3 -0
- package/package.json +36 -0
- package/src/cli.js +72 -0
- package/src/commands/check.js +33 -0
- package/src/commands/diff.js +90 -0
- package/src/commands/init.js +232 -0
- package/src/commands/list.js +50 -0
- package/src/commands/remove.js +74 -0
- package/src/commands/upgrade.js +108 -0
- package/src/lib/detector.js +93 -0
- package/src/lib/hasher.js +21 -0
- package/src/lib/installer.js +175 -0
- package/src/lib/logger.js +16 -0
- package/src/lib/manifest.js +79 -0
- package/templates/.claude/CLAUDE.md +74 -0
- package/templates/.claude/commands/challenge.md +210 -0
- package/templates/.claude/commands/commit.md +97 -0
- package/templates/.claude/commands/fix.md +95 -0
- package/templates/.claude/commands/plan.md +141 -0
- package/templates/.claude/commands/review.md +109 -0
- package/templates/.claude/commands/test.md +99 -0
- package/templates/.claude/hooks/comment-guard.js +114 -0
- package/templates/.claude/hooks/file-guard.js +120 -0
- package/templates/.claude/hooks/glob-guard.js +96 -0
- package/templates/.claude/hooks/path-guard.sh +73 -0
- package/templates/.claude/hooks/self-review.sh +29 -0
- package/templates/.claude/hooks/sensitive-guard.sh +214 -0
- package/templates/.claude/settings.json +68 -0
- package/templates/docs/WORKFLOW.md +231 -0
- package/templates/docs/specs/.gitkeep +0 -0
- package/templates/docs/test-plans/.gitkeep +0 -0
- package/templates/scripts/build-test.sh +260 -0
package/bin/devkit.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "claude-devkit-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI toolkit for spec-first development with Claude Code — hooks, commands, guards, and test runners",
|
|
5
|
+
"bin": {
|
|
6
|
+
"claude-devkit": "./bin/devkit.js",
|
|
7
|
+
"claude-devkit-cli": "./bin/devkit.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"src/",
|
|
12
|
+
"templates/"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"chalk": "^5.4.1",
|
|
19
|
+
"commander": "^13.1.0"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"claude",
|
|
23
|
+
"claude-code",
|
|
24
|
+
"devkit",
|
|
25
|
+
"spec-first",
|
|
26
|
+
"testing",
|
|
27
|
+
"hooks",
|
|
28
|
+
"guards"
|
|
29
|
+
],
|
|
30
|
+
"scripts": {
|
|
31
|
+
"prepublishOnly": "rm -rf templates && cp -r ../kit templates",
|
|
32
|
+
"dev": "node bin/devkit.js"
|
|
33
|
+
},
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"type": "module"
|
|
36
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const pkg = JSON.parse(readFileSync(resolve(__dirname, '../package.json'), 'utf-8'));
|
|
8
|
+
|
|
9
|
+
export function cli(argv) {
|
|
10
|
+
const program = new Command();
|
|
11
|
+
|
|
12
|
+
program
|
|
13
|
+
.name('claude-devkit')
|
|
14
|
+
.description('CLI toolkit for spec-first development with Claude Code')
|
|
15
|
+
.version(pkg.version);
|
|
16
|
+
|
|
17
|
+
program
|
|
18
|
+
.command('init [path]')
|
|
19
|
+
.description('Initialize a project with the dev-kit')
|
|
20
|
+
.option('-f, --force', 'Overwrite existing files')
|
|
21
|
+
.option('--only <components>', 'Install only specific components (comma-separated: hooks,commands,scripts,docs,config)')
|
|
22
|
+
.option('--adopt', 'Adopt existing kit files without overwriting (migration from setup.sh)')
|
|
23
|
+
.option('--dry-run', 'Show what would be done without making changes')
|
|
24
|
+
.action(async (path, opts) => {
|
|
25
|
+
const { initCommand } = await import('./commands/init.js');
|
|
26
|
+
await initCommand(path || '.', opts);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
program
|
|
30
|
+
.command('upgrade [path]')
|
|
31
|
+
.description('Smart upgrade — preserves customized files')
|
|
32
|
+
.option('-f, --force', 'Overwrite even customized files')
|
|
33
|
+
.option('--dry-run', 'Show what would be done without making changes')
|
|
34
|
+
.action(async (path, opts) => {
|
|
35
|
+
const { upgradeCommand } = await import('./commands/upgrade.js');
|
|
36
|
+
await upgradeCommand(path || '.', opts);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
program
|
|
40
|
+
.command('check [path]')
|
|
41
|
+
.description('Check if an update is available')
|
|
42
|
+
.action(async (path) => {
|
|
43
|
+
const { checkCommand } = await import('./commands/check.js');
|
|
44
|
+
await checkCommand(path || '.');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
program
|
|
48
|
+
.command('list [path]')
|
|
49
|
+
.description('List installed components and their status')
|
|
50
|
+
.action(async (path) => {
|
|
51
|
+
const { listCommand } = await import('./commands/list.js');
|
|
52
|
+
await listCommand(path || '.');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
program
|
|
56
|
+
.command('diff [path]')
|
|
57
|
+
.description('Show differences between installed and latest kit files')
|
|
58
|
+
.action(async (path) => {
|
|
59
|
+
const { diffCommand } = await import('./commands/diff.js');
|
|
60
|
+
await diffCommand(path || '.');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
program
|
|
64
|
+
.command('remove [path]')
|
|
65
|
+
.description('Uninstall dev-kit (preserves CLAUDE.md and docs/)')
|
|
66
|
+
.action(async (path) => {
|
|
67
|
+
const { removeCommand } = await import('./commands/remove.js');
|
|
68
|
+
await removeCommand(path || '.');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
program.parse(argv);
|
|
72
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { dirname } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { log } from '../lib/logger.js';
|
|
6
|
+
import { readManifest } from '../lib/manifest.js';
|
|
7
|
+
|
|
8
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const pkg = JSON.parse(readFileSync(resolve(__dirname, '../../package.json'), 'utf-8'));
|
|
10
|
+
|
|
11
|
+
export async function checkCommand(path) {
|
|
12
|
+
const targetDir = resolve(path);
|
|
13
|
+
const manifest = await readManifest(targetDir);
|
|
14
|
+
|
|
15
|
+
if (!manifest) {
|
|
16
|
+
log.fail('No manifest found. Run `claude-devkit init` first.');
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const installed = manifest.version;
|
|
21
|
+
const latest = pkg.version;
|
|
22
|
+
|
|
23
|
+
log.info(`Installed: ${installed}`);
|
|
24
|
+
log.info(`Latest: ${latest}`);
|
|
25
|
+
log.blank();
|
|
26
|
+
|
|
27
|
+
if (installed === latest) {
|
|
28
|
+
log.pass('Up to date.');
|
|
29
|
+
} else {
|
|
30
|
+
log.warn(`Update available: ${installed} → ${latest}`);
|
|
31
|
+
console.log('Run: npx claude-devkit upgrade');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { log } from '../lib/logger.js';
|
|
6
|
+
import { readManifest } from '../lib/manifest.js';
|
|
7
|
+
import { hashFile } from '../lib/hasher.js';
|
|
8
|
+
import { getAllFiles, getTemplateDir } from '../lib/installer.js';
|
|
9
|
+
|
|
10
|
+
export async function diffCommand(path) {
|
|
11
|
+
const targetDir = resolve(path);
|
|
12
|
+
const manifest = await readManifest(targetDir);
|
|
13
|
+
|
|
14
|
+
if (!manifest) {
|
|
15
|
+
log.fail('No manifest found. Run `claude-devkit init` first.');
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const templateDir = getTemplateDir();
|
|
20
|
+
const allFiles = getAllFiles();
|
|
21
|
+
let hasDiffs = false;
|
|
22
|
+
|
|
23
|
+
for (const file of allFiles) {
|
|
24
|
+
const templatePath = resolve(templateDir, file);
|
|
25
|
+
const installedPath = resolve(targetDir, file);
|
|
26
|
+
|
|
27
|
+
if (!existsSync(installedPath)) {
|
|
28
|
+
// File in kit but not installed
|
|
29
|
+
console.log(chalk.cyan(`\n${file} (new in kit — not installed)`));
|
|
30
|
+
hasDiffs = true;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const kitHash = await hashFile(templatePath);
|
|
35
|
+
const installedHash = await hashFile(installedPath);
|
|
36
|
+
|
|
37
|
+
if (kitHash === installedHash) continue;
|
|
38
|
+
|
|
39
|
+
hasDiffs = true;
|
|
40
|
+
|
|
41
|
+
// Check if kit changed or user changed
|
|
42
|
+
const entry = manifest.files[file];
|
|
43
|
+
const kitChanged = entry && kitHash !== entry.kitHash;
|
|
44
|
+
const userChanged = entry && entry.customized;
|
|
45
|
+
|
|
46
|
+
let label = '';
|
|
47
|
+
if (kitChanged && userChanged) label = 'both kit and local changed';
|
|
48
|
+
else if (kitChanged) label = 'kit updated';
|
|
49
|
+
else if (userChanged) label = 'locally customized';
|
|
50
|
+
else label = 'differs';
|
|
51
|
+
|
|
52
|
+
console.log(chalk.bold(`\n${file}`) + chalk.gray(` (${label})`));
|
|
53
|
+
console.log('─'.repeat(60));
|
|
54
|
+
|
|
55
|
+
// Simple line-by-line diff
|
|
56
|
+
const kitContent = await readFile(templatePath, 'utf-8');
|
|
57
|
+
const installedContent = await readFile(installedPath, 'utf-8');
|
|
58
|
+
const kitLines = kitContent.split('\n');
|
|
59
|
+
const installedLines = installedContent.split('\n');
|
|
60
|
+
|
|
61
|
+
// Show a simplified diff: lines only in kit (green +), only in installed (red -)
|
|
62
|
+
const kitSet = new Set(kitLines);
|
|
63
|
+
const installedSet = new Set(installedLines);
|
|
64
|
+
|
|
65
|
+
const removed = installedLines.filter((l) => !kitSet.has(l) && l.trim());
|
|
66
|
+
const added = kitLines.filter((l) => !installedSet.has(l) && l.trim());
|
|
67
|
+
|
|
68
|
+
for (const line of removed.slice(0, 10)) {
|
|
69
|
+
console.log(chalk.red(` - ${line}`));
|
|
70
|
+
}
|
|
71
|
+
for (const line of added.slice(0, 10)) {
|
|
72
|
+
console.log(chalk.green(` + ${line}`));
|
|
73
|
+
}
|
|
74
|
+
if (removed.length > 10 || added.length > 10) {
|
|
75
|
+
console.log(chalk.gray(` ... and ${Math.max(removed.length - 10, 0) + Math.max(added.length - 10, 0)} more lines`));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Check for files in manifest not in current kit
|
|
80
|
+
for (const file of Object.keys(manifest.files)) {
|
|
81
|
+
if (!allFiles.includes(file)) {
|
|
82
|
+
console.log(chalk.yellow(`\n${file} (removed from kit)`));
|
|
83
|
+
hasDiffs = true;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!hasDiffs) {
|
|
88
|
+
log.pass('All files match the kit. No differences.');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { execSync } from 'node:child_process';
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
5
|
+
import { dirname } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { log } from '../lib/logger.js';
|
|
8
|
+
import { detectProject } from '../lib/detector.js';
|
|
9
|
+
import { readManifest, writeManifest, createManifest, setFileEntry } from '../lib/manifest.js';
|
|
10
|
+
import { hashFile } from '../lib/hasher.js';
|
|
11
|
+
import {
|
|
12
|
+
getAllFiles, getFilesForComponents, installFile,
|
|
13
|
+
ensurePlaceholderDir, setPermissions, fillTemplate,
|
|
14
|
+
verifySettingsJson, PLACEHOLDER_DIRS, COMPONENTS,
|
|
15
|
+
getTemplateDir,
|
|
16
|
+
} from '../lib/installer.js';
|
|
17
|
+
|
|
18
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const pkg = JSON.parse(readFileSync(resolve(__dirname, '../../package.json'), 'utf-8'));
|
|
20
|
+
|
|
21
|
+
export async function initCommand(path, opts) {
|
|
22
|
+
const targetDir = resolve(path);
|
|
23
|
+
|
|
24
|
+
if (!existsSync(targetDir)) {
|
|
25
|
+
log.fail(`Directory not found: ${targetDir}`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
log.info(`claude-devkit v${pkg.version}`);
|
|
30
|
+
log.info(`Target: ${targetDir}`);
|
|
31
|
+
log.blank();
|
|
32
|
+
|
|
33
|
+
// --- Adopt mode ---
|
|
34
|
+
if (opts.adopt) {
|
|
35
|
+
await adoptExisting(targetDir);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// --- Prerequisites ---
|
|
40
|
+
let warnings = 0;
|
|
41
|
+
|
|
42
|
+
if (!commandExists('git')) {
|
|
43
|
+
log.fail('Git not found — required');
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!commandExists('node')) {
|
|
48
|
+
log.warn('Node.js not found — file-guard.js hook requires it');
|
|
49
|
+
warnings++;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!existsSync(resolve(targetDir, '.git'))) {
|
|
53
|
+
log.warn('Not a git repository. Some features need git.');
|
|
54
|
+
warnings++;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// --- Determine files to install ---
|
|
58
|
+
let components = Object.keys(COMPONENTS);
|
|
59
|
+
if (opts.only) {
|
|
60
|
+
components = opts.only.split(',').map((c) => c.trim());
|
|
61
|
+
const valid = Object.keys(COMPONENTS);
|
|
62
|
+
for (const c of components) {
|
|
63
|
+
if (!valid.includes(c)) {
|
|
64
|
+
log.fail(`Unknown component: ${c}. Valid: ${valid.join(', ')}`);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const files = opts.only ? getFilesForComponents(components) : getAllFiles();
|
|
71
|
+
|
|
72
|
+
// --- Dry run ---
|
|
73
|
+
if (opts.dryRun) {
|
|
74
|
+
log.info('Dry run — no changes will be made');
|
|
75
|
+
log.blank();
|
|
76
|
+
for (const file of files) {
|
|
77
|
+
const dst = resolve(targetDir, file);
|
|
78
|
+
if (existsSync(dst) && !opts.force) {
|
|
79
|
+
log.skip(`${file} (exists)`);
|
|
80
|
+
} else {
|
|
81
|
+
log.copy(`${file} (would copy)`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// --- Install files ---
|
|
88
|
+
console.log('--- Installing ---');
|
|
89
|
+
|
|
90
|
+
const manifest = createManifest(pkg.version, null, components);
|
|
91
|
+
let copied = 0;
|
|
92
|
+
let skipped = 0;
|
|
93
|
+
|
|
94
|
+
for (const file of files) {
|
|
95
|
+
const result = await installFile(file, targetDir, { force: opts.force });
|
|
96
|
+
if (result === 'copied') copied++;
|
|
97
|
+
else skipped++;
|
|
98
|
+
|
|
99
|
+
// Record in manifest
|
|
100
|
+
const templatePath = resolve(getTemplateDir(), file);
|
|
101
|
+
const installedPath = resolve(targetDir, file);
|
|
102
|
+
const kitHash = await hashFile(templatePath);
|
|
103
|
+
let installedHash = kitHash;
|
|
104
|
+
try {
|
|
105
|
+
installedHash = await hashFile(installedPath);
|
|
106
|
+
} catch { /* file might not exist if skipped */ }
|
|
107
|
+
setFileEntry(manifest, file, kitHash, installedHash);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Placeholder directories
|
|
111
|
+
for (const dir of PLACEHOLDER_DIRS) {
|
|
112
|
+
await ensurePlaceholderDir(dir, targetDir);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// --- Permissions ---
|
|
116
|
+
await setPermissions(targetDir);
|
|
117
|
+
|
|
118
|
+
// --- Project detection ---
|
|
119
|
+
log.blank();
|
|
120
|
+
console.log('--- Detecting project ---');
|
|
121
|
+
|
|
122
|
+
const projectInfo = detectProject(targetDir);
|
|
123
|
+
if (projectInfo) {
|
|
124
|
+
log.info(`Detected: ${projectInfo.lang} (${projectInfo.framework})`);
|
|
125
|
+
log.info(`Source: ${projectInfo.srcDir} | Tests: ${projectInfo.testDir}`);
|
|
126
|
+
manifest.projectType = { lang: projectInfo.lang, framework: projectInfo.framework };
|
|
127
|
+
|
|
128
|
+
await fillTemplate(targetDir, projectInfo);
|
|
129
|
+
log.info('Updated CLAUDE.md with project info');
|
|
130
|
+
|
|
131
|
+
// Re-hash CLAUDE.md after template fill
|
|
132
|
+
try {
|
|
133
|
+
const claudeHash = await hashFile(resolve(targetDir, '.claude/CLAUDE.md'));
|
|
134
|
+
if (manifest.files['.claude/CLAUDE.md']) {
|
|
135
|
+
manifest.files['.claude/CLAUDE.md'].installedHash = claudeHash;
|
|
136
|
+
manifest.files['.claude/CLAUDE.md'].customized = false; // Template fill is not "customization"
|
|
137
|
+
}
|
|
138
|
+
} catch { /* */ }
|
|
139
|
+
} else {
|
|
140
|
+
log.warn('Could not detect project type. Fill in CLAUDE.md manually.');
|
|
141
|
+
warnings++;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// --- Write manifest ---
|
|
145
|
+
await writeManifest(targetDir, manifest);
|
|
146
|
+
|
|
147
|
+
// --- Verification ---
|
|
148
|
+
log.blank();
|
|
149
|
+
console.log('--- Verification ---');
|
|
150
|
+
|
|
151
|
+
if (await verifySettingsJson(targetDir)) {
|
|
152
|
+
log.pass('settings.json is valid JSON');
|
|
153
|
+
} else {
|
|
154
|
+
log.fail('settings.json is invalid JSON');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// --- Summary ---
|
|
158
|
+
log.blank();
|
|
159
|
+
console.log('=== Setup Complete ===');
|
|
160
|
+
log.blank();
|
|
161
|
+
console.log('Installed:');
|
|
162
|
+
console.log(' .claude/CLAUDE.md — Project rules (review and customize)');
|
|
163
|
+
console.log(' .claude/settings.json — Hook configuration');
|
|
164
|
+
console.log(' .claude/hooks/ — 6 guards (file, path, glob, comment, sensitive, self-review)');
|
|
165
|
+
console.log(' .claude/commands/ — /plan, /test, /fix, /review, /commit, /challenge');
|
|
166
|
+
console.log(' scripts/build-test.sh — Universal test runner');
|
|
167
|
+
console.log(' docs/WORKFLOW.md — Workflow reference');
|
|
168
|
+
log.blank();
|
|
169
|
+
console.log(` ${copied} files copied, ${skipped} skipped`);
|
|
170
|
+
log.blank();
|
|
171
|
+
console.log('Next steps:');
|
|
172
|
+
console.log(' 1. Review .claude/CLAUDE.md — ensure project info is correct');
|
|
173
|
+
console.log(' 2. Write your first spec: docs/specs/<feature>.md');
|
|
174
|
+
console.log(' 3. Generate test plan: /plan docs/specs/<feature>.md');
|
|
175
|
+
console.log(' 4. Start coding + testing: /test');
|
|
176
|
+
log.blank();
|
|
177
|
+
|
|
178
|
+
if (warnings > 0) {
|
|
179
|
+
console.log(`⚠ ${warnings} warning(s) above — review before proceeding.`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Adopt existing kit files (migration from setup.sh).
|
|
185
|
+
* Scans for existing files and generates a manifest without overwriting.
|
|
186
|
+
*/
|
|
187
|
+
async function adoptExisting(targetDir) {
|
|
188
|
+
log.info('Adopting existing kit files...');
|
|
189
|
+
log.blank();
|
|
190
|
+
|
|
191
|
+
const allFiles = getAllFiles();
|
|
192
|
+
const manifest = createManifest(pkg.version, null, Object.keys(COMPONENTS));
|
|
193
|
+
let adopted = 0;
|
|
194
|
+
|
|
195
|
+
for (const file of allFiles) {
|
|
196
|
+
const installedPath = resolve(targetDir, file);
|
|
197
|
+
const templatePath = resolve(getTemplateDir(), file);
|
|
198
|
+
|
|
199
|
+
if (!existsSync(installedPath)) continue;
|
|
200
|
+
|
|
201
|
+
const installedHash = await hashFile(installedPath);
|
|
202
|
+
let kitHash;
|
|
203
|
+
try {
|
|
204
|
+
kitHash = await hashFile(templatePath);
|
|
205
|
+
} catch {
|
|
206
|
+
kitHash = installedHash; // Template doesn't exist, treat as matching
|
|
207
|
+
}
|
|
208
|
+
setFileEntry(manifest, file, kitHash, installedHash);
|
|
209
|
+
log.adopt(file);
|
|
210
|
+
adopted++;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Detect project
|
|
214
|
+
const projectInfo = detectProject(targetDir);
|
|
215
|
+
if (projectInfo) {
|
|
216
|
+
manifest.projectType = { lang: projectInfo.lang, framework: projectInfo.framework };
|
|
217
|
+
log.info(`Detected: ${projectInfo.lang} (${projectInfo.framework})`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
await writeManifest(targetDir, manifest);
|
|
221
|
+
log.blank();
|
|
222
|
+
log.pass(`Manifest created for ${adopted} existing files. Future upgrades will work.`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function commandExists(cmd) {
|
|
226
|
+
try {
|
|
227
|
+
execSync(`command -v ${cmd}`, { stdio: 'ignore' });
|
|
228
|
+
return true;
|
|
229
|
+
} catch {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { log } from '../lib/logger.js';
|
|
4
|
+
import { readManifest, refreshCustomizationStatus } from '../lib/manifest.js';
|
|
5
|
+
|
|
6
|
+
export async function listCommand(path) {
|
|
7
|
+
const targetDir = resolve(path);
|
|
8
|
+
const manifest = await readManifest(targetDir);
|
|
9
|
+
|
|
10
|
+
if (!manifest) {
|
|
11
|
+
log.fail('No manifest found. Run `claude-devkit init` first.');
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Refresh hashes to get accurate customization status
|
|
16
|
+
await refreshCustomizationStatus(targetDir, manifest);
|
|
17
|
+
|
|
18
|
+
log.info(`claude-devkit v${manifest.version} — installed ${manifest.installedAt.split('T')[0]}`);
|
|
19
|
+
if (manifest.projectType) {
|
|
20
|
+
log.info(`Project: ${manifest.projectType.lang} (${manifest.projectType.framework})`);
|
|
21
|
+
}
|
|
22
|
+
log.blank();
|
|
23
|
+
|
|
24
|
+
// Table header
|
|
25
|
+
const fileCol = 40;
|
|
26
|
+
console.log(
|
|
27
|
+
chalk.bold('File'.padEnd(fileCol)) + chalk.bold('Status')
|
|
28
|
+
);
|
|
29
|
+
console.log('─'.repeat(fileCol) + ' ' + '─'.repeat(12));
|
|
30
|
+
|
|
31
|
+
let totalFiles = 0;
|
|
32
|
+
let customized = 0;
|
|
33
|
+
|
|
34
|
+
for (const [file, entry] of Object.entries(manifest.files)) {
|
|
35
|
+
totalFiles++;
|
|
36
|
+
let status;
|
|
37
|
+
if (entry.installedHash === null) {
|
|
38
|
+
status = chalk.red('deleted');
|
|
39
|
+
} else if (entry.customized) {
|
|
40
|
+
status = chalk.yellow('customized');
|
|
41
|
+
customized++;
|
|
42
|
+
} else {
|
|
43
|
+
status = chalk.green('up-to-date');
|
|
44
|
+
}
|
|
45
|
+
console.log(file.padEnd(fileCol) + status);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
log.blank();
|
|
49
|
+
console.log(`${totalFiles} files | ${customized} customized`);
|
|
50
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { resolve, join } from 'node:path';
|
|
2
|
+
import { unlink, rmdir, rm } from 'node:fs/promises';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { log } from '../lib/logger.js';
|
|
5
|
+
import { readManifest } from '../lib/manifest.js';
|
|
6
|
+
|
|
7
|
+
const PRESERVE = [
|
|
8
|
+
'.claude/CLAUDE.md',
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
const PRESERVE_DIRS = [
|
|
12
|
+
'docs/',
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
export async function removeCommand(path) {
|
|
16
|
+
const targetDir = resolve(path);
|
|
17
|
+
const manifest = await readManifest(targetDir);
|
|
18
|
+
|
|
19
|
+
if (!manifest) {
|
|
20
|
+
log.fail('No manifest found. Nothing to remove.');
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
log.info('Removing claude-devkit files...');
|
|
25
|
+
log.blank();
|
|
26
|
+
|
|
27
|
+
// Remove tracked files (except preserved)
|
|
28
|
+
for (const file of Object.keys(manifest.files)) {
|
|
29
|
+
const fullPath = join(targetDir, file);
|
|
30
|
+
|
|
31
|
+
// Check if preserved
|
|
32
|
+
if (PRESERVE.includes(file)) {
|
|
33
|
+
log.keep(file);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Check if in preserved directory
|
|
38
|
+
if (PRESERVE_DIRS.some((dir) => file.startsWith(dir))) {
|
|
39
|
+
log.keep(file);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (existsSync(fullPath)) {
|
|
44
|
+
await unlink(fullPath);
|
|
45
|
+
log.del(file);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Remove manifest itself
|
|
50
|
+
const manifestPath = join(targetDir, '.claude/.devkit-manifest.json');
|
|
51
|
+
if (existsSync(manifestPath)) {
|
|
52
|
+
await unlink(manifestPath);
|
|
53
|
+
log.del('.claude/.devkit-manifest.json');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Clean up empty directories
|
|
57
|
+
const dirsToClean = [
|
|
58
|
+
'.claude/hooks',
|
|
59
|
+
'.claude/commands',
|
|
60
|
+
'scripts',
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
for (const dir of dirsToClean) {
|
|
64
|
+
const fullPath = join(targetDir, dir);
|
|
65
|
+
try {
|
|
66
|
+
await rmdir(fullPath);
|
|
67
|
+
} catch {
|
|
68
|
+
// Not empty or doesn't exist
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
log.blank();
|
|
73
|
+
log.pass('Removed. CLAUDE.md and docs/ preserved.');
|
|
74
|
+
}
|