@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.
Files changed (3) hide show
  1. package/README.md +13 -3
  2. package/dist/index.js +287 -31
  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
 
@@ -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, path8) {
538
+ async function cpGet(apiUrl, path10) {
537
539
  const token = await getValidAccessToken();
538
- const res = await fetch(`${apiUrl}${path8}`, {
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, path8, body) {
551
+ async function cpPostJson(apiUrl, path10, body) {
550
552
  const token = await getValidAccessToken();
551
- const res = await fetch(`${apiUrl}${path8}`, {
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, path8) {
567
+ async function cpDelete(apiUrl, path10) {
566
568
  const token = await getValidAccessToken();
567
- const res = await fetch(`${apiUrl}${path8}`, {
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, path8, body) {
577
+ async function cpPutJson(apiUrl, path10, body) {
576
578
  const token = await getValidAccessToken();
577
- const res = await fetch(`${apiUrl}${path8}`, {
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, path8, body) {
593
+ async function cpPatchJson(apiUrl, path10, body) {
592
594
  const token = await getValidAccessToken();
593
- const res = await fetch(`${apiUrl}${path8}`, {
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, path8, body, contentType) {
609
+ async function cpPutBytes(apiUrl, path10, body, contentType) {
608
610
  const token = await getValidAccessToken();
609
- const res = await fetch(`${apiUrl}${path8}`, {
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 paths = relpaths.map(normalizeZipEntryName).filter((name) => name && !name.endsWith("/") && !name.split("/").includes(".."));
2008
- const pathset = new Set(paths);
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 path8 of pathset) {
2015
- const parent = parentDir(path8);
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 paths) {
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": "groundfloor-cli/0.1.1" }
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("0.1.5");
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
- program.parseAsync(process.argv).catch((err) => {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groundfloorcloud/cli",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
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": {