@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.
Files changed (3) hide show
  1. package/README.md +19 -0
  2. package/dist/index.js +459 -81
  3. 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 path9 from "path";
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, path10) {
558
+ async function cpGet(apiUrl, path11) {
559
559
  const token = await getValidAccessToken();
560
- const res = await fetch(`${apiUrl}${path10}`, {
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, path10, body) {
571
+ async function cpPostJson(apiUrl, path11, body) {
572
572
  const token = await getValidAccessToken();
573
- const res = await fetch(`${apiUrl}${path10}`, {
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, path10) {
587
+ async function cpDelete(apiUrl, path11) {
588
588
  const token = await getValidAccessToken();
589
- const res = await fetch(`${apiUrl}${path10}`, {
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, path10, body) {
597
+ async function cpPutJson(apiUrl, path11, body) {
598
598
  const token = await getValidAccessToken();
599
- const res = await fetch(`${apiUrl}${path10}`, {
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, path10, body) {
613
+ async function cpPatchJson(apiUrl, path11, body) {
614
614
  const token = await getValidAccessToken();
615
- const res = await fetch(`${apiUrl}${path10}`, {
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, path10, body, contentType) {
629
+ async function cpPutBytes(apiUrl, path11, body, contentType) {
630
630
  const token = await getValidAccessToken();
631
- const res = await fetch(`${apiUrl}${path10}`, {
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, payload) {
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
- payload
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
- process.stdout.write(`${cr.id} ${name} [${status}]
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 path4 from "path";
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 path2 from "path";
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 = path2.join(current, entry.name);
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 = path2.join(sourceDir, rel);
1618
+ const abs = path3.join(sourceDir, rel);
1472
1619
  try {
1473
1620
  const data = await fs2.readFile(abs);
1474
- zip.addFile(rel.split(path2.sep).join("/"), data);
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(path2.join(os2.tmpdir(), "gf-deploy-"));
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 = path2.resolve(opts.path ?? process.cwd());
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 files2 = useGit ? await gitFileList(sourceDir) : await walkFileList(sourceDir);
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 { zip, fileCount: files2.length, sourceDir, cleanup, gitSha };
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(path2.join(dir, "groundfloor.json"), "utf8");
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(path2.join(dir, name));
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 path3 from "path";
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(path3.join(sourceDir, "Dockerfile"));
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(path3.join(sourceDir, "package.json"));
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 ${path4.resolve(opts.path ?? process.cwd())}...`
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(path4.join(pkg.sourceDir, "Dockerfile"));
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) : path4.basename(path4.resolve(opts.path ?? process.cwd())));
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(cfg.apiUrl, cfg.workspaceId, coderunnerId, {
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
- process.stdout.write(`${JSON.stringify(out, null, 2)}
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 path5 from "path";
2400
+ import path6 from "path";
2146
2401
  function guessContentType(filename) {
2147
- const ext = path5.extname(filename).toLowerCase();
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 = path5.resolve(filePath);
2437
+ const resolved = path6.resolve(filePath);
2183
2438
  const buf = await fs5.readFile(resolved);
2184
- const name = opts.name ?? path5.basename(resolved);
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 = path5.resolve(opts.out ?? fileId);
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 path6 from "path";
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 path10 of pathset) {
2280
- const parent = parentDir(path10);
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 names = zip.getEntries().filter((entry) => !entry.isDirectory).map((entry) => normalizeZipEntryName(entry.entryName));
2328
- const entryRel = chooseRemoteEntry(names);
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 names = /* @__PURE__ */ new Set();
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) names.add(match[1]);
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
- names.add(match[1]);
2616
+ names2.add(match[1]);
2362
2617
  }
2363
- return [...names];
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 names = zipRootNames(bytes);
2383
- const present = new Set(names.map((name) => basename(name)));
2384
- const entryRel = chooseRemoteEntry(names);
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 = names.filter((name) => {
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(path6.resolve(filePath));
2691
+ return readJsonObject(path7.resolve(filePath));
2437
2692
  }
2438
2693
  async function findSiblingManifest(bundlePath) {
2439
- const dir = await isDirectory(bundlePath) ? bundlePath : path6.dirname(bundlePath);
2440
- const candidate = path6.join(dir, "groundfloor.manifest.json");
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 = path6.resolve(opts.path ?? "release.zip");
2599
- const bundlePath = await isDirectory(inputPath) ? path6.join(inputPath, "release.zip") : 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 = path6.extname(bundlePath).toLowerCase();
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 ${path6.basename(bundlePath)}. ` + initThenPublishHint(app.slug)
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 path7 from "path";
2933
+ import path8 from "path";
2676
2934
  import AdmZip4 from "adm-zip";
2677
2935
 
2678
2936
  // src/version.ts
2679
- var GF_VERSION = "0.1.8";
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
- path7.join(extracted, "shell-starter-kit"),
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(path7.join(extracted, entry.name));
2983
+ candidates.push(path8.join(extracted, entry.name));
2717
2984
  }
2718
2985
  }
2719
2986
  for (const candidate of candidates) {
2720
- if (await pathExists(path7.join(candidate, "groundfloor.manifest.json"))) {
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 = path7.resolve(opts.dir?.trim() || path7.join(process.cwd(), slug));
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(path7.join(os3.tmpdir(), "gf-starter-kit-"));
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 = path7.join(dest, "src", "appIdentity.ts");
2827
- const manifestPath = path7.join(dest, "groundfloor.manifest.json");
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 path8 from "path";
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(path8.dirname(file), { recursive: true, mode: 448 });
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 payload object", "{}").option("--json", "Output raw JSON").action(
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("-m, --message <text>", "Version description").option("--skip-deploy", "Upload a new version but do not deploy it").action((opts) => run(() => deployCommand(opts)));
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: path9.join(paths.CONFIG_DIR, "update-check.json")
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.9",
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.5.16",
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
  },