@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/README.md +185 -170
- package/dist/index.js +2072 -1444
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
5
|
-
import { join as
|
|
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((
|
|
208
|
-
resolveResult =
|
|
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(
|
|
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}${
|
|
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}${
|
|
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
|
|
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/
|
|
801
|
-
|
|
802
|
-
|
|
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
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
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(
|
|
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}${
|
|
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
|
-
|
|
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/
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
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
|
-
|
|
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/
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
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
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
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
|
|
1070
|
-
|
|
1071
|
-
const
|
|
1072
|
-
|
|
1073
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1106
|
-
|
|
1107
|
-
const
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
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
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
}
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
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
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
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.
|
|
1097
|
+
await reportCliUsage("cli.deployments.deploy", true);
|
|
1165
1098
|
} catch (err) {
|
|
1166
|
-
await reportCliUsage("cli.
|
|
1099
|
+
await reportCliUsage("cli.deployments.deploy", false);
|
|
1167
1100
|
handleError(err, json);
|
|
1168
1101
|
}
|
|
1169
1102
|
});
|
|
1170
1103
|
}
|
|
1171
1104
|
|
|
1172
|
-
// src/commands/
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
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
|
-
|
|
1190
|
-
|
|
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
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
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
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
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
|
-
|
|
1211
|
-
if (
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
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
|
|
1219
|
-
let
|
|
1220
|
-
|
|
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
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
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
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
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
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
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
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
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/
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
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
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
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
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
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
|
-
|
|
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
|
|
1713
|
+
let project;
|
|
1714
|
+
let apiKey;
|
|
1342
1715
|
try {
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
}
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
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
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
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
|
-
|
|
1378
|
-
|
|
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
|
-
|
|
1749
|
+
await installSkills(json);
|
|
1750
|
+
await reportCliUsage("cli.link", true, 6);
|
|
1381
1751
|
try {
|
|
1382
|
-
|
|
1752
|
+
await reportAgentConnected({ project_id: project.id }, apiUrl);
|
|
1383
1753
|
} catch {
|
|
1384
|
-
throw new CLIError("Invalid JSON in --data.");
|
|
1385
1754
|
}
|
|
1386
|
-
const
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
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
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
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
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
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.
|
|
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/
|
|
1473
|
-
|
|
1474
|
-
|
|
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
|
|
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(
|
|
1850
|
+
outputJson(raw);
|
|
1513
1851
|
} else {
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
}
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
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
|
-
|
|
1532
|
-
await reportCliUsage("cli.functions.deploy", true);
|
|
1865
|
+
}
|
|
1866
|
+
await reportCliUsage("cli.db.query", true);
|
|
1533
1867
|
} catch (err) {
|
|
1534
|
-
await reportCliUsage("cli.
|
|
1868
|
+
await reportCliUsage("cli.db.query", false);
|
|
1535
1869
|
handleError(err, json);
|
|
1536
1870
|
}
|
|
1537
1871
|
});
|
|
1538
1872
|
}
|
|
1539
1873
|
|
|
1540
|
-
// src/commands/
|
|
1541
|
-
function
|
|
1542
|
-
|
|
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
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
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
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
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
|
|
1584
|
-
function
|
|
1585
|
-
|
|
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(
|
|
1590
|
-
const
|
|
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(
|
|
1912
|
+
outputJson(raw);
|
|
1593
1913
|
} else {
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
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/
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
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
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
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
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
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(
|
|
1976
|
+
outputJson(raw);
|
|
1629
1977
|
} else {
|
|
1630
|
-
if (
|
|
1631
|
-
|
|
1632
|
-
|
|
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.
|
|
1994
|
+
await reportCliUsage("cli.db.policies", true);
|
|
1637
1995
|
} catch (err) {
|
|
1638
|
-
await reportCliUsage("cli.
|
|
1996
|
+
await reportCliUsage("cli.db.policies", false);
|
|
1639
1997
|
handleError(err, json);
|
|
1640
1998
|
}
|
|
1641
1999
|
});
|
|
1642
2000
|
}
|
|
1643
2001
|
|
|
1644
|
-
// src/commands/
|
|
1645
|
-
function
|
|
1646
|
-
|
|
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/
|
|
2008
|
+
const res = await ossFetch("/api/database/triggers");
|
|
1651
2009
|
const raw = await res.json();
|
|
1652
|
-
const
|
|
2010
|
+
const triggers = Array.isArray(raw) ? raw : raw.triggers ?? [];
|
|
1653
2011
|
if (json) {
|
|
1654
2012
|
outputJson(raw);
|
|
1655
2013
|
} else {
|
|
1656
|
-
if (
|
|
1657
|
-
console.log("No
|
|
2014
|
+
if (triggers.length === 0) {
|
|
2015
|
+
console.log("No database triggers found.");
|
|
1658
2016
|
return;
|
|
1659
2017
|
}
|
|
1660
2018
|
outputTable(
|
|
1661
|
-
["
|
|
1662
|
-
|
|
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.
|
|
2031
|
+
await reportCliUsage("cli.db.triggers", true);
|
|
1666
2032
|
} catch (err) {
|
|
1667
|
-
await reportCliUsage("cli.
|
|
2033
|
+
await reportCliUsage("cli.db.triggers", false);
|
|
1668
2034
|
handleError(err, json);
|
|
1669
2035
|
}
|
|
1670
2036
|
});
|
|
1671
2037
|
}
|
|
1672
2038
|
|
|
1673
|
-
// src/commands/
|
|
1674
|
-
|
|
1675
|
-
|
|
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
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
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
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
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
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
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(
|
|
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
|
-
|
|
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/
|
|
1717
|
-
import {
|
|
1718
|
-
import {
|
|
1719
|
-
function
|
|
1720
|
-
|
|
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
|
|
1727
|
-
const
|
|
1728
|
-
const
|
|
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 ?? `
|
|
2143
|
+
throw new CLIError(err.error ?? `Import failed: ${res.status}`);
|
|
1736
2144
|
}
|
|
1737
|
-
const
|
|
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(
|
|
2147
|
+
outputJson(data);
|
|
1742
2148
|
} else {
|
|
1743
|
-
outputSuccess(`
|
|
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/
|
|
1752
|
-
function
|
|
1753
|
-
|
|
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
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
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
|
-
|
|
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/
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
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 (!
|
|
1784
|
-
|
|
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
|
-
|
|
1792
|
-
|
|
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
|
-
|
|
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/
|
|
1807
|
-
function
|
|
1808
|
-
|
|
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("
|
|
1819
|
-
params.set("
|
|
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/
|
|
2257
|
+
`/api/database/records/${encodeURIComponent(table)}?${params}`,
|
|
2258
|
+
{
|
|
2259
|
+
method: "PATCH",
|
|
2260
|
+
body: JSON.stringify(body)
|
|
2261
|
+
}
|
|
1824
2262
|
);
|
|
1825
|
-
const
|
|
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(
|
|
1835
|
-
} else {
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
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/
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
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
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
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
|
-
|
|
2027
|
-
|
|
2028
|
-
const
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
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
|
-
|
|
2037
|
-
s?.message(`Building and deploying... (${elapsed}s, status: ${deployment.status})`);
|
|
2331
|
+
await reportCliUsage("cli.functions.list", true);
|
|
2038
2332
|
} catch (err) {
|
|
2039
|
-
|
|
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
|
-
|
|
2047
|
-
|
|
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
|
|
2052
|
-
if (!
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
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
|
|
2059
|
-
|
|
2060
|
-
|
|
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
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
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
|
-
|
|
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 (
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
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
|
|
2084
|
-
|
|
2085
|
-
|
|
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(
|
|
2433
|
+
outputJson({ status, body: data });
|
|
2088
2434
|
} else {
|
|
2089
|
-
|
|
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
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
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/
|
|
2113
|
-
|
|
2114
|
-
function
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
}
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
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
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
}
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
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
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
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
|
-
|
|
2500
|
+
outputSuccess(`Failed to delete function "${slug}".`);
|
|
2192
2501
|
}
|
|
2193
2502
|
}
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
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
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
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(
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
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
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
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
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
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
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
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
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
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
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
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
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
await
|
|
2318
|
-
const
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
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
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
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(
|
|
2387
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
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
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
"
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
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
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
if (
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
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
|
-
|
|
2520
|
-
|
|
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
|
-
}
|
|
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(
|
|
2887
|
-
const res = await ossFetch(
|
|
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(
|
|
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);
|