@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.
@@ -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
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
-
12
- // Secrets that shipped as scaffold defaults. Forgeable — never allowed on a
13
- // non-localhost bind. Kept only so the startup guard can recognise them.
14
- export const WEAK_SECRETS = Object.freeze(
15
- new Set(['dev-partner-token', 'dev-share-secret-change-me']),
16
- );
17
-
18
- const LOCAL_HOSTS = Object.freeze(new Set(['127.0.0.1', 'localhost', '::1']));
19
-
20
- export function isLocalhost(host) {
21
- return LOCAL_HOSTS.has(host);
22
- }
23
-
24
- // The bmo-sync daemon's HTTP origin, derived from its WebSocket event-feed
25
- // URL so the two stay consistent when WILD_WORKSPACE_DAEMON_URL is overridden.
26
- function daemonHttpBase(wsUrl) {
27
- try {
28
- const u = new URL(wsUrl);
29
- return `${u.protocol === 'wss:' ? 'https:' : 'http:'}//${u.host}`;
30
- } catch {
31
- return 'http://127.0.0.1:8320';
32
- }
33
- }
34
-
35
- // Per-install secrets. Generated once, persisted to <dataDir>/secrets.json
36
- // (the .wild-workspace dir is gitignored). Replaces the weak scaffold defaults
37
- // so share tokens can't be forged. (Concern C2.)
38
- function loadOrCreateSecrets(dataDir) {
39
- const secretsPath = path.join(dataDir, 'secrets.json');
40
- try {
41
- const parsed = JSON.parse(fs.readFileSync(secretsPath, 'utf8'));
42
- if (parsed.partnerToken && parsed.shareSecret) return parsed;
43
- } catch {
44
- // missing / unreadable / malformed — fall through and regenerate
45
- }
46
- const generated = {
47
- partnerToken: crypto.randomBytes(24).toString('base64url'),
48
- shareSecret: crypto.randomBytes(32).toString('base64url'),
49
- };
50
- try {
51
- fs.mkdirSync(dataDir, { recursive: true });
52
- fs.writeFileSync(secretsPath, JSON.stringify(generated, null, 2), { mode: 0o600 });
53
- } catch {
54
- // can't persist (read-only fs?) — still use the generated pair for this run
55
- }
56
- return generated;
57
- }
58
-
59
- // Refuse to start in public mode with a forgeable secret. (Concerns C1/C2.)
60
- export function assertSecureBinding(config) {
61
- if (!config.publicMode) return;
62
- if (WEAK_SECRETS.has(config.partnerToken) || WEAK_SECRETS.has(config.shareSecret)) {
63
- throw new Error(
64
- `Refusing to run in public mode with a default secret. ` +
65
- `Set WILD_WORKSPACE_PARTNER_TOKEN and WILD_WORKSPACE_SHARE_SECRET, ` +
66
- `or remove them so per-install secrets are generated.`,
67
- );
68
- }
69
- }
70
-
71
- export const ROLES = Object.freeze({
72
- PARTNER: 'partner',
73
- VIEWER: 'viewer',
74
- CLIENT: 'client',
75
- });
76
-
77
- export const ROLE_CAPABILITIES = Object.freeze({
78
- partner: {
79
- chat: true,
80
- chatWrite: true,
81
- preview: true,
82
- fileTree: true,
83
- terminal: true,
84
- inbox: true,
85
- share: true,
86
- sync: true,
87
- deploy: true,
88
- requestChanges: false,
89
- },
90
- viewer: {
91
- chat: true,
92
- chatWrite: false,
93
- preview: true,
94
- fileTree: false,
95
- terminal: false,
96
- inbox: false,
97
- share: false,
98
- sync: false,
99
- deploy: false,
100
- requestChanges: false,
101
- },
102
- client: {
103
- chat: true,
104
- chatWrite: true,
105
- preview: true,
106
- fileTree: false,
107
- terminal: false,
108
- inbox: false,
109
- share: false,
110
- sync: false,
111
- deploy: false,
112
- requestChanges: true,
113
- },
114
- });
115
-
116
- export const DEFAULT_AGENTS = Object.freeze([
117
- {
118
- id: 'claude',
119
- binary: 'claude',
120
- label: 'Claude Code',
121
- description: 'Anthropic Claude Code',
122
- args: ['-p', '--output-format', 'stream-json', '--include-partial-messages', '--verbose'],
123
- streamFormat: 'claude-stream-json',
124
- },
125
- {
126
- id: 'gemini',
127
- binary: 'gemini',
128
- label: 'Gemini CLI',
129
- description: 'Google Gemini',
130
- args: ['-p'],
131
- streamFormat: 'text',
132
- },
133
- {
134
- id: 'glm',
135
- binary: 'glm',
136
- label: 'GLM (Z.AI)',
137
- description: 'GLM-4.6 via Z.AI',
138
- args: ['-p', '--permission-mode', 'bypassPermissions'],
139
- streamFormat: 'text',
140
- },
141
- {
142
- id: 'codex',
143
- binary: 'codex',
144
- label: 'Codex (OpenAI)',
145
- description: 'GPT-5 / o3 via Codex CLI',
146
- args: ['exec', '--skip-git-repo-check'],
147
- streamFormat: 'text',
148
- },
149
- ]);
150
-
151
- export function buildConfig(overrides = {}) {
152
- const env = process.env;
153
- const portOverride = overrides.port;
154
- const resolvedPort =
155
- typeof portOverride === 'number'
156
- ? portOverride
157
- : Number(env.WILD_WORKSPACE_PORT || 5173);
158
- const workspaceDir = path.resolve(
159
- overrides.workspaceDir || env.WILD_WORKSPACE_DIR || process.cwd(),
160
- );
161
- const dataDir = path.resolve(
162
- overrides.dataDir ||
163
- env.WILD_WORKSPACE_DATA_DIR ||
164
- path.join(workspaceDir, '.wild-workspace'),
165
- );
166
- // Lazy: only load/generate persisted secrets if neither an override nor an
167
- // env var supplies one — keeps tests that pass both from touching the fs.
168
- let _secrets = null;
169
- const secrets = () => (_secrets ??= loadOrCreateSecrets(dataDir));
170
- const host = overrides.host || env.WILD_WORKSPACE_HOST || '127.0.0.1';
171
- // publicMode = "treat every request as untrusted". True for a non-localhost
172
- // bind, OR when WILD_WORKSPACE_PUBLIC=1 — needed when a tunnel (Cloudflare
173
- // etc.) forwards public traffic to a localhost-bound server, since the bind
174
- // address alone would otherwise look local. Drives the C1 auth posture.
175
- const publicMode =
176
- overrides.publicMode ??
177
- (env.WILD_WORKSPACE_PUBLIC === '1' || !isLocalhost(host));
178
- // bmo-sync: the local daemon's event feed (a WebSocket URL).
179
- const daemonUrl =
180
- overrides.daemonUrl ||
181
- env.WILD_WORKSPACE_DAEMON_URL ||
182
- 'ws://127.0.0.1:8320/api/events';
183
- return {
184
- port: resolvedPort,
185
- host,
186
- publicMode,
187
- workspaceDir,
188
- dataDir,
189
- webDir:
190
- overrides.webDir ||
191
- env.WILD_WORKSPACE_WEB_DIR ||
192
- path.resolve(__dirname, '..', '..', 'web', 'dist'),
193
- openBrowser: overrides.openBrowser ?? env.WILD_WORKSPACE_NO_OPEN !== '1',
194
- shareBaseUrl:
195
- overrides.shareBaseUrl ||
196
- env.WILD_WORKSPACE_SHARE_BASE_URL ||
197
- `http://${host}:${resolvedPort}`,
198
- partnerToken:
199
- overrides.partnerToken ||
200
- env.WILD_WORKSPACE_PARTNER_TOKEN ||
201
- secrets().partnerToken,
202
- shareSecret:
203
- overrides.shareSecret ||
204
- env.WILD_WORKSPACE_SHARE_SECRET ||
205
- secrets().shareSecret,
206
- workspaceId:
207
- overrides.workspaceId ||
208
- env.WILD_WORKSPACE_ID ||
209
- path.basename(workspaceDir) ||
210
- 'workspace',
211
- role: overrides.role || env.WILD_WORKSPACE_ROLE || ROLES.PARTNER,
212
- // bmo-sync daemon — a separate local process; the bridge retries quietly
213
- // when it is absent. daemonUrl is the WebSocket event feed; daemonHttpUrl
214
- // is the same origin's HTTP API (pair / detach / list).
215
- daemonUrl,
216
- daemonHttpUrl: daemonHttpBase(daemonUrl),
217
- // Central bmo-sync server (Fly.io). Used to redeem invites (via the
218
- // daemon) and — only when an admin key is set — to mint them.
219
- bmoSyncServerUrl:
220
- overrides.bmoSyncServerUrl ||
221
- env.WILD_WORKSPACE_BMO_SYNC_URL ||
222
- env.BMO_SYNC_URL ||
223
- 'https://sync.venturewild.llc',
224
- // Optional. Present only on an install that mints invites (the folder
225
- // owner). Absent installs can still redeem. Never sent to the browser.
226
- bmoSyncAdminKey:
227
- overrides.bmoSyncAdminKey ||
228
- env.BMO_SYNC_ADMIN_KEY ||
229
- env.WILD_WORKSPACE_BMO_ADMIN_KEY ||
230
- null,
231
- home: os.homedir(),
232
- nodeEnv: env.NODE_ENV || 'production',
233
- };
234
- }
235
-
236
- export const APP_VERSION = '0.1.0';
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 = require.resolve(`@venturewild/workspace-daemon-${tag}/package.json`);
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 {