@chronosun/dsh-wsl 0.0.0-stage → 1.5.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.
- package/LICENSE +21 -0
- package/README.md +233 -2
- package/client.js +645 -0
- package/cordis.patch.yml +79 -0
- package/fs-service.mjs +57 -0
- package/host.js +262 -0
- package/icon.svg +11 -0
- package/index.js +5 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +86 -4
- package/prompt.mjs +25 -0
- package/search-tools.mjs +91 -0
- package/src/broker.mjs +373 -0
- package/src/changes-paths.mjs +92 -0
- package/src/collector.mjs +47 -0
- package/src/doctor.mjs +237 -0
- package/src/file-references.mjs +214 -0
- package/src/fs.mjs +312 -0
- package/src/helper.mjs +455 -0
- package/src/launch.mjs +54 -0
- package/src/path-user.mjs +13 -0
- package/src/preset-default.mjs +277 -0
- package/src/registry.mjs +56 -0
- package/src/settings.mjs +51 -0
- package/src/subprocess.mjs +180 -0
- package/src/wsl-diff-v2.mjs +315 -0
- package/terminal-launcher.mjs +59 -0
- package/worktree-tool-v2.mjs +102 -0
- package/world.js +112 -0
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# dsh-wsl — M1a slice:
|
|
2
|
+
# 1) a row named after the package: the panel's host routes + the client half;
|
|
3
|
+
# 2) the WSL preset (bash/terminal/jobs run inside a local WSL distribution);
|
|
4
|
+
# 3) a diagnostic preset with no world, to tell a declaration problem from a
|
|
5
|
+
# world-activation problem.
|
|
6
|
+
- insert:
|
|
7
|
+
- id: dsh-wsl
|
|
8
|
+
name: '@chronosun/dsh-wsl'
|
|
9
|
+
- id: preset-wsl
|
|
10
|
+
name: '@deepseek-ai/dsh-agent-preset'
|
|
11
|
+
config:
|
|
12
|
+
id: wsl
|
|
13
|
+
name: WSL
|
|
14
|
+
description: Linux execution world inside a local WSL distribution.
|
|
15
|
+
order: 5
|
|
16
|
+
plugins:
|
|
17
|
+
- id: persona
|
|
18
|
+
name: '@deepseek-ai/dsh-persona'
|
|
19
|
+
config:
|
|
20
|
+
prefix: You are a coding agent powered by the {{model}} model.
|
|
21
|
+
- id: time-context
|
|
22
|
+
name: '@deepseek-ai/dsh-time-context'
|
|
23
|
+
- id: wsl-world
|
|
24
|
+
name: cordis:group
|
|
25
|
+
group: true
|
|
26
|
+
isolate:
|
|
27
|
+
subprocess: true
|
|
28
|
+
shell: true
|
|
29
|
+
fs: true
|
|
30
|
+
config:
|
|
31
|
+
- id: wsl-subprocess
|
|
32
|
+
name: '@chronosun/dsh-wsl/world'
|
|
33
|
+
- id: fs-wsl
|
|
34
|
+
name: '@chronosun/dsh-wsl/fs'
|
|
35
|
+
- id: bash-local
|
|
36
|
+
name: '@deepseek-ai/dsh-bash-local'
|
|
37
|
+
- id: tool-bash
|
|
38
|
+
name: '@deepseek-ai/dsh-tool-bash'
|
|
39
|
+
- id: tool-fs
|
|
40
|
+
name: '@deepseek-ai/dsh-tool-fs'
|
|
41
|
+
config:
|
|
42
|
+
applyPatch: true
|
|
43
|
+
- id: search-tools
|
|
44
|
+
name: '@chronosun/dsh-wsl/search'
|
|
45
|
+
- id: wsl-prompt
|
|
46
|
+
name: '@chronosun/dsh-wsl/prompt'
|
|
47
|
+
- id: agent-instructions
|
|
48
|
+
name: '@deepseek-ai/dsh-agent-instructions'
|
|
49
|
+
config:
|
|
50
|
+
maxBytes: 65536
|
|
51
|
+
- id: skill-filesystem
|
|
52
|
+
name: '@deepseek-ai/dsh-skill-filesystem'
|
|
53
|
+
config:
|
|
54
|
+
# The WSL filesystem exposes no watcher, so the catalog is
|
|
55
|
+
# collected once instead of being invalidated live.
|
|
56
|
+
watch: false
|
|
57
|
+
- id: tool-skill
|
|
58
|
+
name: '@deepseek-ai/dsh-tool-skill'
|
|
59
|
+
- id: tool-todo
|
|
60
|
+
name: '@deepseek-ai/dsh-tool-todo'
|
|
61
|
+
config:
|
|
62
|
+
allowParallelInProgress: true
|
|
63
|
+
- id: tool-ask-user
|
|
64
|
+
name: '@deepseek-ai/dsh-tool-ask-user'
|
|
65
|
+
- id: tool-web
|
|
66
|
+
name: '@deepseek-ai/dsh-tool-web'
|
|
67
|
+
config:
|
|
68
|
+
fetch: true
|
|
69
|
+
searchTimeoutMs: 60000
|
|
70
|
+
- id: present
|
|
71
|
+
name: '@deepseek-ai/dsh-tool-present'
|
|
72
|
+
- id: command-goal
|
|
73
|
+
name: '@deepseek-ai/dsh-command-goal'
|
|
74
|
+
- id: tool-goal
|
|
75
|
+
name: '@deepseek-ai/dsh-tool-goal'
|
|
76
|
+
- id: tool-worktree-wsl
|
|
77
|
+
name: '@chronosun/dsh-wsl/worktree'
|
|
78
|
+
- id: tool-jobs
|
|
79
|
+
name: '@deepseek-ai/dsh-tool-jobs'
|
package/fs-service.mjs
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// `ctx.fs` for the WSL world. Mirrors world.js: the host's FileSystem base is
|
|
2
|
+
// used when resolvable, otherwise the service is registered through Context.
|
|
3
|
+
// Delegation (not inheritance) keeps the factory free of host imports and
|
|
4
|
+
// avoids `#private` brand checks through Cordis' service proxy.
|
|
5
|
+
import { registry } from './src/registry.mjs'
|
|
6
|
+
import { createWslFileSystem } from './src/fs.mjs'
|
|
7
|
+
|
|
8
|
+
let Base = null
|
|
9
|
+
try {
|
|
10
|
+
const mod = await import('@deepseek-ai/dsh-fs')
|
|
11
|
+
Base = mod.FileSystem ?? mod.default ?? null
|
|
12
|
+
} catch {
|
|
13
|
+
Base = null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
class ServiceShim {
|
|
17
|
+
constructor(ctx) {
|
|
18
|
+
this.ctx = ctx
|
|
19
|
+
if (typeof ctx.set !== 'function') {
|
|
20
|
+
throw new Error('fs-wsl: this Harness build exposes neither the FileSystem base class nor ctx.set')
|
|
21
|
+
}
|
|
22
|
+
ctx.provide?.('fs')
|
|
23
|
+
ctx.set('fs', this)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const Parent = Base ?? ServiceShim
|
|
28
|
+
const METHODS = [
|
|
29
|
+
'resolve', 'processPath', 'processPathFromHostPath', 'fileUrl', 'contains', 'stat', 'lstat',
|
|
30
|
+
'readText', 'streamText', 'readBytes', 'readByteRange', 'listDir', 'writeText', 'editText',
|
|
31
|
+
'patchText', 'deleteFile', 'moveFile', 'watch',
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
export default class WslFileSystemService extends Parent {
|
|
35
|
+
constructor(ctx, config = {}) {
|
|
36
|
+
super(ctx)
|
|
37
|
+
if (!this.ctx) this.ctx = ctx
|
|
38
|
+
const user = config.user ?? ''
|
|
39
|
+
// One provider for every distribution: each target carries its own, so a
|
|
40
|
+
// workspace in another distro resolves through that distro's broker.
|
|
41
|
+
this.impl = createWslFileSystem({
|
|
42
|
+
brokerFor: (distro) => registry.get(distro ?? '', user),
|
|
43
|
+
defaultDistro: config.distro ?? '',
|
|
44
|
+
defaultCwd: config.defaultCwd ?? '/',
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
for (const method of METHODS) {
|
|
50
|
+
Object.defineProperty(WslFileSystemService.prototype, method, {
|
|
51
|
+
configurable: true,
|
|
52
|
+
writable: true,
|
|
53
|
+
value: function delegate(...args) {
|
|
54
|
+
return this.impl[method](...args)
|
|
55
|
+
},
|
|
56
|
+
})
|
|
57
|
+
}
|
package/host.js
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
// Host half: the WSL panel's data routes.
|
|
2
|
+
// Each endpoint is an exact route: prefix matching was measured to not cover
|
|
3
|
+
// sub-paths here, and exact routes are unambiguous. Handler routes (not
|
|
4
|
+
// resources) are used because resources answer GET/HEAD only.
|
|
5
|
+
import { spawn } from 'node:child_process'
|
|
6
|
+
import { readFileSync } from 'node:fs'
|
|
7
|
+
import { registry } from './src/registry.mjs'
|
|
8
|
+
import { installModelCwdTranslation, installSessionPresetDefault, WSL_PRESET } from './src/preset-default.mjs'
|
|
9
|
+
import { installWslFileReferences } from './src/file-references.mjs'
|
|
10
|
+
import { installWorkspaceChangesPaths } from './src/changes-paths.mjs'
|
|
11
|
+
import { installWslDiff } from './src/wsl-diff-v2.mjs'
|
|
12
|
+
import { readSettings, userFor, writeSettings } from './src/settings.mjs'
|
|
13
|
+
import { runDoctor, runFix, detectUsers, listWslDistros } from './src/doctor.mjs'
|
|
14
|
+
|
|
15
|
+
const BASE = '/dsh-wsl'
|
|
16
|
+
const VERSION = (() => {
|
|
17
|
+
try { return JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8')).version ?? '' } catch { return '' }
|
|
18
|
+
})()
|
|
19
|
+
|
|
20
|
+
// A live profile reconcile starts the new row before the previous incarnation
|
|
21
|
+
// is always gone; its exact routes then still sit in webServer. Reinstalling
|
|
22
|
+
// during development must not fail with `duplicate exact route` and must not
|
|
23
|
+
// leave the panel on the old handler, so take the route over when the exact
|
|
24
|
+
// path is already owned. Our disposer removes the path only while it still
|
|
25
|
+
// holds this route, so a later reload cannot delete a successor's handler.
|
|
26
|
+
const ORIGINAL = Symbol.for('cordis.original')
|
|
27
|
+
|
|
28
|
+
function registerExactRoute(webServer, route) {
|
|
29
|
+
try {
|
|
30
|
+
return webServer.register(route)
|
|
31
|
+
} catch (error) {
|
|
32
|
+
const message = String(error?.message ?? error)
|
|
33
|
+
if (!message.includes('duplicate')) throw error
|
|
34
|
+
const raw = webServer?.[ORIGINAL] ?? webServer
|
|
35
|
+
const table = raw?.exact
|
|
36
|
+
if (!(table instanceof Map)) throw error
|
|
37
|
+
table.set(route.path, route)
|
|
38
|
+
console.warn(`[dsh-wsl] took over ${route.path} from a previous plugin instance`)
|
|
39
|
+
return () => { if (table.get(route.path) === route) table.delete(route.path) }
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function send(res, status, body) {
|
|
44
|
+
res.statusCode = status
|
|
45
|
+
res.setHeader('content-type', 'application/json; charset=utf-8')
|
|
46
|
+
res.setHeader('cache-control', 'no-store')
|
|
47
|
+
res.end(JSON.stringify(body))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function readBody(req) {
|
|
51
|
+
return new Promise((resolve) => {
|
|
52
|
+
let data = ''
|
|
53
|
+
req.on('data', (chunk) => { data += chunk; if (data.length > 1 << 20) req.destroy() })
|
|
54
|
+
req.on('end', () => { try { resolve(data ? JSON.parse(data) : {}) } catch { resolve({}) } })
|
|
55
|
+
req.on('error', () => resolve({}))
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function listDistros() {
|
|
60
|
+
return new Promise((resolve) => {
|
|
61
|
+
const child = spawn('wsl.exe', ['-l', '-q'], { windowsHide: true })
|
|
62
|
+
let out = ''
|
|
63
|
+
child.stdout.on('data', (d) => (out += d.toString('utf16le')))
|
|
64
|
+
child.on('error', () => resolve([]))
|
|
65
|
+
child.on('close', () => {
|
|
66
|
+
const names = out
|
|
67
|
+
.replace(/\0/g, '')
|
|
68
|
+
.split(/\r?\n/)
|
|
69
|
+
.map((s) => s.trim())
|
|
70
|
+
.filter(Boolean)
|
|
71
|
+
resolve([...new Set(names)])
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const name = 'dsh-wsl'
|
|
77
|
+
export const inject = ['webServer']
|
|
78
|
+
|
|
79
|
+
// Must be an arrow function: Cordis treats a `function` declaration as a
|
|
80
|
+
// constructible class (`new apply(ctx)`), and then discards the cleanup this
|
|
81
|
+
// function returns. Without that cleanup the exact routes survive a reload and
|
|
82
|
+
// every reinstall fails with `duplicate exact route`.
|
|
83
|
+
export const apply = (ctx) => {
|
|
84
|
+
const webServer = ctx.webServer
|
|
85
|
+
if (!webServer) return
|
|
86
|
+
|
|
87
|
+
// Every New Session / Workspace-open request in a WSL workspace omits the
|
|
88
|
+
// preset; without this patch those sessions run on Windows. Installed before
|
|
89
|
+
// the routes so `/state` can report whether the seam is live.
|
|
90
|
+
const presetDefault = installSessionPresetDefault(ctx)
|
|
91
|
+
// Keep the durable UNC cwd for Host features, but show the model the Linux
|
|
92
|
+
// directory its tools actually use. No-op for non-WSL sessions.
|
|
93
|
+
const modelCwd = installModelCwdTranslation(ctx)
|
|
94
|
+
// `@file` discovery: WSL Agents index the distribution instead of the UNC
|
|
95
|
+
// share. Diff labels translate absolute UNC display paths to Linux while the
|
|
96
|
+
// underlying Host summary/diff still serves the open/compare coordinates.
|
|
97
|
+
const fileReferences = installWslFileReferences(ctx)
|
|
98
|
+
const changesPaths = installWorkspaceChangesPaths(ctx)
|
|
99
|
+
const wslDiff = installWslDiff(ctx)
|
|
100
|
+
|
|
101
|
+
const bootSettings = readSettings()
|
|
102
|
+
registry.idleMinutes = bootSettings.idleDays * 24 * 60
|
|
103
|
+
|
|
104
|
+
const hasService = (name) => {
|
|
105
|
+
try { return ctx.get(name) !== undefined } catch { return false }
|
|
106
|
+
}
|
|
107
|
+
const capabilities = () => ({
|
|
108
|
+
webServer: true,
|
|
109
|
+
sessionController: hasService('sessionController'),
|
|
110
|
+
workingDirectory: hasService('workingDirectory'),
|
|
111
|
+
workspaceRegistry: hasService('workspaceRegistry'),
|
|
112
|
+
agentPresets: hasService('agentPresets'),
|
|
113
|
+
fileReferences: hasService('fileReferences'),
|
|
114
|
+
workspaceChanges: hasService('workspaceChanges'),
|
|
115
|
+
agents: hasService('agents'),
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
const routes = [
|
|
119
|
+
['GET', BASE, async (req, res) => send(res, 200, { ok: true, name, version: VERSION })],
|
|
120
|
+
['GET', `${BASE}/distros`, async (req, res) => send(res, 200, { distros: await listDistros() })],
|
|
121
|
+
['GET', `${BASE}/state`, async (req, res) => send(res, 200, {
|
|
122
|
+
version: VERSION,
|
|
123
|
+
idleMinutes: registry.idleMinutes,
|
|
124
|
+
idleDays: readSettings().idleDays,
|
|
125
|
+
brokers: registry.list(),
|
|
126
|
+
settings: readSettings(),
|
|
127
|
+
capabilities: capabilities(),
|
|
128
|
+
presetDefault: { installed: presetDefault.installed, preset: WSL_PRESET },
|
|
129
|
+
modelCwd: { installed: modelCwd.installed },
|
|
130
|
+
fileReferences: { installed: fileReferences.installed },
|
|
131
|
+
changesPaths: { installed: changesPaths.installed },
|
|
132
|
+
wslDiff: { installed: wslDiff.installed, stats: wslDiff.stats },
|
|
133
|
+
})],
|
|
134
|
+
['*', `${BASE}/settings`, async (req, res) => {
|
|
135
|
+
if ((req.method ?? 'GET') === 'GET') return send(res, 200, readSettings())
|
|
136
|
+
const body = await readBody(req)
|
|
137
|
+
send(res, 200, writeSettings(body))
|
|
138
|
+
}], ['GET', `${BASE}/doctor`, async (req, res) => send(res, 200, await runDoctor(ctx, {
|
|
139
|
+
version: VERSION,
|
|
140
|
+
capabilities: capabilities(),
|
|
141
|
+
flags: {
|
|
142
|
+
presetDefault: presetDefault.installed,
|
|
143
|
+
modelCwd: modelCwd.installed,
|
|
144
|
+
fileReferences: fileReferences.installed,
|
|
145
|
+
changesPaths: changesPaths.installed,
|
|
146
|
+
wslDiff: wslDiff.installed,
|
|
147
|
+
},
|
|
148
|
+
}))],
|
|
149
|
+
['POST', `${BASE}/doctor/fix`, async (req, res) => {
|
|
150
|
+
const body = await readBody(req)
|
|
151
|
+
send(res, 200, await runFix(body.distro ?? '', body.action ?? '', body.user ?? '', body.name ?? ''))
|
|
152
|
+
}],
|
|
153
|
+
['GET', `${BASE}/changes`, async (req, res) => {
|
|
154
|
+
const url = new URL(req.url, 'http://localhost')
|
|
155
|
+
const sessionId = url.searchParams.get('sessionId') ?? ''
|
|
156
|
+
const seq = Number(url.searchParams.get('seq') ?? -1)
|
|
157
|
+
const summary = ctx.get('workspaceChanges')?.summary(sessionId, seq)
|
|
158
|
+
if (summary === undefined) return send(res, 404, { error: 'change summary unavailable' })
|
|
159
|
+
send(res, 200, summary)
|
|
160
|
+
}],
|
|
161
|
+
['GET', `${BASE}/changes/diff`, async (req, res) => {
|
|
162
|
+
const url = new URL(req.url, 'http://localhost')
|
|
163
|
+
const sessionId = url.searchParams.get('sessionId') ?? ''
|
|
164
|
+
const seq = Number(url.searchParams.get('seq') ?? -1)
|
|
165
|
+
const index = Number(url.searchParams.get('index') ?? -1)
|
|
166
|
+
const diff = await ctx.get('workspaceChanges')?.diff(sessionId, seq, index, AbortSignal.timeout(30_000))
|
|
167
|
+
if (diff === undefined) return send(res, 404, { error: 'change comparison unavailable' })
|
|
168
|
+
send(res, 200, diff)
|
|
169
|
+
}],
|
|
170
|
+
['GET', `${BASE}/workspaces`, async (req, res) => {
|
|
171
|
+
// `ctx.get` does not require an inject declaration, unlike `ctx.<name>`;
|
|
172
|
+
// this row only injects webServer and must not fail at request time.
|
|
173
|
+
const list = ctx.get('workspaceRegistry')?.list?.() ?? []
|
|
174
|
+
send(res, 200, {
|
|
175
|
+
workspaces: list.map((w) => ({
|
|
176
|
+
id: w.id ?? w.workspaceId,
|
|
177
|
+
path: w.path ?? w.directory ?? '',
|
|
178
|
+
title: w.title ?? w.name ?? '',
|
|
179
|
+
})),
|
|
180
|
+
})
|
|
181
|
+
}],
|
|
182
|
+
['GET', `${BASE}/browse`, async (req, res) => {
|
|
183
|
+
const url = new URL(req.url, 'http://localhost')
|
|
184
|
+
const distro = url.searchParams.get('distro') ?? ''
|
|
185
|
+
const user = url.searchParams.get('user') ?? userFor(distro)
|
|
186
|
+
const wanted = url.searchParams.get('path') ?? ''
|
|
187
|
+
const broker = registry.get(distro, user)
|
|
188
|
+
const hello = await broker.connect().then(() => broker.hello)
|
|
189
|
+
const target = wanted || hello.home
|
|
190
|
+
const listing = await broker.call('list', { path: target })
|
|
191
|
+
const dirs = listing.entries
|
|
192
|
+
.filter((e) => e.dir)
|
|
193
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
194
|
+
.map((e) => ({ name: e.name, path: target.replace(/\/$/, '') + '/' + e.name }))
|
|
195
|
+
const parent = target.replace(/\/+$/, '').replace(/\/[^/]*$/, '') || '/'
|
|
196
|
+
send(res, 200, { distro, user, path: target, parent, home: hello.home, dirs })
|
|
197
|
+
}],
|
|
198
|
+
['POST', `${BASE}/connect`, async (req, res) => {
|
|
199
|
+
const body = await readBody(req)
|
|
200
|
+
const distro = body.distro ?? ''
|
|
201
|
+
const broker = registry.get(distro, body.user ?? userFor(distro))
|
|
202
|
+
await broker.connect()
|
|
203
|
+
send(res, 200, { ok: true, broker: broker.info, hello: broker.hello })
|
|
204
|
+
}],
|
|
205
|
+
['POST', `${BASE}/disconnect`, async (req, res) => {
|
|
206
|
+
const body = await readBody(req)
|
|
207
|
+
await registry.peek(body.distro ?? '', body.user ?? userFor(body.distro ?? ''))?.disconnect('panel')
|
|
208
|
+
send(res, 200, { ok: true })
|
|
209
|
+
}],
|
|
210
|
+
['POST', `${BASE}/idle`, async (req, res) => {
|
|
211
|
+
const body = await readBody(req)
|
|
212
|
+
const days = body.days !== undefined ? Number(body.days) : Number(body.minutes) / (24 * 60)
|
|
213
|
+
if (!Number.isFinite(days) || days < 0) return send(res, 400, { error: 'days must be a non-negative number' })
|
|
214
|
+
const saved = writeSettings({ idleDays: days })
|
|
215
|
+
registry.idleMinutes = days * 24 * 60
|
|
216
|
+
send(res, 200, { ok: true, idleDays: saved.idleDays, idleMinutes: registry.idleMinutes })
|
|
217
|
+
}],
|
|
218
|
+
]
|
|
219
|
+
|
|
220
|
+
const disposers = routes.map(([method, path, run]) => registerExactRoute(webServer, {
|
|
221
|
+
kind: 'exact',
|
|
222
|
+
path,
|
|
223
|
+
handler: async (req, res) => {
|
|
224
|
+
if (method !== '*' && (req.method ?? 'GET') !== method) return send(res, 405, { error: 'method not allowed', method: req.method })
|
|
225
|
+
try {
|
|
226
|
+
await run(req, res)
|
|
227
|
+
} catch (error) {
|
|
228
|
+
send(res, 500, { error: String(error?.message ?? error) })
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
}))
|
|
232
|
+
|
|
233
|
+
// A distro whose default user is root should not silently run the Agent as
|
|
234
|
+
// root. Detect the first normal user at Host boot and persist it as the
|
|
235
|
+
// default for that distro (no-op when the distro default is already normal).
|
|
236
|
+
const applyDefaultUsers = async () => {
|
|
237
|
+
try {
|
|
238
|
+
const settings = readSettings()
|
|
239
|
+
let changed = false
|
|
240
|
+
for (const distro of await listWslDistros()) {
|
|
241
|
+
if (settings.users[distro]) continue
|
|
242
|
+
const info = await detectUsers(distro)
|
|
243
|
+
if (info.root && info.users.length > 0 && !settings.users[distro]) {
|
|
244
|
+
settings.users[distro] = info.users[0].name
|
|
245
|
+
changed = true
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (changed) writeSettings({ users: settings.users })
|
|
249
|
+
} catch {}
|
|
250
|
+
}
|
|
251
|
+
void applyDefaultUsers()
|
|
252
|
+
|
|
253
|
+
return () => {
|
|
254
|
+
presetDefault.dispose()
|
|
255
|
+
modelCwd.dispose()
|
|
256
|
+
fileReferences.dispose()
|
|
257
|
+
changesPaths.dispose()
|
|
258
|
+
wslDiff.dispose()
|
|
259
|
+
for (const dispose of disposers) dispose()
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
package/icon.svg
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="256" height="256" role="img" aria-label="WSL penguin outline">
|
|
2
|
+
<g fill="none" stroke="#334155" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
3
|
+
<path d="M12 2.5c-2.3 0-4.1 1.9-4.1 4.2 0 1.4.6 2.6 1.6 3.4-1.1 1.2-1.9 2.9-2.2 4.7-.4 2.5.1 4.6 1.8 5.5 1.6.9 4.3.9 5.9 0 1.7-.9 2.2-3 1.8-5.5-.3-1.8-1.1-3.5-2.2-4.7 1-.8 1.6-2 1.6-3.4 0-2.3-1.8-4.2-4.2-4.2Z" fill="#ffffff"/>
|
|
4
|
+
<path d="M7.4 11.3c-1.5 1-2.4 2.7-2.3 4.5.1 1 .6 1.7 1.3 2.1"/>
|
|
5
|
+
<path d="M16.6 11.3c1.5 1 2.4 2.7 2.3 4.5-.1 1-.6 1.7-1.3 2.1"/>
|
|
6
|
+
<path d="M8.4 21.2c-.8 0-1.5.3-2 .8M15.6 21.2c.8 0 1.5.3 2 .8"/>
|
|
7
|
+
</g>
|
|
8
|
+
<circle cx="10.2" cy="7.8" r="0.7" fill="#334155"/>
|
|
9
|
+
<circle cx="13.8" cy="7.8" r="0.7" fill="#334155"/>
|
|
10
|
+
<path d="M10.9 9.1l1.1 1.5 1.1-1.5z" fill="#334155"/>
|
|
11
|
+
</svg>
|
package/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Bundle entry: the patch mounts a row named after this package, which is how
|
|
2
|
+
// the client scanner discovers `dsh.client` and how the panel's host routes
|
|
3
|
+
// come up. The WSL execution world lives in ./world.js and is mounted by the
|
|
4
|
+
// preset instead.
|
|
5
|
+
export { name, inject, apply } from './host.js'
|
package/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,88 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chronosun/dsh-wsl",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"version": "1.5.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "WSL execution world for DeepSeek Harness: a Linux bash/terminal/fs world inside a local WSL distribution, plus a connection panel, Doctor, and a WSL workspace picker.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"dsh",
|
|
12
|
+
"dsh-plugin",
|
|
13
|
+
"deepseek-harness",
|
|
14
|
+
"wsl",
|
|
15
|
+
"wsl2",
|
|
16
|
+
"linux",
|
|
17
|
+
"agent",
|
|
18
|
+
"coding-agent"
|
|
19
|
+
],
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=22"
|
|
22
|
+
},
|
|
23
|
+
"main": "index.js",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": "./index.js",
|
|
26
|
+
"./host": "./host.js",
|
|
27
|
+
"./world": "./world.js",
|
|
28
|
+
"./fs": "./fs-service.mjs",
|
|
29
|
+
"./search": "./search-tools.mjs",
|
|
30
|
+
"./prompt": "./prompt.mjs",
|
|
31
|
+
"./terminal-launcher": "./terminal-launcher.mjs",
|
|
32
|
+
"./worktree": "./worktree-tool-v2.mjs",
|
|
33
|
+
"./client": "./client.js",
|
|
34
|
+
"./icon": "./icon.svg",
|
|
35
|
+
"./locale/*.json": "./locale/*.json",
|
|
36
|
+
"./package.json": "./package.json"
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"index.js",
|
|
40
|
+
"host.js",
|
|
41
|
+
"world.js",
|
|
42
|
+
"fs-service.mjs",
|
|
43
|
+
"search-tools.mjs",
|
|
44
|
+
"prompt.mjs",
|
|
45
|
+
"terminal-launcher.mjs",
|
|
46
|
+
"worktree-tool-v2.mjs",
|
|
47
|
+
"client.js",
|
|
48
|
+
"README.md",
|
|
49
|
+
"LICENSE",
|
|
50
|
+
"cordis.patch.yml",
|
|
51
|
+
"icon.svg",
|
|
52
|
+
"locale/*.json",
|
|
53
|
+
"src/"
|
|
54
|
+
],
|
|
55
|
+
"dsh": {
|
|
56
|
+
"bundle": {
|
|
57
|
+
"patch": "./cordis.patch.yml"
|
|
58
|
+
},
|
|
59
|
+
"client": {
|
|
60
|
+
"platform": "web",
|
|
61
|
+
"immediately": true,
|
|
62
|
+
"inject": [
|
|
63
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
64
|
+
"@deepseek-ai/dsh-client-ui-sidebar"
|
|
65
|
+
]
|
|
66
|
+
},
|
|
67
|
+
"compatibility": {
|
|
68
|
+
"dshReleases": {
|
|
69
|
+
"0.2.2-rc.1": "compatible"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
"engines": {
|
|
74
|
+
"dsh": ">=0.2.2-rc.1"
|
|
75
|
+
},
|
|
76
|
+
"peerDependencies": {
|
|
77
|
+
"@deepseek-ai/cordis": "*",
|
|
78
|
+
"@deepseek-ai/dsh-subprocess": "*"
|
|
79
|
+
},
|
|
80
|
+
"peerDependenciesMeta": {
|
|
81
|
+
"@deepseek-ai/cordis": {
|
|
82
|
+
"optional": true
|
|
83
|
+
},
|
|
84
|
+
"@deepseek-ai/dsh-subprocess": {
|
|
85
|
+
"optional": true
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
package/prompt.mjs
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Prompt section: tell the model it is in Linux, and how to translate the
|
|
2
|
+
// Windows-shaped working directory it is still shown.
|
|
3
|
+
//
|
|
4
|
+
// A stable system section (not per-turn context) so the cached prefix is not
|
|
5
|
+
// perturbed; the text is static, so it never invalidates mid-session.
|
|
6
|
+
export const name = 'dsh-wsl/prompt'
|
|
7
|
+
export const inject = ['systemPrompt']
|
|
8
|
+
|
|
9
|
+
export function apply(ctx, config = {}) {
|
|
10
|
+
const distro = config.distro ? ` (distribution: ${config.distro})` : ''
|
|
11
|
+
const text = [
|
|
12
|
+
'# Execution environment',
|
|
13
|
+
`This session executes inside a WSL distribution${distro} on the user's machine, so the execution world is **Linux, not Windows**.`,
|
|
14
|
+
'',
|
|
15
|
+
'- **Every tool takes Linux paths.** `Current working directory` is a Linux path such as `/home/me/proj`. If a Windows view shows the same directory as `\\\\wsl.localhost\\<distro>\\...`, the Linux equivalent is that path with the prefix removed and backslashes turned into slashes; always use the Linux form in tool calls and in code you write.',
|
|
16
|
+
'- Windows drives are mounted at `/mnt/<drive>`, so `C:\\Users\\me` is `/mnt/c/Users/me`.',
|
|
17
|
+
'- `bash` runs `/bin/bash` inside the distribution with a PATH that contains no `/mnt/*` entries, so Windows binaries are deliberately unreachable; use the distribution\'s own `node`, `npm`, `git`, `python3`.',
|
|
18
|
+
'- `ssh`, `scp`, `rsync` and `curl` are the distribution\'s own binaries; keys and `~/.ssh/config` are the distribution user\'s. The Agent has no TTY, so use key-based auth (`ssh -o BatchMode=yes host \'command\'`) or `ControlMaster` instead of an interactive password prompt; use the sidebar terminal for interactive logins.',
|
|
19
|
+
'- `read`, `write`, `edit`, `glob` and `grep` operate directly on the Linux filesystem. `glob` and `grep` require an explicit `path`.',
|
|
20
|
+
'- `web_fetch` / `web_search` run on the host but share the network. A service started inside the distribution is reachable at `http://localhost:<port>` (WSL2 localhost forwarding); use `curl` in `bash` when it must run from Linux or through an SSH tunnel.',
|
|
21
|
+
'- Quote shell variables (`"$VAR"`): this Harness build parses some commands twice, and an unquoted value containing spaces or parentheses can break the command.',
|
|
22
|
+
].join('\n')
|
|
23
|
+
|
|
24
|
+
return ctx.systemPrompt.section({ name: 'wsl-world', order: 820, text })
|
|
25
|
+
}
|
package/search-tools.mjs
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// `grep` and `glob` inside the WSL world.
|
|
2
|
+
//
|
|
3
|
+
// The host's own search package spawns a bundled **Windows** ripgrep, which a
|
|
4
|
+
// Linux world cannot run. These tools walk the tree inside the distribution
|
|
5
|
+
// instead, through the broker, with no external binary at all.
|
|
6
|
+
import { registry } from './src/registry.mjs'
|
|
7
|
+
import { userFor } from './src/settings.mjs'
|
|
8
|
+
import { userFromLinuxPath } from './src/path-user.mjs'
|
|
9
|
+
|
|
10
|
+
const text = (value) => [{ type: 'text', text: value }]
|
|
11
|
+
|
|
12
|
+
/** The model still sees a UNC cwd, so a host path may arrive here. */
|
|
13
|
+
function toLinuxPath(value, fallback) {
|
|
14
|
+
const raw = String(value ?? '')
|
|
15
|
+
if (!raw) return fallback ?? '/'
|
|
16
|
+
if (raw.startsWith('/')) return raw
|
|
17
|
+
const unc = /^\\\\[?]?(?:wsl\.localhost|wsl\$)\\([^\\]+)\\(.*)$/i.exec(raw)
|
|
18
|
+
if (unc) return `/${unc[2].replace(/\\/g, '/')}`
|
|
19
|
+
const drive = /^([A-Za-z]):[\\/](.*)$/.exec(raw)
|
|
20
|
+
if (drive) return `/mnt/${drive[1].toLowerCase()}/${drive[2].replace(/\\/g, '/')}`
|
|
21
|
+
return fallback ?? raw
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function brokerFor(config, cwd = '') {
|
|
25
|
+
const distro = config?.distro ?? ''
|
|
26
|
+
return registry.get(distro, config?.user || userFromLinuxPath(cwd) || userFor(distro))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const name = 'dsh-wsl/search'
|
|
30
|
+
export const inject = ['tools']
|
|
31
|
+
|
|
32
|
+
export function apply(ctx, config = {}) {
|
|
33
|
+
const disposers = []
|
|
34
|
+
|
|
35
|
+
disposers.push(ctx.tools.register({
|
|
36
|
+
name: 'glob',
|
|
37
|
+
description: 'Find files by glob pattern inside the WSL distribution. Returns paths, oldest-first is not guaranteed; hidden files are included. In a Git worktree, files ignored by .gitignore are skipped; otherwise .git and node_modules are pruned. A pattern without "/" matches basenames at any depth.',
|
|
38
|
+
parameters: {
|
|
39
|
+
type: 'object',
|
|
40
|
+
properties: {
|
|
41
|
+
pattern: { type: 'string', description: 'Glob such as "**/*.ts" or "*.json".' },
|
|
42
|
+
path: { type: 'string', description: 'Directory to search (Linux path). Required: pass the session working directory.' },
|
|
43
|
+
},
|
|
44
|
+
required: ['pattern', 'path'],
|
|
45
|
+
additionalProperties: false,
|
|
46
|
+
},
|
|
47
|
+
output: {
|
|
48
|
+
schema: { type: 'object', properties: { paths: { type: 'array' }, truncated: { type: 'boolean' } }, additionalProperties: true },
|
|
49
|
+
render: (_args, value) => text((value.paths ?? []).join('\n') || 'no files matched'),
|
|
50
|
+
},
|
|
51
|
+
async execute(args, exec) {
|
|
52
|
+
const cwd = exec?.agent?.cwd ?? config.defaultCwd ?? '/'
|
|
53
|
+
const root = toLinuxPath(args.path, cwd)
|
|
54
|
+
return brokerFor(config, cwd).call('glob', { path: root, pattern: args.pattern, max: 1000 })
|
|
55
|
+
},
|
|
56
|
+
}))
|
|
57
|
+
|
|
58
|
+
disposers.push(ctx.tools.register({
|
|
59
|
+
name: 'grep',
|
|
60
|
+
description: 'Search file contents with a JavaScript regular expression inside the WSL distribution. Results are grouped by file with line numbers. In a Git worktree, files ignored by .gitignore are skipped; otherwise .git and node_modules are pruned; files over 2 MiB are skipped.',
|
|
61
|
+
parameters: {
|
|
62
|
+
type: 'object',
|
|
63
|
+
properties: {
|
|
64
|
+
pattern: { type: 'string', description: 'JavaScript regular expression.' },
|
|
65
|
+
path: { type: 'string', description: 'Directory to search (Linux path). Required: pass the session working directory.' },
|
|
66
|
+
include: { type: 'string', description: 'Optional file-name glob filter, such as "*.ts".' },
|
|
67
|
+
},
|
|
68
|
+
required: ['pattern', 'path'],
|
|
69
|
+
additionalProperties: false,
|
|
70
|
+
},
|
|
71
|
+
output: {
|
|
72
|
+
schema: { type: 'object', properties: { files: { type: 'array' }, truncated: { type: 'boolean' } }, additionalProperties: true },
|
|
73
|
+
render: (_args, value) => {
|
|
74
|
+
const lines = []
|
|
75
|
+
for (const file of value.files ?? []) {
|
|
76
|
+
for (const match of file.matches ?? []) lines.push(`${file.path}:${match.lineNumber}: ${match.line}`)
|
|
77
|
+
}
|
|
78
|
+
if (value.truncated) lines.push('[truncated]')
|
|
79
|
+
return text(lines.join('\n') || 'no matches')
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
async execute(args, exec) {
|
|
83
|
+
const cwd = exec?.agent?.cwd ?? config.defaultCwd ?? '/'
|
|
84
|
+
const root = toLinuxPath(args.path, cwd)
|
|
85
|
+
return brokerFor(config, cwd).call('grep', { path: root, pattern: args.pattern, include: args.include, max: 250 })
|
|
86
|
+
},
|
|
87
|
+
}))
|
|
88
|
+
|
|
89
|
+
return () => { for (const dispose of disposers) dispose() }
|
|
90
|
+
}
|
|
91
|
+
|