@worca/ui 0.23.0 → 0.24.0
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/app/main.bundle.js +2341 -1205
- package/app/main.bundle.js.map +4 -4
- package/app/protocol.js +4 -1
- package/app/styles.css +446 -8
- package/app/utils/state-actions.js +21 -3
- package/app/utils/status-constants.js +11 -0
- package/bin/worca-ui.js +2 -2
- package/package.json +2 -1
- package/scripts/build-frontend.js +48 -1
- package/server/app.js +92 -1
- package/server/fleet-routes.js +5 -3
- package/server/index.js +4 -3
- package/server/integrations/commands/fleet.js +1 -1
- package/server/integrations/commands/global.js +9 -0
- package/server/integrations/commands/workspace.js +295 -0
- package/server/integrations/index.js +6 -0
- package/server/integrations/renderers.js +291 -3
- package/server/paths.js +78 -0
- package/server/project-routes.js +68 -5
- package/server/workspace-routes.js +1554 -0
- package/server/worktree-ops.js +12 -1
- package/server/ws-fleet-manifest-watcher.js +4 -3
- package/server/ws-modular.js +10 -2
- package/server/ws-workspace-manifest-watcher.js +136 -0
package/server/worktree-ops.js
CHANGED
|
@@ -59,7 +59,18 @@ export async function removeWorktree(
|
|
|
59
59
|
/* ignore */
|
|
60
60
|
}
|
|
61
61
|
if (isRealDir) {
|
|
62
|
-
|
|
62
|
+
// maxRetries handles transient ENOTEMPTY/EBUSY/EPERM on macOS when a
|
|
63
|
+
// background process (Spotlight, language servers, npm install
|
|
64
|
+
// finishing) touches a deep node_modules subtree between the
|
|
65
|
+
// recursive walk's readdir and the final rmdir. Without retries,
|
|
66
|
+
// a single race surfaces "ENOTEMPTY: directory not empty, rmdir
|
|
67
|
+
// .../node_modules/lucide/dist" to the user.
|
|
68
|
+
await rm(worktreePath, {
|
|
69
|
+
recursive: true,
|
|
70
|
+
force: true,
|
|
71
|
+
maxRetries: 5,
|
|
72
|
+
retryDelay: 200,
|
|
73
|
+
});
|
|
63
74
|
}
|
|
64
75
|
}
|
|
65
76
|
}
|
|
@@ -4,12 +4,11 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { existsSync, readFileSync, watch } from 'node:fs';
|
|
7
|
-
import { homedir } from 'node:os';
|
|
8
7
|
import { join } from 'node:path';
|
|
9
8
|
import { effectiveFleetStatus } from './fleet-routes.js';
|
|
9
|
+
import { fleetRunsDir as resolveFleetRunsDir } from './paths.js';
|
|
10
10
|
|
|
11
11
|
const FLEET_DEBOUNCE_MS = 200;
|
|
12
|
-
const DEFAULT_FLEET_RUNS_DIR = join(homedir(), '.worca', 'fleet-runs');
|
|
13
12
|
|
|
14
13
|
const FAILURE_STATES = new Set(['failed', 'setup_failed', 'unrecoverable']);
|
|
15
14
|
|
|
@@ -40,8 +39,10 @@ function resolveChildStatus(child) {
|
|
|
40
39
|
*/
|
|
41
40
|
export function createFleetManifestWatcher({
|
|
42
41
|
broadcaster,
|
|
43
|
-
fleetRunsDir
|
|
42
|
+
fleetRunsDir: fleetRunsDirArg,
|
|
44
43
|
}) {
|
|
44
|
+
// Lazy resolution honors $WORCA_HOME (issue #162).
|
|
45
|
+
const fleetRunsDir = resolveFleetRunsDir(fleetRunsDirArg);
|
|
45
46
|
let fsWatcher = null;
|
|
46
47
|
/** @type {Map<string, ReturnType<typeof setTimeout>>} */
|
|
47
48
|
const debounceTimers = new Map();
|
package/server/ws-modular.js
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { existsSync, watch } from 'node:fs';
|
|
10
|
-
import { homedir } from 'node:os';
|
|
11
10
|
import { join } from 'node:path';
|
|
12
11
|
import { WebSocketServer } from 'ws';
|
|
12
|
+
import { fleetRunsDir, workspaceRunsDir } from './paths.js';
|
|
13
13
|
import { readProjects, synthesizeDefaultProject } from './project-registry.js';
|
|
14
14
|
import { TIER_FULL, TIER_POLLING, WatcherSet } from './watcher-set.js';
|
|
15
15
|
import { readProjectWorcaVersion } from './worca-setup.js';
|
|
@@ -18,6 +18,7 @@ import { createClientManager } from './ws-client-manager.js';
|
|
|
18
18
|
import { createFleetManifestWatcher } from './ws-fleet-manifest-watcher.js';
|
|
19
19
|
import { createMessageRouter } from './ws-message-router.js';
|
|
20
20
|
import { resolveLatestRunDir } from './ws-status-watcher.js';
|
|
21
|
+
import { createWorkspaceManifestWatcher } from './ws-workspace-manifest-watcher.js';
|
|
21
22
|
|
|
22
23
|
export { resolveLatestRunDir };
|
|
23
24
|
|
|
@@ -50,7 +51,13 @@ export function attachWsServer(httpServer, config) {
|
|
|
50
51
|
// 3a. Fleet manifest watcher — global, not per-project (§13.5)
|
|
51
52
|
const fleetManifestWatcher = createFleetManifestWatcher({
|
|
52
53
|
broadcaster,
|
|
53
|
-
fleetRunsDir:
|
|
54
|
+
fleetRunsDir: fleetRunsDir(),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// 3a-ws. Workspace manifest watcher — global, separate from fleet (W-047 §13.5)
|
|
58
|
+
const workspaceManifestWatcher = createWorkspaceManifestWatcher({
|
|
59
|
+
broadcaster,
|
|
60
|
+
workspaceRunsDir: workspaceRunsDir(),
|
|
54
61
|
});
|
|
55
62
|
|
|
56
63
|
// 3b. Create WatcherSet(s) — one per project
|
|
@@ -278,6 +285,7 @@ export function attachWsServer(httpServer, config) {
|
|
|
278
285
|
wss.on('close', () => {
|
|
279
286
|
clientManager.destroy();
|
|
280
287
|
fleetManifestWatcher.destroy();
|
|
288
|
+
workspaceManifestWatcher.destroy();
|
|
281
289
|
if (dirWatcher) {
|
|
282
290
|
try {
|
|
283
291
|
dirWatcher.close();
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace manifest watcher — monitors ~/.worca/workspace-runs/ for pointer
|
|
3
|
+
* file changes, reads the actual manifest, and broadcasts workspace-update,
|
|
4
|
+
* workspace-tier-update, and guide-conflict WS events.
|
|
5
|
+
*
|
|
6
|
+
* Separate from fleet-update per W-040 §13.5 — never multiplexed.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync, readFileSync, watch } from 'node:fs';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { workspaceRunsDir as resolveWorkspaceRunsDir } from './paths.js';
|
|
12
|
+
|
|
13
|
+
const WS_DEBOUNCE_MS = 200;
|
|
14
|
+
|
|
15
|
+
function readJson(path) {
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function readManifestFromPointer(wsRunsDir, wsId) {
|
|
24
|
+
const pointer = readJson(join(wsRunsDir, `${wsId}.json`));
|
|
25
|
+
if (!pointer?.workspace_root) return null;
|
|
26
|
+
const manifestPath = join(
|
|
27
|
+
pointer.workspace_root,
|
|
28
|
+
'.worca',
|
|
29
|
+
'workspace-runs',
|
|
30
|
+
wsId,
|
|
31
|
+
'workspace-manifest.json',
|
|
32
|
+
);
|
|
33
|
+
return readJson(manifestPath);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {{ broadcaster: { broadcast: Function }, workspaceRunsDir?: string }} deps
|
|
38
|
+
*/
|
|
39
|
+
export function createWorkspaceManifestWatcher({
|
|
40
|
+
broadcaster,
|
|
41
|
+
workspaceRunsDir: workspaceRunsDirArg,
|
|
42
|
+
}) {
|
|
43
|
+
// Lazy resolution honors $WORCA_HOME (issue #162).
|
|
44
|
+
const workspaceRunsDir = resolveWorkspaceRunsDir(workspaceRunsDirArg);
|
|
45
|
+
|
|
46
|
+
let fsWatcher = null;
|
|
47
|
+
/** @type {Map<string, ReturnType<typeof setTimeout>>} */
|
|
48
|
+
const debounceTimers = new Map();
|
|
49
|
+
|
|
50
|
+
function broadcastWorkspaceUpdate(wsId) {
|
|
51
|
+
const manifest = readManifestFromPointer(workspaceRunsDir, wsId);
|
|
52
|
+
if (!manifest) return;
|
|
53
|
+
|
|
54
|
+
const children = Array.isArray(manifest.children) ? manifest.children : [];
|
|
55
|
+
const dag = manifest.dag ?? { tiers: [] };
|
|
56
|
+
|
|
57
|
+
broadcaster.broadcast('workspace-update', {
|
|
58
|
+
workspace_id: wsId,
|
|
59
|
+
workspace_name: manifest.workspace_name ?? null,
|
|
60
|
+
status: manifest.status ?? 'running',
|
|
61
|
+
halt_reason: manifest.halt_reason ?? null,
|
|
62
|
+
dag,
|
|
63
|
+
children: children.map((c) => ({
|
|
64
|
+
repo: c.repo,
|
|
65
|
+
run_id: c.run_id,
|
|
66
|
+
status: c.status,
|
|
67
|
+
tier: c.tier,
|
|
68
|
+
})),
|
|
69
|
+
integration_test: manifest.integration_test ?? null,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const tiers = dag.tiers ?? [];
|
|
73
|
+
for (const tier of tiers) {
|
|
74
|
+
broadcaster.broadcast('workspace-tier-update', {
|
|
75
|
+
workspace_id: wsId,
|
|
76
|
+
tier: tier.tier,
|
|
77
|
+
repos: tier.repos,
|
|
78
|
+
status: tier.status,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const conflicts = manifest.guide_conflicts;
|
|
83
|
+
if (Array.isArray(conflicts) && conflicts.length > 0) {
|
|
84
|
+
broadcaster.broadcast('guide-conflict', {
|
|
85
|
+
workspace_id: wsId,
|
|
86
|
+
conflicts,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function scheduleUpdate(wsId) {
|
|
92
|
+
const existing = debounceTimers.get(wsId);
|
|
93
|
+
if (existing) clearTimeout(existing);
|
|
94
|
+
debounceTimers.set(
|
|
95
|
+
wsId,
|
|
96
|
+
setTimeout(() => {
|
|
97
|
+
debounceTimers.delete(wsId);
|
|
98
|
+
broadcastWorkspaceUpdate(wsId);
|
|
99
|
+
}, WS_DEBOUNCE_MS),
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
if (existsSync(workspaceRunsDir)) {
|
|
105
|
+
fsWatcher = watch(
|
|
106
|
+
workspaceRunsDir,
|
|
107
|
+
{ persistent: false },
|
|
108
|
+
(_event, filename) => {
|
|
109
|
+
if (!filename?.endsWith('.json')) return;
|
|
110
|
+
const wsId = filename.slice(0, -5);
|
|
111
|
+
scheduleUpdate(wsId);
|
|
112
|
+
},
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
} catch {
|
|
116
|
+
// fs.watch unsupported or dir unavailable — skip silently
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function destroy() {
|
|
120
|
+
if (fsWatcher) {
|
|
121
|
+
try {
|
|
122
|
+
fsWatcher.close();
|
|
123
|
+
} catch {
|
|
124
|
+
/* ignore */
|
|
125
|
+
}
|
|
126
|
+
fsWatcher = null;
|
|
127
|
+
}
|
|
128
|
+
for (const timer of debounceTimers.values()) clearTimeout(timer);
|
|
129
|
+
debounceTimers.clear();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
destroy,
|
|
134
|
+
_broadcastForTest: broadcastWorkspaceUpdate,
|
|
135
|
+
};
|
|
136
|
+
}
|