@centralu/darwin-arm64 0.1.0-beta.27 → 0.1.0-beta.28
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 +16 -10
- package/Centralu.app/Contents/Resources/content/content-manifest.json.sig +1 -1
- package/Centralu.app/Contents/Resources/content/host/bundle-info.json +3 -2
- package/Centralu.app/Contents/Resources/content/host/centralu-package.json +5 -5
- package/Centralu.app/Contents/Resources/content/host/main.mjs +29862 -27572
- package/Centralu.app/Contents/Resources/content/host/pty-broker.mjs +96 -0
- package/Centralu.app/Contents/Resources/content/host/remote-install.mjs +214 -5
- package/Centralu.app/Contents/Resources/resources/host/bundle-info.json +3 -2
- package/Centralu.app/Contents/Resources/resources/host/centralu-package.json +5 -5
- package/Centralu.app/Contents/Resources/resources/host/main.mjs +29862 -27572
- package/Centralu.app/Contents/Resources/resources/host/pty-broker.mjs +96 -0
- package/Centralu.app/Contents/Resources/resources/host/remote-install.mjs +214 -5
- package/Centralu.app/Contents/_CodeSignature/CodeResources +46 -24
- package/package.json +1 -1
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **The terminal broker** (Windows, #1067; the host side is `broker-pty.ts`).
|
|
3
|
+
*
|
|
4
|
+
* The host starts this process before it joins the job object that ends its work with it (`lifecycle/work-job.ts`), so
|
|
5
|
+
* it is in no job of the host's. The terminals it starts are therefore outside that job: what a person starts from a
|
|
6
|
+
* Centralu terminal (`code .`, the browser `claude auth login` opens, a server started in the background) outlives the
|
|
7
|
+
* host, as it does on macOS and Linux, while agents, Run commands, git and app servers end with it.
|
|
8
|
+
*
|
|
9
|
+
* A plain `.mjs`, started with `node <path>` in development and beside the bundled host alike (the Codex bridge's way,
|
|
10
|
+
* `adapters/codex/bridge-path.ts`). Its whole protocol is one JSON object per line: the host's requests on stdin,
|
|
11
|
+
* what the terminals do on stdout.
|
|
12
|
+
*
|
|
13
|
+
* - in: `{op:'spawn', id, file, args, cwd, env, cols, rows}` · `{op:'write', id, data}` · `{op:'resize', id, cols, rows}`
|
|
14
|
+
* · `{op:'kill', id}`
|
|
15
|
+
* - out: `{ev:'ready'}` once · `{ev:'spawned', id, pid}` · `{ev:'error', id, message}` · `{ev:'data', id, data}` ·
|
|
16
|
+
* `{ev:'exit', id, exitCode, signal}`
|
|
17
|
+
*
|
|
18
|
+
* Its stdin closing means the host is gone: it exits, which closes every pseudoconsole and so ends each shell as the
|
|
19
|
+
* host's own would have. What a shell started outside its console stays.
|
|
20
|
+
*/
|
|
21
|
+
import { createRequire } from 'node:module'
|
|
22
|
+
import { createInterface } from 'node:readline'
|
|
23
|
+
|
|
24
|
+
const require = createRequire(import.meta.url)
|
|
25
|
+
const pty = require('node-pty')
|
|
26
|
+
|
|
27
|
+
/** @type {Map<number, any>} */
|
|
28
|
+
const ptys = new Map()
|
|
29
|
+
/** Kills asked for before the spawn answered (the host does not wait for a pid to close a tab) */
|
|
30
|
+
const killed = new Set()
|
|
31
|
+
|
|
32
|
+
function send(message) {
|
|
33
|
+
process.stdout.write(`${JSON.stringify(message)}\n`)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* On the ConPTY node-pty ships, as the host does (`spawnPty`, #849): Windows' own left a `conhost.exe` per terminal.
|
|
38
|
+
* Only a missing or unloadable `conpty.dll` falls back to Windows' own.
|
|
39
|
+
*/
|
|
40
|
+
function spawnPty(m) {
|
|
41
|
+
const opts = { name: 'xterm-256color', cols: m.cols, rows: m.rows, cwd: m.cwd, env: m.env }
|
|
42
|
+
try {
|
|
43
|
+
return pty.spawn(m.file, m.args, { ...opts, useConptyDll: true })
|
|
44
|
+
} catch (err) {
|
|
45
|
+
if (!/conpty\.dll/i.test(err.message)) throw err
|
|
46
|
+
process.stderr.write(`[pty-broker] ${err.message}; using the console of Windows instead\n`)
|
|
47
|
+
return pty.spawn(m.file, m.args, opts)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function handle(m) {
|
|
52
|
+
if (m.op === 'spawn') {
|
|
53
|
+
let p
|
|
54
|
+
try {
|
|
55
|
+
p = spawnPty(m)
|
|
56
|
+
} catch (err) {
|
|
57
|
+
return send({ ev: 'error', id: m.id, message: err.message })
|
|
58
|
+
}
|
|
59
|
+
ptys.set(m.id, p)
|
|
60
|
+
send({ ev: 'spawned', id: m.id, pid: p.pid })
|
|
61
|
+
p.onData((data) => send({ ev: 'data', id: m.id, data }))
|
|
62
|
+
p.onExit(({ exitCode, signal }) => {
|
|
63
|
+
ptys.delete(m.id)
|
|
64
|
+
send({ ev: 'exit', id: m.id, exitCode, signal: signal ?? null })
|
|
65
|
+
})
|
|
66
|
+
if (killed.delete(m.id)) p.kill()
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
const p = ptys.get(m.id)
|
|
70
|
+
if (m.op === 'kill') {
|
|
71
|
+
if (p) p.kill()
|
|
72
|
+
else killed.add(m.id)
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
if (!p) return
|
|
76
|
+
try {
|
|
77
|
+
if (m.op === 'write') p.write(m.data)
|
|
78
|
+
else if (m.op === 'resize') p.resize(m.cols, m.rows)
|
|
79
|
+
} catch {
|
|
80
|
+
// a pty in the middle of ending: its exit follows
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const lines = createInterface({ input: process.stdin })
|
|
85
|
+
lines.on('line', (line) => {
|
|
86
|
+
let m
|
|
87
|
+
try {
|
|
88
|
+
m = JSON.parse(line)
|
|
89
|
+
} catch {
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
handle(m)
|
|
93
|
+
})
|
|
94
|
+
// The host is gone (closed, crashed, killed): every pseudoconsole closes with this process
|
|
95
|
+
lines.on('close', () => process.exit(0))
|
|
96
|
+
send({ ev: 'ready' })
|
|
@@ -2,16 +2,18 @@
|
|
|
2
2
|
// ssh and runs it on the pinned Node that step 1 placed; it never runs on the hub. It imports Node's
|
|
3
3
|
// own modules only, and reads one argument: the base64 of a JSON object the hub wrote
|
|
4
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)
|
|
5
|
+
// update's later steps ({ action: 'pointers', current, previous } and { action: 'prune' }; plan §10.5), or a dev
|
|
6
|
+
// build of the host from a git commit in a working copy there ({ action: 'dev', repo, commit, … }; `pnpm remote-dev`, #1036)
|
|
7
|
+
// and the copy of the store taken before it first runs ({ action: 'store-backup' | 'store-restore' }).
|
|
6
8
|
//
|
|
7
9
|
// It prints one line for the hub to read, `CENTRALU-INSTALL done <json>` or
|
|
8
10
|
// `CENTRALU-INSTALL fail <code> <detail>`, and nothing that matters on its exit code (exit codes do
|
|
9
11
|
// not survive the way back from Windows; tunnel.ts). The sentences for the person are the hub's.
|
|
10
12
|
import { spawnSync } from 'node:child_process'
|
|
11
13
|
import { createHash, randomBytes } from 'node:crypto'
|
|
12
|
-
import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
|
14
|
+
import { chmodSync, cpSync, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
|
13
15
|
import { homedir } from 'node:os'
|
|
14
|
-
import { basename, join, win32 } from 'node:path'
|
|
16
|
+
import { basename, isAbsolute, join, win32 } from 'node:path'
|
|
15
17
|
import { pathToFileURL } from 'node:url'
|
|
16
18
|
|
|
17
19
|
const WORD = /^[0-9A-Za-z][0-9A-Za-z.+-]{0,63}$/
|
|
@@ -27,7 +29,16 @@ class Refusal extends Error {
|
|
|
27
29
|
/** The data folder: `CC_DATA_DIR`, else `~/.centralu`, the rule `serve` and the host use */
|
|
28
30
|
export function remoteLayout(dataDir) {
|
|
29
31
|
const root = join(dataDir, 'remote')
|
|
30
|
-
return {
|
|
32
|
+
return {
|
|
33
|
+
root,
|
|
34
|
+
current: join(root, 'current'),
|
|
35
|
+
previous: join(root, 'previous'),
|
|
36
|
+
versions: join(root, 'versions'),
|
|
37
|
+
node: join(root, 'node'),
|
|
38
|
+
bin: join(root, 'bin'),
|
|
39
|
+
lock: join(root, 'install.lock'),
|
|
40
|
+
storeBackup: join(root, 'store-backup'),
|
|
41
|
+
}
|
|
31
42
|
}
|
|
32
43
|
|
|
33
44
|
/** `current` / `previous`: "<centralu version> <node version>", two plain words, or null */
|
|
@@ -97,6 +108,13 @@ export function prune(l) {
|
|
|
97
108
|
}
|
|
98
109
|
} catch {}
|
|
99
110
|
sweep(l.node, new Set(keep.map((p) => `v${p.node}`)))
|
|
111
|
+
// The store copies of versions that are gone (#1036): nothing can start them to need the copy any more. Not reported:
|
|
112
|
+
// `removed` names versions and Nodes
|
|
113
|
+
try {
|
|
114
|
+
for (const name of readdirSync(l.storeBackup).filter((n) => !keep.some((p) => p.version === n))) rmSync(join(l.storeBackup, name), { recursive: true, force: true })
|
|
115
|
+
} catch {
|
|
116
|
+
// none, or one still open; the next prune tries again
|
|
117
|
+
}
|
|
100
118
|
return { removed, left }
|
|
101
119
|
}
|
|
102
120
|
|
|
@@ -365,10 +383,201 @@ export function pruneNow({ dataDir = process.env.CC_DATA_DIR || join(homedir(),
|
|
|
365
383
|
}
|
|
366
384
|
}
|
|
367
385
|
|
|
386
|
+
/** The files of the host's store in the data folder: the database and SQLite's write-ahead log beside it */
|
|
387
|
+
const STORE_FILES = ['store.db', 'store.db-wal', 'store.db-shm']
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* A copy of the store taken while no host runs, before a dev build first opens it (#1036): a dev build may migrate the
|
|
391
|
+
* store past what the version before it reads (a step that raises `min_reader_version`), and that version would then
|
|
392
|
+
* refuse to start (`StoreTooNewError`), leaving the machine with no host. The copy is kept per `owner`, the version
|
|
393
|
+
* that left the store so (`store-backup/<owner>/`), replaced whole, and `before` names the build it was taken before.
|
|
394
|
+
* A copy goes with its version (`prune`)
|
|
395
|
+
*/
|
|
396
|
+
export function storeBackup(p, { dataDir = process.env.CC_DATA_DIR || join(homedir(), '.centralu') } = {}) {
|
|
397
|
+
if (!WORD.test(String(p.owner)) || !WORD.test(String(p.before))) throw new Refusal('params', 'store backup')
|
|
398
|
+
const l = remoteLayout(dataDir)
|
|
399
|
+
mkdirSync(l.storeBackup, { recursive: true })
|
|
400
|
+
const unlock = lock(l)
|
|
401
|
+
try {
|
|
402
|
+
const partial = join(l.storeBackup, `.partial-${process.pid}`)
|
|
403
|
+
rmSync(partial, { recursive: true, force: true })
|
|
404
|
+
try {
|
|
405
|
+
mkdirSync(partial, { recursive: true })
|
|
406
|
+
const files = STORE_FILES.filter((f) => existsSync(join(dataDir, f)))
|
|
407
|
+
for (const f of files) cpSync(join(dataDir, f), join(partial, f))
|
|
408
|
+
writeFileSync(join(partial, 'backup.json'), JSON.stringify({ owner: p.owner, before: p.before, files, at: new Date().toISOString() }, null, 2) + '\n')
|
|
409
|
+
rmSync(join(l.storeBackup, p.owner), { recursive: true, force: true })
|
|
410
|
+
renameSync(partial, join(l.storeBackup, p.owner))
|
|
411
|
+
return { saved: files }
|
|
412
|
+
} catch (err) {
|
|
413
|
+
throw err instanceof Refusal ? err : new Refusal('store_backup', err.message)
|
|
414
|
+
} finally {
|
|
415
|
+
rmSync(partial, { recursive: true, force: true })
|
|
416
|
+
}
|
|
417
|
+
} finally {
|
|
418
|
+
unlock()
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Puts back the copy `storeBackup` took for `owner` (the version about to start), with no host running; without one,
|
|
424
|
+
* touches nothing and says so. A write-ahead log the copy did not have is removed, so SQLite does not replay the dev
|
|
425
|
+
* build's changes onto the restored database. What the dev build wrote to the store since is gone; nothing else in the
|
|
426
|
+
* data folder is touched. The copy stays, so a second attempt finds it again
|
|
427
|
+
*/
|
|
428
|
+
export function storeRestore(p, { dataDir = process.env.CC_DATA_DIR || join(homedir(), '.centralu') } = {}) {
|
|
429
|
+
if (!WORD.test(String(p.owner))) throw new Refusal('params', 'store restore')
|
|
430
|
+
const l = remoteLayout(dataDir)
|
|
431
|
+
mkdirSync(l.root, { recursive: true })
|
|
432
|
+
const unlock = lock(l)
|
|
433
|
+
try {
|
|
434
|
+
const dir = join(l.storeBackup, p.owner)
|
|
435
|
+
let meta
|
|
436
|
+
try {
|
|
437
|
+
meta = JSON.parse(readFileSync(join(dir, 'backup.json'), 'utf8'))
|
|
438
|
+
} catch {
|
|
439
|
+
return { restored: false }
|
|
440
|
+
}
|
|
441
|
+
if (meta.owner !== p.owner || !Array.isArray(meta.files)) return { restored: false }
|
|
442
|
+
try {
|
|
443
|
+
for (const f of STORE_FILES) rmSync(join(dataDir, f), { force: true })
|
|
444
|
+
for (const f of meta.files.filter((f) => STORE_FILES.includes(f))) cpSync(join(dir, f), join(dataDir, f))
|
|
445
|
+
} catch (err) {
|
|
446
|
+
throw new Refusal('store_restore', err.message)
|
|
447
|
+
}
|
|
448
|
+
return { restored: true, before: meta.before ?? null, at: meta.at ?? null }
|
|
449
|
+
} finally {
|
|
450
|
+
unlock()
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/** `a` at least `b`, both plain `x.y.z` (a prerelease or build suffix is ignored) */
|
|
455
|
+
function atLeast(a, b) {
|
|
456
|
+
const n = (v) => String(v).replace(/^v/, '').split(/[-+]/)[0].split('.').map((x) => Number(x) || 0)
|
|
457
|
+
const [x, y] = [n(a), n(b)]
|
|
458
|
+
for (let i = 0; i < 3; i++) if ((x[i] ?? 0) !== (y[i] ?? 0)) return (x[i] ?? 0) > (y[i] ?? 0)
|
|
459
|
+
return true
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/** A release a dev build can be cut from: a prerelease, since `<base>.local.N` is SemVer only on one (`isLocalBuildVersion`) */
|
|
463
|
+
const DEV_BASE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*$/
|
|
464
|
+
|
|
465
|
+
/** git in the working copy, by name on PATH as the person runs it there; what it printed, or a refusal */
|
|
466
|
+
function git(repo, args) {
|
|
467
|
+
const r = spawnSync('git', ['-C', repo, ...args], { cwd: repo, encoding: 'utf8', windowsHide: true, maxBuffer: 16 * 1024 * 1024 })
|
|
468
|
+
if (r.error) throw new Refusal('dev_git', r.error.message)
|
|
469
|
+
if (r.status !== 0) throw new Refusal('dev_git', (r.stderr || '').trim().split('\n').at(-1) || `exit ${r.status}`)
|
|
470
|
+
return r.stdout
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/** The last line of what a run printed that says anything */
|
|
474
|
+
function lastSaid(...texts) {
|
|
475
|
+
for (const t of texts) {
|
|
476
|
+
const line = String(t ?? '').trim().split(/\r?\n/).filter((l) => l.trim()).at(-1)
|
|
477
|
+
if (line) return line.trim()
|
|
478
|
+
}
|
|
479
|
+
return ''
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* A dev build of the host from a git commit (`pnpm remote-dev`, #1036; docs/plans/remote-hub.md §10.11), placed beside
|
|
484
|
+
* what is there as `<base>.local.N` with nothing switched: the hub's update steps stop, switch, start and check it
|
|
485
|
+
* (update.ts `switchRemote`).
|
|
486
|
+
*
|
|
487
|
+
* Checked before anything is built: the working copy's HEAD is exactly `p.commit`, it has no changes unless
|
|
488
|
+
* `p.allowDirty`, it speaks `p.protocol` (the protocol the hub links on), and this Node (the pinned one `current` runs
|
|
489
|
+
* on) is not below its floor. Then the working copy's own `bundle.mjs` runs on this Node into the new version's folder
|
|
490
|
+
* (`CC_BUNDLE_OUT`), its launcher package is copied beside with the version stamped into its `package.json` (what the
|
|
491
|
+
* host says it is, through `CC_HOST_SOURCE`), and `install.json` says where it came from:
|
|
492
|
+
* `source: { kind: 'dev', commit, dirty }`. What the build printed goes to `<data>/remote/dev-build.log`.
|
|
493
|
+
*/
|
|
494
|
+
export function devBuild(p, { dataDir = process.env.CC_DATA_DIR || join(homedir(), '.centralu'), platform = process.platform, arch = process.arch } = {}) {
|
|
495
|
+
if (typeof p.repo !== 'string' || !isAbsolute(p.repo)) throw new Refusal('dev_repo', String(p.repo ?? ''))
|
|
496
|
+
if (typeof p.commit !== 'string' || !/^[0-9a-f]{40}$/.test(p.commit)) throw new Refusal('params', 'commit')
|
|
497
|
+
const l = remoteLayout(dataDir)
|
|
498
|
+
mkdirSync(l.root, { recursive: true })
|
|
499
|
+
const unlock = lock(l)
|
|
500
|
+
try {
|
|
501
|
+
const current = readPointer(l.current)
|
|
502
|
+
if (!current) throw new Refusal('dev_unmanaged')
|
|
503
|
+
const repo = p.repo
|
|
504
|
+
const bundler = join(repo, 'packages', 'agent-host', 'scripts', 'bundle.mjs')
|
|
505
|
+
if (!existsSync(bundler)) throw new Refusal('dev_repo', repo)
|
|
506
|
+
const head = git(repo, ['rev-parse', 'HEAD']).trim()
|
|
507
|
+
if (head !== p.commit) throw new Refusal('dev_commit', `${head} ${p.commit}`)
|
|
508
|
+
const changes = git(repo, ['status', '--porcelain']).split('\n').filter((line) => line.trim()).length
|
|
509
|
+
if (changes > 0 && !p.allowDirty) throw new Refusal('dev_dirty', String(changes))
|
|
510
|
+
const shim = JSON.parse(readFileSync(join(repo, 'packaging', 'npm', 'centralu', 'package.json'), 'utf8'))
|
|
511
|
+
const base = String(shim.version ?? '')
|
|
512
|
+
if (!DEV_BASE.test(base) || base.includes('.local.')) throw new Refusal('dev_base', base)
|
|
513
|
+
const protocol = Number(/export const PROTOCOL_VERSION = (\d+)/.exec(readFileSync(join(repo, 'packages', 'protocol', 'src', 'envelope.ts'), 'utf8'))?.[1])
|
|
514
|
+
if (protocol !== p.protocol) throw new Refusal('dev_protocol', `${protocol} ${p.protocol}`)
|
|
515
|
+
const floor = /(\d+\.\d+\.\d+)/.exec(String(shim.engines?.node ?? ''))?.[1]
|
|
516
|
+
if (floor && !atLeast(process.versions.node, floor)) throw new Refusal('dev_node', `${process.versions.node} ${floor}`)
|
|
517
|
+
if (!existsSync(join(repo, 'node_modules', 'esbuild'))) throw new Refusal('dev_deps', repo)
|
|
518
|
+
// The next number after every dev build of this release still there, and any a pointer names
|
|
519
|
+
const local = new RegExp(`^${base.replace(/[.+-]/g, '\\$&')}\\.local\\.([1-9]\\d*)$`)
|
|
520
|
+
let names = []
|
|
521
|
+
try {
|
|
522
|
+
names = readdirSync(l.versions)
|
|
523
|
+
} catch {
|
|
524
|
+
// no versions folder: the first one
|
|
525
|
+
}
|
|
526
|
+
const taken = [...names, current.version, readPointer(l.previous)?.version ?? ''].map((n) => Number(local.exec(n)?.[1] ?? 0))
|
|
527
|
+
const version = `${base}.local.${Math.max(0, ...taken) + 1}`
|
|
528
|
+
if (!WORD.test(version)) throw new Refusal('dev_base', version)
|
|
529
|
+
const target = `${platform}-${arch}`
|
|
530
|
+
const partial = join(l.root, `.partial-${version}-${process.pid}`)
|
|
531
|
+
rmSync(partial, { recursive: true, force: true })
|
|
532
|
+
try {
|
|
533
|
+
const hostPkg = join(partial, 'node_modules', '@centralu', `host-${target}`)
|
|
534
|
+
const r = spawnSync(process.execPath, [bundler], {
|
|
535
|
+
cwd: repo,
|
|
536
|
+
env: { ...process.env, CC_BUNDLE_OUT: join(hostPkg, 'host') },
|
|
537
|
+
encoding: 'utf8',
|
|
538
|
+
windowsHide: true,
|
|
539
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
540
|
+
timeout: 20 * 60_000,
|
|
541
|
+
})
|
|
542
|
+
writeFileSync(join(l.root, 'dev-build.log'), `${r.stdout ?? ''}${r.stderr ?? ''}${r.error ? `\n${r.error.message}\n` : ''}`)
|
|
543
|
+
if (r.status !== 0 || !existsSync(join(hostPkg, 'host', 'main.mjs'))) throw new Refusal('dev_build', lastSaid(r.stderr, r.error?.message, r.stdout) || `exit ${r.status}`)
|
|
544
|
+
writeFileSync(join(hostPkg, 'package.json'), JSON.stringify({ name: `@centralu/host-${target}`, version, files: ['host'] }, null, 2) + '\n')
|
|
545
|
+
const shimDir = join(partial, 'node_modules', 'centralu')
|
|
546
|
+
cpSync(join(repo, 'packaging', 'npm', 'centralu', 'bin'), join(shimDir, 'bin'), { recursive: true })
|
|
547
|
+
// The platform packages it names are npm's to fetch; here the host sits beside it, as a hub's install puts it
|
|
548
|
+
const { optionalDependencies: _platforms, ...stamped } = shim
|
|
549
|
+
writeFileSync(join(shimDir, 'package.json'), JSON.stringify({ ...stamped, version }, null, 2) + '\n')
|
|
550
|
+
const source = { kind: 'dev', commit: p.commit, dirty: changes > 0 }
|
|
551
|
+
writeFileSync(
|
|
552
|
+
join(partial, 'install.json'),
|
|
553
|
+
JSON.stringify({ version, node: current.node, platform: target, packages: [], installedAt: new Date().toISOString(), by: p.hub ?? null, source }, null, 2) + '\n',
|
|
554
|
+
)
|
|
555
|
+
mkdirSync(l.versions, { recursive: true })
|
|
556
|
+
placeVersion(l, partial, join(l.versions, version))
|
|
557
|
+
return { installed: { version, node: current.node, source }, current, previous: readPointer(l.previous), removed: [], left: [] }
|
|
558
|
+
} finally {
|
|
559
|
+
rmSync(partial, { recursive: true, force: true })
|
|
560
|
+
}
|
|
561
|
+
} finally {
|
|
562
|
+
unlock()
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
368
566
|
async function main() {
|
|
369
567
|
try {
|
|
370
568
|
const p = JSON.parse(Buffer.from(process.argv[2] ?? '', 'base64').toString('utf8'))
|
|
371
|
-
const r =
|
|
569
|
+
const r =
|
|
570
|
+
p.action === 'pointers'
|
|
571
|
+
? await setPointers(p)
|
|
572
|
+
: p.action === 'prune'
|
|
573
|
+
? pruneNow()
|
|
574
|
+
: p.action === 'dev'
|
|
575
|
+
? devBuild(p)
|
|
576
|
+
: p.action === 'store-backup'
|
|
577
|
+
? storeBackup(p)
|
|
578
|
+
: p.action === 'store-restore'
|
|
579
|
+
? storeRestore(p)
|
|
580
|
+
: await install(p)
|
|
372
581
|
process.stdout.write(`CENTRALU-INSTALL done ${JSON.stringify(r)}\n`)
|
|
373
582
|
} catch (err) {
|
|
374
583
|
const code = err instanceof Refusal ? err.code : 'error'
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"commit": "
|
|
2
|
+
"commit": "9b15c3f",
|
|
3
3
|
"protocolVersion": 1,
|
|
4
4
|
"hostProof": 1,
|
|
5
|
-
"
|
|
5
|
+
"tokenOnStdin": 1,
|
|
6
|
+
"builtAt": "2026-10-18T02:49:02.042Z",
|
|
6
7
|
"runtime": "system-node",
|
|
7
8
|
"platform": "darwin",
|
|
8
9
|
"arch": "arm64"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "centralu",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.28",
|
|
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",
|
|
@@ -38,9 +38,9 @@
|
|
|
38
38
|
"win32"
|
|
39
39
|
],
|
|
40
40
|
"optionalDependencies": {
|
|
41
|
-
"@centralu/darwin-arm64": "0.1.0-beta.
|
|
42
|
-
"@centralu/linux-arm64": "0.1.0-beta.
|
|
43
|
-
"@centralu/linux-x64": "0.1.0-beta.
|
|
44
|
-
"@centralu/win32-x64": "0.1.0-beta.
|
|
41
|
+
"@centralu/darwin-arm64": "0.1.0-beta.28",
|
|
42
|
+
"@centralu/linux-arm64": "0.1.0-beta.28",
|
|
43
|
+
"@centralu/linux-x64": "0.1.0-beta.28",
|
|
44
|
+
"@centralu/win32-x64": "0.1.0-beta.28"
|
|
45
45
|
}
|
|
46
46
|
}
|