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.
- package/dist/cli.js +230 -73
- 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
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3901
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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: () =>
|
|
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
|
-
|
|
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
|
|
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)
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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(
|
|
18271
|
+
console.log(USAGE3);
|
|
18116
18272
|
break;
|
|
18117
18273
|
}
|
|
18118
18274
|
}
|
|
18119
18275
|
|
|
18120
18276
|
// src/commands/chat.ts
|
|
18121
|
-
var
|
|
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(
|
|
18423
|
+
console.log(USAGE4);
|
|
18268
18424
|
break;
|
|
18269
18425
|
default:
|
|
18270
18426
|
console.error(`Unknown subcommand: ${subcommand}`);
|
|
18271
|
-
console.log(
|
|
18427
|
+
console.log(USAGE4);
|
|
18272
18428
|
process.exit(1);
|
|
18273
18429
|
}
|
|
18274
18430
|
}
|
|
18275
18431
|
|
|
18276
18432
|
// src/commands/mcp.ts
|
|
18277
|
-
var
|
|
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(
|
|
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(
|
|
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
|
|
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}" (
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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.
|
|
18839
|
+
printRaw("0.1.7");
|
|
18683
18840
|
break;
|
|
18684
18841
|
case "-h":
|
|
18685
18842
|
case "--help":
|
|
18686
18843
|
case void 0:
|
|
18687
|
-
printRaw(
|
|
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(
|
|
18855
|
+
printRaw(USAGE8);
|
|
18699
18856
|
process.exit(1);
|
|
18700
18857
|
}
|
|
18701
18858
|
}
|