@raidiant/notifai 1.0.0 → 1.1.0

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 (49) hide show
  1. package/CHANGELOG.md +26 -1
  2. package/README.md +17 -7
  3. package/dist/commands.d.ts +2 -0
  4. package/dist/commands.js +43 -38
  5. package/dist/commands.js.map +1 -1
  6. package/dist/config.d.ts +2 -2
  7. package/dist/config.js +5 -9
  8. package/dist/config.js.map +1 -1
  9. package/dist/credentials.d.ts +54 -3
  10. package/dist/credentials.js +237 -18
  11. package/dist/credentials.js.map +1 -1
  12. package/dist/file-lock.js +15 -6
  13. package/dist/file-lock.js.map +1 -1
  14. package/dist/hook-adapter.d.ts +6 -2
  15. package/dist/hook-adapter.js +110 -29
  16. package/dist/hook-adapter.js.map +1 -1
  17. package/dist/hooks.js +2 -1
  18. package/dist/hooks.js.map +1 -1
  19. package/dist/install-hooks.d.ts +54 -22
  20. package/dist/install-hooks.js +125 -56
  21. package/dist/install-hooks.js.map +1 -1
  22. package/dist/native-skills.js +5 -10
  23. package/dist/native-skills.js.map +1 -1
  24. package/dist/opencode-plugin.d.ts +7 -2
  25. package/dist/opencode-plugin.js +142 -4
  26. package/dist/opencode-plugin.js.map +1 -1
  27. package/dist/platform.d.ts +73 -0
  28. package/dist/platform.js +143 -0
  29. package/dist/platform.js.map +1 -0
  30. package/dist/ui/config-view.js +5 -1
  31. package/dist/ui/config-view.js.map +1 -1
  32. package/package.json +3 -3
  33. package/src/commands.test.ts +144 -34
  34. package/src/commands.ts +65 -35
  35. package/src/config.ts +11 -9
  36. package/src/credentials.test.ts +291 -0
  37. package/src/credentials.ts +274 -17
  38. package/src/file-lock.ts +34 -9
  39. package/src/hook-adapter.test.ts +136 -0
  40. package/src/hook-adapter.ts +120 -28
  41. package/src/hooks.test.ts +27 -1
  42. package/src/hooks.ts +2 -1
  43. package/src/install-hooks.test.ts +188 -27
  44. package/src/install-hooks.ts +177 -52
  45. package/src/native-skills.ts +5 -10
  46. package/src/opencode-plugin.ts +148 -3
  47. package/src/platform.test.ts +152 -0
  48. package/src/platform.ts +173 -0
  49. package/src/ui/config-view.ts +5 -5
package/CHANGELOG.md CHANGED
@@ -8,8 +8,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
8
8
  This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
9
9
  and [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/).
10
10
 
11
+ ## [1.1.0](https://github.com/Raidiant-io/notifai/compare/v1.0.1...v1.1.0) (2026-08-13)
12
+
13
+
14
+ ### Features
15
+
16
+ * **cli:** support Windows and Linux harness workflows ([779ab6b](https://github.com/Raidiant-io/notifai/commit/779ab6b075900a7a431b809ddcb715fd80836688))
17
+
18
+
19
+ ### Bug Fixes
20
+
21
+ * **cli:** cover Windows terminal behavior ([cda7abd](https://github.com/Raidiant-io/notifai/commit/cda7abd134744b2cac44204dd3eb1b20b9038edc))
22
+ * **cli:** isolate Codex hooks and reply routing ([#1](https://github.com/Raidiant-io/notifai/issues/1)) ([d060b6a](https://github.com/Raidiant-io/notifai/commit/d060b6a18b5937d82f74619a6c1bc711243be26c))
23
+ * **cli:** keep Codex hook install on ~/.codex, ignore CODEX_HOME ([3f75f13](https://github.com/Raidiant-io/notifai/commit/3f75f131c24534d3813a336bb72422f78e24a972))
24
+ * **cli:** preserve Codex hook trust on reinstall ([#3](https://github.com/Raidiant-io/notifai/issues/3)) ([5e2c2f4](https://github.com/Raidiant-io/notifai/commit/5e2c2f4d618276573f810a989f41a89ab1053564))
25
+ * **cli:** report observed setup receipts ([99d04bb](https://github.com/Raidiant-io/notifai/commit/99d04bbd487681db3023468fbd88b77a1ba13ceb))
26
+ * **cli:** stabilize Linux portability checks ([1462693](https://github.com/Raidiant-io/notifai/commit/146269306ae3dfc91f3559cce802460d3e058925))
27
+
11
28
  ## [Unreleased]
12
29
 
30
+ ## [1.0.1] - 2026-08-13
31
+
32
+ ### Fixed
33
+
34
+ - Codex hook installation writes inline `[hooks]` in `config.toml`. It does not create the legacy `hooks.json`.
35
+ - Hook installation is one scope: project or machine-global, not both. A project install is a no-op when global already covers the machine.
36
+
13
37
  ## [1.0.0] - 2026-08-13
14
38
 
15
39
  ### Removed
@@ -146,7 +170,8 @@ and [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/).
146
170
 
147
171
  - First public release of the `notifai` CLI under Apache-2.0.
148
172
 
149
- [unreleased]: https://github.com/Raidiant-io/notifai/compare/v1.0.0...HEAD
173
+ [unreleased]: https://github.com/Raidiant-io/notifai/compare/v1.0.1...HEAD
174
+ [1.0.1]: https://github.com/Raidiant-io/notifai/compare/v1.0.0...v1.0.1
150
175
  [1.0.0]: https://github.com/Raidiant-io/notifai/compare/v0.5.1...v1.0.0
151
176
  [0.5.1]: https://github.com/Raidiant-io/notifai/compare/v0.5.0...v0.5.1
152
177
  [0.5.0]: https://github.com/Raidiant-io/notifai/compare/v0.4.0...v0.5.0
package/README.md CHANGED
@@ -56,6 +56,10 @@ For a harness that cannot resume an idle agent turn, `notifai send
56
56
 
57
57
  ## Agent harnesses
58
58
 
59
+ The CLI and managed hook adapters support macOS, Linux, and Windows. Receiving
60
+ still requires the current iPhone or macOS Companion App; native Windows and
61
+ Linux Companion Apps are not shipped yet.
62
+
59
63
  ```sh
60
64
  notifai hooks install
61
65
  ```
@@ -67,13 +71,19 @@ or add one it did not see. This is what lets an agent's question reach
67
71
  your phone without the agent having to cooperate — no question detection,
68
72
  no state in a context window that compaction will eat.
69
73
 
70
- Every harness definition calls one stable user-level adapter at
71
- `~/.notifai/bin/hook-adapter`. Node, package manager, CLI version, checkout,
72
- XDG directories, and notification preferences are resolved behind it, so
73
- upgrades and configuration changes do not rewrite trusted hook definitions.
74
- The first migration may require one Codex `/hooks` approval; later repairs keep
75
- the same identity. Codex's Stop definition omits its timeout so the host default
76
- remains authoritative; prompt-submit and session-end retain fixed short limits.
74
+ Every harness definition calls one stable user-level adapter under the account
75
+ home (`~/.notifai/bin/hook-adapter` on macOS/Linux and the corresponding user
76
+ profile path on Windows). Node, package manager, CLI version, checkout,
77
+ configuration directories, and notification preferences are resolved behind
78
+ it, so upgrades and configuration changes do not rewrite trusted hook
79
+ definitions. Windows invokes its JavaScript adapter through the registered Node
80
+ executable; macOS/Linux retain the POSIX adapter and its stable command bytes.
81
+ Claude Code Stop returns immediately into the live inbox route on macOS/Linux;
82
+ on Windows, where upstream exposes no inbox socket, Stop stays open and returns
83
+ the accepted answer through the harness continuation. The first migration may
84
+ require one Codex `/hooks` approval; later repairs keep the same identity.
85
+ Codex's Stop definition omits its timeout so the host default remains
86
+ authoritative; prompt-submit and session-end retain fixed short limits.
77
87
 
78
88
  `notifai doctor` compares Codex trust on a best-effort basis against Codex's
79
89
  current persisted representation. Notifai never writes that trust store;
@@ -61,6 +61,8 @@ export interface CommandDeps {
61
61
  hookAdapterHome?: string;
62
62
  /** Test seam; production uses this process's Node and CLI paths. */
63
63
  hookInstallTarget?: HookAdapterTarget;
64
+ /** Test seam; production uses process.platform for adapter format and quoting. */
65
+ hookPlatform?: NodeJS.Platform;
64
66
  /** Test seam; production uses fetch against base_url. */
65
67
  clientFactory?: (baseUrl: string, bearer: string | null, options?: ClientOptions) => ApiClient;
66
68
  /** Test seam for bounded polling without wall-clock sleeps. */
package/dist/commands.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import { createHash, randomBytes } from 'node:crypto';
2
- import { spawn } from 'node:child_process';
3
2
  import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, rmSync, unlinkSync, writeFileSync } from 'node:fs';
4
3
  import os from 'node:os';
5
4
  import path from 'node:path';
@@ -7,7 +6,7 @@ import { parse as parseToml, stringify as stringifyToml } from 'smol-toml';
7
6
  import { CAPABILITIES_V1, QUESTION_TEXT_MAX_LENGTH, REPLY_MAX_QUESTIONS, REPLY_MAX_WINDOW_SECONDS, validateDraft, } from '@raidiant/notifai-protocol';
8
7
  import { sha256Hex } from '@raidiant/notifai-protocol/node';
9
8
  import { ApiCallError, NetworkError, createClient, isRetryableReplyPollError, } from './client.js';
10
- import { BOOLEAN_CONFIG_KEYS, CONFIG_KEYS, LOG_LEVELS, NUMERIC_CONFIG_KEYS, configBounds, configDefaultValue, findProjectConfigPath, personalProjectConfigPath, globalConfigPath, loadConfig, sessionConfigPath, } from './config.js';
9
+ import { BOOLEAN_CONFIG_KEYS, CONFIG_KEYS, LOG_LEVELS, NUMERIC_CONFIG_KEYS, configBounds, configDefaultValue, findProjectConfigPath, personalProjectConfigPath, globalConfigPath, loadConfig, sessionConfigPath, stateDir, } from './config.js';
11
10
  import { acceptedValues, configInfo } from './config-schema.js';
12
11
  import { packageVersion, skillsSource } from './release.js';
13
12
  import { atomicWriteFileSync } from './atomic-file.js';
@@ -23,6 +22,7 @@ import { codexWakeRoute, inspectCodexResume, } from './codex-wake.js';
23
22
  import { inspectHookAdapter, installHookAdapter, isNpxAdapterTarget, } from './hook-adapter.js';
24
23
  import { isOurOpencodePlugin, opencodePluginSource, } from './opencode-plugin.js';
25
24
  import { CHOICE_USAGE, buildDraft, formatReceipt, parseChoices, receiptExitCode, slugify, } from './send.js';
25
+ import { openUrl } from './platform.js';
26
26
  /** The invocation's logger, or one that discards everything. */
27
27
  export function log(deps) {
28
28
  return deps.logger ?? nullLogger();
@@ -1128,6 +1128,8 @@ function stopWakeRoute(deps, harness, sessionId, cwd) {
1128
1128
  if (sessionId === undefined)
1129
1129
  return undefined;
1130
1130
  if (harness === 'claude-code') {
1131
+ if ((deps.hookPlatform ?? process.platform) === 'win32')
1132
+ return undefined;
1131
1133
  return claudeWakeRoute({
1132
1134
  sessionId,
1133
1135
  cwd,
@@ -1316,7 +1318,7 @@ export function askCommand(deps, question, flags) {
1316
1318
  const { active, contested } = resolveActiveHarness(deps.env, deps.cwd, now);
1317
1319
  let sessionId;
1318
1320
  if (active !== null) {
1319
- const installations = findInstallations(deps.cwd, deps.env, deps.hookAdapterHome);
1321
+ const installations = findInstallations(deps.cwd, deps.env, deps.hookAdapterHome, deps.hookPlatform);
1320
1322
  const activeInstalled = installations.some((installation) => installation.harness === active.harness);
1321
1323
  if (!activeInstalled) {
1322
1324
  for (const line of diagnoseActiveHarnessSession(active, 'not-installed', installations, contested)) {
@@ -1518,11 +1520,11 @@ function activeQuestionRouteProblems(deps, active, installations) {
1518
1520
  }
1519
1521
  }
1520
1522
  }
1521
- for (const problem of inspectHookAdapter(deps.hookAdapterHome).problems) {
1523
+ for (const problem of inspectHookAdapter(deps.hookAdapterHome, deps.hookPlatform).problems) {
1522
1524
  problems.push(problem);
1523
1525
  }
1524
1526
  for (const installation of matching)
1525
- problems.push(...stopShapeProblems(installation));
1527
+ problems.push(...stopShapeProblems(installation, deps.hookPlatform));
1526
1528
  problems.push(...codexTrustProblems(matching, deps.env));
1527
1529
  return [...new Set(problems)];
1528
1530
  }
@@ -1574,7 +1576,7 @@ function diagnoseActiveHarnessSession(active, problem, installations, contested
1574
1576
  * checked after a prompt before assuming that a new session is required.
1575
1577
  */
1576
1578
  function diagnoseMissingSession(deps) {
1577
- const installations = findInstallations(deps.cwd, deps.env, deps.hookAdapterHome);
1579
+ const installations = findInstallations(deps.cwd, deps.env, deps.hookAdapterHome, deps.hookPlatform);
1578
1580
  if (installations.length === 0) {
1579
1581
  return [
1580
1582
  'Could not tell which harness session this is: no Notifai hooks are installed for this project.',
@@ -1687,21 +1689,38 @@ export function hooksInstallCommand(deps, flags) {
1687
1689
  return EXIT.usage;
1688
1690
  const adapterTarget = resolveHookAdapterTarget(deps, flags);
1689
1691
  const scriptPath = flags.scriptPath ?? fileHookInstallTarget(adapterTarget)?.scriptPath ?? process.argv[1] ?? 'notifai';
1692
+ const hookPlatform = deps.hookPlatform ?? process.platform;
1693
+ const nodePath = adapterTarget.execPath;
1690
1694
  let adapterPath;
1691
1695
  try {
1692
- adapterPath = installHookAdapter(adapterTarget, deps.hookAdapterHome).path;
1696
+ adapterPath = installHookAdapter(adapterTarget, deps.hookAdapterHome, hookPlatform).path;
1693
1697
  }
1694
1698
  catch (err) {
1695
1699
  deps.io.err(`Could not prepare the stable hook adapter: ${String(err)}`);
1696
1700
  return EXIT.failed;
1697
1701
  }
1698
- const file = settingsFile(harness, flags.global ?? false, deps.cwd, deps.env);
1702
+ const wantGlobal = flags.global === true;
1703
+ const existing = findInstallations(deps.cwd, deps.env, deps.hookAdapterHome, deps.hookPlatform).filter((installation) => installation.harness === harness);
1704
+ const otherScope = existing.filter((installation) => installation.global !== wantGlobal);
1705
+ if (!wantGlobal && otherScope.some((installation) => installation.global)) {
1706
+ const globalFile = otherScope.find((installation) => installation.global)?.file;
1707
+ deps.io.out(`${HARNESS_LABELS[harness]} hooks already cover this machine (${globalFile}). This project does not need its own copy. To wire only this project: notifai hooks uninstall --harness ${harness} --global && notifai hooks install --harness ${harness}`);
1708
+ return EXIT.ok;
1709
+ }
1710
+ if (wantGlobal && otherScope.some((installation) => !installation.global)) {
1711
+ if (hooksUninstallCommand(deps, { ...flags, global: false, harness }) !== EXIT.ok) {
1712
+ return EXIT.failed;
1713
+ }
1714
+ }
1715
+ const file = settingsFile(harness, wantGlobal, deps.cwd, deps.env, hookPlatform);
1699
1716
  // OpenCode's adapter is a generated plugin module rather than a handler
1700
1717
  // merged into a settings document, so it owns the whole file.
1701
1718
  if (harness === 'opencode') {
1702
1719
  return installOpencodePlugin(deps, file, {
1703
1720
  adapterPath,
1704
1721
  timeoutSeconds: BLOCKING_STOP_TIMEOUT_SECONDS,
1722
+ platform: hookPlatform,
1723
+ nodePath,
1705
1724
  });
1706
1725
  }
1707
1726
  if (harness === 'cursor') {
@@ -1711,6 +1730,8 @@ export function hooksInstallCommand(deps, flags) {
1711
1730
  const result = mergeCursorHooks(document, buildCursorHookConfig({
1712
1731
  adapterPath,
1713
1732
  harness: 'cursor',
1733
+ platform: hookPlatform,
1734
+ nodePath,
1714
1735
  }), scriptPath);
1715
1736
  applyPlan(file, result.document);
1716
1737
  return result;
@@ -1732,6 +1753,8 @@ export function hooksInstallCommand(deps, flags) {
1732
1753
  const result = mergeHooks(document, buildHookConfig({
1733
1754
  adapterPath,
1734
1755
  harness,
1756
+ platform: hookPlatform,
1757
+ nodePath,
1735
1758
  }), scriptPath);
1736
1759
  applyPlan(file, result.document);
1737
1760
  return result;
@@ -1805,7 +1828,7 @@ export function hooksUninstallCommand(deps, flags) {
1805
1828
  if (!harness)
1806
1829
  return EXIT.usage;
1807
1830
  const scriptPath = flags.scriptPath ?? process.argv[1] ?? 'notifai';
1808
- const file = settingsFile(harness, flags.global ?? false, deps.cwd, deps.env);
1831
+ const file = settingsFile(harness, flags.global ?? false, deps.cwd, deps.env, deps.hookPlatform);
1809
1832
  if (harness === 'opencode') {
1810
1833
  try {
1811
1834
  return withTargetFileLock(file, () => {
@@ -2502,9 +2525,7 @@ function setupProofPath(deps) {
2502
2525
  // the resolved absolute path is still a stable local identity for it.
2503
2526
  }
2504
2527
  const digest = createHash('sha256').update(projectDir).digest('hex').slice(0, 32);
2505
- const xdg = deps.env['XDG_STATE_HOME'];
2506
- const base = xdg && xdg !== '' ? xdg : path.join(os.homedir(), '.local', 'state');
2507
- return path.join(base, 'notifai', 'setup-proofs', `${digest}.json`);
2528
+ return path.join(stateDir(deps.env), 'setup-proofs', `${digest}.json`);
2508
2529
  }
2509
2530
  function readSetupProof(deps) {
2510
2531
  const file = setupProofPath(deps);
@@ -2803,6 +2824,7 @@ async function runSetupProof(deps) {
2803
2824
  const observed = observedCompanionReceipt(snapshot, proof.device_id);
2804
2825
  if (observed) {
2805
2826
  spinner?.stop(`Receipt observed from ${observed.delivery.device_name}`);
2827
+ deps.io.out(`Companion Receipt (the app's delivery confirmation) observed from ${observed.delivery.device_name}.`);
2806
2828
  return 'closed';
2807
2829
  }
2808
2830
  lastError = null;
@@ -3546,7 +3568,7 @@ function remedyLine(state) {
3546
3568
  * is, is each check's own call (`HookCheck`).
3547
3569
  */
3548
3570
  function hookStates(deps) {
3549
- const installations = findInstallations(deps.cwd, deps.env, deps.hookAdapterHome);
3571
+ const installations = findInstallations(deps.cwd, deps.env, deps.hookAdapterHome, deps.hookPlatform);
3550
3572
  const active = activeHarnessSession(deps.env, deps.cwd, (deps.now ?? Date.now)());
3551
3573
  const config = loadLoggedConfig(deps, { cwd: deps.cwd, env: deps.env });
3552
3574
  const settings = {
@@ -3634,12 +3656,12 @@ function hookStates(deps) {
3634
3656
  * hashes into `trusted_hash`, and an untrusted handler is simply not run.
3635
3657
  * - Everything else blocks its turn and needs a ceiling above the wait.
3636
3658
  */
3637
- function stopShapeProblems(installation) {
3659
+ function stopShapeProblems(installation, platform = process.platform) {
3638
3660
  if (installation.harness === 'codex')
3639
3661
  return [];
3640
3662
  const problems = [];
3641
3663
  for (const handler of installation.handlers.filter((entry) => handlerEvent(entry.command) === 'stop')) {
3642
- if (installation.harness === 'claude-code') {
3664
+ if (installation.harness === 'claude-code' && platform !== 'win32') {
3643
3665
  if (handler.async !== true) {
3644
3666
  problems.push(`${installation.file} declares a blocking Stop handler; the Claude Code wake route needs \`async: true\` so the turn ends while the waiter runs`);
3645
3667
  }
@@ -3656,7 +3678,7 @@ function stopShapeProblems(installation) {
3656
3678
  }
3657
3679
  function hookChecks(deps) {
3658
3680
  const checks = [];
3659
- const installations = findInstallations(deps.cwd, deps.env, deps.hookAdapterHome);
3681
+ const installations = findInstallations(deps.cwd, deps.env, deps.hookAdapterHome, deps.hookPlatform);
3660
3682
  // Not having hooks is a setup someone chose, not a fault: `send` works
3661
3683
  // without them. A setup that cannot work is what deserves to go red.
3662
3684
  if (installations.length === 0) {
@@ -3753,7 +3775,7 @@ function hookChecks(deps) {
3753
3775
  : `${stale.join('; ')} — rerun \`notifai hooks install\` to drop ${stale.length === 1 ? 'it' : 'them'}`,
3754
3776
  });
3755
3777
  const adapterProblems = installations.flatMap((installation) => (installation.problems ?? []).map((problem) => `${installation.file}: ${problem}`));
3756
- const sharedAdapter = inspectHookAdapter(deps.hookAdapterHome);
3778
+ const sharedAdapter = inspectHookAdapter(deps.hookAdapterHome, deps.hookPlatform);
3757
3779
  adapterProblems.push(...sharedAdapter.problems);
3758
3780
  if (adapterProblems.length > 0) {
3759
3781
  checks.push({
@@ -3781,7 +3803,7 @@ function hookChecks(deps) {
3781
3803
  },
3782
3804
  }),
3783
3805
  });
3784
- const shapeProblems = installations.flatMap((installation) => stopShapeProblems(installation).map((problem) => `${problem} — run \`notifai hooks install --harness ${installation.harness}${installation.global ? ' --global' : ''}\``));
3806
+ const shapeProblems = installations.flatMap((installation) => stopShapeProblems(installation, deps.hookPlatform).map((problem) => `${problem} — run \`notifai hooks install --harness ${installation.harness}${installation.global ? ' --global' : ''}\``));
3785
3807
  checks.push({
3786
3808
  name: 'hooks (stop shape)',
3787
3809
  ok: shapeProblems.length === 0,
@@ -3950,7 +3972,7 @@ function wakeRouteCheck(deps, active, activeInstallations) {
3950
3972
  if (active.harness === 'claude-code') {
3951
3973
  const readiness = inspectClaudeInbox({
3952
3974
  pid: deps.claudeSourcePid ?? claudeSessionPid(deps.env),
3953
- platform: process.platform,
3975
+ platform: deps.hookPlatform ?? process.platform,
3954
3976
  readDescriptor: deps.claudeWake?.readDescriptor ?? systemClaudeWakeAdapters(deps.env).readDescriptor,
3955
3977
  socketExists: (socketPath) => existsSync(socketPath),
3956
3978
  });
@@ -3965,7 +3987,7 @@ function wakeRouteCheck(deps, active, activeInstallations) {
3965
3987
  }
3966
3988
  if (active.harness === 'codex') {
3967
3989
  const readiness = inspectCodexResume(deps.env, {
3968
- platform: process.platform,
3990
+ platform: deps.hookPlatform ?? process.platform,
3969
3991
  directoryExists: (directory) => existsSync(directory),
3970
3992
  });
3971
3993
  return {
@@ -4120,24 +4142,7 @@ export function realIo(env = process.env) {
4120
4142
  return log.error(message);
4121
4143
  }
4122
4144
  },
4123
- openUrl: (url) => {
4124
- try {
4125
- if (process.platform === 'darwin') {
4126
- spawn('open', [url], { stdio: 'ignore', detached: true }).unref();
4127
- }
4128
- else if (process.platform === 'win32') {
4129
- // `start` is a cmd builtin; spawn('start', …) cannot work on Windows.
4130
- // Empty title argument keeps URLs with special characters intact.
4131
- spawn('cmd', ['/c', 'start', '', url], { stdio: 'ignore', detached: true }).unref();
4132
- }
4133
- else {
4134
- spawn('xdg-open', [url], { stdio: 'ignore', detached: true }).unref();
4135
- }
4136
- }
4137
- catch {
4138
- // Browser opening is best-effort; the URL is printed anyway.
4139
- }
4140
- },
4145
+ openUrl,
4141
4146
  };
4142
4147
  }
4143
4148
  //# sourceMappingURL=commands.js.map