@bretwardjames/ghp-cli 0.16.3 → 0.17.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/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +12 -18
- package/dist/commands/agents.js.map +1 -1
- package/dist/commands/dashboard-pipeline.d.ts.map +1 -1
- package/dist/commands/dashboard-pipeline.js +167 -17
- package/dist/commands/dashboard-pipeline.js.map +1 -1
- package/dist/commands/done.d.ts.map +1 -1
- package/dist/commands/done.js +10 -1
- package/dist/commands/done.js.map +1 -1
- package/dist/commands/pipeline-commands.d.ts +75 -3
- package/dist/commands/pipeline-commands.d.ts.map +1 -1
- package/dist/commands/pipeline-commands.js +342 -5
- package/dist/commands/pipeline-commands.js.map +1 -1
- package/dist/commands/pipeline-setup.d.ts +23 -0
- package/dist/commands/pipeline-setup.d.ts.map +1 -0
- package/dist/commands/pipeline-setup.js +740 -0
- package/dist/commands/pipeline-setup.js.map +1 -0
- package/dist/commands/start.js +1 -1
- package/dist/commands/start.js.map +1 -1
- package/dist/commands/stop.d.ts.map +1 -1
- package/dist/commands/stop.js +10 -1
- package/dist/commands/stop.js.map +1 -1
- package/dist/commands/worktree.d.ts.map +1 -1
- package/dist/commands/worktree.js +10 -0
- package/dist/commands/worktree.js.map +1 -1
- package/dist/config.d.ts +33 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +20 -0
- package/dist/config.js.map +1 -1
- package/dist/index.js +44 -1
- package/dist/index.js.map +1 -1
- package/dist/pipeline-registry.d.ts +1 -1
- package/dist/pipeline-registry.d.ts.map +1 -1
- package/dist/pipeline-registry.js +3 -24
- package/dist/pipeline-registry.js.map +1 -1
- package/dist/terminal-utils.d.ts +19 -5
- package/dist/terminal-utils.d.ts.map +1 -1
- package/dist/terminal-utils.js +61 -30
- package/dist/terminal-utils.js.map +1 -1
- package/dist/worktree-utils.d.ts.map +1 -1
- package/dist/worktree-utils.js +22 -7
- package/dist/worktree-utils.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,740 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ghp pipeline setup — Agent-friendly setup wizard for pipeline configuration.
|
|
3
|
+
*
|
|
4
|
+
* Three modes:
|
|
5
|
+
* --questions Output JSON question schema (no side effects)
|
|
6
|
+
* --apply Read answers JSON from stdin, write config + hook scripts
|
|
7
|
+
* (no flags) Interactive CLI wizard
|
|
8
|
+
*
|
|
9
|
+
* Flavors (saved presets):
|
|
10
|
+
* --save <name> Save answers from stdin as a named flavor
|
|
11
|
+
* --flavor <name> Load and apply a saved flavor
|
|
12
|
+
* --flavors List saved flavors
|
|
13
|
+
* --delete-flavor <n> Delete a saved flavor
|
|
14
|
+
*/
|
|
15
|
+
import chalk from 'chalk';
|
|
16
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, chmodSync } from 'fs';
|
|
17
|
+
import { join } from 'path';
|
|
18
|
+
import { setConfigByPath, getUserConfigPath, } from '../config.js';
|
|
19
|
+
import { isInteractive, promptSelect, promptWithDefault, confirmWithDefault, } from '../prompts.js';
|
|
20
|
+
import { getMainWorktreeRoot } from '../git-utils.js';
|
|
21
|
+
import { exit } from '../exit.js';
|
|
22
|
+
const QUESTIONS = [
|
|
23
|
+
// ── General config ──────────────────────────────────────────────────────
|
|
24
|
+
{
|
|
25
|
+
id: 'config_scope',
|
|
26
|
+
question: 'Where should this configuration be saved?',
|
|
27
|
+
type: 'select',
|
|
28
|
+
options: [
|
|
29
|
+
{ value: 'workspace', label: 'This project only (.ghp/config.json)', description: 'Checked into the repo — the whole team gets this config' },
|
|
30
|
+
{ value: 'user', label: 'Global (~/.config/ghp-cli/config.json)', description: 'Your personal preference across all projects' },
|
|
31
|
+
],
|
|
32
|
+
default: 'user',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: 'save_flavor',
|
|
36
|
+
question: 'Also save these answers as a reusable flavor?',
|
|
37
|
+
type: 'confirm',
|
|
38
|
+
default: false,
|
|
39
|
+
hint: 'Flavors let you reapply this exact setup later with: ghp pipeline setup --flavor <name>',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: 'flavor_name',
|
|
43
|
+
question: 'Flavor name:',
|
|
44
|
+
type: 'text',
|
|
45
|
+
default: '',
|
|
46
|
+
dependsOn: { save_flavor: true },
|
|
47
|
+
hint: 'Reapply later with: ghp pipeline setup --flavor <name>',
|
|
48
|
+
},
|
|
49
|
+
// ── Dashboard layout ────────────────────────────────────────────────────
|
|
50
|
+
{
|
|
51
|
+
id: 'dashboard_mode',
|
|
52
|
+
question: 'Where should the pipeline dashboard open?',
|
|
53
|
+
type: 'select',
|
|
54
|
+
options: [
|
|
55
|
+
{ value: 'pane', label: 'Split pane', description: 'Split the current tmux window' },
|
|
56
|
+
{ value: 'window', label: 'New window', description: 'Separate tmux window' },
|
|
57
|
+
],
|
|
58
|
+
default: 'window',
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: 'dashboard_direction',
|
|
62
|
+
question: 'When opening as a pane, which direction should it split?',
|
|
63
|
+
type: 'select',
|
|
64
|
+
dependsOn: { dashboard_mode: 'pane' },
|
|
65
|
+
options: [
|
|
66
|
+
{ value: 'horizontal', label: 'Side by side' },
|
|
67
|
+
{ value: 'vertical', label: 'Stacked (top/bottom)' },
|
|
68
|
+
],
|
|
69
|
+
default: 'horizontal',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: 'dashboard_size',
|
|
73
|
+
question: 'What percentage of space should the dashboard take?',
|
|
74
|
+
type: 'text',
|
|
75
|
+
dependsOn: { dashboard_mode: 'pane' },
|
|
76
|
+
default: '50%',
|
|
77
|
+
hint: 'e.g., 30%, 50%, 40',
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: 'focused_agent_direction',
|
|
81
|
+
question: 'When you pull an agent into view, where should it appear relative to the dashboard?',
|
|
82
|
+
type: 'select',
|
|
83
|
+
options: [
|
|
84
|
+
{ value: 'vertical', label: 'Below the dashboard' },
|
|
85
|
+
{ value: 'horizontal', label: 'Beside the dashboard' },
|
|
86
|
+
],
|
|
87
|
+
default: 'vertical',
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: 'focused_agent_size',
|
|
91
|
+
question: 'What percentage of space should the focused agent pane take?',
|
|
92
|
+
type: 'text',
|
|
93
|
+
default: '50%',
|
|
94
|
+
hint: 'e.g., 50%, 60%, 70%',
|
|
95
|
+
},
|
|
96
|
+
// ── Pipeline stages ─────────────────────────────────────────────────────
|
|
97
|
+
{
|
|
98
|
+
id: 'custom_stages',
|
|
99
|
+
question: 'Do you want to use custom pipeline stages beyond the defaults (working, stopped)?',
|
|
100
|
+
type: 'confirm',
|
|
101
|
+
default: false,
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: 'stage_list',
|
|
105
|
+
question: 'List your pipeline stages in order (comma-separated):',
|
|
106
|
+
type: 'text',
|
|
107
|
+
dependsOn: { custom_stages: true },
|
|
108
|
+
default: 'working, stopped',
|
|
109
|
+
hint: 'e.g., planning, working, code_review, pr_submitted, stopped',
|
|
110
|
+
},
|
|
111
|
+
// ── Hook modes ──────────────────────────────────────────────────────────
|
|
112
|
+
{
|
|
113
|
+
id: 'hook_modes',
|
|
114
|
+
question: 'What workflow modes do you want? (comma-separated, leave blank for none)',
|
|
115
|
+
type: 'text',
|
|
116
|
+
default: '',
|
|
117
|
+
hint: 'Modes change which hook scripts run. e.g., "planning, testing, review". When mode is "testing", hooks resolve as .ghp/hooks/<name>.testing before falling back to .ghp/hooks/<name>. Press [m] in the dashboard to cycle modes. Leave blank to skip mode support.',
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
id: 'hook_default_mode',
|
|
121
|
+
question: 'Which mode should be active when the dashboard starts?',
|
|
122
|
+
type: 'text',
|
|
123
|
+
default: '',
|
|
124
|
+
hint: 'Must be one of the modes listed above. Leave blank to start with no mode active.',
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
id: 'hook_swap_order',
|
|
128
|
+
question: 'When hot-swapping agents, should the old agent\'s hooks run first or the new agent\'s?',
|
|
129
|
+
type: 'select',
|
|
130
|
+
options: [
|
|
131
|
+
{ value: 'unfocus-first', label: 'Unfocus first (default)', description: 'Stop old servers before starting new ones — avoids port conflicts' },
|
|
132
|
+
{ value: 'focus-first', label: 'Focus first', description: 'Start new servers before stopping old ones — minimizes downtime' },
|
|
133
|
+
],
|
|
134
|
+
default: 'unfocus-first',
|
|
135
|
+
},
|
|
136
|
+
// ── Directory hooks (.ghp/hooks/<name>) ─────────────────────────────────
|
|
137
|
+
{
|
|
138
|
+
id: 'hook_dashboard_opened',
|
|
139
|
+
question: 'What should happen when the pipeline dashboard opens?',
|
|
140
|
+
type: 'text',
|
|
141
|
+
default: '',
|
|
142
|
+
hint: 'Creates .ghp/hooks/dashboard-opened. Fires when the dashboard starts. Stdin JSON: {"pane_id":"%42","window_name":"ghp-admin"}. Runs from the main repo root. Use this for companion panes (dev servers, log viewers). If modes are configured, mode-specific variants (.ghp/hooks/dashboard-opened.<mode>) will also be scaffolded. Leave blank to skip.',
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
id: 'hook_agent_active',
|
|
146
|
+
question: 'What should happen when an agent starts working (PostToolUse)?',
|
|
147
|
+
type: 'text',
|
|
148
|
+
default: '',
|
|
149
|
+
hint: 'Creates .ghp/hooks/agent-active. Fires on Claude Code PostToolUse hook. Stdin: Claude Code hook JSON (includes "cwd"). Runs from the agent\'s cwd. If modes are configured, mode-specific variants will also be scaffolded. Leave blank to skip.',
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
id: 'hook_agent_stopped',
|
|
153
|
+
question: 'What should happen when an agent stops?',
|
|
154
|
+
type: 'text',
|
|
155
|
+
default: '',
|
|
156
|
+
hint: 'Creates .ghp/hooks/agent-stopped. Fires on Claude Code Stop hook. Stdin: Claude Code hook JSON (includes "cwd"). Runs from the agent\'s cwd. If modes are configured, mode-specific variants will also be scaffolded. Leave blank to skip.',
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
id: 'hook_agent_focused',
|
|
160
|
+
question: 'What should happen when you pull an agent into the dashboard view?',
|
|
161
|
+
type: 'text',
|
|
162
|
+
default: '',
|
|
163
|
+
hint: 'Creates .ghp/hooks/agent-focused. Fires when an agent pane is focused via [1-9] in the dashboard. Stdin JSON: {"issueNumber":123,"worktreePath":"/path/to/worktree","branch":"user/123-feature"}. Runs from the worktree directory. If modes are configured, mode-specific variants will also be scaffolded. Leave blank to skip.',
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
id: 'hook_agent_unfocused',
|
|
167
|
+
question: 'What should happen when an agent is released from the dashboard view?',
|
|
168
|
+
type: 'text',
|
|
169
|
+
default: '',
|
|
170
|
+
hint: 'Creates .ghp/hooks/agent-unfocused. Fires when an agent pane is sent back via [esc]. Same stdin payload as agent-focused. Runs from the worktree directory. If modes are configured, mode-specific variants will also be scaffolded. Leave blank to skip.',
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
id: 'hook_agent_swapped',
|
|
174
|
+
question: 'What should happen when switching directly between focused agents (hot-swap)?',
|
|
175
|
+
type: 'text',
|
|
176
|
+
default: '',
|
|
177
|
+
hint: 'Creates .ghp/hooks/agent-swapped. Fires when you press [3] while [1] is focused — an atomic swap. Stdin JSON: {"old":{"issueNumber":123,"worktreePath":"/old","branch":"..."},"new":{"issueNumber":456,"worktreePath":"/new","branch":"..."}}. If this hook doesn\'t exist, falls back to sequential unfocus→focus. If modes are configured, mode-specific variants will also be scaffolded. Leave blank to skip.',
|
|
178
|
+
},
|
|
179
|
+
// ── Event hooks (registered via ghp hooks add) ──────────────────────────
|
|
180
|
+
{
|
|
181
|
+
id: 'hook_issue_created',
|
|
182
|
+
question: 'What command should run when a new issue is created (ghp add)?',
|
|
183
|
+
type: 'text',
|
|
184
|
+
default: '',
|
|
185
|
+
hint: 'Registers an event hook for "issue-created". Template vars: ${number}, ${title}, ${url}. Example: "echo Issue #${number} created: ${title}". This is an event hook (registered with ghp hooks add --event issue-created), not a directory hook. Leave blank to skip.',
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
id: 'hook_issue_started',
|
|
189
|
+
question: 'What command should run when work starts on an issue (ghp start)?',
|
|
190
|
+
type: 'text',
|
|
191
|
+
default: '',
|
|
192
|
+
hint: 'Registers an event hook for "issue-started". Template vars: ${number}, ${branch}, ${worktreePath}. Leave blank to skip.',
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
id: 'hook_worktree_created',
|
|
196
|
+
question: 'What command should run after a worktree is created?',
|
|
197
|
+
type: 'text',
|
|
198
|
+
default: '',
|
|
199
|
+
hint: 'Registers an event hook for "worktree-created". Template vars: ${number}, ${worktreePath}, ${branch}. Leave blank to skip.',
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
id: 'hook_worktree_removed',
|
|
203
|
+
question: 'What command should run after a worktree is removed?',
|
|
204
|
+
type: 'text',
|
|
205
|
+
default: '',
|
|
206
|
+
hint: 'Registers an event hook for "worktree-removed". Template vars: ${number}, ${worktreePath}, ${branch}. Leave blank to skip.',
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
id: 'hook_pre_pr',
|
|
210
|
+
question: 'What command should run before PR creation?',
|
|
211
|
+
type: 'text',
|
|
212
|
+
default: '',
|
|
213
|
+
hint: 'Registers an event hook for "pre-pr". Template vars: ${number}, ${branch}. Runs in blocking mode by default. Leave blank to skip.',
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
id: 'hook_pr_created',
|
|
217
|
+
question: 'What command should run after a PR is created?',
|
|
218
|
+
type: 'text',
|
|
219
|
+
default: '',
|
|
220
|
+
hint: 'Registers an event hook for "pr-created". Template vars: ${number}, ${prNumber}, ${prUrl}. Leave blank to skip.',
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
id: 'hook_pr_merged',
|
|
224
|
+
question: 'What command should run after a PR is merged?',
|
|
225
|
+
type: 'text',
|
|
226
|
+
default: '',
|
|
227
|
+
hint: 'Registers an event hook for "pr-merged". Template vars: ${number}, ${prNumber}, ${branch}. Leave blank to skip.',
|
|
228
|
+
},
|
|
229
|
+
];
|
|
230
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
231
|
+
// Helpers
|
|
232
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
233
|
+
/** Read all of stdin as a string. */
|
|
234
|
+
async function readStdin() {
|
|
235
|
+
return new Promise((resolve, reject) => {
|
|
236
|
+
const chunks = [];
|
|
237
|
+
process.stdin.on('data', (chunk) => chunks.push(chunk));
|
|
238
|
+
process.stdin.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
|
|
239
|
+
process.stdin.on('error', reject);
|
|
240
|
+
setTimeout(() => resolve(Buffer.concat(chunks).toString('utf-8')), 3000);
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
/** Check if a question's dependsOn conditions are met. */
|
|
244
|
+
function isDependencyMet(question, answers) {
|
|
245
|
+
if (!question.dependsOn)
|
|
246
|
+
return true;
|
|
247
|
+
for (const [key, expected] of Object.entries(question.dependsOn)) {
|
|
248
|
+
if (answers[key] !== expected)
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
/** Read the raw user config JSON (including non-Config fields like flavors). */
|
|
254
|
+
function loadRawUserConfig() {
|
|
255
|
+
const configPath = getUserConfigPath();
|
|
256
|
+
try {
|
|
257
|
+
if (existsSync(configPath)) {
|
|
258
|
+
return JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
catch { /* ignore */ }
|
|
262
|
+
return {};
|
|
263
|
+
}
|
|
264
|
+
/** Write raw user config JSON. */
|
|
265
|
+
function saveRawUserConfig(data) {
|
|
266
|
+
const configPath = getUserConfigPath();
|
|
267
|
+
const dir = join(configPath, '..');
|
|
268
|
+
if (!existsSync(dir)) {
|
|
269
|
+
mkdirSync(dir, { recursive: true });
|
|
270
|
+
}
|
|
271
|
+
writeFileSync(configPath, JSON.stringify(data, null, 2));
|
|
272
|
+
}
|
|
273
|
+
/** Parse comma-separated string into array, filtering empties. */
|
|
274
|
+
function parseCommaSeparated(value) {
|
|
275
|
+
if (!value || typeof value !== 'string')
|
|
276
|
+
return [];
|
|
277
|
+
return value.split(',').map(s => s.trim()).filter(Boolean);
|
|
278
|
+
}
|
|
279
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
280
|
+
// Flavor CRUD
|
|
281
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
282
|
+
function getFlavors() {
|
|
283
|
+
const raw = loadRawUserConfig();
|
|
284
|
+
return raw.pipelineSetupFlavors ?? {};
|
|
285
|
+
}
|
|
286
|
+
function saveFlavor(name, answers) {
|
|
287
|
+
const raw = loadRawUserConfig();
|
|
288
|
+
const flavors = raw.pipelineSetupFlavors ?? {};
|
|
289
|
+
flavors[name] = answers;
|
|
290
|
+
raw.pipelineSetupFlavors = flavors;
|
|
291
|
+
saveRawUserConfig(raw);
|
|
292
|
+
}
|
|
293
|
+
function deleteFlavor(name) {
|
|
294
|
+
const raw = loadRawUserConfig();
|
|
295
|
+
const flavors = raw.pipelineSetupFlavors ?? {};
|
|
296
|
+
if (!(name in flavors))
|
|
297
|
+
return false;
|
|
298
|
+
delete flavors[name];
|
|
299
|
+
raw.pipelineSetupFlavors = flavors;
|
|
300
|
+
saveRawUserConfig(raw);
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
304
|
+
// Directory hook scaffolding
|
|
305
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
306
|
+
/** Map of directory hook answer IDs to hook script names and templates. */
|
|
307
|
+
const DIRECTORY_HOOKS = {
|
|
308
|
+
hook_dashboard_opened: { hookName: 'dashboard-opened', template: DEFAULT_DASHBOARD_OPENED_SCRIPT() },
|
|
309
|
+
hook_agent_active: { hookName: 'agent-active', template: DEFAULT_AGENT_ACTIVE_SCRIPT() },
|
|
310
|
+
hook_agent_stopped: { hookName: 'agent-stopped', template: DEFAULT_AGENT_STOPPED_SCRIPT() },
|
|
311
|
+
hook_agent_focused: { hookName: 'agent-focused', template: DEFAULT_AGENT_FOCUSED_SCRIPT() },
|
|
312
|
+
hook_agent_unfocused: { hookName: 'agent-unfocused', template: DEFAULT_AGENT_UNFOCUSED_SCRIPT() },
|
|
313
|
+
hook_agent_swapped: { hookName: 'agent-swapped', template: DEFAULT_AGENT_SWAPPED_SCRIPT() },
|
|
314
|
+
};
|
|
315
|
+
/** Map of event hook answer IDs to event names. */
|
|
316
|
+
const EVENT_HOOKS = {
|
|
317
|
+
hook_issue_created: { event: 'issue-created' },
|
|
318
|
+
hook_issue_started: { event: 'issue-started' },
|
|
319
|
+
hook_worktree_created: { event: 'worktree-created' },
|
|
320
|
+
hook_worktree_removed: { event: 'worktree-removed' },
|
|
321
|
+
hook_pre_pr: { event: 'pre-pr', defaultMode: 'blocking' },
|
|
322
|
+
hook_pr_created: { event: 'pr-created' },
|
|
323
|
+
hook_pr_merged: { event: 'pr-merged' },
|
|
324
|
+
};
|
|
325
|
+
/**
|
|
326
|
+
* Scaffold a directory hook script (and mode-specific variants).
|
|
327
|
+
* Returns array of summary messages.
|
|
328
|
+
*/
|
|
329
|
+
function scaffoldDirectoryHook(hooksDir, hookName, template, description, modes) {
|
|
330
|
+
const summary = [];
|
|
331
|
+
// Base hook
|
|
332
|
+
const basePath = join(hooksDir, hookName);
|
|
333
|
+
if (existsSync(basePath)) {
|
|
334
|
+
summary.push(`Skipped ${basePath} (already exists)`);
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
writeFileSync(basePath, template);
|
|
338
|
+
chmodSync(basePath, 0o755);
|
|
339
|
+
summary.push(`Created ${basePath} — ${description}`);
|
|
340
|
+
}
|
|
341
|
+
// Mode-specific variants
|
|
342
|
+
for (const mode of modes) {
|
|
343
|
+
const modePath = join(hooksDir, `${hookName}.${mode}`);
|
|
344
|
+
if (existsSync(modePath)) {
|
|
345
|
+
summary.push(`Skipped ${modePath} (already exists)`);
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
const modeTemplate = template.replace(/^(# Hook: .+)$/m, `$1 (mode: ${mode})`);
|
|
349
|
+
writeFileSync(modePath, modeTemplate);
|
|
350
|
+
chmodSync(modePath, 0o755);
|
|
351
|
+
summary.push(`Created ${modePath}`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return summary;
|
|
355
|
+
}
|
|
356
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
357
|
+
// Apply logic
|
|
358
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
359
|
+
async function applyAnswers(answers) {
|
|
360
|
+
const scope = answers.config_scope || 'user';
|
|
361
|
+
const summary = [];
|
|
362
|
+
// Save flavor if requested
|
|
363
|
+
if (answers.save_flavor === true && answers.flavor_name && typeof answers.flavor_name === 'string' && answers.flavor_name.trim()) {
|
|
364
|
+
saveFlavor(answers.flavor_name.trim(), answers);
|
|
365
|
+
summary.push(`Saved flavor "${answers.flavor_name.trim()}" — reapply with: ghp pipeline setup --flavor ${answers.flavor_name.trim()}`);
|
|
366
|
+
}
|
|
367
|
+
// Dashboard config
|
|
368
|
+
if (answers.dashboard_mode) {
|
|
369
|
+
setConfigByPath('parallelWork.dashboard.mode', answers.dashboard_mode, scope);
|
|
370
|
+
summary.push(`dashboard.mode = ${answers.dashboard_mode}`);
|
|
371
|
+
}
|
|
372
|
+
if (answers.dashboard_direction) {
|
|
373
|
+
setConfigByPath('parallelWork.dashboard.direction', answers.dashboard_direction, scope);
|
|
374
|
+
summary.push(`dashboard.direction = ${answers.dashboard_direction}`);
|
|
375
|
+
}
|
|
376
|
+
if (answers.dashboard_size) {
|
|
377
|
+
setConfigByPath('parallelWork.dashboard.size', answers.dashboard_size, scope);
|
|
378
|
+
summary.push(`dashboard.size = ${answers.dashboard_size}`);
|
|
379
|
+
}
|
|
380
|
+
if (answers.focused_agent_direction) {
|
|
381
|
+
setConfigByPath('parallelWork.dashboard.focusedAgent.direction', answers.focused_agent_direction, scope);
|
|
382
|
+
summary.push(`focusedAgent.direction = ${answers.focused_agent_direction}`);
|
|
383
|
+
}
|
|
384
|
+
if (answers.focused_agent_size) {
|
|
385
|
+
setConfigByPath('parallelWork.dashboard.focusedAgent.size', answers.focused_agent_size, scope);
|
|
386
|
+
summary.push(`focusedAgent.size = ${answers.focused_agent_size}`);
|
|
387
|
+
}
|
|
388
|
+
// Pipeline stages
|
|
389
|
+
if (answers.custom_stages === true && answers.stage_list) {
|
|
390
|
+
const stages = parseCommaSeparated(answers.stage_list);
|
|
391
|
+
if (stages.length > 0) {
|
|
392
|
+
setConfigByPath('pipeline.stages', stages, scope);
|
|
393
|
+
summary.push(`pipeline.stages = [${stages.join(', ')}]`);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
// Hook modes config
|
|
397
|
+
const hookModes = parseCommaSeparated(answers.hook_modes);
|
|
398
|
+
if (hookModes.length > 0) {
|
|
399
|
+
setConfigByPath('pipeline.hookModes', hookModes, scope);
|
|
400
|
+
summary.push(`pipeline.hookModes = [${hookModes.join(', ')}]`);
|
|
401
|
+
}
|
|
402
|
+
if (answers.hook_default_mode && typeof answers.hook_default_mode === 'string' && answers.hook_default_mode.trim()) {
|
|
403
|
+
setConfigByPath('pipeline.defaultHookMode', answers.hook_default_mode.trim(), scope);
|
|
404
|
+
summary.push(`pipeline.defaultHookMode = ${answers.hook_default_mode}`);
|
|
405
|
+
}
|
|
406
|
+
if (answers.hook_swap_order && answers.hook_swap_order !== 'unfocus-first') {
|
|
407
|
+
setConfigByPath('pipeline.hookModeSwapOrder', answers.hook_swap_order, scope);
|
|
408
|
+
summary.push(`pipeline.hookModeSwapOrder = ${answers.hook_swap_order}`);
|
|
409
|
+
}
|
|
410
|
+
// Directory hooks
|
|
411
|
+
const repoRoot = await getMainWorktreeRoot();
|
|
412
|
+
if (repoRoot) {
|
|
413
|
+
const hooksDir = join(repoRoot, '.ghp', 'hooks');
|
|
414
|
+
for (const [answerId, { hookName, template }] of Object.entries(DIRECTORY_HOOKS)) {
|
|
415
|
+
const answer = answers[answerId];
|
|
416
|
+
if (answer && typeof answer === 'string' && answer.trim()) {
|
|
417
|
+
if (!existsSync(hooksDir)) {
|
|
418
|
+
mkdirSync(hooksDir, { recursive: true });
|
|
419
|
+
}
|
|
420
|
+
const msgs = scaffoldDirectoryHook(hooksDir, hookName, template, answer.trim(), hookModes);
|
|
421
|
+
summary.push(...msgs);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
else if (Object.keys(DIRECTORY_HOOKS).some(id => {
|
|
426
|
+
const a = answers[id];
|
|
427
|
+
return a && typeof a === 'string' && a.trim();
|
|
428
|
+
})) {
|
|
429
|
+
console.error(chalk.yellow('Warning:'), 'Not in a git repository — skipped hook script generation');
|
|
430
|
+
}
|
|
431
|
+
// Event hooks (register via ghp hooks add)
|
|
432
|
+
for (const [answerId, { event, defaultMode }] of Object.entries(EVENT_HOOKS)) {
|
|
433
|
+
const answer = answers[answerId];
|
|
434
|
+
if (answer && typeof answer === 'string' && answer.trim()) {
|
|
435
|
+
const hookName = `setup-${event}`;
|
|
436
|
+
const mode = defaultMode || 'fire-and-forget';
|
|
437
|
+
try {
|
|
438
|
+
const { execFileSync } = await import('child_process');
|
|
439
|
+
execFileSync('ghp', [
|
|
440
|
+
'hooks', 'add', hookName,
|
|
441
|
+
'--event', event,
|
|
442
|
+
'--command', answer.trim(),
|
|
443
|
+
'--mode', mode,
|
|
444
|
+
], { stdio: 'pipe' });
|
|
445
|
+
summary.push(`Registered event hook: ${hookName} (${event}) → ${answer.trim().substring(0, 60)}`);
|
|
446
|
+
}
|
|
447
|
+
catch (err) {
|
|
448
|
+
summary.push(`Failed to register event hook ${hookName}: ${err instanceof Error ? err.message : 'unknown error'}`);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
// Print summary
|
|
453
|
+
console.log();
|
|
454
|
+
console.log(chalk.bold.green('Setup complete!'));
|
|
455
|
+
console.log();
|
|
456
|
+
const scopeLabel = scope === 'workspace' ? '.ghp/config.json' : '~/.config/ghp-cli/config.json';
|
|
457
|
+
console.log(chalk.dim(`Config scope: ${scopeLabel}`));
|
|
458
|
+
console.log();
|
|
459
|
+
for (const line of summary) {
|
|
460
|
+
console.log(` ${chalk.green('✓')} ${line}`);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
464
|
+
// Default hook scripts (scaffolded for devs/agents to customize)
|
|
465
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
466
|
+
function DEFAULT_DASHBOARD_OPENED_SCRIPT() {
|
|
467
|
+
return `#!/usr/bin/env bash
|
|
468
|
+
# Hook: dashboard-opened
|
|
469
|
+
# Runs when the pipeline dashboard opens. Use this to spawn companion panes
|
|
470
|
+
# (dev servers, log viewers, test watchers, etc.) alongside the dashboard.
|
|
471
|
+
#
|
|
472
|
+
# Receives JSON on stdin with these exact keys:
|
|
473
|
+
# { "pane_id": "%42", "window_name": "ghp-admin" }
|
|
474
|
+
#
|
|
475
|
+
# IMPORTANT: The pane_id is the tmux pane where the dashboard is running.
|
|
476
|
+
# All split-window commands should target this pane with -t "$DASHBOARD_PANE".
|
|
477
|
+
# This script runs from the main repo root as its working directory.
|
|
478
|
+
|
|
479
|
+
INPUT=$(cat)
|
|
480
|
+
DASHBOARD_PANE=$(echo "$INPUT" | jq -r '.pane_id')
|
|
481
|
+
|
|
482
|
+
if [ -z "$DASHBOARD_PANE" ] || [ "$DASHBOARD_PANE" = "null" ]; then
|
|
483
|
+
DASHBOARD_PANE=$(tmux display-message -p "#{pane_id}")
|
|
484
|
+
fi
|
|
485
|
+
|
|
486
|
+
# ── Add your companion panes below ──────────────────────────────────────────
|
|
487
|
+
# Examples:
|
|
488
|
+
#
|
|
489
|
+
# Split left of dashboard, run dev server (50% width):
|
|
490
|
+
# DEV=$(tmux split-window -hb -l 50% -t "$DASHBOARD_PANE" -P -F '#{pane_id}' 'pnpm dev')
|
|
491
|
+
# tmux select-pane -t "$DEV" -T 'Dev Server'
|
|
492
|
+
#
|
|
493
|
+
# Split below dashboard, run tests (30% height):
|
|
494
|
+
# TESTS=$(tmux split-window -v -l 30% -t "$DASHBOARD_PANE" -P -F '#{pane_id}' 'pnpm test --watch')
|
|
495
|
+
# tmux select-pane -t "$TESTS" -T 'Tests'
|
|
496
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
497
|
+
|
|
498
|
+
# Refocus the dashboard
|
|
499
|
+
tmux select-pane -t "$DASHBOARD_PANE"
|
|
500
|
+
`;
|
|
501
|
+
}
|
|
502
|
+
function DEFAULT_AGENT_ACTIVE_SCRIPT() {
|
|
503
|
+
return `#!/usr/bin/env bash
|
|
504
|
+
# Hook: agent-active
|
|
505
|
+
# Runs when a Claude agent starts working (PostToolUse hook).
|
|
506
|
+
# Stdin: Claude Code hook JSON (includes "cwd", "tool_name", etc.)
|
|
507
|
+
# This script runs from the agent's cwd.
|
|
508
|
+
|
|
509
|
+
INPUT=$(cat)
|
|
510
|
+
CWD=$(echo "$INPUT" | jq -r '.cwd')
|
|
511
|
+
|
|
512
|
+
# ── Add your actions below ───────────────────────────────────────────────────
|
|
513
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
514
|
+
`;
|
|
515
|
+
}
|
|
516
|
+
function DEFAULT_AGENT_STOPPED_SCRIPT() {
|
|
517
|
+
return `#!/usr/bin/env bash
|
|
518
|
+
# Hook: agent-stopped
|
|
519
|
+
# Runs when a Claude agent stops (Stop hook).
|
|
520
|
+
# Stdin: Claude Code hook JSON (includes "cwd")
|
|
521
|
+
# This script runs from the agent's cwd.
|
|
522
|
+
|
|
523
|
+
INPUT=$(cat)
|
|
524
|
+
CWD=$(echo "$INPUT" | jq -r '.cwd')
|
|
525
|
+
|
|
526
|
+
# ── Add your actions below ───────────────────────────────────────────────────
|
|
527
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
528
|
+
`;
|
|
529
|
+
}
|
|
530
|
+
function DEFAULT_AGENT_FOCUSED_SCRIPT() {
|
|
531
|
+
return `#!/usr/bin/env bash
|
|
532
|
+
# Hook: agent-focused
|
|
533
|
+
# Runs when an agent pane is pulled into the dashboard view (via [1-9] key).
|
|
534
|
+
# Use this to show extra context for the focused agent.
|
|
535
|
+
#
|
|
536
|
+
# Receives JSON on stdin with these exact keys:
|
|
537
|
+
# { "issueNumber": 123, "worktreePath": "/path/to/worktree", "branch": "user/123-feature" }
|
|
538
|
+
#
|
|
539
|
+
# This script runs from the worktree directory.
|
|
540
|
+
|
|
541
|
+
INPUT=$(cat)
|
|
542
|
+
ISSUE=$(echo "$INPUT" | jq -r '.issueNumber')
|
|
543
|
+
WORKTREE=$(echo "$INPUT" | jq -r '.worktreePath')
|
|
544
|
+
BRANCH=$(echo "$INPUT" | jq -r '.branch')
|
|
545
|
+
|
|
546
|
+
# ── Add your focus actions below ─────────────────────────────────────────────
|
|
547
|
+
# Examples:
|
|
548
|
+
#
|
|
549
|
+
# Show git log for this agent's branch:
|
|
550
|
+
# tmux split-window -v -l 20% -t "$TMUX_PANE" "cd '$WORKTREE' && git log --oneline -20"
|
|
551
|
+
#
|
|
552
|
+
# Tail the agent's conversation log:
|
|
553
|
+
# tmux split-window -v -l 30% -t "$TMUX_PANE" "tail -f '$WORKTREE/.claude/logs/latest.log'"
|
|
554
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
555
|
+
`;
|
|
556
|
+
}
|
|
557
|
+
function DEFAULT_AGENT_UNFOCUSED_SCRIPT() {
|
|
558
|
+
return `#!/usr/bin/env bash
|
|
559
|
+
# Hook: agent-unfocused
|
|
560
|
+
# Runs when an agent pane is released from the dashboard view (via [esc] key).
|
|
561
|
+
# Use this to clean up anything spawned by agent-focused.
|
|
562
|
+
#
|
|
563
|
+
# Receives JSON on stdin with these exact keys:
|
|
564
|
+
# { "issueNumber": 123, "worktreePath": "/path/to/worktree", "branch": "user/123-feature" }
|
|
565
|
+
#
|
|
566
|
+
# This script runs from the worktree directory.
|
|
567
|
+
|
|
568
|
+
INPUT=$(cat)
|
|
569
|
+
ISSUE=$(echo "$INPUT" | jq -r '.issueNumber')
|
|
570
|
+
WORKTREE=$(echo "$INPUT" | jq -r '.worktreePath')
|
|
571
|
+
BRANCH=$(echo "$INPUT" | jq -r '.branch')
|
|
572
|
+
|
|
573
|
+
# ── Add your cleanup actions below ───────────────────────────────────────────
|
|
574
|
+
# Examples:
|
|
575
|
+
#
|
|
576
|
+
# Kill any extra panes spawned by agent-focused:
|
|
577
|
+
# # (track pane IDs in a temp file from agent-focused, then kill them here)
|
|
578
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
579
|
+
`;
|
|
580
|
+
}
|
|
581
|
+
function DEFAULT_AGENT_SWAPPED_SCRIPT() {
|
|
582
|
+
return `#!/usr/bin/env bash
|
|
583
|
+
# Hook: agent-swapped
|
|
584
|
+
# Runs when switching directly from one focused agent to another (hot-swap).
|
|
585
|
+
# This is an atomic swap — use it to stop old servers and start new ones
|
|
586
|
+
# without port conflicts.
|
|
587
|
+
#
|
|
588
|
+
# Receives JSON on stdin with these exact keys:
|
|
589
|
+
# {
|
|
590
|
+
# "old": { "issueNumber": 123, "worktreePath": "/path/old", "branch": "user/123-feat" },
|
|
591
|
+
# "new": { "issueNumber": 456, "worktreePath": "/path/new", "branch": "user/456-other" }
|
|
592
|
+
# }
|
|
593
|
+
#
|
|
594
|
+
# If this hook doesn't exist, falls back to sequential unfocus→focus.
|
|
595
|
+
# This script runs from the NEW agent's worktree directory.
|
|
596
|
+
|
|
597
|
+
INPUT=$(cat)
|
|
598
|
+
OLD_ISSUE=$(echo "$INPUT" | jq -r '.old.issueNumber')
|
|
599
|
+
OLD_WORKTREE=$(echo "$INPUT" | jq -r '.old.worktreePath')
|
|
600
|
+
OLD_BRANCH=$(echo "$INPUT" | jq -r '.old.branch')
|
|
601
|
+
NEW_ISSUE=$(echo "$INPUT" | jq -r '.new.issueNumber')
|
|
602
|
+
NEW_WORKTREE=$(echo "$INPUT" | jq -r '.new.worktreePath')
|
|
603
|
+
NEW_BRANCH=$(echo "$INPUT" | jq -r '.new.branch')
|
|
604
|
+
|
|
605
|
+
# ── Add your swap actions below ──────────────────────────────────────────────
|
|
606
|
+
# Examples:
|
|
607
|
+
#
|
|
608
|
+
# Stop old dev server, start new one:
|
|
609
|
+
# kill $(cat "$OLD_WORKTREE/.dev-server.pid") 2>/dev/null
|
|
610
|
+
# cd "$NEW_WORKTREE" && pnpm dev &
|
|
611
|
+
# echo $! > "$NEW_WORKTREE/.dev-server.pid"
|
|
612
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
613
|
+
`;
|
|
614
|
+
}
|
|
615
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
616
|
+
// Interactive wizard
|
|
617
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
618
|
+
async function runInteractiveWizard() {
|
|
619
|
+
const answers = {};
|
|
620
|
+
console.log(chalk.bold('Pipeline Setup Wizard'));
|
|
621
|
+
console.log(chalk.dim('Configure your pipeline dashboard.\n'));
|
|
622
|
+
for (const q of QUESTIONS) {
|
|
623
|
+
if (!isDependencyMet(q, answers))
|
|
624
|
+
continue;
|
|
625
|
+
if (q.type === 'select' && q.options) {
|
|
626
|
+
const optionLabels = q.options.map(o => o.description ? `${o.label} ${chalk.dim(`— ${o.description}`)}` : o.label);
|
|
627
|
+
const defaultIdx = q.options.findIndex(o => o.value === q.default);
|
|
628
|
+
const idx = await promptSelect(chalk.bold(q.question), optionLabels);
|
|
629
|
+
answers[q.id] = q.options[idx >= 0 ? idx : (defaultIdx >= 0 ? defaultIdx : 0)].value;
|
|
630
|
+
console.log();
|
|
631
|
+
}
|
|
632
|
+
else if (q.type === 'confirm') {
|
|
633
|
+
const defaultVal = q.default === true;
|
|
634
|
+
const result = await confirmWithDefault(chalk.bold(q.question), defaultVal);
|
|
635
|
+
answers[q.id] = result;
|
|
636
|
+
console.log();
|
|
637
|
+
}
|
|
638
|
+
else if (q.type === 'text') {
|
|
639
|
+
const defaultVal = q.default || '';
|
|
640
|
+
if (q.hint)
|
|
641
|
+
console.log(chalk.dim(` ${q.hint}`));
|
|
642
|
+
const val = await promptWithDefault(`${chalk.bold(q.question)} [${defaultVal}] `, defaultVal);
|
|
643
|
+
answers[q.id] = val || defaultVal;
|
|
644
|
+
console.log();
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
return answers;
|
|
648
|
+
}
|
|
649
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
650
|
+
// Main command
|
|
651
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
652
|
+
export async function pipelineSetupCommand(options) {
|
|
653
|
+
// ── --questions: output schema ───────────────────────────────────────────
|
|
654
|
+
if (options.questions) {
|
|
655
|
+
console.log(JSON.stringify(QUESTIONS, null, 2));
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
// ── --flavors: list saved flavors ────────────────────────────────────────
|
|
659
|
+
if (options.flavors) {
|
|
660
|
+
const flavors = getFlavors();
|
|
661
|
+
const names = Object.keys(flavors);
|
|
662
|
+
if (names.length === 0) {
|
|
663
|
+
console.log(chalk.dim('No saved flavors.'));
|
|
664
|
+
console.log(chalk.dim('Save one with: echo \'{"answers":"..."}\' | ghp pipeline setup --save <name>'));
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
console.log(chalk.bold('Saved flavors:'));
|
|
668
|
+
for (const name of names) {
|
|
669
|
+
const flavor = flavors[name];
|
|
670
|
+
const mode = flavor.dashboard_mode || 'default';
|
|
671
|
+
console.log(` ${chalk.cyan(name)} ${chalk.dim(`— dashboard: ${mode}`)}`);
|
|
672
|
+
}
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
// ── --delete-flavor <name> ───────────────────────────────────────────────
|
|
676
|
+
if (options.deleteFlavor) {
|
|
677
|
+
if (deleteFlavor(options.deleteFlavor)) {
|
|
678
|
+
console.log(chalk.green('✓'), `Deleted flavor "${options.deleteFlavor}"`);
|
|
679
|
+
}
|
|
680
|
+
else {
|
|
681
|
+
console.error(chalk.red('Error:'), `Flavor "${options.deleteFlavor}" not found`);
|
|
682
|
+
exit(1);
|
|
683
|
+
}
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
// ── --save <name>: save answers from stdin as a flavor ───────────────────
|
|
687
|
+
if (options.save) {
|
|
688
|
+
let answers;
|
|
689
|
+
try {
|
|
690
|
+
const input = await readStdin();
|
|
691
|
+
answers = JSON.parse(input.trim());
|
|
692
|
+
}
|
|
693
|
+
catch {
|
|
694
|
+
console.error(chalk.red('Error:'), 'Could not parse JSON from stdin');
|
|
695
|
+
exit(1);
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
saveFlavor(options.save, answers);
|
|
699
|
+
console.log(chalk.green('✓'), `Saved flavor "${options.save}"`);
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
// ── --flavor <name>: load and apply a saved flavor ───────────────────────
|
|
703
|
+
if (options.flavor) {
|
|
704
|
+
const flavors = getFlavors();
|
|
705
|
+
const answers = flavors[options.flavor];
|
|
706
|
+
if (!answers) {
|
|
707
|
+
console.error(chalk.red('Error:'), `Flavor "${options.flavor}" not found`);
|
|
708
|
+
console.error('Available flavors:', Object.keys(flavors).join(', ') || '(none)');
|
|
709
|
+
exit(1);
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
console.log(chalk.dim(`Applying flavor "${options.flavor}"...`));
|
|
713
|
+
await applyAnswers(answers);
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
// ── --apply: read answers from stdin and apply ───────────────────────────
|
|
717
|
+
if (options.apply) {
|
|
718
|
+
let answers;
|
|
719
|
+
try {
|
|
720
|
+
const input = await readStdin();
|
|
721
|
+
answers = JSON.parse(input.trim());
|
|
722
|
+
}
|
|
723
|
+
catch {
|
|
724
|
+
console.error(chalk.red('Error:'), 'Could not parse JSON from stdin');
|
|
725
|
+
exit(1);
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
await applyAnswers(answers);
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
// ── Interactive wizard (no flags) ────────────────────────────────────────
|
|
732
|
+
if (!isInteractive()) {
|
|
733
|
+
console.error(chalk.red('Error:'), 'Interactive mode requires a TTY. Use --questions/--apply for non-interactive use.');
|
|
734
|
+
exit(1);
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
const answers = await runInteractiveWizard();
|
|
738
|
+
await applyAnswers(answers);
|
|
739
|
+
}
|
|
740
|
+
//# sourceMappingURL=pipeline-setup.js.map
|