@venturewild/workspace 0.1.0 → 0.1.2
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/LICENSE +21 -21
- package/README.md +112 -73
- package/package.json +75 -69
- package/server/bin/wild-workspace.mjs +725 -95
- package/server/src/account.mjs +114 -0
- package/server/src/agent-identity.mjs +65 -0
- package/server/src/agent-readiness.mjs +200 -0
- package/server/src/agent.mjs +356 -335
- package/server/src/config.mjs +302 -236
- package/server/src/daemon-bin.mjs +6 -2
- package/server/src/daemon-supervisor.mjs +216 -0
- package/server/src/daemon.mjs +6 -0
- package/server/src/doctor.mjs +246 -0
- package/server/src/error-reporter.mjs +86 -0
- package/server/src/inbox.mjs +86 -81
- package/server/src/index.mjs +1330 -635
- package/server/src/logpaths.mjs +97 -0
- package/server/src/observability.mjs +45 -0
- package/server/src/operator.mjs +65 -0
- package/server/src/service.mjs +127 -0
- package/server/src/session-reporter.mjs +201 -0
- package/server/src/supervisor.mjs +217 -0
- package/server/src/sync.mjs +248 -176
- package/server/src/transcript.mjs +121 -0
- package/web/dist/assets/index-Bj-mdLGj.css +1 -0
- package/web/dist/assets/index-DLRgyr9j.js +89 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-DOwej8U4.js +0 -89
- package/web/dist/assets/index-DZkyDo10.css +0 -1
package/server/src/config.mjs
CHANGED
|
@@ -1,236 +1,302 @@
|
|
|
1
|
-
// Central config + role definitions.
|
|
2
|
-
// One UI permission-flagged per AR-19.
|
|
3
|
-
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import os from 'node:os';
|
|
6
|
-
import fs from 'node:fs';
|
|
7
|
-
import crypto from 'node:crypto';
|
|
8
|
-
import { fileURLToPath } from 'node:url';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
1
|
+
// Central config + role definitions.
|
|
2
|
+
// One UI permission-flagged per AR-19.
|
|
3
|
+
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import crypto from 'node:crypto';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
|
|
10
|
+
import { loadAccount } from './account.mjs';
|
|
11
|
+
import { loadOperatorToken } from './operator.mjs';
|
|
12
|
+
|
|
13
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
|
|
15
|
+
// Secrets that shipped as scaffold defaults. Forgeable — never allowed on a
|
|
16
|
+
// non-localhost bind. Kept only so the startup guard can recognise them.
|
|
17
|
+
export const WEAK_SECRETS = Object.freeze(
|
|
18
|
+
new Set(['dev-partner-token', 'dev-share-secret-change-me']),
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const LOCAL_HOSTS = Object.freeze(new Set(['127.0.0.1', 'localhost', '::1']));
|
|
22
|
+
|
|
23
|
+
export function isLocalhost(host) {
|
|
24
|
+
return LOCAL_HOSTS.has(host);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// The bmo-sync daemon's HTTP origin, derived from its WebSocket event-feed
|
|
28
|
+
// URL so the two stay consistent when WILD_WORKSPACE_DAEMON_URL is overridden.
|
|
29
|
+
function daemonHttpBase(wsUrl) {
|
|
30
|
+
try {
|
|
31
|
+
const u = new URL(wsUrl);
|
|
32
|
+
return `${u.protocol === 'wss:' ? 'https:' : 'http:'}//${u.host}`;
|
|
33
|
+
} catch {
|
|
34
|
+
return 'http://127.0.0.1:8320';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Per-install secrets. Generated once, persisted to <dataDir>/secrets.json
|
|
39
|
+
// (the .wild-workspace dir is gitignored). Replaces the weak scaffold defaults
|
|
40
|
+
// so share tokens can't be forged. (Concern C2.)
|
|
41
|
+
function loadOrCreateSecrets(dataDir) {
|
|
42
|
+
const secretsPath = path.join(dataDir, 'secrets.json');
|
|
43
|
+
try {
|
|
44
|
+
const parsed = JSON.parse(fs.readFileSync(secretsPath, 'utf8'));
|
|
45
|
+
if (parsed.partnerToken && parsed.shareSecret) return parsed;
|
|
46
|
+
} catch {
|
|
47
|
+
// missing / unreadable / malformed — fall through and regenerate
|
|
48
|
+
}
|
|
49
|
+
const generated = {
|
|
50
|
+
partnerToken: crypto.randomBytes(24).toString('base64url'),
|
|
51
|
+
shareSecret: crypto.randomBytes(32).toString('base64url'),
|
|
52
|
+
};
|
|
53
|
+
try {
|
|
54
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
55
|
+
fs.writeFileSync(secretsPath, JSON.stringify(generated, null, 2), { mode: 0o600 });
|
|
56
|
+
} catch {
|
|
57
|
+
// can't persist (read-only fs?) — still use the generated pair for this run
|
|
58
|
+
}
|
|
59
|
+
return generated;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Refuse to start in public mode with a forgeable secret. (Concerns C1/C2.)
|
|
63
|
+
export function assertSecureBinding(config) {
|
|
64
|
+
if (!config.publicMode) return;
|
|
65
|
+
if (WEAK_SECRETS.has(config.partnerToken) || WEAK_SECRETS.has(config.shareSecret)) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`Refusing to run in public mode with a default secret. ` +
|
|
68
|
+
`Set WILD_WORKSPACE_PARTNER_TOKEN and WILD_WORKSPACE_SHARE_SECRET, ` +
|
|
69
|
+
`or remove them so per-install secrets are generated.`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const ROLES = Object.freeze({
|
|
75
|
+
PARTNER: 'partner',
|
|
76
|
+
VIEWER: 'viewer',
|
|
77
|
+
CLIENT: 'client',
|
|
78
|
+
// The consented support/operator channel (off by default — see operator.mjs).
|
|
79
|
+
OPERATOR: 'operator',
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
export const ROLE_CAPABILITIES = Object.freeze({
|
|
83
|
+
partner: {
|
|
84
|
+
chat: true,
|
|
85
|
+
chatWrite: true,
|
|
86
|
+
preview: true,
|
|
87
|
+
fileTree: true,
|
|
88
|
+
terminal: true,
|
|
89
|
+
inbox: true,
|
|
90
|
+
share: true,
|
|
91
|
+
sync: true,
|
|
92
|
+
deploy: true,
|
|
93
|
+
requestChanges: false,
|
|
94
|
+
operate: true, // the owner can also drive the operator allowlist locally
|
|
95
|
+
},
|
|
96
|
+
viewer: {
|
|
97
|
+
chat: true,
|
|
98
|
+
chatWrite: false,
|
|
99
|
+
preview: true,
|
|
100
|
+
fileTree: false,
|
|
101
|
+
terminal: false,
|
|
102
|
+
inbox: false,
|
|
103
|
+
share: false,
|
|
104
|
+
sync: false,
|
|
105
|
+
deploy: false,
|
|
106
|
+
requestChanges: false,
|
|
107
|
+
operate: false,
|
|
108
|
+
},
|
|
109
|
+
client: {
|
|
110
|
+
chat: true,
|
|
111
|
+
chatWrite: true,
|
|
112
|
+
preview: true,
|
|
113
|
+
fileTree: false,
|
|
114
|
+
terminal: false,
|
|
115
|
+
inbox: false,
|
|
116
|
+
share: false,
|
|
117
|
+
sync: false,
|
|
118
|
+
deploy: false,
|
|
119
|
+
requestChanges: true,
|
|
120
|
+
operate: false,
|
|
121
|
+
},
|
|
122
|
+
// Operator: remote diagnose + a curated remediation allowlist. Read-only on
|
|
123
|
+
// chat (can SEE the conversation to help, cannot drive the agent — chatWrite
|
|
124
|
+
// stays false), plus the `operate` capability the /api/operator/* routes gate
|
|
125
|
+
// on. Reachable only with the dedicated operator token (operator.mjs).
|
|
126
|
+
operator: {
|
|
127
|
+
chat: true,
|
|
128
|
+
chatWrite: false,
|
|
129
|
+
preview: true,
|
|
130
|
+
fileTree: true,
|
|
131
|
+
terminal: false,
|
|
132
|
+
inbox: false,
|
|
133
|
+
share: false,
|
|
134
|
+
sync: false,
|
|
135
|
+
deploy: false,
|
|
136
|
+
requestChanges: false,
|
|
137
|
+
operate: true,
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
export const DEFAULT_AGENTS = Object.freeze([
|
|
142
|
+
{
|
|
143
|
+
id: 'claude',
|
|
144
|
+
binary: 'claude',
|
|
145
|
+
label: 'Claude Code',
|
|
146
|
+
description: 'Anthropic Claude Code',
|
|
147
|
+
args: ['-p', '--output-format', 'stream-json', '--include-partial-messages', '--verbose'],
|
|
148
|
+
streamFormat: 'claude-stream-json',
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
id: 'gemini',
|
|
152
|
+
binary: 'gemini',
|
|
153
|
+
label: 'Gemini CLI',
|
|
154
|
+
description: 'Google Gemini',
|
|
155
|
+
args: ['-p'],
|
|
156
|
+
streamFormat: 'text',
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
id: 'glm',
|
|
160
|
+
binary: 'glm',
|
|
161
|
+
label: 'GLM (Z.AI)',
|
|
162
|
+
description: 'GLM-4.6 via Z.AI',
|
|
163
|
+
args: ['-p', '--permission-mode', 'bypassPermissions'],
|
|
164
|
+
streamFormat: 'text',
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
id: 'codex',
|
|
168
|
+
binary: 'codex',
|
|
169
|
+
label: 'Codex (OpenAI)',
|
|
170
|
+
description: 'GPT-5 / o3 via Codex CLI',
|
|
171
|
+
args: ['exec', '--skip-git-repo-check'],
|
|
172
|
+
streamFormat: 'text',
|
|
173
|
+
},
|
|
174
|
+
]);
|
|
175
|
+
|
|
176
|
+
export function buildConfig(overrides = {}) {
|
|
177
|
+
const env = process.env;
|
|
178
|
+
const portOverride = overrides.port;
|
|
179
|
+
const resolvedPort =
|
|
180
|
+
typeof portOverride === 'number'
|
|
181
|
+
? portOverride
|
|
182
|
+
: Number(env.WILD_WORKSPACE_PORT || 5173);
|
|
183
|
+
const workspaceDir = path.resolve(
|
|
184
|
+
overrides.workspaceDir || env.WILD_WORKSPACE_DIR || process.cwd(),
|
|
185
|
+
);
|
|
186
|
+
const dataDir = path.resolve(
|
|
187
|
+
overrides.dataDir ||
|
|
188
|
+
env.WILD_WORKSPACE_DATA_DIR ||
|
|
189
|
+
path.join(workspaceDir, '.wild-workspace'),
|
|
190
|
+
);
|
|
191
|
+
// Lazy: only load/generate persisted secrets if neither an override nor an
|
|
192
|
+
// env var supplies one — keeps tests that pass both from touching the fs.
|
|
193
|
+
let _secrets = null;
|
|
194
|
+
const secrets = () => (_secrets ??= loadOrCreateSecrets(dataDir));
|
|
195
|
+
const host = overrides.host || env.WILD_WORKSPACE_HOST || '127.0.0.1';
|
|
196
|
+
// publicMode = "treat every request as untrusted". True for a non-localhost
|
|
197
|
+
// bind, OR when WILD_WORKSPACE_PUBLIC=1 — needed when a tunnel (Cloudflare
|
|
198
|
+
// etc.) forwards public traffic to a localhost-bound server, since the bind
|
|
199
|
+
// address alone would otherwise look local. Drives the C1 auth posture.
|
|
200
|
+
const publicMode =
|
|
201
|
+
overrides.publicMode ??
|
|
202
|
+
(env.WILD_WORKSPACE_PUBLIC === '1' || !isLocalhost(host));
|
|
203
|
+
// bmo-sync: the local daemon's event feed (a WebSocket URL).
|
|
204
|
+
const daemonUrl =
|
|
205
|
+
overrides.daemonUrl ||
|
|
206
|
+
env.WILD_WORKSPACE_DAEMON_URL ||
|
|
207
|
+
'ws://127.0.0.1:8320/api/events';
|
|
208
|
+
// Per-install bmo-sync account — null until the user runs `wild-workspace
|
|
209
|
+
// login` with the payload from `workspace.venturewild.llc`. Its presence
|
|
210
|
+
// upgrades the install to a real slug (shareBaseUrl flips to the user's
|
|
211
|
+
// subdomain) and lights up the /api/session.account field for the UI.
|
|
212
|
+
const account =
|
|
213
|
+
overrides.account === undefined ? loadAccount(dataDir) : overrides.account;
|
|
214
|
+
const accountShareBase = account?.slug
|
|
215
|
+
? `https://${account.slug}.venturewild.llc`
|
|
216
|
+
: null;
|
|
217
|
+
return {
|
|
218
|
+
port: resolvedPort,
|
|
219
|
+
host,
|
|
220
|
+
publicMode,
|
|
221
|
+
workspaceDir,
|
|
222
|
+
dataDir,
|
|
223
|
+
webDir:
|
|
224
|
+
overrides.webDir ||
|
|
225
|
+
env.WILD_WORKSPACE_WEB_DIR ||
|
|
226
|
+
path.resolve(__dirname, '..', '..', 'web', 'dist'),
|
|
227
|
+
openBrowser: overrides.openBrowser ?? env.WILD_WORKSPACE_NO_OPEN !== '1',
|
|
228
|
+
shareBaseUrl:
|
|
229
|
+
overrides.shareBaseUrl ||
|
|
230
|
+
env.WILD_WORKSPACE_SHARE_BASE_URL ||
|
|
231
|
+
accountShareBase ||
|
|
232
|
+
`http://${host}:${resolvedPort}`,
|
|
233
|
+
// The signed-in account (if any). Kept on the server-side config so
|
|
234
|
+
// /api/session can expose the public bits (slug, email, accountId, displayName)
|
|
235
|
+
// to the UI while accountToken stays here only.
|
|
236
|
+
account: account
|
|
237
|
+
? {
|
|
238
|
+
slug: account.slug,
|
|
239
|
+
email: account.email,
|
|
240
|
+
accountId: account.accountId,
|
|
241
|
+
displayName: account.displayName,
|
|
242
|
+
loggedInAt: account.loggedInAt,
|
|
243
|
+
// accountToken is intentionally kept out of the broadcasted config
|
|
244
|
+
// shape — it's read separately by code that needs to authenticate
|
|
245
|
+
// against bmo-sync.
|
|
246
|
+
}
|
|
247
|
+
: null,
|
|
248
|
+
accountToken: account?.accountToken || null,
|
|
249
|
+
partnerToken:
|
|
250
|
+
overrides.partnerToken ||
|
|
251
|
+
env.WILD_WORKSPACE_PARTNER_TOKEN ||
|
|
252
|
+
secrets().partnerToken,
|
|
253
|
+
shareSecret:
|
|
254
|
+
overrides.shareSecret ||
|
|
255
|
+
env.WILD_WORKSPACE_SHARE_SECRET ||
|
|
256
|
+
secrets().shareSecret,
|
|
257
|
+
// The operator-channel token — null unless the user explicitly enabled the
|
|
258
|
+
// channel (`wild-workspace operator enable`). Off by default. Server-side
|
|
259
|
+
// only; never broadcast to the browser.
|
|
260
|
+
operatorToken:
|
|
261
|
+
overrides.operatorToken ??
|
|
262
|
+
env.WILD_WORKSPACE_OPERATOR_TOKEN ??
|
|
263
|
+
loadOperatorToken(dataDir),
|
|
264
|
+
workspaceId:
|
|
265
|
+
overrides.workspaceId ||
|
|
266
|
+
env.WILD_WORKSPACE_ID ||
|
|
267
|
+
path.basename(workspaceDir) ||
|
|
268
|
+
'workspace',
|
|
269
|
+
role: overrides.role || env.WILD_WORKSPACE_ROLE || ROLES.PARTNER,
|
|
270
|
+
// bmo-sync daemon — a separate local process; the bridge retries quietly
|
|
271
|
+
// when it is absent. daemonUrl is the WebSocket event feed; daemonHttpUrl
|
|
272
|
+
// is the same origin's HTTP API (pair / detach / list).
|
|
273
|
+
daemonUrl,
|
|
274
|
+
daemonHttpUrl: daemonHttpBase(daemonUrl),
|
|
275
|
+
// Auto-start the bmo-sync daemon when the server boots. On by default;
|
|
276
|
+
// WILD_WORKSPACE_DAEMON_AUTOSTART=0 disables it. Forced off under the test
|
|
277
|
+
// runner (VITEST / NODE_ENV=test) so the suite never spawns a real daemon.
|
|
278
|
+
daemonAutostart:
|
|
279
|
+
overrides.daemonAutostart ??
|
|
280
|
+
(env.WILD_WORKSPACE_DAEMON_AUTOSTART !== '0' &&
|
|
281
|
+
!env.VITEST &&
|
|
282
|
+
env.NODE_ENV !== 'test'),
|
|
283
|
+
// Central bmo-sync server (Fly.io). Used to redeem invites (via the
|
|
284
|
+
// daemon) and — only when an admin key is set — to mint them.
|
|
285
|
+
bmoSyncServerUrl:
|
|
286
|
+
overrides.bmoSyncServerUrl ||
|
|
287
|
+
env.WILD_WORKSPACE_BMO_SYNC_URL ||
|
|
288
|
+
env.BMO_SYNC_URL ||
|
|
289
|
+
'https://sync.venturewild.llc',
|
|
290
|
+
// Optional. Present only on an install that mints invites (the folder
|
|
291
|
+
// owner). Absent installs can still redeem. Never sent to the browser.
|
|
292
|
+
bmoSyncAdminKey:
|
|
293
|
+
overrides.bmoSyncAdminKey ||
|
|
294
|
+
env.BMO_SYNC_ADMIN_KEY ||
|
|
295
|
+
env.WILD_WORKSPACE_BMO_ADMIN_KEY ||
|
|
296
|
+
null,
|
|
297
|
+
home: os.homedir(),
|
|
298
|
+
nodeEnv: env.NODE_ENV || 'production',
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export const APP_VERSION = '0.1.0';
|
|
@@ -36,9 +36,13 @@ export function daemonBinaryName() {
|
|
|
36
36
|
*
|
|
37
37
|
* @param {{ env?: NodeJS.ProcessEnv, vendorRoot?: string }} [opts]
|
|
38
38
|
*/
|
|
39
|
-
export function resolveDaemonBinary({ env = process.env, vendorRoot } = {}) {
|
|
39
|
+
export function resolveDaemonBinary({ env = process.env, vendorRoot, requireResolve } = {}) {
|
|
40
40
|
const binName = daemonBinaryName();
|
|
41
41
|
const tag = platformTag();
|
|
42
|
+
// Injected seam: lets a test simulate "the platform subpackage isn't
|
|
43
|
+
// installed" deterministically, regardless of what's in this machine's
|
|
44
|
+
// node_modules (the win32-x64 subpackage IS present on a Windows dev box).
|
|
45
|
+
const resolvePkg = requireResolve || ((id) => require.resolve(id));
|
|
42
46
|
|
|
43
47
|
// 1. explicit override — if set but missing, that's an error, not a miss.
|
|
44
48
|
const override = env.WILD_WORKSPACE_DAEMON_BIN;
|
|
@@ -49,7 +53,7 @@ export function resolveDaemonBinary({ env = process.env, vendorRoot } = {}) {
|
|
|
49
53
|
// 2. per-platform npm subpackage — resolve via its package.json so the
|
|
50
54
|
// lookup doesn't depend on an `exports` map for the binary file.
|
|
51
55
|
try {
|
|
52
|
-
const pkgJson =
|
|
56
|
+
const pkgJson = resolvePkg(`@venturewild/workspace-daemon-${tag}/package.json`);
|
|
53
57
|
const candidate = path.join(path.dirname(pkgJson), binName);
|
|
54
58
|
if (existsSync(candidate)) return { path: candidate, source: 'subpackage' };
|
|
55
59
|
} catch {
|