@insforge/cli 0.1.76 → 0.1.79
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 +14 -0
- package/dist/index.js +917 -92
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { readFileSync as
|
|
5
|
-
import { join as
|
|
4
|
+
import { readFileSync as readFileSync14 } from "fs";
|
|
5
|
+
import { join as join18, dirname as dirname3 } from "path";
|
|
6
6
|
import { fileURLToPath } from "url";
|
|
7
7
|
import { Command } from "commander";
|
|
8
|
-
import * as
|
|
8
|
+
import * as clack18 from "@clack/prompts";
|
|
9
9
|
|
|
10
10
|
// src/lib/prompts.ts
|
|
11
11
|
import * as readline from "readline";
|
|
@@ -41,8 +41,8 @@ var LineReader = class {
|
|
|
41
41
|
this.output.write(prompt);
|
|
42
42
|
if (this.queue.length > 0) return this.queue.shift();
|
|
43
43
|
if (this.closed) return null;
|
|
44
|
-
return new Promise((
|
|
45
|
-
this.waiter =
|
|
44
|
+
return new Promise((resolve9) => {
|
|
45
|
+
this.waiter = resolve9;
|
|
46
46
|
});
|
|
47
47
|
}
|
|
48
48
|
close() {
|
|
@@ -424,8 +424,8 @@ function startCallbackServer() {
|
|
|
424
424
|
return new Promise((resolveServer) => {
|
|
425
425
|
let resolveResult;
|
|
426
426
|
let rejectResult;
|
|
427
|
-
const resultPromise = new Promise((
|
|
428
|
-
resolveResult =
|
|
427
|
+
const resultPromise = new Promise((resolve9, reject) => {
|
|
428
|
+
resolveResult = resolve9;
|
|
429
429
|
rejectResult = reject;
|
|
430
430
|
});
|
|
431
431
|
const server = createServer((req, res) => {
|
|
@@ -1329,36 +1329,36 @@ function registerBranchCreateCommand(branch) {
|
|
|
1329
1329
|
throw new CLIError(`Invalid --mode: ${opts.mode} (must be "full" or "schema-only")`);
|
|
1330
1330
|
}
|
|
1331
1331
|
const mode = opts.mode;
|
|
1332
|
-
const
|
|
1332
|
+
const spinner11 = !json ? clack5.spinner() : null;
|
|
1333
1333
|
let ready;
|
|
1334
1334
|
let provisioned = false;
|
|
1335
1335
|
try {
|
|
1336
|
-
|
|
1336
|
+
spinner11?.start(`Creating branch '${name}'...`);
|
|
1337
1337
|
const created = await createBranchApi(project.project_id, { mode, name }, apiUrl);
|
|
1338
1338
|
captureEvent(project.project_id, "cli_branch_create", {
|
|
1339
1339
|
mode,
|
|
1340
1340
|
parent_project_id: project.project_id
|
|
1341
1341
|
});
|
|
1342
|
-
|
|
1343
|
-
ready = await pollUntilReady(created.id, apiUrl,
|
|
1342
|
+
spinner11?.message(`Branch '${name}' created (appkey: ${created.appkey}). Provisioning...`);
|
|
1343
|
+
ready = await pollUntilReady(created.id, apiUrl, spinner11);
|
|
1344
1344
|
provisioned = ready.branch_state === "ready";
|
|
1345
1345
|
if (provisioned && opts.switch) {
|
|
1346
|
-
|
|
1346
|
+
spinner11?.message("Branch ready. Switching context...");
|
|
1347
1347
|
await runBranchSwitch({ name, apiUrl, json, silent: true });
|
|
1348
|
-
|
|
1348
|
+
spinner11?.stop(`Branch '${name}' is ready and active`);
|
|
1349
1349
|
} else if (provisioned) {
|
|
1350
|
-
|
|
1350
|
+
spinner11?.stop(`Branch '${name}' is ready`);
|
|
1351
1351
|
} else {
|
|
1352
|
-
|
|
1352
|
+
spinner11?.stop(`Branch '${name}' is in '${ready.branch_state}' state`);
|
|
1353
1353
|
}
|
|
1354
1354
|
} catch (err) {
|
|
1355
1355
|
if (provisioned) {
|
|
1356
|
-
|
|
1356
|
+
spinner11?.stop(
|
|
1357
1357
|
`Branch '${name}' is ready, but switching context failed \u2014 run \`insforge branch switch ${name}\` to retry`,
|
|
1358
1358
|
1
|
|
1359
1359
|
);
|
|
1360
1360
|
} else {
|
|
1361
|
-
|
|
1361
|
+
spinner11?.stop(`Branch '${name}' creation failed`, 1);
|
|
1362
1362
|
}
|
|
1363
1363
|
throw err;
|
|
1364
1364
|
}
|
|
@@ -1382,7 +1382,7 @@ function registerBranchCreateCommand(branch) {
|
|
|
1382
1382
|
}
|
|
1383
1383
|
});
|
|
1384
1384
|
}
|
|
1385
|
-
async function pollUntilReady(branchId, apiUrl,
|
|
1385
|
+
async function pollUntilReady(branchId, apiUrl, spinner11) {
|
|
1386
1386
|
const start = Date.now();
|
|
1387
1387
|
let lastState = "";
|
|
1388
1388
|
while (Date.now() - start < POLL_TIMEOUT_MS) {
|
|
@@ -1391,8 +1391,8 @@ async function pollUntilReady(branchId, apiUrl, spinner10) {
|
|
|
1391
1391
|
if (branch2.branch_state === "deleted" || branch2.branch_state === "conflicted") {
|
|
1392
1392
|
throw new CLIError(`Branch creation failed (state: ${branch2.branch_state})`);
|
|
1393
1393
|
}
|
|
1394
|
-
if (
|
|
1395
|
-
|
|
1394
|
+
if (spinner11 && branch2.branch_state !== lastState) {
|
|
1395
|
+
spinner11.message(`Provisioning branch (state: ${branch2.branch_state})...`);
|
|
1396
1396
|
lastState = branch2.branch_state;
|
|
1397
1397
|
}
|
|
1398
1398
|
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
@@ -1946,6 +1946,9 @@ ${err.nextActions}`;
|
|
|
1946
1946
|
if (res.status === 404 && isRouteLevel404 && path6 === "/api/database/migrations") {
|
|
1947
1947
|
message = "Database migrations are not available on this backend.\nSelf-hosted: upgrade your InsForge instance. Cloud: contact your InsForge admin about database migration support.";
|
|
1948
1948
|
}
|
|
1949
|
+
if (res.status === 404 && isRouteLevel404 && path6.startsWith("/api/ai")) {
|
|
1950
|
+
message = "AI Model Gateway setup is not available on this backend.\nUpgrade your InsForge project to a version with Model Gateway support, or keep using the legacy @insforge/sdk AI modules for projects that still rely on the older AI API surface.";
|
|
1951
|
+
}
|
|
1949
1952
|
throw new CLIError(message);
|
|
1950
1953
|
}
|
|
1951
1954
|
return res;
|
|
@@ -2346,11 +2349,11 @@ async function collectDeploymentFiles(sourceDir) {
|
|
|
2346
2349
|
return files;
|
|
2347
2350
|
}
|
|
2348
2351
|
async function createZipBuffer(sourceDir) {
|
|
2349
|
-
return new Promise((
|
|
2352
|
+
return new Promise((resolve9, reject) => {
|
|
2350
2353
|
const archive = archiver("zip", { zlib: { level: 9 } });
|
|
2351
2354
|
const chunks = [];
|
|
2352
2355
|
archive.on("data", (chunk) => chunks.push(chunk));
|
|
2353
|
-
archive.on("end", () =>
|
|
2356
|
+
archive.on("end", () => resolve9(Buffer.concat(chunks)));
|
|
2354
2357
|
archive.on("error", (err) => reject(err));
|
|
2355
2358
|
archive.directory(sourceDir, false, (entry) => {
|
|
2356
2359
|
if (shouldExclude(entry.name)) return false;
|
|
@@ -2427,12 +2430,12 @@ async function startDirectDeployment(deploymentId, startBody) {
|
|
|
2427
2430
|
});
|
|
2428
2431
|
await response.json();
|
|
2429
2432
|
}
|
|
2430
|
-
async function pollDeployment(deploymentId,
|
|
2431
|
-
|
|
2433
|
+
async function pollDeployment(deploymentId, spinner11, syncBeforeRead) {
|
|
2434
|
+
spinner11?.message("Building and deploying...");
|
|
2432
2435
|
const startTime = Date.now();
|
|
2433
2436
|
let deployment = null;
|
|
2434
2437
|
while (Date.now() - startTime < POLL_TIMEOUT_MS3) {
|
|
2435
|
-
await new Promise((
|
|
2438
|
+
await new Promise((resolve9) => setTimeout(resolve9, POLL_INTERVAL_MS3));
|
|
2436
2439
|
try {
|
|
2437
2440
|
if (syncBeforeRead) {
|
|
2438
2441
|
await ossFetch(`/api/deployments/${deploymentId}/sync`, { method: "POST" });
|
|
@@ -2444,13 +2447,13 @@ async function pollDeployment(deploymentId, spinner10, syncBeforeRead) {
|
|
|
2444
2447
|
break;
|
|
2445
2448
|
}
|
|
2446
2449
|
if (status === "ERROR" || status === "CANCELED") {
|
|
2447
|
-
|
|
2450
|
+
spinner11?.stop("Deployment failed");
|
|
2448
2451
|
throw new CLIError(
|
|
2449
2452
|
getDeploymentError(deployment.metadata) ?? `Deployment failed with status: ${deployment.status}`
|
|
2450
2453
|
);
|
|
2451
2454
|
}
|
|
2452
2455
|
const elapsed = Math.round((Date.now() - startTime) / 1e3);
|
|
2453
|
-
|
|
2456
|
+
spinner11?.message(`Building and deploying... (${elapsed}s, status: ${deployment.status})`);
|
|
2454
2457
|
} catch (err) {
|
|
2455
2458
|
if (err instanceof CLIError) throw err;
|
|
2456
2459
|
}
|
|
@@ -2460,20 +2463,20 @@ async function pollDeployment(deploymentId, spinner10, syncBeforeRead) {
|
|
|
2460
2463
|
return { deploymentId, deployment, isReady, liveUrl };
|
|
2461
2464
|
}
|
|
2462
2465
|
async function deployProjectDirect(opts, config) {
|
|
2463
|
-
const { sourceDir, startBody = {}, spinner:
|
|
2464
|
-
|
|
2466
|
+
const { sourceDir, startBody = {}, spinner: spinner11 } = opts;
|
|
2467
|
+
spinner11?.start("Scanning source files...");
|
|
2465
2468
|
const localFiles = await collectDeploymentFiles(sourceDir);
|
|
2466
2469
|
if (localFiles.length === 0) {
|
|
2467
2470
|
throw new CLIError("No deployable files found in the source directory.");
|
|
2468
2471
|
}
|
|
2469
|
-
|
|
2472
|
+
spinner11?.message("Creating deployment...");
|
|
2470
2473
|
const createResult = await createDirectDeploymentSession(
|
|
2471
2474
|
config,
|
|
2472
2475
|
localFiles.map(({ path: relativePath, sha, size }) => ({ path: relativePath, sha, size }))
|
|
2473
2476
|
);
|
|
2474
2477
|
const localFileByPath = new Map(localFiles.map((file) => [file.path, file]));
|
|
2475
2478
|
const pendingFiles = createResult.files.filter((file) => !file.uploadedAt);
|
|
2476
|
-
|
|
2479
|
+
spinner11?.message(`Uploading ${pendingFiles.length} file${pendingFiles.length === 1 ? "" : "s"}...`);
|
|
2477
2480
|
await runWithConcurrency(pendingFiles, DIRECT_UPLOAD_CONCURRENCY, async (manifestFile) => {
|
|
2478
2481
|
const localFile = localFileByPath.get(manifestFile.path);
|
|
2479
2482
|
if (!localFile) {
|
|
@@ -2484,18 +2487,18 @@ async function deployProjectDirect(opts, config) {
|
|
|
2484
2487
|
}
|
|
2485
2488
|
await uploadDirectDeploymentFile(createResult.id, manifestFile, localFile);
|
|
2486
2489
|
});
|
|
2487
|
-
|
|
2490
|
+
spinner11?.message("Starting deployment...");
|
|
2488
2491
|
await startDirectDeployment(createResult.id, startBody);
|
|
2489
|
-
return await pollDeployment(createResult.id,
|
|
2492
|
+
return await pollDeployment(createResult.id, spinner11, !isInsforgeCloudOssHost(config.oss_host));
|
|
2490
2493
|
}
|
|
2491
2494
|
async function deployProjectLegacy(opts) {
|
|
2492
|
-
const { sourceDir, startBody = {}, spinner:
|
|
2493
|
-
|
|
2495
|
+
const { sourceDir, startBody = {}, spinner: spinner11 } = opts;
|
|
2496
|
+
spinner11?.message("Creating deployment...");
|
|
2494
2497
|
const createRes = await ossFetch("/api/deployments", { method: "POST" });
|
|
2495
2498
|
const { id: deploymentId, uploadUrl, uploadFields } = await createRes.json();
|
|
2496
|
-
|
|
2499
|
+
spinner11?.message("Compressing source files...");
|
|
2497
2500
|
const zipBuffer = await createZipBuffer(sourceDir);
|
|
2498
|
-
|
|
2501
|
+
spinner11?.message("Uploading...");
|
|
2499
2502
|
const formData = new FormData();
|
|
2500
2503
|
for (const [key, value] of Object.entries(uploadFields)) {
|
|
2501
2504
|
formData.append(key, value);
|
|
@@ -2506,13 +2509,13 @@ async function deployProjectLegacy(opts) {
|
|
|
2506
2509
|
const uploadErr = await uploadRes.text();
|
|
2507
2510
|
throw new CLIError(`Failed to upload: ${uploadErr}`);
|
|
2508
2511
|
}
|
|
2509
|
-
|
|
2512
|
+
spinner11?.message("Starting deployment...");
|
|
2510
2513
|
const startRes = await ossFetch(`/api/deployments/${deploymentId}/start`, {
|
|
2511
2514
|
method: "POST",
|
|
2512
2515
|
body: JSON.stringify(startBody)
|
|
2513
2516
|
});
|
|
2514
2517
|
await startRes.json();
|
|
2515
|
-
return await pollDeployment(deploymentId,
|
|
2518
|
+
return await pollDeployment(deploymentId, spinner11, false);
|
|
2516
2519
|
}
|
|
2517
2520
|
async function deployProject(opts) {
|
|
2518
2521
|
const config = getProjectConfig();
|
|
@@ -2547,7 +2550,7 @@ function registerDeploymentsDeployCommand(deploymentsCmd2) {
|
|
|
2547
2550
|
`"${dirName}" is an excluded directory and cannot be used as a deploy source. Please specify your project root or output directory instead.`
|
|
2548
2551
|
);
|
|
2549
2552
|
}
|
|
2550
|
-
const
|
|
2553
|
+
const spinner11 = !json ? clack11.spinner() : null;
|
|
2551
2554
|
const startBody = {};
|
|
2552
2555
|
if (opts.env) {
|
|
2553
2556
|
try {
|
|
@@ -2573,9 +2576,9 @@ function registerDeploymentsDeployCommand(deploymentsCmd2) {
|
|
|
2573
2576
|
throw new CLIError("Invalid --meta JSON.");
|
|
2574
2577
|
}
|
|
2575
2578
|
}
|
|
2576
|
-
const result = await deployProject({ sourceDir, startBody, spinner:
|
|
2579
|
+
const result = await deployProject({ sourceDir, startBody, spinner: spinner11 });
|
|
2577
2580
|
if (result.isReady) {
|
|
2578
|
-
|
|
2581
|
+
spinner11?.stop("Deployment complete");
|
|
2579
2582
|
if (json) {
|
|
2580
2583
|
outputJson(result.deployment);
|
|
2581
2584
|
} else {
|
|
@@ -2585,7 +2588,7 @@ function registerDeploymentsDeployCommand(deploymentsCmd2) {
|
|
|
2585
2588
|
clack11.log.info(`Deployment ID: ${result.deploymentId}`);
|
|
2586
2589
|
}
|
|
2587
2590
|
} else {
|
|
2588
|
-
|
|
2591
|
+
spinner11?.stop("Deployment is still building");
|
|
2589
2592
|
if (json) {
|
|
2590
2593
|
outputJson({
|
|
2591
2594
|
id: result.deploymentId,
|
|
@@ -3151,13 +3154,13 @@ async function downloadGitHubTemplate(templateName, projectConfig, json) {
|
|
|
3151
3154
|
// src/commands/projects/link.ts
|
|
3152
3155
|
var execAsync3 = promisify4(exec3);
|
|
3153
3156
|
async function runNpmInstall(startMessage = "Installing dependencies...") {
|
|
3154
|
-
const
|
|
3155
|
-
|
|
3157
|
+
const spinner11 = clack13.spinner();
|
|
3158
|
+
spinner11.start(startMessage);
|
|
3156
3159
|
try {
|
|
3157
3160
|
await execAsync3("npm install", { cwd: process.cwd(), maxBuffer: 10 * 1024 * 1024 });
|
|
3158
|
-
|
|
3161
|
+
spinner11.stop("Dependencies installed");
|
|
3159
3162
|
} catch (err) {
|
|
3160
|
-
|
|
3163
|
+
spinner11.stop("Failed to install dependencies");
|
|
3161
3164
|
clack13.log.warn(`npm install failed: ${err.message}`);
|
|
3162
3165
|
clack13.log.info("Run `npm install` manually to install dependencies.");
|
|
3163
3166
|
}
|
|
@@ -3171,13 +3174,13 @@ async function runNpmSetupIfPresent() {
|
|
|
3171
3174
|
} catch {
|
|
3172
3175
|
}
|
|
3173
3176
|
if (!hasSetup) return;
|
|
3174
|
-
const
|
|
3175
|
-
|
|
3177
|
+
const spinner11 = clack13.spinner();
|
|
3178
|
+
spinner11.start("Running setup (schema + migrations)...");
|
|
3176
3179
|
try {
|
|
3177
3180
|
await execAsync3("npm run setup", { cwd: process.cwd(), maxBuffer: 20 * 1024 * 1024 });
|
|
3178
|
-
|
|
3181
|
+
spinner11.stop("Setup complete");
|
|
3179
3182
|
} catch (err) {
|
|
3180
|
-
|
|
3183
|
+
spinner11.stop("Setup failed");
|
|
3181
3184
|
clack13.log.warn(`npm run setup failed: ${err.message.split("\n")[0]}`);
|
|
3182
3185
|
clack13.log.info("Inspect the error, fix DATABASE_URL or network access, then run `npm run setup` manually.");
|
|
3183
3186
|
}
|
|
@@ -3203,7 +3206,7 @@ function registerProjectLinkCommand(program2) {
|
|
|
3203
3206
|
outputJson({ success: true, skills_only: true });
|
|
3204
3207
|
} else {
|
|
3205
3208
|
clack13.note(
|
|
3206
|
-
`Open your coding agent (Claude Code, Codex, Cursor, etc.) and ask it to build something. It will walk you through provisioning an InsForge project when needed.`,
|
|
3209
|
+
`Open your coding agent (Claude Code, Codex, Cursor, etc.) and ask it to build something. It will walk you through provisioning an InsForge project when needed. If you're not signed in yet, your browser will open for sign-in at that point.`,
|
|
3207
3210
|
"What's next"
|
|
3208
3211
|
);
|
|
3209
3212
|
}
|
|
@@ -6017,7 +6020,7 @@ primary_region = "${opts.region}"
|
|
|
6017
6020
|
};
|
|
6018
6021
|
}
|
|
6019
6022
|
function flyctlBuildAndPush(opts) {
|
|
6020
|
-
return new Promise((
|
|
6023
|
+
return new Promise((resolve9, reject) => {
|
|
6021
6024
|
const cleanupStub = ensureFlyTomlStub({
|
|
6022
6025
|
dir: opts.dir,
|
|
6023
6026
|
appId: opts.appId,
|
|
@@ -6073,7 +6076,7 @@ function flyctlBuildAndPush(opts) {
|
|
|
6073
6076
|
)
|
|
6074
6077
|
);
|
|
6075
6078
|
}
|
|
6076
|
-
|
|
6079
|
+
resolve9({ imageRef: `registry.fly.io/${opts.appId}@${m[1]}` });
|
|
6077
6080
|
});
|
|
6078
6081
|
});
|
|
6079
6082
|
}
|
|
@@ -6958,7 +6961,7 @@ function registerDiagnoseCommands(diagnoseCmd2) {
|
|
|
6958
6961
|
const s = !json ? clack15.spinner() : null;
|
|
6959
6962
|
s?.start("Collecting diagnostic data...");
|
|
6960
6963
|
const data2 = await collectDiagnosticData(projectId, ossMode, apiUrl);
|
|
6961
|
-
const cliVersion = "0.1.
|
|
6964
|
+
const cliVersion = "0.1.79";
|
|
6962
6965
|
s?.stop("Data collected");
|
|
6963
6966
|
if (!json) {
|
|
6964
6967
|
console.log(`
|
|
@@ -8472,14 +8475,14 @@ async function startPosthogCliFlow(projectId, jwt, apiUrl) {
|
|
|
8472
8475
|
throw new CLIError("PostHog cli-start returned an unexpected response shape.");
|
|
8473
8476
|
}
|
|
8474
8477
|
function sleep(ms, signal) {
|
|
8475
|
-
return new Promise((
|
|
8478
|
+
return new Promise((resolve9, reject) => {
|
|
8476
8479
|
if (signal?.aborted) {
|
|
8477
8480
|
reject(new CLIError("Connection wait cancelled."));
|
|
8478
8481
|
return;
|
|
8479
8482
|
}
|
|
8480
8483
|
const timer = setTimeout(() => {
|
|
8481
8484
|
signal?.removeEventListener("abort", onAbort);
|
|
8482
|
-
|
|
8485
|
+
resolve9();
|
|
8483
8486
|
}, ms);
|
|
8484
8487
|
const onAbort = () => {
|
|
8485
8488
|
clearTimeout(timer);
|
|
@@ -8782,8 +8785,8 @@ async function runConnectFlow(projectId, token, authorizeUrl, opts) {
|
|
|
8782
8785
|
} catch {
|
|
8783
8786
|
}
|
|
8784
8787
|
}
|
|
8785
|
-
const
|
|
8786
|
-
|
|
8788
|
+
const spinner11 = !opts.json && isInteractive ? clack16.spinner() : null;
|
|
8789
|
+
spinner11?.start("Waiting for connection... (timeout: 15 minutes)");
|
|
8787
8790
|
try {
|
|
8788
8791
|
const conn = await pollPosthogConnection(
|
|
8789
8792
|
projectId,
|
|
@@ -8793,20 +8796,20 @@ async function runConnectFlow(projectId, token, authorizeUrl, opts) {
|
|
|
8793
8796
|
timeoutMs: POLL_TIMEOUT_MS4,
|
|
8794
8797
|
maxTransientRetries: MAX_TRANSIENT_RETRIES,
|
|
8795
8798
|
onTick: (elapsed) => {
|
|
8796
|
-
if (
|
|
8799
|
+
if (spinner11) {
|
|
8797
8800
|
const secs = Math.floor(elapsed / 1e3);
|
|
8798
8801
|
const mins = Math.floor(secs / 60);
|
|
8799
8802
|
const remaining = `${mins}m ${secs % 60}s elapsed`;
|
|
8800
|
-
|
|
8803
|
+
spinner11.message(`Waiting for connection... (${remaining})`);
|
|
8801
8804
|
}
|
|
8802
8805
|
}
|
|
8803
8806
|
},
|
|
8804
8807
|
opts.apiUrl
|
|
8805
8808
|
);
|
|
8806
|
-
|
|
8809
|
+
spinner11?.stop("Connection received from PostHog.");
|
|
8807
8810
|
return conn;
|
|
8808
8811
|
} catch (err) {
|
|
8809
|
-
|
|
8812
|
+
spinner11?.stop("Connection wait failed.");
|
|
8810
8813
|
throw err;
|
|
8811
8814
|
}
|
|
8812
8815
|
}
|
|
@@ -8824,14 +8827,14 @@ function resolveFramework(opts) {
|
|
|
8824
8827
|
}
|
|
8825
8828
|
async function installSdk(pm, cwd, opts) {
|
|
8826
8829
|
const cmd = installCommand(pm, "posthog-js");
|
|
8827
|
-
const
|
|
8828
|
-
|
|
8830
|
+
const spinner11 = !opts.json && isInteractive ? clack16.spinner() : null;
|
|
8831
|
+
spinner11?.start(`Installing posthog-js (${cmd})...`);
|
|
8829
8832
|
try {
|
|
8830
8833
|
await runInstall(pm, "posthog-js", cwd);
|
|
8831
|
-
|
|
8834
|
+
spinner11?.stop("Installed posthog-js.");
|
|
8832
8835
|
return true;
|
|
8833
8836
|
} catch (err) {
|
|
8834
|
-
|
|
8837
|
+
spinner11?.stop("Install failed.");
|
|
8835
8838
|
if (!opts.json) {
|
|
8836
8839
|
clack16.log.warn(
|
|
8837
8840
|
`Could not run \`${cmd}\` automatically: ${err.message}
|
|
@@ -9060,6 +9063,75 @@ import pc4 from "picocolors";
|
|
|
9060
9063
|
// src/lib/config-toml.ts
|
|
9061
9064
|
import * as smolToml from "smol-toml";
|
|
9062
9065
|
|
|
9066
|
+
// src/lib/config-secrets.ts
|
|
9067
|
+
var ENV_REF_PATTERN = /^env\(([A-Z_][A-Z0-9_]*)\)$/;
|
|
9068
|
+
function parseEnvRef(value) {
|
|
9069
|
+
const match = value.match(ENV_REF_PATTERN);
|
|
9070
|
+
return match ? match[1] : null;
|
|
9071
|
+
}
|
|
9072
|
+
function validateSensitiveString(path6, value, suggestedSecretName) {
|
|
9073
|
+
if (typeof value !== "string") {
|
|
9074
|
+
throw new ConfigValidationError(path6, "must be a string");
|
|
9075
|
+
}
|
|
9076
|
+
if (parseEnvRef(value) !== null) {
|
|
9077
|
+
return value;
|
|
9078
|
+
}
|
|
9079
|
+
throw new ConfigValidationError(
|
|
9080
|
+
path6,
|
|
9081
|
+
`sensitive field must be an env() reference; got literal value.
|
|
9082
|
+
fix:
|
|
9083
|
+
1. insforge secrets add ${suggestedSecretName} "<value>"
|
|
9084
|
+
2. update insforge.toml:
|
|
9085
|
+
${path6.split(".").pop()} = "env(${suggestedSecretName})"
|
|
9086
|
+
3. insforge config apply`
|
|
9087
|
+
);
|
|
9088
|
+
}
|
|
9089
|
+
async function resolveEnvRef(envRef, fieldPath) {
|
|
9090
|
+
const secretName = parseEnvRef(envRef);
|
|
9091
|
+
if (!secretName) {
|
|
9092
|
+
throw new ConfigValidationError(
|
|
9093
|
+
fieldPath,
|
|
9094
|
+
`expected env() reference, got "${envRef}"`
|
|
9095
|
+
);
|
|
9096
|
+
}
|
|
9097
|
+
let res;
|
|
9098
|
+
try {
|
|
9099
|
+
res = await ossFetch(`/api/secrets/${encodeURIComponent(secretName)}`);
|
|
9100
|
+
} catch (err) {
|
|
9101
|
+
const message = err.message ?? "";
|
|
9102
|
+
if (/not found/i.test(message)) {
|
|
9103
|
+
throw new CLIError(
|
|
9104
|
+
`${fieldPath} references env(${secretName}) but no such secret exists.
|
|
9105
|
+
fix: insforge secrets add ${secretName} "<value>"`,
|
|
9106
|
+
1,
|
|
9107
|
+
"SECRET_NOT_FOUND"
|
|
9108
|
+
);
|
|
9109
|
+
}
|
|
9110
|
+
throw new CLIError(
|
|
9111
|
+
`failed to resolve env(${secretName}) for ${fieldPath}: ${message}`,
|
|
9112
|
+
1,
|
|
9113
|
+
"SECRET_LOOKUP_FAILED"
|
|
9114
|
+
);
|
|
9115
|
+
}
|
|
9116
|
+
if (!res.ok) {
|
|
9117
|
+
throw new CLIError(
|
|
9118
|
+
`failed to resolve env(${secretName}) for ${fieldPath}: HTTP ${res.status}`,
|
|
9119
|
+
1,
|
|
9120
|
+
"SECRET_LOOKUP_FAILED"
|
|
9121
|
+
);
|
|
9122
|
+
}
|
|
9123
|
+
const body = await res.json();
|
|
9124
|
+
if (typeof body.value !== "string" || body.value.length === 0) {
|
|
9125
|
+
throw new CLIError(
|
|
9126
|
+
`env(${secretName}) resolved to an empty value (secret may be inactive).
|
|
9127
|
+
fix: insforge secrets update ${secretName} --active true`,
|
|
9128
|
+
1,
|
|
9129
|
+
"SECRET_EMPTY"
|
|
9130
|
+
);
|
|
9131
|
+
}
|
|
9132
|
+
return body.value;
|
|
9133
|
+
}
|
|
9134
|
+
|
|
9063
9135
|
// src/lib/config-schema.ts
|
|
9064
9136
|
var ConfigValidationError = class extends Error {
|
|
9065
9137
|
constructor(path6, message) {
|
|
@@ -9081,6 +9153,25 @@ function validateConfig(input) {
|
|
|
9081
9153
|
out.project_id = obj.project_id;
|
|
9082
9154
|
}
|
|
9083
9155
|
if ("auth" in obj) out.auth = validateAuth(obj.auth);
|
|
9156
|
+
if ("deployments" in obj) out.deployments = validateDeployments(obj.deployments);
|
|
9157
|
+
return out;
|
|
9158
|
+
}
|
|
9159
|
+
function validateDeployments(input) {
|
|
9160
|
+
if (input === null || typeof input !== "object" || Array.isArray(input)) {
|
|
9161
|
+
throw new ConfigValidationError("deployments", "must be an object");
|
|
9162
|
+
}
|
|
9163
|
+
const obj = input;
|
|
9164
|
+
const out = {};
|
|
9165
|
+
if ("subdomain" in obj) {
|
|
9166
|
+
const v = obj.subdomain;
|
|
9167
|
+
if (v !== null && typeof v !== "string") {
|
|
9168
|
+
throw new ConfigValidationError(
|
|
9169
|
+
"deployments.subdomain",
|
|
9170
|
+
"must be a string or null"
|
|
9171
|
+
);
|
|
9172
|
+
}
|
|
9173
|
+
out.subdomain = v;
|
|
9174
|
+
}
|
|
9084
9175
|
return out;
|
|
9085
9176
|
}
|
|
9086
9177
|
function validateAuth(input) {
|
|
@@ -9099,6 +9190,128 @@ function validateAuth(input) {
|
|
|
9099
9190
|
}
|
|
9100
9191
|
out.allowed_redirect_urls = v;
|
|
9101
9192
|
}
|
|
9193
|
+
if ("require_email_verification" in obj) {
|
|
9194
|
+
if (typeof obj.require_email_verification !== "boolean") {
|
|
9195
|
+
throw new ConfigValidationError(
|
|
9196
|
+
"auth.require_email_verification",
|
|
9197
|
+
"must be a boolean"
|
|
9198
|
+
);
|
|
9199
|
+
}
|
|
9200
|
+
out.require_email_verification = obj.require_email_verification;
|
|
9201
|
+
}
|
|
9202
|
+
if ("verify_email_method" in obj) {
|
|
9203
|
+
out.verify_email_method = validateVerificationMethod(
|
|
9204
|
+
"auth.verify_email_method",
|
|
9205
|
+
obj.verify_email_method
|
|
9206
|
+
);
|
|
9207
|
+
}
|
|
9208
|
+
if ("reset_password_method" in obj) {
|
|
9209
|
+
out.reset_password_method = validateVerificationMethod(
|
|
9210
|
+
"auth.reset_password_method",
|
|
9211
|
+
obj.reset_password_method
|
|
9212
|
+
);
|
|
9213
|
+
}
|
|
9214
|
+
if ("password" in obj) out.password = validatePassword(obj.password);
|
|
9215
|
+
if ("smtp" in obj) out.smtp = validateSmtp(obj.smtp);
|
|
9216
|
+
return out;
|
|
9217
|
+
}
|
|
9218
|
+
function validateVerificationMethod(path6, value) {
|
|
9219
|
+
if (value !== "code" && value !== "link") {
|
|
9220
|
+
throw new ConfigValidationError(path6, 'must be "code" or "link"');
|
|
9221
|
+
}
|
|
9222
|
+
return value;
|
|
9223
|
+
}
|
|
9224
|
+
function validatePassword(input) {
|
|
9225
|
+
if (input === null || typeof input !== "object" || Array.isArray(input)) {
|
|
9226
|
+
throw new ConfigValidationError("auth.password", "must be a table");
|
|
9227
|
+
}
|
|
9228
|
+
const obj = input;
|
|
9229
|
+
const out = {};
|
|
9230
|
+
if ("min_length" in obj) {
|
|
9231
|
+
if (typeof obj.min_length !== "number" || !Number.isInteger(obj.min_length) || obj.min_length < 4 || obj.min_length > 128) {
|
|
9232
|
+
throw new ConfigValidationError(
|
|
9233
|
+
"auth.password.min_length",
|
|
9234
|
+
"must be an integer between 4 and 128"
|
|
9235
|
+
);
|
|
9236
|
+
}
|
|
9237
|
+
out.min_length = obj.min_length;
|
|
9238
|
+
}
|
|
9239
|
+
for (const key of [
|
|
9240
|
+
"require_number",
|
|
9241
|
+
"require_lowercase",
|
|
9242
|
+
"require_uppercase",
|
|
9243
|
+
"require_special_char"
|
|
9244
|
+
]) {
|
|
9245
|
+
if (key in obj) {
|
|
9246
|
+
if (typeof obj[key] !== "boolean") {
|
|
9247
|
+
throw new ConfigValidationError(`auth.password.${key}`, "must be a boolean");
|
|
9248
|
+
}
|
|
9249
|
+
out[key] = obj[key];
|
|
9250
|
+
}
|
|
9251
|
+
}
|
|
9252
|
+
return out;
|
|
9253
|
+
}
|
|
9254
|
+
function validateSmtp(input) {
|
|
9255
|
+
if (input === null || typeof input !== "object" || Array.isArray(input)) {
|
|
9256
|
+
throw new ConfigValidationError("auth.smtp", "must be a table");
|
|
9257
|
+
}
|
|
9258
|
+
const obj = input;
|
|
9259
|
+
const out = {};
|
|
9260
|
+
if ("enabled" in obj) {
|
|
9261
|
+
if (typeof obj.enabled !== "boolean") {
|
|
9262
|
+
throw new ConfigValidationError("auth.smtp.enabled", "must be a boolean");
|
|
9263
|
+
}
|
|
9264
|
+
out.enabled = obj.enabled;
|
|
9265
|
+
}
|
|
9266
|
+
if ("host" in obj) {
|
|
9267
|
+
if (typeof obj.host !== "string") {
|
|
9268
|
+
throw new ConfigValidationError("auth.smtp.host", "must be a string");
|
|
9269
|
+
}
|
|
9270
|
+
out.host = obj.host;
|
|
9271
|
+
}
|
|
9272
|
+
if ("port" in obj) {
|
|
9273
|
+
if (typeof obj.port !== "number" || !Number.isInteger(obj.port) || obj.port < 1 || obj.port > 65535) {
|
|
9274
|
+
throw new ConfigValidationError(
|
|
9275
|
+
"auth.smtp.port",
|
|
9276
|
+
"must be an integer between 1 and 65535"
|
|
9277
|
+
);
|
|
9278
|
+
}
|
|
9279
|
+
out.port = obj.port;
|
|
9280
|
+
}
|
|
9281
|
+
if ("username" in obj) {
|
|
9282
|
+
if (typeof obj.username !== "string") {
|
|
9283
|
+
throw new ConfigValidationError("auth.smtp.username", "must be a string");
|
|
9284
|
+
}
|
|
9285
|
+
out.username = obj.username;
|
|
9286
|
+
}
|
|
9287
|
+
if ("password" in obj) {
|
|
9288
|
+
out.password = validateSensitiveString(
|
|
9289
|
+
"auth.smtp.password",
|
|
9290
|
+
obj.password,
|
|
9291
|
+
"SMTP_PASSWORD"
|
|
9292
|
+
);
|
|
9293
|
+
}
|
|
9294
|
+
if ("sender_email" in obj) {
|
|
9295
|
+
if (typeof obj.sender_email !== "string") {
|
|
9296
|
+
throw new ConfigValidationError("auth.smtp.sender_email", "must be a string");
|
|
9297
|
+
}
|
|
9298
|
+
out.sender_email = obj.sender_email;
|
|
9299
|
+
}
|
|
9300
|
+
if ("sender_name" in obj) {
|
|
9301
|
+
if (typeof obj.sender_name !== "string") {
|
|
9302
|
+
throw new ConfigValidationError("auth.smtp.sender_name", "must be a string");
|
|
9303
|
+
}
|
|
9304
|
+
out.sender_name = obj.sender_name;
|
|
9305
|
+
}
|
|
9306
|
+
if ("min_interval_seconds" in obj) {
|
|
9307
|
+
if (typeof obj.min_interval_seconds !== "number" || !Number.isInteger(obj.min_interval_seconds) || obj.min_interval_seconds < 0) {
|
|
9308
|
+
throw new ConfigValidationError(
|
|
9309
|
+
"auth.smtp.min_interval_seconds",
|
|
9310
|
+
"must be a non-negative integer"
|
|
9311
|
+
);
|
|
9312
|
+
}
|
|
9313
|
+
out.min_interval_seconds = obj.min_interval_seconds;
|
|
9314
|
+
}
|
|
9102
9315
|
return out;
|
|
9103
9316
|
}
|
|
9104
9317
|
|
|
@@ -9120,14 +9333,204 @@ function stringifyConfigToml(config) {
|
|
|
9120
9333
|
}
|
|
9121
9334
|
if (config.auth) {
|
|
9122
9335
|
lines.push("[auth]");
|
|
9123
|
-
|
|
9124
|
-
const urls = config.auth.allowed_redirect_urls.map((u) => JSON.stringify(u)).join(", ");
|
|
9125
|
-
lines.push(`allowed_redirect_urls = [${urls}]`);
|
|
9126
|
-
}
|
|
9336
|
+
renderAuthFlatFields(config.auth, lines);
|
|
9127
9337
|
lines.push("");
|
|
9338
|
+
if (config.auth.password !== void 0) {
|
|
9339
|
+
lines.push("[auth.password]");
|
|
9340
|
+
renderPasswordFields(config.auth.password, lines);
|
|
9341
|
+
lines.push("");
|
|
9342
|
+
}
|
|
9343
|
+
if (config.auth.smtp !== void 0) {
|
|
9344
|
+
lines.push("[auth.smtp]");
|
|
9345
|
+
renderSmtpFields(config.auth.smtp, lines);
|
|
9346
|
+
lines.push("");
|
|
9347
|
+
}
|
|
9348
|
+
}
|
|
9349
|
+
if (config.deployments) {
|
|
9350
|
+
if (typeof config.deployments.subdomain === "string" && config.deployments.subdomain !== "") {
|
|
9351
|
+
lines.push("[deployments]");
|
|
9352
|
+
lines.push(`subdomain = ${JSON.stringify(config.deployments.subdomain)}`);
|
|
9353
|
+
lines.push("");
|
|
9354
|
+
}
|
|
9128
9355
|
}
|
|
9129
9356
|
return lines.join("\n").replace(/\n+$/, "\n");
|
|
9130
9357
|
}
|
|
9358
|
+
function renderAuthFlatFields(auth, lines) {
|
|
9359
|
+
if (auth.allowed_redirect_urls !== void 0) {
|
|
9360
|
+
const urls = auth.allowed_redirect_urls.map((u) => JSON.stringify(u)).join(", ");
|
|
9361
|
+
lines.push(`allowed_redirect_urls = [${urls}]`);
|
|
9362
|
+
}
|
|
9363
|
+
if (auth.require_email_verification !== void 0) {
|
|
9364
|
+
lines.push(`require_email_verification = ${auth.require_email_verification}`);
|
|
9365
|
+
}
|
|
9366
|
+
if (auth.verify_email_method !== void 0) {
|
|
9367
|
+
lines.push(`verify_email_method = ${JSON.stringify(auth.verify_email_method)}`);
|
|
9368
|
+
}
|
|
9369
|
+
if (auth.reset_password_method !== void 0) {
|
|
9370
|
+
lines.push(`reset_password_method = ${JSON.stringify(auth.reset_password_method)}`);
|
|
9371
|
+
}
|
|
9372
|
+
}
|
|
9373
|
+
function renderPasswordFields(pw, lines) {
|
|
9374
|
+
if (pw.min_length !== void 0) lines.push(`min_length = ${pw.min_length}`);
|
|
9375
|
+
if (pw.require_number !== void 0) lines.push(`require_number = ${pw.require_number}`);
|
|
9376
|
+
if (pw.require_lowercase !== void 0) {
|
|
9377
|
+
lines.push(`require_lowercase = ${pw.require_lowercase}`);
|
|
9378
|
+
}
|
|
9379
|
+
if (pw.require_uppercase !== void 0) {
|
|
9380
|
+
lines.push(`require_uppercase = ${pw.require_uppercase}`);
|
|
9381
|
+
}
|
|
9382
|
+
if (pw.require_special_char !== void 0) {
|
|
9383
|
+
lines.push(`require_special_char = ${pw.require_special_char}`);
|
|
9384
|
+
}
|
|
9385
|
+
}
|
|
9386
|
+
function renderSmtpFields(smtp, lines) {
|
|
9387
|
+
if (smtp.enabled !== void 0) lines.push(`enabled = ${smtp.enabled}`);
|
|
9388
|
+
if (smtp.host !== void 0) lines.push(`host = ${JSON.stringify(smtp.host)}`);
|
|
9389
|
+
if (smtp.port !== void 0) lines.push(`port = ${smtp.port}`);
|
|
9390
|
+
if (smtp.username !== void 0) lines.push(`username = ${JSON.stringify(smtp.username)}`);
|
|
9391
|
+
if (smtp.password !== void 0) {
|
|
9392
|
+
const secretName = parseEnvRef(smtp.password) ?? "SMTP_PASSWORD";
|
|
9393
|
+
lines.push(
|
|
9394
|
+
`# password is managed via secrets \u2014 run \`insforge secrets add ${secretName} "<value>"\``
|
|
9395
|
+
);
|
|
9396
|
+
lines.push(`password = ${JSON.stringify(smtp.password)}`);
|
|
9397
|
+
}
|
|
9398
|
+
if (smtp.sender_email !== void 0) {
|
|
9399
|
+
lines.push(`sender_email = ${JSON.stringify(smtp.sender_email)}`);
|
|
9400
|
+
}
|
|
9401
|
+
if (smtp.sender_name !== void 0) {
|
|
9402
|
+
lines.push(`sender_name = ${JSON.stringify(smtp.sender_name)}`);
|
|
9403
|
+
}
|
|
9404
|
+
if (smtp.min_interval_seconds !== void 0) {
|
|
9405
|
+
lines.push(`min_interval_seconds = ${smtp.min_interval_seconds}`);
|
|
9406
|
+
}
|
|
9407
|
+
}
|
|
9408
|
+
|
|
9409
|
+
// src/lib/config-metadata.ts
|
|
9410
|
+
function liveFromMetadata(raw) {
|
|
9411
|
+
const live = { auth: {} };
|
|
9412
|
+
const a = isPlainObject(raw.auth) ? raw.auth : void 0;
|
|
9413
|
+
if (a && "allowedRedirectUrls" in a) {
|
|
9414
|
+
live.auth.allowed_redirect_urls = asStringArray(a.allowedRedirectUrls) ?? [];
|
|
9415
|
+
}
|
|
9416
|
+
if (a && "requireEmailVerification" in a) {
|
|
9417
|
+
live.auth.require_email_verification = a.requireEmailVerification ?? false;
|
|
9418
|
+
}
|
|
9419
|
+
if (a && "verifyEmailMethod" in a && (a.verifyEmailMethod === "code" || a.verifyEmailMethod === "link")) {
|
|
9420
|
+
live.auth.verify_email_method = a.verifyEmailMethod;
|
|
9421
|
+
}
|
|
9422
|
+
if (a && "resetPasswordMethod" in a && (a.resetPasswordMethod === "code" || a.resetPasswordMethod === "link")) {
|
|
9423
|
+
live.auth.reset_password_method = a.resetPasswordMethod;
|
|
9424
|
+
}
|
|
9425
|
+
if (a && ("passwordMinLength" in a || "requireNumber" in a || "requireLowercase" in a || "requireUppercase" in a || "requireSpecialChar" in a)) {
|
|
9426
|
+
live.auth.password = {
|
|
9427
|
+
min_length: a.passwordMinLength ?? 8,
|
|
9428
|
+
require_number: a.requireNumber ?? false,
|
|
9429
|
+
require_lowercase: a.requireLowercase ?? false,
|
|
9430
|
+
require_uppercase: a.requireUppercase ?? false,
|
|
9431
|
+
require_special_char: a.requireSpecialChar ?? false
|
|
9432
|
+
};
|
|
9433
|
+
}
|
|
9434
|
+
if (isPlainObject(a?.smtpConfig)) {
|
|
9435
|
+
const s = a.smtpConfig;
|
|
9436
|
+
live.auth.smtp = {
|
|
9437
|
+
enabled: s.enabled ?? false,
|
|
9438
|
+
host: s.host ?? "",
|
|
9439
|
+
port: s.port ?? 587,
|
|
9440
|
+
username: s.username ?? "",
|
|
9441
|
+
hasPassword: s.hasPassword ?? false,
|
|
9442
|
+
sender_email: s.senderEmail ?? "",
|
|
9443
|
+
sender_name: s.senderName ?? "",
|
|
9444
|
+
min_interval_seconds: s.minIntervalSeconds ?? 60
|
|
9445
|
+
};
|
|
9446
|
+
}
|
|
9447
|
+
const d = isPlainObject(raw.deployments) ? raw.deployments : void 0;
|
|
9448
|
+
if (d) {
|
|
9449
|
+
live.deployments = {
|
|
9450
|
+
subdomain: typeof d.customSlug === "string" && d.customSlug ? d.customSlug : null
|
|
9451
|
+
};
|
|
9452
|
+
}
|
|
9453
|
+
return live;
|
|
9454
|
+
}
|
|
9455
|
+
function isPlainObject(v) {
|
|
9456
|
+
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
9457
|
+
}
|
|
9458
|
+
function asStringArray(v) {
|
|
9459
|
+
return Array.isArray(v) && v.every((x) => typeof x === "string") ? v : null;
|
|
9460
|
+
}
|
|
9461
|
+
function configFromMetadata(raw) {
|
|
9462
|
+
const config = {};
|
|
9463
|
+
const skipped = [];
|
|
9464
|
+
const a = isPlainObject(raw.auth) ? raw.auth : void 0;
|
|
9465
|
+
if (a && "allowedRedirectUrls" in a) {
|
|
9466
|
+
config.auth = config.auth ?? {};
|
|
9467
|
+
config.auth.allowed_redirect_urls = asStringArray(a.allowedRedirectUrls) ?? [];
|
|
9468
|
+
} else {
|
|
9469
|
+
skipped.push("auth.allowed_redirect_urls");
|
|
9470
|
+
}
|
|
9471
|
+
if (a && "requireEmailVerification" in a) {
|
|
9472
|
+
config.auth = config.auth ?? {};
|
|
9473
|
+
config.auth.require_email_verification = a.requireEmailVerification ?? false;
|
|
9474
|
+
} else {
|
|
9475
|
+
skipped.push("auth.require_email_verification");
|
|
9476
|
+
}
|
|
9477
|
+
if (a && "verifyEmailMethod" in a && (a.verifyEmailMethod === "code" || a.verifyEmailMethod === "link")) {
|
|
9478
|
+
config.auth = config.auth ?? {};
|
|
9479
|
+
config.auth.verify_email_method = a.verifyEmailMethod;
|
|
9480
|
+
} else {
|
|
9481
|
+
skipped.push("auth.verify_email_method");
|
|
9482
|
+
}
|
|
9483
|
+
if (a && "resetPasswordMethod" in a && (a.resetPasswordMethod === "code" || a.resetPasswordMethod === "link")) {
|
|
9484
|
+
config.auth = config.auth ?? {};
|
|
9485
|
+
config.auth.reset_password_method = a.resetPasswordMethod;
|
|
9486
|
+
} else {
|
|
9487
|
+
skipped.push("auth.reset_password_method");
|
|
9488
|
+
}
|
|
9489
|
+
if (a && ("passwordMinLength" in a || "requireNumber" in a || "requireLowercase" in a || "requireUppercase" in a || "requireSpecialChar" in a)) {
|
|
9490
|
+
config.auth = config.auth ?? {};
|
|
9491
|
+
config.auth.password = {};
|
|
9492
|
+
if ("passwordMinLength" in a) config.auth.password.min_length = a.passwordMinLength ?? 8;
|
|
9493
|
+
if ("requireNumber" in a) config.auth.password.require_number = a.requireNumber ?? false;
|
|
9494
|
+
if ("requireLowercase" in a) config.auth.password.require_lowercase = a.requireLowercase ?? false;
|
|
9495
|
+
if ("requireUppercase" in a) config.auth.password.require_uppercase = a.requireUppercase ?? false;
|
|
9496
|
+
if ("requireSpecialChar" in a) {
|
|
9497
|
+
config.auth.password.require_special_char = a.requireSpecialChar ?? false;
|
|
9498
|
+
}
|
|
9499
|
+
} else {
|
|
9500
|
+
skipped.push("auth.password");
|
|
9501
|
+
}
|
|
9502
|
+
if (a && "smtpConfig" in a) {
|
|
9503
|
+
const s = a.smtpConfig;
|
|
9504
|
+
if (isPlainObject(s)) {
|
|
9505
|
+
config.auth = config.auth ?? {};
|
|
9506
|
+
config.auth.smtp = {
|
|
9507
|
+
enabled: s.enabled ?? false,
|
|
9508
|
+
host: s.host ?? "",
|
|
9509
|
+
port: s.port ?? 587,
|
|
9510
|
+
username: s.username ?? "",
|
|
9511
|
+
// When backend has a password set, emit a deterministic env() placeholder
|
|
9512
|
+
// so the user knows which secret to define. We do NOT round-trip the
|
|
9513
|
+
// value (it never leaves the backend). Re-applying this TOML force-resends
|
|
9514
|
+
// from the secrets store — see config-diff.ts for the force-resend rationale.
|
|
9515
|
+
...s.hasPassword ? { password: "env(SMTP_PASSWORD)" } : {},
|
|
9516
|
+
sender_email: s.senderEmail ?? "",
|
|
9517
|
+
sender_name: s.senderName ?? "",
|
|
9518
|
+
min_interval_seconds: s.minIntervalSeconds ?? 60
|
|
9519
|
+
};
|
|
9520
|
+
}
|
|
9521
|
+
} else {
|
|
9522
|
+
skipped.push("auth.smtp");
|
|
9523
|
+
}
|
|
9524
|
+
const d = isPlainObject(raw.deployments) ? raw.deployments : void 0;
|
|
9525
|
+
if (d) {
|
|
9526
|
+
if (typeof d.customSlug === "string" && d.customSlug) {
|
|
9527
|
+
config.deployments = { subdomain: d.customSlug };
|
|
9528
|
+
}
|
|
9529
|
+
} else {
|
|
9530
|
+
skipped.push("deployments.subdomain");
|
|
9531
|
+
}
|
|
9532
|
+
return { config, skipped };
|
|
9533
|
+
}
|
|
9131
9534
|
|
|
9132
9535
|
// src/commands/config/export.ts
|
|
9133
9536
|
function registerConfigExportCommand(cfg) {
|
|
@@ -9155,16 +9558,7 @@ function registerConfigExportCommand(cfg) {
|
|
|
9155
9558
|
}
|
|
9156
9559
|
const res = await ossFetch("/api/metadata");
|
|
9157
9560
|
const raw = await res.json();
|
|
9158
|
-
const config =
|
|
9159
|
-
const skipped = [];
|
|
9160
|
-
const authSlice = raw?.auth;
|
|
9161
|
-
if (authSlice && typeof authSlice === "object" && "allowedRedirectUrls" in authSlice) {
|
|
9162
|
-
config.auth = {
|
|
9163
|
-
allowed_redirect_urls: authSlice.allowedRedirectUrls ?? []
|
|
9164
|
-
};
|
|
9165
|
-
} else {
|
|
9166
|
-
skipped.push("auth.allowed_redirect_urls");
|
|
9167
|
-
}
|
|
9561
|
+
const { config, skipped } = configFromMetadata(raw);
|
|
9168
9562
|
const toml = stringifyConfigToml(config);
|
|
9169
9563
|
writeFileSync9(target, toml, "utf8");
|
|
9170
9564
|
if (json) {
|
|
@@ -9210,8 +9604,165 @@ function diffConfig({ live, file }) {
|
|
|
9210
9604
|
});
|
|
9211
9605
|
}
|
|
9212
9606
|
}
|
|
9607
|
+
if (fileAuth && "require_email_verification" in fileAuth) {
|
|
9608
|
+
const fromV = liveAuth.require_email_verification ?? false;
|
|
9609
|
+
const toV = fileAuth.require_email_verification ?? false;
|
|
9610
|
+
if (fromV !== toV) {
|
|
9611
|
+
changes.push({
|
|
9612
|
+
section: "auth",
|
|
9613
|
+
op: "modify",
|
|
9614
|
+
key: "require_email_verification",
|
|
9615
|
+
from: fromV,
|
|
9616
|
+
to: toV
|
|
9617
|
+
});
|
|
9618
|
+
}
|
|
9619
|
+
}
|
|
9620
|
+
if (fileAuth && "verify_email_method" in fileAuth && fileAuth.verify_email_method) {
|
|
9621
|
+
const fromV = liveAuth.verify_email_method ?? "code";
|
|
9622
|
+
const toV = fileAuth.verify_email_method;
|
|
9623
|
+
if (fromV !== toV) {
|
|
9624
|
+
changes.push({
|
|
9625
|
+
section: "auth",
|
|
9626
|
+
op: "modify",
|
|
9627
|
+
key: "verify_email_method",
|
|
9628
|
+
from: fromV,
|
|
9629
|
+
to: toV
|
|
9630
|
+
});
|
|
9631
|
+
}
|
|
9632
|
+
}
|
|
9633
|
+
if (fileAuth && "reset_password_method" in fileAuth && fileAuth.reset_password_method) {
|
|
9634
|
+
const fromV = liveAuth.reset_password_method ?? "code";
|
|
9635
|
+
const toV = fileAuth.reset_password_method;
|
|
9636
|
+
if (fromV !== toV) {
|
|
9637
|
+
changes.push({
|
|
9638
|
+
section: "auth",
|
|
9639
|
+
op: "modify",
|
|
9640
|
+
key: "reset_password_method",
|
|
9641
|
+
from: fromV,
|
|
9642
|
+
to: toV
|
|
9643
|
+
});
|
|
9644
|
+
}
|
|
9645
|
+
}
|
|
9646
|
+
if (fileAuth?.password) {
|
|
9647
|
+
diffPassword(liveAuth.password, fileAuth.password, changes);
|
|
9648
|
+
}
|
|
9649
|
+
if (fileAuth?.smtp !== void 0) {
|
|
9650
|
+
const smtpChange = diffSmtp(liveAuth.smtp, fileAuth.smtp);
|
|
9651
|
+
if (smtpChange) changes.push(smtpChange);
|
|
9652
|
+
}
|
|
9653
|
+
const fileDeployments = file.deployments;
|
|
9654
|
+
const liveDeployments = live.deployments ?? {};
|
|
9655
|
+
if (fileDeployments && "subdomain" in fileDeployments) {
|
|
9656
|
+
const fromV = liveDeployments.subdomain ?? null;
|
|
9657
|
+
const rawTo = fileDeployments.subdomain;
|
|
9658
|
+
const toV = rawTo === null || rawTo === void 0 || rawTo === "" ? null : rawTo;
|
|
9659
|
+
if (fromV !== toV) {
|
|
9660
|
+
changes.push({
|
|
9661
|
+
section: "deployments",
|
|
9662
|
+
op: "modify",
|
|
9663
|
+
key: "subdomain",
|
|
9664
|
+
from: fromV,
|
|
9665
|
+
to: toV
|
|
9666
|
+
});
|
|
9667
|
+
}
|
|
9668
|
+
}
|
|
9213
9669
|
return { changes, summary: summarize(changes) };
|
|
9214
9670
|
}
|
|
9671
|
+
function diffPassword(live, file, changes) {
|
|
9672
|
+
const liveView = live ?? EMPTY_PASSWORD_POLICY;
|
|
9673
|
+
if (file.min_length !== void 0 && liveView.min_length !== file.min_length) {
|
|
9674
|
+
changes.push({
|
|
9675
|
+
section: "auth.password",
|
|
9676
|
+
op: "modify",
|
|
9677
|
+
key: "min_length",
|
|
9678
|
+
from: liveView.min_length,
|
|
9679
|
+
to: file.min_length
|
|
9680
|
+
});
|
|
9681
|
+
}
|
|
9682
|
+
for (const key of [
|
|
9683
|
+
"require_number",
|
|
9684
|
+
"require_lowercase",
|
|
9685
|
+
"require_uppercase",
|
|
9686
|
+
"require_special_char"
|
|
9687
|
+
]) {
|
|
9688
|
+
const fromV = liveView[key];
|
|
9689
|
+
const toV = file[key];
|
|
9690
|
+
if (toV !== void 0 && fromV !== toV) {
|
|
9691
|
+
changes.push({
|
|
9692
|
+
section: "auth.password",
|
|
9693
|
+
op: "modify",
|
|
9694
|
+
key,
|
|
9695
|
+
from: fromV,
|
|
9696
|
+
to: toV
|
|
9697
|
+
});
|
|
9698
|
+
}
|
|
9699
|
+
}
|
|
9700
|
+
}
|
|
9701
|
+
function diffSmtp(live, fileSmtp) {
|
|
9702
|
+
const livedView = renderLiveSmtp(live);
|
|
9703
|
+
const tomlView = renderFileSmtp(fileSmtp);
|
|
9704
|
+
const envRef = fileSmtp.password ? parseEnvRef(fileSmtp.password) : null;
|
|
9705
|
+
const nonPasswordFieldsChanged = livedView.enabled !== tomlView.enabled || livedView.host !== tomlView.host || livedView.port !== tomlView.port || livedView.username !== tomlView.username || livedView.sender_email !== tomlView.sender_email || livedView.sender_name !== tomlView.sender_name || livedView.min_interval_seconds !== tomlView.min_interval_seconds;
|
|
9706
|
+
if (!nonPasswordFieldsChanged && envRef === null) {
|
|
9707
|
+
return null;
|
|
9708
|
+
}
|
|
9709
|
+
return {
|
|
9710
|
+
section: "auth.smtp",
|
|
9711
|
+
op: "modify",
|
|
9712
|
+
key: "config",
|
|
9713
|
+
from: livedView,
|
|
9714
|
+
to: tomlView,
|
|
9715
|
+
passwordEnvRef: envRef ?? void 0
|
|
9716
|
+
};
|
|
9717
|
+
}
|
|
9718
|
+
function renderLiveSmtp(live) {
|
|
9719
|
+
const empty = EMPTY_SMTP_VIEW;
|
|
9720
|
+
if (!live) return empty;
|
|
9721
|
+
return {
|
|
9722
|
+
enabled: live.enabled,
|
|
9723
|
+
host: live.host,
|
|
9724
|
+
port: live.port,
|
|
9725
|
+
username: live.username,
|
|
9726
|
+
password: live.hasPassword ? "(set)" : "(unset)",
|
|
9727
|
+
sender_email: live.sender_email,
|
|
9728
|
+
sender_name: live.sender_name,
|
|
9729
|
+
min_interval_seconds: live.min_interval_seconds
|
|
9730
|
+
};
|
|
9731
|
+
}
|
|
9732
|
+
function renderFileSmtp(file) {
|
|
9733
|
+
return {
|
|
9734
|
+
enabled: file.enabled ?? false,
|
|
9735
|
+
host: file.host ?? "",
|
|
9736
|
+
port: file.port ?? 587,
|
|
9737
|
+
username: file.username ?? "",
|
|
9738
|
+
password: renderFilePassword(file.password),
|
|
9739
|
+
sender_email: file.sender_email ?? "",
|
|
9740
|
+
sender_name: file.sender_name ?? "",
|
|
9741
|
+
min_interval_seconds: file.min_interval_seconds ?? 60
|
|
9742
|
+
};
|
|
9743
|
+
}
|
|
9744
|
+
function renderFilePassword(value) {
|
|
9745
|
+
if (value === void 0) return "(unchanged)";
|
|
9746
|
+
const ref = parseEnvRef(value);
|
|
9747
|
+
return ref ? `env(${ref})` : "(invalid)";
|
|
9748
|
+
}
|
|
9749
|
+
var EMPTY_SMTP_VIEW = {
|
|
9750
|
+
enabled: false,
|
|
9751
|
+
host: "",
|
|
9752
|
+
port: 587,
|
|
9753
|
+
username: "",
|
|
9754
|
+
password: "(unset)",
|
|
9755
|
+
sender_email: "",
|
|
9756
|
+
sender_name: "",
|
|
9757
|
+
min_interval_seconds: 60
|
|
9758
|
+
};
|
|
9759
|
+
var EMPTY_PASSWORD_POLICY = {
|
|
9760
|
+
min_length: 8,
|
|
9761
|
+
require_number: false,
|
|
9762
|
+
require_lowercase: false,
|
|
9763
|
+
require_uppercase: false,
|
|
9764
|
+
require_special_char: false
|
|
9765
|
+
};
|
|
9215
9766
|
function summarize(changes) {
|
|
9216
9767
|
const s = { add: 0, modify: 0, remove: 0, kept: 0 };
|
|
9217
9768
|
for (const c of changes) {
|
|
@@ -9253,19 +9804,82 @@ function formatPlan(result) {
|
|
|
9253
9804
|
return lines.join("\n");
|
|
9254
9805
|
}
|
|
9255
9806
|
function formatChange(c) {
|
|
9807
|
+
if (c.section === "auth.smtp") {
|
|
9808
|
+
const lines = [`~ smtp config:`];
|
|
9809
|
+
const from = c.from;
|
|
9810
|
+
const to = c.to;
|
|
9811
|
+
for (const key of [
|
|
9812
|
+
"enabled",
|
|
9813
|
+
"host",
|
|
9814
|
+
"port",
|
|
9815
|
+
"username",
|
|
9816
|
+
"password",
|
|
9817
|
+
"sender_email",
|
|
9818
|
+
"sender_name",
|
|
9819
|
+
"min_interval_seconds"
|
|
9820
|
+
]) {
|
|
9821
|
+
if (from[key] !== to[key]) {
|
|
9822
|
+
lines.push(` ${key}: ${JSON.stringify(from[key])} \u2192 ${JSON.stringify(to[key])}`);
|
|
9823
|
+
}
|
|
9824
|
+
}
|
|
9825
|
+
if (c.passwordEnvRef) {
|
|
9826
|
+
lines.push(` (password force-resent from env(${c.passwordEnvRef}))`);
|
|
9827
|
+
}
|
|
9828
|
+
return lines.join("\n ");
|
|
9829
|
+
}
|
|
9830
|
+
if (c.section === "deployments" && c.key === "subdomain") {
|
|
9831
|
+
const fromLabel = c.from === null ? "(unset)" : JSON.stringify(c.from);
|
|
9832
|
+
const toLabel = c.to === null ? "(unset)" : JSON.stringify(c.to);
|
|
9833
|
+
return `~ ${c.key}: ${fromLabel} \u2192 ${toLabel}`;
|
|
9834
|
+
}
|
|
9256
9835
|
return `~ ${c.key}: ${JSON.stringify(c.from)} \u2192 ${JSON.stringify(c.to)}`;
|
|
9257
9836
|
}
|
|
9258
9837
|
|
|
9259
9838
|
// src/lib/config-capabilities.ts
|
|
9260
9839
|
function metadataSupports(raw, change) {
|
|
9261
9840
|
if (change.section === "auth" && change.key === "allowed_redirect_urls") {
|
|
9262
|
-
return raw
|
|
9841
|
+
return hasAuthKey(raw, "allowedRedirectUrls");
|
|
9842
|
+
}
|
|
9843
|
+
if (change.section === "auth" && change.key === "require_email_verification") {
|
|
9844
|
+
return hasAuthKey(raw, "requireEmailVerification");
|
|
9845
|
+
}
|
|
9846
|
+
if (change.section === "auth" && change.key === "verify_email_method") {
|
|
9847
|
+
return hasAuthKey(raw, "verifyEmailMethod");
|
|
9848
|
+
}
|
|
9849
|
+
if (change.section === "auth" && change.key === "reset_password_method") {
|
|
9850
|
+
return hasAuthKey(raw, "resetPasswordMethod");
|
|
9851
|
+
}
|
|
9852
|
+
if (change.section === "auth.password") {
|
|
9853
|
+
return hasAuthKey(raw, AUTH_PASSWORD_WIRE_KEY[change.key]);
|
|
9263
9854
|
}
|
|
9855
|
+
if (change.section === "auth.smtp") {
|
|
9856
|
+
return hasAuthKey(raw, "smtpConfig");
|
|
9857
|
+
}
|
|
9858
|
+
if (change.section === "deployments" && change.key === "subdomain") {
|
|
9859
|
+
return raw?.deployments !== void 0 && raw.deployments !== null && typeof raw.deployments === "object";
|
|
9860
|
+
}
|
|
9861
|
+
const _exhaustive = change;
|
|
9862
|
+
void _exhaustive;
|
|
9264
9863
|
return false;
|
|
9265
9864
|
}
|
|
9865
|
+
function hasAuthKey(raw, key) {
|
|
9866
|
+
const auth = raw?.auth;
|
|
9867
|
+
return auth !== void 0 && auth !== null && typeof auth === "object" && key in auth;
|
|
9868
|
+
}
|
|
9869
|
+
var AUTH_PASSWORD_WIRE_KEY = {
|
|
9870
|
+
min_length: "passwordMinLength",
|
|
9871
|
+
require_number: "requireNumber",
|
|
9872
|
+
require_lowercase: "requireLowercase",
|
|
9873
|
+
require_uppercase: "requireUppercase",
|
|
9874
|
+
require_special_char: "requireSpecialChar"
|
|
9875
|
+
};
|
|
9266
9876
|
function changePath(change) {
|
|
9877
|
+
if (change.section === "auth.smtp") return "auth.smtp";
|
|
9267
9878
|
return `${change.section}.${change.key}`;
|
|
9268
9879
|
}
|
|
9880
|
+
function authPasswordWireKey(key) {
|
|
9881
|
+
return AUTH_PASSWORD_WIRE_KEY[key];
|
|
9882
|
+
}
|
|
9269
9883
|
|
|
9270
9884
|
// src/commands/config/plan.ts
|
|
9271
9885
|
function registerConfigPlanCommand(cfg) {
|
|
@@ -9278,9 +9892,7 @@ function registerConfigPlanCommand(cfg) {
|
|
|
9278
9892
|
const file = parseConfigToml(tomlSource);
|
|
9279
9893
|
const res = await ossFetch("/api/metadata");
|
|
9280
9894
|
const raw = await res.json();
|
|
9281
|
-
const live =
|
|
9282
|
-
auth: { allowed_redirect_urls: raw.auth?.allowedRedirectUrls ?? [] }
|
|
9283
|
-
};
|
|
9895
|
+
const live = liveFromMetadata(raw);
|
|
9284
9896
|
const result = diffConfig({ live, file });
|
|
9285
9897
|
const skipped = result.changes.filter((c) => !metadataSupports(raw, c)).map((c) => changePath(c));
|
|
9286
9898
|
if (json) {
|
|
@@ -9318,9 +9930,7 @@ function registerConfigApplyCommand(cfg) {
|
|
|
9318
9930
|
const file = parseConfigToml(tomlSource);
|
|
9319
9931
|
const res = await ossFetch("/api/metadata");
|
|
9320
9932
|
const raw = await res.json();
|
|
9321
|
-
const live =
|
|
9322
|
-
auth: { allowed_redirect_urls: raw.auth?.allowedRedirectUrls ?? [] }
|
|
9323
|
-
};
|
|
9933
|
+
const live = liveFromMetadata(raw);
|
|
9324
9934
|
const result = diffConfig({ live, file });
|
|
9325
9935
|
const approved = opts.autoApprove || yes;
|
|
9326
9936
|
if (!json) {
|
|
@@ -9400,7 +10010,68 @@ async function applyChange(change) {
|
|
|
9400
10010
|
});
|
|
9401
10011
|
return;
|
|
9402
10012
|
}
|
|
9403
|
-
|
|
10013
|
+
if (change.section === "auth" && change.key === "require_email_verification") {
|
|
10014
|
+
await ossFetch("/api/auth/config", {
|
|
10015
|
+
method: "PUT",
|
|
10016
|
+
body: JSON.stringify({ requireEmailVerification: change.to })
|
|
10017
|
+
});
|
|
10018
|
+
return;
|
|
10019
|
+
}
|
|
10020
|
+
if (change.section === "auth" && change.key === "verify_email_method") {
|
|
10021
|
+
await ossFetch("/api/auth/config", {
|
|
10022
|
+
method: "PUT",
|
|
10023
|
+
body: JSON.stringify({ verifyEmailMethod: change.to })
|
|
10024
|
+
});
|
|
10025
|
+
return;
|
|
10026
|
+
}
|
|
10027
|
+
if (change.section === "auth" && change.key === "reset_password_method") {
|
|
10028
|
+
await ossFetch("/api/auth/config", {
|
|
10029
|
+
method: "PUT",
|
|
10030
|
+
body: JSON.stringify({ resetPasswordMethod: change.to })
|
|
10031
|
+
});
|
|
10032
|
+
return;
|
|
10033
|
+
}
|
|
10034
|
+
if (change.section === "auth.password") {
|
|
10035
|
+
const wireKey = authPasswordWireKey(change.key);
|
|
10036
|
+
await ossFetch("/api/auth/config", {
|
|
10037
|
+
method: "PUT",
|
|
10038
|
+
body: JSON.stringify({ [wireKey]: change.to })
|
|
10039
|
+
});
|
|
10040
|
+
return;
|
|
10041
|
+
}
|
|
10042
|
+
if (change.section === "auth.smtp") {
|
|
10043
|
+
const to = change.to;
|
|
10044
|
+
const body = {
|
|
10045
|
+
enabled: to.enabled,
|
|
10046
|
+
host: to.host,
|
|
10047
|
+
port: to.port,
|
|
10048
|
+
username: to.username,
|
|
10049
|
+
senderEmail: to.sender_email,
|
|
10050
|
+
senderName: to.sender_name,
|
|
10051
|
+
minIntervalSeconds: to.min_interval_seconds
|
|
10052
|
+
};
|
|
10053
|
+
if (change.passwordEnvRef) {
|
|
10054
|
+
const value = await resolveEnvRef(
|
|
10055
|
+
`env(${change.passwordEnvRef})`,
|
|
10056
|
+
"auth.smtp.password"
|
|
10057
|
+
);
|
|
10058
|
+
body.password = value;
|
|
10059
|
+
}
|
|
10060
|
+
await ossFetch("/api/auth/smtp-config", {
|
|
10061
|
+
method: "PUT",
|
|
10062
|
+
body: JSON.stringify(body)
|
|
10063
|
+
});
|
|
10064
|
+
return;
|
|
10065
|
+
}
|
|
10066
|
+
if (change.section === "deployments" && change.key === "subdomain") {
|
|
10067
|
+
await ossFetch("/api/deployments/slug", {
|
|
10068
|
+
method: "PUT",
|
|
10069
|
+
body: JSON.stringify({ slug: change.to })
|
|
10070
|
+
});
|
|
10071
|
+
return;
|
|
10072
|
+
}
|
|
10073
|
+
const _exhaustive = change;
|
|
10074
|
+
throw new Error(`Unsupported change: ${JSON.stringify(_exhaustive)}`);
|
|
9404
10075
|
}
|
|
9405
10076
|
|
|
9406
10077
|
// src/commands/config/index.ts
|
|
@@ -9411,9 +10082,161 @@ function registerConfigCommand(program2) {
|
|
|
9411
10082
|
registerConfigApplyCommand(cfg);
|
|
9412
10083
|
}
|
|
9413
10084
|
|
|
10085
|
+
// src/commands/ai/setup.ts
|
|
10086
|
+
import { appendFileSync as appendFileSync2, existsSync as existsSync15, readFileSync as readFileSync13 } from "fs";
|
|
10087
|
+
import { isAbsolute, join as join17, relative as relative4, resolve as resolve8 } from "path";
|
|
10088
|
+
import * as clack17 from "@clack/prompts";
|
|
10089
|
+
import pc7 from "picocolors";
|
|
10090
|
+
|
|
10091
|
+
// src/lib/api/ai.ts
|
|
10092
|
+
async function getOpenRouterApiKey() {
|
|
10093
|
+
const res = await ossFetch("/api/ai/openrouter/api-key");
|
|
10094
|
+
const data = await res.json();
|
|
10095
|
+
const apiKey = typeof data.apiKey === "string" ? data.apiKey.trim() : "";
|
|
10096
|
+
const maskedKey = typeof data.maskedKey === "string" ? data.maskedKey.trim() : void 0;
|
|
10097
|
+
if (apiKey.length === 0) {
|
|
10098
|
+
throw new CLIError(
|
|
10099
|
+
"AI gateway returned no OpenRouter API key. Open the InsForge dashboard AI page and verify Model Gateway is configured."
|
|
10100
|
+
);
|
|
10101
|
+
}
|
|
10102
|
+
return {
|
|
10103
|
+
apiKey,
|
|
10104
|
+
maskedKey
|
|
10105
|
+
};
|
|
10106
|
+
}
|
|
10107
|
+
|
|
10108
|
+
// src/commands/ai/setup.ts
|
|
10109
|
+
var DEFAULT_ENV_FILE = ".env.local";
|
|
10110
|
+
var OPENROUTER_ENV_KEY = "OPENROUTER_API_KEY";
|
|
10111
|
+
function registerAiSetupCommand(aiCmd2) {
|
|
10112
|
+
aiCmd2.command("setup").description("Write the linked project OpenRouter key to a local env file").option("--env-file <path>", `Env file to update (default: ${DEFAULT_ENV_FILE})`).action(async (opts, cmd) => {
|
|
10113
|
+
const { json } = getRootOpts(cmd);
|
|
10114
|
+
try {
|
|
10115
|
+
const result = await runAiSetup({
|
|
10116
|
+
envFile: opts.envFile,
|
|
10117
|
+
json
|
|
10118
|
+
});
|
|
10119
|
+
if (json) {
|
|
10120
|
+
outputJson({ success: true, ...result });
|
|
10121
|
+
}
|
|
10122
|
+
} catch (err) {
|
|
10123
|
+
handleError(err, json);
|
|
10124
|
+
} finally {
|
|
10125
|
+
await shutdownAnalytics();
|
|
10126
|
+
}
|
|
10127
|
+
});
|
|
10128
|
+
}
|
|
10129
|
+
async function runAiSetup(opts) {
|
|
10130
|
+
const project = getProjectConfig();
|
|
10131
|
+
if (!project) {
|
|
10132
|
+
throw new ProjectNotLinkedError();
|
|
10133
|
+
}
|
|
10134
|
+
if (!opts.json) {
|
|
10135
|
+
clack17.intro("AI setup");
|
|
10136
|
+
outputSuccess(`Linked to InsForge project: ${project.project_name} (${project.project_id})`);
|
|
10137
|
+
}
|
|
10138
|
+
const spinner11 = !opts.json && isInteractive ? clack17.spinner() : null;
|
|
10139
|
+
spinner11?.start("Fetching OpenRouter key...");
|
|
10140
|
+
let key;
|
|
10141
|
+
try {
|
|
10142
|
+
key = await getOpenRouterApiKey();
|
|
10143
|
+
spinner11?.stop("Fetched OpenRouter key.");
|
|
10144
|
+
} catch (err) {
|
|
10145
|
+
spinner11?.stop("Could not fetch OpenRouter key.");
|
|
10146
|
+
throw err;
|
|
10147
|
+
}
|
|
10148
|
+
const envFile = opts.envFile ?? DEFAULT_ENV_FILE;
|
|
10149
|
+
const envPath = resolve8(process.cwd(), envFile);
|
|
10150
|
+
const envLabel = displayPath(envPath);
|
|
10151
|
+
const update = upsertEnvFile(envPath, { [OPENROUTER_ENV_KEY]: key.apiKey });
|
|
10152
|
+
const gitignoreUpdated = ensureLocalEnvIgnored(process.cwd(), envFile);
|
|
10153
|
+
captureEvent(project.project_id, "cli_ai_setup", {
|
|
10154
|
+
project_id: project.project_id,
|
|
10155
|
+
project_name: project.project_name,
|
|
10156
|
+
org_id: project.org_id,
|
|
10157
|
+
region: project.region,
|
|
10158
|
+
env_file: envLabel,
|
|
10159
|
+
added: update.added.includes(OPENROUTER_ENV_KEY),
|
|
10160
|
+
skipped: update.skipped.includes(OPENROUTER_ENV_KEY),
|
|
10161
|
+
mismatched: update.mismatched.some((m) => m.key === OPENROUTER_ENV_KEY)
|
|
10162
|
+
});
|
|
10163
|
+
if (!opts.json) {
|
|
10164
|
+
if (update.added.length > 0) {
|
|
10165
|
+
outputSuccess(`Wrote ${envLabel}: ${update.added.join(", ")}`);
|
|
10166
|
+
}
|
|
10167
|
+
if (update.skipped.length > 0) {
|
|
10168
|
+
outputInfo(pc7.dim(`${envLabel}: ${update.skipped.join(", ")} already set (matching) - left as-is.`));
|
|
10169
|
+
}
|
|
10170
|
+
for (const m of update.mismatched) {
|
|
10171
|
+
clack17.log.warn(
|
|
10172
|
+
`${envLabel} already has ${m.key}; left existing value untouched. Remove it or pass --env-file to write elsewhere.`
|
|
10173
|
+
);
|
|
10174
|
+
}
|
|
10175
|
+
if (gitignoreUpdated) {
|
|
10176
|
+
outputInfo(pc7.dim("Added .env*.local to .gitignore."));
|
|
10177
|
+
}
|
|
10178
|
+
if (!isLocalEnvFile(envFile)) {
|
|
10179
|
+
clack17.log.warn(
|
|
10180
|
+
`${envLabel} may be committed unless it is listed in .gitignore. Keep ${OPENROUTER_ENV_KEY} server-only.`
|
|
10181
|
+
);
|
|
10182
|
+
}
|
|
10183
|
+
outputInfo("");
|
|
10184
|
+
outputInfo("Use this key only from server-side code as process.env.OPENROUTER_API_KEY.");
|
|
10185
|
+
outputInfo("For deployment, add OPENROUTER_API_KEY to your hosting provider environment.");
|
|
10186
|
+
outputInfo(`Do not rename it to ${pc7.bold("NEXT_PUBLIC_")}, ${pc7.bold("VITE_")}, or ${pc7.bold("PUBLIC_")}.`);
|
|
10187
|
+
clack17.outro("Done.");
|
|
10188
|
+
}
|
|
10189
|
+
return {
|
|
10190
|
+
envFile: envLabel,
|
|
10191
|
+
added: update.added,
|
|
10192
|
+
skipped: update.skipped,
|
|
10193
|
+
mismatched: update.mismatched.map((m) => m.key),
|
|
10194
|
+
gitignoreUpdated,
|
|
10195
|
+
maskedKey: key.maskedKey
|
|
10196
|
+
};
|
|
10197
|
+
}
|
|
10198
|
+
function displayPath(path6) {
|
|
10199
|
+
const rel = relative4(process.cwd(), path6);
|
|
10200
|
+
if (!rel || rel.startsWith("..") || isAbsolute(rel)) {
|
|
10201
|
+
return path6;
|
|
10202
|
+
}
|
|
10203
|
+
return rel;
|
|
10204
|
+
}
|
|
10205
|
+
function isLocalEnvFile(envFile) {
|
|
10206
|
+
const normalized = envFile.replace(/\\/g, "/");
|
|
10207
|
+
const basename8 = normalized.split("/").pop() ?? normalized;
|
|
10208
|
+
return basename8 === ".env.local" || /^\.env\..+\.local$/.test(basename8);
|
|
10209
|
+
}
|
|
10210
|
+
function ensureLocalEnvIgnored(cwd, envFile) {
|
|
10211
|
+
if (!isLocalEnvFile(envFile)) return false;
|
|
10212
|
+
const envPath = resolve8(cwd, envFile);
|
|
10213
|
+
const relEnvPath = relative4(cwd, envPath);
|
|
10214
|
+
if (!relEnvPath || relEnvPath.startsWith("..") || isAbsolute(relEnvPath)) {
|
|
10215
|
+
return false;
|
|
10216
|
+
}
|
|
10217
|
+
const gitignorePath = join17(cwd, ".gitignore");
|
|
10218
|
+
const existing = existsSync15(gitignorePath) ? readFileSync13(gitignorePath, "utf-8") : "";
|
|
10219
|
+
const lines = new Set(existing.split(/\r?\n/).map((line) => line.trim()));
|
|
10220
|
+
const envBasename = envFile.replace(/\\/g, "/").split("/").pop() ?? envFile;
|
|
10221
|
+
if (lines.has(".env*") || lines.has(".env.*") || lines.has(".env*.local") || lines.has(".env.local") && envBasename === ".env.local") {
|
|
10222
|
+
return false;
|
|
10223
|
+
}
|
|
10224
|
+
const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
10225
|
+
const spacer = existing.length > 0 ? "\n" : "";
|
|
10226
|
+
appendFileSync2(gitignorePath, `${prefix}${spacer}# Local environment secrets
|
|
10227
|
+
.env*.local
|
|
10228
|
+
`);
|
|
10229
|
+
return true;
|
|
10230
|
+
}
|
|
10231
|
+
|
|
10232
|
+
// src/commands/ai/index.ts
|
|
10233
|
+
function registerAiCommands(aiCmd2) {
|
|
10234
|
+
registerAiSetupCommand(aiCmd2);
|
|
10235
|
+
}
|
|
10236
|
+
|
|
9414
10237
|
// src/index.ts
|
|
9415
10238
|
var __dirname = dirname3(fileURLToPath(import.meta.url));
|
|
9416
|
-
var pkg = JSON.parse(
|
|
10239
|
+
var pkg = JSON.parse(readFileSync14(join18(__dirname, "../package.json"), "utf-8"));
|
|
9417
10240
|
var INSFORGE_LOGO = `
|
|
9418
10241
|
\u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557
|
|
9419
10242
|
\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D
|
|
@@ -9497,6 +10320,8 @@ registerComputeStopCommand(computeCmd);
|
|
|
9497
10320
|
registerComputeEventsCommand(computeCmd);
|
|
9498
10321
|
var posthogCmd = program.command("posthog").description("Manage PostHog product analytics integration");
|
|
9499
10322
|
registerPosthogSetupCommand(posthogCmd);
|
|
10323
|
+
var aiCmd = program.command("ai").description("Manage AI model gateway setup");
|
|
10324
|
+
registerAiCommands(aiCmd);
|
|
9500
10325
|
var schedulesCmd = program.command("schedules").description("Manage scheduled tasks (cron jobs)");
|
|
9501
10326
|
registerSchedulesListCommand(schedulesCmd);
|
|
9502
10327
|
registerSchedulesGetCommand(schedulesCmd);
|
|
@@ -9522,7 +10347,7 @@ async function showInteractiveMenu() {
|
|
|
9522
10347
|
} catch {
|
|
9523
10348
|
}
|
|
9524
10349
|
console.log(INSFORGE_LOGO);
|
|
9525
|
-
|
|
10350
|
+
clack18.intro(`InsForge CLI v${pkg.version}`);
|
|
9526
10351
|
const options = [];
|
|
9527
10352
|
if (!isLoggedIn) {
|
|
9528
10353
|
options.push({ value: "login", label: "Log in to InsForge" });
|
|
@@ -9543,7 +10368,7 @@ async function showInteractiveMenu() {
|
|
|
9543
10368
|
options
|
|
9544
10369
|
});
|
|
9545
10370
|
if (isCancel2(action)) {
|
|
9546
|
-
|
|
10371
|
+
clack18.cancel("Bye!");
|
|
9547
10372
|
process.exit(0);
|
|
9548
10373
|
}
|
|
9549
10374
|
switch (action) {
|