@houseofwolvesllc/claude-scrum-skill 2.1.1 → 2.1.2

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "claude-scrum-skill",
3
3
  "description": "Complete scrum pipeline — PRD to production release with Claude as scrum master. Includes project scaffolding, sprint planning, status tracking, sprint releases, and full-project emulation testing.",
4
- "version": "2.1.1",
4
+ "version": "2.1.2",
5
5
  "author": {
6
6
  "name": "House of Wolves LLC"
7
7
  },
package/bin/install.js CHANGED
@@ -7,24 +7,7 @@ const HOME = process.env.HOME || process.env.USERPROFILE;
7
7
  const SOURCE_DIR = path.join(__dirname, '..', 'skills');
8
8
  const WORKFLOWS_SOURCE_DIR = path.join(__dirname, '..', 'lib', 'workflows');
9
9
  const IS_GLOBAL = process.env.npm_config_global === 'true';
10
-
11
- // Global install → ~/.claude/skills/
12
- // Local install → <project>/.claude/skills/
13
- let skillsDir;
14
- if (IS_GLOBAL) {
15
- skillsDir = path.join(HOME, '.claude', 'skills');
16
- } else {
17
- // Walk up from node_modules to find the project root
18
- let projectRoot = path.resolve(__dirname, '..');
19
- while (projectRoot !== path.dirname(projectRoot)) {
20
- if (path.basename(projectRoot) === 'node_modules') {
21
- projectRoot = path.dirname(projectRoot);
22
- break;
23
- }
24
- projectRoot = path.dirname(projectRoot);
25
- }
26
- skillsDir = path.join(projectRoot, '.claude', 'skills');
27
- }
10
+ const CONFIG_FILENAME = 'config.json';
28
11
 
29
12
  const skills = [
30
13
  'project-scaffold',
@@ -37,76 +20,196 @@ const skills = [
37
20
  'project-cleanup'
38
21
  ];
39
22
 
40
- function copyRecursive(src, dest) {
41
- if (!fs.existsSync(src)) return;
23
+ function main() {
24
+ const skillsDir = resolveSkillsDir();
25
+ const location = IS_GLOBAL ? 'global (~/.claude/skills/)' : `project (${skillsDir})`;
26
+ console.log(`\n📋 Installing claude-scrum-skill (${location})...\n`);
42
27
 
43
- if (fs.statSync(src).isDirectory()) {
44
- fs.mkdirSync(dest, { recursive: true });
45
- for (const item of fs.readdirSync(src)) {
46
- copyRecursive(path.join(src, item), path.join(dest, item));
47
- }
48
- } else {
49
- fs.copyFileSync(src, dest);
28
+ fs.mkdirSync(skillsDir, { recursive: true });
29
+
30
+ installSharedReferences(skillsDir);
31
+ const installed = installSkills(skillsDir);
32
+ installWorkflows(skillsDir);
33
+
34
+ if (!IS_GLOBAL) {
35
+ ensureGitignoreEntry(skillsDir);
50
36
  }
37
+
38
+ console.log(`\n✨ Installed ${installed} skills to ${skillsDir}`);
39
+ console.log(' Restart Claude Code for the skills to become available.\n');
40
+ console.log(' Run /project-scaffold <prd-path> to get started.\n');
51
41
  }
52
42
 
53
- const location = IS_GLOBAL ? 'global (~/.claude/skills/)' : `project (${skillsDir})`;
54
- console.log(`\n📋 Installing claude-scrum-skill (${location})...\n`);
43
+ function resolveSkillsDir() {
44
+ if (IS_GLOBAL) {
45
+ return path.join(HOME, '.claude', 'skills');
46
+ }
47
+
48
+ // Walk up from node_modules to find the project root.
49
+ let projectRoot = path.resolve(__dirname, '..');
50
+ while (projectRoot !== path.dirname(projectRoot)) {
51
+ if (path.basename(projectRoot) === 'node_modules') {
52
+ projectRoot = path.dirname(projectRoot);
53
+ break;
54
+ }
55
+ projectRoot = path.dirname(projectRoot);
56
+ }
57
+ return path.join(projectRoot, '.claude', 'skills');
58
+ }
55
59
 
56
- fs.mkdirSync(skillsDir, { recursive: true });
60
+ function installSharedReferences(skillsDir) {
61
+ const sharedSrc = path.join(SOURCE_DIR, 'shared');
62
+ const sharedDest = path.join(skillsDir, 'shared');
63
+ if (!fs.existsSync(sharedSrc)) return;
57
64
 
58
- // Copy shared references (not a skill, but needed by skills at ../shared/)
59
- const sharedSrc = path.join(SOURCE_DIR, 'shared');
60
- const sharedDest = path.join(skillsDir, 'shared');
61
- if (fs.existsSync(sharedSrc)) {
62
- copyRecursive(sharedSrc, sharedDest);
65
+ // config.json is user-owned: copy everything else, then merge it explicitly.
66
+ const skipConfig = path.join(sharedSrc, CONFIG_FILENAME);
67
+ copyRecursive(sharedSrc, sharedDest, skipConfig);
63
68
  console.log(' 📁 shared references');
69
+
70
+ installSharedConfig(sharedSrc, sharedDest);
64
71
  }
65
72
 
66
- let installed = 0;
67
- for (const skill of skills) {
68
- const src = path.join(SOURCE_DIR, skill);
69
- const dest = path.join(skillsDir, skill);
73
+ function installSharedConfig(sharedSrc, sharedDest) {
74
+ const defaultPath = path.join(sharedSrc, CONFIG_FILENAME);
75
+ const destPath = path.join(sharedDest, CONFIG_FILENAME);
70
76
 
71
- if (fs.existsSync(src)) {
72
- copyRecursive(src, dest);
73
- console.log(` ✅ ${skill}`);
74
- installed++;
75
- } else {
76
- console.log(` ⚠️ ${skill} — source not found, skipping`);
77
+ try {
78
+ const { action } = installConfig(defaultPath, destPath);
79
+ console.log(configLogLine(action));
80
+ } catch (error) {
81
+ console.log(` ⚠️ config install failed (${error.message}) skills still installed`);
77
82
  }
78
83
  }
79
84
 
85
+ function configLogLine(action) {
86
+ switch (action) {
87
+ case 'default':
88
+ return ' 📁 config (default)';
89
+ case 'merged':
90
+ return ' 🔧 config (merged — your settings preserved)';
91
+ case 'recovered':
92
+ return ` ⚠️ config was invalid JSON — backed up to ${CONFIG_FILENAME}.bak, wrote default`;
93
+ default:
94
+ throw new Error(`unknown config action: ${action}`);
95
+ }
96
+ }
97
+
98
+ function installSkills(skillsDir) {
99
+ let installed = 0;
100
+ for (const skill of skills) {
101
+ const src = path.join(SOURCE_DIR, skill);
102
+ const dest = path.join(skillsDir, skill);
103
+
104
+ if (fs.existsSync(src)) {
105
+ copyRecursive(src, dest);
106
+ console.log(` ✅ ${skill}`);
107
+ installed++;
108
+ } else {
109
+ console.log(` ⚠️ ${skill} — source not found, skipping`);
110
+ }
111
+ }
112
+ return installed;
113
+ }
114
+
80
115
  // Copy lib/workflows/ → <skillsDir>/_workflows/ (v2.0.0+).
81
- // Underscore prefix prevents Claude Code from registering as a skill.
82
- if (fs.existsSync(WORKFLOWS_SOURCE_DIR)) {
116
+ // Underscore prefix prevents Claude Code from registering it as a skill.
117
+ function installWorkflows(skillsDir) {
118
+ if (!fs.existsSync(WORKFLOWS_SOURCE_DIR)) return;
83
119
  const workflowsDest = path.join(skillsDir, '_workflows');
84
120
  copyRecursive(WORKFLOWS_SOURCE_DIR, workflowsDest);
85
121
  console.log(' ⚙️ _workflows (lib/workflows + schemas)');
86
122
  }
87
123
 
88
- // Add .claude-scrum-skill to .gitignore if not already present (local install only)
89
- if (!IS_GLOBAL) {
124
+ function ensureGitignoreEntry(skillsDir) {
90
125
  const projectRoot = path.resolve(skillsDir, '..', '..');
91
126
  const gitignorePath = path.join(projectRoot, '.gitignore');
92
127
  const entry = '.claude-scrum-skill';
93
128
 
94
- let needsAppend = true;
95
- if (fs.existsSync(gitignorePath)) {
96
- const contents = fs.readFileSync(gitignorePath, 'utf8');
97
- needsAppend = !contents.split('\n').some(line => line.trim() === entry);
129
+ const existing = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, 'utf8') : '';
130
+ if (existing.split('\n').some(line => line.trim() === entry)) return;
131
+
132
+ const prefix = existing.length > 0 && !existing.endsWith('\n') ? '\n' : '';
133
+ fs.appendFileSync(gitignorePath, `${prefix}${entry}\n`);
134
+ console.log(`\n 📝 Added ${entry} to .gitignore`);
135
+ }
136
+
137
+ function copyRecursive(src, dest, skipPath) {
138
+ if (!fs.existsSync(src)) return;
139
+ if (skipPath && path.resolve(src) === path.resolve(skipPath)) return;
140
+
141
+ if (fs.statSync(src).isDirectory()) {
142
+ fs.mkdirSync(dest, { recursive: true });
143
+ for (const item of fs.readdirSync(src)) {
144
+ copyRecursive(path.join(src, item), path.join(dest, item), skipPath);
145
+ }
146
+ } else {
147
+ fs.copyFileSync(src, dest);
148
+ }
149
+ }
150
+
151
+ function isPlainObject(value) {
152
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
153
+ }
154
+
155
+ // Pure deep-merge: returns a new object. Recurses plain objects; arrays and
156
+ // scalars are leaves with the override winning. Orphan override keys survive.
157
+ function deepMerge(defaults, overrides) {
158
+ const merged = { ...defaults };
159
+
160
+ for (const key of Object.keys(overrides)) {
161
+ const defaultValue = defaults[key];
162
+ const overrideValue = overrides[key];
163
+
164
+ if (isPlainObject(defaultValue) && isPlainObject(overrideValue)) {
165
+ merged[key] = deepMerge(defaultValue, overrideValue);
166
+ } else {
167
+ merged[key] = overrideValue;
168
+ }
169
+ }
170
+
171
+ return merged;
172
+ }
173
+
174
+ function serializeConfig(config) {
175
+ return `${JSON.stringify(config, null, 2)}\n`;
176
+ }
177
+
178
+ // Three-branch config installer. Returns { action }:
179
+ // 'default' — dest absent, default copied verbatim.
180
+ // 'merged' — dest valid JSON, deep-merged with user values winning.
181
+ // 'recovered' — dest invalid JSON, backed up to .bak, default written.
182
+ function installConfig(defaultPath, destPath) {
183
+ const defaults = JSON.parse(fs.readFileSync(defaultPath, 'utf8'));
184
+
185
+ if (!fs.existsSync(destPath)) {
186
+ fs.writeFileSync(destPath, serializeConfig(defaults));
187
+ return { action: 'default' };
98
188
  }
99
189
 
100
- if (needsAppend) {
101
- const prefix = fs.existsSync(gitignorePath)
102
- && fs.readFileSync(gitignorePath, 'utf8').length > 0
103
- && !fs.readFileSync(gitignorePath, 'utf8').endsWith('\n')
104
- ? '\n' : '';
105
- fs.appendFileSync(gitignorePath, `${prefix}${entry}\n`);
106
- console.log(`\n 📝 Added ${entry} to .gitignore`);
190
+ const rawDest = fs.readFileSync(destPath, 'utf8');
191
+ const userConfig = parseConfig(rawDest);
192
+
193
+ if (userConfig === undefined) {
194
+ fs.copyFileSync(destPath, `${destPath}.bak`);
195
+ fs.writeFileSync(destPath, serializeConfig(defaults));
196
+ return { action: 'recovered' };
107
197
  }
198
+
199
+ fs.writeFileSync(destPath, serializeConfig(deepMerge(defaults, userConfig)));
200
+ return { action: 'merged' };
201
+ }
202
+
203
+ function parseConfig(raw) {
204
+ try {
205
+ return JSON.parse(raw);
206
+ } catch {
207
+ return undefined;
208
+ }
209
+ }
210
+
211
+ if (require.main === module) {
212
+ main();
108
213
  }
109
214
 
110
- console.log(`\n✨ Installed ${installed} skills to ${skillsDir}`);
111
- console.log(' Restart Claude Code for the skills to become available.\n');
112
- console.log(' Run /project-scaffold <prd-path> to get started.\n');
215
+ module.exports = { deepMerge, installConfig };
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "@houseofwolvesllc/claude-scrum-skill",
3
- "version": "2.1.1",
3
+ "version": "2.1.2",
4
4
  "description": "Claude Code skills for scrum project management — PRD to production release pipeline with project scaffolding, sprint planning, status tracking, sprint releases, full-project emulation testing, autonomous orchestration, and project cleanup.",
5
5
  "bin": {},
6
6
  "publishConfig": {
7
7
  "access": "public"
8
8
  },
9
9
  "scripts": {
10
- "postinstall": "node bin/install.js"
10
+ "postinstall": "node bin/install.js",
11
+ "test": "node --test"
11
12
  },
12
13
  "keywords": [
13
14
  "claude",