@algolia/wizard 0.18.0 → 0.19.0-rc.111.186
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 +1 -1
- package/dist/main.js +314 -383
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ npx @algolia/wizard --help
|
|
|
23
23
|
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
24
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
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
|
|
26
|
+
| `--reset-on-run` | Wipe this project's wizard state (run state, AI-changes consent) before starting, so the run behaves like a first-ever run. Credentials are untouched. |
|
|
27
27
|
| `-h`, `--help` | Print usage. |
|
|
28
28
|
|
|
29
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).
|
package/dist/main.js
CHANGED
|
@@ -1200,7 +1200,7 @@ var accessItems = [
|
|
|
1200
1200
|
{
|
|
1201
1201
|
tag: "WRITE",
|
|
1202
1202
|
title: "Code changes",
|
|
1203
|
-
description: "creates & edits files (search UI, config) in
|
|
1203
|
+
description: "creates & edits files (search UI, config) directly in your branch. You are asked to confirm first if it has uncommitted changes."
|
|
1204
1204
|
},
|
|
1205
1205
|
{
|
|
1206
1206
|
tag: "EXEC",
|
|
@@ -1215,7 +1215,7 @@ var accessItems = [
|
|
|
1215
1215
|
{
|
|
1216
1216
|
tag: "KEY",
|
|
1217
1217
|
title: "Credentials",
|
|
1218
|
-
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 your project."
|
|
1219
1219
|
}
|
|
1220
1220
|
];
|
|
1221
1221
|
var neverItems = [
|
|
@@ -3246,7 +3246,7 @@ var anthropic = createAnthropic({
|
|
|
3246
3246
|
});
|
|
3247
3247
|
function generateRecordTool(ctx) {
|
|
3248
3248
|
return tool10({
|
|
3249
|
-
description: "Generate realistic sample records for an entity and write them to a JSON file
|
|
3249
|
+
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.",
|
|
3250
3250
|
inputSchema: z17.object({
|
|
3251
3251
|
entityName: z17.string().describe("Name of the entity to generate records for."),
|
|
3252
3252
|
attributes: z17.array(z17.string()).describe("Attribute names each record must contain."),
|
|
@@ -3604,7 +3604,7 @@ async function runAnalysis(mode, extraInstructions = []) {
|
|
|
3604
3604
|
// package.json
|
|
3605
3605
|
var package_default = {
|
|
3606
3606
|
name: "@algolia/wizard",
|
|
3607
|
-
version: "0.
|
|
3607
|
+
version: "0.19.0-rc.111.186",
|
|
3608
3608
|
description: "Magically implement Algolia functionality in your codebase",
|
|
3609
3609
|
type: "module",
|
|
3610
3610
|
engines: {
|
|
@@ -3971,11 +3971,10 @@ ${JSON.stringify(s.output, null, 2)}`
|
|
|
3971
3971
|
function formatReviewSummary(result) {
|
|
3972
3972
|
const nextStepLines = result.nextSteps.map((step) => {
|
|
3973
3973
|
const isIngestCommand = step.includes(".algolia-wizard/ingest.sh");
|
|
3974
|
-
const isWorktreeCommand = step.includes("/worktrees/");
|
|
3975
3974
|
return {
|
|
3976
3975
|
text: `\u2192 ${step}`,
|
|
3977
|
-
color: isIngestCommand ? COLORS.brand :
|
|
3978
|
-
bold: isIngestCommand
|
|
3976
|
+
color: isIngestCommand ? COLORS.brand : void 0,
|
|
3977
|
+
bold: isIngestCommand
|
|
3979
3978
|
};
|
|
3980
3979
|
});
|
|
3981
3980
|
return [
|
|
@@ -4012,13 +4011,11 @@ ${formatCompletedSteps(ctx.completedSteps)}`,
|
|
|
4012
4011
|
import z28 from "zod";
|
|
4013
4012
|
import { join as join12 } from "node:path";
|
|
4014
4013
|
|
|
4015
|
-
// src/lib/
|
|
4014
|
+
// src/lib/git.ts
|
|
4016
4015
|
import { execFile as execFile2 } from "node:child_process";
|
|
4017
|
-
import { copyFile, mkdir as mkdir6,
|
|
4016
|
+
import { copyFile, mkdir as mkdir6, readFile as readFile8, stat as stat3, writeFile as writeFile7 } from "node:fs/promises";
|
|
4018
4017
|
import { basename as basename2, dirname as dirname7, isAbsolute as isAbsolute2, join as join10, resolve as resolve3 } from "node:path";
|
|
4019
4018
|
var MAX_BUFFER = 32 * 1024 * 1024;
|
|
4020
|
-
var MAX_WIZARD_WORKTREES = 3;
|
|
4021
|
-
var WIZARD_BRANCH_PREFIX = "wizard/implement-";
|
|
4022
4019
|
function git(args) {
|
|
4023
4020
|
return new Promise((resolve4, reject) => {
|
|
4024
4021
|
execFile2("git", args, { maxBuffer: MAX_BUFFER }, (err, stdout, stderr) => {
|
|
@@ -4045,40 +4042,7 @@ async function isWorkingTreeDirty(repoRoot) {
|
|
|
4045
4042
|
const out = await git(["-C", repoRoot, "status", "--porcelain"]);
|
|
4046
4043
|
return out.trim().length > 0;
|
|
4047
4044
|
}
|
|
4048
|
-
async function
|
|
4049
|
-
const dir = join10(stateDir(repoRoot), "worktrees");
|
|
4050
|
-
const stale = (await readdir3(dir).catch(() => [])).filter((name) => /^wizard-implement-\d+$/.test(name)).sort().reverse().slice(MAX_WIZARD_WORKTREES - 1);
|
|
4051
|
-
for (const slug of stale) {
|
|
4052
|
-
const branch = slug.replace("wizard-implement-", WIZARD_BRANCH_PREFIX);
|
|
4053
|
-
try {
|
|
4054
|
-
await git([
|
|
4055
|
-
"-C",
|
|
4056
|
-
repoRoot,
|
|
4057
|
-
"worktree",
|
|
4058
|
-
"remove",
|
|
4059
|
-
"--force",
|
|
4060
|
-
join10(dir, slug)
|
|
4061
|
-
]);
|
|
4062
|
-
await git(["-C", repoRoot, "branch", "-D", branch]);
|
|
4063
|
-
} catch (err) {
|
|
4064
|
-
logger.warn(
|
|
4065
|
-
{ branch, err: err.message },
|
|
4066
|
-
"createWorktree: failed to prune a stale wizard worktree; continuing"
|
|
4067
|
-
);
|
|
4068
|
-
}
|
|
4069
|
-
}
|
|
4070
|
-
}
|
|
4071
|
-
async function createWorktree(repoRoot) {
|
|
4072
|
-
const branch = `${WIZARD_BRANCH_PREFIX}${Date.now()}`;
|
|
4073
|
-
const dirSlug = branch.replace(/\//g, "-");
|
|
4074
|
-
const path = join10(stateDir(repoRoot), "worktrees", dirSlug);
|
|
4075
|
-
await git(["-C", repoRoot, "worktree", "prune"]);
|
|
4076
|
-
await pruneOldWorktrees(repoRoot);
|
|
4077
|
-
await mkdir6(dirname7(path), { recursive: true });
|
|
4078
|
-
await git(["-C", repoRoot, "worktree", "add", "-b", branch, path, "HEAD"]);
|
|
4079
|
-
return { path, branch };
|
|
4080
|
-
}
|
|
4081
|
-
async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourcePath) {
|
|
4045
|
+
async function copyUploadIntoProject(repoRoot, ingestDir, sourcePath) {
|
|
4082
4046
|
const trimmed = sourcePath.trim();
|
|
4083
4047
|
if (!trimmed) {
|
|
4084
4048
|
return { ok: false, reason: "no file path was provided" };
|
|
@@ -4092,7 +4056,10 @@ async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourceP
|
|
|
4092
4056
|
return { ok: false, reason: `"${sourcePath}" does not exist` };
|
|
4093
4057
|
}
|
|
4094
4058
|
const relPath = join10(ingestDir, basename2(source));
|
|
4095
|
-
const dest = join10(
|
|
4059
|
+
const dest = join10(repoRoot, relPath);
|
|
4060
|
+
if (resolve3(source) === resolve3(dest)) {
|
|
4061
|
+
return { ok: true, relPath };
|
|
4062
|
+
}
|
|
4096
4063
|
try {
|
|
4097
4064
|
await mkdir6(dirname7(dest), { recursive: true });
|
|
4098
4065
|
await copyFile(source, dest);
|
|
@@ -4107,10 +4074,10 @@ async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourceP
|
|
|
4107
4074
|
function hasEnvVar(content, name) {
|
|
4108
4075
|
return new RegExp(`^(\\s*(?:export\\s+)?${name})\\s*=`, "m").test(content);
|
|
4109
4076
|
}
|
|
4110
|
-
async function readEnvVar(
|
|
4077
|
+
async function readEnvVar(repoRoot, name) {
|
|
4111
4078
|
let content;
|
|
4112
4079
|
try {
|
|
4113
|
-
content = await readFile8(join10(
|
|
4080
|
+
content = await readFile8(join10(repoRoot, ".env"), "utf8");
|
|
4114
4081
|
} catch (err) {
|
|
4115
4082
|
if (err.code !== "ENOENT") throw err;
|
|
4116
4083
|
return void 0;
|
|
@@ -4124,8 +4091,8 @@ async function readEnvVar(worktreePath, name) {
|
|
|
4124
4091
|
if (!value || value.startsWith("<")) return void 0;
|
|
4125
4092
|
return value;
|
|
4126
4093
|
}
|
|
4127
|
-
async function writeSearchEnvValues(
|
|
4128
|
-
const target = join10(
|
|
4094
|
+
async function writeSearchEnvValues(repoRoot, vars) {
|
|
4095
|
+
const target = join10(repoRoot, ".env");
|
|
4129
4096
|
let existing = "";
|
|
4130
4097
|
try {
|
|
4131
4098
|
existing = await readFile8(target, "utf8");
|
|
@@ -4140,8 +4107,8 @@ async function writeSearchEnvValues(worktreePath, vars) {
|
|
|
4140
4107
|
await writeFile7(target, existing + prefix + lines, "utf8");
|
|
4141
4108
|
return missing.map((v) => v.name);
|
|
4142
4109
|
}
|
|
4143
|
-
async function listChangedFiles(
|
|
4144
|
-
const raw = await git(["-C",
|
|
4110
|
+
async function listChangedFiles(repoRoot) {
|
|
4111
|
+
const raw = await git(["-C", repoRoot, "status", "--porcelain", "-z"]);
|
|
4145
4112
|
const entries = raw.split("\0");
|
|
4146
4113
|
const files = [];
|
|
4147
4114
|
for (let i = 0; i < entries.length; i += 1) {
|
|
@@ -4157,20 +4124,20 @@ function normalizeFindingPaths(findings) {
|
|
|
4157
4124
|
...findings,
|
|
4158
4125
|
ingestionAnalysis: findings.ingestionAnalysis?.map((e) => ({
|
|
4159
4126
|
...e,
|
|
4160
|
-
paths: e.paths.map(
|
|
4127
|
+
paths: e.paths.map(toRootRelative)
|
|
4161
4128
|
})),
|
|
4162
4129
|
searchImplementationAnalysis: findings.searchImplementationAnalysis ? normalizeSearchLocation(findings.searchImplementationAnalysis) : void 0,
|
|
4163
4130
|
confirmedEntities: findings.confirmedEntities?.map((e) => ({
|
|
4164
4131
|
...e,
|
|
4165
|
-
paths: e.paths.map(
|
|
4132
|
+
paths: e.paths.map(toRootRelative)
|
|
4166
4133
|
}))
|
|
4167
4134
|
};
|
|
4168
4135
|
}
|
|
4169
4136
|
function normalizeSearchLocation(path) {
|
|
4170
|
-
const normalized = path ?
|
|
4137
|
+
const normalized = path ? toRootRelative(path).trim() : "";
|
|
4171
4138
|
return normalized && normalized.toLowerCase() !== "unknown" ? normalized : void 0;
|
|
4172
4139
|
}
|
|
4173
|
-
function
|
|
4140
|
+
function toRootRelative(p) {
|
|
4174
4141
|
return p.replace(/^\/+/, "");
|
|
4175
4142
|
}
|
|
4176
4143
|
async function confirmDirtyWorkingTree(ctx, repoRoot) {
|
|
@@ -4179,19 +4146,19 @@ async function confirmDirtyWorkingTree(ctx, repoRoot) {
|
|
|
4179
4146
|
const shown = dirty.slice(0, MAX_LISTED_DIRTY_FILES);
|
|
4180
4147
|
const overflow = dirty.length - shown.length;
|
|
4181
4148
|
const answer = await ctx.requestUserInput({
|
|
4182
|
-
prompt: "Proceed
|
|
4149
|
+
prompt: "Proceed with uncommitted changes in your checkout? The wizard will edit files there directly, alongside them.",
|
|
4183
4150
|
promptType: "acceptReject",
|
|
4184
4151
|
options: [],
|
|
4185
4152
|
messages: [
|
|
4186
|
-
`${dirty.length} uncommitted change(s) detected
|
|
4153
|
+
`${dirty.length} uncommitted change(s) detected:`,
|
|
4187
4154
|
...shown.map((file) => ` \u2022 ${file}`),
|
|
4188
4155
|
...overflow > 0 ? [` \u2022 \u2026and ${overflow} more`] : [],
|
|
4189
|
-
"Commit or stash them first
|
|
4156
|
+
"Commit or stash them first if you want to review the wizard's changes on their own."
|
|
4190
4157
|
]
|
|
4191
4158
|
});
|
|
4192
4159
|
if (answer !== true) {
|
|
4193
4160
|
throw new Error(
|
|
4194
|
-
"implement aborted: commit or stash your changes
|
|
4161
|
+
"implement aborted: commit or stash your changes first, then re-run the wizard."
|
|
4195
4162
|
);
|
|
4196
4163
|
}
|
|
4197
4164
|
}
|
|
@@ -4254,16 +4221,10 @@ function getFrameworkSpecificDoc(frameworks) {
|
|
|
4254
4221
|
return loadAlgoliaDoc("js");
|
|
4255
4222
|
}
|
|
4256
4223
|
|
|
4257
|
-
// src/lib/shell.ts
|
|
4258
|
-
function shellQuote(value) {
|
|
4259
|
-
return "'" + value.replace(/'/g, "'\\''") + "'";
|
|
4260
|
-
}
|
|
4261
|
-
|
|
4262
4224
|
// src/actions/implement.ts
|
|
4263
4225
|
var implementSchema = z28.object({
|
|
4264
4226
|
filesChanged: z28.array(z28.string()),
|
|
4265
4227
|
summary: z28.string(),
|
|
4266
|
-
worktreePath: z28.string().optional(),
|
|
4267
4228
|
ingestCommand: z28.string().optional(),
|
|
4268
4229
|
ingestScriptRan: z28.boolean().optional(),
|
|
4269
4230
|
ingestRecordCount: z28.number().optional(),
|
|
@@ -4352,9 +4313,8 @@ function searchEnvVars(language, index, appId, searchKey) {
|
|
|
4352
4313
|
name: searchKeyVar(language),
|
|
4353
4314
|
value: searchKey ?? "<your-algolia-search-only-api-key>"
|
|
4354
4315
|
},
|
|
4355
|
-
// Wizard-supplied rather than written into the generated code
|
|
4356
|
-
//
|
|
4357
|
-
// leaves the UI querying an index that does not exist.
|
|
4316
|
+
// Wizard-supplied rather than written into the generated code: an agent
|
|
4317
|
+
// that retypes the name leaves the UI querying an index that doesn't exist.
|
|
4358
4318
|
{
|
|
4359
4319
|
name: searchIndexVar(language),
|
|
4360
4320
|
value: index
|
|
@@ -4363,8 +4323,6 @@ function searchEnvVars(language, index, appId, searchKey) {
|
|
|
4363
4323
|
}
|
|
4364
4324
|
function baseInstructions(input) {
|
|
4365
4325
|
return [
|
|
4366
|
-
// Agents have renamed this (e.g. appending the project name), which the
|
|
4367
|
-
// index-scoped keys then reject with a 403.
|
|
4368
4326
|
`Target Algolia index, to be used exactly as written \u2014 never renamed, re-cased, prefixed, or suffixed: "${input.targetIndex}"`,
|
|
4369
4327
|
`Project languages and frameworks: ${JSON.stringify(input.language)}`,
|
|
4370
4328
|
"Make minimal, idiomatic changes; do not touch unrelated code.",
|
|
@@ -4382,14 +4340,14 @@ function sourceSpecificInstructions(input) {
|
|
|
4382
4340
|
"Add env vars for any local source access (DB URL, API base, file paths) that is not a fixed repo path."
|
|
4383
4341
|
],
|
|
4384
4342
|
fileUpload: [
|
|
4385
|
-
`Records come from the developer's file, already copied into the
|
|
4343
|
+
`Records come from the developer's file, already copied into the project at "${input.uploadFilePath}". Read and parse that exact file.`,
|
|
4386
4344
|
"Parse by extension: JSON = array of objects; CSV/TSV = header row maps to keys.",
|
|
4387
4345
|
"Map parsed columns/fields to the confirmed entity attributes.",
|
|
4388
4346
|
"Never fabricate, hardcode, or substitute a different file."
|
|
4389
4347
|
],
|
|
4390
4348
|
generated: [
|
|
4391
4349
|
"No real data source exists; use sample records for each confirmed entity.",
|
|
4392
|
-
"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
|
|
4350
|
+
"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.",
|
|
4393
4351
|
"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.",
|
|
4394
4352
|
"Add a prominent TODO where the developer swaps the generated records (and the JSON file under `.algolia-wizard/data/`) for their real record source."
|
|
4395
4353
|
]
|
|
@@ -4438,14 +4396,11 @@ function searchInstructions(input) {
|
|
|
4438
4396
|
"If a search box already exists, replace it with yours.",
|
|
4439
4397
|
`Read the index name from the ${searchIndexVar(input.language)} env var, which the wizard sets to "${input.targetIndex}". Never hardcode an index name or derive one from the project, file, or component name.`,
|
|
4440
4398
|
"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.",
|
|
4441
|
-
// The key is provisioned only after verification passes,
|
|
4442
|
-
//
|
|
4443
|
-
//
|
|
4444
|
-
// would be reused as if it were real.
|
|
4399
|
+
// The key is provisioned only after verification passes, and the wizard
|
|
4400
|
+
// reads .env to decide whether a key already exists — an agent-invented
|
|
4401
|
+
// value there would be reused as if it were real.
|
|
4445
4402
|
`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.`,
|
|
4446
|
-
//
|
|
4447
|
-
// ".env" right after this step, so a renamed prefix would leave the code
|
|
4448
|
-
// reading a var the wizard never wrote.
|
|
4403
|
+
// The wizard writes these exact names into .env right after this step.
|
|
4449
4404
|
`Use exactly these env var names in the code: ${input.searchEnvVars.map(({ name }) => name).join(", ")}.`,
|
|
4450
4405
|
"Install any Algolia packages you import with the project's own package manager via runShell, and declare them in the project's dependency manifest.",
|
|
4451
4406
|
"Match the styles of the application as closely as possible.",
|
|
@@ -4454,10 +4409,10 @@ function searchInstructions(input) {
|
|
|
4454
4409
|
}
|
|
4455
4410
|
function verificationInstructions(input) {
|
|
4456
4411
|
return [
|
|
4457
|
-
"Verify the Algolia implementation changes
|
|
4412
|
+
"Verify the Algolia implementation changes.",
|
|
4458
4413
|
`Verification tools found in the codebase: ${JSON.stringify(input.findings.verification ?? [])}.`,
|
|
4459
4414
|
"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.",
|
|
4460
|
-
"
|
|
4415
|
+
"If a check fails because packages or modules are missing, install the dependencies via runShell and re-run it rather than changing the code.",
|
|
4461
4416
|
"For issues caused by the implementation, make minimal fixes with writeFile and re-run the checks.",
|
|
4462
4417
|
"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.",
|
|
4463
4418
|
"Do not add new Algolia functionality here; only validate and make minimal correctness fixes.",
|
|
@@ -4552,11 +4507,11 @@ function ingestFailure(attempt, executions) {
|
|
|
4552
4507
|
}
|
|
4553
4508
|
return { reason: `it failed (exit ${attempt.exitCode ?? "unknown"})`, detail };
|
|
4554
4509
|
}
|
|
4555
|
-
function makeToolContext(
|
|
4510
|
+
function makeToolContext(root, env = async () => ({})) {
|
|
4556
4511
|
return createToolContext(
|
|
4557
4512
|
DEFAULT_TOOL_LIMITS,
|
|
4558
|
-
|
|
4559
|
-
createShellContext({ env, approve: storeApproval(
|
|
4513
|
+
root,
|
|
4514
|
+
createShellContext({ env, approve: storeApproval(root) })
|
|
4560
4515
|
);
|
|
4561
4516
|
}
|
|
4562
4517
|
function verificationRetryInstructions(verification) {
|
|
@@ -4564,7 +4519,7 @@ function verificationRetryInstructions(verification) {
|
|
|
4564
4519
|
`Implementation insufficient. Address these findings before reporting completion: ${verification.additionalInstructions ?? verification.summary}`
|
|
4565
4520
|
];
|
|
4566
4521
|
}
|
|
4567
|
-
async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES
|
|
4522
|
+
async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES) {
|
|
4568
4523
|
const repoRoot = process.cwd();
|
|
4569
4524
|
const scan = ctx.getStepOutput("project-scan");
|
|
4570
4525
|
const entities = ctx.getStepOutput(
|
|
@@ -4630,9 +4585,11 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
|
|
|
4630
4585
|
const targetIndex = selected?.selection;
|
|
4631
4586
|
useWizard.getState().setTargetIndex(targetIndex ?? null);
|
|
4632
4587
|
await assertGitRepoWithHead(repoRoot);
|
|
4633
|
-
|
|
4588
|
+
const ingestionAlreadyRan = ctx.getStepOutput("ingestion") != null;
|
|
4589
|
+
if (!ingestionAlreadyRan && await isWorkingTreeDirty(repoRoot)) {
|
|
4634
4590
|
await confirmDirtyWorkingTree(ctx, repoRoot);
|
|
4635
4591
|
}
|
|
4592
|
+
const baselineFiles = new Set(await listChangedFiles(repoRoot));
|
|
4636
4593
|
const normalized = normalizeFindingPaths(findings);
|
|
4637
4594
|
const confirmed2 = normalized.confirmedEntities;
|
|
4638
4595
|
const searchLocation = normalized.searchImplementationAnalysis;
|
|
@@ -4644,314 +4601,292 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
|
|
|
4644
4601
|
if (useCases.includes("ingestion")) {
|
|
4645
4602
|
ingestAppId = appId ?? (await requireApplication()).id;
|
|
4646
4603
|
}
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4604
|
+
let uploadFilePath;
|
|
4605
|
+
let uploadWarning;
|
|
4606
|
+
if (ingestionSource === "fileUpload") {
|
|
4607
|
+
const copied = await copyUploadIntoProject(
|
|
4608
|
+
repoRoot,
|
|
4609
|
+
INGEST_DIR,
|
|
4610
|
+
uploadSourcePath ?? ""
|
|
4611
|
+
);
|
|
4612
|
+
if (copied.ok) {
|
|
4613
|
+
uploadFilePath = copied.relPath;
|
|
4614
|
+
} else {
|
|
4615
|
+
ingestionSource = "generated";
|
|
4616
|
+
uploadWarning = `\u26A0\uFE0F Could not use the uploaded file (${copied.reason}); generating sample records instead.`;
|
|
4617
|
+
logger.warn(
|
|
4618
|
+
{ reason: copied.reason },
|
|
4619
|
+
"implement: file upload unavailable; falling back to generated sample records"
|
|
4658
4620
|
);
|
|
4659
|
-
if (copied.ok) {
|
|
4660
|
-
uploadFilePath = copied.relPath;
|
|
4661
|
-
} else {
|
|
4662
|
-
ingestionSource = "generated";
|
|
4663
|
-
uploadWarning = `\u26A0\uFE0F Could not use the uploaded file (${copied.reason}); generating sample records instead.`;
|
|
4664
|
-
logger.warn(
|
|
4665
|
-
{ reason: copied.reason },
|
|
4666
|
-
"implement: file upload unavailable; falling back to generated sample records"
|
|
4667
|
-
);
|
|
4668
|
-
}
|
|
4669
|
-
}
|
|
4670
|
-
const input = {
|
|
4671
|
-
findings: normalized,
|
|
4672
|
-
confirmed: confirmed2,
|
|
4673
|
-
searchLocation,
|
|
4674
|
-
targetIndex,
|
|
4675
|
-
language,
|
|
4676
|
-
appId,
|
|
4677
|
-
searchEnvVars: searchEnvVars(language, targetIndex, appId),
|
|
4678
|
-
ingestDir: INGEST_DIR,
|
|
4679
|
-
ingestionSource,
|
|
4680
|
-
uploadFilePath,
|
|
4681
|
-
searchUiTarget: searchUiTarget(language)
|
|
4682
|
-
};
|
|
4683
|
-
const summaries = [];
|
|
4684
|
-
if (uploadWarning) summaries.push(uploadWarning);
|
|
4685
|
-
let envSearchKey;
|
|
4686
|
-
let envAppIdMismatch = false;
|
|
4687
|
-
if (useCases.includes("search") && appId) {
|
|
4688
|
-
const envAppId = await readEnvVar(worktree, appIdVar(language));
|
|
4689
|
-
if (envAppId === appId) {
|
|
4690
|
-
envSearchKey = await readEnvVar(worktree, searchKeyVar(language));
|
|
4691
|
-
} else if (envAppId) {
|
|
4692
|
-
envAppIdMismatch = true;
|
|
4693
|
-
summaries.push(
|
|
4694
|
-
`\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.`
|
|
4695
|
-
);
|
|
4696
|
-
logger.warn(
|
|
4697
|
-
{ envAppId, appId },
|
|
4698
|
-
"implement: .env holds credentials for a different Algolia application; not reusing its search key"
|
|
4699
|
-
);
|
|
4700
|
-
}
|
|
4701
4621
|
}
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4622
|
+
}
|
|
4623
|
+
const input = {
|
|
4624
|
+
findings: normalized,
|
|
4625
|
+
confirmed: confirmed2,
|
|
4626
|
+
searchLocation,
|
|
4627
|
+
targetIndex,
|
|
4628
|
+
language,
|
|
4629
|
+
appId,
|
|
4630
|
+
searchEnvVars: searchEnvVars(language, targetIndex, appId),
|
|
4631
|
+
ingestDir: INGEST_DIR,
|
|
4632
|
+
ingestionSource,
|
|
4633
|
+
uploadFilePath,
|
|
4634
|
+
searchUiTarget: searchUiTarget(language)
|
|
4635
|
+
};
|
|
4636
|
+
const summaries = [];
|
|
4637
|
+
if (uploadWarning) summaries.push(uploadWarning);
|
|
4638
|
+
let envSearchKey;
|
|
4639
|
+
let envAppIdMismatch = false;
|
|
4640
|
+
if (useCases.includes("search") && appId) {
|
|
4641
|
+
const envAppId = await readEnvVar(repoRoot, appIdVar(language));
|
|
4642
|
+
if (envAppId === appId) {
|
|
4643
|
+
envSearchKey = await readEnvVar(repoRoot, searchKeyVar(language));
|
|
4644
|
+
} else if (envAppId) {
|
|
4645
|
+
envAppIdMismatch = true;
|
|
4646
|
+
summaries.push(
|
|
4647
|
+
`\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.`
|
|
4648
|
+
);
|
|
4649
|
+
logger.warn(
|
|
4650
|
+
{ envAppId, appId },
|
|
4651
|
+
"implement: .env holds credentials for a different Algolia application; not reusing its search key"
|
|
4652
|
+
);
|
|
4729
4653
|
}
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4654
|
+
}
|
|
4655
|
+
let finalSearchEnvVars = input.searchEnvVars;
|
|
4656
|
+
let agentRuns = 0;
|
|
4657
|
+
let ingestCommand;
|
|
4658
|
+
let ingestScriptRan = false;
|
|
4659
|
+
let ingestRecordCount;
|
|
4660
|
+
let ingestDurationMs;
|
|
4661
|
+
let ingestOutcomeMessage;
|
|
4662
|
+
const ingestKeyAppId = ingestAppId;
|
|
4663
|
+
const ingestionTools = ingestKeyAppId ? makeToolContext(repoRoot, async () => ({
|
|
4664
|
+
[APP_ID_VAR]: ingestKeyAppId,
|
|
4665
|
+
[API_KEY_VAR]: (await resolveWriteKey(targetIndex, ingestKeyAppId)).key,
|
|
4666
|
+
[INDEX_NAME_VAR]: targetIndex
|
|
4667
|
+
})) : void 0;
|
|
4668
|
+
const searchTools = makeToolContext(repoRoot);
|
|
4669
|
+
async function runImplementationUseCase(currentUseCase, extraInstructions = []) {
|
|
4670
|
+
if (agentRuns > 0) ctx.recordStepExecution();
|
|
4671
|
+
agentRuns += 1;
|
|
4672
|
+
return runAgent({
|
|
4673
|
+
instructions: buildAgentInstructions(
|
|
4674
|
+
currentUseCase,
|
|
4675
|
+
input,
|
|
4676
|
+
extraInstructions
|
|
4677
|
+
),
|
|
4678
|
+
tools: toolsForUseCase(currentUseCase, input.ingestionSource),
|
|
4679
|
+
outputSchema: implementationOutputSchema,
|
|
4680
|
+
toolContext: currentUseCase === "ingestion" ? ingestionTools ?? searchTools : searchTools
|
|
4681
|
+
});
|
|
4682
|
+
}
|
|
4683
|
+
async function runVerificationUseCase() {
|
|
4684
|
+
if (agentRuns > 0) ctx.recordStepExecution();
|
|
4685
|
+
agentRuns += 1;
|
|
4686
|
+
return runAgent({
|
|
4687
|
+
instructions: buildAgentInstructions("verification", input),
|
|
4688
|
+
tools: toolsForUseCase("verification"),
|
|
4689
|
+
outputSchema: verificationOutputSchema,
|
|
4690
|
+
toolContext: searchTools
|
|
4691
|
+
});
|
|
4692
|
+
}
|
|
4693
|
+
if (useCases.includes("ingestion")) {
|
|
4694
|
+
let ingestFailureDetail;
|
|
4695
|
+
const result = await runImplementationUseCase("ingestion");
|
|
4696
|
+
summaries.push(formatSummary("ingestion", result.summary));
|
|
4697
|
+
ingestCommand = result.ingestCommand;
|
|
4698
|
+
const ingestionContext = ingestionTools ?? searchTools;
|
|
4699
|
+
const executions = ingestionContext.shell.executions;
|
|
4700
|
+
const {
|
|
4701
|
+
run: ingestRun,
|
|
4702
|
+
attempt: ingestAttempt,
|
|
4703
|
+
recordCount
|
|
4704
|
+
} = ingestOutcome(executions, ingestCommand);
|
|
4705
|
+
ingestScriptRan = ingestRun != null;
|
|
4706
|
+
ingestRecordCount = recordCount;
|
|
4707
|
+
ingestDurationMs = ingestRun?.durationMs;
|
|
4708
|
+
if (ingestScriptRan && ingestionContext.reviewed.length === 0) {
|
|
4709
|
+
summaries.push(
|
|
4710
|
+
"\u26A0\uFE0F The ingestion script ran without being shown to you for review. Read it in your project before trusting the index contents."
|
|
4711
|
+
);
|
|
4712
|
+
logger.warn(
|
|
4713
|
+
{ ingestCommand },
|
|
4714
|
+
"implement: ingestion ran without a reviewScript call"
|
|
4715
|
+
);
|
|
4739
4716
|
}
|
|
4740
|
-
if (
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
run: ingestRun,
|
|
4749
|
-
attempt: ingestAttempt,
|
|
4750
|
-
recordCount
|
|
4751
|
-
} = ingestOutcome(executions, ingestCommand);
|
|
4752
|
-
ingestScriptRan = ingestRun != null;
|
|
4753
|
-
ingestRecordCount = recordCount;
|
|
4754
|
-
ingestDurationMs = ingestRun?.durationMs;
|
|
4755
|
-
if (ingestScriptRan && ingestionContext.reviewed.length === 0) {
|
|
4756
|
-
summaries.push(
|
|
4757
|
-
"\u26A0\uFE0F The ingestion script ran without being shown to you for review. Read it in the worktree before trusting the index contents."
|
|
4758
|
-
);
|
|
4759
|
-
logger.warn(
|
|
4760
|
-
{ ingestCommand },
|
|
4761
|
-
"implement: ingestion ran without a reviewScript call"
|
|
4762
|
-
);
|
|
4717
|
+
if (ingestScriptRan) {
|
|
4718
|
+
ingestOutcomeMessage = `\u2705 Ingestion succeeded${ingestRecordCount != null ? ` \u2014 ${ingestRecordCount} record(s) indexed.` : "."}`;
|
|
4719
|
+
if (ingestRecordCount != null) {
|
|
4720
|
+
track("AI Wizard Ingest Successful", {
|
|
4721
|
+
entity_name: confirmed2?.map((e) => e.name).join(", ") || "unknown",
|
|
4722
|
+
record_count: ingestRecordCount,
|
|
4723
|
+
duration_ms: ingestDurationMs ?? 0
|
|
4724
|
+
});
|
|
4763
4725
|
}
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
duration_ms: ingestDurationMs ?? 0
|
|
4771
|
-
});
|
|
4772
|
-
}
|
|
4773
|
-
} else {
|
|
4774
|
-
const { reason, detail } = ingestFailure(ingestAttempt, executions);
|
|
4775
|
-
ingestOutcomeMessage = `\u26A0\uFE0F Records were not indexed \u2014 ${reason}.${ingestCommand ? " Run the command below when you are ready." : ""}`;
|
|
4776
|
-
ingestFailureDetail = detail;
|
|
4777
|
-
summaries.push(
|
|
4778
|
-
`\u26A0\uFE0F Ingestion did not complete: ${reason}.${detail ? `
|
|
4726
|
+
} else {
|
|
4727
|
+
const { reason, detail } = ingestFailure(ingestAttempt, executions);
|
|
4728
|
+
ingestOutcomeMessage = `\u26A0\uFE0F Records were not indexed \u2014 ${reason}.${ingestCommand ? " Run the command below when you are ready." : ""}`;
|
|
4729
|
+
ingestFailureDetail = detail;
|
|
4730
|
+
summaries.push(
|
|
4731
|
+
`\u26A0\uFE0F Ingestion did not complete: ${reason}.${detail ? `
|
|
4779
4732
|
${detail}` : ""}`
|
|
4780
|
-
|
|
4781
|
-
|
|
4733
|
+
);
|
|
4734
|
+
logger.warn(
|
|
4735
|
+
{
|
|
4736
|
+
ingestCommand,
|
|
4737
|
+
reason,
|
|
4738
|
+
approved: ingestAttempt?.approved,
|
|
4739
|
+
exitCode: ingestAttempt?.exitCode,
|
|
4740
|
+
timedOut: ingestAttempt?.timedOut,
|
|
4741
|
+
commandsRun: executions.length
|
|
4742
|
+
},
|
|
4743
|
+
"implement: ingestion script did not complete successfully"
|
|
4744
|
+
);
|
|
4745
|
+
track("Error", {
|
|
4746
|
+
step: "Push Data",
|
|
4747
|
+
error: `ingestion did not complete: ${reason}`,
|
|
4748
|
+
product_area: "AI Wizard"
|
|
4749
|
+
});
|
|
4750
|
+
}
|
|
4751
|
+
const commandMessages = ingestCommand ? [`Ingestion command: ${ingestCommand}`] : [];
|
|
4752
|
+
await ctx.requestUserInput({
|
|
4753
|
+
prompt: "",
|
|
4754
|
+
promptType: "enterToContinue",
|
|
4755
|
+
options: [],
|
|
4756
|
+
// One message per line: Notices.tsx counts a message as one wrapped
|
|
4757
|
+
// line, so an embedded newline overflows the panel's height accounting.
|
|
4758
|
+
messages: [
|
|
4759
|
+
ingestOutcomeMessage,
|
|
4760
|
+
...ingestFailureDetail?.split("\n").filter((l) => l.trim()) ?? [],
|
|
4761
|
+
...commandMessages
|
|
4762
|
+
]
|
|
4763
|
+
});
|
|
4764
|
+
}
|
|
4765
|
+
if (useCases.includes("search")) {
|
|
4766
|
+
let extraInstructions = [];
|
|
4767
|
+
const preSearchFiles = new Set(await listChangedFiles(repoRoot));
|
|
4768
|
+
for (let attempt = 1; attempt <= MAX_IMPLEMENT_VERIFICATION_ATTEMPTS; attempt++) {
|
|
4769
|
+
if (attempt > 1) {
|
|
4770
|
+
logger.info(
|
|
4782
4771
|
{
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
exitCode: ingestAttempt?.exitCode,
|
|
4787
|
-
timedOut: ingestAttempt?.timedOut,
|
|
4788
|
-
commandsRun: executions.length
|
|
4772
|
+
attempt,
|
|
4773
|
+
maxAttempts: MAX_IMPLEMENT_VERIFICATION_ATTEMPTS,
|
|
4774
|
+
extraInstructions
|
|
4789
4775
|
},
|
|
4790
|
-
"implement:
|
|
4776
|
+
"implement: retrying search implementation after failed verification"
|
|
4777
|
+
);
|
|
4778
|
+
}
|
|
4779
|
+
const { summary } = await runImplementationUseCase(
|
|
4780
|
+
"search",
|
|
4781
|
+
extraInstructions
|
|
4782
|
+
);
|
|
4783
|
+
summaries.push(formatSummary("search", summary));
|
|
4784
|
+
const verification = await runVerificationUseCase();
|
|
4785
|
+
summaries.push(formatSummary("verification", verification.summary));
|
|
4786
|
+
if (verification.sufficient) {
|
|
4787
|
+
ctx.setUserInput("implementation", "success");
|
|
4788
|
+
const searchFilesChanged = (await listChangedFiles(repoRoot)).filter(
|
|
4789
|
+
(file) => !preSearchFiles.has(file)
|
|
4791
4790
|
);
|
|
4792
|
-
track("
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4791
|
+
track("AI Wizard Frontend Component Generated", {
|
|
4792
|
+
filePaths: searchFilesChanged
|
|
4793
|
+
});
|
|
4794
|
+
track("AI Wizard Wired to UI", {
|
|
4795
|
+
location_heuristic: searchLocation ?? "unknown"
|
|
4796
4796
|
});
|
|
4797
|
+
break;
|
|
4797
4798
|
}
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
4799
|
+
if (attempt === MAX_IMPLEMENT_VERIFICATION_ATTEMPTS) {
|
|
4800
|
+
ctx.setUserInput("implementation", "fail");
|
|
4801
|
+
throw new Error(
|
|
4802
|
+
`Implementation verification failed after ${MAX_IMPLEMENT_VERIFICATION_ATTEMPTS} attempts: ${verification.additionalInstructions ?? verification.summary}`
|
|
4803
|
+
);
|
|
4801
4804
|
}
|
|
4802
|
-
|
|
4803
|
-
prompt: "",
|
|
4804
|
-
promptType: "enterToContinue",
|
|
4805
|
-
options: [],
|
|
4806
|
-
// The wizard never streams command output, so a failed run's tail is the
|
|
4807
|
-
// only place the developer sees why it failed. One message per line:
|
|
4808
|
-
// the panel's height accounting counts a message as one wrapped line
|
|
4809
|
-
// (see Notices.tsx), so an embedded newline overflows it.
|
|
4810
|
-
messages: [
|
|
4811
|
-
ingestOutcomeMessage,
|
|
4812
|
-
...ingestFailureDetail?.split("\n").filter((l) => l.trim()) ?? [],
|
|
4813
|
-
...commandMessages
|
|
4814
|
-
]
|
|
4815
|
-
});
|
|
4805
|
+
extraInstructions = verificationRetryInstructions(verification);
|
|
4816
4806
|
}
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4807
|
+
let searchKey;
|
|
4808
|
+
let searchKeyError;
|
|
4809
|
+
if (appId) {
|
|
4810
|
+
try {
|
|
4811
|
+
const resolved2 = await resolveSearchOnlyKey(
|
|
4812
|
+
targetIndex,
|
|
4813
|
+
appId,
|
|
4814
|
+
envSearchKey
|
|
4815
|
+
);
|
|
4816
|
+
searchKey = resolved2.key;
|
|
4817
|
+
summaries.push(
|
|
4818
|
+
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}.`
|
|
4819
|
+
);
|
|
4820
|
+
} catch (err) {
|
|
4821
|
+
searchKeyError = err.message;
|
|
4822
|
+
logger.warn(
|
|
4823
|
+
{ err: searchKeyError },
|
|
4824
|
+
"implement: could not provision a search-only API key; the .env value stays a placeholder"
|
|
4834
4825
|
);
|
|
4835
|
-
summaries.push(formatSummary("search", summary));
|
|
4836
|
-
const verification = await runVerificationUseCase();
|
|
4837
|
-
summaries.push(formatSummary("verification", verification.summary));
|
|
4838
|
-
if (verification.sufficient) {
|
|
4839
|
-
ctx.setUserInput("implementation", "success");
|
|
4840
|
-
const searchFilesChanged = (await listChangedFiles(worktree)).filter(
|
|
4841
|
-
(file) => !preSearchFiles.has(file)
|
|
4842
|
-
);
|
|
4843
|
-
track("AI Wizard Frontend Component Generated", {
|
|
4844
|
-
filePaths: searchFilesChanged
|
|
4845
|
-
});
|
|
4846
|
-
track("AI Wizard Wired to UI", {
|
|
4847
|
-
location_heuristic: searchLocation ?? "unknown"
|
|
4848
|
-
});
|
|
4849
|
-
break;
|
|
4850
|
-
}
|
|
4851
|
-
if (attempt === MAX_IMPLEMENT_VERIFICATION_ATTEMPTS) {
|
|
4852
|
-
ctx.setUserInput("implementation", "fail");
|
|
4853
|
-
throw new Error(
|
|
4854
|
-
`Implementation verification failed after ${MAX_IMPLEMENT_VERIFICATION_ATTEMPTS} attempts: ${verification.additionalInstructions ?? verification.summary}`
|
|
4855
|
-
);
|
|
4856
|
-
}
|
|
4857
|
-
extraInstructions = verificationRetryInstructions(verification);
|
|
4858
4826
|
}
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
4867
|
-
|
|
4868
|
-
searchKey = resolved2.key;
|
|
4869
|
-
summaries.push(
|
|
4870
|
-
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}.`
|
|
4871
|
-
);
|
|
4872
|
-
} catch (err) {
|
|
4873
|
-
searchKeyError = err.message;
|
|
4874
|
-
logger.warn(
|
|
4875
|
-
{ err: searchKeyError },
|
|
4876
|
-
"implement: could not provision a search-only API key; the .env value stays a placeholder"
|
|
4877
|
-
);
|
|
4878
|
-
}
|
|
4827
|
+
}
|
|
4828
|
+
finalSearchEnvVars = searchEnvVars(language, targetIndex, appId, searchKey);
|
|
4829
|
+
const resolvedSearchEnvVars = finalSearchEnvVars.filter(
|
|
4830
|
+
(v) => !v.value.startsWith("<")
|
|
4831
|
+
);
|
|
4832
|
+
if (resolvedSearchEnvVars.length > 0) {
|
|
4833
|
+
const written = await writeSearchEnvValues(repoRoot, resolvedSearchEnvVars);
|
|
4834
|
+
if (written.length > 0) {
|
|
4835
|
+
summaries.push(`Wrote ${written.join(", ")} to .env.`);
|
|
4879
4836
|
}
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
const resolvedSearchEnvVars = finalSearchEnvVars.filter(
|
|
4887
|
-
(v) => !v.value.startsWith("<")
|
|
4888
|
-
);
|
|
4889
|
-
if (resolvedSearchEnvVars.length > 0) {
|
|
4890
|
-
const written = await writeSearchEnvValues(
|
|
4891
|
-
worktree,
|
|
4892
|
-
resolvedSearchEnvVars
|
|
4837
|
+
const ignored = await ensureGitIgnored(repoRoot, join12(repoRoot, ".env"));
|
|
4838
|
+
if (ignored === "added") {
|
|
4839
|
+
summaries.push("Added .env to .gitignore.");
|
|
4840
|
+
} else if (ignored === "tracked") {
|
|
4841
|
+
summaries.push(
|
|
4842
|
+
'\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.'
|
|
4893
4843
|
);
|
|
4894
|
-
if (written.length > 0) {
|
|
4895
|
-
summaries.push(`Wrote ${written.join(", ")} to .env.`);
|
|
4896
|
-
}
|
|
4897
|
-
const ignored = await ensureGitIgnored(worktree, join12(worktree, ".env"));
|
|
4898
|
-
if (ignored === "added") {
|
|
4899
|
-
summaries.push("Added .env to .gitignore.");
|
|
4900
|
-
} else if (ignored === "tracked") {
|
|
4901
|
-
summaries.push(
|
|
4902
|
-
'\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.'
|
|
4903
|
-
);
|
|
4904
|
-
}
|
|
4905
|
-
const stale = [];
|
|
4906
|
-
for (const v of resolvedSearchEnvVars) {
|
|
4907
|
-
if (written.includes(v.name)) continue;
|
|
4908
|
-
const current = await readEnvVar(worktree, v.name);
|
|
4909
|
-
if (current && current !== v.value) stale.push(v);
|
|
4910
|
-
}
|
|
4911
|
-
if (stale.length > 0 && !envAppIdMismatch) {
|
|
4912
|
-
summaries.push(
|
|
4913
|
-
`\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.`
|
|
4914
|
-
);
|
|
4915
|
-
logger.warn(
|
|
4916
|
-
{ vars: stale.map((v) => v.name) },
|
|
4917
|
-
"implement: .env holds different values for the resolved search credentials; not overwriting them"
|
|
4918
|
-
);
|
|
4919
|
-
}
|
|
4920
4844
|
}
|
|
4921
|
-
const
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4845
|
+
const stale = [];
|
|
4846
|
+
for (const v of resolvedSearchEnvVars) {
|
|
4847
|
+
if (written.includes(v.name)) continue;
|
|
4848
|
+
const current = await readEnvVar(repoRoot, v.name);
|
|
4849
|
+
if (current && current !== v.value) stale.push(v);
|
|
4850
|
+
}
|
|
4851
|
+
if (stale.length > 0 && !envAppIdMismatch) {
|
|
4925
4852
|
summaries.push(
|
|
4926
|
-
|
|
4927
|
-
|
|
4853
|
+
`\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.`
|
|
4854
|
+
);
|
|
4855
|
+
logger.warn(
|
|
4856
|
+
{ vars: stale.map((v) => v.name) },
|
|
4857
|
+
"implement: .env holds different values for the resolved search credentials; not overwriting them"
|
|
4928
4858
|
);
|
|
4929
4859
|
}
|
|
4930
|
-
} else {
|
|
4931
|
-
ctx.setUserInput("implementation", "success");
|
|
4932
4860
|
}
|
|
4933
|
-
const
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
|
|
4861
|
+
const unresolvedSearchEnvVars = finalSearchEnvVars.filter(
|
|
4862
|
+
(v) => v.value.startsWith("<")
|
|
4863
|
+
);
|
|
4864
|
+
if (unresolvedSearchEnvVars.length > 0) {
|
|
4865
|
+
summaries.push(
|
|
4866
|
+
`Could not resolve a value for ${unresolvedSearchEnvVars.map((v) => v.name).join(", ")} \u2014 fill it in manually in .env.` + (searchKeyError ? ` Reason: ${searchKeyError}` : "")
|
|
4937
4867
|
);
|
|
4938
4868
|
}
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
...ingestRecordCount != null ? { ingestRecordCount } : {},
|
|
4948
|
-
...ingestDurationMs != null ? { ingestDurationMs } : {}
|
|
4949
|
-
} : {},
|
|
4950
|
-
...useCases.includes("search") ? { searchEnvVars: finalSearchEnvVars } : {}
|
|
4951
|
-
};
|
|
4952
|
-
} finally {
|
|
4953
|
-
process.chdir(repoRoot);
|
|
4869
|
+
} else {
|
|
4870
|
+
ctx.setUserInput("implementation", "success");
|
|
4871
|
+
}
|
|
4872
|
+
const filesChanged = (await listChangedFiles(repoRoot)).filter(
|
|
4873
|
+
(file) => !baselineFiles.has(file)
|
|
4874
|
+
);
|
|
4875
|
+
if (filesChanged.length === 0) {
|
|
4876
|
+
logger.warn("implement: agent reported success but no files changed");
|
|
4954
4877
|
}
|
|
4878
|
+
return {
|
|
4879
|
+
ingestionSource,
|
|
4880
|
+
filesChanged,
|
|
4881
|
+
summary: summaries.join("\n\n"),
|
|
4882
|
+
...useCases.includes("ingestion") && ingestCommand ? {
|
|
4883
|
+
ingestCommand,
|
|
4884
|
+
ingestScriptRan,
|
|
4885
|
+
...ingestRecordCount != null ? { ingestRecordCount } : {},
|
|
4886
|
+
...ingestDurationMs != null ? { ingestDurationMs } : {}
|
|
4887
|
+
} : {},
|
|
4888
|
+
...useCases.includes("search") ? { searchEnvVars: finalSearchEnvVars } : {}
|
|
4889
|
+
};
|
|
4955
4890
|
}
|
|
4956
4891
|
|
|
4957
4892
|
// src/workflows/default.ts
|
|
@@ -5023,10 +4958,7 @@ var defaultWorkflow = {
|
|
|
5023
4958
|
ctx.notify({
|
|
5024
4959
|
messages: ["Building your Algolia search experience\u2026"]
|
|
5025
4960
|
});
|
|
5026
|
-
|
|
5027
|
-
"ingestion"
|
|
5028
|
-
);
|
|
5029
|
-
return implement(ctx, ["search"], ingestion2?.worktreePath);
|
|
4961
|
+
return implement(ctx, ["search"]);
|
|
5030
4962
|
}
|
|
5031
4963
|
}),
|
|
5032
4964
|
defineStep({
|
|
@@ -5041,9 +4973,8 @@ var defaultWorkflow = {
|
|
|
5041
4973
|
"ingestion"
|
|
5042
4974
|
);
|
|
5043
4975
|
return reviewStep(ctx, {
|
|
5044
|
-
//
|
|
5045
|
-
//
|
|
5046
|
-
// an LLM-paraphrased command risks being wrong.
|
|
4976
|
+
// ingestCommand was already shown verbatim as a notice; an
|
|
4977
|
+
// LLM-paraphrased restatement in nextSteps risks being wrong.
|
|
5047
4978
|
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."
|
|
5048
4979
|
});
|
|
5049
4980
|
}
|
|
@@ -5114,7 +5045,7 @@ var review = {
|
|
|
5114
5045
|
"Ingested 25 generated Product records into wizard_seed_products.",
|
|
5115
5046
|
"Added an InstantSearch search experience to the shared header."
|
|
5116
5047
|
],
|
|
5117
|
-
reviewPrompt: "Review the Algolia ingestion and search changes
|
|
5048
|
+
reviewPrompt: "Review the Algolia ingestion and search changes.",
|
|
5118
5049
|
nextSteps: ["Point the ingestion script at your real product data."]
|
|
5119
5050
|
};
|
|
5120
5051
|
var SEEDS = {
|
|
@@ -5231,9 +5162,9 @@ Options:
|
|
|
5231
5162
|
steps pre-filled with test data. Pass with no value to print
|
|
5232
5163
|
the step ids. See CONTRIBUTING.md.
|
|
5233
5164
|
--no-telemetry Send no telemetry or analytics for this run.
|
|
5234
|
-
--reset-on-run Wipe this project's wizard state (run state, AI consent
|
|
5235
|
-
|
|
5236
|
-
|
|
5165
|
+
--reset-on-run Wipe this project's wizard state (run state, AI consent)
|
|
5166
|
+
before starting, so the run behaves like a first-ever
|
|
5167
|
+
run. Also drops every API key the wizard has
|
|
5237
5168
|
stored in your keychain (or, where the platform has none,
|
|
5238
5169
|
the encrypted file it falls back to \u2014 see CONTRIBUTING.md),
|
|
5239
5170
|
for this project and any other, so later runs create new
|
|
@@ -5273,7 +5204,7 @@ function parseCliArgs(argv) {
|
|
|
5273
5204
|
}
|
|
5274
5205
|
|
|
5275
5206
|
// src/lib/resetState.ts
|
|
5276
|
-
import { readdir as
|
|
5207
|
+
import { readdir as readdir3, rm as rm2 } from "node:fs/promises";
|
|
5277
5208
|
import { join as join13 } from "node:path";
|
|
5278
5209
|
var KEEP = ["wizard.log"];
|
|
5279
5210
|
async function resetProjectState() {
|
|
@@ -5281,7 +5212,7 @@ async function resetProjectState() {
|
|
|
5281
5212
|
await forgetResolvedKeys();
|
|
5282
5213
|
let entries;
|
|
5283
5214
|
try {
|
|
5284
|
-
entries = await
|
|
5215
|
+
entries = await readdir3(dir);
|
|
5285
5216
|
} catch {
|
|
5286
5217
|
return { dir, removed: [] };
|
|
5287
5218
|
}
|