@algolia/wizard 0.19.0-rc.111.189 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/dist/main.js +554 -374
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,8 +12,22 @@ npx @algolia/wizard
|
|
|
12
12
|
|
|
13
13
|
# run a specific workflow by id
|
|
14
14
|
npx @algolia/wizard <workflow-id>
|
|
15
|
+
|
|
16
|
+
# see all options
|
|
17
|
+
npx @algolia/wizard --help
|
|
15
18
|
```
|
|
16
19
|
|
|
20
|
+
### Options
|
|
21
|
+
|
|
22
|
+
| Flag | Effect |
|
|
23
|
+
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
24
|
+
| `--seed <step-id>` | Start the workflow at the step with this id (e.g. `--seed ingestion`), with the earlier steps pre-filled with test data. Pass with no value to print the step ids. |
|
|
25
|
+
| `--no-telemetry` | Send no telemetry or analytics for this run. |
|
|
26
|
+
| `--reset-on-run` | Wipe this project's wizard state (run state, AI-changes consent, worktrees) before starting, so the run behaves like a first-ever run. Credentials are untouched. |
|
|
27
|
+
| `-h`, `--help` | Print usage. |
|
|
28
|
+
|
|
29
|
+
`--seed` pre-fills the earlier steps with fabricated data so a single step can be exercised without running the whole workflow — useful for testing a step, not for a real implementation. It replaces any in-progress run for that workflow and pre-grants the AI-changes consent. See [Starting mid-workflow](CONTRIBUTING.md#starting-mid-workflow---seed).
|
|
30
|
+
|
|
17
31
|
On first run, Wizard checks whether you're signed in to the Algolia CLI. If you aren't, it runs `algolia auth login --non-interactive` — the browser opens for sign-in, and no prompts land in the wizard's terminal. It then asks which Algolia application to work in (skipping the question when the account has only one) and makes it current with `algolia application select`.
|
|
18
32
|
|
|
19
33
|
You'll also be asked once to consent to AI-authored changes to the repository.
|
package/dist/main.js
CHANGED
|
@@ -251,7 +251,6 @@ var useWizard = create((set, get) => ({
|
|
|
251
251
|
_noticeTimer: null,
|
|
252
252
|
cliOutput: [],
|
|
253
253
|
targetIndex: null,
|
|
254
|
-
writtenFiles: [],
|
|
255
254
|
logs: [],
|
|
256
255
|
error: null,
|
|
257
256
|
inputReq: null,
|
|
@@ -345,8 +344,6 @@ var useWizard = create((set, get) => ({
|
|
|
345
344
|
})),
|
|
346
345
|
clearCliOutput: () => set({ cliOutput: [] }),
|
|
347
346
|
setTargetIndex: (index) => set({ targetIndex: index }),
|
|
348
|
-
recordWrittenFile: (path) => set((s) => ({ writtenFiles: [...s.writtenFiles, path] })),
|
|
349
|
-
clearWrittenFiles: () => set({ writtenFiles: [] }),
|
|
350
347
|
logStart: (kind, name, input) => {
|
|
351
348
|
const id = nanoid();
|
|
352
349
|
set((s) => ({
|
|
@@ -394,7 +391,6 @@ var useWizard = create((set, get) => ({
|
|
|
394
391
|
notices: [],
|
|
395
392
|
cliOutput: [],
|
|
396
393
|
targetIndex: null,
|
|
397
|
-
writtenFiles: [],
|
|
398
394
|
logs: [],
|
|
399
395
|
error: null,
|
|
400
396
|
inputReq: null,
|
|
@@ -1204,7 +1200,7 @@ var accessItems = [
|
|
|
1204
1200
|
{
|
|
1205
1201
|
tag: "WRITE",
|
|
1206
1202
|
title: "Code changes",
|
|
1207
|
-
description: "creates & edits files (search UI, config)
|
|
1203
|
+
description: "creates & edits files (search UI, config) in a throwaway git worktree \u2014 your checkout is never touched."
|
|
1208
1204
|
},
|
|
1209
1205
|
{
|
|
1210
1206
|
tag: "EXEC",
|
|
@@ -1219,7 +1215,7 @@ var accessItems = [
|
|
|
1219
1215
|
{
|
|
1220
1216
|
tag: "KEY",
|
|
1221
1217
|
title: "Credentials",
|
|
1222
|
-
description: "writes your Algolia app id and a search-only key (safe to expose) to .env in
|
|
1218
|
+
description: "writes your Algolia app id and a search-only key (safe to expose) to .env in the worktree."
|
|
1223
1219
|
}
|
|
1224
1220
|
];
|
|
1225
1221
|
var neverItems = [
|
|
@@ -2250,7 +2246,7 @@ async function ensureApplication() {
|
|
|
2250
2246
|
}
|
|
2251
2247
|
|
|
2252
2248
|
// src/workflows/default.ts
|
|
2253
|
-
import { z as
|
|
2249
|
+
import { z as z30 } from "zod";
|
|
2254
2250
|
|
|
2255
2251
|
// src/actions/listIndices.ts
|
|
2256
2252
|
import { z as z5 } from "zod";
|
|
@@ -2500,7 +2496,6 @@ function writeFileTool(ctx) {
|
|
|
2500
2496
|
}
|
|
2501
2497
|
await mkdir3(dirname4(resolved2.target), { recursive: true });
|
|
2502
2498
|
await writeFile3(resolved2.target, content, "utf8");
|
|
2503
|
-
useWizard.getState().recordWrittenFile(resolved2.target);
|
|
2504
2499
|
return `Wrote to ${filePath}`;
|
|
2505
2500
|
} catch (err) {
|
|
2506
2501
|
return `Error writing ${filePath}: ${err.message}`;
|
|
@@ -2772,6 +2767,28 @@ async function ensureGitIgnored(root, target) {
|
|
|
2772
2767
|
var APP_ID_VAR = "ALGOLIA_APPLICATION_ID";
|
|
2773
2768
|
var API_KEY_VAR = "ALGOLIA_WRITE_API_KEY";
|
|
2774
2769
|
var INDEX_NAME_VAR = "ALGOLIA_INDEX_NAME";
|
|
2770
|
+
var PUBLIC_APP_ID_SUFFIX = "ALGOLIA_APP_ID";
|
|
2771
|
+
var PUBLIC_SEARCH_KEY_SUFFIX = "ALGOLIA_SEARCH_KEY";
|
|
2772
|
+
var PUBLIC_INDEX_NAME_SUFFIX = "ALGOLIA_INDEX_NAME";
|
|
2773
|
+
function publicAppIdVar(prefix) {
|
|
2774
|
+
return `${prefix}${PUBLIC_APP_ID_SUFFIX}`;
|
|
2775
|
+
}
|
|
2776
|
+
function publicSearchKeyVar(prefix) {
|
|
2777
|
+
return `${prefix}${PUBLIC_SEARCH_KEY_SUFFIX}`;
|
|
2778
|
+
}
|
|
2779
|
+
function publicIndexNameVar(prefix) {
|
|
2780
|
+
return `${prefix}${PUBLIC_INDEX_NAME_SUFFIX}`;
|
|
2781
|
+
}
|
|
2782
|
+
function publicSearchEnvVars(prefix, index, appId, searchKey) {
|
|
2783
|
+
return [
|
|
2784
|
+
{ name: publicAppIdVar(prefix), value: appId ?? "<your-algolia-app-id>" },
|
|
2785
|
+
{
|
|
2786
|
+
name: publicSearchKeyVar(prefix),
|
|
2787
|
+
value: searchKey ?? "<your-algolia-search-only-api-key>"
|
|
2788
|
+
},
|
|
2789
|
+
{ name: publicIndexNameVar(prefix), value: index }
|
|
2790
|
+
];
|
|
2791
|
+
}
|
|
2775
2792
|
function appendEnv(content, entries) {
|
|
2776
2793
|
const prefix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
|
|
2777
2794
|
const lines = entries.map(([name, value]) => `${name}=${value}
|
|
@@ -3251,7 +3268,7 @@ var anthropic = createAnthropic({
|
|
|
3251
3268
|
});
|
|
3252
3269
|
function generateRecordTool(ctx) {
|
|
3253
3270
|
return tool10({
|
|
3254
|
-
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.",
|
|
3271
|
+
description: "Generate realistic sample records for an entity and write them to a JSON file in the worktree. 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.",
|
|
3255
3272
|
inputSchema: z17.object({
|
|
3256
3273
|
entityName: z17.string().describe("Name of the entity to generate records for."),
|
|
3257
3274
|
attributes: z17.array(z17.string()).describe("Attribute names each record must contain."),
|
|
@@ -3505,7 +3522,10 @@ async function runAgent(req) {
|
|
|
3505
3522
|
import z21 from "zod";
|
|
3506
3523
|
var detectLanguageSchema = z21.object({
|
|
3507
3524
|
languages: z21.array(z21.object({ name: z21.string(), version: z21.string() })),
|
|
3508
|
-
frameworks: z21.array(z21.object({ name: z21.string(), version: z21.string() }))
|
|
3525
|
+
frameworks: z21.array(z21.object({ name: z21.string(), version: z21.string() })),
|
|
3526
|
+
publicEnvVarPrefix: z21.string().regex(/^([A-Z0-9]+_)*$/).describe(
|
|
3527
|
+
`The prefix a bundler/framework requires for an env var to reach client-side code (e.g. "NEXT_PUBLIC_", "VITE_", "NUXT_PUBLIC_"), uppercase with a trailing underscore when non-empty. Prefer the project's own existing convention over a framework default. Empty string when the project has no client-side env var exposure (e.g. a backend-only project).`
|
|
3528
|
+
)
|
|
3509
3529
|
});
|
|
3510
3530
|
var detectLanguage = () => runAgent({
|
|
3511
3531
|
instructions: [
|
|
@@ -3514,6 +3534,7 @@ var detectLanguage = () => runAgent({
|
|
|
3514
3534
|
"If a meta-framework is used, exclude the framework it builds on (e.g. Next.js over React, Rails over Rack).",
|
|
3515
3535
|
"Return the exact version",
|
|
3516
3536
|
"Exclude things like CSS frameworks, build tools, or testing frameworks",
|
|
3537
|
+
`Determine publicEnvVarPrefix: check the project's own env var usage first (e.g. names already referenced in code, .env/.env.example); if none exists, fall back to the detected framework's known convention for exposing env vars to client-side code; use "" when the project has no such convention (e.g. a backend-only project).`,
|
|
3517
3538
|
'Use as few tools as possible, but do not guess. If you cant find the answer, say "unknown"',
|
|
3518
3539
|
"When done, call reportStatus"
|
|
3519
3540
|
],
|
|
@@ -3609,7 +3630,7 @@ async function runAnalysis(mode, extraInstructions = []) {
|
|
|
3609
3630
|
// package.json
|
|
3610
3631
|
var package_default = {
|
|
3611
3632
|
name: "@algolia/wizard",
|
|
3612
|
-
version: "0.
|
|
3633
|
+
version: "0.20.0",
|
|
3613
3634
|
description: "Magically implement Algolia functionality in your codebase",
|
|
3614
3635
|
type: "module",
|
|
3615
3636
|
engines: {
|
|
@@ -3976,10 +3997,11 @@ ${JSON.stringify(s.output, null, 2)}`
|
|
|
3976
3997
|
function formatReviewSummary(result) {
|
|
3977
3998
|
const nextStepLines = result.nextSteps.map((step) => {
|
|
3978
3999
|
const isIngestCommand = step.includes(".algolia-wizard/ingest.sh");
|
|
4000
|
+
const isWorktreeCommand = step.includes("/worktrees/");
|
|
3979
4001
|
return {
|
|
3980
4002
|
text: `\u2192 ${step}`,
|
|
3981
|
-
color: isIngestCommand ? COLORS.brand : void 0,
|
|
3982
|
-
bold: isIngestCommand
|
|
4003
|
+
color: isIngestCommand ? COLORS.brand : isWorktreeCommand ? COLORS.secondary : void 0,
|
|
4004
|
+
bold: isIngestCommand || isWorktreeCommand
|
|
3983
4005
|
};
|
|
3984
4006
|
});
|
|
3985
4007
|
return [
|
|
@@ -4013,14 +4035,16 @@ ${formatCompletedSteps(ctx.completedSteps)}`,
|
|
|
4013
4035
|
};
|
|
4014
4036
|
|
|
4015
4037
|
// src/actions/implement.ts
|
|
4016
|
-
import
|
|
4017
|
-
import { join as join12
|
|
4038
|
+
import z29 from "zod";
|
|
4039
|
+
import { join as join12 } from "node:path";
|
|
4018
4040
|
|
|
4019
|
-
// src/lib/
|
|
4041
|
+
// src/lib/worktree.ts
|
|
4020
4042
|
import { execFile as execFile2 } from "node:child_process";
|
|
4021
|
-
import { copyFile, mkdir as mkdir6, readFile as readFile8, stat as stat3, writeFile as writeFile7 } from "node:fs/promises";
|
|
4043
|
+
import { copyFile, mkdir as mkdir6, readdir as readdir3, readFile as readFile8, stat as stat3, writeFile as writeFile7 } from "node:fs/promises";
|
|
4022
4044
|
import { basename as basename2, dirname as dirname7, isAbsolute as isAbsolute2, join as join10, resolve as resolve3 } from "node:path";
|
|
4023
4045
|
var MAX_BUFFER = 32 * 1024 * 1024;
|
|
4046
|
+
var MAX_WIZARD_WORKTREES = 3;
|
|
4047
|
+
var WIZARD_BRANCH_PREFIX = "wizard/implement-";
|
|
4024
4048
|
function git(args) {
|
|
4025
4049
|
return new Promise((resolve4, reject) => {
|
|
4026
4050
|
execFile2("git", args, { maxBuffer: MAX_BUFFER }, (err, stdout, stderr) => {
|
|
@@ -4043,7 +4067,44 @@ async function assertGitRepoWithHead(repoRoot) {
|
|
|
4043
4067
|
);
|
|
4044
4068
|
}
|
|
4045
4069
|
}
|
|
4046
|
-
async function
|
|
4070
|
+
async function isWorkingTreeDirty(repoRoot) {
|
|
4071
|
+
const out = await git(["-C", repoRoot, "status", "--porcelain"]);
|
|
4072
|
+
return out.trim().length > 0;
|
|
4073
|
+
}
|
|
4074
|
+
async function pruneOldWorktrees(repoRoot) {
|
|
4075
|
+
const dir = join10(stateDir(repoRoot), "worktrees");
|
|
4076
|
+
const stale = (await readdir3(dir).catch(() => [])).filter((name) => /^wizard-implement-\d+$/.test(name)).sort().reverse().slice(MAX_WIZARD_WORKTREES - 1);
|
|
4077
|
+
for (const slug of stale) {
|
|
4078
|
+
const branch = slug.replace("wizard-implement-", WIZARD_BRANCH_PREFIX);
|
|
4079
|
+
try {
|
|
4080
|
+
await git([
|
|
4081
|
+
"-C",
|
|
4082
|
+
repoRoot,
|
|
4083
|
+
"worktree",
|
|
4084
|
+
"remove",
|
|
4085
|
+
"--force",
|
|
4086
|
+
join10(dir, slug)
|
|
4087
|
+
]);
|
|
4088
|
+
await git(["-C", repoRoot, "branch", "-D", branch]);
|
|
4089
|
+
} catch (err) {
|
|
4090
|
+
logger.warn(
|
|
4091
|
+
{ branch, err: err.message },
|
|
4092
|
+
"createWorktree: failed to prune a stale wizard worktree; continuing"
|
|
4093
|
+
);
|
|
4094
|
+
}
|
|
4095
|
+
}
|
|
4096
|
+
}
|
|
4097
|
+
async function createWorktree(repoRoot) {
|
|
4098
|
+
const branch = `${WIZARD_BRANCH_PREFIX}${Date.now()}`;
|
|
4099
|
+
const dirSlug = branch.replace(/\//g, "-");
|
|
4100
|
+
const path = join10(stateDir(repoRoot), "worktrees", dirSlug);
|
|
4101
|
+
await git(["-C", repoRoot, "worktree", "prune"]);
|
|
4102
|
+
await pruneOldWorktrees(repoRoot);
|
|
4103
|
+
await mkdir6(dirname7(path), { recursive: true });
|
|
4104
|
+
await git(["-C", repoRoot, "worktree", "add", "-b", branch, path, "HEAD"]);
|
|
4105
|
+
return { path, branch };
|
|
4106
|
+
}
|
|
4107
|
+
async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourcePath) {
|
|
4047
4108
|
const trimmed = sourcePath.trim();
|
|
4048
4109
|
if (!trimmed) {
|
|
4049
4110
|
return { ok: false, reason: "no file path was provided" };
|
|
@@ -4057,10 +4118,7 @@ async function copyUploadIntoProject(repoRoot, ingestDir, sourcePath) {
|
|
|
4057
4118
|
return { ok: false, reason: `"${sourcePath}" does not exist` };
|
|
4058
4119
|
}
|
|
4059
4120
|
const relPath = join10(ingestDir, basename2(source));
|
|
4060
|
-
const dest = join10(
|
|
4061
|
-
if (resolve3(source) === resolve3(dest)) {
|
|
4062
|
-
return { ok: true, relPath };
|
|
4063
|
-
}
|
|
4121
|
+
const dest = join10(worktreePath, relPath);
|
|
4064
4122
|
try {
|
|
4065
4123
|
await mkdir6(dirname7(dest), { recursive: true });
|
|
4066
4124
|
await copyFile(source, dest);
|
|
@@ -4075,10 +4133,10 @@ async function copyUploadIntoProject(repoRoot, ingestDir, sourcePath) {
|
|
|
4075
4133
|
function hasEnvVar(content, name) {
|
|
4076
4134
|
return new RegExp(`^(\\s*(?:export\\s+)?${name})\\s*=`, "m").test(content);
|
|
4077
4135
|
}
|
|
4078
|
-
async function readEnvVar(
|
|
4136
|
+
async function readEnvVar(worktreePath, name) {
|
|
4079
4137
|
let content;
|
|
4080
4138
|
try {
|
|
4081
|
-
content = await readFile8(join10(
|
|
4139
|
+
content = await readFile8(join10(worktreePath, ".env"), "utf8");
|
|
4082
4140
|
} catch (err) {
|
|
4083
4141
|
if (err.code !== "ENOENT") throw err;
|
|
4084
4142
|
return void 0;
|
|
@@ -4092,8 +4150,8 @@ async function readEnvVar(repoRoot, name) {
|
|
|
4092
4150
|
if (!value || value.startsWith("<")) return void 0;
|
|
4093
4151
|
return value;
|
|
4094
4152
|
}
|
|
4095
|
-
async function writeSearchEnvValues(
|
|
4096
|
-
const target = join10(
|
|
4153
|
+
async function writeSearchEnvValues(worktreePath, vars) {
|
|
4154
|
+
const target = join10(worktreePath, ".env");
|
|
4097
4155
|
let existing = "";
|
|
4098
4156
|
try {
|
|
4099
4157
|
existing = await readFile8(target, "utf8");
|
|
@@ -4108,27 +4166,61 @@ async function writeSearchEnvValues(repoRoot, vars) {
|
|
|
4108
4166
|
await writeFile7(target, existing + prefix + lines, "utf8");
|
|
4109
4167
|
return missing.map((v) => v.name);
|
|
4110
4168
|
}
|
|
4169
|
+
async function listChangedFiles(worktreePath) {
|
|
4170
|
+
const raw = await git(["-C", worktreePath, "status", "--porcelain", "-z"]);
|
|
4171
|
+
const entries = raw.split("\0");
|
|
4172
|
+
const files = [];
|
|
4173
|
+
for (let i = 0; i < entries.length; i += 1) {
|
|
4174
|
+
const entry = entries[i];
|
|
4175
|
+
if (!entry) continue;
|
|
4176
|
+
files.push(entry.slice(3));
|
|
4177
|
+
if (["R", "C"].includes(entry[0]) || ["R", "C"].includes(entry[1])) i += 1;
|
|
4178
|
+
}
|
|
4179
|
+
return files;
|
|
4180
|
+
}
|
|
4111
4181
|
function normalizeFindingPaths(findings) {
|
|
4112
4182
|
return {
|
|
4113
4183
|
...findings,
|
|
4114
4184
|
ingestionAnalysis: findings.ingestionAnalysis?.map((e) => ({
|
|
4115
4185
|
...e,
|
|
4116
|
-
paths: e.paths.map(
|
|
4186
|
+
paths: e.paths.map(toWorktreeRelative)
|
|
4117
4187
|
})),
|
|
4118
4188
|
searchImplementationAnalysis: findings.searchImplementationAnalysis ? normalizeSearchLocation(findings.searchImplementationAnalysis) : void 0,
|
|
4119
4189
|
confirmedEntities: findings.confirmedEntities?.map((e) => ({
|
|
4120
4190
|
...e,
|
|
4121
|
-
paths: e.paths.map(
|
|
4191
|
+
paths: e.paths.map(toWorktreeRelative)
|
|
4122
4192
|
}))
|
|
4123
4193
|
};
|
|
4124
4194
|
}
|
|
4125
4195
|
function normalizeSearchLocation(path) {
|
|
4126
|
-
const normalized = path ?
|
|
4196
|
+
const normalized = path ? toWorktreeRelative(path).trim() : "";
|
|
4127
4197
|
return normalized && normalized.toLowerCase() !== "unknown" ? normalized : void 0;
|
|
4128
4198
|
}
|
|
4129
|
-
function
|
|
4199
|
+
function toWorktreeRelative(p) {
|
|
4130
4200
|
return p.replace(/^\/+/, "");
|
|
4131
4201
|
}
|
|
4202
|
+
async function confirmDirtyWorkingTree(ctx, repoRoot) {
|
|
4203
|
+
const MAX_LISTED_DIRTY_FILES = 10;
|
|
4204
|
+
const dirty = await listChangedFiles(repoRoot);
|
|
4205
|
+
const shown = dirty.slice(0, MAX_LISTED_DIRTY_FILES);
|
|
4206
|
+
const overflow = dirty.length - shown.length;
|
|
4207
|
+
const answer = await ctx.requestUserInput({
|
|
4208
|
+
prompt: "Proceed using HEAD only? Uncommitted changes will NOT be included in the generated implementation.",
|
|
4209
|
+
promptType: "acceptReject",
|
|
4210
|
+
options: [],
|
|
4211
|
+
messages: [
|
|
4212
|
+
`${dirty.length} uncommitted change(s) detected. The wizard builds an isolated worktree from HEAD, so these are ignored:`,
|
|
4213
|
+
...shown.map((file) => ` \u2022 ${file}`),
|
|
4214
|
+
...overflow > 0 ? [` \u2022 \u2026and ${overflow} more`] : [],
|
|
4215
|
+
"Commit or stash them first to include them in the implementation."
|
|
4216
|
+
]
|
|
4217
|
+
});
|
|
4218
|
+
if (answer !== true) {
|
|
4219
|
+
throw new Error(
|
|
4220
|
+
"implement aborted: commit or stash your changes so they are built into the worktree, then re-run the wizard."
|
|
4221
|
+
);
|
|
4222
|
+
}
|
|
4223
|
+
}
|
|
4132
4224
|
|
|
4133
4225
|
// src/lib/algoliaDocs.ts
|
|
4134
4226
|
import { readFileSync, readdirSync, existsSync } from "node:fs";
|
|
@@ -4188,29 +4280,53 @@ function getFrameworkSpecificDoc(frameworks) {
|
|
|
4188
4280
|
return loadAlgoliaDoc("js");
|
|
4189
4281
|
}
|
|
4190
4282
|
|
|
4283
|
+
// src/lib/shell.ts
|
|
4284
|
+
function shellQuote(value) {
|
|
4285
|
+
return "'" + value.replace(/'/g, "'\\''") + "'";
|
|
4286
|
+
}
|
|
4287
|
+
|
|
4288
|
+
// src/actions/resolveEnvVarPrefix.ts
|
|
4289
|
+
import z28 from "zod";
|
|
4290
|
+
var resolveEnvVarPrefixSchema = z28.object({
|
|
4291
|
+
publicEnvVarPrefix: detectLanguageSchema.shape.publicEnvVarPrefix
|
|
4292
|
+
});
|
|
4293
|
+
var resolveEnvVarPrefix = (frameworkName) => runAgent({
|
|
4294
|
+
instructions: [
|
|
4295
|
+
`The developer corrected the project's framework to "${frameworkName}".`,
|
|
4296
|
+
`Determine publicEnvVarPrefix for this framework: check the project's own env var usage first (e.g. names already referenced in code, .env/.env.example); if none exists, fall back to this framework's known convention for exposing env vars to client-side code; use "" when the framework has no such convention (e.g. a backend-only framework).`,
|
|
4297
|
+
'Use as few tools as possible, but do not guess. If you cant find the answer, say "unknown"',
|
|
4298
|
+
"When done, call reportStatus"
|
|
4299
|
+
],
|
|
4300
|
+
tools: ["listFiles", "changeDirectory", "readFile", "searchFiles"],
|
|
4301
|
+
outputSchema: resolveEnvVarPrefixSchema,
|
|
4302
|
+
modelSize: "small"
|
|
4303
|
+
});
|
|
4304
|
+
|
|
4191
4305
|
// src/actions/implement.ts
|
|
4192
|
-
var implementSchema =
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4306
|
+
var implementSchema = z29.object({
|
|
4307
|
+
filesChanged: z29.array(z29.string()),
|
|
4308
|
+
summary: z29.string(),
|
|
4309
|
+
worktreePath: z29.string().optional(),
|
|
4310
|
+
ingestCommand: z29.string().optional(),
|
|
4311
|
+
ingestScriptRan: z29.boolean().optional(),
|
|
4312
|
+
ingestRecordCount: z29.number().optional(),
|
|
4313
|
+
ingestDurationMs: z29.number().optional(),
|
|
4314
|
+
ingestionSource: z29.enum(["local", "fileUpload", "generated"]),
|
|
4315
|
+
searchEnvVars: z29.array(
|
|
4316
|
+
z29.object({
|
|
4317
|
+
name: z29.string(),
|
|
4318
|
+
value: z29.string()
|
|
4203
4319
|
})
|
|
4204
4320
|
).optional()
|
|
4205
4321
|
});
|
|
4206
|
-
var implementationOutputSchema =
|
|
4207
|
-
summary:
|
|
4208
|
-
ingestCommand:
|
|
4322
|
+
var implementationOutputSchema = z29.object({
|
|
4323
|
+
summary: z29.string(),
|
|
4324
|
+
ingestCommand: z29.string().optional()
|
|
4209
4325
|
});
|
|
4210
|
-
var verificationOutputSchema =
|
|
4211
|
-
summary:
|
|
4212
|
-
sufficient:
|
|
4213
|
-
additionalInstructions:
|
|
4326
|
+
var verificationOutputSchema = z29.object({
|
|
4327
|
+
summary: z29.string(),
|
|
4328
|
+
sufficient: z29.boolean(),
|
|
4329
|
+
additionalInstructions: z29.string().optional()
|
|
4214
4330
|
});
|
|
4215
4331
|
var MAX_IMPLEMENT_VERIFICATION_ATTEMPTS = 3;
|
|
4216
4332
|
var DEFAULT_IMPLEMENT_USE_CASES = ["ingestion", "search"];
|
|
@@ -4241,54 +4357,10 @@ function searchUiTarget(language) {
|
|
|
4241
4357
|
function frameworksForDoc(language) {
|
|
4242
4358
|
return [matchUiFramework(language)?.doc ?? "js"];
|
|
4243
4359
|
}
|
|
4244
|
-
function publicEnvPrefix(language) {
|
|
4245
|
-
const frameworkNames = lower(language.frameworks);
|
|
4246
|
-
if (frameworkNames.some((name) => name.includes("next"))) {
|
|
4247
|
-
return "NEXT_PUBLIC_";
|
|
4248
|
-
}
|
|
4249
|
-
if (frameworkNames.some((name) => name.includes("nuxt"))) {
|
|
4250
|
-
return "NUXT_PUBLIC_";
|
|
4251
|
-
}
|
|
4252
|
-
if (frameworkNames.some((name) => name.includes("astro"))) {
|
|
4253
|
-
return "PUBLIC_";
|
|
4254
|
-
}
|
|
4255
|
-
if (frameworkNames.some((name) => name.includes("vite"))) {
|
|
4256
|
-
return "VITE_";
|
|
4257
|
-
}
|
|
4258
|
-
return isJsProject(language) ? "PUBLIC_" : "";
|
|
4259
|
-
}
|
|
4260
|
-
var APP_ID_VAR_SUFFIX = "ALGOLIA_APP_ID";
|
|
4261
|
-
var SEARCH_KEY_VAR_SUFFIX = "ALGOLIA_SEARCH_API_KEY";
|
|
4262
|
-
var INDEX_VAR_SUFFIX = "ALGOLIA_INDEX_NAME";
|
|
4263
|
-
function appIdVar(language) {
|
|
4264
|
-
return `${publicEnvPrefix(language)}${APP_ID_VAR_SUFFIX}`;
|
|
4265
|
-
}
|
|
4266
|
-
function searchKeyVar(language) {
|
|
4267
|
-
return `${publicEnvPrefix(language)}${SEARCH_KEY_VAR_SUFFIX}`;
|
|
4268
|
-
}
|
|
4269
|
-
function searchIndexVar(language) {
|
|
4270
|
-
return `${publicEnvPrefix(language)}${INDEX_VAR_SUFFIX}`;
|
|
4271
|
-
}
|
|
4272
|
-
function searchEnvVars(language, index, appId, searchKey) {
|
|
4273
|
-
return [
|
|
4274
|
-
{
|
|
4275
|
-
name: appIdVar(language),
|
|
4276
|
-
value: appId ?? "<your-algolia-app-id>"
|
|
4277
|
-
},
|
|
4278
|
-
{
|
|
4279
|
-
name: searchKeyVar(language),
|
|
4280
|
-
value: searchKey ?? "<your-algolia-search-only-api-key>"
|
|
4281
|
-
},
|
|
4282
|
-
// Wizard-supplied rather than written into the generated code: an agent
|
|
4283
|
-
// that retypes the name leaves the UI querying an index that doesn't exist.
|
|
4284
|
-
{
|
|
4285
|
-
name: searchIndexVar(language),
|
|
4286
|
-
value: index
|
|
4287
|
-
}
|
|
4288
|
-
];
|
|
4289
|
-
}
|
|
4290
4360
|
function baseInstructions(input) {
|
|
4291
4361
|
return [
|
|
4362
|
+
// Agents have renamed this (e.g. appending the project name), which the
|
|
4363
|
+
// index-scoped keys then reject with a 403.
|
|
4292
4364
|
`Target Algolia index, to be used exactly as written \u2014 never renamed, re-cased, prefixed, or suffixed: "${input.targetIndex}"`,
|
|
4293
4365
|
`Project languages and frameworks: ${JSON.stringify(input.language)}`,
|
|
4294
4366
|
"Make minimal, idiomatic changes; do not touch unrelated code.",
|
|
@@ -4306,14 +4378,14 @@ function sourceSpecificInstructions(input) {
|
|
|
4306
4378
|
"Add env vars for any local source access (DB URL, API base, file paths) that is not a fixed repo path."
|
|
4307
4379
|
],
|
|
4308
4380
|
fileUpload: [
|
|
4309
|
-
`Records come from the developer's file, already copied into the
|
|
4381
|
+
`Records come from the developer's file, already copied into the worktree at "${input.uploadFilePath}". Read and parse that exact file.`,
|
|
4310
4382
|
"Parse by extension: JSON = array of objects; CSV/TSV = header row maps to keys.",
|
|
4311
4383
|
"Map parsed columns/fields to the confirmed entity attributes.",
|
|
4312
4384
|
"Never fabricate, hardcode, or substitute a different file."
|
|
4313
4385
|
],
|
|
4314
4386
|
generated: [
|
|
4315
4387
|
"No real data source exists; use sample records for each confirmed entity.",
|
|
4316
|
-
"Call the generateRecord tool once per entity (entityName, attributes, count 20-50); it invents the values and unique objectIDs and writes them to a JSON file, returning the file path. Do not write records or objectIDs yourself.",
|
|
4388
|
+
"Call the generateRecord tool once per entity (entityName, attributes, count 20-50); it invents the values and unique objectIDs and writes them to a JSON file in the worktree, returning the file path. Do not write records or objectIDs yourself.",
|
|
4317
4389
|
"In the script, read and parse each returned file path at runtime using your language's standard JSON support, instead of inlining the records as literals.",
|
|
4318
4390
|
"Add a prominent TODO where the developer swaps the generated records (and the JSON file under `.algolia-wizard/data/`) for their real record source."
|
|
4319
4391
|
]
|
|
@@ -4360,13 +4432,16 @@ function searchInstructions(input) {
|
|
|
4360
4432
|
],
|
|
4361
4433
|
`Add the search UI at ${input.searchLocation ? `"${input.searchLocation}"` : "the best shared, always-rendered layout location (e.g. a header/nav component)"} so it is reachable across the app \u2014 at least a working search input and results list against the target index.`,
|
|
4362
4434
|
"If a search box already exists, replace it with yours.",
|
|
4363
|
-
`Read the index name from the ${
|
|
4435
|
+
`Read the index name from the ${publicIndexNameVar(input.publicEnvVarPrefix)} env var, which the wizard sets to "${input.targetIndex}". Never hardcode an index name or derive one from the project, file, or component name.`,
|
|
4364
4436
|
"Read the App ID, the search-only API key, and the index name from env vars; never hardcode them. A search-only key is safe to expose client-side.",
|
|
4365
|
-
// The key is provisioned only after verification passes,
|
|
4366
|
-
//
|
|
4367
|
-
//
|
|
4437
|
+
// The key is provisioned only after verification passes, so the agent never
|
|
4438
|
+
// sees one. It must also leave .env alone: the wizard reads that file to
|
|
4439
|
+
// decide whether a key already exists, and an agent-invented value there
|
|
4440
|
+
// would be reused as if it were real.
|
|
4368
4441
|
`Add Algolia App ID "${input.appId}"; leave the search-only key as a placeholder. Do not create or edit .env \u2014 the wizard writes the resolved key there itself.`,
|
|
4369
|
-
//
|
|
4442
|
+
// Not the agent's to rename: the wizard writes these exact names into
|
|
4443
|
+
// ".env" right after this step, so a renamed prefix would leave the code
|
|
4444
|
+
// reading a var the wizard never wrote.
|
|
4370
4445
|
`Use exactly these env var names in the code: ${input.searchEnvVars.map(({ name }) => name).join(", ")}.`,
|
|
4371
4446
|
"Install any Algolia packages you import with the project's own package manager via runShell, and declare them in the project's dependency manifest.",
|
|
4372
4447
|
"Match the styles of the application as closely as possible.",
|
|
@@ -4375,10 +4450,10 @@ function searchInstructions(input) {
|
|
|
4375
4450
|
}
|
|
4376
4451
|
function verificationInstructions(input) {
|
|
4377
4452
|
return [
|
|
4378
|
-
"Verify the Algolia implementation changes.",
|
|
4453
|
+
"Verify the Algolia implementation changes in the current worktree.",
|
|
4379
4454
|
`Verification tools found in the codebase: ${JSON.stringify(input.findings.verification ?? [])}.`,
|
|
4380
4455
|
"Run the project's own checks (lint, type check, tests) via runShell, using the commands the project actually defines \u2014 its task runner, manifest scripts, or Makefile. Run every check that applies, not just the first.",
|
|
4381
|
-
"If a check fails because packages or modules are missing, install the dependencies via runShell and re-run it rather than changing the code.",
|
|
4456
|
+
"This worktree starts with no installed dependencies. If a check fails because packages or modules are missing, install the dependencies via runShell and re-run it rather than changing the code.",
|
|
4382
4457
|
"For issues caused by the implementation, make minimal fixes with writeFile and re-run the checks.",
|
|
4383
4458
|
"Do not make speculative fixes when no checks exist, a check cannot run, or failures are unrelated to these changes \u2014 note the limitation in your summary.",
|
|
4384
4459
|
"Do not add new Algolia functionality here; only validate and make minimal correctness fixes.",
|
|
@@ -4473,11 +4548,11 @@ function ingestFailure(attempt, executions) {
|
|
|
4473
4548
|
}
|
|
4474
4549
|
return { reason: `it failed (exit ${attempt.exitCode ?? "unknown"})`, detail };
|
|
4475
4550
|
}
|
|
4476
|
-
function makeToolContext(
|
|
4551
|
+
function makeToolContext(worktree, env = async () => ({})) {
|
|
4477
4552
|
return createToolContext(
|
|
4478
4553
|
DEFAULT_TOOL_LIMITS,
|
|
4479
|
-
|
|
4480
|
-
createShellContext({ env, approve: storeApproval(
|
|
4554
|
+
worktree,
|
|
4555
|
+
createShellContext({ env, approve: storeApproval(worktree) })
|
|
4481
4556
|
);
|
|
4482
4557
|
}
|
|
4483
4558
|
function verificationRetryInstructions(verification) {
|
|
@@ -4485,7 +4560,7 @@ function verificationRetryInstructions(verification) {
|
|
|
4485
4560
|
`Implementation insufficient. Address these findings before reporting completion: ${verification.additionalInstructions ?? verification.summary}`
|
|
4486
4561
|
];
|
|
4487
4562
|
}
|
|
4488
|
-
async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES) {
|
|
4563
|
+
async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWorktreePath) {
|
|
4489
4564
|
const repoRoot = process.cwd();
|
|
4490
4565
|
const scan = ctx.getStepOutput("project-scan");
|
|
4491
4566
|
const entities = ctx.getStepOutput(
|
|
@@ -4501,6 +4576,21 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES) {
|
|
|
4501
4576
|
languages: ctx.getStepOutput("confirm-language")?.languages ?? scan.languages,
|
|
4502
4577
|
frameworks: ctx.getStepOutput("confirm-framework")?.frameworks ?? scan.frameworks
|
|
4503
4578
|
};
|
|
4579
|
+
const normalizeFrameworkName = (name) => name.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
4580
|
+
const confirmedPrimaryFramework = language.frameworks[0]?.name;
|
|
4581
|
+
const frameworkWasCorrected = confirmedPrimaryFramework !== void 0 && !scan.frameworks.some(
|
|
4582
|
+
(fw) => normalizeFrameworkName(fw.name) === normalizeFrameworkName(confirmedPrimaryFramework)
|
|
4583
|
+
);
|
|
4584
|
+
const publicEnvVarPrefixPromise = frameworkWasCorrected ? resolveEnvVarPrefix(confirmedPrimaryFramework).then(
|
|
4585
|
+
(r) => r.publicEnvVarPrefix,
|
|
4586
|
+
(err) => {
|
|
4587
|
+
logger.warn(
|
|
4588
|
+
{ err, framework: confirmedPrimaryFramework },
|
|
4589
|
+
"implement: could not re-resolve publicEnvVarPrefix after a framework correction; using the stale scan value"
|
|
4590
|
+
);
|
|
4591
|
+
return scan.publicEnvVarPrefix;
|
|
4592
|
+
}
|
|
4593
|
+
) : Promise.resolve(scan.publicEnvVarPrefix);
|
|
4504
4594
|
const selected = ctx.getStepOutput(
|
|
4505
4595
|
"select-index"
|
|
4506
4596
|
);
|
|
@@ -4551,6 +4641,9 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES) {
|
|
|
4551
4641
|
const targetIndex = selected?.selection;
|
|
4552
4642
|
useWizard.getState().setTargetIndex(targetIndex ?? null);
|
|
4553
4643
|
await assertGitRepoWithHead(repoRoot);
|
|
4644
|
+
if (await isWorkingTreeDirty(repoRoot)) {
|
|
4645
|
+
await confirmDirtyWorkingTree(ctx, repoRoot);
|
|
4646
|
+
}
|
|
4554
4647
|
const normalized = normalizeFindingPaths(findings);
|
|
4555
4648
|
const confirmed2 = normalized.confirmedEntities;
|
|
4556
4649
|
const searchLocation = normalized.searchImplementationAnalysis;
|
|
@@ -4562,288 +4655,328 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES) {
|
|
|
4562
4655
|
if (useCases.includes("ingestion")) {
|
|
4563
4656
|
ingestAppId = appId ?? (await requireApplication()).id;
|
|
4564
4657
|
}
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
ingestionSource = "generated";
|
|
4577
|
-
uploadWarning = `\u26A0\uFE0F Could not use the uploaded file (${copied.reason}); generating sample records instead.`;
|
|
4578
|
-
logger.warn(
|
|
4579
|
-
{ reason: copied.reason },
|
|
4580
|
-
"implement: file upload unavailable; falling back to generated sample records"
|
|
4581
|
-
);
|
|
4582
|
-
}
|
|
4583
|
-
}
|
|
4584
|
-
const input = {
|
|
4585
|
-
findings: normalized,
|
|
4586
|
-
confirmed: confirmed2,
|
|
4587
|
-
searchLocation,
|
|
4588
|
-
targetIndex,
|
|
4589
|
-
language,
|
|
4590
|
-
appId,
|
|
4591
|
-
searchEnvVars: searchEnvVars(language, targetIndex, appId),
|
|
4592
|
-
ingestDir: INGEST_DIR,
|
|
4593
|
-
ingestionSource,
|
|
4594
|
-
uploadFilePath,
|
|
4595
|
-
searchUiTarget: searchUiTarget(language)
|
|
4596
|
-
};
|
|
4597
|
-
const summaries = [];
|
|
4598
|
-
if (uploadWarning) summaries.push(uploadWarning);
|
|
4599
|
-
let envSearchKey;
|
|
4600
|
-
let envAppIdMismatch = false;
|
|
4601
|
-
if (useCases.includes("search") && appId) {
|
|
4602
|
-
const envAppId = await readEnvVar(repoRoot, appIdVar(language));
|
|
4603
|
-
if (envAppId === appId) {
|
|
4604
|
-
envSearchKey = await readEnvVar(repoRoot, searchKeyVar(language));
|
|
4605
|
-
} else if (envAppId) {
|
|
4606
|
-
envAppIdMismatch = true;
|
|
4607
|
-
summaries.push(
|
|
4608
|
-
`\u26A0\uFE0F .env already sets ${appIdVar(language)}=${envAppId}, but the active Algolia application is ${appId}. The wizard left those values alone \u2014 update ${appIdVar(language)} and ${searchKeyVar(language)} by hand, or searches will fail.`
|
|
4609
|
-
);
|
|
4610
|
-
logger.warn(
|
|
4611
|
-
{ envAppId, appId },
|
|
4612
|
-
"implement: .env holds credentials for a different Algolia application; not reusing its search key"
|
|
4658
|
+
const worktree = existingWorktreePath ?? (await createWorktree(repoRoot)).path;
|
|
4659
|
+
try {
|
|
4660
|
+
process.chdir(worktree);
|
|
4661
|
+
let uploadFilePath;
|
|
4662
|
+
let uploadWarning;
|
|
4663
|
+
if (ingestionSource === "fileUpload") {
|
|
4664
|
+
const copied = await copyUploadIntoWorktree(
|
|
4665
|
+
repoRoot,
|
|
4666
|
+
worktree,
|
|
4667
|
+
INGEST_DIR,
|
|
4668
|
+
uploadSourcePath ?? ""
|
|
4613
4669
|
);
|
|
4670
|
+
if (copied.ok) {
|
|
4671
|
+
uploadFilePath = copied.relPath;
|
|
4672
|
+
} else {
|
|
4673
|
+
ingestionSource = "generated";
|
|
4674
|
+
uploadWarning = `\u26A0\uFE0F Could not use the uploaded file (${copied.reason}); generating sample records instead.`;
|
|
4675
|
+
logger.warn(
|
|
4676
|
+
{ reason: copied.reason },
|
|
4677
|
+
"implement: file upload unavailable; falling back to generated sample records"
|
|
4678
|
+
);
|
|
4679
|
+
}
|
|
4614
4680
|
}
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
})) : void 0;
|
|
4629
|
-
const searchTools = makeToolContext(repoRoot);
|
|
4630
|
-
async function runImplementationUseCase(currentUseCase, extraInstructions = []) {
|
|
4631
|
-
if (agentRuns > 0) ctx.recordStepExecution();
|
|
4632
|
-
agentRuns += 1;
|
|
4633
|
-
return runAgent({
|
|
4634
|
-
instructions: buildAgentInstructions(
|
|
4635
|
-
currentUseCase,
|
|
4636
|
-
input,
|
|
4637
|
-
extraInstructions
|
|
4681
|
+
const publicEnvVarPrefix = await publicEnvVarPrefixPromise;
|
|
4682
|
+
const input = {
|
|
4683
|
+
findings: normalized,
|
|
4684
|
+
confirmed: confirmed2,
|
|
4685
|
+
searchLocation,
|
|
4686
|
+
targetIndex,
|
|
4687
|
+
language,
|
|
4688
|
+
publicEnvVarPrefix,
|
|
4689
|
+
appId,
|
|
4690
|
+
searchEnvVars: publicSearchEnvVars(
|
|
4691
|
+
publicEnvVarPrefix,
|
|
4692
|
+
targetIndex,
|
|
4693
|
+
appId
|
|
4638
4694
|
),
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
if (
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
});
|
|
4653
|
-
}
|
|
4654
|
-
if (useCases.includes("ingestion")) {
|
|
4655
|
-
let ingestFailureDetail;
|
|
4656
|
-
const result = await runImplementationUseCase("ingestion");
|
|
4657
|
-
summaries.push(formatSummary("ingestion", result.summary));
|
|
4658
|
-
ingestCommand = result.ingestCommand;
|
|
4659
|
-
const ingestionContext = ingestionTools ?? searchTools;
|
|
4660
|
-
const executions = ingestionContext.shell.executions;
|
|
4661
|
-
const {
|
|
4662
|
-
run: ingestRun,
|
|
4663
|
-
attempt: ingestAttempt,
|
|
4664
|
-
recordCount
|
|
4665
|
-
} = ingestOutcome(executions, ingestCommand);
|
|
4666
|
-
ingestScriptRan = ingestRun != null;
|
|
4667
|
-
ingestRecordCount = recordCount;
|
|
4668
|
-
ingestDurationMs = ingestRun?.durationMs;
|
|
4669
|
-
if (ingestScriptRan && ingestionContext.reviewed.length === 0) {
|
|
4670
|
-
summaries.push(
|
|
4671
|
-
"\u26A0\uFE0F The ingestion script ran without being shown to you for review. Read it in your project before trusting the index contents."
|
|
4672
|
-
);
|
|
4673
|
-
logger.warn(
|
|
4674
|
-
{ ingestCommand },
|
|
4675
|
-
"implement: ingestion ran without a reviewScript call"
|
|
4695
|
+
ingestDir: INGEST_DIR,
|
|
4696
|
+
ingestionSource,
|
|
4697
|
+
uploadFilePath,
|
|
4698
|
+
searchUiTarget: searchUiTarget(language)
|
|
4699
|
+
};
|
|
4700
|
+
const summaries = [];
|
|
4701
|
+
if (uploadWarning) summaries.push(uploadWarning);
|
|
4702
|
+
let envSearchKey;
|
|
4703
|
+
let envAppIdMismatch = false;
|
|
4704
|
+
if (useCases.includes("search") && appId) {
|
|
4705
|
+
const envAppId = await readEnvVar(
|
|
4706
|
+
worktree,
|
|
4707
|
+
publicAppIdVar(publicEnvVarPrefix)
|
|
4676
4708
|
);
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4709
|
+
if (envAppId === appId) {
|
|
4710
|
+
envSearchKey = await readEnvVar(
|
|
4711
|
+
worktree,
|
|
4712
|
+
publicSearchKeyVar(publicEnvVarPrefix)
|
|
4713
|
+
);
|
|
4714
|
+
} else if (envAppId) {
|
|
4715
|
+
envAppIdMismatch = true;
|
|
4716
|
+
const appIdVarName = publicAppIdVar(publicEnvVarPrefix);
|
|
4717
|
+
const searchKeyVarName = publicSearchKeyVar(publicEnvVarPrefix);
|
|
4718
|
+
summaries.push(
|
|
4719
|
+
`\u26A0\uFE0F .env already sets ${appIdVarName}=${envAppId}, but the active Algolia application is ${appId}. The wizard left those values alone \u2014 update ${appIdVarName} and ${searchKeyVarName} by hand, or searches will fail.`
|
|
4720
|
+
);
|
|
4721
|
+
logger.warn(
|
|
4722
|
+
{ envAppId, appId },
|
|
4723
|
+
"implement: .env holds credentials for a different Algolia application; not reusing its search key"
|
|
4724
|
+
);
|
|
4686
4725
|
}
|
|
4687
|
-
}
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4726
|
+
}
|
|
4727
|
+
let finalSearchEnvVars = input.searchEnvVars;
|
|
4728
|
+
let agentRuns = 0;
|
|
4729
|
+
let ingestCommand;
|
|
4730
|
+
let ingestScriptRan = false;
|
|
4731
|
+
let ingestRecordCount;
|
|
4732
|
+
let ingestDurationMs;
|
|
4733
|
+
let ingestOutcomeMessage;
|
|
4734
|
+
const ingestKeyAppId = ingestAppId;
|
|
4735
|
+
const ingestionTools = ingestKeyAppId ? makeToolContext(worktree, async () => ({
|
|
4736
|
+
[APP_ID_VAR]: ingestKeyAppId,
|
|
4737
|
+
[API_KEY_VAR]: (await resolveWriteKey(targetIndex, ingestKeyAppId)).key,
|
|
4738
|
+
[INDEX_NAME_VAR]: targetIndex
|
|
4739
|
+
})) : void 0;
|
|
4740
|
+
const searchTools = makeToolContext(worktree);
|
|
4741
|
+
async function runImplementationUseCase(currentUseCase, extraInstructions = []) {
|
|
4742
|
+
if (agentRuns > 0) ctx.recordStepExecution();
|
|
4743
|
+
agentRuns += 1;
|
|
4744
|
+
return runAgent({
|
|
4745
|
+
instructions: buildAgentInstructions(
|
|
4746
|
+
currentUseCase,
|
|
4747
|
+
input,
|
|
4748
|
+
extraInstructions
|
|
4749
|
+
),
|
|
4750
|
+
tools: toolsForUseCase(currentUseCase, input.ingestionSource),
|
|
4751
|
+
outputSchema: implementationOutputSchema,
|
|
4752
|
+
toolContext: currentUseCase === "ingestion" ? ingestionTools ?? searchTools : searchTools
|
|
4710
4753
|
});
|
|
4711
4754
|
}
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
4755
|
+
async function runVerificationUseCase() {
|
|
4756
|
+
if (agentRuns > 0) ctx.recordStepExecution();
|
|
4757
|
+
agentRuns += 1;
|
|
4758
|
+
return runAgent({
|
|
4759
|
+
instructions: buildAgentInstructions("verification", input),
|
|
4760
|
+
tools: toolsForUseCase("verification"),
|
|
4761
|
+
outputSchema: verificationOutputSchema,
|
|
4762
|
+
toolContext: searchTools
|
|
4763
|
+
});
|
|
4764
|
+
}
|
|
4765
|
+
if (useCases.includes("ingestion")) {
|
|
4766
|
+
let ingestFailureDetail;
|
|
4767
|
+
const result = await runImplementationUseCase("ingestion");
|
|
4768
|
+
summaries.push(formatSummary("ingestion", result.summary));
|
|
4769
|
+
ingestCommand = result.ingestCommand;
|
|
4770
|
+
const ingestionContext = ingestionTools ?? searchTools;
|
|
4771
|
+
const executions = ingestionContext.shell.executions;
|
|
4772
|
+
const {
|
|
4773
|
+
run: ingestRun,
|
|
4774
|
+
attempt: ingestAttempt,
|
|
4775
|
+
recordCount
|
|
4776
|
+
} = ingestOutcome(executions, ingestCommand);
|
|
4777
|
+
ingestScriptRan = ingestRun != null;
|
|
4778
|
+
ingestRecordCount = recordCount;
|
|
4779
|
+
ingestDurationMs = ingestRun?.durationMs;
|
|
4780
|
+
if (ingestScriptRan && ingestionContext.reviewed.length === 0) {
|
|
4781
|
+
summaries.push(
|
|
4782
|
+
"\u26A0\uFE0F The ingestion script ran without being shown to you for review. Read it in the worktree before trusting the index contents."
|
|
4738
4783
|
);
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
extraInstructions
|
|
4743
|
-
);
|
|
4744
|
-
summaries.push(formatSummary("search", summary));
|
|
4745
|
-
const verification = await runVerificationUseCase();
|
|
4746
|
-
summaries.push(formatSummary("verification", verification.summary));
|
|
4747
|
-
if (verification.sufficient) {
|
|
4748
|
-
ctx.setUserInput("implementation", "success");
|
|
4749
|
-
const searchFilesChanged = [
|
|
4750
|
-
...new Set(useWizard.getState().writtenFiles)
|
|
4751
|
-
].map((file) => relative6(repoRoot, file));
|
|
4752
|
-
track("AI Wizard Frontend Component Generated", {
|
|
4753
|
-
filePaths: searchFilesChanged
|
|
4754
|
-
});
|
|
4755
|
-
track("AI Wizard Wired to UI", {
|
|
4756
|
-
location_heuristic: searchLocation ?? "unknown"
|
|
4757
|
-
});
|
|
4758
|
-
break;
|
|
4759
|
-
}
|
|
4760
|
-
if (attempt === MAX_IMPLEMENT_VERIFICATION_ATTEMPTS) {
|
|
4761
|
-
ctx.setUserInput("implementation", "fail");
|
|
4762
|
-
throw new Error(
|
|
4763
|
-
`Implementation verification failed after ${MAX_IMPLEMENT_VERIFICATION_ATTEMPTS} attempts: ${verification.additionalInstructions ?? verification.summary}`
|
|
4784
|
+
logger.warn(
|
|
4785
|
+
{ ingestCommand },
|
|
4786
|
+
"implement: ingestion ran without a reviewScript call"
|
|
4764
4787
|
);
|
|
4765
4788
|
}
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
);
|
|
4777
|
-
|
|
4789
|
+
if (ingestScriptRan) {
|
|
4790
|
+
ingestOutcomeMessage = `\u2705 Ingestion succeeded${ingestRecordCount != null ? ` \u2014 ${ingestRecordCount} record(s) indexed.` : "."}`;
|
|
4791
|
+
if (ingestRecordCount != null) {
|
|
4792
|
+
track("AI Wizard Ingest Successful", {
|
|
4793
|
+
entity_name: confirmed2?.map((e) => e.name).join(", ") || "unknown",
|
|
4794
|
+
record_count: ingestRecordCount,
|
|
4795
|
+
duration_ms: ingestDurationMs ?? 0
|
|
4796
|
+
});
|
|
4797
|
+
}
|
|
4798
|
+
} else {
|
|
4799
|
+
const { reason, detail } = ingestFailure(ingestAttempt, executions);
|
|
4800
|
+
ingestOutcomeMessage = `\u26A0\uFE0F Records were not indexed \u2014 ${reason}.${ingestCommand ? " Run the command below when you are ready." : ""}`;
|
|
4801
|
+
ingestFailureDetail = detail;
|
|
4778
4802
|
summaries.push(
|
|
4779
|
-
|
|
4803
|
+
`\u26A0\uFE0F Ingestion did not complete: ${reason}.${detail ? `
|
|
4804
|
+
${detail}` : ""}`
|
|
4780
4805
|
);
|
|
4781
|
-
} catch (err) {
|
|
4782
|
-
searchKeyError = err.message;
|
|
4783
4806
|
logger.warn(
|
|
4784
|
-
{
|
|
4785
|
-
|
|
4807
|
+
{
|
|
4808
|
+
ingestCommand,
|
|
4809
|
+
reason,
|
|
4810
|
+
approved: ingestAttempt?.approved,
|
|
4811
|
+
exitCode: ingestAttempt?.exitCode,
|
|
4812
|
+
timedOut: ingestAttempt?.timedOut,
|
|
4813
|
+
commandsRun: executions.length
|
|
4814
|
+
},
|
|
4815
|
+
"implement: ingestion script did not complete successfully"
|
|
4786
4816
|
);
|
|
4817
|
+
track("Error", {
|
|
4818
|
+
step: "Push Data",
|
|
4819
|
+
error: `ingestion did not complete: ${reason}`,
|
|
4820
|
+
product_area: "AI Wizard"
|
|
4821
|
+
});
|
|
4787
4822
|
}
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
(v) => !v.value.startsWith("<")
|
|
4792
|
-
);
|
|
4793
|
-
if (resolvedSearchEnvVars.length > 0) {
|
|
4794
|
-
const written = await writeSearchEnvValues(
|
|
4795
|
-
repoRoot,
|
|
4796
|
-
resolvedSearchEnvVars
|
|
4797
|
-
);
|
|
4798
|
-
if (written.length > 0) {
|
|
4799
|
-
summaries.push(`Wrote ${written.join(", ")} to .env.`);
|
|
4823
|
+
const commandMessages = [`Open the worktree: cd ${shellQuote(worktree)}`];
|
|
4824
|
+
if (ingestCommand) {
|
|
4825
|
+
commandMessages.push(`Ingestion command: ${ingestCommand}`);
|
|
4800
4826
|
}
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4827
|
+
await ctx.requestUserInput({
|
|
4828
|
+
prompt: "",
|
|
4829
|
+
promptType: "enterToContinue",
|
|
4830
|
+
options: [],
|
|
4831
|
+
// The wizard never streams command output, so a failed run's tail is the
|
|
4832
|
+
// only place the developer sees why it failed. One message per line:
|
|
4833
|
+
// the panel's height accounting counts a message as one wrapped line
|
|
4834
|
+
// (see Notices.tsx), so an embedded newline overflows it.
|
|
4835
|
+
messages: [
|
|
4836
|
+
ingestOutcomeMessage,
|
|
4837
|
+
...ingestFailureDetail?.split("\n").filter((l) => l.trim()) ?? [],
|
|
4838
|
+
...commandMessages
|
|
4839
|
+
]
|
|
4840
|
+
});
|
|
4841
|
+
}
|
|
4842
|
+
if (useCases.includes("search")) {
|
|
4843
|
+
let extraInstructions = [];
|
|
4844
|
+
const preSearchFiles = new Set(await listChangedFiles(worktree));
|
|
4845
|
+
for (let attempt = 1; attempt <= MAX_IMPLEMENT_VERIFICATION_ATTEMPTS; attempt++) {
|
|
4846
|
+
if (attempt > 1) {
|
|
4847
|
+
logger.info(
|
|
4848
|
+
{
|
|
4849
|
+
attempt,
|
|
4850
|
+
maxAttempts: MAX_IMPLEMENT_VERIFICATION_ATTEMPTS,
|
|
4851
|
+
extraInstructions
|
|
4852
|
+
},
|
|
4853
|
+
"implement: retrying search implementation after failed verification"
|
|
4854
|
+
);
|
|
4855
|
+
}
|
|
4856
|
+
const { summary } = await runImplementationUseCase(
|
|
4857
|
+
"search",
|
|
4858
|
+
extraInstructions
|
|
4807
4859
|
);
|
|
4860
|
+
summaries.push(formatSummary("search", summary));
|
|
4861
|
+
const verification = await runVerificationUseCase();
|
|
4862
|
+
summaries.push(formatSummary("verification", verification.summary));
|
|
4863
|
+
if (verification.sufficient) {
|
|
4864
|
+
ctx.setUserInput("implementation", "success");
|
|
4865
|
+
const searchFilesChanged = (await listChangedFiles(worktree)).filter(
|
|
4866
|
+
(file) => !preSearchFiles.has(file)
|
|
4867
|
+
);
|
|
4868
|
+
track("AI Wizard Frontend Component Generated", {
|
|
4869
|
+
filePaths: searchFilesChanged
|
|
4870
|
+
});
|
|
4871
|
+
track("AI Wizard Wired to UI", {
|
|
4872
|
+
location_heuristic: searchLocation ?? "unknown"
|
|
4873
|
+
});
|
|
4874
|
+
break;
|
|
4875
|
+
}
|
|
4876
|
+
if (attempt === MAX_IMPLEMENT_VERIFICATION_ATTEMPTS) {
|
|
4877
|
+
ctx.setUserInput("implementation", "fail");
|
|
4878
|
+
throw new Error(
|
|
4879
|
+
`Implementation verification failed after ${MAX_IMPLEMENT_VERIFICATION_ATTEMPTS} attempts: ${verification.additionalInstructions ?? verification.summary}`
|
|
4880
|
+
);
|
|
4881
|
+
}
|
|
4882
|
+
extraInstructions = verificationRetryInstructions(verification);
|
|
4808
4883
|
}
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4884
|
+
let searchKey;
|
|
4885
|
+
let searchKeyError;
|
|
4886
|
+
if (appId) {
|
|
4887
|
+
try {
|
|
4888
|
+
const resolved2 = await resolveSearchOnlyKey(
|
|
4889
|
+
targetIndex,
|
|
4890
|
+
appId,
|
|
4891
|
+
envSearchKey
|
|
4892
|
+
);
|
|
4893
|
+
searchKey = resolved2.key;
|
|
4894
|
+
summaries.push(
|
|
4895
|
+
resolved2.source === "created" ? `Created a new search-only Algolia API key for the "${targetIndex}" index in app ${appId} \u2014 safe to expose in frontend code.` : `Reused the existing search-only Algolia API key for the "${targetIndex}" index in app ${appId}.`
|
|
4896
|
+
);
|
|
4897
|
+
} catch (err) {
|
|
4898
|
+
searchKeyError = err.message;
|
|
4899
|
+
logger.warn(
|
|
4900
|
+
{ err: searchKeyError },
|
|
4901
|
+
"implement: could not provision a search-only API key; the .env value stays a placeholder"
|
|
4902
|
+
);
|
|
4903
|
+
}
|
|
4814
4904
|
}
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
4905
|
+
finalSearchEnvVars = publicSearchEnvVars(
|
|
4906
|
+
publicEnvVarPrefix,
|
|
4907
|
+
targetIndex,
|
|
4908
|
+
appId,
|
|
4909
|
+
searchKey
|
|
4910
|
+
);
|
|
4911
|
+
const resolvedSearchEnvVars = finalSearchEnvVars.filter(
|
|
4912
|
+
(v) => !v.value.startsWith("<")
|
|
4913
|
+
);
|
|
4914
|
+
if (resolvedSearchEnvVars.length > 0) {
|
|
4915
|
+
const written = await writeSearchEnvValues(
|
|
4916
|
+
worktree,
|
|
4917
|
+
resolvedSearchEnvVars
|
|
4818
4918
|
);
|
|
4819
|
-
|
|
4820
|
-
{
|
|
4821
|
-
|
|
4919
|
+
if (written.length > 0) {
|
|
4920
|
+
summaries.push(`Wrote ${written.join(", ")} to .env.`);
|
|
4921
|
+
}
|
|
4922
|
+
const ignored = await ensureGitIgnored(worktree, join12(worktree, ".env"));
|
|
4923
|
+
if (ignored === "added") {
|
|
4924
|
+
summaries.push("Added .env to .gitignore.");
|
|
4925
|
+
} else if (ignored === "tracked") {
|
|
4926
|
+
summaries.push(
|
|
4927
|
+
'\u26A0\uFE0F .env is tracked by git, so a .gitignore rule cannot un-stage it. Run "git rm --cached .env" before committing, or the credentials go into history.'
|
|
4928
|
+
);
|
|
4929
|
+
}
|
|
4930
|
+
const stale = [];
|
|
4931
|
+
for (const v of resolvedSearchEnvVars) {
|
|
4932
|
+
if (written.includes(v.name)) continue;
|
|
4933
|
+
const current = await readEnvVar(worktree, v.name);
|
|
4934
|
+
if (current && current !== v.value) stale.push(v);
|
|
4935
|
+
}
|
|
4936
|
+
if (stale.length > 0 && !envAppIdMismatch) {
|
|
4937
|
+
summaries.push(
|
|
4938
|
+
`\u26A0\uFE0F .env already assigns a different value to ${stale.map((v) => `${v.name} (should be ${v.value})`).join(", ")} \u2014 the wizard left it alone. Fix it by hand, or searches will fail.`
|
|
4939
|
+
);
|
|
4940
|
+
logger.warn(
|
|
4941
|
+
{ vars: stale.map((v) => v.name) },
|
|
4942
|
+
"implement: .env holds different values for the resolved search credentials; not overwriting them"
|
|
4943
|
+
);
|
|
4944
|
+
}
|
|
4945
|
+
}
|
|
4946
|
+
const unresolvedSearchEnvVars = finalSearchEnvVars.filter(
|
|
4947
|
+
(v) => v.value.startsWith("<")
|
|
4948
|
+
);
|
|
4949
|
+
if (unresolvedSearchEnvVars.length > 0) {
|
|
4950
|
+
summaries.push(
|
|
4951
|
+
`Could not resolve a value for ${unresolvedSearchEnvVars.map((v) => v.name).join(", ")} \u2014 fill it in manually in .env.` + // Without the reason the line is a dead end.
|
|
4952
|
+
(searchKeyError ? ` Reason: ${searchKeyError}` : "")
|
|
4822
4953
|
);
|
|
4823
4954
|
}
|
|
4955
|
+
} else {
|
|
4956
|
+
ctx.setUserInput("implementation", "success");
|
|
4824
4957
|
}
|
|
4825
|
-
const
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
summaries.push(
|
|
4830
|
-
`Could not resolve a value for ${unresolvedSearchEnvVars.map((v) => v.name).join(", ")} \u2014 fill it in manually in .env.` + (searchKeyError ? ` Reason: ${searchKeyError}` : "")
|
|
4958
|
+
const filesChanged = await listChangedFiles(worktree);
|
|
4959
|
+
if (filesChanged.length === 0) {
|
|
4960
|
+
logger.warn(
|
|
4961
|
+
"implement: agent reported success but no files changed in the worktree"
|
|
4831
4962
|
);
|
|
4832
4963
|
}
|
|
4833
|
-
|
|
4834
|
-
|
|
4964
|
+
return {
|
|
4965
|
+
ingestionSource,
|
|
4966
|
+
filesChanged,
|
|
4967
|
+
summary: summaries.join("\n\n"),
|
|
4968
|
+
worktreePath: worktree,
|
|
4969
|
+
...useCases.includes("ingestion") && ingestCommand ? {
|
|
4970
|
+
ingestCommand,
|
|
4971
|
+
ingestScriptRan,
|
|
4972
|
+
...ingestRecordCount != null ? { ingestRecordCount } : {},
|
|
4973
|
+
...ingestDurationMs != null ? { ingestDurationMs } : {}
|
|
4974
|
+
} : {},
|
|
4975
|
+
...useCases.includes("search") ? { searchEnvVars: finalSearchEnvVars } : {}
|
|
4976
|
+
};
|
|
4977
|
+
} finally {
|
|
4978
|
+
process.chdir(repoRoot);
|
|
4835
4979
|
}
|
|
4836
|
-
return {
|
|
4837
|
-
ingestionSource,
|
|
4838
|
-
summary: summaries.join("\n\n"),
|
|
4839
|
-
...useCases.includes("ingestion") && ingestCommand ? {
|
|
4840
|
-
ingestCommand,
|
|
4841
|
-
ingestScriptRan,
|
|
4842
|
-
...ingestRecordCount != null ? { ingestRecordCount } : {},
|
|
4843
|
-
...ingestDurationMs != null ? { ingestDurationMs } : {}
|
|
4844
|
-
} : {},
|
|
4845
|
-
...useCases.includes("search") ? { searchEnvVars: finalSearchEnvVars } : {}
|
|
4846
|
-
};
|
|
4847
4980
|
}
|
|
4848
4981
|
|
|
4849
4982
|
// src/workflows/default.ts
|
|
@@ -4882,8 +5015,8 @@ var defaultWorkflow = {
|
|
|
4882
5015
|
defineStep({
|
|
4883
5016
|
id: "select-index",
|
|
4884
5017
|
title: "Set up index",
|
|
4885
|
-
outputSchema:
|
|
4886
|
-
selection:
|
|
5018
|
+
outputSchema: z30.object({
|
|
5019
|
+
selection: z30.string()
|
|
4887
5020
|
}),
|
|
4888
5021
|
run: (ctx) => selectIndexStep(ctx)
|
|
4889
5022
|
}),
|
|
@@ -4915,7 +5048,10 @@ var defaultWorkflow = {
|
|
|
4915
5048
|
ctx.notify({
|
|
4916
5049
|
messages: ["Building your Algolia search experience\u2026"]
|
|
4917
5050
|
});
|
|
4918
|
-
|
|
5051
|
+
const ingestion2 = ctx.getStepOutput(
|
|
5052
|
+
"ingestion"
|
|
5053
|
+
);
|
|
5054
|
+
return implement(ctx, ["search"], ingestion2?.worktreePath);
|
|
4919
5055
|
}
|
|
4920
5056
|
}),
|
|
4921
5057
|
defineStep({
|
|
@@ -4930,8 +5066,9 @@ var defaultWorkflow = {
|
|
|
4930
5066
|
"ingestion"
|
|
4931
5067
|
);
|
|
4932
5068
|
return reviewStep(ctx, {
|
|
4933
|
-
//
|
|
4934
|
-
//
|
|
5069
|
+
// The ingestion step already showed the user the exact `ingestCommand`
|
|
5070
|
+
// and worktree path as a notice, so nextSteps must not restate it —
|
|
5071
|
+
// an LLM-paraphrased command risks being wrong.
|
|
4935
5072
|
nextStepsGuidance: ingestion2?.ingestScriptRan ? "The wizard already ran the ingestion script and records are in the index. Do NOT tell the user to run it again; instead point them at the target index to confirm the records. Do not restate the ingestion command \u2014 the wizard already showed it to them." : "Tell the user to run the ingestion script; do not restate the exact command \u2014 the wizard already showed it to them above."
|
|
4936
5073
|
});
|
|
4937
5074
|
}
|
|
@@ -4951,6 +5088,7 @@ function getWorkflow(id) {
|
|
|
4951
5088
|
var projectScan2 = {
|
|
4952
5089
|
languages: [{ name: "TypeScript", version: "5.7.2" }],
|
|
4953
5090
|
frameworks: [{ name: "Next.js", version: "15.1.0" }],
|
|
5091
|
+
publicEnvVarPrefix: "NEXT_PUBLIC_",
|
|
4954
5092
|
ingestionAnalysis: [
|
|
4955
5093
|
{
|
|
4956
5094
|
name: "Product",
|
|
@@ -4977,6 +5115,7 @@ var selectIndex = {
|
|
|
4977
5115
|
selection: "wizard_seed_products"
|
|
4978
5116
|
};
|
|
4979
5117
|
var ingestion = {
|
|
5118
|
+
filesChanged: ["algolia/ingest.mjs", "algolia/records.json", "package.json"],
|
|
4980
5119
|
summary: "Generated sample Product records and an ingestion script that pushes them to the target index with algoliasearch.",
|
|
4981
5120
|
ingestCommand: "node algolia/ingest.mjs",
|
|
4982
5121
|
ingestScriptRan: true,
|
|
@@ -4988,6 +5127,7 @@ var confirmFramework2 = {
|
|
|
4988
5127
|
frameworks: projectScan2.frameworks
|
|
4989
5128
|
};
|
|
4990
5129
|
var search = {
|
|
5130
|
+
filesChanged: ["src/components/Search.tsx", "src/components/Header.tsx", ".env"],
|
|
4991
5131
|
summary: "Added an InstantSearch-powered search box and results list, mounted in the shared header component.",
|
|
4992
5132
|
ingestionSource: "generated",
|
|
4993
5133
|
searchEnvVars: [
|
|
@@ -5000,7 +5140,7 @@ var review = {
|
|
|
5000
5140
|
"Ingested 25 generated Product records into wizard_seed_products.",
|
|
5001
5141
|
"Added an InstantSearch search experience to the shared header."
|
|
5002
5142
|
],
|
|
5003
|
-
reviewPrompt: "Review the Algolia ingestion and search changes.",
|
|
5143
|
+
reviewPrompt: "Review the Algolia ingestion and search changes in this worktree.",
|
|
5004
5144
|
nextSteps: ["Point the ingestion script at your real product data."]
|
|
5005
5145
|
};
|
|
5006
5146
|
var SEEDS = {
|
|
@@ -5117,9 +5257,9 @@ Options:
|
|
|
5117
5257
|
steps pre-filled with test data. Pass with no value to print
|
|
5118
5258
|
the step ids. See CONTRIBUTING.md.
|
|
5119
5259
|
--no-telemetry Send no telemetry or analytics for this run.
|
|
5120
|
-
--reset-on-run Wipe this project's wizard state (run state, AI consent
|
|
5121
|
-
before starting, so the run behaves like a
|
|
5122
|
-
run. Also drops every API key the wizard has
|
|
5260
|
+
--reset-on-run Wipe this project's wizard state (run state, AI consent,
|
|
5261
|
+
worktrees) before starting, so the run behaves like a
|
|
5262
|
+
first-ever run. Also drops every API key the wizard has
|
|
5123
5263
|
stored in your keychain (or, where the platform has none,
|
|
5124
5264
|
the encrypted file it falls back to \u2014 see CONTRIBUTING.md),
|
|
5125
5265
|
for this project and any other, so later runs create new
|
|
@@ -5159,7 +5299,7 @@ function parseCliArgs(argv) {
|
|
|
5159
5299
|
}
|
|
5160
5300
|
|
|
5161
5301
|
// src/lib/resetState.ts
|
|
5162
|
-
import { readdir as
|
|
5302
|
+
import { readdir as readdir4, rm as rm2 } from "node:fs/promises";
|
|
5163
5303
|
import { join as join13 } from "node:path";
|
|
5164
5304
|
var KEEP = ["wizard.log"];
|
|
5165
5305
|
async function resetProjectState() {
|
|
@@ -5167,7 +5307,7 @@ async function resetProjectState() {
|
|
|
5167
5307
|
await forgetResolvedKeys();
|
|
5168
5308
|
let entries;
|
|
5169
5309
|
try {
|
|
5170
|
-
entries = await
|
|
5310
|
+
entries = await readdir4(dir);
|
|
5171
5311
|
} catch {
|
|
5172
5312
|
return { dir, removed: [] };
|
|
5173
5313
|
}
|
|
@@ -5180,6 +5320,45 @@ async function resetProjectState() {
|
|
|
5180
5320
|
return { dir, removed: targets };
|
|
5181
5321
|
}
|
|
5182
5322
|
|
|
5323
|
+
// src/lib/terminalSize.ts
|
|
5324
|
+
var MIN_ROWS = 30;
|
|
5325
|
+
var MIN_COLUMNS = 100;
|
|
5326
|
+
async function requestTerminalSize(stdout = process.stdout, {
|
|
5327
|
+
rows = MIN_ROWS,
|
|
5328
|
+
columns = MIN_COLUMNS,
|
|
5329
|
+
timeoutMs = 150,
|
|
5330
|
+
settleMs = 120
|
|
5331
|
+
} = {}) {
|
|
5332
|
+
if (!stdout.isTTY) return;
|
|
5333
|
+
const currentRows = stdout.rows ?? 0;
|
|
5334
|
+
const currentColumns = stdout.columns ?? 0;
|
|
5335
|
+
if (currentRows >= rows && currentColumns >= columns) return;
|
|
5336
|
+
stdout.write(
|
|
5337
|
+
`\x1B[8;${Math.max(currentRows, rows)};${Math.max(currentColumns, columns)}t`
|
|
5338
|
+
);
|
|
5339
|
+
const resized = await waitForResize(stdout, timeoutMs);
|
|
5340
|
+
if (!resized) return;
|
|
5341
|
+
await delay(settleMs);
|
|
5342
|
+
stdout.write("\x1B[2J\x1B[H");
|
|
5343
|
+
}
|
|
5344
|
+
function waitForResize(stdout, timeoutMs) {
|
|
5345
|
+
return new Promise((resolve4) => {
|
|
5346
|
+
const finish = (didResize) => () => {
|
|
5347
|
+
clearTimeout(timer);
|
|
5348
|
+
stdout.off("resize", onResize);
|
|
5349
|
+
resolve4(didResize);
|
|
5350
|
+
};
|
|
5351
|
+
const onResize = finish(true);
|
|
5352
|
+
const timer = setTimeout(finish(false), timeoutMs);
|
|
5353
|
+
stdout.once("resize", onResize);
|
|
5354
|
+
});
|
|
5355
|
+
}
|
|
5356
|
+
function delay(ms) {
|
|
5357
|
+
return new Promise((resolve4) => {
|
|
5358
|
+
setTimeout(resolve4, ms);
|
|
5359
|
+
});
|
|
5360
|
+
}
|
|
5361
|
+
|
|
5183
5362
|
// src/main.tsx
|
|
5184
5363
|
import { jsx as jsx15 } from "react/jsx-runtime";
|
|
5185
5364
|
async function startup() {
|
|
@@ -5264,6 +5443,7 @@ async function run(workflow) {
|
|
|
5264
5443
|
}
|
|
5265
5444
|
runWorkflow(workflow, app.id);
|
|
5266
5445
|
}
|
|
5446
|
+
await requestTerminalSize();
|
|
5267
5447
|
var started = await startup();
|
|
5268
5448
|
if (typeof started === "number") {
|
|
5269
5449
|
process.exitCode = started;
|