workbite 0.1.18 → 0.1.20

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,71 +1,71 @@
1
- 'use strict';
2
- const fs = require('node:fs/promises');
3
- const fsSync = require('node:fs');
4
- const os = require('node:os');
5
- const { randomBytes } = require('node:crypto');
6
- const { performance } = require('node:perf_hooks');
7
-
8
- function sameFile(a, b) {
9
- // An unavailable/zero inode is not sufficient evidence to remove anything.
10
- return a.isFile() && b.isFile() && a.ino !== 0n && a.ino === b.ino
11
- && a.dev === b.dev && a.birthtimeNs === b.birthtimeNs && b.nlink === 1n;
12
- }
13
-
14
- function releaseOwned(file, handle, identity, bytes) {
15
- try {
16
- // Keep the descriptor open so its inode cannot be recycled. Read through
17
- // that descriptor, never a pathname that could now point at a symlink.
18
- // Synchronous checks avoid yielding to other JS before the final unlink.
19
- // Node has no atomic compare-and-unlink: external actors must not mutate
20
- // the lock directory during release. Cooperating callers never reclaim it.
21
- if (!sameFile(identity, fsSync.fstatSync(handle.fd, { bigint: true }))) return;
22
- const actual = Buffer.alloc(bytes.length + 1);
23
- const count = fsSync.readSync(handle.fd, actual, 0, actual.length, 0);
24
- if (count !== bytes.length || !actual.subarray(0, count).equals(bytes)) return;
25
- if (!sameFile(identity, fsSync.lstatSync(file, { bigint: true }))) return;
26
- fsSync.unlinkSync(file);
27
- } catch {
28
- // Missing, unreadable or unprovable ownership stays closed; cleanup must
29
- // not hide the action's result/error or remove another caller's lock.
30
- }
31
- }
32
-
33
- // `stale` in older callers' options is intentionally ignored. Neither age nor
34
- // a locally absent PID proves a lock owner on shared storage has stopped.
35
- async function withLock(file, action, { wait = 180000 } = {}) {
36
- if (!Number.isFinite(wait) || wait < 0) throw new RangeError('Lock wait must be a finite nonnegative number.');
37
- const bytes = Buffer.from(JSON.stringify({ pid: process.pid, hostname: os.hostname().slice(0, 255),
38
- boot: null, nonce: randomBytes(16).toString('hex') }) + '\n');
39
- const deadline = performance.now() + wait;
40
- let handle;
41
- for (;;) {
42
- try {
43
- handle = await fs.open(file, 'wx+', 0o600);
44
- break;
45
- } catch (error) {
46
- if (error.code !== 'EEXIST') throw error;
47
- const remaining = deadline - performance.now();
48
- if (remaining <= 0) {
49
- const timeout = new Error(`Another WorkBite open may still be starting this state (lock: ${file}). `
50
- + 'Run workbite status --json to inspect it, and retry after the owner finishes. '
51
- + 'An old lock or a PID absent on this host does not prove that a shared-state owner has stopped; '
52
- + 'WorkBite leaves the lock in place.');
53
- timeout.code = 'WORKBITE_STARTUP_LOCK_TIMEOUT';
54
- throw timeout;
55
- }
56
- await new Promise(resolve => setTimeout(resolve, Math.min(200, remaining)));
57
- }
58
- }
59
-
60
- let identity;
61
- try {
62
- identity = await handle.stat({ bigint: true });
63
- await handle.writeFile(bytes);
64
- return await action();
65
- } finally {
66
- if (identity) releaseOwned(file, handle, identity, bytes);
67
- await handle.close();
68
- }
69
- }
70
-
71
- module.exports = { withLock };
1
+ 'use strict';
2
+ const fs = require('node:fs/promises');
3
+ const fsSync = require('node:fs');
4
+ const os = require('node:os');
5
+ const { randomBytes } = require('node:crypto');
6
+ const { performance } = require('node:perf_hooks');
7
+
8
+ function sameFile(a, b) {
9
+ // An unavailable/zero inode is not sufficient evidence to remove anything.
10
+ return a.isFile() && b.isFile() && a.ino !== 0n && a.ino === b.ino
11
+ && a.dev === b.dev && a.birthtimeNs === b.birthtimeNs && b.nlink === 1n;
12
+ }
13
+
14
+ function releaseOwned(file, handle, identity, bytes) {
15
+ try {
16
+ // Keep the descriptor open so its inode cannot be recycled. Read through
17
+ // that descriptor, never a pathname that could now point at a symlink.
18
+ // Synchronous checks avoid yielding to other JS before the final unlink.
19
+ // Node has no atomic compare-and-unlink: external actors must not mutate
20
+ // the lock directory during release. Cooperating callers never reclaim it.
21
+ if (!sameFile(identity, fsSync.fstatSync(handle.fd, { bigint: true }))) return;
22
+ const actual = Buffer.alloc(bytes.length + 1);
23
+ const count = fsSync.readSync(handle.fd, actual, 0, actual.length, 0);
24
+ if (count !== bytes.length || !actual.subarray(0, count).equals(bytes)) return;
25
+ if (!sameFile(identity, fsSync.lstatSync(file, { bigint: true }))) return;
26
+ fsSync.unlinkSync(file);
27
+ } catch {
28
+ // Missing, unreadable or unprovable ownership stays closed; cleanup must
29
+ // not hide the action's result/error or remove another caller's lock.
30
+ }
31
+ }
32
+
33
+ // `stale` in older callers' options is intentionally ignored. Neither age nor
34
+ // a locally absent PID proves a lock owner on shared storage has stopped.
35
+ async function withLock(file, action, { wait = 180000 } = {}) {
36
+ if (!Number.isFinite(wait) || wait < 0) throw new RangeError('Lock wait must be a finite nonnegative number.');
37
+ const bytes = Buffer.from(JSON.stringify({ pid: process.pid, hostname: os.hostname().slice(0, 255),
38
+ boot: null, nonce: randomBytes(16).toString('hex') }) + '\n');
39
+ const deadline = performance.now() + wait;
40
+ let handle;
41
+ for (;;) {
42
+ try {
43
+ handle = await fs.open(file, 'wx+', 0o600);
44
+ break;
45
+ } catch (error) {
46
+ if (error.code !== 'EEXIST') throw error;
47
+ const remaining = deadline - performance.now();
48
+ if (remaining <= 0) {
49
+ const timeout = new Error(`Another WorkBite open may still be starting this state (lock: ${file}). `
50
+ + 'Run workbite status --json to inspect it, and retry after the owner finishes. '
51
+ + 'An old lock or a PID absent on this host does not prove that a shared-state owner has stopped; '
52
+ + 'WorkBite leaves the lock in place.');
53
+ timeout.code = 'WORKBITE_STARTUP_LOCK_TIMEOUT';
54
+ throw timeout;
55
+ }
56
+ await new Promise(resolve => setTimeout(resolve, Math.min(200, remaining)));
57
+ }
58
+ }
59
+
60
+ let identity;
61
+ try {
62
+ identity = await handle.stat({ bigint: true });
63
+ await handle.writeFile(bytes);
64
+ return await action();
65
+ } finally {
66
+ if (identity) releaseOwned(file, handle, identity, bytes);
67
+ await handle.close();
68
+ }
69
+ }
70
+
71
+ module.exports = { withLock };
package/lib/transfer.js CHANGED
@@ -1,113 +1,113 @@
1
- 'use strict';
2
- // Files only: the native client/owner owns all transfer HTTP, graph validation and commits.
3
- const fs = require('node:fs/promises');
4
- const path = require('node:path');
5
- const { constants } = require('node:fs');
6
- const { randomBytes } = require('node:crypto');
7
- const { fail, UUID } = require('./common');
8
-
9
- const MAX_TRANSFER_BYTES = 1024 * 1024;
10
- const DIGEST = /^sha256:[0-9a-f]{64}$/;
11
- const plain = value => value !== null && typeof value === 'object' && !Array.isArray(value);
12
- function localPath(filename) {
13
- if (typeof filename !== 'string' || !filename || /[\x00-\x1f]/.test(filename)
14
- || /^[a-z][a-z0-9+.-]*:\/\//i.test(filename) || /^(?:\\\\|\/\/)/.test(filename)) fail('Transfer files must be local filesystem paths');
15
- return path.resolve(filename);
16
- }
17
- // Fail closed on links in the requested path. Do not create missing output directories.
18
- async function realDirectory(directory, io = fs) {
19
- const parent = path.dirname(directory);
20
- if (parent !== directory) await realDirectory(parent, io);
21
- const info = await io.lstat(directory);
22
- if (!info.isDirectory() || info.isSymbolicLink()) fail('Transfer path contains a link or non-directory');
23
- }
24
- async function outputPath(filename, io = fs) {
25
- const target = localPath(filename);
26
- await realDirectory(path.dirname(target), io);
27
- try { await io.lstat(target); } catch (error) { if (error.code === 'ENOENT') return target; throw error; }
28
- fail('Transfer output already exists; choose a new file (overwrite is not supported)');
29
- }
30
- function archive(value, workspaceUid) {
31
- if (!plain(value) || value.format !== 'workbite.task-transfer.v1'
32
- || typeof value.source_workspace_uid !== 'string' || !UUID.test(value.source_workspace_uid)
33
- || typeof value.source_digest !== 'string' || !DIGEST.test(value.source_digest)
34
- || !Array.isArray(value.tasks) || !value.tasks.every(plain)
35
- || !Array.isArray(value.objectives) || !value.objectives.every(plain)) fail('Invalid workbite.task-transfer.v1 archive');
36
- if (workspaceUid !== undefined && value.source_workspace_uid !== workspaceUid) fail('Export archive does not match the bound source workspace');
37
- if (Buffer.byteLength(JSON.stringify(value)) > MAX_TRANSFER_BYTES) fail(`Transfer archive exceeds ${MAX_TRANSFER_BYTES} bytes`);
38
- return value;
39
- }
40
- async function readArchive(filename, io = fs) {
41
- const target = localPath(filename);
42
- await realDirectory(path.dirname(target), io);
43
- const before = await io.lstat(target);
44
- if (!before.isFile() || before.isSymbolicLink() || before.size > MAX_TRANSFER_BYTES) fail('Transfer input must be a regular file of at most 1048576 bytes');
45
- const handle = await io.open(target, constants.O_RDONLY | (constants.O_NOFOLLOW || 0));
46
- try {
47
- const info = await handle.stat();
48
- if (!info.isFile() || info.size > MAX_TRANSFER_BYTES || info.ino !== before.ino || info.dev !== before.dev) fail('Transfer input changed while opening');
49
- // Read at most the bound plus one byte, even if another process grows the file.
50
- const bytes = Buffer.alloc(MAX_TRANSFER_BYTES + 1);
51
- let size = 0;
52
- while (size < bytes.length) {
53
- const read = await handle.read(bytes, size, bytes.length - size, null);
54
- if (!read.bytesRead) break;
55
- size += read.bytesRead;
56
- }
57
- if (size > MAX_TRANSFER_BYTES) fail(`Transfer archive exceeds ${MAX_TRANSFER_BYTES} bytes`);
58
- let text, value;
59
- try { text = new TextDecoder('utf-8', { fatal: true }).decode(bytes.subarray(0, size)); value = JSON.parse(text); }
60
- catch { fail('Transfer input must contain valid UTF-8 JSON'); }
61
- archive(value);
62
- return text;
63
- } finally { await handle.close(); }
64
- }
65
- // Publish with an atomic hard link: rename would overwrite an existing destination on POSIX.
66
- // The private temporary file is in the same directory; an unsupported link is a safe refusal.
67
- async function writePrivate(filename, value, io = fs) {
68
- const target = await outputPath(filename, io);
69
- const bytes = Buffer.from(JSON.stringify(value));
70
- if (bytes.length > MAX_TRANSFER_BYTES) fail(`Transfer output exceeds ${MAX_TRANSFER_BYTES} bytes`);
71
- const temporary = path.join(path.dirname(target), `.workbite-transfer-${randomBytes(16).toString('hex')}.tmp`);
72
- let handle;
73
- try {
74
- handle = await io.open(temporary, 'wx', 0o600);
75
- await handle.writeFile(bytes);
76
- await handle.sync();
77
- await handle.close(); handle = null;
78
- // Recheck the directory before publishing; link refuses even a raced-in destination.
79
- await realDirectory(path.dirname(target), io);
80
- await io.link(temporary, target);
81
- } finally {
82
- if (handle) await handle.close();
83
- await io.unlink(temporary).catch(error => { if (error.code !== 'ENOENT') throw error; });
84
- }
85
- return target;
86
- }
87
- function resultData(result, command, uid, intent) {
88
- if (typeof result.stdout !== 'string' || Buffer.byteLength(result.stdout) > MAX_TRANSFER_BYTES + 8192) fail('Invalid native transfer output');
89
- let envelope;
90
- try { envelope = JSON.parse(result.stdout); } catch { fail('Invalid native transfer envelope'); }
91
- if (!plain(envelope) || envelope.contract !== 'workstack.cli.v1' || !plain(envelope.data)
92
- || envelope.error !== undefined || envelope.meta?.command !== `agent.${command}` || envelope.meta?.workspace_uid !== uid
93
- || (intent !== undefined && envelope.meta?.intent_id !== intent)) fail('Native transfer output does not match this workspace, command and intent');
94
- return envelope.data;
95
- }
96
- function idMap(value) {
97
- // The owner emits maps by source authority ID; values are generated target display IDs.
98
- if (!plain(value) || Object.keys(value).some(key => !['tasks', 'task_uids', 'objectives', 'key_results'].includes(key))) fail('Invalid native transfer id_map');
99
- for (const [kind, mapping] of Object.entries(value)) {
100
- if (!plain(mapping)) fail('Invalid native transfer id_map');
101
- for (const [source, target] of Object.entries(mapping)) {
102
- if (!source || /[\x00-\x1f]/.test(source)) fail(`Invalid native transfer ${kind} mapping`);
103
- if (kind === 'key_results') {
104
- if (!plain(target) || Object.entries(target).some(([key, id]) => !key || /[\x00-\x1f]/.test(key)
105
- || typeof id !== 'string' || !id || /[\x00-\x1f]/.test(id))) fail('Invalid native transfer key_results mapping');
106
- } else if (typeof target !== 'string' || !target || /[\x00-\x1f]/.test(target)
107
- || (kind === 'task_uids' && !UUID.test(target))) fail(`Invalid native transfer ${kind} mapping`);
108
- }
109
- }
110
- if (!plain(value.tasks)) fail('Native transfer id_map requires tasks');
111
- return value;
112
- }
113
- module.exports = { MAX_TRANSFER_BYTES, DIGEST, readArchive, archive, outputPath, writePrivate, resultData, idMap };
1
+ 'use strict';
2
+ // Files only: the native client/owner owns all transfer HTTP, graph validation and commits.
3
+ const fs = require('node:fs/promises');
4
+ const path = require('node:path');
5
+ const { constants } = require('node:fs');
6
+ const { randomBytes } = require('node:crypto');
7
+ const { fail, UUID } = require('./common');
8
+
9
+ const MAX_TRANSFER_BYTES = 1024 * 1024;
10
+ const DIGEST = /^sha256:[0-9a-f]{64}$/;
11
+ const plain = value => value !== null && typeof value === 'object' && !Array.isArray(value);
12
+ function localPath(filename) {
13
+ if (typeof filename !== 'string' || !filename || /[\x00-\x1f]/.test(filename)
14
+ || /^[a-z][a-z0-9+.-]*:\/\//i.test(filename) || /^(?:\\\\|\/\/)/.test(filename)) fail('Transfer files must be local filesystem paths');
15
+ return path.resolve(filename);
16
+ }
17
+ // Fail closed on links in the requested path. Do not create missing output directories.
18
+ async function realDirectory(directory, io = fs) {
19
+ const parent = path.dirname(directory);
20
+ if (parent !== directory) await realDirectory(parent, io);
21
+ const info = await io.lstat(directory);
22
+ if (!info.isDirectory() || info.isSymbolicLink()) fail('Transfer path contains a link or non-directory');
23
+ }
24
+ async function outputPath(filename, io = fs) {
25
+ const target = localPath(filename);
26
+ await realDirectory(path.dirname(target), io);
27
+ try { await io.lstat(target); } catch (error) { if (error.code === 'ENOENT') return target; throw error; }
28
+ fail('Transfer output already exists; choose a new file (overwrite is not supported)');
29
+ }
30
+ function archive(value, workspaceUid) {
31
+ if (!plain(value) || value.format !== 'workbite.task-transfer.v1'
32
+ || typeof value.source_workspace_uid !== 'string' || !UUID.test(value.source_workspace_uid)
33
+ || typeof value.source_digest !== 'string' || !DIGEST.test(value.source_digest)
34
+ || !Array.isArray(value.tasks) || !value.tasks.every(plain)
35
+ || !Array.isArray(value.objectives) || !value.objectives.every(plain)) fail('Invalid workbite.task-transfer.v1 archive');
36
+ if (workspaceUid !== undefined && value.source_workspace_uid !== workspaceUid) fail('Export archive does not match the bound source workspace');
37
+ if (Buffer.byteLength(JSON.stringify(value)) > MAX_TRANSFER_BYTES) fail(`Transfer archive exceeds ${MAX_TRANSFER_BYTES} bytes`);
38
+ return value;
39
+ }
40
+ async function readArchive(filename, io = fs) {
41
+ const target = localPath(filename);
42
+ await realDirectory(path.dirname(target), io);
43
+ const before = await io.lstat(target);
44
+ if (!before.isFile() || before.isSymbolicLink() || before.size > MAX_TRANSFER_BYTES) fail('Transfer input must be a regular file of at most 1048576 bytes');
45
+ const handle = await io.open(target, constants.O_RDONLY | (constants.O_NOFOLLOW || 0));
46
+ try {
47
+ const info = await handle.stat();
48
+ if (!info.isFile() || info.size > MAX_TRANSFER_BYTES || info.ino !== before.ino || info.dev !== before.dev) fail('Transfer input changed while opening');
49
+ // Read at most the bound plus one byte, even if another process grows the file.
50
+ const bytes = Buffer.alloc(MAX_TRANSFER_BYTES + 1);
51
+ let size = 0;
52
+ while (size < bytes.length) {
53
+ const read = await handle.read(bytes, size, bytes.length - size, null);
54
+ if (!read.bytesRead) break;
55
+ size += read.bytesRead;
56
+ }
57
+ if (size > MAX_TRANSFER_BYTES) fail(`Transfer archive exceeds ${MAX_TRANSFER_BYTES} bytes`);
58
+ let text, value;
59
+ try { text = new TextDecoder('utf-8', { fatal: true }).decode(bytes.subarray(0, size)); value = JSON.parse(text); }
60
+ catch { fail('Transfer input must contain valid UTF-8 JSON'); }
61
+ archive(value);
62
+ return text;
63
+ } finally { await handle.close(); }
64
+ }
65
+ // Publish with an atomic hard link: rename would overwrite an existing destination on POSIX.
66
+ // The private temporary file is in the same directory; an unsupported link is a safe refusal.
67
+ async function writePrivate(filename, value, io = fs) {
68
+ const target = await outputPath(filename, io);
69
+ const bytes = Buffer.from(JSON.stringify(value));
70
+ if (bytes.length > MAX_TRANSFER_BYTES) fail(`Transfer output exceeds ${MAX_TRANSFER_BYTES} bytes`);
71
+ const temporary = path.join(path.dirname(target), `.workbite-transfer-${randomBytes(16).toString('hex')}.tmp`);
72
+ let handle;
73
+ try {
74
+ handle = await io.open(temporary, 'wx', 0o600);
75
+ await handle.writeFile(bytes);
76
+ await handle.sync();
77
+ await handle.close(); handle = null;
78
+ // Recheck the directory before publishing; link refuses even a raced-in destination.
79
+ await realDirectory(path.dirname(target), io);
80
+ await io.link(temporary, target);
81
+ } finally {
82
+ if (handle) await handle.close();
83
+ await io.unlink(temporary).catch(error => { if (error.code !== 'ENOENT') throw error; });
84
+ }
85
+ return target;
86
+ }
87
+ function resultData(result, command, uid, intent) {
88
+ if (typeof result.stdout !== 'string' || Buffer.byteLength(result.stdout) > MAX_TRANSFER_BYTES + 8192) fail('Invalid native transfer output');
89
+ let envelope;
90
+ try { envelope = JSON.parse(result.stdout); } catch { fail('Invalid native transfer envelope'); }
91
+ if (!plain(envelope) || envelope.contract !== 'workstack.cli.v1' || !plain(envelope.data)
92
+ || envelope.error !== undefined || envelope.meta?.command !== `agent.${command}` || envelope.meta?.workspace_uid !== uid
93
+ || (intent !== undefined && envelope.meta?.intent_id !== intent)) fail('Native transfer output does not match this workspace, command and intent');
94
+ return envelope.data;
95
+ }
96
+ function idMap(value) {
97
+ // The owner emits maps by source authority ID; values are generated target display IDs.
98
+ if (!plain(value) || Object.keys(value).some(key => !['tasks', 'task_uids', 'objectives', 'key_results'].includes(key))) fail('Invalid native transfer id_map');
99
+ for (const [kind, mapping] of Object.entries(value)) {
100
+ if (!plain(mapping)) fail('Invalid native transfer id_map');
101
+ for (const [source, target] of Object.entries(mapping)) {
102
+ if (!source || /[\x00-\x1f]/.test(source)) fail(`Invalid native transfer ${kind} mapping`);
103
+ if (kind === 'key_results') {
104
+ if (!plain(target) || Object.entries(target).some(([key, id]) => !key || /[\x00-\x1f]/.test(key)
105
+ || typeof id !== 'string' || !id || /[\x00-\x1f]/.test(id))) fail('Invalid native transfer key_results mapping');
106
+ } else if (typeof target !== 'string' || !target || /[\x00-\x1f]/.test(target)
107
+ || (kind === 'task_uids' && !UUID.test(target))) fail(`Invalid native transfer ${kind} mapping`);
108
+ }
109
+ }
110
+ if (!plain(value.tasks)) fail('Native transfer id_map requires tasks');
111
+ return value;
112
+ }
113
+ module.exports = { MAX_TRANSFER_BYTES, DIGEST, readArchive, archive, outputPath, writePrivate, resultData, idMap };
package/lib/update.js ADDED
@@ -0,0 +1,86 @@
1
+ 'use strict';
2
+ const fs = require('node:fs/promises');
3
+ const path = require('node:path');
4
+ const os = require('node:os');
5
+ const { execFile } = require('node:child_process');
6
+ const { fail, readJson } = require('./common');
7
+ const { platformTarget, locations, releaseSpec, ensureInstalled, stageReporter } = require('./install');
8
+
9
+ const REGISTRY = '--registry=https://registry.npmjs.org/';
10
+ const VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
11
+ const NEXT = 'Running owners, Seats and Phone were not switched or stopped; their versions were not inspected. Next run workbite status --json and workbite doctor --json. Before workbite open, ask the operator to identify the exact owners/backend/host/boot and account for active Seats, preserve private records, then perform the authenticated component-specific handover. See docs/STARTUP-TROUBLESHOOTING.md#operator-handover-and-preserved-evidence (ask your maintainer for this guide if unavailable). Never clear locks or unknown intents; closing the UI leaves backends running.';
12
+ function compare(a, b) {
13
+ if (typeof a !== 'string' || typeof b !== 'string' || !VERSION.test(a) || !VERSION.test(b)) fail('Update requires canonical stable versions (major.minor.patch).');
14
+ const left = a.split('.').map(BigInt), right = b.split('.').map(BigInt);
15
+ for (let i = 0; i < 3; i++) if (left[i] !== right[i]) return left[i] > right[i] ? 1 : -1;
16
+ return 0;
17
+ }
18
+
19
+ // Only fixed words and validated stable versions enter cmd.exe; paths/output never do.
20
+ // npm.cmd needs cmd.exe on Windows; never pass it directly to execFile.
21
+ async function npm(args, { platform, environment, npmExecImpl = execFile }) {
22
+ if (args.some(value => !/^[a-zA-Z0-9@./:=_-]+$/.test(value))) fail('Unsafe internal npm argument');
23
+ const windows = platform === 'win32';
24
+ const command = windows ? path.join(environment.SystemRoot || 'C:\\Windows', 'System32', 'cmd.exe') : 'npm';
25
+ const argv = windows ? ['/d', '/s', '/c', `npm.cmd ${args.join(' ')}`] : args;
26
+ return new Promise((resolve, reject) => {
27
+ npmExecImpl(command, argv, { cwd: os.tmpdir(), env: environment, shell: false, windowsHide: true,
28
+ timeout: args[0] === 'install' ? 300000 : 30000, killSignal: 'SIGKILL', maxBuffer: 64 * 1024,
29
+ encoding: 'utf8' }, (error, stdout) => {
30
+ if (error) reject(new Error(`npm ${args[0]} failed or timed out. Check npm availability, permissions, network and certificate trust. No automatic retry was made.`));
31
+ else resolve(String(stdout).trim());
32
+ });
33
+ });
34
+ }
35
+
36
+ async function runUpdate(options = {}) {
37
+ const { environment = process.env, platform = process.platform, arch = process.arch,
38
+ stdout = process.stdout, stderr = process.stderr, launcherRoot = path.join(__dirname, '..') } = options;
39
+ platformTarget(platform, arch, options);
40
+ const run = args => npm(args, { ...options, environment, platform });
41
+ let latest;
42
+ try { latest = JSON.parse(await run(['view', 'workbite@latest', 'version', '--json', REGISTRY])); }
43
+ catch { fail('Could not resolve official npm workbite@latest. Check npm/network/certificate trust; nothing was installed.'); }
44
+ if (typeof latest !== 'string' || !VERSION.test(latest)) fail('Official npm latest is not a canonical stable version; nothing was installed.');
45
+ const current = (await readJson(path.join(launcherRoot, 'package.json'), 16384)).version;
46
+ if (compare(latest, current) < 0) fail(`Official latest ${latest} is older than this launcher ${current}; refusing downgrade.`);
47
+ if (latest === current) {
48
+ stdout.write(`WorkBite launcher ${current} is already official latest; no installation performed. Running runtime version was not inspected or changed.\n`);
49
+ return { code: 0, signal: null };
50
+ }
51
+ const globalRoot = await run(['root', '--global']);
52
+ if (!path.isAbsolute(globalRoot) || /[\r\n\x00]/.test(globalRoot)) fail('npm returned an invalid global root; nothing was installed.');
53
+ const destination = path.join(globalRoot, 'workbite');
54
+ let globalHome;
55
+ try { globalHome = await fs.realpath(destination); } catch { /* local/npx install */ }
56
+ if (!globalHome || globalHome !== await fs.realpath(launcherRoot)) {
57
+ fail(`This is a local/npx/source launcher. No global package was changed. To install globally, run npm install -g workbite@${latest} --registry=https://registry.npmjs.org/, then use that global workbite install and workbite check. Local project copies require an explicit project dependency update.`);
58
+ }
59
+ const oldManifest = await readJson(path.join(launcherRoot, 'release.json'));
60
+ const oldSpec = releaseSpec(oldManifest, platform, arch, options, current);
61
+ const apps = options.apps || locations(environment, platform).apps;
62
+ stderr.write(`Updating global launcher ${current} to ${latest}; running runtime remains unchanged.\n`);
63
+ try {
64
+ await run(['install', '--global', `workbite@${latest}`, REGISTRY, '--ignore-scripts', '--no-audit', '--no-fund', '--fetch-retries=0']);
65
+ } catch (error) {
66
+ fail(`${error.message} Global npm installation outcome may be partial or unknown (a child may still finish). Inspect npm ls -g workbite --depth=0 and workbite --version before any retry. ${NEXT}`);
67
+ }
68
+ try {
69
+ const installedPackage = await readJson(path.join(destination, 'package.json'), 16384);
70
+ if (installedPackage.name !== 'workbite' || installedPackage.version !== latest) fail('Global package does not match the requested version; possible concurrent update.');
71
+ const manifest = await readJson(path.join(destination, 'release.json'));
72
+ // Validate both pins, including launcher-only releases reusing an older native artifact.
73
+ for (const host of ['win32', 'linux']) releaseSpec(manifest, host, 'x64', { glibcVersionRuntime: '2.28' }, latest);
74
+ const spec = releaseSpec(manifest, platform, arch, options, latest);
75
+ if (compare(spec.version, oldSpec.version) < 0) fail('New launcher pins an older native suite; refusing native downgrade.');
76
+ const installed = await ensureInstalled(spec, { ...options, apps,
77
+ progress: options.progress || stageReporter(stderr) });
78
+ const finalPackage = await readJson(path.join(destination, 'package.json'), 16384);
79
+ if (finalPackage.name !== 'workbite' || finalPackage.version !== latest) fail('Global launcher changed during staging; inspect the concurrent update.');
80
+ stdout.write(`Installed global launcher ${latest}; verified native suite ${spec.version} staged at ${installed.home}.\n${NEXT}\n`);
81
+ return { code: 0, signal: null };
82
+ } catch (error) {
83
+ fail(`npm installation finished, but matching native staging/verification failed: ${error.message} Inspect workbite --version and npm ls -g workbite --depth=0. After resolving the cause, use the global workbite install then workbite check; do not open it yet. ${NEXT}`);
84
+ }
85
+ }
86
+ module.exports = { runUpdate, compare, npm };
package/package.json CHANGED
@@ -1,30 +1,35 @@
1
- {
2
- "name": "workbite",
3
- "version": "0.1.18",
4
- "description": "WorkBite (Work Stack + StackBite) verified native suite installer and launcher",
5
- "license": "UNLICENSED",
6
- "engines": {
7
- "node": ">=20"
8
- },
9
- "os": [
10
- "win32",
11
- "linux"
12
- ],
13
- "cpu": [
14
- "x64"
15
- ],
16
- "bin": {
17
- "workbite": "bin/workbite.js"
18
- },
19
- "files": [
20
- "bin/",
21
- "lib/",
22
- "skills/",
23
- "release.json",
24
- "README.md"
25
- ],
26
- "scripts": {
27
- "test": "node --test test/*.test.js"
28
- },
29
- "homepage": "https://github.com/Shinick-Han/WorkBite-releases"
30
- }
1
+ {
2
+ "name": "workbite",
3
+ "version": "0.1.20",
4
+ "description": "WorkBite (Work Stack + StackBite) verified native suite installer and launcher",
5
+ "license": "UNLICENSED",
6
+ "engines": {
7
+ "node": ">=20"
8
+ },
9
+ "os": [
10
+ "win32",
11
+ "linux"
12
+ ],
13
+ "cpu": [
14
+ "x64"
15
+ ],
16
+ "bin": {
17
+ "workbite": "bin/workbite.js"
18
+ },
19
+ "files": [
20
+ "bin/",
21
+ "lib/",
22
+ "skills/",
23
+ "release.json",
24
+ "README.md"
25
+ ],
26
+ "scripts": {
27
+ "test": "node --test test/*.test.js"
28
+ },
29
+ "homepage": "https://github.com/Shinick-Han/WorkBite-releases",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/Shinick-Han/WorkBite.git",
33
+ "directory": "packaging/npm"
34
+ }
35
+ }
package/release.json CHANGED
@@ -1,17 +1,24 @@
1
- {
2
- "launcherVersion": "0.1.18",
3
- "platforms": {
4
- "windows-x64": {
5
- "asset": "workbite-0.1.18-windows-x64.zip",
6
- "bundleSha256": "393ac3190fbea00abc3ac21cd5a11caa9e867633970d3cde5897aec99fea5314",
7
- "sha256": "805e65e6ea2f9818582c1b6122ecffff098cb7545ea34284145c59e75be6e53c",
8
- "size": 136269760,
9
- "url": "https://github.com/Shinick-Han/WorkBite-releases/releases/download/v0.1.18/workbite-0.1.18-windows-x64.zip"
10
- }
11
- },
12
- "product": "WorkBite",
13
- "repository": "Shinick-Han/WorkBite-releases",
14
- "schema": "workbite.release.v1",
15
- "tag": "v0.1.18",
16
- "version": "0.1.18"
17
- }
1
+ {
2
+ "launcherVersion": "0.1.20",
3
+ "platforms": {
4
+ "linux-x64": {
5
+ "asset": "workbite-0.1.20-linux-x64.tar.gz",
6
+ "bundleSha256": "6ed4e117291ce31035c6d9238687d8fe831980a73005d744d11c8dd26adc42b2",
7
+ "sha256": "888a9554fd40130384b0359193f908338536134cb130fb27a9f57886d7abe38e",
8
+ "size": 82456456,
9
+ "url": "https://github.com/Shinick-Han/WorkBite-releases/releases/download/v0.1.20/workbite-0.1.20-linux-x64.tar.gz"
10
+ },
11
+ "windows-x64": {
12
+ "asset": "workbite-0.1.20-windows-x64.zip",
13
+ "bundleSha256": "2fe008903450407bae8a966eb48cb8a8fc69942fe6d9739f115421693cbd8a53",
14
+ "sha256": "8a2b85cebed50cacf725d65db221b241c388639e1eebb26d36efaac90e8c7a9d",
15
+ "size": 136389662,
16
+ "url": "https://github.com/Shinick-Han/WorkBite-releases/releases/download/v0.1.20/workbite-0.1.20-windows-x64.zip"
17
+ }
18
+ },
19
+ "product": "WorkBite",
20
+ "repository": "Shinick-Han/WorkBite-releases",
21
+ "schema": "workbite.release.v1",
22
+ "tag": "v0.1.20",
23
+ "version": "0.1.20"
24
+ }