@haystackeditor/cli 0.15.3 → 0.15.4
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/setup.js +17 -8
- package/dist/utils/prompter.d.ts +9 -4
- package/dist/utils/prompter.js +35 -14
- package/package.json +1 -1
package/dist/commands/setup.js
CHANGED
|
@@ -790,9 +790,9 @@ async function stepEnsureAppInstalled(token) {
|
|
|
790
790
|
// ui.action emits an `action` event (JSON) or prints + spins (TTY), then polls
|
|
791
791
|
// until the App is detected or it times out. A mid-poll auth failure surfaces
|
|
792
792
|
// as a thrown InstallationsAuthError.
|
|
793
|
-
let
|
|
793
|
+
let result;
|
|
794
794
|
try {
|
|
795
|
-
|
|
795
|
+
result = await ui.action({
|
|
796
796
|
id: 'install_app',
|
|
797
797
|
message: 'Install the Haystack GitHub App, then it is detected automatically.',
|
|
798
798
|
url: HAYSTACK_APP_INSTALL_URL,
|
|
@@ -810,10 +810,18 @@ async function stepEnsureAppInstalled(token) {
|
|
|
810
810
|
}
|
|
811
811
|
throw err;
|
|
812
812
|
}
|
|
813
|
-
if (
|
|
813
|
+
if (result === 'ok') {
|
|
814
814
|
console.log(chalk.green('\n ✓ App installed\n'));
|
|
815
815
|
return;
|
|
816
816
|
}
|
|
817
|
+
if (result === 'skip') {
|
|
818
|
+
// Caller intentionally skipped — proceed (refuse-to-fail), but warn that the
|
|
819
|
+
// App is needed for Haystack to actually act on the configured repos.
|
|
820
|
+
console.log(chalk.yellow('\n Skipping GitHub App verification.'));
|
|
821
|
+
console.log(chalk.dim(' Haystack needs the App installed to analyze, triage, or merge your PRs.\n'));
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
// result === 'timeout'
|
|
817
825
|
trackError('haystack_setup_app_install_timeout', { timeout_ms: APP_INSTALL_TIMEOUT_MS });
|
|
818
826
|
console.log(chalk.yellow('\n Timed out waiting for App install.'));
|
|
819
827
|
console.log(chalk.dim(` Install at ${HAYSTACK_APP_INSTALL_URL}, then re-run \`haystack setup\`.\n`));
|
|
@@ -836,11 +844,12 @@ async function stepSelectRepos(token) {
|
|
|
836
844
|
});
|
|
837
845
|
if (selectedRepos.length > 0)
|
|
838
846
|
break;
|
|
839
|
-
// Interactive: keep the picker open so the user can correct an
|
|
840
|
-
// (restores the old inquirer `validate`
|
|
841
|
-
//
|
|
842
|
-
// so fail
|
|
843
|
-
|
|
847
|
+
// Interactive live pick: keep the picker open so the user can correct an
|
|
848
|
+
// empty selection (restores the old inquirer `validate`). But a PRE-SUPPLIED
|
|
849
|
+
// empty selection (--answers select_repos:[]) would be returned unchanged
|
|
850
|
+
// every iteration — re-prompting can't fix it, so fail instead of looping.
|
|
851
|
+
// Non-interactive likewise can't be corrected.
|
|
852
|
+
if (!ui.interactive || ui.hasPreset('select_repos')) {
|
|
844
853
|
console.log(chalk.yellow('\n No repositories selected. Pass --repo <owner/name>.\n'));
|
|
845
854
|
process.exit(1);
|
|
846
855
|
}
|
package/dist/utils/prompter.d.ts
CHANGED
|
@@ -31,10 +31,16 @@ export declare class PromptCancelledError extends Error {
|
|
|
31
31
|
readonly requestID: string;
|
|
32
32
|
constructor(requestID: string);
|
|
33
33
|
}
|
|
34
|
+
/** Outcome of an `action`: satisfied, intentionally skipped, or timed out —
|
|
35
|
+
* distinct so callers don't report a skip as a timeout. */
|
|
36
|
+
export type ActionResult = 'ok' | 'skip' | 'timeout';
|
|
34
37
|
export interface Prompter {
|
|
35
38
|
/** True for the human TTY prompter — gates side effects like opening a browser
|
|
36
39
|
* that only make sense for a local interactive user. */
|
|
37
40
|
readonly interactive: boolean;
|
|
41
|
+
/** Whether an answer for `id` was pre-supplied (flag / --answers), so the
|
|
42
|
+
* prompt is skipped. Callers use this to avoid re-prompting a fixed answer. */
|
|
43
|
+
hasPreset(id: string): boolean;
|
|
38
44
|
/** Yes/no decision. */
|
|
39
45
|
confirm(q: {
|
|
40
46
|
id: string;
|
|
@@ -49,9 +55,8 @@ export interface Prompter {
|
|
|
49
55
|
}): Promise<T[]>;
|
|
50
56
|
/**
|
|
51
57
|
* An action the user must take out-of-band (e.g. install the GitHub App).
|
|
52
|
-
* Polls `check()` until it returns true (
|
|
53
|
-
* A pre-supplied / stdin
|
|
54
|
-
* Returns true when satisfied, false when skipped or timed out.
|
|
58
|
+
* Polls `check()` until it returns true (`'ok'`) or `timeoutMs` elapses
|
|
59
|
+
* (`'timeout'`). A pre-supplied / stdin "skip"/reject resolves `'skip'`.
|
|
55
60
|
*/
|
|
56
61
|
action(q: {
|
|
57
62
|
id: string;
|
|
@@ -60,7 +65,7 @@ export interface Prompter {
|
|
|
60
65
|
check: () => Promise<boolean>;
|
|
61
66
|
pollIntervalMs?: number;
|
|
62
67
|
timeoutMs?: number;
|
|
63
|
-
}): Promise<
|
|
68
|
+
}): Promise<ActionResult>;
|
|
64
69
|
/** Transient progress line (scan ticker, per-repo write status). */
|
|
65
70
|
progress(message: string, detail?: string): void;
|
|
66
71
|
/** Clear the current progress line. */
|
package/dist/utils/prompter.js
CHANGED
|
@@ -50,9 +50,12 @@ class TtyPrompter {
|
|
|
50
50
|
constructor(answers) {
|
|
51
51
|
this.answers = answers;
|
|
52
52
|
}
|
|
53
|
+
hasPreset(id) {
|
|
54
|
+
return id in this.answers;
|
|
55
|
+
}
|
|
53
56
|
async confirm(q) {
|
|
54
57
|
if (q.id in this.answers)
|
|
55
|
-
return
|
|
58
|
+
return toBool(this.answers[q.id]);
|
|
56
59
|
const { value } = await inquirer.prompt([
|
|
57
60
|
{ type: 'confirm', name: 'value', message: q.message, default: q.default ?? true },
|
|
58
61
|
]);
|
|
@@ -69,9 +72,9 @@ class TtyPrompter {
|
|
|
69
72
|
}
|
|
70
73
|
async action(q) {
|
|
71
74
|
if (this.answers[q.id] === 'skip')
|
|
72
|
-
return
|
|
75
|
+
return 'skip';
|
|
73
76
|
if (await q.check())
|
|
74
|
-
return
|
|
77
|
+
return 'ok';
|
|
75
78
|
console.log(chalk.yellow(` ${q.message}`));
|
|
76
79
|
if (q.url)
|
|
77
80
|
console.log(` ${chalk.cyan(q.url)}`);
|
|
@@ -82,9 +85,9 @@ class TtyPrompter {
|
|
|
82
85
|
while (Date.now() - started < timeout) {
|
|
83
86
|
await sleep(interval);
|
|
84
87
|
if (await q.check())
|
|
85
|
-
return
|
|
88
|
+
return 'ok';
|
|
86
89
|
}
|
|
87
|
-
return
|
|
90
|
+
return 'timeout';
|
|
88
91
|
}
|
|
89
92
|
progress(message, detail) {
|
|
90
93
|
// Only emit carriage-return / clear-line ANSI to a real TTY; piped to a file
|
|
@@ -132,6 +135,9 @@ class JsonPrompter {
|
|
|
132
135
|
console.error = origError;
|
|
133
136
|
};
|
|
134
137
|
}
|
|
138
|
+
hasPreset(id) {
|
|
139
|
+
return id in this.answers;
|
|
140
|
+
}
|
|
135
141
|
emit(event) {
|
|
136
142
|
process.stdout.write(JSON.stringify(event) + '\n');
|
|
137
143
|
}
|
|
@@ -184,15 +190,16 @@ class JsonPrompter {
|
|
|
184
190
|
}
|
|
185
191
|
async confirm(q) {
|
|
186
192
|
if (q.id in this.answers)
|
|
187
|
-
return
|
|
193
|
+
return toBool(this.answers[q.id]);
|
|
188
194
|
// A confirm is a single-choice question; emit OpenCode's `question` envelope.
|
|
189
195
|
// Reply: {requestID, answers: <bool>}; reject: {requestID, reject: true}.
|
|
190
196
|
this.emit({ type: 'question', requestID: q.id, kind: 'confirm', prompt: q.message, default: q.default ?? true });
|
|
191
197
|
const reply = await this.awaitReply(q.id);
|
|
198
|
+
// Reject = cancellation (distinct from "No"); parse the answer robustly so a
|
|
199
|
+
// string "false" isn't coerced truthy.
|
|
192
200
|
if (reply.reject)
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
return typeof a === 'boolean' ? a : Boolean(a);
|
|
201
|
+
throw new PromptCancelledError(q.id);
|
|
202
|
+
return toBool(reply.answers ?? reply.value);
|
|
196
203
|
}
|
|
197
204
|
async multiselect(q) {
|
|
198
205
|
if (q.id in this.answers)
|
|
@@ -212,9 +219,9 @@ class JsonPrompter {
|
|
|
212
219
|
}
|
|
213
220
|
async action(q) {
|
|
214
221
|
if (this.answers[q.id] === 'skip')
|
|
215
|
-
return
|
|
222
|
+
return 'skip';
|
|
216
223
|
if (await q.check())
|
|
217
|
-
return
|
|
224
|
+
return 'ok';
|
|
218
225
|
// An out-of-band action maps to OpenCode's `permission` request. Reply:
|
|
219
226
|
// {permissionID, response: "once"|"always"|"reject"}. We also keep polling
|
|
220
227
|
// check() so it resolves on its own once the action completes.
|
|
@@ -231,9 +238,9 @@ class JsonPrompter {
|
|
|
231
238
|
try {
|
|
232
239
|
while (Date.now() - started < timeout) {
|
|
233
240
|
if (await q.check())
|
|
234
|
-
return
|
|
241
|
+
return 'ok';
|
|
235
242
|
if (override === 'skip')
|
|
236
|
-
return
|
|
243
|
+
return 'skip';
|
|
237
244
|
// 'done' means the agent reports the action complete — but completion may
|
|
238
245
|
// be eventually-consistent, so keep polling check() at the interval until
|
|
239
246
|
// it confirms or we time out (matching TTY behavior), rather than failing
|
|
@@ -244,7 +251,7 @@ class JsonPrompter {
|
|
|
244
251
|
}
|
|
245
252
|
await Promise.race([sleep(interval), answerPromise]);
|
|
246
253
|
}
|
|
247
|
-
return
|
|
254
|
+
return 'timeout';
|
|
248
255
|
}
|
|
249
256
|
finally {
|
|
250
257
|
// If polling or the timeout won before any stdin reply arrived, drop the
|
|
@@ -275,6 +282,20 @@ export function createPrompter(options = {}) {
|
|
|
275
282
|
function sleep(ms) {
|
|
276
283
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
277
284
|
}
|
|
285
|
+
/** Coerce an answer to a boolean without the `Boolean("false") === true` trap:
|
|
286
|
+
* recognizes string forms ("true"/"false", "yes"/"no", "1"/"0") explicitly. */
|
|
287
|
+
function toBool(v) {
|
|
288
|
+
if (typeof v === 'boolean')
|
|
289
|
+
return v;
|
|
290
|
+
if (typeof v === 'string') {
|
|
291
|
+
const s = v.trim().toLowerCase();
|
|
292
|
+
if (['true', 'yes', 'y', '1'].includes(s))
|
|
293
|
+
return true;
|
|
294
|
+
if (['false', 'no', 'n', '0', ''].includes(s))
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
return Boolean(v);
|
|
298
|
+
}
|
|
278
299
|
/** Validate a pre-supplied multiselect answer is actually an array before use,
|
|
279
300
|
* so a bad --answers value (e.g. a string) fails fast with a clear message
|
|
280
301
|
* instead of crashing downstream array operations. */
|