@worca/ui 0.22.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 +7298 -4688
- package/app/main.bundle.js.map +4 -4
- package/app/protocol.js +5 -1
- package/app/styles.css +1315 -23
- package/app/utils/state-actions.js +33 -4
- 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 +159 -1
- package/server/fleet-routes.js +1149 -0
- package/server/index.js +4 -3
- package/server/integrations/commands/fleet.js +266 -0
- package/server/integrations/commands/global.js +18 -0
- package/server/integrations/commands/parser.js +4 -1
- package/server/integrations/commands/workspace.js +295 -0
- package/server/integrations/index.js +9 -0
- package/server/integrations/renderers.js +386 -0
- package/server/integrations/rest_client.js +7 -0
- 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/worktrees-routes.js +34 -0
- package/server/ws-fleet-manifest-watcher.js +131 -0
- package/server/ws-message-router.js +20 -0
- package/server/ws-modular.js +18 -1
- package/server/ws-workspace-manifest-watcher.js +136 -0
|
@@ -6,26 +6,47 @@ export const STATES = [
|
|
|
6
6
|
'failed',
|
|
7
7
|
'interrupted',
|
|
8
8
|
'cancelled',
|
|
9
|
+
'halted',
|
|
10
|
+
'setup_failed',
|
|
11
|
+
'unrecoverable',
|
|
12
|
+
'planning',
|
|
13
|
+
'integration_testing',
|
|
14
|
+
'integration_failed',
|
|
15
|
+
'blocked',
|
|
9
16
|
];
|
|
10
17
|
|
|
11
18
|
const ACTION_MATRIX = {
|
|
12
|
-
stop: { running: true },
|
|
13
|
-
pause: { running: true },
|
|
14
|
-
resume: {
|
|
19
|
+
stop: { running: true, planning: true, integration_testing: true },
|
|
20
|
+
pause: { running: true, planning: true, integration_testing: true },
|
|
21
|
+
resume: {
|
|
22
|
+
paused: true,
|
|
23
|
+
failed: true,
|
|
24
|
+
interrupted: true,
|
|
25
|
+
halted: true,
|
|
26
|
+
integration_failed: true,
|
|
27
|
+
blocked: true,
|
|
28
|
+
},
|
|
15
29
|
cancel: {
|
|
16
30
|
pending: true,
|
|
17
31
|
running: true,
|
|
18
32
|
paused: true,
|
|
19
33
|
failed: true,
|
|
20
34
|
interrupted: true,
|
|
35
|
+
halted: true,
|
|
36
|
+
setup_failed: true,
|
|
37
|
+
integration_failed: true,
|
|
38
|
+
blocked: true,
|
|
21
39
|
},
|
|
22
40
|
archive: {
|
|
23
|
-
pending: true,
|
|
24
41
|
paused: true,
|
|
25
42
|
completed: true,
|
|
26
43
|
failed: true,
|
|
27
44
|
interrupted: true,
|
|
28
45
|
cancelled: true,
|
|
46
|
+
halted: true,
|
|
47
|
+
setup_failed: true,
|
|
48
|
+
unrecoverable: true,
|
|
49
|
+
integration_failed: true,
|
|
29
50
|
},
|
|
30
51
|
unarchive: {
|
|
31
52
|
completed: true,
|
|
@@ -40,6 +61,11 @@ const ACTION_MATRIX = {
|
|
|
40
61
|
failed: true,
|
|
41
62
|
interrupted: true,
|
|
42
63
|
cancelled: true,
|
|
64
|
+
halted: true,
|
|
65
|
+
setup_failed: true,
|
|
66
|
+
unrecoverable: true,
|
|
67
|
+
integration_failed: true,
|
|
68
|
+
blocked: true,
|
|
43
69
|
},
|
|
44
70
|
learn: {
|
|
45
71
|
paused: true,
|
|
@@ -47,6 +73,9 @@ const ACTION_MATRIX = {
|
|
|
47
73
|
failed: true,
|
|
48
74
|
interrupted: true,
|
|
49
75
|
cancelled: true,
|
|
76
|
+
halted: true,
|
|
77
|
+
integration_failed: true,
|
|
78
|
+
blocked: true,
|
|
50
79
|
},
|
|
51
80
|
};
|
|
52
81
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// AUTO-GENERATED by scripts/build-frontend.js — do not edit.
|
|
2
|
+
// Source: src/worca/state/status.py
|
|
3
|
+
|
|
4
|
+
export const FLEET_STICKY = Object.freeze(new Set(["halted","paused"]));
|
|
5
|
+
export const FLEET_TERMINAL = Object.freeze(new Set(["completed","failed","halted"]));
|
|
6
|
+
export const WORKSPACE_TERMINAL = Object.freeze(new Set(["completed","failed","halted","integration_failed"]));
|
|
7
|
+
export const PIPELINE_ACTIVE = Object.freeze(new Set(["paused","resuming","running"]));
|
|
8
|
+
export const PIPELINE_TERMINAL = Object.freeze(new Set(["completed","interrupted"]));
|
|
9
|
+
export const PIPELINE_FAILURE = Object.freeze(new Set(["failed","setup_failed","unrecoverable"]));
|
|
10
|
+
export const PIPELINE_ALL_TERMINAL = Object.freeze(new Set(["cancelled","completed","failed","interrupted","setup_failed","unrecoverable"]));
|
|
11
|
+
export const PIPELINE_IN_FLIGHT = Object.freeze(new Set(["resuming","running"]));
|
package/bin/worca-ui.js
CHANGED
|
@@ -12,9 +12,9 @@ import {
|
|
|
12
12
|
writeFileSync,
|
|
13
13
|
} from 'node:fs';
|
|
14
14
|
import { connect, createServer } from 'node:net';
|
|
15
|
-
import { homedir } from 'node:os';
|
|
16
15
|
import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
|
|
17
16
|
import { fileURLToPath } from 'node:url';
|
|
17
|
+
import { worcaHome } from '../server/paths.js';
|
|
18
18
|
import {
|
|
19
19
|
readProjects,
|
|
20
20
|
removeProject,
|
|
@@ -37,7 +37,7 @@ function findProjectRoot(startDir) {
|
|
|
37
37
|
return startDir;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
const PREFS_DIR =
|
|
40
|
+
const PREFS_DIR = worcaHome();
|
|
41
41
|
const SERVER_SCRIPT = join(__dirname, '..', 'server', 'index.js');
|
|
42
42
|
|
|
43
43
|
/** Exported for testing */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@worca/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"description": "Pipeline monitoring UI for worca-cc",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Sinisha Djukic",
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
"app/protocol.js",
|
|
39
39
|
"app/utils/stage-order.js",
|
|
40
40
|
"app/utils/state-actions.js",
|
|
41
|
+
"app/utils/status-constants.js",
|
|
41
42
|
"scripts/build-frontend.js"
|
|
42
43
|
],
|
|
43
44
|
"engines": {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { copyFileSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
3
4
|
import path from 'node:path';
|
|
4
5
|
import { fileURLToPath } from 'node:url';
|
|
5
6
|
|
|
@@ -41,6 +42,52 @@ async function run() {
|
|
|
41
42
|
console.log('copied', path.relative(repoRoot, dest));
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
// Codegen: derive status-constants.js from the Python enums in
|
|
46
|
+
// src/worca/state/status.py. Single source of truth — when Python adds a
|
|
47
|
+
// status, the JS side picks it up at the next build. Shipped in the npm
|
|
48
|
+
// package via the `files` allowlist in package.json.
|
|
49
|
+
const utilsDir = path.join(appDir, 'utils');
|
|
50
|
+
mkdirSync(utilsDir, { recursive: true });
|
|
51
|
+
const constantsOut = path.join(utilsDir, 'status-constants.js');
|
|
52
|
+
try {
|
|
53
|
+
const pyScript =
|
|
54
|
+
'import json; from worca.state import status as s; ' +
|
|
55
|
+
'print(json.dumps({' +
|
|
56
|
+
'"FLEET_STICKY": sorted(s.FLEET_STICKY), ' +
|
|
57
|
+
'"FLEET_TERMINAL": sorted(s.FLEET_TERMINAL), ' +
|
|
58
|
+
'"WORKSPACE_TERMINAL": sorted(s.WORKSPACE_TERMINAL), ' +
|
|
59
|
+
'"PIPELINE_ACTIVE": sorted(s.PIPELINE_ACTIVE), ' +
|
|
60
|
+
'"PIPELINE_TERMINAL": sorted(s.PIPELINE_TERMINAL), ' +
|
|
61
|
+
'"PIPELINE_FAILURE": sorted(s.PIPELINE_FAILURE), ' +
|
|
62
|
+
'"PIPELINE_ALL_TERMINAL": sorted(s.PIPELINE_ALL_TERMINAL), ' +
|
|
63
|
+
'"PIPELINE_IN_FLIGHT": sorted(s.PIPELINE_IN_FLIGHT)' +
|
|
64
|
+
'}))';
|
|
65
|
+
const raw = execFileSync('python3', ['-c', pyScript], {
|
|
66
|
+
cwd: path.join(repoRoot, '..'),
|
|
67
|
+
encoding: 'utf8',
|
|
68
|
+
});
|
|
69
|
+
const constants = JSON.parse(raw);
|
|
70
|
+
const lines = [
|
|
71
|
+
'// AUTO-GENERATED by scripts/build-frontend.js — do not edit.',
|
|
72
|
+
'// Source: src/worca/state/status.py',
|
|
73
|
+
'',
|
|
74
|
+
];
|
|
75
|
+
for (const [name, values] of Object.entries(constants)) {
|
|
76
|
+
lines.push(
|
|
77
|
+
`export const ${name} = Object.freeze(new Set(${JSON.stringify(values)}));`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
writeFileSync(constantsOut, `${lines.join('\n')}\n`);
|
|
81
|
+
console.log('generated', path.relative(repoRoot, constantsOut));
|
|
82
|
+
} catch (err) {
|
|
83
|
+
console.error(
|
|
84
|
+
'status-constants codegen failed (is python3 + worca-cc installed?):',
|
|
85
|
+
err.message,
|
|
86
|
+
);
|
|
87
|
+
process.exitCode = 1;
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
44
91
|
try {
|
|
45
92
|
const esbuild = await import('esbuild');
|
|
46
93
|
await esbuild.build({
|
package/server/app.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// server/app.js
|
|
2
2
|
|
|
3
|
-
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { execFileSync, spawn } from 'node:child_process';
|
|
4
4
|
import { createHmac, randomUUID } from 'node:crypto';
|
|
5
5
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
6
6
|
import { homedir } from 'node:os';
|
|
@@ -9,9 +9,11 @@ import { fileURLToPath } from 'node:url';
|
|
|
9
9
|
import express from 'express';
|
|
10
10
|
|
|
11
11
|
import { dbExists, getIssue, listIssues } from './beads-reader.js';
|
|
12
|
+
import { createFleetRouter } from './fleet-routes.js';
|
|
12
13
|
import { RAW_BODY } from './integrations/index.js';
|
|
13
14
|
import { verify } from './integrations/verify.js';
|
|
14
15
|
import { LaunchLock } from './launch-lock.js';
|
|
16
|
+
import { fleetRunsDir, workspaceRunsDir, workspacesDir } from './paths.js';
|
|
15
17
|
import { createPreferencesRouter } from './preferences-routes.js';
|
|
16
18
|
import { ProcessManager } from './process-manager.js';
|
|
17
19
|
import { scanDirectory } from './project-registry.js';
|
|
@@ -26,6 +28,30 @@ import { discoverSubagents } from './subagents-discovery.js';
|
|
|
26
28
|
import { checkWorcaVersion } from './version-check.js';
|
|
27
29
|
import { getVersionInfo } from './versions.js';
|
|
28
30
|
import { createInbox } from './webhook-inbox.js';
|
|
31
|
+
import { createWorkspaceRouter } from './workspace-routes.js';
|
|
32
|
+
|
|
33
|
+
// Invokes `worca cleanup --<flag> <id>` as a subprocess and resolves once
|
|
34
|
+
// the cleanup completes. Wired into the fleet/workspace router DELETE
|
|
35
|
+
// ?cleanup=1 path so the UI Cleanup button actually removes the worktrees
|
|
36
|
+
// + manifest dir (without this, the route falls back to a no-op default).
|
|
37
|
+
function runWorcaCleanupSubprocess(flag, id) {
|
|
38
|
+
return new Promise((resolve, reject) => {
|
|
39
|
+
const child = spawn(
|
|
40
|
+
'python3',
|
|
41
|
+
['-m', 'worca.cli.main', 'cleanup', flag, id],
|
|
42
|
+
{ stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env } },
|
|
43
|
+
);
|
|
44
|
+
let stderr = '';
|
|
45
|
+
child.stderr.on('data', (chunk) => {
|
|
46
|
+
stderr += chunk.toString();
|
|
47
|
+
});
|
|
48
|
+
child.on('error', reject);
|
|
49
|
+
child.on('exit', (code) => {
|
|
50
|
+
if (code === 0) resolve({});
|
|
51
|
+
else reject(new Error(`worca cleanup exited ${code}: ${stderr.trim()}`));
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
}
|
|
29
55
|
|
|
30
56
|
export function createApp(options = {}) {
|
|
31
57
|
const app = express();
|
|
@@ -550,6 +576,138 @@ export function createApp(options = {}) {
|
|
|
550
576
|
launchLock,
|
|
551
577
|
}),
|
|
552
578
|
);
|
|
579
|
+
app.use(
|
|
580
|
+
'/api/fleet-runs',
|
|
581
|
+
createFleetRouter({
|
|
582
|
+
fleetRunsDir: fleetRunsDir(),
|
|
583
|
+
prefsDir,
|
|
584
|
+
runCleanup: (id) => runWorcaCleanupSubprocess('--fleet-id', id),
|
|
585
|
+
// Spawn run_fleet.py in a detached subprocess so the route can return
|
|
586
|
+
// immediately. We pass the pre-generated fleet_id so the in-flight
|
|
587
|
+
// manifest path matches what the route just wrote.
|
|
588
|
+
dispatchFleet: async ({ fleet_id, projects, manifest, resume }) => {
|
|
589
|
+
// Resume path: the /resume route already flipped the manifest to
|
|
590
|
+
// "running"; run_fleet.py --resume reads the manifest, continues
|
|
591
|
+
// paused/interrupted children in place and re-dispatches
|
|
592
|
+
// failed/pending ones. No --projects — the manifest is the source.
|
|
593
|
+
if (resume) {
|
|
594
|
+
const child = spawn(
|
|
595
|
+
'python3',
|
|
596
|
+
['-m', 'worca.scripts.run_fleet', '--resume', fleet_id],
|
|
597
|
+
{ detached: true, stdio: 'ignore', env: { ...process.env } },
|
|
598
|
+
);
|
|
599
|
+
child.unref();
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
if (!projects || projects.length === 0) return;
|
|
603
|
+
const args = [
|
|
604
|
+
'-m',
|
|
605
|
+
'worca.scripts.run_fleet',
|
|
606
|
+
'--fleet-id',
|
|
607
|
+
fleet_id,
|
|
608
|
+
'--projects',
|
|
609
|
+
...projects,
|
|
610
|
+
];
|
|
611
|
+
if (manifest.work_request?.source) {
|
|
612
|
+
args.push('--source', manifest.work_request.source);
|
|
613
|
+
} else {
|
|
614
|
+
args.push('--prompt', manifest.work_request?.description ?? '');
|
|
615
|
+
}
|
|
616
|
+
if (manifest.head_template) {
|
|
617
|
+
args.push('--head-template', manifest.head_template);
|
|
618
|
+
}
|
|
619
|
+
if (manifest.base_branch) {
|
|
620
|
+
args.push('--base', manifest.base_branch);
|
|
621
|
+
}
|
|
622
|
+
if (manifest.plan?.path) {
|
|
623
|
+
args.push('--plan', manifest.plan.path);
|
|
624
|
+
}
|
|
625
|
+
for (const p of manifest.guide?.paths || []) {
|
|
626
|
+
args.push('--guide', p);
|
|
627
|
+
}
|
|
628
|
+
if (manifest.max_parallel) {
|
|
629
|
+
args.push('--max-parallel', String(manifest.max_parallel));
|
|
630
|
+
}
|
|
631
|
+
if (manifest.fleet_failure_threshold != null) {
|
|
632
|
+
args.push(
|
|
633
|
+
'--fleet-failure-threshold',
|
|
634
|
+
String(manifest.fleet_failure_threshold),
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
const child = spawn('python3', args, {
|
|
638
|
+
detached: true,
|
|
639
|
+
stdio: 'ignore',
|
|
640
|
+
env: { ...process.env },
|
|
641
|
+
});
|
|
642
|
+
child.unref();
|
|
643
|
+
},
|
|
644
|
+
}),
|
|
645
|
+
);
|
|
646
|
+
|
|
647
|
+
// Workspace routers — both definitions (/api/workspaces) and runs
|
|
648
|
+
// (/api/workspace-runs). The router factory exposes them as a pair.
|
|
649
|
+
const workspaceRouters = createWorkspaceRouter({
|
|
650
|
+
workspaceRunsDir: workspaceRunsDir(),
|
|
651
|
+
workspacesDir: workspacesDir(),
|
|
652
|
+
runCleanup: (id) => runWorcaCleanupSubprocess('--workspace-id', id),
|
|
653
|
+
// Spawn run_workspace.py in a detached subprocess, mirroring the fleet
|
|
654
|
+
// dispatcher. We pass --workspace-id so the script reuses the manifest
|
|
655
|
+
// the route just wrote instead of generating a fresh ID (which would
|
|
656
|
+
// orphan the manifest the UI navigated to).
|
|
657
|
+
dispatchWorkspace: async ({
|
|
658
|
+
workspace_id,
|
|
659
|
+
workspace_root,
|
|
660
|
+
manifest,
|
|
661
|
+
resume,
|
|
662
|
+
}) => {
|
|
663
|
+
if (resume) {
|
|
664
|
+
const child = spawn(
|
|
665
|
+
'python3',
|
|
666
|
+
[
|
|
667
|
+
'-m',
|
|
668
|
+
'worca.scripts.run_workspace',
|
|
669
|
+
workspace_root,
|
|
670
|
+
'--resume',
|
|
671
|
+
workspace_id,
|
|
672
|
+
],
|
|
673
|
+
{ detached: true, stdio: 'ignore', env: { ...process.env } },
|
|
674
|
+
);
|
|
675
|
+
child.unref();
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
const args = [
|
|
679
|
+
'-m',
|
|
680
|
+
'worca.scripts.run_workspace',
|
|
681
|
+
workspace_root,
|
|
682
|
+
'--workspace-id',
|
|
683
|
+
workspace_id,
|
|
684
|
+
];
|
|
685
|
+
if (manifest.work_request?.source) {
|
|
686
|
+
args.push('--source', manifest.work_request.source);
|
|
687
|
+
} else {
|
|
688
|
+
args.push('--prompt', manifest.work_request?.description ?? '');
|
|
689
|
+
}
|
|
690
|
+
if (manifest.branch_template) {
|
|
691
|
+
args.push('--branch', manifest.branch_template);
|
|
692
|
+
}
|
|
693
|
+
if (manifest.skip_integration) args.push('--skip-integration');
|
|
694
|
+
if (manifest.skip_planning) args.push('--skip-planning');
|
|
695
|
+
if (manifest.max_parallel) {
|
|
696
|
+
args.push('--max-parallel', String(manifest.max_parallel));
|
|
697
|
+
}
|
|
698
|
+
for (const p of manifest.guide?.paths || []) {
|
|
699
|
+
args.push('--guide', p);
|
|
700
|
+
}
|
|
701
|
+
const child = spawn('python3', args, {
|
|
702
|
+
detached: true,
|
|
703
|
+
stdio: 'ignore',
|
|
704
|
+
env: { ...process.env },
|
|
705
|
+
});
|
|
706
|
+
child.unref();
|
|
707
|
+
},
|
|
708
|
+
});
|
|
709
|
+
app.use('/api/workspaces', workspaceRouters.workspaces);
|
|
710
|
+
app.use('/api/workspace-runs', workspaceRouters.workspaceRuns);
|
|
553
711
|
}
|
|
554
712
|
|
|
555
713
|
// POST /api/integrations/telegram/detect — find chat IDs from recent messages.
|