@insforge/cli 0.1.43 → 0.1.44

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/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { readFileSync as readFileSync7 } from "fs";
5
- import { join as join8, dirname } from "path";
5
+ import { join as join9, dirname } from "path";
6
6
  import { fileURLToPath } from "url";
7
7
  import { Command } from "commander";
8
8
  import * as clack14 from "@clack/prompts";
@@ -204,8 +204,8 @@ function startCallbackServer() {
204
204
  return new Promise((resolveServer) => {
205
205
  let resolveResult;
206
206
  let rejectResult;
207
- const resultPromise = new Promise((resolve2, reject) => {
208
- resolveResult = resolve2;
207
+ const resultPromise = new Promise((resolve4, reject) => {
208
+ resolveResult = resolve4;
209
209
  rejectResult = reject;
210
210
  });
211
211
  const server = createServer((req, res) => {
@@ -383,7 +383,7 @@ async function refreshAccessToken(apiUrl) {
383
383
  }
384
384
 
385
385
  // src/lib/api/platform.ts
386
- async function platformFetch(path4, options = {}, apiUrl) {
386
+ async function platformFetch(path5, options = {}, apiUrl) {
387
387
  const baseUrl = getPlatformApiUrl(apiUrl);
388
388
  const token = getAccessToken();
389
389
  if (!token) {
@@ -394,11 +394,11 @@ async function platformFetch(path4, options = {}, apiUrl) {
394
394
  Authorization: `Bearer ${token}`,
395
395
  ...options.headers ?? {}
396
396
  };
397
- const res = await fetch(`${baseUrl}${path4}`, { ...options, headers });
397
+ const res = await fetch(`${baseUrl}${path5}`, { ...options, headers });
398
398
  if (res.status === 401) {
399
399
  const newToken = await refreshAccessToken(apiUrl);
400
400
  headers.Authorization = `Bearer ${newToken}`;
401
- const retryRes = await fetch(`${baseUrl}${path4}`, { ...options, headers });
401
+ const retryRes = await fetch(`${baseUrl}${path5}`, { ...options, headers });
402
402
  if (!retryRes.ok) {
403
403
  const err = await retryRes.json().catch(() => ({}));
404
404
  throw new CLIError(err.error ?? `Request failed: ${retryRes.status}`, retryRes.status === 403 ? 5 : 1);
@@ -696,7 +696,11 @@ function registerProjectsCommands(projectsCmd2) {
696
696
  }
697
697
 
698
698
  // src/commands/projects/link.ts
699
- import * as clack6 from "@clack/prompts";
699
+ import { exec as exec3 } from "child_process";
700
+ import { promisify as promisify3 } from "util";
701
+ import * as fs4 from "fs/promises";
702
+ import * as path4 from "path";
703
+ import * as clack8 from "@clack/prompts";
700
704
 
701
705
  // src/lib/skills.ts
702
706
  import { exec } from "child_process";
@@ -838,146 +842,13 @@ async function shutdownAnalytics() {
838
842
  }
839
843
  }
840
844
 
841
- // src/commands/projects/link.ts
842
- function buildOssHost(appkey, region) {
843
- return `https://${appkey}.${region}.insforge.app`;
844
- }
845
- function registerProjectLinkCommand(program2) {
846
- 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("--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) => {
847
- const { json, apiUrl } = getRootOpts(cmd);
848
- try {
849
- if (opts.apiBaseUrl || opts.apiKey) {
850
- try {
851
- if (!opts.apiBaseUrl || !opts.apiKey) {
852
- throw new CLIError("Both --api-base-url and --api-key must be provided together for direct linking.");
853
- }
854
- try {
855
- new URL(opts.apiBaseUrl);
856
- } catch {
857
- throw new CLIError("Invalid --api-base-url. Please provide a valid URL.");
858
- }
859
- const projectConfig2 = {
860
- project_id: "oss-project",
861
- project_name: "oss-project",
862
- org_id: "oss-org",
863
- appkey: "oss",
864
- region: "local",
865
- api_key: opts.apiKey,
866
- oss_host: opts.apiBaseUrl.replace(/\/$/, "")
867
- // remove trailing slash if any
868
- };
869
- saveProjectConfig(projectConfig2);
870
- if (json) {
871
- outputJson({ success: true, project: { id: projectConfig2.project_id, name: projectConfig2.project_name, region: projectConfig2.region } });
872
- } else {
873
- outputSuccess(`Linked to direct project at ${projectConfig2.oss_host}`);
874
- }
875
- trackCommand("link", "oss-org", { direct: true });
876
- await installSkills(json);
877
- await reportCliUsage("cli.link_direct", true, 6);
878
- try {
879
- const urlMatch = opts.apiBaseUrl.match(/^https?:\/\/([^.]+)\.[^.]+\.insforge\.app/);
880
- if (urlMatch) {
881
- await reportAgentConnected({ app_key: urlMatch[1] }, apiUrl);
882
- }
883
- } catch {
884
- }
885
- return;
886
- } catch (err) {
887
- await reportCliUsage("cli.link_direct", false);
888
- handleError(err, json);
889
- }
890
- }
891
- const creds = await requireAuth(apiUrl, false);
892
- let orgId = opts.orgId;
893
- let projectId = opts.projectId;
894
- if (!orgId && !projectId) {
895
- const orgs = await listOrganizations(apiUrl);
896
- if (orgs.length === 0) {
897
- throw new CLIError("No organizations found.");
898
- }
899
- if (json) {
900
- throw new CLIError("Specify --org-id in JSON mode.");
901
- }
902
- const selected = await clack6.select({
903
- message: "Select an organization:",
904
- options: orgs.map((o) => ({
905
- value: o.id,
906
- label: o.name
907
- }))
908
- });
909
- if (clack6.isCancel(selected)) process.exit(0);
910
- orgId = selected;
911
- }
912
- const config = getGlobalConfig();
913
- config.default_org_id = orgId;
914
- saveGlobalConfig(config);
915
- if (!projectId) {
916
- const projects = await listProjects(orgId, apiUrl);
917
- if (projects.length === 0) {
918
- throw new CLIError("No projects found in this organization.");
919
- }
920
- if (json) {
921
- throw new CLIError("Specify --project-id in JSON mode.");
922
- }
923
- const selected = await clack6.select({
924
- message: "Select a project to link:",
925
- options: projects.map((p) => ({
926
- value: p.id,
927
- label: `${p.name} (${p.region}, ${p.status})`
928
- }))
929
- });
930
- if (clack6.isCancel(selected)) process.exit(0);
931
- projectId = selected;
932
- }
933
- let project;
934
- let apiKey;
935
- try {
936
- [project, apiKey] = await Promise.all([
937
- getProject(projectId, apiUrl),
938
- getProjectApiKey(projectId, apiUrl)
939
- ]);
940
- } catch (err) {
941
- if (err instanceof CLIError && (err.exitCode === 5 || err.exitCode === 4 || err.message.includes("not found"))) {
942
- const identity = creds.user?.email ?? creds.user?.name ?? "unknown user";
943
- throw new CLIError(
944
- `You're logged in as ${identity}, and you don't have access to project ${projectId}. Check that the project ID is correct and belongs to one of your organizations.`,
945
- 5,
946
- "PERMISSION_DENIED"
947
- );
948
- }
949
- throw err;
950
- }
951
- const projectConfig = {
952
- project_id: project.id,
953
- project_name: project.name,
954
- org_id: project.organization_id,
955
- appkey: project.appkey,
956
- region: project.region,
957
- api_key: apiKey,
958
- oss_host: buildOssHost(project.appkey, project.region)
959
- };
960
- saveProjectConfig(projectConfig);
961
- trackCommand("link", project.organization_id);
962
- if (json) {
963
- outputJson({ success: true, project: { id: project.id, name: project.name, region: project.region } });
964
- } else {
965
- outputSuccess(`Linked to project "${project.name}" (${project.appkey}.${project.region})`);
966
- }
967
- await installSkills(json);
968
- await reportCliUsage("cli.link", true, 6);
969
- try {
970
- await reportAgentConnected({ project_id: project.id }, apiUrl);
971
- } catch {
972
- }
973
- } catch (err) {
974
- await reportCliUsage("cli.link", false);
975
- handleError(err, json);
976
- } finally {
977
- await shutdownAnalytics();
978
- }
979
- });
980
- }
845
+ // src/commands/create.ts
846
+ import { exec as exec2 } from "child_process";
847
+ import { tmpdir } from "os";
848
+ import { promisify as promisify2 } from "util";
849
+ import * as fs3 from "fs/promises";
850
+ import * as path3 from "path";
851
+ import * as clack7 from "@clack/prompts";
981
852
 
982
853
  // src/lib/api/oss.ts
983
854
  function requireProjectConfig() {
@@ -1002,14 +873,14 @@ async function getAnonKey() {
1002
873
  const data = await res.json();
1003
874
  return data.accessToken;
1004
875
  }
1005
- async function ossFetch(path4, options = {}) {
876
+ async function ossFetch(path5, options = {}) {
1006
877
  const config = requireProjectConfig();
1007
878
  const headers = {
1008
879
  "Content-Type": "application/json",
1009
880
  Authorization: `Bearer ${config.api_key}`,
1010
881
  ...options.headers ?? {}
1011
882
  };
1012
- const res = await fetch(`${config.oss_host}${path4}`, { ...options, headers });
883
+ const res = await fetch(`${config.oss_host}${path5}`, { ...options, headers });
1013
884
  if (!res.ok) {
1014
885
  const err = await res.json().catch(() => ({}));
1015
886
  let message = err.message ?? err.error ?? `OSS request failed: ${res.status}`;
@@ -1017,7 +888,7 @@ async function ossFetch(path4, options = {}) {
1017
888
  message += `
1018
889
  ${err.nextActions}`;
1019
890
  }
1020
- if (res.status === 404 && path4.startsWith("/api/compute")) {
891
+ if (res.status === 404 && path5.startsWith("/api/compute")) {
1021
892
  message = "Compute services are not available on this backend.\nSelf-hosted: upgrade your InsForge instance. Cloud: contact your InsForge admin to enable compute.";
1022
893
  }
1023
894
  throw new CLIError(message);
@@ -1025,240 +896,1183 @@ ${err.nextActions}`;
1025
896
  return res;
1026
897
  }
1027
898
 
1028
- // src/commands/db/query.ts
1029
- function registerDbCommands(dbCmd2) {
1030
- dbCmd2.command("query <sql>").description("Execute a SQL query against the database").option("--unrestricted", "Use unrestricted mode (allows system table access)").action(async (sql, opts, cmd) => {
1031
- const { json } = getRootOpts(cmd);
1032
- try {
1033
- await requireAuth();
1034
- const { rows, raw } = await runRawSql(sql, !!opts.unrestricted);
1035
- if (json) {
1036
- outputJson(raw);
1037
- } else {
1038
- if (rows.length > 0) {
1039
- const headers = Object.keys(rows[0]);
1040
- outputTable(
1041
- headers,
1042
- rows.map((row) => headers.map((h) => String(row[h] ?? "")))
1043
- );
1044
- console.log(`${rows.length} row(s) returned.`);
1045
- } else {
1046
- console.log("Query executed successfully.");
1047
- if (rows.length === 0) {
1048
- console.log("No rows returned.");
1049
- }
1050
- }
899
+ // src/lib/env.ts
900
+ import * as fs from "fs/promises";
901
+ import * as path from "path";
902
+ async function readEnvFile(cwd) {
903
+ const candidates = [".env.local", ".env.production", ".env"];
904
+ for (const name of candidates) {
905
+ const filePath = path.join(cwd, name);
906
+ const exists = await fs.stat(filePath).catch(() => null);
907
+ if (!exists) continue;
908
+ const content = await fs.readFile(filePath, "utf-8");
909
+ const vars = [];
910
+ for (const line of content.split("\n")) {
911
+ const trimmed = line.trim();
912
+ if (!trimmed || trimmed.startsWith("#")) continue;
913
+ const eqIndex = trimmed.indexOf("=");
914
+ if (eqIndex === -1) continue;
915
+ const key = trimmed.slice(0, eqIndex).trim();
916
+ let value = trimmed.slice(eqIndex + 1).trim();
917
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
918
+ value = value.slice(1, -1);
1051
919
  }
1052
- await reportCliUsage("cli.db.query", true);
1053
- } catch (err) {
1054
- await reportCliUsage("cli.db.query", false);
1055
- handleError(err, json);
920
+ if (key) vars.push({ key, value });
1056
921
  }
1057
- });
922
+ return vars;
923
+ }
924
+ return [];
1058
925
  }
1059
926
 
1060
- // src/commands/db/tables.ts
1061
- function registerDbTablesCommand(dbCmd2) {
1062
- dbCmd2.command("tables").description("List all database tables").action(async (_opts, cmd) => {
1063
- const { json } = getRootOpts(cmd);
1064
- try {
1065
- await requireAuth();
1066
- const res = await ossFetch("/api/database/tables");
1067
- const tables = await res.json();
1068
- if (json) {
1069
- outputJson(tables);
1070
- } else {
1071
- if (tables.length === 0) {
1072
- console.log("No tables found.");
1073
- return;
1074
- }
1075
- outputTable(
1076
- ["Table Name"],
1077
- tables.map((t) => [t])
1078
- );
1079
- }
1080
- await reportCliUsage("cli.db.tables", true);
1081
- } catch (err) {
1082
- await reportCliUsage("cli.db.tables", false);
1083
- handleError(err, json);
927
+ // src/commands/deployments/deploy.ts
928
+ import * as path2 from "path";
929
+ import * as fs2 from "fs/promises";
930
+ import * as clack6 from "@clack/prompts";
931
+ import archiver from "archiver";
932
+ var POLL_INTERVAL_MS = 5e3;
933
+ var POLL_TIMEOUT_MS = 3e5;
934
+ var EXCLUDE_PATTERNS = [
935
+ "node_modules",
936
+ ".git",
937
+ ".next",
938
+ ".env",
939
+ ".env.local",
940
+ "dist",
941
+ "build",
942
+ ".DS_Store",
943
+ ".insforge",
944
+ // IDE and AI agent configs
945
+ ".claude",
946
+ ".agents",
947
+ ".augment",
948
+ ".kilocode",
949
+ ".kiro",
950
+ ".qoder",
951
+ ".qwen",
952
+ ".roo",
953
+ ".trae",
954
+ ".windsurf",
955
+ ".vercel",
956
+ ".turbo",
957
+ ".cache",
958
+ "skills",
959
+ "coverage"
960
+ ];
961
+ function shouldExclude(name) {
962
+ const normalized = name.replace(/\\/g, "/");
963
+ for (const pattern of EXCLUDE_PATTERNS) {
964
+ if (normalized === pattern || normalized.startsWith(pattern + "/") || normalized.endsWith("/" + pattern) || normalized.includes("/" + pattern + "/")) {
965
+ return true;
1084
966
  }
1085
- });
967
+ }
968
+ if (normalized.endsWith(".log")) return true;
969
+ return false;
1086
970
  }
1087
-
1088
- // src/commands/db/functions.ts
1089
- function registerDbFunctionsCommand(dbCmd2) {
1090
- dbCmd2.command("functions").description("List all database functions").action(async (_opts, cmd) => {
1091
- const { json } = getRootOpts(cmd);
1092
- try {
1093
- await requireAuth();
1094
- const res = await ossFetch("/api/database/functions");
1095
- const raw = await res.json();
1096
- const functions = Array.isArray(raw) ? raw : raw.functions ?? [];
1097
- if (json) {
1098
- outputJson(raw);
1099
- } else {
1100
- if (functions.length === 0) {
1101
- console.log("No database functions found.");
1102
- return;
1103
- }
1104
- outputTable(
1105
- ["Name", "Definition", "Kind"],
1106
- functions.map((f) => [f.functionName, f.functionDef, f.kind])
1107
- );
1108
- }
1109
- await reportCliUsage("cli.db.functions", true);
1110
- } catch (err) {
1111
- await reportCliUsage("cli.db.functions", false);
1112
- handleError(err, json);
1113
- }
971
+ async function createZipBuffer(sourceDir) {
972
+ return new Promise((resolve4, reject) => {
973
+ const archive = archiver("zip", { zlib: { level: 9 } });
974
+ const chunks = [];
975
+ archive.on("data", (chunk) => chunks.push(chunk));
976
+ archive.on("end", () => resolve4(Buffer.concat(chunks)));
977
+ archive.on("error", (err) => reject(err));
978
+ archive.directory(sourceDir, false, (entry) => {
979
+ if (shouldExclude(entry.name)) return false;
980
+ return entry;
981
+ });
982
+ void archive.finalize();
1114
983
  });
1115
984
  }
1116
-
1117
- // src/commands/db/indexes.ts
1118
- function registerDbIndexesCommand(dbCmd2) {
1119
- dbCmd2.command("indexes").description("List all database indexes").action(async (_opts, cmd) => {
1120
- const { json } = getRootOpts(cmd);
985
+ async function deployProject(opts) {
986
+ const { sourceDir, startBody = {}, spinner: s } = opts;
987
+ s?.start("Creating deployment...");
988
+ const createRes = await ossFetch("/api/deployments", { method: "POST" });
989
+ const { id: deploymentId, uploadUrl, uploadFields } = await createRes.json();
990
+ s?.message("Compressing source files...");
991
+ const zipBuffer = await createZipBuffer(sourceDir);
992
+ s?.message("Uploading...");
993
+ const formData = new FormData();
994
+ for (const [key, value] of Object.entries(uploadFields)) {
995
+ formData.append(key, value);
996
+ }
997
+ formData.append(
998
+ "file",
999
+ new Blob([zipBuffer], { type: "application/zip" }),
1000
+ "deployment.zip"
1001
+ );
1002
+ const uploadRes = await fetch(uploadUrl, { method: "POST", body: formData });
1003
+ if (!uploadRes.ok) {
1004
+ const uploadErr = await uploadRes.text();
1005
+ throw new CLIError(`Failed to upload: ${uploadErr}`);
1006
+ }
1007
+ s?.message("Starting deployment...");
1008
+ const startRes = await ossFetch(`/api/deployments/${deploymentId}/start`, {
1009
+ method: "POST",
1010
+ body: JSON.stringify(startBody)
1011
+ });
1012
+ await startRes.json();
1013
+ s?.message("Building and deploying...");
1014
+ const startTime = Date.now();
1015
+ let deployment = null;
1016
+ while (Date.now() - startTime < POLL_TIMEOUT_MS) {
1017
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
1121
1018
  try {
1122
- await requireAuth();
1123
- const res = await ossFetch("/api/database/indexes");
1124
- const raw = await res.json();
1125
- const indexes = Array.isArray(raw) ? raw : raw.indexes ?? [];
1126
- if (json) {
1127
- outputJson(raw);
1128
- } else {
1129
- if (indexes.length === 0) {
1130
- console.log("No database indexes found.");
1131
- return;
1132
- }
1133
- outputTable(
1134
- ["Table", "Index Name", "Definition", "Unique", "Primary"],
1135
- indexes.map((i) => [
1136
- i.tableName,
1137
- i.indexName,
1138
- i.indexDef,
1139
- i.isUnique ? "Yes" : "No",
1140
- i.isPrimary ? "Yes" : "No"
1141
- ])
1142
- );
1019
+ const statusRes = await ossFetch(`/api/deployments/${deploymentId}`);
1020
+ deployment = await statusRes.json();
1021
+ const status = deployment.status.toUpperCase();
1022
+ if (status === "READY") {
1023
+ break;
1143
1024
  }
1144
- await reportCliUsage("cli.db.indexes", true);
1025
+ if (status === "ERROR" || status === "CANCELED") {
1026
+ s?.stop("Deployment failed");
1027
+ throw new CLIError(getDeploymentError(deployment.metadata) ?? `Deployment failed with status: ${deployment.status}`);
1028
+ }
1029
+ const elapsed = Math.round((Date.now() - startTime) / 1e3);
1030
+ s?.message(`Building and deploying... (${elapsed}s, status: ${deployment.status})`);
1145
1031
  } catch (err) {
1146
- await reportCliUsage("cli.db.indexes", false);
1147
- handleError(err, json);
1032
+ if (err instanceof CLIError) throw err;
1148
1033
  }
1149
- });
1034
+ }
1035
+ const isReady = deployment?.status.toUpperCase() === "READY";
1036
+ const liveUrl = isReady ? deployment?.url ?? null : null;
1037
+ return { deploymentId, deployment, isReady, liveUrl };
1150
1038
  }
1151
-
1152
- // src/commands/db/policies.ts
1153
- function registerDbPoliciesCommand(dbCmd2) {
1154
- dbCmd2.command("policies").description("List all RLS policies").action(async (_opts, cmd) => {
1039
+ function registerDeploymentsDeployCommand(deploymentsCmd2) {
1040
+ deploymentsCmd2.command("deploy [directory]").description("Deploy a frontend project to Vercel").option("--env <vars>", `Environment variables as JSON (e.g. '{"KEY":"value"}')`).option("--meta <meta>", "Deployment metadata as JSON").action(async (directory, opts, cmd) => {
1155
1041
  const { json } = getRootOpts(cmd);
1156
1042
  try {
1157
1043
  await requireAuth();
1158
- const res = await ossFetch("/api/database/policies");
1159
- const raw = await res.json();
1160
- const policies = Array.isArray(raw) ? raw : raw.policies ?? [];
1161
- if (json) {
1162
- outputJson(raw);
1163
- } else {
1164
- if (policies.length === 0) {
1165
- console.log("No RLS policies found.");
1166
- return;
1044
+ const config = getProjectConfig();
1045
+ if (!config) throw new ProjectNotLinkedError();
1046
+ const sourceDir = path2.resolve(directory ?? ".");
1047
+ const stats = await fs2.stat(sourceDir).catch(() => null);
1048
+ if (!stats?.isDirectory()) {
1049
+ throw new CLIError(`"${sourceDir}" is not a valid directory.`);
1050
+ }
1051
+ const dirName = path2.basename(sourceDir);
1052
+ if (EXCLUDE_PATTERNS.includes(dirName)) {
1053
+ throw new CLIError(`"${dirName}" is an excluded directory and cannot be used as a deploy source. Please specify your project root or output directory instead.`);
1054
+ }
1055
+ const s = !json ? clack6.spinner() : null;
1056
+ const startBody = {};
1057
+ if (opts.env) {
1058
+ try {
1059
+ const parsed = JSON.parse(opts.env);
1060
+ if (Array.isArray(parsed)) {
1061
+ startBody.envVars = parsed;
1062
+ } else {
1063
+ startBody.envVars = Object.entries(parsed).map(([key, value]) => ({ key, value }));
1064
+ }
1065
+ } catch {
1066
+ throw new CLIError("Invalid --env JSON.");
1167
1067
  }
1168
- outputTable(
1169
- ["Table", "Policy Name", "Command", "Roles", "Qual", "With Check"],
1170
- policies.map((p) => [
1171
- String(p.tableName ?? "-"),
1172
- String(p.policyName ?? "-"),
1173
- String(p.cmd ?? "-"),
1174
- Array.isArray(p.roles) ? p.roles.join(", ") : String(p.roles ?? "-"),
1175
- String(p.qual ?? "-"),
1176
- String(p.withCheck ?? "-")
1177
- ])
1178
- );
1179
1068
  }
1180
- await reportCliUsage("cli.db.policies", true);
1181
- } catch (err) {
1182
- await reportCliUsage("cli.db.policies", false);
1183
- handleError(err, json);
1184
- }
1185
- });
1186
- }
1187
-
1188
- // src/commands/db/triggers.ts
1189
- function registerDbTriggersCommand(dbCmd2) {
1190
- dbCmd2.command("triggers").description("List all database triggers").action(async (_opts, cmd) => {
1191
- const { json } = getRootOpts(cmd);
1192
- try {
1193
- await requireAuth();
1194
- const res = await ossFetch("/api/database/triggers");
1195
- const raw = await res.json();
1196
- const triggers = Array.isArray(raw) ? raw : raw.triggers ?? [];
1197
- if (json) {
1198
- outputJson(raw);
1199
- } else {
1200
- if (triggers.length === 0) {
1201
- console.log("No database triggers found.");
1202
- return;
1069
+ if (opts.meta) {
1070
+ try {
1071
+ startBody.meta = JSON.parse(opts.meta);
1072
+ } catch {
1073
+ throw new CLIError("Invalid --meta JSON.");
1203
1074
  }
1204
- outputTable(
1205
- ["Name", "Table", "Timing", "Events", "ActionOrientation", "ActionCondition", "ActionStatement"],
1206
- triggers.map((t) => [
1207
- t.triggerName,
1208
- t.tableName,
1209
- t.actionTiming,
1210
- t.eventManipulation,
1211
- t.actionOrientation,
1212
- t.actionCondition ?? "-",
1213
- t.actionStatement
1214
- ])
1215
- );
1216
1075
  }
1217
- await reportCliUsage("cli.db.triggers", true);
1218
- } catch (err) {
1219
- await reportCliUsage("cli.db.triggers", false);
1220
- handleError(err, json);
1221
- }
1222
- });
1223
- }
1224
-
1225
- // src/commands/db/rpc.ts
1226
- function registerDbRpcCommand(dbCmd2) {
1227
- dbCmd2.command("rpc <functionName>").description("Call a database function via RPC").option("--data <json>", "JSON body to pass as function parameters").action(async (functionName, opts, cmd) => {
1228
- const { json } = getRootOpts(cmd);
1229
- try {
1230
- await requireAuth();
1231
- const body = opts.data ? JSON.stringify(JSON.parse(opts.data)) : void 0;
1232
- const res = await ossFetch(`/api/database/rpc/${encodeURIComponent(functionName)}`, {
1233
- method: body ? "POST" : "GET",
1234
- ...body ? { body } : {}
1235
- });
1236
- const result = await res.json();
1237
- if (json) {
1238
- outputJson(result);
1076
+ const result = await deployProject({ sourceDir, startBody, spinner: s });
1077
+ if (result.isReady) {
1078
+ s?.stop("Deployment complete");
1079
+ if (json) {
1080
+ outputJson(result.deployment);
1081
+ } else {
1082
+ if (result.liveUrl) {
1083
+ clack6.log.success(`Live at: ${result.liveUrl}`);
1084
+ }
1085
+ clack6.log.info(`Deployment ID: ${result.deploymentId}`);
1086
+ }
1239
1087
  } else {
1240
- console.log(JSON.stringify(result, null, 2));
1088
+ s?.stop("Deployment is still building");
1089
+ if (json) {
1090
+ outputJson({ id: result.deploymentId, status: result.deployment?.status ?? "building", timedOut: true });
1091
+ } else {
1092
+ clack6.log.info(`Deployment ID: ${result.deploymentId}`);
1093
+ clack6.log.warn("Deployment did not finish within 5 minutes.");
1094
+ clack6.log.info(`Check status with: npx @insforge/cli deployments status ${result.deploymentId}`);
1095
+ }
1241
1096
  }
1242
- await reportCliUsage("cli.db.rpc", true);
1097
+ await reportCliUsage("cli.deployments.deploy", true);
1243
1098
  } catch (err) {
1099
+ await reportCliUsage("cli.deployments.deploy", false);
1244
1100
  handleError(err, json);
1245
1101
  }
1246
1102
  });
1247
1103
  }
1248
1104
 
1249
- // src/commands/db/export.ts
1250
- import { writeFileSync as writeFileSync2 } from "fs";
1251
- function registerDbExportCommand(dbCmd2) {
1252
- 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) => {
1253
- const { json } = getRootOpts(cmd);
1254
- try {
1255
- await requireAuth();
1256
- const body = {
1257
- format: opts.format,
1258
- includeData: opts.data !== false
1259
- };
1260
- if (opts.tables) {
1261
- body.tables = opts.tables.split(",").map((t) => t.trim());
1105
+ // src/commands/create.ts
1106
+ var execAsync2 = promisify2(exec2);
1107
+ function buildOssHost(appkey, region) {
1108
+ return `https://${appkey}.${region}.insforge.app`;
1109
+ }
1110
+ async function waitForProjectActive(projectId, apiUrl, timeoutMs = 12e4) {
1111
+ const start = Date.now();
1112
+ while (Date.now() - start < timeoutMs) {
1113
+ const project = await getProject(projectId, apiUrl);
1114
+ if (project.status === "active") return;
1115
+ await new Promise((r) => setTimeout(r, 3e3));
1116
+ }
1117
+ throw new CLIError("Project creation timed out. Check the dashboard for status.");
1118
+ }
1119
+ var INSFORGE_BANNER = [
1120
+ "\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",
1121
+ "\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",
1122
+ "\u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2557 ",
1123
+ "\u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551\u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255D ",
1124
+ "\u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557",
1125
+ "\u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D"
1126
+ ];
1127
+ async function animateBanner() {
1128
+ const isTTY = process.stderr.isTTY;
1129
+ if (!isTTY || process.env.CI) {
1130
+ for (const line of INSFORGE_BANNER) {
1131
+ process.stderr.write(`${line}
1132
+ `);
1133
+ }
1134
+ process.stderr.write("\n");
1135
+ return;
1136
+ }
1137
+ const totalLines = INSFORGE_BANNER.length;
1138
+ const maxLen = Math.max(...INSFORGE_BANNER.map((l) => l.length));
1139
+ const cols = process.stderr.columns ?? 0;
1140
+ if (cols > 0 && cols < maxLen) {
1141
+ for (const line of INSFORGE_BANNER) {
1142
+ process.stderr.write(`\x1B[97m${line}\x1B[0m
1143
+ `);
1144
+ }
1145
+ process.stderr.write("\n");
1146
+ return;
1147
+ }
1148
+ const REVEAL_STEPS = 10;
1149
+ const REVEAL_DELAY = 30;
1150
+ for (let lineIdx = 0; lineIdx < totalLines; lineIdx++) {
1151
+ const line = INSFORGE_BANNER[lineIdx];
1152
+ for (let step = 0; step <= REVEAL_STEPS; step++) {
1153
+ const pos = Math.floor(step / REVEAL_STEPS * line.length);
1154
+ let rendered = "";
1155
+ for (let i = 0; i < line.length; i++) {
1156
+ if (i < pos) {
1157
+ rendered += `\x1B[97m${line[i]}\x1B[0m`;
1158
+ } else if (i === pos) {
1159
+ rendered += `\x1B[1;37m${line[i]}\x1B[0m`;
1160
+ } else {
1161
+ rendered += `\x1B[90m${line[i]}\x1B[0m`;
1162
+ }
1163
+ }
1164
+ process.stderr.write(`\r${rendered}`);
1165
+ await new Promise((r) => setTimeout(r, REVEAL_DELAY));
1166
+ }
1167
+ process.stderr.write("\n");
1168
+ }
1169
+ const SHIMMER_STEPS = 16;
1170
+ const SHIMMER_DELAY = 40;
1171
+ const SHIMMER_WIDTH = 4;
1172
+ for (let step = 0; step < SHIMMER_STEPS; step++) {
1173
+ const shimmerPos = Math.floor(step / SHIMMER_STEPS * (maxLen + SHIMMER_WIDTH));
1174
+ process.stderr.write(`\x1B[${totalLines}A`);
1175
+ for (const line of INSFORGE_BANNER) {
1176
+ let rendered = "";
1177
+ for (let i = 0; i < line.length; i++) {
1178
+ const dist = Math.abs(i - shimmerPos);
1179
+ if (dist === 0) {
1180
+ rendered += `\x1B[1;97m${line[i]}\x1B[0m`;
1181
+ } else if (dist <= SHIMMER_WIDTH) {
1182
+ rendered += `\x1B[37m${line[i]}\x1B[0m`;
1183
+ } else {
1184
+ rendered += `\x1B[90m${line[i]}\x1B[0m`;
1185
+ }
1186
+ }
1187
+ process.stderr.write(`${rendered}
1188
+ `);
1189
+ }
1190
+ await new Promise((r) => setTimeout(r, SHIMMER_DELAY));
1191
+ }
1192
+ process.stderr.write(`\x1B[${totalLines}A`);
1193
+ for (const line of INSFORGE_BANNER) {
1194
+ process.stderr.write(`\x1B[97m${line}\x1B[0m
1195
+ `);
1196
+ }
1197
+ process.stderr.write("\n");
1198
+ }
1199
+ function getDefaultProjectName() {
1200
+ const dirName = path3.basename(process.cwd());
1201
+ const sanitized = dirName.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
1202
+ return sanitized.length >= 2 ? sanitized : "";
1203
+ }
1204
+ async function copyDir(src, dest) {
1205
+ const entries = await fs3.readdir(src, { withFileTypes: true });
1206
+ for (const entry of entries) {
1207
+ const srcPath = path3.join(src, entry.name);
1208
+ const destPath = path3.join(dest, entry.name);
1209
+ if (entry.isDirectory()) {
1210
+ await fs3.mkdir(destPath, { recursive: true });
1211
+ await copyDir(srcPath, destPath);
1212
+ } else {
1213
+ await fs3.copyFile(srcPath, destPath);
1214
+ }
1215
+ }
1216
+ }
1217
+ function registerCreateCommand(program2) {
1218
+ 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, or empty").action(async (opts, cmd) => {
1219
+ const { json, apiUrl } = getRootOpts(cmd);
1220
+ try {
1221
+ await requireAuth(apiUrl, false);
1222
+ if (!json) {
1223
+ await animateBanner();
1224
+ clack7.intro("Let's build something great");
1225
+ }
1226
+ let orgId = opts.orgId;
1227
+ if (!orgId) {
1228
+ const orgs = await listOrganizations(apiUrl);
1229
+ if (orgs.length === 0) {
1230
+ throw new CLIError("No organizations found.");
1231
+ }
1232
+ if (orgs.length === 1) {
1233
+ orgId = orgs[0].id;
1234
+ if (!json) clack7.log.info(`Using organization: ${orgs[0].name}`);
1235
+ } else {
1236
+ if (json) {
1237
+ throw new CLIError("Multiple organizations found. Specify --org-id.");
1238
+ }
1239
+ const selected = await clack7.select({
1240
+ message: "Select an organization:",
1241
+ options: orgs.map((o) => ({
1242
+ value: o.id,
1243
+ label: o.name
1244
+ }))
1245
+ });
1246
+ if (clack7.isCancel(selected)) process.exit(0);
1247
+ orgId = selected;
1248
+ }
1249
+ }
1250
+ const globalConfig = getGlobalConfig();
1251
+ globalConfig.default_org_id = orgId;
1252
+ saveGlobalConfig(globalConfig);
1253
+ let projectName = opts.name;
1254
+ if (!projectName) {
1255
+ if (json) throw new CLIError("--name is required in JSON mode.");
1256
+ const defaultName = getDefaultProjectName();
1257
+ const name = await clack7.text({
1258
+ message: "Project name:",
1259
+ ...defaultName ? { initialValue: defaultName } : {},
1260
+ validate: (v) => v.length >= 2 ? void 0 : "Name must be at least 2 characters"
1261
+ });
1262
+ if (clack7.isCancel(name)) process.exit(0);
1263
+ projectName = name;
1264
+ }
1265
+ projectName = path3.basename(projectName).replace(/[^a-zA-Z0-9._-]/g, "-").replace(/\.+/g, ".");
1266
+ if (projectName.length < 2 || projectName === "." || projectName === "..") {
1267
+ throw new CLIError("Project name must be at least 2 safe characters (letters, numbers, hyphens).");
1268
+ }
1269
+ const validTemplates = ["react", "nextjs", "chatbot", "crm", "e-commerce", "empty"];
1270
+ let template = opts.template;
1271
+ if (template && !validTemplates.includes(template)) {
1272
+ throw new CLIError(`Invalid template "${template}". Valid options: ${validTemplates.join(", ")}`);
1273
+ }
1274
+ if (!template) {
1275
+ if (json) {
1276
+ template = "empty";
1277
+ } else {
1278
+ const approach = await clack7.select({
1279
+ message: "How would you like to start?",
1280
+ options: [
1281
+ { value: "blank", label: "Blank project", hint: "Start from scratch with .env.local ready" },
1282
+ { value: "template", label: "Start from a template", hint: "Pre-built starter apps" }
1283
+ ]
1284
+ });
1285
+ if (clack7.isCancel(approach)) process.exit(0);
1286
+ captureEvent(orgId, "create_approach_selected", {
1287
+ approach
1288
+ });
1289
+ if (approach === "blank") {
1290
+ template = "empty";
1291
+ } else {
1292
+ const selected = await clack7.select({
1293
+ message: "Choose a starter template:",
1294
+ options: [
1295
+ { value: "react", label: "Web app template with React" },
1296
+ { value: "nextjs", label: "Web app template with Next.js" },
1297
+ { value: "chatbot", label: "AI Chatbot with Next.js" },
1298
+ { value: "crm", label: "CRM with Next.js" },
1299
+ { value: "e-commerce", label: "E-Commerce store with Next.js" }
1300
+ ]
1301
+ });
1302
+ if (clack7.isCancel(selected)) process.exit(0);
1303
+ template = selected;
1304
+ }
1305
+ }
1306
+ }
1307
+ captureEvent(orgId, "template_selected", {
1308
+ template,
1309
+ approach: template === "empty" ? "blank" : "template"
1310
+ });
1311
+ const hasTemplate = template !== "empty";
1312
+ let dirName = null;
1313
+ const originalCwd = process.cwd();
1314
+ let projectDir = originalCwd;
1315
+ if (hasTemplate) {
1316
+ dirName = projectName;
1317
+ if (!json) {
1318
+ const inputDir = await clack7.text({
1319
+ message: "Directory name:",
1320
+ initialValue: projectName,
1321
+ validate: (v) => {
1322
+ if (v.length < 1) return "Directory name is required";
1323
+ const normalized = path3.basename(v).replace(/[^a-zA-Z0-9._-]/g, "-");
1324
+ if (!normalized || normalized === "." || normalized === "..") return "Invalid directory name";
1325
+ return void 0;
1326
+ }
1327
+ });
1328
+ if (clack7.isCancel(inputDir)) process.exit(0);
1329
+ dirName = path3.basename(inputDir).replace(/[^a-zA-Z0-9._-]/g, "-");
1330
+ }
1331
+ if (!dirName || dirName === "." || dirName === "..") {
1332
+ throw new CLIError("Invalid directory name.");
1333
+ }
1334
+ projectDir = path3.resolve(originalCwd, dirName);
1335
+ const dirExists = await fs3.stat(projectDir).catch(() => null);
1336
+ if (dirExists) {
1337
+ throw new CLIError(`Directory "${dirName}" already exists.`);
1338
+ }
1339
+ await fs3.mkdir(projectDir);
1340
+ process.chdir(projectDir);
1341
+ }
1342
+ let projectLinked = false;
1343
+ const s = !json ? clack7.spinner() : null;
1344
+ try {
1345
+ s?.start("Creating project...");
1346
+ const project = await createProject(orgId, projectName, opts.region, apiUrl);
1347
+ s?.message("Waiting for project to become active...");
1348
+ await waitForProjectActive(project.id, apiUrl);
1349
+ const apiKey = await getProjectApiKey(project.id, apiUrl);
1350
+ const projectConfig = {
1351
+ project_id: project.id,
1352
+ project_name: project.name,
1353
+ org_id: project.organization_id,
1354
+ appkey: project.appkey,
1355
+ region: project.region,
1356
+ api_key: apiKey,
1357
+ oss_host: buildOssHost(project.appkey, project.region)
1358
+ };
1359
+ saveProjectConfig(projectConfig);
1360
+ projectLinked = true;
1361
+ s?.stop(`Project "${project.name}" created and linked`);
1362
+ const githubTemplates = ["chatbot", "crm", "e-commerce", "nextjs", "react"];
1363
+ if (githubTemplates.includes(template)) {
1364
+ await downloadGitHubTemplate(template, projectConfig, json);
1365
+ } else if (hasTemplate) {
1366
+ await downloadTemplate(template, projectConfig, projectName, json, apiUrl);
1367
+ } else {
1368
+ try {
1369
+ const anonKey = await getAnonKey();
1370
+ if (!anonKey) {
1371
+ if (!json) clack7.log.warn("Could not retrieve anon key. You can add it to .env.local manually.");
1372
+ } else {
1373
+ const envPath = path3.join(process.cwd(), ".env.local");
1374
+ const envContent = [
1375
+ "# InsForge",
1376
+ `NEXT_PUBLIC_INSFORGE_URL=${projectConfig.oss_host}`,
1377
+ `NEXT_PUBLIC_INSFORGE_ANON_KEY=${anonKey}`,
1378
+ ""
1379
+ ].join("\n");
1380
+ await fs3.writeFile(envPath, envContent, { flag: "wx" });
1381
+ if (!json) {
1382
+ clack7.log.success("Created .env.local with your InsForge credentials");
1383
+ }
1384
+ }
1385
+ } catch (err) {
1386
+ const error = err;
1387
+ if (!json) {
1388
+ if (error.code === "EEXIST") {
1389
+ clack7.log.warn(".env.local already exists; skipping InsForge key seeding.");
1390
+ } else {
1391
+ clack7.log.warn(`Failed to create .env.local: ${error.message}`);
1392
+ }
1393
+ }
1394
+ }
1395
+ }
1396
+ await installSkills(json);
1397
+ trackCommand("create", orgId);
1398
+ await reportCliUsage("cli.create", true, 6);
1399
+ const templateDownloaded = hasTemplate ? await fs3.stat(path3.join(process.cwd(), "package.json")).catch(() => null) : null;
1400
+ if (templateDownloaded) {
1401
+ const installSpinner = !json ? clack7.spinner() : null;
1402
+ installSpinner?.start("Installing dependencies...");
1403
+ try {
1404
+ await execAsync2("npm install", { cwd: process.cwd(), maxBuffer: 10 * 1024 * 1024 });
1405
+ installSpinner?.stop("Dependencies installed");
1406
+ } catch (err) {
1407
+ installSpinner?.stop("Failed to install dependencies");
1408
+ if (!json) {
1409
+ clack7.log.warn(`npm install failed: ${err.message}`);
1410
+ clack7.log.info("Run `npm install` manually to install dependencies.");
1411
+ }
1412
+ }
1413
+ }
1414
+ let liveUrl = null;
1415
+ if (templateDownloaded && !json) {
1416
+ const shouldDeploy = await clack7.confirm({
1417
+ message: "Would you like to deploy now?"
1418
+ });
1419
+ if (!clack7.isCancel(shouldDeploy) && shouldDeploy) {
1420
+ try {
1421
+ const envVars = await readEnvFile(process.cwd());
1422
+ const startBody = {};
1423
+ if (envVars.length > 0) {
1424
+ startBody.envVars = envVars;
1425
+ }
1426
+ const deploySpinner = clack7.spinner();
1427
+ const result = await deployProject({
1428
+ sourceDir: process.cwd(),
1429
+ startBody,
1430
+ spinner: deploySpinner
1431
+ });
1432
+ if (result.isReady) {
1433
+ deploySpinner.stop("Deployment complete");
1434
+ liveUrl = result.liveUrl;
1435
+ } else {
1436
+ deploySpinner.stop("Deployment is still building");
1437
+ clack7.log.info(`Deployment ID: ${result.deploymentId}`);
1438
+ clack7.log.warn("Deployment did not finish within 2 minutes.");
1439
+ clack7.log.info(`Check status with: npx @insforge/cli deployments status ${result.deploymentId}`);
1440
+ }
1441
+ } catch (err) {
1442
+ clack7.log.warn(`Deploy failed: ${err.message}`);
1443
+ }
1444
+ }
1445
+ }
1446
+ const dashboardUrl = `${getFrontendUrl()}/dashboard/project/${project.id}`;
1447
+ if (json) {
1448
+ outputJson({
1449
+ success: true,
1450
+ project: { id: project.id, name: project.name, appkey: project.appkey, region: project.region },
1451
+ template,
1452
+ ...dirName ? { directory: dirName } : {},
1453
+ urls: {
1454
+ dashboard: dashboardUrl,
1455
+ ...liveUrl ? { liveSite: liveUrl } : {}
1456
+ }
1457
+ });
1458
+ } else {
1459
+ clack7.log.step(`Dashboard: ${dashboardUrl}`);
1460
+ if (liveUrl) {
1461
+ clack7.log.success(`Live site: ${liveUrl}`);
1462
+ }
1463
+ if (templateDownloaded) {
1464
+ const steps = [
1465
+ `cd ${dirName}`,
1466
+ "npm run dev"
1467
+ ];
1468
+ clack7.note(steps.join("\n"), "Next steps");
1469
+ clack7.note("Open your coding agent (Claude Code, Codex, Cursor, etc.) to add new features.", "Keep building");
1470
+ } else if (hasTemplate && !templateDownloaded) {
1471
+ clack7.log.warn("Template download failed. You can retry or set up manually.");
1472
+ } else {
1473
+ const prompts = [
1474
+ "Build a todo app with Google OAuth sign-in",
1475
+ "Build an Instagram clone where users can upload photos, like, and comment",
1476
+ "Build an AI chatbot with conversation history"
1477
+ ];
1478
+ clack7.note(
1479
+ `Open your coding agent (Claude Code, Codex, Cursor, etc.) and try:
1480
+
1481
+ ${prompts.map((p) => `\u2022 "${p}"`).join("\n")}`,
1482
+ "Start building"
1483
+ );
1484
+ }
1485
+ clack7.outro("Done!");
1486
+ }
1487
+ } catch (err) {
1488
+ if (!projectLinked && hasTemplate && projectDir !== originalCwd) {
1489
+ process.chdir(originalCwd);
1490
+ await fs3.rm(projectDir, { recursive: true, force: true }).catch(() => {
1491
+ });
1492
+ }
1493
+ throw err;
1494
+ }
1495
+ } catch (err) {
1496
+ handleError(err, json);
1497
+ } finally {
1498
+ await shutdownAnalytics();
1499
+ }
1500
+ });
1501
+ }
1502
+ async function downloadTemplate(framework, projectConfig, projectName, json, _apiUrl) {
1503
+ const s = !json ? clack7.spinner() : null;
1504
+ s?.start("Downloading template...");
1505
+ try {
1506
+ const anonKey = await getAnonKey();
1507
+ if (!anonKey) {
1508
+ throw new Error("Failed to retrieve anon key from backend");
1509
+ }
1510
+ const tempDir = tmpdir();
1511
+ const targetDir = projectName;
1512
+ const templatePath = path3.join(tempDir, targetDir);
1513
+ try {
1514
+ await fs3.rm(templatePath, { recursive: true, force: true });
1515
+ } catch {
1516
+ }
1517
+ const frame = framework === "nextjs" ? "nextjs" : "react";
1518
+ const esc = (s2) => process.platform === "win32" ? `"${s2.replace(/"/g, '\\"')}"` : `'${s2.replace(/'/g, "'\\''")}'`;
1519
+ const command = `npx --yes create-insforge-app@latest ${esc(targetDir)} --frame ${frame} --base-url ${esc(projectConfig.oss_host)} --anon-key ${esc(anonKey)} --skip-install`;
1520
+ s?.message(`Running create-insforge-app (${frame})...`);
1521
+ await execAsync2(command, {
1522
+ maxBuffer: 10 * 1024 * 1024,
1523
+ cwd: tempDir
1524
+ });
1525
+ s?.message("Copying template files...");
1526
+ const cwd = process.cwd();
1527
+ await copyDir(templatePath, cwd);
1528
+ await fs3.rm(templatePath, { recursive: true, force: true }).catch(() => {
1529
+ });
1530
+ s?.stop("Template files downloaded");
1531
+ } catch (err) {
1532
+ s?.stop("Template download failed");
1533
+ if (!json) {
1534
+ clack7.log.warn(`Failed to download template: ${err.message}`);
1535
+ clack7.log.info("You can manually set up the template later.");
1536
+ }
1537
+ }
1538
+ }
1539
+ async function downloadGitHubTemplate(templateName, projectConfig, json) {
1540
+ const s = !json ? clack7.spinner() : null;
1541
+ s?.start(`Downloading ${templateName} template...`);
1542
+ const tempDir = path3.join(tmpdir(), `insforge-template-${Date.now()}`);
1543
+ try {
1544
+ await fs3.mkdir(tempDir, { recursive: true });
1545
+ await execAsync2(
1546
+ "git clone --depth 1 https://github.com/InsForge/insforge-templates.git .",
1547
+ { cwd: tempDir, maxBuffer: 10 * 1024 * 1024, timeout: 6e4 }
1548
+ );
1549
+ const templateDir = path3.join(tempDir, templateName);
1550
+ const stat5 = await fs3.stat(templateDir).catch(() => null);
1551
+ if (!stat5?.isDirectory()) {
1552
+ throw new Error(`Template "${templateName}" not found in repository`);
1553
+ }
1554
+ s?.message("Copying template files...");
1555
+ const cwd = process.cwd();
1556
+ await copyDir(templateDir, cwd);
1557
+ const envExamplePath = path3.join(cwd, ".env.example");
1558
+ const envExampleExists = await fs3.stat(envExamplePath).catch(() => null);
1559
+ if (envExampleExists) {
1560
+ const anonKey = await getAnonKey();
1561
+ const envExample = await fs3.readFile(envExamplePath, "utf-8");
1562
+ const envContent = envExample.replace(
1563
+ /^([A-Z][A-Z0-9_]*=)(.*)$/gm,
1564
+ (_, prefix, _value) => {
1565
+ const key = prefix.slice(0, -1);
1566
+ if (/INSFORGE.*(URL|BASE_URL)$/.test(key)) return `${prefix}${projectConfig.oss_host}`;
1567
+ if (/INSFORGE.*ANON_KEY$/.test(key)) return `${prefix}${anonKey}`;
1568
+ if (key === "NEXT_PUBLIC_APP_URL") return `${prefix}https://${projectConfig.appkey}.insforge.site`;
1569
+ return `${prefix}${_value}`;
1570
+ }
1571
+ );
1572
+ const envLocalPath = path3.join(cwd, ".env.local");
1573
+ try {
1574
+ await fs3.writeFile(envLocalPath, envContent, { flag: "wx" });
1575
+ } catch (e) {
1576
+ if (e.code === "EEXIST") {
1577
+ if (!json) clack7.log.warn(".env.local already exists; skipping env seeding.");
1578
+ } else {
1579
+ throw e;
1580
+ }
1581
+ }
1582
+ }
1583
+ s?.stop(`${templateName} template downloaded`);
1584
+ const migrationPath = path3.join(cwd, "migrations", "db_init.sql");
1585
+ const migrationExists = await fs3.stat(migrationPath).catch(() => null);
1586
+ if (migrationExists) {
1587
+ const dbSpinner = !json ? clack7.spinner() : null;
1588
+ dbSpinner?.start("Running database migrations...");
1589
+ try {
1590
+ const sql = await fs3.readFile(migrationPath, "utf-8");
1591
+ await runRawSql(sql, true);
1592
+ dbSpinner?.stop("Database migrations applied");
1593
+ } catch (err) {
1594
+ dbSpinner?.stop("Database migration failed");
1595
+ if (!json) {
1596
+ clack7.log.warn(`Migration failed: ${err.message}`);
1597
+ clack7.log.info('You can run the migration manually: npx @insforge/cli db query --unrestricted "$(cat migrations/db_init.sql)"');
1598
+ } else {
1599
+ throw err;
1600
+ }
1601
+ }
1602
+ }
1603
+ } catch (err) {
1604
+ s?.stop(`${templateName} template download failed`);
1605
+ if (!json) {
1606
+ clack7.log.warn(`Failed to download ${templateName} template: ${err.message}`);
1607
+ clack7.log.info("You can manually clone from: https://github.com/InsForge/insforge-templates");
1608
+ }
1609
+ } finally {
1610
+ await fs3.rm(tempDir, { recursive: true, force: true }).catch(() => {
1611
+ });
1612
+ }
1613
+ }
1614
+
1615
+ // src/commands/projects/link.ts
1616
+ var execAsync3 = promisify3(exec3);
1617
+ function buildOssHost2(appkey, region) {
1618
+ return `https://${appkey}.${region}.insforge.app`;
1619
+ }
1620
+ function registerProjectLinkCommand(program2) {
1621
+ 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").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) => {
1622
+ const { json, apiUrl } = getRootOpts(cmd);
1623
+ try {
1624
+ if (opts.apiBaseUrl || opts.apiKey) {
1625
+ try {
1626
+ if (!opts.apiBaseUrl || !opts.apiKey) {
1627
+ throw new CLIError("Both --api-base-url and --api-key must be provided together for direct linking.");
1628
+ }
1629
+ try {
1630
+ new URL(opts.apiBaseUrl);
1631
+ } catch {
1632
+ throw new CLIError("Invalid --api-base-url. Please provide a valid URL.");
1633
+ }
1634
+ const projectConfig2 = {
1635
+ project_id: "oss-project",
1636
+ project_name: "oss-project",
1637
+ org_id: "oss-org",
1638
+ appkey: "oss",
1639
+ region: "local",
1640
+ api_key: opts.apiKey,
1641
+ oss_host: opts.apiBaseUrl.replace(/\/$/, "")
1642
+ // remove trailing slash if any
1643
+ };
1644
+ saveProjectConfig(projectConfig2);
1645
+ if (json) {
1646
+ outputJson({ success: true, project: { id: projectConfig2.project_id, name: projectConfig2.project_name, region: projectConfig2.region } });
1647
+ } else {
1648
+ outputSuccess(`Linked to direct project at ${projectConfig2.oss_host}`);
1649
+ }
1650
+ trackCommand("link", "oss-org", { direct: true });
1651
+ await installSkills(json);
1652
+ await reportCliUsage("cli.link_direct", true, 6);
1653
+ try {
1654
+ const urlMatch = opts.apiBaseUrl.match(/^https?:\/\/([^.]+)\.[^.]+\.insforge\.app/);
1655
+ if (urlMatch) {
1656
+ await reportAgentConnected({ app_key: urlMatch[1] }, apiUrl);
1657
+ }
1658
+ } catch {
1659
+ }
1660
+ return;
1661
+ } catch (err) {
1662
+ await reportCliUsage("cli.link_direct", false);
1663
+ handleError(err, json);
1664
+ }
1665
+ }
1666
+ const creds = await requireAuth(apiUrl, false);
1667
+ let orgId = opts.orgId;
1668
+ let projectId = opts.projectId;
1669
+ if (!orgId && !projectId) {
1670
+ const orgs = await listOrganizations(apiUrl);
1671
+ if (orgs.length === 0) {
1672
+ throw new CLIError("No organizations found.");
1673
+ }
1674
+ if (orgs.length === 1) {
1675
+ orgId = orgs[0].id;
1676
+ if (!json) clack8.log.info(`Using organization: ${orgs[0].name}`);
1677
+ } else {
1678
+ if (json) {
1679
+ throw new CLIError("Multiple organizations found. Specify --org-id.");
1680
+ }
1681
+ const selected = await clack8.select({
1682
+ message: "Select an organization:",
1683
+ options: orgs.map((o) => ({
1684
+ value: o.id,
1685
+ label: o.name
1686
+ }))
1687
+ });
1688
+ if (clack8.isCancel(selected)) process.exit(0);
1689
+ orgId = selected;
1690
+ }
1691
+ }
1692
+ const config = getGlobalConfig();
1693
+ config.default_org_id = orgId;
1694
+ saveGlobalConfig(config);
1695
+ if (!projectId) {
1696
+ const projects = await listProjects(orgId, apiUrl);
1697
+ if (projects.length === 0) {
1698
+ throw new CLIError("No projects found in this organization.");
1699
+ }
1700
+ if (json) {
1701
+ throw new CLIError("Specify --project-id in JSON mode.");
1702
+ }
1703
+ const selected = await clack8.select({
1704
+ message: "Select a project to link:",
1705
+ options: projects.map((p) => ({
1706
+ value: p.id,
1707
+ label: `${p.name} (${p.region}, ${p.status})`
1708
+ }))
1709
+ });
1710
+ if (clack8.isCancel(selected)) process.exit(0);
1711
+ projectId = selected;
1712
+ }
1713
+ let project;
1714
+ let apiKey;
1715
+ try {
1716
+ [project, apiKey] = await Promise.all([
1717
+ getProject(projectId, apiUrl),
1718
+ getProjectApiKey(projectId, apiUrl)
1719
+ ]);
1720
+ } catch (err) {
1721
+ if (err instanceof CLIError && (err.exitCode === 5 || err.exitCode === 4 || err.message.includes("not found"))) {
1722
+ const identity = creds.user?.email ?? creds.user?.name ?? "unknown user";
1723
+ throw new CLIError(
1724
+ `You're logged in as ${identity}, and you don't have access to project ${projectId}. Check that the project ID is correct and belongs to one of your organizations.`,
1725
+ 5,
1726
+ "PERMISSION_DENIED"
1727
+ );
1728
+ }
1729
+ throw err;
1730
+ }
1731
+ const projectConfig = {
1732
+ project_id: project.id,
1733
+ project_name: project.name,
1734
+ org_id: project.organization_id,
1735
+ appkey: project.appkey,
1736
+ region: project.region,
1737
+ api_key: apiKey,
1738
+ oss_host: buildOssHost2(project.appkey, project.region)
1739
+ };
1740
+ if (!opts.template) {
1741
+ saveProjectConfig(projectConfig);
1742
+ }
1743
+ trackCommand("link", project.organization_id);
1744
+ if (json) {
1745
+ outputJson({ success: true, project: { id: project.id, name: project.name, region: project.region } });
1746
+ } else {
1747
+ outputSuccess(`Linked to project "${project.name}" (${project.appkey}.${project.region})`);
1748
+ }
1749
+ await installSkills(json);
1750
+ await reportCliUsage("cli.link", true, 6);
1751
+ try {
1752
+ await reportAgentConnected({ project_id: project.id }, apiUrl);
1753
+ } catch {
1754
+ }
1755
+ const template = opts.template;
1756
+ if (template) {
1757
+ const validTemplates = ["react", "nextjs", "chatbot", "crm", "e-commerce"];
1758
+ if (!validTemplates.includes(template)) {
1759
+ throw new CLIError(`Invalid template "${template}". Valid options: ${validTemplates.join(", ")}`);
1760
+ }
1761
+ let dirName = project.name;
1762
+ if (!json) {
1763
+ const inputDir = await clack8.text({
1764
+ message: "Directory name:",
1765
+ initialValue: project.name,
1766
+ validate: (v) => {
1767
+ if (v.length < 1) return "Directory name is required";
1768
+ const normalized = path4.basename(v).replace(/[^a-zA-Z0-9._-]/g, "-");
1769
+ if (!normalized || normalized === "." || normalized === "..") return "Invalid directory name";
1770
+ return void 0;
1771
+ }
1772
+ });
1773
+ if (clack8.isCancel(inputDir)) process.exit(0);
1774
+ dirName = path4.basename(inputDir).replace(/[^a-zA-Z0-9._-]/g, "-");
1775
+ }
1776
+ if (!dirName || dirName === "." || dirName === "..") {
1777
+ throw new CLIError("Invalid directory name.");
1778
+ }
1779
+ const templateDir = path4.resolve(process.cwd(), dirName);
1780
+ const dirExists = await fs4.stat(templateDir).catch(() => null);
1781
+ if (dirExists) {
1782
+ throw new CLIError(`Directory "${dirName}" already exists.`);
1783
+ }
1784
+ await fs4.mkdir(templateDir);
1785
+ process.chdir(templateDir);
1786
+ saveProjectConfig(projectConfig);
1787
+ captureEvent(orgId ?? project.organization_id, "template_selected", { template, source: "link" });
1788
+ const githubTemplates = ["chatbot", "crm", "e-commerce", "nextjs", "react"];
1789
+ if (githubTemplates.includes(template)) {
1790
+ await downloadGitHubTemplate(template, projectConfig, json);
1791
+ } else {
1792
+ await downloadTemplate(template, projectConfig, project.name, json, apiUrl);
1793
+ }
1794
+ const templateDownloaded = await fs4.stat(path4.join(process.cwd(), "package.json")).catch(() => null);
1795
+ if (templateDownloaded && !json) {
1796
+ const installSpinner = clack8.spinner();
1797
+ installSpinner.start("Installing dependencies...");
1798
+ try {
1799
+ await execAsync3("npm install", { cwd: process.cwd(), maxBuffer: 10 * 1024 * 1024 });
1800
+ installSpinner.stop("Dependencies installed");
1801
+ } catch (err) {
1802
+ installSpinner.stop("Failed to install dependencies");
1803
+ clack8.log.warn(`npm install failed: ${err.message}`);
1804
+ clack8.log.info("Run `npm install` manually to install dependencies.");
1805
+ }
1806
+ }
1807
+ if (!json) {
1808
+ const dashboardUrl = `${getFrontendUrl()}/dashboard/project/${project.id}`;
1809
+ clack8.log.step(`Dashboard: ${dashboardUrl}`);
1810
+ if (templateDownloaded) {
1811
+ const steps = [`cd ${dirName}`, "npm run dev"];
1812
+ clack8.note(steps.join("\n"), "Next steps");
1813
+ clack8.note("Open your coding agent (Claude Code, Codex, Cursor, etc.) to add new features.", "Keep building");
1814
+ } else {
1815
+ clack8.log.warn("Template download failed. You can retry or set up manually.");
1816
+ }
1817
+ }
1818
+ } else if (!json) {
1819
+ const dashboardUrl = `${getFrontendUrl()}/dashboard/project/${project.id}`;
1820
+ clack8.log.step(`Dashboard: ${dashboardUrl}`);
1821
+ const prompts = [
1822
+ "Build a todo app with Google OAuth sign-in",
1823
+ "Build an Instagram clone where users can upload photos, like, and comment",
1824
+ "Build an AI chatbot with conversation history"
1825
+ ];
1826
+ clack8.note(
1827
+ `Open your coding agent (Claude Code, Codex, Cursor, etc.) and try:
1828
+
1829
+ ${prompts.map((p) => `\u2022 "${p}"`).join("\n")}`,
1830
+ "Start building"
1831
+ );
1832
+ }
1833
+ } catch (err) {
1834
+ await reportCliUsage("cli.link", false);
1835
+ handleError(err, json);
1836
+ } finally {
1837
+ await shutdownAnalytics();
1838
+ }
1839
+ });
1840
+ }
1841
+
1842
+ // src/commands/db/query.ts
1843
+ function registerDbCommands(dbCmd2) {
1844
+ dbCmd2.command("query <sql>").description("Execute a SQL query against the database").option("--unrestricted", "Use unrestricted mode (allows system table access)").action(async (sql, opts, cmd) => {
1845
+ const { json } = getRootOpts(cmd);
1846
+ try {
1847
+ await requireAuth();
1848
+ const { rows, raw } = await runRawSql(sql, !!opts.unrestricted);
1849
+ if (json) {
1850
+ outputJson(raw);
1851
+ } else {
1852
+ if (rows.length > 0) {
1853
+ const headers = Object.keys(rows[0]);
1854
+ outputTable(
1855
+ headers,
1856
+ rows.map((row) => headers.map((h) => String(row[h] ?? "")))
1857
+ );
1858
+ console.log(`${rows.length} row(s) returned.`);
1859
+ } else {
1860
+ console.log("Query executed successfully.");
1861
+ if (rows.length === 0) {
1862
+ console.log("No rows returned.");
1863
+ }
1864
+ }
1865
+ }
1866
+ await reportCliUsage("cli.db.query", true);
1867
+ } catch (err) {
1868
+ await reportCliUsage("cli.db.query", false);
1869
+ handleError(err, json);
1870
+ }
1871
+ });
1872
+ }
1873
+
1874
+ // src/commands/db/tables.ts
1875
+ function registerDbTablesCommand(dbCmd2) {
1876
+ dbCmd2.command("tables").description("List all database tables").action(async (_opts, cmd) => {
1877
+ const { json } = getRootOpts(cmd);
1878
+ try {
1879
+ await requireAuth();
1880
+ const res = await ossFetch("/api/database/tables");
1881
+ const tables = await res.json();
1882
+ if (json) {
1883
+ outputJson(tables);
1884
+ } else {
1885
+ if (tables.length === 0) {
1886
+ console.log("No tables found.");
1887
+ return;
1888
+ }
1889
+ outputTable(
1890
+ ["Table Name"],
1891
+ tables.map((t) => [t])
1892
+ );
1893
+ }
1894
+ await reportCliUsage("cli.db.tables", true);
1895
+ } catch (err) {
1896
+ await reportCliUsage("cli.db.tables", false);
1897
+ handleError(err, json);
1898
+ }
1899
+ });
1900
+ }
1901
+
1902
+ // src/commands/db/functions.ts
1903
+ function registerDbFunctionsCommand(dbCmd2) {
1904
+ dbCmd2.command("functions").description("List all database functions").action(async (_opts, cmd) => {
1905
+ const { json } = getRootOpts(cmd);
1906
+ try {
1907
+ await requireAuth();
1908
+ const res = await ossFetch("/api/database/functions");
1909
+ const raw = await res.json();
1910
+ const functions = Array.isArray(raw) ? raw : raw.functions ?? [];
1911
+ if (json) {
1912
+ outputJson(raw);
1913
+ } else {
1914
+ if (functions.length === 0) {
1915
+ console.log("No database functions found.");
1916
+ return;
1917
+ }
1918
+ outputTable(
1919
+ ["Name", "Definition", "Kind"],
1920
+ functions.map((f) => [f.functionName, f.functionDef, f.kind])
1921
+ );
1922
+ }
1923
+ await reportCliUsage("cli.db.functions", true);
1924
+ } catch (err) {
1925
+ await reportCliUsage("cli.db.functions", false);
1926
+ handleError(err, json);
1927
+ }
1928
+ });
1929
+ }
1930
+
1931
+ // src/commands/db/indexes.ts
1932
+ function registerDbIndexesCommand(dbCmd2) {
1933
+ dbCmd2.command("indexes").description("List all database indexes").action(async (_opts, cmd) => {
1934
+ const { json } = getRootOpts(cmd);
1935
+ try {
1936
+ await requireAuth();
1937
+ const res = await ossFetch("/api/database/indexes");
1938
+ const raw = await res.json();
1939
+ const indexes = Array.isArray(raw) ? raw : raw.indexes ?? [];
1940
+ if (json) {
1941
+ outputJson(raw);
1942
+ } else {
1943
+ if (indexes.length === 0) {
1944
+ console.log("No database indexes found.");
1945
+ return;
1946
+ }
1947
+ outputTable(
1948
+ ["Table", "Index Name", "Definition", "Unique", "Primary"],
1949
+ indexes.map((i) => [
1950
+ i.tableName,
1951
+ i.indexName,
1952
+ i.indexDef,
1953
+ i.isUnique ? "Yes" : "No",
1954
+ i.isPrimary ? "Yes" : "No"
1955
+ ])
1956
+ );
1957
+ }
1958
+ await reportCliUsage("cli.db.indexes", true);
1959
+ } catch (err) {
1960
+ await reportCliUsage("cli.db.indexes", false);
1961
+ handleError(err, json);
1962
+ }
1963
+ });
1964
+ }
1965
+
1966
+ // src/commands/db/policies.ts
1967
+ function registerDbPoliciesCommand(dbCmd2) {
1968
+ dbCmd2.command("policies").description("List all RLS policies").action(async (_opts, cmd) => {
1969
+ const { json } = getRootOpts(cmd);
1970
+ try {
1971
+ await requireAuth();
1972
+ const res = await ossFetch("/api/database/policies");
1973
+ const raw = await res.json();
1974
+ const policies = Array.isArray(raw) ? raw : raw.policies ?? [];
1975
+ if (json) {
1976
+ outputJson(raw);
1977
+ } else {
1978
+ if (policies.length === 0) {
1979
+ console.log("No RLS policies found.");
1980
+ return;
1981
+ }
1982
+ outputTable(
1983
+ ["Table", "Policy Name", "Command", "Roles", "Qual", "With Check"],
1984
+ policies.map((p) => [
1985
+ String(p.tableName ?? "-"),
1986
+ String(p.policyName ?? "-"),
1987
+ String(p.cmd ?? "-"),
1988
+ Array.isArray(p.roles) ? p.roles.join(", ") : String(p.roles ?? "-"),
1989
+ String(p.qual ?? "-"),
1990
+ String(p.withCheck ?? "-")
1991
+ ])
1992
+ );
1993
+ }
1994
+ await reportCliUsage("cli.db.policies", true);
1995
+ } catch (err) {
1996
+ await reportCliUsage("cli.db.policies", false);
1997
+ handleError(err, json);
1998
+ }
1999
+ });
2000
+ }
2001
+
2002
+ // src/commands/db/triggers.ts
2003
+ function registerDbTriggersCommand(dbCmd2) {
2004
+ dbCmd2.command("triggers").description("List all database triggers").action(async (_opts, cmd) => {
2005
+ const { json } = getRootOpts(cmd);
2006
+ try {
2007
+ await requireAuth();
2008
+ const res = await ossFetch("/api/database/triggers");
2009
+ const raw = await res.json();
2010
+ const triggers = Array.isArray(raw) ? raw : raw.triggers ?? [];
2011
+ if (json) {
2012
+ outputJson(raw);
2013
+ } else {
2014
+ if (triggers.length === 0) {
2015
+ console.log("No database triggers found.");
2016
+ return;
2017
+ }
2018
+ outputTable(
2019
+ ["Name", "Table", "Timing", "Events", "ActionOrientation", "ActionCondition", "ActionStatement"],
2020
+ triggers.map((t) => [
2021
+ t.triggerName,
2022
+ t.tableName,
2023
+ t.actionTiming,
2024
+ t.eventManipulation,
2025
+ t.actionOrientation,
2026
+ t.actionCondition ?? "-",
2027
+ t.actionStatement
2028
+ ])
2029
+ );
2030
+ }
2031
+ await reportCliUsage("cli.db.triggers", true);
2032
+ } catch (err) {
2033
+ await reportCliUsage("cli.db.triggers", false);
2034
+ handleError(err, json);
2035
+ }
2036
+ });
2037
+ }
2038
+
2039
+ // src/commands/db/rpc.ts
2040
+ function registerDbRpcCommand(dbCmd2) {
2041
+ dbCmd2.command("rpc <functionName>").description("Call a database function via RPC").option("--data <json>", "JSON body to pass as function parameters").action(async (functionName, opts, cmd) => {
2042
+ const { json } = getRootOpts(cmd);
2043
+ try {
2044
+ await requireAuth();
2045
+ const body = opts.data ? JSON.stringify(JSON.parse(opts.data)) : void 0;
2046
+ const res = await ossFetch(`/api/database/rpc/${encodeURIComponent(functionName)}`, {
2047
+ method: body ? "POST" : "GET",
2048
+ ...body ? { body } : {}
2049
+ });
2050
+ const result = await res.json();
2051
+ if (json) {
2052
+ outputJson(result);
2053
+ } else {
2054
+ console.log(JSON.stringify(result, null, 2));
2055
+ }
2056
+ await reportCliUsage("cli.db.rpc", true);
2057
+ } catch (err) {
2058
+ handleError(err, json);
2059
+ }
2060
+ });
2061
+ }
2062
+
2063
+ // src/commands/db/export.ts
2064
+ import { writeFileSync as writeFileSync2 } from "fs";
2065
+ function registerDbExportCommand(dbCmd2) {
2066
+ 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) => {
2067
+ const { json } = getRootOpts(cmd);
2068
+ try {
2069
+ await requireAuth();
2070
+ const body = {
2071
+ format: opts.format,
2072
+ includeData: opts.data !== false
2073
+ };
2074
+ if (opts.tables) {
2075
+ body.tables = opts.tables.split(",").map((t) => t.trim());
1262
2076
  }
1263
2077
  if (opts.includeFunctions) body.includeFunctions = true;
1264
2078
  if (opts.includeSequences) body.includeSequences = true;
@@ -1302,7 +2116,7 @@ function registerDbExportCommand(dbCmd2) {
1302
2116
 
1303
2117
  // src/commands/db/import.ts
1304
2118
  import { readFileSync as readFileSync3 } from "fs";
1305
- import { basename } from "path";
2119
+ import { basename as basename4 } from "path";
1306
2120
  function registerDbImportCommand(dbCmd2) {
1307
2121
  dbCmd2.command("import <file>").description("Import database from a local SQL file").option("--truncate", "Truncate existing tables before import").action(async (file, opts, cmd) => {
1308
2122
  const { json } = getRootOpts(cmd);
@@ -1311,7 +2125,7 @@ function registerDbImportCommand(dbCmd2) {
1311
2125
  const config = getProjectConfig();
1312
2126
  if (!config) throw new ProjectNotLinkedError();
1313
2127
  const fileContent = readFileSync3(file);
1314
- const fileName = basename(file);
2128
+ const fileName = basename4(file);
1315
2129
  const formData = new FormData();
1316
2130
  formData.append("file", new Blob([fileContent]), fileName);
1317
2131
  if (opts.truncate) {
@@ -1353,8 +2167,8 @@ function registerRecordsCommands(recordsCmd2) {
1353
2167
  if (opts.limit) params.set("limit", String(opts.limit));
1354
2168
  if (opts.offset) params.set("offset", String(opts.offset));
1355
2169
  const query = params.toString();
1356
- const path4 = `/api/database/records/${encodeURIComponent(table)}${query ? `?${query}` : ""}`;
1357
- const res = await ossFetch(path4);
2170
+ const path5 = `/api/database/records/${encodeURIComponent(table)}${query ? `?${query}` : ""}`;
2171
+ const res = await ossFetch(path5);
1358
2172
  const data = await res.json();
1359
2173
  const records = data.data ?? [];
1360
2174
  if (json) {
@@ -1524,17 +2338,17 @@ function registerFunctionsCommands(functionsCmd2) {
1524
2338
 
1525
2339
  // src/commands/functions/deploy.ts
1526
2340
  import { readFileSync as readFileSync4, existsSync as existsSync3 } from "fs";
1527
- import { join as join3 } from "path";
2341
+ import { join as join6 } from "path";
1528
2342
  function registerFunctionsDeployCommand(functionsCmd2) {
1529
2343
  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) => {
1530
2344
  const { json } = getRootOpts(cmd);
1531
2345
  try {
1532
2346
  await requireAuth();
1533
- const filePath = opts.file ?? join3(process.cwd(), "insforge", "functions", slug, "index.ts");
2347
+ const filePath = opts.file ?? join6(process.cwd(), "insforge", "functions", slug, "index.ts");
1534
2348
  if (!existsSync3(filePath)) {
1535
2349
  throw new CLIError(
1536
2350
  `Source file not found: ${filePath}
1537
- Specify --file <path> or create ${join3("insforge", "functions", slug, "index.ts")}`
2351
+ Specify --file <path> or create ${join6("insforge", "functions", slug, "index.ts")}`
1538
2352
  );
1539
2353
  }
1540
2354
  const code = readFileSync4(filePath, "utf-8");
@@ -1617,238 +2431,15 @@ function registerFunctionsInvokeCommand(functionsCmd2) {
1617
2431
  const data = await res.json();
1618
2432
  if (json) {
1619
2433
  outputJson({ status, body: data });
1620
- } else {
1621
- console.log(JSON.stringify(data, null, 2));
1622
- }
1623
- } else {
1624
- const text3 = await res.text();
1625
- console.log(text3);
1626
- }
1627
- if (status >= 400) {
1628
- throw new CLIError(`HTTP ${status}`, 1, "HTTP_ERROR");
1629
- }
1630
- } catch (err) {
1631
- handleError(err, json);
1632
- }
1633
- });
1634
- }
1635
-
1636
- // src/commands/functions/code.ts
1637
- function registerFunctionsCodeCommand(functionsCmd2) {
1638
- functionsCmd2.command("code <slug>").description("Fetch and display the source code of an edge function").action(async (slug, _opts, cmd) => {
1639
- const { json } = getRootOpts(cmd);
1640
- try {
1641
- await requireAuth();
1642
- const res = await ossFetch(`/api/functions/${encodeURIComponent(slug)}`);
1643
- const fn = await res.json();
1644
- if (json) {
1645
- outputJson(fn);
1646
- } else {
1647
- console.log(`Function: ${fn.name} (${fn.slug})`);
1648
- console.log(`Status: ${fn.status}`);
1649
- if (fn.description) console.log(`Desc: ${fn.description}`);
1650
- if (fn.deployed_at) console.log(`Deployed: ${fn.deployed_at}`);
1651
- console.log("---");
1652
- console.log(fn.code);
1653
- }
1654
- } catch (err) {
1655
- handleError(err, json);
1656
- }
1657
- });
1658
- }
1659
-
1660
- // src/commands/functions/delete.ts
1661
- import * as clack7 from "@clack/prompts";
1662
- function registerFunctionsDeleteCommand(functionsCmd2) {
1663
- functionsCmd2.command("delete <slug>").description("Delete an edge function").action(async (slug, _opts, cmd) => {
1664
- const { json, yes } = getRootOpts(cmd);
1665
- try {
1666
- await requireAuth();
1667
- if (!yes && !json) {
1668
- const confirmed = await clack7.confirm({
1669
- message: `Delete function "${slug}"? This cannot be undone.`
1670
- });
1671
- if (clack7.isCancel(confirmed) || !confirmed) {
1672
- clack7.log.info("Cancelled.");
1673
- return;
1674
- }
1675
- }
1676
- const res = await ossFetch(`/api/functions/${encodeURIComponent(slug)}`, {
1677
- method: "DELETE"
1678
- });
1679
- const result = await res.json();
1680
- if (json) {
1681
- outputJson(result);
1682
- } else {
1683
- if (result.success) {
1684
- outputSuccess(`Function "${slug}" deleted successfully.`);
1685
- } else {
1686
- outputSuccess(`Failed to delete function "${slug}".`);
1687
- }
1688
- }
1689
- await reportCliUsage("cli.functions.delete", true);
1690
- } catch (err) {
1691
- await reportCliUsage("cli.functions.delete", false);
1692
- handleError(err, json);
1693
- }
1694
- });
1695
- }
1696
-
1697
- // src/commands/storage/buckets.ts
1698
- function registerStorageBucketsCommand(storageCmd2) {
1699
- storageCmd2.command("buckets").description("List all storage buckets").action(async (_opts, cmd) => {
1700
- const { json } = getRootOpts(cmd);
1701
- try {
1702
- await requireAuth();
1703
- const res = await ossFetch("/api/storage/buckets");
1704
- const raw = await res.json();
1705
- const buckets = Array.isArray(raw) ? raw : raw && typeof raw === "object" && "buckets" in raw ? raw.buckets ?? [] : [];
1706
- if (json) {
1707
- outputJson(raw);
1708
- } else {
1709
- if (buckets.length === 0) {
1710
- console.log("No buckets found.");
1711
- return;
1712
- }
1713
- outputTable(
1714
- ["Bucket Name", "Public"],
1715
- buckets.map((b) => [b.name, b.public ? "Yes" : "No"])
1716
- );
1717
- }
1718
- await reportCliUsage("cli.storage.buckets", true);
1719
- } catch (err) {
1720
- await reportCliUsage("cli.storage.buckets", false);
1721
- handleError(err, json);
1722
- }
1723
- });
1724
- }
1725
-
1726
- // src/commands/storage/upload.ts
1727
- import { readFileSync as readFileSync5, existsSync as existsSync4 } from "fs";
1728
- import { basename as basename2 } from "path";
1729
- function registerStorageUploadCommand(storageCmd2) {
1730
- 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) => {
1731
- const { json } = getRootOpts(cmd);
1732
- try {
1733
- await requireAuth();
1734
- const config = getProjectConfig();
1735
- if (!config) throw new ProjectNotLinkedError();
1736
- if (!existsSync4(file)) {
1737
- throw new CLIError(`File not found: ${file}`);
1738
- }
1739
- const fileContent = readFileSync5(file);
1740
- const objectKey = opts.key ?? basename2(file);
1741
- const bucketName = opts.bucket;
1742
- const formData = new FormData();
1743
- const blob = new Blob([fileContent]);
1744
- formData.append("file", blob, objectKey);
1745
- const url = `${config.oss_host}/api/storage/buckets/${encodeURIComponent(bucketName)}/objects/${encodeURIComponent(objectKey)}`;
1746
- const res = await fetch(url, {
1747
- method: "PUT",
1748
- headers: {
1749
- Authorization: `Bearer ${config.api_key}`
1750
- },
1751
- body: formData
1752
- });
1753
- if (!res.ok) {
1754
- const err = await res.json().catch(() => ({}));
1755
- throw new CLIError(err.error ?? `Upload failed: ${res.status}`);
1756
- }
1757
- const data = await res.json();
1758
- if (json) {
1759
- outputJson(data);
1760
- } else {
1761
- outputSuccess(`Uploaded "${basename2(file)}" to bucket "${bucketName}".`);
1762
- }
1763
- } catch (err) {
1764
- handleError(err, json);
1765
- }
1766
- });
1767
- }
1768
-
1769
- // src/commands/storage/download.ts
1770
- import { writeFileSync as writeFileSync3 } from "fs";
1771
- import { join as join4, basename as basename3 } from "path";
1772
- function registerStorageDownloadCommand(storageCmd2) {
1773
- 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) => {
1774
- const { json } = getRootOpts(cmd);
1775
- try {
1776
- await requireAuth();
1777
- const config = getProjectConfig();
1778
- if (!config) throw new ProjectNotLinkedError();
1779
- const bucketName = opts.bucket;
1780
- const url = `${config.oss_host}/api/storage/buckets/${encodeURIComponent(bucketName)}/objects/${encodeURIComponent(objectKey)}`;
1781
- const res = await fetch(url, {
1782
- headers: {
1783
- Authorization: `Bearer ${config.api_key}`
1784
- }
1785
- });
1786
- if (!res.ok) {
1787
- const err = await res.json().catch(() => ({}));
1788
- throw new CLIError(err.error ?? `Download failed: ${res.status}`);
1789
- }
1790
- const buffer = Buffer.from(await res.arrayBuffer());
1791
- const outputPath = opts.output ?? join4(process.cwd(), basename3(objectKey));
1792
- writeFileSync3(outputPath, buffer);
1793
- if (json) {
1794
- outputJson({ success: true, path: outputPath, size: buffer.length });
1795
- } else {
1796
- outputSuccess(`Downloaded "${objectKey}" to ${outputPath} (${buffer.length} bytes).`);
1797
- }
1798
- } catch (err) {
1799
- handleError(err, json);
1800
- }
1801
- });
1802
- }
1803
-
1804
- // src/commands/storage/create-bucket.ts
1805
- function registerStorageCreateBucketCommand(storageCmd2) {
1806
- storageCmd2.command("create-bucket <name>").description("Create a new storage bucket").option("--public", "Make the bucket publicly accessible (default)").option("--private", "Make the bucket private").action(async (name, opts, cmd) => {
1807
- const { json } = getRootOpts(cmd);
1808
- try {
1809
- await requireAuth();
1810
- const isPublic = !opts.private;
1811
- const res = await ossFetch("/api/storage/buckets", {
1812
- method: "POST",
1813
- body: JSON.stringify({ bucketName: name, isPublic })
1814
- });
1815
- const data = await res.json();
1816
- if (json) {
1817
- outputJson(data);
1818
- } else {
1819
- outputSuccess(`Bucket "${name}" created (${isPublic ? "public" : "private"}).`);
1820
- }
1821
- await reportCliUsage("cli.storage.create-bucket", true);
1822
- } catch (err) {
1823
- await reportCliUsage("cli.storage.create-bucket", false);
1824
- handleError(err, json);
1825
- }
1826
- });
1827
- }
1828
-
1829
- // src/commands/storage/delete-bucket.ts
1830
- import * as clack8 from "@clack/prompts";
1831
- function registerStorageDeleteBucketCommand(storageCmd2) {
1832
- storageCmd2.command("delete-bucket <name>").description("Delete a storage bucket and all its objects").action(async (name, _opts, cmd) => {
1833
- const { json, yes } = getRootOpts(cmd);
1834
- try {
1835
- await requireAuth();
1836
- if (!yes && !json) {
1837
- const confirm8 = await clack8.confirm({
1838
- message: `Delete bucket "${name}" and all its objects? This cannot be undone.`
1839
- });
1840
- if (!confirm8 || clack8.isCancel(confirm8)) {
1841
- process.exit(0);
1842
- }
1843
- }
1844
- const res = await ossFetch(`/api/storage/buckets/${encodeURIComponent(name)}`, {
1845
- method: "DELETE"
1846
- });
1847
- const data = await res.json();
1848
- if (json) {
1849
- outputJson(data);
2434
+ } else {
2435
+ console.log(JSON.stringify(data, null, 2));
2436
+ }
1850
2437
  } else {
1851
- outputSuccess(`Bucket "${name}" deleted.`);
2438
+ const text4 = await res.text();
2439
+ console.log(text4);
2440
+ }
2441
+ if (status >= 400) {
2442
+ throw new CLIError(`HTTP ${status}`, 1, "HTTP_ERROR");
1852
2443
  }
1853
2444
  } catch (err) {
1854
2445
  handleError(err, json);
@@ -1856,715 +2447,284 @@ function registerStorageDeleteBucketCommand(storageCmd2) {
1856
2447
  });
1857
2448
  }
1858
2449
 
1859
- // src/commands/storage/list-objects.ts
1860
- function formatSize(bytes) {
1861
- if (bytes < 1024) return `${bytes} B`;
1862
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
1863
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1864
- }
1865
- function registerStorageListObjectsCommand(storageCmd2) {
1866
- storageCmd2.command("list-objects <bucket>").description("List objects in a storage bucket").option("--limit <n>", "Maximum number of objects to return", "100").option("--offset <n>", "Number of objects to skip", "0").option("--prefix <prefix>", "Filter objects by key prefix").option("--search <term>", "Search objects by key (partial match)").option("--sort <field>", "Sort by field: key, size, uploadedAt (default: key)").action(async (bucket, opts, cmd) => {
2450
+ // src/commands/functions/code.ts
2451
+ function registerFunctionsCodeCommand(functionsCmd2) {
2452
+ functionsCmd2.command("code <slug>").description("Fetch and display the source code of an edge function").action(async (slug, _opts, cmd) => {
1867
2453
  const { json } = getRootOpts(cmd);
1868
2454
  try {
1869
2455
  await requireAuth();
1870
- const params = new URLSearchParams();
1871
- params.set("limit", opts.limit);
1872
- params.set("offset", opts.offset);
1873
- if (opts.prefix) params.set("prefix", opts.prefix);
1874
- if (opts.search) params.set("search", opts.search);
1875
- const res = await ossFetch(
1876
- `/api/storage/buckets/${encodeURIComponent(bucket)}/objects?${params.toString()}`
1877
- );
1878
- const raw = await res.json();
1879
- const objects = Array.isArray(raw) ? raw : raw.data ?? [];
1880
- const sortField = opts.sort ?? "key";
1881
- objects.sort((a, b) => {
1882
- if (sortField === "size") return a.size - b.size;
1883
- if (sortField === "uploadedAt") return a.uploadedAt.localeCompare(b.uploadedAt);
1884
- return a.key.localeCompare(b.key);
1885
- });
2456
+ const res = await ossFetch(`/api/functions/${encodeURIComponent(slug)}`);
2457
+ const fn = await res.json();
1886
2458
  if (json) {
1887
- outputJson(raw);
2459
+ outputJson(fn);
1888
2460
  } else {
1889
- if (objects.length === 0) {
1890
- console.log(`No objects found in bucket "${bucket}".`);
1891
- return;
1892
- }
1893
- const total = raw.pagination?.total;
1894
- if (total !== void 0) {
1895
- console.log(`Showing ${objects.length} of ${total} objects:
1896
- `);
1897
- }
1898
- outputTable(
1899
- ["Key", "Size", "Type", "Uploaded At"],
1900
- objects.map((o) => [
1901
- o.key,
1902
- formatSize(o.size),
1903
- o.mimeType ?? "-",
1904
- o.uploadedAt
1905
- ])
1906
- );
2461
+ console.log(`Function: ${fn.name} (${fn.slug})`);
2462
+ console.log(`Status: ${fn.status}`);
2463
+ if (fn.description) console.log(`Desc: ${fn.description}`);
2464
+ if (fn.deployed_at) console.log(`Deployed: ${fn.deployed_at}`);
2465
+ console.log("---");
2466
+ console.log(fn.code);
1907
2467
  }
1908
- await reportCliUsage("cli.storage.list-objects", true);
1909
2468
  } catch (err) {
1910
- await reportCliUsage("cli.storage.list-objects", false);
1911
2469
  handleError(err, json);
1912
2470
  }
1913
2471
  });
1914
2472
  }
1915
2473
 
1916
- // src/commands/create.ts
1917
- import { exec as exec2 } from "child_process";
1918
- import { tmpdir } from "os";
1919
- import { promisify as promisify2 } from "util";
1920
- import * as fs3 from "fs/promises";
1921
- import * as path3 from "path";
1922
- import * as clack10 from "@clack/prompts";
1923
-
1924
- // src/lib/env.ts
1925
- import * as fs from "fs/promises";
1926
- import * as path from "path";
1927
- async function readEnvFile(cwd) {
1928
- const candidates = [".env.local", ".env.production", ".env"];
1929
- for (const name of candidates) {
1930
- const filePath = path.join(cwd, name);
1931
- const exists = await fs.stat(filePath).catch(() => null);
1932
- if (!exists) continue;
1933
- const content = await fs.readFile(filePath, "utf-8");
1934
- const vars = [];
1935
- for (const line of content.split("\n")) {
1936
- const trimmed = line.trim();
1937
- if (!trimmed || trimmed.startsWith("#")) continue;
1938
- const eqIndex = trimmed.indexOf("=");
1939
- if (eqIndex === -1) continue;
1940
- const key = trimmed.slice(0, eqIndex).trim();
1941
- let value = trimmed.slice(eqIndex + 1).trim();
1942
- if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
1943
- value = value.slice(1, -1);
1944
- }
1945
- if (key) vars.push({ key, value });
1946
- }
1947
- return vars;
1948
- }
1949
- return [];
1950
- }
1951
-
1952
- // src/commands/deployments/deploy.ts
1953
- import * as path2 from "path";
1954
- import * as fs2 from "fs/promises";
2474
+ // src/commands/functions/delete.ts
1955
2475
  import * as clack9 from "@clack/prompts";
1956
- import archiver from "archiver";
1957
- var POLL_INTERVAL_MS = 5e3;
1958
- var POLL_TIMEOUT_MS = 3e5;
1959
- var EXCLUDE_PATTERNS = [
1960
- "node_modules",
1961
- ".git",
1962
- ".next",
1963
- ".env",
1964
- ".env.local",
1965
- "dist",
1966
- "build",
1967
- ".DS_Store",
1968
- ".insforge",
1969
- // IDE and AI agent configs
1970
- ".claude",
1971
- ".agents",
1972
- ".augment",
1973
- ".kilocode",
1974
- ".kiro",
1975
- ".qoder",
1976
- ".qwen",
1977
- ".roo",
1978
- ".trae",
1979
- ".windsurf",
1980
- ".vercel",
1981
- ".turbo",
1982
- ".cache",
1983
- "skills",
1984
- "coverage"
1985
- ];
1986
- function shouldExclude(name) {
1987
- const normalized = name.replace(/\\/g, "/");
1988
- for (const pattern of EXCLUDE_PATTERNS) {
1989
- if (normalized === pattern || normalized.startsWith(pattern + "/") || normalized.endsWith("/" + pattern) || normalized.includes("/" + pattern + "/")) {
1990
- return true;
1991
- }
1992
- }
1993
- if (normalized.endsWith(".log")) return true;
1994
- return false;
1995
- }
1996
- async function createZipBuffer(sourceDir) {
1997
- return new Promise((resolve2, reject) => {
1998
- const archive = archiver("zip", { zlib: { level: 9 } });
1999
- const chunks = [];
2000
- archive.on("data", (chunk) => chunks.push(chunk));
2001
- archive.on("end", () => resolve2(Buffer.concat(chunks)));
2002
- archive.on("error", (err) => reject(err));
2003
- archive.directory(sourceDir, false, (entry) => {
2004
- if (shouldExclude(entry.name)) return false;
2005
- return entry;
2006
- });
2007
- void archive.finalize();
2008
- });
2009
- }
2010
- async function deployProject(opts) {
2011
- const { sourceDir, startBody = {}, spinner: s } = opts;
2012
- s?.start("Creating deployment...");
2013
- const createRes = await ossFetch("/api/deployments", { method: "POST" });
2014
- const { id: deploymentId, uploadUrl, uploadFields } = await createRes.json();
2015
- s?.message("Compressing source files...");
2016
- const zipBuffer = await createZipBuffer(sourceDir);
2017
- s?.message("Uploading...");
2018
- const formData = new FormData();
2019
- for (const [key, value] of Object.entries(uploadFields)) {
2020
- formData.append(key, value);
2021
- }
2022
- formData.append(
2023
- "file",
2024
- new Blob([zipBuffer], { type: "application/zip" }),
2025
- "deployment.zip"
2026
- );
2027
- const uploadRes = await fetch(uploadUrl, { method: "POST", body: formData });
2028
- if (!uploadRes.ok) {
2029
- const uploadErr = await uploadRes.text();
2030
- throw new CLIError(`Failed to upload: ${uploadErr}`);
2031
- }
2032
- s?.message("Starting deployment...");
2033
- const startRes = await ossFetch(`/api/deployments/${deploymentId}/start`, {
2034
- method: "POST",
2035
- body: JSON.stringify(startBody)
2036
- });
2037
- await startRes.json();
2038
- s?.message("Building and deploying...");
2039
- const startTime = Date.now();
2040
- let deployment = null;
2041
- while (Date.now() - startTime < POLL_TIMEOUT_MS) {
2042
- await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
2043
- try {
2044
- const statusRes = await ossFetch(`/api/deployments/${deploymentId}`);
2045
- deployment = await statusRes.json();
2046
- const status = deployment.status.toUpperCase();
2047
- if (status === "READY") {
2048
- break;
2049
- }
2050
- if (status === "ERROR" || status === "CANCELED") {
2051
- s?.stop("Deployment failed");
2052
- throw new CLIError(getDeploymentError(deployment.metadata) ?? `Deployment failed with status: ${deployment.status}`);
2053
- }
2054
- const elapsed = Math.round((Date.now() - startTime) / 1e3);
2055
- s?.message(`Building and deploying... (${elapsed}s, status: ${deployment.status})`);
2056
- } catch (err) {
2057
- if (err instanceof CLIError) throw err;
2058
- }
2059
- }
2060
- const isReady = deployment?.status.toUpperCase() === "READY";
2061
- const liveUrl = isReady ? deployment?.url ?? null : null;
2062
- return { deploymentId, deployment, isReady, liveUrl };
2063
- }
2064
- function registerDeploymentsDeployCommand(deploymentsCmd2) {
2065
- deploymentsCmd2.command("deploy [directory]").description("Deploy a frontend project to Vercel").option("--env <vars>", `Environment variables as JSON (e.g. '{"KEY":"value"}')`).option("--meta <meta>", "Deployment metadata as JSON").action(async (directory, opts, cmd) => {
2066
- const { json } = getRootOpts(cmd);
2476
+ function registerFunctionsDeleteCommand(functionsCmd2) {
2477
+ functionsCmd2.command("delete <slug>").description("Delete an edge function").action(async (slug, _opts, cmd) => {
2478
+ const { json, yes } = getRootOpts(cmd);
2067
2479
  try {
2068
- await requireAuth();
2069
- const config = getProjectConfig();
2070
- if (!config) throw new ProjectNotLinkedError();
2071
- const sourceDir = path2.resolve(directory ?? ".");
2072
- const stats = await fs2.stat(sourceDir).catch(() => null);
2073
- if (!stats?.isDirectory()) {
2074
- throw new CLIError(`"${sourceDir}" is not a valid directory.`);
2075
- }
2076
- const dirName = path2.basename(sourceDir);
2077
- if (EXCLUDE_PATTERNS.includes(dirName)) {
2078
- throw new CLIError(`"${dirName}" is an excluded directory and cannot be used as a deploy source. Please specify your project root or output directory instead.`);
2079
- }
2080
- const s = !json ? clack9.spinner() : null;
2081
- const startBody = {};
2082
- if (opts.env) {
2083
- try {
2084
- const parsed = JSON.parse(opts.env);
2085
- if (Array.isArray(parsed)) {
2086
- startBody.envVars = parsed;
2087
- } else {
2088
- startBody.envVars = Object.entries(parsed).map(([key, value]) => ({ key, value }));
2089
- }
2090
- } catch {
2091
- throw new CLIError("Invalid --env JSON.");
2092
- }
2093
- }
2094
- if (opts.meta) {
2095
- try {
2096
- startBody.meta = JSON.parse(opts.meta);
2097
- } catch {
2098
- throw new CLIError("Invalid --meta JSON.");
2099
- }
2100
- }
2101
- const result = await deployProject({ sourceDir, startBody, spinner: s });
2102
- if (result.isReady) {
2103
- s?.stop("Deployment complete");
2104
- if (json) {
2105
- outputJson(result.deployment);
2106
- } else {
2107
- if (result.liveUrl) {
2108
- clack9.log.success(`Live at: ${result.liveUrl}`);
2109
- }
2110
- clack9.log.info(`Deployment ID: ${result.deploymentId}`);
2480
+ await requireAuth();
2481
+ if (!yes && !json) {
2482
+ const confirmed = await clack9.confirm({
2483
+ message: `Delete function "${slug}"? This cannot be undone.`
2484
+ });
2485
+ if (clack9.isCancel(confirmed) || !confirmed) {
2486
+ clack9.log.info("Cancelled.");
2487
+ return;
2111
2488
  }
2489
+ }
2490
+ const res = await ossFetch(`/api/functions/${encodeURIComponent(slug)}`, {
2491
+ method: "DELETE"
2492
+ });
2493
+ const result = await res.json();
2494
+ if (json) {
2495
+ outputJson(result);
2112
2496
  } else {
2113
- s?.stop("Deployment is still building");
2114
- if (json) {
2115
- outputJson({ id: result.deploymentId, status: result.deployment?.status ?? "building", timedOut: true });
2497
+ if (result.success) {
2498
+ outputSuccess(`Function "${slug}" deleted successfully.`);
2116
2499
  } else {
2117
- clack9.log.info(`Deployment ID: ${result.deploymentId}`);
2118
- clack9.log.warn("Deployment did not finish within 5 minutes.");
2119
- clack9.log.info(`Check status with: npx @insforge/cli deployments status ${result.deploymentId}`);
2500
+ outputSuccess(`Failed to delete function "${slug}".`);
2120
2501
  }
2121
2502
  }
2122
- await reportCliUsage("cli.deployments.deploy", true);
2503
+ await reportCliUsage("cli.functions.delete", true);
2123
2504
  } catch (err) {
2124
- await reportCliUsage("cli.deployments.deploy", false);
2505
+ await reportCliUsage("cli.functions.delete", false);
2125
2506
  handleError(err, json);
2126
2507
  }
2127
2508
  });
2128
2509
  }
2129
2510
 
2130
- // src/commands/create.ts
2131
- var execAsync2 = promisify2(exec2);
2132
- function buildOssHost2(appkey, region) {
2133
- return `https://${appkey}.${region}.insforge.app`;
2134
- }
2135
- async function waitForProjectActive(projectId, apiUrl, timeoutMs = 12e4) {
2136
- const start = Date.now();
2137
- while (Date.now() - start < timeoutMs) {
2138
- const project = await getProject(projectId, apiUrl);
2139
- if (project.status === "active") return;
2140
- await new Promise((r) => setTimeout(r, 3e3));
2141
- }
2142
- throw new CLIError("Project creation timed out. Check the dashboard for status.");
2143
- }
2144
- var INSFORGE_BANNER = [
2145
- "\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",
2146
- "\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",
2147
- "\u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2557 ",
2148
- "\u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551\u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255D ",
2149
- "\u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557",
2150
- "\u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D"
2151
- ];
2152
- async function animateBanner() {
2153
- const isTTY = process.stderr.isTTY;
2154
- if (!isTTY || process.env.CI) {
2155
- for (const line of INSFORGE_BANNER) {
2156
- process.stderr.write(`${line}
2157
- `);
2158
- }
2159
- process.stderr.write("\n");
2160
- return;
2161
- }
2162
- const totalLines = INSFORGE_BANNER.length;
2163
- const maxLen = Math.max(...INSFORGE_BANNER.map((l) => l.length));
2164
- const cols = process.stderr.columns ?? 0;
2165
- if (cols > 0 && cols < maxLen) {
2166
- for (const line of INSFORGE_BANNER) {
2167
- process.stderr.write(`\x1B[97m${line}\x1B[0m
2168
- `);
2169
- }
2170
- process.stderr.write("\n");
2171
- return;
2172
- }
2173
- const REVEAL_STEPS = 10;
2174
- const REVEAL_DELAY = 30;
2175
- for (let lineIdx = 0; lineIdx < totalLines; lineIdx++) {
2176
- const line = INSFORGE_BANNER[lineIdx];
2177
- for (let step = 0; step <= REVEAL_STEPS; step++) {
2178
- const pos = Math.floor(step / REVEAL_STEPS * line.length);
2179
- let rendered = "";
2180
- for (let i = 0; i < line.length; i++) {
2181
- if (i < pos) {
2182
- rendered += `\x1B[97m${line[i]}\x1B[0m`;
2183
- } else if (i === pos) {
2184
- rendered += `\x1B[1;37m${line[i]}\x1B[0m`;
2185
- } else {
2186
- rendered += `\x1B[90m${line[i]}\x1B[0m`;
2187
- }
2188
- }
2189
- process.stderr.write(`\r${rendered}`);
2190
- await new Promise((r) => setTimeout(r, REVEAL_DELAY));
2191
- }
2192
- process.stderr.write("\n");
2193
- }
2194
- const SHIMMER_STEPS = 16;
2195
- const SHIMMER_DELAY = 40;
2196
- const SHIMMER_WIDTH = 4;
2197
- for (let step = 0; step < SHIMMER_STEPS; step++) {
2198
- const shimmerPos = Math.floor(step / SHIMMER_STEPS * (maxLen + SHIMMER_WIDTH));
2199
- process.stderr.write(`\x1B[${totalLines}A`);
2200
- for (const line of INSFORGE_BANNER) {
2201
- let rendered = "";
2202
- for (let i = 0; i < line.length; i++) {
2203
- const dist = Math.abs(i - shimmerPos);
2204
- if (dist === 0) {
2205
- rendered += `\x1B[1;97m${line[i]}\x1B[0m`;
2206
- } else if (dist <= SHIMMER_WIDTH) {
2207
- rendered += `\x1B[37m${line[i]}\x1B[0m`;
2208
- } else {
2209
- rendered += `\x1B[90m${line[i]}\x1B[0m`;
2511
+ // src/commands/storage/buckets.ts
2512
+ function registerStorageBucketsCommand(storageCmd2) {
2513
+ storageCmd2.command("buckets").description("List all storage buckets").action(async (_opts, cmd) => {
2514
+ const { json } = getRootOpts(cmd);
2515
+ try {
2516
+ await requireAuth();
2517
+ const res = await ossFetch("/api/storage/buckets");
2518
+ const raw = await res.json();
2519
+ const buckets = Array.isArray(raw) ? raw : raw && typeof raw === "object" && "buckets" in raw ? raw.buckets ?? [] : [];
2520
+ if (json) {
2521
+ outputJson(raw);
2522
+ } else {
2523
+ if (buckets.length === 0) {
2524
+ console.log("No buckets found.");
2525
+ return;
2210
2526
  }
2527
+ outputTable(
2528
+ ["Bucket Name", "Public"],
2529
+ buckets.map((b) => [b.name, b.public ? "Yes" : "No"])
2530
+ );
2211
2531
  }
2212
- process.stderr.write(`${rendered}
2213
- `);
2214
- }
2215
- await new Promise((r) => setTimeout(r, SHIMMER_DELAY));
2216
- }
2217
- process.stderr.write(`\x1B[${totalLines}A`);
2218
- for (const line of INSFORGE_BANNER) {
2219
- process.stderr.write(`\x1B[97m${line}\x1B[0m
2220
- `);
2221
- }
2222
- process.stderr.write("\n");
2223
- }
2224
- function getDefaultProjectName() {
2225
- const dirName = path3.basename(process.cwd());
2226
- const sanitized = dirName.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
2227
- return sanitized.length >= 2 ? sanitized : "";
2228
- }
2229
- async function copyDir(src, dest) {
2230
- const entries = await fs3.readdir(src, { withFileTypes: true });
2231
- for (const entry of entries) {
2232
- const srcPath = path3.join(src, entry.name);
2233
- const destPath = path3.join(dest, entry.name);
2234
- if (entry.isDirectory()) {
2235
- await fs3.mkdir(destPath, { recursive: true });
2236
- await copyDir(srcPath, destPath);
2237
- } else {
2238
- await fs3.copyFile(srcPath, destPath);
2532
+ await reportCliUsage("cli.storage.buckets", true);
2533
+ } catch (err) {
2534
+ await reportCliUsage("cli.storage.buckets", false);
2535
+ handleError(err, json);
2239
2536
  }
2240
- }
2537
+ });
2241
2538
  }
2242
- function registerCreateCommand(program2) {
2243
- 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, or empty").action(async (opts, cmd) => {
2244
- const { json, apiUrl } = getRootOpts(cmd);
2539
+
2540
+ // src/commands/storage/upload.ts
2541
+ import { readFileSync as readFileSync5, existsSync as existsSync4 } from "fs";
2542
+ import { basename as basename5 } from "path";
2543
+ function registerStorageUploadCommand(storageCmd2) {
2544
+ 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) => {
2545
+ const { json } = getRootOpts(cmd);
2245
2546
  try {
2246
- await requireAuth(apiUrl, false);
2247
- if (!json) {
2248
- await animateBanner();
2249
- clack10.intro("Let's build something great");
2250
- }
2251
- let orgId = opts.orgId;
2252
- if (!orgId) {
2253
- const orgs = await listOrganizations(apiUrl);
2254
- if (orgs.length === 0) {
2255
- throw new CLIError("No organizations found.");
2256
- }
2257
- if (json) {
2258
- throw new CLIError("Specify --org-id in JSON mode.");
2259
- }
2260
- const selected = await clack10.select({
2261
- message: "Select an organization:",
2262
- options: orgs.map((o) => ({
2263
- value: o.id,
2264
- label: o.name
2265
- }))
2266
- });
2267
- if (clack10.isCancel(selected)) process.exit(0);
2268
- orgId = selected;
2269
- }
2270
- const globalConfig = getGlobalConfig();
2271
- globalConfig.default_org_id = orgId;
2272
- saveGlobalConfig(globalConfig);
2273
- let projectName = opts.name;
2274
- if (!projectName) {
2275
- if (json) throw new CLIError("--name is required in JSON mode.");
2276
- const defaultName = getDefaultProjectName();
2277
- const name = await clack10.text({
2278
- message: "Project name:",
2279
- ...defaultName ? { initialValue: defaultName } : {},
2280
- validate: (v) => v.length >= 2 ? void 0 : "Name must be at least 2 characters"
2281
- });
2282
- if (clack10.isCancel(name)) process.exit(0);
2283
- projectName = name;
2547
+ await requireAuth();
2548
+ const config = getProjectConfig();
2549
+ if (!config) throw new ProjectNotLinkedError();
2550
+ if (!existsSync4(file)) {
2551
+ throw new CLIError(`File not found: ${file}`);
2284
2552
  }
2285
- projectName = path3.basename(projectName).replace(/[^a-zA-Z0-9._-]/g, "-").replace(/\.+/g, ".");
2286
- if (projectName.length < 2 || projectName === "." || projectName === "..") {
2287
- throw new CLIError("Project name must be at least 2 safe characters (letters, numbers, hyphens).");
2553
+ const fileContent = readFileSync5(file);
2554
+ const objectKey = opts.key ?? basename5(file);
2555
+ const bucketName = opts.bucket;
2556
+ const formData = new FormData();
2557
+ const blob = new Blob([fileContent]);
2558
+ formData.append("file", blob, objectKey);
2559
+ const url = `${config.oss_host}/api/storage/buckets/${encodeURIComponent(bucketName)}/objects/${encodeURIComponent(objectKey)}`;
2560
+ const res = await fetch(url, {
2561
+ method: "PUT",
2562
+ headers: {
2563
+ Authorization: `Bearer ${config.api_key}`
2564
+ },
2565
+ body: formData
2566
+ });
2567
+ if (!res.ok) {
2568
+ const err = await res.json().catch(() => ({}));
2569
+ throw new CLIError(err.error ?? `Upload failed: ${res.status}`);
2288
2570
  }
2289
- const validTemplates = ["react", "nextjs", "chatbot", "crm", "e-commerce", "empty"];
2290
- let template = opts.template;
2291
- if (template && !validTemplates.includes(template)) {
2292
- throw new CLIError(`Invalid template "${template}". Valid options: ${validTemplates.join(", ")}`);
2571
+ const data = await res.json();
2572
+ if (json) {
2573
+ outputJson(data);
2574
+ } else {
2575
+ outputSuccess(`Uploaded "${basename5(file)}" to bucket "${bucketName}".`);
2293
2576
  }
2294
- if (!template) {
2295
- if (json) {
2296
- template = "empty";
2297
- } else {
2298
- const approach = await clack10.select({
2299
- message: "How would you like to start?",
2300
- options: [
2301
- { value: "blank", label: "Blank project", hint: "Start from scratch with .env.local ready" },
2302
- { value: "template", label: "Start from a template", hint: "Pre-built starter apps" }
2303
- ]
2304
- });
2305
- if (clack10.isCancel(approach)) process.exit(0);
2306
- captureEvent(orgId, "create_approach_selected", {
2307
- approach
2308
- });
2309
- if (approach === "blank") {
2310
- template = "empty";
2311
- } else {
2312
- const selected = await clack10.select({
2313
- message: "Choose a starter template:",
2314
- options: [
2315
- { value: "react", label: "Web app template with React" },
2316
- { value: "nextjs", label: "Web app template with Next.js" },
2317
- { value: "chatbot", label: "AI Chatbot with Next.js" },
2318
- { value: "crm", label: "CRM with Next.js" },
2319
- { value: "e-commerce", label: "E-Commerce store with Next.js" }
2320
- ]
2321
- });
2322
- if (clack10.isCancel(selected)) process.exit(0);
2323
- template = selected;
2324
- }
2577
+ } catch (err) {
2578
+ handleError(err, json);
2579
+ }
2580
+ });
2581
+ }
2582
+
2583
+ // src/commands/storage/download.ts
2584
+ import { writeFileSync as writeFileSync3 } from "fs";
2585
+ import { join as join7, basename as basename6 } from "path";
2586
+ function registerStorageDownloadCommand(storageCmd2) {
2587
+ 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) => {
2588
+ const { json } = getRootOpts(cmd);
2589
+ try {
2590
+ await requireAuth();
2591
+ const config = getProjectConfig();
2592
+ if (!config) throw new ProjectNotLinkedError();
2593
+ const bucketName = opts.bucket;
2594
+ const url = `${config.oss_host}/api/storage/buckets/${encodeURIComponent(bucketName)}/objects/${encodeURIComponent(objectKey)}`;
2595
+ const res = await fetch(url, {
2596
+ headers: {
2597
+ Authorization: `Bearer ${config.api_key}`
2325
2598
  }
2326
- }
2327
- captureEvent(orgId, "template_selected", {
2328
- template,
2329
- approach: template === "empty" ? "blank" : "template"
2330
2599
  });
2331
- const s = !json ? clack10.spinner() : null;
2332
- s?.start("Creating project...");
2333
- const project = await createProject(orgId, projectName, opts.region, apiUrl);
2334
- s?.message("Waiting for project to become active...");
2335
- await waitForProjectActive(project.id, apiUrl);
2336
- const apiKey = await getProjectApiKey(project.id, apiUrl);
2337
- const projectConfig = {
2338
- project_id: project.id,
2339
- project_name: project.name,
2340
- org_id: project.organization_id,
2341
- appkey: project.appkey,
2342
- region: project.region,
2343
- api_key: apiKey,
2344
- oss_host: buildOssHost2(project.appkey, project.region)
2345
- };
2346
- saveProjectConfig(projectConfig);
2347
- s?.stop(`Project "${project.name}" created and linked`);
2348
- const hasTemplate = template !== "empty";
2349
- const githubTemplates = ["chatbot", "crm", "e-commerce", "nextjs", "react"];
2350
- if (githubTemplates.includes(template)) {
2351
- await downloadGitHubTemplate(template, projectConfig, json);
2352
- } else if (hasTemplate) {
2353
- await downloadTemplate(template, projectConfig, projectName, json, apiUrl);
2600
+ if (!res.ok) {
2601
+ const err = await res.json().catch(() => ({}));
2602
+ throw new CLIError(err.error ?? `Download failed: ${res.status}`);
2603
+ }
2604
+ const buffer = Buffer.from(await res.arrayBuffer());
2605
+ const outputPath = opts.output ?? join7(process.cwd(), basename6(objectKey));
2606
+ writeFileSync3(outputPath, buffer);
2607
+ if (json) {
2608
+ outputJson({ success: true, path: outputPath, size: buffer.length });
2354
2609
  } else {
2355
- try {
2356
- const anonKey = await getAnonKey();
2357
- if (!anonKey) {
2358
- if (!json) clack10.log.warn("Could not retrieve anon key. You can add it to .env.local manually.");
2359
- } else {
2360
- const envPath = path3.join(process.cwd(), ".env.local");
2361
- const envContent = [
2362
- "# InsForge",
2363
- `NEXT_PUBLIC_INSFORGE_URL=${projectConfig.oss_host}`,
2364
- `NEXT_PUBLIC_INSFORGE_ANON_KEY=${anonKey}`,
2365
- ""
2366
- ].join("\n");
2367
- await fs3.writeFile(envPath, envContent, { flag: "wx" });
2368
- if (!json) {
2369
- clack10.log.success("Created .env.local with your InsForge credentials");
2370
- }
2371
- }
2372
- } catch (err) {
2373
- const error = err;
2374
- if (!json) {
2375
- if (error.code === "EEXIST") {
2376
- clack10.log.warn(".env.local already exists; skipping InsForge key seeding.");
2377
- } else {
2378
- clack10.log.warn(`Failed to create .env.local: ${error.message}`);
2379
- }
2380
- }
2381
- }
2610
+ outputSuccess(`Downloaded "${objectKey}" to ${outputPath} (${buffer.length} bytes).`);
2382
2611
  }
2383
- await installSkills(json);
2384
- trackCommand("create", orgId);
2385
- await reportCliUsage("cli.create", true, 6);
2386
- if (hasTemplate) {
2387
- const installSpinner = !json ? clack10.spinner() : null;
2388
- installSpinner?.start("Installing dependencies...");
2389
- try {
2390
- await execAsync2("npm install", { cwd: process.cwd(), maxBuffer: 10 * 1024 * 1024 });
2391
- installSpinner?.stop("Dependencies installed");
2392
- } catch (err) {
2393
- installSpinner?.stop("Failed to install dependencies");
2394
- if (!json) {
2395
- clack10.log.warn(`npm install failed: ${err.message}`);
2396
- clack10.log.info("Run `npm install` manually to install dependencies.");
2397
- }
2398
- }
2612
+ } catch (err) {
2613
+ handleError(err, json);
2614
+ }
2615
+ });
2616
+ }
2617
+
2618
+ // src/commands/storage/create-bucket.ts
2619
+ function registerStorageCreateBucketCommand(storageCmd2) {
2620
+ storageCmd2.command("create-bucket <name>").description("Create a new storage bucket").option("--public", "Make the bucket publicly accessible (default)").option("--private", "Make the bucket private").action(async (name, opts, cmd) => {
2621
+ const { json } = getRootOpts(cmd);
2622
+ try {
2623
+ await requireAuth();
2624
+ const isPublic = !opts.private;
2625
+ const res = await ossFetch("/api/storage/buckets", {
2626
+ method: "POST",
2627
+ body: JSON.stringify({ bucketName: name, isPublic })
2628
+ });
2629
+ const data = await res.json();
2630
+ if (json) {
2631
+ outputJson(data);
2632
+ } else {
2633
+ outputSuccess(`Bucket "${name}" created (${isPublic ? "public" : "private"}).`);
2399
2634
  }
2400
- let liveUrl = null;
2401
- if (hasTemplate && !json) {
2402
- const shouldDeploy = await clack10.confirm({
2403
- message: "Would you like to deploy now?"
2635
+ await reportCliUsage("cli.storage.create-bucket", true);
2636
+ } catch (err) {
2637
+ await reportCliUsage("cli.storage.create-bucket", false);
2638
+ handleError(err, json);
2639
+ }
2640
+ });
2641
+ }
2642
+
2643
+ // src/commands/storage/delete-bucket.ts
2644
+ import * as clack10 from "@clack/prompts";
2645
+ function registerStorageDeleteBucketCommand(storageCmd2) {
2646
+ storageCmd2.command("delete-bucket <name>").description("Delete a storage bucket and all its objects").action(async (name, _opts, cmd) => {
2647
+ const { json, yes } = getRootOpts(cmd);
2648
+ try {
2649
+ await requireAuth();
2650
+ if (!yes && !json) {
2651
+ const confirm8 = await clack10.confirm({
2652
+ message: `Delete bucket "${name}" and all its objects? This cannot be undone.`
2404
2653
  });
2405
- if (!clack10.isCancel(shouldDeploy) && shouldDeploy) {
2406
- try {
2407
- const envVars = await readEnvFile(process.cwd());
2408
- const startBody = {};
2409
- if (envVars.length > 0) {
2410
- startBody.envVars = envVars;
2411
- }
2412
- const deploySpinner = clack10.spinner();
2413
- const result = await deployProject({
2414
- sourceDir: process.cwd(),
2415
- startBody,
2416
- spinner: deploySpinner
2417
- });
2418
- if (result.isReady) {
2419
- deploySpinner.stop("Deployment complete");
2420
- liveUrl = result.liveUrl;
2421
- } else {
2422
- deploySpinner.stop("Deployment is still building");
2423
- clack10.log.info(`Deployment ID: ${result.deploymentId}`);
2424
- clack10.log.warn("Deployment did not finish within 2 minutes.");
2425
- clack10.log.info(`Check status with: npx @insforge/cli deployments status ${result.deploymentId}`);
2426
- }
2427
- } catch (err) {
2428
- clack10.log.warn(`Deploy failed: ${err.message}`);
2429
- }
2654
+ if (!confirm8 || clack10.isCancel(confirm8)) {
2655
+ process.exit(0);
2430
2656
  }
2431
2657
  }
2432
- const dashboardUrl = `${getFrontendUrl()}/dashboard/project/${project.id}`;
2658
+ const res = await ossFetch(`/api/storage/buckets/${encodeURIComponent(name)}`, {
2659
+ method: "DELETE"
2660
+ });
2661
+ const data = await res.json();
2433
2662
  if (json) {
2434
- outputJson({
2435
- success: true,
2436
- project: { id: project.id, name: project.name, appkey: project.appkey, region: project.region },
2437
- template,
2438
- urls: {
2439
- dashboard: dashboardUrl,
2440
- ...liveUrl ? { liveSite: liveUrl } : {}
2441
- }
2442
- });
2663
+ outputJson(data);
2443
2664
  } else {
2444
- clack10.log.step(`Dashboard: ${dashboardUrl}`);
2445
- if (liveUrl) {
2446
- clack10.log.success(`Live site: ${liveUrl}`);
2447
- }
2448
- clack10.outro("Done!");
2665
+ outputSuccess(`Bucket "${name}" deleted.`);
2449
2666
  }
2450
2667
  } catch (err) {
2451
2668
  handleError(err, json);
2452
- } finally {
2453
- await shutdownAnalytics();
2454
2669
  }
2455
2670
  });
2456
2671
  }
2457
- async function downloadTemplate(framework, projectConfig, projectName, json, _apiUrl) {
2458
- const s = !json ? clack10.spinner() : null;
2459
- s?.start("Downloading template...");
2460
- try {
2461
- const anonKey = await getAnonKey();
2462
- if (!anonKey) {
2463
- throw new Error("Failed to retrieve anon key from backend");
2464
- }
2465
- const tempDir = tmpdir();
2466
- const targetDir = projectName;
2467
- const templatePath = path3.join(tempDir, targetDir);
2468
- try {
2469
- await fs3.rm(templatePath, { recursive: true, force: true });
2470
- } catch {
2471
- }
2472
- const frame = framework === "nextjs" ? "nextjs" : "react";
2473
- const esc = (s2) => process.platform === "win32" ? `"${s2.replace(/"/g, '\\"')}"` : `'${s2.replace(/'/g, "'\\''")}'`;
2474
- const command = `npx --yes create-insforge-app@latest ${esc(targetDir)} --frame ${frame} --base-url ${esc(projectConfig.oss_host)} --anon-key ${esc(anonKey)} --skip-install`;
2475
- s?.message(`Running create-insforge-app (${frame})...`);
2476
- await execAsync2(command, {
2477
- maxBuffer: 10 * 1024 * 1024,
2478
- cwd: tempDir
2479
- });
2480
- s?.message("Copying template files...");
2481
- const cwd = process.cwd();
2482
- await copyDir(templatePath, cwd);
2483
- await fs3.rm(templatePath, { recursive: true, force: true }).catch(() => {
2484
- });
2485
- s?.stop("Template files downloaded");
2486
- } catch (err) {
2487
- s?.stop("Template download failed");
2488
- if (!json) {
2489
- clack10.log.warn(`Failed to download template: ${err.message}`);
2490
- clack10.log.info("You can manually set up the template later.");
2491
- }
2492
- }
2672
+
2673
+ // src/commands/storage/list-objects.ts
2674
+ function formatSize(bytes) {
2675
+ if (bytes < 1024) return `${bytes} B`;
2676
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
2677
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
2493
2678
  }
2494
- async function downloadGitHubTemplate(templateName, projectConfig, json) {
2495
- const s = !json ? clack10.spinner() : null;
2496
- s?.start(`Downloading ${templateName} template...`);
2497
- const tempDir = path3.join(tmpdir(), `insforge-template-${Date.now()}`);
2498
- try {
2499
- await fs3.mkdir(tempDir, { recursive: true });
2500
- await execAsync2(
2501
- "git clone --depth 1 https://github.com/InsForge/insforge-templates.git .",
2502
- { cwd: tempDir, maxBuffer: 10 * 1024 * 1024, timeout: 6e4 }
2503
- );
2504
- const templateDir = path3.join(tempDir, templateName);
2505
- const stat4 = await fs3.stat(templateDir).catch(() => null);
2506
- if (!stat4?.isDirectory()) {
2507
- throw new Error(`Template "${templateName}" not found in repository`);
2508
- }
2509
- s?.message("Copying template files...");
2510
- const cwd = process.cwd();
2511
- await copyDir(templateDir, cwd);
2512
- const envExamplePath = path3.join(cwd, ".env.example");
2513
- const envExampleExists = await fs3.stat(envExamplePath).catch(() => null);
2514
- if (envExampleExists) {
2515
- const anonKey = await getAnonKey();
2516
- const envExample = await fs3.readFile(envExamplePath, "utf-8");
2517
- const envContent = envExample.replace(
2518
- /^([A-Z][A-Z0-9_]*=)(.*)$/gm,
2519
- (_, prefix, _value) => {
2520
- const key = prefix.slice(0, -1);
2521
- if (/INSFORGE.*(URL|BASE_URL)$/.test(key)) return `${prefix}${projectConfig.oss_host}`;
2522
- if (/INSFORGE.*ANON_KEY$/.test(key)) return `${prefix}${anonKey}`;
2523
- if (key === "NEXT_PUBLIC_APP_URL") return `${prefix}https://${projectConfig.appkey}.insforge.site`;
2524
- return `${prefix}${_value}`;
2525
- }
2679
+ function registerStorageListObjectsCommand(storageCmd2) {
2680
+ storageCmd2.command("list-objects <bucket>").description("List objects in a storage bucket").option("--limit <n>", "Maximum number of objects to return", "100").option("--offset <n>", "Number of objects to skip", "0").option("--prefix <prefix>", "Filter objects by key prefix").option("--search <term>", "Search objects by key (partial match)").option("--sort <field>", "Sort by field: key, size, uploadedAt (default: key)").action(async (bucket, opts, cmd) => {
2681
+ const { json } = getRootOpts(cmd);
2682
+ try {
2683
+ await requireAuth();
2684
+ const params = new URLSearchParams();
2685
+ params.set("limit", opts.limit);
2686
+ params.set("offset", opts.offset);
2687
+ if (opts.prefix) params.set("prefix", opts.prefix);
2688
+ if (opts.search) params.set("search", opts.search);
2689
+ const res = await ossFetch(
2690
+ `/api/storage/buckets/${encodeURIComponent(bucket)}/objects?${params.toString()}`
2526
2691
  );
2527
- const envLocalPath = path3.join(cwd, ".env.local");
2528
- try {
2529
- await fs3.writeFile(envLocalPath, envContent, { flag: "wx" });
2530
- } catch (e) {
2531
- if (e.code === "EEXIST") {
2532
- if (!json) clack10.log.warn(".env.local already exists; skipping env seeding.");
2533
- } else {
2534
- throw e;
2692
+ const raw = await res.json();
2693
+ const objects = Array.isArray(raw) ? raw : raw.data ?? [];
2694
+ const sortField = opts.sort ?? "key";
2695
+ objects.sort((a, b) => {
2696
+ if (sortField === "size") return a.size - b.size;
2697
+ if (sortField === "uploadedAt") return a.uploadedAt.localeCompare(b.uploadedAt);
2698
+ return a.key.localeCompare(b.key);
2699
+ });
2700
+ if (json) {
2701
+ outputJson(raw);
2702
+ } else {
2703
+ if (objects.length === 0) {
2704
+ console.log(`No objects found in bucket "${bucket}".`);
2705
+ return;
2535
2706
  }
2536
- }
2537
- }
2538
- s?.stop(`${templateName} template downloaded`);
2539
- const migrationPath = path3.join(cwd, "migrations", "db_init.sql");
2540
- const migrationExists = await fs3.stat(migrationPath).catch(() => null);
2541
- if (migrationExists) {
2542
- const dbSpinner = !json ? clack10.spinner() : null;
2543
- dbSpinner?.start("Running database migrations...");
2544
- try {
2545
- const sql = await fs3.readFile(migrationPath, "utf-8");
2546
- await runRawSql(sql, true);
2547
- dbSpinner?.stop("Database migrations applied");
2548
- } catch (err) {
2549
- dbSpinner?.stop("Database migration failed");
2550
- if (!json) {
2551
- clack10.log.warn(`Migration failed: ${err.message}`);
2552
- clack10.log.info('You can run the migration manually: npx @insforge/cli db query --unrestricted "$(cat migrations/db_init.sql)"');
2553
- } else {
2554
- throw err;
2707
+ const total = raw.pagination?.total;
2708
+ if (total !== void 0) {
2709
+ console.log(`Showing ${objects.length} of ${total} objects:
2710
+ `);
2555
2711
  }
2712
+ outputTable(
2713
+ ["Key", "Size", "Type", "Uploaded At"],
2714
+ objects.map((o) => [
2715
+ o.key,
2716
+ formatSize(o.size),
2717
+ o.mimeType ?? "-",
2718
+ o.uploadedAt
2719
+ ])
2720
+ );
2556
2721
  }
2722
+ await reportCliUsage("cli.storage.list-objects", true);
2723
+ } catch (err) {
2724
+ await reportCliUsage("cli.storage.list-objects", false);
2725
+ handleError(err, json);
2557
2726
  }
2558
- } catch (err) {
2559
- s?.stop(`${templateName} template download failed`);
2560
- if (!json) {
2561
- clack10.log.warn(`Failed to download ${templateName} template: ${err.message}`);
2562
- clack10.log.info("You can manually clone from: https://github.com/InsForge/insforge-templates");
2563
- }
2564
- } finally {
2565
- await fs3.rm(tempDir, { recursive: true, force: true }).catch(() => {
2566
- });
2567
- }
2727
+ });
2568
2728
  }
2569
2729
 
2570
2730
  // src/commands/info.ts
@@ -2902,8 +3062,8 @@ async function listDocs(json) {
2902
3062
  );
2903
3063
  }
2904
3064
  }
2905
- async function fetchDoc(path4, label, json) {
2906
- const res = await ossFetch(path4);
3065
+ async function fetchDoc(path5, label, json) {
3066
+ const res = await ossFetch(path5);
2907
3067
  const data = await res.json();
2908
3068
  const doc = data.data ?? data;
2909
3069
  if (json) {
@@ -3554,10 +3714,10 @@ function registerComputeLogsCommand(computeCmd2) {
3554
3714
 
3555
3715
  // src/commands/compute/deploy.ts
3556
3716
  import { existsSync as existsSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync4, unlinkSync as unlinkSync2, renameSync } from "fs";
3557
- import { join as join7 } from "path";
3717
+ import { join as join8 } from "path";
3558
3718
  import { execSync, spawn } from "child_process";
3559
3719
  function parseFlyToml(dir) {
3560
- const tomlPath = join7(dir, "fly.toml");
3720
+ const tomlPath = join8(dir, "fly.toml");
3561
3721
  if (!existsSync5(tomlPath)) return {};
3562
3722
  const content = readFileSync6(tomlPath, "utf-8");
3563
3723
  const config = {};
@@ -3629,7 +3789,7 @@ function registerComputeDeployCommand(computeCmd2) {
3629
3789
  checkFlyctl();
3630
3790
  const flyToken = getFlyToken();
3631
3791
  const dir = directory ?? process.cwd();
3632
- const dockerfilePath = join7(dir, "Dockerfile");
3792
+ const dockerfilePath = join8(dir, "Dockerfile");
3633
3793
  if (!existsSync5(dockerfilePath)) {
3634
3794
  throw new CLIError(`No Dockerfile found in ${dir}`);
3635
3795
  }
@@ -3675,8 +3835,8 @@ function registerComputeDeployCommand(computeCmd2) {
3675
3835
  serviceId = service.id;
3676
3836
  flyAppId = service.flyAppId;
3677
3837
  }
3678
- const existingTomlPath = join7(dir, "fly.toml");
3679
- const backupTomlPath = join7(dir, "fly.toml.insforge-backup");
3838
+ const existingTomlPath = join8(dir, "fly.toml");
3839
+ const backupTomlPath = join8(dir, "fly.toml.insforge-backup");
3680
3840
  let hadExistingToml = false;
3681
3841
  if (existsSync5(existingTomlPath)) {
3682
3842
  hadExistingToml = true;
@@ -3686,14 +3846,14 @@ function registerComputeDeployCommand(computeCmd2) {
3686
3846
  writeFileSync4(existingTomlPath, tomlContent);
3687
3847
  try {
3688
3848
  if (!json) outputInfo("Building and deploying (this may take a few minutes)...");
3689
- await new Promise((resolve2, reject) => {
3849
+ await new Promise((resolve4, reject) => {
3690
3850
  const child = spawn(
3691
3851
  "flyctl",
3692
3852
  ["deploy", "--remote-only", "--app", flyAppId, "--access-token", flyToken, "--yes"],
3693
3853
  { cwd: dir, stdio: json ? "pipe" : "inherit" }
3694
3854
  );
3695
3855
  child.on("close", (code) => {
3696
- if (code === 0) resolve2();
3856
+ if (code === 0) resolve4();
3697
3857
  else reject(new CLIError(`flyctl deploy failed with exit code ${code}`));
3698
3858
  });
3699
3859
  child.on("error", (err) => reject(new CLIError(`flyctl deploy error: ${err.message}`)));
@@ -4477,7 +4637,7 @@ function formatBytesCompact(bytes) {
4477
4637
 
4478
4638
  // src/index.ts
4479
4639
  var __dirname = dirname(fileURLToPath(import.meta.url));
4480
- var pkg = JSON.parse(readFileSync7(join8(__dirname, "../package.json"), "utf-8"));
4640
+ var pkg = JSON.parse(readFileSync7(join9(__dirname, "../package.json"), "utf-8"));
4481
4641
  var INSFORGE_LOGO = `
4482
4642
  \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
4483
4643
  \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