@groundfloorcloud/cli 0.1.5 → 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 +6 -1
  2. package/dist/index.js +139 -20
  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
 
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}`,
@@ -2004,21 +2006,21 @@ function chooseRemoteEntry(relpaths) {
2004
2006
  })[0];
2005
2007
  }
2006
2008
  function flattenFederatedBundlePlan(relpaths) {
2007
- const paths = relpaths.map(normalizeZipEntryName).filter((name) => name && !name.endsWith("/") && !name.split("/").includes(".."));
2008
- 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);
2009
2011
  const entry = chooseRemoteEntry([...pathset]);
2010
2012
  if (!entry) return {};
2011
2013
  const promoteDirs = /* @__PURE__ */ new Set();
2012
2014
  const entryDir = parentDir(entry);
2013
2015
  if (entryDir) promoteDirs.add(entryDir);
2014
- for (const path8 of pathset) {
2015
- const parent = parentDir(path8);
2016
+ for (const path10 of pathset) {
2017
+ const parent = parentDir(path10);
2016
2018
  if (parent === "assets" || parent.endsWith("/assets") && parent.split("/").length - 1 < 2) {
2017
2019
  promoteDirs.add(parent);
2018
2020
  }
2019
2021
  }
2020
2022
  const destToSrc = {};
2021
- for (const src of paths) {
2023
+ for (const src of paths2) {
2022
2024
  const parent = parentDir(src);
2023
2025
  if (!promoteDirs.has(parent)) continue;
2024
2026
  const dest = basename(src);
@@ -2393,6 +2395,12 @@ import { promises as fs7 } from "fs";
2393
2395
  import os3 from "os";
2394
2396
  import path7 from "path";
2395
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
2396
2404
  var MAX_KIT_BYTES = 8 * 1024 * 1024;
2397
2405
  function slugify3(input) {
2398
2406
  const slug = input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
@@ -2465,7 +2473,7 @@ async function downloadKit(url) {
2465
2473
  res = await fetch(url, {
2466
2474
  redirect: "follow",
2467
2475
  signal: AbortSignal.timeout(6e4),
2468
- headers: { "User-Agent": "groundfloor-cli/0.1.1" }
2476
+ headers: { "User-Agent": `groundfloor-cli/${GF_VERSION}` }
2469
2477
  });
2470
2478
  } catch (err) {
2471
2479
  const message = err instanceof Error ? err.message : String(err);
@@ -2567,6 +2575,9 @@ async function appsInitCommand(opts) {
2567
2575
  process.stdout.write(
2568
2576
  "Do not wrap src/App.tsx in BrowserRouter (DevShell already has one).\n"
2569
2577
  );
2578
+ process.stdout.write(
2579
+ "MCP: .mcp.json is included (npx @groundfloorcloud/mcp). Cursor on macOS: copy .cursor/mcp.json.example.\n"
2580
+ );
2570
2581
  process.stdout.write("Next:\n");
2571
2582
  for (const step of result.next) {
2572
2583
  process.stdout.write(` ${step}
@@ -2609,6 +2620,81 @@ async function dataplaneProvisionCommand(opts) {
2609
2620
  }
2610
2621
  }
2611
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
+
2612
2698
  // src/index.ts
2613
2699
  function collect(value, previous) {
2614
2700
  return previous.concat([value]);
@@ -2624,7 +2710,7 @@ function run(fn) {
2624
2710
  var program = new Command();
2625
2711
  program.name("gf").description(
2626
2712
  "Groundfloor CLI \u2014 sign in, deploy coderunners, publish Shell apps, and manage workspace resources."
2627
- ).version("0.1.5");
2713
+ ).version(GF_VERSION);
2628
2714
  program.command("login").description(
2629
2715
  "Log in via the browser (production by default; --dev / --stage for other cells)"
2630
2716
  ).option("--dev", "Sign in to Groundfloor dev").option("--stage", "Sign in to Groundfloor stage").option(
@@ -2759,7 +2845,40 @@ program.command("deploy").description(
2759
2845
  "--app-id <id>",
2760
2846
  "Bind newly created coderunner as a helper of this App (optional)"
2761
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)));
2762
- 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) => {
2763
2882
  const message = err instanceof Error ? err.message : String(err);
2764
2883
  process.stderr.write(`Error: ${message}
2765
2884
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groundfloorcloud/cli",
3
- "version": "0.1.5",
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": {