@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.
Files changed (3) hide show
  1. package/README.md +11 -3
  2. package/dist/index.js +486 -109
  3. 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 login` signs in to production
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 images run `npm start` — add `"scripts": { "start": "node index.js" }`.
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, path7) {
538
+ async function cpGet(apiUrl, path10) {
537
539
  const token = await getValidAccessToken();
538
- const res = await fetch(`${apiUrl}${path7}`, {
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, path7, body) {
551
+ async function cpPostJson(apiUrl, path10, body) {
550
552
  const token = await getValidAccessToken();
551
- const res = await fetch(`${apiUrl}${path7}`, {
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, path7) {
567
+ async function cpDelete(apiUrl, path10) {
566
568
  const token = await getValidAccessToken();
567
- const res = await fetch(`${apiUrl}${path7}`, {
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, path7, body) {
577
+ async function cpPutJson(apiUrl, path10, body) {
576
578
  const token = await getValidAccessToken();
577
- const res = await fetch(`${apiUrl}${path7}`, {
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, path7, body) {
593
+ async function cpPatchJson(apiUrl, path10, body) {
592
594
  const token = await getValidAccessToken();
593
- const res = await fetch(`${apiUrl}${path7}`, {
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, path7, body, contentType) {
609
+ async function cpPutBytes(apiUrl, path10, body, contentType) {
608
610
  const token = await getValidAccessToken();
609
- const res = await fetch(`${apiUrl}${path7}`, {
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 fs3 } from "fs";
1008
- import path3 from "path";
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 ${path3.resolve(opts.path ?? process.cwd())}...`
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 fs3.access(path3.join(pkg.sourceDir, "Dockerfile"));
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
- try {
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) : path3.basename(path3.resolve(opts.path ?? process.cwd())));
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
- throw new Error(
1321
- `Build failed for version ${uploaded.version_id} (status=4).`
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
- throw new Error("Timed out waiting for the build to complete.");
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
- throw new Error(
1362
- `Deployment failed for coderunner ${coderunnerId} (status=4).`
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
- throw new Error("Timed out waiting for the deployment to complete.");
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 fs3.rm(pkg.cleanup, { recursive: true, force: true }).catch(() => {
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 resolveCoderunnerId2(
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 fs4 } from "fs";
1638
- import path4 from "path";
1885
+ import { promises as fs5 } from "fs";
1886
+ import path5 from "path";
1639
1887
  function guessContentType(filename) {
1640
- const ext = path4.extname(filename).toLowerCase();
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 = path4.resolve(filePath);
1676
- const buf = await fs4.readFile(resolved);
1677
- const name = opts.name ?? path4.basename(resolved);
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 = path4.resolve(opts.out ?? fileId);
1715
- await fs4.writeFile(dest, buf);
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 fs5 } from "fs";
1728
- import path5 from "path";
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 paths = relpaths.map(normalizeZipEntryName).filter((name) => name && !name.endsWith("/") && !name.split("/").includes(".."));
1762
- const pathset = new Set(paths);
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 path7 of pathset) {
1769
- const parent = parentDir(path7);
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 paths) {
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 fs5.readFile(filePath, "utf8");
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(path5.resolve(filePath));
2157
+ return readJsonObject(path6.resolve(filePath));
1910
2158
  }
1911
2159
  async function findSiblingManifest(bundlePath) {
1912
- const dir = await isDirectory(bundlePath) ? bundlePath : path5.dirname(bundlePath);
1913
- const candidate = path5.join(dir, "groundfloor.manifest.json");
2160
+ const dir = await isDirectory(bundlePath) ? bundlePath : path6.dirname(bundlePath);
2161
+ const candidate = path6.join(dir, "groundfloor.manifest.json");
1914
2162
  try {
1915
- await fs5.access(candidate);
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 fs5.stat(filePath)).isDirectory();
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 = path5.resolve(opts.path ?? "release.zip");
2072
- const bundlePath = await isDirectory(inputPath) ? path5.join(inputPath, "release.zip") : 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 fs5.readFile(bundlePath);
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 = path5.extname(bundlePath).toLowerCase();
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 ${path5.basename(bundlePath)}. ` + initThenPublishHint(app.slug)
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 fs6 } from "fs";
2394
+ import { promises as fs7 } from "fs";
2147
2395
  import os3 from "os";
2148
- import path6 from "path";
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 fs6.access(filePath);
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 fs6.readdir(dir);
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
- path6.join(extracted, "shell-starter-kit"),
2431
+ path7.join(extracted, "shell-starter-kit"),
2178
2432
  extracted
2179
2433
  ];
2180
- const entries = await fs6.readdir(extracted, { withFileTypes: true });
2434
+ const entries = await fs7.readdir(extracted, { withFileTypes: true });
2181
2435
  for (const entry of entries) {
2182
2436
  if (entry.isDirectory()) {
2183
- candidates.push(path6.join(extracted, entry.name));
2437
+ candidates.push(path7.join(extracted, entry.name));
2184
2438
  }
2185
2439
  }
2186
2440
  for (const candidate of candidates) {
2187
- if (await pathExists(path6.join(candidate, "groundfloor.manifest.json"))) {
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": "groundfloor-cli/0.1.1" }
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 = path6.resolve(opts.dir?.trim() || path6.join(process.cwd(), slug));
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 fs6.rm(dest, { recursive: true, force: true });
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 fs6.mkdtemp(path6.join(os3.tmpdir(), "gf-starter-kit-"));
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 fs6.mkdir(dest, { recursive: true });
2289
- await fs6.cp(kitRoot, dest, { recursive: true, force: true });
2542
+ await fs7.mkdir(dest, { recursive: true });
2543
+ await fs7.cp(kitRoot, dest, { recursive: true, force: true });
2290
2544
  } finally {
2291
- await fs6.rm(tmp, { recursive: true, force: true });
2292
- }
2293
- const identityPath = path6.join(dest, "src", "appIdentity.ts");
2294
- const manifestPath = path6.join(dest, "groundfloor.manifest.json");
2295
- const identity = await fs6.readFile(identityPath, "utf8");
2296
- await fs6.writeFile(identityPath, stampAppIdentity(identity, slug), "utf8");
2297
- const manifest = await fs6.readFile(manifestPath, "utf8");
2298
- await fs6.writeFile(manifestPath, stampManifest(manifest, slug, name), "utf8");
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("0.1.4");
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
- program.parseAsync(process.argv).catch((err) => {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groundfloorcloud/cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
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": {