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 ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { cli } from '../src/cli.js';
3
+ cli(process.argv);
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
+ }