@centralu/darwin-arm64 0.1.0-beta.13 → 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.
- package/Centralu.app/Contents/Info.plist +2 -2
- package/Centralu.app/Contents/MacOS/centralu +0 -0
- package/Centralu.app/Contents/MacOS/centralu-keeper +0 -0
- package/Centralu.app/Contents/Resources/content/centralu-keeper +0 -0
- package/Centralu.app/Contents/Resources/content/content-manifest.json +12 -6
- package/Centralu.app/Contents/Resources/content/content-manifest.json.sig +1 -1
- package/Centralu.app/Contents/Resources/content/host/bundle-info.json +2 -2
- package/Centralu.app/Contents/Resources/content/host/main.mjs +1519 -428
- package/Centralu.app/Contents/Resources/content/host/remote-install.mjs +376 -0
- package/Centralu.app/Contents/Resources/resources/host/bundle-info.json +2 -2
- package/Centralu.app/Contents/Resources/resources/host/main.mjs +1519 -428
- package/Centralu.app/Contents/Resources/resources/host/remote-install.mjs +376 -0
- package/Centralu.app/Contents/_CodeSignature/CodeResources +38 -16
- package/package.json +1 -1
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
// Centralu's remote installer, step 2 (docs/plans/remote-hub.md §10.2). The hub sends this file over
|
|
2
|
+
// ssh and runs it on the pinned Node that step 1 placed; it never runs on the hub. It imports Node's
|
|
3
|
+
// own modules only, and reads one argument: the base64 of a JSON object the hub wrote
|
|
4
|
+
// ({ version, node, platform, hub, packages: [{ name, tarball, integrity }], activate? }), or for the
|
|
5
|
+
// update's later steps ({ action: 'pointers', current, previous } and { action: 'prune' }; plan §10.5).
|
|
6
|
+
//
|
|
7
|
+
// It prints one line for the hub to read, `CENTRALU-INSTALL done <json>` or
|
|
8
|
+
// `CENTRALU-INSTALL fail <code> <detail>`, and nothing that matters on its exit code (exit codes do
|
|
9
|
+
// not survive the way back from Windows; tunnel.ts). The sentences for the person are the hub's.
|
|
10
|
+
import { spawnSync } from 'node:child_process'
|
|
11
|
+
import { createHash, randomBytes } from 'node:crypto'
|
|
12
|
+
import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
|
13
|
+
import { homedir } from 'node:os'
|
|
14
|
+
import { basename, join } from 'node:path'
|
|
15
|
+
import { pathToFileURL } from 'node:url'
|
|
16
|
+
|
|
17
|
+
const WORD = /^[0-9A-Za-z][0-9A-Za-z.+-]{0,63}$/
|
|
18
|
+
|
|
19
|
+
class Refusal extends Error {
|
|
20
|
+
constructor(code, detail = '') {
|
|
21
|
+
super(`${code} ${detail}`.trim())
|
|
22
|
+
this.code = code
|
|
23
|
+
this.detail = detail
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The data folder: `CC_DATA_DIR`, else `~/.centralu`, the rule `serve` and the host use */
|
|
28
|
+
export function remoteLayout(dataDir) {
|
|
29
|
+
const root = join(dataDir, 'remote')
|
|
30
|
+
return { root, current: join(root, 'current'), previous: join(root, 'previous'), versions: join(root, 'versions'), node: join(root, 'node'), bin: join(root, 'bin'), lock: join(root, 'install.lock') }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** `current` / `previous`: "<centralu version> <node version>", two plain words, or null */
|
|
34
|
+
export function readPointer(file) {
|
|
35
|
+
try {
|
|
36
|
+
const m = /^\s*(\S+)[ \t]+(\S+)\s*$/.exec(readFileSync(file, 'utf8'))
|
|
37
|
+
return m && WORD.test(m[1]) && WORD.test(m[2]) ? { version: m[1], node: m[2] } : null
|
|
38
|
+
} catch {
|
|
39
|
+
return null
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Replaces `file` by a rename over it, so a reader (the launcher, `serve --connection`) sees the old
|
|
45
|
+
* line or the new one and never half of one. Atomic on posix, and measured atomic on Windows
|
|
46
|
+
* (`renameSync` over an existing file replaces it; plan §10.1)
|
|
47
|
+
*/
|
|
48
|
+
export function writeAtomic(file, text) {
|
|
49
|
+
const tmp = `${file}.${process.pid}-${randomBytes(4).toString('hex')}.tmp`
|
|
50
|
+
writeFileSync(tmp, text)
|
|
51
|
+
renameSync(tmp, file)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Points `current` at `next`, the old `current` becoming `previous` (plan §10.5 step 3) */
|
|
55
|
+
export function switchTo(l, next) {
|
|
56
|
+
const cur = readPointer(l.current)
|
|
57
|
+
if (cur && cur.version === next.version && cur.node === next.node) return { current: next, previous: readPointer(l.previous) }
|
|
58
|
+
if (cur) writeAtomic(l.previous, `${cur.version} ${cur.node}\n`)
|
|
59
|
+
writeAtomic(l.current, `${next.version} ${next.node}\n`)
|
|
60
|
+
return { current: next, previous: cur ?? readPointer(l.previous) }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Removes every version that is neither `current` nor `previous`, and every Node neither uses. A
|
|
65
|
+
* folder that cannot go (a Windows program still running from it) stays and is named, never fatal
|
|
66
|
+
*/
|
|
67
|
+
export function prune(l) {
|
|
68
|
+
const keep = [readPointer(l.current), readPointer(l.previous)].filter(Boolean)
|
|
69
|
+
const removed = []
|
|
70
|
+
const left = []
|
|
71
|
+
const sweep = (dir, kept) => {
|
|
72
|
+
let names = []
|
|
73
|
+
try {
|
|
74
|
+
names = readdirSync(dir)
|
|
75
|
+
} catch {
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
for (const name of names) {
|
|
79
|
+
if (kept.has(name) || name.startsWith('.')) continue
|
|
80
|
+
try {
|
|
81
|
+
rmSync(join(dir, name), { recursive: true, force: true })
|
|
82
|
+
removed.push(name)
|
|
83
|
+
} catch {
|
|
84
|
+
left.push(name)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
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 {}
|
|
99
|
+
sweep(l.node, new Set(keep.map((p) => `v${p.node}`)))
|
|
100
|
+
return { removed, left }
|
|
101
|
+
}
|
|
102
|
+
|
|
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++) {
|
|
119
|
+
try {
|
|
120
|
+
mkdirSync(l.lock)
|
|
121
|
+
writeFileSync(join(l.lock, 'pid'), String(process.pid))
|
|
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
|
+
}
|
|
126
|
+
} catch (err) {
|
|
127
|
+
if (err.code !== 'EEXIST') throw err
|
|
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 })
|
|
148
|
+
}
|
|
149
|
+
throw new Refusal('busy')
|
|
150
|
+
}
|
|
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
|
+
|
|
162
|
+
function readFileSafe(file) {
|
|
163
|
+
try {
|
|
164
|
+
return readFileSync(file, 'utf8')
|
|
165
|
+
} catch {
|
|
166
|
+
return ''
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function alive(pid) {
|
|
171
|
+
try {
|
|
172
|
+
process.kill(pid, 0)
|
|
173
|
+
return true
|
|
174
|
+
} catch (err) {
|
|
175
|
+
return err.code === 'EPERM'
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** The system's tar. On Windows the one in System32 (bsdtar): a GNU tar on PATH reads `C:` as a host */
|
|
180
|
+
function tarProgram() {
|
|
181
|
+
return process.platform === 'win32' ? join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'tar.exe') : 'tar'
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** The tarball's bytes, refused unless their sha512 is the integrity the hub read from the signed metadata */
|
|
185
|
+
export async function fetchChecked(pkg) {
|
|
186
|
+
let res
|
|
187
|
+
try {
|
|
188
|
+
res = await fetch(pkg.tarball, { signal: AbortSignal.timeout(300_000) })
|
|
189
|
+
} catch (err) {
|
|
190
|
+
throw new Refusal('download', `${pkg.name}: ${err.cause?.message ?? err.message}`)
|
|
191
|
+
}
|
|
192
|
+
if (!res.ok) throw new Refusal('download', `${pkg.name}: HTTP ${res.status}`)
|
|
193
|
+
const bytes = Buffer.from(await res.arrayBuffer())
|
|
194
|
+
const have = `sha512-${createHash('sha512').update(bytes).digest('base64')}`
|
|
195
|
+
if (have !== pkg.integrity) throw new Refusal('integrity', pkg.name)
|
|
196
|
+
return bytes
|
|
197
|
+
}
|
|
198
|
+
|
|
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'] }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function sameInstall(dir, p) {
|
|
213
|
+
try {
|
|
214
|
+
const was = JSON.parse(readFileSync(join(dir, 'install.json'), 'utf8'))
|
|
215
|
+
return was.version === p.version && p.packages.every((pkg) => was.packages?.some((w) => w.name === pkg.name && w.integrity === pkg.integrity))
|
|
216
|
+
} catch {
|
|
217
|
+
return false
|
|
218
|
+
}
|
|
219
|
+
}
|
|
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
|
+
|
|
247
|
+
/** Installs `p.version` beside what is there, switches `current` to it, and keeps `previous` */
|
|
248
|
+
export async function install(p, { dataDir = process.env.CC_DATA_DIR || join(homedir(), '.centralu'), platform = process.platform } = {}) {
|
|
249
|
+
if (!WORD.test(p.version) || !WORD.test(p.node)) throw new Refusal('params', 'version')
|
|
250
|
+
const l = remoteLayout(dataDir)
|
|
251
|
+
mkdirSync(l.root, { recursive: true })
|
|
252
|
+
const unlock = lock(l)
|
|
253
|
+
try {
|
|
254
|
+
const dir = join(l.versions, p.version)
|
|
255
|
+
const parts = hostParts(p.platform, p.packages)
|
|
256
|
+
if (!sameInstall(dir, p)) {
|
|
257
|
+
// Everything lands in a folder of its own first; the version's folder appears by one rename
|
|
258
|
+
const partial = join(l.root, `.partial-${p.version}-${process.pid}`)
|
|
259
|
+
rmSync(partial, { recursive: true, force: true })
|
|
260
|
+
try {
|
|
261
|
+
for (const [i, pkg] of p.packages.entries()) {
|
|
262
|
+
const bytes = await fetchChecked(pkg)
|
|
263
|
+
const file = join(partial, `${i}.tgz`)
|
|
264
|
+
const dest = join(partial, 'node_modules', ...pkg.name.split('/'))
|
|
265
|
+
mkdirSync(dest, { recursive: true })
|
|
266
|
+
writeFileSync(file, bytes)
|
|
267
|
+
const r = spawnSync(tarProgram(), ['-xzf', file, '-C', dest, '--strip-components', '1'], { encoding: 'utf8', windowsHide: true })
|
|
268
|
+
if (r.status !== 0) throw new Refusal('unpack', `${pkg.name}: ${(r.stderr || r.error?.message || '').trim().split('\n').at(-1)}`)
|
|
269
|
+
rmSync(file, { force: true })
|
|
270
|
+
}
|
|
271
|
+
const pkgDir = join(partial, 'node_modules', ...parts.pkg.split('/'))
|
|
272
|
+
if (!existsSync(join(pkgDir, parts.host))) throw new Refusal('no_host', p.platform)
|
|
273
|
+
for (const d of parts.drop) rmSync(join(pkgDir, d), { force: true })
|
|
274
|
+
writeFileSync(
|
|
275
|
+
join(partial, 'install.json'),
|
|
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',
|
|
277
|
+
)
|
|
278
|
+
mkdirSync(l.versions, { recursive: true })
|
|
279
|
+
placeVersion(l, partial, dir)
|
|
280
|
+
} finally {
|
|
281
|
+
rmSync(partial, { recursive: true, force: true })
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
// An update installs beside and switches later, once the running host is stopped (plan §10.5):
|
|
285
|
+
// nothing is pointed at the new version and nothing is removed yet. Without a `current`, no
|
|
286
|
+
// launcher either, so the lookup still finds the Centralu that runs now
|
|
287
|
+
if (p.activate === false) {
|
|
288
|
+
return { installed: { version: p.version, node: p.node }, current: readPointer(l.current), previous: readPointer(l.previous), removed: [], left: [] }
|
|
289
|
+
}
|
|
290
|
+
const launcher = await ensureManagedLauncher(l, dir, p.version, platform)
|
|
291
|
+
const pointers = switchTo(l, { version: p.version, node: p.node })
|
|
292
|
+
return { ...pointers, ...prune(l), launcher }
|
|
293
|
+
} finally {
|
|
294
|
+
unlock()
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function launcherPath(l, platform) {
|
|
299
|
+
return join(l.bin, platform === 'win32' ? 'centralu.cmd' : 'centralu')
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** The launcher reads `current` at every start and never changes (S8): written once, never over */
|
|
303
|
+
async function ensureManagedLauncher(l, dir, version, platform) {
|
|
304
|
+
const launcher = launcherPath(l, platform)
|
|
305
|
+
if (!existsSync(launcher)) {
|
|
306
|
+
const serve = await import(pathToFileURL(join(dir, 'node_modules', 'centralu', 'bin', 'serve.mjs')).href)
|
|
307
|
+
if (typeof serve.managedLauncherScript !== 'function') throw new Refusal('no_launcher', version)
|
|
308
|
+
mkdirSync(l.bin, { recursive: true })
|
|
309
|
+
writeAtomic(launcher, serve.managedLauncherScript(platform))
|
|
310
|
+
if (platform !== 'win32') chmodSync(launcher, 0o755)
|
|
311
|
+
}
|
|
312
|
+
return launcher
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const pointerOf = (v) => (v && WORD.test(v.version) && WORD.test(v.node) ? { version: v.version, node: v.node } : null)
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Writes both pointers as given (plan §10.5 steps 3 and 5, and rollback): `previous` first, then
|
|
319
|
+
* `current`, each by rename; a null `previous` removes that file. `current` must name a version and a
|
|
320
|
+
* Node that are both there. A null `current` puts the machine back to having no managed version: the
|
|
321
|
+
* pointers and the launcher go, so the lookup finds the Centralu it ran before (an npm install).
|
|
322
|
+
* Nothing is removed from `versions/` or `node/`
|
|
323
|
+
*/
|
|
324
|
+
export async function setPointers(p, { dataDir = process.env.CC_DATA_DIR || join(homedir(), '.centralu'), platform = process.platform } = {}) {
|
|
325
|
+
const l = remoteLayout(dataDir)
|
|
326
|
+
mkdirSync(l.root, { recursive: true })
|
|
327
|
+
const unlock = lock(l)
|
|
328
|
+
try {
|
|
329
|
+
const current = pointerOf(p.current)
|
|
330
|
+
const previous = pointerOf(p.previous)
|
|
331
|
+
if (!current) {
|
|
332
|
+
for (const f of [l.previous, l.current, launcherPath(l, platform)]) rmSync(f, { force: true })
|
|
333
|
+
return { current: null, previous: null }
|
|
334
|
+
}
|
|
335
|
+
const dir = join(l.versions, current.version)
|
|
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
|
+
}
|
|
340
|
+
await ensureManagedLauncher(l, dir, current.version, platform)
|
|
341
|
+
if (previous) writeAtomic(l.previous, `${previous.version} ${previous.node}\n`)
|
|
342
|
+
else rmSync(l.previous, { force: true })
|
|
343
|
+
writeAtomic(l.current, `${current.version} ${current.node}\n`)
|
|
344
|
+
return { current, previous }
|
|
345
|
+
} finally {
|
|
346
|
+
unlock()
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** Plan §10.5 step 6, on its own: what is neither `current` nor `previous` goes */
|
|
351
|
+
export function pruneNow({ dataDir = process.env.CC_DATA_DIR || join(homedir(), '.centralu') } = {}) {
|
|
352
|
+
const l = remoteLayout(dataDir)
|
|
353
|
+
mkdirSync(l.root, { recursive: true })
|
|
354
|
+
const unlock = lock(l)
|
|
355
|
+
try {
|
|
356
|
+
return { current: readPointer(l.current), previous: readPointer(l.previous), ...prune(l) }
|
|
357
|
+
} finally {
|
|
358
|
+
unlock()
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async function main() {
|
|
363
|
+
try {
|
|
364
|
+
const p = JSON.parse(Buffer.from(process.argv[2] ?? '', 'base64').toString('utf8'))
|
|
365
|
+
const r = p.action === 'pointers' ? await setPointers(p) : p.action === 'prune' ? pruneNow() : await install(p)
|
|
366
|
+
process.stdout.write(`CENTRALU-INSTALL done ${JSON.stringify(r)}\n`)
|
|
367
|
+
} catch (err) {
|
|
368
|
+
const code = err instanceof Refusal ? err.code : 'error'
|
|
369
|
+
const detail = err instanceof Refusal ? err.detail : err.message
|
|
370
|
+
process.stdout.write(`CENTRALU-INSTALL fail ${code} ${String(detail).replace(/\s+/g, ' ')}\n`)
|
|
371
|
+
process.exitCode = 1
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Run as a program (not imported by a test). Node names its main module by its real path: compare that
|
|
376
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) await main()
|
|
@@ -6,15 +6,15 @@
|
|
|
6
6
|
<dict>
|
|
7
7
|
<key>Resources/content/centralu-keeper</key>
|
|
8
8
|
<data>
|
|
9
|
-
|
|
9
|
+
S6PmDZBcltPbc+YqfMxnMC7Fzqg=
|
|
10
10
|
</data>
|
|
11
11
|
<key>Resources/content/content-manifest.json</key>
|
|
12
12
|
<data>
|
|
13
|
-
|
|
13
|
+
FVQzlGY+W0+fwrw5kHnevgtXdlE=
|
|
14
14
|
</data>
|
|
15
15
|
<key>Resources/content/content-manifest.json.sig</key>
|
|
16
16
|
<data>
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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>
|
|
@@ -336,6 +336,10 @@
|
|
|
336
336
|
<data>
|
|
337
337
|
1+oViP+ynn67yvWRtjtBa0o44U4=
|
|
338
338
|
</data>
|
|
339
|
+
<key>Resources/content/host/remote-install.mjs</key>
|
|
340
|
+
<data>
|
|
341
|
+
yl0J+JffsEv7CSEYqn+zj2IPN6g=
|
|
342
|
+
</data>
|
|
339
343
|
<key>Resources/content/host/remote-runtime.json</key>
|
|
340
344
|
<data>
|
|
341
345
|
mErFPiPmd+t1wnFkXnow4FgoOY4=
|
|
@@ -390,7 +394,7 @@
|
|
|
390
394
|
</data>
|
|
391
395
|
<key>Resources/resources/host/bundle-info.json</key>
|
|
392
396
|
<data>
|
|
393
|
-
|
|
397
|
+
axEp9jRvgUmjCUzQreUbTsVqNx8=
|
|
394
398
|
</data>
|
|
395
399
|
<key>Resources/resources/host/codex-orchestrator-bridge.mjs</key>
|
|
396
400
|
<data>
|
|
@@ -398,7 +402,7 @@
|
|
|
398
402
|
</data>
|
|
399
403
|
<key>Resources/resources/host/main.mjs</key>
|
|
400
404
|
<data>
|
|
401
|
-
|
|
405
|
+
9rA/FpVT1/ZI3E9/g5DTFJYIDQU=
|
|
402
406
|
</data>
|
|
403
407
|
<key>Resources/resources/host/node_modules/better-sqlite3/build/Release/better_sqlite3.node</key>
|
|
404
408
|
<data>
|
|
@@ -668,6 +672,10 @@
|
|
|
668
672
|
<data>
|
|
669
673
|
1+oViP+ynn67yvWRtjtBa0o44U4=
|
|
670
674
|
</data>
|
|
675
|
+
<key>Resources/resources/host/remote-install.mjs</key>
|
|
676
|
+
<data>
|
|
677
|
+
yl0J+JffsEv7CSEYqn+zj2IPN6g=
|
|
678
|
+
</data>
|
|
671
679
|
<key>Resources/resources/host/remote-runtime.json</key>
|
|
672
680
|
<data>
|
|
673
681
|
mErFPiPmd+t1wnFkXnow4FgoOY4=
|
|
@@ -703,30 +711,30 @@
|
|
|
703
711
|
<dict>
|
|
704
712
|
<key>cdhash</key>
|
|
705
713
|
<data>
|
|
706
|
-
|
|
714
|
+
NnxR9qphJM40DeAOreaIeIxBijY=
|
|
707
715
|
</data>
|
|
708
716
|
<key>requirement</key>
|
|
709
|
-
<string>cdhash H"
|
|
717
|
+
<string>cdhash H"367c51f6aa6124ce340de00eade688788c418a36"</string>
|
|
710
718
|
</dict>
|
|
711
719
|
<key>Resources/content/centralu-keeper</key>
|
|
712
720
|
<dict>
|
|
713
721
|
<key>hash2</key>
|
|
714
722
|
<data>
|
|
715
|
-
|
|
723
|
+
9lKqLGO/xKRmsO1KsmG9paSS9FIZoNppZb9nW+MaW6U=
|
|
716
724
|
</data>
|
|
717
725
|
</dict>
|
|
718
726
|
<key>Resources/content/content-manifest.json</key>
|
|
719
727
|
<dict>
|
|
720
728
|
<key>hash2</key>
|
|
721
729
|
<data>
|
|
722
|
-
+
|
|
730
|
+
DZIWHDz+gm5QeQxBEuEDoEh3lncC5pHJPi8k09MhsKk=
|
|
723
731
|
</data>
|
|
724
732
|
</dict>
|
|
725
733
|
<key>Resources/content/content-manifest.json.sig</key>
|
|
726
734
|
<dict>
|
|
727
735
|
<key>hash2</key>
|
|
728
736
|
<data>
|
|
729
|
-
|
|
737
|
+
AkVZ05FH4plYtmB1G7zwZZFPIQKqqeImiB4pSpbT3wU=
|
|
730
738
|
</data>
|
|
731
739
|
</dict>
|
|
732
740
|
<key>Resources/content/host/app-template/AGENTS.md</key>
|
|
@@ -803,7 +811,7 @@
|
|
|
803
811
|
<dict>
|
|
804
812
|
<key>hash2</key>
|
|
805
813
|
<data>
|
|
806
|
-
|
|
814
|
+
nsmeQuwa+zGfJyZ6UZcK6nLkUPCz9FLQeFUYatLMctA=
|
|
807
815
|
</data>
|
|
808
816
|
</dict>
|
|
809
817
|
<key>Resources/content/host/codex-orchestrator-bridge.mjs</key>
|
|
@@ -817,7 +825,7 @@
|
|
|
817
825
|
<dict>
|
|
818
826
|
<key>hash2</key>
|
|
819
827
|
<data>
|
|
820
|
-
|
|
828
|
+
MX2TWPFloAHS/JrMVB4E8+a+iK6FeilesBB8lRkvOwY=
|
|
821
829
|
</data>
|
|
822
830
|
</dict>
|
|
823
831
|
<key>Resources/content/host/node_modules/better-sqlite3/build/Release/better_sqlite3.node</key>
|
|
@@ -1289,6 +1297,13 @@
|
|
|
1289
1297
|
IcWJEJvKQ+KH34hPPDSriIAzqDkn6n0nOUmsUDBYPyY=
|
|
1290
1298
|
</data>
|
|
1291
1299
|
</dict>
|
|
1300
|
+
<key>Resources/content/host/remote-install.mjs</key>
|
|
1301
|
+
<dict>
|
|
1302
|
+
<key>hash2</key>
|
|
1303
|
+
<data>
|
|
1304
|
+
New4g3YdpbDtGTsBcScEixBPE/i5amL0KBDnwJoahDQ=
|
|
1305
|
+
</data>
|
|
1306
|
+
</dict>
|
|
1292
1307
|
<key>Resources/content/host/remote-runtime.json</key>
|
|
1293
1308
|
<dict>
|
|
1294
1309
|
<key>hash2</key>
|
|
@@ -1384,7 +1399,7 @@
|
|
|
1384
1399
|
<dict>
|
|
1385
1400
|
<key>hash2</key>
|
|
1386
1401
|
<data>
|
|
1387
|
-
|
|
1402
|
+
nsmeQuwa+zGfJyZ6UZcK6nLkUPCz9FLQeFUYatLMctA=
|
|
1388
1403
|
</data>
|
|
1389
1404
|
</dict>
|
|
1390
1405
|
<key>Resources/resources/host/codex-orchestrator-bridge.mjs</key>
|
|
@@ -1398,7 +1413,7 @@
|
|
|
1398
1413
|
<dict>
|
|
1399
1414
|
<key>hash2</key>
|
|
1400
1415
|
<data>
|
|
1401
|
-
|
|
1416
|
+
MX2TWPFloAHS/JrMVB4E8+a+iK6FeilesBB8lRkvOwY=
|
|
1402
1417
|
</data>
|
|
1403
1418
|
</dict>
|
|
1404
1419
|
<key>Resources/resources/host/node_modules/better-sqlite3/build/Release/better_sqlite3.node</key>
|
|
@@ -1870,6 +1885,13 @@
|
|
|
1870
1885
|
IcWJEJvKQ+KH34hPPDSriIAzqDkn6n0nOUmsUDBYPyY=
|
|
1871
1886
|
</data>
|
|
1872
1887
|
</dict>
|
|
1888
|
+
<key>Resources/resources/host/remote-install.mjs</key>
|
|
1889
|
+
<dict>
|
|
1890
|
+
<key>hash2</key>
|
|
1891
|
+
<data>
|
|
1892
|
+
New4g3YdpbDtGTsBcScEixBPE/i5amL0KBDnwJoahDQ=
|
|
1893
|
+
</data>
|
|
1894
|
+
</dict>
|
|
1873
1895
|
<key>Resources/resources/host/remote-runtime.json</key>
|
|
1874
1896
|
<dict>
|
|
1875
1897
|
<key>hash2</key>
|