@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.
- 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 +10 -10
- 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/codex-orchestrator-bridge.mjs +5 -4
- package/Centralu.app/Contents/Resources/content/host/main.mjs +3111 -1298
- package/Centralu.app/Contents/Resources/content/host/remote-install.mjs +105 -19
- package/Centralu.app/Contents/Resources/resources/host/bundle-info.json +2 -2
- package/Centralu.app/Contents/Resources/resources/host/codex-orchestrator-bridge.mjs +5 -4
- package/Centralu.app/Contents/Resources/resources/host/main.mjs +3111 -1298
- package/Centralu.app/Contents/Resources/resources/host/remote-install.mjs +105 -19
- package/Centralu.app/Contents/_CodeSignature/CodeResources +24 -24
- package/package.json +1 -1
|
@@ -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
|
-
/**
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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 =
|
|
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', '
|
|
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
|
-
|
|
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
|
-
|
|
253
|
-
|
|
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 })
|
|
@@ -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.
|
|
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(
|
|
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,
|
|
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 = "";
|