episoda 0.2.19 → 0.2.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -295,7 +295,7 @@ var require_git_executor = __commonJS({
295
295
  var git_validator_1 = require_git_validator();
296
296
  var git_parser_1 = require_git_parser();
297
297
  var execAsync = (0, util_1.promisify)(child_process_1.exec);
298
- var GitExecutor = class {
298
+ var GitExecutor6 = class {
299
299
  /**
300
300
  * Execute a git command
301
301
  * @param command - The git command to execute
@@ -381,6 +381,19 @@ var require_git_executor = __commonJS({
381
381
  return await this.executeRebaseContinue(cwd, options);
382
382
  case "rebase_status":
383
383
  return await this.executeRebaseStatus(cwd, options);
384
+ // EP944: Worktree operations
385
+ case "worktree_add":
386
+ return await this.executeWorktreeAdd(command, cwd, options);
387
+ case "worktree_remove":
388
+ return await this.executeWorktreeRemove(command, cwd, options);
389
+ case "worktree_list":
390
+ return await this.executeWorktreeList(cwd, options);
391
+ case "worktree_prune":
392
+ return await this.executeWorktreePrune(cwd, options);
393
+ case "clone_bare":
394
+ return await this.executeCloneBare(command, options);
395
+ case "project_info":
396
+ return await this.executeProjectInfo(cwd, options);
384
397
  default:
385
398
  return {
386
399
  success: false,
@@ -1518,15 +1531,15 @@ var require_git_executor = __commonJS({
1518
1531
  try {
1519
1532
  const { stdout: gitDir } = await execAsync("git rev-parse --git-dir", { cwd, timeout: 5e3 });
1520
1533
  const gitDirPath = gitDir.trim();
1521
- const fs6 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1534
+ const fs12 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1522
1535
  const rebaseMergePath = `${gitDirPath}/rebase-merge`;
1523
1536
  const rebaseApplyPath = `${gitDirPath}/rebase-apply`;
1524
1537
  try {
1525
- await fs6.access(rebaseMergePath);
1538
+ await fs12.access(rebaseMergePath);
1526
1539
  inRebase = true;
1527
1540
  } catch {
1528
1541
  try {
1529
- await fs6.access(rebaseApplyPath);
1542
+ await fs12.access(rebaseApplyPath);
1530
1543
  inRebase = true;
1531
1544
  } catch {
1532
1545
  inRebase = false;
@@ -1564,6 +1577,328 @@ var require_git_executor = __commonJS({
1564
1577
  };
1565
1578
  }
1566
1579
  }
1580
+ // ========================================
1581
+ // EP944: Worktree operations
1582
+ // ========================================
1583
+ /**
1584
+ * EP944: Add a new worktree for a branch
1585
+ * Creates a new working tree at the specified path
1586
+ */
1587
+ async executeWorktreeAdd(command, cwd, options) {
1588
+ try {
1589
+ const validation = (0, git_validator_1.validateBranchName)(command.branch);
1590
+ if (!validation.valid) {
1591
+ return {
1592
+ success: false,
1593
+ error: validation.error || "UNKNOWN_ERROR"
1594
+ };
1595
+ }
1596
+ const fs12 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1597
+ try {
1598
+ await fs12.access(command.path);
1599
+ return {
1600
+ success: false,
1601
+ error: "WORKTREE_EXISTS",
1602
+ output: `Worktree already exists at path: ${command.path}`
1603
+ };
1604
+ } catch {
1605
+ }
1606
+ try {
1607
+ await this.runGitCommand(["fetch", "--all", "--prune"], cwd, options);
1608
+ } catch {
1609
+ }
1610
+ const args = ["worktree", "add"];
1611
+ if (command.create) {
1612
+ args.push("-b", command.branch, command.path);
1613
+ } else {
1614
+ args.push(command.path, command.branch);
1615
+ }
1616
+ const result = await this.runGitCommand(args, cwd, options);
1617
+ if (result.success) {
1618
+ return {
1619
+ success: true,
1620
+ output: `Created worktree at ${command.path} for branch ${command.branch}`,
1621
+ details: {
1622
+ worktreePath: command.path,
1623
+ branchName: command.branch
1624
+ }
1625
+ };
1626
+ }
1627
+ if (result.output?.includes("already checked out")) {
1628
+ return {
1629
+ success: false,
1630
+ error: "BRANCH_IN_USE",
1631
+ output: `Branch '${command.branch}' is already checked out in another worktree`
1632
+ };
1633
+ }
1634
+ return result;
1635
+ } catch (error) {
1636
+ return {
1637
+ success: false,
1638
+ error: "UNKNOWN_ERROR",
1639
+ output: error.message || "Failed to add worktree"
1640
+ };
1641
+ }
1642
+ }
1643
+ /**
1644
+ * EP944: Remove a worktree
1645
+ * Removes the working tree at the specified path
1646
+ */
1647
+ async executeWorktreeRemove(command, cwd, options) {
1648
+ try {
1649
+ const fs12 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1650
+ try {
1651
+ await fs12.access(command.path);
1652
+ } catch {
1653
+ return {
1654
+ success: false,
1655
+ error: "WORKTREE_NOT_FOUND",
1656
+ output: `Worktree not found at path: ${command.path}`
1657
+ };
1658
+ }
1659
+ if (!command.force) {
1660
+ try {
1661
+ const { stdout } = await execAsync("git status --porcelain", {
1662
+ cwd: command.path,
1663
+ timeout: options?.timeout || 1e4
1664
+ });
1665
+ if (stdout.trim()) {
1666
+ return {
1667
+ success: false,
1668
+ error: "UNCOMMITTED_CHANGES",
1669
+ output: "Worktree has uncommitted changes. Use force to remove anyway.",
1670
+ details: {
1671
+ uncommittedFiles: stdout.trim().split("\n").map((line) => line.slice(3))
1672
+ }
1673
+ };
1674
+ }
1675
+ } catch {
1676
+ }
1677
+ }
1678
+ const args = ["worktree", "remove"];
1679
+ if (command.force) {
1680
+ args.push("--force");
1681
+ }
1682
+ args.push(command.path);
1683
+ const result = await this.runGitCommand(args, cwd, options);
1684
+ if (result.success) {
1685
+ return {
1686
+ success: true,
1687
+ output: `Removed worktree at ${command.path}`,
1688
+ details: {
1689
+ worktreePath: command.path
1690
+ }
1691
+ };
1692
+ }
1693
+ if (result.output?.includes("locked")) {
1694
+ return {
1695
+ success: false,
1696
+ error: "WORKTREE_LOCKED",
1697
+ output: `Worktree at ${command.path} is locked`
1698
+ };
1699
+ }
1700
+ return result;
1701
+ } catch (error) {
1702
+ return {
1703
+ success: false,
1704
+ error: "UNKNOWN_ERROR",
1705
+ output: error.message || "Failed to remove worktree"
1706
+ };
1707
+ }
1708
+ }
1709
+ /**
1710
+ * EP944: List all worktrees
1711
+ * Returns information about all worktrees in the repository
1712
+ */
1713
+ async executeWorktreeList(cwd, options) {
1714
+ try {
1715
+ const { stdout } = await execAsync("git worktree list --porcelain", {
1716
+ cwd,
1717
+ timeout: options?.timeout || 1e4
1718
+ });
1719
+ const worktrees = [];
1720
+ const lines = stdout.trim().split("\n");
1721
+ let current = {};
1722
+ for (const line of lines) {
1723
+ if (line.startsWith("worktree ")) {
1724
+ current.path = line.slice(9);
1725
+ } else if (line.startsWith("HEAD ")) {
1726
+ current.commit = line.slice(5);
1727
+ } else if (line.startsWith("branch ")) {
1728
+ const refPath = line.slice(7);
1729
+ current.branch = refPath.replace("refs/heads/", "");
1730
+ } else if (line === "locked") {
1731
+ current.locked = true;
1732
+ } else if (line === "prunable") {
1733
+ current.prunable = true;
1734
+ } else if (line.startsWith("detached")) {
1735
+ current.branch = "HEAD (detached)";
1736
+ } else if (line === "" && current.path) {
1737
+ worktrees.push({
1738
+ path: current.path,
1739
+ branch: current.branch || "unknown",
1740
+ commit: current.commit || "",
1741
+ locked: current.locked,
1742
+ prunable: current.prunable
1743
+ });
1744
+ current = {};
1745
+ }
1746
+ }
1747
+ if (current.path) {
1748
+ worktrees.push({
1749
+ path: current.path,
1750
+ branch: current.branch || "unknown",
1751
+ commit: current.commit || "",
1752
+ locked: current.locked,
1753
+ prunable: current.prunable
1754
+ });
1755
+ }
1756
+ return {
1757
+ success: true,
1758
+ output: `Found ${worktrees.length} worktree(s)`,
1759
+ details: {
1760
+ worktrees
1761
+ }
1762
+ };
1763
+ } catch (error) {
1764
+ return {
1765
+ success: false,
1766
+ error: "UNKNOWN_ERROR",
1767
+ output: error.message || "Failed to list worktrees"
1768
+ };
1769
+ }
1770
+ }
1771
+ /**
1772
+ * EP944: Prune stale worktrees
1773
+ * Removes worktree administrative files for worktrees whose directories are missing
1774
+ */
1775
+ async executeWorktreePrune(cwd, options) {
1776
+ try {
1777
+ const listResult = await this.executeWorktreeList(cwd, options);
1778
+ const prunableCount = listResult.details?.worktrees?.filter((w) => w.prunable).length || 0;
1779
+ const result = await this.runGitCommand(["worktree", "prune"], cwd, options);
1780
+ if (result.success) {
1781
+ return {
1782
+ success: true,
1783
+ output: prunableCount > 0 ? `Pruned ${prunableCount} stale worktree(s)` : "No stale worktrees to prune",
1784
+ details: {
1785
+ prunedCount: prunableCount
1786
+ }
1787
+ };
1788
+ }
1789
+ return result;
1790
+ } catch (error) {
1791
+ return {
1792
+ success: false,
1793
+ error: "UNKNOWN_ERROR",
1794
+ output: error.message || "Failed to prune worktrees"
1795
+ };
1796
+ }
1797
+ }
1798
+ /**
1799
+ * EP944: Clone a repository as a bare repository
1800
+ * Used for worktree-based development setup
1801
+ */
1802
+ async executeCloneBare(command, options) {
1803
+ try {
1804
+ const fs12 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1805
+ const path14 = await Promise.resolve().then(() => __importStar(require("path")));
1806
+ try {
1807
+ await fs12.access(command.path);
1808
+ return {
1809
+ success: false,
1810
+ error: "BRANCH_ALREADY_EXISTS",
1811
+ // Reusing for path exists
1812
+ output: `Directory already exists at path: ${command.path}`
1813
+ };
1814
+ } catch {
1815
+ }
1816
+ const parentDir = path14.dirname(command.path);
1817
+ try {
1818
+ await fs12.mkdir(parentDir, { recursive: true });
1819
+ } catch {
1820
+ }
1821
+ const { stdout, stderr } = await execAsync(
1822
+ `git clone --bare "${command.url}" "${command.path}"`,
1823
+ { timeout: options?.timeout || 12e4 }
1824
+ // 2 minutes for clone
1825
+ );
1826
+ return {
1827
+ success: true,
1828
+ output: `Cloned bare repository to ${command.path}`,
1829
+ details: {
1830
+ worktreePath: command.path
1831
+ }
1832
+ };
1833
+ } catch (error) {
1834
+ if (error.message?.includes("Authentication") || error.message?.includes("Permission denied")) {
1835
+ return {
1836
+ success: false,
1837
+ error: "AUTH_FAILURE",
1838
+ output: "Authentication failed. Please check your credentials."
1839
+ };
1840
+ }
1841
+ if (error.message?.includes("Could not resolve") || error.message?.includes("unable to access")) {
1842
+ return {
1843
+ success: false,
1844
+ error: "NETWORK_ERROR",
1845
+ output: "Network error. Please check your connection."
1846
+ };
1847
+ }
1848
+ return {
1849
+ success: false,
1850
+ error: "UNKNOWN_ERROR",
1851
+ output: error.message || "Failed to clone repository"
1852
+ };
1853
+ }
1854
+ }
1855
+ /**
1856
+ * EP944: Get project info including worktree mode
1857
+ * Returns information about the project configuration
1858
+ */
1859
+ async executeProjectInfo(cwd, options) {
1860
+ try {
1861
+ const fs12 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1862
+ const path14 = await Promise.resolve().then(() => __importStar(require("path")));
1863
+ let currentPath = cwd;
1864
+ let worktreeMode = false;
1865
+ let projectPath = cwd;
1866
+ let bareRepoPath;
1867
+ for (let i = 0; i < 10; i++) {
1868
+ const bareDir = path14.join(currentPath, ".bare");
1869
+ const episodaDir = path14.join(currentPath, ".episoda");
1870
+ try {
1871
+ await fs12.access(bareDir);
1872
+ await fs12.access(episodaDir);
1873
+ worktreeMode = true;
1874
+ projectPath = currentPath;
1875
+ bareRepoPath = bareDir;
1876
+ break;
1877
+ } catch {
1878
+ const parentPath = path14.dirname(currentPath);
1879
+ if (parentPath === currentPath) {
1880
+ break;
1881
+ }
1882
+ currentPath = parentPath;
1883
+ }
1884
+ }
1885
+ return {
1886
+ success: true,
1887
+ output: worktreeMode ? "Worktree mode project" : "Standard git project",
1888
+ details: {
1889
+ worktreeMode,
1890
+ projectPath,
1891
+ bareRepoPath
1892
+ }
1893
+ };
1894
+ } catch (error) {
1895
+ return {
1896
+ success: false,
1897
+ error: "UNKNOWN_ERROR",
1898
+ output: error.message || "Failed to get project info"
1899
+ };
1900
+ }
1901
+ }
1567
1902
  /**
1568
1903
  * Run a git command and return structured result
1569
1904
  */
@@ -1663,7 +1998,7 @@ var require_git_executor = __commonJS({
1663
1998
  }
1664
1999
  }
1665
2000
  };
1666
- exports2.GitExecutor = GitExecutor;
2001
+ exports2.GitExecutor = GitExecutor6;
1667
2002
  }
1668
2003
  });
1669
2004
 
@@ -1754,7 +2089,7 @@ var require_websocket_client = __commonJS({
1754
2089
  clearTimeout(this.reconnectTimeout);
1755
2090
  this.reconnectTimeout = void 0;
1756
2091
  }
1757
- return new Promise((resolve2, reject) => {
2092
+ return new Promise((resolve4, reject) => {
1758
2093
  const connectionTimeout = setTimeout(() => {
1759
2094
  if (this.ws) {
1760
2095
  this.ws.terminate();
@@ -1781,7 +2116,7 @@ var require_websocket_client = __commonJS({
1781
2116
  daemonPid: this.daemonPid
1782
2117
  });
1783
2118
  this.startHeartbeat();
1784
- resolve2();
2119
+ resolve4();
1785
2120
  });
1786
2121
  this.ws.on("pong", () => {
1787
2122
  if (this.heartbeatTimeoutTimer) {
@@ -1897,13 +2232,13 @@ var require_websocket_client = __commonJS({
1897
2232
  if (!this.ws || !this.isConnected) {
1898
2233
  throw new Error("WebSocket not connected");
1899
2234
  }
1900
- return new Promise((resolve2, reject) => {
2235
+ return new Promise((resolve4, reject) => {
1901
2236
  this.ws.send(JSON.stringify(message), (error) => {
1902
2237
  if (error) {
1903
2238
  console.error("[EpisodaClient] Failed to send message:", error);
1904
2239
  reject(error);
1905
2240
  } else {
1906
- resolve2();
2241
+ resolve4();
1907
2242
  }
1908
2243
  });
1909
2244
  });
@@ -2016,12 +2351,13 @@ var require_websocket_client = __commonJS({
2016
2351
  console.log(`[EpisodaClient] Server restarting, reconnecting in ${delay}ms (attempt ${this.reconnectAttempts + 1}/7)`);
2017
2352
  }
2018
2353
  } else {
2019
- if (this.reconnectAttempts >= 1) {
2020
- console.error('[EpisodaClient] Connection lost. Retry failed. Check server status or restart with "episoda dev".');
2354
+ const MAX_NON_GRACEFUL_RETRIES = 5;
2355
+ if (this.reconnectAttempts >= MAX_NON_GRACEFUL_RETRIES) {
2356
+ console.error(`[EpisodaClient] Connection lost. Reconnection failed after ${MAX_NON_GRACEFUL_RETRIES} attempts. Check server status or restart with "episoda dev".`);
2021
2357
  shouldRetry = false;
2022
2358
  } else {
2023
- delay = 1e3;
2024
- console.log("[EpisodaClient] Connection lost, retrying in 1 second...");
2359
+ delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts), 16e3);
2360
+ console.log(`[EpisodaClient] Connection lost, retrying in ${delay / 1e3}s... (attempt ${this.reconnectAttempts + 1}/${MAX_NON_GRACEFUL_RETRIES})`);
2025
2361
  }
2026
2362
  }
2027
2363
  if (!shouldRetry) {
@@ -2129,36 +2465,36 @@ var require_auth = __commonJS({
2129
2465
  };
2130
2466
  })();
2131
2467
  Object.defineProperty(exports2, "__esModule", { value: true });
2132
- exports2.getConfigDir = getConfigDir4;
2468
+ exports2.getConfigDir = getConfigDir5;
2133
2469
  exports2.getConfigPath = getConfigPath4;
2134
- exports2.loadConfig = loadConfig3;
2470
+ exports2.loadConfig = loadConfig7;
2135
2471
  exports2.saveConfig = saveConfig4;
2136
2472
  exports2.validateToken = validateToken;
2137
- var fs6 = __importStar(require("fs"));
2138
- var path8 = __importStar(require("path"));
2473
+ var fs12 = __importStar(require("fs"));
2474
+ var path14 = __importStar(require("path"));
2139
2475
  var os3 = __importStar(require("os"));
2140
2476
  var child_process_1 = require("child_process");
2141
2477
  var DEFAULT_CONFIG_FILE = "config.json";
2142
- function getConfigDir4() {
2143
- return process.env.EPISODA_CONFIG_DIR || path8.join(os3.homedir(), ".episoda");
2478
+ function getConfigDir5() {
2479
+ return process.env.EPISODA_CONFIG_DIR || path14.join(os3.homedir(), ".episoda");
2144
2480
  }
2145
2481
  function getConfigPath4(configPath) {
2146
2482
  if (configPath) {
2147
2483
  return configPath;
2148
2484
  }
2149
- return path8.join(getConfigDir4(), DEFAULT_CONFIG_FILE);
2485
+ return path14.join(getConfigDir5(), DEFAULT_CONFIG_FILE);
2150
2486
  }
2151
2487
  function ensureConfigDir(configPath) {
2152
- const dir = path8.dirname(configPath);
2153
- const isNew = !fs6.existsSync(dir);
2488
+ const dir = path14.dirname(configPath);
2489
+ const isNew = !fs12.existsSync(dir);
2154
2490
  if (isNew) {
2155
- fs6.mkdirSync(dir, { recursive: true, mode: 448 });
2491
+ fs12.mkdirSync(dir, { recursive: true, mode: 448 });
2156
2492
  }
2157
2493
  if (process.platform === "darwin") {
2158
- const nosyncPath = path8.join(dir, ".nosync");
2159
- if (isNew || !fs6.existsSync(nosyncPath)) {
2494
+ const nosyncPath = path14.join(dir, ".nosync");
2495
+ if (isNew || !fs12.existsSync(nosyncPath)) {
2160
2496
  try {
2161
- fs6.writeFileSync(nosyncPath, "", { mode: 384 });
2497
+ fs12.writeFileSync(nosyncPath, "", { mode: 384 });
2162
2498
  (0, child_process_1.execSync)(`xattr -w com.apple.fileprovider.ignore 1 "${dir}"`, {
2163
2499
  stdio: "ignore",
2164
2500
  timeout: 5e3
@@ -2168,13 +2504,13 @@ var require_auth = __commonJS({
2168
2504
  }
2169
2505
  }
2170
2506
  }
2171
- async function loadConfig3(configPath) {
2507
+ async function loadConfig7(configPath) {
2172
2508
  const fullPath = getConfigPath4(configPath);
2173
- if (!fs6.existsSync(fullPath)) {
2509
+ if (!fs12.existsSync(fullPath)) {
2174
2510
  return null;
2175
2511
  }
2176
2512
  try {
2177
- const content = fs6.readFileSync(fullPath, "utf8");
2513
+ const content = fs12.readFileSync(fullPath, "utf8");
2178
2514
  const config = JSON.parse(content);
2179
2515
  return config;
2180
2516
  } catch (error) {
@@ -2187,7 +2523,7 @@ var require_auth = __commonJS({
2187
2523
  ensureConfigDir(fullPath);
2188
2524
  try {
2189
2525
  const content = JSON.stringify(config, null, 2);
2190
- fs6.writeFileSync(fullPath, content, { mode: 384 });
2526
+ fs12.writeFileSync(fullPath, content, { mode: 384 });
2191
2527
  } catch (error) {
2192
2528
  throw new Error(`Failed to save config: ${error instanceof Error ? error.message : String(error)}`);
2193
2529
  }
@@ -2227,6 +2563,11 @@ var require_errors = __commonJS({
2227
2563
  "BRANCH_ALREADY_EXISTS": "Branch already exists",
2228
2564
  "PUSH_REJECTED": "Push rejected by remote",
2229
2565
  "COMMAND_TIMEOUT": "Command timed out",
2566
+ // EP944: Worktree error messages
2567
+ "WORKTREE_EXISTS": "Worktree already exists at this path",
2568
+ "WORKTREE_NOT_FOUND": "Worktree not found at this path",
2569
+ "WORKTREE_LOCKED": "Worktree is locked",
2570
+ "BRANCH_IN_USE": "Branch is already checked out in another worktree",
2230
2571
  "UNKNOWN_ERROR": "Unknown error occurred"
2231
2572
  };
2232
2573
  let message = messages[code] || `Error: ${code}`;
@@ -2293,10 +2634,10 @@ var require_dist = __commonJS({
2293
2634
 
2294
2635
  // src/index.ts
2295
2636
  var import_commander = require("commander");
2296
- var import_core8 = __toESM(require_dist());
2637
+ var import_core15 = __toESM(require_dist());
2297
2638
 
2298
2639
  // src/commands/dev.ts
2299
- var import_core3 = __toESM(require_dist());
2640
+ var import_core6 = __toESM(require_dist());
2300
2641
 
2301
2642
  // src/framework-detector.ts
2302
2643
  var fs = __toESM(require("fs"));
@@ -2630,7 +2971,7 @@ async function startDaemon() {
2630
2971
  const pid = child.pid;
2631
2972
  const pidPath = getPidFilePath();
2632
2973
  fs2.writeFileSync(pidPath, pid.toString(), "utf-8");
2633
- await new Promise((resolve2) => setTimeout(resolve2, 500));
2974
+ await new Promise((resolve4) => setTimeout(resolve4, 500));
2634
2975
  const runningPid = isDaemonRunning();
2635
2976
  if (!runningPid) {
2636
2977
  throw new Error("Daemon failed to start");
@@ -2652,7 +2993,7 @@ async function stopDaemon(timeout = 5e3) {
2652
2993
  while (Date.now() - startTime < timeout) {
2653
2994
  try {
2654
2995
  process.kill(pid, 0);
2655
- await new Promise((resolve2) => setTimeout(resolve2, 100));
2996
+ await new Promise((resolve4) => setTimeout(resolve4, 100));
2656
2997
  } catch (error) {
2657
2998
  const pidPath2 = getPidFilePath();
2658
2999
  if (fs2.existsSync(pidPath2)) {
@@ -2682,7 +3023,7 @@ var import_core2 = __toESM(require_dist());
2682
3023
  var getSocketPath = () => path3.join((0, import_core2.getConfigDir)(), "daemon.sock");
2683
3024
  var DEFAULT_TIMEOUT = 15e3;
2684
3025
  async function sendCommand(command, params, timeout = DEFAULT_TIMEOUT) {
2685
- return new Promise((resolve2, reject) => {
3026
+ return new Promise((resolve4, reject) => {
2686
3027
  const socket = net.createConnection(getSocketPath());
2687
3028
  const requestId = crypto.randomUUID();
2688
3029
  let buffer = "";
@@ -2713,7 +3054,7 @@ async function sendCommand(command, params, timeout = DEFAULT_TIMEOUT) {
2713
3054
  clearTimeout(timeoutHandle);
2714
3055
  socket.end();
2715
3056
  if (response.success) {
2716
- resolve2(response.data);
3057
+ resolve4(response.data);
2717
3058
  } else {
2718
3059
  reject(new Error(response.error || "Command failed"));
2719
3060
  }
@@ -2769,40 +3110,824 @@ async function getDevServerStatus() {
2769
3110
  }
2770
3111
 
2771
3112
  // src/commands/dev.ts
2772
- var import_child_process2 = require("child_process");
2773
- var path4 = __toESM(require("path"));
3113
+ var import_child_process3 = require("child_process");
3114
+ var path7 = __toESM(require("path"));
3115
+ var fs6 = __toESM(require("fs"));
2774
3116
 
2775
3117
  // src/utils/port-check.ts
2776
3118
  var net2 = __toESM(require("net"));
2777
3119
  async function isPortInUse(port) {
2778
- return new Promise((resolve2) => {
3120
+ return new Promise((resolve4) => {
2779
3121
  const server = net2.createServer();
2780
3122
  server.once("error", (err) => {
2781
3123
  if (err.code === "EADDRINUSE") {
2782
- resolve2(true);
3124
+ resolve4(true);
2783
3125
  } else {
2784
- resolve2(false);
3126
+ resolve4(false);
2785
3127
  }
2786
3128
  });
2787
3129
  server.once("listening", () => {
2788
3130
  server.close();
2789
- resolve2(false);
3131
+ resolve4(false);
3132
+ });
3133
+ server.listen(port);
3134
+ });
3135
+ }
3136
+ function getServerPort() {
3137
+ if (process.env.PORT) {
3138
+ return parseInt(process.env.PORT, 10);
3139
+ }
3140
+ return 3e3;
3141
+ }
3142
+
3143
+ // src/daemon/worktree-manager.ts
3144
+ var fs3 = __toESM(require("fs"));
3145
+ var path4 = __toESM(require("path"));
3146
+ var import_core3 = __toESM(require_dist());
3147
+ function validateModuleUid(moduleUid) {
3148
+ if (!moduleUid || typeof moduleUid !== "string" || !moduleUid.trim()) {
3149
+ return false;
3150
+ }
3151
+ if (moduleUid.includes("/") || moduleUid.includes("\\") || moduleUid.includes("..")) {
3152
+ return false;
3153
+ }
3154
+ if (moduleUid.includes("\0")) {
3155
+ return false;
3156
+ }
3157
+ if (moduleUid.startsWith(".")) {
3158
+ return false;
3159
+ }
3160
+ return true;
3161
+ }
3162
+ var WorktreeManager = class _WorktreeManager {
3163
+ constructor(projectRoot) {
3164
+ // ============================================================
3165
+ // Private methods
3166
+ // ============================================================
3167
+ this.lockPath = "";
3168
+ this.projectRoot = projectRoot;
3169
+ this.bareRepoPath = path4.join(projectRoot, ".bare");
3170
+ this.configPath = path4.join(projectRoot, ".episoda", "config.json");
3171
+ this.gitExecutor = new import_core3.GitExecutor();
3172
+ }
3173
+ /**
3174
+ * Initialize worktree manager from existing project root
3175
+ * @returns true if valid worktree project, false otherwise
3176
+ */
3177
+ async initialize() {
3178
+ if (!fs3.existsSync(this.bareRepoPath)) {
3179
+ return false;
3180
+ }
3181
+ if (!fs3.existsSync(this.configPath)) {
3182
+ return false;
3183
+ }
3184
+ try {
3185
+ const config = this.readConfig();
3186
+ return config?.worktreeMode === true;
3187
+ } catch {
3188
+ return false;
3189
+ }
3190
+ }
3191
+ /**
3192
+ * Create a new worktree project from scratch
3193
+ */
3194
+ static async createProject(projectRoot, repoUrl, projectId, workspaceSlug, projectSlug) {
3195
+ const manager = new _WorktreeManager(projectRoot);
3196
+ const episodaDir = path4.join(projectRoot, ".episoda");
3197
+ fs3.mkdirSync(episodaDir, { recursive: true });
3198
+ const cloneResult = await manager.gitExecutor.execute({
3199
+ action: "clone_bare",
3200
+ url: repoUrl,
3201
+ path: manager.bareRepoPath
3202
+ });
3203
+ if (!cloneResult.success) {
3204
+ throw new Error(`Failed to clone repository: ${cloneResult.output}`);
3205
+ }
3206
+ const config = {
3207
+ projectId,
3208
+ workspaceSlug,
3209
+ projectSlug,
3210
+ bareRepoPath: manager.bareRepoPath,
3211
+ worktreeMode: true,
3212
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3213
+ worktrees: []
3214
+ };
3215
+ manager.writeConfig(config);
3216
+ return manager;
3217
+ }
3218
+ /**
3219
+ * Create a worktree for a module
3220
+ * The entire operation is locked to prevent race conditions
3221
+ */
3222
+ async createWorktree(moduleUid, branchName, createBranch = false) {
3223
+ if (!validateModuleUid(moduleUid)) {
3224
+ return {
3225
+ success: false,
3226
+ error: `Invalid module UID: "${moduleUid}" - contains disallowed characters`
3227
+ };
3228
+ }
3229
+ const worktreePath = path4.join(this.projectRoot, moduleUid);
3230
+ const lockAcquired = await this.acquireLock();
3231
+ if (!lockAcquired) {
3232
+ return {
3233
+ success: false,
3234
+ error: "Could not acquire lock for worktree creation"
3235
+ };
3236
+ }
3237
+ try {
3238
+ const existing = this.getWorktreeByModuleUid(moduleUid);
3239
+ if (existing) {
3240
+ return {
3241
+ success: true,
3242
+ worktreePath: existing.worktreePath,
3243
+ worktreeInfo: existing
3244
+ };
3245
+ }
3246
+ const result = await this.gitExecutor.execute({
3247
+ action: "worktree_add",
3248
+ path: worktreePath,
3249
+ branch: branchName,
3250
+ create: createBranch
3251
+ }, { cwd: this.bareRepoPath });
3252
+ if (!result.success) {
3253
+ return {
3254
+ success: false,
3255
+ error: result.output || "Failed to create worktree"
3256
+ };
3257
+ }
3258
+ const worktreeInfo = {
3259
+ moduleUid,
3260
+ branchName,
3261
+ worktreePath,
3262
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3263
+ lastAccessed: (/* @__PURE__ */ new Date()).toISOString()
3264
+ };
3265
+ const config = this.readConfig();
3266
+ if (config) {
3267
+ config.worktrees.push(worktreeInfo);
3268
+ this.writeConfig(config);
3269
+ }
3270
+ return {
3271
+ success: true,
3272
+ worktreePath,
3273
+ worktreeInfo
3274
+ };
3275
+ } finally {
3276
+ this.releaseLock();
3277
+ }
3278
+ }
3279
+ /**
3280
+ * Remove a worktree for a module
3281
+ * P1-2: Wrapped entire operation in lock to prevent race with createWorktree
3282
+ */
3283
+ async removeWorktree(moduleUid, force = false) {
3284
+ if (!validateModuleUid(moduleUid)) {
3285
+ return {
3286
+ success: false,
3287
+ error: `Invalid module UID: "${moduleUid}" - contains disallowed characters`
3288
+ };
3289
+ }
3290
+ const lockAcquired = await this.acquireLock();
3291
+ if (!lockAcquired) {
3292
+ return {
3293
+ success: false,
3294
+ error: "Could not acquire lock for worktree removal"
3295
+ };
3296
+ }
3297
+ try {
3298
+ const existing = this.getWorktreeByModuleUid(moduleUid);
3299
+ if (!existing) {
3300
+ return {
3301
+ success: false,
3302
+ error: `No worktree found for module ${moduleUid}`
3303
+ };
3304
+ }
3305
+ const result = await this.gitExecutor.execute({
3306
+ action: "worktree_remove",
3307
+ path: existing.worktreePath,
3308
+ force
3309
+ }, { cwd: this.bareRepoPath });
3310
+ if (!result.success) {
3311
+ return {
3312
+ success: false,
3313
+ error: result.output || "Failed to remove worktree"
3314
+ };
3315
+ }
3316
+ const config = this.readConfig();
3317
+ if (config) {
3318
+ config.worktrees = config.worktrees.filter((w) => w.moduleUid !== moduleUid);
3319
+ this.writeConfig(config);
3320
+ }
3321
+ return {
3322
+ success: true,
3323
+ worktreePath: existing.worktreePath
3324
+ };
3325
+ } finally {
3326
+ this.releaseLock();
3327
+ }
3328
+ }
3329
+ /**
3330
+ * Get worktree path for a module
3331
+ */
3332
+ getWorktreePath(moduleUid) {
3333
+ if (!validateModuleUid(moduleUid)) {
3334
+ return null;
3335
+ }
3336
+ const worktree = this.getWorktreeByModuleUid(moduleUid);
3337
+ return worktree?.worktreePath || null;
3338
+ }
3339
+ /**
3340
+ * Get worktree info by module UID
3341
+ */
3342
+ getWorktreeByModuleUid(moduleUid) {
3343
+ const config = this.readConfig();
3344
+ return config?.worktrees.find((w) => w.moduleUid === moduleUid) || null;
3345
+ }
3346
+ /**
3347
+ * Get worktree info by branch name
3348
+ */
3349
+ getWorktreeByBranch(branchName) {
3350
+ const config = this.readConfig();
3351
+ return config?.worktrees.find((w) => w.branchName === branchName) || null;
3352
+ }
3353
+ /**
3354
+ * List all active worktrees
3355
+ */
3356
+ listWorktrees() {
3357
+ const config = this.readConfig();
3358
+ return config?.worktrees || [];
3359
+ }
3360
+ /**
3361
+ * EP957: Audit worktrees against known active module UIDs
3362
+ *
3363
+ * Compares local worktrees against a list of module UIDs that should be active.
3364
+ * Used by daemon startup to detect orphaned worktrees from crashed sessions.
3365
+ *
3366
+ * @param activeModuleUids - UIDs of modules currently in doing/review state
3367
+ * @returns Object with orphaned and valid worktree lists
3368
+ */
3369
+ auditWorktrees(activeModuleUids) {
3370
+ const allWorktrees = this.listWorktrees();
3371
+ const activeSet = new Set(activeModuleUids);
3372
+ const orphaned = allWorktrees.filter((w) => !activeSet.has(w.moduleUid));
3373
+ const valid = allWorktrees.filter((w) => activeSet.has(w.moduleUid));
3374
+ return { orphaned, valid };
3375
+ }
3376
+ /**
3377
+ * Update last accessed timestamp for a worktree
3378
+ */
3379
+ async touchWorktree(moduleUid) {
3380
+ await this.updateConfigSafe((config) => {
3381
+ const worktree = config.worktrees.find((w) => w.moduleUid === moduleUid);
3382
+ if (worktree) {
3383
+ worktree.lastAccessed = (/* @__PURE__ */ new Date()).toISOString();
3384
+ }
3385
+ return config;
3386
+ });
3387
+ }
3388
+ /**
3389
+ * Prune stale worktrees (directories that no longer exist)
3390
+ */
3391
+ async pruneStaleWorktrees() {
3392
+ await this.gitExecutor.execute({
3393
+ action: "worktree_prune"
3394
+ }, { cwd: this.bareRepoPath });
3395
+ let prunedCount = 0;
3396
+ await this.updateConfigSafe((config) => {
3397
+ const initialCount = config.worktrees.length;
3398
+ config.worktrees = config.worktrees.filter((w) => fs3.existsSync(w.worktreePath));
3399
+ prunedCount = initialCount - config.worktrees.length;
3400
+ return config;
3401
+ });
3402
+ return prunedCount;
3403
+ }
3404
+ /**
3405
+ * Validate all worktrees and sync with git
3406
+ */
3407
+ async validateWorktrees() {
3408
+ const config = this.readConfig();
3409
+ const valid = [];
3410
+ const stale = [];
3411
+ const orphaned = [];
3412
+ const listResult = await this.gitExecutor.execute({
3413
+ action: "worktree_list"
3414
+ }, { cwd: this.bareRepoPath });
3415
+ const actualWorktrees = new Set(
3416
+ listResult.details?.worktrees?.map((w) => w.path) || []
3417
+ );
3418
+ for (const worktree of config?.worktrees || []) {
3419
+ if (actualWorktrees.has(worktree.worktreePath)) {
3420
+ valid.push(worktree);
3421
+ actualWorktrees.delete(worktree.worktreePath);
3422
+ } else {
3423
+ stale.push(worktree);
3424
+ }
3425
+ }
3426
+ for (const wpath of actualWorktrees) {
3427
+ if (wpath !== this.bareRepoPath) {
3428
+ orphaned.push(wpath);
3429
+ }
3430
+ }
3431
+ return { valid, stale, orphaned };
3432
+ }
3433
+ /**
3434
+ * Get project configuration
3435
+ */
3436
+ getConfig() {
3437
+ return this.readConfig();
3438
+ }
3439
+ /**
3440
+ * Get the bare repo path
3441
+ */
3442
+ getBareRepoPath() {
3443
+ return this.bareRepoPath;
3444
+ }
3445
+ /**
3446
+ * Get the project root path
3447
+ */
3448
+ getProjectRoot() {
3449
+ return this.projectRoot;
3450
+ }
3451
+ getLockPath() {
3452
+ if (!this.lockPath) {
3453
+ this.lockPath = this.configPath + ".lock";
3454
+ }
3455
+ return this.lockPath;
3456
+ }
3457
+ /**
3458
+ * Check if a process is still running
3459
+ */
3460
+ isProcessRunning(pid) {
3461
+ try {
3462
+ process.kill(pid, 0);
3463
+ return true;
3464
+ } catch {
3465
+ return false;
3466
+ }
3467
+ }
3468
+ /**
3469
+ * Acquire a file lock with timeout
3470
+ * Uses atomic file creation to ensure only one process holds the lock
3471
+ * P1-1: Added PID verification before removing stale locks to prevent race conditions
3472
+ */
3473
+ async acquireLock(timeoutMs = 5e3) {
3474
+ const lockPath = this.getLockPath();
3475
+ const startTime = Date.now();
3476
+ const retryInterval = 50;
3477
+ while (Date.now() - startTime < timeoutMs) {
3478
+ try {
3479
+ fs3.writeFileSync(lockPath, String(process.pid), { flag: "wx" });
3480
+ return true;
3481
+ } catch (err) {
3482
+ if (err.code === "EEXIST") {
3483
+ try {
3484
+ const stats = fs3.statSync(lockPath);
3485
+ const lockAge = Date.now() - stats.mtimeMs;
3486
+ if (lockAge > 3e4) {
3487
+ try {
3488
+ const lockContent = fs3.readFileSync(lockPath, "utf-8").trim();
3489
+ const lockPid = parseInt(lockContent, 10);
3490
+ if (!isNaN(lockPid) && this.isProcessRunning(lockPid)) {
3491
+ await new Promise((resolve4) => setTimeout(resolve4, retryInterval));
3492
+ continue;
3493
+ }
3494
+ } catch {
3495
+ }
3496
+ try {
3497
+ fs3.unlinkSync(lockPath);
3498
+ } catch {
3499
+ }
3500
+ continue;
3501
+ }
3502
+ } catch {
3503
+ continue;
3504
+ }
3505
+ await new Promise((resolve4) => setTimeout(resolve4, retryInterval));
3506
+ continue;
3507
+ }
3508
+ throw err;
3509
+ }
3510
+ }
3511
+ return false;
3512
+ }
3513
+ /**
3514
+ * Release the file lock
3515
+ */
3516
+ releaseLock() {
3517
+ try {
3518
+ fs3.unlinkSync(this.getLockPath());
3519
+ } catch {
3520
+ }
3521
+ }
3522
+ readConfig() {
3523
+ try {
3524
+ if (!fs3.existsSync(this.configPath)) {
3525
+ return null;
3526
+ }
3527
+ const content = fs3.readFileSync(this.configPath, "utf-8");
3528
+ return JSON.parse(content);
3529
+ } catch (error) {
3530
+ console.error("[WorktreeManager] Failed to read config:", error);
3531
+ return null;
3532
+ }
3533
+ }
3534
+ writeConfig(config) {
3535
+ try {
3536
+ const dir = path4.dirname(this.configPath);
3537
+ if (!fs3.existsSync(dir)) {
3538
+ fs3.mkdirSync(dir, { recursive: true });
3539
+ }
3540
+ fs3.writeFileSync(this.configPath, JSON.stringify(config, null, 2), "utf-8");
3541
+ } catch (error) {
3542
+ console.error("[WorktreeManager] Failed to write config:", error);
3543
+ throw error;
3544
+ }
3545
+ }
3546
+ /**
3547
+ * Read-modify-write with file locking for safe concurrent access
3548
+ */
3549
+ async updateConfigSafe(updater) {
3550
+ const lockAcquired = await this.acquireLock();
3551
+ if (!lockAcquired) {
3552
+ console.error("[WorktreeManager] Failed to acquire lock for config update");
3553
+ return false;
3554
+ }
3555
+ try {
3556
+ const config = this.readConfig();
3557
+ if (!config) {
3558
+ return false;
3559
+ }
3560
+ const updated = updater(config);
3561
+ this.writeConfig(updated);
3562
+ return true;
3563
+ } finally {
3564
+ this.releaseLock();
3565
+ }
3566
+ }
3567
+ };
3568
+ function getEpisodaRoot() {
3569
+ return process.env.EPISODA_ROOT || path4.join(require("os").homedir(), "episoda");
3570
+ }
3571
+ function getProjectPath(workspaceSlug, projectSlug) {
3572
+ return path4.join(getEpisodaRoot(), workspaceSlug, projectSlug);
3573
+ }
3574
+ async function isWorktreeProject(projectRoot) {
3575
+ const manager = new WorktreeManager(projectRoot);
3576
+ return manager.initialize();
3577
+ }
3578
+ async function findProjectRoot(startPath) {
3579
+ let current = path4.resolve(startPath);
3580
+ const episodaRoot = getEpisodaRoot();
3581
+ if (!current.startsWith(episodaRoot)) {
3582
+ return null;
3583
+ }
3584
+ for (let i = 0; i < 10; i++) {
3585
+ const bareDir = path4.join(current, ".bare");
3586
+ const episodaDir = path4.join(current, ".episoda");
3587
+ if (fs3.existsSync(bareDir) && fs3.existsSync(episodaDir)) {
3588
+ if (await isWorktreeProject(current)) {
3589
+ return current;
3590
+ }
3591
+ }
3592
+ const parent = path4.dirname(current);
3593
+ if (parent === current) {
3594
+ break;
3595
+ }
3596
+ current = parent;
3597
+ }
3598
+ return null;
3599
+ }
3600
+
3601
+ // src/commands/migrate.ts
3602
+ var fs5 = __toESM(require("fs"));
3603
+ var path6 = __toESM(require("path"));
3604
+ var import_child_process2 = require("child_process");
3605
+ var import_core5 = __toESM(require_dist());
3606
+
3607
+ // src/daemon/project-tracker.ts
3608
+ var fs4 = __toESM(require("fs"));
3609
+ var path5 = __toESM(require("path"));
3610
+ var import_core4 = __toESM(require_dist());
3611
+ function getProjectsFilePath() {
3612
+ return path5.join((0, import_core4.getConfigDir)(), "projects.json");
3613
+ }
3614
+ function readProjects() {
3615
+ const projectsPath = getProjectsFilePath();
3616
+ try {
3617
+ if (!fs4.existsSync(projectsPath)) {
3618
+ return { projects: [] };
3619
+ }
3620
+ const content = fs4.readFileSync(projectsPath, "utf-8");
3621
+ const data = JSON.parse(content);
3622
+ if (!data.projects || !Array.isArray(data.projects)) {
3623
+ console.warn("Invalid projects.json structure, resetting");
3624
+ return { projects: [] };
3625
+ }
3626
+ return data;
3627
+ } catch (error) {
3628
+ console.error("Error reading projects.json:", error);
3629
+ return { projects: [] };
3630
+ }
3631
+ }
3632
+ function writeProjects(data) {
3633
+ const projectsPath = getProjectsFilePath();
3634
+ try {
3635
+ const dir = path5.dirname(projectsPath);
3636
+ if (!fs4.existsSync(dir)) {
3637
+ fs4.mkdirSync(dir, { recursive: true });
3638
+ }
3639
+ fs4.writeFileSync(projectsPath, JSON.stringify(data, null, 2), "utf-8");
3640
+ } catch (error) {
3641
+ throw new Error(`Failed to write projects.json: ${error}`);
3642
+ }
3643
+ }
3644
+ function addProject2(projectId, projectPath, options) {
3645
+ const data = readProjects();
3646
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3647
+ const existingByPath = data.projects.find((p) => p.path === projectPath);
3648
+ if (existingByPath) {
3649
+ existingByPath.id = projectId;
3650
+ existingByPath.last_active = now;
3651
+ if (options?.worktreeMode !== void 0) {
3652
+ existingByPath.worktreeMode = options.worktreeMode;
3653
+ }
3654
+ if (options?.bareRepoPath) {
3655
+ existingByPath.bareRepoPath = options.bareRepoPath;
3656
+ }
3657
+ writeProjects(data);
3658
+ return existingByPath;
3659
+ }
3660
+ const existingByIdIndex = data.projects.findIndex((p) => p.id === projectId);
3661
+ if (existingByIdIndex !== -1) {
3662
+ const existingById = data.projects[existingByIdIndex];
3663
+ console.log(`[ProjectTracker] Replacing project entry: ${existingById.path} -> ${projectPath}`);
3664
+ data.projects.splice(existingByIdIndex, 1);
3665
+ }
3666
+ const projectName = path5.basename(projectPath);
3667
+ const newProject = {
3668
+ id: projectId,
3669
+ path: projectPath,
3670
+ name: projectName,
3671
+ added_at: now,
3672
+ last_active: now,
3673
+ // EP944: Worktree mode fields
3674
+ worktreeMode: options?.worktreeMode,
3675
+ bareRepoPath: options?.bareRepoPath
3676
+ };
3677
+ data.projects.push(newProject);
3678
+ writeProjects(data);
3679
+ return newProject;
3680
+ }
3681
+ function getProject(projectPath) {
3682
+ const data = readProjects();
3683
+ return data.projects.find((p) => p.path === projectPath) || null;
3684
+ }
3685
+
3686
+ // src/commands/migrate.ts
3687
+ async function migrateCommand(options = {}) {
3688
+ const cwd = options.cwd || process.cwd();
3689
+ status.info("Checking current project...");
3690
+ status.info("");
3691
+ if (!fs5.existsSync(path6.join(cwd, ".git"))) {
3692
+ throw new Error(
3693
+ "Not a git repository.\nRun this command from the root of an existing git project."
3694
+ );
3695
+ }
3696
+ const bareDir = path6.join(cwd, ".bare");
3697
+ if (fs5.existsSync(bareDir)) {
3698
+ throw new Error(
3699
+ "This project is already in worktree mode.\nUse `episoda checkout {module}` to create worktrees."
3700
+ );
3701
+ }
3702
+ const config = await (0, import_core5.loadConfig)();
3703
+ if (!config || !config.access_token) {
3704
+ throw new Error(
3705
+ "Not authenticated. Please run `episoda auth` first."
3706
+ );
3707
+ }
3708
+ const episodaConfigPath = path6.join(cwd, ".episoda", "config.json");
3709
+ if (!fs5.existsSync(episodaConfigPath)) {
3710
+ throw new Error(
3711
+ "No .episoda/config.json found.\nThis project is not connected to episoda.dev.\nUse `episoda clone {workspace}/{project}` to clone a fresh copy."
3712
+ );
3713
+ }
3714
+ let existingConfig;
3715
+ try {
3716
+ existingConfig = JSON.parse(fs5.readFileSync(episodaConfigPath, "utf-8"));
3717
+ } catch (error) {
3718
+ throw new Error("Failed to read .episoda/config.json");
3719
+ }
3720
+ const trackedProject = getProject(cwd);
3721
+ const projectId = trackedProject?.id || existingConfig.project_id;
3722
+ if (!projectId) {
3723
+ throw new Error(
3724
+ "Cannot determine project ID.\nUse `episoda clone {workspace}/{project}` for a fresh clone."
3725
+ );
3726
+ }
3727
+ const gitExecutor = new import_core5.GitExecutor();
3728
+ let currentBranch;
3729
+ try {
3730
+ currentBranch = (0, import_child_process2.execSync)("git branch --show-current", {
3731
+ cwd,
3732
+ encoding: "utf-8"
3733
+ }).trim();
3734
+ } catch {
3735
+ throw new Error("Failed to determine current branch");
3736
+ }
3737
+ const statusResult = await gitExecutor.execute({ action: "status" }, { cwd });
3738
+ const hasUncommitted = (statusResult.details?.uncommittedFiles?.length || 0) > 0;
3739
+ if (hasUncommitted && !options.force) {
3740
+ status.warning("You have uncommitted changes:");
3741
+ for (const file of statusResult.details?.uncommittedFiles || []) {
3742
+ status.info(` - ${file}`);
3743
+ }
3744
+ status.info("");
3745
+ throw new Error(
3746
+ "Please commit or stash your changes first.\nOr use --force to stash changes automatically."
3747
+ );
3748
+ }
3749
+ let remoteUrl;
3750
+ try {
3751
+ remoteUrl = (0, import_child_process2.execSync)("git remote get-url origin", {
3752
+ cwd,
3753
+ encoding: "utf-8"
3754
+ }).trim();
3755
+ } catch {
3756
+ throw new Error(
3757
+ 'No git remote "origin" found.\nPlease configure a remote before migrating.'
3758
+ );
3759
+ }
3760
+ let workspaceSlug = existingConfig.workspace_slug || "default";
3761
+ let projectSlug = existingConfig.project_slug || path6.basename(cwd);
3762
+ try {
3763
+ const apiUrl = config.api_url || "https://episoda.dev";
3764
+ const response = await fetch(`${apiUrl}/api/projects/${projectId}`, {
3765
+ headers: {
3766
+ "Authorization": `Bearer ${config.access_token}`,
3767
+ "Content-Type": "application/json"
3768
+ }
3769
+ });
3770
+ if (response.ok) {
3771
+ const projectData = await response.json();
3772
+ workspaceSlug = projectData.workspace_slug || workspaceSlug;
3773
+ projectSlug = projectData.slug || projectSlug;
3774
+ }
3775
+ } catch {
3776
+ }
3777
+ const targetPath = getProjectPath(workspaceSlug, projectSlug);
3778
+ status.info("Migration Plan:");
3779
+ status.info(` Current path: ${cwd}`);
3780
+ status.info(` Target path: ${targetPath}`);
3781
+ status.info(` Remote URL: ${remoteUrl}`);
3782
+ status.info(` Current branch: ${currentBranch}`);
3783
+ if (hasUncommitted) {
3784
+ status.warning(` Uncommitted changes will be stashed`);
3785
+ }
3786
+ status.info("");
3787
+ if (options.dryRun) {
3788
+ status.info("Dry run complete. No changes made.");
3789
+ status.info("Remove --dry-run to perform the migration.");
3790
+ return;
3791
+ }
3792
+ const uncommittedFiles = statusResult.details?.uncommittedFiles || [];
3793
+ if (hasUncommitted) {
3794
+ status.info(`Found ${uncommittedFiles.length} uncommitted file(s) to preserve...`);
3795
+ }
3796
+ try {
3797
+ status.info("Creating target directory...");
3798
+ const episodaRoot = getEpisodaRoot();
3799
+ fs5.mkdirSync(targetPath, { recursive: true });
3800
+ status.info("Cloning as bare repository...");
3801
+ const bareRepoPath = path6.join(targetPath, ".bare");
3802
+ const cloneResult = await gitExecutor.execute({
3803
+ action: "clone_bare",
3804
+ url: remoteUrl,
3805
+ path: bareRepoPath
3806
+ });
3807
+ if (!cloneResult.success) {
3808
+ throw new Error(`Failed to clone: ${cloneResult.output}`);
3809
+ }
3810
+ status.success("\u2713 Bare repository cloned");
3811
+ status.info("Creating worktree configuration...");
3812
+ const episodaDir = path6.join(targetPath, ".episoda");
3813
+ fs5.mkdirSync(episodaDir, { recursive: true });
3814
+ const worktreeConfig = {
3815
+ projectId,
3816
+ workspaceSlug,
3817
+ projectSlug,
3818
+ bareRepoPath,
3819
+ worktreeMode: true,
3820
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3821
+ worktrees: []
3822
+ };
3823
+ fs5.writeFileSync(
3824
+ path6.join(episodaDir, "config.json"),
3825
+ JSON.stringify(worktreeConfig, null, 2),
3826
+ "utf-8"
3827
+ );
3828
+ status.info(`Creating worktree for branch "${currentBranch}"...`);
3829
+ const moduleUid = extractModuleUid(currentBranch) || "main";
3830
+ const worktreePath = path6.join(targetPath, moduleUid);
3831
+ const worktreeResult = await gitExecutor.execute({
3832
+ action: "worktree_add",
3833
+ path: worktreePath,
3834
+ branch: currentBranch,
3835
+ create: false
3836
+ }, { cwd: bareRepoPath });
3837
+ if (!worktreeResult.success) {
3838
+ throw new Error(`Failed to create worktree: ${worktreeResult.output}`);
3839
+ }
3840
+ status.success(`\u2713 Worktree created at ${worktreePath}`);
3841
+ worktreeConfig.worktrees.push({
3842
+ moduleUid,
3843
+ branchName: currentBranch,
3844
+ worktreePath,
3845
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3846
+ lastAccessed: (/* @__PURE__ */ new Date()).toISOString()
3847
+ });
3848
+ fs5.writeFileSync(
3849
+ path6.join(episodaDir, "config.json"),
3850
+ JSON.stringify(worktreeConfig, null, 2),
3851
+ "utf-8"
3852
+ );
3853
+ if (hasUncommitted && uncommittedFiles.length > 0) {
3854
+ status.info("Copying uncommitted changes to new worktree...");
3855
+ let copiedCount = 0;
3856
+ let failedCount = 0;
3857
+ for (const file of uncommittedFiles) {
3858
+ const sourcePath = path6.join(cwd, file);
3859
+ const destPath = path6.join(worktreePath, file);
3860
+ try {
3861
+ if (fs5.existsSync(sourcePath)) {
3862
+ const destDir = path6.dirname(destPath);
3863
+ if (!fs5.existsSync(destDir)) {
3864
+ fs5.mkdirSync(destDir, { recursive: true });
3865
+ }
3866
+ fs5.copyFileSync(sourcePath, destPath);
3867
+ copiedCount++;
3868
+ }
3869
+ } catch (copyError) {
3870
+ failedCount++;
3871
+ status.warning(`Could not copy: ${file}`);
3872
+ }
3873
+ }
3874
+ if (copiedCount > 0) {
3875
+ status.success(`\u2713 Copied ${copiedCount} uncommitted file(s)`);
3876
+ }
3877
+ if (failedCount > 0) {
3878
+ status.warning(`${failedCount} file(s) could not be copied`);
3879
+ status.info("You may need to copy some files manually from the original directory");
3880
+ }
3881
+ }
3882
+ addProject2(projectId, targetPath, {
3883
+ worktreeMode: true,
3884
+ bareRepoPath
2790
3885
  });
2791
- server.listen(port);
2792
- });
3886
+ status.success("\u2713 Project registered with daemon");
3887
+ status.info("");
3888
+ status.success("Migration complete!");
3889
+ status.info("");
3890
+ status.info("New project structure:");
3891
+ status.info(` ${targetPath}/`);
3892
+ status.info(" \u251C\u2500\u2500 .bare/ # Git repository");
3893
+ status.info(" \u251C\u2500\u2500 .episoda/");
3894
+ status.info(" \u2502 \u2514\u2500\u2500 config.json");
3895
+ status.info(` \u2514\u2500\u2500 ${moduleUid}/ # Your current worktree`);
3896
+ status.info("");
3897
+ status.info("Next steps:");
3898
+ status.info(` 1. cd ${worktreePath}`);
3899
+ status.info(" 2. Continue working on your current branch");
3900
+ status.info(" 3. Use `episoda checkout EP###` for other modules");
3901
+ status.info("");
3902
+ status.info("Your original repository at:");
3903
+ status.info(` ${cwd}`);
3904
+ status.info("can be safely removed after verifying the migration.");
3905
+ status.info("");
3906
+ } catch (error) {
3907
+ status.error("Migration failed, cleaning up...");
3908
+ if (fs5.existsSync(targetPath)) {
3909
+ try {
3910
+ fs5.rmSync(targetPath, { recursive: true, force: true });
3911
+ } catch {
3912
+ status.warning(`Could not remove ${targetPath}`);
3913
+ }
3914
+ }
3915
+ throw error;
3916
+ }
2793
3917
  }
2794
- function getServerPort() {
2795
- if (process.env.PORT) {
2796
- return parseInt(process.env.PORT, 10);
3918
+ function extractModuleUid(branchName) {
3919
+ const match = branchName.match(/ep(\d+)/i);
3920
+ if (match) {
3921
+ return `EP${match[1]}`;
2797
3922
  }
2798
- return 3e3;
3923
+ return null;
2799
3924
  }
2800
3925
 
2801
3926
  // src/commands/dev.ts
2802
3927
  var CONNECTION_MAX_RETRIES = 3;
2803
3928
  function findGitRoot(startDir) {
2804
3929
  try {
2805
- const result = (0, import_child_process2.execSync)("git rev-parse --show-toplevel", {
3930
+ const result = (0, import_child_process3.execSync)("git rev-parse --show-toplevel", {
2806
3931
  cwd: startDir,
2807
3932
  encoding: "utf-8",
2808
3933
  stdio: ["pipe", "pipe", "pipe"]
@@ -2814,7 +3939,7 @@ function findGitRoot(startDir) {
2814
3939
  }
2815
3940
  async function devCommand(options = {}) {
2816
3941
  try {
2817
- const config = await (0, import_core3.loadConfig)();
3942
+ const config = await (0, import_core6.loadConfig)();
2818
3943
  if (!config || !config.access_token || !config.project_id) {
2819
3944
  status.error("No authentication found. Please run:");
2820
3945
  status.info("");
@@ -2853,7 +3978,7 @@ async function devCommand(options = {}) {
2853
3978
  const killedCount = killAllEpisodaProcesses();
2854
3979
  if (killedCount > 0) {
2855
3980
  status.info(`Cleaned up ${killedCount} stale process${killedCount > 1 ? "es" : ""}`);
2856
- await new Promise((resolve2) => setTimeout(resolve2, 2e3));
3981
+ await new Promise((resolve4) => setTimeout(resolve4, 2e3));
2857
3982
  }
2858
3983
  }
2859
3984
  const serverUrl = config.api_url || process.env.EPISODA_API_URL || "https://episoda.dev";
@@ -2866,7 +3991,7 @@ async function devCommand(options = {}) {
2866
3991
  status.debug(`Using cached project path: ${projectPath}`);
2867
3992
  } else {
2868
3993
  const detectedRoot = findGitRoot(options.cwd || process.cwd());
2869
- projectPath = detectedRoot || path4.resolve(options.cwd || process.cwd());
3994
+ projectPath = detectedRoot || path7.resolve(options.cwd || process.cwd());
2870
3995
  if (detectedRoot) {
2871
3996
  status.debug(`Detected project root: ${projectPath}`);
2872
3997
  } else {
@@ -2880,7 +4005,7 @@ async function devCommand(options = {}) {
2880
4005
  cached_at: Date.now()
2881
4006
  }
2882
4007
  };
2883
- await (0, import_core3.saveConfig)(updatedConfig);
4008
+ await (0, import_core6.saveConfig)(updatedConfig);
2884
4009
  status.debug("Cached project settings locally");
2885
4010
  const settingsUrl = `${serverUrl}/api/projects/${config.project_id}/settings`;
2886
4011
  fetch(settingsUrl, {
@@ -2893,6 +4018,23 @@ async function devCommand(options = {}) {
2893
4018
  }).catch(() => {
2894
4019
  });
2895
4020
  }
4021
+ const hasGitDir = fs6.existsSync(path7.join(projectPath, ".git"));
4022
+ const isWorktree = await isWorktreeProject(projectPath);
4023
+ if (hasGitDir && !isWorktree) {
4024
+ status.info("");
4025
+ status.info("EP944: Migrating project to worktree mode...");
4026
+ status.info("This is a one-time operation that enables multi-module development.");
4027
+ status.info("");
4028
+ try {
4029
+ await migrateCommand({ cwd: projectPath, silent: false });
4030
+ status.success("Project migrated to worktree mode!");
4031
+ status.info("");
4032
+ } catch (error) {
4033
+ status.warning(`Migration skipped: ${error.message}`);
4034
+ status.info("You can run `episoda migrate` manually later.");
4035
+ status.info("");
4036
+ }
4037
+ }
2896
4038
  let daemonPid = isDaemonRunning();
2897
4039
  if (!daemonPid) {
2898
4040
  status.info("Starting Episoda daemon...");
@@ -2917,7 +4059,7 @@ async function devCommand(options = {}) {
2917
4059
  for (let retry = 0; retry < CONNECTION_MAX_RETRIES && !connected; retry++) {
2918
4060
  if (retry > 0) {
2919
4061
  status.info(`Retrying connection (attempt ${retry + 1}/${CONNECTION_MAX_RETRIES})...`);
2920
- await new Promise((resolve2) => setTimeout(resolve2, 1e3));
4062
+ await new Promise((resolve4) => setTimeout(resolve4, 1e3));
2921
4063
  }
2922
4064
  try {
2923
4065
  const result = await addProject(config.project_id, projectPath);
@@ -2985,7 +4127,7 @@ async function runDevServer(command, cwd, autoRestart) {
2985
4127
  let shuttingDown = false;
2986
4128
  const startServer = () => {
2987
4129
  status.info(`Starting dev server: ${command.join(" ")}`);
2988
- devProcess = (0, import_child_process2.spawn)(command[0], command.slice(1), {
4130
+ devProcess = (0, import_child_process3.spawn)(command[0], command.slice(1), {
2989
4131
  cwd,
2990
4132
  stdio: ["inherit", "inherit", "inherit"],
2991
4133
  shell: true
@@ -3038,33 +4180,33 @@ Received ${signal}, shutting down...`);
3038
4180
 
3039
4181
  // src/commands/auth.ts
3040
4182
  var os = __toESM(require("os"));
3041
- var fs4 = __toESM(require("fs"));
3042
- var path6 = __toESM(require("path"));
3043
- var import_child_process4 = require("child_process");
3044
- var import_core5 = __toESM(require_dist());
4183
+ var fs8 = __toESM(require("fs"));
4184
+ var path9 = __toESM(require("path"));
4185
+ var import_child_process5 = require("child_process");
4186
+ var import_core8 = __toESM(require_dist());
3045
4187
 
3046
4188
  // src/daemon/machine-id.ts
3047
- var fs3 = __toESM(require("fs"));
3048
- var path5 = __toESM(require("path"));
4189
+ var fs7 = __toESM(require("fs"));
4190
+ var path8 = __toESM(require("path"));
3049
4191
  var crypto2 = __toESM(require("crypto"));
3050
- var import_child_process3 = require("child_process");
3051
- var import_core4 = __toESM(require_dist());
4192
+ var import_child_process4 = require("child_process");
4193
+ var import_core7 = __toESM(require_dist());
3052
4194
  function isValidUUID(str) {
3053
4195
  const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
3054
4196
  return uuidRegex.test(str);
3055
4197
  }
3056
4198
  async function getMachineId() {
3057
- const machineIdPath = path5.join((0, import_core4.getConfigDir)(), "machine-id");
4199
+ const machineIdPath = path8.join((0, import_core7.getConfigDir)(), "machine-id");
3058
4200
  try {
3059
- if (fs3.existsSync(machineIdPath)) {
3060
- const existingId = fs3.readFileSync(machineIdPath, "utf-8").trim();
4201
+ if (fs7.existsSync(machineIdPath)) {
4202
+ const existingId = fs7.readFileSync(machineIdPath, "utf-8").trim();
3061
4203
  if (existingId) {
3062
4204
  if (isValidUUID(existingId)) {
3063
4205
  return existingId;
3064
4206
  }
3065
4207
  console.log("[MachineId] Migrating legacy machine ID to UUID format...");
3066
4208
  const newUUID = generateMachineId();
3067
- fs3.writeFileSync(machineIdPath, newUUID, "utf-8");
4209
+ fs7.writeFileSync(machineIdPath, newUUID, "utf-8");
3068
4210
  console.log(`[MachineId] Migrated: ${existingId} \u2192 ${newUUID}`);
3069
4211
  return newUUID;
3070
4212
  }
@@ -3073,11 +4215,11 @@ async function getMachineId() {
3073
4215
  }
3074
4216
  const machineId = generateMachineId();
3075
4217
  try {
3076
- const dir = path5.dirname(machineIdPath);
3077
- if (!fs3.existsSync(dir)) {
3078
- fs3.mkdirSync(dir, { recursive: true });
4218
+ const dir = path8.dirname(machineIdPath);
4219
+ if (!fs7.existsSync(dir)) {
4220
+ fs7.mkdirSync(dir, { recursive: true });
3079
4221
  }
3080
- fs3.writeFileSync(machineIdPath, machineId, "utf-8");
4222
+ fs7.writeFileSync(machineIdPath, machineId, "utf-8");
3081
4223
  } catch (error) {
3082
4224
  console.error("Warning: Could not save machine ID to disk:", error);
3083
4225
  }
@@ -3086,7 +4228,7 @@ async function getMachineId() {
3086
4228
  function getHardwareUUID() {
3087
4229
  try {
3088
4230
  if (process.platform === "darwin") {
3089
- const output = (0, import_child_process3.execSync)(
4231
+ const output = (0, import_child_process4.execSync)(
3090
4232
  `ioreg -d2 -c IOPlatformExpertDevice | awk -F\\" '/IOPlatformUUID/{print $(NF-1)}'`,
3091
4233
  { encoding: "utf-8", timeout: 5e3 }
3092
4234
  ).trim();
@@ -3094,20 +4236,20 @@ function getHardwareUUID() {
3094
4236
  return output;
3095
4237
  }
3096
4238
  } else if (process.platform === "linux") {
3097
- if (fs3.existsSync("/etc/machine-id")) {
3098
- const machineId = fs3.readFileSync("/etc/machine-id", "utf-8").trim();
4239
+ if (fs7.existsSync("/etc/machine-id")) {
4240
+ const machineId = fs7.readFileSync("/etc/machine-id", "utf-8").trim();
3099
4241
  if (machineId && machineId.length > 0) {
3100
4242
  return machineId;
3101
4243
  }
3102
4244
  }
3103
- if (fs3.existsSync("/var/lib/dbus/machine-id")) {
3104
- const dbusId = fs3.readFileSync("/var/lib/dbus/machine-id", "utf-8").trim();
4245
+ if (fs7.existsSync("/var/lib/dbus/machine-id")) {
4246
+ const dbusId = fs7.readFileSync("/var/lib/dbus/machine-id", "utf-8").trim();
3105
4247
  if (dbusId && dbusId.length > 0) {
3106
4248
  return dbusId;
3107
4249
  }
3108
4250
  }
3109
4251
  } else if (process.platform === "win32") {
3110
- const output = (0, import_child_process3.execSync)("wmic csproduct get uuid", {
4252
+ const output = (0, import_child_process4.execSync)("wmic csproduct get uuid", {
3111
4253
  encoding: "utf-8",
3112
4254
  timeout: 5e3
3113
4255
  });
@@ -3488,18 +4630,21 @@ async function authCommand(options = {}) {
3488
4630
  status.info(` Project: ${tokenResponse.project_uid}`);
3489
4631
  status.info(` Workspace: ${tokenResponse.workspace_uid}`);
3490
4632
  status.info("");
3491
- await (0, import_core5.saveConfig)({
4633
+ await (0, import_core8.saveConfig)({
3492
4634
  project_id: tokenResponse.project_id,
3493
4635
  user_id: tokenResponse.user_id,
3494
4636
  workspace_id: tokenResponse.workspace_id,
4637
+ workspace_slug: tokenResponse.workspace_uid,
4638
+ // EP956: For worktree paths
4639
+ project_slug: tokenResponse.project_uid,
4640
+ // EP956: For worktree paths
3495
4641
  access_token: tokenResponse.access_token,
3496
4642
  refresh_token: tokenResponse.refresh_token,
3497
4643
  expires_at: Date.now() + tokenResponse.expires_in * 1e3,
3498
4644
  // Convert to Unix timestamp
3499
- api_url: apiUrl,
3500
- cli_version: import_core5.VERSION
4645
+ api_url: apiUrl
3501
4646
  });
3502
- status.success(`\u2713 Configuration saved to ${(0, import_core5.getConfigPath)()}`);
4647
+ status.success(`\u2713 Configuration saved to ${(0, import_core8.getConfigPath)()}`);
3503
4648
  status.info("");
3504
4649
  status.info("Installing git credential helper...");
3505
4650
  const credentialHelperInstalled = await installGitCredentialHelper(apiUrl);
@@ -3537,13 +4682,13 @@ async function initiateDeviceFlow(apiUrl, machineId) {
3537
4682
  return await response.json();
3538
4683
  }
3539
4684
  async function monitorAuthorization(apiUrl, deviceCode, expiresIn) {
3540
- return new Promise((resolve2) => {
4685
+ return new Promise((resolve4) => {
3541
4686
  const timeout = setTimeout(() => {
3542
4687
  status.error("Authorization timed out");
3543
- resolve2(false);
4688
+ resolve4(false);
3544
4689
  }, expiresIn * 1e3);
3545
4690
  const url = `${apiUrl}/api/oauth/authorize-stream?device_code=${deviceCode}`;
3546
- const curlProcess = (0, import_child_process4.spawn)("curl", ["-N", url]);
4691
+ const curlProcess = (0, import_child_process5.spawn)("curl", ["-N", url]);
3547
4692
  let buffer = "";
3548
4693
  curlProcess.stdout.on("data", (chunk) => {
3549
4694
  buffer += chunk.toString();
@@ -3566,26 +4711,26 @@ async function monitorAuthorization(apiUrl, deviceCode, expiresIn) {
3566
4711
  if (eventType === "authorized") {
3567
4712
  clearTimeout(timeout);
3568
4713
  curlProcess.kill();
3569
- resolve2(true);
4714
+ resolve4(true);
3570
4715
  return;
3571
4716
  } else if (eventType === "denied") {
3572
4717
  clearTimeout(timeout);
3573
4718
  curlProcess.kill();
3574
4719
  status.error("Authorization denied by user");
3575
- resolve2(false);
4720
+ resolve4(false);
3576
4721
  return;
3577
4722
  } else if (eventType === "expired") {
3578
4723
  clearTimeout(timeout);
3579
4724
  curlProcess.kill();
3580
4725
  status.error("Authorization code expired");
3581
- resolve2(false);
4726
+ resolve4(false);
3582
4727
  return;
3583
4728
  } else if (eventType === "error") {
3584
4729
  const errorData = JSON.parse(data);
3585
4730
  clearTimeout(timeout);
3586
4731
  curlProcess.kill();
3587
4732
  status.error(`Authorization error: ${errorData.error_description || errorData.error || "Unknown error"}`);
3588
- resolve2(false);
4733
+ resolve4(false);
3589
4734
  return;
3590
4735
  }
3591
4736
  } catch (error) {
@@ -3595,13 +4740,13 @@ async function monitorAuthorization(apiUrl, deviceCode, expiresIn) {
3595
4740
  curlProcess.on("error", (error) => {
3596
4741
  clearTimeout(timeout);
3597
4742
  status.error(`Failed to monitor authorization: ${error.message}`);
3598
- resolve2(false);
4743
+ resolve4(false);
3599
4744
  });
3600
4745
  curlProcess.on("close", (code) => {
3601
4746
  clearTimeout(timeout);
3602
4747
  if (code !== 0 && code !== null) {
3603
4748
  status.error(`Authorization monitoring failed with code ${code}`);
3604
- resolve2(false);
4749
+ resolve4(false);
3605
4750
  }
3606
4751
  });
3607
4752
  });
@@ -3647,7 +4792,7 @@ function openBrowser(url) {
3647
4792
  break;
3648
4793
  }
3649
4794
  try {
3650
- (0, import_child_process4.spawn)(command, args, {
4795
+ (0, import_child_process5.spawn)(command, args, {
3651
4796
  detached: true,
3652
4797
  stdio: "ignore"
3653
4798
  }).unref();
@@ -3658,17 +4803,17 @@ function openBrowser(url) {
3658
4803
  async function installGitCredentialHelper(apiUrl) {
3659
4804
  try {
3660
4805
  const homeDir = os.homedir();
3661
- const episodaBinDir = path6.join(homeDir, ".episoda", "bin");
3662
- const helperPath = path6.join(episodaBinDir, "git-credential-episoda");
3663
- fs4.mkdirSync(episodaBinDir, { recursive: true });
4806
+ const episodaBinDir = path9.join(homeDir, ".episoda", "bin");
4807
+ const helperPath = path9.join(episodaBinDir, "git-credential-episoda");
4808
+ fs8.mkdirSync(episodaBinDir, { recursive: true });
3664
4809
  const scriptContent = generateCredentialHelperScript(apiUrl);
3665
- fs4.writeFileSync(helperPath, scriptContent, { mode: 493 });
4810
+ fs8.writeFileSync(helperPath, scriptContent, { mode: 493 });
3666
4811
  try {
3667
- fs4.accessSync(helperPath, fs4.constants.X_OK);
4812
+ fs8.accessSync(helperPath, fs8.constants.X_OK);
3668
4813
  } catch {
3669
4814
  }
3670
4815
  try {
3671
- const allHelpers = (0, import_child_process4.execSync)("git config --global --get-all credential.helper", {
4816
+ const allHelpers = (0, import_child_process5.execSync)("git config --global --get-all credential.helper", {
3672
4817
  encoding: "utf8",
3673
4818
  stdio: ["pipe", "pipe", "pipe"]
3674
4819
  }).trim().split("\n");
@@ -3677,7 +4822,7 @@ async function installGitCredentialHelper(apiUrl) {
3677
4822
  }
3678
4823
  } catch {
3679
4824
  }
3680
- (0, import_child_process4.execSync)(`git config --global --add credential.helper "${helperPath}"`, {
4825
+ (0, import_child_process5.execSync)(`git config --global --add credential.helper "${helperPath}"`, {
3681
4826
  encoding: "utf8",
3682
4827
  stdio: ["pipe", "pipe", "pipe"]
3683
4828
  });
@@ -3695,19 +4840,19 @@ function updateShellProfile(binDir) {
3695
4840
  }
3696
4841
  const homeDir = os.homedir();
3697
4842
  const profiles = [
3698
- path6.join(homeDir, ".bashrc"),
3699
- path6.join(homeDir, ".zshrc"),
3700
- path6.join(homeDir, ".profile")
4843
+ path9.join(homeDir, ".bashrc"),
4844
+ path9.join(homeDir, ".zshrc"),
4845
+ path9.join(homeDir, ".profile")
3701
4846
  ];
3702
4847
  const exportLine = `export PATH="${binDir}:$PATH" # Added by episoda auth`;
3703
4848
  for (const profile of profiles) {
3704
4849
  try {
3705
- if (fs4.existsSync(profile)) {
3706
- const content = fs4.readFileSync(profile, "utf8");
4850
+ if (fs8.existsSync(profile)) {
4851
+ const content = fs8.readFileSync(profile, "utf8");
3707
4852
  if (content.includes(".episoda/bin")) {
3708
4853
  continue;
3709
4854
  }
3710
- fs4.appendFileSync(profile, `
4855
+ fs8.appendFileSync(profile, `
3711
4856
  # Episoda CLI
3712
4857
  ${exportLine}
3713
4858
  `);
@@ -3719,10 +4864,10 @@ ${exportLine}
3719
4864
 
3720
4865
  // src/commands/connect.ts
3721
4866
  var os2 = __toESM(require("os"));
3722
- var fs5 = __toESM(require("fs"));
3723
- var path7 = __toESM(require("path"));
3724
- var import_child_process5 = require("child_process");
3725
- var import_core6 = __toESM(require_dist());
4867
+ var fs9 = __toESM(require("fs"));
4868
+ var path10 = __toESM(require("path"));
4869
+ var import_child_process6 = require("child_process");
4870
+ var import_core9 = __toESM(require_dist());
3726
4871
  async function connectCommand(options) {
3727
4872
  const { code } = options;
3728
4873
  const apiUrl = options.apiUrl || process.env.EPISODA_API_URL || "https://episoda.dev";
@@ -3738,15 +4883,14 @@ async function connectCommand(options) {
3738
4883
  status.info(` Project: ${tokenResponse.project_uid}`);
3739
4884
  status.info(` Workspace: ${tokenResponse.workspace_uid}`);
3740
4885
  status.info("");
3741
- await (0, import_core6.saveConfig)({
4886
+ await (0, import_core9.saveConfig)({
3742
4887
  project_id: tokenResponse.project_id,
3743
4888
  user_id: tokenResponse.user_id,
3744
4889
  workspace_id: tokenResponse.workspace_id,
3745
4890
  access_token: tokenResponse.access_token,
3746
- api_url: apiUrl,
3747
- cli_version: import_core6.VERSION
4891
+ api_url: apiUrl
3748
4892
  });
3749
- status.success(`\u2713 Configuration saved to ${(0, import_core6.getConfigPath)()}`);
4893
+ status.success(`\u2713 Configuration saved to ${(0, import_core9.getConfigPath)()}`);
3750
4894
  status.info("");
3751
4895
  status.info("Installing git credential helper...");
3752
4896
  const credentialHelperInstalled = await installGitCredentialHelper2(apiUrl);
@@ -3796,13 +4940,13 @@ async function exchangeUserCode(apiUrl, userCode, machineId) {
3796
4940
  async function installGitCredentialHelper2(apiUrl) {
3797
4941
  try {
3798
4942
  const homeDir = os2.homedir();
3799
- const episodaBinDir = path7.join(homeDir, ".episoda", "bin");
3800
- const helperPath = path7.join(episodaBinDir, "git-credential-episoda");
3801
- fs5.mkdirSync(episodaBinDir, { recursive: true });
4943
+ const episodaBinDir = path10.join(homeDir, ".episoda", "bin");
4944
+ const helperPath = path10.join(episodaBinDir, "git-credential-episoda");
4945
+ fs9.mkdirSync(episodaBinDir, { recursive: true });
3802
4946
  const scriptContent = generateCredentialHelperScript(apiUrl);
3803
- fs5.writeFileSync(helperPath, scriptContent, { mode: 493 });
4947
+ fs9.writeFileSync(helperPath, scriptContent, { mode: 493 });
3804
4948
  try {
3805
- const allHelpers = (0, import_child_process5.execSync)("git config --global --get-all credential.helper", {
4949
+ const allHelpers = (0, import_child_process6.execSync)("git config --global --get-all credential.helper", {
3806
4950
  encoding: "utf8",
3807
4951
  stdio: ["pipe", "pipe", "pipe"]
3808
4952
  }).trim().split("\n");
@@ -3811,7 +4955,7 @@ async function installGitCredentialHelper2(apiUrl) {
3811
4955
  }
3812
4956
  } catch {
3813
4957
  }
3814
- (0, import_child_process5.execSync)(`git config --global --add credential.helper "${helperPath}"`, {
4958
+ (0, import_child_process6.execSync)(`git config --global --add credential.helper "${helperPath}"`, {
3815
4959
  encoding: "utf8",
3816
4960
  stdio: ["pipe", "pipe", "pipe"]
3817
4961
  });
@@ -3829,19 +4973,19 @@ function updateShellProfile2(binDir) {
3829
4973
  }
3830
4974
  const homeDir = os2.homedir();
3831
4975
  const profiles = [
3832
- path7.join(homeDir, ".bashrc"),
3833
- path7.join(homeDir, ".zshrc"),
3834
- path7.join(homeDir, ".profile")
4976
+ path10.join(homeDir, ".bashrc"),
4977
+ path10.join(homeDir, ".zshrc"),
4978
+ path10.join(homeDir, ".profile")
3835
4979
  ];
3836
4980
  const exportLine = `export PATH="${binDir}:$PATH" # Added by episoda`;
3837
4981
  for (const profile of profiles) {
3838
4982
  try {
3839
- if (fs5.existsSync(profile)) {
3840
- const content = fs5.readFileSync(profile, "utf8");
4983
+ if (fs9.existsSync(profile)) {
4984
+ const content = fs9.readFileSync(profile, "utf8");
3841
4985
  if (content.includes(".episoda/bin")) {
3842
4986
  continue;
3843
4987
  }
3844
- fs5.appendFileSync(profile, `
4988
+ fs9.appendFileSync(profile, `
3845
4989
  # Episoda CLI
3846
4990
  ${exportLine}
3847
4991
  `);
@@ -3852,11 +4996,11 @@ ${exportLine}
3852
4996
  }
3853
4997
 
3854
4998
  // src/commands/status.ts
3855
- var import_core7 = __toESM(require_dist());
4999
+ var import_core10 = __toESM(require_dist());
3856
5000
  async function statusCommand(options = {}) {
3857
5001
  status.info("Checking CLI status...");
3858
5002
  status.info("");
3859
- const config = await (0, import_core7.loadConfig)();
5003
+ const config = await (0, import_core10.loadConfig)();
3860
5004
  if (!config) {
3861
5005
  status.error("\u2717 CLI not initialized");
3862
5006
  status.info("");
@@ -3871,8 +5015,8 @@ async function statusCommand(options = {}) {
3871
5015
  status.info("Configuration:");
3872
5016
  status.info(` Project ID: ${config.project_id}`);
3873
5017
  status.info(` API URL: ${config.api_url}`);
3874
- status.info(` CLI Version: ${config.cli_version}`);
3875
- status.info(` Config file: ${(0, import_core7.getConfigPath)()}`);
5018
+ status.info(` CLI Version: ${import_core10.VERSION}`);
5019
+ status.info(` Config file: ${(0, import_core10.getConfigPath)()}`);
3876
5020
  status.info("");
3877
5021
  if (!config.access_token || config.access_token === "") {
3878
5022
  status.warning("\u26A0 Not authenticated");
@@ -4031,8 +5175,588 @@ async function stopCommand(options = {}) {
4031
5175
  }
4032
5176
  }
4033
5177
 
5178
+ // src/commands/clone.ts
5179
+ var fs10 = __toESM(require("fs"));
5180
+ var path11 = __toESM(require("path"));
5181
+ var import_core11 = __toESM(require_dist());
5182
+ async function cloneCommand(slugArg, options = {}) {
5183
+ const slugParts = slugArg.split("/");
5184
+ if (slugParts.length !== 2 || !slugParts[0] || !slugParts[1]) {
5185
+ throw new Error(
5186
+ "Invalid format. Usage: episoda clone {workspace}/{project}\nExample: episoda clone my-team/my-project"
5187
+ );
5188
+ }
5189
+ const [workspaceSlug, projectSlug] = slugParts;
5190
+ status.info(`Cloning ${workspaceSlug}/${projectSlug}...`);
5191
+ status.info("");
5192
+ const config = await (0, import_core11.loadConfig)();
5193
+ if (!config || !config.access_token) {
5194
+ throw new Error(
5195
+ "Not authenticated. Please run `episoda auth` first."
5196
+ );
5197
+ }
5198
+ const apiUrl = options.apiUrl || config.api_url || "https://episoda.dev";
5199
+ const projectPath = getProjectPath(workspaceSlug, projectSlug);
5200
+ if (fs10.existsSync(projectPath)) {
5201
+ const bareRepoPath = path11.join(projectPath, ".bare");
5202
+ if (fs10.existsSync(bareRepoPath)) {
5203
+ status.warning(`Project already cloned at ${projectPath}`);
5204
+ status.info("");
5205
+ status.info("Next steps:");
5206
+ status.info(` \u2022 cd ${projectPath}`);
5207
+ status.info(" \u2022 episoda checkout {module}");
5208
+ return;
5209
+ }
5210
+ throw new Error(
5211
+ `Directory exists but is not a worktree project: ${projectPath}
5212
+ Please remove it manually or use a different location.`
5213
+ );
5214
+ }
5215
+ status.info("Fetching project details...");
5216
+ const projectDetails = await fetchProjectDetails(
5217
+ apiUrl,
5218
+ workspaceSlug,
5219
+ projectSlug,
5220
+ config.access_token
5221
+ );
5222
+ if (!projectDetails.repo_url) {
5223
+ throw new Error(
5224
+ `Project "${projectSlug}" has no repository configured.
5225
+ Please configure a repository in the project settings on episoda.dev.`
5226
+ );
5227
+ }
5228
+ validateRepoUrl(projectDetails.repo_url);
5229
+ status.success(`\u2713 Found project: ${projectDetails.name}`);
5230
+ status.info(` Repository: ${projectDetails.repo_url}`);
5231
+ status.info("");
5232
+ status.info("Creating project directory...");
5233
+ const episodaRoot = getEpisodaRoot();
5234
+ fs10.mkdirSync(projectPath, { recursive: true });
5235
+ status.success(`\u2713 Created ${projectPath}`);
5236
+ status.info("Cloning repository (bare)...");
5237
+ try {
5238
+ const worktreeManager = await WorktreeManager.createProject(
5239
+ projectPath,
5240
+ projectDetails.repo_url,
5241
+ projectDetails.id,
5242
+ workspaceSlug,
5243
+ projectSlug
5244
+ );
5245
+ status.success("\u2713 Repository cloned");
5246
+ status.info("");
5247
+ const bareRepoPath = worktreeManager.getBareRepoPath();
5248
+ addProject2(projectDetails.id, projectPath, {
5249
+ worktreeMode: true,
5250
+ bareRepoPath
5251
+ });
5252
+ status.success("\u2713 Project registered with daemon");
5253
+ status.info("");
5254
+ status.success("Project cloned successfully!");
5255
+ status.info("");
5256
+ status.info("Project structure:");
5257
+ status.info(` ${projectPath}/`);
5258
+ status.info(" \u251C\u2500\u2500 .bare/ # Git repository");
5259
+ status.info(" \u2514\u2500\u2500 .episoda/");
5260
+ status.info(" \u2514\u2500\u2500 config.json # Project config");
5261
+ status.info("");
5262
+ status.info("Next steps:");
5263
+ status.info(` 1. cd ${projectPath}`);
5264
+ status.info(" 2. episoda checkout {moduleUid} # e.g., episoda checkout EP100");
5265
+ status.info("");
5266
+ } catch (error) {
5267
+ if (fs10.existsSync(projectPath)) {
5268
+ try {
5269
+ fs10.rmSync(projectPath, { recursive: true, force: true });
5270
+ } catch {
5271
+ }
5272
+ }
5273
+ throw error;
5274
+ }
5275
+ }
5276
+ function validateRepoUrl(url) {
5277
+ const dangerousChars = /[;|&$`\\<>(){}[\]!#*?~'"]/;
5278
+ if (dangerousChars.test(url)) {
5279
+ throw new Error(
5280
+ "Repository URL contains invalid characters.\nPlease check the repository configuration on episoda.dev."
5281
+ );
5282
+ }
5283
+ let parsed;
5284
+ try {
5285
+ parsed = new URL(url);
5286
+ } catch {
5287
+ if (/^[\w.-]+@[\w.-]+:[\w./-]+$/.test(url)) {
5288
+ return;
5289
+ }
5290
+ throw new Error(
5291
+ `Repository URL is not a valid URL format.
5292
+ Received: ${url}`
5293
+ );
5294
+ }
5295
+ const allowedProtocols = ["https:", "http:", "git:", "ssh:"];
5296
+ if (!allowedProtocols.includes(parsed.protocol)) {
5297
+ throw new Error(
5298
+ `Repository URL uses unsupported protocol: ${parsed.protocol}
5299
+ Allowed protocols: https, http, git, ssh`
5300
+ );
5301
+ }
5302
+ if (!parsed.hostname || parsed.hostname.length < 3) {
5303
+ throw new Error(
5304
+ `Repository URL has invalid hostname.
5305
+ Received: ${url}`
5306
+ );
5307
+ }
5308
+ }
5309
+ async function fetchProjectDetails(apiUrl, workspaceSlug, projectSlug, accessToken) {
5310
+ const url = `${apiUrl}/api/projects/by-slug/${workspaceSlug}/${projectSlug}`;
5311
+ const response = await fetch(url, {
5312
+ method: "GET",
5313
+ headers: {
5314
+ "Authorization": `Bearer ${accessToken}`,
5315
+ "Content-Type": "application/json"
5316
+ }
5317
+ });
5318
+ if (response.status === 404) {
5319
+ throw new Error(
5320
+ `Project "${workspaceSlug}/${projectSlug}" not found.
5321
+ Check the workspace and project slugs are correct.`
5322
+ );
5323
+ }
5324
+ if (response.status === 401) {
5325
+ throw new Error(
5326
+ "Authentication expired. Please run `episoda auth` again."
5327
+ );
5328
+ }
5329
+ if (!response.ok) {
5330
+ const errorBody = await response.text();
5331
+ throw new Error(
5332
+ `Failed to fetch project details: ${response.status}
5333
+ ${errorBody}`
5334
+ );
5335
+ }
5336
+ const data = await response.json();
5337
+ if (!data.id || !data.slug) {
5338
+ throw new Error("Invalid project response from server");
5339
+ }
5340
+ return data;
5341
+ }
5342
+
5343
+ // src/commands/checkout.ts
5344
+ var import_core12 = __toESM(require_dist());
5345
+
5346
+ // src/utils/http.ts
5347
+ async function fetchWithRetry(url, options, maxRetries = 3) {
5348
+ let lastError = null;
5349
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
5350
+ try {
5351
+ const response = await fetch(url, options);
5352
+ if (response.status === 401 || response.status === 404) {
5353
+ return response;
5354
+ }
5355
+ if (response.ok || response.status >= 400 && response.status < 500) {
5356
+ return response;
5357
+ }
5358
+ lastError = new Error(`Server error: ${response.status}`);
5359
+ } catch (error) {
5360
+ lastError = error instanceof Error ? error : new Error("Network error");
5361
+ }
5362
+ if (attempt < maxRetries - 1) {
5363
+ await new Promise((resolve4) => setTimeout(resolve4, Math.pow(2, attempt) * 1e3));
5364
+ }
5365
+ }
5366
+ throw lastError || new Error("Request failed after retries");
5367
+ }
5368
+
5369
+ // src/commands/checkout.ts
5370
+ async function checkoutCommand(moduleUid, options = {}) {
5371
+ if (!moduleUid || !moduleUid.match(/^EP\d+$/)) {
5372
+ throw new Error(
5373
+ "Invalid module UID format. Expected format: EP###\nExample: episoda checkout EP100"
5374
+ );
5375
+ }
5376
+ status.info(`Checking out ${moduleUid}...`);
5377
+ status.info("");
5378
+ const projectRoot = await findProjectRoot(process.cwd());
5379
+ if (!projectRoot) {
5380
+ throw new Error(
5381
+ "Not in a worktree project.\nRun this command from within a project cloned with `episoda clone`.\nOr cd to ~/episoda/{workspace}/{project}/"
5382
+ );
5383
+ }
5384
+ const config = await (0, import_core12.loadConfig)();
5385
+ if (!config || !config.access_token) {
5386
+ throw new Error(
5387
+ "Not authenticated. Please run `episoda auth` first."
5388
+ );
5389
+ }
5390
+ const apiUrl = options.apiUrl || config.api_url || "https://episoda.dev";
5391
+ const worktreeManager = new WorktreeManager(projectRoot);
5392
+ const initialized = await worktreeManager.initialize();
5393
+ if (!initialized) {
5394
+ throw new Error(
5395
+ `Invalid worktree project at ${projectRoot}.
5396
+ The .bare directory or .episoda/config.json may be missing.`
5397
+ );
5398
+ }
5399
+ const existing = worktreeManager.getWorktreeByModuleUid(moduleUid);
5400
+ if (existing) {
5401
+ status.success(`Module ${moduleUid} is already checked out.`);
5402
+ status.info("");
5403
+ status.info(`Path: ${existing.worktreePath}`);
5404
+ status.info(`Branch: ${existing.branchName}`);
5405
+ status.info("");
5406
+ status.info(`cd ${existing.worktreePath}`);
5407
+ return;
5408
+ }
5409
+ status.info("Fetching module details...");
5410
+ const worktreeConfig = worktreeManager.getConfig();
5411
+ if (!worktreeConfig) {
5412
+ throw new Error("Could not read worktree project config");
5413
+ }
5414
+ const moduleDetails = await fetchModuleDetails(
5415
+ apiUrl,
5416
+ worktreeConfig.projectId,
5417
+ moduleUid,
5418
+ config.access_token
5419
+ );
5420
+ const branchName = moduleDetails.branch_name || `feature/${moduleUid.toLowerCase()}`;
5421
+ let createBranch = !moduleDetails.branch_name || options.create;
5422
+ if (createBranch && !moduleDetails.branch_name) {
5423
+ const { GitExecutor: GitExecutor6 } = await Promise.resolve().then(() => __toESM(require_dist()));
5424
+ const gitExecutor = new GitExecutor6();
5425
+ const branchCheck = await gitExecutor.execute(
5426
+ { action: "branch_exists", branch: branchName },
5427
+ { cwd: worktreeManager.getBareRepoPath() }
5428
+ );
5429
+ if (branchCheck.success && branchCheck.details?.branchExists) {
5430
+ status.info(`Branch ${branchName} already exists, will use existing`);
5431
+ createBranch = false;
5432
+ }
5433
+ }
5434
+ status.success(`\u2713 Found module: ${moduleDetails.name || moduleUid}`);
5435
+ status.info(` Branch: ${branchName}${createBranch ? " (will be created)" : ""}`);
5436
+ status.info("");
5437
+ status.info("Creating worktree...");
5438
+ const result = await worktreeManager.createWorktree(
5439
+ moduleUid,
5440
+ branchName,
5441
+ createBranch
5442
+ );
5443
+ if (!result.success) {
5444
+ throw new Error(`Failed to create worktree: ${result.error}`);
5445
+ }
5446
+ status.success("\u2713 Worktree created");
5447
+ status.info("");
5448
+ const branchWasGenerated = !moduleDetails.branch_name;
5449
+ try {
5450
+ await updateModuleCheckout(
5451
+ apiUrl,
5452
+ moduleDetails.id,
5453
+ config.access_token,
5454
+ branchWasGenerated ? branchName : void 0
5455
+ );
5456
+ } catch (error) {
5457
+ status.warning("Could not update server with checkout status");
5458
+ }
5459
+ status.success(`Module ${moduleUid} checked out successfully!`);
5460
+ status.info("");
5461
+ status.info(`Path: ${result.worktreePath}`);
5462
+ status.info(`Branch: ${branchName}`);
5463
+ status.info("");
5464
+ status.info("Next step:");
5465
+ status.info(` cd ${result.worktreePath}`);
5466
+ status.info("");
5467
+ }
5468
+ async function fetchModuleDetails(apiUrl, projectId, moduleUid, accessToken) {
5469
+ const url = `${apiUrl}/api/modules/by-uid/${moduleUid}?project_id=${projectId}`;
5470
+ const response = await fetchWithRetry(url, {
5471
+ method: "GET",
5472
+ headers: {
5473
+ "Authorization": `Bearer ${accessToken}`,
5474
+ "Content-Type": "application/json"
5475
+ }
5476
+ });
5477
+ if (response.status === 404) {
5478
+ throw new Error(
5479
+ `Module "${moduleUid}" not found in this project.
5480
+ Check the module UID is correct.`
5481
+ );
5482
+ }
5483
+ if (response.status === 401) {
5484
+ throw new Error(
5485
+ "Authentication expired. Please run `episoda auth` again."
5486
+ );
5487
+ }
5488
+ if (!response.ok) {
5489
+ const errorBody = await response.text();
5490
+ throw new Error(
5491
+ `Failed to fetch module details: ${response.status}
5492
+ ${errorBody}`
5493
+ );
5494
+ }
5495
+ const data = await response.json();
5496
+ if (!data.id || !data.uid) {
5497
+ throw new Error("Invalid module response from server");
5498
+ }
5499
+ return data;
5500
+ }
5501
+ async function updateModuleCheckout(apiUrl, moduleId, accessToken, branchName) {
5502
+ const url = `${apiUrl}/api/modules/${moduleId}/checkout`;
5503
+ const body = {
5504
+ checked_out: true,
5505
+ checkout_type: "worktree"
5506
+ };
5507
+ if (branchName) {
5508
+ body.branch_name = branchName;
5509
+ }
5510
+ await fetchWithRetry(url, {
5511
+ method: "POST",
5512
+ headers: {
5513
+ "Authorization": `Bearer ${accessToken}`,
5514
+ "Content-Type": "application/json"
5515
+ },
5516
+ body: JSON.stringify(body)
5517
+ });
5518
+ }
5519
+
5520
+ // src/commands/release.ts
5521
+ var path12 = __toESM(require("path"));
5522
+ var import_core13 = __toESM(require_dist());
5523
+ async function releaseCommand(moduleUid, options = {}) {
5524
+ if (!moduleUid || !moduleUid.match(/^EP\d+$/)) {
5525
+ throw new Error(
5526
+ "Invalid module UID format. Expected format: EP###\nExample: episoda release EP100"
5527
+ );
5528
+ }
5529
+ status.info(`Releasing ${moduleUid}...`);
5530
+ status.info("");
5531
+ const projectRoot = await findProjectRoot(process.cwd());
5532
+ if (!projectRoot) {
5533
+ throw new Error(
5534
+ "Not in a worktree project.\nRun this command from within a project cloned with `episoda clone`."
5535
+ );
5536
+ }
5537
+ const worktreeManager = new WorktreeManager(projectRoot);
5538
+ const initialized = await worktreeManager.initialize();
5539
+ if (!initialized) {
5540
+ throw new Error(
5541
+ `Invalid worktree project at ${projectRoot}.`
5542
+ );
5543
+ }
5544
+ const existing = worktreeManager.getWorktreeByModuleUid(moduleUid);
5545
+ if (!existing) {
5546
+ status.warning(`Module ${moduleUid} is not checked out.`);
5547
+ status.info("");
5548
+ status.info("Available worktrees:");
5549
+ const worktrees = worktreeManager.listWorktrees();
5550
+ if (worktrees.length === 0) {
5551
+ status.info(" (none)");
5552
+ } else {
5553
+ for (const wt of worktrees) {
5554
+ status.info(` ${wt.moduleUid} \u2192 ${wt.branchName}`);
5555
+ }
5556
+ }
5557
+ return;
5558
+ }
5559
+ if (!options.force) {
5560
+ const hasChanges = await checkUncommittedChanges(existing.worktreePath);
5561
+ if (hasChanges) {
5562
+ throw new Error(
5563
+ `Module ${moduleUid} has uncommitted changes.
5564
+ Commit or stash your changes first, or use --force to discard them.`
5565
+ );
5566
+ }
5567
+ }
5568
+ const currentPath = path12.resolve(process.cwd());
5569
+ if (currentPath.startsWith(existing.worktreePath)) {
5570
+ status.warning("You are inside the worktree being released.");
5571
+ status.info(`Please cd to ${projectRoot} first.`);
5572
+ throw new Error("Cannot release worktree while inside it");
5573
+ }
5574
+ status.info("Removing worktree...");
5575
+ const result = await worktreeManager.removeWorktree(moduleUid, options.force);
5576
+ if (!result.success) {
5577
+ throw new Error(`Failed to remove worktree: ${result.error}`);
5578
+ }
5579
+ status.success("\u2713 Worktree removed");
5580
+ status.info("");
5581
+ try {
5582
+ const config = await (0, import_core13.loadConfig)();
5583
+ if (config?.access_token) {
5584
+ const worktreeConfig = worktreeManager.getConfig();
5585
+ if (worktreeConfig) {
5586
+ await updateModuleRelease(
5587
+ config.api_url || "https://episoda.dev",
5588
+ worktreeConfig.projectId,
5589
+ worktreeConfig.workspaceSlug,
5590
+ moduleUid,
5591
+ config.access_token
5592
+ );
5593
+ }
5594
+ }
5595
+ } catch (error) {
5596
+ }
5597
+ status.success(`Module ${moduleUid} released successfully!`);
5598
+ status.info("");
5599
+ status.info(`Branch "${existing.branchName}" is preserved in the repository.`);
5600
+ status.info("");
5601
+ status.info("To check out again:");
5602
+ status.info(` episoda checkout ${moduleUid}`);
5603
+ status.info("");
5604
+ }
5605
+ async function checkUncommittedChanges(worktreePath) {
5606
+ const gitExecutor = new import_core13.GitExecutor();
5607
+ const result = await gitExecutor.execute(
5608
+ { action: "status" },
5609
+ { cwd: worktreePath }
5610
+ );
5611
+ if (!result.success) {
5612
+ return true;
5613
+ }
5614
+ const uncommitted = result.details?.uncommittedFiles || [];
5615
+ return uncommitted.length > 0;
5616
+ }
5617
+ async function updateModuleRelease(apiUrl, projectId, workspaceSlug, moduleUid, accessToken) {
5618
+ const url = `${apiUrl}/api/modules/${moduleUid}/checkout/release`;
5619
+ await fetchWithRetry(url, {
5620
+ method: "POST",
5621
+ headers: {
5622
+ "Authorization": `Bearer ${accessToken}`,
5623
+ "Content-Type": "application/json",
5624
+ "X-Project-Id": projectId,
5625
+ "X-Workspace-Slug": workspaceSlug
5626
+ }
5627
+ });
5628
+ }
5629
+
5630
+ // src/commands/list.ts
5631
+ var fs11 = __toESM(require("fs"));
5632
+ var path13 = __toESM(require("path"));
5633
+ var import_chalk2 = __toESM(require("chalk"));
5634
+ var import_core14 = __toESM(require_dist());
5635
+ async function listCommand(subcommand, options = {}) {
5636
+ if (subcommand === "worktrees" || subcommand === "wt") {
5637
+ await listWorktrees(options);
5638
+ } else {
5639
+ await listProjects(options);
5640
+ }
5641
+ }
5642
+ async function listProjects(options) {
5643
+ const episodaRoot = getEpisodaRoot();
5644
+ if (!fs11.existsSync(episodaRoot)) {
5645
+ status.info("No projects cloned yet.");
5646
+ status.info("");
5647
+ status.info("Clone a project with:");
5648
+ status.info(" episoda clone {workspace}/{project}");
5649
+ return;
5650
+ }
5651
+ const projects = [];
5652
+ const workspaces = fs11.readdirSync(episodaRoot, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith("."));
5653
+ for (const workspace of workspaces) {
5654
+ const workspacePath = path13.join(episodaRoot, workspace.name);
5655
+ const projectDirs = fs11.readdirSync(workspacePath, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith("."));
5656
+ for (const projectDir of projectDirs) {
5657
+ const projectPath = path13.join(workspacePath, projectDir.name);
5658
+ if (await isWorktreeProject(projectPath)) {
5659
+ const manager = new WorktreeManager(projectPath);
5660
+ await manager.initialize();
5661
+ const worktrees = manager.listWorktrees();
5662
+ projects.push({
5663
+ workspaceSlug: workspace.name,
5664
+ projectSlug: projectDir.name,
5665
+ projectPath,
5666
+ worktreeCount: worktrees.length
5667
+ });
5668
+ }
5669
+ }
5670
+ }
5671
+ if (projects.length === 0) {
5672
+ status.info("No worktree projects found.");
5673
+ status.info("");
5674
+ status.info("Clone a project with:");
5675
+ status.info(" episoda clone {workspace}/{project}");
5676
+ return;
5677
+ }
5678
+ console.log("");
5679
+ console.log(import_chalk2.default.bold("Cloned Projects"));
5680
+ console.log("\u2500".repeat(60));
5681
+ for (const project of projects) {
5682
+ const worktreeLabel = project.worktreeCount === 0 ? import_chalk2.default.gray("(no worktrees)") : project.worktreeCount === 1 ? import_chalk2.default.cyan("1 worktree") : import_chalk2.default.cyan(`${project.worktreeCount} worktrees`);
5683
+ console.log("");
5684
+ console.log(` ${import_chalk2.default.bold(project.workspaceSlug)}/${import_chalk2.default.bold(project.projectSlug)} ${worktreeLabel}`);
5685
+ console.log(import_chalk2.default.gray(` ${project.projectPath}`));
5686
+ }
5687
+ console.log("");
5688
+ console.log(import_chalk2.default.gray("\u2500".repeat(60)));
5689
+ console.log(`${projects.length} project${projects.length === 1 ? "" : "s"}`);
5690
+ console.log("");
5691
+ if (options.verbose) {
5692
+ status.info("Commands:");
5693
+ status.info(" episoda list worktrees # Show worktrees for current project");
5694
+ status.info(" episoda checkout EP### # Create a new worktree");
5695
+ }
5696
+ }
5697
+ async function listWorktrees(options) {
5698
+ const projectRoot = await findProjectRoot(process.cwd());
5699
+ if (!projectRoot) {
5700
+ throw new Error(
5701
+ "Not in a worktree project.\nRun this from within a project cloned with `episoda clone`."
5702
+ );
5703
+ }
5704
+ const manager = new WorktreeManager(projectRoot);
5705
+ const initialized = await manager.initialize();
5706
+ if (!initialized) {
5707
+ throw new Error(`Invalid worktree project at ${projectRoot}`);
5708
+ }
5709
+ const config = manager.getConfig();
5710
+ const worktrees = manager.listWorktrees();
5711
+ console.log("");
5712
+ console.log(import_chalk2.default.bold(`Worktrees for ${config?.workspaceSlug}/${config?.projectSlug}`));
5713
+ console.log("\u2500".repeat(60));
5714
+ if (worktrees.length === 0) {
5715
+ console.log("");
5716
+ console.log(import_chalk2.default.gray(" No worktrees checked out"));
5717
+ console.log("");
5718
+ status.info("Create a worktree with:");
5719
+ status.info(" episoda checkout {moduleUid}");
5720
+ return;
5721
+ }
5722
+ const gitExecutor = new import_core14.GitExecutor();
5723
+ for (const wt of worktrees) {
5724
+ console.log("");
5725
+ let statusIndicator = import_chalk2.default.green("\u25CF");
5726
+ let statusNote = "";
5727
+ try {
5728
+ const result = await gitExecutor.execute(
5729
+ { action: "status" },
5730
+ { cwd: wt.worktreePath }
5731
+ );
5732
+ if (result.success) {
5733
+ const uncommitted = result.details?.uncommittedFiles || [];
5734
+ if (uncommitted.length > 0) {
5735
+ statusIndicator = import_chalk2.default.yellow("\u25CF");
5736
+ statusNote = import_chalk2.default.yellow(` (${uncommitted.length} uncommitted)`);
5737
+ }
5738
+ }
5739
+ } catch {
5740
+ statusIndicator = import_chalk2.default.gray("\u25CB");
5741
+ }
5742
+ console.log(` ${statusIndicator} ${import_chalk2.default.bold(wt.moduleUid)}${statusNote}`);
5743
+ console.log(import_chalk2.default.gray(` Branch: ${wt.branchName}`));
5744
+ console.log(import_chalk2.default.gray(` Path: ${wt.worktreePath}`));
5745
+ if (options.verbose) {
5746
+ console.log(import_chalk2.default.gray(` Created: ${new Date(wt.createdAt).toLocaleDateString()}`));
5747
+ console.log(import_chalk2.default.gray(` Last accessed: ${new Date(wt.lastAccessed).toLocaleDateString()}`));
5748
+ }
5749
+ }
5750
+ console.log("");
5751
+ console.log(import_chalk2.default.gray("\u2500".repeat(60)));
5752
+ console.log(`${worktrees.length} worktree${worktrees.length === 1 ? "" : "s"}`);
5753
+ console.log("");
5754
+ console.log(import_chalk2.default.gray("\u25CF clean ") + import_chalk2.default.yellow("\u25CF uncommitted changes"));
5755
+ console.log("");
5756
+ }
5757
+
4034
5758
  // src/index.ts
4035
- import_commander.program.name("episoda").description("Episoda local development workflow orchestration").version(import_core8.VERSION);
5759
+ import_commander.program.name("episoda").description("Episoda local development workflow orchestration").version(import_core15.VERSION);
4036
5760
  import_commander.program.command("auth").description("Authenticate to Episoda via OAuth and configure CLI").option("--api-url <url>", "API URL (default: https://episoda.dev)").action(async (options) => {
4037
5761
  try {
4038
5762
  await authCommand(options);
@@ -4088,6 +5812,46 @@ import_commander.program.command("disconnect").description("Disconnect from epis
4088
5812
  process.exit(1);
4089
5813
  }
4090
5814
  });
5815
+ import_commander.program.command("clone").description("Clone a project in worktree mode for multi-module development").argument("<workspace/project>", "Workspace and project slug (e.g., my-team/my-project)").option("--api-url <url>", "API URL (default: https://episoda.dev)").action(async (slug, options) => {
5816
+ try {
5817
+ await cloneCommand(slug, options);
5818
+ } catch (error) {
5819
+ status.error(`Clone failed: ${error instanceof Error ? error.message : String(error)}`);
5820
+ process.exit(1);
5821
+ }
5822
+ });
5823
+ import_commander.program.command("checkout").description("Create a worktree for a module").argument("<moduleUid>", "Module UID (e.g., EP100)").option("--api-url <url>", "API URL (default: https://episoda.dev)").option("--create", "Create branch if it doesn't exist").action(async (moduleUid, options) => {
5824
+ try {
5825
+ await checkoutCommand(moduleUid, options);
5826
+ } catch (error) {
5827
+ status.error(`Checkout failed: ${error instanceof Error ? error.message : String(error)}`);
5828
+ process.exit(1);
5829
+ }
5830
+ });
5831
+ import_commander.program.command("release").description("Remove a worktree for a module (preserves branch)").argument("<moduleUid>", "Module UID (e.g., EP100)").option("--force", "Force release even with uncommitted changes").option("--api-url <url>", "API URL (default: https://episoda.dev)").action(async (moduleUid, options) => {
5832
+ try {
5833
+ await releaseCommand(moduleUid, options);
5834
+ } catch (error) {
5835
+ status.error(`Release failed: ${error instanceof Error ? error.message : String(error)}`);
5836
+ process.exit(1);
5837
+ }
5838
+ });
5839
+ import_commander.program.command("list").description("List cloned projects and worktrees").argument("[subcommand]", 'Subcommand: "worktrees" or "wt" to list worktrees').option("--verbose", "Show detailed information").action(async (subcommand, options) => {
5840
+ try {
5841
+ await listCommand(subcommand, options);
5842
+ } catch (error) {
5843
+ status.error(`List failed: ${error instanceof Error ? error.message : String(error)}`);
5844
+ process.exit(1);
5845
+ }
5846
+ });
5847
+ import_commander.program.command("migrate").description("Convert existing project to worktree mode").option("--force", "Force migration even with uncommitted changes (auto-stash)").option("--dry-run", "Show what would be done without making changes").action(async (options) => {
5848
+ try {
5849
+ await migrateCommand(options);
5850
+ } catch (error) {
5851
+ status.error(`Migration failed: ${error instanceof Error ? error.message : String(error)}`);
5852
+ process.exit(1);
5853
+ }
5854
+ });
4091
5855
  import_commander.program.command("dev:restart").description("Restart the dev server for a module").argument("[moduleUid]", "Module UID (required)").action(async (moduleUid) => {
4092
5856
  try {
4093
5857
  if (!moduleUid) {