@algolia/wizard 0.19.0-rc.111.186 → 0.19.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 +1 -1
- package/dist/main.js +423 -314
- 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) before starting, so the run behaves like a first-ever run. Credentials are untouched. |
|
|
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
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)
|
|
1203
|
+
description: "creates & edits files (search UI, config) in a throwaway git worktree \u2014 your checkout is never touched."
|
|
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 the worktree."
|
|
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. 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.",
|
|
3249
|
+
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.",
|
|
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.19.0
|
|
3607
|
+
version: "0.19.0",
|
|
3608
3608
|
description: "Magically implement Algolia functionality in your codebase",
|
|
3609
3609
|
type: "module",
|
|
3610
3610
|
engines: {
|
|
@@ -3971,10 +3971,11 @@ ${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/");
|
|
3974
3975
|
return {
|
|
3975
3976
|
text: `\u2192 ${step}`,
|
|
3976
|
-
color: isIngestCommand ? COLORS.brand : void 0,
|
|
3977
|
-
bold: isIngestCommand
|
|
3977
|
+
color: isIngestCommand ? COLORS.brand : isWorktreeCommand ? COLORS.secondary : void 0,
|
|
3978
|
+
bold: isIngestCommand || isWorktreeCommand
|
|
3978
3979
|
};
|
|
3979
3980
|
});
|
|
3980
3981
|
return [
|
|
@@ -4011,11 +4012,13 @@ ${formatCompletedSteps(ctx.completedSteps)}`,
|
|
|
4011
4012
|
import z28 from "zod";
|
|
4012
4013
|
import { join as join12 } from "node:path";
|
|
4013
4014
|
|
|
4014
|
-
// src/lib/
|
|
4015
|
+
// src/lib/worktree.ts
|
|
4015
4016
|
import { execFile as execFile2 } from "node:child_process";
|
|
4016
|
-
import { copyFile, mkdir as mkdir6, readFile as readFile8, stat as stat3, writeFile as writeFile7 } from "node:fs/promises";
|
|
4017
|
+
import { copyFile, mkdir as mkdir6, readdir as readdir3, readFile as readFile8, stat as stat3, writeFile as writeFile7 } from "node:fs/promises";
|
|
4017
4018
|
import { basename as basename2, dirname as dirname7, isAbsolute as isAbsolute2, join as join10, resolve as resolve3 } from "node:path";
|
|
4018
4019
|
var MAX_BUFFER = 32 * 1024 * 1024;
|
|
4020
|
+
var MAX_WIZARD_WORKTREES = 3;
|
|
4021
|
+
var WIZARD_BRANCH_PREFIX = "wizard/implement-";
|
|
4019
4022
|
function git(args) {
|
|
4020
4023
|
return new Promise((resolve4, reject) => {
|
|
4021
4024
|
execFile2("git", args, { maxBuffer: MAX_BUFFER }, (err, stdout, stderr) => {
|
|
@@ -4042,7 +4045,40 @@ async function isWorkingTreeDirty(repoRoot) {
|
|
|
4042
4045
|
const out = await git(["-C", repoRoot, "status", "--porcelain"]);
|
|
4043
4046
|
return out.trim().length > 0;
|
|
4044
4047
|
}
|
|
4045
|
-
async function
|
|
4048
|
+
async function pruneOldWorktrees(repoRoot) {
|
|
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) {
|
|
4046
4082
|
const trimmed = sourcePath.trim();
|
|
4047
4083
|
if (!trimmed) {
|
|
4048
4084
|
return { ok: false, reason: "no file path was provided" };
|
|
@@ -4056,10 +4092,7 @@ async function copyUploadIntoProject(repoRoot, ingestDir, sourcePath) {
|
|
|
4056
4092
|
return { ok: false, reason: `"${sourcePath}" does not exist` };
|
|
4057
4093
|
}
|
|
4058
4094
|
const relPath = join10(ingestDir, basename2(source));
|
|
4059
|
-
const dest = join10(
|
|
4060
|
-
if (resolve3(source) === resolve3(dest)) {
|
|
4061
|
-
return { ok: true, relPath };
|
|
4062
|
-
}
|
|
4095
|
+
const dest = join10(worktreePath, relPath);
|
|
4063
4096
|
try {
|
|
4064
4097
|
await mkdir6(dirname7(dest), { recursive: true });
|
|
4065
4098
|
await copyFile(source, dest);
|
|
@@ -4074,10 +4107,10 @@ async function copyUploadIntoProject(repoRoot, ingestDir, sourcePath) {
|
|
|
4074
4107
|
function hasEnvVar(content, name) {
|
|
4075
4108
|
return new RegExp(`^(\\s*(?:export\\s+)?${name})\\s*=`, "m").test(content);
|
|
4076
4109
|
}
|
|
4077
|
-
async function readEnvVar(
|
|
4110
|
+
async function readEnvVar(worktreePath, name) {
|
|
4078
4111
|
let content;
|
|
4079
4112
|
try {
|
|
4080
|
-
content = await readFile8(join10(
|
|
4113
|
+
content = await readFile8(join10(worktreePath, ".env"), "utf8");
|
|
4081
4114
|
} catch (err) {
|
|
4082
4115
|
if (err.code !== "ENOENT") throw err;
|
|
4083
4116
|
return void 0;
|
|
@@ -4091,8 +4124,8 @@ async function readEnvVar(repoRoot, name) {
|
|
|
4091
4124
|
if (!value || value.startsWith("<")) return void 0;
|
|
4092
4125
|
return value;
|
|
4093
4126
|
}
|
|
4094
|
-
async function writeSearchEnvValues(
|
|
4095
|
-
const target = join10(
|
|
4127
|
+
async function writeSearchEnvValues(worktreePath, vars) {
|
|
4128
|
+
const target = join10(worktreePath, ".env");
|
|
4096
4129
|
let existing = "";
|
|
4097
4130
|
try {
|
|
4098
4131
|
existing = await readFile8(target, "utf8");
|
|
@@ -4107,8 +4140,8 @@ async function writeSearchEnvValues(repoRoot, vars) {
|
|
|
4107
4140
|
await writeFile7(target, existing + prefix + lines, "utf8");
|
|
4108
4141
|
return missing.map((v) => v.name);
|
|
4109
4142
|
}
|
|
4110
|
-
async function listChangedFiles(
|
|
4111
|
-
const raw = await git(["-C",
|
|
4143
|
+
async function listChangedFiles(worktreePath) {
|
|
4144
|
+
const raw = await git(["-C", worktreePath, "status", "--porcelain", "-z"]);
|
|
4112
4145
|
const entries = raw.split("\0");
|
|
4113
4146
|
const files = [];
|
|
4114
4147
|
for (let i = 0; i < entries.length; i += 1) {
|
|
@@ -4124,20 +4157,20 @@ function normalizeFindingPaths(findings) {
|
|
|
4124
4157
|
...findings,
|
|
4125
4158
|
ingestionAnalysis: findings.ingestionAnalysis?.map((e) => ({
|
|
4126
4159
|
...e,
|
|
4127
|
-
paths: e.paths.map(
|
|
4160
|
+
paths: e.paths.map(toWorktreeRelative)
|
|
4128
4161
|
})),
|
|
4129
4162
|
searchImplementationAnalysis: findings.searchImplementationAnalysis ? normalizeSearchLocation(findings.searchImplementationAnalysis) : void 0,
|
|
4130
4163
|
confirmedEntities: findings.confirmedEntities?.map((e) => ({
|
|
4131
4164
|
...e,
|
|
4132
|
-
paths: e.paths.map(
|
|
4165
|
+
paths: e.paths.map(toWorktreeRelative)
|
|
4133
4166
|
}))
|
|
4134
4167
|
};
|
|
4135
4168
|
}
|
|
4136
4169
|
function normalizeSearchLocation(path) {
|
|
4137
|
-
const normalized = path ?
|
|
4170
|
+
const normalized = path ? toWorktreeRelative(path).trim() : "";
|
|
4138
4171
|
return normalized && normalized.toLowerCase() !== "unknown" ? normalized : void 0;
|
|
4139
4172
|
}
|
|
4140
|
-
function
|
|
4173
|
+
function toWorktreeRelative(p) {
|
|
4141
4174
|
return p.replace(/^\/+/, "");
|
|
4142
4175
|
}
|
|
4143
4176
|
async function confirmDirtyWorkingTree(ctx, repoRoot) {
|
|
@@ -4146,19 +4179,19 @@ async function confirmDirtyWorkingTree(ctx, repoRoot) {
|
|
|
4146
4179
|
const shown = dirty.slice(0, MAX_LISTED_DIRTY_FILES);
|
|
4147
4180
|
const overflow = dirty.length - shown.length;
|
|
4148
4181
|
const answer = await ctx.requestUserInput({
|
|
4149
|
-
prompt: "Proceed
|
|
4182
|
+
prompt: "Proceed using HEAD only? Uncommitted changes will NOT be included in the generated implementation.",
|
|
4150
4183
|
promptType: "acceptReject",
|
|
4151
4184
|
options: [],
|
|
4152
4185
|
messages: [
|
|
4153
|
-
`${dirty.length} uncommitted change(s) detected:`,
|
|
4186
|
+
`${dirty.length} uncommitted change(s) detected. The wizard builds an isolated worktree from HEAD, so these are ignored:`,
|
|
4154
4187
|
...shown.map((file) => ` \u2022 ${file}`),
|
|
4155
4188
|
...overflow > 0 ? [` \u2022 \u2026and ${overflow} more`] : [],
|
|
4156
|
-
"Commit or stash them first
|
|
4189
|
+
"Commit or stash them first to include them in the implementation."
|
|
4157
4190
|
]
|
|
4158
4191
|
});
|
|
4159
4192
|
if (answer !== true) {
|
|
4160
4193
|
throw new Error(
|
|
4161
|
-
"implement aborted: commit or stash your changes
|
|
4194
|
+
"implement aborted: commit or stash your changes so they are built into the worktree, then re-run the wizard."
|
|
4162
4195
|
);
|
|
4163
4196
|
}
|
|
4164
4197
|
}
|
|
@@ -4221,10 +4254,16 @@ function getFrameworkSpecificDoc(frameworks) {
|
|
|
4221
4254
|
return loadAlgoliaDoc("js");
|
|
4222
4255
|
}
|
|
4223
4256
|
|
|
4257
|
+
// src/lib/shell.ts
|
|
4258
|
+
function shellQuote(value) {
|
|
4259
|
+
return "'" + value.replace(/'/g, "'\\''") + "'";
|
|
4260
|
+
}
|
|
4261
|
+
|
|
4224
4262
|
// src/actions/implement.ts
|
|
4225
4263
|
var implementSchema = z28.object({
|
|
4226
4264
|
filesChanged: z28.array(z28.string()),
|
|
4227
4265
|
summary: z28.string(),
|
|
4266
|
+
worktreePath: z28.string().optional(),
|
|
4228
4267
|
ingestCommand: z28.string().optional(),
|
|
4229
4268
|
ingestScriptRan: z28.boolean().optional(),
|
|
4230
4269
|
ingestRecordCount: z28.number().optional(),
|
|
@@ -4313,8 +4352,9 @@ function searchEnvVars(language, index, appId, searchKey) {
|
|
|
4313
4352
|
name: searchKeyVar(language),
|
|
4314
4353
|
value: searchKey ?? "<your-algolia-search-only-api-key>"
|
|
4315
4354
|
},
|
|
4316
|
-
// Wizard-supplied rather than written into the generated code
|
|
4317
|
-
// that retypes the name
|
|
4355
|
+
// Wizard-supplied rather than written into the generated code, because an
|
|
4356
|
+
// agent that retypes the name (appending the project name, re-casing it)
|
|
4357
|
+
// leaves the UI querying an index that does not exist.
|
|
4318
4358
|
{
|
|
4319
4359
|
name: searchIndexVar(language),
|
|
4320
4360
|
value: index
|
|
@@ -4323,6 +4363,8 @@ function searchEnvVars(language, index, appId, searchKey) {
|
|
|
4323
4363
|
}
|
|
4324
4364
|
function baseInstructions(input) {
|
|
4325
4365
|
return [
|
|
4366
|
+
// Agents have renamed this (e.g. appending the project name), which the
|
|
4367
|
+
// index-scoped keys then reject with a 403.
|
|
4326
4368
|
`Target Algolia index, to be used exactly as written \u2014 never renamed, re-cased, prefixed, or suffixed: "${input.targetIndex}"`,
|
|
4327
4369
|
`Project languages and frameworks: ${JSON.stringify(input.language)}`,
|
|
4328
4370
|
"Make minimal, idiomatic changes; do not touch unrelated code.",
|
|
@@ -4340,14 +4382,14 @@ function sourceSpecificInstructions(input) {
|
|
|
4340
4382
|
"Add env vars for any local source access (DB URL, API base, file paths) that is not a fixed repo path."
|
|
4341
4383
|
],
|
|
4342
4384
|
fileUpload: [
|
|
4343
|
-
`Records come from the developer's file, already copied into the
|
|
4385
|
+
`Records come from the developer's file, already copied into the worktree at "${input.uploadFilePath}". Read and parse that exact file.`,
|
|
4344
4386
|
"Parse by extension: JSON = array of objects; CSV/TSV = header row maps to keys.",
|
|
4345
4387
|
"Map parsed columns/fields to the confirmed entity attributes.",
|
|
4346
4388
|
"Never fabricate, hardcode, or substitute a different file."
|
|
4347
4389
|
],
|
|
4348
4390
|
generated: [
|
|
4349
4391
|
"No real data source exists; use sample records for each confirmed entity.",
|
|
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.",
|
|
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 in the worktree, returning the file path. Do not write records or objectIDs yourself.",
|
|
4351
4393
|
"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.",
|
|
4352
4394
|
"Add a prominent TODO where the developer swaps the generated records (and the JSON file under `.algolia-wizard/data/`) for their real record source."
|
|
4353
4395
|
]
|
|
@@ -4396,11 +4438,14 @@ function searchInstructions(input) {
|
|
|
4396
4438
|
"If a search box already exists, replace it with yours.",
|
|
4397
4439
|
`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.`,
|
|
4398
4440
|
"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.",
|
|
4399
|
-
// The key is provisioned only after verification passes,
|
|
4400
|
-
//
|
|
4401
|
-
//
|
|
4441
|
+
// The key is provisioned only after verification passes, so the agent never
|
|
4442
|
+
// sees one. It must also leave .env alone: the wizard reads that file to
|
|
4443
|
+
// decide whether a key already exists, and an agent-invented value there
|
|
4444
|
+
// would be reused as if it were real.
|
|
4402
4445
|
`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.`,
|
|
4403
|
-
//
|
|
4446
|
+
// Not the agent's to rename: the wizard writes these exact names into
|
|
4447
|
+
// ".env" right after this step, so a renamed prefix would leave the code
|
|
4448
|
+
// reading a var the wizard never wrote.
|
|
4404
4449
|
`Use exactly these env var names in the code: ${input.searchEnvVars.map(({ name }) => name).join(", ")}.`,
|
|
4405
4450
|
"Install any Algolia packages you import with the project's own package manager via runShell, and declare them in the project's dependency manifest.",
|
|
4406
4451
|
"Match the styles of the application as closely as possible.",
|
|
@@ -4409,10 +4454,10 @@ function searchInstructions(input) {
|
|
|
4409
4454
|
}
|
|
4410
4455
|
function verificationInstructions(input) {
|
|
4411
4456
|
return [
|
|
4412
|
-
"Verify the Algolia implementation changes.",
|
|
4457
|
+
"Verify the Algolia implementation changes in the current worktree.",
|
|
4413
4458
|
`Verification tools found in the codebase: ${JSON.stringify(input.findings.verification ?? [])}.`,
|
|
4414
4459
|
"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.",
|
|
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.",
|
|
4460
|
+
"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.",
|
|
4416
4461
|
"For issues caused by the implementation, make minimal fixes with writeFile and re-run the checks.",
|
|
4417
4462
|
"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.",
|
|
4418
4463
|
"Do not add new Algolia functionality here; only validate and make minimal correctness fixes.",
|
|
@@ -4507,11 +4552,11 @@ function ingestFailure(attempt, executions) {
|
|
|
4507
4552
|
}
|
|
4508
4553
|
return { reason: `it failed (exit ${attempt.exitCode ?? "unknown"})`, detail };
|
|
4509
4554
|
}
|
|
4510
|
-
function makeToolContext(
|
|
4555
|
+
function makeToolContext(worktree, env = async () => ({})) {
|
|
4511
4556
|
return createToolContext(
|
|
4512
4557
|
DEFAULT_TOOL_LIMITS,
|
|
4513
|
-
|
|
4514
|
-
createShellContext({ env, approve: storeApproval(
|
|
4558
|
+
worktree,
|
|
4559
|
+
createShellContext({ env, approve: storeApproval(worktree) })
|
|
4515
4560
|
);
|
|
4516
4561
|
}
|
|
4517
4562
|
function verificationRetryInstructions(verification) {
|
|
@@ -4519,7 +4564,7 @@ function verificationRetryInstructions(verification) {
|
|
|
4519
4564
|
`Implementation insufficient. Address these findings before reporting completion: ${verification.additionalInstructions ?? verification.summary}`
|
|
4520
4565
|
];
|
|
4521
4566
|
}
|
|
4522
|
-
async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES) {
|
|
4567
|
+
async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWorktreePath) {
|
|
4523
4568
|
const repoRoot = process.cwd();
|
|
4524
4569
|
const scan = ctx.getStepOutput("project-scan");
|
|
4525
4570
|
const entities = ctx.getStepOutput(
|
|
@@ -4585,11 +4630,9 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES) {
|
|
|
4585
4630
|
const targetIndex = selected?.selection;
|
|
4586
4631
|
useWizard.getState().setTargetIndex(targetIndex ?? null);
|
|
4587
4632
|
await assertGitRepoWithHead(repoRoot);
|
|
4588
|
-
|
|
4589
|
-
if (!ingestionAlreadyRan && await isWorkingTreeDirty(repoRoot)) {
|
|
4633
|
+
if (await isWorkingTreeDirty(repoRoot)) {
|
|
4590
4634
|
await confirmDirtyWorkingTree(ctx, repoRoot);
|
|
4591
4635
|
}
|
|
4592
|
-
const baselineFiles = new Set(await listChangedFiles(repoRoot));
|
|
4593
4636
|
const normalized = normalizeFindingPaths(findings);
|
|
4594
4637
|
const confirmed2 = normalized.confirmedEntities;
|
|
4595
4638
|
const searchLocation = normalized.searchImplementationAnalysis;
|
|
@@ -4601,292 +4644,314 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES) {
|
|
|
4601
4644
|
if (useCases.includes("ingestion")) {
|
|
4602
4645
|
ingestAppId = appId ?? (await requireApplication()).id;
|
|
4603
4646
|
}
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
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"
|
|
4647
|
+
const worktree = existingWorktreePath ?? (await createWorktree(repoRoot)).path;
|
|
4648
|
+
try {
|
|
4649
|
+
process.chdir(worktree);
|
|
4650
|
+
let uploadFilePath;
|
|
4651
|
+
let uploadWarning;
|
|
4652
|
+
if (ingestionSource === "fileUpload") {
|
|
4653
|
+
const copied = await copyUploadIntoWorktree(
|
|
4654
|
+
repoRoot,
|
|
4655
|
+
worktree,
|
|
4656
|
+
INGEST_DIR,
|
|
4657
|
+
uploadSourcePath ?? ""
|
|
4620
4658
|
);
|
|
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
|
+
}
|
|
4621
4669
|
}
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
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
|
+
}
|
|
4653
4701
|
}
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
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
|
-
);
|
|
4702
|
+
let finalSearchEnvVars = input.searchEnvVars;
|
|
4703
|
+
let agentRuns = 0;
|
|
4704
|
+
let ingestCommand;
|
|
4705
|
+
let ingestScriptRan = false;
|
|
4706
|
+
let ingestRecordCount;
|
|
4707
|
+
let ingestDurationMs;
|
|
4708
|
+
let ingestOutcomeMessage;
|
|
4709
|
+
const ingestKeyAppId = ingestAppId;
|
|
4710
|
+
const ingestionTools = ingestKeyAppId ? makeToolContext(worktree, async () => ({
|
|
4711
|
+
[APP_ID_VAR]: ingestKeyAppId,
|
|
4712
|
+
[API_KEY_VAR]: (await resolveWriteKey(targetIndex, ingestKeyAppId)).key,
|
|
4713
|
+
[INDEX_NAME_VAR]: targetIndex
|
|
4714
|
+
})) : void 0;
|
|
4715
|
+
const searchTools = makeToolContext(worktree);
|
|
4716
|
+
async function runImplementationUseCase(currentUseCase, extraInstructions = []) {
|
|
4717
|
+
if (agentRuns > 0) ctx.recordStepExecution();
|
|
4718
|
+
agentRuns += 1;
|
|
4719
|
+
return runAgent({
|
|
4720
|
+
instructions: buildAgentInstructions(
|
|
4721
|
+
currentUseCase,
|
|
4722
|
+
input,
|
|
4723
|
+
extraInstructions
|
|
4724
|
+
),
|
|
4725
|
+
tools: toolsForUseCase(currentUseCase, input.ingestionSource),
|
|
4726
|
+
outputSchema: implementationOutputSchema,
|
|
4727
|
+
toolContext: currentUseCase === "ingestion" ? ingestionTools ?? searchTools : searchTools
|
|
4728
|
+
});
|
|
4716
4729
|
}
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
}
|
|
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 ? `
|
|
4732
|
-
${detail}` : ""}`
|
|
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"
|
|
4730
|
+
async function runVerificationUseCase() {
|
|
4731
|
+
if (agentRuns > 0) ctx.recordStepExecution();
|
|
4732
|
+
agentRuns += 1;
|
|
4733
|
+
return runAgent({
|
|
4734
|
+
instructions: buildAgentInstructions("verification", input),
|
|
4735
|
+
tools: toolsForUseCase("verification"),
|
|
4736
|
+
outputSchema: verificationOutputSchema,
|
|
4737
|
+
toolContext: searchTools
|
|
4749
4738
|
});
|
|
4750
4739
|
}
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
if (attempt > 1) {
|
|
4770
|
-
logger.info(
|
|
4771
|
-
{
|
|
4772
|
-
attempt,
|
|
4773
|
-
maxAttempts: MAX_IMPLEMENT_VERIFICATION_ATTEMPTS,
|
|
4774
|
-
extraInstructions
|
|
4775
|
-
},
|
|
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)
|
|
4740
|
+
if (useCases.includes("ingestion")) {
|
|
4741
|
+
let ingestFailureDetail;
|
|
4742
|
+
const result = await runImplementationUseCase("ingestion");
|
|
4743
|
+
summaries.push(formatSummary("ingestion", result.summary));
|
|
4744
|
+
ingestCommand = result.ingestCommand;
|
|
4745
|
+
const ingestionContext = ingestionTools ?? searchTools;
|
|
4746
|
+
const executions = ingestionContext.shell.executions;
|
|
4747
|
+
const {
|
|
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."
|
|
4790
4758
|
);
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
track("AI Wizard Wired to UI", {
|
|
4795
|
-
location_heuristic: searchLocation ?? "unknown"
|
|
4796
|
-
});
|
|
4797
|
-
break;
|
|
4798
|
-
}
|
|
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}`
|
|
4759
|
+
logger.warn(
|
|
4760
|
+
{ ingestCommand },
|
|
4761
|
+
"implement: ingestion ran without a reviewScript call"
|
|
4803
4762
|
);
|
|
4804
4763
|
}
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
);
|
|
4816
|
-
|
|
4764
|
+
if (ingestScriptRan) {
|
|
4765
|
+
ingestOutcomeMessage = `\u2705 Ingestion succeeded${ingestRecordCount != null ? ` \u2014 ${ingestRecordCount} record(s) indexed.` : "."}`;
|
|
4766
|
+
if (ingestRecordCount != null) {
|
|
4767
|
+
track("AI Wizard Ingest Successful", {
|
|
4768
|
+
entity_name: confirmed2?.map((e) => e.name).join(", ") || "unknown",
|
|
4769
|
+
record_count: ingestRecordCount,
|
|
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;
|
|
4817
4777
|
summaries.push(
|
|
4818
|
-
|
|
4778
|
+
`\u26A0\uFE0F Ingestion did not complete: ${reason}.${detail ? `
|
|
4779
|
+
${detail}` : ""}`
|
|
4819
4780
|
);
|
|
4820
|
-
} catch (err) {
|
|
4821
|
-
searchKeyError = err.message;
|
|
4822
4781
|
logger.warn(
|
|
4823
|
-
{
|
|
4824
|
-
|
|
4782
|
+
{
|
|
4783
|
+
ingestCommand,
|
|
4784
|
+
reason,
|
|
4785
|
+
approved: ingestAttempt?.approved,
|
|
4786
|
+
exitCode: ingestAttempt?.exitCode,
|
|
4787
|
+
timedOut: ingestAttempt?.timedOut,
|
|
4788
|
+
commandsRun: executions.length
|
|
4789
|
+
},
|
|
4790
|
+
"implement: ingestion script did not complete successfully"
|
|
4825
4791
|
);
|
|
4792
|
+
track("Error", {
|
|
4793
|
+
step: "Push Data",
|
|
4794
|
+
error: `ingestion did not complete: ${reason}`,
|
|
4795
|
+
product_area: "AI Wizard"
|
|
4796
|
+
});
|
|
4826
4797
|
}
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
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.`);
|
|
4798
|
+
const commandMessages = [`Open the worktree: cd ${shellQuote(worktree)}`];
|
|
4799
|
+
if (ingestCommand) {
|
|
4800
|
+
commandMessages.push(`Ingestion command: ${ingestCommand}`);
|
|
4836
4801
|
}
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
|
|
4802
|
+
await ctx.requestUserInput({
|
|
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
|
+
});
|
|
4816
|
+
}
|
|
4817
|
+
if (useCases.includes("search")) {
|
|
4818
|
+
let extraInstructions = [];
|
|
4819
|
+
const preSearchFiles = new Set(await listChangedFiles(worktree));
|
|
4820
|
+
for (let attempt = 1; attempt <= MAX_IMPLEMENT_VERIFICATION_ATTEMPTS; attempt++) {
|
|
4821
|
+
if (attempt > 1) {
|
|
4822
|
+
logger.info(
|
|
4823
|
+
{
|
|
4824
|
+
attempt,
|
|
4825
|
+
maxAttempts: MAX_IMPLEMENT_VERIFICATION_ATTEMPTS,
|
|
4826
|
+
extraInstructions
|
|
4827
|
+
},
|
|
4828
|
+
"implement: retrying search implementation after failed verification"
|
|
4829
|
+
);
|
|
4830
|
+
}
|
|
4831
|
+
const { summary } = await runImplementationUseCase(
|
|
4832
|
+
"search",
|
|
4833
|
+
extraInstructions
|
|
4843
4834
|
);
|
|
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);
|
|
4844
4858
|
}
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4859
|
+
let searchKey;
|
|
4860
|
+
let searchKeyError;
|
|
4861
|
+
if (appId) {
|
|
4862
|
+
try {
|
|
4863
|
+
const resolved2 = await resolveSearchOnlyKey(
|
|
4864
|
+
targetIndex,
|
|
4865
|
+
appId,
|
|
4866
|
+
envSearchKey
|
|
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
|
+
}
|
|
4850
4879
|
}
|
|
4851
|
-
|
|
4852
|
-
|
|
4853
|
-
|
|
4880
|
+
finalSearchEnvVars = searchEnvVars(
|
|
4881
|
+
language,
|
|
4882
|
+
targetIndex,
|
|
4883
|
+
appId,
|
|
4884
|
+
searchKey
|
|
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
|
|
4854
4893
|
);
|
|
4855
|
-
|
|
4856
|
-
{
|
|
4857
|
-
|
|
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
|
+
}
|
|
4921
|
+
const unresolvedSearchEnvVars = finalSearchEnvVars.filter(
|
|
4922
|
+
(v) => v.value.startsWith("<")
|
|
4923
|
+
);
|
|
4924
|
+
if (unresolvedSearchEnvVars.length > 0) {
|
|
4925
|
+
summaries.push(
|
|
4926
|
+
`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.
|
|
4927
|
+
(searchKeyError ? ` Reason: ${searchKeyError}` : "")
|
|
4858
4928
|
);
|
|
4859
4929
|
}
|
|
4930
|
+
} else {
|
|
4931
|
+
ctx.setUserInput("implementation", "success");
|
|
4860
4932
|
}
|
|
4861
|
-
const
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
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}` : "")
|
|
4933
|
+
const filesChanged = await listChangedFiles(worktree);
|
|
4934
|
+
if (filesChanged.length === 0) {
|
|
4935
|
+
logger.warn(
|
|
4936
|
+
"implement: agent reported success but no files changed in the worktree"
|
|
4867
4937
|
);
|
|
4868
4938
|
}
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4872
|
-
|
|
4873
|
-
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
|
|
4939
|
+
return {
|
|
4940
|
+
ingestionSource,
|
|
4941
|
+
filesChanged,
|
|
4942
|
+
summary: summaries.join("\n\n"),
|
|
4943
|
+
worktreePath: worktree,
|
|
4944
|
+
...useCases.includes("ingestion") && ingestCommand ? {
|
|
4945
|
+
ingestCommand,
|
|
4946
|
+
ingestScriptRan,
|
|
4947
|
+
...ingestRecordCount != null ? { ingestRecordCount } : {},
|
|
4948
|
+
...ingestDurationMs != null ? { ingestDurationMs } : {}
|
|
4949
|
+
} : {},
|
|
4950
|
+
...useCases.includes("search") ? { searchEnvVars: finalSearchEnvVars } : {}
|
|
4951
|
+
};
|
|
4952
|
+
} finally {
|
|
4953
|
+
process.chdir(repoRoot);
|
|
4877
4954
|
}
|
|
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
|
-
};
|
|
4890
4955
|
}
|
|
4891
4956
|
|
|
4892
4957
|
// src/workflows/default.ts
|
|
@@ -4958,7 +5023,10 @@ var defaultWorkflow = {
|
|
|
4958
5023
|
ctx.notify({
|
|
4959
5024
|
messages: ["Building your Algolia search experience\u2026"]
|
|
4960
5025
|
});
|
|
4961
|
-
|
|
5026
|
+
const ingestion2 = ctx.getStepOutput(
|
|
5027
|
+
"ingestion"
|
|
5028
|
+
);
|
|
5029
|
+
return implement(ctx, ["search"], ingestion2?.worktreePath);
|
|
4962
5030
|
}
|
|
4963
5031
|
}),
|
|
4964
5032
|
defineStep({
|
|
@@ -4973,8 +5041,9 @@ var defaultWorkflow = {
|
|
|
4973
5041
|
"ingestion"
|
|
4974
5042
|
);
|
|
4975
5043
|
return reviewStep(ctx, {
|
|
4976
|
-
//
|
|
4977
|
-
//
|
|
5044
|
+
// The ingestion step already showed the user the exact `ingestCommand`
|
|
5045
|
+
// and worktree path as a notice, so nextSteps must not restate it —
|
|
5046
|
+
// an LLM-paraphrased command risks being wrong.
|
|
4978
5047
|
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."
|
|
4979
5048
|
});
|
|
4980
5049
|
}
|
|
@@ -5045,7 +5114,7 @@ var review = {
|
|
|
5045
5114
|
"Ingested 25 generated Product records into wizard_seed_products.",
|
|
5046
5115
|
"Added an InstantSearch search experience to the shared header."
|
|
5047
5116
|
],
|
|
5048
|
-
reviewPrompt: "Review the Algolia ingestion and search changes.",
|
|
5117
|
+
reviewPrompt: "Review the Algolia ingestion and search changes in this worktree.",
|
|
5049
5118
|
nextSteps: ["Point the ingestion script at your real product data."]
|
|
5050
5119
|
};
|
|
5051
5120
|
var SEEDS = {
|
|
@@ -5162,9 +5231,9 @@ Options:
|
|
|
5162
5231
|
steps pre-filled with test data. Pass with no value to print
|
|
5163
5232
|
the step ids. See CONTRIBUTING.md.
|
|
5164
5233
|
--no-telemetry Send no telemetry or analytics for this run.
|
|
5165
|
-
--reset-on-run Wipe this project's wizard state (run state, AI consent
|
|
5166
|
-
before starting, so the run behaves like a
|
|
5167
|
-
run. Also drops every API key the wizard has
|
|
5234
|
+
--reset-on-run Wipe this project's wizard state (run state, AI consent,
|
|
5235
|
+
worktrees) before starting, so the run behaves like a
|
|
5236
|
+
first-ever run. Also drops every API key the wizard has
|
|
5168
5237
|
stored in your keychain (or, where the platform has none,
|
|
5169
5238
|
the encrypted file it falls back to \u2014 see CONTRIBUTING.md),
|
|
5170
5239
|
for this project and any other, so later runs create new
|
|
@@ -5204,7 +5273,7 @@ function parseCliArgs(argv) {
|
|
|
5204
5273
|
}
|
|
5205
5274
|
|
|
5206
5275
|
// src/lib/resetState.ts
|
|
5207
|
-
import { readdir as
|
|
5276
|
+
import { readdir as readdir4, rm as rm2 } from "node:fs/promises";
|
|
5208
5277
|
import { join as join13 } from "node:path";
|
|
5209
5278
|
var KEEP = ["wizard.log"];
|
|
5210
5279
|
async function resetProjectState() {
|
|
@@ -5212,7 +5281,7 @@ async function resetProjectState() {
|
|
|
5212
5281
|
await forgetResolvedKeys();
|
|
5213
5282
|
let entries;
|
|
5214
5283
|
try {
|
|
5215
|
-
entries = await
|
|
5284
|
+
entries = await readdir4(dir);
|
|
5216
5285
|
} catch {
|
|
5217
5286
|
return { dir, removed: [] };
|
|
5218
5287
|
}
|
|
@@ -5225,6 +5294,45 @@ async function resetProjectState() {
|
|
|
5225
5294
|
return { dir, removed: targets };
|
|
5226
5295
|
}
|
|
5227
5296
|
|
|
5297
|
+
// src/lib/terminalSize.ts
|
|
5298
|
+
var MIN_ROWS = 30;
|
|
5299
|
+
var MIN_COLUMNS = 100;
|
|
5300
|
+
async function requestTerminalSize(stdout = process.stdout, {
|
|
5301
|
+
rows = MIN_ROWS,
|
|
5302
|
+
columns = MIN_COLUMNS,
|
|
5303
|
+
timeoutMs = 150,
|
|
5304
|
+
settleMs = 120
|
|
5305
|
+
} = {}) {
|
|
5306
|
+
if (!stdout.isTTY) return;
|
|
5307
|
+
const currentRows = stdout.rows ?? 0;
|
|
5308
|
+
const currentColumns = stdout.columns ?? 0;
|
|
5309
|
+
if (currentRows >= rows && currentColumns >= columns) return;
|
|
5310
|
+
stdout.write(
|
|
5311
|
+
`\x1B[8;${Math.max(currentRows, rows)};${Math.max(currentColumns, columns)}t`
|
|
5312
|
+
);
|
|
5313
|
+
const resized = await waitForResize(stdout, timeoutMs);
|
|
5314
|
+
if (!resized) return;
|
|
5315
|
+
await delay(settleMs);
|
|
5316
|
+
stdout.write("\x1B[2J\x1B[H");
|
|
5317
|
+
}
|
|
5318
|
+
function waitForResize(stdout, timeoutMs) {
|
|
5319
|
+
return new Promise((resolve4) => {
|
|
5320
|
+
const finish = (didResize) => () => {
|
|
5321
|
+
clearTimeout(timer);
|
|
5322
|
+
stdout.off("resize", onResize);
|
|
5323
|
+
resolve4(didResize);
|
|
5324
|
+
};
|
|
5325
|
+
const onResize = finish(true);
|
|
5326
|
+
const timer = setTimeout(finish(false), timeoutMs);
|
|
5327
|
+
stdout.once("resize", onResize);
|
|
5328
|
+
});
|
|
5329
|
+
}
|
|
5330
|
+
function delay(ms) {
|
|
5331
|
+
return new Promise((resolve4) => {
|
|
5332
|
+
setTimeout(resolve4, ms);
|
|
5333
|
+
});
|
|
5334
|
+
}
|
|
5335
|
+
|
|
5228
5336
|
// src/main.tsx
|
|
5229
5337
|
import { jsx as jsx15 } from "react/jsx-runtime";
|
|
5230
5338
|
async function startup() {
|
|
@@ -5309,6 +5417,7 @@ async function run(workflow) {
|
|
|
5309
5417
|
}
|
|
5310
5418
|
runWorkflow(workflow, app.id);
|
|
5311
5419
|
}
|
|
5420
|
+
await requestTerminalSize();
|
|
5312
5421
|
var started = await startup();
|
|
5313
5422
|
if (typeof started === "number") {
|
|
5314
5423
|
process.exitCode = started;
|