dsh-wsl-workspace 0.2.2 → 0.2.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.
- package/README.de.md +49 -0
- package/README.es.md +49 -0
- package/README.fr.md +49 -0
- package/README.ja.md +49 -0
- package/README.ko.md +49 -0
- package/README.md +1 -1
- package/README.pt.md +49 -0
- package/README.ru.md +49 -0
- package/README.zh.md +3 -3
- package/lib/client.js +76 -5
- package/lib/client.js.map +1 -1
- package/lib/fs.js +1 -1
- package/lib/index.js +92 -13
- package/lib/index.js.map +1 -1
- package/lib/{paths-DBaSmi7x.js → paths-BDE1NVOv.js} +17 -2
- package/lib/paths-BDE1NVOv.js.map +1 -0
- package/lib/shell.js +2 -2
- package/lib/{wsl-GjkUifnx.js → wsl-CW3VPEIA.js} +41 -3
- package/lib/wsl-CW3VPEIA.js.map +1 -0
- package/package.json +8 -3
- package/src/client/AddWslWorkspace.tsx +349 -346
- package/src/client/api.ts +124 -104
- package/src/client/index.ts +233 -194
- package/src/index.ts +108 -13
- package/src/shared/paths.ts +175 -159
- package/src/shared/wsl-credentials.ts +135 -85
- package/image-1.png +0 -0
- package/image.png +0 -0
- package/lib/paths-DBaSmi7x.js.map +0 -1
- package/lib/wsl-GjkUifnx.js.map +0 -1
package/src/client/index.ts
CHANGED
|
@@ -1,194 +1,233 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Browser half of dsh-wsl-workspace. Registers the "Add WSL workspace…"
|
|
3
|
-
* action beside Settings at the sidebar foot (the official
|
|
4
|
-
* `sidebar.footer.action` slot), and keeps every blank session whose
|
|
5
|
-
* workspace is a WSL UNC path composed from the WSL VARIANT of the mode it
|
|
6
|
-
* currently runs (`standard` → `wsl-standard`, PTC → `wsl-code`, …) — so the
|
|
7
|
-
* WSL execution world composes with any mode instead of being a mode itself.
|
|
8
|
-
*
|
|
9
|
-
* The binding is a watching effect rather than a one-shot dialog action so
|
|
10
|
-
* EVERY creation path (this dialog, the workspace row's New Session, the
|
|
11
|
-
* hero picker) converges on the WSL-backed composition automatically.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import type { ConnectionHandle } from '@deepseek-ai/dsh-api-remotes/client'
|
|
15
|
-
// Type-only: pulls the locale plugin's Context merge (ctx.locale), the
|
|
16
|
-
// runtime's ClientContext, and the ui-sidebar SlotMap merge (the
|
|
17
|
-
// 'sidebar.footer.action' entry) into this program.
|
|
18
|
-
import type {} from '@deepseek-ai/dsh-client-locale/client'
|
|
19
|
-
import type {} from '@deepseek-ai/dsh-client-ui-sidebar/client'
|
|
20
|
-
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
|
|
21
|
-
import { check as checkApi, listDir as listDirApi, listDistros as listDistrosApi, setWorkspaceUser as setWorkspaceUserApi } from './api.ts'
|
|
22
|
-
import { AddWslWorkspace, type AddWslWorkspaceInjected } from './AddWslWorkspace.tsx'
|
|
23
|
-
import { ensureStyles } from './styles.ts'
|
|
24
|
-
import { zh, en } from './locales.ts'
|
|
25
|
-
import { isWslUnc } from '../shared/paths.ts'
|
|
26
|
-
|
|
27
|
-
/** Required services (cordis fiber inject). */
|
|
28
|
-
export const inject = ['slots', 'locale', 'connection', 'sessions', 'workspaces']
|
|
29
|
-
|
|
30
|
-
/** The legacy standalone WSL preset id (folded into the mode variants). */
|
|
31
|
-
const LEGACY_WSL_PRESET_ID = 'wsl'
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Minimal sessions-service face. The renderer-host ctx merge types
|
|
35
|
-
* `ctx.sessions` as its own SessionStore; the service the runtime actually
|
|
36
|
-
* registers under that key satisfies this narrower contract, so the cast is
|
|
37
|
-
* the documented boundary for a third-party plugin.
|
|
38
|
-
*/
|
|
39
|
-
interface WslSessionsFace {
|
|
40
|
-
list: {
|
|
41
|
-
getSnapshot(): { ids: string[]; byId: Record<string, { blank: boolean; cwd?: string; agentPreset?: string }> }
|
|
42
|
-
subscribe(fn: () => void): () => void
|
|
43
|
-
}
|
|
44
|
-
noteAgentPreset(sessionId: string, agentPreset: string): void
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** Minimal workspaces-service face (create + start-session only). */
|
|
48
|
-
interface WslWorkspacesFace {
|
|
49
|
-
create(input: { path: string }): Promise<{ workspaceId: string }>
|
|
50
|
-
startSession(workspaceId?: string): void
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Mount the sidebar action and the auto-binding effect.
|
|
55
|
-
* @param ctx - the browser plugin context.
|
|
56
|
-
*/
|
|
57
|
-
export function apply(ctx: ClientContext): void {
|
|
58
|
-
const { api } = ctx.get('connection') as ConnectionHandle
|
|
59
|
-
const workspaces = ctx.get('workspaces') as unknown as WslWorkspacesFace
|
|
60
|
-
const sessions = ctx.get('sessions') as unknown as WslSessionsFace
|
|
61
|
-
|
|
62
|
-
ensureStyles()
|
|
63
|
-
|
|
64
|
-
ctx.effect(
|
|
65
|
-
() => ctx.locale.register('wslWorkspace' as never, { zh, en }),
|
|
66
|
-
'dsh-wsl-workspace: locale dictionaries',
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
// The injected translate function reads the live DeepSeek Harness locale,
|
|
70
|
-
// so the dialog copy follows the app language setting automatically.
|
|
71
|
-
const t = ctx.locale.bind('wslWorkspace' as never) as unknown as (key: string, params?: Record<string, unknown>) => string
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if (!
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Browser half of dsh-wsl-workspace. Registers the "Add WSL workspace…"
|
|
3
|
+
* action beside Settings at the sidebar foot (the official
|
|
4
|
+
* `sidebar.footer.action` slot), and keeps every blank session whose
|
|
5
|
+
* workspace is a WSL UNC path composed from the WSL VARIANT of the mode it
|
|
6
|
+
* currently runs (`standard` → `wsl-standard`, PTC → `wsl-code`, …) — so the
|
|
7
|
+
* WSL execution world composes with any mode instead of being a mode itself.
|
|
8
|
+
*
|
|
9
|
+
* The binding is a watching effect rather than a one-shot dialog action so
|
|
10
|
+
* EVERY creation path (this dialog, the workspace row's New Session, the
|
|
11
|
+
* hero picker) converges on the WSL-backed composition automatically.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { ConnectionHandle } from '@deepseek-ai/dsh-api-remotes/client'
|
|
15
|
+
// Type-only: pulls the locale plugin's Context merge (ctx.locale), the
|
|
16
|
+
// runtime's ClientContext, and the ui-sidebar SlotMap merge (the
|
|
17
|
+
// 'sidebar.footer.action' entry) into this program.
|
|
18
|
+
import type {} from '@deepseek-ai/dsh-client-locale/client'
|
|
19
|
+
import type {} from '@deepseek-ai/dsh-client-ui-sidebar/client'
|
|
20
|
+
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
|
|
21
|
+
import { check as checkApi, listDir as listDirApi, listDistros as listDistrosApi, listWorkspaces as listWorkspacesApi, registerWindows as registerWindowsApi, setWorkspaceUser as setWorkspaceUserApi } from './api.ts'
|
|
22
|
+
import { AddWslWorkspace, type AddWslWorkspaceInjected } from './AddWslWorkspace.tsx'
|
|
23
|
+
import { ensureStyles } from './styles.ts'
|
|
24
|
+
import { zh, en } from './locales.ts'
|
|
25
|
+
import { canonicalWindowsPath, isWslUnc, joinUnc, mntToWindowsPath } from '../shared/paths.ts'
|
|
26
|
+
|
|
27
|
+
/** Required services (cordis fiber inject). */
|
|
28
|
+
export const inject = ['slots', 'locale', 'connection', 'sessions', 'workspaces']
|
|
29
|
+
|
|
30
|
+
/** The legacy standalone WSL preset id (folded into the mode variants). */
|
|
31
|
+
const LEGACY_WSL_PRESET_ID = 'wsl'
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Minimal sessions-service face. The renderer-host ctx merge types
|
|
35
|
+
* `ctx.sessions` as its own SessionStore; the service the runtime actually
|
|
36
|
+
* registers under that key satisfies this narrower contract, so the cast is
|
|
37
|
+
* the documented boundary for a third-party plugin.
|
|
38
|
+
*/
|
|
39
|
+
interface WslSessionsFace {
|
|
40
|
+
list: {
|
|
41
|
+
getSnapshot(): { ids: string[]; byId: Record<string, { blank: boolean; cwd?: string; agentPreset?: string }> }
|
|
42
|
+
subscribe(fn: () => void): () => void
|
|
43
|
+
}
|
|
44
|
+
noteAgentPreset(sessionId: string, agentPreset: string): void
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Minimal workspaces-service face (create + start-session only). */
|
|
48
|
+
interface WslWorkspacesFace {
|
|
49
|
+
create(input: { path: string }): Promise<{ workspaceId: string }>
|
|
50
|
+
startSession(workspaceId?: string): void
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Mount the sidebar action and the auto-binding effect.
|
|
55
|
+
* @param ctx - the browser plugin context.
|
|
56
|
+
*/
|
|
57
|
+
export function apply(ctx: ClientContext): void {
|
|
58
|
+
const { api } = ctx.get('connection') as ConnectionHandle
|
|
59
|
+
const workspaces = ctx.get('workspaces') as unknown as WslWorkspacesFace
|
|
60
|
+
const sessions = ctx.get('sessions') as unknown as WslSessionsFace
|
|
61
|
+
|
|
62
|
+
ensureStyles()
|
|
63
|
+
|
|
64
|
+
ctx.effect(
|
|
65
|
+
() => ctx.locale.register('wslWorkspace' as never, { zh, en }),
|
|
66
|
+
'dsh-wsl-workspace: locale dictionaries',
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
// The injected translate function reads the live DeepSeek Harness locale,
|
|
70
|
+
// so the dialog copy follows the app language setting automatically.
|
|
71
|
+
const t = ctx.locale.bind('wslWorkspace' as never) as unknown as (key: string, params?: Record<string, unknown>) => string
|
|
72
|
+
|
|
73
|
+
// Canonical Windows drive keys of every registered `/mnt/<drive>` workspace
|
|
74
|
+
// (refreshed from the host store; see refreshRoster). A blank session whose
|
|
75
|
+
// cwd is one of these binds to the WSL variant like a UNC-cwd session.
|
|
76
|
+
let wslWindowsPaths = new Set<string>()
|
|
77
|
+
|
|
78
|
+
const injected = (): AddWslWorkspaceInjected => ({
|
|
79
|
+
t,
|
|
80
|
+
checkPreset: async (): Promise<string | undefined> => {
|
|
81
|
+
let roster
|
|
82
|
+
try {
|
|
83
|
+
const response = await api.agentPresets.list({})
|
|
84
|
+
roster = response.result
|
|
85
|
+
} catch (error) {
|
|
86
|
+
return error instanceof Error ? error.message : String(error)
|
|
87
|
+
}
|
|
88
|
+
if (!roster.ok) return roster.error.message
|
|
89
|
+
const healthy = roster.value.presets.find((entry: { id: string; broken?: string }) =>
|
|
90
|
+
entry.id.startsWith('wsl-') && entry.broken === undefined)
|
|
91
|
+
if (healthy === undefined) return t('error.presetMissing')
|
|
92
|
+
return undefined
|
|
93
|
+
},
|
|
94
|
+
listDistros: () => listDistrosApi(),
|
|
95
|
+
listDir: (distro, path) => listDirApi(distro, path),
|
|
96
|
+
check: (distro, path) => checkApi(distro, path),
|
|
97
|
+
createWorkspace: async (linuxPath, username, distro): Promise<string | undefined> => {
|
|
98
|
+
try {
|
|
99
|
+
const winPath = mntToWindowsPath(linuxPath)
|
|
100
|
+
if (winPath !== null) {
|
|
101
|
+
// `/mnt/<drive>` workspace: the workspace registry realpath/stats
|
|
102
|
+
// the path and 9P cannot serve drvfs mounts, so register under the
|
|
103
|
+
// drive spelling and store distro/username for the session env.
|
|
104
|
+
// The browser binding below recognizes the drive cwd as WSL.
|
|
105
|
+
const view = await workspaces.create({ path: winPath })
|
|
106
|
+
await registerWindowsApi(linuxPath, distro, username)
|
|
107
|
+
const canonical = canonicalWindowsPath(winPath)
|
|
108
|
+
if (canonical !== null) wslWindowsPaths = new Set(wslWindowsPaths).add(canonical)
|
|
109
|
+
workspaces.startSession(view.workspaceId)
|
|
110
|
+
return undefined
|
|
111
|
+
}
|
|
112
|
+
const uncPath = joinUnc(distro, linuxPath)
|
|
113
|
+
const view = await workspaces.create({ path: uncPath })
|
|
114
|
+
await setWorkspaceUserApi(uncPath, username)
|
|
115
|
+
workspaces.startSession(view.workspaceId)
|
|
116
|
+
return undefined
|
|
117
|
+
} catch (error) {
|
|
118
|
+
return error instanceof Error ? error.message : String(error)
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
ctx.effect(
|
|
124
|
+
() => ctx.slots.inject(
|
|
125
|
+
'sidebar.footer.action',
|
|
126
|
+
() => ctx.slots.register(
|
|
127
|
+
{ name: 'sidebar.footer.action', id: 'wsl-workspace', inject: injected },
|
|
128
|
+
AddWslWorkspace,
|
|
129
|
+
),
|
|
130
|
+
),
|
|
131
|
+
'dsh-wsl-workspace: sidebar footer action',
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
// Mode-variant binding: a blank session whose workspace is a WSL UNC path
|
|
135
|
+
// is recomposed to the WSL variant of the mode it currently runs — plain
|
|
136
|
+
// 标准 becomes `wsl-standard`, PTC becomes `wsl-code`, and so on — so the
|
|
137
|
+
// WSL execution world composes with ANY mode instead of replacing it. The
|
|
138
|
+
// host refuses non-blank sessions (agent-preset-locked), so the swap is
|
|
139
|
+
// attempted at most a few times per session.
|
|
140
|
+
ctx.effect(() => {
|
|
141
|
+
const inFlight = new Set<string>()
|
|
142
|
+
const attempts = new Map<string, number>()
|
|
143
|
+
const MAX_ATTEMPTS = 3
|
|
144
|
+
// Healthy `wsl-<mode>` variant ids plus the roster's default preset id
|
|
145
|
+
// (what a session with no explicit choice gets). Refreshed periodically
|
|
146
|
+
// so variants generated after this page loaded are picked up.
|
|
147
|
+
let variants = new Set<string>()
|
|
148
|
+
let defaultPreset: string | undefined
|
|
149
|
+
const refreshRoster = (): void => {
|
|
150
|
+
void api.agentPresets.list({}).then((response: {
|
|
151
|
+
result: { ok: boolean; value: { presets: { id: string; broken?: string; isDefault?: boolean }[] } }
|
|
152
|
+
}) => {
|
|
153
|
+
const result = response.result
|
|
154
|
+
if (!result.ok) return
|
|
155
|
+
variants = new Set(result.value.presets
|
|
156
|
+
.filter((entry: { id: string; broken?: string }) =>
|
|
157
|
+
entry.broken === undefined && entry.id.startsWith('wsl-'))
|
|
158
|
+
.map((entry: { id: string }) => entry.id))
|
|
159
|
+
defaultPreset = result.value.presets.find(
|
|
160
|
+
(entry: { id: string; isDefault?: boolean }) => entry.isDefault === true,
|
|
161
|
+
)?.id
|
|
162
|
+
}).catch(() => {
|
|
163
|
+
// A failed roster read leaves the previous mapping; sessions stay on
|
|
164
|
+
// their current composition until the next refresh.
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
refreshRoster()
|
|
168
|
+
const refreshWorkspaces = (): void => {
|
|
169
|
+
void listWorkspacesApi().then((keys: string[]) => {
|
|
170
|
+
const next = new Set<string>()
|
|
171
|
+
for (const key of keys) {
|
|
172
|
+
const canonical = canonicalWindowsPath(key)
|
|
173
|
+
if (canonical !== null) next.add(canonical)
|
|
174
|
+
}
|
|
175
|
+
wslWindowsPaths = next
|
|
176
|
+
}).catch(() => {
|
|
177
|
+
// A failed store read leaves the previous set; sessions stay on
|
|
178
|
+
// their current composition until the next refresh.
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
refreshWorkspaces()
|
|
182
|
+
const maybeBind = (): void => {
|
|
183
|
+
const state = sessions.list.getSnapshot()
|
|
184
|
+
for (const id of state.ids) {
|
|
185
|
+
const summary = state.byId[id]
|
|
186
|
+
if (summary === undefined || !summary.blank || summary.cwd === undefined) continue
|
|
187
|
+
// A session belongs to the WSL world when its cwd is a WSL UNC path,
|
|
188
|
+
// or a Windows drive path registered as a `/mnt/<drive>` workspace
|
|
189
|
+
// (9P cannot serve drvfs, so those workspaces carry drive cwds).
|
|
190
|
+
const canonical = canonicalWindowsPath(summary.cwd)
|
|
191
|
+
const isWsl = isWslUnc(summary.cwd)
|
|
192
|
+
|| (canonical !== null && wslWindowsPaths.has(canonical))
|
|
193
|
+
if (!isWsl) continue
|
|
194
|
+
const current = summary.agentPreset
|
|
195
|
+
if (current !== undefined && current.startsWith('wsl-')) continue
|
|
196
|
+
// Legacy standalone `wsl` (now folded into the variants): remap it to
|
|
197
|
+
// the default mode's variant, since the standalone preset no longer
|
|
198
|
+
// exists in the roster.
|
|
199
|
+
const base = current === LEGACY_WSL_PRESET_ID
|
|
200
|
+
? (defaultPreset ?? 'standard')
|
|
201
|
+
: (current ?? defaultPreset)
|
|
202
|
+
if (base === undefined || base === LEGACY_WSL_PRESET_ID || base.startsWith('wsl-')) continue
|
|
203
|
+
const target = `wsl-${base.toLowerCase()}`
|
|
204
|
+
if (!variants.has(target)) continue
|
|
205
|
+
if (inFlight.has(id) || (attempts.get(id) ?? 0) >= MAX_ATTEMPTS) continue
|
|
206
|
+
inFlight.add(id)
|
|
207
|
+
void api.agentPresets.select({ sessionId: id, agentPreset: target })
|
|
208
|
+
.then((response: { result: { ok: boolean } }) => {
|
|
209
|
+
if (response.result.ok) sessions.noteAgentPreset(id, target)
|
|
210
|
+
})
|
|
211
|
+
.catch(() => {
|
|
212
|
+
// A refused or aborted swap (session already produced output,
|
|
213
|
+
// roster churn, reconnect) leaves the session on its current
|
|
214
|
+
// composition; count the attempt so a stuck session stops
|
|
215
|
+
// retrying after MAX_ATTEMPTS.
|
|
216
|
+
attempts.set(id, (attempts.get(id) ?? 0) + 1)
|
|
217
|
+
})
|
|
218
|
+
.finally(() => {
|
|
219
|
+
inFlight.delete(id)
|
|
220
|
+
})
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
maybeBind()
|
|
224
|
+
const unsubscribe = sessions.list.subscribe(() => maybeBind())
|
|
225
|
+
// Variants are generated at host boot; a page loaded before that would
|
|
226
|
+
// never see them without a periodic refresh.
|
|
227
|
+
const timer = window.setInterval(refreshRoster, 60_000)
|
|
228
|
+
return () => {
|
|
229
|
+
unsubscribe()
|
|
230
|
+
window.clearInterval(timer)
|
|
231
|
+
}
|
|
232
|
+
}, 'dsh-wsl-workspace: WSL mode-variant binding')
|
|
233
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -25,13 +25,13 @@
|
|
|
25
25
|
|
|
26
26
|
import { Context } from '@deepseek-ai/cordis'
|
|
27
27
|
import z from '@deepseek-ai/schemastery'
|
|
28
|
-
import { mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
|
28
|
+
import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
|
29
29
|
import { fileURLToPath } from 'node:url'
|
|
30
30
|
import { dirname, join } from 'node:path'
|
|
31
31
|
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
32
32
|
import { homedir } from 'node:os'
|
|
33
|
-
import { joinUnc, normalizeLinuxPath, isAbsoluteLinuxPath, isValidWslUsername, parseWslUnc } from './shared/paths.ts'
|
|
34
|
-
import { canonicalWslUnc, getWorkspaceUsername, setWorkspaceUsername } from './shared/wsl-credentials.ts'
|
|
33
|
+
import { joinUnc, mntToWindowsPath, normalizeLinuxPath, isAbsoluteLinuxPath, isValidWslUsername, parseWslUnc } from './shared/paths.ts'
|
|
34
|
+
import { canonicalWslUnc, getWindowsWorkspace, getWorkspaceUsername, listWorkspaceKeys, registerWindowsWorkspace, setWorkspaceUsername } from './shared/wsl-credentials.ts'
|
|
35
35
|
import { defaultDistro, listDistros } from './shared/wsl.ts'
|
|
36
36
|
import { isWslVariantId, transformPresetForWsl, variantIdFor } from './host/variants.ts'
|
|
37
37
|
|
|
@@ -213,10 +213,29 @@ function requireWslUnc(value: unknown): string {
|
|
|
213
213
|
return canonical
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
-
/**
|
|
216
|
+
/**
|
|
217
|
+
* Resolve one directory listing for the dialog. The 9P share (`\\wsl.localhost\…`)
|
|
218
|
+
* serves only the ext4 volume: `/mnt/<drive>` (drvfs) reads return Access
|
|
219
|
+
* denied, so drvfs paths are read through their Windows drive spelling and
|
|
220
|
+
* `/mnt` itself is synthesized from the drives present on the host.
|
|
221
|
+
*/
|
|
217
222
|
function listWslDir(distro: string, linuxPath: string): WslDirListingWire {
|
|
218
|
-
|
|
219
|
-
|
|
223
|
+
if (linuxPath === '/mnt') {
|
|
224
|
+
const entries: WslDirEntryWire[] = []
|
|
225
|
+
for (let i = 0; i < 26; i++) {
|
|
226
|
+
const letter = String.fromCharCode(65 + i)
|
|
227
|
+
try {
|
|
228
|
+
const info = statSync(`${letter}:\\`)
|
|
229
|
+
if (info.isDirectory()) entries.push({ name: letter.toLowerCase(), kind: 'directory' })
|
|
230
|
+
} catch {
|
|
231
|
+
// Absent drive: skip.
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return { path: '/mnt', parent: '/', entries }
|
|
235
|
+
}
|
|
236
|
+
const winPath = mntToWindowsPath(linuxPath)
|
|
237
|
+
const readPath = winPath !== null ? winPath : joinUnc(distro, linuxPath)
|
|
238
|
+
const dirents = readdirSync(readPath, { withFileTypes: true })
|
|
220
239
|
const entries: WslDirEntryWire[] = dirents
|
|
221
240
|
.slice(0, 1000)
|
|
222
241
|
.map((dirent): WslDirEntryWire => {
|
|
@@ -253,14 +272,33 @@ async function dispatch(method: string, params: Record<string, unknown>): Promis
|
|
|
253
272
|
case 'check': {
|
|
254
273
|
const distro = requireDistro(params.distro)
|
|
255
274
|
const path = requireLinuxPath(params.path, 'path')
|
|
256
|
-
|
|
275
|
+
// drvfs paths (Access denied over 9P) are checked through their drive spelling.
|
|
276
|
+
const winPath = mntToWindowsPath(path)
|
|
277
|
+
const readPath = winPath !== null ? winPath : joinUnc(distro, path)
|
|
257
278
|
try {
|
|
258
|
-
const info = statSync(
|
|
279
|
+
const info = statSync(readPath)
|
|
259
280
|
return { exists: true, isDirectory: info.isDirectory() }
|
|
260
281
|
} catch {
|
|
261
282
|
return { exists: false, isDirectory: false }
|
|
262
283
|
}
|
|
263
284
|
}
|
|
285
|
+
case 'registerWindows': {
|
|
286
|
+
// A `/mnt/<drive>` workspace registers under its Windows drive path
|
|
287
|
+
// (the registry realpath/stats it, and 9P cannot serve drvfs) with the
|
|
288
|
+
// distro stored for the per-session env contributor.
|
|
289
|
+
const distro = requireDistro(params.distro)
|
|
290
|
+
const linuxPath = requireLinuxPath(params.linuxPath, 'path')
|
|
291
|
+
const winPath = mntToWindowsPath(linuxPath)
|
|
292
|
+
if (winPath === null) throw new Error('registerWindows requires a /mnt/<drive> Linux path')
|
|
293
|
+
const username = typeof params.username === 'string' ? params.username : undefined
|
|
294
|
+
registerWindowsWorkspace(winPath, distro, username)
|
|
295
|
+
return null
|
|
296
|
+
}
|
|
297
|
+
case 'listWorkspaces': {
|
|
298
|
+
// Every registered WSL workspace key (UNC and Windows drive spellings):
|
|
299
|
+
// the client uses the drive keys to recognize `/mnt` workspaces.
|
|
300
|
+
return listWorkspaceKeys()
|
|
301
|
+
}
|
|
264
302
|
case 'setUser': {
|
|
265
303
|
const path = requireWslUnc(params.path)
|
|
266
304
|
const username = params.username
|
|
@@ -296,6 +334,21 @@ interface AgentPresetsService {
|
|
|
296
334
|
* @param shellPath - absolute path of the plugin's built WSL shell provider.
|
|
297
335
|
* @param fsPath - absolute path of the plugin's built WSL fs provider.
|
|
298
336
|
*/
|
|
337
|
+
/**
|
|
338
|
+
* Relative './x.mjs' row files a composition references. Local function-plugin
|
|
339
|
+
* rows travel with their preset directory; generated variants must copy them
|
|
340
|
+
* along or the variant composition fails to load.
|
|
341
|
+
* @param composition - the composition text.
|
|
342
|
+
*/
|
|
343
|
+
function localRowFiles(composition: string): Set<string> {
|
|
344
|
+
const files = new Set<string>()
|
|
345
|
+
for (const line of composition.split('\n')) {
|
|
346
|
+
const match = /^\s*name:\s*['"]?(\.\/[^'"]+\.m?js)['"]?\s*$/.exec(line)
|
|
347
|
+
if (match?.[1] !== undefined) files.add(match[1])
|
|
348
|
+
}
|
|
349
|
+
return files
|
|
350
|
+
}
|
|
351
|
+
|
|
299
352
|
async function materializeVariants(
|
|
300
353
|
agentPresets: AgentPresetsService,
|
|
301
354
|
dshHome: string,
|
|
@@ -314,6 +367,35 @@ async function materializeVariants(
|
|
|
314
367
|
const dir = join(userRoot, variantId)
|
|
315
368
|
mkdirSync(dir, { recursive: true })
|
|
316
369
|
writeFileSync(join(dir, 'agent.cordis.yml'), transformed, 'utf8')
|
|
370
|
+
// Copy local function-plugin row files (name: './x.mjs') from the source
|
|
371
|
+
// preset into the variant directory, including their transitive relative
|
|
372
|
+
// imports — the variant composition references them relatively and they
|
|
373
|
+
// must travel with it, or the variant fails to load at mount time.
|
|
374
|
+
const pending = [...localRowFiles(transformed)]
|
|
375
|
+
const visited = new Set<string>()
|
|
376
|
+
while (pending.length > 0) {
|
|
377
|
+
const rowFile = pending.pop()
|
|
378
|
+
if (rowFile === undefined || visited.has(rowFile)) continue
|
|
379
|
+
visited.add(rowFile)
|
|
380
|
+
const srcFile = join(dirname(preset.path), rowFile)
|
|
381
|
+
if (!existsSync(srcFile)) continue
|
|
382
|
+
mkdirSync(dirname(join(dir, rowFile)), { recursive: true })
|
|
383
|
+
copyFileSync(srcFile, join(dir, rowFile))
|
|
384
|
+
try {
|
|
385
|
+
const content = readFileSync(srcFile, 'utf8')
|
|
386
|
+
const importRe = /(?:from\s+|import\s*\(\s*|require\s*\(\s*)['"](\.\/[^'"]+)['"]/g
|
|
387
|
+
let match: RegExpExecArray | null
|
|
388
|
+
while ((match = importRe.exec(content)) !== null) {
|
|
389
|
+
let spec = match[1]
|
|
390
|
+
if (spec === undefined) continue
|
|
391
|
+
if (!/\.m?js$/.test(spec)) spec += '.mjs'
|
|
392
|
+
pending.push(spec)
|
|
393
|
+
}
|
|
394
|
+
} catch {
|
|
395
|
+
// Unreadable row file: keep whatever was copied; the variant reports
|
|
396
|
+
// the failure through the roster when mounted.
|
|
397
|
+
}
|
|
398
|
+
}
|
|
317
399
|
const labels = MODE_DISPLAY_LABELS[preset.id]
|
|
318
400
|
let name = variantName(preset.id, preset.id)
|
|
319
401
|
let orderLine = ''
|
|
@@ -410,11 +492,24 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
410
492
|
resolve(execution) {
|
|
411
493
|
const cwd = execution.agent?.session.header.cwd
|
|
412
494
|
const unc = cwd === undefined ? null : parseWslUnc(cwd)
|
|
413
|
-
if (unc
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
495
|
+
if (unc !== null) {
|
|
496
|
+
const username = getWorkspaceUsername(joinUnc(unc.distro, unc.linuxPath))
|
|
497
|
+
return username === undefined || username === ''
|
|
498
|
+
? { DSH_WSL_DISTRO: unc.distro }
|
|
499
|
+
: { DSH_WSL_DISTRO: unc.distro, DSH_WSL_USER: username }
|
|
500
|
+
}
|
|
501
|
+
// A Windows-drive cwd belongs to a `/mnt/<drive>` WSL workspace (9P
|
|
502
|
+
// cannot serve drvfs, so those register under their drive path): the
|
|
503
|
+
// stored distro drives `wsl.exe -d` for bash.
|
|
504
|
+
if (cwd !== undefined && /^[A-Za-z]:[\\/]/.test(cwd)) {
|
|
505
|
+
const entry = getWindowsWorkspace(cwd)
|
|
506
|
+
if (entry !== undefined && entry.distro !== undefined && entry.distro !== '') {
|
|
507
|
+
return entry.username === undefined || entry.username === ''
|
|
508
|
+
? { DSH_WSL_DISTRO: entry.distro }
|
|
509
|
+
: { DSH_WSL_DISTRO: entry.distro, DSH_WSL_USER: entry.username }
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return {}
|
|
418
513
|
},
|
|
419
514
|
}), 'dsh-wsl-workspace: per-session distro env fact')
|
|
420
515
|
}
|