@algolia/wizard 0.120.0 → 0.122.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 (2) hide show
  1. package/dist/main.js +117 -22
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -325,6 +325,7 @@ var useWizard = create((set, get) => ({
325
325
  logs: [],
326
326
  _outputLogCount: 0,
327
327
  error: null,
328
+ exitRequested: false,
328
329
  inputReq: null,
329
330
  _resolve: null,
330
331
  settingUpAppId: null,
@@ -488,7 +489,15 @@ var useWizard = create((set, get) => ({
488
489
  get().logEnd(id, "success");
489
490
  },
490
491
  setDone: () => set({ phase: "done", loadingMessage: null }),
491
- setError: (message) => set({ phase: "error", error: message, loadingMessage: null }),
492
+ setError: (message, options) => {
493
+ if (options?.exit) get()._clearNoticeQueue();
494
+ set({
495
+ phase: "error",
496
+ error: message,
497
+ loadingMessage: null,
498
+ exitRequested: options?.exit ?? false
499
+ });
500
+ },
492
501
  reset: () => {
493
502
  get()._clearNoticeQueue();
494
503
  get()._resolve?.(false);
@@ -508,6 +517,7 @@ var useWizard = create((set, get) => ({
508
517
  logs: [],
509
518
  _outputLogCount: 0,
510
519
  error: null,
520
+ exitRequested: false,
511
521
  inputReq: null,
512
522
  _resolve: null,
513
523
  settingUpAppId: null,
@@ -1460,12 +1470,13 @@ function EnterToContinuePrompt({
1460
1470
  ] });
1461
1471
  }
1462
1472
  function PromptInput() {
1463
- const { phase, inputReq, submitInput } = useWizard();
1473
+ const { phase, inputReq, submitInput, exitRequested } = useWizard();
1464
1474
  const [draft, setDraft] = useState9("");
1465
1475
  const [submittedReq, setSubmittedReq] = useState9(null);
1466
1476
  const promptKey = inputReq && `${inputReq.promptType}:${inputReq.prompt}`;
1467
1477
  const submitting = inputReq != null && submittedReq === inputReq;
1468
1478
  if (phase === "done" || phase === "error") {
1479
+ if (exitRequested) return null;
1469
1480
  return /* @__PURE__ */ jsx10(Box11, { marginTop: 1, children: /* @__PURE__ */ jsx10(Text12, { color: "gray", dimColor: true, children: "Press Enter or Esc to exit" }) });
1470
1481
  }
1471
1482
  if (phase !== "awaitingInput" || !inputReq) return null;
@@ -2544,10 +2555,24 @@ async function ensureApplication(resumeAppId) {
2544
2555
  }
2545
2556
  }
2546
2557
 
2558
+ // src/lib/rateLimit.ts
2559
+ var RateLimitError = class extends Error {
2560
+ constructor(retryAfterSeconds, options) {
2561
+ super(rateLimitMessage(retryAfterSeconds), options);
2562
+ this.retryAfterSeconds = retryAfterSeconds;
2563
+ this.name = "RateLimitError";
2564
+ }
2565
+ retryAfterSeconds;
2566
+ };
2567
+ function rateLimitMessage(retryAfterSeconds) {
2568
+ const when = retryAfterSeconds === void 0 ? "in a moment" : `in ${retryAfterSeconds} ${retryAfterSeconds === 1 ? "second" : "seconds"}`;
2569
+ return `You've hit the rate limit. Try again ${when}.`;
2570
+ }
2571
+
2547
2572
  // package.json
2548
2573
  var package_default = {
2549
2574
  name: "@algolia/wizard",
2550
- version: "0.120.0",
2575
+ version: "0.122.0",
2551
2576
  description: "Magically implement Algolia functionality in your codebase",
2552
2577
  type: "module",
2553
2578
  engines: {
@@ -2638,6 +2663,16 @@ var RestartWorkflowSignal = class extends Error {
2638
2663
  }
2639
2664
  appId;
2640
2665
  };
2666
+ var ExitWizardSignal = class extends Error {
2667
+ constructor(original) {
2668
+ super(original instanceof Error ? original.message : String(original), {
2669
+ cause: original
2670
+ });
2671
+ this.original = original;
2672
+ this.name = "ExitWizardSignal";
2673
+ }
2674
+ original;
2675
+ };
2641
2676
  function ensureExecutedStepCount(state) {
2642
2677
  if (state.executedStepCount == null) {
2643
2678
  state.executedStepCount = state.steps.filter(
@@ -2828,6 +2863,29 @@ async function rewindTo(state, targetIndex, fromIndex) {
2828
2863
  store.syncSteps([...state.steps], targetIndex);
2829
2864
  await persistState(state);
2830
2865
  }
2866
+ var RATE_LIMIT_RETRY_OPTION = "Retry";
2867
+ var RATE_LIMIT_EXIT_OPTION = "Exit";
2868
+ async function runStepWithRateLimitRecovery(state, step, ctx) {
2869
+ for (; ; ) {
2870
+ try {
2871
+ return await step.run(ctx);
2872
+ } catch (err) {
2873
+ if (!(err instanceof RateLimitError)) throw err;
2874
+ const choice = await useWizard.getState().requestUserInput({
2875
+ prompt: "Retry the step, or exit the wizard?",
2876
+ promptType: "multipleChoice",
2877
+ options: [RATE_LIMIT_RETRY_OPTION, RATE_LIMIT_EXIT_OPTION],
2878
+ alert: {
2879
+ variant: "error",
2880
+ title: "Too many requests",
2881
+ message: err.message
2882
+ }
2883
+ });
2884
+ if (choice !== RATE_LIMIT_RETRY_OPTION) throw new ExitWizardSignal(err);
2885
+ ctx = await makeContext(state, true);
2886
+ }
2887
+ }
2888
+ }
2831
2889
  async function runStep(state, index, step) {
2832
2890
  const store = useWizard.getState();
2833
2891
  const record = state.steps[index];
@@ -2854,7 +2912,7 @@ async function runStep(state, index, step) {
2854
2912
  const stopSavingCost = saveCostDuringStep(state);
2855
2913
  let output;
2856
2914
  try {
2857
- const raw = await step.run(ctx);
2915
+ const raw = await runStepWithRateLimitRecovery(state, step, ctx);
2858
2916
  output = step.outputSchema.parse(raw);
2859
2917
  await step.onStepComplete?.(output, ctx);
2860
2918
  } catch (err) {
@@ -3014,7 +3072,9 @@ async function runWorkflow(workflow, startWorkflowAppId, options) {
3014
3072
  await recordWorkflowRun(workflow.id, nowIso());
3015
3073
  await clearWorkflowState(workflow.id);
3016
3074
  store.setDone();
3017
- } catch (err) {
3075
+ } catch (thrown) {
3076
+ const exitRequested = thrown instanceof ExitWizardSignal;
3077
+ const err = exitRequested ? thrown.original : thrown;
3018
3078
  const message = err instanceof Error ? err.message : String(err);
3019
3079
  const failed = await loadWorkflowState(workflow.id);
3020
3080
  const failedAppId = failed ? runAppId(failed) : startWorkflowAppId;
@@ -3063,7 +3123,7 @@ async function runWorkflow(workflow, startWorkflowAppId, options) {
3063
3123
  ...errorType ? { error_type: errorType } : {},
3064
3124
  ...errorCode2 ? { error_code: errorCode2 } : {}
3065
3125
  });
3066
- store.setError(message);
3126
+ store.setError(message, { exit: exitRequested });
3067
3127
  }
3068
3128
  }
3069
3129
 
@@ -5559,6 +5619,7 @@ var GLOBAL_BUDGET_USER_MESSAGE = "The wizard reached its daily AI usage limit fo
5559
5619
  var PROXY_AUTH_REJECTED_HEADER2 = "x-wizard-proxy-auth";
5560
5620
  var PROXY_REJECTION_BODY = "You shall not pass!";
5561
5621
  var BUDGET_EXCEEDED_HEADER = "x-wizard-budget-exceeded";
5622
+ var RETRY_AFTER_HEADER = "x-wizard-retry-after";
5562
5623
  function lastCallError(err) {
5563
5624
  const cause = RetryError.isInstance(err) ? err.lastError : err;
5564
5625
  return APICallError.isInstance(cause) ? cause : void 0;
@@ -5574,6 +5635,17 @@ function budgetExceededMessage(err) {
5574
5635
  if (scope === "global") return GLOBAL_BUDGET_USER_MESSAGE;
5575
5636
  return void 0;
5576
5637
  }
5638
+ function rateLimitError(err) {
5639
+ const cause = lastCallError(err);
5640
+ if (cause?.statusCode !== 429) return void 0;
5641
+ const headers = cause.responseHeaders;
5642
+ const seconds = Number(
5643
+ headers?.[RETRY_AFTER_HEADER] ?? headers?.["retry-after"]
5644
+ );
5645
+ return new RateLimitError(seconds > 0 ? Math.ceil(seconds) : void 0, {
5646
+ cause: err
5647
+ });
5648
+ }
5577
5649
  var INCOMPLETE_CAUSES = {
5578
5650
  stepLimit: {
5579
5651
  kind: "missingReportAtStepLimit",
@@ -5612,11 +5684,19 @@ function incompleteCauseOf(stepCount, maxSteps, finishReason) {
5612
5684
  if (finishReason !== "stop") return "stopped";
5613
5685
  return null;
5614
5686
  }
5687
+ var NO_FRONTEND_REASON = "no_frontend";
5688
+ var NO_FRONTEND_MESSAGE = "No frontend found in this folder. `cd` into the directory where your frontend lives (in a monorepo, the app folder, usually the one with that app's `package.json`), then rerun the Wizard.";
5689
+ function userFacingAgentFailure(reason) {
5690
+ if (reason?.trim().toLowerCase() === NO_FRONTEND_REASON) {
5691
+ return NO_FRONTEND_MESSAGE;
5692
+ }
5693
+ return `Agent reported failure: ${reason ?? "no reason given"}`;
5694
+ }
5615
5695
  var AgentReportedFailureError = class extends Error {
5616
5696
  reason;
5617
5697
  output;
5618
5698
  constructor(reason, output) {
5619
- super(`Agent reported failure: ${reason ?? "no reason given"}`);
5699
+ super(userFacingAgentFailure(reason));
5620
5700
  this.reason = reason;
5621
5701
  this.output = output;
5622
5702
  }
@@ -5799,7 +5879,7 @@ async function runAgentAttempt(req, attempt, retryReason, continuation) {
5799
5879
  ...continuation.messages,
5800
5880
  { role: "user", content: ENDED_TURN_NUDGE }
5801
5881
  ] : [{ role: "user", content: INITIAL_PROMPT }];
5802
- const stream = await agent.stream({ prompt });
5882
+ const stream = await agent.stream({ prompt, abortSignal: req.abortSignal });
5803
5883
  let chunks = [];
5804
5884
  const chunkLimit = 5;
5805
5885
  let streamError;
@@ -5820,6 +5900,10 @@ async function runAgentAttempt(req, attempt, retryReason, continuation) {
5820
5900
  if (chunks.length) {
5821
5901
  logger.debug(chunks.join(""));
5822
5902
  }
5903
+ if (req.abortSignal?.aborted) {
5904
+ logger.info({ operation: req.operation }, "runAgent aborted");
5905
+ req.abortSignal.throwIfAborted();
5906
+ }
5823
5907
  if (streamError !== void 0) {
5824
5908
  logger.error({ err: streamError }, "agent stream reported an error");
5825
5909
  if (isProxyInactivityRejection(streamError)) {
@@ -5827,7 +5911,7 @@ async function runAgentAttempt(req, attempt, retryReason, continuation) {
5827
5911
  }
5828
5912
  const budgetMessage = budgetExceededMessage(streamError);
5829
5913
  if (budgetMessage) throw new Error(budgetMessage, { cause: streamError });
5830
- throw streamError;
5914
+ throw rateLimitError(streamError) ?? streamError;
5831
5915
  }
5832
5916
  const end = Date.now();
5833
5917
  const usage = await stream.totalUsage;
@@ -5960,9 +6044,10 @@ function prioritizeFrontendFrameworks(result) {
5960
6044
  );
5961
6045
  return { ...result, frameworks: [...frontend, ...remaining] };
5962
6046
  }
5963
- var detectLanguage = async () => {
6047
+ var detectLanguage = async (abortSignal) => {
5964
6048
  const result = await runAgent({
5965
6049
  operation: "language-detection",
6050
+ abortSignal,
5966
6051
  instructions: [
5967
6052
  "Analyze the codebase and determine the programming languages and frameworks used",
5968
6053
  "If a superset language is found, exclude the subset language (e.g. TypeScript over JavaScript).",
@@ -6058,20 +6143,21 @@ var MODE_CONFIG = {
6058
6143
  outputSchema: verificationSchema
6059
6144
  }
6060
6145
  };
6061
- function runMode(mode, extraInstructions = []) {
6146
+ function runMode(mode, extraInstructions = [], abortSignal) {
6062
6147
  const { instructions, outputSchema } = MODE_CONFIG[mode];
6063
6148
  return runAgent({
6064
6149
  operation: `code-analysis-${mode}`,
6065
6150
  instructions: [...instructions, ...extraInstructions],
6066
6151
  tools: READONLY_TOOLS,
6067
6152
  outputSchema,
6068
- modelProfile: "analysis" /* analysis */
6153
+ modelProfile: "analysis" /* analysis */,
6154
+ abortSignal
6069
6155
  });
6070
6156
  }
6071
- async function runAnalysis(mode, extraInstructions = []) {
6157
+ async function runAnalysis(mode, extraInstructions = [], abortSignal) {
6072
6158
  const modes = mode ? [mode] : ["ingestion", "searchImplementation", "verification"];
6073
6159
  const results = await Promise.all(
6074
- modes.map((m) => runMode(m, extraInstructions))
6160
+ modes.map((m) => runMode(m, extraInstructions, abortSignal))
6075
6161
  );
6076
6162
  return Object.assign({}, ...results);
6077
6163
  }
@@ -6085,9 +6171,14 @@ var projectScanSchema = detectLanguageSchema.extend({
6085
6171
  });
6086
6172
  async function projectScan(ctx) {
6087
6173
  const startedAt = Date.now();
6174
+ const controller = new AbortController();
6175
+ const abortOthersOnFailure = (run2) => run2.catch((err) => {
6176
+ controller.abort();
6177
+ throw err;
6178
+ });
6088
6179
  const [detected, analyses] = await Promise.all([
6089
- detectLanguage(),
6090
- runAnalysis()
6180
+ abortOthersOnFailure(detectLanguage(controller.signal)),
6181
+ abortOthersOnFailure(runAnalysis(void 0, [], controller.signal))
6091
6182
  ]);
6092
6183
  const entities = toEntitySummary(analyses.ingestionAnalysis ?? []);
6093
6184
  track("AI Wizard Scan Completed", {
@@ -6540,7 +6631,7 @@ async function assertGitRepoWithHead(repoRoot) {
6540
6631
  await git(["-C", repoRoot, "rev-parse", "HEAD"]);
6541
6632
  } catch {
6542
6633
  throw new Error(
6543
- "implement requires a git repository with at least one commit. Initialize git and commit before running the wizard."
6634
+ "This folder must be a Git repository with at least one commit so you can review and revert Wizard changes. Run `git init`, commit your project files, then rerun."
6544
6635
  );
6545
6636
  }
6546
6637
  }
@@ -7341,7 +7432,7 @@ function searchInstructions(input) {
7341
7432
  "Match the application styles closely, including light, dark, and high-contrast themes the project already supports.",
7342
7433
  "Give the search input frame the same height, corner radius, border color and width, background, and focus style as the input it replaces. When it replaces nothing, copy them from the form controls beside it. Read these values from their source (the theme, the component props, or the CSS). Do not keep the widget theme defaults, for example a 44px height and 4px corners, and do not invent colors. When the app theme exists only in code, take the values from that theme object.",
7343
7434
  "When you add UI text to a localized dictionary or message file, add the same keys, translated, to every locale the project supports.",
7344
- "Before reporting, mount the component. Do not run a type check, build, or test command unless a later instruction allows it: the validation pass runs the type check, after the developer agrees to it. If a tool limit is reached or an error remains unresolved, keep the implementation, report status=success, and name the remaining issue in the summary so validation can drive the next pass. Report status=fail only when no search implementation can be written, with the concrete blocker as the reason.",
7435
+ `Before reporting, mount the component. Do not run a type check, build, or test command unless a later instruction allows it: the validation pass runs the type check, after the developer agrees to it. If a tool limit is reached or an error remains unresolved, keep the implementation, report status=success, and name the remaining issue in the summary so validation can drive the next pass. Report status=fail only when no search implementation can be written. When this folder has no frontend to build search UI in (for example a monorepo root or a backend-only package), use reason exactly \`${NO_FRONTEND_REASON}\`. For any other blocker, use a short concrete reason.`,
7345
7436
  "The summary should be extremely concise; do not mention manual testing steps.",
7346
7437
  ...input.uiTemplate ? [
7347
7438
  "For the playbook recap, report uiFiles as every file you created or edited for this UI. Each entry needs a short action label and its repo-relative path. Report devCommand only when you found the project\u2019s existing command for starting the app locally. Do not invent a command, host, or port."
@@ -10501,7 +10592,7 @@ function Ribbon() {
10501
10592
  }
10502
10593
 
10503
10594
  // src/ui/App.tsx
10504
- import { useState as useState14 } from "react";
10595
+ import { useEffect as useEffect5, useState as useState14 } from "react";
10505
10596
 
10506
10597
  // src/ui/Logs.tsx
10507
10598
  import { Box as Box21, Text as Text22, useInput as useInput9 } from "ink";
@@ -11135,9 +11226,13 @@ function App() {
11135
11226
  workflow,
11136
11227
  settingUpAppId,
11137
11228
  loadingMessage,
11138
- review: review2
11229
+ review: review2,
11230
+ exitRequested
11139
11231
  } = useWizard();
11140
11232
  const { exit } = useApp2();
11233
+ useEffect5(() => {
11234
+ if (exitRequested) exit();
11235
+ }, [exitRequested, exit]);
11141
11236
  const { columns, rows } = useWindowSize8();
11142
11237
  useRepaintOnResize();
11143
11238
  const compactRun = isCompactTerminal(columns, rows);
@@ -11158,7 +11253,7 @@ function App() {
11158
11253
  const holdForTip = !compactRun && tipsActive && isAwaitingUserInput && tipState === "typing";
11159
11254
  const showTips = !compactRun && tipsActive && (phase === "running" && currentStep?.status === "running" && !loadingMessage && !settingUpAppId || holdForTip);
11160
11255
  const showNoticesInMain = !tipsActive;
11161
- const showNotices = !compactRun && (!isAwaitingUserInput || holdForTip) && !review2;
11256
+ const showNotices = !compactRun && (!isAwaitingUserInput || holdForTip) && !review2 && !exitRequested;
11162
11257
  useInput10(
11163
11258
  (_input, key) => {
11164
11259
  if (key.return) {
@@ -11572,7 +11667,7 @@ async function promptResumeOrStartNew(session, next) {
11572
11667
  // package.json with { type: 'json' }
11573
11668
  var package_default2 = {
11574
11669
  name: "@algolia/wizard",
11575
- version: "0.120.0",
11670
+ version: "0.122.0",
11576
11671
  description: "Magically implement Algolia functionality in your codebase",
11577
11672
  type: "module",
11578
11673
  engines: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@algolia/wizard",
3
- "version": "0.120.0",
3
+ "version": "0.122.0",
4
4
  "description": "Magically implement Algolia functionality in your codebase",
5
5
  "type": "module",
6
6
  "engines": {