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.
Files changed (3) hide show
  1. package/README.md +20 -4
  2. package/package.json +2 -6
  3. 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 | Non-interactive flag used |
22
+ | Tool | What it does | How |
23
23
  |---|---|---|
24
- | **OpenSpec** | Spec-driven development | `openspec init --tools claude --profile expanded --force` |
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.1",
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
- const full = join(TARGET_DIR, filePath);
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
- mkdirSync(join(TARGET_DIR, filePath.split("/").slice(0, -1).join("/")), { recursive: true });
90
- writeFileSync(full, content, "utf8");
91
- log.ok(`Created ${filePath}${description ? ` — ${description}` : ""}`);
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
- AI Stack Init v1.0
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 : select Claude Code without interactive prompt
158
- // --profile expanded: enable all workflow commands (new, ff, verify, sync, etc.)
159
- // --force : skip all remaining prompts, auto-cleanup legacy files
160
- const ok = run("openspec init --tools claude --profile expanded --force");
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 (expanded profile, Claude tools)");
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
- // Fallback: openspec init may not support --force on older versions
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: Unity 2022, C#, Flutter backend
187
- # Architecture: HMVC
188
- # Testing: NUnit
189
- # Key constraints: legacy codebase, brownfield migration
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, no prompts, no spinner
218
- const ok = run("bd init --quiet");
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 = pathJoin(__dirname, "..", "skills", "migrate-to-openspec");
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
- mkdirSync(join(TARGET_DIR, ".claude", "skills"), { recursive: true });
295
- cpSync(skillSrc, skillDst, { recursive: true });
296
- log.ok("Skill /migrate-to-openspec installed .claude/skills/migrate-to-openspec/");
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
  }