episoda 0.2.30 → 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.
- package/dist/daemon/daemon-process.js +238 -4
- package/dist/daemon/daemon-process.js.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -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",
|
|
@@ -5529,6 +5529,13 @@ var WorktreeManager = class _WorktreeManager {
|
|
|
5529
5529
|
worktreeInfo: existing
|
|
5530
5530
|
};
|
|
5531
5531
|
}
|
|
5532
|
+
const fetchResult = await this.gitExecutor.execute({
|
|
5533
|
+
action: "fetch",
|
|
5534
|
+
remote: "origin"
|
|
5535
|
+
}, { cwd: this.bareRepoPath });
|
|
5536
|
+
if (!fetchResult.success) {
|
|
5537
|
+
console.warn("[worktree-manager] Failed to fetch from origin:", fetchResult.output);
|
|
5538
|
+
}
|
|
5532
5539
|
const result = await this.gitExecutor.execute({
|
|
5533
5540
|
action: "worktree_add",
|
|
5534
5541
|
path: worktreePath,
|
|
@@ -7143,6 +7150,9 @@ var Daemon = class _Daemon {
|
|
|
7143
7150
|
this.syncProjectSettings(projectId).catch((err) => {
|
|
7144
7151
|
console.warn("[Daemon] EP964: Settings sync failed:", err.message);
|
|
7145
7152
|
});
|
|
7153
|
+
this.syncMachineProjectPath(projectId, projectPath).catch((err) => {
|
|
7154
|
+
console.warn("[Daemon] EP995: Project path sync failed:", err.message);
|
|
7155
|
+
});
|
|
7146
7156
|
this.autoStartTunnelsForProject(projectPath, projectId).catch((error) => {
|
|
7147
7157
|
console.error(`[Daemon] EP819: Failed to auto-start tunnels:`, error);
|
|
7148
7158
|
});
|
|
@@ -7153,6 +7163,9 @@ var Daemon = class _Daemon {
|
|
|
7153
7163
|
}).catch((err) => {
|
|
7154
7164
|
console.warn("[Daemon] EP950: Cleanup on connect failed:", err.message);
|
|
7155
7165
|
});
|
|
7166
|
+
this.reconcileWorktrees(projectId, projectPath).catch((err) => {
|
|
7167
|
+
console.warn("[Daemon] EP995: Reconciliation failed:", err.message);
|
|
7168
|
+
});
|
|
7156
7169
|
});
|
|
7157
7170
|
client.on("module_state_changed", async (message) => {
|
|
7158
7171
|
if (message.type === "module_state_changed") {
|
|
@@ -7236,6 +7249,7 @@ var Daemon = class _Daemon {
|
|
|
7236
7249
|
{
|
|
7237
7250
|
console.log(`[Daemon] EP986: Starting async worktree setup for ${moduleUid}${hasSetupConfig ? " (with config)" : " (for dependency installation)"}`);
|
|
7238
7251
|
await worktreeManager.updateWorktreeStatus(moduleUid, "pending");
|
|
7252
|
+
await this.updateModuleWorktreeStatus(moduleUid, "pending", worktree.path);
|
|
7239
7253
|
this.runWorktreeSetupAsync(
|
|
7240
7254
|
moduleUid,
|
|
7241
7255
|
worktreeManager,
|
|
@@ -7568,6 +7582,189 @@ var Daemon = class _Daemon {
|
|
|
7568
7582
|
console.warn("[Daemon] EP964: Failed to sync project settings:", error instanceof Error ? error.message : error);
|
|
7569
7583
|
}
|
|
7570
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
|
+
}
|
|
7571
7768
|
/**
|
|
7572
7769
|
* EP956: Cleanup module worktree when module moves to done
|
|
7573
7770
|
*
|
|
@@ -7586,8 +7783,42 @@ var Daemon = class _Daemon {
|
|
|
7586
7783
|
await stopDevServer(moduleUid);
|
|
7587
7784
|
console.log(`[Daemon] EP956: Dev server stopped for ${moduleUid}`);
|
|
7588
7785
|
const worktree = await getWorktreeInfoForModule(moduleUid);
|
|
7589
|
-
if (worktree?.exists) {
|
|
7590
|
-
console.log(`[Daemon]
|
|
7786
|
+
if (worktree?.exists && worktree.path) {
|
|
7787
|
+
console.log(`[Daemon] EP994: Removing worktree for ${moduleUid} at ${worktree.path}`);
|
|
7788
|
+
const projectRoot = await findProjectRoot(worktree.path);
|
|
7789
|
+
if (projectRoot) {
|
|
7790
|
+
const manager = new WorktreeManager(projectRoot);
|
|
7791
|
+
if (await manager.initialize()) {
|
|
7792
|
+
const result = await manager.removeWorktree(moduleUid, true);
|
|
7793
|
+
if (result.success) {
|
|
7794
|
+
console.log(`[Daemon] EP994: Successfully removed worktree for ${moduleUid}`);
|
|
7795
|
+
} else {
|
|
7796
|
+
console.warn(`[Daemon] EP994: Could not remove worktree for ${moduleUid}: ${result.error}`);
|
|
7797
|
+
}
|
|
7798
|
+
} else {
|
|
7799
|
+
console.warn(`[Daemon] EP994: Could not initialize WorktreeManager for ${moduleUid}`);
|
|
7800
|
+
}
|
|
7801
|
+
} else {
|
|
7802
|
+
console.warn(`[Daemon] EP994: Could not find project root for ${moduleUid} worktree`);
|
|
7803
|
+
}
|
|
7804
|
+
} else {
|
|
7805
|
+
console.log(`[Daemon] EP994: No worktree to remove for ${moduleUid}`);
|
|
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);
|
|
7591
7822
|
}
|
|
7592
7823
|
console.log(`[Daemon] EP956: Async cleanup complete for ${moduleUid}`);
|
|
7593
7824
|
} catch (error) {
|
|
@@ -7604,6 +7835,7 @@ var Daemon = class _Daemon {
|
|
|
7604
7835
|
console.log(`[Daemon] EP959: Running async worktree setup for ${moduleUid}`);
|
|
7605
7836
|
try {
|
|
7606
7837
|
await worktreeManager.updateWorktreeStatus(moduleUid, "running");
|
|
7838
|
+
await this.updateModuleWorktreeStatus(moduleUid, "setup", worktreePath);
|
|
7607
7839
|
if (Object.keys(envVars).length > 0) {
|
|
7608
7840
|
console.log(`[Daemon] EP988: Writing .env with ${Object.keys(envVars).length} variables to ${moduleUid}`);
|
|
7609
7841
|
writeEnvFile(worktreePath, envVars);
|
|
@@ -7647,11 +7879,13 @@ var Daemon = class _Daemon {
|
|
|
7647
7879
|
}
|
|
7648
7880
|
}
|
|
7649
7881
|
await worktreeManager.updateWorktreeStatus(moduleUid, "ready");
|
|
7882
|
+
await this.updateModuleWorktreeStatus(moduleUid, "ready", worktreePath);
|
|
7650
7883
|
console.log(`[Daemon] EP959: Worktree setup complete for ${moduleUid}`);
|
|
7651
7884
|
} catch (error) {
|
|
7652
7885
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
7653
7886
|
console.error(`[Daemon] EP959: Worktree setup failed for ${moduleUid}:`, errorMessage);
|
|
7654
7887
|
await worktreeManager.updateWorktreeStatus(moduleUid, "error", errorMessage);
|
|
7888
|
+
await this.updateModuleWorktreeStatus(moduleUid, "error", worktreePath, errorMessage);
|
|
7655
7889
|
throw error;
|
|
7656
7890
|
}
|
|
7657
7891
|
}
|
|
@@ -7724,7 +7958,7 @@ var Daemon = class _Daemon {
|
|
|
7724
7958
|
}
|
|
7725
7959
|
const apiUrl = config.api_url || "https://episoda.dev";
|
|
7726
7960
|
const response = await fetchWithAuth(
|
|
7727
|
-
`${apiUrl}/api/modules?state=
|
|
7961
|
+
`${apiUrl}/api/modules?state=doing,review&fields=id,uid,dev_mode,tunnel_url,checkout_machine_id`
|
|
7728
7962
|
);
|
|
7729
7963
|
if (!response.ok) {
|
|
7730
7964
|
console.warn(`[Daemon] EP819: Failed to fetch modules: ${response.status}`);
|