flyspace-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 (2) hide show
  1. package/dist/cli.js +763 -481
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -430,13 +430,352 @@ async function runConfig(args) {
430
430
  }
431
431
 
432
432
  // src/commands/setup.ts
433
- import { mkdirSync as mkdirSync4 } from "fs";
433
+ import { execSync as execSync4 } from "child_process";
434
+ import { mkdirSync as mkdirSync5 } from "fs";
435
+ import { join as join7 } from "path";
436
+ import { confirm as confirm2 } from "@inquirer/prompts";
437
+
438
+ // src/service.ts
439
+ import { execSync } from "child_process";
440
+ import { existsSync as existsSync3 } from "fs";
441
+ import { join as join2 } from "path";
442
+ import { homedir, platform } from "os";
443
+ function serviceLabel(profile) {
444
+ return profile === DEFAULT_PROFILE ? "ai.flyspace" : `ai.flyspace.${profile}`;
445
+ }
446
+ function plistName(profile) {
447
+ return `${serviceLabel(profile)}.plist`;
448
+ }
449
+ function plistPath(profile) {
450
+ return join2(homedir(), "Library", "LaunchAgents", plistName(profile));
451
+ }
452
+ function legacyServiceLabel(profile) {
453
+ return profile === DEFAULT_PROFILE ? "io.flycoder.flyspace" : `io.flycoder.flyspace.${profile}`;
454
+ }
455
+ function legacyPlistPath(profile) {
456
+ return join2(homedir(), "Library", "LaunchAgents", `${legacyServiceLabel(profile)}.plist`);
457
+ }
458
+ function systemdUnitName(profile) {
459
+ return profile === DEFAULT_PROFILE ? "flyspace" : `flyspace-${profile}`;
460
+ }
461
+ function systemdUnitPath(profile) {
462
+ return join2(homedir(), ".config", "systemd", "user", `${systemdUnitName(profile)}.service`);
463
+ }
464
+ function isServiceInstalled(profile) {
465
+ if (platform() === "darwin") return existsSync3(plistPath(profile));
466
+ if (platform() === "linux") return existsSync3(systemdUnitPath(profile));
467
+ return false;
468
+ }
469
+ function restartService(profile) {
470
+ const os = platform();
471
+ if (os === "darwin") {
472
+ const plist = plistPath(profile);
473
+ try {
474
+ execSync(`launchctl unload "${plist}"`, { stdio: "pipe" });
475
+ } catch {
476
+ }
477
+ try {
478
+ execSync(`launchctl load "${plist}"`, { stdio: "pipe" });
479
+ return true;
480
+ } catch {
481
+ return false;
482
+ }
483
+ }
484
+ if (os === "linux") {
485
+ try {
486
+ execSync(`systemctl --user restart ${systemdUnitName(profile)}`, { stdio: "pipe" });
487
+ return true;
488
+ } catch {
489
+ return false;
490
+ }
491
+ }
492
+ return false;
493
+ }
494
+ var HOST_SERVICE_LABEL = "ai.flyspace.dashboard";
495
+ var HOST_SYSTEMD_UNIT = "flyspace-host";
496
+ function hostPlistPath() {
497
+ return join2(homedir(), "Library", "LaunchAgents", `${HOST_SERVICE_LABEL}.plist`);
498
+ }
499
+ function isHostServiceInstalled() {
500
+ if (platform() === "darwin") return existsSync3(hostPlistPath());
501
+ if (platform() === "linux") {
502
+ return existsSync3(join2(homedir(), ".config", "systemd", "user", `${HOST_SYSTEMD_UNIT}.service`));
503
+ }
504
+ return false;
505
+ }
506
+ function restartHostService() {
507
+ const os = platform();
508
+ try {
509
+ if (os === "darwin") {
510
+ execSync(`launchctl kickstart -k gui/${process.getuid?.()}/${HOST_SERVICE_LABEL}`, { stdio: "pipe" });
511
+ return true;
512
+ }
513
+ if (os === "linux") {
514
+ execSync(`systemctl --user restart ${HOST_SYSTEMD_UNIT}`, { stdio: "pipe" });
515
+ return true;
516
+ }
517
+ } catch {
518
+ }
519
+ return false;
520
+ }
521
+
522
+ // src/commands/install.ts
523
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync3, unlinkSync } from "fs";
524
+ import { execSync as execSync2 } from "child_process";
525
+ import { dirname as dirname2, join as join3, resolve as resolve3 } from "path";
526
+ import { homedir as homedir2, platform as platform2 } from "os";
527
+ function log(msg) {
528
+ console.log(`
529
+ \x1B[36m>\x1B[0m ${msg}`);
530
+ }
531
+ function success(msg) {
532
+ console.log(`\x1B[32m+\x1B[0m ${msg}`);
533
+ }
534
+ function warn(msg) {
535
+ console.log(`\x1B[33m!\x1B[0m ${msg}`);
536
+ }
537
+ function run(cmd, opts = {}) {
538
+ return execSync2(cmd, {
539
+ encoding: "utf-8",
540
+ stdio: opts.silent ? "pipe" : "inherit"
541
+ }).toString().trim();
542
+ }
543
+ function detectClaudeBinDir() {
544
+ try {
545
+ const claudePath = execSync2("which claude", { encoding: "utf-8", stdio: "pipe" }).trim();
546
+ return claudePath ? dirname2(claudePath) : void 0;
547
+ } catch {
548
+ return void 0;
549
+ }
550
+ }
551
+ function detectCustieBin() {
552
+ const argv1 = process.argv[1];
553
+ if (argv1) {
554
+ return resolve3(argv1);
555
+ }
556
+ try {
557
+ return run("which flyspace", { silent: true });
558
+ } catch {
559
+ return "flyspace";
560
+ }
561
+ }
562
+ function readEnvFile() {
563
+ if (!existsSync4(paths.CONFIG_FILE)) return {};
564
+ const vars = {};
565
+ for (const line of readFileSync3(paths.CONFIG_FILE, "utf-8").split("\n")) {
566
+ const trimmed = line.trim();
567
+ if (!trimmed || trimmed.startsWith("#")) continue;
568
+ const eq = trimmed.indexOf("=");
569
+ if (eq === -1) continue;
570
+ const key = trimmed.slice(0, eq).trim();
571
+ const val = trimmed.slice(eq + 1).trim();
572
+ if (val) vars[key] = val;
573
+ }
574
+ return vars;
575
+ }
576
+ function escapeXml(str) {
577
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
578
+ }
579
+ function profileSpec() {
580
+ const profile = getProfile();
581
+ return {
582
+ label: serviceLabel(profile),
583
+ plistPath: plistPath(profile),
584
+ unitName: systemdUnitName(profile),
585
+ description: `Flyspace agent host${profile === "default" ? "" : ` (${profile})`}`,
586
+ // The default profile keeps the bare `start` args so its plist stays
587
+ // byte-for-byte identical to pre-profile installs (no reinstall needed).
588
+ args: profile === "default" ? ["start"] : ["start", "--profile", profile],
589
+ env: readEnvFile(),
590
+ logName: "custie"
591
+ };
592
+ }
593
+ function hostSpec() {
594
+ return {
595
+ label: HOST_SERVICE_LABEL,
596
+ plistPath: hostPlistPath(),
597
+ unitName: HOST_SYSTEMD_UNIT,
598
+ description: "Flyspace agent Host",
599
+ args: ["dashboard", "--mode", "host", "--no-open"],
600
+ env: {},
601
+ logName: "host"
602
+ };
603
+ }
604
+ async function installMacOS(spec) {
605
+ log("Setting up macOS LaunchAgent...");
606
+ const custieBin = detectCustieBin();
607
+ const nodePath = process.execPath;
608
+ const claudeBinDir = detectClaudeBinDir();
609
+ const logDir = paths.LOG_DIR;
610
+ mkdirSync2(logDir, { recursive: true });
611
+ const envVars = spec.env;
612
+ const homebrewBin = existsSync4("/opt/homebrew/bin") ? "/opt/homebrew/bin" : void 0;
613
+ const asdfShims = join3(homedir2(), ".asdf", "shims");
614
+ const asdfBin = existsSync4(asdfShims) ? asdfShims : void 0;
615
+ const pathParts = [
616
+ dirname2(nodePath),
617
+ claudeBinDir,
618
+ asdfBin,
619
+ homebrewBin,
620
+ "/usr/local/bin",
621
+ "/usr/bin",
622
+ "/bin"
623
+ ].filter(Boolean);
624
+ const programArgsXml = [custieBin, ...spec.args].map((a) => ` <string>${escapeXml(a)}</string>`).join("\n");
625
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
626
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
627
+ "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
628
+ <plist version="1.0">
629
+ <dict>
630
+ <key>Label</key>
631
+ <string>${spec.label}</string>
632
+
633
+ <key>ProgramArguments</key>
634
+ <array>
635
+ ${programArgsXml}
636
+ </array>
637
+
638
+ <key>EnvironmentVariables</key>
639
+ <dict>
640
+ <key>PATH</key>
641
+ <string>${pathParts.join(":")}</string>
642
+ ${Object.entries(envVars).map(([k, v]) => ` <key>${k}</key>
643
+ <string>${escapeXml(v)}</string>`).join("\n")}
644
+ </dict>
645
+
646
+ <key>RunAtLoad</key>
647
+ <true/>
648
+
649
+ <key>KeepAlive</key>
650
+ <true/>
651
+
652
+ <key>StandardOutPath</key>
653
+ <string>${join3(logDir, `${spec.logName}.log`)}</string>
654
+
655
+ <key>StandardErrorPath</key>
656
+ <string>${join3(logDir, `${spec.logName}-error.log`)}</string>
657
+ </dict>
658
+ </plist>`;
659
+ const agentPlistPath = spec.plistPath;
660
+ mkdirSync2(dirname2(agentPlistPath), { recursive: true });
661
+ try {
662
+ run(`launchctl unload "${agentPlistPath}"`, { silent: true });
663
+ } catch {
664
+ }
665
+ try {
666
+ unlinkSync(agentPlistPath);
667
+ } catch {
668
+ }
669
+ writeFileSync3(agentPlistPath, plist);
670
+ run(`launchctl load "${agentPlistPath}"`, { silent: true });
671
+ success(`LaunchAgent installed and loaded (${spec.label}).`);
672
+ console.log(` Logs: ${logDir}`);
673
+ console.log(` Check: launchctl list | grep flyspace`);
674
+ }
675
+ async function installLinux(spec) {
676
+ log("Setting up systemd user service...");
677
+ const custieBin = detectCustieBin();
678
+ const nodePath = process.execPath;
679
+ const claudeBinDir = detectClaudeBinDir();
680
+ const logDir = paths.LOG_DIR;
681
+ mkdirSync2(logDir, { recursive: true });
682
+ const envLines = Object.entries(spec.env).map(([k, v]) => `Environment="${k}=${v}"`).join("\n");
683
+ const linuxbrewBin = existsSync4("/home/linuxbrew/.linuxbrew/bin") ? "/home/linuxbrew/.linuxbrew/bin" : void 0;
684
+ const pathParts = [
685
+ dirname2(nodePath),
686
+ claudeBinDir,
687
+ linuxbrewBin,
688
+ "/usr/local/bin",
689
+ "/usr/bin",
690
+ "/bin"
691
+ ].filter(Boolean);
692
+ const execStart = [custieBin, ...spec.args].join(" ");
693
+ const unit = `[Unit]
694
+ Description=${spec.description}
695
+ After=network-online.target
696
+ Wants=network-online.target
697
+
698
+ [Service]
699
+ Type=simple
700
+ ExecStart=${execStart}
701
+ Restart=on-failure
702
+ RestartSec=5
703
+ ${envLines}
704
+ Environment="PATH=${pathParts.join(":")}"
705
+
706
+ StandardOutput=append:${join3(logDir, `${spec.logName}.log`)}
707
+ StandardError=append:${join3(logDir, `${spec.logName}-error.log`)}
708
+
709
+ [Install]
710
+ WantedBy=default.target
711
+ `;
712
+ const unitName = spec.unitName;
713
+ const unitDir = join3(homedir2(), ".config", "systemd", "user");
714
+ mkdirSync2(unitDir, { recursive: true });
715
+ writeFileSync3(join3(unitDir, `${unitName}.service`), unit);
716
+ run("systemctl --user daemon-reload", { silent: true });
717
+ run(`systemctl --user enable --now ${unitName}`, { silent: true });
718
+ success(`systemd user service installed and started (${unitName}).`);
719
+ console.log(` Logs: ${logDir}`);
720
+ console.log(` Check: systemctl --user status ${unitName}`);
721
+ }
722
+ async function installService(spec) {
723
+ const os = platform2();
724
+ if (os === "darwin") await installMacOS(spec);
725
+ else if (os === "linux") await installLinux(spec);
726
+ else {
727
+ warn(`Unsupported platform: ${os}. You'll need to configure autostart manually.`);
728
+ return false;
729
+ }
730
+ return true;
731
+ }
732
+ async function runInstall() {
733
+ ensureDirs();
734
+ await installService(profileSpec());
735
+ }
736
+ async function installHostService() {
737
+ ensureDirs();
738
+ return installService(hostSpec());
739
+ }
740
+
741
+ // src/commands/profiles.ts
742
+ import { existsSync as existsSync5, readdirSync, statSync } from "fs";
743
+ import { execSync as execSync3 } from "child_process";
434
744
  import { join as join4 } from "path";
745
+ import { platform as platform3 } from "os";
746
+ function listProfiles() {
747
+ const profiles = [];
748
+ if (existsSync5(paths.PROFILES_DIR)) {
749
+ for (const entry of readdirSync(paths.PROFILES_DIR)) {
750
+ try {
751
+ if (statSync(join4(paths.PROFILES_DIR, entry)).isDirectory()) {
752
+ profiles.push(entry);
753
+ }
754
+ } catch {
755
+ }
756
+ }
757
+ }
758
+ return profiles;
759
+ }
760
+ function isServiceRunning(profile) {
761
+ try {
762
+ if (platform3() === "darwin") {
763
+ const out = execSync3("launchctl list", { encoding: "utf-8", stdio: "pipe" });
764
+ return out.split("\n").some((line) => line.endsWith(` ${serviceLabel(profile)}`));
765
+ }
766
+ if (platform3() === "linux") {
767
+ execSync3(`systemctl --user is-active ${systemdUnitName(profile)}`, { stdio: "pipe" });
768
+ return true;
769
+ }
770
+ } catch {
771
+ }
772
+ return false;
773
+ }
435
774
 
436
775
  // src/auth/device-auth.ts
437
776
  import { createPrivateKey, createPublicKey, generateKeyPairSync, sign } from "crypto";
438
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
439
- import { dirname as dirname2, join as join2 } from "path";
777
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync4, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
778
+ import { dirname as dirname3, join as join5 } from "path";
440
779
  var DEFAULT_API_URL = "https://app.flyspace.ai";
441
780
  var EXPIRY_SKEW_MS = 6e4;
442
781
  var AuthApiError = class extends Error {
@@ -450,36 +789,36 @@ function apiUrl() {
450
789
  return (process.env["FLYSPACE_API_URL"] || DEFAULT_API_URL).replace(/\/+$/, "");
451
790
  }
452
791
  function deviceKeyPath() {
453
- return join2(paths.BASE_CONFIG_DIR, "device.key");
792
+ return join5(paths.BASE_CONFIG_DIR, "device.key");
454
793
  }
455
794
  function credentialsPath() {
456
- return join2(paths.BASE_CONFIG_DIR, "credentials.json");
795
+ return join5(paths.BASE_CONFIG_DIR, "credentials.json");
457
796
  }
458
797
  function ensureDeviceKey(path = deviceKeyPath()) {
459
798
  let created = false;
460
- if (!existsSync3(path)) {
799
+ if (!existsSync6(path)) {
461
800
  const { privateKey: privateKey2 } = generateKeyPairSync("ed25519");
462
- mkdirSync2(dirname2(path), { recursive: true });
463
- writeFileSync3(path, privateKey2.export({ type: "pkcs8", format: "pem" }), { mode: 384 });
801
+ mkdirSync3(dirname3(path), { recursive: true });
802
+ writeFileSync4(path, privateKey2.export({ type: "pkcs8", format: "pem" }), { mode: 384 });
464
803
  created = true;
465
804
  }
466
- const privateKey = createPrivateKey(readFileSync3(path, "utf8"));
805
+ const privateKey = createPrivateKey(readFileSync4(path, "utf8"));
467
806
  const publicKeyBase64 = createPublicKey(privateKey).export({ type: "spki", format: "der" }).toString(
468
807
  "base64"
469
808
  );
470
809
  return { privateKey, publicKeyBase64, created };
471
810
  }
472
811
  function loadCredentials(path = credentialsPath()) {
473
- if (!existsSync3(path)) return null;
812
+ if (!existsSync6(path)) return null;
474
813
  try {
475
- return JSON.parse(readFileSync3(path, "utf8"));
814
+ return JSON.parse(readFileSync4(path, "utf8"));
476
815
  } catch {
477
816
  return null;
478
817
  }
479
818
  }
480
819
  function saveCredentials(creds, path = credentialsPath()) {
481
- mkdirSync2(dirname2(path), { recursive: true });
482
- writeFileSync3(path, `${JSON.stringify(creds, null, 2)}
820
+ mkdirSync3(dirname3(path), { recursive: true });
821
+ writeFileSync4(path, `${JSON.stringify(creds, null, 2)}
483
822
  `, { mode: 384 });
484
823
  }
485
824
  function clearLocalAuth() {
@@ -585,6 +924,17 @@ async function fetchWhoAmI(base, token) {
585
924
  workspaces: toSlugs(json["workspaces"])
586
925
  };
587
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
+ }
588
938
  async function revokeDevice(base, token) {
589
939
  await expectOk(await post(base, "/api/auth/device/revoke", {}, token), "Revoking this device");
590
940
  }
@@ -603,20 +953,20 @@ async function getAccessToken() {
603
953
  // src/dashboard/onboarding-scan.ts
604
954
  import { execFileSync } from "child_process";
605
955
  import {
606
- existsSync as existsSync4,
956
+ existsSync as existsSync7,
607
957
  lstatSync,
608
- mkdirSync as mkdirSync3,
609
- readdirSync,
610
- readFileSync as readFileSync4,
611
- statSync,
958
+ mkdirSync as mkdirSync4,
959
+ readdirSync as readdirSync2,
960
+ readFileSync as readFileSync5,
961
+ statSync as statSync2,
612
962
  symlinkSync
613
963
  } from "fs";
614
- import { homedir } from "os";
615
- import { basename, join as join3, resolve as resolve3 } from "path";
964
+ import { homedir as homedir3 } from "os";
965
+ import { basename, join as join6, resolve as resolve4 } from "path";
616
966
  var MAX_JSONL_HEAD_BYTES = 64 * 1024;
617
967
  var GIT_SCAN_ROOTS = ["Workspaces", "Projects", "code"];
618
968
  function flyspaceRoot() {
619
- return process.env["FLYSPACE_ROOT"] || join3(homedir(), "Flyspace");
969
+ return process.env["FLYSPACE_ROOT"] || join6(homedir3(), "Flyspace");
620
970
  }
621
971
  function onPath(cmd) {
622
972
  try {
@@ -627,16 +977,16 @@ function onPath(cmd) {
627
977
  }
628
978
  }
629
979
  function claudeDir() {
630
- return process.env["CLAUDE_CONFIG_DIR"] || join3(homedir(), ".claude");
980
+ return process.env["CLAUDE_CONFIG_DIR"] || join6(homedir3(), ".claude");
631
981
  }
632
982
  function codexDir() {
633
- return process.env["CODEX_HOME"] || join3(homedir(), ".codex");
983
+ return process.env["CODEX_HOME"] || join6(homedir3(), ".codex");
634
984
  }
635
985
  function claudeLoggedIn() {
636
986
  if (process.env["ANTHROPIC_API_KEY"]) return true;
637
- if (existsSync4(join3(claudeDir(), ".credentials.json"))) return true;
987
+ if (existsSync7(join6(claudeDir(), ".credentials.json"))) return true;
638
988
  try {
639
- const raw = JSON.parse(readFileSync4(join3(homedir(), ".claude.json"), "utf-8"));
989
+ const raw = JSON.parse(readFileSync5(join6(homedir3(), ".claude.json"), "utf-8"));
640
990
  if (raw.oauthAccount) return true;
641
991
  } catch {
642
992
  }
@@ -651,23 +1001,23 @@ function claudeLoggedIn() {
651
1001
  }
652
1002
  }
653
1003
  function codexLoggedIn() {
654
- return !!process.env["OPENAI_API_KEY"] || existsSync4(join3(codexDir(), "auth.json"));
1004
+ return !!process.env["OPENAI_API_KEY"] || existsSync7(join6(codexDir(), "auth.json"));
655
1005
  }
656
1006
  function jsonlFiles(dir, depth) {
657
1007
  const out = [];
658
1008
  let entries2;
659
1009
  try {
660
- entries2 = readdirSync(dir, { withFileTypes: true });
1010
+ entries2 = readdirSync2(dir, { withFileTypes: true });
661
1011
  } catch {
662
1012
  return out;
663
1013
  }
664
1014
  for (const e of entries2) {
665
- const full = join3(dir, e.name);
1015
+ const full = join6(dir, e.name);
666
1016
  if (e.isDirectory()) {
667
1017
  if (depth > 0) out.push(...jsonlFiles(full, depth - 1));
668
1018
  } else if (e.name.endsWith(".jsonl")) {
669
1019
  try {
670
- out.push({ file: full, mtime: statSync(full).mtimeMs });
1020
+ out.push({ file: full, mtime: statSync2(full).mtimeMs });
671
1021
  } catch {
672
1022
  }
673
1023
  }
@@ -677,7 +1027,7 @@ function jsonlFiles(dir, depth) {
677
1027
  function cwdFromTranscript(file) {
678
1028
  let head;
679
1029
  try {
680
- head = readFileSync4(file, { encoding: "utf-8", flag: "r" }).slice(0, MAX_JSONL_HEAD_BYTES);
1030
+ head = readFileSync5(file, { encoding: "utf-8", flag: "r" }).slice(0, MAX_JSONL_HEAD_BYTES);
681
1031
  } catch {
682
1032
  return null;
683
1033
  }
@@ -694,38 +1044,38 @@ function cwdFromTranscript(file) {
694
1044
  }
695
1045
  function codexTrustedPaths() {
696
1046
  try {
697
- const toml = readFileSync4(join3(codexDir(), "config.toml"), "utf-8");
1047
+ const toml = readFileSync5(join6(codexDir(), "config.toml"), "utf-8");
698
1048
  return [...toml.matchAll(/^\[projects\.(?:"([^"]+)"|'([^']+)')\]/gm)].map((m) => m[1] ?? m[2]);
699
1049
  } catch {
700
1050
  return [];
701
1051
  }
702
1052
  }
703
1053
  function isExcluded(path) {
704
- const home = homedir();
1054
+ const home = homedir3();
705
1055
  if (path === home || path === "/") return true;
706
1056
  if (path === "/tmp" || path.startsWith("/tmp/")) return true;
707
1057
  if (path === "/private/tmp" || path.startsWith("/private/tmp/")) return true;
708
1058
  const root = flyspaceRoot();
709
1059
  if (path === root || path.startsWith(`${root}/`)) return true;
710
1060
  try {
711
- return !statSync(path).isDirectory();
1061
+ return !statSync2(path).isDirectory();
712
1062
  } catch {
713
1063
  return true;
714
1064
  }
715
1065
  }
716
1066
  function gitRepos() {
717
- const home = homedir();
1067
+ const home = homedir3();
718
1068
  const found = [];
719
1069
  for (const rootName of GIT_SCAN_ROOTS) {
720
- const root = join3(home, rootName);
1070
+ const root = join6(home, rootName);
721
1071
  let entries2;
722
1072
  try {
723
- entries2 = readdirSync(root, { withFileTypes: true });
1073
+ entries2 = readdirSync2(root, { withFileTypes: true });
724
1074
  } catch {
725
1075
  continue;
726
1076
  }
727
1077
  for (const e of entries2) {
728
- if (e.isDirectory() && existsSync4(join3(root, e.name, ".git"))) found.push(join3(root, e.name));
1078
+ if (e.isDirectory() && existsSync7(join6(root, e.name, ".git"))) found.push(join6(root, e.name));
729
1079
  }
730
1080
  }
731
1081
  return found;
@@ -750,7 +1100,7 @@ function scanOnboarding() {
750
1100
  const byPath = /* @__PURE__ */ new Map();
751
1101
  const history = { claude: false, codex: false };
752
1102
  const add = (rawPath, source, mtime, sessions, trusted = false) => {
753
- const path = resolve3(rawPath);
1103
+ const path = resolve4(rawPath);
754
1104
  if (isExcluded(path)) return;
755
1105
  const existing = byPath.get(path);
756
1106
  if (existing) {
@@ -769,11 +1119,11 @@ function scanOnboarding() {
769
1119
  });
770
1120
  }
771
1121
  };
772
- const claudeProjects = join3(claudeDir(), "projects");
1122
+ const claudeProjects = join6(claudeDir(), "projects");
773
1123
  try {
774
- for (const e of readdirSync(claudeProjects, { withFileTypes: true })) {
1124
+ for (const e of readdirSync2(claudeProjects, { withFileTypes: true })) {
775
1125
  if (!e.isDirectory()) continue;
776
- const files = jsonlFiles(join3(claudeProjects, e.name), 0);
1126
+ const files = jsonlFiles(join6(claudeProjects, e.name), 0);
777
1127
  if (!files.length) continue;
778
1128
  history.claude = true;
779
1129
  files.sort((a, b) => b.mtime - a.mtime);
@@ -786,7 +1136,7 @@ function scanOnboarding() {
786
1136
  }
787
1137
  } catch {
788
1138
  }
789
- const codexSessions = jsonlFiles(join3(codexDir(), "sessions"), 4);
1139
+ const codexSessions = jsonlFiles(join6(codexDir(), "sessions"), 4);
790
1140
  if (codexSessions.length) history.codex = true;
791
1141
  const perCwd = /* @__PURE__ */ new Map();
792
1142
  for (const f of codexSessions) {
@@ -806,7 +1156,7 @@ function scanOnboarding() {
806
1156
  for (const repo of gitRepos()) {
807
1157
  let mtime = 0;
808
1158
  try {
809
- mtime = statSync(repo).mtimeMs;
1159
+ mtime = statSync2(repo).mtimeMs;
810
1160
  } catch {
811
1161
  }
812
1162
  add(repo, "git", mtime, 0);
@@ -828,12 +1178,12 @@ function makeLink(target, linkPath) {
828
1178
  }
829
1179
  function linkProjectFolders(slug, items) {
830
1180
  if (!/^[a-z0-9][a-z0-9-]*$/.test(slug)) throw new Error(`invalid workspace slug: ${slug}`);
831
- const dir = join3(flyspaceRoot(), slug);
832
- mkdirSync3(dir, { recursive: true });
1181
+ const dir = join6(flyspaceRoot(), slug);
1182
+ mkdirSync4(dir, { recursive: true });
833
1183
  const results = [];
834
1184
  for (const item of items) {
835
1185
  if (!/^[a-z0-9][a-z0-9-]*$/.test(item.name)) throw new Error(`invalid channel name: ${item.name}`);
836
- const dest = join3(dir, item.name);
1186
+ const dest = join6(dir, item.name);
837
1187
  let exists = true;
838
1188
  try {
839
1189
  lstatSync(dest);
@@ -842,11 +1192,11 @@ function linkProjectFolders(slug, items) {
842
1192
  }
843
1193
  if (!exists) {
844
1194
  if (item.path) {
845
- const target = resolve3(item.path);
846
- if (!statSync(target).isDirectory()) throw new Error(`not a directory: ${target}`);
1195
+ const target = resolve4(item.path);
1196
+ if (!statSync2(target).isDirectory()) throw new Error(`not a directory: ${target}`);
847
1197
  makeLink(target, dest);
848
1198
  } else {
849
- mkdirSync3(dest, { recursive: true });
1199
+ mkdirSync4(dest, { recursive: true });
850
1200
  }
851
1201
  }
852
1202
  results.push({ name: item.name, cwd: `${slug}/${item.name}` });
@@ -857,7 +1207,7 @@ function linkProjectFolders(slug, items) {
857
1207
  // src/commands/auth.ts
858
1208
  import { spawn as spawn3 } from "child_process";
859
1209
  import { confirm } from "@inquirer/prompts";
860
- import { hostname, platform, release } from "os";
1210
+ import { hostname, platform as platform4, release } from "os";
861
1211
  var BOLD = "\x1B[1m";
862
1212
  var DIM = "\x1B[2m";
863
1213
  var GREEN = "\x1B[32m";
@@ -891,8 +1241,8 @@ function defaultDeviceName() {
891
1241
  return hostname().replace(/\.local$/, "");
892
1242
  }
893
1243
  function platformLabel() {
894
- if (platform() === "darwin") return `macOS (Darwin ${release()})`;
895
- return `${platform()} ${release()}`;
1244
+ if (platform4() === "darwin") return `macOS (Darwin ${release()})`;
1245
+ return `${platform4()} ${release()}`;
896
1246
  }
897
1247
  function formatCountdown(ms) {
898
1248
  const s = Math.max(0, Math.round(ms / 1e3));
@@ -900,7 +1250,7 @@ function formatCountdown(ms) {
900
1250
  }
901
1251
  function openBrowser(url) {
902
1252
  if (process.env["SSH_CONNECTION"] || process.env["SSH_TTY"]) return false;
903
- const cmd = platform() === "darwin" ? "open" : platform() === "win32" ? "explorer" : "xdg-open";
1253
+ const cmd = platform4() === "darwin" ? "open" : platform4() === "win32" ? "explorer" : "xdg-open";
904
1254
  try {
905
1255
  const child = spawn3(cmd, [url], { stdio: "ignore", detached: true });
906
1256
  child.on("error", () => {
@@ -1146,350 +1496,172 @@ async function runAuthCmd(args) {
1146
1496
  await login(args.slice(1));
1147
1497
  break;
1148
1498
  case "status":
1149
- await runStatus();
1150
- break;
1151
- case "logout":
1152
- await runLogout(args.slice(1));
1153
- break;
1154
- case "-h":
1155
- case "--help":
1156
- case void 0:
1157
- console.log(HELP);
1158
- break;
1159
- default:
1160
- console.error(`Unknown auth command: ${sub}`);
1161
- console.log(HELP);
1162
- process.exit(1);
1163
- }
1164
- }
1165
-
1166
- // src/commands/setup.ts
1167
- var BOLD2 = "\x1B[1m";
1168
- var DIM2 = "\x1B[2m";
1169
- var GREEN2 = "\x1B[32m";
1170
- var YELLOW2 = "\x1B[33m";
1171
- var RESET2 = "\x1B[0m";
1172
- var HELP2 = `
1173
- Usage: flyspace setup [options]
1174
-
1175
- Logs this machine in as a Host, checks for the claude / codex CLIs, and
1176
- creates ~/Flyspace/<workspace>/ for each workspace it serves. Safe to re-run.
1177
-
1178
- Options:
1179
- --name <name> Device name shown in the Workspace (default: hostname)
1180
- --no-browser Only print the approval URL, don't open a browser
1181
-
1182
- To switch accounts, run: flyspace auth login --force
1183
- `;
1184
- var RUNNER_HINTS = {
1185
- claude: { install: "npm install -g @anthropic-ai/claude-code", login: "claude" },
1186
- codex: { install: "npm install -g @openai/codex", login: "codex login" }
1187
- };
1188
- var tilde2 = (p) => p.replace(process.env["HOME"] ?? "\0", "~");
1189
- var ok = (msg) => console.log(` ${GREEN2}\u2713${RESET2} ${msg}`);
1190
- var bad = (msg) => console.log(` ${YELLOW2}\u2717${RESET2} ${msg}`);
1191
- async function servedWorkspaces() {
1192
- const creds = loadCredentials();
1193
- try {
1194
- const me = await fetchWhoAmI(creds.apiUrl, await getAccessToken());
1195
- saveCredentials({ ...loadCredentials() ?? creds, workspaces: me.workspaces });
1196
- return me.workspaces;
1197
- } catch {
1198
- console.log(` ${DIM2}Couldn't reach the server; using the workspaces saved at login.${RESET2}`);
1199
- return creds.workspaces ?? [];
1200
- }
1201
- }
1202
- async function runSetup(args) {
1203
- if (args.includes("-h") || args.includes("--help")) return void console.log(HELP2);
1204
- console.log(`
1205
- ${BOLD2}Flyspace Setup${RESET2}
1206
- `);
1207
- const existing = loadCredentials();
1208
- if (existing) {
1209
- ok(`Logged in as ${existing.userEmail} on ${existing.deviceName}`);
1210
- } else {
1211
- await login(args);
1212
- }
1213
- const workspaces = await servedWorkspaces();
1214
- if (workspaces.length) ok(`Serving ${workspaces.join(", ")}`);
1215
- else bad(`Not serving any workspace. Only workspace admins can add a Host.`);
1216
- const runners = detectRunners();
1217
- for (const r of runners) {
1218
- if (!r.installed) bad(`${r.id} not installed ${DIM2}${RUNNER_HINTS[r.id].install}${RESET2}`);
1219
- else if (!r.loggedIn) bad(`${r.id} not logged in ${DIM2}run: ${RUNNER_HINTS[r.id].login}${RESET2}`);
1220
- else ok(`${r.id} ready`);
1221
- }
1222
- const anyReady = runners.some((r) => r.installed && r.loggedIn);
1223
- for (const slug of workspaces) {
1224
- if (!/^[a-z0-9][a-z0-9-]*$/.test(slug)) continue;
1225
- const dir = join4(flyspaceRoot(), slug);
1226
- mkdirSync4(dir, { recursive: true });
1227
- ok(`Folder ${tilde2(dir)}`);
1228
- }
1229
- console.log("\n Next:");
1230
- if (!anyReady) {
1231
- console.log(` Install and log in to ${BOLD2}claude${RESET2} or ${BOLD2}codex${RESET2} (above) so agents can run here.`);
1232
- }
1233
- console.log(` Start the Host: ${BOLD2}flyspace dashboard --mode host${RESET2}`);
1234
- console.log(" Then go back to your browser to finish onboarding.\n");
1235
- if (!workspaces.length) process.exitCode = 1;
1236
- }
1237
-
1238
- // src/commands/install.ts
1239
- import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4, unlinkSync } from "fs";
1240
- import { execSync as execSync2 } from "child_process";
1241
- import { dirname as dirname3, join as join6, resolve as resolve4 } from "path";
1242
- import { homedir as homedir3, platform as platform3 } from "os";
1243
-
1244
- // src/service.ts
1245
- import { execSync } from "child_process";
1246
- import { existsSync as existsSync5 } from "fs";
1247
- import { join as join5 } from "path";
1248
- import { homedir as homedir2, platform as platform2 } from "os";
1249
- function serviceLabel(profile) {
1250
- return profile === DEFAULT_PROFILE ? "ai.flyspace" : `ai.flyspace.${profile}`;
1251
- }
1252
- function plistName(profile) {
1253
- return `${serviceLabel(profile)}.plist`;
1254
- }
1255
- function plistPath(profile) {
1256
- return join5(homedir2(), "Library", "LaunchAgents", plistName(profile));
1257
- }
1258
- function legacyServiceLabel(profile) {
1259
- return profile === DEFAULT_PROFILE ? "io.flycoder.flyspace" : `io.flycoder.flyspace.${profile}`;
1260
- }
1261
- function legacyPlistPath(profile) {
1262
- return join5(homedir2(), "Library", "LaunchAgents", `${legacyServiceLabel(profile)}.plist`);
1263
- }
1264
- function systemdUnitName(profile) {
1265
- return profile === DEFAULT_PROFILE ? "flyspace" : `flyspace-${profile}`;
1266
- }
1267
- function systemdUnitPath(profile) {
1268
- return join5(homedir2(), ".config", "systemd", "user", `${systemdUnitName(profile)}.service`);
1269
- }
1270
- function isServiceInstalled(profile) {
1271
- if (platform2() === "darwin") return existsSync5(plistPath(profile));
1272
- if (platform2() === "linux") return existsSync5(systemdUnitPath(profile));
1273
- return false;
1274
- }
1275
- function restartService(profile) {
1276
- const os = platform2();
1277
- if (os === "darwin") {
1278
- const plist = plistPath(profile);
1279
- try {
1280
- execSync(`launchctl unload "${plist}"`, { stdio: "pipe" });
1281
- } catch {
1282
- }
1283
- try {
1284
- execSync(`launchctl load "${plist}"`, { stdio: "pipe" });
1285
- return true;
1286
- } catch {
1287
- return false;
1288
- }
1289
- }
1290
- if (os === "linux") {
1291
- try {
1292
- execSync(`systemctl --user restart ${systemdUnitName(profile)}`, { stdio: "pipe" });
1293
- return true;
1294
- } catch {
1295
- return false;
1296
- }
1297
- }
1298
- return false;
1299
- }
1300
-
1301
- // src/commands/install.ts
1302
- function log(msg) {
1303
- console.log(`
1304
- \x1B[36m>\x1B[0m ${msg}`);
1305
- }
1306
- function success(msg) {
1307
- console.log(`\x1B[32m+\x1B[0m ${msg}`);
1308
- }
1309
- function warn(msg) {
1310
- console.log(`\x1B[33m!\x1B[0m ${msg}`);
1311
- }
1312
- function run(cmd, opts = {}) {
1313
- return execSync2(cmd, {
1314
- encoding: "utf-8",
1315
- stdio: opts.silent ? "pipe" : "inherit"
1316
- }).toString().trim();
1317
- }
1318
- function detectClaudeBinDir() {
1319
- try {
1320
- const claudePath = execSync2("which claude", { encoding: "utf-8", stdio: "pipe" }).trim();
1321
- return claudePath ? dirname3(claudePath) : void 0;
1322
- } catch {
1323
- return void 0;
1324
- }
1325
- }
1326
- function detectCustieBin() {
1327
- const argv1 = process.argv[1];
1328
- if (argv1) {
1329
- return resolve4(argv1);
1330
- }
1331
- try {
1332
- return run("which flyspace", { silent: true });
1333
- } catch {
1334
- return "flyspace";
1335
- }
1336
- }
1337
- function readEnvFile() {
1338
- if (!existsSync6(paths.CONFIG_FILE)) return {};
1339
- const vars = {};
1340
- for (const line of readFileSync5(paths.CONFIG_FILE, "utf-8").split("\n")) {
1341
- const trimmed = line.trim();
1342
- if (!trimmed || trimmed.startsWith("#")) continue;
1343
- const eq = trimmed.indexOf("=");
1344
- if (eq === -1) continue;
1345
- const key = trimmed.slice(0, eq).trim();
1346
- const val = trimmed.slice(eq + 1).trim();
1347
- if (val) vars[key] = val;
1499
+ await runStatus();
1500
+ break;
1501
+ case "logout":
1502
+ await runLogout(args.slice(1));
1503
+ break;
1504
+ case "-h":
1505
+ case "--help":
1506
+ case void 0:
1507
+ console.log(HELP);
1508
+ break;
1509
+ default:
1510
+ console.error(`Unknown auth command: ${sub}`);
1511
+ console.log(HELP);
1512
+ process.exit(1);
1348
1513
  }
1349
- return vars;
1350
- }
1351
- function escapeXml(str) {
1352
- return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1353
1514
  }
1354
- async function installMacOS() {
1355
- log("Setting up macOS LaunchAgent...");
1356
- const profile = getProfile();
1357
- const custieBin = detectCustieBin();
1358
- const nodePath = process.execPath;
1359
- const claudeBinDir = detectClaudeBinDir();
1360
- const logDir = paths.LOG_DIR;
1361
- mkdirSync5(logDir, { recursive: true });
1362
- const envVars = readEnvFile();
1363
- const homebrewBin = existsSync6("/opt/homebrew/bin") ? "/opt/homebrew/bin" : void 0;
1364
- const asdfShims = join6(homedir3(), ".asdf", "shims");
1365
- const asdfBin = existsSync6(asdfShims) ? asdfShims : void 0;
1366
- const pathParts = [
1367
- dirname3(nodePath),
1368
- claudeBinDir,
1369
- asdfBin,
1370
- homebrewBin,
1371
- "/usr/local/bin",
1372
- "/usr/bin",
1373
- "/bin"
1374
- ].filter(Boolean);
1375
- const programArgs = profile === "default" ? ["start"] : ["start", "--profile", profile];
1376
- const programArgsXml = [custieBin, ...programArgs].map((a) => ` <string>${escapeXml(a)}</string>`).join("\n");
1377
- const plist = `<?xml version="1.0" encoding="UTF-8"?>
1378
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
1379
- "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1380
- <plist version="1.0">
1381
- <dict>
1382
- <key>Label</key>
1383
- <string>${serviceLabel(profile)}</string>
1384
-
1385
- <key>ProgramArguments</key>
1386
- <array>
1387
- ${programArgsXml}
1388
- </array>
1389
1515
 
1390
- <key>EnvironmentVariables</key>
1391
- <dict>
1392
- <key>PATH</key>
1393
- <string>${pathParts.join(":")}</string>
1394
- ${Object.entries(envVars).map(([k, v]) => ` <key>${k}</key>
1395
- <string>${escapeXml(v)}</string>`).join("\n")}
1396
- </dict>
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
+ }
1397
1525
 
1398
- <key>RunAtLoad</key>
1399
- <true/>
1526
+ // src/commands/setup.ts
1527
+ var BOLD2 = "\x1B[1m";
1528
+ var DIM2 = "\x1B[2m";
1529
+ var GREEN2 = "\x1B[32m";
1530
+ var YELLOW2 = "\x1B[33m";
1531
+ var RESET2 = "\x1B[0m";
1532
+ var HELP2 = `
1533
+ Usage: flyspace setup [options]
1400
1534
 
1401
- <key>KeepAlive</key>
1402
- <true/>
1535
+ Logs this machine in as a Host, checks for the claude / codex CLIs, and
1536
+ creates ~/Flyspace/<workspace>/ for each workspace it serves. Safe to re-run.
1403
1537
 
1404
- <key>StandardOutPath</key>
1405
- <string>${join6(logDir, "custie.log")}</string>
1538
+ Options:
1539
+ --name <name> Device name shown in the Workspace (default: hostname)
1540
+ --no-browser Only print the approval URL, don't open a browser
1541
+ --yes Keep the current login without asking
1542
+ --start-host Run the agent Host in the background (installs it as a
1543
+ service the first time)
1544
+ --restart With --start-host: restart the Host and this machine's
1545
+ workspace services so they pick up a new CLI version
1406
1546
 
1407
- <key>StandardErrorPath</key>
1408
- <string>${join6(logDir, "custie-error.log")}</string>
1409
- </dict>
1410
- </plist>`;
1411
- const agentPlistPath = plistPath(profile);
1412
- mkdirSync5(dirname3(agentPlistPath), { recursive: true });
1547
+ To switch accounts, run: flyspace auth login --force
1548
+ `;
1549
+ var RUNNER_HINTS = {
1550
+ claude: { install: "npm install -g @anthropic-ai/claude-code", login: "claude" },
1551
+ codex: { install: "npm install -g @openai/codex", login: "codex login" }
1552
+ };
1553
+ var tilde2 = (p) => p.replace(process.env["HOME"] ?? "\0", "~");
1554
+ var ok = (msg) => console.log(` ${GREEN2}\u2713${RESET2} ${msg}`);
1555
+ var bad = (msg) => console.log(` ${YELLOW2}\u2717${RESET2} ${msg}`);
1556
+ async function servedWorkspaces() {
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 = "";
1413
1564
  try {
1414
- run(`launchctl unload "${agentPlistPath}"`, { silent: true });
1565
+ latest = execSync4(`npm view ${"flyspace-cli"} version`, { encoding: "utf-8", stdio: "pipe", timeout: 1e4 }).trim();
1415
1566
  } catch {
1567
+ return;
1416
1568
  }
1417
- try {
1418
- unlinkSync(agentPlistPath);
1419
- } catch {
1569
+ if (!latest || !isOlderVersion(CLI_VERSION, latest)) return;
1570
+ bad(`flyspace ${CLI_VERSION} is out of date (latest ${latest}) ${DIM2}run: flyspace upgrade${RESET2}`);
1571
+ }
1572
+ function loginArgs(args) {
1573
+ const out = [];
1574
+ for (let i = 0; i < args.length; i++) {
1575
+ const a = args[i];
1576
+ if (a === "--name") out.push(a, args[++i] ?? "");
1577
+ else if (a.startsWith("--name=") || a === "--no-browser") out.push(a);
1420
1578
  }
1421
- writeFileSync4(agentPlistPath, plist);
1422
- run(`launchctl load "${agentPlistPath}"`, { silent: true });
1423
- success(`LaunchAgent installed and loaded (profile: ${profile}).`);
1424
- console.log(` Logs: ${logDir}`);
1425
- console.log(` Check: launchctl list | grep flyspace`);
1579
+ return out;
1426
1580
  }
1427
- async function installLinux() {
1428
- log("Setting up systemd user service...");
1429
- const profile = getProfile();
1430
- const custieBin = detectCustieBin();
1431
- const nodePath = process.execPath;
1432
- const claudeBinDir = detectClaudeBinDir();
1433
- const logDir = paths.LOG_DIR;
1434
- mkdirSync5(logDir, { recursive: true });
1435
- const envVars = readEnvFile();
1436
- const envLines = Object.entries(envVars).map(([k, v]) => `Environment="${k}=${v}"`).join("\n");
1437
- const linuxbrewBin = existsSync6("/home/linuxbrew/.linuxbrew/bin") ? "/home/linuxbrew/.linuxbrew/bin" : void 0;
1438
- const pathParts = [
1439
- dirname3(nodePath),
1440
- claudeBinDir,
1441
- linuxbrewBin,
1442
- "/usr/local/bin",
1443
- "/usr/bin",
1444
- "/bin"
1445
- ].filter(Boolean);
1446
- const execStart = profile === "default" ? `${custieBin} start` : `${custieBin} start --profile ${profile}`;
1447
- const unit = `[Unit]
1448
- Description=Flyspace agent host${profile === "default" ? "" : ` (${profile})`}
1449
- After=network-online.target
1450
- Wants=network-online.target
1451
-
1452
- [Service]
1453
- Type=simple
1454
- ExecStart=${execStart}
1455
- Restart=on-failure
1456
- RestartSec=5
1457
- ${envLines}
1458
- Environment="PATH=${pathParts.join(":")}"
1459
-
1460
- StandardOutput=append:${join6(logDir, "custie.log")}
1461
- StandardError=append:${join6(logDir, "custie-error.log")}
1462
-
1463
- [Install]
1464
- WantedBy=default.target
1465
- `;
1466
- const unitName = systemdUnitName(profile);
1467
- const unitDir = join6(homedir3(), ".config", "systemd", "user");
1468
- mkdirSync5(unitDir, { recursive: true });
1469
- writeFileSync4(join6(unitDir, `${unitName}.service`), unit);
1470
- run("systemctl --user daemon-reload", { silent: true });
1471
- run(`systemctl --user enable --now ${unitName}`, { silent: true });
1472
- success(`systemd user service installed and started (profile: ${profile}).`);
1473
- console.log(` Logs: ${logDir}`);
1474
- console.log(` Check: systemctl --user status ${unitName}`);
1581
+ function restartHostServices(workspaces) {
1582
+ if (restartHostService()) ok("Host restarted on the new version");
1583
+ else bad("Could not restart the Host service run: flyspace setup --start-host --restart");
1584
+ for (const slug of workspaces) {
1585
+ if (!isServiceInstalled(slug)) continue;
1586
+ if (restartService(slug)) ok(`${slug} scheduler restarted`);
1587
+ else bad(`${slug} scheduler did not restart run: flyspace restart --profile ${slug}`);
1588
+ }
1475
1589
  }
1476
- async function runInstall() {
1477
- ensureDirs();
1478
- const os = platform3();
1479
- if (os === "darwin") {
1480
- await installMacOS();
1481
- } else if (os === "linux") {
1482
- await installLinux();
1590
+ async function runSetup(args) {
1591
+ if (args.includes("-h") || args.includes("--help")) return void console.log(HELP2);
1592
+ console.log(`
1593
+ ${BOLD2}Flyspace Setup${RESET2}
1594
+ `);
1595
+ checkLatestVersion();
1596
+ const existing = loadCredentials();
1597
+ let workspaces;
1598
+ if (existing) {
1599
+ workspaces = await servedWorkspaces();
1600
+ console.log(` This machine is already a Host:`);
1601
+ console.log(` Account ${existing.userEmail}`);
1602
+ console.log(` Device ${existing.deviceName}`);
1603
+ console.log(` Workspaces ${workspaces.length ? workspaces.join(", ") : "(none)"}`);
1604
+ const others = listProfiles().filter((p) => p !== "default" && !workspaces.includes(p));
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
+ }
1609
+ console.log("");
1610
+ const keep = args.includes("--yes") || !process.stdin.isTTY ? true : await confirm2({ message: "Keep using this account and device?", default: true });
1611
+ if (!keep) {
1612
+ await login([...loginArgs(args), "--force"]);
1613
+ workspaces = await servedWorkspaces();
1614
+ }
1483
1615
  } else {
1484
- warn(`Unsupported platform: ${os}. You'll need to configure autostart manually.`);
1616
+ await login(loginArgs(args));
1617
+ workspaces = await servedWorkspaces();
1618
+ }
1619
+ if (workspaces.length) ok(`Registered as the Host for ${workspaces.join(", ")}`);
1620
+ else bad(`Not serving any workspace. Only workspace admins can add a Host.`);
1621
+ const runners = detectRunners();
1622
+ for (const r of runners) {
1623
+ if (!r.installed) bad(`${r.id} not installed ${DIM2}${RUNNER_HINTS[r.id].install}${RESET2}`);
1624
+ else if (!r.loggedIn) bad(`${r.id} not logged in ${DIM2}run: ${RUNNER_HINTS[r.id].login}${RESET2}`);
1625
+ else ok(`${r.id} ready`);
1626
+ }
1627
+ const anyReady = runners.some((r) => r.installed && r.loggedIn);
1628
+ for (const slug of workspaces) {
1629
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(slug)) continue;
1630
+ const dir = join7(flyspaceRoot(), slug);
1631
+ mkdirSync5(dir, { recursive: true });
1632
+ ok(`Folder ${tilde2(dir)}`);
1633
+ }
1634
+ let hostRunning = false;
1635
+ if (args.includes("--start-host") && workspaces.length) {
1636
+ if (!isHostServiceInstalled()) {
1637
+ hostRunning = await installHostService();
1638
+ if (hostRunning) ok(`Host running in the background ${DIM2}logs: ${tilde2(paths.LOG_DIR)}/host.log${RESET2}`);
1639
+ } else {
1640
+ hostRunning = true;
1641
+ if (args.includes("--restart")) restartHostServices(workspaces);
1642
+ else ok("Host service already installed");
1643
+ }
1644
+ }
1645
+ console.log("\n Next:");
1646
+ if (!anyReady) {
1647
+ console.log(` Install and log in to ${BOLD2}claude${RESET2} or ${BOLD2}codex${RESET2} (above) so agents can run here.`);
1648
+ }
1649
+ if (hostRunning) {
1650
+ console.log(" The Host keeps running after you close this terminal and starts again at login.");
1651
+ const stop = process.platform === "darwin" ? `launchctl bootout gui/$(id -u)/${HOST_SERVICE_LABEL}` : `systemctl --user disable --now ${HOST_SYSTEMD_UNIT}`;
1652
+ console.log(` To stop it: ${BOLD2}${stop}${RESET2}`);
1653
+ } else {
1654
+ console.log(` Start the Host: ${BOLD2}flyspace setup --start-host${RESET2}`);
1485
1655
  }
1656
+ console.log(" Then go back to your browser to finish onboarding.\n");
1657
+ if (!workspaces.length) process.exitCode = 1;
1486
1658
  }
1487
1659
 
1488
1660
  // src/commands/uninstall.ts
1489
- import { existsSync as existsSync7, unlinkSync as unlinkSync2 } from "fs";
1490
- import { execSync as execSync3 } from "child_process";
1491
- import { basename as basename2, join as join7 } from "path";
1492
- import { homedir as homedir4, platform as platform4 } from "os";
1661
+ import { existsSync as existsSync8, unlinkSync as unlinkSync2 } from "fs";
1662
+ import { execSync as execSync5 } from "child_process";
1663
+ import { basename as basename2, join as join8 } from "path";
1664
+ import { homedir as homedir4, platform as platform5 } from "os";
1493
1665
  function log2(msg) {
1494
1666
  console.log(`
1495
1667
  \x1B[36m>\x1B[0m ${msg}`);
@@ -1501,16 +1673,16 @@ function warn2(msg) {
1501
1673
  console.log(`\x1B[33m!\x1B[0m ${msg}`);
1502
1674
  }
1503
1675
  function run2(cmd) {
1504
- execSync3(cmd, { encoding: "utf-8", stdio: "pipe" });
1676
+ execSync5(cmd, { encoding: "utf-8", stdio: "pipe" });
1505
1677
  }
1506
1678
  async function runUninstall() {
1507
- const os = platform4();
1679
+ const os = platform5();
1508
1680
  const profile = getProfile();
1509
1681
  log2("Uninstalling Flyspace service...");
1510
1682
  if (os === "darwin") {
1511
1683
  const removed = [];
1512
1684
  for (const plist of [plistPath(profile), legacyPlistPath(profile)]) {
1513
- if (!existsSync7(plist)) continue;
1685
+ if (!existsSync8(plist)) continue;
1514
1686
  try {
1515
1687
  run2(`launchctl unload "${plist}"`);
1516
1688
  } catch {
@@ -1532,7 +1704,7 @@ async function runUninstall() {
1532
1704
  run2(`systemctl --user disable --now ${unitName}`);
1533
1705
  } catch {
1534
1706
  }
1535
- const unitPath = join7(homedir4(), ".config", "systemd", "user", `${unitName}.service`);
1707
+ const unitPath = join8(homedir4(), ".config", "systemd", "user", `${unitName}.service`);
1536
1708
  try {
1537
1709
  unlinkSync2(unitPath);
1538
1710
  } catch {
@@ -1548,53 +1720,94 @@ async function runUninstall() {
1548
1720
  console.log("\n Config and data directories were NOT removed.");
1549
1721
  }
1550
1722
 
1551
- // src/commands/profiles.ts
1552
- import { existsSync as existsSync8, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
1553
- import { execSync as execSync4 } from "child_process";
1554
- import { join as join8 } from "path";
1555
- import { platform as platform5 } from "os";
1556
- function listProfiles() {
1557
- const profiles = [];
1558
- if (existsSync8(paths.PROFILES_DIR)) {
1559
- for (const entry of readdirSync2(paths.PROFILES_DIR)) {
1560
- try {
1561
- if (statSync2(join8(paths.PROFILES_DIR, entry)).isDirectory()) {
1562
- profiles.push(entry);
1563
- }
1564
- } catch {
1565
- }
1566
- }
1567
- }
1568
- return profiles;
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;
1569
1740
  }
1570
- function isServiceRunning(profile) {
1571
- try {
1572
- if (platform5() === "darwin") {
1573
- const out = execSync4("launchctl list", { encoding: "utf-8", stdio: "pipe" });
1574
- return out.split("\n").some((line) => line.endsWith(` ${serviceLabel(profile)}`));
1575
- }
1576
- if (platform5() === "linux") {
1577
- execSync4(`systemctl --user is-active ${systemdUnitName(profile)}`, { stdio: "pipe" });
1578
- return true;
1579
- }
1580
- } catch {
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}`);
1581
1749
  }
1582
- return false;
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("");
1583
1762
  }
1584
- async function runProfiles() {
1585
- const profiles = listProfiles();
1586
- console.log("\n Profiles:\n");
1587
- for (const profile of profiles) {
1588
- const status = isServiceRunning(profile) ? "\x1B[32mrunning\x1B[0m" : "\x1B[90mstopped\x1B[0m";
1589
- console.log(` ${profile.padEnd(20)} ${status}`);
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);
1590
1786
  }
1591
- console.log("\n Target a profile with --profile <name> on any command.\n");
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);
1592
1802
  }
1593
1803
 
1594
1804
  // src/commands/upgrade.ts
1595
- import { execSync as execSync5 } from "child_process";
1805
+ import { execSync as execSync6 } from "child_process";
1596
1806
  function restartInstalledProfiles() {
1597
- const installed = listProfiles().filter(isServiceInstalled);
1807
+ if (isHostServiceInstalled()) {
1808
+ console.log(restartHostService() ? "\nHost restarted." : "\nHost restart failed; run: flyspace setup --start-host --restart");
1809
+ }
1810
+ const installed = (loadCredentials()?.workspaces ?? listProfiles()).filter(isServiceInstalled);
1598
1811
  if (installed.length === 0) {
1599
1812
  console.log("\nNo installed services to restart.");
1600
1813
  return;
@@ -1610,18 +1823,18 @@ Restarting ${installed.length} instance(s)...`);
1610
1823
  }
1611
1824
  }
1612
1825
  async function runUpgrade() {
1613
- const current = "0.1.5";
1826
+ const current = "0.1.7";
1614
1827
  console.log(`Current version: ${current}`);
1615
1828
  console.log("Checking for updates...\n");
1616
1829
  try {
1617
- 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();
1618
1831
  if (latest === current) {
1619
1832
  console.log(`Already on the latest version (${current}).`);
1620
1833
  return;
1621
1834
  }
1622
1835
  console.log(`New version available: ${latest}`);
1623
1836
  console.log("Upgrading...\n");
1624
- execSync5(`npm install -g ${"flyspace-cli"}@latest`, { stdio: "inherit" });
1837
+ execSync6(`npm install -g ${"flyspace-cli"}@latest`, { stdio: "inherit" });
1625
1838
  console.log(`
1626
1839
  Upgraded from ${current} to ${latest}.`);
1627
1840
  restartInstalledProfiles();
@@ -1637,7 +1850,7 @@ You can upgrade manually with: npm install -g ${"flyspace-cli"}@latest`);
1637
1850
 
1638
1851
  // src/commands/automation.ts
1639
1852
  import { WebClient } from "@slack/web-api";
1640
- var USAGE = `
1853
+ var USAGE2 = `
1641
1854
  Usage: flyspace automation <subcommand> [options]
1642
1855
 
1643
1856
  Subcommands:
@@ -1975,7 +2188,7 @@ async function runAutomationCmd(args) {
1975
2188
  await handleRun(manager, args[1]);
1976
2189
  break;
1977
2190
  default:
1978
- console.log(USAGE);
2191
+ console.log(USAGE2);
1979
2192
  break;
1980
2193
  }
1981
2194
  }
@@ -2166,7 +2379,7 @@ function breakdown(store, by, days) {
2166
2379
  }
2167
2380
 
2168
2381
  // src/commands/stop.ts
2169
- import { execSync as execSync6 } from "child_process";
2382
+ import { execSync as execSync7 } from "child_process";
2170
2383
  import { platform as platform6 } from "os";
2171
2384
  function log3(msg) {
2172
2385
  console.log(`
@@ -2185,7 +2398,7 @@ async function runStop() {
2185
2398
  const plist = plistPath(profile);
2186
2399
  log3("Stopping Flyspace service...");
2187
2400
  try {
2188
- execSync6(`launchctl unload "${plist}"`, { stdio: "pipe" });
2401
+ execSync7(`launchctl unload "${plist}"`, { stdio: "pipe" });
2189
2402
  success3("Service stopped. Run `flyspace restart` to start again.");
2190
2403
  } catch {
2191
2404
  warn3("Service is not currently loaded.");
@@ -2193,7 +2406,7 @@ async function runStop() {
2193
2406
  } else if (os === "linux") {
2194
2407
  log3("Stopping Flyspace service...");
2195
2408
  try {
2196
- execSync6(`systemctl --user stop ${systemdUnitName(profile)}`, { stdio: "pipe" });
2409
+ execSync7(`systemctl --user stop ${systemdUnitName(profile)}`, { stdio: "pipe" });
2197
2410
  success3("Service stopped. Run `flyspace restart` to start again.");
2198
2411
  } catch {
2199
2412
  warn3("Service is not currently running.");
@@ -2253,7 +2466,7 @@ import { execFileSync as execFileSync4 } from "child_process";
2253
2466
  import { existsSync as existsSync27, readFileSync as readFileSync16, writeFileSync as writeFileSync10 } from "fs";
2254
2467
  import { resolve as resolve7 } from "path";
2255
2468
  import { tmpdir as tmpdir3, homedir as homedir9 } from "os";
2256
- import { input, confirm as confirm2, select, checkbox } from "@inquirer/prompts";
2469
+ import { input, confirm as confirm3, select, checkbox } from "@inquirer/prompts";
2257
2470
  import { WebClient as WebClient3 } from "@slack/web-api";
2258
2471
 
2259
2472
  // src/store/auth-links.ts
@@ -3420,8 +3633,8 @@ function setupPage(c, token, invite, form = {}) {
3420
3633
  );
3421
3634
  }
3422
3635
  function createAuthConfirmRouter() {
3423
- const confirm3 = new Hono();
3424
- confirm3.get("/confirm", withResolvedProfile, async (c) => {
3636
+ const confirm4 = new Hono();
3637
+ confirm4.get("/confirm", withResolvedProfile, async (c) => {
3425
3638
  const settings = await loadWorkspaceBranding();
3426
3639
  const display = fontOf(settings.font).stack;
3427
3640
  const name = settings.displayName?.trim() || getProfile();
@@ -3458,7 +3671,7 @@ function createAuthConfirmRouter() {
3458
3671
  )
3459
3672
  );
3460
3673
  });
3461
- confirm3.post("/confirm", withResolvedProfile, async (c) => {
3674
+ confirm4.post("/confirm", withResolvedProfile, async (c) => {
3462
3675
  const form = await c.req.parseBody();
3463
3676
  const params = readParams({
3464
3677
  token: typeof form["token"] === "string" ? form["token"] : void 0,
@@ -3469,7 +3682,7 @@ function createAuthConfirmRouter() {
3469
3682
  if (!target) return c.text("SUPABASE_URL is not configured", 500);
3470
3683
  return c.redirect(target, 303);
3471
3684
  });
3472
- confirm3.get("/invite", withResolvedProfile, async (c) => {
3685
+ confirm4.get("/invite", withResolvedProfile, async (c) => {
3473
3686
  const token = c.req.query("token")?.trim() ?? "";
3474
3687
  const lookup = await findInvite(token);
3475
3688
  const brandingWorkspace = lookup.state === "valid" ? await getWorkspaceById(lookup.invite.workspaceId) : null;
@@ -3515,7 +3728,7 @@ function createAuthConfirmRouter() {
3515
3728
  )
3516
3729
  );
3517
3730
  });
3518
- confirm3.post("/invite", withResolvedProfile, async (c) => {
3731
+ confirm4.post("/invite", withResolvedProfile, async (c) => {
3519
3732
  const form = await c.req.parseBody();
3520
3733
  const token = typeof form["token"] === "string" ? form["token"].trim() : "";
3521
3734
  const back = `/auth/invite?token=${encodeURIComponent(token)}`;
@@ -3532,7 +3745,7 @@ function createAuthConfirmRouter() {
3532
3745
  if (!await consumeInvite(lookup.invite.id)) return c.redirect(back, 303);
3533
3746
  return c.redirect(target, 303);
3534
3747
  });
3535
- confirm3.get("/new-workspace", async (c) => {
3748
+ confirm4.get("/new-workspace", async (c) => {
3536
3749
  const token = c.req.query("token")?.trim() ?? "";
3537
3750
  const lookup = INVITE_TOKEN.test(token) ? await lookupSetupInvite(token) : { state: "unknown" };
3538
3751
  if (lookup.state === "valid") return c.html(setupPage(c, token, lookup.invite));
@@ -3560,7 +3773,7 @@ function createAuthConfirmRouter() {
3560
3773
  status
3561
3774
  );
3562
3775
  });
3563
- confirm3.post("/new-workspace", async (c) => {
3776
+ confirm4.post("/new-workspace", async (c) => {
3564
3777
  const form = await c.req.parseBody();
3565
3778
  const token = typeof form["token"] === "string" ? form["token"].trim() : "";
3566
3779
  const slug = typeof form["slug"] === "string" ? form["slug"].trim().toLowerCase() : "";
@@ -3667,7 +3880,7 @@ function createAuthConfirmRouter() {
3667
3880
  }
3668
3881
  return c.redirect(`${origin}/login?email=${encodeURIComponent(invite.email)}`, 303);
3669
3882
  });
3670
- return confirm3;
3883
+ return confirm4;
3671
3884
  }
3672
3885
  function confirmLinkFor(origin, actionLink, intent) {
3673
3886
  try {
@@ -3787,6 +4000,9 @@ import { serveStatic } from "@hono/node-server/serve-static";
3787
4000
 
3788
4001
  // src/reconcile.ts
3789
4002
  import { existsSync as existsSync11, mkdirSync as mkdirSync6, writeFileSync as writeFileSync5 } from "fs";
4003
+ async function boundWorkspaces2() {
4004
+ return (await boundWorkspaces())?.workspaces ?? null;
4005
+ }
3790
4006
  async function reconcileProfile(profile) {
3791
4007
  return runWithProfile(profile, async () => {
3792
4008
  const log5 = [];
@@ -3812,8 +4028,8 @@ async function reconcileProfile(profile) {
3812
4028
  });
3813
4029
  }
3814
4030
  async function reconcileAll() {
3815
- const workspaces = await listWorkspaces();
3816
- const slugs = /* @__PURE__ */ new Set([...workspaces.map((w) => w.slug), ...listProfiles()]);
4031
+ const bound = hasSupabaseCredentials() ? null : await boundWorkspaces2();
4032
+ const slugs = new Set(bound ?? [...(await listWorkspaces()).map((w) => w.slug), ...listProfiles()]);
3817
4033
  const results = /* @__PURE__ */ new Map();
3818
4034
  for (const slug of slugs) {
3819
4035
  try {
@@ -3826,7 +4042,8 @@ async function reconcileAll() {
3826
4042
  return results;
3827
4043
  }
3828
4044
  async function ensureServices() {
3829
- const slugs = /* @__PURE__ */ new Set([...(await listWorkspaces()).map((w) => w.slug), ...listProfiles()]);
4045
+ const bound = await boundWorkspaces2();
4046
+ const slugs = new Set(bound ?? [...(await listWorkspaces()).map((w) => w.slug), ...listProfiles()]);
3830
4047
  slugs.delete(DEFAULT_PROFILE);
3831
4048
  const installed = [];
3832
4049
  for (const slug of slugs) {
@@ -6839,7 +7056,7 @@ import {
6839
7056
  readFileSync as readFileSync10,
6840
7057
  realpathSync,
6841
7058
  renameSync,
6842
- rmSync as rmSync4,
7059
+ rmSync as rmSync5,
6843
7060
  statSync as statSync5,
6844
7061
  writeFileSync as writeFileSync7
6845
7062
  } from "fs";
@@ -7133,7 +7350,7 @@ import { readFileSync as readFileSync9 } from "fs";
7133
7350
  import { homedir as homedir7 } from "os";
7134
7351
 
7135
7352
  // src/mcp/config.ts
7136
- import { mkdtempSync, rmSync as rmSync3, writeFileSync as writeFileSync6 } from "fs";
7353
+ import { mkdtempSync, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "fs";
7137
7354
  import { tmpdir } from "os";
7138
7355
  import { join as join14 } from "path";
7139
7356
 
@@ -7826,7 +8043,7 @@ function writeMcpConfigFile(config) {
7826
8043
  writeFileSync6(path, JSON.stringify(config), { mode: 384 });
7827
8044
  return {
7828
8045
  path,
7829
- cleanup: () => rmSync3(dir, { recursive: true, force: true })
8046
+ cleanup: () => rmSync4(dir, { recursive: true, force: true })
7830
8047
  };
7831
8048
  }
7832
8049
  async function withMcpConfig(profile, connectionIds, fn) {
@@ -10042,7 +10259,7 @@ function deleteDocEntry(root, path) {
10042
10259
  }
10043
10260
  const isDir2 = stat.isDirectory();
10044
10261
  if (!isDir2 && !stat.isFile() && !stat.isSymbolicLink()) return { status: "not-found" };
10045
- rmSync4(abs, { recursive: isDir2 });
10262
+ rmSync5(abs, { recursive: isDir2 });
10046
10263
  return {
10047
10264
  status: "ok",
10048
10265
  path: relative(root, abs).split(sep).join("/"),
@@ -14759,7 +14976,7 @@ function loadHostPublicKey() {
14759
14976
  // src/dashboard/host-lock.ts
14760
14977
  import { execFileSync as execFileSync3 } from "child_process";
14761
14978
  import { randomBytes as randomBytes5 } from "crypto";
14762
- 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";
14763
14980
  import { join as join21 } from "path";
14764
14981
  function hostLockPath() {
14765
14982
  return join21(paths.BASE_DATA_DIR, "host.lock");
@@ -14795,7 +15012,7 @@ function acquireHostLock() {
14795
15012
  try {
14796
15013
  writeFileSync9(path, body, { flag: "wx", mode: 384 });
14797
15014
  const release2 = () => {
14798
- if (readLockPid(path) === process.pid) rmSync5(path, { force: true });
15015
+ if (readLockPid(path) === process.pid) rmSync6(path, { force: true });
14799
15016
  };
14800
15017
  process.once("exit", release2);
14801
15018
  return release2;
@@ -14809,7 +15026,7 @@ function acquireHostLock() {
14809
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.`
14810
15027
  );
14811
15028
  }
14812
- rmSync5(path, { force: true });
15029
+ rmSync6(path, { force: true });
14813
15030
  }
14814
15031
  }
14815
15032
  throw new Error(`Could not take the Host lock at ${path}`);
@@ -15027,16 +15244,35 @@ async function listWorkspaceDevices(workspaceId) {
15027
15244
  if (error2) fail("listWorkspaceDevices", error2);
15028
15245
  return (data ?? []).map((r) => toDevice(r.devices)).sort((a, b) => b.createdAt.localeCompare(a.createdAt));
15029
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;
15030
15253
  async function listWorkspaceHosts(workspaceIds, exceptPublicKey) {
15031
15254
  const hosts2 = /* @__PURE__ */ new Map();
15032
15255
  if (!workspaceIds.length) return hosts2;
15033
- 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);
15034
15257
  if (error2) fail("listWorkspaceHosts", error2);
15035
15258
  for (const r of data ?? []) {
15036
- 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
+ }
15037
15262
  }
15038
15263
  return hosts2;
15039
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
+ }
15040
15276
  async function userEmail(userId) {
15041
15277
  const { data, error: error2 } = await getSupabaseClient().auth.admin.getUserById(userId);
15042
15278
  if (error2) return null;
@@ -15819,9 +16055,21 @@ async function revalidateDeviceScopes() {
15819
16055
  continue;
15820
16056
  }
15821
16057
  const workspaces = (await listDeviceWorkspaces(device)).map((w) => w.slug);
15822
- 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
+ }
15823
16067
  }
15824
16068
  }
16069
+ var LAST_SEEN_RESOLUTION_MS = 60 * 6e4;
16070
+ function minCliVersion() {
16071
+ return process.env["FLYSPACE_MIN_CLI_VERSION"]?.trim() || null;
16072
+ }
15825
16073
  function attachHostSocket(server) {
15826
16074
  const wss = new WebSocketServer({ server, path: HOST_SOCKET_PATH });
15827
16075
  const legacyPublicKey = loadHostPublicKey();
@@ -15862,10 +16110,19 @@ function attachHostSocket(server) {
15862
16110
  ws.close(1008, "bad signature");
15863
16111
  return;
15864
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
+ }
15865
16120
  supersedeSameIdentity(scope2, ws);
15866
16121
  lastAuthFailure = null;
15867
16122
  addHost(ws, scope2);
15868
- 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
+ });
15869
16126
  console.log(`[host-socket] Host authenticated (authPath: ${scope2.authPath})`);
15870
16127
  if (scope2.workspaces) void markAgentConnected(scope2.workspaces);
15871
16128
  })();
@@ -15988,7 +16245,7 @@ function remoteAgentCallbacks() {
15988
16245
  }
15989
16246
  var RECONNECT_BASE_MS = 1e3;
15990
16247
  var RECONNECT_MAX_MS = 3e4;
15991
- function connectHostSocket(url, callbacks, onAuthenticated) {
16248
+ function connectHostSocket(url, callbacks, onAuthenticated, onWorkspaces) {
15992
16249
  const credentials = loadCredentials();
15993
16250
  const deviceId = credentials?.deviceId;
15994
16251
  const privateKey = deviceId ? ensureDeviceKey().privateKey : loadHostPrivateKey();
@@ -16020,11 +16277,23 @@ function connectHostSocket(url, callbacks, onAuthenticated) {
16020
16277
  if (!msg) return;
16021
16278
  if (msg.type === "challenge") {
16022
16279
  const signature = signRaw(null, Buffer.from(msg.nonce, "base64"), privateKey).toString("base64");
16023
- 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 });
16024
16282
  return;
16025
16283
  }
16026
16284
  if (msg.type === "auth-failed") {
16027
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);
16028
16297
  return;
16029
16298
  }
16030
16299
  if (msg.type === "auth-ok") {
@@ -16036,6 +16305,7 @@ function connectHostSocket(url, callbacks, onAuthenticated) {
16036
16305
  send(ws, { type: "host-config", configs: buildHostConfigSnapshot() });
16037
16306
  }, HOST_CONFIG_PUSH_INTERVAL_MS);
16038
16307
  onAuthenticated?.();
16308
+ if (msg.workspaces) onWorkspaces?.(msg.workspaces);
16039
16309
  return;
16040
16310
  }
16041
16311
  if (msg.type === "db-result") {
@@ -16378,6 +16648,7 @@ function createDeviceAuthRouter() {
16378
16648
  if (!decided) {
16379
16649
  return c.json({ error: "That code was already used." }, 409);
16380
16650
  }
16651
+ await releaseStaleHosts(workspaceIds, pending3.publicKey);
16381
16652
  return c.json({ ok: true });
16382
16653
  });
16383
16654
  return app;
@@ -16750,7 +17021,7 @@ async function startDashboardServer(preferredPort = DEFAULT_DASHBOARD_PORT, host
16750
17021
  const bootCallbacks = new Proxy({}, {
16751
17022
  get: (_t, prop) => lateCallbacks?.[prop]
16752
17023
  });
16753
- 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;
16754
17025
  if (servesHost) {
16755
17026
  await reconcileAll().catch(
16756
17027
  (err) => console.error("[dashboard] reconcileAll failed:", err.message)
@@ -16802,7 +17073,9 @@ async function startDashboardServer(preferredPort = DEFAULT_DASHBOARD_PORT, host
16802
17073
  } else {
16803
17074
  app.route("/share", createPublicShareAssetRouter(WEB_DIST));
16804
17075
  app.route("/", createAppIconRouter());
16805
- app.use("/*", serveStatic({ root: WEB_DIST }));
17076
+ if (existsSync26(WEB_DIST) || existsSync26(join22(paths.PACKAGE_ROOT, "web", "package.json"))) {
17077
+ app.use("/*", serveStatic({ root: WEB_DIST }));
17078
+ }
16806
17079
  app.get("/assets/*", (c) => c.text("stale asset \u2014 reload for the current build", 404));
16807
17080
  const spaFallback = serveStatic({ path: join22(WEB_DIST, "index.html") });
16808
17081
  app.get("*", async (c, next) => {
@@ -16823,7 +17096,7 @@ The JSON API is live under /api.` : "This install has no bundled Workspace UI.\n
16823
17096
  if (mode === "host") relayToken = publishHostEndpoint(port);
16824
17097
  if (mode === "data") attachHostSocket(server);
16825
17098
  const stopHostStatusBroadcast = mode === "data" ? startHostStatusBroadcast() : void 0;
16826
- 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);
16827
17100
  const stopPushWatcher = earlyHostSocket ? () => {
16828
17101
  } : startPushWatcher();
16829
17102
  const displayHost = host === "0.0.0.0" || host === "::" ? "127.0.0.1" : host;
@@ -16844,6 +17117,13 @@ The JSON API is live under /api.` : "This install has no bundled Workspace UI.\n
16844
17117
  }
16845
17118
  };
16846
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
+ }
16847
17127
  var SERVICE_CHECK_MS = 6e4;
16848
17128
  function watchServices() {
16849
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));
@@ -17053,7 +17333,7 @@ Done. ${totalThreads} threads, ${totalReplies} messages upserted.`);
17053
17333
  }
17054
17334
 
17055
17335
  // src/commands/slack.ts
17056
- var USAGE2 = `
17336
+ var USAGE3 = `
17057
17337
  Usage: flyspace slack <subcommand> [options]
17058
17338
 
17059
17339
  Subcommands:
@@ -17381,7 +17661,7 @@ async function ensureClaudeCodeReady() {
17381
17661
  console.log(" Open a new terminal and run \x1B[1mclaude\x1B[0m to complete the sign-in flow,");
17382
17662
  console.log(" then come back here.\n");
17383
17663
  }
17384
- const proceed = await confirm2({
17664
+ const proceed = await confirm3({
17385
17665
  message: "Continue setup anyway?",
17386
17666
  default: false
17387
17667
  });
@@ -17841,7 +18121,7 @@ async function runSlackSetup(args) {
17841
18121
  if (!prereq) return;
17842
18122
  const { claudeConfigDir } = prereq;
17843
18123
  if (existsSync27(paths.CONFIG_FILE)) {
17844
- const reconfigure = await confirm2({
18124
+ const reconfigure = await confirm3({
17845
18125
  message: `Config already exists at ${paths.CONFIG_FILE}. Reconfigure?`,
17846
18126
  default: false
17847
18127
  });
@@ -17988,13 +18268,13 @@ async function runSlackCmd(args) {
17988
18268
  break;
17989
18269
  }
17990
18270
  default:
17991
- console.log(USAGE2);
18271
+ console.log(USAGE3);
17992
18272
  break;
17993
18273
  }
17994
18274
  }
17995
18275
 
17996
18276
  // src/commands/chat.ts
17997
- var USAGE3 = `
18277
+ var USAGE4 = `
17998
18278
  Usage: flyspace chat <subcommand> [options]
17999
18279
 
18000
18280
  Subcommands:
@@ -18140,17 +18420,17 @@ async function runChatCmd(args) {
18140
18420
  case "-h":
18141
18421
  case "--help":
18142
18422
  case void 0:
18143
- console.log(USAGE3);
18423
+ console.log(USAGE4);
18144
18424
  break;
18145
18425
  default:
18146
18426
  console.error(`Unknown subcommand: ${subcommand}`);
18147
- console.log(USAGE3);
18427
+ console.log(USAGE4);
18148
18428
  process.exit(1);
18149
18429
  }
18150
18430
  }
18151
18431
 
18152
18432
  // src/commands/mcp.ts
18153
- var USAGE4 = `
18433
+ var USAGE5 = `
18154
18434
  Usage: flyspace mcp <server>
18155
18435
 
18156
18436
  Run one of Flyspace's own MCP servers on stdio. Spawned by the agent runtime
@@ -18167,7 +18447,7 @@ Usage: flyspace mcp <server>
18167
18447
  async function runMcpCmd(args) {
18168
18448
  const server = args[0];
18169
18449
  if (!server || server === "-h" || server === "--help") {
18170
- console.log(USAGE4.trim());
18450
+ console.log(USAGE5.trim());
18171
18451
  return;
18172
18452
  }
18173
18453
  restoreStdout();
@@ -18180,7 +18460,7 @@ async function runMcpCmd(args) {
18180
18460
  }
18181
18461
  default:
18182
18462
  console.error(`Unknown MCP server: ${server}`);
18183
- console.error(USAGE4.trim());
18463
+ console.error(USAGE5.trim());
18184
18464
  process.exit(1);
18185
18465
  }
18186
18466
  }
@@ -18231,9 +18511,10 @@ async function runDashboard(args) {
18231
18511
  if (mode === "host") acquireHostLock();
18232
18512
  ensureWebBuild();
18233
18513
  const server = await startDashboardServer(preferredPort, host, mode);
18234
- const profile = getProfile();
18514
+ const served = mode === "host" ? loadCredentials()?.workspaces : void 0;
18515
+ const scope = served ? `workspaces: ${served.join(", ") || "none"}` : `profile: ${getProfile()}`;
18235
18516
  console.log(`
18236
- Flyspace dashboard (profile: ${profile}, mode: ${mode})`);
18517
+ Flyspace dashboard (${scope}, mode: ${mode})`);
18237
18518
  console.log(` ${server.url}`);
18238
18519
  if (server.port !== preferredPort) {
18239
18520
  console.log(` (port ${preferredPort} was busy \u2014 using ${server.port})`);
@@ -18255,7 +18536,7 @@ async function runDashboard(args) {
18255
18536
 
18256
18537
  // src/commands/workspace.ts
18257
18538
  import { existsSync as existsSync29, mkdirSync as mkdirSync11, writeFileSync as writeFileSync11 } from "fs";
18258
- var USAGE5 = `
18539
+ var USAGE6 = `
18259
18540
  Usage: flyspace workspace <subcommand> [options]
18260
18541
 
18261
18542
  Subcommands:
@@ -18334,7 +18615,7 @@ async function bindThisHost(workspaceId) {
18334
18615
  return;
18335
18616
  }
18336
18617
  await bindDeviceWorkspace(device.id, workspaceId, device.userId);
18337
- 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)`);
18338
18619
  }
18339
18620
  function provisionLocal(botName) {
18340
18621
  ensureDirs();
@@ -18394,13 +18675,13 @@ async function runWorkspaceCmd(args) {
18394
18675
  await handleCreate(args.slice(1));
18395
18676
  break;
18396
18677
  default:
18397
- console.log(USAGE5);
18678
+ console.log(USAGE6);
18398
18679
  process.exit(args[0] ? 1 : 0);
18399
18680
  }
18400
18681
  }
18401
18682
 
18402
18683
  // src/commands/reconcile.ts
18403
- var USAGE6 = `
18684
+ var USAGE7 = `
18404
18685
  Usage: flyspace reconcile [<workspace> | --all]
18405
18686
 
18406
18687
  Repair a workspace's local filesystem state (profile dir, config.env,
@@ -18420,7 +18701,7 @@ function printLog(name, log5) {
18420
18701
  }
18421
18702
  async function runReconcileCmd(args) {
18422
18703
  if (args.includes("-h") || args.includes("--help")) {
18423
- console.log(USAGE6);
18704
+ console.log(USAGE7);
18424
18705
  return;
18425
18706
  }
18426
18707
  loadEnvFiles();
@@ -18436,7 +18717,7 @@ async function runReconcileCmd(args) {
18436
18717
 
18437
18718
  // src/cli.ts
18438
18719
  var XERO_USAGE = false ? " xero Connect to Xero and expose it as an MCP server\n" : "";
18439
- var USAGE7 = `
18720
+ var USAGE8 = `
18440
18721
  Usage: flyspace <command> [options]
18441
18722
 
18442
18723
  Commands:
@@ -18455,7 +18736,7 @@ var USAGE7 = `
18455
18736
  usage Show agent token usage and cost for this workspace
18456
18737
  slack Query Slack (channels, users, post messages)
18457
18738
  chat Inspect dashboard chat channels and move threads between them
18458
- profiles List instances and their service status
18739
+ profiles List local profiles (bound here? scheduler?) or remove one
18459
18740
  workspace Provision a workspace (Supabase rows + local profile dirs)
18460
18741
  reconcile Repair a workspace's local files against Supabase
18461
18742
  ${XERO_USAGE} dashboard Start the read-only management dashboard (web UI)
@@ -18543,8 +18824,9 @@ async function main() {
18543
18824
  case "chat":
18544
18825
  await runChatCmd(args.slice(1));
18545
18826
  break;
18827
+ case "profile":
18546
18828
  case "profiles":
18547
- await runProfiles();
18829
+ await runProfiles(args.slice(1));
18548
18830
  break;
18549
18831
  case "dashboard":
18550
18832
  await runDashboard(args.slice(1));
@@ -18554,12 +18836,12 @@ async function main() {
18554
18836
  break;
18555
18837
  case "-v":
18556
18838
  case "--version":
18557
- printRaw("0.1.5");
18839
+ printRaw("0.1.7");
18558
18840
  break;
18559
18841
  case "-h":
18560
18842
  case "--help":
18561
18843
  case void 0:
18562
- printRaw(USAGE7);
18844
+ printRaw(USAGE8);
18563
18845
  break;
18564
18846
  case "xero":
18565
18847
  if (false) {
@@ -18570,7 +18852,7 @@ async function main() {
18570
18852
  // falls through
18571
18853
  default:
18572
18854
  printRaw(`Unknown command: ${command}`);
18573
- printRaw(USAGE7);
18855
+ printRaw(USAGE8);
18574
18856
  process.exit(1);
18575
18857
  }
18576
18858
  }