specrails-core 3.5.3 → 3.7.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.
package/README.md CHANGED
@@ -13,7 +13,7 @@ One command gives your repo a full team of specialized AI agents: architect, dev
13
13
 
14
14
  ```bash
15
15
  claude plugin install sr # Claude Code plugin (recommended)
16
- /specrails:setup # configure for your project
16
+ /specrails:enrich # configure for your project
17
17
  ```
18
18
 
19
19
  > **Requirements:** [Claude Code](https://docs.anthropic.com/en/docs/claude-code) or [Codex CLI](https://github.com/openai/codex) (choose one), git
@@ -39,7 +39,7 @@ Every artifact (agents, rules, personas) is generated **specifically for your pr
39
39
 
40
40
  ```bash
41
41
  claude plugin install sr # install the sr plugin
42
- /specrails:setup # run the 5-phase wizard (~5 min)
42
+ /specrails:enrich # run the 5-phase wizard (~5 min)
43
43
  ```
44
44
 
45
45
  Updates automatically: `claude plugin update sr`
@@ -47,8 +47,8 @@ Updates automatically: `claude plugin update sr`
47
47
  ### Scaffold method (alternative)
48
48
 
49
49
  ```bash
50
- npx specrails-core@latest init --root-dir . # copy project files
51
- /specrails:setup # run the wizard
50
+ npx specrails-core@latest init --root-dir . # TUI agent selection + install files
51
+ /specrails:enrich --from-config # run AI analysis using your config
52
52
  ```
53
53
 
54
54
  ### Start building
@@ -71,10 +71,10 @@ That's it. The pipeline takes over.
71
71
  | **What it contains** | Agents, skills, hooks, references | Nothing — project data only |
72
72
  | **Install** | `claude plugin install sr` | `npx specrails-core@latest init` |
73
73
  | **Updates** | `claude plugin update sr` | Re-run npx |
74
- | **Project data** | Generated by `/specrails:setup` into `.specrails/` | Same |
74
+ | **Project data** | Generated by `/specrails:enrich` into `.specrails/` | Same |
75
75
  | **Use case** | Recommended for most projects | When you need full offline control |
76
76
 
77
- The plugin contains the logic (agent prompts, skills, commands). Running `/specrails:setup` generates the project-specific data files (~8–10 files in `.specrails/`) — config, personas, memory, pipeline state. These are yours to edit and commit.
77
+ The plugin contains the logic (agent prompts, skills, commands). Running `/specrails:enrich` generates the project-specific data files (~8–10 files in `.specrails/`) — config, personas, memory, pipeline state. These are yours to edit and commit.
78
78
 
79
79
  ---
80
80
 
@@ -89,7 +89,7 @@ The plugin contains the logic (agent prompts, skills, commands). Running `/specr
89
89
  | **Skills** | Plugin | OpenSpec skills (`/opsx:*`) included |
90
90
  | **Hooks & References** | Plugin | Agent hooks, reference docs |
91
91
 
92
- ### Project data (generated by `/specrails:setup` — lives in your repo)
92
+ ### Project data (generated by `/specrails:enrich` — lives in your repo)
93
93
 
94
94
  | Category | Files | Purpose |
95
95
  |----------|-------|---------|
@@ -198,7 +198,7 @@ specrails-core ships with a built-in ticket system — no GitHub account or exte
198
198
 
199
199
  Tickets live in `.specrails/local-tickets.json` alongside your code. They're plain JSON and git-friendly.
200
200
 
201
- **Local tickets are the default.** The `/specrails:setup` wizard defaults to local tickets and skips GitHub/JIRA credential setup unless you opt in.
201
+ **Local tickets are the default.** The `/specrails:enrich` wizard defaults to local tickets and skips GitHub/JIRA credential setup unless you opt in.
202
202
 
203
203
  ```bash
204
204
  /specrails:implement #1, #4 # implement by ticket ID
@@ -246,7 +246,7 @@ The plugin method has no Node.js requirement. The scaffold method checks for pre
246
246
 
247
247
  ## Supported stacks
248
248
 
249
- Stack-agnostic. The `/specrails:setup` wizard detects and adapts to whatever you're running:
249
+ Stack-agnostic. The `/specrails:enrich` wizard detects and adapts to whatever you're running:
250
250
 
251
251
  **Backend:** Python/FastAPI, Node/Express, Go/Gin, Rust/Actix, Java/Spring, Ruby/Rails, .NET
252
252
  **Frontend:** React, Vue, Angular, Svelte, Next.js, Nuxt
@@ -272,8 +272,8 @@ Stack-agnostic. The `/specrails:setup` wizard detects and adapts to whatever you
272
272
  **Can I customize the agents after installation?**
273
273
  With the plugin method, project data files in `.specrails/` are yours to edit — personas, rules, config. With the scaffold method, `.claude/` agent files are also editable. To update plugin logic, run `claude plugin update sr`.
274
274
 
275
- **Can I re-run setup?**
276
- Run `/specrails:setup` again at any time to regenerate or update project data files.
275
+ **Can I re-run the wizard?**
276
+ Run `/specrails:enrich` again at any time to regenerate or update project data files.
277
277
 
278
278
  **Plugin vs scaffold — which should I use?**
279
279
  Plugin is recommended: it has no Node.js requirement, updates automatically, and separates logic from project data. Use scaffold if you need full offline control or want to version the agent prompts themselves.
@@ -9,6 +9,7 @@ const COMMANDS = {
9
9
  update: "update.sh",
10
10
  doctor: "bin/doctor.sh",
11
11
  "perf-check": "bin/perf-check.sh",
12
+ enrich: null,
12
13
  };
13
14
 
14
15
  const args = process.argv.slice(2);
@@ -18,31 +19,38 @@ if (!subcommand) {
18
19
  console.log(`specrails-core — Agent Workflow System for Claude Code
19
20
 
20
21
  Usage:
21
- specrails-core init [--root-dir <path>] Install into a repository
22
- specrails-core update [--only <component>] Update an existing installation
23
- specrails-core doctor Run health checks
24
- specrails-core perf-check [--files <list>] Performance regression check (CI)
22
+ specrails-core init [--root-dir <path>] [--yes|-y] Install into a repository
23
+ specrails-core update [--only <component>] Update an existing installation
24
+ specrails-core doctor Run health checks
25
+ specrails-core perf-check [--files <list>] Performance regression check (CI)
26
+ specrails-core enrich [--from-config <path>] Run /specrails:enrich via Claude CLI
27
+
28
+ Flags for init:
29
+ --root-dir <path> Target repository path (default: current directory)
30
+ --yes | -y Non-interactive; use defaults, skip TUI
31
+ --provider <value> Force provider: claude or codex
32
+ --no-direct Skip TUI; use the legacy interactive bash installer
33
+ --from-config Skip TUI; use existing .specrails/install-config.yaml
25
34
 
26
35
  More info: https://github.com/fjpulidop/specrails-core`);
27
36
  process.exit(0);
28
37
  }
29
38
 
30
- const script = COMMANDS[subcommand];
31
-
32
- if (!script) {
39
+ if (!(subcommand in COMMANDS)) {
33
40
  console.error(`Unknown command: ${subcommand}\n`);
34
- console.error("Available commands: init, update, doctor, perf-check");
41
+ console.error("Available commands: init, update, doctor, perf-check, enrich");
35
42
  process.exit(1);
36
43
  }
37
44
 
38
- // Allowlisted flags per subcommand (defense-in-depth — spawnSync already
39
- // prevents shell injection, but an explicit allowlist rejects unknown flags
40
- // before the shell script is ever invoked).
45
+ const script = COMMANDS[subcommand];
46
+
47
+ // Allowlisted flags per subcommand
41
48
  const ALLOWED_FLAGS = {
42
- init: ["--root-dir", "--yes", "-y"],
49
+ init: ["--root-dir", "--yes", "-y", "--provider", "--no-direct", "--from-config", "--quick", "--hub-json", "--agent-teams"],
43
50
  update: ["--only"],
44
51
  doctor: [],
45
52
  "perf-check": ["--files", "--context"],
53
+ enrich: ["--from-config", "--quick"],
46
54
  };
47
55
 
48
56
  const subargs = args.slice(1);
@@ -55,7 +63,92 @@ for (const arg of subargs) {
55
63
  }
56
64
  }
57
65
 
58
- const result = spawnSync("bash", [resolve(ROOT, script), ...subargs], {
66
+ // ─── enrich subcommand ───────────────────────────────────────────────────────
67
+ // Launches `claude --command "/specrails:enrich [flags]"` so the AI-powered
68
+ // enrichment runs inside Claude Code with full model access.
69
+
70
+ if (subcommand === "enrich") {
71
+ const enrichFlags = subargs.join(" ");
72
+ const claudeCmd = `/specrails:enrich${enrichFlags ? " " + enrichFlags : ""}`;
73
+ const claudeResult = spawnSync("claude", ["--command", claudeCmd, "--dangerously-skip-permissions"], {
74
+ stdio: "inherit",
75
+ cwd: process.cwd(),
76
+ });
77
+
78
+ if (claudeResult.error) {
79
+ console.error(
80
+ "\nFailed to launch Claude CLI for enrich:",
81
+ claudeResult.error.message,
82
+ "\nEnsure Claude Code is installed: npm install -g @anthropic-ai/claude-code\n"
83
+ );
84
+ process.exit(1);
85
+ }
86
+
87
+ process.exit(claudeResult.status ?? (claudeResult.error ? 1 : 0));
88
+ }
89
+
90
+ // ─── Direct mode (TUI) for `init` ─────────────────────────────────────────────
91
+ //
92
+ // Default behaviour: run the Node.js TUI to collect agent/model configuration,
93
+ // write .specrails/install-config.yaml, then hand off to install.sh.
94
+ //
95
+ // Opt-out with: --no-direct (legacy interactive bash installer)
96
+ // --from-config (config already on disk; skip TUI)
97
+ // --yes / -y (write default config, no prompts)
98
+
99
+ const isInit = subcommand === "init";
100
+ const hasNoTui = subargs.includes("--no-direct") || subargs.includes("--from-config");
101
+ const autoYes = subargs.includes("--yes") || subargs.includes("-y");
102
+ const useTui = isInit && !hasNoTui;
103
+
104
+ if (useTui) {
105
+ // Resolve the target directory for the TUI
106
+ const rootDirIdx = subargs.indexOf("--root-dir");
107
+ const rootDir = rootDirIdx >= 0 ? resolve(subargs[rootDirIdx + 1]) : process.cwd();
108
+
109
+ // Build TUI args: pass rootDir + --yes if set
110
+ const tuiArgs = [resolve(ROOT, "bin/tui-installer.mjs"), rootDir];
111
+ if (autoYes) tuiArgs.push("--yes");
112
+
113
+ const tuiResult = spawnSync("node", tuiArgs, {
114
+ stdio: "inherit",
115
+ cwd: process.cwd(),
116
+ });
117
+
118
+ if (tuiResult.error) {
119
+ // @inquirer/prompts not installed (e.g. during development without npm install)
120
+ console.error(
121
+ "\nFailed to launch TUI installer:",
122
+ tuiResult.error.message,
123
+ "\nRun: npm install or use --no-direct for the legacy installer.\n"
124
+ );
125
+ process.exit(1);
126
+ }
127
+
128
+ if (tuiResult.status !== 0) {
129
+ process.exit(tuiResult.status ?? 1);
130
+ }
131
+
132
+ // TUI succeeded — run install.sh with --from-config so it reads provider/
133
+ // agent_teams from install-config.yaml rather than prompting interactively.
134
+ const installArgs = subargs
135
+ .filter(a => a !== "--no-direct") // strip internal flags
136
+ .concat(["--yes", "--from-config"]);
137
+
138
+ const result = spawnSync("bash", [resolve(ROOT, script), ...installArgs], {
139
+ stdio: "inherit",
140
+ cwd: process.cwd(),
141
+ });
142
+
143
+ process.exit(result.status ?? (result.error ? 1 : 0));
144
+ }
145
+
146
+ // ─── Legacy / non-TUI path ────────────────────────────────────────────────────
147
+
148
+ // Strip only --no-direct (internal flag) before passing args to the shell script
149
+ const cleanArgs = subargs.filter(a => a !== "--no-direct");
150
+
151
+ const result = spawnSync("bash", [resolve(ROOT, script), ...cleanArgs], {
59
152
  stdio: "inherit",
60
153
  cwd: process.cwd(),
61
154
  });
@@ -0,0 +1,309 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * specrails TUI installer — Phase 1
4
+ * Interactive agent selection + model configuration using @inquirer/prompts.
5
+ * Writes .specrails/install-config.yaml to the target repo directory.
6
+ *
7
+ * Usage: node bin/tui-installer.mjs [rootDir] [--yes]
8
+ * rootDir - target repo directory (default: cwd)
9
+ * --yes - skip TUI, write defaults immediately
10
+ */
11
+
12
+ import { checkbox, select, input, Separator } from '@inquirer/prompts';
13
+ import { writeFileSync, mkdirSync, existsSync } from 'node:fs';
14
+ import { execSync } from 'node:child_process';
15
+ import { resolve } from 'node:path';
16
+
17
+ // ─── Agent registry ───────────────────────────────────────────────────────────
18
+
19
+ const AGENTS = [
20
+ // Architecture
21
+ { id: 'sr-architect', category: 'Architecture', description: 'Architecture design, change specs, implementation planning' },
22
+ // Development
23
+ { id: 'sr-developer', category: 'Development', description: 'Full-stack implementation across all layers' },
24
+ { id: 'sr-frontend-developer', category: 'Development', description: 'Frontend implementation (React, Vue, Angular, etc.)' },
25
+ { id: 'sr-backend-developer', category: 'Development', description: 'Backend specialization (APIs, databases, services)' },
26
+ // Review
27
+ { id: 'sr-reviewer', category: 'Review', description: 'General code review — the final quality gate' },
28
+ { id: 'sr-frontend-reviewer', category: 'Review', description: 'Frontend review (UI, accessibility, performance)' },
29
+ { id: 'sr-backend-reviewer', category: 'Review', description: 'Backend review (APIs, security, scalability)' },
30
+ { id: 'sr-security-reviewer', category: 'Review', description: 'Security analysis — OWASP, vulnerabilities, hardening' },
31
+ { id: 'sr-performance-reviewer', category: 'Review', description: 'Performance analysis — profiling, bottlenecks, optimization' },
32
+ // Product
33
+ { id: 'sr-product-manager', category: 'Product', description: 'Product discovery, VPC personas, backlog management' },
34
+ { id: 'sr-product-analyst', category: 'Product', description: 'Backlog analysis, spec gap analysis, reporting' },
35
+ // Utilities
36
+ { id: 'sr-test-writer', category: 'Utilities', description: 'Comprehensive test generation (unit, integration, E2E)' },
37
+ { id: 'sr-doc-sync', category: 'Utilities', description: 'Documentation sync — keeps docs aligned with code' },
38
+ { id: 'sr-merge-resolver', category: 'Utilities', description: 'Merge conflict resolution with context awareness' },
39
+ ];
40
+
41
+ const ALL_AGENT_IDS = AGENTS.map(a => a.id);
42
+
43
+ const DEFAULT_SELECTED = new Set([
44
+ 'sr-architect',
45
+ 'sr-developer',
46
+ 'sr-reviewer',
47
+ 'sr-test-writer',
48
+ 'sr-product-manager',
49
+ ]);
50
+
51
+ // ─── Model presets ────────────────────────────────────────────────────────────
52
+
53
+ const MODEL_PRESETS = {
54
+ balanced: {
55
+ label: 'Balanced (recommended) — Sonnet for all agents',
56
+ defaults: 'sonnet',
57
+ overrides: {},
58
+ },
59
+ budget: {
60
+ label: 'Budget — Haiku for all agents (3× cheaper, faster)',
61
+ defaults: 'haiku',
62
+ overrides: {},
63
+ },
64
+ max: {
65
+ label: 'Max quality — Opus for architect + PM, Sonnet for rest',
66
+ defaults: 'sonnet',
67
+ overrides: { 'sr-architect': 'opus', 'sr-product-manager': 'opus' },
68
+ },
69
+ };
70
+
71
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
72
+
73
+ function detectProvider() {
74
+ let hasClaude = false;
75
+ let hasCodex = false;
76
+ try { execSync('claude --version', { stdio: 'ignore' }); hasClaude = true; } catch { /* not installed */ }
77
+ try { execSync('codex --version', { stdio: 'ignore' }); hasCodex = true; } catch { /* not installed */ }
78
+ return { hasClaude, hasCodex };
79
+ }
80
+
81
+ function detectGitRoot(dir) {
82
+ try {
83
+ return execSync('git rev-parse --show-toplevel', { cwd: dir, stdio: ['ignore', 'pipe', 'ignore'] })
84
+ .toString()
85
+ .trim();
86
+ } catch {
87
+ return dir;
88
+ }
89
+ }
90
+
91
+ function buildCheckboxChoices() {
92
+ const choices = [];
93
+ let currentCategory = null;
94
+ for (const agent of AGENTS) {
95
+ if (agent.category !== currentCategory) {
96
+ if (currentCategory !== null) choices.push(new Separator(''));
97
+ choices.push(new Separator(`── ${agent.category} ${'─'.repeat(Math.max(0, 46 - agent.category.length))}`));
98
+ currentCategory = agent.category;
99
+ }
100
+ choices.push({
101
+ value: agent.id,
102
+ name: `${agent.id.padEnd(28)} ${agent.description}`,
103
+ checked: DEFAULT_SELECTED.has(agent.id),
104
+ });
105
+ }
106
+ return choices;
107
+ }
108
+
109
+ function writeInstallConfig(specrailsDir, cfg) {
110
+ if (!existsSync(specrailsDir)) mkdirSync(specrailsDir, { recursive: true });
111
+
112
+ const overridesEntries = Object.entries(cfg.modelOverrides);
113
+ const overridesYaml = overridesEntries.length > 0
114
+ ? '\n' + overridesEntries.map(([k, v]) => ` ${k}: ${v}`).join('\n')
115
+ : ' {}';
116
+
117
+ const yaml = [
118
+ '# specrails install config — generated by TUI installer',
119
+ '# Re-run: npx specrails-core@latest init to regenerate',
120
+ `version: 1`,
121
+ `provider: ${cfg.provider}`,
122
+ `tier: ${cfg.tier}`,
123
+ `agents:`,
124
+ ` selected: [${cfg.selectedAgents.join(', ')}]`,
125
+ ` excluded: [${cfg.excludedAgents.join(', ')}]`,
126
+ `models:`,
127
+ ` preset: ${cfg.modelPreset}`,
128
+ ` defaults: { model: ${cfg.modelDefaults} }`,
129
+ ` overrides:${overridesYaml}`,
130
+ `quick_context:`,
131
+ ` product_description: "${cfg.productDescription.replace(/"/g, '\\"')}"`,
132
+ ` target_users: "${cfg.targetUsers.replace(/"/g, '\\"')}"`,
133
+ `agent_teams: ${cfg.agentTeams}`,
134
+ '',
135
+ ].join('\n');
136
+
137
+ writeFileSync(resolve(specrailsDir, 'install-config.yaml'), yaml, 'utf8');
138
+ }
139
+
140
+ function writeDefaultConfig(specrailsDir, provider) {
141
+ const defaultSelected = [...DEFAULT_SELECTED];
142
+ const defaultExcluded = ALL_AGENT_IDS.filter(id => !DEFAULT_SELECTED.has(id));
143
+ writeInstallConfig(specrailsDir, {
144
+ provider,
145
+ tier: 'full',
146
+ selectedAgents: defaultSelected,
147
+ excludedAgents: defaultExcluded,
148
+ modelPreset: 'balanced',
149
+ modelDefaults: 'sonnet',
150
+ modelOverrides: {},
151
+ productDescription: '',
152
+ targetUsers: '',
153
+ agentTeams: false,
154
+ });
155
+ }
156
+
157
+ // ─── Main ─────────────────────────────────────────────────────────────────────
158
+
159
+ async function run() {
160
+ const rawArgs = process.argv.slice(2);
161
+ const autoYes = rawArgs.includes('--yes') || rawArgs.includes('-y');
162
+ const rootArg = rawArgs.find(a => !a.startsWith('-'));
163
+ const inputDir = rootArg ? resolve(rootArg) : process.cwd();
164
+ const rootDir = detectGitRoot(inputDir);
165
+
166
+ const specrailsDir = resolve(rootDir, '.specrails');
167
+
168
+ console.log('\n╔══════════════════════════════════════════════╗');
169
+ console.log('║ specrails — Direct Install (v1) ║');
170
+ console.log('║ Configure your AI agent workflow system ║');
171
+ console.log('╚══════════════════════════════════════════════╝\n');
172
+
173
+ // Auto-yes: write defaults and exit
174
+ if (autoYes) {
175
+ const { hasClaude, hasCodex } = detectProvider();
176
+ const provider = hasCodex && !hasClaude ? 'codex' : 'claude';
177
+ writeDefaultConfig(specrailsDir, provider);
178
+ console.log(` ✓ Default config written to .specrails/install-config.yaml`);
179
+ console.log(` ✓ Provider: ${provider}, Tier: full, Agents: ${DEFAULT_SELECTED.size}/${ALL_AGENT_IDS.length}, Preset: balanced\n`);
180
+ return;
181
+ }
182
+
183
+ // ── Step 1: Provider ────────────────────────────────────────────────────────
184
+
185
+ const { hasClaude, hasCodex } = detectProvider();
186
+ let provider;
187
+
188
+ if (hasClaude && hasCodex) {
189
+ provider = await select({
190
+ message: 'Which AI provider will you use?',
191
+ choices: [
192
+ { value: 'claude', name: 'Claude Code (recommended)' },
193
+ { value: 'codex', name: 'Codex' },
194
+ ],
195
+ });
196
+ } else if (hasCodex) {
197
+ provider = 'codex';
198
+ console.log(' → Provider: codex (auto-detected)\n');
199
+ } else {
200
+ provider = 'claude';
201
+ if (hasClaude) {
202
+ console.log(' → Provider: claude (auto-detected)\n');
203
+ } else {
204
+ console.log(' → Provider: claude (default — claude CLI not found, install it at claude.ai/download)\n');
205
+ }
206
+ }
207
+
208
+ // ── Step 2: Installation tier ───────────────────────────────────────────────
209
+
210
+ const tier = await select({
211
+ message: 'Installation tier:',
212
+ choices: [
213
+ {
214
+ value: 'full',
215
+ name: 'Full — AI-powered setup (recommended)',
216
+ description: 'After install, run /specrails:enrich to AI-customize all agents for your codebase',
217
+ },
218
+ {
219
+ value: 'quick',
220
+ name: 'Quick — Template-only install',
221
+ description: 'Agents installed with sensible defaults. No AI step required.',
222
+ },
223
+ ],
224
+ });
225
+
226
+ // ── Step 3: Agent selection ─────────────────────────────────────────────────
227
+
228
+ console.log('\n Select agents to install. Space = toggle, a = all/none, Enter = confirm.\n');
229
+
230
+ const selectedAgents = await checkbox({
231
+ message: 'Agents to install:',
232
+ choices: buildCheckboxChoices(),
233
+ validate: (selected) => selected.length > 0 || 'Select at least one agent.',
234
+ });
235
+
236
+ const excludedAgents = ALL_AGENT_IDS.filter(id => !selectedAgents.includes(id));
237
+
238
+ // ── Step 4: Model preset ────────────────────────────────────────────────────
239
+
240
+ const modelPreset = await select({
241
+ message: 'Model configuration:',
242
+ choices: Object.entries(MODEL_PRESETS).map(([key, val]) => ({
243
+ value: key,
244
+ name: `${key.padEnd(10)} ${val.label}`,
245
+ })),
246
+ });
247
+
248
+ const { defaults: modelDefaults, overrides: modelOverrides } = MODEL_PRESETS[modelPreset];
249
+
250
+ // ── Step 5: Agent Teams (Claude only) ──────────────────────────────────────
251
+
252
+ let agentTeams = false;
253
+ if (provider === 'claude') {
254
+ agentTeams = await select({
255
+ message: 'Install Agent Teams commands? (experimental)',
256
+ choices: [
257
+ { value: false, name: 'No — standard single-agent workflow (recommended)' },
258
+ { value: true, name: 'Yes — /specrails:team-review and :team-debug (requires CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1)' },
259
+ ],
260
+ });
261
+ }
262
+
263
+ // ── Step 6: Quick context ───────────────────────────────────────────────────
264
+
265
+ console.log('\n Quick context helps specrails personalize agents for your project.\n');
266
+
267
+ const productDescription = await input({
268
+ message: 'Product description (2–3 sentences):',
269
+ validate: (v) => v.trim().length > 0 || 'Please enter a brief product description.',
270
+ });
271
+
272
+ const targetUsers = await input({
273
+ message: 'Target users (who will use this product?):',
274
+ validate: (v) => v.trim().length > 0 || 'Please describe your target users.',
275
+ });
276
+
277
+ // ── Write config ────────────────────────────────────────────────────────────
278
+
279
+ writeInstallConfig(specrailsDir, {
280
+ provider,
281
+ tier,
282
+ selectedAgents,
283
+ excludedAgents,
284
+ modelPreset,
285
+ modelDefaults,
286
+ modelOverrides,
287
+ productDescription: productDescription.trim(),
288
+ targetUsers: targetUsers.trim(),
289
+ agentTeams,
290
+ });
291
+
292
+ console.log(`\n ✓ Config written to .specrails/install-config.yaml`);
293
+ console.log(` ✓ Provider: ${provider} | Tier: ${tier} | Agents: ${selectedAgents.length}/${ALL_AGENT_IDS.length} | Preset: ${modelPreset}`);
294
+ if (tier === 'full') {
295
+ console.log(`\n Next: run /specrails:enrich --from-config inside ${provider} to AI-customize your agents.\n`);
296
+ } else {
297
+ console.log(`\n Agents will be installed with template defaults.\n`);
298
+ }
299
+ }
300
+
301
+ run().catch(err => {
302
+ // Graceful Ctrl+C handling
303
+ if (err.name === 'ExitPromptError' || (err.message && err.message.includes('User force closed'))) {
304
+ console.error('\n Installation cancelled by user.\n');
305
+ process.exit(130);
306
+ }
307
+ console.error('\n TUI error:', err.message);
308
+ process.exit(1);
309
+ });