centralu 0.1.0-beta.22 → 0.1.0-beta.23

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
@@ -23,9 +23,10 @@
23
23
  * being first exercised on someone's laptop.
24
24
  */
25
25
  import { execFileSync, spawn } from 'node:child_process'
26
- import { copyDiffers, isNewer } from './semver.mjs'
26
+ import { copyDiffers, isNewer, keepsInstalledCopy } from './semver.mjs'
27
27
  import {
28
28
  asideCopies,
29
+ bundleVersionCommand,
29
30
  busyMessage,
30
31
  earlyExitMessage,
31
32
  executableIn,
@@ -65,7 +66,12 @@ const pkg = require('../package.json')
65
66
 
66
67
  const APP_NAME = 'Centralu'
67
68
  const BUNDLE = `${APP_NAME}.app`
68
- const INSTALLED = `/Applications/${BUNDLE}`
69
+ /*
70
+ * `CENTRALU_INSTALLED_COPY` points `install` and the copy checks at another bundle path. It exists
71
+ * so the launcher's own tests can run `centralu install` against a temporary folder and never
72
+ * against the real /Applications (tooling/launcher-install.test.ts).
73
+ */
74
+ const INSTALLED = process.env.CENTRALU_INSTALLED_COPY || `/Applications/${BUNDLE}`
69
75
 
70
76
  const PLATFORM = process.platform
71
77
  const HOME = homedir()
@@ -275,8 +281,22 @@ function queryRegistry(key) {
275
281
  * **Never done quietly through postinstall.** Writing silently into someone's `/Applications`
276
282
  * erodes trust, and pnpm blocks postinstall by default anyway. This is only run when the
277
283
  * person explicitly asks for it.
284
+ *
285
+ * **Never over a newer local build** (#645), unless `--force`. The in-app update runs this after
286
+ * `npm i -g` with the same rule (`keepsInstalledCopy`): a maintainer's local build
287
+ * (`0.2.0-beta.21.local.1`) waiting for "Apply now" would otherwise be replaced by npm's beta.21.
288
+ * Any released version is replaced, even a higher one: npm is the source of truth for releases,
289
+ * and a release pulled from `latest` must not survive here. The check comes before finding the app
290
+ * package, since keeping the copy needs nothing from it. `--force` replaces a local build on purpose.
278
291
  */
279
- function install() {
292
+ function install(force) {
293
+ const copy = installedCopyVersion()
294
+ if (!force && keepsInstalledCopy(pkg.version, copy)) {
295
+ const where = PLATFORM === 'win32' ? WIN.dir : INSTALLED
296
+ console.log(`${where} holds the local build ${copy}, newer than this package (${pkg.version}), and was left as it is.`)
297
+ console.log('To replace it anyway: centralu install --force')
298
+ return
299
+ }
280
300
  const app = requireApp()
281
301
  if (PLATFORM === 'win32') return installWindows(app)
282
302
  if (PLATFORM !== 'darwin') return installDesktopEntry(app)
@@ -498,6 +518,7 @@ async function update() {
498
518
 
499
519
  /**
500
520
  * The version of the copy placed in /Applications. null if there is nothing to compare.
521
+ * (`bundleVersionCommand`: the in-app update reads it with the same command.)
501
522
  *
502
523
  * `defaults` is used because there is no guarantee Info.plist stays XML — Tauri just happens
503
524
  * to write it as XML today, and if it ever switches to a binary plist, anything reading it
@@ -517,7 +538,8 @@ function installedCopyVersion() {
517
538
  }
518
539
  if (PLATFORM !== 'darwin' || !existsSync(INSTALLED)) return null
519
540
  try {
520
- const out = execFileSync('/usr/bin/defaults', ['read', join(INSTALLED, 'Contents/Info.plist'), 'CFBundleShortVersionString'], {
541
+ const read = bundleVersionCommand(INSTALLED)
542
+ const out = execFileSync(read.command, read.args, {
521
543
  encoding: 'utf8',
522
544
  stdio: ['ignore', 'pipe', 'ignore'],
523
545
  })
@@ -561,7 +583,8 @@ const HELP = `${APP_NAME} ${pkg.version}
561
583
 
562
584
  centralu Launches the app
563
585
  centralu install Registers it in the app list (macOS: /Applications, Linux: a menu entry,
564
- Windows: %LOCALAPPDATA%\\Programs\\Centralu and a Start menu shortcut)
586
+ Windows: %LOCALAPPDATA%\\Programs\\Centralu and a Start menu shortcut).
587
+ A newer local build there is kept; --force replaces it anyway
565
588
  centralu uninstall Removes that registration (conversation history is kept)
566
589
  centralu update Upgrades if a new version is available
567
590
  centralu serve Runs the host alone on this machine, for the app on another computer
@@ -573,7 +596,7 @@ Requires: Node 22+, and the claude or codex CLI (the app's first screen reports
573
596
  const [cmd, ...rest] = process.argv.slice(2)
574
597
  switch (cmd) {
575
598
  case 'install':
576
- install()
599
+ install(rest.includes('--force'))
577
600
  break
578
601
  case 'uninstall':
579
602
  uninstall()
package/bin/platform.mjs CHANGED
@@ -357,3 +357,15 @@ export function busyMessage(path) {
357
357
  export function isBusyError(e) {
358
358
  return e?.code === 'EBUSY' || e?.code === 'EPERM' || e?.code === 'EACCES' || e?.code === 'ENOTEMPTY'
359
359
  }
360
+
361
+ /**
362
+ * The command that prints the version a macOS bundle carries (`CFBundleShortVersionString`).
363
+ *
364
+ * `plutil` reads XML and binary plists alike, so nothing breaks if Tauri ever writes a binary one.
365
+ * The in-app update runs the same command (`installedCopyVersionSource` in
366
+ * `packages/agent-host/src/os.ts`, pinned to this by `updates.test.ts`), so the host and the
367
+ * launcher can never disagree about which version a copy is (#645).
368
+ */
369
+ export function bundleVersionCommand(bundle) {
370
+ return { command: '/usr/bin/plutil', args: ['-extract', 'CFBundleShortVersionString', 'raw', '-o', '-', posix.join(bundle, 'Contents', 'Info.plist')] }
371
+ }
package/bin/semver.mjs CHANGED
@@ -82,3 +82,28 @@ function split(raw) {
82
82
  const pre = dash === -1 ? null : v.slice(dash + 1)
83
83
  return [core, pre]
84
84
  }
85
+
86
+ const LOCAL_BUILD = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)-(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*\.local\.[1-9]\d*$/
87
+
88
+ /** A maintainer's local build: `<base>.local.N`, strict SemVer, with a prerelease base and a positive N (`scripts/local-manifest.mjs`). */
89
+ export function isLocalBuildVersion(v) {
90
+ return typeof v === 'string' && v.length <= 64 && LOCAL_BUILD.test(v)
91
+ }
92
+
93
+ /**
94
+ * Whether the copy `centralu install` made is kept rather than replaced by `target` (#645).
95
+ *
96
+ * npm is the source of truth for releases: any released version in the copy, even a higher one, is
97
+ * replaced by the target, so a release pulled from `latest` (a dist-tag rollback) does not survive in
98
+ * /Applications. The one exception is a maintainer's local build (`<base>.local.N`, `pnpm
99
+ * local-update`, #610/#613) newer than the target: it waits there for "Apply now", and putting the
100
+ * release it was cut from over it would undo it. The copy's version must be well-formed SemVer in
101
+ * that exact shape; anything unreadable or malformed (`garbage`, `$(VERSION)`, an empty file) is
102
+ * replaced, so a corrupt plist or version file can never block every later update.
103
+ *
104
+ * `centralu install` applies this unless `--force` is given; the in-app update applies the mirror in
105
+ * `packages/protocol/src/semver.ts` (pinned answer to answer by `tooling/launcher-semver.test.ts`).
106
+ */
107
+ export function keepsInstalledCopy(target, copyVersion) {
108
+ return isLocalBuildVersion(copyVersion) && isNewer(copyVersion, target)
109
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "centralu",
3
- "version": "0.1.0-beta.22",
3
+ "version": "0.1.0-beta.23",
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",
@@ -37,9 +37,9 @@
37
37
  "win32"
38
38
  ],
39
39
  "optionalDependencies": {
40
- "@centralu/darwin-arm64": "0.1.0-beta.22",
41
- "@centralu/linux-arm64": "0.1.0-beta.22",
42
- "@centralu/linux-x64": "0.1.0-beta.22",
43
- "@centralu/win32-x64": "0.1.0-beta.22"
40
+ "@centralu/darwin-arm64": "0.1.0-beta.23",
41
+ "@centralu/linux-arm64": "0.1.0-beta.23",
42
+ "@centralu/linux-x64": "0.1.0-beta.23",
43
+ "@centralu/win32-x64": "0.1.0-beta.23"
44
44
  }
45
45
  }