flightdesk 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/main.js +211 -67
  2. package/main.js.map +3 -3
  3. package/package.json +1 -1
package/main.js CHANGED
@@ -3059,9 +3059,6 @@ function loadConfig() {
3059
3059
  if (fs.existsSync(CONFIG_FILE)) {
3060
3060
  const content = fs.readFileSync(CONFIG_FILE, "utf-8");
3061
3061
  const parsed = JSON.parse(content);
3062
- if (parsed.organizations?.[0]?.apiKey) {
3063
- return migrateOldConfig(parsed);
3064
- }
3065
3062
  return {
3066
3063
  organizations: [],
3067
3064
  repoMapping: {},
@@ -3076,22 +3073,6 @@ function loadConfig() {
3076
3073
  repoMapping: {}
3077
3074
  };
3078
3075
  }
3079
- function migrateOldConfig(oldConfig) {
3080
- console.log("\u{1F4E6} Migrating config to new format...");
3081
- const defaultOrg = oldConfig.defaultOrganization ? oldConfig.organizations.find((o) => o.id === oldConfig.defaultOrganization) : oldConfig.organizations[0];
3082
- const newConfig = {
3083
- apiKey: defaultOrg?.apiKey,
3084
- activeOrganization: defaultOrg?.id,
3085
- organizations: oldConfig.organizations.map((o) => ({
3086
- id: o.id,
3087
- name: o.name
3088
- })),
3089
- repoMapping: oldConfig.repoMapping || {}
3090
- };
3091
- saveConfig(newConfig);
3092
- console.log("\u2705 Config migrated successfully");
3093
- return newConfig;
3094
- }
3095
3076
  function saveConfig(config) {
3096
3077
  fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
3097
3078
  }
@@ -3162,6 +3143,9 @@ var readline = __toESM(require("readline"));
3162
3143
  // apps/cli/src/lib/api.ts
3163
3144
  var FlightDeskAPI = class _FlightDeskAPI {
3164
3145
  constructor(config, org2) {
3146
+ if (!config.apiKey) {
3147
+ throw new Error("API key is required. Run: flightdesk login");
3148
+ }
3165
3149
  this.apiUrl = getApiUrl();
3166
3150
  this.apiKey = config.apiKey;
3167
3151
  this.organizationId = org2.id;
@@ -3173,6 +3157,12 @@ var FlightDeskAPI = class _FlightDeskAPI {
3173
3157
  return new _FlightDeskAPI(config, org2);
3174
3158
  }
3175
3159
  async graphql(query, variables) {
3160
+ const verbose = process.env.FLIGHTDESK_DEBUG === "1";
3161
+ if (verbose) {
3162
+ console.log("\n--- GraphQL Request ---");
3163
+ console.log("URL:", `${this.apiUrl}/graphql`);
3164
+ console.log("Variables:", JSON.stringify(variables, null, 2));
3165
+ }
3176
3166
  const response = await fetch(`${this.apiUrl}/graphql`, {
3177
3167
  method: "POST",
3178
3168
  headers: {
@@ -3182,9 +3172,19 @@ var FlightDeskAPI = class _FlightDeskAPI {
3182
3172
  body: JSON.stringify({ query, variables })
3183
3173
  });
3184
3174
  if (!response.ok) {
3185
- throw new Error(`API request failed: ${response.status} ${response.statusText}`);
3175
+ const body = await response.text();
3176
+ if (verbose) {
3177
+ console.log("--- Error Response ---");
3178
+ console.log("Status:", response.status, response.statusText);
3179
+ console.log("Body:", body);
3180
+ }
3181
+ throw new Error(`API request failed: ${response.status} ${response.statusText}${verbose ? "" : " (set FLIGHTDESK_DEBUG=1 for details)"}`);
3186
3182
  }
3187
3183
  const result = await response.json();
3184
+ if (verbose) {
3185
+ console.log("--- Response ---");
3186
+ console.log(JSON.stringify(result, null, 2));
3187
+ }
3188
3188
  if (result.errors && result.errors.length > 0) {
3189
3189
  throw new Error(`GraphQL error: ${result.errors.map((e) => e.message).join(", ")}`);
3190
3190
  }
@@ -3577,7 +3577,51 @@ var path2 = __toESM(require("node:path"));
3577
3577
  var os2 = __toESM(require("node:os"));
3578
3578
  var fs2 = __toESM(require("node:fs"));
3579
3579
  var readline2 = __toESM(require("node:readline"));
3580
+ var import_node_child_process = require("node:child_process");
3580
3581
  var playwright = null;
3582
+ var PlaywrightBrowserNotInstalledError = class extends Error {
3583
+ constructor(autoInstallError) {
3584
+ const baseMessage = "Playwright browser not installed.";
3585
+ const autoInstallInfo = autoInstallError ? `
3586
+
3587
+ Auto-install failed: ${autoInstallError}
3588
+ ` : "\n\n";
3589
+ super(
3590
+ baseMessage + autoInstallInfo + "Run one of the following commands:\n\n npx playwright install chromium # Just Chromium (recommended)\n npx playwright install # All browsers\n\nThen retry your command."
3591
+ );
3592
+ this.name = "PlaywrightBrowserNotInstalledError";
3593
+ }
3594
+ };
3595
+ function isBrowserNotInstalledError(error) {
3596
+ if (!(error instanceof Error)) return false;
3597
+ return error.message.includes("Executable doesn't exist") || error.message.includes("browserType.launch") || error.message.includes("npx playwright install");
3598
+ }
3599
+ var autoInstallAttempted = false;
3600
+ function tryAutoInstallBrowsers() {
3601
+ if (autoInstallAttempted) {
3602
+ return false;
3603
+ }
3604
+ autoInstallAttempted = true;
3605
+ console.log("");
3606
+ console.log("\u{1F4E6} Playwright browser not found. Installing automatically...");
3607
+ console.log("");
3608
+ try {
3609
+ (0, import_node_child_process.execSync)("npx playwright install chromium", {
3610
+ stdio: "inherit",
3611
+ timeout: 12e4
3612
+ // 2 minute timeout
3613
+ });
3614
+ console.log("");
3615
+ console.log("\u2705 Browser installed successfully!");
3616
+ console.log("");
3617
+ return true;
3618
+ } catch (error) {
3619
+ console.error("");
3620
+ console.error("\u274C Auto-install failed:", error instanceof Error ? error.message : String(error));
3621
+ console.error("");
3622
+ return false;
3623
+ }
3624
+ }
3581
3625
  var USER_DATA_DIR = path2.join(os2.homedir(), ".flightdesk", "chromium-profile");
3582
3626
  var STORAGE_STATE_FILE = path2.join(os2.homedir(), ".flightdesk", "auth-state.json");
3583
3627
  var PersistentBrowser = class {
@@ -3587,18 +3631,47 @@ var PersistentBrowser = class {
3587
3631
  this.page = null;
3588
3632
  this.headless = headless;
3589
3633
  }
3634
+ /**
3635
+ * Get page, throwing if not initialized
3636
+ */
3637
+ get activePage() {
3638
+ if (!this.page) {
3639
+ throw new Error("Browser not initialized. Call init() first.");
3640
+ }
3641
+ return this.page;
3642
+ }
3590
3643
  /**
3591
3644
  * Initialize the browser context (if not already initialized)
3592
3645
  */
3593
3646
  async init() {
3594
3647
  if (this.context) return;
3595
- if (!await isPlaywrightAvailable()) {
3648
+ if (!await isPlaywrightAvailable() || !playwright) {
3596
3649
  throw new Error("Playwright not available");
3597
3650
  }
3598
3651
  ensureUserDataDir();
3599
- this.browser = await playwright.chromium.launch({
3600
- headless: this.headless
3601
- });
3652
+ try {
3653
+ this.browser = await playwright.chromium.launch({
3654
+ headless: this.headless
3655
+ });
3656
+ } catch (error) {
3657
+ if (isBrowserNotInstalledError(error)) {
3658
+ if (tryAutoInstallBrowsers()) {
3659
+ try {
3660
+ this.browser = await playwright.chromium.launch({
3661
+ headless: this.headless
3662
+ });
3663
+ } catch (retryError) {
3664
+ throw new PlaywrightBrowserNotInstalledError(
3665
+ retryError instanceof Error ? retryError.message : String(retryError)
3666
+ );
3667
+ }
3668
+ } else {
3669
+ throw new PlaywrightBrowserNotInstalledError();
3670
+ }
3671
+ } else {
3672
+ throw error;
3673
+ }
3674
+ }
3602
3675
  const hasAuthState = fs2.existsSync(STORAGE_STATE_FILE);
3603
3676
  const contextOptions = {
3604
3677
  viewport: { width: 1280, height: 720 },
@@ -3617,9 +3690,9 @@ var PersistentBrowser = class {
3617
3690
  async checkAuth() {
3618
3691
  await this.init();
3619
3692
  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();
3693
+ await this.activePage.goto("https://claude.ai/", { waitUntil: "domcontentloaded", timeout: 3e4 });
3694
+ await this.activePage.waitForTimeout(2e3);
3695
+ const url = this.activePage.url();
3623
3696
  console.log(" Final URL:", url);
3624
3697
  return !url.includes("/login") && !url.includes("/oauth") && !url.includes("accounts.google") && url.includes("claude.ai");
3625
3698
  } catch (error) {
@@ -3693,7 +3766,26 @@ async function launchBrowser(headless) {
3693
3766
  throw new Error("Playwright not available");
3694
3767
  }
3695
3768
  ensureUserDataDir();
3696
- const browser = await playwright.chromium.launch({ headless });
3769
+ let browser;
3770
+ try {
3771
+ browser = await playwright.chromium.launch({ headless });
3772
+ } catch (error) {
3773
+ if (isBrowserNotInstalledError(error)) {
3774
+ if (tryAutoInstallBrowsers()) {
3775
+ try {
3776
+ browser = await playwright.chromium.launch({ headless });
3777
+ } catch (retryError) {
3778
+ throw new PlaywrightBrowserNotInstalledError(
3779
+ retryError instanceof Error ? retryError.message : String(retryError)
3780
+ );
3781
+ }
3782
+ } else {
3783
+ throw new PlaywrightBrowserNotInstalledError();
3784
+ }
3785
+ } else {
3786
+ throw error;
3787
+ }
3788
+ }
3697
3789
  const hasAuthState = fs2.existsSync(STORAGE_STATE_FILE);
3698
3790
  const contextOptions = {
3699
3791
  viewport: { width: 1280, height: 720 },
@@ -3713,7 +3805,7 @@ async function checkAuth() {
3713
3805
  try {
3714
3806
  const page = await context.newPage();
3715
3807
  page.setDefaultTimeout(3e4);
3716
- await page.goto("https://claude.ai/", { waitUntil: "networkidle", timeout: 3e4 });
3808
+ await page.goto("https://claude.ai/", { waitUntil: "domcontentloaded", timeout: 3e4 });
3717
3809
  await page.waitForTimeout(2e3);
3718
3810
  const url = page.url();
3719
3811
  console.log(" Final URL:", url);
@@ -3776,7 +3868,7 @@ async function scrapeSession(page, sessionUrl, options = {}) {
3776
3868
  const { timeout = 3e4, autoPr = false } = options;
3777
3869
  try {
3778
3870
  page.setDefaultTimeout(timeout);
3779
- await page.goto(sessionUrl, { waitUntil: "networkidle" });
3871
+ await page.goto(sessionUrl, { waitUntil: "domcontentloaded", timeout });
3780
3872
  await page.waitForSelector('[data-testid="conversation-turn"], .code-spinner-animate, button:has-text("Create PR")', {
3781
3873
  timeout: 1e4
3782
3874
  }).catch(() => {
@@ -3906,7 +3998,23 @@ async function detectActiveSpinner(page) {
3906
3998
  try {
3907
3999
  const spinner = await page.$(".code-spinner-animate");
3908
4000
  if (spinner) {
3909
- return true;
4001
+ const isVisibleAndAnimating = await spinner.evaluate((el) => {
4002
+ const style = globalThis.getComputedStyle(el);
4003
+ if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") {
4004
+ return false;
4005
+ }
4006
+ if (style.animationName && style.animationName !== "none") {
4007
+ return true;
4008
+ }
4009
+ if (style.animationPlayState === "paused") {
4010
+ return false;
4011
+ }
4012
+ const rect = el.getBoundingClientRect();
4013
+ return rect.width > 0 && rect.height > 0;
4014
+ });
4015
+ if (isVisibleAndAnimating) {
4016
+ return true;
4017
+ }
3910
4018
  }
3911
4019
  const spinnerByContent = await page.$('span:has-text("\u273D")');
3912
4020
  if (spinnerByContent) {
@@ -3915,10 +4023,19 @@ async function detectActiveSpinner(page) {
3915
4023
  while (current) {
3916
4024
  const classList = current.classList;
3917
4025
  if (classList?.contains("code-spinner-animate")) {
3918
- return true;
4026
+ const style2 = globalThis.getComputedStyle(current);
4027
+ if (style2.display === "none" || style2.visibility === "hidden" || style2.opacity === "0") {
4028
+ current = current.parentElement;
4029
+ continue;
4030
+ }
4031
+ if (style2.animationPlayState === "paused") {
4032
+ current = current.parentElement;
4033
+ continue;
4034
+ }
4035
+ return style2.animationName !== "none";
3919
4036
  }
3920
4037
  const style = globalThis.getComputedStyle(current);
3921
- if (style.animationName && style.animationName !== "none") {
4038
+ if (style.animationName && style.animationName !== "none" && style.animationPlayState !== "paused") {
3922
4039
  return true;
3923
4040
  }
3924
4041
  current = current.parentElement;
@@ -3945,30 +4062,48 @@ async function authCommand() {
3945
4062
  console.log(`Profile directory: ${USER_DATA_DIR}
3946
4063
  `);
3947
4064
  console.log("Checking current authentication status...");
3948
- const isAuthenticated = await checkAuth();
3949
- if (isAuthenticated) {
3950
- console.log("\u2705 Already logged in to Claude!");
3951
- console.log("\nThe watch daemon will be able to monitor your sessions.");
3952
- return;
4065
+ try {
4066
+ const isAuthenticated = await checkAuth();
4067
+ if (isAuthenticated) {
4068
+ console.log("\u2705 Already logged in to Claude!");
4069
+ console.log("\nThe watch daemon will be able to monitor your sessions.");
4070
+ return;
4071
+ }
4072
+ } catch (error) {
4073
+ if (error instanceof PlaywrightBrowserNotInstalledError) {
4074
+ console.error("");
4075
+ console.error("\u274C " + error.message);
4076
+ process.exit(1);
4077
+ }
4078
+ throw error;
3953
4079
  }
3954
4080
  console.log("\u274C Not logged in to Claude.\n");
3955
4081
  console.log("Opening browser for login...");
3956
4082
  console.log("Please log in to your Claude account.\n");
3957
- const loginSuccessful = await openForLogin();
3958
- if (loginSuccessful) {
3959
- console.log("\n\u2705 Successfully logged in!");
3960
- console.log("The watch daemon can now monitor your Claude Code sessions.");
3961
- } else {
3962
- console.log("\n\u274C Login was not detected.");
3963
- console.log("Please try again with: flightdesk auth");
4083
+ try {
4084
+ const loginSuccessful = await openForLogin();
4085
+ if (loginSuccessful) {
4086
+ console.log("\n\u2705 Successfully logged in!");
4087
+ console.log("The watch daemon can now monitor your Claude Code sessions.");
4088
+ } else {
4089
+ console.log("\n\u274C Login was not detected.");
4090
+ console.log("Please try again with: flightdesk auth");
4091
+ }
4092
+ } catch (error) {
4093
+ if (error instanceof PlaywrightBrowserNotInstalledError) {
4094
+ console.error("");
4095
+ console.error("\u274C " + error.message);
4096
+ process.exit(1);
4097
+ }
4098
+ throw error;
3964
4099
  }
3965
4100
  }
3966
4101
 
3967
4102
  // apps/cli/src/lib/git.ts
3968
- var import_node_child_process = require("node:child_process");
4103
+ var import_node_child_process2 = require("node:child_process");
3969
4104
  function detectGitRepo() {
3970
4105
  try {
3971
- const remoteUrl = (0, import_node_child_process.execSync)("git remote get-url origin", {
4106
+ const remoteUrl = (0, import_node_child_process2.execSync)("git remote get-url origin", {
3972
4107
  encoding: "utf-8",
3973
4108
  stdio: ["pipe", "pipe", "pipe"]
3974
4109
  }).trim();
@@ -3976,7 +4111,7 @@ function detectGitRepo() {
3976
4111
  if (!repoFullName) {
3977
4112
  return null;
3978
4113
  }
3979
- const branch = (0, import_node_child_process.execSync)("git rev-parse --abbrev-ref HEAD", {
4114
+ const branch = (0, import_node_child_process2.execSync)("git rev-parse --abbrev-ref HEAD", {
3980
4115
  encoding: "utf-8",
3981
4116
  stdio: ["pipe", "pipe", "pipe"]
3982
4117
  }).trim();
@@ -4254,16 +4389,25 @@ async function watchCommand(options) {
4254
4389
  } else {
4255
4390
  browser = new PersistentBrowser(options.headless !== false);
4256
4391
  console.log("Checking Claude authentication...");
4257
- const isAuthenticated = await browser.checkAuth();
4258
- if (!isAuthenticated) {
4259
- console.log("\u26A0\uFE0F Not logged into Claude. Run: flightdesk auth");
4260
- console.log("");
4261
- await browser.close();
4262
- browser = null;
4263
- } else {
4264
- console.log("\u2705 Playwright ready, Claude authenticated");
4265
- console.log(" (Browser session kept alive for monitoring)");
4266
- console.log("");
4392
+ try {
4393
+ const isAuthenticated = await browser.checkAuth();
4394
+ if (!isAuthenticated) {
4395
+ console.log("\u26A0\uFE0F Not logged into Claude. Run: flightdesk auth");
4396
+ console.log("");
4397
+ await browser.close();
4398
+ browser = null;
4399
+ } else {
4400
+ console.log("\u2705 Playwright ready, Claude authenticated");
4401
+ console.log(" (Browser session kept alive for monitoring)");
4402
+ console.log("");
4403
+ }
4404
+ } catch (error) {
4405
+ if (error instanceof PlaywrightBrowserNotInstalledError) {
4406
+ console.error("");
4407
+ console.error("\u274C " + error.message);
4408
+ process.exit(1);
4409
+ }
4410
+ throw error;
4267
4411
  }
4268
4412
  }
4269
4413
  const api = FlightDeskAPI.fromConfig(config, org2);
@@ -5214,7 +5358,7 @@ async function scanClaudeSessions(options) {
5214
5358
  }
5215
5359
 
5216
5360
  // apps/cli/src/commands/project.ts
5217
- async function projectCommand(action, options) {
5361
+ async function projectCommand(action, _options) {
5218
5362
  const { config, org: org2 } = requireActiveOrg();
5219
5363
  const api = FlightDeskAPI.fromConfig(config, org2);
5220
5364
  switch (action) {
@@ -5245,7 +5389,7 @@ ${projects.length} project(s)`);
5245
5389
  }
5246
5390
 
5247
5391
  // apps/cli/src/commands/preview.ts
5248
- var import_node_child_process2 = require("node:child_process");
5392
+ var import_node_child_process3 = require("node:child_process");
5249
5393
  var path3 = __toESM(require("node:path"));
5250
5394
  var os3 = __toESM(require("node:os"));
5251
5395
  var fs4 = __toESM(require("node:fs"));
@@ -5351,7 +5495,7 @@ async function handleLogs(api, options) {
5351
5495
  `);
5352
5496
  validateSSHParams(instance);
5353
5497
  const sshCommand = `docker logs -f ${instance.containerId}`;
5354
- const ssh = (0, import_node_child_process2.spawn)("ssh", [
5498
+ const ssh = (0, import_node_child_process3.spawn)("ssh", [
5355
5499
  "-o",
5356
5500
  "StrictHostKeyChecking=no",
5357
5501
  "-o",
@@ -5391,7 +5535,7 @@ async function handleMount(api, options) {
5391
5535
  await new Promise((resolve) => setTimeout(resolve, 3e3));
5392
5536
  }
5393
5537
  try {
5394
- (0, import_node_child_process2.execSync)("which sshfs", { stdio: "ignore" });
5538
+ (0, import_node_child_process3.execSync)("which sshfs", { stdio: "ignore" });
5395
5539
  } catch {
5396
5540
  console.error("\u274C sshfs is not installed.");
5397
5541
  console.error("");
@@ -5412,7 +5556,7 @@ async function handleMount(api, options) {
5412
5556
  fs4.mkdirSync(mountDir, { recursive: true });
5413
5557
  }
5414
5558
  try {
5415
- const mounted = (0, import_node_child_process2.execSync)("mount", { encoding: "utf8" });
5559
+ const mounted = (0, import_node_child_process3.execSync)("mount", { encoding: "utf8" });
5416
5560
  if (mounted.includes(mountDir)) {
5417
5561
  console.log(`\u{1F4C1} Already mounted at ${mountDir}`);
5418
5562
  return;
@@ -5438,7 +5582,7 @@ async function handleMount(api, options) {
5438
5582
  mountDir
5439
5583
  ];
5440
5584
  try {
5441
- const result = (0, import_node_child_process2.spawnSync)("sshfs", sshfsArgs, { stdio: "inherit" });
5585
+ const result = (0, import_node_child_process3.spawnSync)("sshfs", sshfsArgs, { stdio: "inherit" });
5442
5586
  if (result.status !== 0) {
5443
5587
  throw new Error(`sshfs exited with code ${result.status}`);
5444
5588
  }
@@ -5472,9 +5616,9 @@ async function handleUnmount(_api, options) {
5472
5616
  try {
5473
5617
  let result;
5474
5618
  if (process.platform === "darwin") {
5475
- result = (0, import_node_child_process2.spawnSync)("umount", [mountDir], { stdio: "inherit" });
5619
+ result = (0, import_node_child_process3.spawnSync)("umount", [mountDir], { stdio: "inherit" });
5476
5620
  } else {
5477
- result = (0, import_node_child_process2.spawnSync)("fusermount", ["-u", mountDir], { stdio: "inherit" });
5621
+ result = (0, import_node_child_process3.spawnSync)("fusermount", ["-u", mountDir], { stdio: "inherit" });
5478
5622
  }
5479
5623
  if (result.status !== 0) {
5480
5624
  throw new Error(`Unmount exited with code ${result.status}`);
@@ -5514,7 +5658,7 @@ async function handleTeardown(api, options) {
5514
5658
 
5515
5659
  // apps/cli/src/main.ts
5516
5660
  var program2 = new Command();
5517
- program2.name("flightdesk").description("FlightDesk CLI - AI task management for Claude Code sessions").version("0.2.3").option("--dev", "Use local development API (localhost:3000)").option("--api <url>", "Use custom API URL");
5661
+ program2.name("flightdesk").description("FlightDesk CLI - AI task management for Claude Code sessions").version("0.2.5").option("--dev", "Use local development API (localhost:3000)").option("--api <url>", "Use custom API URL");
5518
5662
  program2.hook("preAction", () => {
5519
5663
  const opts = program2.opts();
5520
5664
  if (opts.api) {