dsh-vscode-workbench 0.1.11 → 0.1.12
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.en.md +21 -2
- package/README.md +21 -2
- package/lib/client.js +293 -104
- package/lib/client.js.map +1 -1
- package/lib/index.js +93 -41
- package/lib/index.js.map +1 -1
- package/lib/types/client/Workbench.d.ts +3 -0
- package/lib/types/client/terminal.d.ts +1 -0
- package/lib/types/client/websocket.d.ts +15 -0
- package/lib/types/terminal.d.ts +5 -0
- package/package.json +1 -1
- package/src/client/DshConversationPanel.tsx +2 -0
- package/src/client/TerminalPanel.tsx +12 -5
- package/src/client/Workbench.tsx +140 -15
- package/src/client/styles.ts +9 -4
- package/src/client/terminal.ts +15 -4
- package/src/client/websocket.ts +50 -0
- package/src/host.ts +47 -14
- package/src/terminal.ts +27 -2
package/src/host.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { createHash, randomUUID } from 'node:crypto'
|
|
|
2
2
|
import { spawn } from 'node:child_process'
|
|
3
3
|
import { createRequire } from 'node:module'
|
|
4
4
|
import { dirname, extname, isAbsolute, join, normalize, relative, resolve, sep } from 'node:path'
|
|
5
|
-
import { access, lstat, mkdir, open, readFile, readdir, realpath, rename, rm, stat
|
|
5
|
+
import { access, lstat, mkdir, open, readFile, readdir, realpath, rename, rm, stat } from 'node:fs/promises'
|
|
6
6
|
import { constants } from 'node:fs'
|
|
7
7
|
import type { HostContext } from './context.ts'
|
|
8
8
|
import type { FileDocument, FileEntry, GitChange, GitCommit, GitCommitFile, GitDiff, GitStatus, SearchMatch, SearchRequest } from './types.ts'
|
|
@@ -83,14 +83,42 @@ export async function readDocument(cwd: string, path: string): Promise<FileDocum
|
|
|
83
83
|
} finally { await handle.close() }
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
const pendingSaves = new Map<string, Promise<void>>()
|
|
87
|
+
|
|
86
88
|
export async function saveDocument(cwd: string, path: string, content: string, expected: string): Promise<FileDocument> {
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
89
|
+
const resolved = await workspacePath(cwd, path)
|
|
90
|
+
const absolute = await realpath(resolved.absolute)
|
|
91
|
+
const previous = pendingSaves.get(absolute) ?? Promise.resolve()
|
|
92
|
+
let release!: () => void
|
|
93
|
+
const pending = new Promise<void>(resolve => { release = resolve })
|
|
94
|
+
pendingSaves.set(absolute, pending)
|
|
95
|
+
await previous
|
|
96
|
+
let temp: string | undefined
|
|
97
|
+
try {
|
|
98
|
+
const checkRevision = async () => {
|
|
99
|
+
const checked = await workspacePath(cwd, path)
|
|
100
|
+
if (await realpath(checked.absolute) !== absolute || (await readDocument(cwd, path)).revision !== expected) {
|
|
101
|
+
throw new WorkbenchError('revision-conflict', 'file changed on disk; reload before saving', 409)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
await checkRevision()
|
|
105
|
+
const info = await stat(absolute)
|
|
106
|
+
temp = join(dirname(absolute), `.${randomUUID()}.dsh-vscode.tmp`)
|
|
107
|
+
const handle = await open(temp, 'wx', info.mode & 0o777)
|
|
108
|
+
try {
|
|
109
|
+
await handle.writeFile(content, { encoding: 'utf8' })
|
|
110
|
+
await handle.chmod(info.mode & 0o777)
|
|
111
|
+
} finally { await handle.close() }
|
|
112
|
+
await checkRevision()
|
|
113
|
+
await rename(temp, absolute)
|
|
114
|
+
return await readDocument(cwd, path)
|
|
115
|
+
} finally {
|
|
116
|
+
try { if (temp !== undefined) await rm(temp, { force: true }) }
|
|
117
|
+
finally {
|
|
118
|
+
release()
|
|
119
|
+
if (pendingSaves.get(absolute) === pending) pendingSaves.delete(absolute)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
94
122
|
}
|
|
95
123
|
|
|
96
124
|
export async function createEntry(cwd: string, path: string, kind: 'file' | 'directory'): Promise<void> {
|
|
@@ -120,8 +148,8 @@ export async function renameEntry(cwd: string, path: string, nextPath: string):
|
|
|
120
148
|
}
|
|
121
149
|
|
|
122
150
|
export async function deleteEntry(cwd: string, path: string): Promise<void> {
|
|
123
|
-
|
|
124
|
-
|
|
151
|
+
const { root, absolute } = await workspacePath(cwd, path)
|
|
152
|
+
if (absolute === root) throw new WorkbenchError('root-delete', 'cannot delete workspace root', 403)
|
|
125
153
|
await rm(absolute, { recursive: true, force: false })
|
|
126
154
|
}
|
|
127
155
|
|
|
@@ -220,14 +248,19 @@ export async function gitStatus(cwd: string): Promise<GitStatus> {
|
|
|
220
248
|
if (result.code !== 0) throw new WorkbenchError('git-failed', result.stderr, 500)
|
|
221
249
|
let branch = 'HEAD'; let ahead = 0; let behind = 0
|
|
222
250
|
const changes: GitChange[] = []
|
|
223
|
-
|
|
251
|
+
const records = result.stdout.split('\0')
|
|
252
|
+
for (let index = 0; index < records.length; index++) {
|
|
253
|
+
const record = records[index]!
|
|
224
254
|
if (record.startsWith('# branch.head ')) branch = record.slice(14)
|
|
225
255
|
else if (record.startsWith('# branch.ab ')) {
|
|
226
256
|
const match = /\+(\d+) -(\d+)/.exec(record); ahead = Number(match?.[1] ?? 0); behind = Number(match?.[2] ?? 0)
|
|
227
257
|
} else if (record.startsWith('? ')) changes.push({ path: record.slice(2), index: '?', worktree: '?', staged: false, untracked: true })
|
|
228
|
-
else if (record.startsWith('1 ') || record.startsWith('2 ')) {
|
|
229
|
-
const
|
|
230
|
-
|
|
258
|
+
else if (record.startsWith('1 ') || record.startsWith('2 ') || record.startsWith('u ')) {
|
|
259
|
+
const renamed = record.startsWith('2 ')
|
|
260
|
+
const parts = record.split(' '); const xy = parts[1] ?? '..'
|
|
261
|
+
const path = parts.slice(renamed ? 9 : record.startsWith('u ') ? 10 : 8).join(' ')
|
|
262
|
+
const originalPath = renamed ? records[++index] : undefined
|
|
263
|
+
changes.push({ path, ...(originalPath === undefined ? {} : { originalPath }), index: xy[0] ?? '.', worktree: xy[1] ?? '.', staged: xy[0] !== '.', untracked: false })
|
|
231
264
|
}
|
|
232
265
|
}
|
|
233
266
|
return { repository, branch, ahead, behind, changes }
|
package/src/terminal.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto'
|
|
2
|
-
import { realpathSync } from 'node:fs'
|
|
3
|
-
import {
|
|
2
|
+
import { chmodSync, realpathSync, statSync } from 'node:fs'
|
|
3
|
+
import { createRequire } from 'node:module'
|
|
4
|
+
import { basename, dirname, join } from 'node:path'
|
|
4
5
|
import type { IncomingMessage } from 'node:http'
|
|
5
6
|
import type { Duplex } from 'node:stream'
|
|
6
7
|
import * as pty from 'node-pty'
|
|
@@ -10,6 +11,8 @@ import type { HostContext } from './context.ts'
|
|
|
10
11
|
import { cwdFor } from './host.ts'
|
|
11
12
|
import { authorize, readBody, send, sendError } from './wire.ts'
|
|
12
13
|
|
|
14
|
+
const require = createRequire(import.meta.url)
|
|
15
|
+
|
|
13
16
|
export interface TerminalInfo { id: string; title: string; cwd: string; running: boolean; exitCode?: number; sequence: number }
|
|
14
17
|
export interface TerminalOutput { sequence: number; data: string }
|
|
15
18
|
|
|
@@ -29,10 +32,32 @@ export function defaultTerminalShell(platform = process.platform, env = process.
|
|
|
29
32
|
return { shell: env.SHELL || (platform === 'darwin' ? '/bin/zsh' : '/bin/bash'), args: ['-l'] }
|
|
30
33
|
}
|
|
31
34
|
|
|
35
|
+
/** Add execute bits when a registry package extracted a native helper without them. */
|
|
36
|
+
export function ensureExecutable(path: string): boolean {
|
|
37
|
+
if (process.platform === 'win32') return true
|
|
38
|
+
try {
|
|
39
|
+
const mode = statSync(path).mode
|
|
40
|
+
if ((mode & 0o111) === 0) chmodSync(path, mode | 0o111)
|
|
41
|
+
return true
|
|
42
|
+
} catch {
|
|
43
|
+
return false
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Repair node-pty's macOS/Linux helper, whose npm package currently loses its execute bit. */
|
|
48
|
+
export function repairNodePtyHelper(): void {
|
|
49
|
+
if (process.platform === 'win32') return
|
|
50
|
+
const packageDirectory = dirname(require.resolve('node-pty/package.json'))
|
|
51
|
+
ensureExecutable(join(packageDirectory, 'build', 'Release', 'spawn-helper'))
|
|
52
|
+
ensureExecutable(join(packageDirectory, 'prebuilds', `${process.platform}-${process.arch}`, 'spawn-helper'))
|
|
53
|
+
}
|
|
54
|
+
|
|
32
55
|
/** Own interactive terminals by their canonical workspace directory. */
|
|
33
56
|
export class TerminalManager {
|
|
34
57
|
private readonly workspaces = new Map<string, Map<string, TerminalHandle>>()
|
|
35
58
|
|
|
59
|
+
constructor() { repairNodePtyHelper() }
|
|
60
|
+
|
|
36
61
|
private workspace(cwd: string): string { return realpathSync(cwd) }
|
|
37
62
|
|
|
38
63
|
list(cwd: string): TerminalInfo[] { return [...(this.workspaces.get(this.workspace(cwd))?.values() ?? [])].map(handle => ({ ...handle.info })) }
|