@groundfloorcloud/cli 0.1.8 → 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 +37 -8
  2. package/dist/index.js +650 -89
  3. package/package.json +5 -2
package/README.md CHANGED
@@ -63,8 +63,10 @@ Do not create an App just to run a function, job, schedule, or service
63
63
  (Deployment). Use `gf deploy`. Bind `--app-id` only when the coderunner should
64
64
  be a helper of an existing product App.
65
65
 
66
- **Before the first create:** enable workspace Authentication (Administer →
67
- Authentication, mode `groundfloor` or `external`) until the realm is active.
66
+ **App authentication is optional.** Team **write** can create Coderunners
67
+ when mode is None. For Vault and Files from the workload, attach an M2M
68
+ client (Administer → M2M tokens) and use `gf m2m token` — not a Dataplane
69
+ key. `gf coderunner vault-token` is deprecated.
68
70
 
69
71
  ### Workload types
70
72
 
@@ -104,11 +106,16 @@ port 8080). Add `"scripts": { "start": "node index.js" }` and bind
104
106
  eval "$(gf env)"
105
107
  export PORT=8080
106
108
  npm start
109
+ # files/secrets / operator Vault (builder token):
107
110
  curl -sS -H "Authorization: Bearer $(gf token)" \
111
+ "$CONTROLPLANE_URL/v1/workspaces/$GROUNDFLOOR_WORKSPACE_ID/files"
112
+ # M2M (Coderunner / CI — not gf token):
113
+ curl -sS -H "Authorization: Bearer $(gf m2m token)" \
108
114
  "$CONTROLPLANE_URL/v1/workspaces/$GROUNDFLOOR_WORKSPACE_ID/vault/collections"
109
115
  ```
110
116
 
111
- Then `gf deploy` the same tree. Do not zip `.env` files.
117
+ Then `gf deploy` the same tree. Do not zip `.env` files. `gf token` is the
118
+ wrong identity for testing Coderunner Vault/Files as the workload.
112
119
 
113
120
  ### `groundfloor.json`
114
121
 
@@ -132,7 +139,7 @@ Then `gf deploy` the same tree. Do not zip `.env` files.
132
139
  | `-p, --path <dir>` | Directory to package (default: cwd). |
133
140
  | `--git <url>` / `--ref <ref>` | Clone and deploy a remote repo. |
134
141
  | `--subdir <dir>` | Package a subdirectory of the source. |
135
- | `--runtime <r>` | `python` \| `node` \| `dotnet-script`. |
142
+ | `--runtime <r>` | Language slug from `GET …/coderunners/languages` (`node`, `python`, `go`, `dotnet`, `dotnet-script`, …). |
136
143
  | `--workload-type <t>` | `function` \| `service` (Deployment) \| `job` \| `schedule`. Service requires a Dockerfile. |
137
144
  | `--schedule <cron>` | Cron for `schedule` workloads (or `groundfloor.json` `schedule`). |
138
145
  | `--cpu` / `--memory` | Resource requests, e.g. `250m` / `256Mi`. |
@@ -140,6 +147,25 @@ Then `gf deploy` the same tree. Do not zip `.env` files.
140
147
  | `-m, --message <text>` | Version description. |
141
148
  | `--skip-deploy` | Upload a new version but do not deploy it. |
142
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
+
143
169
  ## Publish a Shell app
144
170
 
145
171
  Product **Apps** are packaging (Shell Module Federation remotes, or a
@@ -163,10 +189,11 @@ gf apps publish --path release.zip
163
189
  `APP_ID` / `groundfloor.manifest.json` `appId` to the slug.
164
190
 
165
191
  `gf apps publish` uploads **only** `release.zip` from `npm run release`.
166
- It flattens a Vite `dist/assets/` layout when needed, then **rejects** a zip
167
- that is only `remoteEntry.js` (missing `__federation_*.js` / CSS) or whose
168
- federation `name` does not match `federationRemoteName(slug)`. A lone
169
- `.js` file is also rejected. Pass an app id/slug, or set `appId` in
192
+ It flattens a Vite `dist/assets/` layout when needed, rewrites `./assets/`
193
+ imports in `remoteEntry.js`, then **rejects** a zip that is only
194
+ `remoteEntry.js` (missing `__federation_*.js` / CSS) or whose federation
195
+ `name` does not match `federationRemoteName(slug)`. A lone `.js` file is
196
+ also rejected. Pass an app id/slug, or set `appId` in
170
197
  `groundfloor.manifest.json` next to the zip.
171
198
 
172
199
  Wrap an existing **service** coderunner as a product:
@@ -215,6 +242,8 @@ gf apps get <id|slug>
215
242
  | `gf coderunner logs -c <id\|slug>` | Process Log (stdout/stderr). Also printed automatically on `gf deploy` failure. |
216
243
  | `gf coderunner cancel-deployment -c <id\|slug>` | Delete the current (or `--deployment-id`) deployment. |
217
244
  | `gf coderunner run -c <id\|slug> [--payload '{}']` | Invoke a deployed function or job. |
245
+ | `gf m2m token` | Print a short Control Plane JWT from M2M client_id + secret. Not `gf token`. |
246
+ | `gf coderunner vault-token` | **Deprecated.** Use `gf m2m token`. |
218
247
  | `gf deploy` | Package code and deploy as a coderunner. |
219
248
  | `gf domains ls\|add\|rm\|verify -c <id\|slug>` | Custom domains for a service. |
220
249
  | `gf secrets ls\|get\|set\|rm` | Workspace secrets. |
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";
@@ -526,18 +526,38 @@ var CpError = class extends Error {
526
526
  }
527
527
  status;
528
528
  };
529
+ function formatCpErrorDetail(detail, fallback) {
530
+ if (typeof detail === "string" && detail.trim()) return detail.trim();
531
+ if (detail && typeof detail === "object") {
532
+ const obj = detail;
533
+ if (typeof obj.message === "string" && obj.message.trim()) {
534
+ return obj.message.trim();
535
+ }
536
+ if (typeof obj.detail === "string" && obj.detail.trim()) {
537
+ return obj.detail.trim();
538
+ }
539
+ try {
540
+ return JSON.stringify(detail);
541
+ } catch {
542
+ return fallback;
543
+ }
544
+ }
545
+ return fallback;
546
+ }
529
547
  async function parseError(res) {
530
548
  let detail = res.statusText;
531
549
  try {
532
550
  const body = await res.json();
533
- if (body?.detail) detail = String(body.detail);
551
+ if (body?.detail !== void 0) {
552
+ detail = formatCpErrorDetail(body.detail, res.statusText);
553
+ }
534
554
  } catch {
535
555
  }
536
556
  return `${res.status} ${detail}`;
537
557
  }
538
- async function cpGet(apiUrl, path10) {
558
+ async function cpGet(apiUrl, path11) {
539
559
  const token = await getValidAccessToken();
540
- const res = await fetch(`${apiUrl}${path10}`, {
560
+ const res = await fetch(`${apiUrl}${path11}`, {
541
561
  headers: {
542
562
  Authorization: `Bearer ${token}`,
543
563
  Accept: "application/json"
@@ -548,9 +568,9 @@ async function cpGet(apiUrl, path10) {
548
568
  }
549
569
  return await res.json();
550
570
  }
551
- async function cpPostJson(apiUrl, path10, body) {
571
+ async function cpPostJson(apiUrl, path11, body) {
552
572
  const token = await getValidAccessToken();
553
- const res = await fetch(`${apiUrl}${path10}`, {
573
+ const res = await fetch(`${apiUrl}${path11}`, {
554
574
  method: "POST",
555
575
  headers: {
556
576
  Authorization: `Bearer ${token}`,
@@ -564,9 +584,9 @@ async function cpPostJson(apiUrl, path10, body) {
564
584
  }
565
585
  return await res.json();
566
586
  }
567
- async function cpDelete(apiUrl, path10) {
587
+ async function cpDelete(apiUrl, path11) {
568
588
  const token = await getValidAccessToken();
569
- const res = await fetch(`${apiUrl}${path10}`, {
589
+ const res = await fetch(`${apiUrl}${path11}`, {
570
590
  method: "DELETE",
571
591
  headers: { Authorization: `Bearer ${token}`, Accept: "application/json" }
572
592
  });
@@ -574,9 +594,9 @@ async function cpDelete(apiUrl, path10) {
574
594
  throw new CpError(res.status, await parseError(res));
575
595
  }
576
596
  }
577
- async function cpPutJson(apiUrl, path10, body) {
597
+ async function cpPutJson(apiUrl, path11, body) {
578
598
  const token = await getValidAccessToken();
579
- const res = await fetch(`${apiUrl}${path10}`, {
599
+ const res = await fetch(`${apiUrl}${path11}`, {
580
600
  method: "PUT",
581
601
  headers: {
582
602
  Authorization: `Bearer ${token}`,
@@ -590,9 +610,9 @@ async function cpPutJson(apiUrl, path10, body) {
590
610
  }
591
611
  return await res.json();
592
612
  }
593
- async function cpPatchJson(apiUrl, path10, body) {
613
+ async function cpPatchJson(apiUrl, path11, body) {
594
614
  const token = await getValidAccessToken();
595
- const res = await fetch(`${apiUrl}${path10}`, {
615
+ const res = await fetch(`${apiUrl}${path11}`, {
596
616
  method: "PATCH",
597
617
  headers: {
598
618
  Authorization: `Bearer ${token}`,
@@ -606,9 +626,9 @@ async function cpPatchJson(apiUrl, path10, body) {
606
626
  }
607
627
  return await res.json();
608
628
  }
609
- async function cpPutBytes(apiUrl, path10, body, contentType) {
629
+ async function cpPutBytes(apiUrl, path11, body, contentType) {
610
630
  const token = await getValidAccessToken();
611
- const res = await fetch(`${apiUrl}${path10}`, {
631
+ const res = await fetch(`${apiUrl}${path11}`, {
612
632
  method: "PUT",
613
633
  headers: {
614
634
  Authorization: `Bearer ${token}`,
@@ -640,6 +660,13 @@ async function listCoderunners(apiUrl, workspaceId) {
640
660
  if (Array.isArray(data)) return data;
641
661
  return data.coderunners ?? [];
642
662
  }
663
+ async function listCoderunnerLanguages(apiUrl, workspaceId) {
664
+ const data = await cpGet(
665
+ apiUrl,
666
+ `/v1/workspaces/${encodeURIComponent(workspaceId)}/coderunners/languages`
667
+ );
668
+ return data.languages ?? [];
669
+ }
643
670
  function createCoderunner(apiUrl, workspaceId, payload) {
644
671
  return cpPostJson(
645
672
  apiUrl,
@@ -733,13 +760,13 @@ function getCoderunnerStatus(apiUrl, workspaceId, coderunnerId) {
733
760
  )}/coderunners/${encodeURIComponent(coderunnerId)}/status`
734
761
  );
735
762
  }
736
- function runCoderunner(apiUrl, workspaceId, coderunnerId, payload) {
763
+ function runCoderunner(apiUrl, workspaceId, coderunnerId, event = {}) {
737
764
  return cpPostJson(
738
765
  apiUrl,
739
766
  `/v1/workspaces/${encodeURIComponent(
740
767
  workspaceId
741
768
  )}/coderunners/${encodeURIComponent(coderunnerId)}/run`,
742
- payload
769
+ event
743
770
  );
744
771
  }
745
772
  function getProcessLogs(apiUrl, workspaceId, query = {}) {
@@ -915,6 +942,69 @@ function provisionDataplane(apiUrl, workspaceId) {
915
942
  {}
916
943
  );
917
944
  }
945
+ async function mintM2mToken(apiUrl, workspaceId, clientId, clientSecret) {
946
+ const res = await fetch(
947
+ `${apiUrl}${wsPath(workspaceId, "/m2m/token")}`,
948
+ {
949
+ method: "POST",
950
+ headers: {
951
+ Accept: "application/json",
952
+ "Content-Type": "application/json"
953
+ },
954
+ body: JSON.stringify({
955
+ client_id: clientId,
956
+ client_secret: clientSecret
957
+ })
958
+ }
959
+ );
960
+ if (!res.ok) {
961
+ throw new CpError(res.status, await parseError(res));
962
+ }
963
+ return await res.json();
964
+ }
965
+ function mintCoderunnerVaultToken(apiUrl, workspaceId, coderunnerId, body = {}) {
966
+ return cpPostJson(
967
+ apiUrl,
968
+ `${wsPath(workspaceId, `/coderunners/${encodeURIComponent(coderunnerId)}/vault-token`)}`,
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
987
+ );
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
+ }
918
1008
 
919
1009
  // src/commands/whoami.ts
920
1010
  async function whoamiCommand() {
@@ -1028,7 +1118,8 @@ async function coderunnerListCommand(opts) {
1028
1118
  for (const cr of coderunners) {
1029
1119
  const name = cr.name ?? cr.slug ?? "";
1030
1120
  const status = cr.status ?? "";
1031
- process.stdout.write(`${cr.id} ${name} [${status}]
1121
+ const type = cr.workload_type ?? "function";
1122
+ process.stdout.write(`${cr.id} ${name} [${type}] [${status}]
1032
1123
  `);
1033
1124
  }
1034
1125
  }
@@ -1043,6 +1134,81 @@ async function resolveCoderunnerId(apiUrl, workspaceId, idOrSlug) {
1043
1134
  return match.id;
1044
1135
  }
1045
1136
 
1137
+ // src/commands/vault-token.ts
1138
+ async function vaultTokenCommand(opts) {
1139
+ const cfg = await resolveConfig({ workspaceId: opts.workspace });
1140
+ if (!cfg.workspaceId) {
1141
+ throw new Error(
1142
+ "No workspace selected. Run `gf workspaces use <id>` or pass --workspace <id>."
1143
+ );
1144
+ }
1145
+ const coderunnerId = await resolveCoderunnerId(
1146
+ cfg.apiUrl,
1147
+ cfg.workspaceId,
1148
+ opts.coderunner
1149
+ );
1150
+ const data = await mintCoderunnerVaultToken(
1151
+ cfg.apiUrl,
1152
+ cfg.workspaceId,
1153
+ coderunnerId
1154
+ );
1155
+ process.stderr.write(
1156
+ "gf coderunner vault-token is deprecated. Attach an M2M client and use gf m2m token.\n"
1157
+ );
1158
+ if (opts.json) {
1159
+ process.stdout.write(`${JSON.stringify(data, null, 2)}
1160
+ `);
1161
+ return;
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
+ }
1170
+ process.stderr.write(
1171
+ `expires_in=${data.expires_in}s identity=${data.workload_identity_id}
1172
+ `
1173
+ );
1174
+ process.stdout.write(`${data.token}
1175
+ `);
1176
+ }
1177
+
1178
+ // src/commands/m2m-token.ts
1179
+ async function m2mTokenCommand(opts) {
1180
+ const cfg = await resolveConfig({ workspaceId: opts.workspace });
1181
+ if (!cfg.workspaceId) {
1182
+ throw new Error(
1183
+ "No workspace selected. Run `gf workspaces use <id>` or pass --workspace <id>."
1184
+ );
1185
+ }
1186
+ const clientId = opts.clientId ?? process.env.GROUNDFLOOR_M2M_CLIENT_ID ?? "";
1187
+ const clientSecret = opts.clientSecret ?? process.env.GROUNDFLOOR_M2M_CLIENT_SECRET ?? "";
1188
+ if (!clientId || !clientSecret) {
1189
+ throw new Error(
1190
+ "Set GROUNDFLOOR_M2M_CLIENT_ID and GROUNDFLOOR_M2M_CLIENT_SECRET, or pass --client-id and --client-secret."
1191
+ );
1192
+ }
1193
+ const data = await mintM2mToken(
1194
+ cfg.apiUrl,
1195
+ cfg.workspaceId,
1196
+ clientId,
1197
+ clientSecret
1198
+ );
1199
+ if (opts.json) {
1200
+ process.stdout.write(`${JSON.stringify(data, null, 2)}
1201
+ `);
1202
+ return;
1203
+ }
1204
+ process.stderr.write(
1205
+ `expires_in=${data.expires_in}s client=${data.m2m_client_id}
1206
+ `
1207
+ );
1208
+ process.stdout.write(`${data.access_token}
1209
+ `);
1210
+ }
1211
+
1046
1212
  // src/commands/cancel-deployment.ts
1047
1213
  async function cancelDeploymentCommand(opts) {
1048
1214
  const cfg = await resolveConfig({ workspaceId: opts.workspace });
@@ -1277,13 +1443,13 @@ portal: ${processLogPortalUrl(
1277
1443
 
1278
1444
  // src/commands/deploy.ts
1279
1445
  import { promises as fs4 } from "fs";
1280
- import path4 from "path";
1446
+ import path5 from "path";
1281
1447
 
1282
1448
  // src/deploy/package.ts
1283
1449
  import { execFile } from "child_process";
1284
1450
  import { promises as fs2 } from "fs";
1285
1451
  import os2 from "os";
1286
- import path2 from "path";
1452
+ import path3 from "path";
1287
1453
  import { promisify } from "util";
1288
1454
  import AdmZip from "adm-zip";
1289
1455
 
@@ -1292,6 +1458,109 @@ function isGitCommitSha(ref) {
1292
1458
  return /^[0-9a-f]{7,40}$/i.test(ref);
1293
1459
  }
1294
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
+
1295
1564
  // src/deploy/package.ts
1296
1565
  var exec = promisify(execFile);
1297
1566
  var DEFAULT_IGNORES = /* @__PURE__ */ new Set([
@@ -1331,7 +1600,7 @@ async function walkFileList(dir) {
1331
1600
  const entries = await fs2.readdir(current, { withFileTypes: true });
1332
1601
  for (const entry of entries) {
1333
1602
  if (DEFAULT_IGNORES.has(entry.name)) continue;
1334
- const abs = path2.join(current, entry.name);
1603
+ const abs = path3.join(current, entry.name);
1335
1604
  const relPath = rel ? `${rel}/${entry.name}` : entry.name;
1336
1605
  if (entry.isDirectory()) {
1337
1606
  await walk(abs, relPath);
@@ -1346,17 +1615,17 @@ async function walkFileList(dir) {
1346
1615
  async function buildZip(sourceDir, files2) {
1347
1616
  const zip = new AdmZip();
1348
1617
  for (const rel of files2) {
1349
- const abs = path2.join(sourceDir, rel);
1618
+ const abs = path3.join(sourceDir, rel);
1350
1619
  try {
1351
1620
  const data = await fs2.readFile(abs);
1352
- zip.addFile(rel.split(path2.sep).join("/"), data);
1621
+ zip.addFile(rel.split(path3.sep).join("/"), data);
1353
1622
  } catch {
1354
1623
  }
1355
1624
  }
1356
1625
  return zip.toBuffer();
1357
1626
  }
1358
1627
  async function cloneRepo(url, ref) {
1359
- const tmp = await fs2.mkdtemp(path2.join(os2.tmpdir(), "gf-deploy-"));
1628
+ const tmp = await fs2.mkdtemp(path3.join(os2.tmpdir(), "gf-deploy-"));
1360
1629
  try {
1361
1630
  if (ref && isGitCommitSha(ref)) {
1362
1631
  await exec("git", ["init"], { cwd: tmp });
@@ -1387,16 +1656,27 @@ async function packageSource(opts) {
1387
1656
  baseDir = await cloneRepo(opts.git, opts.ref);
1388
1657
  cleanup = baseDir;
1389
1658
  } else {
1390
- 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
+ );
1391
1670
  }
1392
- const sourceDir = opts.subdir ? path2.join(baseDir, opts.subdir) : baseDir;
1393
1671
  try {
1394
1672
  const stat = await fs2.stat(sourceDir);
1395
1673
  if (!stat.isDirectory()) {
1396
1674
  throw new Error(`Not a directory: ${sourceDir}`);
1397
1675
  }
1398
1676
  const useGit = await isGitRepo(sourceDir);
1399
- 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;
1400
1680
  if (files2.length === 0) {
1401
1681
  throw new Error(
1402
1682
  `No files to package in ${sourceDir} (everything ignored or empty).`
@@ -1413,7 +1693,14 @@ async function packageSource(opts) {
1413
1693
  gitSha = null;
1414
1694
  }
1415
1695
  }
1416
- 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
+ };
1417
1704
  } catch (err) {
1418
1705
  if (cleanup) {
1419
1706
  await fs2.rm(cleanup, { recursive: true, force: true }).catch(() => {
@@ -1424,7 +1711,7 @@ async function packageSource(opts) {
1424
1711
  }
1425
1712
  async function readProjectManifest(dir) {
1426
1713
  try {
1427
- const raw = await fs2.readFile(path2.join(dir, "groundfloor.json"), "utf8");
1714
+ const raw = await fs2.readFile(path3.join(dir, "groundfloor.json"), "utf8");
1428
1715
  return JSON.parse(raw);
1429
1716
  } catch {
1430
1717
  return null;
@@ -1433,7 +1720,7 @@ async function readProjectManifest(dir) {
1433
1720
  async function detectRuntime(dir) {
1434
1721
  const has = async (name) => {
1435
1722
  try {
1436
- await fs2.access(path2.join(dir, name));
1723
+ await fs2.access(path3.join(dir, name));
1437
1724
  return true;
1438
1725
  } catch {
1439
1726
  return false;
@@ -1447,7 +1734,7 @@ async function detectRuntime(dir) {
1447
1734
 
1448
1735
  // src/deploy/preflight.ts
1449
1736
  import { promises as fs3 } from "fs";
1450
- import path3 from "path";
1737
+ import path4 from "path";
1451
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).';
1452
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).';
1453
1740
  var NODE_DOCKERFILE_NPM_START_MISSING = 'Dockerfile runs `npm start` but package.json has no "scripts.start". Add "scripts": { "start": "node <entrypoint>" }.';
@@ -1462,10 +1749,10 @@ async function readOptional(filePath) {
1462
1749
  }
1463
1750
  }
1464
1751
  async function preflightNodePackage(sourceDir) {
1465
- const dockerfile = await readOptional(path3.join(sourceDir, "Dockerfile"));
1752
+ const dockerfile = await readOptional(path4.join(sourceDir, "Dockerfile"));
1466
1753
  const needsStartScript = dockerfile == null || dockerfileRunsNpmStart(dockerfile);
1467
1754
  if (!needsStartScript) return;
1468
- const raw = await readOptional(path3.join(sourceDir, "package.json"));
1755
+ const raw = await readOptional(path4.join(sourceDir, "package.json"));
1469
1756
  if (raw == null) {
1470
1757
  throw new Error(
1471
1758
  dockerfile == null ? NODE_BOOTSTRAP_MISSING_PACKAGE : NODE_DOCKERFILE_NPM_START_MISSING
@@ -1508,8 +1795,53 @@ function formatDeployTarget(input) {
1508
1795
  return `Deploy target: workspace=${label} cell=${cell} api=${input.apiUrl}`;
1509
1796
  }
1510
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
+
1511
1837
  // src/commands/deploy.ts
1512
- var RUNTIMES = /* @__PURE__ */ new Set(["python", "node", "dotnet-script"]);
1838
+ var FALLBACK_RUNTIMES = /* @__PURE__ */ new Set([
1839
+ "python",
1840
+ "node",
1841
+ "go",
1842
+ "dotnet",
1843
+ "dotnet-script"
1844
+ ]);
1513
1845
  var WORKLOAD_TYPES = /* @__PURE__ */ new Set(["function", "service", "job", "schedule"]);
1514
1846
  var VERSION_POLL_INTERVAL_MS = 2e3;
1515
1847
  var VERSION_POLL_TIMEOUT_MS = 5 * 60 * 1e3;
@@ -1537,6 +1869,22 @@ function parseEnvPairs(pairs) {
1537
1869
  }
1538
1870
  var log = (msg) => process.stderr.write(`${msg}
1539
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
+ }
1540
1888
  async function failDeploy(opts) {
1541
1889
  throw new Error(await diagnoseDeployFailure(opts));
1542
1890
  }
@@ -1566,7 +1914,7 @@ async function deployCommand(opts) {
1566
1914
  })
1567
1915
  );
1568
1916
  log(
1569
- 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())}...`
1570
1918
  );
1571
1919
  const pkg = await packageSource({
1572
1920
  path: opts.path,
@@ -1578,11 +1926,20 @@ async function deployCommand(opts) {
1578
1926
  const manifest = await readProjectManifest(pkg.sourceDir) ?? {};
1579
1927
  const sizeKb = (pkg.zip.byteLength / 1024).toFixed(1);
1580
1928
  log(`Packaged ${pkg.fileCount} file(s), ${sizeKb} KB`);
1929
+ reportFiltering(pkg.filtered);
1581
1930
  const detected = await detectRuntime(pkg.sourceDir);
1582
1931
  const runtime = opts.runtime ?? manifest.runtime ?? detected ?? "python";
1583
- if (!RUNTIMES.has(runtime)) {
1932
+ let allowedRuntimes = FALLBACK_RUNTIMES;
1933
+ try {
1934
+ const catalog = await listCoderunnerLanguages(cfg.apiUrl, ws);
1935
+ if (catalog.length > 0) {
1936
+ allowedRuntimes = new Set(catalog.map((item) => item.runtime));
1937
+ }
1938
+ } catch {
1939
+ }
1940
+ if (!allowedRuntimes.has(runtime)) {
1584
1941
  throw new Error(
1585
- `Invalid runtime "${runtime}" (expected: ${[...RUNTIMES].join(", ")}).`
1942
+ `Invalid runtime "${runtime}" (expected: ${[...allowedRuntimes].join(", ")}).`
1586
1943
  );
1587
1944
  }
1588
1945
  const workloadType = opts.workloadType ?? manifest.workloadType ?? manifest.workload_type ?? "function";
@@ -1593,7 +1950,7 @@ async function deployCommand(opts) {
1593
1950
  }
1594
1951
  if (workloadType === "service") {
1595
1952
  try {
1596
- await fs4.access(path4.join(pkg.sourceDir, "Dockerfile"));
1953
+ await fs4.access(path5.join(pkg.sourceDir, "Dockerfile"));
1597
1954
  } catch {
1598
1955
  throw new Error(
1599
1956
  'workload type "service" (also called Deployment) requires a Dockerfile at the project root.'
@@ -1610,7 +1967,7 @@ async function deployCommand(opts) {
1610
1967
  });
1611
1968
  let coderunnerId = opts.coderunner;
1612
1969
  if (!coderunnerId) {
1613
- 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())));
1614
1971
  const slug = manifest.slug ?? slugify(name);
1615
1972
  const existing = await listCoderunners(cfg.apiUrl, ws);
1616
1973
  const match = existing.find((c) => c.slug === slug || c.name === name);
@@ -1624,7 +1981,8 @@ async function deployCommand(opts) {
1624
1981
  slug,
1625
1982
  runtime,
1626
1983
  workload_type: workloadType,
1627
- ...opts.appId ? { app_id: opts.appId } : {}
1984
+ ...opts.appId ? { app_id: opts.appId } : {},
1985
+ ...schedule ? { schedule } : {}
1628
1986
  });
1629
1987
  coderunnerId = created.id;
1630
1988
  log(`Created coderunner ${created.id}`);
@@ -1704,13 +2062,21 @@ async function deployCommand(opts) {
1704
2062
  ...manifest.env ?? {},
1705
2063
  ...parseEnvPairs(opts.env)
1706
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);
1707
2072
  log("Deploying...");
1708
2073
  const result = await deployCoderunner(cfg.apiUrl, ws, coderunnerId, {
1709
2074
  version_id: uploaded.version_id,
1710
2075
  cpu: opts.cpu ?? manifest.cpu,
1711
2076
  memory: opts.memory ?? manifest.memory,
1712
2077
  envVariable: Object.keys(envVariable).length ? envVariable : void 0,
1713
- schedule
2078
+ schedule,
2079
+ inject_secret_keys: injectSecretKeys.length ? injectSecretKeys : void 0
1714
2080
  });
1715
2081
  log("Waiting for deployment...");
1716
2082
  const deployDeadline = Date.now() + DEPLOY_POLL_TIMEOUT_MS;
@@ -1802,9 +2168,12 @@ async function runCommand(opts) {
1802
2168
  cfg.workspaceId,
1803
2169
  opts.coderunner
1804
2170
  );
1805
- const result = await runCoderunner(cfg.apiUrl, cfg.workspaceId, coderunnerId, {
2171
+ const result = await runCoderunner(
2172
+ cfg.apiUrl,
2173
+ cfg.workspaceId,
2174
+ coderunnerId,
1806
2175
  payload
1807
- });
2176
+ );
1808
2177
  if (opts.json) {
1809
2178
  process.stdout.write(`${JSON.stringify(result, null, 2)}
1810
2179
  `);
@@ -1832,7 +2201,29 @@ async function statusCommand(opts) {
1832
2201
  getDeployment(cfg.apiUrl, cfg.workspaceId, coderunnerId)
1833
2202
  ]);
1834
2203
  const out = { coderunner_id: coderunnerId, status, deployment };
1835
- 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)}
1836
2227
  `);
1837
2228
  }
1838
2229
 
@@ -2006,9 +2397,9 @@ async function secretsRmCommand(key, opts) {
2006
2397
 
2007
2398
  // src/commands/files.ts
2008
2399
  import { promises as fs5 } from "fs";
2009
- import path5 from "path";
2400
+ import path6 from "path";
2010
2401
  function guessContentType(filename) {
2011
- const ext = path5.extname(filename).toLowerCase();
2402
+ const ext = path6.extname(filename).toLowerCase();
2012
2403
  const types = {
2013
2404
  ".json": "application/json",
2014
2405
  ".txt": "text/plain",
@@ -2043,9 +2434,9 @@ async function filesListCommand(opts) {
2043
2434
  }
2044
2435
  async function filesUploadCommand(filePath, opts) {
2045
2436
  const cfg = await requireWorkspace(opts.workspace);
2046
- const resolved = path5.resolve(filePath);
2437
+ const resolved = path6.resolve(filePath);
2047
2438
  const buf = await fs5.readFile(resolved);
2048
- const name = opts.name ?? path5.basename(resolved);
2439
+ const name = opts.name ?? path6.basename(resolved);
2049
2440
  const contentType = guessContentType(name);
2050
2441
  const init = await initFileUpload(cfg.apiUrl, cfg.workspaceId, {
2051
2442
  name,
@@ -2082,7 +2473,7 @@ async function filesDownloadCommand(fileId, opts) {
2082
2473
  throw new Error(`Download failed: ${res.status} ${res.statusText}`);
2083
2474
  }
2084
2475
  const buf = Buffer.from(await res.arrayBuffer());
2085
- const dest = path5.resolve(opts.out ?? fileId);
2476
+ const dest = path6.resolve(opts.out ?? fileId);
2086
2477
  await fs5.writeFile(dest, buf);
2087
2478
  process.stdout.write(`Wrote ${dest}
2088
2479
  `);
@@ -2096,12 +2487,16 @@ async function filesRmCommand(fileId, opts) {
2096
2487
 
2097
2488
  // src/commands/apps.ts
2098
2489
  import { promises as fs6 } from "fs";
2099
- import path6 from "path";
2490
+ import path7 from "path";
2100
2491
 
2101
2492
  // src/release-zip.ts
2102
2493
  import AdmZip2 from "adm-zip";
2103
2494
  var REMOTE_ENTRY = "remoteEntry.js";
2104
2495
  var FLATTEN_SUFFIXES = [".js", ".mjs", ".cjs", ".css", ".map", ".wasm"];
2496
+ var IMPORT_ASSETS = /(__federation_import|import)\((['"])\.\/assets\//g;
2497
+ function rewriteRemoteEntryAssetPrefix(source) {
2498
+ return source.replace(IMPORT_ASSETS, "$1($2./");
2499
+ }
2105
2500
  function normalizeZipEntryName(filename) {
2106
2501
  return filename.replace(/\\/g, "/").replace(/^\/+/, "");
2107
2502
  }
@@ -2136,8 +2531,8 @@ function flattenFederatedBundlePlan(relpaths) {
2136
2531
  const promoteDirs = /* @__PURE__ */ new Set();
2137
2532
  const entryDir = parentDir(entry);
2138
2533
  if (entryDir) promoteDirs.add(entryDir);
2139
- for (const path10 of pathset) {
2140
- const parent = parentDir(path10);
2534
+ for (const path11 of pathset) {
2535
+ const parent = parentDir(path11);
2141
2536
  if (parent === "assets" || parent.endsWith("/assets") && parent.split("/").length - 1 < 2) {
2142
2537
  promoteDirs.add(parent);
2143
2538
  }
@@ -2172,15 +2567,31 @@ function flattenFederatedReleaseZip(bytes) {
2172
2567
  }
2173
2568
  const plan = flattenFederatedBundlePlan(rels);
2174
2569
  const keys = Object.keys(plan);
2175
- if (keys.length === 0) return bytes;
2176
- for (const dest of keys) {
2177
- const src = plan[dest];
2178
- const source = byNorm.get(src);
2179
- if (!source) continue;
2180
- zip.addFile(dest, source.getData());
2570
+ if (keys.length > 0) {
2571
+ for (const dest of keys) {
2572
+ const src = plan[dest];
2573
+ const source = byNorm.get(src);
2574
+ if (!source) continue;
2575
+ zip.addFile(dest, source.getData());
2576
+ }
2181
2577
  }
2578
+ rewriteRemoteEntryInZip(zip);
2182
2579
  return zip.toBuffer();
2183
2580
  }
2581
+ function rewriteRemoteEntryInZip(zip) {
2582
+ const names2 = zip.getEntries().filter((entry) => !entry.isDirectory).map((entry) => normalizeZipEntryName(entry.entryName));
2583
+ const entryRel = chooseRemoteEntry(names2);
2584
+ if (!entryRel) return;
2585
+ const zipEntry = zip.getEntries().find((item) => {
2586
+ if (item.isDirectory) return false;
2587
+ return normalizeZipEntryName(item.entryName) === entryRel;
2588
+ });
2589
+ if (!zipEntry) return;
2590
+ const before = zipEntry.getData().toString("utf8");
2591
+ const after = rewriteRemoteEntryAssetPrefix(before);
2592
+ if (after === before) return;
2593
+ zip.updateFile(zipEntry.entryName, Buffer.from(after, "utf8"));
2594
+ }
2184
2595
  function federatedReleaseZipHint(slug) {
2185
2596
  return `Run \`npm run release\` in a kit from \`gf apps init\`, then publish that zip (do not zip Vite dist/ or upload only remoteEntry.js). ${initThenPublishHint(slug)}`;
2186
2597
  }
@@ -2193,18 +2604,18 @@ function initThenPublishHint(slug) {
2193
2604
  return `gf apps init --slug ${s} && cd ${s} && npm install && npm run release && gf apps publish --path release.zip`;
2194
2605
  }
2195
2606
  function extractReferencedSiblings(remoteEntrySource) {
2196
- const names = /* @__PURE__ */ new Set();
2607
+ const names2 = /* @__PURE__ */ new Set();
2197
2608
  for (const match of remoteEntrySource.matchAll(
2198
- /["']\.\/([^"'/]+\.(?:js|mjs|cjs|css))["']/g
2609
+ /["']\.\/(?:assets\/)?([^"'/]+\.(?:js|mjs|cjs|css))["']/g
2199
2610
  )) {
2200
- if (match[1] !== REMOTE_ENTRY) names.add(match[1]);
2611
+ if (match[1] !== REMOTE_ENTRY) names2.add(match[1]);
2201
2612
  }
2202
2613
  for (const match of remoteEntrySource.matchAll(
2203
2614
  /["']([A-Za-z0-9._-]+\.css)["']/g
2204
2615
  )) {
2205
- names.add(match[1]);
2616
+ names2.add(match[1]);
2206
2617
  }
2207
- return [...names];
2618
+ return [...names2];
2208
2619
  }
2209
2620
  function extractFederationScope(remoteEntrySource) {
2210
2621
  return remoteEntrySource.match(/css__([A-Za-z0-9_-]+)__/)?.[1];
@@ -2223,9 +2634,9 @@ function readZipFile(bytes, rel) {
2223
2634
  }
2224
2635
  function assertFederatedReleaseZip(bytes, slug) {
2225
2636
  const hint = initThenPublishHint(slug);
2226
- const names = zipRootNames(bytes);
2227
- const present = new Set(names.map((name) => basename(name)));
2228
- const entryRel = chooseRemoteEntry(names);
2637
+ const names2 = zipRootNames(bytes);
2638
+ const present = new Set(names2.map((name) => basename(name)));
2639
+ const entryRel = chooseRemoteEntry(names2);
2229
2640
  if (!entryRel) {
2230
2641
  throw new Error(`Zip has no remoteEntry.js. ${hint}`);
2231
2642
  }
@@ -2235,7 +2646,7 @@ function assertFederatedReleaseZip(bytes, slug) {
2235
2646
  }
2236
2647
  const siblings = extractReferencedSiblings(source);
2237
2648
  const missing = siblings.filter((name) => !present.has(name));
2238
- const jsOrCssAtRoot = names.filter((name) => {
2649
+ const jsOrCssAtRoot = names2.filter((name) => {
2239
2650
  if (name.includes("/")) return false;
2240
2651
  const lower = name.toLowerCase();
2241
2652
  return lower.endsWith(".js") || lower.endsWith(".mjs") || lower.endsWith(".css");
@@ -2277,11 +2688,11 @@ async function readJsonObject(filePath) {
2277
2688
  }
2278
2689
  async function maybeReadManifest(filePath) {
2279
2690
  if (!filePath) return void 0;
2280
- return readJsonObject(path6.resolve(filePath));
2691
+ return readJsonObject(path7.resolve(filePath));
2281
2692
  }
2282
2693
  async function findSiblingManifest(bundlePath) {
2283
- const dir = await isDirectory(bundlePath) ? bundlePath : path6.dirname(bundlePath);
2284
- 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");
2285
2696
  try {
2286
2697
  await fs6.access(candidate);
2287
2698
  } catch {
@@ -2439,8 +2850,8 @@ Do not scaffold src/App.tsx from scratch \u2014 BrowserRouter in App.tsx crashes
2439
2850
  }
2440
2851
  async function appsPublishCommand(appRef, opts) {
2441
2852
  const cfg = await requireWorkspace(opts.workspace);
2442
- const inputPath = path6.resolve(opts.path ?? "release.zip");
2443
- 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;
2444
2855
  let bytes;
2445
2856
  try {
2446
2857
  bytes = await fs6.readFile(bundlePath);
@@ -2470,11 +2881,11 @@ async function appsPublishCommand(appRef, opts) {
2470
2881
  `Releases are only for shell_federated apps (this app is ${app.app_kind}). Use \`gf deploy\` for coderunner workloads.`
2471
2882
  );
2472
2883
  }
2473
- const ext = path6.extname(bundlePath).toLowerCase();
2884
+ const ext = path7.extname(bundlePath).toLowerCase();
2474
2885
  const isZip = ext === ".zip";
2475
2886
  if (!isZip) {
2476
2887
  throw new Error(
2477
- `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)
2478
2889
  );
2479
2890
  }
2480
2891
  bytes = prepareReleaseZip(bytes, app.slug);
@@ -2487,7 +2898,10 @@ async function appsPublishCommand(appRef, opts) {
2487
2898
  }
2488
2899
  const init = await initAppReleaseUpload(cfg.apiUrl, cfg.workspaceId, app.id, {
2489
2900
  bundle,
2490
- 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
2491
2905
  });
2492
2906
  await uploadAppReleaseContent(
2493
2907
  cfg.apiUrl,
@@ -2516,11 +2930,20 @@ async function appsPublishCommand(appRef, opts) {
2516
2930
  // src/commands/apps-init.ts
2517
2931
  import { promises as fs7 } from "fs";
2518
2932
  import os3 from "os";
2519
- import path7 from "path";
2933
+ import path8 from "path";
2520
2934
  import AdmZip4 from "adm-zip";
2521
2935
 
2522
2936
  // src/version.ts
2523
- 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();
2524
2947
  var GF_NPM_PACKAGE = "@groundfloorcloud/cli";
2525
2948
 
2526
2949
  // src/commands/apps-init.ts
@@ -2551,17 +2974,17 @@ async function isEmptyDir(dir) {
2551
2974
  }
2552
2975
  async function findKitRoot(extracted) {
2553
2976
  const candidates = [
2554
- path7.join(extracted, "shell-starter-kit"),
2977
+ path8.join(extracted, "shell-starter-kit"),
2555
2978
  extracted
2556
2979
  ];
2557
2980
  const entries = await fs7.readdir(extracted, { withFileTypes: true });
2558
2981
  for (const entry of entries) {
2559
2982
  if (entry.isDirectory()) {
2560
- candidates.push(path7.join(extracted, entry.name));
2983
+ candidates.push(path8.join(extracted, entry.name));
2561
2984
  }
2562
2985
  }
2563
2986
  for (const candidate of candidates) {
2564
- if (await pathExists(path7.join(candidate, "groundfloor.manifest.json"))) {
2987
+ if (await pathExists(path8.join(candidate, "groundfloor.manifest.json"))) {
2565
2988
  return candidate;
2566
2989
  }
2567
2990
  }
@@ -2643,7 +3066,7 @@ async function appsInitCommand(opts) {
2643
3066
  `);
2644
3067
  }
2645
3068
  const name = opts.name?.trim() || titleFromSlug(slug);
2646
- 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));
2647
3070
  const cfg = await resolveConfig();
2648
3071
  const cell = cfg.environment ?? "production";
2649
3072
  const kitUrl = opts.kitUrl?.trim() || starterKitZipUrl(cell);
@@ -2658,7 +3081,7 @@ async function appsInitCommand(opts) {
2658
3081
  const bytes = await downloadKit(kitUrl);
2659
3082
  const zip = new AdmZip4(bytes);
2660
3083
  assertSafeZipEntries(zip);
2661
- 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-"));
2662
3085
  try {
2663
3086
  zip.extractAllTo(tmp, true);
2664
3087
  const kitRoot = await findKitRoot(tmp);
@@ -2667,8 +3090,8 @@ async function appsInitCommand(opts) {
2667
3090
  } finally {
2668
3091
  await fs7.rm(tmp, { recursive: true, force: true });
2669
3092
  }
2670
- const identityPath = path7.join(dest, "src", "appIdentity.ts");
2671
- 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");
2672
3095
  const identity = await fs7.readFile(identityPath, "utf8");
2673
3096
  await fs7.writeFile(identityPath, stampAppIdentity(identity, slug), "utf8");
2674
3097
  const manifest = await fs7.readFile(manifestPath, "utf8");
@@ -2745,7 +3168,7 @@ async function dataplaneProvisionCommand(opts) {
2745
3168
 
2746
3169
  // src/update-check.ts
2747
3170
  import { promises as fs8 } from "fs";
2748
- import path8 from "path";
3171
+ import path9 from "path";
2749
3172
  var UPDATE_TTL_MS = 24 * 60 * 60 * 1e3;
2750
3173
  function isNewer(latest, current) {
2751
3174
  const a = latest.split(".").map((n) => Number.parseInt(n, 10) || 0);
@@ -2798,7 +3221,7 @@ async function readUpdateCache(file) {
2798
3221
  }
2799
3222
  }
2800
3223
  async function writeUpdateCache(file, cache) {
2801
- await fs8.mkdir(path8.dirname(file), { recursive: true, mode: 448 });
3224
+ await fs8.mkdir(path9.dirname(file), { recursive: true, mode: 448 });
2802
3225
  await fs8.writeFile(file, `${JSON.stringify(cache)}
2803
3226
  `, { mode: 384 });
2804
3227
  }
@@ -2836,6 +3259,90 @@ function commanderExitCode(err) {
2836
3259
  return 0;
2837
3260
  }
2838
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
+
2839
3346
  // src/index.ts
2840
3347
  function collect(value, previous) {
2841
3348
  return previous.concat([value]);
@@ -2867,6 +3374,22 @@ program.command("login").description(
2867
3374
  program.command("logout").description("Clear the cached session").action(() => run(() => logoutCommand()));
2868
3375
  program.command("whoami").description("Show the current identity and verify the Control Plane session").action(() => run(() => whoamiCommand()));
2869
3376
  program.command("token").description("Print a valid (auto-refreshed) access token to stdout").action(() => run(() => tokenCommand()));
3377
+ var m2m = program.command("m2m").description("Machine tokens (client_id + secret \u2192 short Control Plane JWT)");
3378
+ m2m.command("token").description(
3379
+ "Exchange GROUNDFLOOR_M2M_CLIENT_ID + SECRET for a short CP JWT (not gf token)"
3380
+ ).option("-w, --workspace <id>", "Workspace id override").option("--client-id <id>", "M2M client_id (else GROUNDFLOOR_M2M_CLIENT_ID)").option(
3381
+ "--client-secret <secret>",
3382
+ "M2M secret (else GROUNDFLOOR_M2M_CLIENT_SECRET)"
3383
+ ).option("--json", "Output the full token response as JSON").action(
3384
+ (opts) => run(
3385
+ () => m2mTokenCommand({
3386
+ workspace: opts.workspace,
3387
+ clientId: opts.clientId,
3388
+ clientSecret: opts.clientSecret,
3389
+ json: opts.json
3390
+ })
3391
+ )
3392
+ );
2870
3393
  program.command("env").description('Print shell exports for `eval "$(gf env)"`').action(() => run(() => envCommand()));
2871
3394
  var environments = program.command("environments").alias("envs").description("List workspace environments");
2872
3395
  environments.command("list", { isDefault: true }).alias("ls").description("List environments in the default (or --workspace) workspace").option("-w, --workspace <id>", "Workspace id override").option("--json", "Output raw JSON").action((opts) => run(() => environmentsListCommand(opts)));
@@ -2899,7 +3422,7 @@ coderunner.command("logs").description("Show Process Log lines for a coderunner
2899
3422
  })
2900
3423
  )
2901
3424
  );
2902
- 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(
2903
3426
  (opts) => run(
2904
3427
  () => runCommand({
2905
3428
  workspace: opts.workspace,
@@ -2909,6 +3432,17 @@ coderunner.command("run").description("Invoke a deployed function/job (smoke run
2909
3432
  })
2910
3433
  )
2911
3434
  );
3435
+ coderunner.command("vault-token").description(
3436
+ "Deprecated: use gf m2m token. D-067 Data Vault JWT for a bound Coderunner"
3437
+ ).requiredOption("-c, --coderunner <id|slug>", "Coderunner id or slug").option("-w, --workspace <id>", "Workspace id override").option("--json", "Output the full token response as JSON").action(
3438
+ (opts) => run(
3439
+ () => vaultTokenCommand({
3440
+ workspace: opts.workspace,
3441
+ coderunner: opts.coderunner,
3442
+ json: opts.json
3443
+ })
3444
+ )
3445
+ );
2912
3446
  var domains = program.command("domains").description("Manage custom domains (CNAME) for a coderunner service");
2913
3447
  domains.command("add <domain>").description("Add a custom domain and print the DNS records to create").requiredOption("-c, --coderunner <id|slug>", "Target coderunner id or slug").option("-w, --workspace <id>", "Workspace id override").option("--json", "Output raw JSON").action((domain, opts) => run(() => domainsAddCommand(domain, opts)));
2914
3448
  domains.command("ls").alias("list").description("List custom domains for a coderunner service").requiredOption("-c, --coderunner <id|slug>", "Target coderunner id or slug").option("-w, --workspace <id>", "Workspace id override").option("--json", "Output raw JSON").action((opts) => run(() => domainsListCommand(opts)));
@@ -2990,7 +3524,7 @@ program.command("deploy").description(
2990
3524
  ).option("-w, --workspace <id>", "Workspace id override").option("--api-url <url>", "Control Plane base URL").option(
2991
3525
  "-c, --coderunner <id>",
2992
3526
  "Deploy into an existing coderunner id (skip find/create)"
2993
- ).option("-n, --name <name>", "Coderunner name (default: folder or repo name)").option("-p, --path <dir>", "Local directory to package (default: cwd)").option("--git <url>", "Clone and deploy a remote git/bitbucket repo").option("--ref <ref>", "Branch/tag/commit for --git").option("--subdir <dir>", "Subdirectory within the source to package").option("--runtime <runtime>", "python | node | dotnet-script").option(
3527
+ ).option("-n, --name <name>", "Coderunner name (default: folder or repo name)").option("-p, --path <dir>", "Local directory to package (default: cwd)").option("--git <url>", "Clone and deploy a remote git/bitbucket repo").option("--ref <ref>", "Branch/tag/commit for --git").option("--subdir <dir>", "Subdirectory within the source to package").option("--runtime <runtime>", "Language slug from Code Runner (node, python, go, \u2026)").option(
2994
3528
  "--workload-type <type>",
2995
3529
  "function | service | job | schedule"
2996
3530
  ).option(
@@ -2999,7 +3533,34 @@ program.command("deploy").description(
2999
3533
  ).option(
3000
3534
  "--app-id <id>",
3001
3535
  "Bind newly created coderunner as a helper of this App (optional)"
3002
- ).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
+ );
3003
3564
  function wireUnknownCommands(root, current, latest) {
3004
3565
  const walk = (cmd) => {
3005
3566
  if (cmd.commands.length === 0) return;
@@ -3023,7 +3584,7 @@ function wireUnknownCommands(root, current, latest) {
3023
3584
  async function notifyUpgrade() {
3024
3585
  if (process.env.GF_SKIP_UPDATE_CHECK === "1") return null;
3025
3586
  const latest = await resolveLatest({
3026
- cacheFile: path9.join(paths.CONFIG_DIR, "update-check.json")
3587
+ cacheFile: path10.join(paths.CONFIG_DIR, "update-check.json")
3027
3588
  });
3028
3589
  if (latest && isNewer(latest, GF_VERSION)) {
3029
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.8",
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
  },