@algolia/wizard 0.7.0 → 0.8.0-rc.49.42
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 +182 -57
- package/package.json +2 -2
package/dist/main.js
CHANGED
|
@@ -1568,7 +1568,8 @@ var configFile = () => join5(stateDir(), "config.json");
|
|
|
1568
1568
|
var DEFAULT_CONFIG = {
|
|
1569
1569
|
version: 1,
|
|
1570
1570
|
aiConsent: false,
|
|
1571
|
-
workflowsRun: []
|
|
1571
|
+
workflowsRun: [],
|
|
1572
|
+
searchApiKeys: {}
|
|
1572
1573
|
};
|
|
1573
1574
|
async function loadConfig() {
|
|
1574
1575
|
try {
|
|
@@ -1587,6 +1588,38 @@ async function recordWorkflowRun(workflowId, completedAt) {
|
|
|
1587
1588
|
config.workflowsRun.push({ workflowId, completedAt });
|
|
1588
1589
|
await saveConfig(config);
|
|
1589
1590
|
}
|
|
1591
|
+
function isStoredSearchKey(value) {
|
|
1592
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1593
|
+
const { appId, key } = value;
|
|
1594
|
+
return typeof appId === "string" && !!appId && typeof key === "string" && !!key;
|
|
1595
|
+
}
|
|
1596
|
+
function storedSearchKeys(config) {
|
|
1597
|
+
const stored = config.searchApiKeys;
|
|
1598
|
+
if (typeof stored !== "object" || stored === null || Array.isArray(stored)) {
|
|
1599
|
+
return {};
|
|
1600
|
+
}
|
|
1601
|
+
return stored;
|
|
1602
|
+
}
|
|
1603
|
+
async function getStoredSearchKey(index, appId) {
|
|
1604
|
+
const entry = storedSearchKeys(await loadConfig())[index];
|
|
1605
|
+
if (!isStoredSearchKey(entry) || entry.appId !== appId) return void 0;
|
|
1606
|
+
return entry.key;
|
|
1607
|
+
}
|
|
1608
|
+
async function storeSearchKey(index, appId, key) {
|
|
1609
|
+
const config = await loadConfig();
|
|
1610
|
+
config.searchApiKeys = {
|
|
1611
|
+
...storedSearchKeys(config),
|
|
1612
|
+
[index]: { appId, key }
|
|
1613
|
+
};
|
|
1614
|
+
await saveConfig(config);
|
|
1615
|
+
}
|
|
1616
|
+
async function forgetSearchKey(index) {
|
|
1617
|
+
const config = await loadConfig();
|
|
1618
|
+
const remaining = { ...storedSearchKeys(config) };
|
|
1619
|
+
delete remaining[index];
|
|
1620
|
+
config.searchApiKeys = remaining;
|
|
1621
|
+
await saveConfig(config);
|
|
1622
|
+
}
|
|
1590
1623
|
|
|
1591
1624
|
// src/core/orchestrator.ts
|
|
1592
1625
|
function defineStep(step) {
|
|
@@ -2701,7 +2734,7 @@ async function runAnalysis(mode, extraInstructions = []) {
|
|
|
2701
2734
|
// package.json
|
|
2702
2735
|
var package_default = {
|
|
2703
2736
|
name: "@algolia/wizard",
|
|
2704
|
-
version: "0.
|
|
2737
|
+
version: "0.8.0-rc.49.42",
|
|
2705
2738
|
description: "Magically implement Algolia functionality in your codebase",
|
|
2706
2739
|
type: "module",
|
|
2707
2740
|
engines: {
|
|
@@ -2749,7 +2782,7 @@ var package_default = {
|
|
|
2749
2782
|
dependencies: {
|
|
2750
2783
|
"@ai-sdk/anthropic": "^3.0.81",
|
|
2751
2784
|
"@ai-sdk/openai-compatible": "^2.0.47",
|
|
2752
|
-
"@algolia/cli": "^5.
|
|
2785
|
+
"@algolia/cli": "^5.15.0",
|
|
2753
2786
|
"@hono/node-server": "^2.0.10",
|
|
2754
2787
|
"@mishieck/ink-titled-box": "^0.4.2",
|
|
2755
2788
|
"@segment/analytics-node": "^3.1.0",
|
|
@@ -3307,6 +3340,23 @@ async function copyUploadIntoWorktree(repoRoot, worktreePath, ingestDir, sourceP
|
|
|
3307
3340
|
function hasEnvVar(content, name) {
|
|
3308
3341
|
return new RegExp(`^(\\s*(?:export\\s+)?${name})\\s*=`, "m").test(content);
|
|
3309
3342
|
}
|
|
3343
|
+
async function readEnvVar(worktreePath, name) {
|
|
3344
|
+
let content;
|
|
3345
|
+
try {
|
|
3346
|
+
content = await readFile8(join10(worktreePath, ".env"), "utf8");
|
|
3347
|
+
} catch (err) {
|
|
3348
|
+
if (err.code !== "ENOENT") throw err;
|
|
3349
|
+
return void 0;
|
|
3350
|
+
}
|
|
3351
|
+
const match = new RegExp(
|
|
3352
|
+
`^[ \\t]*(?:export[ \\t]+)?${name}[ \\t]*=[ \\t]*(.*)$`,
|
|
3353
|
+
"m"
|
|
3354
|
+
).exec(content);
|
|
3355
|
+
if (!match) return void 0;
|
|
3356
|
+
const value = match[1].trim().replace(/^(['"])(.*)\1$/, "$2").trim();
|
|
3357
|
+
if (!value || value.startsWith("<")) return void 0;
|
|
3358
|
+
return value;
|
|
3359
|
+
}
|
|
3310
3360
|
async function writeSearchEnvValues(worktreePath, vars) {
|
|
3311
3361
|
const target = join10(worktreePath, ".env");
|
|
3312
3362
|
let existing = "";
|
|
@@ -3381,50 +3431,72 @@ async function confirmDirtyWorkingTree(ctx, repoRoot) {
|
|
|
3381
3431
|
|
|
3382
3432
|
// src/lib/algoliaApiKey.ts
|
|
3383
3433
|
import { z as z23 } from "zod";
|
|
3384
|
-
var SAFE_ACLS = /* @__PURE__ */ new Set(["search", "browse", "listIndexes"]);
|
|
3385
|
-
var apiKeySchema = z23.object({
|
|
3386
|
-
value: z23.string().min(1),
|
|
3387
|
-
acl: z23.array(z23.string()).default([]),
|
|
3388
|
-
indexes: z23.array(z23.string()).default([])
|
|
3389
|
-
});
|
|
3390
|
-
var apiKeyListSchema = z23.object({
|
|
3391
|
-
items: z23.array(apiKeySchema).optional(),
|
|
3392
|
-
keys: z23.array(apiKeySchema).optional()
|
|
3393
|
-
}).transform((o) => o.items ?? o.keys ?? []);
|
|
3394
3434
|
var createdKeySchema = z23.object({
|
|
3395
3435
|
key: z23.string().min(1).optional(),
|
|
3396
3436
|
value: z23.string().min(1).optional()
|
|
3397
|
-
});
|
|
3398
|
-
function
|
|
3399
|
-
|
|
3400
|
-
}
|
|
3401
|
-
async function createSearchKey(index) {
|
|
3437
|
+
}).transform((o) => o.key ?? o.value);
|
|
3438
|
+
async function createSearchOnlyKey(index) {
|
|
3439
|
+
logger.info({ index }, "creating a search-only API key");
|
|
3402
3440
|
const stdout = await runAlgoliaCli([
|
|
3403
3441
|
"apikeys",
|
|
3404
3442
|
"create",
|
|
3443
|
+
"--acl",
|
|
3444
|
+
"search",
|
|
3405
3445
|
"--indices",
|
|
3406
3446
|
index,
|
|
3407
|
-
"--acl",
|
|
3408
|
-
"search,browse",
|
|
3409
3447
|
"--description",
|
|
3410
|
-
`
|
|
3448
|
+
`Algolia Wizard search-only key for ${index}`,
|
|
3411
3449
|
"-o",
|
|
3412
3450
|
"json"
|
|
3413
3451
|
]);
|
|
3414
|
-
|
|
3415
|
-
|
|
3452
|
+
let payload;
|
|
3453
|
+
try {
|
|
3454
|
+
payload = JSON.parse(stdout);
|
|
3455
|
+
} catch {
|
|
3456
|
+
throw new Error("apikeys create returned output that is not valid JSON");
|
|
3457
|
+
}
|
|
3458
|
+
const created = createdKeySchema.parse(payload);
|
|
3416
3459
|
if (!created) throw new Error("apikeys create returned no key value");
|
|
3417
3460
|
return created;
|
|
3418
3461
|
}
|
|
3419
|
-
async function
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
return
|
|
3462
|
+
async function apiKeyExists(key) {
|
|
3463
|
+
try {
|
|
3464
|
+
await runAlgoliaCli(["apikeys", "get", key, "-o", "json"]);
|
|
3465
|
+
return true;
|
|
3466
|
+
} catch (err) {
|
|
3467
|
+
return !/does not exist|not found|404/i.test(err.message);
|
|
3468
|
+
}
|
|
3469
|
+
}
|
|
3470
|
+
async function resolveSearchOnlyKey(index, appId, envKey) {
|
|
3471
|
+
if (envKey) {
|
|
3472
|
+
await recordSearchKey(index, appId, envKey);
|
|
3473
|
+
return { key: envKey, source: "env" };
|
|
3474
|
+
}
|
|
3475
|
+
const stored = await getStoredSearchKey(index, appId);
|
|
3476
|
+
if (stored) {
|
|
3477
|
+
if (await apiKeyExists(stored)) {
|
|
3478
|
+
logger.info({ index, appId }, "reusing the stored search-only API key");
|
|
3479
|
+
return { key: stored, source: "config" };
|
|
3480
|
+
}
|
|
3481
|
+
logger.warn(
|
|
3482
|
+
{ index, appId },
|
|
3483
|
+
"the stored search-only API key no longer exists; creating a replacement"
|
|
3484
|
+
);
|
|
3485
|
+
await forgetSearchKey(index);
|
|
3486
|
+
}
|
|
3487
|
+
const key = await createSearchOnlyKey(index);
|
|
3488
|
+
await recordSearchKey(index, appId, key);
|
|
3489
|
+
return { key, source: "created" };
|
|
3490
|
+
}
|
|
3491
|
+
async function recordSearchKey(index, appId, key) {
|
|
3492
|
+
try {
|
|
3493
|
+
await storeSearchKey(index, appId, key);
|
|
3494
|
+
} catch (err) {
|
|
3495
|
+
logger.warn(
|
|
3496
|
+
{ err: err.message, index },
|
|
3497
|
+
"could not record the search-only API key; a later run may create another"
|
|
3498
|
+
);
|
|
3425
3499
|
}
|
|
3426
|
-
logger.info({ index }, "no reusable search-only key found; creating one");
|
|
3427
|
-
return createSearchKey(index);
|
|
3428
3500
|
}
|
|
3429
3501
|
|
|
3430
3502
|
// src/lib/algoliaDocs.ts
|
|
@@ -3579,15 +3651,22 @@ function publicEnvPrefix(language) {
|
|
|
3579
3651
|
}
|
|
3580
3652
|
return "PUBLIC_";
|
|
3581
3653
|
}
|
|
3654
|
+
var APP_ID_VAR_SUFFIX = "ALGOLIA_APP_ID";
|
|
3655
|
+
var SEARCH_KEY_VAR_SUFFIX = "ALGOLIA_SEARCH_API_KEY";
|
|
3656
|
+
function appIdVar(language) {
|
|
3657
|
+
return `${publicEnvPrefix(language)}${APP_ID_VAR_SUFFIX}`;
|
|
3658
|
+
}
|
|
3659
|
+
function searchKeyVar(language) {
|
|
3660
|
+
return `${publicEnvPrefix(language)}${SEARCH_KEY_VAR_SUFFIX}`;
|
|
3661
|
+
}
|
|
3582
3662
|
function searchEnvVars(language, appId, searchKey) {
|
|
3583
|
-
const prefix = publicEnvPrefix(language);
|
|
3584
3663
|
return [
|
|
3585
3664
|
{
|
|
3586
|
-
name:
|
|
3665
|
+
name: appIdVar(language),
|
|
3587
3666
|
value: appId ?? "<your-algolia-app-id>"
|
|
3588
3667
|
},
|
|
3589
3668
|
{
|
|
3590
|
-
name:
|
|
3669
|
+
name: searchKeyVar(language),
|
|
3591
3670
|
value: searchKey ?? "<your-algolia-search-only-api-key>"
|
|
3592
3671
|
}
|
|
3593
3672
|
];
|
|
@@ -3649,15 +3728,13 @@ function searchInstructions(input) {
|
|
|
3649
3728
|
"Follow the Algolia JS SDK reference below for client setup and InstantSearch wiring; prefer it over prior knowledge:",
|
|
3650
3729
|
doc,
|
|
3651
3730
|
`Add the search UI at ${input.searchLocation ? `"${input.searchLocation}"` : "the best shared, always-rendered layout location (e.g. a header/nav component)"} so it is reachable across the app \u2014 at least a working SearchBox and Hits against the "${input.targetIndex}" index.`,
|
|
3652
|
-
|
|
3653
|
-
//
|
|
3654
|
-
//
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
// resolved app id / search-only key into ".env" under these exact names
|
|
3658
|
-
// right after this step, so a renamed prefix here would leave the code
|
|
3659
|
-
// reading a var the wizard never wrote.
|
|
3731
|
+
// The key is provisioned only after verification passes, so the agent never
|
|
3732
|
+
// sees one. It must also leave .env alone: the wizard reads that file to
|
|
3733
|
+
// decide whether a key already exists, and an agent-invented value there
|
|
3734
|
+
// would be reused as if it were real.
|
|
3735
|
+
`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.`,
|
|
3660
3736
|
`Use exactly these public env var names in the code: ${input.searchEnvVars.map(({ name }) => name).join(", ")}.`,
|
|
3737
|
+
"Read the App ID and a search-only API key from public env vars; never hardcode them. A search-only key is safe to expose client-side.",
|
|
3661
3738
|
'Add any Algolia/InstantSearch packages you import to package.json "dependencies" with a valid version range; the wizard installs them in the worktree after you finish.',
|
|
3662
3739
|
"The summary should be extremely concise; do not mention env var setup or manual testing steps \u2014 the wizard writes the resolved credentials to .env and reports that separately."
|
|
3663
3740
|
];
|
|
@@ -3807,17 +3884,8 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
|
|
|
3807
3884
|
const confirmed2 = normalized.confirmedEntities;
|
|
3808
3885
|
const searchLocation = normalized.searchImplementationAnalysis;
|
|
3809
3886
|
let appId;
|
|
3810
|
-
let searchKey;
|
|
3811
3887
|
if (useCases.includes("search")) {
|
|
3812
3888
|
appId = (await loadActiveProfile()).appId;
|
|
3813
|
-
try {
|
|
3814
|
-
searchKey = await resolveSearchOnlyKey(targetIndex);
|
|
3815
|
-
} catch (err) {
|
|
3816
|
-
logger.warn(
|
|
3817
|
-
{ err: err.message },
|
|
3818
|
-
"implement: could not resolve a search-only API key; the agent will scaffold a placeholder"
|
|
3819
|
-
);
|
|
3820
|
-
}
|
|
3821
3889
|
}
|
|
3822
3890
|
const worktree = existingWorktreePath ?? (await createWorktree(repoRoot)).path;
|
|
3823
3891
|
try {
|
|
@@ -3849,8 +3917,9 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
|
|
|
3849
3917
|
targetIndex,
|
|
3850
3918
|
language,
|
|
3851
3919
|
appId,
|
|
3852
|
-
|
|
3853
|
-
|
|
3920
|
+
// Names only: the search-only key is provisioned after verification, so
|
|
3921
|
+
// every value here is still a placeholder when the agent reads them.
|
|
3922
|
+
searchEnvVars: searchEnvVars(language, appId),
|
|
3854
3923
|
ingestDir: INGEST_DIR,
|
|
3855
3924
|
ingestionSource,
|
|
3856
3925
|
uploadFilePath,
|
|
@@ -3860,6 +3929,24 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
|
|
|
3860
3929
|
};
|
|
3861
3930
|
const summaries = [];
|
|
3862
3931
|
if (uploadWarning) summaries.push(uploadWarning);
|
|
3932
|
+
let envSearchKey;
|
|
3933
|
+
let envAppIdMismatch = false;
|
|
3934
|
+
if (useCases.includes("search") && appId) {
|
|
3935
|
+
const envAppId = await readEnvVar(worktree, appIdVar(language));
|
|
3936
|
+
if (envAppId === appId) {
|
|
3937
|
+
envSearchKey = await readEnvVar(worktree, searchKeyVar(language));
|
|
3938
|
+
} else if (envAppId) {
|
|
3939
|
+
envAppIdMismatch = true;
|
|
3940
|
+
summaries.push(
|
|
3941
|
+
`\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.`
|
|
3942
|
+
);
|
|
3943
|
+
logger.warn(
|
|
3944
|
+
{ envAppId, appId },
|
|
3945
|
+
"implement: .env holds credentials for a different Algolia application; not reusing its search key"
|
|
3946
|
+
);
|
|
3947
|
+
}
|
|
3948
|
+
}
|
|
3949
|
+
let finalSearchEnvVars = input.searchEnvVars;
|
|
3863
3950
|
let agentRuns = 0;
|
|
3864
3951
|
let ingestRuntime;
|
|
3865
3952
|
let ingestEntrypoint;
|
|
@@ -4052,7 +4139,29 @@ ${run2.output}` : status;
|
|
|
4052
4139
|
}
|
|
4053
4140
|
extraInstructions = verificationRetryInstructions(verification);
|
|
4054
4141
|
}
|
|
4055
|
-
|
|
4142
|
+
let searchKey;
|
|
4143
|
+
let searchKeyError;
|
|
4144
|
+
if (appId) {
|
|
4145
|
+
try {
|
|
4146
|
+
const resolved = await resolveSearchOnlyKey(
|
|
4147
|
+
targetIndex,
|
|
4148
|
+
appId,
|
|
4149
|
+
envSearchKey
|
|
4150
|
+
);
|
|
4151
|
+
searchKey = resolved.key;
|
|
4152
|
+
summaries.push(
|
|
4153
|
+
resolved.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}.`
|
|
4154
|
+
);
|
|
4155
|
+
} catch (err) {
|
|
4156
|
+
searchKeyError = err.message;
|
|
4157
|
+
logger.warn(
|
|
4158
|
+
{ err: searchKeyError },
|
|
4159
|
+
"implement: could not provision a search-only API key; the .env value stays a placeholder"
|
|
4160
|
+
);
|
|
4161
|
+
}
|
|
4162
|
+
}
|
|
4163
|
+
finalSearchEnvVars = searchEnvVars(language, appId, searchKey);
|
|
4164
|
+
const resolvedSearchEnvVars = finalSearchEnvVars.filter(
|
|
4056
4165
|
(v) => !v.value.startsWith("<")
|
|
4057
4166
|
);
|
|
4058
4167
|
if (resolvedSearchEnvVars.length > 0) {
|
|
@@ -4063,13 +4172,29 @@ ${run2.output}` : status;
|
|
|
4063
4172
|
if (written.length > 0) {
|
|
4064
4173
|
summaries.push(`Wrote ${written.join(", ")} to .env.`);
|
|
4065
4174
|
}
|
|
4175
|
+
const stale = [];
|
|
4176
|
+
for (const v of resolvedSearchEnvVars) {
|
|
4177
|
+
if (written.includes(v.name)) continue;
|
|
4178
|
+
const current = await readEnvVar(worktree, v.name);
|
|
4179
|
+
if (current && current !== v.value) stale.push(v);
|
|
4180
|
+
}
|
|
4181
|
+
if (stale.length > 0 && !envAppIdMismatch) {
|
|
4182
|
+
summaries.push(
|
|
4183
|
+
`\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.`
|
|
4184
|
+
);
|
|
4185
|
+
logger.warn(
|
|
4186
|
+
{ vars: stale.map((v) => v.name) },
|
|
4187
|
+
"implement: .env holds different values for the resolved search credentials; not overwriting them"
|
|
4188
|
+
);
|
|
4189
|
+
}
|
|
4066
4190
|
}
|
|
4067
|
-
const unresolvedSearchEnvVars =
|
|
4191
|
+
const unresolvedSearchEnvVars = finalSearchEnvVars.filter(
|
|
4068
4192
|
(v) => v.value.startsWith("<")
|
|
4069
4193
|
);
|
|
4070
4194
|
if (unresolvedSearchEnvVars.length > 0) {
|
|
4071
4195
|
summaries.push(
|
|
4072
|
-
`Could not resolve a value for ${unresolvedSearchEnvVars.map((v) => v.name).join(", ")} \u2014 fill it in manually in .env.`
|
|
4196
|
+
`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.
|
|
4197
|
+
(searchKeyError ? ` Reason: ${searchKeyError}` : "")
|
|
4073
4198
|
);
|
|
4074
4199
|
}
|
|
4075
4200
|
} else {
|
|
@@ -4101,7 +4226,7 @@ ${run2.output}` : status;
|
|
|
4101
4226
|
...ingestRecordCount != null ? { ingestRecordCount } : {},
|
|
4102
4227
|
...ingestDurationMs != null ? { ingestDurationMs } : {}
|
|
4103
4228
|
} : {},
|
|
4104
|
-
...useCases.includes("search") ? { searchEnvVars:
|
|
4229
|
+
...useCases.includes("search") ? { searchEnvVars: finalSearchEnvVars } : {}
|
|
4105
4230
|
};
|
|
4106
4231
|
} finally {
|
|
4107
4232
|
process.chdir(repoRoot);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@algolia/wizard",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0-rc.49.42",
|
|
4
4
|
"description": "Magically implement Algolia functionality in your codebase",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"@ai-sdk/anthropic": "^3.0.81",
|
|
50
50
|
"@ai-sdk/openai-compatible": "^2.0.47",
|
|
51
|
-
"@algolia/cli": "^5.
|
|
51
|
+
"@algolia/cli": "^5.15.0",
|
|
52
52
|
"@hono/node-server": "^2.0.10",
|
|
53
53
|
"@mishieck/ink-titled-box": "^0.4.2",
|
|
54
54
|
"@segment/analytics-node": "^3.1.0",
|