@dsh-workbench/shared 0.1.13 → 0.1.15
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/dist/components/CardHead.js +3 -2
- package/dist/components/Field.css +16 -0
- package/dist/components/Field.d.ts +8 -0
- package/dist/components/Field.js +8 -0
- package/dist/components/Link.css +22 -0
- package/dist/components/Link.d.ts +10 -0
- package/dist/components/Link.js +8 -0
- package/dist/components/LongTaskRow.css +0 -15
- package/dist/components/LongTaskRow.js +3 -4
- package/dist/components/TextField.css +5 -0
- package/dist/host/browser-find.d.ts +38 -0
- package/dist/host/browser-find.js +242 -0
- package/dist/host/browser-read.d.ts +26 -0
- package/dist/host/browser-read.js +240 -0
- package/dist/host/http.d.ts +6 -1
- package/dist/host/http.js +2 -1
- package/dist/host/opencli-runtime.d.ts +3 -0
- package/dist/host/opencli-runtime.js +10 -0
- package/dist/host/opencli-window.d.ts +2 -0
- package/dist/host/opencli-window.js +8 -0
- package/dist/host/opencli.js +2 -0
- package/dist/host.d.ts +2 -0
- package/dist/host.js +2 -0
- package/dist/i18n/en.js +3 -0
- package/dist/i18n/index.d.ts +6 -3
- package/dist/i18n/zh.d.ts +5 -2
- package/dist/i18n/zh.js +5 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/package.json +1 -1
- package/src/components/CardHead.test.tsx +81 -0
- package/src/components/CardHead.tsx +4 -3
- package/src/components/Field.css +16 -0
- package/src/components/Field.test.tsx +27 -0
- package/src/components/Field.tsx +19 -0
- package/src/components/Link.css +22 -0
- package/src/components/Link.test.tsx +37 -0
- package/src/components/Link.tsx +26 -0
- package/src/components/LongTaskRow.css +0 -15
- package/src/components/LongTaskRow.tsx +3 -4
- package/src/components/TextField.css +5 -0
- package/src/host/__tests__/browser-find.test.ts +319 -0
- package/src/host/__tests__/browser-read.test.ts +197 -0
- package/src/host/browser-find.ts +311 -0
- package/src/host/browser-read.ts +308 -0
- package/src/host/http.ts +8 -1
- package/src/host/opencli-runtime.ts +13 -0
- package/src/host/opencli-window.ts +11 -0
- package/src/host/opencli.ts +2 -0
- package/src/host.ts +2 -0
- package/src/i18n/en.ts +3 -0
- package/src/i18n/zh.ts +5 -2
- package/src/index.ts +2 -0
- package/dist/components/StateDot.d.ts +0 -5
- package/dist/components/StateDot.js +0 -10
- package/dist/host/__tests__/fakes.d.ts +0 -41
- package/dist/host/__tests__/fakes.js +0 -76
- package/dist/host/__tests__/preflight-fixtures.d.ts +0 -19
- package/dist/host/__tests__/preflight-fixtures.js +0 -47
- package/dist/host/context-inject.d.ts +0 -24
- package/dist/host/context-inject.js +0 -74
- package/dist/host/events.d.ts +0 -9
- package/dist/host/events.js +0 -70
- package/dist/host/mount-units.d.ts +0 -52
- package/dist/host/mount-units.js +0 -186
- package/dist/host/preflight.d.ts +0 -58
- package/dist/host/preflight.js +0 -233
- package/dist/host/probe-cache.d.ts +0 -6
- package/dist/host/probe-cache.js +0 -19
- package/dist/host/probe-libs.d.ts +0 -45
- package/dist/host/probe-libs.js +0 -61
- package/dist/host/probe-units.d.ts +0 -17
- package/dist/host/probe-units.js +0 -102
- package/dist/host/probe-verdict.d.ts +0 -7
- package/dist/host/probe-verdict.js +0 -143
- package/dist/host/tool-scope.d.ts +0 -32
- package/dist/host/tool-scope.js +0 -84
- package/dist/host/unit-readiness.d.ts +0 -7
- package/dist/host/unit-readiness.js +0 -5
- package/dist/host/unit-registry.d.ts +0 -9
- package/dist/host/unit-registry.js +0 -33
- package/dist/host/version-range.d.ts +0 -2
- package/dist/host/version-range.js +0 -73
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import type { DshShell } from '../api/dsh.js';
|
|
2
|
+
import { parseJsonLoose, tailLines } from '../lib/text.js';
|
|
3
|
+
import { backgroundLine } from './opencli-window.js';
|
|
4
|
+
import { runShell } from './shell.js';
|
|
5
|
+
|
|
6
|
+
export const BROWSER_BRIDGE_SESSION = 'workbench';
|
|
7
|
+
|
|
8
|
+
const DEFAULT_TIMEOUT_MS = 25000;
|
|
9
|
+
const DEFAULT_RETRY_MS = 1200;
|
|
10
|
+
const DEFAULT_SETTLE_MS = 900;
|
|
11
|
+
|
|
12
|
+
const AUTH_CODES = new Set([
|
|
13
|
+
'NOT_LOGGED_IN',
|
|
14
|
+
'NOT_LOGIN',
|
|
15
|
+
'AUTH_FAILED',
|
|
16
|
+
'AUTH_REQUIRED',
|
|
17
|
+
'UNAUTHENTICATED',
|
|
18
|
+
'UNAUTHORIZED',
|
|
19
|
+
'TOKEN_EXPIRED',
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
const PERMISSION_CODES = new Set(['FORBIDDEN', 'PERMISSION_DENIED', 'NO_PERMISSION', 'EACCES']);
|
|
23
|
+
|
|
24
|
+
export interface BrowserReadResult {
|
|
25
|
+
readonly ok: boolean;
|
|
26
|
+
readonly code: string;
|
|
27
|
+
readonly at: number;
|
|
28
|
+
readonly value?: unknown;
|
|
29
|
+
readonly pageCode?: string;
|
|
30
|
+
readonly message: string;
|
|
31
|
+
readonly hint?: string;
|
|
32
|
+
readonly detail?: string;
|
|
33
|
+
readonly helpUrl?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface BrowserReadOptions {
|
|
37
|
+
readonly shell: DshShell;
|
|
38
|
+
readonly script: string;
|
|
39
|
+
readonly url?: string;
|
|
40
|
+
readonly loginUrl?: string;
|
|
41
|
+
readonly command?: string;
|
|
42
|
+
readonly session?: string;
|
|
43
|
+
readonly open?: boolean;
|
|
44
|
+
readonly timeoutMs?: number;
|
|
45
|
+
readonly retryMs?: number;
|
|
46
|
+
readonly settleMs?: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
50
|
+
return typeof value === 'object' && value !== null ? (value as Record<string, unknown>) : undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function textOf(value: unknown): string {
|
|
54
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function codeOf(page: unknown): string {
|
|
58
|
+
const record = asRecord(page);
|
|
59
|
+
return record === undefined ? '' : textOf(record['code']);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function messageOf(page: unknown): string {
|
|
63
|
+
const record = asRecord(page);
|
|
64
|
+
return record === undefined ? '' : textOf(record['message']);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function hintOf(page: unknown): string | undefined {
|
|
68
|
+
const record = asRecord(page);
|
|
69
|
+
const hint = record === undefined ? '' : textOf(record['hint']);
|
|
70
|
+
return hint === '' ? undefined : hint;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function pageFailed(page: unknown): boolean {
|
|
74
|
+
const record = asRecord(page);
|
|
75
|
+
return record !== undefined && record['ok'] === false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function blockerFor(code: string): 'auth-required' | 'permission-denied' | undefined {
|
|
79
|
+
const normalized = code.trim().toUpperCase().replace(/-/g, '_');
|
|
80
|
+
if (normalized === '') return undefined;
|
|
81
|
+
const matches = (set: Set<string>): boolean =>
|
|
82
|
+
[...set].some((one) => normalized === one || normalized.endsWith(`_${one}`));
|
|
83
|
+
if (matches(PERMISSION_CODES)) return 'permission-denied';
|
|
84
|
+
if (matches(AUTH_CODES) || normalized.includes('UNAUTH')) return 'auth-required';
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function sleep(ms: number): Promise<void> {
|
|
89
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
type Attempt =
|
|
93
|
+
| { readonly kind: 'value'; readonly value: unknown }
|
|
94
|
+
| { readonly kind: 'failure'; readonly result: Omit<BrowserReadResult, 'at'> };
|
|
95
|
+
|
|
96
|
+
type ShellOutcome =
|
|
97
|
+
| { readonly kind: 'ok'; readonly stdout: string }
|
|
98
|
+
| { readonly kind: 'timeout'; readonly detail: string; readonly stderr: string }
|
|
99
|
+
| { readonly kind: 'failed'; readonly detail: string; readonly stderr: string };
|
|
100
|
+
|
|
101
|
+
async function shellRun(
|
|
102
|
+
shell: DshShell,
|
|
103
|
+
command: string,
|
|
104
|
+
argv: readonly string[],
|
|
105
|
+
timeoutMs: number,
|
|
106
|
+
): Promise<ShellOutcome> {
|
|
107
|
+
const result = await runShell(shell, backgroundLine(command, argv), { timeoutMs });
|
|
108
|
+
if (result.timedOut) {
|
|
109
|
+
return { kind: 'timeout', detail: `没在 ${Math.round(timeoutMs / 1000)} 秒内返回`, stderr: result.stderr };
|
|
110
|
+
}
|
|
111
|
+
if (result.exitCode !== 0) {
|
|
112
|
+
return { kind: 'failed', detail: tailLines(result.stderr || result.stdout, 4), stderr: result.stderr };
|
|
113
|
+
}
|
|
114
|
+
return { kind: 'ok', stdout: result.stdout };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function evalOnce(
|
|
118
|
+
shell: DshShell,
|
|
119
|
+
command: string,
|
|
120
|
+
session: string,
|
|
121
|
+
script: string,
|
|
122
|
+
timeoutMs: number,
|
|
123
|
+
retryMs: number,
|
|
124
|
+
): Promise<Attempt> {
|
|
125
|
+
const run = async (): Promise<Attempt> => {
|
|
126
|
+
let attempt: Awaited<ReturnType<typeof shellRun>>;
|
|
127
|
+
try {
|
|
128
|
+
attempt = await shellRun(
|
|
129
|
+
shell,
|
|
130
|
+
command,
|
|
131
|
+
['browser', session, 'eval', script, '--window', 'background'],
|
|
132
|
+
timeoutMs,
|
|
133
|
+
);
|
|
134
|
+
} catch (error) {
|
|
135
|
+
return {
|
|
136
|
+
kind: 'failure',
|
|
137
|
+
result: {
|
|
138
|
+
ok: false,
|
|
139
|
+
code: 'cli-exec-failed',
|
|
140
|
+
message: '读不了:opencli 起不来',
|
|
141
|
+
hint: '确认这台机器上 opencli 能跑(工作台会自己把它挂好)',
|
|
142
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
if (attempt.kind === 'timeout') {
|
|
147
|
+
return {
|
|
148
|
+
kind: 'failure',
|
|
149
|
+
result: {
|
|
150
|
+
ok: false,
|
|
151
|
+
code: 'cli-timeout',
|
|
152
|
+
message: '这次没读到(等超时了)',
|
|
153
|
+
hint: '稍后重试',
|
|
154
|
+
detail: attempt.detail,
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
if (attempt.kind === 'failed') {
|
|
159
|
+
const normalized = attempt.stderr.trim().toUpperCase().replace(/-/g, '_');
|
|
160
|
+
const forbidden = [...PERMISSION_CODES].some((one) => normalized.includes(one));
|
|
161
|
+
return {
|
|
162
|
+
kind: 'failure',
|
|
163
|
+
result: {
|
|
164
|
+
ok: false,
|
|
165
|
+
code: forbidden ? 'permission-denied' : 'cli-exec-failed',
|
|
166
|
+
message: forbidden ? '没有权限在这台机器上读' : '读不了(命令失败)',
|
|
167
|
+
hint: forbidden ? '换一个有权限的账号,或让用户开权限' : '把下面这段原始输出看一眼再重试',
|
|
168
|
+
detail: attempt.detail,
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
const value = parseJsonLoose(attempt.stdout);
|
|
173
|
+
if (value === undefined) {
|
|
174
|
+
return {
|
|
175
|
+
kind: 'failure',
|
|
176
|
+
result: {
|
|
177
|
+
ok: false,
|
|
178
|
+
code: 'cli-exec-failed',
|
|
179
|
+
message: '读到了、但不是 JSON',
|
|
180
|
+
hint: '脚本必须回 JSON',
|
|
181
|
+
detail: tailLines(attempt.stdout, 4),
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
return { kind: 'value', value };
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const first = await run();
|
|
189
|
+
if (first.kind === 'value') return first;
|
|
190
|
+
if (first.result.code !== 'cli-exec-failed') return first;
|
|
191
|
+
await sleep(retryMs);
|
|
192
|
+
return run();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function openOnce(
|
|
196
|
+
shell: DshShell,
|
|
197
|
+
command: string,
|
|
198
|
+
session: string,
|
|
199
|
+
url: string,
|
|
200
|
+
timeoutMs: number,
|
|
201
|
+
): Promise<Omit<BrowserReadResult, 'at'> | undefined> {
|
|
202
|
+
let attempt: Awaited<ReturnType<typeof shellRun>>;
|
|
203
|
+
try {
|
|
204
|
+
attempt = await shellRun(shell, command, ['browser', session, 'open', url, '--window', 'background'], timeoutMs);
|
|
205
|
+
} catch (error) {
|
|
206
|
+
return {
|
|
207
|
+
ok: false,
|
|
208
|
+
code: 'cli-exec-failed',
|
|
209
|
+
message: '打不开那个页面',
|
|
210
|
+
hint: '确认 opencli 能跑起来',
|
|
211
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
212
|
+
helpUrl: url,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
if (attempt.kind === 'ok') return undefined;
|
|
216
|
+
return {
|
|
217
|
+
ok: false,
|
|
218
|
+
code: attempt.kind === 'timeout' ? 'cli-timeout' : 'cli-exec-failed',
|
|
219
|
+
message: '打不开那个页面',
|
|
220
|
+
hint: '在浏览器里手动打开一次,然后重试',
|
|
221
|
+
detail: attempt.detail,
|
|
222
|
+
helpUrl: url,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export async function browserRead(options: BrowserReadOptions): Promise<BrowserReadResult> {
|
|
227
|
+
const { shell, script, url, loginUrl, open = true } = options;
|
|
228
|
+
const command = options.command ?? 'opencli';
|
|
229
|
+
const session = options.session ?? BROWSER_BRIDGE_SESSION;
|
|
230
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
231
|
+
const retryMs = options.retryMs ?? DEFAULT_RETRY_MS;
|
|
232
|
+
const settleMs = options.settleMs ?? DEFAULT_SETTLE_MS;
|
|
233
|
+
const at = Date.now();
|
|
234
|
+
|
|
235
|
+
const first = await evalOnce(shell, command, session, script, timeoutMs, retryMs);
|
|
236
|
+
if (first.kind === 'failure') return { ...first.result, at };
|
|
237
|
+
let page = first.value;
|
|
238
|
+
|
|
239
|
+
if (codeOf(page) === 'not-on-site') {
|
|
240
|
+
if (!open || url === undefined) {
|
|
241
|
+
return {
|
|
242
|
+
ok: false,
|
|
243
|
+
code: 'cli-exec-failed',
|
|
244
|
+
at,
|
|
245
|
+
message: '没停在那套系统上',
|
|
246
|
+
hint: '先在那个浏览器里把它打开,再读一次',
|
|
247
|
+
detail: JSON.stringify(page),
|
|
248
|
+
...(url === undefined ? {} : { helpUrl: url }),
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
const opened = await openOnce(shell, command, session, url, timeoutMs);
|
|
252
|
+
if (opened !== undefined) return { ...opened, at };
|
|
253
|
+
await sleep(settleMs);
|
|
254
|
+
const second = await evalOnce(shell, command, session, script, timeoutMs, retryMs);
|
|
255
|
+
if (second.kind === 'failure') return { ...second.result, at };
|
|
256
|
+
page = second.value;
|
|
257
|
+
if (codeOf(page) === 'not-on-site') {
|
|
258
|
+
return {
|
|
259
|
+
ok: false,
|
|
260
|
+
code: 'cli-exec-failed',
|
|
261
|
+
at,
|
|
262
|
+
message: '打开了页面、又读了一遍,仍然不在那套系统上',
|
|
263
|
+
hint: '确认这个地址就是那套系统(或者它跳到了别的域)',
|
|
264
|
+
detail: JSON.stringify(page),
|
|
265
|
+
helpUrl: url,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (!pageFailed(page)) return { ok: true, code: 'ok', at, value: page, message: '读到了' };
|
|
271
|
+
|
|
272
|
+
const pageCode = codeOf(page) || 'read-failed';
|
|
273
|
+
const message = messageOf(page) || `读不到(${pageCode})`;
|
|
274
|
+
const blocker = blockerFor(pageCode);
|
|
275
|
+
if (blocker === 'auth-required') {
|
|
276
|
+
return {
|
|
277
|
+
ok: false,
|
|
278
|
+
code: blocker,
|
|
279
|
+
at,
|
|
280
|
+
pageCode,
|
|
281
|
+
message,
|
|
282
|
+
hint: '去登录一次,再重试',
|
|
283
|
+
detail: JSON.stringify(page),
|
|
284
|
+
...(loginUrl === undefined && url === undefined ? {} : { helpUrl: loginUrl ?? url }),
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
if (blocker === 'permission-denied') {
|
|
288
|
+
return {
|
|
289
|
+
ok: false,
|
|
290
|
+
code: blocker,
|
|
291
|
+
at,
|
|
292
|
+
pageCode,
|
|
293
|
+
message,
|
|
294
|
+
hint: '这个账号没有权限',
|
|
295
|
+
detail: JSON.stringify(page),
|
|
296
|
+
...(loginUrl === undefined && url === undefined ? {} : { helpUrl: loginUrl ?? url }),
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
return {
|
|
300
|
+
ok: false,
|
|
301
|
+
code: pageCode,
|
|
302
|
+
at,
|
|
303
|
+
pageCode,
|
|
304
|
+
message,
|
|
305
|
+
detail: JSON.stringify(page),
|
|
306
|
+
...(hintOf(page) === undefined ? {} : { hint: hintOf(page) }),
|
|
307
|
+
};
|
|
308
|
+
}
|
package/src/host/http.ts
CHANGED
|
@@ -249,7 +249,13 @@ export function capabilityHandler(
|
|
|
249
249
|
};
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
-
export
|
|
252
|
+
export interface OpencliHealth {
|
|
253
|
+
readonly command: string;
|
|
254
|
+
readonly source: 'env' | 'isolated' | 'path';
|
|
255
|
+
readonly isolated: string | null;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function healthHandler(ctx: DshHostContext, opencli?: OpencliHealth): RouteHandler {
|
|
253
259
|
return () => {
|
|
254
260
|
const profile = ctx.get<{ dir?: string }>('profileContext');
|
|
255
261
|
const dir = profile?.dir;
|
|
@@ -260,6 +266,7 @@ export function healthHandler(ctx: DshHostContext): RouteHandler {
|
|
|
260
266
|
profileDir: dir ?? null,
|
|
261
267
|
pid: process.pid,
|
|
262
268
|
now: new Date().toISOString(),
|
|
269
|
+
...(opencli === undefined ? {} : { opencli }),
|
|
263
270
|
};
|
|
264
271
|
};
|
|
265
272
|
}
|
|
@@ -28,6 +28,19 @@ export function vendorPrefix(): string {
|
|
|
28
28
|
return `${isolatedRoot()}/plugins-npm`;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
export function vendorPluginsDir(): string {
|
|
32
|
+
return `${vendorPrefix()}/node_modules`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const OPENCLI_CATALOG_ENV = 'DSH_WB_OPENCLI_CATALOG';
|
|
36
|
+
|
|
37
|
+
export function cliCatalogPath(): string {
|
|
38
|
+
const override = process.env[OPENCLI_CATALOG_ENV]?.trim();
|
|
39
|
+
return override === undefined || override === ''
|
|
40
|
+
? `${toolchainOpencliDir()}/cli-manifest.json`
|
|
41
|
+
: override;
|
|
42
|
+
}
|
|
43
|
+
|
|
31
44
|
export function vendorPeerLink(): { readonly dir: string; readonly parent: string } {
|
|
32
45
|
const base = `${vendorPrefix()}/node_modules`;
|
|
33
46
|
const [scope] = OPENCLI_NPM_PACKAGE.split('/');
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { quoteShellArg } from '../lib/text.js';
|
|
2
|
+
|
|
3
|
+
const OPENCLI_WINDOW_ENV = 'OPENCLI_WINDOW=background';
|
|
4
|
+
|
|
5
|
+
export function backgroundPrefix(): readonly string[] {
|
|
6
|
+
return [quoteShellArg('env'), quoteShellArg(OPENCLI_WINDOW_ENV)];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function backgroundLine(command: string, argv: readonly string[]): string {
|
|
10
|
+
return [...backgroundPrefix(), quoteShellArg(command), ...argv.map(quoteShellArg)].join(' ');
|
|
11
|
+
}
|
package/src/host/opencli.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { DshShell } from '../api/dsh.js';
|
|
|
2
2
|
import { OPENCLI_MAX_CONCURRENT, OPENCLI_TIMEOUT_MS } from '../config/constants.js';
|
|
3
3
|
import { parseJsonLoose, quoteShellArg, tailLines } from '../lib/text.js';
|
|
4
4
|
import { helpUrlOf, parseCliErrorEnvelope } from './cli-error.js';
|
|
5
|
+
import { backgroundPrefix } from './opencli-window.js';
|
|
5
6
|
import { runShell } from './shell.js';
|
|
6
7
|
|
|
7
8
|
const FORBIDDEN_CODES = new Set(['FORBIDDEN', 'PERMISSION_DENIED']);
|
|
@@ -43,6 +44,7 @@ export function opencliCommandLine(
|
|
|
43
44
|
session: OpencliSession = 'persistent',
|
|
44
45
|
): string {
|
|
45
46
|
return [
|
|
47
|
+
...backgroundPrefix(),
|
|
46
48
|
quoteShellArg(command),
|
|
47
49
|
...argv.map(quoteShellArg),
|
|
48
50
|
...(session === 'persistent' ? SITE_SESSION_ARGS : []),
|
package/src/host.ts
CHANGED
|
@@ -8,6 +8,8 @@ export * from './lib/ui.js';
|
|
|
8
8
|
export * from './api/blocker-table.js';
|
|
9
9
|
export * from './host/cli-error.js';
|
|
10
10
|
export * from './host/data-root.js';
|
|
11
|
+
export * from './host/browser-read.js';
|
|
12
|
+
export * from './host/browser-find.js';
|
|
11
13
|
export * from './host/facts.js';
|
|
12
14
|
export * from './host/git.js';
|
|
13
15
|
export * from './host/http.js';
|
package/src/i18n/en.ts
CHANGED
|
@@ -101,6 +101,8 @@ export const en: Record<keyof WorkbenchMessages, string> = {
|
|
|
101
101
|
'panels.market.badSource': 'The install method is incomplete — install it yourself',
|
|
102
102
|
'panels.market.empty': 'No modules available',
|
|
103
103
|
'panels.market.installedSection': 'Installed',
|
|
104
|
+
'panels.market.sectionLocal': 'Linked locally',
|
|
105
|
+
'panels.market.sectionUnlisted': 'Not from the market',
|
|
104
106
|
'panels.market.moduleOutdated': 'update available',
|
|
105
107
|
'panels.market.moduleRestartHint': 'installed but not active — restart dsh once to bring it up',
|
|
106
108
|
'panels.market.restartNeeded': 'The new version is installed — restart once to pick it up.',
|
|
@@ -309,6 +311,7 @@ export const en: Record<keyof WorkbenchMessages, string> = {
|
|
|
309
311
|
'rightbar.refreshing': 'Refreshing…',
|
|
310
312
|
'rightbar.updatedAt': 'updated {time}',
|
|
311
313
|
'rightbar.noProject': 'The current session has no workspace',
|
|
314
|
+
'rightbar.openPlatform': 'Open on the platform',
|
|
312
315
|
'coding.notARepo.title': 'This workspace is not a Coding repository',
|
|
313
316
|
'error.unknown': 'Something went wrong',
|
|
314
317
|
|
package/src/i18n/zh.ts
CHANGED
|
@@ -62,7 +62,7 @@ export const zh = {
|
|
|
62
62
|
'panels.browseMarket': '浏览模块市场',
|
|
63
63
|
'panels.noneInstalled': '这些模块要的插件都还没装',
|
|
64
64
|
'panels.noneInstalledHint': '装好之后它们会回到这里,再由你决定放哪几个上板。',
|
|
65
|
-
'panels.market.installed': '
|
|
65
|
+
'panels.market.installed': '已安装',
|
|
66
66
|
'panels.market.outdated': '要更新',
|
|
67
67
|
'panels.market.hasNew': '有新版本',
|
|
68
68
|
'panels.market.missing': '未装',
|
|
@@ -96,7 +96,9 @@ export const zh = {
|
|
|
96
96
|
'panels.market.noSource': '这个模块没声明怎么装,只能自己动手',
|
|
97
97
|
'panels.market.badSource': '安装方式没写全,只能自己动手',
|
|
98
98
|
'panels.market.empty': '暂无可用模块',
|
|
99
|
-
'panels.market.installedSection': '
|
|
99
|
+
'panels.market.installedSection': '已安装',
|
|
100
|
+
'panels.market.sectionLocal': '本地装的',
|
|
101
|
+
'panels.market.sectionUnlisted': '不是从市场装的',
|
|
100
102
|
'panels.market.moduleOutdated': '有新版',
|
|
101
103
|
'panels.market.moduleRestartHint': '装好了但还没生效 —— 重启一次 dsh 就会出来',
|
|
102
104
|
'panels.market.restartNeeded': '新版装好了,重启一次才生效。',
|
|
@@ -305,6 +307,7 @@ export const zh = {
|
|
|
305
307
|
'rightbar.refreshing': '刷新中…',
|
|
306
308
|
'rightbar.updatedAt': '更新于 {time}',
|
|
307
309
|
'rightbar.noProject': '当前会话没有所属项目',
|
|
310
|
+
'rightbar.openPlatform': '在平台上打开',
|
|
308
311
|
'coding.notARepo.title': '这个项目不是 Coding 仓库',
|
|
309
312
|
'error.unknown': '出错了',
|
|
310
313
|
|
package/src/index.ts
CHANGED
|
@@ -27,6 +27,8 @@ export * from './components/SettingsSection.js';
|
|
|
27
27
|
export * from './components/Stack.js';
|
|
28
28
|
export * from './components/StateBlock.js';
|
|
29
29
|
export * from './components/TextField.js';
|
|
30
|
+
export * from './components/Field.js';
|
|
31
|
+
export * from './components/Link.js';
|
|
30
32
|
export * from './components/board-module.js';
|
|
31
33
|
export * from './components/icons.js';
|
|
32
34
|
export * from './config/constants.js';
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { h } from '../lib/react.js';
|
|
2
|
-
export function StateDot({ state, size }) {
|
|
3
|
-
if (state === 'ongoing') {
|
|
4
|
-
return (h("svg", { className: "wb-loading", viewBox: "0 0 24 24", width: size, height: size, "aria-hidden": "true", style: { display: 'block' } },
|
|
5
|
-
h("g", { className: "wb-loadingMotion" },
|
|
6
|
-
h("circle", { className: "wb-loadingTrack", cx: 12, cy: 12, r: 9.5 }),
|
|
7
|
-
h("circle", { className: "wb-loadingArc", cx: 12, cy: 12, r: 9.5 }))));
|
|
8
|
-
}
|
|
9
|
-
return (h("span", { className: "wb-stateDot", "data-state": state, style: { width: size, height: size }, "aria-hidden": "true" }));
|
|
10
|
-
}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import type { DshShell } from '../../api/dsh.js';
|
|
2
|
-
import type { Opencli } from '../../host/opencli.js';
|
|
3
|
-
export interface ShellCall {
|
|
4
|
-
command: string;
|
|
5
|
-
workdir?: string;
|
|
6
|
-
timeoutMs?: number;
|
|
7
|
-
onExpiry?: string;
|
|
8
|
-
stdoutMaxBytes?: number;
|
|
9
|
-
}
|
|
10
|
-
export interface ShellReply {
|
|
11
|
-
stdout?: string;
|
|
12
|
-
stderr?: string;
|
|
13
|
-
exitCode?: number | null;
|
|
14
|
-
timedOut?: boolean;
|
|
15
|
-
aborted?: boolean;
|
|
16
|
-
failWith?: Error;
|
|
17
|
-
}
|
|
18
|
-
export declare function createFakeShell(replies?: ShellReply[] | ((call: ShellCall, index: number) => ShellReply)): {
|
|
19
|
-
shell: DshShell;
|
|
20
|
-
calls: ShellCall[];
|
|
21
|
-
peak: () => number;
|
|
22
|
-
commands: () => string[];
|
|
23
|
-
};
|
|
24
|
-
export interface OpencliCall {
|
|
25
|
-
args: readonly string[];
|
|
26
|
-
key?: string;
|
|
27
|
-
workdir?: string;
|
|
28
|
-
refresh?: boolean;
|
|
29
|
-
}
|
|
30
|
-
export declare function createFakeOpencli(replies?: Array<unknown | Error> | ((call: OpencliCall, index: number) => unknown | Error)): {
|
|
31
|
-
opencli: Opencli;
|
|
32
|
-
calls: OpencliCall[];
|
|
33
|
-
keys: () => (string | undefined)[];
|
|
34
|
-
};
|
|
35
|
-
export declare function createFakeLogger(): {
|
|
36
|
-
logger: {
|
|
37
|
-
info: () => void;
|
|
38
|
-
warn: (message: string) => void;
|
|
39
|
-
};
|
|
40
|
-
warnings: string[];
|
|
41
|
-
};
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import { setTimeout as delay } from 'node:timers/promises';
|
|
2
|
-
const tick = () => delay(0);
|
|
3
|
-
export function createFakeShell(replies = []) {
|
|
4
|
-
const calls = [];
|
|
5
|
-
let running = 0;
|
|
6
|
-
let peak = 0;
|
|
7
|
-
const replyAt = (index) => typeof replies === 'function' ? replies(calls[index], index) : (replies[index] ?? {});
|
|
8
|
-
const shell = {
|
|
9
|
-
resolve(request) {
|
|
10
|
-
calls.push(request);
|
|
11
|
-
return request;
|
|
12
|
-
},
|
|
13
|
-
async execute() {
|
|
14
|
-
const index = calls.length - 1;
|
|
15
|
-
const reply = replyAt(index);
|
|
16
|
-
if (reply.failWith !== undefined)
|
|
17
|
-
throw reply.failWith;
|
|
18
|
-
running += 1;
|
|
19
|
-
peak = Math.max(peak, running);
|
|
20
|
-
return {
|
|
21
|
-
async result() {
|
|
22
|
-
await tick();
|
|
23
|
-
running -= 1;
|
|
24
|
-
return {
|
|
25
|
-
exitCode: reply.exitCode ?? 0,
|
|
26
|
-
timedOut: reply.timedOut ?? false,
|
|
27
|
-
aborted: reply.aborted ?? false,
|
|
28
|
-
stdout: { text: reply.stdout ?? '', truncated: false },
|
|
29
|
-
stderr: { text: reply.stderr ?? '', truncated: false },
|
|
30
|
-
};
|
|
31
|
-
},
|
|
32
|
-
};
|
|
33
|
-
},
|
|
34
|
-
};
|
|
35
|
-
return {
|
|
36
|
-
shell,
|
|
37
|
-
calls,
|
|
38
|
-
peak: () => peak,
|
|
39
|
-
commands: () => calls.map((call) => call.command),
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
export function createFakeOpencli(replies = []) {
|
|
43
|
-
const calls = [];
|
|
44
|
-
const opencli = {
|
|
45
|
-
async json(args, options = {}) {
|
|
46
|
-
const call = {
|
|
47
|
-
args,
|
|
48
|
-
...(options.key === undefined ? {} : { key: options.key }),
|
|
49
|
-
...(options.workdir === undefined ? {} : { workdir: options.workdir }),
|
|
50
|
-
};
|
|
51
|
-
calls.push(call);
|
|
52
|
-
const index = calls.length - 1;
|
|
53
|
-
const reply = typeof replies === 'function' ? replies(call, index) : (replies[index] ?? []);
|
|
54
|
-
if (reply instanceof Error)
|
|
55
|
-
throw reply;
|
|
56
|
-
return { value: reply, at: Date.now() };
|
|
57
|
-
},
|
|
58
|
-
};
|
|
59
|
-
return {
|
|
60
|
-
opencli,
|
|
61
|
-
calls,
|
|
62
|
-
keys: () => calls.map((call) => call.key),
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
export function createFakeLogger() {
|
|
66
|
-
const warnings = [];
|
|
67
|
-
return {
|
|
68
|
-
logger: {
|
|
69
|
-
info: () => { },
|
|
70
|
-
warn: (message) => {
|
|
71
|
-
warnings.push(message);
|
|
72
|
-
},
|
|
73
|
-
},
|
|
74
|
-
warnings,
|
|
75
|
-
};
|
|
76
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { createFakeShell } from './fakes';
|
|
2
|
-
export declare const GOOD_BRIDGE_REPORT: string;
|
|
3
|
-
export declare const ALL_OK: {
|
|
4
|
-
stdout: string;
|
|
5
|
-
}[];
|
|
6
|
-
export declare const STALE_DAEMON_BRIDGE: string;
|
|
7
|
-
export declare const BROKEN_BRIDGE: string;
|
|
8
|
-
export declare function build(replies: Parameters<typeof createFakeShell>[0], install?: () => Promise<{
|
|
9
|
-
ok: boolean;
|
|
10
|
-
detail: string;
|
|
11
|
-
}>, warmReads?: () => readonly (readonly string[])[]): {
|
|
12
|
-
fake: {
|
|
13
|
-
shell: import("../..").DshShell;
|
|
14
|
-
calls: import("./fakes").ShellCall[];
|
|
15
|
-
peak: () => number;
|
|
16
|
-
commands: () => string[];
|
|
17
|
-
};
|
|
18
|
-
preflight: import("../preflight").Preflight;
|
|
19
|
-
};
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { createPreflight } from '../preflight';
|
|
2
|
-
import { createFakeShell } from './fakes';
|
|
3
|
-
export const GOOD_BRIDGE_REPORT = [
|
|
4
|
-
'opencli v1.8.7 doctor (node v24.15.0)',
|
|
5
|
-
'',
|
|
6
|
-
'[OK] Daemon: running on port 19825 (v1.8.7)',
|
|
7
|
-
'[OK] Extension: connected (v1.0.24)',
|
|
8
|
-
'[OK] Connectivity: connected in 0.0s',
|
|
9
|
-
'',
|
|
10
|
-
'Everything looks good!',
|
|
11
|
-
].join('\n');
|
|
12
|
-
export const ALL_OK = [{ stdout: '1.8.8\n' }, { stdout: GOOD_BRIDGE_REPORT }];
|
|
13
|
-
export const STALE_DAEMON_BRIDGE = [
|
|
14
|
-
'opencli v1.8.7 doctor (node v24.15.0)',
|
|
15
|
-
'',
|
|
16
|
-
'[STALE] Daemon: running on port 19825 (v1.8.8 != CLI v1.8.7)',
|
|
17
|
-
'[OK] Extension: connected (v1.0.24)',
|
|
18
|
-
'[OK] Connectivity: connected in 0.0s',
|
|
19
|
-
'',
|
|
20
|
-
'Issues:',
|
|
21
|
-
' • Stale daemon detected: daemon v1.8.8 != CLI v1.8.7.',
|
|
22
|
-
' Run: opencli daemon restart',
|
|
23
|
-
].join('\n');
|
|
24
|
-
export const BROKEN_BRIDGE = [
|
|
25
|
-
'opencli v1.8.7 doctor (node v24.15.0)',
|
|
26
|
-
'',
|
|
27
|
-
'[MISSING] Daemon: not running',
|
|
28
|
-
'[MISSING] Extension: not connected',
|
|
29
|
-
'[FAIL] Connectivity: failed (daemon not reachable)',
|
|
30
|
-
'',
|
|
31
|
-
'Issues:',
|
|
32
|
-
" • Daemon is not running. It should start automatically when you run an opencli browser command.",
|
|
33
|
-
' • Daemon is running but the Chrome/Chromium extension is not connected.',
|
|
34
|
-
'If the extension is not installed:',
|
|
35
|
-
' 1. Download from https://github.com/jackwener/opencli/releases',
|
|
36
|
-
' 2. Open chrome://extensions/ → Enable Developer Mode',
|
|
37
|
-
' 3. Click "Load unpacked" → select the extension folder',
|
|
38
|
-
].join('\n');
|
|
39
|
-
export function build(replies, install, warmReads) {
|
|
40
|
-
const fake = createFakeShell(replies);
|
|
41
|
-
const preflight = createPreflight({
|
|
42
|
-
shell: fake.shell,
|
|
43
|
-
...(install === undefined ? {} : { install }),
|
|
44
|
-
...(warmReads === undefined ? {} : { warmReads }),
|
|
45
|
-
});
|
|
46
|
-
return { fake, preflight };
|
|
47
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { type ContextRef } from '@dsh-workbench/shared';
|
|
2
|
-
import type { ContextRenderer } from './unit-face';
|
|
3
|
-
export declare const CONTEXT_SOURCE_KIND = "workbench";
|
|
4
|
-
export interface ScannableMessage {
|
|
5
|
-
readonly source?: {
|
|
6
|
-
readonly kind?: string;
|
|
7
|
-
};
|
|
8
|
-
readonly content?: readonly {
|
|
9
|
-
readonly type?: string;
|
|
10
|
-
readonly text?: string;
|
|
11
|
-
}[];
|
|
12
|
-
}
|
|
13
|
-
export type { ContextRenderer };
|
|
14
|
-
export declare function collectContextRefs(messages: readonly ScannableMessage[]): ContextRef[];
|
|
15
|
-
export declare function buildContextMessage(text: string, summary: string): unknown;
|
|
16
|
-
export interface ContextInjector {
|
|
17
|
-
inject(messages: readonly ScannableMessage[], agent: unknown): Promise<unknown | undefined>;
|
|
18
|
-
}
|
|
19
|
-
export declare function createContextInjector(options: {
|
|
20
|
-
renderers: ReadonlyMap<string, ContextRenderer>;
|
|
21
|
-
logger?: {
|
|
22
|
-
warn?(message: string): void;
|
|
23
|
-
} | undefined;
|
|
24
|
-
}): ContextInjector;
|