@groundfloorcloud/cli 0.1.5 → 0.1.7
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 +13 -3
- package/dist/index.js +287 -31
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,7 +23,12 @@ Without a global install:
|
|
|
23
23
|
npx @groundfloorcloud/cli login
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
Requires Node.js 20 or later. `gf
|
|
26
|
+
Requires Node.js 20 or later. `gf` checks npm at most once a day and prints a
|
|
27
|
+
one-line upgrade notice when a newer `@groundfloorcloud/cli` exists. Unknown
|
|
28
|
+
subcommands include the installed vs latest version so a stale binary is not
|
|
29
|
+
read as wrong docs.
|
|
30
|
+
|
|
31
|
+
`gf login` signs in to production
|
|
27
32
|
(`https://platform.groundfloor.cloud`). Use `--dev` or `--stage` for
|
|
28
33
|
other cells; later commands reuse that session.
|
|
29
34
|
|
|
@@ -67,7 +72,7 @@ Authentication, mode `groundfloor` or `external`) until the realm is active.
|
|
|
67
72
|
|-------------------|-------------|---------------|
|
|
68
73
|
| `function` | Function | HTTP on `0.0.0.0:PORT`/8080. Node needs `scripts.start`. |
|
|
69
74
|
| `job` | Background job | Run-to-completion. Then `gf coderunner run`. |
|
|
70
|
-
| `schedule` | Scheduled job | Cron. No public URL. |
|
|
75
|
+
| `schedule` | Scheduled job | Cron via `--schedule` or `groundfloor.json` `schedule`. No public URL. |
|
|
71
76
|
| `service` | **Deployment** | Long-running HTTP. **Root `Dockerfile` is required.** |
|
|
72
77
|
|
|
73
78
|
```bash
|
|
@@ -76,6 +81,7 @@ gf deploy --git https://github.com/me/my-fn.git --ref main
|
|
|
76
81
|
gf deploy --name my-fn --runtime python --cpu 250m --memory 256Mi \
|
|
77
82
|
-e API_KEY=secret
|
|
78
83
|
gf deploy --workload-type service
|
|
84
|
+
gf deploy --workload-type schedule --schedule "*/5 * * * *"
|
|
79
85
|
gf deploy --app-id <app-uuid>
|
|
80
86
|
```
|
|
81
87
|
|
|
@@ -113,7 +119,8 @@ Then `gf deploy` the same tree. Do not zip `.env` files.
|
|
|
113
119
|
"workloadType": "function",
|
|
114
120
|
"cpu": "250m",
|
|
115
121
|
"memory": "256Mi",
|
|
116
|
-
"env": { "STAGE": "dev" }
|
|
122
|
+
"env": { "STAGE": "dev" },
|
|
123
|
+
"schedule": "*/5 * * * *"
|
|
117
124
|
}
|
|
118
125
|
```
|
|
119
126
|
|
|
@@ -127,6 +134,7 @@ Then `gf deploy` the same tree. Do not zip `.env` files.
|
|
|
127
134
|
| `--subdir <dir>` | Package a subdirectory of the source. |
|
|
128
135
|
| `--runtime <r>` | `python` \| `node` \| `dotnet-script`. |
|
|
129
136
|
| `--workload-type <t>` | `function` \| `service` (Deployment) \| `job` \| `schedule`. Service requires a Dockerfile. |
|
|
137
|
+
| `--schedule <cron>` | Cron for `schedule` workloads (or `groundfloor.json` `schedule`). |
|
|
130
138
|
| `--cpu` / `--memory` | Resource requests, e.g. `250m` / `256Mi`. |
|
|
131
139
|
| `-e, --env KEY=VALUE` | Deployment env var (repeatable). |
|
|
132
140
|
| `-m, --message <text>` | Version description. |
|
|
@@ -200,10 +208,12 @@ gf apps get <id|slug>
|
|
|
200
208
|
| `gf whoami` | Show identity and verify the Control Plane session. |
|
|
201
209
|
| `gf token` | Print a fresh access token. |
|
|
202
210
|
| `gf env` | Print `export …` lines for `eval "$(gf env)"`. |
|
|
211
|
+
| `gf environments ls` (`envs`) | List workspace environments. |
|
|
203
212
|
| `gf workspaces` (`ws`) | List workspaces; `gf workspaces use <id>` sets the default. |
|
|
204
213
|
| `gf coderunner ls` (`cr ls`) | List coderunners. |
|
|
205
214
|
| `gf coderunner status -c <id\|slug>` | Status and current deployment. |
|
|
206
215
|
| `gf coderunner logs -c <id\|slug>` | Process Log (stdout/stderr). Also printed automatically on `gf deploy` failure. |
|
|
216
|
+
| `gf coderunner cancel-deployment -c <id\|slug>` | Delete the current (or `--deployment-id`) deployment. |
|
|
207
217
|
| `gf coderunner run -c <id\|slug> [--payload '{}']` | Invoke a deployed function or job. |
|
|
208
218
|
| `gf deploy` | Package code and deploy as a coderunner. |
|
|
209
219
|
| `gf domains ls\|add\|rm\|verify -c <id\|slug>` | Custom domains for a service. |
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
+
import path9 from "path";
|
|
5
6
|
|
|
6
7
|
// src/auth/store.ts
|
|
7
8
|
import { promises as fs } from "fs";
|
|
@@ -62,6 +63,7 @@ async function updateConfig(patch) {
|
|
|
62
63
|
await writeConfig(next);
|
|
63
64
|
return next;
|
|
64
65
|
}
|
|
66
|
+
var paths = { CONFIG_DIR, AUTH_FILE, CONFIG_FILE };
|
|
65
67
|
|
|
66
68
|
// src/auth/config.ts
|
|
67
69
|
var CELLS = {
|
|
@@ -533,9 +535,9 @@ async function parseError(res) {
|
|
|
533
535
|
}
|
|
534
536
|
return `${res.status} ${detail}`;
|
|
535
537
|
}
|
|
536
|
-
async function cpGet(apiUrl,
|
|
538
|
+
async function cpGet(apiUrl, path10) {
|
|
537
539
|
const token = await getValidAccessToken();
|
|
538
|
-
const res = await fetch(`${apiUrl}${
|
|
540
|
+
const res = await fetch(`${apiUrl}${path10}`, {
|
|
539
541
|
headers: {
|
|
540
542
|
Authorization: `Bearer ${token}`,
|
|
541
543
|
Accept: "application/json"
|
|
@@ -546,9 +548,9 @@ async function cpGet(apiUrl, path8) {
|
|
|
546
548
|
}
|
|
547
549
|
return await res.json();
|
|
548
550
|
}
|
|
549
|
-
async function cpPostJson(apiUrl,
|
|
551
|
+
async function cpPostJson(apiUrl, path10, body) {
|
|
550
552
|
const token = await getValidAccessToken();
|
|
551
|
-
const res = await fetch(`${apiUrl}${
|
|
553
|
+
const res = await fetch(`${apiUrl}${path10}`, {
|
|
552
554
|
method: "POST",
|
|
553
555
|
headers: {
|
|
554
556
|
Authorization: `Bearer ${token}`,
|
|
@@ -562,9 +564,9 @@ async function cpPostJson(apiUrl, path8, body) {
|
|
|
562
564
|
}
|
|
563
565
|
return await res.json();
|
|
564
566
|
}
|
|
565
|
-
async function cpDelete(apiUrl,
|
|
567
|
+
async function cpDelete(apiUrl, path10) {
|
|
566
568
|
const token = await getValidAccessToken();
|
|
567
|
-
const res = await fetch(`${apiUrl}${
|
|
569
|
+
const res = await fetch(`${apiUrl}${path10}`, {
|
|
568
570
|
method: "DELETE",
|
|
569
571
|
headers: { Authorization: `Bearer ${token}`, Accept: "application/json" }
|
|
570
572
|
});
|
|
@@ -572,9 +574,9 @@ async function cpDelete(apiUrl, path8) {
|
|
|
572
574
|
throw new CpError(res.status, await parseError(res));
|
|
573
575
|
}
|
|
574
576
|
}
|
|
575
|
-
async function cpPutJson(apiUrl,
|
|
577
|
+
async function cpPutJson(apiUrl, path10, body) {
|
|
576
578
|
const token = await getValidAccessToken();
|
|
577
|
-
const res = await fetch(`${apiUrl}${
|
|
579
|
+
const res = await fetch(`${apiUrl}${path10}`, {
|
|
578
580
|
method: "PUT",
|
|
579
581
|
headers: {
|
|
580
582
|
Authorization: `Bearer ${token}`,
|
|
@@ -588,9 +590,9 @@ async function cpPutJson(apiUrl, path8, body) {
|
|
|
588
590
|
}
|
|
589
591
|
return await res.json();
|
|
590
592
|
}
|
|
591
|
-
async function cpPatchJson(apiUrl,
|
|
593
|
+
async function cpPatchJson(apiUrl, path10, body) {
|
|
592
594
|
const token = await getValidAccessToken();
|
|
593
|
-
const res = await fetch(`${apiUrl}${
|
|
595
|
+
const res = await fetch(`${apiUrl}${path10}`, {
|
|
594
596
|
method: "PATCH",
|
|
595
597
|
headers: {
|
|
596
598
|
Authorization: `Bearer ${token}`,
|
|
@@ -604,9 +606,9 @@ async function cpPatchJson(apiUrl, path8, body) {
|
|
|
604
606
|
}
|
|
605
607
|
return await res.json();
|
|
606
608
|
}
|
|
607
|
-
async function cpPutBytes(apiUrl,
|
|
609
|
+
async function cpPutBytes(apiUrl, path10, body, contentType) {
|
|
608
610
|
const token = await getValidAccessToken();
|
|
609
|
-
const res = await fetch(`${apiUrl}${
|
|
611
|
+
const res = await fetch(`${apiUrl}${path10}`, {
|
|
610
612
|
method: "PUT",
|
|
611
613
|
headers: {
|
|
612
614
|
Authorization: `Bearer ${token}`,
|
|
@@ -707,6 +709,22 @@ async function getDeployment(apiUrl, workspaceId, coderunnerId) {
|
|
|
707
709
|
}
|
|
708
710
|
return await res.json();
|
|
709
711
|
}
|
|
712
|
+
function deleteCoderunnerDeployment(apiUrl, workspaceId, coderunnerId, deploymentId) {
|
|
713
|
+
return cpDelete(
|
|
714
|
+
apiUrl,
|
|
715
|
+
`/v1/workspaces/${encodeURIComponent(
|
|
716
|
+
workspaceId
|
|
717
|
+
)}/coderunners/${encodeURIComponent(
|
|
718
|
+
coderunnerId
|
|
719
|
+
)}/deployments/${encodeURIComponent(deploymentId)}`
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
function listWorkspaceEnvironments(apiUrl, workspaceId) {
|
|
723
|
+
return cpGet(
|
|
724
|
+
apiUrl,
|
|
725
|
+
`/v1/workspaces/${encodeURIComponent(workspaceId)}/environments`
|
|
726
|
+
);
|
|
727
|
+
}
|
|
710
728
|
function getCoderunnerStatus(apiUrl, workspaceId, coderunnerId) {
|
|
711
729
|
return cpGet(
|
|
712
730
|
apiUrl,
|
|
@@ -1015,6 +1033,79 @@ async function coderunnerListCommand(opts) {
|
|
|
1015
1033
|
}
|
|
1016
1034
|
}
|
|
1017
1035
|
|
|
1036
|
+
// src/resolve-coderunner.ts
|
|
1037
|
+
async function resolveCoderunnerId(apiUrl, workspaceId, idOrSlug) {
|
|
1038
|
+
const list = await listCoderunners(apiUrl, workspaceId);
|
|
1039
|
+
const match = list.find((c) => c.id === idOrSlug || c.slug === idOrSlug);
|
|
1040
|
+
if (!match) {
|
|
1041
|
+
throw new Error(`Coderunner not found: ${idOrSlug}`);
|
|
1042
|
+
}
|
|
1043
|
+
return match.id;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// src/commands/cancel-deployment.ts
|
|
1047
|
+
async function cancelDeploymentCommand(opts) {
|
|
1048
|
+
const cfg = await resolveConfig({ workspaceId: opts.workspace });
|
|
1049
|
+
if (!cfg.workspaceId) {
|
|
1050
|
+
throw new Error(
|
|
1051
|
+
"No workspace selected. Run `gf workspaces use <id>` or pass --workspace <id>."
|
|
1052
|
+
);
|
|
1053
|
+
}
|
|
1054
|
+
const coderunnerId = await resolveCoderunnerId(
|
|
1055
|
+
cfg.apiUrl,
|
|
1056
|
+
cfg.workspaceId,
|
|
1057
|
+
opts.coderunner
|
|
1058
|
+
);
|
|
1059
|
+
let deploymentId = opts.deploymentId?.trim();
|
|
1060
|
+
if (!deploymentId) {
|
|
1061
|
+
const current = await getDeployment(cfg.apiUrl, cfg.workspaceId, coderunnerId);
|
|
1062
|
+
deploymentId = current?.id?.trim();
|
|
1063
|
+
}
|
|
1064
|
+
if (!deploymentId) {
|
|
1065
|
+
throw new Error(
|
|
1066
|
+
`No current deployment for coderunner ${coderunnerId}. Pass --deployment-id.`
|
|
1067
|
+
);
|
|
1068
|
+
}
|
|
1069
|
+
await deleteCoderunnerDeployment(
|
|
1070
|
+
cfg.apiUrl,
|
|
1071
|
+
cfg.workspaceId,
|
|
1072
|
+
coderunnerId,
|
|
1073
|
+
deploymentId
|
|
1074
|
+
);
|
|
1075
|
+
process.stdout.write(
|
|
1076
|
+
`Deleted deployment ${deploymentId} for coderunner ${coderunnerId}
|
|
1077
|
+
`
|
|
1078
|
+
);
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// src/commands/environments.ts
|
|
1082
|
+
async function environmentsListCommand(opts) {
|
|
1083
|
+
const cfg = await resolveConfig({ workspaceId: opts.workspace });
|
|
1084
|
+
if (!cfg.workspaceId) {
|
|
1085
|
+
throw new Error(
|
|
1086
|
+
"No workspace selected. Run `gf workspaces use <id>` or pass --workspace <id>."
|
|
1087
|
+
);
|
|
1088
|
+
}
|
|
1089
|
+
const environments2 = await listWorkspaceEnvironments(
|
|
1090
|
+
cfg.apiUrl,
|
|
1091
|
+
cfg.workspaceId
|
|
1092
|
+
);
|
|
1093
|
+
if (opts.json) {
|
|
1094
|
+
process.stdout.write(`${JSON.stringify(environments2, null, 2)}
|
|
1095
|
+
`);
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
if (environments2.length === 0) {
|
|
1099
|
+
process.stdout.write("No environments in this workspace.\n");
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
for (const env of environments2) {
|
|
1103
|
+
const status = env.status ?? "";
|
|
1104
|
+
process.stdout.write(`${env.id} ${env.name} [${status}]
|
|
1105
|
+
`);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1018
1109
|
// src/process-log.ts
|
|
1019
1110
|
var PROCESS_STATUS_LABELS = {
|
|
1020
1111
|
1: "Pending",
|
|
@@ -1128,16 +1219,6 @@ async function diagnoseDeployFailure(opts) {
|
|
|
1128
1219
|
});
|
|
1129
1220
|
}
|
|
1130
1221
|
|
|
1131
|
-
// src/resolve-coderunner.ts
|
|
1132
|
-
async function resolveCoderunnerId(apiUrl, workspaceId, idOrSlug) {
|
|
1133
|
-
const list = await listCoderunners(apiUrl, workspaceId);
|
|
1134
|
-
const match = list.find((c) => c.id === idOrSlug || c.slug === idOrSlug);
|
|
1135
|
-
if (!match) {
|
|
1136
|
-
throw new Error(`Coderunner not found: ${idOrSlug}`);
|
|
1137
|
-
}
|
|
1138
|
-
return match.id;
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
1222
|
// src/commands/logs.ts
|
|
1142
1223
|
async function logsCommand(opts) {
|
|
1143
1224
|
const cfg = await resolveConfig({ workspaceId: opts.workspace });
|
|
@@ -1403,6 +1484,30 @@ async function preflightNodePackage(sourceDir) {
|
|
|
1403
1484
|
}
|
|
1404
1485
|
}
|
|
1405
1486
|
|
|
1487
|
+
// src/deploy/schedule.ts
|
|
1488
|
+
var SCHEDULE_REQUIRED = 'workload type "schedule" requires --schedule "<cron>" or groundfloor.json "schedule".';
|
|
1489
|
+
var SCHEDULE_WRONG_TYPE = '--schedule / groundfloor.json "schedule" is only valid with --workload-type schedule.';
|
|
1490
|
+
function resolveDeploySchedule(opts) {
|
|
1491
|
+
const cron = (opts.flag ?? opts.manifestSchedule)?.trim() || void 0;
|
|
1492
|
+
if (opts.workloadType === "schedule") {
|
|
1493
|
+
if (!cron) {
|
|
1494
|
+
throw new Error(SCHEDULE_REQUIRED);
|
|
1495
|
+
}
|
|
1496
|
+
return cron;
|
|
1497
|
+
}
|
|
1498
|
+
if (cron) {
|
|
1499
|
+
throw new Error(SCHEDULE_WRONG_TYPE);
|
|
1500
|
+
}
|
|
1501
|
+
return void 0;
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
// src/deploy/target.ts
|
|
1505
|
+
function formatDeployTarget(input) {
|
|
1506
|
+
const label = input.workspaceName?.trim() ? `${input.workspaceName.trim()} (${input.workspaceId})` : input.workspaceId;
|
|
1507
|
+
const cell = input.cell ?? "unknown";
|
|
1508
|
+
return `Deploy target: workspace=${label} cell=${cell} api=${input.apiUrl}`;
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1406
1511
|
// src/commands/deploy.ts
|
|
1407
1512
|
var RUNTIMES = /* @__PURE__ */ new Set(["python", "node", "dotnet-script"]);
|
|
1408
1513
|
var WORKLOAD_TYPES = /* @__PURE__ */ new Set(["function", "service", "job", "schedule"]);
|
|
@@ -1446,6 +1551,20 @@ async function deployCommand(opts) {
|
|
|
1446
1551
|
);
|
|
1447
1552
|
}
|
|
1448
1553
|
const ws = cfg.workspaceId;
|
|
1554
|
+
let workspaceName;
|
|
1555
|
+
try {
|
|
1556
|
+
const workspaces2 = await listWorkspaces(cfg.apiUrl);
|
|
1557
|
+
workspaceName = workspaces2.find((item) => item.id === ws)?.name;
|
|
1558
|
+
} catch {
|
|
1559
|
+
}
|
|
1560
|
+
log(
|
|
1561
|
+
formatDeployTarget({
|
|
1562
|
+
workspaceId: ws,
|
|
1563
|
+
workspaceName,
|
|
1564
|
+
cell: cfg.environment,
|
|
1565
|
+
apiUrl: cfg.apiUrl
|
|
1566
|
+
})
|
|
1567
|
+
);
|
|
1449
1568
|
log(
|
|
1450
1569
|
opts.git ? `Cloning ${opts.git}${opts.ref ? ` (${opts.ref})` : ""}...` : `Packaging ${path4.resolve(opts.path ?? process.cwd())}...`
|
|
1451
1570
|
);
|
|
@@ -1484,6 +1603,11 @@ async function deployCommand(opts) {
|
|
|
1484
1603
|
if (runtime === "node") {
|
|
1485
1604
|
await preflightNodePackage(pkg.sourceDir);
|
|
1486
1605
|
}
|
|
1606
|
+
const schedule = resolveDeploySchedule({
|
|
1607
|
+
workloadType,
|
|
1608
|
+
flag: opts.schedule,
|
|
1609
|
+
manifestSchedule: manifest.schedule
|
|
1610
|
+
});
|
|
1487
1611
|
let coderunnerId = opts.coderunner;
|
|
1488
1612
|
if (!coderunnerId) {
|
|
1489
1613
|
const name = opts.name ?? manifest.name ?? (opts.git ? nameFromGitUrl(opts.git) : path4.basename(path4.resolve(opts.path ?? process.cwd())));
|
|
@@ -1585,7 +1709,8 @@ async function deployCommand(opts) {
|
|
|
1585
1709
|
version_id: uploaded.version_id,
|
|
1586
1710
|
cpu: opts.cpu ?? manifest.cpu,
|
|
1587
1711
|
memory: opts.memory ?? manifest.memory,
|
|
1588
|
-
envVariable: Object.keys(envVariable).length ? envVariable : void 0
|
|
1712
|
+
envVariable: Object.keys(envVariable).length ? envVariable : void 0,
|
|
1713
|
+
schedule
|
|
1589
1714
|
});
|
|
1590
1715
|
log("Waiting for deployment...");
|
|
1591
1716
|
const deployDeadline = Date.now() + DEPLOY_POLL_TIMEOUT_MS;
|
|
@@ -2004,21 +2129,21 @@ function chooseRemoteEntry(relpaths) {
|
|
|
2004
2129
|
})[0];
|
|
2005
2130
|
}
|
|
2006
2131
|
function flattenFederatedBundlePlan(relpaths) {
|
|
2007
|
-
const
|
|
2008
|
-
const pathset = new Set(
|
|
2132
|
+
const paths2 = relpaths.map(normalizeZipEntryName).filter((name) => name && !name.endsWith("/") && !name.split("/").includes(".."));
|
|
2133
|
+
const pathset = new Set(paths2);
|
|
2009
2134
|
const entry = chooseRemoteEntry([...pathset]);
|
|
2010
2135
|
if (!entry) return {};
|
|
2011
2136
|
const promoteDirs = /* @__PURE__ */ new Set();
|
|
2012
2137
|
const entryDir = parentDir(entry);
|
|
2013
2138
|
if (entryDir) promoteDirs.add(entryDir);
|
|
2014
|
-
for (const
|
|
2015
|
-
const parent = parentDir(
|
|
2139
|
+
for (const path10 of pathset) {
|
|
2140
|
+
const parent = parentDir(path10);
|
|
2016
2141
|
if (parent === "assets" || parent.endsWith("/assets") && parent.split("/").length - 1 < 2) {
|
|
2017
2142
|
promoteDirs.add(parent);
|
|
2018
2143
|
}
|
|
2019
2144
|
}
|
|
2020
2145
|
const destToSrc = {};
|
|
2021
|
-
for (const src of
|
|
2146
|
+
for (const src of paths2) {
|
|
2022
2147
|
const parent = parentDir(src);
|
|
2023
2148
|
if (!promoteDirs.has(parent)) continue;
|
|
2024
2149
|
const dest = basename(src);
|
|
@@ -2393,6 +2518,12 @@ import { promises as fs7 } from "fs";
|
|
|
2393
2518
|
import os3 from "os";
|
|
2394
2519
|
import path7 from "path";
|
|
2395
2520
|
import AdmZip4 from "adm-zip";
|
|
2521
|
+
|
|
2522
|
+
// src/version.ts
|
|
2523
|
+
var GF_VERSION = "0.1.7";
|
|
2524
|
+
var GF_NPM_PACKAGE = "@groundfloorcloud/cli";
|
|
2525
|
+
|
|
2526
|
+
// src/commands/apps-init.ts
|
|
2396
2527
|
var MAX_KIT_BYTES = 8 * 1024 * 1024;
|
|
2397
2528
|
function slugify3(input) {
|
|
2398
2529
|
const slug = input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
@@ -2465,7 +2596,7 @@ async function downloadKit(url) {
|
|
|
2465
2596
|
res = await fetch(url, {
|
|
2466
2597
|
redirect: "follow",
|
|
2467
2598
|
signal: AbortSignal.timeout(6e4),
|
|
2468
|
-
headers: { "User-Agent":
|
|
2599
|
+
headers: { "User-Agent": `groundfloor-cli/${GF_VERSION}` }
|
|
2469
2600
|
});
|
|
2470
2601
|
} catch (err) {
|
|
2471
2602
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -2567,6 +2698,9 @@ async function appsInitCommand(opts) {
|
|
|
2567
2698
|
process.stdout.write(
|
|
2568
2699
|
"Do not wrap src/App.tsx in BrowserRouter (DevShell already has one).\n"
|
|
2569
2700
|
);
|
|
2701
|
+
process.stdout.write(
|
|
2702
|
+
"MCP: .mcp.json is included (npx @groundfloorcloud/mcp). Cursor on macOS: copy .cursor/mcp.json.example.\n"
|
|
2703
|
+
);
|
|
2570
2704
|
process.stdout.write("Next:\n");
|
|
2571
2705
|
for (const step of result.next) {
|
|
2572
2706
|
process.stdout.write(` ${step}
|
|
@@ -2609,6 +2743,81 @@ async function dataplaneProvisionCommand(opts) {
|
|
|
2609
2743
|
}
|
|
2610
2744
|
}
|
|
2611
2745
|
|
|
2746
|
+
// src/update-check.ts
|
|
2747
|
+
import { promises as fs8 } from "fs";
|
|
2748
|
+
import path8 from "path";
|
|
2749
|
+
var UPDATE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
2750
|
+
function isNewer(latest, current) {
|
|
2751
|
+
const a = latest.split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
2752
|
+
const b = current.split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
2753
|
+
const len = Math.max(a.length, b.length);
|
|
2754
|
+
for (let i = 0; i < len; i += 1) {
|
|
2755
|
+
const lv = a[i] ?? 0;
|
|
2756
|
+
const cv = b[i] ?? 0;
|
|
2757
|
+
if (lv > cv) return true;
|
|
2758
|
+
if (lv < cv) return false;
|
|
2759
|
+
}
|
|
2760
|
+
return false;
|
|
2761
|
+
}
|
|
2762
|
+
function upgradeNotice(current, latest) {
|
|
2763
|
+
return `gf ${latest} is available (you have ${current}). npm i -g ${GF_NPM_PACKAGE}@latest`;
|
|
2764
|
+
}
|
|
2765
|
+
function unknownCommandNotice(command, current, latest) {
|
|
2766
|
+
if (latest && isNewer(latest, current)) {
|
|
2767
|
+
return `unknown command '${command}' \u2014 your gf is ${current}, latest is ${latest}. npm i -g ${GF_NPM_PACKAGE}@latest`;
|
|
2768
|
+
}
|
|
2769
|
+
return `unknown command '${command}' \u2014 your gf is ${current}`;
|
|
2770
|
+
}
|
|
2771
|
+
function parseUnknownCommandName(message) {
|
|
2772
|
+
const match = message.match(/unknown command ['"]([^'"]+)['"]/i);
|
|
2773
|
+
return match?.[1] ?? "that";
|
|
2774
|
+
}
|
|
2775
|
+
async function fetchNpmLatest(pkg = GF_NPM_PACKAGE) {
|
|
2776
|
+
try {
|
|
2777
|
+
const res = await fetch(`https://registry.npmjs.org/${pkg}/latest`, {
|
|
2778
|
+
headers: { Accept: "application/json" },
|
|
2779
|
+
signal: AbortSignal.timeout(1500)
|
|
2780
|
+
});
|
|
2781
|
+
if (!res.ok) return null;
|
|
2782
|
+
const body = await res.json();
|
|
2783
|
+
return typeof body.version === "string" && body.version.trim() ? body.version.trim() : null;
|
|
2784
|
+
} catch {
|
|
2785
|
+
return null;
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2788
|
+
async function readUpdateCache(file) {
|
|
2789
|
+
try {
|
|
2790
|
+
const raw = await fs8.readFile(file, "utf8");
|
|
2791
|
+
const parsed = JSON.parse(raw);
|
|
2792
|
+
if (typeof parsed.checkedAt !== "number" || typeof parsed.latest !== "string" || !parsed.latest.trim()) {
|
|
2793
|
+
return null;
|
|
2794
|
+
}
|
|
2795
|
+
return parsed;
|
|
2796
|
+
} catch {
|
|
2797
|
+
return null;
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
2800
|
+
async function writeUpdateCache(file, cache) {
|
|
2801
|
+
await fs8.mkdir(path8.dirname(file), { recursive: true, mode: 448 });
|
|
2802
|
+
await fs8.writeFile(file, `${JSON.stringify(cache)}
|
|
2803
|
+
`, { mode: 384 });
|
|
2804
|
+
}
|
|
2805
|
+
async function resolveLatest(opts) {
|
|
2806
|
+
const now = opts.now ?? Date.now();
|
|
2807
|
+
const ttl = opts.ttlMs ?? UPDATE_TTL_MS;
|
|
2808
|
+
const cached = await readUpdateCache(opts.cacheFile);
|
|
2809
|
+
if (cached && now - cached.checkedAt < ttl) {
|
|
2810
|
+
return cached.latest;
|
|
2811
|
+
}
|
|
2812
|
+
const fetchLatest = opts.fetchLatest ?? fetchNpmLatest;
|
|
2813
|
+
const latest = await fetchLatest();
|
|
2814
|
+
if (latest) {
|
|
2815
|
+
await writeUpdateCache(opts.cacheFile, { checkedAt: now, latest });
|
|
2816
|
+
return latest;
|
|
2817
|
+
}
|
|
2818
|
+
return cached?.latest ?? null;
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2612
2821
|
// src/index.ts
|
|
2613
2822
|
function collect(value, previous) {
|
|
2614
2823
|
return previous.concat([value]);
|
|
@@ -2624,7 +2833,7 @@ function run(fn) {
|
|
|
2624
2833
|
var program = new Command();
|
|
2625
2834
|
program.name("gf").description(
|
|
2626
2835
|
"Groundfloor CLI \u2014 sign in, deploy coderunners, publish Shell apps, and manage workspace resources."
|
|
2627
|
-
).version(
|
|
2836
|
+
).version(GF_VERSION);
|
|
2628
2837
|
program.command("login").description(
|
|
2629
2838
|
"Log in via the browser (production by default; --dev / --stage for other cells)"
|
|
2630
2839
|
).option("--dev", "Sign in to Groundfloor dev").option("--stage", "Sign in to Groundfloor stage").option(
|
|
@@ -2641,6 +2850,8 @@ program.command("logout").description("Clear the cached session").action(() => r
|
|
|
2641
2850
|
program.command("whoami").description("Show the current identity and verify the Control Plane session").action(() => run(() => whoamiCommand()));
|
|
2642
2851
|
program.command("token").description("Print a valid (auto-refreshed) access token to stdout").action(() => run(() => tokenCommand()));
|
|
2643
2852
|
program.command("env").description('Print shell exports for `eval "$(gf env)"`').action(() => run(() => envCommand()));
|
|
2853
|
+
var environments = program.command("environments").alias("envs").description("List workspace environments");
|
|
2854
|
+
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)));
|
|
2644
2855
|
var workspaces = program.command("workspaces").alias("ws").description("List workspaces or set the default workspace");
|
|
2645
2856
|
workspaces.command("list", { isDefault: true }).description("List workspaces visible to your user").option("--json", "Output raw JSON").action((opts) => run(() => workspacesListCommand(opts)));
|
|
2646
2857
|
workspaces.command("use <workspaceId>").description("Set the default workspace").action(
|
|
@@ -2649,6 +2860,15 @@ workspaces.command("use <workspaceId>").description("Set the default workspace")
|
|
|
2649
2860
|
var coderunner = program.command("coderunner").alias("cr").description("Coderunner commands");
|
|
2650
2861
|
coderunner.command("ls").alias("list").description("List coderunners in the default (or --workspace) workspace").option("-w, --workspace <id>", "Workspace id override").option("--json", "Output raw JSON").action((opts) => run(() => coderunnerListCommand(opts)));
|
|
2651
2862
|
coderunner.command("status").description("Show coderunner status + current deployment JSON").requiredOption("-c, --coderunner <id|slug>", "Coderunner id or slug").option("-w, --workspace <id>", "Workspace id override").option("--json", "Output raw JSON").action((opts) => run(() => statusCommand(opts)));
|
|
2863
|
+
coderunner.command("cancel-deployment").description("Delete the current (or --deployment-id) deployment").requiredOption("-c, --coderunner <id|slug>", "Coderunner id or slug").option("-w, --workspace <id>", "Workspace id override").option("--deployment-id <id>", "Deployment id (default: current deployment)").action(
|
|
2864
|
+
(opts) => run(
|
|
2865
|
+
() => cancelDeploymentCommand({
|
|
2866
|
+
workspace: opts.workspace,
|
|
2867
|
+
coderunner: opts.coderunner,
|
|
2868
|
+
deploymentId: opts.deploymentId
|
|
2869
|
+
})
|
|
2870
|
+
)
|
|
2871
|
+
);
|
|
2652
2872
|
coderunner.command("logs").description("Show Process Log lines for a coderunner (stdout/stderr)").requiredOption("-c, --coderunner <id|slug>", "Coderunner id or slug").option("-w, --workspace <id>", "Workspace id override").option("--limit <n>", "Max lines (default 50)").option("--since-seconds <n>", "Lookback window in seconds (default 3600)").option("--search <text>", "Substring filter").option("--json", "Output raw JSON").action(
|
|
2653
2873
|
(opts) => run(
|
|
2654
2874
|
() => logsCommand({
|
|
@@ -2755,11 +2975,47 @@ program.command("deploy").description(
|
|
|
2755
2975
|
).option("-n, --name <name>", "Coderunner name (default: folder or repo name)").option("-p, --path <dir>", "Local directory to package (default: cwd)").option("--git <url>", "Clone and deploy a remote git/bitbucket repo").option("--ref <ref>", "Branch/tag/commit for --git").option("--subdir <dir>", "Subdirectory within the source to package").option("--runtime <runtime>", "python | node | dotnet-script").option(
|
|
2756
2976
|
"--workload-type <type>",
|
|
2757
2977
|
"function | service | job | schedule"
|
|
2978
|
+
).option(
|
|
2979
|
+
"--schedule <cron>",
|
|
2980
|
+
'Cron for --workload-type schedule (or groundfloor.json "schedule")'
|
|
2758
2981
|
).option(
|
|
2759
2982
|
"--app-id <id>",
|
|
2760
2983
|
"Bind newly created coderunner as a helper of this App (optional)"
|
|
2761
2984
|
).option("--cpu <cpu>", "CPU request, e.g. 250m").option("--memory <memory>", "Memory request, e.g. 256Mi").option("-e, --env <KEY=VALUE>", "Environment variable (repeatable)", collect, []).option("-m, --message <text>", "Version description").option("--skip-deploy", "Upload a new version but do not deploy it").action((opts) => run(() => deployCommand(opts)));
|
|
2762
|
-
|
|
2985
|
+
function wireUnknownCommands(root, current, latest) {
|
|
2986
|
+
const walk = (cmd) => {
|
|
2987
|
+
if (cmd.commands.length === 0) return;
|
|
2988
|
+
cmd.exitOverride((err) => {
|
|
2989
|
+
if (err.code === "commander.unknownCommand") {
|
|
2990
|
+
process.stderr.write(
|
|
2991
|
+
`${unknownCommandNotice(parseUnknownCommandName(err.message), current, latest)}
|
|
2992
|
+
`
|
|
2993
|
+
);
|
|
2994
|
+
process.exit(1);
|
|
2995
|
+
}
|
|
2996
|
+
throw err;
|
|
2997
|
+
});
|
|
2998
|
+
for (const child of cmd.commands) walk(child);
|
|
2999
|
+
};
|
|
3000
|
+
walk(root);
|
|
3001
|
+
}
|
|
3002
|
+
async function notifyUpgrade() {
|
|
3003
|
+
if (process.env.GF_SKIP_UPDATE_CHECK === "1") return null;
|
|
3004
|
+
const latest = await resolveLatest({
|
|
3005
|
+
cacheFile: path9.join(paths.CONFIG_DIR, "update-check.json")
|
|
3006
|
+
});
|
|
3007
|
+
if (latest && isNewer(latest, GF_VERSION)) {
|
|
3008
|
+
process.stderr.write(`${upgradeNotice(GF_VERSION, latest)}
|
|
3009
|
+
`);
|
|
3010
|
+
}
|
|
3011
|
+
return latest;
|
|
3012
|
+
}
|
|
3013
|
+
async function main() {
|
|
3014
|
+
const latest = await notifyUpgrade();
|
|
3015
|
+
wireUnknownCommands(program, GF_VERSION, latest);
|
|
3016
|
+
await program.parseAsync(process.argv);
|
|
3017
|
+
}
|
|
3018
|
+
main().catch((err) => {
|
|
2763
3019
|
const message = err instanceof Error ? err.message : String(err);
|
|
2764
3020
|
process.stderr.write(`Error: ${message}
|
|
2765
3021
|
`);
|
package/package.json
CHANGED