@groundfloorcloud/cli 0.1.4 → 0.1.6
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 +11 -3
- package/dist/index.js +486 -109
- 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
|
|
|
@@ -65,7 +70,7 @@ Authentication, mode `groundfloor` or `external`) until the realm is active.
|
|
|
65
70
|
|
|
66
71
|
| `--workload-type` | Also called | Package notes |
|
|
67
72
|
|-------------------|-------------|---------------|
|
|
68
|
-
| `function` | Function | HTTP on `PORT`/8080. Node needs `scripts.start`. |
|
|
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
75
|
| `schedule` | Scheduled job | Cron. No public URL. |
|
|
71
76
|
| `service` | **Deployment** | Long-running HTTP. **Root `Dockerfile` is required.** |
|
|
@@ -88,7 +93,9 @@ How the target is chosen:
|
|
|
88
93
|
|
|
89
94
|
Runtime is auto-detected when omitted (`package.json` → node,
|
|
90
95
|
`requirements.txt` / `pyproject.toml` → python), defaulting to python.
|
|
91
|
-
Node
|
|
96
|
+
Node without a Dockerfile uses the platform bootstrap (`CMD ["npm","start"]`,
|
|
97
|
+
port 8080). Add `"scripts": { "start": "node index.js" }` and bind
|
|
98
|
+
`0.0.0.0`. `gf deploy` fails immediately if `scripts.start` is missing.
|
|
92
99
|
|
|
93
100
|
### Local with live data
|
|
94
101
|
|
|
@@ -201,6 +208,7 @@ gf apps get <id|slug>
|
|
|
201
208
|
| `gf workspaces` (`ws`) | List workspaces; `gf workspaces use <id>` sets the default. |
|
|
202
209
|
| `gf coderunner ls` (`cr ls`) | List coderunners. |
|
|
203
210
|
| `gf coderunner status -c <id\|slug>` | Status and current deployment. |
|
|
211
|
+
| `gf coderunner logs -c <id\|slug>` | Process Log (stdout/stderr). Also printed automatically on `gf deploy` failure. |
|
|
204
212
|
| `gf coderunner run -c <id\|slug> [--payload '{}']` | Invoke a deployed function or job. |
|
|
205
213
|
| `gf deploy` | Package code and deploy as a coderunner. |
|
|
206
214
|
| `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, path7) {
|
|
|
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, path7, 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, path7) {
|
|
|
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, path7, 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, path7, 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}`,
|
|
@@ -724,6 +726,18 @@ function runCoderunner(apiUrl, workspaceId, coderunnerId, payload) {
|
|
|
724
726
|
payload
|
|
725
727
|
);
|
|
726
728
|
}
|
|
729
|
+
function getProcessLogs(apiUrl, workspaceId, query = {}) {
|
|
730
|
+
const params = new URLSearchParams();
|
|
731
|
+
for (const [key, value] of Object.entries(query)) {
|
|
732
|
+
if (value == null || value === "") continue;
|
|
733
|
+
params.set(key, String(value));
|
|
734
|
+
}
|
|
735
|
+
const qs = params.toString();
|
|
736
|
+
return cpGet(
|
|
737
|
+
apiUrl,
|
|
738
|
+
`/v1/workspaces/${encodeURIComponent(workspaceId)}/logs${qs ? `?${qs}` : ""}`
|
|
739
|
+
);
|
|
740
|
+
}
|
|
727
741
|
function domainsBase(workspaceId, coderunnerId) {
|
|
728
742
|
return `/v1/workspaces/${encodeURIComponent(
|
|
729
743
|
workspaceId
|
|
@@ -1003,9 +1017,188 @@ async function coderunnerListCommand(opts) {
|
|
|
1003
1017
|
}
|
|
1004
1018
|
}
|
|
1005
1019
|
|
|
1020
|
+
// src/process-log.ts
|
|
1021
|
+
var PROCESS_STATUS_LABELS = {
|
|
1022
|
+
1: "Pending",
|
|
1023
|
+
2: "Processing",
|
|
1024
|
+
3: "Completed",
|
|
1025
|
+
4: "Failed",
|
|
1026
|
+
5: "Deleting",
|
|
1027
|
+
6: "Deleted",
|
|
1028
|
+
7: "Stopped"
|
|
1029
|
+
};
|
|
1030
|
+
function statusLabel(status) {
|
|
1031
|
+
if (status == null) return "unknown";
|
|
1032
|
+
const name = PROCESS_STATUS_LABELS[status];
|
|
1033
|
+
return name ? `${name} (${status})` : `status=${status}`;
|
|
1034
|
+
}
|
|
1035
|
+
function customerPortalOrigin(apiUrl) {
|
|
1036
|
+
try {
|
|
1037
|
+
const url = new URL(apiUrl);
|
|
1038
|
+
url.host = url.host.replace(/^platform\./, "app.");
|
|
1039
|
+
return url.origin;
|
|
1040
|
+
} catch {
|
|
1041
|
+
return "https://app.groundfloor.cloud";
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
function processLogPortalUrl(portalOrigin, workspaceId, runnerId) {
|
|
1045
|
+
const params = new URLSearchParams({
|
|
1046
|
+
workspace_id: workspaceId,
|
|
1047
|
+
runner_id: runnerId
|
|
1048
|
+
});
|
|
1049
|
+
return `${portalOrigin.replace(/\/+$/, "")}/process-log?${params.toString()}`;
|
|
1050
|
+
}
|
|
1051
|
+
function formatLogPreview(entries, limit = 20) {
|
|
1052
|
+
const slice = entries.slice(0, limit);
|
|
1053
|
+
return [...slice].reverse().map((entry) => {
|
|
1054
|
+
const ts = entry.ts?.trim() ? `${entry.ts} ` : "";
|
|
1055
|
+
return `${ts}${entry.line}`;
|
|
1056
|
+
}).join("\n");
|
|
1057
|
+
}
|
|
1058
|
+
function formatDeployFailure(input) {
|
|
1059
|
+
const lines = [input.headline, ""];
|
|
1060
|
+
lines.push(`coderunner: ${input.coderunnerId}`);
|
|
1061
|
+
if (input.versionId) lines.push(`version: ${input.versionId}`);
|
|
1062
|
+
if (input.deploymentId) lines.push(`deployment: ${input.deploymentId}`);
|
|
1063
|
+
if (input.status != null) lines.push(`status: ${statusLabel(input.status)}`);
|
|
1064
|
+
if (input.logLines) {
|
|
1065
|
+
lines.push("", "logs:", input.logLines);
|
|
1066
|
+
} else {
|
|
1067
|
+
lines.push("", "logs: (none yet)");
|
|
1068
|
+
}
|
|
1069
|
+
if (input.portalUrl) {
|
|
1070
|
+
lines.push("", `portal: ${input.portalUrl}`);
|
|
1071
|
+
}
|
|
1072
|
+
lines.push(`more logs: gf coderunner logs -c ${input.coderunnerId}`);
|
|
1073
|
+
return lines.join("\n");
|
|
1074
|
+
}
|
|
1075
|
+
async function fetchFailureLogEntries(apiUrl, workspaceId, ids) {
|
|
1076
|
+
const attempts = [];
|
|
1077
|
+
if (ids.deploymentId) attempts.push({ deployment_id: ids.deploymentId });
|
|
1078
|
+
attempts.push({ runner_id: ids.coderunnerId });
|
|
1079
|
+
if (ids.versionId && ids.versionId !== ids.coderunnerId) {
|
|
1080
|
+
attempts.push({ runner_id: ids.versionId });
|
|
1081
|
+
}
|
|
1082
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1083
|
+
let lastError;
|
|
1084
|
+
let anyOk = false;
|
|
1085
|
+
for (const query of attempts) {
|
|
1086
|
+
const key = JSON.stringify(query);
|
|
1087
|
+
if (seen.has(key)) continue;
|
|
1088
|
+
seen.add(key);
|
|
1089
|
+
try {
|
|
1090
|
+
const page = await getProcessLogs(apiUrl, workspaceId, {
|
|
1091
|
+
...query,
|
|
1092
|
+
limit: 20,
|
|
1093
|
+
since_seconds: 3600
|
|
1094
|
+
});
|
|
1095
|
+
anyOk = true;
|
|
1096
|
+
if (page.entries?.length) return page.entries;
|
|
1097
|
+
} catch (err) {
|
|
1098
|
+
lastError = err;
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
if (!anyOk && lastError) throw lastError;
|
|
1102
|
+
return [];
|
|
1103
|
+
}
|
|
1104
|
+
async function diagnoseDeployFailure(opts) {
|
|
1105
|
+
let logLines = "";
|
|
1106
|
+
try {
|
|
1107
|
+
const entries = await fetchFailureLogEntries(opts.apiUrl, opts.workspaceId, {
|
|
1108
|
+
coderunnerId: opts.coderunnerId,
|
|
1109
|
+
versionId: opts.versionId ?? void 0,
|
|
1110
|
+
deploymentId: opts.deploymentId ?? void 0
|
|
1111
|
+
});
|
|
1112
|
+
logLines = formatLogPreview(entries);
|
|
1113
|
+
} catch (err) {
|
|
1114
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1115
|
+
logLines = `(could not fetch Process Log: ${message})`;
|
|
1116
|
+
}
|
|
1117
|
+
const runnerForPortal = opts.deploymentId || opts.coderunnerId;
|
|
1118
|
+
return formatDeployFailure({
|
|
1119
|
+
headline: opts.headline,
|
|
1120
|
+
coderunnerId: opts.coderunnerId,
|
|
1121
|
+
versionId: opts.versionId,
|
|
1122
|
+
deploymentId: opts.deploymentId,
|
|
1123
|
+
status: opts.status,
|
|
1124
|
+
logLines,
|
|
1125
|
+
portalUrl: processLogPortalUrl(
|
|
1126
|
+
customerPortalOrigin(opts.apiUrl),
|
|
1127
|
+
opts.workspaceId,
|
|
1128
|
+
runnerForPortal
|
|
1129
|
+
)
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
// src/resolve-coderunner.ts
|
|
1134
|
+
async function resolveCoderunnerId(apiUrl, workspaceId, idOrSlug) {
|
|
1135
|
+
const list = await listCoderunners(apiUrl, workspaceId);
|
|
1136
|
+
const match = list.find((c) => c.id === idOrSlug || c.slug === idOrSlug);
|
|
1137
|
+
if (!match) {
|
|
1138
|
+
throw new Error(`Coderunner not found: ${idOrSlug}`);
|
|
1139
|
+
}
|
|
1140
|
+
return match.id;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// src/commands/logs.ts
|
|
1144
|
+
async function logsCommand(opts) {
|
|
1145
|
+
const cfg = await resolveConfig({ workspaceId: opts.workspace });
|
|
1146
|
+
if (!cfg.workspaceId) {
|
|
1147
|
+
throw new Error(
|
|
1148
|
+
"No workspace selected. Run `gf workspaces use <id>` or pass --workspace <id>."
|
|
1149
|
+
);
|
|
1150
|
+
}
|
|
1151
|
+
const coderunnerId = await resolveCoderunnerId(
|
|
1152
|
+
cfg.apiUrl,
|
|
1153
|
+
cfg.workspaceId,
|
|
1154
|
+
opts.coderunner
|
|
1155
|
+
);
|
|
1156
|
+
const limit = opts.limit ? Number(opts.limit) : 50;
|
|
1157
|
+
const sinceSeconds = opts.sinceSeconds ? Number(opts.sinceSeconds) : 3600;
|
|
1158
|
+
if (!Number.isFinite(limit) || limit < 1) {
|
|
1159
|
+
throw new Error("Invalid --limit (expected a positive number).");
|
|
1160
|
+
}
|
|
1161
|
+
if (!Number.isFinite(sinceSeconds) || sinceSeconds < 10) {
|
|
1162
|
+
throw new Error("Invalid --since-seconds (minimum 10).");
|
|
1163
|
+
}
|
|
1164
|
+
const page = await getProcessLogs(cfg.apiUrl, cfg.workspaceId, {
|
|
1165
|
+
runner_id: coderunnerId,
|
|
1166
|
+
limit,
|
|
1167
|
+
since_seconds: sinceSeconds,
|
|
1168
|
+
search: opts.search
|
|
1169
|
+
});
|
|
1170
|
+
if (opts.json) {
|
|
1171
|
+
process.stdout.write(`${JSON.stringify(page, null, 2)}
|
|
1172
|
+
`);
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
const preview = formatLogPreview(page.entries ?? [], limit);
|
|
1176
|
+
if (!preview) {
|
|
1177
|
+
process.stdout.write("No log entries.\n");
|
|
1178
|
+
} else {
|
|
1179
|
+
process.stdout.write(`${preview}
|
|
1180
|
+
`);
|
|
1181
|
+
}
|
|
1182
|
+
if (page.has_more) {
|
|
1183
|
+
process.stdout.write(
|
|
1184
|
+
`(more available \u2014 raise --limit or narrow --since-seconds)
|
|
1185
|
+
`
|
|
1186
|
+
);
|
|
1187
|
+
}
|
|
1188
|
+
process.stdout.write(
|
|
1189
|
+
`
|
|
1190
|
+
portal: ${processLogPortalUrl(
|
|
1191
|
+
customerPortalOrigin(cfg.apiUrl),
|
|
1192
|
+
cfg.workspaceId,
|
|
1193
|
+
coderunnerId
|
|
1194
|
+
)}
|
|
1195
|
+
`
|
|
1196
|
+
);
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1006
1199
|
// src/commands/deploy.ts
|
|
1007
|
-
import { promises as
|
|
1008
|
-
import
|
|
1200
|
+
import { promises as fs4 } from "fs";
|
|
1201
|
+
import path4 from "path";
|
|
1009
1202
|
|
|
1010
1203
|
// src/deploy/package.ts
|
|
1011
1204
|
import { execFile } from "child_process";
|
|
@@ -1173,6 +1366,45 @@ async function detectRuntime(dir) {
|
|
|
1173
1366
|
return null;
|
|
1174
1367
|
}
|
|
1175
1368
|
|
|
1369
|
+
// src/deploy/preflight.ts
|
|
1370
|
+
import { promises as fs3 } from "fs";
|
|
1371
|
+
import path3 from "path";
|
|
1372
|
+
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).';
|
|
1373
|
+
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).';
|
|
1374
|
+
var NODE_DOCKERFILE_NPM_START_MISSING = 'Dockerfile runs `npm start` but package.json has no "scripts.start". Add "scripts": { "start": "node <entrypoint>" }.';
|
|
1375
|
+
function dockerfileRunsNpmStart(text) {
|
|
1376
|
+
return /npm["'\s,]+start/.test(text);
|
|
1377
|
+
}
|
|
1378
|
+
async function readOptional(filePath) {
|
|
1379
|
+
try {
|
|
1380
|
+
return await fs3.readFile(filePath, "utf8");
|
|
1381
|
+
} catch {
|
|
1382
|
+
return null;
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
async function preflightNodePackage(sourceDir) {
|
|
1386
|
+
const dockerfile = await readOptional(path3.join(sourceDir, "Dockerfile"));
|
|
1387
|
+
const needsStartScript = dockerfile == null || dockerfileRunsNpmStart(dockerfile);
|
|
1388
|
+
if (!needsStartScript) return;
|
|
1389
|
+
const raw = await readOptional(path3.join(sourceDir, "package.json"));
|
|
1390
|
+
if (raw == null) {
|
|
1391
|
+
throw new Error(
|
|
1392
|
+
dockerfile == null ? NODE_BOOTSTRAP_MISSING_PACKAGE : NODE_DOCKERFILE_NPM_START_MISSING
|
|
1393
|
+
);
|
|
1394
|
+
}
|
|
1395
|
+
let npm;
|
|
1396
|
+
try {
|
|
1397
|
+
npm = JSON.parse(raw);
|
|
1398
|
+
} catch {
|
|
1399
|
+
throw new Error("package.json is not valid JSON.");
|
|
1400
|
+
}
|
|
1401
|
+
if (!npm.scripts?.start?.trim()) {
|
|
1402
|
+
throw new Error(
|
|
1403
|
+
dockerfile == null ? NODE_BOOTSTRAP_MISSING_START : NODE_DOCKERFILE_NPM_START_MISSING
|
|
1404
|
+
);
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1176
1408
|
// src/commands/deploy.ts
|
|
1177
1409
|
var RUNTIMES = /* @__PURE__ */ new Set(["python", "node", "dotnet-script"]);
|
|
1178
1410
|
var WORKLOAD_TYPES = /* @__PURE__ */ new Set(["function", "service", "job", "schedule"]);
|
|
@@ -1202,6 +1434,9 @@ function parseEnvPairs(pairs) {
|
|
|
1202
1434
|
}
|
|
1203
1435
|
var log = (msg) => process.stderr.write(`${msg}
|
|
1204
1436
|
`);
|
|
1437
|
+
async function failDeploy(opts) {
|
|
1438
|
+
throw new Error(await diagnoseDeployFailure(opts));
|
|
1439
|
+
}
|
|
1205
1440
|
async function deployCommand(opts) {
|
|
1206
1441
|
const cfg = await resolveConfig({
|
|
1207
1442
|
apiUrl: opts.apiUrl,
|
|
@@ -1214,7 +1449,7 @@ async function deployCommand(opts) {
|
|
|
1214
1449
|
}
|
|
1215
1450
|
const ws = cfg.workspaceId;
|
|
1216
1451
|
log(
|
|
1217
|
-
opts.git ? `Cloning ${opts.git}${opts.ref ? ` (${opts.ref})` : ""}...` : `Packaging ${
|
|
1452
|
+
opts.git ? `Cloning ${opts.git}${opts.ref ? ` (${opts.ref})` : ""}...` : `Packaging ${path4.resolve(opts.path ?? process.cwd())}...`
|
|
1218
1453
|
);
|
|
1219
1454
|
const pkg = await packageSource({
|
|
1220
1455
|
path: opts.path,
|
|
@@ -1241,7 +1476,7 @@ async function deployCommand(opts) {
|
|
|
1241
1476
|
}
|
|
1242
1477
|
if (workloadType === "service") {
|
|
1243
1478
|
try {
|
|
1244
|
-
await
|
|
1479
|
+
await fs4.access(path4.join(pkg.sourceDir, "Dockerfile"));
|
|
1245
1480
|
} catch {
|
|
1246
1481
|
throw new Error(
|
|
1247
1482
|
'workload type "service" (also called Deployment) requires a Dockerfile at the project root.'
|
|
@@ -1249,26 +1484,11 @@ async function deployCommand(opts) {
|
|
|
1249
1484
|
}
|
|
1250
1485
|
}
|
|
1251
1486
|
if (runtime === "node") {
|
|
1252
|
-
|
|
1253
|
-
const raw = await fs3.readFile(
|
|
1254
|
-
path3.join(pkg.sourceDir, "package.json"),
|
|
1255
|
-
"utf8"
|
|
1256
|
-
);
|
|
1257
|
-
const npm = JSON.parse(raw);
|
|
1258
|
-
if (!npm.scripts?.start?.trim()) {
|
|
1259
|
-
throw new Error(
|
|
1260
|
-
'Node Coderunner packages require package.json "scripts.start" (runtime CMD is npm start).'
|
|
1261
|
-
);
|
|
1262
|
-
}
|
|
1263
|
-
} catch (err) {
|
|
1264
|
-
if (err instanceof Error && err.message.includes("scripts.start")) {
|
|
1265
|
-
throw err;
|
|
1266
|
-
}
|
|
1267
|
-
}
|
|
1487
|
+
await preflightNodePackage(pkg.sourceDir);
|
|
1268
1488
|
}
|
|
1269
1489
|
let coderunnerId = opts.coderunner;
|
|
1270
1490
|
if (!coderunnerId) {
|
|
1271
|
-
const name = opts.name ?? manifest.name ?? (opts.git ? nameFromGitUrl(opts.git) :
|
|
1491
|
+
const name = opts.name ?? manifest.name ?? (opts.git ? nameFromGitUrl(opts.git) : path4.basename(path4.resolve(opts.path ?? process.cwd())));
|
|
1272
1492
|
const slug = manifest.slug ?? slugify(name);
|
|
1273
1493
|
const existing = await listCoderunners(cfg.apiUrl, ws);
|
|
1274
1494
|
const match = existing.find((c) => c.slug === slug || c.name === name);
|
|
@@ -1317,16 +1537,38 @@ async function deployCommand(opts) {
|
|
|
1317
1537
|
break;
|
|
1318
1538
|
}
|
|
1319
1539
|
if (status === 4) {
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1540
|
+
return await failDeploy({
|
|
1541
|
+
apiUrl: cfg.apiUrl,
|
|
1542
|
+
workspaceId: ws,
|
|
1543
|
+
headline: `Build failed for version ${uploaded.version_id} (${statusLabel(4)}).`,
|
|
1544
|
+
coderunnerId,
|
|
1545
|
+
versionId: uploaded.version_id,
|
|
1546
|
+
status: 4
|
|
1547
|
+
});
|
|
1323
1548
|
}
|
|
1324
1549
|
process.stderr.write(".");
|
|
1325
1550
|
await new Promise((r) => setTimeout(r, VERSION_POLL_INTERVAL_MS));
|
|
1326
1551
|
}
|
|
1327
1552
|
process.stderr.write("\n");
|
|
1328
1553
|
if (!built) {
|
|
1329
|
-
|
|
1554
|
+
let lastStatus = null;
|
|
1555
|
+
try {
|
|
1556
|
+
lastStatus = (await getVersion(
|
|
1557
|
+
cfg.apiUrl,
|
|
1558
|
+
ws,
|
|
1559
|
+
coderunnerId,
|
|
1560
|
+
uploaded.version_id
|
|
1561
|
+
)).status ?? null;
|
|
1562
|
+
} catch {
|
|
1563
|
+
}
|
|
1564
|
+
return await failDeploy({
|
|
1565
|
+
apiUrl: cfg.apiUrl,
|
|
1566
|
+
workspaceId: ws,
|
|
1567
|
+
headline: "Timed out waiting for the build to complete.",
|
|
1568
|
+
coderunnerId,
|
|
1569
|
+
versionId: uploaded.version_id,
|
|
1570
|
+
status: lastStatus
|
|
1571
|
+
});
|
|
1330
1572
|
}
|
|
1331
1573
|
log("Build completed.");
|
|
1332
1574
|
if (opts.skipDeploy) {
|
|
@@ -1358,16 +1600,38 @@ async function deployCommand(opts) {
|
|
|
1358
1600
|
break;
|
|
1359
1601
|
}
|
|
1360
1602
|
if (dStatus === 4) {
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1603
|
+
return await failDeploy({
|
|
1604
|
+
apiUrl: cfg.apiUrl,
|
|
1605
|
+
workspaceId: ws,
|
|
1606
|
+
headline: `Deployment failed for coderunner ${coderunnerId} (${statusLabel(4)}).`,
|
|
1607
|
+
coderunnerId,
|
|
1608
|
+
versionId: uploaded.version_id,
|
|
1609
|
+
deploymentId: result.deployment_id ?? deployment?.id,
|
|
1610
|
+
status: 4
|
|
1611
|
+
});
|
|
1364
1612
|
}
|
|
1365
1613
|
process.stderr.write(".");
|
|
1366
1614
|
await new Promise((r) => setTimeout(r, DEPLOY_POLL_INTERVAL_MS));
|
|
1367
1615
|
}
|
|
1368
1616
|
process.stderr.write("\n");
|
|
1369
1617
|
if (!deployOk) {
|
|
1370
|
-
|
|
1618
|
+
let lastStatus = null;
|
|
1619
|
+
let deploymentId = result.deployment_id;
|
|
1620
|
+
try {
|
|
1621
|
+
const latest = await getDeployment(cfg.apiUrl, ws, coderunnerId);
|
|
1622
|
+
lastStatus = latest?.status ?? null;
|
|
1623
|
+
deploymentId = latest?.id ?? deploymentId;
|
|
1624
|
+
} catch {
|
|
1625
|
+
}
|
|
1626
|
+
return await failDeploy({
|
|
1627
|
+
apiUrl: cfg.apiUrl,
|
|
1628
|
+
workspaceId: ws,
|
|
1629
|
+
headline: "Timed out waiting for the deployment to complete.",
|
|
1630
|
+
coderunnerId,
|
|
1631
|
+
versionId: uploaded.version_id,
|
|
1632
|
+
deploymentId,
|
|
1633
|
+
status: lastStatus
|
|
1634
|
+
});
|
|
1371
1635
|
}
|
|
1372
1636
|
log("Deployment completed.");
|
|
1373
1637
|
process.stdout.write(`
|
|
@@ -1388,21 +1652,13 @@ Deployed coderunner ${coderunnerId}
|
|
|
1388
1652
|
}
|
|
1389
1653
|
} finally {
|
|
1390
1654
|
if (pkg.cleanup) {
|
|
1391
|
-
await
|
|
1655
|
+
await fs4.rm(pkg.cleanup, { recursive: true, force: true }).catch(() => {
|
|
1392
1656
|
});
|
|
1393
1657
|
}
|
|
1394
1658
|
}
|
|
1395
1659
|
}
|
|
1396
1660
|
|
|
1397
1661
|
// src/commands/run.ts
|
|
1398
|
-
async function resolveCoderunnerId(apiUrl, workspaceId, idOrSlug) {
|
|
1399
|
-
const list = await listCoderunners(apiUrl, workspaceId);
|
|
1400
|
-
const match = list.find((c) => c.id === idOrSlug || c.slug === idOrSlug);
|
|
1401
|
-
if (!match) {
|
|
1402
|
-
throw new Error(`Coderunner not found: ${idOrSlug}`);
|
|
1403
|
-
}
|
|
1404
|
-
return match.id;
|
|
1405
|
-
}
|
|
1406
1662
|
async function runCommand(opts) {
|
|
1407
1663
|
const cfg = await resolveConfig({ workspaceId: opts.workspace });
|
|
1408
1664
|
if (!cfg.workspaceId) {
|
|
@@ -1436,14 +1692,6 @@ async function runCommand(opts) {
|
|
|
1436
1692
|
}
|
|
1437
1693
|
|
|
1438
1694
|
// src/commands/status.ts
|
|
1439
|
-
async function resolveCoderunnerId2(apiUrl, workspaceId, idOrSlug) {
|
|
1440
|
-
const list = await listCoderunners(apiUrl, workspaceId);
|
|
1441
|
-
const match = list.find((c) => c.id === idOrSlug || c.slug === idOrSlug);
|
|
1442
|
-
if (!match) {
|
|
1443
|
-
throw new Error(`Coderunner not found: ${idOrSlug}`);
|
|
1444
|
-
}
|
|
1445
|
-
return match.id;
|
|
1446
|
-
}
|
|
1447
1695
|
async function statusCommand(opts) {
|
|
1448
1696
|
const cfg = await resolveConfig({ workspaceId: opts.workspace });
|
|
1449
1697
|
if (!cfg.workspaceId) {
|
|
@@ -1451,7 +1699,7 @@ async function statusCommand(opts) {
|
|
|
1451
1699
|
"No workspace selected. Run `gf workspaces use <id>` or pass --workspace <id>."
|
|
1452
1700
|
);
|
|
1453
1701
|
}
|
|
1454
|
-
const coderunnerId = await
|
|
1702
|
+
const coderunnerId = await resolveCoderunnerId(
|
|
1455
1703
|
cfg.apiUrl,
|
|
1456
1704
|
cfg.workspaceId,
|
|
1457
1705
|
opts.coderunner
|
|
@@ -1634,10 +1882,10 @@ async function secretsRmCommand(key, opts) {
|
|
|
1634
1882
|
}
|
|
1635
1883
|
|
|
1636
1884
|
// src/commands/files.ts
|
|
1637
|
-
import { promises as
|
|
1638
|
-
import
|
|
1885
|
+
import { promises as fs5 } from "fs";
|
|
1886
|
+
import path5 from "path";
|
|
1639
1887
|
function guessContentType(filename) {
|
|
1640
|
-
const ext =
|
|
1888
|
+
const ext = path5.extname(filename).toLowerCase();
|
|
1641
1889
|
const types = {
|
|
1642
1890
|
".json": "application/json",
|
|
1643
1891
|
".txt": "text/plain",
|
|
@@ -1672,9 +1920,9 @@ async function filesListCommand(opts) {
|
|
|
1672
1920
|
}
|
|
1673
1921
|
async function filesUploadCommand(filePath, opts) {
|
|
1674
1922
|
const cfg = await requireWorkspace(opts.workspace);
|
|
1675
|
-
const resolved =
|
|
1676
|
-
const buf = await
|
|
1677
|
-
const name = opts.name ??
|
|
1923
|
+
const resolved = path5.resolve(filePath);
|
|
1924
|
+
const buf = await fs5.readFile(resolved);
|
|
1925
|
+
const name = opts.name ?? path5.basename(resolved);
|
|
1678
1926
|
const contentType = guessContentType(name);
|
|
1679
1927
|
const init = await initFileUpload(cfg.apiUrl, cfg.workspaceId, {
|
|
1680
1928
|
name,
|
|
@@ -1711,8 +1959,8 @@ async function filesDownloadCommand(fileId, opts) {
|
|
|
1711
1959
|
throw new Error(`Download failed: ${res.status} ${res.statusText}`);
|
|
1712
1960
|
}
|
|
1713
1961
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
1714
|
-
const dest =
|
|
1715
|
-
await
|
|
1962
|
+
const dest = path5.resolve(opts.out ?? fileId);
|
|
1963
|
+
await fs5.writeFile(dest, buf);
|
|
1716
1964
|
process.stdout.write(`Wrote ${dest}
|
|
1717
1965
|
`);
|
|
1718
1966
|
}
|
|
@@ -1724,8 +1972,8 @@ async function filesRmCommand(fileId, opts) {
|
|
|
1724
1972
|
}
|
|
1725
1973
|
|
|
1726
1974
|
// src/commands/apps.ts
|
|
1727
|
-
import { promises as
|
|
1728
|
-
import
|
|
1975
|
+
import { promises as fs6 } from "fs";
|
|
1976
|
+
import path6 from "path";
|
|
1729
1977
|
|
|
1730
1978
|
// src/release-zip.ts
|
|
1731
1979
|
import AdmZip2 from "adm-zip";
|
|
@@ -1758,21 +2006,21 @@ function chooseRemoteEntry(relpaths) {
|
|
|
1758
2006
|
})[0];
|
|
1759
2007
|
}
|
|
1760
2008
|
function flattenFederatedBundlePlan(relpaths) {
|
|
1761
|
-
const
|
|
1762
|
-
const pathset = new Set(
|
|
2009
|
+
const paths2 = relpaths.map(normalizeZipEntryName).filter((name) => name && !name.endsWith("/") && !name.split("/").includes(".."));
|
|
2010
|
+
const pathset = new Set(paths2);
|
|
1763
2011
|
const entry = chooseRemoteEntry([...pathset]);
|
|
1764
2012
|
if (!entry) return {};
|
|
1765
2013
|
const promoteDirs = /* @__PURE__ */ new Set();
|
|
1766
2014
|
const entryDir = parentDir(entry);
|
|
1767
2015
|
if (entryDir) promoteDirs.add(entryDir);
|
|
1768
|
-
for (const
|
|
1769
|
-
const parent = parentDir(
|
|
2016
|
+
for (const path10 of pathset) {
|
|
2017
|
+
const parent = parentDir(path10);
|
|
1770
2018
|
if (parent === "assets" || parent.endsWith("/assets") && parent.split("/").length - 1 < 2) {
|
|
1771
2019
|
promoteDirs.add(parent);
|
|
1772
2020
|
}
|
|
1773
2021
|
}
|
|
1774
2022
|
const destToSrc = {};
|
|
1775
|
-
for (const src of
|
|
2023
|
+
for (const src of paths2) {
|
|
1776
2024
|
const parent = parentDir(src);
|
|
1777
2025
|
if (!promoteDirs.has(parent)) continue;
|
|
1778
2026
|
const dest = basename(src);
|
|
@@ -1897,7 +2145,7 @@ function formatApp(app) {
|
|
|
1897
2145
|
return `${app.id} ${app.name ?? app.slug ?? ""} ${app.app_kind ?? ""} [${app.status ?? ""}]`;
|
|
1898
2146
|
}
|
|
1899
2147
|
async function readJsonObject(filePath) {
|
|
1900
|
-
const raw = await
|
|
2148
|
+
const raw = await fs6.readFile(filePath, "utf8");
|
|
1901
2149
|
const parsed = JSON.parse(raw);
|
|
1902
2150
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1903
2151
|
throw new Error(`${filePath} must contain a JSON object`);
|
|
@@ -1906,13 +2154,13 @@ async function readJsonObject(filePath) {
|
|
|
1906
2154
|
}
|
|
1907
2155
|
async function maybeReadManifest(filePath) {
|
|
1908
2156
|
if (!filePath) return void 0;
|
|
1909
|
-
return readJsonObject(
|
|
2157
|
+
return readJsonObject(path6.resolve(filePath));
|
|
1910
2158
|
}
|
|
1911
2159
|
async function findSiblingManifest(bundlePath) {
|
|
1912
|
-
const dir = await isDirectory(bundlePath) ? bundlePath :
|
|
1913
|
-
const candidate =
|
|
2160
|
+
const dir = await isDirectory(bundlePath) ? bundlePath : path6.dirname(bundlePath);
|
|
2161
|
+
const candidate = path6.join(dir, "groundfloor.manifest.json");
|
|
1914
2162
|
try {
|
|
1915
|
-
await
|
|
2163
|
+
await fs6.access(candidate);
|
|
1916
2164
|
} catch {
|
|
1917
2165
|
return void 0;
|
|
1918
2166
|
}
|
|
@@ -1948,7 +2196,7 @@ Prefer: ${federatedReleaseZipHint(slug)}
|
|
|
1948
2196
|
}
|
|
1949
2197
|
async function isDirectory(filePath) {
|
|
1950
2198
|
try {
|
|
1951
|
-
return (await
|
|
2199
|
+
return (await fs6.stat(filePath)).isDirectory();
|
|
1952
2200
|
} catch {
|
|
1953
2201
|
return false;
|
|
1954
2202
|
}
|
|
@@ -2068,11 +2316,11 @@ Do not scaffold src/App.tsx from scratch \u2014 BrowserRouter in App.tsx crashes
|
|
|
2068
2316
|
}
|
|
2069
2317
|
async function appsPublishCommand(appRef, opts) {
|
|
2070
2318
|
const cfg = await requireWorkspace(opts.workspace);
|
|
2071
|
-
const inputPath =
|
|
2072
|
-
const bundlePath = await isDirectory(inputPath) ?
|
|
2319
|
+
const inputPath = path6.resolve(opts.path ?? "release.zip");
|
|
2320
|
+
const bundlePath = await isDirectory(inputPath) ? path6.join(inputPath, "release.zip") : inputPath;
|
|
2073
2321
|
let bytes;
|
|
2074
2322
|
try {
|
|
2075
|
-
bytes = await
|
|
2323
|
+
bytes = await fs6.readFile(bundlePath);
|
|
2076
2324
|
} catch {
|
|
2077
2325
|
throw new Error(
|
|
2078
2326
|
`No bundle at ${bundlePath}. ${initThenPublishHint()}`
|
|
@@ -2099,11 +2347,11 @@ async function appsPublishCommand(appRef, opts) {
|
|
|
2099
2347
|
`Releases are only for shell_federated apps (this app is ${app.app_kind}). Use \`gf deploy\` for coderunner workloads.`
|
|
2100
2348
|
);
|
|
2101
2349
|
}
|
|
2102
|
-
const ext =
|
|
2350
|
+
const ext = path6.extname(bundlePath).toLowerCase();
|
|
2103
2351
|
const isZip = ext === ".zip";
|
|
2104
2352
|
if (!isZip) {
|
|
2105
2353
|
throw new Error(
|
|
2106
|
-
`Publish release.zip from \`npm run release\`, not a lone ${
|
|
2354
|
+
`Publish release.zip from \`npm run release\`, not a lone ${path6.basename(bundlePath)}. ` + initThenPublishHint(app.slug)
|
|
2107
2355
|
);
|
|
2108
2356
|
}
|
|
2109
2357
|
bytes = prepareReleaseZip(bytes, app.slug);
|
|
@@ -2143,10 +2391,16 @@ async function appsPublishCommand(appRef, opts) {
|
|
|
2143
2391
|
}
|
|
2144
2392
|
|
|
2145
2393
|
// src/commands/apps-init.ts
|
|
2146
|
-
import { promises as
|
|
2394
|
+
import { promises as fs7 } from "fs";
|
|
2147
2395
|
import os3 from "os";
|
|
2148
|
-
import
|
|
2396
|
+
import path7 from "path";
|
|
2149
2397
|
import AdmZip4 from "adm-zip";
|
|
2398
|
+
|
|
2399
|
+
// src/version.ts
|
|
2400
|
+
var GF_VERSION = "0.1.6";
|
|
2401
|
+
var GF_NPM_PACKAGE = "@groundfloorcloud/cli";
|
|
2402
|
+
|
|
2403
|
+
// src/commands/apps-init.ts
|
|
2150
2404
|
var MAX_KIT_BYTES = 8 * 1024 * 1024;
|
|
2151
2405
|
function slugify3(input) {
|
|
2152
2406
|
const slug = input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
@@ -2157,7 +2411,7 @@ function titleFromSlug(slug) {
|
|
|
2157
2411
|
}
|
|
2158
2412
|
async function pathExists(filePath) {
|
|
2159
2413
|
try {
|
|
2160
|
-
await
|
|
2414
|
+
await fs7.access(filePath);
|
|
2161
2415
|
return true;
|
|
2162
2416
|
} catch {
|
|
2163
2417
|
return false;
|
|
@@ -2165,7 +2419,7 @@ async function pathExists(filePath) {
|
|
|
2165
2419
|
}
|
|
2166
2420
|
async function isEmptyDir(dir) {
|
|
2167
2421
|
try {
|
|
2168
|
-
const entries = await
|
|
2422
|
+
const entries = await fs7.readdir(dir);
|
|
2169
2423
|
return entries.length === 0;
|
|
2170
2424
|
} catch (err) {
|
|
2171
2425
|
if (err.code === "ENOENT") return true;
|
|
@@ -2174,17 +2428,17 @@ async function isEmptyDir(dir) {
|
|
|
2174
2428
|
}
|
|
2175
2429
|
async function findKitRoot(extracted) {
|
|
2176
2430
|
const candidates = [
|
|
2177
|
-
|
|
2431
|
+
path7.join(extracted, "shell-starter-kit"),
|
|
2178
2432
|
extracted
|
|
2179
2433
|
];
|
|
2180
|
-
const entries = await
|
|
2434
|
+
const entries = await fs7.readdir(extracted, { withFileTypes: true });
|
|
2181
2435
|
for (const entry of entries) {
|
|
2182
2436
|
if (entry.isDirectory()) {
|
|
2183
|
-
candidates.push(
|
|
2437
|
+
candidates.push(path7.join(extracted, entry.name));
|
|
2184
2438
|
}
|
|
2185
2439
|
}
|
|
2186
2440
|
for (const candidate of candidates) {
|
|
2187
|
-
if (await pathExists(
|
|
2441
|
+
if (await pathExists(path7.join(candidate, "groundfloor.manifest.json"))) {
|
|
2188
2442
|
return candidate;
|
|
2189
2443
|
}
|
|
2190
2444
|
}
|
|
@@ -2219,7 +2473,7 @@ async function downloadKit(url) {
|
|
|
2219
2473
|
res = await fetch(url, {
|
|
2220
2474
|
redirect: "follow",
|
|
2221
2475
|
signal: AbortSignal.timeout(6e4),
|
|
2222
|
-
headers: { "User-Agent":
|
|
2476
|
+
headers: { "User-Agent": `groundfloor-cli/${GF_VERSION}` }
|
|
2223
2477
|
});
|
|
2224
2478
|
} catch (err) {
|
|
2225
2479
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -2266,7 +2520,7 @@ async function appsInitCommand(opts) {
|
|
|
2266
2520
|
`);
|
|
2267
2521
|
}
|
|
2268
2522
|
const name = opts.name?.trim() || titleFromSlug(slug);
|
|
2269
|
-
const dest =
|
|
2523
|
+
const dest = path7.resolve(opts.dir?.trim() || path7.join(process.cwd(), slug));
|
|
2270
2524
|
const cfg = await resolveConfig();
|
|
2271
2525
|
const cell = cfg.environment ?? "production";
|
|
2272
2526
|
const kitUrl = opts.kitUrl?.trim() || starterKitZipUrl(cell);
|
|
@@ -2276,26 +2530,26 @@ async function appsInitCommand(opts) {
|
|
|
2276
2530
|
);
|
|
2277
2531
|
}
|
|
2278
2532
|
if (opts.force && await pathExists(dest)) {
|
|
2279
|
-
await
|
|
2533
|
+
await fs7.rm(dest, { recursive: true, force: true });
|
|
2280
2534
|
}
|
|
2281
2535
|
const bytes = await downloadKit(kitUrl);
|
|
2282
2536
|
const zip = new AdmZip4(bytes);
|
|
2283
2537
|
assertSafeZipEntries(zip);
|
|
2284
|
-
const tmp = await
|
|
2538
|
+
const tmp = await fs7.mkdtemp(path7.join(os3.tmpdir(), "gf-starter-kit-"));
|
|
2285
2539
|
try {
|
|
2286
2540
|
zip.extractAllTo(tmp, true);
|
|
2287
2541
|
const kitRoot = await findKitRoot(tmp);
|
|
2288
|
-
await
|
|
2289
|
-
await
|
|
2542
|
+
await fs7.mkdir(dest, { recursive: true });
|
|
2543
|
+
await fs7.cp(kitRoot, dest, { recursive: true, force: true });
|
|
2290
2544
|
} finally {
|
|
2291
|
-
await
|
|
2292
|
-
}
|
|
2293
|
-
const identityPath =
|
|
2294
|
-
const manifestPath =
|
|
2295
|
-
const identity = await
|
|
2296
|
-
await
|
|
2297
|
-
const manifest = await
|
|
2298
|
-
await
|
|
2545
|
+
await fs7.rm(tmp, { recursive: true, force: true });
|
|
2546
|
+
}
|
|
2547
|
+
const identityPath = path7.join(dest, "src", "appIdentity.ts");
|
|
2548
|
+
const manifestPath = path7.join(dest, "groundfloor.manifest.json");
|
|
2549
|
+
const identity = await fs7.readFile(identityPath, "utf8");
|
|
2550
|
+
await fs7.writeFile(identityPath, stampAppIdentity(identity, slug), "utf8");
|
|
2551
|
+
const manifest = await fs7.readFile(manifestPath, "utf8");
|
|
2552
|
+
await fs7.writeFile(manifestPath, stampManifest(manifest, slug, name), "utf8");
|
|
2299
2553
|
const result = {
|
|
2300
2554
|
dir: dest,
|
|
2301
2555
|
slug,
|
|
@@ -2321,6 +2575,9 @@ async function appsInitCommand(opts) {
|
|
|
2321
2575
|
process.stdout.write(
|
|
2322
2576
|
"Do not wrap src/App.tsx in BrowserRouter (DevShell already has one).\n"
|
|
2323
2577
|
);
|
|
2578
|
+
process.stdout.write(
|
|
2579
|
+
"MCP: .mcp.json is included (npx @groundfloorcloud/mcp). Cursor on macOS: copy .cursor/mcp.json.example.\n"
|
|
2580
|
+
);
|
|
2324
2581
|
process.stdout.write("Next:\n");
|
|
2325
2582
|
for (const step of result.next) {
|
|
2326
2583
|
process.stdout.write(` ${step}
|
|
@@ -2363,6 +2620,81 @@ async function dataplaneProvisionCommand(opts) {
|
|
|
2363
2620
|
}
|
|
2364
2621
|
}
|
|
2365
2622
|
|
|
2623
|
+
// src/update-check.ts
|
|
2624
|
+
import { promises as fs8 } from "fs";
|
|
2625
|
+
import path8 from "path";
|
|
2626
|
+
var UPDATE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
2627
|
+
function isNewer(latest, current) {
|
|
2628
|
+
const a = latest.split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
2629
|
+
const b = current.split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
2630
|
+
const len = Math.max(a.length, b.length);
|
|
2631
|
+
for (let i = 0; i < len; i += 1) {
|
|
2632
|
+
const lv = a[i] ?? 0;
|
|
2633
|
+
const cv = b[i] ?? 0;
|
|
2634
|
+
if (lv > cv) return true;
|
|
2635
|
+
if (lv < cv) return false;
|
|
2636
|
+
}
|
|
2637
|
+
return false;
|
|
2638
|
+
}
|
|
2639
|
+
function upgradeNotice(current, latest) {
|
|
2640
|
+
return `gf ${latest} is available (you have ${current}). npm i -g ${GF_NPM_PACKAGE}@latest`;
|
|
2641
|
+
}
|
|
2642
|
+
function unknownCommandNotice(command, current, latest) {
|
|
2643
|
+
if (latest && isNewer(latest, current)) {
|
|
2644
|
+
return `unknown command '${command}' \u2014 your gf is ${current}, latest is ${latest}. npm i -g ${GF_NPM_PACKAGE}@latest`;
|
|
2645
|
+
}
|
|
2646
|
+
return `unknown command '${command}' \u2014 your gf is ${current}`;
|
|
2647
|
+
}
|
|
2648
|
+
function parseUnknownCommandName(message) {
|
|
2649
|
+
const match = message.match(/unknown command ['"]([^'"]+)['"]/i);
|
|
2650
|
+
return match?.[1] ?? "that";
|
|
2651
|
+
}
|
|
2652
|
+
async function fetchNpmLatest(pkg = GF_NPM_PACKAGE) {
|
|
2653
|
+
try {
|
|
2654
|
+
const res = await fetch(`https://registry.npmjs.org/${pkg}/latest`, {
|
|
2655
|
+
headers: { Accept: "application/json" },
|
|
2656
|
+
signal: AbortSignal.timeout(1500)
|
|
2657
|
+
});
|
|
2658
|
+
if (!res.ok) return null;
|
|
2659
|
+
const body = await res.json();
|
|
2660
|
+
return typeof body.version === "string" && body.version.trim() ? body.version.trim() : null;
|
|
2661
|
+
} catch {
|
|
2662
|
+
return null;
|
|
2663
|
+
}
|
|
2664
|
+
}
|
|
2665
|
+
async function readUpdateCache(file) {
|
|
2666
|
+
try {
|
|
2667
|
+
const raw = await fs8.readFile(file, "utf8");
|
|
2668
|
+
const parsed = JSON.parse(raw);
|
|
2669
|
+
if (typeof parsed.checkedAt !== "number" || typeof parsed.latest !== "string" || !parsed.latest.trim()) {
|
|
2670
|
+
return null;
|
|
2671
|
+
}
|
|
2672
|
+
return parsed;
|
|
2673
|
+
} catch {
|
|
2674
|
+
return null;
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
async function writeUpdateCache(file, cache) {
|
|
2678
|
+
await fs8.mkdir(path8.dirname(file), { recursive: true, mode: 448 });
|
|
2679
|
+
await fs8.writeFile(file, `${JSON.stringify(cache)}
|
|
2680
|
+
`, { mode: 384 });
|
|
2681
|
+
}
|
|
2682
|
+
async function resolveLatest(opts) {
|
|
2683
|
+
const now = opts.now ?? Date.now();
|
|
2684
|
+
const ttl = opts.ttlMs ?? UPDATE_TTL_MS;
|
|
2685
|
+
const cached = await readUpdateCache(opts.cacheFile);
|
|
2686
|
+
if (cached && now - cached.checkedAt < ttl) {
|
|
2687
|
+
return cached.latest;
|
|
2688
|
+
}
|
|
2689
|
+
const fetchLatest = opts.fetchLatest ?? fetchNpmLatest;
|
|
2690
|
+
const latest = await fetchLatest();
|
|
2691
|
+
if (latest) {
|
|
2692
|
+
await writeUpdateCache(opts.cacheFile, { checkedAt: now, latest });
|
|
2693
|
+
return latest;
|
|
2694
|
+
}
|
|
2695
|
+
return cached?.latest ?? null;
|
|
2696
|
+
}
|
|
2697
|
+
|
|
2366
2698
|
// src/index.ts
|
|
2367
2699
|
function collect(value, previous) {
|
|
2368
2700
|
return previous.concat([value]);
|
|
@@ -2378,7 +2710,7 @@ function run(fn) {
|
|
|
2378
2710
|
var program = new Command();
|
|
2379
2711
|
program.name("gf").description(
|
|
2380
2712
|
"Groundfloor CLI \u2014 sign in, deploy coderunners, publish Shell apps, and manage workspace resources."
|
|
2381
|
-
).version(
|
|
2713
|
+
).version(GF_VERSION);
|
|
2382
2714
|
program.command("login").description(
|
|
2383
2715
|
"Log in via the browser (production by default; --dev / --stage for other cells)"
|
|
2384
2716
|
).option("--dev", "Sign in to Groundfloor dev").option("--stage", "Sign in to Groundfloor stage").option(
|
|
@@ -2403,6 +2735,18 @@ workspaces.command("use <workspaceId>").description("Set the default workspace")
|
|
|
2403
2735
|
var coderunner = program.command("coderunner").alias("cr").description("Coderunner commands");
|
|
2404
2736
|
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)));
|
|
2405
2737
|
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)));
|
|
2738
|
+
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(
|
|
2739
|
+
(opts) => run(
|
|
2740
|
+
() => logsCommand({
|
|
2741
|
+
workspace: opts.workspace,
|
|
2742
|
+
coderunner: opts.coderunner,
|
|
2743
|
+
limit: opts.limit,
|
|
2744
|
+
sinceSeconds: opts.sinceSeconds,
|
|
2745
|
+
search: opts.search,
|
|
2746
|
+
json: opts.json
|
|
2747
|
+
})
|
|
2748
|
+
)
|
|
2749
|
+
);
|
|
2406
2750
|
coderunner.command("run").description("Invoke a deployed function/job (smoke run)").requiredOption("-c, --coderunner <id|slug>", "Coderunner id or slug").option("-w, --workspace <id>", "Workspace id override").option("--payload <json>", "JSON payload object", "{}").option("--json", "Output raw JSON").action(
|
|
2407
2751
|
(opts) => run(
|
|
2408
2752
|
() => runCommand({
|
|
@@ -2501,7 +2845,40 @@ program.command("deploy").description(
|
|
|
2501
2845
|
"--app-id <id>",
|
|
2502
2846
|
"Bind newly created coderunner as a helper of this App (optional)"
|
|
2503
2847
|
).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)));
|
|
2504
|
-
|
|
2848
|
+
function wireUnknownCommands(root, current, latest) {
|
|
2849
|
+
const walk = (cmd) => {
|
|
2850
|
+
if (cmd.commands.length === 0) return;
|
|
2851
|
+
cmd.exitOverride((err) => {
|
|
2852
|
+
if (err.code === "commander.unknownCommand") {
|
|
2853
|
+
process.stderr.write(
|
|
2854
|
+
`${unknownCommandNotice(parseUnknownCommandName(err.message), current, latest)}
|
|
2855
|
+
`
|
|
2856
|
+
);
|
|
2857
|
+
process.exit(1);
|
|
2858
|
+
}
|
|
2859
|
+
throw err;
|
|
2860
|
+
});
|
|
2861
|
+
for (const child of cmd.commands) walk(child);
|
|
2862
|
+
};
|
|
2863
|
+
walk(root);
|
|
2864
|
+
}
|
|
2865
|
+
async function notifyUpgrade() {
|
|
2866
|
+
if (process.env.GF_SKIP_UPDATE_CHECK === "1") return null;
|
|
2867
|
+
const latest = await resolveLatest({
|
|
2868
|
+
cacheFile: path9.join(paths.CONFIG_DIR, "update-check.json")
|
|
2869
|
+
});
|
|
2870
|
+
if (latest && isNewer(latest, GF_VERSION)) {
|
|
2871
|
+
process.stderr.write(`${upgradeNotice(GF_VERSION, latest)}
|
|
2872
|
+
`);
|
|
2873
|
+
}
|
|
2874
|
+
return latest;
|
|
2875
|
+
}
|
|
2876
|
+
async function main() {
|
|
2877
|
+
const latest = await notifyUpgrade();
|
|
2878
|
+
wireUnknownCommands(program, GF_VERSION, latest);
|
|
2879
|
+
await program.parseAsync(process.argv);
|
|
2880
|
+
}
|
|
2881
|
+
main().catch((err) => {
|
|
2505
2882
|
const message = err instanceof Error ? err.message : String(err);
|
|
2506
2883
|
process.stderr.write(`Error: ${message}
|
|
2507
2884
|
`);
|
package/package.json
CHANGED