@algolia/wizard 0.33.0-rc.126.240 → 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 +124 -288
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -253,7 +253,6 @@ var useWizard = create((set, get) => ({
|
|
|
253
253
|
cliOutput: [],
|
|
254
254
|
targetIndex: null,
|
|
255
255
|
writtenFiles: [],
|
|
256
|
-
approvedCommands: /* @__PURE__ */ new Set(),
|
|
257
256
|
logs: [],
|
|
258
257
|
error: null,
|
|
259
258
|
inputReq: null,
|
|
@@ -349,10 +348,6 @@ var useWizard = create((set, get) => ({
|
|
|
349
348
|
setTargetIndex: (index) => set({ targetIndex: index }),
|
|
350
349
|
recordWrittenFile: (path) => set((s) => ({ writtenFiles: [...s.writtenFiles, path] })),
|
|
351
350
|
clearWrittenFiles: () => set({ writtenFiles: [] }),
|
|
352
|
-
isCommandApproved: (command, cwd) => get().approvedCommands.has(`${cwd}\0${command}`),
|
|
353
|
-
approveCommand: (command, cwd) => set((s) => ({
|
|
354
|
-
approvedCommands: new Set(s.approvedCommands).add(`${cwd}\0${command}`)
|
|
355
|
-
})),
|
|
356
351
|
logStart: (kind, name, input) => {
|
|
357
352
|
const id = nanoid();
|
|
358
353
|
set((s) => ({
|
|
@@ -401,7 +396,6 @@ var useWizard = create((set, get) => ({
|
|
|
401
396
|
cliOutput: [],
|
|
402
397
|
targetIndex: null,
|
|
403
398
|
writtenFiles: [],
|
|
404
|
-
approvedCommands: /* @__PURE__ */ new Set(),
|
|
405
399
|
logs: [],
|
|
406
400
|
error: null,
|
|
407
401
|
inputReq: null,
|
|
@@ -1026,6 +1020,7 @@ function EnterToContinuePrompt({
|
|
|
1026
1020
|
function PromptInput() {
|
|
1027
1021
|
const { phase, inputReq, submitInput } = useWizard();
|
|
1028
1022
|
const [draft, setDraft] = useState6("");
|
|
1023
|
+
const promptKey = inputReq && `${inputReq.promptType}:${inputReq.prompt}`;
|
|
1029
1024
|
if (phase === "done" || phase === "error") {
|
|
1030
1025
|
return /* @__PURE__ */ jsx7(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text9, { color: "gray", dimColor: true, children: "Press Enter or Esc to exit" }) });
|
|
1031
1026
|
}
|
|
@@ -1044,7 +1039,8 @@ function PromptInput() {
|
|
|
1044
1039
|
cancelable: inputReq.cancelable,
|
|
1045
1040
|
error: inputReq.error,
|
|
1046
1041
|
onSelect: submitInput
|
|
1047
|
-
}
|
|
1042
|
+
},
|
|
1043
|
+
promptKey
|
|
1048
1044
|
) });
|
|
1049
1045
|
}
|
|
1050
1046
|
if (inputReq.promptType === "multiSelect") {
|
|
@@ -1059,7 +1055,8 @@ function PromptInput() {
|
|
|
1059
1055
|
options: inputReq.options,
|
|
1060
1056
|
error: inputReq.error,
|
|
1061
1057
|
onSelect: submitInput
|
|
1062
|
-
}
|
|
1058
|
+
},
|
|
1059
|
+
promptKey
|
|
1063
1060
|
) });
|
|
1064
1061
|
}
|
|
1065
1062
|
if (inputReq.promptType === "notice") {
|
|
@@ -1070,7 +1067,8 @@ function PromptInput() {
|
|
|
1070
1067
|
messages: inputReq.messages,
|
|
1071
1068
|
options: ["Continue"],
|
|
1072
1069
|
onSelect: submitInput
|
|
1073
|
-
}
|
|
1070
|
+
},
|
|
1071
|
+
promptKey
|
|
1074
1072
|
) });
|
|
1075
1073
|
}
|
|
1076
1074
|
if (inputReq.promptType === "enterToContinue") {
|
|
@@ -1096,7 +1094,8 @@ function PromptInput() {
|
|
|
1096
1094
|
options: labels,
|
|
1097
1095
|
secondary: inputReq.secondary,
|
|
1098
1096
|
onSelect: (value) => submitInput(value === labels[0])
|
|
1099
|
-
}
|
|
1097
|
+
},
|
|
1098
|
+
promptKey
|
|
1100
1099
|
) });
|
|
1101
1100
|
}
|
|
1102
1101
|
return /* @__PURE__ */ jsxs8(Box9, { flexDirection: "column", children: [
|
|
@@ -2716,8 +2715,8 @@ var selectIndexStep = async (ctx) => {
|
|
|
2716
2715
|
};
|
|
2717
2716
|
|
|
2718
2717
|
// src/lib/agent.ts
|
|
2719
|
-
import { ToolLoopAgent, hasToolCall, Output as
|
|
2720
|
-
import { createAnthropic as
|
|
2718
|
+
import { ToolLoopAgent, hasToolCall, Output as Output2 } from "ai";
|
|
2719
|
+
import { createAnthropic as createAnthropic2 } from "@ai-sdk/anthropic";
|
|
2721
2720
|
import "zod";
|
|
2722
2721
|
|
|
2723
2722
|
// src/lib/tools/index.ts
|
|
@@ -2760,17 +2759,23 @@ async function hasSymlinkParent(ctx, target) {
|
|
|
2760
2759
|
// src/lib/tools/listFiles.ts
|
|
2761
2760
|
function listFilesTool(ctx) {
|
|
2762
2761
|
return tool({
|
|
2763
|
-
description:
|
|
2764
|
-
inputSchema: z6.object(
|
|
2765
|
-
|
|
2766
|
-
|
|
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");
|
|
2767
2768
|
if (++ctx.counts.list > ctx.limits.list) {
|
|
2768
2769
|
return `Refused: list limit (${ctx.limits.list}) reached. Stop listing and proceed with the information you already have.`;
|
|
2769
2770
|
}
|
|
2770
|
-
const resolved2 = resolveInRoot(ctx,
|
|
2771
|
+
const resolved2 = resolveInRoot(ctx, path);
|
|
2771
2772
|
if (!resolved2.ok) return resolved2.error;
|
|
2772
|
-
|
|
2773
|
-
|
|
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
|
+
}
|
|
2774
2779
|
}
|
|
2775
2780
|
});
|
|
2776
2781
|
}
|
|
@@ -3197,11 +3202,13 @@ function appendEnv(content, entries) {
|
|
|
3197
3202
|
return content + prefix + lines;
|
|
3198
3203
|
}
|
|
3199
3204
|
function hasEnv(content, name) {
|
|
3200
|
-
return new RegExp(`^(\\
|
|
3205
|
+
return new RegExp(`^([ \\t]*(?:export[ \\t]+)?${name})[ \\t]*=`, "m").test(
|
|
3206
|
+
content
|
|
3207
|
+
);
|
|
3201
3208
|
}
|
|
3202
3209
|
function readEnv(content, name) {
|
|
3203
3210
|
const found = content.match(
|
|
3204
|
-
new RegExp(
|
|
3211
|
+
new RegExp(`^[ \\t]*(?:export[ \\t]+)?${name}[ \\t]*=[ \\t]*(.*)$`, "m")
|
|
3205
3212
|
);
|
|
3206
3213
|
if (!found) return null;
|
|
3207
3214
|
const raw = found[1].trim();
|
|
@@ -3212,16 +3219,16 @@ function readEnv(content, name) {
|
|
|
3212
3219
|
function upsertEnv(content, name, value) {
|
|
3213
3220
|
if (!hasEnv(content, name)) return appendEnv(content, [[name, value]]);
|
|
3214
3221
|
return content.replace(
|
|
3215
|
-
new RegExp(
|
|
3222
|
+
new RegExp(`^[ \\t]*(?:export[ \\t]+)?${name}[ \\t]*=.*$`, "gm"),
|
|
3216
3223
|
() => `${name}=${value}`
|
|
3217
3224
|
);
|
|
3218
3225
|
}
|
|
3219
3226
|
function writeCredentialsTool(ctx) {
|
|
3220
3227
|
return tool6({
|
|
3221
|
-
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.`,
|
|
3222
3229
|
inputSchema: z13.object({
|
|
3223
3230
|
filePath: z13.string().describe(
|
|
3224
|
-
'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)'
|
|
3225
3232
|
)
|
|
3226
3233
|
}),
|
|
3227
3234
|
execute: async ({ filePath }) => {
|
|
@@ -3399,8 +3406,7 @@ function searchFilesTool(ctx) {
|
|
|
3399
3406
|
}
|
|
3400
3407
|
|
|
3401
3408
|
// src/lib/tools/runShell.ts
|
|
3402
|
-
import { tool as tool8
|
|
3403
|
-
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
3409
|
+
import { tool as tool8 } from "ai";
|
|
3404
3410
|
import z15 from "zod";
|
|
3405
3411
|
import { relative as relative4 } from "node:path";
|
|
3406
3412
|
|
|
@@ -3509,10 +3515,8 @@ function runShell(command, opts) {
|
|
|
3509
3515
|
// src/lib/tools/runShell.ts
|
|
3510
3516
|
function storeApproval(root) {
|
|
3511
3517
|
return async (req) => {
|
|
3512
|
-
const store = useWizard.getState();
|
|
3513
|
-
if (store.isCommandApproved(req.command, req.cwd)) return "approve";
|
|
3514
3518
|
const rel = relative4(root, req.cwd);
|
|
3515
|
-
const answer = await
|
|
3519
|
+
const answer = await useWizard.getState().requestUserInput({
|
|
3516
3520
|
prompt: "Run this command?",
|
|
3517
3521
|
promptType: "commandApproval",
|
|
3518
3522
|
options: [],
|
|
@@ -3521,170 +3525,20 @@ function storeApproval(root) {
|
|
|
3521
3525
|
cwd: rel === "" || rel.startsWith("..") ? req.cwd : rel
|
|
3522
3526
|
}
|
|
3523
3527
|
});
|
|
3524
|
-
|
|
3525
|
-
store.approveCommand(req.command, req.cwd);
|
|
3526
|
-
return "approve";
|
|
3528
|
+
return answer === "approve" ? "approve" : "reject";
|
|
3527
3529
|
};
|
|
3528
3530
|
}
|
|
3529
3531
|
var EXPLORATORY_COMMANDS = /* @__PURE__ */ new Set(["ls", "find", "tree", "dir"]);
|
|
3530
|
-
function commandSegments(command) {
|
|
3531
|
-
return command.split(/&&|;|\|/).map((segment) => segment.trim());
|
|
3532
|
-
}
|
|
3533
3532
|
function isExploratoryCommand(command) {
|
|
3534
|
-
return
|
|
3535
|
-
}
|
|
3536
|
-
var READ_ONLY_BINARIES = /* @__PURE__ */ new Set([
|
|
3537
|
-
"cat",
|
|
3538
|
-
"head",
|
|
3539
|
-
"tail",
|
|
3540
|
-
"wc",
|
|
3541
|
-
"pwd",
|
|
3542
|
-
"echo",
|
|
3543
|
-
"date",
|
|
3544
|
-
"whoami",
|
|
3545
|
-
"hostname",
|
|
3546
|
-
"uname",
|
|
3547
|
-
"which",
|
|
3548
|
-
"file",
|
|
3549
|
-
"stat",
|
|
3550
|
-
"grep",
|
|
3551
|
-
"egrep",
|
|
3552
|
-
"fgrep",
|
|
3553
|
-
"rg",
|
|
3554
|
-
"diff"
|
|
3555
|
-
]);
|
|
3556
|
-
var READ_ONLY_NO_ARGS_BINARIES = /* @__PURE__ */ new Set(["env", "printenv"]);
|
|
3557
|
-
var GIT_READ_ONLY_SUBCOMMANDS = /* @__PURE__ */ new Set([
|
|
3558
|
-
"status",
|
|
3559
|
-
"log",
|
|
3560
|
-
"diff",
|
|
3561
|
-
"show",
|
|
3562
|
-
"describe",
|
|
3563
|
-
"blame",
|
|
3564
|
-
"ls-files",
|
|
3565
|
-
"rev-parse",
|
|
3566
|
-
"cat-file",
|
|
3567
|
-
"shortlog",
|
|
3568
|
-
"ls-remote"
|
|
3569
|
-
]);
|
|
3570
|
-
var VERSION_CHECK_BINARIES = /* @__PURE__ */ new Set([
|
|
3571
|
-
"node",
|
|
3572
|
-
"tsc",
|
|
3573
|
-
"npm",
|
|
3574
|
-
"pnpm",
|
|
3575
|
-
"yarn",
|
|
3576
|
-
"python",
|
|
3577
|
-
"python3",
|
|
3578
|
-
"ruby",
|
|
3579
|
-
"go",
|
|
3580
|
-
"cargo",
|
|
3581
|
-
"rustc",
|
|
3582
|
-
"php",
|
|
3583
|
-
"composer",
|
|
3584
|
-
"java",
|
|
3585
|
-
"mvn",
|
|
3586
|
-
"gradle",
|
|
3587
|
-
"bundle",
|
|
3588
|
-
"git"
|
|
3589
|
-
]);
|
|
3590
|
-
var VERSION_FLAGS = /* @__PURE__ */ new Set(["--version", "-v", "-V"]);
|
|
3591
|
-
function hasFileRedirect(segment) {
|
|
3592
|
-
return segment.replace(/\d>&\d/g, "").includes(">");
|
|
3593
|
-
}
|
|
3594
|
-
function hasShellInjectionRisk(segment) {
|
|
3595
|
-
if (segment.includes("$(") || segment.includes("<(") || segment.includes("`")) {
|
|
3596
|
-
return true;
|
|
3597
|
-
}
|
|
3598
|
-
return segment.replace(/\d>&\d/g, "").includes("&");
|
|
3599
|
-
}
|
|
3600
|
-
function fastPathSegmentSafety(segment) {
|
|
3601
|
-
if (!segment) return true;
|
|
3602
|
-
if (hasFileRedirect(segment)) return false;
|
|
3603
|
-
if (hasShellInjectionRisk(segment)) return false;
|
|
3604
|
-
const [cmd0, ...rest] = segment.split(/\s+/);
|
|
3605
|
-
if (cmd0 === void 0) return true;
|
|
3606
|
-
if (VERSION_CHECK_BINARIES.has(cmd0) && rest.length === 1 && VERSION_FLAGS.has(rest[0])) {
|
|
3607
|
-
return true;
|
|
3608
|
-
}
|
|
3609
|
-
if (READ_ONLY_BINARIES.has(cmd0)) return true;
|
|
3610
|
-
if (READ_ONLY_NO_ARGS_BINARIES.has(cmd0)) {
|
|
3611
|
-
return rest.length === 0 ? true : void 0;
|
|
3612
|
-
}
|
|
3613
|
-
if (cmd0 === "git") {
|
|
3614
|
-
return GIT_READ_ONLY_SUBCOMMANDS.has(rest[0] ?? "") ? true : void 0;
|
|
3615
|
-
}
|
|
3616
|
-
return void 0;
|
|
3617
|
-
}
|
|
3618
|
-
function fastPathSafety(command) {
|
|
3619
|
-
const results = commandSegments(command).map(fastPathSegmentSafety);
|
|
3620
|
-
if (results.some((r) => r === false)) return false;
|
|
3621
|
-
if (results.every((r) => r === true)) return true;
|
|
3622
|
-
return void 0;
|
|
3623
|
-
}
|
|
3624
|
-
var CLASSIFIER_MODEL = "claude-haiku-4-5";
|
|
3625
|
-
var commandSafetySchema = z15.object({
|
|
3626
|
-
safe: z15.boolean(),
|
|
3627
|
-
reason: z15.string().describe("One short sentence explaining the verdict.")
|
|
3628
|
-
});
|
|
3629
|
-
function defaultCreateModel() {
|
|
3630
|
-
const token = getAuthToken();
|
|
3631
|
-
if (!token) {
|
|
3632
|
-
throw new Error("Not authenticated: no user token available");
|
|
3633
|
-
}
|
|
3634
|
-
return createAnthropic({
|
|
3635
|
-
apiKey: token,
|
|
3636
|
-
baseURL: PROXY_BASE_URL,
|
|
3637
|
-
fetch: proxyFetch
|
|
3638
|
-
});
|
|
3639
|
-
}
|
|
3640
|
-
function approvedCommandHistory(approvedCommands) {
|
|
3641
|
-
return Array.from(approvedCommands).map((entry) => {
|
|
3642
|
-
const sep2 = entry.indexOf("\0");
|
|
3643
|
-
return { cwd: entry.slice(0, sep2), command: entry.slice(sep2 + 1) };
|
|
3644
|
-
});
|
|
3533
|
+
return command.split(/&&|;|\|/).map((segment) => segment.trim().split(/\s+/)[0]).some((word) => word !== void 0 && EXPLORATORY_COMMANDS.has(word));
|
|
3645
3534
|
}
|
|
3646
|
-
async function
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
output: Output.object({ schema: commandSafetySchema }),
|
|
3653
|
-
prompt: [
|
|
3654
|
-
"A coding agent wants to run this shell command in a user's project without asking for approval first. The command may be in any programming language or ecosystem.",
|
|
3655
|
-
"Judge it SAFE only if it cannot modify, delete, move, rename, or install anything the project cares about, cannot push/publish/deploy/commit anything, and cannot make a network request that changes remote state.",
|
|
3656
|
-
"Three broad categories are safe, and most commands you will see fall into one of them:",
|
|
3657
|
-
"(1) Reporting or listing existing state, with no side effect \u2014 e.g. `git status`, `npm ls`, `npm outdated`, `pip show`, `pip freeze`, `cargo tree`, `docker ps`, `docker images`, `kubectl get pods`, `terraform state list`. This includes a read-only GET-style query to a remote registry or API that only fetches public metadata and changes nothing remote (e.g. `npm view <package>`, `pip index versions <package>`, `curl` with no -X/--request other than GET and no -d/--data) \u2014 safe because nothing changes, not because it stays local.",
|
|
3658
|
-
'(2) A "preview" or "check" mode that reports what a change WOULD do, or reports a problem, without making the change. This is a general pattern, not a fixed list \u2014 ANY command that takes a dry-run/check/plan/validate/diff/list-only flag is safe when that flag is present, regardless of whether the same flag would normally appear on a "safe" kind of command: `terraform plan`, `terraform validate`, `terraform fmt -check`, `black --check`, `prettier --check`, `isort --check`, `gofmt -l` (list-only), `stylelint` with no `--fix`, and equally `git push --dry-run`, `npm publish --dry-run`, `kubectl apply --dry-run=client` \u2014 these last three are otherwise-unsafe operations (push, publish, cluster changes) that the dry-run flag turns into a report. The corresponding command WITHOUT that flag (e.g. `terraform apply`, `black .`, `gofmt -w`, `git push`) is a different, unsafe command \u2014 the flag is what makes the difference, for any tool, not just the ones named here. This cuts both ways, so do not assume a bare invocation is the safe one: `cargo fmt`, `black`, `prettier`, `isort`, `rustfmt`, and `terraform fmt` all REWRITE FILES by default and need an explicit check/diff/dry-run flag to become safe, while `gofmt`, `eslint`, `stylelint`, `rubocop`, and `ruff check` default to a safe check-only mode and need an explicit fix/write flag to become unsafe \u2014 the same-looking bare command is safe for one group and unsafe for the other, so judge each by what its own flags actually say, not by resemblance to a tool you already judged.',
|
|
3659
|
-
"(3) Running the project's own tests, type checker, linter, or build/compile step (e.g. `npm run build`, `yarn build`, `vite build`, `webpack`, `tsc`, `cargo build`, `go build`, `mvn compile`), as long as it is not passed an autofix/write/update flag (e.g. --fix, -u, --write, rubocop -a) \u2014 this holds even though the point of a build step is to write compiled output to a build/dist/target directory inside the project (e.g. a target/, build/, dist/, or __pycache__ directory): writing that output does not make the command unsafe. It stops being safe the moment the command chain goes past building \u2014 a step that also deploys, publishes, uploads, or pushes the build (e.g. `next build && vercel deploy`, `npm run build && npm publish`) is unsafe for that later segment even though the build segment itself is fine; judge each chained segment on its own, same as elsewhere in this list.",
|
|
3660
|
-
"Deleting or removing anything is unsafe, even something in a cache or build directory (e.g. `rm -rf __pycache__`, `cargo clean`, `git clean`), and even if the command otherwise fits one of the three categories above. Installing, uninstalling, or upgrading a dependency, changing a database schema, or writing to a path outside the project is also unsafe. A command chained with && or ; is only safe if every part of the chain is safe on its own.",
|
|
3661
|
-
"One specific, narrow exception to all of the above: when the command literally invokes one of these runner programs BY NAME as the leading command \u2014 npx, bunx, pnpm dlx, yarn dlx, pipx run, uvx \u2014 it is unsafe regardless of what it then runs, even something that looks like a harmless test/lint/typecheck command (e.g. `npx tsc --noEmit`, `uvx ruff check .`), because that runner can fetch and execute a different, unreviewed version of a package from a registry each time. This exception is about that specific syntax, not a general doubt about whether a tool is installed: a plain `ruff check .`, `pytest`, or any other bare command name is NOT this exception merely because you cannot verify from the string alone that it is installed \u2014 judge those the same as any other already-installed project tool, per the categories above. If you are unsure, judge it unsafe.",
|
|
3662
|
-
...approvedHistory.length > 0 ? [
|
|
3663
|
-
"The user has already explicitly approved these exact commands earlier in this session (working directory in parentheses, then the command):",
|
|
3664
|
-
approvedHistory.map((h) => `- (${h.cwd}) ${h.command}`).join("\n"),
|
|
3665
|
-
"If the new command performs the same action and side-effect profile as one of these \u2014 differing only in a trivial way, such as a different file path, package name, or argument value that does not change what kind of action it is \u2014 judge it SAFE on that precedent, even if it would not otherwise fit categories (1)-(3) above. Do not stretch this to a command that merely shares a binary name, or superficially resembles one on the list while doing something riskier or of a different kind (e.g. an approved `rm build/tmp.log` does not license a new `rm -rf src/`, and an approved `git push origin feature-x` does not license `git push --force`)."
|
|
3666
|
-
] : [],
|
|
3667
|
-
`Command: ${command}`,
|
|
3668
|
-
`Working directory: ${cwd}`,
|
|
3669
|
-
`Stated purpose: ${explanation}`
|
|
3670
|
-
].join("\n")
|
|
3671
|
-
});
|
|
3672
|
-
if (!output.safe) {
|
|
3673
|
-
logger.info(
|
|
3674
|
-
{ command, reason: output.reason },
|
|
3675
|
-
"runShell: classifier judged command unsafe, requiring approval"
|
|
3676
|
-
);
|
|
3677
|
-
}
|
|
3678
|
-
return output.safe;
|
|
3679
|
-
} catch (err) {
|
|
3680
|
-
logger.warn(
|
|
3681
|
-
{ err, command },
|
|
3682
|
-
"runShell: command safety classifier failed, requiring approval"
|
|
3683
|
-
);
|
|
3684
|
-
return false;
|
|
3535
|
+
async function approveAndRun(ctx, command, cwd, explanation) {
|
|
3536
|
+
const decision = await ctx.shell.approve({ command, cwd, explanation });
|
|
3537
|
+
if (decision === "reject") {
|
|
3538
|
+
ctx.shell.executions.push({ command, cwd, approved: false });
|
|
3539
|
+
logger.info({ command }, "runShell: user rejected the command");
|
|
3540
|
+
return "The user rejected this command. Do not retry it. Propose a different command, or report the limitation via reportStatus.";
|
|
3685
3541
|
}
|
|
3686
|
-
}
|
|
3687
|
-
async function runAndRecord(ctx, command, cwd) {
|
|
3688
3542
|
useWizard.getState().pushNotice({ messages: [`Running: ${command}`] });
|
|
3689
3543
|
const env = await ctx.shell.env().catch((err) => {
|
|
3690
3544
|
logger.warn({ err, command }, "runShell: could not resolve command env");
|
|
@@ -3720,18 +3574,9 @@ async function runAndRecord(ctx, command, cwd) {
|
|
|
3720
3574
|
output: run2.output
|
|
3721
3575
|
};
|
|
3722
3576
|
}
|
|
3723
|
-
|
|
3724
|
-
const decision = await ctx.shell.approve({ command, cwd, explanation });
|
|
3725
|
-
if (decision === "reject") {
|
|
3726
|
-
ctx.shell.executions.push({ command, cwd, approved: false });
|
|
3727
|
-
logger.info({ command }, "runShell: user rejected the command");
|
|
3728
|
-
return "The user rejected this command. Do not retry it. Propose a different command, or report the limitation via reportStatus.";
|
|
3729
|
-
}
|
|
3730
|
-
return runAndRecord(ctx, command, cwd);
|
|
3731
|
-
}
|
|
3732
|
-
function runShellTool(ctx, createModel = defaultCreateModel) {
|
|
3577
|
+
function runShellTool(ctx) {
|
|
3733
3578
|
return tool8({
|
|
3734
|
-
description: "Run a shell command in the project. Use this for anything the project needs done in its own ecosystem: installing dependencies, running a script you wrote, running the project's lint/typecheck/test commands.
|
|
3579
|
+
description: "Run a shell command in the project. Use this for anything the project needs done in its own ecosystem: installing dependencies, running a script you wrote, running the project's lint/typecheck/test commands. To inspect the project, use listFiles or searchFiles instead of ls/find \u2014 this tool refuses those. The user sees and approves every command before it runs, so write a clear `explanation`. If the user rejects a command, do not retry it \u2014 propose a different approach.",
|
|
3735
3580
|
inputSchema: z15.object({
|
|
3736
3581
|
command: z15.string().describe(
|
|
3737
3582
|
"The command to run, exactly as it would be typed in a shell. Pipes, && and redirects are allowed."
|
|
@@ -3750,29 +3595,9 @@ function runShellTool(ctx, createModel = defaultCreateModel) {
|
|
|
3750
3595
|
const resolved2 = resolveInRoot(ctx, cwd ?? ".");
|
|
3751
3596
|
if (!resolved2.ok) return resolved2.error;
|
|
3752
3597
|
if (isExploratoryCommand(command)) {
|
|
3753
|
-
return "Refused: use listFiles or searchFiles to
|
|
3598
|
+
return "Refused: use listFiles or searchFiles to inspect the project instead of ls/find/tree.";
|
|
3754
3599
|
}
|
|
3755
3600
|
logger.info({ command, cwd: resolved2.target }, "called runShell tool");
|
|
3756
|
-
const fast = fastPathSafety(command);
|
|
3757
|
-
let isSafe = fast;
|
|
3758
|
-
if (isSafe === void 0) {
|
|
3759
|
-
const store = useWizard.getState();
|
|
3760
|
-
isSafe = store.isCommandApproved(command, resolved2.target);
|
|
3761
|
-
if (!isSafe) {
|
|
3762
|
-
isSafe = await classifyCommandSafety(
|
|
3763
|
-
createModel,
|
|
3764
|
-
command,
|
|
3765
|
-
resolved2.target,
|
|
3766
|
-
explanation,
|
|
3767
|
-
approvedCommandHistory(store.approvedCommands)
|
|
3768
|
-
);
|
|
3769
|
-
}
|
|
3770
|
-
}
|
|
3771
|
-
if (isSafe) {
|
|
3772
|
-
return serializePrompt(
|
|
3773
|
-
() => runAndRecord(ctx, command, resolved2.target)
|
|
3774
|
-
);
|
|
3775
|
-
}
|
|
3776
3601
|
return serializePrompt(
|
|
3777
3602
|
() => approveAndRun(ctx, command, resolved2.target, explanation)
|
|
3778
3603
|
);
|
|
@@ -3822,8 +3647,8 @@ function reviewScriptTool(ctx) {
|
|
|
3822
3647
|
}
|
|
3823
3648
|
|
|
3824
3649
|
// src/lib/tools/generateRecord.ts
|
|
3825
|
-
import { tool as tool10, generateText
|
|
3826
|
-
import { createAnthropic
|
|
3650
|
+
import { tool as tool10, generateText, Output, NoObjectGeneratedError } from "ai";
|
|
3651
|
+
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
3827
3652
|
import { nanoid as nanoid2 } from "nanoid";
|
|
3828
3653
|
import { mkdir as mkdir5, writeFile as writeFile6 } from "node:fs/promises";
|
|
3829
3654
|
import { dirname as dirname6 } from "node:path";
|
|
@@ -3833,18 +3658,18 @@ var RECORD_MODEL = "claude-haiku-4-5";
|
|
|
3833
3658
|
var MAX_RECORDS = 100;
|
|
3834
3659
|
var BATCH_SIZE = 10;
|
|
3835
3660
|
var MAX_BATCH_ATTEMPTS = 3;
|
|
3836
|
-
function
|
|
3661
|
+
function defaultCreateModel() {
|
|
3837
3662
|
const token = getAuthToken();
|
|
3838
3663
|
if (!token) {
|
|
3839
3664
|
throw new Error("Not authenticated: no user token available");
|
|
3840
3665
|
}
|
|
3841
|
-
return
|
|
3666
|
+
return createAnthropic({
|
|
3842
3667
|
apiKey: token,
|
|
3843
3668
|
baseURL: PROXY_BASE_URL,
|
|
3844
3669
|
fetch: proxyFetch
|
|
3845
3670
|
});
|
|
3846
3671
|
}
|
|
3847
|
-
function generateRecordTool(ctx, createModel =
|
|
3672
|
+
function generateRecordTool(ctx, createModel = defaultCreateModel) {
|
|
3848
3673
|
return tool10({
|
|
3849
3674
|
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.",
|
|
3850
3675
|
inputSchema: z17.object({
|
|
@@ -3865,9 +3690,9 @@ function generateRecordTool(ctx, createModel = defaultCreateModel2) {
|
|
|
3865
3690
|
let lastError;
|
|
3866
3691
|
for (let attempt = 1; attempt <= MAX_BATCH_ATTEMPTS; attempt++) {
|
|
3867
3692
|
try {
|
|
3868
|
-
const { output } = await
|
|
3693
|
+
const { output } = await generateText({
|
|
3869
3694
|
model: anthropic(RECORD_MODEL),
|
|
3870
|
-
output:
|
|
3695
|
+
output: Output.object({
|
|
3871
3696
|
schema: z17.object({
|
|
3872
3697
|
records: z17.array(recordSchema).length(batchCount)
|
|
3873
3698
|
})
|
|
@@ -4032,7 +3857,7 @@ async function runAgentAttempt(req, attempt) {
|
|
|
4032
3857
|
if (!token) {
|
|
4033
3858
|
throw new Error("Not authenticated: no user token available");
|
|
4034
3859
|
}
|
|
4035
|
-
const anthropic =
|
|
3860
|
+
const anthropic = createAnthropic2({
|
|
4036
3861
|
apiKey: token,
|
|
4037
3862
|
baseURL: PROXY_BASE_URL,
|
|
4038
3863
|
fetch: proxyFetch
|
|
@@ -4069,7 +3894,7 @@ async function runAgentAttempt(req, attempt) {
|
|
|
4069
3894
|
}
|
|
4070
3895
|
};
|
|
4071
3896
|
}),
|
|
4072
|
-
output:
|
|
3897
|
+
output: Output2.object({ schema: req.outputSchema }),
|
|
4073
3898
|
tools: createTools(toolContext, {
|
|
4074
3899
|
output: req.outputSchema,
|
|
4075
3900
|
tools: req.tools
|
|
@@ -4157,6 +3982,7 @@ var detectLanguage = () => runAgent({
|
|
|
4157
3982
|
"Return the exact version",
|
|
4158
3983
|
"Exclude things like CSS frameworks, build tools, or testing frameworks",
|
|
4159
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.",
|
|
4160
3986
|
'Use as few tools as possible, but do not guess. If you cant find the answer, say "unknown"',
|
|
4161
3987
|
"When done, call reportStatus"
|
|
4162
3988
|
],
|
|
@@ -4252,7 +4078,7 @@ async function runAnalysis(mode, extraInstructions = []) {
|
|
|
4252
4078
|
// package.json
|
|
4253
4079
|
var package_default = {
|
|
4254
4080
|
name: "@algolia/wizard",
|
|
4255
|
-
version: "0.
|
|
4081
|
+
version: "0.34.0-rc.125.243",
|
|
4256
4082
|
description: "Magically implement Algolia functionality in your codebase",
|
|
4257
4083
|
type: "module",
|
|
4258
4084
|
engines: {
|
|
@@ -4658,8 +4484,7 @@ ${formatCompletedSteps(ctx.completedSteps)}`,
|
|
|
4658
4484
|
|
|
4659
4485
|
// src/actions/implement.ts
|
|
4660
4486
|
import z29 from "zod";
|
|
4661
|
-
import {
|
|
4662
|
-
import { join as join12, relative as relative6 } from "node:path";
|
|
4487
|
+
import { relative as relative6 } from "node:path";
|
|
4663
4488
|
|
|
4664
4489
|
// src/lib/git.ts
|
|
4665
4490
|
import { execFile as execFile2 } from "node:child_process";
|
|
@@ -4717,41 +4542,43 @@ async function copyUploadIntoProject(repoRoot, ingestDir, sourcePath) {
|
|
|
4717
4542
|
}
|
|
4718
4543
|
return { ok: true, relPath };
|
|
4719
4544
|
}
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
}
|
|
4723
|
-
async function readEnvVar(repoRoot, name) {
|
|
4724
|
-
let content;
|
|
4545
|
+
var ENV_FILE_PRECEDENCE = [".env", ".env.local"];
|
|
4546
|
+
async function readEnvFileIfExists(path) {
|
|
4725
4547
|
try {
|
|
4726
|
-
|
|
4548
|
+
return await readFile8(path, "utf8");
|
|
4727
4549
|
} catch (err) {
|
|
4728
4550
|
if (err.code !== "ENOENT") throw err;
|
|
4729
4551
|
return void 0;
|
|
4730
4552
|
}
|
|
4731
|
-
const match = new RegExp(
|
|
4732
|
-
`^[ \\t]*(?:export[ \\t]+)?${name}[ \\t]*=[ \\t]*(.*)$`,
|
|
4733
|
-
"m"
|
|
4734
|
-
).exec(content);
|
|
4735
|
-
if (!match) return void 0;
|
|
4736
|
-
const value = match[1].trim().replace(/^(['"])(.*)\1$/, "$2").trim();
|
|
4737
|
-
if (!value || value.startsWith("<")) return void 0;
|
|
4738
|
-
return value;
|
|
4739
4553
|
}
|
|
4740
|
-
async function
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
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 };
|
|
4747
4560
|
}
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
const
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
|
|
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;
|
|
4755
4582
|
}
|
|
4756
4583
|
function normalizeFindingPaths(findings) {
|
|
4757
4584
|
return {
|
|
@@ -4842,6 +4669,7 @@ var resolveEnvVarPrefix = (frameworkName) => runAgent({
|
|
|
4842
4669
|
instructions: [
|
|
4843
4670
|
`The developer corrected the project's framework to "${frameworkName}".`,
|
|
4844
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.",
|
|
4845
4673
|
'Use as few tools as possible, but do not guess. If you cant find the answer, say "unknown"',
|
|
4846
4674
|
"When done, call reportStatus"
|
|
4847
4675
|
],
|
|
@@ -4878,6 +4706,7 @@ var MAX_IMPLEMENT_VERIFICATION_ATTEMPTS = 3;
|
|
|
4878
4706
|
var DEFAULT_IMPLEMENT_USE_CASES = ["ingestion", "search"];
|
|
4879
4707
|
var INGEST_DIR = ".algolia-wizard";
|
|
4880
4708
|
var JS_LANGUAGES = ["javascript", "typescript", "jsx", "tsx", "node"];
|
|
4709
|
+
var ENV_LOCAL_AWARE_PREFIXES = ["VITE_", "NEXT_PUBLIC_", "REACT_APP_"];
|
|
4881
4710
|
function lower(entries) {
|
|
4882
4711
|
return entries.map((entry) => entry.name.toLowerCase());
|
|
4883
4712
|
}
|
|
@@ -4948,7 +4777,7 @@ function algoliaClientDoc(input) {
|
|
|
4948
4777
|
function ingestionInstructions(input) {
|
|
4949
4778
|
return [
|
|
4950
4779
|
...input.confirmed && input.confirmed.length ? [
|
|
4951
|
-
`Create an ingestion script under "${input.ingestDir}/" at the repo root
|
|
4780
|
+
`Create an ingestion script under "${input.ingestDir}/" at the repo root.`,
|
|
4952
4781
|
`Ingest only the confirmed entity (name, source paths, attributes): ${JSON.stringify(input.confirmed)}.`,
|
|
4953
4782
|
`Ingesting writes to Algolia, so the script needs a write API key and App ID \u2014 read them from the ${API_KEY_VAR} and ${APP_ID_VAR} environment variables rather than hardcoding them. The wizard sets these when it runs the script.`,
|
|
4954
4783
|
`Read the index name from the ${INDEX_NAME_VAR} environment variable, which the wizard sets to "${input.targetIndex}". Never hardcode an index name or derive one from the project, file, or entity name \u2014 the write key only works for that exact index. Exit with an error if ${INDEX_NAME_VAR} is unset.`,
|
|
@@ -4977,17 +4806,19 @@ function searchInstructions(input) {
|
|
|
4977
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.`,
|
|
4978
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.`,
|
|
4979
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.",
|
|
4980
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.`,
|
|
4981
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.",
|
|
4982
4812
|
// The key is provisioned only after verification passes, and the wizard
|
|
4983
|
-
// reads
|
|
4984
|
-
// value there
|
|
4985
|
-
|
|
4986
|
-
|
|
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.
|
|
4987
4818
|
`Use exactly these env var names in the code: ${input.searchEnvVars.map(({ name }) => name).join(", ")}.`,
|
|
4988
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.",
|
|
4989
4820
|
"Match the styles of the application as closely as possible.",
|
|
4990
|
-
"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."
|
|
4991
4822
|
];
|
|
4992
4823
|
}
|
|
4993
4824
|
function verificationInstructions(input) {
|
|
@@ -5183,9 +5014,6 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES) {
|
|
|
5183
5014
|
const targetIndex = selected?.selection;
|
|
5184
5015
|
useWizard.getState().setTargetIndex(targetIndex ?? null);
|
|
5185
5016
|
await assertGitRepoWithHead(repoRoot);
|
|
5186
|
-
if (useCases.includes("ingestion")) {
|
|
5187
|
-
await mkdir7(join12(repoRoot, INGEST_DIR), { recursive: true });
|
|
5188
|
-
}
|
|
5189
5017
|
const normalized = normalizeFindingPaths(findings);
|
|
5190
5018
|
const confirmed2 = normalized.confirmedEntities;
|
|
5191
5019
|
const searchLocation = normalized.searchImplementationAnalysis;
|
|
@@ -5250,11 +5078,11 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES) {
|
|
|
5250
5078
|
const appIdVarName = publicAppIdVar(publicEnvVarPrefix);
|
|
5251
5079
|
const searchKeyVarName = publicSearchKeyVar(publicEnvVarPrefix);
|
|
5252
5080
|
summaries.push(
|
|
5253
|
-
`\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.`
|
|
5254
5082
|
);
|
|
5255
5083
|
logger.warn(
|
|
5256
5084
|
{ envAppId, appId },
|
|
5257
|
-
"implement:
|
|
5085
|
+
"implement: env files hold credentials for a different Algolia application; not reusing its search key"
|
|
5258
5086
|
);
|
|
5259
5087
|
}
|
|
5260
5088
|
}
|
|
@@ -5427,7 +5255,7 @@ ${detail}` : ""}`
|
|
|
5427
5255
|
searchKeyError = err.message;
|
|
5428
5256
|
logger.warn(
|
|
5429
5257
|
{ err: searchKeyError },
|
|
5430
|
-
"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"
|
|
5431
5259
|
);
|
|
5432
5260
|
}
|
|
5433
5261
|
}
|
|
@@ -5441,34 +5269,42 @@ ${detail}` : ""}`
|
|
|
5441
5269
|
(v) => !v.value.startsWith("<")
|
|
5442
5270
|
);
|
|
5443
5271
|
if (resolvedSearchEnvVars.length > 0) {
|
|
5272
|
+
const defaultEnvFile = ENV_LOCAL_AWARE_PREFIXES.includes(
|
|
5273
|
+
publicEnvVarPrefix
|
|
5274
|
+
) ? ".env.local" : ".env";
|
|
5444
5275
|
const written = await writeSearchEnvValues(
|
|
5445
5276
|
repoRoot,
|
|
5446
|
-
resolvedSearchEnvVars
|
|
5277
|
+
resolvedSearchEnvVars,
|
|
5278
|
+
defaultEnvFile
|
|
5447
5279
|
);
|
|
5448
|
-
|
|
5449
|
-
|
|
5450
|
-
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
|
|
5455
|
-
|
|
5456
|
-
|
|
5457
|
-
|
|
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
|
+
}
|
|
5458
5293
|
}
|
|
5294
|
+
const writtenNames = new Set(written.map((w) => w.name));
|
|
5459
5295
|
const stale = [];
|
|
5460
5296
|
for (const v of resolvedSearchEnvVars) {
|
|
5461
|
-
if (
|
|
5297
|
+
if (writtenNames.has(v.name)) continue;
|
|
5462
5298
|
const current = await readEnvVar(repoRoot, v.name);
|
|
5463
5299
|
if (current && current !== v.value) stale.push(v);
|
|
5464
5300
|
}
|
|
5465
5301
|
if (stale.length > 0 && !envAppIdMismatch) {
|
|
5466
5302
|
summaries.push(
|
|
5467
|
-
`\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.`
|
|
5468
5304
|
);
|
|
5469
5305
|
logger.warn(
|
|
5470
5306
|
{ vars: stale.map((v) => v.name) },
|
|
5471
|
-
"implement:
|
|
5307
|
+
"implement: env files hold different values for the resolved search credentials; not overwriting them"
|
|
5472
5308
|
);
|
|
5473
5309
|
}
|
|
5474
5310
|
}
|
|
@@ -5477,7 +5313,7 @@ ${detail}` : ""}`
|
|
|
5477
5313
|
);
|
|
5478
5314
|
if (unresolvedSearchEnvVars.length > 0) {
|
|
5479
5315
|
summaries.push(
|
|
5480
|
-
`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}` : "")
|
|
5481
5317
|
);
|
|
5482
5318
|
}
|
|
5483
5319
|
} else {
|
|
@@ -5811,7 +5647,7 @@ function parseCliArgs(argv) {
|
|
|
5811
5647
|
|
|
5812
5648
|
// src/lib/resetState.ts
|
|
5813
5649
|
import { readdir as readdir3, rm as rm2 } from "node:fs/promises";
|
|
5814
|
-
import { join as
|
|
5650
|
+
import { join as join12 } from "node:path";
|
|
5815
5651
|
var KEEP = ["wizard.log"];
|
|
5816
5652
|
async function resetProjectState() {
|
|
5817
5653
|
const dir = stateDir();
|
|
@@ -5825,7 +5661,7 @@ async function resetProjectState() {
|
|
|
5825
5661
|
const targets = entries.filter((name) => !KEEP.includes(name));
|
|
5826
5662
|
await Promise.all(
|
|
5827
5663
|
targets.map(
|
|
5828
|
-
(name) => rm2(
|
|
5664
|
+
(name) => rm2(join12(dir, name), { recursive: true, force: true })
|
|
5829
5665
|
)
|
|
5830
5666
|
);
|
|
5831
5667
|
return { dir, removed: targets };
|