@dsh-workbench/shared 0.1.16 → 0.1.18
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/unit-declaration.d.ts +29 -0
- package/dist/api/unit-declaration.js +189 -0
- package/dist/api/unit.d.ts +0 -2
- package/dist/api/workbench-contract.d.ts +1 -1
- package/dist/components/ApprovalDetail.js +1 -1
- package/dist/components/board-module.d.ts +4 -2
- package/dist/host/plugin-ledger.d.ts +32 -0
- package/dist/host/plugin-ledger.js +99 -0
- package/dist/host/provision.d.ts +1 -1
- package/dist/host/provision.js +6 -0
- package/dist/host/unit-face.d.ts +7 -2
- package/dist/host.d.ts +2 -0
- package/dist/host.js +2 -0
- package/dist/i18n/en.js +17 -4
- package/dist/i18n/index.d.ts +18 -5
- package/dist/i18n/zh.d.ts +17 -4
- package/dist/i18n/zh.js +17 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/testing/declaration.d.ts +2 -0
- package/dist/testing/declaration.js +21 -0
- package/dist/testing.d.ts +1 -0
- package/dist/testing.js +1 -0
- package/package.json +1 -1
- package/src/api/__tests__/unit-declaration.test.ts +52 -0
- package/src/api/unit-declaration.ts +244 -0
- package/src/api/unit.ts +0 -2
- package/src/api/workbench-contract.ts +3 -1
- package/src/components/ApprovalDetail.tsx +1 -1
- package/src/components/board-module.ts +5 -2
- package/src/host/__tests__/plugin-ledger.test.ts +94 -0
- package/src/host/__tests__/provision.test.ts +18 -0
- package/src/host/plugin-ledger.ts +152 -0
- package/src/host/provision.ts +4 -0
- package/src/host/unit-face.ts +8 -2
- package/src/host.ts +2 -0
- package/src/i18n/en.ts +18 -4
- package/src/i18n/zh.ts +18 -4
- package/src/index.ts +1 -0
- package/src/testing/__tests__/declaration.test.ts +48 -0
- package/src/testing/declaration.ts +23 -0
- package/src/testing.ts +1 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
heldEntriesOfHome,
|
|
4
|
+
heldEntryOfPlugin,
|
|
5
|
+
ledgerKeyOf,
|
|
6
|
+
parsePluginLedger,
|
|
7
|
+
pluginRetentionOf,
|
|
8
|
+
withReleasedEntry,
|
|
9
|
+
type PluginLedgerEntry,
|
|
10
|
+
} from '../plugin-ledger.js';
|
|
11
|
+
|
|
12
|
+
const HOME = '/h/workbench/opencli/home';
|
|
13
|
+
|
|
14
|
+
function entry(over: Partial<PluginLedgerEntry> = {}): PluginLedgerEntry {
|
|
15
|
+
return {
|
|
16
|
+
opencliHome: HOME,
|
|
17
|
+
package: 'opencli-plugin-cmdb',
|
|
18
|
+
installedAt: '2026-10-14T00:00:00.000Z',
|
|
19
|
+
...over,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('台账条目的「放手」那一笔', () => {
|
|
24
|
+
it('老文件(没有 releasedAt)读出来还是"用户持着"(读旧数据,不是垫片)', () => {
|
|
25
|
+
const parsed = parsePluginLedger({ entries: [entry()] });
|
|
26
|
+
expect(parsed).toHaveLength(1);
|
|
27
|
+
expect(parsed[0]?.releasedAt).toBeUndefined();
|
|
28
|
+
expect(heldEntriesOfHome(parsed, HOME)).toHaveLength(1);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('★ 放手了的那一笔还在文件里(自动清得靠它认得"这是我们装的"),但不再是"持着"', () => {
|
|
32
|
+
const released = withReleasedEntry([entry()], HOME, 'opencli-plugin-cmdb', '2026-10-15T00:00:00.000Z');
|
|
33
|
+
|
|
34
|
+
expect(released).toHaveLength(1);
|
|
35
|
+
expect(released[0]?.releasedAt).toBe('2026-10-15T00:00:00.000Z');
|
|
36
|
+
expect(heldEntriesOfHome(released, HOME)).toEqual([]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('记两次放手不覆盖第一次的时间(第二次是重放,不是新事实)', () => {
|
|
40
|
+
const once = withReleasedEntry([entry()], HOME, 'opencli-plugin-cmdb', '2026-10-15T00:00:00.000Z');
|
|
41
|
+
const twice = withReleasedEntry(once, HOME, 'opencli-plugin-cmdb', '2026-10-16T00:00:00.000Z');
|
|
42
|
+
|
|
43
|
+
expect(twice[0]?.releasedAt).toBe('2026-10-15T00:00:00.000Z');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('另一个 home 的那一笔不受影响(隔离 wrapper 会换 home)', () => {
|
|
47
|
+
const other = entry({ opencliHome: '/h/global/opencli/home' });
|
|
48
|
+
const released = withReleasedEntry([entry(), other], HOME, 'opencli-plugin-cmdb', '2026-10-15T00:00:00.000Z');
|
|
49
|
+
|
|
50
|
+
expect(released.find((item) => item.opencliHome !== HOME)?.releasedAt).toBeUndefined();
|
|
51
|
+
expect(heldEntriesOfHome(released, other.opencliHome)).toHaveLength(1);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('★ 按插件名找"用户持着的"那一份:插件名与包名两边都认(§3.4 那条映射)', () => {
|
|
55
|
+
const entries = [entry({ name: 'cmdb' })];
|
|
56
|
+
|
|
57
|
+
expect(heldEntryOfPlugin(entries, HOME, 'cmdb')?.package).toBe('opencli-plugin-cmdb');
|
|
58
|
+
expect(heldEntryOfPlugin(entries, HOME, 'opencli-plugin-cmdb')?.name).toBe('cmdb');
|
|
59
|
+
expect(heldEntryOfPlugin(entries, HOME, '别的')).toBeUndefined();
|
|
60
|
+
expect(heldEntryOfPlugin(entries, HOME, ' ')).toBeUndefined();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('放手了的那一笔不再算"用户持着"(模块放手时就可以删它了)', () => {
|
|
64
|
+
const released = withReleasedEntry([entry({ name: 'cmdb' })], HOME, 'opencli-plugin-cmdb', '2026-10-15T00:00:00.000Z');
|
|
65
|
+
|
|
66
|
+
expect(heldEntryOfPlugin(released, HOME, 'cmdb')).toBeUndefined();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('键还是「home + 包名」(同一份文件里两个 home 不互相撒谎)', () => {
|
|
70
|
+
expect(ledgerKeyOf({ opencliHome: '/a', package: 'x' })).not.toBe(
|
|
71
|
+
ledgerKeyOf({ opencliHome: '/b', package: 'x' }),
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('物理去留:由谁主张说话(动作与结果分层那条)', () => {
|
|
77
|
+
it('用户还持着 → 留着(模块放手不动它)', () => {
|
|
78
|
+
expect(pluginRetentionOf({ held: true, claimedBy: [], trustworthy: true })).toBe('keep-held');
|
|
79
|
+
expect(pluginRetentionOf({ held: true, claimedBy: ['x'], trustworthy: false })).toBe('keep-held');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('★ 不是用户持的、引用又不可信 → 一律不删(宁可留一个孤儿,也不删别人的文件)', () => {
|
|
83
|
+
expect(pluginRetentionOf({ held: false, claimedBy: [], trustworthy: false })).toBe('keep-unsure');
|
|
84
|
+
expect(pluginRetentionOf({ held: false, claimedBy: ['x'], trustworthy: false })).toBe('keep-unsure');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('还有模块在用 → 留着(点了卸载,文件也不动)', () => {
|
|
88
|
+
expect(pluginRetentionOf({ held: false, claimedBy: ['issues'], trustworthy: true })).toBe('keep-claimed');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('★ 引用可信 + 没人主张 → 这才是唯一敢删的那一格', () => {
|
|
92
|
+
expect(pluginRetentionOf({ held: false, claimedBy: [], trustworthy: true })).toBe('remove');
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -12,6 +12,8 @@ describe('provision 请求体:每类动作各有各的目标字段', () => {
|
|
|
12
12
|
['remove-bundle', 'name'],
|
|
13
13
|
['cancel-install', 'requestId'],
|
|
14
14
|
['update-dependency', 'id'],
|
|
15
|
+
['update-plugin', 'name'],
|
|
16
|
+
['uninstall-plugin', 'name'],
|
|
15
17
|
]);
|
|
16
18
|
});
|
|
17
19
|
|
|
@@ -55,6 +57,22 @@ describe('provision 请求体:每类动作各有各的目标字段', () => {
|
|
|
55
57
|
});
|
|
56
58
|
});
|
|
57
59
|
|
|
60
|
+
it('用户装的那两笔插件动作按包名认目标(凭台账记的那个包名)', () => {
|
|
61
|
+
expect(parseProvisionBody({ action: 'update-plugin', name: ' opencli-plugin-cmdb ' })).toEqual({
|
|
62
|
+
ok: true,
|
|
63
|
+
request: { action: 'update-plugin', name: 'opencli-plugin-cmdb' },
|
|
64
|
+
});
|
|
65
|
+
expect(parseProvisionBody({ action: 'uninstall-plugin', name: 'opencli-plugin-cmdb' })).toEqual({
|
|
66
|
+
ok: true,
|
|
67
|
+
request: { action: 'uninstall-plugin', name: 'opencli-plugin-cmdb' },
|
|
68
|
+
});
|
|
69
|
+
expect(parseProvisionBody({ action: 'uninstall-plugin', unit: 'panels' })).toEqual({
|
|
70
|
+
ok: false,
|
|
71
|
+
code: 'workbench/no-name',
|
|
72
|
+
message: '缺少 name',
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
58
76
|
it('拿错字段不算数(spec 动作给了 unit 也要打回)', () => {
|
|
59
77
|
expect(parseProvisionBody({ action: 'install-bundle', unit: 'issues' })).toEqual({
|
|
60
78
|
ok: false,
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import type { ModuleStore } from './workspace-store.js';
|
|
2
|
+
|
|
3
|
+
export const PLUGIN_LEDGER_FILE = 'plugins.json';
|
|
4
|
+
|
|
5
|
+
export interface PluginLedgerEntry {
|
|
6
|
+
readonly opencliHome: string;
|
|
7
|
+
readonly package: string;
|
|
8
|
+
readonly name?: string;
|
|
9
|
+
readonly registry?: string;
|
|
10
|
+
readonly local?: boolean;
|
|
11
|
+
readonly installedAt: string;
|
|
12
|
+
readonly releasedAt?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function ledgerKeyOf(entry: {
|
|
16
|
+
readonly opencliHome: string;
|
|
17
|
+
readonly package: string;
|
|
18
|
+
}): string {
|
|
19
|
+
return `${entry.opencliHome}\u0000${entry.package}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function textOf(value: unknown): string | undefined {
|
|
23
|
+
if (typeof value !== 'string') return undefined;
|
|
24
|
+
const trimmed = value.trim();
|
|
25
|
+
return trimmed === '' ? undefined : trimmed;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function entryOf(value: unknown): PluginLedgerEntry | undefined {
|
|
29
|
+
if (typeof value !== 'object' || value === null) return undefined;
|
|
30
|
+
const box = value as Readonly<Record<string, unknown>>;
|
|
31
|
+
const opencliHome = textOf(box['opencliHome']);
|
|
32
|
+
const pkg = textOf(box['package']);
|
|
33
|
+
const installedAt = textOf(box['installedAt']);
|
|
34
|
+
if (opencliHome === undefined || pkg === undefined || installedAt === undefined) return undefined;
|
|
35
|
+
const name = textOf(box['name']);
|
|
36
|
+
const registry = textOf(box['registry']);
|
|
37
|
+
const releasedAt = textOf(box['releasedAt']);
|
|
38
|
+
return {
|
|
39
|
+
opencliHome,
|
|
40
|
+
package: pkg,
|
|
41
|
+
installedAt,
|
|
42
|
+
...(name === undefined ? {} : { name }),
|
|
43
|
+
...(registry === undefined ? {} : { registry }),
|
|
44
|
+
...(releasedAt === undefined ? {} : { releasedAt }),
|
|
45
|
+
...(box['local'] === true ? { local: true } : {}),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function parsePluginLedger(raw: unknown): PluginLedgerEntry[] {
|
|
50
|
+
if (typeof raw !== 'object' || raw === null) return [];
|
|
51
|
+
const list = (raw as Readonly<Record<string, unknown>>)['entries'];
|
|
52
|
+
if (!Array.isArray(list)) return [];
|
|
53
|
+
const entries: PluginLedgerEntry[] = [];
|
|
54
|
+
for (const item of list) {
|
|
55
|
+
const entry = entryOf(item);
|
|
56
|
+
if (entry === undefined) continue;
|
|
57
|
+
if (entries.some((known) => ledgerKeyOf(known) === ledgerKeyOf(entry))) continue;
|
|
58
|
+
entries.push(entry);
|
|
59
|
+
}
|
|
60
|
+
return entries;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function readPluginLedger(store: ModuleStore): PluginLedgerEntry[] {
|
|
64
|
+
return parsePluginLedger(store.read(PLUGIN_LEDGER_FILE));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function writePluginLedger(store: ModuleStore, entries: readonly PluginLedgerEntry[]): void {
|
|
68
|
+
if (entries.length === 0) {
|
|
69
|
+
store.remove(PLUGIN_LEDGER_FILE);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
store.write(PLUGIN_LEDGER_FILE, { entries });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function pluginEntriesOfHome(
|
|
76
|
+
entries: readonly PluginLedgerEntry[],
|
|
77
|
+
opencliHome: string,
|
|
78
|
+
): PluginLedgerEntry[] {
|
|
79
|
+
return entries.filter((entry) => entry.opencliHome === opencliHome);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function isHeldEntry(entry: PluginLedgerEntry): boolean {
|
|
83
|
+
return entry.releasedAt === undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function heldEntriesOfHome(
|
|
87
|
+
entries: readonly PluginLedgerEntry[],
|
|
88
|
+
opencliHome: string,
|
|
89
|
+
): PluginLedgerEntry[] {
|
|
90
|
+
return pluginEntriesOfHome(entries, opencliHome).filter(isHeldEntry);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function heldEntryOfPlugin(
|
|
94
|
+
entries: readonly PluginLedgerEntry[],
|
|
95
|
+
opencliHome: string,
|
|
96
|
+
plugin: string,
|
|
97
|
+
): PluginLedgerEntry | undefined {
|
|
98
|
+
const key = plugin.trim();
|
|
99
|
+
if (key === '') return undefined;
|
|
100
|
+
return heldEntriesOfHome(entries, opencliHome).find(
|
|
101
|
+
(entry) => entry.name === key || entry.package === key,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function withReleasedEntry(
|
|
106
|
+
entries: readonly PluginLedgerEntry[],
|
|
107
|
+
opencliHome: string,
|
|
108
|
+
pkg: string,
|
|
109
|
+
releasedAt: string,
|
|
110
|
+
): PluginLedgerEntry[] {
|
|
111
|
+
const entry = pluginEntryOf(entries, opencliHome, pkg);
|
|
112
|
+
if (entry === undefined || entry.releasedAt !== undefined) return [...entries];
|
|
113
|
+
return withPluginEntry(entries, { ...entry, releasedAt });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export type PluginRetention = 'remove' | 'keep-held' | 'keep-claimed' | 'keep-unsure';
|
|
117
|
+
|
|
118
|
+
export function pluginRetentionOf(options: {
|
|
119
|
+
readonly held: boolean;
|
|
120
|
+
readonly claimedBy: readonly string[];
|
|
121
|
+
readonly trustworthy: boolean;
|
|
122
|
+
}): PluginRetention {
|
|
123
|
+
if (options.held) return 'keep-held';
|
|
124
|
+
if (!options.trustworthy) return 'keep-unsure';
|
|
125
|
+
return options.claimedBy.length > 0 ? 'keep-claimed' : 'remove';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function pluginEntryOf(
|
|
129
|
+
entries: readonly PluginLedgerEntry[],
|
|
130
|
+
opencliHome: string,
|
|
131
|
+
pkg: string,
|
|
132
|
+
): PluginLedgerEntry | undefined {
|
|
133
|
+
const key = ledgerKeyOf({ opencliHome, package: pkg });
|
|
134
|
+
return entries.find((entry) => ledgerKeyOf(entry) === key);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function withPluginEntry(
|
|
138
|
+
entries: readonly PluginLedgerEntry[],
|
|
139
|
+
entry: PluginLedgerEntry,
|
|
140
|
+
): PluginLedgerEntry[] {
|
|
141
|
+
const key = ledgerKeyOf(entry);
|
|
142
|
+
return [...entries.filter((item) => ledgerKeyOf(item) !== key), entry];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function withoutPluginEntry(
|
|
146
|
+
entries: readonly PluginLedgerEntry[],
|
|
147
|
+
opencliHome: string,
|
|
148
|
+
pkg: string,
|
|
149
|
+
): PluginLedgerEntry[] {
|
|
150
|
+
const key = ledgerKeyOf({ opencliHome, package: pkg });
|
|
151
|
+
return entries.filter((entry) => ledgerKeyOf(entry) !== key);
|
|
152
|
+
}
|
package/src/host/provision.ts
CHANGED
|
@@ -11,6 +11,8 @@ export const PROVISION_ACTIONS = [
|
|
|
11
11
|
'remove-bundle',
|
|
12
12
|
'cancel-install',
|
|
13
13
|
'update-dependency',
|
|
14
|
+
'update-plugin',
|
|
15
|
+
'uninstall-plugin',
|
|
14
16
|
] as const satisfies readonly ProvisionAction[];
|
|
15
17
|
|
|
16
18
|
export const DEPENDENCY_ACTIONS = ['update-dependency'] as const satisfies readonly ProvisionAction[];
|
|
@@ -32,6 +34,7 @@ export function provisionTargetOf(action: ProvisionAction): ProvisionTarget {
|
|
|
32
34
|
if (action === 'update-dependency') return 'id';
|
|
33
35
|
if (action === 'install-bundle') return 'spec';
|
|
34
36
|
if (action === 'remove-bundle') return 'name';
|
|
37
|
+
if (action === 'update-plugin' || action === 'uninstall-plugin') return 'name';
|
|
35
38
|
if (action === 'cancel-install') return 'requestId';
|
|
36
39
|
return 'unit';
|
|
37
40
|
}
|
|
@@ -74,6 +77,7 @@ function requestOf(action: ProvisionAction, value: string): ProvisionRequest {
|
|
|
74
77
|
if (action === 'update-dependency') return { action, id: value };
|
|
75
78
|
if (action === 'install-bundle') return { action, spec: value };
|
|
76
79
|
if (action === 'remove-bundle') return { action, name: value };
|
|
80
|
+
if (action === 'update-plugin' || action === 'uninstall-plugin') return { action, name: value };
|
|
77
81
|
if (action === 'cancel-install') return { action, requestId: value };
|
|
78
82
|
return { action, unit: value };
|
|
79
83
|
}
|
package/src/host/unit-face.ts
CHANGED
|
@@ -9,9 +9,11 @@ import type { ValueHandoff } from '../host/value-handoff.js';
|
|
|
9
9
|
import type { WorkspaceStore } from '../host/workspace-store.js';
|
|
10
10
|
|
|
11
11
|
export interface UnitMountDeps {
|
|
12
|
+
readonly manifest: UnitManifest;
|
|
12
13
|
readonly ctx: DshHostContext;
|
|
13
14
|
readonly logger?: { info?(message: string): void; warn?(message: string): void } | undefined;
|
|
14
15
|
readonly manifests: () => readonly UnitManifest[];
|
|
16
|
+
readonly ownedUnits: () => readonly string[];
|
|
15
17
|
readonly opencliCommand?: string;
|
|
16
18
|
readonly store: WorkspaceStore;
|
|
17
19
|
readonly values?: ValueHandoff | undefined;
|
|
@@ -60,7 +62,11 @@ export interface UnitMountResult {
|
|
|
60
62
|
readonly needsApproval?: ApprovalPolicy | undefined;
|
|
61
63
|
}
|
|
62
64
|
|
|
63
|
-
export interface
|
|
64
|
-
readonly
|
|
65
|
+
export interface UnitModuleFace {
|
|
66
|
+
readonly unit: string;
|
|
65
67
|
mount(deps: UnitMountDeps): UnitMountResult;
|
|
66
68
|
}
|
|
69
|
+
|
|
70
|
+
export interface UnitOwnedFace extends UnitModuleFace {
|
|
71
|
+
readonly manifest: UnitManifest;
|
|
72
|
+
}
|
package/src/host.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from './config/constants.js';
|
|
2
2
|
export * from './api/context-ref.js';
|
|
3
3
|
export * from './api/unit.js';
|
|
4
|
+
export * from './api/unit-declaration.js';
|
|
4
5
|
export * from './api/facts.js';
|
|
5
6
|
export * from './api/long-task.js';
|
|
6
7
|
export * from './lib/text.js';
|
|
@@ -16,6 +17,7 @@ export * from './host/http.js';
|
|
|
16
17
|
export * from './host/long-task.js';
|
|
17
18
|
export * from './host/opencli-runtime.js';
|
|
18
19
|
export * from './host/opencli.js';
|
|
20
|
+
export * from './host/plugin-ledger.js';
|
|
19
21
|
export * from './host/plugin-source.js';
|
|
20
22
|
export * from './host/provision.js';
|
|
21
23
|
export * from './host/shell.js';
|
package/src/i18n/en.ts
CHANGED
|
@@ -70,7 +70,7 @@ export const en: Record<keyof WorkbenchMessages, string> = {
|
|
|
70
70
|
'panels.market.missing': 'not installed',
|
|
71
71
|
'panels.market.unchecked': 'unknown',
|
|
72
72
|
'panels.market.checking': 'checking',
|
|
73
|
-
'panels.market.sectionInstalled': 'Installed',
|
|
73
|
+
'panels.market.sectionInstalled': 'Installed modules',
|
|
74
74
|
'panels.market.sectionPending': 'Not installed',
|
|
75
75
|
'panels.market.sectionShared': 'Shared dependencies',
|
|
76
76
|
'panels.market.sharedUsedBy': 'Used by {modules}',
|
|
@@ -100,11 +100,19 @@ export const en: Record<keyof WorkbenchMessages, string> = {
|
|
|
100
100
|
'panels.market.noSource': 'This module declares no install method — install it yourself',
|
|
101
101
|
'panels.market.badSource': 'The install method is incomplete — install it yourself',
|
|
102
102
|
'panels.market.empty': 'No modules available',
|
|
103
|
-
'panels.market.
|
|
104
|
-
'panels.market.sectionLocal': 'Linked locally',
|
|
103
|
+
'panels.market.sectionLocal': 'Local modules',
|
|
105
104
|
'panels.market.sectionUnlisted': 'Not from the market',
|
|
105
|
+
'panels.market.sectionPlugins': 'Installed opencli plugins',
|
|
106
106
|
'panels.market.moduleOutdated': 'update available',
|
|
107
|
+
'panels.market.moduleRefused': 'This module was built for an older version of the workbench, so it does not work here. Update it, or uninstall.',
|
|
108
|
+
'panels.market.moduleRefusedDetail': 'Why it cannot be used: {reason}',
|
|
107
109
|
'panels.market.moduleRestartHint': 'installed but not active — restart dsh once to bring it up',
|
|
110
|
+
'panels.market.pluginKeptInUse': 'Files kept for now: {names} still uses it.',
|
|
111
|
+
'panels.market.pluginKeptUnsure': 'Files kept for now: some modules on this machine do not say what they use, so it is unclear who needs it.',
|
|
112
|
+
'panels.market.pluginKeptHeld': 'Files kept: this one is yours, and a module letting go will not remove it.',
|
|
113
|
+
'panels.market.pluginKeptUnnamed': 'Files kept for now: this one cannot be identified as a plugin, so it is unclear who needs it.',
|
|
114
|
+
'panels.market.pluginAlsoKnownAs': 'Its package is {pkg} (updates and uninstall use that)',
|
|
115
|
+
'panels.market.pluginBroken': 'This install is broken: it cannot be identified as a plugin. Uninstall it, or install it again by package name above.',
|
|
108
116
|
'panels.market.restartNeeded': 'The new version is installed — restart once to pick it up.',
|
|
109
117
|
'panels.market.foot': 'After installing, enable it in the panel page of that project.',
|
|
110
118
|
'panels.market.recheck': 'Check for updates',
|
|
@@ -152,7 +160,12 @@ export const en: Record<keyof WorkbenchMessages, string> = {
|
|
|
152
160
|
'panels.market.add.problemInstalled': 'This module is already installed',
|
|
153
161
|
'panels.market.add.problemNotFound': 'No such module found',
|
|
154
162
|
'panels.market.add.problemNotPackage': 'That path does not exist or is not a valid module package',
|
|
155
|
-
'panels.market.add.problemNotBundle':
|
|
163
|
+
'panels.market.add.problemNotBundle':
|
|
164
|
+
'This package cannot be installed here: {reason}. If it is actually an opencli plugin, use its package name (for example opencli-plugin-cmdb) or a local folder — a git URL or tarball does not reveal what it is.',
|
|
165
|
+
'panels.market.add.problemNotWorkbenchModule': 'This package does not match the workbench you are running, so installing it would not work. Pick another version, or ask the module author.',
|
|
166
|
+
'panels.market.add.problemNotModuleOrPlugin': 'This package is neither a workbench module nor an opencli plugin, so installing it would do nothing. Pick another version, or ask the package author.',
|
|
167
|
+
'panels.market.add.failureNotModuleOrPlugin': 'This package is neither a workbench module nor an opencli plugin, so installing it would do nothing',
|
|
168
|
+
'panels.market.add.installedPlugin': 'Installed: the opencli plugin {name}',
|
|
156
169
|
'panels.market.add.problemNetwork': 'Cannot reach the module source; check the network and retry',
|
|
157
170
|
'panels.market.add.problemUnknown': 'Cannot fetch module information: {reason}',
|
|
158
171
|
'panels.market.add.failureNetwork': 'The network connection failed',
|
|
@@ -183,6 +196,7 @@ export const en: Record<keyof WorkbenchMessages, string> = {
|
|
|
183
196
|
'panels.market.add.registryPnpm': "pnpm's own registry",
|
|
184
197
|
'panels.market.add.check': 'Check',
|
|
185
198
|
'panels.market.add.checking': 'Checking…',
|
|
199
|
+
'panels.market.add.classifying': 'Working out what it is…',
|
|
186
200
|
'panels.market.add.change': 'Edit',
|
|
187
201
|
'panels.market.add.previewTitle': 'About to install',
|
|
188
202
|
'panels.market.add.fieldName': 'Package',
|
package/src/i18n/zh.ts
CHANGED
|
@@ -68,7 +68,7 @@ export const zh = {
|
|
|
68
68
|
'panels.market.missing': '未装',
|
|
69
69
|
'panels.market.unchecked': '状态未知',
|
|
70
70
|
'panels.market.checking': '检测中',
|
|
71
|
-
'panels.market.sectionInstalled': '
|
|
71
|
+
'panels.market.sectionInstalled': '已安装模块',
|
|
72
72
|
'panels.market.sectionPending': '未安装',
|
|
73
73
|
'panels.market.sectionShared': '共用依赖',
|
|
74
74
|
'panels.market.sharedUsedBy': '供 {modules} 使用',
|
|
@@ -96,11 +96,19 @@ export const zh = {
|
|
|
96
96
|
'panels.market.noSource': '这个模块没声明怎么装,只能自己动手',
|
|
97
97
|
'panels.market.badSource': '安装方式没写全,只能自己动手',
|
|
98
98
|
'panels.market.empty': '暂无可用模块',
|
|
99
|
-
'panels.market.
|
|
100
|
-
'panels.market.sectionLocal': '本地装的',
|
|
99
|
+
'panels.market.sectionLocal': '本地模块',
|
|
101
100
|
'panels.market.sectionUnlisted': '不是从市场装的',
|
|
101
|
+
'panels.market.sectionPlugins': '已安装 opencli 插件',
|
|
102
102
|
'panels.market.moduleOutdated': '有新版',
|
|
103
|
+
'panels.market.moduleRefused': '这个模块是给旧版本的工作台写的,现在用不了。更新它,或者卸载。',
|
|
104
|
+
'panels.market.moduleRefusedDetail': '装不上的原因:{reason}',
|
|
103
105
|
'panels.market.moduleRestartHint': '装好了但还没生效 —— 重启一次 dsh 就会出来',
|
|
106
|
+
'panels.market.pluginKeptInUse': '文件先留着:{names} 还要用它。',
|
|
107
|
+
'panels.market.pluginKeptUnsure': '文件先留着:这台机器上还有模块看不出用了什么,说不准谁在用它。',
|
|
108
|
+
'panels.market.pluginKeptHeld': '文件留着:你自己装的那一份,模块放手不动它。',
|
|
109
|
+
'panels.market.pluginKeptUnnamed': '文件先留着:看不出这是哪个插件,说不准谁在用它。',
|
|
110
|
+
'panels.market.pluginAlsoKnownAs': '包名是 {pkg}(更新与卸载按它来)',
|
|
111
|
+
'panels.market.pluginBroken': '这份装坏了:看不出它是个插件。可以卸载,或者去上面按包名再装一次。',
|
|
104
112
|
'panels.market.restartNeeded': '新版装好了,重启一次才生效。',
|
|
105
113
|
'panels.market.foot': '装好后到那个项目的面板页勾选它,模块才会出现。',
|
|
106
114
|
'panels.market.recheck': '重新检查更新',
|
|
@@ -148,7 +156,12 @@ export const zh = {
|
|
|
148
156
|
'panels.market.add.problemInstalled': '该模块已安装',
|
|
149
157
|
'panels.market.add.problemNotFound': '未找到相关模块',
|
|
150
158
|
'panels.market.add.problemNotPackage': '该路径不存在或不是有效的模块包',
|
|
151
|
-
'panels.market.add.problemNotBundle':
|
|
159
|
+
'panels.market.add.problemNotBundle':
|
|
160
|
+
'这个包装不进这里:{reason}。要是它其实是个 opencli 插件,请改用包名(例如 opencli-plugin-cmdb)或者本地目录 —— 从 git 地址和 tarball 判断不出它是什么。',
|
|
161
|
+
'panels.market.add.problemNotWorkbenchModule': '这个包跟现在的工作台对不上,装上也用不了。换一个版本,或者找这个模块的作者。',
|
|
162
|
+
'panels.market.add.problemNotModuleOrPlugin': '这个包既不是工作台的模块,也不是 opencli 的插件,装了也用不上。换一个版本,或者找这个包的作者。',
|
|
163
|
+
'panels.market.add.failureNotModuleOrPlugin': '这个包既不是工作台的模块,也不是 opencli 的插件,装了也用不上',
|
|
164
|
+
'panels.market.add.installedPlugin': '装上了:opencli 插件 {name}',
|
|
152
165
|
'panels.market.add.problemNetwork': '无法连接模块源,请检查网络后重试',
|
|
153
166
|
'panels.market.add.problemUnknown': '无法获取模块信息:{reason}',
|
|
154
167
|
'panels.market.add.failureNetwork': '网络连接失败',
|
|
@@ -179,6 +192,7 @@ export const zh = {
|
|
|
179
192
|
'panels.market.add.registryPnpm': 'pnpm 自己配的源',
|
|
180
193
|
'panels.market.add.check': '预检',
|
|
181
194
|
'panels.market.add.checking': '正在预检…',
|
|
195
|
+
'panels.market.add.classifying': '判定中…',
|
|
182
196
|
'panels.market.add.change': '改一下',
|
|
183
197
|
'panels.market.add.previewTitle': '将要安装',
|
|
184
198
|
'panels.market.add.fieldName': '包名',
|
package/src/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ export * from './api/host-write.js';
|
|
|
8
8
|
export * from './api/long-task.js';
|
|
9
9
|
export * from './api/unit-board.js';
|
|
10
10
|
export * from './api/unit.js';
|
|
11
|
+
export * from './api/unit-declaration.js';
|
|
11
12
|
export * from './api/workbench-client.js';
|
|
12
13
|
export * from './api/workbench-contract.js';
|
|
13
14
|
export * from './components/Activity.js';
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { declarationOfPackage } from '../declaration.js';
|
|
6
|
+
|
|
7
|
+
const roots: string[] = [];
|
|
8
|
+
|
|
9
|
+
function packageAt(manifest: unknown, text?: string): string {
|
|
10
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'wb-declaration-'));
|
|
11
|
+
roots.push(root);
|
|
12
|
+
const at = path.join(root, 'package.json');
|
|
13
|
+
fs.writeFileSync(at, text ?? JSON.stringify({ name: '@t/demo', dsh: { workbench: manifest } }), 'utf8');
|
|
14
|
+
return at;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
for (const root of roots.splice(0)) fs.rmSync(root, { recursive: true, force: true });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const DECLARED = {
|
|
22
|
+
apiVersion: 1,
|
|
23
|
+
unit: 'demo',
|
|
24
|
+
card: { titleKey: 'demo.cardTitle', order: 7 },
|
|
25
|
+
tools: [{ name: 'workbench_demo_list', writes: false }],
|
|
26
|
+
host: { inject: [] },
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
describe('模块作者写用例时读自己那份声明', () => {
|
|
30
|
+
it('读出来的就是外壳会拿到的那份清单', () => {
|
|
31
|
+
const manifest = declarationOfPackage('@t/demo', packageAt(DECLARED));
|
|
32
|
+
|
|
33
|
+
expect(manifest.id).toBe('demo');
|
|
34
|
+
expect(manifest.card).toEqual({ titleKey: 'demo.cardTitle', order: 7 });
|
|
35
|
+
expect(manifest.tools).toEqual([{ name: 'workbench_demo_list', writes: false }]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('★ 声明有毛病就当场抛,并把 problems 原样带出来(别让用例拿着半个清单继续跑)', () => {
|
|
39
|
+
expect(() => declarationOfPackage('@t/demo', packageAt({ unit: 'demo' }))).toThrow(/apiVersion/);
|
|
40
|
+
expect(() =>
|
|
41
|
+
declarationOfPackage('@t/demo', packageAt({ ...DECLARED, faces: { host: 'src/host.ts' } })),
|
|
42
|
+
).toThrow(/faces/);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('package.json 本身读不动时也说清是哪个文件(不是一句 JSON 语法错)', () => {
|
|
46
|
+
expect(() => declarationOfPackage('@t/demo', packageAt({}, '{ 坏掉的'))).toThrow(/读不动/);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import type { UnitManifest } from '../api/unit.js';
|
|
4
|
+
import { readDeclaration } from '../api/unit-declaration.js';
|
|
5
|
+
|
|
6
|
+
export function declarationOfPackage(pkg: string, packageJson: URL | string): UnitManifest {
|
|
7
|
+
const at = typeof packageJson === 'string' ? packageJson : fileURLToPath(packageJson);
|
|
8
|
+
let parsed: unknown;
|
|
9
|
+
try {
|
|
10
|
+
parsed = JSON.parse(fs.readFileSync(at, 'utf8'));
|
|
11
|
+
} catch (error) {
|
|
12
|
+
throw new Error(`${pkg}: 读不动 ${at} —— ${String(error)}`, { cause: error });
|
|
13
|
+
}
|
|
14
|
+
const declared =
|
|
15
|
+
typeof parsed === 'object' && parsed !== null
|
|
16
|
+
? (parsed as { dsh?: { workbench?: unknown } }).dsh?.workbench
|
|
17
|
+
: undefined;
|
|
18
|
+
const read = readDeclaration(pkg, declared);
|
|
19
|
+
if (read.manifest === undefined) {
|
|
20
|
+
throw new Error(`${pkg} 的 dsh.workbench 读不出来:${read.problems.join(';')}`);
|
|
21
|
+
}
|
|
22
|
+
return read.manifest;
|
|
23
|
+
}
|
package/src/testing.ts
CHANGED