centralu 0.1.0-beta.5 → 0.1.0-beta.7

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
@@ -1,14 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Centralu 실행기.
3
+ * The Centralu launcher.
4
4
  *
5
- * **왜 npm으로 주는가:** macOS는 앱을 검사해서 위험하다고 판정하는 게 아니라,
6
- * 파일에 붙은 `com.apple.quarantine` 딱지를 보고 개발자 신원을 따진다. 그 딱지는
7
- * **받아온 프로그램이** 붙인다 — 브라우저는 붙이고 npm은 안 붙인다. 그래서 npm으로
8
- * 설치하면 서명이 애드혹이어도 경고 없이 그냥 열린다 (docs/plans/beta-release-checklist.md §2).
5
+ * **Why this ships through npm:** macOS does not inspect the app and decide it is dangerous —
6
+ * it looks at the `com.apple.quarantine` flag attached to the file and checks the developer's
7
+ * identity from that. That flag is attached **by the program that downloaded it** — a browser
8
+ * attaches it, npm does not. So an npm install just opens with no warning even with an ad hoc
9
+ * signature (docs/plans/beta-release-checklist.md §2).
9
10
  *
10
- * 앱 본체는 아키텍처별 패키지(`centralu-darwin-arm64`)에 들어 있다. 이 패키지는
11
- * 그것을 찾아 띄우는 얇은 껍데기다 — esbuild·swc가 쓰는 것과 같은 구조.
11
+ * The app itself lives inside an architecture-specific package (`centralu-darwin-arm64`). This
12
+ * package is a thin shell that finds it and launches it — the same structure esbuild and swc
13
+ * use.
12
14
  *
13
15
  * Linux is served from the same shim (issue #14). The two platforms disagree about what
14
16
  * "the app" even is — macOS hands a `.app` directory to LaunchServices, Linux runs a
@@ -52,22 +54,22 @@ const TARGETS = {
52
54
 
53
55
  const TARGET = TARGETS[`${process.platform}-${process.arch}`]
54
56
 
55
- /** 아키텍처별 패키지 안의 앱. 못 찾으면 null */
57
+ /** The app inside the architecture-specific package. null if it cannot be found. */
56
58
  function bundledApp() {
57
59
  if (!TARGET) return null
58
60
  let root
59
61
  try {
60
62
  root = dirname(require.resolve(`${TARGET.pkg}/package.json`))
61
63
  } catch {
62
- return null // 이 아키텍처용 패키지가 안 깔린 것 — 아래에서 이유를 말한다
64
+ return null // The package for this architecture is not installed — the reason is explained below.
63
65
  }
64
66
  const app = join(root, TARGET.artifact)
65
67
  return existsSync(app) ? app : null
66
68
  }
67
69
 
68
70
  /**
69
- * 지금 이 기계에서 왜 못 쓰는지를 **구체적으로** 말한다.
70
- * "설치가 안 됐습니다"만 뜨면 사용자가 할 수 있는 일이 없다.
71
+ * States **specifically** why this machine cannot run it right now.
72
+ * If all that shows is "not installed", there is nothing the person can act on.
71
73
  */
72
74
  function explainMissing() {
73
75
  if (!TARGET) {
@@ -75,20 +77,20 @@ function explainMissing() {
75
77
  // Intel Macs, and here is the reason" send the user to different places.
76
78
  if (process.platform === 'darwin') {
77
79
  return (
78
- `${APP_NAME}는 아직 Apple Silicon 전용입니다 (지금 아키텍처: ${process.arch}).\n` +
79
- 'Intel 맥 지원은 네이티브 애드온까지 함께 묶어야 해서 별도 작업으로 잡혀 있습니다.'
80
+ `${APP_NAME} is Apple Silicon only for now (current architecture: ${process.arch}).\n` +
81
+ 'Intel Mac support needs native addons bundled in as well, and is tracked as separate work.'
80
82
  )
81
83
  }
82
84
  const supported = Object.keys(TARGETS).join(', ')
83
85
  return (
84
- `${APP_NAME}는 아직 ${process.platform}/${process.arch}를 지원하지 않습니다.\n` +
85
- `지금 지원하는 조합: ${supported}\n` +
86
- 'https://github.com/ijun17/centralu/issues/14 에 진행 상황이 있습니다.'
86
+ `${APP_NAME} does not support ${process.platform}/${process.arch} yet.\n` +
87
+ `Currently supported combinations: ${supported}\n` +
88
+ 'Progress is tracked at https://github.com/ijun17/centralu/issues/14.'
87
89
  )
88
90
  }
89
91
  return (
90
- `앱 본체 패키지(${TARGET.pkg})를 찾지 못했습니다.\n` +
91
- '설치가 중간에 끊겼을 수 있습니다 — `npm i -g centralu`로 다시 설치해 주세요.'
92
+ `Could not find the app package (${TARGET.pkg}).\n` +
93
+ 'The install may have been interrupted — please reinstall with `npm i -g centralu`.'
92
94
  )
93
95
  }
94
96
 
@@ -102,7 +104,7 @@ function requireApp() {
102
104
  }
103
105
 
104
106
  /**
105
- * 앱을 띄운다. `/Applications`에 설치돼 있으면 그쪽을 먼저 쓴다.
107
+ * Launches the app. If it is installed in `/Applications`, that copy is used first.
106
108
  *
107
109
  * That preference is macOS-only, and not for lack of an equivalent: `centralu install`
108
110
  * on Linux writes a launcher that points back at this same package, so there is never a
@@ -111,7 +113,8 @@ function requireApp() {
111
113
  function run(args) {
112
114
  if (process.platform === 'darwin') {
113
115
  const app = existsSync(INSTALLED) ? INSTALLED : requireApp()
114
- // `open`은 LaunchServices를 거친다 — Dock 아이콘·단일 인스턴스가 그래야 제대로 산다
116
+ // `open` goes through LaunchServices — the dock icon and single-instance behavior only
117
+ // work correctly that way.
115
118
  const r = spawn('open', ['-a', app, ...(args.length ? ['--args', ...args] : [])], { stdio: 'inherit' })
116
119
  r.on('exit', (code) => process.exit(code ?? 0))
117
120
  return
@@ -126,32 +129,34 @@ function run(args) {
126
129
  const app = requireApp()
127
130
  const r = spawn(app, args, { stdio: 'inherit' })
128
131
  r.on('error', (e) => {
129
- console.error(`${app}를 실행하지 못했습니다: ${e.message}`)
132
+ console.error(`Failed to run ${app}: ${e.message}`)
130
133
  // EACCES here means the executable bit did not survive the trip through npm, which
131
134
  // is invisible from the message alone.
132
- if (e.code === 'EACCES') console.error(`실행 권한이 없습니다 — \`chmod +x "${app}"\` 뒤에 다시 시도해 주세요.`)
135
+ if (e.code === 'EACCES') console.error(`No execute permission — run \`chmod +x "${app}"\` then try again.`)
133
136
  process.exit(1)
134
137
  })
135
138
  r.on('exit', (code) => process.exit(code ?? 0))
136
139
  }
137
140
 
138
141
  /**
139
- * 앱을 시스템 메뉴에 등록한다.
142
+ * Registers the app in the system menu.
140
143
  *
141
- * **postinstall로 몰래 하지 않는다.** 남의 `/Applications`에 조용히 쓰는 것은 신뢰를 깎고,
142
- * pnpm은 postinstall을 기본 차단한다. 사용자가 원할 때 명시적으로 부르게 한다.
144
+ * **Never done quietly through postinstall.** Writing silently into someone's `/Applications`
145
+ * erodes trust, and pnpm blocks postinstall by default anyway. This is only run when the
146
+ * person explicitly asks for it.
143
147
  */
144
148
  function install() {
145
149
  const app = requireApp()
146
150
  if (process.platform !== 'darwin') return installDesktopEntry(app)
147
151
  if (existsSync(INSTALLED)) {
148
- console.log(`기존 ${INSTALLED}를 새 버전으로 교체합니다.`)
152
+ console.log(`Replacing the existing ${INSTALLED} with the new version.`)
149
153
  rmSync(INSTALLED, { recursive: true, force: true })
150
154
  }
151
- // cp가 아니라 ditto — 번들의 권한·확장속성을 그대로 옮긴다 (서명이 깨지지 않게)
155
+ // ditto, not cp — carries over the bundle's permissions and extended attributes unchanged
156
+ // (so the signature does not break).
152
157
  execFileSync('/usr/bin/ditto', [app, INSTALLED], { stdio: 'inherit' })
153
- console.log(`설치했습니다: ${INSTALLED}`)
154
- console.log('이제 Launchpad·Spotlight에서도 찾을 수 있습니다.')
158
+ console.log(`Installed: ${INSTALLED}`)
159
+ console.log('It can now be found in Launchpad and Spotlight too.')
155
160
  }
156
161
 
157
162
  /**
@@ -184,27 +189,28 @@ function installDesktopEntry(app) {
184
189
  mkdirSync(dirname(DESKTOP_ENTRY), { recursive: true })
185
190
  writeFileSync(DESKTOP_ENTRY, entry)
186
191
  chmodSync(DESKTOP_ENTRY, 0o755)
187
- console.log(`등록했습니다: ${DESKTOP_ENTRY}`)
188
- console.log('이제 앱 목록에서도 찾을 수 있습니다 (데스크톱에 따라 다시 로그인해야 보일 수 있습니다).')
192
+ console.log(`Registered: ${DESKTOP_ENTRY}`)
193
+ console.log('It can now be found in the app list too (some desktops require logging back in before it shows up).')
189
194
  }
190
195
 
191
196
  function uninstall() {
192
197
  const installed = process.platform === 'darwin' ? INSTALLED : DESKTOP_ENTRY
193
198
  if (!existsSync(installed)) {
194
- console.log(`${installed}가 없습니다 — 지울 것이 없습니다.`)
199
+ console.log(`${installed} does not exist — nothing to remove.`)
195
200
  return
196
201
  }
197
202
  rmSync(installed, { recursive: true, force: true })
198
- console.log(`지웠습니다: ${installed}`)
199
- console.log('패키지 자체를 지우려면: npm uninstall -g centralu')
200
- console.log(`대화 기록은 그대로 남아 있습니다 (~/.centralu). 지우려면 직접 지우세요.`)
203
+ console.log(`Removed: ${installed}`)
204
+ console.log('To remove the package itself: npm uninstall -g centralu')
205
+ console.log(`Conversation history is left in place (~/.centralu). Remove it yourself if you want to.`)
201
206
  }
202
207
 
203
208
  /**
204
- * npm 레지스트리가 곧 업데이트 채널이다 — 서버도 서명 키도 따로 필요 없다.
209
+ * The npm registry itself is the update channel — no separate server or signing key needed.
205
210
  *
206
- * 실패를 뭉뚱그리지 않는다. 못 닿은 것(네트워크)과 없는 것(404)은 **사용자가 할 일이 다르다** —
207
- * 앞은 연결을 보라는 말이고, 뒤는 봐도 소용없다는 말이다.
211
+ * Failures are not lumped together. Being unreachable (network) and not existing (404) call
212
+ * for **different actions from the person** — the first says to check the connection, the
213
+ * second says checking will not help.
208
214
  */
209
215
  async function latestVersion(timeoutMs = 2000) {
210
216
  try {
@@ -217,7 +223,7 @@ async function latestVersion(timeoutMs = 2000) {
217
223
  const version = body?.version
218
224
  return typeof version === 'string' ? { ok: true, version } : { ok: false, reason: 'bad-response' }
219
225
  } catch {
220
- return { ok: false, reason: 'network' } // 네트워크가 없어도 앱은 떠야 한다
226
+ return { ok: false, reason: 'network' } // The app has to launch even with no network.
221
227
  }
222
228
  }
223
229
 
@@ -226,41 +232,44 @@ async function update() {
226
232
  const res = await latestVersion(8000)
227
233
  if (!res.ok) {
228
234
  if (res.reason === 'missing') {
229
- console.error(`레지스트리에 ${pkg.name}가 없습니다. 이름이 바뀌었거나 아직 발행 전입니다.`)
230
- console.error('https://github.com/ijun17/centralu/releases 를 확인해 주세요.')
235
+ console.error(`${pkg.name} is not on the registry. The name may have changed, or it has not been published yet.`)
236
+ console.error('Please check https://github.com/ijun17/centralu/releases.')
231
237
  } else if (res.reason === 'network') {
232
- console.error('레지스트리에 닿지 못했습니다. 네트워크를 확인하고 다시 시도해 주세요.')
238
+ console.error('Could not reach the registry. Please check your network and try again.')
233
239
  } else {
234
- console.error(`레지스트리가 예상 밖의 응답을 했습니다 (${res.reason}).`)
240
+ console.error(`The registry gave an unexpected response (${res.reason}).`)
235
241
  }
236
242
  process.exit(1)
237
243
  }
238
244
  const latest = res.version
239
245
  if (!isNewer(latest, pkg.version)) {
240
- console.log(`이미 최신입니다 (${pkg.version}).`)
241
- // 패키지가 최신이어도 사본은 아닐 수 있다 — `npm i -g`로 올린 사람이 정확히 그 상태다
246
+ console.log(`Already up to date (${pkg.version}).`)
247
+ // The package can be up to date while the copy is not — that is exactly the state of
248
+ // someone who upgraded with `npm i -g`.
242
249
  notifyIfCopyStale()
243
250
  return
244
251
  }
245
- console.log(`${pkg.version} → ${latest} 로 올립니다.`)
252
+ console.log(`Upgrading ${pkg.version} → ${latest}.`)
246
253
  execFileSync('npm', ['i', '-g', `${pkg.name}@${latest}`], { stdio: 'inherit' })
247
- // /Applications에 넣어둔 사람은 그쪽도 함께 갱신해야 옛 버전이 남지 않는다.
254
+ // Someone with a copy in /Applications needs that updated too, or the old version stays
255
+ // behind.
248
256
  // On Linux the menu entry points at the package instead of a copy, so rewriting it is
249
257
  // cheap — but it is still worth doing, because the Exec path is what would go stale.
250
258
  const installed = process.platform === 'darwin' ? INSTALLED : DESKTOP_ENTRY
251
259
  if (existsSync(installed)) {
252
- console.log(`${installed}도 갱신합니다.`)
260
+ console.log(`Updating ${installed} too.`)
253
261
  execFileSync(process.argv[1], ['install'], { stdio: 'inherit' })
254
262
  }
255
- console.log('끝났습니다. 앱이 떠 있다면 다시 시작해 주세요.')
263
+ console.log('Done. If the app is open, please restart it.')
256
264
  }
257
265
 
258
266
  /**
259
- * /Applications에 넣어둔 사본의 버전. 견줄 것이 없으면 null.
267
+ * The version of the copy placed in /Applications. null if there is nothing to compare.
260
268
  *
261
- * `defaults`를 쓰는 이유는 Info.plist가 XML이라는 보장이 없어서다 — 오늘 Tauri가 XML로
262
- * 쓰고 있을 뿐이고, 바이너리 plist로 바뀌면 정규식으로 읽던 쪽이 조용히 못 읽게 된다.
263
- * `defaults`는 맥에 항상 있고 둘 다 읽는다.
269
+ * `defaults` is used because there is no guarantee Info.plist stays XML — Tauri just happens
270
+ * to write it as XML today, and if it ever switches to a binary plist, anything reading it
271
+ * with a regex would silently stop working. `defaults` is always present on a Mac and reads
272
+ * both formats.
264
273
  */
265
274
  function installedCopyVersion() {
266
275
  if (process.platform !== 'darwin' || !existsSync(INSTALLED)) return null
@@ -271,45 +280,48 @@ function installedCopyVersion() {
271
280
  })
272
281
  return out.trim()
273
282
  } catch {
274
- // 못 읽는 것은 말할 거리가 아니다 — 앱은 이미 떴고, 여기서 할 수 있는 말이 없다
283
+ // Failing to read this is not worth mentioning — the app has already launched, and there
284
+ // is nothing useful to say here.
275
285
  return null
276
286
  }
277
287
  }
278
288
 
279
289
  /**
280
- * 패키지와 /Applications 사본이 어긋났으면 한 줄.
290
+ * One line, if the package and the /Applications copy have drifted apart.
281
291
  *
282
- * `npm i -g centralu`는 패키지만 바꾼다. 그래서 런처는 새것인데 Spotlight에서 열리는 앱은
283
- * 옛것인 상태가 조용히 만들어진다 — 실제로 그렇게 됐고(beta.1이 /Applications에 남아 있는
284
- * 채로 패키지만 beta.3), 화면 어디에도 그 사실이 없었다.
292
+ * `npm i -g centralu` only changes the package. So a state quietly forms where the launcher is
293
+ * new but the app that opens from Spotlight is old — this actually happened (beta.1 stayed in
294
+ * /Applications while only the package moved to beta.3), and nothing on screen said so.
285
295
  *
286
- * **말하되 대신 하지 않는다.** 앱 안의 업데이트 줄과 같은 규칙이다. 묻지도 않고 남의
287
- * /Applications를 덮는 일은 이 패키지가 `install`을 따로 둔 이유 그 자체다.
296
+ * **Says it, does not do it.** The same rule as the in-app update line. Overwriting someone's
297
+ * /Applications without asking is the exact reason this package keeps `install` as a separate
298
+ * step.
288
299
  */
289
300
  function notifyIfCopyStale() {
290
301
  const version = installedCopyVersion()
291
302
  if (!copyDiffers(pkg.version, version)) return
292
- console.log(`\n/Applications 사본은 ${version}입니다 (이 패키지는 ${pkg.version}).\n centralu install`)
303
+ console.log(`\nThe /Applications copy is ${version} (this package is ${pkg.version}).\n centralu install`)
293
304
  }
294
305
 
295
- /** 앱을 띄운 뒤에만 알린다 — 업데이트 확인 때문에 실행이 늦어지면 안 된다 */
306
+ /** Only announced after the app has launched — checking for updates must never delay startup. */
296
307
  async function notifyIfOutdated() {
297
- // 여기서는 실패를 **전부 삼킨다.** 앱을 띄우러 온 사람에게 레지스트리 사정을 말할 이유가 없다
308
+ // Every failure here is **swallowed entirely.** Someone who came here to launch the app has
309
+ // no reason to be told about registry troubles.
298
310
  const res = await latestVersion()
299
311
  if (res.ok && isNewer(res.version, pkg.version)) {
300
- console.log(`\n새 버전이 있습니다: ${pkg.version} → ${res.version}\n centralu update`)
312
+ console.log(`\nA new version is available: ${pkg.version} → ${res.version}\n centralu update`)
301
313
  }
302
314
  }
303
315
 
304
316
  const HELP = `${APP_NAME} ${pkg.version}
305
317
 
306
- centralu 앱을 실행합니다
307
- centralu install 앱 목록에 등록합니다 (macOS는 /Applications, Linux는 메뉴 항목)
308
- centralu uninstall 그 등록을 지웁니다 (대화 기록은 남습니다)
309
- centralu update 새 버전이 있으면 올립니다
310
- centralu --version 버전을 출력합니다
318
+ centralu Launches the app
319
+ centralu install Registers it in the app list (macOS: /Applications, Linux: a menu entry)
320
+ centralu uninstall Removes that registration (conversation history is kept)
321
+ centralu update Upgrades if a new version is available
322
+ centralu --version Prints the version
311
323
 
312
- 필요한 것: Node 22+, 그리고 claude 또는 codex CLI (앱 첫 화면이 상태를 알려줍니다)`
324
+ Requires: Node 22+, and the claude or codex CLI (the app's first screen reports the status)`
313
325
 
314
326
  const [cmd, ...rest] = process.argv.slice(2)
315
327
  switch (cmd) {
package/bin/semver.mjs CHANGED
@@ -10,8 +10,8 @@
10
10
  * Why not `.split('.').map(Number)`: that turns `0.1.0-beta.2` into
11
11
  * [0, 1, NaN, 2], and `NaN !== NaN` ends the comparison at the third slot with
12
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).
13
+ * `centralu update` on 0.1.0-beta.1 answered "Already up to date" while
14
+ * beta.2 sat on the registry (found the day beta.2 shipped).
15
15
  */
16
16
  export function isNewer(a, b) {
17
17
  const [coreA, preA] = split(String(a))
@@ -62,7 +62,7 @@ export function isNewer(a, b) {
62
62
  *
63
63
  * Lives beside `isNewer` because it ships and is tested for the same reason — a launcher
64
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.
65
+ * answering "Already up to date" and always will be.
66
66
  */
67
67
  export function copyDiffers(pkgVersion, copyVersion) {
68
68
  return typeof copyVersion === 'string' && copyVersion !== '' && copyVersion !== pkgVersion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "centralu",
3
- "version": "0.1.0-beta.5",
3
+ "version": "0.1.0-beta.7",
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,8 +35,8 @@
35
35
  "linux"
36
36
  ],
37
37
  "optionalDependencies": {
38
- "centralu-darwin-arm64": "0.1.0-beta.5",
39
- "centralu-linux-arm64": "0.1.0-beta.5",
40
- "centralu-linux-x64": "0.1.0-beta.5"
38
+ "centralu-darwin-arm64": "0.1.0-beta.7",
39
+ "centralu-linux-arm64": "0.1.0-beta.7",
40
+ "centralu-linux-x64": "0.1.0-beta.7"
41
41
  }
42
42
  }