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

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 })
@@ -6,15 +6,15 @@
6
6
  <dict>
7
7
  <key>Resources/content/centralu-keeper</key>
8
8
  <data>
9
- S7v2PfHWBgngchkj8HFntKoLjkg=
9
+ S6PmDZBcltPbc+YqfMxnMC7Fzqg=
10
10
  </data>
11
11
  <key>Resources/content/content-manifest.json</key>
12
12
  <data>
13
- l8oZ2yfkDpa0RG9k9n3ggrgll0o=
13
+ FVQzlGY+W0+fwrw5kHnevgtXdlE=
14
14
  </data>
15
15
  <key>Resources/content/content-manifest.json.sig</key>
16
16
  <data>
17
- w6R1VzpdNe393HUjLUrXFIc3lSg=
17
+ M1moCCTklHFZgQKwIoA5ssQ95iY=
18
18
  </data>
19
19
  <key>Resources/content/host/app-template/AGENTS.md</key>
20
20
  <data>
@@ -58,7 +58,7 @@
58
58
  </data>
59
59
  <key>Resources/content/host/bundle-info.json</key>
60
60
  <data>
61
- //UCqNhQ9W91KcpU1LrZRc87ueE=
61
+ axEp9jRvgUmjCUzQreUbTsVqNx8=
62
62
  </data>
63
63
  <key>Resources/content/host/codex-orchestrator-bridge.mjs</key>
64
64
  <data>
@@ -66,7 +66,7 @@
66
66
  </data>
67
67
  <key>Resources/content/host/main.mjs</key>
68
68
  <data>
69
- zi5y0ODJw4+mh9+aEN+6JZ3fNOs=
69
+ 9rA/FpVT1/ZI3E9/g5DTFJYIDQU=
70
70
  </data>
71
71
  <key>Resources/content/host/node_modules/better-sqlite3/build/Release/better_sqlite3.node</key>
72
72
  <data>
@@ -338,7 +338,7 @@
338
338
  </data>
339
339
  <key>Resources/content/host/remote-install.mjs</key>
340
340
  <data>
341
- oBEeGV3tzhnPu+t4hbKvmwGeK8A=
341
+ yl0J+JffsEv7CSEYqn+zj2IPN6g=
342
342
  </data>
343
343
  <key>Resources/content/host/remote-runtime.json</key>
344
344
  <data>
@@ -394,7 +394,7 @@
394
394
  </data>
395
395
  <key>Resources/resources/host/bundle-info.json</key>
396
396
  <data>
397
- //UCqNhQ9W91KcpU1LrZRc87ueE=
397
+ axEp9jRvgUmjCUzQreUbTsVqNx8=
398
398
  </data>
399
399
  <key>Resources/resources/host/codex-orchestrator-bridge.mjs</key>
400
400
  <data>
@@ -402,7 +402,7 @@
402
402
  </data>
403
403
  <key>Resources/resources/host/main.mjs</key>
404
404
  <data>
405
- zi5y0ODJw4+mh9+aEN+6JZ3fNOs=
405
+ 9rA/FpVT1/ZI3E9/g5DTFJYIDQU=
406
406
  </data>
407
407
  <key>Resources/resources/host/node_modules/better-sqlite3/build/Release/better_sqlite3.node</key>
408
408
  <data>
@@ -674,7 +674,7 @@
674
674
  </data>
675
675
  <key>Resources/resources/host/remote-install.mjs</key>
676
676
  <data>
677
- oBEeGV3tzhnPu+t4hbKvmwGeK8A=
677
+ yl0J+JffsEv7CSEYqn+zj2IPN6g=
678
678
  </data>
679
679
  <key>Resources/resources/host/remote-runtime.json</key>
680
680
  <data>
@@ -711,30 +711,30 @@
711
711
  <dict>
712
712
  <key>cdhash</key>
713
713
  <data>
714
- TYOZDMNCfHV/xonCXdmbeHEcvu8=
714
+ NnxR9qphJM40DeAOreaIeIxBijY=
715
715
  </data>
716
716
  <key>requirement</key>
717
- <string>cdhash H"4d83990cc3427c757fc689c25dd99b78711cbeef"</string>
717
+ <string>cdhash H"367c51f6aa6124ce340de00eade688788c418a36"</string>
718
718
  </dict>
719
719
  <key>Resources/content/centralu-keeper</key>
720
720
  <dict>
721
721
  <key>hash2</key>
722
722
  <data>
723
- mT/KBgb3VSMXaLyvH6R3M7HSWZ5U5yIJQjBxjvG5bTI=
723
+ 9lKqLGO/xKRmsO1KsmG9paSS9FIZoNppZb9nW+MaW6U=
724
724
  </data>
725
725
  </dict>
726
726
  <key>Resources/content/content-manifest.json</key>
727
727
  <dict>
728
728
  <key>hash2</key>
729
729
  <data>
730
- Zi9WNUkuR5eGyb732JhIpjs+bm84f0ZsZs0G/pu2n8k=
730
+ DZIWHDz+gm5QeQxBEuEDoEh3lncC5pHJPi8k09MhsKk=
731
731
  </data>
732
732
  </dict>
733
733
  <key>Resources/content/content-manifest.json.sig</key>
734
734
  <dict>
735
735
  <key>hash2</key>
736
736
  <data>
737
- GJT7ghepd8Hzl//eUZiFJv7/uGRoNeZ9dOL60urMs74=
737
+ AkVZ05FH4plYtmB1G7zwZZFPIQKqqeImiB4pSpbT3wU=
738
738
  </data>
739
739
  </dict>
740
740
  <key>Resources/content/host/app-template/AGENTS.md</key>
@@ -811,7 +811,7 @@
811
811
  <dict>
812
812
  <key>hash2</key>
813
813
  <data>
814
- M9oPlezP2R7RPxfStrDSAgd3Es0NNDdgC5zda+/LIe4=
814
+ nsmeQuwa+zGfJyZ6UZcK6nLkUPCz9FLQeFUYatLMctA=
815
815
  </data>
816
816
  </dict>
817
817
  <key>Resources/content/host/codex-orchestrator-bridge.mjs</key>
@@ -825,7 +825,7 @@
825
825
  <dict>
826
826
  <key>hash2</key>
827
827
  <data>
828
- 2q/GzFq/mtLIXcGZfYcteqGx5pKyL/3DKg/fmE4CGCI=
828
+ MX2TWPFloAHS/JrMVB4E8+a+iK6FeilesBB8lRkvOwY=
829
829
  </data>
830
830
  </dict>
831
831
  <key>Resources/content/host/node_modules/better-sqlite3/build/Release/better_sqlite3.node</key>
@@ -1301,7 +1301,7 @@
1301
1301
  <dict>
1302
1302
  <key>hash2</key>
1303
1303
  <data>
1304
- fXZNRS0VI+EkGDBqdBHPzXK+T2KBvjEHC+Q87j86oYc=
1304
+ New4g3YdpbDtGTsBcScEixBPE/i5amL0KBDnwJoahDQ=
1305
1305
  </data>
1306
1306
  </dict>
1307
1307
  <key>Resources/content/host/remote-runtime.json</key>
@@ -1399,7 +1399,7 @@
1399
1399
  <dict>
1400
1400
  <key>hash2</key>
1401
1401
  <data>
1402
- M9oPlezP2R7RPxfStrDSAgd3Es0NNDdgC5zda+/LIe4=
1402
+ nsmeQuwa+zGfJyZ6UZcK6nLkUPCz9FLQeFUYatLMctA=
1403
1403
  </data>
1404
1404
  </dict>
1405
1405
  <key>Resources/resources/host/codex-orchestrator-bridge.mjs</key>
@@ -1413,7 +1413,7 @@
1413
1413
  <dict>
1414
1414
  <key>hash2</key>
1415
1415
  <data>
1416
- 2q/GzFq/mtLIXcGZfYcteqGx5pKyL/3DKg/fmE4CGCI=
1416
+ MX2TWPFloAHS/JrMVB4E8+a+iK6FeilesBB8lRkvOwY=
1417
1417
  </data>
1418
1418
  </dict>
1419
1419
  <key>Resources/resources/host/node_modules/better-sqlite3/build/Release/better_sqlite3.node</key>
@@ -1889,7 +1889,7 @@
1889
1889
  <dict>
1890
1890
  <key>hash2</key>
1891
1891
  <data>
1892
- fXZNRS0VI+EkGDBqdBHPzXK+T2KBvjEHC+Q87j86oYc=
1892
+ New4g3YdpbDtGTsBcScEixBPE/i5amL0KBDnwJoahDQ=
1893
1893
  </data>
1894
1894
  </dict>
1895
1895
  <key>Resources/resources/host/remote-runtime.json</key>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@centralu/darwin-arm64",
3
- "version": "0.1.0-beta.14",
3
+ "version": "0.1.0-beta.15",
4
4
  "description": "Centralu app bundle for macOS on Apple Silicon",
5
5
  "license": "MIT",
6
6
  "author": "ijun17",