@insforge/cli 0.1.74 → 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 +1036 -116
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
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) => {
|
|
@@ -1158,7 +1158,7 @@ function registerProjectsCommands(projectsCmd2) {
|
|
|
1158
1158
|
}
|
|
1159
1159
|
outputTable(
|
|
1160
1160
|
["ID", "Name", "Region", "Status", "AppKey"],
|
|
1161
|
-
projects.map((
|
|
1161
|
+
projects.map((p3) => [p3.id, p3.name, p3.region, p3.status, p3.appkey])
|
|
1162
1162
|
);
|
|
1163
1163
|
}
|
|
1164
1164
|
} catch (err) {
|
|
@@ -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));
|
|
@@ -1888,18 +1888,34 @@ async function getJwtSecret() {
|
|
|
1888
1888
|
function spliceDatabasePassword(maskedUrl, password3) {
|
|
1889
1889
|
return maskedUrl.replace(/^(postgresql:\/\/[^:]+:)[^@]+(@)/, `$1${password3}$2`);
|
|
1890
1890
|
}
|
|
1891
|
+
function isMaskedDatabasePassword(value) {
|
|
1892
|
+
return /^\*+$/.test(value);
|
|
1893
|
+
}
|
|
1894
|
+
async function fetchDatabasePasswordOnce() {
|
|
1895
|
+
try {
|
|
1896
|
+
const res = await ossFetch("/api/metadata/database-password");
|
|
1897
|
+
const body = await res.json();
|
|
1898
|
+
const pw = body.databasePassword;
|
|
1899
|
+
if (typeof pw !== "string" || !pw || isMaskedDatabasePassword(pw)) return null;
|
|
1900
|
+
return pw;
|
|
1901
|
+
} catch {
|
|
1902
|
+
return null;
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1891
1905
|
async function getDatabaseConnectionString() {
|
|
1892
1906
|
try {
|
|
1893
|
-
const
|
|
1894
|
-
ossFetch("/api/metadata/database-connection-string"),
|
|
1895
|
-
ossFetch("/api/metadata/database-password")
|
|
1896
|
-
]);
|
|
1907
|
+
const urlRes = await ossFetch("/api/metadata/database-connection-string");
|
|
1897
1908
|
const urlBody = await urlRes.json();
|
|
1898
|
-
const pwBody = await pwRes.json();
|
|
1899
1909
|
const masked = urlBody.connectionURL;
|
|
1900
|
-
const password3 = pwBody.databasePassword;
|
|
1901
1910
|
if (typeof masked !== "string" || !masked) return null;
|
|
1902
|
-
|
|
1911
|
+
let password3 = await fetchDatabasePasswordOnce();
|
|
1912
|
+
const POLL_ATTEMPTS = 9;
|
|
1913
|
+
const POLL_DELAY_MS = 2e3;
|
|
1914
|
+
for (let attempt = 0; password3 === null && attempt < POLL_ATTEMPTS; attempt++) {
|
|
1915
|
+
await new Promise((r) => setTimeout(r, POLL_DELAY_MS));
|
|
1916
|
+
password3 = await fetchDatabasePasswordOnce();
|
|
1917
|
+
}
|
|
1918
|
+
if (password3 === null) return null;
|
|
1903
1919
|
return spliceDatabasePassword(masked, password3);
|
|
1904
1920
|
} catch {
|
|
1905
1921
|
return null;
|
|
@@ -1930,6 +1946,9 @@ ${err.nextActions}`;
|
|
|
1930
1946
|
if (res.status === 404 && isRouteLevel404 && path6 === "/api/database/migrations") {
|
|
1931
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.";
|
|
1932
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
|
+
}
|
|
1933
1952
|
throw new CLIError(message);
|
|
1934
1953
|
}
|
|
1935
1954
|
return res;
|
|
@@ -1938,8 +1957,8 @@ ${err.nextActions}`;
|
|
|
1938
1957
|
// src/auth-providers/apply.ts
|
|
1939
1958
|
var execFileAsync = promisify2(execFile);
|
|
1940
1959
|
var VALID_AUTH_PROVIDERS = ["better-auth"];
|
|
1941
|
-
function pathExists(
|
|
1942
|
-
return fs.stat(
|
|
1960
|
+
function pathExists(p3) {
|
|
1961
|
+
return fs.stat(p3).then(() => true, () => false);
|
|
1943
1962
|
}
|
|
1944
1963
|
function deepMergeKeepBase(base, patch) {
|
|
1945
1964
|
const out = { ...base };
|
|
@@ -2330,11 +2349,11 @@ async function collectDeploymentFiles(sourceDir) {
|
|
|
2330
2349
|
return files;
|
|
2331
2350
|
}
|
|
2332
2351
|
async function createZipBuffer(sourceDir) {
|
|
2333
|
-
return new Promise((
|
|
2352
|
+
return new Promise((resolve9, reject) => {
|
|
2334
2353
|
const archive = archiver("zip", { zlib: { level: 9 } });
|
|
2335
2354
|
const chunks = [];
|
|
2336
2355
|
archive.on("data", (chunk) => chunks.push(chunk));
|
|
2337
|
-
archive.on("end", () =>
|
|
2356
|
+
archive.on("end", () => resolve9(Buffer.concat(chunks)));
|
|
2338
2357
|
archive.on("error", (err) => reject(err));
|
|
2339
2358
|
archive.directory(sourceDir, false, (entry) => {
|
|
2340
2359
|
if (shouldExclude(entry.name)) return false;
|
|
@@ -2411,12 +2430,12 @@ async function startDirectDeployment(deploymentId, startBody) {
|
|
|
2411
2430
|
});
|
|
2412
2431
|
await response.json();
|
|
2413
2432
|
}
|
|
2414
|
-
async function pollDeployment(deploymentId,
|
|
2415
|
-
|
|
2433
|
+
async function pollDeployment(deploymentId, spinner11, syncBeforeRead) {
|
|
2434
|
+
spinner11?.message("Building and deploying...");
|
|
2416
2435
|
const startTime = Date.now();
|
|
2417
2436
|
let deployment = null;
|
|
2418
2437
|
while (Date.now() - startTime < POLL_TIMEOUT_MS3) {
|
|
2419
|
-
await new Promise((
|
|
2438
|
+
await new Promise((resolve9) => setTimeout(resolve9, POLL_INTERVAL_MS3));
|
|
2420
2439
|
try {
|
|
2421
2440
|
if (syncBeforeRead) {
|
|
2422
2441
|
await ossFetch(`/api/deployments/${deploymentId}/sync`, { method: "POST" });
|
|
@@ -2428,13 +2447,13 @@ async function pollDeployment(deploymentId, spinner10, syncBeforeRead) {
|
|
|
2428
2447
|
break;
|
|
2429
2448
|
}
|
|
2430
2449
|
if (status === "ERROR" || status === "CANCELED") {
|
|
2431
|
-
|
|
2450
|
+
spinner11?.stop("Deployment failed");
|
|
2432
2451
|
throw new CLIError(
|
|
2433
2452
|
getDeploymentError(deployment.metadata) ?? `Deployment failed with status: ${deployment.status}`
|
|
2434
2453
|
);
|
|
2435
2454
|
}
|
|
2436
2455
|
const elapsed = Math.round((Date.now() - startTime) / 1e3);
|
|
2437
|
-
|
|
2456
|
+
spinner11?.message(`Building and deploying... (${elapsed}s, status: ${deployment.status})`);
|
|
2438
2457
|
} catch (err) {
|
|
2439
2458
|
if (err instanceof CLIError) throw err;
|
|
2440
2459
|
}
|
|
@@ -2444,20 +2463,20 @@ async function pollDeployment(deploymentId, spinner10, syncBeforeRead) {
|
|
|
2444
2463
|
return { deploymentId, deployment, isReady, liveUrl };
|
|
2445
2464
|
}
|
|
2446
2465
|
async function deployProjectDirect(opts, config) {
|
|
2447
|
-
const { sourceDir, startBody = {}, spinner:
|
|
2448
|
-
|
|
2466
|
+
const { sourceDir, startBody = {}, spinner: spinner11 } = opts;
|
|
2467
|
+
spinner11?.start("Scanning source files...");
|
|
2449
2468
|
const localFiles = await collectDeploymentFiles(sourceDir);
|
|
2450
2469
|
if (localFiles.length === 0) {
|
|
2451
2470
|
throw new CLIError("No deployable files found in the source directory.");
|
|
2452
2471
|
}
|
|
2453
|
-
|
|
2472
|
+
spinner11?.message("Creating deployment...");
|
|
2454
2473
|
const createResult = await createDirectDeploymentSession(
|
|
2455
2474
|
config,
|
|
2456
2475
|
localFiles.map(({ path: relativePath, sha, size }) => ({ path: relativePath, sha, size }))
|
|
2457
2476
|
);
|
|
2458
2477
|
const localFileByPath = new Map(localFiles.map((file) => [file.path, file]));
|
|
2459
2478
|
const pendingFiles = createResult.files.filter((file) => !file.uploadedAt);
|
|
2460
|
-
|
|
2479
|
+
spinner11?.message(`Uploading ${pendingFiles.length} file${pendingFiles.length === 1 ? "" : "s"}...`);
|
|
2461
2480
|
await runWithConcurrency(pendingFiles, DIRECT_UPLOAD_CONCURRENCY, async (manifestFile) => {
|
|
2462
2481
|
const localFile = localFileByPath.get(manifestFile.path);
|
|
2463
2482
|
if (!localFile) {
|
|
@@ -2468,18 +2487,18 @@ async function deployProjectDirect(opts, config) {
|
|
|
2468
2487
|
}
|
|
2469
2488
|
await uploadDirectDeploymentFile(createResult.id, manifestFile, localFile);
|
|
2470
2489
|
});
|
|
2471
|
-
|
|
2490
|
+
spinner11?.message("Starting deployment...");
|
|
2472
2491
|
await startDirectDeployment(createResult.id, startBody);
|
|
2473
|
-
return await pollDeployment(createResult.id,
|
|
2492
|
+
return await pollDeployment(createResult.id, spinner11, !isInsforgeCloudOssHost(config.oss_host));
|
|
2474
2493
|
}
|
|
2475
2494
|
async function deployProjectLegacy(opts) {
|
|
2476
|
-
const { sourceDir, startBody = {}, spinner:
|
|
2477
|
-
|
|
2495
|
+
const { sourceDir, startBody = {}, spinner: spinner11 } = opts;
|
|
2496
|
+
spinner11?.message("Creating deployment...");
|
|
2478
2497
|
const createRes = await ossFetch("/api/deployments", { method: "POST" });
|
|
2479
2498
|
const { id: deploymentId, uploadUrl, uploadFields } = await createRes.json();
|
|
2480
|
-
|
|
2499
|
+
spinner11?.message("Compressing source files...");
|
|
2481
2500
|
const zipBuffer = await createZipBuffer(sourceDir);
|
|
2482
|
-
|
|
2501
|
+
spinner11?.message("Uploading...");
|
|
2483
2502
|
const formData = new FormData();
|
|
2484
2503
|
for (const [key, value] of Object.entries(uploadFields)) {
|
|
2485
2504
|
formData.append(key, value);
|
|
@@ -2490,13 +2509,13 @@ async function deployProjectLegacy(opts) {
|
|
|
2490
2509
|
const uploadErr = await uploadRes.text();
|
|
2491
2510
|
throw new CLIError(`Failed to upload: ${uploadErr}`);
|
|
2492
2511
|
}
|
|
2493
|
-
|
|
2512
|
+
spinner11?.message("Starting deployment...");
|
|
2494
2513
|
const startRes = await ossFetch(`/api/deployments/${deploymentId}/start`, {
|
|
2495
2514
|
method: "POST",
|
|
2496
2515
|
body: JSON.stringify(startBody)
|
|
2497
2516
|
});
|
|
2498
2517
|
await startRes.json();
|
|
2499
|
-
return await pollDeployment(deploymentId,
|
|
2518
|
+
return await pollDeployment(deploymentId, spinner11, false);
|
|
2500
2519
|
}
|
|
2501
2520
|
async function deployProject(opts) {
|
|
2502
2521
|
const config = getProjectConfig();
|
|
@@ -2531,7 +2550,7 @@ function registerDeploymentsDeployCommand(deploymentsCmd2) {
|
|
|
2531
2550
|
`"${dirName}" is an excluded directory and cannot be used as a deploy source. Please specify your project root or output directory instead.`
|
|
2532
2551
|
);
|
|
2533
2552
|
}
|
|
2534
|
-
const
|
|
2553
|
+
const spinner11 = !json ? clack11.spinner() : null;
|
|
2535
2554
|
const startBody = {};
|
|
2536
2555
|
if (opts.env) {
|
|
2537
2556
|
try {
|
|
@@ -2557,9 +2576,9 @@ function registerDeploymentsDeployCommand(deploymentsCmd2) {
|
|
|
2557
2576
|
throw new CLIError("Invalid --meta JSON.");
|
|
2558
2577
|
}
|
|
2559
2578
|
}
|
|
2560
|
-
const result = await deployProject({ sourceDir, startBody, spinner:
|
|
2579
|
+
const result = await deployProject({ sourceDir, startBody, spinner: spinner11 });
|
|
2561
2580
|
if (result.isReady) {
|
|
2562
|
-
|
|
2581
|
+
spinner11?.stop("Deployment complete");
|
|
2563
2582
|
if (json) {
|
|
2564
2583
|
outputJson(result.deployment);
|
|
2565
2584
|
} else {
|
|
@@ -2569,7 +2588,7 @@ function registerDeploymentsDeployCommand(deploymentsCmd2) {
|
|
|
2569
2588
|
clack11.log.info(`Deployment ID: ${result.deploymentId}`);
|
|
2570
2589
|
}
|
|
2571
2590
|
} else {
|
|
2572
|
-
|
|
2591
|
+
spinner11?.stop("Deployment is still building");
|
|
2573
2592
|
if (json) {
|
|
2574
2593
|
outputJson({
|
|
2575
2594
|
id: result.deploymentId,
|
|
@@ -2982,7 +3001,7 @@ function registerCreateCommand(program2) {
|
|
|
2982
3001
|
clack12.note(
|
|
2983
3002
|
`Open your coding agent (Claude Code, Codex, Cursor, etc.) and try:
|
|
2984
3003
|
|
|
2985
|
-
${prompts.map((
|
|
3004
|
+
${prompts.map((p3) => `\u2022 "${p3}"`).join("\n")}`,
|
|
2986
3005
|
"Start building"
|
|
2987
3006
|
);
|
|
2988
3007
|
}
|
|
@@ -3135,13 +3154,13 @@ async function downloadGitHubTemplate(templateName, projectConfig, json) {
|
|
|
3135
3154
|
// src/commands/projects/link.ts
|
|
3136
3155
|
var execAsync3 = promisify4(exec3);
|
|
3137
3156
|
async function runNpmInstall(startMessage = "Installing dependencies...") {
|
|
3138
|
-
const
|
|
3139
|
-
|
|
3157
|
+
const spinner11 = clack13.spinner();
|
|
3158
|
+
spinner11.start(startMessage);
|
|
3140
3159
|
try {
|
|
3141
3160
|
await execAsync3("npm install", { cwd: process.cwd(), maxBuffer: 10 * 1024 * 1024 });
|
|
3142
|
-
|
|
3161
|
+
spinner11.stop("Dependencies installed");
|
|
3143
3162
|
} catch (err) {
|
|
3144
|
-
|
|
3163
|
+
spinner11.stop("Failed to install dependencies");
|
|
3145
3164
|
clack13.log.warn(`npm install failed: ${err.message}`);
|
|
3146
3165
|
clack13.log.info("Run `npm install` manually to install dependencies.");
|
|
3147
3166
|
}
|
|
@@ -3155,13 +3174,13 @@ async function runNpmSetupIfPresent() {
|
|
|
3155
3174
|
} catch {
|
|
3156
3175
|
}
|
|
3157
3176
|
if (!hasSetup) return;
|
|
3158
|
-
const
|
|
3159
|
-
|
|
3177
|
+
const spinner11 = clack13.spinner();
|
|
3178
|
+
spinner11.start("Running setup (schema + migrations)...");
|
|
3160
3179
|
try {
|
|
3161
3180
|
await execAsync3("npm run setup", { cwd: process.cwd(), maxBuffer: 20 * 1024 * 1024 });
|
|
3162
|
-
|
|
3181
|
+
spinner11.stop("Setup complete");
|
|
3163
3182
|
} catch (err) {
|
|
3164
|
-
|
|
3183
|
+
spinner11.stop("Setup failed");
|
|
3165
3184
|
clack13.log.warn(`npm run setup failed: ${err.message.split("\n")[0]}`);
|
|
3166
3185
|
clack13.log.info("Inspect the error, fix DATABASE_URL or network access, then run `npm run setup` manually.");
|
|
3167
3186
|
}
|
|
@@ -3187,7 +3206,7 @@ function registerProjectLinkCommand(program2) {
|
|
|
3187
3206
|
outputJson({ success: true, skills_only: true });
|
|
3188
3207
|
} else {
|
|
3189
3208
|
clack13.note(
|
|
3190
|
-
`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.`,
|
|
3191
3210
|
"What's next"
|
|
3192
3211
|
);
|
|
3193
3212
|
}
|
|
@@ -3379,9 +3398,9 @@ function registerProjectLinkCommand(program2) {
|
|
|
3379
3398
|
}
|
|
3380
3399
|
const selected = await select2({
|
|
3381
3400
|
message: "Select a project to link:",
|
|
3382
|
-
options: projects.map((
|
|
3383
|
-
value:
|
|
3384
|
-
label: `${
|
|
3401
|
+
options: projects.map((p3) => ({
|
|
3402
|
+
value: p3.id,
|
|
3403
|
+
label: `${p3.name} (${p3.region}, ${p3.status})`
|
|
3385
3404
|
}))
|
|
3386
3405
|
});
|
|
3387
3406
|
if (isCancel2(selected)) process.exit(0);
|
|
@@ -3520,7 +3539,7 @@ function registerProjectLinkCommand(program2) {
|
|
|
3520
3539
|
clack13.note(
|
|
3521
3540
|
`Open your coding agent (Claude Code, Codex, Cursor, etc.) and try:
|
|
3522
3541
|
|
|
3523
|
-
${prompts.map((
|
|
3542
|
+
${prompts.map((p3) => `\u2022 "${p3}"`).join("\n")}`,
|
|
3524
3543
|
"Start building"
|
|
3525
3544
|
);
|
|
3526
3545
|
}
|
|
@@ -3677,13 +3696,13 @@ function registerDbPoliciesCommand(dbCmd2) {
|
|
|
3677
3696
|
}
|
|
3678
3697
|
outputTable(
|
|
3679
3698
|
["Table", "Policy Name", "Command", "Roles", "Qual", "With Check"],
|
|
3680
|
-
policies.map((
|
|
3681
|
-
String(
|
|
3682
|
-
String(
|
|
3683
|
-
String(
|
|
3684
|
-
Array.isArray(
|
|
3685
|
-
String(
|
|
3686
|
-
String(
|
|
3699
|
+
policies.map((p3) => [
|
|
3700
|
+
String(p3.tableName ?? "-"),
|
|
3701
|
+
String(p3.policyName ?? "-"),
|
|
3702
|
+
String(p3.cmd ?? "-"),
|
|
3703
|
+
Array.isArray(p3.roles) ? p3.roles.join(", ") : String(p3.roles ?? "-"),
|
|
3704
|
+
String(p3.qual ?? "-"),
|
|
3705
|
+
String(p3.withCheck ?? "-")
|
|
3687
3706
|
])
|
|
3688
3707
|
);
|
|
3689
3708
|
}
|
|
@@ -4829,10 +4848,10 @@ function registerStorageDeleteBucketCommand(storageCmd2) {
|
|
|
4829
4848
|
try {
|
|
4830
4849
|
await requireAuth();
|
|
4831
4850
|
if (!yes && !json) {
|
|
4832
|
-
const
|
|
4851
|
+
const confirm8 = await confirm2({
|
|
4833
4852
|
message: `Delete bucket "${name}" and all its objects? This cannot be undone.`
|
|
4834
4853
|
});
|
|
4835
|
-
if (isCancel2(
|
|
4854
|
+
if (isCancel2(confirm8) || !confirm8) {
|
|
4836
4855
|
process.exit(0);
|
|
4837
4856
|
}
|
|
4838
4857
|
}
|
|
@@ -4979,12 +4998,12 @@ function registerListCommand(program2) {
|
|
|
4979
4998
|
id: org.id,
|
|
4980
4999
|
name: org.name,
|
|
4981
5000
|
type: org.type ?? null,
|
|
4982
|
-
projects: projects.map((
|
|
4983
|
-
id:
|
|
4984
|
-
name:
|
|
4985
|
-
region:
|
|
4986
|
-
status:
|
|
4987
|
-
appkey:
|
|
5001
|
+
projects: projects.map((p3) => ({
|
|
5002
|
+
id: p3.id,
|
|
5003
|
+
name: p3.name,
|
|
5004
|
+
region: p3.region,
|
|
5005
|
+
status: p3.status,
|
|
5006
|
+
appkey: p3.appkey
|
|
4988
5007
|
}))
|
|
4989
5008
|
}))
|
|
4990
5009
|
);
|
|
@@ -4996,13 +5015,13 @@ function registerListCommand(program2) {
|
|
|
4996
5015
|
rows.push([org.name, "-", "-", "-", "-"]);
|
|
4997
5016
|
} else {
|
|
4998
5017
|
for (let i = 0; i < projects.length; i++) {
|
|
4999
|
-
const
|
|
5018
|
+
const p3 = projects[i];
|
|
5000
5019
|
rows.push([
|
|
5001
5020
|
i === 0 ? org.name : "",
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5021
|
+
p3.name,
|
|
5022
|
+
p3.region,
|
|
5023
|
+
p3.status,
|
|
5024
|
+
p3.appkey
|
|
5006
5025
|
]);
|
|
5007
5026
|
}
|
|
5008
5027
|
}
|
|
@@ -5383,10 +5402,10 @@ function registerSecretsDeleteCommand(secretsCmd2) {
|
|
|
5383
5402
|
try {
|
|
5384
5403
|
await requireAuth();
|
|
5385
5404
|
if (!yes && !json) {
|
|
5386
|
-
const
|
|
5405
|
+
const confirm8 = await confirm2({
|
|
5387
5406
|
message: `Delete secret "${key}"? This cannot be undone.`
|
|
5388
5407
|
});
|
|
5389
|
-
if (isCancel2(
|
|
5408
|
+
if (isCancel2(confirm8) || !confirm8) {
|
|
5390
5409
|
process.exit(0);
|
|
5391
5410
|
}
|
|
5392
5411
|
}
|
|
@@ -5574,10 +5593,10 @@ function registerSchedulesDeleteCommand(schedulesCmd2) {
|
|
|
5574
5593
|
try {
|
|
5575
5594
|
await requireAuth();
|
|
5576
5595
|
if (!yes && !json) {
|
|
5577
|
-
const
|
|
5596
|
+
const confirm8 = await confirm2({
|
|
5578
5597
|
message: `Delete schedule "${id}"? This cannot be undone.`
|
|
5579
5598
|
});
|
|
5580
|
-
if (isCancel2(
|
|
5599
|
+
if (isCancel2(confirm8) || !confirm8) {
|
|
5581
5600
|
process.exit(0);
|
|
5582
5601
|
}
|
|
5583
5602
|
}
|
|
@@ -5799,6 +5818,8 @@ function registerComputeUpdateCommand(computeCmd2) {
|
|
|
5799
5818
|
outputJson(service);
|
|
5800
5819
|
} else {
|
|
5801
5820
|
outputSuccess(`Service "${service.name}" updated [${service.status}]`);
|
|
5821
|
+
if (service.endpointUrl) console.log(` Endpoint: ${service.endpointUrl}`);
|
|
5822
|
+
if (service.port !== void 0) console.log(` Port: ${service.port} (container must listen on this port)`);
|
|
5802
5823
|
}
|
|
5803
5824
|
await reportCliUsage("cli.compute.update", true);
|
|
5804
5825
|
} catch (err) {
|
|
@@ -5999,7 +6020,7 @@ primary_region = "${opts.region}"
|
|
|
5999
6020
|
};
|
|
6000
6021
|
}
|
|
6001
6022
|
function flyctlBuildAndPush(opts) {
|
|
6002
|
-
return new Promise((
|
|
6023
|
+
return new Promise((resolve9, reject) => {
|
|
6003
6024
|
const cleanupStub = ensureFlyTomlStub({
|
|
6004
6025
|
dir: opts.dir,
|
|
6005
6026
|
appId: opts.appId,
|
|
@@ -6055,7 +6076,7 @@ function flyctlBuildAndPush(opts) {
|
|
|
6055
6076
|
)
|
|
6056
6077
|
);
|
|
6057
6078
|
}
|
|
6058
|
-
|
|
6079
|
+
resolve9({ imageRef: `registry.fly.io/${opts.appId}@${m[1]}` });
|
|
6059
6080
|
});
|
|
6060
6081
|
});
|
|
6061
6082
|
}
|
|
@@ -6154,6 +6175,7 @@ function registerComputeDeployCommand(computeCmd2) {
|
|
|
6154
6175
|
const verb = existing2 ? "updated" : "deployed";
|
|
6155
6176
|
outputSuccess(`Service "${service2.name}" ${verb} [${service2.status}]`);
|
|
6156
6177
|
if (service2.endpointUrl) console.log(` Endpoint: ${service2.endpointUrl}`);
|
|
6178
|
+
if (service2.port !== void 0) console.log(` Port: ${service2.port} (container must listen on this port)`);
|
|
6157
6179
|
}
|
|
6158
6180
|
await reportCliUsage("cli.compute.deploy", true);
|
|
6159
6181
|
return;
|
|
@@ -6249,6 +6271,7 @@ function registerComputeDeployCommand(computeCmd2) {
|
|
|
6249
6271
|
const verb = existing ? "updated" : "deployed";
|
|
6250
6272
|
outputSuccess(`Service "${service.name}" ${verb} [${service.status}]`);
|
|
6251
6273
|
if (service.endpointUrl) console.log(` Endpoint: ${service.endpointUrl}`);
|
|
6274
|
+
if (service.port !== void 0) console.log(` Port: ${service.port} (container must listen on this port)`);
|
|
6252
6275
|
console.log(` Image: ${imageRef} (built remotely; no local image to clean up)`);
|
|
6253
6276
|
}
|
|
6254
6277
|
await reportCliUsage("cli.compute.deploy", true);
|
|
@@ -6938,7 +6961,7 @@ function registerDiagnoseCommands(diagnoseCmd2) {
|
|
|
6938
6961
|
const s = !json ? clack15.spinner() : null;
|
|
6939
6962
|
s?.start("Collecting diagnostic data...");
|
|
6940
6963
|
const data2 = await collectDiagnosticData(projectId, ossMode, apiUrl);
|
|
6941
|
-
const cliVersion = "0.1.
|
|
6964
|
+
const cliVersion = "0.1.78";
|
|
6942
6965
|
s?.stop("Data collected");
|
|
6943
6966
|
if (!json) {
|
|
6944
6967
|
console.log(`
|
|
@@ -7572,10 +7595,10 @@ function registerPaymentsConfigCommand(paymentsCmd2) {
|
|
|
7572
7595
|
throw new CLIError("Use --yes with --json to remove a Stripe key non-interactively.");
|
|
7573
7596
|
}
|
|
7574
7597
|
if (!yes) {
|
|
7575
|
-
const
|
|
7598
|
+
const confirm8 = await confirm2({
|
|
7576
7599
|
message: `Remove Stripe ${environment} key? Payment sync and mutations for this environment will stop.`
|
|
7577
7600
|
});
|
|
7578
|
-
if (isCancel2(
|
|
7601
|
+
if (isCancel2(confirm8) || !confirm8) process.exit(0);
|
|
7579
7602
|
}
|
|
7580
7603
|
const data = await removeStripeSecretKey(environment);
|
|
7581
7604
|
if (json) {
|
|
@@ -8098,10 +8121,10 @@ function registerPaymentsProductsCommand(paymentsCmd2) {
|
|
|
8098
8121
|
);
|
|
8099
8122
|
}
|
|
8100
8123
|
if (!yes) {
|
|
8101
|
-
const
|
|
8124
|
+
const confirm8 = await confirm2({
|
|
8102
8125
|
message: `Delete Stripe ${environment} product "${productId}"?`
|
|
8103
8126
|
});
|
|
8104
|
-
if (isCancel2(
|
|
8127
|
+
if (isCancel2(confirm8) || !confirm8) process.exit(0);
|
|
8105
8128
|
}
|
|
8106
8129
|
const data = await deletePaymentProduct(environment, productId);
|
|
8107
8130
|
if (json) {
|
|
@@ -8452,14 +8475,14 @@ async function startPosthogCliFlow(projectId, jwt, apiUrl) {
|
|
|
8452
8475
|
throw new CLIError("PostHog cli-start returned an unexpected response shape.");
|
|
8453
8476
|
}
|
|
8454
8477
|
function sleep(ms, signal) {
|
|
8455
|
-
return new Promise((
|
|
8478
|
+
return new Promise((resolve9, reject) => {
|
|
8456
8479
|
if (signal?.aborted) {
|
|
8457
8480
|
reject(new CLIError("Connection wait cancelled."));
|
|
8458
8481
|
return;
|
|
8459
8482
|
}
|
|
8460
8483
|
const timer = setTimeout(() => {
|
|
8461
8484
|
signal?.removeEventListener("abort", onAbort);
|
|
8462
|
-
|
|
8485
|
+
resolve9();
|
|
8463
8486
|
}, ms);
|
|
8464
8487
|
const onAbort = () => {
|
|
8465
8488
|
clearTimeout(timer);
|
|
@@ -8762,8 +8785,8 @@ async function runConnectFlow(projectId, token, authorizeUrl, opts) {
|
|
|
8762
8785
|
} catch {
|
|
8763
8786
|
}
|
|
8764
8787
|
}
|
|
8765
|
-
const
|
|
8766
|
-
|
|
8788
|
+
const spinner11 = !opts.json && isInteractive ? clack16.spinner() : null;
|
|
8789
|
+
spinner11?.start("Waiting for connection... (timeout: 15 minutes)");
|
|
8767
8790
|
try {
|
|
8768
8791
|
const conn = await pollPosthogConnection(
|
|
8769
8792
|
projectId,
|
|
@@ -8773,20 +8796,20 @@ async function runConnectFlow(projectId, token, authorizeUrl, opts) {
|
|
|
8773
8796
|
timeoutMs: POLL_TIMEOUT_MS4,
|
|
8774
8797
|
maxTransientRetries: MAX_TRANSIENT_RETRIES,
|
|
8775
8798
|
onTick: (elapsed) => {
|
|
8776
|
-
if (
|
|
8799
|
+
if (spinner11) {
|
|
8777
8800
|
const secs = Math.floor(elapsed / 1e3);
|
|
8778
8801
|
const mins = Math.floor(secs / 60);
|
|
8779
8802
|
const remaining = `${mins}m ${secs % 60}s elapsed`;
|
|
8780
|
-
|
|
8803
|
+
spinner11.message(`Waiting for connection... (${remaining})`);
|
|
8781
8804
|
}
|
|
8782
8805
|
}
|
|
8783
8806
|
},
|
|
8784
8807
|
opts.apiUrl
|
|
8785
8808
|
);
|
|
8786
|
-
|
|
8809
|
+
spinner11?.stop("Connection received from PostHog.");
|
|
8787
8810
|
return conn;
|
|
8788
8811
|
} catch (err) {
|
|
8789
|
-
|
|
8812
|
+
spinner11?.stop("Connection wait failed.");
|
|
8790
8813
|
throw err;
|
|
8791
8814
|
}
|
|
8792
8815
|
}
|
|
@@ -8804,14 +8827,14 @@ function resolveFramework(opts) {
|
|
|
8804
8827
|
}
|
|
8805
8828
|
async function installSdk(pm, cwd, opts) {
|
|
8806
8829
|
const cmd = installCommand(pm, "posthog-js");
|
|
8807
|
-
const
|
|
8808
|
-
|
|
8830
|
+
const spinner11 = !opts.json && isInteractive ? clack16.spinner() : null;
|
|
8831
|
+
spinner11?.start(`Installing posthog-js (${cmd})...`);
|
|
8809
8832
|
try {
|
|
8810
8833
|
await runInstall(pm, "posthog-js", cwd);
|
|
8811
|
-
|
|
8834
|
+
spinner11?.stop("Installed posthog-js.");
|
|
8812
8835
|
return true;
|
|
8813
8836
|
} catch (err) {
|
|
8814
|
-
|
|
8837
|
+
spinner11?.stop("Install failed.");
|
|
8815
8838
|
if (!opts.json) {
|
|
8816
8839
|
clack16.log.warn(
|
|
8817
8840
|
`Could not run \`${cmd}\` automatically: ${err.message}
|
|
@@ -9027,13 +9050,907 @@ function frameworkLabel(framework) {
|
|
|
9027
9050
|
return "Astro";
|
|
9028
9051
|
}
|
|
9029
9052
|
}
|
|
9030
|
-
function relative3(
|
|
9031
|
-
return
|
|
9053
|
+
function relative3(p3) {
|
|
9054
|
+
return p3.replace(process.cwd() + "/", "");
|
|
9055
|
+
}
|
|
9056
|
+
|
|
9057
|
+
// src/commands/config/export.ts
|
|
9058
|
+
import { writeFileSync as writeFileSync9, existsSync as existsSync14 } from "fs";
|
|
9059
|
+
import { resolve as resolve5 } from "path";
|
|
9060
|
+
import * as p from "@clack/prompts";
|
|
9061
|
+
import pc4 from "picocolors";
|
|
9062
|
+
|
|
9063
|
+
// src/lib/config-toml.ts
|
|
9064
|
+
import * as smolToml from "smol-toml";
|
|
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
|
+
|
|
9135
|
+
// src/lib/config-schema.ts
|
|
9136
|
+
var ConfigValidationError = class extends Error {
|
|
9137
|
+
constructor(path6, message) {
|
|
9138
|
+
super(`config.${path6}: ${message}`);
|
|
9139
|
+
this.path = path6;
|
|
9140
|
+
this.name = "ConfigValidationError";
|
|
9141
|
+
}
|
|
9142
|
+
};
|
|
9143
|
+
function validateConfig(input) {
|
|
9144
|
+
if (input === null || typeof input !== "object" || Array.isArray(input)) {
|
|
9145
|
+
throw new ConfigValidationError("", "must be an object");
|
|
9146
|
+
}
|
|
9147
|
+
const obj = input;
|
|
9148
|
+
const out = {};
|
|
9149
|
+
if ("project_id" in obj) {
|
|
9150
|
+
if (typeof obj.project_id !== "string") {
|
|
9151
|
+
throw new ConfigValidationError("project_id", "must be a string");
|
|
9152
|
+
}
|
|
9153
|
+
out.project_id = obj.project_id;
|
|
9154
|
+
}
|
|
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
|
+
}
|
|
9175
|
+
return out;
|
|
9176
|
+
}
|
|
9177
|
+
function validateAuth(input) {
|
|
9178
|
+
if (input === null || typeof input !== "object" || Array.isArray(input)) {
|
|
9179
|
+
throw new ConfigValidationError("auth", "must be an object");
|
|
9180
|
+
}
|
|
9181
|
+
const obj = input;
|
|
9182
|
+
const out = {};
|
|
9183
|
+
if ("allowed_redirect_urls" in obj) {
|
|
9184
|
+
const v = obj.allowed_redirect_urls;
|
|
9185
|
+
if (!Array.isArray(v) || !v.every((u) => typeof u === "string")) {
|
|
9186
|
+
throw new ConfigValidationError(
|
|
9187
|
+
"auth.allowed_redirect_urls",
|
|
9188
|
+
"must be an array of strings"
|
|
9189
|
+
);
|
|
9190
|
+
}
|
|
9191
|
+
out.allowed_redirect_urls = v;
|
|
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
|
+
}
|
|
9257
|
+
return out;
|
|
9258
|
+
}
|
|
9259
|
+
|
|
9260
|
+
// src/lib/config-toml.ts
|
|
9261
|
+
function parseConfigToml(input) {
|
|
9262
|
+
let parsed;
|
|
9263
|
+
try {
|
|
9264
|
+
parsed = smolToml.parse(input);
|
|
9265
|
+
} catch (err) {
|
|
9266
|
+
throw new Error(`TOML parse error: ${err.message}`, { cause: err });
|
|
9267
|
+
}
|
|
9268
|
+
return validateConfig(parsed);
|
|
9269
|
+
}
|
|
9270
|
+
function stringifyConfigToml(config) {
|
|
9271
|
+
const lines = [];
|
|
9272
|
+
if (config.project_id !== void 0) {
|
|
9273
|
+
lines.push(`project_id = ${JSON.stringify(config.project_id)}`);
|
|
9274
|
+
lines.push("");
|
|
9275
|
+
}
|
|
9276
|
+
if (config.auth) {
|
|
9277
|
+
lines.push("[auth]");
|
|
9278
|
+
if (config.auth.allowed_redirect_urls !== void 0) {
|
|
9279
|
+
const urls = config.auth.allowed_redirect_urls.map((u) => JSON.stringify(u)).join(", ");
|
|
9280
|
+
lines.push(`allowed_redirect_urls = [${urls}]`);
|
|
9281
|
+
}
|
|
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
|
+
}
|
|
9295
|
+
}
|
|
9296
|
+
return lines.join("\n").replace(/\n+$/, "\n");
|
|
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
|
+
}
|
|
9320
|
+
|
|
9321
|
+
// src/commands/config/export.ts
|
|
9322
|
+
function registerConfigExportCommand(cfg) {
|
|
9323
|
+
cfg.command("export").description("Pull live project config and write insforge.toml").option("--out <path>", "output path", "insforge.toml").option("--force", "overwrite without confirmation").action(async (opts, cmd) => {
|
|
9324
|
+
const { json } = getRootOpts(cmd);
|
|
9325
|
+
try {
|
|
9326
|
+
await requireAuth();
|
|
9327
|
+
const target = resolve5(process.cwd(), opts.out);
|
|
9328
|
+
if (existsSync14(target) && !opts.force) {
|
|
9329
|
+
if (json) {
|
|
9330
|
+
throw new CLIError(
|
|
9331
|
+
`${opts.out} exists. Re-run with --force to overwrite.`,
|
|
9332
|
+
1,
|
|
9333
|
+
"OUTPUT_EXISTS"
|
|
9334
|
+
);
|
|
9335
|
+
}
|
|
9336
|
+
const ok = await p.confirm({
|
|
9337
|
+
message: `${opts.out} exists. Overwrite?`,
|
|
9338
|
+
initialValue: false
|
|
9339
|
+
});
|
|
9340
|
+
if (!ok || p.isCancel(ok)) {
|
|
9341
|
+
console.log("Aborted.");
|
|
9342
|
+
return;
|
|
9343
|
+
}
|
|
9344
|
+
}
|
|
9345
|
+
const res = await ossFetch("/api/metadata");
|
|
9346
|
+
const raw = await res.json();
|
|
9347
|
+
const config = {};
|
|
9348
|
+
const skipped = [];
|
|
9349
|
+
const authSlice = raw?.auth;
|
|
9350
|
+
if (authSlice && typeof authSlice === "object" && "allowedRedirectUrls" in authSlice) {
|
|
9351
|
+
config.auth = config.auth ?? {};
|
|
9352
|
+
config.auth.allowed_redirect_urls = authSlice.allowedRedirectUrls ?? [];
|
|
9353
|
+
} else {
|
|
9354
|
+
skipped.push("auth.allowed_redirect_urls");
|
|
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
|
+
}
|
|
9385
|
+
const toml = stringifyConfigToml(config);
|
|
9386
|
+
writeFileSync9(target, toml, "utf8");
|
|
9387
|
+
if (json) {
|
|
9388
|
+
console.log(JSON.stringify({ written: target, config, skipped }, null, 2));
|
|
9389
|
+
} else {
|
|
9390
|
+
console.log(`${pc4.green("\u2713")} Wrote ${target}`);
|
|
9391
|
+
if (skipped.length) {
|
|
9392
|
+
console.warn(
|
|
9393
|
+
pc4.yellow(
|
|
9394
|
+
`\u26A0 Skipped ${skipped.length} section(s) not supported by this backend:`
|
|
9395
|
+
) + "\n" + skipped.map((k) => ` - ${k}`).join("\n")
|
|
9396
|
+
);
|
|
9397
|
+
}
|
|
9398
|
+
}
|
|
9399
|
+
await reportCliUsage("cli.config.export", true);
|
|
9400
|
+
} catch (err) {
|
|
9401
|
+
await reportCliUsage("cli.config.export", false);
|
|
9402
|
+
handleError(err, json);
|
|
9403
|
+
}
|
|
9404
|
+
});
|
|
9405
|
+
}
|
|
9406
|
+
|
|
9407
|
+
// src/commands/config/plan.ts
|
|
9408
|
+
import { readFileSync as readFileSync11 } from "fs";
|
|
9409
|
+
import { resolve as resolve6 } from "path";
|
|
9410
|
+
import pc5 from "picocolors";
|
|
9411
|
+
|
|
9412
|
+
// src/lib/config-diff.ts
|
|
9413
|
+
function diffConfig({ live, file }) {
|
|
9414
|
+
const changes = [];
|
|
9415
|
+
const fileAuth = file.auth;
|
|
9416
|
+
const liveAuth = live.auth ?? {};
|
|
9417
|
+
if (fileAuth && "allowed_redirect_urls" in fileAuth) {
|
|
9418
|
+
const fromV = normalizeUrlList(liveAuth.allowed_redirect_urls);
|
|
9419
|
+
const toV = normalizeUrlList(fileAuth.allowed_redirect_urls);
|
|
9420
|
+
if (!arrayEquals(fromV, toV)) {
|
|
9421
|
+
changes.push({
|
|
9422
|
+
section: "auth",
|
|
9423
|
+
op: "modify",
|
|
9424
|
+
key: "allowed_redirect_urls",
|
|
9425
|
+
from: fromV,
|
|
9426
|
+
to: toV
|
|
9427
|
+
});
|
|
9428
|
+
}
|
|
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
|
+
}
|
|
9450
|
+
return { changes, summary: summarize(changes) };
|
|
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
|
+
};
|
|
9510
|
+
function summarize(changes) {
|
|
9511
|
+
const s = { add: 0, modify: 0, remove: 0, kept: 0 };
|
|
9512
|
+
for (const c of changes) {
|
|
9513
|
+
if (c.op === "modify") s.modify++;
|
|
9514
|
+
}
|
|
9515
|
+
return s;
|
|
9516
|
+
}
|
|
9517
|
+
function normalizeUrlList(input) {
|
|
9518
|
+
return Array.from(new Set(input ?? [])).sort();
|
|
9519
|
+
}
|
|
9520
|
+
function arrayEquals(a, b) {
|
|
9521
|
+
if (a.length !== b.length) return false;
|
|
9522
|
+
return a.every((v, i) => v === b[i]);
|
|
9523
|
+
}
|
|
9524
|
+
|
|
9525
|
+
// src/lib/config-format.ts
|
|
9526
|
+
function formatPlan(result) {
|
|
9527
|
+
if (result.changes.length === 0) {
|
|
9528
|
+
return "No changes. Live state matches insforge.toml.";
|
|
9529
|
+
}
|
|
9530
|
+
const bySection = /* @__PURE__ */ new Map();
|
|
9531
|
+
for (const c of result.changes) {
|
|
9532
|
+
const arr = bySection.get(c.section) ?? [];
|
|
9533
|
+
arr.push(c);
|
|
9534
|
+
bySection.set(c.section, arr);
|
|
9535
|
+
}
|
|
9536
|
+
const lines = [];
|
|
9537
|
+
for (const [section, changes] of bySection) {
|
|
9538
|
+
lines.push(` ${section}:`);
|
|
9539
|
+
for (const c of changes) {
|
|
9540
|
+
lines.push(` ${formatChange(c)}`);
|
|
9541
|
+
}
|
|
9542
|
+
lines.push("");
|
|
9543
|
+
}
|
|
9544
|
+
const s = result.summary;
|
|
9545
|
+
lines.push(
|
|
9546
|
+
`${s.add} add, ${s.modify} modify, ${s.remove} remove, ${s.kept} untracked kept.`
|
|
9547
|
+
);
|
|
9548
|
+
return lines.join("\n");
|
|
9549
|
+
}
|
|
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
|
+
}
|
|
9579
|
+
return `~ ${c.key}: ${JSON.stringify(c.from)} \u2192 ${JSON.stringify(c.to)}`;
|
|
9580
|
+
}
|
|
9581
|
+
|
|
9582
|
+
// src/lib/config-capabilities.ts
|
|
9583
|
+
function metadataSupports(raw, change) {
|
|
9584
|
+
if (change.section === "auth" && change.key === "allowed_redirect_urls") {
|
|
9585
|
+
return raw?.auth !== void 0 && raw.auth !== null && typeof raw.auth === "object" && "allowedRedirectUrls" in raw.auth;
|
|
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;
|
|
9595
|
+
return false;
|
|
9596
|
+
}
|
|
9597
|
+
function changePath(change) {
|
|
9598
|
+
if (change.section === "auth.smtp") return "auth.smtp";
|
|
9599
|
+
return `${change.section}.${change.key}`;
|
|
9600
|
+
}
|
|
9601
|
+
|
|
9602
|
+
// src/commands/config/plan.ts
|
|
9603
|
+
function registerConfigPlanCommand(cfg) {
|
|
9604
|
+
cfg.command("plan").description("Show diff between insforge.toml and live project state").option("--file <path>", "path to insforge.toml", "insforge.toml").action(async (opts, cmd) => {
|
|
9605
|
+
const { json } = getRootOpts(cmd);
|
|
9606
|
+
try {
|
|
9607
|
+
await requireAuth();
|
|
9608
|
+
const tomlPath = resolve6(process.cwd(), opts.file);
|
|
9609
|
+
const tomlSource = readFileSync11(tomlPath, "utf8");
|
|
9610
|
+
const file = parseConfigToml(tomlSource);
|
|
9611
|
+
const res = await ossFetch("/api/metadata");
|
|
9612
|
+
const raw = await res.json();
|
|
9613
|
+
const live = {
|
|
9614
|
+
auth: { allowed_redirect_urls: raw.auth?.allowedRedirectUrls ?? [] }
|
|
9615
|
+
};
|
|
9616
|
+
const result = diffConfig({ live, file });
|
|
9617
|
+
const skipped = result.changes.filter((c) => !metadataSupports(raw, c)).map((c) => changePath(c));
|
|
9618
|
+
if (json) {
|
|
9619
|
+
console.log(JSON.stringify({ ...result, skipped }, null, 2));
|
|
9620
|
+
} else {
|
|
9621
|
+
console.log(`Plan for insforge.toml (file: ${opts.file}):
|
|
9622
|
+
`);
|
|
9623
|
+
console.log(formatPlan(result));
|
|
9624
|
+
if (skipped.length) {
|
|
9625
|
+
console.warn(
|
|
9626
|
+
"\n" + pc5.yellow(`\u26A0 Apply will skip ${skipped.length} section(s) \u2014 backend doesn't support them yet:`) + "\n" + skipped.map((k) => ` - ${k}`).join("\n")
|
|
9627
|
+
);
|
|
9628
|
+
}
|
|
9629
|
+
}
|
|
9630
|
+
await reportCliUsage("cli.config.plan", true);
|
|
9631
|
+
} catch (err) {
|
|
9632
|
+
await reportCliUsage("cli.config.plan", false);
|
|
9633
|
+
handleError(err, json);
|
|
9634
|
+
}
|
|
9635
|
+
});
|
|
9636
|
+
}
|
|
9637
|
+
|
|
9638
|
+
// src/commands/config/apply.ts
|
|
9639
|
+
import { readFileSync as readFileSync12 } from "fs";
|
|
9640
|
+
import { resolve as resolve7 } from "path";
|
|
9641
|
+
import * as p2 from "@clack/prompts";
|
|
9642
|
+
import pc6 from "picocolors";
|
|
9643
|
+
function registerConfigApplyCommand(cfg) {
|
|
9644
|
+
cfg.command("apply").description("Apply insforge.toml to the live project").option("--file <path>", "path to insforge.toml", "insforge.toml").option("--dry-run", "show plan, do not apply").option("--auto-approve", "skip confirmation prompt").action(async (opts, cmd) => {
|
|
9645
|
+
const { json, yes } = getRootOpts(cmd);
|
|
9646
|
+
try {
|
|
9647
|
+
await requireAuth();
|
|
9648
|
+
const tomlPath = resolve7(process.cwd(), opts.file);
|
|
9649
|
+
const tomlSource = readFileSync12(tomlPath, "utf8");
|
|
9650
|
+
const file = parseConfigToml(tomlSource);
|
|
9651
|
+
const res = await ossFetch("/api/metadata");
|
|
9652
|
+
const raw = await res.json();
|
|
9653
|
+
const live = liveFromMetadata(raw);
|
|
9654
|
+
const result = diffConfig({ live, file });
|
|
9655
|
+
const approved = opts.autoApprove || yes;
|
|
9656
|
+
if (!json) {
|
|
9657
|
+
console.log(formatPlan(result));
|
|
9658
|
+
}
|
|
9659
|
+
if (result.changes.length === 0 || opts.dryRun) {
|
|
9660
|
+
if (json) {
|
|
9661
|
+
console.log(
|
|
9662
|
+
JSON.stringify({ plan: result, applied: false, dryRun: !!opts.dryRun }, null, 2)
|
|
9663
|
+
);
|
|
9664
|
+
}
|
|
9665
|
+
await reportCliUsage("cli.config.apply", true);
|
|
9666
|
+
return;
|
|
9667
|
+
}
|
|
9668
|
+
if (!approved) {
|
|
9669
|
+
if (json) {
|
|
9670
|
+
throw new CLIError(
|
|
9671
|
+
"Refusing to apply in --json mode without --auto-approve or --yes.",
|
|
9672
|
+
1,
|
|
9673
|
+
"CONFIRMATION_REQUIRED"
|
|
9674
|
+
);
|
|
9675
|
+
}
|
|
9676
|
+
const ok = await p2.confirm({
|
|
9677
|
+
message: "Apply these changes?",
|
|
9678
|
+
initialValue: false
|
|
9679
|
+
});
|
|
9680
|
+
if (!ok || p2.isCancel(ok)) {
|
|
9681
|
+
console.log("Aborted.");
|
|
9682
|
+
await reportCliUsage("cli.config.apply", true);
|
|
9683
|
+
return;
|
|
9684
|
+
}
|
|
9685
|
+
}
|
|
9686
|
+
const applied = [];
|
|
9687
|
+
const skipped = [];
|
|
9688
|
+
for (const change of result.changes) {
|
|
9689
|
+
const path6 = changePath(change);
|
|
9690
|
+
if (!metadataSupports(raw, change)) {
|
|
9691
|
+
skipped.push({
|
|
9692
|
+
key: path6,
|
|
9693
|
+
reason: `your backend doesn't expose ${path6} \u2014 upgrade the project to apply this section`
|
|
9694
|
+
});
|
|
9695
|
+
continue;
|
|
9696
|
+
}
|
|
9697
|
+
await applyChange(change);
|
|
9698
|
+
applied.push(change);
|
|
9699
|
+
}
|
|
9700
|
+
if (json) {
|
|
9701
|
+
console.log(
|
|
9702
|
+
JSON.stringify({ plan: result, applied, skipped }, null, 2)
|
|
9703
|
+
);
|
|
9704
|
+
} else {
|
|
9705
|
+
if (skipped.length) {
|
|
9706
|
+
console.warn(
|
|
9707
|
+
pc6.yellow(`\u26A0 Skipped ${skipped.length} section(s):`) + "\n" + skipped.map((s) => ` - ${s.key}: ${s.reason}`).join("\n")
|
|
9708
|
+
);
|
|
9709
|
+
}
|
|
9710
|
+
if (applied.length) {
|
|
9711
|
+
console.log(
|
|
9712
|
+
`${pc6.green("\u2713")} Applied ${applied.length} of ${result.changes.length} change(s).`
|
|
9713
|
+
);
|
|
9714
|
+
} else {
|
|
9715
|
+
console.log("Nothing applied.");
|
|
9716
|
+
}
|
|
9717
|
+
}
|
|
9718
|
+
await reportCliUsage("cli.config.apply", true);
|
|
9719
|
+
} catch (err) {
|
|
9720
|
+
await reportCliUsage("cli.config.apply", false);
|
|
9721
|
+
handleError(err, json);
|
|
9722
|
+
}
|
|
9723
|
+
});
|
|
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
|
+
}
|
|
9748
|
+
async function applyChange(change) {
|
|
9749
|
+
if (change.section === "auth" && change.key === "allowed_redirect_urls") {
|
|
9750
|
+
await ossFetch("/api/auth/config", {
|
|
9751
|
+
method: "PUT",
|
|
9752
|
+
body: JSON.stringify({ allowedRedirectUrls: change.to })
|
|
9753
|
+
});
|
|
9754
|
+
return;
|
|
9755
|
+
}
|
|
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)}`);
|
|
9789
|
+
}
|
|
9790
|
+
|
|
9791
|
+
// src/commands/config/index.ts
|
|
9792
|
+
function registerConfigCommand(program2) {
|
|
9793
|
+
const cfg = program2.command("config").description("Manage insforge.toml (declarative project configuration)");
|
|
9794
|
+
registerConfigExportCommand(cfg);
|
|
9795
|
+
registerConfigPlanCommand(cfg);
|
|
9796
|
+
registerConfigApplyCommand(cfg);
|
|
9797
|
+
}
|
|
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);
|
|
9032
9949
|
}
|
|
9033
9950
|
|
|
9034
9951
|
// src/index.ts
|
|
9035
9952
|
var __dirname = dirname3(fileURLToPath(import.meta.url));
|
|
9036
|
-
var pkg = JSON.parse(
|
|
9953
|
+
var pkg = JSON.parse(readFileSync14(join18(__dirname, "../package.json"), "utf-8"));
|
|
9037
9954
|
var INSFORGE_LOGO = `
|
|
9038
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
|
|
9039
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
|
|
@@ -9117,6 +10034,8 @@ registerComputeStopCommand(computeCmd);
|
|
|
9117
10034
|
registerComputeEventsCommand(computeCmd);
|
|
9118
10035
|
var posthogCmd = program.command("posthog").description("Manage PostHog product analytics integration");
|
|
9119
10036
|
registerPosthogSetupCommand(posthogCmd);
|
|
10037
|
+
var aiCmd = program.command("ai").description("Manage AI model gateway setup");
|
|
10038
|
+
registerAiCommands(aiCmd);
|
|
9120
10039
|
var schedulesCmd = program.command("schedules").description("Manage scheduled tasks (cron jobs)");
|
|
9121
10040
|
registerSchedulesListCommand(schedulesCmd);
|
|
9122
10041
|
registerSchedulesGetCommand(schedulesCmd);
|
|
@@ -9124,6 +10043,7 @@ registerSchedulesCreateCommand(schedulesCmd);
|
|
|
9124
10043
|
registerSchedulesUpdateCommand(schedulesCmd);
|
|
9125
10044
|
registerSchedulesDeleteCommand(schedulesCmd);
|
|
9126
10045
|
registerSchedulesLogsCommand(schedulesCmd);
|
|
10046
|
+
registerConfigCommand(program);
|
|
9127
10047
|
if (process.argv.length <= 2 && process.stdout.isTTY) {
|
|
9128
10048
|
await showInteractiveMenu();
|
|
9129
10049
|
} else {
|
|
@@ -9141,7 +10061,7 @@ async function showInteractiveMenu() {
|
|
|
9141
10061
|
} catch {
|
|
9142
10062
|
}
|
|
9143
10063
|
console.log(INSFORGE_LOGO);
|
|
9144
|
-
|
|
10064
|
+
clack18.intro(`InsForge CLI v${pkg.version}`);
|
|
9145
10065
|
const options = [];
|
|
9146
10066
|
if (!isLoggedIn) {
|
|
9147
10067
|
options.push({ value: "login", label: "Log in to InsForge" });
|
|
@@ -9162,7 +10082,7 @@ async function showInteractiveMenu() {
|
|
|
9162
10082
|
options
|
|
9163
10083
|
});
|
|
9164
10084
|
if (isCancel2(action)) {
|
|
9165
|
-
|
|
10085
|
+
clack18.cancel("Bye!");
|
|
9166
10086
|
process.exit(0);
|
|
9167
10087
|
}
|
|
9168
10088
|
switch (action) {
|