@cloudgrid-io/cli 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command as Command22 } from "commander";
5
- import { readFileSync as readFileSync9 } from "fs";
6
- import { fileURLToPath as fileURLToPath2 } from "url";
7
- import { dirname as dirname2, join as join9 } from "path";
4
+ import { Command as Command26 } from "commander";
5
+ import { readFileSync as readFileSync12 } from "fs";
6
+ import { fileURLToPath as fileURLToPath3 } from "url";
7
+ import { dirname as dirname3, join as join11 } from "path";
8
8
 
9
9
  // src/utils.ts
10
10
  import { execa } from "execa";
@@ -212,6 +212,18 @@ var CloudGridApiClient = class {
212
212
  async getUsage(name) {
213
213
  return this.request("GET", `/apps/${name}/usage`);
214
214
  }
215
+ async rollback(name) {
216
+ return this.request("POST", `/apps/${name}/rollback`);
217
+ }
218
+ async getSource(name) {
219
+ return this.request("GET", `/apps/${name}/source`);
220
+ }
221
+ async reportError(data) {
222
+ return this.request("POST", "/errors", data);
223
+ }
224
+ async submitFeedback(data) {
225
+ return this.request("POST", "/feedback", data);
226
+ }
215
227
  // ── WebSocket tunnel ────────────────────────────────────────
216
228
  /**
217
229
  * Opens a local TCP server on `localPort`. For each incoming connection,
@@ -897,16 +909,50 @@ async function interactiveWizard(name, appDir) {
897
909
 
898
910
  // src/commands/dev.ts
899
911
  import { Command as Command3 } from "commander";
900
- import { existsSync as existsSync7, writeFileSync as writeFileSync5, readFileSync as readFileSync5, unlinkSync } from "fs";
901
- import { join as join8 } from "path";
912
+ import { existsSync as existsSync7, writeFileSync as writeFileSync5, readFileSync as readFileSync6, unlinkSync } from "fs";
913
+ import { join as join9 } from "path";
902
914
  import { tmpdir } from "os";
903
915
  import { createInterface } from "readline";
904
916
  import { execa as execa2 } from "execa";
905
917
  import chalk2 from "chalk";
906
918
 
919
+ // src/services/error-reporter.ts
920
+ import { readFileSync as readFileSync5 } from "fs";
921
+ import { fileURLToPath as fileURLToPath2 } from "url";
922
+ import { dirname as dirname2, join as join7 } from "path";
923
+ var __dirname = dirname2(fileURLToPath2(import.meta.url));
924
+ function getCliVersion() {
925
+ try {
926
+ const pkg2 = JSON.parse(readFileSync5(join7(__dirname, "../../package.json"), "utf-8"));
927
+ return pkg2.version || "unknown";
928
+ } catch {
929
+ return "unknown";
930
+ }
931
+ }
932
+ async function reportError(opts) {
933
+ try {
934
+ if (!configExists()) return;
935
+ const cfg = loadConfig();
936
+ if (!cfg.jwt || !cfg.api_url) return;
937
+ const client = new CloudGridApiClient(cfg.api_url, cfg.jwt);
938
+ await client.reportError({
939
+ type: "error",
940
+ category: opts.category,
941
+ app: opts.app,
942
+ message: opts.message,
943
+ stack: opts.stack,
944
+ context: opts.context,
945
+ cli_version: getCliVersion(),
946
+ node_version: process.version,
947
+ platform: `${process.platform} ${process.arch}`
948
+ });
949
+ } catch {
950
+ }
951
+ }
952
+
907
953
  // ../shared/src/generator/docker-compose.ts
908
954
  import { writeFileSync as writeFileSync4 } from "fs";
909
- import { join as join7 } from "path";
955
+ import { join as join8 } from "path";
910
956
  function generateDockerCompose(opts) {
911
957
  const { outputPath, appDir, config, requires, sharedServices, portMap, token, apiUrl } = opts;
912
958
  const svcNames = Object.keys(config.services);
@@ -920,7 +966,7 @@ function generateDockerCompose(opts) {
920
966
  const containerPort = svc.type === "static" ? 80 : 8080;
921
967
  yaml += ` ${svcName}:
922
968
  `;
923
- yaml += ` build: ${join7(appDir, "services", svcName)}
969
+ yaml += ` build: ${join8(appDir, "services", svcName)}
924
970
  `;
925
971
  yaml += ` ports:
926
972
  `;
@@ -933,12 +979,12 @@ function generateDockerCompose(opts) {
933
979
  if (svc.type === "static") {
934
980
  yaml += ` volumes:
935
981
  `;
936
- yaml += ` - ${join7(appDir, "services", svcName, "public")}:/usr/share/nginx/html
982
+ yaml += ` - ${join8(appDir, "services", svcName, "public")}:/usr/share/nginx/html
937
983
  `;
938
984
  } else {
939
985
  yaml += ` volumes:
940
986
  `;
941
- yaml += ` - ${join7(appDir, "services", svcName, "src")}:/app/src
987
+ yaml += ` - ${join8(appDir, "services", svcName, "src")}:/app/src
942
988
  `;
943
989
  }
944
990
  yaml += ` environment:
@@ -1000,10 +1046,10 @@ var RUNNERS = {
1000
1046
  static: (port) => ({ cmd: "npx", args: ["serve", "public/", "-l", String(port)] })
1001
1047
  };
1002
1048
  function checkDevLock(appDir) {
1003
- const lockPath = join8(appDir, LOCK_FILE);
1049
+ const lockPath = join9(appDir, LOCK_FILE);
1004
1050
  if (!existsSync7(lockPath)) return;
1005
1051
  try {
1006
- const lock = JSON.parse(readFileSync5(lockPath, "utf-8"));
1052
+ const lock = JSON.parse(readFileSync6(lockPath, "utf-8"));
1007
1053
  try {
1008
1054
  process.kill(lock.pid, 0);
1009
1055
  die2(`Dev already running for this app (PID ${lock.pid}). Stop it first (Ctrl+C) or use --force`);
@@ -1018,7 +1064,7 @@ function checkDevLock(appDir) {
1018
1064
  }
1019
1065
  }
1020
1066
  function writeDevLock(appDir, ports) {
1021
- writeFileSync5(join8(appDir, LOCK_FILE), JSON.stringify({
1067
+ writeFileSync5(join9(appDir, LOCK_FILE), JSON.stringify({
1022
1068
  pid: process.pid,
1023
1069
  ports: Object.fromEntries(ports),
1024
1070
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -1026,7 +1072,7 @@ function writeDevLock(appDir, ports) {
1026
1072
  }
1027
1073
  function removeDevLock(appDir) {
1028
1074
  try {
1029
- unlinkSync(join8(appDir, LOCK_FILE));
1075
+ unlinkSync(join9(appDir, LOCK_FILE));
1030
1076
  } catch {
1031
1077
  }
1032
1078
  }
@@ -1038,9 +1084,24 @@ function prefixStream(stream, name, color) {
1038
1084
  }
1039
1085
  var devCommand = new Command3("dev").description("Start local development").option("--compose", "Use docker-compose instead of native processes").option("--force", "Ignore existing dev lock").action(async (opts) => {
1040
1086
  const appDir = process.cwd();
1041
- if (!existsSync7(join8(appDir, "cloudgrid.yaml"))) {
1087
+ if (!existsSync7(join9(appDir, "cloudgrid.yaml"))) {
1042
1088
  die2("No cloudgrid.yaml found. Run: cloudgrid create <name>");
1043
1089
  }
1090
+ let _devAppName;
1091
+ try {
1092
+ await runDev(appDir, opts);
1093
+ } catch (err) {
1094
+ reportError({
1095
+ category: "dev",
1096
+ message: err?.message || String(err),
1097
+ stack: err?.stack,
1098
+ app: _devAppName
1099
+ }).catch(() => {
1100
+ });
1101
+ throw err;
1102
+ }
1103
+ });
1104
+ async function runDev(appDir, opts) {
1044
1105
  if (!opts.force) checkDevLock(appDir);
1045
1106
  if (!configExists()) die2("No CLI config. Run: cloudgrid init");
1046
1107
  const cliConfig = loadConfig();
@@ -1116,7 +1177,7 @@ var devCommand = new Command3("dev").description("Start local development").opti
1116
1177
  process.exit(0);
1117
1178
  });
1118
1179
  if (opts.compose) {
1119
- tmpComposePath = join8(tmpdir(), `cloudgrid-${config.name}-compose.yaml`);
1180
+ tmpComposePath = join9(tmpdir(), `cloudgrid-${config.name}-compose.yaml`);
1120
1181
  log.info("Starting docker-compose...");
1121
1182
  generateDockerCompose({
1122
1183
  outputPath: tmpComposePath,
@@ -1148,7 +1209,7 @@ var devCommand = new Command3("dev").description("Start local development").opti
1148
1209
  const runMode = svc.run || "job";
1149
1210
  const color = SERVICE_COLORS[svcNames.indexOf(svcName) % SERVICE_COLORS.length];
1150
1211
  if (runMode === "job") {
1151
- const svcDir = join8(appDir, "services", svcName);
1212
+ const svcDir = join9(appDir, "services", svcName);
1152
1213
  const svcEnv = envMap.get(svcName) || {};
1153
1214
  log.info(`${color(svcName)}: Running cron job once...`);
1154
1215
  try {
@@ -1180,10 +1241,10 @@ var devCommand = new Command3("dev").description("Start local development").opti
1180
1241
  }
1181
1242
  }
1182
1243
  for (const svcName of svcNames) {
1183
- const svcDir = join8(appDir, "services", svcName);
1244
+ const svcDir = join9(appDir, "services", svcName);
1184
1245
  const svcType = config.services[svcName].type;
1185
1246
  if (svcType === "cron") continue;
1186
- if ((svcType === "node" || svcType === "nextjs") && !existsSync7(join8(svcDir, "node_modules"))) {
1247
+ if ((svcType === "node" || svcType === "nextjs") && !existsSync7(join9(svcDir, "node_modules"))) {
1187
1248
  const si = spinner(`Installing dependencies for ${svcName}...`);
1188
1249
  si.start();
1189
1250
  try {
@@ -1212,7 +1273,7 @@ var devCommand = new Command3("dev").description("Start local development").opti
1212
1273
  const svc = config.services[svcName];
1213
1274
  if (svc.type === "cron") continue;
1214
1275
  const port = portMap.get(svcName);
1215
- const svcDir = join8(appDir, "services", svcName);
1276
+ const svcDir = join9(appDir, "services", svcName);
1216
1277
  const svcEnv = envMap.get(svcName);
1217
1278
  let runner = RUNNERS[svc.type];
1218
1279
  const color = SERVICE_COLORS[i % SERVICE_COLORS.length];
@@ -1238,11 +1299,11 @@ var devCommand = new Command3("dev").description("Start local development").opti
1238
1299
  await new Promise(() => {
1239
1300
  });
1240
1301
  }
1241
- });
1302
+ }
1242
1303
 
1243
1304
  // src/commands/deploy.ts
1244
1305
  import { Command as Command4 } from "commander";
1245
- import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
1306
+ import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
1246
1307
  import { execSync } from "child_process";
1247
1308
  var deployCommand = new Command4("deploy").description("Deploy app to production").action(async () => {
1248
1309
  if (!existsSync8("cloudgrid.yaml")) die2("No cloudgrid.yaml in current directory");
@@ -1253,10 +1314,21 @@ var deployCommand = new Command4("deploy").description("Deploy app to production
1253
1314
  console.log(`
1254
1315
  Deploying ${config.name} to production...
1255
1316
  `);
1256
- await serverBuild(config, apiClient);
1317
+ try {
1318
+ await serverBuild(config, apiClient);
1319
+ } catch (err) {
1320
+ reportError({
1321
+ category: "deploy",
1322
+ message: err?.message || String(err),
1323
+ stack: err?.stack,
1324
+ app: config?.name
1325
+ }).catch(() => {
1326
+ });
1327
+ throw err;
1328
+ }
1257
1329
  });
1258
1330
  async function serverBuild(config, apiClient) {
1259
- const cloudgridYaml = readFileSync6("cloudgrid.yaml", "utf-8");
1331
+ const cloudgridYaml = readFileSync7("cloudgrid.yaml", "utf-8");
1260
1332
  const services = {};
1261
1333
  for (const [svcName, svc] of Object.entries(config.services)) {
1262
1334
  if (svc.type === "cron" && svc.run && (svc.run.startsWith("http://") || svc.run.startsWith("https://"))) {
@@ -1375,13 +1447,34 @@ var removeCommand = new Command6("remove").argument("<name>", "App name to remov
1375
1447
  // src/commands/logs.ts
1376
1448
  import { Command as Command7 } from "commander";
1377
1449
  import { existsSync as existsSync9 } from "fs";
1378
- var logsCommand = new Command7("logs").argument("[name]", "App name (auto-detected from cloudgrid.yaml if in app dir)").option("-s, --service <service>", "Filter by service name").option("-t, --tail <lines>", "Number of lines to show", "100").description("Show app logs").action(async (name, opts) => {
1450
+ var logsCommand = new Command7("logs").argument("[name]", "App name (auto-detected from cloudgrid.yaml if in app dir)").option("-s, --service <service>", "Filter by service name").option("-t, --tail <lines>", "Number of lines to show", "100").option("--build", "Show Cloud Build logs for last deploy").description("Show app logs").action(async (name, opts) => {
1379
1451
  if (!configExists()) die2("No CLI config. Run: cloudgrid init");
1380
1452
  const appName = name || (existsSync9("cloudgrid.yaml") ? loadCloudGridYaml(process.cwd()).name : null);
1381
1453
  if (!appName) {
1382
1454
  die2("Provide app name or run from app directory");
1383
1455
  }
1384
1456
  const apiClient = createApiClient(loadConfig());
1457
+ if (opts.build) {
1458
+ try {
1459
+ const { events } = await apiClient.listEvents(appName);
1460
+ const latest = events?.[0];
1461
+ if (latest?.context?.build_log_url) {
1462
+ console.log(`Cloud Build logs for last deploy:`);
1463
+ console.log(latest.context.build_log_url);
1464
+ } else if (latest?.context?.build_id) {
1465
+ console.log(`Cloud Build ID: ${latest.context.build_id}`);
1466
+ console.log(`View logs at: https://console.cloud.google.com/cloud-build/builds/${latest.context.build_id}`);
1467
+ } else {
1468
+ console.log("No build logs available.");
1469
+ }
1470
+ } catch (err) {
1471
+ if (err instanceof ApiError && err.code === "NOT_FOUND") {
1472
+ die2(`App "${appName}" not found`);
1473
+ }
1474
+ throw err;
1475
+ }
1476
+ return;
1477
+ }
1385
1478
  try {
1386
1479
  const logs = await apiClient.logs(appName, {
1387
1480
  service: opts.service,
@@ -1877,7 +1970,7 @@ adminCommand.command("revoke <email>").description("Revoke user access").action(
1877
1970
 
1878
1971
  // src/commands/secrets.ts
1879
1972
  import { Command as Command18 } from "commander";
1880
- import { existsSync as existsSync15, readFileSync as readFileSync7 } from "fs";
1973
+ import { existsSync as existsSync15, readFileSync as readFileSync8 } from "fs";
1881
1974
  var secretsCommand = new Command18("secrets").description("Manage app secrets");
1882
1975
  secretsCommand.command("set <name> [pairs...]").description("Set secrets (KEY=VALUE)").action(async (name, pairs) => {
1883
1976
  if (!configExists()) die2("Run: cloudgrid login");
@@ -1934,7 +2027,7 @@ secretsCommand.command("remove <name> <key>").description("Remove a secret").act
1934
2027
  secretsCommand.command("import <name> <file>").description("Import secrets from .env file").action(async (name, file) => {
1935
2028
  if (!configExists()) die2("Run: cloudgrid login");
1936
2029
  if (!existsSync15(file)) die2(`File not found: ${file}`);
1937
- const values = parseEnvFile(readFileSync7(file, "utf-8"));
2030
+ const values = parseEnvFile(readFileSync8(file, "utf-8"));
1938
2031
  if (Object.keys(values).length === 0) die2("No valid KEY=VALUE pairs found");
1939
2032
  try {
1940
2033
  const result = await createApiClient().setSecrets(name, values);
@@ -1946,7 +2039,7 @@ secretsCommand.command("import <name> <file>").description("Import secrets from
1946
2039
 
1947
2040
  // src/commands/env-cmd.ts
1948
2041
  import { Command as Command19 } from "commander";
1949
- import { existsSync as existsSync16, readFileSync as readFileSync8 } from "fs";
2042
+ import { existsSync as existsSync16, readFileSync as readFileSync9 } from "fs";
1950
2043
  var envCommand = new Command19("env").description("Manage app environment variables");
1951
2044
  envCommand.command("set <name> [pairs...]").description("Set env vars (KEY=VALUE)").action(async (name, pairs) => {
1952
2045
  if (!configExists()) die2("Run: cloudgrid login");
@@ -1992,7 +2085,7 @@ envCommand.command("remove <name> <key>").description("Remove an env var").actio
1992
2085
  envCommand.command("import <name> <file>").description("Import env vars from .env file").action(async (name, file) => {
1993
2086
  if (!configExists()) die2("Run: cloudgrid login");
1994
2087
  if (!existsSync16(file)) die2(`File not found: ${file}`);
1995
- const values = parseEnvFile(readFileSync8(file, "utf-8"));
2088
+ const values = parseEnvFile(readFileSync9(file, "utf-8"));
1996
2089
  if (Object.keys(values).length === 0) die2("No valid KEY=VALUE pairs found");
1997
2090
  try {
1998
2091
  const result = await createApiClient().setEnv(name, values);
@@ -2055,10 +2148,268 @@ var whoamiCommand = new Command21("whoami").description("Show current logged-in
2055
2148
  }
2056
2149
  });
2057
2150
 
2151
+ // src/commands/support.ts
2152
+ import { Command as Command22 } from "commander";
2153
+ import { existsSync as existsSync18, readFileSync as readFileSync10 } from "fs";
2154
+ import { execSync as execSync4 } from "child_process";
2155
+ var supportCommand = new Command22("support").description("Generate a troubleshoot report to share with your admin").argument("[name]", "App name (defaults to current directory)").action(async (name) => {
2156
+ const lines = [];
2157
+ const add = (section, content) => {
2158
+ lines.push(`
2159
+ === ${section} ===`);
2160
+ lines.push(content);
2161
+ };
2162
+ lines.push("Cloud Grid Support Report");
2163
+ lines.push(`Generated: ${(/* @__PURE__ */ new Date()).toISOString()}`);
2164
+ try {
2165
+ const pkg2 = JSON.parse(readFileSync10(new URL("../../package.json", import.meta.url), "utf-8"));
2166
+ add("CLI Version", pkg2.version);
2167
+ } catch {
2168
+ add("CLI Version", "unknown");
2169
+ }
2170
+ add("Node.js", process.version);
2171
+ add("Platform", `${process.platform} ${process.arch}`);
2172
+ if (configExists()) {
2173
+ add("Auth", "Logged in");
2174
+ } else {
2175
+ add("Auth", "NOT LOGGED IN");
2176
+ }
2177
+ const appName = name || (existsSync18("cloudgrid.yaml") ? loadCloudGridYaml(process.cwd()).name : null);
2178
+ if (appName) {
2179
+ add("App", appName);
2180
+ if (existsSync18("cloudgrid.yaml")) {
2181
+ add("cloudgrid.yaml", readFileSync10("cloudgrid.yaml", "utf-8"));
2182
+ }
2183
+ if (configExists()) {
2184
+ const apiClient = createApiClient();
2185
+ try {
2186
+ const status = await apiClient.request("GET", `/apps/${appName}`);
2187
+ const pods = (status.pods || []).map(
2188
+ (p) => ` ${p.name} ${p.status} ready=${p.ready} restarts=${p.restarts}`
2189
+ ).join("\n");
2190
+ add("Pods", pods || "No pods found");
2191
+ } catch (e) {
2192
+ add("Pods", `Error: ${e.message}`);
2193
+ }
2194
+ try {
2195
+ const events = await apiClient.request("GET", `/apps/${appName}/events`);
2196
+ const list = (events.events || []).slice(0, 3).map(
2197
+ (e) => ` ${e.status} ${e.sha || "N/A"} by ${e.triggered_by} (${e.source}) ${e.started_at || ""} ${e.error ? " ERROR: " + e.error : ""}`
2198
+ ).join("\n");
2199
+ add("Recent Deploys", list || "No deploy events");
2200
+ } catch {
2201
+ add("Recent Deploys", "Unable to fetch");
2202
+ }
2203
+ try {
2204
+ const logsData = await apiClient.request("GET", `/apps/${appName}/logs?tail=30`);
2205
+ const logs = typeof logsData === "string" ? logsData : logsData.logs || logsData.output || JSON.stringify(logsData);
2206
+ add("Logs (last 30 lines)", logs);
2207
+ } catch (e) {
2208
+ add("Logs", `Error: ${e.message}`);
2209
+ }
2210
+ try {
2211
+ const envData = await apiClient.request("GET", `/apps/${appName}/env`);
2212
+ const keys = (envData.env || []).map((e) => ` ${e.key}=${e.value}`).join("\n");
2213
+ add("Env Vars", keys || "None set");
2214
+ } catch {
2215
+ add("Env Vars", "Unable to fetch");
2216
+ }
2217
+ try {
2218
+ const secretsData = await apiClient.request("GET", `/apps/${appName}/secrets`);
2219
+ const keys = (secretsData.secrets || []).map((s) => ` ${s.key}`).join("\n");
2220
+ add("Secrets", keys || "None set");
2221
+ } catch {
2222
+ add("Secrets", "Unable to fetch");
2223
+ }
2224
+ try {
2225
+ const usage = await apiClient.request("GET", `/apps/${appName}/usage`);
2226
+ add("AI Usage", `Requests: ${usage.total_requests || 0}, Tokens: ${usage.total_tokens || 0}`);
2227
+ } catch {
2228
+ add("AI Usage", "Unable to fetch");
2229
+ }
2230
+ }
2231
+ } else {
2232
+ add("App", "No app detected (run from app directory or specify name)");
2233
+ }
2234
+ add("Doctor", "");
2235
+ try {
2236
+ const doctorOutput = execSync4("cloudgrid doctor 2>&1", { timeout: 15e3 }).toString();
2237
+ lines.push(doctorOutput);
2238
+ } catch (e) {
2239
+ lines.push(e.stdout?.toString() || "Doctor failed");
2240
+ }
2241
+ const report = lines.join("\n");
2242
+ console.log("\n" + report);
2243
+ console.log("\n" + "=".repeat(50));
2244
+ console.log("Copy everything above and send to your admin.");
2245
+ console.log("=".repeat(50) + "\n");
2246
+ });
2247
+
2248
+ // src/commands/feedback.ts
2249
+ import { Command as Command23 } from "commander";
2250
+ import { existsSync as existsSync19, readFileSync as readFileSync11 } from "fs";
2251
+ var feedbackCommand = new Command23("feedback").description("Send feedback or report an issue to your admin").argument("<message>", "Your feedback message").action(async (message) => {
2252
+ if (!configExists()) die2("Run: cloudgrid login");
2253
+ const apiClient = createApiClient();
2254
+ const appName = existsSync19("cloudgrid.yaml") ? loadCloudGridYaml(process.cwd()).name : null;
2255
+ const context = {};
2256
+ if (appName) {
2257
+ try {
2258
+ const status = await apiClient.request("GET", `/apps/${appName}`);
2259
+ context.pod_status = (status.pods || []).map((p) => `${p.name}: ${p.status}`).join(", ");
2260
+ } catch {
2261
+ }
2262
+ try {
2263
+ const events = await apiClient.request("GET", `/apps/${appName}/events`);
2264
+ const last = (events.events || [])[0];
2265
+ if (last) context.last_deploy_status = last.status;
2266
+ } catch {
2267
+ }
2268
+ try {
2269
+ const envData = await apiClient.request("GET", `/apps/${appName}/env`);
2270
+ context.env_keys = (envData.env || []).map((e) => e.key);
2271
+ } catch {
2272
+ }
2273
+ try {
2274
+ const secretsData = await apiClient.request("GET", `/apps/${appName}/secrets`);
2275
+ context.secrets_count = (secretsData.secrets || []).length;
2276
+ } catch {
2277
+ }
2278
+ }
2279
+ try {
2280
+ let pkg2 = {};
2281
+ try {
2282
+ pkg2 = JSON.parse(readFileSync11(new URL("../../package.json", import.meta.url), "utf-8"));
2283
+ } catch {
2284
+ }
2285
+ await apiClient.request("POST", "/feedback", {
2286
+ app: appName || "",
2287
+ message,
2288
+ cli_version: pkg2.version || "unknown",
2289
+ node_version: process.version,
2290
+ platform: `${process.platform} ${process.arch}`,
2291
+ context
2292
+ });
2293
+ log.success("Feedback sent. Thank you!");
2294
+ } catch (err) {
2295
+ die2(`Failed to send feedback: ${err.message}`);
2296
+ }
2297
+ });
2298
+
2299
+ // src/commands/rollback.ts
2300
+ import { Command as Command24 } from "commander";
2301
+ import inquirer5 from "inquirer";
2302
+ import { existsSync as existsSync20 } from "fs";
2303
+ var rollbackCommand = new Command24("rollback").argument("[name]", "App name (auto-detected from cloudgrid.yaml if in app dir)").option("-y, --yes", "Skip confirmation prompt").description("Roll back app to previous successful deploy").action(async (name, opts) => {
2304
+ if (!configExists()) die2("No CLI config. Run: cloudgrid init");
2305
+ const appName = name || (existsSync20("cloudgrid.yaml") ? loadCloudGridYaml(process.cwd()).name : null);
2306
+ if (!appName) {
2307
+ die2("Provide app name or run from app directory");
2308
+ }
2309
+ console.log("");
2310
+ console.log("WARNING: Rollback only reverts container images.");
2311
+ console.log("Config, env vars, and secrets are NOT rolled back.");
2312
+ console.log("");
2313
+ if (!opts.yes) {
2314
+ const { confirm } = await inquirer5.prompt([
2315
+ {
2316
+ type: "confirm",
2317
+ name: "confirm",
2318
+ message: `Roll back ${appName} to the previous successful deploy?`,
2319
+ default: false
2320
+ }
2321
+ ]);
2322
+ if (!confirm) {
2323
+ log("Rollback cancelled.");
2324
+ return;
2325
+ }
2326
+ }
2327
+ const apiClient = createApiClient(loadConfig());
2328
+ const s = spinner(`Rolling back ${appName}...`);
2329
+ s.start();
2330
+ try {
2331
+ const result = await apiClient.rollback(appName);
2332
+ s.succeed(`${appName} rolled back.`);
2333
+ console.log("");
2334
+ console.log(`URL: ${result.url}`);
2335
+ if (result.services?.length) {
2336
+ console.log("Services:");
2337
+ for (const svc of result.services) {
2338
+ console.log(` ${svc.name}: ${svc.image}`);
2339
+ }
2340
+ }
2341
+ } catch (err) {
2342
+ s.fail("Rollback failed");
2343
+ if (err instanceof ApiError && err.code === "NOT_FOUND") {
2344
+ die2(`App "${appName}" not found or no previous version available`);
2345
+ }
2346
+ if (err instanceof ApiError && err.code === "DEPLOY_IN_PROGRESS") {
2347
+ die2("A deploy is already in progress. Try again shortly.");
2348
+ }
2349
+ if (err instanceof ApiError) {
2350
+ die2(`API error (${err.code}): ${err.message}`);
2351
+ }
2352
+ throw err;
2353
+ }
2354
+ });
2355
+
2356
+ // src/commands/pull.ts
2357
+ import { Command as Command25 } from "commander";
2358
+ import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync6, existsSync as existsSync21, readdirSync as readdirSync2 } from "fs";
2359
+ import { execSync as execSync5 } from "child_process";
2360
+ import { join as join10 } from "path";
2361
+ var pullCommand = new Command25("pull").description("Download app source from last deploy").argument("<name>", "App name").action(async (name) => {
2362
+ if (!configExists()) die2("Run: cloudgrid login");
2363
+ const apiClient = createApiClient();
2364
+ let sourceData;
2365
+ try {
2366
+ sourceData = await apiClient.getSource(name);
2367
+ } catch (err) {
2368
+ if (err.code === "SOURCE_NOT_AVAILABLE") {
2369
+ die2("Source not available \u2014 this app was deployed before source retention was enabled.\nRedeploy the app to enable: cloudgrid deploy");
2370
+ }
2371
+ if (err.code === "SOURCE_EXPIRED") {
2372
+ die2("Source tarballs have expired (90-day retention). Redeploy to create new ones.");
2373
+ }
2374
+ die2(`Failed: ${err.message}`);
2375
+ }
2376
+ const services = sourceData.services || {};
2377
+ if (Object.keys(services).length === 0) {
2378
+ die2("No service sources found");
2379
+ }
2380
+ const appDir = join10(process.cwd(), name);
2381
+ if (existsSync21(appDir)) {
2382
+ const contents = readdirSync2(appDir);
2383
+ if (contents.length > 0) {
2384
+ log.warn(`Directory ./${name}/ already exists and is not empty. Files may be overwritten.`);
2385
+ }
2386
+ }
2387
+ mkdirSync5(appDir, { recursive: true });
2388
+ mkdirSync5(join10(appDir, "services"), { recursive: true });
2389
+ for (const [svcName, url] of Object.entries(services)) {
2390
+ const svcDir = join10(appDir, "services", svcName);
2391
+ mkdirSync5(svcDir, { recursive: true });
2392
+ log.step(`Downloading ${svcName}...`);
2393
+ const res = await fetch(url);
2394
+ if (!res.ok) die2(`Failed to download ${svcName}: ${res.status}`);
2395
+ const buffer = Buffer.from(await res.arrayBuffer());
2396
+ const tarPath = join10(svcDir, ".source.tar.gz");
2397
+ writeFileSync6(tarPath, buffer);
2398
+ execSync5(`tar -xzf .source.tar.gz --strip-components=0 --no-overwrite-dir -C .`, { cwd: svcDir });
2399
+ execSync5(`rm .source.tar.gz`, { cwd: svcDir });
2400
+ const traversalFiles = execSync5('find . -name "*..*"', { cwd: svcDir }).toString().trim();
2401
+ if (traversalFiles) {
2402
+ execSync5("rm -rf *", { cwd: svcDir });
2403
+ die2(`Security: tarball contained path traversal attempt for service '${svcName}'`);
2404
+ }
2405
+ }
2406
+ log.success(`Source downloaded to ./${name}/`);
2407
+ });
2408
+
2058
2409
  // src/index.ts
2059
- var __dirname = dirname2(fileURLToPath2(import.meta.url));
2060
- var pkg = JSON.parse(readFileSync9(join9(__dirname, "../package.json"), "utf-8"));
2061
- var program = new Command22();
2410
+ var __dirname2 = dirname3(fileURLToPath3(import.meta.url));
2411
+ var pkg = JSON.parse(readFileSync12(join11(__dirname2, "../package.json"), "utf-8"));
2412
+ var program = new Command26();
2062
2413
  program.name("cloudgrid").description("Deploy apps with a single YAML file").version(pkg.version).option("-v, --verbose", "Show detailed output");
2063
2414
  program.hook("preAction", (_thisCommand, _actionCommand) => {
2064
2415
  const opts = program.opts();
@@ -2085,6 +2436,10 @@ program.addCommand(secretsCommand);
2085
2436
  program.addCommand(envCommand);
2086
2437
  program.addCommand(usageCommand);
2087
2438
  program.addCommand(whoamiCommand);
2439
+ program.addCommand(supportCommand);
2440
+ program.addCommand(feedbackCommand);
2441
+ program.addCommand(rollbackCommand);
2442
+ program.addCommand(pullCommand);
2088
2443
  if (process.argv.includes("--verbose") || process.argv.includes("-v")) {
2089
2444
  setVerbose(true);
2090
2445
  }