@groundfloorcloud/cli 0.1.9 → 0.1.10
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 +19 -0
- package/dist/index.js +459 -81
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -147,6 +147,25 @@ wrong identity for testing Coderunner Vault/Files as the workload.
|
|
|
147
147
|
| `-m, --message <text>` | Version description. |
|
|
148
148
|
| `--skip-deploy` | Upload a new version but do not deploy it. |
|
|
149
149
|
|
|
150
|
+
### What is never uploaded
|
|
151
|
+
|
|
152
|
+
`gf deploy` drops credential files from the package on **both** paths — the
|
|
153
|
+
plain-directory walk and the git file list (which includes untracked files that
|
|
154
|
+
no `.gitignore` rule covers). Excluded by name: `.env*`, `.npmrc`, `.yarnrc*`,
|
|
155
|
+
`.netrc`/`_netrc`, `.pypirc`, `.git-credentials`, `.htpasswd`, `.pgpass`,
|
|
156
|
+
`.s3cfg`, `id_rsa`/`id_dsa`/`id_ecdsa`/`id_ed25519` (but not their `.pub`
|
|
157
|
+
halves), `*.pem`, `*.key`, `*.p12`, `*.pfx`, `*.jks`, `*.keystore`, `*.ppk`,
|
|
158
|
+
`*.kdbx`, `*.gpg`, `*.tfstate*`, and anything under `.ssh/`, `.aws/`, `.azure/`,
|
|
159
|
+
`.gcloud/`, `.gnupg/`, `.docker/`, `.kube/`, `.terraform/` or `.chef/`.
|
|
160
|
+
|
|
161
|
+
Every exclusion is printed, and so is every dot-file that survived the filter.
|
|
162
|
+
There is no flag to turn it off: pass values with `gf secrets` or
|
|
163
|
+
`-e KEY=VALUE` instead.
|
|
164
|
+
|
|
165
|
+
This is a **denylist**, so it only knows names. A secret in a file called
|
|
166
|
+
`config.json`, `settings.py` or `serviceAccount.json` is still uploaded —
|
|
167
|
+
check what the packaged file list says before deploying.
|
|
168
|
+
|
|
150
169
|
## Publish a Shell app
|
|
151
170
|
|
|
152
171
|
Product **Apps** are packaging (Shell Module Federation remotes, or a
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
-
import
|
|
5
|
+
import path10 from "path";
|
|
6
6
|
|
|
7
7
|
// src/auth/store.ts
|
|
8
8
|
import { promises as fs } from "fs";
|
|
@@ -555,9 +555,9 @@ async function parseError(res) {
|
|
|
555
555
|
}
|
|
556
556
|
return `${res.status} ${detail}`;
|
|
557
557
|
}
|
|
558
|
-
async function cpGet(apiUrl,
|
|
558
|
+
async function cpGet(apiUrl, path11) {
|
|
559
559
|
const token = await getValidAccessToken();
|
|
560
|
-
const res = await fetch(`${apiUrl}${
|
|
560
|
+
const res = await fetch(`${apiUrl}${path11}`, {
|
|
561
561
|
headers: {
|
|
562
562
|
Authorization: `Bearer ${token}`,
|
|
563
563
|
Accept: "application/json"
|
|
@@ -568,9 +568,9 @@ async function cpGet(apiUrl, path10) {
|
|
|
568
568
|
}
|
|
569
569
|
return await res.json();
|
|
570
570
|
}
|
|
571
|
-
async function cpPostJson(apiUrl,
|
|
571
|
+
async function cpPostJson(apiUrl, path11, body) {
|
|
572
572
|
const token = await getValidAccessToken();
|
|
573
|
-
const res = await fetch(`${apiUrl}${
|
|
573
|
+
const res = await fetch(`${apiUrl}${path11}`, {
|
|
574
574
|
method: "POST",
|
|
575
575
|
headers: {
|
|
576
576
|
Authorization: `Bearer ${token}`,
|
|
@@ -584,9 +584,9 @@ async function cpPostJson(apiUrl, path10, body) {
|
|
|
584
584
|
}
|
|
585
585
|
return await res.json();
|
|
586
586
|
}
|
|
587
|
-
async function cpDelete(apiUrl,
|
|
587
|
+
async function cpDelete(apiUrl, path11) {
|
|
588
588
|
const token = await getValidAccessToken();
|
|
589
|
-
const res = await fetch(`${apiUrl}${
|
|
589
|
+
const res = await fetch(`${apiUrl}${path11}`, {
|
|
590
590
|
method: "DELETE",
|
|
591
591
|
headers: { Authorization: `Bearer ${token}`, Accept: "application/json" }
|
|
592
592
|
});
|
|
@@ -594,9 +594,9 @@ async function cpDelete(apiUrl, path10) {
|
|
|
594
594
|
throw new CpError(res.status, await parseError(res));
|
|
595
595
|
}
|
|
596
596
|
}
|
|
597
|
-
async function cpPutJson(apiUrl,
|
|
597
|
+
async function cpPutJson(apiUrl, path11, body) {
|
|
598
598
|
const token = await getValidAccessToken();
|
|
599
|
-
const res = await fetch(`${apiUrl}${
|
|
599
|
+
const res = await fetch(`${apiUrl}${path11}`, {
|
|
600
600
|
method: "PUT",
|
|
601
601
|
headers: {
|
|
602
602
|
Authorization: `Bearer ${token}`,
|
|
@@ -610,9 +610,9 @@ async function cpPutJson(apiUrl, path10, body) {
|
|
|
610
610
|
}
|
|
611
611
|
return await res.json();
|
|
612
612
|
}
|
|
613
|
-
async function cpPatchJson(apiUrl,
|
|
613
|
+
async function cpPatchJson(apiUrl, path11, body) {
|
|
614
614
|
const token = await getValidAccessToken();
|
|
615
|
-
const res = await fetch(`${apiUrl}${
|
|
615
|
+
const res = await fetch(`${apiUrl}${path11}`, {
|
|
616
616
|
method: "PATCH",
|
|
617
617
|
headers: {
|
|
618
618
|
Authorization: `Bearer ${token}`,
|
|
@@ -626,9 +626,9 @@ async function cpPatchJson(apiUrl, path10, body) {
|
|
|
626
626
|
}
|
|
627
627
|
return await res.json();
|
|
628
628
|
}
|
|
629
|
-
async function cpPutBytes(apiUrl,
|
|
629
|
+
async function cpPutBytes(apiUrl, path11, body, contentType) {
|
|
630
630
|
const token = await getValidAccessToken();
|
|
631
|
-
const res = await fetch(`${apiUrl}${
|
|
631
|
+
const res = await fetch(`${apiUrl}${path11}`, {
|
|
632
632
|
method: "PUT",
|
|
633
633
|
headers: {
|
|
634
634
|
Authorization: `Bearer ${token}`,
|
|
@@ -760,13 +760,13 @@ function getCoderunnerStatus(apiUrl, workspaceId, coderunnerId) {
|
|
|
760
760
|
)}/coderunners/${encodeURIComponent(coderunnerId)}/status`
|
|
761
761
|
);
|
|
762
762
|
}
|
|
763
|
-
function runCoderunner(apiUrl, workspaceId, coderunnerId,
|
|
763
|
+
function runCoderunner(apiUrl, workspaceId, coderunnerId, event = {}) {
|
|
764
764
|
return cpPostJson(
|
|
765
765
|
apiUrl,
|
|
766
766
|
`/v1/workspaces/${encodeURIComponent(
|
|
767
767
|
workspaceId
|
|
768
768
|
)}/coderunners/${encodeURIComponent(coderunnerId)}/run`,
|
|
769
|
-
|
|
769
|
+
event
|
|
770
770
|
);
|
|
771
771
|
}
|
|
772
772
|
function getProcessLogs(apiUrl, workspaceId, query = {}) {
|
|
@@ -962,13 +962,49 @@ async function mintM2mToken(apiUrl, workspaceId, clientId, clientSecret) {
|
|
|
962
962
|
}
|
|
963
963
|
return await res.json();
|
|
964
964
|
}
|
|
965
|
-
function mintCoderunnerVaultToken(apiUrl, workspaceId, coderunnerId) {
|
|
965
|
+
function mintCoderunnerVaultToken(apiUrl, workspaceId, coderunnerId, body = {}) {
|
|
966
966
|
return cpPostJson(
|
|
967
967
|
apiUrl,
|
|
968
968
|
`${wsPath(workspaceId, `/coderunners/${encodeURIComponent(coderunnerId)}/vault-token`)}`,
|
|
969
|
-
|
|
969
|
+
body
|
|
970
|
+
);
|
|
971
|
+
}
|
|
972
|
+
async function listWorkloadIdentities(apiUrl, workspaceId) {
|
|
973
|
+
const data = await cpGet(
|
|
974
|
+
apiUrl,
|
|
975
|
+
wsPath(workspaceId, "/workload-identities")
|
|
976
|
+
);
|
|
977
|
+
return data.identities ?? [];
|
|
978
|
+
}
|
|
979
|
+
function mintWorkloadIdentityVaultToken(apiUrl, workspaceId, identityId, body = {}) {
|
|
980
|
+
return cpPostJson(
|
|
981
|
+
apiUrl,
|
|
982
|
+
wsPath(
|
|
983
|
+
workspaceId,
|
|
984
|
+
`/workload-identities/${encodeURIComponent(identityId)}/vault-token`
|
|
985
|
+
),
|
|
986
|
+
body
|
|
970
987
|
);
|
|
971
988
|
}
|
|
989
|
+
async function controlPlaneRequest(apiUrl, method, path11, body) {
|
|
990
|
+
const token = await getValidAccessToken();
|
|
991
|
+
const res = await fetch(`${apiUrl}${path11}`, {
|
|
992
|
+
method,
|
|
993
|
+
headers: {
|
|
994
|
+
Authorization: `Bearer ${token}`,
|
|
995
|
+
Accept: "application/json",
|
|
996
|
+
...body !== void 0 ? { "Content-Type": "application/json" } : {}
|
|
997
|
+
},
|
|
998
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
999
|
+
});
|
|
1000
|
+
if (res.status === 204) return void 0;
|
|
1001
|
+
if (!res.ok) {
|
|
1002
|
+
throw new CpError(res.status, await parseError(res));
|
|
1003
|
+
}
|
|
1004
|
+
const text = await res.text();
|
|
1005
|
+
if (!text) return void 0;
|
|
1006
|
+
return JSON.parse(text);
|
|
1007
|
+
}
|
|
972
1008
|
|
|
973
1009
|
// src/commands/whoami.ts
|
|
974
1010
|
async function whoamiCommand() {
|
|
@@ -1082,7 +1118,8 @@ async function coderunnerListCommand(opts) {
|
|
|
1082
1118
|
for (const cr of coderunners) {
|
|
1083
1119
|
const name = cr.name ?? cr.slug ?? "";
|
|
1084
1120
|
const status = cr.status ?? "";
|
|
1085
|
-
|
|
1121
|
+
const type = cr.workload_type ?? "function";
|
|
1122
|
+
process.stdout.write(`${cr.id} ${name} [${type}] [${status}]
|
|
1086
1123
|
`);
|
|
1087
1124
|
}
|
|
1088
1125
|
}
|
|
@@ -1123,6 +1160,13 @@ async function vaultTokenCommand(opts) {
|
|
|
1123
1160
|
`);
|
|
1124
1161
|
return;
|
|
1125
1162
|
}
|
|
1163
|
+
if (data.refresh_secret) {
|
|
1164
|
+
process.stderr.write(
|
|
1165
|
+
"Save refresh_secret now; it is not shown again unless you rotate.\n"
|
|
1166
|
+
);
|
|
1167
|
+
process.stderr.write(`refresh_secret=${data.refresh_secret}
|
|
1168
|
+
`);
|
|
1169
|
+
}
|
|
1126
1170
|
process.stderr.write(
|
|
1127
1171
|
`expires_in=${data.expires_in}s identity=${data.workload_identity_id}
|
|
1128
1172
|
`
|
|
@@ -1399,13 +1443,13 @@ portal: ${processLogPortalUrl(
|
|
|
1399
1443
|
|
|
1400
1444
|
// src/commands/deploy.ts
|
|
1401
1445
|
import { promises as fs4 } from "fs";
|
|
1402
|
-
import
|
|
1446
|
+
import path5 from "path";
|
|
1403
1447
|
|
|
1404
1448
|
// src/deploy/package.ts
|
|
1405
1449
|
import { execFile } from "child_process";
|
|
1406
1450
|
import { promises as fs2 } from "fs";
|
|
1407
1451
|
import os2 from "os";
|
|
1408
|
-
import
|
|
1452
|
+
import path3 from "path";
|
|
1409
1453
|
import { promisify } from "util";
|
|
1410
1454
|
import AdmZip from "adm-zip";
|
|
1411
1455
|
|
|
@@ -1414,6 +1458,109 @@ function isGitCommitSha(ref) {
|
|
|
1414
1458
|
return /^[0-9a-f]{7,40}$/i.test(ref);
|
|
1415
1459
|
}
|
|
1416
1460
|
|
|
1461
|
+
// src/deploy/secret-files.ts
|
|
1462
|
+
import path2 from "path";
|
|
1463
|
+
var DENIED_NAMES = /* @__PURE__ */ new Set([
|
|
1464
|
+
".netrc",
|
|
1465
|
+
"_netrc",
|
|
1466
|
+
".npmrc",
|
|
1467
|
+
".yarnrc",
|
|
1468
|
+
".yarnrc.yml",
|
|
1469
|
+
".pypirc",
|
|
1470
|
+
".git-credentials",
|
|
1471
|
+
".htpasswd",
|
|
1472
|
+
".htdigest",
|
|
1473
|
+
".pgpass",
|
|
1474
|
+
".dockercfg",
|
|
1475
|
+
".s3cfg",
|
|
1476
|
+
".pdbrc",
|
|
1477
|
+
".envrc",
|
|
1478
|
+
".my.cnf",
|
|
1479
|
+
"my.cnf",
|
|
1480
|
+
".boto",
|
|
1481
|
+
".databrickscfg",
|
|
1482
|
+
"kubeconfig",
|
|
1483
|
+
"kube.config",
|
|
1484
|
+
"deploy_key",
|
|
1485
|
+
"deploykey",
|
|
1486
|
+
"credentials",
|
|
1487
|
+
"credentials.json",
|
|
1488
|
+
"credentials.yaml",
|
|
1489
|
+
"credentials.yml",
|
|
1490
|
+
"secrets.json",
|
|
1491
|
+
"secrets.yaml",
|
|
1492
|
+
"secrets.yml"
|
|
1493
|
+
]);
|
|
1494
|
+
var DENIED_SEGMENTS = /* @__PURE__ */ new Set([
|
|
1495
|
+
".ssh",
|
|
1496
|
+
".aws",
|
|
1497
|
+
".azure",
|
|
1498
|
+
".gcloud",
|
|
1499
|
+
".gnupg",
|
|
1500
|
+
".docker",
|
|
1501
|
+
".kube",
|
|
1502
|
+
".terraform",
|
|
1503
|
+
".chef",
|
|
1504
|
+
".pki"
|
|
1505
|
+
]);
|
|
1506
|
+
var DENIED_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
1507
|
+
".pem",
|
|
1508
|
+
".key",
|
|
1509
|
+
".p12",
|
|
1510
|
+
".pfx",
|
|
1511
|
+
".jks",
|
|
1512
|
+
".keystore",
|
|
1513
|
+
".ppk",
|
|
1514
|
+
".kdbx",
|
|
1515
|
+
".gpg",
|
|
1516
|
+
".tfstate",
|
|
1517
|
+
".tfvars"
|
|
1518
|
+
]);
|
|
1519
|
+
var SSH_PRIVATE_KEY_RE = /^id_(rsa|dsa|ecdsa|ed25519)(_[\w.-]+)?$/;
|
|
1520
|
+
var BACKUP_SUFFIX_RE = /(~|\.(bak|old|orig|save|backup|swp|swo|tmp|copy))$/;
|
|
1521
|
+
function normaliseName(name) {
|
|
1522
|
+
let out = name.toLowerCase();
|
|
1523
|
+
for (let i = 0; i < 8; i++) {
|
|
1524
|
+
const next = out.replace(BACKUP_SUFFIX_RE, "");
|
|
1525
|
+
if (next === out) break;
|
|
1526
|
+
out = next;
|
|
1527
|
+
}
|
|
1528
|
+
return out;
|
|
1529
|
+
}
|
|
1530
|
+
function isCredentialFile(relPath) {
|
|
1531
|
+
const parts = relPath.split("/").filter(Boolean);
|
|
1532
|
+
const rawName = parts[parts.length - 1] ?? "";
|
|
1533
|
+
const name = normaliseName(rawName);
|
|
1534
|
+
for (const segment of parts.slice(0, -1)) {
|
|
1535
|
+
if (DENIED_SEGMENTS.has(segment.toLowerCase())) return true;
|
|
1536
|
+
}
|
|
1537
|
+
if (DENIED_SEGMENTS.has(name)) return true;
|
|
1538
|
+
if (name === ".env" || name.startsWith(".env.")) return true;
|
|
1539
|
+
if (DENIED_NAMES.has(name)) return true;
|
|
1540
|
+
if (SSH_PRIVATE_KEY_RE.test(name)) return true;
|
|
1541
|
+
if (name.endsWith(".tfvars.json") || name.endsWith(".tfstate.json")) {
|
|
1542
|
+
return true;
|
|
1543
|
+
}
|
|
1544
|
+
if (DENIED_EXTENSIONS.has(path2.extname(name))) return true;
|
|
1545
|
+
return false;
|
|
1546
|
+
}
|
|
1547
|
+
function isDotPath(relPath) {
|
|
1548
|
+
return relPath.split("/").some((part) => part.startsWith("."));
|
|
1549
|
+
}
|
|
1550
|
+
function filterCredentialFiles(files2) {
|
|
1551
|
+
const kept = [];
|
|
1552
|
+
const excluded = [];
|
|
1553
|
+
for (const rel of files2) {
|
|
1554
|
+
if (isCredentialFile(rel)) excluded.push(rel);
|
|
1555
|
+
else kept.push(rel);
|
|
1556
|
+
}
|
|
1557
|
+
return {
|
|
1558
|
+
kept,
|
|
1559
|
+
excluded: excluded.sort(),
|
|
1560
|
+
dotfilesIncluded: kept.filter(isDotPath).sort()
|
|
1561
|
+
};
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1417
1564
|
// src/deploy/package.ts
|
|
1418
1565
|
var exec = promisify(execFile);
|
|
1419
1566
|
var DEFAULT_IGNORES = /* @__PURE__ */ new Set([
|
|
@@ -1453,7 +1600,7 @@ async function walkFileList(dir) {
|
|
|
1453
1600
|
const entries = await fs2.readdir(current, { withFileTypes: true });
|
|
1454
1601
|
for (const entry of entries) {
|
|
1455
1602
|
if (DEFAULT_IGNORES.has(entry.name)) continue;
|
|
1456
|
-
const abs =
|
|
1603
|
+
const abs = path3.join(current, entry.name);
|
|
1457
1604
|
const relPath = rel ? `${rel}/${entry.name}` : entry.name;
|
|
1458
1605
|
if (entry.isDirectory()) {
|
|
1459
1606
|
await walk(abs, relPath);
|
|
@@ -1468,17 +1615,17 @@ async function walkFileList(dir) {
|
|
|
1468
1615
|
async function buildZip(sourceDir, files2) {
|
|
1469
1616
|
const zip = new AdmZip();
|
|
1470
1617
|
for (const rel of files2) {
|
|
1471
|
-
const abs =
|
|
1618
|
+
const abs = path3.join(sourceDir, rel);
|
|
1472
1619
|
try {
|
|
1473
1620
|
const data = await fs2.readFile(abs);
|
|
1474
|
-
zip.addFile(rel.split(
|
|
1621
|
+
zip.addFile(rel.split(path3.sep).join("/"), data);
|
|
1475
1622
|
} catch {
|
|
1476
1623
|
}
|
|
1477
1624
|
}
|
|
1478
1625
|
return zip.toBuffer();
|
|
1479
1626
|
}
|
|
1480
1627
|
async function cloneRepo(url, ref) {
|
|
1481
|
-
const tmp = await fs2.mkdtemp(
|
|
1628
|
+
const tmp = await fs2.mkdtemp(path3.join(os2.tmpdir(), "gf-deploy-"));
|
|
1482
1629
|
try {
|
|
1483
1630
|
if (ref && isGitCommitSha(ref)) {
|
|
1484
1631
|
await exec("git", ["init"], { cwd: tmp });
|
|
@@ -1509,16 +1656,27 @@ async function packageSource(opts) {
|
|
|
1509
1656
|
baseDir = await cloneRepo(opts.git, opts.ref);
|
|
1510
1657
|
cleanup = baseDir;
|
|
1511
1658
|
} else {
|
|
1512
|
-
baseDir =
|
|
1659
|
+
baseDir = path3.resolve(opts.path ?? process.cwd());
|
|
1660
|
+
}
|
|
1661
|
+
const sourceDir = opts.subdir ? path3.resolve(baseDir, opts.subdir) : baseDir;
|
|
1662
|
+
if (sourceDir !== baseDir && !sourceDir.startsWith(baseDir + path3.sep)) {
|
|
1663
|
+
if (cleanup) {
|
|
1664
|
+
await fs2.rm(cleanup, { recursive: true, force: true }).catch(() => {
|
|
1665
|
+
});
|
|
1666
|
+
}
|
|
1667
|
+
throw new Error(
|
|
1668
|
+
`--subdir "${opts.subdir}" resolves to ${sourceDir}, outside ${baseDir}.`
|
|
1669
|
+
);
|
|
1513
1670
|
}
|
|
1514
|
-
const sourceDir = opts.subdir ? path2.join(baseDir, opts.subdir) : baseDir;
|
|
1515
1671
|
try {
|
|
1516
1672
|
const stat = await fs2.stat(sourceDir);
|
|
1517
1673
|
if (!stat.isDirectory()) {
|
|
1518
1674
|
throw new Error(`Not a directory: ${sourceDir}`);
|
|
1519
1675
|
}
|
|
1520
1676
|
const useGit = await isGitRepo(sourceDir);
|
|
1521
|
-
const
|
|
1677
|
+
const listed = useGit ? await gitFileList(sourceDir) : await walkFileList(sourceDir);
|
|
1678
|
+
const filtered = filterCredentialFiles(listed);
|
|
1679
|
+
const files2 = filtered.kept;
|
|
1522
1680
|
if (files2.length === 0) {
|
|
1523
1681
|
throw new Error(
|
|
1524
1682
|
`No files to package in ${sourceDir} (everything ignored or empty).`
|
|
@@ -1535,7 +1693,14 @@ async function packageSource(opts) {
|
|
|
1535
1693
|
gitSha = null;
|
|
1536
1694
|
}
|
|
1537
1695
|
}
|
|
1538
|
-
return {
|
|
1696
|
+
return {
|
|
1697
|
+
zip,
|
|
1698
|
+
fileCount: files2.length,
|
|
1699
|
+
sourceDir,
|
|
1700
|
+
cleanup,
|
|
1701
|
+
gitSha,
|
|
1702
|
+
filtered
|
|
1703
|
+
};
|
|
1539
1704
|
} catch (err) {
|
|
1540
1705
|
if (cleanup) {
|
|
1541
1706
|
await fs2.rm(cleanup, { recursive: true, force: true }).catch(() => {
|
|
@@ -1546,7 +1711,7 @@ async function packageSource(opts) {
|
|
|
1546
1711
|
}
|
|
1547
1712
|
async function readProjectManifest(dir) {
|
|
1548
1713
|
try {
|
|
1549
|
-
const raw = await fs2.readFile(
|
|
1714
|
+
const raw = await fs2.readFile(path3.join(dir, "groundfloor.json"), "utf8");
|
|
1550
1715
|
return JSON.parse(raw);
|
|
1551
1716
|
} catch {
|
|
1552
1717
|
return null;
|
|
@@ -1555,7 +1720,7 @@ async function readProjectManifest(dir) {
|
|
|
1555
1720
|
async function detectRuntime(dir) {
|
|
1556
1721
|
const has = async (name) => {
|
|
1557
1722
|
try {
|
|
1558
|
-
await fs2.access(
|
|
1723
|
+
await fs2.access(path3.join(dir, name));
|
|
1559
1724
|
return true;
|
|
1560
1725
|
} catch {
|
|
1561
1726
|
return false;
|
|
@@ -1569,7 +1734,7 @@ async function detectRuntime(dir) {
|
|
|
1569
1734
|
|
|
1570
1735
|
// src/deploy/preflight.ts
|
|
1571
1736
|
import { promises as fs3 } from "fs";
|
|
1572
|
-
import
|
|
1737
|
+
import path4 from "path";
|
|
1573
1738
|
var NODE_BOOTSTRAP_MISSING_START = 'Node packages without a Dockerfile use the platform bootstrap image (CMD ["npm","start"], EXPOSE 8080). Add package.json "scripts": { "start": "node <entrypoint>" }. The process must listen on process.env.PORT || 8080 and bind 0.0.0.0 (not 127.0.0.1).';
|
|
1574
1739
|
var NODE_BOOTSTRAP_MISSING_PACKAGE = 'Node packages without a Dockerfile need package.json with "scripts.start" (bootstrap image runs `npm start` and probes 0.0.0.0:8080).';
|
|
1575
1740
|
var NODE_DOCKERFILE_NPM_START_MISSING = 'Dockerfile runs `npm start` but package.json has no "scripts.start". Add "scripts": { "start": "node <entrypoint>" }.';
|
|
@@ -1584,10 +1749,10 @@ async function readOptional(filePath) {
|
|
|
1584
1749
|
}
|
|
1585
1750
|
}
|
|
1586
1751
|
async function preflightNodePackage(sourceDir) {
|
|
1587
|
-
const dockerfile = await readOptional(
|
|
1752
|
+
const dockerfile = await readOptional(path4.join(sourceDir, "Dockerfile"));
|
|
1588
1753
|
const needsStartScript = dockerfile == null || dockerfileRunsNpmStart(dockerfile);
|
|
1589
1754
|
if (!needsStartScript) return;
|
|
1590
|
-
const raw = await readOptional(
|
|
1755
|
+
const raw = await readOptional(path4.join(sourceDir, "package.json"));
|
|
1591
1756
|
if (raw == null) {
|
|
1592
1757
|
throw new Error(
|
|
1593
1758
|
dockerfile == null ? NODE_BOOTSTRAP_MISSING_PACKAGE : NODE_DOCKERFILE_NPM_START_MISSING
|
|
@@ -1630,6 +1795,45 @@ function formatDeployTarget(input) {
|
|
|
1630
1795
|
return `Deploy target: workspace=${label} cell=${cell} api=${input.apiUrl}`;
|
|
1631
1796
|
}
|
|
1632
1797
|
|
|
1798
|
+
// src/credential-env.ts
|
|
1799
|
+
var CREDENTIAL_KEYS = /* @__PURE__ */ new Set([
|
|
1800
|
+
"DATABASE_URL",
|
|
1801
|
+
"DATABASE_URI",
|
|
1802
|
+
"DATABASE_PASSWORD",
|
|
1803
|
+
"DATABASE_USER",
|
|
1804
|
+
"DATABASE_USERNAME",
|
|
1805
|
+
"DB_URL",
|
|
1806
|
+
"DB_URI",
|
|
1807
|
+
"POSTGRES_URL",
|
|
1808
|
+
"POSTGRES_URI",
|
|
1809
|
+
"POSTGRES_PASSWORD",
|
|
1810
|
+
"POSTGRES_USER",
|
|
1811
|
+
"MYSQL_URL",
|
|
1812
|
+
"MYSQL_PASSWORD",
|
|
1813
|
+
"MONGO_URI",
|
|
1814
|
+
"MONGODB_URI",
|
|
1815
|
+
"MONGODB_URL"
|
|
1816
|
+
]);
|
|
1817
|
+
var CREDENTIAL_SUFFIXES = [
|
|
1818
|
+
"_PASSWORD",
|
|
1819
|
+
"_SECRET",
|
|
1820
|
+
"_PRIVATE_KEY",
|
|
1821
|
+
"_API_KEY",
|
|
1822
|
+
"_ACCESS_KEY",
|
|
1823
|
+
"_SECRET_KEY"
|
|
1824
|
+
];
|
|
1825
|
+
function isCredentialEnvKey(key) {
|
|
1826
|
+
const cleaned = key.trim();
|
|
1827
|
+
if (!cleaned) return false;
|
|
1828
|
+
const upper = cleaned.toUpperCase();
|
|
1829
|
+
if (CREDENTIAL_KEYS.has(upper)) return true;
|
|
1830
|
+
if (upper.startsWith("MANAGED_DB_")) return true;
|
|
1831
|
+
return CREDENTIAL_SUFFIXES.some((suffix) => upper.endsWith(suffix));
|
|
1832
|
+
}
|
|
1833
|
+
function credentialEnvKeys(env) {
|
|
1834
|
+
return Object.keys(env).filter((key) => isCredentialEnvKey(key)).sort();
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1633
1837
|
// src/commands/deploy.ts
|
|
1634
1838
|
var FALLBACK_RUNTIMES = /* @__PURE__ */ new Set([
|
|
1635
1839
|
"python",
|
|
@@ -1665,6 +1869,22 @@ function parseEnvPairs(pairs) {
|
|
|
1665
1869
|
}
|
|
1666
1870
|
var log = (msg) => process.stderr.write(`${msg}
|
|
1667
1871
|
`);
|
|
1872
|
+
function names(list, max = 10) {
|
|
1873
|
+
return list.length > max ? `${list.slice(0, max).join(", ")} \u2026 and ${list.length - max} more` : list.join(", ");
|
|
1874
|
+
}
|
|
1875
|
+
function reportFiltering(filtered) {
|
|
1876
|
+
if (filtered.excluded.length) {
|
|
1877
|
+
log(
|
|
1878
|
+
`Excluded ${filtered.excluded.length} credential file(s): ${names(filtered.excluded)}`
|
|
1879
|
+
);
|
|
1880
|
+
log(" (use `gf secrets` or `--env KEY=VALUE` to pass values to the app)");
|
|
1881
|
+
}
|
|
1882
|
+
if (filtered.dotfilesIncluded.length) {
|
|
1883
|
+
log(
|
|
1884
|
+
`Including ${filtered.dotfilesIncluded.length} dot-file(s): ${names(filtered.dotfilesIncluded)}`
|
|
1885
|
+
);
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1668
1888
|
async function failDeploy(opts) {
|
|
1669
1889
|
throw new Error(await diagnoseDeployFailure(opts));
|
|
1670
1890
|
}
|
|
@@ -1694,7 +1914,7 @@ async function deployCommand(opts) {
|
|
|
1694
1914
|
})
|
|
1695
1915
|
);
|
|
1696
1916
|
log(
|
|
1697
|
-
opts.git ? `Cloning ${opts.git}${opts.ref ? ` (${opts.ref})` : ""}...` : `Packaging ${
|
|
1917
|
+
opts.git ? `Cloning ${opts.git}${opts.ref ? ` (${opts.ref})` : ""}...` : `Packaging ${path5.resolve(opts.path ?? process.cwd())}...`
|
|
1698
1918
|
);
|
|
1699
1919
|
const pkg = await packageSource({
|
|
1700
1920
|
path: opts.path,
|
|
@@ -1706,6 +1926,7 @@ async function deployCommand(opts) {
|
|
|
1706
1926
|
const manifest = await readProjectManifest(pkg.sourceDir) ?? {};
|
|
1707
1927
|
const sizeKb = (pkg.zip.byteLength / 1024).toFixed(1);
|
|
1708
1928
|
log(`Packaged ${pkg.fileCount} file(s), ${sizeKb} KB`);
|
|
1929
|
+
reportFiltering(pkg.filtered);
|
|
1709
1930
|
const detected = await detectRuntime(pkg.sourceDir);
|
|
1710
1931
|
const runtime = opts.runtime ?? manifest.runtime ?? detected ?? "python";
|
|
1711
1932
|
let allowedRuntimes = FALLBACK_RUNTIMES;
|
|
@@ -1729,7 +1950,7 @@ async function deployCommand(opts) {
|
|
|
1729
1950
|
}
|
|
1730
1951
|
if (workloadType === "service") {
|
|
1731
1952
|
try {
|
|
1732
|
-
await fs4.access(
|
|
1953
|
+
await fs4.access(path5.join(pkg.sourceDir, "Dockerfile"));
|
|
1733
1954
|
} catch {
|
|
1734
1955
|
throw new Error(
|
|
1735
1956
|
'workload type "service" (also called Deployment) requires a Dockerfile at the project root.'
|
|
@@ -1746,7 +1967,7 @@ async function deployCommand(opts) {
|
|
|
1746
1967
|
});
|
|
1747
1968
|
let coderunnerId = opts.coderunner;
|
|
1748
1969
|
if (!coderunnerId) {
|
|
1749
|
-
const name = opts.name ?? manifest.name ?? (opts.git ? nameFromGitUrl(opts.git) :
|
|
1970
|
+
const name = opts.name ?? manifest.name ?? (opts.git ? nameFromGitUrl(opts.git) : path5.basename(path5.resolve(opts.path ?? process.cwd())));
|
|
1750
1971
|
const slug = manifest.slug ?? slugify(name);
|
|
1751
1972
|
const existing = await listCoderunners(cfg.apiUrl, ws);
|
|
1752
1973
|
const match = existing.find((c) => c.slug === slug || c.name === name);
|
|
@@ -1760,7 +1981,8 @@ async function deployCommand(opts) {
|
|
|
1760
1981
|
slug,
|
|
1761
1982
|
runtime,
|
|
1762
1983
|
workload_type: workloadType,
|
|
1763
|
-
...opts.appId ? { app_id: opts.appId } : {}
|
|
1984
|
+
...opts.appId ? { app_id: opts.appId } : {},
|
|
1985
|
+
...schedule ? { schedule } : {}
|
|
1764
1986
|
});
|
|
1765
1987
|
coderunnerId = created.id;
|
|
1766
1988
|
log(`Created coderunner ${created.id}`);
|
|
@@ -1840,13 +2062,21 @@ async function deployCommand(opts) {
|
|
|
1840
2062
|
...manifest.env ?? {},
|
|
1841
2063
|
...parseEnvPairs(opts.env)
|
|
1842
2064
|
};
|
|
2065
|
+
const plaintextSecrets = credentialEnvKeys(envVariable);
|
|
2066
|
+
if (plaintextSecrets.length) {
|
|
2067
|
+
throw new Error(
|
|
2068
|
+
`Do not pass secrets as --env: ${plaintextSecrets.join(", ")}. Store them with \`gf secrets set KEY\` and deploy with \`--secret KEY\`.`
|
|
2069
|
+
);
|
|
2070
|
+
}
|
|
2071
|
+
const injectSecretKeys = (opts.secret ?? []).map((key) => key.trim()).filter(Boolean);
|
|
1843
2072
|
log("Deploying...");
|
|
1844
2073
|
const result = await deployCoderunner(cfg.apiUrl, ws, coderunnerId, {
|
|
1845
2074
|
version_id: uploaded.version_id,
|
|
1846
2075
|
cpu: opts.cpu ?? manifest.cpu,
|
|
1847
2076
|
memory: opts.memory ?? manifest.memory,
|
|
1848
2077
|
envVariable: Object.keys(envVariable).length ? envVariable : void 0,
|
|
1849
|
-
schedule
|
|
2078
|
+
schedule,
|
|
2079
|
+
inject_secret_keys: injectSecretKeys.length ? injectSecretKeys : void 0
|
|
1850
2080
|
});
|
|
1851
2081
|
log("Waiting for deployment...");
|
|
1852
2082
|
const deployDeadline = Date.now() + DEPLOY_POLL_TIMEOUT_MS;
|
|
@@ -1938,9 +2168,12 @@ async function runCommand(opts) {
|
|
|
1938
2168
|
cfg.workspaceId,
|
|
1939
2169
|
opts.coderunner
|
|
1940
2170
|
);
|
|
1941
|
-
const result = await runCoderunner(
|
|
2171
|
+
const result = await runCoderunner(
|
|
2172
|
+
cfg.apiUrl,
|
|
2173
|
+
cfg.workspaceId,
|
|
2174
|
+
coderunnerId,
|
|
1942
2175
|
payload
|
|
1943
|
-
|
|
2176
|
+
);
|
|
1944
2177
|
if (opts.json) {
|
|
1945
2178
|
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
1946
2179
|
`);
|
|
@@ -1968,7 +2201,29 @@ async function statusCommand(opts) {
|
|
|
1968
2201
|
getDeployment(cfg.apiUrl, cfg.workspaceId, coderunnerId)
|
|
1969
2202
|
]);
|
|
1970
2203
|
const out = { coderunner_id: coderunnerId, status, deployment };
|
|
1971
|
-
|
|
2204
|
+
if (opts.json) {
|
|
2205
|
+
process.stdout.write(`${JSON.stringify(out, null, 2)}
|
|
2206
|
+
`);
|
|
2207
|
+
return;
|
|
2208
|
+
}
|
|
2209
|
+
const state = status && typeof status === "object" ? status.state : void 0;
|
|
2210
|
+
const type = String(state?.workload_type ?? "function");
|
|
2211
|
+
const schedule = state?.schedule ? String(state.schedule) : "\u2014";
|
|
2212
|
+
const lastJob = state?.last_job_name ? `${state.last_job_name} (${state.last_job_status ?? "?"})` : "\u2014";
|
|
2213
|
+
process.stdout.write(`coderunner: ${coderunnerId}
|
|
2214
|
+
`);
|
|
2215
|
+
process.stdout.write(`type: ${type}
|
|
2216
|
+
`);
|
|
2217
|
+
process.stdout.write(`schedule: ${schedule}
|
|
2218
|
+
`);
|
|
2219
|
+
process.stdout.write(`last_job: ${lastJob}
|
|
2220
|
+
`);
|
|
2221
|
+
if (deployment?.status != null) {
|
|
2222
|
+
process.stdout.write(`deployment: ${deployment.id ?? "\u2014"} [${deployment.status}]
|
|
2223
|
+
`);
|
|
2224
|
+
}
|
|
2225
|
+
process.stdout.write(`
|
|
2226
|
+
${JSON.stringify(out, null, 2)}
|
|
1972
2227
|
`);
|
|
1973
2228
|
}
|
|
1974
2229
|
|
|
@@ -2142,9 +2397,9 @@ async function secretsRmCommand(key, opts) {
|
|
|
2142
2397
|
|
|
2143
2398
|
// src/commands/files.ts
|
|
2144
2399
|
import { promises as fs5 } from "fs";
|
|
2145
|
-
import
|
|
2400
|
+
import path6 from "path";
|
|
2146
2401
|
function guessContentType(filename) {
|
|
2147
|
-
const ext =
|
|
2402
|
+
const ext = path6.extname(filename).toLowerCase();
|
|
2148
2403
|
const types = {
|
|
2149
2404
|
".json": "application/json",
|
|
2150
2405
|
".txt": "text/plain",
|
|
@@ -2179,9 +2434,9 @@ async function filesListCommand(opts) {
|
|
|
2179
2434
|
}
|
|
2180
2435
|
async function filesUploadCommand(filePath, opts) {
|
|
2181
2436
|
const cfg = await requireWorkspace(opts.workspace);
|
|
2182
|
-
const resolved =
|
|
2437
|
+
const resolved = path6.resolve(filePath);
|
|
2183
2438
|
const buf = await fs5.readFile(resolved);
|
|
2184
|
-
const name = opts.name ??
|
|
2439
|
+
const name = opts.name ?? path6.basename(resolved);
|
|
2185
2440
|
const contentType = guessContentType(name);
|
|
2186
2441
|
const init = await initFileUpload(cfg.apiUrl, cfg.workspaceId, {
|
|
2187
2442
|
name,
|
|
@@ -2218,7 +2473,7 @@ async function filesDownloadCommand(fileId, opts) {
|
|
|
2218
2473
|
throw new Error(`Download failed: ${res.status} ${res.statusText}`);
|
|
2219
2474
|
}
|
|
2220
2475
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
2221
|
-
const dest =
|
|
2476
|
+
const dest = path6.resolve(opts.out ?? fileId);
|
|
2222
2477
|
await fs5.writeFile(dest, buf);
|
|
2223
2478
|
process.stdout.write(`Wrote ${dest}
|
|
2224
2479
|
`);
|
|
@@ -2232,7 +2487,7 @@ async function filesRmCommand(fileId, opts) {
|
|
|
2232
2487
|
|
|
2233
2488
|
// src/commands/apps.ts
|
|
2234
2489
|
import { promises as fs6 } from "fs";
|
|
2235
|
-
import
|
|
2490
|
+
import path7 from "path";
|
|
2236
2491
|
|
|
2237
2492
|
// src/release-zip.ts
|
|
2238
2493
|
import AdmZip2 from "adm-zip";
|
|
@@ -2276,8 +2531,8 @@ function flattenFederatedBundlePlan(relpaths) {
|
|
|
2276
2531
|
const promoteDirs = /* @__PURE__ */ new Set();
|
|
2277
2532
|
const entryDir = parentDir(entry);
|
|
2278
2533
|
if (entryDir) promoteDirs.add(entryDir);
|
|
2279
|
-
for (const
|
|
2280
|
-
const parent = parentDir(
|
|
2534
|
+
for (const path11 of pathset) {
|
|
2535
|
+
const parent = parentDir(path11);
|
|
2281
2536
|
if (parent === "assets" || parent.endsWith("/assets") && parent.split("/").length - 1 < 2) {
|
|
2282
2537
|
promoteDirs.add(parent);
|
|
2283
2538
|
}
|
|
@@ -2324,8 +2579,8 @@ function flattenFederatedReleaseZip(bytes) {
|
|
|
2324
2579
|
return zip.toBuffer();
|
|
2325
2580
|
}
|
|
2326
2581
|
function rewriteRemoteEntryInZip(zip) {
|
|
2327
|
-
const
|
|
2328
|
-
const entryRel = chooseRemoteEntry(
|
|
2582
|
+
const names2 = zip.getEntries().filter((entry) => !entry.isDirectory).map((entry) => normalizeZipEntryName(entry.entryName));
|
|
2583
|
+
const entryRel = chooseRemoteEntry(names2);
|
|
2329
2584
|
if (!entryRel) return;
|
|
2330
2585
|
const zipEntry = zip.getEntries().find((item) => {
|
|
2331
2586
|
if (item.isDirectory) return false;
|
|
@@ -2349,18 +2604,18 @@ function initThenPublishHint(slug) {
|
|
|
2349
2604
|
return `gf apps init --slug ${s} && cd ${s} && npm install && npm run release && gf apps publish --path release.zip`;
|
|
2350
2605
|
}
|
|
2351
2606
|
function extractReferencedSiblings(remoteEntrySource) {
|
|
2352
|
-
const
|
|
2607
|
+
const names2 = /* @__PURE__ */ new Set();
|
|
2353
2608
|
for (const match of remoteEntrySource.matchAll(
|
|
2354
2609
|
/["']\.\/(?:assets\/)?([^"'/]+\.(?:js|mjs|cjs|css))["']/g
|
|
2355
2610
|
)) {
|
|
2356
|
-
if (match[1] !== REMOTE_ENTRY)
|
|
2611
|
+
if (match[1] !== REMOTE_ENTRY) names2.add(match[1]);
|
|
2357
2612
|
}
|
|
2358
2613
|
for (const match of remoteEntrySource.matchAll(
|
|
2359
2614
|
/["']([A-Za-z0-9._-]+\.css)["']/g
|
|
2360
2615
|
)) {
|
|
2361
|
-
|
|
2616
|
+
names2.add(match[1]);
|
|
2362
2617
|
}
|
|
2363
|
-
return [...
|
|
2618
|
+
return [...names2];
|
|
2364
2619
|
}
|
|
2365
2620
|
function extractFederationScope(remoteEntrySource) {
|
|
2366
2621
|
return remoteEntrySource.match(/css__([A-Za-z0-9_-]+)__/)?.[1];
|
|
@@ -2379,9 +2634,9 @@ function readZipFile(bytes, rel) {
|
|
|
2379
2634
|
}
|
|
2380
2635
|
function assertFederatedReleaseZip(bytes, slug) {
|
|
2381
2636
|
const hint = initThenPublishHint(slug);
|
|
2382
|
-
const
|
|
2383
|
-
const present = new Set(
|
|
2384
|
-
const entryRel = chooseRemoteEntry(
|
|
2637
|
+
const names2 = zipRootNames(bytes);
|
|
2638
|
+
const present = new Set(names2.map((name) => basename(name)));
|
|
2639
|
+
const entryRel = chooseRemoteEntry(names2);
|
|
2385
2640
|
if (!entryRel) {
|
|
2386
2641
|
throw new Error(`Zip has no remoteEntry.js. ${hint}`);
|
|
2387
2642
|
}
|
|
@@ -2391,7 +2646,7 @@ function assertFederatedReleaseZip(bytes, slug) {
|
|
|
2391
2646
|
}
|
|
2392
2647
|
const siblings = extractReferencedSiblings(source);
|
|
2393
2648
|
const missing = siblings.filter((name) => !present.has(name));
|
|
2394
|
-
const jsOrCssAtRoot =
|
|
2649
|
+
const jsOrCssAtRoot = names2.filter((name) => {
|
|
2395
2650
|
if (name.includes("/")) return false;
|
|
2396
2651
|
const lower = name.toLowerCase();
|
|
2397
2652
|
return lower.endsWith(".js") || lower.endsWith(".mjs") || lower.endsWith(".css");
|
|
@@ -2433,11 +2688,11 @@ async function readJsonObject(filePath) {
|
|
|
2433
2688
|
}
|
|
2434
2689
|
async function maybeReadManifest(filePath) {
|
|
2435
2690
|
if (!filePath) return void 0;
|
|
2436
|
-
return readJsonObject(
|
|
2691
|
+
return readJsonObject(path7.resolve(filePath));
|
|
2437
2692
|
}
|
|
2438
2693
|
async function findSiblingManifest(bundlePath) {
|
|
2439
|
-
const dir = await isDirectory(bundlePath) ? bundlePath :
|
|
2440
|
-
const candidate =
|
|
2694
|
+
const dir = await isDirectory(bundlePath) ? bundlePath : path7.dirname(bundlePath);
|
|
2695
|
+
const candidate = path7.join(dir, "groundfloor.manifest.json");
|
|
2441
2696
|
try {
|
|
2442
2697
|
await fs6.access(candidate);
|
|
2443
2698
|
} catch {
|
|
@@ -2595,8 +2850,8 @@ Do not scaffold src/App.tsx from scratch \u2014 BrowserRouter in App.tsx crashes
|
|
|
2595
2850
|
}
|
|
2596
2851
|
async function appsPublishCommand(appRef, opts) {
|
|
2597
2852
|
const cfg = await requireWorkspace(opts.workspace);
|
|
2598
|
-
const inputPath =
|
|
2599
|
-
const bundlePath = await isDirectory(inputPath) ?
|
|
2853
|
+
const inputPath = path7.resolve(opts.path ?? "release.zip");
|
|
2854
|
+
const bundlePath = await isDirectory(inputPath) ? path7.join(inputPath, "release.zip") : inputPath;
|
|
2600
2855
|
let bytes;
|
|
2601
2856
|
try {
|
|
2602
2857
|
bytes = await fs6.readFile(bundlePath);
|
|
@@ -2626,11 +2881,11 @@ async function appsPublishCommand(appRef, opts) {
|
|
|
2626
2881
|
`Releases are only for shell_federated apps (this app is ${app.app_kind}). Use \`gf deploy\` for coderunner workloads.`
|
|
2627
2882
|
);
|
|
2628
2883
|
}
|
|
2629
|
-
const ext =
|
|
2884
|
+
const ext = path7.extname(bundlePath).toLowerCase();
|
|
2630
2885
|
const isZip = ext === ".zip";
|
|
2631
2886
|
if (!isZip) {
|
|
2632
2887
|
throw new Error(
|
|
2633
|
-
`Publish release.zip from \`npm run release\`, not a lone ${
|
|
2888
|
+
`Publish release.zip from \`npm run release\`, not a lone ${path7.basename(bundlePath)}. ` + initThenPublishHint(app.slug)
|
|
2634
2889
|
);
|
|
2635
2890
|
}
|
|
2636
2891
|
bytes = prepareReleaseZip(bytes, app.slug);
|
|
@@ -2643,7 +2898,10 @@ async function appsPublishCommand(appRef, opts) {
|
|
|
2643
2898
|
}
|
|
2644
2899
|
const init = await initAppReleaseUpload(cfg.apiUrl, cfg.workspaceId, app.id, {
|
|
2645
2900
|
bundle,
|
|
2646
|
-
label: opts.label
|
|
2901
|
+
label: opts.label,
|
|
2902
|
+
// Required since wave 5: the server pins this into the presigned PUT and
|
|
2903
|
+
// checks it against the quota before minting a URL. Omitting it is a 422.
|
|
2904
|
+
size_bytes: bytes.byteLength
|
|
2647
2905
|
});
|
|
2648
2906
|
await uploadAppReleaseContent(
|
|
2649
2907
|
cfg.apiUrl,
|
|
@@ -2672,11 +2930,20 @@ async function appsPublishCommand(appRef, opts) {
|
|
|
2672
2930
|
// src/commands/apps-init.ts
|
|
2673
2931
|
import { promises as fs7 } from "fs";
|
|
2674
2932
|
import os3 from "os";
|
|
2675
|
-
import
|
|
2933
|
+
import path8 from "path";
|
|
2676
2934
|
import AdmZip4 from "adm-zip";
|
|
2677
2935
|
|
|
2678
2936
|
// src/version.ts
|
|
2679
|
-
|
|
2937
|
+
import { readFileSync } from "fs";
|
|
2938
|
+
import { dirname, join } from "path";
|
|
2939
|
+
import { fileURLToPath } from "url";
|
|
2940
|
+
var moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
2941
|
+
function readPackageVersion() {
|
|
2942
|
+
const pkgPath = join(moduleDir, "..", "package.json");
|
|
2943
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
2944
|
+
return pkg.version;
|
|
2945
|
+
}
|
|
2946
|
+
var GF_VERSION = readPackageVersion();
|
|
2680
2947
|
var GF_NPM_PACKAGE = "@groundfloorcloud/cli";
|
|
2681
2948
|
|
|
2682
2949
|
// src/commands/apps-init.ts
|
|
@@ -2707,17 +2974,17 @@ async function isEmptyDir(dir) {
|
|
|
2707
2974
|
}
|
|
2708
2975
|
async function findKitRoot(extracted) {
|
|
2709
2976
|
const candidates = [
|
|
2710
|
-
|
|
2977
|
+
path8.join(extracted, "shell-starter-kit"),
|
|
2711
2978
|
extracted
|
|
2712
2979
|
];
|
|
2713
2980
|
const entries = await fs7.readdir(extracted, { withFileTypes: true });
|
|
2714
2981
|
for (const entry of entries) {
|
|
2715
2982
|
if (entry.isDirectory()) {
|
|
2716
|
-
candidates.push(
|
|
2983
|
+
candidates.push(path8.join(extracted, entry.name));
|
|
2717
2984
|
}
|
|
2718
2985
|
}
|
|
2719
2986
|
for (const candidate of candidates) {
|
|
2720
|
-
if (await pathExists(
|
|
2987
|
+
if (await pathExists(path8.join(candidate, "groundfloor.manifest.json"))) {
|
|
2721
2988
|
return candidate;
|
|
2722
2989
|
}
|
|
2723
2990
|
}
|
|
@@ -2799,7 +3066,7 @@ async function appsInitCommand(opts) {
|
|
|
2799
3066
|
`);
|
|
2800
3067
|
}
|
|
2801
3068
|
const name = opts.name?.trim() || titleFromSlug(slug);
|
|
2802
|
-
const dest =
|
|
3069
|
+
const dest = path8.resolve(opts.dir?.trim() || path8.join(process.cwd(), slug));
|
|
2803
3070
|
const cfg = await resolveConfig();
|
|
2804
3071
|
const cell = cfg.environment ?? "production";
|
|
2805
3072
|
const kitUrl = opts.kitUrl?.trim() || starterKitZipUrl(cell);
|
|
@@ -2814,7 +3081,7 @@ async function appsInitCommand(opts) {
|
|
|
2814
3081
|
const bytes = await downloadKit(kitUrl);
|
|
2815
3082
|
const zip = new AdmZip4(bytes);
|
|
2816
3083
|
assertSafeZipEntries(zip);
|
|
2817
|
-
const tmp = await fs7.mkdtemp(
|
|
3084
|
+
const tmp = await fs7.mkdtemp(path8.join(os3.tmpdir(), "gf-starter-kit-"));
|
|
2818
3085
|
try {
|
|
2819
3086
|
zip.extractAllTo(tmp, true);
|
|
2820
3087
|
const kitRoot = await findKitRoot(tmp);
|
|
@@ -2823,8 +3090,8 @@ async function appsInitCommand(opts) {
|
|
|
2823
3090
|
} finally {
|
|
2824
3091
|
await fs7.rm(tmp, { recursive: true, force: true });
|
|
2825
3092
|
}
|
|
2826
|
-
const identityPath =
|
|
2827
|
-
const manifestPath =
|
|
3093
|
+
const identityPath = path8.join(dest, "src", "appIdentity.ts");
|
|
3094
|
+
const manifestPath = path8.join(dest, "groundfloor.manifest.json");
|
|
2828
3095
|
const identity = await fs7.readFile(identityPath, "utf8");
|
|
2829
3096
|
await fs7.writeFile(identityPath, stampAppIdentity(identity, slug), "utf8");
|
|
2830
3097
|
const manifest = await fs7.readFile(manifestPath, "utf8");
|
|
@@ -2901,7 +3168,7 @@ async function dataplaneProvisionCommand(opts) {
|
|
|
2901
3168
|
|
|
2902
3169
|
// src/update-check.ts
|
|
2903
3170
|
import { promises as fs8 } from "fs";
|
|
2904
|
-
import
|
|
3171
|
+
import path9 from "path";
|
|
2905
3172
|
var UPDATE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
2906
3173
|
function isNewer(latest, current) {
|
|
2907
3174
|
const a = latest.split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
@@ -2954,7 +3221,7 @@ async function readUpdateCache(file) {
|
|
|
2954
3221
|
}
|
|
2955
3222
|
}
|
|
2956
3223
|
async function writeUpdateCache(file, cache) {
|
|
2957
|
-
await fs8.mkdir(
|
|
3224
|
+
await fs8.mkdir(path9.dirname(file), { recursive: true, mode: 448 });
|
|
2958
3225
|
await fs8.writeFile(file, `${JSON.stringify(cache)}
|
|
2959
3226
|
`, { mode: 384 });
|
|
2960
3227
|
}
|
|
@@ -2992,6 +3259,90 @@ function commanderExitCode(err) {
|
|
|
2992
3259
|
return 0;
|
|
2993
3260
|
}
|
|
2994
3261
|
|
|
3262
|
+
// src/commands/api.ts
|
|
3263
|
+
var METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE"]);
|
|
3264
|
+
async function apiCommand(opts) {
|
|
3265
|
+
const cfg = await resolveConfig({ workspaceId: opts.workspace });
|
|
3266
|
+
const method = (opts.method ?? "GET").toUpperCase();
|
|
3267
|
+
if (!METHODS.has(method)) {
|
|
3268
|
+
throw new Error(`Unsupported method ${method}`);
|
|
3269
|
+
}
|
|
3270
|
+
let path11 = opts.path.trim();
|
|
3271
|
+
if (!path11.startsWith("/")) path11 = `/${path11}`;
|
|
3272
|
+
path11 = path11.replaceAll("{workspace_id}", cfg.workspaceId ?? "");
|
|
3273
|
+
path11 = path11.replaceAll("{workspaceId}", cfg.workspaceId ?? "");
|
|
3274
|
+
let body;
|
|
3275
|
+
if (opts.body !== void 0) {
|
|
3276
|
+
body = JSON.parse(opts.body);
|
|
3277
|
+
}
|
|
3278
|
+
const data = await controlPlaneRequest(cfg.apiUrl, method, path11, body);
|
|
3279
|
+
if (data === void 0) {
|
|
3280
|
+
process.stdout.write("\n");
|
|
3281
|
+
return;
|
|
3282
|
+
}
|
|
3283
|
+
process.stdout.write(`${JSON.stringify(data, null, 2)}
|
|
3284
|
+
`);
|
|
3285
|
+
}
|
|
3286
|
+
|
|
3287
|
+
// src/commands/identities.ts
|
|
3288
|
+
async function identitiesListCommand(opts) {
|
|
3289
|
+
const cfg = await resolveConfig({ workspaceId: opts.workspace });
|
|
3290
|
+
if (!cfg.workspaceId) {
|
|
3291
|
+
throw new Error(
|
|
3292
|
+
"No workspace selected. Run `gf workspaces use <id>` or pass --workspace <id>."
|
|
3293
|
+
);
|
|
3294
|
+
}
|
|
3295
|
+
const identities2 = await listWorkloadIdentities(cfg.apiUrl, cfg.workspaceId);
|
|
3296
|
+
if (opts.json) {
|
|
3297
|
+
process.stdout.write(`${JSON.stringify(identities2, null, 2)}
|
|
3298
|
+
`);
|
|
3299
|
+
return;
|
|
3300
|
+
}
|
|
3301
|
+
if (identities2.length === 0) {
|
|
3302
|
+
process.stdout.write("No workload identities in this workspace.\n");
|
|
3303
|
+
return;
|
|
3304
|
+
}
|
|
3305
|
+
for (const identity of identities2) {
|
|
3306
|
+
const refresh = identity.has_refresh_secret ? "refresh" : "no-refresh";
|
|
3307
|
+
process.stdout.write(
|
|
3308
|
+
`${identity.id} ${identity.slug ?? identity.name} [${refresh}]
|
|
3309
|
+
`
|
|
3310
|
+
);
|
|
3311
|
+
}
|
|
3312
|
+
}
|
|
3313
|
+
async function identitiesMintCommand(opts) {
|
|
3314
|
+
const cfg = await resolveConfig({ workspaceId: opts.workspace });
|
|
3315
|
+
if (!cfg.workspaceId) {
|
|
3316
|
+
throw new Error(
|
|
3317
|
+
"No workspace selected. Run `gf workspaces use <id>` or pass --workspace <id>."
|
|
3318
|
+
);
|
|
3319
|
+
}
|
|
3320
|
+
const data = await mintWorkloadIdentityVaultToken(
|
|
3321
|
+
cfg.apiUrl,
|
|
3322
|
+
cfg.workspaceId,
|
|
3323
|
+
opts.identity,
|
|
3324
|
+
{ rotate_refresh: Boolean(opts.rotateRefresh) }
|
|
3325
|
+
);
|
|
3326
|
+
if (opts.json) {
|
|
3327
|
+
process.stdout.write(`${JSON.stringify(data, null, 2)}
|
|
3328
|
+
`);
|
|
3329
|
+
return;
|
|
3330
|
+
}
|
|
3331
|
+
if (data.refresh_secret) {
|
|
3332
|
+
process.stderr.write(
|
|
3333
|
+
"Save refresh_secret now; it is not shown again unless you --rotate-refresh.\n"
|
|
3334
|
+
);
|
|
3335
|
+
process.stderr.write(`refresh_secret=${data.refresh_secret}
|
|
3336
|
+
`);
|
|
3337
|
+
}
|
|
3338
|
+
process.stderr.write(
|
|
3339
|
+
`expires_in=${data.expires_in}s identity=${data.workload_identity_id}
|
|
3340
|
+
`
|
|
3341
|
+
);
|
|
3342
|
+
process.stdout.write(`${data.token}
|
|
3343
|
+
`);
|
|
3344
|
+
}
|
|
3345
|
+
|
|
2995
3346
|
// src/index.ts
|
|
2996
3347
|
function collect(value, previous) {
|
|
2997
3348
|
return previous.concat([value]);
|
|
@@ -3071,7 +3422,7 @@ coderunner.command("logs").description("Show Process Log lines for a coderunner
|
|
|
3071
3422
|
})
|
|
3072
3423
|
)
|
|
3073
3424
|
);
|
|
3074
|
-
coderunner.command("run").description("Invoke a deployed function/job (smoke run)").requiredOption("-c, --coderunner <id|slug>", "Coderunner id or slug").option("-w, --workspace <id>", "Workspace id override").option("--payload <json>", "JSON
|
|
3425
|
+
coderunner.command("run").description("Invoke a deployed function/job (smoke run)").requiredOption("-c, --coderunner <id|slug>", "Coderunner id or slug").option("-w, --workspace <id>", "Workspace id override").option("--payload <json>", "Event JSON object posted as the /run body", "{}").option("--json", "Output raw JSON").action(
|
|
3075
3426
|
(opts) => run(
|
|
3076
3427
|
() => runCommand({
|
|
3077
3428
|
workspace: opts.workspace,
|
|
@@ -3182,7 +3533,34 @@ program.command("deploy").description(
|
|
|
3182
3533
|
).option(
|
|
3183
3534
|
"--app-id <id>",
|
|
3184
3535
|
"Bind newly created coderunner as a helper of this App (optional)"
|
|
3185
|
-
).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(
|
|
3536
|
+
).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(
|
|
3537
|
+
"--secret <KEY>",
|
|
3538
|
+
"Inject this workspace secret by reference (repeatable). Use for *_SECRET keys.",
|
|
3539
|
+
collect,
|
|
3540
|
+
[]
|
|
3541
|
+
).option("-m, --message <text>", "Version description").option("--skip-deploy", "Upload a new version but do not deploy it").action((opts) => run(() => deployCommand(opts)));
|
|
3542
|
+
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(
|
|
3543
|
+
(path11, opts) => run(
|
|
3544
|
+
() => apiCommand({
|
|
3545
|
+
path: path11,
|
|
3546
|
+
method: opts.method,
|
|
3547
|
+
body: opts.body,
|
|
3548
|
+
workspace: opts.workspace
|
|
3549
|
+
})
|
|
3550
|
+
)
|
|
3551
|
+
);
|
|
3552
|
+
var identities = program.command("identities").description("Workload identities (Vault principals)");
|
|
3553
|
+
identities.command("ls").alias("list").description("List workload identities").option("-w, --workspace <id>", "Workspace id override").option("--json", "Output raw JSON").action((opts) => run(() => identitiesListCommand(opts)));
|
|
3554
|
+
identities.command("mint").description("Mint a Vault JWT; first mint returns refresh_secret once").requiredOption("--identity <id>", "Workload identity id").option("-w, --workspace <id>", "Workspace id override").option("--rotate-refresh", "Rotate and return a new refresh_secret").option("--json", "Output raw JSON").action(
|
|
3555
|
+
(opts) => run(
|
|
3556
|
+
() => identitiesMintCommand({
|
|
3557
|
+
workspace: opts.workspace,
|
|
3558
|
+
identity: opts.identity,
|
|
3559
|
+
rotateRefresh: opts.rotateRefresh,
|
|
3560
|
+
json: opts.json
|
|
3561
|
+
})
|
|
3562
|
+
)
|
|
3563
|
+
);
|
|
3186
3564
|
function wireUnknownCommands(root, current, latest) {
|
|
3187
3565
|
const walk = (cmd) => {
|
|
3188
3566
|
if (cmd.commands.length === 0) return;
|
|
@@ -3206,7 +3584,7 @@ function wireUnknownCommands(root, current, latest) {
|
|
|
3206
3584
|
async function notifyUpgrade() {
|
|
3207
3585
|
if (process.env.GF_SKIP_UPDATE_CHECK === "1") return null;
|
|
3208
3586
|
const latest = await resolveLatest({
|
|
3209
|
-
cacheFile:
|
|
3587
|
+
cacheFile: path10.join(paths.CONFIG_DIR, "update-check.json")
|
|
3210
3588
|
});
|
|
3211
3589
|
if (latest && isNewer(latest, GF_VERSION)) {
|
|
3212
3590
|
process.stderr.write(`${upgradeNotice(GF_VERSION, latest)}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@groundfloorcloud/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "Groundfloor gf CLI — sign in, deploy coderunners, publish Shell apps, and manage secrets, files, and Dataplane.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -28,17 +28,20 @@
|
|
|
28
28
|
"build": "tsup",
|
|
29
29
|
"dev": "tsup --watch",
|
|
30
30
|
"typecheck": "tsc --noEmit",
|
|
31
|
+
"test": "vitest run",
|
|
32
|
+
"test:watch": "vitest",
|
|
31
33
|
"clean": "rm -rf dist *.tsbuildinfo .turbo",
|
|
32
34
|
"prepublishOnly": "npm run build"
|
|
33
35
|
},
|
|
34
36
|
"dependencies": {
|
|
35
|
-
"adm-zip": "^0.
|
|
37
|
+
"adm-zip": "^0.6.0",
|
|
36
38
|
"commander": "^12.1.0",
|
|
37
39
|
"open": "^10.1.0"
|
|
38
40
|
},
|
|
39
41
|
"devDependencies": {
|
|
40
42
|
"@types/adm-zip": "^0.5.7",
|
|
41
43
|
"@types/node": "^20",
|
|
44
|
+
"esbuild": "^0.27.7",
|
|
42
45
|
"tsup": "^8.3.0",
|
|
43
46
|
"typescript": "^5"
|
|
44
47
|
},
|