episoda 0.2.21 → 0.2.23

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 GitExecutor6 = class {
298
+ var GitExecutor4 = class {
299
299
  /**
300
300
  * Execute a git command
301
301
  * @param command - The git command to execute
@@ -530,6 +530,24 @@ var require_git_executor = __commonJS({
530
530
  * Execute status command
531
531
  */
532
532
  async executeStatus(cwd, options) {
533
+ try {
534
+ const isBareResult = await execAsync("git rev-parse --is-bare-repository", { cwd, timeout: 5e3 });
535
+ if (isBareResult.stdout.trim() === "true") {
536
+ const headResult = await execAsync("git symbolic-ref --short HEAD", { cwd, timeout: 5e3 });
537
+ const branchName = headResult.stdout.trim();
538
+ return {
539
+ success: true,
540
+ output: `## ${branchName}`,
541
+ details: {
542
+ uncommittedFiles: [],
543
+ // No working tree in bare repo
544
+ branchName,
545
+ currentBranch: branchName
546
+ }
547
+ };
548
+ }
549
+ } catch {
550
+ }
533
551
  const result = await this.runGitCommand(["status", "--porcelain", "-b"], cwd, options);
534
552
  if (result.success && result.output) {
535
553
  const statusInfo = (0, git_parser_1.parseGitStatus)(result.output);
@@ -1531,15 +1549,15 @@ var require_git_executor = __commonJS({
1531
1549
  try {
1532
1550
  const { stdout: gitDir } = await execAsync("git rev-parse --git-dir", { cwd, timeout: 5e3 });
1533
1551
  const gitDirPath = gitDir.trim();
1534
- const fs12 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1552
+ const fs11 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1535
1553
  const rebaseMergePath = `${gitDirPath}/rebase-merge`;
1536
1554
  const rebaseApplyPath = `${gitDirPath}/rebase-apply`;
1537
1555
  try {
1538
- await fs12.access(rebaseMergePath);
1556
+ await fs11.access(rebaseMergePath);
1539
1557
  inRebase = true;
1540
1558
  } catch {
1541
1559
  try {
1542
- await fs12.access(rebaseApplyPath);
1560
+ await fs11.access(rebaseApplyPath);
1543
1561
  inRebase = true;
1544
1562
  } catch {
1545
1563
  inRebase = false;
@@ -1593,9 +1611,9 @@ var require_git_executor = __commonJS({
1593
1611
  error: validation.error || "UNKNOWN_ERROR"
1594
1612
  };
1595
1613
  }
1596
- const fs12 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1614
+ const fs11 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1597
1615
  try {
1598
- await fs12.access(command.path);
1616
+ await fs11.access(command.path);
1599
1617
  return {
1600
1618
  success: false,
1601
1619
  error: "WORKTREE_EXISTS",
@@ -1646,9 +1664,9 @@ var require_git_executor = __commonJS({
1646
1664
  */
1647
1665
  async executeWorktreeRemove(command, cwd, options) {
1648
1666
  try {
1649
- const fs12 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1667
+ const fs11 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1650
1668
  try {
1651
- await fs12.access(command.path);
1669
+ await fs11.access(command.path);
1652
1670
  } catch {
1653
1671
  return {
1654
1672
  success: false,
@@ -1801,10 +1819,10 @@ var require_git_executor = __commonJS({
1801
1819
  */
1802
1820
  async executeCloneBare(command, options) {
1803
1821
  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")));
1822
+ const fs11 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1823
+ const path13 = await Promise.resolve().then(() => __importStar(require("path")));
1806
1824
  try {
1807
- await fs12.access(command.path);
1825
+ await fs11.access(command.path);
1808
1826
  return {
1809
1827
  success: false,
1810
1828
  error: "BRANCH_ALREADY_EXISTS",
@@ -1813,9 +1831,9 @@ var require_git_executor = __commonJS({
1813
1831
  };
1814
1832
  } catch {
1815
1833
  }
1816
- const parentDir = path14.dirname(command.path);
1834
+ const parentDir = path13.dirname(command.path);
1817
1835
  try {
1818
- await fs12.mkdir(parentDir, { recursive: true });
1836
+ await fs11.mkdir(parentDir, { recursive: true });
1819
1837
  } catch {
1820
1838
  }
1821
1839
  const { stdout, stderr } = await execAsync(
@@ -1858,24 +1876,22 @@ var require_git_executor = __commonJS({
1858
1876
  */
1859
1877
  async executeProjectInfo(cwd, options) {
1860
1878
  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")));
1879
+ const fs11 = await Promise.resolve().then(() => __importStar(require("fs"))).then((m) => m.promises);
1880
+ const path13 = await Promise.resolve().then(() => __importStar(require("path")));
1863
1881
  let currentPath = cwd;
1864
- let worktreeMode = false;
1865
1882
  let projectPath = cwd;
1866
1883
  let bareRepoPath;
1867
1884
  for (let i = 0; i < 10; i++) {
1868
- const bareDir = path14.join(currentPath, ".bare");
1869
- const episodaDir = path14.join(currentPath, ".episoda");
1885
+ const bareDir = path13.join(currentPath, ".bare");
1886
+ const episodaDir = path13.join(currentPath, ".episoda");
1870
1887
  try {
1871
- await fs12.access(bareDir);
1872
- await fs12.access(episodaDir);
1873
- worktreeMode = true;
1888
+ await fs11.access(bareDir);
1889
+ await fs11.access(episodaDir);
1874
1890
  projectPath = currentPath;
1875
1891
  bareRepoPath = bareDir;
1876
1892
  break;
1877
1893
  } catch {
1878
- const parentPath = path14.dirname(currentPath);
1894
+ const parentPath = path13.dirname(currentPath);
1879
1895
  if (parentPath === currentPath) {
1880
1896
  break;
1881
1897
  }
@@ -1884,9 +1900,8 @@ var require_git_executor = __commonJS({
1884
1900
  }
1885
1901
  return {
1886
1902
  success: true,
1887
- output: worktreeMode ? "Worktree mode project" : "Standard git project",
1903
+ output: bareRepoPath ? "Episoda project" : "Git repository",
1888
1904
  details: {
1889
- worktreeMode,
1890
1905
  projectPath,
1891
1906
  bareRepoPath
1892
1907
  }
@@ -1998,7 +2013,7 @@ var require_git_executor = __commonJS({
1998
2013
  }
1999
2014
  }
2000
2015
  };
2001
- exports2.GitExecutor = GitExecutor6;
2016
+ exports2.GitExecutor = GitExecutor4;
2002
2017
  }
2003
2018
  });
2004
2019
 
@@ -2467,34 +2482,34 @@ var require_auth = __commonJS({
2467
2482
  Object.defineProperty(exports2, "__esModule", { value: true });
2468
2483
  exports2.getConfigDir = getConfigDir5;
2469
2484
  exports2.getConfigPath = getConfigPath4;
2470
- exports2.loadConfig = loadConfig7;
2471
- exports2.saveConfig = saveConfig5;
2485
+ exports2.loadConfig = loadConfig6;
2486
+ exports2.saveConfig = saveConfig3;
2472
2487
  exports2.validateToken = validateToken;
2473
- var fs12 = __importStar(require("fs"));
2474
- var path14 = __importStar(require("path"));
2488
+ var fs11 = __importStar(require("fs"));
2489
+ var path13 = __importStar(require("path"));
2475
2490
  var os3 = __importStar(require("os"));
2476
2491
  var child_process_1 = require("child_process");
2477
2492
  var DEFAULT_CONFIG_FILE = "config.json";
2478
2493
  function getConfigDir5() {
2479
- return process.env.EPISODA_CONFIG_DIR || path14.join(os3.homedir(), ".episoda");
2494
+ return process.env.EPISODA_CONFIG_DIR || path13.join(os3.homedir(), ".episoda");
2480
2495
  }
2481
2496
  function getConfigPath4(configPath) {
2482
2497
  if (configPath) {
2483
2498
  return configPath;
2484
2499
  }
2485
- return path14.join(getConfigDir5(), DEFAULT_CONFIG_FILE);
2500
+ return path13.join(getConfigDir5(), DEFAULT_CONFIG_FILE);
2486
2501
  }
2487
2502
  function ensureConfigDir(configPath) {
2488
- const dir = path14.dirname(configPath);
2489
- const isNew = !fs12.existsSync(dir);
2503
+ const dir = path13.dirname(configPath);
2504
+ const isNew = !fs11.existsSync(dir);
2490
2505
  if (isNew) {
2491
- fs12.mkdirSync(dir, { recursive: true, mode: 448 });
2506
+ fs11.mkdirSync(dir, { recursive: true, mode: 448 });
2492
2507
  }
2493
2508
  if (process.platform === "darwin") {
2494
- const nosyncPath = path14.join(dir, ".nosync");
2495
- if (isNew || !fs12.existsSync(nosyncPath)) {
2509
+ const nosyncPath = path13.join(dir, ".nosync");
2510
+ if (isNew || !fs11.existsSync(nosyncPath)) {
2496
2511
  try {
2497
- fs12.writeFileSync(nosyncPath, "", { mode: 384 });
2512
+ fs11.writeFileSync(nosyncPath, "", { mode: 384 });
2498
2513
  (0, child_process_1.execSync)(`xattr -w com.apple.fileprovider.ignore 1 "${dir}"`, {
2499
2514
  stdio: "ignore",
2500
2515
  timeout: 5e3
@@ -2504,13 +2519,13 @@ var require_auth = __commonJS({
2504
2519
  }
2505
2520
  }
2506
2521
  }
2507
- async function loadConfig7(configPath) {
2522
+ async function loadConfig6(configPath) {
2508
2523
  const fullPath = getConfigPath4(configPath);
2509
- if (!fs12.existsSync(fullPath)) {
2524
+ if (!fs11.existsSync(fullPath)) {
2510
2525
  return null;
2511
2526
  }
2512
2527
  try {
2513
- const content = fs12.readFileSync(fullPath, "utf8");
2528
+ const content = fs11.readFileSync(fullPath, "utf8");
2514
2529
  const config = JSON.parse(content);
2515
2530
  return config;
2516
2531
  } catch (error) {
@@ -2518,12 +2533,12 @@ var require_auth = __commonJS({
2518
2533
  return null;
2519
2534
  }
2520
2535
  }
2521
- async function saveConfig5(config, configPath) {
2536
+ async function saveConfig3(config, configPath) {
2522
2537
  const fullPath = getConfigPath4(configPath);
2523
2538
  ensureConfigDir(fullPath);
2524
2539
  try {
2525
2540
  const content = JSON.stringify(config, null, 2);
2526
- fs12.writeFileSync(fullPath, content, { mode: 384 });
2541
+ fs11.writeFileSync(fullPath, content, { mode: 384 });
2527
2542
  } catch (error) {
2528
2543
  throw new Error(`Failed to save config: ${error instanceof Error ? error.message : String(error)}`);
2529
2544
  }
@@ -2634,10 +2649,10 @@ var require_dist = __commonJS({
2634
2649
 
2635
2650
  // src/index.ts
2636
2651
  var import_commander = require("commander");
2637
- var import_core15 = __toESM(require_dist());
2652
+ var import_core14 = __toESM(require_dist());
2638
2653
 
2639
2654
  // src/commands/dev.ts
2640
- var import_core6 = __toESM(require_dist());
2655
+ var import_core4 = __toESM(require_dist());
2641
2656
 
2642
2657
  // src/framework-detector.ts
2643
2658
  var fs = __toESM(require("fs"));
@@ -3110,9 +3125,9 @@ async function getDevServerStatus() {
3110
3125
  }
3111
3126
 
3112
3127
  // src/commands/dev.ts
3113
- var import_child_process3 = require("child_process");
3114
- var path7 = __toESM(require("path"));
3115
- var fs6 = __toESM(require("fs"));
3128
+ var import_child_process2 = require("child_process");
3129
+ var path5 = __toESM(require("path"));
3130
+ var fs4 = __toESM(require("fs"));
3116
3131
 
3117
3132
  // src/utils/port-check.ts
3118
3133
  var net2 = __toESM(require("net"));
@@ -3172,7 +3187,8 @@ var WorktreeManager = class _WorktreeManager {
3172
3187
  }
3173
3188
  /**
3174
3189
  * Initialize worktree manager from existing project root
3175
- * @returns true if valid worktree project, false otherwise
3190
+ * EP971: All projects use worktree architecture
3191
+ * @returns true if valid project, false otherwise
3176
3192
  */
3177
3193
  async initialize() {
3178
3194
  if (!fs3.existsSync(this.bareRepoPath)) {
@@ -3183,7 +3199,7 @@ var WorktreeManager = class _WorktreeManager {
3183
3199
  }
3184
3200
  try {
3185
3201
  const config = this.readConfig();
3186
- return config?.worktreeMode === true;
3202
+ return config !== null;
3187
3203
  } catch {
3188
3204
  return false;
3189
3205
  }
@@ -3208,7 +3224,6 @@ var WorktreeManager = class _WorktreeManager {
3208
3224
  workspaceSlug,
3209
3225
  projectSlug,
3210
3226
  bareRepoPath: manager.bareRepoPath,
3211
- worktreeMode: true,
3212
3227
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3213
3228
  worktrees: []
3214
3229
  };
@@ -3564,6 +3579,148 @@ var WorktreeManager = class _WorktreeManager {
3564
3579
  this.releaseLock();
3565
3580
  }
3566
3581
  }
3582
+ // EP959-11: Worktree setup methods
3583
+ /**
3584
+ * Update the setup status of a worktree
3585
+ */
3586
+ async updateWorktreeStatus(moduleUid, status2, error) {
3587
+ return this.updateConfigSafe((config) => {
3588
+ const worktree = config.worktrees.find((w) => w.moduleUid === moduleUid);
3589
+ if (worktree) {
3590
+ worktree.setupStatus = status2;
3591
+ if (status2 === "running") {
3592
+ worktree.setupStartedAt = (/* @__PURE__ */ new Date()).toISOString();
3593
+ } else if (status2 === "ready" || status2 === "error") {
3594
+ worktree.setupCompletedAt = (/* @__PURE__ */ new Date()).toISOString();
3595
+ }
3596
+ if (error) {
3597
+ worktree.setupError = error;
3598
+ }
3599
+ }
3600
+ return config;
3601
+ });
3602
+ }
3603
+ /**
3604
+ * Get worktree info including setup status
3605
+ */
3606
+ getWorktreeStatus(moduleUid) {
3607
+ const config = this.readConfig();
3608
+ if (!config) return null;
3609
+ return config.worktrees.find((w) => w.moduleUid === moduleUid) || null;
3610
+ }
3611
+ /**
3612
+ * Copy files from main worktree to module worktree
3613
+ *
3614
+ * @deprecated EP964: This function is deprecated. Use worktree_env_vars for
3615
+ * environment variables or worktree_setup_script for other file operations.
3616
+ * This function now returns success (no-op) when main/ worktree doesn't exist.
3617
+ */
3618
+ async copyFilesFromMain(moduleUid, files) {
3619
+ console.warn(`[WorktreeManager] EP964: copyFilesFromMain is DEPRECATED.`);
3620
+ console.warn(`[WorktreeManager] EP964: Use worktree_env_vars for .env or worktree_setup_script for other files.`);
3621
+ const config = this.readConfig();
3622
+ if (!config) {
3623
+ return { success: false, error: "Config not found" };
3624
+ }
3625
+ const worktree = config.worktrees.find((w) => w.moduleUid === moduleUid);
3626
+ if (!worktree) {
3627
+ return { success: false, error: `Worktree not found for ${moduleUid}` };
3628
+ }
3629
+ const mainWorktree = config.worktrees.find((w) => w.moduleUid === "main");
3630
+ if (!mainWorktree) {
3631
+ console.warn(`[WorktreeManager] EP964: No 'main' worktree - skipping file copy (this is expected).`);
3632
+ return { success: true };
3633
+ }
3634
+ try {
3635
+ for (const file of files) {
3636
+ const srcPath = path4.join(mainWorktree.worktreePath, file);
3637
+ const destPath = path4.join(worktree.worktreePath, file);
3638
+ if (fs3.existsSync(srcPath)) {
3639
+ const destDir = path4.dirname(destPath);
3640
+ if (!fs3.existsSync(destDir)) {
3641
+ fs3.mkdirSync(destDir, { recursive: true });
3642
+ }
3643
+ fs3.copyFileSync(srcPath, destPath);
3644
+ console.log(`[WorktreeManager] EP964: Copied ${file} to ${moduleUid} (deprecated)`);
3645
+ } else {
3646
+ console.log(`[WorktreeManager] EP964: Skipped ${file} (not found in main)`);
3647
+ }
3648
+ }
3649
+ return { success: true };
3650
+ } catch (error) {
3651
+ return { success: false, error: error instanceof Error ? error.message : String(error) };
3652
+ }
3653
+ }
3654
+ /**
3655
+ * Run worktree setup script
3656
+ * EP959-M1: Enhanced logging with working directory, timeout info, and script preview
3657
+ */
3658
+ async runSetupScript(moduleUid, script) {
3659
+ const config = this.readConfig();
3660
+ if (!config) {
3661
+ return { success: false, error: "Config not found" };
3662
+ }
3663
+ const worktree = config.worktrees.find((w) => w.moduleUid === moduleUid);
3664
+ if (!worktree) {
3665
+ return { success: false, error: `Worktree not found for ${moduleUid}` };
3666
+ }
3667
+ const TIMEOUT_MINUTES = 10;
3668
+ const scriptPreview = script.length > 200 ? script.slice(0, 200) + "..." : script;
3669
+ console.log(`[WorktreeManager] EP959: Running setup script for ${moduleUid}`);
3670
+ console.log(`[WorktreeManager] EP959: Working directory: ${worktree.worktreePath}`);
3671
+ console.log(`[WorktreeManager] EP959: Timeout: ${TIMEOUT_MINUTES} minutes`);
3672
+ console.log(`[WorktreeManager] EP959: Script: ${scriptPreview}`);
3673
+ try {
3674
+ const { execSync: execSync7 } = require("child_process");
3675
+ execSync7(script, {
3676
+ cwd: worktree.worktreePath,
3677
+ stdio: "inherit",
3678
+ timeout: TIMEOUT_MINUTES * 60 * 1e3,
3679
+ env: { ...process.env, NODE_ENV: "development" }
3680
+ });
3681
+ console.log(`[WorktreeManager] EP959: Setup script completed successfully for ${moduleUid}`);
3682
+ return { success: true };
3683
+ } catch (error) {
3684
+ const errorMessage = error instanceof Error ? error.message : String(error);
3685
+ console.error(`[WorktreeManager] EP959: Setup script failed for ${moduleUid}:`, errorMessage);
3686
+ return { success: false, error: errorMessage };
3687
+ }
3688
+ }
3689
+ /**
3690
+ * Run worktree cleanup script before removal
3691
+ * EP959-m3: Execute cleanup script when worktree is being released
3692
+ */
3693
+ async runCleanupScript(moduleUid, script) {
3694
+ const config = this.readConfig();
3695
+ if (!config) {
3696
+ return { success: false, error: "Config not found" };
3697
+ }
3698
+ const worktree = config.worktrees.find((w) => w.moduleUid === moduleUid);
3699
+ if (!worktree) {
3700
+ return { success: false, error: `Worktree not found for ${moduleUid}` };
3701
+ }
3702
+ const TIMEOUT_MINUTES = 5;
3703
+ const scriptPreview = script.length > 200 ? script.slice(0, 200) + "..." : script;
3704
+ console.log(`[WorktreeManager] EP959: Running cleanup script for ${moduleUid}`);
3705
+ console.log(`[WorktreeManager] EP959: Working directory: ${worktree.worktreePath}`);
3706
+ console.log(`[WorktreeManager] EP959: Timeout: ${TIMEOUT_MINUTES} minutes`);
3707
+ console.log(`[WorktreeManager] EP959: Script: ${scriptPreview}`);
3708
+ try {
3709
+ const { execSync: execSync7 } = require("child_process");
3710
+ execSync7(script, {
3711
+ cwd: worktree.worktreePath,
3712
+ stdio: "inherit",
3713
+ timeout: TIMEOUT_MINUTES * 60 * 1e3,
3714
+ env: { ...process.env, NODE_ENV: "development" }
3715
+ });
3716
+ console.log(`[WorktreeManager] EP959: Cleanup script completed successfully for ${moduleUid}`);
3717
+ return { success: true };
3718
+ } catch (error) {
3719
+ const errorMessage = error instanceof Error ? error.message : String(error);
3720
+ console.warn(`[WorktreeManager] EP959: Cleanup script failed for ${moduleUid} (non-blocking):`, errorMessage);
3721
+ return { success: false, error: errorMessage };
3722
+ }
3723
+ }
3567
3724
  };
3568
3725
  function getEpisodaRoot() {
3569
3726
  return process.env.EPISODA_ROOT || path4.join(require("os").homedir(), "episoda");
@@ -3598,401 +3755,65 @@ async function findProjectRoot(startPath) {
3598
3755
  return null;
3599
3756
  }
3600
3757
 
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
- );
3758
+ // src/api/machine-settings.ts
3759
+ async function fetchProjectPath(config, projectId) {
3760
+ if (!config.device_id || !config.access_token) {
3761
+ return null;
3759
3762
  }
3760
- let workspaceSlug = existingConfig.workspace_slug || "default";
3761
- let projectSlug = existingConfig.project_slug || path6.basename(cwd);
3763
+ const serverUrl = config.api_url || "https://episoda.dev";
3764
+ const url = `${serverUrl}/api/dev/machines/${config.device_id}/project-path/${projectId}`;
3762
3765
  try {
3763
- const apiUrl = config.api_url || "https://episoda.dev";
3764
- const response = await fetch(`${apiUrl}/api/projects/${projectId}`, {
3766
+ const response = await fetch(url, {
3767
+ method: "GET",
3765
3768
  headers: {
3766
3769
  "Authorization": `Bearer ${config.access_token}`,
3767
3770
  "Content-Type": "application/json"
3768
3771
  }
3769
3772
  });
3770
- if (response.ok) {
3771
- const responseData = await response.json();
3772
- const projectData = responseData.data?.project;
3773
- if (projectData) {
3774
- workspaceSlug = projectData.workspace_slug || workspaceSlug;
3775
- projectSlug = projectData.slug || projectSlug;
3773
+ if (!response.ok) {
3774
+ if (response.status === 404) {
3775
+ return null;
3776
3776
  }
3777
+ console.debug(`[MachineSettings] fetchProjectPath failed: ${response.status} ${response.statusText}`);
3778
+ return null;
3777
3779
  }
3778
- } catch {
3779
- }
3780
- const targetPath = getProjectPath(workspaceSlug, projectSlug);
3781
- status.info("Migration Plan:");
3782
- status.info(` Current path: ${cwd}`);
3783
- status.info(` Target path: ${targetPath}`);
3784
- status.info(` Remote URL: ${remoteUrl}`);
3785
- status.info(` Current branch: ${currentBranch}`);
3786
- if (hasUncommitted) {
3787
- status.warning(` Uncommitted changes will be stashed`);
3788
- }
3789
- status.info("");
3790
- if (options.dryRun) {
3791
- status.info("Dry run complete. No changes made.");
3792
- status.info("Remove --dry-run to perform the migration.");
3793
- return;
3780
+ const data = await response.json();
3781
+ return data.data?.path || null;
3782
+ } catch (error) {
3783
+ console.debug(`[MachineSettings] fetchProjectPath network error:`, error);
3784
+ return null;
3794
3785
  }
3795
- const uncommittedFiles = statusResult.details?.uncommittedFiles || [];
3796
- if (hasUncommitted) {
3797
- status.info(`Found ${uncommittedFiles.length} uncommitted file(s) to preserve...`);
3786
+ }
3787
+ async function syncProjectPath(config, projectId, path13) {
3788
+ if (!config.device_id || !config.access_token) {
3789
+ return false;
3798
3790
  }
3799
- const targetExistedBefore = fs5.existsSync(targetPath);
3791
+ const serverUrl = config.api_url || "https://episoda.dev";
3792
+ const url = `${serverUrl}/api/dev/machines/${config.device_id}/project-path/${projectId}`;
3800
3793
  try {
3801
- status.info("Creating target directory...");
3802
- const episodaRoot = getEpisodaRoot();
3803
- fs5.mkdirSync(targetPath, { recursive: true });
3804
- status.info("Cloning as bare repository...");
3805
- const bareRepoPath = path6.join(targetPath, ".bare");
3806
- const cloneResult = await gitExecutor.execute({
3807
- action: "clone_bare",
3808
- url: remoteUrl,
3809
- path: bareRepoPath
3810
- });
3811
- if (!cloneResult.success) {
3812
- throw new Error(`Failed to clone: ${cloneResult.output}`);
3813
- }
3814
- status.success("\u2713 Bare repository cloned");
3815
- status.info("Creating worktree configuration...");
3816
- const episodaDir = path6.join(targetPath, ".episoda");
3817
- fs5.mkdirSync(episodaDir, { recursive: true });
3818
- const worktreeConfig = {
3819
- projectId,
3820
- workspaceSlug,
3821
- projectSlug,
3822
- bareRepoPath,
3823
- worktreeMode: true,
3824
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3825
- worktrees: []
3826
- };
3827
- fs5.writeFileSync(
3828
- path6.join(episodaDir, "config.json"),
3829
- JSON.stringify(worktreeConfig, null, 2),
3830
- "utf-8"
3831
- );
3832
- status.info(`Creating worktree for branch "${currentBranch}"...`);
3833
- const moduleUid = extractModuleUid(currentBranch) || "main";
3834
- const worktreePath = path6.join(targetPath, moduleUid);
3835
- const worktreeResult = await gitExecutor.execute({
3836
- action: "worktree_add",
3837
- path: worktreePath,
3838
- branch: currentBranch,
3839
- create: false
3840
- }, { cwd: bareRepoPath });
3841
- if (!worktreeResult.success) {
3842
- throw new Error(`Failed to create worktree: ${worktreeResult.output}`);
3843
- }
3844
- status.success(`\u2713 Worktree created at ${worktreePath}`);
3845
- worktreeConfig.worktrees.push({
3846
- moduleUid,
3847
- branchName: currentBranch,
3848
- worktreePath,
3849
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3850
- lastAccessed: (/* @__PURE__ */ new Date()).toISOString()
3851
- });
3852
- fs5.writeFileSync(
3853
- path6.join(episodaDir, "config.json"),
3854
- JSON.stringify(worktreeConfig, null, 2),
3855
- "utf-8"
3856
- );
3857
- if (hasUncommitted && uncommittedFiles.length > 0) {
3858
- status.info("Copying uncommitted changes to new worktree...");
3859
- let copiedCount = 0;
3860
- let failedCount = 0;
3861
- for (const file of uncommittedFiles) {
3862
- const sourcePath = path6.join(cwd, file);
3863
- const destPath = path6.join(worktreePath, file);
3864
- try {
3865
- if (fs5.existsSync(sourcePath)) {
3866
- const destDir = path6.dirname(destPath);
3867
- if (!fs5.existsSync(destDir)) {
3868
- fs5.mkdirSync(destDir, { recursive: true });
3869
- }
3870
- fs5.copyFileSync(sourcePath, destPath);
3871
- copiedCount++;
3872
- }
3873
- } catch (copyError) {
3874
- failedCount++;
3875
- status.warning(`Could not copy: ${file}`);
3876
- }
3877
- }
3878
- if (copiedCount > 0) {
3879
- status.success(`\u2713 Copied ${copiedCount} uncommitted file(s)`);
3880
- }
3881
- if (failedCount > 0) {
3882
- status.warning(`${failedCount} file(s) could not be copied`);
3883
- status.info("You may need to copy some files manually from the original directory");
3884
- }
3885
- }
3886
- addProject2(projectId, targetPath, {
3887
- worktreeMode: true,
3888
- bareRepoPath
3794
+ const response = await fetch(url, {
3795
+ method: "PUT",
3796
+ headers: {
3797
+ "Authorization": `Bearer ${config.access_token}`,
3798
+ "Content-Type": "application/json"
3799
+ },
3800
+ body: JSON.stringify({ path: path13 })
3889
3801
  });
3890
- status.success("\u2713 Project registered with daemon");
3891
- try {
3892
- const apiUrl = config.api_url || "https://episoda.dev";
3893
- const settingsResponse = await fetch(`${apiUrl}/api/projects/${projectId}/settings`, {
3894
- method: "PATCH",
3895
- headers: {
3896
- "Authorization": `Bearer ${config.access_token}`,
3897
- "Content-Type": "application/json"
3898
- },
3899
- body: JSON.stringify({
3900
- local_project_path: worktreePath
3901
- })
3902
- });
3903
- if (settingsResponse.ok) {
3904
- status.success("\u2713 Project settings updated");
3905
- } else {
3906
- status.warning("Could not update project settings in database");
3907
- }
3908
- } catch {
3909
- status.warning("Could not update project settings in database");
3802
+ if (!response.ok) {
3803
+ console.debug(`[MachineSettings] syncProjectPath failed: ${response.status} ${response.statusText}`);
3910
3804
  }
3911
- try {
3912
- const updatedConfig = {
3913
- ...config,
3914
- project_settings: {
3915
- ...config.project_settings,
3916
- local_project_path: worktreePath,
3917
- cached_at: Date.now()
3918
- }
3919
- };
3920
- await (0, import_core5.saveConfig)(updatedConfig);
3921
- status.success("\u2713 Local config updated");
3922
- } catch {
3923
- status.warning("Could not update local config cache");
3924
- }
3925
- status.info("");
3926
- status.success("Migration complete!");
3927
- status.info("");
3928
- status.info("New project structure:");
3929
- status.info(` ${targetPath}/`);
3930
- status.info(" \u251C\u2500\u2500 .bare/ # Git repository");
3931
- status.info(" \u251C\u2500\u2500 .episoda/");
3932
- status.info(" \u2502 \u2514\u2500\u2500 config.json");
3933
- status.info(` \u2514\u2500\u2500 ${moduleUid}/ # Your current worktree`);
3934
- status.info("");
3935
- status.info("Next steps:");
3936
- status.info(` 1. cd ${worktreePath}`);
3937
- status.info(" 2. Continue working on your current branch");
3938
- status.info(" 3. Use `episoda checkout EP###` for other modules");
3939
- status.info("");
3940
- status.info("Your original repository at:");
3941
- status.info(` ${cwd}`);
3942
- status.info("can be safely removed after verifying the migration.");
3943
- status.info("");
3805
+ return response.ok;
3944
3806
  } catch (error) {
3945
- status.error("Migration failed, cleaning up...");
3946
- if (!targetExistedBefore && fs5.existsSync(targetPath)) {
3947
- try {
3948
- fs5.rmSync(targetPath, { recursive: true, force: true });
3949
- status.info(`Removed ${targetPath}`);
3950
- } catch {
3951
- status.warning(`Could not remove ${targetPath}`);
3952
- }
3953
- } else if (targetExistedBefore) {
3954
- status.info("Target directory existed before migration, only removing migration artifacts...");
3955
- const bareRepoPath = path6.join(targetPath, ".bare");
3956
- const episodaDir = path6.join(targetPath, ".episoda");
3957
- if (fs5.existsSync(bareRepoPath)) {
3958
- try {
3959
- fs5.rmSync(bareRepoPath, { recursive: true, force: true });
3960
- status.info("Removed .bare directory");
3961
- } catch {
3962
- status.warning("Could not remove .bare directory");
3963
- }
3964
- }
3965
- if (fs5.existsSync(episodaDir)) {
3966
- try {
3967
- const configPath = path6.join(episodaDir, "config.json");
3968
- if (fs5.existsSync(configPath)) {
3969
- const config2 = JSON.parse(fs5.readFileSync(configPath, "utf-8"));
3970
- if (config2.worktreeMode) {
3971
- fs5.rmSync(episodaDir, { recursive: true, force: true });
3972
- status.info("Removed .episoda directory");
3973
- }
3974
- }
3975
- } catch {
3976
- status.warning("Could not remove .episoda directory");
3977
- }
3978
- }
3979
- }
3980
- throw error;
3981
- }
3982
- }
3983
- function extractModuleUid(branchName) {
3984
- const match = branchName.match(/ep(\d+)/i);
3985
- if (match) {
3986
- return `EP${match[1]}`;
3807
+ console.debug(`[MachineSettings] syncProjectPath network error:`, error);
3808
+ return false;
3987
3809
  }
3988
- return null;
3989
3810
  }
3990
3811
 
3991
3812
  // src/commands/dev.ts
3992
3813
  var CONNECTION_MAX_RETRIES = 3;
3993
3814
  function findGitRoot(startDir) {
3994
3815
  try {
3995
- const result = (0, import_child_process3.execSync)("git rev-parse --show-toplevel", {
3816
+ const result = (0, import_child_process2.execSync)("git rev-parse --show-toplevel", {
3996
3817
  cwd: startDir,
3997
3818
  encoding: "utf-8",
3998
3819
  stdio: ["pipe", "pipe", "pipe"]
@@ -4004,7 +3825,7 @@ function findGitRoot(startDir) {
4004
3825
  }
4005
3826
  async function devCommand(options = {}) {
4006
3827
  try {
4007
- const config = await (0, import_core6.loadConfig)();
3828
+ const config = await (0, import_core4.loadConfig)();
4008
3829
  if (!config || !config.access_token || !config.project_id) {
4009
3830
  status.error("No authentication found. Please run:");
4010
3831
  status.info("");
@@ -4046,59 +3867,33 @@ async function devCommand(options = {}) {
4046
3867
  await new Promise((resolve4) => setTimeout(resolve4, 2e3));
4047
3868
  }
4048
3869
  }
4049
- const serverUrl = config.api_url || process.env.EPISODA_API_URL || "https://episoda.dev";
4050
3870
  let projectPath;
4051
- const SETTINGS_CACHE_TTL = 24 * 60 * 60 * 1e3;
4052
- const cachedSettings = config.project_settings;
4053
- const isCacheFresh = cachedSettings?.cached_at && Date.now() - cachedSettings.cached_at < SETTINGS_CACHE_TTL;
4054
- if (isCacheFresh && cachedSettings?.local_project_path) {
4055
- projectPath = cachedSettings.local_project_path;
4056
- status.debug(`Using cached project path: ${projectPath}`);
3871
+ const serverPath = await fetchProjectPath(config, config.project_id);
3872
+ if (serverPath && fs4.existsSync(serverPath)) {
3873
+ projectPath = serverPath;
3874
+ status.debug(`Using server-synced project path: ${projectPath}`);
4057
3875
  } else {
4058
3876
  const detectedRoot = findGitRoot(options.cwd || process.cwd());
4059
- projectPath = detectedRoot || path7.resolve(options.cwd || process.cwd());
3877
+ projectPath = detectedRoot || path5.resolve(options.cwd || process.cwd());
4060
3878
  if (detectedRoot) {
4061
3879
  status.debug(`Detected project root: ${projectPath}`);
4062
3880
  } else {
4063
3881
  status.warning(`Could not detect git root, using: ${projectPath}`);
4064
3882
  }
4065
- const updatedConfig = {
4066
- ...config,
4067
- project_settings: {
4068
- ...cachedSettings,
4069
- local_project_path: projectPath,
4070
- cached_at: Date.now()
3883
+ syncProjectPath(config, config.project_id, projectPath).then((synced) => {
3884
+ if (synced) {
3885
+ status.debug("Project path synced to server");
4071
3886
  }
4072
- };
4073
- await (0, import_core6.saveConfig)(updatedConfig);
4074
- status.debug("Cached project settings locally");
4075
- const settingsUrl = `${serverUrl}/api/projects/${config.project_id}/settings`;
4076
- fetch(settingsUrl, {
4077
- method: "PATCH",
4078
- headers: {
4079
- "Authorization": `Bearer ${config.access_token}`,
4080
- "Content-Type": "application/json"
4081
- },
4082
- body: JSON.stringify({ local_project_path: projectPath })
4083
3887
  }).catch(() => {
4084
3888
  });
4085
3889
  }
4086
- const hasGitDir = fs6.existsSync(path7.join(projectPath, ".git"));
4087
3890
  const isWorktree = await isWorktreeProject(projectPath);
4088
- if (hasGitDir && !isWorktree) {
3891
+ if (!isWorktree) {
3892
+ status.error("Not an Episoda project.");
4089
3893
  status.info("");
4090
- status.info("EP944: Migrating project to worktree mode...");
4091
- status.info("This is a one-time operation that enables multi-module development.");
3894
+ status.info("Use `episoda clone {workspace}/{project}` to set up a project.");
4092
3895
  status.info("");
4093
- try {
4094
- await migrateCommand({ cwd: projectPath, silent: false });
4095
- status.success("Project migrated to worktree mode!");
4096
- status.info("");
4097
- } catch (error) {
4098
- status.warning(`Migration skipped: ${error.message}`);
4099
- status.info("You can run `episoda migrate` manually later.");
4100
- status.info("");
4101
- }
3896
+ process.exit(1);
4102
3897
  }
4103
3898
  let daemonPid = isDaemonRunning();
4104
3899
  if (!daemonPid) {
@@ -4192,7 +3987,7 @@ async function runDevServer(command, cwd, autoRestart) {
4192
3987
  let shuttingDown = false;
4193
3988
  const startServer = () => {
4194
3989
  status.info(`Starting dev server: ${command.join(" ")}`);
4195
- devProcess = (0, import_child_process3.spawn)(command[0], command.slice(1), {
3990
+ devProcess = (0, import_child_process2.spawn)(command[0], command.slice(1), {
4196
3991
  cwd,
4197
3992
  stdio: ["inherit", "inherit", "inherit"],
4198
3993
  shell: true
@@ -4245,33 +4040,33 @@ Received ${signal}, shutting down...`);
4245
4040
 
4246
4041
  // src/commands/auth.ts
4247
4042
  var os = __toESM(require("os"));
4248
- var fs8 = __toESM(require("fs"));
4249
- var path9 = __toESM(require("path"));
4250
- var import_child_process5 = require("child_process");
4251
- var import_core8 = __toESM(require_dist());
4043
+ var fs6 = __toESM(require("fs"));
4044
+ var path7 = __toESM(require("path"));
4045
+ var import_child_process4 = require("child_process");
4046
+ var import_core6 = __toESM(require_dist());
4252
4047
 
4253
4048
  // src/daemon/machine-id.ts
4254
- var fs7 = __toESM(require("fs"));
4255
- var path8 = __toESM(require("path"));
4049
+ var fs5 = __toESM(require("fs"));
4050
+ var path6 = __toESM(require("path"));
4256
4051
  var crypto2 = __toESM(require("crypto"));
4257
- var import_child_process4 = require("child_process");
4258
- var import_core7 = __toESM(require_dist());
4052
+ var import_child_process3 = require("child_process");
4053
+ var import_core5 = __toESM(require_dist());
4259
4054
  function isValidUUID(str) {
4260
4055
  const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
4261
4056
  return uuidRegex.test(str);
4262
4057
  }
4263
4058
  async function getMachineId() {
4264
- const machineIdPath = path8.join((0, import_core7.getConfigDir)(), "machine-id");
4059
+ const machineIdPath = path6.join((0, import_core5.getConfigDir)(), "machine-id");
4265
4060
  try {
4266
- if (fs7.existsSync(machineIdPath)) {
4267
- const existingId = fs7.readFileSync(machineIdPath, "utf-8").trim();
4061
+ if (fs5.existsSync(machineIdPath)) {
4062
+ const existingId = fs5.readFileSync(machineIdPath, "utf-8").trim();
4268
4063
  if (existingId) {
4269
4064
  if (isValidUUID(existingId)) {
4270
4065
  return existingId;
4271
4066
  }
4272
4067
  console.log("[MachineId] Migrating legacy machine ID to UUID format...");
4273
4068
  const newUUID = generateMachineId();
4274
- fs7.writeFileSync(machineIdPath, newUUID, "utf-8");
4069
+ fs5.writeFileSync(machineIdPath, newUUID, "utf-8");
4275
4070
  console.log(`[MachineId] Migrated: ${existingId} \u2192 ${newUUID}`);
4276
4071
  return newUUID;
4277
4072
  }
@@ -4280,11 +4075,11 @@ async function getMachineId() {
4280
4075
  }
4281
4076
  const machineId = generateMachineId();
4282
4077
  try {
4283
- const dir = path8.dirname(machineIdPath);
4284
- if (!fs7.existsSync(dir)) {
4285
- fs7.mkdirSync(dir, { recursive: true });
4078
+ const dir = path6.dirname(machineIdPath);
4079
+ if (!fs5.existsSync(dir)) {
4080
+ fs5.mkdirSync(dir, { recursive: true });
4286
4081
  }
4287
- fs7.writeFileSync(machineIdPath, machineId, "utf-8");
4082
+ fs5.writeFileSync(machineIdPath, machineId, "utf-8");
4288
4083
  } catch (error) {
4289
4084
  console.error("Warning: Could not save machine ID to disk:", error);
4290
4085
  }
@@ -4293,7 +4088,7 @@ async function getMachineId() {
4293
4088
  function getHardwareUUID() {
4294
4089
  try {
4295
4090
  if (process.platform === "darwin") {
4296
- const output = (0, import_child_process4.execSync)(
4091
+ const output = (0, import_child_process3.execSync)(
4297
4092
  `ioreg -d2 -c IOPlatformExpertDevice | awk -F\\" '/IOPlatformUUID/{print $(NF-1)}'`,
4298
4093
  { encoding: "utf-8", timeout: 5e3 }
4299
4094
  ).trim();
@@ -4301,20 +4096,20 @@ function getHardwareUUID() {
4301
4096
  return output;
4302
4097
  }
4303
4098
  } else if (process.platform === "linux") {
4304
- if (fs7.existsSync("/etc/machine-id")) {
4305
- const machineId = fs7.readFileSync("/etc/machine-id", "utf-8").trim();
4099
+ if (fs5.existsSync("/etc/machine-id")) {
4100
+ const machineId = fs5.readFileSync("/etc/machine-id", "utf-8").trim();
4306
4101
  if (machineId && machineId.length > 0) {
4307
4102
  return machineId;
4308
4103
  }
4309
4104
  }
4310
- if (fs7.existsSync("/var/lib/dbus/machine-id")) {
4311
- const dbusId = fs7.readFileSync("/var/lib/dbus/machine-id", "utf-8").trim();
4105
+ if (fs5.existsSync("/var/lib/dbus/machine-id")) {
4106
+ const dbusId = fs5.readFileSync("/var/lib/dbus/machine-id", "utf-8").trim();
4312
4107
  if (dbusId && dbusId.length > 0) {
4313
4108
  return dbusId;
4314
4109
  }
4315
4110
  }
4316
4111
  } else if (process.platform === "win32") {
4317
- const output = (0, import_child_process4.execSync)("wmic csproduct get uuid", {
4112
+ const output = (0, import_child_process3.execSync)("wmic csproduct get uuid", {
4318
4113
  encoding: "utf-8",
4319
4114
  timeout: 5e3
4320
4115
  });
@@ -4695,7 +4490,7 @@ async function authCommand(options = {}) {
4695
4490
  status.info(` Project: ${tokenResponse.project_uid}`);
4696
4491
  status.info(` Workspace: ${tokenResponse.workspace_uid}`);
4697
4492
  status.info("");
4698
- await (0, import_core8.saveConfig)({
4493
+ await (0, import_core6.saveConfig)({
4699
4494
  project_id: tokenResponse.project_id,
4700
4495
  user_id: tokenResponse.user_id,
4701
4496
  workspace_id: tokenResponse.workspace_id,
@@ -4709,7 +4504,7 @@ async function authCommand(options = {}) {
4709
4504
  // Convert to Unix timestamp
4710
4505
  api_url: apiUrl
4711
4506
  });
4712
- status.success(`\u2713 Configuration saved to ${(0, import_core8.getConfigPath)()}`);
4507
+ status.success(`\u2713 Configuration saved to ${(0, import_core6.getConfigPath)()}`);
4713
4508
  status.info("");
4714
4509
  status.info("Installing git credential helper...");
4715
4510
  const credentialHelperInstalled = await installGitCredentialHelper(apiUrl);
@@ -4753,7 +4548,7 @@ async function monitorAuthorization(apiUrl, deviceCode, expiresIn) {
4753
4548
  resolve4(false);
4754
4549
  }, expiresIn * 1e3);
4755
4550
  const url = `${apiUrl}/api/oauth/authorize-stream?device_code=${deviceCode}`;
4756
- const curlProcess = (0, import_child_process5.spawn)("curl", ["-N", url]);
4551
+ const curlProcess = (0, import_child_process4.spawn)("curl", ["-N", url]);
4757
4552
  let buffer = "";
4758
4553
  curlProcess.stdout.on("data", (chunk) => {
4759
4554
  buffer += chunk.toString();
@@ -4857,7 +4652,7 @@ function openBrowser(url) {
4857
4652
  break;
4858
4653
  }
4859
4654
  try {
4860
- (0, import_child_process5.spawn)(command, args, {
4655
+ (0, import_child_process4.spawn)(command, args, {
4861
4656
  detached: true,
4862
4657
  stdio: "ignore"
4863
4658
  }).unref();
@@ -4868,17 +4663,17 @@ function openBrowser(url) {
4868
4663
  async function installGitCredentialHelper(apiUrl) {
4869
4664
  try {
4870
4665
  const homeDir = os.homedir();
4871
- const episodaBinDir = path9.join(homeDir, ".episoda", "bin");
4872
- const helperPath = path9.join(episodaBinDir, "git-credential-episoda");
4873
- fs8.mkdirSync(episodaBinDir, { recursive: true });
4666
+ const episodaBinDir = path7.join(homeDir, ".episoda", "bin");
4667
+ const helperPath = path7.join(episodaBinDir, "git-credential-episoda");
4668
+ fs6.mkdirSync(episodaBinDir, { recursive: true });
4874
4669
  const scriptContent = generateCredentialHelperScript(apiUrl);
4875
- fs8.writeFileSync(helperPath, scriptContent, { mode: 493 });
4670
+ fs6.writeFileSync(helperPath, scriptContent, { mode: 493 });
4876
4671
  try {
4877
- fs8.accessSync(helperPath, fs8.constants.X_OK);
4672
+ fs6.accessSync(helperPath, fs6.constants.X_OK);
4878
4673
  } catch {
4879
4674
  }
4880
4675
  try {
4881
- const allHelpers = (0, import_child_process5.execSync)("git config --global --get-all credential.helper", {
4676
+ const allHelpers = (0, import_child_process4.execSync)("git config --global --get-all credential.helper", {
4882
4677
  encoding: "utf8",
4883
4678
  stdio: ["pipe", "pipe", "pipe"]
4884
4679
  }).trim().split("\n");
@@ -4887,7 +4682,7 @@ async function installGitCredentialHelper(apiUrl) {
4887
4682
  }
4888
4683
  } catch {
4889
4684
  }
4890
- (0, import_child_process5.execSync)(`git config --global --add credential.helper "${helperPath}"`, {
4685
+ (0, import_child_process4.execSync)(`git config --global --add credential.helper "${helperPath}"`, {
4891
4686
  encoding: "utf8",
4892
4687
  stdio: ["pipe", "pipe", "pipe"]
4893
4688
  });
@@ -4905,19 +4700,19 @@ function updateShellProfile(binDir) {
4905
4700
  }
4906
4701
  const homeDir = os.homedir();
4907
4702
  const profiles = [
4908
- path9.join(homeDir, ".bashrc"),
4909
- path9.join(homeDir, ".zshrc"),
4910
- path9.join(homeDir, ".profile")
4703
+ path7.join(homeDir, ".bashrc"),
4704
+ path7.join(homeDir, ".zshrc"),
4705
+ path7.join(homeDir, ".profile")
4911
4706
  ];
4912
4707
  const exportLine = `export PATH="${binDir}:$PATH" # Added by episoda auth`;
4913
4708
  for (const profile of profiles) {
4914
4709
  try {
4915
- if (fs8.existsSync(profile)) {
4916
- const content = fs8.readFileSync(profile, "utf8");
4710
+ if (fs6.existsSync(profile)) {
4711
+ const content = fs6.readFileSync(profile, "utf8");
4917
4712
  if (content.includes(".episoda/bin")) {
4918
4713
  continue;
4919
4714
  }
4920
- fs8.appendFileSync(profile, `
4715
+ fs6.appendFileSync(profile, `
4921
4716
  # Episoda CLI
4922
4717
  ${exportLine}
4923
4718
  `);
@@ -4929,10 +4724,10 @@ ${exportLine}
4929
4724
 
4930
4725
  // src/commands/connect.ts
4931
4726
  var os2 = __toESM(require("os"));
4932
- var fs9 = __toESM(require("fs"));
4933
- var path10 = __toESM(require("path"));
4934
- var import_child_process6 = require("child_process");
4935
- var import_core9 = __toESM(require_dist());
4727
+ var fs7 = __toESM(require("fs"));
4728
+ var path8 = __toESM(require("path"));
4729
+ var import_child_process5 = require("child_process");
4730
+ var import_core7 = __toESM(require_dist());
4936
4731
  async function connectCommand(options) {
4937
4732
  const { code } = options;
4938
4733
  const apiUrl = options.apiUrl || process.env.EPISODA_API_URL || "https://episoda.dev";
@@ -4948,14 +4743,14 @@ async function connectCommand(options) {
4948
4743
  status.info(` Project: ${tokenResponse.project_uid}`);
4949
4744
  status.info(` Workspace: ${tokenResponse.workspace_uid}`);
4950
4745
  status.info("");
4951
- await (0, import_core9.saveConfig)({
4746
+ await (0, import_core7.saveConfig)({
4952
4747
  project_id: tokenResponse.project_id,
4953
4748
  user_id: tokenResponse.user_id,
4954
4749
  workspace_id: tokenResponse.workspace_id,
4955
4750
  access_token: tokenResponse.access_token,
4956
4751
  api_url: apiUrl
4957
4752
  });
4958
- status.success(`\u2713 Configuration saved to ${(0, import_core9.getConfigPath)()}`);
4753
+ status.success(`\u2713 Configuration saved to ${(0, import_core7.getConfigPath)()}`);
4959
4754
  status.info("");
4960
4755
  status.info("Installing git credential helper...");
4961
4756
  const credentialHelperInstalled = await installGitCredentialHelper2(apiUrl);
@@ -5005,13 +4800,13 @@ async function exchangeUserCode(apiUrl, userCode, machineId) {
5005
4800
  async function installGitCredentialHelper2(apiUrl) {
5006
4801
  try {
5007
4802
  const homeDir = os2.homedir();
5008
- const episodaBinDir = path10.join(homeDir, ".episoda", "bin");
5009
- const helperPath = path10.join(episodaBinDir, "git-credential-episoda");
5010
- fs9.mkdirSync(episodaBinDir, { recursive: true });
4803
+ const episodaBinDir = path8.join(homeDir, ".episoda", "bin");
4804
+ const helperPath = path8.join(episodaBinDir, "git-credential-episoda");
4805
+ fs7.mkdirSync(episodaBinDir, { recursive: true });
5011
4806
  const scriptContent = generateCredentialHelperScript(apiUrl);
5012
- fs9.writeFileSync(helperPath, scriptContent, { mode: 493 });
4807
+ fs7.writeFileSync(helperPath, scriptContent, { mode: 493 });
5013
4808
  try {
5014
- const allHelpers = (0, import_child_process6.execSync)("git config --global --get-all credential.helper", {
4809
+ const allHelpers = (0, import_child_process5.execSync)("git config --global --get-all credential.helper", {
5015
4810
  encoding: "utf8",
5016
4811
  stdio: ["pipe", "pipe", "pipe"]
5017
4812
  }).trim().split("\n");
@@ -5020,7 +4815,7 @@ async function installGitCredentialHelper2(apiUrl) {
5020
4815
  }
5021
4816
  } catch {
5022
4817
  }
5023
- (0, import_child_process6.execSync)(`git config --global --add credential.helper "${helperPath}"`, {
4818
+ (0, import_child_process5.execSync)(`git config --global --add credential.helper "${helperPath}"`, {
5024
4819
  encoding: "utf8",
5025
4820
  stdio: ["pipe", "pipe", "pipe"]
5026
4821
  });
@@ -5038,19 +4833,19 @@ function updateShellProfile2(binDir) {
5038
4833
  }
5039
4834
  const homeDir = os2.homedir();
5040
4835
  const profiles = [
5041
- path10.join(homeDir, ".bashrc"),
5042
- path10.join(homeDir, ".zshrc"),
5043
- path10.join(homeDir, ".profile")
4836
+ path8.join(homeDir, ".bashrc"),
4837
+ path8.join(homeDir, ".zshrc"),
4838
+ path8.join(homeDir, ".profile")
5044
4839
  ];
5045
4840
  const exportLine = `export PATH="${binDir}:$PATH" # Added by episoda`;
5046
4841
  for (const profile of profiles) {
5047
4842
  try {
5048
- if (fs9.existsSync(profile)) {
5049
- const content = fs9.readFileSync(profile, "utf8");
4843
+ if (fs7.existsSync(profile)) {
4844
+ const content = fs7.readFileSync(profile, "utf8");
5050
4845
  if (content.includes(".episoda/bin")) {
5051
4846
  continue;
5052
4847
  }
5053
- fs9.appendFileSync(profile, `
4848
+ fs7.appendFileSync(profile, `
5054
4849
  # Episoda CLI
5055
4850
  ${exportLine}
5056
4851
  `);
@@ -5061,11 +4856,11 @@ ${exportLine}
5061
4856
  }
5062
4857
 
5063
4858
  // src/commands/status.ts
5064
- var import_core10 = __toESM(require_dist());
4859
+ var import_core8 = __toESM(require_dist());
5065
4860
  async function statusCommand(options = {}) {
5066
4861
  status.info("Checking CLI status...");
5067
4862
  status.info("");
5068
- const config = await (0, import_core10.loadConfig)();
4863
+ const config = await (0, import_core8.loadConfig)();
5069
4864
  if (!config) {
5070
4865
  status.error("\u2717 CLI not initialized");
5071
4866
  status.info("");
@@ -5080,8 +4875,8 @@ async function statusCommand(options = {}) {
5080
4875
  status.info("Configuration:");
5081
4876
  status.info(` Project ID: ${config.project_id}`);
5082
4877
  status.info(` API URL: ${config.api_url}`);
5083
- status.info(` CLI Version: ${import_core10.VERSION}`);
5084
- status.info(` Config file: ${(0, import_core10.getConfigPath)()}`);
4878
+ status.info(` CLI Version: ${import_core8.VERSION}`);
4879
+ status.info(` Config file: ${(0, import_core8.getConfigPath)()}`);
5085
4880
  status.info("");
5086
4881
  if (!config.access_token || config.access_token === "") {
5087
4882
  status.warning("\u26A0 Not authenticated");
@@ -5241,9 +5036,83 @@ async function stopCommand(options = {}) {
5241
5036
  }
5242
5037
 
5243
5038
  // src/commands/clone.ts
5244
- var fs10 = __toESM(require("fs"));
5245
- var path11 = __toESM(require("path"));
5246
- var import_core11 = __toESM(require_dist());
5039
+ var fs9 = __toESM(require("fs"));
5040
+ var path10 = __toESM(require("path"));
5041
+ var import_child_process6 = require("child_process");
5042
+ var import_core10 = __toESM(require_dist());
5043
+
5044
+ // src/daemon/project-tracker.ts
5045
+ var fs8 = __toESM(require("fs"));
5046
+ var path9 = __toESM(require("path"));
5047
+ var import_core9 = __toESM(require_dist());
5048
+ function getProjectsFilePath() {
5049
+ return path9.join((0, import_core9.getConfigDir)(), "projects.json");
5050
+ }
5051
+ function readProjects() {
5052
+ const projectsPath = getProjectsFilePath();
5053
+ try {
5054
+ if (!fs8.existsSync(projectsPath)) {
5055
+ return { projects: [] };
5056
+ }
5057
+ const content = fs8.readFileSync(projectsPath, "utf-8");
5058
+ const data = JSON.parse(content);
5059
+ if (!data.projects || !Array.isArray(data.projects)) {
5060
+ console.warn("Invalid projects.json structure, resetting");
5061
+ return { projects: [] };
5062
+ }
5063
+ return data;
5064
+ } catch (error) {
5065
+ console.error("Error reading projects.json:", error);
5066
+ return { projects: [] };
5067
+ }
5068
+ }
5069
+ function writeProjects(data) {
5070
+ const projectsPath = getProjectsFilePath();
5071
+ try {
5072
+ const dir = path9.dirname(projectsPath);
5073
+ if (!fs8.existsSync(dir)) {
5074
+ fs8.mkdirSync(dir, { recursive: true });
5075
+ }
5076
+ fs8.writeFileSync(projectsPath, JSON.stringify(data, null, 2), "utf-8");
5077
+ } catch (error) {
5078
+ throw new Error(`Failed to write projects.json: ${error}`);
5079
+ }
5080
+ }
5081
+ function addProject2(projectId, projectPath, options) {
5082
+ const data = readProjects();
5083
+ const now = (/* @__PURE__ */ new Date()).toISOString();
5084
+ const existingByPath = data.projects.find((p) => p.path === projectPath);
5085
+ if (existingByPath) {
5086
+ existingByPath.id = projectId;
5087
+ existingByPath.last_active = now;
5088
+ if (options?.bareRepoPath) {
5089
+ existingByPath.bareRepoPath = options.bareRepoPath;
5090
+ }
5091
+ writeProjects(data);
5092
+ return existingByPath;
5093
+ }
5094
+ const existingByIdIndex = data.projects.findIndex((p) => p.id === projectId);
5095
+ if (existingByIdIndex !== -1) {
5096
+ const existingById = data.projects[existingByIdIndex];
5097
+ console.log(`[ProjectTracker] Replacing project entry: ${existingById.path} -> ${projectPath}`);
5098
+ data.projects.splice(existingByIdIndex, 1);
5099
+ }
5100
+ const projectName = path9.basename(projectPath);
5101
+ const newProject = {
5102
+ id: projectId,
5103
+ path: projectPath,
5104
+ name: projectName,
5105
+ added_at: now,
5106
+ last_active: now,
5107
+ // EP971: Bare repo path for worktree architecture
5108
+ bareRepoPath: options?.bareRepoPath
5109
+ };
5110
+ data.projects.push(newProject);
5111
+ writeProjects(data);
5112
+ return newProject;
5113
+ }
5114
+
5115
+ // src/commands/clone.ts
5247
5116
  async function cloneCommand(slugArg, options = {}) {
5248
5117
  const slugParts = slugArg.split("/");
5249
5118
  if (slugParts.length !== 2 || !slugParts[0] || !slugParts[1]) {
@@ -5254,7 +5123,7 @@ async function cloneCommand(slugArg, options = {}) {
5254
5123
  const [workspaceSlug, projectSlug] = slugParts;
5255
5124
  status.info(`Cloning ${workspaceSlug}/${projectSlug}...`);
5256
5125
  status.info("");
5257
- const config = await (0, import_core11.loadConfig)();
5126
+ const config = await (0, import_core10.loadConfig)();
5258
5127
  if (!config || !config.access_token) {
5259
5128
  throw new Error(
5260
5129
  "Not authenticated. Please run `episoda auth` first."
@@ -5262,9 +5131,9 @@ async function cloneCommand(slugArg, options = {}) {
5262
5131
  }
5263
5132
  const apiUrl = options.apiUrl || config.api_url || "https://episoda.dev";
5264
5133
  const projectPath = getProjectPath(workspaceSlug, projectSlug);
5265
- if (fs10.existsSync(projectPath)) {
5266
- const bareRepoPath = path11.join(projectPath, ".bare");
5267
- if (fs10.existsSync(bareRepoPath)) {
5134
+ if (fs9.existsSync(projectPath)) {
5135
+ const bareRepoPath = path10.join(projectPath, ".bare");
5136
+ if (fs9.existsSync(bareRepoPath)) {
5268
5137
  status.warning(`Project already cloned at ${projectPath}`);
5269
5138
  status.info("");
5270
5139
  status.info("Next steps:");
@@ -5273,7 +5142,7 @@ async function cloneCommand(slugArg, options = {}) {
5273
5142
  return;
5274
5143
  }
5275
5144
  throw new Error(
5276
- `Directory exists but is not a worktree project: ${projectPath}
5145
+ `Directory exists but is not an Episoda project: ${projectPath}
5277
5146
  Please remove it manually or use a different location.`
5278
5147
  );
5279
5148
  }
@@ -5296,7 +5165,7 @@ Please configure a repository in the project settings on episoda.dev.`
5296
5165
  status.info("");
5297
5166
  status.info("Creating project directory...");
5298
5167
  const episodaRoot = getEpisodaRoot();
5299
- fs10.mkdirSync(projectPath, { recursive: true });
5168
+ fs9.mkdirSync(projectPath, { recursive: true });
5300
5169
  status.success(`\u2713 Created ${projectPath}`);
5301
5170
  status.info("Cloning repository (bare)...");
5302
5171
  try {
@@ -5310,8 +5179,10 @@ Please configure a repository in the project settings on episoda.dev.`
5310
5179
  status.success("\u2713 Repository cloned");
5311
5180
  status.info("");
5312
5181
  const bareRepoPath = worktreeManager.getBareRepoPath();
5182
+ await extractBootstrapScripts(bareRepoPath, projectPath);
5183
+ status.success("\u2713 Bootstrap scripts extracted");
5184
+ status.info("");
5313
5185
  addProject2(projectDetails.id, projectPath, {
5314
- worktreeMode: true,
5315
5186
  bareRepoPath
5316
5187
  });
5317
5188
  status.success("\u2713 Project registered with daemon");
@@ -5320,18 +5191,21 @@ Please configure a repository in the project settings on episoda.dev.`
5320
5191
  status.info("");
5321
5192
  status.info("Project structure:");
5322
5193
  status.info(` ${projectPath}/`);
5323
- status.info(" \u251C\u2500\u2500 .bare/ # Git repository");
5194
+ status.info(" \u251C\u2500\u2500 .bare/ # Git repository");
5324
5195
  status.info(" \u2514\u2500\u2500 .episoda/");
5325
- status.info(" \u2514\u2500\u2500 config.json # Project config");
5196
+ status.info(" \u251C\u2500\u2500 config.json # Project config");
5197
+ status.info(" \u2514\u2500\u2500 scripts/");
5198
+ status.info(" \u2514\u2500\u2500 api-helper.sh # API authentication");
5326
5199
  status.info("");
5327
5200
  status.info("Next steps:");
5328
5201
  status.info(` 1. cd ${projectPath}`);
5329
- status.info(" 2. episoda checkout {moduleUid} # e.g., episoda checkout EP100");
5202
+ status.info(" 2. source .episoda/scripts/api-helper.sh # Load API helpers");
5203
+ status.info(" 3. episoda checkout {moduleUid} # e.g., episoda checkout EP100");
5330
5204
  status.info("");
5331
5205
  } catch (error) {
5332
- if (fs10.existsSync(projectPath)) {
5206
+ if (fs9.existsSync(projectPath)) {
5333
5207
  try {
5334
- fs10.rmSync(projectPath, { recursive: true, force: true });
5208
+ fs9.rmSync(projectPath, { recursive: true, force: true });
5335
5209
  } catch {
5336
5210
  }
5337
5211
  }
@@ -5404,9 +5278,23 @@ ${errorBody}`
5404
5278
  }
5405
5279
  return data;
5406
5280
  }
5281
+ async function extractBootstrapScripts(bareRepoPath, projectPath) {
5282
+ const scriptsDir = path10.join(projectPath, ".episoda", "scripts");
5283
+ fs9.mkdirSync(scriptsDir, { recursive: true });
5284
+ try {
5285
+ const scriptContent = (0, import_child_process6.execSync)(
5286
+ `git --git-dir="${bareRepoPath}" show main:scripts/api-helper.sh`,
5287
+ { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
5288
+ );
5289
+ const scriptPath = path10.join(scriptsDir, "api-helper.sh");
5290
+ fs9.writeFileSync(scriptPath, scriptContent, { mode: 493 });
5291
+ } catch (error) {
5292
+ console.log("[clone] Could not extract api-helper.sh:", error);
5293
+ }
5294
+ }
5407
5295
 
5408
5296
  // src/commands/checkout.ts
5409
- var import_core12 = __toESM(require_dist());
5297
+ var import_core11 = __toESM(require_dist());
5410
5298
 
5411
5299
  // src/utils/http.ts
5412
5300
  async function fetchWithRetry(url, options, maxRetries = 3) {
@@ -5446,7 +5334,7 @@ async function checkoutCommand(moduleUid, options = {}) {
5446
5334
  "Not in a worktree project.\nRun this command from within a project cloned with `episoda clone`.\nOr cd to ~/episoda/{workspace}/{project}/"
5447
5335
  );
5448
5336
  }
5449
- const config = await (0, import_core12.loadConfig)();
5337
+ const config = await (0, import_core11.loadConfig)();
5450
5338
  if (!config || !config.access_token) {
5451
5339
  throw new Error(
5452
5340
  "Not authenticated. Please run `episoda auth` first."
@@ -5485,8 +5373,8 @@ The .bare directory or .episoda/config.json may be missing.`
5485
5373
  const branchName = moduleDetails.branch_name || `feature/${moduleUid.toLowerCase()}`;
5486
5374
  let createBranch = !moduleDetails.branch_name || options.create;
5487
5375
  if (createBranch && !moduleDetails.branch_name) {
5488
- const { GitExecutor: GitExecutor6 } = await Promise.resolve().then(() => __toESM(require_dist()));
5489
- const gitExecutor = new GitExecutor6();
5376
+ const { GitExecutor: GitExecutor4 } = await Promise.resolve().then(() => __toESM(require_dist()));
5377
+ const gitExecutor = new GitExecutor4();
5490
5378
  const branchCheck = await gitExecutor.execute(
5491
5379
  { action: "branch_exists", branch: branchName },
5492
5380
  { cwd: worktreeManager.getBareRepoPath() }
@@ -5583,8 +5471,8 @@ async function updateModuleCheckout(apiUrl, moduleId, accessToken, branchName) {
5583
5471
  }
5584
5472
 
5585
5473
  // src/commands/release.ts
5586
- var path12 = __toESM(require("path"));
5587
- var import_core13 = __toESM(require_dist());
5474
+ var path11 = __toESM(require("path"));
5475
+ var import_core12 = __toESM(require_dist());
5588
5476
  async function releaseCommand(moduleUid, options = {}) {
5589
5477
  if (!moduleUid || !moduleUid.match(/^EP\d+$/)) {
5590
5478
  throw new Error(
@@ -5630,12 +5518,24 @@ Commit or stash your changes first, or use --force to discard them.`
5630
5518
  );
5631
5519
  }
5632
5520
  }
5633
- const currentPath = path12.resolve(process.cwd());
5521
+ const currentPath = path11.resolve(process.cwd());
5634
5522
  if (currentPath.startsWith(existing.worktreePath)) {
5635
5523
  status.warning("You are inside the worktree being released.");
5636
5524
  status.info(`Please cd to ${projectRoot} first.`);
5637
5525
  throw new Error("Cannot release worktree while inside it");
5638
5526
  }
5527
+ const config = await (0, import_core12.loadConfig)();
5528
+ const cleanupScript = config?.project_settings?.worktree_cleanup_script;
5529
+ if (cleanupScript) {
5530
+ status.info("Running cleanup script...");
5531
+ const cleanupResult = await worktreeManager.runCleanupScript(moduleUid, cleanupScript);
5532
+ if (!cleanupResult.success) {
5533
+ status.warning(`Cleanup script failed: ${cleanupResult.error}`);
5534
+ status.info("Continuing with worktree removal...");
5535
+ } else {
5536
+ status.success("\u2713 Cleanup script completed");
5537
+ }
5538
+ }
5639
5539
  status.info("Removing worktree...");
5640
5540
  const result = await worktreeManager.removeWorktree(moduleUid, options.force);
5641
5541
  if (!result.success) {
@@ -5644,7 +5544,6 @@ Commit or stash your changes first, or use --force to discard them.`
5644
5544
  status.success("\u2713 Worktree removed");
5645
5545
  status.info("");
5646
5546
  try {
5647
- const config = await (0, import_core13.loadConfig)();
5648
5547
  if (config?.access_token) {
5649
5548
  const worktreeConfig = worktreeManager.getConfig();
5650
5549
  if (worktreeConfig) {
@@ -5668,7 +5567,7 @@ Commit or stash your changes first, or use --force to discard them.`
5668
5567
  status.info("");
5669
5568
  }
5670
5569
  async function checkUncommittedChanges(worktreePath) {
5671
- const gitExecutor = new import_core13.GitExecutor();
5570
+ const gitExecutor = new import_core12.GitExecutor();
5672
5571
  const result = await gitExecutor.execute(
5673
5572
  { action: "status" },
5674
5573
  { cwd: worktreePath }
@@ -5693,10 +5592,10 @@ async function updateModuleRelease(apiUrl, projectId, workspaceSlug, moduleUid,
5693
5592
  }
5694
5593
 
5695
5594
  // src/commands/list.ts
5696
- var fs11 = __toESM(require("fs"));
5697
- var path13 = __toESM(require("path"));
5595
+ var fs10 = __toESM(require("fs"));
5596
+ var path12 = __toESM(require("path"));
5698
5597
  var import_chalk2 = __toESM(require("chalk"));
5699
- var import_core14 = __toESM(require_dist());
5598
+ var import_core13 = __toESM(require_dist());
5700
5599
  async function listCommand(subcommand, options = {}) {
5701
5600
  if (subcommand === "worktrees" || subcommand === "wt") {
5702
5601
  await listWorktrees(options);
@@ -5706,7 +5605,7 @@ async function listCommand(subcommand, options = {}) {
5706
5605
  }
5707
5606
  async function listProjects(options) {
5708
5607
  const episodaRoot = getEpisodaRoot();
5709
- if (!fs11.existsSync(episodaRoot)) {
5608
+ if (!fs10.existsSync(episodaRoot)) {
5710
5609
  status.info("No projects cloned yet.");
5711
5610
  status.info("");
5712
5611
  status.info("Clone a project with:");
@@ -5714,12 +5613,12 @@ async function listProjects(options) {
5714
5613
  return;
5715
5614
  }
5716
5615
  const projects = [];
5717
- const workspaces = fs11.readdirSync(episodaRoot, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith("."));
5616
+ const workspaces = fs10.readdirSync(episodaRoot, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith("."));
5718
5617
  for (const workspace of workspaces) {
5719
- const workspacePath = path13.join(episodaRoot, workspace.name);
5720
- const projectDirs = fs11.readdirSync(workspacePath, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith("."));
5618
+ const workspacePath = path12.join(episodaRoot, workspace.name);
5619
+ const projectDirs = fs10.readdirSync(workspacePath, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith("."));
5721
5620
  for (const projectDir of projectDirs) {
5722
- const projectPath = path13.join(workspacePath, projectDir.name);
5621
+ const projectPath = path12.join(workspacePath, projectDir.name);
5723
5622
  if (await isWorktreeProject(projectPath)) {
5724
5623
  const manager = new WorktreeManager(projectPath);
5725
5624
  await manager.initialize();
@@ -5784,7 +5683,7 @@ async function listWorktrees(options) {
5784
5683
  status.info(" episoda checkout {moduleUid}");
5785
5684
  return;
5786
5685
  }
5787
- const gitExecutor = new import_core14.GitExecutor();
5686
+ const gitExecutor = new import_core13.GitExecutor();
5788
5687
  for (const wt of worktrees) {
5789
5688
  console.log("");
5790
5689
  let statusIndicator = import_chalk2.default.green("\u25CF");
@@ -5821,7 +5720,7 @@ async function listWorktrees(options) {
5821
5720
  }
5822
5721
 
5823
5722
  // src/index.ts
5824
- import_commander.program.name("episoda").description("Episoda local development workflow orchestration").version(import_core15.VERSION);
5723
+ import_commander.program.name("episoda").description("Episoda CLI - local development with git worktree isolation").version(import_core14.VERSION);
5825
5724
  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) => {
5826
5725
  try {
5827
5726
  await authCommand(options);
@@ -5838,7 +5737,7 @@ import_commander.program.command("connect").description("Connect using a pre-aut
5838
5737
  process.exit(1);
5839
5738
  }
5840
5739
  });
5841
- import_commander.program.command("dev").description("Start dev server with Episoda orchestration").option("--auto-restart", "Auto-restart dev server if it crashes").option("--connection-only", "Connection-only mode (don't start dev server)").allowUnknownOption().action(async (options, cmd) => {
5740
+ import_commander.program.command("dev").description("Connect to Episoda and start dev server").option("--auto-restart", "Auto-restart dev server if it crashes").option("--connection-only", "Connection-only mode (don't start dev server)").allowUnknownOption().action(async (options, cmd) => {
5842
5741
  try {
5843
5742
  const args = process.argv.slice(process.argv.indexOf("dev") + 1);
5844
5743
  const separatorIndex = args.indexOf("--");
@@ -5877,7 +5776,7 @@ import_commander.program.command("disconnect").description("Disconnect from epis
5877
5776
  process.exit(1);
5878
5777
  }
5879
5778
  });
5880
- 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) => {
5779
+ import_commander.program.command("clone").description("Clone a project 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) => {
5881
5780
  try {
5882
5781
  await cloneCommand(slug, options);
5883
5782
  } catch (error) {
@@ -5909,14 +5808,6 @@ import_commander.program.command("list").description("List cloned projects and w
5909
5808
  process.exit(1);
5910
5809
  }
5911
5810
  });
5912
- 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) => {
5913
- try {
5914
- await migrateCommand(options);
5915
- } catch (error) {
5916
- status.error(`Migration failed: ${error instanceof Error ? error.message : String(error)}`);
5917
- process.exit(1);
5918
- }
5919
- });
5920
5811
  import_commander.program.command("dev:restart").description("Restart the dev server for a module").argument("[moduleUid]", "Module UID (required)").action(async (moduleUid) => {
5921
5812
  try {
5922
5813
  if (!moduleUid) {