spec-first-copilot 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 +129 -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-copilot init --name=<project-name> [--target=<path>]');
11
+ console.log('Usage:');
12
+ console.log(' spec-first-copilot init --name=<project-name> [--target=<path>]');
13
+ console.log(' spec-first-copilot update [--target=<path>] [--force]');
11
14
  console.log('');
12
- console.log('Creates a new spec-first project configured for GitHub Copilot.');
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 skills/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,129 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const FRAMEWORK_DIRS = [
5
+ path.join('.github', 'adapters'),
6
+ path.join('.github', 'skills'),
7
+ path.join('.github', 'agents'),
8
+ path.join('.github', 'templates'),
9
+ path.join('.github', 'instructions'),
10
+ ];
11
+
12
+ const FRAMEWORK_FILES = [
13
+ path.join('.github', 'rules.md'),
14
+ path.join('.github', 'copilot-instructions.md'),
15
+ 'sfw.config.yml.example',
16
+ ];
17
+
18
+ function update({ templatesDir, targetDir, force }) {
19
+ const dest = targetDir || process.cwd();
20
+
21
+ if (!fs.existsSync(dest)) {
22
+ console.error(`Error: directory "${dest}" does not exist.`);
23
+ console.error('Run "spec-first-copilot init --name=<name>" first to create a project.');
24
+ process.exit(1);
25
+ }
26
+
27
+ const githubDir = path.join(dest, '.github');
28
+ if (!fs.existsSync(githubDir)) {
29
+ console.error('Error: not a spec-first project (missing .github/ directory).');
30
+ console.error('Run "spec-first-copilot init --name=<name>" first.');
31
+ process.exit(1);
32
+ }
33
+
34
+ console.log(`\nUpdating project in ${dest}\n`);
35
+
36
+ const stats = { added: [], updated: [], skipped: 0 };
37
+ syncDir(templatesDir, dest, force, stats, dest);
38
+
39
+ console.log('');
40
+ if (stats.added.length > 0) {
41
+ console.log(`Added (${stats.added.length}):`);
42
+ for (const f of stats.added) console.log(` + ${f}`);
43
+ }
44
+ if (stats.updated.length > 0) {
45
+ console.log(`Updated (${stats.updated.length}):`);
46
+ for (const f of stats.updated) console.log(` ~ ${f}`);
47
+ }
48
+ if (stats.added.length === 0 && stats.updated.length === 0) {
49
+ console.log('Already up to date — no new files to add.');
50
+ } else {
51
+ console.log(`\n${stats.added.length} added, ${stats.updated.length} updated, ${stats.skipped} unchanged`);
52
+ }
53
+ console.log('');
54
+ }
55
+
56
+ function syncDir(src, dest, force, stats, root) {
57
+ fs.mkdirSync(dest, { recursive: true });
58
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
59
+ const srcPath = path.join(src, entry.name);
60
+ const destPath = path.join(dest, entry.name);
61
+ if (entry.isDirectory()) {
62
+ syncDir(srcPath, destPath, force, stats, root);
63
+ } else {
64
+ syncFile(srcPath, destPath, force, stats, root);
65
+ }
66
+ }
67
+ }
68
+
69
+ function syncFile(src, dest, force, stats, root) {
70
+ const rel = path.relative(root, dest);
71
+ const exists = fs.existsSync(dest);
72
+
73
+ if (exists && !force) {
74
+ const isFramework = isFrameworkPath(rel);
75
+ if (!isFramework) {
76
+ stats.skipped++;
77
+ return;
78
+ }
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
+ fs.writeFileSync(dest, srcContent, 'utf-8');
86
+ stats.updated.push(rel);
87
+ return;
88
+ }
89
+
90
+ if (exists && force) {
91
+ const srcContent = fs.readFileSync(src, 'utf-8');
92
+ const destContent = fs.readFileSync(dest, 'utf-8');
93
+ if (srcContent === destContent) {
94
+ stats.skipped++;
95
+ return;
96
+ }
97
+ fs.writeFileSync(dest, srcContent, 'utf-8');
98
+ stats.updated.push(rel);
99
+ return;
100
+ }
101
+
102
+ const textExtensions = ['.md', '.yaml', '.yml', '.json', '.js', '.ts', '.gitignore', '.gitkeep', ''];
103
+ const ext = path.extname(src).toLowerCase();
104
+ const basename = path.basename(src);
105
+ const isText = textExtensions.includes(ext) || basename.startsWith('.');
106
+
107
+ if (isText) {
108
+ const content = fs.readFileSync(src, 'utf-8');
109
+ fs.writeFileSync(dest, content, 'utf-8');
110
+ } else {
111
+ fs.copyFileSync(src, dest);
112
+ }
113
+ stats.added.push(rel);
114
+ }
115
+
116
+ function isFrameworkPath(rel) {
117
+ const normalized = rel.split(path.sep).join('/');
118
+ for (const dir of FRAMEWORK_DIRS) {
119
+ const normalizedDir = dir.split(path.sep).join('/');
120
+ if (normalized.startsWith(normalizedDir + '/')) return true;
121
+ }
122
+ for (const file of FRAMEWORK_FILES) {
123
+ const normalizedFile = file.split(path.sep).join('/');
124
+ if (normalized === normalizedFile) return true;
125
+ }
126
+ return false;
127
+ }
128
+
129
+ module.exports = { update };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spec-first-copilot",
3
- "version": "0.5.0-beta.6",
3
+ "version": "0.5.0-beta.7",
4
4
  "description": "Spec-first workflow kit for GitHub Copilot — AI-driven development with specs, not guesswork",
5
5
  "bin": {
6
6
  "spec-first-copilot": "bin/cli.js"