@algolia/wizard 0.33.0 → 0.34.0-rc.125.243
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/dist/main.js +90 -67
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -2759,17 +2759,23 @@ async function hasSymlinkParent(ctx, target) {
|
|
|
2759
2759
|
// src/lib/tools/listFiles.ts
|
|
2760
2760
|
function listFilesTool(ctx) {
|
|
2761
2761
|
return tool({
|
|
2762
|
-
description:
|
|
2763
|
-
inputSchema: z6.object(
|
|
2764
|
-
|
|
2765
|
-
|
|
2762
|
+
description: 'List files in a directory (default: the current working directory). Pass path to list a subdirectory directly \u2014 e.g. "packages/api" \u2014 without first changeDirectory-ing into it.',
|
|
2763
|
+
inputSchema: z6.object({
|
|
2764
|
+
path: z6.string().optional().describe("Directory to list, relative to cwd (default: cwd)")
|
|
2765
|
+
}),
|
|
2766
|
+
execute: async ({ path = "." }) => {
|
|
2767
|
+
logger.info({ path }, "called listFiles tool");
|
|
2766
2768
|
if (++ctx.counts.list > ctx.limits.list) {
|
|
2767
2769
|
return `Refused: list limit (${ctx.limits.list}) reached. Stop listing and proceed with the information you already have.`;
|
|
2768
2770
|
}
|
|
2769
|
-
const resolved2 = resolveInRoot(ctx,
|
|
2771
|
+
const resolved2 = resolveInRoot(ctx, path);
|
|
2770
2772
|
if (!resolved2.ok) return resolved2.error;
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
+
try {
|
|
2774
|
+
const entries = await readdir(resolved2.target, { withFileTypes: true });
|
|
2775
|
+
return entries.map((e) => e.isDirectory() ? `${e.name}/` : e.name).join("\n");
|
|
2776
|
+
} catch (err) {
|
|
2777
|
+
return `Error listing ${path}: ${err.message}`;
|
|
2778
|
+
}
|
|
2773
2779
|
}
|
|
2774
2780
|
});
|
|
2775
2781
|
}
|
|
@@ -3196,11 +3202,13 @@ function appendEnv(content, entries) {
|
|
|
3196
3202
|
return content + prefix + lines;
|
|
3197
3203
|
}
|
|
3198
3204
|
function hasEnv(content, name) {
|
|
3199
|
-
return new RegExp(`^(\\
|
|
3205
|
+
return new RegExp(`^([ \\t]*(?:export[ \\t]+)?${name})[ \\t]*=`, "m").test(
|
|
3206
|
+
content
|
|
3207
|
+
);
|
|
3200
3208
|
}
|
|
3201
3209
|
function readEnv(content, name) {
|
|
3202
3210
|
const found = content.match(
|
|
3203
|
-
new RegExp(
|
|
3211
|
+
new RegExp(`^[ \\t]*(?:export[ \\t]+)?${name}[ \\t]*=[ \\t]*(.*)$`, "m")
|
|
3204
3212
|
);
|
|
3205
3213
|
if (!found) return null;
|
|
3206
3214
|
const raw = found[1].trim();
|
|
@@ -3211,16 +3219,16 @@ function readEnv(content, name) {
|
|
|
3211
3219
|
function upsertEnv(content, name, value) {
|
|
3212
3220
|
if (!hasEnv(content, name)) return appendEnv(content, [[name, value]]);
|
|
3213
3221
|
return content.replace(
|
|
3214
|
-
new RegExp(
|
|
3222
|
+
new RegExp(`^[ \\t]*(?:export[ \\t]+)?${name}[ \\t]*=.*$`, "gm"),
|
|
3215
3223
|
() => `${name}=${value}`
|
|
3216
3224
|
);
|
|
3217
3225
|
}
|
|
3218
3226
|
function writeCredentialsTool(ctx) {
|
|
3219
3227
|
return tool6({
|
|
3220
|
-
description: `Write the active Algolia credentials (${APP_ID_VAR} and ${API_KEY_VAR}) and the target index name (${INDEX_NAME_VAR}) into the given env file. The credentials come from the selected Algolia application, with a write key scoped to the target index; you only pass the path to the env file. ${INDEX_NAME_VAR} is always set to this run's target index, replacing any value already there. An ${APP_ID_VAR} or ${API_KEY_VAR} the file already gives a value is left untouched; a missing or blank one is filled in when it can be paired with the selected application. The env file is added to .gitignore automatically; do not edit .gitignore yourself.`,
|
|
3228
|
+
description: `Write the active Algolia credentials (${APP_ID_VAR} and ${API_KEY_VAR}) and the target index name (${INDEX_NAME_VAR}) into the given env file. The credentials come from the selected Algolia application, with a write key scoped to the target index; you only pass the path to the env file. ${INDEX_NAME_VAR} is always set to this run's target index, replacing any value already there. An ${APP_ID_VAR} or ${API_KEY_VAR} the file already gives a value is left untouched; a missing or blank one is filled in when it can be paired with the selected application. The env file is added to .gitignore automatically; do not edit .gitignore yourself. If the script or app that reads these credentials lives in a subdirectory (e.g. a package in a monorepo), an env file at the repo root is the wrong default \u2014 a script only loads env vars from its own directory (or one it's explicitly configured to read), so check every directory from the script's own up to the repo root, not just those two: its own directory, each ancestor in between (a shared workspace-level directory above the immediate package is common), and the root. Use whichever of those already holds real credentials; only fall back to the repo root when none of them do. Never invent a brand-new file in one of those directories when a real one already exists in another \u2014 that leaves the real one stale and the new one wrong. Listing just the script's own directory and the very top-level root is not enough to find a workspace-level file in between; check the intermediate ones too. If instructions describe a location that doesn't match the project you actually find (e.g. a path outside the repo, or a convention the project doesn't follow), don't stop and ask before doing anything \u2014 call this tool on the real, in-repo file the script actually reads (that's always the safe default), then note the mismatch afterward. Ending your turn with only a question and no call to this tool leaves the project unconfigured.`,
|
|
3221
3229
|
inputSchema: z13.object({
|
|
3222
3230
|
filePath: z13.string().describe(
|
|
3223
|
-
'Path to the env file to write credentials into (e.g. ".env")'
|
|
3231
|
+
'Path to the env file to write credentials into, relative to the repo root (e.g. ".env", or "packages/api/.env" when the consuming script lives in that package)'
|
|
3224
3232
|
)
|
|
3225
3233
|
}),
|
|
3226
3234
|
execute: async ({ filePath }) => {
|
|
@@ -3974,6 +3982,7 @@ var detectLanguage = () => runAgent({
|
|
|
3974
3982
|
"Return the exact version",
|
|
3975
3983
|
"Exclude things like CSS frameworks, build tools, or testing frameworks",
|
|
3976
3984
|
`Determine publicEnvVarPrefix: check the project's own env var usage first (e.g. names already referenced in code, .env/.env.example); if none exists, fall back to the detected framework's known convention for exposing env vars to client-side code; use "" when the project has no such convention (e.g. a backend-only project).`,
|
|
3985
|
+
"A brand-new project has no existing env var usage to find \u2014 one or two targeted checks (e.g. .env/.env.example, or a grep for the bundler's public-prefix convention) are enough to confirm that. Do not keep searching once those turn up nothing; fall back to the framework convention immediately.",
|
|
3977
3986
|
'Use as few tools as possible, but do not guess. If you cant find the answer, say "unknown"',
|
|
3978
3987
|
"When done, call reportStatus"
|
|
3979
3988
|
],
|
|
@@ -4069,7 +4078,7 @@ async function runAnalysis(mode, extraInstructions = []) {
|
|
|
4069
4078
|
// package.json
|
|
4070
4079
|
var package_default = {
|
|
4071
4080
|
name: "@algolia/wizard",
|
|
4072
|
-
version: "0.
|
|
4081
|
+
version: "0.34.0-rc.125.243",
|
|
4073
4082
|
description: "Magically implement Algolia functionality in your codebase",
|
|
4074
4083
|
type: "module",
|
|
4075
4084
|
engines: {
|
|
@@ -4475,7 +4484,7 @@ ${formatCompletedSteps(ctx.completedSteps)}`,
|
|
|
4475
4484
|
|
|
4476
4485
|
// src/actions/implement.ts
|
|
4477
4486
|
import z29 from "zod";
|
|
4478
|
-
import {
|
|
4487
|
+
import { relative as relative6 } from "node:path";
|
|
4479
4488
|
|
|
4480
4489
|
// src/lib/git.ts
|
|
4481
4490
|
import { execFile as execFile2 } from "node:child_process";
|
|
@@ -4533,41 +4542,43 @@ async function copyUploadIntoProject(repoRoot, ingestDir, sourcePath) {
|
|
|
4533
4542
|
}
|
|
4534
4543
|
return { ok: true, relPath };
|
|
4535
4544
|
}
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
}
|
|
4539
|
-
async function readEnvVar(repoRoot, name) {
|
|
4540
|
-
let content;
|
|
4545
|
+
var ENV_FILE_PRECEDENCE = [".env", ".env.local"];
|
|
4546
|
+
async function readEnvFileIfExists(path) {
|
|
4541
4547
|
try {
|
|
4542
|
-
|
|
4548
|
+
return await readFile8(path, "utf8");
|
|
4543
4549
|
} catch (err) {
|
|
4544
4550
|
if (err.code !== "ENOENT") throw err;
|
|
4545
4551
|
return void 0;
|
|
4546
4552
|
}
|
|
4547
|
-
const match = new RegExp(
|
|
4548
|
-
`^[ \\t]*(?:export[ \\t]+)?${name}[ \\t]*=[ \\t]*(.*)$`,
|
|
4549
|
-
"m"
|
|
4550
|
-
).exec(content);
|
|
4551
|
-
if (!match) return void 0;
|
|
4552
|
-
const value = match[1].trim().replace(/^(['"])(.*)\1$/, "$2").trim();
|
|
4553
|
-
if (!value || value.startsWith("<")) return void 0;
|
|
4554
|
-
return value;
|
|
4555
4553
|
}
|
|
4556
|
-
async function
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
if (err.code !== "ENOENT") throw err;
|
|
4554
|
+
async function definingEnvFile(repoRoot, name) {
|
|
4555
|
+
let found;
|
|
4556
|
+
for (const file of ENV_FILE_PRECEDENCE) {
|
|
4557
|
+
const path = join10(repoRoot, file);
|
|
4558
|
+
const content = await readEnvFileIfExists(path);
|
|
4559
|
+
if (content !== void 0 && hasEnv(content, name)) found = { path, content };
|
|
4563
4560
|
}
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
const
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4561
|
+
return found;
|
|
4562
|
+
}
|
|
4563
|
+
function usableEnvValue(content, name) {
|
|
4564
|
+
const value = readEnv(content, name);
|
|
4565
|
+
return value && !value.startsWith("<") ? value : void 0;
|
|
4566
|
+
}
|
|
4567
|
+
async function readEnvVar(repoRoot, name) {
|
|
4568
|
+
const found = await definingEnvFile(repoRoot, name);
|
|
4569
|
+
return found ? usableEnvValue(found.content, name) : void 0;
|
|
4570
|
+
}
|
|
4571
|
+
async function writeSearchEnvValues(repoRoot, vars, defaultFileName = ".env") {
|
|
4572
|
+
const written = [];
|
|
4573
|
+
for (const { name, value } of vars) {
|
|
4574
|
+
const found = await definingEnvFile(repoRoot, name);
|
|
4575
|
+
if (found && usableEnvValue(found.content, name) !== void 0) continue;
|
|
4576
|
+
const path = found?.path ?? join10(repoRoot, defaultFileName);
|
|
4577
|
+
const existing = found?.content ?? await readEnvFileIfExists(path) ?? "";
|
|
4578
|
+
await writeFile7(path, upsertEnv(existing, name, value), "utf8");
|
|
4579
|
+
written.push({ name, file: path });
|
|
4580
|
+
}
|
|
4581
|
+
return written;
|
|
4571
4582
|
}
|
|
4572
4583
|
function normalizeFindingPaths(findings) {
|
|
4573
4584
|
return {
|
|
@@ -4658,6 +4669,7 @@ var resolveEnvVarPrefix = (frameworkName) => runAgent({
|
|
|
4658
4669
|
instructions: [
|
|
4659
4670
|
`The developer corrected the project's framework to "${frameworkName}".`,
|
|
4660
4671
|
`Determine publicEnvVarPrefix for this framework: check the project's own env var usage first (e.g. names already referenced in code, .env/.env.example); if none exists, fall back to this framework's known convention for exposing env vars to client-side code; use "" when the framework has no such convention (e.g. a backend-only framework).`,
|
|
4672
|
+
"A brand-new project has no existing env var usage to find \u2014 one or two targeted checks (e.g. .env/.env.example, or a grep for the framework's public-prefix convention) are enough to confirm that. Do not keep searching once those turn up nothing; fall back to the framework convention immediately.",
|
|
4661
4673
|
'Use as few tools as possible, but do not guess. If you cant find the answer, say "unknown"',
|
|
4662
4674
|
"When done, call reportStatus"
|
|
4663
4675
|
],
|
|
@@ -4694,6 +4706,7 @@ var MAX_IMPLEMENT_VERIFICATION_ATTEMPTS = 3;
|
|
|
4694
4706
|
var DEFAULT_IMPLEMENT_USE_CASES = ["ingestion", "search"];
|
|
4695
4707
|
var INGEST_DIR = ".algolia-wizard";
|
|
4696
4708
|
var JS_LANGUAGES = ["javascript", "typescript", "jsx", "tsx", "node"];
|
|
4709
|
+
var ENV_LOCAL_AWARE_PREFIXES = ["VITE_", "NEXT_PUBLIC_", "REACT_APP_"];
|
|
4697
4710
|
function lower(entries) {
|
|
4698
4711
|
return entries.map((entry) => entry.name.toLowerCase());
|
|
4699
4712
|
}
|
|
@@ -4793,17 +4806,19 @@ function searchInstructions(input) {
|
|
|
4793
4806
|
`Create the search experience as its own component in a new file, following the project's existing component conventions (location, naming, styling approach). Do not write it inline into an existing file.`,
|
|
4794
4807
|
`Import and render that new component from ${input.searchLocation ? `"${input.searchLocation}"` : "the best shared, always-rendered layout location (e.g. a header/nav component)"} so it is reachable across the app \u2014 at least a working search input and results list against the target index.`,
|
|
4795
4808
|
"If a search box already exists, replace its usage with an import and render of your new component; remove the old implementation.",
|
|
4809
|
+
"When rendering results with an existing shared component (e.g. a card), import and reuse that component rather than inlining its markup \u2014 inlining silently drops the styles and behavior its own file provides.",
|
|
4796
4810
|
`Read the index name from the ${publicIndexNameVar(input.publicEnvVarPrefix)} env var, which the wizard sets to "${input.targetIndex}". Never hardcode an index name or derive one from the project, file, or component name.`,
|
|
4797
4811
|
"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.",
|
|
4798
4812
|
// The key is provisioned only after verification passes, and the wizard
|
|
4799
|
-
// reads
|
|
4800
|
-
// value there
|
|
4801
|
-
|
|
4802
|
-
|
|
4813
|
+
// reads the project's env files to decide whether a key already exists —
|
|
4814
|
+
// an agent-scaffolded value there, even a blank one, would be treated as
|
|
4815
|
+
// real and silently override whatever the wizard writes.
|
|
4816
|
+
`Add Algolia App ID "${input.appId}"; leave the search-only key as a placeholder in the code only. Do not create or edit any .env* file (.env, .env.local, etc.) yourself, not even to add a blank placeholder line \u2014 the wizard resolves the real key and writes it there itself.`,
|
|
4817
|
+
// The wizard writes these exact names into the project's env files right after this step.
|
|
4803
4818
|
`Use exactly these env var names in the code: ${input.searchEnvVars.map(({ name }) => name).join(", ")}.`,
|
|
4804
4819
|
"Install any Algolia packages you import with the project's own package manager via runShell, and declare them in the project's dependency manifest.",
|
|
4805
4820
|
"Match the styles of the application as closely as possible.",
|
|
4806
|
-
"The summary should be extremely concise; do not mention env var setup or manual testing steps \u2014 the wizard writes the resolved credentials to
|
|
4821
|
+
"The summary should be extremely concise; do not mention env var setup or manual testing steps \u2014 the wizard writes the resolved credentials to the project env files and reports that separately."
|
|
4807
4822
|
];
|
|
4808
4823
|
}
|
|
4809
4824
|
function verificationInstructions(input) {
|
|
@@ -5063,11 +5078,11 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES) {
|
|
|
5063
5078
|
const appIdVarName = publicAppIdVar(publicEnvVarPrefix);
|
|
5064
5079
|
const searchKeyVarName = publicSearchKeyVar(publicEnvVarPrefix);
|
|
5065
5080
|
summaries.push(
|
|
5066
|
-
`\u26A0\uFE0F
|
|
5081
|
+
`\u26A0\uFE0F Your project's env files already set ${appIdVarName}=${envAppId}, but the active Algolia application is ${appId}. The wizard left those values alone \u2014 update ${appIdVarName} and ${searchKeyVarName} by hand, or searches will fail.`
|
|
5067
5082
|
);
|
|
5068
5083
|
logger.warn(
|
|
5069
5084
|
{ envAppId, appId },
|
|
5070
|
-
"implement:
|
|
5085
|
+
"implement: env files hold credentials for a different Algolia application; not reusing its search key"
|
|
5071
5086
|
);
|
|
5072
5087
|
}
|
|
5073
5088
|
}
|
|
@@ -5240,7 +5255,7 @@ ${detail}` : ""}`
|
|
|
5240
5255
|
searchKeyError = err.message;
|
|
5241
5256
|
logger.warn(
|
|
5242
5257
|
{ err: searchKeyError },
|
|
5243
|
-
"implement: could not provision a search-only API key; the
|
|
5258
|
+
"implement: could not provision a search-only API key; the env value stays a placeholder"
|
|
5244
5259
|
);
|
|
5245
5260
|
}
|
|
5246
5261
|
}
|
|
@@ -5254,34 +5269,42 @@ ${detail}` : ""}`
|
|
|
5254
5269
|
(v) => !v.value.startsWith("<")
|
|
5255
5270
|
);
|
|
5256
5271
|
if (resolvedSearchEnvVars.length > 0) {
|
|
5272
|
+
const defaultEnvFile = ENV_LOCAL_AWARE_PREFIXES.includes(
|
|
5273
|
+
publicEnvVarPrefix
|
|
5274
|
+
) ? ".env.local" : ".env";
|
|
5257
5275
|
const written = await writeSearchEnvValues(
|
|
5258
5276
|
repoRoot,
|
|
5259
|
-
resolvedSearchEnvVars
|
|
5277
|
+
resolvedSearchEnvVars,
|
|
5278
|
+
defaultEnvFile
|
|
5260
5279
|
);
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
5280
|
+
const writtenFiles = [...new Set(written.map((w) => w.file))];
|
|
5281
|
+
for (const file of writtenFiles) {
|
|
5282
|
+
const label = relative6(repoRoot, file);
|
|
5283
|
+
const names = written.filter((w) => w.file === file).map((w) => w.name);
|
|
5284
|
+
summaries.push(`Wrote ${names.join(", ")} to ${label}.`);
|
|
5285
|
+
const ignored = await ensureGitIgnored(repoRoot, file);
|
|
5286
|
+
if (ignored === "added") {
|
|
5287
|
+
summaries.push(`Added ${label} to .gitignore.`);
|
|
5288
|
+
} else if (ignored === "tracked") {
|
|
5289
|
+
summaries.push(
|
|
5290
|
+
`\u26A0\uFE0F ${label} is tracked by git, so a .gitignore rule cannot un-stage it. Run "git rm --cached ${label}" before committing, or the credentials go into history.`
|
|
5291
|
+
);
|
|
5292
|
+
}
|
|
5271
5293
|
}
|
|
5294
|
+
const writtenNames = new Set(written.map((w) => w.name));
|
|
5272
5295
|
const stale = [];
|
|
5273
5296
|
for (const v of resolvedSearchEnvVars) {
|
|
5274
|
-
if (
|
|
5297
|
+
if (writtenNames.has(v.name)) continue;
|
|
5275
5298
|
const current = await readEnvVar(repoRoot, v.name);
|
|
5276
5299
|
if (current && current !== v.value) stale.push(v);
|
|
5277
5300
|
}
|
|
5278
5301
|
if (stale.length > 0 && !envAppIdMismatch) {
|
|
5279
5302
|
summaries.push(
|
|
5280
|
-
`\u26A0\uFE0F
|
|
5303
|
+
`\u26A0\uFE0F Your project's env files already assign 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.`
|
|
5281
5304
|
);
|
|
5282
5305
|
logger.warn(
|
|
5283
5306
|
{ vars: stale.map((v) => v.name) },
|
|
5284
|
-
"implement:
|
|
5307
|
+
"implement: env files hold different values for the resolved search credentials; not overwriting them"
|
|
5285
5308
|
);
|
|
5286
5309
|
}
|
|
5287
5310
|
}
|
|
@@ -5290,7 +5313,7 @@ ${detail}` : ""}`
|
|
|
5290
5313
|
);
|
|
5291
5314
|
if (unresolvedSearchEnvVars.length > 0) {
|
|
5292
5315
|
summaries.push(
|
|
5293
|
-
`Could not resolve a value for ${unresolvedSearchEnvVars.map((v) => v.name).join(", ")} \u2014 fill it in manually in .env.` + (searchKeyError ? ` Reason: ${searchKeyError}` : "")
|
|
5316
|
+
`Could not resolve a value for ${unresolvedSearchEnvVars.map((v) => v.name).join(", ")} \u2014 fill it in manually in .env or .env.local.` + (searchKeyError ? ` Reason: ${searchKeyError}` : "")
|
|
5294
5317
|
);
|
|
5295
5318
|
}
|
|
5296
5319
|
} else {
|
|
@@ -5624,7 +5647,7 @@ function parseCliArgs(argv) {
|
|
|
5624
5647
|
|
|
5625
5648
|
// src/lib/resetState.ts
|
|
5626
5649
|
import { readdir as readdir3, rm as rm2 } from "node:fs/promises";
|
|
5627
|
-
import { join as
|
|
5650
|
+
import { join as join12 } from "node:path";
|
|
5628
5651
|
var KEEP = ["wizard.log"];
|
|
5629
5652
|
async function resetProjectState() {
|
|
5630
5653
|
const dir = stateDir();
|
|
@@ -5638,7 +5661,7 @@ async function resetProjectState() {
|
|
|
5638
5661
|
const targets = entries.filter((name) => !KEEP.includes(name));
|
|
5639
5662
|
await Promise.all(
|
|
5640
5663
|
targets.map(
|
|
5641
|
-
(name) => rm2(
|
|
5664
|
+
(name) => rm2(join12(dir, name), { recursive: true, force: true })
|
|
5642
5665
|
)
|
|
5643
5666
|
);
|
|
5644
5667
|
return { dir, removed: targets };
|