orchestr8 2.8.0 → 3.0.0

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 (26) hide show
  1. package/.blueprint/agents/AGENT_BA_CASS.md +18 -34
  2. package/.blueprint/agents/AGENT_DEVELOPER_CODEY.md +21 -28
  3. package/.blueprint/agents/AGENT_SPECIFICATION_ALEX.md +6 -0
  4. package/.blueprint/agents/AGENT_TESTER_NIGEL.md +5 -3
  5. package/.blueprint/agents/WHAT_WE_STAND_FOR.md +0 -0
  6. package/.blueprint/features/feature_interactive-alex/FEATURE_SPEC.md +263 -0
  7. package/.blueprint/features/feature_interactive-alex/IMPLEMENTATION_PLAN.md +69 -0
  8. package/.blueprint/features/feature_interactive-alex/handoff-alex.md +19 -0
  9. package/.blueprint/features/feature_interactive-alex/handoff-cass.md +21 -0
  10. package/.blueprint/features/feature_interactive-alex/handoff-nigel.md +19 -0
  11. package/.blueprint/features/feature_interactive-alex/story-flag-routing.md +54 -0
  12. package/.blueprint/features/feature_interactive-alex/story-iterative-drafting.md +65 -0
  13. package/.blueprint/features/feature_interactive-alex/story-pipeline-integration.md +66 -0
  14. package/.blueprint/features/feature_interactive-alex/story-session-lifecycle.md +75 -0
  15. package/.blueprint/features/feature_interactive-alex/story-system-spec-creation.md +57 -0
  16. package/.blueprint/prompts/codey-implement-runtime.md +1 -1
  17. package/.blueprint/prompts/nigel-runtime.md +1 -1
  18. package/.blueprint/ways_of_working/DEVELOPMENT_RITUAL.md +4 -4
  19. package/README.md +31 -0
  20. package/SKILL.md +35 -1
  21. package/bin/cli.js +28 -0
  22. package/package.json +2 -2
  23. package/src/index.js +61 -1
  24. package/src/init.js +21 -3
  25. package/src/interactive.js +338 -0
  26. package/src/stack.js +320 -0
package/src/stack.js ADDED
@@ -0,0 +1,320 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const CONFIG_FILE = '.claude/stack-config.json';
5
+
6
+ /**
7
+ * Returns the default (empty) stack configuration.
8
+ */
9
+ function getDefaultStackConfig() {
10
+ return {
11
+ language: '',
12
+ runtime: '',
13
+ packageManager: '',
14
+ frameworks: [],
15
+ testRunner: '',
16
+ testCommand: '',
17
+ linter: '',
18
+ tools: []
19
+ };
20
+ }
21
+
22
+ /**
23
+ * Ensures the .claude directory exists.
24
+ */
25
+ function ensureConfigDir() {
26
+ const dir = path.dirname(CONFIG_FILE);
27
+ if (!fs.existsSync(dir)) {
28
+ fs.mkdirSync(dir, { recursive: true });
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Reads the stack config from file.
34
+ * Returns defaults if file is missing or corrupted.
35
+ */
36
+ function readStackConfig() {
37
+ ensureConfigDir();
38
+ if (!fs.existsSync(CONFIG_FILE)) {
39
+ return getDefaultStackConfig();
40
+ }
41
+ try {
42
+ const content = fs.readFileSync(CONFIG_FILE, 'utf8');
43
+ return JSON.parse(content);
44
+ } catch (err) {
45
+ return getDefaultStackConfig();
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Writes the stack config to file.
51
+ */
52
+ function writeStackConfig(config) {
53
+ ensureConfigDir();
54
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
55
+ }
56
+
57
+ /**
58
+ * Resets stack config to defaults by writing empty config to file.
59
+ */
60
+ function resetStackConfig() {
61
+ writeStackConfig(getDefaultStackConfig());
62
+ }
63
+
64
+ const VALID_KEYS = [
65
+ 'language', 'runtime', 'packageManager',
66
+ 'frameworks', 'testRunner', 'testCommand',
67
+ 'linter', 'tools'
68
+ ];
69
+
70
+ const ARRAY_KEYS = ['frameworks', 'tools'];
71
+
72
+ /**
73
+ * Sets a config value by key.
74
+ * Array keys (frameworks, tools) accept JSON array strings.
75
+ * @param {string} key - Config key
76
+ * @param {string} value - New value (string or JSON array string)
77
+ */
78
+ function setStackConfigValue(key, value) {
79
+ if (!VALID_KEYS.includes(key)) {
80
+ throw new Error(
81
+ `Unknown config key: ${key}. Valid keys: ${VALID_KEYS.join(', ')}`
82
+ );
83
+ }
84
+
85
+ const config = readStackConfig();
86
+
87
+ if (ARRAY_KEYS.includes(key)) {
88
+ // Try parsing as JSON array
89
+ try {
90
+ const parsed = JSON.parse(value);
91
+ if (!Array.isArray(parsed)) {
92
+ throw new Error(`${key} must be a JSON array, e.g. '["express","react"]'`);
93
+ }
94
+ config[key] = parsed;
95
+ } catch (err) {
96
+ if (err.message.includes('must be a JSON array')) throw err;
97
+ throw new Error(`${key} must be a valid JSON array, e.g. '["express","react"]'`);
98
+ }
99
+ } else {
100
+ config[key] = value;
101
+ }
102
+
103
+ writeStackConfig(config);
104
+ const display = Array.isArray(config[key]) ? JSON.stringify(config[key]) : config[key];
105
+ console.log(`Set ${key} = ${display}`);
106
+ }
107
+
108
+ /**
109
+ * Auto-detect tech stack from project files.
110
+ * Scans for manifest files and infers configuration.
111
+ * @param {string} projectDir - Directory to scan (defaults to cwd)
112
+ * @returns {object} Detected stack config
113
+ */
114
+ function detectStackConfig(projectDir) {
115
+ const dir = projectDir || process.cwd();
116
+ const config = getDefaultStackConfig();
117
+
118
+ const exists = (file) => fs.existsSync(path.join(dir, file));
119
+ const readJSON = (file) => {
120
+ try {
121
+ return JSON.parse(fs.readFileSync(path.join(dir, file), 'utf8'));
122
+ } catch {
123
+ return null;
124
+ }
125
+ };
126
+ const readText = (file) => {
127
+ try {
128
+ return fs.readFileSync(path.join(dir, file), 'utf8');
129
+ } catch {
130
+ return null;
131
+ }
132
+ };
133
+
134
+ // Node.js / JavaScript detection
135
+ if (exists('package.json')) {
136
+ config.language = 'JavaScript';
137
+ config.runtime = 'Node.js';
138
+
139
+ const pkg = readJSON('package.json');
140
+ if (pkg) {
141
+ // Runtime version from engines
142
+ if (pkg.engines && pkg.engines.node) {
143
+ config.runtime = `Node.js ${pkg.engines.node}`;
144
+ }
145
+
146
+ const deps = pkg.dependencies || {};
147
+ const devDeps = pkg.devDependencies || {};
148
+ const allDeps = { ...deps, ...devDeps };
149
+
150
+ // Frameworks
151
+ const frameworkNames = [
152
+ 'express', 'fastify', 'koa', 'react', 'next',
153
+ 'vue', 'angular', 'hapi', 'nest', '@hapi/hapi', '@nestjs/core'
154
+ ];
155
+ const detected = [];
156
+ for (const fw of frameworkNames) {
157
+ if (fw in deps || fw in devDeps) {
158
+ // Normalize package names to friendly names
159
+ if (fw === '@hapi/hapi') detected.push('hapi');
160
+ else if (fw === '@nestjs/core') detected.push('nest');
161
+ else detected.push(fw);
162
+ }
163
+ }
164
+ if (detected.length > 0) config.frameworks = detected;
165
+
166
+ // Test runner
167
+ const testRunners = ['jest', 'mocha', 'vitest', 'ava'];
168
+ for (const tr of testRunners) {
169
+ if (tr in devDeps || tr in deps) {
170
+ config.testRunner = tr;
171
+ break;
172
+ }
173
+ }
174
+
175
+ // Test command from scripts
176
+ if (pkg.scripts && pkg.scripts.test) {
177
+ config.testCommand = pkg.scripts.test;
178
+ // Also try to detect test runner from test script if not found in deps
179
+ if (!config.testRunner) {
180
+ for (const tr of testRunners) {
181
+ if (pkg.scripts.test.includes(tr)) {
182
+ config.testRunner = tr;
183
+ break;
184
+ }
185
+ }
186
+ }
187
+ }
188
+ if (!config.testCommand) {
189
+ config.testCommand = 'npm test';
190
+ }
191
+
192
+ // Linter
193
+ const linters = ['eslint', 'biome', 'oxlint', '@biomejs/biome'];
194
+ for (const l of linters) {
195
+ if (l in devDeps || l in deps) {
196
+ if (l === '@biomejs/biome') config.linter = 'biome';
197
+ else config.linter = l;
198
+ break;
199
+ }
200
+ }
201
+
202
+ // Tools
203
+ const toolNames = ['nodemon', 'supertest', 'prettier', 'typescript'];
204
+ const detectedTools = [];
205
+ for (const t of toolNames) {
206
+ if (t in devDeps || t in deps) {
207
+ detectedTools.push(t);
208
+ }
209
+ }
210
+ if (detectedTools.length > 0) config.tools = detectedTools;
211
+
212
+ // Package manager from lockfiles
213
+ if (exists('yarn.lock')) {
214
+ config.packageManager = 'yarn';
215
+ } else if (exists('pnpm-lock.yaml')) {
216
+ config.packageManager = 'pnpm';
217
+ } else {
218
+ config.packageManager = 'npm';
219
+ }
220
+ }
221
+ }
222
+
223
+ // TypeScript overrides JavaScript
224
+ if (exists('tsconfig.json')) {
225
+ config.language = 'TypeScript';
226
+ if (!config.runtime) config.runtime = 'Node.js';
227
+ }
228
+
229
+ // Python detection
230
+ if (exists('pyproject.toml') || exists('requirements.txt')) {
231
+ config.language = 'Python';
232
+ config.runtime = 'Python 3.x';
233
+
234
+ const pyproject = readText('pyproject.toml');
235
+ if (pyproject) {
236
+ // Test runner
237
+ if (pyproject.includes('pytest')) config.testRunner = 'pytest';
238
+ else if (pyproject.includes('unittest')) config.testRunner = 'unittest';
239
+
240
+ // Linter/tools
241
+ if (pyproject.includes('ruff')) config.linter = 'ruff';
242
+ else if (pyproject.includes('flake8')) config.linter = 'flake8';
243
+
244
+ const pyTools = [];
245
+ if (pyproject.includes('black')) pyTools.push('black');
246
+ if (pyproject.includes('mypy')) pyTools.push('mypy');
247
+ if (pyTools.length > 0) config.tools = pyTools;
248
+
249
+ // Frameworks
250
+ const pyFrameworks = [];
251
+ if (pyproject.includes('django')) pyFrameworks.push('django');
252
+ if (pyproject.includes('flask')) pyFrameworks.push('flask');
253
+ if (pyproject.includes('fastapi')) pyFrameworks.push('fastapi');
254
+ if (pyFrameworks.length > 0) config.frameworks = pyFrameworks;
255
+ }
256
+
257
+ // Package manager
258
+ if (exists('poetry.lock')) config.packageManager = 'poetry';
259
+ else if (exists('Pipfile.lock') || exists('Pipfile')) config.packageManager = 'pipenv';
260
+ else config.packageManager = 'pip';
261
+
262
+ if (!config.testCommand && config.testRunner) {
263
+ config.testCommand = config.testRunner === 'pytest' ? 'pytest' : 'python -m unittest';
264
+ }
265
+ }
266
+
267
+ // Go detection
268
+ if (exists('go.mod')) {
269
+ config.language = 'Go';
270
+ config.runtime = 'Go';
271
+ config.testRunner = 'go test';
272
+ config.testCommand = 'go test ./...';
273
+ }
274
+
275
+ // Rust detection
276
+ if (exists('Cargo.toml')) {
277
+ config.language = 'Rust';
278
+ config.runtime = 'Rust';
279
+ config.packageManager = 'cargo';
280
+ config.testRunner = 'cargo test';
281
+ config.testCommand = 'cargo test';
282
+ }
283
+
284
+ // Ruby detection
285
+ if (exists('Gemfile')) {
286
+ config.language = 'Ruby';
287
+ config.runtime = 'Ruby';
288
+ config.packageManager = 'bundler';
289
+ }
290
+
291
+ return config;
292
+ }
293
+
294
+ /**
295
+ * Displays the current stack configuration.
296
+ */
297
+ function displayStackConfig() {
298
+ const config = readStackConfig();
299
+ console.log('\nStack Configuration\n');
300
+ console.log(` language: ${config.language || '(not set)'}`);
301
+ console.log(` runtime: ${config.runtime || '(not set)'}`);
302
+ console.log(` packageManager: ${config.packageManager || '(not set)'}`);
303
+ console.log(` frameworks: ${config.frameworks.length > 0 ? config.frameworks.join(', ') : '(not set)'}`);
304
+ console.log(` testRunner: ${config.testRunner || '(not set)'}`);
305
+ console.log(` testCommand: ${config.testCommand || '(not set)'}`);
306
+ console.log(` linter: ${config.linter || '(not set)'}`);
307
+ console.log(` tools: ${config.tools.length > 0 ? config.tools.join(', ') : '(not set)'}`);
308
+ console.log('\nTo change: orchestr8 stack-config set <key> <value>');
309
+ }
310
+
311
+ module.exports = {
312
+ CONFIG_FILE,
313
+ getDefaultStackConfig,
314
+ readStackConfig,
315
+ writeStackConfig,
316
+ resetStackConfig,
317
+ setStackConfigValue,
318
+ detectStackConfig,
319
+ displayStackConfig
320
+ };