@iloom/cli 0.7.4 → 0.7.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 (92) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +2 -4
  3. package/dist/{BranchNamingService-UB2EJGFQ.js → BranchNamingService-AO7BPIUJ.js} +2 -2
  4. package/dist/{ClaudeContextManager-M57BQUMY.js → ClaudeContextManager-Y2YJC6BU.js} +4 -4
  5. package/dist/{ClaudeService-FLZ2IXAO.js → ClaudeService-NDVFQRKC.js} +3 -3
  6. package/dist/{LoomLauncher-5PPVFTFN.js → LoomLauncher-U2B3VHPC.js} +4 -4
  7. package/dist/{PRManager-YTG6XPMG.js → PRManager-6ZJZRG5Z.js} +4 -4
  8. package/dist/README.md +2 -4
  9. package/dist/agents/iloom-issue-analyze-and-plan.md +1 -1
  10. package/dist/agents/iloom-issue-analyzer.md +1 -1
  11. package/dist/agents/iloom-issue-complexity-evaluator.md +1 -1
  12. package/dist/agents/iloom-issue-enhancer.md +1 -1
  13. package/dist/agents/iloom-issue-implementer.md +1 -1
  14. package/dist/agents/iloom-issue-planner.md +1 -1
  15. package/dist/agents/iloom-issue-reviewer.md +1 -1
  16. package/dist/{chunk-7GKMQJGQ.js → chunk-64HCHVJM.js} +2 -2
  17. package/dist/{chunk-RVLRPQU4.js → chunk-77VLG2KP.js} +20 -17
  18. package/dist/chunk-77VLG2KP.js.map +1 -0
  19. package/dist/{chunk-33P5VSKS.js → chunk-C7YW5IMS.js} +2 -2
  20. package/dist/{chunk-37V2NBYR.js → chunk-CAXFWFV6.js} +2 -2
  21. package/dist/chunk-CFQVOTHO.js +111 -0
  22. package/dist/chunk-CFQVOTHO.js.map +1 -0
  23. package/dist/{chunk-AFRICMSW.js → chunk-ENMTWE74.js} +2 -2
  24. package/dist/{chunk-ITIXKM24.js → chunk-IGKPPACU.js} +2 -2
  25. package/dist/chunk-IGKPPACU.js.map +1 -0
  26. package/dist/{chunk-EDDIAWVM.js → chunk-KSXA2NOJ.js} +3 -3
  27. package/dist/{chunk-GJMEKEI5.js → chunk-LZBSLO6S.js} +76 -1
  28. package/dist/chunk-LZBSLO6S.js.map +1 -0
  29. package/dist/{chunk-GH4FLYV5.js → chunk-NEPH2O4C.js} +2 -2
  30. package/dist/{chunk-XAHE76RL.js → chunk-O36JLYNW.js} +2 -2
  31. package/dist/{chunk-6VQNF44G.js → chunk-Q457PKGH.js} +2 -2
  32. package/dist/{chunk-JWUYPJ7K.js → chunk-TB6475EW.js} +3 -3
  33. package/dist/{chunk-7FM7AL7S.js → chunk-VYKKWU36.js} +2 -2
  34. package/dist/{chunk-453NC377.js → chunk-WZYBHD7P.js} +3 -106
  35. package/dist/chunk-WZYBHD7P.js.map +1 -0
  36. package/dist/{claude-SNWHWWWM.js → claude-V4HRPR4Z.js} +2 -2
  37. package/dist/{cleanup-PLMS2KWF.js → cleanup-DB7EFBF3.js} +9 -6
  38. package/dist/{cleanup-PLMS2KWF.js.map → cleanup-DB7EFBF3.js.map} +1 -1
  39. package/dist/cli.js +84 -63
  40. package/dist/cli.js.map +1 -1
  41. package/dist/{commit-NAGJH4J4.js → commit-NGMDWWAP.js} +4 -4
  42. package/dist/{dev-server-UKAPBGUR.js → dev-server-OAP3RZC6.js} +4 -3
  43. package/dist/{dev-server-UKAPBGUR.js.map → dev-server-OAP3RZC6.js.map} +1 -1
  44. package/dist/{feedback-ICJ44XGB.js → feedback-ZLAX3BVL.js} +3 -3
  45. package/dist/{ignite-U2JSVOEZ.js → ignite-HA2OJF6Z.js} +20 -36
  46. package/dist/ignite-HA2OJF6Z.js.map +1 -0
  47. package/dist/index.js +1 -1
  48. package/dist/index.js.map +1 -1
  49. package/dist/{init-YDKOPB54.js → init-S6IEGRSX.js} +3 -3
  50. package/dist/mcp/issue-management-server.js +157 -9
  51. package/dist/mcp/issue-management-server.js.map +1 -1
  52. package/dist/{open-QI63XQ4F.js → open-IN3LUZXX.js} +4 -3
  53. package/dist/{open-QI63XQ4F.js.map → open-IN3LUZXX.js.map} +1 -1
  54. package/dist/{projects-TWY4RT2Z.js → projects-CTRTTMSK.js} +25 -9
  55. package/dist/projects-CTRTTMSK.js.map +1 -0
  56. package/dist/prompts/issue-prompt.txt +16 -0
  57. package/dist/prompts/pr-prompt.txt +33 -13
  58. package/dist/prompts/regular-prompt.txt +7 -0
  59. package/dist/{rebase-AONLKM2V.js → rebase-RLEVFHWN.js} +3 -3
  60. package/dist/{run-YDVYORT2.js → run-QEIS2EH2.js} +4 -3
  61. package/dist/{run-YDVYORT2.js.map → run-QEIS2EH2.js.map} +1 -1
  62. package/dist/{summary-7KYFRAIM.js → summary-2KLNHVTN.js} +4 -4
  63. package/dist/{test-webserver-NRMGT2HB.js → test-webserver-J6SMNLU2.js} +3 -2
  64. package/dist/{test-webserver-NRMGT2HB.js.map → test-webserver-J6SMNLU2.js.map} +1 -1
  65. package/package.json +1 -1
  66. package/dist/chunk-453NC377.js.map +0 -1
  67. package/dist/chunk-GJMEKEI5.js.map +0 -1
  68. package/dist/chunk-ITIXKM24.js.map +0 -1
  69. package/dist/chunk-RVLRPQU4.js.map +0 -1
  70. package/dist/ignite-U2JSVOEZ.js.map +0 -1
  71. package/dist/projects-TWY4RT2Z.js.map +0 -1
  72. /package/dist/{BranchNamingService-UB2EJGFQ.js.map → BranchNamingService-AO7BPIUJ.js.map} +0 -0
  73. /package/dist/{ClaudeContextManager-M57BQUMY.js.map → ClaudeContextManager-Y2YJC6BU.js.map} +0 -0
  74. /package/dist/{ClaudeService-FLZ2IXAO.js.map → ClaudeService-NDVFQRKC.js.map} +0 -0
  75. /package/dist/{LoomLauncher-5PPVFTFN.js.map → LoomLauncher-U2B3VHPC.js.map} +0 -0
  76. /package/dist/{PRManager-YTG6XPMG.js.map → PRManager-6ZJZRG5Z.js.map} +0 -0
  77. /package/dist/{chunk-7GKMQJGQ.js.map → chunk-64HCHVJM.js.map} +0 -0
  78. /package/dist/{chunk-33P5VSKS.js.map → chunk-C7YW5IMS.js.map} +0 -0
  79. /package/dist/{chunk-37V2NBYR.js.map → chunk-CAXFWFV6.js.map} +0 -0
  80. /package/dist/{chunk-AFRICMSW.js.map → chunk-ENMTWE74.js.map} +0 -0
  81. /package/dist/{chunk-EDDIAWVM.js.map → chunk-KSXA2NOJ.js.map} +0 -0
  82. /package/dist/{chunk-GH4FLYV5.js.map → chunk-NEPH2O4C.js.map} +0 -0
  83. /package/dist/{chunk-XAHE76RL.js.map → chunk-O36JLYNW.js.map} +0 -0
  84. /package/dist/{chunk-6VQNF44G.js.map → chunk-Q457PKGH.js.map} +0 -0
  85. /package/dist/{chunk-JWUYPJ7K.js.map → chunk-TB6475EW.js.map} +0 -0
  86. /package/dist/{chunk-7FM7AL7S.js.map → chunk-VYKKWU36.js.map} +0 -0
  87. /package/dist/{claude-SNWHWWWM.js.map → claude-V4HRPR4Z.js.map} +0 -0
  88. /package/dist/{commit-NAGJH4J4.js.map → commit-NGMDWWAP.js.map} +0 -0
  89. /package/dist/{feedback-ICJ44XGB.js.map → feedback-ZLAX3BVL.js.map} +0 -0
  90. /package/dist/{init-YDKOPB54.js.map → init-S6IEGRSX.js.map} +0 -0
  91. /package/dist/{rebase-AONLKM2V.js.map → rebase-RLEVFHWN.js.map} +0 -0
  92. /package/dist/{summary-7KYFRAIM.js.map → summary-2KLNHVTN.js.map} +0 -0
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  DevServerManager
4
- } from "./chunk-AFRICMSW.js";
4
+ } from "./chunk-ENMTWE74.js";
5
+ import "./chunk-WZYBHD7P.js";
5
6
  import {
6
7
  getWorkspacePort
7
- } from "./chunk-453NC377.js";
8
+ } from "./chunk-CFQVOTHO.js";
8
9
  import {
9
10
  IdentifierParser
10
11
  } from "./chunk-5V74K5ZA.js";
@@ -237,4 +238,4 @@ Make sure the project is built (run 'il start' first)`
237
238
  export {
238
239
  OpenCommand
239
240
  };
240
- //# sourceMappingURL=open-QI63XQ4F.js.map
241
+ //# sourceMappingURL=open-IN3LUZXX.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/commands/open.ts"],"sourcesContent":["import path from 'path'\nimport fs from 'fs-extra'\nimport { execa } from 'execa'\nimport { GitWorktreeManager } from '../lib/GitWorktreeManager.js'\nimport { ProjectCapabilityDetector } from '../lib/ProjectCapabilityDetector.js'\nimport { DevServerManager } from '../lib/DevServerManager.js'\nimport { SettingsManager } from '../lib/SettingsManager.js'\nimport { IdentifierParser } from '../utils/IdentifierParser.js'\nimport { openBrowser } from '../utils/browser.js'\nimport { getWorkspacePort } from '../utils/port.js'\nimport { extractIssueNumber } from '../utils/git.js'\nimport { logger } from '../utils/logger.js'\nimport { extractSettingsOverrides } from '../utils/cli-overrides.js'\nimport type { GitWorktree } from '../types/worktree.js'\n\nexport interface OpenCommandInput {\n\tidentifier?: string\n\targs?: string[]\n}\n\ninterface ParsedOpenInput {\n\ttype: 'issue' | 'pr' | 'branch'\n\tnumber?: string | number // For issues and PRs\n\tbranchName?: string // For branches\n\toriginalInput: string\n\tautoDetected: boolean\n}\n\n/**\n * OpenCommand - Opens workspace in browser or runs CLI tool\n * Priority: Web first, CLI fallback\n */\nexport class OpenCommand {\n\tconstructor(\n\t\tprivate gitWorktreeManager = new GitWorktreeManager(),\n\t\tprivate capabilityDetector = new ProjectCapabilityDetector(),\n\t\tprivate identifierParser = new IdentifierParser(new GitWorktreeManager()),\n\t\tprivate devServerManager = new DevServerManager(),\n\t\tprivate settingsManager = new SettingsManager()\n\t) {}\n\n\tasync execute(input: OpenCommandInput): Promise<void> {\n\t\t// 1. Parse or auto-detect identifier\n\t\tconst parsed = input.identifier\n\t\t\t? await this.parseExplicitInput(input.identifier)\n\t\t\t: await this.autoDetectFromCurrentDirectory()\n\n\t\tlogger.debug(`Parsed input: ${JSON.stringify(parsed)}`)\n\n\t\t// 2. Find worktree path based on identifier\n\t\tconst worktree = await this.findWorktreeForIdentifier(parsed)\n\n\t\tlogger.info(`Found worktree at: ${worktree.path}`)\n\n\t\t// 3. Detect project capabilities\n\t\tconst { capabilities, binEntries } =\n\t\t\tawait this.capabilityDetector.detectCapabilities(worktree.path)\n\n\t\tlogger.debug(`Detected capabilities: ${capabilities.join(', ')}`)\n\n\t\t// 4. Execute based on capabilities (web first, CLI fallback)\n\t\tif (capabilities.includes('web')) {\n\t\t\tawait this.openWebBrowser(worktree)\n\t\t} else if (capabilities.includes('cli')) {\n\t\t\tawait this.runCLITool(worktree.path, binEntries, input.args ?? [])\n\t\t} else {\n\t\t\tthrow new Error(\n\t\t\t\t`No web or CLI capabilities detected for workspace at ${worktree.path}`\n\t\t\t)\n\t\t}\n\t}\n\n\t/**\n\t * Parse explicit identifier input\n\t */\n\tprivate async parseExplicitInput(identifier: string): Promise<ParsedOpenInput> {\n\t\tconst parsed = await this.identifierParser.parseForPatternDetection(identifier)\n\n\t\t// Description type should never reach open command (converted in start)\n\t\tif (parsed.type === 'description') {\n\t\t\tthrow new Error('Description input type is not supported in open command')\n\t\t}\n\n\t\tconst result: ParsedOpenInput = {\n\t\t\ttype: parsed.type,\n\t\t\toriginalInput: parsed.originalInput,\n\t\t\tautoDetected: false,\n\t\t}\n\n\t\tif (parsed.number !== undefined) {\n\t\t\tresult.number = parsed.number\n\t\t}\n\t\tif (parsed.branchName !== undefined) {\n\t\t\tresult.branchName = parsed.branchName\n\t\t}\n\n\t\treturn result\n\t}\n\n\t/**\n\t * Auto-detect identifier from current directory\n\t * Same logic as FinishCommand.autoDetectFromCurrentDirectory()\n\t */\n\tprivate async autoDetectFromCurrentDirectory(): Promise<ParsedOpenInput> {\n\t\tconst currentDir = path.basename(process.cwd())\n\n\t\t// Check for PR worktree pattern: _pr_N suffix\n\t\tconst prPattern = /_pr_(\\d+)$/\n\t\tconst prMatch = currentDir.match(prPattern)\n\n\t\tif (prMatch?.[1]) {\n\t\t\tconst prNumber = parseInt(prMatch[1], 10)\n\t\t\tlogger.debug(`Auto-detected PR #${prNumber} from directory: ${currentDir}`)\n\t\t\treturn {\n\t\t\t\ttype: 'pr',\n\t\t\t\tnumber: prNumber,\n\t\t\t\toriginalInput: currentDir,\n\t\t\t\tautoDetected: true,\n\t\t\t}\n\t\t}\n\n\t\t// Check for issue pattern in directory\n\t\tconst issueNumber = extractIssueNumber(currentDir)\n\n\t\tif (issueNumber !== null) {\n\t\t\tlogger.debug(`Auto-detected issue #${issueNumber} from directory: ${currentDir}`)\n\t\t\treturn {\n\t\t\t\ttype: 'issue',\n\t\t\t\tnumber: issueNumber,\n\t\t\t\toriginalInput: currentDir,\n\t\t\t\tautoDetected: true,\n\t\t\t}\n\t\t}\n\n\t\t// Fallback: get current branch name\n\t\tconst repoInfo = await this.gitWorktreeManager.getRepoInfo()\n\t\tconst currentBranch = repoInfo.currentBranch\n\n\t\tif (!currentBranch) {\n\t\t\tthrow new Error(\n\t\t\t\t'Could not auto-detect identifier. Please provide an issue number, PR number, or branch name.\\n' +\n\t\t\t\t\t'Expected directory pattern: feat/issue-XX-description OR worktree with _pr_N suffix'\n\t\t\t)\n\t\t}\n\n\t\t// Try to extract issue from branch name\n\t\tconst branchIssueNumber = extractIssueNumber(currentBranch)\n\t\tif (branchIssueNumber !== null) {\n\t\t\tlogger.debug(`Auto-detected issue #${branchIssueNumber} from branch: ${currentBranch}`)\n\t\t\treturn {\n\t\t\t\ttype: 'issue',\n\t\t\t\tnumber: branchIssueNumber,\n\t\t\t\toriginalInput: currentBranch,\n\t\t\t\tautoDetected: true,\n\t\t\t}\n\t\t}\n\n\t\t// Last resort: use branch name\n\t\treturn {\n\t\t\ttype: 'branch',\n\t\t\tbranchName: currentBranch,\n\t\t\toriginalInput: currentBranch,\n\t\t\tautoDetected: true,\n\t\t}\n\t}\n\n\t/**\n\t * Find worktree for the given identifier\n\t */\n\tprivate async findWorktreeForIdentifier(parsed: ParsedOpenInput): Promise<GitWorktree> {\n\t\tlet worktree: GitWorktree | null = null\n\n\t\tif (parsed.type === 'issue' && parsed.number !== undefined) {\n\t\t\tworktree = await this.gitWorktreeManager.findWorktreeForIssue(parsed.number)\n\t\t} else if (parsed.type === 'pr' && parsed.number !== undefined) {\n\t\t\t// For PRs, ensure the number is numeric (PRs are always numeric per GitHub)\n\t\t\tconst prNumber = typeof parsed.number === 'number' ? parsed.number : Number(parsed.number)\n\t\t\tif (isNaN(prNumber) || !isFinite(prNumber)) {\n\t\t\t\tthrow new Error(`Invalid PR number: ${parsed.number}. PR numbers must be numeric.`)\n\t\t\t}\n\t\t\t// Pass empty string for branch name since we don't know it yet\n\t\t\tworktree = await this.gitWorktreeManager.findWorktreeForPR(prNumber, '')\n\t\t} else if (parsed.type === 'branch' && parsed.branchName) {\n\t\t\tworktree = await this.gitWorktreeManager.findWorktreeForBranch(\n\t\t\t\tparsed.branchName\n\t\t\t)\n\t\t}\n\n\t\tif (!worktree) {\n\t\t\tthrow new Error(\n\t\t\t\t`No worktree found for ${this.formatParsedInput(parsed)}. ` +\n\t\t\t\t\t`Run 'il start ${parsed.originalInput}' to create one.`\n\t\t\t)\n\t\t}\n\n\t\treturn worktree\n\t}\n\n\t/**\n\t * Format parsed input for display\n\t */\n\tprivate formatParsedInput(parsed: ParsedOpenInput): string {\n\t\tconst autoLabel = parsed.autoDetected ? ' (auto-detected)' : ''\n\n\t\tif (parsed.type === 'issue') {\n\t\t\treturn `issue #${parsed.number}${autoLabel}`\n\t\t}\n\t\tif (parsed.type === 'pr') {\n\t\t\treturn `PR #${parsed.number}${autoLabel}`\n\t\t}\n\t\treturn `branch \"${parsed.branchName}\"${autoLabel}`\n\t}\n\n\t/**\n\t * Open web browser with workspace URL\n\t * Auto-starts dev server if not already running\n\t */\n\tprivate async openWebBrowser(worktree: GitWorktree): Promise<void> {\n\t\tconst cliOverrides = extractSettingsOverrides()\n\t\tconst settings = await this.settingsManager.loadSettings(undefined, cliOverrides)\n\t\tconst port = await getWorkspacePort({\n\t\t\tworktreePath: worktree.path,\n\t\t\tworktreeBranch: worktree.branch,\n\t\t\tbasePort: settings.capabilities?.web?.basePort,\n\t\t\tcheckEnvFile: true,\n\t\t})\n\n\t\t// Ensure dev server is running on the port\n\t\tconst serverReady = await this.devServerManager.ensureServerRunning(\n\t\t\tworktree.path,\n\t\t\tport\n\t\t)\n\n\t\tif (!serverReady) {\n\t\t\tlogger.warn(\n\t\t\t\t`Dev server failed to start on port ${port}. Opening browser anyway...`\n\t\t\t)\n\t\t}\n\n\t\t// Construct URL and open browser\n\t\tconst url = `http://localhost:${port}`\n\t\tlogger.info(`Opening browser: ${url}`)\n\t\tawait openBrowser(url)\n\t\tlogger.success('Browser opened')\n\t}\n\n\t/**\n\t * Run CLI tool directly from worktree bin path (NO SYMLINKS!)\n\t */\n\tprivate async runCLITool(\n\t\tworktreePath: string,\n\t\tbinEntries: Record<string, string>,\n\t\targs: string[]\n\t): Promise<void> {\n\t\t// Validate binEntries exist\n\t\tif (Object.keys(binEntries).length === 0) {\n\t\t\tthrow new Error('No bin entries found in package.json')\n\t\t}\n\n\t\t// Get first bin entry (deterministic)\n\t\tconst firstEntry = Object.entries(binEntries)[0]\n\t\tif (!firstEntry) {\n\t\t\tthrow new Error('No bin entries found in package.json')\n\t\t}\n\t\tconst [binName, binPath] = firstEntry\n\t\tlogger.debug(`Using bin entry: ${binName} -> ${binPath}`)\n\n\t\t// CRITICAL: Construct absolute path (NO SYMLINKS!)\n\t\tconst binFilePath = path.resolve(worktreePath, binPath)\n\t\tlogger.debug(`Resolved bin file path: ${binFilePath}`)\n\n\t\t// Verify file exists\n\t\tif (!(await fs.pathExists(binFilePath))) {\n\t\t\tthrow new Error(\n\t\t\t\t`CLI executable not found: ${binFilePath}\\n` +\n\t\t\t\t\t`Make sure the project is built (run 'il start' first)`\n\t\t\t)\n\t\t}\n\n\t\t// Execute with Node.js\n\t\tlogger.info(`Running CLI: node ${binFilePath} ${args.join(' ')}`)\n\t\tawait execa('node', [binFilePath, ...args], {\n\t\t\tstdio: 'inherit', // Allow interactive CLIs (prompts, colors, etc.)\n\t\t\tcwd: worktreePath, // Execute in worktree context\n\t\t\tenv: process.env, // Inherit environment\n\t\t})\n\t}\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/commands/open.ts"],"sourcesContent":["import path from 'path'\nimport fs from 'fs-extra'\nimport { execa } from 'execa'\nimport { GitWorktreeManager } from '../lib/GitWorktreeManager.js'\nimport { ProjectCapabilityDetector } from '../lib/ProjectCapabilityDetector.js'\nimport { DevServerManager } from '../lib/DevServerManager.js'\nimport { SettingsManager } from '../lib/SettingsManager.js'\nimport { IdentifierParser } from '../utils/IdentifierParser.js'\nimport { openBrowser } from '../utils/browser.js'\nimport { getWorkspacePort } from '../utils/port.js'\nimport { extractIssueNumber } from '../utils/git.js'\nimport { logger } from '../utils/logger.js'\nimport { extractSettingsOverrides } from '../utils/cli-overrides.js'\nimport type { GitWorktree } from '../types/worktree.js'\n\nexport interface OpenCommandInput {\n\tidentifier?: string\n\targs?: string[]\n}\n\ninterface ParsedOpenInput {\n\ttype: 'issue' | 'pr' | 'branch'\n\tnumber?: string | number // For issues and PRs\n\tbranchName?: string // For branches\n\toriginalInput: string\n\tautoDetected: boolean\n}\n\n/**\n * OpenCommand - Opens workspace in browser or runs CLI tool\n * Priority: Web first, CLI fallback\n */\nexport class OpenCommand {\n\tconstructor(\n\t\tprivate gitWorktreeManager = new GitWorktreeManager(),\n\t\tprivate capabilityDetector = new ProjectCapabilityDetector(),\n\t\tprivate identifierParser = new IdentifierParser(new GitWorktreeManager()),\n\t\tprivate devServerManager = new DevServerManager(),\n\t\tprivate settingsManager = new SettingsManager()\n\t) {}\n\n\tasync execute(input: OpenCommandInput): Promise<void> {\n\t\t// 1. Parse or auto-detect identifier\n\t\tconst parsed = input.identifier\n\t\t\t? await this.parseExplicitInput(input.identifier)\n\t\t\t: await this.autoDetectFromCurrentDirectory()\n\n\t\tlogger.debug(`Parsed input: ${JSON.stringify(parsed)}`)\n\n\t\t// 2. Find worktree path based on identifier\n\t\tconst worktree = await this.findWorktreeForIdentifier(parsed)\n\n\t\tlogger.info(`Found worktree at: ${worktree.path}`)\n\n\t\t// 3. Detect project capabilities\n\t\tconst { capabilities, binEntries } =\n\t\t\tawait this.capabilityDetector.detectCapabilities(worktree.path)\n\n\t\tlogger.debug(`Detected capabilities: ${capabilities.join(', ')}`)\n\n\t\t// 4. Execute based on capabilities (web first, CLI fallback)\n\t\tif (capabilities.includes('web')) {\n\t\t\tawait this.openWebBrowser(worktree)\n\t\t} else if (capabilities.includes('cli')) {\n\t\t\tawait this.runCLITool(worktree.path, binEntries, input.args ?? [])\n\t\t} else {\n\t\t\tthrow new Error(\n\t\t\t\t`No web or CLI capabilities detected for workspace at ${worktree.path}`\n\t\t\t)\n\t\t}\n\t}\n\n\t/**\n\t * Parse explicit identifier input\n\t */\n\tprivate async parseExplicitInput(identifier: string): Promise<ParsedOpenInput> {\n\t\tconst parsed = await this.identifierParser.parseForPatternDetection(identifier)\n\n\t\t// Description type should never reach open command (converted in start)\n\t\tif (parsed.type === 'description') {\n\t\t\tthrow new Error('Description input type is not supported in open command')\n\t\t}\n\n\t\tconst result: ParsedOpenInput = {\n\t\t\ttype: parsed.type,\n\t\t\toriginalInput: parsed.originalInput,\n\t\t\tautoDetected: false,\n\t\t}\n\n\t\tif (parsed.number !== undefined) {\n\t\t\tresult.number = parsed.number\n\t\t}\n\t\tif (parsed.branchName !== undefined) {\n\t\t\tresult.branchName = parsed.branchName\n\t\t}\n\n\t\treturn result\n\t}\n\n\t/**\n\t * Auto-detect identifier from current directory\n\t * Same logic as FinishCommand.autoDetectFromCurrentDirectory()\n\t */\n\tprivate async autoDetectFromCurrentDirectory(): Promise<ParsedOpenInput> {\n\t\tconst currentDir = path.basename(process.cwd())\n\n\t\t// Check for PR worktree pattern: _pr_N suffix\n\t\tconst prPattern = /_pr_(\\d+)$/\n\t\tconst prMatch = currentDir.match(prPattern)\n\n\t\tif (prMatch?.[1]) {\n\t\t\tconst prNumber = parseInt(prMatch[1], 10)\n\t\t\tlogger.debug(`Auto-detected PR #${prNumber} from directory: ${currentDir}`)\n\t\t\treturn {\n\t\t\t\ttype: 'pr',\n\t\t\t\tnumber: prNumber,\n\t\t\t\toriginalInput: currentDir,\n\t\t\t\tautoDetected: true,\n\t\t\t}\n\t\t}\n\n\t\t// Check for issue pattern in directory\n\t\tconst issueNumber = extractIssueNumber(currentDir)\n\n\t\tif (issueNumber !== null) {\n\t\t\tlogger.debug(`Auto-detected issue #${issueNumber} from directory: ${currentDir}`)\n\t\t\treturn {\n\t\t\t\ttype: 'issue',\n\t\t\t\tnumber: issueNumber,\n\t\t\t\toriginalInput: currentDir,\n\t\t\t\tautoDetected: true,\n\t\t\t}\n\t\t}\n\n\t\t// Fallback: get current branch name\n\t\tconst repoInfo = await this.gitWorktreeManager.getRepoInfo()\n\t\tconst currentBranch = repoInfo.currentBranch\n\n\t\tif (!currentBranch) {\n\t\t\tthrow new Error(\n\t\t\t\t'Could not auto-detect identifier. Please provide an issue number, PR number, or branch name.\\n' +\n\t\t\t\t\t'Expected directory pattern: feat/issue-XX-description OR worktree with _pr_N suffix'\n\t\t\t)\n\t\t}\n\n\t\t// Try to extract issue from branch name\n\t\tconst branchIssueNumber = extractIssueNumber(currentBranch)\n\t\tif (branchIssueNumber !== null) {\n\t\t\tlogger.debug(`Auto-detected issue #${branchIssueNumber} from branch: ${currentBranch}`)\n\t\t\treturn {\n\t\t\t\ttype: 'issue',\n\t\t\t\tnumber: branchIssueNumber,\n\t\t\t\toriginalInput: currentBranch,\n\t\t\t\tautoDetected: true,\n\t\t\t}\n\t\t}\n\n\t\t// Last resort: use branch name\n\t\treturn {\n\t\t\ttype: 'branch',\n\t\t\tbranchName: currentBranch,\n\t\t\toriginalInput: currentBranch,\n\t\t\tautoDetected: true,\n\t\t}\n\t}\n\n\t/**\n\t * Find worktree for the given identifier\n\t */\n\tprivate async findWorktreeForIdentifier(parsed: ParsedOpenInput): Promise<GitWorktree> {\n\t\tlet worktree: GitWorktree | null = null\n\n\t\tif (parsed.type === 'issue' && parsed.number !== undefined) {\n\t\t\tworktree = await this.gitWorktreeManager.findWorktreeForIssue(parsed.number)\n\t\t} else if (parsed.type === 'pr' && parsed.number !== undefined) {\n\t\t\t// For PRs, ensure the number is numeric (PRs are always numeric per GitHub)\n\t\t\tconst prNumber = typeof parsed.number === 'number' ? parsed.number : Number(parsed.number)\n\t\t\tif (isNaN(prNumber) || !isFinite(prNumber)) {\n\t\t\t\tthrow new Error(`Invalid PR number: ${parsed.number}. PR numbers must be numeric.`)\n\t\t\t}\n\t\t\t// Pass empty string for branch name since we don't know it yet\n\t\t\tworktree = await this.gitWorktreeManager.findWorktreeForPR(prNumber, '')\n\t\t} else if (parsed.type === 'branch' && parsed.branchName) {\n\t\t\tworktree = await this.gitWorktreeManager.findWorktreeForBranch(\n\t\t\t\tparsed.branchName\n\t\t\t)\n\t\t}\n\n\t\tif (!worktree) {\n\t\t\tthrow new Error(\n\t\t\t\t`No worktree found for ${this.formatParsedInput(parsed)}. ` +\n\t\t\t\t\t`Run 'il start ${parsed.originalInput}' to create one.`\n\t\t\t)\n\t\t}\n\n\t\treturn worktree\n\t}\n\n\t/**\n\t * Format parsed input for display\n\t */\n\tprivate formatParsedInput(parsed: ParsedOpenInput): string {\n\t\tconst autoLabel = parsed.autoDetected ? ' (auto-detected)' : ''\n\n\t\tif (parsed.type === 'issue') {\n\t\t\treturn `issue #${parsed.number}${autoLabel}`\n\t\t}\n\t\tif (parsed.type === 'pr') {\n\t\t\treturn `PR #${parsed.number}${autoLabel}`\n\t\t}\n\t\treturn `branch \"${parsed.branchName}\"${autoLabel}`\n\t}\n\n\t/**\n\t * Open web browser with workspace URL\n\t * Auto-starts dev server if not already running\n\t */\n\tprivate async openWebBrowser(worktree: GitWorktree): Promise<void> {\n\t\tconst cliOverrides = extractSettingsOverrides()\n\t\tconst settings = await this.settingsManager.loadSettings(undefined, cliOverrides)\n\t\tconst port = await getWorkspacePort({\n\t\t\tworktreePath: worktree.path,\n\t\t\tworktreeBranch: worktree.branch,\n\t\t\tbasePort: settings.capabilities?.web?.basePort,\n\t\t\tcheckEnvFile: true,\n\t\t})\n\n\t\t// Ensure dev server is running on the port\n\t\tconst serverReady = await this.devServerManager.ensureServerRunning(\n\t\t\tworktree.path,\n\t\t\tport\n\t\t)\n\n\t\tif (!serverReady) {\n\t\t\tlogger.warn(\n\t\t\t\t`Dev server failed to start on port ${port}. Opening browser anyway...`\n\t\t\t)\n\t\t}\n\n\t\t// Construct URL and open browser\n\t\tconst url = `http://localhost:${port}`\n\t\tlogger.info(`Opening browser: ${url}`)\n\t\tawait openBrowser(url)\n\t\tlogger.success('Browser opened')\n\t}\n\n\t/**\n\t * Run CLI tool directly from worktree bin path (NO SYMLINKS!)\n\t */\n\tprivate async runCLITool(\n\t\tworktreePath: string,\n\t\tbinEntries: Record<string, string>,\n\t\targs: string[]\n\t): Promise<void> {\n\t\t// Validate binEntries exist\n\t\tif (Object.keys(binEntries).length === 0) {\n\t\t\tthrow new Error('No bin entries found in package.json')\n\t\t}\n\n\t\t// Get first bin entry (deterministic)\n\t\tconst firstEntry = Object.entries(binEntries)[0]\n\t\tif (!firstEntry) {\n\t\t\tthrow new Error('No bin entries found in package.json')\n\t\t}\n\t\tconst [binName, binPath] = firstEntry\n\t\tlogger.debug(`Using bin entry: ${binName} -> ${binPath}`)\n\n\t\t// CRITICAL: Construct absolute path (NO SYMLINKS!)\n\t\tconst binFilePath = path.resolve(worktreePath, binPath)\n\t\tlogger.debug(`Resolved bin file path: ${binFilePath}`)\n\n\t\t// Verify file exists\n\t\tif (!(await fs.pathExists(binFilePath))) {\n\t\t\tthrow new Error(\n\t\t\t\t`CLI executable not found: ${binFilePath}\\n` +\n\t\t\t\t\t`Make sure the project is built (run 'il start' first)`\n\t\t\t)\n\t\t}\n\n\t\t// Execute with Node.js\n\t\tlogger.info(`Running CLI: node ${binFilePath} ${args.join(' ')}`)\n\t\tawait execa('node', [binFilePath, ...args], {\n\t\t\tstdio: 'inherit', // Allow interactive CLIs (prompts, colors, etc.)\n\t\t\tcwd: worktreePath, // Execute in worktree context\n\t\t\tenv: process.env, // Inherit environment\n\t\t})\n\t}\n}\n"],"mappings":"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","names":[]}
@@ -12,12 +12,30 @@ import {
12
12
  import "./chunk-VT4PDUYT.js";
13
13
 
14
14
  // src/commands/projects.ts
15
- import path from "path";
15
+ import path2 from "path";
16
16
  import os from "os";
17
17
  import fs from "fs-extra";
18
+
19
+ // src/utils/xplat-utils.ts
20
+ import path from "path";
21
+ function normalizePath(inputPath) {
22
+ const normalized = path.normalize(inputPath);
23
+ if (process.platform === "win32") {
24
+ return normalized.replace(/\\/g, "/").toLowerCase();
25
+ }
26
+ return normalized;
27
+ }
28
+ function pathsEqual(path1, path22) {
29
+ if (path1 == null || path22 == null) {
30
+ return path1 === path22;
31
+ }
32
+ return normalizePath(path1) === normalizePath(path22);
33
+ }
34
+
35
+ // src/commands/projects.ts
18
36
  var ProjectsCommand = class {
19
37
  constructor(metadataManager, capabilityDetector) {
20
- this.projectsDir = path.join(os.homedir(), ".config", "iloom-ai", "projects");
38
+ this.projectsDir = path2.join(os.homedir(), ".config", "iloom-ai", "projects");
21
39
  this.metadataManager = metadataManager ?? new MetadataManager();
22
40
  this.capabilityDetector = capabilityDetector ?? new ProjectCapabilityDetector();
23
41
  }
@@ -38,7 +56,7 @@ var ProjectsCommand = class {
38
56
  for (const file of files) {
39
57
  if (file.startsWith(".")) continue;
40
58
  try {
41
- const filePath = path.join(this.projectsDir, file);
59
+ const filePath = path2.join(this.projectsDir, file);
42
60
  const content = await fs.readFile(filePath, "utf8");
43
61
  const marker = JSON.parse(content);
44
62
  if (!marker.projectPath || !marker.projectName) continue;
@@ -63,17 +81,15 @@ var ProjectsCommand = class {
63
81
  }
64
82
  /**
65
83
  * Count active looms for a project
66
- * Looms are counted if their worktreePath is in the project's -looms directory
67
- * or if they share the same parent directory as the project.
84
+ * Uses the metadata's projectPath field to determine project membership.
68
85
  * Only counts looms where the worktree is a valid git worktree (.git exists).
69
86
  */
70
87
  async countActiveLooms(projectPath, allMetadata) {
71
88
  let count = 0;
72
89
  for (const meta of allMetadata) {
73
90
  if (!meta.worktreePath) continue;
74
- const parentDir = path.dirname(meta.worktreePath);
75
- const isProjectLoom = parentDir === path.dirname(projectPath) || parentDir.startsWith(projectPath + "-looms");
76
- const isValidWorktree = await fs.pathExists(path.join(meta.worktreePath, ".git"));
91
+ const isProjectLoom = pathsEqual(meta.projectPath, projectPath);
92
+ const isValidWorktree = await fs.pathExists(path2.join(meta.worktreePath, ".git"));
77
93
  if (isProjectLoom && isValidWorktree) {
78
94
  count++;
79
95
  }
@@ -84,4 +100,4 @@ var ProjectsCommand = class {
84
100
  export {
85
101
  ProjectsCommand
86
102
  };
87
- //# sourceMappingURL=projects-TWY4RT2Z.js.map
103
+ //# sourceMappingURL=projects-CTRTTMSK.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/projects.ts","../src/utils/xplat-utils.ts"],"sourcesContent":["import path from 'path'\nimport os from 'os'\nimport fs from 'fs-extra'\nimport { getLogger } from '../utils/logger-context.js'\nimport { pathsEqual } from '../utils/xplat-utils.js'\nimport { MetadataManager, type LoomMetadata } from '../lib/MetadataManager.js'\nimport { ProjectCapabilityDetector } from '../lib/ProjectCapabilityDetector.js'\nimport type { ProjectCapability } from '../types/loom.js'\n\n/**\n * Project marker file structure (from FirstRunManager)\n */\ninterface ProjectMarker {\n configuredAt: string\n projectPath: string\n projectName: string\n}\n\n/**\n * Output schema for project\n */\nexport interface ProjectOutput {\n configuredAt: string\n projectPath: string\n projectName: string\n activeLooms: number\n capabilities: ProjectCapability[]\n}\n\n/**\n * ProjectsCommand: List configured iloom projects\n *\n * Returns JSON array of configured projects from ~/.config/iloom-ai/projects/\n * Only includes projects where the directory still exists.\n * Each project includes an activeLooms count.\n */\nexport class ProjectsCommand {\n private readonly projectsDir: string\n private readonly metadataManager: MetadataManager\n private readonly capabilityDetector: ProjectCapabilityDetector\n\n constructor(metadataManager?: MetadataManager, capabilityDetector?: ProjectCapabilityDetector) {\n this.projectsDir = path.join(os.homedir(), '.config', 'iloom-ai', 'projects')\n this.metadataManager = metadataManager ?? new MetadataManager()\n this.capabilityDetector = capabilityDetector ?? new ProjectCapabilityDetector()\n }\n\n /**\n * Execute the projects command\n * @param _options - Options object (json flag accepted but ignored - always returns JSON)\n * @returns Array of project outputs\n */\n async execute(_options?: { json?: boolean }): Promise<ProjectOutput[]> {\n // Options.json is accepted but ignored - always returns structured data\n const results: ProjectOutput[] = []\n const logger = getLogger()\n\n try {\n // Check if projects directory exists\n if (!(await fs.pathExists(this.projectsDir))) {\n return results\n }\n\n // Read all files in projects directory\n const files = await fs.readdir(this.projectsDir)\n\n // Get all loom metadata for active looms lookup\n const allMetadata = await this.metadataManager.listAllMetadata()\n\n for (const file of files) {\n // Skip hidden files (like .DS_Store)\n if (file.startsWith('.')) continue\n\n try {\n const filePath = path.join(this.projectsDir, file)\n const content = await fs.readFile(filePath, 'utf8')\n const marker: ProjectMarker = JSON.parse(content)\n\n // Skip if required fields missing\n if (!marker.projectPath || !marker.projectName) continue\n\n // Filter: only include if directory exists\n if (!(await fs.pathExists(marker.projectPath))) continue\n\n // Count active looms for this project\n const activeLooms = await this.countActiveLooms(marker.projectPath, allMetadata)\n\n // Detect project capabilities\n const { capabilities } = await this.capabilityDetector.detectCapabilities(marker.projectPath)\n\n results.push({\n configuredAt: marker.configuredAt,\n projectPath: marker.projectPath,\n projectName: marker.projectName,\n activeLooms,\n capabilities,\n })\n } catch {\n // Skip invalid files (graceful degradation)\n logger.debug(`Skipping invalid project file: ${file}`)\n }\n }\n } catch (error) {\n // Graceful degradation on read errors\n logger.debug(`Failed to list projects: ${error instanceof Error ? error.message : 'Unknown'}`)\n }\n\n return results\n }\n\n /**\n * Count active looms for a project\n * Uses the metadata's projectPath field to determine project membership.\n * Only counts looms where the worktree is a valid git worktree (.git exists).\n */\n private async countActiveLooms(projectPath: string, allMetadata: LoomMetadata[]): Promise<number> {\n let count = 0\n for (const meta of allMetadata) {\n if (!meta.worktreePath) continue\n // Use metadata's projectPath field for accurate project membership (with cross-platform normalization)\n const isProjectLoom = pathsEqual(meta.projectPath, projectPath)\n // Check for .git (file for worktrees, directory for main repo) to verify it's a valid git worktree\n const isValidWorktree = await fs.pathExists(path.join(meta.worktreePath, '.git'))\n if (isProjectLoom && isValidWorktree) {\n count++\n }\n }\n return count\n }\n}\n","import path from 'path'\n\n/**\n * Normalize a path for cross-platform comparison.\n *\n * On Windows:\n * - Converts backslashes to forward slashes\n * - Lowercases the path (Windows paths are case-insensitive)\n *\n * On Unix:\n * - Normalizes path separators via path.normalize()\n *\n * Use this when comparing paths that may come from different sources\n * (e.g., metadata files, filesystem, user input).\n */\nexport function normalizePath(inputPath: string): string {\n // First normalize using Node's path module (handles . and .. and duplicate separators)\n const normalized = path.normalize(inputPath)\n\n if (process.platform === 'win32') {\n // Windows: convert backslashes to forward slashes and lowercase\n return normalized.replace(/\\\\/g, '/').toLowerCase()\n }\n\n return normalized\n}\n\n/**\n * Compare two paths for equality, handling cross-platform differences.\n */\nexport function pathsEqual(path1: string | null | undefined, path2: string | null | undefined): boolean {\n if (path1 == null || path2 == null) {\n return path1 === path2\n }\n return normalizePath(path1) === normalizePath(path2)\n}\n"],"mappings":";;;;;;;;;;;;;;AAAA,OAAOA,WAAU;AACjB,OAAO,QAAQ;AACf,OAAO,QAAQ;;;ACFf,OAAO,UAAU;AAeV,SAAS,cAAc,WAA2B;AAEvD,QAAM,aAAa,KAAK,UAAU,SAAS;AAE3C,MAAI,QAAQ,aAAa,SAAS;AAEhC,WAAO,WAAW,QAAQ,OAAO,GAAG,EAAE,YAAY;AAAA,EACpD;AAEA,SAAO;AACT;AAKO,SAAS,WAAW,OAAkCC,QAA2C;AACtG,MAAI,SAAS,QAAQA,UAAS,MAAM;AAClC,WAAO,UAAUA;AAAA,EACnB;AACA,SAAO,cAAc,KAAK,MAAM,cAAcA,MAAK;AACrD;;;ADCO,IAAM,kBAAN,MAAsB;AAAA,EAK3B,YAAY,iBAAmC,oBAAgD;AAC7F,SAAK,cAAcC,MAAK,KAAK,GAAG,QAAQ,GAAG,WAAW,YAAY,UAAU;AAC5E,SAAK,kBAAkB,mBAAmB,IAAI,gBAAgB;AAC9D,SAAK,qBAAqB,sBAAsB,IAAI,0BAA0B;AAAA,EAChF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,QAAQ,UAAyD;AAErE,UAAM,UAA2B,CAAC;AAClC,UAAM,SAAS,UAAU;AAEzB,QAAI;AAEF,UAAI,CAAE,MAAM,GAAG,WAAW,KAAK,WAAW,GAAI;AAC5C,eAAO;AAAA,MACT;AAGA,YAAM,QAAQ,MAAM,GAAG,QAAQ,KAAK,WAAW;AAG/C,YAAM,cAAc,MAAM,KAAK,gBAAgB,gBAAgB;AAE/D,iBAAW,QAAQ,OAAO;AAExB,YAAI,KAAK,WAAW,GAAG,EAAG;AAE1B,YAAI;AACF,gBAAM,WAAWA,MAAK,KAAK,KAAK,aAAa,IAAI;AACjD,gBAAM,UAAU,MAAM,GAAG,SAAS,UAAU,MAAM;AAClD,gBAAM,SAAwB,KAAK,MAAM,OAAO;AAGhD,cAAI,CAAC,OAAO,eAAe,CAAC,OAAO,YAAa;AAGhD,cAAI,CAAE,MAAM,GAAG,WAAW,OAAO,WAAW,EAAI;AAGhD,gBAAM,cAAc,MAAM,KAAK,iBAAiB,OAAO,aAAa,WAAW;AAG/E,gBAAM,EAAE,aAAa,IAAI,MAAM,KAAK,mBAAmB,mBAAmB,OAAO,WAAW;AAE5F,kBAAQ,KAAK;AAAA,YACX,cAAc,OAAO;AAAA,YACrB,aAAa,OAAO;AAAA,YACpB,aAAa,OAAO;AAAA,YACpB;AAAA,YACA;AAAA,UACF,CAAC;AAAA,QACH,QAAQ;AAEN,iBAAO,MAAM,kCAAkC,IAAI,EAAE;AAAA,QACvD;AAAA,MACF;AAAA,IACF,SAAS,OAAO;AAEd,aAAO,MAAM,4BAA4B,iBAAiB,QAAQ,MAAM,UAAU,SAAS,EAAE;AAAA,IAC/F;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAc,iBAAiB,aAAqB,aAA8C;AAChG,QAAI,QAAQ;AACZ,eAAW,QAAQ,aAAa;AAC9B,UAAI,CAAC,KAAK,aAAc;AAExB,YAAM,gBAAgB,WAAW,KAAK,aAAa,WAAW;AAE9D,YAAM,kBAAkB,MAAM,GAAG,WAAWA,MAAK,KAAK,KAAK,cAAc,MAAM,CAAC;AAChF,UAAI,iBAAiB,iBAAiB;AACpC;AAAA,MACF;AAAA,IACF;AACA,WAAO;AAAA,EACT;AACF;","names":["path","path2","path"]}
@@ -20,6 +20,13 @@ Before sending any response, verify it doesn't contain:
20
20
 
21
21
  ---
22
22
 
23
+ {{#if PORT}}
24
+ <additional_env>
25
+ Dev Server Port: {{PORT}}
26
+ If you need to access the web server for testing purposes, use localhost:{{PORT}}
27
+ </additional_env>
28
+ {{/if}}
29
+
23
30
  ## Loom Recap
24
31
 
25
32
  **IMPORTANT: The recap is NOT a work log. It's a knowledge capture system for the USER.**
@@ -53,8 +60,17 @@ Use these Recap MCP tools:
53
60
  **IMPORTANT: This loom is using draft PR mode.**
54
61
 
55
62
  - **Read issue details** from Issue #{{ISSUE_NUMBER}} using `mcp__issue_management__get_issue`
63
+ - **Read draft PR details** from PR #{{DRAFT_PR_NUMBER}} using `mcp__issue_management__get_pr`
56
64
  - **Write ALL workflow comments** to PR #{{DRAFT_PR_NUMBER}} using `type: "pr"`
57
65
 
66
+ When calling `mcp__issue_management__get_pr` to read the draft PR:
67
+ ```
68
+ {
69
+ number: "{{DRAFT_PR_NUMBER}}",
70
+ includeComments: true
71
+ }
72
+ ```
73
+
58
74
  When calling `mcp__issue_management__create_comment`:
59
75
  ```
60
76
  {
@@ -20,6 +20,13 @@ Before sending any response, verify it doesn't contain:
20
20
 
21
21
  ---
22
22
 
23
+ {{#if PORT}}
24
+ <additional_env>
25
+ Dev Server Port: {{PORT}}
26
+ If you need to access the web server for testing purposes, use localhost:{{PORT}}
27
+ </additional_env>
28
+ {{/if}}
29
+
23
30
  ## Loom Recap
24
31
 
25
32
  **IMPORTANT: The recap is NOT a work log. It's a knowledge capture system for the USER.**
@@ -46,19 +53,37 @@ Use these Recap MCP tools:
46
53
 
47
54
  ---
48
55
 
56
+ ## Comment Routing: PR Mode
57
+
58
+ - **Read PR details** using `mcp__issue_management__get_pr` with `{ number: "{{PR_NUMBER}}", includeComments: true }`
59
+ - **Write comments** to PR #{{PR_NUMBER}} using `type: "pr"`
60
+
61
+ When calling `mcp__issue_management__create_comment`:
62
+ ```
63
+ {
64
+ number: "{{PR_NUMBER}}",
65
+ body: "your comment content",
66
+ type: "pr"
67
+ }
68
+ ```
69
+
70
+ This ensures PR comments always go to GitHub regardless of the configured issue tracker.
71
+
72
+ ---
73
+
49
74
  You are a senior software engineer responsible for well-architected, easy to maintain and understandable code.
50
75
 
51
- Please read the Pull Request and all its details using JSON output:
52
- gh pr view {{PR_NUMBER}} --json title,body,comments,commits,files,state,createdAt,updatedAt,headRefName
76
+ Please read the Pull Request and all its details using the MCP tool:
77
+ `mcp__issue_management__get_pr` with `{ number: "{{PR_NUMBER}}", includeComments: true }`
53
78
 
54
- This single command will give you:
55
- - PR title and description
79
+ This will give you:
80
+ - PR title and description (body)
56
81
  - All comments and discussion
57
- - Commit history and changed files
58
- - Current state and metadata
82
+ - Commit history (commits) and changed files (files)
83
+ - Current state and branch metadata (headRefName, baseRefName)
59
84
 
60
85
  Also check if the branch name contains an issue number (like issue-123) and if so, read that issue for context:
61
- gh issue view {{ISSUE_NUMBER}} --json title,body,comments,assignees,labels,state,createdAt,updatedAt
86
+ `mcp__issue_management__get_issue` with `{ number: "{{ISSUE_NUMBER}}", includeComments: true }`
62
87
 
63
88
  Since this is an existing PR, the issue may be partially addressed or in an unknown state.
64
89
 
@@ -69,12 +94,7 @@ When working with libraries, frameworks, or APIs that you need documentation for
69
94
  - Working with unfamiliar APIs
70
95
  - When you need current documentation that may have changed since your training data
71
96
 
72
- After reading both the PR and any referenced issue, check if the dev server is running:
73
- 1. Check if dev server is already running: "lsof -i :{{PORT}}"
74
- 2. If not running, start it: pnpm dev (the PORT environment variable is already set)
75
- 3. If running, you can optionally restart it if your changes require it
76
-
77
- Then wait for the user to provide guidance on what they would like you to work on next.
97
+ After reading both the PR and any referenced issue, wait for the user to provide guidance on what they would like you to work on next.
78
98
 
79
99
  Remember to:
80
100
  - Follow the project's coding conventions and patterns
@@ -20,6 +20,13 @@ Before sending any response, verify it doesn't contain:
20
20
 
21
21
  ---
22
22
 
23
+ {{#if PORT}}
24
+ <additional_env>
25
+ Dev Server Port: {{PORT}}
26
+ If you need to access the web server for testing purposes, use localhost:{{PORT}}
27
+ </additional_env>
28
+ {{/if}}
29
+
23
30
  ## Loom Recap
24
31
 
25
32
  **IMPORTANT: The recap is NOT a work log. It's a knowledge capture system for the USER.**
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  BuildRunner,
4
4
  MergeManager
5
- } from "./chunk-37V2NBYR.js";
5
+ } from "./chunk-CAXFWFV6.js";
6
6
  import "./chunk-7GLZVDPQ.js";
7
7
  import {
8
8
  installDependencies
@@ -19,7 +19,7 @@ import {
19
19
  SettingsManager
20
20
  } from "./chunk-WFQ5CLTR.js";
21
21
  import "./chunk-VWGKGNJP.js";
22
- import "./chunk-ITIXKM24.js";
22
+ import "./chunk-IGKPPACU.js";
23
23
  import "./chunk-6MLEBAYZ.js";
24
24
  import {
25
25
  logger
@@ -139,4 +139,4 @@ export {
139
139
  RebaseCommand,
140
140
  WorktreeValidationError
141
141
  };
142
- //# sourceMappingURL=rebase-AONLKM2V.js.map
142
+ //# sourceMappingURL=rebase-RLEVFHWN.js.map
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  DevServerManager
4
- } from "./chunk-AFRICMSW.js";
4
+ } from "./chunk-ENMTWE74.js";
5
+ import "./chunk-WZYBHD7P.js";
5
6
  import {
6
7
  getWorkspacePort
7
- } from "./chunk-453NC377.js";
8
+ } from "./chunk-CFQVOTHO.js";
8
9
  import {
9
10
  IdentifierParser
10
11
  } from "./chunk-5V74K5ZA.js";
@@ -237,4 +238,4 @@ Make sure the project is built (run 'il start' first)`
237
238
  export {
238
239
  RunCommand
239
240
  };
240
- //# sourceMappingURL=run-YDVYORT2.js.map
241
+ //# sourceMappingURL=run-QEIS2EH2.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/commands/run.ts"],"sourcesContent":["import path from 'path'\nimport fs from 'fs-extra'\nimport { execa } from 'execa'\nimport { GitWorktreeManager } from '../lib/GitWorktreeManager.js'\nimport { ProjectCapabilityDetector } from '../lib/ProjectCapabilityDetector.js'\nimport { DevServerManager } from '../lib/DevServerManager.js'\nimport { SettingsManager } from '../lib/SettingsManager.js'\nimport { IdentifierParser } from '../utils/IdentifierParser.js'\nimport { openBrowser } from '../utils/browser.js'\nimport { getWorkspacePort } from '../utils/port.js'\nimport { extractIssueNumber } from '../utils/git.js'\nimport { logger } from '../utils/logger.js'\nimport { extractSettingsOverrides } from '../utils/cli-overrides.js'\nimport type { GitWorktree } from '../types/worktree.js'\n\nexport interface RunCommandInput {\n\tidentifier?: string\n\targs?: string[]\n}\n\ninterface ParsedRunInput {\n\ttype: 'issue' | 'pr' | 'branch'\n\tnumber?: string | number // For issues and PRs\n\tbranchName?: string // For branches\n\toriginalInput: string\n\tautoDetected: boolean\n}\n\n/**\n * RunCommand - Runs CLI tool or opens workspace in browser\n * Priority: CLI first, Web fallback\n */\nexport class RunCommand {\n\tconstructor(\n\t\tprivate gitWorktreeManager = new GitWorktreeManager(),\n\t\tprivate capabilityDetector = new ProjectCapabilityDetector(),\n\t\tprivate identifierParser = new IdentifierParser(new GitWorktreeManager()),\n\t\tprivate devServerManager = new DevServerManager(),\n\t\tprivate settingsManager = new SettingsManager()\n\t) {}\n\n\tasync execute(input: RunCommandInput): Promise<void> {\n\t\t// 1. Parse or auto-detect identifier\n\t\tconst parsed = input.identifier\n\t\t\t? await this.parseExplicitInput(input.identifier)\n\t\t\t: await this.autoDetectFromCurrentDirectory()\n\n\t\tlogger.debug(`Parsed input: ${JSON.stringify(parsed)}`)\n\n\t\t// 2. Find worktree path based on identifier\n\t\tconst worktree = await this.findWorktreeForIdentifier(parsed)\n\n\t\tlogger.info(`Found worktree at: ${worktree.path}`)\n\n\t\t// 3. Detect project capabilities\n\t\tconst { capabilities, binEntries } =\n\t\t\tawait this.capabilityDetector.detectCapabilities(worktree.path)\n\n\t\tlogger.debug(`Detected capabilities: ${capabilities.join(', ')}`)\n\n\t\t// 4. Execute based on capabilities (CLI first, web fallback)\n\t\tif (capabilities.includes('cli')) {\n\t\t\tawait this.runCLITool(worktree.path, binEntries, input.args ?? [])\n\t\t} else if (capabilities.includes('web')) {\n\t\t\tawait this.openWebBrowser(worktree)\n\t\t} else {\n\t\t\tthrow new Error(\n\t\t\t\t`No CLI or web capabilities detected for workspace at ${worktree.path}`\n\t\t\t)\n\t\t}\n\t}\n\n\t/**\n\t * Parse explicit identifier input\n\t */\n\tprivate async parseExplicitInput(identifier: string): Promise<ParsedRunInput> {\n\t\tconst parsed = await this.identifierParser.parseForPatternDetection(identifier)\n\n\t\t// Description type should never reach run command (converted in start)\n\t\tif (parsed.type === 'description') {\n\t\t\tthrow new Error('Description input type is not supported in run command')\n\t\t}\n\n\t\tconst result: ParsedRunInput = {\n\t\t\ttype: parsed.type,\n\t\t\toriginalInput: parsed.originalInput,\n\t\t\tautoDetected: false,\n\t\t}\n\n\t\tif (parsed.number !== undefined) {\n\t\t\tresult.number = parsed.number\n\t\t}\n\t\tif (parsed.branchName !== undefined) {\n\t\t\tresult.branchName = parsed.branchName\n\t\t}\n\n\t\treturn result\n\t}\n\n\t/**\n\t * Auto-detect identifier from current directory\n\t * Same logic as FinishCommand.autoDetectFromCurrentDirectory()\n\t */\n\tprivate async autoDetectFromCurrentDirectory(): Promise<ParsedRunInput> {\n\t\tconst currentDir = path.basename(process.cwd())\n\n\t\t// Check for PR worktree pattern: _pr_N suffix\n\t\tconst prPattern = /_pr_(\\d+)$/\n\t\tconst prMatch = currentDir.match(prPattern)\n\n\t\tif (prMatch?.[1]) {\n\t\t\tconst prNumber = parseInt(prMatch[1], 10)\n\t\t\tlogger.debug(`Auto-detected PR #${prNumber} from directory: ${currentDir}`)\n\t\t\treturn {\n\t\t\t\ttype: 'pr',\n\t\t\t\tnumber: prNumber,\n\t\t\t\toriginalInput: currentDir,\n\t\t\t\tautoDetected: true,\n\t\t\t}\n\t\t}\n\n\t\t// Check for issue pattern in directory\n\t\tconst issueNumber = extractIssueNumber(currentDir)\n\n\t\tif (issueNumber !== null) {\n\t\t\tlogger.debug(`Auto-detected issue #${issueNumber} from directory: ${currentDir}`)\n\t\t\treturn {\n\t\t\t\ttype: 'issue',\n\t\t\t\tnumber: issueNumber,\n\t\t\t\toriginalInput: currentDir,\n\t\t\t\tautoDetected: true,\n\t\t\t}\n\t\t}\n\n\t\t// Fallback: get current branch name\n\t\tconst repoInfo = await this.gitWorktreeManager.getRepoInfo()\n\t\tconst currentBranch = repoInfo.currentBranch\n\n\t\tif (!currentBranch) {\n\t\t\tthrow new Error(\n\t\t\t\t'Could not auto-detect identifier. Please provide an issue number, PR number, or branch name.\\n' +\n\t\t\t\t\t'Expected directory pattern: feat/issue-XX-description OR worktree with _pr_N suffix'\n\t\t\t)\n\t\t}\n\n\t\t// Try to extract issue from branch name\n\t\tconst branchIssueNumber = extractIssueNumber(currentBranch)\n\t\tif (branchIssueNumber !== null) {\n\t\t\tlogger.debug(`Auto-detected issue #${branchIssueNumber} from branch: ${currentBranch}`)\n\t\t\treturn {\n\t\t\t\ttype: 'issue',\n\t\t\t\tnumber: branchIssueNumber,\n\t\t\t\toriginalInput: currentBranch,\n\t\t\t\tautoDetected: true,\n\t\t\t}\n\t\t}\n\n\t\t// Last resort: use branch name\n\t\treturn {\n\t\t\ttype: 'branch',\n\t\t\tbranchName: currentBranch,\n\t\t\toriginalInput: currentBranch,\n\t\t\tautoDetected: true,\n\t\t}\n\t}\n\n\t/**\n\t * Find worktree for the given identifier\n\t */\n\tprivate async findWorktreeForIdentifier(parsed: ParsedRunInput): Promise<GitWorktree> {\n\t\tlet worktree: GitWorktree | null = null\n\n\t\tif (parsed.type === 'issue' && parsed.number !== undefined) {\n\t\t\tworktree = await this.gitWorktreeManager.findWorktreeForIssue(parsed.number)\n\t\t} else if (parsed.type === 'pr' && parsed.number !== undefined) {\n\t\t\t// For PRs, ensure the number is numeric (PRs are always numeric per GitHub)\n\t\t\tconst prNumber = typeof parsed.number === 'number' ? parsed.number : Number(parsed.number)\n\t\t\tif (isNaN(prNumber) || !isFinite(prNumber)) {\n\t\t\t\tthrow new Error(`Invalid PR number: ${parsed.number}. PR numbers must be numeric.`)\n\t\t\t}\n\t\t\t// Pass empty string for branch name since we don't know it yet\n\t\t\tworktree = await this.gitWorktreeManager.findWorktreeForPR(prNumber, '')\n\t\t} else if (parsed.type === 'branch' && parsed.branchName) {\n\t\t\tworktree = await this.gitWorktreeManager.findWorktreeForBranch(\n\t\t\t\tparsed.branchName\n\t\t\t)\n\t\t}\n\n\t\tif (!worktree) {\n\t\t\tthrow new Error(\n\t\t\t\t`No worktree found for ${this.formatParsedInput(parsed)}. ` +\n\t\t\t\t\t`Run 'il start ${parsed.originalInput}' to create one.`\n\t\t\t)\n\t\t}\n\n\t\treturn worktree\n\t}\n\n\t/**\n\t * Format parsed input for display\n\t */\n\tprivate formatParsedInput(parsed: ParsedRunInput): string {\n\t\tconst autoLabel = parsed.autoDetected ? ' (auto-detected)' : ''\n\n\t\tif (parsed.type === 'issue') {\n\t\t\treturn `issue #${parsed.number}${autoLabel}`\n\t\t}\n\t\tif (parsed.type === 'pr') {\n\t\t\treturn `PR #${parsed.number}${autoLabel}`\n\t\t}\n\t\treturn `branch \"${parsed.branchName}\"${autoLabel}`\n\t}\n\n\t/**\n\t * Run CLI tool directly from worktree bin path (NO SYMLINKS!)\n\t */\n\tprivate async runCLITool(\n\t\tworktreePath: string,\n\t\tbinEntries: Record<string, string>,\n\t\targs: string[]\n\t): Promise<void> {\n\t\t// Validate binEntries exist\n\t\tif (Object.keys(binEntries).length === 0) {\n\t\t\tthrow new Error('No bin entries found in package.json')\n\t\t}\n\n\t\t// Get first bin entry (deterministic)\n\t\tconst firstEntry = Object.entries(binEntries)[0]\n\t\tif (!firstEntry) {\n\t\t\tthrow new Error('No bin entries found in package.json')\n\t\t}\n\t\tconst [binName, binPath] = firstEntry\n\t\tlogger.debug(`Using bin entry: ${binName} -> ${binPath}`)\n\n\t\t// CRITICAL: Construct absolute path (NO SYMLINKS!)\n\t\tconst binFilePath = path.resolve(worktreePath, binPath)\n\t\tlogger.debug(`Resolved bin file path: ${binFilePath}`)\n\n\t\t// Verify file exists\n\t\tif (!(await fs.pathExists(binFilePath))) {\n\t\t\tthrow new Error(\n\t\t\t\t`CLI executable not found: ${binFilePath}\\n` +\n\t\t\t\t\t`Make sure the project is built (run 'il start' first)`\n\t\t\t)\n\t\t}\n\n\t\t// Execute with Node.js\n\t\tlogger.info(`Running CLI: node ${binFilePath} ${args.join(' ')}`)\n\t\tawait execa('node', [binFilePath, ...args], {\n\t\t\tstdio: 'inherit', // Allow interactive CLIs (prompts, colors, etc.)\n\t\t\tcwd: worktreePath, // Execute in worktree context\n\t\t\tenv: process.env, // Inherit environment\n\t\t})\n\t}\n\n\t/**\n\t * Open web browser with workspace URL\n\t * Auto-starts dev server if not already running\n\t */\n\tprivate async openWebBrowser(worktree: GitWorktree): Promise<void> {\n\t\tconst cliOverrides = extractSettingsOverrides()\n\t\tconst settings = await this.settingsManager.loadSettings(undefined, cliOverrides)\n\t\tconst port = await getWorkspacePort({\n\t\t\tworktreePath: worktree.path,\n\t\t\tworktreeBranch: worktree.branch,\n\t\t\tbasePort: settings.capabilities?.web?.basePort,\n\t\t\tcheckEnvFile: true,\n\t\t})\n\n\t\t// Ensure dev server is running on the port\n\t\tconst serverReady = await this.devServerManager.ensureServerRunning(\n\t\t\tworktree.path,\n\t\t\tport\n\t\t)\n\n\t\tif (!serverReady) {\n\t\t\tlogger.warn(\n\t\t\t\t`Dev server failed to start on port ${port}. Opening browser anyway...`\n\t\t\t)\n\t\t}\n\n\t\t// Construct URL and open browser\n\t\tconst url = `http://localhost:${port}`\n\t\tlogger.info(`Opening browser: ${url}`)\n\t\tawait openBrowser(url)\n\t\tlogger.success('Browser opened')\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,OAAO,UAAU;AACjB,OAAO,QAAQ;AACf,SAAS,aAAa;AA8Bf,IAAM,aAAN,MAAiB;AAAA,EACvB,YACS,qBAAqB,IAAI,mBAAmB,GAC5C,qBAAqB,IAAI,0BAA0B,GACnD,mBAAmB,IAAI,iBAAiB,IAAI,mBAAmB,CAAC,GAChE,mBAAmB,IAAI,iBAAiB,GACxC,kBAAkB,IAAI,gBAAgB,GAC7C;AALO;AACA;AACA;AACA;AACA;AAAA,EACN;AAAA,EAEH,MAAM,QAAQ,OAAuC;AAEpD,UAAM,SAAS,MAAM,aAClB,MAAM,KAAK,mBAAmB,MAAM,UAAU,IAC9C,MAAM,KAAK,+BAA+B;AAE7C,WAAO,MAAM,iBAAiB,KAAK,UAAU,MAAM,CAAC,EAAE;AAGtD,UAAM,WAAW,MAAM,KAAK,0BAA0B,MAAM;AAE5D,WAAO,KAAK,sBAAsB,SAAS,IAAI,EAAE;AAGjD,UAAM,EAAE,cAAc,WAAW,IAChC,MAAM,KAAK,mBAAmB,mBAAmB,SAAS,IAAI;AAE/D,WAAO,MAAM,0BAA0B,aAAa,KAAK,IAAI,CAAC,EAAE;AAGhE,QAAI,aAAa,SAAS,KAAK,GAAG;AACjC,YAAM,KAAK,WAAW,SAAS,MAAM,YAAY,MAAM,QAAQ,CAAC,CAAC;AAAA,IAClE,WAAW,aAAa,SAAS,KAAK,GAAG;AACxC,YAAM,KAAK,eAAe,QAAQ;AAAA,IACnC,OAAO;AACN,YAAM,IAAI;AAAA,QACT,wDAAwD,SAAS,IAAI;AAAA,MACtE;AAAA,IACD;AAAA,EACD;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,mBAAmB,YAA6C;AAC7E,UAAM,SAAS,MAAM,KAAK,iBAAiB,yBAAyB,UAAU;AAG9E,QAAI,OAAO,SAAS,eAAe;AAClC,YAAM,IAAI,MAAM,wDAAwD;AAAA,IACzE;AAEA,UAAM,SAAyB;AAAA,MAC9B,MAAM,OAAO;AAAA,MACb,eAAe,OAAO;AAAA,MACtB,cAAc;AAAA,IACf;AAEA,QAAI,OAAO,WAAW,QAAW;AAChC,aAAO,SAAS,OAAO;AAAA,IACxB;AACA,QAAI,OAAO,eAAe,QAAW;AACpC,aAAO,aAAa,OAAO;AAAA,IAC5B;AAEA,WAAO;AAAA,EACR;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAc,iCAA0D;AACvE,UAAM,aAAa,KAAK,SAAS,QAAQ,IAAI,CAAC;AAG9C,UAAM,YAAY;AAClB,UAAM,UAAU,WAAW,MAAM,SAAS;AAE1C,QAAI,mCAAU,IAAI;AACjB,YAAM,WAAW,SAAS,QAAQ,CAAC,GAAG,EAAE;AACxC,aAAO,MAAM,qBAAqB,QAAQ,oBAAoB,UAAU,EAAE;AAC1E,aAAO;AAAA,QACN,MAAM;AAAA,QACN,QAAQ;AAAA,QACR,eAAe;AAAA,QACf,cAAc;AAAA,MACf;AAAA,IACD;AAGA,UAAM,cAAc,mBAAmB,UAAU;AAEjD,QAAI,gBAAgB,MAAM;AACzB,aAAO,MAAM,wBAAwB,WAAW,oBAAoB,UAAU,EAAE;AAChF,aAAO;AAAA,QACN,MAAM;AAAA,QACN,QAAQ;AAAA,QACR,eAAe;AAAA,QACf,cAAc;AAAA,MACf;AAAA,IACD;AAGA,UAAM,WAAW,MAAM,KAAK,mBAAmB,YAAY;AAC3D,UAAM,gBAAgB,SAAS;AAE/B,QAAI,CAAC,eAAe;AACnB,YAAM,IAAI;AAAA,QACT;AAAA,MAED;AAAA,IACD;AAGA,UAAM,oBAAoB,mBAAmB,aAAa;AAC1D,QAAI,sBAAsB,MAAM;AAC/B,aAAO,MAAM,wBAAwB,iBAAiB,iBAAiB,aAAa,EAAE;AACtF,aAAO;AAAA,QACN,MAAM;AAAA,QACN,QAAQ;AAAA,QACR,eAAe;AAAA,QACf,cAAc;AAAA,MACf;AAAA,IACD;AAGA,WAAO;AAAA,MACN,MAAM;AAAA,MACN,YAAY;AAAA,MACZ,eAAe;AAAA,MACf,cAAc;AAAA,IACf;AAAA,EACD;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,0BAA0B,QAA8C;AACrF,QAAI,WAA+B;AAEnC,QAAI,OAAO,SAAS,WAAW,OAAO,WAAW,QAAW;AAC3D,iBAAW,MAAM,KAAK,mBAAmB,qBAAqB,OAAO,MAAM;AAAA,IAC5E,WAAW,OAAO,SAAS,QAAQ,OAAO,WAAW,QAAW;AAE/D,YAAM,WAAW,OAAO,OAAO,WAAW,WAAW,OAAO,SAAS,OAAO,OAAO,MAAM;AACzF,UAAI,MAAM,QAAQ,KAAK,CAAC,SAAS,QAAQ,GAAG;AAC3C,cAAM,IAAI,MAAM,sBAAsB,OAAO,MAAM,+BAA+B;AAAA,MACnF;AAEA,iBAAW,MAAM,KAAK,mBAAmB,kBAAkB,UAAU,EAAE;AAAA,IACxE,WAAW,OAAO,SAAS,YAAY,OAAO,YAAY;AACzD,iBAAW,MAAM,KAAK,mBAAmB;AAAA,QACxC,OAAO;AAAA,MACR;AAAA,IACD;AAEA,QAAI,CAAC,UAAU;AACd,YAAM,IAAI;AAAA,QACT,yBAAyB,KAAK,kBAAkB,MAAM,CAAC,mBACrC,OAAO,aAAa;AAAA,MACvC;AAAA,IACD;AAEA,WAAO;AAAA,EACR;AAAA;AAAA;AAAA;AAAA,EAKQ,kBAAkB,QAAgC;AACzD,UAAM,YAAY,OAAO,eAAe,qBAAqB;AAE7D,QAAI,OAAO,SAAS,SAAS;AAC5B,aAAO,UAAU,OAAO,MAAM,GAAG,SAAS;AAAA,IAC3C;AACA,QAAI,OAAO,SAAS,MAAM;AACzB,aAAO,OAAO,OAAO,MAAM,GAAG,SAAS;AAAA,IACxC;AACA,WAAO,WAAW,OAAO,UAAU,IAAI,SAAS;AAAA,EACjD;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,WACb,cACA,YACA,MACgB;AAEhB,QAAI,OAAO,KAAK,UAAU,EAAE,WAAW,GAAG;AACzC,YAAM,IAAI,MAAM,sCAAsC;AAAA,IACvD;AAGA,UAAM,aAAa,OAAO,QAAQ,UAAU,EAAE,CAAC;AAC/C,QAAI,CAAC,YAAY;AAChB,YAAM,IAAI,MAAM,sCAAsC;AAAA,IACvD;AACA,UAAM,CAAC,SAAS,OAAO,IAAI;AAC3B,WAAO,MAAM,oBAAoB,OAAO,OAAO,OAAO,EAAE;AAGxD,UAAM,cAAc,KAAK,QAAQ,cAAc,OAAO;AACtD,WAAO,MAAM,2BAA2B,WAAW,EAAE;AAGrD,QAAI,CAAE,MAAM,GAAG,WAAW,WAAW,GAAI;AACxC,YAAM,IAAI;AAAA,QACT,6BAA6B,WAAW;AAAA;AAAA,MAEzC;AAAA,IACD;AAGA,WAAO,KAAK,qBAAqB,WAAW,IAAI,KAAK,KAAK,GAAG,CAAC,EAAE;AAChE,UAAM,MAAM,QAAQ,CAAC,aAAa,GAAG,IAAI,GAAG;AAAA,MAC3C,OAAO;AAAA;AAAA,MACP,KAAK;AAAA;AAAA,MACL,KAAK,QAAQ;AAAA;AAAA,IACd,CAAC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAc,eAAe,UAAsC;AAnQpE;AAoQE,UAAM,eAAe,yBAAyB;AAC9C,UAAM,WAAW,MAAM,KAAK,gBAAgB,aAAa,QAAW,YAAY;AAChF,UAAM,OAAO,MAAM,iBAAiB;AAAA,MACnC,cAAc,SAAS;AAAA,MACvB,gBAAgB,SAAS;AAAA,MACzB,WAAU,oBAAS,iBAAT,mBAAuB,QAAvB,mBAA4B;AAAA,MACtC,cAAc;AAAA,IACf,CAAC;AAGD,UAAM,cAAc,MAAM,KAAK,iBAAiB;AAAA,MAC/C,SAAS;AAAA,MACT;AAAA,IACD;AAEA,QAAI,CAAC,aAAa;AACjB,aAAO;AAAA,QACN,sCAAsC,IAAI;AAAA,MAC3C;AAAA,IACD;AAGA,UAAM,MAAM,oBAAoB,IAAI;AACpC,WAAO,KAAK,oBAAoB,GAAG,EAAE;AACrC,UAAM,YAAY,GAAG;AACrB,WAAO,QAAQ,gBAAgB;AAAA,EAChC;AACD;","names":[]}
1
+ {"version":3,"sources":["../src/commands/run.ts"],"sourcesContent":["import path from 'path'\nimport fs from 'fs-extra'\nimport { execa } from 'execa'\nimport { GitWorktreeManager } from '../lib/GitWorktreeManager.js'\nimport { ProjectCapabilityDetector } from '../lib/ProjectCapabilityDetector.js'\nimport { DevServerManager } from '../lib/DevServerManager.js'\nimport { SettingsManager } from '../lib/SettingsManager.js'\nimport { IdentifierParser } from '../utils/IdentifierParser.js'\nimport { openBrowser } from '../utils/browser.js'\nimport { getWorkspacePort } from '../utils/port.js'\nimport { extractIssueNumber } from '../utils/git.js'\nimport { logger } from '../utils/logger.js'\nimport { extractSettingsOverrides } from '../utils/cli-overrides.js'\nimport type { GitWorktree } from '../types/worktree.js'\n\nexport interface RunCommandInput {\n\tidentifier?: string\n\targs?: string[]\n}\n\ninterface ParsedRunInput {\n\ttype: 'issue' | 'pr' | 'branch'\n\tnumber?: string | number // For issues and PRs\n\tbranchName?: string // For branches\n\toriginalInput: string\n\tautoDetected: boolean\n}\n\n/**\n * RunCommand - Runs CLI tool or opens workspace in browser\n * Priority: CLI first, Web fallback\n */\nexport class RunCommand {\n\tconstructor(\n\t\tprivate gitWorktreeManager = new GitWorktreeManager(),\n\t\tprivate capabilityDetector = new ProjectCapabilityDetector(),\n\t\tprivate identifierParser = new IdentifierParser(new GitWorktreeManager()),\n\t\tprivate devServerManager = new DevServerManager(),\n\t\tprivate settingsManager = new SettingsManager()\n\t) {}\n\n\tasync execute(input: RunCommandInput): Promise<void> {\n\t\t// 1. Parse or auto-detect identifier\n\t\tconst parsed = input.identifier\n\t\t\t? await this.parseExplicitInput(input.identifier)\n\t\t\t: await this.autoDetectFromCurrentDirectory()\n\n\t\tlogger.debug(`Parsed input: ${JSON.stringify(parsed)}`)\n\n\t\t// 2. Find worktree path based on identifier\n\t\tconst worktree = await this.findWorktreeForIdentifier(parsed)\n\n\t\tlogger.info(`Found worktree at: ${worktree.path}`)\n\n\t\t// 3. Detect project capabilities\n\t\tconst { capabilities, binEntries } =\n\t\t\tawait this.capabilityDetector.detectCapabilities(worktree.path)\n\n\t\tlogger.debug(`Detected capabilities: ${capabilities.join(', ')}`)\n\n\t\t// 4. Execute based on capabilities (CLI first, web fallback)\n\t\tif (capabilities.includes('cli')) {\n\t\t\tawait this.runCLITool(worktree.path, binEntries, input.args ?? [])\n\t\t} else if (capabilities.includes('web')) {\n\t\t\tawait this.openWebBrowser(worktree)\n\t\t} else {\n\t\t\tthrow new Error(\n\t\t\t\t`No CLI or web capabilities detected for workspace at ${worktree.path}`\n\t\t\t)\n\t\t}\n\t}\n\n\t/**\n\t * Parse explicit identifier input\n\t */\n\tprivate async parseExplicitInput(identifier: string): Promise<ParsedRunInput> {\n\t\tconst parsed = await this.identifierParser.parseForPatternDetection(identifier)\n\n\t\t// Description type should never reach run command (converted in start)\n\t\tif (parsed.type === 'description') {\n\t\t\tthrow new Error('Description input type is not supported in run command')\n\t\t}\n\n\t\tconst result: ParsedRunInput = {\n\t\t\ttype: parsed.type,\n\t\t\toriginalInput: parsed.originalInput,\n\t\t\tautoDetected: false,\n\t\t}\n\n\t\tif (parsed.number !== undefined) {\n\t\t\tresult.number = parsed.number\n\t\t}\n\t\tif (parsed.branchName !== undefined) {\n\t\t\tresult.branchName = parsed.branchName\n\t\t}\n\n\t\treturn result\n\t}\n\n\t/**\n\t * Auto-detect identifier from current directory\n\t * Same logic as FinishCommand.autoDetectFromCurrentDirectory()\n\t */\n\tprivate async autoDetectFromCurrentDirectory(): Promise<ParsedRunInput> {\n\t\tconst currentDir = path.basename(process.cwd())\n\n\t\t// Check for PR worktree pattern: _pr_N suffix\n\t\tconst prPattern = /_pr_(\\d+)$/\n\t\tconst prMatch = currentDir.match(prPattern)\n\n\t\tif (prMatch?.[1]) {\n\t\t\tconst prNumber = parseInt(prMatch[1], 10)\n\t\t\tlogger.debug(`Auto-detected PR #${prNumber} from directory: ${currentDir}`)\n\t\t\treturn {\n\t\t\t\ttype: 'pr',\n\t\t\t\tnumber: prNumber,\n\t\t\t\toriginalInput: currentDir,\n\t\t\t\tautoDetected: true,\n\t\t\t}\n\t\t}\n\n\t\t// Check for issue pattern in directory\n\t\tconst issueNumber = extractIssueNumber(currentDir)\n\n\t\tif (issueNumber !== null) {\n\t\t\tlogger.debug(`Auto-detected issue #${issueNumber} from directory: ${currentDir}`)\n\t\t\treturn {\n\t\t\t\ttype: 'issue',\n\t\t\t\tnumber: issueNumber,\n\t\t\t\toriginalInput: currentDir,\n\t\t\t\tautoDetected: true,\n\t\t\t}\n\t\t}\n\n\t\t// Fallback: get current branch name\n\t\tconst repoInfo = await this.gitWorktreeManager.getRepoInfo()\n\t\tconst currentBranch = repoInfo.currentBranch\n\n\t\tif (!currentBranch) {\n\t\t\tthrow new Error(\n\t\t\t\t'Could not auto-detect identifier. Please provide an issue number, PR number, or branch name.\\n' +\n\t\t\t\t\t'Expected directory pattern: feat/issue-XX-description OR worktree with _pr_N suffix'\n\t\t\t)\n\t\t}\n\n\t\t// Try to extract issue from branch name\n\t\tconst branchIssueNumber = extractIssueNumber(currentBranch)\n\t\tif (branchIssueNumber !== null) {\n\t\t\tlogger.debug(`Auto-detected issue #${branchIssueNumber} from branch: ${currentBranch}`)\n\t\t\treturn {\n\t\t\t\ttype: 'issue',\n\t\t\t\tnumber: branchIssueNumber,\n\t\t\t\toriginalInput: currentBranch,\n\t\t\t\tautoDetected: true,\n\t\t\t}\n\t\t}\n\n\t\t// Last resort: use branch name\n\t\treturn {\n\t\t\ttype: 'branch',\n\t\t\tbranchName: currentBranch,\n\t\t\toriginalInput: currentBranch,\n\t\t\tautoDetected: true,\n\t\t}\n\t}\n\n\t/**\n\t * Find worktree for the given identifier\n\t */\n\tprivate async findWorktreeForIdentifier(parsed: ParsedRunInput): Promise<GitWorktree> {\n\t\tlet worktree: GitWorktree | null = null\n\n\t\tif (parsed.type === 'issue' && parsed.number !== undefined) {\n\t\t\tworktree = await this.gitWorktreeManager.findWorktreeForIssue(parsed.number)\n\t\t} else if (parsed.type === 'pr' && parsed.number !== undefined) {\n\t\t\t// For PRs, ensure the number is numeric (PRs are always numeric per GitHub)\n\t\t\tconst prNumber = typeof parsed.number === 'number' ? parsed.number : Number(parsed.number)\n\t\t\tif (isNaN(prNumber) || !isFinite(prNumber)) {\n\t\t\t\tthrow new Error(`Invalid PR number: ${parsed.number}. PR numbers must be numeric.`)\n\t\t\t}\n\t\t\t// Pass empty string for branch name since we don't know it yet\n\t\t\tworktree = await this.gitWorktreeManager.findWorktreeForPR(prNumber, '')\n\t\t} else if (parsed.type === 'branch' && parsed.branchName) {\n\t\t\tworktree = await this.gitWorktreeManager.findWorktreeForBranch(\n\t\t\t\tparsed.branchName\n\t\t\t)\n\t\t}\n\n\t\tif (!worktree) {\n\t\t\tthrow new Error(\n\t\t\t\t`No worktree found for ${this.formatParsedInput(parsed)}. ` +\n\t\t\t\t\t`Run 'il start ${parsed.originalInput}' to create one.`\n\t\t\t)\n\t\t}\n\n\t\treturn worktree\n\t}\n\n\t/**\n\t * Format parsed input for display\n\t */\n\tprivate formatParsedInput(parsed: ParsedRunInput): string {\n\t\tconst autoLabel = parsed.autoDetected ? ' (auto-detected)' : ''\n\n\t\tif (parsed.type === 'issue') {\n\t\t\treturn `issue #${parsed.number}${autoLabel}`\n\t\t}\n\t\tif (parsed.type === 'pr') {\n\t\t\treturn `PR #${parsed.number}${autoLabel}`\n\t\t}\n\t\treturn `branch \"${parsed.branchName}\"${autoLabel}`\n\t}\n\n\t/**\n\t * Run CLI tool directly from worktree bin path (NO SYMLINKS!)\n\t */\n\tprivate async runCLITool(\n\t\tworktreePath: string,\n\t\tbinEntries: Record<string, string>,\n\t\targs: string[]\n\t): Promise<void> {\n\t\t// Validate binEntries exist\n\t\tif (Object.keys(binEntries).length === 0) {\n\t\t\tthrow new Error('No bin entries found in package.json')\n\t\t}\n\n\t\t// Get first bin entry (deterministic)\n\t\tconst firstEntry = Object.entries(binEntries)[0]\n\t\tif (!firstEntry) {\n\t\t\tthrow new Error('No bin entries found in package.json')\n\t\t}\n\t\tconst [binName, binPath] = firstEntry\n\t\tlogger.debug(`Using bin entry: ${binName} -> ${binPath}`)\n\n\t\t// CRITICAL: Construct absolute path (NO SYMLINKS!)\n\t\tconst binFilePath = path.resolve(worktreePath, binPath)\n\t\tlogger.debug(`Resolved bin file path: ${binFilePath}`)\n\n\t\t// Verify file exists\n\t\tif (!(await fs.pathExists(binFilePath))) {\n\t\t\tthrow new Error(\n\t\t\t\t`CLI executable not found: ${binFilePath}\\n` +\n\t\t\t\t\t`Make sure the project is built (run 'il start' first)`\n\t\t\t)\n\t\t}\n\n\t\t// Execute with Node.js\n\t\tlogger.info(`Running CLI: node ${binFilePath} ${args.join(' ')}`)\n\t\tawait execa('node', [binFilePath, ...args], {\n\t\t\tstdio: 'inherit', // Allow interactive CLIs (prompts, colors, etc.)\n\t\t\tcwd: worktreePath, // Execute in worktree context\n\t\t\tenv: process.env, // Inherit environment\n\t\t})\n\t}\n\n\t/**\n\t * Open web browser with workspace URL\n\t * Auto-starts dev server if not already running\n\t */\n\tprivate async openWebBrowser(worktree: GitWorktree): Promise<void> {\n\t\tconst cliOverrides = extractSettingsOverrides()\n\t\tconst settings = await this.settingsManager.loadSettings(undefined, cliOverrides)\n\t\tconst port = await getWorkspacePort({\n\t\t\tworktreePath: worktree.path,\n\t\t\tworktreeBranch: worktree.branch,\n\t\t\tbasePort: settings.capabilities?.web?.basePort,\n\t\t\tcheckEnvFile: true,\n\t\t})\n\n\t\t// Ensure dev server is running on the port\n\t\tconst serverReady = await this.devServerManager.ensureServerRunning(\n\t\t\tworktree.path,\n\t\t\tport\n\t\t)\n\n\t\tif (!serverReady) {\n\t\t\tlogger.warn(\n\t\t\t\t`Dev server failed to start on port ${port}. Opening browser anyway...`\n\t\t\t)\n\t\t}\n\n\t\t// Construct URL and open browser\n\t\tconst url = `http://localhost:${port}`\n\t\tlogger.info(`Opening browser: ${url}`)\n\t\tawait openBrowser(url)\n\t\tlogger.success('Browser opened')\n\t}\n}\n"],"mappings":"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","names":[]}
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  SessionSummaryService
4
- } from "./chunk-EDDIAWVM.js";
4
+ } from "./chunk-KSXA2NOJ.js";
5
5
  import "./chunk-NXMDEL3F.js";
6
6
  import {
7
7
  GitWorktreeManager
8
8
  } from "./chunk-UDRZY65Y.js";
9
- import "./chunk-GJMEKEI5.js";
9
+ import "./chunk-LZBSLO6S.js";
10
10
  import "./chunk-HBJITKSZ.js";
11
11
  import "./chunk-FXDYIV3K.js";
12
12
  import "./chunk-TIYJEEVO.js";
@@ -18,7 +18,7 @@ import {
18
18
  MetadataManager
19
19
  } from "./chunk-VWGKGNJP.js";
20
20
  import "./chunk-GCPAZSGV.js";
21
- import "./chunk-ITIXKM24.js";
21
+ import "./chunk-IGKPPACU.js";
22
22
  import {
23
23
  getLogger
24
24
  } from "./chunk-6MLEBAYZ.js";
@@ -249,4 +249,4 @@ Please provide an issue number, PR number, or branch name.`
249
249
  export {
250
250
  SummaryCommand
251
251
  };
252
- //# sourceMappingURL=summary-7KYFRAIM.js.map
252
+ //# sourceMappingURL=summary-2KLNHVTN.js.map
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  ProcessManager
4
- } from "./chunk-453NC377.js";
4
+ } from "./chunk-WZYBHD7P.js";
5
+ import "./chunk-CFQVOTHO.js";
5
6
  import "./chunk-ZA575VLF.js";
6
7
  import {
7
8
  SettingsManager
@@ -80,4 +81,4 @@ var TestWebserverCommand = class {
80
81
  export {
81
82
  TestWebserverCommand
82
83
  };
83
- //# sourceMappingURL=test-webserver-NRMGT2HB.js.map
84
+ //# sourceMappingURL=test-webserver-J6SMNLU2.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/commands/test-webserver.ts"],"sourcesContent":["import { logger } from '../utils/logger.js'\nimport { ProcessManager } from '../lib/process/ProcessManager.js'\nimport { SettingsManager } from '../lib/SettingsManager.js'\n\nexport interface TestWebserverOptions {\n\tkill?: boolean\n}\n\nexport interface TestWebserverCommandInput {\n\tissueNumber: string | number\n\toptions: TestWebserverOptions\n}\n\n/**\n * Command to test if a web server is running on a workspace port\n * and optionally kill it if detected.\n *\n * This uses the ProcessManager to detect processes on ports and\n * safely terminate web servers when requested.\n */\nexport class TestWebserverCommand {\n\tprivate readonly settingsManager: SettingsManager\n\tprivate processManager: ProcessManager | null\n\n\tconstructor(processManager?: ProcessManager, settingsManager?: SettingsManager) {\n\t\tthis.settingsManager = settingsManager ?? new SettingsManager()\n\t\tthis.processManager = processManager ?? null\n\t}\n\n\t/**\n\t * Get or create ProcessManager with correct basePort from settings\n\t */\n\tprivate async getProcessManager(): Promise<ProcessManager> {\n\t\tif (this.processManager) {\n\t\t\treturn this.processManager\n\t\t}\n\t\tconst settings = await this.settingsManager.loadSettings()\n\t\tconst basePort = settings.capabilities?.web?.basePort ?? 3000\n\t\tthis.processManager = new ProcessManager(basePort)\n\t\treturn this.processManager\n\t}\n\n\t/**\n\t * Main entry point for the test-webserver command\n\t */\n\tpublic async execute(input: TestWebserverCommandInput): Promise<void> {\n\t\tconst { issueNumber, options } = input\n\n\t\t// Validate issue number - test-webserver requires numeric for port calculation\n\t\tif (typeof issueNumber !== 'number' || !Number.isInteger(issueNumber) || issueNumber <= 0) {\n\t\t\tthrow new Error('Issue number must be a positive integer for port calculation')\n\t\t}\n\n\t\t// Get ProcessManager (will load settings to get basePort if needed)\n\t\tconst processManager = await this.getProcessManager()\n\n\t\t// Calculate port from issue number\n\t\tconst port = processManager.calculatePort(issueNumber)\n\t\tlogger.info(`Checking port ${port} (issue #${issueNumber})...`)\n\n\t\t// Detect what's running on the port\n\t\tconst processInfo = await processManager.detectDevServer(port)\n\n\t\tif (!processInfo) {\n\t\t\tlogger.info(`No process found on port ${port}`)\n\t\t\treturn\n\t\t}\n\n\t\t// Report what we found\n\t\tlogger.info(`Found process on port ${port}:`)\n\t\tlogger.info(` PID: ${processInfo.pid}`)\n\t\tlogger.info(` Name: ${processInfo.name}`)\n\t\tlogger.info(` Command: ${processInfo.command}`)\n\n\t\tif (processInfo.isDevServer) {\n\t\t\tlogger.success(`Detected as web server`)\n\t\t} else {\n\t\t\tlogger.warn(`Not detected as web server`)\n\t\t\tlogger.info(`This appears to be a non-web-server process. Skipping for safety.`)\n\t\t}\n\n\t\t// Kill process if requested (and it's a web server)\n\t\tif (options.kill) {\n\t\t\tif (!processInfo.isDevServer) {\n\t\t\t\tlogger.error(`Cannot kill non-web-server process (PID ${processInfo.pid}) for safety reasons`)\n\t\t\t\tlogger.info(`Process must match web server patterns to be killed automatically`)\n\t\t\t\tthrow new Error('Cannot kill non-web-server process')\n\t\t\t}\n\n\t\t\tlogger.info(`Terminating web server (PID ${processInfo.pid})...`)\n\t\t\tconst terminated = await processManager.terminateProcess(processInfo.pid)\n\n\t\t\tif (terminated) {\n\t\t\t\tlogger.success(`Web server terminated successfully`)\n\n\t\t\t\t// Verify port is now free\n\t\t\t\tconst isFree = await processManager.verifyPortFree(port)\n\t\t\t\tif (isFree) {\n\t\t\t\t\tlogger.success(`Port ${port} is now free`)\n\t\t\t\t} else {\n\t\t\t\t\tlogger.warn(`Port ${port} may still be in use (TIME_WAIT state)`)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;;AAoBO,IAAM,uBAAN,MAA2B;AAAA,EAIjC,YAAY,gBAAiC,iBAAmC;AAC/E,SAAK,kBAAkB,mBAAmB,IAAI,gBAAgB;AAC9D,SAAK,iBAAiB,kBAAkB;AAAA,EACzC;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,oBAA6C;AAhC5D;AAiCE,QAAI,KAAK,gBAAgB;AACxB,aAAO,KAAK;AAAA,IACb;AACA,UAAM,WAAW,MAAM,KAAK,gBAAgB,aAAa;AACzD,UAAM,aAAW,oBAAS,iBAAT,mBAAuB,QAAvB,mBAA4B,aAAY;AACzD,SAAK,iBAAiB,IAAI,eAAe,QAAQ;AACjD,WAAO,KAAK;AAAA,EACb;AAAA;AAAA;AAAA;AAAA,EAKA,MAAa,QAAQ,OAAiD;AACrE,UAAM,EAAE,aAAa,QAAQ,IAAI;AAGjC,QAAI,OAAO,gBAAgB,YAAY,CAAC,OAAO,UAAU,WAAW,KAAK,eAAe,GAAG;AAC1F,YAAM,IAAI,MAAM,8DAA8D;AAAA,IAC/E;AAGA,UAAM,iBAAiB,MAAM,KAAK,kBAAkB;AAGpD,UAAM,OAAO,eAAe,cAAc,WAAW;AACrD,WAAO,KAAK,iBAAiB,IAAI,YAAY,WAAW,MAAM;AAG9D,UAAM,cAAc,MAAM,eAAe,gBAAgB,IAAI;AAE7D,QAAI,CAAC,aAAa;AACjB,aAAO,KAAK,4BAA4B,IAAI,EAAE;AAC9C;AAAA,IACD;AAGA,WAAO,KAAK,yBAAyB,IAAI,GAAG;AAC5C,WAAO,KAAK,WAAW,YAAY,GAAG,EAAE;AACxC,WAAO,KAAK,YAAY,YAAY,IAAI,EAAE;AAC1C,WAAO,KAAK,eAAe,YAAY,OAAO,EAAE;AAEhD,QAAI,YAAY,aAAa;AAC5B,aAAO,QAAQ,wBAAwB;AAAA,IACxC,OAAO;AACN,aAAO,KAAK,4BAA4B;AACxC,aAAO,KAAK,mEAAmE;AAAA,IAChF;AAGA,QAAI,QAAQ,MAAM;AACjB,UAAI,CAAC,YAAY,aAAa;AAC7B,eAAO,MAAM,2CAA2C,YAAY,GAAG,sBAAsB;AAC7F,eAAO,KAAK,mEAAmE;AAC/E,cAAM,IAAI,MAAM,oCAAoC;AAAA,MACrD;AAEA,aAAO,KAAK,+BAA+B,YAAY,GAAG,MAAM;AAChE,YAAM,aAAa,MAAM,eAAe,iBAAiB,YAAY,GAAG;AAExE,UAAI,YAAY;AACf,eAAO,QAAQ,oCAAoC;AAGnD,cAAM,SAAS,MAAM,eAAe,eAAe,IAAI;AACvD,YAAI,QAAQ;AACX,iBAAO,QAAQ,QAAQ,IAAI,cAAc;AAAA,QAC1C,OAAO;AACN,iBAAO,KAAK,QAAQ,IAAI,wCAAwC;AAAA,QACjE;AAAA,MACD;AAAA,IACD;AAAA,EACD;AACD;","names":[]}
1
+ {"version":3,"sources":["../src/commands/test-webserver.ts"],"sourcesContent":["import { logger } from '../utils/logger.js'\nimport { ProcessManager } from '../lib/process/ProcessManager.js'\nimport { SettingsManager } from '../lib/SettingsManager.js'\n\nexport interface TestWebserverOptions {\n\tkill?: boolean\n}\n\nexport interface TestWebserverCommandInput {\n\tissueNumber: string | number\n\toptions: TestWebserverOptions\n}\n\n/**\n * Command to test if a web server is running on a workspace port\n * and optionally kill it if detected.\n *\n * This uses the ProcessManager to detect processes on ports and\n * safely terminate web servers when requested.\n */\nexport class TestWebserverCommand {\n\tprivate readonly settingsManager: SettingsManager\n\tprivate processManager: ProcessManager | null\n\n\tconstructor(processManager?: ProcessManager, settingsManager?: SettingsManager) {\n\t\tthis.settingsManager = settingsManager ?? new SettingsManager()\n\t\tthis.processManager = processManager ?? null\n\t}\n\n\t/**\n\t * Get or create ProcessManager with correct basePort from settings\n\t */\n\tprivate async getProcessManager(): Promise<ProcessManager> {\n\t\tif (this.processManager) {\n\t\t\treturn this.processManager\n\t\t}\n\t\tconst settings = await this.settingsManager.loadSettings()\n\t\tconst basePort = settings.capabilities?.web?.basePort ?? 3000\n\t\tthis.processManager = new ProcessManager(basePort)\n\t\treturn this.processManager\n\t}\n\n\t/**\n\t * Main entry point for the test-webserver command\n\t */\n\tpublic async execute(input: TestWebserverCommandInput): Promise<void> {\n\t\tconst { issueNumber, options } = input\n\n\t\t// Validate issue number - test-webserver requires numeric for port calculation\n\t\tif (typeof issueNumber !== 'number' || !Number.isInteger(issueNumber) || issueNumber <= 0) {\n\t\t\tthrow new Error('Issue number must be a positive integer for port calculation')\n\t\t}\n\n\t\t// Get ProcessManager (will load settings to get basePort if needed)\n\t\tconst processManager = await this.getProcessManager()\n\n\t\t// Calculate port from issue number\n\t\tconst port = processManager.calculatePort(issueNumber)\n\t\tlogger.info(`Checking port ${port} (issue #${issueNumber})...`)\n\n\t\t// Detect what's running on the port\n\t\tconst processInfo = await processManager.detectDevServer(port)\n\n\t\tif (!processInfo) {\n\t\t\tlogger.info(`No process found on port ${port}`)\n\t\t\treturn\n\t\t}\n\n\t\t// Report what we found\n\t\tlogger.info(`Found process on port ${port}:`)\n\t\tlogger.info(` PID: ${processInfo.pid}`)\n\t\tlogger.info(` Name: ${processInfo.name}`)\n\t\tlogger.info(` Command: ${processInfo.command}`)\n\n\t\tif (processInfo.isDevServer) {\n\t\t\tlogger.success(`Detected as web server`)\n\t\t} else {\n\t\t\tlogger.warn(`Not detected as web server`)\n\t\t\tlogger.info(`This appears to be a non-web-server process. Skipping for safety.`)\n\t\t}\n\n\t\t// Kill process if requested (and it's a web server)\n\t\tif (options.kill) {\n\t\t\tif (!processInfo.isDevServer) {\n\t\t\t\tlogger.error(`Cannot kill non-web-server process (PID ${processInfo.pid}) for safety reasons`)\n\t\t\t\tlogger.info(`Process must match web server patterns to be killed automatically`)\n\t\t\t\tthrow new Error('Cannot kill non-web-server process')\n\t\t\t}\n\n\t\t\tlogger.info(`Terminating web server (PID ${processInfo.pid})...`)\n\t\t\tconst terminated = await processManager.terminateProcess(processInfo.pid)\n\n\t\t\tif (terminated) {\n\t\t\t\tlogger.success(`Web server terminated successfully`)\n\n\t\t\t\t// Verify port is now free\n\t\t\t\tconst isFree = await processManager.verifyPortFree(port)\n\t\t\t\tif (isFree) {\n\t\t\t\t\tlogger.success(`Port ${port} is now free`)\n\t\t\t\t} else {\n\t\t\t\t\tlogger.warn(`Port ${port} may still be in use (TIME_WAIT state)`)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAoBO,IAAM,uBAAN,MAA2B;AAAA,EAIjC,YAAY,gBAAiC,iBAAmC;AAC/E,SAAK,kBAAkB,mBAAmB,IAAI,gBAAgB;AAC9D,SAAK,iBAAiB,kBAAkB;AAAA,EACzC;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,oBAA6C;AAhC5D;AAiCE,QAAI,KAAK,gBAAgB;AACxB,aAAO,KAAK;AAAA,IACb;AACA,UAAM,WAAW,MAAM,KAAK,gBAAgB,aAAa;AACzD,UAAM,aAAW,oBAAS,iBAAT,mBAAuB,QAAvB,mBAA4B,aAAY;AACzD,SAAK,iBAAiB,IAAI,eAAe,QAAQ;AACjD,WAAO,KAAK;AAAA,EACb;AAAA;AAAA;AAAA;AAAA,EAKA,MAAa,QAAQ,OAAiD;AACrE,UAAM,EAAE,aAAa,QAAQ,IAAI;AAGjC,QAAI,OAAO,gBAAgB,YAAY,CAAC,OAAO,UAAU,WAAW,KAAK,eAAe,GAAG;AAC1F,YAAM,IAAI,MAAM,8DAA8D;AAAA,IAC/E;AAGA,UAAM,iBAAiB,MAAM,KAAK,kBAAkB;AAGpD,UAAM,OAAO,eAAe,cAAc,WAAW;AACrD,WAAO,KAAK,iBAAiB,IAAI,YAAY,WAAW,MAAM;AAG9D,UAAM,cAAc,MAAM,eAAe,gBAAgB,IAAI;AAE7D,QAAI,CAAC,aAAa;AACjB,aAAO,KAAK,4BAA4B,IAAI,EAAE;AAC9C;AAAA,IACD;AAGA,WAAO,KAAK,yBAAyB,IAAI,GAAG;AAC5C,WAAO,KAAK,WAAW,YAAY,GAAG,EAAE;AACxC,WAAO,KAAK,YAAY,YAAY,IAAI,EAAE;AAC1C,WAAO,KAAK,eAAe,YAAY,OAAO,EAAE;AAEhD,QAAI,YAAY,aAAa;AAC5B,aAAO,QAAQ,wBAAwB;AAAA,IACxC,OAAO;AACN,aAAO,KAAK,4BAA4B;AACxC,aAAO,KAAK,mEAAmE;AAAA,IAChF;AAGA,QAAI,QAAQ,MAAM;AACjB,UAAI,CAAC,YAAY,aAAa;AAC7B,eAAO,MAAM,2CAA2C,YAAY,GAAG,sBAAsB;AAC7F,eAAO,KAAK,mEAAmE;AAC/E,cAAM,IAAI,MAAM,oCAAoC;AAAA,MACrD;AAEA,aAAO,KAAK,+BAA+B,YAAY,GAAG,MAAM;AAChE,YAAM,aAAa,MAAM,eAAe,iBAAiB,YAAY,GAAG;AAExE,UAAI,YAAY;AACf,eAAO,QAAQ,oCAAoC;AAGnD,cAAM,SAAS,MAAM,eAAe,eAAe,IAAI;AACvD,YAAI,QAAQ;AACX,iBAAO,QAAQ,QAAQ,IAAI,cAAc;AAAA,QAC1C,OAAO;AACN,iBAAO,KAAK,QAAQ,IAAI,wCAAwC;AAAA,QACjE;AAAA,MACD;AAAA,IACD;AAAA,EACD;AACD;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iloom/cli",
3
- "version": "0.7.4",
3
+ "version": "0.7.5",
4
4
  "description": "Control plane for maintaining alignment between you and Claude Code as you work across multiple issues using isolated environments, visible context, and multi-agent workflows to scale understanding, not just output",
5
5
  "keywords": [
6
6
  "ai",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/utils/port.ts","../src/lib/process/ProcessManager.ts"],"sourcesContent":["import { createHash } from 'crypto'\nimport path from 'path'\nimport fs from 'fs-extra'\nimport { parseEnvFile, extractPort, findEnvFileContainingVariable } from './env.js'\nimport { extractIssueNumber } from './git.js'\nimport { logger } from './logger.js'\n\n/**\n * Wrap a raw port that exceeds 65535 into the valid port range.\n * Uses modulo arithmetic to wrap back into [basePort+1, 65535].\n *\n * @param rawPort - The calculated port (basePort + issueNumber)\n * @param basePort - The base port (default: 3000)\n * @returns Port in valid range [basePort+1, 65535]\n */\nexport function wrapPort(rawPort: number, basePort: number): number {\n\tif (rawPort <= 65535) return rawPort\n\tconst range = 65535 - basePort\n\treturn ((rawPort - basePort - 1) % range) + basePort + 1\n}\n\n/**\n * Extract numeric suffix from alphanumeric issue ID (e.g., MARK-324 -> 324)\n * @returns The numeric part or null if no trailing number found\n */\nexport function extractNumericSuffix(issueId: string): number | null {\n\t// Match trailing digits after optional separator (-, _)\n\tconst match = issueId.match(/[-_]?(\\d+)$/)\n\tconst digits = match?.[1]\n\tif (digits === undefined) return null\n\treturn parseInt(digits, 10)\n}\n\n/**\n * Generate deterministic port offset from branch name using SHA256 hash\n * Range: 1-999 (matches existing random range for branches)\n *\n * @param branchName - Branch name to generate port offset from\n * @returns Port offset in range [1, 999]\n * @throws Error if branchName is empty\n */\nexport function generatePortOffsetFromBranchName(branchName: string): number {\n\t// Validate input\n\tif (!branchName || branchName.trim().length === 0) {\n\t\tthrow new Error('Branch name cannot be empty')\n\t}\n\n\t// Generate SHA256 hash of branch name (same pattern as color.ts)\n\tconst hash = createHash('sha256').update(branchName).digest('hex')\n\n\t// Take first 8 hex characters and convert to port offset (1-999)\n\tconst hashPrefix = hash.slice(0, 8)\n\tconst hashAsInt = parseInt(hashPrefix, 16)\n\tconst portOffset = (hashAsInt % 999) + 1 // +1 ensures range is 1-999, not 0-998\n\n\treturn portOffset\n}\n\n/**\n * Calculate deterministic port for branch-based workspace\n *\n * @param branchName - Branch name\n * @param basePort - Base port (default: 3000)\n * @returns Port number\n * @throws Error if branchName is empty\n */\nexport function calculatePortForBranch(branchName: string, basePort: number = 3000): number {\n\tconst offset = generatePortOffsetFromBranchName(branchName)\n\tconst port = basePort + offset\n\n\t// Use wrap-around for port overflow\n\treturn wrapPort(port, basePort)\n}\n\n/**\n * Calculate port from an identifier (issue number, PR number, or string).\n * This is the single source of truth for port calculation logic.\n *\n * Algorithm:\n * 1. Numeric identifiers: basePort + number (with wrapPort for overflow)\n * 2. String numeric (e.g., \"42\"): parse and same as above\n * 3. Alphanumeric with suffix (e.g., \"MARK-324\"): extract suffix and same as above\n * 4. Pure strings without numeric suffix: hash-based calculation via calculatePortForBranch\n *\n * @param identifier - The identifier (issue number, PR number, or string)\n * @param basePort - Base port (default: 3000)\n * @returns Port number in valid range\n */\nexport function calculatePortFromIdentifier(\n\tidentifier: string | number,\n\tbasePort: number = 3000\n): number {\n\t// Handle numeric identifiers directly\n\tif (typeof identifier === 'number') {\n\t\treturn wrapPort(basePort + identifier, basePort)\n\t}\n\n\t// Handle string identifiers\n\t// First, try to parse as pure numeric string\n\tconst numericValue = parseInt(identifier, 10)\n\tif (!isNaN(numericValue) && String(numericValue) === identifier) {\n\t\treturn wrapPort(basePort + numericValue, basePort)\n\t}\n\n\t// Try extracting numeric suffix from alphanumeric identifiers (e.g., MARK-324 -> 324)\n\tconst numericSuffix = extractNumericSuffix(identifier)\n\tif (numericSuffix !== null) {\n\t\treturn wrapPort(basePort + numericSuffix, basePort)\n\t}\n\n\t// For non-numeric strings without numeric suffix, use hash-based calculation\n\treturn calculatePortForBranch(`issue-${identifier}`, basePort)\n}\n\nexport interface GetWorkspacePortOptions {\n\tbasePort?: number | undefined\n\tworktreePath: string\n\tworktreeBranch: string\n\t/** If true, check .env files for PORT override before calculating. Defaults to false. */\n\tcheckEnvFile?: boolean\n}\n\nexport interface GetWorkspacePortDependencies {\n\tfileExists?: (path: string) => Promise<boolean>\n\treadFile?: (path: string) => Promise<string>\n\tlistWorktrees?: () => Promise<Array<{ path: string; branch: string }>>\n}\n\n/**\n * Get port for workspace - calculates based on workspace type, optionally checking .env files first.\n * Consolidates logic previously duplicated across dev-server, run, open commands.\n *\n * Priority (when checkEnvFile is true):\n * 1. Read PORT from dotenv-flow files (if present)\n * 2. Calculate from PR pattern (_pr_N suffix in directory name)\n * 3. Calculate from issue pattern (issue-N or alphanumeric like MARK-324)\n * 4. Calculate from branch name using deterministic hash\n *\n * When checkEnvFile is false (default), skips step 1 and only calculates.\n */\nexport async function getWorkspacePort(\n\toptions: GetWorkspacePortOptions,\n\tdependencies?: GetWorkspacePortDependencies\n): Promise<number> {\n\tconst basePort = options.basePort ?? 3000\n\tconst checkEnvFile = options.checkEnvFile ?? false\n\n\t// Only check .env files if explicitly requested\n\tif (checkEnvFile) {\n\t\tconst deps = {\n\t\t\tfileExists:\n\t\t\t\tdependencies?.fileExists ?? ((p: string): Promise<boolean> => fs.pathExists(p)),\n\t\t\treadFile:\n\t\t\t\tdependencies?.readFile ?? ((p: string): Promise<string> => fs.readFile(p, 'utf8')),\n\t\t}\n\n\t\t// Try to read PORT from any dotenv-flow file (as override)\n\t\tconst envFile = await findEnvFileContainingVariable(\n\t\t\toptions.worktreePath,\n\t\t\t'PORT',\n\t\t\tasync (p) => deps.fileExists(p),\n\t\t\tasync (p, varName) => {\n\t\t\t\tconst content = await deps.readFile(p)\n\t\t\t\tconst envMap = parseEnvFile(content)\n\t\t\t\treturn envMap.get(varName) ?? null\n\t\t\t}\n\t\t)\n\n\t\tif (envFile) {\n\t\t\tconst envPath = path.join(options.worktreePath, envFile)\n\t\t\tconst envContent = await deps.readFile(envPath)\n\t\t\tconst envMap = parseEnvFile(envContent)\n\t\t\tconst port = extractPort(envMap)\n\n\t\t\tif (port) {\n\t\t\t\tlogger.debug(`Using PORT from ${envFile}: ${port}`)\n\t\t\t\treturn port\n\t\t\t}\n\t\t}\n\n\t\tlogger.debug('PORT not found in any dotenv-flow file, calculating from workspace identifier')\n\t}\n\n\t// Calculate based on workspace identifier\n\n\t// Extract identifier from worktree path/branch\n\tconst dirName = path.basename(options.worktreePath)\n\n\t// Check for PR pattern: _pr_N\n\tconst prPattern = /_pr_(\\d+)$/\n\tconst prMatch = dirName.match(prPattern)\n\tif (prMatch?.[1]) {\n\t\tconst prNumber = parseInt(prMatch[1], 10)\n\t\tconst port = calculatePortFromIdentifier(prNumber, basePort)\n\t\tlogger.debug(`Calculated PORT for PR #${prNumber}: ${port}`)\n\t\treturn port\n\t}\n\n\t// Check for issue pattern: issue-N or alphanumeric like MARK-324\n\tconst issueId = extractIssueNumber(dirName) ?? extractIssueNumber(options.worktreeBranch)\n\tif (issueId !== null) {\n\t\tconst port = calculatePortFromIdentifier(issueId, basePort)\n\t\tlogger.debug(`Calculated PORT for issue ${issueId}: ${port}`)\n\t\treturn port\n\t}\n\n\t// Branch-based workspace - use deterministic hash\n\tconst port = calculatePortForBranch(options.worktreeBranch, basePort)\n\tlogger.debug(`Calculated PORT for branch \"${options.worktreeBranch}\": ${port}`)\n\treturn port\n}\n","import { execa } from 'execa'\nimport { setTimeout } from 'timers/promises'\nimport type { ProcessInfo, Platform } from '../../types/process.js'\nimport { calculatePortFromIdentifier } from '../../utils/port.js'\n\n/**\n * Manages process detection and termination across platforms\n * Ports dev server termination logic from bash/merge-and-clean.sh lines 1092-1148\n */\nexport class ProcessManager {\n\tprivate readonly platform: Platform\n\tprivate readonly basePort: number\n\n\tconstructor(basePort: number = 3000) {\n\t\tthis.basePort = basePort\n\t\tthis.platform = this.detectPlatform()\n\t}\n\n\t/**\n\t * Detect current platform\n\t */\n\tprivate detectPlatform(): Platform {\n\t\tswitch (process.platform) {\n\t\t\tcase 'darwin':\n\t\t\t\treturn 'darwin'\n\t\t\tcase 'linux':\n\t\t\t\treturn 'linux'\n\t\t\tcase 'win32':\n\t\t\t\treturn 'win32'\n\t\t\tdefault:\n\t\t\t\treturn 'unsupported'\n\t\t}\n\t}\n\n\t/**\n\t * Detect if a dev server is running on the specified port\n\t * Ports logic from merge-and-clean.sh lines 1107-1123\n\t */\n\tasync detectDevServer(port: number): Promise<ProcessInfo | null> {\n\t\tif (this.platform === 'unsupported') {\n\t\t\tthrow new Error('Process detection not supported on this platform')\n\t\t}\n\n\t\t// Use platform-specific detection\n\t\tif (this.platform === 'win32') {\n\t\t\treturn await this.detectOnPortWindows(port)\n\t\t} else {\n\t\t\treturn await this.detectOnPortUnix(port)\n\t\t}\n\t}\n\n\t/**\n\t * Unix/macOS implementation using lsof\n\t * Ports bash lines 1107-1123\n\t */\n\tprivate async detectOnPortUnix(port: number): Promise<ProcessInfo | null> {\n\t\ttry {\n\t\t\t// Run lsof to find process listening on port (LISTEN only)\n\t\t\tconst result = await execa('lsof', ['-i', `:${port}`, '-P'], {\n\t\t\t\treject: false,\n\t\t\t})\n\n\t\t\t// Filter for LISTEN state only\n\t\t\tconst lines = result.stdout.split('\\n').filter(line => line.includes('LISTEN'))\n\n\t\t\tif (lines.length === 0) {\n\t\t\t\treturn null\n\t\t\t}\n\n\t\t\t// Parse first LISTEN line\n\t\t\tconst firstLine = lines[0]\n\t\t\tif (!firstLine) return null\n\n\t\t\tconst parts = firstLine.split(/\\s+/)\n\t\t\tif (parts.length < 2) return null\n\n\t\t\tconst processName = parts[0] ?? ''\n\t\t\tconst pid = parseInt(parts[1] ?? '', 10)\n\n\t\t\tif (isNaN(pid)) {\n\t\t\t\treturn null\n\t\t\t}\n\n\t\t\t// Get full command line using ps\n\t\t\tconst psResult = await execa('ps', ['-p', pid.toString(), '-o', 'command='], {\n\t\t\t\treject: false,\n\t\t\t})\n\t\t\tconst fullCommand = psResult.stdout.trim()\n\n\t\t\t// Validate if this is a dev server\n\t\t\tconst isDevServer = this.isDevServerProcess(processName, fullCommand)\n\n\t\t\treturn {\n\t\t\t\tpid,\n\t\t\t\tname: processName,\n\t\t\t\tcommand: fullCommand,\n\t\t\t\tport,\n\t\t\t\tisDevServer,\n\t\t\t}\n\t\t} catch {\n\t\t\t// If lsof fails, assume no process on port\n\t\t\treturn null\n\t\t}\n\t}\n\n\t/**\n\t * Windows implementation using netstat and tasklist\n\t */\n\tprivate async detectOnPortWindows(port: number): Promise<ProcessInfo | null> {\n\t\ttry {\n\t\t\t// Use netstat to find PID listening on port\n\t\t\tconst result = await execa('netstat', ['-ano'], { reject: false })\n\t\t\tconst lines = result.stdout.split('\\n')\n\n\t\t\t// Find line with our port and LISTENING state\n\t\t\tconst portLine = lines.find(\n\t\t\t\tline => line.includes(`:${port}`) && line.includes('LISTENING')\n\t\t\t)\n\n\t\t\tif (!portLine) {\n\t\t\t\treturn null\n\t\t\t}\n\n\t\t\t// Extract PID (last column)\n\t\t\tconst parts = portLine.trim().split(/\\s+/)\n\t\t\tconst lastPart = parts[parts.length - 1]\n\t\t\tif (!lastPart) return null\n\n\t\t\tconst pid = parseInt(lastPart, 10)\n\n\t\t\tif (isNaN(pid)) {\n\t\t\t\treturn null\n\t\t\t}\n\n\t\t\t// Get process info using tasklist\n\t\t\tconst taskResult = await execa(\n\t\t\t\t'tasklist',\n\t\t\t\t['/FI', `PID eq ${pid}`, '/FO', 'CSV'],\n\t\t\t\t{\n\t\t\t\t\treject: false,\n\t\t\t\t}\n\t\t\t)\n\n\t\t\t// Parse CSV output\n\t\t\tconst lines2 = taskResult.stdout.split('\\n')\n\t\t\tif (lines2.length < 2) {\n\t\t\t\treturn null\n\t\t\t}\n\n\t\t\tconst secondLine = lines2[1]\n\t\t\tif (!secondLine) return null\n\n\t\t\tconst parts2 = secondLine.split(',')\n\t\t\tconst processName = (parts2[0] ?? '').replace(/\"/g, '')\n\n\t\t\t// TODO: Get full command line on Windows (more complex)\n\t\t\tconst fullCommand = processName\n\n\t\t\tconst isDevServer = this.isDevServerProcess(processName, fullCommand)\n\n\t\t\treturn {\n\t\t\t\tpid,\n\t\t\t\tname: processName,\n\t\t\t\tcommand: fullCommand,\n\t\t\t\tport,\n\t\t\t\tisDevServer,\n\t\t\t}\n\t\t} catch {\n\t\t\treturn null\n\t\t}\n\t}\n\n\t/**\n\t * Validate if process is a dev server\n\t * Ports logic from merge-and-clean.sh lines 1121-1123\n\t */\n\tprivate isDevServerProcess(processName: string, command: string): boolean {\n\t\t// Check process name patterns\n\t\tconst devServerNames = /^(node|npm|pnpm|yarn|next|next-server|vite|webpack|dev-server)$/i\n\t\tif (devServerNames.test(processName)) {\n\t\t\t// Additional validation via command line\n\t\t\tconst devServerCommands =\n\t\t\t\t/(next dev|next-server|npm.*dev|pnpm.*dev|yarn.*dev|vite|webpack.*serve|turbo.*dev|dev.*server)/i\n\t\t\treturn devServerCommands.test(command)\n\t\t}\n\n\t\t// Check command line alone\n\t\tconst devServerCommands =\n\t\t\t/(next dev|next-server|npm.*dev|pnpm.*dev|yarn.*dev|vite|webpack.*serve|turbo.*dev|dev.*server)/i\n\t\treturn devServerCommands.test(command)\n\t}\n\n\t/**\n\t * Terminate a process by PID\n\t * Ports logic from merge-and-clean.sh lines 1126-1139\n\t */\n\tasync terminateProcess(pid: number): Promise<boolean> {\n\t\ttry {\n\t\t\tif (this.platform === 'win32') {\n\t\t\t\t// Windows: use taskkill\n\t\t\t\tawait execa('taskkill', ['/PID', pid.toString(), '/F'], { reject: true })\n\t\t\t} else {\n\t\t\t\t// Unix/macOS: use kill -9\n\t\t\t\tprocess.kill(pid, 'SIGKILL')\n\t\t\t}\n\n\t\t\t// Wait briefly for process to die\n\t\t\tawait setTimeout(1000)\n\n\t\t\treturn true\n\t\t} catch (error) {\n\t\t\tthrow new Error(\n\t\t\t\t`Failed to terminate process ${pid}: ${error instanceof Error ? error.message : 'Unknown error'}`\n\t\t\t)\n\t\t}\n\t}\n\n\t/**\n\t * Verify that a port is free\n\t * Ports verification logic from merge-and-clean.sh lines 1135-1139\n\t */\n\tasync verifyPortFree(port: number): Promise<boolean> {\n\t\tconst processInfo = await this.detectDevServer(port)\n\t\treturn processInfo === null\n\t}\n\n\t/**\n\t * Calculate dev server port from issue/PR number\n\t * Ports logic from merge-and-clean.sh lines 1093-1098\n\t * Delegates to calculatePortFromIdentifier for the actual calculation.\n\t */\n\tcalculatePort(identifier: string | number): number {\n\t\treturn calculatePortFromIdentifier(identifier, this.basePort)\n\t}\n}\n"],"mappings":"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","names":["port","devServerCommands"]}