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