@groundfloorcloud/cli 0.1.8 → 0.1.9

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 +18 -8
  2. package/dist/index.js +195 -12
  3. package/package.json +1 -1
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`. |
@@ -163,10 +170,11 @@ gf apps publish --path release.zip
163
170
  `APP_ID` / `groundfloor.manifest.json` `appId` to the slug.
164
171
 
165
172
  `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
173
+ It flattens a Vite `dist/assets/` layout when needed, rewrites `./assets/`
174
+ imports in `remoteEntry.js`, then **rejects** a zip that is only
175
+ `remoteEntry.js` (missing `__federation_*.js` / CSS) or whose federation
176
+ `name` does not match `federationRemoteName(slug)`. A lone `.js` file is
177
+ also rejected. Pass an app id/slug, or set `appId` in
170
178
  `groundfloor.manifest.json` next to the zip.
171
179
 
172
180
  Wrap an existing **service** coderunner as a product:
@@ -215,6 +223,8 @@ gf apps get <id|slug>
215
223
  | `gf coderunner logs -c <id\|slug>` | Process Log (stdout/stderr). Also printed automatically on `gf deploy` failure. |
216
224
  | `gf coderunner cancel-deployment -c <id\|slug>` | Delete the current (or `--deployment-id`) deployment. |
217
225
  | `gf coderunner run -c <id\|slug> [--payload '{}']` | Invoke a deployed function or job. |
226
+ | `gf m2m token` | Print a short Control Plane JWT from M2M client_id + secret. Not `gf token`. |
227
+ | `gf coderunner vault-token` | **Deprecated.** Use `gf m2m token`. |
218
228
  | `gf deploy` | Package code and deploy as a coderunner. |
219
229
  | `gf domains ls\|add\|rm\|verify -c <id\|slug>` | Custom domains for a service. |
220
230
  | `gf secrets ls\|get\|set\|rm` | Workspace secrets. |
package/dist/index.js CHANGED
@@ -526,11 +526,31 @@ 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}`;
@@ -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,
@@ -915,6 +942,33 @@ 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) {
966
+ return cpPostJson(
967
+ apiUrl,
968
+ `${wsPath(workspaceId, `/coderunners/${encodeURIComponent(coderunnerId)}/vault-token`)}`,
969
+ {}
970
+ );
971
+ }
918
972
 
919
973
  // src/commands/whoami.ts
920
974
  async function whoamiCommand() {
@@ -1043,6 +1097,74 @@ async function resolveCoderunnerId(apiUrl, workspaceId, idOrSlug) {
1043
1097
  return match.id;
1044
1098
  }
1045
1099
 
1100
+ // src/commands/vault-token.ts
1101
+ async function vaultTokenCommand(opts) {
1102
+ const cfg = await resolveConfig({ workspaceId: opts.workspace });
1103
+ if (!cfg.workspaceId) {
1104
+ throw new Error(
1105
+ "No workspace selected. Run `gf workspaces use <id>` or pass --workspace <id>."
1106
+ );
1107
+ }
1108
+ const coderunnerId = await resolveCoderunnerId(
1109
+ cfg.apiUrl,
1110
+ cfg.workspaceId,
1111
+ opts.coderunner
1112
+ );
1113
+ const data = await mintCoderunnerVaultToken(
1114
+ cfg.apiUrl,
1115
+ cfg.workspaceId,
1116
+ coderunnerId
1117
+ );
1118
+ process.stderr.write(
1119
+ "gf coderunner vault-token is deprecated. Attach an M2M client and use gf m2m token.\n"
1120
+ );
1121
+ if (opts.json) {
1122
+ process.stdout.write(`${JSON.stringify(data, null, 2)}
1123
+ `);
1124
+ return;
1125
+ }
1126
+ process.stderr.write(
1127
+ `expires_in=${data.expires_in}s identity=${data.workload_identity_id}
1128
+ `
1129
+ );
1130
+ process.stdout.write(`${data.token}
1131
+ `);
1132
+ }
1133
+
1134
+ // src/commands/m2m-token.ts
1135
+ async function m2mTokenCommand(opts) {
1136
+ const cfg = await resolveConfig({ workspaceId: opts.workspace });
1137
+ if (!cfg.workspaceId) {
1138
+ throw new Error(
1139
+ "No workspace selected. Run `gf workspaces use <id>` or pass --workspace <id>."
1140
+ );
1141
+ }
1142
+ const clientId = opts.clientId ?? process.env.GROUNDFLOOR_M2M_CLIENT_ID ?? "";
1143
+ const clientSecret = opts.clientSecret ?? process.env.GROUNDFLOOR_M2M_CLIENT_SECRET ?? "";
1144
+ if (!clientId || !clientSecret) {
1145
+ throw new Error(
1146
+ "Set GROUNDFLOOR_M2M_CLIENT_ID and GROUNDFLOOR_M2M_CLIENT_SECRET, or pass --client-id and --client-secret."
1147
+ );
1148
+ }
1149
+ const data = await mintM2mToken(
1150
+ cfg.apiUrl,
1151
+ cfg.workspaceId,
1152
+ clientId,
1153
+ clientSecret
1154
+ );
1155
+ if (opts.json) {
1156
+ process.stdout.write(`${JSON.stringify(data, null, 2)}
1157
+ `);
1158
+ return;
1159
+ }
1160
+ process.stderr.write(
1161
+ `expires_in=${data.expires_in}s client=${data.m2m_client_id}
1162
+ `
1163
+ );
1164
+ process.stdout.write(`${data.access_token}
1165
+ `);
1166
+ }
1167
+
1046
1168
  // src/commands/cancel-deployment.ts
1047
1169
  async function cancelDeploymentCommand(opts) {
1048
1170
  const cfg = await resolveConfig({ workspaceId: opts.workspace });
@@ -1509,7 +1631,13 @@ function formatDeployTarget(input) {
1509
1631
  }
1510
1632
 
1511
1633
  // src/commands/deploy.ts
1512
- var RUNTIMES = /* @__PURE__ */ new Set(["python", "node", "dotnet-script"]);
1634
+ var FALLBACK_RUNTIMES = /* @__PURE__ */ new Set([
1635
+ "python",
1636
+ "node",
1637
+ "go",
1638
+ "dotnet",
1639
+ "dotnet-script"
1640
+ ]);
1513
1641
  var WORKLOAD_TYPES = /* @__PURE__ */ new Set(["function", "service", "job", "schedule"]);
1514
1642
  var VERSION_POLL_INTERVAL_MS = 2e3;
1515
1643
  var VERSION_POLL_TIMEOUT_MS = 5 * 60 * 1e3;
@@ -1580,9 +1708,17 @@ async function deployCommand(opts) {
1580
1708
  log(`Packaged ${pkg.fileCount} file(s), ${sizeKb} KB`);
1581
1709
  const detected = await detectRuntime(pkg.sourceDir);
1582
1710
  const runtime = opts.runtime ?? manifest.runtime ?? detected ?? "python";
1583
- if (!RUNTIMES.has(runtime)) {
1711
+ let allowedRuntimes = FALLBACK_RUNTIMES;
1712
+ try {
1713
+ const catalog = await listCoderunnerLanguages(cfg.apiUrl, ws);
1714
+ if (catalog.length > 0) {
1715
+ allowedRuntimes = new Set(catalog.map((item) => item.runtime));
1716
+ }
1717
+ } catch {
1718
+ }
1719
+ if (!allowedRuntimes.has(runtime)) {
1584
1720
  throw new Error(
1585
- `Invalid runtime "${runtime}" (expected: ${[...RUNTIMES].join(", ")}).`
1721
+ `Invalid runtime "${runtime}" (expected: ${[...allowedRuntimes].join(", ")}).`
1586
1722
  );
1587
1723
  }
1588
1724
  const workloadType = opts.workloadType ?? manifest.workloadType ?? manifest.workload_type ?? "function";
@@ -2102,6 +2238,10 @@ import path6 from "path";
2102
2238
  import AdmZip2 from "adm-zip";
2103
2239
  var REMOTE_ENTRY = "remoteEntry.js";
2104
2240
  var FLATTEN_SUFFIXES = [".js", ".mjs", ".cjs", ".css", ".map", ".wasm"];
2241
+ var IMPORT_ASSETS = /(__federation_import|import)\((['"])\.\/assets\//g;
2242
+ function rewriteRemoteEntryAssetPrefix(source) {
2243
+ return source.replace(IMPORT_ASSETS, "$1($2./");
2244
+ }
2105
2245
  function normalizeZipEntryName(filename) {
2106
2246
  return filename.replace(/\\/g, "/").replace(/^\/+/, "");
2107
2247
  }
@@ -2172,15 +2312,31 @@ function flattenFederatedReleaseZip(bytes) {
2172
2312
  }
2173
2313
  const plan = flattenFederatedBundlePlan(rels);
2174
2314
  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());
2315
+ if (keys.length > 0) {
2316
+ for (const dest of keys) {
2317
+ const src = plan[dest];
2318
+ const source = byNorm.get(src);
2319
+ if (!source) continue;
2320
+ zip.addFile(dest, source.getData());
2321
+ }
2181
2322
  }
2323
+ rewriteRemoteEntryInZip(zip);
2182
2324
  return zip.toBuffer();
2183
2325
  }
2326
+ function rewriteRemoteEntryInZip(zip) {
2327
+ const names = zip.getEntries().filter((entry) => !entry.isDirectory).map((entry) => normalizeZipEntryName(entry.entryName));
2328
+ const entryRel = chooseRemoteEntry(names);
2329
+ if (!entryRel) return;
2330
+ const zipEntry = zip.getEntries().find((item) => {
2331
+ if (item.isDirectory) return false;
2332
+ return normalizeZipEntryName(item.entryName) === entryRel;
2333
+ });
2334
+ if (!zipEntry) return;
2335
+ const before = zipEntry.getData().toString("utf8");
2336
+ const after = rewriteRemoteEntryAssetPrefix(before);
2337
+ if (after === before) return;
2338
+ zip.updateFile(zipEntry.entryName, Buffer.from(after, "utf8"));
2339
+ }
2184
2340
  function federatedReleaseZipHint(slug) {
2185
2341
  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
2342
  }
@@ -2195,7 +2351,7 @@ function initThenPublishHint(slug) {
2195
2351
  function extractReferencedSiblings(remoteEntrySource) {
2196
2352
  const names = /* @__PURE__ */ new Set();
2197
2353
  for (const match of remoteEntrySource.matchAll(
2198
- /["']\.\/([^"'/]+\.(?:js|mjs|cjs|css))["']/g
2354
+ /["']\.\/(?:assets\/)?([^"'/]+\.(?:js|mjs|cjs|css))["']/g
2199
2355
  )) {
2200
2356
  if (match[1] !== REMOTE_ENTRY) names.add(match[1]);
2201
2357
  }
@@ -2867,6 +3023,22 @@ program.command("login").description(
2867
3023
  program.command("logout").description("Clear the cached session").action(() => run(() => logoutCommand()));
2868
3024
  program.command("whoami").description("Show the current identity and verify the Control Plane session").action(() => run(() => whoamiCommand()));
2869
3025
  program.command("token").description("Print a valid (auto-refreshed) access token to stdout").action(() => run(() => tokenCommand()));
3026
+ var m2m = program.command("m2m").description("Machine tokens (client_id + secret \u2192 short Control Plane JWT)");
3027
+ m2m.command("token").description(
3028
+ "Exchange GROUNDFLOOR_M2M_CLIENT_ID + SECRET for a short CP JWT (not gf token)"
3029
+ ).option("-w, --workspace <id>", "Workspace id override").option("--client-id <id>", "M2M client_id (else GROUNDFLOOR_M2M_CLIENT_ID)").option(
3030
+ "--client-secret <secret>",
3031
+ "M2M secret (else GROUNDFLOOR_M2M_CLIENT_SECRET)"
3032
+ ).option("--json", "Output the full token response as JSON").action(
3033
+ (opts) => run(
3034
+ () => m2mTokenCommand({
3035
+ workspace: opts.workspace,
3036
+ clientId: opts.clientId,
3037
+ clientSecret: opts.clientSecret,
3038
+ json: opts.json
3039
+ })
3040
+ )
3041
+ );
2870
3042
  program.command("env").description('Print shell exports for `eval "$(gf env)"`').action(() => run(() => envCommand()));
2871
3043
  var environments = program.command("environments").alias("envs").description("List workspace environments");
2872
3044
  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)));
@@ -2909,6 +3081,17 @@ coderunner.command("run").description("Invoke a deployed function/job (smoke run
2909
3081
  })
2910
3082
  )
2911
3083
  );
3084
+ coderunner.command("vault-token").description(
3085
+ "Deprecated: use gf m2m token. D-067 Data Vault JWT for a bound Coderunner"
3086
+ ).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(
3087
+ (opts) => run(
3088
+ () => vaultTokenCommand({
3089
+ workspace: opts.workspace,
3090
+ coderunner: opts.coderunner,
3091
+ json: opts.json
3092
+ })
3093
+ )
3094
+ );
2912
3095
  var domains = program.command("domains").description("Manage custom domains (CNAME) for a coderunner service");
2913
3096
  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
3097
  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 +3173,7 @@ program.command("deploy").description(
2990
3173
  ).option("-w, --workspace <id>", "Workspace id override").option("--api-url <url>", "Control Plane base URL").option(
2991
3174
  "-c, --coderunner <id>",
2992
3175
  "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(
3176
+ ).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
3177
  "--workload-type <type>",
2995
3178
  "function | service | job | schedule"
2996
3179
  ).option(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groundfloorcloud/cli",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
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": {