flyspace-cli 0.1.6 → 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 (2) hide show
  1. package/dist/cli.js +230 -73
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -430,6 +430,7 @@ async function runConfig(args) {
430
430
  }
431
431
 
432
432
  // src/commands/setup.ts
433
+ import { execSync as execSync4 } from "child_process";
433
434
  import { mkdirSync as mkdirSync5 } from "fs";
434
435
  import { join as join7 } from "path";
435
436
  import { confirm as confirm2 } from "@inquirer/prompts";
@@ -770,15 +771,6 @@ function isServiceRunning(profile) {
770
771
  }
771
772
  return false;
772
773
  }
773
- async function runProfiles() {
774
- const profiles = listProfiles();
775
- console.log("\n Profiles:\n");
776
- for (const profile of profiles) {
777
- const status = isServiceRunning(profile) ? "\x1B[32mrunning\x1B[0m" : "\x1B[90mstopped\x1B[0m";
778
- console.log(` ${profile.padEnd(20)} ${status}`);
779
- }
780
- console.log("\n Target a profile with --profile <name> on any command.\n");
781
- }
782
774
 
783
775
  // src/auth/device-auth.ts
784
776
  import { createPrivateKey, createPublicKey, generateKeyPairSync, sign } from "crypto";
@@ -932,6 +924,17 @@ async function fetchWhoAmI(base, token) {
932
924
  workspaces: toSlugs(json["workspaces"])
933
925
  };
934
926
  }
927
+ async function boundWorkspaces() {
928
+ const creds = loadCredentials();
929
+ if (!creds) return null;
930
+ try {
931
+ const me = await fetchWhoAmI(creds.apiUrl, await getAccessToken());
932
+ saveCredentials({ ...loadCredentials() ?? creds, workspaces: me.workspaces });
933
+ return { workspaces: me.workspaces, fresh: true };
934
+ } catch {
935
+ return { workspaces: creds.workspaces ?? [], fresh: false };
936
+ }
937
+ }
935
938
  async function revokeDevice(base, token) {
936
939
  await expectOk(await post(base, "/api/auth/device/revoke", {}, token), "Revoking this device");
937
940
  }
@@ -1510,6 +1513,16 @@ async function runAuthCmd(args) {
1510
1513
  }
1511
1514
  }
1512
1515
 
1516
+ // src/version.ts
1517
+ var CLI_VERSION = true ? "0.1.7" : null;
1518
+ function isOlderVersion(a, b) {
1519
+ const parse2 = (v) => v.replace(/^v/, "").split("-")[0].split(".").map(Number);
1520
+ const [pa, pb] = [parse2(a), parse2(b)];
1521
+ if (pa.length !== 3 || pb.length !== 3 || [...pa, ...pb].some(Number.isNaN)) return false;
1522
+ for (let i = 0; i < 3; i++) if (pa[i] !== pb[i]) return pa[i] < pb[i];
1523
+ return false;
1524
+ }
1525
+
1513
1526
  // src/commands/setup.ts
1514
1527
  var BOLD2 = "\x1B[1m";
1515
1528
  var DIM2 = "\x1B[2m";
@@ -1541,15 +1554,20 @@ var tilde2 = (p) => p.replace(process.env["HOME"] ?? "\0", "~");
1541
1554
  var ok = (msg) => console.log(` ${GREEN2}\u2713${RESET2} ${msg}`);
1542
1555
  var bad = (msg) => console.log(` ${YELLOW2}\u2717${RESET2} ${msg}`);
1543
1556
  async function servedWorkspaces() {
1544
- const creds = loadCredentials();
1557
+ const bound = await boundWorkspaces();
1558
+ if (!bound.fresh) console.log(` ${DIM2}Couldn't reach the server; using the workspaces saved at login.${RESET2}`);
1559
+ return bound.workspaces;
1560
+ }
1561
+ function checkLatestVersion() {
1562
+ if (!CLI_VERSION) return;
1563
+ let latest = "";
1545
1564
  try {
1546
- const me = await fetchWhoAmI(creds.apiUrl, await getAccessToken());
1547
- saveCredentials({ ...loadCredentials() ?? creds, workspaces: me.workspaces });
1548
- return me.workspaces;
1565
+ latest = execSync4(`npm view ${"flyspace-cli"} version`, { encoding: "utf-8", stdio: "pipe", timeout: 1e4 }).trim();
1549
1566
  } catch {
1550
- console.log(` ${DIM2}Couldn't reach the server; using the workspaces saved at login.${RESET2}`);
1551
- return creds.workspaces ?? [];
1567
+ return;
1552
1568
  }
1569
+ if (!latest || !isOlderVersion(CLI_VERSION, latest)) return;
1570
+ bad(`flyspace ${CLI_VERSION} is out of date (latest ${latest}) ${DIM2}run: flyspace upgrade${RESET2}`);
1553
1571
  }
1554
1572
  function loginArgs(args) {
1555
1573
  const out = [];
@@ -1574,6 +1592,7 @@ async function runSetup(args) {
1574
1592
  console.log(`
1575
1593
  ${BOLD2}Flyspace Setup${RESET2}
1576
1594
  `);
1595
+ checkLatestVersion();
1577
1596
  const existing = loadCredentials();
1578
1597
  let workspaces;
1579
1598
  if (existing) {
@@ -1583,7 +1602,10 @@ ${BOLD2}Flyspace Setup${RESET2}
1583
1602
  console.log(` Device ${existing.deviceName}`);
1584
1603
  console.log(` Workspaces ${workspaces.length ? workspaces.join(", ") : "(none)"}`);
1585
1604
  const others = listProfiles().filter((p) => p !== "default" && !workspaces.includes(p));
1586
- if (others.length) console.log(` ${DIM2}Other local profiles (left alone): ${others.join(", ")}${RESET2}`);
1605
+ if (others.length) {
1606
+ console.log(` ${DIM2}Not bound here, nothing runs for them: ${others.join(", ")}${RESET2}`);
1607
+ console.log(` ${DIM2}See: flyspace profiles Clean up: flyspace profiles remove <name>${RESET2}`);
1608
+ }
1587
1609
  console.log("");
1588
1610
  const keep = args.includes("--yes") || !process.stdin.isTTY ? true : await confirm2({ message: "Keep using this account and device?", default: true });
1589
1611
  if (!keep) {
@@ -1637,7 +1659,7 @@ ${BOLD2}Flyspace Setup${RESET2}
1637
1659
 
1638
1660
  // src/commands/uninstall.ts
1639
1661
  import { existsSync as existsSync8, unlinkSync as unlinkSync2 } from "fs";
1640
- import { execSync as execSync4 } from "child_process";
1662
+ import { execSync as execSync5 } from "child_process";
1641
1663
  import { basename as basename2, join as join8 } from "path";
1642
1664
  import { homedir as homedir4, platform as platform5 } from "os";
1643
1665
  function log2(msg) {
@@ -1651,7 +1673,7 @@ function warn2(msg) {
1651
1673
  console.log(`\x1B[33m!\x1B[0m ${msg}`);
1652
1674
  }
1653
1675
  function run2(cmd) {
1654
- execSync4(cmd, { encoding: "utf-8", stdio: "pipe" });
1676
+ execSync5(cmd, { encoding: "utf-8", stdio: "pipe" });
1655
1677
  }
1656
1678
  async function runUninstall() {
1657
1679
  const os = platform5();
@@ -1698,8 +1720,89 @@ async function runUninstall() {
1698
1720
  console.log("\n Config and data directories were NOT removed.");
1699
1721
  }
1700
1722
 
1723
+ // src/commands/profiles-cmd.ts
1724
+ import { rmSync as rmSync3 } from "fs";
1725
+ var OPERATOR_NOTE = "This machine is an operator Host (it has Supabase credentials), so it serves every workspace, bound or not.";
1726
+ var USAGE = `
1727
+ Usage: flyspace profiles [list]
1728
+ flyspace profiles remove <name>
1729
+
1730
+ list Every local profile, whether this Host is bound to it, and the
1731
+ state of its scheduler service.
1732
+ remove Unload a profile's scheduler and delete its local config. Only for
1733
+ profiles this Host is not bound to; the Workspace itself, its
1734
+ channel folders and its data directory are left alone.
1735
+ `;
1736
+ async function boundHere() {
1737
+ const bound = await boundWorkspaces();
1738
+ if (bound && !bound.fresh) console.log(" \x1B[2mCouldn't reach the server; using the workspaces saved at login.\x1B[0m");
1739
+ return bound?.workspaces ?? null;
1740
+ }
1741
+ async function listCmd() {
1742
+ const bound = await boundHere();
1743
+ console.log("\n Profiles:\n");
1744
+ console.log(` ${"NAME".padEnd(24)} ${"BOUND HERE".padEnd(12)} SCHEDULER`);
1745
+ for (const profile of listProfiles()) {
1746
+ const scheduler = !isServiceInstalled(profile) ? "\x1B[90mnot installed\x1B[0m" : isServiceRunning(profile) ? "\x1B[32mrunning\x1B[0m" : "\x1B[90mstopped\x1B[0m";
1747
+ const here = bound === null ? "-".padEnd(12) : bound.includes(profile) ? "yes".padEnd(12) : `\x1B[33m${"no".padEnd(12)}\x1B[0m`;
1748
+ console.log(` ${profile.padEnd(24)} ${here} ${scheduler}`);
1749
+ }
1750
+ const missing = (bound ?? []).filter((w) => !listProfiles().includes(w));
1751
+ for (const w of missing) console.log(` ${w.padEnd(24)} ${"yes".padEnd(12)} \x1B[33mno local profile yet\x1B[0m`);
1752
+ if (bound?.length === 0) {
1753
+ console.log("\n This Host is not bound to any workspace, so no scheduler runs here. Run: flyspace setup");
1754
+ }
1755
+ if (hasSupabaseCredentials()) {
1756
+ console.log(`
1757
+ ${OPERATOR_NOTE}`);
1758
+ } else if (bound && listProfiles().some((p) => p !== DEFAULT_PROFILE && !bound.includes(p))) {
1759
+ console.log("\n Profiles not bound here run nothing on this machine. Remove one with: flyspace profiles remove <name>");
1760
+ }
1761
+ console.log("");
1762
+ }
1763
+ async function removeCmd(name) {
1764
+ if (!name) return void console.log(USAGE);
1765
+ if (name === DEFAULT_PROFILE) {
1766
+ console.error(" The default profile cannot be removed.");
1767
+ process.exit(1);
1768
+ }
1769
+ if (!listProfiles().includes(name)) {
1770
+ console.error(` No local profile named "${name}".`);
1771
+ process.exit(1);
1772
+ }
1773
+ if (hasSupabaseCredentials()) {
1774
+ console.error(` ${OPERATOR_NOTE} Removing a profile would stop that workspace's schedules.`);
1775
+ process.exit(1);
1776
+ }
1777
+ const bound = await boundHere();
1778
+ if (bound === null) {
1779
+ console.error(" This machine has no device login, so it serves every workspace; removing a profile would be undone.");
1780
+ process.exit(1);
1781
+ }
1782
+ if (bound.includes(name)) {
1783
+ console.error(` This Host is still bound to "${name}" on the server, so the Host would reinstall it within a minute.`);
1784
+ console.error(" Unbinding a Host from a workspace is not available yet.");
1785
+ process.exit(1);
1786
+ }
1787
+ const { configDir: configDir2, dataDir } = runWithProfile(name, () => ({ configDir: paths.CONFIG_DIR, dataDir: paths.DATA_DIR }));
1788
+ if (isServiceInstalled(name)) await runWithProfile(name, runUninstall);
1789
+ rmSync3(configDir2, { recursive: true, force: true });
1790
+ console.log(`\x1B[32m+\x1B[0m Local config removed: ${configDir2}`);
1791
+ console.log(`
1792
+ Kept: ${dataDir} (sessions, logs) and any ~/Flyspace/${name}/ channel folders.`);
1793
+ console.log(` The workspace and its server-side records are unchanged; this only cleaned up this machine.
1794
+ `);
1795
+ }
1796
+ async function runProfiles(args = []) {
1797
+ loadEnvFiles();
1798
+ const [sub, name] = args;
1799
+ if (sub === "remove" || sub === "rm") return removeCmd(name);
1800
+ if (!sub || sub === "list" || sub === "ls") return listCmd();
1801
+ console.log(USAGE);
1802
+ }
1803
+
1701
1804
  // src/commands/upgrade.ts
1702
- import { execSync as execSync5 } from "child_process";
1805
+ import { execSync as execSync6 } from "child_process";
1703
1806
  function restartInstalledProfiles() {
1704
1807
  if (isHostServiceInstalled()) {
1705
1808
  console.log(restartHostService() ? "\nHost restarted." : "\nHost restart failed; run: flyspace setup --start-host --restart");
@@ -1720,18 +1823,18 @@ Restarting ${installed.length} instance(s)...`);
1720
1823
  }
1721
1824
  }
1722
1825
  async function runUpgrade() {
1723
- const current = "0.1.6";
1826
+ const current = "0.1.7";
1724
1827
  console.log(`Current version: ${current}`);
1725
1828
  console.log("Checking for updates...\n");
1726
1829
  try {
1727
- const latest = execSync5(`npm view ${"flyspace-cli"} version`, { encoding: "utf-8" }).trim();
1830
+ const latest = execSync6(`npm view ${"flyspace-cli"} version`, { encoding: "utf-8" }).trim();
1728
1831
  if (latest === current) {
1729
1832
  console.log(`Already on the latest version (${current}).`);
1730
1833
  return;
1731
1834
  }
1732
1835
  console.log(`New version available: ${latest}`);
1733
1836
  console.log("Upgrading...\n");
1734
- execSync5(`npm install -g ${"flyspace-cli"}@latest`, { stdio: "inherit" });
1837
+ execSync6(`npm install -g ${"flyspace-cli"}@latest`, { stdio: "inherit" });
1735
1838
  console.log(`
1736
1839
  Upgraded from ${current} to ${latest}.`);
1737
1840
  restartInstalledProfiles();
@@ -1747,7 +1850,7 @@ You can upgrade manually with: npm install -g ${"flyspace-cli"}@latest`);
1747
1850
 
1748
1851
  // src/commands/automation.ts
1749
1852
  import { WebClient } from "@slack/web-api";
1750
- var USAGE = `
1853
+ var USAGE2 = `
1751
1854
  Usage: flyspace automation <subcommand> [options]
1752
1855
 
1753
1856
  Subcommands:
@@ -2085,7 +2188,7 @@ async function runAutomationCmd(args) {
2085
2188
  await handleRun(manager, args[1]);
2086
2189
  break;
2087
2190
  default:
2088
- console.log(USAGE);
2191
+ console.log(USAGE2);
2089
2192
  break;
2090
2193
  }
2091
2194
  }
@@ -2276,7 +2379,7 @@ function breakdown(store, by, days) {
2276
2379
  }
2277
2380
 
2278
2381
  // src/commands/stop.ts
2279
- import { execSync as execSync6 } from "child_process";
2382
+ import { execSync as execSync7 } from "child_process";
2280
2383
  import { platform as platform6 } from "os";
2281
2384
  function log3(msg) {
2282
2385
  console.log(`
@@ -2295,7 +2398,7 @@ async function runStop() {
2295
2398
  const plist = plistPath(profile);
2296
2399
  log3("Stopping Flyspace service...");
2297
2400
  try {
2298
- execSync6(`launchctl unload "${plist}"`, { stdio: "pipe" });
2401
+ execSync7(`launchctl unload "${plist}"`, { stdio: "pipe" });
2299
2402
  success3("Service stopped. Run `flyspace restart` to start again.");
2300
2403
  } catch {
2301
2404
  warn3("Service is not currently loaded.");
@@ -2303,7 +2406,7 @@ async function runStop() {
2303
2406
  } else if (os === "linux") {
2304
2407
  log3("Stopping Flyspace service...");
2305
2408
  try {
2306
- execSync6(`systemctl --user stop ${systemdUnitName(profile)}`, { stdio: "pipe" });
2409
+ execSync7(`systemctl --user stop ${systemdUnitName(profile)}`, { stdio: "pipe" });
2307
2410
  success3("Service stopped. Run `flyspace restart` to start again.");
2308
2411
  } catch {
2309
2412
  warn3("Service is not currently running.");
@@ -3897,16 +4000,8 @@ import { serveStatic } from "@hono/node-server/serve-static";
3897
4000
 
3898
4001
  // src/reconcile.ts
3899
4002
  import { existsSync as existsSync11, mkdirSync as mkdirSync6, writeFileSync as writeFileSync5 } from "fs";
3900
- async function boundWorkspaces() {
3901
- const creds = loadCredentials();
3902
- if (!creds) return null;
3903
- try {
3904
- const me = await fetchWhoAmI(creds.apiUrl, await getAccessToken());
3905
- saveCredentials({ ...loadCredentials() ?? creds, workspaces: me.workspaces });
3906
- return me.workspaces;
3907
- } catch {
3908
- return creds.workspaces ?? [];
3909
- }
4003
+ async function boundWorkspaces2() {
4004
+ return (await boundWorkspaces())?.workspaces ?? null;
3910
4005
  }
3911
4006
  async function reconcileProfile(profile) {
3912
4007
  return runWithProfile(profile, async () => {
@@ -3933,7 +4028,7 @@ async function reconcileProfile(profile) {
3933
4028
  });
3934
4029
  }
3935
4030
  async function reconcileAll() {
3936
- const bound = hasSupabaseCredentials() ? null : await boundWorkspaces();
4031
+ const bound = hasSupabaseCredentials() ? null : await boundWorkspaces2();
3937
4032
  const slugs = new Set(bound ?? [...(await listWorkspaces()).map((w) => w.slug), ...listProfiles()]);
3938
4033
  const results = /* @__PURE__ */ new Map();
3939
4034
  for (const slug of slugs) {
@@ -3947,7 +4042,7 @@ async function reconcileAll() {
3947
4042
  return results;
3948
4043
  }
3949
4044
  async function ensureServices() {
3950
- const bound = await boundWorkspaces();
4045
+ const bound = await boundWorkspaces2();
3951
4046
  const slugs = new Set(bound ?? [...(await listWorkspaces()).map((w) => w.slug), ...listProfiles()]);
3952
4047
  slugs.delete(DEFAULT_PROFILE);
3953
4048
  const installed = [];
@@ -6961,7 +7056,7 @@ import {
6961
7056
  readFileSync as readFileSync10,
6962
7057
  realpathSync,
6963
7058
  renameSync,
6964
- rmSync as rmSync4,
7059
+ rmSync as rmSync5,
6965
7060
  statSync as statSync5,
6966
7061
  writeFileSync as writeFileSync7
6967
7062
  } from "fs";
@@ -7255,7 +7350,7 @@ import { readFileSync as readFileSync9 } from "fs";
7255
7350
  import { homedir as homedir7 } from "os";
7256
7351
 
7257
7352
  // src/mcp/config.ts
7258
- import { mkdtempSync, rmSync as rmSync3, writeFileSync as writeFileSync6 } from "fs";
7353
+ import { mkdtempSync, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "fs";
7259
7354
  import { tmpdir } from "os";
7260
7355
  import { join as join14 } from "path";
7261
7356
 
@@ -7948,7 +8043,7 @@ function writeMcpConfigFile(config) {
7948
8043
  writeFileSync6(path, JSON.stringify(config), { mode: 384 });
7949
8044
  return {
7950
8045
  path,
7951
- cleanup: () => rmSync3(dir, { recursive: true, force: true })
8046
+ cleanup: () => rmSync4(dir, { recursive: true, force: true })
7952
8047
  };
7953
8048
  }
7954
8049
  async function withMcpConfig(profile, connectionIds, fn) {
@@ -10164,7 +10259,7 @@ function deleteDocEntry(root, path) {
10164
10259
  }
10165
10260
  const isDir2 = stat.isDirectory();
10166
10261
  if (!isDir2 && !stat.isFile() && !stat.isSymbolicLink()) return { status: "not-found" };
10167
- rmSync4(abs, { recursive: isDir2 });
10262
+ rmSync5(abs, { recursive: isDir2 });
10168
10263
  return {
10169
10264
  status: "ok",
10170
10265
  path: relative(root, abs).split(sep).join("/"),
@@ -14881,7 +14976,7 @@ function loadHostPublicKey() {
14881
14976
  // src/dashboard/host-lock.ts
14882
14977
  import { execFileSync as execFileSync3 } from "child_process";
14883
14978
  import { randomBytes as randomBytes5 } from "crypto";
14884
- import { mkdirSync as mkdirSync10, readFileSync as readFileSync13, rmSync as rmSync5, writeFileSync as writeFileSync9 } from "fs";
14979
+ import { mkdirSync as mkdirSync10, readFileSync as readFileSync13, rmSync as rmSync6, writeFileSync as writeFileSync9 } from "fs";
14885
14980
  import { join as join21 } from "path";
14886
14981
  function hostLockPath() {
14887
14982
  return join21(paths.BASE_DATA_DIR, "host.lock");
@@ -14917,7 +15012,7 @@ function acquireHostLock() {
14917
15012
  try {
14918
15013
  writeFileSync9(path, body, { flag: "wx", mode: 384 });
14919
15014
  const release2 = () => {
14920
- if (readLockPid(path) === process.pid) rmSync5(path, { force: true });
15015
+ if (readLockPid(path) === process.pid) rmSync6(path, { force: true });
14921
15016
  };
14922
15017
  process.once("exit", release2);
14923
15018
  return release2;
@@ -14931,7 +15026,7 @@ function acquireHostLock() {
14931
15026
  `Another Flyspace Host is already running on this machine (pid ${holder}). Stop it first, or check for a second CLI install (\`which -a flyspace\`). Two Hosts overwrite each other's skill mirror.`
14932
15027
  );
14933
15028
  }
14934
- rmSync5(path, { force: true });
15029
+ rmSync6(path, { force: true });
14935
15030
  }
14936
15031
  }
14937
15032
  throw new Error(`Could not take the Host lock at ${path}`);
@@ -15149,16 +15244,35 @@ async function listWorkspaceDevices(workspaceId) {
15149
15244
  if (error2) fail("listWorkspaceDevices", error2);
15150
15245
  return (data ?? []).map((r) => toDevice(r.devices)).sort((a, b) => b.createdAt.localeCompare(a.createdAt));
15151
15246
  }
15247
+ async function touchDevice(deviceId) {
15248
+ const { error: error2 } = await getSupabaseClient().from("devices").update({ last_seen_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", deviceId);
15249
+ if (error2) fail("touchDevice", error2);
15250
+ }
15251
+ var STALE_HOST_MS = 14 * 24 * 60 * 6e4;
15252
+ var isStale = (lastSeenAt) => !!lastSeenAt && Date.now() - Date.parse(lastSeenAt) > STALE_HOST_MS;
15152
15253
  async function listWorkspaceHosts(workspaceIds, exceptPublicKey) {
15153
15254
  const hosts2 = /* @__PURE__ */ new Map();
15154
15255
  if (!workspaceIds.length) return hosts2;
15155
- const { data, error: error2 } = await getSupabaseClient().from("device_workspaces").select("workspace_id, devices!inner(name, public_key, revoked_at)").in("workspace_id", workspaceIds).is("devices.revoked_at", null);
15256
+ const { data, error: error2 } = await getSupabaseClient().from("device_workspaces").select("workspace_id, devices!inner(name, public_key, revoked_at, last_seen_at)").in("workspace_id", workspaceIds).is("devices.revoked_at", null);
15156
15257
  if (error2) fail("listWorkspaceHosts", error2);
15157
15258
  for (const r of data ?? []) {
15158
- if (r.devices.public_key !== exceptPublicKey) hosts2.set(r.workspace_id, r.devices.name);
15259
+ if (r.devices.public_key !== exceptPublicKey && !isStale(r.devices.last_seen_at)) {
15260
+ hosts2.set(r.workspace_id, r.devices.name);
15261
+ }
15159
15262
  }
15160
15263
  return hosts2;
15161
15264
  }
15265
+ async function releaseStaleHosts(workspaceIds, exceptPublicKey) {
15266
+ if (!workspaceIds.length) return;
15267
+ const db = getSupabaseClient();
15268
+ const { data, error: error2 } = await db.from("device_workspaces").select("device_id, workspace_id, devices!inner(public_key, last_seen_at)").in("workspace_id", workspaceIds);
15269
+ if (error2) fail("releaseStaleHosts", error2);
15270
+ for (const r of data ?? []) {
15271
+ if (r.devices.public_key === exceptPublicKey || !isStale(r.devices.last_seen_at)) continue;
15272
+ const { error: delError } = await db.from("device_workspaces").delete().eq("device_id", r.device_id).eq("workspace_id", r.workspace_id);
15273
+ if (delError) fail("releaseStaleHosts", delError);
15274
+ }
15275
+ }
15162
15276
  async function userEmail(userId) {
15163
15277
  const { data, error: error2 } = await getSupabaseClient().auth.admin.getUserById(userId);
15164
15278
  if (error2) return null;
@@ -15941,9 +16055,21 @@ async function revalidateDeviceScopes() {
15941
16055
  continue;
15942
16056
  }
15943
16057
  const workspaces = (await listDeviceWorkspaces(device)).map((w) => w.slug);
15944
- for (const scope of hosts.values()) if (scope.deviceId === deviceId) scope.workspaces = workspaces;
16058
+ for (const [ws, scope] of hosts) {
16059
+ if (scope.deviceId !== deviceId) continue;
16060
+ const changed = (scope.workspaces ?? []).join() !== workspaces.join();
16061
+ scope.workspaces = workspaces;
16062
+ if (changed) send(ws, { type: "workspaces", workspaces });
16063
+ }
16064
+ if (!device.lastSeenAt || Date.now() - Date.parse(device.lastSeenAt) > LAST_SEEN_RESOLUTION_MS) {
16065
+ await touchDevice(deviceId);
16066
+ }
15945
16067
  }
15946
16068
  }
16069
+ var LAST_SEEN_RESOLUTION_MS = 60 * 6e4;
16070
+ function minCliVersion() {
16071
+ return process.env["FLYSPACE_MIN_CLI_VERSION"]?.trim() || null;
16072
+ }
15947
16073
  function attachHostSocket(server) {
15948
16074
  const wss = new WebSocketServer({ server, path: HOST_SOCKET_PATH });
15949
16075
  const legacyPublicKey = loadHostPublicKey();
@@ -15984,10 +16110,19 @@ function attachHostSocket(server) {
15984
16110
  ws.close(1008, "bad signature");
15985
16111
  return;
15986
16112
  }
16113
+ const min = minCliVersion();
16114
+ if (min && msg.cliVersion && isOlderVersion(msg.cliVersion, min)) {
16115
+ console.error(`[host-socket] rejecting Host on CLI ${msg.cliVersion} (minimum ${min})`);
16116
+ send(ws, { type: "auth-failed", reason: "cli too old", minCliVersion: min });
16117
+ ws.close(1008, "cli too old");
16118
+ return;
16119
+ }
15987
16120
  supersedeSameIdentity(scope2, ws);
15988
16121
  lastAuthFailure = null;
15989
16122
  addHost(ws, scope2);
15990
- send(ws, { type: "auth-ok" });
16123
+ send(ws, scope2.workspaces ? { type: "auth-ok", workspaces: scope2.workspaces } : { type: "auth-ok" });
16124
+ if (scope2.deviceId) void touchDevice(scope2.deviceId).catch(() => {
16125
+ });
15991
16126
  console.log(`[host-socket] Host authenticated (authPath: ${scope2.authPath})`);
15992
16127
  if (scope2.workspaces) void markAgentConnected(scope2.workspaces);
15993
16128
  })();
@@ -16110,7 +16245,7 @@ function remoteAgentCallbacks() {
16110
16245
  }
16111
16246
  var RECONNECT_BASE_MS = 1e3;
16112
16247
  var RECONNECT_MAX_MS = 3e4;
16113
- function connectHostSocket(url, callbacks, onAuthenticated) {
16248
+ function connectHostSocket(url, callbacks, onAuthenticated, onWorkspaces) {
16114
16249
  const credentials = loadCredentials();
16115
16250
  const deviceId = credentials?.deviceId;
16116
16251
  const privateKey = deviceId ? ensureDeviceKey().privateKey : loadHostPrivateKey();
@@ -16142,11 +16277,23 @@ function connectHostSocket(url, callbacks, onAuthenticated) {
16142
16277
  if (!msg) return;
16143
16278
  if (msg.type === "challenge") {
16144
16279
  const signature = signRaw(null, Buffer.from(msg.nonce, "base64"), privateKey).toString("base64");
16145
- send(ws, deviceId ? { type: "auth", signature, deviceId } : { type: "auth", signature });
16280
+ const version = CLI_VERSION ? { cliVersion: CLI_VERSION } : {};
16281
+ send(ws, deviceId ? { type: "auth", signature, deviceId, ...version } : { type: "auth", signature, ...version });
16146
16282
  return;
16147
16283
  }
16148
16284
  if (msg.type === "auth-failed") {
16149
16285
  console.error(`[host-socket] auth rejected: ${msg.reason}`);
16286
+ if (msg.minCliVersion) {
16287
+ console.error(
16288
+ `[host-socket] This Host runs flyspace ${CLI_VERSION} but the Workspace needs ${msg.minCliVersion} or later.
16289
+ Upgrade, then restart the Host: flyspace upgrade`
16290
+ );
16291
+ stopped = true;
16292
+ }
16293
+ return;
16294
+ }
16295
+ if (msg.type === "workspaces") {
16296
+ onWorkspaces?.(msg.workspaces);
16150
16297
  return;
16151
16298
  }
16152
16299
  if (msg.type === "auth-ok") {
@@ -16158,6 +16305,7 @@ function connectHostSocket(url, callbacks, onAuthenticated) {
16158
16305
  send(ws, { type: "host-config", configs: buildHostConfigSnapshot() });
16159
16306
  }, HOST_CONFIG_PUSH_INTERVAL_MS);
16160
16307
  onAuthenticated?.();
16308
+ if (msg.workspaces) onWorkspaces?.(msg.workspaces);
16161
16309
  return;
16162
16310
  }
16163
16311
  if (msg.type === "db-result") {
@@ -16500,6 +16648,7 @@ function createDeviceAuthRouter() {
16500
16648
  if (!decided) {
16501
16649
  return c.json({ error: "That code was already used." }, 409);
16502
16650
  }
16651
+ await releaseStaleHosts(workspaceIds, pending3.publicKey);
16503
16652
  return c.json({ ok: true });
16504
16653
  });
16505
16654
  return app;
@@ -16872,7 +17021,7 @@ async function startDashboardServer(preferredPort = DEFAULT_DASHBOARD_PORT, host
16872
17021
  const bootCallbacks = new Proxy({}, {
16873
17022
  get: (_t, prop) => lateCallbacks?.[prop]
16874
17023
  });
16875
- const earlyHostSocket = mode === "host" && !hasSupabaseCredentials() && loadCredentials() ? (setSupabaseClient(createHostDbClient()), connectHostSocket(dataSocketUrl(), bootCallbacks, () => onHostConnected?.())) : void 0;
17024
+ const earlyHostSocket = mode === "host" && !hasSupabaseCredentials() && loadCredentials() ? (setSupabaseClient(createHostDbClient()), connectHostSocket(dataSocketUrl(), bootCallbacks, () => onHostConnected?.(), onBindingsChanged)) : void 0;
16876
17025
  if (servesHost) {
16877
17026
  await reconcileAll().catch(
16878
17027
  (err) => console.error("[dashboard] reconcileAll failed:", err.message)
@@ -16947,7 +17096,7 @@ The JSON API is live under /api.` : "This install has no bundled Workspace UI.\n
16947
17096
  if (mode === "host") relayToken = publishHostEndpoint(port);
16948
17097
  if (mode === "data") attachHostSocket(server);
16949
17098
  const stopHostStatusBroadcast = mode === "data" ? startHostStatusBroadcast() : void 0;
16950
- const hostSocketClient = earlyHostSocket ?? (mode === "host" && chatAgent ? connectHostSocket(dataSocketUrl(), localAgentCallbacks(chatAgent), () => onHostConnected?.()) : void 0);
17099
+ const hostSocketClient = earlyHostSocket ?? (mode === "host" && chatAgent ? connectHostSocket(dataSocketUrl(), localAgentCallbacks(chatAgent), () => onHostConnected?.(), onBindingsChanged) : void 0);
16951
17100
  const stopPushWatcher = earlyHostSocket ? () => {
16952
17101
  } : startPushWatcher();
16953
17102
  const displayHost = host === "0.0.0.0" || host === "::" ? "127.0.0.1" : host;
@@ -16968,6 +17117,13 @@ The JSON API is live under /api.` : "This install has no bundled Workspace UI.\n
16968
17117
  }
16969
17118
  };
16970
17119
  }
17120
+ function onBindingsChanged(workspaces) {
17121
+ const creds = loadCredentials();
17122
+ if (!creds || (creds.workspaces ?? []).join() === workspaces.join()) return;
17123
+ saveCredentials({ ...creds, workspaces });
17124
+ console.log(`[dashboard] now serving: ${workspaces.join(", ") || "(none)"}`);
17125
+ void reconcileAll().then(() => ensureServices()).catch((err) => console.error("[dashboard] applying new bindings failed:", err.message));
17126
+ }
16971
17127
  var SERVICE_CHECK_MS = 6e4;
16972
17128
  function watchServices() {
16973
17129
  const tick = () => void ensureServices().then((slugs) => slugs.length && console.log(`[dashboard] installed scheduler service for: ${slugs.join(", ")}`)).catch((err) => console.error("[dashboard] ensureServices failed:", err.message));
@@ -17177,7 +17333,7 @@ Done. ${totalThreads} threads, ${totalReplies} messages upserted.`);
17177
17333
  }
17178
17334
 
17179
17335
  // src/commands/slack.ts
17180
- var USAGE2 = `
17336
+ var USAGE3 = `
17181
17337
  Usage: flyspace slack <subcommand> [options]
17182
17338
 
17183
17339
  Subcommands:
@@ -18112,13 +18268,13 @@ async function runSlackCmd(args) {
18112
18268
  break;
18113
18269
  }
18114
18270
  default:
18115
- console.log(USAGE2);
18271
+ console.log(USAGE3);
18116
18272
  break;
18117
18273
  }
18118
18274
  }
18119
18275
 
18120
18276
  // src/commands/chat.ts
18121
- var USAGE3 = `
18277
+ var USAGE4 = `
18122
18278
  Usage: flyspace chat <subcommand> [options]
18123
18279
 
18124
18280
  Subcommands:
@@ -18264,17 +18420,17 @@ async function runChatCmd(args) {
18264
18420
  case "-h":
18265
18421
  case "--help":
18266
18422
  case void 0:
18267
- console.log(USAGE3);
18423
+ console.log(USAGE4);
18268
18424
  break;
18269
18425
  default:
18270
18426
  console.error(`Unknown subcommand: ${subcommand}`);
18271
- console.log(USAGE3);
18427
+ console.log(USAGE4);
18272
18428
  process.exit(1);
18273
18429
  }
18274
18430
  }
18275
18431
 
18276
18432
  // src/commands/mcp.ts
18277
- var USAGE4 = `
18433
+ var USAGE5 = `
18278
18434
  Usage: flyspace mcp <server>
18279
18435
 
18280
18436
  Run one of Flyspace's own MCP servers on stdio. Spawned by the agent runtime
@@ -18291,7 +18447,7 @@ Usage: flyspace mcp <server>
18291
18447
  async function runMcpCmd(args) {
18292
18448
  const server = args[0];
18293
18449
  if (!server || server === "-h" || server === "--help") {
18294
- console.log(USAGE4.trim());
18450
+ console.log(USAGE5.trim());
18295
18451
  return;
18296
18452
  }
18297
18453
  restoreStdout();
@@ -18304,7 +18460,7 @@ async function runMcpCmd(args) {
18304
18460
  }
18305
18461
  default:
18306
18462
  console.error(`Unknown MCP server: ${server}`);
18307
- console.error(USAGE4.trim());
18463
+ console.error(USAGE5.trim());
18308
18464
  process.exit(1);
18309
18465
  }
18310
18466
  }
@@ -18380,7 +18536,7 @@ async function runDashboard(args) {
18380
18536
 
18381
18537
  // src/commands/workspace.ts
18382
18538
  import { existsSync as existsSync29, mkdirSync as mkdirSync11, writeFileSync as writeFileSync11 } from "fs";
18383
- var USAGE5 = `
18539
+ var USAGE6 = `
18384
18540
  Usage: flyspace workspace <subcommand> [options]
18385
18541
 
18386
18542
  Subcommands:
@@ -18459,7 +18615,7 @@ async function bindThisHost(workspaceId) {
18459
18615
  return;
18460
18616
  }
18461
18617
  await bindDeviceWorkspace(device.id, workspaceId, device.userId);
18462
- console.log(` bound to Host "${device.name}" (restart it to pick up the new workspace)`);
18618
+ console.log(` bound to Host "${device.name}" (a running Host picks it up within a minute)`);
18463
18619
  }
18464
18620
  function provisionLocal(botName) {
18465
18621
  ensureDirs();
@@ -18519,13 +18675,13 @@ async function runWorkspaceCmd(args) {
18519
18675
  await handleCreate(args.slice(1));
18520
18676
  break;
18521
18677
  default:
18522
- console.log(USAGE5);
18678
+ console.log(USAGE6);
18523
18679
  process.exit(args[0] ? 1 : 0);
18524
18680
  }
18525
18681
  }
18526
18682
 
18527
18683
  // src/commands/reconcile.ts
18528
- var USAGE6 = `
18684
+ var USAGE7 = `
18529
18685
  Usage: flyspace reconcile [<workspace> | --all]
18530
18686
 
18531
18687
  Repair a workspace's local filesystem state (profile dir, config.env,
@@ -18545,7 +18701,7 @@ function printLog(name, log5) {
18545
18701
  }
18546
18702
  async function runReconcileCmd(args) {
18547
18703
  if (args.includes("-h") || args.includes("--help")) {
18548
- console.log(USAGE6);
18704
+ console.log(USAGE7);
18549
18705
  return;
18550
18706
  }
18551
18707
  loadEnvFiles();
@@ -18561,7 +18717,7 @@ async function runReconcileCmd(args) {
18561
18717
 
18562
18718
  // src/cli.ts
18563
18719
  var XERO_USAGE = false ? " xero Connect to Xero and expose it as an MCP server\n" : "";
18564
- var USAGE7 = `
18720
+ var USAGE8 = `
18565
18721
  Usage: flyspace <command> [options]
18566
18722
 
18567
18723
  Commands:
@@ -18580,7 +18736,7 @@ var USAGE7 = `
18580
18736
  usage Show agent token usage and cost for this workspace
18581
18737
  slack Query Slack (channels, users, post messages)
18582
18738
  chat Inspect dashboard chat channels and move threads between them
18583
- profiles List instances and their service status
18739
+ profiles List local profiles (bound here? scheduler?) or remove one
18584
18740
  workspace Provision a workspace (Supabase rows + local profile dirs)
18585
18741
  reconcile Repair a workspace's local files against Supabase
18586
18742
  ${XERO_USAGE} dashboard Start the read-only management dashboard (web UI)
@@ -18668,8 +18824,9 @@ async function main() {
18668
18824
  case "chat":
18669
18825
  await runChatCmd(args.slice(1));
18670
18826
  break;
18827
+ case "profile":
18671
18828
  case "profiles":
18672
- await runProfiles();
18829
+ await runProfiles(args.slice(1));
18673
18830
  break;
18674
18831
  case "dashboard":
18675
18832
  await runDashboard(args.slice(1));
@@ -18679,12 +18836,12 @@ async function main() {
18679
18836
  break;
18680
18837
  case "-v":
18681
18838
  case "--version":
18682
- printRaw("0.1.6");
18839
+ printRaw("0.1.7");
18683
18840
  break;
18684
18841
  case "-h":
18685
18842
  case "--help":
18686
18843
  case void 0:
18687
- printRaw(USAGE7);
18844
+ printRaw(USAGE8);
18688
18845
  break;
18689
18846
  case "xero":
18690
18847
  if (false) {
@@ -18695,7 +18852,7 @@ async function main() {
18695
18852
  // falls through
18696
18853
  default:
18697
18854
  printRaw(`Unknown command: ${command}`);
18698
- printRaw(USAGE7);
18855
+ printRaw(USAGE8);
18699
18856
  process.exit(1);
18700
18857
  }
18701
18858
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flyspace-cli",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "AI-native headless community OS",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "private": false,