@algolia/wizard 0.34.0-rc.125.245 → 0.35.0-rc.125.251

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 +1597 -1339
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -253,6 +253,7 @@ var useWizard = create((set, get) => ({
253
253
  cliOutput: [],
254
254
  targetIndex: null,
255
255
  writtenFiles: [],
256
+ approvedCommands: /* @__PURE__ */ new Set(),
256
257
  logs: [],
257
258
  error: null,
258
259
  inputReq: null,
@@ -348,6 +349,10 @@ var useWizard = create((set, get) => ({
348
349
  setTargetIndex: (index) => set({ targetIndex: index }),
349
350
  recordWrittenFile: (path) => set((s) => ({ writtenFiles: [...s.writtenFiles, path] })),
350
351
  clearWrittenFiles: () => set({ writtenFiles: [] }),
352
+ isCommandApproved: (command, cwd) => get().approvedCommands.has(`${cwd}\0${command}`),
353
+ approveCommand: (command, cwd) => set((s) => ({
354
+ approvedCommands: new Set(s.approvedCommands).add(`${cwd}\0${command}`)
355
+ })),
351
356
  logStart: (kind, name, input) => {
352
357
  const id = nanoid();
353
358
  set((s) => ({
@@ -396,6 +401,7 @@ var useWizard = create((set, get) => ({
396
401
  cliOutput: [],
397
402
  targetIndex: null,
398
403
  writtenFiles: [],
404
+ approvedCommands: /* @__PURE__ */ new Set(),
399
405
  logs: [],
400
406
  error: null,
401
407
  inputReq: null,
@@ -1121,576 +1127,138 @@ function PromptInput() {
1121
1127
  ] });
1122
1128
  }
1123
1129
 
1124
- // src/ui/Welcome.tsx
1125
- import { dirname as dirname2, join as join3 } from "node:path";
1126
- import { fileURLToPath } from "node:url";
1127
- import { useState as useState7 } from "react";
1128
- import { Box as Box10, Spacer, Text as Text10, useInput as useInput3, useWindowSize as useWindowSize5 } from "ink";
1130
+ // src/workflows/default.ts
1131
+ import { z as z29 } from "zod";
1129
1132
 
1130
- // src/ui/copy/welcome.ts
1131
- var sidebarItems = [
1132
- {
1133
- title: "scan your project",
1134
- description: "detect models, schemas, and data worth indexing"
1135
- },
1136
- {
1137
- title: "select and index",
1138
- description: "push 100 records to Algolia in seconds"
1139
- },
1140
- {
1141
- title: "detect your stack",
1142
- description: "whatever language and framework you already use"
1143
- },
1144
- {
1145
- title: "scaffold a search UI",
1146
- description: "a search box and results, wired into your app"
1147
- },
1148
- {
1149
- title: "ship it",
1150
- description: "build check passes, search is live with your real data"
1151
- }
1152
- ];
1133
+ // src/core/orchestrator.ts
1134
+ import "zod";
1153
1135
 
1154
- // src/ui/Welcome.tsx
1155
- import Image, { TerminalInfoContext, defaultTerminalInfo } from "ink-picture";
1156
- import { jsx as jsx8, jsxs as jsxs9 } from "react/jsx-runtime";
1157
- var IMAGE_PATH = join3(dirname2(fileURLToPath(import.meta.url)), "algolia.png");
1158
- var TERMINAL_INFO = {
1159
- ...defaultTerminalInfo,
1160
- supportsUnicode: true,
1161
- supportsColor: true
1162
- };
1163
- function SidebarItem({
1164
- title,
1165
- description
1166
- }) {
1167
- return /* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", children: [
1168
- /* @__PURE__ */ jsxs9(Box10, { gap: 1, children: [
1169
- /* @__PURE__ */ jsx8(Text10, { color: COLORS.success, children: "\u2192" }),
1170
- /* @__PURE__ */ jsx8(Text10, { color: COLORS.strong, bold: true, children: title })
1171
- ] }),
1172
- /* @__PURE__ */ jsxs9(Box10, { flexDirection: "row", gap: 2, children: [
1173
- /* @__PURE__ */ jsx8(Spacer, {}),
1174
- /* @__PURE__ */ jsx8(Text10, { color: COLORS.muted, children: description })
1175
- ] })
1176
- ] });
1177
- }
1178
- function Welcome() {
1179
- const confirmStart = useWizard((s) => s.confirmStart);
1180
- const openLearnMore = useWizard((s) => s.openLearnMore);
1181
- const { rows } = useWindowSize5();
1182
- const actions = [
1183
- { label: "start wizard", run: confirmStart },
1184
- { label: "learn more", run: openLearnMore }
1185
- ];
1186
- const [index, setIndex] = useState7(0);
1187
- useInput3((input, key) => {
1188
- if (key.upArrow || input === "k") {
1189
- setIndex((i) => (i - 1 + actions.length) % actions.length);
1190
- } else if (key.downArrow || input === "j") {
1191
- setIndex((i) => (i + 1) % actions.length);
1192
- } else if (key.return) {
1193
- actions[index].run();
1194
- }
1195
- });
1196
- const scales = {
1197
- large: {
1198
- sidebar: { padding: { x: 4, y: 2 }, gap: 2 },
1199
- main: { padding: { x: 8, y: 4 } }
1200
- },
1201
- small: {
1202
- sidebar: { padding: { x: 2, y: 1 }, gap: 1 },
1203
- main: { padding: { x: 4, y: 2 } }
1204
- }
1205
- };
1206
- let layout = scales["large"];
1207
- if (rows < 30) {
1208
- layout = scales["small"];
1136
+ // src/core/config.ts
1137
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
1138
+ import { join as join3 } from "node:path";
1139
+ var configFile = () => join3(stateDir(), "config.json");
1140
+ var defaultConfig = () => ({
1141
+ version: 1,
1142
+ aiConsent: false,
1143
+ workflowsRun: []
1144
+ });
1145
+ async function loadConfig() {
1146
+ try {
1147
+ const raw = await readFile(configFile(), "utf8");
1148
+ return { ...defaultConfig(), ...JSON.parse(raw) };
1149
+ } catch {
1150
+ return defaultConfig();
1209
1151
  }
1210
- return /* @__PURE__ */ jsxs9(Box10, { flexDirection: "row", justifyContent: "space-between", width: "100%", children: [
1211
- /* @__PURE__ */ jsx8(
1212
- Box10,
1213
- {
1214
- paddingY: layout.main.padding.y,
1215
- paddingX: layout.main.padding.x,
1216
- flexDirection: "column",
1217
- justifyContent: "center",
1218
- children: /* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", gap: 2, children: [
1219
- /* @__PURE__ */ jsx8(TerminalInfoContext.Provider, { value: TERMINAL_INFO, children: /* @__PURE__ */ jsx8(
1220
- Image,
1221
- {
1222
- src: IMAGE_PATH,
1223
- objectFit: "contain",
1224
- alt: "Algolia",
1225
- width: 20,
1226
- height: 10,
1227
- protocol: "halfBlock"
1228
- }
1229
- ) }),
1230
- /* @__PURE__ */ jsx8(Text10, { color: COLORS.muted, children: "\u2726 From zero \u2192 working search in ~10 minutes" }),
1231
- /* @__PURE__ */ jsx8(Box10, { flexDirection: "column", children: actions.map((action, i) => /* @__PURE__ */ jsx8(
1232
- SelectRow,
1233
- {
1234
- highlighted: i === index,
1235
- highlightBackground: false,
1236
- label: action.label
1237
- },
1238
- action.label
1239
- )) }),
1240
- /* @__PURE__ */ jsxs9(Box10, { gap: 2, children: [
1241
- /* @__PURE__ */ jsxs9(Text10, { children: [
1242
- /* @__PURE__ */ jsx8(Text10, { color: COLORS.primary, children: "[\u2191] [\u2193]" }),
1243
- /* @__PURE__ */ jsx8(Text10, { color: COLORS.dim, children: " move" })
1244
- ] }),
1245
- /* @__PURE__ */ jsxs9(Text10, { children: [
1246
- /* @__PURE__ */ jsx8(Text10, { color: COLORS.primary, children: "[enter]" }),
1247
- /* @__PURE__ */ jsx8(Text10, { color: COLORS.dim, children: " confirm" })
1248
- ] })
1249
- ] })
1250
- ] })
1251
- }
1252
- ),
1253
- /* @__PURE__ */ jsxs9(
1254
- Box10,
1255
- {
1256
- backgroundColor: COLORS.bg.sidebar,
1257
- width: 40,
1258
- paddingY: layout.sidebar.padding.y,
1259
- paddingX: layout.sidebar.padding.x,
1260
- gap: layout.sidebar.gap,
1261
- flexDirection: "column",
1262
- justifyContent: "center",
1263
- children: [
1264
- /* @__PURE__ */ jsx8(Text10, { color: COLORS.muted, children: "WHAT THIS WIZARD WILL DO" }),
1265
- sidebarItems.map((i, idx) => /* @__PURE__ */ jsx8(SidebarItem, { title: i.title, description: i.description }, idx))
1266
- ]
1267
- }
1268
- )
1269
- ] });
1152
+ }
1153
+ async function saveConfig(config) {
1154
+ await mkdir(stateDir(), { recursive: true });
1155
+ await writeFile(configFile(), JSON.stringify(config, null, 2), "utf8");
1156
+ }
1157
+ async function recordWorkflowRun(workflowId, completedAt) {
1158
+ const config = await loadConfig();
1159
+ config.workflowsRun.push({ workflowId, completedAt });
1160
+ await saveConfig(config);
1270
1161
  }
1271
1162
 
1272
- // src/ui/LearnMore.tsx
1273
- import { Fragment } from "react";
1274
- import { Box as Box11, Text as Text11, useInput as useInput4, useWindowSize as useWindowSize6 } from "ink";
1275
-
1276
- // src/ui/copy/learn-more.ts
1277
- var accessIntro = "Everything runs locally on your machine. Nothing is written or sent without an explicit yes from you.";
1278
- var accessItems = [
1279
- {
1280
- tag: "READ",
1281
- title: "Project files",
1282
- description: "reads manifests, configs & source to detect your stack. Read-only; nothing is uploaded."
1283
- },
1284
- {
1285
- tag: "WRITE",
1286
- title: "Code changes",
1287
- description: "creates & edits files (search UI, config) directly in your branch."
1288
- },
1289
- {
1290
- tag: "EXEC",
1291
- title: "Setup commands",
1292
- description: "runs dependency installs, the ingestion script & your own checks. Every command is shown in full and needs your OK; its output is shown as-is, so a command that prints a secret will display it."
1293
- },
1294
- {
1295
- tag: "NET",
1296
- title: "Algolia API",
1297
- description: "sends index settings & the records you pick to your Algolia app over HTTPS."
1298
- },
1299
- {
1300
- tag: "KEY",
1301
- title: "Credentials",
1302
- description: "writes your Algolia app id and a search-only key (safe to expose) to .env in your project."
1163
+ // src/core/persistence.ts
1164
+ import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2, rm } from "node:fs/promises";
1165
+ import { join as join4 } from "node:path";
1166
+ var isStepVisible = (s) => s.visible !== false;
1167
+ var stateFile = (workflowId) => join4(stateDir(), `state-${workflowId}.json`);
1168
+ async function loadWorkflowState(workflowId) {
1169
+ try {
1170
+ const raw = await readFile2(stateFile(workflowId), "utf8");
1171
+ return JSON.parse(raw);
1172
+ } catch {
1173
+ return null;
1303
1174
  }
1304
- ];
1305
- var neverItems = [
1306
- "Send your source code to a model or third party",
1307
- "Commit or push to git",
1308
- "Run a command you haven't approved"
1309
- ];
1310
- var policyLinks = [
1311
- { label: "Terms", url: "https://www.algolia.com/policies/terms" },
1312
- { label: "Privacy Policy", url: "https://www.algolia.com/policies/privacy" }
1313
- ];
1175
+ }
1176
+ async function saveWorkflowState(state) {
1177
+ await mkdir2(stateDir(), { recursive: true });
1178
+ await writeFile2(
1179
+ stateFile(state.workflowId),
1180
+ JSON.stringify(state, null, 2),
1181
+ "utf8"
1182
+ );
1183
+ }
1184
+ async function clearWorkflowState(workflowId) {
1185
+ await rm(stateFile(workflowId), { force: true });
1186
+ }
1314
1187
 
1315
- // src/ui/LearnMore.tsx
1316
- import { jsx as jsx9, jsxs as jsxs10 } from "react/jsx-runtime";
1317
- var TAG_COLORS = {
1318
- READ: COLORS.success,
1319
- WRITE: COLORS.badge,
1320
- EXEC: COLORS.danger,
1321
- NET: COLORS.accent,
1322
- KEY: COLORS.muted
1323
- };
1324
- var TAG_COLUMN_WIDTH = 10;
1325
- var PADDING_X = 6;
1326
- var NEVER_BOX_PAD_X = 2;
1327
- function NeverLine({
1328
- width,
1329
- segments = []
1330
- }) {
1331
- const used = segments.reduce((n, s) => n + s.text.length, 0);
1332
- const rightPad = Math.max(0, width - 2 - NEVER_BOX_PAD_X - used);
1333
- return /* @__PURE__ */ jsxs10(Text11, { color: COLORS.primary, children: [
1334
- /* @__PURE__ */ jsx9(Text11, { color: COLORS.danger, children: "\u2502" }),
1335
- " ".repeat(NEVER_BOX_PAD_X),
1336
- segments.map((s, i) => /* @__PURE__ */ jsx9(Text11, { color: s.color, bold: s.bold, children: s.text }, i)),
1337
- " ".repeat(rightPad),
1338
- /* @__PURE__ */ jsx9(Text11, { color: COLORS.danger, children: "\u2502" })
1339
- ] });
1188
+ // src/lib/telemetry.ts
1189
+ function isTelemetryOptedOut() {
1190
+ return process.env.WIZARD_TELEMETRY === "false";
1340
1191
  }
1341
- function LearnMore() {
1342
- const confirmStart = useWizard((s) => s.confirmStart);
1343
- const backToHome = useWizard((s) => s.backToHome);
1344
- const { columns } = useWindowSize6();
1345
- const dividerWidth = Math.max(0, columns - PADDING_X * 2);
1346
- useInput4((_input, key) => {
1347
- if (key.escape) backToHome();
1348
- else if (key.return) confirmStart();
1349
- });
1350
- return /* @__PURE__ */ jsxs10(
1351
- Box11,
1352
- {
1353
- flexDirection: "column",
1354
- paddingX: PADDING_X,
1355
- paddingY: 2,
1356
- width: "100%",
1357
- gap: 1,
1358
- children: [
1359
- /* @__PURE__ */ jsx9(Text11, { color: COLORS.strong, bold: true, children: "What algolia wizard accesses" }),
1360
- /* @__PURE__ */ jsx9(Text11, { color: COLORS.muted, children: accessIntro }),
1361
- /* @__PURE__ */ jsx9(Box11, { flexDirection: "column", children: accessItems.map((item) => /* @__PURE__ */ jsxs10(Box11, { flexDirection: "column", marginTop: 1, children: [
1362
- /* @__PURE__ */ jsx9(Text11, { color: COLORS.border, children: "\u2500".repeat(dividerWidth) }),
1363
- /* @__PURE__ */ jsxs10(Box11, { flexDirection: "row", gap: 1, marginTop: 1, children: [
1364
- /* @__PURE__ */ jsx9(Box11, { width: TAG_COLUMN_WIDTH, flexShrink: 0, children: /* @__PURE__ */ jsx9(Text11, { color: TAG_COLORS[item.tag], bold: true, children: `[${item.tag}]` }) }),
1365
- /* @__PURE__ */ jsx9(Box11, { flexDirection: "column", children: /* @__PURE__ */ jsxs10(Text11, { color: COLORS.primary, children: [
1366
- /* @__PURE__ */ jsx9(Text11, { color: COLORS.strong, bold: true, children: item.title }),
1367
- /* @__PURE__ */ jsx9(Text11, { color: COLORS.muted, children: ` \u2014 ${item.description}` })
1368
- ] }) })
1369
- ] })
1370
- ] }, item.tag)) }),
1371
- /* @__PURE__ */ jsxs10(Box11, { marginTop: 1, flexDirection: "column", children: [
1372
- /* @__PURE__ */ jsx9(Text11, { color: COLORS.danger, children: `\u256D${"\u2500".repeat(Math.max(0, dividerWidth - 2))}\u256E` }),
1373
- /* @__PURE__ */ jsx9(NeverLine, { width: dividerWidth }),
1374
- /* @__PURE__ */ jsx9(
1375
- NeverLine,
1376
- {
1377
- width: dividerWidth,
1378
- segments: [{ text: "I NEVER", color: COLORS.danger, bold: true }]
1379
- }
1380
- ),
1381
- neverItems.map((item) => /* @__PURE__ */ jsxs10(Fragment, { children: [
1382
- /* @__PURE__ */ jsx9(NeverLine, { width: dividerWidth }),
1383
- /* @__PURE__ */ jsx9(
1384
- NeverLine,
1385
- {
1386
- width: dividerWidth,
1387
- segments: [
1388
- { text: "\u2715", color: COLORS.danger },
1389
- { text: " " },
1390
- { text: item, color: COLORS.primary }
1391
- ]
1392
- }
1393
- )
1394
- ] }, item)),
1395
- /* @__PURE__ */ jsx9(NeverLine, { width: dividerWidth }),
1396
- /* @__PURE__ */ jsx9(Text11, { color: COLORS.danger, children: `\u2570${"\u2500".repeat(Math.max(0, dividerWidth - 2))}\u256F` })
1397
- ] }),
1398
- /* @__PURE__ */ jsx9(Box11, { marginTop: 1, flexDirection: "column", children: policyLinks.map((link) => /* @__PURE__ */ jsxs10(Box11, { flexDirection: "row", gap: 1, children: [
1399
- /* @__PURE__ */ jsx9(Text11, { color: COLORS.strong, bold: true, children: `${link.label}:` }),
1400
- /* @__PURE__ */ jsx9(Text11, { color: COLORS.accent, children: link.url })
1401
- ] }, link.label)) }),
1402
- /* @__PURE__ */ jsxs10(Box11, { marginTop: 1, flexDirection: "row", gap: 3, children: [
1403
- /* @__PURE__ */ jsxs10(Box11, { flexDirection: "row", gap: 1, children: [
1404
- /* @__PURE__ */ jsx9(Text11, { color: COLORS.muted, children: "[" }),
1405
- /* @__PURE__ */ jsx9(Text11, { color: COLORS.primary, children: "esc" }),
1406
- /* @__PURE__ */ jsx9(Text11, { color: COLORS.muted, children: "] back" })
1407
- ] }),
1408
- /* @__PURE__ */ jsxs10(Box11, { flexDirection: "row", gap: 1, children: [
1409
- /* @__PURE__ */ jsx9(Text11, { color: COLORS.muted, children: "[" }),
1410
- /* @__PURE__ */ jsx9(Text11, { color: COLORS.primary, children: "enter" }),
1411
- /* @__PURE__ */ jsx9(Text11, { color: COLORS.muted, children: "]" }),
1412
- /* @__PURE__ */ jsx9(Text11, { color: COLORS.success, bold: true, children: "start wizard" })
1413
- ] })
1414
- ] })
1415
- ]
1416
- }
1417
- );
1192
+ function isTelemetryEnabled() {
1193
+ return Boolean(getAuthToken()) && !process.env.VITEST && !isTelemetryOptedOut();
1418
1194
  }
1419
-
1420
- // src/ui/Sidebar.tsx
1421
- import { Box as Box14, Text as Text14 } from "ink";
1422
-
1423
- // src/ui/Steps.tsx
1424
- import { Box as Box12, Text as Text12 } from "ink";
1425
- import Spinner from "ink-spinner";
1426
-
1427
- // src/core/persistence.ts
1428
- import { mkdir, readFile, writeFile, rm } from "node:fs/promises";
1429
- import { join as join4 } from "node:path";
1430
- var isStepVisible = (s) => s.visible !== false;
1431
- var stateFile = (workflowId) => join4(stateDir(), `state-${workflowId}.json`);
1432
- async function loadWorkflowState(workflowId) {
1433
- try {
1434
- const raw = await readFile(stateFile(workflowId), "utf8");
1435
- return JSON.parse(raw);
1436
- } catch {
1437
- return null;
1438
- }
1195
+ function fireAndForget(promise) {
1196
+ void promise.catch(() => {
1197
+ });
1439
1198
  }
1440
- async function saveWorkflowState(state) {
1441
- await mkdir(stateDir(), { recursive: true });
1442
- await writeFile(
1443
- stateFile(state.workflowId),
1444
- JSON.stringify(state, null, 2),
1445
- "utf8"
1199
+ function sendTelemetry(payload) {
1200
+ if (!isTelemetryEnabled()) return;
1201
+ const token = getAuthToken();
1202
+ if (!token) return;
1203
+ const body = {
1204
+ env: process.env.DD_ENV ?? "development",
1205
+ ...payload
1206
+ };
1207
+ fireAndForget(
1208
+ proxyFetch(`${PROXY_BASE_URL}/telemetry`, {
1209
+ method: "POST",
1210
+ headers: {
1211
+ "Content-Type": "application/json",
1212
+ authorization: `Bearer ${token}`
1213
+ },
1214
+ body: JSON.stringify(body)
1215
+ }).then((res) => {
1216
+ if (!res.ok) {
1217
+ throw new Error(`Proxy telemetry ${res.status}`);
1218
+ }
1219
+ })
1446
1220
  );
1447
1221
  }
1448
- async function clearWorkflowState(workflowId) {
1449
- await rm(stateFile(workflowId), { force: true });
1222
+ function sendLog(status, message, attributes = {}, tags = []) {
1223
+ sendTelemetry({
1224
+ logs: [{ status, message, attributes, tags }]
1225
+ });
1450
1226
  }
1451
-
1452
- // src/ui/Steps.tsx
1453
- import { jsx as jsx10, jsxs as jsxs11 } from "react/jsx-runtime";
1454
- function Steps() {
1455
- const { steps } = useWizard();
1456
- const visibleSteps = steps.filter(isStepVisible);
1457
- return /* @__PURE__ */ jsx10(Box12, { flexDirection: "column", gap: 1, children: visibleSteps.map((s) => /* @__PURE__ */ jsx10(Box12, { flexDirection: "column", children: /* @__PURE__ */ jsxs11(Text12, { color: COLORS.status[s.status], children: [
1458
- s.status === "running" ? /* @__PURE__ */ jsx10(Spinner, { type: "dots" }) : MARKER[s.status],
1459
- " ",
1460
- s.title
1461
- ] }) }, s.id)) });
1227
+ function sendMetric(name, value, type, tags = []) {
1228
+ sendTelemetry({
1229
+ metrics: [{ name, value, type, tags }]
1230
+ });
1462
1231
  }
1463
- function CurrentStep() {
1464
- const { steps } = useWizard();
1465
- const currentStep = steps.filter(isStepVisible).find((s) => s.status === "running");
1466
- if (!currentStep) return null;
1467
- return /* @__PURE__ */ jsxs11(Text12, { color: COLORS.status.running, children: [
1468
- /* @__PURE__ */ jsx10(Spinner, { type: "dots" }),
1469
- " ",
1470
- ` ${currentStep.title}`
1471
- ] });
1232
+ function metricTags(workflowId, actionId) {
1233
+ return actionId ? [`workflow:${workflowId}`, `action:${actionId}`] : [`workflow:${workflowId}`];
1472
1234
  }
1473
-
1474
- // src/ui/Progress.tsx
1475
- import { Box as Box13, Text as Text13 } from "ink";
1476
- import { jsx as jsx11, jsxs as jsxs12 } from "react/jsx-runtime";
1477
- function Progress() {
1478
- const { steps, currentStepIndex } = useWizard();
1479
- const visibleSteps = steps.filter(isStepVisible);
1480
- if (visibleSteps.length === 0) return null;
1481
- const visibleCountThroughCurrent = steps.slice(0, currentStepIndex + 1).filter(isStepVisible).length;
1482
- const activeStepNumber = Math.max(1, visibleCountThroughCurrent);
1483
- return /* @__PURE__ */ jsxs12(Box13, { flexDirection: "row", gap: 1, children: [
1484
- /* @__PURE__ */ jsx11(Text13, { color: COLORS.muted, children: "STEP" }),
1485
- /* @__PURE__ */ jsx11(Text13, { color: COLORS.strong, bold: true, children: activeStepNumber }),
1486
- /* @__PURE__ */ jsx11(Text13, { color: COLORS.strong, bold: true, children: "/" }),
1487
- /* @__PURE__ */ jsx11(Text13, { color: COLORS.strong, bold: true, children: visibleSteps.length })
1488
- ] });
1235
+ function logTags(workflowId, actionId, appId) {
1236
+ return [
1237
+ ...metricTags(workflowId, actionId),
1238
+ ...appId ? [`app_id:${appId}`] : []
1239
+ ];
1489
1240
  }
1490
-
1491
- // src/ui/copy/sidebar-commands.ts
1492
- var sidebarCommands = [
1493
- { keyHint: "tab", description: "toggle logs" },
1494
- { keyHint: "esc", description: "exit wizard" }
1495
- ];
1496
-
1497
- // src/ui/Sidebar.tsx
1498
- import { jsx as jsx12, jsxs as jsxs13 } from "react/jsx-runtime";
1499
- function Sidebar() {
1500
- return /* @__PURE__ */ jsxs13(
1501
- Box14,
1502
- {
1503
- backgroundColor: "#14171E",
1504
- width: 30,
1505
- paddingX: 4,
1506
- paddingY: 2,
1507
- flexDirection: "column",
1508
- justifyContent: "space-between",
1509
- children: [
1510
- /* @__PURE__ */ jsxs13(Box14, { flexDirection: "column", gap: 1, children: [
1511
- /* @__PURE__ */ jsx12(Text14, { color: COLORS.muted, children: "PROGRESS" }),
1512
- /* @__PURE__ */ jsx12(Steps, {})
1513
- ] }),
1514
- /* @__PURE__ */ jsxs13(Box14, { flexDirection: "column", gap: 1, children: [
1515
- /* @__PURE__ */ jsx12(Progress, {}),
1516
- /* @__PURE__ */ jsx12(Box14, { flexDirection: "column", children: sidebarCommands.map((c) => {
1517
- return /* @__PURE__ */ jsxs13(Box14, { flexDirection: "row", gap: 1, children: [
1518
- /* @__PURE__ */ jsx12(Text14, { color: COLORS.primary, children: `[${c.keyHint}]` }),
1519
- /* @__PURE__ */ jsx12(Text14, { color: COLORS.muted, children: c.description })
1520
- ] });
1521
- }) })
1522
- ] })
1523
- ]
1524
- }
1525
- );
1241
+ function emitTelemetryLog(level, message, attributes, tags) {
1242
+ if (level === "info") {
1243
+ logger.info(attributes, message);
1244
+ } else {
1245
+ logger.error(attributes, message);
1246
+ }
1247
+ sendLog(level, message, attributes, tags);
1526
1248
  }
1527
-
1528
- // src/ui/Ribbon.tsx
1529
- import { Box as Box15, Text as Text15 } from "ink";
1530
- import { jsx as jsx13, jsxs as jsxs14 } from "react/jsx-runtime";
1531
- function Ribbon() {
1532
- const firstCommand = sidebarCommands[0];
1533
- return /* @__PURE__ */ jsxs14(
1534
- Box15,
1535
- {
1536
- backgroundColor: "#14171E",
1537
- flexDirection: "row",
1538
- justifyContent: "space-between",
1539
- paddingX: 2,
1540
- paddingY: 1,
1541
- children: [
1542
- /* @__PURE__ */ jsx13(Progress, {}),
1543
- /* @__PURE__ */ jsx13(CurrentStep, {}),
1544
- /* @__PURE__ */ jsxs14(Box15, { flexDirection: "row", gap: 1, children: [
1545
- /* @__PURE__ */ jsx13(Text15, { color: COLORS.primary, children: `[${firstCommand.keyHint}]` }),
1546
- /* @__PURE__ */ jsx13(Text15, { color: COLORS.muted, children: firstCommand.description })
1547
- ] })
1548
- ]
1549
- }
1249
+ function trackWorkflowStart(ctx) {
1250
+ const attributes = {
1251
+ event: "wizard.workflow.start",
1252
+ workflow_id: ctx.workflowId,
1253
+ app_id: ctx.appId
1254
+ };
1255
+ emitTelemetryLog(
1256
+ "info",
1257
+ "wizard workflow started",
1258
+ attributes,
1259
+ logTags(ctx.workflowId, void 0, ctx.appId)
1550
1260
  );
1551
- }
1552
-
1553
- // src/ui/App.tsx
1554
- import { useState as useState9 } from "react";
1555
-
1556
- // src/ui/Logs.tsx
1557
- import { Box as Box16, Text as Text16, useInput as useInput5 } from "ink";
1558
- import { jsx as jsx14, jsxs as jsxs15 } from "react/jsx-runtime";
1559
- var KIND_COLOR = {
1560
- tool: COLORS.primary,
1561
- prompt: COLORS.badge
1562
- };
1563
- var STATUS_COLOR = {
1564
- running: COLORS.status.running,
1565
- error: COLORS.danger
1566
- };
1567
- function logNameColor(entry) {
1568
- return STATUS_COLOR[entry.status] ?? KIND_COLOR[entry.kind];
1569
- }
1570
- var ROW_GAP = 1;
1571
- function truncate2(str, maxWidth) {
1572
- if (maxWidth <= 0) return "";
1573
- return str.length > maxWidth ? `${str.slice(0, maxWidth - 1)}\u2026` : str;
1574
- }
1575
- function rawInputText(input) {
1576
- if (input === void 0) return "";
1577
- const str = typeof input === "string" ? input : JSON.stringify(input);
1578
- if (!str || str === "{}") return "";
1579
- return str.replace(/\s+/g, " ").trim();
1580
- }
1581
- function formatTimestamp(ms) {
1582
- const d = new Date(ms);
1583
- const pad = (n) => String(n).padStart(2, "0");
1584
- return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
1585
- }
1586
- function Logs() {
1587
- const logs = useWizard((s) => s.logs);
1588
- const scroll = useScrollWindow({ itemCount: logs.length, followBottom: true });
1589
- useInput5((_input, key) => {
1590
- if (key.upArrow) scroll.scrollBy(-1);
1591
- else if (key.downArrow) scroll.scrollBy(1);
1592
- });
1593
- const visible = logs.slice(scroll.offset, scroll.offset + scroll.capacity);
1594
- return /* @__PURE__ */ jsxs15(Box16, { flexDirection: "column", paddingX: 4, paddingY: 2, flexGrow: 1, children: [
1595
- logs.length === 0 && /* @__PURE__ */ jsx14(Text16, { color: COLORS.dim, children: "No logs yet." }),
1596
- /* @__PURE__ */ jsx14(ScrollView, { scroll, children: visible.map((entry) => {
1597
- const timestamp = `[${formatTimestamp(entry.startedAt)}]`;
1598
- const durationText = entry.kind === "tool" && entry.durationMs !== void 0 ? `${entry.durationMs}ms` : "";
1599
- const rawPreview = rawInputText(entry.input);
1600
- const partCount = 2 + (rawPreview ? 1 : 0) + (durationText ? 1 : 0);
1601
- const gaps = (partCount - 1) * ROW_GAP;
1602
- let budget = scroll.width - timestamp.length - durationText.length - gaps;
1603
- const name = truncate2(entry.name, budget);
1604
- budget -= name.length;
1605
- const preview = rawPreview ? truncate2(rawPreview, budget) : "";
1606
- return /* @__PURE__ */ jsxs15(Box16, { flexDirection: "row", gap: ROW_GAP, children: [
1607
- /* @__PURE__ */ jsx14(Text16, { color: COLORS.dim, children: timestamp }),
1608
- /* @__PURE__ */ jsx14(Text16, { color: logNameColor(entry), wrap: "truncate", children: name }),
1609
- preview && /* @__PURE__ */ jsx14(Text16, { color: COLORS.dim, wrap: "truncate", children: preview }),
1610
- durationText && /* @__PURE__ */ jsx14(Text16, { color: COLORS.dim, children: durationText })
1611
- ] }, entry.id);
1612
- }) }),
1613
- /* @__PURE__ */ jsx14(Text16, { color: COLORS.dim, children: "\u2191/\u2193 scroll" })
1614
- ] });
1615
- }
1616
-
1617
- // src/lib/events.ts
1618
- import "zod";
1619
-
1620
- // src/lib/telemetry.ts
1621
- function isTelemetryOptedOut() {
1622
- return process.env.WIZARD_TELEMETRY === "false";
1623
- }
1624
- function isTelemetryEnabled() {
1625
- return Boolean(getAuthToken()) && !process.env.VITEST && !isTelemetryOptedOut();
1626
- }
1627
- function fireAndForget(promise) {
1628
- void promise.catch(() => {
1629
- });
1630
- }
1631
- function sendTelemetry(payload) {
1632
- if (!isTelemetryEnabled()) return;
1633
- const token = getAuthToken();
1634
- if (!token) return;
1635
- const body = {
1636
- env: process.env.DD_ENV ?? "development",
1637
- ...payload
1638
- };
1639
- fireAndForget(
1640
- proxyFetch(`${PROXY_BASE_URL}/telemetry`, {
1641
- method: "POST",
1642
- headers: {
1643
- "Content-Type": "application/json",
1644
- authorization: `Bearer ${token}`
1645
- },
1646
- body: JSON.stringify(body)
1647
- }).then((res) => {
1648
- if (!res.ok) {
1649
- throw new Error(`Proxy telemetry ${res.status}`);
1650
- }
1651
- })
1652
- );
1653
- }
1654
- function sendLog(status, message, attributes = {}, tags = []) {
1655
- sendTelemetry({
1656
- logs: [{ status, message, attributes, tags }]
1657
- });
1658
- }
1659
- function sendMetric(name, value, type, tags = []) {
1660
- sendTelemetry({
1661
- metrics: [{ name, value, type, tags }]
1662
- });
1663
- }
1664
- function metricTags(workflowId, actionId) {
1665
- return actionId ? [`workflow:${workflowId}`, `action:${actionId}`] : [`workflow:${workflowId}`];
1666
- }
1667
- function logTags(workflowId, actionId, appId) {
1668
- return [
1669
- ...metricTags(workflowId, actionId),
1670
- ...appId ? [`app_id:${appId}`] : []
1671
- ];
1672
- }
1673
- function emitTelemetryLog(level, message, attributes, tags) {
1674
- if (level === "info") {
1675
- logger.info(attributes, message);
1676
- } else {
1677
- logger.error(attributes, message);
1678
- }
1679
- sendLog(level, message, attributes, tags);
1680
- }
1681
- function trackWorkflowStart(ctx) {
1682
- const attributes = {
1683
- event: "wizard.workflow.start",
1684
- workflow_id: ctx.workflowId,
1685
- app_id: ctx.appId
1686
- };
1687
- emitTelemetryLog(
1688
- "info",
1689
- "wizard workflow started",
1690
- attributes,
1691
- logTags(ctx.workflowId, void 0, ctx.appId)
1692
- );
1693
- sendMetric("wizard.workflow.invocation", 1, 1, metricTags(ctx.workflowId));
1261
+ sendMetric("wizard.workflow.invocation", 1, 1, metricTags(ctx.workflowId));
1694
1262
  }
1695
1263
  function trackActionStart(ctx) {
1696
1264
  const attributes = {
@@ -1783,6 +1351,7 @@ function trackWorkflowError(ctx) {
1783
1351
  }
1784
1352
 
1785
1353
  // src/lib/events.ts
1354
+ import "zod";
1786
1355
  function track(event, payload) {
1787
1356
  if (isTelemetryOptedOut()) return;
1788
1357
  const token = getAuthToken();
@@ -1801,514 +1370,81 @@ function track(event, payload) {
1801
1370
  });
1802
1371
  }
1803
1372
 
1804
- // src/ui/Tips.tsx
1805
- import { useEffect as useEffect3, useState as useState8 } from "react";
1806
- import { Box as Box18, Text as Text18 } from "ink";
1807
- import terminalLink from "terminal-link";
1808
-
1809
- // src/ui/Code.tsx
1810
- import { Box as Box17, Text as Text17 } from "ink";
1811
- import { jsx as jsx15 } from "react/jsx-runtime";
1812
- var TOKEN_RE = /("(?:\\.|[^"\\])*"|\btrue\b|\bfalse\b|\bnull\b|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|[{}[\]:,])/g;
1813
- function highlightJson(json) {
1814
- const parts = json.split(TOKEN_RE);
1815
- return parts.map((part, i) => {
1816
- if (!part) return null;
1817
- if (part[0] === '"') {
1818
- const next = parts.slice(i + 1).find((p) => p.trim());
1819
- const isKey = next?.trimStart().startsWith(":");
1820
- return /* @__PURE__ */ jsx15(Text17, { color: isKey ? "cyan" : "green", children: part }, i);
1821
- }
1822
- if (part === "true" || part === "false" || part === "null") {
1823
- return /* @__PURE__ */ jsx15(Text17, { color: "magenta", children: part }, i);
1824
- }
1825
- if (/^-?\d/.test(part)) {
1826
- return /* @__PURE__ */ jsx15(Text17, { color: "yellow", children: part }, i);
1827
- }
1828
- if (/^[{}[\]:,]$/.test(part)) {
1829
- return /* @__PURE__ */ jsx15(Text17, { dimColor: true, children: part }, i);
1830
- }
1831
- return /* @__PURE__ */ jsx15(Text17, { children: part }, i);
1832
- });
1373
+ // src/core/orchestrator.ts
1374
+ function defineStep(step) {
1375
+ return { visible: true, ...step };
1833
1376
  }
1834
- function Code({ children }) {
1835
- return /* @__PURE__ */ jsx15(Box17, { children: /* @__PURE__ */ jsx15(Text17, { children: highlightJson(children) }) });
1377
+ var nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
1378
+ function ensureExecutedStepCount(state) {
1379
+ if (state.executedStepCount == null) {
1380
+ state.executedStepCount = state.steps.filter(
1381
+ (s) => s.status === "done" && isStepVisible(s)
1382
+ ).length;
1383
+ }
1836
1384
  }
1837
-
1838
- // src/ui/copy/tips.ts
1839
- var tips = [
1840
- {
1841
- title: "An Application is like your library. Indices are the books in it.",
1842
- chunks: [
1843
- {
1844
- type: "text",
1845
- value: "Applications hold your API keys and Indices."
1846
- },
1847
- {
1848
- type: "text",
1849
- value: "Each index is a searchable collection of records (think: products, articles, orders, concerts)."
1850
- }
1851
- ]
1852
- },
1853
- {
1854
- title: "A Record is a JSON object inside of an index.",
1855
- chunks: [
1856
- {
1857
- type: "text",
1858
- value: "Every object you index is just a JSON document with a unique objectID."
1859
- },
1860
- {
1861
- type: "text",
1862
- value: "No fixed schema, no hard requirements."
1863
- },
1864
- {
1865
- type: "text",
1866
- value: "Everything else is up to you:"
1867
- },
1868
- {
1869
- type: "code",
1870
- value: JSON.stringify(
1871
- {
1872
- objectID: "sku_48213",
1873
- name: "Trail Running Shoe",
1874
- brand: "Northline",
1875
- categories: ["Footwear", "Running", "Mens"],
1876
- price: 129.99,
1877
- in_stock: true,
1878
- rating: 4.6
1879
- },
1880
- null,
1881
- 2
1882
- )
1385
+ function reconcileWorkflowState(state, workflow) {
1386
+ const persistedIds = state.steps.map((s) => s.id).join("\n");
1387
+ const definedIds = workflow.steps.map((s) => s.id).join("\n");
1388
+ if (persistedIds !== definedIds) return null;
1389
+ for (let i = 0; i < state.steps.length; i++) {
1390
+ const record = state.steps[i];
1391
+ const step = workflow.steps[i];
1392
+ if (record.status === "done") {
1393
+ const parsed = step.outputSchema.safeParse(record.output);
1394
+ if (!parsed.success) {
1395
+ logger.warn(
1396
+ { stepId: step.id, issues: parsed.error.issues },
1397
+ "reconcileWorkflowState: persisted output for a completed step no longer matches its schema; restarting the run from scratch"
1398
+ );
1399
+ return null;
1883
1400
  }
1884
- ]
1885
- },
1886
- {
1887
- title: "Speed isn't a feature, it's our business",
1888
- chunks: [
1889
- {
1890
- type: "text",
1891
- value: "Algolia's engine processes most search queries in 1 to 50 milliseconds."
1892
- },
1893
- {
1894
- type: "text",
1895
- value: "This is why our search-as-you-type experience feels instant."
1896
- }
1897
- ]
1898
- },
1899
- {
1900
- title: "Searchable attributes are your relevance dial #1",
1901
- chunks: [
1902
- {
1903
- type: "text",
1904
- value: "Order matters. Attributes listed first in"
1905
- },
1906
- { type: "codeword", value: "searchableAttributes" },
1907
- { type: "text", value: "carry more ranking weight." },
1908
- {
1909
- type: "text",
1910
- value: "This is the single highest-leverage lever new users don't know exists."
1911
- }
1912
- ]
1913
- },
1914
- {
1915
- title: "Facets aren't just filters, they help build your UI",
1916
- chunks: [
1917
- { type: "codeword", value: "attributesForFaceting" },
1918
- {
1919
- type: "text",
1920
- value: "unlock category sidebars, price sliders, tag clouds without extra backend work."
1921
- }
1922
- ]
1923
- },
1924
- {
1925
- title: "Test relevance in the dashboard before you write a line of ranking code",
1926
- chunks: [
1927
- {
1928
- type: "text",
1929
- value: "Our Search dashboard has a live preview where you can browse results."
1930
- },
1931
- {
1932
- type: "text",
1933
- value: "Tune your settings and see how it alters your results in"
1934
- },
1935
- {
1936
- type: "link",
1937
- value: "real-time.",
1938
- href: "https://dashboard.algolia.com/explorer/browse"
1939
- }
1940
- ]
1941
- },
1942
- {
1943
- title: "Search Analytics helps you discover opportunities",
1944
- chunks: [
1945
- {
1946
- type: "text",
1947
- value: "No click results for a particular query? Low click-through rates for another?"
1948
- },
1949
- {
1950
- type: "text",
1951
- value: "Our Search Analytics will help you identify synonyms, rules or other relevancy settings to improve your results."
1952
- }
1953
- ]
1954
- },
1955
- {
1956
- title: "Let Algolia act as your recommendation engine",
1957
- chunks: [
1958
- {
1959
- type: "text",
1960
- value: "Beyond search, Algolia Recommend runs models trained on your existing indices and event data to power your recommendation engine."
1961
- },
1962
- {
1963
- type: "text",
1964
- value: "You can improve engagement with related, popular or visually similar items."
1965
- }
1966
- ]
1401
+ }
1402
+ record.title = step.title;
1403
+ record.visible = step.visible;
1967
1404
  }
1968
- ];
1969
-
1970
- // src/ui/Tips.tsx
1971
- import { jsx as jsx16, jsxs as jsxs16 } from "react/jsx-runtime";
1972
- var TICK_MS = 16;
1973
- var CHUNK_HOLD_MS = 2e3;
1974
- var HOLD_MS = 8e3;
1975
- function TipTitle({ children }) {
1976
- return /* @__PURE__ */ jsxs16(Box18, { gap: 1, children: [
1977
- /* @__PURE__ */ jsx16(Text18, { color: "cyan", children: "\u2726" }),
1978
- /* @__PURE__ */ jsx16(Text18, { color: "white", bold: true, children })
1979
- ] });
1405
+ return state;
1980
1406
  }
1981
- function InlineSegment({ segment }) {
1982
- switch (segment.type) {
1983
- case "highlight":
1984
- return /* @__PURE__ */ jsx16(Text18, { color: COLORS.success, children: segment.value });
1985
- case "link":
1986
- return /* @__PURE__ */ jsx16(Text18, { color: "cyan", underline: true, children: segment.href ? terminalLink(segment.value, segment.href) : segment.value });
1987
- case "codeword":
1988
- return /* @__PURE__ */ jsx16(Text18, { color: COLORS.highlight.fg, backgroundColor: COLORS.highlight.bg, children: segment.value });
1989
- default:
1990
- return /* @__PURE__ */ jsx16(Text18, { color: COLORS.muted, children: segment.value });
1991
- }
1407
+ function initWorkflowState(workflow, now) {
1408
+ return {
1409
+ workflowId: workflow.id,
1410
+ startedAt: now,
1411
+ updatedAt: now,
1412
+ currentStepIndex: 0,
1413
+ steps: workflow.steps.map((s) => ({
1414
+ id: s.id,
1415
+ title: s.title,
1416
+ visible: s.visible,
1417
+ status: "pending"
1418
+ })),
1419
+ algoliaState: {},
1420
+ userInputs: {}
1421
+ };
1992
1422
  }
1993
- function TipContent({
1994
- chunks,
1995
- revealed
1996
- }) {
1997
- let remaining = revealed;
1998
- const slices = chunks.map((chunk) => {
1999
- const slice = chunk.value.slice(0, Math.max(0, remaining));
2000
- remaining -= chunk.value.length;
2001
- return slice;
1423
+ async function ensureConsent() {
1424
+ const config = await loadConfig();
1425
+ if (config.aiConsent) return;
1426
+ const store = useWizard.getState();
1427
+ const answer = await store.requestUserInput({
1428
+ prompt: "Wizard will make AI-authored changes to this repository, and will propose shell commands to set it up. You approve each command before it runs.",
1429
+ promptType: "enterToContinue",
1430
+ options: []
2002
1431
  });
2003
- const blocks = [];
2004
- for (let i = 0; i < chunks.length; i++) {
2005
- const chunk = chunks[i];
2006
- const slice = slices[i];
2007
- if (!slice) continue;
2008
- const segment = {
2009
- type: chunk.type,
2010
- value: slice,
2011
- href: chunk.type === "link" ? chunk.href : void 0
2012
- };
2013
- if (chunk.type === "code") {
2014
- blocks.push({ type: "code", segment });
2015
- continue;
2016
- }
2017
- const last = blocks[blocks.length - 1];
2018
- if (last?.type === "inline") {
2019
- last.segments.push(segment);
2020
- } else {
2021
- blocks.push({ type: "inline", segments: [segment] });
2022
- }
2023
- }
2024
- return /* @__PURE__ */ jsx16(Box18, { flexDirection: "column", marginLeft: 2, gap: 1, children: blocks.map(
2025
- (block, i) => block.type === "code" ? /* @__PURE__ */ jsx16(Code, { children: block.segment.value }, i) : /* @__PURE__ */ jsx16(Text18, { children: block.segments.map((segment, j) => /* @__PURE__ */ jsxs16(Text18, { children: [
2026
- j > 0 && " ",
2027
- /* @__PURE__ */ jsx16(InlineSegment, { segment })
2028
- ] }, j)) }, i)
2029
- ) });
2030
- }
2031
- function Tips() {
2032
- const [tipIndex, setTipIndex] = useState8(0);
2033
- const [revealed, setRevealed] = useState8(0);
2034
- const tip = tips[tipIndex];
2035
- const contentLength = tip.chunks.reduce((sum, c) => sum + c.value.length, 0);
2036
- const totalLength = tip.title.length + contentLength;
2037
- const segments = [
2038
- { type: "title", length: tip.title.length },
2039
- ...tip.chunks.map((c) => ({ type: c.type, length: c.value.length }))
2040
- ];
2041
- const noPauseAfterText = [
2042
- "highlight",
2043
- "link",
2044
- "codeword"
2045
- ];
2046
- const chunkBoundaries = [];
2047
- let cumulative = 0;
2048
- for (let i = 0; i < segments.length - 1; i++) {
2049
- cumulative += segments[i].length;
2050
- const skipPause = noPauseAfterText.includes(
2051
- segments[i + 1].type
1432
+ if (answer !== true) {
1433
+ throw new Error(
1434
+ "AI consent declined \u2014 cannot proceed. If you change your mind, just run the Wizard again!"
2052
1435
  );
2053
- if (!skipPause) chunkBoundaries.push(cumulative);
2054
1436
  }
2055
- useEffect3(() => {
2056
- if (revealed < totalLength) {
2057
- const delay2 = chunkBoundaries.includes(revealed) ? CHUNK_HOLD_MS : TICK_MS;
2058
- const timer2 = setTimeout(() => setRevealed((r) => r + 1), delay2);
2059
- return () => clearTimeout(timer2);
2060
- }
2061
- const timer = setTimeout(() => {
2062
- setTipIndex((i) => {
2063
- if (i < tips.length - 1) return i + 1;
2064
- return 0;
2065
- });
2066
- setRevealed(0);
2067
- }, HOLD_MS);
2068
- return () => clearTimeout(timer);
2069
- }, [revealed, totalLength]);
2070
- const titleRevealed = tip.title.slice(0, revealed);
2071
- const contentRevealed = Math.max(0, revealed - tip.title.length);
2072
- return /* @__PURE__ */ jsxs16(Box18, { flexDirection: "column", marginBottom: 1, gap: 1, children: [
2073
- /* @__PURE__ */ jsx16(TipTitle, { children: titleRevealed }),
2074
- /* @__PURE__ */ jsx16(TipContent, { chunks: tip.chunks, revealed: contentRevealed })
2075
- ] });
1437
+ config.aiConsent = true;
1438
+ await saveConfig(config);
2076
1439
  }
2077
-
2078
- // src/ui/App.tsx
2079
- import { jsx as jsx17, jsxs as jsxs17 } from "react/jsx-runtime";
2080
- function App() {
2081
- const {
2082
- phase,
2083
- error,
2084
- homeScreen,
2085
- currentStepIndex,
2086
- steps,
2087
- inputReq,
2088
- user,
2089
- notices
2090
- } = useWizard();
2091
- const { exit } = useApp();
2092
- const { columns, rows } = useWindowSize7();
2093
- const [showLogs, setShowLogs] = useState9(false);
2094
- const finished = phase === "done" || phase === "error";
2095
- const currentStep = steps[currentStepIndex];
2096
- const isInitialAnalysisStep = currentStepIndex === 0;
2097
- const isAwaitingUserInput = phase === "awaitingInput" && !!inputReq;
2098
- const showTips = phase === "running" && isInitialAnalysisStep && notices.length > 0;
2099
- const showNoticesInMain = !isInitialAnalysisStep;
2100
- const showNotices = !isAwaitingUserInput;
2101
- useInput6(
2102
- (_input, key) => {
2103
- if (key.return) {
2104
- exit();
2105
- }
2106
- },
2107
- { isActive: finished }
2108
- );
2109
- useInput6((_input, key) => {
2110
- if (phase === "idle" || phase === "authenticating") return;
2111
- if (key.tab) {
2112
- setShowLogs(!showLogs);
2113
- track("AI Wizard Interaction", {
2114
- context: "global",
2115
- key: "tab",
2116
- currentStep: currentStep?.id
2117
- });
2118
- }
2119
- });
2120
- const escOwnedElsewhere = phase === "idle" || phase === "authenticating" || phase === "awaitingInput" && (inputReq?.promptType === "enterToContinue" || inputReq?.promptType === "commandApproval");
2121
- useInput6((_input, key) => {
2122
- if (escOwnedElsewhere) return;
2123
- if (key.escape) {
2124
- track("AI Wizard Interaction", {
2125
- context: "global",
2126
- key: "esc",
2127
- currentStep: currentStep?.id ?? phase
2128
- });
2129
- exit();
2130
- }
2131
- });
2132
- const mainWindowVisible = phase === "authenticating" || phase === "preflight" || phase === "running" || phase === "awaitingInput" || phase === "error" || phase === "done";
2133
- const flexDirection = columns > 90 ? "row" : "column";
2134
- const showSidebar = flexDirection === "row";
2135
- const scrollsPastViewport = phase === "idle" && homeScreen === "learnMore";
2136
- return (
2137
- /* Clamped to exactly the viewport: a taller frame makes Ink clear and repaint
2138
- the whole screen, and the scrolling throws off its cursor arithmetic —
2139
- flicker and leftover rows. */
2140
- /* @__PURE__ */ jsxs17(
2141
- Box19,
2142
- {
2143
- backgroundColor: COLORS.bg.main,
2144
- flexDirection: "row",
2145
- width: columns,
2146
- height: scrollsPastViewport ? void 0 : rows,
2147
- overflow: scrollsPastViewport ? "visible" : "hidden",
2148
- children: [
2149
- mainWindowVisible && /* @__PURE__ */ jsxs17(
2150
- Box19,
2151
- {
2152
- flexDirection,
2153
- width: "100%",
2154
- maxHeight: rows,
2155
- justifyContent: "space-between",
2156
- children: [
2157
- showLogs ? /* @__PURE__ */ jsx17(Logs, {}) : /* @__PURE__ */ jsxs17(
2158
- Box19,
2159
- {
2160
- flexDirection: "column",
2161
- paddingX: 4,
2162
- paddingY: 2,
2163
- width: showSidebar ? 70 : "100%",
2164
- flexGrow: 1,
2165
- gap: 1,
2166
- children: [
2167
- /* @__PURE__ */ jsxs17(Box19, { flexGrow: 2, flexDirection: "column", children: [
2168
- phase === "preflight" && !user && /* @__PURE__ */ jsx17(Box19, { flexDirection: "column", marginBottom: 1, children: /* @__PURE__ */ jsxs17(Text19, { color: COLORS.strong, bold: true, children: [
2169
- /* @__PURE__ */ jsx17(Spinner2, { type: "dots" }),
2170
- " Signing in to Algolia"
2171
- ] }) }),
2172
- phase === "preflight" && user && /* @__PURE__ */ jsx17(Box19, { flexDirection: "column", marginBottom: 1, children: /* @__PURE__ */ jsxs17(Text19, { color: COLORS.strong, bold: true, children: [
2173
- /* @__PURE__ */ jsx17(Spinner2, { type: "dots" }),
2174
- " Getting things ready"
2175
- ] }) }),
2176
- phase === "authenticating" && /* @__PURE__ */ jsxs17(Box19, { flexDirection: "column", marginBottom: 1, children: [
2177
- /* @__PURE__ */ jsxs17(Text19, { color: COLORS.strong, bold: true, children: [
2178
- /* @__PURE__ */ jsx17(Spinner2, { type: "dots" }),
2179
- " Signing in to Algolia"
2180
- ] }),
2181
- /* @__PURE__ */ jsx17(Text19, { color: COLORS.muted, children: "A browser window will open \u2014 complete sign-in there." })
2182
- ] }),
2183
- /* @__PURE__ */ jsx17(CliOutput, {}),
2184
- showTips && /* @__PURE__ */ jsx17(Tips, {}),
2185
- showNoticesInMain && showNotices && /* @__PURE__ */ jsx17(Notices, { showAll: true, border: false }),
2186
- /* @__PURE__ */ jsx17(PromptInput, {}),
2187
- phase === "error" && error && /* @__PURE__ */ jsx17(Box19, { marginTop: 1, children: /* @__PURE__ */ jsxs17(Text19, { color: COLORS.status.error, children: [
2188
- "\u2716 ",
2189
- error
2190
- ] }) })
2191
- ] }),
2192
- !showNoticesInMain && showNotices && /* @__PURE__ */ jsx17(Notices, {})
2193
- ]
2194
- }
2195
- ),
2196
- showSidebar ? /* @__PURE__ */ jsx17(Sidebar, {}) : /* @__PURE__ */ jsx17(Ribbon, {})
2197
- ]
2198
- }
2199
- ),
2200
- phase === "idle" && (homeScreen === "learnMore" ? /* @__PURE__ */ jsx17(LearnMore, {}) : /* @__PURE__ */ jsx17(Welcome, {}))
2201
- ]
2202
- }
2203
- )
2204
- );
2205
- }
2206
-
2207
- // src/core/orchestrator.ts
2208
- import "zod";
2209
-
2210
- // src/core/config.ts
2211
- import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
2212
- import { join as join5 } from "node:path";
2213
- var configFile = () => join5(stateDir(), "config.json");
2214
- var defaultConfig = () => ({
2215
- version: 1,
2216
- aiConsent: false,
2217
- workflowsRun: []
2218
- });
2219
- async function loadConfig() {
2220
- try {
2221
- const raw = await readFile2(configFile(), "utf8");
2222
- return { ...defaultConfig(), ...JSON.parse(raw) };
2223
- } catch {
2224
- return defaultConfig();
2225
- }
2226
- }
2227
- async function saveConfig(config) {
2228
- await mkdir2(stateDir(), { recursive: true });
2229
- await writeFile2(configFile(), JSON.stringify(config, null, 2), "utf8");
2230
- }
2231
- async function recordWorkflowRun(workflowId, completedAt) {
2232
- const config = await loadConfig();
2233
- config.workflowsRun.push({ workflowId, completedAt });
2234
- await saveConfig(config);
2235
- }
2236
-
2237
- // src/core/orchestrator.ts
2238
- function defineStep(step) {
2239
- return { visible: true, ...step };
2240
- }
2241
- var nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
2242
- function ensureExecutedStepCount(state) {
2243
- if (state.executedStepCount == null) {
2244
- state.executedStepCount = state.steps.filter(
2245
- (s) => s.status === "done" && isStepVisible(s)
2246
- ).length;
2247
- }
2248
- }
2249
- function reconcileWorkflowState(state, workflow) {
2250
- const persistedIds = state.steps.map((s) => s.id).join("\n");
2251
- const definedIds = workflow.steps.map((s) => s.id).join("\n");
2252
- if (persistedIds !== definedIds) return null;
2253
- for (let i = 0; i < state.steps.length; i++) {
2254
- const record = state.steps[i];
2255
- const step = workflow.steps[i];
2256
- if (record.status === "done") {
2257
- const parsed = step.outputSchema.safeParse(record.output);
2258
- if (!parsed.success) {
2259
- logger.warn(
2260
- { stepId: step.id, issues: parsed.error.issues },
2261
- "reconcileWorkflowState: persisted output for a completed step no longer matches its schema; restarting the run from scratch"
2262
- );
2263
- return null;
2264
- }
2265
- }
2266
- record.title = step.title;
2267
- record.visible = step.visible;
2268
- }
2269
- return state;
2270
- }
2271
- function initWorkflowState(workflow, now) {
2272
- return {
2273
- workflowId: workflow.id,
2274
- startedAt: now,
2275
- updatedAt: now,
2276
- currentStepIndex: 0,
2277
- steps: workflow.steps.map((s) => ({
2278
- id: s.id,
2279
- title: s.title,
2280
- visible: s.visible,
2281
- status: "pending"
2282
- })),
2283
- algoliaState: {},
2284
- userInputs: {}
2285
- };
2286
- }
2287
- async function ensureConsent() {
2288
- const config = await loadConfig();
2289
- if (config.aiConsent) return;
2290
- const store = useWizard.getState();
2291
- const answer = await store.requestUserInput({
2292
- prompt: "Wizard will make AI-authored changes to this repository, and will propose shell commands to set it up. You approve each command before it runs.",
2293
- promptType: "enterToContinue",
2294
- options: []
2295
- });
2296
- if (answer !== true) {
2297
- throw new Error(
2298
- "AI consent declined \u2014 cannot proceed. If you change your mind, just run the Wizard again!"
2299
- );
2300
- }
2301
- config.aiConsent = true;
2302
- await saveConfig(config);
2303
- }
2304
- async function makeContext(state) {
2305
- const outputs = {};
2306
- const completedSteps = [];
2307
- for (const s of state.steps) {
2308
- if (s.status === "done") {
2309
- outputs[s.id] = s.output;
2310
- if (s.visible) {
2311
- completedSteps.push({ id: s.id, title: s.title, output: s.output });
1440
+ async function makeContext(state) {
1441
+ const outputs = {};
1442
+ const completedSteps = [];
1443
+ for (const s of state.steps) {
1444
+ if (s.status === "done") {
1445
+ outputs[s.id] = s.output;
1446
+ if (s.visible) {
1447
+ completedSteps.push({ id: s.id, title: s.title, output: s.output });
2312
1448
  }
2313
1449
  }
2314
1450
  }
@@ -2448,219 +1584,20 @@ async function runWorkflow(workflow, appId) {
2448
1584
  }
2449
1585
  }
2450
1586
 
2451
- // src/lib/algoliaApp.ts
1587
+ // src/actions/listIndices.ts
2452
1588
  import { z as z4 } from "zod";
2453
- var applicationSchema = z4.object({
2454
- id: z4.string().min(1),
2455
- name: z4.string().default(""),
2456
- plan: z4.string().optional()
1589
+ var indicesListSchema = z4.object({
1590
+ items: z4.array(
1591
+ z4.object({
1592
+ name: z4.string(),
1593
+ entries: z4.number().default(0)
1594
+ })
1595
+ )
2457
1596
  });
2458
- var listSchema = z4.array(
2459
- z4.object({
2460
- id: z4.string().min(1),
2461
- name: z4.string().default(""),
2462
- plan_label: z4.string().optional()
2463
- }).transform(({ id, name, plan_label }) => ({ id, name, plan: plan_label }))
2464
- );
2465
- async function currentApplication() {
2466
- let raw;
2467
- try {
2468
- raw = await runAlgoliaCli(["application", "current", "-o", "json"]);
2469
- } catch {
2470
- return null;
2471
- }
2472
- const parsed = applicationSchema.safeParse(parseJson(raw));
2473
- return parsed.success ? parsed.data : null;
2474
- }
2475
- async function requireApplication() {
2476
- const app = await currentApplication();
2477
- if (!app) {
2478
- throw new Error(
2479
- "No Algolia application is selected. Run `npx @algolia/cli@latest application select` and restart the wizard."
2480
- );
2481
- }
2482
- return app;
2483
- }
2484
- async function listApplications() {
2485
- const raw = await runAlgoliaCli(["application", "list", "-o", "json"]);
2486
- const parsed = listSchema.safeParse(parseJson(raw));
2487
- if (!parsed.success) {
2488
- throw new Error("Could not read the list of Algolia applications.");
2489
- }
2490
- return parsed.data;
2491
- }
2492
- async function selectApplication(id) {
2493
- const raw = await runAlgoliaCli(
2494
- ["application", "select", "--non-interactive", "--app-id", id],
2495
- { onOutput: stderrSink }
2496
- );
2497
- const parsed = applicationSchema.safeParse(parseJson(raw));
2498
- if (!parsed.success) {
2499
- throw new Error(
2500
- `Selected application ${id}, but the Algolia CLI returned an unreadable result.`
2501
- );
2502
- }
2503
- return parsed.data;
2504
- }
2505
- function parseJson(text) {
2506
- try {
2507
- return JSON.parse(text);
2508
- } catch {
2509
- return void 0;
2510
- }
2511
- }
2512
-
2513
- // src/lib/envAppId.ts
2514
- import { readFile as readFile3 } from "node:fs/promises";
2515
- import { join as join6 } from "node:path";
2516
- var ENV_FILES = [".env", ".env.local"];
2517
- var APP_ID_LINE = /^[ \t]*(?:export[ \t]+)?([A-Z0-9_]*ALGOLIA_APP(?:LICATION)?_ID)[ \t]*=[ \t]*(.*)$/gm;
2518
- async function findEnvApplicationId(root = process.cwd()) {
2519
- for (const file of ENV_FILES) {
2520
- let content;
2521
- try {
2522
- content = await readFile3(join6(root, file), "utf8");
2523
- } catch (err) {
2524
- if (err.code !== "ENOENT") {
2525
- logger.warn(
2526
- { file, err },
2527
- "could not read env file for an application id"
2528
- );
2529
- }
2530
- continue;
2531
- }
2532
- for (const [, name, raw] of content.matchAll(APP_ID_LINE)) {
2533
- const id = readValue(raw);
2534
- if (id) {
2535
- logger.info({ file, name, app: id }, "found an application id in env");
2536
- return { id, name, file };
2537
- }
2538
- }
2539
- }
2540
- return null;
2541
- }
2542
- function readValue(raw) {
2543
- const trimmed = raw.trim();
2544
- const quoted = trimmed.match(/^(['"])(.*)\1/);
2545
- const value = quoted ? quoted[2].trim() : trimmed.replace(/\s+#.*$/, "").trim();
2546
- return value.length > 0 && !value.startsWith("<") ? value : null;
2547
- }
2548
-
2549
- // src/lib/algoliaAppPicker.ts
2550
- function secondaryFor(app) {
2551
- return app.plan ? { kind: "badge", value: app.plan } : void 0;
2552
- }
2553
- function labelFor(app) {
2554
- return app.name.trim() ? `${app.name} \u2014 ${app.id}` : app.id;
2555
- }
2556
- function selectAndReport(app) {
2557
- useWizard.getState().pushCliOutput(
2558
- "stdout",
2559
- `Selecting ${labelFor(app)} \u2014 provisioning its API key\u2026`
2560
- );
2561
- return selectApplication(app.id);
2562
- }
2563
- async function promptForApplication(leadIn = []) {
2564
- const store = useWizard.getState();
2565
- const apps = await listApplications();
2566
- if (apps.length === 0) {
2567
- throw new Error(
2568
- "This Algolia account has no applications. Create one in the Algolia dashboard, or with `npx @algolia/cli@latest application create`, then restart the wizard."
2569
- );
2570
- }
2571
- if (apps.length === 1) {
2572
- const only = apps[0];
2573
- logger.info(
2574
- { app: only.id },
2575
- "single application on the account; selecting it"
2576
- );
2577
- for (const line of leadIn) store.pushCliOutput("stdout", line);
2578
- return selectAndReport(only);
2579
- }
2580
- const messages = [
2581
- ...leadIn,
2582
- "Which Algolia application should the wizard work in?"
2583
- ];
2584
- for (; ; ) {
2585
- const choice = await store.requestUserInput({
2586
- prompt: "Select an application",
2587
- promptType: "multipleChoice",
2588
- options: apps.map(labelFor),
2589
- secondary: apps.map(secondaryFor),
2590
- messages
2591
- });
2592
- const chosen = apps.find((app) => labelFor(app) === choice);
2593
- if (!chosen) {
2594
- throw new Error("Application picker received an unexpected selection");
2595
- }
2596
- try {
2597
- return await selectAndReport(chosen);
2598
- } catch (err) {
2599
- logger.warn(
2600
- { app: chosen.id, err: err.message },
2601
- "application select failed; re-prompting"
2602
- );
2603
- messages.push(
2604
- `Could not select \u201C${labelFor(chosen)}\u201D. It may have been removed \u2014 pick another.`
2605
- );
2606
- }
2607
- }
2608
- }
2609
- async function confirmEnvApplication(env, current) {
2610
- const useEnv = `Use ${env.id} (from ${env.file})`;
2611
- const choice = await useWizard.getState().requestUserInput({
2612
- prompt: "Select an application",
2613
- promptType: "multipleChoice",
2614
- options: [
2615
- useEnv,
2616
- current ? `Use ${labelFor(current)} (already selected)` : "Pick a different application"
2617
- ],
2618
- messages: [
2619
- `${env.file} already sets ${env.name}=${env.id}.`,
2620
- "Which Algolia application should the wizard work in?"
2621
- ]
2622
- });
2623
- return choice === useEnv;
2624
- }
2625
- async function selectEnvApplication(env) {
2626
- try {
2627
- return await selectAndReport({ id: env.id, name: "" });
2628
- } catch (err) {
2629
- logger.warn(
2630
- { app: env.id, err: err.message },
2631
- "could not select the application named in env; falling back to the picker"
2632
- );
2633
- return promptForApplication([
2634
- `Could not select ${env.id} from ${env.file} \u2014 it may have been removed, or this account may not have access to it.`
2635
- ]);
2636
- }
2637
- }
2638
- async function ensureApplication() {
2639
- const current = await currentApplication();
2640
- const env = await findEnvApplicationId();
2641
- if (env && env.id !== current?.id && await confirmEnvApplication(env, current)) {
2642
- return selectEnvApplication(env);
2643
- }
2644
- return current ?? await promptForApplication();
2645
- }
2646
-
2647
- // src/workflows/default.ts
2648
- import { z as z29 } from "zod";
2649
-
2650
- // src/actions/listIndices.ts
2651
- import { z as z5 } from "zod";
2652
- var indicesListSchema = z5.object({
2653
- items: z5.array(
2654
- z5.object({
2655
- name: z5.string(),
2656
- entries: z5.number().default(0)
2657
- })
2658
- )
2659
- });
2660
- async function listIndices() {
2661
- const stdout = await runAlgoliaCli(["indices", "list", "-o", "json"]);
2662
- const { items } = indicesListSchema.parse(JSON.parse(stdout));
2663
- return items.map((i) => ({ name: i.name, entries: i.entries })).sort((a, b) => a.name.localeCompare(b.name));
1597
+ async function listIndices() {
1598
+ const stdout = await runAlgoliaCli(["indices", "list", "-o", "json"]);
1599
+ const { items } = indicesListSchema.parse(JSON.parse(stdout));
1600
+ return items.map((i) => ({ name: i.name, entries: i.entries })).sort((a, b) => a.name.localeCompare(b.name));
2664
1601
  }
2665
1602
 
2666
1603
  // src/actions/selectIndex.ts
@@ -2715,8 +1652,8 @@ var selectIndexStep = async (ctx) => {
2715
1652
  };
2716
1653
 
2717
1654
  // src/lib/agent.ts
2718
- import { ToolLoopAgent, hasToolCall, Output as Output2 } from "ai";
2719
- import { createAnthropic as createAnthropic2 } from "@ai-sdk/anthropic";
1655
+ import { ToolLoopAgent, hasToolCall, Output as Output3 } from "ai";
1656
+ import { createAnthropic as createAnthropic3 } from "@ai-sdk/anthropic";
2720
1657
  import "zod";
2721
1658
 
2722
1659
  // src/lib/tools/index.ts
@@ -2724,12 +1661,12 @@ import "zod";
2724
1661
 
2725
1662
  // src/lib/tools/listFiles.ts
2726
1663
  import { tool } from "ai";
2727
- import z6 from "zod";
1664
+ import z5 from "zod";
2728
1665
  import { readdir } from "node:fs/promises";
2729
1666
 
2730
1667
  // src/lib/tools/path.ts
2731
1668
  import { lstat } from "node:fs/promises";
2732
- import { resolve as resolve2, relative, isAbsolute, dirname as dirname3, join as join7, sep } from "node:path";
1669
+ import { resolve as resolve2, relative, isAbsolute, dirname as dirname2, join as join5, sep } from "node:path";
2733
1670
  function resolveInRoot(ctx, path) {
2734
1671
  const target = resolve2(ctx.cwd, path);
2735
1672
  const rel = relative(ctx.root, target);
@@ -2743,9 +1680,9 @@ function resolveInRoot(ctx, path) {
2743
1680
  }
2744
1681
  async function hasSymlinkParent(ctx, target) {
2745
1682
  let current = ctx.root;
2746
- const parts = relative(ctx.root, dirname3(target)).split(sep).filter(Boolean);
1683
+ const parts = relative(ctx.root, dirname2(target)).split(sep).filter(Boolean);
2747
1684
  for (const part of parts) {
2748
- current = join7(current, part);
1685
+ current = join5(current, part);
2749
1686
  try {
2750
1687
  if ((await lstat(current)).isSymbolicLink()) return true;
2751
1688
  } catch (err) {
@@ -2760,8 +1697,8 @@ async function hasSymlinkParent(ctx, target) {
2760
1697
  function listFilesTool(ctx) {
2761
1698
  return tool({
2762
1699
  description: 'List files in a directory (default: the current working directory). Pass path to list a subdirectory directly \u2014 e.g. "packages/api" \u2014 without first changeDirectory-ing into it.',
2763
- inputSchema: z6.object({
2764
- path: z6.string().optional().describe("Directory to list, relative to cwd (default: cwd)")
1700
+ inputSchema: z5.object({
1701
+ path: z5.string().optional().describe("Directory to list, relative to cwd (default: cwd)")
2765
1702
  }),
2766
1703
  execute: async ({ path = "." }) => {
2767
1704
  logger.info({ path }, "called listFiles tool");
@@ -2782,13 +1719,13 @@ function listFilesTool(ctx) {
2782
1719
 
2783
1720
  // src/lib/tools/changeDirectory.ts
2784
1721
  import { tool as tool2 } from "ai";
2785
- import z7 from "zod";
1722
+ import z6 from "zod";
2786
1723
  import { stat } from "node:fs/promises";
2787
1724
  function changeDirectoryTool(ctx) {
2788
1725
  return tool2({
2789
1726
  description: "Change the current working directory. Subsequent file operations resolve relative to it. Returns the new working directory.",
2790
- inputSchema: z7.object({
2791
- path: z7.string().describe("Directory to change into")
1727
+ inputSchema: z6.object({
1728
+ path: z6.string().describe("Directory to change into")
2792
1729
  }),
2793
1730
  execute: async ({ path }) => {
2794
1731
  logger.info({ path }, "called changeDirectory tool");
@@ -2810,13 +1747,13 @@ function changeDirectoryTool(ctx) {
2810
1747
 
2811
1748
  // src/lib/tools/reportStatus.ts
2812
1749
  import { tool as tool3 } from "ai";
2813
- import z8 from "zod";
1750
+ import z7 from "zod";
2814
1751
  function reportStatusTool(output) {
2815
1752
  return tool3({
2816
1753
  description: "Report the status of your execution. Return a reason in case of failure.",
2817
- inputSchema: z8.object({
2818
- status: z8.enum(["success", "fail"]),
2819
- reason: z8.string().optional(),
1754
+ inputSchema: z7.object({
1755
+ status: z7.enum(["success", "fail"]),
1756
+ reason: z7.string().optional(),
2820
1757
  output
2821
1758
  }),
2822
1759
  execute: async ({ status, reason, output: output2 }) => {
@@ -2828,8 +1765,8 @@ function reportStatusTool(output) {
2828
1765
 
2829
1766
  // src/lib/tools/readFile.ts
2830
1767
  import { tool as tool4 } from "ai";
2831
- import z9 from "zod";
2832
- import { readFile as readFile4 } from "node:fs/promises";
1768
+ import z8 from "zod";
1769
+ import { readFile as readFile3 } from "node:fs/promises";
2833
1770
 
2834
1771
  // src/lib/tools/env.ts
2835
1772
  import { basename } from "node:path";
@@ -2856,8 +1793,8 @@ function redactEnvValues(content) {
2856
1793
  function readFileTool(ctx) {
2857
1794
  return tool4({
2858
1795
  description: "Read the contents of a file at the given path",
2859
- inputSchema: z9.object({
2860
- filePath: z9.string().describe("Path to the file to read")
1796
+ inputSchema: z8.object({
1797
+ filePath: z8.string().describe("Path to the file to read")
2861
1798
  }),
2862
1799
  execute: async ({ filePath }) => {
2863
1800
  if (++ctx.counts.read > ctx.limits.read) {
@@ -2867,7 +1804,7 @@ function readFileTool(ctx) {
2867
1804
  const resolved2 = resolveInRoot(ctx, filePath);
2868
1805
  if (!resolved2.ok) return resolved2.error;
2869
1806
  try {
2870
- const content = await readFile4(resolved2.target, "utf8");
1807
+ const content = await readFile3(resolved2.target, "utf8");
2871
1808
  return isEnvFile(resolved2.target) ? redactEnvValues(content) : content;
2872
1809
  } catch (err) {
2873
1810
  return `Error reading ${filePath}: ${err.message}`;
@@ -2878,15 +1815,15 @@ function readFileTool(ctx) {
2878
1815
 
2879
1816
  // src/lib/tools/writeFile.ts
2880
1817
  import { tool as tool5 } from "ai";
2881
- import z10 from "zod";
1818
+ import z9 from "zod";
2882
1819
  import { mkdir as mkdir3, writeFile as writeFile3 } from "node:fs/promises";
2883
- import { dirname as dirname4 } from "node:path";
1820
+ import { dirname as dirname3 } from "node:path";
2884
1821
  function writeFileTool(ctx) {
2885
1822
  return tool5({
2886
1823
  description: "Write content to a file at the given path, overwriting it. To set Algolia credentials in an env file, use writeCredentials instead of this tool.",
2887
- inputSchema: z10.object({
2888
- filePath: z10.string().describe("Path to the file to write"),
2889
- content: z10.string().describe("Content to write to the file")
1824
+ inputSchema: z9.object({
1825
+ filePath: z9.string().describe("Path to the file to write"),
1826
+ content: z9.string().describe("Content to write to the file")
2890
1827
  }),
2891
1828
  execute: async ({ filePath, content }) => {
2892
1829
  logger.info({ filePath }, "called writeFile tool");
@@ -2899,7 +1836,7 @@ function writeFileTool(ctx) {
2899
1836
  if (await hasSymlinkParent(ctx, resolved2.target)) {
2900
1837
  return `Refused: ${resolved2.target} is outside the repo root (${ctx.root}).`;
2901
1838
  }
2902
- await mkdir3(dirname4(resolved2.target), { recursive: true });
1839
+ await mkdir3(dirname3(resolved2.target), { recursive: true });
2903
1840
  await writeFile3(resolved2.target, content, "utf8");
2904
1841
  useWizard.getState().recordWrittenFile(resolved2.target);
2905
1842
  return `Wrote to ${filePath}`;
@@ -2913,8 +1850,70 @@ function writeFileTool(ctx) {
2913
1850
  // src/lib/tools/writeAlgoliaCredentials.ts
2914
1851
  import { tool as tool6 } from "ai";
2915
1852
  import z13 from "zod";
2916
- import { mkdir as mkdir4, readFile as readFile6, writeFile as writeFile5 } from "node:fs/promises";
2917
- import { dirname as dirname5, relative as relative3 } from "node:path";
1853
+ import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile5 } from "node:fs/promises";
1854
+ import { dirname as dirname4, relative as relative3 } from "node:path";
1855
+
1856
+ // src/lib/algoliaApp.ts
1857
+ import { z as z10 } from "zod";
1858
+ var applicationSchema = z10.object({
1859
+ id: z10.string().min(1),
1860
+ name: z10.string().default(""),
1861
+ plan: z10.string().optional()
1862
+ });
1863
+ var listSchema = z10.array(
1864
+ z10.object({
1865
+ id: z10.string().min(1),
1866
+ name: z10.string().default(""),
1867
+ plan_label: z10.string().optional()
1868
+ }).transform(({ id, name, plan_label }) => ({ id, name, plan: plan_label }))
1869
+ );
1870
+ async function currentApplication() {
1871
+ let raw;
1872
+ try {
1873
+ raw = await runAlgoliaCli(["application", "current", "-o", "json"]);
1874
+ } catch {
1875
+ return null;
1876
+ }
1877
+ const parsed = applicationSchema.safeParse(parseJson(raw));
1878
+ return parsed.success ? parsed.data : null;
1879
+ }
1880
+ async function requireApplication() {
1881
+ const app = await currentApplication();
1882
+ if (!app) {
1883
+ throw new Error(
1884
+ "No Algolia application is selected. Run `npx @algolia/cli@latest application select` and restart the wizard."
1885
+ );
1886
+ }
1887
+ return app;
1888
+ }
1889
+ async function listApplications() {
1890
+ const raw = await runAlgoliaCli(["application", "list", "-o", "json"]);
1891
+ const parsed = listSchema.safeParse(parseJson(raw));
1892
+ if (!parsed.success) {
1893
+ throw new Error("Could not read the list of Algolia applications.");
1894
+ }
1895
+ return parsed.data;
1896
+ }
1897
+ async function selectApplication(id) {
1898
+ const raw = await runAlgoliaCli(
1899
+ ["application", "select", "--non-interactive", "--app-id", id],
1900
+ { onOutput: stderrSink }
1901
+ );
1902
+ const parsed = applicationSchema.safeParse(parseJson(raw));
1903
+ if (!parsed.success) {
1904
+ throw new Error(
1905
+ `Selected application ${id}, but the Algolia CLI returned an unreadable result.`
1906
+ );
1907
+ }
1908
+ return parsed.data;
1909
+ }
1910
+ function parseJson(text) {
1911
+ try {
1912
+ return JSON.parse(text);
1913
+ } catch {
1914
+ return void 0;
1915
+ }
1916
+ }
2918
1917
 
2919
1918
  // src/lib/algoliaApiKey.ts
2920
1919
  import { z as z12 } from "zod";
@@ -3097,8 +2096,8 @@ async function resolveSearchOnlyKey(index, appId) {
3097
2096
 
3098
2097
  // src/lib/gitignore.ts
3099
2098
  import { execFile } from "node:child_process";
3100
- import { lstat as lstat2, readFile as readFile5, writeFile as writeFile4 } from "node:fs/promises";
3101
- import { join as join8, relative as relative2 } from "node:path";
2099
+ import { lstat as lstat2, readFile as readFile4, writeFile as writeFile4 } from "node:fs/promises";
2100
+ import { join as join6, relative as relative2 } from "node:path";
3102
2101
  var GIT_ENV_OVERRIDES = [
3103
2102
  "GIT_DIR",
3104
2103
  "GIT_WORK_TREE",
@@ -3150,7 +2149,7 @@ async function ensureGitIgnored(root, target) {
3150
2149
  if (ignoredByRule === void 0) return "unknown";
3151
2150
  if (ignoredByRule) return tracked ? "tracked" : "covered";
3152
2151
  const pattern = relative2(root, target);
3153
- const gitIgnore = join8(root, ".gitignore");
2152
+ const gitIgnore = join6(root, ".gitignore");
3154
2153
  try {
3155
2154
  const link = await lstat2(gitIgnore).catch(() => null);
3156
2155
  if (link?.isSymbolicLink()) {
@@ -3160,7 +2159,7 @@ async function ensureGitIgnored(root, target) {
3160
2159
  );
3161
2160
  return "unknown";
3162
2161
  }
3163
- const existing = link ? await readFile5(gitIgnore, "utf8") : "";
2162
+ const existing = link ? await readFile4(gitIgnore, "utf8") : "";
3164
2163
  const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
3165
2164
  await writeFile4(gitIgnore, `${existing}${prefix}${pattern}
3166
2165
  `, "utf8");
@@ -3229,7 +2228,7 @@ function writeCredentialsTool(ctx) {
3229
2228
  return `Refused: ${resolved2.target} is outside the repo root (${ctx.root}).`;
3230
2229
  }
3231
2230
  try {
3232
- existing = await readFile6(resolved2.target, "utf8");
2231
+ existing = await readFile5(resolved2.target, "utf8");
3233
2232
  } catch (err) {
3234
2233
  if (err.code !== "ENOENT") throw err;
3235
2234
  }
@@ -3279,7 +2278,7 @@ function writeCredentialsTool(ctx) {
3279
2278
  (content, [name, value]) => upsertEnv(content, name, value),
3280
2279
  existing
3281
2280
  );
3282
- await mkdir4(dirname5(resolved2.target), { recursive: true });
2281
+ await mkdir4(dirname4(resolved2.target), { recursive: true });
3283
2282
  await writeFile5(resolved2.target, updated, "utf8");
3284
2283
  const sentences = [
3285
2284
  `Wrote ${[...credentials.map(([name]) => name), INDEX_NAME_VAR].join(", ")} to ${filePath}.`
@@ -3319,8 +2318,8 @@ async function gitIgnoreOutcome(ctx, target) {
3319
2318
  // src/lib/tools/searchFiles.ts
3320
2319
  import { tool as tool7 } from "ai";
3321
2320
  import z14 from "zod";
3322
- import { readdir as readdir2, readFile as readFile7 } from "node:fs/promises";
3323
- import { join as join9 } from "node:path";
2321
+ import { readdir as readdir2, readFile as readFile6 } from "node:fs/promises";
2322
+ import { join as join7 } from "node:path";
3324
2323
  var MAX_QUERY_LENGTH = 1e3;
3325
2324
  var SKIP_DIRS = /* @__PURE__ */ new Set([
3326
2325
  "node_modules",
@@ -3335,7 +2334,7 @@ async function walkFiles(dir) {
3335
2334
  const out = [];
3336
2335
  for (const e of await readdir2(dir, { withFileTypes: true })) {
3337
2336
  if (e.name.startsWith(".") || SKIP_DIRS.has(e.name)) continue;
3338
- const full = join9(dir, e.name);
2337
+ const full = join7(dir, e.name);
3339
2338
  if (e.isDirectory()) out.push(...await walkFiles(full));
3340
2339
  else if (e.isFile()) out.push(full);
3341
2340
  }
@@ -3368,7 +2367,7 @@ function searchFilesTool(ctx) {
3368
2367
  for (const file of await walkFiles(resolved2.target)) {
3369
2368
  let content;
3370
2369
  try {
3371
- content = await readFile7(file, "utf8");
2370
+ content = await readFile6(file, "utf8");
3372
2371
  } catch {
3373
2372
  continue;
3374
2373
  }
@@ -3389,7 +2388,8 @@ function searchFilesTool(ctx) {
3389
2388
  }
3390
2389
 
3391
2390
  // src/lib/tools/runShell.ts
3392
- import { tool as tool8 } from "ai";
2391
+ import { tool as tool8, generateText, Output } from "ai";
2392
+ import { createAnthropic } from "@ai-sdk/anthropic";
3393
2393
  import z15 from "zod";
3394
2394
  import { relative as relative4 } from "node:path";
3395
2395
 
@@ -3498,8 +2498,10 @@ function runShell(command, opts) {
3498
2498
  // src/lib/tools/runShell.ts
3499
2499
  function storeApproval(root) {
3500
2500
  return async (req) => {
2501
+ const store = useWizard.getState();
2502
+ if (store.isCommandApproved(req.command, req.cwd)) return "approve";
3501
2503
  const rel = relative4(root, req.cwd);
3502
- const answer = await useWizard.getState().requestUserInput({
2504
+ const answer = await store.requestUserInput({
3503
2505
  prompt: "Run this command?",
3504
2506
  promptType: "commandApproval",
3505
2507
  options: [],
@@ -3508,20 +2510,195 @@ function storeApproval(root) {
3508
2510
  cwd: rel === "" || rel.startsWith("..") ? req.cwd : rel
3509
2511
  }
3510
2512
  });
3511
- return answer === "approve" ? "approve" : "reject";
2513
+ if (answer !== "approve") return "reject";
2514
+ store.approveCommand(req.command, req.cwd);
2515
+ return "approve";
3512
2516
  };
3513
2517
  }
3514
2518
  var EXPLORATORY_COMMANDS = /* @__PURE__ */ new Set(["ls", "find", "tree", "dir"]);
2519
+ function commandSegments(command) {
2520
+ return command.split(/&&|;|\|/).map((segment) => segment.trim());
2521
+ }
3515
2522
  function isExploratoryCommand(command) {
3516
- return command.split(/&&|;|\|/).map((segment) => segment.trim().split(/\s+/)[0]).some((word) => word !== void 0 && EXPLORATORY_COMMANDS.has(word));
2523
+ return commandSegments(command).map((segment) => segment.split(/\s+/)[0]).some((word) => word !== void 0 && EXPLORATORY_COMMANDS.has(word));
2524
+ }
2525
+ var READ_ONLY_BINARIES = /* @__PURE__ */ new Set([
2526
+ "cat",
2527
+ "head",
2528
+ "tail",
2529
+ "wc",
2530
+ "pwd",
2531
+ "echo",
2532
+ "date",
2533
+ "whoami",
2534
+ "hostname",
2535
+ "uname",
2536
+ "which",
2537
+ "file",
2538
+ "stat",
2539
+ "grep",
2540
+ "egrep",
2541
+ "fgrep",
2542
+ "rg",
2543
+ "diff"
2544
+ ]);
2545
+ var READ_ONLY_NO_ARGS_BINARIES = /* @__PURE__ */ new Set(["env", "printenv"]);
2546
+ var GIT_READ_ONLY_SUBCOMMANDS = /* @__PURE__ */ new Set([
2547
+ "status",
2548
+ "log",
2549
+ "diff",
2550
+ "show",
2551
+ "describe",
2552
+ "blame",
2553
+ "ls-files",
2554
+ "rev-parse",
2555
+ "cat-file",
2556
+ "shortlog",
2557
+ "ls-remote"
2558
+ ]);
2559
+ var VERSION_CHECK_BINARIES = /* @__PURE__ */ new Set([
2560
+ "node",
2561
+ "tsc",
2562
+ "npm",
2563
+ "pnpm",
2564
+ "yarn",
2565
+ "python",
2566
+ "python3",
2567
+ "ruby",
2568
+ "go",
2569
+ "cargo",
2570
+ "rustc",
2571
+ "php",
2572
+ "composer",
2573
+ "java",
2574
+ "mvn",
2575
+ "gradle",
2576
+ "bundle",
2577
+ "git"
2578
+ ]);
2579
+ var VERSION_FLAGS = /* @__PURE__ */ new Set(["--version", "-v", "-V"]);
2580
+ var FD_DUP_REDIRECT = /\d*>&\d+/g;
2581
+ function stripQuoted(segment) {
2582
+ let result = "";
2583
+ let quote = null;
2584
+ let i = 0;
2585
+ while (i < segment.length) {
2586
+ const char = segment[i];
2587
+ if (quote === "'") {
2588
+ if (char === "'") quote = null;
2589
+ i++;
2590
+ } else if (char === "\\") {
2591
+ i += 2;
2592
+ } else if (quote) {
2593
+ if (char === quote) quote = null;
2594
+ i++;
2595
+ } else if (char === '"' || char === "'") {
2596
+ quote = char;
2597
+ i++;
2598
+ } else {
2599
+ result += char;
2600
+ i++;
2601
+ }
2602
+ }
2603
+ return quote === null ? result : segment;
3517
2604
  }
3518
- async function approveAndRun(ctx, command, cwd, explanation) {
3519
- const decision = await ctx.shell.approve({ command, cwd, explanation });
3520
- if (decision === "reject") {
3521
- ctx.shell.executions.push({ command, cwd, approved: false });
3522
- logger.info({ command }, "runShell: user rejected the command");
3523
- return "The user rejected this command. Do not retry it. Propose a different command, or report the limitation via reportStatus.";
2605
+ function hasFileRedirect(segment) {
2606
+ return stripQuoted(segment.replace(FD_DUP_REDIRECT, "")).includes(">");
2607
+ }
2608
+ function hasShellInjectionRisk(segment) {
2609
+ if (segment.includes("$(") || segment.includes("<(") || segment.includes("`")) {
2610
+ return true;
2611
+ }
2612
+ return stripQuoted(segment.replace(FD_DUP_REDIRECT, "")).includes("&");
2613
+ }
2614
+ function fastPathSegmentSafety(segment) {
2615
+ if (!segment) return true;
2616
+ if (hasFileRedirect(segment)) return false;
2617
+ if (hasShellInjectionRisk(segment)) return false;
2618
+ const [cmd0, ...rest] = segment.split(/\s+/);
2619
+ if (cmd0 === void 0) return true;
2620
+ if (VERSION_CHECK_BINARIES.has(cmd0) && rest.length === 1 && VERSION_FLAGS.has(rest[0])) {
2621
+ return true;
2622
+ }
2623
+ if (READ_ONLY_BINARIES.has(cmd0)) return true;
2624
+ if (READ_ONLY_NO_ARGS_BINARIES.has(cmd0)) {
2625
+ return rest.length === 0 ? true : void 0;
2626
+ }
2627
+ if (cmd0 === "git") {
2628
+ return GIT_READ_ONLY_SUBCOMMANDS.has(rest[0] ?? "") ? true : void 0;
2629
+ }
2630
+ return void 0;
2631
+ }
2632
+ function fastPathSafety(command) {
2633
+ const results = commandSegments(command).map(fastPathSegmentSafety);
2634
+ if (results.some((r) => r === false)) return false;
2635
+ if (results.every((r) => r === true)) return true;
2636
+ return void 0;
2637
+ }
2638
+ var CLASSIFIER_MODEL = "claude-haiku-4-5";
2639
+ var commandSafetySchema = z15.object({
2640
+ safe: z15.boolean(),
2641
+ reason: z15.string().describe("One short sentence explaining the verdict.")
2642
+ });
2643
+ function defaultCreateModel() {
2644
+ const token = getAuthToken();
2645
+ if (!token) {
2646
+ throw new Error("Not authenticated: no user token available");
2647
+ }
2648
+ return createAnthropic({
2649
+ apiKey: token,
2650
+ baseURL: PROXY_BASE_URL,
2651
+ fetch: proxyFetch
2652
+ });
2653
+ }
2654
+ function approvedCommandHistory(approvedCommands) {
2655
+ return Array.from(approvedCommands).map((entry) => {
2656
+ const sep2 = entry.indexOf("\0");
2657
+ return { cwd: entry.slice(0, sep2), command: entry.slice(sep2 + 1) };
2658
+ });
2659
+ }
2660
+ async function classifyCommandSafety(createModel, command, cwd, explanation, approvedHistory) {
2661
+ try {
2662
+ const anthropic = createModel();
2663
+ const { output } = await generateText({
2664
+ model: anthropic(CLASSIFIER_MODEL),
2665
+ temperature: 0,
2666
+ output: Output.object({ schema: commandSafetySchema }),
2667
+ prompt: [
2668
+ "A coding agent wants to run this shell command in a user's project without asking for approval first. The command may be in any programming language or ecosystem.",
2669
+ "Judge it SAFE only if it cannot modify, delete, move, rename, or install anything the project cares about, cannot push/publish/deploy/commit anything, and cannot make a network request that changes remote state.",
2670
+ "Three broad categories are safe, and most commands you will see fall into one of them:",
2671
+ "(1) Reporting or listing existing state, with no side effect \u2014 e.g. `git status`, `npm ls`, `npm outdated`, `pip show`, `pip freeze`, `cargo tree`, `docker ps`, `docker images`, `kubectl get pods`, `terraform state list`. This includes a read-only GET-style query to a remote registry or API that only fetches public metadata and changes nothing remote (e.g. `npm view <package>`, `pip index versions <package>`, `curl` with no -X/--request other than GET and no -d/--data) \u2014 safe because nothing changes, not because it stays local.",
2672
+ '(2) A "preview" or "check" mode that reports what a change WOULD do, or reports a problem, without making the change. This is a general pattern, not a fixed list \u2014 ANY command that takes a dry-run/check/plan/validate/diff/list-only flag is safe when that flag is present, regardless of whether the same flag would normally appear on a "safe" kind of command: `terraform plan`, `terraform validate`, `terraform fmt -check`, `black --check`, `prettier --check`, `isort --check`, `gofmt -l` (list-only), `stylelint` with no `--fix`, and equally `git push --dry-run`, `npm publish --dry-run`, `kubectl apply --dry-run=client` \u2014 these last three are otherwise-unsafe operations (push, publish, cluster changes) that the dry-run flag turns into a report. The corresponding command WITHOUT that flag (e.g. `terraform apply`, `black .`, `gofmt -w`, `git push`) is a different, unsafe command \u2014 the flag is what makes the difference, for any tool, not just the ones named here. This cuts both ways, so do not assume a bare invocation is the safe one: `cargo fmt`, `black`, `prettier`, `isort`, `rustfmt`, and `terraform fmt` all REWRITE FILES by default and need an explicit check/diff/dry-run flag to become safe, while `gofmt`, `eslint`, `stylelint`, `rubocop`, and `ruff check` default to a safe check-only mode and need an explicit fix/write flag to become unsafe \u2014 the same-looking bare command is safe for one group and unsafe for the other, so judge each by what its own flags actually say, not by resemblance to a tool you already judged.',
2673
+ "(3) Running the project's own tests, type checker, linter, or build/compile step (e.g. `npm run build`, `yarn build`, `vite build`, `webpack`, `tsc`, `cargo build`, `go build`, `mvn compile`), as long as it is not passed an autofix/write/update flag (e.g. --fix, -u, --write, rubocop -a) \u2014 this holds even though the point of a build step is to write compiled output to a build/dist/target directory inside the project (e.g. a target/, build/, dist/, or __pycache__ directory): writing that output does not make the command unsafe. It stops being safe the moment the command chain goes past building \u2014 a step that also deploys, publishes, uploads, or pushes the build (e.g. `next build && vercel deploy`, `npm run build && npm publish`) is unsafe for that later segment even though the build segment itself is fine; judge each chained segment on its own, same as elsewhere in this list.",
2674
+ "Deleting or removing anything is unsafe, even something in a cache or build directory (e.g. `rm -rf __pycache__`, `cargo clean`, `git clean`), and even if the command otherwise fits one of the three categories above. Installing, uninstalling, or upgrading a dependency, changing a database schema, or writing to a path outside the project is also unsafe. A command chained with && or ; is only safe if every part of the chain is safe on its own.",
2675
+ "One specific, narrow exception to all of the above: when the command literally invokes one of these runner programs BY NAME as the leading command \u2014 npx, bunx, pnpm dlx, yarn dlx, pipx run, uvx \u2014 it is unsafe regardless of what it then runs, even something that looks like a harmless test/lint/typecheck command (e.g. `npx tsc --noEmit`, `uvx ruff check .`), because that runner can fetch and execute a different, unreviewed version of a package from a registry each time. This exception is about that specific syntax, not a general doubt about whether a tool is installed: a plain `ruff check .`, `pytest`, or any other bare command name is NOT this exception merely because you cannot verify from the string alone that it is installed \u2014 judge those the same as any other already-installed project tool, per the categories above. If you are unsure, judge it unsafe.",
2676
+ ...approvedHistory.length > 0 ? [
2677
+ "The user has already explicitly approved these exact commands earlier in this session (working directory in parentheses, then the command):",
2678
+ approvedHistory.map((h) => `- (${h.cwd}) ${h.command}`).join("\n"),
2679
+ "If the new command performs the same action and side-effect profile as one of these \u2014 differing only in a trivial way, such as a different file path, package name, or argument value that does not change what kind of action it is \u2014 judge it SAFE on that precedent, even if it would not otherwise fit categories (1)-(3) above. Do not stretch this to a command that merely shares a binary name, or superficially resembles one on the list while doing something riskier or of a different kind (e.g. an approved `rm build/tmp.log` does not license a new `rm -rf src/`, and an approved `git push origin feature-x` does not license `git push --force`)."
2680
+ ] : [],
2681
+ `Command: ${command}`,
2682
+ `Working directory: ${cwd}`,
2683
+ `Stated purpose: ${explanation}`
2684
+ ].join("\n")
2685
+ });
2686
+ if (!output.safe) {
2687
+ logger.info(
2688
+ { command, reason: output.reason },
2689
+ "runShell: classifier judged command unsafe, requiring approval"
2690
+ );
2691
+ }
2692
+ return output.safe;
2693
+ } catch (err) {
2694
+ logger.warn(
2695
+ { err, command },
2696
+ "runShell: command safety classifier failed, requiring approval"
2697
+ );
2698
+ return false;
3524
2699
  }
2700
+ }
2701
+ async function runAndRecord(ctx, command, cwd) {
3525
2702
  useWizard.getState().pushNotice({ messages: [`Running: ${command}`] });
3526
2703
  const env = await ctx.shell.env().catch((err) => {
3527
2704
  logger.warn({ err, command }, "runShell: could not resolve command env");
@@ -3557,9 +2734,18 @@ async function approveAndRun(ctx, command, cwd, explanation) {
3557
2734
  output: run2.output
3558
2735
  };
3559
2736
  }
3560
- function runShellTool(ctx) {
2737
+ async function approveAndRun(ctx, command, cwd, explanation) {
2738
+ const decision = await ctx.shell.approve({ command, cwd, explanation });
2739
+ if (decision === "reject") {
2740
+ ctx.shell.executions.push({ command, cwd, approved: false });
2741
+ logger.info({ command }, "runShell: user rejected the command");
2742
+ return "The user rejected this command. Do not retry it. Propose a different command, or report the limitation via reportStatus.";
2743
+ }
2744
+ return runAndRecord(ctx, command, cwd);
2745
+ }
2746
+ function runShellTool(ctx, createModel = defaultCreateModel) {
3561
2747
  return tool8({
3562
- description: "Run a shell command in the project. Use this for anything the project needs done in its own ecosystem: installing dependencies, running a script you wrote, running the project's lint/typecheck/test commands. To inspect the project, use listFiles or searchFiles instead of ls/find \u2014 this tool refuses those. The user sees and approves every command before it runs, so write a clear `explanation`. If the user rejects a command, do not retry it \u2014 propose a different approach.",
2748
+ description: "Run a shell command in the project. Use this for anything the project needs done in its own ecosystem: installing dependencies, running a script you wrote, running the project's lint/typecheck/test commands. Do not use this to find or read files \u2014 use listFiles, searchFiles, and readFile instead of ls/find/cat/head/tail/grep/rg. This tool refuses ls/find/tree/dir outright; a read command like cat is not refused (some read-only commands run without approval, see below), but it's still the wrong tool for reading a file \u2014 the dedicated tools exist for that and won't count against this tool's command budget. The user approves any command that could change the project before it runs, so write a clear `explanation`. A command judged read-only (inspection, or running tests/typecheck/lint without an autofix flag, in any language) runs immediately without approval. If the user rejects a command, do not retry it \u2014 propose a different approach.",
3563
2749
  inputSchema: z15.object({
3564
2750
  command: z15.string().describe(
3565
2751
  "The command to run, exactly as it would be typed in a shell. Pipes, && and redirects are allowed."
@@ -3578,9 +2764,29 @@ function runShellTool(ctx) {
3578
2764
  const resolved2 = resolveInRoot(ctx, cwd ?? ".");
3579
2765
  if (!resolved2.ok) return resolved2.error;
3580
2766
  if (isExploratoryCommand(command)) {
3581
- return "Refused: use listFiles or searchFiles to inspect the project instead of ls/find/tree.";
2767
+ return "Refused: use listFiles or searchFiles to find files, and readFile to read one, instead of ls/find/tree/dir.";
3582
2768
  }
3583
2769
  logger.info({ command, cwd: resolved2.target }, "called runShell tool");
2770
+ const fast = fastPathSafety(command);
2771
+ let isSafe = fast;
2772
+ if (isSafe === void 0) {
2773
+ const store = useWizard.getState();
2774
+ isSafe = store.isCommandApproved(command, resolved2.target);
2775
+ if (!isSafe) {
2776
+ isSafe = await classifyCommandSafety(
2777
+ createModel,
2778
+ command,
2779
+ resolved2.target,
2780
+ explanation,
2781
+ approvedCommandHistory(store.approvedCommands)
2782
+ );
2783
+ }
2784
+ }
2785
+ if (isSafe) {
2786
+ return serializePrompt(
2787
+ () => runAndRecord(ctx, command, resolved2.target)
2788
+ );
2789
+ }
3584
2790
  return serializePrompt(
3585
2791
  () => approveAndRun(ctx, command, resolved2.target, explanation)
3586
2792
  );
@@ -3630,29 +2836,29 @@ function reviewScriptTool(ctx) {
3630
2836
  }
3631
2837
 
3632
2838
  // src/lib/tools/generateRecord.ts
3633
- import { tool as tool10, generateText, Output, NoObjectGeneratedError } from "ai";
3634
- import { createAnthropic } from "@ai-sdk/anthropic";
2839
+ import { tool as tool10, generateText as generateText2, Output as Output2, NoObjectGeneratedError } from "ai";
2840
+ import { createAnthropic as createAnthropic2 } from "@ai-sdk/anthropic";
3635
2841
  import { nanoid as nanoid2 } from "nanoid";
3636
2842
  import { mkdir as mkdir5, writeFile as writeFile6 } from "node:fs/promises";
3637
- import { dirname as dirname6 } from "node:path";
2843
+ import { dirname as dirname5 } from "node:path";
3638
2844
  import z17 from "zod";
3639
2845
  var DATA_DIR = ".algolia-wizard/data";
3640
2846
  var RECORD_MODEL = "claude-haiku-4-5";
3641
2847
  var MAX_RECORDS = 100;
3642
2848
  var BATCH_SIZE = 10;
3643
2849
  var MAX_BATCH_ATTEMPTS = 3;
3644
- function defaultCreateModel() {
2850
+ function defaultCreateModel2() {
3645
2851
  const token = getAuthToken();
3646
2852
  if (!token) {
3647
2853
  throw new Error("Not authenticated: no user token available");
3648
2854
  }
3649
- return createAnthropic({
2855
+ return createAnthropic2({
3650
2856
  apiKey: token,
3651
2857
  baseURL: PROXY_BASE_URL,
3652
2858
  fetch: proxyFetch
3653
2859
  });
3654
2860
  }
3655
- function generateRecordTool(ctx, createModel = defaultCreateModel) {
2861
+ function generateRecordTool(ctx, createModel = defaultCreateModel2) {
3656
2862
  return tool10({
3657
2863
  description: "Generate realistic sample records for an entity and write them to a JSON file. Provide the entity name and its attributes; this tool asks a model to invent varied, realistic values, each with a unique objectID, and returns the file path to read them from at runtime. Do not invent the record values or objectIDs yourself, and do not inline the returned records into the script \u2014 call this tool and read the file it writes.",
3658
2864
  inputSchema: z17.object({
@@ -3673,9 +2879,9 @@ function generateRecordTool(ctx, createModel = defaultCreateModel) {
3673
2879
  let lastError;
3674
2880
  for (let attempt = 1; attempt <= MAX_BATCH_ATTEMPTS; attempt++) {
3675
2881
  try {
3676
- const { output } = await generateText({
2882
+ const { output } = await generateText2({
3677
2883
  model: anthropic(RECORD_MODEL),
3678
- output: Output.object({
2884
+ output: Output2.object({
3679
2885
  schema: z17.object({
3680
2886
  records: z17.array(recordSchema).length(batchCount)
3681
2887
  })
@@ -3714,7 +2920,7 @@ function generateRecordTool(ctx, createModel = defaultCreateModel) {
3714
2920
  if (await hasSymlinkParent(ctx, resolved2.target)) {
3715
2921
  return `Refused: ${resolved2.target} is outside the repo root (${ctx.root}).`;
3716
2922
  }
3717
- await mkdir5(dirname6(resolved2.target), { recursive: true });
2923
+ await mkdir5(dirname5(resolved2.target), { recursive: true });
3718
2924
  await writeFile6(
3719
2925
  resolved2.target,
3720
2926
  JSON.stringify(records, null, 2),
@@ -3840,7 +3046,7 @@ async function runAgentAttempt(req, attempt) {
3840
3046
  if (!token) {
3841
3047
  throw new Error("Not authenticated: no user token available");
3842
3048
  }
3843
- const anthropic = createAnthropic2({
3049
+ const anthropic = createAnthropic3({
3844
3050
  apiKey: token,
3845
3051
  baseURL: PROXY_BASE_URL,
3846
3052
  fetch: proxyFetch
@@ -3877,7 +3083,7 @@ async function runAgentAttempt(req, attempt) {
3877
3083
  }
3878
3084
  };
3879
3085
  }),
3880
- output: Output2.object({ schema: req.outputSchema }),
3086
+ output: Output3.object({ schema: req.outputSchema }),
3881
3087
  tools: createTools(toolContext, {
3882
3088
  output: req.outputSchema,
3883
3089
  tools: req.tools
@@ -4061,7 +3267,7 @@ async function runAnalysis(mode, extraInstructions = []) {
4061
3267
  // package.json
4062
3268
  var package_default = {
4063
3269
  name: "@algolia/wizard",
4064
- version: "0.34.0-rc.125.245",
3270
+ version: "0.35.0-rc.125.251",
4065
3271
  description: "Magically implement Algolia functionality in your codebase",
4066
3272
  type: "module",
4067
3273
  engines: {
@@ -4467,12 +3673,13 @@ ${formatCompletedSteps(ctx.completedSteps)}`,
4467
3673
 
4468
3674
  // src/actions/implement.ts
4469
3675
  import z28 from "zod";
4470
- import { relative as relative6, join as join12 } from "node:path";
3676
+ import { mkdir as mkdir7 } from "node:fs/promises";
3677
+ import { join as join10, relative as relative6 } from "node:path";
4471
3678
 
4472
3679
  // src/lib/git.ts
4473
3680
  import { execFile as execFile2 } from "node:child_process";
4474
3681
  import { copyFile, mkdir as mkdir6, stat as stat3 } from "node:fs/promises";
4475
- import { basename as basename2, dirname as dirname7, isAbsolute as isAbsolute2, join as join10, resolve as resolve3 } from "node:path";
3682
+ import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute2, join as join8, resolve as resolve3 } from "node:path";
4476
3683
  var MAX_BUFFER = 32 * 1024 * 1024;
4477
3684
  function git(args) {
4478
3685
  return new Promise((resolve4, reject) => {
@@ -4509,13 +3716,13 @@ async function copyUploadIntoProject(repoRoot, ingestDir, sourcePath) {
4509
3716
  } catch {
4510
3717
  return { ok: false, reason: `"${sourcePath}" does not exist` };
4511
3718
  }
4512
- const relPath = join10(ingestDir, basename2(source));
4513
- const dest = join10(repoRoot, relPath);
3719
+ const relPath = join8(ingestDir, basename2(source));
3720
+ const dest = join8(repoRoot, relPath);
4514
3721
  if (resolve3(source) === resolve3(dest)) {
4515
3722
  return { ok: true, relPath };
4516
3723
  }
4517
3724
  try {
4518
- await mkdir6(dirname7(dest), { recursive: true });
3725
+ await mkdir6(dirname6(dest), { recursive: true });
4519
3726
  await copyFile(source, dest);
4520
3727
  } catch (err) {
4521
3728
  return {
@@ -4549,15 +3756,15 @@ function toRootRelative(p) {
4549
3756
 
4550
3757
  // src/lib/algoliaDocs.ts
4551
3758
  import { readFileSync, readdirSync, existsSync } from "node:fs";
4552
- import { dirname as dirname8, join as join11 } from "node:path";
4553
- import { fileURLToPath as fileURLToPath2 } from "node:url";
4554
- var DOCS_SUBPATH = join11("docs", "algolia-sdk");
3759
+ import { dirname as dirname7, join as join9 } from "node:path";
3760
+ import { fileURLToPath } from "node:url";
3761
+ var DOCS_SUBPATH = join9("docs", "algolia-sdk");
4555
3762
  function findDocsDir() {
4556
- let dir = dirname8(fileURLToPath2(import.meta.url));
3763
+ let dir = dirname7(fileURLToPath(import.meta.url));
4557
3764
  for (; ; ) {
4558
- const candidate = join11(dir, DOCS_SUBPATH);
3765
+ const candidate = join9(dir, DOCS_SUBPATH);
4559
3766
  if (existsSync(candidate)) return candidate;
4560
- const parent = dirname8(dir);
3767
+ const parent = dirname7(dir);
4561
3768
  if (parent === dir) return void 0;
4562
3769
  dir = parent;
4563
3770
  }
@@ -4578,7 +3785,7 @@ function loadAlgoliaDoc(language) {
4578
3785
  );
4579
3786
  return "";
4580
3787
  }
4581
- return readFileSync(join11(docsDir, files[0]), "utf8").trim();
3788
+ return readFileSync(join9(docsDir, files[0]), "utf8").trim();
4582
3789
  }
4583
3790
  function getNamedDoc(name, language) {
4584
3791
  const docsDir = findDocsDir();
@@ -4586,7 +3793,7 @@ function getNamedDoc(name, language) {
4586
3793
  logger.warn("docs/algolia-sdk not found");
4587
3794
  return "";
4588
3795
  }
4589
- const file = join11(docsDir, `${name}-${language}.md`);
3796
+ const file = join9(docsDir, `${name}-${language}.md`);
4590
3797
  if (!existsSync(file)) {
4591
3798
  logger.warn({ name, language }, "named SDK reference not found");
4592
3799
  return "";
@@ -4714,7 +3921,7 @@ function algoliaClientDoc(input) {
4714
3921
  function ingestionInstructions(input) {
4715
3922
  return [
4716
3923
  ...input.confirmed && input.confirmed.length ? [
4717
- `Create an ingestion script under "${input.ingestDir}/" at the repo root.`,
3924
+ `Create an ingestion script under "${input.ingestDir}/" at the repo root. That directory already exists \u2014 writeFile creates any nested path itself, so never run a shell command just to create a directory.`,
4718
3925
  `Ingest only the confirmed entity (name, source paths, attributes): ${JSON.stringify(input.confirmed)}.`,
4719
3926
  `Ingesting writes to Algolia, so the script needs a write API key and App ID \u2014 read them from the ${API_KEY_VAR} and ${APP_ID_VAR} environment variables rather than hardcoding them. The wizard sets these when it runs the script.`,
4720
3927
  `Read the index name from the ${INDEX_NAME_VAR} environment variable, which the wizard sets to "${input.targetIndex}". Never hardcode an index name or derive one from the project, file, or entity name \u2014 the write key only works for that exact index. Exit with an error if ${INDEX_NAME_VAR} is unset.`,
@@ -4932,6 +4139,9 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES) {
4932
4139
  const targetIndex = selected?.selection;
4933
4140
  useWizard.getState().setTargetIndex(targetIndex ?? null);
4934
4141
  await assertGitRepoWithHead(repoRoot);
4142
+ if (useCases.includes("ingestion")) {
4143
+ await mkdir7(join10(repoRoot, INGEST_DIR), { recursive: true });
4144
+ }
4935
4145
  const normalized = normalizeFindingPaths(findings);
4936
4146
  const confirmed2 = normalized.confirmedEntities;
4937
4147
  const searchLocation = normalized.searchImplementationAnalysis;
@@ -5156,7 +4366,7 @@ ${detail}` : ""}`
5156
4366
  if (searchConfigFile) {
5157
4367
  const ignoreStatus = await gitIgnoreStatus(
5158
4368
  repoRoot,
5159
- join12(repoRoot, searchConfigFile)
4369
+ join10(repoRoot, searchConfigFile)
5160
4370
  );
5161
4371
  if (ignoreStatus === "covered") {
5162
4372
  summaries.push(
@@ -5202,6 +4412,7 @@ var defaultWorkflow = {
5202
4412
  id: "project-scan",
5203
4413
  title: "project scan",
5204
4414
  outputSchema: projectScanSchema,
4415
+ tips: ["apps-and-indices", "records-are-json", "speed"],
5205
4416
  run: (ctx) => {
5206
4417
  ctx.notify({
5207
4418
  messages: [
@@ -5237,6 +4448,7 @@ var defaultWorkflow = {
5237
4448
  id: "ingestion",
5238
4449
  title: "ingest records",
5239
4450
  outputSchema: implementSchema,
4451
+ tips: ["records-are-json", "searchable-attributes", "facets"],
5240
4452
  run: (ctx) => {
5241
4453
  ctx.notify({
5242
4454
  messages: [
@@ -5257,6 +4469,13 @@ var defaultWorkflow = {
5257
4469
  id: "search",
5258
4470
  title: "create search ui",
5259
4471
  outputSchema: implementSchema,
4472
+ tips: [
4473
+ "test-relevance",
4474
+ "search-analytics",
4475
+ "recommend",
4476
+ "searchable-attributes",
4477
+ "facets"
4478
+ ],
5260
4479
  run: (ctx) => {
5261
4480
  ctx.notify({
5262
4481
  messages: ["Building your Algolia search experience\u2026"]
@@ -5293,6 +4512,1045 @@ function getWorkflow(id) {
5293
4512
  return workflows[id];
5294
4513
  }
5295
4514
 
4515
+ // src/ui/Welcome.tsx
4516
+ import { dirname as dirname8, join as join11 } from "node:path";
4517
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
4518
+ import { useState as useState7 } from "react";
4519
+ import { Box as Box10, Spacer, Text as Text10, useInput as useInput3, useWindowSize as useWindowSize5 } from "ink";
4520
+
4521
+ // src/ui/copy/welcome.ts
4522
+ var sidebarItems = [
4523
+ {
4524
+ title: "scan your project",
4525
+ description: "detect models, schemas, and data worth indexing"
4526
+ },
4527
+ {
4528
+ title: "select and index",
4529
+ description: "push 100 records to Algolia in seconds"
4530
+ },
4531
+ {
4532
+ title: "detect your stack",
4533
+ description: "whatever language and framework you already use"
4534
+ },
4535
+ {
4536
+ title: "scaffold a search UI",
4537
+ description: "a search box and results, wired into your app"
4538
+ },
4539
+ {
4540
+ title: "ship it",
4541
+ description: "build check passes, search is live with your real data"
4542
+ }
4543
+ ];
4544
+
4545
+ // src/ui/Welcome.tsx
4546
+ import Image, { TerminalInfoContext, defaultTerminalInfo } from "ink-picture";
4547
+ import { jsx as jsx8, jsxs as jsxs9 } from "react/jsx-runtime";
4548
+ var IMAGE_PATH = join11(dirname8(fileURLToPath2(import.meta.url)), "algolia.png");
4549
+ var TERMINAL_INFO = {
4550
+ ...defaultTerminalInfo,
4551
+ supportsUnicode: true,
4552
+ supportsColor: true
4553
+ };
4554
+ function SidebarItem({
4555
+ title,
4556
+ description
4557
+ }) {
4558
+ return /* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", children: [
4559
+ /* @__PURE__ */ jsxs9(Box10, { gap: 1, children: [
4560
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.success, children: "\u2192" }),
4561
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.strong, bold: true, children: title })
4562
+ ] }),
4563
+ /* @__PURE__ */ jsxs9(Box10, { flexDirection: "row", gap: 2, children: [
4564
+ /* @__PURE__ */ jsx8(Spacer, {}),
4565
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.muted, children: description })
4566
+ ] })
4567
+ ] });
4568
+ }
4569
+ function Welcome() {
4570
+ const confirmStart = useWizard((s) => s.confirmStart);
4571
+ const openLearnMore = useWizard((s) => s.openLearnMore);
4572
+ const { rows } = useWindowSize5();
4573
+ const actions = [
4574
+ { label: "start wizard", run: confirmStart },
4575
+ { label: "learn more", run: openLearnMore }
4576
+ ];
4577
+ const [index, setIndex] = useState7(0);
4578
+ useInput3((input, key) => {
4579
+ if (key.upArrow || input === "k") {
4580
+ setIndex((i) => (i - 1 + actions.length) % actions.length);
4581
+ } else if (key.downArrow || input === "j") {
4582
+ setIndex((i) => (i + 1) % actions.length);
4583
+ } else if (key.return) {
4584
+ actions[index].run();
4585
+ }
4586
+ });
4587
+ const scales = {
4588
+ large: {
4589
+ sidebar: { padding: { x: 4, y: 2 }, gap: 2 },
4590
+ main: { padding: { x: 8, y: 4 } }
4591
+ },
4592
+ small: {
4593
+ sidebar: { padding: { x: 2, y: 1 }, gap: 1 },
4594
+ main: { padding: { x: 4, y: 2 } }
4595
+ }
4596
+ };
4597
+ let layout = scales["large"];
4598
+ if (rows < 30) {
4599
+ layout = scales["small"];
4600
+ }
4601
+ return /* @__PURE__ */ jsxs9(Box10, { flexDirection: "row", justifyContent: "space-between", width: "100%", children: [
4602
+ /* @__PURE__ */ jsx8(
4603
+ Box10,
4604
+ {
4605
+ paddingY: layout.main.padding.y,
4606
+ paddingX: layout.main.padding.x,
4607
+ flexDirection: "column",
4608
+ justifyContent: "center",
4609
+ children: /* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", gap: 2, children: [
4610
+ /* @__PURE__ */ jsx8(TerminalInfoContext.Provider, { value: TERMINAL_INFO, children: /* @__PURE__ */ jsx8(
4611
+ Image,
4612
+ {
4613
+ src: IMAGE_PATH,
4614
+ objectFit: "contain",
4615
+ alt: "Algolia",
4616
+ width: 20,
4617
+ height: 10,
4618
+ protocol: "halfBlock"
4619
+ }
4620
+ ) }),
4621
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.muted, children: "\u2726 From zero \u2192 working search in ~10 minutes" }),
4622
+ /* @__PURE__ */ jsx8(Box10, { flexDirection: "column", children: actions.map((action, i) => /* @__PURE__ */ jsx8(
4623
+ SelectRow,
4624
+ {
4625
+ highlighted: i === index,
4626
+ highlightBackground: false,
4627
+ label: action.label
4628
+ },
4629
+ action.label
4630
+ )) }),
4631
+ /* @__PURE__ */ jsxs9(Box10, { gap: 2, children: [
4632
+ /* @__PURE__ */ jsxs9(Text10, { children: [
4633
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.primary, children: "[\u2191] [\u2193]" }),
4634
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.dim, children: " move" })
4635
+ ] }),
4636
+ /* @__PURE__ */ jsxs9(Text10, { children: [
4637
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.primary, children: "[enter]" }),
4638
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.dim, children: " confirm" })
4639
+ ] })
4640
+ ] })
4641
+ ] })
4642
+ }
4643
+ ),
4644
+ /* @__PURE__ */ jsxs9(
4645
+ Box10,
4646
+ {
4647
+ backgroundColor: COLORS.bg.sidebar,
4648
+ width: 40,
4649
+ paddingY: layout.sidebar.padding.y,
4650
+ paddingX: layout.sidebar.padding.x,
4651
+ gap: layout.sidebar.gap,
4652
+ flexDirection: "column",
4653
+ justifyContent: "center",
4654
+ children: [
4655
+ /* @__PURE__ */ jsx8(Text10, { color: COLORS.muted, children: "WHAT THIS WIZARD WILL DO" }),
4656
+ sidebarItems.map((i, idx) => /* @__PURE__ */ jsx8(SidebarItem, { title: i.title, description: i.description }, idx))
4657
+ ]
4658
+ }
4659
+ )
4660
+ ] });
4661
+ }
4662
+
4663
+ // src/ui/LearnMore.tsx
4664
+ import { Fragment } from "react";
4665
+ import { Box as Box11, Text as Text11, useInput as useInput4, useWindowSize as useWindowSize6 } from "ink";
4666
+
4667
+ // src/ui/copy/learn-more.ts
4668
+ var accessIntro = "Everything runs locally on your machine. Nothing is written or sent without an explicit yes from you.";
4669
+ var accessItems = [
4670
+ {
4671
+ tag: "READ",
4672
+ title: "Project files",
4673
+ description: "reads manifests, configs & source to detect your stack. Read-only; nothing is uploaded."
4674
+ },
4675
+ {
4676
+ tag: "WRITE",
4677
+ title: "Code changes",
4678
+ description: "creates & edits files (search UI, config) directly in your branch."
4679
+ },
4680
+ {
4681
+ tag: "EXEC",
4682
+ title: "Setup commands",
4683
+ description: "runs dependency installs, the ingestion script & your own checks. Every command is shown in full and needs your OK; its output is shown as-is, so a command that prints a secret will display it."
4684
+ },
4685
+ {
4686
+ tag: "NET",
4687
+ title: "Algolia API",
4688
+ description: "sends index settings & the records you pick to your Algolia app over HTTPS."
4689
+ },
4690
+ {
4691
+ tag: "KEY",
4692
+ title: "Credentials",
4693
+ description: "writes your Algolia app id and a search-only key (safe to expose) to .env in your project."
4694
+ }
4695
+ ];
4696
+ var neverItems = [
4697
+ "Send your source code to a model or third party",
4698
+ "Commit or push to git",
4699
+ "Run a command you haven't approved"
4700
+ ];
4701
+ var policyLinks = [
4702
+ { label: "Terms", url: "https://www.algolia.com/policies/terms" },
4703
+ { label: "Privacy Policy", url: "https://www.algolia.com/policies/privacy" }
4704
+ ];
4705
+
4706
+ // src/ui/LearnMore.tsx
4707
+ import { jsx as jsx9, jsxs as jsxs10 } from "react/jsx-runtime";
4708
+ var TAG_COLORS = {
4709
+ READ: COLORS.success,
4710
+ WRITE: COLORS.badge,
4711
+ EXEC: COLORS.danger,
4712
+ NET: COLORS.accent,
4713
+ KEY: COLORS.muted
4714
+ };
4715
+ var TAG_COLUMN_WIDTH = 10;
4716
+ var PADDING_X = 6;
4717
+ var NEVER_BOX_PAD_X = 2;
4718
+ function NeverLine({
4719
+ width,
4720
+ segments = []
4721
+ }) {
4722
+ const used = segments.reduce((n, s) => n + s.text.length, 0);
4723
+ const rightPad = Math.max(0, width - 2 - NEVER_BOX_PAD_X - used);
4724
+ return /* @__PURE__ */ jsxs10(Text11, { color: COLORS.primary, children: [
4725
+ /* @__PURE__ */ jsx9(Text11, { color: COLORS.danger, children: "\u2502" }),
4726
+ " ".repeat(NEVER_BOX_PAD_X),
4727
+ segments.map((s, i) => /* @__PURE__ */ jsx9(Text11, { color: s.color, bold: s.bold, children: s.text }, i)),
4728
+ " ".repeat(rightPad),
4729
+ /* @__PURE__ */ jsx9(Text11, { color: COLORS.danger, children: "\u2502" })
4730
+ ] });
4731
+ }
4732
+ function LearnMore() {
4733
+ const confirmStart = useWizard((s) => s.confirmStart);
4734
+ const backToHome = useWizard((s) => s.backToHome);
4735
+ const { columns } = useWindowSize6();
4736
+ const dividerWidth = Math.max(0, columns - PADDING_X * 2);
4737
+ useInput4((_input, key) => {
4738
+ if (key.escape) backToHome();
4739
+ else if (key.return) confirmStart();
4740
+ });
4741
+ return /* @__PURE__ */ jsxs10(
4742
+ Box11,
4743
+ {
4744
+ flexDirection: "column",
4745
+ paddingX: PADDING_X,
4746
+ paddingY: 2,
4747
+ width: "100%",
4748
+ gap: 1,
4749
+ children: [
4750
+ /* @__PURE__ */ jsx9(Text11, { color: COLORS.strong, bold: true, children: "What algolia wizard accesses" }),
4751
+ /* @__PURE__ */ jsx9(Text11, { color: COLORS.muted, children: accessIntro }),
4752
+ /* @__PURE__ */ jsx9(Box11, { flexDirection: "column", children: accessItems.map((item) => /* @__PURE__ */ jsxs10(Box11, { flexDirection: "column", marginTop: 1, children: [
4753
+ /* @__PURE__ */ jsx9(Text11, { color: COLORS.border, children: "\u2500".repeat(dividerWidth) }),
4754
+ /* @__PURE__ */ jsxs10(Box11, { flexDirection: "row", gap: 1, marginTop: 1, children: [
4755
+ /* @__PURE__ */ jsx9(Box11, { width: TAG_COLUMN_WIDTH, flexShrink: 0, children: /* @__PURE__ */ jsx9(Text11, { color: TAG_COLORS[item.tag], bold: true, children: `[${item.tag}]` }) }),
4756
+ /* @__PURE__ */ jsx9(Box11, { flexDirection: "column", children: /* @__PURE__ */ jsxs10(Text11, { color: COLORS.primary, children: [
4757
+ /* @__PURE__ */ jsx9(Text11, { color: COLORS.strong, bold: true, children: item.title }),
4758
+ /* @__PURE__ */ jsx9(Text11, { color: COLORS.muted, children: ` \u2014 ${item.description}` })
4759
+ ] }) })
4760
+ ] })
4761
+ ] }, item.tag)) }),
4762
+ /* @__PURE__ */ jsxs10(Box11, { marginTop: 1, flexDirection: "column", children: [
4763
+ /* @__PURE__ */ jsx9(Text11, { color: COLORS.danger, children: `\u256D${"\u2500".repeat(Math.max(0, dividerWidth - 2))}\u256E` }),
4764
+ /* @__PURE__ */ jsx9(NeverLine, { width: dividerWidth }),
4765
+ /* @__PURE__ */ jsx9(
4766
+ NeverLine,
4767
+ {
4768
+ width: dividerWidth,
4769
+ segments: [{ text: "I NEVER", color: COLORS.danger, bold: true }]
4770
+ }
4771
+ ),
4772
+ neverItems.map((item) => /* @__PURE__ */ jsxs10(Fragment, { children: [
4773
+ /* @__PURE__ */ jsx9(NeverLine, { width: dividerWidth }),
4774
+ /* @__PURE__ */ jsx9(
4775
+ NeverLine,
4776
+ {
4777
+ width: dividerWidth,
4778
+ segments: [
4779
+ { text: "\u2715", color: COLORS.danger },
4780
+ { text: " " },
4781
+ { text: item, color: COLORS.primary }
4782
+ ]
4783
+ }
4784
+ )
4785
+ ] }, item)),
4786
+ /* @__PURE__ */ jsx9(NeverLine, { width: dividerWidth }),
4787
+ /* @__PURE__ */ jsx9(Text11, { color: COLORS.danger, children: `\u2570${"\u2500".repeat(Math.max(0, dividerWidth - 2))}\u256F` })
4788
+ ] }),
4789
+ /* @__PURE__ */ jsx9(Box11, { marginTop: 1, flexDirection: "column", children: policyLinks.map((link) => /* @__PURE__ */ jsxs10(Box11, { flexDirection: "row", gap: 1, children: [
4790
+ /* @__PURE__ */ jsx9(Text11, { color: COLORS.strong, bold: true, children: `${link.label}:` }),
4791
+ /* @__PURE__ */ jsx9(Text11, { color: COLORS.accent, children: link.url })
4792
+ ] }, link.label)) }),
4793
+ /* @__PURE__ */ jsxs10(Box11, { marginTop: 1, flexDirection: "row", gap: 3, children: [
4794
+ /* @__PURE__ */ jsxs10(Box11, { flexDirection: "row", gap: 1, children: [
4795
+ /* @__PURE__ */ jsx9(Text11, { color: COLORS.muted, children: "[" }),
4796
+ /* @__PURE__ */ jsx9(Text11, { color: COLORS.primary, children: "esc" }),
4797
+ /* @__PURE__ */ jsx9(Text11, { color: COLORS.muted, children: "] back" })
4798
+ ] }),
4799
+ /* @__PURE__ */ jsxs10(Box11, { flexDirection: "row", gap: 1, children: [
4800
+ /* @__PURE__ */ jsx9(Text11, { color: COLORS.muted, children: "[" }),
4801
+ /* @__PURE__ */ jsx9(Text11, { color: COLORS.primary, children: "enter" }),
4802
+ /* @__PURE__ */ jsx9(Text11, { color: COLORS.muted, children: "]" }),
4803
+ /* @__PURE__ */ jsx9(Text11, { color: COLORS.success, bold: true, children: "start wizard" })
4804
+ ] })
4805
+ ] })
4806
+ ]
4807
+ }
4808
+ );
4809
+ }
4810
+
4811
+ // src/ui/Sidebar.tsx
4812
+ import { Box as Box14, Text as Text14 } from "ink";
4813
+
4814
+ // src/ui/Steps.tsx
4815
+ import { Box as Box12, Text as Text12 } from "ink";
4816
+ import Spinner from "ink-spinner";
4817
+ import { jsx as jsx10, jsxs as jsxs11 } from "react/jsx-runtime";
4818
+ function Steps() {
4819
+ const { steps } = useWizard();
4820
+ const visibleSteps = steps.filter(isStepVisible);
4821
+ return /* @__PURE__ */ jsx10(Box12, { flexDirection: "column", gap: 1, children: visibleSteps.map((s) => /* @__PURE__ */ jsx10(Box12, { flexDirection: "column", children: /* @__PURE__ */ jsxs11(Text12, { color: COLORS.status[s.status], children: [
4822
+ s.status === "running" ? /* @__PURE__ */ jsx10(Spinner, { type: "dots" }) : MARKER[s.status],
4823
+ " ",
4824
+ s.title
4825
+ ] }) }, s.id)) });
4826
+ }
4827
+ function CurrentStep() {
4828
+ const { steps } = useWizard();
4829
+ const currentStep = steps.filter(isStepVisible).find((s) => s.status === "running");
4830
+ if (!currentStep) return null;
4831
+ return /* @__PURE__ */ jsxs11(Text12, { color: COLORS.status.running, children: [
4832
+ /* @__PURE__ */ jsx10(Spinner, { type: "dots" }),
4833
+ " ",
4834
+ ` ${currentStep.title}`
4835
+ ] });
4836
+ }
4837
+
4838
+ // src/ui/Progress.tsx
4839
+ import { Box as Box13, Text as Text13 } from "ink";
4840
+ import { jsx as jsx11, jsxs as jsxs12 } from "react/jsx-runtime";
4841
+ function Progress() {
4842
+ const { steps, currentStepIndex } = useWizard();
4843
+ const visibleSteps = steps.filter(isStepVisible);
4844
+ if (visibleSteps.length === 0) return null;
4845
+ const visibleCountThroughCurrent = steps.slice(0, currentStepIndex + 1).filter(isStepVisible).length;
4846
+ const activeStepNumber = Math.max(1, visibleCountThroughCurrent);
4847
+ return /* @__PURE__ */ jsxs12(Box13, { flexDirection: "row", gap: 1, children: [
4848
+ /* @__PURE__ */ jsx11(Text13, { color: COLORS.muted, children: "STEP" }),
4849
+ /* @__PURE__ */ jsx11(Text13, { color: COLORS.strong, bold: true, children: activeStepNumber }),
4850
+ /* @__PURE__ */ jsx11(Text13, { color: COLORS.strong, bold: true, children: "/" }),
4851
+ /* @__PURE__ */ jsx11(Text13, { color: COLORS.strong, bold: true, children: visibleSteps.length })
4852
+ ] });
4853
+ }
4854
+
4855
+ // src/ui/copy/sidebar-commands.ts
4856
+ var sidebarCommands = [
4857
+ { keyHint: "tab", description: "toggle logs" },
4858
+ { keyHint: "esc", description: "exit wizard" }
4859
+ ];
4860
+
4861
+ // src/ui/Sidebar.tsx
4862
+ import { jsx as jsx12, jsxs as jsxs13 } from "react/jsx-runtime";
4863
+ function Sidebar() {
4864
+ return /* @__PURE__ */ jsxs13(
4865
+ Box14,
4866
+ {
4867
+ backgroundColor: "#14171E",
4868
+ width: 30,
4869
+ paddingX: 4,
4870
+ paddingY: 2,
4871
+ flexDirection: "column",
4872
+ justifyContent: "space-between",
4873
+ children: [
4874
+ /* @__PURE__ */ jsxs13(Box14, { flexDirection: "column", gap: 1, children: [
4875
+ /* @__PURE__ */ jsx12(Text14, { color: COLORS.muted, children: "PROGRESS" }),
4876
+ /* @__PURE__ */ jsx12(Steps, {})
4877
+ ] }),
4878
+ /* @__PURE__ */ jsxs13(Box14, { flexDirection: "column", gap: 1, children: [
4879
+ /* @__PURE__ */ jsx12(Progress, {}),
4880
+ /* @__PURE__ */ jsx12(Box14, { flexDirection: "column", children: sidebarCommands.map((c) => {
4881
+ return /* @__PURE__ */ jsxs13(Box14, { flexDirection: "row", gap: 1, children: [
4882
+ /* @__PURE__ */ jsx12(Text14, { color: COLORS.primary, children: `[${c.keyHint}]` }),
4883
+ /* @__PURE__ */ jsx12(Text14, { color: COLORS.muted, children: c.description })
4884
+ ] });
4885
+ }) })
4886
+ ] })
4887
+ ]
4888
+ }
4889
+ );
4890
+ }
4891
+
4892
+ // src/ui/Ribbon.tsx
4893
+ import { Box as Box15, Text as Text15 } from "ink";
4894
+ import { jsx as jsx13, jsxs as jsxs14 } from "react/jsx-runtime";
4895
+ function Ribbon() {
4896
+ const firstCommand = sidebarCommands[0];
4897
+ return /* @__PURE__ */ jsxs14(
4898
+ Box15,
4899
+ {
4900
+ backgroundColor: "#14171E",
4901
+ flexDirection: "row",
4902
+ justifyContent: "space-between",
4903
+ paddingX: 2,
4904
+ paddingY: 1,
4905
+ children: [
4906
+ /* @__PURE__ */ jsx13(Progress, {}),
4907
+ /* @__PURE__ */ jsx13(CurrentStep, {}),
4908
+ /* @__PURE__ */ jsxs14(Box15, { flexDirection: "row", gap: 1, children: [
4909
+ /* @__PURE__ */ jsx13(Text15, { color: COLORS.primary, children: `[${firstCommand.keyHint}]` }),
4910
+ /* @__PURE__ */ jsx13(Text15, { color: COLORS.muted, children: firstCommand.description })
4911
+ ] })
4912
+ ]
4913
+ }
4914
+ );
4915
+ }
4916
+
4917
+ // src/ui/App.tsx
4918
+ import { useState as useState9 } from "react";
4919
+
4920
+ // src/ui/Logs.tsx
4921
+ import { Box as Box16, Text as Text16, useInput as useInput5 } from "ink";
4922
+ import { jsx as jsx14, jsxs as jsxs15 } from "react/jsx-runtime";
4923
+ var KIND_COLOR = {
4924
+ tool: COLORS.primary,
4925
+ prompt: COLORS.badge
4926
+ };
4927
+ var STATUS_COLOR = {
4928
+ running: COLORS.status.running,
4929
+ error: COLORS.danger
4930
+ };
4931
+ function logNameColor(entry) {
4932
+ return STATUS_COLOR[entry.status] ?? KIND_COLOR[entry.kind];
4933
+ }
4934
+ var ROW_GAP = 1;
4935
+ function truncate2(str, maxWidth) {
4936
+ if (maxWidth <= 0) return "";
4937
+ return str.length > maxWidth ? `${str.slice(0, maxWidth - 1)}\u2026` : str;
4938
+ }
4939
+ function rawInputText(input) {
4940
+ if (input === void 0) return "";
4941
+ const str = typeof input === "string" ? input : JSON.stringify(input);
4942
+ if (!str || str === "{}") return "";
4943
+ return str.replace(/\s+/g, " ").trim();
4944
+ }
4945
+ function formatTimestamp(ms) {
4946
+ const d = new Date(ms);
4947
+ const pad = (n) => String(n).padStart(2, "0");
4948
+ return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
4949
+ }
4950
+ function Logs() {
4951
+ const logs = useWizard((s) => s.logs);
4952
+ const scroll = useScrollWindow({ itemCount: logs.length, followBottom: true });
4953
+ useInput5((_input, key) => {
4954
+ if (key.upArrow) scroll.scrollBy(-1);
4955
+ else if (key.downArrow) scroll.scrollBy(1);
4956
+ });
4957
+ const visible = logs.slice(scroll.offset, scroll.offset + scroll.capacity);
4958
+ return /* @__PURE__ */ jsxs15(Box16, { flexDirection: "column", paddingX: 4, paddingY: 2, flexGrow: 1, children: [
4959
+ logs.length === 0 && /* @__PURE__ */ jsx14(Text16, { color: COLORS.dim, children: "No logs yet." }),
4960
+ /* @__PURE__ */ jsx14(ScrollView, { scroll, children: visible.map((entry) => {
4961
+ const timestamp = `[${formatTimestamp(entry.startedAt)}]`;
4962
+ const durationText = entry.kind === "tool" && entry.durationMs !== void 0 ? `${entry.durationMs}ms` : "";
4963
+ const rawPreview = rawInputText(entry.input);
4964
+ const partCount = 2 + (rawPreview ? 1 : 0) + (durationText ? 1 : 0);
4965
+ const gaps = (partCount - 1) * ROW_GAP;
4966
+ let budget = scroll.width - timestamp.length - durationText.length - gaps;
4967
+ const name = truncate2(entry.name, budget);
4968
+ budget -= name.length;
4969
+ const preview = rawPreview ? truncate2(rawPreview, budget) : "";
4970
+ return /* @__PURE__ */ jsxs15(Box16, { flexDirection: "row", gap: ROW_GAP, children: [
4971
+ /* @__PURE__ */ jsx14(Text16, { color: COLORS.dim, children: timestamp }),
4972
+ /* @__PURE__ */ jsx14(Text16, { color: logNameColor(entry), wrap: "truncate", children: name }),
4973
+ preview && /* @__PURE__ */ jsx14(Text16, { color: COLORS.dim, wrap: "truncate", children: preview }),
4974
+ durationText && /* @__PURE__ */ jsx14(Text16, { color: COLORS.dim, children: durationText })
4975
+ ] }, entry.id);
4976
+ }) }),
4977
+ /* @__PURE__ */ jsx14(Text16, { color: COLORS.dim, children: "\u2191/\u2193 scroll" })
4978
+ ] });
4979
+ }
4980
+
4981
+ // src/ui/Tips.tsx
4982
+ import { useEffect as useEffect3, useState as useState8 } from "react";
4983
+ import { Box as Box18, Text as Text18 } from "ink";
4984
+ import terminalLink from "terminal-link";
4985
+
4986
+ // src/ui/Code.tsx
4987
+ import { Box as Box17, Text as Text17 } from "ink";
4988
+ import { jsx as jsx15 } from "react/jsx-runtime";
4989
+ var TOKEN_RE = /("(?:\\.|[^"\\])*"|\btrue\b|\bfalse\b|\bnull\b|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|[{}[\]:,])/g;
4990
+ function highlightJson(json) {
4991
+ const parts = json.split(TOKEN_RE);
4992
+ return parts.map((part, i) => {
4993
+ if (!part) return null;
4994
+ if (part[0] === '"') {
4995
+ const next = parts.slice(i + 1).find((p) => p.trim());
4996
+ const isKey = next?.trimStart().startsWith(":");
4997
+ return /* @__PURE__ */ jsx15(Text17, { color: isKey ? "cyan" : "green", children: part }, i);
4998
+ }
4999
+ if (part === "true" || part === "false" || part === "null") {
5000
+ return /* @__PURE__ */ jsx15(Text17, { color: "magenta", children: part }, i);
5001
+ }
5002
+ if (/^-?\d/.test(part)) {
5003
+ return /* @__PURE__ */ jsx15(Text17, { color: "yellow", children: part }, i);
5004
+ }
5005
+ if (/^[{}[\]:,]$/.test(part)) {
5006
+ return /* @__PURE__ */ jsx15(Text17, { dimColor: true, children: part }, i);
5007
+ }
5008
+ return /* @__PURE__ */ jsx15(Text17, { children: part }, i);
5009
+ });
5010
+ }
5011
+ function Code({ children }) {
5012
+ return /* @__PURE__ */ jsx15(Box17, { children: /* @__PURE__ */ jsx15(Text17, { children: highlightJson(children) }) });
5013
+ }
5014
+
5015
+ // src/ui/copy/tips.ts
5016
+ var tips = [
5017
+ {
5018
+ id: "apps-and-indices",
5019
+ title: "An Application is like your library. Indices are the books in it.",
5020
+ chunks: [
5021
+ {
5022
+ type: "text",
5023
+ value: "Applications hold your API keys and Indices."
5024
+ },
5025
+ {
5026
+ type: "text",
5027
+ value: "Each index is a searchable collection of records (think: products, articles, orders, concerts)."
5028
+ }
5029
+ ]
5030
+ },
5031
+ {
5032
+ id: "records-are-json",
5033
+ title: "A Record is a JSON object inside of an index.",
5034
+ chunks: [
5035
+ {
5036
+ type: "text",
5037
+ value: "Every object you index is just a JSON document with a unique objectID."
5038
+ },
5039
+ {
5040
+ type: "text",
5041
+ value: "No fixed schema, no hard requirements."
5042
+ },
5043
+ {
5044
+ type: "text",
5045
+ value: "Everything else is up to you:"
5046
+ },
5047
+ {
5048
+ type: "code",
5049
+ value: JSON.stringify(
5050
+ {
5051
+ objectID: "sku_48213",
5052
+ name: "Trail Running Shoe",
5053
+ brand: "Northline",
5054
+ categories: ["Footwear", "Running", "Mens"],
5055
+ price: 129.99,
5056
+ in_stock: true,
5057
+ rating: 4.6
5058
+ },
5059
+ null,
5060
+ 2
5061
+ )
5062
+ }
5063
+ ]
5064
+ },
5065
+ {
5066
+ id: "speed",
5067
+ title: "Speed isn't a feature, it's our business",
5068
+ chunks: [
5069
+ {
5070
+ type: "text",
5071
+ value: "Algolia's engine processes most search queries in 1 to 50 milliseconds."
5072
+ },
5073
+ {
5074
+ type: "text",
5075
+ value: "This is why our search-as-you-type experience feels instant."
5076
+ }
5077
+ ]
5078
+ },
5079
+ {
5080
+ id: "searchable-attributes",
5081
+ title: "Searchable attributes are your relevance dial #1",
5082
+ chunks: [
5083
+ {
5084
+ type: "text",
5085
+ value: "Order matters. Attributes listed first in"
5086
+ },
5087
+ { type: "codeword", value: "searchableAttributes" },
5088
+ { type: "text", value: "carry more ranking weight." },
5089
+ {
5090
+ type: "text",
5091
+ value: "This is the single highest-leverage lever new users don't know exists."
5092
+ }
5093
+ ]
5094
+ },
5095
+ {
5096
+ id: "facets",
5097
+ title: "Facets aren't just filters, they help build your UI",
5098
+ chunks: [
5099
+ { type: "codeword", value: "attributesForFaceting" },
5100
+ {
5101
+ type: "text",
5102
+ value: "unlock category sidebars, price sliders, tag clouds without extra backend work."
5103
+ }
5104
+ ]
5105
+ },
5106
+ {
5107
+ id: "test-relevance",
5108
+ title: "Test relevance in the dashboard before you write a line of ranking code",
5109
+ chunks: [
5110
+ {
5111
+ type: "text",
5112
+ value: "Our Search dashboard has a live preview where you can browse results."
5113
+ },
5114
+ {
5115
+ type: "text",
5116
+ value: "Tune your settings and see how it alters your results in"
5117
+ },
5118
+ {
5119
+ type: "link",
5120
+ value: "real-time.",
5121
+ href: "https://dashboard.algolia.com/explorer/browse"
5122
+ }
5123
+ ]
5124
+ },
5125
+ {
5126
+ id: "search-analytics",
5127
+ title: "Search Analytics helps you discover opportunities",
5128
+ chunks: [
5129
+ {
5130
+ type: "text",
5131
+ value: "No click results for a particular query? Low click-through rates for another?"
5132
+ },
5133
+ {
5134
+ type: "text",
5135
+ value: "Our Search Analytics will help you identify synonyms, rules or other relevancy settings to improve your results."
5136
+ }
5137
+ ]
5138
+ },
5139
+ {
5140
+ id: "recommend",
5141
+ title: "Let Algolia act as your recommendation engine",
5142
+ chunks: [
5143
+ {
5144
+ type: "text",
5145
+ value: "Beyond search, Algolia Recommend runs models trained on your existing indices and event data to power your recommendation engine."
5146
+ },
5147
+ {
5148
+ type: "text",
5149
+ value: "You can improve engagement with related, popular or visually similar items."
5150
+ }
5151
+ ]
5152
+ }
5153
+ ];
5154
+ var tipsById = Object.fromEntries(
5155
+ tips.map((tip) => [tip.id, tip])
5156
+ );
5157
+ function tipsByIds(ids) {
5158
+ return ids.map((id) => tipsById[id]).filter((tip) => tip !== void 0);
5159
+ }
5160
+
5161
+ // src/ui/Tips.tsx
5162
+ import { jsx as jsx16, jsxs as jsxs16 } from "react/jsx-runtime";
5163
+ var TICK_MS = 16;
5164
+ var CHUNK_HOLD_MS = 2e3;
5165
+ var HOLD_MS = 8e3;
5166
+ function TipTitle({ children }) {
5167
+ return /* @__PURE__ */ jsxs16(Box18, { gap: 1, children: [
5168
+ /* @__PURE__ */ jsx16(Text18, { color: "cyan", children: "\u2726" }),
5169
+ /* @__PURE__ */ jsx16(Text18, { color: "white", bold: true, children })
5170
+ ] });
5171
+ }
5172
+ function InlineSegment({ segment }) {
5173
+ switch (segment.type) {
5174
+ case "highlight":
5175
+ return /* @__PURE__ */ jsx16(Text18, { color: COLORS.success, children: segment.value });
5176
+ case "link":
5177
+ return /* @__PURE__ */ jsx16(Text18, { color: "cyan", underline: true, children: segment.href ? terminalLink(segment.value, segment.href) : segment.value });
5178
+ case "codeword":
5179
+ return /* @__PURE__ */ jsx16(Text18, { color: COLORS.highlight.fg, backgroundColor: COLORS.highlight.bg, children: segment.value });
5180
+ default:
5181
+ return /* @__PURE__ */ jsx16(Text18, { color: COLORS.muted, children: segment.value });
5182
+ }
5183
+ }
5184
+ function TipContent({
5185
+ chunks,
5186
+ revealed
5187
+ }) {
5188
+ let remaining = revealed;
5189
+ const slices = chunks.map((chunk) => {
5190
+ const slice = chunk.value.slice(0, Math.max(0, remaining));
5191
+ remaining -= chunk.value.length;
5192
+ return slice;
5193
+ });
5194
+ const blocks = [];
5195
+ for (let i = 0; i < chunks.length; i++) {
5196
+ const chunk = chunks[i];
5197
+ const slice = slices[i];
5198
+ if (!slice) continue;
5199
+ const segment = {
5200
+ type: chunk.type,
5201
+ value: slice,
5202
+ href: chunk.type === "link" ? chunk.href : void 0
5203
+ };
5204
+ if (chunk.type === "code") {
5205
+ blocks.push({ type: "code", segment });
5206
+ continue;
5207
+ }
5208
+ const last = blocks[blocks.length - 1];
5209
+ if (last?.type === "inline") {
5210
+ last.segments.push(segment);
5211
+ } else {
5212
+ blocks.push({ type: "inline", segments: [segment] });
5213
+ }
5214
+ }
5215
+ return /* @__PURE__ */ jsx16(Box18, { flexDirection: "column", marginLeft: 2, gap: 1, children: blocks.map(
5216
+ (block, i) => block.type === "code" ? /* @__PURE__ */ jsx16(Code, { children: block.segment.value }, i) : /* @__PURE__ */ jsx16(Text18, { children: block.segments.map((segment, j) => /* @__PURE__ */ jsxs16(Text18, { children: [
5217
+ j > 0 && " ",
5218
+ /* @__PURE__ */ jsx16(InlineSegment, { segment })
5219
+ ] }, j)) }, i)
5220
+ ) });
5221
+ }
5222
+ var progress = { stepId: null, tipIndex: 0, revealed: 0 };
5223
+ function Tips({ stepId, ids }) {
5224
+ const tips2 = tipsByIds(ids);
5225
+ const resuming = progress.stepId === stepId;
5226
+ if (!resuming) {
5227
+ progress.stepId = stepId;
5228
+ progress.tipIndex = 0;
5229
+ progress.revealed = 0;
5230
+ }
5231
+ const [tipIndex, setTipIndex] = useState8(resuming ? progress.tipIndex : 0);
5232
+ const [revealed, setRevealed] = useState8(resuming ? progress.revealed : 0);
5233
+ const tip = tips2.length > 0 ? tips2[tipIndex % tips2.length] : void 0;
5234
+ const contentLength = tip?.chunks.reduce((sum, c) => sum + c.value.length, 0) ?? 0;
5235
+ const totalLength = (tip?.title.length ?? 0) + contentLength;
5236
+ const segments = tip ? [
5237
+ { type: "title", length: tip.title.length },
5238
+ ...tip.chunks.map((c) => ({ type: c.type, length: c.value.length }))
5239
+ ] : [];
5240
+ const noPauseAfterText = [
5241
+ "highlight",
5242
+ "link",
5243
+ "codeword"
5244
+ ];
5245
+ const chunkBoundaries = [];
5246
+ let cumulative = 0;
5247
+ for (let i = 0; i < segments.length - 1; i++) {
5248
+ cumulative += segments[i].length;
5249
+ const skipPause = noPauseAfterText.includes(
5250
+ segments[i + 1].type
5251
+ );
5252
+ if (!skipPause) chunkBoundaries.push(cumulative);
5253
+ }
5254
+ useEffect3(() => {
5255
+ if (tips2.length === 0) return;
5256
+ if (revealed < totalLength) {
5257
+ const delay2 = chunkBoundaries.includes(revealed) ? CHUNK_HOLD_MS : TICK_MS;
5258
+ const timer2 = setTimeout(() => {
5259
+ setRevealed((r) => {
5260
+ const next = r + 1;
5261
+ progress.revealed = next;
5262
+ return next;
5263
+ });
5264
+ }, delay2);
5265
+ return () => clearTimeout(timer2);
5266
+ }
5267
+ const timer = setTimeout(() => {
5268
+ setTipIndex((i) => {
5269
+ const next = (i + 1) % tips2.length;
5270
+ progress.tipIndex = next;
5271
+ return next;
5272
+ });
5273
+ setRevealed(0);
5274
+ progress.revealed = 0;
5275
+ }, HOLD_MS);
5276
+ return () => clearTimeout(timer);
5277
+ }, [revealed, totalLength, tips2.length]);
5278
+ if (!tip) return null;
5279
+ const titleRevealed = tip.title.slice(0, revealed);
5280
+ const contentRevealed = Math.max(0, revealed - tip.title.length);
5281
+ return /* @__PURE__ */ jsxs16(Box18, { flexDirection: "column", marginBottom: 1, gap: 1, children: [
5282
+ /* @__PURE__ */ jsx16(TipTitle, { children: titleRevealed }),
5283
+ /* @__PURE__ */ jsx16(TipContent, { chunks: tip.chunks, revealed: contentRevealed })
5284
+ ] });
5285
+ }
5286
+
5287
+ // src/ui/App.tsx
5288
+ import { jsx as jsx17, jsxs as jsxs17 } from "react/jsx-runtime";
5289
+ function App() {
5290
+ const {
5291
+ phase,
5292
+ error,
5293
+ homeScreen,
5294
+ currentStepIndex,
5295
+ steps,
5296
+ inputReq,
5297
+ user,
5298
+ workflow
5299
+ } = useWizard();
5300
+ const { exit } = useApp();
5301
+ const { columns, rows } = useWindowSize7();
5302
+ const [showLogs, setShowLogs] = useState9(false);
5303
+ const finished = phase === "done" || phase === "error";
5304
+ const currentStep = steps[currentStepIndex];
5305
+ const stepDef = workflow ? getWorkflow(workflow.id)?.steps.find((s) => s.id === currentStep?.id) : void 0;
5306
+ const tipIds = stepDef?.tips ?? [];
5307
+ const stepHasTips = tipIds.length > 0;
5308
+ const isAwaitingUserInput = phase === "awaitingInput" && !!inputReq;
5309
+ const isCommandApprovalPrompt = isAwaitingUserInput && inputReq?.promptType === "commandApproval";
5310
+ const showTips = stepHasTips && (phase === "running" || isCommandApprovalPrompt);
5311
+ const showNoticesInMain = !stepHasTips;
5312
+ const showNotices = !isAwaitingUserInput;
5313
+ useInput6(
5314
+ (_input, key) => {
5315
+ if (key.return) {
5316
+ exit();
5317
+ }
5318
+ },
5319
+ { isActive: finished }
5320
+ );
5321
+ useInput6((_input, key) => {
5322
+ if (phase === "idle" || phase === "authenticating") return;
5323
+ if (key.tab) {
5324
+ setShowLogs(!showLogs);
5325
+ track("AI Wizard Interaction", {
5326
+ context: "global",
5327
+ key: "tab",
5328
+ currentStep: currentStep?.id
5329
+ });
5330
+ }
5331
+ });
5332
+ const escOwnedElsewhere = phase === "idle" || phase === "authenticating" || phase === "awaitingInput" && (inputReq?.promptType === "enterToContinue" || inputReq?.promptType === "commandApproval");
5333
+ useInput6((_input, key) => {
5334
+ if (escOwnedElsewhere) return;
5335
+ if (key.escape) {
5336
+ track("AI Wizard Interaction", {
5337
+ context: "global",
5338
+ key: "esc",
5339
+ currentStep: currentStep?.id ?? phase
5340
+ });
5341
+ exit();
5342
+ }
5343
+ });
5344
+ const mainWindowVisible = phase === "authenticating" || phase === "preflight" || phase === "running" || phase === "awaitingInput" || phase === "error" || phase === "done";
5345
+ const flexDirection = columns > 90 ? "row" : "column";
5346
+ const showSidebar = flexDirection === "row";
5347
+ const scrollsPastViewport = phase === "idle" && homeScreen === "learnMore";
5348
+ return (
5349
+ /* Clamped to exactly the viewport: a taller frame makes Ink clear and repaint
5350
+ the whole screen, and the scrolling throws off its cursor arithmetic —
5351
+ flicker and leftover rows. */
5352
+ /* @__PURE__ */ jsxs17(
5353
+ Box19,
5354
+ {
5355
+ backgroundColor: COLORS.bg.main,
5356
+ flexDirection: "row",
5357
+ width: columns,
5358
+ height: scrollsPastViewport ? void 0 : rows,
5359
+ overflow: scrollsPastViewport ? "visible" : "hidden",
5360
+ children: [
5361
+ mainWindowVisible && /* @__PURE__ */ jsxs17(
5362
+ Box19,
5363
+ {
5364
+ flexDirection,
5365
+ width: "100%",
5366
+ maxHeight: rows,
5367
+ justifyContent: "space-between",
5368
+ children: [
5369
+ showLogs ? /* @__PURE__ */ jsx17(Logs, {}) : /* @__PURE__ */ jsxs17(
5370
+ Box19,
5371
+ {
5372
+ flexDirection: "column",
5373
+ paddingX: 4,
5374
+ paddingY: 2,
5375
+ width: showSidebar ? 70 : "100%",
5376
+ flexGrow: 1,
5377
+ gap: 1,
5378
+ children: [
5379
+ /* @__PURE__ */ jsxs17(Box19, { flexGrow: 2, flexDirection: "column", children: [
5380
+ phase === "preflight" && !user && /* @__PURE__ */ jsx17(Box19, { flexDirection: "column", marginBottom: 1, children: /* @__PURE__ */ jsxs17(Text19, { color: COLORS.strong, bold: true, children: [
5381
+ /* @__PURE__ */ jsx17(Spinner2, { type: "dots" }),
5382
+ " Signing in to Algolia"
5383
+ ] }) }),
5384
+ phase === "preflight" && user && /* @__PURE__ */ jsx17(Box19, { flexDirection: "column", marginBottom: 1, children: /* @__PURE__ */ jsxs17(Text19, { color: COLORS.strong, bold: true, children: [
5385
+ /* @__PURE__ */ jsx17(Spinner2, { type: "dots" }),
5386
+ " Getting things ready"
5387
+ ] }) }),
5388
+ phase === "authenticating" && /* @__PURE__ */ jsxs17(Box19, { flexDirection: "column", marginBottom: 1, children: [
5389
+ /* @__PURE__ */ jsxs17(Text19, { color: COLORS.strong, bold: true, children: [
5390
+ /* @__PURE__ */ jsx17(Spinner2, { type: "dots" }),
5391
+ " Signing in to Algolia"
5392
+ ] }),
5393
+ /* @__PURE__ */ jsx17(Text19, { color: COLORS.muted, children: "A browser window will open \u2014 complete sign-in there." })
5394
+ ] }),
5395
+ /* @__PURE__ */ jsx17(CliOutput, {}),
5396
+ showTips && currentStep && /* @__PURE__ */ jsx17(Tips, { stepId: currentStep.id, ids: tipIds }),
5397
+ showNoticesInMain && showNotices && /* @__PURE__ */ jsx17(Notices, { showAll: true, border: false }),
5398
+ !showTips && /* @__PURE__ */ jsx17(PromptInput, {}),
5399
+ phase === "error" && error && /* @__PURE__ */ jsx17(Box19, { marginTop: 1, children: /* @__PURE__ */ jsxs17(Text19, { color: COLORS.status.error, children: [
5400
+ "\u2716 ",
5401
+ error
5402
+ ] }) })
5403
+ ] }),
5404
+ !showNoticesInMain && showNotices && /* @__PURE__ */ jsx17(Notices, {}),
5405
+ showTips && /* @__PURE__ */ jsx17(PromptInput, {})
5406
+ ]
5407
+ }
5408
+ ),
5409
+ showSidebar ? /* @__PURE__ */ jsx17(Sidebar, {}) : /* @__PURE__ */ jsx17(Ribbon, {})
5410
+ ]
5411
+ }
5412
+ ),
5413
+ phase === "idle" && (homeScreen === "learnMore" ? /* @__PURE__ */ jsx17(LearnMore, {}) : /* @__PURE__ */ jsx17(Welcome, {}))
5414
+ ]
5415
+ }
5416
+ )
5417
+ );
5418
+ }
5419
+
5420
+ // src/lib/envAppId.ts
5421
+ import { readFile as readFile7 } from "node:fs/promises";
5422
+ import { join as join12 } from "node:path";
5423
+ var ENV_FILES = [".env", ".env.local"];
5424
+ var APP_ID_LINE = /^[ \t]*(?:export[ \t]+)?([A-Z0-9_]*ALGOLIA_APP(?:LICATION)?_ID)[ \t]*=[ \t]*(.*)$/gm;
5425
+ async function findEnvApplicationId(root = process.cwd()) {
5426
+ for (const file of ENV_FILES) {
5427
+ let content;
5428
+ try {
5429
+ content = await readFile7(join12(root, file), "utf8");
5430
+ } catch (err) {
5431
+ if (err.code !== "ENOENT") {
5432
+ logger.warn(
5433
+ { file, err },
5434
+ "could not read env file for an application id"
5435
+ );
5436
+ }
5437
+ continue;
5438
+ }
5439
+ for (const [, name, raw] of content.matchAll(APP_ID_LINE)) {
5440
+ const id = readValue(raw);
5441
+ if (id) {
5442
+ logger.info({ file, name, app: id }, "found an application id in env");
5443
+ return { id, name, file };
5444
+ }
5445
+ }
5446
+ }
5447
+ return null;
5448
+ }
5449
+ function readValue(raw) {
5450
+ const trimmed = raw.trim();
5451
+ const quoted = trimmed.match(/^(['"])(.*)\1/);
5452
+ const value = quoted ? quoted[2].trim() : trimmed.replace(/\s+#.*$/, "").trim();
5453
+ return value.length > 0 && !value.startsWith("<") ? value : null;
5454
+ }
5455
+
5456
+ // src/lib/algoliaAppPicker.ts
5457
+ function secondaryFor(app) {
5458
+ return app.plan ? { kind: "badge", value: app.plan } : void 0;
5459
+ }
5460
+ function labelFor(app) {
5461
+ return app.name.trim() ? `${app.name} \u2014 ${app.id}` : app.id;
5462
+ }
5463
+ function selectAndReport(app) {
5464
+ useWizard.getState().pushCliOutput(
5465
+ "stdout",
5466
+ `Selecting ${labelFor(app)} \u2014 provisioning its API key\u2026`
5467
+ );
5468
+ return selectApplication(app.id);
5469
+ }
5470
+ async function promptForApplication(leadIn = []) {
5471
+ const store = useWizard.getState();
5472
+ const apps = await listApplications();
5473
+ if (apps.length === 0) {
5474
+ throw new Error(
5475
+ "This Algolia account has no applications. Create one in the Algolia dashboard, or with `npx @algolia/cli@latest application create`, then restart the wizard."
5476
+ );
5477
+ }
5478
+ if (apps.length === 1) {
5479
+ const only = apps[0];
5480
+ logger.info(
5481
+ { app: only.id },
5482
+ "single application on the account; selecting it"
5483
+ );
5484
+ for (const line of leadIn) store.pushCliOutput("stdout", line);
5485
+ return selectAndReport(only);
5486
+ }
5487
+ const messages = [
5488
+ ...leadIn,
5489
+ "Which Algolia application should the wizard work in?"
5490
+ ];
5491
+ for (; ; ) {
5492
+ const choice = await store.requestUserInput({
5493
+ prompt: "Select an application",
5494
+ promptType: "multipleChoice",
5495
+ options: apps.map(labelFor),
5496
+ secondary: apps.map(secondaryFor),
5497
+ messages
5498
+ });
5499
+ const chosen = apps.find((app) => labelFor(app) === choice);
5500
+ if (!chosen) {
5501
+ throw new Error("Application picker received an unexpected selection");
5502
+ }
5503
+ try {
5504
+ return await selectAndReport(chosen);
5505
+ } catch (err) {
5506
+ logger.warn(
5507
+ { app: chosen.id, err: err.message },
5508
+ "application select failed; re-prompting"
5509
+ );
5510
+ messages.push(
5511
+ `Could not select \u201C${labelFor(chosen)}\u201D. It may have been removed \u2014 pick another.`
5512
+ );
5513
+ }
5514
+ }
5515
+ }
5516
+ async function confirmEnvApplication(env, current) {
5517
+ const useEnv = `Use ${env.id} (from ${env.file})`;
5518
+ const choice = await useWizard.getState().requestUserInput({
5519
+ prompt: "Select an application",
5520
+ promptType: "multipleChoice",
5521
+ options: [
5522
+ useEnv,
5523
+ current ? `Use ${labelFor(current)} (already selected)` : "Pick a different application"
5524
+ ],
5525
+ messages: [
5526
+ `${env.file} already sets ${env.name}=${env.id}.`,
5527
+ "Which Algolia application should the wizard work in?"
5528
+ ]
5529
+ });
5530
+ return choice === useEnv;
5531
+ }
5532
+ async function selectEnvApplication(env) {
5533
+ try {
5534
+ return await selectAndReport({ id: env.id, name: "" });
5535
+ } catch (err) {
5536
+ logger.warn(
5537
+ { app: env.id, err: err.message },
5538
+ "could not select the application named in env; falling back to the picker"
5539
+ );
5540
+ return promptForApplication([
5541
+ `Could not select ${env.id} from ${env.file} \u2014 it may have been removed, or this account may not have access to it.`
5542
+ ]);
5543
+ }
5544
+ }
5545
+ async function ensureApplication() {
5546
+ const current = await currentApplication();
5547
+ const env = await findEnvApplicationId();
5548
+ if (env && env.id !== current?.id && await confirmEnvApplication(env, current)) {
5549
+ return selectEnvApplication(env);
5550
+ }
5551
+ return current ?? await promptForApplication();
5552
+ }
5553
+
5296
5554
  // src/lib/seed.ts
5297
5555
  var projectScan2 = {
5298
5556
  languages: [{ name: "TypeScript", version: "5.7.2" }],