openspec-stack-init 1.0.1 → 1.0.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.
Files changed (2) hide show
  1. package/package.json +2 -6
  2. package/src/index.js +108 -27
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.2",
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,24 @@ 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
+ // Switch to expanded profile to unlock: new, ff, verify, sync, bulk-archive, onboard
230
+ // openspec config profile sets the global default, openspec update regenerates skill files
231
+ const expanded = run("openspec config profile expanded", { silent: true }) &&
232
+ run("openspec update --tools claude --force", { silent: true });
233
+ if (expanded) {
234
+ log.ok("OpenSpec profile upgraded to expanded");
235
+ } else {
236
+ log.warn("Could not auto-upgrade to expanded profile.");
237
+ log.warn("Run manually: openspec config profile → then select expanded → openspec update");
238
+ }
163
239
  } 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");
240
+ log.warn("OpenSpec init failed — run manually: openspec init --tools claude --profile core --force");
167
241
  }
168
242
  } else {
169
243
  log.warn("openspec not found — skipping");
@@ -183,10 +257,10 @@ context: |
183
257
  Project: ${PROJECT_NAME}
184
258
 
185
259
  # 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
260
+ # Tech stack: e.g. TypeScript, React, Node.js / Unity, C# / Python, Django
261
+ # Architecture: e.g. MVC, HMVC, ECS, microservices
262
+ # Testing: e.g. Jest, NUnit, pytest
263
+ # Key constraints: e.g. legacy codebase, must support IE11, no breaking API changes
190
264
 
191
265
  rules:
192
266
  proposal:
@@ -214,10 +288,12 @@ const beadsInitialized =
214
288
  if (beadsInitialized) {
215
289
  log.skip("Beads already initialized");
216
290
  } else if (hasCmd("bd")) {
217
- // --quiet: non-interactive, no prompts, no spinner
218
- const ok = run("bd init --quiet");
291
+ // --quiet: non-interactive mode
292
+ // echo N: answers "Contributing to someone else's repo? [y/N]" automatically
293
+ const bdCmd = "echo N | bd init --quiet";
294
+ const ok = run(bdCmd);
219
295
  if (ok) log.ok("Beads initialized (quiet mode)");
220
- else log.warn("bd init failed — run manually: bd init --quiet");
296
+ else log.warn("bd init failed — run manually: echo N | bd init --quiet");
221
297
  } else {
222
298
  log.warn("bd not found — skipping Beads init");
223
299
  }
@@ -279,21 +355,26 @@ log.step("Skill — /migrate-to-openspec (brownfield migration)");
279
355
 
280
356
  // The skill files are bundled alongside this script in ../skills/
281
357
  import { fileURLToPath } from "url";
282
- import { dirname, join as pathJoin } from "path";
283
358
  import { cpSync } from "fs";
284
359
 
285
360
  const __filename = fileURLToPath(import.meta.url);
286
361
  const __dirname = dirname(__filename);
287
- const skillSrc = pathJoin(__dirname, "..", "skills", "migrate-to-openspec");
362
+ const skillSrc = join(__dirname, "..", "skills", "migrate-to-openspec");
288
363
  const skillDst = join(TARGET_DIR, ".claude", "skills", "migrate-to-openspec");
289
364
 
290
365
  if (existsSync(skillDst)) {
291
366
  log.skip(".claude/skills/migrate-to-openspec/ already exists");
292
367
  } else if (existsSync(skillSrc)) {
293
368
  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/");
369
+ try {
370
+ mkdirSync(join(TARGET_DIR, ".claude", "skills"), { recursive: true });
371
+ cpSync(skillSrc, skillDst, { recursive: true });
372
+ log.ok("Skill /migrate-to-openspec installed → .claude/skills/migrate-to-openspec/");
373
+ } catch (err) {
374
+ log.error(`Failed to install skill: ${err.message}`);
375
+ log.warn("Continuing without skill installation...");
376
+ // Don't exit - skill is optional enhancement
377
+ }
297
378
  } else {
298
379
  log.info("[DRY RUN] Would install: .claude/skills/migrate-to-openspec/");
299
380
  }