centralu 0.1.0-beta.6 → 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 +82 -70
- package/bin/semver.mjs +3 -3
- package/package.json +4 -4
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
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
*
|
|
11
|
-
*
|
|
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
|
-
/**
|
|
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}
|
|
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}
|
|
85
|
-
|
|
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
|
-
|
|
91
|
-
'
|
|
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
|
-
*
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
* **
|
|
142
|
-
* pnpm
|
|
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(
|
|
152
|
+
console.log(`Replacing the existing ${INSTALLED} with the new version.`)
|
|
149
153
|
rmSync(INSTALLED, { recursive: true, force: true })
|
|
150
154
|
}
|
|
151
|
-
//
|
|
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(
|
|
154
|
-
console.log('
|
|
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(
|
|
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(
|
|
199
|
-
console.log('
|
|
200
|
-
console.log(
|
|
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
|
-
*
|
|
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(
|
|
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(
|
|
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(
|
|
241
|
-
//
|
|
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(
|
|
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(
|
|
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
|
|
267
|
+
* The version of the copy placed in /Applications. null if there is nothing to compare.
|
|
260
268
|
*
|
|
261
|
-
* `defaults
|
|
262
|
-
*
|
|
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
|
-
*
|
|
290
|
+
* One line, if the package and the /Applications copy have drifted apart.
|
|
281
291
|
*
|
|
282
|
-
* `npm i -g centralu
|
|
283
|
-
*
|
|
284
|
-
*
|
|
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
|
|
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(`\
|
|
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(`\
|
|
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
|
|
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
|
-
|
|
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 "
|
|
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 "
|
|
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.
|
|
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.
|
|
39
|
-
"centralu-linux-arm64": "0.1.0-beta.
|
|
40
|
-
"centralu-linux-x64": "0.1.0-beta.
|
|
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
|
}
|