dsh-vscode-mode 0.5.1 → 0.5.2
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.md +9 -2
- package/lib/index.js +183 -40
- package/lib/index.js.map +1 -1
- package/package.json +2 -1
- package/src/compat.ts +2 -2
- package/src/fileOpenSettings.ts +86 -10
- package/src/lsp/extmgr.ts +56 -3
- package/src/lsp/zip.ts +25 -9
- package/src/revert.ts +20 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vscode-mode",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "DSH 上的类 VSCode 编码体验:Monaco 编辑器(文件页签/QuickOpen/状态栏,可驻 DSH 0.1.5+ 官方右侧 Sidebar 与对话同屏)+ 命令栏(Ctrl+Shift+P)与可扩展指令系统 + VS Code 兼容代码片段(.code-snippets 全局/项目,IntelliSense 展开)+ Agent 编辑差异审查(整文件差异/采纳/拒绝/归档/回滚)+ SVN 集成(侧栏变更面板/日志弹窗/并排差异)+ 语言服务器(LSP)智能(跳转定义/引用/hover/大纲 + VSIX 扩展市场安装),状态持久化到工作区旁车",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
"@deepseek-ai/dsh-client-ui-primitives": ">=0.1.0-rc.1 <0.2.0-0 || >=0.1.1-0 <0.2.0-0 || >=0.1.2-0 <0.2.0-0 || >=0.1.3-0 <0.2.0-0 || >=0.1.4-0 <0.2.0-0 || >=0.1.5-0 <0.2.0-0 || >=0.1.6-0 <0.2.0-0",
|
|
52
52
|
"@deepseek-ai/dsh-llm": ">=0.0.1-rc.1 <0.1.0 || >=0.1.0-rc.1 <0.2.0-0 || >=0.1.1-0 <0.2.0-0 || >=0.1.2-0 <0.2.0-0 || >=0.1.3-0 <0.2.0-0 || >=0.1.4-0 <0.2.0-0 || >=0.1.5-0 <0.2.0-0 || >=0.1.6-0 <0.2.0-0",
|
|
53
53
|
"@deepseek-ai/dsh-tools": ">=0.0.1-rc.1 <0.1.0 || >=0.1.0-rc.1 <0.2.0-0 || >=0.1.1-0 <0.2.0-0 || >=0.1.2-0 <0.2.0-0 || >=0.1.3-0 <0.2.0-0 || >=0.1.4-0 <0.2.0-0 || >=0.1.5-0 <0.2.0-0 || >=0.1.6-0 <0.2.0-0",
|
|
54
|
+
"@deepseek-ai/schemastery": ">=3.18.0 <4",
|
|
54
55
|
"cordis": ">=4.0.0-rc <5",
|
|
55
56
|
"react": "^18.2.0",
|
|
56
57
|
"react-dom": "^18.2.0",
|
package/src/compat.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { readFileSync } from 'node:fs'
|
|
|
10
10
|
import { dirname, join } from 'node:path'
|
|
11
11
|
import { fileURLToPath } from 'node:url'
|
|
12
12
|
import { entriesOf } from './mcp.js'
|
|
13
|
-
import { loadSettingsDeps, settingsInstallNote, settingsInstallStrategy } from './fileOpenSettings.js'
|
|
13
|
+
import { loadSettingsDeps, schemaLibName, settingsInstallNote, settingsInstallStrategy } from './fileOpenSettings.js'
|
|
14
14
|
import { readDevForm } from './devForm.js'
|
|
15
15
|
import { compareDshVersions, detectDshVersion, familyLabel, parseDshVersion } from './dshVersion.js'
|
|
16
16
|
import { SKILL_PREFIXES, skillGroupState } from './skills.js'
|
|
@@ -59,7 +59,7 @@ export function detectExternal(ctx: Ctx, depsAvailable: boolean): CompatAdapter[
|
|
|
59
59
|
const skills = skillGroupState()
|
|
60
60
|
return [
|
|
61
61
|
{ name: MCP_PACKAGE, active: mcpCount > 0, note: mcpCount > 0 ? mcpCount + ' 个 MCP 服务条目' : '未检测到 MCP 条目(MCP 管理页显示为空)' },
|
|
62
|
-
{ name: '设置持久化(@deepseek-ai/dsh-settings)', active: depsAvailable, note: depsAvailable ? '设置 section
|
|
62
|
+
{ name: '设置持久化(@deepseek-ai/dsh-settings)', active: depsAvailable, note: depsAvailable ? '设置 section 已安装(schema: ' + (schemaLibName() || '未知') + ')' : '未安装:fileOpenTool 持久化降级为配置值' },
|
|
63
63
|
{ name: 'settings 服务', active: hasSettings, note: hasSettings ? '可读写设置' : '不可用(设置读写走配置回退)' },
|
|
64
64
|
{ name: '文件浏览器打开(subprocess 服务)', active: typeof sub?.spawn === 'function', note: typeof sub?.spawn === 'function' ? '可定位/打开 OS 文件浏览器' : '不可用(右键「在文件浏览器中打开」将提示失败)' },
|
|
65
65
|
// 新增项一律追加在末尾:既有下标被 tests/compat.test.ts 断言,不得前插。
|
package/src/fileOpenSettings.ts
CHANGED
|
@@ -73,20 +73,96 @@ async function hostImport(specifier: string): Promise<unknown> {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
/**
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
76
|
+
* schema 库候选名(新名在前)。
|
|
77
|
+
* DSH 官方自 0.1.5 起把 vendored 包改名为 @deepseek-ai/schemastery 并全树改用新名,
|
|
78
|
+
* 安装树里**没有**裸 schemastery;裸名保留给 rc 线(其 dsh-settings 仍 peers 旧名),
|
|
79
|
+
* 也让开发形态(插件 node_modules 有 devDependency 副本)继续可用。
|
|
80
|
+
*/
|
|
81
|
+
const SCHEMA_SPECIFIERS = ['@deepseek-ai/schemastery', 'schemastery'] as const
|
|
82
|
+
|
|
83
|
+
/** 设置持久化包名(installSettingsSection 仅 rc 线提供,alpha 线缺失属正常)。 */
|
|
84
|
+
const SETTINGS_SPECIFIER = '@deepseek-ai/dsh-settings'
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 逐个尝试候选名,返回首个加载成功的模块及其包名(全失败返回 null,不抛错)。
|
|
88
|
+
* @author ddj 2026年09月18号
|
|
89
|
+
* @param specifiers 候选包名(按优先级)
|
|
90
|
+
* @param importFn 加载函数(测试注入)
|
|
91
|
+
* @returns 命中的模块与包名;全失败返回 null
|
|
92
|
+
*/
|
|
93
|
+
async function firstImport(
|
|
94
|
+
specifiers: readonly string[],
|
|
95
|
+
importFn: (specifier: string) => Promise<unknown> = hostImport,
|
|
96
|
+
): Promise<{ specifier: string; module: unknown } | null> {
|
|
97
|
+
for (const specifier of specifiers) {
|
|
98
|
+
try {
|
|
99
|
+
return { specifier, module: await importFn(specifier) }
|
|
100
|
+
} catch {
|
|
101
|
+
/* 该候选不可解析:尝试下一个 */
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return null
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 抹平 schema 库的 ESM/CJS 互操作形态取默认导出。
|
|
109
|
+
* 三种实测形态:真 ESM(`default` 即 z)、CJS 经 import()(`default` 与
|
|
110
|
+
* `module.exports` 同为 z)、双层包装(`default.default` 才是 z)。
|
|
111
|
+
* @author ddj 2026年09月18号
|
|
112
|
+
* @param module 加载到的模块命名空间(可空)
|
|
113
|
+
* @returns 具备 object/string 等构造器的 z;取不到返回 null
|
|
114
|
+
*/
|
|
115
|
+
export function pickSchema(module: unknown): SettingsDeps['z'] | null {
|
|
116
|
+
const layers = [module, (module as { default?: unknown } | null)?.default, (module as Record<string, unknown> | null)?.['module.exports']]
|
|
117
|
+
for (const layer of layers) {
|
|
118
|
+
const z = ((layer as { default?: unknown } | null)?.default ?? layer) as SettingsDeps['z'] | undefined
|
|
119
|
+
if (z && typeof z.object === 'function' && typeof z.string === 'function') return z
|
|
120
|
+
}
|
|
121
|
+
return null
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** 实际命中的 schema 库名(供兼容性报告展示;未命中为空串)。 */
|
|
125
|
+
let schemaLib = ''
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* 读取实际命中的 schema 库名(报告文案用)。
|
|
129
|
+
* @author ddj 2026年09月18号
|
|
130
|
+
* @returns 包名;未解析到为空串
|
|
131
|
+
*/
|
|
132
|
+
export function schemaLibName(): string {
|
|
133
|
+
return schemaLib
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** 复位依赖缓存与命中库名(测试隔离用)。 */
|
|
137
|
+
export function resetSettingsDeps(): void {
|
|
138
|
+
depsPromise = undefined
|
|
139
|
+
schemaLib = ''
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* 动态加载设置依赖(模块级缓存;schema 库缺失返回 null 而非抛错)。
|
|
144
|
+
* ⚠️ 两个依赖**独立解析**:@deepseek-ai/dsh-settings 仅 rc 线跑 legacy 策略时需要,
|
|
145
|
+
* alpha 线走 settings 服务 installSection 用不到它;原先 Promise.all 让该包缺失
|
|
146
|
+
* 拖垮整体 → 用户端(npm 安装)settings section 永不装配。
|
|
147
|
+
* 同理,schema 库按候选链解析(安装树只有新名 @deepseek-ai/schemastery)。
|
|
148
|
+
* @author ddj 2026年08月24号 / 2026年09月15号 / 2026年09月18号
|
|
149
|
+
* @param importFn 加载函数(测试注入;缺省宿主锚点动态导入)
|
|
80
150
|
* @returns 设置依赖或 null
|
|
81
151
|
*/
|
|
82
|
-
export function loadSettingsDeps(): Promise<SettingsDeps | null> {
|
|
152
|
+
export function loadSettingsDeps(importFn: (specifier: string) => Promise<unknown> = hostImport): Promise<SettingsDeps | null> {
|
|
83
153
|
if (!depsPromise) {
|
|
84
|
-
depsPromise = Promise.all([
|
|
85
|
-
.then(([
|
|
154
|
+
depsPromise = Promise.all([firstImport([SETTINGS_SPECIFIER], importFn), firstImport(SCHEMA_SPECIFIERS, importFn)])
|
|
155
|
+
.then(([settingsHit, schemaHit]) => {
|
|
156
|
+
const z = pickSchema(schemaHit?.module)
|
|
157
|
+
if (!z) {
|
|
158
|
+
schemaLib = ''
|
|
159
|
+
return null
|
|
160
|
+
}
|
|
161
|
+
schemaLib = schemaHit?.specifier ?? ''
|
|
86
162
|
// dsh-settings 类型声明随版本变化(rc.8 有 d.ts、alpha 已移除导出),统一经 unknown 松绑
|
|
87
|
-
installSettingsSection
|
|
88
|
-
|
|
89
|
-
})
|
|
163
|
+
const installSettingsSection = (settingsHit?.module as unknown as { installSettingsSection?: SettingsDeps['installSettingsSection'] } | undefined)?.installSettingsSection
|
|
164
|
+
return { installSettingsSection, z }
|
|
165
|
+
})
|
|
90
166
|
.catch(() => null)
|
|
91
167
|
}
|
|
92
168
|
return depsPromise
|
package/src/lsp/extmgr.ts
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
* 已装扩展作为 LSP provider 的"扩展源"(kind=extension),见 providers.ts。
|
|
6
6
|
* 作者 ddj 2026-08-27
|
|
7
7
|
*/
|
|
8
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs'
|
|
8
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs'
|
|
9
9
|
import { basename, dirname, join } from 'node:path'
|
|
10
|
-
import { unzip } from './zip.js'
|
|
10
|
+
import { unzip, type ZipEntry } from './zip.js'
|
|
11
11
|
import { dshHome, PLUGIN_ID } from '../paths.js'
|
|
12
12
|
|
|
13
13
|
/** 单个已装扩展信息(edrv.lsp.ext.list 载荷元素)。 */
|
|
@@ -57,9 +57,61 @@ export function vsixManifest(vsix: Buffer): Record<string, unknown> {
|
|
|
57
57
|
throw new Error('VSIX 缺少 extension/package.json')
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
/**
|
|
61
|
+
* 判定解包路径是否为可执行入口(无 mode 时的启发式兜底)。
|
|
62
|
+
* 只认确定语义:`bin/` 段下的文件、语言服务器/运行时常见可执行名、`.sh` 脚本。
|
|
63
|
+
* @author ddj 2026年09月18号
|
|
64
|
+
* @param rel 相对路径(/ 分隔)
|
|
65
|
+
* @returns 是否应补执行位
|
|
66
|
+
*/
|
|
67
|
+
export function looksExec(rel: string): boolean {
|
|
68
|
+
const path = String(rel ?? '').replace(/\\/g, '/')
|
|
69
|
+
if (!path) return false
|
|
70
|
+
if (path.split('/').includes('bin')) return true
|
|
71
|
+
if (/\.(sh|bash|zsh)$/i.test(path)) return true
|
|
72
|
+
const name = path.split('/').pop() ?? ''
|
|
73
|
+
const base = name.replace(/\.exe$/i, '')
|
|
74
|
+
return ['lua-language-server', 'emmylua_ls', 'OmniSharp', 'dotnet', 'node'].includes(base)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 计算解包后要施加的权限位(纯函数,可单测)。
|
|
79
|
+
* 优先信任归档自带的 Unix mode 中的执行位(VS Code 自身解 VSIX 即此口径);
|
|
80
|
+
* 归档无 mode(Windows 打包的 VSIX)时按路径启发式补 0o755;
|
|
81
|
+
* 其余保持 undefined(交给 umask 默认,不越权改动)。
|
|
82
|
+
* @author ddj 2026年09月18号
|
|
83
|
+
* @param entry 解压条目
|
|
84
|
+
* @returns 目标权限位;无需处理返回 undefined
|
|
85
|
+
*/
|
|
86
|
+
export function execModeOf(entry: ZipEntry): number | undefined {
|
|
87
|
+
if (entry.isDirectory) return undefined
|
|
88
|
+
const mode = entry.mode
|
|
89
|
+
if (mode !== undefined && (mode & 0o111) !== 0) return mode
|
|
90
|
+
return looksExec(entry.path) ? 0o755 : undefined
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 落盘后补可执行位(POSIX 专属;Windows 无此概念,直接跳过)。
|
|
95
|
+
* 必须 best-effort:只读文件系统/无权限时不能拖垮整个扩展安装。
|
|
96
|
+
* @author ddj 2026年09月18号
|
|
97
|
+
* @param target 落盘绝对路径
|
|
98
|
+
* @param entry 解压条目
|
|
99
|
+
* @param platform 目标平台(测试注入)
|
|
100
|
+
*/
|
|
101
|
+
export function applyExecBit(target: string, entry: ZipEntry, platform: NodeJS.Platform = process.platform): void {
|
|
102
|
+
if (platform === 'win32') return
|
|
103
|
+
const mode = execModeOf(entry)
|
|
104
|
+
if (mode === undefined) return
|
|
105
|
+
try {
|
|
106
|
+
chmodSync(target, mode)
|
|
107
|
+
} catch (error) {
|
|
108
|
+
/* 权限补不上不阻塞安装(后续 spawn 会给出真实错误) */
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
60
112
|
/**
|
|
61
113
|
* 解包 VSIX 到目录(仅取 extension/ 子树,剥前缀;返回清单)。
|
|
62
|
-
* @author ddj 2026年08月27号
|
|
114
|
+
* @author ddj 2026年08月27号 / 2026年09月18号
|
|
63
115
|
* @param vsix vsix 字节
|
|
64
116
|
* @param dest 目标目录(已存在则先清空)
|
|
65
117
|
* @returns 清单对象
|
|
@@ -80,6 +132,7 @@ export function unpackVsix(vsix: Buffer, dest: string): Record<string, unknown>
|
|
|
80
132
|
}
|
|
81
133
|
mkdirSync(dirname(target), { recursive: true })
|
|
82
134
|
writeFileSync(target, entry.data)
|
|
135
|
+
applyExecBit(target, entry)
|
|
83
136
|
if (rel === 'package.json') manifest = JSON.parse(entry.data.toString('utf8')) as Record<string, unknown>
|
|
84
137
|
}
|
|
85
138
|
if (!manifest) throw new Error('VSIX 解包后缺少 package.json')
|
package/src/lsp/zip.ts
CHANGED
|
@@ -6,11 +6,13 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { inflateRawSync } from 'node:zlib'
|
|
8
8
|
|
|
9
|
-
/** 单条解压结果:相对路径 + 内容(目录条目 path 以 /
|
|
9
|
+
/** 单条解压结果:相对路径 + 内容(目录条目 path 以 / 结尾)+ Unix 权限位(可缺失)。 */
|
|
10
10
|
export interface ZipEntry {
|
|
11
11
|
path: string
|
|
12
12
|
data: Buffer
|
|
13
13
|
isDirectory: boolean
|
|
14
|
+
/** Unix 权限位(中央目录 external attributes 高 16 位;0 或非 Unix 归档时缺失)。 */
|
|
15
|
+
mode?: number
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
const EOCD_SIG = 0x06054b50
|
|
@@ -19,16 +21,16 @@ const LHDR_SIG = 0x04034b50
|
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
23
|
* 解析 ZIP 缓冲区 → 条目列表(不解压内容,仅元数据)。
|
|
22
|
-
* @author ddj 2026年08月27号
|
|
24
|
+
* @author ddj 2026年08月27号 / 2026年09月18号
|
|
23
25
|
* @param buf zip 字节
|
|
24
|
-
* @returns
|
|
26
|
+
* @returns 条目元数据(含 Unix mode,见 modeOf)
|
|
25
27
|
*/
|
|
26
|
-
export function zipEntries(buf: Buffer): { name: string; method: number; compressed: number; uncompressed: number; localOffset: number }[] {
|
|
28
|
+
export function zipEntries(buf: Buffer): { name: string; method: number; compressed: number; uncompressed: number; localOffset: number; mode?: number }[] {
|
|
27
29
|
const eocd = findEocd(buf)
|
|
28
30
|
if (eocd < 0) throw new Error('不是有效的 ZIP 文件(找不到 EOCD)')
|
|
29
31
|
const total = buf.readUInt16LE(eocd + 10)
|
|
30
32
|
let offset = buf.readUInt32LE(eocd + 16)
|
|
31
|
-
const out: { name: string; method: number; compressed: number; uncompressed: number; localOffset: number }[] = []
|
|
33
|
+
const out: { name: string; method: number; compressed: number; uncompressed: number; localOffset: number; mode?: number }[] = []
|
|
32
34
|
for (let i = 0; i < total; i++) {
|
|
33
35
|
if (buf.readUInt32LE(offset) !== CDIR_SIG) throw new Error('ZIP 中央目录损坏')
|
|
34
36
|
const method = buf.readUInt16LE(offset + 10)
|
|
@@ -39,17 +41,31 @@ export function zipEntries(buf: Buffer): { name: string; method: number; compres
|
|
|
39
41
|
const commentLen = buf.readUInt16LE(offset + 32)
|
|
40
42
|
const localOffset = buf.readUInt32LE(offset + 42)
|
|
41
43
|
const name = buf.subarray(offset + 46, offset + 46 + nameLen).toString('utf8')
|
|
42
|
-
|
|
44
|
+
const mode = modeOf(buf.readUInt32LE(offset + 38))
|
|
45
|
+
out.push({ name, method, compressed, uncompressed, localOffset, ...(mode === undefined ? {} : { mode }) })
|
|
43
46
|
offset += 46 + nameLen + extraLen + commentLen
|
|
44
47
|
}
|
|
45
48
|
return out
|
|
46
49
|
}
|
|
47
50
|
|
|
51
|
+
/**
|
|
52
|
+
* 中央目录 external attributes → Unix 权限位。
|
|
53
|
+
* ZIP 把 Unix mode 放在 `st_mode << 16`(含 S_IFREG 等类型位),低 16 位仅 DOS 属性;
|
|
54
|
+
* 故必须右移 16 再取低 9 位权限。非 Unix 归档(Windows 打包)该字段为 0 或纯 DOS 位。
|
|
55
|
+
* @author ddj 2026年09月18号
|
|
56
|
+
* @param attributes 中央目录 offset+38 的 4 字节
|
|
57
|
+
* @returns 0o000~0o777 权限位;无有效 mode 返回 undefined
|
|
58
|
+
*/
|
|
59
|
+
export function modeOf(attributes: number): number | undefined {
|
|
60
|
+
const mode = (attributes >>> 16) & 0o777
|
|
61
|
+
return mode === 0 ? undefined : mode
|
|
62
|
+
}
|
|
63
|
+
|
|
48
64
|
/**
|
|
49
65
|
* 解压 ZIP → 条目列表(目录条目含尾部 /)。
|
|
50
|
-
* @author ddj 2026年08月27号
|
|
66
|
+
* @author ddj 2026年08月27号 / 2026年09月18号
|
|
51
67
|
* @param buf zip 字节
|
|
52
|
-
* @returns
|
|
68
|
+
* @returns 条目(含内容与可选 mode)
|
|
53
69
|
*/
|
|
54
70
|
export function unzip(buf: Buffer): ZipEntry[] {
|
|
55
71
|
const metas = zipEntries(buf)
|
|
@@ -66,7 +82,7 @@ export function unzip(buf: Buffer): ZipEntry[] {
|
|
|
66
82
|
const raw = buf.subarray(dataStart, dataStart + meta.compressed)
|
|
67
83
|
data = meta.method === 8 ? Buffer.from(inflateRawSync(raw)) : meta.method === 0 ? Buffer.from(raw) : Buffer.alloc(0)
|
|
68
84
|
}
|
|
69
|
-
out.push({ path: meta.name, data, isDirectory: isDir })
|
|
85
|
+
out.push({ path: meta.name, data, isDirectory: isDir, ...(meta.mode === undefined ? {} : { mode: meta.mode }) })
|
|
70
86
|
}
|
|
71
87
|
return out
|
|
72
88
|
}
|
package/src/revert.ts
CHANGED
|
@@ -11,9 +11,26 @@ import { applyLocations, locateHunks, preciseHunk } from './shared/diff.js'
|
|
|
11
11
|
/** 回滚结果;stale=true 表示该 hunk 的新文本已不在文件中(无可回滚内容,不算失败)。 */
|
|
12
12
|
export type Result = { ok: true } | { ok: false; error: string; stale?: boolean }
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* 删除单文件的 argv(纯函数,platform 可注入便于单测)。
|
|
16
|
+
* Windows 走 PowerShell Remove-Item;其余平台走 /bin/rm。
|
|
17
|
+
* 原先无条件先发 powershell:macOS/Linux 上必然 spawn 失败后才回落,
|
|
18
|
+
* 每删一个文件多一次无谓 spawn 与失败噪声。
|
|
19
|
+
* @author ddj 2026年09月18号
|
|
20
|
+
* @param absPath 绝对路径
|
|
21
|
+
* @param platform 目标平台(缺省当前进程平台)
|
|
22
|
+
* @returns 删除命令 argv
|
|
23
|
+
*/
|
|
24
|
+
export function removeFileArgv(absPath: string, platform: NodeJS.Platform = process.platform): string[] {
|
|
25
|
+
if (platform === 'win32') {
|
|
26
|
+
return ['powershell', '-NoProfile', '-NonInteractive', '-Command', 'Remove-Item -LiteralPath "' + absPath + '" -Force']
|
|
27
|
+
}
|
|
28
|
+
return ['/bin/rm', '-f', '--', absPath]
|
|
29
|
+
}
|
|
30
|
+
|
|
14
31
|
/**
|
|
15
32
|
* 删除新建文件(拒绝创建时):subprocess 删除,路径先经 fs.contains 校验工作区边界。
|
|
16
|
-
* @author ddj 2026年08月20号
|
|
33
|
+
* @author ddj 2026年08月20号 / 2026年09月18号
|
|
17
34
|
* @returns 成功或失败原因
|
|
18
35
|
*/
|
|
19
36
|
export async function deleteCreated(ctx: Ctx, session: Session, record: DiffRecord): Promise<Result> {
|
|
@@ -40,15 +57,10 @@ export async function deleteCreated(ctx: Ctx, session: Session, record: DiffReco
|
|
|
40
57
|
}
|
|
41
58
|
}
|
|
42
59
|
try {
|
|
43
|
-
await attempt(
|
|
60
|
+
await attempt(removeFileArgv(p))
|
|
44
61
|
return { ok: true }
|
|
45
62
|
} catch (error) {
|
|
46
|
-
|
|
47
|
-
await attempt(['/bin/rm', '-f', '--', p])
|
|
48
|
-
return { ok: true }
|
|
49
|
-
} catch (error2) {
|
|
50
|
-
return { ok: false, error: '删除失败(文件仍存在):' + String(error) + ' / ' + String(error2) }
|
|
51
|
-
}
|
|
63
|
+
return { ok: false, error: '删除失败(文件仍存在):' + String(error) }
|
|
52
64
|
}
|
|
53
65
|
}
|
|
54
66
|
|