flightdesk 0.2.0 → 0.2.2
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/main.js +404 -81
- package/main.js.map +4 -4
- package/package.json +1 -1
package/main.js
CHANGED
|
@@ -3579,6 +3579,86 @@ var fs2 = __toESM(require("node:fs"));
|
|
|
3579
3579
|
var readline2 = __toESM(require("node:readline"));
|
|
3580
3580
|
var playwright = null;
|
|
3581
3581
|
var USER_DATA_DIR = path2.join(os2.homedir(), ".flightdesk", "chromium-profile");
|
|
3582
|
+
var STORAGE_STATE_FILE = path2.join(os2.homedir(), ".flightdesk", "auth-state.json");
|
|
3583
|
+
var PersistentBrowser = class {
|
|
3584
|
+
constructor(headless = true) {
|
|
3585
|
+
this.browser = null;
|
|
3586
|
+
this.context = null;
|
|
3587
|
+
this.page = null;
|
|
3588
|
+
this.headless = headless;
|
|
3589
|
+
}
|
|
3590
|
+
/**
|
|
3591
|
+
* Initialize the browser context (if not already initialized)
|
|
3592
|
+
*/
|
|
3593
|
+
async init() {
|
|
3594
|
+
if (this.context) return;
|
|
3595
|
+
if (!await isPlaywrightAvailable()) {
|
|
3596
|
+
throw new Error("Playwright not available");
|
|
3597
|
+
}
|
|
3598
|
+
ensureUserDataDir();
|
|
3599
|
+
this.browser = await playwright.chromium.launch({
|
|
3600
|
+
headless: this.headless
|
|
3601
|
+
});
|
|
3602
|
+
const hasAuthState = fs2.existsSync(STORAGE_STATE_FILE);
|
|
3603
|
+
const contextOptions = {
|
|
3604
|
+
viewport: { width: 1280, height: 720 },
|
|
3605
|
+
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
|
3606
|
+
};
|
|
3607
|
+
if (hasAuthState) {
|
|
3608
|
+
contextOptions.storageState = STORAGE_STATE_FILE;
|
|
3609
|
+
}
|
|
3610
|
+
this.context = await this.browser.newContext(contextOptions);
|
|
3611
|
+
this.page = await this.context.newPage();
|
|
3612
|
+
this.page.setDefaultTimeout(3e4);
|
|
3613
|
+
}
|
|
3614
|
+
/**
|
|
3615
|
+
* Check if user is logged into Claude
|
|
3616
|
+
*/
|
|
3617
|
+
async checkAuth() {
|
|
3618
|
+
await this.init();
|
|
3619
|
+
try {
|
|
3620
|
+
await this.page.goto("https://claude.ai/", { waitUntil: "networkidle", timeout: 3e4 });
|
|
3621
|
+
await this.page.waitForTimeout(2e3);
|
|
3622
|
+
const url = this.page.url();
|
|
3623
|
+
console.log(" Final URL:", url);
|
|
3624
|
+
return !url.includes("/login") && !url.includes("/oauth") && !url.includes("accounts.google") && url.includes("claude.ai");
|
|
3625
|
+
} catch (error) {
|
|
3626
|
+
console.error("Auth check failed:", error.message);
|
|
3627
|
+
return false;
|
|
3628
|
+
}
|
|
3629
|
+
}
|
|
3630
|
+
/**
|
|
3631
|
+
* Monitor a Claude Code session URL
|
|
3632
|
+
* Uses shared scrapeSession helper for consistent behavior with one-shot mode.
|
|
3633
|
+
*/
|
|
3634
|
+
async monitorSession(sessionUrl, options = {}) {
|
|
3635
|
+
await this.init();
|
|
3636
|
+
if (!this.page) {
|
|
3637
|
+
return { status: "error", error: "Browser page not initialized" };
|
|
3638
|
+
}
|
|
3639
|
+
return scrapeSession(this.page, sessionUrl, options);
|
|
3640
|
+
}
|
|
3641
|
+
/**
|
|
3642
|
+
* Close the browser context and browser
|
|
3643
|
+
*/
|
|
3644
|
+
async close() {
|
|
3645
|
+
if (this.context) {
|
|
3646
|
+
try {
|
|
3647
|
+
await this.context.close();
|
|
3648
|
+
} catch {
|
|
3649
|
+
}
|
|
3650
|
+
this.context = null;
|
|
3651
|
+
this.page = null;
|
|
3652
|
+
}
|
|
3653
|
+
if (this.browser) {
|
|
3654
|
+
try {
|
|
3655
|
+
await this.browser.close();
|
|
3656
|
+
} catch {
|
|
3657
|
+
}
|
|
3658
|
+
this.browser = null;
|
|
3659
|
+
}
|
|
3660
|
+
}
|
|
3661
|
+
};
|
|
3582
3662
|
async function isPlaywrightAvailable() {
|
|
3583
3663
|
try {
|
|
3584
3664
|
playwright = await import("playwright");
|
|
@@ -3589,7 +3669,23 @@ async function isPlaywrightAvailable() {
|
|
|
3589
3669
|
}
|
|
3590
3670
|
function ensureUserDataDir() {
|
|
3591
3671
|
if (!fs2.existsSync(USER_DATA_DIR)) {
|
|
3592
|
-
fs2.mkdirSync(USER_DATA_DIR, { recursive: true });
|
|
3672
|
+
fs2.mkdirSync(USER_DATA_DIR, { recursive: true, mode: 448 });
|
|
3673
|
+
}
|
|
3674
|
+
}
|
|
3675
|
+
async function ensureStorageDir() {
|
|
3676
|
+
const storageDir = path2.dirname(STORAGE_STATE_FILE);
|
|
3677
|
+
try {
|
|
3678
|
+
await fs2.promises.mkdir(storageDir, { recursive: true, mode: 448 });
|
|
3679
|
+
} catch (err) {
|
|
3680
|
+
if (err?.code !== "EEXIST") {
|
|
3681
|
+
throw err;
|
|
3682
|
+
}
|
|
3683
|
+
}
|
|
3684
|
+
}
|
|
3685
|
+
async function setStorageFilePermissions() {
|
|
3686
|
+
try {
|
|
3687
|
+
await fs2.promises.chmod(STORAGE_STATE_FILE, 384);
|
|
3688
|
+
} catch {
|
|
3593
3689
|
}
|
|
3594
3690
|
}
|
|
3595
3691
|
async function launchBrowser(headless) {
|
|
@@ -3597,24 +3693,28 @@ async function launchBrowser(headless) {
|
|
|
3597
3693
|
throw new Error("Playwright not available");
|
|
3598
3694
|
}
|
|
3599
3695
|
ensureUserDataDir();
|
|
3600
|
-
const
|
|
3601
|
-
|
|
3696
|
+
const browser = await playwright.chromium.launch({ headless });
|
|
3697
|
+
const hasAuthState = fs2.existsSync(STORAGE_STATE_FILE);
|
|
3698
|
+
const contextOptions = {
|
|
3602
3699
|
viewport: { width: 1280, height: 720 },
|
|
3603
3700
|
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
|
3604
|
-
}
|
|
3605
|
-
|
|
3701
|
+
};
|
|
3702
|
+
if (hasAuthState) {
|
|
3703
|
+
contextOptions.storageState = STORAGE_STATE_FILE;
|
|
3704
|
+
}
|
|
3705
|
+
const context = await browser.newContext(contextOptions);
|
|
3606
3706
|
return { browser, context };
|
|
3607
3707
|
}
|
|
3608
3708
|
async function checkAuth() {
|
|
3609
3709
|
if (!await isPlaywrightAvailable()) {
|
|
3610
3710
|
throw new Error("Playwright not installed");
|
|
3611
3711
|
}
|
|
3612
|
-
const { context } = await launchBrowser(true);
|
|
3712
|
+
const { browser, context } = await launchBrowser(true);
|
|
3613
3713
|
try {
|
|
3614
3714
|
const page = await context.newPage();
|
|
3615
3715
|
page.setDefaultTimeout(3e4);
|
|
3616
|
-
await page.goto("https://claude.ai/", { waitUntil: "
|
|
3617
|
-
await page.waitForTimeout(
|
|
3716
|
+
await page.goto("https://claude.ai/", { waitUntil: "networkidle", timeout: 3e4 });
|
|
3717
|
+
await page.waitForTimeout(2e3);
|
|
3618
3718
|
const url = page.url();
|
|
3619
3719
|
console.log(" Final URL:", url);
|
|
3620
3720
|
const isLoggedIn = !url.includes("/login") && !url.includes("/oauth") && !url.includes("accounts.google") && url.includes("claude.ai");
|
|
@@ -3624,6 +3724,7 @@ async function checkAuth() {
|
|
|
3624
3724
|
return false;
|
|
3625
3725
|
} finally {
|
|
3626
3726
|
await context.close();
|
|
3727
|
+
await browser.close();
|
|
3627
3728
|
}
|
|
3628
3729
|
}
|
|
3629
3730
|
async function openForLogin() {
|
|
@@ -3643,6 +3744,13 @@ async function openForLogin() {
|
|
|
3643
3744
|
const url = page.url();
|
|
3644
3745
|
console.log(" Final URL:", url);
|
|
3645
3746
|
const isLoggedIn = !url.includes("/login") && !url.includes("/oauth") && !url.includes("accounts.google") && url.includes("claude.ai");
|
|
3747
|
+
if (isLoggedIn) {
|
|
3748
|
+
await ensureStorageDir();
|
|
3749
|
+
console.log("Saving session state...");
|
|
3750
|
+
await context.storageState({ path: STORAGE_STATE_FILE });
|
|
3751
|
+
await setStorageFilePermissions();
|
|
3752
|
+
console.log(` Saved to: ${STORAGE_STATE_FILE}`);
|
|
3753
|
+
}
|
|
3646
3754
|
console.log("Closing browser...");
|
|
3647
3755
|
return isLoggedIn;
|
|
3648
3756
|
} finally {
|
|
@@ -3664,17 +3772,15 @@ function waitForEnter() {
|
|
|
3664
3772
|
});
|
|
3665
3773
|
});
|
|
3666
3774
|
}
|
|
3667
|
-
async function
|
|
3668
|
-
|
|
3669
|
-
return { status: "error", error: "Playwright not installed" };
|
|
3670
|
-
}
|
|
3671
|
-
const { headless = true, timeout = 3e4, autoPr = false } = options;
|
|
3672
|
-
const { context } = await launchBrowser(headless);
|
|
3775
|
+
async function scrapeSession(page, sessionUrl, options = {}) {
|
|
3776
|
+
const { timeout = 3e4, autoPr = false } = options;
|
|
3673
3777
|
try {
|
|
3674
|
-
const page = await context.newPage();
|
|
3675
3778
|
page.setDefaultTimeout(timeout);
|
|
3676
|
-
await page.goto(sessionUrl, { waitUntil: "
|
|
3677
|
-
await page.
|
|
3779
|
+
await page.goto(sessionUrl, { waitUntil: "networkidle" });
|
|
3780
|
+
await page.waitForSelector('[data-testid="conversation-turn"], .code-spinner-animate, button:has-text("Create PR")', {
|
|
3781
|
+
timeout: 1e4
|
|
3782
|
+
}).catch(() => {
|
|
3783
|
+
});
|
|
3678
3784
|
const result = { status: "active" };
|
|
3679
3785
|
const archiveIndicator = await page.$("text=This session has been archived");
|
|
3680
3786
|
if (archiveIndicator) {
|
|
@@ -3687,15 +3793,18 @@ async function monitorSession(sessionUrl, options = {}) {
|
|
|
3687
3793
|
error: "Not logged in to Claude. Run: flightdesk auth"
|
|
3688
3794
|
};
|
|
3689
3795
|
}
|
|
3796
|
+
const isActive = await detectActiveSpinner(page);
|
|
3797
|
+
result.isActive = isActive;
|
|
3690
3798
|
const branchName = await extractBranchName(page);
|
|
3691
3799
|
if (branchName) {
|
|
3692
3800
|
result.branchName = branchName;
|
|
3693
3801
|
}
|
|
3694
|
-
const
|
|
3695
|
-
|
|
3696
|
-
|
|
3802
|
+
const createPrButton = await page.$('button:has-text("Create PR"):not([aria-haspopup])');
|
|
3803
|
+
const viewPrButton = await page.$('button:has-text("View PR")');
|
|
3804
|
+
result.hasPrButton = !!createPrButton && !viewPrButton;
|
|
3805
|
+
if (autoPr && createPrButton && !viewPrButton) {
|
|
3697
3806
|
console.log(' Clicking "Create PR" button...');
|
|
3698
|
-
await
|
|
3807
|
+
await createPrButton.click();
|
|
3699
3808
|
await page.waitForTimeout(5e3);
|
|
3700
3809
|
const prUrl = await extractPrUrl(page);
|
|
3701
3810
|
if (prUrl) {
|
|
@@ -3708,29 +3817,54 @@ async function monitorSession(sessionUrl, options = {}) {
|
|
|
3708
3817
|
status: "error",
|
|
3709
3818
|
error: error instanceof Error ? error.message : String(error)
|
|
3710
3819
|
};
|
|
3711
|
-
}
|
|
3712
|
-
|
|
3820
|
+
}
|
|
3821
|
+
}
|
|
3822
|
+
function isValidBranchName(text) {
|
|
3823
|
+
if (!text) return false;
|
|
3824
|
+
const cleaned = text.trim();
|
|
3825
|
+
return cleaned.length > 2 && !cleaned.includes(" ");
|
|
3826
|
+
}
|
|
3827
|
+
function matchesBranchPattern(text) {
|
|
3828
|
+
return /^[a-zA-Z0-9_-]+\/[a-zA-Z0-9_-]+/.test(text) && !text.includes(" ");
|
|
3829
|
+
}
|
|
3830
|
+
async function tryExtractFromElement(page, selector) {
|
|
3831
|
+
try {
|
|
3832
|
+
const element = await page.$(selector);
|
|
3833
|
+
if (!element) return null;
|
|
3834
|
+
const text = await element.textContent();
|
|
3835
|
+
const cleaned = text?.trim() ?? null;
|
|
3836
|
+
return isValidBranchName(cleaned) ? cleaned : null;
|
|
3837
|
+
} catch {
|
|
3838
|
+
return null;
|
|
3713
3839
|
}
|
|
3714
3840
|
}
|
|
3715
3841
|
async function extractBranchName(page) {
|
|
3842
|
+
if (!page) return null;
|
|
3716
3843
|
const primarySelectors = [
|
|
3717
|
-
String.raw`button.group\/copy span.truncate
|
|
3844
|
+
String.raw`button.group\/copy span.truncate`,
|
|
3718
3845
|
// New branch name (verified)
|
|
3846
|
+
'button[class*="group/copy"] span.truncate',
|
|
3847
|
+
// Alternative escaping
|
|
3848
|
+
String.raw`button[class*="group\/copy"] span.truncate`
|
|
3849
|
+
// CSS escape
|
|
3719
3850
|
];
|
|
3720
3851
|
for (const selector of primarySelectors) {
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3852
|
+
const result = await tryExtractFromElement(page, selector);
|
|
3853
|
+
if (result) return result;
|
|
3854
|
+
}
|
|
3855
|
+
return await tryExtractBranchFromSpans(page);
|
|
3856
|
+
}
|
|
3857
|
+
async function tryExtractBranchFromSpans(page) {
|
|
3858
|
+
try {
|
|
3859
|
+
const allSpans = await page.$$("span.truncate");
|
|
3860
|
+
for (const span of allSpans) {
|
|
3861
|
+
const text = await span.textContent();
|
|
3862
|
+
const cleaned = text?.trim();
|
|
3863
|
+
if (cleaned && matchesBranchPattern(cleaned)) {
|
|
3864
|
+
return cleaned;
|
|
3731
3865
|
}
|
|
3732
|
-
} catch {
|
|
3733
3866
|
}
|
|
3867
|
+
} catch {
|
|
3734
3868
|
}
|
|
3735
3869
|
return null;
|
|
3736
3870
|
}
|
|
@@ -3768,6 +3902,36 @@ async function extractPrUrl(page) {
|
|
|
3768
3902
|
}
|
|
3769
3903
|
return null;
|
|
3770
3904
|
}
|
|
3905
|
+
async function detectActiveSpinner(page) {
|
|
3906
|
+
try {
|
|
3907
|
+
const spinner = await page.$(".code-spinner-animate");
|
|
3908
|
+
if (spinner) {
|
|
3909
|
+
return true;
|
|
3910
|
+
}
|
|
3911
|
+
const spinnerByContent = await page.$('span:has-text("\u273D")');
|
|
3912
|
+
if (spinnerByContent) {
|
|
3913
|
+
const hasAnimation = await spinnerByContent.evaluate((el) => {
|
|
3914
|
+
let current = el;
|
|
3915
|
+
while (current) {
|
|
3916
|
+
const classList = current.classList;
|
|
3917
|
+
if (classList?.contains("code-spinner-animate")) {
|
|
3918
|
+
return true;
|
|
3919
|
+
}
|
|
3920
|
+
const style = globalThis.getComputedStyle(current);
|
|
3921
|
+
if (style.animationName && style.animationName !== "none") {
|
|
3922
|
+
return true;
|
|
3923
|
+
}
|
|
3924
|
+
current = current.parentElement;
|
|
3925
|
+
}
|
|
3926
|
+
return false;
|
|
3927
|
+
});
|
|
3928
|
+
return hasAnimation;
|
|
3929
|
+
}
|
|
3930
|
+
return false;
|
|
3931
|
+
} catch {
|
|
3932
|
+
return false;
|
|
3933
|
+
}
|
|
3934
|
+
}
|
|
3771
3935
|
|
|
3772
3936
|
// apps/cli/src/commands/auth.ts
|
|
3773
3937
|
async function authCommand() {
|
|
@@ -3800,11 +3964,54 @@ async function authCommand() {
|
|
|
3800
3964
|
}
|
|
3801
3965
|
}
|
|
3802
3966
|
|
|
3967
|
+
// apps/cli/src/lib/git.ts
|
|
3968
|
+
var import_node_child_process = require("node:child_process");
|
|
3969
|
+
function detectGitRepo() {
|
|
3970
|
+
try {
|
|
3971
|
+
const remoteUrl = (0, import_node_child_process.execSync)("git remote get-url origin", {
|
|
3972
|
+
encoding: "utf-8",
|
|
3973
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3974
|
+
}).trim();
|
|
3975
|
+
const repoFullName = parseGitRemoteUrl(remoteUrl);
|
|
3976
|
+
if (!repoFullName) {
|
|
3977
|
+
return null;
|
|
3978
|
+
}
|
|
3979
|
+
const branch = (0, import_node_child_process.execSync)("git rev-parse --abbrev-ref HEAD", {
|
|
3980
|
+
encoding: "utf-8",
|
|
3981
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3982
|
+
}).trim();
|
|
3983
|
+
return {
|
|
3984
|
+
remote: repoFullName,
|
|
3985
|
+
branch,
|
|
3986
|
+
remoteUrl
|
|
3987
|
+
};
|
|
3988
|
+
} catch {
|
|
3989
|
+
return null;
|
|
3990
|
+
}
|
|
3991
|
+
}
|
|
3992
|
+
function parseGitRemoteUrl(remoteUrl) {
|
|
3993
|
+
const sshPattern = /git@github\.com:([^/]+\/[^/]+?)(?:\.git)?$/;
|
|
3994
|
+
const sshMatch = sshPattern.exec(remoteUrl);
|
|
3995
|
+
if (sshMatch) {
|
|
3996
|
+
return sshMatch[1];
|
|
3997
|
+
}
|
|
3998
|
+
const httpsPattern = /https:\/\/github\.com\/([^/]+\/[^/]+?)(?:\.git)?$/;
|
|
3999
|
+
const httpsMatch = httpsPattern.exec(remoteUrl);
|
|
4000
|
+
if (httpsMatch) {
|
|
4001
|
+
return httpsMatch[1];
|
|
4002
|
+
}
|
|
4003
|
+
return null;
|
|
4004
|
+
}
|
|
4005
|
+
|
|
3803
4006
|
// apps/cli/src/commands/register.ts
|
|
3804
|
-
async function registerCommand(
|
|
4007
|
+
async function registerCommand(taskId, options) {
|
|
3805
4008
|
const { config, org: org2 } = requireActiveOrg();
|
|
3806
4009
|
const api = FlightDeskAPI.fromConfig(config, org2);
|
|
3807
4010
|
try {
|
|
4011
|
+
let projectId = options.project;
|
|
4012
|
+
if (!projectId) {
|
|
4013
|
+
projectId = await autoDetectProject(api, org2);
|
|
4014
|
+
}
|
|
3808
4015
|
let viewUrl = options.viewUrl;
|
|
3809
4016
|
let teleportId = options.teleportId;
|
|
3810
4017
|
if (!process.stdin.isTTY) {
|
|
@@ -3875,6 +4082,53 @@ function parseClaudeOutput(output) {
|
|
|
3875
4082
|
}
|
|
3876
4083
|
return result;
|
|
3877
4084
|
}
|
|
4085
|
+
async function autoDetectProject(api, activeOrg) {
|
|
4086
|
+
const repoInfo = detectGitRepo();
|
|
4087
|
+
if (!repoInfo) {
|
|
4088
|
+
console.error("\u274C Not in a git repository. Please provide a project ID.");
|
|
4089
|
+
console.error(" Usage: flightdesk register --project <project-id> [task-id]");
|
|
4090
|
+
process.exit(1);
|
|
4091
|
+
}
|
|
4092
|
+
console.log(`\u{1F50D} Detecting project from repository: ${repoInfo.remote}`);
|
|
4093
|
+
const mappedOrg = getOrganizationByRepo(repoInfo.remote);
|
|
4094
|
+
if (!mappedOrg) {
|
|
4095
|
+
console.error(`\u274C Repository "${repoInfo.remote}" is not mapped to any organization.`);
|
|
4096
|
+
console.error(" Run: flightdesk sync");
|
|
4097
|
+
console.error(" Or provide a project ID: flightdesk register --project <project-id> [task-id]");
|
|
4098
|
+
process.exit(1);
|
|
4099
|
+
}
|
|
4100
|
+
if (mappedOrg.id !== activeOrg.id) {
|
|
4101
|
+
console.error(`\u274C Repository "${repoInfo.remote}" is mapped to organization "${mappedOrg.name}",`);
|
|
4102
|
+
console.error(` but your active organization is "${activeOrg.name}".`);
|
|
4103
|
+
console.error(" Switch with: flightdesk org switch " + mappedOrg.name);
|
|
4104
|
+
console.error(" Or provide a project ID: flightdesk register --project <project-id> [task-id]");
|
|
4105
|
+
process.exit(1);
|
|
4106
|
+
}
|
|
4107
|
+
const projects = await api.listProjects();
|
|
4108
|
+
const matchingProjects = projects.filter(
|
|
4109
|
+
(p) => p.githubRepo?.toLowerCase() === repoInfo.remote.toLowerCase()
|
|
4110
|
+
);
|
|
4111
|
+
if (matchingProjects.length === 0) {
|
|
4112
|
+
console.error(`\u274C No FlightDesk project found for repository "${repoInfo.remote}".`);
|
|
4113
|
+
console.error(" Available projects in this organization:");
|
|
4114
|
+
for (const p of projects) {
|
|
4115
|
+
console.error(` - ${p.name} (${p.githubRepo || "no repo"}) [${p.id}]`);
|
|
4116
|
+
}
|
|
4117
|
+
console.error("\n Create a project or provide a project ID explicitly.");
|
|
4118
|
+
process.exit(1);
|
|
4119
|
+
}
|
|
4120
|
+
if (matchingProjects.length > 1) {
|
|
4121
|
+
console.error(`\u274C Multiple projects found for repository "${repoInfo.remote}":`);
|
|
4122
|
+
for (const p of matchingProjects) {
|
|
4123
|
+
console.error(` - ${p.name} [${p.id}]`);
|
|
4124
|
+
}
|
|
4125
|
+
console.error("\n Please specify the project ID explicitly.");
|
|
4126
|
+
process.exit(1);
|
|
4127
|
+
}
|
|
4128
|
+
const project2 = matchingProjects[0];
|
|
4129
|
+
console.log(`\u2705 Found project: ${project2.name} (${project2.id})`);
|
|
4130
|
+
return project2.id;
|
|
4131
|
+
}
|
|
3878
4132
|
|
|
3879
4133
|
// apps/cli/src/commands/status.ts
|
|
3880
4134
|
async function statusCommand(options) {
|
|
@@ -3976,17 +4230,23 @@ async function watchCommand(options) {
|
|
|
3976
4230
|
console.log(` Auto-PR: ${options.autoPr ? "enabled" : "disabled"}`);
|
|
3977
4231
|
console.log("");
|
|
3978
4232
|
const playwrightAvailable = await isPlaywrightAvailable();
|
|
4233
|
+
let browser = null;
|
|
3979
4234
|
if (!playwrightAvailable) {
|
|
3980
4235
|
console.log("\u26A0\uFE0F Playwright not installed. Session monitoring disabled.");
|
|
3981
4236
|
console.log(" Install with: pnpm add playwright && npx playwright install chromium");
|
|
3982
4237
|
console.log("");
|
|
3983
4238
|
} else {
|
|
3984
|
-
|
|
4239
|
+
browser = new PersistentBrowser(options.headless !== false);
|
|
4240
|
+
console.log("Checking Claude authentication...");
|
|
4241
|
+
const isAuthenticated = await browser.checkAuth();
|
|
3985
4242
|
if (!isAuthenticated) {
|
|
3986
4243
|
console.log("\u26A0\uFE0F Not logged into Claude. Run: flightdesk auth");
|
|
3987
4244
|
console.log("");
|
|
4245
|
+
await browser.close();
|
|
4246
|
+
browser = null;
|
|
3988
4247
|
} else {
|
|
3989
4248
|
console.log("\u2705 Playwright ready, Claude authenticated");
|
|
4249
|
+
console.log(" (Browser session kept alive for monitoring)");
|
|
3990
4250
|
console.log("");
|
|
3991
4251
|
}
|
|
3992
4252
|
}
|
|
@@ -4021,38 +4281,49 @@ async function watchCommand(options) {
|
|
|
4021
4281
|
if (reconciled) {
|
|
4022
4282
|
continue;
|
|
4023
4283
|
}
|
|
4024
|
-
if (task2.sessionViewUrl &&
|
|
4284
|
+
if (task2.sessionViewUrl && browser) {
|
|
4025
4285
|
console.log(` Checking session...`);
|
|
4026
|
-
const sessionInfo = await monitorSession(task2.sessionViewUrl, {
|
|
4027
|
-
headless: options.headless !== false,
|
|
4286
|
+
const sessionInfo = await browser.monitorSession(task2.sessionViewUrl, {
|
|
4028
4287
|
autoPr: options.autoPr && !task2.prUrl
|
|
4029
4288
|
// Only auto-PR if no PR exists
|
|
4030
4289
|
});
|
|
4031
4290
|
await processSessionInfo(api, task2, sessionInfo);
|
|
4032
4291
|
} else if (!task2.sessionViewUrl) {
|
|
4033
4292
|
console.log(" No session URL registered");
|
|
4293
|
+
} else if (!browser) {
|
|
4294
|
+
console.log(" Browser not available - cannot monitor session");
|
|
4034
4295
|
}
|
|
4035
4296
|
}
|
|
4036
4297
|
} catch (error) {
|
|
4037
4298
|
console.error(` Error: ${error}`);
|
|
4038
4299
|
}
|
|
4039
4300
|
}
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4301
|
+
try {
|
|
4302
|
+
await runCheck();
|
|
4303
|
+
if (options.once) {
|
|
4304
|
+
console.log("\nDone (--once flag specified)");
|
|
4305
|
+
return;
|
|
4306
|
+
}
|
|
4307
|
+
console.log(`
|
|
4046
4308
|
Watching... (Ctrl+C to stop)
|
|
4047
4309
|
`);
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4310
|
+
const intervalId = setInterval(runCheck, intervalMinutes * 60 * 1e3);
|
|
4311
|
+
process.on("SIGINT", async () => {
|
|
4312
|
+
console.log("\nShutting down...");
|
|
4313
|
+
clearInterval(intervalId);
|
|
4314
|
+
if (browser) {
|
|
4315
|
+
console.log("Closing browser...");
|
|
4316
|
+
await browser.close();
|
|
4317
|
+
}
|
|
4318
|
+
process.exit(0);
|
|
4319
|
+
});
|
|
4320
|
+
await new Promise((_resolve) => {
|
|
4321
|
+
});
|
|
4322
|
+
} finally {
|
|
4323
|
+
if (browser) {
|
|
4324
|
+
await browser.close();
|
|
4325
|
+
}
|
|
4326
|
+
}
|
|
4056
4327
|
}
|
|
4057
4328
|
async function reconcileTaskStatus(api, task2) {
|
|
4058
4329
|
let expectedStatus = null;
|
|
@@ -4096,7 +4367,25 @@ async function processSessionInfo(api, task2, info) {
|
|
|
4096
4367
|
console.log(" \u{1F4E6} Session archived");
|
|
4097
4368
|
return;
|
|
4098
4369
|
}
|
|
4099
|
-
|
|
4370
|
+
let activityIcon;
|
|
4371
|
+
let activityLabel;
|
|
4372
|
+
if (info.isActive === true) {
|
|
4373
|
+
activityIcon = "\u26A1";
|
|
4374
|
+
activityLabel = "Claude is working";
|
|
4375
|
+
} else if (info.isActive === false) {
|
|
4376
|
+
activityIcon = "\u{1F4A4}";
|
|
4377
|
+
activityLabel = "Waiting for input";
|
|
4378
|
+
} else {
|
|
4379
|
+
activityIcon = "\u2754";
|
|
4380
|
+
activityLabel = "Activity unknown";
|
|
4381
|
+
}
|
|
4382
|
+
console.log(` ${activityIcon} ${activityLabel}`);
|
|
4383
|
+
const updates = {
|
|
4384
|
+
// Update session activity state and timestamp
|
|
4385
|
+
// Preserve null when activity detection fails (info.isActive is undefined)
|
|
4386
|
+
sessionActive: info.isActive ?? null,
|
|
4387
|
+
sessionCheckedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4388
|
+
};
|
|
4100
4389
|
if (info.branchName && info.branchName !== task2.branchName) {
|
|
4101
4390
|
console.log(` \u{1F33F} Branch detected: ${info.branchName}`);
|
|
4102
4391
|
updates.branchName = info.branchName;
|
|
@@ -4116,15 +4405,14 @@ async function processSessionInfo(api, task2, info) {
|
|
|
4116
4405
|
if (info.hasPrButton && !info.prUrl && !task2.prUrl) {
|
|
4117
4406
|
console.log(' \u{1F4DD} "Create PR" button available');
|
|
4118
4407
|
}
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4408
|
+
const hasNonActivityUpdates = Object.keys(updates).some((k) => !["sessionActive", "sessionCheckedAt"].includes(k));
|
|
4409
|
+
try {
|
|
4410
|
+
await api.updateTask(task2.id, updates);
|
|
4411
|
+
if (hasNonActivityUpdates) {
|
|
4122
4412
|
console.log(" \u2705 Task updated");
|
|
4123
|
-
} catch (error) {
|
|
4124
|
-
console.log(` \u274C Failed to update task: ${error}`);
|
|
4125
4413
|
}
|
|
4126
|
-
}
|
|
4127
|
-
console.log(
|
|
4414
|
+
} catch (error) {
|
|
4415
|
+
console.log(` \u274C Failed to update task: ${error}`);
|
|
4128
4416
|
}
|
|
4129
4417
|
}
|
|
4130
4418
|
|
|
@@ -4941,10 +5229,27 @@ ${projects.length} project(s)`);
|
|
|
4941
5229
|
}
|
|
4942
5230
|
|
|
4943
5231
|
// apps/cli/src/commands/preview.ts
|
|
4944
|
-
var
|
|
4945
|
-
var path3 = __toESM(require("path"));
|
|
4946
|
-
var os3 = __toESM(require("os"));
|
|
4947
|
-
var fs4 = __toESM(require("fs"));
|
|
5232
|
+
var import_node_child_process2 = require("node:child_process");
|
|
5233
|
+
var path3 = __toESM(require("node:path"));
|
|
5234
|
+
var os3 = __toESM(require("node:os"));
|
|
5235
|
+
var fs4 = __toESM(require("node:fs"));
|
|
5236
|
+
function isValidContainerId(id) {
|
|
5237
|
+
return /^[a-fA-F0-9]+$/.test(id) && id.length >= 12 && id.length <= 64;
|
|
5238
|
+
}
|
|
5239
|
+
function validateSSHParams(instance) {
|
|
5240
|
+
if (instance.containerId && !isValidContainerId(instance.containerId)) {
|
|
5241
|
+
throw new Error(`Invalid container ID format: ${instance.containerId}`);
|
|
5242
|
+
}
|
|
5243
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(instance.sshUser)) {
|
|
5244
|
+
throw new Error(`Invalid SSH user format: ${instance.sshUser}`);
|
|
5245
|
+
}
|
|
5246
|
+
if (!/^[a-zA-Z0-9.-]+$/.test(instance.sshHost)) {
|
|
5247
|
+
throw new Error(`Invalid SSH host format: ${instance.sshHost}`);
|
|
5248
|
+
}
|
|
5249
|
+
if (!Number.isInteger(instance.sshPort) || instance.sshPort < 1 || instance.sshPort > 65535) {
|
|
5250
|
+
throw new Error(`Invalid SSH port: ${instance.sshPort}`);
|
|
5251
|
+
}
|
|
5252
|
+
}
|
|
4948
5253
|
async function previewCommand(action, options) {
|
|
4949
5254
|
const { config, org: org2 } = requireActiveOrg();
|
|
4950
5255
|
const api = FlightDeskAPI.fromConfig(config, org2);
|
|
@@ -5028,8 +5333,9 @@ async function handleLogs(api, options) {
|
|
|
5028
5333
|
`);
|
|
5029
5334
|
console.log(`(Press Ctrl+C to stop)
|
|
5030
5335
|
`);
|
|
5336
|
+
validateSSHParams(instance);
|
|
5031
5337
|
const sshCommand = `docker logs -f ${instance.containerId}`;
|
|
5032
|
-
const ssh = (0,
|
|
5338
|
+
const ssh = (0, import_node_child_process2.spawn)("ssh", [
|
|
5033
5339
|
"-o",
|
|
5034
5340
|
"StrictHostKeyChecking=no",
|
|
5035
5341
|
"-o",
|
|
@@ -5069,7 +5375,7 @@ async function handleMount(api, options) {
|
|
|
5069
5375
|
await new Promise((resolve) => setTimeout(resolve, 3e3));
|
|
5070
5376
|
}
|
|
5071
5377
|
try {
|
|
5072
|
-
(0,
|
|
5378
|
+
(0, import_node_child_process2.execSync)("which sshfs", { stdio: "ignore" });
|
|
5073
5379
|
} catch {
|
|
5074
5380
|
console.error("\u274C sshfs is not installed.");
|
|
5075
5381
|
console.error("");
|
|
@@ -5083,12 +5389,14 @@ async function handleMount(api, options) {
|
|
|
5083
5389
|
}
|
|
5084
5390
|
process.exit(1);
|
|
5085
5391
|
}
|
|
5086
|
-
const
|
|
5392
|
+
const rawTaskIdPrefix = options.taskId.substring(0, 8);
|
|
5393
|
+
const safeTaskId = path3.basename(rawTaskIdPrefix).replaceAll(/[^a-zA-Z0-9_-]/g, "") || "task";
|
|
5394
|
+
const mountDir = options.directory || path3.join(os3.homedir(), "flightdesk-mounts", safeTaskId);
|
|
5087
5395
|
if (!fs4.existsSync(mountDir)) {
|
|
5088
5396
|
fs4.mkdirSync(mountDir, { recursive: true });
|
|
5089
5397
|
}
|
|
5090
5398
|
try {
|
|
5091
|
-
const mounted = (0,
|
|
5399
|
+
const mounted = (0, import_node_child_process2.execSync)("mount", { encoding: "utf8" });
|
|
5092
5400
|
if (mounted.includes(mountDir)) {
|
|
5093
5401
|
console.log(`\u{1F4C1} Already mounted at ${mountDir}`);
|
|
5094
5402
|
return;
|
|
@@ -5096,8 +5404,8 @@ async function handleMount(api, options) {
|
|
|
5096
5404
|
} catch {
|
|
5097
5405
|
}
|
|
5098
5406
|
console.log(`\u{1F4C1} Mounting preview environment to ${mountDir}...`);
|
|
5099
|
-
|
|
5100
|
-
|
|
5407
|
+
validateSSHParams(instance);
|
|
5408
|
+
const sshfsArgs = [
|
|
5101
5409
|
"-o",
|
|
5102
5410
|
"StrictHostKeyChecking=no",
|
|
5103
5411
|
"-o",
|
|
@@ -5114,7 +5422,10 @@ async function handleMount(api, options) {
|
|
|
5114
5422
|
mountDir
|
|
5115
5423
|
];
|
|
5116
5424
|
try {
|
|
5117
|
-
(0,
|
|
5425
|
+
const result = (0, import_node_child_process2.spawnSync)("sshfs", sshfsArgs, { stdio: "inherit" });
|
|
5426
|
+
if (result.status !== 0) {
|
|
5427
|
+
throw new Error(`sshfs exited with code ${result.status}`);
|
|
5428
|
+
}
|
|
5118
5429
|
console.log("");
|
|
5119
5430
|
console.log("\u2705 Mounted successfully!");
|
|
5120
5431
|
console.log(` Location: ${mountDir}`);
|
|
@@ -5134,21 +5445,27 @@ async function handleMount(api, options) {
|
|
|
5134
5445
|
}
|
|
5135
5446
|
}
|
|
5136
5447
|
async function handleUnmount(_api, options) {
|
|
5137
|
-
const
|
|
5448
|
+
const rawTaskIdPrefix = options.taskId.substring(0, 8);
|
|
5449
|
+
const safeTaskId = path3.basename(rawTaskIdPrefix).replaceAll(/[^a-zA-Z0-9_-]/g, "") || "task";
|
|
5450
|
+
const mountDir = path3.join(os3.homedir(), "flightdesk-mounts", safeTaskId);
|
|
5138
5451
|
if (!fs4.existsSync(mountDir)) {
|
|
5139
5452
|
console.log("Mount directory does not exist");
|
|
5140
5453
|
return;
|
|
5141
5454
|
}
|
|
5142
5455
|
console.log(`\u{1F4C1} Unmounting ${mountDir}...`);
|
|
5143
5456
|
try {
|
|
5457
|
+
let result;
|
|
5144
5458
|
if (process.platform === "darwin") {
|
|
5145
|
-
(0,
|
|
5459
|
+
result = (0, import_node_child_process2.spawnSync)("umount", [mountDir], { stdio: "inherit" });
|
|
5146
5460
|
} else {
|
|
5147
|
-
(0,
|
|
5461
|
+
result = (0, import_node_child_process2.spawnSync)("fusermount", ["-u", mountDir], { stdio: "inherit" });
|
|
5462
|
+
}
|
|
5463
|
+
if (result.status !== 0) {
|
|
5464
|
+
throw new Error(`Unmount exited with code ${result.status}`);
|
|
5148
5465
|
}
|
|
5149
5466
|
console.log("\u2705 Unmounted successfully");
|
|
5150
5467
|
try {
|
|
5151
|
-
fs4.
|
|
5468
|
+
fs4.rmSync(mountDir, { recursive: true, force: true });
|
|
5152
5469
|
} catch {
|
|
5153
5470
|
}
|
|
5154
5471
|
} catch (error) {
|
|
@@ -5181,7 +5498,7 @@ async function handleTeardown(api, options) {
|
|
|
5181
5498
|
|
|
5182
5499
|
// apps/cli/src/main.ts
|
|
5183
5500
|
var program2 = new Command();
|
|
5184
|
-
program2.name("flightdesk").description("FlightDesk CLI - AI task management for Claude Code sessions").version("0.2.
|
|
5501
|
+
program2.name("flightdesk").description("FlightDesk CLI - AI task management for Claude Code sessions").version("0.2.2").option("--dev", "Use local development API (localhost:3000)").option("--api <url>", "Use custom API URL");
|
|
5185
5502
|
program2.hook("preAction", () => {
|
|
5186
5503
|
const opts = program2.opts();
|
|
5187
5504
|
if (opts.api) {
|
|
@@ -5195,7 +5512,7 @@ program2.hook("preAction", () => {
|
|
|
5195
5512
|
});
|
|
5196
5513
|
program2.command("init").description("Configure FlightDesk CLI with your API credentials").action(initCommand);
|
|
5197
5514
|
program2.command("auth").description("Log in to Claude for session monitoring").action(authCommand);
|
|
5198
|
-
program2.command("register
|
|
5515
|
+
program2.command("register [task-id]").description("Register a Claude Code session with a FlightDesk task (auto-detects project from git repo)").option("-p, --project <id>", "Project ID (auto-detected from git repo if not provided)").option("--view-url <url>", "Claude Code session view URL").option("--teleport-id <id>", "Claude Code teleport ID").option("--title <title>", "Task title (creates new task if task-id not provided)").option("--description <description>", "Task description").action(registerCommand);
|
|
5199
5516
|
var project = program2.command("project").description("Project management commands");
|
|
5200
5517
|
project.command("list").description("List projects in the active organization").action(() => projectCommand("list", {}));
|
|
5201
5518
|
var task = program2.command("task").description("Task management commands");
|
|
@@ -5215,7 +5532,10 @@ program2.command("context").description("Show current repository context and map
|
|
|
5215
5532
|
program2.command("sync").description("Refresh project-to-repository mappings from all organizations").action(syncCommand);
|
|
5216
5533
|
var preview = program2.command("preview").description("Preview environment management");
|
|
5217
5534
|
preview.command("status <task-id>").description("Show preview environment status").action((taskId) => previewCommand("status", { taskId }));
|
|
5218
|
-
preview.command("logs <task-id>").description("Get logs from preview environment").option("-n, --lines <lines>", "Number of log lines", "100").option("-f, --follow", "Follow log output").action((taskId, options) =>
|
|
5535
|
+
preview.command("logs <task-id>").description("Get logs from preview environment").option("-n, --lines <lines>", "Number of log lines (1-10000)", "100").option("-f, --follow", "Follow log output").action((taskId, options) => {
|
|
5536
|
+
const lines = Math.min(Math.max(Number.parseInt(options.lines || "100"), 1), 1e4);
|
|
5537
|
+
previewCommand("logs", { taskId, lines, follow: options.follow });
|
|
5538
|
+
});
|
|
5219
5539
|
preview.command("mount <task-id>").description("Mount preview environment filesystem via SSHFS").option("-d, --directory <path>", "Custom mount directory").action((taskId, options) => previewCommand("mount", { taskId, directory: options.directory }));
|
|
5220
5540
|
preview.command("unmount <task-id>").description("Unmount preview environment filesystem").action((taskId) => previewCommand("unmount", { taskId }));
|
|
5221
5541
|
preview.command("restart <task-id>").description("Restart preview environment processes").action((taskId) => previewCommand("restart", { taskId }));
|
|
@@ -5223,6 +5543,9 @@ preview.command("resume <task-id>").description("Resume a suspended preview envi
|
|
|
5223
5543
|
preview.command("teardown <task-id>").description("Tear down preview environment").action((taskId) => previewCommand("teardown", { taskId }));
|
|
5224
5544
|
program2.command("mount <task-id>").description('Mount preview environment filesystem (shorthand for "preview mount")').option("-d, --directory <path>", "Custom mount directory").action((taskId, options) => previewCommand("mount", { taskId, directory: options.directory }));
|
|
5225
5545
|
program2.command("unmount <task-id>").description('Unmount preview environment filesystem (shorthand for "preview unmount")').action((taskId) => previewCommand("unmount", { taskId }));
|
|
5226
|
-
program2.command("logs <task-id>").description('Get logs from preview environment (
|
|
5546
|
+
program2.command("logs <task-id>").description('Get logs from preview environment (equivalent to "preview logs")').option("-n, --lines <lines>", "Number of log lines (1-10000)", "100").option("-f, --follow", "Follow log output").action((taskId, options) => {
|
|
5547
|
+
const lines = Math.min(Math.max(Number.parseInt(options.lines || "100"), 1), 1e4);
|
|
5548
|
+
previewCommand("logs", { taskId, lines, follow: options.follow });
|
|
5549
|
+
});
|
|
5227
5550
|
program2.parse();
|
|
5228
5551
|
//# sourceMappingURL=main.js.map
|