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.
Files changed (3) hide show
  1. package/bin/cli.js +23 -5
  2. package/lib/update.js +131 -0
  3. 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: spec-first-claude init --name=<project-name> [--target=<path>]');
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('Creates a new spec-first project configured for Claude Code.');
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spec-first-claude",
3
- "version": "0.5.0-beta.6",
3
+ "version": "0.5.0-beta.7",
4
4
  "description": "Spec-first workflow kit for Claude Code — AI-driven development with specs, not guesswork",
5
5
  "bin": {
6
6
  "spec-first-claude": "bin/cli.js"