@insforge/cli 0.1.61 → 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 +133 -23
- package/dist/index.js +2578 -491
- 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,143 +1160,6 @@ function registerProjectsCommands(projectsCmd2) {
|
|
|
1098
1160
|
});
|
|
1099
1161
|
}
|
|
1100
1162
|
|
|
1101
|
-
// src/commands/projects/link.ts
|
|
1102
|
-
import { exec as exec3 } from "child_process";
|
|
1103
|
-
import { promisify as promisify3 } from "util";
|
|
1104
|
-
import * as fs4 from "fs/promises";
|
|
1105
|
-
import * as path4 from "path";
|
|
1106
|
-
import * as clack8 from "@clack/prompts";
|
|
1107
|
-
import pc2 from "picocolors";
|
|
1108
|
-
|
|
1109
|
-
// src/lib/skills.ts
|
|
1110
|
-
import { exec } from "child_process";
|
|
1111
|
-
import { existsSync as existsSync2, readFileSync as readFileSync2, appendFileSync } from "fs";
|
|
1112
|
-
import { join as join2 } from "path";
|
|
1113
|
-
import { promisify } from "util";
|
|
1114
|
-
import * as clack5 from "@clack/prompts";
|
|
1115
|
-
var execAsync = promisify(exec);
|
|
1116
|
-
var SKILL_INSTALL_TIMEOUT_MS = 6e4;
|
|
1117
|
-
function describeExecError(err) {
|
|
1118
|
-
const e = err;
|
|
1119
|
-
if (e.killed && (e.signal === "SIGTERM" || e.signal === "SIGKILL")) {
|
|
1120
|
-
return `timed out after ${SKILL_INSTALL_TIMEOUT_MS / 1e3}s \u2014 the npm registry may be slow or blocked by your network`;
|
|
1121
|
-
}
|
|
1122
|
-
if (e.code === "ENOENT") {
|
|
1123
|
-
return "`npx` is not on your PATH \u2014 install Node.js 18+ and reopen your shell";
|
|
1124
|
-
}
|
|
1125
|
-
const stderr = (typeof e.stderr === "string" ? e.stderr : e.stderr?.toString()) ?? "";
|
|
1126
|
-
if (/ENOTFOUND|EAI_AGAIN|getaddrinfo/i.test(stderr)) return "cannot reach the npm registry (DNS lookup failed) \u2014 check your internet connection";
|
|
1127
|
-
if (/ECONNREFUSED/i.test(stderr)) return "connection to the npm registry was refused \u2014 a proxy or firewall is likely blocking it";
|
|
1128
|
-
if (/ETIMEDOUT|ESOCKETTIMEDOUT|network timeout/i.test(stderr)) return "the npm registry timed out \u2014 check your VPN, proxy, or corporate network";
|
|
1129
|
-
if (/CERT_HAS_EXPIRED|UNABLE_TO_VERIFY_LEAF_SIGNATURE|SELF_SIGNED_CERT/i.test(stderr)) return "TLS error reaching the npm registry \u2014 a corporate proxy may be intercepting HTTPS";
|
|
1130
|
-
if (/\bE404\b|404 Not Found/i.test(stderr)) return "npm returned 404 \u2014 the `skills` package or a dependency could not be found (check your npm registry config)";
|
|
1131
|
-
if (/EACCES|permission denied/i.test(stderr)) return "permission denied writing files \u2014 run from a directory you own, without sudo";
|
|
1132
|
-
if (/ENOSPC|no space left/i.test(stderr)) return "no disk space left to install the package";
|
|
1133
|
-
if (/\b401\b|EAUTH|authentication/i.test(stderr)) return "npm authentication failed \u2014 check ~/.npmrc";
|
|
1134
|
-
if (typeof e.code === "number") return `npx exited with code ${e.code}`;
|
|
1135
|
-
if (typeof e.code === "string") return e.code;
|
|
1136
|
-
return e.message ?? "unknown error";
|
|
1137
|
-
}
|
|
1138
|
-
var GITIGNORE_ENTRIES = [
|
|
1139
|
-
".insforge",
|
|
1140
|
-
".agent",
|
|
1141
|
-
".agents",
|
|
1142
|
-
".augment",
|
|
1143
|
-
".claude",
|
|
1144
|
-
".cline",
|
|
1145
|
-
".github/copilot*",
|
|
1146
|
-
".kilocode",
|
|
1147
|
-
".qoder",
|
|
1148
|
-
".qwen",
|
|
1149
|
-
".roo",
|
|
1150
|
-
".trae",
|
|
1151
|
-
".windsurf"
|
|
1152
|
-
];
|
|
1153
|
-
function updateGitignore() {
|
|
1154
|
-
const gitignorePath = join2(process.cwd(), ".gitignore");
|
|
1155
|
-
const existing = existsSync2(gitignorePath) ? readFileSync2(gitignorePath, "utf-8") : "";
|
|
1156
|
-
const lines = new Set(existing.split("\n").map((l) => l.trim()));
|
|
1157
|
-
const missing = GITIGNORE_ENTRIES.filter((entry) => !lines.has(entry));
|
|
1158
|
-
if (!missing.length) return;
|
|
1159
|
-
const block = `
|
|
1160
|
-
# InsForge & AI agent skills
|
|
1161
|
-
${missing.join("\n")}
|
|
1162
|
-
`;
|
|
1163
|
-
appendFileSync(gitignorePath, block);
|
|
1164
|
-
}
|
|
1165
|
-
async function installSkills(json) {
|
|
1166
|
-
try {
|
|
1167
|
-
if (!json) clack5.log.info("Installing InsForge agent skills (global)...");
|
|
1168
|
-
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
|
-
cwd: process.cwd(),
|
|
1170
|
-
timeout: SKILL_INSTALL_TIMEOUT_MS
|
|
1171
|
-
});
|
|
1172
|
-
if (!json) clack5.log.success("InsForge agent skills installed.");
|
|
1173
|
-
} catch (err) {
|
|
1174
|
-
if (!json) {
|
|
1175
|
-
clack5.log.warn(`Could not install agent skills: ${describeExecError(err)}`);
|
|
1176
|
-
clack5.log.info("Run `npx skills add insforge/agent-skills` once resolved to see the full output.");
|
|
1177
|
-
}
|
|
1178
|
-
}
|
|
1179
|
-
try {
|
|
1180
|
-
if (!json) clack5.log.info("Installing find-skills (global)...");
|
|
1181
|
-
await execAsync("npx skills add https://github.com/vercel-labs/skills --skill find-skills -g -y", {
|
|
1182
|
-
cwd: process.cwd(),
|
|
1183
|
-
timeout: SKILL_INSTALL_TIMEOUT_MS
|
|
1184
|
-
});
|
|
1185
|
-
if (!json) clack5.log.success("find-skills installed.");
|
|
1186
|
-
} catch (err) {
|
|
1187
|
-
if (!json) {
|
|
1188
|
-
clack5.log.warn(`Could not install find-skills: ${describeExecError(err)}`);
|
|
1189
|
-
clack5.log.info("Run `npx skills add https://github.com/vercel-labs/skills --skill find-skills` once resolved.");
|
|
1190
|
-
}
|
|
1191
|
-
}
|
|
1192
|
-
try {
|
|
1193
|
-
updateGitignore();
|
|
1194
|
-
} catch {
|
|
1195
|
-
}
|
|
1196
|
-
}
|
|
1197
|
-
async function reportCliUsage(toolName, success, maxRetries = 1, explicitConfig) {
|
|
1198
|
-
let config = explicitConfig;
|
|
1199
|
-
if (!config) {
|
|
1200
|
-
try {
|
|
1201
|
-
config = getProjectConfig();
|
|
1202
|
-
} catch {
|
|
1203
|
-
return;
|
|
1204
|
-
}
|
|
1205
|
-
}
|
|
1206
|
-
if (!config) return;
|
|
1207
|
-
const payload = JSON.stringify({
|
|
1208
|
-
tool_name: toolName,
|
|
1209
|
-
success,
|
|
1210
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1211
|
-
});
|
|
1212
|
-
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
1213
|
-
try {
|
|
1214
|
-
const controller = new AbortController();
|
|
1215
|
-
const timer = setTimeout(() => controller.abort(), 3e3);
|
|
1216
|
-
try {
|
|
1217
|
-
const res = await fetch(`${config.oss_host}/api/usage/mcp`, {
|
|
1218
|
-
method: "POST",
|
|
1219
|
-
headers: {
|
|
1220
|
-
"Content-Type": "application/json",
|
|
1221
|
-
"x-api-key": config.api_key
|
|
1222
|
-
},
|
|
1223
|
-
body: payload,
|
|
1224
|
-
signal: controller.signal
|
|
1225
|
-
});
|
|
1226
|
-
if (res.status < 500) return;
|
|
1227
|
-
} finally {
|
|
1228
|
-
clearTimeout(timer);
|
|
1229
|
-
}
|
|
1230
|
-
} catch {
|
|
1231
|
-
}
|
|
1232
|
-
if (attempt < maxRetries - 1) {
|
|
1233
|
-
await new Promise((r) => setTimeout(r, 5e3));
|
|
1234
|
-
}
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1237
|
-
|
|
1238
1163
|
// src/lib/analytics.ts
|
|
1239
1164
|
import { PostHog } from "posthog-node";
|
|
1240
1165
|
var POSTHOG_API_KEY = "phc_ueV1ii62wdBTkH7E70ugyeqHIHu8dFDdjs0qq3TZhJz";
|
|
@@ -1269,6 +1194,17 @@ function trackDiagnose(subcommand, config) {
|
|
|
1269
1194
|
oss_mode: config.project_id === FAKE_PROJECT_ID
|
|
1270
1195
|
});
|
|
1271
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
|
+
}
|
|
1272
1208
|
async function shutdownAnalytics() {
|
|
1273
1209
|
try {
|
|
1274
1210
|
if (client) await client.shutdown();
|
|
@@ -1276,172 +1212,982 @@ async function shutdownAnalytics() {
|
|
|
1276
1212
|
}
|
|
1277
1213
|
}
|
|
1278
1214
|
|
|
1279
|
-
// src/commands/
|
|
1280
|
-
import {
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
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;
|
|
1292
1245
|
}
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
const
|
|
1297
|
-
const
|
|
1298
|
-
|
|
1299
|
-
|
|
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
|
|
1300
1278
|
});
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
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
|
+
}
|
|
1304
1287
|
}
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
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
|
+
});
|
|
1309
1299
|
}
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
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();
|
|
1324
1352
|
}
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
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})`);
|
|
1328
1363
|
}
|
|
1329
|
-
if (
|
|
1330
|
-
|
|
1364
|
+
if (showProgress && branch2.branch_state !== lastState) {
|
|
1365
|
+
outputInfo(` state: ${branch2.branch_state}\u2026`);
|
|
1366
|
+
lastState = branch2.branch_state;
|
|
1331
1367
|
}
|
|
1332
|
-
|
|
1368
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
1333
1369
|
}
|
|
1334
|
-
|
|
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;
|
|
1335
1375
|
}
|
|
1336
1376
|
|
|
1337
|
-
// src/
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
const content = await fs.readFile(filePath, "utf-8");
|
|
1347
|
-
const vars = [];
|
|
1348
|
-
for (const line of content.split("\n")) {
|
|
1349
|
-
const trimmed = line.trim();
|
|
1350
|
-
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
1351
|
-
const eqIndex = trimmed.indexOf("=");
|
|
1352
|
-
if (eqIndex === -1) continue;
|
|
1353
|
-
const key = trimmed.slice(0, eqIndex).trim();
|
|
1354
|
-
let value = trimmed.slice(eqIndex + 1).trim();
|
|
1355
|
-
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
1356
|
-
value = value.slice(1, -1);
|
|
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.");
|
|
1357
1386
|
}
|
|
1358
|
-
|
|
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();
|
|
1359
1411
|
}
|
|
1360
|
-
|
|
1361
|
-
}
|
|
1362
|
-
return [];
|
|
1412
|
+
});
|
|
1363
1413
|
}
|
|
1364
1414
|
|
|
1365
|
-
// src/commands/
|
|
1366
|
-
import
|
|
1367
|
-
import * as
|
|
1368
|
-
|
|
1369
|
-
|
|
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
|
|
1370
1522
|
import * as clack6 from "@clack/prompts";
|
|
1371
|
-
|
|
1372
|
-
var
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
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
|
+
|
|
1660
|
+
// src/commands/projects/link.ts
|
|
1661
|
+
import { exec as exec3 } from "child_process";
|
|
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";
|
|
1666
|
+
import pc2 from "picocolors";
|
|
1667
|
+
|
|
1668
|
+
// src/lib/skills.ts
|
|
1669
|
+
import { exec } from "child_process";
|
|
1670
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2, appendFileSync } from "fs";
|
|
1671
|
+
import { join as join2 } from "path";
|
|
1672
|
+
import { promisify } from "util";
|
|
1673
|
+
import * as clack8 from "@clack/prompts";
|
|
1674
|
+
var execAsync = promisify(exec);
|
|
1675
|
+
var SKILL_INSTALL_TIMEOUT_MS = 6e4;
|
|
1676
|
+
function describeExecError(err) {
|
|
1677
|
+
const e = err;
|
|
1678
|
+
if (e.killed && (e.signal === "SIGTERM" || e.signal === "SIGKILL")) {
|
|
1679
|
+
return `timed out after ${SKILL_INSTALL_TIMEOUT_MS / 1e3}s \u2014 the npm registry may be slow or blocked by your network`;
|
|
1680
|
+
}
|
|
1681
|
+
if (e.code === "ENOENT") {
|
|
1682
|
+
return "`npx` is not on your PATH \u2014 install Node.js 18+ and reopen your shell";
|
|
1683
|
+
}
|
|
1684
|
+
const stderr = (typeof e.stderr === "string" ? e.stderr : e.stderr?.toString()) ?? "";
|
|
1685
|
+
if (/ENOTFOUND|EAI_AGAIN|getaddrinfo/i.test(stderr)) return "cannot reach the npm registry (DNS lookup failed) \u2014 check your internet connection";
|
|
1686
|
+
if (/ECONNREFUSED/i.test(stderr)) return "connection to the npm registry was refused \u2014 a proxy or firewall is likely blocking it";
|
|
1687
|
+
if (/ETIMEDOUT|ESOCKETTIMEDOUT|network timeout/i.test(stderr)) return "the npm registry timed out \u2014 check your VPN, proxy, or corporate network";
|
|
1688
|
+
if (/CERT_HAS_EXPIRED|UNABLE_TO_VERIFY_LEAF_SIGNATURE|SELF_SIGNED_CERT/i.test(stderr)) return "TLS error reaching the npm registry \u2014 a corporate proxy may be intercepting HTTPS";
|
|
1689
|
+
if (/\bE404\b|404 Not Found/i.test(stderr)) return "npm returned 404 \u2014 the `skills` package or a dependency could not be found (check your npm registry config)";
|
|
1690
|
+
if (/EACCES|permission denied/i.test(stderr)) return "permission denied writing files \u2014 run from a directory you own, without sudo";
|
|
1691
|
+
if (/ENOSPC|no space left/i.test(stderr)) return "no disk space left to install the package";
|
|
1692
|
+
if (/\b401\b|EAUTH|authentication/i.test(stderr)) return "npm authentication failed \u2014 check ~/.npmrc";
|
|
1693
|
+
if (typeof e.code === "number") return `npx exited with code ${e.code}`;
|
|
1694
|
+
if (typeof e.code === "string") return e.code;
|
|
1695
|
+
return e.message ?? "unknown error";
|
|
1696
|
+
}
|
|
1697
|
+
var GITIGNORE_ENTRIES = [
|
|
1384
1698
|
".insforge",
|
|
1385
|
-
|
|
1386
|
-
".claude",
|
|
1699
|
+
".agent",
|
|
1387
1700
|
".agents",
|
|
1388
1701
|
".augment",
|
|
1702
|
+
".claude",
|
|
1703
|
+
".cline",
|
|
1704
|
+
".github/copilot*",
|
|
1389
1705
|
".kilocode",
|
|
1390
|
-
".kiro",
|
|
1391
1706
|
".qoder",
|
|
1392
1707
|
".qwen",
|
|
1393
1708
|
".roo",
|
|
1394
1709
|
".trae",
|
|
1395
|
-
".windsurf"
|
|
1396
|
-
".vercel",
|
|
1397
|
-
".turbo",
|
|
1398
|
-
".cache",
|
|
1399
|
-
"skills",
|
|
1400
|
-
"coverage"
|
|
1710
|
+
".windsurf"
|
|
1401
1711
|
];
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1712
|
+
function updateGitignore() {
|
|
1713
|
+
const gitignorePath = join2(process.cwd(), ".gitignore");
|
|
1714
|
+
const existing = existsSync3(gitignorePath) ? readFileSync2(gitignorePath, "utf-8") : "";
|
|
1715
|
+
const lines = new Set(existing.split("\n").map((l) => l.trim()));
|
|
1716
|
+
const missing = GITIGNORE_ENTRIES.filter((entry) => !lines.has(entry));
|
|
1717
|
+
if (!missing.length) return;
|
|
1718
|
+
const block = `
|
|
1719
|
+
# InsForge & AI agent skills
|
|
1720
|
+
${missing.join("\n")}
|
|
1721
|
+
`;
|
|
1722
|
+
appendFileSync(gitignorePath, block);
|
|
1723
|
+
}
|
|
1724
|
+
async function installSkills(json) {
|
|
1725
|
+
try {
|
|
1726
|
+
if (!json) clack8.log.info("Installing InsForge agent skills (global)...");
|
|
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", {
|
|
1728
|
+
cwd: process.cwd(),
|
|
1729
|
+
timeout: SKILL_INSTALL_TIMEOUT_MS
|
|
1730
|
+
});
|
|
1731
|
+
if (!json) clack8.log.success("InsForge agent skills installed.");
|
|
1732
|
+
} catch (err) {
|
|
1733
|
+
if (!json) {
|
|
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.");
|
|
1736
|
+
}
|
|
1406
1737
|
}
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1738
|
+
try {
|
|
1739
|
+
if (!json) clack8.log.info("Installing find-skills (global)...");
|
|
1740
|
+
await execAsync("npx skills add https://github.com/vercel-labs/skills --skill find-skills -g -y", {
|
|
1741
|
+
cwd: process.cwd(),
|
|
1742
|
+
timeout: SKILL_INSTALL_TIMEOUT_MS
|
|
1743
|
+
});
|
|
1744
|
+
if (!json) clack8.log.success("find-skills installed.");
|
|
1745
|
+
} catch (err) {
|
|
1746
|
+
if (!json) {
|
|
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.");
|
|
1413
1749
|
}
|
|
1414
1750
|
}
|
|
1415
|
-
if (normalized.endsWith(".log")) return true;
|
|
1416
|
-
return false;
|
|
1417
|
-
}
|
|
1418
|
-
function isInsforgeCloudOssHost(ossHost) {
|
|
1419
1751
|
try {
|
|
1420
|
-
|
|
1752
|
+
updateGitignore();
|
|
1421
1753
|
} catch {
|
|
1422
|
-
return false;
|
|
1423
|
-
}
|
|
1424
|
-
}
|
|
1425
|
-
function normalizeRelativePath(sourceDir, absolutePath) {
|
|
1426
|
-
return path2.relative(sourceDir, absolutePath).split(path2.sep).join("/").replace(/\\/g, "/");
|
|
1427
|
-
}
|
|
1428
|
-
async function hashFile(filePath) {
|
|
1429
|
-
const hash = createHash2("sha1");
|
|
1430
|
-
let size = 0;
|
|
1431
|
-
for await (const chunk of createReadStream(filePath)) {
|
|
1432
|
-
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
1433
|
-
size += buffer.length;
|
|
1434
|
-
hash.update(buffer);
|
|
1435
1754
|
}
|
|
1436
|
-
|
|
1755
|
+
}
|
|
1756
|
+
async function reportCliUsage(toolName, success, maxRetries = 1, explicitConfig) {
|
|
1757
|
+
let config = explicitConfig;
|
|
1758
|
+
if (!config) {
|
|
1759
|
+
try {
|
|
1760
|
+
config = getProjectConfig();
|
|
1761
|
+
} catch {
|
|
1762
|
+
return;
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
if (!config) return;
|
|
1766
|
+
const payload = JSON.stringify({
|
|
1767
|
+
tool_name: toolName,
|
|
1768
|
+
success,
|
|
1769
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1770
|
+
});
|
|
1771
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
1772
|
+
try {
|
|
1773
|
+
const controller = new AbortController();
|
|
1774
|
+
const timer = setTimeout(() => controller.abort(), 3e3);
|
|
1775
|
+
try {
|
|
1776
|
+
const res = await fetch(`${config.oss_host}/api/usage/mcp`, {
|
|
1777
|
+
method: "POST",
|
|
1778
|
+
headers: {
|
|
1779
|
+
"Content-Type": "application/json",
|
|
1780
|
+
"x-api-key": config.api_key
|
|
1781
|
+
},
|
|
1782
|
+
body: payload,
|
|
1783
|
+
signal: controller.signal
|
|
1784
|
+
});
|
|
1785
|
+
if (res.status < 500) return;
|
|
1786
|
+
} finally {
|
|
1787
|
+
clearTimeout(timer);
|
|
1788
|
+
}
|
|
1789
|
+
} catch {
|
|
1790
|
+
}
|
|
1791
|
+
if (attempt < maxRetries - 1) {
|
|
1792
|
+
await new Promise((r) => setTimeout(r, 5e3));
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
// src/auth-providers/apply.ts
|
|
1798
|
+
import { promises as fs } from "fs";
|
|
1799
|
+
import * as path from "path";
|
|
1800
|
+
import { tmpdir } from "os";
|
|
1801
|
+
import { execFile } from "child_process";
|
|
1802
|
+
import { promisify as promisify2 } from "util";
|
|
1803
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
1804
|
+
import * as clack9 from "@clack/prompts";
|
|
1805
|
+
|
|
1806
|
+
// src/lib/api/oss.ts
|
|
1807
|
+
function requireProjectConfig() {
|
|
1808
|
+
const config = getProjectConfig();
|
|
1809
|
+
if (!config) {
|
|
1810
|
+
throw new ProjectNotLinkedError();
|
|
1811
|
+
}
|
|
1812
|
+
return config;
|
|
1813
|
+
}
|
|
1814
|
+
async function runRawSql(sql, unrestricted = false) {
|
|
1815
|
+
const endpoint = unrestricted ? "/api/database/advance/rawsql/unrestricted" : "/api/database/advance/rawsql";
|
|
1816
|
+
const res = await ossFetch(endpoint, {
|
|
1817
|
+
method: "POST",
|
|
1818
|
+
body: JSON.stringify({ query: sql })
|
|
1819
|
+
});
|
|
1820
|
+
const raw = await res.json();
|
|
1821
|
+
const rows = raw.rows ?? raw.data ?? [];
|
|
1822
|
+
return { rows, raw };
|
|
1823
|
+
}
|
|
1824
|
+
async function getAnonKey() {
|
|
1825
|
+
const res = await ossFetch("/api/auth/tokens/anon", { method: "POST" });
|
|
1826
|
+
const data = await res.json();
|
|
1827
|
+
return data.accessToken;
|
|
1828
|
+
}
|
|
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 = {}) {
|
|
1839
|
+
const config = requireProjectConfig();
|
|
1840
|
+
const headers = {
|
|
1841
|
+
"Content-Type": "application/json",
|
|
1842
|
+
Authorization: `Bearer ${config.api_key}`,
|
|
1843
|
+
...options.headers ?? {}
|
|
1844
|
+
};
|
|
1845
|
+
const res = await fetch(`${config.oss_host}${path6}`, { ...options, headers });
|
|
1846
|
+
if (!res.ok) {
|
|
1847
|
+
const err = await res.json().catch(() => ({}));
|
|
1848
|
+
let message = err.message ?? err.error ?? `OSS request failed: ${res.status}`;
|
|
1849
|
+
if (err.nextActions) {
|
|
1850
|
+
message += `
|
|
1851
|
+
${err.nextActions}`;
|
|
1852
|
+
}
|
|
1853
|
+
const isRouteLevel404 = !err.error || err.error === "NOT_FOUND";
|
|
1854
|
+
if (res.status === 404 && isRouteLevel404 && path6.startsWith("/api/compute")) {
|
|
1855
|
+
message = "Compute services are not available on this backend.\nSelf-hosted: upgrade your InsForge instance. Cloud: contact your InsForge admin to enable compute.";
|
|
1856
|
+
}
|
|
1857
|
+
if (res.status === 404 && isRouteLevel404 && path6.startsWith("/api/payments")) {
|
|
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.";
|
|
1859
|
+
}
|
|
1860
|
+
if (res.status === 404 && isRouteLevel404 && path6 === "/api/database/migrations") {
|
|
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.";
|
|
1862
|
+
}
|
|
1863
|
+
throw new CLIError(message);
|
|
1864
|
+
}
|
|
1865
|
+
return res;
|
|
1866
|
+
}
|
|
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
|
+
|
|
2083
|
+
// src/lib/env.ts
|
|
2084
|
+
import * as fs2 from "fs/promises";
|
|
2085
|
+
import * as path2 from "path";
|
|
2086
|
+
async function readEnvFile(cwd) {
|
|
2087
|
+
const candidates = [".env.local", ".env.production", ".env"];
|
|
2088
|
+
for (const name of candidates) {
|
|
2089
|
+
const filePath = path2.join(cwd, name);
|
|
2090
|
+
const exists = await fs2.stat(filePath).catch(() => null);
|
|
2091
|
+
if (!exists) continue;
|
|
2092
|
+
const content = await fs2.readFile(filePath, "utf-8");
|
|
2093
|
+
const vars = [];
|
|
2094
|
+
for (const line of content.split("\n")) {
|
|
2095
|
+
const trimmed = line.trim();
|
|
2096
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
2097
|
+
const eqIndex = trimmed.indexOf("=");
|
|
2098
|
+
if (eqIndex === -1) continue;
|
|
2099
|
+
const key = trimmed.slice(0, eqIndex).trim();
|
|
2100
|
+
let value = trimmed.slice(eqIndex + 1).trim();
|
|
2101
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
2102
|
+
value = value.slice(1, -1);
|
|
2103
|
+
}
|
|
2104
|
+
if (key) vars.push({ key, value });
|
|
2105
|
+
}
|
|
2106
|
+
return vars;
|
|
2107
|
+
}
|
|
2108
|
+
return [];
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
// src/commands/deployments/deploy.ts
|
|
2112
|
+
import * as path3 from "path";
|
|
2113
|
+
import * as fs3 from "fs/promises";
|
|
2114
|
+
import { createReadStream } from "fs";
|
|
2115
|
+
import { createHash as createHash2 } from "crypto";
|
|
2116
|
+
import * as clack10 from "@clack/prompts";
|
|
2117
|
+
import archiver from "archiver";
|
|
2118
|
+
var POLL_INTERVAL_MS3 = 5e3;
|
|
2119
|
+
var POLL_TIMEOUT_MS3 = 3e5;
|
|
2120
|
+
var DIRECT_UPLOAD_CONCURRENCY = 8;
|
|
2121
|
+
var EXCLUDE_PATTERNS = [
|
|
2122
|
+
"node_modules",
|
|
2123
|
+
".git",
|
|
2124
|
+
".next",
|
|
2125
|
+
".env",
|
|
2126
|
+
".env.local",
|
|
2127
|
+
"dist",
|
|
2128
|
+
"build",
|
|
2129
|
+
".DS_Store",
|
|
2130
|
+
".insforge",
|
|
2131
|
+
// IDE and AI agent configs
|
|
2132
|
+
".claude",
|
|
2133
|
+
".agents",
|
|
2134
|
+
".augment",
|
|
2135
|
+
".kilocode",
|
|
2136
|
+
".kiro",
|
|
2137
|
+
".qoder",
|
|
2138
|
+
".qwen",
|
|
2139
|
+
".roo",
|
|
2140
|
+
".trae",
|
|
2141
|
+
".windsurf",
|
|
2142
|
+
".vercel",
|
|
2143
|
+
".turbo",
|
|
2144
|
+
".cache",
|
|
2145
|
+
"skills",
|
|
2146
|
+
"coverage"
|
|
2147
|
+
];
|
|
2148
|
+
var DirectDeploymentUnsupportedError = class extends Error {
|
|
2149
|
+
constructor() {
|
|
2150
|
+
super("Direct deployment endpoints are not available on this backend");
|
|
2151
|
+
this.name = "DirectDeploymentUnsupportedError";
|
|
2152
|
+
}
|
|
2153
|
+
};
|
|
2154
|
+
function shouldExclude(name) {
|
|
2155
|
+
const normalized = name.replace(/\\/g, "/");
|
|
2156
|
+
for (const pattern of EXCLUDE_PATTERNS) {
|
|
2157
|
+
if (normalized === pattern || normalized.startsWith(pattern + "/") || normalized.endsWith("/" + pattern) || normalized.includes("/" + pattern + "/")) {
|
|
2158
|
+
return true;
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
if (normalized.endsWith(".log")) return true;
|
|
2162
|
+
return false;
|
|
2163
|
+
}
|
|
2164
|
+
function isInsforgeCloudOssHost(ossHost) {
|
|
2165
|
+
try {
|
|
2166
|
+
return new URL(ossHost).hostname.endsWith(".insforge.app");
|
|
2167
|
+
} catch {
|
|
2168
|
+
return false;
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
function normalizeRelativePath(sourceDir, absolutePath) {
|
|
2172
|
+
return path3.relative(sourceDir, absolutePath).split(path3.sep).join("/").replace(/\\/g, "/");
|
|
2173
|
+
}
|
|
2174
|
+
async function hashFile(filePath) {
|
|
2175
|
+
const hash = createHash2("sha1");
|
|
2176
|
+
let size = 0;
|
|
2177
|
+
for await (const chunk of createReadStream(filePath)) {
|
|
2178
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
2179
|
+
size += buffer.length;
|
|
2180
|
+
hash.update(buffer);
|
|
2181
|
+
}
|
|
2182
|
+
return { sha: hash.digest("hex"), size };
|
|
1437
2183
|
}
|
|
1438
2184
|
async function collectDeploymentFiles(sourceDir) {
|
|
1439
2185
|
const files = [];
|
|
1440
2186
|
async function walk(currentDir) {
|
|
1441
|
-
const entries = await
|
|
2187
|
+
const entries = await fs3.readdir(currentDir, { withFileTypes: true });
|
|
1442
2188
|
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
1443
2189
|
for (const entry of entries) {
|
|
1444
|
-
const absolutePath =
|
|
2190
|
+
const absolutePath = path3.join(currentDir, entry.name);
|
|
1445
2191
|
const normalizedPath = normalizeRelativePath(sourceDir, absolutePath);
|
|
1446
2192
|
if (!normalizedPath || shouldExclude(normalizedPath)) {
|
|
1447
2193
|
continue;
|
|
@@ -1547,12 +2293,12 @@ async function startDirectDeployment(deploymentId, startBody) {
|
|
|
1547
2293
|
});
|
|
1548
2294
|
await response.json();
|
|
1549
2295
|
}
|
|
1550
|
-
async function pollDeployment(deploymentId,
|
|
1551
|
-
|
|
2296
|
+
async function pollDeployment(deploymentId, spinner8, syncBeforeRead) {
|
|
2297
|
+
spinner8?.message("Building and deploying...");
|
|
1552
2298
|
const startTime = Date.now();
|
|
1553
2299
|
let deployment = null;
|
|
1554
|
-
while (Date.now() - startTime <
|
|
1555
|
-
await new Promise((resolve5) => setTimeout(resolve5,
|
|
2300
|
+
while (Date.now() - startTime < POLL_TIMEOUT_MS3) {
|
|
2301
|
+
await new Promise((resolve5) => setTimeout(resolve5, POLL_INTERVAL_MS3));
|
|
1556
2302
|
try {
|
|
1557
2303
|
if (syncBeforeRead) {
|
|
1558
2304
|
await ossFetch(`/api/deployments/${deploymentId}/sync`, { method: "POST" });
|
|
@@ -1564,13 +2310,13 @@ async function pollDeployment(deploymentId, spinner7, syncBeforeRead) {
|
|
|
1564
2310
|
break;
|
|
1565
2311
|
}
|
|
1566
2312
|
if (status === "ERROR" || status === "CANCELED") {
|
|
1567
|
-
|
|
2313
|
+
spinner8?.stop("Deployment failed");
|
|
1568
2314
|
throw new CLIError(
|
|
1569
2315
|
getDeploymentError(deployment.metadata) ?? `Deployment failed with status: ${deployment.status}`
|
|
1570
2316
|
);
|
|
1571
2317
|
}
|
|
1572
2318
|
const elapsed = Math.round((Date.now() - startTime) / 1e3);
|
|
1573
|
-
|
|
2319
|
+
spinner8?.message(`Building and deploying... (${elapsed}s, status: ${deployment.status})`);
|
|
1574
2320
|
} catch (err) {
|
|
1575
2321
|
if (err instanceof CLIError) throw err;
|
|
1576
2322
|
}
|
|
@@ -1580,20 +2326,20 @@ async function pollDeployment(deploymentId, spinner7, syncBeforeRead) {
|
|
|
1580
2326
|
return { deploymentId, deployment, isReady, liveUrl };
|
|
1581
2327
|
}
|
|
1582
2328
|
async function deployProjectDirect(opts, config) {
|
|
1583
|
-
const { sourceDir, startBody = {}, spinner:
|
|
1584
|
-
|
|
2329
|
+
const { sourceDir, startBody = {}, spinner: spinner8 } = opts;
|
|
2330
|
+
spinner8?.start("Scanning source files...");
|
|
1585
2331
|
const localFiles = await collectDeploymentFiles(sourceDir);
|
|
1586
2332
|
if (localFiles.length === 0) {
|
|
1587
2333
|
throw new CLIError("No deployable files found in the source directory.");
|
|
1588
2334
|
}
|
|
1589
|
-
|
|
2335
|
+
spinner8?.message("Creating deployment...");
|
|
1590
2336
|
const createResult = await createDirectDeploymentSession(
|
|
1591
2337
|
config,
|
|
1592
2338
|
localFiles.map(({ path: relativePath, sha, size }) => ({ path: relativePath, sha, size }))
|
|
1593
2339
|
);
|
|
1594
2340
|
const localFileByPath = new Map(localFiles.map((file) => [file.path, file]));
|
|
1595
2341
|
const pendingFiles = createResult.files.filter((file) => !file.uploadedAt);
|
|
1596
|
-
|
|
2342
|
+
spinner8?.message(`Uploading ${pendingFiles.length} file${pendingFiles.length === 1 ? "" : "s"}...`);
|
|
1597
2343
|
await runWithConcurrency(pendingFiles, DIRECT_UPLOAD_CONCURRENCY, async (manifestFile) => {
|
|
1598
2344
|
const localFile = localFileByPath.get(manifestFile.path);
|
|
1599
2345
|
if (!localFile) {
|
|
@@ -1604,18 +2350,18 @@ async function deployProjectDirect(opts, config) {
|
|
|
1604
2350
|
}
|
|
1605
2351
|
await uploadDirectDeploymentFile(createResult.id, manifestFile, localFile);
|
|
1606
2352
|
});
|
|
1607
|
-
|
|
2353
|
+
spinner8?.message("Starting deployment...");
|
|
1608
2354
|
await startDirectDeployment(createResult.id, startBody);
|
|
1609
|
-
return await pollDeployment(createResult.id,
|
|
2355
|
+
return await pollDeployment(createResult.id, spinner8, !isInsforgeCloudOssHost(config.oss_host));
|
|
1610
2356
|
}
|
|
1611
2357
|
async function deployProjectLegacy(opts) {
|
|
1612
|
-
const { sourceDir, startBody = {}, spinner:
|
|
1613
|
-
|
|
2358
|
+
const { sourceDir, startBody = {}, spinner: spinner8 } = opts;
|
|
2359
|
+
spinner8?.message("Creating deployment...");
|
|
1614
2360
|
const createRes = await ossFetch("/api/deployments", { method: "POST" });
|
|
1615
2361
|
const { id: deploymentId, uploadUrl, uploadFields } = await createRes.json();
|
|
1616
|
-
|
|
2362
|
+
spinner8?.message("Compressing source files...");
|
|
1617
2363
|
const zipBuffer = await createZipBuffer(sourceDir);
|
|
1618
|
-
|
|
2364
|
+
spinner8?.message("Uploading...");
|
|
1619
2365
|
const formData = new FormData();
|
|
1620
2366
|
for (const [key, value] of Object.entries(uploadFields)) {
|
|
1621
2367
|
formData.append(key, value);
|
|
@@ -1626,13 +2372,13 @@ async function deployProjectLegacy(opts) {
|
|
|
1626
2372
|
const uploadErr = await uploadRes.text();
|
|
1627
2373
|
throw new CLIError(`Failed to upload: ${uploadErr}`);
|
|
1628
2374
|
}
|
|
1629
|
-
|
|
2375
|
+
spinner8?.message("Starting deployment...");
|
|
1630
2376
|
const startRes = await ossFetch(`/api/deployments/${deploymentId}/start`, {
|
|
1631
2377
|
method: "POST",
|
|
1632
2378
|
body: JSON.stringify(startBody)
|
|
1633
2379
|
});
|
|
1634
2380
|
await startRes.json();
|
|
1635
|
-
return await pollDeployment(deploymentId,
|
|
2381
|
+
return await pollDeployment(deploymentId, spinner8, false);
|
|
1636
2382
|
}
|
|
1637
2383
|
async function deployProject(opts) {
|
|
1638
2384
|
const config = getProjectConfig();
|
|
@@ -1656,18 +2402,18 @@ function registerDeploymentsDeployCommand(deploymentsCmd2) {
|
|
|
1656
2402
|
await requireAuth();
|
|
1657
2403
|
const config = getProjectConfig();
|
|
1658
2404
|
if (!config) throw new ProjectNotLinkedError();
|
|
1659
|
-
const sourceDir =
|
|
1660
|
-
const stats = await
|
|
2405
|
+
const sourceDir = path3.resolve(directory ?? ".");
|
|
2406
|
+
const stats = await fs3.stat(sourceDir).catch(() => null);
|
|
1661
2407
|
if (!stats?.isDirectory()) {
|
|
1662
2408
|
throw new CLIError(`"${sourceDir}" is not a valid directory.`);
|
|
1663
2409
|
}
|
|
1664
|
-
const dirName =
|
|
2410
|
+
const dirName = path3.basename(sourceDir);
|
|
1665
2411
|
if (EXCLUDE_PATTERNS.includes(dirName)) {
|
|
1666
2412
|
throw new CLIError(
|
|
1667
2413
|
`"${dirName}" is an excluded directory and cannot be used as a deploy source. Please specify your project root or output directory instead.`
|
|
1668
2414
|
);
|
|
1669
2415
|
}
|
|
1670
|
-
const
|
|
2416
|
+
const spinner8 = !json ? clack10.spinner() : null;
|
|
1671
2417
|
const startBody = {};
|
|
1672
2418
|
if (opts.env) {
|
|
1673
2419
|
try {
|
|
@@ -1693,19 +2439,19 @@ function registerDeploymentsDeployCommand(deploymentsCmd2) {
|
|
|
1693
2439
|
throw new CLIError("Invalid --meta JSON.");
|
|
1694
2440
|
}
|
|
1695
2441
|
}
|
|
1696
|
-
const result = await deployProject({ sourceDir, startBody, spinner:
|
|
2442
|
+
const result = await deployProject({ sourceDir, startBody, spinner: spinner8 });
|
|
1697
2443
|
if (result.isReady) {
|
|
1698
|
-
|
|
2444
|
+
spinner8?.stop("Deployment complete");
|
|
1699
2445
|
if (json) {
|
|
1700
2446
|
outputJson(result.deployment);
|
|
1701
2447
|
} else {
|
|
1702
2448
|
if (result.liveUrl) {
|
|
1703
|
-
|
|
2449
|
+
clack10.log.success(`Live at: ${result.liveUrl}`);
|
|
1704
2450
|
}
|
|
1705
|
-
|
|
2451
|
+
clack10.log.info(`Deployment ID: ${result.deploymentId}`);
|
|
1706
2452
|
}
|
|
1707
2453
|
} else {
|
|
1708
|
-
|
|
2454
|
+
spinner8?.stop("Deployment is still building");
|
|
1709
2455
|
if (json) {
|
|
1710
2456
|
outputJson({
|
|
1711
2457
|
id: result.deploymentId,
|
|
@@ -1713,9 +2459,9 @@ function registerDeploymentsDeployCommand(deploymentsCmd2) {
|
|
|
1713
2459
|
timedOut: true
|
|
1714
2460
|
});
|
|
1715
2461
|
} else {
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
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}`);
|
|
1719
2465
|
}
|
|
1720
2466
|
}
|
|
1721
2467
|
await reportCliUsage("cli.deployments.deploy", true);
|
|
@@ -1727,7 +2473,10 @@ function registerDeploymentsDeployCommand(deploymentsCmd2) {
|
|
|
1727
2473
|
}
|
|
1728
2474
|
|
|
1729
2475
|
// src/commands/create.ts
|
|
1730
|
-
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._/-]+$/;
|
|
1731
2480
|
function buildOssHost(appkey, region) {
|
|
1732
2481
|
return `https://${appkey}.${region}.insforge.app`;
|
|
1733
2482
|
}
|
|
@@ -1821,31 +2570,31 @@ async function animateBanner() {
|
|
|
1821
2570
|
process.stderr.write("\n");
|
|
1822
2571
|
}
|
|
1823
2572
|
function getDefaultProjectName() {
|
|
1824
|
-
const dirName =
|
|
2573
|
+
const dirName = path4.basename(process.cwd());
|
|
1825
2574
|
const sanitized = dirName.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
1826
2575
|
return sanitized.length >= 2 ? sanitized : "";
|
|
1827
2576
|
}
|
|
1828
2577
|
async function copyDir(src, dest) {
|
|
1829
|
-
const entries = await
|
|
2578
|
+
const entries = await fs4.readdir(src, { withFileTypes: true });
|
|
1830
2579
|
for (const entry of entries) {
|
|
1831
|
-
const srcPath =
|
|
1832
|
-
const destPath =
|
|
2580
|
+
const srcPath = path4.join(src, entry.name);
|
|
2581
|
+
const destPath = path4.join(dest, entry.name);
|
|
1833
2582
|
if (entry.isDirectory()) {
|
|
1834
|
-
await
|
|
2583
|
+
await fs4.mkdir(destPath, { recursive: true });
|
|
1835
2584
|
await copyDir(srcPath, destPath);
|
|
1836
2585
|
} else {
|
|
1837
|
-
await
|
|
2586
|
+
await fs4.copyFile(srcPath, destPath);
|
|
1838
2587
|
}
|
|
1839
2588
|
}
|
|
1840
2589
|
}
|
|
1841
2590
|
function registerCreateCommand(program2) {
|
|
1842
|
-
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) => {
|
|
1843
2592
|
const { json, apiUrl } = getRootOpts(cmd);
|
|
1844
2593
|
try {
|
|
1845
2594
|
await requireAuth(apiUrl, false);
|
|
1846
2595
|
if (!json) {
|
|
1847
2596
|
await animateBanner();
|
|
1848
|
-
|
|
2597
|
+
clack11.intro("Let's build something great");
|
|
1849
2598
|
}
|
|
1850
2599
|
let orgId = opts.orgId;
|
|
1851
2600
|
if (!orgId) {
|
|
@@ -1855,7 +2604,7 @@ function registerCreateCommand(program2) {
|
|
|
1855
2604
|
}
|
|
1856
2605
|
if (orgs.length === 1) {
|
|
1857
2606
|
orgId = orgs[0].id;
|
|
1858
|
-
if (!json)
|
|
2607
|
+
if (!json) clack11.log.info(`Using organization: ${orgs[0].name}`);
|
|
1859
2608
|
} else {
|
|
1860
2609
|
if (json) {
|
|
1861
2610
|
throw new CLIError("Multiple organizations found. Specify --org-id.");
|
|
@@ -1886,12 +2635,15 @@ function registerCreateCommand(program2) {
|
|
|
1886
2635
|
if (isCancel2(name)) process.exit(0);
|
|
1887
2636
|
projectName = name;
|
|
1888
2637
|
}
|
|
1889
|
-
projectName =
|
|
2638
|
+
projectName = path4.basename(projectName).replace(/[^a-zA-Z0-9._-]/g, "-").replace(/\.+/g, ".");
|
|
1890
2639
|
if (projectName.length < 2 || projectName === "." || projectName === "..") {
|
|
1891
2640
|
throw new CLIError("Project name must be at least 2 safe characters (letters, numbers, hyphens).");
|
|
1892
2641
|
}
|
|
1893
2642
|
const validTemplates = ["react", "nextjs", "chatbot", "crm", "e-commerce", "todo", "empty"];
|
|
1894
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
|
+
}
|
|
1895
2647
|
if (template && !validTemplates.includes(template)) {
|
|
1896
2648
|
throw new CLIError(`Invalid template "${template}". Valid options: ${validTemplates.join(", ")}`);
|
|
1897
2649
|
}
|
|
@@ -1945,27 +2697,27 @@ function registerCreateCommand(program2) {
|
|
|
1945
2697
|
initialValue: projectName,
|
|
1946
2698
|
validate: (v) => {
|
|
1947
2699
|
if (v.length < 1) return "Directory name is required";
|
|
1948
|
-
const normalized =
|
|
2700
|
+
const normalized = path4.basename(v).replace(/[^a-zA-Z0-9._-]/g, "-");
|
|
1949
2701
|
if (!normalized || normalized === "." || normalized === "..") return "Invalid directory name";
|
|
1950
2702
|
return void 0;
|
|
1951
2703
|
}
|
|
1952
2704
|
});
|
|
1953
2705
|
if (isCancel2(inputDir)) process.exit(0);
|
|
1954
|
-
dirName =
|
|
2706
|
+
dirName = path4.basename(inputDir).replace(/[^a-zA-Z0-9._-]/g, "-");
|
|
1955
2707
|
}
|
|
1956
2708
|
if (!dirName || dirName === "." || dirName === "..") {
|
|
1957
2709
|
throw new CLIError("Invalid directory name.");
|
|
1958
2710
|
}
|
|
1959
|
-
projectDir =
|
|
1960
|
-
const dirExists = await
|
|
2711
|
+
projectDir = path4.resolve(originalCwd, dirName);
|
|
2712
|
+
const dirExists = await fs4.stat(projectDir).catch(() => null);
|
|
1961
2713
|
if (dirExists) {
|
|
1962
2714
|
throw new CLIError(`Directory "${dirName}" already exists.`);
|
|
1963
2715
|
}
|
|
1964
|
-
await
|
|
2716
|
+
await fs4.mkdir(projectDir);
|
|
1965
2717
|
process.chdir(projectDir);
|
|
1966
2718
|
}
|
|
1967
2719
|
let projectLinked = false;
|
|
1968
|
-
const s = !json ?
|
|
2720
|
+
const s = !json ? clack11.spinner() : null;
|
|
1969
2721
|
try {
|
|
1970
2722
|
s?.start("Creating project...");
|
|
1971
2723
|
const project = await createProject(orgId, projectName, opts.region, apiUrl);
|
|
@@ -1993,37 +2745,49 @@ function registerCreateCommand(program2) {
|
|
|
1993
2745
|
try {
|
|
1994
2746
|
const anonKey = await getAnonKey();
|
|
1995
2747
|
if (!anonKey) {
|
|
1996
|
-
if (!json)
|
|
2748
|
+
if (!json) clack11.log.warn("Could not retrieve anon key. You can add it to .env.local manually.");
|
|
1997
2749
|
} else {
|
|
1998
|
-
const envPath =
|
|
2750
|
+
const envPath = path4.join(process.cwd(), ".env.local");
|
|
1999
2751
|
const envContent = [
|
|
2000
2752
|
"# InsForge",
|
|
2001
2753
|
`NEXT_PUBLIC_INSFORGE_URL=${projectConfig.oss_host}`,
|
|
2002
2754
|
`NEXT_PUBLIC_INSFORGE_ANON_KEY=${anonKey}`,
|
|
2003
2755
|
""
|
|
2004
2756
|
].join("\n");
|
|
2005
|
-
await
|
|
2757
|
+
await fs4.writeFile(envPath, envContent, { flag: "wx" });
|
|
2006
2758
|
if (!json) {
|
|
2007
|
-
|
|
2759
|
+
clack11.log.success("Created .env.local with your InsForge credentials");
|
|
2008
2760
|
}
|
|
2009
2761
|
}
|
|
2010
2762
|
} catch (err) {
|
|
2011
2763
|
const error = err;
|
|
2012
2764
|
if (!json) {
|
|
2013
2765
|
if (error.code === "EEXIST") {
|
|
2014
|
-
|
|
2766
|
+
clack11.log.warn(".env.local already exists; skipping InsForge key seeding.");
|
|
2015
2767
|
} else {
|
|
2016
|
-
|
|
2768
|
+
clack11.log.warn(`Failed to create .env.local: ${error.message}`);
|
|
2017
2769
|
}
|
|
2018
2770
|
}
|
|
2019
2771
|
}
|
|
2020
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
|
+
}
|
|
2021
2785
|
await installSkills(json);
|
|
2022
2786
|
trackCommand("create", orgId);
|
|
2023
2787
|
await reportCliUsage("cli.create", true, 6);
|
|
2024
|
-
const templateDownloaded = hasTemplate ? await
|
|
2788
|
+
const templateDownloaded = hasTemplate ? await fs4.stat(path4.join(process.cwd(), "package.json")).catch(() => null) : null;
|
|
2025
2789
|
if (templateDownloaded) {
|
|
2026
|
-
const installSpinner = !json ?
|
|
2790
|
+
const installSpinner = !json ? clack11.spinner() : null;
|
|
2027
2791
|
installSpinner?.start("Installing dependencies...");
|
|
2028
2792
|
try {
|
|
2029
2793
|
await execAsync2("npm install", { cwd: process.cwd(), maxBuffer: 10 * 1024 * 1024 });
|
|
@@ -2031,8 +2795,8 @@ function registerCreateCommand(program2) {
|
|
|
2031
2795
|
} catch (err) {
|
|
2032
2796
|
installSpinner?.stop("Failed to install dependencies");
|
|
2033
2797
|
if (!json) {
|
|
2034
|
-
|
|
2035
|
-
|
|
2798
|
+
clack11.log.warn(`npm install failed: ${err.message}`);
|
|
2799
|
+
clack11.log.info("Run `npm install` manually to install dependencies.");
|
|
2036
2800
|
}
|
|
2037
2801
|
}
|
|
2038
2802
|
}
|
|
@@ -2048,7 +2812,7 @@ function registerCreateCommand(program2) {
|
|
|
2048
2812
|
if (envVars.length > 0) {
|
|
2049
2813
|
startBody.envVars = envVars;
|
|
2050
2814
|
}
|
|
2051
|
-
const deploySpinner =
|
|
2815
|
+
const deploySpinner = clack11.spinner();
|
|
2052
2816
|
const result = await deployProject({
|
|
2053
2817
|
sourceDir: process.cwd(),
|
|
2054
2818
|
startBody,
|
|
@@ -2059,12 +2823,12 @@ function registerCreateCommand(program2) {
|
|
|
2059
2823
|
liveUrl = result.liveUrl;
|
|
2060
2824
|
} else {
|
|
2061
2825
|
deploySpinner.stop("Deployment is still building");
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
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}`);
|
|
2065
2829
|
}
|
|
2066
2830
|
} catch (err) {
|
|
2067
|
-
|
|
2831
|
+
clack11.log.warn(`Deploy failed: ${err.message}`);
|
|
2068
2832
|
}
|
|
2069
2833
|
}
|
|
2070
2834
|
}
|
|
@@ -2081,38 +2845,38 @@ function registerCreateCommand(program2) {
|
|
|
2081
2845
|
}
|
|
2082
2846
|
});
|
|
2083
2847
|
} else {
|
|
2084
|
-
|
|
2848
|
+
clack11.log.step(`Dashboard: ${dashboardUrl}`);
|
|
2085
2849
|
if (liveUrl) {
|
|
2086
|
-
|
|
2850
|
+
clack11.log.success(`Live site: ${liveUrl}`);
|
|
2087
2851
|
}
|
|
2088
2852
|
if (templateDownloaded) {
|
|
2089
2853
|
const steps = [
|
|
2090
2854
|
`cd ${dirName}`,
|
|
2091
2855
|
"npm run dev"
|
|
2092
2856
|
];
|
|
2093
|
-
|
|
2094
|
-
|
|
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");
|
|
2095
2859
|
} else if (hasTemplate && !templateDownloaded) {
|
|
2096
|
-
|
|
2860
|
+
clack11.log.warn("Template download failed. You can retry or set up manually.");
|
|
2097
2861
|
} else {
|
|
2098
2862
|
const prompts = [
|
|
2099
2863
|
"Build a todo app with Google OAuth sign-in",
|
|
2100
2864
|
"Build an Instagram clone where users can upload photos, like, and comment",
|
|
2101
2865
|
"Build an AI chatbot with conversation history"
|
|
2102
2866
|
];
|
|
2103
|
-
|
|
2867
|
+
clack11.note(
|
|
2104
2868
|
`Open your coding agent (Claude Code, Codex, Cursor, etc.) and try:
|
|
2105
2869
|
|
|
2106
2870
|
${prompts.map((p) => `\u2022 "${p}"`).join("\n")}`,
|
|
2107
2871
|
"Start building"
|
|
2108
2872
|
);
|
|
2109
2873
|
}
|
|
2110
|
-
|
|
2874
|
+
clack11.outro("Done!");
|
|
2111
2875
|
}
|
|
2112
2876
|
} catch (err) {
|
|
2113
2877
|
if (!projectLinked && hasTemplate && projectDir !== originalCwd) {
|
|
2114
2878
|
process.chdir(originalCwd);
|
|
2115
|
-
await
|
|
2879
|
+
await fs4.rm(projectDir, { recursive: true, force: true }).catch(() => {
|
|
2116
2880
|
});
|
|
2117
2881
|
}
|
|
2118
2882
|
throw err;
|
|
@@ -2126,18 +2890,18 @@ ${prompts.map((p) => `\u2022 "${p}"`).join("\n")}`,
|
|
|
2126
2890
|
});
|
|
2127
2891
|
}
|
|
2128
2892
|
async function downloadTemplate(framework, projectConfig, projectName, json, _apiUrl) {
|
|
2129
|
-
const s = !json ?
|
|
2893
|
+
const s = !json ? clack11.spinner() : null;
|
|
2130
2894
|
s?.start("Downloading template...");
|
|
2131
2895
|
try {
|
|
2132
2896
|
const anonKey = await getAnonKey();
|
|
2133
2897
|
if (!anonKey) {
|
|
2134
2898
|
throw new Error("Failed to retrieve anon key from backend");
|
|
2135
2899
|
}
|
|
2136
|
-
const tempDir =
|
|
2900
|
+
const tempDir = tmpdir2();
|
|
2137
2901
|
const targetDir = projectName;
|
|
2138
|
-
const templatePath =
|
|
2902
|
+
const templatePath = path4.join(tempDir, targetDir);
|
|
2139
2903
|
try {
|
|
2140
|
-
await
|
|
2904
|
+
await fs4.rm(templatePath, { recursive: true, force: true });
|
|
2141
2905
|
} catch {
|
|
2142
2906
|
}
|
|
2143
2907
|
const frame = framework === "nextjs" ? "nextjs" : "react";
|
|
@@ -2151,41 +2915,53 @@ async function downloadTemplate(framework, projectConfig, projectName, json, _ap
|
|
|
2151
2915
|
s?.message("Copying template files...");
|
|
2152
2916
|
const cwd = process.cwd();
|
|
2153
2917
|
await copyDir(templatePath, cwd);
|
|
2154
|
-
await
|
|
2918
|
+
await fs4.rm(templatePath, { recursive: true, force: true }).catch(() => {
|
|
2155
2919
|
});
|
|
2156
2920
|
s?.stop("Template files downloaded");
|
|
2157
2921
|
} catch (err) {
|
|
2158
2922
|
s?.stop("Template download failed");
|
|
2159
2923
|
if (!json) {
|
|
2160
|
-
|
|
2161
|
-
|
|
2924
|
+
clack11.log.warn(`Failed to download template: ${err.message}`);
|
|
2925
|
+
clack11.log.info("You can manually set up the template later.");
|
|
2162
2926
|
}
|
|
2163
2927
|
}
|
|
2164
2928
|
}
|
|
2165
2929
|
async function downloadGitHubTemplate(templateName, projectConfig, json) {
|
|
2166
|
-
const s = !json ?
|
|
2930
|
+
const s = !json ? clack11.spinner() : null;
|
|
2167
2931
|
s?.start(`Downloading ${templateName} template...`);
|
|
2168
|
-
const tempDir =
|
|
2932
|
+
const tempDir = path4.join(tmpdir2(), `insforge-template-${Date.now()}`);
|
|
2169
2933
|
try {
|
|
2170
|
-
await
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
const
|
|
2176
|
-
|
|
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);
|
|
2177
2953
|
if (!stat5?.isDirectory()) {
|
|
2178
2954
|
throw new Error(`Template "${templateName}" not found in repository`);
|
|
2179
2955
|
}
|
|
2180
2956
|
s?.message("Copying template files...");
|
|
2181
2957
|
const cwd = process.cwd();
|
|
2182
2958
|
await copyDir(templateDir, cwd);
|
|
2183
|
-
const envExamplePath =
|
|
2184
|
-
const envExampleExists = await
|
|
2959
|
+
const envExamplePath = path4.join(cwd, ".env.example");
|
|
2960
|
+
const envExampleExists = await fs4.stat(envExamplePath).catch(() => null);
|
|
2185
2961
|
if (envExampleExists) {
|
|
2186
2962
|
const anonKey = await getAnonKey();
|
|
2187
|
-
const envExample = await
|
|
2188
|
-
const
|
|
2963
|
+
const envExample = await fs4.readFile(envExamplePath, "utf-8");
|
|
2964
|
+
const envFinal = envExample.replace(
|
|
2189
2965
|
/^([A-Z][A-Z0-9_]*=)(.*)$/gm,
|
|
2190
2966
|
(_, prefix, _value) => {
|
|
2191
2967
|
const key = prefix.slice(0, -1);
|
|
@@ -2195,32 +2971,32 @@ async function downloadGitHubTemplate(templateName, projectConfig, json) {
|
|
|
2195
2971
|
return `${prefix}${_value}`;
|
|
2196
2972
|
}
|
|
2197
2973
|
);
|
|
2198
|
-
const envLocalPath =
|
|
2974
|
+
const envLocalPath = path4.join(cwd, ".env.local");
|
|
2199
2975
|
try {
|
|
2200
|
-
await
|
|
2976
|
+
await fs4.writeFile(envLocalPath, envFinal, { flag: "wx" });
|
|
2201
2977
|
} catch (e) {
|
|
2202
2978
|
if (e.code === "EEXIST") {
|
|
2203
|
-
if (!json)
|
|
2979
|
+
if (!json) clack11.log.warn(".env.local already exists; skipping env seeding.");
|
|
2204
2980
|
} else {
|
|
2205
2981
|
throw e;
|
|
2206
2982
|
}
|
|
2207
2983
|
}
|
|
2208
2984
|
}
|
|
2209
2985
|
s?.stop(`${templateName} template downloaded`);
|
|
2210
|
-
const migrationPath =
|
|
2211
|
-
const migrationExists = await
|
|
2986
|
+
const migrationPath = path4.join(cwd, "migrations", "db_init.sql");
|
|
2987
|
+
const migrationExists = await fs4.stat(migrationPath).catch(() => null);
|
|
2212
2988
|
if (migrationExists) {
|
|
2213
|
-
const dbSpinner = !json ?
|
|
2989
|
+
const dbSpinner = !json ? clack11.spinner() : null;
|
|
2214
2990
|
dbSpinner?.start("Running database migrations...");
|
|
2215
2991
|
try {
|
|
2216
|
-
const sql = await
|
|
2992
|
+
const sql = await fs4.readFile(migrationPath, "utf-8");
|
|
2217
2993
|
await runRawSql(sql, true);
|
|
2218
2994
|
dbSpinner?.stop("Database migrations applied");
|
|
2219
2995
|
} catch (err) {
|
|
2220
2996
|
dbSpinner?.stop("Database migration failed");
|
|
2221
2997
|
if (!json) {
|
|
2222
|
-
|
|
2223
|
-
|
|
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)"');
|
|
2224
3000
|
} else {
|
|
2225
3001
|
throw err;
|
|
2226
3002
|
}
|
|
@@ -2228,25 +3004,31 @@ async function downloadGitHubTemplate(templateName, projectConfig, json) {
|
|
|
2228
3004
|
}
|
|
2229
3005
|
} catch (err) {
|
|
2230
3006
|
s?.stop(`${templateName} template download failed`);
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
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");
|
|
2234
3013
|
}
|
|
2235
3014
|
} finally {
|
|
2236
|
-
await
|
|
3015
|
+
await fs4.rm(tempDir, { recursive: true, force: true }).catch(() => {
|
|
2237
3016
|
});
|
|
2238
3017
|
}
|
|
2239
3018
|
}
|
|
2240
3019
|
|
|
2241
3020
|
// src/commands/projects/link.ts
|
|
2242
|
-
var execAsync3 =
|
|
3021
|
+
var execAsync3 = promisify4(exec3);
|
|
2243
3022
|
function buildOssHost2(appkey, region) {
|
|
2244
3023
|
return `https://${appkey}.${region}.insforge.app`;
|
|
2245
3024
|
}
|
|
2246
3025
|
function registerProjectLinkCommand(program2) {
|
|
2247
|
-
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) => {
|
|
2248
3027
|
const { json, apiUrl } = getRootOpts(cmd);
|
|
2249
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
|
+
}
|
|
2250
3032
|
try {
|
|
2251
3033
|
if (opts.template && !validTemplates.includes(opts.template)) {
|
|
2252
3034
|
throw new CLIError(`Invalid template "${opts.template}". Valid options: ${validTemplates.join(", ")}`);
|
|
@@ -2281,23 +3063,23 @@ function registerProjectLinkCommand(program2) {
|
|
|
2281
3063
|
initialValue: defaultDir,
|
|
2282
3064
|
validate: (v) => {
|
|
2283
3065
|
if (v.length < 1) return "Directory name is required";
|
|
2284
|
-
const normalized =
|
|
3066
|
+
const normalized = path5.basename(v).replace(/[^a-zA-Z0-9._-]/g, "-");
|
|
2285
3067
|
if (!normalized || normalized === "." || normalized === "..") return "Invalid directory name";
|
|
2286
3068
|
return void 0;
|
|
2287
3069
|
}
|
|
2288
3070
|
});
|
|
2289
3071
|
if (isCancel2(inputDir)) process.exit(0);
|
|
2290
|
-
dirName =
|
|
3072
|
+
dirName = path5.basename(inputDir).replace(/[^a-zA-Z0-9._-]/g, "-");
|
|
2291
3073
|
}
|
|
2292
3074
|
if (!dirName || dirName === "." || dirName === "..") {
|
|
2293
3075
|
throw new CLIError("Invalid directory name.");
|
|
2294
3076
|
}
|
|
2295
|
-
const templateDir =
|
|
2296
|
-
const dirExists = await
|
|
3077
|
+
const templateDir = path5.resolve(process.cwd(), dirName);
|
|
3078
|
+
const dirExists = await fs5.stat(templateDir).catch(() => null);
|
|
2297
3079
|
if (dirExists) {
|
|
2298
3080
|
throw new CLIError(`Directory "${dirName}" already exists.`);
|
|
2299
3081
|
}
|
|
2300
|
-
await
|
|
3082
|
+
await fs5.mkdir(templateDir);
|
|
2301
3083
|
process.chdir(templateDir);
|
|
2302
3084
|
saveProjectConfig(projectConfig2);
|
|
2303
3085
|
if (json) {
|
|
@@ -2312,17 +3094,27 @@ function registerProjectLinkCommand(program2) {
|
|
|
2312
3094
|
}
|
|
2313
3095
|
captureEvent(FAKE_ORG_ID, "template_selected", { template: template2, source: "link_direct" });
|
|
2314
3096
|
await downloadGitHubTemplate(template2, projectConfig2, json);
|
|
2315
|
-
const templateDownloaded = await
|
|
2316
|
-
if (
|
|
2317
|
-
|
|
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
|
+
}
|
|
3108
|
+
if (templateDownloaded && !json) {
|
|
3109
|
+
const installSpinner = clack12.spinner();
|
|
2318
3110
|
installSpinner.start("Installing dependencies...");
|
|
2319
3111
|
try {
|
|
2320
3112
|
await execAsync3("npm install", { cwd: process.cwd(), maxBuffer: 10 * 1024 * 1024 });
|
|
2321
3113
|
installSpinner.stop("Dependencies installed");
|
|
2322
3114
|
} catch (err) {
|
|
2323
3115
|
installSpinner.stop("Failed to install dependencies");
|
|
2324
|
-
|
|
2325
|
-
|
|
3116
|
+
clack12.log.warn(`npm install failed: ${err.message}`);
|
|
3117
|
+
clack12.log.info("Run `npm install` manually to install dependencies.");
|
|
2326
3118
|
}
|
|
2327
3119
|
}
|
|
2328
3120
|
await installSkills(json);
|
|
@@ -2342,9 +3134,9 @@ function registerProjectLinkCommand(program2) {
|
|
|
2342
3134
|
`${pc2.bold("1.")} ${runCommand}`,
|
|
2343
3135
|
`${pc2.bold("2.")} Open ${pc2.cyan("Claude Code")} or ${pc2.cyan("Cursor")} and prompt your agent to add more features`
|
|
2344
3136
|
];
|
|
2345
|
-
|
|
3137
|
+
clack12.note(steps.join("\n"), "What's next");
|
|
2346
3138
|
} else {
|
|
2347
|
-
|
|
3139
|
+
clack12.log.warn("Template download failed. You can retry or set up manually.");
|
|
2348
3140
|
}
|
|
2349
3141
|
}
|
|
2350
3142
|
return;
|
|
@@ -2355,6 +3147,31 @@ function registerProjectLinkCommand(program2) {
|
|
|
2355
3147
|
} else {
|
|
2356
3148
|
outputSuccess(`Linked to direct project at ${projectConfig2.oss_host}`);
|
|
2357
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
|
+
}
|
|
2358
3175
|
trackCommand("link", "oss-org", { direct: true });
|
|
2359
3176
|
await installSkills(json);
|
|
2360
3177
|
await reportCliUsage("cli.link_direct", true, 6, projectConfig2);
|
|
@@ -2382,7 +3199,7 @@ function registerProjectLinkCommand(program2) {
|
|
|
2382
3199
|
}
|
|
2383
3200
|
if (orgs.length === 1) {
|
|
2384
3201
|
orgId = orgs[0].id;
|
|
2385
|
-
if (!json)
|
|
3202
|
+
if (!json) clack12.log.info(`Using organization: ${orgs[0].name}`);
|
|
2386
3203
|
} else {
|
|
2387
3204
|
if (json) {
|
|
2388
3205
|
throw new CLIError("Multiple organizations found. Specify --org-id.");
|
|
@@ -2468,68 +3285,91 @@ function registerProjectLinkCommand(program2) {
|
|
|
2468
3285
|
initialValue: project.name,
|
|
2469
3286
|
validate: (v) => {
|
|
2470
3287
|
if (v.length < 1) return "Directory name is required";
|
|
2471
|
-
const normalized =
|
|
3288
|
+
const normalized = path5.basename(v).replace(/[^a-zA-Z0-9._-]/g, "-");
|
|
2472
3289
|
if (!normalized || normalized === "." || normalized === "..") return "Invalid directory name";
|
|
2473
3290
|
return void 0;
|
|
2474
3291
|
}
|
|
2475
3292
|
});
|
|
2476
3293
|
if (isCancel2(inputDir)) process.exit(0);
|
|
2477
|
-
dirName =
|
|
3294
|
+
dirName = path5.basename(inputDir).replace(/[^a-zA-Z0-9._-]/g, "-");
|
|
2478
3295
|
}
|
|
2479
3296
|
if (!dirName || dirName === "." || dirName === "..") {
|
|
2480
3297
|
throw new CLIError("Invalid directory name.");
|
|
2481
3298
|
}
|
|
2482
|
-
const templateDir =
|
|
2483
|
-
const dirExists = await
|
|
3299
|
+
const templateDir = path5.resolve(process.cwd(), dirName);
|
|
3300
|
+
const dirExists = await fs5.stat(templateDir).catch(() => null);
|
|
2484
3301
|
if (dirExists) {
|
|
2485
3302
|
throw new CLIError(`Directory "${dirName}" already exists.`);
|
|
2486
3303
|
}
|
|
2487
|
-
await
|
|
3304
|
+
await fs5.mkdir(templateDir);
|
|
2488
3305
|
process.chdir(templateDir);
|
|
2489
3306
|
saveProjectConfig(projectConfig);
|
|
2490
3307
|
captureEvent(orgId ?? project.organization_id, "template_selected", { template, source: "link" });
|
|
2491
3308
|
await downloadGitHubTemplate(template, projectConfig, json);
|
|
2492
|
-
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
|
+
}
|
|
2493
3320
|
if (templateDownloaded && !json) {
|
|
2494
|
-
const installSpinner =
|
|
3321
|
+
const installSpinner = clack12.spinner();
|
|
2495
3322
|
installSpinner.start("Installing dependencies...");
|
|
2496
3323
|
try {
|
|
2497
3324
|
await execAsync3("npm install", { cwd: process.cwd(), maxBuffer: 10 * 1024 * 1024 });
|
|
2498
3325
|
installSpinner.stop("Dependencies installed");
|
|
2499
3326
|
} catch (err) {
|
|
2500
3327
|
installSpinner.stop("Failed to install dependencies");
|
|
2501
|
-
|
|
2502
|
-
|
|
3328
|
+
clack12.log.warn(`npm install failed: ${err.message}`);
|
|
3329
|
+
clack12.log.info("Run `npm install` manually to install dependencies.");
|
|
2503
3330
|
}
|
|
2504
3331
|
}
|
|
2505
3332
|
await installSkills(json);
|
|
2506
3333
|
await reportCliUsage("cli.link", true, 6, projectConfig);
|
|
2507
3334
|
if (!json) {
|
|
2508
3335
|
const dashboardUrl = `${getFrontendUrl()}/dashboard/project/${project.id}`;
|
|
2509
|
-
|
|
3336
|
+
clack12.log.step(`Dashboard: ${pc2.underline(dashboardUrl)}`);
|
|
2510
3337
|
if (templateDownloaded) {
|
|
2511
3338
|
const runCommand = `${pc2.cyan("cd")} ${pc2.green(dirName)} ${pc2.dim("&&")} ${pc2.cyan("npm run dev")}`;
|
|
2512
3339
|
const steps = [
|
|
2513
3340
|
`${pc2.bold("1.")} ${runCommand}`,
|
|
2514
3341
|
`${pc2.bold("2.")} Open ${pc2.cyan("Claude Code")} or ${pc2.cyan("Cursor")} and prompt your agent to add more features`
|
|
2515
3342
|
];
|
|
2516
|
-
|
|
3343
|
+
clack12.note(steps.join("\n"), "What's next");
|
|
2517
3344
|
} else {
|
|
2518
|
-
|
|
3345
|
+
clack12.log.warn("Template download failed. You can retry or set up manually.");
|
|
2519
3346
|
}
|
|
2520
3347
|
}
|
|
2521
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
|
+
}
|
|
2522
3362
|
await installSkills(json);
|
|
2523
3363
|
await reportCliUsage("cli.link", true, 6, projectConfig);
|
|
2524
3364
|
if (!json) {
|
|
2525
3365
|
const dashboardUrl = `${getFrontendUrl()}/dashboard/project/${project.id}`;
|
|
2526
|
-
|
|
3366
|
+
clack12.log.step(`Dashboard: ${dashboardUrl}`);
|
|
2527
3367
|
const prompts = [
|
|
2528
3368
|
"Build a todo app with Google OAuth sign-in",
|
|
2529
3369
|
"Build an Instagram clone where users can upload photos, like, and comment",
|
|
2530
3370
|
"Build an AI chatbot with conversation history and deploy it to a live URL"
|
|
2531
3371
|
];
|
|
2532
|
-
|
|
3372
|
+
clack12.note(
|
|
2533
3373
|
`Open your coding agent (Claude Code, Codex, Cursor, etc.) and try:
|
|
2534
3374
|
|
|
2535
3375
|
${prompts.map((p) => `\u2022 "${p}"`).join("\n")}`,
|
|
@@ -2769,7 +3609,7 @@ function registerDbRpcCommand(dbCmd2) {
|
|
|
2769
3609
|
}
|
|
2770
3610
|
|
|
2771
3611
|
// src/commands/db/export.ts
|
|
2772
|
-
import { writeFileSync as
|
|
3612
|
+
import { writeFileSync as writeFileSync3 } from "fs";
|
|
2773
3613
|
function registerDbExportCommand(dbCmd2) {
|
|
2774
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) => {
|
|
2775
3615
|
const { json } = getRootOpts(cmd);
|
|
@@ -2809,7 +3649,7 @@ function registerDbExportCommand(dbCmd2) {
|
|
|
2809
3649
|
return;
|
|
2810
3650
|
}
|
|
2811
3651
|
if (opts.output) {
|
|
2812
|
-
|
|
3652
|
+
writeFileSync3(opts.output, content);
|
|
2813
3653
|
const tableCount = meta?.tables?.length;
|
|
2814
3654
|
const suffix = tableCount ? ` (${tableCount} tables, format: ${meta?.format ?? opts.format})` : "";
|
|
2815
3655
|
outputSuccess(`Exported to ${opts.output}${suffix}`);
|
|
@@ -2824,7 +3664,7 @@ function registerDbExportCommand(dbCmd2) {
|
|
|
2824
3664
|
|
|
2825
3665
|
// src/commands/db/import.ts
|
|
2826
3666
|
import { readFileSync as readFileSync3 } from "fs";
|
|
2827
|
-
import { basename as
|
|
3667
|
+
import { basename as basename5 } from "path";
|
|
2828
3668
|
function registerDbImportCommand(dbCmd2) {
|
|
2829
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) => {
|
|
2830
3670
|
const { json } = getRootOpts(cmd);
|
|
@@ -2833,7 +3673,7 @@ function registerDbImportCommand(dbCmd2) {
|
|
|
2833
3673
|
const config = getProjectConfig();
|
|
2834
3674
|
if (!config) throw new ProjectNotLinkedError();
|
|
2835
3675
|
const fileContent = readFileSync3(file);
|
|
2836
|
-
const fileName =
|
|
3676
|
+
const fileName = basename5(file);
|
|
2837
3677
|
const formData = new FormData();
|
|
2838
3678
|
formData.append("file", new Blob([fileContent]), fileName);
|
|
2839
3679
|
if (opts.truncate) {
|
|
@@ -2863,12 +3703,12 @@ function registerDbImportCommand(dbCmd2) {
|
|
|
2863
3703
|
}
|
|
2864
3704
|
|
|
2865
3705
|
// src/commands/db/migrations.ts
|
|
2866
|
-
import { existsSync as
|
|
2867
|
-
import { join as
|
|
3706
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
3707
|
+
import { join as join9 } from "path";
|
|
2868
3708
|
|
|
2869
3709
|
// src/lib/migrations.ts
|
|
2870
|
-
import { existsSync as
|
|
2871
|
-
import { join as
|
|
3710
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readdirSync } from "fs";
|
|
3711
|
+
import { join as join8 } from "path";
|
|
2872
3712
|
var MIGRATION_VERSION_REGEX = /^\d{1,64}$/u;
|
|
2873
3713
|
var MIGRATION_FILENAME_REGEX = /^(\d{1,64})_([a-z0-9-]+)\.sql$/u;
|
|
2874
3714
|
function assertValidMigrationVersion(version) {
|
|
@@ -2932,18 +3772,18 @@ function incrementMigrationVersion(version) {
|
|
|
2932
3772
|
return formatMigrationVersion(new Date(nextTimestamp));
|
|
2933
3773
|
}
|
|
2934
3774
|
function getMigrationsDir(cwd = process.cwd()) {
|
|
2935
|
-
return
|
|
3775
|
+
return join8(cwd, "migrations");
|
|
2936
3776
|
}
|
|
2937
3777
|
function ensureMigrationsDir(cwd = process.cwd()) {
|
|
2938
3778
|
const migrationsDir = getMigrationsDir(cwd);
|
|
2939
|
-
if (!
|
|
3779
|
+
if (!existsSync4(migrationsDir)) {
|
|
2940
3780
|
mkdirSync2(migrationsDir, { recursive: true });
|
|
2941
3781
|
}
|
|
2942
3782
|
return migrationsDir;
|
|
2943
3783
|
}
|
|
2944
3784
|
function listLocalMigrationFilenames(cwd = process.cwd()) {
|
|
2945
3785
|
const migrationsDir = getMigrationsDir(cwd);
|
|
2946
|
-
if (!
|
|
3786
|
+
if (!existsSync4(migrationsDir)) {
|
|
2947
3787
|
return [];
|
|
2948
3788
|
}
|
|
2949
3789
|
return readdirSync(migrationsDir).sort((left, right) => left.localeCompare(right));
|
|
@@ -3131,12 +3971,12 @@ function registerDbMigrationsCommand(dbCmd2) {
|
|
|
3131
3971
|
migration.version,
|
|
3132
3972
|
migration.name
|
|
3133
3973
|
);
|
|
3134
|
-
const filePath =
|
|
3135
|
-
if (existingLocalVersions.has(migration.version) ||
|
|
3974
|
+
const filePath = join9(migrationsDir, filename);
|
|
3975
|
+
if (existingLocalVersions.has(migration.version) || existsSync5(filePath)) {
|
|
3136
3976
|
skippedFiles.push(filename);
|
|
3137
3977
|
continue;
|
|
3138
3978
|
}
|
|
3139
|
-
|
|
3979
|
+
writeFileSync4(filePath, formatMigrationSql(migration.statements));
|
|
3140
3980
|
createdFiles.push(filename);
|
|
3141
3981
|
existingLocalVersions.add(migration.version);
|
|
3142
3982
|
}
|
|
@@ -3174,9 +4014,9 @@ function registerDbMigrationsCommand(dbCmd2) {
|
|
|
3174
4014
|
);
|
|
3175
4015
|
const filename = buildMigrationFilename(nextVersion, migrationName);
|
|
3176
4016
|
const migrationsDir = ensureMigrationsDir();
|
|
3177
|
-
const filePath =
|
|
4017
|
+
const filePath = join9(migrationsDir, filename);
|
|
3178
4018
|
try {
|
|
3179
|
-
|
|
4019
|
+
writeFileSync4(filePath, "", { flag: "wx" });
|
|
3180
4020
|
} catch (error) {
|
|
3181
4021
|
if (error.code === "EEXIST") {
|
|
3182
4022
|
throw new CLIError(`Migration file already exists: ${filename}`);
|
|
@@ -3232,8 +4072,8 @@ function registerDbMigrationsCommand(dbCmd2) {
|
|
|
3232
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.`
|
|
3233
4073
|
);
|
|
3234
4074
|
}
|
|
3235
|
-
const filePath =
|
|
3236
|
-
if (!
|
|
4075
|
+
const filePath = join9(getMigrationsDir(), targetMigration.filename);
|
|
4076
|
+
if (!existsSync5(filePath)) {
|
|
3237
4077
|
throw new CLIError(`Local migration file not found: ${targetMigration.filename}`);
|
|
3238
4078
|
}
|
|
3239
4079
|
const sql = readFileSync4(filePath, "utf-8");
|
|
@@ -3298,8 +4138,8 @@ function registerDbMigrationsCommand(dbCmd2) {
|
|
|
3298
4138
|
}
|
|
3299
4139
|
}
|
|
3300
4140
|
for (const migration of migrationsToApply) {
|
|
3301
|
-
const filePath =
|
|
3302
|
-
if (!
|
|
4141
|
+
const filePath = join9(getMigrationsDir(), migration.filename);
|
|
4142
|
+
if (!existsSync5(filePath)) {
|
|
3303
4143
|
throw new CLIError(`Local migration file not found: ${migration.filename}`);
|
|
3304
4144
|
}
|
|
3305
4145
|
const sql = readFileSync4(filePath, "utf-8");
|
|
@@ -3338,8 +4178,8 @@ function registerRecordsCommands(recordsCmd2) {
|
|
|
3338
4178
|
if (opts.limit) params.set("limit", String(opts.limit));
|
|
3339
4179
|
if (opts.offset) params.set("offset", String(opts.offset));
|
|
3340
4180
|
const query = params.toString();
|
|
3341
|
-
const
|
|
3342
|
-
const res = await ossFetch(
|
|
4181
|
+
const path6 = `/api/database/records/${encodeURIComponent(table)}${query ? `?${query}` : ""}`;
|
|
4182
|
+
const res = await ossFetch(path6);
|
|
3343
4183
|
const data = await res.json();
|
|
3344
4184
|
const records = data.data ?? [];
|
|
3345
4185
|
if (json) {
|
|
@@ -3508,18 +4348,18 @@ function registerFunctionsCommands(functionsCmd2) {
|
|
|
3508
4348
|
}
|
|
3509
4349
|
|
|
3510
4350
|
// src/commands/functions/deploy.ts
|
|
3511
|
-
import { readFileSync as readFileSync5, existsSync as
|
|
3512
|
-
import { join as
|
|
4351
|
+
import { readFileSync as readFileSync5, existsSync as existsSync6 } from "fs";
|
|
4352
|
+
import { join as join10 } from "path";
|
|
3513
4353
|
function registerFunctionsDeployCommand(functionsCmd2) {
|
|
3514
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) => {
|
|
3515
4355
|
const { json } = getRootOpts(cmd);
|
|
3516
4356
|
try {
|
|
3517
4357
|
await requireAuth();
|
|
3518
|
-
const filePath = opts.file ??
|
|
3519
|
-
if (!
|
|
4358
|
+
const filePath = opts.file ?? join10(process.cwd(), "insforge", "functions", slug, "index.ts");
|
|
4359
|
+
if (!existsSync6(filePath)) {
|
|
3520
4360
|
throw new CLIError(
|
|
3521
4361
|
`Source file not found: ${filePath}
|
|
3522
|
-
Specify --file <path> or create ${
|
|
4362
|
+
Specify --file <path> or create ${join10("insforge", "functions", slug, "index.ts")}`
|
|
3523
4363
|
);
|
|
3524
4364
|
}
|
|
3525
4365
|
const code = readFileSync5(filePath, "utf-8");
|
|
@@ -3643,7 +4483,7 @@ function registerFunctionsCodeCommand(functionsCmd2) {
|
|
|
3643
4483
|
}
|
|
3644
4484
|
|
|
3645
4485
|
// src/commands/functions/delete.ts
|
|
3646
|
-
import * as
|
|
4486
|
+
import * as clack13 from "@clack/prompts";
|
|
3647
4487
|
function registerFunctionsDeleteCommand(functionsCmd2) {
|
|
3648
4488
|
functionsCmd2.command("delete <slug>").description("Delete an edge function").action(async (slug, _opts, cmd) => {
|
|
3649
4489
|
const { json, yes } = getRootOpts(cmd);
|
|
@@ -3654,7 +4494,7 @@ function registerFunctionsDeleteCommand(functionsCmd2) {
|
|
|
3654
4494
|
message: `Delete function "${slug}"? This cannot be undone.`
|
|
3655
4495
|
});
|
|
3656
4496
|
if (isCancel2(confirmed) || !confirmed) {
|
|
3657
|
-
|
|
4497
|
+
clack13.log.info("Cancelled.");
|
|
3658
4498
|
return;
|
|
3659
4499
|
}
|
|
3660
4500
|
}
|
|
@@ -3709,8 +4549,8 @@ function registerStorageBucketsCommand(storageCmd2) {
|
|
|
3709
4549
|
}
|
|
3710
4550
|
|
|
3711
4551
|
// src/commands/storage/upload.ts
|
|
3712
|
-
import { readFileSync as readFileSync6, existsSync as
|
|
3713
|
-
import { basename as
|
|
4552
|
+
import { readFileSync as readFileSync6, existsSync as existsSync7 } from "fs";
|
|
4553
|
+
import { basename as basename6 } from "path";
|
|
3714
4554
|
function registerStorageUploadCommand(storageCmd2) {
|
|
3715
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) => {
|
|
3716
4556
|
const { json } = getRootOpts(cmd);
|
|
@@ -3718,11 +4558,11 @@ function registerStorageUploadCommand(storageCmd2) {
|
|
|
3718
4558
|
await requireAuth();
|
|
3719
4559
|
const config = getProjectConfig();
|
|
3720
4560
|
if (!config) throw new ProjectNotLinkedError();
|
|
3721
|
-
if (!
|
|
4561
|
+
if (!existsSync7(file)) {
|
|
3722
4562
|
throw new CLIError(`File not found: ${file}`);
|
|
3723
4563
|
}
|
|
3724
4564
|
const fileContent = readFileSync6(file);
|
|
3725
|
-
const objectKey = opts.key ??
|
|
4565
|
+
const objectKey = opts.key ?? basename6(file);
|
|
3726
4566
|
const bucketName = opts.bucket;
|
|
3727
4567
|
const formData = new FormData();
|
|
3728
4568
|
const blob = new Blob([fileContent]);
|
|
@@ -3743,7 +4583,7 @@ function registerStorageUploadCommand(storageCmd2) {
|
|
|
3743
4583
|
if (json) {
|
|
3744
4584
|
outputJson(data);
|
|
3745
4585
|
} else {
|
|
3746
|
-
outputSuccess(`Uploaded "${
|
|
4586
|
+
outputSuccess(`Uploaded "${basename6(file)}" to bucket "${bucketName}".`);
|
|
3747
4587
|
}
|
|
3748
4588
|
} catch (err) {
|
|
3749
4589
|
handleError(err, json);
|
|
@@ -3752,8 +4592,8 @@ function registerStorageUploadCommand(storageCmd2) {
|
|
|
3752
4592
|
}
|
|
3753
4593
|
|
|
3754
4594
|
// src/commands/storage/download.ts
|
|
3755
|
-
import { writeFileSync as
|
|
3756
|
-
import { join as
|
|
4595
|
+
import { writeFileSync as writeFileSync5 } from "fs";
|
|
4596
|
+
import { join as join11, basename as basename7 } from "path";
|
|
3757
4597
|
function registerStorageDownloadCommand(storageCmd2) {
|
|
3758
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) => {
|
|
3759
4599
|
const { json } = getRootOpts(cmd);
|
|
@@ -3773,8 +4613,8 @@ function registerStorageDownloadCommand(storageCmd2) {
|
|
|
3773
4613
|
throw new CLIError(err.error ?? `Download failed: ${res.status}`);
|
|
3774
4614
|
}
|
|
3775
4615
|
const buffer = Buffer.from(await res.arrayBuffer());
|
|
3776
|
-
const outputPath = opts.output ??
|
|
3777
|
-
|
|
4616
|
+
const outputPath = opts.output ?? join11(process.cwd(), basename7(objectKey));
|
|
4617
|
+
writeFileSync5(outputPath, buffer);
|
|
3778
4618
|
if (json) {
|
|
3779
4619
|
outputJson({ success: true, path: outputPath, size: buffer.length });
|
|
3780
4620
|
} else {
|
|
@@ -3818,10 +4658,10 @@ function registerStorageDeleteBucketCommand(storageCmd2) {
|
|
|
3818
4658
|
try {
|
|
3819
4659
|
await requireAuth();
|
|
3820
4660
|
if (!yes && !json) {
|
|
3821
|
-
const
|
|
4661
|
+
const confirm6 = await confirm2({
|
|
3822
4662
|
message: `Delete bucket "${name}" and all its objects? This cannot be undone.`
|
|
3823
4663
|
});
|
|
3824
|
-
if (isCancel2(
|
|
4664
|
+
if (isCancel2(confirm6) || !confirm6) {
|
|
3825
4665
|
process.exit(0);
|
|
3826
4666
|
}
|
|
3827
4667
|
}
|
|
@@ -4231,8 +5071,8 @@ async function listDocs(json) {
|
|
|
4231
5071
|
);
|
|
4232
5072
|
}
|
|
4233
5073
|
}
|
|
4234
|
-
async function fetchDoc(
|
|
4235
|
-
const res = await ossFetch(
|
|
5074
|
+
async function fetchDoc(path6, label, json) {
|
|
5075
|
+
const res = await ossFetch(path6);
|
|
4236
5076
|
const data = await res.json();
|
|
4237
5077
|
const doc = data.data ?? data;
|
|
4238
5078
|
if (json) {
|
|
@@ -4372,10 +5212,10 @@ function registerSecretsDeleteCommand(secretsCmd2) {
|
|
|
4372
5212
|
try {
|
|
4373
5213
|
await requireAuth();
|
|
4374
5214
|
if (!yes && !json) {
|
|
4375
|
-
const
|
|
5215
|
+
const confirm6 = await confirm2({
|
|
4376
5216
|
message: `Delete secret "${key}"? This cannot be undone.`
|
|
4377
5217
|
});
|
|
4378
|
-
if (isCancel2(
|
|
5218
|
+
if (isCancel2(confirm6) || !confirm6) {
|
|
4379
5219
|
process.exit(0);
|
|
4380
5220
|
}
|
|
4381
5221
|
}
|
|
@@ -4563,10 +5403,10 @@ function registerSchedulesDeleteCommand(schedulesCmd2) {
|
|
|
4563
5403
|
try {
|
|
4564
5404
|
await requireAuth();
|
|
4565
5405
|
if (!yes && !json) {
|
|
4566
|
-
const
|
|
5406
|
+
const confirm6 = await confirm2({
|
|
4567
5407
|
message: `Delete schedule "${id}"? This cannot be undone.`
|
|
4568
5408
|
});
|
|
4569
|
-
if (isCancel2(
|
|
5409
|
+
if (isCancel2(confirm6) || !confirm6) {
|
|
4570
5410
|
process.exit(0);
|
|
4571
5411
|
}
|
|
4572
5412
|
}
|
|
@@ -4690,8 +5530,44 @@ function registerComputeGetCommand(computeCmd2) {
|
|
|
4690
5530
|
}
|
|
4691
5531
|
|
|
4692
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
|
+
}
|
|
4693
5556
|
function registerComputeUpdateCommand(computeCmd2) {
|
|
4694
|
-
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) => {
|
|
4695
5571
|
const { json } = getRootOpts(cmd);
|
|
4696
5572
|
try {
|
|
4697
5573
|
await requireAuth();
|
|
@@ -4711,6 +5587,14 @@ function registerComputeUpdateCommand(computeCmd2) {
|
|
|
4711
5587
|
body.memory = Number(opts.memory);
|
|
4712
5588
|
}
|
|
4713
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
|
+
}
|
|
4714
5598
|
if (opts.env) {
|
|
4715
5599
|
try {
|
|
4716
5600
|
body.envVars = JSON.parse(opts.env);
|
|
@@ -4718,8 +5602,22 @@ function registerComputeUpdateCommand(computeCmd2) {
|
|
|
4718
5602
|
throw new CLIError("Invalid JSON for --env");
|
|
4719
5603
|
}
|
|
4720
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
|
+
}
|
|
4721
5617
|
if (Object.keys(body).length === 0) {
|
|
4722
|
-
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
|
+
);
|
|
4723
5621
|
}
|
|
4724
5622
|
const res = await ossFetch(`/api/compute/services/${encodeURIComponent(id)}`, {
|
|
4725
5623
|
method: "PATCH",
|
|
@@ -4811,46 +5709,86 @@ function registerComputeStopCommand(computeCmd2) {
|
|
|
4811
5709
|
});
|
|
4812
5710
|
}
|
|
4813
5711
|
|
|
4814
|
-
// src/commands/compute/
|
|
4815
|
-
function
|
|
4816
|
-
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) => {
|
|
4817
5715
|
const { json } = getRootOpts(cmd);
|
|
4818
5716
|
try {
|
|
4819
5717
|
await requireAuth();
|
|
4820
5718
|
const limit = Math.max(1, Math.min(Number(opts.limit) || 50, 1e3));
|
|
4821
5719
|
const res = await ossFetch(
|
|
4822
|
-
`/api/compute/services/${encodeURIComponent(id)}/
|
|
5720
|
+
`/api/compute/services/${encodeURIComponent(id)}/events?limit=${limit}`
|
|
4823
5721
|
);
|
|
4824
|
-
const
|
|
5722
|
+
const events = await res.json();
|
|
4825
5723
|
if (json) {
|
|
4826
|
-
outputJson(
|
|
5724
|
+
outputJson(events);
|
|
4827
5725
|
} else {
|
|
4828
|
-
if (!Array.isArray(
|
|
4829
|
-
console.log("No
|
|
4830
|
-
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);
|
|
4831
5729
|
return;
|
|
4832
5730
|
}
|
|
4833
|
-
for (const entry of
|
|
5731
|
+
for (const entry of events) {
|
|
4834
5732
|
const ts = new Date(entry.timestamp).toISOString();
|
|
4835
5733
|
console.log(`${ts} ${entry.message}`);
|
|
4836
5734
|
}
|
|
4837
5735
|
}
|
|
4838
|
-
await reportCliUsage("cli.compute.
|
|
5736
|
+
await reportCliUsage("cli.compute.events", true);
|
|
4839
5737
|
} catch (err) {
|
|
4840
|
-
await reportCliUsage("cli.compute.
|
|
5738
|
+
await reportCliUsage("cli.compute.events", false);
|
|
4841
5739
|
handleError(err, json);
|
|
4842
5740
|
}
|
|
4843
5741
|
});
|
|
4844
5742
|
}
|
|
4845
5743
|
|
|
4846
5744
|
// src/commands/compute/deploy.ts
|
|
4847
|
-
import { existsSync as
|
|
4848
|
-
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
|
+
}
|
|
4849
5787
|
|
|
4850
5788
|
// src/lib/flyctl.ts
|
|
4851
5789
|
import { spawn, spawnSync } from "child_process";
|
|
4852
|
-
import { existsSync as
|
|
4853
|
-
import { join as
|
|
5790
|
+
import { existsSync as existsSync8, writeFileSync as writeFileSync6, unlinkSync as unlinkSync3 } from "fs";
|
|
5791
|
+
import { join as join12 } from "path";
|
|
4854
5792
|
function ensureFlyctlAvailable() {
|
|
4855
5793
|
const r = spawnSync("flyctl", ["version"], {
|
|
4856
5794
|
encoding: "utf8",
|
|
@@ -4863,8 +5801,8 @@ function ensureFlyctlAvailable() {
|
|
|
4863
5801
|
}
|
|
4864
5802
|
}
|
|
4865
5803
|
function ensureFlyTomlStub(opts) {
|
|
4866
|
-
const
|
|
4867
|
-
if (
|
|
5804
|
+
const path6 = join12(opts.dir, "fly.toml");
|
|
5805
|
+
if (existsSync8(path6)) {
|
|
4868
5806
|
return () => {
|
|
4869
5807
|
};
|
|
4870
5808
|
}
|
|
@@ -4881,10 +5819,10 @@ primary_region = "${opts.region}"
|
|
|
4881
5819
|
auto_start_machines = true
|
|
4882
5820
|
min_machines_running = 0
|
|
4883
5821
|
`;
|
|
4884
|
-
|
|
5822
|
+
writeFileSync6(path6, stub, "utf8");
|
|
4885
5823
|
return () => {
|
|
4886
5824
|
try {
|
|
4887
|
-
|
|
5825
|
+
unlinkSync3(path6);
|
|
4888
5826
|
} catch {
|
|
4889
5827
|
}
|
|
4890
5828
|
};
|
|
@@ -4959,7 +5897,10 @@ function registerComputeDeployCommand(computeCmd2) {
|
|
|
4959
5897
|
"--cpu <tier>",
|
|
4960
5898
|
"CPU tier in <kind>-<N>x format (e.g. shared-1x, performance-2x)",
|
|
4961
5899
|
"shared-1x"
|
|
4962
|
-
).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) => {
|
|
4963
5904
|
const { json } = getRootOpts(cmd);
|
|
4964
5905
|
try {
|
|
4965
5906
|
await requireAuth();
|
|
@@ -4979,6 +5920,11 @@ function registerComputeDeployCommand(computeCmd2) {
|
|
|
4979
5920
|
if (!Number.isInteger(memory) || memory <= 0) {
|
|
4980
5921
|
throw new CLIError(`Invalid --memory: ${opts.memory}`);
|
|
4981
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
|
+
}
|
|
4982
5928
|
let envVars;
|
|
4983
5929
|
if (opts.env) {
|
|
4984
5930
|
let parsed;
|
|
@@ -4998,6 +5944,8 @@ function registerComputeDeployCommand(computeCmd2) {
|
|
|
4998
5944
|
}
|
|
4999
5945
|
}
|
|
5000
5946
|
envVars = parsed;
|
|
5947
|
+
} else if (opts.envFile) {
|
|
5948
|
+
envVars = parseEnvFile(resolve4(opts.envFile));
|
|
5001
5949
|
}
|
|
5002
5950
|
const baseBody = {
|
|
5003
5951
|
name: opts.name,
|
|
@@ -5040,8 +5988,8 @@ function registerComputeDeployCommand(computeCmd2) {
|
|
|
5040
5988
|
return;
|
|
5041
5989
|
}
|
|
5042
5990
|
const absDir = resolve4(dir);
|
|
5043
|
-
const dockerfilePath =
|
|
5044
|
-
if (!
|
|
5991
|
+
const dockerfilePath = join13(absDir, "Dockerfile");
|
|
5992
|
+
if (!existsSync9(dockerfilePath)) {
|
|
5045
5993
|
throw new CLIError(
|
|
5046
5994
|
`No Dockerfile at ${dockerfilePath}.
|
|
5047
5995
|
Either:
|
|
@@ -5275,7 +6223,7 @@ function formatSize2(gb) {
|
|
|
5275
6223
|
|
|
5276
6224
|
// src/commands/diagnose/index.ts
|
|
5277
6225
|
import * as os from "os";
|
|
5278
|
-
import * as
|
|
6226
|
+
import * as clack14 from "@clack/prompts";
|
|
5279
6227
|
|
|
5280
6228
|
// src/commands/diagnose/metrics.ts
|
|
5281
6229
|
var METRIC_LABELS = {
|
|
@@ -5816,10 +6764,10 @@ function registerDiagnoseCommands(diagnoseCmd2) {
|
|
|
5816
6764
|
if (question.length === 0 || question.length > 2e3) {
|
|
5817
6765
|
throw new CLIError("Question must be between 1 and 2000 characters.");
|
|
5818
6766
|
}
|
|
5819
|
-
const s = !json ?
|
|
6767
|
+
const s = !json ? clack14.spinner() : null;
|
|
5820
6768
|
s?.start("Collecting diagnostic data...");
|
|
5821
6769
|
const data2 = await collectDiagnosticData(projectId, ossMode, apiUrl);
|
|
5822
|
-
const cliVersion = "0.1.
|
|
6770
|
+
const cliVersion = "0.1.63";
|
|
5823
6771
|
s?.stop("Data collected");
|
|
5824
6772
|
if (!json) {
|
|
5825
6773
|
console.log(`
|
|
@@ -5952,9 +6900,9 @@ function registerDiagnoseCommands(diagnoseCmd2) {
|
|
|
5952
6900
|
void 0,
|
|
5953
6901
|
apiUrl
|
|
5954
6902
|
);
|
|
5955
|
-
|
|
6903
|
+
clack14.log.success("Thanks for your feedback!");
|
|
5956
6904
|
} catch {
|
|
5957
|
-
|
|
6905
|
+
clack14.log.warn("Failed to submit rating.");
|
|
5958
6906
|
}
|
|
5959
6907
|
}
|
|
5960
6908
|
}
|
|
@@ -6046,9 +6994,1145 @@ function formatBytesCompact(bytes) {
|
|
|
6046
6994
|
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
6047
6995
|
}
|
|
6048
6996
|
|
|
6997
|
+
// src/lib/api/payments.ts
|
|
6998
|
+
function withQuery(path6, params) {
|
|
6999
|
+
const query = new URLSearchParams();
|
|
7000
|
+
for (const [key, value] of Object.entries(params)) {
|
|
7001
|
+
if (value !== void 0) query.set(key, String(value));
|
|
7002
|
+
}
|
|
7003
|
+
const suffix = query.toString();
|
|
7004
|
+
return suffix ? `${path6}?${suffix}` : path6;
|
|
7005
|
+
}
|
|
7006
|
+
async function readJson(res) {
|
|
7007
|
+
return await res.json();
|
|
7008
|
+
}
|
|
7009
|
+
function withEnvironmentPath(environment, suffix) {
|
|
7010
|
+
return `/api/payments/${encodeURIComponent(environment)}${suffix}`;
|
|
7011
|
+
}
|
|
7012
|
+
async function getPaymentsStatus() {
|
|
7013
|
+
return readJson(await ossFetch("/api/payments/status"));
|
|
7014
|
+
}
|
|
7015
|
+
async function getPaymentsConfig() {
|
|
7016
|
+
return readJson(await ossFetch("/api/payments/config"));
|
|
7017
|
+
}
|
|
7018
|
+
async function setStripeSecretKey(environment, secretKey) {
|
|
7019
|
+
return readJson(
|
|
7020
|
+
await ossFetch(withEnvironmentPath(environment, "/config"), {
|
|
7021
|
+
method: "PUT",
|
|
7022
|
+
body: JSON.stringify({ secretKey })
|
|
7023
|
+
})
|
|
7024
|
+
);
|
|
7025
|
+
}
|
|
7026
|
+
async function removeStripeSecretKey(environment) {
|
|
7027
|
+
return readJson(
|
|
7028
|
+
await ossFetch(withEnvironmentPath(environment, "/config"), {
|
|
7029
|
+
method: "DELETE"
|
|
7030
|
+
})
|
|
7031
|
+
);
|
|
7032
|
+
}
|
|
7033
|
+
async function syncPayments(environment = "all") {
|
|
7034
|
+
return readJson(
|
|
7035
|
+
await ossFetch(
|
|
7036
|
+
environment === "all" ? "/api/payments/sync" : withEnvironmentPath(environment, "/sync"),
|
|
7037
|
+
{ method: "POST" }
|
|
7038
|
+
)
|
|
7039
|
+
);
|
|
7040
|
+
}
|
|
7041
|
+
async function configurePaymentWebhook(environment) {
|
|
7042
|
+
return readJson(
|
|
7043
|
+
await ossFetch(withEnvironmentPath(environment, "/webhook"), {
|
|
7044
|
+
method: "POST"
|
|
7045
|
+
})
|
|
7046
|
+
);
|
|
7047
|
+
}
|
|
7048
|
+
async function listPaymentCatalog(environment) {
|
|
7049
|
+
return readJson(await ossFetch(withEnvironmentPath(environment, "/catalog")));
|
|
7050
|
+
}
|
|
7051
|
+
async function listPaymentProducts(environment) {
|
|
7052
|
+
return readJson(
|
|
7053
|
+
await ossFetch(withEnvironmentPath(environment, "/catalog/products"))
|
|
7054
|
+
);
|
|
7055
|
+
}
|
|
7056
|
+
async function getPaymentProduct(environment, productId) {
|
|
7057
|
+
return readJson(
|
|
7058
|
+
await ossFetch(
|
|
7059
|
+
withEnvironmentPath(
|
|
7060
|
+
environment,
|
|
7061
|
+
`/catalog/products/${encodeURIComponent(productId)}`
|
|
7062
|
+
)
|
|
7063
|
+
)
|
|
7064
|
+
);
|
|
7065
|
+
}
|
|
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
|
+
);
|
|
7073
|
+
}
|
|
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
|
+
);
|
|
7087
|
+
}
|
|
7088
|
+
async function deletePaymentProduct(environment, productId) {
|
|
7089
|
+
return readJson(
|
|
7090
|
+
await ossFetch(
|
|
7091
|
+
withEnvironmentPath(
|
|
7092
|
+
environment,
|
|
7093
|
+
`/catalog/products/${encodeURIComponent(productId)}`
|
|
7094
|
+
),
|
|
7095
|
+
{ method: "DELETE" }
|
|
7096
|
+
)
|
|
7097
|
+
);
|
|
7098
|
+
}
|
|
7099
|
+
async function listPaymentPrices(environment, stripeProductId) {
|
|
7100
|
+
return readJson(
|
|
7101
|
+
await ossFetch(
|
|
7102
|
+
withQuery(withEnvironmentPath(environment, "/catalog/prices"), {
|
|
7103
|
+
stripeProductId
|
|
7104
|
+
})
|
|
7105
|
+
)
|
|
7106
|
+
);
|
|
7107
|
+
}
|
|
7108
|
+
async function getPaymentPrice(environment, priceId) {
|
|
7109
|
+
return readJson(
|
|
7110
|
+
await ossFetch(
|
|
7111
|
+
withEnvironmentPath(
|
|
7112
|
+
environment,
|
|
7113
|
+
`/catalog/prices/${encodeURIComponent(priceId)}`
|
|
7114
|
+
)
|
|
7115
|
+
)
|
|
7116
|
+
);
|
|
7117
|
+
}
|
|
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
|
+
);
|
|
7125
|
+
}
|
|
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
|
+
);
|
|
7139
|
+
}
|
|
7140
|
+
async function archivePaymentPrice(environment, priceId) {
|
|
7141
|
+
return readJson(
|
|
7142
|
+
await ossFetch(
|
|
7143
|
+
withEnvironmentPath(
|
|
7144
|
+
environment,
|
|
7145
|
+
`/catalog/prices/${encodeURIComponent(priceId)}`
|
|
7146
|
+
),
|
|
7147
|
+
{ method: "DELETE" }
|
|
7148
|
+
)
|
|
7149
|
+
);
|
|
7150
|
+
}
|
|
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
|
+
);
|
|
7164
|
+
}
|
|
7165
|
+
async function listPaymentHistory(environment, request) {
|
|
7166
|
+
return readJson(
|
|
7167
|
+
await ossFetch(
|
|
7168
|
+
withQuery(withEnvironmentPath(environment, "/payment-history"), request)
|
|
7169
|
+
)
|
|
7170
|
+
);
|
|
7171
|
+
}
|
|
7172
|
+
|
|
7173
|
+
// src/commands/payments/utils.ts
|
|
7174
|
+
function parseEnvironment(value) {
|
|
7175
|
+
if (value === "test" || value === "live") return value;
|
|
7176
|
+
throw new CLIError('Environment must be "test" or "live".');
|
|
7177
|
+
}
|
|
7178
|
+
function parseEnvironmentOrAll(value) {
|
|
7179
|
+
if (value === "all") return value;
|
|
7180
|
+
return parseEnvironment(value);
|
|
7181
|
+
}
|
|
7182
|
+
function parseBooleanOption(value, flagName) {
|
|
7183
|
+
if (value === void 0) return void 0;
|
|
7184
|
+
const normalized = value.toLowerCase();
|
|
7185
|
+
if (normalized === "true") return true;
|
|
7186
|
+
if (normalized === "false") return false;
|
|
7187
|
+
throw new CLIError(`${flagName} must be "true" or "false".`);
|
|
7188
|
+
}
|
|
7189
|
+
function parseIntegerOption(value, flagName, options = {}) {
|
|
7190
|
+
if (value === void 0) return void 0;
|
|
7191
|
+
const parsed = Number.parseInt(value, 10);
|
|
7192
|
+
if (!Number.isInteger(parsed) || String(parsed) !== value.trim()) {
|
|
7193
|
+
throw new CLIError(`${flagName} must be an integer.`);
|
|
7194
|
+
}
|
|
7195
|
+
if (options.min !== void 0 && parsed < options.min) {
|
|
7196
|
+
throw new CLIError(`${flagName} must be at least ${options.min}.`);
|
|
7197
|
+
}
|
|
7198
|
+
if (options.max !== void 0 && parsed > options.max) {
|
|
7199
|
+
throw new CLIError(`${flagName} must be at most ${options.max}.`);
|
|
7200
|
+
}
|
|
7201
|
+
return parsed;
|
|
7202
|
+
}
|
|
7203
|
+
function parseMetadataOption(value) {
|
|
7204
|
+
if (value === void 0) return void 0;
|
|
7205
|
+
let parsed;
|
|
7206
|
+
try {
|
|
7207
|
+
parsed = JSON.parse(value);
|
|
7208
|
+
} catch {
|
|
7209
|
+
throw new CLIError("Invalid JSON for --metadata.");
|
|
7210
|
+
}
|
|
7211
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
7212
|
+
throw new CLIError("--metadata must be a JSON object.");
|
|
7213
|
+
}
|
|
7214
|
+
const metadata = {};
|
|
7215
|
+
for (const [key, raw] of Object.entries(parsed)) {
|
|
7216
|
+
if (typeof raw !== "string") {
|
|
7217
|
+
throw new CLIError(`Metadata value for "${key}" must be a string.`);
|
|
7218
|
+
}
|
|
7219
|
+
metadata[key] = raw;
|
|
7220
|
+
}
|
|
7221
|
+
return metadata;
|
|
7222
|
+
}
|
|
7223
|
+
function formatDate(value) {
|
|
7224
|
+
if (!value) return "-";
|
|
7225
|
+
const date = new Date(value);
|
|
7226
|
+
return Number.isNaN(date.getTime()) ? value : date.toLocaleString();
|
|
7227
|
+
}
|
|
7228
|
+
function formatAmount(amount, currency) {
|
|
7229
|
+
if (amount === null || amount === void 0) return "-";
|
|
7230
|
+
const code = currency?.toUpperCase();
|
|
7231
|
+
let fractionDigits = 2;
|
|
7232
|
+
if (code) {
|
|
7233
|
+
try {
|
|
7234
|
+
fractionDigits = new Intl.NumberFormat(void 0, {
|
|
7235
|
+
style: "currency",
|
|
7236
|
+
currency: code
|
|
7237
|
+
}).resolvedOptions().maximumFractionDigits;
|
|
7238
|
+
} catch {
|
|
7239
|
+
fractionDigits = 2;
|
|
7240
|
+
}
|
|
7241
|
+
}
|
|
7242
|
+
const divisor = 10 ** fractionDigits;
|
|
7243
|
+
return `${(amount / divisor).toFixed(fractionDigits)} ${code ?? ""}`.trim();
|
|
7244
|
+
}
|
|
7245
|
+
function formatRecurring(interval, intervalCount) {
|
|
7246
|
+
if (!interval) return "one-time";
|
|
7247
|
+
return `${intervalCount && intervalCount > 1 ? `${intervalCount} ` : ""}${interval}`;
|
|
7248
|
+
}
|
|
7249
|
+
async function trackPaymentUsage(subcommand, success, properties = {}) {
|
|
7250
|
+
try {
|
|
7251
|
+
try {
|
|
7252
|
+
const config = getProjectConfig();
|
|
7253
|
+
if (config) {
|
|
7254
|
+
trackPayments(subcommand, config, {
|
|
7255
|
+
success,
|
|
7256
|
+
...properties
|
|
7257
|
+
});
|
|
7258
|
+
}
|
|
7259
|
+
} catch {
|
|
7260
|
+
}
|
|
7261
|
+
} finally {
|
|
7262
|
+
await shutdownAnalytics();
|
|
7263
|
+
}
|
|
7264
|
+
}
|
|
7265
|
+
|
|
7266
|
+
// src/commands/payments/catalog.ts
|
|
7267
|
+
function registerPaymentsCatalogCommand(paymentsCmd2) {
|
|
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) => {
|
|
7272
|
+
const { json } = getRootOpts(cmd);
|
|
7273
|
+
try {
|
|
7274
|
+
const environment = parseEnvironment(opts.environment);
|
|
7275
|
+
await requireAuth();
|
|
7276
|
+
const data = await listPaymentCatalog(environment);
|
|
7277
|
+
if (json) {
|
|
7278
|
+
outputJson(data);
|
|
7279
|
+
} else {
|
|
7280
|
+
if (data.products.length === 0 && data.prices.length === 0) {
|
|
7281
|
+
console.log("No Stripe catalog records found.");
|
|
7282
|
+
await trackPaymentUsage("catalog", true, { environment });
|
|
7283
|
+
return;
|
|
7284
|
+
}
|
|
7285
|
+
if (data.products.length > 0) {
|
|
7286
|
+
console.log("Products");
|
|
7287
|
+
outputTable(
|
|
7288
|
+
["Env", "Product ID", "Name", "Active", "Default Price"],
|
|
7289
|
+
data.products.map((product) => [
|
|
7290
|
+
product.environment,
|
|
7291
|
+
product.stripeProductId,
|
|
7292
|
+
product.name,
|
|
7293
|
+
product.active ? "Yes" : "No",
|
|
7294
|
+
product.defaultPriceId ?? "-"
|
|
7295
|
+
])
|
|
7296
|
+
);
|
|
7297
|
+
}
|
|
7298
|
+
if (data.prices.length > 0) {
|
|
7299
|
+
console.log("Prices");
|
|
7300
|
+
outputTable(
|
|
7301
|
+
[
|
|
7302
|
+
"Env",
|
|
7303
|
+
"Price ID",
|
|
7304
|
+
"Product ID",
|
|
7305
|
+
"Amount",
|
|
7306
|
+
"Type",
|
|
7307
|
+
"Active",
|
|
7308
|
+
"Recurring"
|
|
7309
|
+
],
|
|
7310
|
+
data.prices.map((price) => [
|
|
7311
|
+
price.environment,
|
|
7312
|
+
price.stripePriceId,
|
|
7313
|
+
price.stripeProductId ?? "-",
|
|
7314
|
+
formatAmount(price.unitAmount, price.currency),
|
|
7315
|
+
price.type,
|
|
7316
|
+
price.active ? "Yes" : "No",
|
|
7317
|
+
formatRecurring(
|
|
7318
|
+
price.recurringInterval,
|
|
7319
|
+
price.recurringIntervalCount
|
|
7320
|
+
)
|
|
7321
|
+
])
|
|
7322
|
+
);
|
|
7323
|
+
}
|
|
7324
|
+
}
|
|
7325
|
+
await trackPaymentUsage("catalog", true, { environment });
|
|
7326
|
+
} catch (err) {
|
|
7327
|
+
await trackPaymentUsage("catalog", false, {
|
|
7328
|
+
environment: opts.environment
|
|
7329
|
+
});
|
|
7330
|
+
handleError(err, json);
|
|
7331
|
+
}
|
|
7332
|
+
});
|
|
7333
|
+
}
|
|
7334
|
+
|
|
7335
|
+
// src/commands/payments/config.ts
|
|
7336
|
+
function outputConfigTable(data) {
|
|
7337
|
+
if (data.keys.length === 0) {
|
|
7338
|
+
console.log("No Stripe keys configured.");
|
|
7339
|
+
return;
|
|
7340
|
+
}
|
|
7341
|
+
outputTable(
|
|
7342
|
+
["Env", "Configured", "Key"],
|
|
7343
|
+
data.keys.map((key) => [
|
|
7344
|
+
key.environment,
|
|
7345
|
+
key.hasKey ? "Yes" : "No",
|
|
7346
|
+
key.maskedKey ?? "-"
|
|
7347
|
+
])
|
|
7348
|
+
);
|
|
7349
|
+
}
|
|
7350
|
+
function registerPaymentsConfigCommand(paymentsCmd2) {
|
|
7351
|
+
const configCmd = paymentsCmd2.command("config").description("Manage Stripe API keys for payments").action(async (_opts, cmd) => {
|
|
7352
|
+
const { json } = getRootOpts(cmd);
|
|
7353
|
+
try {
|
|
7354
|
+
await requireAuth();
|
|
7355
|
+
const data = await getPaymentsConfig();
|
|
7356
|
+
if (json) {
|
|
7357
|
+
outputJson(data);
|
|
7358
|
+
} else {
|
|
7359
|
+
outputConfigTable(data);
|
|
7360
|
+
}
|
|
7361
|
+
await trackPaymentUsage("config", true);
|
|
7362
|
+
} catch (err) {
|
|
7363
|
+
await trackPaymentUsage("config", false);
|
|
7364
|
+
handleError(err, json);
|
|
7365
|
+
}
|
|
7366
|
+
});
|
|
7367
|
+
configCmd.command("set <environment> [secretKey]").description("Configure a Stripe secret key for test or live payments").action(async (environmentValue, secretKeyValue, _opts, cmd) => {
|
|
7368
|
+
const { json } = getRootOpts(cmd);
|
|
7369
|
+
try {
|
|
7370
|
+
const environment = parseEnvironment(environmentValue);
|
|
7371
|
+
await requireAuth();
|
|
7372
|
+
let secretKey = secretKeyValue;
|
|
7373
|
+
if (!secretKey) {
|
|
7374
|
+
if (json) {
|
|
7375
|
+
throw new CLIError("Provide secretKey when using --json.");
|
|
7376
|
+
}
|
|
7377
|
+
const input = await password2({
|
|
7378
|
+
message: `Stripe ${environment} secret key`
|
|
7379
|
+
});
|
|
7380
|
+
if (isCancel2(input)) process.exit(0);
|
|
7381
|
+
secretKey = input;
|
|
7382
|
+
}
|
|
7383
|
+
const data = await setStripeSecretKey(environment, secretKey);
|
|
7384
|
+
if (json) {
|
|
7385
|
+
outputJson(data);
|
|
7386
|
+
} else {
|
|
7387
|
+
outputSuccess(`Stripe ${environment} key configured.`);
|
|
7388
|
+
}
|
|
7389
|
+
await trackPaymentUsage("config.set", true, { environment });
|
|
7390
|
+
} catch (err) {
|
|
7391
|
+
await trackPaymentUsage("config.set", false, { environment: environmentValue });
|
|
7392
|
+
handleError(err, json);
|
|
7393
|
+
}
|
|
7394
|
+
});
|
|
7395
|
+
configCmd.command("remove <environment>").alias("delete").description("Remove a configured Stripe secret key").action(async (environmentValue, _opts, cmd) => {
|
|
7396
|
+
const { json, yes } = getRootOpts(cmd);
|
|
7397
|
+
try {
|
|
7398
|
+
const environment = parseEnvironment(environmentValue);
|
|
7399
|
+
await requireAuth();
|
|
7400
|
+
if (json && !yes) {
|
|
7401
|
+
throw new CLIError("Use --yes with --json to remove a Stripe key non-interactively.");
|
|
7402
|
+
}
|
|
7403
|
+
if (!yes) {
|
|
7404
|
+
const confirm6 = await confirm2({
|
|
7405
|
+
message: `Remove Stripe ${environment} key? Payment sync and mutations for this environment will stop.`
|
|
7406
|
+
});
|
|
7407
|
+
if (isCancel2(confirm6) || !confirm6) process.exit(0);
|
|
7408
|
+
}
|
|
7409
|
+
const data = await removeStripeSecretKey(environment);
|
|
7410
|
+
if (json) {
|
|
7411
|
+
outputJson(data);
|
|
7412
|
+
} else {
|
|
7413
|
+
outputSuccess(`Stripe ${environment} key removed.`);
|
|
7414
|
+
}
|
|
7415
|
+
await trackPaymentUsage("config.remove", true, { environment });
|
|
7416
|
+
} catch (err) {
|
|
7417
|
+
await trackPaymentUsage("config.remove", false, { environment: environmentValue });
|
|
7418
|
+
handleError(err, json);
|
|
7419
|
+
}
|
|
7420
|
+
});
|
|
7421
|
+
}
|
|
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
|
+
|
|
7485
|
+
// src/commands/payments/history.ts
|
|
7486
|
+
function registerPaymentsHistoryCommand(paymentsCmd2) {
|
|
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) => {
|
|
7491
|
+
const { json } = getRootOpts(cmd);
|
|
7492
|
+
try {
|
|
7493
|
+
const environment = parseEnvironment(opts.environment);
|
|
7494
|
+
const limit = parseIntegerOption(opts.limit, "--limit", { min: 1, max: 100 }) ?? 50;
|
|
7495
|
+
await requireAuth();
|
|
7496
|
+
const data = await listPaymentHistory(environment, {
|
|
7497
|
+
limit,
|
|
7498
|
+
...opts.subjectType !== void 0 ? { subjectType: opts.subjectType } : {},
|
|
7499
|
+
...opts.subjectId !== void 0 ? { subjectId: opts.subjectId } : {}
|
|
7500
|
+
});
|
|
7501
|
+
if (json) {
|
|
7502
|
+
outputJson(data);
|
|
7503
|
+
} else if (data.paymentHistory.length === 0) {
|
|
7504
|
+
console.log("No Stripe payment history found.");
|
|
7505
|
+
} else {
|
|
7506
|
+
outputTable(
|
|
7507
|
+
[
|
|
7508
|
+
"Type",
|
|
7509
|
+
"Status",
|
|
7510
|
+
"Subject",
|
|
7511
|
+
"Amount",
|
|
7512
|
+
"Customer",
|
|
7513
|
+
"Stripe Object",
|
|
7514
|
+
"When"
|
|
7515
|
+
],
|
|
7516
|
+
data.paymentHistory.map((entry) => [
|
|
7517
|
+
entry.type,
|
|
7518
|
+
entry.status,
|
|
7519
|
+
entry.subjectType && entry.subjectId ? `${entry.subjectType}:${entry.subjectId}` : "-",
|
|
7520
|
+
formatAmount(entry.amount, entry.currency),
|
|
7521
|
+
entry.stripeCustomerId ?? "-",
|
|
7522
|
+
entry.stripeCheckoutSessionId ?? entry.stripeInvoiceId ?? entry.stripePaymentIntentId ?? entry.stripeRefundId ?? "-",
|
|
7523
|
+
formatDate(
|
|
7524
|
+
entry.paidAt ?? entry.failedAt ?? entry.refundedAt ?? entry.stripeCreatedAt
|
|
7525
|
+
)
|
|
7526
|
+
])
|
|
7527
|
+
);
|
|
7528
|
+
}
|
|
7529
|
+
await trackPaymentUsage("history", true, { environment });
|
|
7530
|
+
} catch (err) {
|
|
7531
|
+
await trackPaymentUsage("history", false, {
|
|
7532
|
+
environment: opts.environment
|
|
7533
|
+
});
|
|
7534
|
+
handleError(err, json);
|
|
7535
|
+
}
|
|
7536
|
+
});
|
|
7537
|
+
}
|
|
7538
|
+
|
|
7539
|
+
// src/commands/payments/prices.ts
|
|
7540
|
+
function nullableString(value) {
|
|
7541
|
+
if (value === void 0) return void 0;
|
|
7542
|
+
return value === "null" ? null : value;
|
|
7543
|
+
}
|
|
7544
|
+
function parseRecurringInterval(value) {
|
|
7545
|
+
if (value === void 0) {
|
|
7546
|
+
return void 0;
|
|
7547
|
+
}
|
|
7548
|
+
if (value === "day" || value === "week" || value === "month" || value === "year") {
|
|
7549
|
+
return value;
|
|
7550
|
+
}
|
|
7551
|
+
throw new CLIError("--interval must be one of: day, week, month, year.");
|
|
7552
|
+
}
|
|
7553
|
+
function parseTaxBehavior(value) {
|
|
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
|
+
);
|
|
7563
|
+
}
|
|
7564
|
+
function outputPricesTable(prices) {
|
|
7565
|
+
if (prices.length === 0) {
|
|
7566
|
+
console.log("No Stripe prices found.");
|
|
7567
|
+
return;
|
|
7568
|
+
}
|
|
7569
|
+
outputTable(
|
|
7570
|
+
[
|
|
7571
|
+
"Env",
|
|
7572
|
+
"Price ID",
|
|
7573
|
+
"Product ID",
|
|
7574
|
+
"Amount",
|
|
7575
|
+
"Type",
|
|
7576
|
+
"Active",
|
|
7577
|
+
"Recurring",
|
|
7578
|
+
"Synced At"
|
|
7579
|
+
],
|
|
7580
|
+
prices.map((price) => [
|
|
7581
|
+
price.environment,
|
|
7582
|
+
price.stripePriceId,
|
|
7583
|
+
price.stripeProductId ?? "-",
|
|
7584
|
+
formatAmount(price.unitAmount, price.currency),
|
|
7585
|
+
price.type,
|
|
7586
|
+
price.active ? "Yes" : "No",
|
|
7587
|
+
formatRecurring(price.recurringInterval, price.recurringIntervalCount),
|
|
7588
|
+
formatDate(price.syncedAt)
|
|
7589
|
+
])
|
|
7590
|
+
);
|
|
7591
|
+
}
|
|
7592
|
+
function registerPaymentsPricesCommand(paymentsCmd2) {
|
|
7593
|
+
const pricesCmd = paymentsCmd2.command("prices").description("Manage Stripe prices");
|
|
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) => {
|
|
7598
|
+
const { json } = getRootOpts(cmd);
|
|
7599
|
+
try {
|
|
7600
|
+
const environment = parseEnvironment(opts.environment);
|
|
7601
|
+
await requireAuth();
|
|
7602
|
+
const data = await listPaymentPrices(environment, opts.product);
|
|
7603
|
+
if (json) {
|
|
7604
|
+
outputJson(data);
|
|
7605
|
+
} else {
|
|
7606
|
+
outputPricesTable(data.prices);
|
|
7607
|
+
}
|
|
7608
|
+
await trackPaymentUsage("prices.list", true, { environment });
|
|
7609
|
+
} catch (err) {
|
|
7610
|
+
await trackPaymentUsage("prices.list", false, {
|
|
7611
|
+
environment: opts.environment
|
|
7612
|
+
});
|
|
7613
|
+
handleError(err, json);
|
|
7614
|
+
}
|
|
7615
|
+
});
|
|
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) => {
|
|
7620
|
+
const { json } = getRootOpts(cmd);
|
|
7621
|
+
try {
|
|
7622
|
+
const environment = parseEnvironment(opts.environment);
|
|
7623
|
+
await requireAuth();
|
|
7624
|
+
const data = await getPaymentPrice(environment, priceId);
|
|
7625
|
+
if (json) {
|
|
7626
|
+
outputJson(data);
|
|
7627
|
+
} else {
|
|
7628
|
+
outputPricesTable([data.price]);
|
|
7629
|
+
}
|
|
7630
|
+
await trackPaymentUsage("prices.get", true, { environment });
|
|
7631
|
+
} catch (err) {
|
|
7632
|
+
await trackPaymentUsage("prices.get", false, {
|
|
7633
|
+
environment: opts.environment
|
|
7634
|
+
});
|
|
7635
|
+
handleError(err, json);
|
|
7636
|
+
}
|
|
7637
|
+
});
|
|
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) => {
|
|
7651
|
+
const { json } = getRootOpts(cmd);
|
|
7652
|
+
try {
|
|
7653
|
+
const environment = parseEnvironment(opts.environment);
|
|
7654
|
+
await requireAuth();
|
|
7655
|
+
const interval = parseRecurringInterval(opts.interval);
|
|
7656
|
+
const intervalCount = parseIntegerOption(
|
|
7657
|
+
opts.intervalCount,
|
|
7658
|
+
"--interval-count",
|
|
7659
|
+
{ min: 1 }
|
|
7660
|
+
);
|
|
7661
|
+
if (!interval && intervalCount !== void 0) {
|
|
7662
|
+
throw new CLIError("Provide --interval when using --interval-count.");
|
|
7663
|
+
}
|
|
7664
|
+
const request = {
|
|
7665
|
+
stripeProductId: opts.product,
|
|
7666
|
+
currency: opts.currency,
|
|
7667
|
+
unitAmount: parseIntegerOption(opts.unitAmount, "--unit-amount", { min: 0 }) ?? 0
|
|
7668
|
+
};
|
|
7669
|
+
const lookupKey = nullableString(opts.lookupKey);
|
|
7670
|
+
const active = parseBooleanOption(opts.active, "--active");
|
|
7671
|
+
const taxBehavior = parseTaxBehavior(opts.taxBehavior);
|
|
7672
|
+
const metadata = parseMetadataOption(opts.metadata);
|
|
7673
|
+
if (lookupKey !== void 0) request.lookupKey = lookupKey;
|
|
7674
|
+
if (active !== void 0) request.active = active;
|
|
7675
|
+
if (taxBehavior !== void 0) request.taxBehavior = taxBehavior;
|
|
7676
|
+
if (metadata !== void 0) request.metadata = metadata;
|
|
7677
|
+
if (opts.idempotencyKey !== void 0) {
|
|
7678
|
+
request.idempotencyKey = opts.idempotencyKey;
|
|
7679
|
+
}
|
|
7680
|
+
if (interval) {
|
|
7681
|
+
request.recurring = {
|
|
7682
|
+
interval,
|
|
7683
|
+
...intervalCount !== void 0 ? { intervalCount } : {}
|
|
7684
|
+
};
|
|
7685
|
+
}
|
|
7686
|
+
const data = await createPaymentPrice(environment, request);
|
|
7687
|
+
if (json) {
|
|
7688
|
+
outputJson(data);
|
|
7689
|
+
} else {
|
|
7690
|
+
outputSuccess(`Stripe price created: ${data.price.stripePriceId}`);
|
|
7691
|
+
}
|
|
7692
|
+
await trackPaymentUsage("prices.create", true, { environment });
|
|
7693
|
+
} catch (err) {
|
|
7694
|
+
await trackPaymentUsage("prices.create", false, {
|
|
7695
|
+
environment: opts.environment
|
|
7696
|
+
});
|
|
7697
|
+
handleError(err, json);
|
|
7698
|
+
}
|
|
7699
|
+
});
|
|
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) => {
|
|
7704
|
+
const { json } = getRootOpts(cmd);
|
|
7705
|
+
try {
|
|
7706
|
+
const environment = parseEnvironment(opts.environment);
|
|
7707
|
+
await requireAuth();
|
|
7708
|
+
const request = {};
|
|
7709
|
+
const active = parseBooleanOption(opts.active, "--active");
|
|
7710
|
+
const lookupKey = nullableString(opts.lookupKey);
|
|
7711
|
+
const taxBehavior = parseTaxBehavior(opts.taxBehavior);
|
|
7712
|
+
const metadata = parseMetadataOption(opts.metadata);
|
|
7713
|
+
if (active !== void 0) request.active = active;
|
|
7714
|
+
if (lookupKey !== void 0) request.lookupKey = lookupKey;
|
|
7715
|
+
if (taxBehavior !== void 0) request.taxBehavior = taxBehavior;
|
|
7716
|
+
if (metadata !== void 0) request.metadata = metadata;
|
|
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
|
+
);
|
|
7721
|
+
}
|
|
7722
|
+
const data = await updatePaymentPrice(environment, priceId, request);
|
|
7723
|
+
if (json) {
|
|
7724
|
+
outputJson(data);
|
|
7725
|
+
} else {
|
|
7726
|
+
outputSuccess(`Stripe price updated: ${data.price.stripePriceId}`);
|
|
7727
|
+
}
|
|
7728
|
+
await trackPaymentUsage("prices.update", true, { environment });
|
|
7729
|
+
} catch (err) {
|
|
7730
|
+
await trackPaymentUsage("prices.update", false, {
|
|
7731
|
+
environment: opts.environment
|
|
7732
|
+
});
|
|
7733
|
+
handleError(err, json);
|
|
7734
|
+
}
|
|
7735
|
+
});
|
|
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) => {
|
|
7740
|
+
const { json } = getRootOpts(cmd);
|
|
7741
|
+
try {
|
|
7742
|
+
const environment = parseEnvironment(opts.environment);
|
|
7743
|
+
await requireAuth();
|
|
7744
|
+
const data = await archivePaymentPrice(environment, priceId);
|
|
7745
|
+
if (json) {
|
|
7746
|
+
outputJson(data);
|
|
7747
|
+
} else {
|
|
7748
|
+
outputSuccess(`Stripe price archived: ${data.price.stripePriceId}`);
|
|
7749
|
+
}
|
|
7750
|
+
await trackPaymentUsage("prices.archive", true, { environment });
|
|
7751
|
+
} catch (err) {
|
|
7752
|
+
await trackPaymentUsage("prices.archive", false, {
|
|
7753
|
+
environment: opts.environment
|
|
7754
|
+
});
|
|
7755
|
+
handleError(err, json);
|
|
7756
|
+
}
|
|
7757
|
+
});
|
|
7758
|
+
}
|
|
7759
|
+
|
|
7760
|
+
// src/commands/payments/products.ts
|
|
7761
|
+
function nullableString2(value) {
|
|
7762
|
+
if (value === void 0) return void 0;
|
|
7763
|
+
return value === "null" ? null : value;
|
|
7764
|
+
}
|
|
7765
|
+
function outputProductsTable(products) {
|
|
7766
|
+
if (products.length === 0) {
|
|
7767
|
+
console.log("No Stripe products found.");
|
|
7768
|
+
return;
|
|
7769
|
+
}
|
|
7770
|
+
outputTable(
|
|
7771
|
+
["Env", "Product ID", "Name", "Active", "Default Price", "Synced At"],
|
|
7772
|
+
products.map((product) => [
|
|
7773
|
+
product.environment,
|
|
7774
|
+
product.stripeProductId,
|
|
7775
|
+
product.name,
|
|
7776
|
+
product.active ? "Yes" : "No",
|
|
7777
|
+
product.defaultPriceId ?? "-",
|
|
7778
|
+
formatDate(product.syncedAt)
|
|
7779
|
+
])
|
|
7780
|
+
);
|
|
7781
|
+
}
|
|
7782
|
+
function registerPaymentsProductsCommand(paymentsCmd2) {
|
|
7783
|
+
const productsCmd = paymentsCmd2.command("products").description("Manage Stripe products");
|
|
7784
|
+
productsCmd.command("list").description("List mirrored Stripe products").requiredOption(
|
|
7785
|
+
"--environment <environment>",
|
|
7786
|
+
"Stripe environment: test or live"
|
|
7787
|
+
).action(async (opts, cmd) => {
|
|
7788
|
+
const { json } = getRootOpts(cmd);
|
|
7789
|
+
try {
|
|
7790
|
+
const environment = parseEnvironment(opts.environment);
|
|
7791
|
+
await requireAuth();
|
|
7792
|
+
const data = await listPaymentProducts(environment);
|
|
7793
|
+
if (json) {
|
|
7794
|
+
outputJson(data);
|
|
7795
|
+
} else {
|
|
7796
|
+
outputProductsTable(data.products);
|
|
7797
|
+
}
|
|
7798
|
+
await trackPaymentUsage("products.list", true, { environment });
|
|
7799
|
+
} catch (err) {
|
|
7800
|
+
await trackPaymentUsage("products.list", false, {
|
|
7801
|
+
environment: opts.environment
|
|
7802
|
+
});
|
|
7803
|
+
handleError(err, json);
|
|
7804
|
+
}
|
|
7805
|
+
});
|
|
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) => {
|
|
7810
|
+
const { json } = getRootOpts(cmd);
|
|
7811
|
+
try {
|
|
7812
|
+
const environment = parseEnvironment(opts.environment);
|
|
7813
|
+
await requireAuth();
|
|
7814
|
+
const data = await getPaymentProduct(environment, productId);
|
|
7815
|
+
if (json) {
|
|
7816
|
+
outputJson(data);
|
|
7817
|
+
} else {
|
|
7818
|
+
outputProductsTable([data.product]);
|
|
7819
|
+
if (data.prices.length > 0) {
|
|
7820
|
+
console.log("Prices");
|
|
7821
|
+
outputTable(
|
|
7822
|
+
["Price ID", "Amount", "Type", "Active", "Lookup Key"],
|
|
7823
|
+
data.prices.map((price) => [
|
|
7824
|
+
price.stripePriceId,
|
|
7825
|
+
formatAmount(price.unitAmount, price.currency),
|
|
7826
|
+
price.type,
|
|
7827
|
+
price.active ? "Yes" : "No",
|
|
7828
|
+
price.lookupKey ?? "-"
|
|
7829
|
+
])
|
|
7830
|
+
);
|
|
7831
|
+
}
|
|
7832
|
+
}
|
|
7833
|
+
await trackPaymentUsage("products.get", true, { environment });
|
|
7834
|
+
} catch (err) {
|
|
7835
|
+
await trackPaymentUsage("products.get", false, {
|
|
7836
|
+
environment: opts.environment
|
|
7837
|
+
});
|
|
7838
|
+
handleError(err, json);
|
|
7839
|
+
}
|
|
7840
|
+
});
|
|
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) => {
|
|
7845
|
+
const { json } = getRootOpts(cmd);
|
|
7846
|
+
try {
|
|
7847
|
+
const environment = parseEnvironment(opts.environment);
|
|
7848
|
+
await requireAuth();
|
|
7849
|
+
const request = { name: opts.name };
|
|
7850
|
+
const description = nullableString2(opts.description);
|
|
7851
|
+
const active = parseBooleanOption(opts.active, "--active");
|
|
7852
|
+
const metadata = parseMetadataOption(opts.metadata);
|
|
7853
|
+
if (description !== void 0) request.description = description;
|
|
7854
|
+
if (active !== void 0) request.active = active;
|
|
7855
|
+
if (metadata !== void 0) request.metadata = metadata;
|
|
7856
|
+
if (opts.idempotencyKey !== void 0) {
|
|
7857
|
+
request.idempotencyKey = opts.idempotencyKey;
|
|
7858
|
+
}
|
|
7859
|
+
const data = await createPaymentProduct(environment, request);
|
|
7860
|
+
if (json) {
|
|
7861
|
+
outputJson(data);
|
|
7862
|
+
} else {
|
|
7863
|
+
outputSuccess(
|
|
7864
|
+
`Stripe product created: ${data.product.stripeProductId}`
|
|
7865
|
+
);
|
|
7866
|
+
}
|
|
7867
|
+
await trackPaymentUsage("products.create", true, { environment });
|
|
7868
|
+
} catch (err) {
|
|
7869
|
+
await trackPaymentUsage("products.create", false, {
|
|
7870
|
+
environment: opts.environment
|
|
7871
|
+
});
|
|
7872
|
+
handleError(err, json);
|
|
7873
|
+
}
|
|
7874
|
+
});
|
|
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) => {
|
|
7879
|
+
const { json } = getRootOpts(cmd);
|
|
7880
|
+
try {
|
|
7881
|
+
const environment = parseEnvironment(opts.environment);
|
|
7882
|
+
await requireAuth();
|
|
7883
|
+
const request = {};
|
|
7884
|
+
const description = nullableString2(opts.description);
|
|
7885
|
+
const active = parseBooleanOption(opts.active, "--active");
|
|
7886
|
+
const metadata = parseMetadataOption(opts.metadata);
|
|
7887
|
+
if (opts.name !== void 0) request.name = opts.name;
|
|
7888
|
+
if (description !== void 0) request.description = description;
|
|
7889
|
+
if (active !== void 0) request.active = active;
|
|
7890
|
+
if (metadata !== void 0) request.metadata = metadata;
|
|
7891
|
+
if (Object.keys(request).length === 0) {
|
|
7892
|
+
throw new CLIError(
|
|
7893
|
+
"Provide at least one option to update (--name, --description, --active, --metadata)."
|
|
7894
|
+
);
|
|
7895
|
+
}
|
|
7896
|
+
const data = await updatePaymentProduct(
|
|
7897
|
+
environment,
|
|
7898
|
+
productId,
|
|
7899
|
+
request
|
|
7900
|
+
);
|
|
7901
|
+
if (json) {
|
|
7902
|
+
outputJson(data);
|
|
7903
|
+
} else {
|
|
7904
|
+
outputSuccess(
|
|
7905
|
+
`Stripe product updated: ${data.product.stripeProductId}`
|
|
7906
|
+
);
|
|
7907
|
+
}
|
|
7908
|
+
await trackPaymentUsage("products.update", true, { environment });
|
|
7909
|
+
} catch (err) {
|
|
7910
|
+
await trackPaymentUsage("products.update", false, {
|
|
7911
|
+
environment: opts.environment
|
|
7912
|
+
});
|
|
7913
|
+
handleError(err, json);
|
|
7914
|
+
}
|
|
7915
|
+
});
|
|
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) => {
|
|
7920
|
+
const { json, yes } = getRootOpts(cmd);
|
|
7921
|
+
try {
|
|
7922
|
+
const environment = parseEnvironment(opts.environment);
|
|
7923
|
+
await requireAuth();
|
|
7924
|
+
if (json && !yes) {
|
|
7925
|
+
throw new CLIError(
|
|
7926
|
+
"Use --yes with --json to delete a Stripe product non-interactively."
|
|
7927
|
+
);
|
|
7928
|
+
}
|
|
7929
|
+
if (!yes) {
|
|
7930
|
+
const confirm6 = await confirm2({
|
|
7931
|
+
message: `Delete Stripe ${environment} product "${productId}"?`
|
|
7932
|
+
});
|
|
7933
|
+
if (isCancel2(confirm6) || !confirm6) process.exit(0);
|
|
7934
|
+
}
|
|
7935
|
+
const data = await deletePaymentProduct(environment, productId);
|
|
7936
|
+
if (json) {
|
|
7937
|
+
outputJson(data);
|
|
7938
|
+
} else {
|
|
7939
|
+
outputSuccess(`Stripe product deleted: ${data.stripeProductId}`);
|
|
7940
|
+
}
|
|
7941
|
+
await trackPaymentUsage("products.delete", true, { environment });
|
|
7942
|
+
} catch (err) {
|
|
7943
|
+
await trackPaymentUsage("products.delete", false, {
|
|
7944
|
+
environment: opts.environment
|
|
7945
|
+
});
|
|
7946
|
+
handleError(err, json);
|
|
7947
|
+
}
|
|
7948
|
+
});
|
|
7949
|
+
}
|
|
7950
|
+
|
|
7951
|
+
// src/commands/payments/status.ts
|
|
7952
|
+
function registerPaymentsStatusCommand(paymentsCmd2) {
|
|
7953
|
+
paymentsCmd2.command("status").description("Show Stripe payment connection, sync, and webhook status").action(async (_opts, cmd) => {
|
|
7954
|
+
const { json } = getRootOpts(cmd);
|
|
7955
|
+
try {
|
|
7956
|
+
await requireAuth();
|
|
7957
|
+
const data = await getPaymentsStatus();
|
|
7958
|
+
if (json) {
|
|
7959
|
+
outputJson(data);
|
|
7960
|
+
} else if (data.connections.length === 0) {
|
|
7961
|
+
console.log("No Stripe payment environments found.");
|
|
7962
|
+
} else {
|
|
7963
|
+
outputTable(
|
|
7964
|
+
["Env", "Status", "Key", "Account", "Webhook", "Last Sync", "Synced At"],
|
|
7965
|
+
data.connections.map((connection) => [
|
|
7966
|
+
connection.environment,
|
|
7967
|
+
connection.status,
|
|
7968
|
+
connection.maskedKey ?? "-",
|
|
7969
|
+
connection.stripeAccountId ?? "-",
|
|
7970
|
+
connection.webhookEndpointId ? "Configured" : "-",
|
|
7971
|
+
connection.lastSyncStatus ?? "-",
|
|
7972
|
+
formatDate(connection.lastSyncedAt)
|
|
7973
|
+
])
|
|
7974
|
+
);
|
|
7975
|
+
}
|
|
7976
|
+
await trackPaymentUsage("status", true);
|
|
7977
|
+
} catch (err) {
|
|
7978
|
+
await trackPaymentUsage("status", false);
|
|
7979
|
+
handleError(err, json);
|
|
7980
|
+
}
|
|
7981
|
+
});
|
|
7982
|
+
}
|
|
7983
|
+
|
|
7984
|
+
// src/commands/payments/subscriptions.ts
|
|
7985
|
+
function registerPaymentsSubscriptionsCommand(paymentsCmd2) {
|
|
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) => {
|
|
7990
|
+
const { json } = getRootOpts(cmd);
|
|
7991
|
+
try {
|
|
7992
|
+
const environment = parseEnvironment(opts.environment);
|
|
7993
|
+
const limit = parseIntegerOption(opts.limit, "--limit", { min: 1, max: 100 }) ?? 50;
|
|
7994
|
+
await requireAuth();
|
|
7995
|
+
const data = await listSubscriptions(environment, {
|
|
7996
|
+
limit,
|
|
7997
|
+
...opts.subjectType !== void 0 ? { subjectType: opts.subjectType } : {},
|
|
7998
|
+
...opts.subjectId !== void 0 ? { subjectId: opts.subjectId } : {}
|
|
7999
|
+
});
|
|
8000
|
+
if (json) {
|
|
8001
|
+
outputJson(data);
|
|
8002
|
+
} else if (data.subscriptions.length === 0) {
|
|
8003
|
+
console.log("No Stripe subscriptions found.");
|
|
8004
|
+
} else {
|
|
8005
|
+
outputTable(
|
|
8006
|
+
[
|
|
8007
|
+
"Subscription ID",
|
|
8008
|
+
"Customer",
|
|
8009
|
+
"Subject",
|
|
8010
|
+
"Status",
|
|
8011
|
+
"Items",
|
|
8012
|
+
"Period End"
|
|
8013
|
+
],
|
|
8014
|
+
data.subscriptions.map((subscription) => [
|
|
8015
|
+
subscription.stripeSubscriptionId,
|
|
8016
|
+
subscription.stripeCustomerId,
|
|
8017
|
+
subscription.subjectType && subscription.subjectId ? `${subscription.subjectType}:${subscription.subjectId}` : "-",
|
|
8018
|
+
subscription.status,
|
|
8019
|
+
String(subscription.items?.length ?? 0),
|
|
8020
|
+
formatDate(subscription.currentPeriodEnd)
|
|
8021
|
+
])
|
|
8022
|
+
);
|
|
8023
|
+
}
|
|
8024
|
+
await trackPaymentUsage("subscriptions", true, { environment });
|
|
8025
|
+
} catch (err) {
|
|
8026
|
+
await trackPaymentUsage("subscriptions", false, {
|
|
8027
|
+
environment: opts.environment
|
|
8028
|
+
});
|
|
8029
|
+
handleError(err, json);
|
|
8030
|
+
}
|
|
8031
|
+
});
|
|
8032
|
+
}
|
|
8033
|
+
|
|
8034
|
+
// src/commands/payments/sync.ts
|
|
8035
|
+
function registerPaymentsSyncCommand(paymentsCmd2) {
|
|
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) => {
|
|
8043
|
+
const { json } = getRootOpts(cmd);
|
|
8044
|
+
try {
|
|
8045
|
+
const environment = parseEnvironmentOrAll(opts.environment);
|
|
8046
|
+
await requireAuth();
|
|
8047
|
+
const data = await syncPayments(environment);
|
|
8048
|
+
if (json) {
|
|
8049
|
+
outputJson(data);
|
|
8050
|
+
} else if (data.results.length === 0) {
|
|
8051
|
+
console.log("No configured Stripe environments to sync.");
|
|
8052
|
+
} else {
|
|
8053
|
+
outputTable(
|
|
8054
|
+
[
|
|
8055
|
+
"Env",
|
|
8056
|
+
"Status",
|
|
8057
|
+
"Products",
|
|
8058
|
+
"Prices",
|
|
8059
|
+
"Customers",
|
|
8060
|
+
"Subscriptions",
|
|
8061
|
+
"Unmapped",
|
|
8062
|
+
"Synced At"
|
|
8063
|
+
],
|
|
8064
|
+
data.results.map((result) => [
|
|
8065
|
+
result.environment,
|
|
8066
|
+
result.connection.lastSyncStatus ?? result.connection.status,
|
|
8067
|
+
String(result.connection.lastSyncCounts.products ?? 0),
|
|
8068
|
+
String(result.connection.lastSyncCounts.prices ?? 0),
|
|
8069
|
+
String(result.connection.lastSyncCounts.customers ?? 0),
|
|
8070
|
+
String(result.subscriptions?.synced ?? 0),
|
|
8071
|
+
String(result.subscriptions?.unmapped ?? 0),
|
|
8072
|
+
formatDate(result.connection.lastSyncedAt)
|
|
8073
|
+
])
|
|
8074
|
+
);
|
|
8075
|
+
outputSuccess("Stripe payments synced.");
|
|
8076
|
+
}
|
|
8077
|
+
await trackPaymentUsage("sync", true, { environment });
|
|
8078
|
+
} catch (err) {
|
|
8079
|
+
await trackPaymentUsage("sync", false, {
|
|
8080
|
+
environment: opts.environment
|
|
8081
|
+
});
|
|
8082
|
+
handleError(err, json);
|
|
8083
|
+
}
|
|
8084
|
+
});
|
|
8085
|
+
}
|
|
8086
|
+
|
|
8087
|
+
// src/commands/payments/webhooks.ts
|
|
8088
|
+
function registerPaymentsWebhooksCommand(paymentsCmd2) {
|
|
8089
|
+
const webhooksCmd = paymentsCmd2.command("webhooks").description("Manage InsForge-managed Stripe webhooks");
|
|
8090
|
+
webhooksCmd.command("configure <environment>").description("Create or recreate the managed Stripe webhook endpoint").action(async (environmentValue, _opts, cmd) => {
|
|
8091
|
+
const { json } = getRootOpts(cmd);
|
|
8092
|
+
try {
|
|
8093
|
+
const environment = parseEnvironment(environmentValue);
|
|
8094
|
+
await requireAuth();
|
|
8095
|
+
const data = await configurePaymentWebhook(environment);
|
|
8096
|
+
if (json) {
|
|
8097
|
+
outputJson(data);
|
|
8098
|
+
} else {
|
|
8099
|
+
outputTable(
|
|
8100
|
+
["Env", "Webhook ID", "URL", "Configured At"],
|
|
8101
|
+
[[
|
|
8102
|
+
data.connection.environment,
|
|
8103
|
+
data.connection.webhookEndpointId ?? "-",
|
|
8104
|
+
data.connection.webhookEndpointUrl ?? "-",
|
|
8105
|
+
formatDate(data.connection.webhookConfiguredAt)
|
|
8106
|
+
]]
|
|
8107
|
+
);
|
|
8108
|
+
outputSuccess(`Stripe ${environment} webhook configured.`);
|
|
8109
|
+
}
|
|
8110
|
+
await trackPaymentUsage("webhooks.configure", true, { environment });
|
|
8111
|
+
} catch (err) {
|
|
8112
|
+
await trackPaymentUsage("webhooks.configure", false, { environment: environmentValue });
|
|
8113
|
+
handleError(err, json);
|
|
8114
|
+
}
|
|
8115
|
+
});
|
|
8116
|
+
}
|
|
8117
|
+
|
|
8118
|
+
// src/commands/payments/index.ts
|
|
8119
|
+
function registerPaymentsCommands(paymentsCmd2) {
|
|
8120
|
+
paymentsCmd2.description("Manage Stripe payments");
|
|
8121
|
+
registerPaymentsStatusCommand(paymentsCmd2);
|
|
8122
|
+
registerPaymentsConfigCommand(paymentsCmd2);
|
|
8123
|
+
registerPaymentsSyncCommand(paymentsCmd2);
|
|
8124
|
+
registerPaymentsWebhooksCommand(paymentsCmd2);
|
|
8125
|
+
registerPaymentsCatalogCommand(paymentsCmd2);
|
|
8126
|
+
registerPaymentsCustomersCommand(paymentsCmd2);
|
|
8127
|
+
registerPaymentsProductsCommand(paymentsCmd2);
|
|
8128
|
+
registerPaymentsPricesCommand(paymentsCmd2);
|
|
8129
|
+
registerPaymentsSubscriptionsCommand(paymentsCmd2);
|
|
8130
|
+
registerPaymentsHistoryCommand(paymentsCmd2);
|
|
8131
|
+
}
|
|
8132
|
+
|
|
6049
8133
|
// src/index.ts
|
|
6050
|
-
var __dirname =
|
|
6051
|
-
var pkg = JSON.parse(
|
|
8134
|
+
var __dirname = dirname2(fileURLToPath(import.meta.url));
|
|
8135
|
+
var pkg = JSON.parse(readFileSync8(join14(__dirname, "../package.json"), "utf-8"));
|
|
6052
8136
|
var INSFORGE_LOGO = `
|
|
6053
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
|
|
6054
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
|
|
@@ -6072,6 +8156,7 @@ var orgsCmd = program.command("orgs", { hidden: true }).description("Manage orga
|
|
|
6072
8156
|
registerOrgsCommands(orgsCmd);
|
|
6073
8157
|
var projectsCmd = program.command("projects", { hidden: true }).description("Manage projects");
|
|
6074
8158
|
registerProjectsCommands(projectsCmd);
|
|
8159
|
+
registerBranchCommands(program);
|
|
6075
8160
|
var dbCmd = program.command("db").description("Database operations");
|
|
6076
8161
|
registerDbCommands(dbCmd);
|
|
6077
8162
|
registerDbTablesCommand(dbCmd);
|
|
@@ -6117,6 +8202,8 @@ registerLogsCommand(program);
|
|
|
6117
8202
|
registerMetadataCommand(program);
|
|
6118
8203
|
var diagnoseCmd = program.command("diagnose");
|
|
6119
8204
|
registerDiagnoseCommands(diagnoseCmd);
|
|
8205
|
+
var paymentsCmd = program.command("payments").description("Manage Stripe payments");
|
|
8206
|
+
registerPaymentsCommands(paymentsCmd);
|
|
6120
8207
|
var computeCmd = program.command("compute").description("Manage compute services (Docker containers on Fly.io)");
|
|
6121
8208
|
registerComputeListCommand(computeCmd);
|
|
6122
8209
|
registerComputeGetCommand(computeCmd);
|
|
@@ -6125,7 +8212,7 @@ registerComputeUpdateCommand(computeCmd);
|
|
|
6125
8212
|
registerComputeDeleteCommand(computeCmd);
|
|
6126
8213
|
registerComputeStartCommand(computeCmd);
|
|
6127
8214
|
registerComputeStopCommand(computeCmd);
|
|
6128
|
-
|
|
8215
|
+
registerComputeEventsCommand(computeCmd);
|
|
6129
8216
|
var schedulesCmd = program.command("schedules").description("Manage scheduled tasks (cron jobs)");
|
|
6130
8217
|
registerSchedulesListCommand(schedulesCmd);
|
|
6131
8218
|
registerSchedulesGetCommand(schedulesCmd);
|
|
@@ -6150,7 +8237,7 @@ async function showInteractiveMenu() {
|
|
|
6150
8237
|
} catch {
|
|
6151
8238
|
}
|
|
6152
8239
|
console.log(INSFORGE_LOGO);
|
|
6153
|
-
|
|
8240
|
+
clack15.intro(`InsForge CLI v${pkg.version}`);
|
|
6154
8241
|
const options = [];
|
|
6155
8242
|
if (!isLoggedIn) {
|
|
6156
8243
|
options.push({ value: "login", label: "Log in to InsForge" });
|
|
@@ -6171,7 +8258,7 @@ async function showInteractiveMenu() {
|
|
|
6171
8258
|
options
|
|
6172
8259
|
});
|
|
6173
8260
|
if (isCancel2(action)) {
|
|
6174
|
-
|
|
8261
|
+
clack15.cancel("Bye!");
|
|
6175
8262
|
process.exit(0);
|
|
6176
8263
|
}
|
|
6177
8264
|
switch (action) {
|