@cloudgrid-io/cli 0.3.2 → 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 +292 -35
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
5
|
-
import { readFileSync as
|
|
6
|
-
import { fileURLToPath as
|
|
7
|
-
import { dirname as
|
|
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
|
|
901
|
-
import { join as
|
|
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
|
|
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: ${
|
|
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 += ` - ${
|
|
982
|
+
yaml += ` - ${join8(appDir, "services", svcName, "public")}:/usr/share/nginx/html
|
|
937
983
|
`;
|
|
938
984
|
} else {
|
|
939
985
|
yaml += ` volumes:
|
|
940
986
|
`;
|
|
941
|
-
yaml += ` - ${
|
|
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 =
|
|
1049
|
+
const lockPath = join9(appDir, LOCK_FILE);
|
|
1004
1050
|
if (!existsSync7(lockPath)) return;
|
|
1005
1051
|
try {
|
|
1006
|
-
const lock = JSON.parse(
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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);
|
|
@@ -2057,7 +2150,7 @@ var whoamiCommand = new Command21("whoami").description("Show current logged-in
|
|
|
2057
2150
|
|
|
2058
2151
|
// src/commands/support.ts
|
|
2059
2152
|
import { Command as Command22 } from "commander";
|
|
2060
|
-
import { existsSync as existsSync18, readFileSync as
|
|
2153
|
+
import { existsSync as existsSync18, readFileSync as readFileSync10 } from "fs";
|
|
2061
2154
|
import { execSync as execSync4 } from "child_process";
|
|
2062
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) => {
|
|
2063
2156
|
const lines = [];
|
|
@@ -2069,7 +2162,7 @@ var supportCommand = new Command22("support").description("Generate a troublesho
|
|
|
2069
2162
|
lines.push("Cloud Grid Support Report");
|
|
2070
2163
|
lines.push(`Generated: ${(/* @__PURE__ */ new Date()).toISOString()}`);
|
|
2071
2164
|
try {
|
|
2072
|
-
const pkg2 = JSON.parse(
|
|
2165
|
+
const pkg2 = JSON.parse(readFileSync10(new URL("../../package.json", import.meta.url), "utf-8"));
|
|
2073
2166
|
add("CLI Version", pkg2.version);
|
|
2074
2167
|
} catch {
|
|
2075
2168
|
add("CLI Version", "unknown");
|
|
@@ -2085,7 +2178,7 @@ var supportCommand = new Command22("support").description("Generate a troublesho
|
|
|
2085
2178
|
if (appName) {
|
|
2086
2179
|
add("App", appName);
|
|
2087
2180
|
if (existsSync18("cloudgrid.yaml")) {
|
|
2088
|
-
add("cloudgrid.yaml",
|
|
2181
|
+
add("cloudgrid.yaml", readFileSync10("cloudgrid.yaml", "utf-8"));
|
|
2089
2182
|
}
|
|
2090
2183
|
if (configExists()) {
|
|
2091
2184
|
const apiClient = createApiClient();
|
|
@@ -2152,10 +2245,171 @@ var supportCommand = new Command22("support").description("Generate a troublesho
|
|
|
2152
2245
|
console.log("=".repeat(50) + "\n");
|
|
2153
2246
|
});
|
|
2154
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
|
+
|
|
2155
2409
|
// src/index.ts
|
|
2156
|
-
var
|
|
2157
|
-
var pkg = JSON.parse(
|
|
2158
|
-
var program = new
|
|
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();
|
|
2159
2413
|
program.name("cloudgrid").description("Deploy apps with a single YAML file").version(pkg.version).option("-v, --verbose", "Show detailed output");
|
|
2160
2414
|
program.hook("preAction", (_thisCommand, _actionCommand) => {
|
|
2161
2415
|
const opts = program.opts();
|
|
@@ -2183,6 +2437,9 @@ program.addCommand(envCommand);
|
|
|
2183
2437
|
program.addCommand(usageCommand);
|
|
2184
2438
|
program.addCommand(whoamiCommand);
|
|
2185
2439
|
program.addCommand(supportCommand);
|
|
2440
|
+
program.addCommand(feedbackCommand);
|
|
2441
|
+
program.addCommand(rollbackCommand);
|
|
2442
|
+
program.addCommand(pullCommand);
|
|
2186
2443
|
if (process.argv.includes("--verbose") || process.argv.includes("-v")) {
|
|
2187
2444
|
setVerbose(true);
|
|
2188
2445
|
}
|