stackbite 0.5.98 → 0.5.99
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
|
@@ -171,17 +171,26 @@ async function publicExecutable(root, spec, hashes) {
|
|
|
171
171
|
const manifest = JSON.parse((await readSmall(path.join(root, 'manifest.json'))).toString('utf8'));
|
|
172
172
|
const entry = manifest.binaries?.['stackbite-launcher'], info = entry?.build_info;
|
|
173
173
|
const target = { win32: 'x86_64-pc-windows-msvc', linux: 'x86_64-unknown-linux-gnu', darwin: 'aarch64-apple-darwin' }[spec.platform];
|
|
174
|
-
if (manifest
|
|
175
|
-
||
|
|
176
|
-
|| !entry || Object.keys(entry).sort().join(',') !== 'build_info,path,sha256'
|
|
177
|
-
|| entry.path !== name || entry.sha256 !== hashes[name] || manifest.artifacts?.[name]?.sha256 !== hashes[name]
|
|
178
|
-
|| manifest.artifacts?.[name]?.kind !== 'binary' || !Number.isSafeInteger(manifest.artifacts?.[name]?.size) || manifest.artifacts[name].size < 1
|
|
179
|
-
|| !info || Object.keys(info).sort().join(',') !== 'binary,commit,product,protocol,target,version'
|
|
180
|
-
|| info.product !== 'Stackbite' || info.binary !== 'stackbite-launcher' || info.version !== spec.version
|
|
181
|
-
|| info.commit !== spec.commit || info.target !== target || info.protocol !== 1) fail('Native entry package identity or pin is invalid');
|
|
174
|
+
if (!terminalBuildMatches(manifest, spec, target) || !launcherPinMatches(manifest, entry, name, hashes)
|
|
175
|
+
|| !launcherBuildInfoMatches(info, spec, target)) fail('Native entry package identity or pin is invalid');
|
|
182
176
|
const launcher = await nativeExecutable(root, spec, 'stackbite-launcher');
|
|
183
177
|
return spec.platform === 'win32' ? engine : launcher;
|
|
184
178
|
}
|
|
179
|
+
function terminalBuildMatches(manifest, spec, target) {
|
|
180
|
+
return manifest.schema === 'stackbite-rust-terminal-build-v1' && manifest.product === 'Stackbite'
|
|
181
|
+
&& manifest.version === spec.version && manifest.commit === spec.commit && manifest.target === target && manifest.protocol === 1;
|
|
182
|
+
}
|
|
183
|
+
// The manifest entry and its artifact record both pin the release hash of `name`.
|
|
184
|
+
function launcherPinMatches(manifest, entry, name, hashes) {
|
|
185
|
+
return !!entry && Object.keys(entry).sort().join(',') === 'build_info,path,sha256'
|
|
186
|
+
&& entry.path === name && entry.sha256 === hashes[name] && manifest.artifacts?.[name]?.sha256 === hashes[name]
|
|
187
|
+
&& manifest.artifacts?.[name]?.kind === 'binary' && Number.isSafeInteger(manifest.artifacts?.[name]?.size) && !(manifest.artifacts[name].size < 1);
|
|
188
|
+
}
|
|
189
|
+
function launcherBuildInfoMatches(info, spec, target) {
|
|
190
|
+
return !!info && Object.keys(info).sort().join(',') === 'binary,commit,product,protocol,target,version'
|
|
191
|
+
&& info.product === 'Stackbite' && info.binary === 'stackbite-launcher' && info.version === spec.version
|
|
192
|
+
&& info.commit === spec.commit && info.target === target && info.protocol === 1;
|
|
193
|
+
}
|
|
185
194
|
async function validatePayload(directory, spec, { includePhone = true } = {}) {
|
|
186
195
|
const root = await payloadRoot(directory);
|
|
187
196
|
const { hashes, metadata } = await pinnedDocuments(root, spec);
|
|
@@ -301,34 +310,40 @@ async function assertBaseline(environment, platform) {
|
|
|
301
310
|
if (Buffer.byteLength(raw) > 16384) fail('Invalid installation baseline');
|
|
302
311
|
const expected = JSON.parse(raw), root = installationRoot(environment, platform);
|
|
303
312
|
if (expected.preparation !== undefined) {
|
|
304
|
-
|
|
305
|
-
if (Object.keys(expected).sort().join(',') !== 'preparation,root' || expected.root !== root
|
|
306
|
-
|| !pin || Object.keys(pin).sort().join(',') !== 'identity,path' || pin.path !== file || !pin.identity) fail('Invalid installation preparation');
|
|
307
|
-
let identity = pin.identity;
|
|
308
|
-
const actual = await baselinePin(file);
|
|
309
|
-
if (platform === 'win32' && actual) identity = { ...identity, device: (BigInt(identity.device) & 0xffffffffn).toString() };
|
|
310
|
-
if (JSON.stringify(actual) !== JSON.stringify(identity)) fail('Installation changed while staging');
|
|
313
|
+
await assertPreparationBaseline(expected, root, platform);
|
|
311
314
|
return;
|
|
312
315
|
}
|
|
313
|
-
|
|
316
|
+
const current = await locatorInstallationId(root);
|
|
317
|
+
if (expected.root !== root || expected.incarnation !== current) fail('Installation changed while staging');
|
|
318
|
+
if (current === null) await assertLegacyBaseline(expected, platform);
|
|
319
|
+
}
|
|
320
|
+
// Python-origin restrictions retain FILE_ID_INFO's full volume serial;
|
|
321
|
+
// libuv reports its 32-bit component. The engine still checks the full pin.
|
|
322
|
+
function libuvIdentity(platform, actual, identity) {
|
|
323
|
+
return platform === 'win32' && actual && identity ? { ...identity, device: (BigInt(identity.device) & 0xffffffffn).toString() } : identity;
|
|
324
|
+
}
|
|
325
|
+
async function assertPreparationBaseline(expected, root, platform) {
|
|
326
|
+
const pin = expected.preparation, file = path.join(root, '.stackbite-update', 'update.lock');
|
|
327
|
+
if (Object.keys(expected).sort().join(',') !== 'preparation,root' || expected.root !== root
|
|
328
|
+
|| !pin || Object.keys(pin).sort().join(',') !== 'identity,path' || pin.path !== file || !pin.identity) fail('Invalid installation preparation');
|
|
329
|
+
const actual = await baselinePin(file);
|
|
330
|
+
if (JSON.stringify(actual) !== JSON.stringify(libuvIdentity(platform, actual, pin.identity))) fail('Installation changed while staging');
|
|
331
|
+
}
|
|
332
|
+
// The first present locator's incarnation; null for a legacy or absent locator.
|
|
333
|
+
async function locatorInstallationId(root) {
|
|
314
334
|
for (const name of ['default-install.json', 'install.json']) {
|
|
315
335
|
const file = path.join(root, name);
|
|
316
336
|
if (!await exists(file)) continue;
|
|
317
337
|
const value = JSON.parse((await readSmall(file, 16384)).toString('utf8').replace(/^\uFEFF/, ''));
|
|
318
|
-
|
|
319
|
-
break;
|
|
338
|
+
return value.installation_id ?? null;
|
|
320
339
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
// libuv reports its 32-bit component. The engine still checks the full pin.
|
|
329
|
-
if (platform === 'win32' && actual && identity) identity = { ...identity, device: (BigInt(identity.device) & 0xffffffffn).toString() };
|
|
330
|
-
if (JSON.stringify(actual) !== JSON.stringify(identity)) fail('Installation changed while staging');
|
|
331
|
-
}
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
async function assertLegacyBaseline(expected, platform) {
|
|
343
|
+
if (!Array.isArray(expected.legacy) || !expected.legacy.length || expected.legacy.length > 32) fail('Invalid legacy installation baseline');
|
|
344
|
+
for (const pin of expected.legacy) {
|
|
345
|
+
const actual = await baselinePin(pin.path);
|
|
346
|
+
if (JSON.stringify(actual) !== JSON.stringify(libuvIdentity(platform, actual, pin.identity))) fail('Installation changed while staging');
|
|
332
347
|
}
|
|
333
348
|
}
|
|
334
349
|
|
|
@@ -390,6 +405,30 @@ async function settlePreparation(preparation, environment, platform, source) {
|
|
|
390
405
|
} finally { await preparation.handle.close(); }
|
|
391
406
|
}
|
|
392
407
|
|
|
408
|
+
// Identities of the candidates that this mkdir created (the first created directory and below).
|
|
409
|
+
async function createdDirectories(firstCreated, candidates) {
|
|
410
|
+
const directories = [];
|
|
411
|
+
if (firstCreated) {
|
|
412
|
+
for (const selected of candidates) {
|
|
413
|
+
// libuv may return a Windows namespace spelling for the first directory.
|
|
414
|
+
const createdName = path.toNamespacedPath(firstCreated), selectedName = path.toNamespacedPath(selected);
|
|
415
|
+
if (selectedName === createdName || selectedName.startsWith(createdName + path.sep)) {
|
|
416
|
+
directories.push([selected, await fs.lstat(selected, { bigint: true })]);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
return directories;
|
|
421
|
+
}
|
|
422
|
+
// Creates the control file, or opens the existing one; `created` marks ours.
|
|
423
|
+
async function openPreparationFile(file) {
|
|
424
|
+
const constants = require('node:fs').constants;
|
|
425
|
+
try {
|
|
426
|
+
return { handle: await fs.open(file, constants.O_CREAT | constants.O_EXCL | constants.O_RDWR | (constants.O_NOFOLLOW || 0), 0o600), created: true };
|
|
427
|
+
} catch (error) {
|
|
428
|
+
if (error.code !== 'EEXIST') throw error;
|
|
429
|
+
return { handle: await fs.open(file, constants.O_RDWR | (constants.O_NOFOLLOW || 0)), created: false };
|
|
430
|
+
}
|
|
431
|
+
}
|
|
393
432
|
async function reservePreparation(environment, platform, target, source) {
|
|
394
433
|
if (Object.hasOwn(environment, 'STACKBITE_INSTALL_BASELINE')) return null;
|
|
395
434
|
let baseline = await captureBaseline(environment, platform, target);
|
|
@@ -410,26 +449,10 @@ async function reservePreparation(environment, platform, target, source) {
|
|
|
410
449
|
if (source) await source.validate();
|
|
411
450
|
const firstCreated = await fs.mkdir(directory, { recursive: true, mode: 0o700 });
|
|
412
451
|
await existingCacheDirectory(directory);
|
|
413
|
-
const directories = [];
|
|
414
|
-
if (firstCreated) {
|
|
415
|
-
for (const selected of [directory, root]) {
|
|
416
|
-
// libuv may return a Windows namespace spelling for the first directory.
|
|
417
|
-
const createdName = path.toNamespacedPath(firstCreated), selectedName = path.toNamespacedPath(selected);
|
|
418
|
-
if (selectedName === createdName || selectedName.startsWith(createdName + path.sep)) {
|
|
419
|
-
directories.push([selected, await fs.lstat(selected, { bigint: true })]);
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
const constants = require('node:fs').constants;
|
|
452
|
+
const directories = await createdDirectories(firstCreated, [directory, root]);
|
|
424
453
|
const file = path.join(directory, 'update.lock');
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
handle = await fs.open(file, constants.O_CREAT | constants.O_EXCL | constants.O_RDWR | (constants.O_NOFOLLOW || 0), 0o600);
|
|
428
|
-
created = true;
|
|
429
|
-
} catch (error) {
|
|
430
|
-
if (error.code !== 'EEXIST') throw error;
|
|
431
|
-
handle = await fs.open(file, constants.O_RDWR | (constants.O_NOFOLLOW || 0));
|
|
432
|
-
}
|
|
454
|
+
const { handle, created } = await openPreparationFile(file);
|
|
455
|
+
let preparation;
|
|
433
456
|
try {
|
|
434
457
|
const held = await handle.stat({ bigint: true });
|
|
435
458
|
const identity = await baselinePin(file);
|
|
@@ -88,36 +88,51 @@ async function captureCommand(root, prefix, platform, node, cli, recoveryOptions
|
|
|
88
88
|
if (!entryInfo.isFile() || entryInfo.isSymbolicLink() || entryInfo.nlink !== 1) throw new Error('Linked npm command preserved');
|
|
89
89
|
const entryData = await fs.readFile(entry);
|
|
90
90
|
for (const shim of platform === 'win32' ? ['stackbite', 'stackbite.cmd', 'stackbite.ps1'].map(name => path.join(prefix, name)) : [path.join(prefix, 'bin', 'stackbite')]) {
|
|
91
|
-
await
|
|
92
|
-
|
|
93
|
-
try { info = await fs.lstat(shim); } catch (error) { if (error.code === 'ENOENT') continue; throw error; }
|
|
94
|
-
if (info.isSymbolicLink()) {
|
|
95
|
-
const target = await fs.readlink(shim);
|
|
96
|
-
if (path.resolve(path.dirname(shim), target) !== entry) throw new Error('Foreign npm command preserved');
|
|
97
|
-
shims.push({ path: shim, info, target });
|
|
98
|
-
} else {
|
|
99
|
-
const data = await fs.readFile(shim);
|
|
100
|
-
if (!info.isFile() || info.nlink !== 1 || data.length > 128 * 1024 || !data.toString('utf8').replaceAll('\\', '/').includes(relative)) throw new Error('Foreign npm command preserved');
|
|
101
|
-
shims.push({ path: shim, info, data });
|
|
102
|
-
}
|
|
91
|
+
const captured = await captureShim(shim, entry, relative);
|
|
92
|
+
if (captured) shims.push(captured);
|
|
103
93
|
}
|
|
104
94
|
if (!shims.length) return null;
|
|
105
|
-
const
|
|
106
|
-
installRoot: recoveryOptions.installRoot, cache: recoveryOptions.cache });
|
|
107
|
-
const recovery = Buffer.from(`const m=require('./uninstall-registration'),cp=require('node:child_process');\nconst launch=async(exe,args,env)=>{const r=cp.spawnSync(exe,args,{env,stdio:'inherit'});if(r.error)throw r.error;return {code:r.status,signal:r.signal};};\nmodule.exports.main=async()=>{const r=await m.registrationOnly(process.argv.slice(2),process.env,launch,${options});if(!r)throw Error('Native removal remains incomplete');return r;};\n`);
|
|
95
|
+
const recovery = recoveryModule(root, platform, node, cli, recoveryOptions);
|
|
108
96
|
const files = [];
|
|
109
97
|
for (const [file, data] of [[path.join(root, 'package.json'), metadata], [entry, entryData],
|
|
110
98
|
[path.join(root, 'packaging/npm/lib/bootstrap.js'), recovery],
|
|
111
99
|
[path.join(root, 'packaging/npm/lib/uninstall-registration.js'), await fs.readFile(__filename)]]) {
|
|
112
|
-
await
|
|
113
|
-
let info;
|
|
114
|
-
try { info = await fs.lstat(file); } catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
115
|
-
if (info && (!info.isFile() || info.isSymbolicLink() || info.nlink !== 1)) throw new Error('Linked npm command preserved');
|
|
116
|
-
files.push({ path: file, info, data });
|
|
100
|
+
files.push(await captureFile(file, data));
|
|
117
101
|
}
|
|
118
102
|
return { root, info: await fs.lstat(root), prefix, prefixInfo: await fs.lstat(prefix), shims, files };
|
|
119
103
|
}
|
|
120
104
|
|
|
105
|
+
// The file's current identity (undefined when absent) with the bytes to restore.
|
|
106
|
+
async function captureFile(file, data) {
|
|
107
|
+
await commandParent(file);
|
|
108
|
+
let info;
|
|
109
|
+
try { info = await fs.lstat(file); } catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
110
|
+
if (info && (!info.isFile() || info.isSymbolicLink() || info.nlink !== 1)) throw new Error('Linked npm command preserved');
|
|
111
|
+
return { path: file, info, data };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// An absent shim is null. A present one must link or point to this package's entry.
|
|
115
|
+
async function captureShim(shim, entry, relative) {
|
|
116
|
+
await commandParent(shim);
|
|
117
|
+
let info;
|
|
118
|
+
try { info = await fs.lstat(shim); } catch (error) { if (error.code === 'ENOENT') return null; throw error; }
|
|
119
|
+
if (info.isSymbolicLink()) {
|
|
120
|
+
const target = await fs.readlink(shim);
|
|
121
|
+
if (path.resolve(path.dirname(shim), target) !== entry) throw new Error('Foreign npm command preserved');
|
|
122
|
+
return { path: shim, info, target };
|
|
123
|
+
}
|
|
124
|
+
const data = await fs.readFile(shim);
|
|
125
|
+
if (!info.isFile() || info.nlink !== 1 || data.length > 128 * 1024 || !data.toString('utf8').replaceAll('\\', '/').includes(relative)) throw new Error('Foreign npm command preserved');
|
|
126
|
+
return { path: shim, info, data };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// The bootstrap.js replacement that lets the original bin finish registration-only removal.
|
|
130
|
+
function recoveryModule(root, platform, node, cli, recoveryOptions) {
|
|
131
|
+
const options = JSON.stringify({ packageRoot: root, platform, nodeExecutable: node, npmCli: cli,
|
|
132
|
+
installRoot: recoveryOptions.installRoot, cache: recoveryOptions.cache });
|
|
133
|
+
return Buffer.from(`const m=require('./uninstall-registration'),cp=require('node:child_process');\nconst launch=async(exe,args,env)=>{const r=cp.spawnSync(exe,args,{env,stdio:'inherit'});if(r.error)throw r.error;return {code:r.status,signal:r.signal};};\nmodule.exports.main=async()=>{const r=await m.registrationOnly(process.argv.slice(2),process.env,launch,${options});if(!r)throw Error('Native removal remains incomplete');return r;};\n`);
|
|
134
|
+
}
|
|
135
|
+
|
|
121
136
|
async function restoreCommand(backup) {
|
|
122
137
|
if (!backup) return;
|
|
123
138
|
const info = await fs.lstat(backup.root);
|
|
@@ -125,32 +140,37 @@ async function restoreCommand(backup) {
|
|
|
125
140
|
const prefix = await fs.lstat(backup.prefix);
|
|
126
141
|
if (prefix.dev !== backup.prefixInfo.dev || prefix.ino !== backup.prefixInfo.ino) throw new Error('npm prefix replaced; preserved');
|
|
127
142
|
const items = [...backup.files, ...backup.shims];
|
|
128
|
-
for (const item of items)
|
|
129
|
-
await commandParent(item.path);
|
|
130
|
-
try {
|
|
131
|
-
const current = await fs.lstat(item.path);
|
|
132
|
-
if (!item.info || current.dev !== item.info.dev || current.ino !== item.info.ino) throw new Error('npm command replaced; preserved');
|
|
133
|
-
if (!item.target && (!current.isFile() || current.isSymbolicLink() || current.nlink !== 1)) throw new Error('Linked npm command preserved');
|
|
134
|
-
} catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
135
|
-
}
|
|
143
|
+
for (const item of items) await assertRestorable(item);
|
|
136
144
|
for (const item of items) {
|
|
137
145
|
if (item.target) {
|
|
138
146
|
try { await fs.lstat(item.path); } catch (error) { if (error.code !== 'ENOENT') throw error; await fs.symlink(item.target, item.path); }
|
|
139
|
-
} else
|
|
140
|
-
// An npm partial uninstall can leave this original bin read/share-only.
|
|
141
|
-
// Its existing require('../lib/bootstrap') reaches the small recovery module.
|
|
142
|
-
try { if ((await fs.readFile(item.path)).equals(item.data)) continue; }
|
|
143
|
-
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
144
|
-
await fs.mkdir(path.dirname(item.path), { recursive: true });
|
|
145
|
-
await commandParent(item.path);
|
|
146
|
-
const file = await fs.open(item.path, require('node:fs').constants.O_WRONLY | require('node:fs').constants.O_CREAT
|
|
147
|
-
| require('node:fs').constants.O_TRUNC | (require('node:fs').constants.O_NOFOLLOW || 0), 0o755);
|
|
148
|
-
try { await file.writeFile(item.data); } finally { await file.close(); }
|
|
149
|
-
await fs.chmod(item.path, 0o755);
|
|
150
|
-
}
|
|
147
|
+
} else await restoreFile(item);
|
|
151
148
|
}
|
|
152
149
|
}
|
|
153
150
|
|
|
151
|
+
// A present item is still the captured object; a file is still unlinked.
|
|
152
|
+
async function assertRestorable(item) {
|
|
153
|
+
await commandParent(item.path);
|
|
154
|
+
try {
|
|
155
|
+
const current = await fs.lstat(item.path);
|
|
156
|
+
if (!item.info || current.dev !== item.info.dev || current.ino !== item.info.ino) throw new Error('npm command replaced; preserved');
|
|
157
|
+
if (!item.target && (!current.isFile() || current.isSymbolicLink() || current.nlink !== 1)) throw new Error('Linked npm command preserved');
|
|
158
|
+
} catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function restoreFile(item) {
|
|
162
|
+
// An npm partial uninstall can leave this original bin read/share-only.
|
|
163
|
+
// Its existing require('../lib/bootstrap') reaches the small recovery module.
|
|
164
|
+
try { if ((await fs.readFile(item.path)).equals(item.data)) return; }
|
|
165
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
166
|
+
await fs.mkdir(path.dirname(item.path), { recursive: true });
|
|
167
|
+
await commandParent(item.path);
|
|
168
|
+
const file = await fs.open(item.path, require('node:fs').constants.O_WRONLY | require('node:fs').constants.O_CREAT
|
|
169
|
+
| require('node:fs').constants.O_TRUNC | (require('node:fs').constants.O_NOFOLLOW || 0), 0o755);
|
|
170
|
+
try { await file.writeFile(item.data); } finally { await file.close(); }
|
|
171
|
+
await fs.chmod(item.path, 0o755);
|
|
172
|
+
}
|
|
173
|
+
|
|
154
174
|
async function commandParent(file) {
|
|
155
175
|
for (let parent = path.dirname(file); ; parent = path.dirname(parent)) {
|
|
156
176
|
try { const info = await fs.lstat(parent); if (!info.isDirectory() || info.isSymbolicLink()) throw new Error('Linked npm command preserved'); }
|
|
@@ -188,119 +208,154 @@ function windowsCleanupCommand(helper, pin) {
|
|
|
188
208
|
return ['powershell.exe', '-NoProfile', '-Command', dispatch];
|
|
189
209
|
}
|
|
190
210
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const prefix = await registration(packageRoot, platform);
|
|
196
|
-
if (prefix === null) return null;
|
|
211
|
+
const sameIdentity = (a, b) => a.dev === b.dev && a.ino === b.ino;
|
|
212
|
+
|
|
213
|
+
// The user's home and the data directory that holds the canonical installation root.
|
|
214
|
+
function productData(environment, platform) {
|
|
197
215
|
const home = (platform === 'win32' ? environment.USERPROFILE || environment.HOME : environment.HOME) || require('node:os').homedir();
|
|
198
216
|
const data = platform === 'win32' ? environment.LOCALAPPDATA || home
|
|
199
217
|
: environment.XDG_DATA_HOME || path.join(home, '.local', 'share');
|
|
200
|
-
|
|
218
|
+
return { home, data };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Every location an installed native product could occupy. Registration-only
|
|
222
|
+
// removal is allowed only when all of them are absent or empty directory shells.
|
|
223
|
+
function nativeLocations(environment, platform, prefix, options) {
|
|
224
|
+
const { home, data } = productData(environment, platform);
|
|
201
225
|
const canonical = path.join(data, platform === 'win32' ? 'Stackbite' : 'stackbite');
|
|
226
|
+
const root = options.installRoot || canonical;
|
|
202
227
|
const old = path.join(data, platform === 'win32' ? 'Helm' : 'helm');
|
|
228
|
+
const legacyNames = ['app', 'cache', 'state', 'default-install.json', 'install.json'];
|
|
203
229
|
// Keep this recovery module standalone after npm removes the other modules.
|
|
204
230
|
// This is cache.js's npm namespace, not Python's locator/control fallback.
|
|
205
231
|
const npmData = environment.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
|
|
206
|
-
const roots = [root, ...
|
|
207
|
-
...(platform === 'win32' ? [path.join(npmData, 'Stackbite'),
|
|
208
|
-
...['app', 'cache', 'state', 'default-install.json', 'install.json'].map(name =>
|
|
209
|
-
path.join(npmData, 'Helm', name))] : []),
|
|
232
|
+
const roots = [root, ...legacyNames.map(name => path.join(old, name)),
|
|
233
|
+
...(platform === 'win32' ? [path.join(npmData, 'Stackbite'), ...legacyNames.map(name => path.join(npmData, 'Helm', name))] : []),
|
|
210
234
|
...(platform === 'win32' ? [] : ['stackbite', 'helm'].flatMap(name => [
|
|
211
235
|
path.join(environment.XDG_STATE_HOME || path.join(home, '.local', 'state'), name),
|
|
212
236
|
path.join(environment.XDG_CACHE_HOME || path.join(home, '.cache'), name)])),
|
|
213
237
|
...[options.cache, environment.STACKBITE_HOME, environment.STACKBITE_STATE_DIR].filter(Boolean)];
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const info = await fs.lstat(location);
|
|
220
|
-
if (preparation && location === preparation.file) return preparationCurrent();
|
|
221
|
-
if (!info.isDirectory() || info.isSymbolicLink() || await fs.realpath(location) !== path.resolve(location)) return false;
|
|
222
|
-
for (const name of await fs.readdir(location)) if (!await empty(path.join(location, name))) return false;
|
|
223
|
-
return true;
|
|
224
|
-
} catch (error) { if (error.code === 'ENOENT') return true; throw error; }
|
|
238
|
+
const commands = platform === 'win32' ? ['stackbite.cmd', 'stackbite-portable.cmd', 'stackbite.exe', 'stackbite.exe.next'] : ['stackbite'];
|
|
239
|
+
for (const name of commands) {
|
|
240
|
+
const native = path.join(home, '.local', 'bin', name);
|
|
241
|
+
const own = platform === 'win32' ? path.join(prefix, name) : path.join(prefix, 'bin', name);
|
|
242
|
+
if (native !== own) roots.push(native);
|
|
225
243
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
244
|
+
return { root, canonical, roots: [...new Set(roots)] };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// True when `start` and each ancestor is a real, unlinked directory outside a Git checkout.
|
|
248
|
+
async function plainDirectoryChain(start) {
|
|
249
|
+
for (let parent = start; ; parent = path.dirname(parent)) {
|
|
250
|
+
const info = await fs.lstat(parent);
|
|
251
|
+
if (!info.isDirectory() || info.isSymbolicLink() || await fs.realpath(parent) !== parent) return false;
|
|
252
|
+
try { await fs.lstat(path.join(parent, '.git')); return false; }
|
|
253
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
254
|
+
if (parent === path.dirname(parent)) return true;
|
|
236
255
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
}
|
|
245
|
-
for (const [directory, expected, names] of [
|
|
246
|
-
[root, preparation.rootInfo, ['.stackbite-update']],
|
|
247
|
-
[preparation.directory, preparation.directoryInfo, ['update.lock']],
|
|
248
|
-
]) {
|
|
249
|
-
const info = await fs.lstat(directory, { bigint: true });
|
|
250
|
-
if (!info.isDirectory() || info.isSymbolicLink() || !same(info, expected)
|
|
251
|
-
|| await fs.realpath(directory) !== directory
|
|
252
|
-
|| JSON.stringify((await fs.readdir(directory)).sort()) !== JSON.stringify(names)) return false;
|
|
253
|
-
}
|
|
254
|
-
return preparationCurrent();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async function ancestorsAreDirectories(location) {
|
|
259
|
+
for (let parent = path.dirname(location); ; parent = path.dirname(parent)) {
|
|
260
|
+
try { const info = await fs.lstat(parent); if (!info.isDirectory() || info.isSymbolicLink()) return false; }
|
|
261
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
262
|
+
if (parent === path.dirname(parent)) return true;
|
|
255
263
|
}
|
|
256
|
-
|
|
257
|
-
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// A first-ever publisher pins this existing control object across download.
|
|
267
|
+
// Unlinking its verified name invalidates that publisher; this FD is no mutex.
|
|
268
|
+
// Returns null when there is no preparation, false when it is not ours to remove.
|
|
269
|
+
async function openPreparation(root, canonical) {
|
|
258
270
|
const directory = path.join(root, '.stackbite-update'), file = path.join(directory, 'update.lock');
|
|
271
|
+
let info;
|
|
272
|
+
try { info = await fs.lstat(file, { bigint: true }); }
|
|
273
|
+
catch (error) { if (error.code === 'ENOENT') return null; throw error; }
|
|
274
|
+
if (root !== canonical || !path.isAbsolute(root) || path.resolve(root) !== root
|
|
275
|
+
|| root.split(path.sep).includes('..') || /[\0\r\n]/.test(root)
|
|
276
|
+
|| !info.isFile() || info.isSymbolicLink() || info.nlink !== 1n || info.size > 1n) return false;
|
|
259
277
|
try {
|
|
278
|
+
if (!await plainDirectoryChain(directory)) return false;
|
|
279
|
+
const { constants } = require('node:fs');
|
|
280
|
+
const handle = await fs.open(file, constants.O_RDONLY | (constants.O_NOFOLLOW || 0));
|
|
281
|
+
const preparation = { root, file, directory, info, handle };
|
|
260
282
|
try {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
283
|
+
preparation.rootInfo = await fs.lstat(root, { bigint: true });
|
|
284
|
+
preparation.directoryInfo = await fs.lstat(directory, { bigint: true });
|
|
285
|
+
} catch (error) { await handle.close(); throw error; }
|
|
286
|
+
return preparation;
|
|
287
|
+
} catch (error) { if (error.code === 'ENOENT') return null; throw error; }
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// The held lock is still the same single-link file holding nothing or the byte '0'.
|
|
291
|
+
async function preparationCurrent(preparation) {
|
|
292
|
+
const held = await preparation.handle.stat({ bigint: true });
|
|
293
|
+
const current = await fs.lstat(preparation.file, { bigint: true });
|
|
294
|
+
const bytes = Buffer.alloc(2);
|
|
295
|
+
const { bytesRead } = await preparation.handle.read(bytes, 0, bytes.length, 0);
|
|
296
|
+
return held.isFile() && current.isFile() && !current.isSymbolicLink()
|
|
297
|
+
&& held.nlink === 1n && current.nlink === 1n && sameIdentity(held, preparation.info) && sameIdentity(current, held)
|
|
298
|
+
&& held.size <= 1n && current.size === held.size && BigInt(bytesRead) === held.size
|
|
299
|
+
&& (bytesRead === 0 || bytesRead === 1 && bytes[0] === 0x30);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async function preparationUnchanged(preparation) {
|
|
303
|
+
if (!await plainDirectoryChain(preparation.directory)) return false;
|
|
304
|
+
for (const [directory, expected, names] of [
|
|
305
|
+
[preparation.root, preparation.rootInfo, ['.stackbite-update']],
|
|
306
|
+
[preparation.directory, preparation.directoryInfo, ['update.lock']],
|
|
307
|
+
]) {
|
|
308
|
+
const info = await fs.lstat(directory, { bigint: true });
|
|
309
|
+
if (!info.isDirectory() || info.isSymbolicLink() || !sameIdentity(info, expected)
|
|
310
|
+
|| await fs.realpath(directory) !== directory
|
|
311
|
+
|| JSON.stringify((await fs.readdir(directory)).sort()) !== JSON.stringify(names)) return false;
|
|
312
|
+
}
|
|
313
|
+
return preparationCurrent(preparation);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Directory shells left by a completed physical tail contain no artifacts.
|
|
317
|
+
// The pinned canonical preparation is the only permitted file.
|
|
318
|
+
async function emptyLocation(location, preparation) {
|
|
319
|
+
try {
|
|
320
|
+
const info = await fs.lstat(location);
|
|
321
|
+
if (preparation && location === preparation.file) return preparationCurrent(preparation);
|
|
322
|
+
if (!info.isDirectory() || info.isSymbolicLink() || await fs.realpath(location) !== path.resolve(location)) return false;
|
|
323
|
+
for (const name of await fs.readdir(location)) if (!await emptyLocation(path.join(location, name), preparation)) return false;
|
|
324
|
+
return true;
|
|
325
|
+
} catch (error) { if (error.code === 'ENOENT') return true; throw error; }
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async function removePreparation(preparation) {
|
|
329
|
+
await fs.unlink(preparation.file);
|
|
330
|
+
for (const [location, expected] of [[preparation.directory, preparation.directoryInfo], [preparation.root, preparation.rootInfo]]) {
|
|
331
|
+
const current = await fs.lstat(location, { bigint: true });
|
|
332
|
+
if (!sameIdentity(current, expected) || !current.isDirectory() || current.isSymbolicLink()
|
|
333
|
+
|| await fs.realpath(location) !== location) throw new Error('Installation preparation replaced; preserved');
|
|
334
|
+
await fs.rmdir(location);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function registrationOnly(argv, environment, launch, options = {}) {
|
|
339
|
+
if (!explicitApply(argv)) return null;
|
|
340
|
+
const platform = options.platform || process.platform;
|
|
341
|
+
const packageRoot = options.packageRoot || path.resolve(__dirname, '../../..');
|
|
342
|
+
const prefix = await registration(packageRoot, platform);
|
|
343
|
+
if (prefix === null) return null;
|
|
344
|
+
const { root, canonical, roots } = nativeLocations(environment, platform, prefix, options);
|
|
345
|
+
const preparation = await openPreparation(root, canonical);
|
|
346
|
+
if (preparation === false) return null;
|
|
347
|
+
try {
|
|
348
|
+
if (preparation && !await preparationUnchanged(preparation)) return null;
|
|
349
|
+
for (const location of roots) {
|
|
284
350
|
const absolute = path.resolve(location);
|
|
285
|
-
|
|
286
|
-
try { const info = await fs.lstat(parent); if (!info.isDirectory() || info.isSymbolicLink()) return null; }
|
|
287
|
-
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
288
|
-
if (parent === path.dirname(parent)) break;
|
|
289
|
-
}
|
|
290
|
-
if (!await empty(absolute)) return null;
|
|
351
|
+
if (!await ancestorsAreDirectories(absolute) || !await emptyLocation(absolute, preparation)) return null;
|
|
291
352
|
}
|
|
292
353
|
if (preparation) {
|
|
293
|
-
if (!await
|
|
294
|
-
await
|
|
295
|
-
for (const [location, expected] of [[directory, preparation.directoryInfo], [root, preparation.rootInfo]]) {
|
|
296
|
-
const current = await fs.lstat(location, { bigint: true });
|
|
297
|
-
if (!same(current, expected) || !current.isDirectory() || current.isSymbolicLink()
|
|
298
|
-
|| await fs.realpath(location) !== location) throw new Error('Installation preparation replaced; preserved');
|
|
299
|
-
await fs.rmdir(location);
|
|
300
|
-
}
|
|
354
|
+
if (!await preparationUnchanged(preparation)) return null;
|
|
355
|
+
await removePreparation(preparation);
|
|
301
356
|
}
|
|
302
357
|
return removeRegistration(argv, { code: 0, signal: null }, environment, launch, { ...options, packageRoot });
|
|
303
|
-
} finally { if (
|
|
358
|
+
} finally { if (preparation) await preparation.handle.close(); }
|
|
304
359
|
}
|
|
305
360
|
|
|
306
361
|
// Recovery never installs/downloads another runtime. Only the accepted journal's
|
|
@@ -308,22 +363,46 @@ async function registrationOnly(argv, environment, launch, options = {}) {
|
|
|
308
363
|
async function resumeWithoutRuntime(argv, environment, launch, options = {}) {
|
|
309
364
|
if (!explicitApply(argv)) return null;
|
|
310
365
|
const platform = options.platform || process.platform;
|
|
311
|
-
const
|
|
312
|
-
const root = options.installRoot || (platform === 'win32'
|
|
313
|
-
? path.join(environment.LOCALAPPDATA || home, 'Stackbite')
|
|
314
|
-
: path.join(environment.XDG_DATA_HOME || path.join(home, '.local', 'share'), 'stackbite'));
|
|
366
|
+
const root = options.installRoot || path.join(productData(environment, platform).data, platform === 'win32' ? 'Stackbite' : 'stackbite');
|
|
315
367
|
const journal = path.join(root, '.stackbite-uninstall.json');
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
const info = await fs.lstat(journal);
|
|
319
|
-
if (!info.isFile() || info.isSymbolicLink() || info.nlink !== 1 || info.size > 8 * 1024 * 1024
|
|
320
|
-
|| await fs.realpath(journal) !== journal) throw new Error('Unsafe removal journal');
|
|
321
|
-
record = JSON.parse(await fs.readFile(journal, 'utf8'));
|
|
322
|
-
} catch (error) { if (error.code === 'ENOENT') return null; throw error; }
|
|
368
|
+
const record = await readRemovalJournal(journal);
|
|
369
|
+
if (record === undefined) return null;
|
|
323
370
|
if (record.schema !== 'stackbite.uninstall.v1' || !record.accepted || !record.processes_settled
|
|
324
371
|
|| record.install_identity?.root !== root) return null;
|
|
325
372
|
// The capsule takes the existing installation/removal locks and owns all
|
|
326
373
|
// journal mutation, including npm settlement. This reader never rewrites it.
|
|
374
|
+
const recovery = recoveryCommand(record, journal, platform);
|
|
375
|
+
if (recovery === null) return null;
|
|
376
|
+
const { selected, helper, pin } = recovery;
|
|
377
|
+
const info = await fs.lstat(helper);
|
|
378
|
+
if (!await soleRegularFile(helper, info, 128 * 1024) || await sha256File(helper) !== pin.sha256) {
|
|
379
|
+
throw new Error('Removal capsule changed; preserved');
|
|
380
|
+
}
|
|
381
|
+
if (platform !== 'win32') await assertShellCapsule(record, journal);
|
|
382
|
+
return launch(selected[0], selected.slice(1), environment, options);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// The parsed journal; undefined when there is none.
|
|
386
|
+
async function readRemovalJournal(journal) {
|
|
387
|
+
try {
|
|
388
|
+
const info = await fs.lstat(journal);
|
|
389
|
+
if (!await soleRegularFile(journal, info, 8 * 1024 * 1024)) throw new Error('Unsafe removal journal');
|
|
390
|
+
return JSON.parse(await fs.readFile(journal, 'utf8'));
|
|
391
|
+
} catch (error) { if (error.code === 'ENOENT') return undefined; throw error; }
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// A regular, singly linked file within `limit` bytes that is its own real path.
|
|
395
|
+
async function soleRegularFile(location, info, limit = Infinity) {
|
|
396
|
+
return info.isFile() && !info.isSymbolicLink() && info.nlink === 1 && !(info.size > limit)
|
|
397
|
+
&& await fs.realpath(location) === location;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async function sha256File(location) {
|
|
401
|
+
return require('node:crypto').createHash('sha256').update(await fs.readFile(location)).digest('hex');
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// The journal's recovery argv when it is exactly this platform's pinned helper invocation.
|
|
405
|
+
function recoveryCommand(record, journal, platform) {
|
|
327
406
|
const command = record.recovery_command;
|
|
328
407
|
const suffix = platform === 'win32' ? '.ps1' : '.sh';
|
|
329
408
|
const helper = journal.replace(/\.json$/, suffix);
|
|
@@ -334,43 +413,36 @@ async function resumeWithoutRuntime(argv, environment, launch, options = {}) {
|
|
|
334
413
|
const selected = platform === 'win32' && JSON.stringify(command) === JSON.stringify(legacy) ? legacy : expected;
|
|
335
414
|
if (!Array.isArray(command) || JSON.stringify(command) !== JSON.stringify(selected)
|
|
336
415
|
|| !record.helper_files?.some(item => item.path === helper)) return null;
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
416
|
+
return { selected, helper, pin };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
async function assertShellCapsule(record, journal) {
|
|
420
|
+
const capsule = journal.replace(/\.json$/, '.cleanup');
|
|
421
|
+
const pinned = record.helper_files.find(item => item.path === capsule);
|
|
422
|
+
const current = await fs.lstat(capsule);
|
|
423
|
+
if (!pinned || !await soleRegularFile(capsule, current) || await sha256File(capsule) !== pinned.sha256) {
|
|
341
424
|
throw new Error('Removal capsule changed; preserved');
|
|
342
425
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
if (
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
if (typeof location !== 'string' || !path.isAbsolute(location) || /[\x00\r\n]/.test(location)
|
|
362
|
-
|| location.split(path.sep).includes('..')) throw new Error('Source cleanup runtime changed');
|
|
363
|
-
const current = await fs.lstat(location);
|
|
364
|
-
if (!current.isFile() || current.isSymbolicLink() || current.nlink !== 1
|
|
365
|
-
|| await fs.realpath(location) !== location || current.dev !== pin.identity?.device
|
|
366
|
-
|| current.ino !== pin.identity?.inode
|
|
367
|
-
|| require('node:crypto').createHash('sha256').update(await fs.readFile(location)).digest('hex') !== pin.sha256) {
|
|
368
|
-
throw new Error('Source cleanup runtime changed');
|
|
369
|
-
}
|
|
370
|
-
}
|
|
426
|
+
// Source invocations retain their existing interpreter/entry outside the
|
|
427
|
+
// removal inventory. Validate those exact pins before executing the shell
|
|
428
|
+
// capsule; neither environment PATH nor journal argv selects a runtime.
|
|
429
|
+
if (record.source_cleanup_pins !== undefined) await assertSourceCleanupPins(record.source_cleanup_pins);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
async function assertSourceCleanupPins(pins) {
|
|
433
|
+
if (!Array.isArray(pins) || pins.length !== 4) {
|
|
434
|
+
throw new Error('Source cleanup identity changed');
|
|
435
|
+
}
|
|
436
|
+
for (const pin of pins) {
|
|
437
|
+
const location = pin.path;
|
|
438
|
+
if (typeof location !== 'string' || !path.isAbsolute(location) || /[\x00\r\n]/.test(location)
|
|
439
|
+
|| location.split(path.sep).includes('..')) throw new Error('Source cleanup runtime changed');
|
|
440
|
+
const current = await fs.lstat(location);
|
|
441
|
+
if (!await soleRegularFile(location, current) || current.dev !== pin.identity?.device
|
|
442
|
+
|| current.ino !== pin.identity?.inode || await sha256File(location) !== pin.sha256) {
|
|
443
|
+
throw new Error('Source cleanup runtime changed');
|
|
371
444
|
}
|
|
372
445
|
}
|
|
373
|
-
return launch(selected[0], selected.slice(1), environment, options);
|
|
374
446
|
}
|
|
375
447
|
|
|
376
448
|
module.exports = { registration, npmProgram, removeRegistration, explicitApply, descriptor, resumeWithoutRuntime, registrationOnly };
|
|
@@ -1,35 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema": 1,
|
|
3
|
-
"version": "0.5.
|
|
4
|
-
"commit": "
|
|
3
|
+
"version": "0.5.99",
|
|
4
|
+
"commit": "90aae4864b24be65ebfb31fcf6fe17b9d5b28cb5",
|
|
5
5
|
"repository": "Shinick-Han/stackbite",
|
|
6
6
|
"platforms": {
|
|
7
7
|
"win32-x64": {
|
|
8
|
-
"asset": "stackbite-0.5.
|
|
9
|
-
"url": "https://github.com/Shinick-Han/stackbite/releases/download/v0.5.
|
|
10
|
-
"sha256": "
|
|
11
|
-
"size":
|
|
12
|
-
"root": "stackbite-0.5.
|
|
13
|
-
"manifestSha256": "
|
|
14
|
-
"metadataSha256": "
|
|
8
|
+
"asset": "stackbite-0.5.99-windows-x64-90aae4864b24.zip",
|
|
9
|
+
"url": "https://github.com/Shinick-Han/stackbite/releases/download/v0.5.99/stackbite-0.5.99-windows-x64-90aae4864b24.zip",
|
|
10
|
+
"sha256": "1d2e469d71802398b095a1ef77abe26050c7a43523005119449e01108350f13c",
|
|
11
|
+
"size": 95935897,
|
|
12
|
+
"root": "stackbite-0.5.99-windows-x64-90aae4864b24",
|
|
13
|
+
"manifestSha256": "4544eb749c94ef633748650466298a013cbf950533e42a54c3fc1a87537e0475",
|
|
14
|
+
"metadataSha256": "91e564369939e31d1e2e5d549d1185fc081b2ad80e478fa0d0658f8870898b70"
|
|
15
15
|
},
|
|
16
16
|
"linux-x64": {
|
|
17
|
-
"asset": "stackbite-0.5.
|
|
18
|
-
"url": "https://github.com/Shinick-Han/stackbite/releases/download/v0.5.
|
|
19
|
-
"sha256": "
|
|
20
|
-
"size":
|
|
21
|
-
"root": "stackbite-0.5.
|
|
22
|
-
"manifestSha256": "
|
|
23
|
-
"metadataSha256": "
|
|
17
|
+
"asset": "stackbite-0.5.99-linux-x86_64-90aae4864b24.tar.gz",
|
|
18
|
+
"url": "https://github.com/Shinick-Han/stackbite/releases/download/v0.5.99/stackbite-0.5.99-linux-x86_64-90aae4864b24.tar.gz",
|
|
19
|
+
"sha256": "84e78372b8ef337480fe230a4eaf4cf2ff39048ff8af2c21929e8b4535122d5a",
|
|
20
|
+
"size": 76221146,
|
|
21
|
+
"root": "stackbite-0.5.99-linux-x86_64-90aae4864b24",
|
|
22
|
+
"manifestSha256": "a892cd3e8ff2d338e6bd8580df67b357b7e15b2129107d359d97e12cb98d1673",
|
|
23
|
+
"metadataSha256": "441c8da312ee420767e761665c83753018a01e20d2d954670f43bdc640a1369c"
|
|
24
24
|
},
|
|
25
25
|
"darwin-arm64": {
|
|
26
|
-
"asset": "stackbite-0.5.
|
|
27
|
-
"url": "https://github.com/Shinick-Han/stackbite/releases/download/v0.5.
|
|
28
|
-
"sha256": "
|
|
29
|
-
"size":
|
|
30
|
-
"root": "stackbite-0.5.
|
|
31
|
-
"manifestSha256": "
|
|
32
|
-
"metadataSha256": "
|
|
26
|
+
"asset": "stackbite-0.5.99-macos-arm64-90aae4864b24.tar.gz",
|
|
27
|
+
"url": "https://github.com/Shinick-Han/stackbite/releases/download/v0.5.99/stackbite-0.5.99-macos-arm64-90aae4864b24.tar.gz",
|
|
28
|
+
"sha256": "e40810f5c5b18728bddbc494e27c89d46b32ce4eb3c0db4fe5bca1b0f0e712f8",
|
|
29
|
+
"size": 31167159,
|
|
30
|
+
"root": "stackbite-0.5.99-macos-arm64-90aae4864b24",
|
|
31
|
+
"manifestSha256": "49b23c300843d261c6195b252cb6d10dec2810795fc6d7a599d57d71d6a3d288",
|
|
32
|
+
"metadataSha256": "5768005ea1b1c5beecbc3f5933818db04f2f5cdd533f29b511ad6e0d6af46b19"
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
}
|