@insforge/cli 0.1.41 → 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
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { readFileSync as readFileSync6 } from "fs";
5
- import { join as join7, dirname } from "path";
4
+ import { readFileSync as readFileSync7 } from "fs";
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";
@@ -797,142 +801,54 @@ async function reportCliUsage(toolName, success, maxRetries = 1) {
797
801
  }
798
802
  }
799
803
 
800
- // src/commands/projects/link.ts
801
- function buildOssHost(appkey, region) {
802
- return `https://${appkey}.${region}.insforge.app`;
804
+ // src/lib/analytics.ts
805
+ import { PostHog } from "posthog-node";
806
+ var POSTHOG_API_KEY = "phc_ueV1ii62wdBTkH7E70ugyeqHIHu8dFDdjs0qq3TZhJz";
807
+ var POSTHOG_HOST = process.env.POSTHOG_HOST || "https://us.i.posthog.com";
808
+ var client = null;
809
+ function getClient() {
810
+ if (!POSTHOG_API_KEY) return null;
811
+ if (!client) {
812
+ client = new PostHog(POSTHOG_API_KEY, { host: POSTHOG_HOST });
813
+ }
814
+ return client;
803
815
  }
804
- function registerProjectLinkCommand(program2) {
805
- 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) => {
806
- const { json, apiUrl } = getRootOpts(cmd);
807
- try {
808
- if (opts.apiBaseUrl || opts.apiKey) {
809
- try {
810
- if (!opts.apiBaseUrl || !opts.apiKey) {
811
- throw new CLIError("Both --api-base-url and --api-key must be provided together for direct linking.");
812
- }
813
- try {
814
- new URL(opts.apiBaseUrl);
815
- } catch {
816
- throw new CLIError("Invalid --api-base-url. Please provide a valid URL.");
817
- }
818
- const projectConfig2 = {
819
- project_id: "oss-project",
820
- project_name: "oss-project",
821
- org_id: "oss-org",
822
- appkey: "oss",
823
- region: "local",
824
- api_key: opts.apiKey,
825
- oss_host: opts.apiBaseUrl.replace(/\/$/, "")
826
- // remove trailing slash if any
827
- };
828
- saveProjectConfig(projectConfig2);
829
- if (json) {
830
- outputJson({ success: true, project: { id: projectConfig2.project_id, name: projectConfig2.project_name, region: projectConfig2.region } });
831
- } else {
832
- outputSuccess(`Linked to direct project at ${projectConfig2.oss_host}`);
833
- }
834
- await installSkills(json);
835
- await reportCliUsage("cli.link_direct", true, 6);
836
- try {
837
- const urlMatch = opts.apiBaseUrl.match(/^https?:\/\/([^.]+)\.[^.]+\.insforge\.app/);
838
- if (urlMatch) {
839
- await reportAgentConnected({ app_key: urlMatch[1] }, apiUrl);
840
- }
841
- } catch {
842
- }
843
- return;
844
- } catch (err) {
845
- await reportCliUsage("cli.link_direct", false);
846
- handleError(err, json);
847
- }
848
- }
849
- const creds = await requireAuth(apiUrl, false);
850
- let orgId = opts.orgId;
851
- let projectId = opts.projectId;
852
- if (!orgId && !projectId) {
853
- const orgs = await listOrganizations(apiUrl);
854
- if (orgs.length === 0) {
855
- throw new CLIError("No organizations found.");
856
- }
857
- if (json) {
858
- throw new CLIError("Specify --org-id in JSON mode.");
859
- }
860
- const selected = await clack6.select({
861
- message: "Select an organization:",
862
- options: orgs.map((o) => ({
863
- value: o.id,
864
- label: o.name
865
- }))
866
- });
867
- if (clack6.isCancel(selected)) process.exit(0);
868
- orgId = selected;
869
- }
870
- const config = getGlobalConfig();
871
- config.default_org_id = orgId;
872
- saveGlobalConfig(config);
873
- if (!projectId) {
874
- const projects = await listProjects(orgId, apiUrl);
875
- if (projects.length === 0) {
876
- throw new CLIError("No projects found in this organization.");
877
- }
878
- if (json) {
879
- throw new CLIError("Specify --project-id in JSON mode.");
880
- }
881
- const selected = await clack6.select({
882
- message: "Select a project to link:",
883
- options: projects.map((p) => ({
884
- value: p.id,
885
- label: `${p.name} (${p.region}, ${p.status})`
886
- }))
887
- });
888
- if (clack6.isCancel(selected)) process.exit(0);
889
- projectId = selected;
890
- }
891
- let project;
892
- let apiKey;
893
- try {
894
- [project, apiKey] = await Promise.all([
895
- getProject(projectId, apiUrl),
896
- getProjectApiKey(projectId, apiUrl)
897
- ]);
898
- } catch (err) {
899
- if (err instanceof CLIError && (err.exitCode === 5 || err.exitCode === 4 || err.message.includes("not found"))) {
900
- const identity = creds.user?.email ?? creds.user?.name ?? "unknown user";
901
- throw new CLIError(
902
- `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.`,
903
- 5,
904
- "PERMISSION_DENIED"
905
- );
906
- }
907
- throw err;
908
- }
909
- const projectConfig = {
910
- project_id: project.id,
911
- project_name: project.name,
912
- org_id: project.organization_id,
913
- appkey: project.appkey,
914
- region: project.region,
915
- api_key: apiKey,
916
- oss_host: buildOssHost(project.appkey, project.region)
917
- };
918
- saveProjectConfig(projectConfig);
919
- if (json) {
920
- outputJson({ success: true, project: { id: project.id, name: project.name, region: project.region } });
921
- } else {
922
- outputSuccess(`Linked to project "${project.name}" (${project.appkey}.${project.region})`);
923
- }
924
- await installSkills(json);
925
- await reportCliUsage("cli.link", true, 6);
926
- try {
927
- await reportAgentConnected({ project_id: project.id }, apiUrl);
928
- } catch {
929
- }
930
- } catch (err) {
931
- await reportCliUsage("cli.link", false);
932
- handleError(err, json);
933
- }
816
+ function captureEvent(distinctId, event, properties) {
817
+ try {
818
+ getClient()?.capture({ distinctId, event, properties });
819
+ } catch {
820
+ }
821
+ }
822
+ function trackCommand(command, distinctId, properties) {
823
+ captureEvent(distinctId, "cli_command_invoked", {
824
+ command,
825
+ ...properties
826
+ });
827
+ }
828
+ function trackDiagnose(subcommand, config) {
829
+ captureEvent(config.project_id, "cli_diagnose_invoked", {
830
+ subcommand,
831
+ project_id: config.project_id,
832
+ project_name: config.project_name,
833
+ org_id: config.org_id,
834
+ region: config.region,
835
+ oss_mode: config.project_id === "oss-project"
934
836
  });
935
837
  }
838
+ async function shutdownAnalytics() {
839
+ try {
840
+ if (client) await client.shutdown();
841
+ } catch {
842
+ }
843
+ }
844
+
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";
936
852
 
937
853
  // src/lib/api/oss.ts
938
854
  function requireProjectConfig() {
@@ -957,755 +873,1240 @@ async function getAnonKey() {
957
873
  const data = await res.json();
958
874
  return data.accessToken;
959
875
  }
960
- async function ossFetch(path4, options = {}) {
876
+ async function ossFetch(path5, options = {}) {
961
877
  const config = requireProjectConfig();
962
878
  const headers = {
963
879
  "Content-Type": "application/json",
964
880
  Authorization: `Bearer ${config.api_key}`,
965
881
  ...options.headers ?? {}
966
882
  };
967
- const res = await fetch(`${config.oss_host}${path4}`, { ...options, headers });
883
+ const res = await fetch(`${config.oss_host}${path5}`, { ...options, headers });
968
884
  if (!res.ok) {
969
885
  const err = await res.json().catch(() => ({}));
970
- throw new CLIError(err.error ?? `OSS request failed: ${res.status}`);
886
+ let message = err.message ?? err.error ?? `OSS request failed: ${res.status}`;
887
+ if (err.nextActions) {
888
+ message += `
889
+ ${err.nextActions}`;
890
+ }
891
+ if (res.status === 404 && path5.startsWith("/api/compute")) {
892
+ message = "Compute services are not available on this backend.\nSelf-hosted: upgrade your InsForge instance. Cloud: contact your InsForge admin to enable compute.";
893
+ }
894
+ throw new CLIError(message);
971
895
  }
972
896
  return res;
973
897
  }
974
898
 
975
- // src/commands/db/query.ts
976
- function registerDbCommands(dbCmd2) {
977
- 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) => {
978
- const { json } = getRootOpts(cmd);
979
- try {
980
- await requireAuth();
981
- const { rows, raw } = await runRawSql(sql, !!opts.unrestricted);
982
- if (json) {
983
- outputJson(raw);
984
- } else {
985
- if (rows.length > 0) {
986
- const headers = Object.keys(rows[0]);
987
- outputTable(
988
- headers,
989
- rows.map((row) => headers.map((h) => String(row[h] ?? "")))
990
- );
991
- console.log(`${rows.length} row(s) returned.`);
992
- } else {
993
- console.log("Query executed successfully.");
994
- if (rows.length === 0) {
995
- console.log("No rows returned.");
996
- }
997
- }
998
- }
999
- await reportCliUsage("cli.db.query", true);
1000
- } catch (err) {
1001
- await reportCliUsage("cli.db.query", false);
1002
- handleError(err, json);
1003
- }
1004
- });
1005
- }
1006
-
1007
- // src/commands/db/tables.ts
1008
- function registerDbTablesCommand(dbCmd2) {
1009
- dbCmd2.command("tables").description("List all database tables").action(async (_opts, cmd) => {
1010
- const { json } = getRootOpts(cmd);
1011
- try {
1012
- await requireAuth();
1013
- const res = await ossFetch("/api/database/tables");
1014
- const tables = await res.json();
1015
- if (json) {
1016
- outputJson(tables);
1017
- } else {
1018
- if (tables.length === 0) {
1019
- console.log("No tables found.");
1020
- return;
1021
- }
1022
- outputTable(
1023
- ["Table Name"],
1024
- tables.map((t) => [t])
1025
- );
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);
1026
919
  }
1027
- await reportCliUsage("cli.db.tables", true);
1028
- } catch (err) {
1029
- await reportCliUsage("cli.db.tables", false);
1030
- handleError(err, json);
920
+ if (key) vars.push({ key, value });
1031
921
  }
1032
- });
922
+ return vars;
923
+ }
924
+ return [];
1033
925
  }
1034
926
 
1035
- // src/commands/db/functions.ts
1036
- function registerDbFunctionsCommand(dbCmd2) {
1037
- dbCmd2.command("functions").description("List all database functions").action(async (_opts, cmd) => {
1038
- const { json } = getRootOpts(cmd);
1039
- try {
1040
- await requireAuth();
1041
- const res = await ossFetch("/api/database/functions");
1042
- const raw = await res.json();
1043
- const functions = Array.isArray(raw) ? raw : raw.functions ?? [];
1044
- if (json) {
1045
- outputJson(raw);
1046
- } else {
1047
- if (functions.length === 0) {
1048
- console.log("No database functions found.");
1049
- return;
1050
- }
1051
- outputTable(
1052
- ["Name", "Definition", "Kind"],
1053
- functions.map((f) => [f.functionName, f.functionDef, f.kind])
1054
- );
1055
- }
1056
- await reportCliUsage("cli.db.functions", true);
1057
- } catch (err) {
1058
- await reportCliUsage("cli.db.functions", false);
1059
- 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;
1060
966
  }
967
+ }
968
+ if (normalized.endsWith(".log")) return true;
969
+ return false;
970
+ }
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();
1061
983
  });
1062
984
  }
1063
-
1064
- // src/commands/db/indexes.ts
1065
- function registerDbIndexesCommand(dbCmd2) {
1066
- dbCmd2.command("indexes").description("List all database indexes").action(async (_opts, cmd) => {
1067
- 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));
1068
1018
  try {
1069
- await requireAuth();
1070
- const res = await ossFetch("/api/database/indexes");
1071
- const raw = await res.json();
1072
- const indexes = Array.isArray(raw) ? raw : raw.indexes ?? [];
1073
- if (json) {
1074
- outputJson(raw);
1075
- } else {
1076
- if (indexes.length === 0) {
1077
- console.log("No database indexes found.");
1078
- return;
1079
- }
1080
- outputTable(
1081
- ["Table", "Index Name", "Definition", "Unique", "Primary"],
1082
- indexes.map((i) => [
1083
- i.tableName,
1084
- i.indexName,
1085
- i.indexDef,
1086
- i.isUnique ? "Yes" : "No",
1087
- i.isPrimary ? "Yes" : "No"
1088
- ])
1089
- );
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;
1090
1024
  }
1091
- 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})`);
1092
1031
  } catch (err) {
1093
- await reportCliUsage("cli.db.indexes", false);
1094
- handleError(err, json);
1032
+ if (err instanceof CLIError) throw err;
1095
1033
  }
1096
- });
1034
+ }
1035
+ const isReady = deployment?.status.toUpperCase() === "READY";
1036
+ const liveUrl = isReady ? deployment?.url ?? null : null;
1037
+ return { deploymentId, deployment, isReady, liveUrl };
1097
1038
  }
1098
-
1099
- // src/commands/db/policies.ts
1100
- function registerDbPoliciesCommand(dbCmd2) {
1101
- 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) => {
1102
1041
  const { json } = getRootOpts(cmd);
1103
1042
  try {
1104
1043
  await requireAuth();
1105
- const res = await ossFetch("/api/database/policies");
1106
- const raw = await res.json();
1107
- const policies = Array.isArray(raw) ? raw : raw.policies ?? [];
1108
- if (json) {
1109
- outputJson(raw);
1110
- } else {
1111
- if (policies.length === 0) {
1112
- console.log("No RLS policies found.");
1113
- 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.");
1114
1067
  }
1115
- outputTable(
1116
- ["Table", "Policy Name", "Command", "Roles", "Qual", "With Check"],
1117
- policies.map((p) => [
1118
- String(p.tableName ?? "-"),
1119
- String(p.policyName ?? "-"),
1120
- String(p.cmd ?? "-"),
1121
- Array.isArray(p.roles) ? p.roles.join(", ") : String(p.roles ?? "-"),
1122
- String(p.qual ?? "-"),
1123
- String(p.withCheck ?? "-")
1124
- ])
1125
- );
1126
1068
  }
1127
- await reportCliUsage("cli.db.policies", true);
1128
- } catch (err) {
1129
- await reportCliUsage("cli.db.policies", false);
1130
- handleError(err, json);
1131
- }
1132
- });
1133
- }
1134
-
1135
- // src/commands/db/triggers.ts
1136
- function registerDbTriggersCommand(dbCmd2) {
1137
- dbCmd2.command("triggers").description("List all database triggers").action(async (_opts, cmd) => {
1138
- const { json } = getRootOpts(cmd);
1139
- try {
1140
- await requireAuth();
1141
- const res = await ossFetch("/api/database/triggers");
1142
- const raw = await res.json();
1143
- const triggers = Array.isArray(raw) ? raw : raw.triggers ?? [];
1144
- if (json) {
1145
- outputJson(raw);
1069
+ if (opts.meta) {
1070
+ try {
1071
+ startBody.meta = JSON.parse(opts.meta);
1072
+ } catch {
1073
+ throw new CLIError("Invalid --meta JSON.");
1074
+ }
1075
+ }
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
+ }
1146
1087
  } else {
1147
- if (triggers.length === 0) {
1148
- console.log("No database triggers found.");
1149
- return;
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}`);
1150
1095
  }
1151
- outputTable(
1152
- ["Name", "Table", "Timing", "Events", "ActionOrientation", "ActionCondition", "ActionStatement"],
1153
- triggers.map((t) => [
1154
- t.triggerName,
1155
- t.tableName,
1156
- t.actionTiming,
1157
- t.eventManipulation,
1158
- t.actionOrientation,
1159
- t.actionCondition ?? "-",
1160
- t.actionStatement
1161
- ])
1162
- );
1163
1096
  }
1164
- await reportCliUsage("cli.db.triggers", true);
1097
+ await reportCliUsage("cli.deployments.deploy", true);
1165
1098
  } catch (err) {
1166
- await reportCliUsage("cli.db.triggers", false);
1099
+ await reportCliUsage("cli.deployments.deploy", false);
1167
1100
  handleError(err, json);
1168
1101
  }
1169
1102
  });
1170
1103
  }
1171
1104
 
1172
- // src/commands/db/rpc.ts
1173
- function registerDbRpcCommand(dbCmd2) {
1174
- 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) => {
1175
- const { json } = getRootOpts(cmd);
1176
- try {
1177
- await requireAuth();
1178
- const body = opts.data ? JSON.stringify(JSON.parse(opts.data)) : void 0;
1179
- const res = await ossFetch(`/api/database/rpc/${encodeURIComponent(functionName)}`, {
1180
- method: body ? "POST" : "GET",
1181
- ...body ? { body } : {}
1182
- });
1183
- const result = await res.json();
1184
- if (json) {
1185
- outputJson(result);
1186
- } else {
1187
- console.log(JSON.stringify(result, null, 2));
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
+ }
1188
1163
  }
1189
- await reportCliUsage("cli.db.rpc", true);
1190
- } catch (err) {
1191
- handleError(err, json);
1164
+ process.stderr.write(`\r${rendered}`);
1165
+ await new Promise((r) => setTimeout(r, REVEAL_DELAY));
1192
1166
  }
1193
- });
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");
1194
1198
  }
1195
-
1196
- // src/commands/db/export.ts
1197
- import { writeFileSync as writeFileSync2 } from "fs";
1198
- function registerDbExportCommand(dbCmd2) {
1199
- 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) => {
1200
- const { json } = getRootOpts(cmd);
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);
1201
1220
  try {
1202
- await requireAuth();
1203
- const body = {
1204
- format: opts.format,
1205
- includeData: opts.data !== false
1206
- };
1207
- if (opts.tables) {
1208
- body.tables = opts.tables.split(",").map((t) => t.trim());
1221
+ await requireAuth(apiUrl, false);
1222
+ if (!json) {
1223
+ await animateBanner();
1224
+ clack7.intro("Let's build something great");
1209
1225
  }
1210
- if (opts.includeFunctions) body.includeFunctions = true;
1211
- if (opts.includeSequences) body.includeSequences = true;
1212
- if (opts.includeViews) body.includeViews = true;
1213
- if (opts.rowLimit) body.rowLimit = parseInt(opts.rowLimit, 10);
1214
- const res = await ossFetch("/api/database/advance/export", {
1215
- method: "POST",
1216
- body: JSON.stringify(body)
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"
1217
1310
  });
1218
- const raw = await res.text();
1219
- let content;
1220
- let meta = null;
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;
1221
1344
  try {
1222
- const parsed = JSON.parse(raw);
1223
- if (typeof parsed.content === "string") {
1224
- content = parsed.content;
1225
- meta = { format: parsed.format, tables: parsed.tables };
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);
1226
1367
  } else {
1227
- content = raw;
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!");
1228
1486
  }
1229
- } catch {
1230
- content = raw;
1231
- }
1232
- if (json) {
1233
- outputJson(meta ?? { content });
1234
- return;
1235
- }
1236
- if (opts.output) {
1237
- writeFileSync2(opts.output, content);
1238
- const tableCount = meta?.tables?.length;
1239
- const suffix = tableCount ? ` (${tableCount} tables, format: ${meta?.format ?? opts.format})` : "";
1240
- outputSuccess(`Exported to ${opts.output}${suffix}`);
1241
- } else {
1242
- console.log(content);
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;
1243
1494
  }
1244
1495
  } catch (err) {
1245
1496
  handleError(err, json);
1497
+ } finally {
1498
+ await shutdownAnalytics();
1246
1499
  }
1247
1500
  });
1248
1501
  }
1249
-
1250
- // src/commands/db/import.ts
1251
- import { readFileSync as readFileSync3 } from "fs";
1252
- import { basename } from "path";
1253
- function registerDbImportCommand(dbCmd2) {
1254
- dbCmd2.command("import <file>").description("Import database from a local SQL file").option("--truncate", "Truncate existing tables before import").action(async (file, opts, cmd) => {
1255
- const { json } = getRootOpts(cmd);
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);
1256
1513
  try {
1257
- await requireAuth();
1258
- const config = getProjectConfig();
1259
- if (!config) throw new ProjectNotLinkedError();
1260
- const fileContent = readFileSync3(file);
1261
- const fileName = basename(file);
1262
- const formData = new FormData();
1263
- formData.append("file", new Blob([fileContent]), fileName);
1264
- if (opts.truncate) {
1265
- formData.append("truncate", "true");
1266
- }
1267
- const res = await fetch(`${config.oss_host}/api/database/advance/import`, {
1268
- method: "POST",
1269
- headers: {
1270
- Authorization: `Bearer ${config.api_key}`
1271
- },
1272
- body: formData
1273
- });
1274
- if (!res.ok) {
1275
- const err = await res.json().catch(() => ({}));
1276
- throw new CLIError(err.error ?? `Import failed: ${res.status}`);
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
+ }
1277
1581
  }
1278
- const data = await res.json();
1279
- if (json) {
1280
- outputJson(data);
1281
- } else {
1282
- outputSuccess(`Imported ${data.filename} (${data.tables.length} tables, ${data.rowsImported} rows)`);
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
+ }
1283
1601
  }
1284
- } catch (err) {
1285
- handleError(err, json);
1286
1602
  }
1287
- });
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
+ }
1288
1613
  }
1289
1614
 
1290
- // src/commands/records/list.ts
1291
- function registerRecordsCommands(recordsCmd2) {
1292
- recordsCmd2.command("list <table>").description("List records from a table").option("--select <columns>", "Columns to select (comma-separated)").option("--filter <filter>", 'Filter expression (e.g. "name=eq.John")').option("--order <order>", 'Order by (e.g. "created_at.desc")').option("--limit <n>", "Limit number of records", parseInt).option("--offset <n>", "Offset for pagination", parseInt).action(async (table, opts, cmd) => {
1293
- const { json } = getRootOpts(cmd);
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);
1294
1623
  try {
1295
- await requireAuth();
1296
- const params = new URLSearchParams();
1297
- if (opts.select) params.set("select", opts.select);
1298
- if (opts.filter) params.set(opts.filter.split("=")[0], opts.filter.split("=").slice(1).join("="));
1299
- if (opts.order) params.set("order", opts.order);
1300
- if (opts.limit) params.set("limit", String(opts.limit));
1301
- if (opts.offset) params.set("offset", String(opts.offset));
1302
- const query = params.toString();
1303
- const path4 = `/api/database/records/${encodeURIComponent(table)}${query ? `?${query}` : ""}`;
1304
- const res = await ossFetch(path4);
1305
- const data = await res.json();
1306
- const records = data.data ?? [];
1307
- if (json) {
1308
- outputJson(data);
1309
- } else {
1310
- if (records.length === 0) {
1311
- console.log("No records found.");
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
+ }
1312
1660
  return;
1661
+ } catch (err) {
1662
+ await reportCliUsage("cli.link_direct", false);
1663
+ handleError(err, json);
1313
1664
  }
1314
- const headers = Object.keys(records[0]);
1315
- outputTable(
1316
- headers,
1317
- records.map((r) => headers.map((h) => {
1318
- const val = r[h];
1319
- if (val === null || val === void 0) return "";
1320
- if (typeof val === "object") return JSON.stringify(val);
1321
- return String(val);
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})`
1322
1708
  }))
1323
- );
1324
- console.log(`${records.length} record(s).`);
1325
- }
1326
- } catch (err) {
1327
- handleError(err, json);
1328
- }
1329
- });
1330
- }
1331
-
1332
- // src/commands/records/create.ts
1333
- function registerRecordsCreateCommand(recordsCmd2) {
1334
- recordsCmd2.command("create <table>").description("Create record(s) in a table").option("--data <json>", "JSON data to insert (object or array of objects)").action(async (table, opts, cmd) => {
1335
- const { json } = getRootOpts(cmd);
1336
- try {
1337
- await requireAuth();
1338
- if (!opts.data) {
1339
- throw new CLIError(`--data is required. Example: --data '{"name":"John"}'`);
1709
+ });
1710
+ if (clack8.isCancel(selected)) process.exit(0);
1711
+ projectId = selected;
1340
1712
  }
1341
- let records;
1713
+ let project;
1714
+ let apiKey;
1342
1715
  try {
1343
- const parsed = JSON.parse(opts.data);
1344
- records = Array.isArray(parsed) ? parsed : [parsed];
1345
- } catch {
1346
- throw new CLIError("Invalid JSON in --data. Provide a JSON object or array.");
1347
- }
1348
- const res = await ossFetch(
1349
- `/api/database/records/${encodeURIComponent(table)}?return=representation`,
1350
- {
1351
- method: "POST",
1352
- body: JSON.stringify(records)
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
+ );
1353
1728
  }
1354
- );
1355
- const data = await res.json();
1356
- if (json) {
1357
- outputJson(data);
1358
- } else {
1359
- const created = data.data ?? [];
1360
- outputSuccess(`Created ${created.length || records.length} record(s) in "${table}".`);
1729
+ throw err;
1361
1730
  }
1362
- } catch (err) {
1363
- handleError(err, json);
1364
- }
1365
- });
1366
- }
1367
-
1368
- // src/commands/records/update.ts
1369
- function registerRecordsUpdateCommand(recordsCmd2) {
1370
- recordsCmd2.command("update <table>").description("Update records in a table matching a filter").option("--filter <filter>", 'Filter expression (e.g. "id=eq.123")').option("--data <json>", "JSON data to update").action(async (table, opts, cmd) => {
1371
- const { json } = getRootOpts(cmd);
1372
- try {
1373
- await requireAuth();
1374
- if (!opts.filter) {
1375
- throw new CLIError("--filter is required to prevent accidental updates to all rows.");
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);
1376
1742
  }
1377
- if (!opts.data) {
1378
- throw new CLIError(`--data is required. Example: --data '{"name":"Jane"}'`);
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})`);
1379
1748
  }
1380
- let body;
1749
+ await installSkills(json);
1750
+ await reportCliUsage("cli.link", true, 6);
1381
1751
  try {
1382
- body = JSON.parse(opts.data);
1752
+ await reportAgentConnected({ project_id: project.id }, apiUrl);
1383
1753
  } catch {
1384
- throw new CLIError("Invalid JSON in --data.");
1385
1754
  }
1386
- const params = new URLSearchParams();
1387
- params.set(opts.filter.split("=")[0], opts.filter.split("=").slice(1).join("="));
1388
- params.set("return", "representation");
1389
- const res = await ossFetch(
1390
- `/api/database/records/${encodeURIComponent(table)}?${params}`,
1391
- {
1392
- method: "PATCH",
1393
- body: JSON.stringify(body)
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(", ")}`);
1394
1760
  }
1395
- );
1396
- const data = await res.json();
1397
- if (json) {
1398
- outputJson(data);
1399
- } else {
1400
- const updated = data.data ?? [];
1401
- outputSuccess(`Updated ${updated.length} record(s) in "${table}".`);
1402
- }
1403
- } catch (err) {
1404
- handleError(err, json);
1405
- }
1406
- });
1407
- }
1408
-
1409
- // src/commands/records/delete.ts
1410
- function registerRecordsDeleteCommand(recordsCmd2) {
1411
- recordsCmd2.command("delete <table>").description("Delete records from a table matching a filter").option("--filter <filter>", 'Filter expression (e.g. "id=eq.123")').action(async (table, opts, cmd) => {
1412
- const { json } = getRootOpts(cmd);
1413
- try {
1414
- await requireAuth();
1415
- if (!opts.filter) {
1416
- throw new CLIError("--filter is required to prevent accidental deletion of all rows.");
1417
- }
1418
- const params = new URLSearchParams();
1419
- params.set(opts.filter.split("=")[0], opts.filter.split("=").slice(1).join("="));
1420
- params.set("return", "representation");
1421
- const res = await ossFetch(
1422
- `/api/database/records/${encodeURIComponent(table)}?${params}`,
1423
- { method: "DELETE" }
1424
- );
1425
- const data = await res.json();
1426
- if (json) {
1427
- outputJson(data);
1428
- } else {
1429
- const deleted = data.data ?? [];
1430
- outputSuccess(`Deleted ${deleted.length} record(s) from "${table}".`);
1431
- }
1432
- } catch (err) {
1433
- handleError(err, json);
1434
- }
1435
- });
1436
- }
1437
-
1438
- // src/commands/functions/list.ts
1439
- function registerFunctionsCommands(functionsCmd2) {
1440
- functionsCmd2.command("list").description("List all edge functions").action(async (_opts, cmd) => {
1441
- const { json } = getRootOpts(cmd);
1442
- try {
1443
- await requireAuth();
1444
- const res = await ossFetch("/api/functions");
1445
- const raw = await res.json();
1446
- const functions = Array.isArray(raw) ? raw : raw && typeof raw === "object" && "functions" in raw ? raw.functions ?? [] : [];
1447
- if (json) {
1448
- outputJson(raw);
1449
- } else {
1450
- if (functions.length === 0) {
1451
- console.log("No functions found.");
1452
- return;
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, "-");
1453
1775
  }
1454
- outputTable(
1455
- ["Slug", "Name", "Status", "Created At"],
1456
- functions.map((f) => [
1457
- f.slug,
1458
- f.name ?? "-",
1459
- f.status ?? "-",
1460
- f.createdAt ? new Date(f.createdAt).toLocaleString() : "-"
1461
- ])
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"
1462
1831
  );
1463
1832
  }
1464
- await reportCliUsage("cli.functions.list", true);
1465
1833
  } catch (err) {
1466
- await reportCliUsage("cli.functions.list", false);
1834
+ await reportCliUsage("cli.link", false);
1467
1835
  handleError(err, json);
1836
+ } finally {
1837
+ await shutdownAnalytics();
1468
1838
  }
1469
1839
  });
1470
1840
  }
1471
1841
 
1472
- // src/commands/functions/deploy.ts
1473
- import { readFileSync as readFileSync4, existsSync as existsSync3 } from "fs";
1474
- import { join as join3 } from "path";
1475
- function registerFunctionsDeployCommand(functionsCmd2) {
1476
- 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) => {
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) => {
1477
1845
  const { json } = getRootOpts(cmd);
1478
1846
  try {
1479
1847
  await requireAuth();
1480
- const filePath = opts.file ?? join3(process.cwd(), "insforge", "functions", slug, "index.ts");
1481
- if (!existsSync3(filePath)) {
1482
- throw new CLIError(
1483
- `Source file not found: ${filePath}
1484
- Specify --file <path> or create ${join3("insforge", "functions", slug, "index.ts")}`
1485
- );
1486
- }
1487
- const code = readFileSync4(filePath, "utf-8");
1488
- const name = opts.name ?? slug;
1489
- const description = opts.description ?? "";
1490
- let exists = false;
1491
- try {
1492
- await ossFetch(`/api/functions/${encodeURIComponent(slug)}`);
1493
- exists = true;
1494
- } catch {
1495
- exists = false;
1496
- }
1497
- let res;
1498
- if (exists) {
1499
- res = await ossFetch(`/api/functions/${encodeURIComponent(slug)}`, {
1500
- method: "PUT",
1501
- body: JSON.stringify({ name, description, code })
1502
- });
1503
- } else {
1504
- res = await ossFetch("/api/functions", {
1505
- method: "POST",
1506
- body: JSON.stringify({ slug, name, description, code })
1507
- });
1508
- }
1509
- const result = await res.json();
1510
- const deployFailed = result.deployment?.status === "failed";
1848
+ const { rows, raw } = await runRawSql(sql, !!opts.unrestricted);
1511
1849
  if (json) {
1512
- outputJson(result);
1850
+ outputJson(raw);
1513
1851
  } else {
1514
- const action = exists ? "updation" : "creation";
1515
- const resultStatus = result.success ? "success" : "failed";
1516
- outputSuccess(`Function "${result.function.slug}" ${action} ${resultStatus}.`);
1517
- if (result.deployment) {
1518
- if (result.deployment.status === "success") {
1519
- console.log(` Deployment: ${result.deployment.status}${result.deployment.url ? ` \u2192 ${result.deployment.url}` : ""}`);
1520
- } else {
1521
- console.log(` Deployment: ${result.deployment.status}`);
1522
- if (result.deployment.buildLogs?.length) {
1523
- console.log(" Build logs:");
1524
- for (const line of result.deployment.buildLogs) {
1525
- console.log(` ${line}`);
1526
- }
1527
- }
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.");
1528
1863
  }
1529
1864
  }
1530
- }
1531
- if (deployFailed) throw new CLIError("Function deployment failed");
1532
- await reportCliUsage("cli.functions.deploy", true);
1865
+ }
1866
+ await reportCliUsage("cli.db.query", true);
1533
1867
  } catch (err) {
1534
- await reportCliUsage("cli.functions.deploy", false);
1868
+ await reportCliUsage("cli.db.query", false);
1535
1869
  handleError(err, json);
1536
1870
  }
1537
1871
  });
1538
1872
  }
1539
1873
 
1540
- // src/commands/functions/invoke.ts
1541
- function registerFunctionsInvokeCommand(functionsCmd2) {
1542
- functionsCmd2.command("invoke <slug>").description("Invoke an edge function").option("--data <json>", "JSON body to send to the function").option("--method <method>", "HTTP method (GET, POST, PUT, PATCH, DELETE)", "POST").action(async (slug, opts, cmd) => {
1874
+ // src/commands/db/tables.ts
1875
+ function registerDbTablesCommand(dbCmd2) {
1876
+ dbCmd2.command("tables").description("List all database tables").action(async (_opts, cmd) => {
1543
1877
  const { json } = getRootOpts(cmd);
1544
1878
  try {
1545
1879
  await requireAuth();
1546
- const config = getProjectConfig();
1547
- if (!config) throw new ProjectNotLinkedError();
1548
- const method = opts.method.toUpperCase();
1549
- const headers = {
1550
- "Content-Type": "application/json",
1551
- Authorization: `Bearer ${config.api_key}`
1552
- };
1553
- const fetchOpts = { method, headers };
1554
- if (opts.data && ["POST", "PUT", "PATCH"].includes(method)) {
1555
- fetchOpts.body = opts.data;
1556
- }
1557
- const res = await fetch(
1558
- `${config.oss_host}/functions/${encodeURIComponent(slug)}`,
1559
- fetchOpts
1560
- );
1561
- const contentType = res.headers.get("content-type") ?? "";
1562
- const status = res.status;
1563
- if (contentType.includes("application/json")) {
1564
- const data = await res.json();
1565
- if (json) {
1566
- outputJson({ status, body: data });
1567
- } else {
1568
- console.log(JSON.stringify(data, null, 2));
1569
- }
1880
+ const res = await ossFetch("/api/database/tables");
1881
+ const tables = await res.json();
1882
+ if (json) {
1883
+ outputJson(tables);
1570
1884
  } else {
1571
- const text3 = await res.text();
1572
- console.log(text3);
1573
- }
1574
- if (status >= 400) {
1575
- throw new CLIError(`HTTP ${status}`, 1, "HTTP_ERROR");
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
+ );
1576
1893
  }
1894
+ await reportCliUsage("cli.db.tables", true);
1577
1895
  } catch (err) {
1896
+ await reportCliUsage("cli.db.tables", false);
1578
1897
  handleError(err, json);
1579
1898
  }
1580
1899
  });
1581
1900
  }
1582
1901
 
1583
- // src/commands/functions/code.ts
1584
- function registerFunctionsCodeCommand(functionsCmd2) {
1585
- functionsCmd2.command("code <slug>").description("Fetch and display the source code of an edge function").action(async (slug, _opts, cmd) => {
1902
+ // src/commands/db/functions.ts
1903
+ function registerDbFunctionsCommand(dbCmd2) {
1904
+ dbCmd2.command("functions").description("List all database functions").action(async (_opts, cmd) => {
1586
1905
  const { json } = getRootOpts(cmd);
1587
1906
  try {
1588
1907
  await requireAuth();
1589
- const res = await ossFetch(`/api/functions/${encodeURIComponent(slug)}`);
1590
- const fn = await res.json();
1908
+ const res = await ossFetch("/api/database/functions");
1909
+ const raw = await res.json();
1910
+ const functions = Array.isArray(raw) ? raw : raw.functions ?? [];
1591
1911
  if (json) {
1592
- outputJson(fn);
1912
+ outputJson(raw);
1593
1913
  } else {
1594
- console.log(`Function: ${fn.name} (${fn.slug})`);
1595
- console.log(`Status: ${fn.status}`);
1596
- if (fn.description) console.log(`Desc: ${fn.description}`);
1597
- if (fn.deployed_at) console.log(`Deployed: ${fn.deployed_at}`);
1598
- console.log("---");
1599
- console.log(fn.code);
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
+ );
1600
1922
  }
1923
+ await reportCliUsage("cli.db.functions", true);
1601
1924
  } catch (err) {
1925
+ await reportCliUsage("cli.db.functions", false);
1602
1926
  handleError(err, json);
1603
1927
  }
1604
1928
  });
1605
1929
  }
1606
1930
 
1607
- // src/commands/functions/delete.ts
1608
- import * as clack7 from "@clack/prompts";
1609
- function registerFunctionsDeleteCommand(functionsCmd2) {
1610
- functionsCmd2.command("delete <slug>").description("Delete an edge function").action(async (slug, _opts, cmd) => {
1611
- const { json, yes } = getRootOpts(cmd);
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);
1612
1935
  try {
1613
1936
  await requireAuth();
1614
- if (!yes && !json) {
1615
- const confirmed = await clack7.confirm({
1616
- message: `Delete function "${slug}"? This cannot be undone.`
1617
- });
1618
- if (clack7.isCancel(confirmed) || !confirmed) {
1619
- clack7.log.info("Cancelled.");
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.");
1620
1945
  return;
1621
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
+ );
1622
1957
  }
1623
- const res = await ossFetch(`/api/functions/${encodeURIComponent(slug)}`, {
1624
- method: "DELETE"
1625
- });
1626
- const result = await res.json();
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 ?? [];
1627
1975
  if (json) {
1628
- outputJson(result);
1976
+ outputJson(raw);
1629
1977
  } else {
1630
- if (result.success) {
1631
- outputSuccess(`Function "${slug}" deleted successfully.`);
1632
- } else {
1633
- outputSuccess(`Failed to delete function "${slug}".`);
1978
+ if (policies.length === 0) {
1979
+ console.log("No RLS policies found.");
1980
+ return;
1634
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
+ );
1635
1993
  }
1636
- await reportCliUsage("cli.functions.delete", true);
1994
+ await reportCliUsage("cli.db.policies", true);
1637
1995
  } catch (err) {
1638
- await reportCliUsage("cli.functions.delete", false);
1996
+ await reportCliUsage("cli.db.policies", false);
1639
1997
  handleError(err, json);
1640
1998
  }
1641
1999
  });
1642
2000
  }
1643
2001
 
1644
- // src/commands/storage/buckets.ts
1645
- function registerStorageBucketsCommand(storageCmd2) {
1646
- storageCmd2.command("buckets").description("List all storage buckets").action(async (_opts, cmd) => {
2002
+ // src/commands/db/triggers.ts
2003
+ function registerDbTriggersCommand(dbCmd2) {
2004
+ dbCmd2.command("triggers").description("List all database triggers").action(async (_opts, cmd) => {
1647
2005
  const { json } = getRootOpts(cmd);
1648
2006
  try {
1649
2007
  await requireAuth();
1650
- const res = await ossFetch("/api/storage/buckets");
2008
+ const res = await ossFetch("/api/database/triggers");
1651
2009
  const raw = await res.json();
1652
- const buckets = Array.isArray(raw) ? raw : raw && typeof raw === "object" && "buckets" in raw ? raw.buckets ?? [] : [];
2010
+ const triggers = Array.isArray(raw) ? raw : raw.triggers ?? [];
1653
2011
  if (json) {
1654
2012
  outputJson(raw);
1655
2013
  } else {
1656
- if (buckets.length === 0) {
1657
- console.log("No buckets found.");
2014
+ if (triggers.length === 0) {
2015
+ console.log("No database triggers found.");
1658
2016
  return;
1659
2017
  }
1660
2018
  outputTable(
1661
- ["Bucket Name", "Public"],
1662
- buckets.map((b) => [b.name, b.public ? "Yes" : "No"])
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
+ ])
1663
2029
  );
1664
2030
  }
1665
- await reportCliUsage("cli.storage.buckets", true);
2031
+ await reportCliUsage("cli.db.triggers", true);
1666
2032
  } catch (err) {
1667
- await reportCliUsage("cli.storage.buckets", false);
2033
+ await reportCliUsage("cli.db.triggers", false);
1668
2034
  handleError(err, json);
1669
2035
  }
1670
2036
  });
1671
2037
  }
1672
2038
 
1673
- // src/commands/storage/upload.ts
1674
- import { readFileSync as readFileSync5, existsSync as existsSync4 } from "fs";
1675
- import { basename as basename2 } from "path";
1676
- function registerStorageUploadCommand(storageCmd2) {
1677
- 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) => {
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) => {
1678
2042
  const { json } = getRootOpts(cmd);
1679
2043
  try {
1680
2044
  await requireAuth();
1681
- const config = getProjectConfig();
1682
- if (!config) throw new ProjectNotLinkedError();
1683
- if (!existsSync4(file)) {
1684
- throw new CLIError(`File not found: ${file}`);
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));
1685
2055
  }
1686
- const fileContent = readFileSync5(file);
1687
- const objectKey = opts.key ?? basename2(file);
1688
- const bucketName = opts.bucket;
1689
- const formData = new FormData();
1690
- const blob = new Blob([fileContent]);
1691
- formData.append("file", blob, objectKey);
1692
- const url = `${config.oss_host}/api/storage/buckets/${encodeURIComponent(bucketName)}/objects/${encodeURIComponent(objectKey)}`;
1693
- const res = await fetch(url, {
1694
- method: "PUT",
1695
- headers: {
1696
- Authorization: `Bearer ${config.api_key}`
1697
- },
1698
- body: formData
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());
2076
+ }
2077
+ if (opts.includeFunctions) body.includeFunctions = true;
2078
+ if (opts.includeSequences) body.includeSequences = true;
2079
+ if (opts.includeViews) body.includeViews = true;
2080
+ if (opts.rowLimit) body.rowLimit = parseInt(opts.rowLimit, 10);
2081
+ const res = await ossFetch("/api/database/advance/export", {
2082
+ method: "POST",
2083
+ body: JSON.stringify(body)
1699
2084
  });
1700
- if (!res.ok) {
1701
- const err = await res.json().catch(() => ({}));
1702
- throw new CLIError(err.error ?? `Upload failed: ${res.status}`);
2085
+ const raw = await res.text();
2086
+ let content;
2087
+ let meta = null;
2088
+ try {
2089
+ const parsed = JSON.parse(raw);
2090
+ if (typeof parsed.content === "string") {
2091
+ content = parsed.content;
2092
+ meta = { format: parsed.format, tables: parsed.tables };
2093
+ } else {
2094
+ content = raw;
2095
+ }
2096
+ } catch {
2097
+ content = raw;
1703
2098
  }
1704
- const data = await res.json();
1705
2099
  if (json) {
1706
- outputJson(data);
2100
+ outputJson(meta ?? { content });
2101
+ return;
2102
+ }
2103
+ if (opts.output) {
2104
+ writeFileSync2(opts.output, content);
2105
+ const tableCount = meta?.tables?.length;
2106
+ const suffix = tableCount ? ` (${tableCount} tables, format: ${meta?.format ?? opts.format})` : "";
2107
+ outputSuccess(`Exported to ${opts.output}${suffix}`);
1707
2108
  } else {
1708
- outputSuccess(`Uploaded "${basename2(file)}" to bucket "${bucketName}".`);
2109
+ console.log(content);
1709
2110
  }
1710
2111
  } catch (err) {
1711
2112
  handleError(err, json);
@@ -1713,34 +2114,39 @@ function registerStorageUploadCommand(storageCmd2) {
1713
2114
  });
1714
2115
  }
1715
2116
 
1716
- // src/commands/storage/download.ts
1717
- import { writeFileSync as writeFileSync3 } from "fs";
1718
- import { join as join4, basename as basename3 } from "path";
1719
- function registerStorageDownloadCommand(storageCmd2) {
1720
- 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) => {
2117
+ // src/commands/db/import.ts
2118
+ import { readFileSync as readFileSync3 } from "fs";
2119
+ import { basename as basename4 } from "path";
2120
+ function registerDbImportCommand(dbCmd2) {
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) => {
1721
2122
  const { json } = getRootOpts(cmd);
1722
2123
  try {
1723
2124
  await requireAuth();
1724
2125
  const config = getProjectConfig();
1725
2126
  if (!config) throw new ProjectNotLinkedError();
1726
- const bucketName = opts.bucket;
1727
- const url = `${config.oss_host}/api/storage/buckets/${encodeURIComponent(bucketName)}/objects/${encodeURIComponent(objectKey)}`;
1728
- const res = await fetch(url, {
2127
+ const fileContent = readFileSync3(file);
2128
+ const fileName = basename4(file);
2129
+ const formData = new FormData();
2130
+ formData.append("file", new Blob([fileContent]), fileName);
2131
+ if (opts.truncate) {
2132
+ formData.append("truncate", "true");
2133
+ }
2134
+ const res = await fetch(`${config.oss_host}/api/database/advance/import`, {
2135
+ method: "POST",
1729
2136
  headers: {
1730
2137
  Authorization: `Bearer ${config.api_key}`
1731
- }
2138
+ },
2139
+ body: formData
1732
2140
  });
1733
2141
  if (!res.ok) {
1734
2142
  const err = await res.json().catch(() => ({}));
1735
- throw new CLIError(err.error ?? `Download failed: ${res.status}`);
2143
+ throw new CLIError(err.error ?? `Import failed: ${res.status}`);
1736
2144
  }
1737
- const buffer = Buffer.from(await res.arrayBuffer());
1738
- const outputPath = opts.output ?? join4(process.cwd(), basename3(objectKey));
1739
- writeFileSync3(outputPath, buffer);
2145
+ const data = await res.json();
1740
2146
  if (json) {
1741
- outputJson({ success: true, path: outputPath, size: buffer.length });
2147
+ outputJson(data);
1742
2148
  } else {
1743
- outputSuccess(`Downloaded "${objectKey}" to ${outputPath} (${buffer.length} bytes).`);
2149
+ outputSuccess(`Imported ${data.filename} (${data.tables.length} tables, ${data.rowsImported} rows)`);
1744
2150
  }
1745
2151
  } catch (err) {
1746
2152
  handleError(err, json);
@@ -1748,54 +2154,77 @@ function registerStorageDownloadCommand(storageCmd2) {
1748
2154
  });
1749
2155
  }
1750
2156
 
1751
- // src/commands/storage/create-bucket.ts
1752
- function registerStorageCreateBucketCommand(storageCmd2) {
1753
- 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) => {
2157
+ // src/commands/records/list.ts
2158
+ function registerRecordsCommands(recordsCmd2) {
2159
+ recordsCmd2.command("list <table>").description("List records from a table").option("--select <columns>", "Columns to select (comma-separated)").option("--filter <filter>", 'Filter expression (e.g. "name=eq.John")').option("--order <order>", 'Order by (e.g. "created_at.desc")').option("--limit <n>", "Limit number of records", parseInt).option("--offset <n>", "Offset for pagination", parseInt).action(async (table, opts, cmd) => {
1754
2160
  const { json } = getRootOpts(cmd);
1755
2161
  try {
1756
2162
  await requireAuth();
1757
- const isPublic = !opts.private;
1758
- const res = await ossFetch("/api/storage/buckets", {
1759
- method: "POST",
1760
- body: JSON.stringify({ bucketName: name, isPublic })
1761
- });
2163
+ const params = new URLSearchParams();
2164
+ if (opts.select) params.set("select", opts.select);
2165
+ if (opts.filter) params.set(opts.filter.split("=")[0], opts.filter.split("=").slice(1).join("="));
2166
+ if (opts.order) params.set("order", opts.order);
2167
+ if (opts.limit) params.set("limit", String(opts.limit));
2168
+ if (opts.offset) params.set("offset", String(opts.offset));
2169
+ const query = params.toString();
2170
+ const path5 = `/api/database/records/${encodeURIComponent(table)}${query ? `?${query}` : ""}`;
2171
+ const res = await ossFetch(path5);
1762
2172
  const data = await res.json();
2173
+ const records = data.data ?? [];
1763
2174
  if (json) {
1764
2175
  outputJson(data);
1765
2176
  } else {
1766
- outputSuccess(`Bucket "${name}" created (${isPublic ? "public" : "private"}).`);
2177
+ if (records.length === 0) {
2178
+ console.log("No records found.");
2179
+ return;
2180
+ }
2181
+ const headers = Object.keys(records[0]);
2182
+ outputTable(
2183
+ headers,
2184
+ records.map((r) => headers.map((h) => {
2185
+ const val = r[h];
2186
+ if (val === null || val === void 0) return "";
2187
+ if (typeof val === "object") return JSON.stringify(val);
2188
+ return String(val);
2189
+ }))
2190
+ );
2191
+ console.log(`${records.length} record(s).`);
1767
2192
  }
1768
- await reportCliUsage("cli.storage.create-bucket", true);
1769
2193
  } catch (err) {
1770
- await reportCliUsage("cli.storage.create-bucket", false);
1771
2194
  handleError(err, json);
1772
2195
  }
1773
2196
  });
1774
2197
  }
1775
2198
 
1776
- // src/commands/storage/delete-bucket.ts
1777
- import * as clack8 from "@clack/prompts";
1778
- function registerStorageDeleteBucketCommand(storageCmd2) {
1779
- storageCmd2.command("delete-bucket <name>").description("Delete a storage bucket and all its objects").action(async (name, _opts, cmd) => {
1780
- const { json, yes } = getRootOpts(cmd);
2199
+ // src/commands/records/create.ts
2200
+ function registerRecordsCreateCommand(recordsCmd2) {
2201
+ recordsCmd2.command("create <table>").description("Create record(s) in a table").option("--data <json>", "JSON data to insert (object or array of objects)").action(async (table, opts, cmd) => {
2202
+ const { json } = getRootOpts(cmd);
1781
2203
  try {
1782
2204
  await requireAuth();
1783
- if (!yes && !json) {
1784
- const confirm8 = await clack8.confirm({
1785
- message: `Delete bucket "${name}" and all its objects? This cannot be undone.`
1786
- });
1787
- if (!confirm8 || clack8.isCancel(confirm8)) {
1788
- process.exit(0);
1789
- }
2205
+ if (!opts.data) {
2206
+ throw new CLIError(`--data is required. Example: --data '{"name":"John"}'`);
1790
2207
  }
1791
- const res = await ossFetch(`/api/storage/buckets/${encodeURIComponent(name)}`, {
1792
- method: "DELETE"
1793
- });
2208
+ let records;
2209
+ try {
2210
+ const parsed = JSON.parse(opts.data);
2211
+ records = Array.isArray(parsed) ? parsed : [parsed];
2212
+ } catch {
2213
+ throw new CLIError("Invalid JSON in --data. Provide a JSON object or array.");
2214
+ }
2215
+ const res = await ossFetch(
2216
+ `/api/database/records/${encodeURIComponent(table)}?return=representation`,
2217
+ {
2218
+ method: "POST",
2219
+ body: JSON.stringify(records)
2220
+ }
2221
+ );
1794
2222
  const data = await res.json();
1795
2223
  if (json) {
1796
2224
  outputJson(data);
1797
2225
  } else {
1798
- outputSuccess(`Bucket "${name}" deleted.`);
2226
+ const created = data.data ?? [];
2227
+ outputSuccess(`Created ${created.length || records.length} record(s) in "${table}".`);
1799
2228
  }
1800
2229
  } catch (err) {
1801
2230
  handleError(err, json);
@@ -1803,749 +2232,499 @@ function registerStorageDeleteBucketCommand(storageCmd2) {
1803
2232
  });
1804
2233
  }
1805
2234
 
1806
- // src/commands/storage/list-objects.ts
1807
- function formatSize(bytes) {
1808
- if (bytes < 1024) return `${bytes} B`;
1809
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
1810
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1811
- }
1812
- function registerStorageListObjectsCommand(storageCmd2) {
1813
- 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) => {
2235
+ // src/commands/records/update.ts
2236
+ function registerRecordsUpdateCommand(recordsCmd2) {
2237
+ recordsCmd2.command("update <table>").description("Update records in a table matching a filter").option("--filter <filter>", 'Filter expression (e.g. "id=eq.123")').option("--data <json>", "JSON data to update").action(async (table, opts, cmd) => {
1814
2238
  const { json } = getRootOpts(cmd);
1815
2239
  try {
1816
2240
  await requireAuth();
2241
+ if (!opts.filter) {
2242
+ throw new CLIError("--filter is required to prevent accidental updates to all rows.");
2243
+ }
2244
+ if (!opts.data) {
2245
+ throw new CLIError(`--data is required. Example: --data '{"name":"Jane"}'`);
2246
+ }
2247
+ let body;
2248
+ try {
2249
+ body = JSON.parse(opts.data);
2250
+ } catch {
2251
+ throw new CLIError("Invalid JSON in --data.");
2252
+ }
1817
2253
  const params = new URLSearchParams();
1818
- params.set("limit", opts.limit);
1819
- params.set("offset", opts.offset);
1820
- if (opts.prefix) params.set("prefix", opts.prefix);
1821
- if (opts.search) params.set("search", opts.search);
2254
+ params.set(opts.filter.split("=")[0], opts.filter.split("=").slice(1).join("="));
2255
+ params.set("return", "representation");
1822
2256
  const res = await ossFetch(
1823
- `/api/storage/buckets/${encodeURIComponent(bucket)}/objects?${params.toString()}`
2257
+ `/api/database/records/${encodeURIComponent(table)}?${params}`,
2258
+ {
2259
+ method: "PATCH",
2260
+ body: JSON.stringify(body)
2261
+ }
1824
2262
  );
1825
- const raw = await res.json();
1826
- const objects = Array.isArray(raw) ? raw : raw.data ?? [];
1827
- const sortField = opts.sort ?? "key";
1828
- objects.sort((a, b) => {
1829
- if (sortField === "size") return a.size - b.size;
1830
- if (sortField === "uploadedAt") return a.uploadedAt.localeCompare(b.uploadedAt);
1831
- return a.key.localeCompare(b.key);
1832
- });
2263
+ const data = await res.json();
1833
2264
  if (json) {
1834
- outputJson(raw);
1835
- } else {
1836
- if (objects.length === 0) {
1837
- console.log(`No objects found in bucket "${bucket}".`);
1838
- return;
1839
- }
1840
- const total = raw.pagination?.total;
1841
- if (total !== void 0) {
1842
- console.log(`Showing ${objects.length} of ${total} objects:
1843
- `);
1844
- }
1845
- outputTable(
1846
- ["Key", "Size", "Type", "Uploaded At"],
1847
- objects.map((o) => [
1848
- o.key,
1849
- formatSize(o.size),
1850
- o.mimeType ?? "-",
1851
- o.uploadedAt
1852
- ])
1853
- );
1854
- }
1855
- await reportCliUsage("cli.storage.list-objects", true);
1856
- } catch (err) {
1857
- await reportCliUsage("cli.storage.list-objects", false);
1858
- handleError(err, json);
1859
- }
1860
- });
1861
- }
1862
-
1863
- // src/commands/create.ts
1864
- import { exec as exec2 } from "child_process";
1865
- import { tmpdir } from "os";
1866
- import { promisify as promisify2 } from "util";
1867
- import * as fs3 from "fs/promises";
1868
- import * as path3 from "path";
1869
- import * as clack10 from "@clack/prompts";
1870
-
1871
- // src/lib/env.ts
1872
- import * as fs from "fs/promises";
1873
- import * as path from "path";
1874
- async function readEnvFile(cwd) {
1875
- const candidates = [".env.local", ".env.production", ".env"];
1876
- for (const name of candidates) {
1877
- const filePath = path.join(cwd, name);
1878
- const exists = await fs.stat(filePath).catch(() => null);
1879
- if (!exists) continue;
1880
- const content = await fs.readFile(filePath, "utf-8");
1881
- const vars = [];
1882
- for (const line of content.split("\n")) {
1883
- const trimmed = line.trim();
1884
- if (!trimmed || trimmed.startsWith("#")) continue;
1885
- const eqIndex = trimmed.indexOf("=");
1886
- if (eqIndex === -1) continue;
1887
- const key = trimmed.slice(0, eqIndex).trim();
1888
- let value = trimmed.slice(eqIndex + 1).trim();
1889
- if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
1890
- value = value.slice(1, -1);
1891
- }
1892
- if (key) vars.push({ key, value });
1893
- }
1894
- return vars;
1895
- }
1896
- return [];
1897
- }
1898
-
1899
- // src/lib/analytics.ts
1900
- import { PostHog } from "posthog-node";
1901
- var POSTHOG_API_KEY = "phc_ueV1ii62wdBTkH7E70ugyeqHIHu8dFDdjs0qq3TZhJz";
1902
- var POSTHOG_HOST = process.env.POSTHOG_HOST || "https://us.i.posthog.com";
1903
- var client = null;
1904
- function getClient() {
1905
- if (!POSTHOG_API_KEY) return null;
1906
- if (!client) {
1907
- client = new PostHog(POSTHOG_API_KEY, { host: POSTHOG_HOST });
1908
- }
1909
- return client;
1910
- }
1911
- function captureEvent(distinctId, event, properties) {
1912
- try {
1913
- getClient()?.capture({ distinctId, event, properties });
1914
- } catch {
1915
- }
1916
- }
1917
- function trackDiagnose(subcommand, config) {
1918
- captureEvent(config.project_id, "cli_diagnose_invoked", {
1919
- subcommand,
1920
- project_id: config.project_id,
1921
- project_name: config.project_name,
1922
- org_id: config.org_id,
1923
- region: config.region,
1924
- oss_mode: config.project_id === "oss-project"
2265
+ outputJson(data);
2266
+ } else {
2267
+ const updated = data.data ?? [];
2268
+ outputSuccess(`Updated ${updated.length} record(s) in "${table}".`);
2269
+ }
2270
+ } catch (err) {
2271
+ handleError(err, json);
2272
+ }
1925
2273
  });
1926
2274
  }
1927
- async function shutdownAnalytics() {
1928
- try {
1929
- if (client) await client.shutdown();
1930
- } catch {
1931
- }
1932
- }
1933
2275
 
1934
- // src/commands/deployments/deploy.ts
1935
- import * as path2 from "path";
1936
- import * as fs2 from "fs/promises";
1937
- import * as clack9 from "@clack/prompts";
1938
- import archiver from "archiver";
1939
- var POLL_INTERVAL_MS = 5e3;
1940
- var POLL_TIMEOUT_MS = 3e5;
1941
- var EXCLUDE_PATTERNS = [
1942
- "node_modules",
1943
- ".git",
1944
- ".next",
1945
- ".env",
1946
- ".env.local",
1947
- "dist",
1948
- "build",
1949
- ".DS_Store",
1950
- ".insforge",
1951
- // IDE and AI agent configs
1952
- ".claude",
1953
- ".agents",
1954
- ".augment",
1955
- ".kilocode",
1956
- ".kiro",
1957
- ".qoder",
1958
- ".qwen",
1959
- ".roo",
1960
- ".trae",
1961
- ".windsurf",
1962
- ".vercel",
1963
- ".turbo",
1964
- ".cache",
1965
- "skills",
1966
- "coverage"
1967
- ];
1968
- function shouldExclude(name) {
1969
- const normalized = name.replace(/\\/g, "/");
1970
- for (const pattern of EXCLUDE_PATTERNS) {
1971
- if (normalized === pattern || normalized.startsWith(pattern + "/") || normalized.endsWith("/" + pattern) || normalized.includes("/" + pattern + "/")) {
1972
- return true;
2276
+ // src/commands/records/delete.ts
2277
+ function registerRecordsDeleteCommand(recordsCmd2) {
2278
+ recordsCmd2.command("delete <table>").description("Delete records from a table matching a filter").option("--filter <filter>", 'Filter expression (e.g. "id=eq.123")').action(async (table, opts, cmd) => {
2279
+ const { json } = getRootOpts(cmd);
2280
+ try {
2281
+ await requireAuth();
2282
+ if (!opts.filter) {
2283
+ throw new CLIError("--filter is required to prevent accidental deletion of all rows.");
2284
+ }
2285
+ const params = new URLSearchParams();
2286
+ params.set(opts.filter.split("=")[0], opts.filter.split("=").slice(1).join("="));
2287
+ params.set("return", "representation");
2288
+ const res = await ossFetch(
2289
+ `/api/database/records/${encodeURIComponent(table)}?${params}`,
2290
+ { method: "DELETE" }
2291
+ );
2292
+ const data = await res.json();
2293
+ if (json) {
2294
+ outputJson(data);
2295
+ } else {
2296
+ const deleted = data.data ?? [];
2297
+ outputSuccess(`Deleted ${deleted.length} record(s) from "${table}".`);
2298
+ }
2299
+ } catch (err) {
2300
+ handleError(err, json);
1973
2301
  }
1974
- }
1975
- if (normalized.endsWith(".log")) return true;
1976
- return false;
1977
- }
1978
- async function createZipBuffer(sourceDir) {
1979
- return new Promise((resolve2, reject) => {
1980
- const archive = archiver("zip", { zlib: { level: 9 } });
1981
- const chunks = [];
1982
- archive.on("data", (chunk) => chunks.push(chunk));
1983
- archive.on("end", () => resolve2(Buffer.concat(chunks)));
1984
- archive.on("error", (err) => reject(err));
1985
- archive.directory(sourceDir, false, (entry) => {
1986
- if (shouldExclude(entry.name)) return false;
1987
- return entry;
1988
- });
1989
- void archive.finalize();
1990
2302
  });
1991
2303
  }
1992
- async function deployProject(opts) {
1993
- const { sourceDir, startBody = {}, spinner: s } = opts;
1994
- s?.start("Creating deployment...");
1995
- const createRes = await ossFetch("/api/deployments", { method: "POST" });
1996
- const { id: deploymentId, uploadUrl, uploadFields } = await createRes.json();
1997
- s?.message("Compressing source files...");
1998
- const zipBuffer = await createZipBuffer(sourceDir);
1999
- s?.message("Uploading...");
2000
- const formData = new FormData();
2001
- for (const [key, value] of Object.entries(uploadFields)) {
2002
- formData.append(key, value);
2003
- }
2004
- formData.append(
2005
- "file",
2006
- new Blob([zipBuffer], { type: "application/zip" }),
2007
- "deployment.zip"
2008
- );
2009
- const uploadRes = await fetch(uploadUrl, { method: "POST", body: formData });
2010
- if (!uploadRes.ok) {
2011
- const uploadErr = await uploadRes.text();
2012
- throw new CLIError(`Failed to upload: ${uploadErr}`);
2013
- }
2014
- s?.message("Starting deployment...");
2015
- const startRes = await ossFetch(`/api/deployments/${deploymentId}/start`, {
2016
- method: "POST",
2017
- body: JSON.stringify(startBody)
2018
- });
2019
- await startRes.json();
2020
- s?.message("Building and deploying...");
2021
- const startTime = Date.now();
2022
- let deployment = null;
2023
- while (Date.now() - startTime < POLL_TIMEOUT_MS) {
2024
- await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
2304
+
2305
+ // src/commands/functions/list.ts
2306
+ function registerFunctionsCommands(functionsCmd2) {
2307
+ functionsCmd2.command("list").description("List all edge functions").action(async (_opts, cmd) => {
2308
+ const { json } = getRootOpts(cmd);
2025
2309
  try {
2026
- const statusRes = await ossFetch(`/api/deployments/${deploymentId}`);
2027
- deployment = await statusRes.json();
2028
- const status = deployment.status.toUpperCase();
2029
- if (status === "READY") {
2030
- break;
2031
- }
2032
- if (status === "ERROR" || status === "CANCELED") {
2033
- s?.stop("Deployment failed");
2034
- throw new CLIError(getDeploymentError(deployment.metadata) ?? `Deployment failed with status: ${deployment.status}`);
2310
+ await requireAuth();
2311
+ const res = await ossFetch("/api/functions");
2312
+ const raw = await res.json();
2313
+ const functions = Array.isArray(raw) ? raw : raw && typeof raw === "object" && "functions" in raw ? raw.functions ?? [] : [];
2314
+ if (json) {
2315
+ outputJson(raw);
2316
+ } else {
2317
+ if (functions.length === 0) {
2318
+ console.log("No functions found.");
2319
+ return;
2320
+ }
2321
+ outputTable(
2322
+ ["Slug", "Name", "Status", "Created At"],
2323
+ functions.map((f) => [
2324
+ f.slug,
2325
+ f.name ?? "-",
2326
+ f.status ?? "-",
2327
+ f.createdAt ? new Date(f.createdAt).toLocaleString() : "-"
2328
+ ])
2329
+ );
2035
2330
  }
2036
- const elapsed = Math.round((Date.now() - startTime) / 1e3);
2037
- s?.message(`Building and deploying... (${elapsed}s, status: ${deployment.status})`);
2331
+ await reportCliUsage("cli.functions.list", true);
2038
2332
  } catch (err) {
2039
- if (err instanceof CLIError) throw err;
2333
+ await reportCliUsage("cli.functions.list", false);
2334
+ handleError(err, json);
2040
2335
  }
2041
- }
2042
- const isReady = deployment?.status.toUpperCase() === "READY";
2043
- const liveUrl = isReady ? deployment?.url ?? null : null;
2044
- return { deploymentId, deployment, isReady, liveUrl };
2336
+ });
2045
2337
  }
2046
- function registerDeploymentsDeployCommand(deploymentsCmd2) {
2047
- 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) => {
2338
+
2339
+ // src/commands/functions/deploy.ts
2340
+ import { readFileSync as readFileSync4, existsSync as existsSync3 } from "fs";
2341
+ import { join as join6 } from "path";
2342
+ function registerFunctionsDeployCommand(functionsCmd2) {
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) => {
2048
2344
  const { json } = getRootOpts(cmd);
2049
2345
  try {
2050
2346
  await requireAuth();
2051
- const config = getProjectConfig();
2052
- if (!config) throw new ProjectNotLinkedError();
2053
- const sourceDir = path2.resolve(directory ?? ".");
2054
- const stats = await fs2.stat(sourceDir).catch(() => null);
2055
- if (!stats?.isDirectory()) {
2056
- throw new CLIError(`"${sourceDir}" is not a valid directory.`);
2347
+ const filePath = opts.file ?? join6(process.cwd(), "insforge", "functions", slug, "index.ts");
2348
+ if (!existsSync3(filePath)) {
2349
+ throw new CLIError(
2350
+ `Source file not found: ${filePath}
2351
+ Specify --file <path> or create ${join6("insforge", "functions", slug, "index.ts")}`
2352
+ );
2057
2353
  }
2058
- const dirName = path2.basename(sourceDir);
2059
- if (EXCLUDE_PATTERNS.includes(dirName)) {
2060
- 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.`);
2354
+ const code = readFileSync4(filePath, "utf-8");
2355
+ const name = opts.name ?? slug;
2356
+ const description = opts.description ?? "";
2357
+ let exists = false;
2358
+ try {
2359
+ await ossFetch(`/api/functions/${encodeURIComponent(slug)}`);
2360
+ exists = true;
2361
+ } catch {
2362
+ exists = false;
2061
2363
  }
2062
- const s = !json ? clack9.spinner() : null;
2063
- const startBody = {};
2064
- if (opts.env) {
2065
- try {
2066
- const parsed = JSON.parse(opts.env);
2067
- if (Array.isArray(parsed)) {
2068
- startBody.envVars = parsed;
2364
+ let res;
2365
+ if (exists) {
2366
+ res = await ossFetch(`/api/functions/${encodeURIComponent(slug)}`, {
2367
+ method: "PUT",
2368
+ body: JSON.stringify({ name, description, code })
2369
+ });
2370
+ } else {
2371
+ res = await ossFetch("/api/functions", {
2372
+ method: "POST",
2373
+ body: JSON.stringify({ slug, name, description, code })
2374
+ });
2375
+ }
2376
+ const result = await res.json();
2377
+ const deployFailed = result.deployment?.status === "failed";
2378
+ if (json) {
2379
+ outputJson(result);
2380
+ } else {
2381
+ const action = exists ? "updation" : "creation";
2382
+ const resultStatus = result.success ? "success" : "failed";
2383
+ outputSuccess(`Function "${result.function.slug}" ${action} ${resultStatus}.`);
2384
+ if (result.deployment) {
2385
+ if (result.deployment.status === "success") {
2386
+ console.log(` Deployment: ${result.deployment.status}${result.deployment.url ? ` \u2192 ${result.deployment.url}` : ""}`);
2069
2387
  } else {
2070
- startBody.envVars = Object.entries(parsed).map(([key, value]) => ({ key, value }));
2388
+ console.log(` Deployment: ${result.deployment.status}`);
2389
+ if (result.deployment.buildLogs?.length) {
2390
+ console.log(" Build logs:");
2391
+ for (const line of result.deployment.buildLogs) {
2392
+ console.log(` ${line}`);
2393
+ }
2394
+ }
2071
2395
  }
2072
- } catch {
2073
- throw new CLIError("Invalid --env JSON.");
2074
2396
  }
2075
2397
  }
2076
- if (opts.meta) {
2077
- try {
2078
- startBody.meta = JSON.parse(opts.meta);
2079
- } catch {
2080
- throw new CLIError("Invalid --meta JSON.");
2081
- }
2398
+ if (deployFailed) throw new CLIError("Function deployment failed");
2399
+ await reportCliUsage("cli.functions.deploy", true);
2400
+ } catch (err) {
2401
+ await reportCliUsage("cli.functions.deploy", false);
2402
+ handleError(err, json);
2403
+ }
2404
+ });
2405
+ }
2406
+
2407
+ // src/commands/functions/invoke.ts
2408
+ function registerFunctionsInvokeCommand(functionsCmd2) {
2409
+ functionsCmd2.command("invoke <slug>").description("Invoke an edge function").option("--data <json>", "JSON body to send to the function").option("--method <method>", "HTTP method (GET, POST, PUT, PATCH, DELETE)", "POST").action(async (slug, opts, cmd) => {
2410
+ const { json } = getRootOpts(cmd);
2411
+ try {
2412
+ await requireAuth();
2413
+ const config = getProjectConfig();
2414
+ if (!config) throw new ProjectNotLinkedError();
2415
+ const method = opts.method.toUpperCase();
2416
+ const headers = {
2417
+ "Content-Type": "application/json",
2418
+ Authorization: `Bearer ${config.api_key}`
2419
+ };
2420
+ const fetchOpts = { method, headers };
2421
+ if (opts.data && ["POST", "PUT", "PATCH"].includes(method)) {
2422
+ fetchOpts.body = opts.data;
2082
2423
  }
2083
- const result = await deployProject({ sourceDir, startBody, spinner: s });
2084
- if (result.isReady) {
2085
- s?.stop("Deployment complete");
2424
+ const res = await fetch(
2425
+ `${config.oss_host}/functions/${encodeURIComponent(slug)}`,
2426
+ fetchOpts
2427
+ );
2428
+ const contentType = res.headers.get("content-type") ?? "";
2429
+ const status = res.status;
2430
+ if (contentType.includes("application/json")) {
2431
+ const data = await res.json();
2086
2432
  if (json) {
2087
- outputJson(result.deployment);
2433
+ outputJson({ status, body: data });
2088
2434
  } else {
2089
- if (result.liveUrl) {
2090
- clack9.log.success(`Live at: ${result.liveUrl}`);
2091
- }
2092
- clack9.log.info(`Deployment ID: ${result.deploymentId}`);
2435
+ console.log(JSON.stringify(data, null, 2));
2093
2436
  }
2094
2437
  } else {
2095
- s?.stop("Deployment is still building");
2096
- if (json) {
2097
- outputJson({ id: result.deploymentId, status: result.deployment?.status ?? "building", timedOut: true });
2098
- } else {
2099
- clack9.log.info(`Deployment ID: ${result.deploymentId}`);
2100
- clack9.log.warn("Deployment did not finish within 5 minutes.");
2101
- clack9.log.info(`Check status with: npx @insforge/cli deployments status ${result.deploymentId}`);
2102
- }
2438
+ const text4 = await res.text();
2439
+ console.log(text4);
2440
+ }
2441
+ if (status >= 400) {
2442
+ throw new CLIError(`HTTP ${status}`, 1, "HTTP_ERROR");
2103
2443
  }
2104
- await reportCliUsage("cli.deployments.deploy", true);
2105
2444
  } catch (err) {
2106
- await reportCliUsage("cli.deployments.deploy", false);
2107
2445
  handleError(err, json);
2108
2446
  }
2109
2447
  });
2110
2448
  }
2111
2449
 
2112
- // src/commands/create.ts
2113
- var execAsync2 = promisify2(exec2);
2114
- function buildOssHost2(appkey, region) {
2115
- return `https://${appkey}.${region}.insforge.app`;
2116
- }
2117
- async function waitForProjectActive(projectId, apiUrl, timeoutMs = 12e4) {
2118
- const start = Date.now();
2119
- while (Date.now() - start < timeoutMs) {
2120
- const project = await getProject(projectId, apiUrl);
2121
- if (project.status === "active") return;
2122
- await new Promise((r) => setTimeout(r, 3e3));
2123
- }
2124
- throw new CLIError("Project creation timed out. Check the dashboard for status.");
2125
- }
2126
- var INSFORGE_BANNER = [
2127
- "\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",
2128
- "\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",
2129
- "\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 ",
2130
- "\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 ",
2131
- "\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",
2132
- "\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"
2133
- ];
2134
- async function animateBanner() {
2135
- const isTTY = process.stderr.isTTY;
2136
- if (!isTTY || process.env.CI) {
2137
- for (const line of INSFORGE_BANNER) {
2138
- process.stderr.write(`${line}
2139
- `);
2140
- }
2141
- process.stderr.write("\n");
2142
- return;
2143
- }
2144
- const totalLines = INSFORGE_BANNER.length;
2145
- const maxLen = Math.max(...INSFORGE_BANNER.map((l) => l.length));
2146
- const cols = process.stderr.columns ?? 0;
2147
- if (cols > 0 && cols < maxLen) {
2148
- for (const line of INSFORGE_BANNER) {
2149
- process.stderr.write(`\x1B[97m${line}\x1B[0m
2150
- `);
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) => {
2453
+ const { json } = getRootOpts(cmd);
2454
+ try {
2455
+ await requireAuth();
2456
+ const res = await ossFetch(`/api/functions/${encodeURIComponent(slug)}`);
2457
+ const fn = await res.json();
2458
+ if (json) {
2459
+ outputJson(fn);
2460
+ } else {
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);
2467
+ }
2468
+ } catch (err) {
2469
+ handleError(err, json);
2151
2470
  }
2152
- process.stderr.write("\n");
2153
- return;
2154
- }
2155
- const REVEAL_STEPS = 10;
2156
- const REVEAL_DELAY = 30;
2157
- for (let lineIdx = 0; lineIdx < totalLines; lineIdx++) {
2158
- const line = INSFORGE_BANNER[lineIdx];
2159
- for (let step = 0; step <= REVEAL_STEPS; step++) {
2160
- const pos = Math.floor(step / REVEAL_STEPS * line.length);
2161
- let rendered = "";
2162
- for (let i = 0; i < line.length; i++) {
2163
- if (i < pos) {
2164
- rendered += `\x1B[97m${line[i]}\x1B[0m`;
2165
- } else if (i === pos) {
2166
- rendered += `\x1B[1;37m${line[i]}\x1B[0m`;
2167
- } else {
2168
- rendered += `\x1B[90m${line[i]}\x1B[0m`;
2471
+ });
2472
+ }
2473
+
2474
+ // src/commands/functions/delete.ts
2475
+ import * as clack9 from "@clack/prompts";
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);
2479
+ try {
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;
2169
2488
  }
2170
2489
  }
2171
- process.stderr.write(`\r${rendered}`);
2172
- await new Promise((r) => setTimeout(r, REVEAL_DELAY));
2173
- }
2174
- process.stderr.write("\n");
2175
- }
2176
- const SHIMMER_STEPS = 16;
2177
- const SHIMMER_DELAY = 40;
2178
- const SHIMMER_WIDTH = 4;
2179
- for (let step = 0; step < SHIMMER_STEPS; step++) {
2180
- const shimmerPos = Math.floor(step / SHIMMER_STEPS * (maxLen + SHIMMER_WIDTH));
2181
- process.stderr.write(`\x1B[${totalLines}A`);
2182
- for (const line of INSFORGE_BANNER) {
2183
- let rendered = "";
2184
- for (let i = 0; i < line.length; i++) {
2185
- const dist = Math.abs(i - shimmerPos);
2186
- if (dist === 0) {
2187
- rendered += `\x1B[1;97m${line[i]}\x1B[0m`;
2188
- } else if (dist <= SHIMMER_WIDTH) {
2189
- rendered += `\x1B[37m${line[i]}\x1B[0m`;
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);
2496
+ } else {
2497
+ if (result.success) {
2498
+ outputSuccess(`Function "${slug}" deleted successfully.`);
2190
2499
  } else {
2191
- rendered += `\x1B[90m${line[i]}\x1B[0m`;
2500
+ outputSuccess(`Failed to delete function "${slug}".`);
2192
2501
  }
2193
2502
  }
2194
- process.stderr.write(`${rendered}
2195
- `);
2196
- }
2197
- await new Promise((r) => setTimeout(r, SHIMMER_DELAY));
2198
- }
2199
- process.stderr.write(`\x1B[${totalLines}A`);
2200
- for (const line of INSFORGE_BANNER) {
2201
- process.stderr.write(`\x1B[97m${line}\x1B[0m
2202
- `);
2203
- }
2204
- process.stderr.write("\n");
2205
- }
2206
- function getDefaultProjectName() {
2207
- const dirName = path3.basename(process.cwd());
2208
- const sanitized = dirName.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
2209
- return sanitized.length >= 2 ? sanitized : "";
2210
- }
2211
- async function copyDir(src, dest) {
2212
- const entries = await fs3.readdir(src, { withFileTypes: true });
2213
- for (const entry of entries) {
2214
- const srcPath = path3.join(src, entry.name);
2215
- const destPath = path3.join(dest, entry.name);
2216
- if (entry.isDirectory()) {
2217
- await fs3.mkdir(destPath, { recursive: true });
2218
- await copyDir(srcPath, destPath);
2219
- } else {
2220
- await fs3.copyFile(srcPath, destPath);
2503
+ await reportCliUsage("cli.functions.delete", true);
2504
+ } catch (err) {
2505
+ await reportCliUsage("cli.functions.delete", false);
2506
+ handleError(err, json);
2221
2507
  }
2222
- }
2508
+ });
2223
2509
  }
2224
- function registerCreateCommand(program2) {
2225
- 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) => {
2226
- const { json, apiUrl } = getRootOpts(cmd);
2510
+
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);
2227
2515
  try {
2228
- await requireAuth(apiUrl, false);
2229
- if (!json) {
2230
- await animateBanner();
2231
- clack10.intro("Let's build something great");
2232
- }
2233
- let orgId = opts.orgId;
2234
- if (!orgId) {
2235
- const orgs = await listOrganizations(apiUrl);
2236
- if (orgs.length === 0) {
2237
- throw new CLIError("No organizations found.");
2238
- }
2239
- if (json) {
2240
- throw new CLIError("Specify --org-id in JSON mode.");
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;
2241
2526
  }
2242
- const selected = await clack10.select({
2243
- message: "Select an organization:",
2244
- options: orgs.map((o) => ({
2245
- value: o.id,
2246
- label: o.name
2247
- }))
2248
- });
2249
- if (clack10.isCancel(selected)) process.exit(0);
2250
- orgId = selected;
2527
+ outputTable(
2528
+ ["Bucket Name", "Public"],
2529
+ buckets.map((b) => [b.name, b.public ? "Yes" : "No"])
2530
+ );
2251
2531
  }
2252
- const globalConfig = getGlobalConfig();
2253
- globalConfig.default_org_id = orgId;
2254
- saveGlobalConfig(globalConfig);
2255
- let projectName = opts.name;
2256
- if (!projectName) {
2257
- if (json) throw new CLIError("--name is required in JSON mode.");
2258
- const defaultName = getDefaultProjectName();
2259
- const name = await clack10.text({
2260
- message: "Project name:",
2261
- ...defaultName ? { initialValue: defaultName } : {},
2262
- validate: (v) => v.length >= 2 ? void 0 : "Name must be at least 2 characters"
2263
- });
2264
- if (clack10.isCancel(name)) process.exit(0);
2265
- projectName = name;
2532
+ await reportCliUsage("cli.storage.buckets", true);
2533
+ } catch (err) {
2534
+ await reportCliUsage("cli.storage.buckets", false);
2535
+ handleError(err, json);
2536
+ }
2537
+ });
2538
+ }
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);
2546
+ try {
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}`);
2266
2552
  }
2267
- projectName = path3.basename(projectName).replace(/[^a-zA-Z0-9._-]/g, "-").replace(/\.+/g, ".");
2268
- if (projectName.length < 2 || projectName === "." || projectName === "..") {
2269
- 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}`);
2270
2570
  }
2271
- const validTemplates = ["react", "nextjs", "chatbot", "crm", "e-commerce", "empty"];
2272
- let template = opts.template;
2273
- if (template && !validTemplates.includes(template)) {
2274
- 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}".`);
2275
2576
  }
2276
- if (!template) {
2277
- if (json) {
2278
- template = "empty";
2279
- } else {
2280
- const approach = await clack10.select({
2281
- message: "How would you like to start?",
2282
- options: [
2283
- { value: "blank", label: "Blank project", hint: "Start from scratch with .env.local ready" },
2284
- { value: "template", label: "Start from a template", hint: "Pre-built starter apps" }
2285
- ]
2286
- });
2287
- if (clack10.isCancel(approach)) process.exit(0);
2288
- captureEvent(orgId, "create_approach_selected", {
2289
- approach
2290
- });
2291
- if (approach === "blank") {
2292
- template = "empty";
2293
- } else {
2294
- const selected = await clack10.select({
2295
- message: "Choose a starter template:",
2296
- options: [
2297
- { value: "react", label: "Web app template with React" },
2298
- { value: "nextjs", label: "Web app template with Next.js" },
2299
- { value: "chatbot", label: "AI Chatbot with Next.js" },
2300
- { value: "crm", label: "CRM with Next.js" },
2301
- { value: "e-commerce", label: "E-Commerce store with Next.js" }
2302
- ]
2303
- });
2304
- if (clack10.isCancel(selected)) process.exit(0);
2305
- template = selected;
2306
- }
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}`
2307
2598
  }
2308
- }
2309
- captureEvent(orgId, "template_selected", {
2310
- template,
2311
- approach: template === "empty" ? "blank" : "template"
2312
2599
  });
2313
- const s = !json ? clack10.spinner() : null;
2314
- s?.start("Creating project...");
2315
- const project = await createProject(orgId, projectName, opts.region, apiUrl);
2316
- s?.message("Waiting for project to become active...");
2317
- await waitForProjectActive(project.id, apiUrl);
2318
- const apiKey = await getProjectApiKey(project.id, apiUrl);
2319
- const projectConfig = {
2320
- project_id: project.id,
2321
- project_name: project.name,
2322
- org_id: project.organization_id,
2323
- appkey: project.appkey,
2324
- region: project.region,
2325
- api_key: apiKey,
2326
- oss_host: buildOssHost2(project.appkey, project.region)
2327
- };
2328
- saveProjectConfig(projectConfig);
2329
- s?.stop(`Project "${project.name}" created and linked`);
2330
- const hasTemplate = template !== "empty";
2331
- const githubTemplates = ["chatbot", "crm", "e-commerce", "nextjs", "react"];
2332
- if (githubTemplates.includes(template)) {
2333
- await downloadGitHubTemplate(template, projectConfig, json);
2334
- } else if (hasTemplate) {
2335
- 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 });
2336
2609
  } else {
2337
- try {
2338
- const anonKey = await getAnonKey();
2339
- if (!anonKey) {
2340
- if (!json) clack10.log.warn("Could not retrieve anon key. You can add it to .env.local manually.");
2341
- } else {
2342
- const envPath = path3.join(process.cwd(), ".env.local");
2343
- const envContent = [
2344
- "# InsForge",
2345
- `NEXT_PUBLIC_INSFORGE_URL=${projectConfig.oss_host}`,
2346
- `NEXT_PUBLIC_INSFORGE_ANON_KEY=${anonKey}`,
2347
- ""
2348
- ].join("\n");
2349
- await fs3.writeFile(envPath, envContent, { flag: "wx" });
2350
- if (!json) {
2351
- clack10.log.success("Created .env.local with your InsForge credentials");
2352
- }
2353
- }
2354
- } catch (err) {
2355
- const error = err;
2356
- if (!json) {
2357
- if (error.code === "EEXIST") {
2358
- clack10.log.warn(".env.local already exists; skipping InsForge key seeding.");
2359
- } else {
2360
- clack10.log.warn(`Failed to create .env.local: ${error.message}`);
2361
- }
2362
- }
2363
- }
2610
+ outputSuccess(`Downloaded "${objectKey}" to ${outputPath} (${buffer.length} bytes).`);
2364
2611
  }
2365
- await installSkills(json);
2366
- await reportCliUsage("cli.create", true, 6);
2367
- if (hasTemplate) {
2368
- const installSpinner = !json ? clack10.spinner() : null;
2369
- installSpinner?.start("Installing dependencies...");
2370
- try {
2371
- await execAsync2("npm install", { cwd: process.cwd(), maxBuffer: 10 * 1024 * 1024 });
2372
- installSpinner?.stop("Dependencies installed");
2373
- } catch (err) {
2374
- installSpinner?.stop("Failed to install dependencies");
2375
- if (!json) {
2376
- clack10.log.warn(`npm install failed: ${err.message}`);
2377
- clack10.log.info("Run `npm install` manually to install dependencies.");
2378
- }
2379
- }
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"}).`);
2380
2634
  }
2381
- let liveUrl = null;
2382
- if (hasTemplate && !json) {
2383
- const shouldDeploy = await clack10.confirm({
2384
- 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.`
2385
2653
  });
2386
- if (!clack10.isCancel(shouldDeploy) && shouldDeploy) {
2387
- try {
2388
- const envVars = await readEnvFile(process.cwd());
2389
- const startBody = {};
2390
- if (envVars.length > 0) {
2391
- startBody.envVars = envVars;
2392
- }
2393
- const deploySpinner = clack10.spinner();
2394
- const result = await deployProject({
2395
- sourceDir: process.cwd(),
2396
- startBody,
2397
- spinner: deploySpinner
2398
- });
2399
- if (result.isReady) {
2400
- deploySpinner.stop("Deployment complete");
2401
- liveUrl = result.liveUrl;
2402
- } else {
2403
- deploySpinner.stop("Deployment is still building");
2404
- clack10.log.info(`Deployment ID: ${result.deploymentId}`);
2405
- clack10.log.warn("Deployment did not finish within 2 minutes.");
2406
- clack10.log.info(`Check status with: npx @insforge/cli deployments status ${result.deploymentId}`);
2407
- }
2408
- } catch (err) {
2409
- clack10.log.warn(`Deploy failed: ${err.message}`);
2410
- }
2654
+ if (!confirm8 || clack10.isCancel(confirm8)) {
2655
+ process.exit(0);
2411
2656
  }
2412
2657
  }
2413
- 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();
2414
2662
  if (json) {
2415
- outputJson({
2416
- success: true,
2417
- project: { id: project.id, name: project.name, appkey: project.appkey, region: project.region },
2418
- template,
2419
- urls: {
2420
- dashboard: dashboardUrl,
2421
- ...liveUrl ? { liveSite: liveUrl } : {}
2422
- }
2423
- });
2663
+ outputJson(data);
2424
2664
  } else {
2425
- clack10.log.step(`Dashboard: ${dashboardUrl}`);
2426
- if (liveUrl) {
2427
- clack10.log.success(`Live site: ${liveUrl}`);
2428
- }
2429
- clack10.outro("Done!");
2665
+ outputSuccess(`Bucket "${name}" deleted.`);
2430
2666
  }
2431
2667
  } catch (err) {
2432
2668
  handleError(err, json);
2433
- } finally {
2434
- await shutdownAnalytics();
2435
2669
  }
2436
2670
  });
2437
2671
  }
2438
- async function downloadTemplate(framework, projectConfig, projectName, json, _apiUrl) {
2439
- const s = !json ? clack10.spinner() : null;
2440
- s?.start("Downloading template...");
2441
- try {
2442
- const anonKey = await getAnonKey();
2443
- if (!anonKey) {
2444
- throw new Error("Failed to retrieve anon key from backend");
2445
- }
2446
- const tempDir = tmpdir();
2447
- const targetDir = projectName;
2448
- const templatePath = path3.join(tempDir, targetDir);
2449
- try {
2450
- await fs3.rm(templatePath, { recursive: true, force: true });
2451
- } catch {
2452
- }
2453
- const frame = framework === "nextjs" ? "nextjs" : "react";
2454
- const esc = (s2) => process.platform === "win32" ? `"${s2.replace(/"/g, '\\"')}"` : `'${s2.replace(/'/g, "'\\''")}'`;
2455
- const command = `npx --yes create-insforge-app@latest ${esc(targetDir)} --frame ${frame} --base-url ${esc(projectConfig.oss_host)} --anon-key ${esc(anonKey)} --skip-install`;
2456
- s?.message(`Running create-insforge-app (${frame})...`);
2457
- await execAsync2(command, {
2458
- maxBuffer: 10 * 1024 * 1024,
2459
- cwd: tempDir
2460
- });
2461
- s?.message("Copying template files...");
2462
- const cwd = process.cwd();
2463
- await copyDir(templatePath, cwd);
2464
- await fs3.rm(templatePath, { recursive: true, force: true }).catch(() => {
2465
- });
2466
- s?.stop("Template files downloaded");
2467
- } catch (err) {
2468
- s?.stop("Template download failed");
2469
- if (!json) {
2470
- clack10.log.warn(`Failed to download template: ${err.message}`);
2471
- clack10.log.info("You can manually set up the template later.");
2472
- }
2473
- }
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`;
2474
2678
  }
2475
- async function downloadGitHubTemplate(templateName, projectConfig, json) {
2476
- const s = !json ? clack10.spinner() : null;
2477
- s?.start(`Downloading ${templateName} template...`);
2478
- const tempDir = path3.join(tmpdir(), `insforge-template-${Date.now()}`);
2479
- try {
2480
- await fs3.mkdir(tempDir, { recursive: true });
2481
- await execAsync2(
2482
- "git clone --depth 1 https://github.com/InsForge/insforge-templates.git .",
2483
- { cwd: tempDir, maxBuffer: 10 * 1024 * 1024, timeout: 6e4 }
2484
- );
2485
- const templateDir = path3.join(tempDir, templateName);
2486
- const stat4 = await fs3.stat(templateDir).catch(() => null);
2487
- if (!stat4?.isDirectory()) {
2488
- throw new Error(`Template "${templateName}" not found in repository`);
2489
- }
2490
- s?.message("Copying template files...");
2491
- const cwd = process.cwd();
2492
- await copyDir(templateDir, cwd);
2493
- const envExamplePath = path3.join(cwd, ".env.example");
2494
- const envExampleExists = await fs3.stat(envExamplePath).catch(() => null);
2495
- if (envExampleExists) {
2496
- const anonKey = await getAnonKey();
2497
- const envExample = await fs3.readFile(envExamplePath, "utf-8");
2498
- const envContent = envExample.replace(
2499
- /^([A-Z][A-Z0-9_]*=)(.*)$/gm,
2500
- (_, prefix, _value) => {
2501
- const key = prefix.slice(0, -1);
2502
- if (/INSFORGE.*(URL|BASE_URL)$/.test(key)) return `${prefix}${projectConfig.oss_host}`;
2503
- if (/INSFORGE.*ANON_KEY$/.test(key)) return `${prefix}${anonKey}`;
2504
- if (key === "NEXT_PUBLIC_APP_URL") return `${prefix}https://${projectConfig.appkey}.insforge.site`;
2505
- return `${prefix}${_value}`;
2506
- }
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()}`
2507
2691
  );
2508
- const envLocalPath = path3.join(cwd, ".env.local");
2509
- try {
2510
- await fs3.writeFile(envLocalPath, envContent, { flag: "wx" });
2511
- } catch (e) {
2512
- if (e.code === "EEXIST") {
2513
- if (!json) clack10.log.warn(".env.local already exists; skipping env seeding.");
2514
- } else {
2515
- 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;
2516
2706
  }
2517
- }
2518
- }
2519
- s?.stop(`${templateName} template downloaded`);
2520
- const migrationPath = path3.join(cwd, "migrations", "db_init.sql");
2521
- const migrationExists = await fs3.stat(migrationPath).catch(() => null);
2522
- if (migrationExists) {
2523
- const dbSpinner = !json ? clack10.spinner() : null;
2524
- dbSpinner?.start("Running database migrations...");
2525
- try {
2526
- const sql = await fs3.readFile(migrationPath, "utf-8");
2527
- await runRawSql(sql, true);
2528
- dbSpinner?.stop("Database migrations applied");
2529
- } catch (err) {
2530
- dbSpinner?.stop("Database migration failed");
2531
- if (!json) {
2532
- clack10.log.warn(`Migration failed: ${err.message}`);
2533
- clack10.log.info('You can run the migration manually: npx @insforge/cli db query --unrestricted "$(cat migrations/db_init.sql)"');
2534
- } else {
2535
- throw err;
2707
+ const total = raw.pagination?.total;
2708
+ if (total !== void 0) {
2709
+ console.log(`Showing ${objects.length} of ${total} objects:
2710
+ `);
2536
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
+ );
2537
2721
  }
2722
+ await reportCliUsage("cli.storage.list-objects", true);
2723
+ } catch (err) {
2724
+ await reportCliUsage("cli.storage.list-objects", false);
2725
+ handleError(err, json);
2538
2726
  }
2539
- } catch (err) {
2540
- s?.stop(`${templateName} template download failed`);
2541
- if (!json) {
2542
- clack10.log.warn(`Failed to download ${templateName} template: ${err.message}`);
2543
- clack10.log.info("You can manually clone from: https://github.com/InsForge/insforge-templates");
2544
- }
2545
- } finally {
2546
- await fs3.rm(tempDir, { recursive: true, force: true }).catch(() => {
2547
- });
2548
- }
2727
+ });
2549
2728
  }
2550
2729
 
2551
2730
  // src/commands/info.ts
@@ -2883,8 +3062,8 @@ async function listDocs(json) {
2883
3062
  );
2884
3063
  }
2885
3064
  }
2886
- async function fetchDoc(path4, label, json) {
2887
- const res = await ossFetch(path4);
3065
+ async function fetchDoc(path5, label, json) {
3066
+ const res = await ossFetch(path5);
2888
3067
  const data = await res.json();
2889
3068
  const doc = data.data ?? data;
2890
3069
  if (json) {
@@ -3271,6 +3450,445 @@ function registerSchedulesLogsCommand(schedulesCmd2) {
3271
3450
  });
3272
3451
  }
3273
3452
 
3453
+ // src/commands/compute/list.ts
3454
+ function registerComputeListCommand(computeCmd2) {
3455
+ computeCmd2.command("list").description("List all compute services").action(async (_opts, cmd) => {
3456
+ const { json } = getRootOpts(cmd);
3457
+ try {
3458
+ await requireAuth();
3459
+ const res = await ossFetch("/api/compute/services");
3460
+ const raw = await res.json();
3461
+ const services = Array.isArray(raw) ? raw : [];
3462
+ if (json) {
3463
+ outputJson(services);
3464
+ } else {
3465
+ if (services.length === 0) {
3466
+ console.log("No compute services found.");
3467
+ await reportCliUsage("cli.compute.list", true);
3468
+ return;
3469
+ }
3470
+ outputTable(
3471
+ ["Name", "Status", "Image", "CPU", "Memory", "Endpoint"],
3472
+ services.map((s) => [
3473
+ String(s.name ?? "-"),
3474
+ String(s.status ?? "-"),
3475
+ String(s.imageUrl ?? "-"),
3476
+ String(s.cpu ?? "-"),
3477
+ s.memory ? `${s.memory}MB` : "-",
3478
+ String(s.endpointUrl ?? "-")
3479
+ ])
3480
+ );
3481
+ }
3482
+ await reportCliUsage("cli.compute.list", true);
3483
+ } catch (err) {
3484
+ await reportCliUsage("cli.compute.list", false);
3485
+ handleError(err, json);
3486
+ }
3487
+ });
3488
+ }
3489
+
3490
+ // src/commands/compute/get.ts
3491
+ function registerComputeGetCommand(computeCmd2) {
3492
+ computeCmd2.command("get <id>").description("Get details of a compute service").action(async (id, _opts, cmd) => {
3493
+ const { json } = getRootOpts(cmd);
3494
+ try {
3495
+ await requireAuth();
3496
+ const res = await ossFetch(`/api/compute/services/${encodeURIComponent(id)}`);
3497
+ const service = await res.json();
3498
+ if (json) {
3499
+ outputJson(service);
3500
+ } else {
3501
+ outputInfo(`Name: ${service.name}`);
3502
+ outputInfo(`ID: ${service.id}`);
3503
+ outputInfo(`Status: ${service.status}`);
3504
+ outputInfo(`Image: ${service.imageUrl}`);
3505
+ outputInfo(`CPU: ${service.cpu}`);
3506
+ outputInfo(`Memory: ${service.memory}MB`);
3507
+ outputInfo(`Region: ${service.region}`);
3508
+ outputInfo(`Endpoint: ${service.endpointUrl ?? "n/a"}`);
3509
+ outputInfo(`Created: ${service.createdAt}`);
3510
+ }
3511
+ await reportCliUsage("cli.compute.get", true);
3512
+ } catch (err) {
3513
+ await reportCliUsage("cli.compute.get", false);
3514
+ handleError(err, json);
3515
+ }
3516
+ });
3517
+ }
3518
+
3519
+ // src/commands/compute/create.ts
3520
+ function registerComputeCreateCommand(computeCmd2) {
3521
+ computeCmd2.command("create").description("Create and deploy a compute service").requiredOption("--name <name>", "Service name (DNS-safe, e.g. my-api)").requiredOption("--image <image>", "Docker image URL (e.g. nginx:alpine)").option("--port <port>", "Container port", "8080").option("--cpu <tier>", "CPU tier (shared-1x, shared-2x, performance-1x, performance-2x, performance-4x)", "shared-1x").option("--memory <mb>", "Memory in MB (256, 512, 1024, 2048, 4096, 8192)", "512").option("--region <region>", "Fly.io region", "iad").option("--env <json>", "Environment variables as JSON object").action(async (opts, cmd) => {
3522
+ const { json } = getRootOpts(cmd);
3523
+ try {
3524
+ await requireAuth();
3525
+ const body = {
3526
+ name: opts.name,
3527
+ imageUrl: opts.image,
3528
+ port: Number(opts.port),
3529
+ cpu: opts.cpu,
3530
+ memory: Number(opts.memory),
3531
+ region: opts.region
3532
+ };
3533
+ if (opts.env) {
3534
+ try {
3535
+ body.envVars = JSON.parse(opts.env);
3536
+ } catch {
3537
+ throw new CLIError("Invalid JSON for --env");
3538
+ }
3539
+ }
3540
+ const res = await ossFetch("/api/compute/services", {
3541
+ method: "POST",
3542
+ body: JSON.stringify(body)
3543
+ });
3544
+ const service = await res.json();
3545
+ if (json) {
3546
+ outputJson(service);
3547
+ } else {
3548
+ outputSuccess(`Service "${service.name}" created [${service.status}]`);
3549
+ if (service.endpointUrl) {
3550
+ console.log(` Endpoint: ${service.endpointUrl}`);
3551
+ }
3552
+ }
3553
+ await reportCliUsage("cli.compute.create", true);
3554
+ } catch (err) {
3555
+ await reportCliUsage("cli.compute.create", false);
3556
+ handleError(err, json);
3557
+ }
3558
+ });
3559
+ }
3560
+
3561
+ // src/commands/compute/update.ts
3562
+ function registerComputeUpdateCommand(computeCmd2) {
3563
+ computeCmd2.command("update <id>").description("Update a compute service").option("--image <image>", "Docker image URL").option("--port <port>", "Container port").option("--cpu <tier>", "CPU tier").option("--memory <mb>", "Memory in MB").option("--region <region>", "Fly.io region").option("--env <json>", "Environment variables as JSON object").action(async (id, opts, cmd) => {
3564
+ const { json } = getRootOpts(cmd);
3565
+ try {
3566
+ await requireAuth();
3567
+ const body = {};
3568
+ if (opts.image) body.imageUrl = opts.image;
3569
+ if (opts.port) {
3570
+ if (!Number.isFinite(Number(opts.port))) {
3571
+ throw new CLIError("Invalid value for --port: must be a number");
3572
+ }
3573
+ body.port = Number(opts.port);
3574
+ }
3575
+ if (opts.cpu) body.cpu = opts.cpu;
3576
+ if (opts.memory) {
3577
+ if (!Number.isFinite(Number(opts.memory))) {
3578
+ throw new CLIError("Invalid value for --memory: must be a number");
3579
+ }
3580
+ body.memory = Number(opts.memory);
3581
+ }
3582
+ if (opts.region) body.region = opts.region;
3583
+ if (opts.env) {
3584
+ try {
3585
+ body.envVars = JSON.parse(opts.env);
3586
+ } catch {
3587
+ throw new CLIError("Invalid JSON for --env");
3588
+ }
3589
+ }
3590
+ if (Object.keys(body).length === 0) {
3591
+ throw new CLIError("No update fields provided. Use --image, --port, --cpu, --memory, --region, or --env.");
3592
+ }
3593
+ const res = await ossFetch(`/api/compute/services/${encodeURIComponent(id)}`, {
3594
+ method: "PATCH",
3595
+ body: JSON.stringify(body)
3596
+ });
3597
+ const service = await res.json();
3598
+ if (json) {
3599
+ outputJson(service);
3600
+ } else {
3601
+ outputSuccess(`Service "${service.name}" updated [${service.status}]`);
3602
+ }
3603
+ await reportCliUsage("cli.compute.update", true);
3604
+ } catch (err) {
3605
+ await reportCliUsage("cli.compute.update", false);
3606
+ handleError(err, json);
3607
+ }
3608
+ });
3609
+ }
3610
+
3611
+ // src/commands/compute/delete.ts
3612
+ function registerComputeDeleteCommand(computeCmd2) {
3613
+ computeCmd2.command("delete <id>").description("Delete a compute service and its Fly.io resources").action(async (id, _opts, cmd) => {
3614
+ const { json } = getRootOpts(cmd);
3615
+ try {
3616
+ await requireAuth();
3617
+ const res = await ossFetch(`/api/compute/services/${encodeURIComponent(id)}`, {
3618
+ method: "DELETE"
3619
+ });
3620
+ const data = await res.json();
3621
+ if (json) {
3622
+ outputJson(data);
3623
+ } else {
3624
+ outputSuccess("Service deleted.");
3625
+ }
3626
+ await reportCliUsage("cli.compute.delete", true);
3627
+ } catch (err) {
3628
+ await reportCliUsage("cli.compute.delete", false);
3629
+ handleError(err, json);
3630
+ }
3631
+ });
3632
+ }
3633
+
3634
+ // src/commands/compute/start.ts
3635
+ function registerComputeStartCommand(computeCmd2) {
3636
+ computeCmd2.command("start <id>").description("Start a stopped compute service").action(async (id, _opts, cmd) => {
3637
+ const { json } = getRootOpts(cmd);
3638
+ try {
3639
+ await requireAuth();
3640
+ const res = await ossFetch(`/api/compute/services/${encodeURIComponent(id)}/start`, {
3641
+ method: "POST"
3642
+ });
3643
+ const service = await res.json();
3644
+ if (json) {
3645
+ outputJson(service);
3646
+ } else {
3647
+ outputSuccess(`Service "${service.name}" started [${service.status}]`);
3648
+ if (service.endpointUrl) {
3649
+ console.log(` Endpoint: ${service.endpointUrl}`);
3650
+ }
3651
+ }
3652
+ await reportCliUsage("cli.compute.start", true);
3653
+ } catch (err) {
3654
+ await reportCliUsage("cli.compute.start", false);
3655
+ handleError(err, json);
3656
+ }
3657
+ });
3658
+ }
3659
+
3660
+ // src/commands/compute/stop.ts
3661
+ function registerComputeStopCommand(computeCmd2) {
3662
+ computeCmd2.command("stop <id>").description("Stop a running compute service").action(async (id, _opts, cmd) => {
3663
+ const { json } = getRootOpts(cmd);
3664
+ try {
3665
+ await requireAuth();
3666
+ const res = await ossFetch(`/api/compute/services/${encodeURIComponent(id)}/stop`, {
3667
+ method: "POST"
3668
+ });
3669
+ const service = await res.json();
3670
+ if (json) {
3671
+ outputJson(service);
3672
+ } else {
3673
+ outputSuccess(`Service "${service.name}" stopped.`);
3674
+ }
3675
+ await reportCliUsage("cli.compute.stop", true);
3676
+ } catch (err) {
3677
+ await reportCliUsage("cli.compute.stop", false);
3678
+ handleError(err, json);
3679
+ }
3680
+ });
3681
+ }
3682
+
3683
+ // src/commands/compute/logs.ts
3684
+ function registerComputeLogsCommand(computeCmd2) {
3685
+ computeCmd2.command("logs <id>").description("Get compute service logs (machine events)").option("--limit <n>", "Max number of log entries", "50").action(async (id, opts, cmd) => {
3686
+ const { json } = getRootOpts(cmd);
3687
+ try {
3688
+ await requireAuth();
3689
+ const limit = Math.max(1, Math.min(Number(opts.limit) || 50, 1e3));
3690
+ const res = await ossFetch(
3691
+ `/api/compute/services/${encodeURIComponent(id)}/logs?limit=${limit}`
3692
+ );
3693
+ const logs = await res.json();
3694
+ if (json) {
3695
+ outputJson(logs);
3696
+ } else {
3697
+ if (!Array.isArray(logs) || logs.length === 0) {
3698
+ console.log("No logs found.");
3699
+ await reportCliUsage("cli.compute.logs", true);
3700
+ return;
3701
+ }
3702
+ for (const entry of logs) {
3703
+ const ts = new Date(entry.timestamp).toISOString();
3704
+ console.log(`${ts} ${entry.message}`);
3705
+ }
3706
+ }
3707
+ await reportCliUsage("cli.compute.logs", true);
3708
+ } catch (err) {
3709
+ await reportCliUsage("cli.compute.logs", false);
3710
+ handleError(err, json);
3711
+ }
3712
+ });
3713
+ }
3714
+
3715
+ // src/commands/compute/deploy.ts
3716
+ import { existsSync as existsSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync4, unlinkSync as unlinkSync2, renameSync } from "fs";
3717
+ import { join as join8 } from "path";
3718
+ import { execSync, spawn } from "child_process";
3719
+ function parseFlyToml(dir) {
3720
+ const tomlPath = join8(dir, "fly.toml");
3721
+ if (!existsSync5(tomlPath)) return {};
3722
+ const content = readFileSync6(tomlPath, "utf-8");
3723
+ const config = {};
3724
+ const portMatch = content.match(/internal_port\s*=\s*(\d+)/);
3725
+ if (portMatch) config.internalPort = Number(portMatch[1]);
3726
+ const regionMatch = content.match(/primary_region\s*=\s*'([^']+)'/);
3727
+ if (regionMatch) config.region = regionMatch[1];
3728
+ const memoryMatch = content.match(/memory\s*=\s*'(\d+)/);
3729
+ if (memoryMatch) config.memory = memoryMatch[1];
3730
+ const cpuKindMatch = content.match(/cpu_kind\s*=\s*'([^']+)'/);
3731
+ if (cpuKindMatch) config.cpuKind = cpuKindMatch[1];
3732
+ const cpusMatch = content.match(/cpus\s*=\s*(\d+)/);
3733
+ if (cpusMatch) config.cpus = Number(cpusMatch[1]);
3734
+ return config;
3735
+ }
3736
+ function memoryToCpuTier(cpuKind, cpus) {
3737
+ if (!cpuKind) return void 0;
3738
+ const kind = cpuKind === "performance" ? "performance" : "shared";
3739
+ const count = cpus ?? 1;
3740
+ return `${kind}-${count}x`;
3741
+ }
3742
+ function generateFlyToml(appName, opts) {
3743
+ const cpuParts = opts.cpu.split("-");
3744
+ const cpuKind = cpuParts[0] ?? "shared";
3745
+ const cpuCount = parseInt(cpuParts[1] ?? "1", 10);
3746
+ return `# Auto-generated by InsForge CLI
3747
+ app = '${appName}'
3748
+ primary_region = '${opts.region}'
3749
+
3750
+ [build]
3751
+ dockerfile = 'Dockerfile'
3752
+
3753
+ [http_service]
3754
+ internal_port = ${opts.port}
3755
+ force_https = true
3756
+ auto_stop_machines = 'stop'
3757
+ auto_start_machines = true
3758
+ min_machines_running = 0
3759
+
3760
+ [[vm]]
3761
+ memory = '${opts.memory}mb'
3762
+ cpu_kind = '${cpuKind}'
3763
+ cpus = ${cpuCount}
3764
+ `;
3765
+ }
3766
+ function checkFlyctl() {
3767
+ try {
3768
+ execSync("flyctl version", { stdio: "pipe" });
3769
+ } catch {
3770
+ throw new CLIError(
3771
+ "flyctl is not installed.\nInstall it with: curl -L https://fly.io/install.sh | sh\nOr: brew install flyctl"
3772
+ );
3773
+ }
3774
+ }
3775
+ function getFlyToken() {
3776
+ const token = process.env.FLY_API_TOKEN;
3777
+ if (!token) {
3778
+ throw new CLIError(
3779
+ "FLY_API_TOKEN environment variable is required for compute deploy.\nSet it with: export FLY_API_TOKEN=<your-fly-token>"
3780
+ );
3781
+ }
3782
+ return token;
3783
+ }
3784
+ function registerComputeDeployCommand(computeCmd2) {
3785
+ computeCmd2.command("deploy [directory]").description("Build and deploy a Dockerfile as a compute service").requiredOption("--name <name>", "Service name").option("--port <port>", "Container port").option("--cpu <tier>", "CPU tier (shared-1x, shared-2x, performance-1x, etc.)").option("--memory <mb>", "Memory in MB (256, 512, 1024, 2048, 4096, 8192)").option("--region <region>", "Fly.io region").option("--env <json>", "Environment variables as JSON object").action(async (directory, opts, cmd) => {
3786
+ const { json } = getRootOpts(cmd);
3787
+ try {
3788
+ await requireAuth();
3789
+ checkFlyctl();
3790
+ const flyToken = getFlyToken();
3791
+ const dir = directory ?? process.cwd();
3792
+ const dockerfilePath = join8(dir, "Dockerfile");
3793
+ if (!existsSync5(dockerfilePath)) {
3794
+ throw new CLIError(`No Dockerfile found in ${dir}`);
3795
+ }
3796
+ const flyTomlDefaults = parseFlyToml(dir);
3797
+ const port = Number(opts.port) || flyTomlDefaults.internalPort || 8080;
3798
+ const cpu = opts.cpu || memoryToCpuTier(flyTomlDefaults.cpuKind, flyTomlDefaults.cpus) || "shared-1x";
3799
+ const memory = Number(opts.memory) || Number(flyTomlDefaults.memory) || 512;
3800
+ const region = opts.region || flyTomlDefaults.region || "iad";
3801
+ let envVars;
3802
+ if (opts.env) {
3803
+ try {
3804
+ envVars = JSON.parse(opts.env);
3805
+ } catch {
3806
+ throw new CLIError("Invalid JSON for --env");
3807
+ }
3808
+ }
3809
+ if (!json) outputInfo(`Checking for existing service "${opts.name}"...`);
3810
+ const listRes = await ossFetch("/api/compute/services");
3811
+ const services = await listRes.json();
3812
+ const existing = services.find((s) => s.name === opts.name);
3813
+ let serviceId;
3814
+ let flyAppId;
3815
+ if (existing) {
3816
+ serviceId = existing.id;
3817
+ flyAppId = existing.flyAppId;
3818
+ if (!json) outputInfo(`Found existing service, redeploying...`);
3819
+ } else {
3820
+ if (!json) outputInfo(`Creating service "${opts.name}"...`);
3821
+ const body = {
3822
+ name: opts.name,
3823
+ imageUrl: "dockerfile",
3824
+ port,
3825
+ cpu,
3826
+ memory,
3827
+ region
3828
+ };
3829
+ if (envVars) body.envVars = envVars;
3830
+ const deployRes = await ossFetch("/api/compute/services/deploy", {
3831
+ method: "POST",
3832
+ body: JSON.stringify(body)
3833
+ });
3834
+ const service = await deployRes.json();
3835
+ serviceId = service.id;
3836
+ flyAppId = service.flyAppId;
3837
+ }
3838
+ const existingTomlPath = join8(dir, "fly.toml");
3839
+ const backupTomlPath = join8(dir, "fly.toml.insforge-backup");
3840
+ let hadExistingToml = false;
3841
+ if (existsSync5(existingTomlPath)) {
3842
+ hadExistingToml = true;
3843
+ renameSync(existingTomlPath, backupTomlPath);
3844
+ }
3845
+ const tomlContent = generateFlyToml(flyAppId, { port, memory, cpu, region });
3846
+ writeFileSync4(existingTomlPath, tomlContent);
3847
+ try {
3848
+ if (!json) outputInfo("Building and deploying (this may take a few minutes)...");
3849
+ await new Promise((resolve4, reject) => {
3850
+ const child = spawn(
3851
+ "flyctl",
3852
+ ["deploy", "--remote-only", "--app", flyAppId, "--access-token", flyToken, "--yes"],
3853
+ { cwd: dir, stdio: json ? "pipe" : "inherit" }
3854
+ );
3855
+ child.on("close", (code) => {
3856
+ if (code === 0) resolve4();
3857
+ else reject(new CLIError(`flyctl deploy failed with exit code ${code}`));
3858
+ });
3859
+ child.on("error", (err) => reject(new CLIError(`flyctl deploy error: ${err.message}`)));
3860
+ });
3861
+ if (!json) outputInfo("Syncing deployment info...");
3862
+ const syncRes = await ossFetch(
3863
+ `/api/compute/services/${encodeURIComponent(serviceId)}/sync`,
3864
+ { method: "PATCH" }
3865
+ );
3866
+ const synced = await syncRes.json();
3867
+ if (json) {
3868
+ outputJson(synced);
3869
+ } else {
3870
+ outputSuccess(`Service "${synced.name}" deployed [${synced.status}]`);
3871
+ if (synced.endpointUrl) {
3872
+ console.log(` Endpoint: ${synced.endpointUrl}`);
3873
+ }
3874
+ }
3875
+ await reportCliUsage("cli.compute.deploy", true);
3876
+ } finally {
3877
+ try {
3878
+ unlinkSync2(existingTomlPath);
3879
+ if (hadExistingToml) {
3880
+ renameSync(backupTomlPath, existingTomlPath);
3881
+ }
3882
+ } catch {
3883
+ }
3884
+ }
3885
+ } catch (err) {
3886
+ await reportCliUsage("cli.compute.deploy", false);
3887
+ handleError(err, json);
3888
+ }
3889
+ });
3890
+ }
3891
+
3274
3892
  // src/commands/logs.ts
3275
3893
  var VALID_SOURCES = ["insforge.logs", "postgREST.logs", "postgres.logs", "function.logs", "function-deploy.logs"];
3276
3894
  var SOURCE_LOOKUP = new Map(VALID_SOURCES.map((s) => [s.toLowerCase(), s]));
@@ -4019,7 +4637,7 @@ function formatBytesCompact(bytes) {
4019
4637
 
4020
4638
  // src/index.ts
4021
4639
  var __dirname = dirname(fileURLToPath(import.meta.url));
4022
- var pkg = JSON.parse(readFileSync6(join7(__dirname, "../package.json"), "utf-8"));
4640
+ var pkg = JSON.parse(readFileSync7(join9(__dirname, "../package.json"), "utf-8"));
4023
4641
  var INSFORGE_LOGO = `
4024
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
4025
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
@@ -4087,6 +4705,16 @@ registerLogsCommand(program);
4087
4705
  registerMetadataCommand(program);
4088
4706
  var diagnoseCmd = program.command("diagnose");
4089
4707
  registerDiagnoseCommands(diagnoseCmd);
4708
+ var computeCmd = program.command("compute").description("Manage compute services (Docker containers on Fly.io)");
4709
+ registerComputeListCommand(computeCmd);
4710
+ registerComputeGetCommand(computeCmd);
4711
+ registerComputeCreateCommand(computeCmd);
4712
+ registerComputeDeployCommand(computeCmd);
4713
+ registerComputeUpdateCommand(computeCmd);
4714
+ registerComputeDeleteCommand(computeCmd);
4715
+ registerComputeStartCommand(computeCmd);
4716
+ registerComputeStopCommand(computeCmd);
4717
+ registerComputeLogsCommand(computeCmd);
4090
4718
  var schedulesCmd = program.command("schedules").description("Manage scheduled tasks (cron jobs)");
4091
4719
  registerSchedulesListCommand(schedulesCmd);
4092
4720
  registerSchedulesGetCommand(schedulesCmd);