@dexlyai/dexly 0.1.2 → 0.1.3

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.
Files changed (4) hide show
  1. package/README.md +4 -2
  2. package/dist/cli.js +611 -299
  3. package/dist/host.js +332 -110
  4. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -117,29 +117,21 @@ var require_companion = __commonJS({
117
117
  });
118
118
 
119
119
  // src/cli.ts
120
- var import_node_path4 = __toESM(require("node:path"));
120
+ var import_node_path5 = __toESM(require("node:path"));
121
121
  var import_node_process5 = __toESM(require("node:process"));
122
- var import_companion6 = __toESM(require_companion());
122
+ var import_companion7 = __toESM(require_companion());
123
123
 
124
124
  // src/doctor.ts
125
- var import_promises = require("node:fs/promises");
126
- var import_node_child_process = require("node:child_process");
125
+ var import_promises2 = require("node:fs/promises");
127
126
  var import_node_events = require("node:events");
128
- var import_companion2 = __toESM(require_companion());
127
+ var import_companion4 = __toESM(require_companion());
129
128
 
130
129
  // src/constants.ts
131
- var import_node_os = __toESM(require("node:os"));
132
130
  var import_node_path = __toESM(require("node:path"));
133
131
  var import_companion = __toESM(require_companion());
134
- var DEXLY_COMPANION_VERSION = true ? "0.1.2" : packageJson.version;
132
+ var DEXLY_COMPANION_VERSION = true ? "0.1.3" : packageJson.version;
135
133
  var DEXLY_COMPANION_DESCRIPTION = "Dexly native bridge for Codex";
136
134
  var DEXLY_COMPANION_METADATA_FILE_NAME = "install-metadata.json";
137
- function defaultInstallRoot(homeDir = import_node_os.default.homedir()) {
138
- return import_node_path.default.join(homeDir, "Library", "Application Support", "Dexly", "companion");
139
- }
140
- function defaultChromeHostManifestDir(homeDir = import_node_os.default.homedir()) {
141
- return import_node_path.default.join(homeDir, "Library", "Application Support", "Google", "Chrome", "NativeMessagingHosts");
142
- }
143
135
  function defaultChromeAllowedOrigins() {
144
136
  return [...import_companion.DEXLY_FIXED_ALLOWED_ORIGINS];
145
137
  }
@@ -158,8 +150,8 @@ function versionInstallPath(installRoot, version) {
158
150
  function installedHostScriptPath(installRoot) {
159
151
  return import_node_path.default.join(currentInstallPath(installRoot), "dist", "host.js");
160
152
  }
161
- function installedHostLauncherPath(installRoot) {
162
- return import_node_path.default.join(currentInstallPath(installRoot), "bin", "dexly-companion-host");
153
+ function installedHostLauncherPath(installRoot, platform = process.platform) {
154
+ return import_node_path.default.join(currentInstallPath(installRoot), "bin", resolveLauncherFileName(platform));
163
155
  }
164
156
  function versionedCliPath(versionRoot) {
165
157
  return import_node_path.default.join(versionRoot, "dist", "cli.js");
@@ -167,15 +159,12 @@ function versionedCliPath(versionRoot) {
167
159
  function versionedHostScriptPath(versionRoot) {
168
160
  return import_node_path.default.join(versionRoot, "dist", "host.js");
169
161
  }
170
- function versionedHostLauncherPath(versionRoot) {
171
- return import_node_path.default.join(versionRoot, "bin", "dexly-companion-host");
162
+ function versionedHostLauncherPath(versionRoot, platform = process.platform) {
163
+ return import_node_path.default.join(versionRoot, "bin", resolveLauncherFileName(platform));
172
164
  }
173
165
  function versionedPackageJsonPath(versionRoot) {
174
166
  return import_node_path.default.join(versionRoot, "package.json");
175
167
  }
176
- function installedHostManifestPath(manifestDir) {
177
- return import_node_path.default.join(manifestDir, `${import_companion.DEXLY_COMPANION_HOST_NAME}.json`);
178
- }
179
168
  function buildChromeHostManifest(hostPath, allowedOrigins) {
180
169
  return {
181
170
  name: import_companion.DEXLY_COMPANION_HOST_NAME,
@@ -185,6 +174,68 @@ function buildChromeHostManifest(hostPath, allowedOrigins) {
185
174
  allowed_origins: allowedOrigins
186
175
  };
187
176
  }
177
+ function resolveLauncherFileName(platform) {
178
+ return platform === "win32" ? "dexly-companion-host.cmd" : "dexly-companion-host";
179
+ }
180
+
181
+ // src/install-metadata.ts
182
+ var import_promises = require("node:fs/promises");
183
+ var import_companion2 = __toESM(require_companion());
184
+ function normalizeString(value) {
185
+ return typeof value === "string" && value.trim().length > 0 ? value : null;
186
+ }
187
+ function normalizeToolPaths(value) {
188
+ if (!value || typeof value !== "object") {
189
+ return null;
190
+ }
191
+ const candidate = value;
192
+ const nodePath = normalizeString(candidate.nodePath);
193
+ const npmPath = normalizeString(candidate.npmPath);
194
+ if (!nodePath || !npmPath) {
195
+ return null;
196
+ }
197
+ return {
198
+ nodePath,
199
+ npmPath,
200
+ codexPath: normalizeString(candidate.codexPath)
201
+ };
202
+ }
203
+ async function loadInstallMetadata(installRoot) {
204
+ try {
205
+ const raw = await (0, import_promises.readFile)(installMetadataPath(installRoot), "utf8");
206
+ const parsed = JSON.parse(raw);
207
+ const tools = normalizeToolPaths(parsed.tools);
208
+ const currentVersion = normalizeString(parsed.currentVersion);
209
+ const installedAt = normalizeString(parsed.installedAt);
210
+ const updatedAt = normalizeString(parsed.updatedAt);
211
+ if (!tools || !currentVersion || !installedAt || !updatedAt) {
212
+ return null;
213
+ }
214
+ const knownGoodVersions = Array.isArray(parsed.knownGoodVersions) ? parsed.knownGoodVersions.filter((value) => typeof value === "string" && value.trim().length > 0) : [currentVersion];
215
+ return {
216
+ schemaVersion: 1,
217
+ packageName: import_companion2.DEXLY_COMPANION_PACKAGE_NAME,
218
+ executableName: import_companion2.DEXLY_COMPANION_EXECUTABLE_NAME,
219
+ currentVersion,
220
+ previousVersion: normalizeString(parsed.previousVersion),
221
+ knownGoodVersions: knownGoodVersions.length > 0 ? knownGoodVersions : [currentVersion],
222
+ currentChannel: normalizeString(parsed.currentChannel),
223
+ installedAt,
224
+ updatedAt,
225
+ tools
226
+ };
227
+ } catch {
228
+ return null;
229
+ }
230
+ }
231
+ async function saveInstallMetadata(installRoot, metadata) {
232
+ await (0, import_promises.writeFile)(
233
+ installMetadataPath(installRoot),
234
+ `${JSON.stringify(metadata, null, 2)}
235
+ `,
236
+ "utf8"
237
+ );
238
+ }
188
239
 
189
240
  // src/native-protocol.ts
190
241
  var import_node_buffer = require("node:buffer");
@@ -223,33 +274,352 @@ function writeNativeMessage(output, message) {
223
274
  output.write(encodeNativeMessage(message));
224
275
  }
225
276
 
277
+ // src/platform.ts
278
+ var import_node_os = __toESM(require("node:os"));
279
+ var import_node_path2 = __toESM(require("node:path"));
280
+ var import_node_process = __toESM(require("node:process"));
281
+ var import_companion3 = __toESM(require_companion());
282
+ function assertSupportedPlatform(platform = import_node_process.default.platform) {
283
+ switch (platform) {
284
+ case "darwin":
285
+ case "linux":
286
+ case "win32":
287
+ return platform;
288
+ default:
289
+ throw new Error(
290
+ `Dexly Companion currently supports macOS, Windows, and Linux only. Unsupported platform: ${platform}.`
291
+ );
292
+ }
293
+ }
294
+ function describePlatform(platform = import_node_process.default.platform) {
295
+ switch (platform) {
296
+ case "darwin":
297
+ return "macOS";
298
+ case "linux":
299
+ return "Linux";
300
+ case "win32":
301
+ return "Windows";
302
+ default:
303
+ return platform;
304
+ }
305
+ }
306
+ function defaultChromeAllowedOrigins2() {
307
+ return [...import_companion3.DEXLY_FIXED_ALLOWED_ORIGINS];
308
+ }
309
+ function defaultInstallRoot(options = {}) {
310
+ const platform = assertSupportedPlatform(options.platform);
311
+ const homeDir = options.homeDir ?? import_node_os.default.homedir();
312
+ const env = options.env ?? import_node_process.default.env;
313
+ switch (platform) {
314
+ case "darwin":
315
+ return import_node_path2.default.join(homeDir, "Library", "Application Support", "Dexly", "companion");
316
+ case "linux": {
317
+ const dataHome = env.XDG_DATA_HOME?.trim() || import_node_path2.default.join(homeDir, ".local", "share");
318
+ return import_node_path2.default.join(dataHome, "Dexly", "companion");
319
+ }
320
+ case "win32": {
321
+ const localAppData = env.LOCALAPPDATA?.trim() || import_node_path2.default.join(homeDir, "AppData", "Local");
322
+ return import_node_path2.default.join(localAppData, "Dexly", "companion");
323
+ }
324
+ }
325
+ }
326
+ function resolveChromeHostRegistration(options = {}) {
327
+ const platform = assertSupportedPlatform(options.platform);
328
+ const homeDir = options.homeDir ?? import_node_os.default.homedir();
329
+ const env = options.env ?? import_node_process.default.env;
330
+ const installRoot = options.installRoot ?? defaultInstallRoot({
331
+ platform,
332
+ homeDir,
333
+ env
334
+ });
335
+ const manifestPath = options.manifestDir ? import_node_path2.default.join(options.manifestDir, `${import_companion3.DEXLY_COMPANION_HOST_NAME}.json`) : defaultChromeManifestPath({
336
+ platform,
337
+ homeDir,
338
+ env,
339
+ installRoot
340
+ });
341
+ if (platform === "win32") {
342
+ return {
343
+ kind: "windows-registry",
344
+ manifestPath,
345
+ allowedOrigins: defaultChromeAllowedOrigins2(),
346
+ registryKey: options.windowsRegistryKey ?? `HKCU\\Software\\Google\\Chrome\\NativeMessagingHosts\\${import_companion3.DEXLY_COMPANION_HOST_NAME}`
347
+ };
348
+ }
349
+ return {
350
+ kind: "manifest-file",
351
+ manifestPath,
352
+ allowedOrigins: defaultChromeAllowedOrigins2(),
353
+ registryKey: null
354
+ };
355
+ }
356
+ function resolvePlatformLayout(options = {}) {
357
+ const platform = assertSupportedPlatform(options.platform);
358
+ const installRoot = options.installRoot ?? defaultInstallRoot(options);
359
+ return {
360
+ platform,
361
+ installRoot,
362
+ activationStrategy: platform === "win32" ? "copy" : "symlink",
363
+ launcherKind: platform === "win32" ? "windows-cmd" : "posix",
364
+ launcherFileName: platform === "win32" ? "dexly-companion-host.cmd" : "dexly-companion-host",
365
+ hostRegistration: resolveChromeHostRegistration({
366
+ ...options,
367
+ platform,
368
+ installRoot
369
+ })
370
+ };
371
+ }
372
+ function defaultLauncherPathEntries(platform = import_node_process.default.platform, env = import_node_process.default.env) {
373
+ switch (assertSupportedPlatform(platform)) {
374
+ case "darwin":
375
+ return ["/usr/local/bin", "/opt/homebrew/bin", "/usr/bin", "/bin", "/usr/sbin", "/sbin"];
376
+ case "linux":
377
+ return ["/usr/local/bin", "/usr/bin", "/bin", "/usr/sbin", "/sbin"];
378
+ case "win32": {
379
+ const systemRoot = env.SystemRoot?.trim() || "C:\\Windows";
380
+ return [import_node_path2.default.join(systemRoot, "System32"), systemRoot];
381
+ }
382
+ }
383
+ }
384
+ function renderHostLauncher(options) {
385
+ const platform = assertSupportedPlatform(options.platform);
386
+ const pathEntries = [
387
+ import_node_path2.default.dirname(options.nodePath),
388
+ options.codexPath ? import_node_path2.default.dirname(options.codexPath) : null,
389
+ ...defaultLauncherPathEntries(platform, options.env ?? import_node_process.default.env)
390
+ ].filter((entry, index, entries) => typeof entry === "string" && entries.indexOf(entry) === index);
391
+ if (platform === "win32") {
392
+ const lines2 = [
393
+ "@echo off",
394
+ "setlocal",
395
+ `set "PATH=${toBatchLiteral(pathEntries.join(import_node_path2.default.delimiter))};%PATH%"`,
396
+ options.codexPath ? `set "DEXLY_COMPANION_CODEX_PATH=${toBatchLiteral(options.codexPath)}"` : null,
397
+ `set "DEXLY_COMPANION_NODE_PATH=${toBatchLiteral(options.nodePath)}"`,
398
+ `set "DEXLY_COMPANION_HOST_SCRIPT=${toBatchLiteral(options.hostScriptPath)}"`,
399
+ '"%DEXLY_COMPANION_NODE_PATH%" "%DEXLY_COMPANION_HOST_SCRIPT%" %*'
400
+ ].filter((line) => typeof line === "string");
401
+ return `${lines2.join("\r\n")}\r
402
+ `;
403
+ }
404
+ const lines = [
405
+ "#!/bin/sh",
406
+ "set -eu",
407
+ `export PATH=${toShellLiteral(pathEntries.join(import_node_path2.default.delimiter))}`,
408
+ options.codexPath ? `export DEXLY_COMPANION_CODEX_PATH=${toShellLiteral(options.codexPath)}` : null,
409
+ `exec ${toShellLiteral(options.nodePath)} ${toShellLiteral(options.hostScriptPath)} "$@"`
410
+ ].filter((line) => typeof line === "string");
411
+ return `${lines.join("\n")}
412
+ `;
413
+ }
414
+ function defaultChromeManifestPath(options) {
415
+ switch (options.platform) {
416
+ case "darwin":
417
+ return import_node_path2.default.join(
418
+ options.homeDir,
419
+ "Library",
420
+ "Application Support",
421
+ "Google",
422
+ "Chrome",
423
+ "NativeMessagingHosts",
424
+ `${import_companion3.DEXLY_COMPANION_HOST_NAME}.json`
425
+ );
426
+ case "linux": {
427
+ const configHome = options.env.XDG_CONFIG_HOME?.trim() || import_node_path2.default.join(options.homeDir, ".config");
428
+ return import_node_path2.default.join(
429
+ configHome,
430
+ "google-chrome",
431
+ "NativeMessagingHosts",
432
+ `${import_companion3.DEXLY_COMPANION_HOST_NAME}.json`
433
+ );
434
+ }
435
+ case "win32":
436
+ return import_node_path2.default.join(options.installRoot, "native-host", `${import_companion3.DEXLY_COMPANION_HOST_NAME}.json`);
437
+ }
438
+ }
439
+ function toShellLiteral(value) {
440
+ return `'${value.replace(/'/g, `'"'"'`)}'`;
441
+ }
442
+ function toBatchLiteral(value) {
443
+ return value.replace(/%/g, "%%").replace(/"/g, '""');
444
+ }
445
+
446
+ // src/tooling.ts
447
+ var import_node_path3 = __toESM(require("node:path"));
448
+ var import_node_process2 = __toESM(require("node:process"));
449
+ var import_node_child_process = require("node:child_process");
450
+ function detectToolPaths() {
451
+ const nodePath = import_node_process2.default.execPath;
452
+ const npmPath = resolveExecutablePath("npm") ?? "npm";
453
+ const codexPath = resolveExecutablePath("codex");
454
+ return {
455
+ nodePath,
456
+ npmPath,
457
+ codexPath
458
+ };
459
+ }
460
+ function resolveExecutablePath(command, env = import_node_process2.default.env, platform = import_node_process2.default.platform) {
461
+ const lookupCommand = platform === "win32" ? "where" : "which";
462
+ const result = (0, import_node_child_process.spawnSync)(lookupCommand, [command], {
463
+ encoding: "utf8",
464
+ env
465
+ });
466
+ if (result.error || result.status !== 0) {
467
+ return null;
468
+ }
469
+ const executablePath = result.stdout.split(/\r?\n/).map((value) => value.trim()).find((value) => value.length > 0);
470
+ return executablePath ?? null;
471
+ }
472
+ function buildToolEnv(toolPaths, extraPaths = [], platform = import_node_process2.default.platform) {
473
+ const supportedPlatform = assertSupportedPlatform(platform);
474
+ const pathSeparator = supportedPlatform === "win32" ? ";" : ":";
475
+ const pathEntries = [
476
+ import_node_path3.default.dirname(toolPaths.nodePath),
477
+ import_node_path3.default.dirname(toolPaths.npmPath),
478
+ toolPaths.codexPath ? import_node_path3.default.dirname(toolPaths.codexPath) : null,
479
+ ...extraPaths,
480
+ ...defaultLauncherPathEntries(supportedPlatform, import_node_process2.default.env),
481
+ import_node_process2.default.env.PATH ?? null
482
+ ].filter((entry, index, entries) => typeof entry === "string" && entries.indexOf(entry) === index);
483
+ return {
484
+ ...import_node_process2.default.env,
485
+ PATH: pathEntries.join(pathSeparator),
486
+ ...toolPaths.codexPath ? { DEXLY_COMPANION_CODEX_PATH: toolPaths.codexPath } : {}
487
+ };
488
+ }
489
+ async function runCommand(command, args, options = {}) {
490
+ return await new Promise((resolve, reject) => {
491
+ const child = spawnManaged(command, args, {
492
+ cwd: options.cwd,
493
+ env: options.env,
494
+ stdio: "pipe"
495
+ });
496
+ let stdout = "";
497
+ let stderr = "";
498
+ child.stdout.on("data", (chunk) => {
499
+ stdout += (Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)).toString("utf8");
500
+ });
501
+ child.stderr.on("data", (chunk) => {
502
+ stderr += (Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)).toString("utf8");
503
+ });
504
+ child.once("error", (error) => {
505
+ reject(error);
506
+ });
507
+ child.once("exit", (code, signal) => {
508
+ if (code === 0) {
509
+ resolve({ stdout, stderr });
510
+ return;
511
+ }
512
+ const detail = stderr.trim() || stdout.trim();
513
+ reject(new Error(
514
+ detail.length > 0 ? `${command} ${args.join(" ")} failed (${code ?? signal ?? "unknown"}): ${detail}` : `${command} ${args.join(" ")} failed (${code ?? signal ?? "unknown"}).`
515
+ ));
516
+ });
517
+ });
518
+ }
519
+ function resolveCodexVersion(codexCommand = import_node_process2.default.env.DEXLY_COMPANION_CODEX_PATH ?? "codex", env = import_node_process2.default.env) {
520
+ const result = spawnManagedSync(codexCommand, ["--version"], {
521
+ encoding: "utf8",
522
+ env
523
+ });
524
+ if (result.error || result.status !== 0) {
525
+ return null;
526
+ }
527
+ return result.stdout.trim() || null;
528
+ }
529
+ function resolveNpmGlobalBinDir(npmPath, env = import_node_process2.default.env, platform = import_node_process2.default.platform) {
530
+ const result = spawnManagedSync(npmPath, ["config", "get", "prefix"], {
531
+ encoding: "utf8",
532
+ env
533
+ });
534
+ if (result.error || result.status !== 0) {
535
+ return null;
536
+ }
537
+ const prefix = result.stdout.trim();
538
+ if (prefix.length === 0) {
539
+ return null;
540
+ }
541
+ return npmGlobalBinDirFromPrefix(prefix, platform);
542
+ }
543
+ function spawnManaged(command, args, options = {}) {
544
+ const invocation = buildSpawnInvocation(command, args, options.env);
545
+ return (0, import_node_child_process.spawn)(invocation.command, invocation.args, {
546
+ ...options,
547
+ windowsVerbatimArguments: invocation.windowsVerbatimArguments
548
+ });
549
+ }
550
+ function spawnManagedSync(command, args, options = {}) {
551
+ const invocation = buildSpawnInvocation(command, args, options.env);
552
+ return (0, import_node_child_process.spawnSync)(invocation.command, invocation.args, {
553
+ ...options,
554
+ windowsVerbatimArguments: invocation.windowsVerbatimArguments
555
+ });
556
+ }
557
+ function buildSpawnInvocation(command, args, env = import_node_process2.default.env, platform = import_node_process2.default.platform) {
558
+ if (platform !== "win32" || !isWindowsCommandScript(command, platform)) {
559
+ return {
560
+ command,
561
+ args
562
+ };
563
+ }
564
+ return {
565
+ command: resolveWindowsCommandShell(env),
566
+ args: ["/d", "/s", "/c", quoteWindowsCommand([command, ...args])],
567
+ windowsVerbatimArguments: false
568
+ };
569
+ }
570
+ function isWindowsCommandScript(command, platform = import_node_process2.default.platform) {
571
+ if (platform !== "win32") {
572
+ return false;
573
+ }
574
+ const normalized = command.trim().toLowerCase();
575
+ return normalized.endsWith(".cmd") || normalized.endsWith(".bat");
576
+ }
577
+ function resolveWindowsCommandShell(env = import_node_process2.default.env) {
578
+ const comSpec = env.ComSpec?.trim();
579
+ if (comSpec) {
580
+ return comSpec;
581
+ }
582
+ const systemRoot = env.SystemRoot?.trim() || "C:\\Windows";
583
+ return import_node_path3.default.join(systemRoot, "System32", "cmd.exe");
584
+ }
585
+ function quoteWindowsCommand(parts) {
586
+ return parts.map(quoteWindowsArgument).join(" ");
587
+ }
588
+ function quoteWindowsArgument(value) {
589
+ if (value.length === 0) {
590
+ return '""';
591
+ }
592
+ if (!/[ \t"&()^<>|]/.test(value)) {
593
+ return value;
594
+ }
595
+ return `"${value.replace(/"/g, '""')}"`;
596
+ }
597
+ function npmGlobalBinDirFromPrefix(prefix, platform = import_node_process2.default.platform) {
598
+ return platform === "win32" ? prefix : import_node_path3.default.join(prefix, "bin");
599
+ }
600
+
226
601
  // src/doctor.ts
227
- async function runDoctor(options) {
228
- const installRoot = options?.installRoot ?? defaultInstallRoot();
229
- const manifestDir = options?.manifestDir ?? defaultChromeHostManifestDir();
230
- const manifestPath = installedHostManifestPath(manifestDir);
231
- const hostLauncherPath = installedHostLauncherPath(installRoot);
602
+ async function runDoctor(options = {}) {
603
+ const layout = resolvePlatformLayout(options);
604
+ const installRoot = layout.installRoot;
605
+ const manifestPath = layout.hostRegistration.manifestPath;
606
+ const hostLauncherPath = installedHostLauncherPath(installRoot, layout.platform);
232
607
  const hostScriptPath = installedHostScriptPath(installRoot);
233
608
  const metadataPath = installMetadataPath(installRoot);
234
609
  const checks = [];
235
610
  checks.push({
236
611
  label: "platform",
237
- ok: process.platform === "darwin",
238
- detail: process.platform === "darwin" ? "macOS supported." : `Unsupported platform: ${process.platform}`
612
+ ok: true,
613
+ detail: `${describePlatform(layout.platform)} supported.`
239
614
  });
615
+ checks.push(await checkHostRegistration(layout.hostRegistration, options.registryReader));
240
616
  checks.push(await checkPath("host launcher", hostLauncherPath));
241
617
  checks.push(await checkPath("host bundle", hostScriptPath));
242
618
  checks.push(await checkPath("install metadata", metadataPath));
243
619
  checks.push(await checkPath("native host manifest", manifestPath));
244
- const manifestCheck = await checkHostManifest(manifestPath, hostLauncherPath);
245
- checks.push(manifestCheck);
246
- checks.push(await checkNativeHostRuntime(manifestPath));
247
- const codexVersion = resolveCodexVersion();
248
- checks.push({
249
- label: "codex",
250
- ok: codexVersion != null,
251
- detail: codexVersion ?? "Codex CLI not found in PATH."
252
- });
620
+ checks.push(await checkHostManifest(manifestPath, hostLauncherPath));
621
+ checks.push(await checkNativeHostRuntime(layout.hostRegistration, options.registryReader));
622
+ checks.push(await checkCodexAvailability(installRoot, layout.platform));
253
623
  return {
254
624
  ok: checks.every((check) => check.ok),
255
625
  checks
@@ -257,7 +627,7 @@ async function runDoctor(options) {
257
627
  }
258
628
  async function checkPath(label, filePath) {
259
629
  try {
260
- await (0, import_promises.access)(filePath);
630
+ await (0, import_promises2.access)(filePath);
261
631
  return {
262
632
  label,
263
633
  ok: true,
@@ -271,16 +641,39 @@ async function checkPath(label, filePath) {
271
641
  };
272
642
  }
273
643
  }
644
+ async function checkHostRegistration(registration, registryReader) {
645
+ if (registration.kind === "manifest-file") {
646
+ return {
647
+ label: "native host registration",
648
+ ok: true,
649
+ detail: registration.manifestPath
650
+ };
651
+ }
652
+ const registeredManifestPath = await (registryReader ?? readWindowsRegistryManifestPath)(registration.registryKey ?? "");
653
+ if (!registeredManifestPath) {
654
+ return {
655
+ label: "native host registration",
656
+ ok: false,
657
+ detail: `Chrome registry key is missing or empty: ${registration.registryKey}`
658
+ };
659
+ }
660
+ const ok = registeredManifestPath === registration.manifestPath;
661
+ return {
662
+ label: "native host registration",
663
+ ok,
664
+ detail: ok ? `Registry points to ${registeredManifestPath}` : `Registry points to ${registeredManifestPath}, expected ${registration.manifestPath}`
665
+ };
666
+ }
274
667
  async function checkHostManifest(filePath, expectedHostPath) {
275
668
  try {
276
- const raw = await (0, import_promises.readFile)(filePath, "utf8");
669
+ const raw = await (0, import_promises2.readFile)(filePath, "utf8");
277
670
  const manifest = JSON.parse(raw);
278
671
  const allowedOrigins = Array.isArray(manifest.allowed_origins) ? manifest.allowed_origins.filter((value) => typeof value === "string") : [];
279
- const ok = manifest.name === import_companion2.DEXLY_COMPANION_HOST_NAME && typeof manifest.path === "string" && manifest.path === expectedHostPath && import_companion2.DEXLY_FIXED_ALLOWED_ORIGINS.every((origin) => allowedOrigins.includes(origin));
672
+ const ok = manifest.name === import_companion4.DEXLY_COMPANION_HOST_NAME && typeof manifest.path === "string" && manifest.path === expectedHostPath && import_companion4.DEXLY_FIXED_ALLOWED_ORIGINS.every((origin) => allowedOrigins.includes(origin));
280
673
  return {
281
674
  label: "manifest contents",
282
675
  ok,
283
- detail: ok ? `Native host manifest is valid, points to ${expectedHostPath}, and includes ${import_companion2.DEXLY_FIXED_ALLOWED_ORIGINS.join(", ")}` : "Native host manifest is missing the expected name, expected launcher path, or one of Dexly's fixed extension origins."
676
+ detail: ok ? `Native host manifest is valid, points to ${expectedHostPath}, and includes ${import_companion4.DEXLY_FIXED_ALLOWED_ORIGINS.join(", ")}` : "Native host manifest is missing the expected name, expected launcher path, or one of Dexly's fixed extension origins."
284
677
  };
285
678
  } catch (error) {
286
679
  return {
@@ -290,10 +683,18 @@ async function checkHostManifest(filePath, expectedHostPath) {
290
683
  };
291
684
  }
292
685
  }
293
- async function checkNativeHostRuntime(manifestPath) {
294
- const defaultOrigin = `chrome-extension://${import_companion2.DEXLY_DEV_EXTENSION_ID}/`;
686
+ async function checkNativeHostRuntime(registration, registryReader) {
687
+ const defaultOrigin = `chrome-extension://${import_companion4.DEXLY_DEV_EXTENSION_ID}/`;
688
+ const registeredManifestPath = registration.kind === "windows-registry" ? await (registryReader ?? readWindowsRegistryManifestPath)(registration.registryKey ?? "") : registration.manifestPath;
689
+ if (!registeredManifestPath) {
690
+ return {
691
+ label: "native host runtime",
692
+ ok: false,
693
+ detail: "Dexly Companion could not resolve the registered native host manifest path."
694
+ };
695
+ }
295
696
  try {
296
- const raw = await (0, import_promises.readFile)(manifestPath, "utf8");
697
+ const raw = await (0, import_promises2.readFile)(registeredManifestPath, "utf8");
297
698
  const manifest = JSON.parse(raw);
298
699
  if (typeof manifest.path !== "string" || manifest.path.length === 0) {
299
700
  return {
@@ -332,8 +733,32 @@ async function checkNativeHostRuntime(manifestPath) {
332
733
  };
333
734
  }
334
735
  }
736
+ async function checkCodexAvailability(installRoot, platform) {
737
+ const metadata = await loadInstallMetadata(installRoot);
738
+ const env = metadata ? buildToolEnv(metadata.tools, [], platform) : process.env;
739
+ const codexVersion = metadata ? resolveCodexVersion(metadata.tools.codexPath ?? void 0, env) : resolveCodexVersion(void 0, env);
740
+ if (codexVersion) {
741
+ return {
742
+ label: "codex",
743
+ ok: true,
744
+ detail: codexVersion
745
+ };
746
+ }
747
+ if (metadata?.tools.codexPath) {
748
+ return {
749
+ label: "codex",
750
+ ok: false,
751
+ detail: `Codex CLI was not available via stored install metadata (${metadata.tools.codexPath}).`
752
+ };
753
+ }
754
+ return {
755
+ label: "codex",
756
+ ok: false,
757
+ detail: "Codex CLI not found in stored install metadata or PATH."
758
+ };
759
+ }
335
760
  async function runHostHealthCheck(hostPath, origin) {
336
- const child = (0, import_node_child_process.spawn)(hostPath, [origin], {
761
+ const child = spawnManaged(hostPath, [origin], {
337
762
  stdio: ["pipe", "pipe", "pipe"]
338
763
  });
339
764
  const parser = new NativeMessageParser();
@@ -345,8 +770,8 @@ async function runHostHealthCheck(hostPath, origin) {
345
770
  }, 3e3);
346
771
  const cleanup = () => {
347
772
  clearTimeout(timeout);
348
- child.stdout.off("data", handleStdout);
349
- child.stderr.off("data", handleStderr);
773
+ child.stdout?.off("data", handleStdout);
774
+ child.stderr?.off("data", handleStderr);
350
775
  child.off("error", handleError);
351
776
  child.off("exit", handleExit);
352
777
  };
@@ -375,19 +800,19 @@ async function runHostHealthCheck(hostPath, origin) {
375
800
  stderrTail ? `Dexly Companion exited before replying (code ${code ?? "null"}, signal ${signal ?? "null"}): ${stderrTail}` : `Dexly Companion exited before replying (code ${code ?? "null"}, signal ${signal ?? "null"}).`
376
801
  ));
377
802
  };
378
- child.stdout.on("data", handleStdout);
379
- child.stderr.on("data", handleStderr);
803
+ child.stdout?.on("data", handleStdout);
804
+ child.stderr?.on("data", handleStderr);
380
805
  child.once("error", handleError);
381
806
  child.once("exit", handleExit);
382
807
  });
383
- child.stdin.write(
808
+ child.stdin?.write(
384
809
  encodeNativeMessage({
385
810
  kind: "host/health",
386
811
  requestId: "doctor-health"
387
812
  })
388
813
  );
389
814
  const response = await messagePromise;
390
- child.stdin.end();
815
+ child.stdin?.end();
391
816
  await Promise.race([
392
817
  (0, import_node_events.once)(child, "exit"),
393
818
  new Promise((resolve) => setTimeout(resolve, 250))
@@ -397,14 +822,21 @@ async function runHostHealthCheck(hostPath, origin) {
397
822
  child.kill();
398
823
  }
399
824
  }
400
- function resolveCodexVersion() {
401
- const result = (0, import_node_child_process.spawnSync)("codex", ["--version"], {
402
- encoding: "utf8"
403
- });
404
- if (result.error || result.status !== 0) {
825
+ async function readWindowsRegistryManifestPath(registryKey) {
826
+ if (!registryKey.trim()) {
827
+ return null;
828
+ }
829
+ try {
830
+ const result = await runCommand("reg.exe", ["query", registryKey, "/ve"]);
831
+ const match = result.stdout.split(/\r?\n/).map((line) => line.trim()).find((line) => line.includes("REG_SZ"));
832
+ if (!match) {
833
+ return null;
834
+ }
835
+ const [, value] = match.split(/REG_SZ/i);
836
+ return value?.trim() || null;
837
+ } catch {
405
838
  return null;
406
839
  }
407
- return result.stdout.trim() || null;
408
840
  }
409
841
 
410
842
  // src/host-runtime.ts
@@ -412,220 +844,62 @@ var import_node_process4 = __toESM(require("node:process"));
412
844
 
413
845
  // src/codex-host.ts
414
846
  var import_node_process3 = __toESM(require("node:process"));
415
- var import_node_child_process3 = require("node:child_process");
416
847
  var import_node_readline = __toESM(require("node:readline"));
417
- var import_companion5 = __toESM(require_companion());
848
+ var import_companion6 = __toESM(require_companion());
418
849
 
419
850
  // src/management.ts
420
851
  var import_promises4 = require("node:fs/promises");
421
- var import_companion4 = __toESM(require_companion());
422
-
423
- // src/install-metadata.ts
424
- var import_promises2 = require("node:fs/promises");
425
- var import_companion3 = __toESM(require_companion());
426
- function normalizeString(value) {
427
- return typeof value === "string" && value.trim().length > 0 ? value : null;
428
- }
429
- function normalizeToolPaths(value) {
430
- if (!value || typeof value !== "object") {
431
- return null;
432
- }
433
- const candidate = value;
434
- const nodePath = normalizeString(candidate.nodePath);
435
- const npmPath = normalizeString(candidate.npmPath);
436
- if (!nodePath || !npmPath) {
437
- return null;
438
- }
439
- return {
440
- nodePath,
441
- npmPath,
442
- codexPath: normalizeString(candidate.codexPath)
443
- };
444
- }
445
- async function loadInstallMetadata(installRoot) {
446
- try {
447
- const raw = await (0, import_promises2.readFile)(installMetadataPath(installRoot), "utf8");
448
- const parsed = JSON.parse(raw);
449
- const tools = normalizeToolPaths(parsed.tools);
450
- const currentVersion = normalizeString(parsed.currentVersion);
451
- const installedAt = normalizeString(parsed.installedAt);
452
- const updatedAt = normalizeString(parsed.updatedAt);
453
- if (!tools || !currentVersion || !installedAt || !updatedAt) {
454
- return null;
455
- }
456
- const knownGoodVersions = Array.isArray(parsed.knownGoodVersions) ? parsed.knownGoodVersions.filter((value) => typeof value === "string" && value.trim().length > 0) : [currentVersion];
457
- return {
458
- schemaVersion: 1,
459
- packageName: import_companion3.DEXLY_COMPANION_PACKAGE_NAME,
460
- executableName: import_companion3.DEXLY_COMPANION_EXECUTABLE_NAME,
461
- currentVersion,
462
- previousVersion: normalizeString(parsed.previousVersion),
463
- knownGoodVersions: knownGoodVersions.length > 0 ? knownGoodVersions : [currentVersion],
464
- currentChannel: normalizeString(parsed.currentChannel),
465
- installedAt,
466
- updatedAt,
467
- tools
468
- };
469
- } catch {
470
- return null;
471
- }
472
- }
473
- async function saveInstallMetadata(installRoot, metadata) {
474
- await (0, import_promises2.writeFile)(
475
- installMetadataPath(installRoot),
476
- `${JSON.stringify(metadata, null, 2)}
477
- `,
478
- "utf8"
479
- );
480
- }
852
+ var import_companion5 = __toESM(require_companion());
481
853
 
482
854
  // src/install.ts
483
855
  var import_promises3 = require("node:fs/promises");
484
- var import_node_path3 = __toESM(require("node:path"));
485
- var import_node_process2 = __toESM(require("node:process"));
486
-
487
- // src/tooling.ts
488
- var import_node_path2 = __toESM(require("node:path"));
489
- var import_node_process = __toESM(require("node:process"));
490
- var import_node_child_process2 = require("node:child_process");
491
- function detectToolPaths() {
492
- const nodePath = import_node_process.default.execPath;
493
- const npmPath = resolveExecutablePath("npm") ?? "npm";
494
- const codexPath = resolveExecutablePath("codex");
495
- return {
496
- nodePath,
497
- npmPath,
498
- codexPath
499
- };
500
- }
501
- function resolveExecutablePath(command, env = import_node_process.default.env) {
502
- const result = (0, import_node_child_process2.spawnSync)("which", [command], {
503
- encoding: "utf8",
504
- env
505
- });
506
- if (result.status !== 0) {
507
- return null;
508
- }
509
- const executablePath = result.stdout.trim();
510
- return executablePath.length > 0 ? executablePath : null;
511
- }
512
- function buildToolEnv(toolPaths, extraPaths = []) {
513
- const pathEntries = [
514
- import_node_path2.default.dirname(toolPaths.nodePath),
515
- import_node_path2.default.dirname(toolPaths.npmPath),
516
- toolPaths.codexPath ? import_node_path2.default.dirname(toolPaths.codexPath) : null,
517
- ...extraPaths,
518
- "/usr/local/bin",
519
- "/opt/homebrew/bin",
520
- "/usr/bin",
521
- "/bin",
522
- "/usr/sbin",
523
- "/sbin",
524
- import_node_process.default.env.PATH ?? null
525
- ].filter((entry, index, entries) => typeof entry === "string" && entries.indexOf(entry) === index);
526
- return {
527
- ...import_node_process.default.env,
528
- PATH: pathEntries.join(":"),
529
- ...toolPaths.codexPath ? { DEXLY_COMPANION_CODEX_PATH: toolPaths.codexPath } : {}
530
- };
531
- }
532
- async function runCommand(command, args, options = {}) {
533
- return await new Promise((resolve, reject) => {
534
- const child = (0, import_node_child_process2.spawn)(command, args, {
535
- cwd: options.cwd,
536
- env: options.env,
537
- stdio: "pipe"
538
- });
539
- let stdout = "";
540
- let stderr = "";
541
- child.stdout.on("data", (chunk) => {
542
- stdout += (Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)).toString("utf8");
543
- });
544
- child.stderr.on("data", (chunk) => {
545
- stderr += (Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)).toString("utf8");
546
- });
547
- child.once("error", (error) => {
548
- reject(error);
549
- });
550
- child.once("exit", (code, signal) => {
551
- if (code === 0) {
552
- resolve({ stdout, stderr });
553
- return;
554
- }
555
- const detail = stderr.trim() || stdout.trim();
556
- reject(new Error(
557
- detail.length > 0 ? `${command} ${args.join(" ")} failed (${code ?? signal ?? "unknown"}): ${detail}` : `${command} ${args.join(" ")} failed (${code ?? signal ?? "unknown"}).`
558
- ));
559
- });
560
- });
561
- }
562
- function resolveCodexVersion2(codexCommand = import_node_process.default.env.DEXLY_COMPANION_CODEX_PATH ?? "codex", env = import_node_process.default.env) {
563
- const result = (0, import_node_child_process2.spawnSync)(codexCommand, ["--version"], {
564
- encoding: "utf8",
565
- env
566
- });
567
- if (result.error || result.status !== 0) {
568
- return null;
569
- }
570
- return result.stdout.trim() || null;
571
- }
572
- function resolveNpmGlobalBinDir(npmPath, env = import_node_process.default.env) {
573
- const result = (0, import_node_child_process2.spawnSync)(npmPath, ["config", "get", "prefix"], {
574
- encoding: "utf8",
575
- env
576
- });
577
- if (result.error || result.status !== 0) {
578
- return null;
579
- }
580
- const prefix = result.stdout.trim();
581
- return prefix.length > 0 ? import_node_path2.default.join(prefix, "bin") : null;
582
- }
583
-
584
- // src/install.ts
856
+ var import_node_path4 = __toESM(require("node:path"));
585
857
  async function installCompanion(options) {
586
- if (import_node_process2.default.platform !== "darwin") {
587
- throw new Error("Dexly Companion phase one currently supports macOS only.");
588
- }
589
858
  await assertInstallablePackageExists(options.sourcePackageDir);
590
- const installRoot = options.installRoot ?? defaultInstallRoot();
591
- const manifestDir = options.manifestDir ?? defaultChromeHostManifestDir();
592
- const manifestPath = installedHostManifestPath(manifestDir);
593
- const hostPath = installedHostLauncherPath(installRoot);
859
+ const layout = resolvePlatformLayout(options);
860
+ const installRoot = layout.installRoot;
861
+ const manifestPath = layout.hostRegistration.manifestPath;
862
+ const hostPath = installedHostLauncherPath(installRoot, layout.platform);
594
863
  const allowedOrigins = defaultChromeAllowedOrigins();
595
864
  const toolPaths = options.toolPaths ?? detectToolPaths();
596
- const sourceDistDir = import_node_path3.default.join(options.sourcePackageDir, "dist");
597
- const sourcePackageJsonPath = import_node_path3.default.join(options.sourcePackageDir, "package.json");
865
+ const sourceDistDir = import_node_path4.default.join(options.sourcePackageDir, "dist");
866
+ const sourcePackageJsonPath = import_node_path4.default.join(options.sourcePackageDir, "package.json");
598
867
  const installVersion = await readInstallPackageVersion(sourcePackageJsonPath);
599
868
  const versionRoot = versionInstallPath(installRoot, installVersion);
600
869
  const stagingVersionRoot = `${versionRoot}.tmp-${Date.now()}-${Math.random().toString(16).slice(2)}`;
601
870
  await (0, import_promises3.mkdir)(versionStorePath(installRoot), { recursive: true });
602
871
  await (0, import_promises3.rm)(stagingVersionRoot, { recursive: true, force: true });
603
872
  try {
604
- await (0, import_promises3.mkdir)(import_node_path3.default.join(stagingVersionRoot, "dist"), { recursive: true });
605
- await (0, import_promises3.cp)(sourceDistDir, import_node_path3.default.join(stagingVersionRoot, "dist"), { recursive: true });
873
+ await (0, import_promises3.mkdir)(import_node_path4.default.join(stagingVersionRoot, "dist"), { recursive: true });
874
+ await (0, import_promises3.cp)(sourceDistDir, import_node_path4.default.join(stagingVersionRoot, "dist"), { recursive: true });
606
875
  await (0, import_promises3.copyFile)(sourcePackageJsonPath, versionedPackageJsonPath(stagingVersionRoot));
607
- await (0, import_promises3.chmod)(versionedCliPath(stagingVersionRoot), 493);
608
- await (0, import_promises3.chmod)(versionedHostScriptPath(stagingVersionRoot), 493);
609
- await (0, import_promises3.mkdir)(import_node_path3.default.dirname(versionedHostLauncherPath(stagingVersionRoot)), { recursive: true });
876
+ await chmodIfSupported(layout.platform, versionedCliPath(stagingVersionRoot));
877
+ await chmodIfSupported(layout.platform, versionedHostScriptPath(stagingVersionRoot));
878
+ await (0, import_promises3.mkdir)(import_node_path4.default.dirname(versionedHostLauncherPath(stagingVersionRoot, layout.platform)), { recursive: true });
610
879
  await (0, import_promises3.writeFile)(
611
- versionedHostLauncherPath(stagingVersionRoot),
880
+ versionedHostLauncherPath(stagingVersionRoot, layout.platform),
612
881
  renderHostLauncher({
882
+ platform: layout.platform,
613
883
  nodePath: toolPaths.nodePath,
614
884
  hostScriptPath: installedHostScriptPath(installRoot),
615
885
  codexPath: toolPaths.codexPath
616
886
  }),
617
887
  "utf8"
618
888
  );
619
- await (0, import_promises3.chmod)(versionedHostLauncherPath(stagingVersionRoot), 493);
889
+ await chmodIfSupported(layout.platform, versionedHostLauncherPath(stagingVersionRoot, layout.platform));
620
890
  await (0, import_promises3.rm)(versionRoot, { recursive: true, force: true });
621
891
  await (0, import_promises3.rename)(stagingVersionRoot, versionRoot);
622
892
  } catch (error) {
623
893
  await (0, import_promises3.rm)(stagingVersionRoot, { recursive: true, force: true }).catch(() => void 0);
624
894
  throw error;
625
895
  }
626
- await activateInstalledVersion(installRoot, installVersion);
627
- await rewriteCurrentLauncher(installRoot, toolPaths);
628
- await ensureChromeHostManifest(installRoot, manifestDir);
896
+ await activateInstalledVersion(installRoot, installVersion, { platform: layout.platform });
897
+ await rewriteCurrentLauncher(installRoot, toolPaths, { platform: layout.platform });
898
+ await ensureChromeHostRegistration(installRoot, {
899
+ ...options,
900
+ platform: layout.platform,
901
+ registryWriter: options.registryWriter
902
+ });
629
903
  const existingMetadata = await loadInstallMetadata(installRoot);
630
904
  const now = (/* @__PURE__ */ new Date()).toISOString();
631
905
  const metadata = {
@@ -651,13 +925,30 @@ async function installCompanion(options) {
651
925
  hostPath,
652
926
  allowedOrigins,
653
927
  version: installVersion,
654
- metadataPath: import_node_path3.default.join(installRoot, "install-metadata.json")
928
+ metadataPath: import_node_path4.default.join(installRoot, "install-metadata.json")
655
929
  };
656
930
  }
657
- async function activateInstalledVersion(installRoot, version) {
931
+ async function activateInstalledVersion(installRoot, version, options = {}) {
932
+ const layout = resolvePlatformLayout({
933
+ installRoot,
934
+ platform: options.platform
935
+ });
658
936
  const currentPath = currentInstallPath(installRoot);
937
+ const versionPath = versionInstallPath(installRoot, version);
938
+ if (layout.activationStrategy === "copy") {
939
+ const stagingCurrentPath = `${currentPath}.tmp-${Date.now()}-${Math.random().toString(16).slice(2)}`;
940
+ await (0, import_promises3.rm)(stagingCurrentPath, { recursive: true, force: true });
941
+ await (0, import_promises3.cp)(versionPath, stagingCurrentPath, { recursive: true });
942
+ try {
943
+ await (0, import_promises3.rename)(stagingCurrentPath, currentPath);
944
+ } catch {
945
+ await (0, import_promises3.rm)(currentPath, { recursive: true, force: true });
946
+ await (0, import_promises3.rename)(stagingCurrentPath, currentPath);
947
+ }
948
+ return;
949
+ }
659
950
  const tempLinkPath = `${currentPath}.tmp-${Date.now()}-${Math.random().toString(16).slice(2)}`;
660
- const targetPath = import_node_path3.default.join("versions", version);
951
+ const targetPath = import_node_path4.default.join("versions", version);
661
952
  await (0, import_promises3.mkdir)(installRoot, { recursive: true });
662
953
  await (0, import_promises3.rm)(tempLinkPath, { recursive: true, force: true });
663
954
  await (0, import_promises3.symlink)(targetPath, tempLinkPath);
@@ -668,30 +959,51 @@ async function activateInstalledVersion(installRoot, version) {
668
959
  await (0, import_promises3.rename)(tempLinkPath, currentPath);
669
960
  }
670
961
  }
671
- async function rewriteCurrentLauncher(installRoot, toolPaths) {
672
- const launcherPath = installedHostLauncherPath(installRoot);
673
- await (0, import_promises3.mkdir)(import_node_path3.default.dirname(launcherPath), { recursive: true });
962
+ async function rewriteCurrentLauncher(installRoot, toolPaths, options = {}) {
963
+ const layout = resolvePlatformLayout({
964
+ installRoot,
965
+ platform: options.platform
966
+ });
967
+ const launcherPath = installedHostLauncherPath(installRoot, layout.platform);
968
+ await (0, import_promises3.mkdir)(import_node_path4.default.dirname(launcherPath), { recursive: true });
674
969
  await (0, import_promises3.writeFile)(
675
970
  launcherPath,
676
971
  renderHostLauncher({
972
+ platform: layout.platform,
677
973
  nodePath: toolPaths.nodePath,
678
974
  hostScriptPath: installedHostScriptPath(installRoot),
679
975
  codexPath: toolPaths.codexPath
680
976
  }),
681
977
  "utf8"
682
978
  );
683
- await (0, import_promises3.chmod)(launcherPath, 493);
979
+ await chmodIfSupported(layout.platform, launcherPath);
684
980
  return launcherPath;
685
981
  }
686
- async function ensureChromeHostManifest(installRoot, manifestDir) {
687
- const manifestPath = installedHostManifestPath(manifestDir);
688
- await (0, import_promises3.mkdir)(manifestDir, { recursive: true });
982
+ async function ensureChromeHostRegistration(installRoot, options = {}) {
983
+ const layout = resolvePlatformLayout({
984
+ installRoot,
985
+ platform: options.platform,
986
+ manifestDir: options.manifestDir,
987
+ windowsRegistryKey: options.windowsRegistryKey
988
+ });
989
+ const manifestPath = layout.hostRegistration.manifestPath;
990
+ await (0, import_promises3.mkdir)(import_node_path4.default.dirname(manifestPath), { recursive: true });
689
991
  await (0, import_promises3.writeFile)(
690
992
  manifestPath,
691
- `${JSON.stringify(buildChromeHostManifest(installedHostLauncherPath(installRoot), defaultChromeAllowedOrigins()), null, 2)}
993
+ `${JSON.stringify(
994
+ buildChromeHostManifest(installedHostLauncherPath(installRoot, layout.platform), layout.hostRegistration.allowedOrigins),
995
+ null,
996
+ 2
997
+ )}
692
998
  `,
693
999
  "utf8"
694
1000
  );
1001
+ if (layout.hostRegistration.kind === "windows-registry" && layout.hostRegistration.registryKey) {
1002
+ await (options.registryWriter ?? writeWindowsRegistryManifestPath)(
1003
+ layout.hostRegistration.registryKey,
1004
+ manifestPath
1005
+ );
1006
+ }
695
1007
  return manifestPath;
696
1008
  }
697
1009
  async function pruneVersionStore(installRoot, keepVersions) {
@@ -702,14 +1014,14 @@ async function pruneVersionStore(installRoot, keepVersions) {
702
1014
  if (keep.has(entry)) {
703
1015
  return;
704
1016
  }
705
- await (0, import_promises3.rm)(import_node_path3.default.join(versionsPath, entry), { recursive: true, force: true });
1017
+ await (0, import_promises3.rm)(import_node_path4.default.join(versionsPath, entry), { recursive: true, force: true });
706
1018
  }));
707
1019
  }
708
1020
  async function assertInstallablePackageExists(sourcePackageDir) {
709
1021
  const requiredPaths = [
710
- import_node_path3.default.join(sourcePackageDir, "dist", "cli.js"),
711
- import_node_path3.default.join(sourcePackageDir, "dist", "host.js"),
712
- import_node_path3.default.join(sourcePackageDir, "package.json")
1022
+ import_node_path4.default.join(sourcePackageDir, "dist", "cli.js"),
1023
+ import_node_path4.default.join(sourcePackageDir, "dist", "host.js"),
1024
+ import_node_path4.default.join(sourcePackageDir, "package.json")
713
1025
  ];
714
1026
  for (const filePath of requiredPaths) {
715
1027
  try {
@@ -728,29 +1040,23 @@ async function readInstallPackageVersion(packageJsonPath) {
728
1040
  }
729
1041
  return packageJson.version.trim();
730
1042
  }
731
- function renderHostLauncher(options) {
732
- const pathEntries = [
733
- import_node_path3.default.dirname(options.nodePath),
734
- options.codexPath ? import_node_path3.default.dirname(options.codexPath) : null,
735
- "/usr/local/bin",
736
- "/opt/homebrew/bin",
737
- "/usr/bin",
738
- "/bin",
739
- "/usr/sbin",
740
- "/sbin"
741
- ].filter((entry, index, entries) => typeof entry === "string" && entries.indexOf(entry) === index);
742
- const lines = [
743
- "#!/bin/sh",
744
- "set -eu",
745
- `export PATH=${toShellLiteral(pathEntries.join(":"))}`,
746
- options.codexPath ? `export DEXLY_COMPANION_CODEX_PATH=${toShellLiteral(options.codexPath)}` : null,
747
- `exec ${toShellLiteral(options.nodePath)} ${toShellLiteral(options.hostScriptPath)} "$@"`
748
- ].filter((line) => typeof line === "string");
749
- return `${lines.join("\n")}
750
- `;
1043
+ async function chmodIfSupported(platform, targetPath) {
1044
+ if (platform === "win32") {
1045
+ return;
1046
+ }
1047
+ await (0, import_promises3.chmod)(targetPath, 493);
751
1048
  }
752
- function toShellLiteral(value) {
753
- return `'${value.replace(/'/g, `'"'"'`)}'`;
1049
+ async function writeWindowsRegistryManifestPath(registryKey, manifestPath) {
1050
+ await runCommand("reg.exe", [
1051
+ "add",
1052
+ registryKey,
1053
+ "/ve",
1054
+ "/t",
1055
+ "REG_SZ",
1056
+ "/d",
1057
+ manifestPath,
1058
+ "/f"
1059
+ ]);
754
1060
  }
755
1061
 
756
1062
  // src/management.ts
@@ -771,19 +1077,19 @@ function nextMetadata(metadata, nextVersion, channel) {
771
1077
  };
772
1078
  }
773
1079
  async function upgradeInstalledCompanion(options) {
774
- const installRoot = options.installRoot ?? defaultInstallRoot();
1080
+ const installRoot = options.installRoot ?? defaultInstallRoot({ platform: options.platform });
775
1081
  const metadata = await loadInstallMetadata(installRoot);
776
1082
  if (!metadata) {
777
1083
  throw new Error("Dexly Companion install metadata is missing. Reinstall Dexly Companion first.");
778
1084
  }
779
1085
  const specifier = options.version?.trim() ? options.version.trim() : options.distTag.trim();
780
- const targetSpecifier = `${import_companion4.DEXLY_COMPANION_PACKAGE_NAME}@${specifier}`;
781
- const env = buildToolEnv(metadata.tools);
1086
+ const targetSpecifier = `${import_companion5.DEXLY_COMPANION_PACKAGE_NAME}@${specifier}`;
1087
+ const env = buildToolEnv(metadata.tools, [], options.platform);
782
1088
  await runCommand(metadata.tools.npmPath, [
783
1089
  "exec",
784
1090
  "--yes",
785
1091
  `--package=${targetSpecifier}`,
786
- import_companion4.DEXLY_COMPANION_EXECUTABLE_NAME,
1092
+ import_companion5.DEXLY_COMPANION_EXECUTABLE_NAME,
787
1093
  "install",
788
1094
  "--channel",
789
1095
  options.distTag
@@ -799,8 +1105,7 @@ async function upgradeInstalledCompanion(options) {
799
1105
  };
800
1106
  }
801
1107
  async function rollbackInstalledCompanion(options) {
802
- const installRoot = options?.installRoot ?? defaultInstallRoot();
803
- const manifestDir = options?.manifestDir ?? defaultChromeHostManifestDir();
1108
+ const installRoot = options?.installRoot ?? defaultInstallRoot({ platform: options?.platform });
804
1109
  const metadata = await loadInstallMetadata(installRoot);
805
1110
  if (!metadata) {
806
1111
  throw new Error("Dexly Companion install metadata is missing. Reinstall Dexly Companion first.");
@@ -810,9 +1115,13 @@ async function rollbackInstalledCompanion(options) {
810
1115
  throw new Error("Dexly Companion does not have a previous known-good version to roll back to.");
811
1116
  }
812
1117
  await (0, import_promises4.access)(versionInstallPath(installRoot, previousVersion));
813
- await activateInstalledVersion(installRoot, previousVersion);
814
- await rewriteCurrentLauncher(installRoot, metadata.tools);
815
- await ensureChromeHostManifest(installRoot, manifestDir);
1118
+ await activateInstalledVersion(installRoot, previousVersion, { platform: options?.platform });
1119
+ await rewriteCurrentLauncher(installRoot, metadata.tools, { platform: options?.platform });
1120
+ await ensureChromeHostRegistration(installRoot, {
1121
+ platform: options?.platform,
1122
+ manifestDir: options?.manifestDir,
1123
+ windowsRegistryKey: options?.windowsRegistryKey
1124
+ });
816
1125
  const next = nextMetadata(metadata, previousVersion, metadata.currentChannel);
817
1126
  next.previousVersion = metadata.currentVersion;
818
1127
  await saveInstallMetadata(installRoot, next);
@@ -822,16 +1131,15 @@ async function rollbackInstalledCompanion(options) {
822
1131
  };
823
1132
  }
824
1133
  async function installCodexWithCompanion(options) {
825
- const installRoot = options?.installRoot ?? defaultInstallRoot();
826
- const manifestDir = options?.manifestDir ?? defaultChromeHostManifestDir();
1134
+ const installRoot = options?.installRoot ?? defaultInstallRoot({ platform: options?.platform });
827
1135
  const existingMetadata = await loadInstallMetadata(installRoot);
828
1136
  const toolPaths = existingMetadata?.tools ?? detectToolPaths();
829
- const env = buildToolEnv(toolPaths);
1137
+ const env = buildToolEnv(toolPaths, [], options?.platform);
830
1138
  await runCommand(toolPaths.npmPath, ["install", "-g", "@openai/codex"], { env });
831
- const globalBinDir = resolveNpmGlobalBinDir(toolPaths.npmPath, env);
832
- const nextEnv = buildToolEnv(toolPaths, globalBinDir ? [globalBinDir] : []);
833
- const codexPath = resolveExecutablePath("codex", nextEnv);
834
- const codexVersion = resolveCodexVersion2(codexPath ?? void 0, nextEnv);
1139
+ const globalBinDir = resolveNpmGlobalBinDir(toolPaths.npmPath, env, options?.platform);
1140
+ const nextEnv = buildToolEnv(toolPaths, globalBinDir ? [globalBinDir] : [], options?.platform);
1141
+ const codexPath = resolveExecutablePath("codex", nextEnv, options?.platform);
1142
+ const codexVersion = resolveCodexVersion(codexPath ?? void 0, nextEnv);
835
1143
  if (!codexPath || !codexVersion) {
836
1144
  throw new Error("Codex install completed, but Dexly Companion could not resolve the installed codex binary.");
837
1145
  }
@@ -839,8 +1147,12 @@ async function installCodexWithCompanion(options) {
839
1147
  ...toolPaths,
840
1148
  codexPath
841
1149
  };
842
- await rewriteCurrentLauncher(installRoot, nextToolPaths);
843
- await ensureChromeHostManifest(installRoot, manifestDir);
1150
+ await rewriteCurrentLauncher(installRoot, nextToolPaths, { platform: options?.platform });
1151
+ await ensureChromeHostRegistration(installRoot, {
1152
+ platform: options?.platform,
1153
+ manifestDir: options?.manifestDir,
1154
+ windowsRegistryKey: options?.windowsRegistryKey
1155
+ });
844
1156
  if (existingMetadata) {
845
1157
  await saveInstallMetadata(installRoot, {
846
1158
  ...existingMetadata,
@@ -851,7 +1163,7 @@ async function installCodexWithCompanion(options) {
851
1163
  return {
852
1164
  codexPath,
853
1165
  codexVersion,
854
- installCommand: import_companion4.DEXLY_CODEX_INSTALL_COMMAND
1166
+ installCommand: import_companion5.DEXLY_CODEX_INSTALL_COMMAND
855
1167
  };
856
1168
  }
857
1169
 
@@ -869,8 +1181,8 @@ var DexlyNativeHost = class {
869
1181
  stderrLines = [];
870
1182
  constructor(options) {
871
1183
  this.send = options.send;
872
- this.spawnProcess = options.spawnProcess ?? import_node_child_process3.spawn;
873
- this.spawnProcessSync = options.spawnProcessSync ?? import_node_child_process3.spawnSync;
1184
+ this.spawnProcess = options.spawnProcess ?? ((command, args, spawnOptions) => spawnManaged(command, [...args], spawnOptions));
1185
+ this.spawnProcessSync = options.spawnProcessSync ?? ((command, args, spawnOptions) => spawnManagedSync(command, [...args], spawnOptions));
874
1186
  this.codexCommand = options.codexCommand ?? import_node_process3.default.env.DEXLY_COMPANION_CODEX_PATH ?? "codex";
875
1187
  this.requestProcessExit = options.requestProcessExit ?? null;
876
1188
  }
@@ -1131,7 +1443,7 @@ var DexlyNativeHost = class {
1131
1443
  null,
1132
1444
  null,
1133
1445
  "codex_not_connected",
1134
- `${import_companion5.DEXLY_COMPANION_DISPLAY_NAME} is not connected to Codex app-server.`
1446
+ `${import_companion6.DEXLY_COMPANION_DISPLAY_NAME} is not connected to Codex app-server.`
1135
1447
  ));
1136
1448
  return;
1137
1449
  }
@@ -1266,7 +1578,7 @@ async function runHost() {
1266
1578
  async function runInstall(args) {
1267
1579
  const parsed = parseArgs(args);
1268
1580
  const result = await installCompanion({
1269
- sourcePackageDir: import_node_path4.default.resolve(__dirname, ".."),
1581
+ sourcePackageDir: import_node_path5.default.resolve(__dirname, ".."),
1270
1582
  installRoot: parsed.installRoot,
1271
1583
  manifestDir: parsed.manifestDir,
1272
1584
  channel: parsed.channel
@@ -1294,7 +1606,7 @@ async function runDoctorCommand(args) {
1294
1606
  }
1295
1607
  if (!result.ok) {
1296
1608
  import_node_process5.default.stdout.write(`
1297
- If Dexly Companion is missing, run ${import_companion6.DEXLY_COMPANION_INSTALL_COMMAND}.
1609
+ If Dexly Companion is missing, run ${import_companion7.DEXLY_COMPANION_INSTALL_COMMAND}.
1298
1610
  `);
1299
1611
  import_node_process5.default.exitCode = 1;
1300
1612
  }