episoda 0.2.31 → 0.2.32

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.
@@ -2696,7 +2696,7 @@ var require_package = __commonJS({
2696
2696
  "package.json"(exports2, module2) {
2697
2697
  module2.exports = {
2698
2698
  name: "episoda",
2699
- version: "0.2.30",
2699
+ version: "0.2.31",
2700
2700
  description: "CLI tool for Episoda local development workflow orchestration",
2701
2701
  main: "dist/index.js",
2702
2702
  types: "dist/index.d.ts",
@@ -7150,6 +7150,9 @@ var Daemon = class _Daemon {
7150
7150
  this.syncProjectSettings(projectId).catch((err) => {
7151
7151
  console.warn("[Daemon] EP964: Settings sync failed:", err.message);
7152
7152
  });
7153
+ this.syncMachineProjectPath(projectId, projectPath).catch((err) => {
7154
+ console.warn("[Daemon] EP995: Project path sync failed:", err.message);
7155
+ });
7153
7156
  this.autoStartTunnelsForProject(projectPath, projectId).catch((error) => {
7154
7157
  console.error(`[Daemon] EP819: Failed to auto-start tunnels:`, error);
7155
7158
  });
@@ -7160,6 +7163,9 @@ var Daemon = class _Daemon {
7160
7163
  }).catch((err) => {
7161
7164
  console.warn("[Daemon] EP950: Cleanup on connect failed:", err.message);
7162
7165
  });
7166
+ this.reconcileWorktrees(projectId, projectPath).catch((err) => {
7167
+ console.warn("[Daemon] EP995: Reconciliation failed:", err.message);
7168
+ });
7163
7169
  });
7164
7170
  client.on("module_state_changed", async (message) => {
7165
7171
  if (message.type === "module_state_changed") {
@@ -7243,6 +7249,7 @@ var Daemon = class _Daemon {
7243
7249
  {
7244
7250
  console.log(`[Daemon] EP986: Starting async worktree setup for ${moduleUid}${hasSetupConfig ? " (with config)" : " (for dependency installation)"}`);
7245
7251
  await worktreeManager.updateWorktreeStatus(moduleUid, "pending");
7252
+ await this.updateModuleWorktreeStatus(moduleUid, "pending", worktree.path);
7246
7253
  this.runWorktreeSetupAsync(
7247
7254
  moduleUid,
7248
7255
  worktreeManager,
@@ -7575,6 +7582,189 @@ var Daemon = class _Daemon {
7575
7582
  console.warn("[Daemon] EP964: Failed to sync project settings:", error instanceof Error ? error.message : error);
7576
7583
  }
7577
7584
  }
7585
+ /**
7586
+ * EP995: Sync project path to server (local_machine.project_paths)
7587
+ *
7588
+ * Reports the local filesystem path for this project to the server,
7589
+ * enabling server-side visibility into where projects are checked out.
7590
+ * Uses atomic RPC to prevent race conditions.
7591
+ */
7592
+ async syncMachineProjectPath(projectId, projectPath) {
7593
+ try {
7594
+ if (!this.deviceId) {
7595
+ console.warn("[Daemon] EP995: Cannot sync project path - deviceId not available");
7596
+ return;
7597
+ }
7598
+ const config = await (0, import_core10.loadConfig)();
7599
+ if (!config) return;
7600
+ const apiUrl = config.api_url || "https://episoda.dev";
7601
+ const response = await fetchWithAuth(`${apiUrl}/api/account/machines/${this.deviceId}`, {
7602
+ method: "PATCH",
7603
+ headers: {
7604
+ "Content-Type": "application/json"
7605
+ },
7606
+ body: JSON.stringify({
7607
+ project_id: projectId,
7608
+ project_path: projectPath
7609
+ })
7610
+ });
7611
+ if (!response.ok) {
7612
+ const errorData = await response.json().catch(() => ({}));
7613
+ console.warn(`[Daemon] EP995: Failed to sync project path: ${response.status}`, errorData);
7614
+ return;
7615
+ }
7616
+ console.log(`[Daemon] EP995: Synced project path to server: ${projectPath}`);
7617
+ } catch (error) {
7618
+ console.warn("[Daemon] EP995: Failed to sync project path:", error instanceof Error ? error.message : error);
7619
+ }
7620
+ }
7621
+ /**
7622
+ * EP995: Update module worktree status on server
7623
+ *
7624
+ * Reports worktree setup progress to the server, enabling:
7625
+ * - Server-side visibility into worktree state
7626
+ * - UI progress display (compatible with EP978)
7627
+ * - Reconciliation queries on daemon reconnect
7628
+ */
7629
+ async updateModuleWorktreeStatus(moduleUid, status, worktreePath, errorMessage) {
7630
+ try {
7631
+ const config = await (0, import_core10.loadConfig)();
7632
+ if (!config) return;
7633
+ const apiUrl = config.api_url || "https://episoda.dev";
7634
+ const body = {
7635
+ worktree_status: status
7636
+ };
7637
+ if (worktreePath) {
7638
+ body.worktree_path = worktreePath;
7639
+ }
7640
+ if (status === "error" && errorMessage) {
7641
+ body.worktree_error = errorMessage;
7642
+ }
7643
+ if (status !== "error") {
7644
+ body.worktree_error = null;
7645
+ }
7646
+ const response = await fetchWithAuth(`${apiUrl}/api/modules/${moduleUid}`, {
7647
+ method: "PATCH",
7648
+ headers: {
7649
+ "Content-Type": "application/json"
7650
+ },
7651
+ body: JSON.stringify(body)
7652
+ });
7653
+ if (!response.ok) {
7654
+ const errorData = await response.json().catch(() => ({}));
7655
+ console.warn(`[Daemon] EP995: Failed to update worktree status: ${response.status}`, errorData);
7656
+ return;
7657
+ }
7658
+ console.log(`[Daemon] EP995: Updated module ${moduleUid} worktree_status=${status}`);
7659
+ } catch (error) {
7660
+ console.warn("[Daemon] EP995: Failed to update worktree status:", error instanceof Error ? error.message : error);
7661
+ }
7662
+ }
7663
+ /**
7664
+ * EP995: Reconcile worktrees on daemon connect/reconnect
7665
+ *
7666
+ * Figma-style "fresh snapshot on reconnect" approach:
7667
+ * 1. Query server for modules that should have worktrees on this machine
7668
+ * 2. For modules missing local worktrees, create and setup
7669
+ * 3. Log orphaned worktrees (local exists but module not in doing/review)
7670
+ *
7671
+ * This self-healing mechanism catches modules that transitioned
7672
+ * while the daemon was disconnected.
7673
+ */
7674
+ async reconcileWorktrees(projectId, projectPath) {
7675
+ console.log(`[Daemon] EP995: Starting worktree reconciliation for project ${projectId}`);
7676
+ try {
7677
+ if (!this.deviceId) {
7678
+ console.log("[Daemon] EP995: Cannot reconcile - deviceId not available yet");
7679
+ return;
7680
+ }
7681
+ const config = await (0, import_core10.loadConfig)();
7682
+ if (!config) return;
7683
+ const apiUrl = config.api_url || "https://episoda.dev";
7684
+ const modulesResponse = await fetchWithAuth(
7685
+ `${apiUrl}/api/modules?state=doing,review&dev_mode=local&checkout_machine_id=${this.deviceId}&project_id=${projectId}`
7686
+ );
7687
+ if (!modulesResponse.ok) {
7688
+ console.warn(`[Daemon] EP995: Failed to fetch modules for reconciliation: ${modulesResponse.status}`);
7689
+ return;
7690
+ }
7691
+ const modulesData = await modulesResponse.json();
7692
+ const modules = modulesData.modules || [];
7693
+ if (modules.length === 0) {
7694
+ console.log("[Daemon] EP995: No modules need reconciliation");
7695
+ return;
7696
+ }
7697
+ console.log(`[Daemon] EP995: Found ${modules.length} module(s) to check`);
7698
+ const worktreeManager = new WorktreeManager(projectPath);
7699
+ const initialized = await worktreeManager.initialize();
7700
+ if (!initialized) {
7701
+ console.error(`[Daemon] EP995: Failed to initialize WorktreeManager`);
7702
+ return;
7703
+ }
7704
+ for (const module2 of modules) {
7705
+ const moduleUid = module2.uid;
7706
+ const branchName = module2.branch_name;
7707
+ const worktree = await getWorktreeInfoForModule(moduleUid);
7708
+ if (!worktree?.exists) {
7709
+ console.log(`[Daemon] EP995: Module ${moduleUid} missing local worktree - creating...`);
7710
+ const moduleBranchName = branchName || moduleUid;
7711
+ const createResult = await worktreeManager.createWorktree(moduleUid, moduleBranchName, true);
7712
+ if (!createResult.success) {
7713
+ console.error(`[Daemon] EP995: Failed to create worktree for ${moduleUid}: ${createResult.error}`);
7714
+ continue;
7715
+ }
7716
+ console.log(`[Daemon] EP995: Created worktree for ${moduleUid} at ${createResult.worktreePath}`);
7717
+ if (this.deviceId) {
7718
+ try {
7719
+ await fetchWithAuth(`${apiUrl}/api/modules/${moduleUid}`, {
7720
+ method: "PATCH",
7721
+ body: JSON.stringify({ checkout_machine_id: this.deviceId })
7722
+ });
7723
+ console.log(`[Daemon] EP995: Claimed ownership of ${moduleUid}`);
7724
+ } catch (ownershipError) {
7725
+ console.warn(`[Daemon] EP995: Failed to claim ownership of ${moduleUid}`);
7726
+ }
7727
+ }
7728
+ const newWorktree = await getWorktreeInfoForModule(moduleUid);
7729
+ if (!newWorktree?.exists) {
7730
+ console.error(`[Daemon] EP995: Worktree still not found after creation`);
7731
+ continue;
7732
+ }
7733
+ const setupConfig = config.project_settings;
7734
+ const envVars = await fetchEnvVars();
7735
+ console.log(`[Daemon] EP995: Starting setup for reconciled module ${moduleUid}`);
7736
+ await worktreeManager.updateWorktreeStatus(moduleUid, "pending");
7737
+ await this.updateModuleWorktreeStatus(moduleUid, "pending", newWorktree.path);
7738
+ this.runWorktreeSetupAsync(
7739
+ moduleUid,
7740
+ worktreeManager,
7741
+ setupConfig?.worktree_copy_files || [],
7742
+ setupConfig?.worktree_setup_script,
7743
+ newWorktree.path,
7744
+ envVars
7745
+ ).then(() => {
7746
+ console.log(`[Daemon] EP995: Setup complete for reconciled ${moduleUid}`);
7747
+ this.startTunnelForModule(moduleUid, newWorktree.path);
7748
+ }).catch((err) => {
7749
+ console.error(`[Daemon] EP995: Setup failed for reconciled ${moduleUid}:`, err);
7750
+ });
7751
+ } else {
7752
+ const tunnelManager = getTunnelManager();
7753
+ await tunnelManager.initialize();
7754
+ if (!tunnelManager.hasTunnel(moduleUid)) {
7755
+ console.log(`[Daemon] EP995: Module ${moduleUid} has worktree but no tunnel - starting...`);
7756
+ await this.startTunnelForModule(moduleUid, worktree.path);
7757
+ } else {
7758
+ console.log(`[Daemon] EP995: Module ${moduleUid} OK - worktree and tunnel exist`);
7759
+ }
7760
+ }
7761
+ }
7762
+ console.log("[Daemon] EP995: Reconciliation complete");
7763
+ } catch (error) {
7764
+ console.error("[Daemon] EP995: Reconciliation error:", error instanceof Error ? error.message : error);
7765
+ throw error;
7766
+ }
7767
+ }
7578
7768
  /**
7579
7769
  * EP956: Cleanup module worktree when module moves to done
7580
7770
  *
@@ -7614,6 +7804,22 @@ var Daemon = class _Daemon {
7614
7804
  } else {
7615
7805
  console.log(`[Daemon] EP994: No worktree to remove for ${moduleUid}`);
7616
7806
  }
7807
+ try {
7808
+ const cleanupConfig = await (0, import_core10.loadConfig)();
7809
+ const cleanupApiUrl = cleanupConfig?.api_url || "https://episoda.dev";
7810
+ await fetchWithAuth(`${cleanupApiUrl}/api/modules/${moduleUid}`, {
7811
+ method: "PATCH",
7812
+ headers: { "Content-Type": "application/json" },
7813
+ body: JSON.stringify({
7814
+ worktree_status: null,
7815
+ worktree_path: null,
7816
+ worktree_error: null
7817
+ })
7818
+ });
7819
+ console.log(`[Daemon] EP995: Cleared worktree status for ${moduleUid}`);
7820
+ } catch (clearError) {
7821
+ console.warn(`[Daemon] EP995: Failed to clear worktree status for ${moduleUid}:`, clearError);
7822
+ }
7617
7823
  console.log(`[Daemon] EP956: Async cleanup complete for ${moduleUid}`);
7618
7824
  } catch (error) {
7619
7825
  console.error(`[Daemon] EP956: Cleanup error for ${moduleUid}:`, error instanceof Error ? error.message : error);
@@ -7629,6 +7835,7 @@ var Daemon = class _Daemon {
7629
7835
  console.log(`[Daemon] EP959: Running async worktree setup for ${moduleUid}`);
7630
7836
  try {
7631
7837
  await worktreeManager.updateWorktreeStatus(moduleUid, "running");
7838
+ await this.updateModuleWorktreeStatus(moduleUid, "setup", worktreePath);
7632
7839
  if (Object.keys(envVars).length > 0) {
7633
7840
  console.log(`[Daemon] EP988: Writing .env with ${Object.keys(envVars).length} variables to ${moduleUid}`);
7634
7841
  writeEnvFile(worktreePath, envVars);
@@ -7672,11 +7879,13 @@ var Daemon = class _Daemon {
7672
7879
  }
7673
7880
  }
7674
7881
  await worktreeManager.updateWorktreeStatus(moduleUid, "ready");
7882
+ await this.updateModuleWorktreeStatus(moduleUid, "ready", worktreePath);
7675
7883
  console.log(`[Daemon] EP959: Worktree setup complete for ${moduleUid}`);
7676
7884
  } catch (error) {
7677
7885
  const errorMessage = error instanceof Error ? error.message : String(error);
7678
7886
  console.error(`[Daemon] EP959: Worktree setup failed for ${moduleUid}:`, errorMessage);
7679
7887
  await worktreeManager.updateWorktreeStatus(moduleUid, "error", errorMessage);
7888
+ await this.updateModuleWorktreeStatus(moduleUid, "error", worktreePath, errorMessage);
7680
7889
  throw error;
7681
7890
  }
7682
7891
  }