workbite 0.1.3 → 0.1.5
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/README.md +15 -4
- package/lib/cli.js +12 -4
- package/lib/install.js +15 -5
- package/lib/private-state.js +116 -0
- package/lib/runtime.js +165 -16
- package/package.json +1 -1
- package/release.json +13 -13
package/README.md
CHANGED
|
@@ -3,11 +3,20 @@
|
|
|
3
3
|
WorkBite installs and opens **Work Stack** (organizes the work) and
|
|
4
4
|
**StackBite** (gets it moving) as one native suite.
|
|
5
5
|
|
|
6
|
+
Launcher 0.1.5 (native bundle 0.1.4, unchanged) opens the familiar TypeScript
|
|
7
|
+
Work Stack UI by default. The unified Rust UI, where you select a Task, start
|
|
8
|
+
its agent and use `9 Execution` to see and control its native terminal in the
|
|
9
|
+
same window, is opt-in per open: `workbite open --frontend rust` (or bare
|
|
10
|
+
`workbite --frontend rust`). The TypeScript UI does not yet embed `9 Execution`.
|
|
11
|
+
Either frontend reuses the same owner and StackBite backend; the choice is not
|
|
12
|
+
saved. Task completion stays an explicit action.
|
|
13
|
+
|
|
6
14
|
```sh
|
|
7
15
|
npm install -g workbite
|
|
8
16
|
workbite install
|
|
9
17
|
workbite open --workspace /abs/path/new-folder --init # first time, once
|
|
10
|
-
workbite # afterwards
|
|
18
|
+
workbite # afterwards (TypeScript UI)
|
|
19
|
+
workbite --frontend rust # unified Rust UI with 9 Execution
|
|
11
20
|
```
|
|
12
21
|
|
|
13
22
|
Node.js 20+ on Windows x64 or Linux x64 (glibc 2.28+).
|
|
@@ -17,9 +26,11 @@ Node.js 20+ on Windows x64 or Linux x64 (glibc 2.28+).
|
|
|
17
26
|
every file listed in its `bundle.json`, and installs it into an immutable
|
|
18
27
|
per-user version folder. No auto-update, no install scripts, no dependencies.
|
|
19
28
|
- `workbite check` re-verifies the installed bundle without downloading.
|
|
20
|
-
- `workbite open --workspace ABS [--init] [--no-ui]
|
|
21
|
-
workspace's Work Stack owner and a StackBite backend
|
|
22
|
-
prove it started), then runs the Work Stack
|
|
29
|
+
- `workbite open --workspace ABS [--init] [--no-ui] [--frontend typescript|rust]`
|
|
30
|
+
starts or reuses the workspace's Work Stack owner and a StackBite backend
|
|
31
|
+
(only processes it can prove it started), then runs the TypeScript Work Stack
|
|
32
|
+
UI, or the unified Rust UI with `--frontend rust` (0.1.4 execution bundle
|
|
33
|
+
only; an older bundle refuses it before starting anything). Backends keep
|
|
23
34
|
running after the UI exits.
|
|
24
35
|
- `workbite` (or `workbite open` without `--workspace`) reopens the last
|
|
25
36
|
workspace that opened successfully; an explicit `--workspace` always wins.
|
package/lib/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
const path = require('node:path');
|
|
3
3
|
const { fail, readSmall } = require('./common');
|
|
4
4
|
const { locations, releaseSpec, ensureInstalled, stageReporter, packageVersion } = require('./install');
|
|
5
|
-
const { openSuite } = require('./runtime');
|
|
5
|
+
const { openSuite, FRONTENDS } = require('./runtime');
|
|
6
6
|
const { bindAgent, executeAgent, agentCapabilities, WRITE_COMMANDS, MAX_PACKET_BYTES } = require('./agent');
|
|
7
7
|
|
|
8
8
|
const HELP = `WorkBite ${packageVersion} - Work Stack organizes the work. StackBite gets it moving.
|
|
@@ -10,10 +10,11 @@ const HELP = `WorkBite ${packageVersion} - Work Stack organizes the work. StackB
|
|
|
10
10
|
Usage:
|
|
11
11
|
workbite install download, verify and install the pinned native suite
|
|
12
12
|
workbite check verify the installed suite (no download, no processes)
|
|
13
|
-
workbite open --workspace ABS [--init] [--no-ui]
|
|
13
|
+
workbite open --workspace ABS [--init] [--no-ui] [--frontend typescript|rust]
|
|
14
14
|
start or reuse this workspace's Work Stack owner and the
|
|
15
15
|
StackBite backend, then run the Work Stack terminal UI
|
|
16
|
-
workbite [open] [--no-ui]
|
|
16
|
+
workbite [open] [--no-ui] [--frontend typescript|rust]
|
|
17
|
+
reopen the last workspace WorkBite opened successfully
|
|
17
18
|
workbite agent [--workspace ABS --workspace-uid UUID] COMMAND [--input-file FILE | --stdin] [flags]
|
|
18
19
|
curated Work Stack agent commands against the running,
|
|
19
20
|
WorkBite-owned owner (never starts one): capabilities, status,
|
|
@@ -32,6 +33,8 @@ open:
|
|
|
32
33
|
--workspace ABS absolute workspace folder; always wins over the remembered one
|
|
33
34
|
--init create a new Work Stack workspace in a new or empty folder (needs --workspace)
|
|
34
35
|
--no-ui start or reuse the backends, print one JSON summary and exit
|
|
36
|
+
--frontend NAME typescript (default): the familiar Work Stack UI; rust: the unified UI with
|
|
37
|
+
9 Execution (execution bundles only). Chosen per open, never saved.
|
|
35
38
|
|
|
36
39
|
First time, name the workspace once:
|
|
37
40
|
workbite open --workspace /abs/path/new-folder --init
|
|
@@ -148,6 +151,11 @@ function parseArguments(argv) {
|
|
|
148
151
|
else if (argument === '--no-ui' && options.ui) options.ui = false;
|
|
149
152
|
else if (argument === '--workspace' && options.workspace === undefined && rest[index + 1] !== undefined
|
|
150
153
|
&& !rest[index + 1].startsWith('--')) options.workspace = rest[++index];
|
|
154
|
+
else if (argument === '--frontend' && options.frontend === undefined) {
|
|
155
|
+
const name = rest[++index];
|
|
156
|
+
if (!FRONTENDS.includes(name)) fail(`--frontend must be one of: ${FRONTENDS.join(', ')}`);
|
|
157
|
+
options.frontend = name;
|
|
158
|
+
}
|
|
151
159
|
else fail(`Unknown or repeated open option: ${String(argument).slice(0, 64)}. See workbite --help.`);
|
|
152
160
|
}
|
|
153
161
|
// Without --workspace the remembered workspace is reopened; --init never picks a folder by itself.
|
|
@@ -237,7 +245,7 @@ async function main(argv = process.argv.slice(2), options = {}) {
|
|
|
237
245
|
return { code: 0, signal: null };
|
|
238
246
|
}
|
|
239
247
|
return openSuite({ installed, commands: options.commands, workspace: parsed.workspace, init: parsed.init, ui: parsed.ui,
|
|
240
|
-
stateRoot, environment, spawnImpl: options.spawnImpl, stdout, timeouts: options.timeouts });
|
|
248
|
+
frontend: parsed.frontend, stateRoot, environment, spawnImpl: options.spawnImpl, execImpl: options.execImpl, stdout, timeouts: options.timeouts });
|
|
241
249
|
}
|
|
242
250
|
|
|
243
251
|
module.exports = { main, parseArguments, HELP };
|
package/lib/install.js
CHANGED
|
@@ -15,6 +15,14 @@ const EXECUTABLES = Object.freeze({
|
|
|
15
15
|
workstack_tui: 'workstack/workstack-tui',
|
|
16
16
|
stackbite: 'stackbite/stackbite',
|
|
17
17
|
});
|
|
18
|
+
// Exact executable roster per bundle schema: v1 keeps the four legacy entries; v2 adds only the
|
|
19
|
+
// same-window execution UI (proprietary assembly binary workstack-native-tui). Any other schema is refused.
|
|
20
|
+
const BUNDLE_V1 = 'workbite.bundle.v1';
|
|
21
|
+
const BUNDLE_V2 = 'workbite.bundle.v2';
|
|
22
|
+
const ROSTERS = Object.freeze({
|
|
23
|
+
[BUNDLE_V1]: EXECUTABLES,
|
|
24
|
+
[BUNDLE_V2]: Object.freeze({ ...EXECUTABLES, workbite_tui: 'workstack/workstack-native-tui' }),
|
|
25
|
+
});
|
|
18
26
|
|
|
19
27
|
function absoluteOverride(environment, name) {
|
|
20
28
|
const value = environment[name];
|
|
@@ -75,9 +83,10 @@ function releaseSpec(manifest, platform = process.platform, arch = process.arch,
|
|
|
75
83
|
platform, target, version: manifest.version };
|
|
76
84
|
}
|
|
77
85
|
|
|
78
|
-
function expectedExecutables(spec) {
|
|
86
|
+
function expectedExecutables(spec, schema = BUNDLE_V1) {
|
|
87
|
+
if (!Object.hasOwn(ROSTERS, schema)) fail('bundle.json identity or executable layout mismatch');
|
|
79
88
|
const suffix = spec.platform === 'win32' ? '.exe' : '';
|
|
80
|
-
return Object.fromEntries(Object.entries(
|
|
89
|
+
return Object.fromEntries(Object.entries(ROSTERS[schema]).map(([name, file]) => [name, file + suffix]));
|
|
81
90
|
}
|
|
82
91
|
function plainObject(value) { return value !== null && typeof value === 'object' && !Array.isArray(value); }
|
|
83
92
|
function sameKeys(value, keys) {
|
|
@@ -93,8 +102,9 @@ async function validateBundle(directory, spec) {
|
|
|
93
102
|
if (sha256(bundleBytes) !== spec.bundleSha256) fail('Pinned bundle.json SHA-256 mismatch');
|
|
94
103
|
let bundle;
|
|
95
104
|
try { bundle = JSON.parse(bundleBytes.toString('utf8')); } catch { fail('bundle.json is not valid JSON'); }
|
|
96
|
-
|
|
97
|
-
|
|
105
|
+
if (!plainObject(bundle) || !Object.hasOwn(ROSTERS, bundle.schema)) fail('bundle.json identity or executable layout mismatch');
|
|
106
|
+
const executables = expectedExecutables(spec, bundle.schema);
|
|
107
|
+
if (bundle.product !== 'WorkBite'
|
|
98
108
|
|| bundle.version !== spec.version || bundle.target !== spec.target || !plainObject(bundle.components)
|
|
99
109
|
|| !sameKeys(bundle.executables, Object.keys(executables))
|
|
100
110
|
|| Object.entries(executables).some(([name, file]) => bundle.executables[name] !== file)
|
|
@@ -220,5 +230,5 @@ function stageReporter(stderr) {
|
|
|
220
230
|
return stage => stderr.write(`WorkBite: ${messages[stage]}\n`);
|
|
221
231
|
}
|
|
222
232
|
|
|
223
|
-
module.exports = { REPOSITORY, TARGETS, EXECUTABLES, locations, platformTarget, releaseSpec, expectedExecutables,
|
|
233
|
+
module.exports = { REPOSITORY, TARGETS, EXECUTABLES, BUNDLE_V1, BUNDLE_V2, ROSTERS, locations, platformTarget, releaseSpec, expectedExecutables,
|
|
224
234
|
validateBundle, ensureInstalled, installName, stageReporter, packageVersion };
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Private Windows state for the v2 execution UI: the native TUI intent directory and the
|
|
3
|
+
// deploy journal must be owned by the current user and grant mutating access only to that
|
|
4
|
+
// user, SYSTEM and Administrators (the Work Stack TUI's private-parent check), but a folder
|
|
5
|
+
// created under %LOCALAPPDATA% inherits whatever its parents grant (sandbox groups included).
|
|
6
|
+
// One bounded Windows PowerShell child replaces exactly the named directories' DACLs with a
|
|
7
|
+
// protected private one; owner, contents and file identities are kept. Linux keeps 0700.
|
|
8
|
+
const path = require('node:path');
|
|
9
|
+
const { execFile } = require('node:child_process');
|
|
10
|
+
const { fail } = require('./common');
|
|
11
|
+
|
|
12
|
+
const RESULT_SCHEMA = 'workbite.private-directories.v1';
|
|
13
|
+
const MAXIMUM = 8;
|
|
14
|
+
// Constant script, no interpolation: paths arrive only as WORKBITE_PRIVATE_DIRECTORY_<n>.
|
|
15
|
+
// .NET types only (no cmdlets), so a foreign PSModulePath cannot change what runs.
|
|
16
|
+
const SCRIPT = String.raw`
|
|
17
|
+
$ErrorActionPreference = 'Stop'
|
|
18
|
+
$ProgressPreference = 'SilentlyContinue'
|
|
19
|
+
try {
|
|
20
|
+
$count = [int]$env:WORKBITE_PRIVATE_DIRECTORY_COUNT
|
|
21
|
+
if ($count -lt 1 -or $count -gt ${MAXIMUM}) { throw 'invalid directory count' }
|
|
22
|
+
$me = [System.Security.Principal.WindowsIdentity]::GetCurrent().User
|
|
23
|
+
$allowed = @($me.Value, 'S-1-5-18', 'S-1-5-32-544')
|
|
24
|
+
$sid = [System.Security.Principal.SecurityIdentifier]
|
|
25
|
+
$sections = [System.Security.AccessControl.AccessControlSections]'Access, Owner'
|
|
26
|
+
$results = @()
|
|
27
|
+
for ($index = 0; $index -lt $count; $index++) {
|
|
28
|
+
$target = [Environment]::GetEnvironmentVariable('WORKBITE_PRIVATE_DIRECTORY_' + $index)
|
|
29
|
+
if (-not $target -or -not [System.IO.Path]::IsPathRooted($target)) { throw ('directory ' + $index + ' is not an absolute path') }
|
|
30
|
+
$info = [System.IO.DirectoryInfo]::new($target)
|
|
31
|
+
$info.Refresh()
|
|
32
|
+
if (-not $info.Exists) { throw ('directory ' + $index + ' is missing or not a directory') }
|
|
33
|
+
if (($info.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) { throw ('directory ' + $index + ' is a link or reparse point') }
|
|
34
|
+
$current = $info.GetAccessControl($sections)
|
|
35
|
+
if ($current.GetOwner($sid).Value -ne $me.Value) { throw ('directory ' + $index + ' is not owned by the current Windows user') }
|
|
36
|
+
$rules = @($current.GetAccessRules($true, $true, $sid))
|
|
37
|
+
$private = $current.AreAccessRulesProtected -and $rules.Count -gt 0
|
|
38
|
+
$mine = $false
|
|
39
|
+
foreach ($rule in $rules) {
|
|
40
|
+
if ($rule.IsInherited -or $rule.AccessControlType -ne 'Allow' -or $allowed -notcontains $rule.IdentityReference.Value) { $private = $false }
|
|
41
|
+
if ($rule.IdentityReference.Value -eq $me.Value -and ($rule.FileSystemRights -band [System.Security.AccessControl.FileSystemRights]::FullControl) -eq [System.Security.AccessControl.FileSystemRights]::FullControl) { $mine = $true }
|
|
42
|
+
}
|
|
43
|
+
if ($private -and $mine) { $results += 'kept'; continue }
|
|
44
|
+
$security = [System.Security.AccessControl.DirectorySecurity]::new()
|
|
45
|
+
$security.SetAccessRuleProtection($true, $false)
|
|
46
|
+
foreach ($principal in $allowed) {
|
|
47
|
+
$security.AddAccessRule([System.Security.AccessControl.FileSystemAccessRule]::new($sid::new($principal),
|
|
48
|
+
[System.Security.AccessControl.FileSystemRights]::FullControl,
|
|
49
|
+
[System.Security.AccessControl.InheritanceFlags]'ContainerInherit, ObjectInherit',
|
|
50
|
+
[System.Security.AccessControl.PropagationFlags]::None, [System.Security.AccessControl.AccessControlType]::Allow))
|
|
51
|
+
}
|
|
52
|
+
# Only the DACL section is written; the owner is left as verified above.
|
|
53
|
+
$info.SetAccessControl($security)
|
|
54
|
+
$info.Refresh()
|
|
55
|
+
if (($info.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) { throw ('directory ' + $index + ' changed while protecting it') }
|
|
56
|
+
$after = $info.GetAccessControl($sections)
|
|
57
|
+
if ($after.GetOwner($sid).Value -ne $me.Value -or -not $after.AreAccessRulesProtected) { throw ('directory ' + $index + ' did not keep a protected private DACL') }
|
|
58
|
+
foreach ($rule in @($after.GetAccessRules($true, $true, $sid))) {
|
|
59
|
+
if ($rule.IsInherited -or $rule.AccessControlType -ne 'Allow' -or $allowed -notcontains $rule.IdentityReference.Value) { throw ('directory ' + $index + ' still grants another principal access') }
|
|
60
|
+
}
|
|
61
|
+
$results += 'protected'
|
|
62
|
+
}
|
|
63
|
+
[Console]::Out.Write('{"schema":"${RESULT_SCHEMA}","results":["' + ($results -join '","') + '"]}')
|
|
64
|
+
exit 0
|
|
65
|
+
} catch {
|
|
66
|
+
$message = ($_.Exception.Message -replace '[\x00-\x1f\x7f]', ' ')
|
|
67
|
+
if ($message.Length -gt 300) { $message = $message.Substring(0, 300) }
|
|
68
|
+
[Console]::Error.Write($message)
|
|
69
|
+
exit 3
|
|
70
|
+
}
|
|
71
|
+
`;
|
|
72
|
+
const ENCODED = Buffer.from(SCRIPT, 'utf16le').toString('base64');
|
|
73
|
+
|
|
74
|
+
function windowsPowerShell(environment) {
|
|
75
|
+
const root = environment.SystemRoot || environment.SYSTEMROOT || environment.windir || 'C:\\Windows';
|
|
76
|
+
if (!/^[A-Za-z]:\\[^\x00-\x1f"*?<>|]*$/.test(root)) fail('Cannot locate Windows PowerShell: SystemRoot is not an absolute local path.');
|
|
77
|
+
return path.win32.join(root, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
|
|
78
|
+
}
|
|
79
|
+
const bounded = text => String(text || '').replace(/[\x00-\x1f\x7f]/g, ' ').trim().slice(0, 300);
|
|
80
|
+
|
|
81
|
+
// Windows only: each directory must already exist (created by safeDirectory). Returns 'kept'
|
|
82
|
+
// or 'protected' per directory; anything else refuses and nothing that uses them is started.
|
|
83
|
+
// No-op elsewhere: Linux directories are created 0700 and checked by the native TUI itself.
|
|
84
|
+
async function protectPrivateDirectories(directories, { platform = process.platform, environment = process.env,
|
|
85
|
+
execImpl = execFile, timeout = 60000 } = {}) {
|
|
86
|
+
if (platform !== 'win32') return null;
|
|
87
|
+
if (!Array.isArray(directories) || !directories.length || directories.length > MAXIMUM
|
|
88
|
+
|| directories.some(value => typeof value !== 'string' || !path.win32.isAbsolute(value) || /^[\\/]{2}|[\x00-\x1f]/.test(value))) {
|
|
89
|
+
fail('WorkBite private state directories must be absolute local paths.');
|
|
90
|
+
}
|
|
91
|
+
const childEnvironment = {};
|
|
92
|
+
for (const [key, value] of Object.entries(environment)) {
|
|
93
|
+
// Windows PowerShell must not load another PowerShell's modules; earlier helper values are replaced.
|
|
94
|
+
if (value !== undefined && key.toUpperCase() !== 'PSMODULEPATH' && !/^WORKBITE_PRIVATE_DIRECTORY_/i.test(key)) childEnvironment[key] = value;
|
|
95
|
+
}
|
|
96
|
+
childEnvironment.WORKBITE_PRIVATE_DIRECTORY_COUNT = String(directories.length);
|
|
97
|
+
directories.forEach((value, index) => { childEnvironment[`WORKBITE_PRIVATE_DIRECTORY_${index}`] = path.win32.resolve(value); });
|
|
98
|
+
const file = windowsPowerShell(environment);
|
|
99
|
+
const result = await new Promise(resolve => {
|
|
100
|
+
execImpl(file, ['-NoLogo', '-NoProfile', '-NonInteractive', '-EncodedCommand', ENCODED],
|
|
101
|
+
{ env: childEnvironment, shell: false, windowsHide: true, timeout, maxBuffer: 64 * 1024, encoding: 'utf8' },
|
|
102
|
+
(error, stdout, stderr) => resolve({ error, stdout: stdout || '', stderr: stderr || '' }));
|
|
103
|
+
});
|
|
104
|
+
let record = null;
|
|
105
|
+
try { record = JSON.parse(result.stdout.trim()); } catch { record = null; }
|
|
106
|
+
if (result.error || record === null || typeof record !== 'object' || Array.isArray(record)
|
|
107
|
+
|| Object.keys(record).length !== 2 || record.schema !== RESULT_SCHEMA || !Array.isArray(record.results)
|
|
108
|
+
|| record.results.length !== directories.length || record.results.some(value => value !== 'kept' && value !== 'protected')) {
|
|
109
|
+
fail(`WorkBite could not make its private execution state private (${bounded(result.stderr) || 'no result'}); `
|
|
110
|
+
+ `nothing that uses ${directories.join(', ')} was started. Each must be a real directory owned by the current `
|
|
111
|
+
+ 'Windows user; WorkBite does not take ownership or follow links.');
|
|
112
|
+
}
|
|
113
|
+
return record.results;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
module.exports = { protectPrivateDirectories, RESULT_SCHEMA };
|
package/lib/runtime.js
CHANGED
|
@@ -12,12 +12,16 @@ const path = require('node:path');
|
|
|
12
12
|
const { spawn, execFile } = require('node:child_process');
|
|
13
13
|
const { randomBytes, randomUUID } = require('node:crypto');
|
|
14
14
|
const { fail, exists, safeDirectory, writeAtomic, readJson, readSmall, sha256, UUID, BACKEND_ID } = require('./common');
|
|
15
|
+
const { BUNDLE_V2 } = require('./install');
|
|
16
|
+
const { protectPrivateDirectories } = require('./private-state');
|
|
15
17
|
|
|
16
18
|
const BACKEND_SCHEMA = 'stackbite.workstack-backend.v1';
|
|
17
19
|
const CONNECTION_SCHEMA = 'workstack.helm-connection.v1';
|
|
18
20
|
const OWNER_SCHEMA = 'workbite.owner.v2';
|
|
19
21
|
const STACKBITE_SCHEMA = 'workbite.stackbite-backend.v2';
|
|
20
22
|
const SELECTION_SCHEMA = 'workbite.workspace-selection.v1';
|
|
23
|
+
// Foreground UI per open: the familiar TypeScript Work Stack UI by default, the unified Rust UI on request.
|
|
24
|
+
const FRONTENDS = Object.freeze(['typescript', 'rust']);
|
|
21
25
|
const LOOPBACK = /^http:\/\/127\.0\.0\.1:([1-9][0-9]{0,4})$/;
|
|
22
26
|
// Ports of the existing official StackBite and its previews are never chosen.
|
|
23
27
|
const RESERVED_PORTS = new Set([8089, 8090, 8091, 54036, 54039, 54236, 54239]);
|
|
@@ -48,7 +52,11 @@ function stateLayout(root) {
|
|
|
48
52
|
journal: path.join(root, 'deploy-journal'), tuiState: path.join(root, 'workstack-tui'), owners: path.join(root, 'owners'),
|
|
49
53
|
logs: path.join(root, 'logs'), dock: path.join(root, 'execution-dock.json'),
|
|
50
54
|
stackbiteRecord: path.join(root, 'stackbite-backend.json'), lock: path.join(root, 'open.lock'),
|
|
51
|
-
selection: path.join(root, 'last-workspace.json')
|
|
55
|
+
selection: path.join(root, 'last-workspace.json'),
|
|
56
|
+
// v2 execution UI only: its private intent journal and device settings, never under a workspace.
|
|
57
|
+
nativeTui: path.join(root, 'workstack-native-tui'),
|
|
58
|
+
tuiIntent: path.join(root, 'workstack-native-tui', 'intent.json'),
|
|
59
|
+
tuiSettings: path.join(root, 'workstack-native-tui', 'tui.json') };
|
|
52
60
|
}
|
|
53
61
|
async function prepareState(layout) {
|
|
54
62
|
for (const directory of [layout.root, layout.stackbite, layout.workstackRuntime, layout.journal, layout.tuiState,
|
|
@@ -292,7 +300,29 @@ const isRecord = value => value !== null && typeof value === 'object' && !Array.
|
|
|
292
300
|
&& (value.state === 'starting' || value.state === 'ready') && typeof value.launch === 'string' && LAUNCH.test(value.launch);
|
|
293
301
|
const recordBytes = value => Buffer.from(JSON.stringify(value, null, 2) + '\n');
|
|
294
302
|
|
|
295
|
-
|
|
303
|
+
// A v2 (execution UI) bundle reuses only processes its own startup intent marked: the owner needs
|
|
304
|
+
// this bundle's execution commands, the StackBite backend its native launch opt-in (read once at
|
|
305
|
+
// startup). A matching token or UID alone is not that proof. Checked from the record before any
|
|
306
|
+
// network contact; the record is kept and nothing is stopped. A v1 bundle never checks or writes it.
|
|
307
|
+
const HANDOVER = 'Operator handover needed: WorkBite never stops, adopts or replaces it. Close the UIs using it, '
|
|
308
|
+
+ 'stop that process yourself (its pid is in the record), move the record aside and open again; WorkBite then '
|
|
309
|
+
+ 'starts a new one from this bundle.';
|
|
310
|
+
const shown = value => (typeof value === 'string' ? JSON.stringify(value.slice(0, 40)) : value === undefined ? 'absent' : 'invalid');
|
|
311
|
+
function requireExecutionMarker(saved, execution, recordPath, what, extra = {}) {
|
|
312
|
+
if (!execution) return;
|
|
313
|
+
const missing = saved.execution_bundle_version !== execution.bundleVersion
|
|
314
|
+
|| Object.entries(extra).some(([key, value]) => saved[key] !== value);
|
|
315
|
+
if (missing) {
|
|
316
|
+
fail(`The ${what} recorded in ${recordPath} was not started by the WorkBite ${execution.bundleVersion} execution bundle `
|
|
317
|
+
+ `(execution_bundle_version ${shown(saved.execution_bundle_version)}`
|
|
318
|
+
+ Object.keys(extra).map(key => `, ${key} ${saved[key] === undefined ? 'absent' : JSON.stringify(saved[key])}`).join('')
|
|
319
|
+
+ `), so it cannot serve 9 Execution. ${HANDOVER}`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
const executionFields = (execution, extra = {}) => (execution ? { execution_bundle_version: execution.bundleVersion, ...extra } : {});
|
|
323
|
+
const NATIVE_LAUNCH = { native_launch: true };
|
|
324
|
+
|
|
325
|
+
async function ensureEngine({ commands, workspace, uid, layout, environment, spawnImpl, timeout = 60000, execution = null }) {
|
|
296
326
|
const key = workspaceKey(workspace);
|
|
297
327
|
const recordPath = path.join(layout.owners, `${key}.json`);
|
|
298
328
|
const logFor = launch => path.join(layout.logs, `workstack-engine-${key}-${launch}.log`);
|
|
@@ -303,6 +333,7 @@ async function ensureEngine({ commands, workspace, uid, layout, environment, spa
|
|
|
303
333
|
|| typeof saved.token !== 'string' || !/^[0-9a-f]{64}$/.test(saved.token)) {
|
|
304
334
|
fail(`The Work Stack owner record ${recordPath} is invalid for this workspace; WorkBite does not replace it. ${RECOVERY}`);
|
|
305
335
|
}
|
|
336
|
+
requireExecutionMarker(saved, execution, recordPath, 'Work Stack owner');
|
|
306
337
|
let endpoint = saved.state === 'ready' ? saved.endpoint : null;
|
|
307
338
|
if (saved.state === 'starting') {
|
|
308
339
|
const ready = await readyRecord(logFor(saved.launch));
|
|
@@ -315,7 +346,7 @@ async function ensureEngine({ commands, workspace, uid, layout, environment, spa
|
|
|
315
346
|
return { endpoint, reused: true, pid: saved.pid };
|
|
316
347
|
}
|
|
317
348
|
const intent = { schema: OWNER_SCHEMA, state: 'starting', workspace, workspace_uid: uid,
|
|
318
|
-
launch: randomBytes(8).toString('hex'), token: randomBytes(32).toString('hex') };
|
|
349
|
+
launch: randomBytes(8).toString('hex'), token: randomBytes(32).toString('hex'), ...executionFields(execution) };
|
|
319
350
|
const log = logFor(intent.launch);
|
|
320
351
|
await writeAtomic(recordPath, recordBytes(intent));
|
|
321
352
|
const { child, exited } = spawnDetached(commands.workstack_engine,
|
|
@@ -329,7 +360,7 @@ async function ensureEngine({ commands, workspace, uid, layout, environment, spa
|
|
|
329
360
|
return { endpoint: ready.endpoint, reused: false, pid: child.pid };
|
|
330
361
|
}
|
|
331
362
|
|
|
332
|
-
async function ensureStackbite({ commands, layout, backendId, environment, spawnImpl, timeout = 120000 }) {
|
|
363
|
+
async function ensureStackbite({ commands, layout, backendId, environment, spawnImpl, timeout = 120000, execution = null }) {
|
|
333
364
|
const recordPath = layout.stackbiteRecord;
|
|
334
365
|
const logFor = launch => path.join(layout.logs, `stackbite-backend-${launch}.log`);
|
|
335
366
|
const saved = await savedRecord(recordPath, 'StackBite backend');
|
|
@@ -338,6 +369,7 @@ async function ensureStackbite({ commands, layout, backendId, environment, spawn
|
|
|
338
369
|
|| typeof saved.url !== 'string' || LOOPBACK.exec(saved.url)?.[1] !== String(saved.port)) {
|
|
339
370
|
fail(`The StackBite backend record ${recordPath} is invalid for this state; WorkBite does not replace it. ${RECOVERY}`);
|
|
340
371
|
}
|
|
372
|
+
requireExecutionMarker(saved, execution, recordPath, 'StackBite backend', NATIVE_LAUNCH);
|
|
341
373
|
if (!await stackbiteIdentity(saved.url, layout.stackbite, backendId)) {
|
|
342
374
|
fail(`The StackBite backend recorded in ${recordPath} could not be authenticated (still starting, or stopped). ${RECOVERY}`);
|
|
343
375
|
}
|
|
@@ -346,17 +378,21 @@ async function ensureStackbite({ commands, layout, backendId, environment, spawn
|
|
|
346
378
|
}
|
|
347
379
|
const port = await freePort();
|
|
348
380
|
const url = `http://127.0.0.1:${port}`;
|
|
349
|
-
const intent = { schema: STACKBITE_SCHEMA, state: 'starting', backend_id: backendId, launch: randomBytes(8).toString('hex'), url, port
|
|
381
|
+
const intent = { schema: STACKBITE_SCHEMA, state: 'starting', backend_id: backendId, launch: randomBytes(8).toString('hex'), url, port,
|
|
382
|
+
...executionFields(execution, NATIVE_LAUNCH) };
|
|
350
383
|
const log = logFor(intent.launch);
|
|
351
384
|
const home = path.dirname(commands.stackbite[commands.stackbite.length - 1]);
|
|
352
385
|
await writeAtomic(recordPath, recordBytes(intent));
|
|
353
386
|
// The private Python terminal core and this backend's own loopback bridge are
|
|
354
387
|
// mandatory for the room backend (taskrooms reads STACKBITE_BRIDGE_URL; the agent
|
|
355
388
|
// CLI and frontend read STACKBITE_ROOM_BRIDGE_URL); caller values are overridden here only.
|
|
389
|
+
// A v2 bundle also starts it with the WorkBite native launch opt-in (cleanEnvironment drops any
|
|
390
|
+
// caller value, so a v1 backend never gets it); STACKBITE_HOME lets it resolve the bundle's native assets.
|
|
356
391
|
const { child, exited } = spawnDetached(commands.stackbite, ['--room-backend', '--port', String(port)], {
|
|
357
392
|
env: withPath({ ...environment, STACKBITE_HOME: home, STACKBITE_STATE_DIR: layout.stackbite,
|
|
358
393
|
STACKBITE_PTY_BACKEND: 'private', STACKBITE_ROOM_BACKEND: '1', STACKBITE_TERMINAL_CORE: 'python',
|
|
359
|
-
STACKBITE_BRIDGE_URL: url, STACKBITE_ROOM_BRIDGE_URL: url, PYINSTALLER_RESET_ENVIRONMENT: '1'
|
|
394
|
+
STACKBITE_BRIDGE_URL: url, STACKBITE_ROOM_BRIDGE_URL: url, PYINSTALLER_RESET_ENVIRONMENT: '1',
|
|
395
|
+
...(execution ? { STACKBITE_WORKBITE_NATIVE_LAUNCH: '1' } : {}) }, home),
|
|
360
396
|
cwd: home, log, spawnImpl });
|
|
361
397
|
await writeAtomic(recordPath, recordBytes({ ...intent, pid: child.pid }));
|
|
362
398
|
await waitFor(() => stackbiteIdentity(url, layout.stackbite, backendId), exited, timeout, 'StackBite backend', log);
|
|
@@ -364,6 +400,36 @@ async function ensureStackbite({ commands, layout, backendId, environment, spawn
|
|
|
364
400
|
return { url, reused: false, pid: child.pid };
|
|
365
401
|
}
|
|
366
402
|
|
|
403
|
+
// v2 only, after this backend's identity is proven: one headless `stackbite --workbite-native-init`
|
|
404
|
+
// selects (or, through StackBite's own durable coordinator, reuses) the native terminal daemon for
|
|
405
|
+
// this backend. No UI, window, Room or Seat; exactly one exit-0 selected record is accepted.
|
|
406
|
+
// Anything else (refused, Unknown, malformed, timed out) refuses the open: WorkBite never retries,
|
|
407
|
+
// starts another daemon or falls back, and a later open asks the same coordinator again.
|
|
408
|
+
const NATIVE_INIT_SCHEMA = 'stackbite.workbite-native-init.v1';
|
|
409
|
+
async function initializeNativeRuntime({ commands, layout, url, environment, execImpl = execFile, timeout = 120000 }) {
|
|
410
|
+
const home = path.dirname(commands.stackbite[commands.stackbite.length - 1]);
|
|
411
|
+
const [file, ...prefix] = commands.stackbite;
|
|
412
|
+
const result = await new Promise(resolve => {
|
|
413
|
+
execImpl(file, [...prefix, '--workbite-native-init'], {
|
|
414
|
+
env: withPath({ ...environment, STACKBITE_HOME: home, STACKBITE_STATE_DIR: layout.stackbite,
|
|
415
|
+
STACKBITE_BRIDGE_URL: url, STACKBITE_ROOM_BRIDGE_URL: url, STACKBITE_WORKBITE_NATIVE_LAUNCH: '1',
|
|
416
|
+
PYINSTALLER_RESET_ENVIRONMENT: '1' }, home),
|
|
417
|
+
cwd: home, shell: false, windowsHide: true, timeout, maxBuffer: 64 * 1024, encoding: 'utf8',
|
|
418
|
+
}, (error, stdout, stderr) => resolve({ error, stdout: stdout || '', stderr: stderr || '' }));
|
|
419
|
+
});
|
|
420
|
+
const text = result.stdout.trim();
|
|
421
|
+
let record = null;
|
|
422
|
+
if (text && !/[\r\n]/.test(text) && text.length <= 4096) { try { record = JSON.parse(text); } catch { record = null; } }
|
|
423
|
+
if (result.error || record === null || typeof record !== 'object' || Array.isArray(record) || Object.keys(record).length !== 2
|
|
424
|
+
|| record.schema !== NATIVE_INIT_SCHEMA || record.status !== 'selected') {
|
|
425
|
+
const exit = result.error ? (result.error.killed ? 'timed out' : `exit ${result.error.code ?? 'unknown'}`) : 'no selected record';
|
|
426
|
+
fail(`StackBite native runtime initialization did not select a native runtime (${exit}: ${boundedText(result.stderr) || 'no reason'}); `
|
|
427
|
+
+ 'the execution UI was not started. WorkBite does not retry, start another daemon or fall back; '
|
|
428
|
+
+ 'if the outcome is unknown, open again later and StackBite looks the same selection up.');
|
|
429
|
+
}
|
|
430
|
+
return record.status;
|
|
431
|
+
}
|
|
432
|
+
|
|
367
433
|
async function writeDockConfig(layout, url) {
|
|
368
434
|
const bytes = Buffer.from(JSON.stringify({ schema: CONNECTION_SCHEMA, base_url: url, state_dir: layout.stackbite }, null, 2) + '\n');
|
|
369
435
|
let current = null;
|
|
@@ -397,12 +463,65 @@ async function withLock(file, action, { wait = 180000, stale = 300000 } = {}) {
|
|
|
397
463
|
}
|
|
398
464
|
}
|
|
399
465
|
|
|
400
|
-
|
|
466
|
+
// WB-EXEC-UI-V1 §4.4 device opt-in for 9 Execution, in WorkBite's own private settings file.
|
|
467
|
+
// Written only when no execution_ui choice exists; a present choice (enabled or disabled, any
|
|
468
|
+
// prefix) is kept byte for byte, and anything else there is refused, never repaired.
|
|
469
|
+
const EXECUTION_UI_SCHEMA = 'workbite.execution-ui-settings.v1';
|
|
470
|
+
const EXECUTION_UI_DEFAULT = Object.freeze({ schema: EXECUTION_UI_SCHEMA, enabled: true, prefix: 'ctrl+]', last_view_execution: false });
|
|
471
|
+
const SETTINGS_FIX = 'WorkBite does not repair or replace it; fix it or move it aside and open again.';
|
|
472
|
+
function validExecutionUi(value) {
|
|
473
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === 4
|
|
474
|
+
&& value.schema === EXECUTION_UI_SCHEMA && typeof value.enabled === 'boolean' && typeof value.prefix === 'string'
|
|
475
|
+
&& typeof value.last_view_execution === 'boolean';
|
|
476
|
+
}
|
|
477
|
+
// Same-directory temporary file published by hard link: never replaces a file another writer created.
|
|
478
|
+
async function publishNew(file, bytes) {
|
|
479
|
+
const temporary = path.join(path.dirname(file), `.${path.basename(file)}.${randomBytes(8).toString('hex')}.tmp`);
|
|
480
|
+
const handle = await fs.open(temporary, 'wx', 0o600);
|
|
481
|
+
try { await handle.writeFile(bytes); await handle.sync(); } finally { await handle.close(); }
|
|
482
|
+
try { await fs.link(temporary, file); return true; } catch (error) {
|
|
483
|
+
if (error.code !== 'EEXIST') throw error;
|
|
484
|
+
return false;
|
|
485
|
+
} finally { await fs.unlink(temporary); }
|
|
486
|
+
}
|
|
487
|
+
async function ensureExecutionSettings(file, attempt = 0) {
|
|
488
|
+
let bytes = null;
|
|
489
|
+
try { bytes = await readSmall(file, 1024 * 1024); } catch (error) {
|
|
490
|
+
if (error.code !== 'ENOENT') fail(`The execution UI settings ${file} are unreadable. ${SETTINGS_FIX}`);
|
|
491
|
+
}
|
|
492
|
+
if (bytes === null) {
|
|
493
|
+
const created = await publishNew(file, Buffer.from(JSON.stringify({ execution_ui: EXECUTION_UI_DEFAULT }, null, 2) + '\n'));
|
|
494
|
+
if (created) return 'initialized';
|
|
495
|
+
if (attempt) fail(`The execution UI settings ${file} keep changing while opening. ${SETTINGS_FIX}`);
|
|
496
|
+
return ensureExecutionSettings(file, attempt + 1);
|
|
497
|
+
}
|
|
498
|
+
let document;
|
|
499
|
+
try {
|
|
500
|
+
const text = bytes.toString('utf8');
|
|
501
|
+
document = JSON.parse(text.charCodeAt(0) === 0xfeff ? text.slice(1) : text);
|
|
502
|
+
} catch { fail(`The execution UI settings ${file} are not valid JSON. ${SETTINGS_FIX}`); }
|
|
503
|
+
if (document === null || typeof document !== 'object' || Array.isArray(document)) {
|
|
504
|
+
fail(`The execution UI settings ${file} are not a JSON object. ${SETTINGS_FIX}`);
|
|
505
|
+
}
|
|
506
|
+
if (!Object.hasOwn(document, 'execution_ui')) {
|
|
507
|
+
// Other settings are kept; only the missing opt-in is added.
|
|
508
|
+
await writeAtomic(file, Buffer.from(JSON.stringify({ ...document, execution_ui: EXECUTION_UI_DEFAULT }, null, 2) + '\n'));
|
|
509
|
+
return 'initialized';
|
|
510
|
+
}
|
|
511
|
+
if (!validExecutionUi(document.execution_ui)) {
|
|
512
|
+
fail(`The execution UI settings ${file} hold an execution_ui value that is not the exact four-key opt-in object `
|
|
513
|
+
+ `(schema "${EXECUTION_UI_SCHEMA}", enabled, prefix, last_view_execution). ${SETTINGS_FIX}`);
|
|
514
|
+
}
|
|
515
|
+
return document.execution_ui.enabled ? 'enabled' : 'disabled';
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function uiEnvironment(base, { executables, layout, endpoint, uid, backendId, execution = null }) {
|
|
401
519
|
const home = path.dirname(executables.stackbite);
|
|
402
520
|
return withPath({ ...base, WORKSTACK_TRANSPORT: 'native', WORKSTACK_NATIVE_CLIENT: executables.workstack_client,
|
|
403
521
|
WORKSTACK_ENDPOINT: endpoint, WORKSTACK_WORKSPACE_UID: uid, WORKSTACK_STATE_DIR: layout.tuiState,
|
|
404
522
|
WORKSTACK_EXECUTION_DOCK_CONFIG: layout.dock, WORKSTACK_EXECUTION_DOCK_BACKEND_ID: backendId,
|
|
405
|
-
WORKSTACK_DEPLOY_JOURNAL_DIR: layout.journal, WORKSTACK_STACKBITE_EXECUTABLE: executables.stackbite
|
|
523
|
+
WORKSTACK_DEPLOY_JOURNAL_DIR: layout.journal, WORKSTACK_STACKBITE_EXECUTABLE: executables.stackbite,
|
|
524
|
+
...(execution ? { WORKSTACK_TUI_STATE: layout.tuiIntent, WORKSTACK_TUI_SETTINGS: layout.tuiSettings } : {}) }, home);
|
|
406
525
|
}
|
|
407
526
|
|
|
408
527
|
function runForeground(command, args, environment, { spawnImpl = spawn, signalSource = process } = {}) {
|
|
@@ -423,13 +542,31 @@ function runForeground(command, args, environment, { spawnImpl = spawn, signalSo
|
|
|
423
542
|
|
|
424
543
|
// commands maps each bundle executable to its argv prefix; tests substitute fixtures.
|
|
425
544
|
async function openSuite({ installed, commands, workspace, init = false, stateRoot, environment = process.env,
|
|
426
|
-
ui = true, spawnImpl = spawn,
|
|
545
|
+
ui = true, frontend = 'typescript', spawnImpl = spawn, execImpl = execFile, platform = process.platform,
|
|
546
|
+
stdout = process.stdout, timeouts = {} }) {
|
|
547
|
+
if (!FRONTENDS.includes(frontend)) fail(`--frontend must be one of: ${FRONTENDS.join(', ')}`);
|
|
427
548
|
if (workspace !== undefined) workspace = checkedWorkspace(workspace);
|
|
428
549
|
else if (init) fail('--init needs --workspace ABS naming the new or empty folder');
|
|
429
550
|
commands = commands || Object.fromEntries(Object.entries(installed.executables).map(([name, file]) => [name, [file]]));
|
|
551
|
+
// A v2 bundle keeps its owner/backend markers and native init whichever frontend runs; only the
|
|
552
|
+
// foreground UI differs. TypeScript is the default; rust needs a v2 bundle. There is no fallback between them.
|
|
553
|
+
const execution = installed.bundle?.schema === BUNDLE_V2 ? { bundleVersion: installed.bundle.version } : null;
|
|
554
|
+
if (frontend === 'rust' && !execution) {
|
|
555
|
+
fail('--frontend rust needs a WorkBite execution (v2) bundle; this installed bundle has only the TypeScript UI. Nothing was started.');
|
|
556
|
+
}
|
|
557
|
+
const uiCommand = frontend === 'rust' ? 'workbite_tui' : 'workstack_tui';
|
|
558
|
+
if (!(Array.isArray(commands[uiCommand]) && commands[uiCommand].length)) {
|
|
559
|
+
fail(frontend === 'rust' ? 'This WorkBite bundle has no verified execution UI executable; nothing was started.'
|
|
560
|
+
: 'This WorkBite bundle has no verified TypeScript UI executable; nothing was started.');
|
|
561
|
+
}
|
|
430
562
|
const layout = stateLayout(path.resolve(stateRoot));
|
|
431
563
|
await prepareState(layout);
|
|
432
564
|
const base = cleanEnvironment(environment);
|
|
565
|
+
if (execution) {
|
|
566
|
+
// The execution UI's intent parent and deploy journal, made private (Windows DACL) before any file in them is written.
|
|
567
|
+
await safeDirectory(layout.nativeTui);
|
|
568
|
+
await protectPrivateDirectories([layout.nativeTui, layout.journal], { platform, environment: base, execImpl });
|
|
569
|
+
}
|
|
433
570
|
const engineEnvironment = { ...base, WORK_STACK_RUNTIME: layout.workstackRuntime };
|
|
434
571
|
const session = await withLock(layout.lock, async () => {
|
|
435
572
|
let remembered = null;
|
|
@@ -437,29 +574,41 @@ async function openSuite({ installed, commands, workspace, init = false, stateRo
|
|
|
437
574
|
remembered = await rememberedWorkspace(layout);
|
|
438
575
|
workspace = checkedWorkspace(remembered.workspace);
|
|
439
576
|
}
|
|
577
|
+
// Settings are settled before any workspace init or backend start, so a refused file changes nothing else.
|
|
578
|
+
const executionUi = execution ? await ensureExecutionSettings(layout.tuiSettings) : null;
|
|
440
579
|
const uid = init ? await initializeWorkspace(commands, workspace, engineEnvironment) : await readWorkspaceUid(workspace);
|
|
441
580
|
if (remembered && uid !== remembered.uid) fail(`The workspace ${workspace} changed while opening. ${EXPLICIT}`);
|
|
442
581
|
const backendId = await ensureBackendId(layout);
|
|
443
|
-
const engine = await ensureEngine({ commands, workspace, uid, layout, environment: engineEnvironment, spawnImpl,
|
|
444
|
-
|
|
582
|
+
const engine = await ensureEngine({ commands, workspace, uid, layout, environment: engineEnvironment, spawnImpl,
|
|
583
|
+
timeout: timeouts.engine, execution });
|
|
584
|
+
const stackbite = await ensureStackbite({ commands, layout, backendId, environment: base, spawnImpl,
|
|
585
|
+
timeout: timeouts.stackbite, execution });
|
|
586
|
+
if (execution) {
|
|
587
|
+
await initializeNativeRuntime({ commands, layout, url: stackbite.url, environment: base, execImpl,
|
|
588
|
+
timeout: timeouts.nativeInit });
|
|
589
|
+
}
|
|
445
590
|
await writeDockConfig(layout, stackbite.url);
|
|
446
591
|
// Only after the owner, StackBite and dock config are proven ready, and before the UI.
|
|
447
592
|
await rememberWorkspace(layout, workspace, uid);
|
|
448
|
-
return { uid, backendId, engine, stackbite };
|
|
593
|
+
return { uid, backendId, engine, stackbite, executionUi };
|
|
449
594
|
});
|
|
450
595
|
const summary = { schema: 'workbite.open.v1', workspace, workspace_uid: session.uid, endpoint: session.engine.endpoint,
|
|
451
596
|
owner_reused: session.engine.reused, stackbite_url: session.stackbite.url, stackbite_reused: session.stackbite.reused,
|
|
452
|
-
backend_id: session.backendId, state: layout.root
|
|
597
|
+
backend_id: session.backendId, state: layout.root, frontend,
|
|
598
|
+
...(execution ? { execution_ui: session.executionUi, execution_bundle_version: execution.bundleVersion } : {}) };
|
|
453
599
|
if (!ui) {
|
|
454
600
|
stdout.write(JSON.stringify(summary) + '\n');
|
|
455
601
|
return { code: 0, signal: null, summary };
|
|
456
602
|
}
|
|
457
603
|
const environmentForUi = uiEnvironment(base, { executables: installed.executables, layout, endpoint: session.engine.endpoint,
|
|
458
|
-
uid: session.uid, backendId: session.backendId });
|
|
459
|
-
|
|
604
|
+
uid: session.uid, backendId: session.backendId, execution: frontend === 'rust' ? execution : null });
|
|
605
|
+
// Exactly one foreground child in this terminal (inherited stdio, never detached or a new console).
|
|
606
|
+
// The TypeScript UI gets the same environment as on a v1 bundle, so it keeps its own familiar settings file.
|
|
607
|
+
const result = await runForeground(commands[uiCommand], [], environmentForUi, { spawnImpl });
|
|
460
608
|
return { ...result, summary };
|
|
461
609
|
}
|
|
462
610
|
|
|
463
611
|
module.exports = { cleanEnvironment, stateLayout, prepareState, ensureBackendId, workspaceKey, readWorkspaceUid,
|
|
464
612
|
initializeWorkspace, engineIdentity, stackbiteIdentity, freePort, ensureEngine, ensureStackbite, writeDockConfig,
|
|
465
|
-
withLock, uiEnvironment, openSuite, rememberedWorkspace,
|
|
613
|
+
withLock, uiEnvironment, openSuite, rememberedWorkspace, ensureExecutionSettings, initializeNativeRuntime, RESERVED_PORTS,
|
|
614
|
+
SELECTION_SCHEMA, EXECUTION_UI_DEFAULT, NATIVE_INIT_SCHEMA, FRONTENDS };
|
package/package.json
CHANGED
package/release.json
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema": "workbite.release.v1",
|
|
3
3
|
"product": "WorkBite",
|
|
4
|
-
"version": "0.1.
|
|
5
|
-
"launcherVersion": "0.1.
|
|
4
|
+
"version": "0.1.4",
|
|
5
|
+
"launcherVersion": "0.1.5",
|
|
6
6
|
"repository": "Shinick-Han/WorkBite-releases",
|
|
7
|
-
"tag": "v0.1.
|
|
7
|
+
"tag": "v0.1.4",
|
|
8
8
|
"platforms": {
|
|
9
9
|
"windows-x64": {
|
|
10
|
-
"asset": "workbite-0.1.
|
|
11
|
-
"sha256": "
|
|
12
|
-
"size":
|
|
13
|
-
"bundleSha256": "
|
|
14
|
-
"url": "https://github.com/Shinick-Han/WorkBite-releases/releases/download/v0.1.
|
|
10
|
+
"asset": "workbite-0.1.4-windows-x64.zip",
|
|
11
|
+
"sha256": "82150e6731bbe2cc00b08265ea5f97f12dc92e6213d32fc53ba4cd3bb1f98c50",
|
|
12
|
+
"size": 97936126,
|
|
13
|
+
"bundleSha256": "77efe214a312c14f4565075574541777a96e541ce3d0065d3e85184b0c4a2568",
|
|
14
|
+
"url": "https://github.com/Shinick-Han/WorkBite-releases/releases/download/v0.1.4/workbite-0.1.4-windows-x64.zip"
|
|
15
15
|
},
|
|
16
16
|
"linux-x64": {
|
|
17
|
-
"asset": "workbite-0.1.
|
|
18
|
-
"sha256": "
|
|
19
|
-
"size":
|
|
20
|
-
"bundleSha256": "
|
|
21
|
-
"url": "https://github.com/Shinick-Han/WorkBite-releases/releases/download/v0.1.
|
|
17
|
+
"asset": "workbite-0.1.4-linux-x64.tar.gz",
|
|
18
|
+
"sha256": "f349f8c0eebbcf171a488bd1108d74b123cb57633ec6fc54c55c79532b47ae12",
|
|
19
|
+
"size": 81647804,
|
|
20
|
+
"bundleSha256": "aa37923f4dd74e8a12ceac09d26cea956a960ea6d34939be1bef303c79042222",
|
|
21
|
+
"url": "https://github.com/Shinick-Han/WorkBite-releases/releases/download/v0.1.4/workbite-0.1.4-linux-x64.tar.gz"
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
}
|