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.
- package/dist/cli.js +763 -481
- 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 {
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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
|
|
439
|
-
import { dirname as
|
|
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
|
|
792
|
+
return join5(paths.BASE_CONFIG_DIR, "device.key");
|
|
454
793
|
}
|
|
455
794
|
function credentialsPath() {
|
|
456
|
-
return
|
|
795
|
+
return join5(paths.BASE_CONFIG_DIR, "credentials.json");
|
|
457
796
|
}
|
|
458
797
|
function ensureDeviceKey(path = deviceKeyPath()) {
|
|
459
798
|
let created = false;
|
|
460
|
-
if (!
|
|
799
|
+
if (!existsSync6(path)) {
|
|
461
800
|
const { privateKey: privateKey2 } = generateKeyPairSync("ed25519");
|
|
462
|
-
|
|
463
|
-
|
|
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(
|
|
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 (!
|
|
812
|
+
if (!existsSync6(path)) return null;
|
|
474
813
|
try {
|
|
475
|
-
return JSON.parse(
|
|
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
|
-
|
|
482
|
-
|
|
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
|
|
956
|
+
existsSync as existsSync7,
|
|
607
957
|
lstatSync,
|
|
608
|
-
mkdirSync as
|
|
609
|
-
readdirSync,
|
|
610
|
-
readFileSync as
|
|
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
|
|
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"] ||
|
|
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"] ||
|
|
980
|
+
return process.env["CLAUDE_CONFIG_DIR"] || join6(homedir3(), ".claude");
|
|
631
981
|
}
|
|
632
982
|
function codexDir() {
|
|
633
|
-
return process.env["CODEX_HOME"] ||
|
|
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 (
|
|
987
|
+
if (existsSync7(join6(claudeDir(), ".credentials.json"))) return true;
|
|
638
988
|
try {
|
|
639
|
-
const raw = JSON.parse(
|
|
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"] ||
|
|
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 =
|
|
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 =
|
|
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:
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 !
|
|
1061
|
+
return !statSync2(path).isDirectory();
|
|
712
1062
|
} catch {
|
|
713
1063
|
return true;
|
|
714
1064
|
}
|
|
715
1065
|
}
|
|
716
1066
|
function gitRepos() {
|
|
717
|
-
const home =
|
|
1067
|
+
const home = homedir3();
|
|
718
1068
|
const found = [];
|
|
719
1069
|
for (const rootName of GIT_SCAN_ROOTS) {
|
|
720
|
-
const root =
|
|
1070
|
+
const root = join6(home, rootName);
|
|
721
1071
|
let entries2;
|
|
722
1072
|
try {
|
|
723
|
-
entries2 =
|
|
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() &&
|
|
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 =
|
|
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 =
|
|
1122
|
+
const claudeProjects = join6(claudeDir(), "projects");
|
|
773
1123
|
try {
|
|
774
|
-
for (const e of
|
|
1124
|
+
for (const e of readdirSync2(claudeProjects, { withFileTypes: true })) {
|
|
775
1125
|
if (!e.isDirectory()) continue;
|
|
776
|
-
const files = jsonlFiles(
|
|
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(
|
|
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 =
|
|
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 =
|
|
832
|
-
|
|
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 =
|
|
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 =
|
|
846
|
-
if (!
|
|
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
|
-
|
|
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 (
|
|
895
|
-
return `${
|
|
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 =
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
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
|
-
|
|
1399
|
-
|
|
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
|
-
|
|
1402
|
-
|
|
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
|
-
|
|
1405
|
-
|
|
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
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
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
|
-
|
|
1565
|
+
latest = execSync4(`npm view ${"flyspace-cli"} version`, { encoding: "utf-8", stdio: "pipe", timeout: 1e4 }).trim();
|
|
1415
1566
|
} catch {
|
|
1567
|
+
return;
|
|
1416
1568
|
}
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
const
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
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
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
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
|
-
|
|
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
|
|
1490
|
-
import { execSync as
|
|
1491
|
-
import { basename as basename2, join as
|
|
1492
|
-
import { homedir as homedir4, platform as
|
|
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
|
-
|
|
1676
|
+
execSync5(cmd, { encoding: "utf-8", stdio: "pipe" });
|
|
1505
1677
|
}
|
|
1506
1678
|
async function runUninstall() {
|
|
1507
|
-
const os =
|
|
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 (!
|
|
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 =
|
|
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 {
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
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
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
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
|
-
|
|
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
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
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
|
-
|
|
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
|
|
1805
|
+
import { execSync as execSync6 } from "child_process";
|
|
1596
1806
|
function restartInstalledProfiles() {
|
|
1597
|
-
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
3424
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
3816
|
-
const slugs =
|
|
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
|
|
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
|
|
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
|
|
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: () =>
|
|
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
|
-
|
|
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
|
|
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)
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
18271
|
+
console.log(USAGE3);
|
|
17992
18272
|
break;
|
|
17993
18273
|
}
|
|
17994
18274
|
}
|
|
17995
18275
|
|
|
17996
18276
|
// src/commands/chat.ts
|
|
17997
|
-
var
|
|
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(
|
|
18423
|
+
console.log(USAGE4);
|
|
18144
18424
|
break;
|
|
18145
18425
|
default:
|
|
18146
18426
|
console.error(`Unknown subcommand: ${subcommand}`);
|
|
18147
|
-
console.log(
|
|
18427
|
+
console.log(USAGE4);
|
|
18148
18428
|
process.exit(1);
|
|
18149
18429
|
}
|
|
18150
18430
|
}
|
|
18151
18431
|
|
|
18152
18432
|
// src/commands/mcp.ts
|
|
18153
|
-
var
|
|
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(
|
|
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(
|
|
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
|
|
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 (
|
|
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
|
|
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}" (
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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.
|
|
18839
|
+
printRaw("0.1.7");
|
|
18558
18840
|
break;
|
|
18559
18841
|
case "-h":
|
|
18560
18842
|
case "--help":
|
|
18561
18843
|
case void 0:
|
|
18562
|
-
printRaw(
|
|
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(
|
|
18855
|
+
printRaw(USAGE8);
|
|
18574
18856
|
process.exit(1);
|
|
18575
18857
|
}
|
|
18576
18858
|
}
|