centralu 0.1.0-beta.2 → 0.1.0-beta.4
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/bin/centralu.mjs +42 -11
- package/bin/semver.mjs +76 -0
- package/package.json +4 -3
package/bin/centralu.mjs
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* instead of being rediscovered in every function.
|
|
17
17
|
*/
|
|
18
18
|
import { execFileSync, spawn } from 'node:child_process'
|
|
19
|
+
import { copyDiffers, isNewer } from './semver.mjs'
|
|
19
20
|
import { chmodSync, existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
|
|
20
21
|
import { createRequire } from 'node:module'
|
|
21
22
|
import { homedir } from 'node:os'
|
|
@@ -45,6 +46,7 @@ const DESKTOP_ENTRY = join(homedir(), '.local/share/applications/centralu.deskto
|
|
|
45
46
|
*/
|
|
46
47
|
const TARGETS = {
|
|
47
48
|
'darwin-arm64': { pkg: 'centralu-darwin-arm64', artifact: BUNDLE },
|
|
49
|
+
'linux-arm64': { pkg: 'centralu-linux-arm64', artifact: `${APP_NAME}.AppImage` },
|
|
48
50
|
'linux-x64': { pkg: 'centralu-linux-x64', artifact: `${APP_NAME}.AppImage` },
|
|
49
51
|
}
|
|
50
52
|
|
|
@@ -219,17 +221,6 @@ async function latestVersion(timeoutMs = 2000) {
|
|
|
219
221
|
}
|
|
220
222
|
}
|
|
221
223
|
|
|
222
|
-
/** `1.2.10` > `1.2.9` — 문자열 비교로는 틀린다 */
|
|
223
|
-
function isNewer(a, b) {
|
|
224
|
-
const pa = String(a).split('.').map(Number)
|
|
225
|
-
const pb = String(b).split('.').map(Number)
|
|
226
|
-
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
227
|
-
const x = pa[i] ?? 0
|
|
228
|
-
const y = pb[i] ?? 0
|
|
229
|
-
if (x !== y) return x > y
|
|
230
|
-
}
|
|
231
|
-
return false
|
|
232
|
-
}
|
|
233
224
|
|
|
234
225
|
async function update() {
|
|
235
226
|
const res = await latestVersion(8000)
|
|
@@ -247,6 +238,8 @@ async function update() {
|
|
|
247
238
|
const latest = res.version
|
|
248
239
|
if (!isNewer(latest, pkg.version)) {
|
|
249
240
|
console.log(`이미 최신입니다 (${pkg.version}).`)
|
|
241
|
+
// 패키지가 최신이어도 사본은 아닐 수 있다 — `npm i -g`로 올린 사람이 정확히 그 상태다
|
|
242
|
+
notifyIfCopyStale()
|
|
250
243
|
return
|
|
251
244
|
}
|
|
252
245
|
console.log(`${pkg.version} → ${latest} 로 올립니다.`)
|
|
@@ -262,6 +255,43 @@ async function update() {
|
|
|
262
255
|
console.log('끝났습니다. 앱이 떠 있다면 다시 시작해 주세요.')
|
|
263
256
|
}
|
|
264
257
|
|
|
258
|
+
/**
|
|
259
|
+
* /Applications에 넣어둔 사본의 버전. 견줄 것이 없으면 null.
|
|
260
|
+
*
|
|
261
|
+
* `defaults`를 쓰는 이유는 Info.plist가 XML이라는 보장이 없어서다 — 오늘 Tauri가 XML로
|
|
262
|
+
* 쓰고 있을 뿐이고, 바이너리 plist로 바뀌면 정규식으로 읽던 쪽이 조용히 못 읽게 된다.
|
|
263
|
+
* `defaults`는 맥에 항상 있고 둘 다 읽는다.
|
|
264
|
+
*/
|
|
265
|
+
function installedCopyVersion() {
|
|
266
|
+
if (process.platform !== 'darwin' || !existsSync(INSTALLED)) return null
|
|
267
|
+
try {
|
|
268
|
+
const out = execFileSync('/usr/bin/defaults', ['read', join(INSTALLED, 'Contents/Info.plist'), 'CFBundleShortVersionString'], {
|
|
269
|
+
encoding: 'utf8',
|
|
270
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
271
|
+
})
|
|
272
|
+
return out.trim()
|
|
273
|
+
} catch {
|
|
274
|
+
// 못 읽는 것은 말할 거리가 아니다 — 앱은 이미 떴고, 여기서 할 수 있는 말이 없다
|
|
275
|
+
return null
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* 패키지와 /Applications 사본이 어긋났으면 한 줄.
|
|
281
|
+
*
|
|
282
|
+
* `npm i -g centralu`는 패키지만 바꾼다. 그래서 런처는 새것인데 Spotlight에서 열리는 앱은
|
|
283
|
+
* 옛것인 상태가 조용히 만들어진다 — 실제로 그렇게 됐고(beta.1이 /Applications에 남아 있는
|
|
284
|
+
* 채로 패키지만 beta.3), 화면 어디에도 그 사실이 없었다.
|
|
285
|
+
*
|
|
286
|
+
* **말하되 대신 하지 않는다.** 앱 안의 업데이트 줄과 같은 규칙이다. 묻지도 않고 남의
|
|
287
|
+
* /Applications를 덮는 일은 이 패키지가 `install`을 따로 둔 이유 그 자체다.
|
|
288
|
+
*/
|
|
289
|
+
function notifyIfCopyStale() {
|
|
290
|
+
const version = installedCopyVersion()
|
|
291
|
+
if (!copyDiffers(pkg.version, version)) return
|
|
292
|
+
console.log(`\n/Applications 사본은 ${version}입니다 (이 패키지는 ${pkg.version}).\n centralu install`)
|
|
293
|
+
}
|
|
294
|
+
|
|
265
295
|
/** 앱을 띄운 뒤에만 알린다 — 업데이트 확인 때문에 실행이 늦어지면 안 된다 */
|
|
266
296
|
async function notifyIfOutdated() {
|
|
267
297
|
// 여기서는 실패를 **전부 삼킨다.** 앱을 띄우러 온 사람에게 레지스트리 사정을 말할 이유가 없다
|
|
@@ -303,5 +333,6 @@ switch (cmd) {
|
|
|
303
333
|
break
|
|
304
334
|
default:
|
|
305
335
|
run(cmd ? [cmd, ...rest] : [])
|
|
336
|
+
notifyIfCopyStale()
|
|
306
337
|
await notifyIfOutdated()
|
|
307
338
|
}
|
package/bin/semver.mjs
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version comparison for the launcher's update check.
|
|
3
|
+
*
|
|
4
|
+
* Lives in its own file so it can be tested (tooling/launcher-semver.test.ts):
|
|
5
|
+
* the launcher itself runs commands at import time and cannot be imported by a
|
|
6
|
+
* test. package.json "files" lists the whole bin/ directory, so this ships
|
|
7
|
+
* automatically — but note npm silently drops anything "files" does not cover,
|
|
8
|
+
* and an unshipped import only crashes on users' machines.
|
|
9
|
+
*
|
|
10
|
+
* Why not `.split('.').map(Number)`: that turns `0.1.0-beta.2` into
|
|
11
|
+
* [0, 1, NaN, 2], and `NaN !== NaN` ends the comparison at the third slot with
|
|
12
|
+
* `NaN > NaN` = false — so no beta ever saw the next beta. That is exactly how
|
|
13
|
+
* `centralu update` on 0.1.0-beta.1 answered "이미 최신입니다" while beta.2 sat
|
|
14
|
+
* on the registry (found the day beta.2 shipped).
|
|
15
|
+
*/
|
|
16
|
+
export function isNewer(a, b) {
|
|
17
|
+
const [coreA, preA] = split(String(a))
|
|
18
|
+
const [coreB, preB] = split(String(b))
|
|
19
|
+
for (let i = 0; i < 3; i++) {
|
|
20
|
+
const x = coreA[i] ?? 0
|
|
21
|
+
const y = coreB[i] ?? 0
|
|
22
|
+
if (x !== y) return x > y
|
|
23
|
+
}
|
|
24
|
+
// Same core: a release outranks any prerelease of it (1.0.0 > 1.0.0-beta.9).
|
|
25
|
+
if (preA === null && preB === null) return false
|
|
26
|
+
if (preA === null) return true
|
|
27
|
+
if (preB === null) return false
|
|
28
|
+
// Both prereleases: compare dot-separated identifiers, numeric ones as
|
|
29
|
+
// numbers (semver: numeric identifiers always rank below alphanumeric).
|
|
30
|
+
const ia = preA.split('.')
|
|
31
|
+
const ib = preB.split('.')
|
|
32
|
+
for (let i = 0; i < Math.max(ia.length, ib.length); i++) {
|
|
33
|
+
const x = ia[i]
|
|
34
|
+
const y = ib[i]
|
|
35
|
+
if (x === undefined) return false // shorter prerelease ranks lower
|
|
36
|
+
if (y === undefined) return true
|
|
37
|
+
const nx = /^\d+$/.test(x) ? Number(x) : null
|
|
38
|
+
const ny = /^\d+$/.test(y) ? Number(y) : null
|
|
39
|
+
if (nx !== null && ny !== null) {
|
|
40
|
+
if (nx !== ny) return nx > ny
|
|
41
|
+
} else if (nx !== null) {
|
|
42
|
+
return false // numeric < alphanumeric
|
|
43
|
+
} else if (ny !== null) {
|
|
44
|
+
return true
|
|
45
|
+
} else if (x !== y) {
|
|
46
|
+
return x > y
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return false
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Whether the copy in /Applications is a different app than the one this package carries.
|
|
54
|
+
*
|
|
55
|
+
* `npm i -g centralu` replaces the package and nothing else. That is deliberate — this
|
|
56
|
+
* project does not write into someone's /Applications unless asked (`centralu install`) —
|
|
57
|
+
* but the cost is that the two drift apart in silence, and the copy is the one people
|
|
58
|
+
* actually click. So the launcher says so. It still does not act.
|
|
59
|
+
*
|
|
60
|
+
* Not `isNewer`. Drift in either direction is worth a line: a copy *newer* than the package
|
|
61
|
+
* means an update went backwards, which is more surprising, not less.
|
|
62
|
+
*
|
|
63
|
+
* Lives beside `isNewer` because it ships and is tested for the same reason — a launcher
|
|
64
|
+
* already on someone's machine cannot be corrected later. 0.1.0-beta.1 is still out there
|
|
65
|
+
* answering "이미 최신입니다" and always will be.
|
|
66
|
+
*/
|
|
67
|
+
export function copyDiffers(pkgVersion, copyVersion) {
|
|
68
|
+
return typeof copyVersion === 'string' && copyVersion !== '' && copyVersion !== pkgVersion
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function split(v) {
|
|
72
|
+
const dash = v.indexOf('-')
|
|
73
|
+
const core = (dash === -1 ? v : v.slice(0, dash)).split('.').map(Number)
|
|
74
|
+
const pre = dash === -1 ? null : v.slice(dash + 1)
|
|
75
|
+
return [core, pre]
|
|
76
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "centralu",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.4",
|
|
4
4
|
"description": "Run, watch and steer several Claude Code and Codex CLI sessions from one window",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "ijun17",
|
|
@@ -35,7 +35,8 @@
|
|
|
35
35
|
"linux"
|
|
36
36
|
],
|
|
37
37
|
"optionalDependencies": {
|
|
38
|
-
"centralu-darwin-arm64": "0.1.0-beta.
|
|
39
|
-
"centralu-linux-
|
|
38
|
+
"centralu-darwin-arm64": "0.1.0-beta.4",
|
|
39
|
+
"centralu-linux-arm64": "0.1.0-beta.4",
|
|
40
|
+
"centralu-linux-x64": "0.1.0-beta.4"
|
|
40
41
|
}
|
|
41
42
|
}
|