episoda 0.2.31 → 0.2.33

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.
@@ -1635,6 +1635,9 @@ var require_git_executor = __commonJS({
1635
1635
  const args = ["worktree", "add"];
1636
1636
  if (command.create) {
1637
1637
  args.push("-b", command.branch, command.path);
1638
+ if (command.startPoint) {
1639
+ args.push(command.startPoint);
1640
+ }
1638
1641
  } else {
1639
1642
  args.push(command.path, command.branch);
1640
1643
  }
@@ -2696,7 +2699,7 @@ var require_package = __commonJS({
2696
2699
  "package.json"(exports2, module2) {
2697
2700
  module2.exports = {
2698
2701
  name: "episoda",
2699
- version: "0.2.30",
2702
+ version: "0.2.32",
2700
2703
  description: "CLI tool for Episoda local development workflow orchestration",
2701
2704
  main: "dist/index.js",
2702
2705
  types: "dist/index.d.ts",
@@ -5533,14 +5536,19 @@ var WorktreeManager = class _WorktreeManager {
5533
5536
  action: "fetch",
5534
5537
  remote: "origin"
5535
5538
  }, { cwd: this.bareRepoPath });
5536
- if (!fetchResult.success) {
5537
- console.warn("[worktree-manager] Failed to fetch from origin:", fetchResult.output);
5539
+ if (!fetchResult.success && createBranch) {
5540
+ console.error("[worktree-manager] Failed to fetch from origin:", fetchResult.output);
5541
+ return {
5542
+ success: false,
5543
+ error: "Failed to fetch latest refs from origin. Cannot create worktree with stale refs."
5544
+ };
5538
5545
  }
5539
5546
  const result = await this.gitExecutor.execute({
5540
5547
  action: "worktree_add",
5541
5548
  path: worktreePath,
5542
5549
  branch: branchName,
5543
- create: createBranch
5550
+ create: createBranch,
5551
+ startPoint: createBranch ? "origin/main" : void 0
5544
5552
  }, { cwd: this.bareRepoPath });
5545
5553
  if (!result.success) {
5546
5554
  return {
@@ -7150,6 +7158,9 @@ var Daemon = class _Daemon {
7150
7158
  this.syncProjectSettings(projectId).catch((err) => {
7151
7159
  console.warn("[Daemon] EP964: Settings sync failed:", err.message);
7152
7160
  });
7161
+ this.syncMachineProjectPath(projectId, projectPath).catch((err) => {
7162
+ console.warn("[Daemon] EP995: Project path sync failed:", err.message);
7163
+ });
7153
7164
  this.autoStartTunnelsForProject(projectPath, projectId).catch((error) => {
7154
7165
  console.error(`[Daemon] EP819: Failed to auto-start tunnels:`, error);
7155
7166
  });
@@ -7160,6 +7171,9 @@ var Daemon = class _Daemon {
7160
7171
  }).catch((err) => {
7161
7172
  console.warn("[Daemon] EP950: Cleanup on connect failed:", err.message);
7162
7173
  });
7174
+ this.reconcileWorktrees(projectId, projectPath).catch((err) => {
7175
+ console.warn("[Daemon] EP995: Reconciliation failed:", err.message);
7176
+ });
7163
7177
  });
7164
7178
  client.on("module_state_changed", async (message) => {
7165
7179
  if (message.type === "module_state_changed") {
@@ -7243,6 +7257,7 @@ var Daemon = class _Daemon {
7243
7257
  {
7244
7258
  console.log(`[Daemon] EP986: Starting async worktree setup for ${moduleUid}${hasSetupConfig ? " (with config)" : " (for dependency installation)"}`);
7245
7259
  await worktreeManager.updateWorktreeStatus(moduleUid, "pending");
7260
+ await this.updateModuleWorktreeStatus(moduleUid, "pending", worktree.path);
7246
7261
  this.runWorktreeSetupAsync(
7247
7262
  moduleUid,
7248
7263
  worktreeManager,
@@ -7575,6 +7590,189 @@ var Daemon = class _Daemon {
7575
7590
  console.warn("[Daemon] EP964: Failed to sync project settings:", error instanceof Error ? error.message : error);
7576
7591
  }
7577
7592
  }
7593
+ /**
7594
+ * EP995: Sync project path to server (local_machine.project_paths)
7595
+ *
7596
+ * Reports the local filesystem path for this project to the server,
7597
+ * enabling server-side visibility into where projects are checked out.
7598
+ * Uses atomic RPC to prevent race conditions.
7599
+ */
7600
+ async syncMachineProjectPath(projectId, projectPath) {
7601
+ try {
7602
+ if (!this.deviceId) {
7603
+ console.warn("[Daemon] EP995: Cannot sync project path - deviceId not available");
7604
+ return;
7605
+ }
7606
+ const config = await (0, import_core10.loadConfig)();
7607
+ if (!config) return;
7608
+ const apiUrl = config.api_url || "https://episoda.dev";
7609
+ const response = await fetchWithAuth(`${apiUrl}/api/account/machines/${this.deviceId}`, {
7610
+ method: "PATCH",
7611
+ headers: {
7612
+ "Content-Type": "application/json"
7613
+ },
7614
+ body: JSON.stringify({
7615
+ project_id: projectId,
7616
+ project_path: projectPath
7617
+ })
7618
+ });
7619
+ if (!response.ok) {
7620
+ const errorData = await response.json().catch(() => ({}));
7621
+ console.warn(`[Daemon] EP995: Failed to sync project path: ${response.status}`, errorData);
7622
+ return;
7623
+ }
7624
+ console.log(`[Daemon] EP995: Synced project path to server: ${projectPath}`);
7625
+ } catch (error) {
7626
+ console.warn("[Daemon] EP995: Failed to sync project path:", error instanceof Error ? error.message : error);
7627
+ }
7628
+ }
7629
+ /**
7630
+ * EP995: Update module worktree status on server
7631
+ *
7632
+ * Reports worktree setup progress to the server, enabling:
7633
+ * - Server-side visibility into worktree state
7634
+ * - UI progress display (compatible with EP978)
7635
+ * - Reconciliation queries on daemon reconnect
7636
+ */
7637
+ async updateModuleWorktreeStatus(moduleUid, status, worktreePath, errorMessage) {
7638
+ try {
7639
+ const config = await (0, import_core10.loadConfig)();
7640
+ if (!config) return;
7641
+ const apiUrl = config.api_url || "https://episoda.dev";
7642
+ const body = {
7643
+ worktree_status: status
7644
+ };
7645
+ if (worktreePath) {
7646
+ body.worktree_path = worktreePath;
7647
+ }
7648
+ if (status === "error" && errorMessage) {
7649
+ body.worktree_error = errorMessage;
7650
+ }
7651
+ if (status !== "error") {
7652
+ body.worktree_error = null;
7653
+ }
7654
+ const response = await fetchWithAuth(`${apiUrl}/api/modules/${moduleUid}`, {
7655
+ method: "PATCH",
7656
+ headers: {
7657
+ "Content-Type": "application/json"
7658
+ },
7659
+ body: JSON.stringify(body)
7660
+ });
7661
+ if (!response.ok) {
7662
+ const errorData = await response.json().catch(() => ({}));
7663
+ console.warn(`[Daemon] EP995: Failed to update worktree status: ${response.status}`, errorData);
7664
+ return;
7665
+ }
7666
+ console.log(`[Daemon] EP995: Updated module ${moduleUid} worktree_status=${status}`);
7667
+ } catch (error) {
7668
+ console.warn("[Daemon] EP995: Failed to update worktree status:", error instanceof Error ? error.message : error);
7669
+ }
7670
+ }
7671
+ /**
7672
+ * EP995: Reconcile worktrees on daemon connect/reconnect
7673
+ *
7674
+ * Figma-style "fresh snapshot on reconnect" approach:
7675
+ * 1. Query server for modules that should have worktrees on this machine
7676
+ * 2. For modules missing local worktrees, create and setup
7677
+ * 3. Log orphaned worktrees (local exists but module not in doing/review)
7678
+ *
7679
+ * This self-healing mechanism catches modules that transitioned
7680
+ * while the daemon was disconnected.
7681
+ */
7682
+ async reconcileWorktrees(projectId, projectPath) {
7683
+ console.log(`[Daemon] EP995: Starting worktree reconciliation for project ${projectId}`);
7684
+ try {
7685
+ if (!this.deviceId) {
7686
+ console.log("[Daemon] EP995: Cannot reconcile - deviceId not available yet");
7687
+ return;
7688
+ }
7689
+ const config = await (0, import_core10.loadConfig)();
7690
+ if (!config) return;
7691
+ const apiUrl = config.api_url || "https://episoda.dev";
7692
+ const modulesResponse = await fetchWithAuth(
7693
+ `${apiUrl}/api/modules?state=doing,review&dev_mode=local&checkout_machine_id=${this.deviceId}&project_id=${projectId}`
7694
+ );
7695
+ if (!modulesResponse.ok) {
7696
+ console.warn(`[Daemon] EP995: Failed to fetch modules for reconciliation: ${modulesResponse.status}`);
7697
+ return;
7698
+ }
7699
+ const modulesData = await modulesResponse.json();
7700
+ const modules = modulesData.modules || [];
7701
+ if (modules.length === 0) {
7702
+ console.log("[Daemon] EP995: No modules need reconciliation");
7703
+ return;
7704
+ }
7705
+ console.log(`[Daemon] EP995: Found ${modules.length} module(s) to check`);
7706
+ const worktreeManager = new WorktreeManager(projectPath);
7707
+ const initialized = await worktreeManager.initialize();
7708
+ if (!initialized) {
7709
+ console.error(`[Daemon] EP995: Failed to initialize WorktreeManager`);
7710
+ return;
7711
+ }
7712
+ for (const module2 of modules) {
7713
+ const moduleUid = module2.uid;
7714
+ const branchName = module2.branch_name;
7715
+ const worktree = await getWorktreeInfoForModule(moduleUid);
7716
+ if (!worktree?.exists) {
7717
+ console.log(`[Daemon] EP995: Module ${moduleUid} missing local worktree - creating...`);
7718
+ const moduleBranchName = branchName || moduleUid;
7719
+ const createResult = await worktreeManager.createWorktree(moduleUid, moduleBranchName, true);
7720
+ if (!createResult.success) {
7721
+ console.error(`[Daemon] EP995: Failed to create worktree for ${moduleUid}: ${createResult.error}`);
7722
+ continue;
7723
+ }
7724
+ console.log(`[Daemon] EP995: Created worktree for ${moduleUid} at ${createResult.worktreePath}`);
7725
+ if (this.deviceId) {
7726
+ try {
7727
+ await fetchWithAuth(`${apiUrl}/api/modules/${moduleUid}`, {
7728
+ method: "PATCH",
7729
+ body: JSON.stringify({ checkout_machine_id: this.deviceId })
7730
+ });
7731
+ console.log(`[Daemon] EP995: Claimed ownership of ${moduleUid}`);
7732
+ } catch (ownershipError) {
7733
+ console.warn(`[Daemon] EP995: Failed to claim ownership of ${moduleUid}`);
7734
+ }
7735
+ }
7736
+ const newWorktree = await getWorktreeInfoForModule(moduleUid);
7737
+ if (!newWorktree?.exists) {
7738
+ console.error(`[Daemon] EP995: Worktree still not found after creation`);
7739
+ continue;
7740
+ }
7741
+ const setupConfig = config.project_settings;
7742
+ const envVars = await fetchEnvVars();
7743
+ console.log(`[Daemon] EP995: Starting setup for reconciled module ${moduleUid}`);
7744
+ await worktreeManager.updateWorktreeStatus(moduleUid, "pending");
7745
+ await this.updateModuleWorktreeStatus(moduleUid, "pending", newWorktree.path);
7746
+ this.runWorktreeSetupAsync(
7747
+ moduleUid,
7748
+ worktreeManager,
7749
+ setupConfig?.worktree_copy_files || [],
7750
+ setupConfig?.worktree_setup_script,
7751
+ newWorktree.path,
7752
+ envVars
7753
+ ).then(() => {
7754
+ console.log(`[Daemon] EP995: Setup complete for reconciled ${moduleUid}`);
7755
+ this.startTunnelForModule(moduleUid, newWorktree.path);
7756
+ }).catch((err) => {
7757
+ console.error(`[Daemon] EP995: Setup failed for reconciled ${moduleUid}:`, err);
7758
+ });
7759
+ } else {
7760
+ const tunnelManager = getTunnelManager();
7761
+ await tunnelManager.initialize();
7762
+ if (!tunnelManager.hasTunnel(moduleUid)) {
7763
+ console.log(`[Daemon] EP995: Module ${moduleUid} has worktree but no tunnel - starting...`);
7764
+ await this.startTunnelForModule(moduleUid, worktree.path);
7765
+ } else {
7766
+ console.log(`[Daemon] EP995: Module ${moduleUid} OK - worktree and tunnel exist`);
7767
+ }
7768
+ }
7769
+ }
7770
+ console.log("[Daemon] EP995: Reconciliation complete");
7771
+ } catch (error) {
7772
+ console.error("[Daemon] EP995: Reconciliation error:", error instanceof Error ? error.message : error);
7773
+ throw error;
7774
+ }
7775
+ }
7578
7776
  /**
7579
7777
  * EP956: Cleanup module worktree when module moves to done
7580
7778
  *
@@ -7614,6 +7812,22 @@ var Daemon = class _Daemon {
7614
7812
  } else {
7615
7813
  console.log(`[Daemon] EP994: No worktree to remove for ${moduleUid}`);
7616
7814
  }
7815
+ try {
7816
+ const cleanupConfig = await (0, import_core10.loadConfig)();
7817
+ const cleanupApiUrl = cleanupConfig?.api_url || "https://episoda.dev";
7818
+ await fetchWithAuth(`${cleanupApiUrl}/api/modules/${moduleUid}`, {
7819
+ method: "PATCH",
7820
+ headers: { "Content-Type": "application/json" },
7821
+ body: JSON.stringify({
7822
+ worktree_status: null,
7823
+ worktree_path: null,
7824
+ worktree_error: null
7825
+ })
7826
+ });
7827
+ console.log(`[Daemon] EP995: Cleared worktree status for ${moduleUid}`);
7828
+ } catch (clearError) {
7829
+ console.warn(`[Daemon] EP995: Failed to clear worktree status for ${moduleUid}:`, clearError);
7830
+ }
7617
7831
  console.log(`[Daemon] EP956: Async cleanup complete for ${moduleUid}`);
7618
7832
  } catch (error) {
7619
7833
  console.error(`[Daemon] EP956: Cleanup error for ${moduleUid}:`, error instanceof Error ? error.message : error);
@@ -7629,6 +7843,7 @@ var Daemon = class _Daemon {
7629
7843
  console.log(`[Daemon] EP959: Running async worktree setup for ${moduleUid}`);
7630
7844
  try {
7631
7845
  await worktreeManager.updateWorktreeStatus(moduleUid, "running");
7846
+ await this.updateModuleWorktreeStatus(moduleUid, "setup", worktreePath);
7632
7847
  if (Object.keys(envVars).length > 0) {
7633
7848
  console.log(`[Daemon] EP988: Writing .env with ${Object.keys(envVars).length} variables to ${moduleUid}`);
7634
7849
  writeEnvFile(worktreePath, envVars);
@@ -7672,11 +7887,13 @@ var Daemon = class _Daemon {
7672
7887
  }
7673
7888
  }
7674
7889
  await worktreeManager.updateWorktreeStatus(moduleUid, "ready");
7890
+ await this.updateModuleWorktreeStatus(moduleUid, "ready", worktreePath);
7675
7891
  console.log(`[Daemon] EP959: Worktree setup complete for ${moduleUid}`);
7676
7892
  } catch (error) {
7677
7893
  const errorMessage = error instanceof Error ? error.message : String(error);
7678
7894
  console.error(`[Daemon] EP959: Worktree setup failed for ${moduleUid}:`, errorMessage);
7679
7895
  await worktreeManager.updateWorktreeStatus(moduleUid, "error", errorMessage);
7896
+ await this.updateModuleWorktreeStatus(moduleUid, "error", worktreePath, errorMessage);
7680
7897
  throw error;
7681
7898
  }
7682
7899
  }