@centralu/win32-x64 0.1.0-beta.45 → 0.1.0-beta.47

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.
@@ -1,10 +1,9 @@
1
1
  // Centralu's remote installer, step 2 (docs/plans/remote-hub.md §10.2). The hub sends this file over
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
- // ({ 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), 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' }).
4
+ // ({ version, node, platform, hub, packages: [{ name, tarball, integrity }], activate? }), or a dev build of the host
5
+ // from a git commit in a working copy there ({ action: 'dev', repo, commit, … }; `pnpm remote-dev`, #1036). What an
6
+ // update does after placing (stop, switch, start, check, put back, prune) is the new version's own `serve --switch`.
8
7
  //
9
8
  // It prints one line for the hub to read, `CENTRALU-INSTALL done <json>` or
10
9
  // `CENTRALU-INSTALL fail <code> <detail>`, and nothing that matters on its exit code (exit codes do
@@ -14,6 +13,7 @@ import { createHash, randomBytes } from 'node:crypto'
14
13
  import { chmodSync, cpSync, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs'
15
14
  import { homedir } from 'node:os'
16
15
  import { basename, isAbsolute, join, win32 } from 'node:path'
16
+ import { createRequire } from 'node:module'
17
17
  import { pathToFileURL } from 'node:url'
18
18
 
19
19
  const WORD = /^[0-9A-Za-z][0-9A-Za-z.+-]{0,63}$/
@@ -37,7 +37,6 @@ export function remoteLayout(dataDir) {
37
37
  node: join(root, 'node'),
38
38
  bin: join(root, 'bin'),
39
39
  lock: join(root, 'install.lock'),
40
- storeBackup: join(root, 'store-backup'),
41
40
  }
42
41
  }
43
42
 
@@ -71,53 +70,6 @@ export function switchTo(l, next) {
71
70
  return { current: next, previous: cur ?? readPointer(l.previous) }
72
71
  }
73
72
 
74
- /**
75
- * Removes every version that is neither `current` nor `previous`, and every Node neither uses. A
76
- * folder that cannot go (a Windows program still running from it) stays and is named, never fatal
77
- */
78
- export function prune(l) {
79
- const keep = [readPointer(l.current), readPointer(l.previous)].filter(Boolean)
80
- const removed = []
81
- const left = []
82
- const sweep = (dir, kept) => {
83
- let names = []
84
- try {
85
- names = readdirSync(dir)
86
- } catch {
87
- return
88
- }
89
- for (const name of names) {
90
- if (kept.has(name) || name.startsWith('.')) continue
91
- try {
92
- rmSync(join(dir, name), { recursive: true, force: true })
93
- removed.push(name)
94
- } catch {
95
- left.push(name)
96
- }
97
- }
98
- }
99
- sweep(l.versions, new Set(keep.map((p) => p.version)))
100
- // What `placeVersion` moved aside and could not remove then
101
- try {
102
- for (const name of readdirSync(l.root).filter((n) => n.startsWith('.replaced-'))) {
103
- try {
104
- rmSync(join(l.root, name), { recursive: true, force: true })
105
- } catch {
106
- left.push(name)
107
- }
108
- }
109
- } catch {}
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
- }
118
- return { removed, left }
119
- }
120
-
121
73
  /** A lock folder with no pid in it, younger than this, is held: its installer has made it and not yet written the pid */
122
74
  const LOCK_YOUNG_MS = 60_000
123
75
 
@@ -312,8 +264,8 @@ export async function install(p, { dataDir = process.env.CC_DATA_DIR || join(hom
312
264
  return { installed: { version: p.version, node: p.node }, current: readPointer(l.current), previous: readPointer(l.previous), removed: [], left: [] }
313
265
  }
314
266
  const launcher = await ensureManagedLauncher(l, dir, p.version, platform)
315
- const pointers = switchTo(l, { version: p.version, node: p.node })
316
- return { ...pointers, ...prune(l), launcher }
267
+ // Nothing is pruned here: the switch that replaces a version prunes (the launcher's serve --switch, plan §10.5a)
268
+ return { ...switchTo(l, { version: p.version, node: p.node }), removed: [], left: [], launcher }
317
269
  } finally {
318
270
  unlock()
319
271
  }
@@ -336,121 +288,6 @@ async function ensureManagedLauncher(l, dir, version, platform) {
336
288
  return launcher
337
289
  }
338
290
 
339
- const pointerOf = (v) => (v && WORD.test(v.version) && WORD.test(v.node) ? { version: v.version, node: v.node } : null)
340
-
341
- /**
342
- * Writes both pointers as given (plan §10.5 steps 3 and 5, and rollback): `previous` first, then
343
- * `current`, each by rename; a null `previous` removes that file. `current` must name a version and a
344
- * Node that are both there. A null `current` puts the machine back to having no managed version: the
345
- * pointers and the launcher go, so the lookup finds the Centralu it ran before (an npm install).
346
- * Nothing is removed from `versions/` or `node/`
347
- */
348
- export async function setPointers(p, { dataDir = process.env.CC_DATA_DIR || join(homedir(), '.centralu'), platform = process.platform } = {}) {
349
- const l = remoteLayout(dataDir)
350
- mkdirSync(l.root, { recursive: true })
351
- const unlock = lock(l)
352
- try {
353
- const current = pointerOf(p.current)
354
- const previous = pointerOf(p.previous)
355
- if (!current) {
356
- for (const f of [l.previous, l.current, launcherPath(l, platform)]) rmSync(f, { force: true })
357
- return { current: null, previous: null }
358
- }
359
- const dir = join(l.versions, current.version)
360
- // Both must be there: a `previous` naming nothing would offer a rollback to a version that is gone
361
- for (const v of [current, previous].filter(Boolean)) {
362
- if (!existsSync(join(l.versions, v.version, 'install.json')) || !existsSync(join(l.node, `v${v.node}`))) throw new Refusal('missing', `${v.version} ${v.node}`)
363
- }
364
- await ensureManagedLauncher(l, dir, current.version, platform)
365
- if (previous) writeAtomic(l.previous, `${previous.version} ${previous.node}\n`)
366
- else rmSync(l.previous, { force: true })
367
- writeAtomic(l.current, `${current.version} ${current.node}\n`)
368
- return { current, previous }
369
- } finally {
370
- unlock()
371
- }
372
- }
373
-
374
- /** Plan §10.5 step 6, on its own: what is neither `current` nor `previous` goes */
375
- export function pruneNow({ dataDir = process.env.CC_DATA_DIR || join(homedir(), '.centralu') } = {}) {
376
- const l = remoteLayout(dataDir)
377
- mkdirSync(l.root, { recursive: true })
378
- const unlock = lock(l)
379
- try {
380
- return { current: readPointer(l.current), previous: readPointer(l.previous), ...prune(l) }
381
- } finally {
382
- unlock()
383
- }
384
- }
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
291
  /** `a` at least `b`, both plain `x.y.z` (a prerelease or build suffix is ignored) */
455
292
  function atLeast(a, b) {
456
293
  const n = (v) => String(v).replace(/^v/, '').split(/[-+]/)[0].split('.').map((x) => Number(x) || 0)
@@ -499,8 +336,8 @@ function lastSaid(...texts) {
499
336
 
500
337
  /**
501
338
  * A dev build of the host from a git commit (`pnpm remote-dev`, #1036; docs/plans/remote-hub.md §10.11), placed beside
502
- * what is there as `<base>.local.N` with nothing switched: the hub's update steps stop, switch, start and check it
503
- * (update.ts `switchRemote`).
339
+ * what is there as `<base>.local.N` with nothing switched: the dev build's own `serve --switch` stops, switches, starts
340
+ * and checks it (update.ts `handOver`).
504
341
  *
505
342
  * Checked before anything is built: the working copy's HEAD is exactly `p.commit`, it has no changes unless
506
343
  * `p.allowDirty`, it speaks `p.protocol` (the protocol the hub links on), and this Node (the pinned one `current` runs
@@ -532,7 +369,12 @@ export function devBuild(p, { dataDir = process.env.CC_DATA_DIR || join(homedir(
532
369
  if (protocol !== p.protocol) throw new Refusal('dev_protocol', `${protocol} ${p.protocol}`)
533
370
  const floor = /(\d+\.\d+\.\d+)/.exec(String(shim.engines?.node ?? ''))?.[1]
534
371
  if (floor && !atLeast(process.versions.node, floor)) throw new Refusal('dev_node', `${process.versions.node} ${floor}`)
535
- if (!existsSync(join(repo, 'node_modules', 'esbuild'))) throw new Refusal('dev_deps', repo)
372
+ // Found the way the bundler finds it (its own folder, then up), so a hoisted install passes too
373
+ try {
374
+ createRequire(join(repo, 'packages', 'agent-host', 'package.json')).resolve('esbuild')
375
+ } catch {
376
+ throw new Refusal('dev_deps', repo)
377
+ }
536
378
  // The next number after every dev build of this release still there, and any a pointer names
537
379
  const local = new RegExp(`^${base.replace(/[.+-]/g, '\\$&')}\\.local\\.([1-9]\\d*)$`)
538
380
  let names = []
@@ -584,18 +426,7 @@ export function devBuild(p, { dataDir = process.env.CC_DATA_DIR || join(homedir(
584
426
  async function main() {
585
427
  try {
586
428
  const p = JSON.parse(Buffer.from(process.argv[2] ?? '', 'base64').toString('utf8'))
587
- const r =
588
- p.action === 'pointers'
589
- ? await setPointers(p)
590
- : p.action === 'prune'
591
- ? pruneNow()
592
- : p.action === 'dev'
593
- ? devBuild(p)
594
- : p.action === 'store-backup'
595
- ? storeBackup(p)
596
- : p.action === 'store-restore'
597
- ? storeRestore(p)
598
- : await install(p)
429
+ const r = p.action === 'dev' ? devBuild(p) : await install(p)
599
430
  process.stdout.write(`CENTRALU-INSTALL done ${JSON.stringify(r)}\n`)
600
431
  } catch (err) {
601
432
  const code = err instanceof Refusal ? err.code : 'error'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@centralu/win32-x64",
3
- "version": "0.1.0-beta.45",
3
+ "version": "0.1.0-beta.47",
4
4
  "description": "Centralu app folder for Windows on x86-64",
5
5
  "license": "MIT",
6
6
  "author": "ijun17",