@worca/ui 0.1.0-rc.5 → 0.1.1
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 +600 -531
- package/app/main.bundle.js.map +3 -3
- package/app/styles.css +19 -0
- package/package.json +1 -2
- package/server/app.js +14 -0
- package/server/beads-reader.js +70 -146
- package/server/preferences.js +1 -1
- package/server/project-routes.js +64 -18
- package/server/versions.js +260 -0
- package/server/worca-setup.js +18 -1
- package/server/ws-beads-watcher.js +1 -1
|
@@ -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.
|
|
@@ -8,7 +8,7 @@ import { existsSync, watch } from 'node:fs';
|
|
|
8
8
|
import { join, resolve } from 'node:path';
|
|
9
9
|
import { listIssues } from './beads-reader.js';
|
|
10
10
|
|
|
11
|
-
const BEADS_DEBOUNCE_MS =
|
|
11
|
+
const BEADS_DEBOUNCE_MS = 300;
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* @param {{ worcaDir: string, broadcaster: { broadcast: Function }, projectId?: string }} deps
|