@insforge/cli 0.1.62 → 0.1.63
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 +48 -24
- package/dist/index.js +1667 -446
- package/dist/index.js.map +1 -1
- package/package.json +2 -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 readFileSync8 } from "fs";
|
|
5
|
+
import { join as join14, dirname as dirname2 } from "path";
|
|
6
6
|
import { fileURLToPath } from "url";
|
|
7
7
|
import { Command } from "commander";
|
|
8
|
-
import * as
|
|
8
|
+
import * as clack15 from "@clack/prompts";
|
|
9
9
|
|
|
10
10
|
// src/lib/prompts.ts
|
|
11
11
|
import * as readline from "readline";
|
|
@@ -220,6 +220,12 @@ function getLocalConfigDir() {
|
|
|
220
220
|
function getLocalConfigFile() {
|
|
221
221
|
return join(getLocalConfigDir(), "project.json");
|
|
222
222
|
}
|
|
223
|
+
function getParentBackupFile() {
|
|
224
|
+
return join(getLocalConfigDir(), "project.parent.json");
|
|
225
|
+
}
|
|
226
|
+
function getProjectConfigFile() {
|
|
227
|
+
return getLocalConfigFile();
|
|
228
|
+
}
|
|
223
229
|
function getProjectConfig() {
|
|
224
230
|
const file = getLocalConfigFile();
|
|
225
231
|
if (!existsSync(file)) {
|
|
@@ -644,7 +650,8 @@ async function refreshAccessToken(apiUrl) {
|
|
|
644
650
|
}
|
|
645
651
|
|
|
646
652
|
// src/lib/api/platform.ts
|
|
647
|
-
async function platformFetch(
|
|
653
|
+
async function platformFetch(path6, options = {}, apiUrl) {
|
|
654
|
+
const { passThroughStatuses, ...fetchOptions } = options;
|
|
648
655
|
const baseUrl = getPlatformApiUrl(apiUrl);
|
|
649
656
|
const token = getAccessToken();
|
|
650
657
|
if (!token) {
|
|
@@ -653,19 +660,19 @@ async function platformFetch(path5, options = {}, apiUrl) {
|
|
|
653
660
|
const headers = {
|
|
654
661
|
"Content-Type": "application/json",
|
|
655
662
|
Authorization: `Bearer ${token}`,
|
|
656
|
-
...
|
|
663
|
+
...fetchOptions.headers ?? {}
|
|
657
664
|
};
|
|
658
|
-
const fullUrl = `${baseUrl}${
|
|
665
|
+
const fullUrl = `${baseUrl}${path6}`;
|
|
659
666
|
if (process.env.INSFORGE_DEBUG) {
|
|
660
|
-
console.error(`[DEBUG] ${
|
|
667
|
+
console.error(`[DEBUG] ${fetchOptions.method ?? "GET"} ${fullUrl}`);
|
|
661
668
|
console.error(`[DEBUG] Headers: ${JSON.stringify(headers, null, 2)}`);
|
|
662
|
-
if (
|
|
663
|
-
console.error(`[DEBUG] Body: ${typeof
|
|
669
|
+
if (fetchOptions.body) {
|
|
670
|
+
console.error(`[DEBUG] Body: ${typeof fetchOptions.body === "string" ? fetchOptions.body : JSON.stringify(fetchOptions.body)}`);
|
|
664
671
|
}
|
|
665
672
|
}
|
|
666
673
|
let res;
|
|
667
674
|
try {
|
|
668
|
-
res = await fetch(fullUrl, { ...
|
|
675
|
+
res = await fetch(fullUrl, { ...fetchOptions, headers });
|
|
669
676
|
} catch (err) {
|
|
670
677
|
throw new CLIError(formatFetchError(err, fullUrl));
|
|
671
678
|
}
|
|
@@ -674,16 +681,22 @@ async function platformFetch(path5, options = {}, apiUrl) {
|
|
|
674
681
|
headers.Authorization = `Bearer ${newToken}`;
|
|
675
682
|
let retryRes;
|
|
676
683
|
try {
|
|
677
|
-
retryRes = await fetch(fullUrl, { ...
|
|
684
|
+
retryRes = await fetch(fullUrl, { ...fetchOptions, headers });
|
|
678
685
|
} catch (err) {
|
|
679
686
|
throw new CLIError(formatFetchError(err, fullUrl));
|
|
680
687
|
}
|
|
688
|
+
if (passThroughStatuses?.includes(retryRes.status)) {
|
|
689
|
+
return retryRes;
|
|
690
|
+
}
|
|
681
691
|
if (!retryRes.ok) {
|
|
682
692
|
const err = await retryRes.json().catch(() => ({}));
|
|
683
693
|
throw new CLIError(err.error ?? `Request failed: ${retryRes.status}`, retryRes.status === 403 ? 5 : 1);
|
|
684
694
|
}
|
|
685
695
|
return retryRes;
|
|
686
696
|
}
|
|
697
|
+
if (passThroughStatuses?.includes(res.status)) {
|
|
698
|
+
return res;
|
|
699
|
+
}
|
|
687
700
|
if (!res.ok) {
|
|
688
701
|
const err = await res.json().catch(() => ({}));
|
|
689
702
|
const msg = err.message ? `${err.error ?? res.status}: ${err.message}` : err.error ?? `Request failed: ${res.status}`;
|
|
@@ -811,6 +824,55 @@ async function createProject(orgId, name, region, apiUrl) {
|
|
|
811
824
|
const data = await res.json();
|
|
812
825
|
return data.project ?? data;
|
|
813
826
|
}
|
|
827
|
+
async function createBranchApi(parentProjectId, body, apiUrl) {
|
|
828
|
+
const res = await platformFetch(`/projects/v1/${parentProjectId}/branches`, {
|
|
829
|
+
method: "POST",
|
|
830
|
+
body: JSON.stringify(body)
|
|
831
|
+
}, apiUrl);
|
|
832
|
+
const data = await res.json();
|
|
833
|
+
return data.branch;
|
|
834
|
+
}
|
|
835
|
+
async function listBranchesApi(parentProjectId, apiUrl) {
|
|
836
|
+
const res = await platformFetch(`/projects/v1/${parentProjectId}/branches`, {}, apiUrl);
|
|
837
|
+
const data = await res.json();
|
|
838
|
+
return data.data ?? [];
|
|
839
|
+
}
|
|
840
|
+
async function getBranchApi(branchId, apiUrl) {
|
|
841
|
+
const res = await platformFetch(`/projects/v1/branches/${branchId}`, {}, apiUrl);
|
|
842
|
+
const data = await res.json();
|
|
843
|
+
return data.branch;
|
|
844
|
+
}
|
|
845
|
+
async function deleteBranchApi(branchId, apiUrl) {
|
|
846
|
+
await platformFetch(`/projects/v1/branches/${branchId}`, { method: "DELETE" }, apiUrl);
|
|
847
|
+
}
|
|
848
|
+
async function resetBranchApi(branchId, apiUrl) {
|
|
849
|
+
const res = await platformFetch(
|
|
850
|
+
`/projects/v1/branches/${branchId}/reset`,
|
|
851
|
+
{ method: "POST" },
|
|
852
|
+
apiUrl
|
|
853
|
+
);
|
|
854
|
+
const data = await res.json();
|
|
855
|
+
return data.branch;
|
|
856
|
+
}
|
|
857
|
+
async function mergeBranchDryRunApi(branchId, apiUrl) {
|
|
858
|
+
const res = await platformFetch(
|
|
859
|
+
`/projects/v1/branches/${branchId}/merge?dryRun=true`,
|
|
860
|
+
{ method: "POST" },
|
|
861
|
+
apiUrl
|
|
862
|
+
);
|
|
863
|
+
return await res.json();
|
|
864
|
+
}
|
|
865
|
+
async function mergeBranchExecuteApi(branchId, apiUrl) {
|
|
866
|
+
const res = await platformFetch(
|
|
867
|
+
`/projects/v1/branches/${branchId}/merge`,
|
|
868
|
+
{ method: "POST", passThroughStatuses: [409] },
|
|
869
|
+
apiUrl
|
|
870
|
+
);
|
|
871
|
+
if (res.status === 409) {
|
|
872
|
+
return { ok: false, conflict: await res.json() };
|
|
873
|
+
}
|
|
874
|
+
return { ok: true, result: await res.json() };
|
|
875
|
+
}
|
|
814
876
|
|
|
815
877
|
// src/commands/login.ts
|
|
816
878
|
function registerLoginCommand(program2) {
|
|
@@ -1098,20 +1160,517 @@ function registerProjectsCommands(projectsCmd2) {
|
|
|
1098
1160
|
});
|
|
1099
1161
|
}
|
|
1100
1162
|
|
|
1163
|
+
// src/lib/analytics.ts
|
|
1164
|
+
import { PostHog } from "posthog-node";
|
|
1165
|
+
var POSTHOG_API_KEY = "phc_ueV1ii62wdBTkH7E70ugyeqHIHu8dFDdjs0qq3TZhJz";
|
|
1166
|
+
var POSTHOG_HOST = process.env.POSTHOG_HOST || "https://us.i.posthog.com";
|
|
1167
|
+
var client = null;
|
|
1168
|
+
function getClient() {
|
|
1169
|
+
if (!POSTHOG_API_KEY) return null;
|
|
1170
|
+
if (!client) {
|
|
1171
|
+
client = new PostHog(POSTHOG_API_KEY, { host: POSTHOG_HOST });
|
|
1172
|
+
}
|
|
1173
|
+
return client;
|
|
1174
|
+
}
|
|
1175
|
+
function captureEvent(distinctId, event, properties) {
|
|
1176
|
+
try {
|
|
1177
|
+
getClient()?.capture({ distinctId, event, properties });
|
|
1178
|
+
} catch {
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
function trackCommand(command, distinctId, properties) {
|
|
1182
|
+
captureEvent(distinctId, "cli_command_invoked", {
|
|
1183
|
+
command,
|
|
1184
|
+
...properties
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
function trackDiagnose(subcommand, config) {
|
|
1188
|
+
captureEvent(config.project_id, "cli_diagnose_invoked", {
|
|
1189
|
+
subcommand,
|
|
1190
|
+
project_id: config.project_id,
|
|
1191
|
+
project_name: config.project_name,
|
|
1192
|
+
org_id: config.org_id,
|
|
1193
|
+
region: config.region,
|
|
1194
|
+
oss_mode: config.project_id === FAKE_PROJECT_ID
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
function trackPayments(subcommand, config, properties) {
|
|
1198
|
+
captureEvent(config.project_id, "cli_payments_invoked", {
|
|
1199
|
+
subcommand,
|
|
1200
|
+
project_id: config.project_id,
|
|
1201
|
+
project_name: config.project_name,
|
|
1202
|
+
org_id: config.org_id,
|
|
1203
|
+
region: config.region,
|
|
1204
|
+
oss_mode: config.project_id === FAKE_PROJECT_ID,
|
|
1205
|
+
...properties
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
async function shutdownAnalytics() {
|
|
1209
|
+
try {
|
|
1210
|
+
if (client) await client.shutdown();
|
|
1211
|
+
} catch {
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
// src/commands/branch/switch.ts
|
|
1216
|
+
import { copyFileSync, existsSync as existsSync2, unlinkSync as unlinkSync2 } from "fs";
|
|
1217
|
+
async function runBranchSwitch(input) {
|
|
1218
|
+
await requireAuth(input.apiUrl);
|
|
1219
|
+
const current = getProjectConfig();
|
|
1220
|
+
if (!current) {
|
|
1221
|
+
throw new CLIError("No project linked. Run `insforge link` first.");
|
|
1222
|
+
}
|
|
1223
|
+
if (input.toParent && input.name) {
|
|
1224
|
+
throw new CLIError("Pass either a branch name or --parent, not both.");
|
|
1225
|
+
}
|
|
1226
|
+
const projectFile = getProjectConfigFile();
|
|
1227
|
+
const parentBackup = getParentBackupFile();
|
|
1228
|
+
if (input.toParent) {
|
|
1229
|
+
if (!existsSync2(parentBackup)) {
|
|
1230
|
+
throw new CLIError(
|
|
1231
|
+
"No parent backup found. Re-link the directory with `insforge link --project-id <parent>`."
|
|
1232
|
+
);
|
|
1233
|
+
}
|
|
1234
|
+
copyFileSync(parentBackup, projectFile);
|
|
1235
|
+
unlinkSync2(parentBackup);
|
|
1236
|
+
captureEvent(current.project_id, "cli_branch_switch", { direction: "to_parent" });
|
|
1237
|
+
if (!input.silent) {
|
|
1238
|
+
if (input.json) {
|
|
1239
|
+
outputJson({ switched: "parent" });
|
|
1240
|
+
} else {
|
|
1241
|
+
outputSuccess("Switched back to parent.");
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
if (!input.name) {
|
|
1247
|
+
throw new CLIError("Branch name required (or pass --parent).");
|
|
1248
|
+
}
|
|
1249
|
+
const parentId = current.branched_from?.project_id ?? current.project_id;
|
|
1250
|
+
const branches = await listBranchesApi(parentId, input.apiUrl);
|
|
1251
|
+
const target = branches.find((b) => b.name === input.name);
|
|
1252
|
+
if (!target) {
|
|
1253
|
+
throw new CLIError(`Branch '${input.name}' not found.`);
|
|
1254
|
+
}
|
|
1255
|
+
if (target.branch_state !== "ready") {
|
|
1256
|
+
throw new CLIError(
|
|
1257
|
+
`Branch '${input.name}' is in state '${target.branch_state}', cannot switch.`
|
|
1258
|
+
);
|
|
1259
|
+
}
|
|
1260
|
+
if (!existsSync2(parentBackup)) {
|
|
1261
|
+
copyFileSync(projectFile, parentBackup);
|
|
1262
|
+
}
|
|
1263
|
+
const apiKey = await getProjectApiKey(target.id, input.apiUrl);
|
|
1264
|
+
const ossHost = `${target.appkey}.${target.region}.insforge.app`;
|
|
1265
|
+
const branched_from = current.branched_from ?? {
|
|
1266
|
+
project_id: current.project_id,
|
|
1267
|
+
project_name: current.project_name
|
|
1268
|
+
};
|
|
1269
|
+
saveProjectConfig({
|
|
1270
|
+
project_id: target.id,
|
|
1271
|
+
project_name: target.name,
|
|
1272
|
+
org_id: target.organization_id,
|
|
1273
|
+
appkey: target.appkey,
|
|
1274
|
+
region: target.region,
|
|
1275
|
+
api_key: apiKey,
|
|
1276
|
+
oss_host: ossHost,
|
|
1277
|
+
branched_from
|
|
1278
|
+
});
|
|
1279
|
+
captureEvent(parentId, "cli_branch_switch", { direction: "to_branch" });
|
|
1280
|
+
if (!input.silent) {
|
|
1281
|
+
if (input.json) {
|
|
1282
|
+
outputJson({ switched: "branch", branch_id: target.id });
|
|
1283
|
+
} else {
|
|
1284
|
+
outputSuccess(`Switched to branch '${target.name}'.`);
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
function registerBranchSwitchCommand(branch) {
|
|
1289
|
+
branch.command("switch [name]").description("Switch this directory's context to a branch (or back with --parent)").option("--parent", "Switch back to the parent project").action(async (name, opts, cmd) => {
|
|
1290
|
+
const { json, apiUrl } = getRootOpts(cmd);
|
|
1291
|
+
try {
|
|
1292
|
+
await runBranchSwitch({ name, toParent: opts.parent, apiUrl, json });
|
|
1293
|
+
} catch (err) {
|
|
1294
|
+
handleError(err, json);
|
|
1295
|
+
} finally {
|
|
1296
|
+
await shutdownAnalytics();
|
|
1297
|
+
}
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
// src/commands/branch/create.ts
|
|
1302
|
+
var POLL_INTERVAL_MS = 3e3;
|
|
1303
|
+
var POLL_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
1304
|
+
function registerBranchCreateCommand(branch) {
|
|
1305
|
+
branch.command("create <name>").description("Create a branch from the currently linked project").option("--mode <mode>", "full | schema-only", "full").option("--no-switch", "Do not auto-switch context after creation").action(async (name, opts, cmd) => {
|
|
1306
|
+
const { json, apiUrl } = getRootOpts(cmd);
|
|
1307
|
+
try {
|
|
1308
|
+
await requireAuth(apiUrl);
|
|
1309
|
+
const project = getProjectConfig();
|
|
1310
|
+
if (!project) {
|
|
1311
|
+
throw new CLIError("No project linked. Run `insforge link` first.");
|
|
1312
|
+
}
|
|
1313
|
+
if (project.branched_from) {
|
|
1314
|
+
throw new CLIError(
|
|
1315
|
+
"This directory is currently switched to a branch. Run `insforge branch switch --parent` first, then create a new branch from the parent."
|
|
1316
|
+
);
|
|
1317
|
+
}
|
|
1318
|
+
if (opts.mode !== "full" && opts.mode !== "schema-only") {
|
|
1319
|
+
throw new CLIError(`Invalid --mode: ${opts.mode} (must be "full" or "schema-only")`);
|
|
1320
|
+
}
|
|
1321
|
+
const mode = opts.mode;
|
|
1322
|
+
const created = await createBranchApi(project.project_id, { mode, name }, apiUrl);
|
|
1323
|
+
captureEvent(project.project_id, "cli_branch_create", {
|
|
1324
|
+
mode,
|
|
1325
|
+
parent_project_id: project.project_id
|
|
1326
|
+
});
|
|
1327
|
+
if (!json) {
|
|
1328
|
+
outputSuccess(`Branch '${name}' created (appkey: ${created.appkey}). Provisioning\u2026`);
|
|
1329
|
+
}
|
|
1330
|
+
const ready = await pollUntilReady(created.id, apiUrl, !json);
|
|
1331
|
+
if (opts.switch && ready.branch_state === "ready") {
|
|
1332
|
+
await runBranchSwitch({ name, apiUrl, json, silent: json });
|
|
1333
|
+
}
|
|
1334
|
+
if (json) {
|
|
1335
|
+
outputJson({ branch: ready });
|
|
1336
|
+
} else if (ready.branch_state === "ready") {
|
|
1337
|
+
outputSuccess(`Branch '${name}' is ready.`);
|
|
1338
|
+
if (opts.switch) {
|
|
1339
|
+
outputInfo(
|
|
1340
|
+
"\u26A0 Re-source your dev server env (.env) to pick up the new INSFORGE_URL / ANON_KEY."
|
|
1341
|
+
);
|
|
1342
|
+
}
|
|
1343
|
+
} else {
|
|
1344
|
+
outputInfo(
|
|
1345
|
+
`Branch '${name}' is still in '${ready.branch_state}' state. Run \`insforge branch list\` to check.`
|
|
1346
|
+
);
|
|
1347
|
+
}
|
|
1348
|
+
} catch (err) {
|
|
1349
|
+
handleError(err, json);
|
|
1350
|
+
} finally {
|
|
1351
|
+
await shutdownAnalytics();
|
|
1352
|
+
}
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1355
|
+
async function pollUntilReady(branchId, apiUrl, showProgress) {
|
|
1356
|
+
const start = Date.now();
|
|
1357
|
+
let lastState = "";
|
|
1358
|
+
while (Date.now() - start < POLL_TIMEOUT_MS) {
|
|
1359
|
+
const branch2 = await getBranchApi(branchId, apiUrl);
|
|
1360
|
+
if (branch2.branch_state === "ready") return branch2;
|
|
1361
|
+
if (branch2.branch_state === "deleted" || branch2.branch_state === "conflicted") {
|
|
1362
|
+
throw new CLIError(`Branch creation failed (state: ${branch2.branch_state})`);
|
|
1363
|
+
}
|
|
1364
|
+
if (showProgress && branch2.branch_state !== lastState) {
|
|
1365
|
+
outputInfo(` state: ${branch2.branch_state}\u2026`);
|
|
1366
|
+
lastState = branch2.branch_state;
|
|
1367
|
+
}
|
|
1368
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
1369
|
+
}
|
|
1370
|
+
const branch = await getBranchApi(branchId, apiUrl);
|
|
1371
|
+
if (branch.branch_state === "deleted" || branch.branch_state === "conflicted") {
|
|
1372
|
+
throw new CLIError(`Branch creation failed (state: ${branch.branch_state})`);
|
|
1373
|
+
}
|
|
1374
|
+
return branch;
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
// src/commands/branch/list.ts
|
|
1378
|
+
function registerBranchListCommand(branch) {
|
|
1379
|
+
branch.command("list").description("List branches of the currently linked project").action(async (_opts, cmd) => {
|
|
1380
|
+
const { json, apiUrl } = getRootOpts(cmd);
|
|
1381
|
+
try {
|
|
1382
|
+
await requireAuth(apiUrl);
|
|
1383
|
+
const project = getProjectConfig();
|
|
1384
|
+
if (!project) {
|
|
1385
|
+
throw new CLIError("No project linked. Run `insforge link` first.");
|
|
1386
|
+
}
|
|
1387
|
+
const parentId = project.branched_from?.project_id ?? project.project_id;
|
|
1388
|
+
const branches = await listBranchesApi(parentId, apiUrl);
|
|
1389
|
+
captureEvent(parentId, "cli_branch_list", { count: branches.length });
|
|
1390
|
+
if (json) {
|
|
1391
|
+
outputJson({ data: branches });
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1394
|
+
if (branches.length === 0) {
|
|
1395
|
+
outputInfo("No branches.");
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
const currentBranchId = project.branched_from ? project.project_id : null;
|
|
1399
|
+
const rows = branches.map((b) => [
|
|
1400
|
+
b.id === currentBranchId ? "*" : " ",
|
|
1401
|
+
b.name,
|
|
1402
|
+
b.branch_state,
|
|
1403
|
+
b.branch_metadata?.mode ?? "?",
|
|
1404
|
+
new Date(b.branch_created_at).toLocaleString()
|
|
1405
|
+
]);
|
|
1406
|
+
outputTable(["", "Name", "State", "Mode", "Created"], rows);
|
|
1407
|
+
} catch (err) {
|
|
1408
|
+
handleError(err, json);
|
|
1409
|
+
} finally {
|
|
1410
|
+
await shutdownAnalytics();
|
|
1411
|
+
}
|
|
1412
|
+
});
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
// src/commands/branch/merge.ts
|
|
1416
|
+
import { writeFileSync as writeFileSync2 } from "fs";
|
|
1417
|
+
import * as clack5 from "@clack/prompts";
|
|
1418
|
+
function registerBranchMergeCommand(branch) {
|
|
1419
|
+
branch.command("merge <name>").description("Merge a branch back to its parent project").option("--dry-run", "Compute the diff and print rendered SQL; do not apply").option("-y, --yes", "Skip confirmation prompt").option("--save-sql <path>", "Write rendered SQL preview to a file").action(async (name, opts, cmd) => {
|
|
1420
|
+
const { json, apiUrl } = getRootOpts(cmd);
|
|
1421
|
+
try {
|
|
1422
|
+
await requireAuth(apiUrl);
|
|
1423
|
+
const project = getProjectConfig();
|
|
1424
|
+
if (!project) throw new CLIError("No project linked. Run `insforge link` first.");
|
|
1425
|
+
const parentId = project.branched_from?.project_id ?? project.project_id;
|
|
1426
|
+
const branches = await listBranchesApi(parentId, apiUrl);
|
|
1427
|
+
const target = branches.find((b) => b.name === name);
|
|
1428
|
+
if (!target) throw new CLIError(`Branch '${name}' not found.`);
|
|
1429
|
+
const diff = await mergeBranchDryRunApi(target.id, apiUrl);
|
|
1430
|
+
if (opts.saveSql) {
|
|
1431
|
+
writeFileSync2(opts.saveSql, diff.rendered_sql);
|
|
1432
|
+
if (!json) outputInfo(`SQL preview saved to ${opts.saveSql}`);
|
|
1433
|
+
}
|
|
1434
|
+
if (!json) {
|
|
1435
|
+
console.log(diff.rendered_sql);
|
|
1436
|
+
console.log();
|
|
1437
|
+
outputInfo(
|
|
1438
|
+
`${diff.summary.added} added, ${diff.summary.modified} modified, ${diff.summary.conflicts} conflict(s).`
|
|
1439
|
+
);
|
|
1440
|
+
}
|
|
1441
|
+
if (diff.summary.conflicts > 0) {
|
|
1442
|
+
captureEvent(parentId, "cli_branch_merge_conflict", {
|
|
1443
|
+
conflicts: diff.summary.conflicts
|
|
1444
|
+
});
|
|
1445
|
+
if (json) {
|
|
1446
|
+
outputJson({
|
|
1447
|
+
diff,
|
|
1448
|
+
applied: false,
|
|
1449
|
+
dryRun: !!opts.dryRun,
|
|
1450
|
+
error: "merge_conflict"
|
|
1451
|
+
});
|
|
1452
|
+
} else {
|
|
1453
|
+
outputInfo("");
|
|
1454
|
+
outputInfo("Merge blocked: resolve conflicts before retrying.");
|
|
1455
|
+
for (const c of diff.conflicts) {
|
|
1456
|
+
outputInfo(` - ${c.schema}.${c.object} [${c.type}] \u2014 ${c.hint}`);
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
process.exit(2);
|
|
1460
|
+
}
|
|
1461
|
+
if (opts.dryRun) {
|
|
1462
|
+
captureEvent(parentId, "cli_branch_merge", {
|
|
1463
|
+
dry_run: true,
|
|
1464
|
+
conflicts: 0,
|
|
1465
|
+
applied: false
|
|
1466
|
+
});
|
|
1467
|
+
if (json) {
|
|
1468
|
+
outputJson({ diff, applied: false, dryRun: true });
|
|
1469
|
+
}
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
1472
|
+
if (!opts.yes && !json) {
|
|
1473
|
+
const parentLabel = project.branched_from?.project_name ?? project.project_name;
|
|
1474
|
+
const confirmed = await clack5.confirm({
|
|
1475
|
+
message: `Apply this merge to parent project '${parentLabel}'?`
|
|
1476
|
+
});
|
|
1477
|
+
if (clack5.isCancel(confirmed) || !confirmed) {
|
|
1478
|
+
outputInfo("Merge cancelled.");
|
|
1479
|
+
return;
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
const result = await mergeBranchExecuteApi(target.id, apiUrl);
|
|
1483
|
+
if (!result.ok) {
|
|
1484
|
+
captureEvent(parentId, "cli_branch_merge_conflict", {
|
|
1485
|
+
conflicts: result.conflict.diff.summary.conflicts
|
|
1486
|
+
});
|
|
1487
|
+
if (json) {
|
|
1488
|
+
outputJson({
|
|
1489
|
+
diff: result.conflict.diff,
|
|
1490
|
+
applied: false,
|
|
1491
|
+
dryRun: false,
|
|
1492
|
+
error: "merge_conflict"
|
|
1493
|
+
});
|
|
1494
|
+
} else {
|
|
1495
|
+
outputInfo("Merge blocked by a conflict that appeared between dry-run and apply:");
|
|
1496
|
+
for (const c of result.conflict.diff.conflicts) {
|
|
1497
|
+
outputInfo(` - ${c.schema}.${c.object} [${c.type}] \u2014 ${c.hint}`);
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
process.exit(2);
|
|
1501
|
+
}
|
|
1502
|
+
captureEvent(parentId, "cli_branch_merge", {
|
|
1503
|
+
dry_run: false,
|
|
1504
|
+
conflicts: 0,
|
|
1505
|
+
applied: true
|
|
1506
|
+
});
|
|
1507
|
+
if (json) {
|
|
1508
|
+
outputJson({ ...result.result, diff, applied: true, dryRun: false });
|
|
1509
|
+
} else {
|
|
1510
|
+
outputSuccess(`Merged. Branch '${name}' is now in 'merged' state.`);
|
|
1511
|
+
outputInfo("\u26A0 Reminder: redeploy edge functions, website, and compute as needed.");
|
|
1512
|
+
}
|
|
1513
|
+
} catch (err) {
|
|
1514
|
+
handleError(err, json);
|
|
1515
|
+
} finally {
|
|
1516
|
+
await shutdownAnalytics();
|
|
1517
|
+
}
|
|
1518
|
+
});
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
// src/commands/branch/reset.ts
|
|
1522
|
+
import * as clack6 from "@clack/prompts";
|
|
1523
|
+
var POLL_INTERVAL_MS2 = 3e3;
|
|
1524
|
+
var POLL_TIMEOUT_MS2 = 5 * 60 * 1e3;
|
|
1525
|
+
function registerBranchResetCommand(branch) {
|
|
1526
|
+
branch.command("reset <name>").description("Reset a branch's database back to T0 (the parent snapshot at branch creation)").option("-y, --yes", "Skip confirmation").action(async (name, opts, cmd) => {
|
|
1527
|
+
const { json, apiUrl } = getRootOpts(cmd);
|
|
1528
|
+
try {
|
|
1529
|
+
await requireAuth(apiUrl);
|
|
1530
|
+
const project = getProjectConfig();
|
|
1531
|
+
if (!project) throw new CLIError("No project linked. Run `insforge link` first.");
|
|
1532
|
+
const parentId = project.branched_from?.project_id ?? project.project_id;
|
|
1533
|
+
const branches = await listBranchesApi(parentId, apiUrl);
|
|
1534
|
+
const target = branches.find((b) => b.name === name);
|
|
1535
|
+
if (!target) throw new CLIError(`Branch '${name}' not found.`);
|
|
1536
|
+
if (target.branch_state !== "ready" && target.branch_state !== "merged") {
|
|
1537
|
+
throw new CLIError(
|
|
1538
|
+
`Branch '${name}' is in '${target.branch_state}' state; reset requires 'ready' or 'merged'.`
|
|
1539
|
+
);
|
|
1540
|
+
}
|
|
1541
|
+
const entryState = target.branch_state;
|
|
1542
|
+
if (!opts.yes && !json) {
|
|
1543
|
+
const confirmed = await clack6.confirm({
|
|
1544
|
+
message: `Reset branch '${name}' back to T0? This wipes all schema/data/policy/function/migration changes made on the branch since creation.` + (entryState === "merged" ? " (Branch is currently merged \u2014 reset will reopen it for further work.)" : "")
|
|
1545
|
+
});
|
|
1546
|
+
if (clack6.isCancel(confirmed) || !confirmed) {
|
|
1547
|
+
outputInfo("Cancelled.");
|
|
1548
|
+
return;
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
const initial = await resetBranchApi(target.id, apiUrl);
|
|
1552
|
+
captureEvent(parentId, "cli_branch_reset", {
|
|
1553
|
+
entry_state: entryState,
|
|
1554
|
+
mode: target.branch_metadata?.mode
|
|
1555
|
+
});
|
|
1556
|
+
if (!json) {
|
|
1557
|
+
outputSuccess(`Reset enqueued for branch '${name}'. Restoring T0\u2026`);
|
|
1558
|
+
}
|
|
1559
|
+
const final = await pollUntilReady2(target.id, apiUrl, !json, initial.branch_state);
|
|
1560
|
+
if (json) {
|
|
1561
|
+
outputJson({ branch: final });
|
|
1562
|
+
} else if (final.branch_state === "ready") {
|
|
1563
|
+
outputSuccess(`Branch '${name}' is back to T0 and ready.`);
|
|
1564
|
+
outputInfo("\u26A0 Reminder: edge functions, website, and compute aren\u2019t touched by reset; redeploy if needed.");
|
|
1565
|
+
} else {
|
|
1566
|
+
outputInfo(
|
|
1567
|
+
`Branch '${name}' is still in '${final.branch_state}' state. Run \`insforge branch list\` to check.`
|
|
1568
|
+
);
|
|
1569
|
+
}
|
|
1570
|
+
} catch (err) {
|
|
1571
|
+
handleError(err, json);
|
|
1572
|
+
} finally {
|
|
1573
|
+
await shutdownAnalytics();
|
|
1574
|
+
}
|
|
1575
|
+
});
|
|
1576
|
+
}
|
|
1577
|
+
async function pollUntilReady2(branchId, apiUrl, showProgress, startingState) {
|
|
1578
|
+
const start = Date.now();
|
|
1579
|
+
let lastState = startingState;
|
|
1580
|
+
if (showProgress) outputInfo(` state: ${startingState}\u2026`);
|
|
1581
|
+
while (Date.now() - start < POLL_TIMEOUT_MS2) {
|
|
1582
|
+
const branch2 = await getBranchApi(branchId, apiUrl);
|
|
1583
|
+
if (branch2.branch_state === "ready") return branch2;
|
|
1584
|
+
if (branch2.branch_state === "merged") return branch2;
|
|
1585
|
+
if (branch2.branch_state === "deleted" || branch2.branch_state === "conflicted") {
|
|
1586
|
+
throw new CLIError(`Branch reset failed (state: ${branch2.branch_state})`);
|
|
1587
|
+
}
|
|
1588
|
+
if (showProgress && branch2.branch_state !== lastState) {
|
|
1589
|
+
outputInfo(` state: ${branch2.branch_state}\u2026`);
|
|
1590
|
+
lastState = branch2.branch_state;
|
|
1591
|
+
}
|
|
1592
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS2));
|
|
1593
|
+
}
|
|
1594
|
+
const branch = await getBranchApi(branchId, apiUrl);
|
|
1595
|
+
if (branch.branch_state === "deleted" || branch.branch_state === "conflicted") {
|
|
1596
|
+
throw new CLIError(`Branch reset failed (state: ${branch.branch_state})`);
|
|
1597
|
+
}
|
|
1598
|
+
return branch;
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
// src/commands/branch/delete.ts
|
|
1602
|
+
import * as clack7 from "@clack/prompts";
|
|
1603
|
+
function registerBranchDeleteCommand(branch) {
|
|
1604
|
+
branch.command("delete <name>").description("Delete a branch").option("-y, --yes", "Skip confirmation").action(async (name, opts, cmd) => {
|
|
1605
|
+
const { json, apiUrl } = getRootOpts(cmd);
|
|
1606
|
+
try {
|
|
1607
|
+
await requireAuth(apiUrl);
|
|
1608
|
+
const project = getProjectConfig();
|
|
1609
|
+
if (!project) throw new CLIError("No project linked. Run `insforge link` first.");
|
|
1610
|
+
const parentId = project.branched_from?.project_id ?? project.project_id;
|
|
1611
|
+
const branches = await listBranchesApi(parentId, apiUrl);
|
|
1612
|
+
const target = branches.find((b) => b.name === name);
|
|
1613
|
+
if (!target) throw new CLIError(`Branch '${name}' not found.`);
|
|
1614
|
+
if (!opts.yes && !json) {
|
|
1615
|
+
const confirmed = await clack7.confirm({
|
|
1616
|
+
message: `Delete branch '${name}'? This terminates its EC2 instance.`
|
|
1617
|
+
});
|
|
1618
|
+
if (clack7.isCancel(confirmed) || !confirmed) {
|
|
1619
|
+
outputInfo("Cancelled.");
|
|
1620
|
+
return;
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
await deleteBranchApi(target.id, apiUrl);
|
|
1624
|
+
captureEvent(parentId, "cli_branch_delete", {});
|
|
1625
|
+
const currentlyOnDeleted = project.project_id === target.id;
|
|
1626
|
+
if (currentlyOnDeleted) {
|
|
1627
|
+
try {
|
|
1628
|
+
await runBranchSwitch({ toParent: true, apiUrl, json, silent: json });
|
|
1629
|
+
} catch (err) {
|
|
1630
|
+
outputInfo(
|
|
1631
|
+
`Switched-to-parent failed (${err.message}). Run \`insforge branch switch --parent\` manually.`
|
|
1632
|
+
);
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
if (json) {
|
|
1636
|
+
outputJson({ deleted: true, branch_id: target.id, switched_back: currentlyOnDeleted });
|
|
1637
|
+
} else {
|
|
1638
|
+
outputSuccess(`Branch '${name}' deletion enqueued.`);
|
|
1639
|
+
if (currentlyOnDeleted) outputInfo("Switched back to parent.");
|
|
1640
|
+
}
|
|
1641
|
+
} catch (err) {
|
|
1642
|
+
handleError(err, json);
|
|
1643
|
+
} finally {
|
|
1644
|
+
await shutdownAnalytics();
|
|
1645
|
+
}
|
|
1646
|
+
});
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
// src/commands/branch/index.ts
|
|
1650
|
+
function registerBranchCommands(program2) {
|
|
1651
|
+
const branch = program2.command("branch").description("Manage backend branches");
|
|
1652
|
+
registerBranchCreateCommand(branch);
|
|
1653
|
+
registerBranchListCommand(branch);
|
|
1654
|
+
registerBranchSwitchCommand(branch);
|
|
1655
|
+
registerBranchMergeCommand(branch);
|
|
1656
|
+
registerBranchResetCommand(branch);
|
|
1657
|
+
registerBranchDeleteCommand(branch);
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1101
1660
|
// src/commands/projects/link.ts
|
|
1102
1661
|
import { exec as exec3 } from "child_process";
|
|
1103
|
-
import { promisify as
|
|
1104
|
-
import * as
|
|
1105
|
-
import * as
|
|
1106
|
-
import * as
|
|
1662
|
+
import { promisify as promisify4 } from "util";
|
|
1663
|
+
import * as fs5 from "fs/promises";
|
|
1664
|
+
import * as path5 from "path";
|
|
1665
|
+
import * as clack12 from "@clack/prompts";
|
|
1107
1666
|
import pc2 from "picocolors";
|
|
1108
1667
|
|
|
1109
1668
|
// src/lib/skills.ts
|
|
1110
1669
|
import { exec } from "child_process";
|
|
1111
|
-
import { existsSync as
|
|
1670
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2, appendFileSync } from "fs";
|
|
1112
1671
|
import { join as join2 } from "path";
|
|
1113
1672
|
import { promisify } from "util";
|
|
1114
|
-
import * as
|
|
1673
|
+
import * as clack8 from "@clack/prompts";
|
|
1115
1674
|
var execAsync = promisify(exec);
|
|
1116
1675
|
var SKILL_INSTALL_TIMEOUT_MS = 6e4;
|
|
1117
1676
|
function describeExecError(err) {
|
|
@@ -1152,7 +1711,7 @@ var GITIGNORE_ENTRIES = [
|
|
|
1152
1711
|
];
|
|
1153
1712
|
function updateGitignore() {
|
|
1154
1713
|
const gitignorePath = join2(process.cwd(), ".gitignore");
|
|
1155
|
-
const existing =
|
|
1714
|
+
const existing = existsSync3(gitignorePath) ? readFileSync2(gitignorePath, "utf-8") : "";
|
|
1156
1715
|
const lines = new Set(existing.split("\n").map((l) => l.trim()));
|
|
1157
1716
|
const missing = GITIGNORE_ENTRIES.filter((entry) => !lines.has(entry));
|
|
1158
1717
|
if (!missing.length) return;
|
|
@@ -1164,29 +1723,29 @@ ${missing.join("\n")}
|
|
|
1164
1723
|
}
|
|
1165
1724
|
async function installSkills(json) {
|
|
1166
1725
|
try {
|
|
1167
|
-
if (!json)
|
|
1726
|
+
if (!json) clack8.log.info("Installing InsForge agent skills (global)...");
|
|
1168
1727
|
await execAsync("npx skills add insforge/agent-skills -g -y -a antigravity -a augment -a claude-code -a cline -a codex -a cursor -a gemini-cli -a github-copilot -a kilo -a qoder -a qwen-code -a roo -a trae -a windsurf", {
|
|
1169
1728
|
cwd: process.cwd(),
|
|
1170
1729
|
timeout: SKILL_INSTALL_TIMEOUT_MS
|
|
1171
1730
|
});
|
|
1172
|
-
if (!json)
|
|
1731
|
+
if (!json) clack8.log.success("InsForge agent skills installed.");
|
|
1173
1732
|
} catch (err) {
|
|
1174
1733
|
if (!json) {
|
|
1175
|
-
|
|
1176
|
-
|
|
1734
|
+
clack8.log.warn(`Could not install agent skills: ${describeExecError(err)}`);
|
|
1735
|
+
clack8.log.info("Run `npx skills add insforge/agent-skills` once resolved to see the full output.");
|
|
1177
1736
|
}
|
|
1178
1737
|
}
|
|
1179
1738
|
try {
|
|
1180
|
-
if (!json)
|
|
1739
|
+
if (!json) clack8.log.info("Installing find-skills (global)...");
|
|
1181
1740
|
await execAsync("npx skills add https://github.com/vercel-labs/skills --skill find-skills -g -y", {
|
|
1182
1741
|
cwd: process.cwd(),
|
|
1183
1742
|
timeout: SKILL_INSTALL_TIMEOUT_MS
|
|
1184
1743
|
});
|
|
1185
|
-
if (!json)
|
|
1744
|
+
if (!json) clack8.log.success("find-skills installed.");
|
|
1186
1745
|
} catch (err) {
|
|
1187
1746
|
if (!json) {
|
|
1188
|
-
|
|
1189
|
-
|
|
1747
|
+
clack8.log.warn(`Could not install find-skills: ${describeExecError(err)}`);
|
|
1748
|
+
clack8.log.info("Run `npx skills add https://github.com/vercel-labs/skills --skill find-skills` once resolved.");
|
|
1190
1749
|
}
|
|
1191
1750
|
}
|
|
1192
1751
|
try {
|
|
@@ -1235,65 +1794,14 @@ async function reportCliUsage(toolName, success, maxRetries = 1, explicitConfig)
|
|
|
1235
1794
|
}
|
|
1236
1795
|
}
|
|
1237
1796
|
|
|
1238
|
-
// src/
|
|
1239
|
-
import {
|
|
1240
|
-
|
|
1241
|
-
var POSTHOG_HOST = process.env.POSTHOG_HOST || "https://us.i.posthog.com";
|
|
1242
|
-
var client = null;
|
|
1243
|
-
function getClient() {
|
|
1244
|
-
if (!POSTHOG_API_KEY) return null;
|
|
1245
|
-
if (!client) {
|
|
1246
|
-
client = new PostHog(POSTHOG_API_KEY, { host: POSTHOG_HOST });
|
|
1247
|
-
}
|
|
1248
|
-
return client;
|
|
1249
|
-
}
|
|
1250
|
-
function captureEvent(distinctId, event, properties) {
|
|
1251
|
-
try {
|
|
1252
|
-
getClient()?.capture({ distinctId, event, properties });
|
|
1253
|
-
} catch {
|
|
1254
|
-
}
|
|
1255
|
-
}
|
|
1256
|
-
function trackCommand(command, distinctId, properties) {
|
|
1257
|
-
captureEvent(distinctId, "cli_command_invoked", {
|
|
1258
|
-
command,
|
|
1259
|
-
...properties
|
|
1260
|
-
});
|
|
1261
|
-
}
|
|
1262
|
-
function trackDiagnose(subcommand, config) {
|
|
1263
|
-
captureEvent(config.project_id, "cli_diagnose_invoked", {
|
|
1264
|
-
subcommand,
|
|
1265
|
-
project_id: config.project_id,
|
|
1266
|
-
project_name: config.project_name,
|
|
1267
|
-
org_id: config.org_id,
|
|
1268
|
-
region: config.region,
|
|
1269
|
-
oss_mode: config.project_id === FAKE_PROJECT_ID
|
|
1270
|
-
});
|
|
1271
|
-
}
|
|
1272
|
-
function trackPayments(subcommand, config, properties) {
|
|
1273
|
-
captureEvent(config.project_id, "cli_payments_invoked", {
|
|
1274
|
-
subcommand,
|
|
1275
|
-
project_id: config.project_id,
|
|
1276
|
-
project_name: config.project_name,
|
|
1277
|
-
org_id: config.org_id,
|
|
1278
|
-
region: config.region,
|
|
1279
|
-
oss_mode: config.project_id === FAKE_PROJECT_ID,
|
|
1280
|
-
...properties
|
|
1281
|
-
});
|
|
1282
|
-
}
|
|
1283
|
-
async function shutdownAnalytics() {
|
|
1284
|
-
try {
|
|
1285
|
-
if (client) await client.shutdown();
|
|
1286
|
-
} catch {
|
|
1287
|
-
}
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
|
-
// src/commands/create.ts
|
|
1291
|
-
import { exec as exec2 } from "child_process";
|
|
1797
|
+
// src/auth-providers/apply.ts
|
|
1798
|
+
import { promises as fs } from "fs";
|
|
1799
|
+
import * as path from "path";
|
|
1292
1800
|
import { tmpdir } from "os";
|
|
1801
|
+
import { execFile } from "child_process";
|
|
1293
1802
|
import { promisify as promisify2 } from "util";
|
|
1294
|
-
import
|
|
1295
|
-
import * as
|
|
1296
|
-
import * as clack7 from "@clack/prompts";
|
|
1803
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
1804
|
+
import * as clack9 from "@clack/prompts";
|
|
1297
1805
|
|
|
1298
1806
|
// src/lib/api/oss.ts
|
|
1299
1807
|
function requireProjectConfig() {
|
|
@@ -1318,14 +1826,23 @@ async function getAnonKey() {
|
|
|
1318
1826
|
const data = await res.json();
|
|
1319
1827
|
return data.accessToken;
|
|
1320
1828
|
}
|
|
1321
|
-
async function
|
|
1829
|
+
async function getJwtSecret() {
|
|
1830
|
+
try {
|
|
1831
|
+
const res = await ossFetch("/api/secrets/JWT_SECRET");
|
|
1832
|
+
const data = await res.json();
|
|
1833
|
+
return typeof data.value === "string" && data.value.length > 0 ? data.value : null;
|
|
1834
|
+
} catch {
|
|
1835
|
+
return null;
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
async function ossFetch(path6, options = {}) {
|
|
1322
1839
|
const config = requireProjectConfig();
|
|
1323
1840
|
const headers = {
|
|
1324
1841
|
"Content-Type": "application/json",
|
|
1325
1842
|
Authorization: `Bearer ${config.api_key}`,
|
|
1326
1843
|
...options.headers ?? {}
|
|
1327
1844
|
};
|
|
1328
|
-
const res = await fetch(`${config.oss_host}${
|
|
1845
|
+
const res = await fetch(`${config.oss_host}${path6}`, { ...options, headers });
|
|
1329
1846
|
if (!res.ok) {
|
|
1330
1847
|
const err = await res.json().catch(() => ({}));
|
|
1331
1848
|
let message = err.message ?? err.error ?? `OSS request failed: ${res.status}`;
|
|
@@ -1334,13 +1851,13 @@ async function ossFetch(path5, options = {}) {
|
|
|
1334
1851
|
${err.nextActions}`;
|
|
1335
1852
|
}
|
|
1336
1853
|
const isRouteLevel404 = !err.error || err.error === "NOT_FOUND";
|
|
1337
|
-
if (res.status === 404 && isRouteLevel404 &&
|
|
1854
|
+
if (res.status === 404 && isRouteLevel404 && path6.startsWith("/api/compute")) {
|
|
1338
1855
|
message = "Compute services are not available on this backend.\nSelf-hosted: upgrade your InsForge instance. Cloud: contact your InsForge admin to enable compute.";
|
|
1339
1856
|
}
|
|
1340
|
-
if (res.status === 404 && isRouteLevel404 &&
|
|
1857
|
+
if (res.status === 404 && isRouteLevel404 && path6.startsWith("/api/payments")) {
|
|
1341
1858
|
message = "Payments are not available on this backend.\nSelf-hosted: upgrade your InsForge instance. Cloud/private preview: contact your InsForge admin to enable payments.";
|
|
1342
1859
|
}
|
|
1343
|
-
if (res.status === 404 && isRouteLevel404 &&
|
|
1860
|
+
if (res.status === 404 && isRouteLevel404 && path6 === "/api/database/migrations") {
|
|
1344
1861
|
message = "Database migrations are not available on this backend.\nSelf-hosted: upgrade your InsForge instance. Cloud: contact your InsForge admin about database migration support.";
|
|
1345
1862
|
}
|
|
1346
1863
|
throw new CLIError(message);
|
|
@@ -1348,16 +1865,231 @@ ${err.nextActions}`;
|
|
|
1348
1865
|
return res;
|
|
1349
1866
|
}
|
|
1350
1867
|
|
|
1868
|
+
// src/auth-providers/apply.ts
|
|
1869
|
+
var execFileAsync = promisify2(execFile);
|
|
1870
|
+
var VALID_AUTH_PROVIDERS = ["better-auth"];
|
|
1871
|
+
function pathExists(p) {
|
|
1872
|
+
return fs.stat(p).then(() => true, () => false);
|
|
1873
|
+
}
|
|
1874
|
+
function deepMergeKeepBase(base, patch) {
|
|
1875
|
+
const out = { ...base };
|
|
1876
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
1877
|
+
if (v && typeof v === "object" && !Array.isArray(v) && out[k] && typeof out[k] === "object" && !Array.isArray(out[k])) {
|
|
1878
|
+
out[k] = deepMergeKeepBase(out[k], v);
|
|
1879
|
+
} else if (out[k] === void 0) {
|
|
1880
|
+
out[k] = v;
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
return out;
|
|
1884
|
+
}
|
|
1885
|
+
function extractEnvKeys(content) {
|
|
1886
|
+
const keys = /* @__PURE__ */ new Set();
|
|
1887
|
+
for (const line of content.split("\n")) {
|
|
1888
|
+
const trimmed = line.replace(/^\s*export\s+/, "").trimStart();
|
|
1889
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
1890
|
+
const m = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=/);
|
|
1891
|
+
if (m) keys.add(m[1]);
|
|
1892
|
+
}
|
|
1893
|
+
return keys;
|
|
1894
|
+
}
|
|
1895
|
+
function filterCollidingEnvLines(append, existingKeys) {
|
|
1896
|
+
const dropped = [];
|
|
1897
|
+
const out = [];
|
|
1898
|
+
for (const line of append.split("\n")) {
|
|
1899
|
+
const m = line.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=/);
|
|
1900
|
+
if (m && existingKeys.has(m[1])) {
|
|
1901
|
+
dropped.push(m[1]);
|
|
1902
|
+
continue;
|
|
1903
|
+
}
|
|
1904
|
+
out.push(line);
|
|
1905
|
+
}
|
|
1906
|
+
return { filtered: out.join("\n"), dropped };
|
|
1907
|
+
}
|
|
1908
|
+
async function walkFiles(dir, base = dir) {
|
|
1909
|
+
const out = [];
|
|
1910
|
+
for (const entry of await fs.readdir(dir, { withFileTypes: true })) {
|
|
1911
|
+
const full = path.join(dir, entry.name);
|
|
1912
|
+
if (entry.isDirectory()) out.push(...await walkFiles(full, base));
|
|
1913
|
+
else out.push(path.relative(base, full));
|
|
1914
|
+
}
|
|
1915
|
+
return out;
|
|
1916
|
+
}
|
|
1917
|
+
var PROVIDER_META_FILES = /* @__PURE__ */ new Set(["manifest.json", "README.md"]);
|
|
1918
|
+
var SAFE_REPO_PATTERN = /^(https?:\/\/|git@)[A-Za-z0-9._:/@~+-]+(\.git)?$/;
|
|
1919
|
+
var SAFE_BRANCH_PATTERN = /^[A-Za-z0-9._/-]+$/;
|
|
1920
|
+
async function fetchProviderTree(provider) {
|
|
1921
|
+
const tempDir = path.join(tmpdir(), `insforge-auth-${provider}-${Date.now()}`);
|
|
1922
|
+
await fs.mkdir(tempDir, { recursive: true });
|
|
1923
|
+
const cleanup = () => fs.rm(tempDir, { recursive: true, force: true }).catch(() => void 0);
|
|
1924
|
+
try {
|
|
1925
|
+
const repo = process.env.INSFORGE_TEMPLATES_REPO ?? "https://github.com/InsForge/insforge-templates.git";
|
|
1926
|
+
if (!SAFE_REPO_PATTERN.test(repo)) {
|
|
1927
|
+
throw new Error(`INSFORGE_TEMPLATES_REPO has unsupported characters: ${repo}`);
|
|
1928
|
+
}
|
|
1929
|
+
const branch = process.env.INSFORGE_TEMPLATES_BRANCH;
|
|
1930
|
+
if (branch !== void 0 && !SAFE_BRANCH_PATTERN.test(branch)) {
|
|
1931
|
+
throw new Error(`INSFORGE_TEMPLATES_BRANCH has unsupported characters: ${branch}`);
|
|
1932
|
+
}
|
|
1933
|
+
const args = ["clone", "--depth", "1"];
|
|
1934
|
+
if (branch) args.push("-b", branch);
|
|
1935
|
+
args.push("--", repo, ".");
|
|
1936
|
+
await execFileAsync("git", args, {
|
|
1937
|
+
cwd: tempDir,
|
|
1938
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
1939
|
+
timeout: 6e4
|
|
1940
|
+
});
|
|
1941
|
+
const providerDir = path.join(tempDir, "auth-providers", provider);
|
|
1942
|
+
if (!await pathExists(providerDir)) {
|
|
1943
|
+
await cleanup();
|
|
1944
|
+
throw new Error(
|
|
1945
|
+
`Auth provider "${provider}" not found in templates repo. Looked for auth-providers/${provider}/ in ${repo}${branch ? ` (branch: ${branch})` : ""}.`
|
|
1946
|
+
);
|
|
1947
|
+
}
|
|
1948
|
+
return { dir: providerDir, cleanup };
|
|
1949
|
+
} catch (err) {
|
|
1950
|
+
await cleanup();
|
|
1951
|
+
throw err;
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
async function loadManifest(providerDir) {
|
|
1955
|
+
const manifestPath = path.join(providerDir, "manifest.json");
|
|
1956
|
+
if (!await pathExists(manifestPath)) {
|
|
1957
|
+
throw new Error(`Missing manifest.json in ${providerDir}`);
|
|
1958
|
+
}
|
|
1959
|
+
const parsed = JSON.parse(await fs.readFile(manifestPath, "utf-8"));
|
|
1960
|
+
const missing = [];
|
|
1961
|
+
if (typeof parsed.name !== "string") missing.push("name");
|
|
1962
|
+
if (!Array.isArray(parsed.files)) missing.push("files (array)");
|
|
1963
|
+
if (!parsed.packageJsonPatch || typeof parsed.packageJsonPatch !== "object") missing.push("packageJsonPatch (object)");
|
|
1964
|
+
if (typeof parsed.envExampleAppend !== "string") missing.push("envExampleAppend (string)");
|
|
1965
|
+
if (typeof parsed.nextSteps !== "string") missing.push("nextSteps (string)");
|
|
1966
|
+
if (missing.length > 0) {
|
|
1967
|
+
throw new Error(`Malformed manifest.json in ${providerDir} \u2014 missing or wrong-typed fields: ${missing.join(", ")}`);
|
|
1968
|
+
}
|
|
1969
|
+
return parsed;
|
|
1970
|
+
}
|
|
1971
|
+
async function applyAuthProvider(provider, cwd, projectConfig, json) {
|
|
1972
|
+
if (!VALID_AUTH_PROVIDERS.includes(provider)) {
|
|
1973
|
+
throw new Error(`Unknown auth provider: ${provider}`);
|
|
1974
|
+
}
|
|
1975
|
+
const fetchSpinner = !json ? clack9.spinner() : null;
|
|
1976
|
+
fetchSpinner?.start(`Fetching ${provider} scaffold from templates repo...`);
|
|
1977
|
+
const { dir: providerDir, cleanup } = await fetchProviderTree(provider);
|
|
1978
|
+
fetchSpinner?.stop(`${provider} scaffold ready`);
|
|
1979
|
+
try {
|
|
1980
|
+
const manifest = await loadManifest(providerDir);
|
|
1981
|
+
const result = {
|
|
1982
|
+
written: [],
|
|
1983
|
+
skipped: [],
|
|
1984
|
+
overwritten: [],
|
|
1985
|
+
packageJsonPatched: false,
|
|
1986
|
+
envExampleAppended: false,
|
|
1987
|
+
envLocalWritten: false,
|
|
1988
|
+
envKeysSkipped: [],
|
|
1989
|
+
nextSteps: manifest.nextSteps
|
|
1990
|
+
};
|
|
1991
|
+
const allFiles = (await walkFiles(providerDir)).filter((rel) => !PROVIDER_META_FILES.has(rel));
|
|
1992
|
+
for (const rel of allFiles) {
|
|
1993
|
+
const src = path.join(providerDir, rel);
|
|
1994
|
+
const dest = path.join(cwd, rel);
|
|
1995
|
+
const existed = await pathExists(dest);
|
|
1996
|
+
await fs.mkdir(path.dirname(dest), { recursive: true });
|
|
1997
|
+
await fs.copyFile(src, dest);
|
|
1998
|
+
if (existed) result.overwritten.push(rel);
|
|
1999
|
+
else result.written.push(rel);
|
|
2000
|
+
}
|
|
2001
|
+
const pkgPath = path.join(cwd, "package.json");
|
|
2002
|
+
if (await pathExists(pkgPath)) {
|
|
2003
|
+
const existing = JSON.parse(await fs.readFile(pkgPath, "utf-8"));
|
|
2004
|
+
const merged = deepMergeKeepBase(existing, manifest.packageJsonPatch);
|
|
2005
|
+
await fs.writeFile(pkgPath, JSON.stringify(merged, null, 2) + "\n");
|
|
2006
|
+
result.packageJsonPatched = true;
|
|
2007
|
+
} else {
|
|
2008
|
+
const fresh = {
|
|
2009
|
+
name: path.basename(cwd),
|
|
2010
|
+
version: "0.0.1",
|
|
2011
|
+
private: true,
|
|
2012
|
+
...manifest.packageJsonPatch
|
|
2013
|
+
};
|
|
2014
|
+
await fs.writeFile(pkgPath, JSON.stringify(fresh, null, 2) + "\n");
|
|
2015
|
+
result.packageJsonPatched = true;
|
|
2016
|
+
}
|
|
2017
|
+
const envExamplePath = path.join(cwd, ".env.example");
|
|
2018
|
+
if (await pathExists(envExamplePath)) {
|
|
2019
|
+
const existing = await fs.readFile(envExamplePath, "utf-8");
|
|
2020
|
+
if (!existing.includes("# \u2500\u2500\u2500 Better Auth + InsForge bridge")) {
|
|
2021
|
+
const existingKeys = extractEnvKeys(existing);
|
|
2022
|
+
const { filtered, dropped } = filterCollidingEnvLines(manifest.envExampleAppend, existingKeys);
|
|
2023
|
+
result.envKeysSkipped = dropped;
|
|
2024
|
+
await fs.writeFile(envExamplePath, existing.replace(/\n*$/, "\n\n") + filtered + "\n");
|
|
2025
|
+
result.envExampleAppended = true;
|
|
2026
|
+
}
|
|
2027
|
+
} else {
|
|
2028
|
+
await fs.writeFile(envExamplePath, manifest.envExampleAppend + "\n");
|
|
2029
|
+
result.envExampleAppended = true;
|
|
2030
|
+
}
|
|
2031
|
+
const envLocalPath = path.join(cwd, ".env.local");
|
|
2032
|
+
const envLocalExists = await pathExists(envLocalPath);
|
|
2033
|
+
const existingLocal = envLocalExists ? await fs.readFile(envLocalPath, "utf-8") : "";
|
|
2034
|
+
const existingLocalKeys = envLocalExists ? extractEnvKeys(existingLocal) : /* @__PURE__ */ new Set();
|
|
2035
|
+
const anonKey = await getAnonKey();
|
|
2036
|
+
const jwtSecret = await getJwtSecret();
|
|
2037
|
+
const filled = manifest.envExampleAppend.replace(
|
|
2038
|
+
/^([A-Z][A-Z0-9_]*=)(.*)$/gm,
|
|
2039
|
+
(_, prefix, value) => {
|
|
2040
|
+
const key = prefix.slice(0, -1);
|
|
2041
|
+
if (/INSFORGE.*(URL|BASE_URL)$/.test(key)) return `${prefix}${projectConfig.oss_host}`;
|
|
2042
|
+
if (/INSFORGE.*ANON_KEY$/.test(key)) return `${prefix}${anonKey}`;
|
|
2043
|
+
if (key === "NEXT_PUBLIC_APP_URL") return `${prefix}https://${projectConfig.appkey}.insforge.site`;
|
|
2044
|
+
if (/JWT_SECRET$/.test(key)) return `${prefix}${jwtSecret ?? value}`;
|
|
2045
|
+
if (key === "BETTER_AUTH_SECRET") return `${prefix}${randomBytes2(32).toString("hex")}`;
|
|
2046
|
+
return `${prefix}${value}`;
|
|
2047
|
+
}
|
|
2048
|
+
);
|
|
2049
|
+
if (!envLocalExists) {
|
|
2050
|
+
await fs.writeFile(envLocalPath, filled + "\n");
|
|
2051
|
+
result.envLocalWritten = true;
|
|
2052
|
+
} else {
|
|
2053
|
+
const { filtered, dropped } = filterCollidingEnvLines(filled, existingLocalKeys);
|
|
2054
|
+
const hasNewKey = filtered.split("\n").some((l) => /^[A-Z][A-Z0-9_]*=/.test(l));
|
|
2055
|
+
if (hasNewKey) {
|
|
2056
|
+
await fs.writeFile(envLocalPath, existingLocal.replace(/\n*$/, "\n\n") + filtered + "\n");
|
|
2057
|
+
result.envLocalWritten = true;
|
|
2058
|
+
}
|
|
2059
|
+
result.envKeysSkipped = Array.from(/* @__PURE__ */ new Set([...result.envKeysSkipped, ...dropped]));
|
|
2060
|
+
}
|
|
2061
|
+
if (!jwtSecret && !json) {
|
|
2062
|
+
clack9.log.warn("Could not auto-fill JWT_SECRET \u2014 run `npx @insforge/cli secrets get JWT_SECRET` and paste it into .env.local.");
|
|
2063
|
+
}
|
|
2064
|
+
if (result.envKeysSkipped.length > 0 && !json) {
|
|
2065
|
+
clack9.log.warn(
|
|
2066
|
+
`Kept your existing values for: ${result.envKeysSkipped.join(", ")}. If any of these need the auth-provider's defaults, see .env.example for reference.`
|
|
2067
|
+
);
|
|
2068
|
+
}
|
|
2069
|
+
return result;
|
|
2070
|
+
} finally {
|
|
2071
|
+
await cleanup();
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
// src/commands/create.ts
|
|
2076
|
+
import { exec as exec2, execFile as execFile2 } from "child_process";
|
|
2077
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
2078
|
+
import { promisify as promisify3 } from "util";
|
|
2079
|
+
import * as fs4 from "fs/promises";
|
|
2080
|
+
import * as path4 from "path";
|
|
2081
|
+
import * as clack11 from "@clack/prompts";
|
|
2082
|
+
|
|
1351
2083
|
// src/lib/env.ts
|
|
1352
|
-
import * as
|
|
1353
|
-
import * as
|
|
2084
|
+
import * as fs2 from "fs/promises";
|
|
2085
|
+
import * as path2 from "path";
|
|
1354
2086
|
async function readEnvFile(cwd) {
|
|
1355
2087
|
const candidates = [".env.local", ".env.production", ".env"];
|
|
1356
2088
|
for (const name of candidates) {
|
|
1357
|
-
const filePath =
|
|
1358
|
-
const exists = await
|
|
2089
|
+
const filePath = path2.join(cwd, name);
|
|
2090
|
+
const exists = await fs2.stat(filePath).catch(() => null);
|
|
1359
2091
|
if (!exists) continue;
|
|
1360
|
-
const content = await
|
|
2092
|
+
const content = await fs2.readFile(filePath, "utf-8");
|
|
1361
2093
|
const vars = [];
|
|
1362
2094
|
for (const line of content.split("\n")) {
|
|
1363
2095
|
const trimmed = line.trim();
|
|
@@ -1377,14 +2109,14 @@ async function readEnvFile(cwd) {
|
|
|
1377
2109
|
}
|
|
1378
2110
|
|
|
1379
2111
|
// src/commands/deployments/deploy.ts
|
|
1380
|
-
import * as
|
|
1381
|
-
import * as
|
|
2112
|
+
import * as path3 from "path";
|
|
2113
|
+
import * as fs3 from "fs/promises";
|
|
1382
2114
|
import { createReadStream } from "fs";
|
|
1383
2115
|
import { createHash as createHash2 } from "crypto";
|
|
1384
|
-
import * as
|
|
2116
|
+
import * as clack10 from "@clack/prompts";
|
|
1385
2117
|
import archiver from "archiver";
|
|
1386
|
-
var
|
|
1387
|
-
var
|
|
2118
|
+
var POLL_INTERVAL_MS3 = 5e3;
|
|
2119
|
+
var POLL_TIMEOUT_MS3 = 3e5;
|
|
1388
2120
|
var DIRECT_UPLOAD_CONCURRENCY = 8;
|
|
1389
2121
|
var EXCLUDE_PATTERNS = [
|
|
1390
2122
|
"node_modules",
|
|
@@ -1437,7 +2169,7 @@ function isInsforgeCloudOssHost(ossHost) {
|
|
|
1437
2169
|
}
|
|
1438
2170
|
}
|
|
1439
2171
|
function normalizeRelativePath(sourceDir, absolutePath) {
|
|
1440
|
-
return
|
|
2172
|
+
return path3.relative(sourceDir, absolutePath).split(path3.sep).join("/").replace(/\\/g, "/");
|
|
1441
2173
|
}
|
|
1442
2174
|
async function hashFile(filePath) {
|
|
1443
2175
|
const hash = createHash2("sha1");
|
|
@@ -1452,10 +2184,10 @@ async function hashFile(filePath) {
|
|
|
1452
2184
|
async function collectDeploymentFiles(sourceDir) {
|
|
1453
2185
|
const files = [];
|
|
1454
2186
|
async function walk(currentDir) {
|
|
1455
|
-
const entries = await
|
|
2187
|
+
const entries = await fs3.readdir(currentDir, { withFileTypes: true });
|
|
1456
2188
|
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
1457
2189
|
for (const entry of entries) {
|
|
1458
|
-
const absolutePath =
|
|
2190
|
+
const absolutePath = path3.join(currentDir, entry.name);
|
|
1459
2191
|
const normalizedPath = normalizeRelativePath(sourceDir, absolutePath);
|
|
1460
2192
|
if (!normalizedPath || shouldExclude(normalizedPath)) {
|
|
1461
2193
|
continue;
|
|
@@ -1561,12 +2293,12 @@ async function startDirectDeployment(deploymentId, startBody) {
|
|
|
1561
2293
|
});
|
|
1562
2294
|
await response.json();
|
|
1563
2295
|
}
|
|
1564
|
-
async function pollDeployment(deploymentId,
|
|
1565
|
-
|
|
2296
|
+
async function pollDeployment(deploymentId, spinner8, syncBeforeRead) {
|
|
2297
|
+
spinner8?.message("Building and deploying...");
|
|
1566
2298
|
const startTime = Date.now();
|
|
1567
2299
|
let deployment = null;
|
|
1568
|
-
while (Date.now() - startTime <
|
|
1569
|
-
await new Promise((resolve5) => setTimeout(resolve5,
|
|
2300
|
+
while (Date.now() - startTime < POLL_TIMEOUT_MS3) {
|
|
2301
|
+
await new Promise((resolve5) => setTimeout(resolve5, POLL_INTERVAL_MS3));
|
|
1570
2302
|
try {
|
|
1571
2303
|
if (syncBeforeRead) {
|
|
1572
2304
|
await ossFetch(`/api/deployments/${deploymentId}/sync`, { method: "POST" });
|
|
@@ -1578,13 +2310,13 @@ async function pollDeployment(deploymentId, spinner7, syncBeforeRead) {
|
|
|
1578
2310
|
break;
|
|
1579
2311
|
}
|
|
1580
2312
|
if (status === "ERROR" || status === "CANCELED") {
|
|
1581
|
-
|
|
2313
|
+
spinner8?.stop("Deployment failed");
|
|
1582
2314
|
throw new CLIError(
|
|
1583
2315
|
getDeploymentError(deployment.metadata) ?? `Deployment failed with status: ${deployment.status}`
|
|
1584
2316
|
);
|
|
1585
2317
|
}
|
|
1586
2318
|
const elapsed = Math.round((Date.now() - startTime) / 1e3);
|
|
1587
|
-
|
|
2319
|
+
spinner8?.message(`Building and deploying... (${elapsed}s, status: ${deployment.status})`);
|
|
1588
2320
|
} catch (err) {
|
|
1589
2321
|
if (err instanceof CLIError) throw err;
|
|
1590
2322
|
}
|
|
@@ -1594,20 +2326,20 @@ async function pollDeployment(deploymentId, spinner7, syncBeforeRead) {
|
|
|
1594
2326
|
return { deploymentId, deployment, isReady, liveUrl };
|
|
1595
2327
|
}
|
|
1596
2328
|
async function deployProjectDirect(opts, config) {
|
|
1597
|
-
const { sourceDir, startBody = {}, spinner:
|
|
1598
|
-
|
|
2329
|
+
const { sourceDir, startBody = {}, spinner: spinner8 } = opts;
|
|
2330
|
+
spinner8?.start("Scanning source files...");
|
|
1599
2331
|
const localFiles = await collectDeploymentFiles(sourceDir);
|
|
1600
2332
|
if (localFiles.length === 0) {
|
|
1601
2333
|
throw new CLIError("No deployable files found in the source directory.");
|
|
1602
2334
|
}
|
|
1603
|
-
|
|
2335
|
+
spinner8?.message("Creating deployment...");
|
|
1604
2336
|
const createResult = await createDirectDeploymentSession(
|
|
1605
2337
|
config,
|
|
1606
2338
|
localFiles.map(({ path: relativePath, sha, size }) => ({ path: relativePath, sha, size }))
|
|
1607
2339
|
);
|
|
1608
2340
|
const localFileByPath = new Map(localFiles.map((file) => [file.path, file]));
|
|
1609
2341
|
const pendingFiles = createResult.files.filter((file) => !file.uploadedAt);
|
|
1610
|
-
|
|
2342
|
+
spinner8?.message(`Uploading ${pendingFiles.length} file${pendingFiles.length === 1 ? "" : "s"}...`);
|
|
1611
2343
|
await runWithConcurrency(pendingFiles, DIRECT_UPLOAD_CONCURRENCY, async (manifestFile) => {
|
|
1612
2344
|
const localFile = localFileByPath.get(manifestFile.path);
|
|
1613
2345
|
if (!localFile) {
|
|
@@ -1618,18 +2350,18 @@ async function deployProjectDirect(opts, config) {
|
|
|
1618
2350
|
}
|
|
1619
2351
|
await uploadDirectDeploymentFile(createResult.id, manifestFile, localFile);
|
|
1620
2352
|
});
|
|
1621
|
-
|
|
2353
|
+
spinner8?.message("Starting deployment...");
|
|
1622
2354
|
await startDirectDeployment(createResult.id, startBody);
|
|
1623
|
-
return await pollDeployment(createResult.id,
|
|
2355
|
+
return await pollDeployment(createResult.id, spinner8, !isInsforgeCloudOssHost(config.oss_host));
|
|
1624
2356
|
}
|
|
1625
2357
|
async function deployProjectLegacy(opts) {
|
|
1626
|
-
const { sourceDir, startBody = {}, spinner:
|
|
1627
|
-
|
|
2358
|
+
const { sourceDir, startBody = {}, spinner: spinner8 } = opts;
|
|
2359
|
+
spinner8?.message("Creating deployment...");
|
|
1628
2360
|
const createRes = await ossFetch("/api/deployments", { method: "POST" });
|
|
1629
2361
|
const { id: deploymentId, uploadUrl, uploadFields } = await createRes.json();
|
|
1630
|
-
|
|
2362
|
+
spinner8?.message("Compressing source files...");
|
|
1631
2363
|
const zipBuffer = await createZipBuffer(sourceDir);
|
|
1632
|
-
|
|
2364
|
+
spinner8?.message("Uploading...");
|
|
1633
2365
|
const formData = new FormData();
|
|
1634
2366
|
for (const [key, value] of Object.entries(uploadFields)) {
|
|
1635
2367
|
formData.append(key, value);
|
|
@@ -1640,13 +2372,13 @@ async function deployProjectLegacy(opts) {
|
|
|
1640
2372
|
const uploadErr = await uploadRes.text();
|
|
1641
2373
|
throw new CLIError(`Failed to upload: ${uploadErr}`);
|
|
1642
2374
|
}
|
|
1643
|
-
|
|
2375
|
+
spinner8?.message("Starting deployment...");
|
|
1644
2376
|
const startRes = await ossFetch(`/api/deployments/${deploymentId}/start`, {
|
|
1645
2377
|
method: "POST",
|
|
1646
2378
|
body: JSON.stringify(startBody)
|
|
1647
2379
|
});
|
|
1648
2380
|
await startRes.json();
|
|
1649
|
-
return await pollDeployment(deploymentId,
|
|
2381
|
+
return await pollDeployment(deploymentId, spinner8, false);
|
|
1650
2382
|
}
|
|
1651
2383
|
async function deployProject(opts) {
|
|
1652
2384
|
const config = getProjectConfig();
|
|
@@ -1670,18 +2402,18 @@ function registerDeploymentsDeployCommand(deploymentsCmd2) {
|
|
|
1670
2402
|
await requireAuth();
|
|
1671
2403
|
const config = getProjectConfig();
|
|
1672
2404
|
if (!config) throw new ProjectNotLinkedError();
|
|
1673
|
-
const sourceDir =
|
|
1674
|
-
const stats = await
|
|
2405
|
+
const sourceDir = path3.resolve(directory ?? ".");
|
|
2406
|
+
const stats = await fs3.stat(sourceDir).catch(() => null);
|
|
1675
2407
|
if (!stats?.isDirectory()) {
|
|
1676
2408
|
throw new CLIError(`"${sourceDir}" is not a valid directory.`);
|
|
1677
2409
|
}
|
|
1678
|
-
const dirName =
|
|
2410
|
+
const dirName = path3.basename(sourceDir);
|
|
1679
2411
|
if (EXCLUDE_PATTERNS.includes(dirName)) {
|
|
1680
2412
|
throw new CLIError(
|
|
1681
2413
|
`"${dirName}" is an excluded directory and cannot be used as a deploy source. Please specify your project root or output directory instead.`
|
|
1682
2414
|
);
|
|
1683
2415
|
}
|
|
1684
|
-
const
|
|
2416
|
+
const spinner8 = !json ? clack10.spinner() : null;
|
|
1685
2417
|
const startBody = {};
|
|
1686
2418
|
if (opts.env) {
|
|
1687
2419
|
try {
|
|
@@ -1707,19 +2439,19 @@ function registerDeploymentsDeployCommand(deploymentsCmd2) {
|
|
|
1707
2439
|
throw new CLIError("Invalid --meta JSON.");
|
|
1708
2440
|
}
|
|
1709
2441
|
}
|
|
1710
|
-
const result = await deployProject({ sourceDir, startBody, spinner:
|
|
2442
|
+
const result = await deployProject({ sourceDir, startBody, spinner: spinner8 });
|
|
1711
2443
|
if (result.isReady) {
|
|
1712
|
-
|
|
2444
|
+
spinner8?.stop("Deployment complete");
|
|
1713
2445
|
if (json) {
|
|
1714
2446
|
outputJson(result.deployment);
|
|
1715
2447
|
} else {
|
|
1716
2448
|
if (result.liveUrl) {
|
|
1717
|
-
|
|
2449
|
+
clack10.log.success(`Live at: ${result.liveUrl}`);
|
|
1718
2450
|
}
|
|
1719
|
-
|
|
2451
|
+
clack10.log.info(`Deployment ID: ${result.deploymentId}`);
|
|
1720
2452
|
}
|
|
1721
2453
|
} else {
|
|
1722
|
-
|
|
2454
|
+
spinner8?.stop("Deployment is still building");
|
|
1723
2455
|
if (json) {
|
|
1724
2456
|
outputJson({
|
|
1725
2457
|
id: result.deploymentId,
|
|
@@ -1727,9 +2459,9 @@ function registerDeploymentsDeployCommand(deploymentsCmd2) {
|
|
|
1727
2459
|
timedOut: true
|
|
1728
2460
|
});
|
|
1729
2461
|
} else {
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
2462
|
+
clack10.log.info(`Deployment ID: ${result.deploymentId}`);
|
|
2463
|
+
clack10.log.warn("Deployment did not finish within 5 minutes.");
|
|
2464
|
+
clack10.log.info(`Check status with: npx @insforge/cli deployments status ${result.deploymentId}`);
|
|
1733
2465
|
}
|
|
1734
2466
|
}
|
|
1735
2467
|
await reportCliUsage("cli.deployments.deploy", true);
|
|
@@ -1741,7 +2473,10 @@ function registerDeploymentsDeployCommand(deploymentsCmd2) {
|
|
|
1741
2473
|
}
|
|
1742
2474
|
|
|
1743
2475
|
// src/commands/create.ts
|
|
1744
|
-
var execAsync2 =
|
|
2476
|
+
var execAsync2 = promisify3(exec2);
|
|
2477
|
+
var execFileAsync2 = promisify3(execFile2);
|
|
2478
|
+
var SAFE_REPO_PATTERN2 = /^(https?:\/\/|git@)[A-Za-z0-9._:/@~+-]+(\.git)?$/;
|
|
2479
|
+
var SAFE_BRANCH_PATTERN2 = /^[A-Za-z0-9._/-]+$/;
|
|
1745
2480
|
function buildOssHost(appkey, region) {
|
|
1746
2481
|
return `https://${appkey}.${region}.insforge.app`;
|
|
1747
2482
|
}
|
|
@@ -1835,31 +2570,31 @@ async function animateBanner() {
|
|
|
1835
2570
|
process.stderr.write("\n");
|
|
1836
2571
|
}
|
|
1837
2572
|
function getDefaultProjectName() {
|
|
1838
|
-
const dirName =
|
|
2573
|
+
const dirName = path4.basename(process.cwd());
|
|
1839
2574
|
const sanitized = dirName.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
1840
2575
|
return sanitized.length >= 2 ? sanitized : "";
|
|
1841
2576
|
}
|
|
1842
2577
|
async function copyDir(src, dest) {
|
|
1843
|
-
const entries = await
|
|
2578
|
+
const entries = await fs4.readdir(src, { withFileTypes: true });
|
|
1844
2579
|
for (const entry of entries) {
|
|
1845
|
-
const srcPath =
|
|
1846
|
-
const destPath =
|
|
2580
|
+
const srcPath = path4.join(src, entry.name);
|
|
2581
|
+
const destPath = path4.join(dest, entry.name);
|
|
1847
2582
|
if (entry.isDirectory()) {
|
|
1848
|
-
await
|
|
2583
|
+
await fs4.mkdir(destPath, { recursive: true });
|
|
1849
2584
|
await copyDir(srcPath, destPath);
|
|
1850
2585
|
} else {
|
|
1851
|
-
await
|
|
2586
|
+
await fs4.copyFile(srcPath, destPath);
|
|
1852
2587
|
}
|
|
1853
2588
|
}
|
|
1854
2589
|
}
|
|
1855
2590
|
function registerCreateCommand(program2) {
|
|
1856
|
-
program2.command("create").description("Create a new InsForge project").option("--name <name>", "Project name").option("--org-id <id>", "Organization ID").option("--region <region>", "Deployment region (us-east, us-west, eu-central, ap-southeast)").option("--template <template>", "Template to use: react, nextjs, chatbot, crm, e-commerce, todo, or empty").action(async (opts, cmd) => {
|
|
2591
|
+
program2.command("create").description("Create a new InsForge project").option("--name <name>", "Project name").option("--org-id <id>", "Organization ID").option("--region <region>", "Deployment region (us-east, us-west, eu-central, ap-southeast)").option("--template <template>", "Template to use: react, nextjs, chatbot, crm, e-commerce, todo, or empty").option("--auth <provider>", "Wire a third-party auth provider into the chosen template (currently: better-auth)").action(async (opts, cmd) => {
|
|
1857
2592
|
const { json, apiUrl } = getRootOpts(cmd);
|
|
1858
2593
|
try {
|
|
1859
2594
|
await requireAuth(apiUrl, false);
|
|
1860
2595
|
if (!json) {
|
|
1861
2596
|
await animateBanner();
|
|
1862
|
-
|
|
2597
|
+
clack11.intro("Let's build something great");
|
|
1863
2598
|
}
|
|
1864
2599
|
let orgId = opts.orgId;
|
|
1865
2600
|
if (!orgId) {
|
|
@@ -1869,7 +2604,7 @@ function registerCreateCommand(program2) {
|
|
|
1869
2604
|
}
|
|
1870
2605
|
if (orgs.length === 1) {
|
|
1871
2606
|
orgId = orgs[0].id;
|
|
1872
|
-
if (!json)
|
|
2607
|
+
if (!json) clack11.log.info(`Using organization: ${orgs[0].name}`);
|
|
1873
2608
|
} else {
|
|
1874
2609
|
if (json) {
|
|
1875
2610
|
throw new CLIError("Multiple organizations found. Specify --org-id.");
|
|
@@ -1900,12 +2635,15 @@ function registerCreateCommand(program2) {
|
|
|
1900
2635
|
if (isCancel2(name)) process.exit(0);
|
|
1901
2636
|
projectName = name;
|
|
1902
2637
|
}
|
|
1903
|
-
projectName =
|
|
2638
|
+
projectName = path4.basename(projectName).replace(/[^a-zA-Z0-9._-]/g, "-").replace(/\.+/g, ".");
|
|
1904
2639
|
if (projectName.length < 2 || projectName === "." || projectName === "..") {
|
|
1905
2640
|
throw new CLIError("Project name must be at least 2 safe characters (letters, numbers, hyphens).");
|
|
1906
2641
|
}
|
|
1907
2642
|
const validTemplates = ["react", "nextjs", "chatbot", "crm", "e-commerce", "todo", "empty"];
|
|
1908
2643
|
let template = opts.template;
|
|
2644
|
+
if (opts.auth && !VALID_AUTH_PROVIDERS.includes(opts.auth)) {
|
|
2645
|
+
throw new CLIError(`Invalid --auth "${opts.auth}". Valid: ${VALID_AUTH_PROVIDERS.join(", ")}`);
|
|
2646
|
+
}
|
|
1909
2647
|
if (template && !validTemplates.includes(template)) {
|
|
1910
2648
|
throw new CLIError(`Invalid template "${template}". Valid options: ${validTemplates.join(", ")}`);
|
|
1911
2649
|
}
|
|
@@ -1959,27 +2697,27 @@ function registerCreateCommand(program2) {
|
|
|
1959
2697
|
initialValue: projectName,
|
|
1960
2698
|
validate: (v) => {
|
|
1961
2699
|
if (v.length < 1) return "Directory name is required";
|
|
1962
|
-
const normalized =
|
|
2700
|
+
const normalized = path4.basename(v).replace(/[^a-zA-Z0-9._-]/g, "-");
|
|
1963
2701
|
if (!normalized || normalized === "." || normalized === "..") return "Invalid directory name";
|
|
1964
2702
|
return void 0;
|
|
1965
2703
|
}
|
|
1966
2704
|
});
|
|
1967
2705
|
if (isCancel2(inputDir)) process.exit(0);
|
|
1968
|
-
dirName =
|
|
2706
|
+
dirName = path4.basename(inputDir).replace(/[^a-zA-Z0-9._-]/g, "-");
|
|
1969
2707
|
}
|
|
1970
2708
|
if (!dirName || dirName === "." || dirName === "..") {
|
|
1971
2709
|
throw new CLIError("Invalid directory name.");
|
|
1972
2710
|
}
|
|
1973
|
-
projectDir =
|
|
1974
|
-
const dirExists = await
|
|
2711
|
+
projectDir = path4.resolve(originalCwd, dirName);
|
|
2712
|
+
const dirExists = await fs4.stat(projectDir).catch(() => null);
|
|
1975
2713
|
if (dirExists) {
|
|
1976
2714
|
throw new CLIError(`Directory "${dirName}" already exists.`);
|
|
1977
2715
|
}
|
|
1978
|
-
await
|
|
2716
|
+
await fs4.mkdir(projectDir);
|
|
1979
2717
|
process.chdir(projectDir);
|
|
1980
2718
|
}
|
|
1981
2719
|
let projectLinked = false;
|
|
1982
|
-
const s = !json ?
|
|
2720
|
+
const s = !json ? clack11.spinner() : null;
|
|
1983
2721
|
try {
|
|
1984
2722
|
s?.start("Creating project...");
|
|
1985
2723
|
const project = await createProject(orgId, projectName, opts.region, apiUrl);
|
|
@@ -2007,37 +2745,49 @@ function registerCreateCommand(program2) {
|
|
|
2007
2745
|
try {
|
|
2008
2746
|
const anonKey = await getAnonKey();
|
|
2009
2747
|
if (!anonKey) {
|
|
2010
|
-
if (!json)
|
|
2748
|
+
if (!json) clack11.log.warn("Could not retrieve anon key. You can add it to .env.local manually.");
|
|
2011
2749
|
} else {
|
|
2012
|
-
const envPath =
|
|
2750
|
+
const envPath = path4.join(process.cwd(), ".env.local");
|
|
2013
2751
|
const envContent = [
|
|
2014
2752
|
"# InsForge",
|
|
2015
2753
|
`NEXT_PUBLIC_INSFORGE_URL=${projectConfig.oss_host}`,
|
|
2016
2754
|
`NEXT_PUBLIC_INSFORGE_ANON_KEY=${anonKey}`,
|
|
2017
2755
|
""
|
|
2018
2756
|
].join("\n");
|
|
2019
|
-
await
|
|
2757
|
+
await fs4.writeFile(envPath, envContent, { flag: "wx" });
|
|
2020
2758
|
if (!json) {
|
|
2021
|
-
|
|
2759
|
+
clack11.log.success("Created .env.local with your InsForge credentials");
|
|
2022
2760
|
}
|
|
2023
2761
|
}
|
|
2024
2762
|
} catch (err) {
|
|
2025
2763
|
const error = err;
|
|
2026
2764
|
if (!json) {
|
|
2027
2765
|
if (error.code === "EEXIST") {
|
|
2028
|
-
|
|
2766
|
+
clack11.log.warn(".env.local already exists; skipping InsForge key seeding.");
|
|
2029
2767
|
} else {
|
|
2030
|
-
|
|
2768
|
+
clack11.log.warn(`Failed to create .env.local: ${error.message}`);
|
|
2031
2769
|
}
|
|
2032
2770
|
}
|
|
2033
2771
|
}
|
|
2034
2772
|
}
|
|
2773
|
+
if (opts.auth) {
|
|
2774
|
+
try {
|
|
2775
|
+
const result = await applyAuthProvider(opts.auth, process.cwd(), projectConfig, json);
|
|
2776
|
+
if (!json) {
|
|
2777
|
+
clack11.log.success(`Wired in ${opts.auth}: ${result.written.length} new, ${result.overwritten.length} replaced`);
|
|
2778
|
+
}
|
|
2779
|
+
} catch (err) {
|
|
2780
|
+
const msg = `Failed to apply --auth ${opts.auth}: ${err.message}`;
|
|
2781
|
+
if (json) console.error(JSON.stringify({ warning: msg }));
|
|
2782
|
+
else clack11.log.warn(msg);
|
|
2783
|
+
}
|
|
2784
|
+
}
|
|
2035
2785
|
await installSkills(json);
|
|
2036
2786
|
trackCommand("create", orgId);
|
|
2037
2787
|
await reportCliUsage("cli.create", true, 6);
|
|
2038
|
-
const templateDownloaded = hasTemplate ? await
|
|
2788
|
+
const templateDownloaded = hasTemplate ? await fs4.stat(path4.join(process.cwd(), "package.json")).catch(() => null) : null;
|
|
2039
2789
|
if (templateDownloaded) {
|
|
2040
|
-
const installSpinner = !json ?
|
|
2790
|
+
const installSpinner = !json ? clack11.spinner() : null;
|
|
2041
2791
|
installSpinner?.start("Installing dependencies...");
|
|
2042
2792
|
try {
|
|
2043
2793
|
await execAsync2("npm install", { cwd: process.cwd(), maxBuffer: 10 * 1024 * 1024 });
|
|
@@ -2045,8 +2795,8 @@ function registerCreateCommand(program2) {
|
|
|
2045
2795
|
} catch (err) {
|
|
2046
2796
|
installSpinner?.stop("Failed to install dependencies");
|
|
2047
2797
|
if (!json) {
|
|
2048
|
-
|
|
2049
|
-
|
|
2798
|
+
clack11.log.warn(`npm install failed: ${err.message}`);
|
|
2799
|
+
clack11.log.info("Run `npm install` manually to install dependencies.");
|
|
2050
2800
|
}
|
|
2051
2801
|
}
|
|
2052
2802
|
}
|
|
@@ -2062,7 +2812,7 @@ function registerCreateCommand(program2) {
|
|
|
2062
2812
|
if (envVars.length > 0) {
|
|
2063
2813
|
startBody.envVars = envVars;
|
|
2064
2814
|
}
|
|
2065
|
-
const deploySpinner =
|
|
2815
|
+
const deploySpinner = clack11.spinner();
|
|
2066
2816
|
const result = await deployProject({
|
|
2067
2817
|
sourceDir: process.cwd(),
|
|
2068
2818
|
startBody,
|
|
@@ -2073,12 +2823,12 @@ function registerCreateCommand(program2) {
|
|
|
2073
2823
|
liveUrl = result.liveUrl;
|
|
2074
2824
|
} else {
|
|
2075
2825
|
deploySpinner.stop("Deployment is still building");
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2826
|
+
clack11.log.info(`Deployment ID: ${result.deploymentId}`);
|
|
2827
|
+
clack11.log.warn("Deployment did not finish within 2 minutes.");
|
|
2828
|
+
clack11.log.info(`Check status with: npx @insforge/cli deployments status ${result.deploymentId}`);
|
|
2079
2829
|
}
|
|
2080
2830
|
} catch (err) {
|
|
2081
|
-
|
|
2831
|
+
clack11.log.warn(`Deploy failed: ${err.message}`);
|
|
2082
2832
|
}
|
|
2083
2833
|
}
|
|
2084
2834
|
}
|
|
@@ -2095,38 +2845,38 @@ function registerCreateCommand(program2) {
|
|
|
2095
2845
|
}
|
|
2096
2846
|
});
|
|
2097
2847
|
} else {
|
|
2098
|
-
|
|
2848
|
+
clack11.log.step(`Dashboard: ${dashboardUrl}`);
|
|
2099
2849
|
if (liveUrl) {
|
|
2100
|
-
|
|
2850
|
+
clack11.log.success(`Live site: ${liveUrl}`);
|
|
2101
2851
|
}
|
|
2102
2852
|
if (templateDownloaded) {
|
|
2103
2853
|
const steps = [
|
|
2104
2854
|
`cd ${dirName}`,
|
|
2105
2855
|
"npm run dev"
|
|
2106
2856
|
];
|
|
2107
|
-
|
|
2108
|
-
|
|
2857
|
+
clack11.note(steps.join("\n"), "Next steps");
|
|
2858
|
+
clack11.note("Open your coding agent (Claude Code, Codex, Cursor, etc.) to add new features.", "Keep building");
|
|
2109
2859
|
} else if (hasTemplate && !templateDownloaded) {
|
|
2110
|
-
|
|
2860
|
+
clack11.log.warn("Template download failed. You can retry or set up manually.");
|
|
2111
2861
|
} else {
|
|
2112
2862
|
const prompts = [
|
|
2113
2863
|
"Build a todo app with Google OAuth sign-in",
|
|
2114
2864
|
"Build an Instagram clone where users can upload photos, like, and comment",
|
|
2115
2865
|
"Build an AI chatbot with conversation history"
|
|
2116
2866
|
];
|
|
2117
|
-
|
|
2867
|
+
clack11.note(
|
|
2118
2868
|
`Open your coding agent (Claude Code, Codex, Cursor, etc.) and try:
|
|
2119
2869
|
|
|
2120
2870
|
${prompts.map((p) => `\u2022 "${p}"`).join("\n")}`,
|
|
2121
2871
|
"Start building"
|
|
2122
2872
|
);
|
|
2123
2873
|
}
|
|
2124
|
-
|
|
2874
|
+
clack11.outro("Done!");
|
|
2125
2875
|
}
|
|
2126
2876
|
} catch (err) {
|
|
2127
2877
|
if (!projectLinked && hasTemplate && projectDir !== originalCwd) {
|
|
2128
2878
|
process.chdir(originalCwd);
|
|
2129
|
-
await
|
|
2879
|
+
await fs4.rm(projectDir, { recursive: true, force: true }).catch(() => {
|
|
2130
2880
|
});
|
|
2131
2881
|
}
|
|
2132
2882
|
throw err;
|
|
@@ -2140,18 +2890,18 @@ ${prompts.map((p) => `\u2022 "${p}"`).join("\n")}`,
|
|
|
2140
2890
|
});
|
|
2141
2891
|
}
|
|
2142
2892
|
async function downloadTemplate(framework, projectConfig, projectName, json, _apiUrl) {
|
|
2143
|
-
const s = !json ?
|
|
2893
|
+
const s = !json ? clack11.spinner() : null;
|
|
2144
2894
|
s?.start("Downloading template...");
|
|
2145
2895
|
try {
|
|
2146
2896
|
const anonKey = await getAnonKey();
|
|
2147
2897
|
if (!anonKey) {
|
|
2148
2898
|
throw new Error("Failed to retrieve anon key from backend");
|
|
2149
2899
|
}
|
|
2150
|
-
const tempDir =
|
|
2900
|
+
const tempDir = tmpdir2();
|
|
2151
2901
|
const targetDir = projectName;
|
|
2152
|
-
const templatePath =
|
|
2902
|
+
const templatePath = path4.join(tempDir, targetDir);
|
|
2153
2903
|
try {
|
|
2154
|
-
await
|
|
2904
|
+
await fs4.rm(templatePath, { recursive: true, force: true });
|
|
2155
2905
|
} catch {
|
|
2156
2906
|
}
|
|
2157
2907
|
const frame = framework === "nextjs" ? "nextjs" : "react";
|
|
@@ -2165,41 +2915,53 @@ async function downloadTemplate(framework, projectConfig, projectName, json, _ap
|
|
|
2165
2915
|
s?.message("Copying template files...");
|
|
2166
2916
|
const cwd = process.cwd();
|
|
2167
2917
|
await copyDir(templatePath, cwd);
|
|
2168
|
-
await
|
|
2918
|
+
await fs4.rm(templatePath, { recursive: true, force: true }).catch(() => {
|
|
2169
2919
|
});
|
|
2170
2920
|
s?.stop("Template files downloaded");
|
|
2171
2921
|
} catch (err) {
|
|
2172
2922
|
s?.stop("Template download failed");
|
|
2173
2923
|
if (!json) {
|
|
2174
|
-
|
|
2175
|
-
|
|
2924
|
+
clack11.log.warn(`Failed to download template: ${err.message}`);
|
|
2925
|
+
clack11.log.info("You can manually set up the template later.");
|
|
2176
2926
|
}
|
|
2177
2927
|
}
|
|
2178
2928
|
}
|
|
2179
2929
|
async function downloadGitHubTemplate(templateName, projectConfig, json) {
|
|
2180
|
-
const s = !json ?
|
|
2930
|
+
const s = !json ? clack11.spinner() : null;
|
|
2181
2931
|
s?.start(`Downloading ${templateName} template...`);
|
|
2182
|
-
const tempDir =
|
|
2932
|
+
const tempDir = path4.join(tmpdir2(), `insforge-template-${Date.now()}`);
|
|
2183
2933
|
try {
|
|
2184
|
-
await
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
const
|
|
2190
|
-
|
|
2934
|
+
await fs4.mkdir(tempDir, { recursive: true });
|
|
2935
|
+
const templatesRepo = process.env.INSFORGE_TEMPLATES_REPO ?? "https://github.com/InsForge/insforge-templates.git";
|
|
2936
|
+
if (!SAFE_REPO_PATTERN2.test(templatesRepo)) {
|
|
2937
|
+
throw new Error(`INSFORGE_TEMPLATES_REPO has unsupported characters: ${templatesRepo}`);
|
|
2938
|
+
}
|
|
2939
|
+
const templatesBranch = process.env.INSFORGE_TEMPLATES_BRANCH;
|
|
2940
|
+
if (templatesBranch !== void 0 && !SAFE_BRANCH_PATTERN2.test(templatesBranch)) {
|
|
2941
|
+
throw new Error(`INSFORGE_TEMPLATES_BRANCH has unsupported characters: ${templatesBranch}`);
|
|
2942
|
+
}
|
|
2943
|
+
const cloneArgs = ["clone", "--depth", "1"];
|
|
2944
|
+
if (templatesBranch) cloneArgs.push("-b", templatesBranch);
|
|
2945
|
+
cloneArgs.push("--", templatesRepo, ".");
|
|
2946
|
+
await execFileAsync2("git", cloneArgs, {
|
|
2947
|
+
cwd: tempDir,
|
|
2948
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
2949
|
+
timeout: 6e4
|
|
2950
|
+
});
|
|
2951
|
+
const templateDir = path4.join(tempDir, templateName);
|
|
2952
|
+
const stat5 = await fs4.stat(templateDir).catch(() => null);
|
|
2191
2953
|
if (!stat5?.isDirectory()) {
|
|
2192
2954
|
throw new Error(`Template "${templateName}" not found in repository`);
|
|
2193
2955
|
}
|
|
2194
2956
|
s?.message("Copying template files...");
|
|
2195
2957
|
const cwd = process.cwd();
|
|
2196
2958
|
await copyDir(templateDir, cwd);
|
|
2197
|
-
const envExamplePath =
|
|
2198
|
-
const envExampleExists = await
|
|
2959
|
+
const envExamplePath = path4.join(cwd, ".env.example");
|
|
2960
|
+
const envExampleExists = await fs4.stat(envExamplePath).catch(() => null);
|
|
2199
2961
|
if (envExampleExists) {
|
|
2200
2962
|
const anonKey = await getAnonKey();
|
|
2201
|
-
const envExample = await
|
|
2202
|
-
const
|
|
2963
|
+
const envExample = await fs4.readFile(envExamplePath, "utf-8");
|
|
2964
|
+
const envFinal = envExample.replace(
|
|
2203
2965
|
/^([A-Z][A-Z0-9_]*=)(.*)$/gm,
|
|
2204
2966
|
(_, prefix, _value) => {
|
|
2205
2967
|
const key = prefix.slice(0, -1);
|
|
@@ -2209,32 +2971,32 @@ async function downloadGitHubTemplate(templateName, projectConfig, json) {
|
|
|
2209
2971
|
return `${prefix}${_value}`;
|
|
2210
2972
|
}
|
|
2211
2973
|
);
|
|
2212
|
-
const envLocalPath =
|
|
2974
|
+
const envLocalPath = path4.join(cwd, ".env.local");
|
|
2213
2975
|
try {
|
|
2214
|
-
await
|
|
2976
|
+
await fs4.writeFile(envLocalPath, envFinal, { flag: "wx" });
|
|
2215
2977
|
} catch (e) {
|
|
2216
2978
|
if (e.code === "EEXIST") {
|
|
2217
|
-
if (!json)
|
|
2979
|
+
if (!json) clack11.log.warn(".env.local already exists; skipping env seeding.");
|
|
2218
2980
|
} else {
|
|
2219
2981
|
throw e;
|
|
2220
2982
|
}
|
|
2221
2983
|
}
|
|
2222
2984
|
}
|
|
2223
2985
|
s?.stop(`${templateName} template downloaded`);
|
|
2224
|
-
const migrationPath =
|
|
2225
|
-
const migrationExists = await
|
|
2986
|
+
const migrationPath = path4.join(cwd, "migrations", "db_init.sql");
|
|
2987
|
+
const migrationExists = await fs4.stat(migrationPath).catch(() => null);
|
|
2226
2988
|
if (migrationExists) {
|
|
2227
|
-
const dbSpinner = !json ?
|
|
2989
|
+
const dbSpinner = !json ? clack11.spinner() : null;
|
|
2228
2990
|
dbSpinner?.start("Running database migrations...");
|
|
2229
2991
|
try {
|
|
2230
|
-
const sql = await
|
|
2992
|
+
const sql = await fs4.readFile(migrationPath, "utf-8");
|
|
2231
2993
|
await runRawSql(sql, true);
|
|
2232
2994
|
dbSpinner?.stop("Database migrations applied");
|
|
2233
2995
|
} catch (err) {
|
|
2234
2996
|
dbSpinner?.stop("Database migration failed");
|
|
2235
2997
|
if (!json) {
|
|
2236
|
-
|
|
2237
|
-
|
|
2998
|
+
clack11.log.warn(`Migration failed: ${err.message}`);
|
|
2999
|
+
clack11.log.info('You can run the migration manually: npx @insforge/cli db query --unrestricted "$(cat migrations/db_init.sql)"');
|
|
2238
3000
|
} else {
|
|
2239
3001
|
throw err;
|
|
2240
3002
|
}
|
|
@@ -2242,25 +3004,31 @@ async function downloadGitHubTemplate(templateName, projectConfig, json) {
|
|
|
2242
3004
|
}
|
|
2243
3005
|
} catch (err) {
|
|
2244
3006
|
s?.stop(`${templateName} template download failed`);
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
3007
|
+
const msg = `Failed to download ${templateName} template: ${err.message}`;
|
|
3008
|
+
if (json) {
|
|
3009
|
+
console.error(JSON.stringify({ warning: msg }));
|
|
3010
|
+
} else {
|
|
3011
|
+
clack11.log.warn(msg);
|
|
3012
|
+
clack11.log.info("You can manually clone from: https://github.com/InsForge/insforge-templates");
|
|
2248
3013
|
}
|
|
2249
3014
|
} finally {
|
|
2250
|
-
await
|
|
3015
|
+
await fs4.rm(tempDir, { recursive: true, force: true }).catch(() => {
|
|
2251
3016
|
});
|
|
2252
3017
|
}
|
|
2253
3018
|
}
|
|
2254
3019
|
|
|
2255
3020
|
// src/commands/projects/link.ts
|
|
2256
|
-
var execAsync3 =
|
|
3021
|
+
var execAsync3 = promisify4(exec3);
|
|
2257
3022
|
function buildOssHost2(appkey, region) {
|
|
2258
3023
|
return `https://${appkey}.${region}.insforge.app`;
|
|
2259
3024
|
}
|
|
2260
3025
|
function registerProjectLinkCommand(program2) {
|
|
2261
|
-
program2.command("link").description("Link current directory to an InsForge project").option("--project-id <id>", "Project ID to link").option("--org-id <id>", "Organization ID").option("--template <template>", "Download a template after linking: react, nextjs, chatbot, crm, e-commerce, todo").option("--api-base-url <url>", "API Base URL for direct linking (OSS/Self-hosted)").option("--api-key <key>", "API Key for direct linking (OSS/Self-hosted)").action(async (opts, cmd) => {
|
|
3026
|
+
program2.command("link").description("Link current directory to an InsForge project").option("--project-id <id>", "Project ID to link").option("--org-id <id>", "Organization ID").option("--template <template>", "Download a template after linking: react, nextjs, chatbot, crm, e-commerce, todo").option("--auth <provider>", "Wire a third-party auth provider into the chosen template (currently: better-auth)").option("--api-base-url <url>", "API Base URL for direct linking (OSS/Self-hosted)").option("--api-key <key>", "API Key for direct linking (OSS/Self-hosted)").action(async (opts, cmd) => {
|
|
2262
3027
|
const { json, apiUrl } = getRootOpts(cmd);
|
|
2263
3028
|
const validTemplates = ["react", "nextjs", "chatbot", "crm", "e-commerce", "todo"];
|
|
3029
|
+
if (opts.auth && !VALID_AUTH_PROVIDERS.includes(opts.auth)) {
|
|
3030
|
+
throw new CLIError(`Invalid --auth "${opts.auth}". Valid: ${VALID_AUTH_PROVIDERS.join(", ")}`);
|
|
3031
|
+
}
|
|
2264
3032
|
try {
|
|
2265
3033
|
if (opts.template && !validTemplates.includes(opts.template)) {
|
|
2266
3034
|
throw new CLIError(`Invalid template "${opts.template}". Valid options: ${validTemplates.join(", ")}`);
|
|
@@ -2295,23 +3063,23 @@ function registerProjectLinkCommand(program2) {
|
|
|
2295
3063
|
initialValue: defaultDir,
|
|
2296
3064
|
validate: (v) => {
|
|
2297
3065
|
if (v.length < 1) return "Directory name is required";
|
|
2298
|
-
const normalized =
|
|
3066
|
+
const normalized = path5.basename(v).replace(/[^a-zA-Z0-9._-]/g, "-");
|
|
2299
3067
|
if (!normalized || normalized === "." || normalized === "..") return "Invalid directory name";
|
|
2300
3068
|
return void 0;
|
|
2301
3069
|
}
|
|
2302
3070
|
});
|
|
2303
3071
|
if (isCancel2(inputDir)) process.exit(0);
|
|
2304
|
-
dirName =
|
|
3072
|
+
dirName = path5.basename(inputDir).replace(/[^a-zA-Z0-9._-]/g, "-");
|
|
2305
3073
|
}
|
|
2306
3074
|
if (!dirName || dirName === "." || dirName === "..") {
|
|
2307
3075
|
throw new CLIError("Invalid directory name.");
|
|
2308
3076
|
}
|
|
2309
|
-
const templateDir =
|
|
2310
|
-
const dirExists = await
|
|
3077
|
+
const templateDir = path5.resolve(process.cwd(), dirName);
|
|
3078
|
+
const dirExists = await fs5.stat(templateDir).catch(() => null);
|
|
2311
3079
|
if (dirExists) {
|
|
2312
3080
|
throw new CLIError(`Directory "${dirName}" already exists.`);
|
|
2313
3081
|
}
|
|
2314
|
-
await
|
|
3082
|
+
await fs5.mkdir(templateDir);
|
|
2315
3083
|
process.chdir(templateDir);
|
|
2316
3084
|
saveProjectConfig(projectConfig2);
|
|
2317
3085
|
if (json) {
|
|
@@ -2326,17 +3094,27 @@ function registerProjectLinkCommand(program2) {
|
|
|
2326
3094
|
}
|
|
2327
3095
|
captureEvent(FAKE_ORG_ID, "template_selected", { template: template2, source: "link_direct" });
|
|
2328
3096
|
await downloadGitHubTemplate(template2, projectConfig2, json);
|
|
2329
|
-
const templateDownloaded = await
|
|
3097
|
+
const templateDownloaded = await fs5.stat(path5.join(process.cwd(), "package.json")).catch(() => null);
|
|
3098
|
+
if (opts.auth) {
|
|
3099
|
+
try {
|
|
3100
|
+
const result = await applyAuthProvider(opts.auth, process.cwd(), projectConfig2, json);
|
|
3101
|
+
if (!json) clack12.log.success(`Wired in ${opts.auth}: ${result.written.length} new, ${result.overwritten.length} replaced`);
|
|
3102
|
+
} catch (err) {
|
|
3103
|
+
const msg = `Failed to apply --auth ${opts.auth}: ${err.message}`;
|
|
3104
|
+
if (json) console.error(JSON.stringify({ warning: msg }));
|
|
3105
|
+
else clack12.log.warn(msg);
|
|
3106
|
+
}
|
|
3107
|
+
}
|
|
2330
3108
|
if (templateDownloaded && !json) {
|
|
2331
|
-
const installSpinner =
|
|
3109
|
+
const installSpinner = clack12.spinner();
|
|
2332
3110
|
installSpinner.start("Installing dependencies...");
|
|
2333
3111
|
try {
|
|
2334
3112
|
await execAsync3("npm install", { cwd: process.cwd(), maxBuffer: 10 * 1024 * 1024 });
|
|
2335
3113
|
installSpinner.stop("Dependencies installed");
|
|
2336
3114
|
} catch (err) {
|
|
2337
3115
|
installSpinner.stop("Failed to install dependencies");
|
|
2338
|
-
|
|
2339
|
-
|
|
3116
|
+
clack12.log.warn(`npm install failed: ${err.message}`);
|
|
3117
|
+
clack12.log.info("Run `npm install` manually to install dependencies.");
|
|
2340
3118
|
}
|
|
2341
3119
|
}
|
|
2342
3120
|
await installSkills(json);
|
|
@@ -2356,9 +3134,9 @@ function registerProjectLinkCommand(program2) {
|
|
|
2356
3134
|
`${pc2.bold("1.")} ${runCommand}`,
|
|
2357
3135
|
`${pc2.bold("2.")} Open ${pc2.cyan("Claude Code")} or ${pc2.cyan("Cursor")} and prompt your agent to add more features`
|
|
2358
3136
|
];
|
|
2359
|
-
|
|
3137
|
+
clack12.note(steps.join("\n"), "What's next");
|
|
2360
3138
|
} else {
|
|
2361
|
-
|
|
3139
|
+
clack12.log.warn("Template download failed. You can retry or set up manually.");
|
|
2362
3140
|
}
|
|
2363
3141
|
}
|
|
2364
3142
|
return;
|
|
@@ -2369,6 +3147,31 @@ function registerProjectLinkCommand(program2) {
|
|
|
2369
3147
|
} else {
|
|
2370
3148
|
outputSuccess(`Linked to direct project at ${projectConfig2.oss_host}`);
|
|
2371
3149
|
}
|
|
3150
|
+
if (opts.auth) {
|
|
3151
|
+
try {
|
|
3152
|
+
const result = await applyAuthProvider(opts.auth, process.cwd(), projectConfig2, json);
|
|
3153
|
+
if (!json) {
|
|
3154
|
+
clack12.log.success(`Wired in ${opts.auth}: ${result.written.length} new, ${result.overwritten.length} replaced`);
|
|
3155
|
+
}
|
|
3156
|
+
if (result.packageJsonPatched && !json) {
|
|
3157
|
+
const installSpinner = clack12.spinner();
|
|
3158
|
+
installSpinner.start("Installing new dependencies...");
|
|
3159
|
+
try {
|
|
3160
|
+
await execAsync3("npm install", { cwd: process.cwd(), maxBuffer: 10 * 1024 * 1024 });
|
|
3161
|
+
installSpinner.stop("Dependencies installed");
|
|
3162
|
+
} catch (err) {
|
|
3163
|
+
installSpinner.stop("Failed to install dependencies");
|
|
3164
|
+
clack12.log.warn(`npm install failed: ${err.message}`);
|
|
3165
|
+
clack12.log.info("Run `npm install` manually to install dependencies.");
|
|
3166
|
+
}
|
|
3167
|
+
}
|
|
3168
|
+
if (!json) clack12.note(result.nextSteps, "What's next");
|
|
3169
|
+
} catch (err) {
|
|
3170
|
+
const msg = `Failed to apply --auth ${opts.auth}: ${err.message}`;
|
|
3171
|
+
if (json) console.error(JSON.stringify({ warning: msg }));
|
|
3172
|
+
else clack12.log.warn(msg);
|
|
3173
|
+
}
|
|
3174
|
+
}
|
|
2372
3175
|
trackCommand("link", "oss-org", { direct: true });
|
|
2373
3176
|
await installSkills(json);
|
|
2374
3177
|
await reportCliUsage("cli.link_direct", true, 6, projectConfig2);
|
|
@@ -2396,7 +3199,7 @@ function registerProjectLinkCommand(program2) {
|
|
|
2396
3199
|
}
|
|
2397
3200
|
if (orgs.length === 1) {
|
|
2398
3201
|
orgId = orgs[0].id;
|
|
2399
|
-
if (!json)
|
|
3202
|
+
if (!json) clack12.log.info(`Using organization: ${orgs[0].name}`);
|
|
2400
3203
|
} else {
|
|
2401
3204
|
if (json) {
|
|
2402
3205
|
throw new CLIError("Multiple organizations found. Specify --org-id.");
|
|
@@ -2482,68 +3285,91 @@ function registerProjectLinkCommand(program2) {
|
|
|
2482
3285
|
initialValue: project.name,
|
|
2483
3286
|
validate: (v) => {
|
|
2484
3287
|
if (v.length < 1) return "Directory name is required";
|
|
2485
|
-
const normalized =
|
|
3288
|
+
const normalized = path5.basename(v).replace(/[^a-zA-Z0-9._-]/g, "-");
|
|
2486
3289
|
if (!normalized || normalized === "." || normalized === "..") return "Invalid directory name";
|
|
2487
3290
|
return void 0;
|
|
2488
3291
|
}
|
|
2489
3292
|
});
|
|
2490
3293
|
if (isCancel2(inputDir)) process.exit(0);
|
|
2491
|
-
dirName =
|
|
3294
|
+
dirName = path5.basename(inputDir).replace(/[^a-zA-Z0-9._-]/g, "-");
|
|
2492
3295
|
}
|
|
2493
3296
|
if (!dirName || dirName === "." || dirName === "..") {
|
|
2494
3297
|
throw new CLIError("Invalid directory name.");
|
|
2495
3298
|
}
|
|
2496
|
-
const templateDir =
|
|
2497
|
-
const dirExists = await
|
|
3299
|
+
const templateDir = path5.resolve(process.cwd(), dirName);
|
|
3300
|
+
const dirExists = await fs5.stat(templateDir).catch(() => null);
|
|
2498
3301
|
if (dirExists) {
|
|
2499
3302
|
throw new CLIError(`Directory "${dirName}" already exists.`);
|
|
2500
3303
|
}
|
|
2501
|
-
await
|
|
3304
|
+
await fs5.mkdir(templateDir);
|
|
2502
3305
|
process.chdir(templateDir);
|
|
2503
3306
|
saveProjectConfig(projectConfig);
|
|
2504
3307
|
captureEvent(orgId ?? project.organization_id, "template_selected", { template, source: "link" });
|
|
2505
3308
|
await downloadGitHubTemplate(template, projectConfig, json);
|
|
2506
|
-
const templateDownloaded = await
|
|
3309
|
+
const templateDownloaded = await fs5.stat(path5.join(process.cwd(), "package.json")).catch(() => null);
|
|
3310
|
+
if (opts.auth) {
|
|
3311
|
+
try {
|
|
3312
|
+
const result = await applyAuthProvider(opts.auth, process.cwd(), projectConfig, json);
|
|
3313
|
+
if (!json) clack12.log.success(`Wired in ${opts.auth}: ${result.written.length} new, ${result.overwritten.length} replaced`);
|
|
3314
|
+
} catch (err) {
|
|
3315
|
+
const msg = `Failed to apply --auth ${opts.auth}: ${err.message}`;
|
|
3316
|
+
if (json) console.error(JSON.stringify({ warning: msg }));
|
|
3317
|
+
else clack12.log.warn(msg);
|
|
3318
|
+
}
|
|
3319
|
+
}
|
|
2507
3320
|
if (templateDownloaded && !json) {
|
|
2508
|
-
const installSpinner =
|
|
3321
|
+
const installSpinner = clack12.spinner();
|
|
2509
3322
|
installSpinner.start("Installing dependencies...");
|
|
2510
3323
|
try {
|
|
2511
3324
|
await execAsync3("npm install", { cwd: process.cwd(), maxBuffer: 10 * 1024 * 1024 });
|
|
2512
3325
|
installSpinner.stop("Dependencies installed");
|
|
2513
3326
|
} catch (err) {
|
|
2514
3327
|
installSpinner.stop("Failed to install dependencies");
|
|
2515
|
-
|
|
2516
|
-
|
|
3328
|
+
clack12.log.warn(`npm install failed: ${err.message}`);
|
|
3329
|
+
clack12.log.info("Run `npm install` manually to install dependencies.");
|
|
2517
3330
|
}
|
|
2518
3331
|
}
|
|
2519
3332
|
await installSkills(json);
|
|
2520
3333
|
await reportCliUsage("cli.link", true, 6, projectConfig);
|
|
2521
3334
|
if (!json) {
|
|
2522
3335
|
const dashboardUrl = `${getFrontendUrl()}/dashboard/project/${project.id}`;
|
|
2523
|
-
|
|
3336
|
+
clack12.log.step(`Dashboard: ${pc2.underline(dashboardUrl)}`);
|
|
2524
3337
|
if (templateDownloaded) {
|
|
2525
3338
|
const runCommand = `${pc2.cyan("cd")} ${pc2.green(dirName)} ${pc2.dim("&&")} ${pc2.cyan("npm run dev")}`;
|
|
2526
3339
|
const steps = [
|
|
2527
3340
|
`${pc2.bold("1.")} ${runCommand}`,
|
|
2528
3341
|
`${pc2.bold("2.")} Open ${pc2.cyan("Claude Code")} or ${pc2.cyan("Cursor")} and prompt your agent to add more features`
|
|
2529
3342
|
];
|
|
2530
|
-
|
|
3343
|
+
clack12.note(steps.join("\n"), "What's next");
|
|
2531
3344
|
} else {
|
|
2532
|
-
|
|
3345
|
+
clack12.log.warn("Template download failed. You can retry or set up manually.");
|
|
2533
3346
|
}
|
|
2534
3347
|
}
|
|
2535
3348
|
} else {
|
|
3349
|
+
if (opts.auth) {
|
|
3350
|
+
try {
|
|
3351
|
+
const result = await applyAuthProvider(opts.auth, process.cwd(), projectConfig, json);
|
|
3352
|
+
if (!json) {
|
|
3353
|
+
clack12.log.success(`Wired in ${opts.auth}: ${result.written.length} new, ${result.overwritten.length} replaced`);
|
|
3354
|
+
clack12.note(result.nextSteps, "What's next");
|
|
3355
|
+
}
|
|
3356
|
+
} catch (err) {
|
|
3357
|
+
const msg = `Failed to apply --auth ${opts.auth}: ${err.message}`;
|
|
3358
|
+
if (json) console.error(JSON.stringify({ warning: msg }));
|
|
3359
|
+
else clack12.log.warn(msg);
|
|
3360
|
+
}
|
|
3361
|
+
}
|
|
2536
3362
|
await installSkills(json);
|
|
2537
3363
|
await reportCliUsage("cli.link", true, 6, projectConfig);
|
|
2538
3364
|
if (!json) {
|
|
2539
3365
|
const dashboardUrl = `${getFrontendUrl()}/dashboard/project/${project.id}`;
|
|
2540
|
-
|
|
3366
|
+
clack12.log.step(`Dashboard: ${dashboardUrl}`);
|
|
2541
3367
|
const prompts = [
|
|
2542
3368
|
"Build a todo app with Google OAuth sign-in",
|
|
2543
3369
|
"Build an Instagram clone where users can upload photos, like, and comment",
|
|
2544
3370
|
"Build an AI chatbot with conversation history and deploy it to a live URL"
|
|
2545
3371
|
];
|
|
2546
|
-
|
|
3372
|
+
clack12.note(
|
|
2547
3373
|
`Open your coding agent (Claude Code, Codex, Cursor, etc.) and try:
|
|
2548
3374
|
|
|
2549
3375
|
${prompts.map((p) => `\u2022 "${p}"`).join("\n")}`,
|
|
@@ -2783,7 +3609,7 @@ function registerDbRpcCommand(dbCmd2) {
|
|
|
2783
3609
|
}
|
|
2784
3610
|
|
|
2785
3611
|
// src/commands/db/export.ts
|
|
2786
|
-
import { writeFileSync as
|
|
3612
|
+
import { writeFileSync as writeFileSync3 } from "fs";
|
|
2787
3613
|
function registerDbExportCommand(dbCmd2) {
|
|
2788
3614
|
dbCmd2.command("export").description("Export database schema and/or data").option("--format <format>", "Export format: sql or json", "sql").option("--tables <tables>", "Comma-separated list of tables to export (default: all)").option("--no-data", "Exclude table data (schema only)").option("--include-functions", "Include database functions").option("--include-sequences", "Include sequences").option("--include-views", "Include views").option("--row-limit <n>", "Maximum rows per table").option("-o, --output <file>", "Output file path (default: stdout)").action(async (opts, cmd) => {
|
|
2789
3615
|
const { json } = getRootOpts(cmd);
|
|
@@ -2823,7 +3649,7 @@ function registerDbExportCommand(dbCmd2) {
|
|
|
2823
3649
|
return;
|
|
2824
3650
|
}
|
|
2825
3651
|
if (opts.output) {
|
|
2826
|
-
|
|
3652
|
+
writeFileSync3(opts.output, content);
|
|
2827
3653
|
const tableCount = meta?.tables?.length;
|
|
2828
3654
|
const suffix = tableCount ? ` (${tableCount} tables, format: ${meta?.format ?? opts.format})` : "";
|
|
2829
3655
|
outputSuccess(`Exported to ${opts.output}${suffix}`);
|
|
@@ -2838,7 +3664,7 @@ function registerDbExportCommand(dbCmd2) {
|
|
|
2838
3664
|
|
|
2839
3665
|
// src/commands/db/import.ts
|
|
2840
3666
|
import { readFileSync as readFileSync3 } from "fs";
|
|
2841
|
-
import { basename as
|
|
3667
|
+
import { basename as basename5 } from "path";
|
|
2842
3668
|
function registerDbImportCommand(dbCmd2) {
|
|
2843
3669
|
dbCmd2.command("import <file>").description("Import database from a local SQL file").option("--truncate", "Truncate existing tables before import").action(async (file, opts, cmd) => {
|
|
2844
3670
|
const { json } = getRootOpts(cmd);
|
|
@@ -2847,7 +3673,7 @@ function registerDbImportCommand(dbCmd2) {
|
|
|
2847
3673
|
const config = getProjectConfig();
|
|
2848
3674
|
if (!config) throw new ProjectNotLinkedError();
|
|
2849
3675
|
const fileContent = readFileSync3(file);
|
|
2850
|
-
const fileName =
|
|
3676
|
+
const fileName = basename5(file);
|
|
2851
3677
|
const formData = new FormData();
|
|
2852
3678
|
formData.append("file", new Blob([fileContent]), fileName);
|
|
2853
3679
|
if (opts.truncate) {
|
|
@@ -2877,12 +3703,12 @@ function registerDbImportCommand(dbCmd2) {
|
|
|
2877
3703
|
}
|
|
2878
3704
|
|
|
2879
3705
|
// src/commands/db/migrations.ts
|
|
2880
|
-
import { existsSync as
|
|
2881
|
-
import { join as
|
|
3706
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
3707
|
+
import { join as join9 } from "path";
|
|
2882
3708
|
|
|
2883
3709
|
// src/lib/migrations.ts
|
|
2884
|
-
import { existsSync as
|
|
2885
|
-
import { join as
|
|
3710
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readdirSync } from "fs";
|
|
3711
|
+
import { join as join8 } from "path";
|
|
2886
3712
|
var MIGRATION_VERSION_REGEX = /^\d{1,64}$/u;
|
|
2887
3713
|
var MIGRATION_FILENAME_REGEX = /^(\d{1,64})_([a-z0-9-]+)\.sql$/u;
|
|
2888
3714
|
function assertValidMigrationVersion(version) {
|
|
@@ -2946,18 +3772,18 @@ function incrementMigrationVersion(version) {
|
|
|
2946
3772
|
return formatMigrationVersion(new Date(nextTimestamp));
|
|
2947
3773
|
}
|
|
2948
3774
|
function getMigrationsDir(cwd = process.cwd()) {
|
|
2949
|
-
return
|
|
3775
|
+
return join8(cwd, "migrations");
|
|
2950
3776
|
}
|
|
2951
3777
|
function ensureMigrationsDir(cwd = process.cwd()) {
|
|
2952
3778
|
const migrationsDir = getMigrationsDir(cwd);
|
|
2953
|
-
if (!
|
|
3779
|
+
if (!existsSync4(migrationsDir)) {
|
|
2954
3780
|
mkdirSync2(migrationsDir, { recursive: true });
|
|
2955
3781
|
}
|
|
2956
3782
|
return migrationsDir;
|
|
2957
3783
|
}
|
|
2958
3784
|
function listLocalMigrationFilenames(cwd = process.cwd()) {
|
|
2959
3785
|
const migrationsDir = getMigrationsDir(cwd);
|
|
2960
|
-
if (!
|
|
3786
|
+
if (!existsSync4(migrationsDir)) {
|
|
2961
3787
|
return [];
|
|
2962
3788
|
}
|
|
2963
3789
|
return readdirSync(migrationsDir).sort((left, right) => left.localeCompare(right));
|
|
@@ -3145,12 +3971,12 @@ function registerDbMigrationsCommand(dbCmd2) {
|
|
|
3145
3971
|
migration.version,
|
|
3146
3972
|
migration.name
|
|
3147
3973
|
);
|
|
3148
|
-
const filePath =
|
|
3149
|
-
if (existingLocalVersions.has(migration.version) ||
|
|
3974
|
+
const filePath = join9(migrationsDir, filename);
|
|
3975
|
+
if (existingLocalVersions.has(migration.version) || existsSync5(filePath)) {
|
|
3150
3976
|
skippedFiles.push(filename);
|
|
3151
3977
|
continue;
|
|
3152
3978
|
}
|
|
3153
|
-
|
|
3979
|
+
writeFileSync4(filePath, formatMigrationSql(migration.statements));
|
|
3154
3980
|
createdFiles.push(filename);
|
|
3155
3981
|
existingLocalVersions.add(migration.version);
|
|
3156
3982
|
}
|
|
@@ -3188,9 +4014,9 @@ function registerDbMigrationsCommand(dbCmd2) {
|
|
|
3188
4014
|
);
|
|
3189
4015
|
const filename = buildMigrationFilename(nextVersion, migrationName);
|
|
3190
4016
|
const migrationsDir = ensureMigrationsDir();
|
|
3191
|
-
const filePath =
|
|
4017
|
+
const filePath = join9(migrationsDir, filename);
|
|
3192
4018
|
try {
|
|
3193
|
-
|
|
4019
|
+
writeFileSync4(filePath, "", { flag: "wx" });
|
|
3194
4020
|
} catch (error) {
|
|
3195
4021
|
if (error.code === "EEXIST") {
|
|
3196
4022
|
throw new CLIError(`Migration file already exists: ${filename}`);
|
|
@@ -3246,8 +4072,8 @@ function registerDbMigrationsCommand(dbCmd2) {
|
|
|
3246
4072
|
`Migration ${targetMigration.filename} is not the next pending local migration. Apply ${earlierPendingMigration.filename} first, or fix/delete it locally if it is invalid or no longer needed.`
|
|
3247
4073
|
);
|
|
3248
4074
|
}
|
|
3249
|
-
const filePath =
|
|
3250
|
-
if (!
|
|
4075
|
+
const filePath = join9(getMigrationsDir(), targetMigration.filename);
|
|
4076
|
+
if (!existsSync5(filePath)) {
|
|
3251
4077
|
throw new CLIError(`Local migration file not found: ${targetMigration.filename}`);
|
|
3252
4078
|
}
|
|
3253
4079
|
const sql = readFileSync4(filePath, "utf-8");
|
|
@@ -3312,8 +4138,8 @@ function registerDbMigrationsCommand(dbCmd2) {
|
|
|
3312
4138
|
}
|
|
3313
4139
|
}
|
|
3314
4140
|
for (const migration of migrationsToApply) {
|
|
3315
|
-
const filePath =
|
|
3316
|
-
if (!
|
|
4141
|
+
const filePath = join9(getMigrationsDir(), migration.filename);
|
|
4142
|
+
if (!existsSync5(filePath)) {
|
|
3317
4143
|
throw new CLIError(`Local migration file not found: ${migration.filename}`);
|
|
3318
4144
|
}
|
|
3319
4145
|
const sql = readFileSync4(filePath, "utf-8");
|
|
@@ -3352,8 +4178,8 @@ function registerRecordsCommands(recordsCmd2) {
|
|
|
3352
4178
|
if (opts.limit) params.set("limit", String(opts.limit));
|
|
3353
4179
|
if (opts.offset) params.set("offset", String(opts.offset));
|
|
3354
4180
|
const query = params.toString();
|
|
3355
|
-
const
|
|
3356
|
-
const res = await ossFetch(
|
|
4181
|
+
const path6 = `/api/database/records/${encodeURIComponent(table)}${query ? `?${query}` : ""}`;
|
|
4182
|
+
const res = await ossFetch(path6);
|
|
3357
4183
|
const data = await res.json();
|
|
3358
4184
|
const records = data.data ?? [];
|
|
3359
4185
|
if (json) {
|
|
@@ -3522,18 +4348,18 @@ function registerFunctionsCommands(functionsCmd2) {
|
|
|
3522
4348
|
}
|
|
3523
4349
|
|
|
3524
4350
|
// src/commands/functions/deploy.ts
|
|
3525
|
-
import { readFileSync as readFileSync5, existsSync as
|
|
3526
|
-
import { join as
|
|
4351
|
+
import { readFileSync as readFileSync5, existsSync as existsSync6 } from "fs";
|
|
4352
|
+
import { join as join10 } from "path";
|
|
3527
4353
|
function registerFunctionsDeployCommand(functionsCmd2) {
|
|
3528
4354
|
functionsCmd2.command("deploy <slug>").description("Deploy an edge function (create or update)").option("--file <path>", "Path to the function source file").option("--name <name>", "Function display name").option("--description <desc>", "Function description").action(async (slug, opts, cmd) => {
|
|
3529
4355
|
const { json } = getRootOpts(cmd);
|
|
3530
4356
|
try {
|
|
3531
4357
|
await requireAuth();
|
|
3532
|
-
const filePath = opts.file ??
|
|
3533
|
-
if (!
|
|
4358
|
+
const filePath = opts.file ?? join10(process.cwd(), "insforge", "functions", slug, "index.ts");
|
|
4359
|
+
if (!existsSync6(filePath)) {
|
|
3534
4360
|
throw new CLIError(
|
|
3535
4361
|
`Source file not found: ${filePath}
|
|
3536
|
-
Specify --file <path> or create ${
|
|
4362
|
+
Specify --file <path> or create ${join10("insforge", "functions", slug, "index.ts")}`
|
|
3537
4363
|
);
|
|
3538
4364
|
}
|
|
3539
4365
|
const code = readFileSync5(filePath, "utf-8");
|
|
@@ -3657,7 +4483,7 @@ function registerFunctionsCodeCommand(functionsCmd2) {
|
|
|
3657
4483
|
}
|
|
3658
4484
|
|
|
3659
4485
|
// src/commands/functions/delete.ts
|
|
3660
|
-
import * as
|
|
4486
|
+
import * as clack13 from "@clack/prompts";
|
|
3661
4487
|
function registerFunctionsDeleteCommand(functionsCmd2) {
|
|
3662
4488
|
functionsCmd2.command("delete <slug>").description("Delete an edge function").action(async (slug, _opts, cmd) => {
|
|
3663
4489
|
const { json, yes } = getRootOpts(cmd);
|
|
@@ -3668,7 +4494,7 @@ function registerFunctionsDeleteCommand(functionsCmd2) {
|
|
|
3668
4494
|
message: `Delete function "${slug}"? This cannot be undone.`
|
|
3669
4495
|
});
|
|
3670
4496
|
if (isCancel2(confirmed) || !confirmed) {
|
|
3671
|
-
|
|
4497
|
+
clack13.log.info("Cancelled.");
|
|
3672
4498
|
return;
|
|
3673
4499
|
}
|
|
3674
4500
|
}
|
|
@@ -3723,8 +4549,8 @@ function registerStorageBucketsCommand(storageCmd2) {
|
|
|
3723
4549
|
}
|
|
3724
4550
|
|
|
3725
4551
|
// src/commands/storage/upload.ts
|
|
3726
|
-
import { readFileSync as readFileSync6, existsSync as
|
|
3727
|
-
import { basename as
|
|
4552
|
+
import { readFileSync as readFileSync6, existsSync as existsSync7 } from "fs";
|
|
4553
|
+
import { basename as basename6 } from "path";
|
|
3728
4554
|
function registerStorageUploadCommand(storageCmd2) {
|
|
3729
4555
|
storageCmd2.command("upload <file>").description("Upload a file to a storage bucket").requiredOption("--bucket <name>", "Target bucket name").option("--key <objectKey>", "Object key (defaults to filename)").action(async (file, opts, cmd) => {
|
|
3730
4556
|
const { json } = getRootOpts(cmd);
|
|
@@ -3732,11 +4558,11 @@ function registerStorageUploadCommand(storageCmd2) {
|
|
|
3732
4558
|
await requireAuth();
|
|
3733
4559
|
const config = getProjectConfig();
|
|
3734
4560
|
if (!config) throw new ProjectNotLinkedError();
|
|
3735
|
-
if (!
|
|
4561
|
+
if (!existsSync7(file)) {
|
|
3736
4562
|
throw new CLIError(`File not found: ${file}`);
|
|
3737
4563
|
}
|
|
3738
4564
|
const fileContent = readFileSync6(file);
|
|
3739
|
-
const objectKey = opts.key ??
|
|
4565
|
+
const objectKey = opts.key ?? basename6(file);
|
|
3740
4566
|
const bucketName = opts.bucket;
|
|
3741
4567
|
const formData = new FormData();
|
|
3742
4568
|
const blob = new Blob([fileContent]);
|
|
@@ -3757,7 +4583,7 @@ function registerStorageUploadCommand(storageCmd2) {
|
|
|
3757
4583
|
if (json) {
|
|
3758
4584
|
outputJson(data);
|
|
3759
4585
|
} else {
|
|
3760
|
-
outputSuccess(`Uploaded "${
|
|
4586
|
+
outputSuccess(`Uploaded "${basename6(file)}" to bucket "${bucketName}".`);
|
|
3761
4587
|
}
|
|
3762
4588
|
} catch (err) {
|
|
3763
4589
|
handleError(err, json);
|
|
@@ -3766,8 +4592,8 @@ function registerStorageUploadCommand(storageCmd2) {
|
|
|
3766
4592
|
}
|
|
3767
4593
|
|
|
3768
4594
|
// src/commands/storage/download.ts
|
|
3769
|
-
import { writeFileSync as
|
|
3770
|
-
import { join as
|
|
4595
|
+
import { writeFileSync as writeFileSync5 } from "fs";
|
|
4596
|
+
import { join as join11, basename as basename7 } from "path";
|
|
3771
4597
|
function registerStorageDownloadCommand(storageCmd2) {
|
|
3772
4598
|
storageCmd2.command("download <objectKey>").description("Download a file from a storage bucket").requiredOption("--bucket <name>", "Source bucket name").option("--output <path>", "Output file path (defaults to current directory)").action(async (objectKey, opts, cmd) => {
|
|
3773
4599
|
const { json } = getRootOpts(cmd);
|
|
@@ -3787,8 +4613,8 @@ function registerStorageDownloadCommand(storageCmd2) {
|
|
|
3787
4613
|
throw new CLIError(err.error ?? `Download failed: ${res.status}`);
|
|
3788
4614
|
}
|
|
3789
4615
|
const buffer = Buffer.from(await res.arrayBuffer());
|
|
3790
|
-
const outputPath = opts.output ??
|
|
3791
|
-
|
|
4616
|
+
const outputPath = opts.output ?? join11(process.cwd(), basename7(objectKey));
|
|
4617
|
+
writeFileSync5(outputPath, buffer);
|
|
3792
4618
|
if (json) {
|
|
3793
4619
|
outputJson({ success: true, path: outputPath, size: buffer.length });
|
|
3794
4620
|
} else {
|
|
@@ -3832,10 +4658,10 @@ function registerStorageDeleteBucketCommand(storageCmd2) {
|
|
|
3832
4658
|
try {
|
|
3833
4659
|
await requireAuth();
|
|
3834
4660
|
if (!yes && !json) {
|
|
3835
|
-
const
|
|
4661
|
+
const confirm6 = await confirm2({
|
|
3836
4662
|
message: `Delete bucket "${name}" and all its objects? This cannot be undone.`
|
|
3837
4663
|
});
|
|
3838
|
-
if (isCancel2(
|
|
4664
|
+
if (isCancel2(confirm6) || !confirm6) {
|
|
3839
4665
|
process.exit(0);
|
|
3840
4666
|
}
|
|
3841
4667
|
}
|
|
@@ -4245,8 +5071,8 @@ async function listDocs(json) {
|
|
|
4245
5071
|
);
|
|
4246
5072
|
}
|
|
4247
5073
|
}
|
|
4248
|
-
async function fetchDoc(
|
|
4249
|
-
const res = await ossFetch(
|
|
5074
|
+
async function fetchDoc(path6, label, json) {
|
|
5075
|
+
const res = await ossFetch(path6);
|
|
4250
5076
|
const data = await res.json();
|
|
4251
5077
|
const doc = data.data ?? data;
|
|
4252
5078
|
if (json) {
|
|
@@ -4386,10 +5212,10 @@ function registerSecretsDeleteCommand(secretsCmd2) {
|
|
|
4386
5212
|
try {
|
|
4387
5213
|
await requireAuth();
|
|
4388
5214
|
if (!yes && !json) {
|
|
4389
|
-
const
|
|
5215
|
+
const confirm6 = await confirm2({
|
|
4390
5216
|
message: `Delete secret "${key}"? This cannot be undone.`
|
|
4391
5217
|
});
|
|
4392
|
-
if (isCancel2(
|
|
5218
|
+
if (isCancel2(confirm6) || !confirm6) {
|
|
4393
5219
|
process.exit(0);
|
|
4394
5220
|
}
|
|
4395
5221
|
}
|
|
@@ -4577,10 +5403,10 @@ function registerSchedulesDeleteCommand(schedulesCmd2) {
|
|
|
4577
5403
|
try {
|
|
4578
5404
|
await requireAuth();
|
|
4579
5405
|
if (!yes && !json) {
|
|
4580
|
-
const
|
|
5406
|
+
const confirm6 = await confirm2({
|
|
4581
5407
|
message: `Delete schedule "${id}"? This cannot be undone.`
|
|
4582
5408
|
});
|
|
4583
|
-
if (isCancel2(
|
|
5409
|
+
if (isCancel2(confirm6) || !confirm6) {
|
|
4584
5410
|
process.exit(0);
|
|
4585
5411
|
}
|
|
4586
5412
|
}
|
|
@@ -4704,8 +5530,44 @@ function registerComputeGetCommand(computeCmd2) {
|
|
|
4704
5530
|
}
|
|
4705
5531
|
|
|
4706
5532
|
// src/commands/compute/update.ts
|
|
5533
|
+
var ENV_KEY_REGEX = /^[A-Z_][A-Z0-9_]*$/;
|
|
5534
|
+
function collect(value, previous) {
|
|
5535
|
+
return previous.concat([value]);
|
|
5536
|
+
}
|
|
5537
|
+
function parseKeyValue(raw) {
|
|
5538
|
+
const eq = raw.indexOf("=");
|
|
5539
|
+
if (eq <= 0) {
|
|
5540
|
+
throw new CLIError(
|
|
5541
|
+
`Invalid --env-set "${raw}": expected KEY=VALUE (key first, then '=', then value)`
|
|
5542
|
+
);
|
|
5543
|
+
}
|
|
5544
|
+
const key = raw.slice(0, eq);
|
|
5545
|
+
const value = raw.slice(eq + 1);
|
|
5546
|
+
if (!ENV_KEY_REGEX.test(key)) {
|
|
5547
|
+
throw new CLIError(`Invalid env var key "${key}": must match [A-Z_][A-Z0-9_]*`);
|
|
5548
|
+
}
|
|
5549
|
+
return [key, value];
|
|
5550
|
+
}
|
|
5551
|
+
function assertValidKey(key) {
|
|
5552
|
+
if (!ENV_KEY_REGEX.test(key)) {
|
|
5553
|
+
throw new CLIError(`Invalid env var key "${key}": must match [A-Z_][A-Z0-9_]*`);
|
|
5554
|
+
}
|
|
5555
|
+
}
|
|
4707
5556
|
function registerComputeUpdateCommand(computeCmd2) {
|
|
4708
|
-
computeCmd2.command("update <id>").description("Update a compute service").option("--image <image>", "Docker image URL").option("--port <port>", "Container port").option("--cpu <tier>", "CPU tier").option("--memory <mb>", "Memory in MB").option("--region <region>", "Fly.io region").option(
|
|
5557
|
+
computeCmd2.command("update <id>").description("Update a compute service").option("--image <image>", "Docker image URL").option("--port <port>", "Container port").option("--cpu <tier>", "CPU tier").option("--memory <mb>", "Memory in MB").option("--region <region>", "Fly.io region").option(
|
|
5558
|
+
"--env <json>",
|
|
5559
|
+
"Replace ALL env vars with this JSON object. To rotate one secret without restating the others, use --env-set instead."
|
|
5560
|
+
).option(
|
|
5561
|
+
"--env-set <KEY=VALUE>",
|
|
5562
|
+
"Set or update one env var (repeatable). Merges with existing \u2014 does not clear other vars.",
|
|
5563
|
+
collect,
|
|
5564
|
+
[]
|
|
5565
|
+
).option(
|
|
5566
|
+
"--env-unset <KEY>",
|
|
5567
|
+
"Remove one env var (repeatable). Merges with existing \u2014 leaves other vars in place.",
|
|
5568
|
+
collect,
|
|
5569
|
+
[]
|
|
5570
|
+
).action(async (id, opts, cmd) => {
|
|
4709
5571
|
const { json } = getRootOpts(cmd);
|
|
4710
5572
|
try {
|
|
4711
5573
|
await requireAuth();
|
|
@@ -4725,6 +5587,14 @@ function registerComputeUpdateCommand(computeCmd2) {
|
|
|
4725
5587
|
body.memory = Number(opts.memory);
|
|
4726
5588
|
}
|
|
4727
5589
|
if (opts.region) body.region = opts.region;
|
|
5590
|
+
const envSetArgs = opts.envSet;
|
|
5591
|
+
const envUnsetArgs = opts.envUnset;
|
|
5592
|
+
const hasPatch = envSetArgs.length > 0 || envUnsetArgs.length > 0;
|
|
5593
|
+
if (opts.env && hasPatch) {
|
|
5594
|
+
throw new CLIError(
|
|
5595
|
+
"--env (wholesale replace) and --env-set/--env-unset (partial merge) are mutually exclusive \u2014 pick one."
|
|
5596
|
+
);
|
|
5597
|
+
}
|
|
4728
5598
|
if (opts.env) {
|
|
4729
5599
|
try {
|
|
4730
5600
|
body.envVars = JSON.parse(opts.env);
|
|
@@ -4732,8 +5602,22 @@ function registerComputeUpdateCommand(computeCmd2) {
|
|
|
4732
5602
|
throw new CLIError("Invalid JSON for --env");
|
|
4733
5603
|
}
|
|
4734
5604
|
}
|
|
5605
|
+
if (hasPatch) {
|
|
5606
|
+
const setMap = {};
|
|
5607
|
+
for (const arg of envSetArgs) {
|
|
5608
|
+
const [k, v] = parseKeyValue(arg);
|
|
5609
|
+
setMap[k] = v;
|
|
5610
|
+
}
|
|
5611
|
+
for (const k of envUnsetArgs) assertValidKey(k);
|
|
5612
|
+
body.envVarsPatch = {
|
|
5613
|
+
...envSetArgs.length > 0 && { set: setMap },
|
|
5614
|
+
...envUnsetArgs.length > 0 && { unset: envUnsetArgs }
|
|
5615
|
+
};
|
|
5616
|
+
}
|
|
4735
5617
|
if (Object.keys(body).length === 0) {
|
|
4736
|
-
throw new CLIError(
|
|
5618
|
+
throw new CLIError(
|
|
5619
|
+
"No update fields provided. Use --image, --port, --cpu, --memory, --region, --env, --env-set, or --env-unset."
|
|
5620
|
+
);
|
|
4737
5621
|
}
|
|
4738
5622
|
const res = await ossFetch(`/api/compute/services/${encodeURIComponent(id)}`, {
|
|
4739
5623
|
method: "PATCH",
|
|
@@ -4825,46 +5709,86 @@ function registerComputeStopCommand(computeCmd2) {
|
|
|
4825
5709
|
});
|
|
4826
5710
|
}
|
|
4827
5711
|
|
|
4828
|
-
// src/commands/compute/
|
|
4829
|
-
function
|
|
4830
|
-
computeCmd2.command("
|
|
5712
|
+
// src/commands/compute/events.ts
|
|
5713
|
+
function registerComputeEventsCommand(computeCmd2) {
|
|
5714
|
+
computeCmd2.command("events <id>").description("Get compute service machine events (start/stop/exit/restart)").option("--limit <n>", "Max number of event entries", "50").action(async (id, opts, cmd) => {
|
|
4831
5715
|
const { json } = getRootOpts(cmd);
|
|
4832
5716
|
try {
|
|
4833
5717
|
await requireAuth();
|
|
4834
5718
|
const limit = Math.max(1, Math.min(Number(opts.limit) || 50, 1e3));
|
|
4835
5719
|
const res = await ossFetch(
|
|
4836
|
-
`/api/compute/services/${encodeURIComponent(id)}/
|
|
5720
|
+
`/api/compute/services/${encodeURIComponent(id)}/events?limit=${limit}`
|
|
4837
5721
|
);
|
|
4838
|
-
const
|
|
5722
|
+
const events = await res.json();
|
|
4839
5723
|
if (json) {
|
|
4840
|
-
outputJson(
|
|
5724
|
+
outputJson(events);
|
|
4841
5725
|
} else {
|
|
4842
|
-
if (!Array.isArray(
|
|
4843
|
-
console.log("No
|
|
4844
|
-
await reportCliUsage("cli.compute.
|
|
5726
|
+
if (!Array.isArray(events) || events.length === 0) {
|
|
5727
|
+
console.log("No events found.");
|
|
5728
|
+
await reportCliUsage("cli.compute.events", true);
|
|
4845
5729
|
return;
|
|
4846
5730
|
}
|
|
4847
|
-
for (const entry of
|
|
5731
|
+
for (const entry of events) {
|
|
4848
5732
|
const ts = new Date(entry.timestamp).toISOString();
|
|
4849
5733
|
console.log(`${ts} ${entry.message}`);
|
|
4850
5734
|
}
|
|
4851
5735
|
}
|
|
4852
|
-
await reportCliUsage("cli.compute.
|
|
5736
|
+
await reportCliUsage("cli.compute.events", true);
|
|
4853
5737
|
} catch (err) {
|
|
4854
|
-
await reportCliUsage("cli.compute.
|
|
5738
|
+
await reportCliUsage("cli.compute.events", false);
|
|
4855
5739
|
handleError(err, json);
|
|
4856
5740
|
}
|
|
4857
5741
|
});
|
|
4858
5742
|
}
|
|
4859
5743
|
|
|
4860
5744
|
// src/commands/compute/deploy.ts
|
|
4861
|
-
import { existsSync as
|
|
4862
|
-
import { join as
|
|
5745
|
+
import { existsSync as existsSync9 } from "fs";
|
|
5746
|
+
import { join as join13, resolve as resolve4 } from "path";
|
|
5747
|
+
|
|
5748
|
+
// src/lib/env-file.ts
|
|
5749
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
5750
|
+
var ENV_KEY_REGEX2 = /^[A-Z_][A-Z0-9_]*$/;
|
|
5751
|
+
function parseEnvFile(path6) {
|
|
5752
|
+
let raw;
|
|
5753
|
+
try {
|
|
5754
|
+
raw = readFileSync7(path6, "utf-8");
|
|
5755
|
+
} catch (err) {
|
|
5756
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5757
|
+
throw new CLIError(`Could not read --env-file at ${path6}: ${msg}`);
|
|
5758
|
+
}
|
|
5759
|
+
const result = {};
|
|
5760
|
+
const lines = raw.split(/\r?\n/);
|
|
5761
|
+
for (let i = 0; i < lines.length; i++) {
|
|
5762
|
+
const line = lines[i].trim();
|
|
5763
|
+
if (line === "" || line.startsWith("#")) continue;
|
|
5764
|
+
const eq = line.indexOf("=");
|
|
5765
|
+
if (eq <= 0) {
|
|
5766
|
+
throw new CLIError(
|
|
5767
|
+
`${path6}:${i + 1}: expected KEY=VALUE, got "${line}"`
|
|
5768
|
+
);
|
|
5769
|
+
}
|
|
5770
|
+
const key = line.slice(0, eq).trim();
|
|
5771
|
+
if (!ENV_KEY_REGEX2.test(key)) {
|
|
5772
|
+
throw new CLIError(
|
|
5773
|
+
`${path6}:${i + 1}: invalid env var key "${key}" (must match [A-Z_][A-Z0-9_]*)`
|
|
5774
|
+
);
|
|
5775
|
+
}
|
|
5776
|
+
let value = line.slice(eq + 1).trim();
|
|
5777
|
+
if (value.startsWith('"') && value.endsWith('"') && value.length >= 2 || value.startsWith("'") && value.endsWith("'") && value.length >= 2) {
|
|
5778
|
+
value = value.slice(1, -1);
|
|
5779
|
+
} else {
|
|
5780
|
+
const hash = value.indexOf(" #");
|
|
5781
|
+
if (hash >= 0) value = value.slice(0, hash).trimEnd();
|
|
5782
|
+
}
|
|
5783
|
+
result[key] = value;
|
|
5784
|
+
}
|
|
5785
|
+
return result;
|
|
5786
|
+
}
|
|
4863
5787
|
|
|
4864
5788
|
// src/lib/flyctl.ts
|
|
4865
5789
|
import { spawn, spawnSync } from "child_process";
|
|
4866
|
-
import { existsSync as
|
|
4867
|
-
import { join as
|
|
5790
|
+
import { existsSync as existsSync8, writeFileSync as writeFileSync6, unlinkSync as unlinkSync3 } from "fs";
|
|
5791
|
+
import { join as join12 } from "path";
|
|
4868
5792
|
function ensureFlyctlAvailable() {
|
|
4869
5793
|
const r = spawnSync("flyctl", ["version"], {
|
|
4870
5794
|
encoding: "utf8",
|
|
@@ -4877,8 +5801,8 @@ function ensureFlyctlAvailable() {
|
|
|
4877
5801
|
}
|
|
4878
5802
|
}
|
|
4879
5803
|
function ensureFlyTomlStub(opts) {
|
|
4880
|
-
const
|
|
4881
|
-
if (
|
|
5804
|
+
const path6 = join12(opts.dir, "fly.toml");
|
|
5805
|
+
if (existsSync8(path6)) {
|
|
4882
5806
|
return () => {
|
|
4883
5807
|
};
|
|
4884
5808
|
}
|
|
@@ -4895,10 +5819,10 @@ primary_region = "${opts.region}"
|
|
|
4895
5819
|
auto_start_machines = true
|
|
4896
5820
|
min_machines_running = 0
|
|
4897
5821
|
`;
|
|
4898
|
-
|
|
5822
|
+
writeFileSync6(path6, stub, "utf8");
|
|
4899
5823
|
return () => {
|
|
4900
5824
|
try {
|
|
4901
|
-
|
|
5825
|
+
unlinkSync3(path6);
|
|
4902
5826
|
} catch {
|
|
4903
5827
|
}
|
|
4904
5828
|
};
|
|
@@ -4973,7 +5897,10 @@ function registerComputeDeployCommand(computeCmd2) {
|
|
|
4973
5897
|
"--cpu <tier>",
|
|
4974
5898
|
"CPU tier in <kind>-<N>x format (e.g. shared-1x, performance-2x)",
|
|
4975
5899
|
"shared-1x"
|
|
4976
|
-
).option("--memory <mb>", "Memory in MB", "512").option("--region <region>", "Fly.io region", "iad").option("--env <json>", "Env vars as JSON object").
|
|
5900
|
+
).option("--memory <mb>", "Memory in MB", "512").option("--region <region>", "Fly.io region", "iad").option("--env <json>", "Env vars as JSON object").option(
|
|
5901
|
+
"--env-file <path>",
|
|
5902
|
+
"Path to a .env file (KEY=VALUE per line, #-comments + blank lines ok). Mutually exclusive with --env."
|
|
5903
|
+
).action(async (dir, opts, cmd) => {
|
|
4977
5904
|
const { json } = getRootOpts(cmd);
|
|
4978
5905
|
try {
|
|
4979
5906
|
await requireAuth();
|
|
@@ -4993,6 +5920,11 @@ function registerComputeDeployCommand(computeCmd2) {
|
|
|
4993
5920
|
if (!Number.isInteger(memory) || memory <= 0) {
|
|
4994
5921
|
throw new CLIError(`Invalid --memory: ${opts.memory}`);
|
|
4995
5922
|
}
|
|
5923
|
+
if (opts.env && opts.envFile) {
|
|
5924
|
+
throw new CLIError(
|
|
5925
|
+
"--env and --env-file are mutually exclusive \u2014 pick one source for the env vars."
|
|
5926
|
+
);
|
|
5927
|
+
}
|
|
4996
5928
|
let envVars;
|
|
4997
5929
|
if (opts.env) {
|
|
4998
5930
|
let parsed;
|
|
@@ -5012,6 +5944,8 @@ function registerComputeDeployCommand(computeCmd2) {
|
|
|
5012
5944
|
}
|
|
5013
5945
|
}
|
|
5014
5946
|
envVars = parsed;
|
|
5947
|
+
} else if (opts.envFile) {
|
|
5948
|
+
envVars = parseEnvFile(resolve4(opts.envFile));
|
|
5015
5949
|
}
|
|
5016
5950
|
const baseBody = {
|
|
5017
5951
|
name: opts.name,
|
|
@@ -5054,8 +5988,8 @@ function registerComputeDeployCommand(computeCmd2) {
|
|
|
5054
5988
|
return;
|
|
5055
5989
|
}
|
|
5056
5990
|
const absDir = resolve4(dir);
|
|
5057
|
-
const dockerfilePath =
|
|
5058
|
-
if (!
|
|
5991
|
+
const dockerfilePath = join13(absDir, "Dockerfile");
|
|
5992
|
+
if (!existsSync9(dockerfilePath)) {
|
|
5059
5993
|
throw new CLIError(
|
|
5060
5994
|
`No Dockerfile at ${dockerfilePath}.
|
|
5061
5995
|
Either:
|
|
@@ -5289,7 +6223,7 @@ function formatSize2(gb) {
|
|
|
5289
6223
|
|
|
5290
6224
|
// src/commands/diagnose/index.ts
|
|
5291
6225
|
import * as os from "os";
|
|
5292
|
-
import * as
|
|
6226
|
+
import * as clack14 from "@clack/prompts";
|
|
5293
6227
|
|
|
5294
6228
|
// src/commands/diagnose/metrics.ts
|
|
5295
6229
|
var METRIC_LABELS = {
|
|
@@ -5830,10 +6764,10 @@ function registerDiagnoseCommands(diagnoseCmd2) {
|
|
|
5830
6764
|
if (question.length === 0 || question.length > 2e3) {
|
|
5831
6765
|
throw new CLIError("Question must be between 1 and 2000 characters.");
|
|
5832
6766
|
}
|
|
5833
|
-
const s = !json ?
|
|
6767
|
+
const s = !json ? clack14.spinner() : null;
|
|
5834
6768
|
s?.start("Collecting diagnostic data...");
|
|
5835
6769
|
const data2 = await collectDiagnosticData(projectId, ossMode, apiUrl);
|
|
5836
|
-
const cliVersion = "0.1.
|
|
6770
|
+
const cliVersion = "0.1.63";
|
|
5837
6771
|
s?.stop("Data collected");
|
|
5838
6772
|
if (!json) {
|
|
5839
6773
|
console.log(`
|
|
@@ -5966,9 +6900,9 @@ function registerDiagnoseCommands(diagnoseCmd2) {
|
|
|
5966
6900
|
void 0,
|
|
5967
6901
|
apiUrl
|
|
5968
6902
|
);
|
|
5969
|
-
|
|
6903
|
+
clack14.log.success("Thanks for your feedback!");
|
|
5970
6904
|
} catch {
|
|
5971
|
-
|
|
6905
|
+
clack14.log.warn("Failed to submit rating.");
|
|
5972
6906
|
}
|
|
5973
6907
|
}
|
|
5974
6908
|
}
|
|
@@ -6061,17 +6995,20 @@ function formatBytesCompact(bytes) {
|
|
|
6061
6995
|
}
|
|
6062
6996
|
|
|
6063
6997
|
// src/lib/api/payments.ts
|
|
6064
|
-
function withQuery(
|
|
6998
|
+
function withQuery(path6, params) {
|
|
6065
6999
|
const query = new URLSearchParams();
|
|
6066
7000
|
for (const [key, value] of Object.entries(params)) {
|
|
6067
7001
|
if (value !== void 0) query.set(key, String(value));
|
|
6068
7002
|
}
|
|
6069
7003
|
const suffix = query.toString();
|
|
6070
|
-
return suffix ? `${
|
|
7004
|
+
return suffix ? `${path6}?${suffix}` : path6;
|
|
6071
7005
|
}
|
|
6072
7006
|
async function readJson(res) {
|
|
6073
7007
|
return await res.json();
|
|
6074
7008
|
}
|
|
7009
|
+
function withEnvironmentPath(environment, suffix) {
|
|
7010
|
+
return `/api/payments/${encodeURIComponent(environment)}${suffix}`;
|
|
7011
|
+
}
|
|
6075
7012
|
async function getPaymentsStatus() {
|
|
6076
7013
|
return readJson(await ossFetch("/api/payments/status"));
|
|
6077
7014
|
}
|
|
@@ -6079,93 +7016,158 @@ async function getPaymentsConfig() {
|
|
|
6079
7016
|
return readJson(await ossFetch("/api/payments/config"));
|
|
6080
7017
|
}
|
|
6081
7018
|
async function setStripeSecretKey(environment, secretKey) {
|
|
6082
|
-
return readJson(
|
|
6083
|
-
|
|
6084
|
-
|
|
6085
|
-
|
|
7019
|
+
return readJson(
|
|
7020
|
+
await ossFetch(withEnvironmentPath(environment, "/config"), {
|
|
7021
|
+
method: "PUT",
|
|
7022
|
+
body: JSON.stringify({ secretKey })
|
|
7023
|
+
})
|
|
7024
|
+
);
|
|
6086
7025
|
}
|
|
6087
7026
|
async function removeStripeSecretKey(environment) {
|
|
6088
|
-
return readJson(
|
|
6089
|
-
|
|
6090
|
-
|
|
7027
|
+
return readJson(
|
|
7028
|
+
await ossFetch(withEnvironmentPath(environment, "/config"), {
|
|
7029
|
+
method: "DELETE"
|
|
7030
|
+
})
|
|
7031
|
+
);
|
|
6091
7032
|
}
|
|
6092
7033
|
async function syncPayments(environment = "all") {
|
|
6093
|
-
return readJson(
|
|
6094
|
-
|
|
6095
|
-
|
|
6096
|
-
|
|
7034
|
+
return readJson(
|
|
7035
|
+
await ossFetch(
|
|
7036
|
+
environment === "all" ? "/api/payments/sync" : withEnvironmentPath(environment, "/sync"),
|
|
7037
|
+
{ method: "POST" }
|
|
7038
|
+
)
|
|
7039
|
+
);
|
|
6097
7040
|
}
|
|
6098
7041
|
async function configurePaymentWebhook(environment) {
|
|
6099
|
-
return readJson(
|
|
6100
|
-
|
|
6101
|
-
|
|
6102
|
-
|
|
7042
|
+
return readJson(
|
|
7043
|
+
await ossFetch(withEnvironmentPath(environment, "/webhook"), {
|
|
7044
|
+
method: "POST"
|
|
7045
|
+
})
|
|
7046
|
+
);
|
|
6103
7047
|
}
|
|
6104
7048
|
async function listPaymentCatalog(environment) {
|
|
6105
|
-
return readJson(await ossFetch(
|
|
7049
|
+
return readJson(await ossFetch(withEnvironmentPath(environment, "/catalog")));
|
|
6106
7050
|
}
|
|
6107
7051
|
async function listPaymentProducts(environment) {
|
|
6108
|
-
return readJson(
|
|
7052
|
+
return readJson(
|
|
7053
|
+
await ossFetch(withEnvironmentPath(environment, "/catalog/products"))
|
|
7054
|
+
);
|
|
6109
7055
|
}
|
|
6110
7056
|
async function getPaymentProduct(environment, productId) {
|
|
6111
|
-
return readJson(
|
|
6112
|
-
|
|
6113
|
-
|
|
6114
|
-
|
|
7057
|
+
return readJson(
|
|
7058
|
+
await ossFetch(
|
|
7059
|
+
withEnvironmentPath(
|
|
7060
|
+
environment,
|
|
7061
|
+
`/catalog/products/${encodeURIComponent(productId)}`
|
|
7062
|
+
)
|
|
7063
|
+
)
|
|
7064
|
+
);
|
|
6115
7065
|
}
|
|
6116
|
-
async function createPaymentProduct(request) {
|
|
6117
|
-
return readJson(
|
|
6118
|
-
|
|
6119
|
-
|
|
6120
|
-
|
|
7066
|
+
async function createPaymentProduct(environment, request) {
|
|
7067
|
+
return readJson(
|
|
7068
|
+
await ossFetch(withEnvironmentPath(environment, "/catalog/products"), {
|
|
7069
|
+
method: "POST",
|
|
7070
|
+
body: JSON.stringify(request)
|
|
7071
|
+
})
|
|
7072
|
+
);
|
|
6121
7073
|
}
|
|
6122
|
-
async function updatePaymentProduct(productId, request) {
|
|
6123
|
-
return readJson(
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
|
|
7074
|
+
async function updatePaymentProduct(environment, productId, request) {
|
|
7075
|
+
return readJson(
|
|
7076
|
+
await ossFetch(
|
|
7077
|
+
withEnvironmentPath(
|
|
7078
|
+
environment,
|
|
7079
|
+
`/catalog/products/${encodeURIComponent(productId)}`
|
|
7080
|
+
),
|
|
7081
|
+
{
|
|
7082
|
+
method: "PATCH",
|
|
7083
|
+
body: JSON.stringify(request)
|
|
7084
|
+
}
|
|
7085
|
+
)
|
|
7086
|
+
);
|
|
6127
7087
|
}
|
|
6128
7088
|
async function deletePaymentProduct(environment, productId) {
|
|
6129
|
-
return readJson(
|
|
6130
|
-
|
|
6131
|
-
|
|
6132
|
-
|
|
7089
|
+
return readJson(
|
|
7090
|
+
await ossFetch(
|
|
7091
|
+
withEnvironmentPath(
|
|
7092
|
+
environment,
|
|
7093
|
+
`/catalog/products/${encodeURIComponent(productId)}`
|
|
7094
|
+
),
|
|
7095
|
+
{ method: "DELETE" }
|
|
7096
|
+
)
|
|
7097
|
+
);
|
|
6133
7098
|
}
|
|
6134
7099
|
async function listPaymentPrices(environment, stripeProductId) {
|
|
6135
|
-
return readJson(
|
|
6136
|
-
|
|
6137
|
-
|
|
6138
|
-
|
|
7100
|
+
return readJson(
|
|
7101
|
+
await ossFetch(
|
|
7102
|
+
withQuery(withEnvironmentPath(environment, "/catalog/prices"), {
|
|
7103
|
+
stripeProductId
|
|
7104
|
+
})
|
|
7105
|
+
)
|
|
7106
|
+
);
|
|
6139
7107
|
}
|
|
6140
7108
|
async function getPaymentPrice(environment, priceId) {
|
|
6141
|
-
return readJson(
|
|
6142
|
-
|
|
6143
|
-
|
|
6144
|
-
|
|
7109
|
+
return readJson(
|
|
7110
|
+
await ossFetch(
|
|
7111
|
+
withEnvironmentPath(
|
|
7112
|
+
environment,
|
|
7113
|
+
`/catalog/prices/${encodeURIComponent(priceId)}`
|
|
7114
|
+
)
|
|
7115
|
+
)
|
|
7116
|
+
);
|
|
6145
7117
|
}
|
|
6146
|
-
async function createPaymentPrice(request) {
|
|
6147
|
-
return readJson(
|
|
6148
|
-
|
|
6149
|
-
|
|
6150
|
-
|
|
7118
|
+
async function createPaymentPrice(environment, request) {
|
|
7119
|
+
return readJson(
|
|
7120
|
+
await ossFetch(withEnvironmentPath(environment, "/catalog/prices"), {
|
|
7121
|
+
method: "POST",
|
|
7122
|
+
body: JSON.stringify(request)
|
|
7123
|
+
})
|
|
7124
|
+
);
|
|
6151
7125
|
}
|
|
6152
|
-
async function updatePaymentPrice(priceId, request) {
|
|
6153
|
-
return readJson(
|
|
6154
|
-
|
|
6155
|
-
|
|
6156
|
-
|
|
7126
|
+
async function updatePaymentPrice(environment, priceId, request) {
|
|
7127
|
+
return readJson(
|
|
7128
|
+
await ossFetch(
|
|
7129
|
+
withEnvironmentPath(
|
|
7130
|
+
environment,
|
|
7131
|
+
`/catalog/prices/${encodeURIComponent(priceId)}`
|
|
7132
|
+
),
|
|
7133
|
+
{
|
|
7134
|
+
method: "PATCH",
|
|
7135
|
+
body: JSON.stringify(request)
|
|
7136
|
+
}
|
|
7137
|
+
)
|
|
7138
|
+
);
|
|
6157
7139
|
}
|
|
6158
7140
|
async function archivePaymentPrice(environment, priceId) {
|
|
6159
|
-
return readJson(
|
|
6160
|
-
|
|
6161
|
-
|
|
6162
|
-
|
|
7141
|
+
return readJson(
|
|
7142
|
+
await ossFetch(
|
|
7143
|
+
withEnvironmentPath(
|
|
7144
|
+
environment,
|
|
7145
|
+
`/catalog/prices/${encodeURIComponent(priceId)}`
|
|
7146
|
+
),
|
|
7147
|
+
{ method: "DELETE" }
|
|
7148
|
+
)
|
|
7149
|
+
);
|
|
6163
7150
|
}
|
|
6164
|
-
async function listSubscriptions(request) {
|
|
6165
|
-
return readJson(
|
|
7151
|
+
async function listSubscriptions(environment, request) {
|
|
7152
|
+
return readJson(
|
|
7153
|
+
await ossFetch(
|
|
7154
|
+
withQuery(withEnvironmentPath(environment, "/subscriptions"), request)
|
|
7155
|
+
)
|
|
7156
|
+
);
|
|
7157
|
+
}
|
|
7158
|
+
async function listPaymentCustomers(environment, request = {}) {
|
|
7159
|
+
return readJson(
|
|
7160
|
+
await ossFetch(
|
|
7161
|
+
withQuery(withEnvironmentPath(environment, "/customers"), request)
|
|
7162
|
+
)
|
|
7163
|
+
);
|
|
6166
7164
|
}
|
|
6167
|
-
async function listPaymentHistory(request) {
|
|
6168
|
-
return readJson(
|
|
7165
|
+
async function listPaymentHistory(environment, request) {
|
|
7166
|
+
return readJson(
|
|
7167
|
+
await ossFetch(
|
|
7168
|
+
withQuery(withEnvironmentPath(environment, "/payment-history"), request)
|
|
7169
|
+
)
|
|
7170
|
+
);
|
|
6169
7171
|
}
|
|
6170
7172
|
|
|
6171
7173
|
// src/commands/payments/utils.ts
|
|
@@ -6263,10 +7265,13 @@ async function trackPaymentUsage(subcommand, success, properties = {}) {
|
|
|
6263
7265
|
|
|
6264
7266
|
// src/commands/payments/catalog.ts
|
|
6265
7267
|
function registerPaymentsCatalogCommand(paymentsCmd2) {
|
|
6266
|
-
paymentsCmd2.command("catalog").description("List mirrored Stripe products and prices
|
|
7268
|
+
paymentsCmd2.command("catalog").description("List mirrored Stripe products and prices for one environment").requiredOption(
|
|
7269
|
+
"--environment <environment>",
|
|
7270
|
+
"Stripe environment: test or live"
|
|
7271
|
+
).action(async (opts, cmd) => {
|
|
6267
7272
|
const { json } = getRootOpts(cmd);
|
|
6268
7273
|
try {
|
|
6269
|
-
const environment =
|
|
7274
|
+
const environment = parseEnvironment(opts.environment);
|
|
6270
7275
|
await requireAuth();
|
|
6271
7276
|
const data = await listPaymentCatalog(environment);
|
|
6272
7277
|
if (json) {
|
|
@@ -6293,7 +7298,15 @@ function registerPaymentsCatalogCommand(paymentsCmd2) {
|
|
|
6293
7298
|
if (data.prices.length > 0) {
|
|
6294
7299
|
console.log("Prices");
|
|
6295
7300
|
outputTable(
|
|
6296
|
-
[
|
|
7301
|
+
[
|
|
7302
|
+
"Env",
|
|
7303
|
+
"Price ID",
|
|
7304
|
+
"Product ID",
|
|
7305
|
+
"Amount",
|
|
7306
|
+
"Type",
|
|
7307
|
+
"Active",
|
|
7308
|
+
"Recurring"
|
|
7309
|
+
],
|
|
6297
7310
|
data.prices.map((price) => [
|
|
6298
7311
|
price.environment,
|
|
6299
7312
|
price.stripePriceId,
|
|
@@ -6301,14 +7314,19 @@ function registerPaymentsCatalogCommand(paymentsCmd2) {
|
|
|
6301
7314
|
formatAmount(price.unitAmount, price.currency),
|
|
6302
7315
|
price.type,
|
|
6303
7316
|
price.active ? "Yes" : "No",
|
|
6304
|
-
formatRecurring(
|
|
7317
|
+
formatRecurring(
|
|
7318
|
+
price.recurringInterval,
|
|
7319
|
+
price.recurringIntervalCount
|
|
7320
|
+
)
|
|
6305
7321
|
])
|
|
6306
7322
|
);
|
|
6307
7323
|
}
|
|
6308
7324
|
}
|
|
6309
7325
|
await trackPaymentUsage("catalog", true, { environment });
|
|
6310
7326
|
} catch (err) {
|
|
6311
|
-
await trackPaymentUsage("catalog", false, {
|
|
7327
|
+
await trackPaymentUsage("catalog", false, {
|
|
7328
|
+
environment: opts.environment
|
|
7329
|
+
});
|
|
6312
7330
|
handleError(err, json);
|
|
6313
7331
|
}
|
|
6314
7332
|
});
|
|
@@ -6383,10 +7401,10 @@ function registerPaymentsConfigCommand(paymentsCmd2) {
|
|
|
6383
7401
|
throw new CLIError("Use --yes with --json to remove a Stripe key non-interactively.");
|
|
6384
7402
|
}
|
|
6385
7403
|
if (!yes) {
|
|
6386
|
-
const
|
|
7404
|
+
const confirm6 = await confirm2({
|
|
6387
7405
|
message: `Remove Stripe ${environment} key? Payment sync and mutations for this environment will stop.`
|
|
6388
7406
|
});
|
|
6389
|
-
if (isCancel2(
|
|
7407
|
+
if (isCancel2(confirm6) || !confirm6) process.exit(0);
|
|
6390
7408
|
}
|
|
6391
7409
|
const data = await removeStripeSecretKey(environment);
|
|
6392
7410
|
if (json) {
|
|
@@ -6402,16 +7420,80 @@ function registerPaymentsConfigCommand(paymentsCmd2) {
|
|
|
6402
7420
|
});
|
|
6403
7421
|
}
|
|
6404
7422
|
|
|
7423
|
+
// src/commands/payments/customers.ts
|
|
7424
|
+
function formatPaymentMethod(customer) {
|
|
7425
|
+
if (customer.paymentMethodBrand && customer.paymentMethodLast4) {
|
|
7426
|
+
return `${customer.paymentMethodBrand} **** ${customer.paymentMethodLast4}`;
|
|
7427
|
+
}
|
|
7428
|
+
if (customer.paymentMethodBrand) {
|
|
7429
|
+
return customer.paymentMethodBrand;
|
|
7430
|
+
}
|
|
7431
|
+
if (customer.paymentMethodLast4) {
|
|
7432
|
+
return `**** ${customer.paymentMethodLast4}`;
|
|
7433
|
+
}
|
|
7434
|
+
return "-";
|
|
7435
|
+
}
|
|
7436
|
+
function registerPaymentsCustomersCommand(paymentsCmd2) {
|
|
7437
|
+
paymentsCmd2.command("customers").description("List mirrored Stripe customers").requiredOption(
|
|
7438
|
+
"--environment <environment>",
|
|
7439
|
+
"Stripe environment: test or live"
|
|
7440
|
+
).option("--limit <limit>", "Maximum rows to return (1-100)", "50").action(async (opts, cmd) => {
|
|
7441
|
+
const { json } = getRootOpts(cmd);
|
|
7442
|
+
try {
|
|
7443
|
+
const environment = parseEnvironment(opts.environment);
|
|
7444
|
+
const limit = parseIntegerOption(opts.limit, "--limit", { min: 1, max: 100 }) ?? 50;
|
|
7445
|
+
await requireAuth();
|
|
7446
|
+
const data = await listPaymentCustomers(environment, { limit });
|
|
7447
|
+
if (json) {
|
|
7448
|
+
outputJson(data);
|
|
7449
|
+
} else if (data.customers.length === 0) {
|
|
7450
|
+
console.log("No Stripe customers found.");
|
|
7451
|
+
} else {
|
|
7452
|
+
outputTable(
|
|
7453
|
+
[
|
|
7454
|
+
"Customer ID",
|
|
7455
|
+
"Email",
|
|
7456
|
+
"Name",
|
|
7457
|
+
"Payments",
|
|
7458
|
+
"Total Spend",
|
|
7459
|
+
"Last Payment",
|
|
7460
|
+
"Method",
|
|
7461
|
+
"Country"
|
|
7462
|
+
],
|
|
7463
|
+
data.customers.map((customer) => [
|
|
7464
|
+
customer.stripeCustomerId,
|
|
7465
|
+
customer.email ?? "-",
|
|
7466
|
+
customer.name ?? "-",
|
|
7467
|
+
String(customer.paymentsCount),
|
|
7468
|
+
formatAmount(customer.totalSpend, customer.totalSpendCurrency),
|
|
7469
|
+
formatDate(customer.lastPaymentAt),
|
|
7470
|
+
formatPaymentMethod(customer),
|
|
7471
|
+
customer.countryCode?.toUpperCase() ?? "-"
|
|
7472
|
+
])
|
|
7473
|
+
);
|
|
7474
|
+
}
|
|
7475
|
+
await trackPaymentUsage("customers", true, { environment });
|
|
7476
|
+
} catch (err) {
|
|
7477
|
+
await trackPaymentUsage("customers", false, {
|
|
7478
|
+
environment: opts.environment
|
|
7479
|
+
});
|
|
7480
|
+
handleError(err, json);
|
|
7481
|
+
}
|
|
7482
|
+
});
|
|
7483
|
+
}
|
|
7484
|
+
|
|
6405
7485
|
// src/commands/payments/history.ts
|
|
6406
7486
|
function registerPaymentsHistoryCommand(paymentsCmd2) {
|
|
6407
|
-
paymentsCmd2.command("history").description("List mirrored Stripe payment history").requiredOption(
|
|
7487
|
+
paymentsCmd2.command("history").description("List mirrored Stripe payment history").requiredOption(
|
|
7488
|
+
"--environment <environment>",
|
|
7489
|
+
"Stripe environment: test or live"
|
|
7490
|
+
).option("--subject-type <type>", "Filter by billing subject type").option("--subject-id <id>", "Filter by billing subject id").option("--limit <limit>", "Maximum rows to return (1-100)", "50").action(async (opts, cmd) => {
|
|
6408
7491
|
const { json } = getRootOpts(cmd);
|
|
6409
7492
|
try {
|
|
6410
7493
|
const environment = parseEnvironment(opts.environment);
|
|
6411
7494
|
const limit = parseIntegerOption(opts.limit, "--limit", { min: 1, max: 100 }) ?? 50;
|
|
6412
7495
|
await requireAuth();
|
|
6413
|
-
const data = await listPaymentHistory({
|
|
6414
|
-
environment,
|
|
7496
|
+
const data = await listPaymentHistory(environment, {
|
|
6415
7497
|
limit,
|
|
6416
7498
|
...opts.subjectType !== void 0 ? { subjectType: opts.subjectType } : {},
|
|
6417
7499
|
...opts.subjectId !== void 0 ? { subjectId: opts.subjectId } : {}
|
|
@@ -6422,7 +7504,15 @@ function registerPaymentsHistoryCommand(paymentsCmd2) {
|
|
|
6422
7504
|
console.log("No Stripe payment history found.");
|
|
6423
7505
|
} else {
|
|
6424
7506
|
outputTable(
|
|
6425
|
-
[
|
|
7507
|
+
[
|
|
7508
|
+
"Type",
|
|
7509
|
+
"Status",
|
|
7510
|
+
"Subject",
|
|
7511
|
+
"Amount",
|
|
7512
|
+
"Customer",
|
|
7513
|
+
"Stripe Object",
|
|
7514
|
+
"When"
|
|
7515
|
+
],
|
|
6426
7516
|
data.paymentHistory.map((entry) => [
|
|
6427
7517
|
entry.type,
|
|
6428
7518
|
entry.status,
|
|
@@ -6430,13 +7520,17 @@ function registerPaymentsHistoryCommand(paymentsCmd2) {
|
|
|
6430
7520
|
formatAmount(entry.amount, entry.currency),
|
|
6431
7521
|
entry.stripeCustomerId ?? "-",
|
|
6432
7522
|
entry.stripeCheckoutSessionId ?? entry.stripeInvoiceId ?? entry.stripePaymentIntentId ?? entry.stripeRefundId ?? "-",
|
|
6433
|
-
formatDate(
|
|
7523
|
+
formatDate(
|
|
7524
|
+
entry.paidAt ?? entry.failedAt ?? entry.refundedAt ?? entry.stripeCreatedAt
|
|
7525
|
+
)
|
|
6434
7526
|
])
|
|
6435
7527
|
);
|
|
6436
7528
|
}
|
|
6437
7529
|
await trackPaymentUsage("history", true, { environment });
|
|
6438
7530
|
} catch (err) {
|
|
6439
|
-
await trackPaymentUsage("history", false, {
|
|
7531
|
+
await trackPaymentUsage("history", false, {
|
|
7532
|
+
environment: opts.environment
|
|
7533
|
+
});
|
|
6440
7534
|
handleError(err, json);
|
|
6441
7535
|
}
|
|
6442
7536
|
});
|
|
@@ -6448,14 +7542,24 @@ function nullableString(value) {
|
|
|
6448
7542
|
return value === "null" ? null : value;
|
|
6449
7543
|
}
|
|
6450
7544
|
function parseRecurringInterval(value) {
|
|
6451
|
-
if (value === void 0)
|
|
6452
|
-
|
|
7545
|
+
if (value === void 0) {
|
|
7546
|
+
return void 0;
|
|
7547
|
+
}
|
|
7548
|
+
if (value === "day" || value === "week" || value === "month" || value === "year") {
|
|
7549
|
+
return value;
|
|
7550
|
+
}
|
|
6453
7551
|
throw new CLIError("--interval must be one of: day, week, month, year.");
|
|
6454
7552
|
}
|
|
6455
7553
|
function parseTaxBehavior(value) {
|
|
6456
|
-
if (value === void 0)
|
|
6457
|
-
|
|
6458
|
-
|
|
7554
|
+
if (value === void 0) {
|
|
7555
|
+
return void 0;
|
|
7556
|
+
}
|
|
7557
|
+
if (value === "exclusive" || value === "inclusive" || value === "unspecified") {
|
|
7558
|
+
return value;
|
|
7559
|
+
}
|
|
7560
|
+
throw new CLIError(
|
|
7561
|
+
"--tax-behavior must be one of: exclusive, inclusive, unspecified."
|
|
7562
|
+
);
|
|
6459
7563
|
}
|
|
6460
7564
|
function outputPricesTable(prices) {
|
|
6461
7565
|
if (prices.length === 0) {
|
|
@@ -6463,7 +7567,16 @@ function outputPricesTable(prices) {
|
|
|
6463
7567
|
return;
|
|
6464
7568
|
}
|
|
6465
7569
|
outputTable(
|
|
6466
|
-
[
|
|
7570
|
+
[
|
|
7571
|
+
"Env",
|
|
7572
|
+
"Price ID",
|
|
7573
|
+
"Product ID",
|
|
7574
|
+
"Amount",
|
|
7575
|
+
"Type",
|
|
7576
|
+
"Active",
|
|
7577
|
+
"Recurring",
|
|
7578
|
+
"Synced At"
|
|
7579
|
+
],
|
|
6467
7580
|
prices.map((price) => [
|
|
6468
7581
|
price.environment,
|
|
6469
7582
|
price.stripePriceId,
|
|
@@ -6478,7 +7591,10 @@ function outputPricesTable(prices) {
|
|
|
6478
7591
|
}
|
|
6479
7592
|
function registerPaymentsPricesCommand(paymentsCmd2) {
|
|
6480
7593
|
const pricesCmd = paymentsCmd2.command("prices").description("Manage Stripe prices");
|
|
6481
|
-
pricesCmd.command("list").description("List mirrored Stripe prices").requiredOption(
|
|
7594
|
+
pricesCmd.command("list").description("List mirrored Stripe prices").requiredOption(
|
|
7595
|
+
"--environment <environment>",
|
|
7596
|
+
"Stripe environment: test or live"
|
|
7597
|
+
).option("--product <productId>", "Filter by Stripe product id").action(async (opts, cmd) => {
|
|
6482
7598
|
const { json } = getRootOpts(cmd);
|
|
6483
7599
|
try {
|
|
6484
7600
|
const environment = parseEnvironment(opts.environment);
|
|
@@ -6491,11 +7607,16 @@ function registerPaymentsPricesCommand(paymentsCmd2) {
|
|
|
6491
7607
|
}
|
|
6492
7608
|
await trackPaymentUsage("prices.list", true, { environment });
|
|
6493
7609
|
} catch (err) {
|
|
6494
|
-
await trackPaymentUsage("prices.list", false, {
|
|
7610
|
+
await trackPaymentUsage("prices.list", false, {
|
|
7611
|
+
environment: opts.environment
|
|
7612
|
+
});
|
|
6495
7613
|
handleError(err, json);
|
|
6496
7614
|
}
|
|
6497
7615
|
});
|
|
6498
|
-
pricesCmd.command("get <priceId>").description("Show one Stripe price").requiredOption(
|
|
7616
|
+
pricesCmd.command("get <priceId>").description("Show one Stripe price").requiredOption(
|
|
7617
|
+
"--environment <environment>",
|
|
7618
|
+
"Stripe environment: test or live"
|
|
7619
|
+
).action(async (priceId, opts, cmd) => {
|
|
6499
7620
|
const { json } = getRootOpts(cmd);
|
|
6500
7621
|
try {
|
|
6501
7622
|
const environment = parseEnvironment(opts.environment);
|
|
@@ -6508,22 +7629,39 @@ function registerPaymentsPricesCommand(paymentsCmd2) {
|
|
|
6508
7629
|
}
|
|
6509
7630
|
await trackPaymentUsage("prices.get", true, { environment });
|
|
6510
7631
|
} catch (err) {
|
|
6511
|
-
await trackPaymentUsage("prices.get", false, {
|
|
7632
|
+
await trackPaymentUsage("prices.get", false, {
|
|
7633
|
+
environment: opts.environment
|
|
7634
|
+
});
|
|
6512
7635
|
handleError(err, json);
|
|
6513
7636
|
}
|
|
6514
7637
|
});
|
|
6515
|
-
pricesCmd.command("create").description("Create a Stripe one-time or recurring price").requiredOption(
|
|
7638
|
+
pricesCmd.command("create").description("Create a Stripe one-time or recurring price").requiredOption(
|
|
7639
|
+
"--environment <environment>",
|
|
7640
|
+
"Stripe environment: test or live"
|
|
7641
|
+
).requiredOption("--product <productId>", "Stripe product id").requiredOption(
|
|
7642
|
+
"--currency <currency>",
|
|
7643
|
+
"Three-letter currency code, e.g. usd"
|
|
7644
|
+
).requiredOption(
|
|
7645
|
+
"--unit-amount <amount>",
|
|
7646
|
+
"Unit amount in the smallest currency unit, e.g. cents"
|
|
7647
|
+
).option(
|
|
7648
|
+
"--interval <interval>",
|
|
7649
|
+
"Recurring interval: day, week, month, or year"
|
|
7650
|
+
).option("--interval-count <count>", "Recurring interval count").option("--lookup-key <key>", 'Stripe lookup key, or "null"').option("--active <bool>", "Set active status (true/false)").option("--tax-behavior <behavior>", "exclusive, inclusive, or unspecified").option("--metadata <json>", "Metadata JSON object with string values").option("--idempotency-key <key>", "Caller-stable idempotency key").action(async (opts, cmd) => {
|
|
6516
7651
|
const { json } = getRootOpts(cmd);
|
|
6517
7652
|
try {
|
|
6518
7653
|
const environment = parseEnvironment(opts.environment);
|
|
6519
7654
|
await requireAuth();
|
|
6520
7655
|
const interval = parseRecurringInterval(opts.interval);
|
|
6521
|
-
const intervalCount = parseIntegerOption(
|
|
7656
|
+
const intervalCount = parseIntegerOption(
|
|
7657
|
+
opts.intervalCount,
|
|
7658
|
+
"--interval-count",
|
|
7659
|
+
{ min: 1 }
|
|
7660
|
+
);
|
|
6522
7661
|
if (!interval && intervalCount !== void 0) {
|
|
6523
7662
|
throw new CLIError("Provide --interval when using --interval-count.");
|
|
6524
7663
|
}
|
|
6525
7664
|
const request = {
|
|
6526
|
-
environment,
|
|
6527
7665
|
stripeProductId: opts.product,
|
|
6528
7666
|
currency: opts.currency,
|
|
6529
7667
|
unitAmount: parseIntegerOption(opts.unitAmount, "--unit-amount", { min: 0 }) ?? 0
|
|
@@ -6536,14 +7674,16 @@ function registerPaymentsPricesCommand(paymentsCmd2) {
|
|
|
6536
7674
|
if (active !== void 0) request.active = active;
|
|
6537
7675
|
if (taxBehavior !== void 0) request.taxBehavior = taxBehavior;
|
|
6538
7676
|
if (metadata !== void 0) request.metadata = metadata;
|
|
6539
|
-
if (opts.idempotencyKey !== void 0)
|
|
7677
|
+
if (opts.idempotencyKey !== void 0) {
|
|
7678
|
+
request.idempotencyKey = opts.idempotencyKey;
|
|
7679
|
+
}
|
|
6540
7680
|
if (interval) {
|
|
6541
7681
|
request.recurring = {
|
|
6542
7682
|
interval,
|
|
6543
7683
|
...intervalCount !== void 0 ? { intervalCount } : {}
|
|
6544
7684
|
};
|
|
6545
7685
|
}
|
|
6546
|
-
const data = await createPaymentPrice(request);
|
|
7686
|
+
const data = await createPaymentPrice(environment, request);
|
|
6547
7687
|
if (json) {
|
|
6548
7688
|
outputJson(data);
|
|
6549
7689
|
} else {
|
|
@@ -6551,16 +7691,21 @@ function registerPaymentsPricesCommand(paymentsCmd2) {
|
|
|
6551
7691
|
}
|
|
6552
7692
|
await trackPaymentUsage("prices.create", true, { environment });
|
|
6553
7693
|
} catch (err) {
|
|
6554
|
-
await trackPaymentUsage("prices.create", false, {
|
|
7694
|
+
await trackPaymentUsage("prices.create", false, {
|
|
7695
|
+
environment: opts.environment
|
|
7696
|
+
});
|
|
6555
7697
|
handleError(err, json);
|
|
6556
7698
|
}
|
|
6557
7699
|
});
|
|
6558
|
-
pricesCmd.command("update <priceId>").description("Update a Stripe price").requiredOption(
|
|
7700
|
+
pricesCmd.command("update <priceId>").description("Update a Stripe price").requiredOption(
|
|
7701
|
+
"--environment <environment>",
|
|
7702
|
+
"Stripe environment: test or live"
|
|
7703
|
+
).option("--active <bool>", "Set active status (true/false)").option("--lookup-key <key>", 'Stripe lookup key, or "null"').option("--tax-behavior <behavior>", "exclusive, inclusive, or unspecified").option("--metadata <json>", "Metadata JSON object with string values").action(async (priceId, opts, cmd) => {
|
|
6559
7704
|
const { json } = getRootOpts(cmd);
|
|
6560
7705
|
try {
|
|
6561
7706
|
const environment = parseEnvironment(opts.environment);
|
|
6562
7707
|
await requireAuth();
|
|
6563
|
-
const request = {
|
|
7708
|
+
const request = {};
|
|
6564
7709
|
const active = parseBooleanOption(opts.active, "--active");
|
|
6565
7710
|
const lookupKey = nullableString(opts.lookupKey);
|
|
6566
7711
|
const taxBehavior = parseTaxBehavior(opts.taxBehavior);
|
|
@@ -6569,10 +7714,12 @@ function registerPaymentsPricesCommand(paymentsCmd2) {
|
|
|
6569
7714
|
if (lookupKey !== void 0) request.lookupKey = lookupKey;
|
|
6570
7715
|
if (taxBehavior !== void 0) request.taxBehavior = taxBehavior;
|
|
6571
7716
|
if (metadata !== void 0) request.metadata = metadata;
|
|
6572
|
-
if (Object.keys(request).length ===
|
|
6573
|
-
throw new CLIError(
|
|
7717
|
+
if (Object.keys(request).length === 0) {
|
|
7718
|
+
throw new CLIError(
|
|
7719
|
+
"Provide at least one option to update (--active, --lookup-key, --tax-behavior, --metadata)."
|
|
7720
|
+
);
|
|
6574
7721
|
}
|
|
6575
|
-
const data = await updatePaymentPrice(priceId, request);
|
|
7722
|
+
const data = await updatePaymentPrice(environment, priceId, request);
|
|
6576
7723
|
if (json) {
|
|
6577
7724
|
outputJson(data);
|
|
6578
7725
|
} else {
|
|
@@ -6580,11 +7727,16 @@ function registerPaymentsPricesCommand(paymentsCmd2) {
|
|
|
6580
7727
|
}
|
|
6581
7728
|
await trackPaymentUsage("prices.update", true, { environment });
|
|
6582
7729
|
} catch (err) {
|
|
6583
|
-
await trackPaymentUsage("prices.update", false, {
|
|
7730
|
+
await trackPaymentUsage("prices.update", false, {
|
|
7731
|
+
environment: opts.environment
|
|
7732
|
+
});
|
|
6584
7733
|
handleError(err, json);
|
|
6585
7734
|
}
|
|
6586
7735
|
});
|
|
6587
|
-
pricesCmd.command("archive <priceId>").alias("delete").description("Archive a Stripe price").requiredOption(
|
|
7736
|
+
pricesCmd.command("archive <priceId>").alias("delete").description("Archive a Stripe price").requiredOption(
|
|
7737
|
+
"--environment <environment>",
|
|
7738
|
+
"Stripe environment: test or live"
|
|
7739
|
+
).action(async (priceId, opts, cmd) => {
|
|
6588
7740
|
const { json } = getRootOpts(cmd);
|
|
6589
7741
|
try {
|
|
6590
7742
|
const environment = parseEnvironment(opts.environment);
|
|
@@ -6597,7 +7749,9 @@ function registerPaymentsPricesCommand(paymentsCmd2) {
|
|
|
6597
7749
|
}
|
|
6598
7750
|
await trackPaymentUsage("prices.archive", true, { environment });
|
|
6599
7751
|
} catch (err) {
|
|
6600
|
-
await trackPaymentUsage("prices.archive", false, {
|
|
7752
|
+
await trackPaymentUsage("prices.archive", false, {
|
|
7753
|
+
environment: opts.environment
|
|
7754
|
+
});
|
|
6601
7755
|
handleError(err, json);
|
|
6602
7756
|
}
|
|
6603
7757
|
});
|
|
@@ -6627,7 +7781,10 @@ function outputProductsTable(products) {
|
|
|
6627
7781
|
}
|
|
6628
7782
|
function registerPaymentsProductsCommand(paymentsCmd2) {
|
|
6629
7783
|
const productsCmd = paymentsCmd2.command("products").description("Manage Stripe products");
|
|
6630
|
-
productsCmd.command("list").description("List mirrored Stripe products").requiredOption(
|
|
7784
|
+
productsCmd.command("list").description("List mirrored Stripe products").requiredOption(
|
|
7785
|
+
"--environment <environment>",
|
|
7786
|
+
"Stripe environment: test or live"
|
|
7787
|
+
).action(async (opts, cmd) => {
|
|
6631
7788
|
const { json } = getRootOpts(cmd);
|
|
6632
7789
|
try {
|
|
6633
7790
|
const environment = parseEnvironment(opts.environment);
|
|
@@ -6640,11 +7797,16 @@ function registerPaymentsProductsCommand(paymentsCmd2) {
|
|
|
6640
7797
|
}
|
|
6641
7798
|
await trackPaymentUsage("products.list", true, { environment });
|
|
6642
7799
|
} catch (err) {
|
|
6643
|
-
await trackPaymentUsage("products.list", false, {
|
|
7800
|
+
await trackPaymentUsage("products.list", false, {
|
|
7801
|
+
environment: opts.environment
|
|
7802
|
+
});
|
|
6644
7803
|
handleError(err, json);
|
|
6645
7804
|
}
|
|
6646
7805
|
});
|
|
6647
|
-
productsCmd.command("get <productId>").description("Show one Stripe product and its prices").requiredOption(
|
|
7806
|
+
productsCmd.command("get <productId>").description("Show one Stripe product and its prices").requiredOption(
|
|
7807
|
+
"--environment <environment>",
|
|
7808
|
+
"Stripe environment: test or live"
|
|
7809
|
+
).action(async (productId, opts, cmd) => {
|
|
6648
7810
|
const { json } = getRootOpts(cmd);
|
|
6649
7811
|
try {
|
|
6650
7812
|
const environment = parseEnvironment(opts.environment);
|
|
@@ -6670,44 +7832,55 @@ function registerPaymentsProductsCommand(paymentsCmd2) {
|
|
|
6670
7832
|
}
|
|
6671
7833
|
await trackPaymentUsage("products.get", true, { environment });
|
|
6672
7834
|
} catch (err) {
|
|
6673
|
-
await trackPaymentUsage("products.get", false, {
|
|
7835
|
+
await trackPaymentUsage("products.get", false, {
|
|
7836
|
+
environment: opts.environment
|
|
7837
|
+
});
|
|
6674
7838
|
handleError(err, json);
|
|
6675
7839
|
}
|
|
6676
7840
|
});
|
|
6677
|
-
productsCmd.command("create").description("Create a Stripe product").requiredOption(
|
|
7841
|
+
productsCmd.command("create").description("Create a Stripe product").requiredOption(
|
|
7842
|
+
"--environment <environment>",
|
|
7843
|
+
"Stripe environment: test or live"
|
|
7844
|
+
).requiredOption("--name <name>", "Product name").option("--description <description>", 'Product description, or "null"').option("--active <bool>", "Set active status (true/false)").option("--metadata <json>", "Metadata JSON object with string values").option("--idempotency-key <key>", "Caller-stable idempotency key").action(async (opts, cmd) => {
|
|
6678
7845
|
const { json } = getRootOpts(cmd);
|
|
6679
7846
|
try {
|
|
6680
7847
|
const environment = parseEnvironment(opts.environment);
|
|
6681
7848
|
await requireAuth();
|
|
6682
|
-
const request = {
|
|
6683
|
-
environment,
|
|
6684
|
-
name: opts.name
|
|
6685
|
-
};
|
|
7849
|
+
const request = { name: opts.name };
|
|
6686
7850
|
const description = nullableString2(opts.description);
|
|
6687
7851
|
const active = parseBooleanOption(opts.active, "--active");
|
|
6688
7852
|
const metadata = parseMetadataOption(opts.metadata);
|
|
6689
7853
|
if (description !== void 0) request.description = description;
|
|
6690
7854
|
if (active !== void 0) request.active = active;
|
|
6691
7855
|
if (metadata !== void 0) request.metadata = metadata;
|
|
6692
|
-
if (opts.idempotencyKey !== void 0)
|
|
6693
|
-
|
|
7856
|
+
if (opts.idempotencyKey !== void 0) {
|
|
7857
|
+
request.idempotencyKey = opts.idempotencyKey;
|
|
7858
|
+
}
|
|
7859
|
+
const data = await createPaymentProduct(environment, request);
|
|
6694
7860
|
if (json) {
|
|
6695
7861
|
outputJson(data);
|
|
6696
7862
|
} else {
|
|
6697
|
-
outputSuccess(
|
|
7863
|
+
outputSuccess(
|
|
7864
|
+
`Stripe product created: ${data.product.stripeProductId}`
|
|
7865
|
+
);
|
|
6698
7866
|
}
|
|
6699
7867
|
await trackPaymentUsage("products.create", true, { environment });
|
|
6700
7868
|
} catch (err) {
|
|
6701
|
-
await trackPaymentUsage("products.create", false, {
|
|
7869
|
+
await trackPaymentUsage("products.create", false, {
|
|
7870
|
+
environment: opts.environment
|
|
7871
|
+
});
|
|
6702
7872
|
handleError(err, json);
|
|
6703
7873
|
}
|
|
6704
7874
|
});
|
|
6705
|
-
productsCmd.command("update <productId>").description("Update a Stripe product").requiredOption(
|
|
7875
|
+
productsCmd.command("update <productId>").description("Update a Stripe product").requiredOption(
|
|
7876
|
+
"--environment <environment>",
|
|
7877
|
+
"Stripe environment: test or live"
|
|
7878
|
+
).option("--name <name>", "Product name").option("--description <description>", 'Product description, or "null"').option("--active <bool>", "Set active status (true/false)").option("--metadata <json>", "Metadata JSON object with string values").action(async (productId, opts, cmd) => {
|
|
6706
7879
|
const { json } = getRootOpts(cmd);
|
|
6707
7880
|
try {
|
|
6708
7881
|
const environment = parseEnvironment(opts.environment);
|
|
6709
7882
|
await requireAuth();
|
|
6710
|
-
const request = {
|
|
7883
|
+
const request = {};
|
|
6711
7884
|
const description = nullableString2(opts.description);
|
|
6712
7885
|
const active = parseBooleanOption(opts.active, "--active");
|
|
6713
7886
|
const metadata = parseMetadataOption(opts.metadata);
|
|
@@ -6715,34 +7888,49 @@ function registerPaymentsProductsCommand(paymentsCmd2) {
|
|
|
6715
7888
|
if (description !== void 0) request.description = description;
|
|
6716
7889
|
if (active !== void 0) request.active = active;
|
|
6717
7890
|
if (metadata !== void 0) request.metadata = metadata;
|
|
6718
|
-
if (Object.keys(request).length ===
|
|
6719
|
-
throw new CLIError(
|
|
7891
|
+
if (Object.keys(request).length === 0) {
|
|
7892
|
+
throw new CLIError(
|
|
7893
|
+
"Provide at least one option to update (--name, --description, --active, --metadata)."
|
|
7894
|
+
);
|
|
6720
7895
|
}
|
|
6721
|
-
const data = await updatePaymentProduct(
|
|
7896
|
+
const data = await updatePaymentProduct(
|
|
7897
|
+
environment,
|
|
7898
|
+
productId,
|
|
7899
|
+
request
|
|
7900
|
+
);
|
|
6722
7901
|
if (json) {
|
|
6723
7902
|
outputJson(data);
|
|
6724
7903
|
} else {
|
|
6725
|
-
outputSuccess(
|
|
7904
|
+
outputSuccess(
|
|
7905
|
+
`Stripe product updated: ${data.product.stripeProductId}`
|
|
7906
|
+
);
|
|
6726
7907
|
}
|
|
6727
7908
|
await trackPaymentUsage("products.update", true, { environment });
|
|
6728
7909
|
} catch (err) {
|
|
6729
|
-
await trackPaymentUsage("products.update", false, {
|
|
7910
|
+
await trackPaymentUsage("products.update", false, {
|
|
7911
|
+
environment: opts.environment
|
|
7912
|
+
});
|
|
6730
7913
|
handleError(err, json);
|
|
6731
7914
|
}
|
|
6732
7915
|
});
|
|
6733
|
-
productsCmd.command("delete <productId>").description("Delete a Stripe product that has no prices").requiredOption(
|
|
7916
|
+
productsCmd.command("delete <productId>").description("Delete a Stripe product that has no prices").requiredOption(
|
|
7917
|
+
"--environment <environment>",
|
|
7918
|
+
"Stripe environment: test or live"
|
|
7919
|
+
).action(async (productId, opts, cmd) => {
|
|
6734
7920
|
const { json, yes } = getRootOpts(cmd);
|
|
6735
7921
|
try {
|
|
6736
7922
|
const environment = parseEnvironment(opts.environment);
|
|
6737
7923
|
await requireAuth();
|
|
6738
7924
|
if (json && !yes) {
|
|
6739
|
-
throw new CLIError(
|
|
7925
|
+
throw new CLIError(
|
|
7926
|
+
"Use --yes with --json to delete a Stripe product non-interactively."
|
|
7927
|
+
);
|
|
6740
7928
|
}
|
|
6741
7929
|
if (!yes) {
|
|
6742
|
-
const
|
|
7930
|
+
const confirm6 = await confirm2({
|
|
6743
7931
|
message: `Delete Stripe ${environment} product "${productId}"?`
|
|
6744
7932
|
});
|
|
6745
|
-
if (isCancel2(
|
|
7933
|
+
if (isCancel2(confirm6) || !confirm6) process.exit(0);
|
|
6746
7934
|
}
|
|
6747
7935
|
const data = await deletePaymentProduct(environment, productId);
|
|
6748
7936
|
if (json) {
|
|
@@ -6752,7 +7940,9 @@ function registerPaymentsProductsCommand(paymentsCmd2) {
|
|
|
6752
7940
|
}
|
|
6753
7941
|
await trackPaymentUsage("products.delete", true, { environment });
|
|
6754
7942
|
} catch (err) {
|
|
6755
|
-
await trackPaymentUsage("products.delete", false, {
|
|
7943
|
+
await trackPaymentUsage("products.delete", false, {
|
|
7944
|
+
environment: opts.environment
|
|
7945
|
+
});
|
|
6756
7946
|
handleError(err, json);
|
|
6757
7947
|
}
|
|
6758
7948
|
});
|
|
@@ -6793,14 +7983,16 @@ function registerPaymentsStatusCommand(paymentsCmd2) {
|
|
|
6793
7983
|
|
|
6794
7984
|
// src/commands/payments/subscriptions.ts
|
|
6795
7985
|
function registerPaymentsSubscriptionsCommand(paymentsCmd2) {
|
|
6796
|
-
paymentsCmd2.command("subscriptions").description("List mirrored Stripe subscriptions").requiredOption(
|
|
7986
|
+
paymentsCmd2.command("subscriptions").description("List mirrored Stripe subscriptions").requiredOption(
|
|
7987
|
+
"--environment <environment>",
|
|
7988
|
+
"Stripe environment: test or live"
|
|
7989
|
+
).option("--subject-type <type>", "Filter by billing subject type").option("--subject-id <id>", "Filter by billing subject id").option("--limit <limit>", "Maximum rows to return (1-100)", "50").action(async (opts, cmd) => {
|
|
6797
7990
|
const { json } = getRootOpts(cmd);
|
|
6798
7991
|
try {
|
|
6799
7992
|
const environment = parseEnvironment(opts.environment);
|
|
6800
7993
|
const limit = parseIntegerOption(opts.limit, "--limit", { min: 1, max: 100 }) ?? 50;
|
|
6801
7994
|
await requireAuth();
|
|
6802
|
-
const data = await listSubscriptions({
|
|
6803
|
-
environment,
|
|
7995
|
+
const data = await listSubscriptions(environment, {
|
|
6804
7996
|
limit,
|
|
6805
7997
|
...opts.subjectType !== void 0 ? { subjectType: opts.subjectType } : {},
|
|
6806
7998
|
...opts.subjectId !== void 0 ? { subjectId: opts.subjectId } : {}
|
|
@@ -6811,7 +8003,14 @@ function registerPaymentsSubscriptionsCommand(paymentsCmd2) {
|
|
|
6811
8003
|
console.log("No Stripe subscriptions found.");
|
|
6812
8004
|
} else {
|
|
6813
8005
|
outputTable(
|
|
6814
|
-
[
|
|
8006
|
+
[
|
|
8007
|
+
"Subscription ID",
|
|
8008
|
+
"Customer",
|
|
8009
|
+
"Subject",
|
|
8010
|
+
"Status",
|
|
8011
|
+
"Items",
|
|
8012
|
+
"Period End"
|
|
8013
|
+
],
|
|
6815
8014
|
data.subscriptions.map((subscription) => [
|
|
6816
8015
|
subscription.stripeSubscriptionId,
|
|
6817
8016
|
subscription.stripeCustomerId,
|
|
@@ -6824,7 +8023,9 @@ function registerPaymentsSubscriptionsCommand(paymentsCmd2) {
|
|
|
6824
8023
|
}
|
|
6825
8024
|
await trackPaymentUsage("subscriptions", true, { environment });
|
|
6826
8025
|
} catch (err) {
|
|
6827
|
-
await trackPaymentUsage("subscriptions", false, {
|
|
8026
|
+
await trackPaymentUsage("subscriptions", false, {
|
|
8027
|
+
environment: opts.environment
|
|
8028
|
+
});
|
|
6828
8029
|
handleError(err, json);
|
|
6829
8030
|
}
|
|
6830
8031
|
});
|
|
@@ -6832,7 +8033,13 @@ function registerPaymentsSubscriptionsCommand(paymentsCmd2) {
|
|
|
6832
8033
|
|
|
6833
8034
|
// src/commands/payments/sync.ts
|
|
6834
8035
|
function registerPaymentsSyncCommand(paymentsCmd2) {
|
|
6835
|
-
paymentsCmd2.command("sync").description(
|
|
8036
|
+
paymentsCmd2.command("sync").description(
|
|
8037
|
+
"Sync configured Stripe products, prices, customers, and subscriptions"
|
|
8038
|
+
).option(
|
|
8039
|
+
"--environment <environment>",
|
|
8040
|
+
"Stripe environment: test, live, or all",
|
|
8041
|
+
"all"
|
|
8042
|
+
).action(async (opts, cmd) => {
|
|
6836
8043
|
const { json } = getRootOpts(cmd);
|
|
6837
8044
|
try {
|
|
6838
8045
|
const environment = parseEnvironmentOrAll(opts.environment);
|
|
@@ -6844,12 +8051,22 @@ function registerPaymentsSyncCommand(paymentsCmd2) {
|
|
|
6844
8051
|
console.log("No configured Stripe environments to sync.");
|
|
6845
8052
|
} else {
|
|
6846
8053
|
outputTable(
|
|
6847
|
-
[
|
|
8054
|
+
[
|
|
8055
|
+
"Env",
|
|
8056
|
+
"Status",
|
|
8057
|
+
"Products",
|
|
8058
|
+
"Prices",
|
|
8059
|
+
"Customers",
|
|
8060
|
+
"Subscriptions",
|
|
8061
|
+
"Unmapped",
|
|
8062
|
+
"Synced At"
|
|
8063
|
+
],
|
|
6848
8064
|
data.results.map((result) => [
|
|
6849
8065
|
result.environment,
|
|
6850
8066
|
result.connection.lastSyncStatus ?? result.connection.status,
|
|
6851
8067
|
String(result.connection.lastSyncCounts.products ?? 0),
|
|
6852
8068
|
String(result.connection.lastSyncCounts.prices ?? 0),
|
|
8069
|
+
String(result.connection.lastSyncCounts.customers ?? 0),
|
|
6853
8070
|
String(result.subscriptions?.synced ?? 0),
|
|
6854
8071
|
String(result.subscriptions?.unmapped ?? 0),
|
|
6855
8072
|
formatDate(result.connection.lastSyncedAt)
|
|
@@ -6859,7 +8076,9 @@ function registerPaymentsSyncCommand(paymentsCmd2) {
|
|
|
6859
8076
|
}
|
|
6860
8077
|
await trackPaymentUsage("sync", true, { environment });
|
|
6861
8078
|
} catch (err) {
|
|
6862
|
-
await trackPaymentUsage("sync", false, {
|
|
8079
|
+
await trackPaymentUsage("sync", false, {
|
|
8080
|
+
environment: opts.environment
|
|
8081
|
+
});
|
|
6863
8082
|
handleError(err, json);
|
|
6864
8083
|
}
|
|
6865
8084
|
});
|
|
@@ -6904,6 +8123,7 @@ function registerPaymentsCommands(paymentsCmd2) {
|
|
|
6904
8123
|
registerPaymentsSyncCommand(paymentsCmd2);
|
|
6905
8124
|
registerPaymentsWebhooksCommand(paymentsCmd2);
|
|
6906
8125
|
registerPaymentsCatalogCommand(paymentsCmd2);
|
|
8126
|
+
registerPaymentsCustomersCommand(paymentsCmd2);
|
|
6907
8127
|
registerPaymentsProductsCommand(paymentsCmd2);
|
|
6908
8128
|
registerPaymentsPricesCommand(paymentsCmd2);
|
|
6909
8129
|
registerPaymentsSubscriptionsCommand(paymentsCmd2);
|
|
@@ -6911,8 +8131,8 @@ function registerPaymentsCommands(paymentsCmd2) {
|
|
|
6911
8131
|
}
|
|
6912
8132
|
|
|
6913
8133
|
// src/index.ts
|
|
6914
|
-
var __dirname =
|
|
6915
|
-
var pkg = JSON.parse(
|
|
8134
|
+
var __dirname = dirname2(fileURLToPath(import.meta.url));
|
|
8135
|
+
var pkg = JSON.parse(readFileSync8(join14(__dirname, "../package.json"), "utf-8"));
|
|
6916
8136
|
var INSFORGE_LOGO = `
|
|
6917
8137
|
\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
|
|
6918
8138
|
\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
|
|
@@ -6936,6 +8156,7 @@ var orgsCmd = program.command("orgs", { hidden: true }).description("Manage orga
|
|
|
6936
8156
|
registerOrgsCommands(orgsCmd);
|
|
6937
8157
|
var projectsCmd = program.command("projects", { hidden: true }).description("Manage projects");
|
|
6938
8158
|
registerProjectsCommands(projectsCmd);
|
|
8159
|
+
registerBranchCommands(program);
|
|
6939
8160
|
var dbCmd = program.command("db").description("Database operations");
|
|
6940
8161
|
registerDbCommands(dbCmd);
|
|
6941
8162
|
registerDbTablesCommand(dbCmd);
|
|
@@ -6991,7 +8212,7 @@ registerComputeUpdateCommand(computeCmd);
|
|
|
6991
8212
|
registerComputeDeleteCommand(computeCmd);
|
|
6992
8213
|
registerComputeStartCommand(computeCmd);
|
|
6993
8214
|
registerComputeStopCommand(computeCmd);
|
|
6994
|
-
|
|
8215
|
+
registerComputeEventsCommand(computeCmd);
|
|
6995
8216
|
var schedulesCmd = program.command("schedules").description("Manage scheduled tasks (cron jobs)");
|
|
6996
8217
|
registerSchedulesListCommand(schedulesCmd);
|
|
6997
8218
|
registerSchedulesGetCommand(schedulesCmd);
|
|
@@ -7016,7 +8237,7 @@ async function showInteractiveMenu() {
|
|
|
7016
8237
|
} catch {
|
|
7017
8238
|
}
|
|
7018
8239
|
console.log(INSFORGE_LOGO);
|
|
7019
|
-
|
|
8240
|
+
clack15.intro(`InsForge CLI v${pkg.version}`);
|
|
7020
8241
|
const options = [];
|
|
7021
8242
|
if (!isLoggedIn) {
|
|
7022
8243
|
options.push({ value: "login", label: "Log in to InsForge" });
|
|
@@ -7037,7 +8258,7 @@ async function showInteractiveMenu() {
|
|
|
7037
8258
|
options
|
|
7038
8259
|
});
|
|
7039
8260
|
if (isCancel2(action)) {
|
|
7040
|
-
|
|
8261
|
+
clack15.cancel("Bye!");
|
|
7041
8262
|
process.exit(0);
|
|
7042
8263
|
}
|
|
7043
8264
|
switch (action) {
|