@groundfloorcloud/cli 0.1.9 → 0.1.11

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 +42 -1
  2. package/dist/index.js +718 -81
  3. package/package.json +5 -2
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # `@groundfloorcloud/cli` (`gf`)
2
2
 
3
3
  Sign in to Groundfloor, manage workspaces, and work with Coderunner,
4
- secrets, files, apps, and Dataplane from your terminal.
4
+ secrets, feature flags, files, apps, and Dataplane from your terminal.
5
5
 
6
6
  `gf login` opens a browser, you sign in, and the session is cached and
7
7
  refreshed automatically.
@@ -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
@@ -207,6 +226,27 @@ gf apps ls
207
226
  gf apps get <id|slug>
208
227
  ```
209
228
 
229
+ ## Feature flags
230
+
231
+ Per-environment toggles and config values. `-e` takes an environment id or name
232
+ (`dev`, `staging`, `prod`); without it the workspace default (`dev`) is used.
233
+
234
+ ```bash
235
+ gf flags ls -e prod
236
+ gf flags set new-checkout true -e dev # boolean (inferred)
237
+ gf flags set max-items 25 -e dev # number (inferred)
238
+ gf flags set banner '{"text":"Hi"}' --type json -e dev
239
+ gf flags get new-checkout -e prod
240
+ gf flags compare dev prod
241
+ gf flags promote dev prod --dry-run # then without --dry-run
242
+ gf flags rm new-checkout -e dev
243
+ ```
244
+
245
+ Keys are lowercase letters, digits, `-`, `_` and `.`, starting with a letter.
246
+ Apps read flags at runtime from `GET /v1/workspaces/{id}/flags/runtime`: Shell
247
+ apps with the end-user token, Coderunners with `gf m2m token` (read-only). See
248
+ https://docs.groundfloor.cloud/docs/customer-portal/feature-flags.
249
+
210
250
  ## Commands
211
251
 
212
252
  | Command | Description |
@@ -228,6 +268,7 @@ gf apps get <id|slug>
228
268
  | `gf deploy` | Package code and deploy as a coderunner. |
229
269
  | `gf domains ls\|add\|rm\|verify -c <id\|slug>` | Custom domains for a service. |
230
270
  | `gf secrets ls\|get\|set\|rm` | Workspace secrets. |
271
+ | `gf flags ls\|get\|set\|rm\|compare\|promote` | Feature flags per environment (`-e <id\|name>`). |
231
272
  | `gf files ls\|upload\|download\|rm` | Workspace files. |
232
273
  | `gf apps init --slug <slug>` | Download the official Shell starter-kit and stamp APP_ID. |
233
274
  | `gf apps ls\|get\|create\|publish` | Product apps (Shell publish / standalone wrap). |
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 = {}) {
@@ -850,6 +850,53 @@ function deleteSecret(apiUrl, workspaceId, key, environmentId) {
850
850
  })}`
851
851
  );
852
852
  }
853
+ function listFlags(apiUrl, workspaceId, environmentId) {
854
+ return cpGet(
855
+ apiUrl,
856
+ `${wsPath(workspaceId, "/flags")}${queryString({ environment_id: environmentId })}`
857
+ );
858
+ }
859
+ function getFlag(apiUrl, workspaceId, key, environmentId) {
860
+ return cpGet(
861
+ apiUrl,
862
+ `${wsPath(workspaceId, `/flags/${encodeURIComponent(key)}`)}${queryString({
863
+ environment_id: environmentId
864
+ })}`
865
+ );
866
+ }
867
+ function putFlag(apiUrl, workspaceId, key, payload, environmentId) {
868
+ return cpPutJson(
869
+ apiUrl,
870
+ `${wsPath(workspaceId, `/flags/${encodeURIComponent(key)}`)}${queryString({
871
+ environment_id: environmentId
872
+ })}`,
873
+ payload
874
+ );
875
+ }
876
+ function deleteFlag(apiUrl, workspaceId, key, environmentId) {
877
+ return cpDelete(
878
+ apiUrl,
879
+ `${wsPath(workspaceId, `/flags/${encodeURIComponent(key)}`)}${queryString({
880
+ environment_id: environmentId
881
+ })}`
882
+ );
883
+ }
884
+ function compareFlags(apiUrl, workspaceId, sourceEnvironmentId, targetEnvironmentId) {
885
+ return cpGet(
886
+ apiUrl,
887
+ `${wsPath(workspaceId, "/flags/compare")}${queryString({
888
+ source_environment_id: sourceEnvironmentId,
889
+ target_environment_id: targetEnvironmentId
890
+ })}`
891
+ );
892
+ }
893
+ function promoteFlags(apiUrl, workspaceId, body) {
894
+ return cpPostJson(
895
+ apiUrl,
896
+ wsPath(workspaceId, "/flags/promote"),
897
+ body
898
+ );
899
+ }
853
900
  function listFiles(apiUrl, workspaceId) {
854
901
  return cpGet(apiUrl, wsPath(workspaceId, "/files"));
855
902
  }
@@ -962,13 +1009,49 @@ async function mintM2mToken(apiUrl, workspaceId, clientId, clientSecret) {
962
1009
  }
963
1010
  return await res.json();
964
1011
  }
965
- function mintCoderunnerVaultToken(apiUrl, workspaceId, coderunnerId) {
1012
+ function mintCoderunnerVaultToken(apiUrl, workspaceId, coderunnerId, body = {}) {
966
1013
  return cpPostJson(
967
1014
  apiUrl,
968
1015
  `${wsPath(workspaceId, `/coderunners/${encodeURIComponent(coderunnerId)}/vault-token`)}`,
969
- {}
1016
+ body
970
1017
  );
971
1018
  }
1019
+ async function listWorkloadIdentities(apiUrl, workspaceId) {
1020
+ const data = await cpGet(
1021
+ apiUrl,
1022
+ wsPath(workspaceId, "/workload-identities")
1023
+ );
1024
+ return data.identities ?? [];
1025
+ }
1026
+ function mintWorkloadIdentityVaultToken(apiUrl, workspaceId, identityId, body = {}) {
1027
+ return cpPostJson(
1028
+ apiUrl,
1029
+ wsPath(
1030
+ workspaceId,
1031
+ `/workload-identities/${encodeURIComponent(identityId)}/vault-token`
1032
+ ),
1033
+ body
1034
+ );
1035
+ }
1036
+ async function controlPlaneRequest(apiUrl, method, path11, body) {
1037
+ const token = await getValidAccessToken();
1038
+ const res = await fetch(`${apiUrl}${path11}`, {
1039
+ method,
1040
+ headers: {
1041
+ Authorization: `Bearer ${token}`,
1042
+ Accept: "application/json",
1043
+ ...body !== void 0 ? { "Content-Type": "application/json" } : {}
1044
+ },
1045
+ body: body !== void 0 ? JSON.stringify(body) : void 0
1046
+ });
1047
+ if (res.status === 204) return void 0;
1048
+ if (!res.ok) {
1049
+ throw new CpError(res.status, await parseError(res));
1050
+ }
1051
+ const text = await res.text();
1052
+ if (!text) return void 0;
1053
+ return JSON.parse(text);
1054
+ }
972
1055
 
973
1056
  // src/commands/whoami.ts
974
1057
  async function whoamiCommand() {
@@ -1082,7 +1165,8 @@ async function coderunnerListCommand(opts) {
1082
1165
  for (const cr of coderunners) {
1083
1166
  const name = cr.name ?? cr.slug ?? "";
1084
1167
  const status = cr.status ?? "";
1085
- process.stdout.write(`${cr.id} ${name} [${status}]
1168
+ const type = cr.workload_type ?? "function";
1169
+ process.stdout.write(`${cr.id} ${name} [${type}] [${status}]
1086
1170
  `);
1087
1171
  }
1088
1172
  }
@@ -1123,6 +1207,13 @@ async function vaultTokenCommand(opts) {
1123
1207
  `);
1124
1208
  return;
1125
1209
  }
1210
+ if (data.refresh_secret) {
1211
+ process.stderr.write(
1212
+ "Save refresh_secret now; it is not shown again unless you rotate.\n"
1213
+ );
1214
+ process.stderr.write(`refresh_secret=${data.refresh_secret}
1215
+ `);
1216
+ }
1126
1217
  process.stderr.write(
1127
1218
  `expires_in=${data.expires_in}s identity=${data.workload_identity_id}
1128
1219
  `
@@ -1399,13 +1490,13 @@ portal: ${processLogPortalUrl(
1399
1490
 
1400
1491
  // src/commands/deploy.ts
1401
1492
  import { promises as fs4 } from "fs";
1402
- import path4 from "path";
1493
+ import path5 from "path";
1403
1494
 
1404
1495
  // src/deploy/package.ts
1405
1496
  import { execFile } from "child_process";
1406
1497
  import { promises as fs2 } from "fs";
1407
1498
  import os2 from "os";
1408
- import path2 from "path";
1499
+ import path3 from "path";
1409
1500
  import { promisify } from "util";
1410
1501
  import AdmZip from "adm-zip";
1411
1502
 
@@ -1414,6 +1505,109 @@ function isGitCommitSha(ref) {
1414
1505
  return /^[0-9a-f]{7,40}$/i.test(ref);
1415
1506
  }
1416
1507
 
1508
+ // src/deploy/secret-files.ts
1509
+ import path2 from "path";
1510
+ var DENIED_NAMES = /* @__PURE__ */ new Set([
1511
+ ".netrc",
1512
+ "_netrc",
1513
+ ".npmrc",
1514
+ ".yarnrc",
1515
+ ".yarnrc.yml",
1516
+ ".pypirc",
1517
+ ".git-credentials",
1518
+ ".htpasswd",
1519
+ ".htdigest",
1520
+ ".pgpass",
1521
+ ".dockercfg",
1522
+ ".s3cfg",
1523
+ ".pdbrc",
1524
+ ".envrc",
1525
+ ".my.cnf",
1526
+ "my.cnf",
1527
+ ".boto",
1528
+ ".databrickscfg",
1529
+ "kubeconfig",
1530
+ "kube.config",
1531
+ "deploy_key",
1532
+ "deploykey",
1533
+ "credentials",
1534
+ "credentials.json",
1535
+ "credentials.yaml",
1536
+ "credentials.yml",
1537
+ "secrets.json",
1538
+ "secrets.yaml",
1539
+ "secrets.yml"
1540
+ ]);
1541
+ var DENIED_SEGMENTS = /* @__PURE__ */ new Set([
1542
+ ".ssh",
1543
+ ".aws",
1544
+ ".azure",
1545
+ ".gcloud",
1546
+ ".gnupg",
1547
+ ".docker",
1548
+ ".kube",
1549
+ ".terraform",
1550
+ ".chef",
1551
+ ".pki"
1552
+ ]);
1553
+ var DENIED_EXTENSIONS = /* @__PURE__ */ new Set([
1554
+ ".pem",
1555
+ ".key",
1556
+ ".p12",
1557
+ ".pfx",
1558
+ ".jks",
1559
+ ".keystore",
1560
+ ".ppk",
1561
+ ".kdbx",
1562
+ ".gpg",
1563
+ ".tfstate",
1564
+ ".tfvars"
1565
+ ]);
1566
+ var SSH_PRIVATE_KEY_RE = /^id_(rsa|dsa|ecdsa|ed25519)(_[\w.-]+)?$/;
1567
+ var BACKUP_SUFFIX_RE = /(~|\.(bak|old|orig|save|backup|swp|swo|tmp|copy))$/;
1568
+ function normaliseName(name) {
1569
+ let out = name.toLowerCase();
1570
+ for (let i = 0; i < 8; i++) {
1571
+ const next = out.replace(BACKUP_SUFFIX_RE, "");
1572
+ if (next === out) break;
1573
+ out = next;
1574
+ }
1575
+ return out;
1576
+ }
1577
+ function isCredentialFile(relPath) {
1578
+ const parts = relPath.split("/").filter(Boolean);
1579
+ const rawName = parts[parts.length - 1] ?? "";
1580
+ const name = normaliseName(rawName);
1581
+ for (const segment of parts.slice(0, -1)) {
1582
+ if (DENIED_SEGMENTS.has(segment.toLowerCase())) return true;
1583
+ }
1584
+ if (DENIED_SEGMENTS.has(name)) return true;
1585
+ if (name === ".env" || name.startsWith(".env.")) return true;
1586
+ if (DENIED_NAMES.has(name)) return true;
1587
+ if (SSH_PRIVATE_KEY_RE.test(name)) return true;
1588
+ if (name.endsWith(".tfvars.json") || name.endsWith(".tfstate.json")) {
1589
+ return true;
1590
+ }
1591
+ if (DENIED_EXTENSIONS.has(path2.extname(name))) return true;
1592
+ return false;
1593
+ }
1594
+ function isDotPath(relPath) {
1595
+ return relPath.split("/").some((part) => part.startsWith("."));
1596
+ }
1597
+ function filterCredentialFiles(files2) {
1598
+ const kept = [];
1599
+ const excluded = [];
1600
+ for (const rel of files2) {
1601
+ if (isCredentialFile(rel)) excluded.push(rel);
1602
+ else kept.push(rel);
1603
+ }
1604
+ return {
1605
+ kept,
1606
+ excluded: excluded.sort(),
1607
+ dotfilesIncluded: kept.filter(isDotPath).sort()
1608
+ };
1609
+ }
1610
+
1417
1611
  // src/deploy/package.ts
1418
1612
  var exec = promisify(execFile);
1419
1613
  var DEFAULT_IGNORES = /* @__PURE__ */ new Set([
@@ -1453,7 +1647,7 @@ async function walkFileList(dir) {
1453
1647
  const entries = await fs2.readdir(current, { withFileTypes: true });
1454
1648
  for (const entry of entries) {
1455
1649
  if (DEFAULT_IGNORES.has(entry.name)) continue;
1456
- const abs = path2.join(current, entry.name);
1650
+ const abs = path3.join(current, entry.name);
1457
1651
  const relPath = rel ? `${rel}/${entry.name}` : entry.name;
1458
1652
  if (entry.isDirectory()) {
1459
1653
  await walk(abs, relPath);
@@ -1468,17 +1662,17 @@ async function walkFileList(dir) {
1468
1662
  async function buildZip(sourceDir, files2) {
1469
1663
  const zip = new AdmZip();
1470
1664
  for (const rel of files2) {
1471
- const abs = path2.join(sourceDir, rel);
1665
+ const abs = path3.join(sourceDir, rel);
1472
1666
  try {
1473
1667
  const data = await fs2.readFile(abs);
1474
- zip.addFile(rel.split(path2.sep).join("/"), data);
1668
+ zip.addFile(rel.split(path3.sep).join("/"), data);
1475
1669
  } catch {
1476
1670
  }
1477
1671
  }
1478
1672
  return zip.toBuffer();
1479
1673
  }
1480
1674
  async function cloneRepo(url, ref) {
1481
- const tmp = await fs2.mkdtemp(path2.join(os2.tmpdir(), "gf-deploy-"));
1675
+ const tmp = await fs2.mkdtemp(path3.join(os2.tmpdir(), "gf-deploy-"));
1482
1676
  try {
1483
1677
  if (ref && isGitCommitSha(ref)) {
1484
1678
  await exec("git", ["init"], { cwd: tmp });
@@ -1509,16 +1703,27 @@ async function packageSource(opts) {
1509
1703
  baseDir = await cloneRepo(opts.git, opts.ref);
1510
1704
  cleanup = baseDir;
1511
1705
  } else {
1512
- baseDir = path2.resolve(opts.path ?? process.cwd());
1706
+ baseDir = path3.resolve(opts.path ?? process.cwd());
1707
+ }
1708
+ const sourceDir = opts.subdir ? path3.resolve(baseDir, opts.subdir) : baseDir;
1709
+ if (sourceDir !== baseDir && !sourceDir.startsWith(baseDir + path3.sep)) {
1710
+ if (cleanup) {
1711
+ await fs2.rm(cleanup, { recursive: true, force: true }).catch(() => {
1712
+ });
1713
+ }
1714
+ throw new Error(
1715
+ `--subdir "${opts.subdir}" resolves to ${sourceDir}, outside ${baseDir}.`
1716
+ );
1513
1717
  }
1514
- const sourceDir = opts.subdir ? path2.join(baseDir, opts.subdir) : baseDir;
1515
1718
  try {
1516
1719
  const stat = await fs2.stat(sourceDir);
1517
1720
  if (!stat.isDirectory()) {
1518
1721
  throw new Error(`Not a directory: ${sourceDir}`);
1519
1722
  }
1520
1723
  const useGit = await isGitRepo(sourceDir);
1521
- const files2 = useGit ? await gitFileList(sourceDir) : await walkFileList(sourceDir);
1724
+ const listed = useGit ? await gitFileList(sourceDir) : await walkFileList(sourceDir);
1725
+ const filtered = filterCredentialFiles(listed);
1726
+ const files2 = filtered.kept;
1522
1727
  if (files2.length === 0) {
1523
1728
  throw new Error(
1524
1729
  `No files to package in ${sourceDir} (everything ignored or empty).`
@@ -1535,7 +1740,14 @@ async function packageSource(opts) {
1535
1740
  gitSha = null;
1536
1741
  }
1537
1742
  }
1538
- return { zip, fileCount: files2.length, sourceDir, cleanup, gitSha };
1743
+ return {
1744
+ zip,
1745
+ fileCount: files2.length,
1746
+ sourceDir,
1747
+ cleanup,
1748
+ gitSha,
1749
+ filtered
1750
+ };
1539
1751
  } catch (err) {
1540
1752
  if (cleanup) {
1541
1753
  await fs2.rm(cleanup, { recursive: true, force: true }).catch(() => {
@@ -1546,7 +1758,7 @@ async function packageSource(opts) {
1546
1758
  }
1547
1759
  async function readProjectManifest(dir) {
1548
1760
  try {
1549
- const raw = await fs2.readFile(path2.join(dir, "groundfloor.json"), "utf8");
1761
+ const raw = await fs2.readFile(path3.join(dir, "groundfloor.json"), "utf8");
1550
1762
  return JSON.parse(raw);
1551
1763
  } catch {
1552
1764
  return null;
@@ -1555,7 +1767,7 @@ async function readProjectManifest(dir) {
1555
1767
  async function detectRuntime(dir) {
1556
1768
  const has = async (name) => {
1557
1769
  try {
1558
- await fs2.access(path2.join(dir, name));
1770
+ await fs2.access(path3.join(dir, name));
1559
1771
  return true;
1560
1772
  } catch {
1561
1773
  return false;
@@ -1569,7 +1781,7 @@ async function detectRuntime(dir) {
1569
1781
 
1570
1782
  // src/deploy/preflight.ts
1571
1783
  import { promises as fs3 } from "fs";
1572
- import path3 from "path";
1784
+ import path4 from "path";
1573
1785
  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
1786
  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
1787
  var NODE_DOCKERFILE_NPM_START_MISSING = 'Dockerfile runs `npm start` but package.json has no "scripts.start". Add "scripts": { "start": "node <entrypoint>" }.';
@@ -1584,10 +1796,10 @@ async function readOptional(filePath) {
1584
1796
  }
1585
1797
  }
1586
1798
  async function preflightNodePackage(sourceDir) {
1587
- const dockerfile = await readOptional(path3.join(sourceDir, "Dockerfile"));
1799
+ const dockerfile = await readOptional(path4.join(sourceDir, "Dockerfile"));
1588
1800
  const needsStartScript = dockerfile == null || dockerfileRunsNpmStart(dockerfile);
1589
1801
  if (!needsStartScript) return;
1590
- const raw = await readOptional(path3.join(sourceDir, "package.json"));
1802
+ const raw = await readOptional(path4.join(sourceDir, "package.json"));
1591
1803
  if (raw == null) {
1592
1804
  throw new Error(
1593
1805
  dockerfile == null ? NODE_BOOTSTRAP_MISSING_PACKAGE : NODE_DOCKERFILE_NPM_START_MISSING
@@ -1630,6 +1842,45 @@ function formatDeployTarget(input) {
1630
1842
  return `Deploy target: workspace=${label} cell=${cell} api=${input.apiUrl}`;
1631
1843
  }
1632
1844
 
1845
+ // src/credential-env.ts
1846
+ var CREDENTIAL_KEYS = /* @__PURE__ */ new Set([
1847
+ "DATABASE_URL",
1848
+ "DATABASE_URI",
1849
+ "DATABASE_PASSWORD",
1850
+ "DATABASE_USER",
1851
+ "DATABASE_USERNAME",
1852
+ "DB_URL",
1853
+ "DB_URI",
1854
+ "POSTGRES_URL",
1855
+ "POSTGRES_URI",
1856
+ "POSTGRES_PASSWORD",
1857
+ "POSTGRES_USER",
1858
+ "MYSQL_URL",
1859
+ "MYSQL_PASSWORD",
1860
+ "MONGO_URI",
1861
+ "MONGODB_URI",
1862
+ "MONGODB_URL"
1863
+ ]);
1864
+ var CREDENTIAL_SUFFIXES = [
1865
+ "_PASSWORD",
1866
+ "_SECRET",
1867
+ "_PRIVATE_KEY",
1868
+ "_API_KEY",
1869
+ "_ACCESS_KEY",
1870
+ "_SECRET_KEY"
1871
+ ];
1872
+ function isCredentialEnvKey(key) {
1873
+ const cleaned = key.trim();
1874
+ if (!cleaned) return false;
1875
+ const upper = cleaned.toUpperCase();
1876
+ if (CREDENTIAL_KEYS.has(upper)) return true;
1877
+ if (upper.startsWith("MANAGED_DB_")) return true;
1878
+ return CREDENTIAL_SUFFIXES.some((suffix) => upper.endsWith(suffix));
1879
+ }
1880
+ function credentialEnvKeys(env) {
1881
+ return Object.keys(env).filter((key) => isCredentialEnvKey(key)).sort();
1882
+ }
1883
+
1633
1884
  // src/commands/deploy.ts
1634
1885
  var FALLBACK_RUNTIMES = /* @__PURE__ */ new Set([
1635
1886
  "python",
@@ -1665,6 +1916,22 @@ function parseEnvPairs(pairs) {
1665
1916
  }
1666
1917
  var log = (msg) => process.stderr.write(`${msg}
1667
1918
  `);
1919
+ function names(list, max = 10) {
1920
+ return list.length > max ? `${list.slice(0, max).join(", ")} \u2026 and ${list.length - max} more` : list.join(", ");
1921
+ }
1922
+ function reportFiltering(filtered) {
1923
+ if (filtered.excluded.length) {
1924
+ log(
1925
+ `Excluded ${filtered.excluded.length} credential file(s): ${names(filtered.excluded)}`
1926
+ );
1927
+ log(" (use `gf secrets` or `--env KEY=VALUE` to pass values to the app)");
1928
+ }
1929
+ if (filtered.dotfilesIncluded.length) {
1930
+ log(
1931
+ `Including ${filtered.dotfilesIncluded.length} dot-file(s): ${names(filtered.dotfilesIncluded)}`
1932
+ );
1933
+ }
1934
+ }
1668
1935
  async function failDeploy(opts) {
1669
1936
  throw new Error(await diagnoseDeployFailure(opts));
1670
1937
  }
@@ -1694,7 +1961,7 @@ async function deployCommand(opts) {
1694
1961
  })
1695
1962
  );
1696
1963
  log(
1697
- opts.git ? `Cloning ${opts.git}${opts.ref ? ` (${opts.ref})` : ""}...` : `Packaging ${path4.resolve(opts.path ?? process.cwd())}...`
1964
+ opts.git ? `Cloning ${opts.git}${opts.ref ? ` (${opts.ref})` : ""}...` : `Packaging ${path5.resolve(opts.path ?? process.cwd())}...`
1698
1965
  );
1699
1966
  const pkg = await packageSource({
1700
1967
  path: opts.path,
@@ -1706,6 +1973,7 @@ async function deployCommand(opts) {
1706
1973
  const manifest = await readProjectManifest(pkg.sourceDir) ?? {};
1707
1974
  const sizeKb = (pkg.zip.byteLength / 1024).toFixed(1);
1708
1975
  log(`Packaged ${pkg.fileCount} file(s), ${sizeKb} KB`);
1976
+ reportFiltering(pkg.filtered);
1709
1977
  const detected = await detectRuntime(pkg.sourceDir);
1710
1978
  const runtime = opts.runtime ?? manifest.runtime ?? detected ?? "python";
1711
1979
  let allowedRuntimes = FALLBACK_RUNTIMES;
@@ -1729,7 +1997,7 @@ async function deployCommand(opts) {
1729
1997
  }
1730
1998
  if (workloadType === "service") {
1731
1999
  try {
1732
- await fs4.access(path4.join(pkg.sourceDir, "Dockerfile"));
2000
+ await fs4.access(path5.join(pkg.sourceDir, "Dockerfile"));
1733
2001
  } catch {
1734
2002
  throw new Error(
1735
2003
  'workload type "service" (also called Deployment) requires a Dockerfile at the project root.'
@@ -1746,7 +2014,7 @@ async function deployCommand(opts) {
1746
2014
  });
1747
2015
  let coderunnerId = opts.coderunner;
1748
2016
  if (!coderunnerId) {
1749
- const name = opts.name ?? manifest.name ?? (opts.git ? nameFromGitUrl(opts.git) : path4.basename(path4.resolve(opts.path ?? process.cwd())));
2017
+ const name = opts.name ?? manifest.name ?? (opts.git ? nameFromGitUrl(opts.git) : path5.basename(path5.resolve(opts.path ?? process.cwd())));
1750
2018
  const slug = manifest.slug ?? slugify(name);
1751
2019
  const existing = await listCoderunners(cfg.apiUrl, ws);
1752
2020
  const match = existing.find((c) => c.slug === slug || c.name === name);
@@ -1760,7 +2028,8 @@ async function deployCommand(opts) {
1760
2028
  slug,
1761
2029
  runtime,
1762
2030
  workload_type: workloadType,
1763
- ...opts.appId ? { app_id: opts.appId } : {}
2031
+ ...opts.appId ? { app_id: opts.appId } : {},
2032
+ ...schedule ? { schedule } : {}
1764
2033
  });
1765
2034
  coderunnerId = created.id;
1766
2035
  log(`Created coderunner ${created.id}`);
@@ -1840,13 +2109,21 @@ async function deployCommand(opts) {
1840
2109
  ...manifest.env ?? {},
1841
2110
  ...parseEnvPairs(opts.env)
1842
2111
  };
2112
+ const plaintextSecrets = credentialEnvKeys(envVariable);
2113
+ if (plaintextSecrets.length) {
2114
+ throw new Error(
2115
+ `Do not pass secrets as --env: ${plaintextSecrets.join(", ")}. Store them with \`gf secrets set KEY\` and deploy with \`--secret KEY\`.`
2116
+ );
2117
+ }
2118
+ const injectSecretKeys = (opts.secret ?? []).map((key) => key.trim()).filter(Boolean);
1843
2119
  log("Deploying...");
1844
2120
  const result = await deployCoderunner(cfg.apiUrl, ws, coderunnerId, {
1845
2121
  version_id: uploaded.version_id,
1846
2122
  cpu: opts.cpu ?? manifest.cpu,
1847
2123
  memory: opts.memory ?? manifest.memory,
1848
2124
  envVariable: Object.keys(envVariable).length ? envVariable : void 0,
1849
- schedule
2125
+ schedule,
2126
+ inject_secret_keys: injectSecretKeys.length ? injectSecretKeys : void 0
1850
2127
  });
1851
2128
  log("Waiting for deployment...");
1852
2129
  const deployDeadline = Date.now() + DEPLOY_POLL_TIMEOUT_MS;
@@ -1938,9 +2215,12 @@ async function runCommand(opts) {
1938
2215
  cfg.workspaceId,
1939
2216
  opts.coderunner
1940
2217
  );
1941
- const result = await runCoderunner(cfg.apiUrl, cfg.workspaceId, coderunnerId, {
2218
+ const result = await runCoderunner(
2219
+ cfg.apiUrl,
2220
+ cfg.workspaceId,
2221
+ coderunnerId,
1942
2222
  payload
1943
- });
2223
+ );
1944
2224
  if (opts.json) {
1945
2225
  process.stdout.write(`${JSON.stringify(result, null, 2)}
1946
2226
  `);
@@ -1968,7 +2248,29 @@ async function statusCommand(opts) {
1968
2248
  getDeployment(cfg.apiUrl, cfg.workspaceId, coderunnerId)
1969
2249
  ]);
1970
2250
  const out = { coderunner_id: coderunnerId, status, deployment };
1971
- process.stdout.write(`${JSON.stringify(out, null, 2)}
2251
+ if (opts.json) {
2252
+ process.stdout.write(`${JSON.stringify(out, null, 2)}
2253
+ `);
2254
+ return;
2255
+ }
2256
+ const state = status && typeof status === "object" ? status.state : void 0;
2257
+ const type = String(state?.workload_type ?? "function");
2258
+ const schedule = state?.schedule ? String(state.schedule) : "\u2014";
2259
+ const lastJob = state?.last_job_name ? `${state.last_job_name} (${state.last_job_status ?? "?"})` : "\u2014";
2260
+ process.stdout.write(`coderunner: ${coderunnerId}
2261
+ `);
2262
+ process.stdout.write(`type: ${type}
2263
+ `);
2264
+ process.stdout.write(`schedule: ${schedule}
2265
+ `);
2266
+ process.stdout.write(`last_job: ${lastJob}
2267
+ `);
2268
+ if (deployment?.status != null) {
2269
+ process.stdout.write(`deployment: ${deployment.id ?? "\u2014"} [${deployment.status}]
2270
+ `);
2271
+ }
2272
+ process.stdout.write(`
2273
+ ${JSON.stringify(out, null, 2)}
1972
2274
  `);
1973
2275
  }
1974
2276
 
@@ -2140,11 +2442,205 @@ async function secretsRmCommand(key, opts) {
2140
2442
  `);
2141
2443
  }
2142
2444
 
2445
+ // src/commands/flags.ts
2446
+ var VALUE_TYPES = [
2447
+ "boolean",
2448
+ "string",
2449
+ "number",
2450
+ "json"
2451
+ ];
2452
+ var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2453
+ function matchEnvironment(environments2, idOrName) {
2454
+ const wanted = idOrName.trim();
2455
+ const byId = environments2.find((env) => env.id === wanted);
2456
+ if (byId) return byId.id;
2457
+ const byName = environments2.filter(
2458
+ (env) => env.name.toLowerCase() === wanted.toLowerCase()
2459
+ );
2460
+ if (byName.length === 1) return byName[0].id;
2461
+ const names2 = environments2.map((env) => env.name).join(", ") || "none";
2462
+ throw new Error(
2463
+ `Environment "${wanted}" not found in this workspace (have: ${names2}). Run \`gf environments ls\`.`
2464
+ );
2465
+ }
2466
+ async function resolveEnvironment(apiUrl, workspaceId, idOrName) {
2467
+ if (!idOrName) return void 0;
2468
+ if (UUID_RE2.test(idOrName.trim())) return idOrName.trim();
2469
+ const environments2 = await listWorkspaceEnvironments(apiUrl, workspaceId);
2470
+ return matchEnvironment(environments2, idOrName);
2471
+ }
2472
+ function parseFlagValue(raw, type) {
2473
+ if (type !== void 0 && !VALUE_TYPES.includes(type)) {
2474
+ throw new Error(`--type must be one of: ${VALUE_TYPES.join(", ")}`);
2475
+ }
2476
+ const lower = raw.trim().toLowerCase();
2477
+ const valueType = type ?? (lower === "true" || lower === "false" ? "boolean" : raw.trim() !== "" && Number.isFinite(Number(raw)) ? "number" : "string");
2478
+ switch (valueType) {
2479
+ case "boolean":
2480
+ if (lower !== "true" && lower !== "false") {
2481
+ throw new Error("boolean flags take true or false");
2482
+ }
2483
+ return { value: lower === "true", value_type: "boolean" };
2484
+ case "number": {
2485
+ const n = Number(raw);
2486
+ if (raw.trim() === "" || !Number.isFinite(n)) {
2487
+ throw new Error(`"${raw}" is not a number`);
2488
+ }
2489
+ return { value: n, value_type: "number" };
2490
+ }
2491
+ case "json":
2492
+ try {
2493
+ return { value: JSON.parse(raw), value_type: "json" };
2494
+ } catch {
2495
+ throw new Error("--type json needs a valid JSON value");
2496
+ }
2497
+ default:
2498
+ return { value: raw, value_type: "string" };
2499
+ }
2500
+ }
2501
+ function formatValue(value) {
2502
+ return typeof value === "string" ? value : JSON.stringify(value);
2503
+ }
2504
+ async function flagsListCommand(opts) {
2505
+ const cfg = await requireWorkspace(opts.workspace);
2506
+ const env = await resolveEnvironment(
2507
+ cfg.apiUrl,
2508
+ cfg.workspaceId,
2509
+ opts.environment
2510
+ );
2511
+ const data = await listFlags(cfg.apiUrl, cfg.workspaceId, env);
2512
+ if (opts.json) {
2513
+ process.stdout.write(`${JSON.stringify(data, null, 2)}
2514
+ `);
2515
+ return;
2516
+ }
2517
+ process.stderr.write(`environment ${data.environment_id}
2518
+ `);
2519
+ if (data.flags.length === 0) {
2520
+ process.stdout.write("No flags in this environment.\n");
2521
+ return;
2522
+ }
2523
+ for (const flag of data.flags) {
2524
+ process.stdout.write(
2525
+ `${flag.key} ${flag.value_type} ${formatValue(flag.value)}
2526
+ `
2527
+ );
2528
+ }
2529
+ }
2530
+ async function flagsGetCommand(key, opts) {
2531
+ const cfg = await requireWorkspace(opts.workspace);
2532
+ const env = await resolveEnvironment(
2533
+ cfg.apiUrl,
2534
+ cfg.workspaceId,
2535
+ opts.environment
2536
+ );
2537
+ const flag = await getFlag(cfg.apiUrl, cfg.workspaceId, key, env);
2538
+ if (opts.json) {
2539
+ process.stdout.write(`${JSON.stringify(flag, null, 2)}
2540
+ `);
2541
+ return;
2542
+ }
2543
+ process.stdout.write(`${formatValue(flag.value)}
2544
+ `);
2545
+ }
2546
+ async function flagsSetCommand(key, value, opts) {
2547
+ const cfg = await requireWorkspace(opts.workspace);
2548
+ const env = await resolveEnvironment(
2549
+ cfg.apiUrl,
2550
+ cfg.workspaceId,
2551
+ opts.environment
2552
+ );
2553
+ const parsed = parseFlagValue(value, opts.type);
2554
+ const saved = await putFlag(
2555
+ cfg.apiUrl,
2556
+ cfg.workspaceId,
2557
+ key,
2558
+ { ...parsed, description: opts.description },
2559
+ env
2560
+ );
2561
+ if (opts.json) {
2562
+ process.stdout.write(`${JSON.stringify(saved, null, 2)}
2563
+ `);
2564
+ return;
2565
+ }
2566
+ process.stdout.write(
2567
+ `Set ${saved.key} = ${formatValue(saved.value)} (${saved.value_type}) in environment ${saved.environment_id}
2568
+ `
2569
+ );
2570
+ }
2571
+ async function flagsRmCommand(key, opts) {
2572
+ const cfg = await requireWorkspace(opts.workspace);
2573
+ const env = await resolveEnvironment(
2574
+ cfg.apiUrl,
2575
+ cfg.workspaceId,
2576
+ opts.environment
2577
+ );
2578
+ await deleteFlag(cfg.apiUrl, cfg.workspaceId, key, env);
2579
+ process.stdout.write(`Deleted flag ${key}
2580
+ `);
2581
+ }
2582
+ async function flagsCompareCommand(source, target, opts) {
2583
+ const cfg = await requireWorkspace(opts.workspace);
2584
+ const [src, tgt] = await Promise.all([
2585
+ resolveEnvironment(cfg.apiUrl, cfg.workspaceId, source),
2586
+ resolveEnvironment(cfg.apiUrl, cfg.workspaceId, target)
2587
+ ]);
2588
+ const data = await compareFlags(cfg.apiUrl, cfg.workspaceId, src, tgt);
2589
+ if (opts.json) {
2590
+ process.stdout.write(`${JSON.stringify(data, null, 2)}
2591
+ `);
2592
+ return;
2593
+ }
2594
+ if (data.keys.length === 0) {
2595
+ process.stdout.write("No flags in either environment.\n");
2596
+ return;
2597
+ }
2598
+ for (const row of data.keys) {
2599
+ process.stdout.write(
2600
+ `${row.key} ${row.status} ${formatValue(row.source_value ?? null)} \u2192 ${formatValue(row.target_value ?? null)}
2601
+ `
2602
+ );
2603
+ }
2604
+ }
2605
+ async function flagsPromoteCommand(source, target, opts) {
2606
+ const cfg = await requireWorkspace(opts.workspace);
2607
+ const [src, tgt] = await Promise.all([
2608
+ resolveEnvironment(cfg.apiUrl, cfg.workspaceId, source),
2609
+ resolveEnvironment(cfg.apiUrl, cfg.workspaceId, target)
2610
+ ]);
2611
+ const keys = opts.keys?.split(",").map((k) => k.trim()).filter(Boolean);
2612
+ const result = await promoteFlags(cfg.apiUrl, cfg.workspaceId, {
2613
+ source_environment_id: src,
2614
+ target_environment_id: tgt,
2615
+ keys: keys && keys.length > 0 ? keys : void 0,
2616
+ mode: opts.overwrite ? "overwrite" : "skip_existing",
2617
+ dry_run: Boolean(opts.dryRun)
2618
+ });
2619
+ if (opts.json) {
2620
+ process.stdout.write(`${JSON.stringify(result, null, 2)}
2621
+ `);
2622
+ return;
2623
+ }
2624
+ const prefix = result.dry_run ? "[dry run] " : "";
2625
+ process.stdout.write(
2626
+ `${prefix}copied: ${result.copied.join(", ") || "none"}
2627
+ ${prefix}skipped (already set; use --overwrite): ${result.skipped.join(", ") || "none"}
2628
+ `
2629
+ );
2630
+ for (const failure of result.failed) {
2631
+ process.stdout.write(`${prefix}failed: ${failure.key} \u2014 ${failure.error}
2632
+ `);
2633
+ }
2634
+ if (result.failed.length > 0) {
2635
+ process.exitCode = 1;
2636
+ }
2637
+ }
2638
+
2143
2639
  // src/commands/files.ts
2144
2640
  import { promises as fs5 } from "fs";
2145
- import path5 from "path";
2641
+ import path6 from "path";
2146
2642
  function guessContentType(filename) {
2147
- const ext = path5.extname(filename).toLowerCase();
2643
+ const ext = path6.extname(filename).toLowerCase();
2148
2644
  const types = {
2149
2645
  ".json": "application/json",
2150
2646
  ".txt": "text/plain",
@@ -2179,9 +2675,9 @@ async function filesListCommand(opts) {
2179
2675
  }
2180
2676
  async function filesUploadCommand(filePath, opts) {
2181
2677
  const cfg = await requireWorkspace(opts.workspace);
2182
- const resolved = path5.resolve(filePath);
2678
+ const resolved = path6.resolve(filePath);
2183
2679
  const buf = await fs5.readFile(resolved);
2184
- const name = opts.name ?? path5.basename(resolved);
2680
+ const name = opts.name ?? path6.basename(resolved);
2185
2681
  const contentType = guessContentType(name);
2186
2682
  const init = await initFileUpload(cfg.apiUrl, cfg.workspaceId, {
2187
2683
  name,
@@ -2218,7 +2714,7 @@ async function filesDownloadCommand(fileId, opts) {
2218
2714
  throw new Error(`Download failed: ${res.status} ${res.statusText}`);
2219
2715
  }
2220
2716
  const buf = Buffer.from(await res.arrayBuffer());
2221
- const dest = path5.resolve(opts.out ?? fileId);
2717
+ const dest = path6.resolve(opts.out ?? fileId);
2222
2718
  await fs5.writeFile(dest, buf);
2223
2719
  process.stdout.write(`Wrote ${dest}
2224
2720
  `);
@@ -2232,7 +2728,7 @@ async function filesRmCommand(fileId, opts) {
2232
2728
 
2233
2729
  // src/commands/apps.ts
2234
2730
  import { promises as fs6 } from "fs";
2235
- import path6 from "path";
2731
+ import path7 from "path";
2236
2732
 
2237
2733
  // src/release-zip.ts
2238
2734
  import AdmZip2 from "adm-zip";
@@ -2276,8 +2772,8 @@ function flattenFederatedBundlePlan(relpaths) {
2276
2772
  const promoteDirs = /* @__PURE__ */ new Set();
2277
2773
  const entryDir = parentDir(entry);
2278
2774
  if (entryDir) promoteDirs.add(entryDir);
2279
- for (const path10 of pathset) {
2280
- const parent = parentDir(path10);
2775
+ for (const path11 of pathset) {
2776
+ const parent = parentDir(path11);
2281
2777
  if (parent === "assets" || parent.endsWith("/assets") && parent.split("/").length - 1 < 2) {
2282
2778
  promoteDirs.add(parent);
2283
2779
  }
@@ -2324,8 +2820,8 @@ function flattenFederatedReleaseZip(bytes) {
2324
2820
  return zip.toBuffer();
2325
2821
  }
2326
2822
  function rewriteRemoteEntryInZip(zip) {
2327
- const names = zip.getEntries().filter((entry) => !entry.isDirectory).map((entry) => normalizeZipEntryName(entry.entryName));
2328
- const entryRel = chooseRemoteEntry(names);
2823
+ const names2 = zip.getEntries().filter((entry) => !entry.isDirectory).map((entry) => normalizeZipEntryName(entry.entryName));
2824
+ const entryRel = chooseRemoteEntry(names2);
2329
2825
  if (!entryRel) return;
2330
2826
  const zipEntry = zip.getEntries().find((item) => {
2331
2827
  if (item.isDirectory) return false;
@@ -2349,18 +2845,18 @@ function initThenPublishHint(slug) {
2349
2845
  return `gf apps init --slug ${s} && cd ${s} && npm install && npm run release && gf apps publish --path release.zip`;
2350
2846
  }
2351
2847
  function extractReferencedSiblings(remoteEntrySource) {
2352
- const names = /* @__PURE__ */ new Set();
2848
+ const names2 = /* @__PURE__ */ new Set();
2353
2849
  for (const match of remoteEntrySource.matchAll(
2354
2850
  /["']\.\/(?:assets\/)?([^"'/]+\.(?:js|mjs|cjs|css))["']/g
2355
2851
  )) {
2356
- if (match[1] !== REMOTE_ENTRY) names.add(match[1]);
2852
+ if (match[1] !== REMOTE_ENTRY) names2.add(match[1]);
2357
2853
  }
2358
2854
  for (const match of remoteEntrySource.matchAll(
2359
2855
  /["']([A-Za-z0-9._-]+\.css)["']/g
2360
2856
  )) {
2361
- names.add(match[1]);
2857
+ names2.add(match[1]);
2362
2858
  }
2363
- return [...names];
2859
+ return [...names2];
2364
2860
  }
2365
2861
  function extractFederationScope(remoteEntrySource) {
2366
2862
  return remoteEntrySource.match(/css__([A-Za-z0-9_-]+)__/)?.[1];
@@ -2379,9 +2875,9 @@ function readZipFile(bytes, rel) {
2379
2875
  }
2380
2876
  function assertFederatedReleaseZip(bytes, slug) {
2381
2877
  const hint = initThenPublishHint(slug);
2382
- const names = zipRootNames(bytes);
2383
- const present = new Set(names.map((name) => basename(name)));
2384
- const entryRel = chooseRemoteEntry(names);
2878
+ const names2 = zipRootNames(bytes);
2879
+ const present = new Set(names2.map((name) => basename(name)));
2880
+ const entryRel = chooseRemoteEntry(names2);
2385
2881
  if (!entryRel) {
2386
2882
  throw new Error(`Zip has no remoteEntry.js. ${hint}`);
2387
2883
  }
@@ -2391,7 +2887,7 @@ function assertFederatedReleaseZip(bytes, slug) {
2391
2887
  }
2392
2888
  const siblings = extractReferencedSiblings(source);
2393
2889
  const missing = siblings.filter((name) => !present.has(name));
2394
- const jsOrCssAtRoot = names.filter((name) => {
2890
+ const jsOrCssAtRoot = names2.filter((name) => {
2395
2891
  if (name.includes("/")) return false;
2396
2892
  const lower = name.toLowerCase();
2397
2893
  return lower.endsWith(".js") || lower.endsWith(".mjs") || lower.endsWith(".css");
@@ -2433,11 +2929,11 @@ async function readJsonObject(filePath) {
2433
2929
  }
2434
2930
  async function maybeReadManifest(filePath) {
2435
2931
  if (!filePath) return void 0;
2436
- return readJsonObject(path6.resolve(filePath));
2932
+ return readJsonObject(path7.resolve(filePath));
2437
2933
  }
2438
2934
  async function findSiblingManifest(bundlePath) {
2439
- const dir = await isDirectory(bundlePath) ? bundlePath : path6.dirname(bundlePath);
2440
- const candidate = path6.join(dir, "groundfloor.manifest.json");
2935
+ const dir = await isDirectory(bundlePath) ? bundlePath : path7.dirname(bundlePath);
2936
+ const candidate = path7.join(dir, "groundfloor.manifest.json");
2441
2937
  try {
2442
2938
  await fs6.access(candidate);
2443
2939
  } catch {
@@ -2595,8 +3091,8 @@ Do not scaffold src/App.tsx from scratch \u2014 BrowserRouter in App.tsx crashes
2595
3091
  }
2596
3092
  async function appsPublishCommand(appRef, opts) {
2597
3093
  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;
3094
+ const inputPath = path7.resolve(opts.path ?? "release.zip");
3095
+ const bundlePath = await isDirectory(inputPath) ? path7.join(inputPath, "release.zip") : inputPath;
2600
3096
  let bytes;
2601
3097
  try {
2602
3098
  bytes = await fs6.readFile(bundlePath);
@@ -2626,11 +3122,11 @@ async function appsPublishCommand(appRef, opts) {
2626
3122
  `Releases are only for shell_federated apps (this app is ${app.app_kind}). Use \`gf deploy\` for coderunner workloads.`
2627
3123
  );
2628
3124
  }
2629
- const ext = path6.extname(bundlePath).toLowerCase();
3125
+ const ext = path7.extname(bundlePath).toLowerCase();
2630
3126
  const isZip = ext === ".zip";
2631
3127
  if (!isZip) {
2632
3128
  throw new Error(
2633
- `Publish release.zip from \`npm run release\`, not a lone ${path6.basename(bundlePath)}. ` + initThenPublishHint(app.slug)
3129
+ `Publish release.zip from \`npm run release\`, not a lone ${path7.basename(bundlePath)}. ` + initThenPublishHint(app.slug)
2634
3130
  );
2635
3131
  }
2636
3132
  bytes = prepareReleaseZip(bytes, app.slug);
@@ -2643,7 +3139,10 @@ async function appsPublishCommand(appRef, opts) {
2643
3139
  }
2644
3140
  const init = await initAppReleaseUpload(cfg.apiUrl, cfg.workspaceId, app.id, {
2645
3141
  bundle,
2646
- label: opts.label
3142
+ label: opts.label,
3143
+ // Required since wave 5: the server pins this into the presigned PUT and
3144
+ // checks it against the quota before minting a URL. Omitting it is a 422.
3145
+ size_bytes: bytes.byteLength
2647
3146
  });
2648
3147
  await uploadAppReleaseContent(
2649
3148
  cfg.apiUrl,
@@ -2672,11 +3171,20 @@ async function appsPublishCommand(appRef, opts) {
2672
3171
  // src/commands/apps-init.ts
2673
3172
  import { promises as fs7 } from "fs";
2674
3173
  import os3 from "os";
2675
- import path7 from "path";
3174
+ import path8 from "path";
2676
3175
  import AdmZip4 from "adm-zip";
2677
3176
 
2678
3177
  // src/version.ts
2679
- var GF_VERSION = "0.1.8";
3178
+ import { readFileSync } from "fs";
3179
+ import { dirname, join } from "path";
3180
+ import { fileURLToPath } from "url";
3181
+ var moduleDir = dirname(fileURLToPath(import.meta.url));
3182
+ function readPackageVersion() {
3183
+ const pkgPath = join(moduleDir, "..", "package.json");
3184
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
3185
+ return pkg.version;
3186
+ }
3187
+ var GF_VERSION = readPackageVersion();
2680
3188
  var GF_NPM_PACKAGE = "@groundfloorcloud/cli";
2681
3189
 
2682
3190
  // src/commands/apps-init.ts
@@ -2707,17 +3215,17 @@ async function isEmptyDir(dir) {
2707
3215
  }
2708
3216
  async function findKitRoot(extracted) {
2709
3217
  const candidates = [
2710
- path7.join(extracted, "shell-starter-kit"),
3218
+ path8.join(extracted, "shell-starter-kit"),
2711
3219
  extracted
2712
3220
  ];
2713
3221
  const entries = await fs7.readdir(extracted, { withFileTypes: true });
2714
3222
  for (const entry of entries) {
2715
3223
  if (entry.isDirectory()) {
2716
- candidates.push(path7.join(extracted, entry.name));
3224
+ candidates.push(path8.join(extracted, entry.name));
2717
3225
  }
2718
3226
  }
2719
3227
  for (const candidate of candidates) {
2720
- if (await pathExists(path7.join(candidate, "groundfloor.manifest.json"))) {
3228
+ if (await pathExists(path8.join(candidate, "groundfloor.manifest.json"))) {
2721
3229
  return candidate;
2722
3230
  }
2723
3231
  }
@@ -2799,7 +3307,7 @@ async function appsInitCommand(opts) {
2799
3307
  `);
2800
3308
  }
2801
3309
  const name = opts.name?.trim() || titleFromSlug(slug);
2802
- const dest = path7.resolve(opts.dir?.trim() || path7.join(process.cwd(), slug));
3310
+ const dest = path8.resolve(opts.dir?.trim() || path8.join(process.cwd(), slug));
2803
3311
  const cfg = await resolveConfig();
2804
3312
  const cell = cfg.environment ?? "production";
2805
3313
  const kitUrl = opts.kitUrl?.trim() || starterKitZipUrl(cell);
@@ -2814,7 +3322,7 @@ async function appsInitCommand(opts) {
2814
3322
  const bytes = await downloadKit(kitUrl);
2815
3323
  const zip = new AdmZip4(bytes);
2816
3324
  assertSafeZipEntries(zip);
2817
- const tmp = await fs7.mkdtemp(path7.join(os3.tmpdir(), "gf-starter-kit-"));
3325
+ const tmp = await fs7.mkdtemp(path8.join(os3.tmpdir(), "gf-starter-kit-"));
2818
3326
  try {
2819
3327
  zip.extractAllTo(tmp, true);
2820
3328
  const kitRoot = await findKitRoot(tmp);
@@ -2823,8 +3331,8 @@ async function appsInitCommand(opts) {
2823
3331
  } finally {
2824
3332
  await fs7.rm(tmp, { recursive: true, force: true });
2825
3333
  }
2826
- const identityPath = path7.join(dest, "src", "appIdentity.ts");
2827
- const manifestPath = path7.join(dest, "groundfloor.manifest.json");
3334
+ const identityPath = path8.join(dest, "src", "appIdentity.ts");
3335
+ const manifestPath = path8.join(dest, "groundfloor.manifest.json");
2828
3336
  const identity = await fs7.readFile(identityPath, "utf8");
2829
3337
  await fs7.writeFile(identityPath, stampAppIdentity(identity, slug), "utf8");
2830
3338
  const manifest = await fs7.readFile(manifestPath, "utf8");
@@ -2901,7 +3409,7 @@ async function dataplaneProvisionCommand(opts) {
2901
3409
 
2902
3410
  // src/update-check.ts
2903
3411
  import { promises as fs8 } from "fs";
2904
- import path8 from "path";
3412
+ import path9 from "path";
2905
3413
  var UPDATE_TTL_MS = 24 * 60 * 60 * 1e3;
2906
3414
  function isNewer(latest, current) {
2907
3415
  const a = latest.split(".").map((n) => Number.parseInt(n, 10) || 0);
@@ -2954,7 +3462,7 @@ async function readUpdateCache(file) {
2954
3462
  }
2955
3463
  }
2956
3464
  async function writeUpdateCache(file, cache) {
2957
- await fs8.mkdir(path8.dirname(file), { recursive: true, mode: 448 });
3465
+ await fs8.mkdir(path9.dirname(file), { recursive: true, mode: 448 });
2958
3466
  await fs8.writeFile(file, `${JSON.stringify(cache)}
2959
3467
  `, { mode: 384 });
2960
3468
  }
@@ -2992,6 +3500,90 @@ function commanderExitCode(err) {
2992
3500
  return 0;
2993
3501
  }
2994
3502
 
3503
+ // src/commands/api.ts
3504
+ var METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE"]);
3505
+ async function apiCommand(opts) {
3506
+ const cfg = await resolveConfig({ workspaceId: opts.workspace });
3507
+ const method = (opts.method ?? "GET").toUpperCase();
3508
+ if (!METHODS.has(method)) {
3509
+ throw new Error(`Unsupported method ${method}`);
3510
+ }
3511
+ let path11 = opts.path.trim();
3512
+ if (!path11.startsWith("/")) path11 = `/${path11}`;
3513
+ path11 = path11.replaceAll("{workspace_id}", cfg.workspaceId ?? "");
3514
+ path11 = path11.replaceAll("{workspaceId}", cfg.workspaceId ?? "");
3515
+ let body;
3516
+ if (opts.body !== void 0) {
3517
+ body = JSON.parse(opts.body);
3518
+ }
3519
+ const data = await controlPlaneRequest(cfg.apiUrl, method, path11, body);
3520
+ if (data === void 0) {
3521
+ process.stdout.write("\n");
3522
+ return;
3523
+ }
3524
+ process.stdout.write(`${JSON.stringify(data, null, 2)}
3525
+ `);
3526
+ }
3527
+
3528
+ // src/commands/identities.ts
3529
+ async function identitiesListCommand(opts) {
3530
+ const cfg = await resolveConfig({ workspaceId: opts.workspace });
3531
+ if (!cfg.workspaceId) {
3532
+ throw new Error(
3533
+ "No workspace selected. Run `gf workspaces use <id>` or pass --workspace <id>."
3534
+ );
3535
+ }
3536
+ const identities2 = await listWorkloadIdentities(cfg.apiUrl, cfg.workspaceId);
3537
+ if (opts.json) {
3538
+ process.stdout.write(`${JSON.stringify(identities2, null, 2)}
3539
+ `);
3540
+ return;
3541
+ }
3542
+ if (identities2.length === 0) {
3543
+ process.stdout.write("No workload identities in this workspace.\n");
3544
+ return;
3545
+ }
3546
+ for (const identity of identities2) {
3547
+ const refresh = identity.has_refresh_secret ? "refresh" : "no-refresh";
3548
+ process.stdout.write(
3549
+ `${identity.id} ${identity.slug ?? identity.name} [${refresh}]
3550
+ `
3551
+ );
3552
+ }
3553
+ }
3554
+ async function identitiesMintCommand(opts) {
3555
+ const cfg = await resolveConfig({ workspaceId: opts.workspace });
3556
+ if (!cfg.workspaceId) {
3557
+ throw new Error(
3558
+ "No workspace selected. Run `gf workspaces use <id>` or pass --workspace <id>."
3559
+ );
3560
+ }
3561
+ const data = await mintWorkloadIdentityVaultToken(
3562
+ cfg.apiUrl,
3563
+ cfg.workspaceId,
3564
+ opts.identity,
3565
+ { rotate_refresh: Boolean(opts.rotateRefresh) }
3566
+ );
3567
+ if (opts.json) {
3568
+ process.stdout.write(`${JSON.stringify(data, null, 2)}
3569
+ `);
3570
+ return;
3571
+ }
3572
+ if (data.refresh_secret) {
3573
+ process.stderr.write(
3574
+ "Save refresh_secret now; it is not shown again unless you --rotate-refresh.\n"
3575
+ );
3576
+ process.stderr.write(`refresh_secret=${data.refresh_secret}
3577
+ `);
3578
+ }
3579
+ process.stderr.write(
3580
+ `expires_in=${data.expires_in}s identity=${data.workload_identity_id}
3581
+ `
3582
+ );
3583
+ process.stdout.write(`${data.token}
3584
+ `);
3585
+ }
3586
+
2995
3587
  // src/index.ts
2996
3588
  function collect(value, previous) {
2997
3589
  return previous.concat([value]);
@@ -3071,7 +3663,7 @@ coderunner.command("logs").description("Show Process Log lines for a coderunner
3071
3663
  })
3072
3664
  )
3073
3665
  );
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(
3666
+ 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
3667
  (opts) => run(
3076
3668
  () => runCommand({
3077
3669
  workspace: opts.workspace,
@@ -3108,6 +3700,24 @@ secrets.command("set <key> [value]").description("Create or update a secret (pas
3108
3700
  (key, value, opts) => run(() => secretsSetCommand(key, value, opts))
3109
3701
  );
3110
3702
  secrets.command("rm <key>").alias("delete").description("Delete a secret").option("-w, --workspace <id>", "Workspace id override").option("-e, --environment <id>", "Environment id").action((key, opts) => run(() => secretsRmCommand(key, opts)));
3703
+ var flags = program.command("flags").description(
3704
+ "Manage feature flags per environment (apps read them via GET \u2026/flags/runtime)"
3705
+ );
3706
+ var ENV_HELP = "Environment id or name (dev, staging, prod). Default: workspace default (dev)";
3707
+ flags.command("ls").alias("list").description("List flags and values in an environment").option("-w, --workspace <id>", "Workspace id override").option("-e, --environment <idOrName>", ENV_HELP).option("--json", "Output raw JSON").action((opts) => run(() => flagsListCommand(opts)));
3708
+ flags.command("get <key>").description("Print a flag value").option("-w, --workspace <id>", "Workspace id override").option("-e, --environment <idOrName>", ENV_HELP).option("--json", "Output raw JSON").action((key, opts) => run(() => flagsGetCommand(key, opts)));
3709
+ flags.command("set <key> <value>").description(
3710
+ "Create or update a flag (type inferred: true/false \u2192 boolean, numeric \u2192 number, else string)"
3711
+ ).option("-w, --workspace <id>", "Workspace id override").option("-e, --environment <idOrName>", ENV_HELP).option("-t, --type <type>", "boolean | string | number | json").option("--description <text>", "Optional description (max 255 chars)").option("--json", "Output raw JSON").action(
3712
+ (key, value, opts) => run(() => flagsSetCommand(key, value, opts))
3713
+ );
3714
+ flags.command("rm <key>").alias("delete").description("Delete a flag from one environment").option("-w, --workspace <id>", "Workspace id override").option("-e, --environment <idOrName>", ENV_HELP).action((key, opts) => run(() => flagsRmCommand(key, opts)));
3715
+ flags.command("compare <source> <target>").alias("diff").description("Diff flags between two environments (ids or names)").option("-w, --workspace <id>", "Workspace id override").option("--json", "Output raw JSON").action(
3716
+ (source, target, opts) => run(() => flagsCompareCommand(source, target, opts))
3717
+ );
3718
+ flags.command("promote <source> <target>").description("Copy flags from one environment to another").option("-w, --workspace <id>", "Workspace id override").option("-k, --keys <keys>", "Comma-separated keys (default: all)").option("--overwrite", "Replace values already set in the target").option("--dry-run", "Show what would change without writing").option("--json", "Output raw JSON").action(
3719
+ (source, target, opts) => run(() => flagsPromoteCommand(source, target, opts))
3720
+ );
3111
3721
  var files = program.command("files").description("Manage workspace files");
3112
3722
  files.command("ls").alias("list").description("List files").option("-w, --workspace <id>", "Workspace id override").option("--json", "Output raw JSON").action((opts) => run(() => filesListCommand(opts)));
3113
3723
  files.command("upload <path>").description("Upload a local file").option("-w, --workspace <id>", "Workspace id override").option("-n, --name <name>", "Display name (default: basename)").action(
@@ -3182,7 +3792,34 @@ program.command("deploy").description(
3182
3792
  ).option(
3183
3793
  "--app-id <id>",
3184
3794
  "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)));
3795
+ ).option("--cpu <cpu>", "CPU request, e.g. 250m").option("--memory <memory>", "Memory request, e.g. 256Mi").option("-e, --env <KEY=VALUE>", "Environment variable (repeatable)", collect, []).option(
3796
+ "--secret <KEY>",
3797
+ "Inject this workspace secret by reference (repeatable). Use for *_SECRET keys.",
3798
+ collect,
3799
+ []
3800
+ ).option("-m, --message <text>", "Version description").option("--skip-deploy", "Upload a new version but do not deploy it").action((opts) => run(() => deployCommand(opts)));
3801
+ program.command("api").description("Call a Control Plane path (passthrough for gaps in gf commands)").argument("<path>", "Path beginning with /v1/\u2026 Use {workspace_id} for the current workspace").option("-X, --method <method>", "HTTP method", "GET").option("--body <json>", "JSON body for POST/PUT/PATCH").option("-w, --workspace <id>", "Workspace id override").action(
3802
+ (path11, opts) => run(
3803
+ () => apiCommand({
3804
+ path: path11,
3805
+ method: opts.method,
3806
+ body: opts.body,
3807
+ workspace: opts.workspace
3808
+ })
3809
+ )
3810
+ );
3811
+ var identities = program.command("identities").description("Workload identities (Vault principals)");
3812
+ 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)));
3813
+ 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(
3814
+ (opts) => run(
3815
+ () => identitiesMintCommand({
3816
+ workspace: opts.workspace,
3817
+ identity: opts.identity,
3818
+ rotateRefresh: opts.rotateRefresh,
3819
+ json: opts.json
3820
+ })
3821
+ )
3822
+ );
3186
3823
  function wireUnknownCommands(root, current, latest) {
3187
3824
  const walk = (cmd) => {
3188
3825
  if (cmd.commands.length === 0) return;
@@ -3206,7 +3843,7 @@ function wireUnknownCommands(root, current, latest) {
3206
3843
  async function notifyUpgrade() {
3207
3844
  if (process.env.GF_SKIP_UPDATE_CHECK === "1") return null;
3208
3845
  const latest = await resolveLatest({
3209
- cacheFile: path9.join(paths.CONFIG_DIR, "update-check.json")
3846
+ cacheFile: path10.join(paths.CONFIG_DIR, "update-check.json")
3210
3847
  });
3211
3848
  if (latest && isNewer(latest, GF_VERSION)) {
3212
3849
  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.11",
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
  },