stackbite 0.5.107 → 0.5.108

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stackbite",
3
- "version": "0.5.107",
3
+ "version": "0.5.108",
4
4
  "description": "Verified native Stackbite CLI bootstrapper",
5
5
  "license": "UNLICENSED",
6
6
  "engines": {
@@ -505,6 +505,46 @@ function previousCacheHome(home, cache, platform) {
505
505
  return typeof home === 'string' && path.isAbsolute(home) && /^v1-[a-f0-9]{64}$/.test(path.basename(home))
506
506
  && !!cache && spelling(path.dirname(path.resolve(home))) === spelling(cache);
507
507
  }
508
+ // Bounded portable.json of an image; null when absent, linked, oversized or malformed.
509
+ async function portableMetadata(root) {
510
+ try { return JSON.parse((await readSmall(path.join(root, 'portable.json'), 16384)).toString('utf8')); }
511
+ catch { return null; }
512
+ }
513
+ // The release identity Python's former-cache check compares (Windows may omit platform).
514
+ function portableMatches(value, spec) {
515
+ return !!value && typeof value === 'object' && value.product === 'Stackbite' && value.command === 'stackbite'
516
+ && value.version === spec.version && value.commit === spec.commit
517
+ && (Object.hasOwn(value, 'platform') ? value.platform : spec.platform === 'win32' ? spec.host : undefined) === spec.host;
518
+ }
519
+ // An absolute literal path with no linked ancestor and no source repository.
520
+ async function literalPath(home) {
521
+ if (!path.isAbsolute(home) || path.resolve(home) !== home) return false;
522
+ for (let ancestor = home; ; ancestor = path.dirname(ancestor)) {
523
+ let info = null;
524
+ try { info = await fs.lstat(ancestor); } catch (error) { if (error.code !== 'ENOENT') return false; }
525
+ if (info?.isSymbolicLink() || info && await exists(path.join(ancestor, '.git'))) return false;
526
+ if (ancestor === path.dirname(ancestor)) return true;
527
+ }
528
+ }
529
+ // A previous-layout cache image recorded by a host with another cache root:
530
+ // the shape and portable identity Python's _former_cache_image accepts. This
531
+ // classifies the file only; no environment state changes it. It is only read.
532
+ async function formerCache(home, spec) {
533
+ if (typeof home !== 'string' || !/^v1-[a-f0-9]{64}$/.test(path.basename(home))
534
+ || path.basename(path.dirname(home)) !== 'stackbite') return null;
535
+ try {
536
+ if (!await literalPath(home) || !(await fs.lstat(home)).isDirectory()) return null;
537
+ } catch { return null; }
538
+ return portableMatches(await portableMetadata(home), spec) ? home : null;
539
+ }
540
+ // A former cache selection this run may migrate: the engine moves a selection
541
+ // only under its own state, so a per-run STACKBITE_STATE_DIR never does. The
542
+ // engine reads a legacy HELM_STATE_DIR only when STACKBITE_STATE_DIR is absent
543
+ // (even empty, the canonical name wins). It decides again under its locks.
544
+ async function formerSelection(saved, environment, spec) {
545
+ const state = Object.hasOwn(environment, 'STACKBITE_STATE_DIR') ? environment.STACKBITE_STATE_DIR : environment.HELM_STATE_DIR;
546
+ return state && path.resolve(state) !== saved?.state ? null : formerCache(saved?.home, spec);
547
+ }
508
548
  async function publishCache(payload, target, spec, options) {
509
549
  await assertBaseline(options.environment, spec.platform);
510
550
  let migrate = false;
@@ -515,7 +555,8 @@ async function publishCache(payload, target, spec, options) {
515
555
  if (!await exists(locator)) continue;
516
556
  const saved = JSON.parse((await readSmall(locator, 16384)).toString('utf8').replace(/^\uFEFF/, ''));
517
557
  const descriptor = options.environment.STACKBITE_NPM_REGISTRATION;
518
- migrate = previousCacheHome(saved.home, options.cache, spec.platform);
558
+ migrate = previousCacheHome(saved.home, options.cache, spec.platform)
559
+ || !!await formerSelection(saved, options.environment, spec);
519
560
  if (!migrate && typeof saved.home === 'string' && path.isAbsolute(saved.home)
520
561
  && typeof saved.state === 'string' && path.isAbsolute(saved.state) && /^[a-f0-9]{32}$/.test(saved.installation_id)
521
562
  && (!descriptor || JSON.stringify(saved.npm_registration) === JSON.stringify(JSON.parse(descriptor)))) return;
@@ -533,15 +574,73 @@ async function publishCache(payload, target, spec, options) {
533
574
  function runtimeDirectory(environment, platform) {
534
575
  return path.join(installationRoot(environment, platform), 'app');
535
576
  }
536
- // The previous layout's image of this exact pinned identity, when it still
537
- // passes the full pinned validation. It is only read; a live process may use it.
577
+ // This root's first locator, when it fully names an installation: the same
578
+ // shape publishCache keeps without rewriting its home.
579
+ async function establishedLocator(root) {
580
+ for (const name of ['default-install.json', 'install.json']) {
581
+ const locator = path.join(root, name);
582
+ if (!await exists(locator)) continue;
583
+ const saved = JSON.parse((await readSmall(locator, 16384)).toString('utf8').replace(/^\uFEFF/, ''));
584
+ return saved && typeof saved.home === 'string' && path.isAbsolute(saved.home)
585
+ && typeof saved.state === 'string' && path.isAbsolute(saved.state)
586
+ && /^[a-f0-9]{32}$/.test(saved.installation_id) ? saved : null;
587
+ }
588
+ return null;
589
+ }
590
+ async function installationLocator(environment, platform) {
591
+ const root = installationRoot(environment, platform);
592
+ return await selectedDirectory(root, environment, platform) ? establishedLocator(root) : null;
593
+ }
594
+ // The image root when its portable identity is this release; null otherwise.
595
+ async function sameRelease(home, spec) {
596
+ let root;
597
+ try { root = await payloadRoot(home); } catch { return null; }
598
+ return portableMatches(await portableMetadata(root), spec) ? root : null;
599
+ }
600
+ // Another archive of this release (a re-signed build) carries its own valid
601
+ // checksum manifest that pins its portable.json. A damaged image does not.
602
+ async function anotherPin(root) {
603
+ try {
604
+ const hashes = JSON.parse((await readSmall(path.join(root, 'SHA256SUMS.json'))).toString('utf8'));
605
+ expectedMembers(hashes);
606
+ const metadata = await readSmall(path.join(root, 'portable.json'), 16384);
607
+ return hashes['portable.json'] === createHash('sha256').update(metadata).digest('hex');
608
+ } catch { return false; }
609
+ }
610
+ // An image the established installation already selects outside app/: an
611
+ // explicit STACKBITE_HOME, else the locator's home. Another release or pin is
612
+ // skipped; an image of this release must otherwise carry this package's pinned
613
+ // documents, and the caller then validates its whole payload, so a damaged image
614
+ // of this release is refused, not replaced. Publication keeps the locator's
615
+ // home, except that a cache image (this host's or a former host's) is never run
616
+ // in place.
617
+ async function selectedImage(environment, spec, cache) {
618
+ const saved = await installationLocator(environment, spec.platform);
619
+ // A previous-cache selection keeps its existing migration path.
620
+ if (!saved || previousCacheHome(saved.home, cache, spec.platform)) return null;
621
+ const former = await formerCache(saved.home, spec);
622
+ for (const home of [environment.STACKBITE_HOME, saved.home]) {
623
+ if (typeof home !== 'string' || !path.isAbsolute(home) || home === former) continue;
624
+ const root = await sameRelease(home, spec);
625
+ if (!root) continue;
626
+ try { await pinnedDocuments(root, spec); }
627
+ catch (error) { if (await anotherPin(root)) continue; throw error; }
628
+ return path.resolve(home);
629
+ }
630
+ return null;
631
+ }
632
+ // An image of this exact pinned identity, when it passes the full pinned
633
+ // validation. It is only read; a live process may use it.
634
+ async function verifiedImage(home, spec) {
635
+ if (!home) return null;
636
+ try { await validatePayload(home, spec); } catch { return null; }
637
+ return home;
638
+ }
538
639
  async function previousImage(legacy, cache, spec) {
539
- if (!await existingCacheDirectory(cache) || !await exists(legacy)) return null;
540
- try { await validatePayload(legacy, spec); } catch { return null; }
541
- return legacy;
640
+ return await existingCacheDirectory(cache) && await exists(legacy) ? verifiedImage(legacy, spec) : null;
542
641
  }
543
642
  async function ensureBinary(spec, { cache = cacheDirectory(), runtime = null, downloadImpl = download, extractImpl = extractArchive,
544
- checkOnly = false, reportCached = false, includePhone = false, progress = () => {},
643
+ checkOnly = false, reportCached = false, includePhone = false, reuseSelected = false, progress = () => {},
545
644
  environment = process.env, publishImpl = publishCache, registrationSource = null, ...options } = {}) {
546
645
  cache = await resolveCacheDirectory(cache);
547
646
  environment = { ...environment };
@@ -560,18 +659,25 @@ async function ensureBinary(spec, { cache = cacheDirectory(), runtime = null, do
560
659
  if (registrationSource) await registrationSource.validate();
561
660
  if (!checkOnly) await assertBaseline(environment, spec.platform);
562
661
  // Check-only callers (removal, setup --check) never publish; they may still
563
- // use an image of the previous cache layout.
564
- const home = await selectedDirectory(runtime, environment, spec.platform) && await exists(target) ? target
565
- : checkOnly && await existingCacheDirectory(cache) && await exists(legacy) ? legacy : null;
662
+ // use an image of the previous cache layout, this host's or a former host's.
663
+ const published = await selectedDirectory(runtime, environment, spec.platform) && await exists(target);
664
+ const former = !published && reuseSelected
665
+ ? await formerCache((await installationLocator(environment, spec.platform))?.home, spec) : null;
666
+ const home = published ? target
667
+ : reuseSelected && await selectedImage(environment, spec, cache)
668
+ || (checkOnly ? (await existingCacheDirectory(cache) && await exists(legacy) ? legacy : former) : null);
566
669
  if (home) {
567
670
  if (checkOnly || reportCached) progress('validate');
568
671
  const installed = { home, executable: await validatePayload(home, spec, { includePhone }) };
569
- if (!checkOnly) await publishImpl(target, target, spec, { ...options, environment, cache, existing: true });
672
+ // A selected image outside app/ is its own maintenance engine.
673
+ if (!checkOnly) await publishImpl(home, target, spec, { ...options, environment, cache, existing: true });
570
674
  if (checkOnly || reportCached) progress('ready');
571
675
  return installed;
572
676
  }
573
677
  if (checkOnly) throw notCached();
574
- const previous = await previousImage(legacy, cache, spec);
678
+ // A verified previous-layout image is copied once into the durable target;
679
+ // the engine then moves a cache selection there.
680
+ const previous = await previousImage(legacy, cache, spec) || await verifiedImage(former, spec);
575
681
  if (previous) {
576
682
  progress('publish');
577
683
  await admitPublication(environment, spec.platform);
@@ -615,9 +721,11 @@ async function stageRelease(spec, staging, { downloadImpl, extractImpl, progress
615
721
  await fs.mkdir(extraction, { mode: 0o700 });
616
722
  progress('download');
617
723
  await downloadImpl(spec, archive);
618
- // Recheck at the extraction boundary, including for injected test transports.
724
+ // download() itself verifies the exact size and SHA-256 before returning.
725
+ // Recheck any other transport at the extraction boundary.
619
726
  progress('archive');
620
- if ((await fs.stat(archive)).size !== spec.size || await hashFile(archive, LIMITS.download) !== spec.sha256) fail('Pinned archive SHA-256 or size mismatch');
727
+ if (downloadImpl !== download && ((await fs.stat(archive)).size !== spec.size
728
+ || await hashFile(archive, LIMITS.download) !== spec.sha256)) fail('Pinned archive SHA-256 or size mismatch');
621
729
  progress('extract');
622
730
  await extractImpl(archive, extraction, spec);
623
731
  const payload = path.join(extraction, spec.root);
@@ -718,7 +826,7 @@ async function main(argv = process.argv.slice(2), options = {}) {
718
826
  try { installed = await timing.phase(environment, 'launcher.ensure', () => ensureBinary(spec, { ...options,
719
827
  environment, registrationSource,
720
828
  cache: options.cache || cacheDirectory(environment, platform),
721
- checkOnly: removing || setup?.checkOnly || false, reportCached: Boolean(setup),
829
+ checkOnly: removing || setup?.checkOnly || false, reportCached: Boolean(setup), reuseSelected: !removing,
722
830
  includePhone: removing || Boolean(setup) || argv[0] === 'phone',
723
831
  progress: options.progress || stageReporter(options.stderr || process.stderr) })); }
724
832
  catch (error) {
@@ -1,35 +1,35 @@
1
1
  {
2
2
  "schema": 1,
3
- "version": "0.5.107",
4
- "commit": "506288e3f01c20dd709905c95eaa46ad04471b17",
3
+ "version": "0.5.108",
4
+ "commit": "bcd3e47ee369cdbf209688fb8333b7b582e292cb",
5
5
  "repository": "Shinick-Han/stackbite",
6
6
  "platforms": {
7
7
  "win32-x64": {
8
- "asset": "stackbite-0.5.107-windows-x64-506288e3f01c.zip",
9
- "url": "https://github.com/Shinick-Han/stackbite/releases/download/v0.5.107/stackbite-0.5.107-windows-x64-506288e3f01c.zip",
10
- "sha256": "6da273fbd3805fff41c3dc1346c39eb7290f4969b30e5f14124a9f9948659701",
11
- "size": 96379204,
12
- "root": "stackbite-0.5.107-windows-x64-506288e3f01c",
13
- "manifestSha256": "fb81b39e291be6bd012b2d1e481f38aa506e38681ea463ddb55dd21bfeab309a",
14
- "metadataSha256": "871eed20e507d284102373ce50e586919cf0ed3b5a791b6afa9d8cc7df762604"
8
+ "asset": "stackbite-0.5.108-windows-x64-bcd3e47ee369.zip",
9
+ "url": "https://github.com/Shinick-Han/stackbite/releases/download/v0.5.108/stackbite-0.5.108-windows-x64-bcd3e47ee369.zip",
10
+ "sha256": "416b55e30a6b3670b74477e078515b927ffa61f01d1aadb255a41a30d5145c7a",
11
+ "size": 82803119,
12
+ "root": "stackbite-0.5.108-windows-x64-bcd3e47ee369",
13
+ "manifestSha256": "8d00d3cbe4ddb913c06392427be2e0b6a8162d88fdb3bcb2fa573fb55a95d34c",
14
+ "metadataSha256": "4fdcdaeeab139ac774f67c61c27bb32ba07a3151df059f7918cd042f6b89a72e"
15
15
  },
16
16
  "linux-x64": {
17
- "asset": "stackbite-0.5.107-linux-x86_64-506288e3f01c.tar.gz",
18
- "url": "https://github.com/Shinick-Han/stackbite/releases/download/v0.5.107/stackbite-0.5.107-linux-x86_64-506288e3f01c.tar.gz",
19
- "sha256": "74e451669bd460b690dc45d4abe9f7bbe6af10d0ab56519c5c4a7c0ff55b2350",
20
- "size": 77586426,
21
- "root": "stackbite-0.5.107-linux-x86_64-506288e3f01c",
22
- "manifestSha256": "a3030ce79fe2b84314064f7a5f5794e744fe8d16cfa223d467a8670cdc957e15",
23
- "metadataSha256": "9df20d2f675d4e4457a060d07b1b821146d9dcfaa92a532960cb8c6b39239d35"
17
+ "asset": "stackbite-0.5.108-linux-x86_64-bcd3e47ee369.tar.gz",
18
+ "url": "https://github.com/Shinick-Han/stackbite/releases/download/v0.5.108/stackbite-0.5.108-linux-x86_64-bcd3e47ee369.tar.gz",
19
+ "sha256": "83fba4c60508d2a4fcbaa276c51e8eb7db9f2f762e2f28228b9b6ece9ac10327",
20
+ "size": 82791962,
21
+ "root": "stackbite-0.5.108-linux-x86_64-bcd3e47ee369",
22
+ "manifestSha256": "36dfdbf9f1fc5dcdc882e438ff855065d3456f6674dea115fd219b6e7a7893fb",
23
+ "metadataSha256": "ee20086e1c78590a83dab038dce86e0b28d288fc6d0665f00c041644777340ad"
24
24
  },
25
25
  "darwin-arm64": {
26
- "asset": "stackbite-0.5.107-macos-arm64-506288e3f01c.tar.gz",
27
- "url": "https://github.com/Shinick-Han/stackbite/releases/download/v0.5.107/stackbite-0.5.107-macos-arm64-506288e3f01c.tar.gz",
28
- "sha256": "ddf4c956c7657df6bb4684aa3f7a9d677144cca2deb4e425e3a6442b88671262",
29
- "size": 33557501,
30
- "root": "stackbite-0.5.107-macos-arm64-506288e3f01c",
31
- "manifestSha256": "4c2ab4d509a2fb1a48696c820b8c3f8cadea9441817e314a595ab8b08f34cef3",
32
- "metadataSha256": "e07aedd09b464458bef021518136bbcfb3d9b521f72e1da991b6d1091cccaed3"
26
+ "asset": "stackbite-0.5.108-macos-arm64-bcd3e47ee369.tar.gz",
27
+ "url": "https://github.com/Shinick-Han/stackbite/releases/download/v0.5.108/stackbite-0.5.108-macos-arm64-bcd3e47ee369.tar.gz",
28
+ "sha256": "3d1afcb0b7c197cf76691bff3807f8b11bfecda2c93e70f67a7b44c71f312299",
29
+ "size": 38100942,
30
+ "root": "stackbite-0.5.108-macos-arm64-bcd3e47ee369",
31
+ "manifestSha256": "0e1c7f4958fe4852d2e25732e18ef85523fe2a2047647362837cacf8243bed45",
32
+ "metadataSha256": "cac6ffae5862aa0cb5f3b51c62c798c042ae0bcc3ed06172d1f74632c77848e1"
33
33
  }
34
34
  }
35
35
  }