@insforge/cli 0.1.76 → 0.1.78
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 +617 -78
- 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) => {
|
|
@@ -1172,7 +1172,7 @@ import * as clack5 from "@clack/prompts";
|
|
|
1172
1172
|
|
|
1173
1173
|
// src/lib/analytics.ts
|
|
1174
1174
|
import { PostHog } from "posthog-node";
|
|
1175
|
-
var POSTHOG_API_KEY = "
|
|
1175
|
+
var POSTHOG_API_KEY = "";
|
|
1176
1176
|
var POSTHOG_HOST = process.env.POSTHOG_HOST || "https://us.i.posthog.com";
|
|
1177
1177
|
var client = null;
|
|
1178
1178
|
function getClient() {
|
|
@@ -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.78";
|
|
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,70 @@ function validateAuth(input) {
|
|
|
9099
9190
|
}
|
|
9100
9191
|
out.allowed_redirect_urls = v;
|
|
9101
9192
|
}
|
|
9193
|
+
if ("smtp" in obj) out.smtp = validateSmtp(obj.smtp);
|
|
9194
|
+
return out;
|
|
9195
|
+
}
|
|
9196
|
+
function validateSmtp(input) {
|
|
9197
|
+
if (input === null || typeof input !== "object" || Array.isArray(input)) {
|
|
9198
|
+
throw new ConfigValidationError("auth.smtp", "must be a table");
|
|
9199
|
+
}
|
|
9200
|
+
const obj = input;
|
|
9201
|
+
const out = {};
|
|
9202
|
+
if ("enabled" in obj) {
|
|
9203
|
+
if (typeof obj.enabled !== "boolean") {
|
|
9204
|
+
throw new ConfigValidationError("auth.smtp.enabled", "must be a boolean");
|
|
9205
|
+
}
|
|
9206
|
+
out.enabled = obj.enabled;
|
|
9207
|
+
}
|
|
9208
|
+
if ("host" in obj) {
|
|
9209
|
+
if (typeof obj.host !== "string") {
|
|
9210
|
+
throw new ConfigValidationError("auth.smtp.host", "must be a string");
|
|
9211
|
+
}
|
|
9212
|
+
out.host = obj.host;
|
|
9213
|
+
}
|
|
9214
|
+
if ("port" in obj) {
|
|
9215
|
+
if (typeof obj.port !== "number" || !Number.isInteger(obj.port) || obj.port < 1 || obj.port > 65535) {
|
|
9216
|
+
throw new ConfigValidationError(
|
|
9217
|
+
"auth.smtp.port",
|
|
9218
|
+
"must be an integer between 1 and 65535"
|
|
9219
|
+
);
|
|
9220
|
+
}
|
|
9221
|
+
out.port = obj.port;
|
|
9222
|
+
}
|
|
9223
|
+
if ("username" in obj) {
|
|
9224
|
+
if (typeof obj.username !== "string") {
|
|
9225
|
+
throw new ConfigValidationError("auth.smtp.username", "must be a string");
|
|
9226
|
+
}
|
|
9227
|
+
out.username = obj.username;
|
|
9228
|
+
}
|
|
9229
|
+
if ("password" in obj) {
|
|
9230
|
+
out.password = validateSensitiveString(
|
|
9231
|
+
"auth.smtp.password",
|
|
9232
|
+
obj.password,
|
|
9233
|
+
"SMTP_PASSWORD"
|
|
9234
|
+
);
|
|
9235
|
+
}
|
|
9236
|
+
if ("sender_email" in obj) {
|
|
9237
|
+
if (typeof obj.sender_email !== "string") {
|
|
9238
|
+
throw new ConfigValidationError("auth.smtp.sender_email", "must be a string");
|
|
9239
|
+
}
|
|
9240
|
+
out.sender_email = obj.sender_email;
|
|
9241
|
+
}
|
|
9242
|
+
if ("sender_name" in obj) {
|
|
9243
|
+
if (typeof obj.sender_name !== "string") {
|
|
9244
|
+
throw new ConfigValidationError("auth.smtp.sender_name", "must be a string");
|
|
9245
|
+
}
|
|
9246
|
+
out.sender_name = obj.sender_name;
|
|
9247
|
+
}
|
|
9248
|
+
if ("min_interval_seconds" in obj) {
|
|
9249
|
+
if (typeof obj.min_interval_seconds !== "number" || !Number.isInteger(obj.min_interval_seconds) || obj.min_interval_seconds < 0) {
|
|
9250
|
+
throw new ConfigValidationError(
|
|
9251
|
+
"auth.smtp.min_interval_seconds",
|
|
9252
|
+
"must be a non-negative integer"
|
|
9253
|
+
);
|
|
9254
|
+
}
|
|
9255
|
+
out.min_interval_seconds = obj.min_interval_seconds;
|
|
9256
|
+
}
|
|
9102
9257
|
return out;
|
|
9103
9258
|
}
|
|
9104
9259
|
|
|
@@ -9125,9 +9280,43 @@ function stringifyConfigToml(config) {
|
|
|
9125
9280
|
lines.push(`allowed_redirect_urls = [${urls}]`);
|
|
9126
9281
|
}
|
|
9127
9282
|
lines.push("");
|
|
9283
|
+
if (config.auth.smtp !== void 0) {
|
|
9284
|
+
lines.push("[auth.smtp]");
|
|
9285
|
+
renderSmtpFields(config.auth.smtp, lines);
|
|
9286
|
+
lines.push("");
|
|
9287
|
+
}
|
|
9288
|
+
}
|
|
9289
|
+
if (config.deployments) {
|
|
9290
|
+
if (typeof config.deployments.subdomain === "string" && config.deployments.subdomain !== "") {
|
|
9291
|
+
lines.push("[deployments]");
|
|
9292
|
+
lines.push(`subdomain = ${JSON.stringify(config.deployments.subdomain)}`);
|
|
9293
|
+
lines.push("");
|
|
9294
|
+
}
|
|
9128
9295
|
}
|
|
9129
9296
|
return lines.join("\n").replace(/\n+$/, "\n");
|
|
9130
9297
|
}
|
|
9298
|
+
function renderSmtpFields(smtp, lines) {
|
|
9299
|
+
if (smtp.enabled !== void 0) lines.push(`enabled = ${smtp.enabled}`);
|
|
9300
|
+
if (smtp.host !== void 0) lines.push(`host = ${JSON.stringify(smtp.host)}`);
|
|
9301
|
+
if (smtp.port !== void 0) lines.push(`port = ${smtp.port}`);
|
|
9302
|
+
if (smtp.username !== void 0) lines.push(`username = ${JSON.stringify(smtp.username)}`);
|
|
9303
|
+
if (smtp.password !== void 0) {
|
|
9304
|
+
const secretName = parseEnvRef(smtp.password) ?? "SMTP_PASSWORD";
|
|
9305
|
+
lines.push(
|
|
9306
|
+
`# password is managed via secrets \u2014 run \`insforge secrets add ${secretName} "<value>"\``
|
|
9307
|
+
);
|
|
9308
|
+
lines.push(`password = ${JSON.stringify(smtp.password)}`);
|
|
9309
|
+
}
|
|
9310
|
+
if (smtp.sender_email !== void 0) {
|
|
9311
|
+
lines.push(`sender_email = ${JSON.stringify(smtp.sender_email)}`);
|
|
9312
|
+
}
|
|
9313
|
+
if (smtp.sender_name !== void 0) {
|
|
9314
|
+
lines.push(`sender_name = ${JSON.stringify(smtp.sender_name)}`);
|
|
9315
|
+
}
|
|
9316
|
+
if (smtp.min_interval_seconds !== void 0) {
|
|
9317
|
+
lines.push(`min_interval_seconds = ${smtp.min_interval_seconds}`);
|
|
9318
|
+
}
|
|
9319
|
+
}
|
|
9131
9320
|
|
|
9132
9321
|
// src/commands/config/export.ts
|
|
9133
9322
|
function registerConfigExportCommand(cfg) {
|
|
@@ -9159,12 +9348,40 @@ function registerConfigExportCommand(cfg) {
|
|
|
9159
9348
|
const skipped = [];
|
|
9160
9349
|
const authSlice = raw?.auth;
|
|
9161
9350
|
if (authSlice && typeof authSlice === "object" && "allowedRedirectUrls" in authSlice) {
|
|
9162
|
-
config.auth = {
|
|
9163
|
-
|
|
9164
|
-
};
|
|
9351
|
+
config.auth = config.auth ?? {};
|
|
9352
|
+
config.auth.allowed_redirect_urls = authSlice.allowedRedirectUrls ?? [];
|
|
9165
9353
|
} else {
|
|
9166
9354
|
skipped.push("auth.allowed_redirect_urls");
|
|
9167
9355
|
}
|
|
9356
|
+
if (authSlice && typeof authSlice === "object" && "smtpConfig" in authSlice && authSlice.smtpConfig) {
|
|
9357
|
+
const s = authSlice.smtpConfig;
|
|
9358
|
+
config.auth = config.auth ?? {};
|
|
9359
|
+
config.auth.smtp = {
|
|
9360
|
+
enabled: s.enabled ?? false,
|
|
9361
|
+
host: s.host ?? "",
|
|
9362
|
+
port: s.port ?? 587,
|
|
9363
|
+
username: s.username ?? "",
|
|
9364
|
+
// When backend has a password set, emit a deterministic env()
|
|
9365
|
+
// placeholder so the user knows which secret to define. We do
|
|
9366
|
+
// NOT round-trip the value (it never leaves the backend).
|
|
9367
|
+
// Re-applying this TOML force-resends from the secrets store
|
|
9368
|
+
// — see config-diff.ts for the force-resend rationale.
|
|
9369
|
+
...s.hasPassword ? { password: "env(SMTP_PASSWORD)" } : {},
|
|
9370
|
+
sender_email: s.senderEmail ?? "",
|
|
9371
|
+
sender_name: s.senderName ?? "",
|
|
9372
|
+
min_interval_seconds: s.minIntervalSeconds ?? 60
|
|
9373
|
+
};
|
|
9374
|
+
} else {
|
|
9375
|
+
skipped.push("auth.smtp");
|
|
9376
|
+
}
|
|
9377
|
+
const deploymentsSlice = raw?.deployments;
|
|
9378
|
+
if (deploymentsSlice && typeof deploymentsSlice === "object") {
|
|
9379
|
+
if (typeof deploymentsSlice.customSlug === "string" && deploymentsSlice.customSlug) {
|
|
9380
|
+
config.deployments = { subdomain: deploymentsSlice.customSlug };
|
|
9381
|
+
}
|
|
9382
|
+
} else {
|
|
9383
|
+
skipped.push("deployments.subdomain");
|
|
9384
|
+
}
|
|
9168
9385
|
const toml = stringifyConfigToml(config);
|
|
9169
9386
|
writeFileSync9(target, toml, "utf8");
|
|
9170
9387
|
if (json) {
|
|
@@ -9210,8 +9427,86 @@ function diffConfig({ live, file }) {
|
|
|
9210
9427
|
});
|
|
9211
9428
|
}
|
|
9212
9429
|
}
|
|
9430
|
+
if (fileAuth?.smtp !== void 0) {
|
|
9431
|
+
const smtpChange = diffSmtp(liveAuth.smtp, fileAuth.smtp);
|
|
9432
|
+
if (smtpChange) changes.push(smtpChange);
|
|
9433
|
+
}
|
|
9434
|
+
const fileDeployments = file.deployments;
|
|
9435
|
+
const liveDeployments = live.deployments ?? {};
|
|
9436
|
+
if (fileDeployments && "subdomain" in fileDeployments) {
|
|
9437
|
+
const fromV = liveDeployments.subdomain ?? null;
|
|
9438
|
+
const rawTo = fileDeployments.subdomain;
|
|
9439
|
+
const toV = rawTo === null || rawTo === "" ? null : rawTo;
|
|
9440
|
+
if (fromV !== toV) {
|
|
9441
|
+
changes.push({
|
|
9442
|
+
section: "deployments",
|
|
9443
|
+
op: "modify",
|
|
9444
|
+
key: "subdomain",
|
|
9445
|
+
from: fromV,
|
|
9446
|
+
to: toV
|
|
9447
|
+
});
|
|
9448
|
+
}
|
|
9449
|
+
}
|
|
9213
9450
|
return { changes, summary: summarize(changes) };
|
|
9214
9451
|
}
|
|
9452
|
+
function diffSmtp(live, fileSmtp) {
|
|
9453
|
+
const livedView = renderLiveSmtp(live);
|
|
9454
|
+
const tomlView = renderFileSmtp(fileSmtp);
|
|
9455
|
+
const envRef = fileSmtp.password ? parseEnvRef(fileSmtp.password) : null;
|
|
9456
|
+
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;
|
|
9457
|
+
if (!nonPasswordFieldsChanged && envRef === null) {
|
|
9458
|
+
return null;
|
|
9459
|
+
}
|
|
9460
|
+
return {
|
|
9461
|
+
section: "auth.smtp",
|
|
9462
|
+
op: "modify",
|
|
9463
|
+
key: "config",
|
|
9464
|
+
from: livedView,
|
|
9465
|
+
to: tomlView,
|
|
9466
|
+
passwordEnvRef: envRef ?? void 0
|
|
9467
|
+
};
|
|
9468
|
+
}
|
|
9469
|
+
function renderLiveSmtp(live) {
|
|
9470
|
+
const empty = EMPTY_SMTP_VIEW;
|
|
9471
|
+
if (!live) return empty;
|
|
9472
|
+
return {
|
|
9473
|
+
enabled: live.enabled,
|
|
9474
|
+
host: live.host,
|
|
9475
|
+
port: live.port,
|
|
9476
|
+
username: live.username,
|
|
9477
|
+
password: live.hasPassword ? "(set)" : "(unset)",
|
|
9478
|
+
sender_email: live.sender_email,
|
|
9479
|
+
sender_name: live.sender_name,
|
|
9480
|
+
min_interval_seconds: live.min_interval_seconds
|
|
9481
|
+
};
|
|
9482
|
+
}
|
|
9483
|
+
function renderFileSmtp(file) {
|
|
9484
|
+
return {
|
|
9485
|
+
enabled: file.enabled ?? false,
|
|
9486
|
+
host: file.host ?? "",
|
|
9487
|
+
port: file.port ?? 587,
|
|
9488
|
+
username: file.username ?? "",
|
|
9489
|
+
password: renderFilePassword(file.password),
|
|
9490
|
+
sender_email: file.sender_email ?? "",
|
|
9491
|
+
sender_name: file.sender_name ?? "",
|
|
9492
|
+
min_interval_seconds: file.min_interval_seconds ?? 60
|
|
9493
|
+
};
|
|
9494
|
+
}
|
|
9495
|
+
function renderFilePassword(value) {
|
|
9496
|
+
if (value === void 0) return "(unchanged)";
|
|
9497
|
+
const ref = parseEnvRef(value);
|
|
9498
|
+
return ref ? `env(${ref})` : "(invalid)";
|
|
9499
|
+
}
|
|
9500
|
+
var EMPTY_SMTP_VIEW = {
|
|
9501
|
+
enabled: false,
|
|
9502
|
+
host: "",
|
|
9503
|
+
port: 587,
|
|
9504
|
+
username: "",
|
|
9505
|
+
password: "(unset)",
|
|
9506
|
+
sender_email: "",
|
|
9507
|
+
sender_name: "",
|
|
9508
|
+
min_interval_seconds: 60
|
|
9509
|
+
};
|
|
9215
9510
|
function summarize(changes) {
|
|
9216
9511
|
const s = { add: 0, modify: 0, remove: 0, kept: 0 };
|
|
9217
9512
|
for (const c of changes) {
|
|
@@ -9253,6 +9548,34 @@ function formatPlan(result) {
|
|
|
9253
9548
|
return lines.join("\n");
|
|
9254
9549
|
}
|
|
9255
9550
|
function formatChange(c) {
|
|
9551
|
+
if (c.section === "auth.smtp") {
|
|
9552
|
+
const lines = [`~ smtp config:`];
|
|
9553
|
+
const from = c.from;
|
|
9554
|
+
const to = c.to;
|
|
9555
|
+
for (const key of [
|
|
9556
|
+
"enabled",
|
|
9557
|
+
"host",
|
|
9558
|
+
"port",
|
|
9559
|
+
"username",
|
|
9560
|
+
"password",
|
|
9561
|
+
"sender_email",
|
|
9562
|
+
"sender_name",
|
|
9563
|
+
"min_interval_seconds"
|
|
9564
|
+
]) {
|
|
9565
|
+
if (from[key] !== to[key]) {
|
|
9566
|
+
lines.push(` ${key}: ${JSON.stringify(from[key])} \u2192 ${JSON.stringify(to[key])}`);
|
|
9567
|
+
}
|
|
9568
|
+
}
|
|
9569
|
+
if (c.passwordEnvRef) {
|
|
9570
|
+
lines.push(` (password force-resent from env(${c.passwordEnvRef}))`);
|
|
9571
|
+
}
|
|
9572
|
+
return lines.join("\n ");
|
|
9573
|
+
}
|
|
9574
|
+
if (c.section === "deployments" && c.key === "subdomain") {
|
|
9575
|
+
const fromLabel = c.from === null ? "(unset)" : JSON.stringify(c.from);
|
|
9576
|
+
const toLabel = c.to === null ? "(unset)" : JSON.stringify(c.to);
|
|
9577
|
+
return `~ ${c.key}: ${fromLabel} \u2192 ${toLabel}`;
|
|
9578
|
+
}
|
|
9256
9579
|
return `~ ${c.key}: ${JSON.stringify(c.from)} \u2192 ${JSON.stringify(c.to)}`;
|
|
9257
9580
|
}
|
|
9258
9581
|
|
|
@@ -9261,9 +9584,18 @@ function metadataSupports(raw, change) {
|
|
|
9261
9584
|
if (change.section === "auth" && change.key === "allowed_redirect_urls") {
|
|
9262
9585
|
return raw?.auth !== void 0 && raw.auth !== null && typeof raw.auth === "object" && "allowedRedirectUrls" in raw.auth;
|
|
9263
9586
|
}
|
|
9587
|
+
if (change.section === "auth.smtp") {
|
|
9588
|
+
return raw?.auth !== void 0 && raw.auth !== null && typeof raw.auth === "object" && "smtpConfig" in raw.auth;
|
|
9589
|
+
}
|
|
9590
|
+
if (change.section === "deployments" && change.key === "subdomain") {
|
|
9591
|
+
return raw?.deployments !== void 0 && raw.deployments !== null && typeof raw.deployments === "object";
|
|
9592
|
+
}
|
|
9593
|
+
const _exhaustive = change;
|
|
9594
|
+
void _exhaustive;
|
|
9264
9595
|
return false;
|
|
9265
9596
|
}
|
|
9266
9597
|
function changePath(change) {
|
|
9598
|
+
if (change.section === "auth.smtp") return "auth.smtp";
|
|
9267
9599
|
return `${change.section}.${change.key}`;
|
|
9268
9600
|
}
|
|
9269
9601
|
|
|
@@ -9318,9 +9650,7 @@ function registerConfigApplyCommand(cfg) {
|
|
|
9318
9650
|
const file = parseConfigToml(tomlSource);
|
|
9319
9651
|
const res = await ossFetch("/api/metadata");
|
|
9320
9652
|
const raw = await res.json();
|
|
9321
|
-
const live =
|
|
9322
|
-
auth: { allowed_redirect_urls: raw.auth?.allowedRedirectUrls ?? [] }
|
|
9323
|
-
};
|
|
9653
|
+
const live = liveFromMetadata(raw);
|
|
9324
9654
|
const result = diffConfig({ live, file });
|
|
9325
9655
|
const approved = opts.autoApprove || yes;
|
|
9326
9656
|
if (!json) {
|
|
@@ -9392,6 +9722,29 @@ function registerConfigApplyCommand(cfg) {
|
|
|
9392
9722
|
}
|
|
9393
9723
|
});
|
|
9394
9724
|
}
|
|
9725
|
+
function liveFromMetadata(raw) {
|
|
9726
|
+
const live = { auth: {} };
|
|
9727
|
+
if (raw.auth?.allowedRedirectUrls !== void 0) {
|
|
9728
|
+
live.auth.allowed_redirect_urls = raw.auth.allowedRedirectUrls;
|
|
9729
|
+
}
|
|
9730
|
+
if (raw.auth?.smtpConfig) {
|
|
9731
|
+
const s = raw.auth.smtpConfig;
|
|
9732
|
+
live.auth.smtp = {
|
|
9733
|
+
enabled: s.enabled ?? false,
|
|
9734
|
+
host: s.host ?? "",
|
|
9735
|
+
port: s.port ?? 587,
|
|
9736
|
+
username: s.username ?? "",
|
|
9737
|
+
hasPassword: s.hasPassword ?? false,
|
|
9738
|
+
sender_email: s.senderEmail ?? "",
|
|
9739
|
+
sender_name: s.senderName ?? "",
|
|
9740
|
+
min_interval_seconds: s.minIntervalSeconds ?? 60
|
|
9741
|
+
};
|
|
9742
|
+
}
|
|
9743
|
+
if (raw.deployments) {
|
|
9744
|
+
live.deployments = { subdomain: raw.deployments.customSlug ?? null };
|
|
9745
|
+
}
|
|
9746
|
+
return live;
|
|
9747
|
+
}
|
|
9395
9748
|
async function applyChange(change) {
|
|
9396
9749
|
if (change.section === "auth" && change.key === "allowed_redirect_urls") {
|
|
9397
9750
|
await ossFetch("/api/auth/config", {
|
|
@@ -9400,7 +9753,39 @@ async function applyChange(change) {
|
|
|
9400
9753
|
});
|
|
9401
9754
|
return;
|
|
9402
9755
|
}
|
|
9403
|
-
|
|
9756
|
+
if (change.section === "auth.smtp") {
|
|
9757
|
+
const to = change.to;
|
|
9758
|
+
const body = {
|
|
9759
|
+
enabled: to.enabled,
|
|
9760
|
+
host: to.host,
|
|
9761
|
+
port: to.port,
|
|
9762
|
+
username: to.username,
|
|
9763
|
+
senderEmail: to.sender_email,
|
|
9764
|
+
senderName: to.sender_name,
|
|
9765
|
+
minIntervalSeconds: to.min_interval_seconds
|
|
9766
|
+
};
|
|
9767
|
+
if (change.passwordEnvRef) {
|
|
9768
|
+
const value = await resolveEnvRef(
|
|
9769
|
+
`env(${change.passwordEnvRef})`,
|
|
9770
|
+
"auth.smtp.password"
|
|
9771
|
+
);
|
|
9772
|
+
body.password = value;
|
|
9773
|
+
}
|
|
9774
|
+
await ossFetch("/api/auth/smtp-config", {
|
|
9775
|
+
method: "PUT",
|
|
9776
|
+
body: JSON.stringify(body)
|
|
9777
|
+
});
|
|
9778
|
+
return;
|
|
9779
|
+
}
|
|
9780
|
+
if (change.section === "deployments" && change.key === "subdomain") {
|
|
9781
|
+
await ossFetch("/api/deployments/slug", {
|
|
9782
|
+
method: "PUT",
|
|
9783
|
+
body: JSON.stringify({ slug: change.to })
|
|
9784
|
+
});
|
|
9785
|
+
return;
|
|
9786
|
+
}
|
|
9787
|
+
const _exhaustive = change;
|
|
9788
|
+
throw new Error(`Unsupported change: ${JSON.stringify(_exhaustive)}`);
|
|
9404
9789
|
}
|
|
9405
9790
|
|
|
9406
9791
|
// src/commands/config/index.ts
|
|
@@ -9411,9 +9796,161 @@ function registerConfigCommand(program2) {
|
|
|
9411
9796
|
registerConfigApplyCommand(cfg);
|
|
9412
9797
|
}
|
|
9413
9798
|
|
|
9799
|
+
// src/commands/ai/setup.ts
|
|
9800
|
+
import { appendFileSync as appendFileSync2, existsSync as existsSync15, readFileSync as readFileSync13 } from "fs";
|
|
9801
|
+
import { isAbsolute, join as join17, relative as relative4, resolve as resolve8 } from "path";
|
|
9802
|
+
import * as clack17 from "@clack/prompts";
|
|
9803
|
+
import pc7 from "picocolors";
|
|
9804
|
+
|
|
9805
|
+
// src/lib/api/ai.ts
|
|
9806
|
+
async function getOpenRouterApiKey() {
|
|
9807
|
+
const res = await ossFetch("/api/ai/openrouter/api-key");
|
|
9808
|
+
const data = await res.json();
|
|
9809
|
+
const apiKey = typeof data.apiKey === "string" ? data.apiKey.trim() : "";
|
|
9810
|
+
const maskedKey = typeof data.maskedKey === "string" ? data.maskedKey.trim() : void 0;
|
|
9811
|
+
if (apiKey.length === 0) {
|
|
9812
|
+
throw new CLIError(
|
|
9813
|
+
"AI gateway returned no OpenRouter API key. Open the InsForge dashboard AI page and verify Model Gateway is configured."
|
|
9814
|
+
);
|
|
9815
|
+
}
|
|
9816
|
+
return {
|
|
9817
|
+
apiKey,
|
|
9818
|
+
maskedKey
|
|
9819
|
+
};
|
|
9820
|
+
}
|
|
9821
|
+
|
|
9822
|
+
// src/commands/ai/setup.ts
|
|
9823
|
+
var DEFAULT_ENV_FILE = ".env.local";
|
|
9824
|
+
var OPENROUTER_ENV_KEY = "OPENROUTER_API_KEY";
|
|
9825
|
+
function registerAiSetupCommand(aiCmd2) {
|
|
9826
|
+
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) => {
|
|
9827
|
+
const { json } = getRootOpts(cmd);
|
|
9828
|
+
try {
|
|
9829
|
+
const result = await runAiSetup({
|
|
9830
|
+
envFile: opts.envFile,
|
|
9831
|
+
json
|
|
9832
|
+
});
|
|
9833
|
+
if (json) {
|
|
9834
|
+
outputJson({ success: true, ...result });
|
|
9835
|
+
}
|
|
9836
|
+
} catch (err) {
|
|
9837
|
+
handleError(err, json);
|
|
9838
|
+
} finally {
|
|
9839
|
+
await shutdownAnalytics();
|
|
9840
|
+
}
|
|
9841
|
+
});
|
|
9842
|
+
}
|
|
9843
|
+
async function runAiSetup(opts) {
|
|
9844
|
+
const project = getProjectConfig();
|
|
9845
|
+
if (!project) {
|
|
9846
|
+
throw new ProjectNotLinkedError();
|
|
9847
|
+
}
|
|
9848
|
+
if (!opts.json) {
|
|
9849
|
+
clack17.intro("AI setup");
|
|
9850
|
+
outputSuccess(`Linked to InsForge project: ${project.project_name} (${project.project_id})`);
|
|
9851
|
+
}
|
|
9852
|
+
const spinner11 = !opts.json && isInteractive ? clack17.spinner() : null;
|
|
9853
|
+
spinner11?.start("Fetching OpenRouter key...");
|
|
9854
|
+
let key;
|
|
9855
|
+
try {
|
|
9856
|
+
key = await getOpenRouterApiKey();
|
|
9857
|
+
spinner11?.stop("Fetched OpenRouter key.");
|
|
9858
|
+
} catch (err) {
|
|
9859
|
+
spinner11?.stop("Could not fetch OpenRouter key.");
|
|
9860
|
+
throw err;
|
|
9861
|
+
}
|
|
9862
|
+
const envFile = opts.envFile ?? DEFAULT_ENV_FILE;
|
|
9863
|
+
const envPath = resolve8(process.cwd(), envFile);
|
|
9864
|
+
const envLabel = displayPath(envPath);
|
|
9865
|
+
const update = upsertEnvFile(envPath, { [OPENROUTER_ENV_KEY]: key.apiKey });
|
|
9866
|
+
const gitignoreUpdated = ensureLocalEnvIgnored(process.cwd(), envFile);
|
|
9867
|
+
captureEvent(project.project_id, "cli_ai_setup", {
|
|
9868
|
+
project_id: project.project_id,
|
|
9869
|
+
project_name: project.project_name,
|
|
9870
|
+
org_id: project.org_id,
|
|
9871
|
+
region: project.region,
|
|
9872
|
+
env_file: envLabel,
|
|
9873
|
+
added: update.added.includes(OPENROUTER_ENV_KEY),
|
|
9874
|
+
skipped: update.skipped.includes(OPENROUTER_ENV_KEY),
|
|
9875
|
+
mismatched: update.mismatched.some((m) => m.key === OPENROUTER_ENV_KEY)
|
|
9876
|
+
});
|
|
9877
|
+
if (!opts.json) {
|
|
9878
|
+
if (update.added.length > 0) {
|
|
9879
|
+
outputSuccess(`Wrote ${envLabel}: ${update.added.join(", ")}`);
|
|
9880
|
+
}
|
|
9881
|
+
if (update.skipped.length > 0) {
|
|
9882
|
+
outputInfo(pc7.dim(`${envLabel}: ${update.skipped.join(", ")} already set (matching) - left as-is.`));
|
|
9883
|
+
}
|
|
9884
|
+
for (const m of update.mismatched) {
|
|
9885
|
+
clack17.log.warn(
|
|
9886
|
+
`${envLabel} already has ${m.key}; left existing value untouched. Remove it or pass --env-file to write elsewhere.`
|
|
9887
|
+
);
|
|
9888
|
+
}
|
|
9889
|
+
if (gitignoreUpdated) {
|
|
9890
|
+
outputInfo(pc7.dim("Added .env*.local to .gitignore."));
|
|
9891
|
+
}
|
|
9892
|
+
if (!isLocalEnvFile(envFile)) {
|
|
9893
|
+
clack17.log.warn(
|
|
9894
|
+
`${envLabel} may be committed unless it is listed in .gitignore. Keep ${OPENROUTER_ENV_KEY} server-only.`
|
|
9895
|
+
);
|
|
9896
|
+
}
|
|
9897
|
+
outputInfo("");
|
|
9898
|
+
outputInfo("Use this key only from server-side code as process.env.OPENROUTER_API_KEY.");
|
|
9899
|
+
outputInfo("For deployment, add OPENROUTER_API_KEY to your hosting provider environment.");
|
|
9900
|
+
outputInfo(`Do not rename it to ${pc7.bold("NEXT_PUBLIC_")}, ${pc7.bold("VITE_")}, or ${pc7.bold("PUBLIC_")}.`);
|
|
9901
|
+
clack17.outro("Done.");
|
|
9902
|
+
}
|
|
9903
|
+
return {
|
|
9904
|
+
envFile: envLabel,
|
|
9905
|
+
added: update.added,
|
|
9906
|
+
skipped: update.skipped,
|
|
9907
|
+
mismatched: update.mismatched.map((m) => m.key),
|
|
9908
|
+
gitignoreUpdated,
|
|
9909
|
+
maskedKey: key.maskedKey
|
|
9910
|
+
};
|
|
9911
|
+
}
|
|
9912
|
+
function displayPath(path6) {
|
|
9913
|
+
const rel = relative4(process.cwd(), path6);
|
|
9914
|
+
if (!rel || rel.startsWith("..") || isAbsolute(rel)) {
|
|
9915
|
+
return path6;
|
|
9916
|
+
}
|
|
9917
|
+
return rel;
|
|
9918
|
+
}
|
|
9919
|
+
function isLocalEnvFile(envFile) {
|
|
9920
|
+
const normalized = envFile.replace(/\\/g, "/");
|
|
9921
|
+
const basename8 = normalized.split("/").pop() ?? normalized;
|
|
9922
|
+
return basename8 === ".env.local" || /^\.env\..+\.local$/.test(basename8);
|
|
9923
|
+
}
|
|
9924
|
+
function ensureLocalEnvIgnored(cwd, envFile) {
|
|
9925
|
+
if (!isLocalEnvFile(envFile)) return false;
|
|
9926
|
+
const envPath = resolve8(cwd, envFile);
|
|
9927
|
+
const relEnvPath = relative4(cwd, envPath);
|
|
9928
|
+
if (!relEnvPath || relEnvPath.startsWith("..") || isAbsolute(relEnvPath)) {
|
|
9929
|
+
return false;
|
|
9930
|
+
}
|
|
9931
|
+
const gitignorePath = join17(cwd, ".gitignore");
|
|
9932
|
+
const existing = existsSync15(gitignorePath) ? readFileSync13(gitignorePath, "utf-8") : "";
|
|
9933
|
+
const lines = new Set(existing.split(/\r?\n/).map((line) => line.trim()));
|
|
9934
|
+
const envBasename = envFile.replace(/\\/g, "/").split("/").pop() ?? envFile;
|
|
9935
|
+
if (lines.has(".env*") || lines.has(".env.*") || lines.has(".env*.local") || lines.has(".env.local") && envBasename === ".env.local") {
|
|
9936
|
+
return false;
|
|
9937
|
+
}
|
|
9938
|
+
const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
9939
|
+
const spacer = existing.length > 0 ? "\n" : "";
|
|
9940
|
+
appendFileSync2(gitignorePath, `${prefix}${spacer}# Local environment secrets
|
|
9941
|
+
.env*.local
|
|
9942
|
+
`);
|
|
9943
|
+
return true;
|
|
9944
|
+
}
|
|
9945
|
+
|
|
9946
|
+
// src/commands/ai/index.ts
|
|
9947
|
+
function registerAiCommands(aiCmd2) {
|
|
9948
|
+
registerAiSetupCommand(aiCmd2);
|
|
9949
|
+
}
|
|
9950
|
+
|
|
9414
9951
|
// src/index.ts
|
|
9415
9952
|
var __dirname = dirname3(fileURLToPath(import.meta.url));
|
|
9416
|
-
var pkg = JSON.parse(
|
|
9953
|
+
var pkg = JSON.parse(readFileSync14(join18(__dirname, "../package.json"), "utf-8"));
|
|
9417
9954
|
var INSFORGE_LOGO = `
|
|
9418
9955
|
\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
9956
|
\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 +10034,8 @@ registerComputeStopCommand(computeCmd);
|
|
|
9497
10034
|
registerComputeEventsCommand(computeCmd);
|
|
9498
10035
|
var posthogCmd = program.command("posthog").description("Manage PostHog product analytics integration");
|
|
9499
10036
|
registerPosthogSetupCommand(posthogCmd);
|
|
10037
|
+
var aiCmd = program.command("ai").description("Manage AI model gateway setup");
|
|
10038
|
+
registerAiCommands(aiCmd);
|
|
9500
10039
|
var schedulesCmd = program.command("schedules").description("Manage scheduled tasks (cron jobs)");
|
|
9501
10040
|
registerSchedulesListCommand(schedulesCmd);
|
|
9502
10041
|
registerSchedulesGetCommand(schedulesCmd);
|
|
@@ -9522,7 +10061,7 @@ async function showInteractiveMenu() {
|
|
|
9522
10061
|
} catch {
|
|
9523
10062
|
}
|
|
9524
10063
|
console.log(INSFORGE_LOGO);
|
|
9525
|
-
|
|
10064
|
+
clack18.intro(`InsForge CLI v${pkg.version}`);
|
|
9526
10065
|
const options = [];
|
|
9527
10066
|
if (!isLoggedIn) {
|
|
9528
10067
|
options.push({ value: "login", label: "Log in to InsForge" });
|
|
@@ -9543,7 +10082,7 @@ async function showInteractiveMenu() {
|
|
|
9543
10082
|
options
|
|
9544
10083
|
});
|
|
9545
10084
|
if (isCancel2(action)) {
|
|
9546
|
-
|
|
10085
|
+
clack18.cancel("Bye!");
|
|
9547
10086
|
process.exit(0);
|
|
9548
10087
|
}
|
|
9549
10088
|
switch (action) {
|