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.
|
|
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
|
}
|