openspec-stack-init 1.0.1 → 1.0.3
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/README.md +20 -4
- package/package.json +2 -6
- package/src/index.js +116 -27
package/README.md
CHANGED
|
@@ -19,14 +19,30 @@ npx openspec-stack-init ./my-project --dry-run
|
|
|
19
19
|
|
|
20
20
|
## What it installs
|
|
21
21
|
|
|
22
|
-
| Tool | What it does |
|
|
22
|
+
| Tool | What it does | How |
|
|
23
23
|
|---|---|---|
|
|
24
|
-
| **OpenSpec** | Spec-driven development | `openspec init --tools claude --profile
|
|
25
|
-
| **Beads** | Git-backed issue tracker / agent memory | `bd init --quiet` + `bd setup claude` |
|
|
24
|
+
| **OpenSpec** | Spec-driven development | `openspec init --tools claude --profile core --force` |
|
|
25
|
+
| **Beads** | Git-backed issue tracker / agent memory | `bd init --quiet` + `bd setup claude` (CLI hooks) |
|
|
26
26
|
| **claude-mem** | Automatic session memory via hooks | `claude plugin marketplace add` + `claude plugin install` |
|
|
27
27
|
| **openspec-to-beads** | Syncs OpenSpec tasks.md → Beads issues | `npx @smithery/cli skill add` |
|
|
28
28
|
| **/migrate-to-openspec** | Brownfield migration skill | Copied to `.claude/skills/migrate-to-openspec/` |
|
|
29
29
|
|
|
30
|
+
### Beads: CLI vs Claude Code Plugin
|
|
31
|
+
|
|
32
|
+
This package installs **Beads CLI level** only:
|
|
33
|
+
|
|
34
|
+
- `bd init` — creates `.beads/` and `issues.jsonl` in your project (git-committed)
|
|
35
|
+
- `bd setup claude` — installs `SessionStart` and `PreCompact` hooks so Claude Code gets task context automatically
|
|
36
|
+
|
|
37
|
+
There is also an **optional Beads Claude Code Plugin** that adds slash commands (`/beads:ready`, `/beads:create`, etc.) and an MCP server. It cannot be installed automatically from a shell script — install it manually inside Claude Code if you want it:
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
/plugin marketplace add steveyegge/beads
|
|
41
|
+
/plugin install beads
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
For most projects the CLI level is sufficient — Claude Code agents call `bd` via bash.
|
|
45
|
+
|
|
30
46
|
## Prerequisites
|
|
31
47
|
|
|
32
48
|
> ⚠️ **This package does NOT install the tools below for you.**
|
|
@@ -101,4 +117,4 @@ node src/index.js ./target-project
|
|
|
101
117
|
```bash
|
|
102
118
|
npm publish --access public
|
|
103
119
|
# Then anyone can run: npx openspec-stack-init
|
|
104
|
-
```
|
|
120
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openspec-stack-init",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
|
+
"type": "module",
|
|
4
5
|
"description": "Initialize OpenSpec + Beads + claude-mem + skills in any project",
|
|
5
6
|
"author": {
|
|
6
7
|
"name": "VirtualMaestro",
|
|
@@ -20,11 +21,6 @@
|
|
|
20
21
|
"scripts": {
|
|
21
22
|
"start": "node src/index.js"
|
|
22
23
|
},
|
|
23
|
-
"dependencies": {
|
|
24
|
-
"chalk": "^5.3.0",
|
|
25
|
-
"execa": "^8.0.1",
|
|
26
|
-
"ora": "^8.0.1"
|
|
27
|
-
},
|
|
28
24
|
"engines": {
|
|
29
25
|
"node": ">=18.0.0"
|
|
30
26
|
},
|
package/src/index.js
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
// npx openspec-stack-init --dry-run — preview without executing
|
|
10
10
|
|
|
11
11
|
import { execSync, spawnSync } from "child_process";
|
|
12
|
-
import { existsSync, mkdirSync, writeFileSync, appendFileSync, readFileSync } from "fs";
|
|
13
|
-
import { resolve, basename, join } from "path";
|
|
12
|
+
import { existsSync, mkdirSync, writeFileSync, appendFileSync, readFileSync, statSync, unlinkSync } from "fs";
|
|
13
|
+
import { resolve, basename, join, sep, dirname } from "path";
|
|
14
14
|
import { platform } from "os";
|
|
15
15
|
import process from "process";
|
|
16
16
|
|
|
@@ -60,6 +60,21 @@ function run(cmd, opts = {}) {
|
|
|
60
60
|
});
|
|
61
61
|
return true;
|
|
62
62
|
} catch (e) {
|
|
63
|
+
// Provide detailed error information
|
|
64
|
+
const errorMsg = opts.silent && e.stderr
|
|
65
|
+
? e.stderr.toString().trim()
|
|
66
|
+
: e.message;
|
|
67
|
+
|
|
68
|
+
if (!opts.silent) {
|
|
69
|
+
log.error(`Command failed: ${cmd}`);
|
|
70
|
+
if (errorMsg) {
|
|
71
|
+
log.error(`Error: ${errorMsg}`);
|
|
72
|
+
}
|
|
73
|
+
if (e.status) {
|
|
74
|
+
log.error(`Exit code: ${e.status}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
63
78
|
return false;
|
|
64
79
|
}
|
|
65
80
|
}
|
|
@@ -77,18 +92,46 @@ function hasCmd(cmd) {
|
|
|
77
92
|
|
|
78
93
|
/** Write file only if it doesn't exist (or DRY_RUN) */
|
|
79
94
|
function writeIfMissing(filePath, content, description) {
|
|
80
|
-
|
|
95
|
+
// Normalize path separators for cross-platform compatibility
|
|
96
|
+
const normalizedPath = filePath.split(/[/\\]+/).join(sep);
|
|
97
|
+
|
|
98
|
+
// Resolve full path
|
|
99
|
+
const full = resolve(TARGET_DIR, normalizedPath);
|
|
100
|
+
|
|
101
|
+
// SECURITY: Verify resolved path stays within TARGET_DIR
|
|
102
|
+
const normalizedTarget = resolve(TARGET_DIR);
|
|
103
|
+
if (!full.startsWith(normalizedTarget + sep) && full !== normalizedTarget) {
|
|
104
|
+
log.error(`Security: Path traversal detected in "${filePath}"`);
|
|
105
|
+
log.error(`Attempted to write outside target directory`);
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
|
|
81
109
|
if (existsSync(full)) {
|
|
82
110
|
log.skip(`${filePath} already exists`);
|
|
83
111
|
return;
|
|
84
112
|
}
|
|
113
|
+
|
|
85
114
|
if (DRY_RUN) {
|
|
86
115
|
log.info(`[DRY RUN] Would create: ${filePath}`);
|
|
87
116
|
return;
|
|
88
117
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
118
|
+
|
|
119
|
+
// Use dirname instead of string manipulation
|
|
120
|
+
const parentDir = dirname(full);
|
|
121
|
+
try {
|
|
122
|
+
mkdirSync(parentDir, { recursive: true });
|
|
123
|
+
} catch (err) {
|
|
124
|
+
log.error(`Failed to create directory ${parentDir}: ${err.message}`);
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
writeFileSync(full, content, "utf8");
|
|
130
|
+
log.ok(`Created ${filePath}${description ? ` — ${description}` : ""}`);
|
|
131
|
+
} catch (err) {
|
|
132
|
+
log.error(`Failed to write ${filePath}: ${err.message}`);
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
92
135
|
}
|
|
93
136
|
|
|
94
137
|
/** Append to .gitignore if entry is missing */
|
|
@@ -110,7 +153,7 @@ function gitignoreAdd(entry, comment) {
|
|
|
110
153
|
// ─── Banner ───────────────────────────────────────────────────────────────────
|
|
111
154
|
console.log(`
|
|
112
155
|
${c.bold}╔══════════════════════════════════════════╗
|
|
113
|
-
║
|
|
156
|
+
║ OpenSpec Stack Init v1.0 ║
|
|
114
157
|
║ OpenSpec + Beads + claude-mem + skills ║
|
|
115
158
|
╚══════════════════════════════════════════╝${c.reset}
|
|
116
159
|
Project : ${c.cyan}${PROJECT_NAME}${c.reset}
|
|
@@ -123,6 +166,29 @@ if (!existsSync(TARGET_DIR)) {
|
|
|
123
166
|
process.exit(1);
|
|
124
167
|
}
|
|
125
168
|
|
|
169
|
+
// Validate it's actually a directory
|
|
170
|
+
try {
|
|
171
|
+
const stats = statSync(TARGET_DIR);
|
|
172
|
+
if (!stats.isDirectory()) {
|
|
173
|
+
log.error(`Target path is not a directory: ${TARGET_DIR}`);
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
} catch (err) {
|
|
177
|
+
log.error(`Failed to access target directory: ${err.message}`);
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Test write permissions
|
|
182
|
+
const testFile = join(TARGET_DIR, `.openspec-test-${Date.now()}`);
|
|
183
|
+
try {
|
|
184
|
+
writeFileSync(testFile, '');
|
|
185
|
+
unlinkSync(testFile);
|
|
186
|
+
} catch (permErr) {
|
|
187
|
+
log.error(`No write permission for directory: ${TARGET_DIR}`);
|
|
188
|
+
log.error(`Error: ${permErr.message}`);
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
|
|
126
192
|
// ─── 1. Dependency check ──────────────────────────────────────────────────────
|
|
127
193
|
log.step("Checking required tools");
|
|
128
194
|
|
|
@@ -154,16 +220,31 @@ log.step("OpenSpec — init");
|
|
|
154
220
|
if (existsSync(join(TARGET_DIR, "openspec"))) {
|
|
155
221
|
log.skip("openspec/ already exists");
|
|
156
222
|
} else if (hasCmd("openspec")) {
|
|
157
|
-
// --tools claude
|
|
158
|
-
// --profile
|
|
159
|
-
// --force
|
|
160
|
-
const ok = run("openspec init --tools claude --profile
|
|
223
|
+
// --tools claude : select Claude Code without interactive prompt
|
|
224
|
+
// --profile core : valid profile for init (expanded is set separately after)
|
|
225
|
+
// --force : skip all remaining prompts, auto-cleanup legacy files
|
|
226
|
+
const ok = run("openspec init --tools claude --profile core --force");
|
|
161
227
|
if (ok) {
|
|
162
|
-
log.ok("OpenSpec initialized (
|
|
228
|
+
log.ok("OpenSpec initialized (core profile, Claude tools)");
|
|
229
|
+
|
|
230
|
+
// Remove default config.yaml so custom template can be written in Step 3
|
|
231
|
+
const defaultConfig = join(TARGET_DIR, "openspec", "config.yaml");
|
|
232
|
+
if (existsSync(defaultConfig)) {
|
|
233
|
+
unlinkSync(defaultConfig);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Switch to expanded profile to unlock: new, ff, verify, sync, bulk-archive, onboard
|
|
237
|
+
// openspec config profile sets the global default, openspec update regenerates skill files
|
|
238
|
+
const expanded = run("openspec config profile expanded", { silent: true }) &&
|
|
239
|
+
run("openspec update --tools claude --force", { silent: true });
|
|
240
|
+
if (expanded) {
|
|
241
|
+
log.ok("OpenSpec profile upgraded to expanded");
|
|
242
|
+
} else {
|
|
243
|
+
log.warn("Could not auto-upgrade to expanded profile.");
|
|
244
|
+
log.warn("Run manually: openspec config profile → then select expanded → openspec update");
|
|
245
|
+
}
|
|
163
246
|
} else {
|
|
164
|
-
|
|
165
|
-
log.warn("Non-interactive init failed — trying interactive...");
|
|
166
|
-
log.warn("Run manually: openspec init --tools claude --profile expanded");
|
|
247
|
+
log.warn("OpenSpec init failed — run manually: openspec init --tools claude --profile core --force");
|
|
167
248
|
}
|
|
168
249
|
} else {
|
|
169
250
|
log.warn("openspec not found — skipping");
|
|
@@ -183,15 +264,16 @@ context: |
|
|
|
183
264
|
Project: ${PROJECT_NAME}
|
|
184
265
|
|
|
185
266
|
# TODO: fill in your actual stack and conventions
|
|
186
|
-
# Tech stack:
|
|
187
|
-
# Architecture: HMVC
|
|
188
|
-
# Testing: NUnit
|
|
189
|
-
# Key constraints: legacy codebase,
|
|
267
|
+
# Tech stack: e.g. TypeScript, React, Node.js / Unity, C# / Python, Django
|
|
268
|
+
# Architecture: e.g. MVC, HMVC, ECS, Redux, MVVM, microservices
|
|
269
|
+
# Testing: e.g. Jest, NUnit, pytest
|
|
270
|
+
# Key constraints: e.g. legacy codebase, must support IE11, no breaking API changes
|
|
190
271
|
|
|
191
272
|
rules:
|
|
192
273
|
proposal:
|
|
193
274
|
- Always include a rollback plan for legacy code changes
|
|
194
275
|
- List all affected modules
|
|
276
|
+
- Always include an "## Alternatives Considered" section listing at least 2 alternative approaches with their pros, cons, and reason for rejection. Format each as: "### Option: <name> / Pros: ... / Cons: ... / Why rejected: ..."
|
|
195
277
|
specs:
|
|
196
278
|
- Use Given/When/Then format for scenarios
|
|
197
279
|
design:
|
|
@@ -214,10 +296,12 @@ const beadsInitialized =
|
|
|
214
296
|
if (beadsInitialized) {
|
|
215
297
|
log.skip("Beads already initialized");
|
|
216
298
|
} else if (hasCmd("bd")) {
|
|
217
|
-
// --quiet: non-interactive
|
|
218
|
-
|
|
299
|
+
// --quiet: non-interactive mode
|
|
300
|
+
// echo N: answers "Contributing to someone else's repo? [y/N]" automatically
|
|
301
|
+
const bdCmd = "echo N | bd init --quiet";
|
|
302
|
+
const ok = run(bdCmd);
|
|
219
303
|
if (ok) log.ok("Beads initialized (quiet mode)");
|
|
220
|
-
else log.warn("bd init failed — run manually: bd init --quiet");
|
|
304
|
+
else log.warn("bd init failed — run manually: echo N | bd init --quiet");
|
|
221
305
|
} else {
|
|
222
306
|
log.warn("bd not found — skipping Beads init");
|
|
223
307
|
}
|
|
@@ -279,21 +363,26 @@ log.step("Skill — /migrate-to-openspec (brownfield migration)");
|
|
|
279
363
|
|
|
280
364
|
// The skill files are bundled alongside this script in ../skills/
|
|
281
365
|
import { fileURLToPath } from "url";
|
|
282
|
-
import { dirname, join as pathJoin } from "path";
|
|
283
366
|
import { cpSync } from "fs";
|
|
284
367
|
|
|
285
368
|
const __filename = fileURLToPath(import.meta.url);
|
|
286
369
|
const __dirname = dirname(__filename);
|
|
287
|
-
const skillSrc =
|
|
370
|
+
const skillSrc = join(__dirname, "..", "skills", "migrate-to-openspec");
|
|
288
371
|
const skillDst = join(TARGET_DIR, ".claude", "skills", "migrate-to-openspec");
|
|
289
372
|
|
|
290
373
|
if (existsSync(skillDst)) {
|
|
291
374
|
log.skip(".claude/skills/migrate-to-openspec/ already exists");
|
|
292
375
|
} else if (existsSync(skillSrc)) {
|
|
293
376
|
if (!DRY_RUN) {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
377
|
+
try {
|
|
378
|
+
mkdirSync(join(TARGET_DIR, ".claude", "skills"), { recursive: true });
|
|
379
|
+
cpSync(skillSrc, skillDst, { recursive: true });
|
|
380
|
+
log.ok("Skill /migrate-to-openspec installed → .claude/skills/migrate-to-openspec/");
|
|
381
|
+
} catch (err) {
|
|
382
|
+
log.error(`Failed to install skill: ${err.message}`);
|
|
383
|
+
log.warn("Continuing without skill installation...");
|
|
384
|
+
// Don't exit - skill is optional enhancement
|
|
385
|
+
}
|
|
297
386
|
} else {
|
|
298
387
|
log.info("[DRY RUN] Would install: .claude/skills/migrate-to-openspec/");
|
|
299
388
|
}
|