@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.
- package/README.md +37 -8
- package/dist/index.js +650 -89
- 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
|
-
**
|
|
67
|
-
|
|
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>` | `
|
|
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,
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
|
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
|
|
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,
|
|
558
|
+
async function cpGet(apiUrl, path11) {
|
|
539
559
|
const token = await getValidAccessToken();
|
|
540
|
-
const res = await fetch(`${apiUrl}${
|
|
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,
|
|
571
|
+
async function cpPostJson(apiUrl, path11, body) {
|
|
552
572
|
const token = await getValidAccessToken();
|
|
553
|
-
const res = await fetch(`${apiUrl}${
|
|
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,
|
|
587
|
+
async function cpDelete(apiUrl, path11) {
|
|
568
588
|
const token = await getValidAccessToken();
|
|
569
|
-
const res = await fetch(`${apiUrl}${
|
|
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,
|
|
597
|
+
async function cpPutJson(apiUrl, path11, body) {
|
|
578
598
|
const token = await getValidAccessToken();
|
|
579
|
-
const res = await fetch(`${apiUrl}${
|
|
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,
|
|
613
|
+
async function cpPatchJson(apiUrl, path11, body) {
|
|
594
614
|
const token = await getValidAccessToken();
|
|
595
|
-
const res = await fetch(`${apiUrl}${
|
|
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,
|
|
629
|
+
async function cpPutBytes(apiUrl, path11, body, contentType) {
|
|
610
630
|
const token = await getValidAccessToken();
|
|
611
|
-
const res = await fetch(`${apiUrl}${
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
1618
|
+
const abs = path3.join(sourceDir, rel);
|
|
1350
1619
|
try {
|
|
1351
1620
|
const data = await fs2.readFile(abs);
|
|
1352
|
-
zip.addFile(rel.split(
|
|
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(
|
|
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 =
|
|
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
|
|
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 {
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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 ${
|
|
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
|
-
|
|
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: ${[...
|
|
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(
|
|
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) :
|
|
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(
|
|
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
|
-
|
|
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
|
|
2400
|
+
import path6 from "path";
|
|
2010
2401
|
function guessContentType(filename) {
|
|
2011
|
-
const ext =
|
|
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 =
|
|
2437
|
+
const resolved = path6.resolve(filePath);
|
|
2047
2438
|
const buf = await fs5.readFile(resolved);
|
|
2048
|
-
const name = opts.name ??
|
|
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 =
|
|
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
|
|
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
|
|
2140
|
-
const parent = parentDir(
|
|
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
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
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
|
|
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)
|
|
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
|
-
|
|
2616
|
+
names2.add(match[1]);
|
|
2206
2617
|
}
|
|
2207
|
-
return [...
|
|
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
|
|
2227
|
-
const present = new Set(
|
|
2228
|
-
const entryRel = chooseRemoteEntry(
|
|
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 =
|
|
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(
|
|
2691
|
+
return readJsonObject(path7.resolve(filePath));
|
|
2281
2692
|
}
|
|
2282
2693
|
async function findSiblingManifest(bundlePath) {
|
|
2283
|
-
const dir = await isDirectory(bundlePath) ? bundlePath :
|
|
2284
|
-
const candidate =
|
|
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 =
|
|
2443
|
-
const bundlePath = await isDirectory(inputPath) ?
|
|
2853
|
+
const inputPath = path7.resolve(opts.path ?? "release.zip");
|
|
2854
|
+
const bundlePath = await isDirectory(inputPath) ? path7.join(inputPath, "release.zip") : inputPath;
|
|
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 =
|
|
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 ${
|
|
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
|
|
2933
|
+
import path8 from "path";
|
|
2520
2934
|
import AdmZip4 from "adm-zip";
|
|
2521
2935
|
|
|
2522
2936
|
// src/version.ts
|
|
2523
|
-
|
|
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
|
-
|
|
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(
|
|
2983
|
+
candidates.push(path8.join(extracted, entry.name));
|
|
2561
2984
|
}
|
|
2562
2985
|
}
|
|
2563
2986
|
for (const candidate of candidates) {
|
|
2564
|
-
if (await pathExists(
|
|
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 =
|
|
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(
|
|
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 =
|
|
2671
|
-
const manifestPath =
|
|
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
|
|
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(
|
|
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
|
|
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>", "
|
|
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(
|
|
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:
|
|
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.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "Groundfloor gf CLI — sign in, deploy coderunners, publish Shell apps, and manage secrets, files, and Dataplane.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -28,17 +28,20 @@
|
|
|
28
28
|
"build": "tsup",
|
|
29
29
|
"dev": "tsup --watch",
|
|
30
30
|
"typecheck": "tsc --noEmit",
|
|
31
|
+
"test": "vitest run",
|
|
32
|
+
"test:watch": "vitest",
|
|
31
33
|
"clean": "rm -rf dist *.tsbuildinfo .turbo",
|
|
32
34
|
"prepublishOnly": "npm run build"
|
|
33
35
|
},
|
|
34
36
|
"dependencies": {
|
|
35
|
-
"adm-zip": "^0.
|
|
37
|
+
"adm-zip": "^0.6.0",
|
|
36
38
|
"commander": "^12.1.0",
|
|
37
39
|
"open": "^10.1.0"
|
|
38
40
|
},
|
|
39
41
|
"devDependencies": {
|
|
40
42
|
"@types/adm-zip": "^0.5.7",
|
|
41
43
|
"@types/node": "^20",
|
|
44
|
+
"esbuild": "^0.27.7",
|
|
42
45
|
"tsup": "^8.3.0",
|
|
43
46
|
"typescript": "^5"
|
|
44
47
|
},
|