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.
- package/package.json +2 -6
- 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.
|
|
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
|
-
|
|
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,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
|
|
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
|
+
// 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
|
-
|
|
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:
|
|
187
|
-
# Architecture: HMVC
|
|
188
|
-
# Testing: NUnit
|
|
189
|
-
# Key constraints: legacy codebase,
|
|
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
|
|
218
|
-
|
|
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 =
|
|
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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
}
|