dsh-wsl-tool 1.10.1 → 1.10.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 (3) hide show
  1. package/index.js +41 -14
  2. package/lib/client.js +68 -8
  3. package/package.json +1 -1
package/index.js CHANGED
@@ -64,26 +64,37 @@ if (Schema === null) {
64
64
  * documents the precedence: this configuration wins, the environment is the
65
65
  * deployment default, the built-in defaults are last.
66
66
  *
67
- * `distro` and `timeoutMs` are deliberately absent from the sidebar panel (they
68
- * are values, not features) but stay here so a patch or a panel could grow a
69
- * field for them without a schema change.
67
+ * `distro` and `timeoutMs` are values rather than switches, but they are here so a
68
+ * patch (or a future row in the panel) can set them without a schema change.
69
+ *
70
+ * EVERY field is `.volatile()`, and that is not decoration: DSH's settings service
71
+ * projects a plugin's config into a form only through `volatileForm()`, which keeps
72
+ * a field only when its nearest ancestor is marked volatile — a schema without one
73
+ * contributes no form at all, so the entry never reaches `ctx.settings.describe()`,
74
+ * its namespace is never served to the browser, and the sidebar panel waits for a
75
+ * scope that will never arrive. Volatile is also what the platform means by "can be
76
+ * edited without remounting", which is what the update hook below relies on.
77
+ *
78
+ * A volatile field must not sit inside another volatile one, so `tools` is a plain
79
+ * object whose leaf switches are volatile, not a volatile object.
70
80
  */
71
81
  export const Config = Schema?.object({
72
82
  tools: Schema.object({
73
- wsl: Schema.boolean().default(true).description('注册 `wsl` 工具:在 WSL 里执行 Linux 命令。'),
74
- path: Schema.boolean().default(true).description('注册 `wsl-path` 工具:Windows 路径与 /mnt/... 互转。'),
75
- env: Schema.boolean().default(true).description('注册 `wsl-env` 工具:汇总 WSL 环境能力。'),
76
- }).description('要注册哪几个工具。'),
77
- backgroundJobs: Schema.boolean().default(true).description('允许 `runInBackground`,由内置 job 工具读回结果。'),
78
- translatePaths: Schema.boolean().default(true).description('默认把命令里的 Windows 路径转成 /mnt/...。'),
79
- startInSessionWorkspace: Schema.boolean().default(false).description('未传 `workdir` 时从会话工作区开始,而不是 Linux 家目录。'),
80
- dangerGuard: Schema.boolean().default(true).description('危险命令必须显式 `allowDangerous` 才放行。关掉后模型可直接删除/分区。'),
81
- distro: Schema.string().default('').description('要固定使用的发行版;留空则用系统默认(也可用 DSH_WSL_DISTRO)。'),
82
- timeoutMs: Schema.number().default(0).description('默认命令超时毫秒数;0 表示用内置默认(也可用 DSH_WSL_TIMEOUT_MS)。'),
83
+ wsl: Schema.boolean().default(true).description('注册 `wsl` 工具:在 WSL 里执行 Linux 命令。').volatile(),
84
+ path: Schema.boolean().default(true).description('注册 `wsl-path` 工具:Windows 路径与 /mnt/... 互转。').volatile(),
85
+ env: Schema.boolean().default(true).description('注册 `wsl-env` 工具:汇总 WSL 环境能力。').volatile(),
86
+ }).description('要注册哪几个工具;这三个开关在下次启动 DSH 后生效。'),
87
+ backgroundJobs: Schema.boolean().default(true).description('允许 `runInBackground`,由内置 job 工具读回结果。').volatile(),
88
+ translatePaths: Schema.boolean().default(true).description('默认把命令里的 Windows 路径转成 /mnt/...。').volatile(),
89
+ startInSessionWorkspace: Schema.boolean().default(false).description('未传 `workdir` 时从会话工作区开始,而不是 Linux 家目录。').volatile(),
90
+ dangerGuard: Schema.boolean().default(true).description('危险命令必须显式 `allowDangerous` 才放行。关掉后模型可直接删除/分区。').volatile(),
91
+ distro: Schema.string().default('').description('要固定使用的发行版;留空则用系统默认(也可用 DSH_WSL_DISTRO)。').volatile(),
92
+ timeoutMs: Schema.number().default(0).description('默认命令超时毫秒数;0 表示用内置默认(也可用 DSH_WSL_TIMEOUT_MS)。').volatile(),
83
93
  }).description('dsh-wsl 的功能开关与默认值。')
84
94
 
85
95
  export function apply(ctx, settings = {}) {
86
- // Resolved once per mount: a settings change is a restart, not a live edit.
96
+ // Resolved once per mount. Behaviour reads this object at call time, so the
97
+ // volatile-update hook below can change it in place without remounting.
87
98
  const config = resolveConfig(process.env, settings)
88
99
  const runner = createRunner(ctx, config)
89
100
  // `ctx` is passed to the `wsl` tool for the optional `jobs` service only
@@ -92,4 +103,20 @@ export function apply(ctx, settings = {}) {
92
103
  if (config.tools.wsl) ctx.tools.register(createWslTool({ ctx, config, runner }))
93
104
  if (config.tools.path) ctx.tools.register(createWslPathTool({ config, runner }))
94
105
  if (config.tools.env) ctx.tools.register(createWslEnvTool({ config, runner }))
106
+
107
+ // A volatile edit is written into the same object the loader handed us and then
108
+ // announced, so re-resolving in place is what makes the behaviour switches
109
+ // (background jobs, path translation, the guard, workdir, distro, timeout) take
110
+ // effect without a restart. The three `tools.*` switches are the exception: they
111
+ // decide which tools were registered, so they need the next start, and the panel
112
+ // says so on those rows.
113
+ if (typeof ctx.on === 'function') {
114
+ ctx.on('loader/volatile-update', () => {
115
+ try {
116
+ Object.assign(config, resolveConfig(process.env, settings))
117
+ } catch (error) {
118
+ console.error('dsh-wsl-tool: applying a live settings change failed', error)
119
+ }
120
+ })
121
+ }
95
122
  }
package/lib/client.js CHANGED
@@ -163,6 +163,25 @@ window.__ModuleLoader__.load({
163
163
  revision: undefined,
164
164
  }
165
165
 
166
+ /**
167
+ * No scope yet — the Host has not listed this namespace, or its `describe()`
168
+ * round trip is still in flight.
169
+ *
170
+ * This is deliberately NOT the unavailable snapshot: a panel that reads "this
171
+ * DSH does not provide the plugin's settings scope" while it is merely still
172
+ * waiting sends whoever is debugging it after the wrong thing (measured: it
173
+ * said exactly that for a namespace the Host was already serving, because the
174
+ * form had been taken before the round trip settled).
175
+ */
176
+ const PENDING_SNAPSHOT = {
177
+ status: 'loading',
178
+ value: undefined,
179
+ base: undefined,
180
+ user: undefined,
181
+ writable: false,
182
+ revision: undefined,
183
+ }
184
+
166
185
  // ---------------------------------------------------------------- helpers
167
186
 
168
187
  /** Read one path out of a possibly absent object, never throwing. */
@@ -218,7 +237,7 @@ window.__ModuleLoader__.load({
218
237
 
219
238
  const read = () => {
220
239
  if (scope === undefined || scope === null || typeof scope.getSnapshot !== 'function') {
221
- return UNAVAILABLE_SNAPSHOT
240
+ return PENDING_SNAPSHOT
222
241
  }
223
242
  try {
224
243
  const snapshot = scope.getSnapshot()
@@ -231,8 +250,15 @@ window.__ModuleLoader__.load({
231
250
 
232
251
  return {
233
252
  /** Whether the `configForms` service was there at all. A service this DSH
234
- * does not ship reads differently from a row it does not serve. */
253
+ * does not ship reads differently from a row it does not serve — and both
254
+ * read differently from a namespace that simply has not been listed yet. */
235
255
  hasService: () => served,
256
+ /** Record that the service exists, before any scope is available. Without
257
+ * this, a namespace that has not been served yet reads as "this DSH has no
258
+ * configuration forms", which is a different (and wrong) diagnosis. */
259
+ noteService: () => {
260
+ served = true
261
+ },
236
262
  /** Bind the scope once the service appears. */
237
263
  attach: (next) => {
238
264
  if (next === undefined || next === null) return
@@ -528,7 +554,7 @@ window.__ModuleLoader__.load({
528
554
  { key: 'header', style: HEADER_STYLE },
529
555
  React.createElement('h2', { style: TITLE_STYLE }, 'WSL'),
530
556
  React.createElement('p', { style: DESCRIPTION_STYLE }, '通过 WSL 在 Windows 上执行 Linux 命令的开关与说明'),
531
- React.createElement('p', { style: NOTE_STYLE }, '改动会在下次启动 DSH 后生效'),
557
+ React.createElement('p', { style: NOTE_STYLE }, '工具开关在下次启动 DSH 后生效;其余开关即时生效。'),
532
558
  ),
533
559
  )
534
560
 
@@ -542,7 +568,8 @@ window.__ModuleLoader__.load({
542
568
  ),
543
569
  )
544
570
  } else if (snapshot.status === 'loading') {
545
- children.push(React.createElement('p', { key: 'note-loading', style: NOTE_STYLE }, '正在读取配置…'))
571
+ children.push(React.createElement('p', { key: 'note-loading', style: NOTE_STYLE },
572
+ '正在等待 Host 提供该插件的配置作用域(' + CONFIG_ROW_ID + ')…'))
546
573
  } else if (snapshot.status !== 'ready') {
547
574
  children.push(
548
575
  React.createElement('p', { key: 'note-unavailable', style: NOTE_STYLE }, '此 DSH 未提供该插件的配置作用域'),
@@ -632,18 +659,51 @@ window.__ModuleLoader__.load({
632
659
  try {
633
660
  if (typeof ctx.inject === 'function') {
634
661
  ctx.inject(['configForms'], (inner) => {
635
- let scope
662
+ const configForms = inner.configForms
663
+ if (configForms === undefined || configForms === null) return
664
+ // The service is here; whether it lists our namespace is a separate
665
+ // question that `whileServed` answers below.
666
+ model.noteService()
667
+ /**
668
+ * Take the form once the Host actually lists the namespace.
669
+ *
670
+ * A form fetched before the `describe()` round trip settles is born
671
+ * with its persistence already fixed to "not host" and never upgrades
672
+ * — it stays `unavailable` forever. That is exactly how this panel
673
+ * lost every switch while the tools kept working, so the namespace is
674
+ * awaited through the platform's own gate instead of being requested
675
+ * eagerly. The shipped settings pages use this same call.
676
+ */
677
+ const attachWhenServed = () => {
678
+ if (typeof configForms.whileServed !== 'function') {
679
+ // Older host face: no gate to wait on, so take whatever it gives
680
+ // and let the model report `unavailable` honestly.
681
+ attach(configForms.get(CONFIG_ROW_ID))
682
+ return () => model.detach()
683
+ }
684
+ return configForms.whileServed([CONFIG_ROW_ID], () => {
685
+ attach(configForms.get(CONFIG_ROW_ID))
686
+ return () => model.detach()
687
+ })
688
+ }
689
+ const attach = (scope) => {
690
+ try {
691
+ model.attach(scope)
692
+ } catch (error) {
693
+ console.error('dsh-wsl-tool: reading the "' + CONFIG_ROW_ID + '" configuration scope failed', error)
694
+ }
695
+ }
696
+ let stop
636
697
  try {
637
- scope = inner.configForms.get(CONFIG_ROW_ID)
698
+ stop = attachWhenServed()
638
699
  } catch (error) {
639
700
  console.error('dsh-wsl-tool: this DSH serves no "' + CONFIG_ROW_ID + '" configuration row', error)
640
701
  return
641
702
  }
642
- model.attach(scope)
643
703
  // The subscription lives exactly as long as this fiber: `effect` runs
644
704
  // the returned disposer on unload or recomposition.
645
705
  if (typeof inner.effect === 'function') {
646
- inner.effect(() => () => model.detach(), 'dsh-wsl-tool: configuration scope')
706
+ inner.effect(() => stop, 'dsh-wsl-tool: configuration scope')
647
707
  }
648
708
  })
649
709
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-wsl-tool",
3
- "version": "1.10.1",
3
+ "version": "1.10.3",
4
4
  "description": "Run Linux commands from Windows through WSL, essentially matching a native Linux DSH for command execution: background jobs, stdin, path translation, a destructive-command guard and a WSL capability report. A left-sidebar panel switches each capability on or off, each with a one-line explanation, and an opt-in patch gives the desktop app's sidebar terminal a WSL shell. WSL calls run below the DSH sandbox, and a project on a Windows drive keeps Windows filesystem semantics.",
5
5
  "type": "module",
6
6
  "main": "index.js",