@groundfloorcloud/cli 0.1.12 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -11
- package/dist/index.js +1199 -828
- 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
|
}
|
|
@@ -781,36 +797,29 @@ function getProcessLogs(apiUrl, workspaceId, query = {}) {
|
|
|
781
797
|
`/v1/workspaces/${encodeURIComponent(workspaceId)}/logs${qs ? `?${qs}` : ""}`
|
|
782
798
|
);
|
|
783
799
|
}
|
|
784
|
-
function domainsBase(workspaceId,
|
|
800
|
+
function domainsBase(workspaceId, appId) {
|
|
785
801
|
return `/v1/workspaces/${encodeURIComponent(
|
|
786
802
|
workspaceId
|
|
787
|
-
)}/
|
|
803
|
+
)}/apps/${encodeURIComponent(appId)}/coderunner/domains`;
|
|
788
804
|
}
|
|
789
|
-
function listDomains(apiUrl, workspaceId,
|
|
790
|
-
return cpGet(
|
|
791
|
-
apiUrl,
|
|
792
|
-
domainsBase(workspaceId, coderunnerId)
|
|
793
|
-
);
|
|
805
|
+
function listDomains(apiUrl, workspaceId, appId) {
|
|
806
|
+
return cpGet(apiUrl, domainsBase(workspaceId, appId));
|
|
794
807
|
}
|
|
795
|
-
function addDomain(apiUrl, workspaceId,
|
|
796
|
-
return cpPostJson(
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
{ domain }
|
|
800
|
-
);
|
|
808
|
+
function addDomain(apiUrl, workspaceId, appId, domain) {
|
|
809
|
+
return cpPostJson(apiUrl, domainsBase(workspaceId, appId), {
|
|
810
|
+
domain
|
|
811
|
+
});
|
|
801
812
|
}
|
|
802
|
-
function removeDomain(apiUrl, workspaceId,
|
|
813
|
+
function removeDomain(apiUrl, workspaceId, appId, domain) {
|
|
803
814
|
return cpDelete(
|
|
804
815
|
apiUrl,
|
|
805
|
-
`${domainsBase(workspaceId,
|
|
816
|
+
`${domainsBase(workspaceId, appId)}/${encodeURIComponent(domain)}`
|
|
806
817
|
);
|
|
807
818
|
}
|
|
808
|
-
function verifyDomain(apiUrl, workspaceId,
|
|
819
|
+
function verifyDomain(apiUrl, workspaceId, appId, domain) {
|
|
809
820
|
return cpPostJson(
|
|
810
821
|
apiUrl,
|
|
811
|
-
`${domainsBase(workspaceId,
|
|
812
|
-
domain
|
|
813
|
-
)}/verify`,
|
|
822
|
+
`${domainsBase(workspaceId, appId)}/${encodeURIComponent(domain)}/verify`,
|
|
814
823
|
{}
|
|
815
824
|
);
|
|
816
825
|
}
|
|
@@ -1052,6 +1061,20 @@ async function controlPlaneRequest(apiUrl, method, path11, body) {
|
|
|
1052
1061
|
if (!text) return void 0;
|
|
1053
1062
|
return JSON.parse(text);
|
|
1054
1063
|
}
|
|
1064
|
+
function getAccountQuota(apiUrl, accountId) {
|
|
1065
|
+
return cpGet(
|
|
1066
|
+
apiUrl,
|
|
1067
|
+
`/v1/accounts/${encodeURIComponent(accountId)}/quota`
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
function getBillingSummary(apiUrl, accountId, sinceSeconds) {
|
|
1071
|
+
return cpGet(
|
|
1072
|
+
apiUrl,
|
|
1073
|
+
`/v1/accounts/${encodeURIComponent(accountId)}/billing/summary${queryString({
|
|
1074
|
+
since_seconds: sinceSeconds ? String(sinceSeconds) : void 0
|
|
1075
|
+
})}`
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
1055
1078
|
|
|
1056
1079
|
// src/commands/whoami.ts
|
|
1057
1080
|
async function whoamiCommand() {
|
|
@@ -1139,6 +1162,14 @@ Tip: run \`gf workspaces use <id>\` to set a default.
|
|
|
1139
1162
|
}
|
|
1140
1163
|
}
|
|
1141
1164
|
async function workspacesUseCommand(workspaceId) {
|
|
1165
|
+
const cfg = await resolveConfig();
|
|
1166
|
+
const workspaces2 = await listWorkspaces(cfg.apiUrl);
|
|
1167
|
+
const match = workspaces2.find((ws) => ws.id === workspaceId);
|
|
1168
|
+
if (!match) {
|
|
1169
|
+
throw new Error(
|
|
1170
|
+
`Workspace ${workspaceId} was not found among the workspaces visible to you. Run \`gf workspaces\` to list valid ids.`
|
|
1171
|
+
);
|
|
1172
|
+
}
|
|
1142
1173
|
await updateConfig({ workspaceId });
|
|
1143
1174
|
process.stdout.write(`Default workspace set to ${workspaceId}
|
|
1144
1175
|
`);
|
|
@@ -1256,6 +1287,49 @@ async function m2mTokenCommand(opts) {
|
|
|
1256
1287
|
`);
|
|
1257
1288
|
}
|
|
1258
1289
|
|
|
1290
|
+
// src/prompt.ts
|
|
1291
|
+
import readline from "readline";
|
|
1292
|
+
function defaultPromptIO() {
|
|
1293
|
+
return {
|
|
1294
|
+
isTTY: Boolean(process.stdin.isTTY && process.stderr.isTTY),
|
|
1295
|
+
ask(question) {
|
|
1296
|
+
return new Promise((resolve2) => {
|
|
1297
|
+
const rl = readline.createInterface({
|
|
1298
|
+
input: process.stdin,
|
|
1299
|
+
output: process.stderr
|
|
1300
|
+
});
|
|
1301
|
+
rl.question(question, (answer) => {
|
|
1302
|
+
rl.close();
|
|
1303
|
+
resolve2(answer);
|
|
1304
|
+
});
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
function isYes(answer) {
|
|
1310
|
+
return /^(y|yes)$/i.test(answer.trim());
|
|
1311
|
+
}
|
|
1312
|
+
var AbortedError = class extends Error {
|
|
1313
|
+
constructor() {
|
|
1314
|
+
super("Aborted. Nothing was changed.");
|
|
1315
|
+
this.name = "AbortedError";
|
|
1316
|
+
}
|
|
1317
|
+
};
|
|
1318
|
+
async function confirmAction(opts) {
|
|
1319
|
+
const io = opts.io ?? defaultPromptIO();
|
|
1320
|
+
const write = opts.write ?? ((line) => process.stderr.write(`${line}
|
|
1321
|
+
`));
|
|
1322
|
+
for (const line of opts.summary) write(line);
|
|
1323
|
+
if (opts.yes) return;
|
|
1324
|
+
if (!io.isTTY) {
|
|
1325
|
+
throw new Error(
|
|
1326
|
+
`Refusing to continue without confirmation (no interactive terminal). Re-run with ${opts.bypassHint ?? "--yes"} to confirm.`
|
|
1327
|
+
);
|
|
1328
|
+
}
|
|
1329
|
+
const answer = await io.ask(`${opts.question ?? "Continue?"} [y/N] `);
|
|
1330
|
+
if (!isYes(answer)) throw new AbortedError();
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1259
1333
|
// src/commands/cancel-deployment.ts
|
|
1260
1334
|
async function cancelDeploymentCommand(opts) {
|
|
1261
1335
|
const cfg = await resolveConfig({ workspaceId: opts.workspace });
|
|
@@ -1264,14 +1338,26 @@ async function cancelDeploymentCommand(opts) {
|
|
|
1264
1338
|
"No workspace selected. Run `gf workspaces use <id>` or pass --workspace <id>."
|
|
1265
1339
|
);
|
|
1266
1340
|
}
|
|
1267
|
-
const
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
opts.coderunner
|
|
1341
|
+
const ws = cfg.workspaceId;
|
|
1342
|
+
const all = await listCoderunners(cfg.apiUrl, ws);
|
|
1343
|
+
const target = all.find(
|
|
1344
|
+
(c) => c.id === opts.coderunner || c.slug === opts.coderunner
|
|
1271
1345
|
);
|
|
1346
|
+
if (!target) {
|
|
1347
|
+
throw new Error(`Coderunner not found: ${opts.coderunner}`);
|
|
1348
|
+
}
|
|
1349
|
+
const coderunnerId = target.id;
|
|
1350
|
+
let workspaceName;
|
|
1351
|
+
try {
|
|
1352
|
+
workspaceName = (await listWorkspaces(cfg.apiUrl)).find(
|
|
1353
|
+
(item) => item.id === ws
|
|
1354
|
+
)?.name;
|
|
1355
|
+
} catch {
|
|
1356
|
+
}
|
|
1272
1357
|
let deploymentId = opts.deploymentId?.trim();
|
|
1358
|
+
const defaulted = !deploymentId;
|
|
1273
1359
|
if (!deploymentId) {
|
|
1274
|
-
const current = await getDeployment(cfg.apiUrl,
|
|
1360
|
+
const current = await getDeployment(cfg.apiUrl, ws, coderunnerId);
|
|
1275
1361
|
deploymentId = current?.id?.trim();
|
|
1276
1362
|
}
|
|
1277
1363
|
if (!deploymentId) {
|
|
@@ -1279,12 +1365,20 @@ async function cancelDeploymentCommand(opts) {
|
|
|
1279
1365
|
`No current deployment for coderunner ${coderunnerId}. Pass --deployment-id.`
|
|
1280
1366
|
);
|
|
1281
1367
|
}
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
)
|
|
1368
|
+
const label = (name, id) => name?.trim() ? `${name.trim()} (${id})` : id;
|
|
1369
|
+
await confirmAction({
|
|
1370
|
+
summary: [
|
|
1371
|
+
"This will DELETE a deployment. If it is the live one, the service goes offline.",
|
|
1372
|
+
` workspace: ${label(workspaceName, ws)}`,
|
|
1373
|
+
` coderunner: ${label(target.name ?? target.slug, coderunnerId)}`,
|
|
1374
|
+
` deployment: ${deploymentId}${defaulted ? " (the current live deployment)" : ""}`
|
|
1375
|
+
],
|
|
1376
|
+
yes: opts.yes,
|
|
1377
|
+
bypassHint: "--yes",
|
|
1378
|
+
question: "Delete this deployment?",
|
|
1379
|
+
io: opts.io
|
|
1380
|
+
});
|
|
1381
|
+
await deleteCoderunnerDeployment(cfg.apiUrl, ws, coderunnerId, deploymentId);
|
|
1288
1382
|
process.stdout.write(
|
|
1289
1383
|
`Deleted deployment ${deploymentId} for coderunner ${coderunnerId}
|
|
1290
1384
|
`
|
|
@@ -1337,10 +1431,10 @@ function statusLabel(status) {
|
|
|
1337
1431
|
function customerPortalOrigin(apiUrl) {
|
|
1338
1432
|
try {
|
|
1339
1433
|
const url = new URL(apiUrl);
|
|
1340
|
-
url.host = url.host.replace(/^platform\./, "
|
|
1434
|
+
url.host = url.host.replace(/^platform\./, "console.");
|
|
1341
1435
|
return url.origin;
|
|
1342
1436
|
} catch {
|
|
1343
|
-
return "https://
|
|
1437
|
+
return "https://console.groundfloor.cloud";
|
|
1344
1438
|
}
|
|
1345
1439
|
}
|
|
1346
1440
|
function processLogPortalUrl(portalOrigin, workspaceId, runnerId) {
|
|
@@ -1433,6 +1527,34 @@ async function diagnoseDeployFailure(opts) {
|
|
|
1433
1527
|
}
|
|
1434
1528
|
|
|
1435
1529
|
// src/commands/logs.ts
|
|
1530
|
+
var MAX_LOG_LIMIT = 1e3;
|
|
1531
|
+
var MAX_LOG_SINCE_SECONDS = 7 * 24 * 60 * 60;
|
|
1532
|
+
var DEFAULT_LOG_LIMIT = 50;
|
|
1533
|
+
var DEFAULT_LOG_SINCE_SECONDS = 3600;
|
|
1534
|
+
function parseWholeNumber(raw) {
|
|
1535
|
+
return /^\d+$/.test(raw.trim()) ? Number(raw) : Number.NaN;
|
|
1536
|
+
}
|
|
1537
|
+
function parseLogsWindow(opts) {
|
|
1538
|
+
const limit = opts.limit !== void 0 ? parseWholeNumber(opts.limit) : DEFAULT_LOG_LIMIT;
|
|
1539
|
+
const sinceSeconds = opts.sinceSeconds !== void 0 ? parseWholeNumber(opts.sinceSeconds) : DEFAULT_LOG_SINCE_SECONDS;
|
|
1540
|
+
if (!Number.isInteger(limit) || limit < 1) {
|
|
1541
|
+
throw new Error("Invalid --limit (expected a whole number from 1 to 1000).");
|
|
1542
|
+
}
|
|
1543
|
+
if (limit > MAX_LOG_LIMIT) {
|
|
1544
|
+
throw new Error(
|
|
1545
|
+
`--limit ${limit} is above the maximum of ${MAX_LOG_LIMIT} lines per request.`
|
|
1546
|
+
);
|
|
1547
|
+
}
|
|
1548
|
+
if (!Number.isInteger(sinceSeconds) || sinceSeconds < 10) {
|
|
1549
|
+
throw new Error("Invalid --since-seconds (minimum 10).");
|
|
1550
|
+
}
|
|
1551
|
+
if (sinceSeconds > MAX_LOG_SINCE_SECONDS) {
|
|
1552
|
+
throw new Error(
|
|
1553
|
+
`--since-seconds ${sinceSeconds} is above the maximum of ${MAX_LOG_SINCE_SECONDS} (7 days).`
|
|
1554
|
+
);
|
|
1555
|
+
}
|
|
1556
|
+
return { limit, sinceSeconds };
|
|
1557
|
+
}
|
|
1436
1558
|
async function logsCommand(opts) {
|
|
1437
1559
|
const cfg = await resolveConfig({ workspaceId: opts.workspace });
|
|
1438
1560
|
if (!cfg.workspaceId) {
|
|
@@ -1445,14 +1567,7 @@ async function logsCommand(opts) {
|
|
|
1445
1567
|
cfg.workspaceId,
|
|
1446
1568
|
opts.coderunner
|
|
1447
1569
|
);
|
|
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
|
-
}
|
|
1570
|
+
const { limit, sinceSeconds } = parseLogsWindow(opts);
|
|
1456
1571
|
const page = await getProcessLogs(cfg.apiUrl, cfg.workspaceId, {
|
|
1457
1572
|
runner_id: coderunnerId,
|
|
1458
1573
|
limit,
|
|
@@ -1795,8 +1910,36 @@ async function readOptional(filePath) {
|
|
|
1795
1910
|
return null;
|
|
1796
1911
|
}
|
|
1797
1912
|
}
|
|
1798
|
-
|
|
1913
|
+
var NODE_HANDLER_MISSING = "Function/Job/Schedule Node projects need a handler.js exporting `handler` at the project root (or a package.json \"main\" pointing at an existing file). The platform runs a generated /app/main.js that does require('./handler'); `npm start` is not run for these workload types. See the runtime docs for the handler signature.";
|
|
1914
|
+
async function exists(filePath) {
|
|
1915
|
+
try {
|
|
1916
|
+
return (await fs3.stat(filePath)).isFile();
|
|
1917
|
+
} catch {
|
|
1918
|
+
return false;
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
async function hasHandlerEntry(sourceDir) {
|
|
1922
|
+
if (await exists(path4.join(sourceDir, "handler.js"))) return true;
|
|
1923
|
+
const raw = await readOptional(path4.join(sourceDir, "package.json"));
|
|
1924
|
+
if (raw == null) return false;
|
|
1925
|
+
let main2;
|
|
1926
|
+
try {
|
|
1927
|
+
main2 = JSON.parse(raw).main;
|
|
1928
|
+
} catch {
|
|
1929
|
+
throw new Error("package.json is not valid JSON.");
|
|
1930
|
+
}
|
|
1931
|
+
if (typeof main2 !== "string" || !main2.trim()) return false;
|
|
1932
|
+
const resolved = path4.resolve(sourceDir, main2);
|
|
1933
|
+
const rel = path4.relative(sourceDir, resolved);
|
|
1934
|
+
if (rel.startsWith("..") || path4.isAbsolute(rel)) return false;
|
|
1935
|
+
return exists(resolved);
|
|
1936
|
+
}
|
|
1937
|
+
async function preflightNodePackage(sourceDir, workloadType = "service") {
|
|
1799
1938
|
const dockerfile = await readOptional(path4.join(sourceDir, "Dockerfile"));
|
|
1939
|
+
if (dockerfile == null && workloadType !== "service") {
|
|
1940
|
+
if (!await hasHandlerEntry(sourceDir)) throw new Error(NODE_HANDLER_MISSING);
|
|
1941
|
+
return;
|
|
1942
|
+
}
|
|
1800
1943
|
const needsStartScript = dockerfile == null || dockerfileRunsNpmStart(dockerfile);
|
|
1801
1944
|
if (!needsStartScript) return;
|
|
1802
1945
|
const raw = await readOptional(path4.join(sourceDir, "package.json"));
|
|
@@ -1881,6 +2024,45 @@ function credentialEnvKeys(env) {
|
|
|
1881
2024
|
return Object.keys(env).filter((key) => isCredentialEnvKey(key)).sort();
|
|
1882
2025
|
}
|
|
1883
2026
|
|
|
2027
|
+
// src/deploy/safety.ts
|
|
2028
|
+
function workspaceSource(flag, env = process.env) {
|
|
2029
|
+
if (flag) return "flag";
|
|
2030
|
+
if (env.GROUNDFLOOR_WORKSPACE_ID) return "env";
|
|
2031
|
+
return "saved";
|
|
2032
|
+
}
|
|
2033
|
+
function labelWithId(name, id) {
|
|
2034
|
+
return name?.trim() ? `${name.trim()} (${id})` : id;
|
|
2035
|
+
}
|
|
2036
|
+
function savedDefaultRefusal(workspaceLabel) {
|
|
2037
|
+
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.`;
|
|
2038
|
+
}
|
|
2039
|
+
function priorEnvKeys(coderunner2) {
|
|
2040
|
+
const manifest = coderunner2.manifest;
|
|
2041
|
+
const keys = manifest?.coderunner?.deploy?.env_keys;
|
|
2042
|
+
return Array.isArray(keys) ? keys.map(String) : [];
|
|
2043
|
+
}
|
|
2044
|
+
function envRedeployWarning(priorKeys, sentKeys) {
|
|
2045
|
+
const sent = new Set(sentKeys);
|
|
2046
|
+
const missing = priorKeys.filter((k) => !sent.has(k)).sort();
|
|
2047
|
+
const base = 'Environment variable VALUES are not stored by Groundfloor: every deploy must resend them with -e KEY=VALUE or groundfloor.json "env".';
|
|
2048
|
+
if (priorKeys.length > 0) {
|
|
2049
|
+
if (missing.length === 0) return [];
|
|
2050
|
+
return [
|
|
2051
|
+
`WARNING: this deploy does not resend ${missing.length} variable(s) from the previous deployment: ${missing.join(", ")}.`,
|
|
2052
|
+
" The new deployment will run WITHOUT them. " + base
|
|
2053
|
+
];
|
|
2054
|
+
}
|
|
2055
|
+
if (sentKeys.length === 0) {
|
|
2056
|
+
return [
|
|
2057
|
+
"WARNING: this deploy sends no environment variables. If the running service has any, the new deployment will run WITHOUT them. " + base
|
|
2058
|
+
];
|
|
2059
|
+
}
|
|
2060
|
+
return [];
|
|
2061
|
+
}
|
|
2062
|
+
function credentialEnvMessage(keys) {
|
|
2063
|
+
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.`;
|
|
2064
|
+
}
|
|
2065
|
+
|
|
1884
2066
|
// src/commands/deploy.ts
|
|
1885
2067
|
var FALLBACK_RUNTIMES = /* @__PURE__ */ new Set([
|
|
1886
2068
|
"python",
|
|
@@ -1924,7 +2106,9 @@ function reportFiltering(filtered) {
|
|
|
1924
2106
|
log(
|
|
1925
2107
|
`Excluded ${filtered.excluded.length} credential file(s): ${names(filtered.excluded)}`
|
|
1926
2108
|
);
|
|
1927
|
-
log(
|
|
2109
|
+
log(
|
|
2110
|
+
" (use `--env KEY=VALUE` for non-secret config; read secrets at runtime via an M2M client with `secrets:read`)"
|
|
2111
|
+
);
|
|
1928
2112
|
}
|
|
1929
2113
|
if (filtered.dotfilesIncluded.length) {
|
|
1930
2114
|
log(
|
|
@@ -1932,6 +2116,17 @@ function reportFiltering(filtered) {
|
|
|
1932
2116
|
);
|
|
1933
2117
|
}
|
|
1934
2118
|
}
|
|
2119
|
+
async function environmentLabel(apiUrl, workspaceId, environmentId) {
|
|
2120
|
+
if (typeof environmentId !== "string" || !environmentId) {
|
|
2121
|
+
return "(workspace default)";
|
|
2122
|
+
}
|
|
2123
|
+
try {
|
|
2124
|
+
const envs = await listWorkspaceEnvironments(apiUrl, workspaceId);
|
|
2125
|
+
return labelWithId(envs.find((e) => e.id === environmentId)?.name, environmentId);
|
|
2126
|
+
} catch {
|
|
2127
|
+
return environmentId;
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
1935
2130
|
async function failDeploy(opts) {
|
|
1936
2131
|
throw new Error(await diagnoseDeployFailure(opts));
|
|
1937
2132
|
}
|
|
@@ -1946,6 +2141,8 @@ async function deployCommand(opts) {
|
|
|
1946
2141
|
);
|
|
1947
2142
|
}
|
|
1948
2143
|
const ws = cfg.workspaceId;
|
|
2144
|
+
const source = workspaceSource(opts.workspace);
|
|
2145
|
+
const io = opts.io ?? defaultPromptIO();
|
|
1949
2146
|
let workspaceName;
|
|
1950
2147
|
try {
|
|
1951
2148
|
const workspaces2 = await listWorkspaces(cfg.apiUrl);
|
|
@@ -1960,6 +2157,19 @@ async function deployCommand(opts) {
|
|
|
1960
2157
|
apiUrl: cfg.apiUrl
|
|
1961
2158
|
})
|
|
1962
2159
|
);
|
|
2160
|
+
if (source === "saved") {
|
|
2161
|
+
const target = labelWithId(workspaceName, ws);
|
|
2162
|
+
if (!io.isTTY) {
|
|
2163
|
+
throw new Error(savedDefaultRefusal(target));
|
|
2164
|
+
}
|
|
2165
|
+
await confirmAction({
|
|
2166
|
+
summary: [
|
|
2167
|
+
`No -w/--workspace or GROUNDFLOOR_WORKSPACE_ID given: using the saved default workspace ${target}.`
|
|
2168
|
+
],
|
|
2169
|
+
question: `Deploy to ${target}?`,
|
|
2170
|
+
io
|
|
2171
|
+
});
|
|
2172
|
+
}
|
|
1963
2173
|
log(
|
|
1964
2174
|
opts.git ? `Cloning ${opts.git}${opts.ref ? ` (${opts.ref})` : ""}...` : `Packaging ${path5.resolve(opts.path ?? process.cwd())}...`
|
|
1965
2175
|
);
|
|
@@ -2005,20 +2215,84 @@ async function deployCommand(opts) {
|
|
|
2005
2215
|
}
|
|
2006
2216
|
}
|
|
2007
2217
|
if (runtime === "node") {
|
|
2008
|
-
await preflightNodePackage(pkg.sourceDir);
|
|
2218
|
+
await preflightNodePackage(pkg.sourceDir, workloadType);
|
|
2009
2219
|
}
|
|
2010
2220
|
const schedule = resolveDeploySchedule({
|
|
2011
2221
|
workloadType,
|
|
2012
2222
|
flag: opts.schedule,
|
|
2013
2223
|
manifestSchedule: manifest.schedule
|
|
2014
2224
|
});
|
|
2225
|
+
const envVariable = {
|
|
2226
|
+
...manifest.env ?? {},
|
|
2227
|
+
...parseEnvPairs(opts.env)
|
|
2228
|
+
};
|
|
2229
|
+
if (!opts.skipDeploy) {
|
|
2230
|
+
const plaintextSecrets = credentialEnvKeys(envVariable);
|
|
2231
|
+
if (plaintextSecrets.length) {
|
|
2232
|
+
throw new Error(credentialEnvMessage(plaintextSecrets));
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
const injectSecretKeys = (opts.secret ?? []).map((key) => key.trim()).filter(Boolean);
|
|
2015
2236
|
let coderunnerId = opts.coderunner;
|
|
2237
|
+
if (coderunnerId && !opts.skipDeploy) {
|
|
2238
|
+
try {
|
|
2239
|
+
const known = (await listCoderunners(cfg.apiUrl, ws)).find(
|
|
2240
|
+
(c) => c.id === coderunnerId
|
|
2241
|
+
);
|
|
2242
|
+
if (known) {
|
|
2243
|
+
for (const line of envRedeployWarning(
|
|
2244
|
+
priorEnvKeys(known),
|
|
2245
|
+
Object.keys(envVariable)
|
|
2246
|
+
)) {
|
|
2247
|
+
log(line);
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
} catch {
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
if (opts.appId && coderunnerId) {
|
|
2254
|
+
log(
|
|
2255
|
+
`WARNING: --app-id ${opts.appId} is IGNORED with --coderunner: it only applies when a new coderunner is created.`
|
|
2256
|
+
);
|
|
2257
|
+
}
|
|
2016
2258
|
if (!coderunnerId) {
|
|
2017
2259
|
const name = opts.name ?? manifest.name ?? (opts.git ? nameFromGitUrl(opts.git) : path5.basename(path5.resolve(opts.path ?? process.cwd())));
|
|
2018
2260
|
const slug = manifest.slug ?? slugify(name);
|
|
2019
2261
|
const existing = await listCoderunners(cfg.apiUrl, ws);
|
|
2020
2262
|
const match = existing.find((c) => c.slug === slug || c.name === name);
|
|
2021
2263
|
if (match) {
|
|
2264
|
+
const lines = [
|
|
2265
|
+
`A coderunner named "${match.name ?? match.slug ?? name}" already exists and will be UPDATED (a new version replaces the live deployment):`,
|
|
2266
|
+
` name: ${match.name ?? match.slug ?? name}`,
|
|
2267
|
+
` id: ${match.id}`,
|
|
2268
|
+
` environment: ${await environmentLabel(cfg.apiUrl, ws, match.environment_id)}`,
|
|
2269
|
+
` workspace: ${labelWithId(workspaceName, ws)}`
|
|
2270
|
+
];
|
|
2271
|
+
if (opts.appId) {
|
|
2272
|
+
lines.push(
|
|
2273
|
+
`WARNING: --app-id ${opts.appId} is IGNORED: it only applies when a new coderunner is created.`
|
|
2274
|
+
);
|
|
2275
|
+
}
|
|
2276
|
+
if (opts.workloadType && match.workload_type && opts.workloadType !== match.workload_type) {
|
|
2277
|
+
lines.push(
|
|
2278
|
+
`WARNING: --workload-type ${opts.workloadType} is ignored; the existing coderunner is ${match.workload_type}.`
|
|
2279
|
+
);
|
|
2280
|
+
}
|
|
2281
|
+
if (!opts.skipDeploy) {
|
|
2282
|
+
lines.push(
|
|
2283
|
+
...envRedeployWarning(priorEnvKeys(match), Object.keys(envVariable))
|
|
2284
|
+
);
|
|
2285
|
+
}
|
|
2286
|
+
lines.push(
|
|
2287
|
+
" (pass -c <id> to target a specific coderunner, or -n <name> to create a different one)"
|
|
2288
|
+
);
|
|
2289
|
+
await confirmAction({
|
|
2290
|
+
summary: lines,
|
|
2291
|
+
yes: opts.yes,
|
|
2292
|
+
bypassHint: "--yes",
|
|
2293
|
+
question: "Update this coderunner?",
|
|
2294
|
+
io
|
|
2295
|
+
});
|
|
2022
2296
|
coderunnerId = match.id;
|
|
2023
2297
|
log(`Using existing coderunner ${slug} (${coderunnerId})`);
|
|
2024
2298
|
} else {
|
|
@@ -2105,26 +2379,26 @@ async function deployCommand(opts) {
|
|
|
2105
2379
|
);
|
|
2106
2380
|
return;
|
|
2107
2381
|
}
|
|
2108
|
-
const envVariable = {
|
|
2109
|
-
...manifest.env ?? {},
|
|
2110
|
-
...parseEnvPairs(opts.env)
|
|
2111
|
-
};
|
|
2112
|
-
const plaintextSecrets = credentialEnvKeys(envVariable);
|
|
2113
|
-
if (plaintextSecrets.length) {
|
|
2114
|
-
throw new Error(
|
|
2115
|
-
`Do not pass secrets as --env: ${plaintextSecrets.join(", ")}. Store them with \`gf secrets set KEY\` and deploy with \`--secret KEY\`.`
|
|
2116
|
-
);
|
|
2117
|
-
}
|
|
2118
|
-
const injectSecretKeys = (opts.secret ?? []).map((key) => key.trim()).filter(Boolean);
|
|
2119
2382
|
log("Deploying...");
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2383
|
+
let result;
|
|
2384
|
+
try {
|
|
2385
|
+
result = await deployCoderunner(cfg.apiUrl, ws, coderunnerId, {
|
|
2386
|
+
version_id: uploaded.version_id,
|
|
2387
|
+
cpu: opts.cpu ?? manifest.cpu,
|
|
2388
|
+
memory: opts.memory ?? manifest.memory,
|
|
2389
|
+
envVariable: Object.keys(envVariable).length ? envVariable : void 0,
|
|
2390
|
+
schedule,
|
|
2391
|
+
inject_secret_keys: injectSecretKeys.length ? injectSecretKeys : void 0
|
|
2392
|
+
});
|
|
2393
|
+
} catch (err) {
|
|
2394
|
+
if (err instanceof CpError && err.status === 422 && injectSecretKeys.length) {
|
|
2395
|
+
throw new Error(
|
|
2396
|
+
`${err.message}
|
|
2397
|
+
--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\`).`
|
|
2398
|
+
);
|
|
2399
|
+
}
|
|
2400
|
+
throw err;
|
|
2401
|
+
}
|
|
2128
2402
|
log("Waiting for deployment...");
|
|
2129
2403
|
const deployDeadline = Date.now() + DEPLOY_POLL_TIMEOUT_MS;
|
|
2130
2404
|
let deployOk = false;
|
|
@@ -2221,11 +2495,6 @@ async function runCommand(opts) {
|
|
|
2221
2495
|
coderunnerId,
|
|
2222
2496
|
payload
|
|
2223
2497
|
);
|
|
2224
|
-
if (opts.json) {
|
|
2225
|
-
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
2226
|
-
`);
|
|
2227
|
-
return;
|
|
2228
|
-
}
|
|
2229
2498
|
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
2230
2499
|
`);
|
|
2231
2500
|
}
|
|
@@ -2269,903 +2538,887 @@ async function statusCommand(opts) {
|
|
|
2269
2538
|
process.stdout.write(`deployment: ${deployment.id ?? "\u2014"} [${deployment.status}]
|
|
2270
2539
|
`);
|
|
2271
2540
|
}
|
|
2272
|
-
process.stdout.write(`
|
|
2273
|
-
${JSON.stringify(out, null, 2)}
|
|
2274
|
-
`);
|
|
2275
2541
|
}
|
|
2276
2542
|
|
|
2277
|
-
// src/commands/
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
throw new Error(
|
|
2289
|
-
"Specify the coderunner with --coderunner <id|slug>."
|
|
2290
|
-
);
|
|
2291
|
-
}
|
|
2292
|
-
let coderunnerId = ref;
|
|
2293
|
-
if (!UUID_RE.test(ref)) {
|
|
2294
|
-
const all = await listCoderunners(cfg.apiUrl, cfg.workspaceId);
|
|
2295
|
-
const match = all.find((c) => c.slug === ref || c.name === ref);
|
|
2296
|
-
if (!match) {
|
|
2297
|
-
throw new Error(`No coderunner matching "${ref}" in this workspace.`);
|
|
2298
|
-
}
|
|
2299
|
-
coderunnerId = match.id;
|
|
2300
|
-
}
|
|
2301
|
-
return {
|
|
2302
|
-
apiUrl: cfg.apiUrl,
|
|
2303
|
-
workspaceId: cfg.workspaceId,
|
|
2304
|
-
coderunnerId
|
|
2305
|
-
};
|
|
2543
|
+
// src/commands/apps.ts
|
|
2544
|
+
import { promises as fs5 } from "fs";
|
|
2545
|
+
import path6 from "path";
|
|
2546
|
+
|
|
2547
|
+
// src/release-zip.ts
|
|
2548
|
+
import AdmZip2 from "adm-zip";
|
|
2549
|
+
var REMOTE_ENTRY = "remoteEntry.js";
|
|
2550
|
+
var FLATTEN_SUFFIXES = [".js", ".mjs", ".cjs", ".css", ".map", ".wasm"];
|
|
2551
|
+
var IMPORT_ASSETS = /(__federation_import|import)\((['"])\.\/assets\//g;
|
|
2552
|
+
function rewriteRemoteEntryAssetPrefix(source) {
|
|
2553
|
+
return source.replace(IMPORT_ASSETS, "$1($2./");
|
|
2306
2554
|
}
|
|
2307
|
-
function
|
|
2308
|
-
|
|
2309
|
-
out.write(`
|
|
2310
|
-
Added ${item.domain} (status: ${item.status})
|
|
2311
|
-
`);
|
|
2312
|
-
out.write("\nCreate these DNS records at your registrar:\n");
|
|
2313
|
-
if (item.cname_target) {
|
|
2314
|
-
out.write(` CNAME ${item.domain} \u2192 ${item.cname_target}
|
|
2315
|
-
`);
|
|
2316
|
-
}
|
|
2317
|
-
if (item.verification_record) {
|
|
2318
|
-
const r = item.verification_record;
|
|
2319
|
-
out.write(` ${r.type} ${r.name} = ${r.value}
|
|
2320
|
-
`);
|
|
2321
|
-
}
|
|
2322
|
-
out.write(`
|
|
2323
|
-
Then run: gf domains verify ${item.domain} --coderunner <id|slug>
|
|
2324
|
-
`);
|
|
2555
|
+
function normalizeZipEntryName(filename) {
|
|
2556
|
+
return filename.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
2325
2557
|
}
|
|
2326
|
-
|
|
2327
|
-
const
|
|
2328
|
-
|
|
2329
|
-
if (opts.json) {
|
|
2330
|
-
process.stdout.write(`${JSON.stringify(item, null, 2)}
|
|
2331
|
-
`);
|
|
2332
|
-
return;
|
|
2333
|
-
}
|
|
2334
|
-
printInstructions(item);
|
|
2558
|
+
function parentDir(rel) {
|
|
2559
|
+
const i = rel.lastIndexOf("/");
|
|
2560
|
+
return i === -1 ? "" : rel.slice(0, i);
|
|
2335
2561
|
}
|
|
2336
|
-
|
|
2337
|
-
const
|
|
2338
|
-
|
|
2339
|
-
if (opts.json) {
|
|
2340
|
-
process.stdout.write(`${JSON.stringify(result.domains, null, 2)}
|
|
2341
|
-
`);
|
|
2342
|
-
return;
|
|
2343
|
-
}
|
|
2344
|
-
if (result.domains.length === 0) {
|
|
2345
|
-
process.stdout.write("No custom domains.\n");
|
|
2346
|
-
return;
|
|
2347
|
-
}
|
|
2348
|
-
for (const d of result.domains) {
|
|
2349
|
-
const target = d.cname_target ? ` \u2192 ${d.cname_target}` : "";
|
|
2350
|
-
process.stdout.write(`${d.domain} [${d.status}]${target}
|
|
2351
|
-
`);
|
|
2352
|
-
}
|
|
2562
|
+
function basename(rel) {
|
|
2563
|
+
const i = rel.lastIndexOf("/");
|
|
2564
|
+
return i === -1 ? rel : rel.slice(i + 1);
|
|
2353
2565
|
}
|
|
2354
|
-
|
|
2355
|
-
const
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2566
|
+
function chooseRemoteEntry(relpaths) {
|
|
2567
|
+
const entries = relpaths.filter(
|
|
2568
|
+
(p) => p === REMOTE_ENTRY || p.endsWith(`/${REMOTE_ENTRY}`)
|
|
2569
|
+
);
|
|
2570
|
+
if (entries.length === 0) return void 0;
|
|
2571
|
+
if (entries.includes(REMOTE_ENTRY)) return REMOTE_ENTRY;
|
|
2572
|
+
return [...entries].sort((a, b) => {
|
|
2573
|
+
const score = (p) => {
|
|
2574
|
+
const parts = p.split("/");
|
|
2575
|
+
const inAssets = parts.length >= 2 && parts[parts.length - 2] === "assets";
|
|
2576
|
+
return `${inAssets ? 0 : 1}:${parts.length}:${p}`;
|
|
2577
|
+
};
|
|
2578
|
+
return score(a).localeCompare(score(b));
|
|
2579
|
+
})[0];
|
|
2359
2580
|
}
|
|
2360
|
-
|
|
2361
|
-
const
|
|
2362
|
-
const
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2581
|
+
function flattenFederatedBundlePlan(relpaths) {
|
|
2582
|
+
const paths2 = relpaths.map(normalizeZipEntryName).filter((name) => name && !name.endsWith("/") && !name.split("/").includes(".."));
|
|
2583
|
+
const pathset = new Set(paths2);
|
|
2584
|
+
const entry = chooseRemoteEntry([...pathset]);
|
|
2585
|
+
if (!entry) return {};
|
|
2586
|
+
const promoteDirs = /* @__PURE__ */ new Set();
|
|
2587
|
+
const entryDir = parentDir(entry);
|
|
2588
|
+
if (entryDir) promoteDirs.add(entryDir);
|
|
2589
|
+
for (const path11 of pathset) {
|
|
2590
|
+
const parent = parentDir(path11);
|
|
2591
|
+
if (parent === "assets" || parent.endsWith("/assets") && parent.split("/").length - 1 < 2) {
|
|
2592
|
+
promoteDirs.add(parent);
|
|
2593
|
+
}
|
|
2367
2594
|
}
|
|
2368
|
-
const
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2595
|
+
const destToSrc = {};
|
|
2596
|
+
for (const src of paths2) {
|
|
2597
|
+
const parent = parentDir(src);
|
|
2598
|
+
if (!promoteDirs.has(parent)) continue;
|
|
2599
|
+
const dest = basename(src);
|
|
2600
|
+
if (dest === "release.zip") continue;
|
|
2601
|
+
const lower = dest.toLowerCase();
|
|
2602
|
+
if (dest !== REMOTE_ENTRY && !FLATTEN_SUFFIXES.some((ext) => lower.endsWith(ext))) {
|
|
2603
|
+
continue;
|
|
2604
|
+
}
|
|
2605
|
+
if (pathset.has(dest)) continue;
|
|
2606
|
+
const prev = destToSrc[dest];
|
|
2607
|
+
if (prev === void 0 || src.split("/").length < prev.split("/").length) {
|
|
2608
|
+
destToSrc[dest] = src;
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
return destToSrc;
|
|
2612
|
+
}
|
|
2613
|
+
function flattenFederatedReleaseZip(bytes) {
|
|
2614
|
+
const zip = new AdmZip2(bytes);
|
|
2615
|
+
const rels = [];
|
|
2616
|
+
const byNorm = /* @__PURE__ */ new Map();
|
|
2617
|
+
for (const entry of zip.getEntries()) {
|
|
2618
|
+
if (entry.isDirectory) continue;
|
|
2619
|
+
const name = normalizeZipEntryName(entry.entryName);
|
|
2620
|
+
rels.push(name);
|
|
2621
|
+
byNorm.set(name, entry);
|
|
2622
|
+
}
|
|
2623
|
+
const plan = flattenFederatedBundlePlan(rels);
|
|
2624
|
+
const keys = Object.keys(plan);
|
|
2625
|
+
if (keys.length > 0) {
|
|
2626
|
+
for (const dest of keys) {
|
|
2627
|
+
const src = plan[dest];
|
|
2628
|
+
const source = byNorm.get(src);
|
|
2629
|
+
if (!source) continue;
|
|
2630
|
+
zip.addFile(dest, source.getData());
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
rewriteRemoteEntryInZip(zip);
|
|
2634
|
+
return zip.toBuffer();
|
|
2635
|
+
}
|
|
2636
|
+
function rewriteRemoteEntryInZip(zip) {
|
|
2637
|
+
const names2 = zip.getEntries().filter((entry) => !entry.isDirectory).map((entry) => normalizeZipEntryName(entry.entryName));
|
|
2638
|
+
const entryRel = chooseRemoteEntry(names2);
|
|
2639
|
+
if (!entryRel) return;
|
|
2640
|
+
const zipEntry = zip.getEntries().find((item) => {
|
|
2641
|
+
if (item.isDirectory) return false;
|
|
2642
|
+
return normalizeZipEntryName(item.entryName) === entryRel;
|
|
2643
|
+
});
|
|
2644
|
+
if (!zipEntry) return;
|
|
2645
|
+
const before = zipEntry.getData().toString("utf8");
|
|
2646
|
+
const after = rewriteRemoteEntryAssetPrefix(before);
|
|
2647
|
+
if (after === before) return;
|
|
2648
|
+
zip.updateFile(zipEntry.entryName, Buffer.from(after, "utf8"));
|
|
2649
|
+
}
|
|
2650
|
+
function federatedReleaseZipHint(slug) {
|
|
2651
|
+
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)}`;
|
|
2652
|
+
}
|
|
2653
|
+
function federationRemoteName(appId) {
|
|
2654
|
+
const sanitized = appId.replace(/[^a-zA-Z0-9_$]/g, "_");
|
|
2655
|
+
return sanitized.length > 0 ? sanitized : "federatedApp";
|
|
2656
|
+
}
|
|
2657
|
+
function initThenPublishHint(slug) {
|
|
2658
|
+
const s = (slug ?? "").trim() || "<portal-slug>";
|
|
2659
|
+
return `gf apps init --slug ${s} && cd ${s} && npm install && npm run release && gf apps publish --path release.zip`;
|
|
2660
|
+
}
|
|
2661
|
+
function extractReferencedSiblings(remoteEntrySource) {
|
|
2662
|
+
const names2 = /* @__PURE__ */ new Set();
|
|
2663
|
+
for (const match of remoteEntrySource.matchAll(
|
|
2664
|
+
/["']\.\/(?:assets\/)?([^"'/]+\.(?:js|mjs|cjs|css))["']/g
|
|
2665
|
+
)) {
|
|
2666
|
+
if (match[1] !== REMOTE_ENTRY) names2.add(match[1]);
|
|
2667
|
+
}
|
|
2668
|
+
for (const match of remoteEntrySource.matchAll(
|
|
2669
|
+
/["']([A-Za-z0-9._-]+\.css)["']/g
|
|
2670
|
+
)) {
|
|
2671
|
+
names2.add(match[1]);
|
|
2672
|
+
}
|
|
2673
|
+
return [...names2];
|
|
2674
|
+
}
|
|
2675
|
+
function extractFederationScope(remoteEntrySource) {
|
|
2676
|
+
return remoteEntrySource.match(/css__([A-Za-z0-9_-]+)__/)?.[1];
|
|
2677
|
+
}
|
|
2678
|
+
function zipRootNames(bytes) {
|
|
2679
|
+
const zip = new AdmZip2(bytes);
|
|
2680
|
+
return zip.getEntries().filter((entry) => !entry.isDirectory).map((entry) => normalizeZipEntryName(entry.entryName));
|
|
2681
|
+
}
|
|
2682
|
+
function readZipFile(bytes, rel) {
|
|
2683
|
+
const zip = new AdmZip2(bytes);
|
|
2684
|
+
const entry = zip.getEntries().find((item) => {
|
|
2685
|
+
if (item.isDirectory) return false;
|
|
2686
|
+
return normalizeZipEntryName(item.entryName) === rel;
|
|
2687
|
+
});
|
|
2688
|
+
return entry ? entry.getData().toString("utf8") : void 0;
|
|
2689
|
+
}
|
|
2690
|
+
function assertFederatedReleaseZip(bytes, slug) {
|
|
2691
|
+
const hint = initThenPublishHint(slug);
|
|
2692
|
+
const names2 = zipRootNames(bytes);
|
|
2693
|
+
const present = new Set(names2.map((name) => basename(name)));
|
|
2694
|
+
const entryRel = chooseRemoteEntry(names2);
|
|
2695
|
+
if (!entryRel) {
|
|
2696
|
+
throw new Error(`Zip has no remoteEntry.js. ${hint}`);
|
|
2697
|
+
}
|
|
2698
|
+
const source = readZipFile(bytes, entryRel);
|
|
2699
|
+
if (!source) {
|
|
2700
|
+
throw new Error(`Zip has no remoteEntry.js. ${hint}`);
|
|
2701
|
+
}
|
|
2702
|
+
const siblings = extractReferencedSiblings(source);
|
|
2703
|
+
const missing = siblings.filter((name) => !present.has(name));
|
|
2704
|
+
const jsOrCssAtRoot = names2.filter((name) => {
|
|
2705
|
+
if (name.includes("/")) return false;
|
|
2706
|
+
const lower = name.toLowerCase();
|
|
2707
|
+
return lower.endsWith(".js") || lower.endsWith(".mjs") || lower.endsWith(".css");
|
|
2708
|
+
});
|
|
2709
|
+
if (missing.length > 0 || siblings.length === 0 && jsOrCssAtRoot.length < 2) {
|
|
2710
|
+
const detail = missing.length > 0 ? `Missing next to remoteEntry.js: ${missing.join(", ")}.` : "Zip is only remoteEntry.js (Vite chunks were never packed).";
|
|
2711
|
+
throw new Error(
|
|
2712
|
+
`${detail} Do not zip dist/ or upload a lone remoteEntry.js. ${hint}`
|
|
2713
|
+
);
|
|
2714
|
+
}
|
|
2715
|
+
const scope = extractFederationScope(source);
|
|
2716
|
+
if (slug && scope) {
|
|
2717
|
+
const expected = federationRemoteName(slug);
|
|
2718
|
+
if (scope !== expected) {
|
|
2719
|
+
throw new Error(
|
|
2720
|
+
`Federation name is "${scope}" but Shell looks up "${expected}" (from slug ${slug}). Use the starter-kit vite.config (federationRemoteName). ${hint}`
|
|
2721
|
+
);
|
|
2722
|
+
}
|
|
2375
2723
|
}
|
|
2376
2724
|
}
|
|
2377
2725
|
|
|
2378
|
-
// src/commands/
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2726
|
+
// src/commands/apps.ts
|
|
2727
|
+
import AdmZip3 from "adm-zip";
|
|
2728
|
+
var MAX_RELEASE_BYTES = 32 * 1024 * 1024;
|
|
2729
|
+
function slugify2(input) {
|
|
2730
|
+
const slug = input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
2731
|
+
return slug || "app";
|
|
2732
|
+
}
|
|
2733
|
+
function formatApp(app) {
|
|
2734
|
+
return `${app.id} ${app.name ?? app.slug ?? ""} ${app.app_kind ?? ""} [${app.status ?? ""}]`;
|
|
2735
|
+
}
|
|
2736
|
+
async function readJsonObject(filePath) {
|
|
2737
|
+
const raw = await fs5.readFile(filePath, "utf8");
|
|
2738
|
+
const parsed = JSON.parse(raw);
|
|
2739
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2740
|
+
throw new Error(`${filePath} must contain a JSON object`);
|
|
2382
2741
|
}
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2742
|
+
return parsed;
|
|
2743
|
+
}
|
|
2744
|
+
async function maybeReadManifest(filePath) {
|
|
2745
|
+
if (!filePath) return void 0;
|
|
2746
|
+
return readJsonObject(path6.resolve(filePath));
|
|
2747
|
+
}
|
|
2748
|
+
async function findSiblingManifest(bundlePath) {
|
|
2749
|
+
const dir = await isDirectory(bundlePath) ? bundlePath : path6.dirname(bundlePath);
|
|
2750
|
+
const candidate = path6.join(dir, "groundfloor.manifest.json");
|
|
2751
|
+
try {
|
|
2752
|
+
await fs5.access(candidate);
|
|
2753
|
+
} catch {
|
|
2754
|
+
return void 0;
|
|
2755
|
+
}
|
|
2756
|
+
return { path: candidate, manifest: await readJsonObject(candidate) };
|
|
2757
|
+
}
|
|
2758
|
+
function zipHasRemoteEntry(bytes) {
|
|
2759
|
+
const zip = new AdmZip3(bytes);
|
|
2760
|
+
return zip.getEntries().some((entry) => {
|
|
2761
|
+
if (entry.isDirectory) return false;
|
|
2762
|
+
const name = normalizeZipEntryName(entry.entryName);
|
|
2763
|
+
return name === REMOTE_ENTRY || name.endsWith(`/${REMOTE_ENTRY}`);
|
|
2764
|
+
});
|
|
2765
|
+
}
|
|
2766
|
+
function prepareReleaseZip(bytes, slug) {
|
|
2767
|
+
if (!zipHasRemoteEntry(bytes)) {
|
|
2768
|
+
throw new Error(
|
|
2769
|
+
`Zip has no remoteEntry.js. ${initThenPublishHint(slug)}`
|
|
2770
|
+
);
|
|
2771
|
+
}
|
|
2772
|
+
const flattened = flattenFederatedReleaseZip(bytes);
|
|
2773
|
+
const plan = flattenFederatedBundlePlan(
|
|
2774
|
+
new AdmZip3(bytes).getEntries().filter((e) => !e.isDirectory).map((e) => normalizeZipEntryName(e.entryName))
|
|
2775
|
+
);
|
|
2776
|
+
if (Object.keys(plan).length > 0) {
|
|
2777
|
+
process.stderr.write(
|
|
2778
|
+
`Note: flattened Vite dist/assets layout so chunks sit next to remoteEntry.js.
|
|
2779
|
+
Prefer: ${federatedReleaseZipHint(slug)}
|
|
2780
|
+
`
|
|
2781
|
+
);
|
|
2782
|
+
}
|
|
2783
|
+
assertFederatedReleaseZip(flattened, slug);
|
|
2784
|
+
return flattened;
|
|
2785
|
+
}
|
|
2786
|
+
async function isDirectory(filePath) {
|
|
2787
|
+
try {
|
|
2788
|
+
return (await fs5.stat(filePath)).isDirectory();
|
|
2789
|
+
} catch {
|
|
2790
|
+
return false;
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
async function resolveAppId(apiUrl, workspaceId, idOrSlug) {
|
|
2794
|
+
const listed = await listApps(apiUrl, workspaceId);
|
|
2795
|
+
const match = listed.apps.find(
|
|
2796
|
+
(app) => app.id === idOrSlug || app.slug === idOrSlug
|
|
2797
|
+
);
|
|
2798
|
+
if (match) return match;
|
|
2799
|
+
try {
|
|
2800
|
+
return await getApp(apiUrl, workspaceId, idOrSlug);
|
|
2801
|
+
} catch (err) {
|
|
2802
|
+
if (err instanceof CpError && err.status === 404) {
|
|
2803
|
+
throw new Error(
|
|
2804
|
+
`App not found: ${idOrSlug}. Run \`gf apps ls\` or \`gf apps create\`.`
|
|
2805
|
+
);
|
|
2387
2806
|
}
|
|
2388
|
-
|
|
2807
|
+
throw err;
|
|
2389
2808
|
}
|
|
2390
|
-
return value;
|
|
2391
2809
|
}
|
|
2392
|
-
async function
|
|
2810
|
+
async function appsListCommand(opts) {
|
|
2393
2811
|
const cfg = await requireWorkspace(opts.workspace);
|
|
2394
|
-
const data = await
|
|
2812
|
+
const data = await listApps(cfg.apiUrl, cfg.workspaceId);
|
|
2395
2813
|
if (opts.json) {
|
|
2396
2814
|
process.stdout.write(`${JSON.stringify(data, null, 2)}
|
|
2397
2815
|
`);
|
|
2398
2816
|
return;
|
|
2399
2817
|
}
|
|
2400
|
-
if (data.
|
|
2401
|
-
process.stdout.write("No
|
|
2818
|
+
if (data.apps.length === 0) {
|
|
2819
|
+
process.stdout.write("No apps in this workspace.\n");
|
|
2402
2820
|
return;
|
|
2403
2821
|
}
|
|
2404
|
-
for (const
|
|
2405
|
-
process.stdout.write(`${
|
|
2822
|
+
for (const app of data.apps) {
|
|
2823
|
+
process.stdout.write(`${formatApp(app)}
|
|
2406
2824
|
`);
|
|
2407
2825
|
}
|
|
2408
2826
|
}
|
|
2409
|
-
async function
|
|
2827
|
+
async function appsGetCommand(appId, opts) {
|
|
2410
2828
|
const cfg = await requireWorkspace(opts.workspace);
|
|
2411
|
-
const
|
|
2412
|
-
cfg.apiUrl,
|
|
2413
|
-
cfg.workspaceId,
|
|
2414
|
-
key,
|
|
2415
|
-
opts.environment
|
|
2416
|
-
);
|
|
2829
|
+
const app = await resolveAppId(cfg.apiUrl, cfg.workspaceId, appId);
|
|
2417
2830
|
if (opts.json) {
|
|
2418
|
-
process.stdout.write(`${JSON.stringify(
|
|
2831
|
+
process.stdout.write(`${JSON.stringify(app, null, 2)}
|
|
2419
2832
|
`);
|
|
2420
2833
|
return;
|
|
2421
2834
|
}
|
|
2422
|
-
process.stdout.write(`${
|
|
2835
|
+
process.stdout.write(`${formatApp(app)}
|
|
2423
2836
|
`);
|
|
2424
2837
|
}
|
|
2425
|
-
async function
|
|
2838
|
+
async function appsCreateCommand(opts) {
|
|
2426
2839
|
const cfg = await requireWorkspace(opts.workspace);
|
|
2427
|
-
const
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
{ value: resolved, description: opts.description },
|
|
2433
|
-
opts.environment
|
|
2434
|
-
);
|
|
2435
|
-
process.stdout.write(`Saved secret ${saved.key}
|
|
2436
|
-
`);
|
|
2437
|
-
}
|
|
2438
|
-
async function secretsRmCommand(key, opts) {
|
|
2439
|
-
const cfg = await requireWorkspace(opts.workspace);
|
|
2440
|
-
await deleteSecret(cfg.apiUrl, cfg.workspaceId, key, opts.environment);
|
|
2441
|
-
process.stdout.write(`Deleted secret ${key}
|
|
2442
|
-
`);
|
|
2443
|
-
}
|
|
2444
|
-
|
|
2445
|
-
// src/commands/flags.ts
|
|
2446
|
-
var VALUE_TYPES = [
|
|
2447
|
-
"boolean",
|
|
2448
|
-
"string",
|
|
2449
|
-
"number",
|
|
2450
|
-
"json"
|
|
2451
|
-
];
|
|
2452
|
-
var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
2453
|
-
function matchEnvironment(environments2, idOrName) {
|
|
2454
|
-
const wanted = idOrName.trim();
|
|
2455
|
-
const byId = environments2.find((env) => env.id === wanted);
|
|
2456
|
-
if (byId) return byId.id;
|
|
2457
|
-
const byName = environments2.filter(
|
|
2458
|
-
(env) => env.name.toLowerCase() === wanted.toLowerCase()
|
|
2459
|
-
);
|
|
2460
|
-
if (byName.length === 1) return byName[0].id;
|
|
2461
|
-
const names2 = environments2.map((env) => env.name).join(", ") || "none";
|
|
2462
|
-
throw new Error(
|
|
2463
|
-
`Environment "${wanted}" not found in this workspace (have: ${names2}). Run \`gf environments ls\`.`
|
|
2464
|
-
);
|
|
2465
|
-
}
|
|
2466
|
-
async function resolveEnvironment(apiUrl, workspaceId, idOrName) {
|
|
2467
|
-
if (!idOrName) return void 0;
|
|
2468
|
-
if (UUID_RE2.test(idOrName.trim())) return idOrName.trim();
|
|
2469
|
-
const environments2 = await listWorkspaceEnvironments(apiUrl, workspaceId);
|
|
2470
|
-
return matchEnvironment(environments2, idOrName);
|
|
2471
|
-
}
|
|
2472
|
-
function parseFlagValue(raw, type) {
|
|
2473
|
-
if (type !== void 0 && !VALUE_TYPES.includes(type)) {
|
|
2474
|
-
throw new Error(`--type must be one of: ${VALUE_TYPES.join(", ")}`);
|
|
2840
|
+
const kindRaw = (opts.kind ?? "shell_federated").trim();
|
|
2841
|
+
if (kindRaw === "coderunner") {
|
|
2842
|
+
throw new Error(
|
|
2843
|
+
"Do not create app_kind=coderunner. Deploy workloads with `gf deploy`. Use --kind shell_federated or --kind standalone."
|
|
2844
|
+
);
|
|
2475
2845
|
}
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2846
|
+
if (kindRaw !== "shell_federated" && kindRaw !== "standalone") {
|
|
2847
|
+
throw new Error(
|
|
2848
|
+
`--kind must be shell_federated or standalone (got ${kindRaw})`
|
|
2849
|
+
);
|
|
2850
|
+
}
|
|
2851
|
+
const kind = kindRaw;
|
|
2852
|
+
const localManifest = await maybeReadManifest(opts.manifest);
|
|
2853
|
+
const name = opts.name?.trim() || (typeof localManifest?.name === "string" ? localManifest.name : "");
|
|
2854
|
+
const slug = opts.slug?.trim() || (typeof localManifest?.appId === "string" ? localManifest.appId : name ? slugify2(name) : "");
|
|
2855
|
+
if (!name) {
|
|
2856
|
+
throw new Error("Pass --name, or a manifest with a name field.");
|
|
2857
|
+
}
|
|
2858
|
+
if (!slug) {
|
|
2859
|
+
throw new Error("Pass --slug, or a manifest with appId.");
|
|
2860
|
+
}
|
|
2861
|
+
if (kind === "standalone" && !opts.primaryCoderunner?.trim()) {
|
|
2862
|
+
throw new Error(
|
|
2863
|
+
"Standalone apps require --primary-coderunner <id> (an existing service coderunner)."
|
|
2864
|
+
);
|
|
2865
|
+
}
|
|
2866
|
+
const body = {
|
|
2867
|
+
name,
|
|
2868
|
+
slug,
|
|
2869
|
+
app_kind: kind
|
|
2870
|
+
};
|
|
2871
|
+
if (opts.environment) body.environment_id = opts.environment;
|
|
2872
|
+
if (opts.primaryCoderunner) {
|
|
2873
|
+
body.primary_coderunner_id = opts.primaryCoderunner;
|
|
2874
|
+
}
|
|
2875
|
+
if (localManifest) body.manifest = localManifest;
|
|
2876
|
+
let app;
|
|
2877
|
+
try {
|
|
2878
|
+
app = await createApp(cfg.apiUrl, cfg.workspaceId, body);
|
|
2879
|
+
} catch (err) {
|
|
2880
|
+
if (err instanceof CpError && err.status === 409) {
|
|
2881
|
+
throw new Error(
|
|
2882
|
+
`App slug ${slug} already exists. Init the kit if you do not have it, then publish:
|
|
2883
|
+
${initThenPublishHint(slug)}`
|
|
2884
|
+
);
|
|
2490
2885
|
}
|
|
2491
|
-
|
|
2492
|
-
try {
|
|
2493
|
-
return { value: JSON.parse(raw), value_type: "json" };
|
|
2494
|
-
} catch {
|
|
2495
|
-
throw new Error("--type json needs a valid JSON value");
|
|
2496
|
-
}
|
|
2497
|
-
default:
|
|
2498
|
-
return { value: raw, value_type: "string" };
|
|
2886
|
+
throw err;
|
|
2499
2887
|
}
|
|
2500
|
-
}
|
|
2501
|
-
function formatValue(value) {
|
|
2502
|
-
return typeof value === "string" ? value : JSON.stringify(value);
|
|
2503
|
-
}
|
|
2504
|
-
async function flagsListCommand(opts) {
|
|
2505
|
-
const cfg = await requireWorkspace(opts.workspace);
|
|
2506
|
-
const env = await resolveEnvironment(
|
|
2507
|
-
cfg.apiUrl,
|
|
2508
|
-
cfg.workspaceId,
|
|
2509
|
-
opts.environment
|
|
2510
|
-
);
|
|
2511
|
-
const data = await listFlags(cfg.apiUrl, cfg.workspaceId, env);
|
|
2512
2888
|
if (opts.json) {
|
|
2513
|
-
process.stdout.write(`${JSON.stringify(
|
|
2889
|
+
process.stdout.write(`${JSON.stringify(app, null, 2)}
|
|
2514
2890
|
`);
|
|
2515
2891
|
return;
|
|
2516
2892
|
}
|
|
2517
|
-
process.
|
|
2893
|
+
process.stdout.write(`Created ${formatApp(app)}
|
|
2518
2894
|
`);
|
|
2519
|
-
if (
|
|
2520
|
-
process.stdout.write("No flags in this environment.\n");
|
|
2521
|
-
return;
|
|
2522
|
-
}
|
|
2523
|
-
for (const flag of data.flags) {
|
|
2895
|
+
if (kind === "shell_federated") {
|
|
2524
2896
|
process.stdout.write(
|
|
2525
|
-
|
|
2897
|
+
`Next:
|
|
2898
|
+
gf apps init --slug ${slug}
|
|
2899
|
+
cd ${slug} && npm install && npm run release
|
|
2900
|
+
gf apps publish --path release.zip
|
|
2901
|
+
Do not scaffold src/App.tsx from scratch \u2014 BrowserRouter in App.tsx crashes the Shell.
|
|
2526
2902
|
`
|
|
2527
2903
|
);
|
|
2528
2904
|
}
|
|
2529
2905
|
}
|
|
2530
|
-
async function
|
|
2906
|
+
async function appsPublishCommand(appRef, opts) {
|
|
2531
2907
|
const cfg = await requireWorkspace(opts.workspace);
|
|
2532
|
-
const
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
return;
|
|
2908
|
+
const inputPath = path6.resolve(opts.path ?? "release.zip");
|
|
2909
|
+
const bundlePath = await isDirectory(inputPath) ? path6.join(inputPath, "release.zip") : inputPath;
|
|
2910
|
+
let bytes;
|
|
2911
|
+
try {
|
|
2912
|
+
bytes = await fs5.readFile(bundlePath);
|
|
2913
|
+
} catch {
|
|
2914
|
+
throw new Error(
|
|
2915
|
+
`No bundle at ${bundlePath}. ${initThenPublishHint()}`
|
|
2916
|
+
);
|
|
2542
2917
|
}
|
|
2543
|
-
|
|
2544
|
-
`);
|
|
2545
|
-
}
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2918
|
+
if (bytes.byteLength === 0) {
|
|
2919
|
+
throw new Error(`Bundle is empty: ${bundlePath}`);
|
|
2920
|
+
}
|
|
2921
|
+
if (bytes.byteLength > MAX_RELEASE_BYTES) {
|
|
2922
|
+
throw new Error(
|
|
2923
|
+
`Bundle exceeds ${MAX_RELEASE_BYTES} bytes (${bytes.byteLength}).`
|
|
2924
|
+
);
|
|
2925
|
+
}
|
|
2926
|
+
const sibling = await findSiblingManifest(bundlePath);
|
|
2927
|
+
const ref = appRef?.trim() || opts.app?.trim() || (typeof sibling?.manifest.appId === "string" ? sibling.manifest.appId : "");
|
|
2928
|
+
if (!ref) {
|
|
2929
|
+
throw new Error(
|
|
2930
|
+
"Pass an app id or slug, or set appId in groundfloor.manifest.json next to the bundle."
|
|
2931
|
+
);
|
|
2932
|
+
}
|
|
2933
|
+
const app = await resolveAppId(cfg.apiUrl, cfg.workspaceId, ref);
|
|
2934
|
+
if (app.app_kind && app.app_kind !== "shell_federated") {
|
|
2935
|
+
throw new Error(
|
|
2936
|
+
`Releases are only for shell_federated apps (this app is ${app.app_kind}). Use \`gf deploy\` for coderunner workloads.`
|
|
2937
|
+
);
|
|
2938
|
+
}
|
|
2939
|
+
const ext = path6.extname(bundlePath).toLowerCase();
|
|
2940
|
+
const isZip = ext === ".zip";
|
|
2941
|
+
if (!isZip) {
|
|
2942
|
+
throw new Error(
|
|
2943
|
+
`Publish release.zip from \`npm run release\`, not a lone ${path6.basename(bundlePath)}. ` + initThenPublishHint(app.slug)
|
|
2944
|
+
);
|
|
2945
|
+
}
|
|
2946
|
+
bytes = prepareReleaseZip(bytes, app.slug);
|
|
2947
|
+
const bundle = "zip";
|
|
2948
|
+
const contentType = "application/zip";
|
|
2949
|
+
if (opts.syncManifest !== false && sibling) {
|
|
2950
|
+
await patchApp(cfg.apiUrl, cfg.workspaceId, app.id, {
|
|
2951
|
+
manifest: sibling.manifest
|
|
2952
|
+
});
|
|
2953
|
+
}
|
|
2954
|
+
const init = await initAppReleaseUpload(cfg.apiUrl, cfg.workspaceId, app.id, {
|
|
2955
|
+
bundle,
|
|
2956
|
+
label: opts.label,
|
|
2957
|
+
// Required since wave 5: the server pins this into the presigned PUT and
|
|
2958
|
+
// checks it against the quota before minting a URL. Omitting it is a 422.
|
|
2959
|
+
size_bytes: bytes.byteLength
|
|
2960
|
+
});
|
|
2961
|
+
await uploadAppReleaseContent(
|
|
2549
2962
|
cfg.apiUrl,
|
|
2550
2963
|
cfg.workspaceId,
|
|
2551
|
-
|
|
2964
|
+
app.id,
|
|
2965
|
+
init.release.id,
|
|
2966
|
+
bytes,
|
|
2967
|
+
contentType
|
|
2552
2968
|
);
|
|
2553
|
-
const
|
|
2554
|
-
const saved = await putFlag(
|
|
2969
|
+
const release = await finalizeAppRelease(
|
|
2555
2970
|
cfg.apiUrl,
|
|
2556
2971
|
cfg.workspaceId,
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
env
|
|
2972
|
+
app.id,
|
|
2973
|
+
init.release.id
|
|
2560
2974
|
);
|
|
2561
2975
|
if (opts.json) {
|
|
2562
|
-
process.stdout.write(`${JSON.stringify(
|
|
2976
|
+
process.stdout.write(`${JSON.stringify({ app, release }, null, 2)}
|
|
2563
2977
|
`);
|
|
2564
2978
|
return;
|
|
2565
2979
|
}
|
|
2566
2980
|
process.stdout.write(
|
|
2567
|
-
`
|
|
2568
|
-
`
|
|
2569
|
-
);
|
|
2570
|
-
}
|
|
2571
|
-
async function flagsRmCommand(key, opts) {
|
|
2572
|
-
const cfg = await requireWorkspace(opts.workspace);
|
|
2573
|
-
const env = await resolveEnvironment(
|
|
2574
|
-
cfg.apiUrl,
|
|
2575
|
-
cfg.workspaceId,
|
|
2576
|
-
opts.environment
|
|
2981
|
+
`Published ${app.slug ?? app.id} build ${release.build_number}` + (release.remote_url ? ` ${release.remote_url}` : "") + "\n"
|
|
2577
2982
|
);
|
|
2578
|
-
await deleteFlag(cfg.apiUrl, cfg.workspaceId, key, env);
|
|
2579
|
-
process.stdout.write(`Deleted flag ${key}
|
|
2580
|
-
`);
|
|
2581
2983
|
}
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
`);
|
|
2592
|
-
return;
|
|
2593
|
-
}
|
|
2594
|
-
if (data.keys.length === 0) {
|
|
2595
|
-
process.stdout.write("No flags in either environment.\n");
|
|
2596
|
-
return;
|
|
2984
|
+
|
|
2985
|
+
// src/commands/domains.ts
|
|
2986
|
+
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>`.";
|
|
2987
|
+
async function resolve(opts) {
|
|
2988
|
+
const cfg = await resolveConfig({ workspaceId: opts.workspace });
|
|
2989
|
+
if (!cfg.workspaceId) {
|
|
2990
|
+
throw new Error(
|
|
2991
|
+
"No workspace selected. Run `gf workspaces use <id>` or pass --workspace <id>."
|
|
2992
|
+
);
|
|
2597
2993
|
}
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
`
|
|
2994
|
+
if (!opts.app) {
|
|
2995
|
+
throw new Error(
|
|
2996
|
+
opts.coderunner ? `--coderunner is no longer supported for domains. ${DOMAINS_APPS_ONLY}` : "Specify the app with --app <id|slug>. " + DOMAINS_APPS_ONLY
|
|
2602
2997
|
);
|
|
2603
2998
|
}
|
|
2999
|
+
const app = await resolveAppId(cfg.apiUrl, cfg.workspaceId, opts.app);
|
|
3000
|
+
return { apiUrl: cfg.apiUrl, workspaceId: cfg.workspaceId, appId: app.id };
|
|
2604
3001
|
}
|
|
2605
|
-
|
|
2606
|
-
const
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
resolveEnvironment(cfg.apiUrl, cfg.workspaceId, target)
|
|
2610
|
-
]);
|
|
2611
|
-
const keys = opts.keys?.split(",").map((k) => k.trim()).filter(Boolean);
|
|
2612
|
-
const result = await promoteFlags(cfg.apiUrl, cfg.workspaceId, {
|
|
2613
|
-
source_environment_id: src,
|
|
2614
|
-
target_environment_id: tgt,
|
|
2615
|
-
keys: keys && keys.length > 0 ? keys : void 0,
|
|
2616
|
-
mode: opts.overwrite ? "overwrite" : "skip_existing",
|
|
2617
|
-
dry_run: Boolean(opts.dryRun)
|
|
2618
|
-
});
|
|
2619
|
-
if (opts.json) {
|
|
2620
|
-
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
3002
|
+
function printInstructions(item) {
|
|
3003
|
+
const out = process.stdout;
|
|
3004
|
+
out.write(`
|
|
3005
|
+
Added ${item.domain} (status: ${item.status})
|
|
2621
3006
|
`);
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
process.stdout.write(
|
|
2626
|
-
`${prefix}copied: ${result.copied.join(", ") || "none"}
|
|
2627
|
-
${prefix}skipped (already set; use --overwrite): ${result.skipped.join(", ") || "none"}
|
|
2628
|
-
`
|
|
2629
|
-
);
|
|
2630
|
-
for (const failure of result.failed) {
|
|
2631
|
-
process.stdout.write(`${prefix}failed: ${failure.key} \u2014 ${failure.error}
|
|
3007
|
+
out.write("\nCreate these DNS records at your registrar:\n");
|
|
3008
|
+
if (item.cname_target) {
|
|
3009
|
+
out.write(` CNAME ${item.domain} \u2192 ${item.cname_target}
|
|
2632
3010
|
`);
|
|
2633
3011
|
}
|
|
2634
|
-
if (
|
|
2635
|
-
|
|
3012
|
+
if (item.verification_record) {
|
|
3013
|
+
const r = item.verification_record;
|
|
3014
|
+
out.write(` ${r.type} ${r.name} = ${r.value}
|
|
3015
|
+
`);
|
|
2636
3016
|
}
|
|
3017
|
+
out.write(`
|
|
3018
|
+
Then run: gf domains verify ${item.domain} --app <id|slug>
|
|
3019
|
+
`);
|
|
2637
3020
|
}
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
".csv": "text/csv",
|
|
2648
|
-
".pdf": "application/pdf",
|
|
2649
|
-
".png": "image/png",
|
|
2650
|
-
".jpg": "image/jpeg",
|
|
2651
|
-
".jpeg": "image/jpeg",
|
|
2652
|
-
".zip": "application/zip"
|
|
2653
|
-
};
|
|
2654
|
-
return types[ext] ?? "application/octet-stream";
|
|
3021
|
+
async function domainsAddCommand(domain, opts) {
|
|
3022
|
+
const { apiUrl, workspaceId, appId } = await resolve(opts);
|
|
3023
|
+
const item = await addDomain(apiUrl, workspaceId, appId, domain);
|
|
3024
|
+
if (opts.json) {
|
|
3025
|
+
process.stdout.write(`${JSON.stringify(item, null, 2)}
|
|
3026
|
+
`);
|
|
3027
|
+
return;
|
|
3028
|
+
}
|
|
3029
|
+
printInstructions(item);
|
|
2655
3030
|
}
|
|
2656
|
-
async function
|
|
2657
|
-
const
|
|
2658
|
-
const
|
|
3031
|
+
async function domainsListCommand(opts) {
|
|
3032
|
+
const { apiUrl, workspaceId, appId } = await resolve(opts);
|
|
3033
|
+
const result = await listDomains(apiUrl, workspaceId, appId);
|
|
2659
3034
|
if (opts.json) {
|
|
2660
|
-
process.stdout.write(`${JSON.stringify(
|
|
3035
|
+
process.stdout.write(`${JSON.stringify(result.domains, null, 2)}
|
|
2661
3036
|
`);
|
|
2662
3037
|
return;
|
|
2663
3038
|
}
|
|
2664
|
-
if (
|
|
2665
|
-
process.stdout.write("No
|
|
3039
|
+
if (result.domains.length === 0) {
|
|
3040
|
+
process.stdout.write("No custom domains.\n");
|
|
2666
3041
|
return;
|
|
2667
3042
|
}
|
|
2668
|
-
for (const
|
|
2669
|
-
const
|
|
2670
|
-
process.stdout.write(
|
|
2671
|
-
|
|
2672
|
-
`
|
|
2673
|
-
);
|
|
3043
|
+
for (const d of result.domains) {
|
|
3044
|
+
const target = d.cname_target ? ` \u2192 ${d.cname_target}` : "";
|
|
3045
|
+
process.stdout.write(`${d.domain} [${d.status}]${target}
|
|
3046
|
+
`);
|
|
2674
3047
|
}
|
|
2675
3048
|
}
|
|
2676
|
-
async function
|
|
2677
|
-
const
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
const name = opts.name ?? path6.basename(resolved);
|
|
2681
|
-
const contentType = guessContentType(name);
|
|
2682
|
-
const init = await initFileUpload(cfg.apiUrl, cfg.workspaceId, {
|
|
2683
|
-
name,
|
|
2684
|
-
content_type: contentType,
|
|
2685
|
-
size_bytes: buf.byteLength
|
|
2686
|
-
});
|
|
2687
|
-
const put = await fetch(init.upload_url, {
|
|
2688
|
-
method: "PUT",
|
|
2689
|
-
headers: { "Content-Type": contentType },
|
|
2690
|
-
body: buf
|
|
2691
|
-
});
|
|
2692
|
-
if (!put.ok) {
|
|
2693
|
-
throw new Error(
|
|
2694
|
-
`Upload to object storage failed: ${put.status} ${put.statusText}`
|
|
2695
|
-
);
|
|
2696
|
-
}
|
|
2697
|
-
const file = await finalizeFileUpload(
|
|
2698
|
-
cfg.apiUrl,
|
|
2699
|
-
cfg.workspaceId,
|
|
2700
|
-
init.file.id
|
|
2701
|
-
);
|
|
2702
|
-
process.stdout.write(`Uploaded ${file.id} ${file.name ?? name}
|
|
3049
|
+
async function domainsRemoveCommand(domain, opts) {
|
|
3050
|
+
const { apiUrl, workspaceId, appId } = await resolve(opts);
|
|
3051
|
+
await removeDomain(apiUrl, workspaceId, appId, domain);
|
|
3052
|
+
process.stdout.write(`Removed ${domain}
|
|
2703
3053
|
`);
|
|
2704
3054
|
}
|
|
2705
|
-
async function
|
|
2706
|
-
const
|
|
2707
|
-
const
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
const res = await fetch(url);
|
|
2713
|
-
if (!res.ok) {
|
|
2714
|
-
throw new Error(`Download failed: ${res.status} ${res.statusText}`);
|
|
3055
|
+
async function domainsVerifyCommand(domain, opts) {
|
|
3056
|
+
const { apiUrl, workspaceId, appId } = await resolve(opts);
|
|
3057
|
+
const result = await verifyDomain(apiUrl, workspaceId, appId, domain);
|
|
3058
|
+
if (opts.json) {
|
|
3059
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
3060
|
+
`);
|
|
3061
|
+
return;
|
|
2715
3062
|
}
|
|
2716
|
-
const
|
|
2717
|
-
|
|
2718
|
-
await fs5.writeFile(dest, buf);
|
|
2719
|
-
process.stdout.write(`Wrote ${dest}
|
|
3063
|
+
const mark = result.verified ? "verified" : "not verified";
|
|
3064
|
+
process.stdout.write(`${result.domain}: ${mark} [${result.status}]
|
|
2720
3065
|
`);
|
|
2721
|
-
}
|
|
2722
|
-
async function filesRmCommand(fileId, opts) {
|
|
2723
|
-
const cfg = await requireWorkspace(opts.workspace);
|
|
2724
|
-
await deleteFile(cfg.apiUrl, cfg.workspaceId, fileId);
|
|
2725
|
-
process.stdout.write(`Deleted file ${fileId}
|
|
3066
|
+
process.stdout.write(`${result.detail}
|
|
2726
3067
|
`);
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
// src/commands/apps.ts
|
|
2730
|
-
import { promises as fs6 } from "fs";
|
|
2731
|
-
import path7 from "path";
|
|
2732
|
-
|
|
2733
|
-
// src/release-zip.ts
|
|
2734
|
-
import AdmZip2 from "adm-zip";
|
|
2735
|
-
var REMOTE_ENTRY = "remoteEntry.js";
|
|
2736
|
-
var FLATTEN_SUFFIXES = [".js", ".mjs", ".cjs", ".css", ".map", ".wasm"];
|
|
2737
|
-
var IMPORT_ASSETS = /(__federation_import|import)\((['"])\.\/assets\//g;
|
|
2738
|
-
function rewriteRemoteEntryAssetPrefix(source) {
|
|
2739
|
-
return source.replace(IMPORT_ASSETS, "$1($2./");
|
|
2740
|
-
}
|
|
2741
|
-
function normalizeZipEntryName(filename) {
|
|
2742
|
-
return filename.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
2743
|
-
}
|
|
2744
|
-
function parentDir(rel) {
|
|
2745
|
-
const i = rel.lastIndexOf("/");
|
|
2746
|
-
return i === -1 ? "" : rel.slice(0, i);
|
|
2747
|
-
}
|
|
2748
|
-
function basename(rel) {
|
|
2749
|
-
const i = rel.lastIndexOf("/");
|
|
2750
|
-
return i === -1 ? rel : rel.slice(i + 1);
|
|
2751
|
-
}
|
|
2752
|
-
function chooseRemoteEntry(relpaths) {
|
|
2753
|
-
const entries = relpaths.filter(
|
|
2754
|
-
(p) => p === REMOTE_ENTRY || p.endsWith(`/${REMOTE_ENTRY}`)
|
|
2755
|
-
);
|
|
2756
|
-
if (entries.length === 0) return void 0;
|
|
2757
|
-
if (entries.includes(REMOTE_ENTRY)) return REMOTE_ENTRY;
|
|
2758
|
-
return [...entries].sort((a, b) => {
|
|
2759
|
-
const score = (p) => {
|
|
2760
|
-
const parts = p.split("/");
|
|
2761
|
-
const inAssets = parts.length >= 2 && parts[parts.length - 2] === "assets";
|
|
2762
|
-
return `${inAssets ? 0 : 1}:${parts.length}:${p}`;
|
|
2763
|
-
};
|
|
2764
|
-
return score(a).localeCompare(score(b));
|
|
2765
|
-
})[0];
|
|
2766
|
-
}
|
|
2767
|
-
function flattenFederatedBundlePlan(relpaths) {
|
|
2768
|
-
const paths2 = relpaths.map(normalizeZipEntryName).filter((name) => name && !name.endsWith("/") && !name.split("/").includes(".."));
|
|
2769
|
-
const pathset = new Set(paths2);
|
|
2770
|
-
const entry = chooseRemoteEntry([...pathset]);
|
|
2771
|
-
if (!entry) return {};
|
|
2772
|
-
const promoteDirs = /* @__PURE__ */ new Set();
|
|
2773
|
-
const entryDir = parentDir(entry);
|
|
2774
|
-
if (entryDir) promoteDirs.add(entryDir);
|
|
2775
|
-
for (const path11 of pathset) {
|
|
2776
|
-
const parent = parentDir(path11);
|
|
2777
|
-
if (parent === "assets" || parent.endsWith("/assets") && parent.split("/").length - 1 < 2) {
|
|
2778
|
-
promoteDirs.add(parent);
|
|
2779
|
-
}
|
|
2780
|
-
}
|
|
2781
|
-
const destToSrc = {};
|
|
2782
|
-
for (const src of paths2) {
|
|
2783
|
-
const parent = parentDir(src);
|
|
2784
|
-
if (!promoteDirs.has(parent)) continue;
|
|
2785
|
-
const dest = basename(src);
|
|
2786
|
-
if (dest === "release.zip") continue;
|
|
2787
|
-
const lower = dest.toLowerCase();
|
|
2788
|
-
if (dest !== REMOTE_ENTRY && !FLATTEN_SUFFIXES.some((ext) => lower.endsWith(ext))) {
|
|
2789
|
-
continue;
|
|
2790
|
-
}
|
|
2791
|
-
if (pathset.has(dest)) continue;
|
|
2792
|
-
const prev = destToSrc[dest];
|
|
2793
|
-
if (prev === void 0 || src.split("/").length < prev.split("/").length) {
|
|
2794
|
-
destToSrc[dest] = src;
|
|
2795
|
-
}
|
|
3068
|
+
if (!result.verified) {
|
|
3069
|
+
process.exitCode = 1;
|
|
2796
3070
|
}
|
|
2797
|
-
return destToSrc;
|
|
2798
3071
|
}
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
if (entry.isDirectory) continue;
|
|
2805
|
-
const name = normalizeZipEntryName(entry.entryName);
|
|
2806
|
-
rels.push(name);
|
|
2807
|
-
byNorm.set(name, entry);
|
|
3072
|
+
|
|
3073
|
+
// src/commands/secrets.ts
|
|
3074
|
+
async function readValue(value) {
|
|
3075
|
+
if (value === void 0) {
|
|
3076
|
+
throw new Error("Pass a value, or `-` to read from stdin.");
|
|
2808
3077
|
}
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
const src = plan[dest];
|
|
2814
|
-
const source = byNorm.get(src);
|
|
2815
|
-
if (!source) continue;
|
|
2816
|
-
zip.addFile(dest, source.getData());
|
|
3078
|
+
if (value === "-") {
|
|
3079
|
+
const chunks = [];
|
|
3080
|
+
for await (const chunk of process.stdin) {
|
|
3081
|
+
chunks.push(Buffer.from(chunk));
|
|
2817
3082
|
}
|
|
3083
|
+
return Buffer.concat(chunks).toString("utf8").replace(/\n$/, "");
|
|
2818
3084
|
}
|
|
2819
|
-
|
|
2820
|
-
return zip.toBuffer();
|
|
2821
|
-
}
|
|
2822
|
-
function rewriteRemoteEntryInZip(zip) {
|
|
2823
|
-
const names2 = zip.getEntries().filter((entry) => !entry.isDirectory).map((entry) => normalizeZipEntryName(entry.entryName));
|
|
2824
|
-
const entryRel = chooseRemoteEntry(names2);
|
|
2825
|
-
if (!entryRel) return;
|
|
2826
|
-
const zipEntry = zip.getEntries().find((item) => {
|
|
2827
|
-
if (item.isDirectory) return false;
|
|
2828
|
-
return normalizeZipEntryName(item.entryName) === entryRel;
|
|
2829
|
-
});
|
|
2830
|
-
if (!zipEntry) return;
|
|
2831
|
-
const before = zipEntry.getData().toString("utf8");
|
|
2832
|
-
const after = rewriteRemoteEntryAssetPrefix(before);
|
|
2833
|
-
if (after === before) return;
|
|
2834
|
-
zip.updateFile(zipEntry.entryName, Buffer.from(after, "utf8"));
|
|
2835
|
-
}
|
|
2836
|
-
function federatedReleaseZipHint(slug) {
|
|
2837
|
-
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)}`;
|
|
2838
|
-
}
|
|
2839
|
-
function federationRemoteName(appId) {
|
|
2840
|
-
const sanitized = appId.replace(/[^a-zA-Z0-9_$]/g, "_");
|
|
2841
|
-
return sanitized.length > 0 ? sanitized : "federatedApp";
|
|
2842
|
-
}
|
|
2843
|
-
function initThenPublishHint(slug) {
|
|
2844
|
-
const s = (slug ?? "").trim() || "<portal-slug>";
|
|
2845
|
-
return `gf apps init --slug ${s} && cd ${s} && npm install && npm run release && gf apps publish --path release.zip`;
|
|
2846
|
-
}
|
|
2847
|
-
function extractReferencedSiblings(remoteEntrySource) {
|
|
2848
|
-
const names2 = /* @__PURE__ */ new Set();
|
|
2849
|
-
for (const match of remoteEntrySource.matchAll(
|
|
2850
|
-
/["']\.\/(?:assets\/)?([^"'/]+\.(?:js|mjs|cjs|css))["']/g
|
|
2851
|
-
)) {
|
|
2852
|
-
if (match[1] !== REMOTE_ENTRY) names2.add(match[1]);
|
|
2853
|
-
}
|
|
2854
|
-
for (const match of remoteEntrySource.matchAll(
|
|
2855
|
-
/["']([A-Za-z0-9._-]+\.css)["']/g
|
|
2856
|
-
)) {
|
|
2857
|
-
names2.add(match[1]);
|
|
2858
|
-
}
|
|
2859
|
-
return [...names2];
|
|
2860
|
-
}
|
|
2861
|
-
function extractFederationScope(remoteEntrySource) {
|
|
2862
|
-
return remoteEntrySource.match(/css__([A-Za-z0-9_-]+)__/)?.[1];
|
|
2863
|
-
}
|
|
2864
|
-
function zipRootNames(bytes) {
|
|
2865
|
-
const zip = new AdmZip2(bytes);
|
|
2866
|
-
return zip.getEntries().filter((entry) => !entry.isDirectory).map((entry) => normalizeZipEntryName(entry.entryName));
|
|
2867
|
-
}
|
|
2868
|
-
function readZipFile(bytes, rel) {
|
|
2869
|
-
const zip = new AdmZip2(bytes);
|
|
2870
|
-
const entry = zip.getEntries().find((item) => {
|
|
2871
|
-
if (item.isDirectory) return false;
|
|
2872
|
-
return normalizeZipEntryName(item.entryName) === rel;
|
|
2873
|
-
});
|
|
2874
|
-
return entry ? entry.getData().toString("utf8") : void 0;
|
|
3085
|
+
return value;
|
|
2875
3086
|
}
|
|
2876
|
-
function
|
|
2877
|
-
const
|
|
2878
|
-
const
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
}
|
|
2884
|
-
const source = readZipFile(bytes, entryRel);
|
|
2885
|
-
if (!source) {
|
|
2886
|
-
throw new Error(`Zip has no remoteEntry.js. ${hint}`);
|
|
3087
|
+
async function secretsListCommand(opts) {
|
|
3088
|
+
const cfg = await requireWorkspace(opts.workspace);
|
|
3089
|
+
const data = await listSecrets(cfg.apiUrl, cfg.workspaceId, opts.environment);
|
|
3090
|
+
if (opts.json) {
|
|
3091
|
+
process.stdout.write(`${JSON.stringify(data, null, 2)}
|
|
3092
|
+
`);
|
|
3093
|
+
return;
|
|
2887
3094
|
}
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
if (name.includes("/")) return false;
|
|
2892
|
-
const lower = name.toLowerCase();
|
|
2893
|
-
return lower.endsWith(".js") || lower.endsWith(".mjs") || lower.endsWith(".css");
|
|
2894
|
-
});
|
|
2895
|
-
if (missing.length > 0 || siblings.length === 0 && jsOrCssAtRoot.length < 2) {
|
|
2896
|
-
const detail = missing.length > 0 ? `Missing next to remoteEntry.js: ${missing.join(", ")}.` : "Zip is only remoteEntry.js (Vite chunks were never packed).";
|
|
2897
|
-
throw new Error(
|
|
2898
|
-
`${detail} Do not zip dist/ or upload a lone remoteEntry.js. ${hint}`
|
|
2899
|
-
);
|
|
3095
|
+
if (data.secrets.length === 0) {
|
|
3096
|
+
process.stdout.write("No secrets in this workspace.\n");
|
|
3097
|
+
return;
|
|
2900
3098
|
}
|
|
2901
|
-
const
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
if (scope !== expected) {
|
|
2905
|
-
throw new Error(
|
|
2906
|
-
`Federation name is "${scope}" but Shell looks up "${expected}" (from slug ${slug}). Use the starter-kit vite.config (federationRemoteName). ${hint}`
|
|
2907
|
-
);
|
|
2908
|
-
}
|
|
3099
|
+
for (const secret of data.secrets) {
|
|
3100
|
+
process.stdout.write(`${secret.key}
|
|
3101
|
+
`);
|
|
2909
3102
|
}
|
|
2910
3103
|
}
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
3104
|
+
async function secretsGetCommand(key, opts) {
|
|
3105
|
+
const cfg = await requireWorkspace(opts.workspace);
|
|
3106
|
+
const secret = await getSecret(
|
|
3107
|
+
cfg.apiUrl,
|
|
3108
|
+
cfg.workspaceId,
|
|
3109
|
+
key,
|
|
3110
|
+
opts.environment
|
|
3111
|
+
);
|
|
3112
|
+
if (opts.json) {
|
|
3113
|
+
process.stdout.write(`${JSON.stringify(secret, null, 2)}
|
|
3114
|
+
`);
|
|
3115
|
+
return;
|
|
3116
|
+
}
|
|
3117
|
+
process.stdout.write(`${secret.value}
|
|
3118
|
+
`);
|
|
2918
3119
|
}
|
|
2919
|
-
function
|
|
2920
|
-
|
|
3120
|
+
async function secretsSetCommand(key, value, opts) {
|
|
3121
|
+
const cfg = await requireWorkspace(opts.workspace);
|
|
3122
|
+
const resolved = await readValue(value);
|
|
3123
|
+
const saved = await putSecret(
|
|
3124
|
+
cfg.apiUrl,
|
|
3125
|
+
cfg.workspaceId,
|
|
3126
|
+
key,
|
|
3127
|
+
{ value: resolved, description: opts.description },
|
|
3128
|
+
opts.environment
|
|
3129
|
+
);
|
|
3130
|
+
process.stdout.write(`Saved secret ${saved.key}
|
|
3131
|
+
`);
|
|
2921
3132
|
}
|
|
2922
|
-
async function
|
|
2923
|
-
const
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
}
|
|
2928
|
-
return parsed;
|
|
3133
|
+
async function secretsRmCommand(key, opts) {
|
|
3134
|
+
const cfg = await requireWorkspace(opts.workspace);
|
|
3135
|
+
await deleteSecret(cfg.apiUrl, cfg.workspaceId, key, opts.environment);
|
|
3136
|
+
process.stdout.write(`Deleted secret ${key}
|
|
3137
|
+
`);
|
|
2929
3138
|
}
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
3139
|
+
|
|
3140
|
+
// src/commands/flags.ts
|
|
3141
|
+
var VALUE_TYPES = [
|
|
3142
|
+
"boolean",
|
|
3143
|
+
"string",
|
|
3144
|
+
"number",
|
|
3145
|
+
"json"
|
|
3146
|
+
];
|
|
3147
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
3148
|
+
function matchEnvironment(environments2, idOrName) {
|
|
3149
|
+
const wanted = idOrName.trim();
|
|
3150
|
+
const byId = environments2.find((env) => env.id === wanted);
|
|
3151
|
+
if (byId) return byId.id;
|
|
3152
|
+
const byName = environments2.filter(
|
|
3153
|
+
(env) => env.name.toLowerCase() === wanted.toLowerCase()
|
|
3154
|
+
);
|
|
3155
|
+
if (byName.length === 1) return byName[0].id;
|
|
3156
|
+
const names2 = environments2.map((env) => env.name).join(", ") || "none";
|
|
3157
|
+
throw new Error(
|
|
3158
|
+
`Environment "${wanted}" not found in this workspace (have: ${names2}). Run \`gf environments ls\`.`
|
|
3159
|
+
);
|
|
2933
3160
|
}
|
|
2934
|
-
async function
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
3161
|
+
async function resolveEnvironment(apiUrl, workspaceId, idOrName) {
|
|
3162
|
+
if (!idOrName) return void 0;
|
|
3163
|
+
if (UUID_RE.test(idOrName.trim())) return idOrName.trim();
|
|
3164
|
+
const environments2 = await listWorkspaceEnvironments(apiUrl, workspaceId);
|
|
3165
|
+
return matchEnvironment(environments2, idOrName);
|
|
3166
|
+
}
|
|
3167
|
+
function parseFlagValue(raw, type) {
|
|
3168
|
+
if (type !== void 0 && !VALUE_TYPES.includes(type)) {
|
|
3169
|
+
throw new Error(`--type must be one of: ${VALUE_TYPES.join(", ")}`);
|
|
3170
|
+
}
|
|
3171
|
+
const lower = raw.trim().toLowerCase();
|
|
3172
|
+
const valueType = type ?? (lower === "true" || lower === "false" ? "boolean" : raw.trim() !== "" && Number.isFinite(Number(raw)) ? "number" : "string");
|
|
3173
|
+
switch (valueType) {
|
|
3174
|
+
case "boolean":
|
|
3175
|
+
if (lower !== "true" && lower !== "false") {
|
|
3176
|
+
throw new Error("boolean flags take true or false");
|
|
3177
|
+
}
|
|
3178
|
+
return { value: lower === "true", value_type: "boolean" };
|
|
3179
|
+
case "number": {
|
|
3180
|
+
const n = Number(raw);
|
|
3181
|
+
if (raw.trim() === "" || !Number.isFinite(n)) {
|
|
3182
|
+
throw new Error(`"${raw}" is not a number`);
|
|
3183
|
+
}
|
|
3184
|
+
return { value: n, value_type: "number" };
|
|
3185
|
+
}
|
|
3186
|
+
case "json":
|
|
3187
|
+
try {
|
|
3188
|
+
return { value: JSON.parse(raw), value_type: "json" };
|
|
3189
|
+
} catch {
|
|
3190
|
+
throw new Error("--type json needs a valid JSON value");
|
|
3191
|
+
}
|
|
3192
|
+
default:
|
|
3193
|
+
return { value: raw, value_type: "string" };
|
|
2941
3194
|
}
|
|
2942
|
-
return { path: candidate, manifest: await readJsonObject(candidate) };
|
|
2943
3195
|
}
|
|
2944
|
-
function
|
|
2945
|
-
|
|
2946
|
-
return zip.getEntries().some((entry) => {
|
|
2947
|
-
if (entry.isDirectory) return false;
|
|
2948
|
-
const name = normalizeZipEntryName(entry.entryName);
|
|
2949
|
-
return name === REMOTE_ENTRY || name.endsWith(`/${REMOTE_ENTRY}`);
|
|
2950
|
-
});
|
|
3196
|
+
function formatValue(value) {
|
|
3197
|
+
return typeof value === "string" ? value : JSON.stringify(value);
|
|
2951
3198
|
}
|
|
2952
|
-
function
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
const flattened = flattenFederatedReleaseZip(bytes);
|
|
2959
|
-
const plan = flattenFederatedBundlePlan(
|
|
2960
|
-
new AdmZip3(bytes).getEntries().filter((e) => !e.isDirectory).map((e) => normalizeZipEntryName(e.entryName))
|
|
3199
|
+
async function flagsListCommand(opts) {
|
|
3200
|
+
const cfg = await requireWorkspace(opts.workspace);
|
|
3201
|
+
const env = await resolveEnvironment(
|
|
3202
|
+
cfg.apiUrl,
|
|
3203
|
+
cfg.workspaceId,
|
|
3204
|
+
opts.environment
|
|
2961
3205
|
);
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
3206
|
+
const data = await listFlags(cfg.apiUrl, cfg.workspaceId, env);
|
|
3207
|
+
if (opts.json) {
|
|
3208
|
+
process.stdout.write(`${JSON.stringify(data, null, 2)}
|
|
3209
|
+
`);
|
|
3210
|
+
return;
|
|
3211
|
+
}
|
|
3212
|
+
process.stderr.write(`environment ${data.environment_id}
|
|
3213
|
+
`);
|
|
3214
|
+
if (data.flags.length === 0) {
|
|
3215
|
+
process.stdout.write("No flags in this environment.\n");
|
|
3216
|
+
return;
|
|
3217
|
+
}
|
|
3218
|
+
for (const flag of data.flags) {
|
|
3219
|
+
process.stdout.write(
|
|
3220
|
+
`${flag.key} ${flag.value_type} ${formatValue(flag.value)}
|
|
2966
3221
|
`
|
|
2967
3222
|
);
|
|
2968
3223
|
}
|
|
2969
|
-
assertFederatedReleaseZip(flattened, slug);
|
|
2970
|
-
return flattened;
|
|
2971
3224
|
}
|
|
2972
|
-
async function
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
3225
|
+
async function flagsGetCommand(key, opts) {
|
|
3226
|
+
const cfg = await requireWorkspace(opts.workspace);
|
|
3227
|
+
const env = await resolveEnvironment(
|
|
3228
|
+
cfg.apiUrl,
|
|
3229
|
+
cfg.workspaceId,
|
|
3230
|
+
opts.environment
|
|
3231
|
+
);
|
|
3232
|
+
const flag = await getFlag(cfg.apiUrl, cfg.workspaceId, key, env);
|
|
3233
|
+
if (opts.json) {
|
|
3234
|
+
process.stdout.write(`${JSON.stringify(flag, null, 2)}
|
|
3235
|
+
`);
|
|
3236
|
+
return;
|
|
2977
3237
|
}
|
|
3238
|
+
process.stdout.write(`${formatValue(flag.value)}
|
|
3239
|
+
`);
|
|
2978
3240
|
}
|
|
2979
|
-
async function
|
|
2980
|
-
const
|
|
2981
|
-
const
|
|
2982
|
-
|
|
3241
|
+
async function flagsSetCommand(key, value, opts) {
|
|
3242
|
+
const cfg = await requireWorkspace(opts.workspace);
|
|
3243
|
+
const env = await resolveEnvironment(
|
|
3244
|
+
cfg.apiUrl,
|
|
3245
|
+
cfg.workspaceId,
|
|
3246
|
+
opts.environment
|
|
2983
3247
|
);
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
3248
|
+
const parsed = parseFlagValue(value, opts.type);
|
|
3249
|
+
const saved = await putFlag(
|
|
3250
|
+
cfg.apiUrl,
|
|
3251
|
+
cfg.workspaceId,
|
|
3252
|
+
key,
|
|
3253
|
+
{ ...parsed, description: opts.description },
|
|
3254
|
+
env
|
|
3255
|
+
);
|
|
3256
|
+
if (opts.json) {
|
|
3257
|
+
process.stdout.write(`${JSON.stringify(saved, null, 2)}
|
|
3258
|
+
`);
|
|
3259
|
+
return;
|
|
2994
3260
|
}
|
|
3261
|
+
process.stdout.write(
|
|
3262
|
+
`Set ${saved.key} = ${formatValue(saved.value)} (${saved.value_type}) in environment ${saved.environment_id}
|
|
3263
|
+
`
|
|
3264
|
+
);
|
|
2995
3265
|
}
|
|
2996
|
-
async function
|
|
3266
|
+
async function flagsRmCommand(key, opts) {
|
|
2997
3267
|
const cfg = await requireWorkspace(opts.workspace);
|
|
2998
|
-
const
|
|
3268
|
+
const env = await resolveEnvironment(
|
|
3269
|
+
cfg.apiUrl,
|
|
3270
|
+
cfg.workspaceId,
|
|
3271
|
+
opts.environment
|
|
3272
|
+
);
|
|
3273
|
+
await deleteFlag(cfg.apiUrl, cfg.workspaceId, key, env);
|
|
3274
|
+
process.stdout.write(`Deleted flag ${key}
|
|
3275
|
+
`);
|
|
3276
|
+
}
|
|
3277
|
+
async function flagsCompareCommand(source, target, opts) {
|
|
3278
|
+
const cfg = await requireWorkspace(opts.workspace);
|
|
3279
|
+
const [src, tgt] = await Promise.all([
|
|
3280
|
+
resolveEnvironment(cfg.apiUrl, cfg.workspaceId, source),
|
|
3281
|
+
resolveEnvironment(cfg.apiUrl, cfg.workspaceId, target)
|
|
3282
|
+
]);
|
|
3283
|
+
const data = await compareFlags(cfg.apiUrl, cfg.workspaceId, src, tgt);
|
|
2999
3284
|
if (opts.json) {
|
|
3000
3285
|
process.stdout.write(`${JSON.stringify(data, null, 2)}
|
|
3001
3286
|
`);
|
|
3002
3287
|
return;
|
|
3003
3288
|
}
|
|
3004
|
-
if (data.
|
|
3005
|
-
process.stdout.write("No
|
|
3289
|
+
if (data.keys.length === 0) {
|
|
3290
|
+
process.stdout.write("No flags in either environment.\n");
|
|
3006
3291
|
return;
|
|
3007
3292
|
}
|
|
3008
|
-
for (const
|
|
3009
|
-
process.stdout.write(
|
|
3010
|
-
|
|
3293
|
+
for (const row of data.keys) {
|
|
3294
|
+
process.stdout.write(
|
|
3295
|
+
`${row.key} ${row.status} ${formatValue(row.source_value ?? null)} \u2192 ${formatValue(row.target_value ?? null)}
|
|
3296
|
+
`
|
|
3297
|
+
);
|
|
3011
3298
|
}
|
|
3012
3299
|
}
|
|
3013
|
-
async function
|
|
3300
|
+
async function flagsPromoteCommand(source, target, opts) {
|
|
3014
3301
|
const cfg = await requireWorkspace(opts.workspace);
|
|
3015
|
-
const
|
|
3302
|
+
const [src, tgt] = await Promise.all([
|
|
3303
|
+
resolveEnvironment(cfg.apiUrl, cfg.workspaceId, source),
|
|
3304
|
+
resolveEnvironment(cfg.apiUrl, cfg.workspaceId, target)
|
|
3305
|
+
]);
|
|
3306
|
+
const keys = opts.keys?.split(",").map((k) => k.trim()).filter(Boolean);
|
|
3307
|
+
const result = await promoteFlags(cfg.apiUrl, cfg.workspaceId, {
|
|
3308
|
+
source_environment_id: src,
|
|
3309
|
+
target_environment_id: tgt,
|
|
3310
|
+
keys: keys && keys.length > 0 ? keys : void 0,
|
|
3311
|
+
mode: opts.overwrite ? "overwrite" : "skip_existing",
|
|
3312
|
+
dry_run: Boolean(opts.dryRun)
|
|
3313
|
+
});
|
|
3016
3314
|
if (opts.json) {
|
|
3017
|
-
process.stdout.write(`${JSON.stringify(
|
|
3315
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
3018
3316
|
`);
|
|
3019
3317
|
return;
|
|
3020
3318
|
}
|
|
3021
|
-
|
|
3319
|
+
const prefix = result.dry_run ? "[dry run] " : "";
|
|
3320
|
+
process.stdout.write(
|
|
3321
|
+
`${prefix}copied: ${result.copied.join(", ") || "none"}
|
|
3322
|
+
${prefix}skipped (already set; use --overwrite): ${result.skipped.join(", ") || "none"}
|
|
3323
|
+
`
|
|
3324
|
+
);
|
|
3325
|
+
for (const failure of result.failed) {
|
|
3326
|
+
process.stdout.write(`${prefix}failed: ${failure.key} \u2014 ${failure.error}
|
|
3022
3327
|
`);
|
|
3023
|
-
}
|
|
3024
|
-
async function appsCreateCommand(opts) {
|
|
3025
|
-
const cfg = await requireWorkspace(opts.workspace);
|
|
3026
|
-
const kindRaw = (opts.kind ?? "shell_federated").trim();
|
|
3027
|
-
if (kindRaw === "coderunner") {
|
|
3028
|
-
throw new Error(
|
|
3029
|
-
"Do not create app_kind=coderunner. Deploy workloads with `gf deploy`. Use --kind shell_federated or --kind standalone."
|
|
3030
|
-
);
|
|
3031
|
-
}
|
|
3032
|
-
if (kindRaw !== "shell_federated" && kindRaw !== "standalone") {
|
|
3033
|
-
throw new Error(
|
|
3034
|
-
`--kind must be shell_federated or standalone (got ${kindRaw})`
|
|
3035
|
-
);
|
|
3036
|
-
}
|
|
3037
|
-
const kind = kindRaw;
|
|
3038
|
-
const localManifest = await maybeReadManifest(opts.manifest);
|
|
3039
|
-
const name = opts.name?.trim() || (typeof localManifest?.name === "string" ? localManifest.name : "");
|
|
3040
|
-
const slug = opts.slug?.trim() || (typeof localManifest?.appId === "string" ? localManifest.appId : name ? slugify2(name) : "");
|
|
3041
|
-
if (!name) {
|
|
3042
|
-
throw new Error("Pass --name, or a manifest with a name field.");
|
|
3043
|
-
}
|
|
3044
|
-
if (!slug) {
|
|
3045
|
-
throw new Error("Pass --slug, or a manifest with appId.");
|
|
3046
3328
|
}
|
|
3047
|
-
if (
|
|
3048
|
-
|
|
3049
|
-
"Standalone apps require --primary-coderunner <id> (an existing service coderunner)."
|
|
3050
|
-
);
|
|
3329
|
+
if (result.failed.length > 0) {
|
|
3330
|
+
process.exitCode = 1;
|
|
3051
3331
|
}
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3332
|
+
}
|
|
3333
|
+
|
|
3334
|
+
// src/commands/files.ts
|
|
3335
|
+
import { promises as fs6 } from "fs";
|
|
3336
|
+
import path7 from "path";
|
|
3337
|
+
function guessContentType(filename) {
|
|
3338
|
+
const ext = path7.extname(filename).toLowerCase();
|
|
3339
|
+
const types = {
|
|
3340
|
+
".json": "application/json",
|
|
3341
|
+
".txt": "text/plain",
|
|
3342
|
+
".csv": "text/csv",
|
|
3343
|
+
".pdf": "application/pdf",
|
|
3344
|
+
".png": "image/png",
|
|
3345
|
+
".jpg": "image/jpeg",
|
|
3346
|
+
".jpeg": "image/jpeg",
|
|
3347
|
+
".zip": "application/zip"
|
|
3056
3348
|
};
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
let app;
|
|
3063
|
-
try {
|
|
3064
|
-
app = await createApp(cfg.apiUrl, cfg.workspaceId, body);
|
|
3065
|
-
} catch (err) {
|
|
3066
|
-
if (err instanceof CpError && err.status === 409) {
|
|
3067
|
-
throw new Error(
|
|
3068
|
-
`App slug ${slug} already exists. Init the kit if you do not have it, then publish:
|
|
3069
|
-
${initThenPublishHint(slug)}`
|
|
3070
|
-
);
|
|
3071
|
-
}
|
|
3072
|
-
throw err;
|
|
3073
|
-
}
|
|
3349
|
+
return types[ext] ?? "application/octet-stream";
|
|
3350
|
+
}
|
|
3351
|
+
async function filesListCommand(opts) {
|
|
3352
|
+
const cfg = await requireWorkspace(opts.workspace);
|
|
3353
|
+
const data = await listFiles(cfg.apiUrl, cfg.workspaceId);
|
|
3074
3354
|
if (opts.json) {
|
|
3075
|
-
process.stdout.write(`${JSON.stringify(
|
|
3355
|
+
process.stdout.write(`${JSON.stringify(data, null, 2)}
|
|
3076
3356
|
`);
|
|
3077
3357
|
return;
|
|
3078
3358
|
}
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3359
|
+
if (data.files.length === 0) {
|
|
3360
|
+
process.stdout.write("No files in this workspace.\n");
|
|
3361
|
+
return;
|
|
3362
|
+
}
|
|
3363
|
+
for (const file of data.files) {
|
|
3364
|
+
const size = file.size_bytes != null ? `${file.size_bytes}B` : "";
|
|
3082
3365
|
process.stdout.write(
|
|
3083
|
-
|
|
3084
|
-
gf apps init --slug ${slug}
|
|
3085
|
-
cd ${slug} && npm install && npm run release
|
|
3086
|
-
gf apps publish --path release.zip
|
|
3087
|
-
Do not scaffold src/App.tsx from scratch \u2014 BrowserRouter in App.tsx crashes the Shell.
|
|
3366
|
+
`${file.id} ${file.name ?? ""} ${size} [${file.status ?? ""}]
|
|
3088
3367
|
`
|
|
3089
3368
|
);
|
|
3090
3369
|
}
|
|
3091
3370
|
}
|
|
3092
|
-
async function
|
|
3371
|
+
async function filesUploadCommand(filePath, opts) {
|
|
3093
3372
|
const cfg = await requireWorkspace(opts.workspace);
|
|
3094
|
-
const
|
|
3095
|
-
const
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
`Bundle exceeds ${MAX_RELEASE_BYTES} bytes (${bytes.byteLength}).`
|
|
3110
|
-
);
|
|
3111
|
-
}
|
|
3112
|
-
const sibling = await findSiblingManifest(bundlePath);
|
|
3113
|
-
const ref = appRef?.trim() || opts.app?.trim() || (typeof sibling?.manifest.appId === "string" ? sibling.manifest.appId : "");
|
|
3114
|
-
if (!ref) {
|
|
3115
|
-
throw new Error(
|
|
3116
|
-
"Pass an app id or slug, or set appId in groundfloor.manifest.json next to the bundle."
|
|
3117
|
-
);
|
|
3118
|
-
}
|
|
3119
|
-
const app = await resolveAppId(cfg.apiUrl, cfg.workspaceId, ref);
|
|
3120
|
-
if (app.app_kind && app.app_kind !== "shell_federated") {
|
|
3121
|
-
throw new Error(
|
|
3122
|
-
`Releases are only for shell_federated apps (this app is ${app.app_kind}). Use \`gf deploy\` for coderunner workloads.`
|
|
3123
|
-
);
|
|
3124
|
-
}
|
|
3125
|
-
const ext = path7.extname(bundlePath).toLowerCase();
|
|
3126
|
-
const isZip = ext === ".zip";
|
|
3127
|
-
if (!isZip) {
|
|
3373
|
+
const resolved = path7.resolve(filePath);
|
|
3374
|
+
const buf = await fs6.readFile(resolved);
|
|
3375
|
+
const name = opts.name ?? path7.basename(resolved);
|
|
3376
|
+
const contentType = guessContentType(name);
|
|
3377
|
+
const init = await initFileUpload(cfg.apiUrl, cfg.workspaceId, {
|
|
3378
|
+
name,
|
|
3379
|
+
content_type: contentType,
|
|
3380
|
+
size_bytes: buf.byteLength
|
|
3381
|
+
});
|
|
3382
|
+
const put = await fetch(init.upload_url, {
|
|
3383
|
+
method: "PUT",
|
|
3384
|
+
headers: { "Content-Type": contentType },
|
|
3385
|
+
body: buf
|
|
3386
|
+
});
|
|
3387
|
+
if (!put.ok) {
|
|
3128
3388
|
throw new Error(
|
|
3129
|
-
`
|
|
3389
|
+
`Upload to object storage failed: ${put.status} ${put.statusText}`
|
|
3130
3390
|
);
|
|
3131
3391
|
}
|
|
3132
|
-
|
|
3133
|
-
const bundle = "zip";
|
|
3134
|
-
const contentType = "application/zip";
|
|
3135
|
-
if (opts.syncManifest !== false && sibling) {
|
|
3136
|
-
await patchApp(cfg.apiUrl, cfg.workspaceId, app.id, {
|
|
3137
|
-
manifest: sibling.manifest
|
|
3138
|
-
});
|
|
3139
|
-
}
|
|
3140
|
-
const init = await initAppReleaseUpload(cfg.apiUrl, cfg.workspaceId, app.id, {
|
|
3141
|
-
bundle,
|
|
3142
|
-
label: opts.label,
|
|
3143
|
-
// Required since wave 5: the server pins this into the presigned PUT and
|
|
3144
|
-
// checks it against the quota before minting a URL. Omitting it is a 422.
|
|
3145
|
-
size_bytes: bytes.byteLength
|
|
3146
|
-
});
|
|
3147
|
-
await uploadAppReleaseContent(
|
|
3392
|
+
const file = await finalizeFileUpload(
|
|
3148
3393
|
cfg.apiUrl,
|
|
3149
3394
|
cfg.workspaceId,
|
|
3150
|
-
|
|
3151
|
-
init.release.id,
|
|
3152
|
-
bytes,
|
|
3153
|
-
contentType
|
|
3395
|
+
init.file.id
|
|
3154
3396
|
);
|
|
3155
|
-
|
|
3397
|
+
process.stdout.write(`Uploaded ${file.id} ${file.name ?? name}
|
|
3398
|
+
`);
|
|
3399
|
+
}
|
|
3400
|
+
async function filesDownloadCommand(fileId, opts) {
|
|
3401
|
+
const cfg = await requireWorkspace(opts.workspace);
|
|
3402
|
+
const { download_url: url } = await getFileDownloadUrl(
|
|
3156
3403
|
cfg.apiUrl,
|
|
3157
3404
|
cfg.workspaceId,
|
|
3158
|
-
|
|
3159
|
-
init.release.id
|
|
3405
|
+
fileId
|
|
3160
3406
|
);
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
`);
|
|
3164
|
-
return;
|
|
3407
|
+
const res = await fetch(url);
|
|
3408
|
+
if (!res.ok) {
|
|
3409
|
+
throw new Error(`Download failed: ${res.status} ${res.statusText}`);
|
|
3165
3410
|
}
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
);
|
|
3411
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
3412
|
+
const dest = path7.resolve(opts.out ?? fileId);
|
|
3413
|
+
await fs6.writeFile(dest, buf);
|
|
3414
|
+
process.stdout.write(`Wrote ${dest}
|
|
3415
|
+
`);
|
|
3416
|
+
}
|
|
3417
|
+
async function filesRmCommand(fileId, opts) {
|
|
3418
|
+
const cfg = await requireWorkspace(opts.workspace);
|
|
3419
|
+
await deleteFile(cfg.apiUrl, cfg.workspaceId, fileId);
|
|
3420
|
+
process.stdout.write(`Deleted file ${fileId}
|
|
3421
|
+
`);
|
|
3169
3422
|
}
|
|
3170
3423
|
|
|
3171
3424
|
// src/commands/apps-init.ts
|
|
@@ -3525,6 +3778,107 @@ async function apiCommand(opts) {
|
|
|
3525
3778
|
`);
|
|
3526
3779
|
}
|
|
3527
3780
|
|
|
3781
|
+
// src/commands/usage.ts
|
|
3782
|
+
var DAY_S = 86400;
|
|
3783
|
+
async function usageCommand(opts) {
|
|
3784
|
+
const cfg = await resolveConfig({ workspaceId: opts.workspace });
|
|
3785
|
+
let sinceSeconds;
|
|
3786
|
+
if (opts.sinceDays !== void 0) {
|
|
3787
|
+
const days = Number(opts.sinceDays);
|
|
3788
|
+
if (!Number.isFinite(days) || days < 1 / 24 || days > 400) {
|
|
3789
|
+
throw new Error("--since-days must be a number between 0.05 and 400.");
|
|
3790
|
+
}
|
|
3791
|
+
sinceSeconds = Math.round(days * DAY_S);
|
|
3792
|
+
}
|
|
3793
|
+
let accountId = opts.account?.trim();
|
|
3794
|
+
if (!accountId) {
|
|
3795
|
+
if (!cfg.workspaceId) {
|
|
3796
|
+
throw new Error(
|
|
3797
|
+
"No workspace selected. Pass --account <account-id>, --workspace <id>, or run `gf workspaces use <id>`."
|
|
3798
|
+
);
|
|
3799
|
+
}
|
|
3800
|
+
const workspaces2 = await listWorkspaces(cfg.apiUrl);
|
|
3801
|
+
const ws = workspaces2.find((item) => item.id === cfg.workspaceId);
|
|
3802
|
+
const resolved = ws?.account_id ?? ws?.organization_id;
|
|
3803
|
+
if (!resolved) {
|
|
3804
|
+
throw new Error(
|
|
3805
|
+
`Could not find the account for workspace ${cfg.workspaceId}. Pass --account <account-id>.`
|
|
3806
|
+
);
|
|
3807
|
+
}
|
|
3808
|
+
accountId = resolved;
|
|
3809
|
+
}
|
|
3810
|
+
const [quota, billing] = await Promise.allSettled([
|
|
3811
|
+
getAccountQuota(cfg.apiUrl, accountId),
|
|
3812
|
+
getBillingSummary(cfg.apiUrl, accountId, sinceSeconds)
|
|
3813
|
+
]);
|
|
3814
|
+
if (opts.json) {
|
|
3815
|
+
const part = (r) => r.status === "fulfilled" ? r.value : { error: errorText(r.reason) };
|
|
3816
|
+
process.stdout.write(
|
|
3817
|
+
`${JSON.stringify({ account_id: accountId, quota: part(quota), billing: part(billing) }, null, 2)}
|
|
3818
|
+
`
|
|
3819
|
+
);
|
|
3820
|
+
if (quota.status === "rejected" && billing.status === "rejected") {
|
|
3821
|
+
process.exitCode = 1;
|
|
3822
|
+
}
|
|
3823
|
+
return;
|
|
3824
|
+
}
|
|
3825
|
+
process.stdout.write(`Account: ${accountId}
|
|
3826
|
+
`);
|
|
3827
|
+
if (quota.status === "fulfilled") {
|
|
3828
|
+
printQuota(quota.value);
|
|
3829
|
+
} else {
|
|
3830
|
+
process.stdout.write(`
|
|
3831
|
+
Quota: unavailable (${errorText(quota.reason)})
|
|
3832
|
+
`);
|
|
3833
|
+
}
|
|
3834
|
+
if (billing.status === "fulfilled") {
|
|
3835
|
+
printBilling(billing.value);
|
|
3836
|
+
} else {
|
|
3837
|
+
process.stdout.write(`
|
|
3838
|
+
Billing: unavailable (${errorText(billing.reason)})
|
|
3839
|
+
`);
|
|
3840
|
+
}
|
|
3841
|
+
if (quota.status === "rejected" && billing.status === "rejected") {
|
|
3842
|
+
process.exitCode = 1;
|
|
3843
|
+
}
|
|
3844
|
+
}
|
|
3845
|
+
function errorText(reason) {
|
|
3846
|
+
if (reason instanceof CpError && reason.status === 403) {
|
|
3847
|
+
return `${reason.message}; your user lacks permission on this account`;
|
|
3848
|
+
}
|
|
3849
|
+
return reason instanceof Error ? reason.message : String(reason);
|
|
3850
|
+
}
|
|
3851
|
+
function printQuota(q) {
|
|
3852
|
+
const out = process.stdout;
|
|
3853
|
+
out.write(`
|
|
3854
|
+
Quota (plan: ${q.plan_tier})
|
|
3855
|
+
`);
|
|
3856
|
+
for (const l of q.limits) {
|
|
3857
|
+
const limit = l.limit == null ? "unlimited" : String(l.limit);
|
|
3858
|
+
out.write(` ${l.label}: ${l.current} / ${limit} ${l.unit}
|
|
3859
|
+
`);
|
|
3860
|
+
}
|
|
3861
|
+
if (q.note) out.write(` note: ${q.note}
|
|
3862
|
+
`);
|
|
3863
|
+
}
|
|
3864
|
+
function printBilling(b) {
|
|
3865
|
+
const out = process.stdout;
|
|
3866
|
+
const label = b.is_estimate === false ? "Billing" : "Billing ESTIMATE (not a final bill)";
|
|
3867
|
+
out.write(`
|
|
3868
|
+
${label}: ${b.period_start} to ${b.period_end}
|
|
3869
|
+
`);
|
|
3870
|
+
out.write(
|
|
3871
|
+
` CU: compute ${b.breakdown.compute}, storage ${b.breakdown.storage}, network ${b.breakdown.network}, ai ${b.breakdown.ai} (adjusted ${b.adjusted_cu})
|
|
3872
|
+
`
|
|
3873
|
+
);
|
|
3874
|
+
out.write(` indicative amount: $${b.final_bill_usd.toFixed(2)} USD
|
|
3875
|
+
`);
|
|
3876
|
+
if (b.pricing_final === false) out.write(" pricing is not final (placeholder rates)\n");
|
|
3877
|
+
if (b.ai_usage_truncated) out.write(" AI usage may be understated (log window truncated)\n");
|
|
3878
|
+
if (b.note) out.write(` note: ${b.note}
|
|
3879
|
+
`);
|
|
3880
|
+
}
|
|
3881
|
+
|
|
3528
3882
|
// src/commands/identities.ts
|
|
3529
3883
|
async function identitiesListCommand(opts) {
|
|
3530
3884
|
const cfg = await resolveConfig({ workspaceId: opts.workspace });
|
|
@@ -3620,7 +3974,7 @@ m2m.command("token").description(
|
|
|
3620
3974
|
"Exchange GROUNDFLOOR_M2M_CLIENT_ID + SECRET for a short CP JWT (not gf token)"
|
|
3621
3975
|
).option("-w, --workspace <id>", "Workspace id override").option("--client-id <id>", "M2M client_id (else GROUNDFLOOR_M2M_CLIENT_ID)").option(
|
|
3622
3976
|
"--client-secret <secret>",
|
|
3623
|
-
"M2M secret (
|
|
3977
|
+
"M2M secret (prefer GROUNDFLOOR_M2M_CLIENT_SECRET: argv is visible in `ps` and shell history)"
|
|
3624
3978
|
).option("--json", "Output the full token response as JSON").action(
|
|
3625
3979
|
(opts) => run(
|
|
3626
3980
|
() => m2mTokenCommand({
|
|
@@ -3641,17 +3995,23 @@ workspaces.command("use <workspaceId>").description("Set the default workspace")
|
|
|
3641
3995
|
);
|
|
3642
3996
|
var coderunner = program.command("coderunner").alias("cr").description("Coderunner commands");
|
|
3643
3997
|
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)));
|
|
3644
|
-
coderunner.command("status").description("Show coderunner status
|
|
3645
|
-
coderunner.command("cancel-deployment").
|
|
3998
|
+
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)));
|
|
3999
|
+
coderunner.command("cancel-deployment").alias("delete-deployment").description(
|
|
4000
|
+
"DELETE the current (or --deployment-id) deployment. This is not an in-flight cancel: it takes the live service down"
|
|
4001
|
+
).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(
|
|
4002
|
+
"-y, --yes",
|
|
4003
|
+
"Skip the confirmation (required when there is no interactive terminal)"
|
|
4004
|
+
).action(
|
|
3646
4005
|
(opts) => run(
|
|
3647
4006
|
() => cancelDeploymentCommand({
|
|
3648
4007
|
workspace: opts.workspace,
|
|
3649
4008
|
coderunner: opts.coderunner,
|
|
3650
|
-
deploymentId: opts.deploymentId
|
|
4009
|
+
deploymentId: opts.deploymentId,
|
|
4010
|
+
yes: opts.yes
|
|
3651
4011
|
})
|
|
3652
4012
|
)
|
|
3653
4013
|
);
|
|
3654
|
-
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(
|
|
4014
|
+
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(
|
|
3655
4015
|
(opts) => run(
|
|
3656
4016
|
() => logsCommand({
|
|
3657
4017
|
workspace: opts.workspace,
|
|
@@ -3684,13 +4044,15 @@ coderunner.command("vault-token").description(
|
|
|
3684
4044
|
})
|
|
3685
4045
|
)
|
|
3686
4046
|
);
|
|
3687
|
-
var domains = program.command("domains").description(
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
domains.command("
|
|
4047
|
+
var domains = program.command("domains").description(
|
|
4048
|
+
"Manage custom domains (CNAME) for an App (standalone / service apps). Not available on standalone coderunners"
|
|
4049
|
+
);
|
|
4050
|
+
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)));
|
|
4051
|
+
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)));
|
|
4052
|
+
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(
|
|
3691
4053
|
(domain, opts) => run(() => domainsRemoveCommand(domain, opts))
|
|
3692
4054
|
);
|
|
3693
|
-
domains.command("verify <domain>").description("Re-check the DNS records for a custom domain").
|
|
4055
|
+
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(
|
|
3694
4056
|
(domain, opts) => run(() => domainsVerifyCommand(domain, opts))
|
|
3695
4057
|
);
|
|
3696
4058
|
var secrets = program.command("secrets").description("Manage workspace secrets");
|
|
@@ -3779,8 +4141,11 @@ var dataplane = program.command("dataplane").description("Workspace Dataplane se
|
|
|
3779
4141
|
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)));
|
|
3780
4142
|
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)));
|
|
3781
4143
|
program.command("deploy").description(
|
|
3782
|
-
"Package code and deploy as a coderunner (not an App)"
|
|
3783
|
-
).option(
|
|
4144
|
+
"Package code and deploy as a coderunner (not an App). Env var VALUES are not stored: resend them with -e on every deploy"
|
|
4145
|
+
).option(
|
|
4146
|
+
"-w, --workspace <id>",
|
|
4147
|
+
"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"
|
|
4148
|
+
).option("--api-url <url>", "Control Plane base URL").option(
|
|
3784
4149
|
"-c, --coderunner <id>",
|
|
3785
4150
|
"Deploy into an existing coderunner id (skip find/create)"
|
|
3786
4151
|
).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(
|
|
@@ -3791,13 +4156,19 @@ program.command("deploy").description(
|
|
|
3791
4156
|
'Cron for --workload-type schedule (or groundfloor.json "schedule")'
|
|
3792
4157
|
).option(
|
|
3793
4158
|
"--app-id <id>",
|
|
3794
|
-
"Bind
|
|
4159
|
+
"Bind a NEWLY CREATED coderunner as a helper of this App (ignored, with a warning, when an existing coderunner is reused)"
|
|
3795
4160
|
).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(
|
|
3796
4161
|
"--secret <KEY>",
|
|
3797
|
-
"Inject this workspace secret by reference (repeatable).
|
|
4162
|
+
"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",
|
|
3798
4163
|
collect,
|
|
3799
4164
|
[]
|
|
4165
|
+
).option(
|
|
4166
|
+
"-y, --yes",
|
|
4167
|
+
"Confirm updating an existing same-named coderunner (required with no interactive terminal). Does not bypass the saved-default-workspace check: pass -w"
|
|
3800
4168
|
).option("-m, --message <text>", "Version description").option("--skip-deploy", "Upload a new version but do not deploy it").action((opts) => run(() => deployCommand(opts)));
|
|
4169
|
+
program.command("usage").description(
|
|
4170
|
+
"Show account quota/usage and the billing estimate (read-only). The account is the billing parent of the workspace, not the workspace itself"
|
|
4171
|
+
).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)));
|
|
3801
4172
|
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(
|
|
3802
4173
|
(path11, opts) => run(
|
|
3803
4174
|
() => apiCommand({
|