@centralu/darwin-arm64 0.1.0-beta.14 → 0.1.0-beta.16

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.
@@ -9,9 +9,9 @@
9
9
  // not survive the way back from Windows; tunnel.ts). The sentences for the person are the hub's.
10
10
  import { spawnSync } from 'node:child_process'
11
11
  import { createHash, randomBytes } from 'node:crypto'
12
- import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from 'node:fs'
12
+ import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs'
13
13
  import { homedir } from 'node:os'
14
- import { join } from 'node:path'
14
+ import { basename, join } from 'node:path'
15
15
  import { pathToFileURL } from 'node:url'
16
16
 
17
17
  const WORD = /^[0-9A-Za-z][0-9A-Za-z.+-]{0,63}$/
@@ -86,27 +86,79 @@ export function prune(l) {
86
86
  }
87
87
  }
88
88
  sweep(l.versions, new Set(keep.map((p) => p.version)))
89
+ // What `placeVersion` moved aside and could not remove then
90
+ try {
91
+ for (const name of readdirSync(l.root).filter((n) => n.startsWith('.replaced-'))) {
92
+ try {
93
+ rmSync(join(l.root, name), { recursive: true, force: true })
94
+ } catch {
95
+ left.push(name)
96
+ }
97
+ }
98
+ } catch {}
89
99
  sweep(l.node, new Set(keep.map((p) => `v${p.node}`)))
90
100
  return { removed, left }
91
101
  }
92
102
 
93
- /** One installer at a time per data folder; a lock whose process is gone is taken over */
94
- function lock(l) {
95
- for (let i = 0; i < 2; i++) {
103
+ /** A lock folder with no pid in it, younger than this, is held: its installer has made it and not yet written the pid */
104
+ const LOCK_YOUNG_MS = 60_000
105
+
106
+ /**
107
+ * One installer at a time per data folder; a lock whose process is gone is taken over.
108
+ *
109
+ * Taking it is `mkdir`, which only one installer can do; the pid follows. A lock found with no pid is
110
+ * held while it is young (another installer between the two, which reading it as pid 0 once took
111
+ * over), and stale only after `LOCK_YOUNG_MS` (an installer that died between them). A stale lock is
112
+ * taken over by moving it aside first, so of two installers that both found it stale only one removes
113
+ * it, and one that moved a lock just replaced by a live one puts it back. Unlock removes the lock only
114
+ * while it is still this run's own (its `token`).
115
+ */
116
+ export function lock(l) {
117
+ const token = `${process.pid}-${randomBytes(8).toString('hex')}`
118
+ for (let i = 0; i < 3; i++) {
96
119
  try {
97
120
  mkdirSync(l.lock)
98
121
  writeFileSync(join(l.lock, 'pid'), String(process.pid))
99
- return () => rmSync(l.lock, { recursive: true, force: true })
122
+ writeFileSync(join(l.lock, 'token'), token)
123
+ return () => {
124
+ if (readFileSafe(join(l.lock, 'token')) === token) rmSync(l.lock, { recursive: true, force: true })
125
+ }
100
126
  } catch (err) {
101
127
  if (err.code !== 'EEXIST') throw err
102
- const pid = Number(readFileSafe(join(l.lock, 'pid')))
103
- if (pid > 0 && alive(pid)) throw new Refusal('busy', String(pid))
104
- rmSync(l.lock, { recursive: true, force: true })
105
128
  }
129
+ const held = readFileSafe(join(l.lock, 'pid'))
130
+ if (!lockIsStale(l.lock, held)) throw new Refusal('busy', held.trim())
131
+ const aside = `${l.lock}.${token}.stale`
132
+ try {
133
+ renameSync(l.lock, aside)
134
+ } catch {
135
+ continue // gone already, or another installer moved it first
136
+ }
137
+ const moved = readFileSafe(join(aside, 'pid'))
138
+ if (moved !== held) {
139
+ // Replaced by a live lock between reading it and moving it: not ours to remove
140
+ try {
141
+ renameSync(aside, l.lock)
142
+ } catch {
143
+ // Someone holds a newer one; this one stays aside, named, and harmless
144
+ }
145
+ throw new Refusal('busy', moved.trim())
146
+ }
147
+ rmSync(aside, { recursive: true, force: true })
106
148
  }
107
149
  throw new Refusal('busy')
108
150
  }
109
151
 
152
+ function lockIsStale(dir, pidText) {
153
+ const pid = Number(pidText.trim())
154
+ if (pidText.trim() && pid > 0) return !alive(pid)
155
+ try {
156
+ return Date.now() - statSync(dir).mtimeMs > LOCK_YOUNG_MS
157
+ } catch {
158
+ return true // gone: the next turn takes the lock
159
+ }
160
+ }
161
+
110
162
  function readFileSafe(file) {
111
163
  try {
112
164
  return readFileSync(file, 'utf8')
@@ -144,10 +196,17 @@ export async function fetchChecked(pkg) {
144
196
  return bytes
145
197
  }
146
198
 
147
- /** Where the host sits in the platform package, and what of the package a remote never runs (plan S6) */
148
- function platformParts(platform) {
149
- if (platform === 'win32-x64') return { host: join('Centralu', 'resources', 'host', 'main.mjs'), drop: [join('Centralu', 'centralu.exe')] }
150
- return { host: join('host', 'main.mjs'), drop: ['Centralu.AppImage'] }
199
+ /**
200
+ * Which package brings the host, where the host sits in it, and what of it a remote never runs (plan
201
+ * S6). The host-only package (`@centralu/host-<platform>`) is the host and nothing else; a version
202
+ * published before it existed comes as its platform package, whose window is thrown away
203
+ */
204
+ function hostParts(platform, packages) {
205
+ const hostOnly = `@centralu/host-${platform}`
206
+ if (packages.some((pkg) => pkg.name === hostOnly)) return { pkg: hostOnly, host: join('host', 'main.mjs'), drop: [] }
207
+ const pkg = `@centralu/${platform}`
208
+ if (platform === 'win32-x64') return { pkg, host: join('Centralu', 'resources', 'host', 'main.mjs'), drop: [join('Centralu', 'centralu.exe')] }
209
+ return { pkg, host: join('host', 'main.mjs'), drop: ['Centralu.AppImage'] }
151
210
  }
152
211
 
153
212
  function sameInstall(dir, p) {
@@ -159,6 +218,32 @@ function sameInstall(dir, p) {
159
218
  }
160
219
  }
161
220
 
221
+ /**
222
+ * Puts the unpacked `partial` folder in place as `dir`. A `dir` already there (the same version
223
+ * installed again with other bytes, possibly the one `current` runs) is moved aside first and removed
224
+ * only once the new one is in place, and put back if that fails: removing it first would leave
225
+ * `current` naming nothing after a failed rename, and on Windows a removal can stop halfway on the
226
+ * files of a host running from it (moving that folder is allowed, plan §10.5). What cannot be removed
227
+ * stays aside under a dot name, and the next prune tries again
228
+ */
229
+ export function placeVersion(l, partial, dir) {
230
+ const aside = existsSync(dir) ? join(l.root, `.replaced-${basename(dir)}-${process.pid}-${randomBytes(4).toString('hex')}`) : null
231
+ if (aside) renameSync(dir, aside)
232
+ try {
233
+ renameSync(partial, dir)
234
+ } catch (err) {
235
+ if (aside) renameSync(aside, dir)
236
+ throw err
237
+ }
238
+ if (aside) {
239
+ try {
240
+ rmSync(aside, { recursive: true, force: true })
241
+ } catch {
242
+ // A Windows program still running from it; harmless where it is
243
+ }
244
+ }
245
+ }
246
+
162
247
  /** Installs `p.version` beside what is there, switches `current` to it, and keeps `previous` */
163
248
  export async function install(p, { dataDir = process.env.CC_DATA_DIR || join(homedir(), '.centralu'), platform = process.platform } = {}) {
164
249
  if (!WORD.test(p.version) || !WORD.test(p.node)) throw new Refusal('params', 'version')
@@ -167,7 +252,7 @@ export async function install(p, { dataDir = process.env.CC_DATA_DIR || join(hom
167
252
  const unlock = lock(l)
168
253
  try {
169
254
  const dir = join(l.versions, p.version)
170
- const parts = platformParts(p.platform)
255
+ const parts = hostParts(p.platform, p.packages)
171
256
  if (!sameInstall(dir, p)) {
172
257
  // Everything lands in a folder of its own first; the version's folder appears by one rename
173
258
  const partial = join(l.root, `.partial-${p.version}-${process.pid}`)
@@ -183,7 +268,7 @@ export async function install(p, { dataDir = process.env.CC_DATA_DIR || join(hom
183
268
  if (r.status !== 0) throw new Refusal('unpack', `${pkg.name}: ${(r.stderr || r.error?.message || '').trim().split('\n').at(-1)}`)
184
269
  rmSync(file, { force: true })
185
270
  }
186
- const pkgDir = join(partial, 'node_modules', '@centralu', p.platform)
271
+ const pkgDir = join(partial, 'node_modules', ...parts.pkg.split('/'))
187
272
  if (!existsSync(join(pkgDir, parts.host))) throw new Refusal('no_host', p.platform)
188
273
  for (const d of parts.drop) rmSync(join(pkgDir, d), { force: true })
189
274
  writeFileSync(
@@ -191,8 +276,7 @@ export async function install(p, { dataDir = process.env.CC_DATA_DIR || join(hom
191
276
  JSON.stringify({ version: p.version, node: p.node, platform: p.platform, packages: p.packages.map(({ name, integrity }) => ({ name, integrity })), installedAt: new Date().toISOString(), by: p.hub ?? null }, null, 2) + '\n',
192
277
  )
193
278
  mkdirSync(l.versions, { recursive: true })
194
- rmSync(dir, { recursive: true, force: true })
195
- renameSync(partial, dir)
279
+ placeVersion(l, partial, dir)
196
280
  } finally {
197
281
  rmSync(partial, { recursive: true, force: true })
198
282
  }
@@ -249,8 +333,10 @@ export async function setPointers(p, { dataDir = process.env.CC_DATA_DIR || join
249
333
  return { current: null, previous: null }
250
334
  }
251
335
  const dir = join(l.versions, current.version)
252
- const node = join(l.node, `v${current.node}`)
253
- if (!existsSync(join(dir, 'install.json')) || !existsSync(node)) throw new Refusal('missing', `${current.version} ${current.node}`)
336
+ // Both must be there: a `previous` naming nothing would offer a rollback to a version that is gone
337
+ for (const v of [current, previous].filter(Boolean)) {
338
+ if (!existsSync(join(l.versions, v.version, 'install.json')) || !existsSync(join(l.node, `v${v.node}`))) throw new Refusal('missing', `${v.version} ${v.node}`)
339
+ }
254
340
  await ensureManagedLauncher(l, dir, current.version, platform)
255
341
  if (previous) writeAtomic(l.previous, `${previous.version} ${previous.node}\n`)
256
342
  else rmSync(l.previous, { force: true })
@@ -1,7 +1,7 @@
1
1
  {
2
- "commit": "1a563a8",
2
+ "commit": "bea21b0",
3
3
  "protocolVersion": 1,
4
- "builtAt": "2026-10-09T05:25:26.987Z",
4
+ "builtAt": "2026-10-12T04:37:23.446Z",
5
5
  "runtime": "system-node",
6
6
  "platform": "darwin",
7
7
  "arch": "arm64"
@@ -3718,7 +3718,7 @@ var import_websocket_server = __toESM(require_websocket_server(), 1);
3718
3718
 
3719
3719
  // packages/agent-host/src/adapters/codex/orchestrator-bridge.mjs
3720
3720
  var URL_ = process.env.CC_HOST_URL;
3721
- var TOKEN = process.env.CC_HOST_TOKEN;
3721
+ var TOKEN = process.env.CC_BRIDGE_TOKEN;
3722
3722
  var SESSION_ID = process.env.CC_SESSION_ID;
3723
3723
  var APP_SERVER = process.env.CC_APP_SERVER || null;
3724
3724
  var APP_WAIT_MS = Number(process.env.CC_APP_WAIT_MS) || void 0;
@@ -3748,11 +3748,12 @@ function connect() {
3748
3748
  else p.reject(new Error(f.error?.message ?? "host error"));
3749
3749
  }
3750
3750
  });
3751
- sock.on("close", () => {
3751
+ sock.on("close", (code) => {
3752
3752
  if (ws === sock) ws = null;
3753
+ const why = code === 4001 ? "Centralu refused this bridge's credential; restart the session to get a new one" : "the connection to Centralu closed before it answered; call the tool again";
3753
3754
  for (const [id, p] of pending) {
3754
3755
  pending.delete(id);
3755
- p.reject(new Error("the connection to Centralu closed before it answered; call the tool again"));
3756
+ p.reject(new Error(why));
3756
3757
  }
3757
3758
  });
3758
3759
  sock.on("open", () => {
@@ -3834,7 +3835,7 @@ async function handleApp(id, method, params) {
3834
3835
  if (id !== void 0) err(id, `Unsupported method: ${method}`);
3835
3836
  }
3836
3837
  if (!URL_ || !TOKEN || !SESSION_ID) {
3837
- log("CC_HOST_URL, CC_HOST_TOKEN, and CC_SESSION_ID are required");
3838
+ log("CC_HOST_URL, CC_BRIDGE_TOKEN, and CC_SESSION_ID are required");
3838
3839
  process.exit(1);
3839
3840
  }
3840
3841
  var buf = "";