@groundfloorcloud/cli 0.1.13 → 0.2.1
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 +66 -11
- package/dist/index.js +1231 -834
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -56,6 +56,7 @@ async function writeConfig(config) {
|
|
|
56
56
|
`, {
|
|
57
57
|
mode: 384
|
|
58
58
|
});
|
|
59
|
+
await fs.chmod(CONFIG_FILE, 384);
|
|
59
60
|
}
|
|
60
61
|
async function updateConfig(patch) {
|
|
61
62
|
const current = await readConfig();
|
|
@@ -394,7 +395,7 @@ var EXPIRY_SKEW_MS = 3e4;
|
|
|
394
395
|
var NotLoggedInError = class extends Error {
|
|
395
396
|
constructor() {
|
|
396
397
|
super(
|
|
397
|
-
"Not logged in. Set GROUNDFLOOR_TOKEN, or run `gf login` first."
|
|
398
|
+
"Not logged in. Set GROUNDFLOOR_TOKEN to a Groundfloor access token (e.g. from `gf m2m token`), or run `gf login` first."
|
|
398
399
|
);
|
|
399
400
|
this.name = "NotLoggedInError";
|
|
400
401
|
}
|
|
@@ -425,11 +426,26 @@ function sessionFromTokens(tokens, ctx) {
|
|
|
425
426
|
function isFresh(session) {
|
|
426
427
|
return Date.now() < session.expiresAt - EXPIRY_SKEW_MS;
|
|
427
428
|
}
|
|
429
|
+
function isExpiredJwt(token) {
|
|
430
|
+
const exp = decodeJwt(token)?.exp;
|
|
431
|
+
return typeof exp === "number" && Date.now() >= exp * 1e3 - EXPIRY_SKEW_MS;
|
|
432
|
+
}
|
|
428
433
|
async function getValidAccessToken() {
|
|
429
434
|
if (accessTokenProvider) {
|
|
430
435
|
return accessTokenProvider();
|
|
431
436
|
}
|
|
437
|
+
const envToken = process.env.GROUNDFLOOR_TOKEN?.trim();
|
|
438
|
+
let envTokenExpired = false;
|
|
439
|
+
if (envToken) {
|
|
440
|
+
if (!isExpiredJwt(envToken)) return envToken;
|
|
441
|
+
envTokenExpired = true;
|
|
442
|
+
}
|
|
432
443
|
const session = await readAuth();
|
|
444
|
+
if (envTokenExpired && (!session || !session.accessToken)) {
|
|
445
|
+
throw new Error(
|
|
446
|
+
"GROUNDFLOOR_TOKEN has expired and there is no saved `gf login` session. Mint a fresh token (e.g. `gf m2m token`) or run `gf login`."
|
|
447
|
+
);
|
|
448
|
+
}
|
|
433
449
|
if (!session || !session.accessToken) {
|
|
434
450
|
throw new NotLoggedInError();
|
|
435
451
|
}
|
|
@@ -736,6 +752,26 @@ async function getDeployment(apiUrl, workspaceId, coderunnerId) {
|
|
|
736
752
|
}
|
|
737
753
|
return await res.json();
|
|
738
754
|
}
|
|
755
|
+
async function getDeploymentById(apiUrl, workspaceId, coderunnerId, deploymentId) {
|
|
756
|
+
const token = await getValidAccessToken();
|
|
757
|
+
const res = await fetch(
|
|
758
|
+
`${apiUrl}/v1/workspaces/${encodeURIComponent(
|
|
759
|
+
workspaceId
|
|
760
|
+
)}/coderunners/${encodeURIComponent(
|
|
761
|
+
coderunnerId
|
|
762
|
+
)}/deployments/${encodeURIComponent(deploymentId)}`,
|
|
763
|
+
{
|
|
764
|
+
headers: {
|
|
765
|
+
Authorization: `Bearer ${token}`,
|
|
766
|
+
Accept: "application/json"
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
);
|
|
770
|
+
if (!res.ok) {
|
|
771
|
+
throw new CpError(res.status, await parseError(res));
|
|
772
|
+
}
|
|
773
|
+
return await res.json();
|
|
774
|
+
}
|
|
739
775
|
function deleteCoderunnerDeployment(apiUrl, workspaceId, coderunnerId, deploymentId) {
|
|
740
776
|
return cpDelete(
|
|
741
777
|
apiUrl,
|
|
@@ -781,36 +817,29 @@ function getProcessLogs(apiUrl, workspaceId, query = {}) {
|
|
|
781
817
|
`/v1/workspaces/${encodeURIComponent(workspaceId)}/logs${qs ? `?${qs}` : ""}`
|
|
782
818
|
);
|
|
783
819
|
}
|
|
784
|
-
function domainsBase(workspaceId,
|
|
820
|
+
function domainsBase(workspaceId, appId) {
|
|
785
821
|
return `/v1/workspaces/${encodeURIComponent(
|
|
786
822
|
workspaceId
|
|
787
|
-
)}/
|
|
823
|
+
)}/apps/${encodeURIComponent(appId)}/coderunner/domains`;
|
|
788
824
|
}
|
|
789
|
-
function listDomains(apiUrl, workspaceId,
|
|
790
|
-
return cpGet(
|
|
791
|
-
apiUrl,
|
|
792
|
-
domainsBase(workspaceId, coderunnerId)
|
|
793
|
-
);
|
|
825
|
+
function listDomains(apiUrl, workspaceId, appId) {
|
|
826
|
+
return cpGet(apiUrl, domainsBase(workspaceId, appId));
|
|
794
827
|
}
|
|
795
|
-
function addDomain(apiUrl, workspaceId,
|
|
796
|
-
return cpPostJson(
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
{ domain }
|
|
800
|
-
);
|
|
828
|
+
function addDomain(apiUrl, workspaceId, appId, domain) {
|
|
829
|
+
return cpPostJson(apiUrl, domainsBase(workspaceId, appId), {
|
|
830
|
+
domain
|
|
831
|
+
});
|
|
801
832
|
}
|
|
802
|
-
function removeDomain(apiUrl, workspaceId,
|
|
833
|
+
function removeDomain(apiUrl, workspaceId, appId, domain) {
|
|
803
834
|
return cpDelete(
|
|
804
835
|
apiUrl,
|
|
805
|
-
`${domainsBase(workspaceId,
|
|
836
|
+
`${domainsBase(workspaceId, appId)}/${encodeURIComponent(domain)}`
|
|
806
837
|
);
|
|
807
838
|
}
|
|
808
|
-
function verifyDomain(apiUrl, workspaceId,
|
|
839
|
+
function verifyDomain(apiUrl, workspaceId, appId, domain) {
|
|
809
840
|
return cpPostJson(
|
|
810
841
|
apiUrl,
|
|
811
|
-
`${domainsBase(workspaceId,
|
|
812
|
-
domain
|
|
813
|
-
)}/verify`,
|
|
842
|
+
`${domainsBase(workspaceId, appId)}/${encodeURIComponent(domain)}/verify`,
|
|
814
843
|
{}
|
|
815
844
|
);
|
|
816
845
|
}
|
|
@@ -1052,6 +1081,20 @@ async function controlPlaneRequest(apiUrl, method, path11, body) {
|
|
|
1052
1081
|
if (!text) return void 0;
|
|
1053
1082
|
return JSON.parse(text);
|
|
1054
1083
|
}
|
|
1084
|
+
function getAccountQuota(apiUrl, accountId) {
|
|
1085
|
+
return cpGet(
|
|
1086
|
+
apiUrl,
|
|
1087
|
+
`/v1/accounts/${encodeURIComponent(accountId)}/quota`
|
|
1088
|
+
);
|
|
1089
|
+
}
|
|
1090
|
+
function getBillingSummary(apiUrl, accountId, sinceSeconds) {
|
|
1091
|
+
return cpGet(
|
|
1092
|
+
apiUrl,
|
|
1093
|
+
`/v1/accounts/${encodeURIComponent(accountId)}/billing/summary${queryString({
|
|
1094
|
+
since_seconds: sinceSeconds ? String(sinceSeconds) : void 0
|
|
1095
|
+
})}`
|
|
1096
|
+
);
|
|
1097
|
+
}
|
|
1055
1098
|
|
|
1056
1099
|
// src/commands/whoami.ts
|
|
1057
1100
|
async function whoamiCommand() {
|
|
@@ -1139,6 +1182,14 @@ Tip: run \`gf workspaces use <id>\` to set a default.
|
|
|
1139
1182
|
}
|
|
1140
1183
|
}
|
|
1141
1184
|
async function workspacesUseCommand(workspaceId) {
|
|
1185
|
+
const cfg = await resolveConfig();
|
|
1186
|
+
const workspaces2 = await listWorkspaces(cfg.apiUrl);
|
|
1187
|
+
const match = workspaces2.find((ws) => ws.id === workspaceId);
|
|
1188
|
+
if (!match) {
|
|
1189
|
+
throw new Error(
|
|
1190
|
+
`Workspace ${workspaceId} was not found among the workspaces visible to you. Run \`gf workspaces\` to list valid ids.`
|
|
1191
|
+
);
|
|
1192
|
+
}
|
|
1142
1193
|
await updateConfig({ workspaceId });
|
|
1143
1194
|
process.stdout.write(`Default workspace set to ${workspaceId}
|
|
1144
1195
|
`);
|
|
@@ -1256,6 +1307,49 @@ async function m2mTokenCommand(opts) {
|
|
|
1256
1307
|
`);
|
|
1257
1308
|
}
|
|
1258
1309
|
|
|
1310
|
+
// src/prompt.ts
|
|
1311
|
+
import readline from "readline";
|
|
1312
|
+
function defaultPromptIO() {
|
|
1313
|
+
return {
|
|
1314
|
+
isTTY: Boolean(process.stdin.isTTY && process.stderr.isTTY),
|
|
1315
|
+
ask(question) {
|
|
1316
|
+
return new Promise((resolve2) => {
|
|
1317
|
+
const rl = readline.createInterface({
|
|
1318
|
+
input: process.stdin,
|
|
1319
|
+
output: process.stderr
|
|
1320
|
+
});
|
|
1321
|
+
rl.question(question, (answer) => {
|
|
1322
|
+
rl.close();
|
|
1323
|
+
resolve2(answer);
|
|
1324
|
+
});
|
|
1325
|
+
});
|
|
1326
|
+
}
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
function isYes(answer) {
|
|
1330
|
+
return /^(y|yes)$/i.test(answer.trim());
|
|
1331
|
+
}
|
|
1332
|
+
var AbortedError = class extends Error {
|
|
1333
|
+
constructor() {
|
|
1334
|
+
super("Aborted. Nothing was changed.");
|
|
1335
|
+
this.name = "AbortedError";
|
|
1336
|
+
}
|
|
1337
|
+
};
|
|
1338
|
+
async function confirmAction(opts) {
|
|
1339
|
+
const io = opts.io ?? defaultPromptIO();
|
|
1340
|
+
const write = opts.write ?? ((line) => process.stderr.write(`${line}
|
|
1341
|
+
`));
|
|
1342
|
+
for (const line of opts.summary) write(line);
|
|
1343
|
+
if (opts.yes) return;
|
|
1344
|
+
if (!io.isTTY) {
|
|
1345
|
+
throw new Error(
|
|
1346
|
+
`Refusing to continue without confirmation (no interactive terminal). Re-run with ${opts.bypassHint ?? "--yes"} to confirm.`
|
|
1347
|
+
);
|
|
1348
|
+
}
|
|
1349
|
+
const answer = await io.ask(`${opts.question ?? "Continue?"} [y/N] `);
|
|
1350
|
+
if (!isYes(answer)) throw new AbortedError();
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1259
1353
|
// src/commands/cancel-deployment.ts
|
|
1260
1354
|
async function cancelDeploymentCommand(opts) {
|
|
1261
1355
|
const cfg = await resolveConfig({ workspaceId: opts.workspace });
|
|
@@ -1264,14 +1358,26 @@ async function cancelDeploymentCommand(opts) {
|
|
|
1264
1358
|
"No workspace selected. Run `gf workspaces use <id>` or pass --workspace <id>."
|
|
1265
1359
|
);
|
|
1266
1360
|
}
|
|
1267
|
-
const
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
opts.coderunner
|
|
1361
|
+
const ws = cfg.workspaceId;
|
|
1362
|
+
const all = await listCoderunners(cfg.apiUrl, ws);
|
|
1363
|
+
const target = all.find(
|
|
1364
|
+
(c) => c.id === opts.coderunner || c.slug === opts.coderunner
|
|
1271
1365
|
);
|
|
1366
|
+
if (!target) {
|
|
1367
|
+
throw new Error(`Coderunner not found: ${opts.coderunner}`);
|
|
1368
|
+
}
|
|
1369
|
+
const coderunnerId = target.id;
|
|
1370
|
+
let workspaceName;
|
|
1371
|
+
try {
|
|
1372
|
+
workspaceName = (await listWorkspaces(cfg.apiUrl)).find(
|
|
1373
|
+
(item) => item.id === ws
|
|
1374
|
+
)?.name;
|
|
1375
|
+
} catch {
|
|
1376
|
+
}
|
|
1272
1377
|
let deploymentId = opts.deploymentId?.trim();
|
|
1378
|
+
const defaulted = !deploymentId;
|
|
1273
1379
|
if (!deploymentId) {
|
|
1274
|
-
const current = await getDeployment(cfg.apiUrl,
|
|
1380
|
+
const current = await getDeployment(cfg.apiUrl, ws, coderunnerId);
|
|
1275
1381
|
deploymentId = current?.id?.trim();
|
|
1276
1382
|
}
|
|
1277
1383
|
if (!deploymentId) {
|
|
@@ -1279,12 +1385,20 @@ async function cancelDeploymentCommand(opts) {
|
|
|
1279
1385
|
`No current deployment for coderunner ${coderunnerId}. Pass --deployment-id.`
|
|
1280
1386
|
);
|
|
1281
1387
|
}
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
)
|
|
1388
|
+
const label = (name, id) => name?.trim() ? `${name.trim()} (${id})` : id;
|
|
1389
|
+
await confirmAction({
|
|
1390
|
+
summary: [
|
|
1391
|
+
"This will DELETE a deployment. If it is the live one, the service goes offline.",
|
|
1392
|
+
` workspace: ${label(workspaceName, ws)}`,
|
|
1393
|
+
` coderunner: ${label(target.name ?? target.slug, coderunnerId)}`,
|
|
1394
|
+
` deployment: ${deploymentId}${defaulted ? " (the current live deployment)" : ""}`
|
|
1395
|
+
],
|
|
1396
|
+
yes: opts.yes,
|
|
1397
|
+
bypassHint: "--yes",
|
|
1398
|
+
question: "Delete this deployment?",
|
|
1399
|
+
io: opts.io
|
|
1400
|
+
});
|
|
1401
|
+
await deleteCoderunnerDeployment(cfg.apiUrl, ws, coderunnerId, deploymentId);
|
|
1288
1402
|
process.stdout.write(
|
|
1289
1403
|
`Deleted deployment ${deploymentId} for coderunner ${coderunnerId}
|
|
1290
1404
|
`
|
|
@@ -1337,10 +1451,10 @@ function statusLabel(status) {
|
|
|
1337
1451
|
function customerPortalOrigin(apiUrl) {
|
|
1338
1452
|
try {
|
|
1339
1453
|
const url = new URL(apiUrl);
|
|
1340
|
-
url.host = url.host.replace(/^platform\./, "
|
|
1454
|
+
url.host = url.host.replace(/^platform\./, "console.");
|
|
1341
1455
|
return url.origin;
|
|
1342
1456
|
} catch {
|
|
1343
|
-
return "https://
|
|
1457
|
+
return "https://console.groundfloor.cloud";
|
|
1344
1458
|
}
|
|
1345
1459
|
}
|
|
1346
1460
|
function processLogPortalUrl(portalOrigin, workspaceId, runnerId) {
|
|
@@ -1433,6 +1547,34 @@ async function diagnoseDeployFailure(opts) {
|
|
|
1433
1547
|
}
|
|
1434
1548
|
|
|
1435
1549
|
// src/commands/logs.ts
|
|
1550
|
+
var MAX_LOG_LIMIT = 1e3;
|
|
1551
|
+
var MAX_LOG_SINCE_SECONDS = 7 * 24 * 60 * 60;
|
|
1552
|
+
var DEFAULT_LOG_LIMIT = 50;
|
|
1553
|
+
var DEFAULT_LOG_SINCE_SECONDS = 3600;
|
|
1554
|
+
function parseWholeNumber(raw) {
|
|
1555
|
+
return /^\d+$/.test(raw.trim()) ? Number(raw) : Number.NaN;
|
|
1556
|
+
}
|
|
1557
|
+
function parseLogsWindow(opts) {
|
|
1558
|
+
const limit = opts.limit !== void 0 ? parseWholeNumber(opts.limit) : DEFAULT_LOG_LIMIT;
|
|
1559
|
+
const sinceSeconds = opts.sinceSeconds !== void 0 ? parseWholeNumber(opts.sinceSeconds) : DEFAULT_LOG_SINCE_SECONDS;
|
|
1560
|
+
if (!Number.isInteger(limit) || limit < 1) {
|
|
1561
|
+
throw new Error("Invalid --limit (expected a whole number from 1 to 1000).");
|
|
1562
|
+
}
|
|
1563
|
+
if (limit > MAX_LOG_LIMIT) {
|
|
1564
|
+
throw new Error(
|
|
1565
|
+
`--limit ${limit} is above the maximum of ${MAX_LOG_LIMIT} lines per request.`
|
|
1566
|
+
);
|
|
1567
|
+
}
|
|
1568
|
+
if (!Number.isInteger(sinceSeconds) || sinceSeconds < 10) {
|
|
1569
|
+
throw new Error("Invalid --since-seconds (minimum 10).");
|
|
1570
|
+
}
|
|
1571
|
+
if (sinceSeconds > MAX_LOG_SINCE_SECONDS) {
|
|
1572
|
+
throw new Error(
|
|
1573
|
+
`--since-seconds ${sinceSeconds} is above the maximum of ${MAX_LOG_SINCE_SECONDS} (7 days).`
|
|
1574
|
+
);
|
|
1575
|
+
}
|
|
1576
|
+
return { limit, sinceSeconds };
|
|
1577
|
+
}
|
|
1436
1578
|
async function logsCommand(opts) {
|
|
1437
1579
|
const cfg = await resolveConfig({ workspaceId: opts.workspace });
|
|
1438
1580
|
if (!cfg.workspaceId) {
|
|
@@ -1445,14 +1587,7 @@ async function logsCommand(opts) {
|
|
|
1445
1587
|
cfg.workspaceId,
|
|
1446
1588
|
opts.coderunner
|
|
1447
1589
|
);
|
|
1448
|
-
const limit
|
|
1449
|
-
const sinceSeconds = opts.sinceSeconds ? Number(opts.sinceSeconds) : 3600;
|
|
1450
|
-
if (!Number.isFinite(limit) || limit < 1) {
|
|
1451
|
-
throw new Error("Invalid --limit (expected a positive number).");
|
|
1452
|
-
}
|
|
1453
|
-
if (!Number.isFinite(sinceSeconds) || sinceSeconds < 10) {
|
|
1454
|
-
throw new Error("Invalid --since-seconds (minimum 10).");
|
|
1455
|
-
}
|
|
1590
|
+
const { limit, sinceSeconds } = parseLogsWindow(opts);
|
|
1456
1591
|
const page = await getProcessLogs(cfg.apiUrl, cfg.workspaceId, {
|
|
1457
1592
|
runner_id: coderunnerId,
|
|
1458
1593
|
limit,
|
|
@@ -1909,6 +2044,48 @@ function credentialEnvKeys(env) {
|
|
|
1909
2044
|
return Object.keys(env).filter((key) => isCredentialEnvKey(key)).sort();
|
|
1910
2045
|
}
|
|
1911
2046
|
|
|
2047
|
+
// src/deploy/safety.ts
|
|
2048
|
+
function workspaceSource(flag, env = process.env) {
|
|
2049
|
+
if (flag) return "flag";
|
|
2050
|
+
if (env.GROUNDFLOOR_WORKSPACE_ID) return "env";
|
|
2051
|
+
return "saved";
|
|
2052
|
+
}
|
|
2053
|
+
function labelWithId(name, id) {
|
|
2054
|
+
return name?.trim() ? `${name.trim()} (${id})` : id;
|
|
2055
|
+
}
|
|
2056
|
+
function savedDefaultRefusal(workspaceLabel) {
|
|
2057
|
+
return `Refusing to deploy to the saved default workspace ${workspaceLabel} without confirmation (no interactive terminal). The saved default is machine-wide state that another terminal can change. Pass -w <workspace-id> (or set GROUNDFLOOR_WORKSPACE_ID) to choose the target explicitly.`;
|
|
2058
|
+
}
|
|
2059
|
+
function priorEnvKeys(coderunner2) {
|
|
2060
|
+
const manifest = coderunner2.manifest;
|
|
2061
|
+
const keys = manifest?.coderunner?.deploy?.env_keys;
|
|
2062
|
+
return Array.isArray(keys) ? keys.map(String) : [];
|
|
2063
|
+
}
|
|
2064
|
+
function envKeysNotResent(priorKeys, sentKeys) {
|
|
2065
|
+
const sent = new Set(sentKeys);
|
|
2066
|
+
return priorKeys.filter((k) => !sent.has(k)).sort();
|
|
2067
|
+
}
|
|
2068
|
+
function envRedeployWarning(priorKeys, sentKeys) {
|
|
2069
|
+
const missing = envKeysNotResent(priorKeys, sentKeys);
|
|
2070
|
+
const base = 'Environment variable VALUES are not stored by Groundfloor: every deploy must resend them with -e KEY=VALUE or groundfloor.json "env".';
|
|
2071
|
+
if (priorKeys.length > 0) {
|
|
2072
|
+
if (missing.length === 0) return [];
|
|
2073
|
+
return [
|
|
2074
|
+
`WARNING: this deploy does not resend ${missing.length} variable(s) from the previous deployment: ${missing.join(", ")}.`,
|
|
2075
|
+
" The new deployment will run WITHOUT them. " + base
|
|
2076
|
+
];
|
|
2077
|
+
}
|
|
2078
|
+
if (sentKeys.length === 0) {
|
|
2079
|
+
return [
|
|
2080
|
+
"WARNING: this deploy sends no environment variables. If the running service has any, the new deployment will run WITHOUT them. " + base
|
|
2081
|
+
];
|
|
2082
|
+
}
|
|
2083
|
+
return [];
|
|
2084
|
+
}
|
|
2085
|
+
function credentialEnvMessage(keys) {
|
|
2086
|
+
return `Credential-shaped variable names cannot be passed as --env / groundfloor.json "env": ${keys.join(", ")}. Keep secrets out of deploy variables: store them with \`gf secrets set KEY\` and have the service read them at runtime with an M2M client that has the \`secrets:read\` scope (see \`gf m2m token\`). \`gf deploy --secret KEY\` (injection at deploy time) only works on platforms where secret injection is enabled.`;
|
|
2087
|
+
}
|
|
2088
|
+
|
|
1912
2089
|
// src/commands/deploy.ts
|
|
1913
2090
|
var FALLBACK_RUNTIMES = /* @__PURE__ */ new Set([
|
|
1914
2091
|
"python",
|
|
@@ -1922,6 +2099,7 @@ var VERSION_POLL_INTERVAL_MS = 2e3;
|
|
|
1922
2099
|
var VERSION_POLL_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
1923
2100
|
var DEPLOY_POLL_INTERVAL_MS = 2e3;
|
|
1924
2101
|
var DEPLOY_POLL_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
2102
|
+
var DEPLOY_TERMINAL_FAILURE = /* @__PURE__ */ new Set([4, 5, 6]);
|
|
1925
2103
|
function slugify(input) {
|
|
1926
2104
|
const slug = input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1927
2105
|
return slug || "app";
|
|
@@ -1952,7 +2130,9 @@ function reportFiltering(filtered) {
|
|
|
1952
2130
|
log(
|
|
1953
2131
|
`Excluded ${filtered.excluded.length} credential file(s): ${names(filtered.excluded)}`
|
|
1954
2132
|
);
|
|
1955
|
-
log(
|
|
2133
|
+
log(
|
|
2134
|
+
" (use `--env KEY=VALUE` for non-secret config; read secrets at runtime via an M2M client with `secrets:read`)"
|
|
2135
|
+
);
|
|
1956
2136
|
}
|
|
1957
2137
|
if (filtered.dotfilesIncluded.length) {
|
|
1958
2138
|
log(
|
|
@@ -1960,6 +2140,17 @@ function reportFiltering(filtered) {
|
|
|
1960
2140
|
);
|
|
1961
2141
|
}
|
|
1962
2142
|
}
|
|
2143
|
+
async function environmentLabel(apiUrl, workspaceId, environmentId) {
|
|
2144
|
+
if (typeof environmentId !== "string" || !environmentId) {
|
|
2145
|
+
return "(workspace default)";
|
|
2146
|
+
}
|
|
2147
|
+
try {
|
|
2148
|
+
const envs = await listWorkspaceEnvironments(apiUrl, workspaceId);
|
|
2149
|
+
return labelWithId(envs.find((e) => e.id === environmentId)?.name, environmentId);
|
|
2150
|
+
} catch {
|
|
2151
|
+
return environmentId;
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
1963
2154
|
async function failDeploy(opts) {
|
|
1964
2155
|
throw new Error(await diagnoseDeployFailure(opts));
|
|
1965
2156
|
}
|
|
@@ -1974,6 +2165,8 @@ async function deployCommand(opts) {
|
|
|
1974
2165
|
);
|
|
1975
2166
|
}
|
|
1976
2167
|
const ws = cfg.workspaceId;
|
|
2168
|
+
const source = workspaceSource(opts.workspace);
|
|
2169
|
+
const io = opts.io ?? defaultPromptIO();
|
|
1977
2170
|
let workspaceName;
|
|
1978
2171
|
try {
|
|
1979
2172
|
const workspaces2 = await listWorkspaces(cfg.apiUrl);
|
|
@@ -1988,6 +2181,19 @@ async function deployCommand(opts) {
|
|
|
1988
2181
|
apiUrl: cfg.apiUrl
|
|
1989
2182
|
})
|
|
1990
2183
|
);
|
|
2184
|
+
if (source === "saved") {
|
|
2185
|
+
const target = labelWithId(workspaceName, ws);
|
|
2186
|
+
if (!io.isTTY) {
|
|
2187
|
+
throw new Error(savedDefaultRefusal(target));
|
|
2188
|
+
}
|
|
2189
|
+
await confirmAction({
|
|
2190
|
+
summary: [
|
|
2191
|
+
`No -w/--workspace or GROUNDFLOOR_WORKSPACE_ID given: using the saved default workspace ${target}.`
|
|
2192
|
+
],
|
|
2193
|
+
question: `Deploy to ${target}?`,
|
|
2194
|
+
io
|
|
2195
|
+
});
|
|
2196
|
+
}
|
|
1991
2197
|
log(
|
|
1992
2198
|
opts.git ? `Cloning ${opts.git}${opts.ref ? ` (${opts.ref})` : ""}...` : `Packaging ${path5.resolve(opts.path ?? process.cwd())}...`
|
|
1993
2199
|
);
|
|
@@ -2040,13 +2246,88 @@ async function deployCommand(opts) {
|
|
|
2040
2246
|
flag: opts.schedule,
|
|
2041
2247
|
manifestSchedule: manifest.schedule
|
|
2042
2248
|
});
|
|
2249
|
+
const envVariable = {
|
|
2250
|
+
...manifest.env ?? {},
|
|
2251
|
+
...parseEnvPairs(opts.env)
|
|
2252
|
+
};
|
|
2253
|
+
if (!opts.skipDeploy) {
|
|
2254
|
+
const plaintextSecrets = credentialEnvKeys(envVariable);
|
|
2255
|
+
if (plaintextSecrets.length) {
|
|
2256
|
+
throw new Error(credentialEnvMessage(plaintextSecrets));
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
const injectSecretKeys = (opts.secret ?? []).map((key) => key.trim()).filter(Boolean);
|
|
2043
2260
|
let coderunnerId = opts.coderunner;
|
|
2261
|
+
if (coderunnerId && !opts.skipDeploy) {
|
|
2262
|
+
let known;
|
|
2263
|
+
try {
|
|
2264
|
+
known = (await listCoderunners(cfg.apiUrl, ws)).find(
|
|
2265
|
+
(c) => c.id === coderunnerId
|
|
2266
|
+
);
|
|
2267
|
+
} catch {
|
|
2268
|
+
}
|
|
2269
|
+
if (known) {
|
|
2270
|
+
const prior = priorEnvKeys(known);
|
|
2271
|
+
const sentKeys = Object.keys(envVariable);
|
|
2272
|
+
const warning = envRedeployWarning(prior, sentKeys);
|
|
2273
|
+
if (envKeysNotResent(prior, sentKeys).length > 0) {
|
|
2274
|
+
if (io.isTTY && !opts.yes) {
|
|
2275
|
+
await confirmAction({
|
|
2276
|
+
summary: warning,
|
|
2277
|
+
question: "Deploy anyway and clear these variables?",
|
|
2278
|
+
io
|
|
2279
|
+
});
|
|
2280
|
+
} else {
|
|
2281
|
+
for (const line of warning) log(line);
|
|
2282
|
+
}
|
|
2283
|
+
} else {
|
|
2284
|
+
for (const line of warning) log(line);
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
if (opts.appId && coderunnerId) {
|
|
2289
|
+
log(
|
|
2290
|
+
`WARNING: --app-id ${opts.appId} is IGNORED with --coderunner: it only applies when a new coderunner is created.`
|
|
2291
|
+
);
|
|
2292
|
+
}
|
|
2044
2293
|
if (!coderunnerId) {
|
|
2045
2294
|
const name = opts.name ?? manifest.name ?? (opts.git ? nameFromGitUrl(opts.git) : path5.basename(path5.resolve(opts.path ?? process.cwd())));
|
|
2046
2295
|
const slug = manifest.slug ?? slugify(name);
|
|
2047
2296
|
const existing = await listCoderunners(cfg.apiUrl, ws);
|
|
2048
2297
|
const match = existing.find((c) => c.slug === slug || c.name === name);
|
|
2049
2298
|
if (match) {
|
|
2299
|
+
const lines = [
|
|
2300
|
+
`A coderunner named "${match.name ?? match.slug ?? name}" already exists and will be UPDATED (a new version replaces the live deployment):`,
|
|
2301
|
+
` name: ${match.name ?? match.slug ?? name}`,
|
|
2302
|
+
` id: ${match.id}`,
|
|
2303
|
+
` environment: ${await environmentLabel(cfg.apiUrl, ws, match.environment_id)}`,
|
|
2304
|
+
` workspace: ${labelWithId(workspaceName, ws)}`
|
|
2305
|
+
];
|
|
2306
|
+
if (opts.appId) {
|
|
2307
|
+
lines.push(
|
|
2308
|
+
`WARNING: --app-id ${opts.appId} is IGNORED: it only applies when a new coderunner is created.`
|
|
2309
|
+
);
|
|
2310
|
+
}
|
|
2311
|
+
if (opts.workloadType && match.workload_type && opts.workloadType !== match.workload_type) {
|
|
2312
|
+
lines.push(
|
|
2313
|
+
`WARNING: --workload-type ${opts.workloadType} is ignored; the existing coderunner is ${match.workload_type}.`
|
|
2314
|
+
);
|
|
2315
|
+
}
|
|
2316
|
+
if (!opts.skipDeploy) {
|
|
2317
|
+
lines.push(
|
|
2318
|
+
...envRedeployWarning(priorEnvKeys(match), Object.keys(envVariable))
|
|
2319
|
+
);
|
|
2320
|
+
}
|
|
2321
|
+
lines.push(
|
|
2322
|
+
" (pass -c <id> to target a specific coderunner, or -n <name> to create a different one)"
|
|
2323
|
+
);
|
|
2324
|
+
await confirmAction({
|
|
2325
|
+
summary: lines,
|
|
2326
|
+
yes: opts.yes,
|
|
2327
|
+
bypassHint: "--yes",
|
|
2328
|
+
question: "Update this coderunner?",
|
|
2329
|
+
io
|
|
2330
|
+
});
|
|
2050
2331
|
coderunnerId = match.id;
|
|
2051
2332
|
log(`Using existing coderunner ${slug} (${coderunnerId})`);
|
|
2052
2333
|
} else {
|
|
@@ -2133,45 +2414,64 @@ async function deployCommand(opts) {
|
|
|
2133
2414
|
);
|
|
2134
2415
|
return;
|
|
2135
2416
|
}
|
|
2136
|
-
const envVariable = {
|
|
2137
|
-
...manifest.env ?? {},
|
|
2138
|
-
...parseEnvPairs(opts.env)
|
|
2139
|
-
};
|
|
2140
|
-
const plaintextSecrets = credentialEnvKeys(envVariable);
|
|
2141
|
-
if (plaintextSecrets.length) {
|
|
2142
|
-
throw new Error(
|
|
2143
|
-
`Do not pass secrets as --env: ${plaintextSecrets.join(", ")}. Store them with \`gf secrets set KEY\` and deploy with \`--secret KEY\`.`
|
|
2144
|
-
);
|
|
2145
|
-
}
|
|
2146
|
-
const injectSecretKeys = (opts.secret ?? []).map((key) => key.trim()).filter(Boolean);
|
|
2147
2417
|
log("Deploying...");
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2418
|
+
let result;
|
|
2419
|
+
try {
|
|
2420
|
+
result = await deployCoderunner(cfg.apiUrl, ws, coderunnerId, {
|
|
2421
|
+
version_id: uploaded.version_id,
|
|
2422
|
+
cpu: opts.cpu ?? manifest.cpu,
|
|
2423
|
+
memory: opts.memory ?? manifest.memory,
|
|
2424
|
+
envVariable: Object.keys(envVariable).length ? envVariable : void 0,
|
|
2425
|
+
schedule,
|
|
2426
|
+
inject_secret_keys: injectSecretKeys.length ? injectSecretKeys : void 0
|
|
2427
|
+
});
|
|
2428
|
+
} catch (err) {
|
|
2429
|
+
if (err instanceof CpError && err.status === 422 && injectSecretKeys.length) {
|
|
2430
|
+
throw new Error(
|
|
2431
|
+
`${err.message}
|
|
2432
|
+
--secret only works where the platform has secret injection enabled. Otherwise read the secret at runtime with an M2M client that has the \`secrets:read\` scope (see \`gf m2m token\`).`
|
|
2433
|
+
);
|
|
2434
|
+
}
|
|
2435
|
+
throw err;
|
|
2436
|
+
}
|
|
2156
2437
|
log("Waiting for deployment...");
|
|
2438
|
+
const newDeploymentId = result.deployment_id || void 0;
|
|
2439
|
+
const readNewDeployment = async () => {
|
|
2440
|
+
if (newDeploymentId) {
|
|
2441
|
+
try {
|
|
2442
|
+
return await getDeploymentById(
|
|
2443
|
+
cfg.apiUrl,
|
|
2444
|
+
ws,
|
|
2445
|
+
coderunnerId,
|
|
2446
|
+
newDeploymentId
|
|
2447
|
+
);
|
|
2448
|
+
} catch (err) {
|
|
2449
|
+
if (err instanceof CpError && err.status === 404) return null;
|
|
2450
|
+
throw err;
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
const current = await getDeployment(cfg.apiUrl, ws, coderunnerId);
|
|
2454
|
+
return current?.version_id === uploaded.version_id ? current : null;
|
|
2455
|
+
};
|
|
2157
2456
|
const deployDeadline = Date.now() + DEPLOY_POLL_TIMEOUT_MS;
|
|
2158
2457
|
let deployOk = false;
|
|
2159
2458
|
while (Date.now() < deployDeadline) {
|
|
2160
|
-
const deployment = await
|
|
2459
|
+
const deployment = await readNewDeployment();
|
|
2161
2460
|
const dStatus = deployment?.status ?? 0;
|
|
2162
2461
|
if (dStatus === 3) {
|
|
2163
2462
|
deployOk = true;
|
|
2164
2463
|
break;
|
|
2165
2464
|
}
|
|
2166
|
-
if (dStatus
|
|
2465
|
+
if (DEPLOY_TERMINAL_FAILURE.has(dStatus)) {
|
|
2466
|
+
process.stderr.write("\n");
|
|
2167
2467
|
return await failDeploy({
|
|
2168
2468
|
apiUrl: cfg.apiUrl,
|
|
2169
2469
|
workspaceId: ws,
|
|
2170
|
-
headline: `Deployment failed for coderunner ${coderunnerId} (${statusLabel(
|
|
2470
|
+
headline: `Deployment failed for coderunner ${coderunnerId} (${statusLabel(dStatus)}).`,
|
|
2171
2471
|
coderunnerId,
|
|
2172
2472
|
versionId: uploaded.version_id,
|
|
2173
|
-
deploymentId:
|
|
2174
|
-
status:
|
|
2473
|
+
deploymentId: newDeploymentId ?? deployment?.id,
|
|
2474
|
+
status: dStatus
|
|
2175
2475
|
});
|
|
2176
2476
|
}
|
|
2177
2477
|
process.stderr.write(".");
|
|
@@ -2180,9 +2480,9 @@ async function deployCommand(opts) {
|
|
|
2180
2480
|
process.stderr.write("\n");
|
|
2181
2481
|
if (!deployOk) {
|
|
2182
2482
|
let lastStatus = null;
|
|
2183
|
-
let deploymentId =
|
|
2483
|
+
let deploymentId = newDeploymentId;
|
|
2184
2484
|
try {
|
|
2185
|
-
const latest = await
|
|
2485
|
+
const latest = await readNewDeployment();
|
|
2186
2486
|
lastStatus = latest?.status ?? null;
|
|
2187
2487
|
deploymentId = latest?.id ?? deploymentId;
|
|
2188
2488
|
} catch {
|
|
@@ -2249,11 +2549,6 @@ async function runCommand(opts) {
|
|
|
2249
2549
|
coderunnerId,
|
|
2250
2550
|
payload
|
|
2251
2551
|
);
|
|
2252
|
-
if (opts.json) {
|
|
2253
|
-
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
2254
|
-
`);
|
|
2255
|
-
return;
|
|
2256
|
-
}
|
|
2257
2552
|
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
2258
2553
|
`);
|
|
2259
2554
|
}
|
|
@@ -2297,903 +2592,887 @@ async function statusCommand(opts) {
|
|
|
2297
2592
|
process.stdout.write(`deployment: ${deployment.id ?? "\u2014"} [${deployment.status}]
|
|
2298
2593
|
`);
|
|
2299
2594
|
}
|
|
2300
|
-
process.stdout.write(`
|
|
2301
|
-
${JSON.stringify(out, null, 2)}
|
|
2302
|
-
`);
|
|
2303
2595
|
}
|
|
2304
2596
|
|
|
2305
|
-
// src/commands/
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
throw new Error(
|
|
2317
|
-
"Specify the coderunner with --coderunner <id|slug>."
|
|
2318
|
-
);
|
|
2319
|
-
}
|
|
2320
|
-
let coderunnerId = ref;
|
|
2321
|
-
if (!UUID_RE.test(ref)) {
|
|
2322
|
-
const all = await listCoderunners(cfg.apiUrl, cfg.workspaceId);
|
|
2323
|
-
const match = all.find((c) => c.slug === ref || c.name === ref);
|
|
2324
|
-
if (!match) {
|
|
2325
|
-
throw new Error(`No coderunner matching "${ref}" in this workspace.`);
|
|
2326
|
-
}
|
|
2327
|
-
coderunnerId = match.id;
|
|
2328
|
-
}
|
|
2329
|
-
return {
|
|
2330
|
-
apiUrl: cfg.apiUrl,
|
|
2331
|
-
workspaceId: cfg.workspaceId,
|
|
2332
|
-
coderunnerId
|
|
2333
|
-
};
|
|
2597
|
+
// src/commands/apps.ts
|
|
2598
|
+
import { promises as fs5 } from "fs";
|
|
2599
|
+
import path6 from "path";
|
|
2600
|
+
|
|
2601
|
+
// src/release-zip.ts
|
|
2602
|
+
import AdmZip2 from "adm-zip";
|
|
2603
|
+
var REMOTE_ENTRY = "remoteEntry.js";
|
|
2604
|
+
var FLATTEN_SUFFIXES = [".js", ".mjs", ".cjs", ".css", ".map", ".wasm"];
|
|
2605
|
+
var IMPORT_ASSETS = /(__federation_import|import)\((['"])\.\/assets\//g;
|
|
2606
|
+
function rewriteRemoteEntryAssetPrefix(source) {
|
|
2607
|
+
return source.replace(IMPORT_ASSETS, "$1($2./");
|
|
2334
2608
|
}
|
|
2335
|
-
function
|
|
2336
|
-
|
|
2337
|
-
out.write(`
|
|
2338
|
-
Added ${item.domain} (status: ${item.status})
|
|
2339
|
-
`);
|
|
2340
|
-
out.write("\nCreate these DNS records at your registrar:\n");
|
|
2341
|
-
if (item.cname_target) {
|
|
2342
|
-
out.write(` CNAME ${item.domain} \u2192 ${item.cname_target}
|
|
2343
|
-
`);
|
|
2344
|
-
}
|
|
2345
|
-
if (item.verification_record) {
|
|
2346
|
-
const r = item.verification_record;
|
|
2347
|
-
out.write(` ${r.type} ${r.name} = ${r.value}
|
|
2348
|
-
`);
|
|
2349
|
-
}
|
|
2350
|
-
out.write(`
|
|
2351
|
-
Then run: gf domains verify ${item.domain} --coderunner <id|slug>
|
|
2352
|
-
`);
|
|
2609
|
+
function normalizeZipEntryName(filename) {
|
|
2610
|
+
return filename.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
2353
2611
|
}
|
|
2354
|
-
|
|
2355
|
-
const
|
|
2356
|
-
|
|
2357
|
-
if (opts.json) {
|
|
2358
|
-
process.stdout.write(`${JSON.stringify(item, null, 2)}
|
|
2359
|
-
`);
|
|
2360
|
-
return;
|
|
2361
|
-
}
|
|
2362
|
-
printInstructions(item);
|
|
2612
|
+
function parentDir(rel) {
|
|
2613
|
+
const i = rel.lastIndexOf("/");
|
|
2614
|
+
return i === -1 ? "" : rel.slice(0, i);
|
|
2363
2615
|
}
|
|
2364
|
-
|
|
2365
|
-
const
|
|
2366
|
-
|
|
2367
|
-
if (opts.json) {
|
|
2368
|
-
process.stdout.write(`${JSON.stringify(result.domains, null, 2)}
|
|
2369
|
-
`);
|
|
2370
|
-
return;
|
|
2371
|
-
}
|
|
2372
|
-
if (result.domains.length === 0) {
|
|
2373
|
-
process.stdout.write("No custom domains.\n");
|
|
2374
|
-
return;
|
|
2375
|
-
}
|
|
2376
|
-
for (const d of result.domains) {
|
|
2377
|
-
const target = d.cname_target ? ` \u2192 ${d.cname_target}` : "";
|
|
2378
|
-
process.stdout.write(`${d.domain} [${d.status}]${target}
|
|
2379
|
-
`);
|
|
2380
|
-
}
|
|
2616
|
+
function basename(rel) {
|
|
2617
|
+
const i = rel.lastIndexOf("/");
|
|
2618
|
+
return i === -1 ? rel : rel.slice(i + 1);
|
|
2381
2619
|
}
|
|
2382
|
-
|
|
2383
|
-
const
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2620
|
+
function chooseRemoteEntry(relpaths) {
|
|
2621
|
+
const entries = relpaths.filter(
|
|
2622
|
+
(p) => p === REMOTE_ENTRY || p.endsWith(`/${REMOTE_ENTRY}`)
|
|
2623
|
+
);
|
|
2624
|
+
if (entries.length === 0) return void 0;
|
|
2625
|
+
if (entries.includes(REMOTE_ENTRY)) return REMOTE_ENTRY;
|
|
2626
|
+
return [...entries].sort((a, b) => {
|
|
2627
|
+
const score = (p) => {
|
|
2628
|
+
const parts = p.split("/");
|
|
2629
|
+
const inAssets = parts.length >= 2 && parts[parts.length - 2] === "assets";
|
|
2630
|
+
return `${inAssets ? 0 : 1}:${parts.length}:${p}`;
|
|
2631
|
+
};
|
|
2632
|
+
return score(a).localeCompare(score(b));
|
|
2633
|
+
})[0];
|
|
2387
2634
|
}
|
|
2388
|
-
|
|
2389
|
-
const
|
|
2390
|
-
const
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
const
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2635
|
+
function flattenFederatedBundlePlan(relpaths) {
|
|
2636
|
+
const paths2 = relpaths.map(normalizeZipEntryName).filter((name) => name && !name.endsWith("/") && !name.split("/").includes(".."));
|
|
2637
|
+
const pathset = new Set(paths2);
|
|
2638
|
+
const entry = chooseRemoteEntry([...pathset]);
|
|
2639
|
+
if (!entry) return {};
|
|
2640
|
+
const promoteDirs = /* @__PURE__ */ new Set();
|
|
2641
|
+
const entryDir = parentDir(entry);
|
|
2642
|
+
if (entryDir) promoteDirs.add(entryDir);
|
|
2643
|
+
for (const path11 of pathset) {
|
|
2644
|
+
const parent = parentDir(path11);
|
|
2645
|
+
if (parent === "assets" || parent.endsWith("/assets") && parent.split("/").length - 1 < 2) {
|
|
2646
|
+
promoteDirs.add(parent);
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
const destToSrc = {};
|
|
2650
|
+
for (const src of paths2) {
|
|
2651
|
+
const parent = parentDir(src);
|
|
2652
|
+
if (!promoteDirs.has(parent)) continue;
|
|
2653
|
+
const dest = basename(src);
|
|
2654
|
+
if (dest === "release.zip") continue;
|
|
2655
|
+
const lower = dest.toLowerCase();
|
|
2656
|
+
if (dest !== REMOTE_ENTRY && !FLATTEN_SUFFIXES.some((ext) => lower.endsWith(ext))) {
|
|
2657
|
+
continue;
|
|
2658
|
+
}
|
|
2659
|
+
if (pathset.has(dest)) continue;
|
|
2660
|
+
const prev = destToSrc[dest];
|
|
2661
|
+
if (prev === void 0 || src.split("/").length < prev.split("/").length) {
|
|
2662
|
+
destToSrc[dest] = src;
|
|
2663
|
+
}
|
|
2664
|
+
}
|
|
2665
|
+
return destToSrc;
|
|
2666
|
+
}
|
|
2667
|
+
function flattenFederatedReleaseZip(bytes) {
|
|
2668
|
+
const zip = new AdmZip2(bytes);
|
|
2669
|
+
const rels = [];
|
|
2670
|
+
const byNorm = /* @__PURE__ */ new Map();
|
|
2671
|
+
for (const entry of zip.getEntries()) {
|
|
2672
|
+
if (entry.isDirectory) continue;
|
|
2673
|
+
const name = normalizeZipEntryName(entry.entryName);
|
|
2674
|
+
rels.push(name);
|
|
2675
|
+
byNorm.set(name, entry);
|
|
2676
|
+
}
|
|
2677
|
+
const plan = flattenFederatedBundlePlan(rels);
|
|
2678
|
+
const keys = Object.keys(plan);
|
|
2679
|
+
if (keys.length > 0) {
|
|
2680
|
+
for (const dest of keys) {
|
|
2681
|
+
const src = plan[dest];
|
|
2682
|
+
const source = byNorm.get(src);
|
|
2683
|
+
if (!source) continue;
|
|
2684
|
+
zip.addFile(dest, source.getData());
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
rewriteRemoteEntryInZip(zip);
|
|
2688
|
+
return zip.toBuffer();
|
|
2689
|
+
}
|
|
2690
|
+
function rewriteRemoteEntryInZip(zip) {
|
|
2691
|
+
const names2 = zip.getEntries().filter((entry) => !entry.isDirectory).map((entry) => normalizeZipEntryName(entry.entryName));
|
|
2692
|
+
const entryRel = chooseRemoteEntry(names2);
|
|
2693
|
+
if (!entryRel) return;
|
|
2694
|
+
const zipEntry = zip.getEntries().find((item) => {
|
|
2695
|
+
if (item.isDirectory) return false;
|
|
2696
|
+
return normalizeZipEntryName(item.entryName) === entryRel;
|
|
2697
|
+
});
|
|
2698
|
+
if (!zipEntry) return;
|
|
2699
|
+
const before = zipEntry.getData().toString("utf8");
|
|
2700
|
+
const after = rewriteRemoteEntryAssetPrefix(before);
|
|
2701
|
+
if (after === before) return;
|
|
2702
|
+
zip.updateFile(zipEntry.entryName, Buffer.from(after, "utf8"));
|
|
2703
|
+
}
|
|
2704
|
+
function federatedReleaseZipHint(slug) {
|
|
2705
|
+
return `Run \`npm run release\` in a kit from \`gf apps init\`, then publish that zip (do not zip Vite dist/ or upload only remoteEntry.js). ${initThenPublishHint(slug)}`;
|
|
2706
|
+
}
|
|
2707
|
+
function federationRemoteName(appId) {
|
|
2708
|
+
const sanitized = appId.replace(/[^a-zA-Z0-9_$]/g, "_");
|
|
2709
|
+
return sanitized.length > 0 ? sanitized : "federatedApp";
|
|
2710
|
+
}
|
|
2711
|
+
function initThenPublishHint(slug) {
|
|
2712
|
+
const s = (slug ?? "").trim() || "<portal-slug>";
|
|
2713
|
+
return `gf apps init --slug ${s} && cd ${s} && npm install && npm run release && gf apps publish --path release.zip`;
|
|
2714
|
+
}
|
|
2715
|
+
function extractReferencedSiblings(remoteEntrySource) {
|
|
2716
|
+
const names2 = /* @__PURE__ */ new Set();
|
|
2717
|
+
for (const match of remoteEntrySource.matchAll(
|
|
2718
|
+
/["']\.\/(?:assets\/)?([^"'/]+\.(?:js|mjs|cjs|css))["']/g
|
|
2719
|
+
)) {
|
|
2720
|
+
if (match[1] !== REMOTE_ENTRY) names2.add(match[1]);
|
|
2721
|
+
}
|
|
2722
|
+
for (const match of remoteEntrySource.matchAll(
|
|
2723
|
+
/["']([A-Za-z0-9._-]+\.css)["']/g
|
|
2724
|
+
)) {
|
|
2725
|
+
names2.add(match[1]);
|
|
2726
|
+
}
|
|
2727
|
+
return [...names2];
|
|
2728
|
+
}
|
|
2729
|
+
function extractFederationScope(remoteEntrySource) {
|
|
2730
|
+
return remoteEntrySource.match(/css__([A-Za-z0-9_-]+)__/)?.[1];
|
|
2731
|
+
}
|
|
2732
|
+
function zipRootNames(bytes) {
|
|
2733
|
+
const zip = new AdmZip2(bytes);
|
|
2734
|
+
return zip.getEntries().filter((entry) => !entry.isDirectory).map((entry) => normalizeZipEntryName(entry.entryName));
|
|
2735
|
+
}
|
|
2736
|
+
function readZipFile(bytes, rel) {
|
|
2737
|
+
const zip = new AdmZip2(bytes);
|
|
2738
|
+
const entry = zip.getEntries().find((item) => {
|
|
2739
|
+
if (item.isDirectory) return false;
|
|
2740
|
+
return normalizeZipEntryName(item.entryName) === rel;
|
|
2741
|
+
});
|
|
2742
|
+
return entry ? entry.getData().toString("utf8") : void 0;
|
|
2743
|
+
}
|
|
2744
|
+
function assertFederatedReleaseZip(bytes, slug) {
|
|
2745
|
+
const hint = initThenPublishHint(slug);
|
|
2746
|
+
const names2 = zipRootNames(bytes);
|
|
2747
|
+
const present = new Set(names2.map((name) => basename(name)));
|
|
2748
|
+
const entryRel = chooseRemoteEntry(names2);
|
|
2749
|
+
if (!entryRel) {
|
|
2750
|
+
throw new Error(`Zip has no remoteEntry.js. ${hint}`);
|
|
2751
|
+
}
|
|
2752
|
+
const source = readZipFile(bytes, entryRel);
|
|
2753
|
+
if (!source) {
|
|
2754
|
+
throw new Error(`Zip has no remoteEntry.js. ${hint}`);
|
|
2755
|
+
}
|
|
2756
|
+
const siblings = extractReferencedSiblings(source);
|
|
2757
|
+
const missing = siblings.filter((name) => !present.has(name));
|
|
2758
|
+
const jsOrCssAtRoot = names2.filter((name) => {
|
|
2759
|
+
if (name.includes("/")) return false;
|
|
2760
|
+
const lower = name.toLowerCase();
|
|
2761
|
+
return lower.endsWith(".js") || lower.endsWith(".mjs") || lower.endsWith(".css");
|
|
2762
|
+
});
|
|
2763
|
+
if (missing.length > 0 || siblings.length === 0 && jsOrCssAtRoot.length < 2) {
|
|
2764
|
+
const detail = missing.length > 0 ? `Missing next to remoteEntry.js: ${missing.join(", ")}.` : "Zip is only remoteEntry.js (Vite chunks were never packed).";
|
|
2765
|
+
throw new Error(
|
|
2766
|
+
`${detail} Do not zip dist/ or upload a lone remoteEntry.js. ${hint}`
|
|
2767
|
+
);
|
|
2768
|
+
}
|
|
2769
|
+
const scope = extractFederationScope(source);
|
|
2770
|
+
if (slug && scope) {
|
|
2771
|
+
const expected = federationRemoteName(slug);
|
|
2772
|
+
if (scope !== expected) {
|
|
2773
|
+
throw new Error(
|
|
2774
|
+
`Federation name is "${scope}" but Shell looks up "${expected}" (from slug ${slug}). Use the starter-kit vite.config (federationRemoteName). ${hint}`
|
|
2775
|
+
);
|
|
2776
|
+
}
|
|
2403
2777
|
}
|
|
2404
2778
|
}
|
|
2405
2779
|
|
|
2406
|
-
// src/commands/
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2780
|
+
// src/commands/apps.ts
|
|
2781
|
+
import AdmZip3 from "adm-zip";
|
|
2782
|
+
var MAX_RELEASE_BYTES = 32 * 1024 * 1024;
|
|
2783
|
+
function slugify2(input) {
|
|
2784
|
+
const slug = input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
2785
|
+
return slug || "app";
|
|
2786
|
+
}
|
|
2787
|
+
function formatApp(app) {
|
|
2788
|
+
return `${app.id} ${app.name ?? app.slug ?? ""} ${app.app_kind ?? ""} [${app.status ?? ""}]`;
|
|
2789
|
+
}
|
|
2790
|
+
async function readJsonObject(filePath) {
|
|
2791
|
+
const raw = await fs5.readFile(filePath, "utf8");
|
|
2792
|
+
const parsed = JSON.parse(raw);
|
|
2793
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2794
|
+
throw new Error(`${filePath} must contain a JSON object`);
|
|
2410
2795
|
}
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2796
|
+
return parsed;
|
|
2797
|
+
}
|
|
2798
|
+
async function maybeReadManifest(filePath) {
|
|
2799
|
+
if (!filePath) return void 0;
|
|
2800
|
+
return readJsonObject(path6.resolve(filePath));
|
|
2801
|
+
}
|
|
2802
|
+
async function findSiblingManifest(bundlePath) {
|
|
2803
|
+
const dir = await isDirectory(bundlePath) ? bundlePath : path6.dirname(bundlePath);
|
|
2804
|
+
const candidate = path6.join(dir, "groundfloor.manifest.json");
|
|
2805
|
+
try {
|
|
2806
|
+
await fs5.access(candidate);
|
|
2807
|
+
} catch {
|
|
2808
|
+
return void 0;
|
|
2809
|
+
}
|
|
2810
|
+
return { path: candidate, manifest: await readJsonObject(candidate) };
|
|
2811
|
+
}
|
|
2812
|
+
function zipHasRemoteEntry(bytes) {
|
|
2813
|
+
const zip = new AdmZip3(bytes);
|
|
2814
|
+
return zip.getEntries().some((entry) => {
|
|
2815
|
+
if (entry.isDirectory) return false;
|
|
2816
|
+
const name = normalizeZipEntryName(entry.entryName);
|
|
2817
|
+
return name === REMOTE_ENTRY || name.endsWith(`/${REMOTE_ENTRY}`);
|
|
2818
|
+
});
|
|
2819
|
+
}
|
|
2820
|
+
function prepareReleaseZip(bytes, slug) {
|
|
2821
|
+
if (!zipHasRemoteEntry(bytes)) {
|
|
2822
|
+
throw new Error(
|
|
2823
|
+
`Zip has no remoteEntry.js. ${initThenPublishHint(slug)}`
|
|
2824
|
+
);
|
|
2825
|
+
}
|
|
2826
|
+
const flattened = flattenFederatedReleaseZip(bytes);
|
|
2827
|
+
const plan = flattenFederatedBundlePlan(
|
|
2828
|
+
new AdmZip3(bytes).getEntries().filter((e) => !e.isDirectory).map((e) => normalizeZipEntryName(e.entryName))
|
|
2829
|
+
);
|
|
2830
|
+
if (Object.keys(plan).length > 0) {
|
|
2831
|
+
process.stderr.write(
|
|
2832
|
+
`Note: flattened Vite dist/assets layout so chunks sit next to remoteEntry.js.
|
|
2833
|
+
Prefer: ${federatedReleaseZipHint(slug)}
|
|
2834
|
+
`
|
|
2835
|
+
);
|
|
2836
|
+
}
|
|
2837
|
+
assertFederatedReleaseZip(flattened, slug);
|
|
2838
|
+
return flattened;
|
|
2839
|
+
}
|
|
2840
|
+
async function isDirectory(filePath) {
|
|
2841
|
+
try {
|
|
2842
|
+
return (await fs5.stat(filePath)).isDirectory();
|
|
2843
|
+
} catch {
|
|
2844
|
+
return false;
|
|
2845
|
+
}
|
|
2846
|
+
}
|
|
2847
|
+
async function resolveAppId(apiUrl, workspaceId, idOrSlug) {
|
|
2848
|
+
const listed = await listApps(apiUrl, workspaceId);
|
|
2849
|
+
const match = listed.apps.find(
|
|
2850
|
+
(app) => app.id === idOrSlug || app.slug === idOrSlug
|
|
2851
|
+
);
|
|
2852
|
+
if (match) return match;
|
|
2853
|
+
try {
|
|
2854
|
+
return await getApp(apiUrl, workspaceId, idOrSlug);
|
|
2855
|
+
} catch (err) {
|
|
2856
|
+
if (err instanceof CpError && err.status === 404) {
|
|
2857
|
+
throw new Error(
|
|
2858
|
+
`App not found: ${idOrSlug}. Run \`gf apps ls\` or \`gf apps create\`.`
|
|
2859
|
+
);
|
|
2415
2860
|
}
|
|
2416
|
-
|
|
2861
|
+
throw err;
|
|
2417
2862
|
}
|
|
2418
|
-
return value;
|
|
2419
2863
|
}
|
|
2420
|
-
async function
|
|
2864
|
+
async function appsListCommand(opts) {
|
|
2421
2865
|
const cfg = await requireWorkspace(opts.workspace);
|
|
2422
|
-
const data = await
|
|
2866
|
+
const data = await listApps(cfg.apiUrl, cfg.workspaceId);
|
|
2423
2867
|
if (opts.json) {
|
|
2424
2868
|
process.stdout.write(`${JSON.stringify(data, null, 2)}
|
|
2425
2869
|
`);
|
|
2426
2870
|
return;
|
|
2427
2871
|
}
|
|
2428
|
-
if (data.
|
|
2429
|
-
process.stdout.write("No
|
|
2872
|
+
if (data.apps.length === 0) {
|
|
2873
|
+
process.stdout.write("No apps in this workspace.\n");
|
|
2430
2874
|
return;
|
|
2431
2875
|
}
|
|
2432
|
-
for (const
|
|
2433
|
-
process.stdout.write(`${
|
|
2876
|
+
for (const app of data.apps) {
|
|
2877
|
+
process.stdout.write(`${formatApp(app)}
|
|
2434
2878
|
`);
|
|
2435
2879
|
}
|
|
2436
2880
|
}
|
|
2437
|
-
async function
|
|
2881
|
+
async function appsGetCommand(appId, opts) {
|
|
2438
2882
|
const cfg = await requireWorkspace(opts.workspace);
|
|
2439
|
-
const
|
|
2440
|
-
cfg.apiUrl,
|
|
2441
|
-
cfg.workspaceId,
|
|
2442
|
-
key,
|
|
2443
|
-
opts.environment
|
|
2444
|
-
);
|
|
2883
|
+
const app = await resolveAppId(cfg.apiUrl, cfg.workspaceId, appId);
|
|
2445
2884
|
if (opts.json) {
|
|
2446
|
-
process.stdout.write(`${JSON.stringify(
|
|
2885
|
+
process.stdout.write(`${JSON.stringify(app, null, 2)}
|
|
2447
2886
|
`);
|
|
2448
2887
|
return;
|
|
2449
2888
|
}
|
|
2450
|
-
process.stdout.write(`${
|
|
2451
|
-
`);
|
|
2452
|
-
}
|
|
2453
|
-
async function secretsSetCommand(key, value, opts) {
|
|
2454
|
-
const cfg = await requireWorkspace(opts.workspace);
|
|
2455
|
-
const resolved = await readValue(value);
|
|
2456
|
-
const saved = await putSecret(
|
|
2457
|
-
cfg.apiUrl,
|
|
2458
|
-
cfg.workspaceId,
|
|
2459
|
-
key,
|
|
2460
|
-
{ value: resolved, description: opts.description },
|
|
2461
|
-
opts.environment
|
|
2462
|
-
);
|
|
2463
|
-
process.stdout.write(`Saved secret ${saved.key}
|
|
2889
|
+
process.stdout.write(`${formatApp(app)}
|
|
2464
2890
|
`);
|
|
2465
2891
|
}
|
|
2466
|
-
async function
|
|
2892
|
+
async function appsCreateCommand(opts) {
|
|
2467
2893
|
const cfg = await requireWorkspace(opts.workspace);
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
// src/commands/flags.ts
|
|
2474
|
-
var VALUE_TYPES = [
|
|
2475
|
-
"boolean",
|
|
2476
|
-
"string",
|
|
2477
|
-
"number",
|
|
2478
|
-
"json"
|
|
2479
|
-
];
|
|
2480
|
-
var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
2481
|
-
function matchEnvironment(environments2, idOrName) {
|
|
2482
|
-
const wanted = idOrName.trim();
|
|
2483
|
-
const byId = environments2.find((env) => env.id === wanted);
|
|
2484
|
-
if (byId) return byId.id;
|
|
2485
|
-
const byName = environments2.filter(
|
|
2486
|
-
(env) => env.name.toLowerCase() === wanted.toLowerCase()
|
|
2487
|
-
);
|
|
2488
|
-
if (byName.length === 1) return byName[0].id;
|
|
2489
|
-
const names2 = environments2.map((env) => env.name).join(", ") || "none";
|
|
2490
|
-
throw new Error(
|
|
2491
|
-
`Environment "${wanted}" not found in this workspace (have: ${names2}). Run \`gf environments ls\`.`
|
|
2492
|
-
);
|
|
2493
|
-
}
|
|
2494
|
-
async function resolveEnvironment(apiUrl, workspaceId, idOrName) {
|
|
2495
|
-
if (!idOrName) return void 0;
|
|
2496
|
-
if (UUID_RE2.test(idOrName.trim())) return idOrName.trim();
|
|
2497
|
-
const environments2 = await listWorkspaceEnvironments(apiUrl, workspaceId);
|
|
2498
|
-
return matchEnvironment(environments2, idOrName);
|
|
2499
|
-
}
|
|
2500
|
-
function parseFlagValue(raw, type) {
|
|
2501
|
-
if (type !== void 0 && !VALUE_TYPES.includes(type)) {
|
|
2502
|
-
throw new Error(`--type must be one of: ${VALUE_TYPES.join(", ")}`);
|
|
2894
|
+
const kindRaw = (opts.kind ?? "shell_federated").trim();
|
|
2895
|
+
if (kindRaw === "coderunner") {
|
|
2896
|
+
throw new Error(
|
|
2897
|
+
"Do not create app_kind=coderunner. Deploy workloads with `gf deploy`. Use --kind shell_federated or --kind standalone."
|
|
2898
|
+
);
|
|
2503
2899
|
}
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2900
|
+
if (kindRaw !== "shell_federated" && kindRaw !== "standalone") {
|
|
2901
|
+
throw new Error(
|
|
2902
|
+
`--kind must be shell_federated or standalone (got ${kindRaw})`
|
|
2903
|
+
);
|
|
2904
|
+
}
|
|
2905
|
+
const kind = kindRaw;
|
|
2906
|
+
const localManifest = await maybeReadManifest(opts.manifest);
|
|
2907
|
+
const name = opts.name?.trim() || (typeof localManifest?.name === "string" ? localManifest.name : "");
|
|
2908
|
+
const slug = opts.slug?.trim() || (typeof localManifest?.appId === "string" ? localManifest.appId : name ? slugify2(name) : "");
|
|
2909
|
+
if (!name) {
|
|
2910
|
+
throw new Error("Pass --name, or a manifest with a name field.");
|
|
2911
|
+
}
|
|
2912
|
+
if (!slug) {
|
|
2913
|
+
throw new Error("Pass --slug, or a manifest with appId.");
|
|
2914
|
+
}
|
|
2915
|
+
if (kind === "standalone" && !opts.primaryCoderunner?.trim()) {
|
|
2916
|
+
throw new Error(
|
|
2917
|
+
"Standalone apps require --primary-coderunner <id> (an existing service coderunner)."
|
|
2918
|
+
);
|
|
2919
|
+
}
|
|
2920
|
+
const body = {
|
|
2921
|
+
name,
|
|
2922
|
+
slug,
|
|
2923
|
+
app_kind: kind
|
|
2924
|
+
};
|
|
2925
|
+
if (opts.environment) body.environment_id = opts.environment;
|
|
2926
|
+
if (opts.primaryCoderunner) {
|
|
2927
|
+
body.primary_coderunner_id = opts.primaryCoderunner;
|
|
2928
|
+
}
|
|
2929
|
+
if (localManifest) body.manifest = localManifest;
|
|
2930
|
+
let app;
|
|
2931
|
+
try {
|
|
2932
|
+
app = await createApp(cfg.apiUrl, cfg.workspaceId, body);
|
|
2933
|
+
} catch (err) {
|
|
2934
|
+
if (err instanceof CpError && err.status === 409) {
|
|
2935
|
+
throw new Error(
|
|
2936
|
+
`App slug ${slug} already exists. Init the kit if you do not have it, then publish:
|
|
2937
|
+
${initThenPublishHint(slug)}`
|
|
2938
|
+
);
|
|
2518
2939
|
}
|
|
2519
|
-
|
|
2520
|
-
try {
|
|
2521
|
-
return { value: JSON.parse(raw), value_type: "json" };
|
|
2522
|
-
} catch {
|
|
2523
|
-
throw new Error("--type json needs a valid JSON value");
|
|
2524
|
-
}
|
|
2525
|
-
default:
|
|
2526
|
-
return { value: raw, value_type: "string" };
|
|
2940
|
+
throw err;
|
|
2527
2941
|
}
|
|
2528
|
-
}
|
|
2529
|
-
function formatValue(value) {
|
|
2530
|
-
return typeof value === "string" ? value : JSON.stringify(value);
|
|
2531
|
-
}
|
|
2532
|
-
async function flagsListCommand(opts) {
|
|
2533
|
-
const cfg = await requireWorkspace(opts.workspace);
|
|
2534
|
-
const env = await resolveEnvironment(
|
|
2535
|
-
cfg.apiUrl,
|
|
2536
|
-
cfg.workspaceId,
|
|
2537
|
-
opts.environment
|
|
2538
|
-
);
|
|
2539
|
-
const data = await listFlags(cfg.apiUrl, cfg.workspaceId, env);
|
|
2540
2942
|
if (opts.json) {
|
|
2541
|
-
process.stdout.write(`${JSON.stringify(
|
|
2943
|
+
process.stdout.write(`${JSON.stringify(app, null, 2)}
|
|
2542
2944
|
`);
|
|
2543
2945
|
return;
|
|
2544
2946
|
}
|
|
2545
|
-
process.
|
|
2947
|
+
process.stdout.write(`Created ${formatApp(app)}
|
|
2546
2948
|
`);
|
|
2547
|
-
if (
|
|
2548
|
-
process.stdout.write("No flags in this environment.\n");
|
|
2549
|
-
return;
|
|
2550
|
-
}
|
|
2551
|
-
for (const flag of data.flags) {
|
|
2949
|
+
if (kind === "shell_federated") {
|
|
2552
2950
|
process.stdout.write(
|
|
2553
|
-
|
|
2951
|
+
`Next:
|
|
2952
|
+
gf apps init --slug ${slug}
|
|
2953
|
+
cd ${slug} && npm install && npm run release
|
|
2954
|
+
gf apps publish --path release.zip
|
|
2955
|
+
Do not scaffold src/App.tsx from scratch \u2014 BrowserRouter in App.tsx crashes the Shell.
|
|
2554
2956
|
`
|
|
2555
2957
|
);
|
|
2556
2958
|
}
|
|
2557
2959
|
}
|
|
2558
|
-
async function
|
|
2960
|
+
async function appsPublishCommand(appRef, opts) {
|
|
2559
2961
|
const cfg = await requireWorkspace(opts.workspace);
|
|
2560
|
-
const
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
return;
|
|
2962
|
+
const inputPath = path6.resolve(opts.path ?? "release.zip");
|
|
2963
|
+
const bundlePath = await isDirectory(inputPath) ? path6.join(inputPath, "release.zip") : inputPath;
|
|
2964
|
+
let bytes;
|
|
2965
|
+
try {
|
|
2966
|
+
bytes = await fs5.readFile(bundlePath);
|
|
2967
|
+
} catch {
|
|
2968
|
+
throw new Error(
|
|
2969
|
+
`No bundle at ${bundlePath}. ${initThenPublishHint()}`
|
|
2970
|
+
);
|
|
2570
2971
|
}
|
|
2571
|
-
|
|
2572
|
-
`);
|
|
2573
|
-
}
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2972
|
+
if (bytes.byteLength === 0) {
|
|
2973
|
+
throw new Error(`Bundle is empty: ${bundlePath}`);
|
|
2974
|
+
}
|
|
2975
|
+
if (bytes.byteLength > MAX_RELEASE_BYTES) {
|
|
2976
|
+
throw new Error(
|
|
2977
|
+
`Bundle exceeds ${MAX_RELEASE_BYTES} bytes (${bytes.byteLength}).`
|
|
2978
|
+
);
|
|
2979
|
+
}
|
|
2980
|
+
const sibling = await findSiblingManifest(bundlePath);
|
|
2981
|
+
const ref = appRef?.trim() || opts.app?.trim() || (typeof sibling?.manifest.appId === "string" ? sibling.manifest.appId : "");
|
|
2982
|
+
if (!ref) {
|
|
2983
|
+
throw new Error(
|
|
2984
|
+
"Pass an app id or slug, or set appId in groundfloor.manifest.json next to the bundle."
|
|
2985
|
+
);
|
|
2986
|
+
}
|
|
2987
|
+
const app = await resolveAppId(cfg.apiUrl, cfg.workspaceId, ref);
|
|
2988
|
+
if (app.app_kind && app.app_kind !== "shell_federated") {
|
|
2989
|
+
throw new Error(
|
|
2990
|
+
`Releases are only for shell_federated apps (this app is ${app.app_kind}). Use \`gf deploy\` for coderunner workloads.`
|
|
2991
|
+
);
|
|
2992
|
+
}
|
|
2993
|
+
const ext = path6.extname(bundlePath).toLowerCase();
|
|
2994
|
+
const isZip = ext === ".zip";
|
|
2995
|
+
if (!isZip) {
|
|
2996
|
+
throw new Error(
|
|
2997
|
+
`Publish release.zip from \`npm run release\`, not a lone ${path6.basename(bundlePath)}. ` + initThenPublishHint(app.slug)
|
|
2998
|
+
);
|
|
2999
|
+
}
|
|
3000
|
+
bytes = prepareReleaseZip(bytes, app.slug);
|
|
3001
|
+
const bundle = "zip";
|
|
3002
|
+
const contentType = "application/zip";
|
|
3003
|
+
if (opts.syncManifest !== false && sibling) {
|
|
3004
|
+
await patchApp(cfg.apiUrl, cfg.workspaceId, app.id, {
|
|
3005
|
+
manifest: sibling.manifest
|
|
3006
|
+
});
|
|
3007
|
+
}
|
|
3008
|
+
const init = await initAppReleaseUpload(cfg.apiUrl, cfg.workspaceId, app.id, {
|
|
3009
|
+
bundle,
|
|
3010
|
+
label: opts.label,
|
|
3011
|
+
// Required since wave 5: the server pins this into the presigned PUT and
|
|
3012
|
+
// checks it against the quota before minting a URL. Omitting it is a 422.
|
|
3013
|
+
size_bytes: bytes.byteLength
|
|
3014
|
+
});
|
|
3015
|
+
await uploadAppReleaseContent(
|
|
2577
3016
|
cfg.apiUrl,
|
|
2578
3017
|
cfg.workspaceId,
|
|
2579
|
-
|
|
3018
|
+
app.id,
|
|
3019
|
+
init.release.id,
|
|
3020
|
+
bytes,
|
|
3021
|
+
contentType
|
|
2580
3022
|
);
|
|
2581
|
-
const
|
|
2582
|
-
const saved = await putFlag(
|
|
3023
|
+
const release = await finalizeAppRelease(
|
|
2583
3024
|
cfg.apiUrl,
|
|
2584
3025
|
cfg.workspaceId,
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
env
|
|
3026
|
+
app.id,
|
|
3027
|
+
init.release.id
|
|
2588
3028
|
);
|
|
2589
3029
|
if (opts.json) {
|
|
2590
|
-
process.stdout.write(`${JSON.stringify(
|
|
3030
|
+
process.stdout.write(`${JSON.stringify({ app, release }, null, 2)}
|
|
2591
3031
|
`);
|
|
2592
3032
|
return;
|
|
2593
3033
|
}
|
|
2594
3034
|
process.stdout.write(
|
|
2595
|
-
`
|
|
2596
|
-
`
|
|
3035
|
+
`Published ${app.slug ?? app.id} build ${release.build_number}` + (release.remote_url ? ` ${release.remote_url}` : "") + "\n"
|
|
2597
3036
|
);
|
|
2598
3037
|
}
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
3038
|
+
|
|
3039
|
+
// src/commands/domains.ts
|
|
3040
|
+
var DOMAINS_APPS_ONLY = "Custom domains are available on Apps (standalone / service apps), not on standalone coderunners. Wrap the service coderunner with `gf apps create --kind standalone --primary-coderunner <id>`, then use `gf domains ... --app <id|slug>`.";
|
|
3041
|
+
async function resolve(opts) {
|
|
3042
|
+
const cfg = await resolveConfig({ workspaceId: opts.workspace });
|
|
3043
|
+
if (!cfg.workspaceId) {
|
|
3044
|
+
throw new Error(
|
|
3045
|
+
"No workspace selected. Run `gf workspaces use <id>` or pass --workspace <id>."
|
|
3046
|
+
);
|
|
3047
|
+
}
|
|
3048
|
+
if (!opts.app) {
|
|
3049
|
+
throw new Error(
|
|
3050
|
+
opts.coderunner ? `--coderunner is no longer supported for domains. ${DOMAINS_APPS_ONLY}` : "Specify the app with --app <id|slug>. " + DOMAINS_APPS_ONLY
|
|
3051
|
+
);
|
|
3052
|
+
}
|
|
3053
|
+
const app = await resolveAppId(cfg.apiUrl, cfg.workspaceId, opts.app);
|
|
3054
|
+
return { apiUrl: cfg.apiUrl, workspaceId: cfg.workspaceId, appId: app.id };
|
|
2609
3055
|
}
|
|
2610
|
-
|
|
2611
|
-
const
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
process.stdout.write(`${JSON.stringify(data, null, 2)}
|
|
3056
|
+
function printInstructions(item) {
|
|
3057
|
+
const out = process.stdout;
|
|
3058
|
+
out.write(`
|
|
3059
|
+
Added ${item.domain} (status: ${item.status})
|
|
3060
|
+
`);
|
|
3061
|
+
out.write("\nCreate these DNS records at your registrar:\n");
|
|
3062
|
+
if (item.cname_target) {
|
|
3063
|
+
out.write(` CNAME ${item.domain} \u2192 ${item.cname_target}
|
|
2619
3064
|
`);
|
|
2620
|
-
return;
|
|
2621
|
-
}
|
|
2622
|
-
if (data.keys.length === 0) {
|
|
2623
|
-
process.stdout.write("No flags in either environment.\n");
|
|
2624
|
-
return;
|
|
2625
3065
|
}
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
`
|
|
2630
|
-
);
|
|
3066
|
+
if (item.verification_record) {
|
|
3067
|
+
const r = item.verification_record;
|
|
3068
|
+
out.write(` ${r.type} ${r.name} = ${r.value}
|
|
3069
|
+
`);
|
|
2631
3070
|
}
|
|
3071
|
+
out.write(`
|
|
3072
|
+
Then run: gf domains verify ${item.domain} --app <id|slug>
|
|
3073
|
+
`);
|
|
2632
3074
|
}
|
|
2633
|
-
async function
|
|
2634
|
-
const
|
|
2635
|
-
const
|
|
2636
|
-
resolveEnvironment(cfg.apiUrl, cfg.workspaceId, source),
|
|
2637
|
-
resolveEnvironment(cfg.apiUrl, cfg.workspaceId, target)
|
|
2638
|
-
]);
|
|
2639
|
-
const keys = opts.keys?.split(",").map((k) => k.trim()).filter(Boolean);
|
|
2640
|
-
const result = await promoteFlags(cfg.apiUrl, cfg.workspaceId, {
|
|
2641
|
-
source_environment_id: src,
|
|
2642
|
-
target_environment_id: tgt,
|
|
2643
|
-
keys: keys && keys.length > 0 ? keys : void 0,
|
|
2644
|
-
mode: opts.overwrite ? "overwrite" : "skip_existing",
|
|
2645
|
-
dry_run: Boolean(opts.dryRun)
|
|
2646
|
-
});
|
|
3075
|
+
async function domainsAddCommand(domain, opts) {
|
|
3076
|
+
const { apiUrl, workspaceId, appId } = await resolve(opts);
|
|
3077
|
+
const item = await addDomain(apiUrl, workspaceId, appId, domain);
|
|
2647
3078
|
if (opts.json) {
|
|
2648
|
-
process.stdout.write(`${JSON.stringify(
|
|
3079
|
+
process.stdout.write(`${JSON.stringify(item, null, 2)}
|
|
2649
3080
|
`);
|
|
2650
3081
|
return;
|
|
2651
3082
|
}
|
|
2652
|
-
|
|
2653
|
-
process.stdout.write(
|
|
2654
|
-
`${prefix}copied: ${result.copied.join(", ") || "none"}
|
|
2655
|
-
${prefix}skipped (already set; use --overwrite): ${result.skipped.join(", ") || "none"}
|
|
2656
|
-
`
|
|
2657
|
-
);
|
|
2658
|
-
for (const failure of result.failed) {
|
|
2659
|
-
process.stdout.write(`${prefix}failed: ${failure.key} \u2014 ${failure.error}
|
|
2660
|
-
`);
|
|
2661
|
-
}
|
|
2662
|
-
if (result.failed.length > 0) {
|
|
2663
|
-
process.exitCode = 1;
|
|
2664
|
-
}
|
|
2665
|
-
}
|
|
2666
|
-
|
|
2667
|
-
// src/commands/files.ts
|
|
2668
|
-
import { promises as fs5 } from "fs";
|
|
2669
|
-
import path6 from "path";
|
|
2670
|
-
function guessContentType(filename) {
|
|
2671
|
-
const ext = path6.extname(filename).toLowerCase();
|
|
2672
|
-
const types = {
|
|
2673
|
-
".json": "application/json",
|
|
2674
|
-
".txt": "text/plain",
|
|
2675
|
-
".csv": "text/csv",
|
|
2676
|
-
".pdf": "application/pdf",
|
|
2677
|
-
".png": "image/png",
|
|
2678
|
-
".jpg": "image/jpeg",
|
|
2679
|
-
".jpeg": "image/jpeg",
|
|
2680
|
-
".zip": "application/zip"
|
|
2681
|
-
};
|
|
2682
|
-
return types[ext] ?? "application/octet-stream";
|
|
3083
|
+
printInstructions(item);
|
|
2683
3084
|
}
|
|
2684
|
-
async function
|
|
2685
|
-
const
|
|
2686
|
-
const
|
|
3085
|
+
async function domainsListCommand(opts) {
|
|
3086
|
+
const { apiUrl, workspaceId, appId } = await resolve(opts);
|
|
3087
|
+
const result = await listDomains(apiUrl, workspaceId, appId);
|
|
2687
3088
|
if (opts.json) {
|
|
2688
|
-
process.stdout.write(`${JSON.stringify(
|
|
3089
|
+
process.stdout.write(`${JSON.stringify(result.domains, null, 2)}
|
|
2689
3090
|
`);
|
|
2690
3091
|
return;
|
|
2691
3092
|
}
|
|
2692
|
-
if (
|
|
2693
|
-
process.stdout.write("No
|
|
3093
|
+
if (result.domains.length === 0) {
|
|
3094
|
+
process.stdout.write("No custom domains.\n");
|
|
2694
3095
|
return;
|
|
2695
3096
|
}
|
|
2696
|
-
for (const
|
|
2697
|
-
const
|
|
2698
|
-
process.stdout.write(
|
|
2699
|
-
|
|
2700
|
-
`
|
|
2701
|
-
);
|
|
3097
|
+
for (const d of result.domains) {
|
|
3098
|
+
const target = d.cname_target ? ` \u2192 ${d.cname_target}` : "";
|
|
3099
|
+
process.stdout.write(`${d.domain} [${d.status}]${target}
|
|
3100
|
+
`);
|
|
2702
3101
|
}
|
|
2703
3102
|
}
|
|
2704
|
-
async function
|
|
2705
|
-
const
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
const name = opts.name ?? path6.basename(resolved);
|
|
2709
|
-
const contentType = guessContentType(name);
|
|
2710
|
-
const init = await initFileUpload(cfg.apiUrl, cfg.workspaceId, {
|
|
2711
|
-
name,
|
|
2712
|
-
content_type: contentType,
|
|
2713
|
-
size_bytes: buf.byteLength
|
|
2714
|
-
});
|
|
2715
|
-
const put = await fetch(init.upload_url, {
|
|
2716
|
-
method: "PUT",
|
|
2717
|
-
headers: { "Content-Type": contentType },
|
|
2718
|
-
body: buf
|
|
2719
|
-
});
|
|
2720
|
-
if (!put.ok) {
|
|
2721
|
-
throw new Error(
|
|
2722
|
-
`Upload to object storage failed: ${put.status} ${put.statusText}`
|
|
2723
|
-
);
|
|
2724
|
-
}
|
|
2725
|
-
const file = await finalizeFileUpload(
|
|
2726
|
-
cfg.apiUrl,
|
|
2727
|
-
cfg.workspaceId,
|
|
2728
|
-
init.file.id
|
|
2729
|
-
);
|
|
2730
|
-
process.stdout.write(`Uploaded ${file.id} ${file.name ?? name}
|
|
3103
|
+
async function domainsRemoveCommand(domain, opts) {
|
|
3104
|
+
const { apiUrl, workspaceId, appId } = await resolve(opts);
|
|
3105
|
+
await removeDomain(apiUrl, workspaceId, appId, domain);
|
|
3106
|
+
process.stdout.write(`Removed ${domain}
|
|
2731
3107
|
`);
|
|
2732
3108
|
}
|
|
2733
|
-
async function
|
|
2734
|
-
const
|
|
2735
|
-
const
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
const res = await fetch(url);
|
|
2741
|
-
if (!res.ok) {
|
|
2742
|
-
throw new Error(`Download failed: ${res.status} ${res.statusText}`);
|
|
3109
|
+
async function domainsVerifyCommand(domain, opts) {
|
|
3110
|
+
const { apiUrl, workspaceId, appId } = await resolve(opts);
|
|
3111
|
+
const result = await verifyDomain(apiUrl, workspaceId, appId, domain);
|
|
3112
|
+
if (opts.json) {
|
|
3113
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
3114
|
+
`);
|
|
3115
|
+
return;
|
|
2743
3116
|
}
|
|
2744
|
-
const
|
|
2745
|
-
|
|
2746
|
-
await fs5.writeFile(dest, buf);
|
|
2747
|
-
process.stdout.write(`Wrote ${dest}
|
|
3117
|
+
const mark = result.verified ? "verified" : "not verified";
|
|
3118
|
+
process.stdout.write(`${result.domain}: ${mark} [${result.status}]
|
|
2748
3119
|
`);
|
|
2749
|
-
}
|
|
2750
|
-
async function filesRmCommand(fileId, opts) {
|
|
2751
|
-
const cfg = await requireWorkspace(opts.workspace);
|
|
2752
|
-
await deleteFile(cfg.apiUrl, cfg.workspaceId, fileId);
|
|
2753
|
-
process.stdout.write(`Deleted file ${fileId}
|
|
3120
|
+
process.stdout.write(`${result.detail}
|
|
2754
3121
|
`);
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
// src/commands/apps.ts
|
|
2758
|
-
import { promises as fs6 } from "fs";
|
|
2759
|
-
import path7 from "path";
|
|
2760
|
-
|
|
2761
|
-
// src/release-zip.ts
|
|
2762
|
-
import AdmZip2 from "adm-zip";
|
|
2763
|
-
var REMOTE_ENTRY = "remoteEntry.js";
|
|
2764
|
-
var FLATTEN_SUFFIXES = [".js", ".mjs", ".cjs", ".css", ".map", ".wasm"];
|
|
2765
|
-
var IMPORT_ASSETS = /(__federation_import|import)\((['"])\.\/assets\//g;
|
|
2766
|
-
function rewriteRemoteEntryAssetPrefix(source) {
|
|
2767
|
-
return source.replace(IMPORT_ASSETS, "$1($2./");
|
|
2768
|
-
}
|
|
2769
|
-
function normalizeZipEntryName(filename) {
|
|
2770
|
-
return filename.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
2771
|
-
}
|
|
2772
|
-
function parentDir(rel) {
|
|
2773
|
-
const i = rel.lastIndexOf("/");
|
|
2774
|
-
return i === -1 ? "" : rel.slice(0, i);
|
|
2775
|
-
}
|
|
2776
|
-
function basename(rel) {
|
|
2777
|
-
const i = rel.lastIndexOf("/");
|
|
2778
|
-
return i === -1 ? rel : rel.slice(i + 1);
|
|
2779
|
-
}
|
|
2780
|
-
function chooseRemoteEntry(relpaths) {
|
|
2781
|
-
const entries = relpaths.filter(
|
|
2782
|
-
(p) => p === REMOTE_ENTRY || p.endsWith(`/${REMOTE_ENTRY}`)
|
|
2783
|
-
);
|
|
2784
|
-
if (entries.length === 0) return void 0;
|
|
2785
|
-
if (entries.includes(REMOTE_ENTRY)) return REMOTE_ENTRY;
|
|
2786
|
-
return [...entries].sort((a, b) => {
|
|
2787
|
-
const score = (p) => {
|
|
2788
|
-
const parts = p.split("/");
|
|
2789
|
-
const inAssets = parts.length >= 2 && parts[parts.length - 2] === "assets";
|
|
2790
|
-
return `${inAssets ? 0 : 1}:${parts.length}:${p}`;
|
|
2791
|
-
};
|
|
2792
|
-
return score(a).localeCompare(score(b));
|
|
2793
|
-
})[0];
|
|
2794
|
-
}
|
|
2795
|
-
function flattenFederatedBundlePlan(relpaths) {
|
|
2796
|
-
const paths2 = relpaths.map(normalizeZipEntryName).filter((name) => name && !name.endsWith("/") && !name.split("/").includes(".."));
|
|
2797
|
-
const pathset = new Set(paths2);
|
|
2798
|
-
const entry = chooseRemoteEntry([...pathset]);
|
|
2799
|
-
if (!entry) return {};
|
|
2800
|
-
const promoteDirs = /* @__PURE__ */ new Set();
|
|
2801
|
-
const entryDir = parentDir(entry);
|
|
2802
|
-
if (entryDir) promoteDirs.add(entryDir);
|
|
2803
|
-
for (const path11 of pathset) {
|
|
2804
|
-
const parent = parentDir(path11);
|
|
2805
|
-
if (parent === "assets" || parent.endsWith("/assets") && parent.split("/").length - 1 < 2) {
|
|
2806
|
-
promoteDirs.add(parent);
|
|
2807
|
-
}
|
|
2808
|
-
}
|
|
2809
|
-
const destToSrc = {};
|
|
2810
|
-
for (const src of paths2) {
|
|
2811
|
-
const parent = parentDir(src);
|
|
2812
|
-
if (!promoteDirs.has(parent)) continue;
|
|
2813
|
-
const dest = basename(src);
|
|
2814
|
-
if (dest === "release.zip") continue;
|
|
2815
|
-
const lower = dest.toLowerCase();
|
|
2816
|
-
if (dest !== REMOTE_ENTRY && !FLATTEN_SUFFIXES.some((ext) => lower.endsWith(ext))) {
|
|
2817
|
-
continue;
|
|
2818
|
-
}
|
|
2819
|
-
if (pathset.has(dest)) continue;
|
|
2820
|
-
const prev = destToSrc[dest];
|
|
2821
|
-
if (prev === void 0 || src.split("/").length < prev.split("/").length) {
|
|
2822
|
-
destToSrc[dest] = src;
|
|
2823
|
-
}
|
|
3122
|
+
if (!result.verified) {
|
|
3123
|
+
process.exitCode = 1;
|
|
2824
3124
|
}
|
|
2825
|
-
return destToSrc;
|
|
2826
3125
|
}
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
if (entry.isDirectory) continue;
|
|
2833
|
-
const name = normalizeZipEntryName(entry.entryName);
|
|
2834
|
-
rels.push(name);
|
|
2835
|
-
byNorm.set(name, entry);
|
|
3126
|
+
|
|
3127
|
+
// src/commands/secrets.ts
|
|
3128
|
+
async function readValue(value) {
|
|
3129
|
+
if (value === void 0) {
|
|
3130
|
+
throw new Error("Pass a value, or `-` to read from stdin.");
|
|
2836
3131
|
}
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
const src = plan[dest];
|
|
2842
|
-
const source = byNorm.get(src);
|
|
2843
|
-
if (!source) continue;
|
|
2844
|
-
zip.addFile(dest, source.getData());
|
|
3132
|
+
if (value === "-") {
|
|
3133
|
+
const chunks = [];
|
|
3134
|
+
for await (const chunk of process.stdin) {
|
|
3135
|
+
chunks.push(Buffer.from(chunk));
|
|
2845
3136
|
}
|
|
3137
|
+
return Buffer.concat(chunks).toString("utf8").replace(/\n$/, "");
|
|
2846
3138
|
}
|
|
2847
|
-
|
|
2848
|
-
return zip.toBuffer();
|
|
2849
|
-
}
|
|
2850
|
-
function rewriteRemoteEntryInZip(zip) {
|
|
2851
|
-
const names2 = zip.getEntries().filter((entry) => !entry.isDirectory).map((entry) => normalizeZipEntryName(entry.entryName));
|
|
2852
|
-
const entryRel = chooseRemoteEntry(names2);
|
|
2853
|
-
if (!entryRel) return;
|
|
2854
|
-
const zipEntry = zip.getEntries().find((item) => {
|
|
2855
|
-
if (item.isDirectory) return false;
|
|
2856
|
-
return normalizeZipEntryName(item.entryName) === entryRel;
|
|
2857
|
-
});
|
|
2858
|
-
if (!zipEntry) return;
|
|
2859
|
-
const before = zipEntry.getData().toString("utf8");
|
|
2860
|
-
const after = rewriteRemoteEntryAssetPrefix(before);
|
|
2861
|
-
if (after === before) return;
|
|
2862
|
-
zip.updateFile(zipEntry.entryName, Buffer.from(after, "utf8"));
|
|
2863
|
-
}
|
|
2864
|
-
function federatedReleaseZipHint(slug) {
|
|
2865
|
-
return `Run \`npm run release\` in a kit from \`gf apps init\`, then publish that zip (do not zip Vite dist/ or upload only remoteEntry.js). ${initThenPublishHint(slug)}`;
|
|
2866
|
-
}
|
|
2867
|
-
function federationRemoteName(appId) {
|
|
2868
|
-
const sanitized = appId.replace(/[^a-zA-Z0-9_$]/g, "_");
|
|
2869
|
-
return sanitized.length > 0 ? sanitized : "federatedApp";
|
|
2870
|
-
}
|
|
2871
|
-
function initThenPublishHint(slug) {
|
|
2872
|
-
const s = (slug ?? "").trim() || "<portal-slug>";
|
|
2873
|
-
return `gf apps init --slug ${s} && cd ${s} && npm install && npm run release && gf apps publish --path release.zip`;
|
|
2874
|
-
}
|
|
2875
|
-
function extractReferencedSiblings(remoteEntrySource) {
|
|
2876
|
-
const names2 = /* @__PURE__ */ new Set();
|
|
2877
|
-
for (const match of remoteEntrySource.matchAll(
|
|
2878
|
-
/["']\.\/(?:assets\/)?([^"'/]+\.(?:js|mjs|cjs|css))["']/g
|
|
2879
|
-
)) {
|
|
2880
|
-
if (match[1] !== REMOTE_ENTRY) names2.add(match[1]);
|
|
2881
|
-
}
|
|
2882
|
-
for (const match of remoteEntrySource.matchAll(
|
|
2883
|
-
/["']([A-Za-z0-9._-]+\.css)["']/g
|
|
2884
|
-
)) {
|
|
2885
|
-
names2.add(match[1]);
|
|
2886
|
-
}
|
|
2887
|
-
return [...names2];
|
|
2888
|
-
}
|
|
2889
|
-
function extractFederationScope(remoteEntrySource) {
|
|
2890
|
-
return remoteEntrySource.match(/css__([A-Za-z0-9_-]+)__/)?.[1];
|
|
2891
|
-
}
|
|
2892
|
-
function zipRootNames(bytes) {
|
|
2893
|
-
const zip = new AdmZip2(bytes);
|
|
2894
|
-
return zip.getEntries().filter((entry) => !entry.isDirectory).map((entry) => normalizeZipEntryName(entry.entryName));
|
|
2895
|
-
}
|
|
2896
|
-
function readZipFile(bytes, rel) {
|
|
2897
|
-
const zip = new AdmZip2(bytes);
|
|
2898
|
-
const entry = zip.getEntries().find((item) => {
|
|
2899
|
-
if (item.isDirectory) return false;
|
|
2900
|
-
return normalizeZipEntryName(item.entryName) === rel;
|
|
2901
|
-
});
|
|
2902
|
-
return entry ? entry.getData().toString("utf8") : void 0;
|
|
3139
|
+
return value;
|
|
2903
3140
|
}
|
|
2904
|
-
function
|
|
2905
|
-
const
|
|
2906
|
-
const
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
}
|
|
2912
|
-
const source = readZipFile(bytes, entryRel);
|
|
2913
|
-
if (!source) {
|
|
2914
|
-
throw new Error(`Zip has no remoteEntry.js. ${hint}`);
|
|
3141
|
+
async function secretsListCommand(opts) {
|
|
3142
|
+
const cfg = await requireWorkspace(opts.workspace);
|
|
3143
|
+
const data = await listSecrets(cfg.apiUrl, cfg.workspaceId, opts.environment);
|
|
3144
|
+
if (opts.json) {
|
|
3145
|
+
process.stdout.write(`${JSON.stringify(data, null, 2)}
|
|
3146
|
+
`);
|
|
3147
|
+
return;
|
|
2915
3148
|
}
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
if (name.includes("/")) return false;
|
|
2920
|
-
const lower = name.toLowerCase();
|
|
2921
|
-
return lower.endsWith(".js") || lower.endsWith(".mjs") || lower.endsWith(".css");
|
|
2922
|
-
});
|
|
2923
|
-
if (missing.length > 0 || siblings.length === 0 && jsOrCssAtRoot.length < 2) {
|
|
2924
|
-
const detail = missing.length > 0 ? `Missing next to remoteEntry.js: ${missing.join(", ")}.` : "Zip is only remoteEntry.js (Vite chunks were never packed).";
|
|
2925
|
-
throw new Error(
|
|
2926
|
-
`${detail} Do not zip dist/ or upload a lone remoteEntry.js. ${hint}`
|
|
2927
|
-
);
|
|
3149
|
+
if (data.secrets.length === 0) {
|
|
3150
|
+
process.stdout.write("No secrets in this workspace.\n");
|
|
3151
|
+
return;
|
|
2928
3152
|
}
|
|
2929
|
-
const
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
if (scope !== expected) {
|
|
2933
|
-
throw new Error(
|
|
2934
|
-
`Federation name is "${scope}" but Shell looks up "${expected}" (from slug ${slug}). Use the starter-kit vite.config (federationRemoteName). ${hint}`
|
|
2935
|
-
);
|
|
2936
|
-
}
|
|
3153
|
+
for (const secret of data.secrets) {
|
|
3154
|
+
process.stdout.write(`${secret.key}
|
|
3155
|
+
`);
|
|
2937
3156
|
}
|
|
2938
3157
|
}
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
3158
|
+
async function secretsGetCommand(key, opts) {
|
|
3159
|
+
const cfg = await requireWorkspace(opts.workspace);
|
|
3160
|
+
const secret = await getSecret(
|
|
3161
|
+
cfg.apiUrl,
|
|
3162
|
+
cfg.workspaceId,
|
|
3163
|
+
key,
|
|
3164
|
+
opts.environment
|
|
3165
|
+
);
|
|
3166
|
+
if (opts.json) {
|
|
3167
|
+
process.stdout.write(`${JSON.stringify(secret, null, 2)}
|
|
3168
|
+
`);
|
|
3169
|
+
return;
|
|
3170
|
+
}
|
|
3171
|
+
process.stdout.write(`${secret.value}
|
|
3172
|
+
`);
|
|
2946
3173
|
}
|
|
2947
|
-
function
|
|
2948
|
-
|
|
3174
|
+
async function secretsSetCommand(key, value, opts) {
|
|
3175
|
+
const cfg = await requireWorkspace(opts.workspace);
|
|
3176
|
+
const resolved = await readValue(value);
|
|
3177
|
+
const saved = await putSecret(
|
|
3178
|
+
cfg.apiUrl,
|
|
3179
|
+
cfg.workspaceId,
|
|
3180
|
+
key,
|
|
3181
|
+
{ value: resolved, description: opts.description },
|
|
3182
|
+
opts.environment
|
|
3183
|
+
);
|
|
3184
|
+
process.stdout.write(`Saved secret ${saved.key}
|
|
3185
|
+
`);
|
|
2949
3186
|
}
|
|
2950
|
-
async function
|
|
2951
|
-
const
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
}
|
|
2956
|
-
return parsed;
|
|
3187
|
+
async function secretsRmCommand(key, opts) {
|
|
3188
|
+
const cfg = await requireWorkspace(opts.workspace);
|
|
3189
|
+
await deleteSecret(cfg.apiUrl, cfg.workspaceId, key, opts.environment);
|
|
3190
|
+
process.stdout.write(`Deleted secret ${key}
|
|
3191
|
+
`);
|
|
2957
3192
|
}
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
3193
|
+
|
|
3194
|
+
// src/commands/flags.ts
|
|
3195
|
+
var VALUE_TYPES = [
|
|
3196
|
+
"boolean",
|
|
3197
|
+
"string",
|
|
3198
|
+
"number",
|
|
3199
|
+
"json"
|
|
3200
|
+
];
|
|
3201
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
3202
|
+
function matchEnvironment(environments2, idOrName) {
|
|
3203
|
+
const wanted = idOrName.trim();
|
|
3204
|
+
const byId = environments2.find((env) => env.id === wanted);
|
|
3205
|
+
if (byId) return byId.id;
|
|
3206
|
+
const byName = environments2.filter(
|
|
3207
|
+
(env) => env.name.toLowerCase() === wanted.toLowerCase()
|
|
3208
|
+
);
|
|
3209
|
+
if (byName.length === 1) return byName[0].id;
|
|
3210
|
+
const names2 = environments2.map((env) => env.name).join(", ") || "none";
|
|
3211
|
+
throw new Error(
|
|
3212
|
+
`Environment "${wanted}" not found in this workspace (have: ${names2}). Run \`gf environments ls\`.`
|
|
3213
|
+
);
|
|
2961
3214
|
}
|
|
2962
|
-
async function
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
3215
|
+
async function resolveEnvironment(apiUrl, workspaceId, idOrName) {
|
|
3216
|
+
if (!idOrName) return void 0;
|
|
3217
|
+
if (UUID_RE.test(idOrName.trim())) return idOrName.trim();
|
|
3218
|
+
const environments2 = await listWorkspaceEnvironments(apiUrl, workspaceId);
|
|
3219
|
+
return matchEnvironment(environments2, idOrName);
|
|
3220
|
+
}
|
|
3221
|
+
function parseFlagValue(raw, type) {
|
|
3222
|
+
if (type !== void 0 && !VALUE_TYPES.includes(type)) {
|
|
3223
|
+
throw new Error(`--type must be one of: ${VALUE_TYPES.join(", ")}`);
|
|
3224
|
+
}
|
|
3225
|
+
const lower = raw.trim().toLowerCase();
|
|
3226
|
+
const valueType = type ?? (lower === "true" || lower === "false" ? "boolean" : raw.trim() !== "" && Number.isFinite(Number(raw)) ? "number" : "string");
|
|
3227
|
+
switch (valueType) {
|
|
3228
|
+
case "boolean":
|
|
3229
|
+
if (lower !== "true" && lower !== "false") {
|
|
3230
|
+
throw new Error("boolean flags take true or false");
|
|
3231
|
+
}
|
|
3232
|
+
return { value: lower === "true", value_type: "boolean" };
|
|
3233
|
+
case "number": {
|
|
3234
|
+
const n = Number(raw);
|
|
3235
|
+
if (raw.trim() === "" || !Number.isFinite(n)) {
|
|
3236
|
+
throw new Error(`"${raw}" is not a number`);
|
|
3237
|
+
}
|
|
3238
|
+
return { value: n, value_type: "number" };
|
|
3239
|
+
}
|
|
3240
|
+
case "json":
|
|
3241
|
+
try {
|
|
3242
|
+
return { value: JSON.parse(raw), value_type: "json" };
|
|
3243
|
+
} catch {
|
|
3244
|
+
throw new Error("--type json needs a valid JSON value");
|
|
3245
|
+
}
|
|
3246
|
+
default:
|
|
3247
|
+
return { value: raw, value_type: "string" };
|
|
2969
3248
|
}
|
|
2970
|
-
return { path: candidate, manifest: await readJsonObject(candidate) };
|
|
2971
3249
|
}
|
|
2972
|
-
function
|
|
2973
|
-
|
|
2974
|
-
return zip.getEntries().some((entry) => {
|
|
2975
|
-
if (entry.isDirectory) return false;
|
|
2976
|
-
const name = normalizeZipEntryName(entry.entryName);
|
|
2977
|
-
return name === REMOTE_ENTRY || name.endsWith(`/${REMOTE_ENTRY}`);
|
|
2978
|
-
});
|
|
3250
|
+
function formatValue(value) {
|
|
3251
|
+
return typeof value === "string" ? value : JSON.stringify(value);
|
|
2979
3252
|
}
|
|
2980
|
-
function
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
const flattened = flattenFederatedReleaseZip(bytes);
|
|
2987
|
-
const plan = flattenFederatedBundlePlan(
|
|
2988
|
-
new AdmZip3(bytes).getEntries().filter((e) => !e.isDirectory).map((e) => normalizeZipEntryName(e.entryName))
|
|
3253
|
+
async function flagsListCommand(opts) {
|
|
3254
|
+
const cfg = await requireWorkspace(opts.workspace);
|
|
3255
|
+
const env = await resolveEnvironment(
|
|
3256
|
+
cfg.apiUrl,
|
|
3257
|
+
cfg.workspaceId,
|
|
3258
|
+
opts.environment
|
|
2989
3259
|
);
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
3260
|
+
const data = await listFlags(cfg.apiUrl, cfg.workspaceId, env);
|
|
3261
|
+
if (opts.json) {
|
|
3262
|
+
process.stdout.write(`${JSON.stringify(data, null, 2)}
|
|
3263
|
+
`);
|
|
3264
|
+
return;
|
|
3265
|
+
}
|
|
3266
|
+
process.stderr.write(`environment ${data.environment_id}
|
|
3267
|
+
`);
|
|
3268
|
+
if (data.flags.length === 0) {
|
|
3269
|
+
process.stdout.write("No flags in this environment.\n");
|
|
3270
|
+
return;
|
|
3271
|
+
}
|
|
3272
|
+
for (const flag of data.flags) {
|
|
3273
|
+
process.stdout.write(
|
|
3274
|
+
`${flag.key} ${flag.value_type} ${formatValue(flag.value)}
|
|
2994
3275
|
`
|
|
2995
3276
|
);
|
|
2996
3277
|
}
|
|
2997
|
-
assertFederatedReleaseZip(flattened, slug);
|
|
2998
|
-
return flattened;
|
|
2999
3278
|
}
|
|
3000
|
-
async function
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3279
|
+
async function flagsGetCommand(key, opts) {
|
|
3280
|
+
const cfg = await requireWorkspace(opts.workspace);
|
|
3281
|
+
const env = await resolveEnvironment(
|
|
3282
|
+
cfg.apiUrl,
|
|
3283
|
+
cfg.workspaceId,
|
|
3284
|
+
opts.environment
|
|
3285
|
+
);
|
|
3286
|
+
const flag = await getFlag(cfg.apiUrl, cfg.workspaceId, key, env);
|
|
3287
|
+
if (opts.json) {
|
|
3288
|
+
process.stdout.write(`${JSON.stringify(flag, null, 2)}
|
|
3289
|
+
`);
|
|
3290
|
+
return;
|
|
3005
3291
|
}
|
|
3292
|
+
process.stdout.write(`${formatValue(flag.value)}
|
|
3293
|
+
`);
|
|
3006
3294
|
}
|
|
3007
|
-
async function
|
|
3008
|
-
const
|
|
3009
|
-
const
|
|
3010
|
-
|
|
3295
|
+
async function flagsSetCommand(key, value, opts) {
|
|
3296
|
+
const cfg = await requireWorkspace(opts.workspace);
|
|
3297
|
+
const env = await resolveEnvironment(
|
|
3298
|
+
cfg.apiUrl,
|
|
3299
|
+
cfg.workspaceId,
|
|
3300
|
+
opts.environment
|
|
3011
3301
|
);
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3302
|
+
const parsed = parseFlagValue(value, opts.type);
|
|
3303
|
+
const saved = await putFlag(
|
|
3304
|
+
cfg.apiUrl,
|
|
3305
|
+
cfg.workspaceId,
|
|
3306
|
+
key,
|
|
3307
|
+
{ ...parsed, description: opts.description },
|
|
3308
|
+
env
|
|
3309
|
+
);
|
|
3310
|
+
if (opts.json) {
|
|
3311
|
+
process.stdout.write(`${JSON.stringify(saved, null, 2)}
|
|
3312
|
+
`);
|
|
3313
|
+
return;
|
|
3022
3314
|
}
|
|
3315
|
+
process.stdout.write(
|
|
3316
|
+
`Set ${saved.key} = ${formatValue(saved.value)} (${saved.value_type}) in environment ${saved.environment_id}
|
|
3317
|
+
`
|
|
3318
|
+
);
|
|
3023
3319
|
}
|
|
3024
|
-
async function
|
|
3320
|
+
async function flagsRmCommand(key, opts) {
|
|
3025
3321
|
const cfg = await requireWorkspace(opts.workspace);
|
|
3026
|
-
const
|
|
3322
|
+
const env = await resolveEnvironment(
|
|
3323
|
+
cfg.apiUrl,
|
|
3324
|
+
cfg.workspaceId,
|
|
3325
|
+
opts.environment
|
|
3326
|
+
);
|
|
3327
|
+
await deleteFlag(cfg.apiUrl, cfg.workspaceId, key, env);
|
|
3328
|
+
process.stdout.write(`Deleted flag ${key}
|
|
3329
|
+
`);
|
|
3330
|
+
}
|
|
3331
|
+
async function flagsCompareCommand(source, target, opts) {
|
|
3332
|
+
const cfg = await requireWorkspace(opts.workspace);
|
|
3333
|
+
const [src, tgt] = await Promise.all([
|
|
3334
|
+
resolveEnvironment(cfg.apiUrl, cfg.workspaceId, source),
|
|
3335
|
+
resolveEnvironment(cfg.apiUrl, cfg.workspaceId, target)
|
|
3336
|
+
]);
|
|
3337
|
+
const data = await compareFlags(cfg.apiUrl, cfg.workspaceId, src, tgt);
|
|
3027
3338
|
if (opts.json) {
|
|
3028
3339
|
process.stdout.write(`${JSON.stringify(data, null, 2)}
|
|
3029
3340
|
`);
|
|
3030
3341
|
return;
|
|
3031
3342
|
}
|
|
3032
|
-
if (data.
|
|
3033
|
-
process.stdout.write("No
|
|
3343
|
+
if (data.keys.length === 0) {
|
|
3344
|
+
process.stdout.write("No flags in either environment.\n");
|
|
3034
3345
|
return;
|
|
3035
3346
|
}
|
|
3036
|
-
for (const
|
|
3037
|
-
process.stdout.write(
|
|
3038
|
-
|
|
3347
|
+
for (const row of data.keys) {
|
|
3348
|
+
process.stdout.write(
|
|
3349
|
+
`${row.key} ${row.status} ${formatValue(row.source_value ?? null)} \u2192 ${formatValue(row.target_value ?? null)}
|
|
3350
|
+
`
|
|
3351
|
+
);
|
|
3039
3352
|
}
|
|
3040
3353
|
}
|
|
3041
|
-
async function
|
|
3354
|
+
async function flagsPromoteCommand(source, target, opts) {
|
|
3042
3355
|
const cfg = await requireWorkspace(opts.workspace);
|
|
3043
|
-
const
|
|
3356
|
+
const [src, tgt] = await Promise.all([
|
|
3357
|
+
resolveEnvironment(cfg.apiUrl, cfg.workspaceId, source),
|
|
3358
|
+
resolveEnvironment(cfg.apiUrl, cfg.workspaceId, target)
|
|
3359
|
+
]);
|
|
3360
|
+
const keys = opts.keys?.split(",").map((k) => k.trim()).filter(Boolean);
|
|
3361
|
+
const result = await promoteFlags(cfg.apiUrl, cfg.workspaceId, {
|
|
3362
|
+
source_environment_id: src,
|
|
3363
|
+
target_environment_id: tgt,
|
|
3364
|
+
keys: keys && keys.length > 0 ? keys : void 0,
|
|
3365
|
+
mode: opts.overwrite ? "overwrite" : "skip_existing",
|
|
3366
|
+
dry_run: Boolean(opts.dryRun)
|
|
3367
|
+
});
|
|
3044
3368
|
if (opts.json) {
|
|
3045
|
-
process.stdout.write(`${JSON.stringify(
|
|
3369
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
3046
3370
|
`);
|
|
3047
3371
|
return;
|
|
3048
3372
|
}
|
|
3049
|
-
|
|
3373
|
+
const prefix = result.dry_run ? "[dry run] " : "";
|
|
3374
|
+
process.stdout.write(
|
|
3375
|
+
`${prefix}copied: ${result.copied.join(", ") || "none"}
|
|
3376
|
+
${prefix}skipped (already set; use --overwrite): ${result.skipped.join(", ") || "none"}
|
|
3377
|
+
`
|
|
3378
|
+
);
|
|
3379
|
+
for (const failure of result.failed) {
|
|
3380
|
+
process.stdout.write(`${prefix}failed: ${failure.key} \u2014 ${failure.error}
|
|
3050
3381
|
`);
|
|
3051
|
-
}
|
|
3052
|
-
async function appsCreateCommand(opts) {
|
|
3053
|
-
const cfg = await requireWorkspace(opts.workspace);
|
|
3054
|
-
const kindRaw = (opts.kind ?? "shell_federated").trim();
|
|
3055
|
-
if (kindRaw === "coderunner") {
|
|
3056
|
-
throw new Error(
|
|
3057
|
-
"Do not create app_kind=coderunner. Deploy workloads with `gf deploy`. Use --kind shell_federated or --kind standalone."
|
|
3058
|
-
);
|
|
3059
|
-
}
|
|
3060
|
-
if (kindRaw !== "shell_federated" && kindRaw !== "standalone") {
|
|
3061
|
-
throw new Error(
|
|
3062
|
-
`--kind must be shell_federated or standalone (got ${kindRaw})`
|
|
3063
|
-
);
|
|
3064
|
-
}
|
|
3065
|
-
const kind = kindRaw;
|
|
3066
|
-
const localManifest = await maybeReadManifest(opts.manifest);
|
|
3067
|
-
const name = opts.name?.trim() || (typeof localManifest?.name === "string" ? localManifest.name : "");
|
|
3068
|
-
const slug = opts.slug?.trim() || (typeof localManifest?.appId === "string" ? localManifest.appId : name ? slugify2(name) : "");
|
|
3069
|
-
if (!name) {
|
|
3070
|
-
throw new Error("Pass --name, or a manifest with a name field.");
|
|
3071
|
-
}
|
|
3072
|
-
if (!slug) {
|
|
3073
|
-
throw new Error("Pass --slug, or a manifest with appId.");
|
|
3074
3382
|
}
|
|
3075
|
-
if (
|
|
3076
|
-
|
|
3077
|
-
"Standalone apps require --primary-coderunner <id> (an existing service coderunner)."
|
|
3078
|
-
);
|
|
3383
|
+
if (result.failed.length > 0) {
|
|
3384
|
+
process.exitCode = 1;
|
|
3079
3385
|
}
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3386
|
+
}
|
|
3387
|
+
|
|
3388
|
+
// src/commands/files.ts
|
|
3389
|
+
import { promises as fs6 } from "fs";
|
|
3390
|
+
import path7 from "path";
|
|
3391
|
+
function guessContentType(filename) {
|
|
3392
|
+
const ext = path7.extname(filename).toLowerCase();
|
|
3393
|
+
const types = {
|
|
3394
|
+
".json": "application/json",
|
|
3395
|
+
".txt": "text/plain",
|
|
3396
|
+
".csv": "text/csv",
|
|
3397
|
+
".pdf": "application/pdf",
|
|
3398
|
+
".png": "image/png",
|
|
3399
|
+
".jpg": "image/jpeg",
|
|
3400
|
+
".jpeg": "image/jpeg",
|
|
3401
|
+
".zip": "application/zip"
|
|
3084
3402
|
};
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
let app;
|
|
3091
|
-
try {
|
|
3092
|
-
app = await createApp(cfg.apiUrl, cfg.workspaceId, body);
|
|
3093
|
-
} catch (err) {
|
|
3094
|
-
if (err instanceof CpError && err.status === 409) {
|
|
3095
|
-
throw new Error(
|
|
3096
|
-
`App slug ${slug} already exists. Init the kit if you do not have it, then publish:
|
|
3097
|
-
${initThenPublishHint(slug)}`
|
|
3098
|
-
);
|
|
3099
|
-
}
|
|
3100
|
-
throw err;
|
|
3101
|
-
}
|
|
3403
|
+
return types[ext] ?? "application/octet-stream";
|
|
3404
|
+
}
|
|
3405
|
+
async function filesListCommand(opts) {
|
|
3406
|
+
const cfg = await requireWorkspace(opts.workspace);
|
|
3407
|
+
const data = await listFiles(cfg.apiUrl, cfg.workspaceId);
|
|
3102
3408
|
if (opts.json) {
|
|
3103
|
-
process.stdout.write(`${JSON.stringify(
|
|
3409
|
+
process.stdout.write(`${JSON.stringify(data, null, 2)}
|
|
3104
3410
|
`);
|
|
3105
3411
|
return;
|
|
3106
3412
|
}
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3413
|
+
if (data.files.length === 0) {
|
|
3414
|
+
process.stdout.write("No files in this workspace.\n");
|
|
3415
|
+
return;
|
|
3416
|
+
}
|
|
3417
|
+
for (const file of data.files) {
|
|
3418
|
+
const size = file.size_bytes != null ? `${file.size_bytes}B` : "";
|
|
3110
3419
|
process.stdout.write(
|
|
3111
|
-
|
|
3112
|
-
gf apps init --slug ${slug}
|
|
3113
|
-
cd ${slug} && npm install && npm run release
|
|
3114
|
-
gf apps publish --path release.zip
|
|
3115
|
-
Do not scaffold src/App.tsx from scratch \u2014 BrowserRouter in App.tsx crashes the Shell.
|
|
3420
|
+
`${file.id} ${file.name ?? ""} ${size} [${file.status ?? ""}]
|
|
3116
3421
|
`
|
|
3117
3422
|
);
|
|
3118
3423
|
}
|
|
3119
3424
|
}
|
|
3120
|
-
async function
|
|
3425
|
+
async function filesUploadCommand(filePath, opts) {
|
|
3121
3426
|
const cfg = await requireWorkspace(opts.workspace);
|
|
3122
|
-
const
|
|
3123
|
-
const
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
`Bundle exceeds ${MAX_RELEASE_BYTES} bytes (${bytes.byteLength}).`
|
|
3138
|
-
);
|
|
3139
|
-
}
|
|
3140
|
-
const sibling = await findSiblingManifest(bundlePath);
|
|
3141
|
-
const ref = appRef?.trim() || opts.app?.trim() || (typeof sibling?.manifest.appId === "string" ? sibling.manifest.appId : "");
|
|
3142
|
-
if (!ref) {
|
|
3143
|
-
throw new Error(
|
|
3144
|
-
"Pass an app id or slug, or set appId in groundfloor.manifest.json next to the bundle."
|
|
3145
|
-
);
|
|
3146
|
-
}
|
|
3147
|
-
const app = await resolveAppId(cfg.apiUrl, cfg.workspaceId, ref);
|
|
3148
|
-
if (app.app_kind && app.app_kind !== "shell_federated") {
|
|
3149
|
-
throw new Error(
|
|
3150
|
-
`Releases are only for shell_federated apps (this app is ${app.app_kind}). Use \`gf deploy\` for coderunner workloads.`
|
|
3151
|
-
);
|
|
3152
|
-
}
|
|
3153
|
-
const ext = path7.extname(bundlePath).toLowerCase();
|
|
3154
|
-
const isZip = ext === ".zip";
|
|
3155
|
-
if (!isZip) {
|
|
3427
|
+
const resolved = path7.resolve(filePath);
|
|
3428
|
+
const buf = await fs6.readFile(resolved);
|
|
3429
|
+
const name = opts.name ?? path7.basename(resolved);
|
|
3430
|
+
const contentType = guessContentType(name);
|
|
3431
|
+
const init = await initFileUpload(cfg.apiUrl, cfg.workspaceId, {
|
|
3432
|
+
name,
|
|
3433
|
+
content_type: contentType,
|
|
3434
|
+
size_bytes: buf.byteLength
|
|
3435
|
+
});
|
|
3436
|
+
const put = await fetch(init.upload_url, {
|
|
3437
|
+
method: "PUT",
|
|
3438
|
+
headers: { "Content-Type": contentType },
|
|
3439
|
+
body: buf
|
|
3440
|
+
});
|
|
3441
|
+
if (!put.ok) {
|
|
3156
3442
|
throw new Error(
|
|
3157
|
-
`
|
|
3443
|
+
`Upload to object storage failed: ${put.status} ${put.statusText}`
|
|
3158
3444
|
);
|
|
3159
3445
|
}
|
|
3160
|
-
|
|
3161
|
-
const bundle = "zip";
|
|
3162
|
-
const contentType = "application/zip";
|
|
3163
|
-
if (opts.syncManifest !== false && sibling) {
|
|
3164
|
-
await patchApp(cfg.apiUrl, cfg.workspaceId, app.id, {
|
|
3165
|
-
manifest: sibling.manifest
|
|
3166
|
-
});
|
|
3167
|
-
}
|
|
3168
|
-
const init = await initAppReleaseUpload(cfg.apiUrl, cfg.workspaceId, app.id, {
|
|
3169
|
-
bundle,
|
|
3170
|
-
label: opts.label,
|
|
3171
|
-
// Required since wave 5: the server pins this into the presigned PUT and
|
|
3172
|
-
// checks it against the quota before minting a URL. Omitting it is a 422.
|
|
3173
|
-
size_bytes: bytes.byteLength
|
|
3174
|
-
});
|
|
3175
|
-
await uploadAppReleaseContent(
|
|
3446
|
+
const file = await finalizeFileUpload(
|
|
3176
3447
|
cfg.apiUrl,
|
|
3177
3448
|
cfg.workspaceId,
|
|
3178
|
-
|
|
3179
|
-
init.release.id,
|
|
3180
|
-
bytes,
|
|
3181
|
-
contentType
|
|
3449
|
+
init.file.id
|
|
3182
3450
|
);
|
|
3183
|
-
|
|
3451
|
+
process.stdout.write(`Uploaded ${file.id} ${file.name ?? name}
|
|
3452
|
+
`);
|
|
3453
|
+
}
|
|
3454
|
+
async function filesDownloadCommand(fileId, opts) {
|
|
3455
|
+
const cfg = await requireWorkspace(opts.workspace);
|
|
3456
|
+
const { download_url: url } = await getFileDownloadUrl(
|
|
3184
3457
|
cfg.apiUrl,
|
|
3185
3458
|
cfg.workspaceId,
|
|
3186
|
-
|
|
3187
|
-
init.release.id
|
|
3459
|
+
fileId
|
|
3188
3460
|
);
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
`);
|
|
3192
|
-
return;
|
|
3461
|
+
const res = await fetch(url);
|
|
3462
|
+
if (!res.ok) {
|
|
3463
|
+
throw new Error(`Download failed: ${res.status} ${res.statusText}`);
|
|
3193
3464
|
}
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
);
|
|
3465
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
3466
|
+
const dest = path7.resolve(opts.out ?? fileId);
|
|
3467
|
+
await fs6.writeFile(dest, buf);
|
|
3468
|
+
process.stdout.write(`Wrote ${dest}
|
|
3469
|
+
`);
|
|
3470
|
+
}
|
|
3471
|
+
async function filesRmCommand(fileId, opts) {
|
|
3472
|
+
const cfg = await requireWorkspace(opts.workspace);
|
|
3473
|
+
await deleteFile(cfg.apiUrl, cfg.workspaceId, fileId);
|
|
3474
|
+
process.stdout.write(`Deleted file ${fileId}
|
|
3475
|
+
`);
|
|
3197
3476
|
}
|
|
3198
3477
|
|
|
3199
3478
|
// src/commands/apps-init.ts
|
|
@@ -3553,6 +3832,107 @@ async function apiCommand(opts) {
|
|
|
3553
3832
|
`);
|
|
3554
3833
|
}
|
|
3555
3834
|
|
|
3835
|
+
// src/commands/usage.ts
|
|
3836
|
+
var DAY_S = 86400;
|
|
3837
|
+
async function usageCommand(opts) {
|
|
3838
|
+
const cfg = await resolveConfig({ workspaceId: opts.workspace });
|
|
3839
|
+
let sinceSeconds;
|
|
3840
|
+
if (opts.sinceDays !== void 0) {
|
|
3841
|
+
const days = Number(opts.sinceDays);
|
|
3842
|
+
if (!Number.isFinite(days) || days < 1 / 24 || days > 400) {
|
|
3843
|
+
throw new Error("--since-days must be a number between 0.05 and 400.");
|
|
3844
|
+
}
|
|
3845
|
+
sinceSeconds = Math.round(days * DAY_S);
|
|
3846
|
+
}
|
|
3847
|
+
let accountId = opts.account?.trim();
|
|
3848
|
+
if (!accountId) {
|
|
3849
|
+
if (!cfg.workspaceId) {
|
|
3850
|
+
throw new Error(
|
|
3851
|
+
"No workspace selected. Pass --account <account-id>, --workspace <id>, or run `gf workspaces use <id>`."
|
|
3852
|
+
);
|
|
3853
|
+
}
|
|
3854
|
+
const workspaces2 = await listWorkspaces(cfg.apiUrl);
|
|
3855
|
+
const ws = workspaces2.find((item) => item.id === cfg.workspaceId);
|
|
3856
|
+
const resolved = ws?.account_id ?? ws?.organization_id;
|
|
3857
|
+
if (!resolved) {
|
|
3858
|
+
throw new Error(
|
|
3859
|
+
`Could not find the account for workspace ${cfg.workspaceId}. Pass --account <account-id>.`
|
|
3860
|
+
);
|
|
3861
|
+
}
|
|
3862
|
+
accountId = resolved;
|
|
3863
|
+
}
|
|
3864
|
+
const [quota, billing] = await Promise.allSettled([
|
|
3865
|
+
getAccountQuota(cfg.apiUrl, accountId),
|
|
3866
|
+
getBillingSummary(cfg.apiUrl, accountId, sinceSeconds)
|
|
3867
|
+
]);
|
|
3868
|
+
if (opts.json) {
|
|
3869
|
+
const part = (r) => r.status === "fulfilled" ? r.value : { error: errorText(r.reason) };
|
|
3870
|
+
process.stdout.write(
|
|
3871
|
+
`${JSON.stringify({ account_id: accountId, quota: part(quota), billing: part(billing) }, null, 2)}
|
|
3872
|
+
`
|
|
3873
|
+
);
|
|
3874
|
+
if (quota.status === "rejected" && billing.status === "rejected") {
|
|
3875
|
+
process.exitCode = 1;
|
|
3876
|
+
}
|
|
3877
|
+
return;
|
|
3878
|
+
}
|
|
3879
|
+
process.stdout.write(`Account: ${accountId}
|
|
3880
|
+
`);
|
|
3881
|
+
if (quota.status === "fulfilled") {
|
|
3882
|
+
printQuota(quota.value);
|
|
3883
|
+
} else {
|
|
3884
|
+
process.stdout.write(`
|
|
3885
|
+
Quota: unavailable (${errorText(quota.reason)})
|
|
3886
|
+
`);
|
|
3887
|
+
}
|
|
3888
|
+
if (billing.status === "fulfilled") {
|
|
3889
|
+
printBilling(billing.value);
|
|
3890
|
+
} else {
|
|
3891
|
+
process.stdout.write(`
|
|
3892
|
+
Billing: unavailable (${errorText(billing.reason)})
|
|
3893
|
+
`);
|
|
3894
|
+
}
|
|
3895
|
+
if (quota.status === "rejected" && billing.status === "rejected") {
|
|
3896
|
+
process.exitCode = 1;
|
|
3897
|
+
}
|
|
3898
|
+
}
|
|
3899
|
+
function errorText(reason) {
|
|
3900
|
+
if (reason instanceof CpError && reason.status === 403) {
|
|
3901
|
+
return `${reason.message}; your user lacks permission on this account`;
|
|
3902
|
+
}
|
|
3903
|
+
return reason instanceof Error ? reason.message : String(reason);
|
|
3904
|
+
}
|
|
3905
|
+
function printQuota(q) {
|
|
3906
|
+
const out = process.stdout;
|
|
3907
|
+
out.write(`
|
|
3908
|
+
Quota (plan: ${q.plan_tier})
|
|
3909
|
+
`);
|
|
3910
|
+
for (const l of q.limits) {
|
|
3911
|
+
const limit = l.limit == null ? "unlimited" : String(l.limit);
|
|
3912
|
+
out.write(` ${l.label}: ${l.current} / ${limit} ${l.unit}
|
|
3913
|
+
`);
|
|
3914
|
+
}
|
|
3915
|
+
if (q.note) out.write(` note: ${q.note}
|
|
3916
|
+
`);
|
|
3917
|
+
}
|
|
3918
|
+
function printBilling(b) {
|
|
3919
|
+
const out = process.stdout;
|
|
3920
|
+
const label = b.is_estimate === false ? "Billing" : "Billing ESTIMATE (not a final bill)";
|
|
3921
|
+
out.write(`
|
|
3922
|
+
${label}: ${b.period_start} to ${b.period_end}
|
|
3923
|
+
`);
|
|
3924
|
+
out.write(
|
|
3925
|
+
` CU: compute ${b.breakdown.compute}, storage ${b.breakdown.storage}, network ${b.breakdown.network}, ai ${b.breakdown.ai} (adjusted ${b.adjusted_cu})
|
|
3926
|
+
`
|
|
3927
|
+
);
|
|
3928
|
+
out.write(` indicative amount: $${b.final_bill_usd.toFixed(2)} USD
|
|
3929
|
+
`);
|
|
3930
|
+
if (b.pricing_final === false) out.write(" pricing is not final (placeholder rates)\n");
|
|
3931
|
+
if (b.ai_usage_truncated) out.write(" AI usage may be understated (log window truncated)\n");
|
|
3932
|
+
if (b.note) out.write(` note: ${b.note}
|
|
3933
|
+
`);
|
|
3934
|
+
}
|
|
3935
|
+
|
|
3556
3936
|
// src/commands/identities.ts
|
|
3557
3937
|
async function identitiesListCommand(opts) {
|
|
3558
3938
|
const cfg = await resolveConfig({ workspaceId: opts.workspace });
|
|
@@ -3648,7 +4028,7 @@ m2m.command("token").description(
|
|
|
3648
4028
|
"Exchange GROUNDFLOOR_M2M_CLIENT_ID + SECRET for a short CP JWT (not gf token)"
|
|
3649
4029
|
).option("-w, --workspace <id>", "Workspace id override").option("--client-id <id>", "M2M client_id (else GROUNDFLOOR_M2M_CLIENT_ID)").option(
|
|
3650
4030
|
"--client-secret <secret>",
|
|
3651
|
-
"M2M secret (
|
|
4031
|
+
"M2M secret (prefer GROUNDFLOOR_M2M_CLIENT_SECRET: argv is visible in `ps` and shell history)"
|
|
3652
4032
|
).option("--json", "Output the full token response as JSON").action(
|
|
3653
4033
|
(opts) => run(
|
|
3654
4034
|
() => m2mTokenCommand({
|
|
@@ -3669,17 +4049,23 @@ workspaces.command("use <workspaceId>").description("Set the default workspace")
|
|
|
3669
4049
|
);
|
|
3670
4050
|
var coderunner = program.command("coderunner").alias("cr").description("Coderunner commands");
|
|
3671
4051
|
coderunner.command("ls").alias("list").description("List coderunners in the default (or --workspace) workspace").option("-w, --workspace <id>", "Workspace id override").option("--json", "Output raw JSON").action((opts) => run(() => coderunnerListCommand(opts)));
|
|
3672
|
-
coderunner.command("status").description("Show coderunner status
|
|
3673
|
-
coderunner.command("cancel-deployment").
|
|
4052
|
+
coderunner.command("status").description("Show coderunner status and current deployment (use --json for the full payload)").requiredOption("-c, --coderunner <id|slug>", "Coderunner id or slug").option("-w, --workspace <id>", "Workspace id override").option("--json", "Output raw JSON").action((opts) => run(() => statusCommand(opts)));
|
|
4053
|
+
coderunner.command("cancel-deployment").alias("delete-deployment").description(
|
|
4054
|
+
"DELETE the current (or --deployment-id) deployment. This is not an in-flight cancel: it takes the live service down"
|
|
4055
|
+
).requiredOption("-c, --coderunner <id|slug>", "Coderunner id or slug").option("-w, --workspace <id>", "Workspace id override").option("--deployment-id <id>", "Deployment id (default: the current live deployment)").option(
|
|
4056
|
+
"-y, --yes",
|
|
4057
|
+
"Skip the confirmation (required when there is no interactive terminal)"
|
|
4058
|
+
).action(
|
|
3674
4059
|
(opts) => run(
|
|
3675
4060
|
() => cancelDeploymentCommand({
|
|
3676
4061
|
workspace: opts.workspace,
|
|
3677
4062
|
coderunner: opts.coderunner,
|
|
3678
|
-
deploymentId: opts.deploymentId
|
|
4063
|
+
deploymentId: opts.deploymentId,
|
|
4064
|
+
yes: opts.yes
|
|
3679
4065
|
})
|
|
3680
4066
|
)
|
|
3681
4067
|
);
|
|
3682
|
-
coderunner.command("logs").description("Show Process Log lines for a coderunner (stdout/stderr)").requiredOption("-c, --coderunner <id|slug>", "Coderunner id or slug").option("-w, --workspace <id>", "Workspace id override").option("--limit <n>", "Max lines (default 50)").option("--since-seconds <n>", "Lookback window in seconds (default 3600)").option("--search <text>", "Substring filter").option("--json", "Output raw JSON").action(
|
|
4068
|
+
coderunner.command("logs").description("Show Process Log lines for a coderunner (stdout/stderr)").requiredOption("-c, --coderunner <id|slug>", "Coderunner id or slug").option("-w, --workspace <id>", "Workspace id override").option("--limit <n>", "Max lines (default 50, max 1000)").option("--since-seconds <n>", "Lookback window in seconds (default 3600, max 604800 = 7 days)").option("--search <text>", "Substring filter").option("--json", "Output raw JSON").action(
|
|
3683
4069
|
(opts) => run(
|
|
3684
4070
|
() => logsCommand({
|
|
3685
4071
|
workspace: opts.workspace,
|
|
@@ -3712,13 +4098,15 @@ coderunner.command("vault-token").description(
|
|
|
3712
4098
|
})
|
|
3713
4099
|
)
|
|
3714
4100
|
);
|
|
3715
|
-
var domains = program.command("domains").description(
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
domains.command("
|
|
4101
|
+
var domains = program.command("domains").description(
|
|
4102
|
+
"Manage custom domains (CNAME) for an App (standalone / service apps). Not available on standalone coderunners"
|
|
4103
|
+
);
|
|
4104
|
+
domains.command("add <domain>").description("Add a custom domain and print the DNS records to create").option("--app <id|slug>", "Target App id or slug (standalone / service app)").option("-c, --coderunner <id|slug>", "Removed: custom domains are not available on standalone coderunners").option("-w, --workspace <id>", "Workspace id override").option("--json", "Output raw JSON").action((domain, opts) => run(() => domainsAddCommand(domain, opts)));
|
|
4105
|
+
domains.command("ls").alias("list").description("List custom domains for an App").option("--app <id|slug>", "Target App id or slug (standalone / service app)").option("-c, --coderunner <id|slug>", "Removed: custom domains are not available on standalone coderunners").option("-w, --workspace <id>", "Workspace id override").option("--json", "Output raw JSON").action((opts) => run(() => domainsListCommand(opts)));
|
|
4106
|
+
domains.command("rm <domain>").alias("remove").description("Remove a custom domain from an App").option("--app <id|slug>", "Target App id or slug (standalone / service app)").option("-c, --coderunner <id|slug>", "Removed: custom domains are not available on standalone coderunners").option("-w, --workspace <id>", "Workspace id override").action(
|
|
3719
4107
|
(domain, opts) => run(() => domainsRemoveCommand(domain, opts))
|
|
3720
4108
|
);
|
|
3721
|
-
domains.command("verify <domain>").description("Re-check the DNS records for a custom domain").
|
|
4109
|
+
domains.command("verify <domain>").description("Re-check the DNS records for a custom domain").option("--app <id|slug>", "Target App id or slug (standalone / service app)").option("-c, --coderunner <id|slug>", "Removed: custom domains are not available on standalone coderunners").option("-w, --workspace <id>", "Workspace id override").option("--json", "Output raw JSON").action(
|
|
3722
4110
|
(domain, opts) => run(() => domainsVerifyCommand(domain, opts))
|
|
3723
4111
|
);
|
|
3724
4112
|
var secrets = program.command("secrets").description("Manage workspace secrets");
|
|
@@ -3807,8 +4195,11 @@ var dataplane = program.command("dataplane").description("Workspace Dataplane se
|
|
|
3807
4195
|
dataplane.command("status").description("Show Dataplane setup status").option("-w, --workspace <id>", "Workspace id override").option("--json", "Output raw JSON").action((opts) => run(() => dataplaneStatusCommand(opts)));
|
|
3808
4196
|
dataplane.command("provision").description("Provision Dataplane for this workspace (idempotent)").option("-w, --workspace <id>", "Workspace id override").option("--json", "Output raw JSON").action((opts) => run(() => dataplaneProvisionCommand(opts)));
|
|
3809
4197
|
program.command("deploy").description(
|
|
3810
|
-
"Package code and deploy as a coderunner (not an App)"
|
|
3811
|
-
).option(
|
|
4198
|
+
"Package code and deploy as a coderunner (not an App). Env var VALUES are not stored: resend them with -e on every deploy"
|
|
4199
|
+
).option(
|
|
4200
|
+
"-w, --workspace <id>",
|
|
4201
|
+
"Target workspace id. Required with no interactive terminal unless GROUNDFLOOR_WORKSPACE_ID is set; the saved `gf workspaces use` default is only used after an interactive confirm"
|
|
4202
|
+
).option("--api-url <url>", "Control Plane base URL").option(
|
|
3812
4203
|
"-c, --coderunner <id>",
|
|
3813
4204
|
"Deploy into an existing coderunner id (skip find/create)"
|
|
3814
4205
|
).option("-n, --name <name>", "Coderunner name (default: folder or repo name)").option("-p, --path <dir>", "Local directory to package (default: cwd)").option("--git <url>", "Clone and deploy a remote git/bitbucket repo").option("--ref <ref>", "Branch/tag/commit for --git").option("--subdir <dir>", "Subdirectory within the source to package").option("--runtime <runtime>", "Language slug from Code Runner (node, python, go, \u2026)").option(
|
|
@@ -3819,13 +4210,19 @@ program.command("deploy").description(
|
|
|
3819
4210
|
'Cron for --workload-type schedule (or groundfloor.json "schedule")'
|
|
3820
4211
|
).option(
|
|
3821
4212
|
"--app-id <id>",
|
|
3822
|
-
"Bind
|
|
4213
|
+
"Bind a NEWLY CREATED coderunner as a helper of this App (ignored, with a warning, when an existing coderunner is reused)"
|
|
3823
4214
|
).option("--cpu <cpu>", "CPU request, e.g. 250m").option("--memory <memory>", "Memory request, e.g. 256Mi").option("-e, --env <KEY=VALUE>", "Environment variable (repeatable)", collect, []).option(
|
|
3824
4215
|
"--secret <KEY>",
|
|
3825
|
-
"Inject this workspace secret by reference (repeatable).
|
|
4216
|
+
"Inject this workspace secret by reference (repeatable). Only works where the platform has secret injection enabled (off by default); otherwise read secrets at runtime via an M2M client with secrets:read",
|
|
3826
4217
|
collect,
|
|
3827
4218
|
[]
|
|
4219
|
+
).option(
|
|
4220
|
+
"-y, --yes",
|
|
4221
|
+
"Confirm updating an existing same-named coderunner (required with no interactive terminal), and skip the prompt about previously deployed env variables that this deploy does not resend (-c). Does not bypass the saved-default-workspace check: pass -w"
|
|
3828
4222
|
).option("-m, --message <text>", "Version description").option("--skip-deploy", "Upload a new version but do not deploy it").action((opts) => run(() => deployCommand(opts)));
|
|
4223
|
+
program.command("usage").description(
|
|
4224
|
+
"Show account quota/usage and the billing estimate (read-only). The account is the billing parent of the workspace, not the workspace itself"
|
|
4225
|
+
).option("-w, --workspace <id>", "Workspace whose account to show").option("--account <id>", "Account id (overrides the workspace's account)").option("--since-days <n>", "Billing lookback in days (default 30, max 400)").option("--json", "Output raw JSON").action((opts) => run(() => usageCommand(opts)));
|
|
3829
4226
|
program.command("api").description("Call a Control Plane path (passthrough for gaps in gf commands)").argument("<path>", "Path beginning with /v1/\u2026 Use {workspace_id} for the current workspace").option("-X, --method <method>", "HTTP method", "GET").option("--body <json>", "JSON body for POST/PUT/PATCH").option("-w, --workspace <id>", "Workspace id override").action(
|
|
3830
4227
|
(path11, opts) => run(
|
|
3831
4228
|
() => apiCommand({
|