@haystackeditor/cli 0.15.2 → 0.15.3

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.
@@ -19,7 +19,7 @@ import { resolveAuthContext } from '../utils/auth.js';
19
19
  import { hooksInstall } from './hooks.js';
20
20
  import { findGitRoot } from '../utils/hooks.js';
21
21
  import { trackError, trackSetupEvent } from '../utils/telemetry.js';
22
- import { createPrompter, loadAnswersFile } from '../utils/prompter.js';
22
+ import { createPrompter, loadAnswersFile, PromptCancelledError } from '../utils/prompter.js';
23
23
  /** The active prompter for this `setup` invocation (TTY or JSON). */
24
24
  let ui;
25
25
  /**
@@ -744,6 +744,13 @@ async function isAppInstalled(token) {
744
744
  // keeps waiting instead of crashing the wizard.
745
745
  if (err instanceof InstallationsAuthError)
746
746
  throw err;
747
+ // Transient (5xx / network / rate limit): log so it isn't a silent fallback
748
+ // (PR001), then return false so the install poll keeps waiting. Re-throwing
749
+ // here — a literal reading of "no silent fallback" — would reintroduce the
750
+ // poll-crash this function exists to prevent, so we log instead.
751
+ trackError('haystack_setup_app_check_transient_error', {
752
+ error_message: err instanceof Error ? err.message : String(err),
753
+ });
747
754
  return false;
748
755
  }
749
756
  }
@@ -1172,6 +1179,15 @@ export async function setupCommand(options = {}) {
1172
1179
  try {
1173
1180
  await runSetupFlow(options);
1174
1181
  }
1182
+ catch (err) {
1183
+ // An agent cancelling a prompt is a clean exit, not a crash.
1184
+ if (err instanceof PromptCancelledError) {
1185
+ console.log(chalk.yellow('\n Setup cancelled.\n'));
1186
+ ui.result({ status: 'cancelled' });
1187
+ return;
1188
+ }
1189
+ throw err;
1190
+ }
1175
1191
  finally {
1176
1192
  ui.close();
1177
1193
  }
@@ -25,6 +25,12 @@ export type Choice<T> = {
25
25
  } | {
26
26
  separator: string;
27
27
  };
28
+ /** Thrown when an agent explicitly rejects/cancels a prompt (vs. answering it),
29
+ * so callers can distinguish a cancellation from a valid empty answer. */
30
+ export declare class PromptCancelledError extends Error {
31
+ readonly requestID: string;
32
+ constructor(requestID: string);
33
+ }
28
34
  export interface Prompter {
29
35
  /** True for the human TTY prompter — gates side effects like opening a browser
30
36
  * that only make sense for a local interactive user. */
@@ -23,6 +23,16 @@ import { readFileSync } from 'node:fs';
23
23
  function isSeparator(c) {
24
24
  return c.separator !== undefined;
25
25
  }
26
+ /** Thrown when an agent explicitly rejects/cancels a prompt (vs. answering it),
27
+ * so callers can distinguish a cancellation from a valid empty answer. */
28
+ export class PromptCancelledError extends Error {
29
+ requestID;
30
+ constructor(requestID) {
31
+ super(`Prompt "${requestID}" was cancelled.`);
32
+ this.requestID = requestID;
33
+ this.name = 'PromptCancelledError';
34
+ }
35
+ }
26
36
  export function loadAnswersFile(path) {
27
37
  const raw = readFileSync(path, 'utf8');
28
38
  const parsed = JSON.parse(raw);
@@ -193,10 +203,12 @@ class JsonPrompter {
193
203
  // Reply: {requestID, answers: <value[]>}; reject: {requestID, reject: true}.
194
204
  this.emit({ type: 'question', requestID: q.id, kind: 'multiselect', prompt: q.message, options });
195
205
  const reply = await this.awaitReply(q.id);
206
+ // Surface a rejection as a distinct cancellation rather than masking it as an
207
+ // empty selection, and surface a non-array reply as a protocol error rather
208
+ // than silently coercing to [] (PR001: no silent fallback).
196
209
  if (reply.reject)
197
- return [];
198
- const a = reply.answers ?? reply.value;
199
- return Array.isArray(a) ? a : [];
210
+ throw new PromptCancelledError(q.id);
211
+ return asArray(reply.answers ?? reply.value, q.id);
200
212
  }
201
213
  async action(q) {
202
214
  if (this.answers[q.id] === 'skip')
@@ -216,22 +228,29 @@ class JsonPrompter {
216
228
  const r = (reply.response ?? reply.value);
217
229
  override = r === 'reject' || r === 'skip' ? 'skip' : 'done';
218
230
  });
219
- while (Date.now() - started < timeout) {
220
- if (await q.check())
221
- return true;
222
- if (override === 'skip')
223
- return false;
224
- // 'done' means the agent reports the action complete — but completion may
225
- // be eventually-consistent, so keep polling check() at the interval until
226
- // it confirms or we time out (matching TTY behavior), rather than failing
227
- // on a single immediate check.
228
- if (override === 'done') {
229
- await sleep(interval);
230
- continue;
231
+ try {
232
+ while (Date.now() - started < timeout) {
233
+ if (await q.check())
234
+ return true;
235
+ if (override === 'skip')
236
+ return false;
237
+ // 'done' means the agent reports the action complete but completion may
238
+ // be eventually-consistent, so keep polling check() at the interval until
239
+ // it confirms or we time out (matching TTY behavior), rather than failing
240
+ // on a single immediate check.
241
+ if (override === 'done') {
242
+ await sleep(interval);
243
+ continue;
244
+ }
245
+ await Promise.race([sleep(interval), answerPromise]);
231
246
  }
232
- await Promise.race([sleep(interval), answerPromise]);
247
+ return false;
248
+ }
249
+ finally {
250
+ // If polling or the timeout won before any stdin reply arrived, drop the
251
+ // dangling resolver so it doesn't accumulate across action() calls.
252
+ this.pending.delete(q.id);
233
253
  }
234
- return false;
235
254
  }
236
255
  progress(message, detail) {
237
256
  this.emit({ type: 'progress', message, ...(detail ? { detail } : {}) });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haystackeditor/cli",
3
- "version": "0.15.2",
3
+ "version": "0.15.3",
4
4
  "description": "Set up Haystack for your project — automated PR review, triage, and merge queue",
5
5
  "type": "module",
6
6
  "bin": {