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 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.2",
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.2",
39
- "centralu-linux-x64": "0.1.0-beta.2"
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
  }