@worca/ui 0.1.0-rc.4 → 0.1.0-rc.6
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/README.md +2 -0
- package/app/main.bundle.js +595 -530
- package/app/main.bundle.js.map +3 -3
- package/app/styles.css +19 -0
- package/bin/worca-ui.js +46 -10
- package/package.json +1 -1
- package/server/app.js +14 -0
- package/server/preferences.js +1 -1
- package/server/project-routes.js +26 -9
- package/server/versions.js +260 -0
- package/server/worca-setup.js +18 -1
package/app/styles.css
CHANGED
|
@@ -3895,3 +3895,22 @@ sl-details.learnings-panel::part(content) {
|
|
|
3895
3895
|
color: var(--fg);
|
|
3896
3896
|
}
|
|
3897
3897
|
|
|
3898
|
+
/* ─── Version info rows ─────────────────────────────────────────────── */
|
|
3899
|
+
.version-row { display: flex; align-items: center; padding: 4px 0; gap: 8px; }
|
|
3900
|
+
.version-row-label { font-size: 12px; color: var(--muted); white-space: nowrap; min-width: 64px; }
|
|
3901
|
+
.version-row-value { font-size: 13px; font-weight: 500; color: var(--fg); font-family: var(--sl-font-mono); white-space: nowrap; margin-left: auto; display: flex; align-items: center; gap: 6px; }
|
|
3902
|
+
.settings-card-header > sl-badge { margin-left: auto; }
|
|
3903
|
+
.version-title-exact { text-transform: none; }
|
|
3904
|
+
.version-copy-btn {
|
|
3905
|
+
background: none; border: 1px solid var(--border); border-radius: 4px;
|
|
3906
|
+
padding: 2px 4px; cursor: pointer; color: var(--muted);
|
|
3907
|
+
display: inline-flex; align-items: center; margin-left: 4px;
|
|
3908
|
+
transition: var(--transition-fast);
|
|
3909
|
+
}
|
|
3910
|
+
.version-copy-btn:hover { background: var(--bg-tertiary); color: var(--fg); }
|
|
3911
|
+
.version-copy-icon { display: inline-flex; align-items: center; min-width: 12px; }
|
|
3912
|
+
.version-refresh { display: flex; align-items: center; gap: 8px; margin-top: 8px; }
|
|
3913
|
+
.version-refresh-hint { font-size: 11px; color: var(--muted); }
|
|
3914
|
+
.project-worca-version { font-size: 11px; color: var(--muted); font-family: var(--sl-font-mono); margin-top: 2px; }
|
|
3915
|
+
.project-worca-version--behind { color: var(--status-failed, #dc2626); }
|
|
3916
|
+
|
package/bin/worca-ui.js
CHANGED
|
@@ -20,6 +20,11 @@ import {
|
|
|
20
20
|
writeProject,
|
|
21
21
|
} from '../server/project-registry.js';
|
|
22
22
|
|
|
23
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
24
|
+
const pkg = JSON.parse(
|
|
25
|
+
readFileSync(join(__dirname, '..', 'package.json'), 'utf8'),
|
|
26
|
+
);
|
|
27
|
+
|
|
23
28
|
function findProjectRoot(startDir) {
|
|
24
29
|
let dir = startDir;
|
|
25
30
|
while (dir !== dirname(dir)) {
|
|
@@ -30,12 +35,7 @@ function findProjectRoot(startDir) {
|
|
|
30
35
|
}
|
|
31
36
|
|
|
32
37
|
const PREFS_DIR = join(homedir(), '.worca');
|
|
33
|
-
const SERVER_SCRIPT = join(
|
|
34
|
-
dirname(fileURLToPath(import.meta.url)),
|
|
35
|
-
'..',
|
|
36
|
-
'server',
|
|
37
|
-
'index.js',
|
|
38
|
-
);
|
|
38
|
+
const SERVER_SCRIPT = join(__dirname, '..', 'server', 'index.js');
|
|
39
39
|
|
|
40
40
|
/** Exported for testing */
|
|
41
41
|
export function parseArgs(argv) {
|
|
@@ -57,7 +57,11 @@ export function parseArgs(argv) {
|
|
|
57
57
|
};
|
|
58
58
|
for (let i = 2; i < argv.length; i++) {
|
|
59
59
|
const arg = argv[i];
|
|
60
|
-
if (
|
|
60
|
+
if (arg === '--version' || arg === '-v') {
|
|
61
|
+
args.command = 'version';
|
|
62
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
63
|
+
args.command = 'help';
|
|
64
|
+
} else if (
|
|
61
65
|
['start', 'stop', 'restart', 'status', 'projects', 'migrate'].includes(
|
|
62
66
|
arg,
|
|
63
67
|
)
|
|
@@ -493,6 +497,34 @@ function migrateStatus() {
|
|
|
493
497
|
}
|
|
494
498
|
}
|
|
495
499
|
|
|
500
|
+
function printHelp() {
|
|
501
|
+
console.log(`worca-ui v${pkg.version} — Pipeline monitoring UI for worca-cc
|
|
502
|
+
|
|
503
|
+
Usage: worca-ui <command> [options]
|
|
504
|
+
|
|
505
|
+
Commands:
|
|
506
|
+
start Start the server (default)
|
|
507
|
+
stop Stop the running server
|
|
508
|
+
restart Restart the server
|
|
509
|
+
status Show server status
|
|
510
|
+
projects list List registered projects
|
|
511
|
+
projects add <path> [--name] Register a project
|
|
512
|
+
projects remove <name> Unregister a project
|
|
513
|
+
migrate --scan <dir> Scan directory for projects to register
|
|
514
|
+
migrate --add <path> Register a single project
|
|
515
|
+
migrate --status Show registration health
|
|
516
|
+
|
|
517
|
+
Options:
|
|
518
|
+
--port <N> Server port (default: 3400, env: PORT)
|
|
519
|
+
--host <addr> Bind address (default: 127.0.0.1, env: HOST)
|
|
520
|
+
--global Multi-project mode (default)
|
|
521
|
+
--project [path] Single-project mode, optionally scoped to path
|
|
522
|
+
--open Open browser after start
|
|
523
|
+
--dry-run Preview migrate --scan without registering
|
|
524
|
+
-v, --version Show version
|
|
525
|
+
-h, --help Show this help`);
|
|
526
|
+
}
|
|
527
|
+
|
|
496
528
|
const args = parseArgs(process.argv);
|
|
497
529
|
switch (args.command) {
|
|
498
530
|
case 'start':
|
|
@@ -538,8 +570,12 @@ switch (args.command) {
|
|
|
538
570
|
);
|
|
539
571
|
}
|
|
540
572
|
break;
|
|
573
|
+
case 'version':
|
|
574
|
+
console.log(pkg.version);
|
|
575
|
+
break;
|
|
576
|
+
case 'help':
|
|
577
|
+
printHelp();
|
|
578
|
+
break;
|
|
541
579
|
default:
|
|
542
|
-
|
|
543
|
-
'Usage: worca-ui [start|stop|restart|status|projects|migrate] [--port N] [--host H] [--open] [--project [PATH]]',
|
|
544
|
-
);
|
|
580
|
+
printHelp();
|
|
545
581
|
}
|
package/package.json
CHANGED
package/server/app.js
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
createProjectScopedRoutes,
|
|
14
14
|
projectResolver,
|
|
15
15
|
} from './project-routes.js';
|
|
16
|
+
import { getVersionInfo } from './versions.js';
|
|
16
17
|
import { createInbox } from './webhook-inbox.js';
|
|
17
18
|
|
|
18
19
|
export function createApp(options = {}) {
|
|
@@ -403,6 +404,19 @@ export function createApp(options = {}) {
|
|
|
403
404
|
}
|
|
404
405
|
});
|
|
405
406
|
|
|
407
|
+
// GET /api/versions — installed + registry version info
|
|
408
|
+
app.get('/api/versions', async (req, res) => {
|
|
409
|
+
const force = req.query.force === '1';
|
|
410
|
+
const prefsPath = prefsDir ? join(prefsDir, 'preferences.json') : null;
|
|
411
|
+
const worcaVersion = app.locals.worcaVersion || null;
|
|
412
|
+
try {
|
|
413
|
+
const data = await getVersionInfo({ prefsPath, worcaVersion, force });
|
|
414
|
+
res.json(data);
|
|
415
|
+
} catch (err) {
|
|
416
|
+
res.status(500).json({ ok: false, error: err.message });
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
|
|
406
420
|
// ─── Multi-project routes ──────────────────────────────────────────────
|
|
407
421
|
if (prefsDir) {
|
|
408
422
|
app.use('/api/projects', createProjectRoutes({ prefsDir, projectRoot }));
|
package/server/preferences.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { dirname } from 'node:path';
|
|
3
3
|
|
|
4
|
-
const DEFAULTS = { theme: 'light', source_repo: '' };
|
|
4
|
+
const DEFAULTS = { theme: 'light', source_repo: '', notifications: null };
|
|
5
5
|
|
|
6
6
|
export function readPreferences(path) {
|
|
7
7
|
try {
|
package/server/project-routes.js
CHANGED
|
@@ -37,7 +37,11 @@ import {
|
|
|
37
37
|
} from './settings-merge.js';
|
|
38
38
|
import { validateSettingsPayload } from './settings-validator.js';
|
|
39
39
|
import { discoverRuns } from './watcher.js';
|
|
40
|
-
import {
|
|
40
|
+
import {
|
|
41
|
+
checkWorcaInstalled,
|
|
42
|
+
readProjectWorcaVersion,
|
|
43
|
+
runWorcaSetup,
|
|
44
|
+
} from './worca-setup.js';
|
|
41
45
|
|
|
42
46
|
/** Validate a runId — must not contain path traversal characters */
|
|
43
47
|
const RUN_ID_RE = /^[a-zA-Z0-9_-]+$/;
|
|
@@ -133,13 +137,16 @@ export function createProjectRoutes({ prefsDir, projectRoot }) {
|
|
|
133
137
|
|
|
134
138
|
// GET /api/projects — list all projects (or synthesized default)
|
|
135
139
|
router.get('/', (_req, res) => {
|
|
136
|
-
|
|
137
|
-
if (projects.length
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
//
|
|
141
|
-
const
|
|
142
|
-
|
|
140
|
+
let projects = readProjects(prefsDir);
|
|
141
|
+
if (projects.length === 0) {
|
|
142
|
+
projects = [synthesizeDefaultProject(projectRoot)];
|
|
143
|
+
}
|
|
144
|
+
// Enrich each project with its worca-cc version
|
|
145
|
+
const enriched = projects.map((p) => ({
|
|
146
|
+
...p,
|
|
147
|
+
worcaVersion: readProjectWorcaVersion(p.path),
|
|
148
|
+
}));
|
|
149
|
+
res.json({ ok: true, projects: enriched });
|
|
143
150
|
});
|
|
144
151
|
|
|
145
152
|
// POST /api/projects — create a new project
|
|
@@ -203,7 +210,17 @@ export function createProjectScopedRoutes() {
|
|
|
203
210
|
router.get('/runs', requireWorcaDir, (req, res) => {
|
|
204
211
|
try {
|
|
205
212
|
const runs = discoverRuns(req.project.worcaDir);
|
|
206
|
-
|
|
213
|
+
const response = { ok: true, runs };
|
|
214
|
+
// Include settings so multi-project clients can use loop limits, etc.
|
|
215
|
+
const { settingsPath } = req.project;
|
|
216
|
+
if (settingsPath && existsSync(settingsPath)) {
|
|
217
|
+
try {
|
|
218
|
+
response.settings = readMergedSettings(settingsPath);
|
|
219
|
+
} catch {
|
|
220
|
+
/* non-fatal — runs still returned */
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
res.json(response);
|
|
207
224
|
} catch (err) {
|
|
208
225
|
res.status(500).json({ ok: false, error: err.message });
|
|
209
226
|
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
// server/versions.js — version fetching + caching for worca-cc and @worca/ui
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { readPreferences } from './preferences.js';
|
|
6
|
+
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
|
|
9
|
+
/** Cache: { data, timestamp } */
|
|
10
|
+
let _cache = null;
|
|
11
|
+
const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Compare two semver-ish strings. Returns -1, 0, or 1.
|
|
15
|
+
* Strips pre-release suffixes for comparison (e.g. "rc", "dev", "alpha").
|
|
16
|
+
* @param {string} a
|
|
17
|
+
* @param {string} b
|
|
18
|
+
* @returns {number}
|
|
19
|
+
*/
|
|
20
|
+
export function compareVersions(a, b) {
|
|
21
|
+
if (!a || !b) return 0;
|
|
22
|
+
const parse = (v) =>
|
|
23
|
+
v.split('.').map((s) => {
|
|
24
|
+
const n = parseInt(s, 10);
|
|
25
|
+
return Number.isNaN(n) ? 0 : n;
|
|
26
|
+
});
|
|
27
|
+
const pa = parse(a);
|
|
28
|
+
const pb = parse(b);
|
|
29
|
+
const len = Math.max(pa.length, pb.length);
|
|
30
|
+
for (let i = 0; i < len; i++) {
|
|
31
|
+
const x = pa[i] || 0;
|
|
32
|
+
const y = pb[i] || 0;
|
|
33
|
+
if (x > y) return 1;
|
|
34
|
+
if (x < y) return -1;
|
|
35
|
+
}
|
|
36
|
+
return 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Parse a PyPI pre-release version like "0.6.0rc7" → { base: "0.6.0", rc: 7 }
|
|
41
|
+
* Returns null if not an RC version.
|
|
42
|
+
* @param {string} versionStr
|
|
43
|
+
* @returns {{ base: string, rc: number } | null}
|
|
44
|
+
*/
|
|
45
|
+
export function parsePyPIPreRelease(versionStr) {
|
|
46
|
+
const match = versionStr.match(/^(.+?)rc(\d+)$/);
|
|
47
|
+
if (!match) return null;
|
|
48
|
+
return { base: match[1], rc: parseInt(match[2], 10) };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Fetch latest + latestRc versions from npm registry.
|
|
53
|
+
* @param {string} packageName
|
|
54
|
+
* @returns {Promise<{ latest: string|null, latestRc: string|null }>}
|
|
55
|
+
*/
|
|
56
|
+
export async function fetchNpmVersions(packageName) {
|
|
57
|
+
const nullResult = { latest: null, latestRc: null };
|
|
58
|
+
try {
|
|
59
|
+
const controller = new AbortController();
|
|
60
|
+
const timer = setTimeout(() => controller.abort(), 3000);
|
|
61
|
+
let res;
|
|
62
|
+
try {
|
|
63
|
+
res = await fetch(`https://registry.npmjs.org/${packageName}`, {
|
|
64
|
+
signal: controller.signal,
|
|
65
|
+
});
|
|
66
|
+
} finally {
|
|
67
|
+
clearTimeout(timer);
|
|
68
|
+
}
|
|
69
|
+
if (!res.ok) return nullResult;
|
|
70
|
+
const data = await res.json();
|
|
71
|
+
const latest = data['dist-tags']?.latest || null;
|
|
72
|
+
let latestRc = data['dist-tags']?.rc || null;
|
|
73
|
+
if (!latestRc && data.versions) {
|
|
74
|
+
// Scan version keys for highest *-rc.* pattern
|
|
75
|
+
let bestRc = null;
|
|
76
|
+
let bestRcNum = -1;
|
|
77
|
+
for (const ver of Object.keys(data.versions)) {
|
|
78
|
+
const rcMatch = ver.match(/^(.+)-rc\.(\d+)$/);
|
|
79
|
+
if (rcMatch) {
|
|
80
|
+
const rcNum = parseInt(rcMatch[2], 10);
|
|
81
|
+
const base = rcMatch[1];
|
|
82
|
+
// Compare base+rcNum to find the highest RC
|
|
83
|
+
if (
|
|
84
|
+
!bestRc ||
|
|
85
|
+
compareVersions(base, bestRc.base) > 0 ||
|
|
86
|
+
(compareVersions(base, bestRc.base) === 0 && rcNum > bestRcNum)
|
|
87
|
+
) {
|
|
88
|
+
bestRc = { base, full: ver };
|
|
89
|
+
bestRcNum = rcNum;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (bestRc) latestRc = bestRc.full;
|
|
94
|
+
}
|
|
95
|
+
return { latest, latestRc };
|
|
96
|
+
} catch {
|
|
97
|
+
return nullResult;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Fetch latest + latestRc versions from PyPI.
|
|
103
|
+
* @param {string} packageName
|
|
104
|
+
* @returns {Promise<{ latest: string|null, latestRc: string|null }>}
|
|
105
|
+
*/
|
|
106
|
+
export async function fetchPyPIVersions(packageName) {
|
|
107
|
+
const nullResult = { latest: null, latestRc: null };
|
|
108
|
+
try {
|
|
109
|
+
const controller = new AbortController();
|
|
110
|
+
const timer = setTimeout(() => controller.abort(), 3000);
|
|
111
|
+
let res;
|
|
112
|
+
try {
|
|
113
|
+
res = await fetch(`https://pypi.org/pypi/${packageName}/json`, {
|
|
114
|
+
signal: controller.signal,
|
|
115
|
+
});
|
|
116
|
+
} finally {
|
|
117
|
+
clearTimeout(timer);
|
|
118
|
+
}
|
|
119
|
+
if (!res.ok) return nullResult;
|
|
120
|
+
const data = await res.json();
|
|
121
|
+
const latest = data.info?.version || null;
|
|
122
|
+
// Scan releases for highest rc version
|
|
123
|
+
let latestRc = null;
|
|
124
|
+
if (data.releases) {
|
|
125
|
+
let bestRcNum = -1;
|
|
126
|
+
let bestBase = null;
|
|
127
|
+
for (const ver of Object.keys(data.releases)) {
|
|
128
|
+
const parsed = parsePyPIPreRelease(ver);
|
|
129
|
+
if (parsed) {
|
|
130
|
+
if (
|
|
131
|
+
!bestBase ||
|
|
132
|
+
compareVersions(parsed.base, bestBase) > 0 ||
|
|
133
|
+
(compareVersions(parsed.base, bestBase) === 0 &&
|
|
134
|
+
parsed.rc > bestRcNum)
|
|
135
|
+
) {
|
|
136
|
+
bestBase = parsed.base;
|
|
137
|
+
bestRcNum = parsed.rc;
|
|
138
|
+
latestRc = ver;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return { latest, latestRc };
|
|
144
|
+
} catch {
|
|
145
|
+
return nullResult;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Read versions from local dev path.
|
|
151
|
+
* @param {string} sourceRepo - path to local worca-cc repo
|
|
152
|
+
* @returns {{ worcaCc: string|null, worcaUi: string|null }}
|
|
153
|
+
*/
|
|
154
|
+
export function getDevPathVersions(sourceRepo) {
|
|
155
|
+
const result = { worcaCc: null, worcaUi: null };
|
|
156
|
+
if (!sourceRepo) return result;
|
|
157
|
+
try {
|
|
158
|
+
const pyproject = readFileSync(join(sourceRepo, 'pyproject.toml'), 'utf8');
|
|
159
|
+
const match = pyproject.match(/^version\s*=\s*"([^"]+)"/m);
|
|
160
|
+
if (match) result.worcaCc = match[1];
|
|
161
|
+
} catch {
|
|
162
|
+
// pyproject.toml not found or unreadable
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
const pkg = JSON.parse(
|
|
166
|
+
readFileSync(join(sourceRepo, 'worca-ui', 'package.json'), 'utf8'),
|
|
167
|
+
);
|
|
168
|
+
if (pkg.version) result.worcaUi = pkg.version;
|
|
169
|
+
} catch {
|
|
170
|
+
// package.json not found or unreadable
|
|
171
|
+
}
|
|
172
|
+
return result;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Get installed @worca/ui version from own package.json.
|
|
177
|
+
* @returns {string|null}
|
|
178
|
+
*/
|
|
179
|
+
function getInstalledUiVersion() {
|
|
180
|
+
try {
|
|
181
|
+
const pkg = JSON.parse(
|
|
182
|
+
readFileSync(join(__dirname, '..', 'package.json'), 'utf8'),
|
|
183
|
+
);
|
|
184
|
+
return pkg.version || null;
|
|
185
|
+
} catch {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Main orchestrator: fetch all version info with caching.
|
|
192
|
+
* @param {{ prefsPath?: string|null, worcaVersion?: object|null, force?: boolean }} options
|
|
193
|
+
* @returns {Promise<object>}
|
|
194
|
+
*/
|
|
195
|
+
export async function getVersionInfo({ prefsPath, worcaVersion, force } = {}) {
|
|
196
|
+
// Return cached result if fresh
|
|
197
|
+
if (!force && _cache && Date.now() - _cache.timestamp < CACHE_TTL_MS) {
|
|
198
|
+
return _cache.data;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Read source_repo from preferences
|
|
202
|
+
let sourceRepo = null;
|
|
203
|
+
if (prefsPath) {
|
|
204
|
+
const prefs = readPreferences(prefsPath);
|
|
205
|
+
sourceRepo = prefs.source_repo || null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Fetch in parallel
|
|
209
|
+
const [npmResult, pypiResult] = await Promise.allSettled([
|
|
210
|
+
fetchNpmVersions('@worca/ui'),
|
|
211
|
+
fetchPyPIVersions('worca-cc'),
|
|
212
|
+
]);
|
|
213
|
+
|
|
214
|
+
const npm =
|
|
215
|
+
npmResult.status === 'fulfilled'
|
|
216
|
+
? npmResult.value
|
|
217
|
+
: { latest: null, latestRc: null };
|
|
218
|
+
const pypi =
|
|
219
|
+
pypiResult.status === 'fulfilled'
|
|
220
|
+
? pypiResult.value
|
|
221
|
+
: { latest: null, latestRc: null };
|
|
222
|
+
|
|
223
|
+
// Dev path versions
|
|
224
|
+
const devVersions = sourceRepo ? getDevPathVersions(sourceRepo) : null;
|
|
225
|
+
|
|
226
|
+
// Installed versions
|
|
227
|
+
const installedUi = getInstalledUiVersion();
|
|
228
|
+
const installedCc = worcaVersion?.installed || null;
|
|
229
|
+
|
|
230
|
+
const data = {
|
|
231
|
+
ok: true,
|
|
232
|
+
worcaCc: {
|
|
233
|
+
installed: installedCc,
|
|
234
|
+
latest: pypi.latest,
|
|
235
|
+
latestRc: pypi.latestRc,
|
|
236
|
+
},
|
|
237
|
+
worcaUi: {
|
|
238
|
+
installed: installedUi,
|
|
239
|
+
latest: npm.latest,
|
|
240
|
+
latestRc: npm.latestRc,
|
|
241
|
+
},
|
|
242
|
+
devPath: devVersions
|
|
243
|
+
? {
|
|
244
|
+
path: sourceRepo,
|
|
245
|
+
worcaCc: devVersions.worcaCc,
|
|
246
|
+
worcaUi: devVersions.worcaUi,
|
|
247
|
+
}
|
|
248
|
+
: null,
|
|
249
|
+
activeWorcaCc: devVersions?.worcaCc || installedCc,
|
|
250
|
+
cachedAt: new Date().toISOString(),
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
_cache = { data, timestamp: Date.now() };
|
|
254
|
+
return data;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Clear the cache (for testing). */
|
|
258
|
+
export function clearCache() {
|
|
259
|
+
_cache = null;
|
|
260
|
+
}
|
package/server/worca-setup.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { spawn } from 'node:child_process';
|
|
12
|
-
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
13
13
|
import { join } from 'node:path';
|
|
14
14
|
|
|
15
15
|
/**
|
|
@@ -19,6 +19,23 @@ export function checkWorcaInstalled(projectPath) {
|
|
|
19
19
|
return existsSync(join(projectPath, '.claude', 'worca'));
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Read the worca-cc version from a project's .claude/worca/__init__.py.
|
|
24
|
+
* Returns the version string or null if not found.
|
|
25
|
+
*/
|
|
26
|
+
export function readProjectWorcaVersion(projectPath) {
|
|
27
|
+
try {
|
|
28
|
+
const initPy = readFileSync(
|
|
29
|
+
join(projectPath, '.claude', 'worca', '__init__.py'),
|
|
30
|
+
'utf8',
|
|
31
|
+
);
|
|
32
|
+
const match = initPy.match(/^__version__\s*=\s*["']([^"']+)["']/m);
|
|
33
|
+
return match ? match[1] : null;
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
22
39
|
/**
|
|
23
40
|
* Spawn `worca init --upgrade` in the target project directory.
|
|
24
41
|
* Optionally passes --source if a source repo path is provided.
|