dsh-plugin 1.3.8 → 1.3.9
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 +12 -12
- package/README.md +12 -12
- package/client/client.js +379 -322
- package/client/client.js.map +1 -1
- package/lib/http/routes.js +10 -0
- package/lib/services/install/task-queue.js +55 -9
- package/lib/types/services/install/install-types.d.ts +4 -0
- package/lib/types/services/install/task-queue.d.ts +5 -0
- package/package.json +1 -1
- package/src/client/components/PluginHubSection.tsx +16 -3
- package/src/client/components/modals/NotificationsModal.tsx +10 -0
- package/src/client/components/modals/modals.tsx +9 -1
- package/src/client/components/views/CustomInstallView.tsx +9 -7
- package/src/client/hooks/useTaskQueue.ts +47 -11
- package/src/client/locales.ts +16 -0
- package/src/client/logic/failures.ts +23 -3
- package/src/client/logic/urls.ts +2 -0
- package/src/client/types.ts +4 -0
- package/src/server/http/routes.ts +10 -0
- package/src/server/services/install/install-types.ts +4 -1
- package/src/server/services/install/task-queue.ts +58 -9
|
@@ -151,15 +151,22 @@ export function removeNotification(id: number): NotificationRecord[] {
|
|
|
151
151
|
return next
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
export type FailureKind = 'npmTooOld' | 'pnpmIgnoredBuild' | 'pluginPrepare' | 'network' | 'repo'
|
|
154
|
+
export type FailureKind = 'npmTooOld' | 'dshMissing' | 'pnpmMissing' | 'pnpmIgnoredBuild' | 'pluginPrepare' | 'network' | 'repo'
|
|
155
155
|
|
|
156
156
|
/**
|
|
157
|
-
*
|
|
157
|
+
* 失败归类,七态。无论底层机制如何(pnpm 白名单拦截 / 构建脚本被忽略 / prepare 失败),
|
|
158
158
|
* 对用户而言结果都一样 —— 当前安装通道(npm 或 git)装不上,就是插件分发/依赖的问题,
|
|
159
|
-
* 一律引导提 Issue
|
|
159
|
+
* 一律引导提 Issue;唯一的例外是本机环境问题(npm 版本过低 / 找不到 dsh / 找不到 pnpm / 网络不通):
|
|
160
160
|
* - npmTooOld:失败输出含 npm arborist 的 `edgesOut` 崩溃特征(build-ideal-tree.js 解 peer 依赖时
|
|
161
161
|
* 内部抛错,npm 11.6.0 前必现的已知缺陷,npm/cli#8261、#9787),或服务端已核实本机版本低于
|
|
162
162
|
* 阈值并打了 `[npm-too-low]` 标记 —— 是本机 npm 版本过低/自身缺陷,不是插件问题 → 引导升级 npm
|
|
163
|
+
* - dshMissing:安装器 spawn 的 `dsh` 命令找不到(Windows cmd「不是内部或外部命令」/ POSIX
|
|
164
|
+
* 「command not found」/ spawn ENOENT)—— 是本机 DSH 未正确安装或不在 PATH,不是插件问题
|
|
165
|
+
* → 提示检查 PATH/重装 DSH,不引导提 Issue
|
|
166
|
+
* - pnpmMissing:dsh 存在但调用的 `pnpm` 找不到(dsh 报 `pnpm not found on PATH`/POSIX
|
|
167
|
+
* 「pnpm: command not found」/ spawn ENOENT)—— 本机缺 pnpm(dsh 用 pnpm 管理 profile 插件),
|
|
168
|
+
* 不是插件问题(dsh-plugin-hub#13:Linux 下 `dsh: pnpm not found on PATH` 被误归插件侧失败)
|
|
169
|
+
* → 提示安装/开启 pnpm,不引导提 Issue
|
|
163
170
|
* - network:安装前连通性预检拦截(服务端 `[network]` 标记)或底层连接失败
|
|
164
171
|
* (ERR_PNPM_GIT_FETCH_FAILED / ETIMEDOUT / DNS 解析 / TLS 握手 / 代理拒绝)——
|
|
165
172
|
* 是本机网络不通/被墙/代理有问题,不是插件问题 → 提示检查网络,不引导提 Issue
|
|
@@ -176,6 +183,19 @@ export function classifyFailure(message: string): FailureKind {
|
|
|
176
183
|
// 不是插件问题 —— 必须最先判,否则该报错会被外层 ERR_PNPM_PREPARE_PACKAGE 吞成
|
|
177
184
|
// 「插件打包分发问题」,误导用户去提 Issue
|
|
178
185
|
if (/\[npm-too-low\]|edgesOut/i.test(message)) return 'npmTooOld'
|
|
186
|
+
// 找不到 pnpm 命令(dsh 报 `pnpm not found on PATH` —— dsh 用它管理 profile 插件 /
|
|
187
|
+
// POSIX「pnpm: command not found」/ spawn pnpm ENOENT / Windows cmd 中英文报错):
|
|
188
|
+
// 本机缺 pnpm,不是插件问题。必须在 dshMissing 之前 —— dshMissing 正则含裸「command not
|
|
189
|
+
// found」,会把 `pnpm: command not found` 吞成「dsh 缺失」,误引导用户去装 DSH
|
|
190
|
+
// (dsh-plugin-hub#13:Linux 下 `dsh: pnpm not found on PATH` 被误归插件侧失败)
|
|
191
|
+
if (/\[pnpm-missing\]|pnpm not found|pnpm: command not found|spawn pnpm ENOENT|'pnpm' 不是内部或外部命令|"pnpm" 不是内部或外部命令|pnpm['"]?\s*is not recognized/i.test(message)) return 'pnpmMissing'
|
|
192
|
+
// 找不到 dsh 命令(服务端 [dsh-missing] 标记 —— 乱码免疫:Windows cmd 中文版输出 GBK,
|
|
193
|
+
// 经 UTF-8 解码成乱码无法匹配原文,故服务端在 spawn 前用 which/where 探测并打 ASCII 标记;
|
|
194
|
+
// 其余形态:Windows cmd「不是内部或外部命令」/ POSIX「command not found」/
|
|
195
|
+
// node spawn ENOENT):是本机 DSH 未正确安装或不在 PATH,不是插件问题 —— 必须先判,
|
|
196
|
+
// 否则会被外层 "Command failed" 吞成「插件打包问题」,误导用户去提 Issue
|
|
197
|
+
// (dsh-plugin-hub#12:Win 下 'dsh' 不在 PATH,cmd 报「不是内部或外部命令」被误归插件侧失败)
|
|
198
|
+
if (/\[dsh-missing\]|不是内部或外部命令|is not recognized as an internal or external command|command not found|spawn dsh ENOENT/i.test(message)) return 'dshMissing'
|
|
179
199
|
// 构建脚本被 pnpm 白名单(allowBuilds)拦截:插件的 prepare 脚本或依赖里的原生模块构建
|
|
180
200
|
// 被默认拒绝(ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED / ERR_PNPM_IGNORED_BUILDS)。
|
|
181
201
|
// 这类错误出现即说明 pnpm 已成功 fetch 到 tarball(网络是通的),主因是插件构建脚本
|
package/src/client/logic/urls.ts
CHANGED
|
@@ -40,6 +40,8 @@ export function pluginSiteUrl(repo: string): string {
|
|
|
40
40
|
function reasonTitleOf(kind: FailureKind): string {
|
|
41
41
|
switch (kind) {
|
|
42
42
|
case 'npmTooOld': return 'npm too old to install'
|
|
43
|
+
case 'dshMissing': return 'dsh command not found on the user machine'
|
|
44
|
+
case 'pnpmMissing': return 'pnpm command not found on the user machine'
|
|
43
45
|
case 'pnpmIgnoredBuild': return 'build scripts blocked by pnpm allowlist'
|
|
44
46
|
case 'pluginPrepare': return 'plugin distribution incomplete'
|
|
45
47
|
case 'network': return 'network failure on the user side'
|
package/src/client/types.ts
CHANGED
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
|
|
10
10
|
export type LocaleId = 'zh' | 'en'
|
|
11
11
|
|
|
12
|
+
/** 自定义安装入口渠道:三张卡片(NPM 包 / GitHub 源码 / DSH 命令)。
|
|
13
|
+
* 客户端提交时显式上报,服务端落日志/报错提示据此溯源「从哪个入口发起」。 */
|
|
14
|
+
export type InstallChannel = 'npm' | 'git' | 'dsh'
|
|
15
|
+
|
|
12
16
|
/** Translation function signature shared by the host binder and the in-component dictionary. */
|
|
13
17
|
export type Translate = (key: string, params?: Record<string, string | number>) => string
|
|
14
18
|
|
|
@@ -646,6 +646,12 @@ export function mountPluginHubRoutes(webServer: WebServerService, profile: strin
|
|
|
646
646
|
const source = typeof body === 'object' && body !== null && typeof (body as { source?: unknown }).source === 'string'
|
|
647
647
|
? (body as { source: string }).source
|
|
648
648
|
: ''
|
|
649
|
+
// 自定义安装入口渠道(客户端三张卡片:NPM 包 / GitHub 源码 / DSH 命令):
|
|
650
|
+
// 客户端显式上报,服务端日志/报错据此精准溯源「从哪个入口发起」;目录插件安装不传。
|
|
651
|
+
const installChannel = (() => {
|
|
652
|
+
const raw = body !== null && typeof body === 'object' ? (body as { installChannel?: unknown }).installChannel : undefined
|
|
653
|
+
return raw === 'git' || raw === 'dsh' || raw === 'npm' ? raw : undefined
|
|
654
|
+
})()
|
|
649
655
|
const settings = loadSettings(profile)
|
|
650
656
|
// 界面语言(客户端随请求携带):错误消息按用户当前语言提示(中文界面给中文,英文界面给英文)
|
|
651
657
|
const lang = typeof body === 'object' && body !== null && (body as { lang?: unknown }).lang === 'en'
|
|
@@ -684,6 +690,8 @@ export function mountPluginHubRoutes(webServer: WebServerService, profile: strin
|
|
|
684
690
|
globalNpm,
|
|
685
691
|
timeoutMs: COMMAND_TIMEOUT_MS,
|
|
686
692
|
env: mutationEnv(settings),
|
|
693
|
+
// 全局 npm 安装由 NPM 包卡片/官方命令发起:日志体现入口渠道
|
|
694
|
+
installChannel: installChannel ?? 'npm',
|
|
687
695
|
})
|
|
688
696
|
sendJson(response, 200, { ok: true, task: task.id })
|
|
689
697
|
return
|
|
@@ -816,6 +824,8 @@ export function mountPluginHubRoutes(webServer: WebServerService, profile: strin
|
|
|
816
824
|
env: mutationEnv(settings),
|
|
817
825
|
// 覆盖更新:npm 通道命令显式 @latest(pnpm add 无版本对已存在依赖是幂等的,不加版本不会真更新)
|
|
818
826
|
updateNpm: mode === 'update',
|
|
827
|
+
// 自定义安装入口渠道(NPM 包 / GitHub 源码 / DSH 命令卡片):日志/报错据此溯源
|
|
828
|
+
installChannel,
|
|
819
829
|
})
|
|
820
830
|
sendJson(response, 200, { ok: true, task: task.id })
|
|
821
831
|
} catch (error) {
|
|
@@ -28,6 +28,9 @@ export interface InstallTask {
|
|
|
28
28
|
/** 全局 npm 安装(官方 `npm install -g <pkgs>`):非空时任务执行全局 npm 安装,不进任何 profile */
|
|
29
29
|
globalNpm?: string[]
|
|
30
30
|
action: 'add' | 'remove' | 'update'
|
|
31
|
+
/** 自定义安装入口渠道(客户端三张卡片:NPM 包 / GitHub 源码 / DSH 命令):
|
|
32
|
+
* 任务输出行与系统日志据此溯源「从哪个入口发起」;目录插件安装无此字段。 */
|
|
33
|
+
installChannel?: 'npm' | 'git' | 'dsh'
|
|
31
34
|
/** 队列语义:pending 排队中 / running 执行中 / done / failed / cancelled(用户取消) */
|
|
32
35
|
status: 'pending' | 'running' | 'done' | 'failed' | 'cancelled'
|
|
33
36
|
timedOut: boolean
|
|
@@ -51,7 +54,7 @@ export interface Invocation {
|
|
|
51
54
|
/** 排队中的变更任务及其启动参数。 */
|
|
52
55
|
export interface QueueItem {
|
|
53
56
|
task: InstallTask
|
|
54
|
-
options: { action: 'add' | 'remove' | 'update'; profile: string; target: string; timeoutMs?: number; env?: NodeJS.ProcessEnv; globalNpm?: string[] }
|
|
57
|
+
options: { action: 'add' | 'remove' | 'update'; profile: string; target: string; timeoutMs?: number; env?: NodeJS.ProcessEnv; globalNpm?: string[]; installChannel?: 'npm' | 'git' | 'dsh' }
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
/**
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* 每个任务一次 `dsh plugin --profile <profile> <action> <target>` 子进程,
|
|
8
8
|
* 输出流式收集到任务上;卸载成功时尝试从运行中 loader 即时停用(见 loader.ts)。
|
|
9
9
|
*/
|
|
10
|
-
import { spawn } from 'node:child_process'
|
|
10
|
+
import { spawn, spawnSync } from 'node:child_process'
|
|
11
11
|
import { readFileSync, statSync } from 'node:fs'
|
|
12
12
|
import { dirname, join, resolve } from 'node:path'
|
|
13
13
|
import type { InstallResult, InstallTask, Invocation, QueueItem } from './install-types.ts'
|
|
@@ -44,6 +44,8 @@ export interface ActiveTaskInfo {
|
|
|
44
44
|
lines: string[]
|
|
45
45
|
/** 尝试过的安装方式(npm 反查 + 实际执行命令):失败时前端 issue 预填需要 */
|
|
46
46
|
attempts: string[]
|
|
47
|
+
/** 自定义安装入口渠道(NPM 包 / GitHub 源码 / DSH 命令卡片):前端刷新恢复后仍能精准溯源 */
|
|
48
|
+
installChannel?: 'npm' | 'git' | 'dsh'
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
/** All non-terminal tasks in queue order (running first, then pending). Lets the client resume a queue after a page reload. */
|
|
@@ -55,12 +57,12 @@ export function activeTask(): ActiveTaskInfo[] {
|
|
|
55
57
|
for (const item of queue) {
|
|
56
58
|
const task = item.task
|
|
57
59
|
if (task.status === 'pending') {
|
|
58
|
-
found.push({ id: task.id, target: show(task), action: task.action, status: 'pending', progress: task.progress, lines: task.lines.slice(0, MAX_TASK_LINES), attempts: task.attempts })
|
|
60
|
+
found.push({ id: task.id, target: show(task), action: task.action, status: 'pending', progress: task.progress, lines: task.lines.slice(0, MAX_TASK_LINES), attempts: task.attempts, installChannel: task.installChannel })
|
|
59
61
|
}
|
|
60
62
|
}
|
|
61
63
|
tasks.forEach((task) => {
|
|
62
64
|
if (task.status === 'running') {
|
|
63
|
-
found.unshift({ id: task.id, target: show(task), action: task.action, status: 'running', progress: task.progress, lines: task.lines.slice(0, MAX_TASK_LINES), attempts: task.attempts })
|
|
65
|
+
found.unshift({ id: task.id, target: show(task), action: task.action, status: 'running', progress: task.progress, lines: task.lines.slice(0, MAX_TASK_LINES), attempts: task.attempts, installChannel: task.installChannel })
|
|
64
66
|
}
|
|
65
67
|
})
|
|
66
68
|
return found
|
|
@@ -140,6 +142,23 @@ function cliInvocation(): Invocation {
|
|
|
140
142
|
return { file: 'dsh', prefixArgs: [], cwd: process.cwd(), useShell: process.platform === 'win32' }
|
|
141
143
|
}
|
|
142
144
|
|
|
145
|
+
/**
|
|
146
|
+
* dsh 命令存在性探测:which/where 找 PATH 里的 dsh。
|
|
147
|
+
* Windows shell 模式命令缺失时 cmd 只输出 GBK 乱码「不是内部或外部命令」(UTF-8 解码后
|
|
148
|
+
* 无法匹配),POSIX 非 shell 模式才会抛 spawn dsh ENOENT —— 两种形态都不可靠,统一在
|
|
149
|
+
* spawn 前用 which/where 探测,命令不存在则直接失败并打 ASCII 标记 [dsh-missing](乱码免疫)。
|
|
150
|
+
* 探测自身异常(which/where 不可用等)不拦截安装,交给装后校验兜底。
|
|
151
|
+
*/
|
|
152
|
+
function dshOnPath(env?: NodeJS.ProcessEnv): boolean {
|
|
153
|
+
try {
|
|
154
|
+
const probe = process.platform === 'win32' ? 'where' : 'which'
|
|
155
|
+
const res = spawnSync(probe, ['dsh'], { env, encoding: 'utf8', timeout: 3000, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
156
|
+
return res.status === 0 && (res.stdout ?? '').trim().length > 0
|
|
157
|
+
} catch {
|
|
158
|
+
return true
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
143
162
|
function stopChild(child: ReturnType<typeof spawn>): void {
|
|
144
163
|
if (process.platform === 'win32' && child.pid !== undefined) {
|
|
145
164
|
const killer = spawn('taskkill', ['/pid', String(child.pid), '/t', '/f'], { stdio: 'ignore' })
|
|
@@ -168,6 +187,15 @@ function mutationVerb(action: 'add' | 'remove' | 'update'): string {
|
|
|
168
187
|
return action === 'remove' ? '开始卸载' : action === 'update' ? '开始更新' : '开始安装'
|
|
169
188
|
}
|
|
170
189
|
|
|
190
|
+
/** 系统日志入口渠道后缀:自定义安装(带渠道)在日志里体现「从哪张卡片发起」,
|
|
191
|
+
* 目录插件安装(无渠道)返回空串。日志中文语境,渠道名用中文口语。 */
|
|
192
|
+
function channelLabel(channel: 'npm' | 'git' | 'dsh' | undefined): string {
|
|
193
|
+
if (channel === 'git') return '(GitHub 源码入口)'
|
|
194
|
+
if (channel === 'dsh') return '(DSH 命令入口)'
|
|
195
|
+
if (channel === 'npm') return '(NPM 包入口)'
|
|
196
|
+
return ''
|
|
197
|
+
}
|
|
198
|
+
|
|
171
199
|
/**
|
|
172
200
|
* Enqueue a plugin mutation and return its task. Tasks run strictly serially:
|
|
173
201
|
* the queue worker starts the next one only after the previous finishes.
|
|
@@ -189,8 +217,11 @@ export function startPluginMutation(options: {
|
|
|
189
217
|
attempts?: string[]
|
|
190
218
|
/** 更新已安装的 npm 包(mode=update):命令显式带 @latest,让 pnpm 真正解析最新版本 */
|
|
191
219
|
updateNpm?: boolean
|
|
220
|
+
/** 自定义安装入口渠道(客户端三张卡片:NPM 包 / GitHub 源码 / DSH 命令):
|
|
221
|
+
* 任务输出行与系统日志据此溯源「从哪个入口发起」;目录插件安装不传 */
|
|
222
|
+
installChannel?: 'npm' | 'git' | 'dsh'
|
|
192
223
|
}): InstallTask {
|
|
193
|
-
const task: InstallTask = { id: nextTaskId, target: options.target, displayTarget: options.displayTarget, globalNpm: options.globalNpm, action: options.action, status: 'pending', timedOut: false, exitCode: null, progress: 0, lines: [], attempts: options.attempts ?? [], needsRestart: false }
|
|
224
|
+
const task: InstallTask = { id: nextTaskId, target: options.target, displayTarget: options.displayTarget, globalNpm: options.globalNpm, action: options.action, status: 'pending', timedOut: false, exitCode: null, progress: 0, lines: [], attempts: options.attempts ?? [], needsRestart: false, installChannel: options.installChannel }
|
|
194
225
|
nextTaskId = nextTaskId >= Number.MAX_SAFE_INTEGER ? 1 : nextTaskId + 1
|
|
195
226
|
tasks.set(task.id, task)
|
|
196
227
|
if (tasks.size > MAX_TASKS) {
|
|
@@ -203,14 +234,18 @@ export function startPluginMutation(options: {
|
|
|
203
234
|
})
|
|
204
235
|
}
|
|
205
236
|
queue.push({ task, options: { ...options } })
|
|
206
|
-
//
|
|
237
|
+
// 任务输出首行标记入口渠道(ASCII 标记,乱码免疫):进度面板/失败明细里能溯源「从哪张卡片发起」
|
|
238
|
+
if (options.installChannel !== undefined) {
|
|
239
|
+
pushLine(task, `[install-channel] ${options.installChannel}`)
|
|
240
|
+
}
|
|
241
|
+
// 系统日志:记录任务入队(安装/卸载/更新开始),自定义安装带入口渠道后缀
|
|
207
242
|
const category = mutationCategory(options.action)
|
|
208
243
|
appendLog(options.profile, {
|
|
209
244
|
at: Date.now(),
|
|
210
245
|
level: 'info',
|
|
211
246
|
category,
|
|
212
247
|
event: `${category}.start`,
|
|
213
|
-
message: `${mutationVerb(options.action)} ${options.displayTarget ?? options.target}`,
|
|
248
|
+
message: `${mutationVerb(options.action)} ${options.displayTarget ?? options.target}${channelLabel(options.installChannel)}`,
|
|
214
249
|
})
|
|
215
250
|
pumpQueue()
|
|
216
251
|
return task
|
|
@@ -235,7 +270,7 @@ function pumpQueue(): void {
|
|
|
235
270
|
level: 'success',
|
|
236
271
|
category,
|
|
237
272
|
event: `${category}.done`,
|
|
238
|
-
message: `${verb('成功')} ${show}${task.needsRestart ? '(待重启生效)' : ''}`,
|
|
273
|
+
message: `${verb('成功')} ${show}${channelLabel(options.installChannel)}${task.needsRestart ? '(待重启生效)' : ''}`,
|
|
239
274
|
})
|
|
240
275
|
} else if (task.status === 'failed') {
|
|
241
276
|
appendLog(options.profile, {
|
|
@@ -243,7 +278,7 @@ function pumpQueue(): void {
|
|
|
243
278
|
level: 'error',
|
|
244
279
|
category,
|
|
245
280
|
event: `${category}.fail`,
|
|
246
|
-
message: `${verb('失败')} ${show}`,
|
|
281
|
+
message: `${verb('失败')} ${show}${channelLabel(options.installChannel)}`,
|
|
247
282
|
})
|
|
248
283
|
} else if (task.status === 'cancelled') {
|
|
249
284
|
appendLog(options.profile, {
|
|
@@ -251,7 +286,7 @@ function pumpQueue(): void {
|
|
|
251
286
|
level: 'warn',
|
|
252
287
|
category,
|
|
253
288
|
event: 'task.cancel',
|
|
254
|
-
message: `已取消 ${show}`,
|
|
289
|
+
message: `已取消 ${show}${channelLabel(options.installChannel)}`,
|
|
255
290
|
})
|
|
256
291
|
}
|
|
257
292
|
// 已终态的任务移出队列:既防 queue 数组无限增长(内存泄漏),也让下一轮 pump 干净
|
|
@@ -319,6 +354,20 @@ function spawnMutation(options: {
|
|
|
319
354
|
let settled = false
|
|
320
355
|
let child: ReturnType<typeof spawn>
|
|
321
356
|
try {
|
|
357
|
+
// 独立 CLI 场景(file: 'dsh',宿主加载器走 node bin 不经过这里):先探测 dsh 是否在 PATH。
|
|
358
|
+
// 命令不存在时 Windows shell 模式只回传 GBK 乱码、POSIX 才抛 spawn dsh ENOENT —— 统一前置
|
|
359
|
+
// 拦截并打 ASCII 标记 [dsh-missing],前端据此精准提示「dsh 未加入 PATH」而不是笼统的插件失败。
|
|
360
|
+
if (invocation.file === 'dsh' && !dshOnPath(env)) {
|
|
361
|
+
const detail = '[dsh-missing] the dsh CLI was not found on this machine (DeepSeek Harness not installed, or dsh not on the PATH)'
|
|
362
|
+
if (task) {
|
|
363
|
+
task.status = 'failed'
|
|
364
|
+
task.exitCode = null
|
|
365
|
+
task.progress = 0
|
|
366
|
+
pushLine(task, detail)
|
|
367
|
+
}
|
|
368
|
+
resolveOnce({ exitCode: 1, timedOut: false, error: detail, stdout: '', stderr: detail })
|
|
369
|
+
return
|
|
370
|
+
}
|
|
322
371
|
child = spawn(invocation.file, attemptArgs, {
|
|
323
372
|
cwd: invocation.cwd,
|
|
324
373
|
env,
|