@dsh-workbench/shared 1.1.4 → 1.1.6
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/api/host-write.d.ts +4 -1
- package/dist/api/workbench-client.d.ts +2 -0
- package/dist/components/ApprovalPlan.css +7 -8
- package/dist/config/constants.d.ts +4 -0
- package/dist/config/constants.js +4 -0
- package/dist/contract/approval.d.ts +4 -0
- package/dist/contract/approval.js +4 -0
- package/dist/contract/command.d.ts +33 -0
- package/dist/contract/command.js +1 -1
- package/dist/contract/needs.d.ts +17 -0
- package/dist/contract/needs.js +41 -1
- package/dist/contract/protocol/bundle-install.d.ts +2 -0
- package/dist/contract/protocol/market-index.d.ts +3 -1
- package/dist/contract/protocol/market-index.js +2 -0
- package/dist/contract/unit.d.ts +10 -0
- package/dist/host/adapter-types.d.ts +2 -0
- package/dist/host/data-error.d.ts +2 -0
- package/dist/host/data-error.js +3 -0
- package/dist/host/http.js +1 -0
- package/dist/host/install-failure.d.ts +49 -0
- package/dist/host/install-failure.js +167 -0
- package/dist/host/installed-sources.d.ts +10 -0
- package/dist/host/installed-sources.js +64 -0
- package/dist/host/lib-install.d.ts +12 -1
- package/dist/host/lib-install.js +33 -1
- package/dist/host/market-index-claim.d.ts +2 -0
- package/dist/host/market-index-claim.js +7 -0
- package/dist/host/market-index-doc.d.ts +56 -0
- package/dist/host/market-index-doc.js +1 -0
- package/dist/host/market-index-parse.d.ts +23 -0
- package/dist/host/market-index-parse.js +485 -0
- package/dist/host/market-index.d.ts +13 -21
- package/dist/host/market-index.js +120 -368
- package/dist/host/paths.d.ts +7 -0
- package/dist/host/paths.js +17 -0
- package/dist/host/runtime-links.d.ts +7 -0
- package/dist/host/runtime-links.js +25 -0
- package/dist/host/tool-approval.d.ts +4 -2
- package/dist/host/tool-approval.js +4 -2
- package/dist/host.d.ts +3 -0
- package/dist/host.js +3 -0
- package/dist/i18n/en.js +50 -4
- package/dist/i18n/index.d.ts +51 -5
- package/dist/i18n/zh.d.ts +50 -4
- package/dist/i18n/zh.js +50 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +1 -1
- package/src/api/host-write.ts +5 -1
- package/src/api/workbench-client.ts +2 -0
- package/src/components/ApprovalPlan.css +7 -8
- package/src/config/constants.ts +6 -0
- package/src/contract/__tests__/needs.test.ts +53 -0
- package/src/contract/approval.ts +4 -0
- package/src/contract/command.ts +22 -0
- package/src/contract/needs.ts +56 -1
- package/src/contract/protocol/bundle-install.ts +3 -0
- package/src/contract/protocol/market-index.ts +4 -0
- package/src/contract/unit.ts +11 -0
- package/src/host/__tests__/market-index.test.ts +114 -0
- package/src/host/adapter-types.ts +2 -0
- package/src/host/data-error.ts +3 -1
- package/src/host/http.ts +1 -0
- package/src/host/install-failure.test.ts +121 -0
- package/src/host/install-failure.ts +198 -0
- package/src/host/installed-sources.test.ts +80 -0
- package/src/host/installed-sources.ts +73 -0
- package/src/host/lib-install.ts +38 -2
- package/src/host/market-index-claim.ts +9 -0
- package/src/host/market-index-doc.ts +62 -0
- package/src/host/market-index-parse.ts +482 -0
- package/src/host/market-index.ts +162 -357
- package/src/host/paths.test.ts +26 -0
- package/src/host/paths.ts +18 -0
- package/src/host/runtime-links.test.ts +31 -0
- package/src/host/runtime-links.ts +23 -0
- package/src/host/tool-approval.ts +11 -4
- package/src/host.ts +3 -0
- package/src/i18n/en.ts +51 -4
- package/src/i18n/zh.ts +51 -4
- package/src/index.ts +1 -0
package/dist/i18n/zh.js
CHANGED
|
@@ -129,11 +129,14 @@ export const zh = {
|
|
|
129
129
|
'panels.market.update': '更新',
|
|
130
130
|
'panels.market.swapSlow': '还没换上:可能网慢或没装上。再点一次「更新」;一直这样就把 dsh 重启一遍。',
|
|
131
131
|
'panels.market.removeSlow': '还没卸掉:可能网慢或卸载没走完。等一下再看;一直这样就把 dsh 重启一遍。',
|
|
132
|
-
'panels.market.updateNoChange': '
|
|
132
|
+
'panels.market.updateNoChange': '更新没换上:还是 {from};源上最新是 {latest}。',
|
|
133
133
|
'panels.market.updateUnknownVersion': '(问不到)',
|
|
134
134
|
'panels.market.updateGone': '更新失败:更新后找不到这个模块。',
|
|
135
135
|
'panels.market.updateRestored': '更新失败:更新后找不到新版本,已把 {version} 装回去。',
|
|
136
|
-
'panels.market.updateBehind': '装上了 {installed},但源上最新是 {latest}
|
|
136
|
+
'panels.market.updateBehind': '装上了 {installed},但源上最新是 {latest}。',
|
|
137
|
+
'panels.market.updateStuck.title': '更新没换上',
|
|
138
|
+
'panels.market.updateStuck.factNoChange': '盘上还是 {from};源上最新是 {latest}。',
|
|
139
|
+
'panels.market.updateStuck.copiedHint': '粘到终端跑一次,跑完重启一次工作台。',
|
|
137
140
|
'panels.market.selfUpdateFailed': '工作台更新失败:{reason}',
|
|
138
141
|
'panels.market.selfUpdateUnknown': '外部进程没留下原因(看 ~/.dsh/workbench/self-update.log)',
|
|
139
142
|
'panels.market.selfUpdateRolledBack': '工作台更新失败,已回滚到 {version}。',
|
|
@@ -149,6 +152,40 @@ export const zh = {
|
|
|
149
152
|
'panels.market.confirmNo': '取消',
|
|
150
153
|
'panels.market.noMarketSource': '索引里没有这个模块的新版本(刷新一次索引再试)',
|
|
151
154
|
'panels.market.failed': '没成功:{reason}',
|
|
155
|
+
'panels.market.askAgent': '问 agent',
|
|
156
|
+
'panels.market.askAgentCopied': '已复制',
|
|
157
|
+
'panels.market.askAgentText': '工作台里「{name}」(单元 {unit})用不了:{reason}。帮我查清缺什么、怎么补,补完确认真能用。',
|
|
158
|
+
'panels.market.failure.permission': '没装上:没有写入权限',
|
|
159
|
+
'panels.market.failure.diskFull': '没装上:磁盘满了',
|
|
160
|
+
'panels.market.failure.sandbox': '没装上:被系统拦住了',
|
|
161
|
+
'panels.market.failure.unknown': '没装上:原因没认出来',
|
|
162
|
+
'panels.market.failure.factOwner': '{path} 属于 {owner},当前账号({me})写不进去。把它还给自己就行(会要开机密码)。',
|
|
163
|
+
'panels.market.failure.factMode': '{path} 的权限是 {mode},当前账号没有写权限。',
|
|
164
|
+
'panels.market.failure.factSpace': '{path} 所在的磁盘满了。',
|
|
165
|
+
'panels.market.failure.factStuck': '这条命令跑在受限环境里,写不进 {path}。',
|
|
166
|
+
'panels.market.failure.nowhere': '(没认出是哪个目录)',
|
|
167
|
+
'panels.market.failure.copyFix': '复制修复命令',
|
|
168
|
+
'panels.market.failure.copied': '已复制',
|
|
169
|
+
'panels.market.failure.copiedHint': '粘到终端跑一次,回来点「再检测」。',
|
|
170
|
+
'panels.market.failure.askAgent': '复制给 agent',
|
|
171
|
+
'panels.market.failure.askAgentText': '装这个模块时写不进去:`{command}` 退出码 {code},卡在 {path}。帮我看看是被沙箱拦的还是权限问题,换一条能写进去的路再装。',
|
|
172
|
+
'panels.market.failure.askAgentHint': '粘到对话里,让 agent 接着弄。',
|
|
173
|
+
'panels.market.failure.recheck': '再检测',
|
|
174
|
+
'panels.market.failure.showOutput': '查看安装输出',
|
|
175
|
+
'panels.market.failure.hideOutput': '收起安装输出',
|
|
176
|
+
'panels.market.failure.term.commandLine': '第 {line} 行的命令',
|
|
177
|
+
'panels.market.failure.term.signal': '被 {signal} 终止',
|
|
178
|
+
'panels.market.failure.term.exitCode': '退出码 {exitCode}',
|
|
179
|
+
'panels.market.failure.term.noExitCode': '没有退出码',
|
|
180
|
+
'panels.market.failure.term.running': '在跑',
|
|
181
|
+
'panels.market.failure.term.failed': '没跑通',
|
|
182
|
+
'panels.market.failure.term.done': '跑完了',
|
|
183
|
+
'panels.market.failure.term.copy': '复制',
|
|
184
|
+
'panels.market.failure.term.copied': '已复制',
|
|
185
|
+
'panels.market.failure.term.noOutput': '没有输出',
|
|
186
|
+
'panels.market.failure.term.collapse': '收起',
|
|
187
|
+
'panels.market.failure.term.expandAria': '展开剩下的 {hidden} 行',
|
|
188
|
+
'panels.market.failure.term.expand': '展开 {hidden} 行',
|
|
152
189
|
'panels.market.needEnv': '先在启动工作台的终端里设 {env}',
|
|
153
190
|
'panels.market.needCommands': '缺这几条命令:{commands}',
|
|
154
191
|
'panels.market.needBinary': '先装 {command} 命令',
|
|
@@ -185,9 +222,13 @@ export const zh = {
|
|
|
185
222
|
'panels.market.restartNeeded': '新版装好了,重启一次才生效。',
|
|
186
223
|
'panels.market.foot': '装好后到那个项目的面板页勾选它,模块才会出现。',
|
|
187
224
|
'panels.market.checkUpdates': '检查更新',
|
|
225
|
+
'panels.market.checkAll': '检查全部更新',
|
|
188
226
|
'panels.market.checkingNow': '正在检查…',
|
|
189
227
|
'panels.market.checkFailed': '最近一次没查到,检查网络后重试',
|
|
190
228
|
'panels.market.checkedAt': '最近检查:{time}',
|
|
229
|
+
'panels.market.latestPartial': '{n} 个模块没查成',
|
|
230
|
+
'panels.market.latestNoRegistry': '没配来源,未检查新版',
|
|
231
|
+
'panels.market.latestProbeFailed': '问不到源,稍后重试',
|
|
191
232
|
'panels.market.justNow': '刚刚',
|
|
192
233
|
'panels.market.minutesAgo': '{n} 分钟前',
|
|
193
234
|
'panels.market.hoursAgo': '{n} 小时前',
|
|
@@ -246,6 +287,8 @@ export const zh = {
|
|
|
246
287
|
'panels.market.add.failurePermission': '没有写入权限,无法安装',
|
|
247
288
|
'panels.market.add.failureBuildBlocked': '有依赖的安装脚本需要你允许后才能继续',
|
|
248
289
|
'panels.market.add.failureIntegrity': '下载的安装包校验失败',
|
|
290
|
+
'panels.market.add.failureStopProfile': '旧版还在用着(工作台正加载这个模块),这一版 dsh 不肯就地覆盖。',
|
|
291
|
+
'panels.market.add.failureCancelled': '这次操作被取消了,什么都没动。',
|
|
249
292
|
'panels.market.add.failureTimeout': '安装超时',
|
|
250
293
|
'panels.market.add.failurePnpmMissing': '没有找到 pnpm,无法安装',
|
|
251
294
|
'panels.market.add.failureInstalled': '它已经装在这份工作台里了。同一个包不能就地覆盖,得先卸载再装;这次卸载也没成功。',
|
|
@@ -329,7 +372,7 @@ export const zh = {
|
|
|
329
372
|
'panels.discover.missingIndex': '仓库里还没有索引文件 {path}',
|
|
330
373
|
'panels.discover.initTitle': '把这个仓库初始化成模块市场索引?',
|
|
331
374
|
'panels.discover.initCta': '初始化成市场索引',
|
|
332
|
-
'panels.discover.initWhat': '会在 {branch}
|
|
375
|
+
'panels.discover.initWhat': '会在 {branch} 分支写索引目录 market/ 与 README.md,提交一次并推上去;之后从这个工作台发布的模块会把打包好的文件提交到 artifacts/<包名>/<版本>/,条目写到 market/<包名>/<版本>.json。仓库的写权限归它的所有者。',
|
|
333
376
|
'panels.discover.initBranch': '分支',
|
|
334
377
|
'panels.discover.initBranchHint': '索引和打包文件都发到这个分支;空仓库会新建它。',
|
|
335
378
|
'panels.discover.initBranchBad': '分支名只能用字母、数字、点、下划线、斜杠或短横线',
|
|
@@ -652,6 +695,9 @@ export const zh = {
|
|
|
652
695
|
'approval.record.noSwitch': '录完不会替你切回来(左栏底部会亮一条状态)',
|
|
653
696
|
'approval.longTask.what': '要停的那次任务',
|
|
654
697
|
'approval.longTask.signal': '停了只是让工作台发出中断信号 —— 这次任务怎么收尾由模块自己负责',
|
|
698
|
+
'approval.plugin.name': '插件',
|
|
699
|
+
'approval.plugin.command': '命令',
|
|
700
|
+
'approval.plugin.note': 'agent 说',
|
|
655
701
|
'approval.publish.module': '模块',
|
|
656
702
|
'approval.publish.target': '目标',
|
|
657
703
|
'approval.publish.version': '版本',
|
|
@@ -671,7 +717,7 @@ export const zh = {
|
|
|
671
717
|
'approval.publish.artifacts': '打包文件',
|
|
672
718
|
'approval.publish.index': '索引',
|
|
673
719
|
'approval.publish.commits': '提交',
|
|
674
|
-
'approval.publish.twoCommits': '打包文件 +
|
|
720
|
+
'approval.publish.twoCommits': '打包文件 + 条目文件两次提交,一次推送',
|
|
675
721
|
'approval.publish.reading': '读取中…',
|
|
676
722
|
'approval.publish.changelog': '{version} 的更新日志',
|
|
677
723
|
'approval.publish.changelogPending': '{version} 这一版会写进更新日志,随包发布',
|
package/dist/index.d.ts
CHANGED
|
@@ -35,6 +35,7 @@ export * from './components/board-module.js';
|
|
|
35
35
|
export * from './components/icons.js';
|
|
36
36
|
export * from './config/constants.js';
|
|
37
37
|
export * from './config/storage.js';
|
|
38
|
+
export * from './contract/approval.js';
|
|
38
39
|
export * from './contract/blocker.js';
|
|
39
40
|
export * from './contract/data.js';
|
|
40
41
|
export * from './contract/extension.js';
|
package/dist/index.js
CHANGED
|
@@ -35,6 +35,7 @@ export * from './components/board-module.js';
|
|
|
35
35
|
export * from './components/icons.js';
|
|
36
36
|
export * from './config/constants.js';
|
|
37
37
|
export * from './config/storage.js';
|
|
38
|
+
export * from './contract/approval.js';
|
|
38
39
|
export * from './contract/blocker.js';
|
|
39
40
|
export * from './contract/data.js';
|
|
40
41
|
export * from './contract/extension.js';
|
package/package.json
CHANGED
package/src/api/host-write.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { InstallFailure } from '../contract/command.js';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* 宿主写口的五条通道(偏好、动作、供给、工坊调试、工坊录制),是写路由的第一段。
|
|
3
5
|
*/
|
|
@@ -9,7 +11,8 @@ export type WriteChannel =
|
|
|
9
11
|
| 'diy-record';
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
|
-
*
|
|
14
|
+
* 写通道统一的结果:成了带值,没成带一句能给人看的话(可选明细与帮助链接);
|
|
15
|
+
* 安装类的失败多带一份结构化现场,界面据此给修法。
|
|
13
16
|
*/
|
|
14
17
|
export type WriteOutcome<T> =
|
|
15
18
|
| { readonly ok: true; readonly value: T }
|
|
@@ -18,4 +21,5 @@ export type WriteOutcome<T> =
|
|
|
18
21
|
readonly message: string;
|
|
19
22
|
readonly detail?: string;
|
|
20
23
|
readonly helpUrl?: string;
|
|
24
|
+
readonly failure?: InstallFailure;
|
|
21
25
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { API_PREFIX } from '../config/constants.js';
|
|
2
|
+
import type { InstallFailure } from '../contract/command.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* 宿主读口 / 写口回的错误:code 给判据分档,message 给人看,helpUrl 有就去点。
|
|
@@ -9,6 +10,7 @@ export interface ApiError {
|
|
|
9
10
|
readonly detail?: string;
|
|
10
11
|
readonly hint?: string;
|
|
11
12
|
readonly helpUrl?: string;
|
|
13
|
+
readonly failure?: InstallFailure;
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
/**
|
|
@@ -10,22 +10,21 @@
|
|
|
10
10
|
color: var(--dsw-alias-label-secondary);
|
|
11
11
|
}
|
|
12
12
|
.wb-approvalRows {
|
|
13
|
-
display:
|
|
14
|
-
|
|
15
|
-
gap: var(--wb-len-
|
|
13
|
+
display: grid;
|
|
14
|
+
grid-template-columns: minmax(var(--wb-len-56), auto) minmax(0, 1fr);
|
|
15
|
+
column-gap: var(--wb-len-8);
|
|
16
|
+
row-gap: var(--wb-len-2);
|
|
17
|
+
align-items: baseline;
|
|
16
18
|
margin: 0;
|
|
17
19
|
}
|
|
18
20
|
.wb-approvalRow {
|
|
19
|
-
display:
|
|
20
|
-
align-items: baseline;
|
|
21
|
-
gap: var(--wb-len-8);
|
|
21
|
+
display: contents;
|
|
22
22
|
}
|
|
23
23
|
.wb-approvalLabel {
|
|
24
|
-
flex: none;
|
|
25
|
-
width: var(--wb-len-56);
|
|
26
24
|
font-size: var(--dsw-font-xxs-12-font-size);
|
|
27
25
|
line-height: var(--wb-line-18);
|
|
28
26
|
color: var(--dsw-alias-label-tertiary);
|
|
27
|
+
overflow-wrap: anywhere;
|
|
29
28
|
}
|
|
30
29
|
.wb-approvalValue {
|
|
31
30
|
margin: 0;
|
package/src/config/constants.ts
CHANGED
|
@@ -109,6 +109,12 @@ export const STORAGE_KEY_RIGHTBAR_COLUMN = "dsh-workbench.rightbar.column";
|
|
|
109
109
|
/** 记住右栏宽度(像素数字的字符串)的 localStorage 键。 */
|
|
110
110
|
export const STORAGE_KEY_RIGHTBAR_WIDTH = "dsh-workbench.rightbar.width";
|
|
111
111
|
|
|
112
|
+
/** 记住「添加模块」浮层选过的安装源的 localStorage 键。 */
|
|
113
|
+
export const STORAGE_KEY_MARKET_REGISTRY = "dsh-workbench.market.registry";
|
|
114
|
+
|
|
115
|
+
/** 记住「添加模块」浮层上次填过的自定义源地址的 localStorage 键。 */
|
|
116
|
+
export const STORAGE_KEY_MARKET_REGISTRY_CUSTOM = "dsh-workbench.market.registry.custom";
|
|
117
|
+
|
|
112
118
|
/** 右栏宽度达到或超过多少像素就按全屏对待(阈值,像素)。 */
|
|
113
119
|
export const RIGHTBAR_FULLSCREEN_WIDTH = 768;
|
|
114
120
|
|
|
@@ -37,6 +37,59 @@ describe('needs 校验矩阵', () => {
|
|
|
37
37
|
);
|
|
38
38
|
});
|
|
39
39
|
|
|
40
|
+
it('bin 声明 install → 过;install 写歪了要判红并点名', () => {
|
|
41
|
+
expect(
|
|
42
|
+
errorsOf({
|
|
43
|
+
kind: 'bin',
|
|
44
|
+
name: 'gitops',
|
|
45
|
+
purpose: '平台命令',
|
|
46
|
+
install: { kind: 'command', line: 'brew install pagoda/tap/gitops', platforms: ['darwin'] },
|
|
47
|
+
docs: 'https://internal/gitops',
|
|
48
|
+
}),
|
|
49
|
+
).toEqual([]);
|
|
50
|
+
|
|
51
|
+
expect(errorsOf({ kind: 'bin', name: 'x', purpose: 'p', install: { kind: 'brew', line: 'x' } }).join(' ')).toContain(
|
|
52
|
+
'install.kind 只认 command',
|
|
53
|
+
);
|
|
54
|
+
expect(errorsOf({ kind: 'bin', name: 'x', purpose: 'p', install: { kind: 'command', line: '' } }).join(' ')).toContain(
|
|
55
|
+
'install.line 没写',
|
|
56
|
+
);
|
|
57
|
+
expect(
|
|
58
|
+
errorsOf({ kind: 'bin', name: 'x', purpose: 'p', install: { kind: 'command', line: 'x', platforms: ['mac'] } }).join(' '),
|
|
59
|
+
).toContain('darwin / linux / win32');
|
|
60
|
+
expect(errorsOf({ kind: 'bin', name: 'x', purpose: 'p', install: { kind: 'command', line: 'x', url: 'y' } }).join(' ')).toContain(
|
|
61
|
+
'install 有不认识的字段 url',
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('bin 的 install 也可以是一个 npm 包(命令由包提供,装进隔离运行时)', () => {
|
|
66
|
+
expect(
|
|
67
|
+
errorsOf({
|
|
68
|
+
kind: 'bin',
|
|
69
|
+
name: 'gitops',
|
|
70
|
+
purpose: '平台命令',
|
|
71
|
+
install: {
|
|
72
|
+
kind: 'npm',
|
|
73
|
+
package: 'opencli-workflow',
|
|
74
|
+
registryEnv: 'ACME_REGISTRY',
|
|
75
|
+
registryDefault: 'https://registry.test/',
|
|
76
|
+
},
|
|
77
|
+
}),
|
|
78
|
+
).toEqual([]);
|
|
79
|
+
|
|
80
|
+
expect(errorsOf({ kind: 'bin', name: 'x', purpose: 'p', install: { kind: 'npm' } }).join(' ')).toContain(
|
|
81
|
+
'install.package 没写',
|
|
82
|
+
);
|
|
83
|
+
expect(
|
|
84
|
+
errorsOf({
|
|
85
|
+
kind: 'bin',
|
|
86
|
+
name: 'x',
|
|
87
|
+
purpose: 'p',
|
|
88
|
+
install: { kind: 'npm', package: 'x', registryDefault: 'ftp://x' },
|
|
89
|
+
}).join(' '),
|
|
90
|
+
).toContain('registryDefault 要是 http(s):// 或 file:// 的源地址');
|
|
91
|
+
});
|
|
92
|
+
|
|
40
93
|
it('bin / env 写了 sources → 红(没有安装路径)', () => {
|
|
41
94
|
expect(errorsOf({ kind: 'bin', name: 'x', purpose: '测试', sources: [npm] }).join(' ')).toContain(
|
|
42
95
|
'不支持 sources',
|
package/src/contract/command.ts
CHANGED
|
@@ -23,3 +23,25 @@ export interface CommandResult {
|
|
|
23
23
|
export interface CommandReader {
|
|
24
24
|
run(request: CommandRequest): Promise<CommandResult>;
|
|
25
25
|
}
|
|
26
|
+
|
|
27
|
+
export const INSTALL_FAILURE_KINDS = ['permission', 'disk-full', 'sandbox', 'unknown'] as const;
|
|
28
|
+
|
|
29
|
+
export type InstallFailureKind = (typeof INSTALL_FAILURE_KINDS)[number];
|
|
30
|
+
|
|
31
|
+
export type InstallFix =
|
|
32
|
+
| { readonly kind: 'chown'; readonly path: string; readonly uid: number; readonly gid: number }
|
|
33
|
+
| { readonly kind: 'chmod'; readonly path: string }
|
|
34
|
+
| { readonly kind: 'space'; readonly path: string }
|
|
35
|
+
| { readonly kind: 'handoff' };
|
|
36
|
+
|
|
37
|
+
export interface InstallFailure {
|
|
38
|
+
readonly kind: InstallFailureKind;
|
|
39
|
+
readonly command: string;
|
|
40
|
+
readonly exitCode: number | null;
|
|
41
|
+
readonly output: string;
|
|
42
|
+
readonly path?: string | undefined;
|
|
43
|
+
readonly mode?: string | undefined;
|
|
44
|
+
readonly owner?: { readonly uid: number; readonly gid: number } | undefined;
|
|
45
|
+
readonly current: { readonly uid: number; readonly gid: number };
|
|
46
|
+
readonly fix: InstallFix;
|
|
47
|
+
}
|
package/src/contract/needs.ts
CHANGED
|
@@ -41,12 +41,31 @@ export interface NeedCommon {
|
|
|
41
41
|
readonly id?: string;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* 一条装这个可执行文件的命令;模块作者写,工作台照抄执行。
|
|
46
|
+
*/
|
|
47
|
+
export type BinInstall =
|
|
48
|
+
| {
|
|
49
|
+
readonly kind: 'command';
|
|
50
|
+
readonly line: string;
|
|
51
|
+
readonly platforms?: readonly string[] | undefined;
|
|
52
|
+
}
|
|
53
|
+
| {
|
|
54
|
+
readonly kind: 'npm';
|
|
55
|
+
readonly package: string;
|
|
56
|
+
readonly registryEnv?: string | undefined;
|
|
57
|
+
readonly registryDefault?: string | undefined;
|
|
58
|
+
readonly platforms?: readonly string[] | undefined;
|
|
59
|
+
};
|
|
60
|
+
|
|
44
61
|
/**
|
|
45
62
|
* 机器上要有一个可执行文件;探法是 `command -v` / `where`。
|
|
46
63
|
*/
|
|
47
64
|
export interface BinNeed extends NeedCommon {
|
|
48
65
|
readonly kind: 'bin';
|
|
49
66
|
readonly name: string;
|
|
67
|
+
readonly install?: BinInstall | undefined;
|
|
68
|
+
readonly docs?: string | undefined;
|
|
50
69
|
}
|
|
51
70
|
|
|
52
71
|
/**
|
|
@@ -107,6 +126,8 @@ export const NEED_KEYS: readonly string[] = [
|
|
|
107
126
|
'version',
|
|
108
127
|
'sources',
|
|
109
128
|
'secret',
|
|
129
|
+
'install',
|
|
130
|
+
'docs',
|
|
110
131
|
];
|
|
111
132
|
|
|
112
133
|
/**
|
|
@@ -188,7 +209,7 @@ function sourcesProblemsOf(
|
|
|
188
209
|
if (!Array.isArray(raw)) return [`${at}.sources 要是数组`];
|
|
189
210
|
if (raw.length === 0) return [`${at}.sources 是空数组 —— 不代装就整条删掉`];
|
|
190
211
|
if (allowed.length === 0) {
|
|
191
|
-
return [`${at} 这种需要不支持 sources ——
|
|
212
|
+
return [`${at} 这种需要不支持 sources —— 要代装就写 install(一条命令)`];
|
|
192
213
|
}
|
|
193
214
|
const errors: string[] = [];
|
|
194
215
|
raw.forEach((source, index) => {
|
|
@@ -214,6 +235,38 @@ function commonProblemsOf(raw: Readonly<Record<string, unknown>>, at: string): r
|
|
|
214
235
|
return errors;
|
|
215
236
|
}
|
|
216
237
|
|
|
238
|
+
export function binInstallProblemsOf(raw: unknown, at: string): readonly string[] {
|
|
239
|
+
if (raw === undefined) return [];
|
|
240
|
+
if (!isRecord(raw)) return [`${at}.install 要是对象`];
|
|
241
|
+
const errors: string[] = [];
|
|
242
|
+
for (const key of Object.keys(raw)) {
|
|
243
|
+
if (!['kind', 'line', 'platforms', 'package', 'registryEnv', 'registryDefault'].includes(key)) {
|
|
244
|
+
errors.push(`${at}.install 有不认识的字段 ${key}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
const kind = text(raw['kind']);
|
|
248
|
+
if (kind !== 'command' && kind !== 'npm') {
|
|
249
|
+
errors.push(`${at}.install.kind 只认 command(一条命令)或 npm(一个包,装进隔离运行时)`);
|
|
250
|
+
}
|
|
251
|
+
if (kind === 'command' && text(raw['line']) === '') {
|
|
252
|
+
errors.push(`${at}.install.line 没写 —— 要一条能直接粘进终端的命令`);
|
|
253
|
+
}
|
|
254
|
+
if (kind === 'npm' && text(raw['package']) === '') {
|
|
255
|
+
errors.push(`${at}.install.package 没写 —— 这个命令由哪个 npm 包提供`);
|
|
256
|
+
}
|
|
257
|
+
if (raw['registryDefault'] !== undefined && !REGISTRY_PATTERN.test(text(raw['registryDefault']))) {
|
|
258
|
+
errors.push(`${at}.install.registryDefault 要是 http(s):// 或 file:// 的源地址`);
|
|
259
|
+
}
|
|
260
|
+
if (raw['platforms'] !== undefined) {
|
|
261
|
+
if (!Array.isArray(raw['platforms']) || raw['platforms'].length === 0) {
|
|
262
|
+
errors.push(`${at}.install.platforms 要是非空数组`);
|
|
263
|
+
} else if (raw['platforms'].some((one) => !['darwin', 'linux', 'win32'].includes(text(one)))) {
|
|
264
|
+
errors.push(`${at}.install.platforms 只认 darwin / linux / win32`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return errors;
|
|
268
|
+
}
|
|
269
|
+
|
|
217
270
|
function perKindProblemsOf(
|
|
218
271
|
kind: NeedKind,
|
|
219
272
|
raw: Readonly<Record<string, unknown>>,
|
|
@@ -223,6 +276,8 @@ function perKindProblemsOf(
|
|
|
223
276
|
if (kind === 'bin' || kind === 'lib' || kind === 'env') {
|
|
224
277
|
if (text(raw['name']) === '') errors.push(`${at}.name 没写`);
|
|
225
278
|
}
|
|
279
|
+
if (kind === 'bin') errors.push(...binInstallProblemsOf(raw['install'], at));
|
|
280
|
+
if (raw['docs'] !== undefined && text(raw['docs']) === '') errors.push(`${at}.docs 是空串`);
|
|
226
281
|
if (kind === 'extension') {
|
|
227
282
|
if (text(raw['runtime']) === '') errors.push(`${at}.runtime 没写 —— 扩展属于哪个运行时(如 opencli)`);
|
|
228
283
|
if (text(raw['name']) === '') errors.push(`${at}.name 没写`);
|
|
@@ -179,6 +179,8 @@ export interface BundleCancellationPayload {
|
|
|
179
179
|
|
|
180
180
|
export type ModuleUpdateBlock = 'needs-workbench' | 'incompatible';
|
|
181
181
|
|
|
182
|
+
export type LatestUnavailable = 'local' | 'no-registry' | 'not-registry' | 'probe-failed';
|
|
183
|
+
|
|
182
184
|
export interface ModuleVersion {
|
|
183
185
|
readonly unit: string;
|
|
184
186
|
readonly pkg: string;
|
|
@@ -186,6 +188,7 @@ export interface ModuleVersion {
|
|
|
186
188
|
readonly title?: UnitTitle;
|
|
187
189
|
readonly latest?: string;
|
|
188
190
|
readonly latestBlocked?: ModuleUpdateBlock;
|
|
191
|
+
readonly latestUnavailable?: LatestUnavailable;
|
|
189
192
|
readonly local?: boolean;
|
|
190
193
|
readonly refused?: readonly string[];
|
|
191
194
|
readonly market?: MarketInstalledModule;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export const MARKET_INDEX_FORMAT = 1;
|
|
2
|
+
export const MARKET_INDEX_DIR = 'market';
|
|
3
|
+
export const MARKET_ARTIFACTS_DIR = 'artifacts';
|
|
2
4
|
export const MARKET_INDEX_PATH = 'market.json';
|
|
3
5
|
export const MARKET_DISCOVER_ROUTE = 'panels/market-discover';
|
|
4
6
|
export const MARKET_DOC_ROUTE = 'panels/market-doc';
|
|
@@ -127,11 +129,13 @@ export type MarketIndexProblemKind =
|
|
|
127
129
|
| 'format'
|
|
128
130
|
| 'plugins'
|
|
129
131
|
| 'plugin'
|
|
132
|
+
| 'entry'
|
|
130
133
|
| 'name'
|
|
131
134
|
| 'duplicate-name'
|
|
132
135
|
| 'versions'
|
|
133
136
|
| 'version'
|
|
134
137
|
| 'version-order'
|
|
138
|
+
| 'duplicate-version'
|
|
135
139
|
| 'source'
|
|
136
140
|
| 'git-commit'
|
|
137
141
|
| 'timestamp'
|
package/src/contract/unit.ts
CHANGED
|
@@ -24,12 +24,23 @@ export interface UnitCard {
|
|
|
24
24
|
readonly order: number;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* 合成工具背后那条 opencli 命令:插件名与命令名都是 opencli 那边的原名,`args` 是命令自己声明的参数名(按声明顺序)。
|
|
29
|
+
*/
|
|
30
|
+
export interface UnitToolOpencli {
|
|
31
|
+
readonly plugin: string;
|
|
32
|
+
readonly command: string;
|
|
33
|
+
readonly args: readonly string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
27
36
|
/**
|
|
28
37
|
* 一条工具在清单里的样子:全名加会不会写;审批按 writes 认领。
|
|
38
|
+
* `opencli` 只有「按已装插件合成」的单元写,手写模块的工具没有这一格。
|
|
29
39
|
*/
|
|
30
40
|
export interface UnitTool {
|
|
31
41
|
readonly name: string;
|
|
32
42
|
readonly writes: boolean;
|
|
43
|
+
readonly opencli?: UnitToolOpencli;
|
|
33
44
|
}
|
|
34
45
|
|
|
35
46
|
/**
|
|
@@ -2,14 +2,23 @@ import { describe, expect, it } from 'vitest';
|
|
|
2
2
|
import type { MarketIndex, MarketIndexEntry } from '../../contract/protocol/market-index.js';
|
|
3
3
|
import { claimOf, claimOfEntry, entryOwnerOf, entryUnitOf, marketArtifactPath, samePublisher } from '../market-index-claim.js';
|
|
4
4
|
import {
|
|
5
|
+
assembleMarketIndex,
|
|
6
|
+
collectMarketEntryFiles,
|
|
5
7
|
filterMarketIndex,
|
|
8
|
+
marketEntryFilesOf,
|
|
9
|
+
marketEntryPath,
|
|
6
10
|
marketInstallTarget,
|
|
11
|
+
marketMetaPath,
|
|
7
12
|
marketSourceKey,
|
|
13
|
+
parseMarketEntryDoc,
|
|
8
14
|
parseMarketIndex,
|
|
9
15
|
parseMarketIndexJson,
|
|
10
16
|
parseSourceConfig,
|
|
11
17
|
upsertMarketEntry,
|
|
12
18
|
} from '../market-index.js';
|
|
19
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
20
|
+
import { tmpdir } from 'node:os';
|
|
21
|
+
import { join } from 'node:path';
|
|
13
22
|
|
|
14
23
|
function entry(change: Partial<MarketIndexEntry> = {}): MarketIndexEntry {
|
|
15
24
|
return {
|
|
@@ -343,3 +352,108 @@ describe('索引源与安装目标', () => {
|
|
|
343
352
|
).toBe(false);
|
|
344
353
|
});
|
|
345
354
|
});
|
|
355
|
+
|
|
356
|
+
describe('条目文件:一个版本一个文件', () => {
|
|
357
|
+
const versionDoc = (version: string, timestamp: number, title: string) => ({
|
|
358
|
+
name: '@scope/module-x',
|
|
359
|
+
version,
|
|
360
|
+
source: { type: 'npm' },
|
|
361
|
+
publishTimestamp: timestamp,
|
|
362
|
+
title: { zh: title },
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it('路径按包名分层,meta 文件带 epoch 与随机后缀', () => {
|
|
366
|
+
expect(marketEntryPath('market', '@scope/module-x', '0.1.0')).toBe('market/@scope/module-x/0.1.0.json');
|
|
367
|
+
expect(marketMetaPath('market', '@scope/module-x', 3, 'a1b2c3')).toBe(
|
|
368
|
+
'market/@scope/module-x/meta-3-a1b2c3.json',
|
|
369
|
+
);
|
|
370
|
+
expect(marketEntryPath('market', 'plain', '1.0.0')).toBe('market/plain/1.0.0.json');
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('包名/版本号不合法就不给路径(防止写出目录外)', () => {
|
|
374
|
+
expect(() => marketEntryPath('market', '../escape', '1.0.0')).toThrow();
|
|
375
|
+
expect(() => marketEntryPath('market', 'ok', '../1.0.0')).toThrow();
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('单条文档能解析,坏文档整条拒', () => {
|
|
379
|
+
const good = parseMarketEntryDoc(versionDoc('0.1.0', 1_760_000_000, '探针'));
|
|
380
|
+
expect(good.ok).toBe(true);
|
|
381
|
+
if (!good.ok) return;
|
|
382
|
+
expect(good.doc.name).toBe('@scope/module-x');
|
|
383
|
+
const bad = parseMarketEntryDoc({ name: 'Bad Name', version: '0.1.0', source: { type: 'npm' } });
|
|
384
|
+
expect(bad.ok).toBe(false);
|
|
385
|
+
const noSource = parseMarketEntryDoc({ name: '@scope/x', version: '0.1.0' });
|
|
386
|
+
expect(noSource.ok).toBe(false);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it('组装:版本全收并按 SemVer 降序,元数据取最新那份', () => {
|
|
390
|
+
const files = [
|
|
391
|
+
{ path: 'market/@scope/module-x/0.1.0.json', text: JSON.stringify(versionDoc('0.1.0', 1_760_000_000, '旧标题')) },
|
|
392
|
+
{ path: 'market/@scope/module-x/0.2.0.json', text: JSON.stringify(versionDoc('0.2.0', 1_760_000_100, '新标题')) },
|
|
393
|
+
{ path: 'market/@scope/module-x/meta-1760000200000.json', text: JSON.stringify({
|
|
394
|
+
name: '@scope/module-x',
|
|
395
|
+
publishTimestamp: 1_760_000_200,
|
|
396
|
+
title: { zh: '最新标题' },
|
|
397
|
+
}) },
|
|
398
|
+
];
|
|
399
|
+
const assembled = assembleMarketIndex(files);
|
|
400
|
+
expect(assembled.ok).toBe(true);
|
|
401
|
+
if (!assembled.ok) return;
|
|
402
|
+
const entry = assembled.index.plugins[0];
|
|
403
|
+
expect(entry?.versions.map((one) => one.version)).toEqual(['0.2.0', '0.1.0']);
|
|
404
|
+
expect(entry?.title?.zh).toBe('最新标题');
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it('组装:同一个版本出现两次就整份拒(那是真重复发布)', () => {
|
|
408
|
+
const assembled = assembleMarketIndex([
|
|
409
|
+
{ path: 'a/0.1.0.json', text: JSON.stringify(versionDoc('0.1.0', 1_760_000_000, '甲')) },
|
|
410
|
+
{ path: 'b/0.1.0.json', text: JSON.stringify(versionDoc('0.1.0', 1_760_000_100, '乙')) },
|
|
411
|
+
]);
|
|
412
|
+
expect(assembled.ok).toBe(false);
|
|
413
|
+
if (assembled.ok) return;
|
|
414
|
+
expect(assembled.problems.some((one) => one.kind === 'duplicate-version')).toBe(true);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it('从目录读只收 .json,README 不当条目', () => {
|
|
418
|
+
const dir = mkdtempSync(join(tmpdir(), 'wb-entry-'));
|
|
419
|
+
try {
|
|
420
|
+
mkdirSync(join(dir, '@scope/module-x'), { recursive: true });
|
|
421
|
+
writeFileSync(join(dir, 'README.md'), 'index');
|
|
422
|
+
writeFileSync(join(dir, '@scope/module-x', '0.1.0.json'), JSON.stringify(versionDoc('0.1.0', 1_760_000_000, '探针')));
|
|
423
|
+
const files = collectMarketEntryFiles(dir);
|
|
424
|
+
expect(files.map((one) => one.path)).toEqual(['@scope/module-x/0.1.0.json']);
|
|
425
|
+
const assembled = assembleMarketIndex(files);
|
|
426
|
+
expect(assembled.ok).toBe(true);
|
|
427
|
+
if (!assembled.ok) return;
|
|
428
|
+
expect(assembled.index.plugins[0]?.name).toBe('@scope/module-x');
|
|
429
|
+
} finally {
|
|
430
|
+
rmSync(dir, { recursive: true, force: true });
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it('迁移:一份 format 1 能拆成条目文件,再组装回来一模一样', () => {
|
|
435
|
+
const legacy: MarketIndex = {
|
|
436
|
+
format: 1,
|
|
437
|
+
plugins: [
|
|
438
|
+
{
|
|
439
|
+
name: '@scope/module-x',
|
|
440
|
+
owner: '甲',
|
|
441
|
+
metadata: { workbench: true, unit: 'x' },
|
|
442
|
+
versions: [
|
|
443
|
+
{ version: '0.2.0', source: { type: 'npm' }, publishTimestamp: 1_760_000_100 },
|
|
444
|
+
{ version: '0.1.0', source: { type: 'npm' }, publishTimestamp: 1_760_000_000 },
|
|
445
|
+
],
|
|
446
|
+
},
|
|
447
|
+
],
|
|
448
|
+
};
|
|
449
|
+
const files = marketEntryFilesOf(legacy, 'market');
|
|
450
|
+
expect(files.map((one) => one.path)).toEqual([
|
|
451
|
+
'market/@scope/module-x/0.2.0.json',
|
|
452
|
+
'market/@scope/module-x/0.1.0.json',
|
|
453
|
+
]);
|
|
454
|
+
const assembled = assembleMarketIndex(files);
|
|
455
|
+
expect(assembled.ok).toBe(true);
|
|
456
|
+
if (!assembled.ok) return;
|
|
457
|
+
expect(assembled.index.plugins).toEqual(legacy.plugins);
|
|
458
|
+
});
|
|
459
|
+
});
|
|
@@ -25,6 +25,7 @@ export interface ProfileModule {
|
|
|
25
25
|
readonly version: string;
|
|
26
26
|
readonly registry?: string;
|
|
27
27
|
readonly local?: boolean;
|
|
28
|
+
readonly fromRegistry?: boolean;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
export interface ProfilePackage {
|
|
@@ -51,6 +52,7 @@ export interface RegistryEntry {
|
|
|
51
52
|
export interface RegistryNotebook {
|
|
52
53
|
sources(): readonly RegistryEntry[];
|
|
53
54
|
remember(entries: readonly RegistryEntry[]): readonly string[];
|
|
55
|
+
swapScope?(scope: string, registry: string): () => void;
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
export interface SelfUpdatePaths {
|
package/src/host/data-error.ts
CHANGED
|
@@ -4,11 +4,12 @@ export class WorkbenchDataError extends Error {
|
|
|
4
4
|
readonly hint?: string;
|
|
5
5
|
readonly project?: unknown;
|
|
6
6
|
readonly helpUrl?: string;
|
|
7
|
+
readonly failure?: unknown;
|
|
7
8
|
|
|
8
9
|
constructor(
|
|
9
10
|
code: string,
|
|
10
11
|
message: string,
|
|
11
|
-
extra: { detail?: string; hint?: string; project?: unknown; helpUrl?: string } = {},
|
|
12
|
+
extra: { detail?: string; hint?: string; project?: unknown; helpUrl?: string; failure?: unknown } = {},
|
|
12
13
|
) {
|
|
13
14
|
super(message);
|
|
14
15
|
this.name = 'WorkbenchDataError';
|
|
@@ -17,5 +18,6 @@ export class WorkbenchDataError extends Error {
|
|
|
17
18
|
if (extra.hint !== undefined) this.hint = extra.hint;
|
|
18
19
|
if (extra.project !== undefined) this.project = extra.project;
|
|
19
20
|
if (extra.helpUrl !== undefined) this.helpUrl = extra.helpUrl;
|
|
21
|
+
if (extra.failure !== undefined) this.failure = extra.failure;
|
|
20
22
|
}
|
|
21
23
|
}
|
package/src/host/http.ts
CHANGED
|
@@ -49,6 +49,7 @@ function toErrorPayload(error: unknown): Record<string, unknown> {
|
|
|
49
49
|
...(data.hint === undefined ? {} : { hint: data.hint }),
|
|
50
50
|
...(data.helpUrl === undefined ? {} : { helpUrl: data.helpUrl }),
|
|
51
51
|
...(data.project === undefined ? {} : { project: data.project }),
|
|
52
|
+
...(data.failure === undefined ? {} : { failure: data.failure }),
|
|
52
53
|
};
|
|
53
54
|
}
|
|
54
55
|
return { code: 'internal', message: error instanceof Error ? error.message : String(error) };
|