@songtonyli/dsh-cli 0.1.9 → 0.1.11

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 (37) hide show
  1. package/README.md +1 -1
  2. package/bin/dsh.mjs +1 -1
  3. package/node_modules/@deepseek-ai/dsh-api-authorization-controller/README.i18n.yaml +2 -2
  4. package/node_modules/@deepseek-ai/dsh-api-authorization-controller/README.md +1 -1
  5. package/node_modules/@deepseek-ai/dsh-api-authorization-controller/README.zh.md +1 -1
  6. package/node_modules/@deepseek-ai/dsh-api-authorization-controller/lib/index.js +1 -0
  7. package/node_modules/@deepseek-ai/dsh-api-authorization-controller/lib/typert.host.js +2 -1
  8. package/node_modules/@deepseek-ai/dsh-api-authorization-controller/lib/typert.remote-client.js +1 -0
  9. package/node_modules/@deepseek-ai/dsh-api-authorization-controller/lib/types/index.js +1 -0
  10. package/node_modules/@deepseek-ai/dsh-api-remotes/lib/client.js +1 -0
  11. package/node_modules/@deepseek-ai/dsh-authorization/README.i18n.yaml +2 -2
  12. package/node_modules/@deepseek-ai/dsh-authorization/README.md +1 -1
  13. package/node_modules/@deepseek-ai/dsh-authorization/README.zh.md +1 -1
  14. package/node_modules/@deepseek-ai/dsh-client-ui-settings-signin/lib/client.js +1 -0
  15. package/node_modules/@deepseek-ai/dsh-client-ui-sidebar/lib/client.js +1 -1
  16. package/node_modules/@deepseek-ai/dsh-llm-pi-ai/README.i18n.yaml +2 -2
  17. package/node_modules/@deepseek-ai/dsh-llm-pi-ai/README.md +1 -1
  18. package/node_modules/@deepseek-ai/dsh-llm-pi-ai/README.zh.md +1 -1
  19. package/node_modules/@deepseek-ai/dsh-llm-pi-ai/lib/index.js +3 -1
  20. package/node_modules/@deepseek-ai/dsh-native-command/README.i18n.yaml +2 -2
  21. package/node_modules/@deepseek-ai/dsh-native-command/README.md +7 -2
  22. package/node_modules/@deepseek-ai/dsh-native-command/README.zh.md +7 -2
  23. package/node_modules/@deepseek-ai/dsh-native-command/lib/index.js +93 -2
  24. package/node_modules/@deepseek-ai/dsh-native-command/package.json +4 -0
  25. package/node_modules/@deepseek-ai/dsh-tool-cordis/lib/index.js +2 -2
  26. package/node_modules/@deepseek-ai/dsh-tui-app/README.i18n.yaml +2 -2
  27. package/node_modules/@deepseek-ai/dsh-tui-app/README.md +47 -12
  28. package/node_modules/@deepseek-ai/dsh-tui-app/README.zh.md +47 -12
  29. package/node_modules/@deepseek-ai/dsh-tui-app/cordis.patch.yml +3 -2
  30. package/node_modules/@deepseek-ai/dsh-tui-app/lib/index.js +3087 -1012
  31. package/node_modules/@deepseek-ai/dsh-tui-app/lib/startup.js +3 -2
  32. package/node_modules/@deepseek-ai/dsh-tui-app/package.json +2 -0
  33. package/node_modules/@deepseek-ai/dsh-web-app/lib/index.js +2 -77
  34. package/node_modules/@deepseek-ai/dsh-web-app/package.json +1 -2
  35. package/node_modules/@types/node/README.md +1 -1
  36. package/node_modules/@types/node/package.json +2 -2
  37. package/package.json +2 -2
package/README.md CHANGED
@@ -26,7 +26,7 @@ workspace and no build step. Native packages that ship one build per platform
26
26
  ripgrep, the PTY backend's addons, sharp, koffi, and the Landlock launcher — are
27
27
  ordinary optional dependencies, so npm resolves the right one for your machine.
28
28
 
29
- Requires Node `^22.19.0 || >=24.0.0`. Version 0.1.9; `dsh --version`
29
+ Requires Node `^22.19.0 || >=24.0.0`. Version 0.1.11; `dsh --version`
30
30
  also reports the harness build the package was assembled from.
31
31
 
32
32
  ## License
package/bin/dsh.mjs CHANGED
@@ -18,7 +18,7 @@ const argv = process.argv.slice(2)
18
18
  // version this package publishes. Reporting only one of the two would make an
19
19
  // ordinary `npm install @songtonyli/dsh-cli@x` look like it installed something else.
20
20
  if (argv.length === 1 && (argv[0] === '--version' || argv[0] === '-V')) {
21
- console.log('@songtonyli/dsh-cli 0.1.9 (deepseek-harness 0.1.6-alpha.1)')
21
+ console.log('@songtonyli/dsh-cli 0.1.11 (deepseek-harness 0.1.6-alpha.1)')
22
22
  process.exit(0)
23
23
  }
24
24
 
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/api/authorization-controller/README.md
5
- README.md: bdf227b45a0f31adc8ceb7c0d2cf8e5002c487a6
6
- README.zh.md: b65b8ce1840395b89b5feb36083b96650cb122da
5
+ README.md: 6020b15cee27504168035a1c35876fe41be09771
6
+ README.zh.md: 54ef3a48613ca189e6809f83303fdc946dfd7fd1
@@ -50,7 +50,7 @@ This package takes no configuration.
50
50
 
51
51
  ### Running one attempt
52
52
 
53
- `begin(request, signal)` is a stream. Each item is one frame: a `notice` carrying what the human must do and any page or code it refers to, a `prompt` carrying a question and the id an answer names, a `prompt-withdrawn` for a question the flow retired, and a final `settled` frame carrying `authorized` or `cancelled`. The attempt starts when the carrier pulls the first frame and ends when the stream does; closing the stream withdraws it. `cancel(key)` withdraws it from a call that no longer holds the stream, which is what a Cancel button on a re-rendered page uses.
53
+ `begin(request, signal)` is a stream. Each item is one frame: a `notice` carrying what the human must do, any page or code it refers to, and whether a desktop surface should hand that page to the default browser, a `prompt` carrying a question and the id an answer names, a `prompt-withdrawn` for a question the flow retired, and a final `settled` frame carrying `authorized` or `cancelled`. The attempt starts when the carrier pulls the first frame and ends when the stream does; closing the stream withdraws it. `cancel(key)` withdraws it from a call that no longer holds the stream, which is what a Cancel button on a re-rendered page uses.
54
54
 
55
55
  A question is answered by a second call naming the frame's `promptId`: `answer(key, promptId, value)` with the typed text or the chosen option's id, or `decline(key, promptId)` when the human says no, which settles the attempt as `cancelled`. Only the attempt's own prompts can be answered, so a stale id is refused rather than silently applied to a newer attempt.
56
56
 
@@ -50,7 +50,7 @@ kind: "package-reference"
50
50
 
51
51
  ### 运行一次尝试
52
52
 
53
- `begin(request, signal)` 是一条流。每一项都是一个帧:`notice` 携带人需要做的事以及它涉及的页面或验证码,`prompt` 携带一个提问以及回答时要指明的 id,`prompt-withdrawn` 对应 flow 撤回的提问,最后的 `settled` 帧携带 `authorized` 或 `cancelled`。尝试在载体拉取第一帧时开始,随流结束而结束;关闭该流即撤回它。`cancel(key)` 用于已不再持有该流的调用撤回它,这正是重新渲染后的页面上「取消」按钮所用的方式。
53
+ `begin(request, signal)` 是一条流。每一项都是一个帧:`notice` 携带人需要做的事、它涉及的页面或验证码,以及桌面界面是否应把该页面交给默认浏览器,`prompt` 携带一个提问以及回答时要指明的 id,`prompt-withdrawn` 对应 flow 撤回的提问,最后的 `settled` 帧携带 `authorized` 或 `cancelled`。尝试在载体拉取第一帧时开始,随流结束而结束;关闭该流即撤回它。`cancel(key)` 用于已不再持有该流的调用撤回它,这正是重新渲染后的页面上「取消」按钮所用的方式。
54
54
 
55
55
  回答提问要用第二次调用并指明该帧的 `promptId`:`answer(key, promptId, value)` 传入输入的文本或所选项的 id,或在人拒绝时用 `decline(key, promptId)`,后者会让尝试以 `cancelled` 结算。只有该尝试自己的提问可以被回答,因此过期的 id 会被拒绝,而不会被悄悄用在更新的尝试上。
56
56
 
@@ -398,6 +398,7 @@ let AuthorizationController = (() => {
398
398
  type: "notice",
399
399
  message: notice.message,
400
400
  ...notice.url === void 0 ? {} : { url: notice.url },
401
+ ...notice.openInBrowser === void 0 ? {} : { openInBrowser: notice.openInBrowser },
401
402
  ...notice.code === void 0 ? {} : { code: notice.code }
402
403
  });
403
404
  },
@@ -19,6 +19,7 @@ const _deepseek_ai_dsh_api_authorization_controller_authorization_begin_result$s
19
19
  'type': z.literal("notice").readonly(),
20
20
  'message': z.string().readonly(),
21
21
  'url': z.string().readonly().optional(),
22
+ 'openInBrowser': z.literal(true).readonly().optional(),
22
23
  'code': z.string().readonly().optional(),
23
24
  }), z.object({
24
25
  'type': z.literal("prompt").readonly(),
@@ -318,7 +319,7 @@ export const TYPERT = {
318
319
  },
319
320
  {
320
321
  "name": "AuthorizationNoticeFrame",
321
- "declaration": "export interface AuthorizationNoticeFrame {\n readonly type: 'notice';\n readonly message: string;\n readonly url?: string;\n readonly code?: string;\n}"
322
+ "declaration": "export interface AuthorizationNoticeFrame {\n readonly type: 'notice';\n readonly message: string;\n readonly url?: string;\n readonly openInBrowser?: true;\n readonly code?: string;\n}"
322
323
  },
323
324
  {
324
325
  "name": "AuthorizationPromptFrame",
@@ -19,6 +19,7 @@ const _deepseek_ai_dsh_api_authorization_controller_authorization_begin_result$s
19
19
  'type': z.literal("notice").readonly(),
20
20
  'message': z.string().readonly(),
21
21
  'url': z.string().readonly().optional(),
22
+ 'openInBrowser': z.literal(true).readonly().optional(),
22
23
  'code': z.string().readonly().optional(),
23
24
  }), z.object({
24
25
  'type': z.literal("prompt").readonly(),
@@ -315,6 +315,7 @@ let AuthorizationController = (() => {
315
315
  type: 'notice',
316
316
  message: notice.message,
317
317
  ...notice.url === undefined ? {} : { url: notice.url },
318
+ ...notice.openInBrowser === undefined ? {} : { openInBrowser: notice.openInBrowser },
318
319
  ...notice.code === undefined ? {} : { code: notice.code },
319
320
  });
320
321
  },
@@ -4540,6 +4540,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4540
4540
  "type": literal("notice").readonly(),
4541
4541
  "message": string().readonly(),
4542
4542
  "url": string().readonly().optional(),
4543
+ "openInBrowser": literal(true).readonly().optional(),
4543
4544
  "code": string().readonly().optional()
4544
4545
  }),
4545
4546
  object({
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/credentials/authorization/README.md
5
- README.md: 9b4f4e895bc94c0a34a1cee2d6bf28d7663decea
6
- README.zh.md: 44c354fef1cb2bd9261483cb4048858a7f70bd31
5
+ README.md: 6368d6c0e8abe14b4166250bef82a06ce2ddf425
6
+ README.zh.md: f1b4d92c9eb6519323d95778bd2e4e53249c1e9c
@@ -107,7 +107,7 @@ One attempt per key at a time. `begin()` validates the key and method, refuses a
107
107
 
108
108
  ### The interaction vocabulary
109
109
 
110
- A notice is one-way and never carries a secret: a message, optionally the page the human must open and the code they must enter there. A prompt is a question the flow cannot answer for itself — `text`, `secret`, or `select` — where `secret` differs from `text` only in presentation. A prompt carries its own signal so a flow that races a typed code against a browser callback can withdraw the losing question while the attempt continues; the request's signal withdraws the whole attempt instead. The vocabulary is deliberately smaller than any one provider's: it describes what a surface must render, so a surface that renders one flow renders all of them.
110
+ A notice is one-way and never carries a secret: a message, optionally the page the human must open and the code they must enter there, and whether a desktop surface should hand that page to the default browser (`openInBrowser`, set for an authorization destination rather than an informational link; the printed URL always remains the fallback). A prompt is a question the flow cannot answer for itself — `text`, `secret`, or `select` — where `secret` differs from `text` only in presentation. A prompt carries its own signal so a flow that races a typed code against a browser callback can withdraw the losing question while the attempt continues; the request's signal withdraws the whole attempt instead. The vocabulary is deliberately smaller than any one provider's: it describes what a surface must render, so a surface that renders one flow renders all of them.
111
111
 
112
112
  ### Commit confirmation
113
113
 
@@ -107,7 +107,7 @@ flow 声明它写入的凭据记录、面向用户的标签以及它提供的登
107
107
 
108
108
  ### 交互词汇
109
109
 
110
- notice 是单向的,且从不携带机密:一条消息,以及可选的「人需要打开的页面」与「需要在该页面输入的码」。prompt 是 flow 无法自行回答的问题——`text`、`secret` 或 `select`——其中 `secret` 与 `text` 的差别仅在呈现方式。prompt 自带 signal,使得让手输码与浏览器回调赛跑的 flow 可以在尝试继续的同时撤下落败的那个问题;撤销整次尝试则用请求的 signal。这套词汇刻意小于任何单个提供方的词汇:它描述的是界面必须渲染什么,因此能渲染一个 flow 的界面就能渲染全部 flow。
110
+ notice 是单向的,且从不携带机密:一条消息,可选的「人需要打开的页面」与「需要在该页面输入的码」,以及桌面界面是否应把该页面交给默认浏览器(`openInBrowser`,仅为授权目标页设置而非信息链接;打印出的 URL 始终保留为后备)。prompt 是 flow 无法自行回答的问题——`text`、`secret` 或 `select`——其中 `secret` 与 `text` 的差别仅在呈现方式。prompt 自带 signal,使得让手输码与浏览器回调赛跑的 flow 可以在尝试继续的同时撤下落败的那个问题;撤销整次尝试则用请求的 signal。这套词汇刻意小于任何单个提供方的词汇:它描述的是界面必须渲染什么,因此能渲染一个 flow 的界面就能渲染全部 flow。
111
111
 
112
112
  ### 提交确认
113
113
 
@@ -228,6 +228,7 @@ window.__ModuleLoader__.load({
228
228
  directions: [...attempt.directions, {
229
229
  message: frame.message,
230
230
  ...frame.url === void 0 ? {} : { url: frame.url },
231
+ ...frame.openInBrowser === void 0 ? {} : { openInBrowser: frame.openInBrowser },
231
232
  ...frame.code === void 0 ? {} : { code: frame.code }
232
233
  }]
233
234
  };
@@ -100,7 +100,7 @@ window.__ModuleLoader__.load({
100
100
  const SCROLLBAR_LINGER_MS = 2e3;
101
101
  /** Format complete-build metadata for the local brand badge. */
102
102
  function localBuildVersion() {
103
- return `0.1.6-alpha.1-03fee86-dirty`;
103
+ return `0.1.6-alpha.1-9ed61dc-dirty`;
104
104
  }
105
105
  /** Each panel row subscribes only to its own selection state. */
106
106
  function PanelRow({ id, label, wide, usePanelInfo, selectPanel, renderSlot }) {
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/llm/llm-pi-ai/README.md
5
- README.md: a49281b5ddc63c1fe4ce628857ff6f267dc498cd
6
- README.zh.md: 78aed7db98e217691351f49f3f8ee41e74177e17
5
+ README.md: e8b9b1e7aea345a70aaeb7667572ead879d7fcb6
6
+ README.zh.md: 3ad4572a7a0ea57f9bffbb4fce3a5136ab7eecc8
@@ -90,7 +90,7 @@ The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-a
90
90
 
91
91
  ### Sign in to a provider
92
92
 
93
- A provider pi-ai ships a login for can be signed into through the harness authorization seam: the flow offers OAuth or an interactive key prompt (a key is typed into pi-ai's own login prompt, not into the settings form), and the resulting credential is stored in the harness credential store at `llm-pi-ai/<provider id>`. The stored sign-in authenticates its route beneath any `apiKeyEnv` override and refreshes itself under the store's cross-process lock; signing out deletes the stored record. A hand-declared route key outside the record grammar — a lowercase hyphenated identifier — cannot be signed into, because a record write for it refuses with `LlmError('UNSTORABLE_PROVIDER_ID')`; such a route authenticates through `apiKeyEnv` or ambient provider settings instead.
93
+ A provider pi-ai ships a login for can be signed into through the harness authorization seam: the flow offers OAuth or an interactive key prompt (a key is typed into pi-ai's own login prompt, not into the settings form), and the resulting credential is stored in the harness credential store at `llm-pi-ai/<provider id>`. Signing in never activates a route: a dormant catalog provider still needs its profile under `providers.<id>` (an empty `{}` takes the installed defaults), and each surface owns how an authorization page reaches the browser — the Web Models page renders the link, while the terminal hands marked pages to the default browser. The stored sign-in authenticates its route beneath any `apiKeyEnv` override and refreshes itself under the store's cross-process lock; signing out deletes the stored record. A hand-declared route key outside the record grammar — a lowercase hyphenated identifier — cannot be signed into, because a record write for it refuses with `LlmError('UNSTORABLE_PROVIDER_ID')`; such a route authenticates through `apiKeyEnv` or ambient provider settings instead.
94
94
 
95
95
  ### Resolve the model catalog
96
96
 
@@ -90,7 +90,7 @@ kind: "package-reference"
90
90
 
91
91
  ### 登录提供方
92
92
 
93
- pi-ai 提供登录的提供方可以通过 harness 授权 seam 登录:流程提供 OAuth 或交互式密钥提示(密钥键入 pi-ai 自己的登录提示,而非设置表单),得到的凭据存储在 harness 凭据存储的 `llm-pi-ai/<provider id>` 记录中。存储的登录在其路由的 `apiKeyEnv` 覆盖之下完成认证,并在存储的跨进程锁下自行刷新;退出登录即删除存储记录。落在记录文法之外——小写连字符标识符——的手工声明路由键无法登录,因为对它的记录写入会以 `LlmError('UNSTORABLE_PROVIDER_ID')` 拒绝;这类路由改用 `apiKeyEnv` 或提供方 ambient 设置认证。
93
+ pi-ai 提供登录的提供方可以通过 harness 授权 seam 登录:流程提供 OAuth 或交互式密钥提示(密钥键入 pi-ai 自己的登录提示,而非设置表单),得到的凭据存储在 harness 凭据存储的 `llm-pi-ai/<provider id>` 记录中。登录不会激活路由:休眠的 catalog 提供方仍需要 `providers.<id>` 下的 profile(空的 `{}` 取已安装默认值),且各界面自行决定授权页面如何到达浏览器——Web 的 Models 页面渲染链接,终端则把被标记的页面交给默认浏览器。存储的登录在其路由的 `apiKeyEnv` 覆盖之下完成认证,并在存储的跨进程锁下自行刷新;退出登录即删除存储记录。落在记录文法之外——小写连字符标识符——的手工声明路由键无法登录,因为对它的记录写入会以 `LlmError('UNSTORABLE_PROVIDER_ID')` 拒绝;这类路由改用 `apiKeyEnv` 或提供方 ambient 设置认证。
94
94
 
95
95
  ### 解析模型目录
96
96
 
@@ -2379,13 +2379,15 @@ function relay(event, session) {
2379
2379
  case "auth_url":
2380
2380
  session.notify({
2381
2381
  message: event.instructions ?? "Open this page to continue signing in.",
2382
- url: event.url
2382
+ url: event.url,
2383
+ openInBrowser: true
2383
2384
  });
2384
2385
  return;
2385
2386
  case "device_code":
2386
2387
  session.notify({
2387
2388
  message: "Enter this code on the verification page to finish signing in.",
2388
2389
  url: event.verificationUri,
2390
+ openInBrowser: true,
2389
2391
  code: event.userCode
2390
2392
  });
2391
2393
  return;
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/util/native-command/README.md
5
- README.md: 10b60266403f6a479a286cccd369ee2e52dd3aa6
6
- README.zh.md: e8f2adda57ee257983a06bb2798c852cab20bf6a
5
+ README.md: 9dae51fc0c605e9c0ac6d04a0c768897ce04fc9f
6
+ README.zh.md: 92a1885e51dc12c51ef3a6d127bf0e7d3dc3391c
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: "Host-native command and path-opening utilities with shell-free execution, cancellation, desktop detection, and WSL path handoff."
2
+ description: "Host-native command, path-opening, and default-browser URL handoff utilities with shell-free execution, cancellation, desktop detection, and credential scrubbing."
3
3
  kind: "package-library"
4
4
  ---
5
5
 
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
9
9
 
10
10
  ## Summary
11
11
 
12
- `dsh-native-command` runs host executables without a shell and opens Host filesystem paths through the desktop. The command runner captures utf8 output, propagates cancellation, and hides transient Windows consoles. The path opener supports default-application and text-editor intents, browser-renderable documents, WSL translation, and desktop availability checks. It is a library, not a plugin: no `ctx`, no state, no events.
12
+ `dsh-native-command` runs host executables without a shell, opens Host filesystem paths through the desktop, and hands HTTP(S) URLs to the default browser. The command runner captures utf8 output, propagates cancellation, and hides transient Windows consoles. The path opener supports default-application and text-editor intents, browser-renderable documents, WSL translation, and desktop availability checks. The URL opener keeps Harness credentials out of the browser's environment. It is a library, not a plugin: no `ctx`, no state, no events.
13
13
 
14
14
  ## Table of Contents
15
15
 
@@ -49,6 +49,10 @@ The `NativeCommandRunner` type is the injectable command boundary for host integ
49
49
 
50
50
  `revealNativePath(path, signal)` selects the file in Finder or Explorer, including WSL path translation, and opens its parent directory through `xdg-open` on desktop Linux. `nativeFileManager()` identifies that action for Host-derived UI labels; desktop availability remains a separate `canOpenNativePath()` check. Callers must authorize the absolute file path before invoking either operation. Platform dispatch is covered by injected-runner tests; native desktop verification belongs to the corresponding platform. Explorer receives an encoded file URI as a separate argument. Its exit code 1 is accepted as a delegated handoff; cancellation, missing executables, and other exit codes still reject. This acknowledgement does not prove that a desktop window selected the file.
51
51
 
52
+ ### Opening a URL in the default browser
53
+
54
+ `openNativeUrl(url)` hands one HTTP(S) URL to the operating system's default browser and rejects every other protocol before spawning. The URL travels through a short-lived Node helper running the maintained `open` package with a scrubbed environment, so Harness credentials such as API keys and the Harness home never reach a newly launched browser process; on Windows the helper stays alive until PowerShell has accepted the handoff. The returned promise settles when the operating-system launcher exits, not when the browser window closes; a launch failure rejects with the helper's first stderr line, so the caller can fall back to showing the URL. This is the URL counterpart of the filesystem path opener — do not pass a URL to `openNativePath`, which treats its argument as a filesystem path.
55
+
52
56
  -----
53
57
 
54
58
  <a id="understand-the-implementation"></a>
@@ -66,6 +70,7 @@ The command runner is a thin wrapper over Node's `execFile`. The path opener sel
66
70
  | [`src/index.ts`](src/index.ts) | Public command-runner and path-opener exports |
67
71
  | [`src/runner.ts`](src/runner.ts) | Shell-free `execFile` adapter |
68
72
  | [`src/path-opener.ts`](src/path-opener.ts) | Desktop detection, open intents, browser preference, and WSL translation |
73
+ | [`src/url-opener.ts`](src/url-opener.ts) | Credential-scrubbed default-browser handoff for HTTP(S) URLs |
69
74
  | — | No runtime invariant companion is published; each run is one stateless child-process round trip with no owned event stream or mutable runtime data; behavior is enforced by unit tests. |
70
75
 
71
76
  ### What execFile gives the runner
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: "宿主原生命令与路径打开工具,提供无 shell 执行、取消、桌面探测与 WSL 路径交接。"
2
+ description: "宿主原生命令、路径打开与默认浏览器 URL 交接工具,提供无 shell 执行、取消、桌面探测与凭据擦除。"
3
3
  kind: "package-library"
4
4
  ---
5
5
 
@@ -9,7 +9,7 @@ kind: "package-library"
9
9
 
10
10
  ## 概述
11
11
 
12
- `dsh-native-command` 无需 shell 即可运行 Host 可执行文件,并通过桌面打开 Host 文件系统路径。命令运行器捕获 utf8 输出、传播取消,并隐藏 Windows 瞬时控制台。路径打开器支持默认应用与文本编辑器意图、浏览器可渲染文档、WSL 转换与桌面可用性检查。它是库而非插件:没有 `ctx`、无状态、不发事件。
12
+ `dsh-native-command` 无需 shell 即可运行 Host 可执行文件、通过桌面打开 Host 文件系统路径,并把 HTTP(S) URL 交给默认浏览器。命令运行器捕获 utf8 输出、传播取消,并隐藏 Windows 瞬时控制台。路径打开器支持默认应用与文本编辑器意图、浏览器可渲染文档、WSL 转换与桌面可用性检查。URL 打开器让 Harness 凭据不进入浏览器的环境。它是库而非插件:没有 `ctx`、无状态、不发事件。
13
13
 
14
14
  ## 目录
15
15
 
@@ -49,6 +49,10 @@ const { stdout, stderr } = await runNativeCommand('osascript', ['-e', script], s
49
49
 
50
50
  `revealNativePath(path, signal)` 在 Finder 或文件资源管理器中选中文件,包含 WSL 路径转换;在桌面 Linux 上通过 `xdg-open` 打开上层目录。`nativeFileManager()` 标识该操作,供 UI 根据 Host 选择文案;桌面是否可用仍由独立的 `canOpenNativePath()` 检查决定。调用方必须先授权绝对文件路径,再执行操作。平台分派由注入运行器的测试覆盖;原生桌面验证由对应平台负责。 Explorer 接收独立参数中的编码文件 URI。退出码 1 按已转交请求处理;取消、找不到可执行文件和其他退出码仍然报错。该确认不能证明桌面窗口已选中文件。
51
51
 
52
+ ### 在默认浏览器中打开 URL
53
+
54
+ `openNativeUrl(url)` 把一个 HTTP(S) URL 交给操作系统的默认浏览器,并在启动任何进程前拒绝其他协议。URL 经由一个短生命周期的 Node 辅助进程传递,该进程运行受维护的 `open` 包且使用擦除后的环境,因此 API 密钥与 Harness 主目录等 Harness 凭据不会进入新启动的浏览器进程;在 Windows 上辅助进程会存活到 PowerShell 接受交接为止。返回的 promise 在操作系统启动器退出时结算,而不是在浏览器窗口关闭时;启动失败会以辅助进程 stderr 的首行拒绝,调用方因此可以回退到展示 URL。这是与文件系统路径打开器对应的 URL 能力——不要把 URL 传给 `openNativePath`,后者把参数当作文件系统路径处理。
55
+
52
56
  -----
53
57
 
54
58
  <a id="understand-the-implementation"></a>
@@ -66,6 +70,7 @@ const { stdout, stderr } = await runNativeCommand('osascript', ['-e', script], s
66
70
  | [`src/index.ts`](src/index.ts) | 命令运行器与路径打开器的公共导出 |
67
71
  | [`src/runner.ts`](src/runner.ts) | 无 shell 的 `execFile` 适配器 |
68
72
  | [`src/path-opener.ts`](src/path-opener.ts) | 桌面探测、打开意图、浏览器偏好与 WSL 转换 |
73
+ | [`src/url-opener.ts`](src/url-opener.ts) | 面向 HTTP(S) URL 的凭据擦除默认浏览器交接 |
69
74
  | — | 不发布运行时不变式伴生入口;每次运行都是一次无状态的子进程往返,不拥有事件流或可变运行时数据;相关行为由单元测试保障。 |
70
75
 
71
76
  ### execFile 给了运行器什么
@@ -1,4 +1,5 @@
1
- import { execFile } from "node:child_process";
1
+ import { execFile, spawn } from "node:child_process";
2
+ import { scrubbedParentEnv } from "@deepseek-ai/dsh-subprocess";
2
3
  import { release } from "node:os";
3
4
  import { dirname, extname } from "node:path";
4
5
  import { pathToFileURL } from "node:url";
@@ -35,6 +36,96 @@ const runNativeCommand = (command, args, signal) => new Promise((resolve, reject
35
36
  });
36
37
  });
37
38
  //#endregion
39
+ //#region lib/types/url-opener.js
40
+ /**
41
+ * Default-browser handoff for HTTP(S) URLs through a credential-scrubbed helper.
42
+ * @module @deepseek-ai/dsh-native-command/url-opener
43
+ */
44
+ const BROWSER_OPENER_MODULE = import.meta.resolve("open");
45
+ const BROWSER_OPENER_PROGRAM = `
46
+ try {
47
+ const { default: open } = await import(${JSON.stringify(BROWSER_OPENER_MODULE)})
48
+ const launcher = await open(process.argv[1])
49
+ if (process.platform === 'win32') {
50
+ // open resolves at PowerShell spawn; keep it referenced until that launcher hands the URL to Windows.
51
+ const code = launcher.exitCode ?? await new Promise((resolve, reject) => {
52
+ function onError(error) {
53
+ launcher.off('close', onClose)
54
+ reject(error)
55
+ }
56
+ function onClose(code) {
57
+ launcher.off('error', onError)
58
+ resolve(code)
59
+ }
60
+ launcher.ref()
61
+ launcher.once('error', onError)
62
+ launcher.once('close', onClose)
63
+ })
64
+ if (code !== 0) throw new Error('browser operating-system launcher exited with code ' + String(code))
65
+ }
66
+ process.exitCode = 0
67
+ } catch (error) {
68
+ console.error(error)
69
+ process.exitCode = 1
70
+ }
71
+ `;
72
+ /** Start the maintained platform opener without forwarding Harness credentials. */
73
+ function spawnBrowserLauncher(url) {
74
+ return spawn(process.execPath, [
75
+ "--input-type=module",
76
+ "--eval",
77
+ BROWSER_OPENER_PROGRAM,
78
+ "--",
79
+ url
80
+ ], {
81
+ env: scrubbedParentEnv(),
82
+ stdio: [
83
+ "ignore",
84
+ "inherit",
85
+ "pipe"
86
+ ]
87
+ });
88
+ }
89
+ /**
90
+ * Hand an HTTP(S) URL to the operating system's default browser.
91
+ *
92
+ * The promise settles after the short-lived operating-system launcher exits,
93
+ * not after the browser window closes. Browser-launch failures reject with the
94
+ * helper's first stderr line when available. The helper receives a scrubbed
95
+ * environment so Harness credentials do not reach the browser process.
96
+ * @param url - absolute HTTP(S) URL to open.
97
+ * @returns once the operating-system launcher has accepted the URL.
98
+ */
99
+ async function openNativeUrl(url) {
100
+ const protocol = new URL(url).protocol;
101
+ if (protocol !== "http:" && protocol !== "https:") throw new Error(`native URL opener supports only HTTP(S), not ${protocol}`);
102
+ const launcher = spawnBrowserLauncher(url);
103
+ let launcherStderr = "";
104
+ launcher.stderr?.setEncoding("utf8");
105
+ launcher.stderr?.on("data", (chunk) => {
106
+ launcherStderr += chunk;
107
+ });
108
+ await new Promise((resolve, reject) => {
109
+ function onError(error) {
110
+ launcher.off("close", onClose);
111
+ reject(error);
112
+ }
113
+ function onClose(code) {
114
+ launcher.off("error", onError);
115
+ if (code !== 0) {
116
+ const firstLine = launcherStderr.trim().split(/\r?\n/u)[0];
117
+ const reason = firstLine === void 0 || firstLine === "" ? `browser launcher exited with code ${String(code)}` : firstLine.replace(/^(?:[A-Za-z]*Error):\s*/u, "");
118
+ reject(new Error(reason));
119
+ return;
120
+ }
121
+ if (launcherStderr !== "") process.stderr.write(launcherStderr);
122
+ resolve();
123
+ }
124
+ launcher.once("error", onError);
125
+ launcher.once("close", onClose);
126
+ });
127
+ }
128
+ //#endregion
38
129
  //#region lib/types/path-opener.js
39
130
  /**
40
131
  * Cross-platform native path and text-document openers for Host UI
@@ -241,4 +332,4 @@ async function revealNativePath(path, signal, internals = {}) {
241
332
  throw new Error(`native file manager is unsupported on ${platform}`);
242
333
  }
243
334
  //#endregion
244
- export { canOpenNativePath, nativeFileManager, openNativePath, openNativeTextFile, revealNativePath, runNativeCommand };
335
+ export { canOpenNativePath, nativeFileManager, openNativePath, openNativeTextFile, openNativeUrl, revealNativePath, runNativeCommand };
@@ -26,6 +26,10 @@
26
26
  "lib/types/**/*.d.ts"
27
27
  ],
28
28
  "license": "MIT",
29
+ "dependencies": {
30
+ "@deepseek-ai/dsh-subprocess": "0.1.6-alpha.1",
31
+ "open": "^11.0.0"
32
+ },
29
33
  "peerDependencies": {
30
34
  "@deepseek-ai/cordis": "4.0.2"
31
35
  },
@@ -6320,11 +6320,11 @@ const TYPE_API = [
6320
6320
  },
6321
6321
  {
6322
6322
  name: "AuthorizationNotice",
6323
- declaration: "export interface AuthorizationNotice {\n message: string;\n url?: string;\n code?: string;\n}"
6323
+ declaration: "export interface AuthorizationNotice {\n message: string;\n url?: string;\n openInBrowser?: true;\n code?: string;\n}"
6324
6324
  },
6325
6325
  {
6326
6326
  name: "AuthorizationNoticeFrame",
6327
- declaration: "export interface AuthorizationNoticeFrame {\n readonly type: 'notice';\n readonly message: string;\n readonly url?: string;\n readonly code?: string;\n}"
6327
+ declaration: "export interface AuthorizationNoticeFrame {\n readonly type: 'notice';\n readonly message: string;\n readonly url?: string;\n readonly openInBrowser?: true;\n readonly code?: string;\n}"
6328
6328
  },
6329
6329
  {
6330
6330
  name: "AuthorizationOutcome",
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/bundle/tui-app/README.md
5
- README.md: a74c0b8642192cd2b44abd939c3d8082e935991a
6
- README.zh.md: 3b9ba69a63d5461e85fab40b80f21502aaf8558a
5
+ README.md: f99f5ebde7ad398611e91f9e80529503423ee7f9
6
+ README.zh.md: 37fdaf5839a39657d560ee5c56c733d13dddd762