flightdesk 0.2.0 → 0.2.1

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 +145 -20
  2. package/main.js.map +4 -4
  3. package/package.json +1 -1
package/main.js CHANGED
@@ -3800,11 +3800,54 @@ async function authCommand() {
3800
3800
  }
3801
3801
  }
3802
3802
 
3803
+ // apps/cli/src/lib/git.ts
3804
+ var import_node_child_process = require("node:child_process");
3805
+ function detectGitRepo() {
3806
+ try {
3807
+ const remoteUrl = (0, import_node_child_process.execSync)("git remote get-url origin", {
3808
+ encoding: "utf-8",
3809
+ stdio: ["pipe", "pipe", "pipe"]
3810
+ }).trim();
3811
+ const repoFullName = parseGitRemoteUrl(remoteUrl);
3812
+ if (!repoFullName) {
3813
+ return null;
3814
+ }
3815
+ const branch = (0, import_node_child_process.execSync)("git rev-parse --abbrev-ref HEAD", {
3816
+ encoding: "utf-8",
3817
+ stdio: ["pipe", "pipe", "pipe"]
3818
+ }).trim();
3819
+ return {
3820
+ remote: repoFullName,
3821
+ branch,
3822
+ remoteUrl
3823
+ };
3824
+ } catch {
3825
+ return null;
3826
+ }
3827
+ }
3828
+ function parseGitRemoteUrl(remoteUrl) {
3829
+ const sshPattern = /git@github\.com:([^/]+\/[^/]+?)(?:\.git)?$/;
3830
+ const sshMatch = sshPattern.exec(remoteUrl);
3831
+ if (sshMatch) {
3832
+ return sshMatch[1];
3833
+ }
3834
+ const httpsPattern = /https:\/\/github\.com\/([^/]+\/[^/]+?)(?:\.git)?$/;
3835
+ const httpsMatch = httpsPattern.exec(remoteUrl);
3836
+ if (httpsMatch) {
3837
+ return httpsMatch[1];
3838
+ }
3839
+ return null;
3840
+ }
3841
+
3803
3842
  // apps/cli/src/commands/register.ts
3804
- async function registerCommand(projectId, taskId, options) {
3843
+ async function registerCommand(taskId, options) {
3805
3844
  const { config, org: org2 } = requireActiveOrg();
3806
3845
  const api = FlightDeskAPI.fromConfig(config, org2);
3807
3846
  try {
3847
+ let projectId = options.project;
3848
+ if (!projectId) {
3849
+ projectId = await autoDetectProject(api, org2);
3850
+ }
3808
3851
  let viewUrl = options.viewUrl;
3809
3852
  let teleportId = options.teleportId;
3810
3853
  if (!process.stdin.isTTY) {
@@ -3875,6 +3918,53 @@ function parseClaudeOutput(output) {
3875
3918
  }
3876
3919
  return result;
3877
3920
  }
3921
+ async function autoDetectProject(api, activeOrg) {
3922
+ const repoInfo = detectGitRepo();
3923
+ if (!repoInfo) {
3924
+ console.error("\u274C Not in a git repository. Please provide a project ID.");
3925
+ console.error(" Usage: flightdesk register --project <project-id> [task-id]");
3926
+ process.exit(1);
3927
+ }
3928
+ console.log(`\u{1F50D} Detecting project from repository: ${repoInfo.remote}`);
3929
+ const mappedOrg = getOrganizationByRepo(repoInfo.remote);
3930
+ if (!mappedOrg) {
3931
+ console.error(`\u274C Repository "${repoInfo.remote}" is not mapped to any organization.`);
3932
+ console.error(" Run: flightdesk sync");
3933
+ console.error(" Or provide a project ID: flightdesk register --project <project-id> [task-id]");
3934
+ process.exit(1);
3935
+ }
3936
+ if (mappedOrg.id !== activeOrg.id) {
3937
+ console.error(`\u274C Repository "${repoInfo.remote}" is mapped to organization "${mappedOrg.name}",`);
3938
+ console.error(` but your active organization is "${activeOrg.name}".`);
3939
+ console.error(" Switch with: flightdesk org switch " + mappedOrg.name);
3940
+ console.error(" Or provide a project ID: flightdesk register --project <project-id> [task-id]");
3941
+ process.exit(1);
3942
+ }
3943
+ const projects = await api.listProjects();
3944
+ const matchingProjects = projects.filter(
3945
+ (p) => p.githubRepo?.toLowerCase() === repoInfo.remote.toLowerCase()
3946
+ );
3947
+ if (matchingProjects.length === 0) {
3948
+ console.error(`\u274C No FlightDesk project found for repository "${repoInfo.remote}".`);
3949
+ console.error(" Available projects in this organization:");
3950
+ for (const p of projects) {
3951
+ console.error(` - ${p.name} (${p.githubRepo || "no repo"}) [${p.id}]`);
3952
+ }
3953
+ console.error("\n Create a project or provide a project ID explicitly.");
3954
+ process.exit(1);
3955
+ }
3956
+ if (matchingProjects.length > 1) {
3957
+ console.error(`\u274C Multiple projects found for repository "${repoInfo.remote}":`);
3958
+ for (const p of matchingProjects) {
3959
+ console.error(` - ${p.name} [${p.id}]`);
3960
+ }
3961
+ console.error("\n Please specify the project ID explicitly.");
3962
+ process.exit(1);
3963
+ }
3964
+ const project2 = matchingProjects[0];
3965
+ console.log(`\u2705 Found project: ${project2.name} (${project2.id})`);
3966
+ return project2.id;
3967
+ }
3878
3968
 
3879
3969
  // apps/cli/src/commands/status.ts
3880
3970
  async function statusCommand(options) {
@@ -4941,10 +5031,27 @@ ${projects.length} project(s)`);
4941
5031
  }
4942
5032
 
4943
5033
  // apps/cli/src/commands/preview.ts
4944
- var import_child_process2 = require("child_process");
4945
- var path3 = __toESM(require("path"));
4946
- var os3 = __toESM(require("os"));
4947
- var fs4 = __toESM(require("fs"));
5034
+ var import_node_child_process2 = require("node:child_process");
5035
+ var path3 = __toESM(require("node:path"));
5036
+ var os3 = __toESM(require("node:os"));
5037
+ var fs4 = __toESM(require("node:fs"));
5038
+ function isValidContainerId(id) {
5039
+ return /^[a-fA-F0-9]+$/.test(id) && id.length >= 12 && id.length <= 64;
5040
+ }
5041
+ function validateSSHParams(instance) {
5042
+ if (instance.containerId && !isValidContainerId(instance.containerId)) {
5043
+ throw new Error(`Invalid container ID format: ${instance.containerId}`);
5044
+ }
5045
+ if (!/^[a-zA-Z0-9_-]+$/.test(instance.sshUser)) {
5046
+ throw new Error(`Invalid SSH user format: ${instance.sshUser}`);
5047
+ }
5048
+ if (!/^[a-zA-Z0-9.-]+$/.test(instance.sshHost)) {
5049
+ throw new Error(`Invalid SSH host format: ${instance.sshHost}`);
5050
+ }
5051
+ if (!Number.isInteger(instance.sshPort) || instance.sshPort < 1 || instance.sshPort > 65535) {
5052
+ throw new Error(`Invalid SSH port: ${instance.sshPort}`);
5053
+ }
5054
+ }
4948
5055
  async function previewCommand(action, options) {
4949
5056
  const { config, org: org2 } = requireActiveOrg();
4950
5057
  const api = FlightDeskAPI.fromConfig(config, org2);
@@ -5028,8 +5135,9 @@ async function handleLogs(api, options) {
5028
5135
  `);
5029
5136
  console.log(`(Press Ctrl+C to stop)
5030
5137
  `);
5138
+ validateSSHParams(instance);
5031
5139
  const sshCommand = `docker logs -f ${instance.containerId}`;
5032
- const ssh = (0, import_child_process2.spawn)("ssh", [
5140
+ const ssh = (0, import_node_child_process2.spawn)("ssh", [
5033
5141
  "-o",
5034
5142
  "StrictHostKeyChecking=no",
5035
5143
  "-o",
@@ -5069,7 +5177,7 @@ async function handleMount(api, options) {
5069
5177
  await new Promise((resolve) => setTimeout(resolve, 3e3));
5070
5178
  }
5071
5179
  try {
5072
- (0, import_child_process2.execSync)("which sshfs", { stdio: "ignore" });
5180
+ (0, import_node_child_process2.execSync)("which sshfs", { stdio: "ignore" });
5073
5181
  } catch {
5074
5182
  console.error("\u274C sshfs is not installed.");
5075
5183
  console.error("");
@@ -5083,12 +5191,14 @@ async function handleMount(api, options) {
5083
5191
  }
5084
5192
  process.exit(1);
5085
5193
  }
5086
- const mountDir = options.directory || path3.join(os3.homedir(), "flightdesk-mounts", options.taskId.substring(0, 8));
5194
+ const rawTaskIdPrefix = options.taskId.substring(0, 8);
5195
+ const safeTaskId = path3.basename(rawTaskIdPrefix).replaceAll(/[^a-zA-Z0-9_-]/g, "") || "task";
5196
+ const mountDir = options.directory || path3.join(os3.homedir(), "flightdesk-mounts", safeTaskId);
5087
5197
  if (!fs4.existsSync(mountDir)) {
5088
5198
  fs4.mkdirSync(mountDir, { recursive: true });
5089
5199
  }
5090
5200
  try {
5091
- const mounted = (0, import_child_process2.execSync)("mount", { encoding: "utf8" });
5201
+ const mounted = (0, import_node_child_process2.execSync)("mount", { encoding: "utf8" });
5092
5202
  if (mounted.includes(mountDir)) {
5093
5203
  console.log(`\u{1F4C1} Already mounted at ${mountDir}`);
5094
5204
  return;
@@ -5096,8 +5206,8 @@ async function handleMount(api, options) {
5096
5206
  } catch {
5097
5207
  }
5098
5208
  console.log(`\u{1F4C1} Mounting preview environment to ${mountDir}...`);
5099
- const sshfsCmd = [
5100
- "sshfs",
5209
+ validateSSHParams(instance);
5210
+ const sshfsArgs = [
5101
5211
  "-o",
5102
5212
  "StrictHostKeyChecking=no",
5103
5213
  "-o",
@@ -5114,7 +5224,10 @@ async function handleMount(api, options) {
5114
5224
  mountDir
5115
5225
  ];
5116
5226
  try {
5117
- (0, import_child_process2.execSync)(sshfsCmd.join(" "), { stdio: "inherit" });
5227
+ const result = (0, import_node_child_process2.spawnSync)("sshfs", sshfsArgs, { stdio: "inherit" });
5228
+ if (result.status !== 0) {
5229
+ throw new Error(`sshfs exited with code ${result.status}`);
5230
+ }
5118
5231
  console.log("");
5119
5232
  console.log("\u2705 Mounted successfully!");
5120
5233
  console.log(` Location: ${mountDir}`);
@@ -5134,21 +5247,27 @@ async function handleMount(api, options) {
5134
5247
  }
5135
5248
  }
5136
5249
  async function handleUnmount(_api, options) {
5137
- const mountDir = path3.join(os3.homedir(), "flightdesk-mounts", options.taskId.substring(0, 8));
5250
+ const rawTaskIdPrefix = options.taskId.substring(0, 8);
5251
+ const safeTaskId = path3.basename(rawTaskIdPrefix).replaceAll(/[^a-zA-Z0-9_-]/g, "") || "task";
5252
+ const mountDir = path3.join(os3.homedir(), "flightdesk-mounts", safeTaskId);
5138
5253
  if (!fs4.existsSync(mountDir)) {
5139
5254
  console.log("Mount directory does not exist");
5140
5255
  return;
5141
5256
  }
5142
5257
  console.log(`\u{1F4C1} Unmounting ${mountDir}...`);
5143
5258
  try {
5259
+ let result;
5144
5260
  if (process.platform === "darwin") {
5145
- (0, import_child_process2.execSync)(`umount ${mountDir}`, { stdio: "inherit" });
5261
+ result = (0, import_node_child_process2.spawnSync)("umount", [mountDir], { stdio: "inherit" });
5146
5262
  } else {
5147
- (0, import_child_process2.execSync)(`fusermount -u ${mountDir}`, { stdio: "inherit" });
5263
+ result = (0, import_node_child_process2.spawnSync)("fusermount", ["-u", mountDir], { stdio: "inherit" });
5264
+ }
5265
+ if (result.status !== 0) {
5266
+ throw new Error(`Unmount exited with code ${result.status}`);
5148
5267
  }
5149
5268
  console.log("\u2705 Unmounted successfully");
5150
5269
  try {
5151
- fs4.rmdirSync(mountDir);
5270
+ fs4.rmSync(mountDir, { recursive: true, force: true });
5152
5271
  } catch {
5153
5272
  }
5154
5273
  } catch (error) {
@@ -5181,7 +5300,7 @@ async function handleTeardown(api, options) {
5181
5300
 
5182
5301
  // apps/cli/src/main.ts
5183
5302
  var program2 = new Command();
5184
- program2.name("flightdesk").description("FlightDesk CLI - AI task management for Claude Code sessions").version("0.2.0").option("--dev", "Use local development API (localhost:3000)").option("--api <url>", "Use custom API URL");
5303
+ program2.name("flightdesk").description("FlightDesk CLI - AI task management for Claude Code sessions").version("0.2.1").option("--dev", "Use local development API (localhost:3000)").option("--api <url>", "Use custom API URL");
5185
5304
  program2.hook("preAction", () => {
5186
5305
  const opts = program2.opts();
5187
5306
  if (opts.api) {
@@ -5195,7 +5314,7 @@ program2.hook("preAction", () => {
5195
5314
  });
5196
5315
  program2.command("init").description("Configure FlightDesk CLI with your API credentials").action(initCommand);
5197
5316
  program2.command("auth").description("Log in to Claude for session monitoring").action(authCommand);
5198
- program2.command("register <project-id> [task-id]").description("Register a Claude Code session with a FlightDesk task").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);
5317
+ 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
5318
  var project = program2.command("project").description("Project management commands");
5200
5319
  project.command("list").description("List projects in the active organization").action(() => projectCommand("list", {}));
5201
5320
  var task = program2.command("task").description("Task management commands");
@@ -5215,7 +5334,10 @@ program2.command("context").description("Show current repository context and map
5215
5334
  program2.command("sync").description("Refresh project-to-repository mappings from all organizations").action(syncCommand);
5216
5335
  var preview = program2.command("preview").description("Preview environment management");
5217
5336
  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) => previewCommand("logs", { taskId, lines: parseInt(options.lines || "100"), follow: options.follow }));
5337
+ 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) => {
5338
+ const lines = Math.min(Math.max(Number.parseInt(options.lines || "100"), 1), 1e4);
5339
+ previewCommand("logs", { taskId, lines, follow: options.follow });
5340
+ });
5219
5341
  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
5342
  preview.command("unmount <task-id>").description("Unmount preview environment filesystem").action((taskId) => previewCommand("unmount", { taskId }));
5221
5343
  preview.command("restart <task-id>").description("Restart preview environment processes").action((taskId) => previewCommand("restart", { taskId }));
@@ -5223,6 +5345,9 @@ preview.command("resume <task-id>").description("Resume a suspended preview envi
5223
5345
  preview.command("teardown <task-id>").description("Tear down preview environment").action((taskId) => previewCommand("teardown", { taskId }));
5224
5346
  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
5347
  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 (shorthand for "preview logs")').option("-n, --lines <lines>", "Number of log lines", "100").option("-f, --follow", "Follow log output").action((taskId, options) => previewCommand("logs", { taskId, lines: parseInt(options.lines || "100"), follow: options.follow }));
5348
+ 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) => {
5349
+ const lines = Math.min(Math.max(Number.parseInt(options.lines || "100"), 1), 1e4);
5350
+ previewCommand("logs", { taskId, lines, follow: options.follow });
5351
+ });
5227
5352
  program2.parse();
5228
5353
  //# sourceMappingURL=main.js.map