spec-first-claude 0.5.0-beta.6 → 0.5.0-beta.7
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/cli.js +23 -5
- package/lib/update.js +131 -0
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -2,26 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const { init } = require('../lib/init');
|
|
5
|
+
const { update } = require('../lib/update');
|
|
5
6
|
|
|
6
7
|
const args = process.argv.slice(2);
|
|
7
8
|
const command = args[0];
|
|
8
9
|
|
|
9
10
|
function printUsage() {
|
|
10
|
-
console.log('Usage:
|
|
11
|
+
console.log('Usage:');
|
|
12
|
+
console.log(' spec-first-claude init --name=<project-name> [--target=<path>]');
|
|
13
|
+
console.log(' spec-first-claude update [--target=<path>] [--force]');
|
|
11
14
|
console.log('');
|
|
12
|
-
console.log('
|
|
15
|
+
console.log('Commands:');
|
|
16
|
+
console.log(' init Create a new spec-first project');
|
|
17
|
+
console.log(' update Update an existing project with new framework files');
|
|
18
|
+
console.log(' (adds missing files, updates framework files like commands/adapters/agents)');
|
|
13
19
|
console.log('');
|
|
14
20
|
console.log('Options:');
|
|
15
|
-
console.log(' --name=<name> Project name (required)');
|
|
16
|
-
console.log(' --target=<path> Target directory (defaults to ./<name>)');
|
|
21
|
+
console.log(' --name=<name> Project name (required for init)');
|
|
22
|
+
console.log(' --target=<path> Target directory (defaults to ./<name> for init, cwd for update)');
|
|
23
|
+
console.log(' --force Force update all files, not just framework files (use with caution)');
|
|
17
24
|
}
|
|
18
25
|
|
|
19
26
|
function parseArgs(args) {
|
|
20
|
-
const parsed = {};
|
|
27
|
+
const parsed = { flags: [] };
|
|
21
28
|
for (const arg of args) {
|
|
22
29
|
const match = arg.match(/^--(\w+)=(.+)$/);
|
|
23
30
|
if (match) {
|
|
24
31
|
parsed[match[1]] = match[2];
|
|
32
|
+
} else if (arg.startsWith('--')) {
|
|
33
|
+
parsed.flags.push(arg.slice(2));
|
|
25
34
|
}
|
|
26
35
|
}
|
|
27
36
|
return parsed;
|
|
@@ -43,6 +52,15 @@ if (command === 'init') {
|
|
|
43
52
|
templatesDir,
|
|
44
53
|
targetDir: opts.target || undefined,
|
|
45
54
|
});
|
|
55
|
+
} else if (command === 'update') {
|
|
56
|
+
const opts = parseArgs(args.slice(1));
|
|
57
|
+
const templatesDir = path.join(__dirname, '..', 'templates');
|
|
58
|
+
|
|
59
|
+
update({
|
|
60
|
+
templatesDir,
|
|
61
|
+
targetDir: opts.target || undefined,
|
|
62
|
+
force: opts.flags.includes('force'),
|
|
63
|
+
});
|
|
46
64
|
} else {
|
|
47
65
|
printUsage();
|
|
48
66
|
if (command) {
|
package/lib/update.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const FRAMEWORK_DIRS = [
|
|
5
|
+
path.join('.claude', 'adapters'),
|
|
6
|
+
path.join('.claude', 'commands'),
|
|
7
|
+
path.join('.claude', 'agents'),
|
|
8
|
+
path.join('.claude', 'templates'),
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
const FRAMEWORK_FILES = [
|
|
12
|
+
path.join('.claude', 'rules.md'),
|
|
13
|
+
'sfw.config.yml.example',
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
function update({ templatesDir, targetDir, force }) {
|
|
17
|
+
const dest = targetDir || process.cwd();
|
|
18
|
+
|
|
19
|
+
if (!fs.existsSync(dest)) {
|
|
20
|
+
console.error(`Error: directory "${dest}" does not exist.`);
|
|
21
|
+
console.error('Run "spec-first-claude init --name=<name>" first to create a project.');
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const claudeDir = path.join(dest, '.claude');
|
|
26
|
+
if (!fs.existsSync(claudeDir)) {
|
|
27
|
+
console.error('Error: not a spec-first project (missing .claude/ directory).');
|
|
28
|
+
console.error('Run "spec-first-claude init --name=<name>" first.');
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
console.log(`\nUpdating project in ${dest}\n`);
|
|
33
|
+
|
|
34
|
+
const stats = { added: [], updated: [], skipped: 0 };
|
|
35
|
+
syncDir(templatesDir, dest, force, stats, dest);
|
|
36
|
+
|
|
37
|
+
console.log('');
|
|
38
|
+
if (stats.added.length > 0) {
|
|
39
|
+
console.log(`Added (${stats.added.length}):`);
|
|
40
|
+
for (const f of stats.added) console.log(` + ${f}`);
|
|
41
|
+
}
|
|
42
|
+
if (stats.updated.length > 0) {
|
|
43
|
+
console.log(`Updated (${stats.updated.length}):`);
|
|
44
|
+
for (const f of stats.updated) console.log(` ~ ${f}`);
|
|
45
|
+
}
|
|
46
|
+
if (stats.added.length === 0 && stats.updated.length === 0) {
|
|
47
|
+
console.log('Already up to date — no new files to add.');
|
|
48
|
+
} else {
|
|
49
|
+
console.log(`\n${stats.added.length} added, ${stats.updated.length} updated, ${stats.skipped} unchanged`);
|
|
50
|
+
}
|
|
51
|
+
console.log('');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function syncDir(src, dest, force, stats, root) {
|
|
55
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
56
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
57
|
+
const srcPath = path.join(src, entry.name);
|
|
58
|
+
const destPath = path.join(dest, entry.name);
|
|
59
|
+
if (entry.isDirectory()) {
|
|
60
|
+
syncDir(srcPath, destPath, force, stats, root);
|
|
61
|
+
} else {
|
|
62
|
+
syncFile(srcPath, destPath, force, stats, root);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function syncFile(src, dest, force, stats, root) {
|
|
68
|
+
const rel = path.relative(root, dest);
|
|
69
|
+
const exists = fs.existsSync(dest);
|
|
70
|
+
|
|
71
|
+
if (exists && !force) {
|
|
72
|
+
// File exists and not forcing — check if it's a framework file we should update
|
|
73
|
+
const isFramework = isFrameworkPath(rel);
|
|
74
|
+
if (!isFramework) {
|
|
75
|
+
stats.skipped++;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
// Framework file — check if content differs
|
|
79
|
+
const srcContent = fs.readFileSync(src, 'utf-8');
|
|
80
|
+
const destContent = fs.readFileSync(dest, 'utf-8');
|
|
81
|
+
if (srcContent === destContent) {
|
|
82
|
+
stats.skipped++;
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
// Framework file with different content — update it
|
|
86
|
+
fs.writeFileSync(dest, srcContent, 'utf-8');
|
|
87
|
+
stats.updated.push(rel);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (exists && force) {
|
|
92
|
+
const srcContent = fs.readFileSync(src, 'utf-8');
|
|
93
|
+
const destContent = fs.readFileSync(dest, 'utf-8');
|
|
94
|
+
if (srcContent === destContent) {
|
|
95
|
+
stats.skipped++;
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
fs.writeFileSync(dest, srcContent, 'utf-8');
|
|
99
|
+
stats.updated.push(rel);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// File doesn't exist — add it
|
|
104
|
+
const textExtensions = ['.md', '.yaml', '.yml', '.json', '.js', '.ts', '.gitignore', '.gitkeep', ''];
|
|
105
|
+
const ext = path.extname(src).toLowerCase();
|
|
106
|
+
const basename = path.basename(src);
|
|
107
|
+
const isText = textExtensions.includes(ext) || basename.startsWith('.');
|
|
108
|
+
|
|
109
|
+
if (isText) {
|
|
110
|
+
const content = fs.readFileSync(src, 'utf-8');
|
|
111
|
+
fs.writeFileSync(dest, content, 'utf-8');
|
|
112
|
+
} else {
|
|
113
|
+
fs.copyFileSync(src, dest);
|
|
114
|
+
}
|
|
115
|
+
stats.added.push(rel);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function isFrameworkPath(rel) {
|
|
119
|
+
const normalized = rel.split(path.sep).join('/');
|
|
120
|
+
for (const dir of FRAMEWORK_DIRS) {
|
|
121
|
+
const normalizedDir = dir.split(path.sep).join('/');
|
|
122
|
+
if (normalized.startsWith(normalizedDir + '/')) return true;
|
|
123
|
+
}
|
|
124
|
+
for (const file of FRAMEWORK_FILES) {
|
|
125
|
+
const normalizedFile = file.split(path.sep).join('/');
|
|
126
|
+
if (normalized === normalizedFile) return true;
|
|
127
|
+
}
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
module.exports = { update };
|
package/package.json
CHANGED