@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.
Files changed (42) hide show
  1. package/dist/api/unit-declaration.d.ts +29 -0
  2. package/dist/api/unit-declaration.js +189 -0
  3. package/dist/api/unit.d.ts +0 -2
  4. package/dist/api/workbench-contract.d.ts +1 -1
  5. package/dist/components/ApprovalDetail.js +1 -1
  6. package/dist/components/board-module.d.ts +4 -2
  7. package/dist/host/plugin-ledger.d.ts +32 -0
  8. package/dist/host/plugin-ledger.js +99 -0
  9. package/dist/host/provision.d.ts +1 -1
  10. package/dist/host/provision.js +6 -0
  11. package/dist/host/unit-face.d.ts +7 -2
  12. package/dist/host.d.ts +2 -0
  13. package/dist/host.js +2 -0
  14. package/dist/i18n/en.js +17 -4
  15. package/dist/i18n/index.d.ts +18 -5
  16. package/dist/i18n/zh.d.ts +17 -4
  17. package/dist/i18n/zh.js +17 -4
  18. package/dist/index.d.ts +1 -0
  19. package/dist/index.js +1 -0
  20. package/dist/testing/declaration.d.ts +2 -0
  21. package/dist/testing/declaration.js +21 -0
  22. package/dist/testing.d.ts +1 -0
  23. package/dist/testing.js +1 -0
  24. package/package.json +1 -1
  25. package/src/api/__tests__/unit-declaration.test.ts +52 -0
  26. package/src/api/unit-declaration.ts +244 -0
  27. package/src/api/unit.ts +0 -2
  28. package/src/api/workbench-contract.ts +3 -1
  29. package/src/components/ApprovalDetail.tsx +1 -1
  30. package/src/components/board-module.ts +5 -2
  31. package/src/host/__tests__/plugin-ledger.test.ts +94 -0
  32. package/src/host/__tests__/provision.test.ts +18 -0
  33. package/src/host/plugin-ledger.ts +152 -0
  34. package/src/host/provision.ts +4 -0
  35. package/src/host/unit-face.ts +8 -2
  36. package/src/host.ts +2 -0
  37. package/src/i18n/en.ts +18 -4
  38. package/src/i18n/zh.ts +18 -4
  39. package/src/index.ts +1 -0
  40. package/src/testing/__tests__/declaration.test.ts +48 -0
  41. package/src/testing/declaration.ts +23 -0
  42. package/src/testing.ts +1 -0
@@ -0,0 +1,29 @@
1
+ import type { UnitEnvNeeds, UnitHostNeeds, UnitManifest } from './unit.js';
2
+ export declare const WORKBENCH_API_VERSION = 1;
3
+ export declare const SUPPORTED_API_VERSIONS: readonly number[];
4
+ export interface UnitCardDeclaration {
5
+ readonly titleKey: string;
6
+ readonly order: number;
7
+ }
8
+ export interface UnitToolDeclaration {
9
+ readonly name: string;
10
+ readonly writes: boolean;
11
+ readonly key?: string;
12
+ }
13
+ export interface UnitDeclaration {
14
+ readonly apiVersion: number;
15
+ readonly unit: string;
16
+ readonly card: UnitCardDeclaration | null;
17
+ readonly tools: readonly UnitToolDeclaration[];
18
+ readonly host: UnitHostNeeds;
19
+ readonly env?: UnitEnvNeeds;
20
+ }
21
+ export declare const DECLARATION_KEYS: readonly string[];
22
+ export declare const DECLARATION_REQUIRED_KEYS: readonly string[];
23
+ export declare function declarationProblemsOf(subject: string, raw: unknown): readonly string[];
24
+ export interface DeclarationRead {
25
+ readonly declaration?: UnitDeclaration;
26
+ readonly manifest?: UnitManifest;
27
+ readonly problems: readonly string[];
28
+ }
29
+ export declare function readDeclaration(subject: string, raw: unknown): DeclarationRead;
@@ -0,0 +1,189 @@
1
+ export const WORKBENCH_API_VERSION = 1;
2
+ export const SUPPORTED_API_VERSIONS = [WORKBENCH_API_VERSION];
3
+ export const DECLARATION_KEYS = [
4
+ 'apiVersion',
5
+ 'unit',
6
+ 'card',
7
+ 'tools',
8
+ 'host',
9
+ 'env',
10
+ ];
11
+ export const DECLARATION_REQUIRED_KEYS = [
12
+ 'apiVersion',
13
+ 'unit',
14
+ 'card',
15
+ 'tools',
16
+ 'host',
17
+ ];
18
+ const CARD_KEYS = ['titleKey', 'order'];
19
+ const TOOL_KEYS = ['name', 'writes', 'key'];
20
+ const TOOL_KEY_SHAPE = /^[a-z][a-zA-Z0-9]*$/;
21
+ const HOST_KEYS = ['inject', 'optional'];
22
+ function text(value) {
23
+ return typeof value === 'string' ? value.trim() : '';
24
+ }
25
+ function isRecord(value) {
26
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
27
+ }
28
+ function outsideKeys(value, allowed) {
29
+ return Object.keys(value).filter((key) => !allowed.includes(key));
30
+ }
31
+ function listOf(keys) {
32
+ return keys.map((key) => JSON.stringify(key)).join(' / ');
33
+ }
34
+ function stringArrayProblems(at, value) {
35
+ if (!Array.isArray(value))
36
+ return [`${at} 要是一串字符串,现在是 ${JSON.stringify(value)}`];
37
+ return value.flatMap((item, index) => text(item) === '' ? [`${at}[${index}] 不是非空字符串:${JSON.stringify(item)}`] : []);
38
+ }
39
+ export function declarationProblemsOf(subject, raw) {
40
+ if (!isRecord(raw))
41
+ return [`${subject}: dsh.workbench 不是一个对象 —— ${JSON.stringify(raw)}`];
42
+ const version = raw.apiVersion;
43
+ if (version === undefined) {
44
+ return [
45
+ `${subject}: dsh.workbench 没有 apiVersion —— 协议从 v${WORKBENCH_API_VERSION} 起,` +
46
+ `写 \`"apiVersion": ${WORKBENCH_API_VERSION}\`。缺它 = 外壳不认这个模块(ADR-0038 §2.3)`,
47
+ ];
48
+ }
49
+ if (!Number.isInteger(version) || !SUPPORTED_API_VERSIONS.includes(version)) {
50
+ return [
51
+ `${subject}: dsh.workbench.apiVersion 是 ${JSON.stringify(version)},这里只认 ` +
52
+ `${SUPPORTED_API_VERSIONS.join(' / ')}(要整数)`,
53
+ ];
54
+ }
55
+ const problems = [];
56
+ const unknown = outsideKeys(raw, DECLARATION_KEYS);
57
+ if (unknown.length > 0) {
58
+ problems.push(`${subject}: dsh.workbench 里有协议 v${WORKBENCH_API_VERSION} 不认识的键 ${listOf(unknown)} —— ` +
59
+ `只有 ${listOf(DECLARATION_KEYS)}。(拼错的键会被外壳**静默忽略**:那句声明等于没写)`);
60
+ }
61
+ for (const key of DECLARATION_REQUIRED_KEYS) {
62
+ if (raw[key] === undefined) {
63
+ problems.push(`${subject}: dsh.workbench 缺 ${key}` +
64
+ (key === 'card' ? '(这个模块没有卡片就写 `"card": null`)' : ''));
65
+ }
66
+ }
67
+ const card = raw.card;
68
+ if (card !== undefined && card !== null) {
69
+ if (!isRecord(card)) {
70
+ problems.push(`${subject}: dsh.workbench.card 要么是对象、要么是 null,现在是 ${JSON.stringify(card)}`);
71
+ }
72
+ else {
73
+ const extra = outsideKeys(card, CARD_KEYS);
74
+ if (extra.length > 0) {
75
+ problems.push(`${subject}: dsh.workbench.card 里有不认的键 ${listOf(extra)} —— card 只有 ${listOf(CARD_KEYS)}` +
76
+ `(卡片默认显不显示由运行期的面板配置决定,清单里没有这一位)`);
77
+ }
78
+ if (text(card.titleKey) === '') {
79
+ problems.push(`${subject}: dsh.workbench.card.titleKey 要是非空字符串(面板那一格的文案键)`);
80
+ }
81
+ if (typeof card.order !== 'number' || !Number.isFinite(card.order)) {
82
+ problems.push(`${subject}: dsh.workbench.card.order 要是数字,现在是 ${JSON.stringify(card.order)}`);
83
+ }
84
+ }
85
+ }
86
+ const tools = raw.tools;
87
+ if (tools !== undefined) {
88
+ if (!Array.isArray(tools)) {
89
+ problems.push(`${subject}: dsh.workbench.tools 要是一串声明,现在是 ${JSON.stringify(tools)}`);
90
+ }
91
+ else {
92
+ const seen = new Set();
93
+ const seenKeys = new Set();
94
+ tools.forEach((tool, index) => {
95
+ const at = `dsh.workbench.tools[${index}]`;
96
+ if (!isRecord(tool)) {
97
+ problems.push(`${subject}: ${at} 要是对象,现在是 ${JSON.stringify(tool)}`);
98
+ return;
99
+ }
100
+ const extra = outsideKeys(tool, TOOL_KEYS);
101
+ if (extra.length > 0) {
102
+ problems.push(`${subject}: ${at} 里有不认的键 ${listOf(extra)} —— tools 只有 ${listOf(TOOL_KEYS)}`);
103
+ }
104
+ const name = text(tool.name);
105
+ if (name === '') {
106
+ problems.push(`${subject}: ${at}.name 要是非空字符串`);
107
+ }
108
+ else if (seen.has(name)) {
109
+ problems.push(`${subject}: ${at}.name「${name}」与前面那条撞了 —— 工具名会进 dsh 的工具表,不许重`);
110
+ }
111
+ else {
112
+ seen.add(name);
113
+ }
114
+ if (typeof tool.writes !== 'boolean') {
115
+ problems.push(`${subject}: ${at}.writes 要是 true / false(它是"这会写"的唯一声明,缺了审批无从认领)`);
116
+ }
117
+ if (tool.key !== undefined) {
118
+ const key = text(tool.key);
119
+ if (!TOOL_KEY_SHAPE.test(key)) {
120
+ problems.push(`${subject}: ${at}.key 要是 lowerCamelCase 的标识符(如 \`sync\` / \`podLogs\`),` +
121
+ `现在是 ${JSON.stringify(tool.key)} —— 它会变成生成物里的键名,所以不引号、不许带点或横线`);
122
+ }
123
+ else if (seenKeys.has(key)) {
124
+ problems.push(`${subject}: ${at}.key「${key}」与前面那条撞了 —— 生成物里的键只能有一个`);
125
+ }
126
+ else {
127
+ seenKeys.add(key);
128
+ }
129
+ }
130
+ });
131
+ }
132
+ }
133
+ const host = raw.host;
134
+ if (host !== undefined) {
135
+ if (!isRecord(host)) {
136
+ problems.push(`${subject}: dsh.workbench.host 要是对象,现在是 ${JSON.stringify(host)}`);
137
+ }
138
+ else {
139
+ const extra = outsideKeys(host, HOST_KEYS);
140
+ if (extra.length > 0) {
141
+ problems.push(`${subject}: dsh.workbench.host 里有不认的键 ${listOf(extra)} —— host 只有 ${listOf(HOST_KEYS)}`);
142
+ }
143
+ if (host.inject === undefined) {
144
+ problems.push(`${subject}: dsh.workbench.host.inject 没写 —— 它声明这个模块要外壳的哪几个服务,` +
145
+ `外壳靠它判"能不能挂"。一个都不要也要写 \`"inject": []\``);
146
+ }
147
+ else {
148
+ problems.push(...stringArrayProblems(`${subject}: dsh.workbench.host.inject`, host.inject));
149
+ }
150
+ if (host.optional !== undefined) {
151
+ problems.push(...stringArrayProblems(`${subject}: dsh.workbench.host.optional`, host.optional));
152
+ }
153
+ }
154
+ }
155
+ if (raw.env !== undefined && !isRecord(raw.env)) {
156
+ problems.push(`${subject}: dsh.workbench.env 要是对象(command / plugins / libs)—— ` +
157
+ `语义由门禁的 env 判据查,这里只查形状`);
158
+ }
159
+ return problems;
160
+ }
161
+ export function readDeclaration(subject, raw) {
162
+ const problems = declarationProblemsOf(subject, raw);
163
+ if (problems.length > 0)
164
+ return { problems };
165
+ const declaration = raw;
166
+ const card = declaration.card;
167
+ return {
168
+ declaration,
169
+ problems: [],
170
+ manifest: {
171
+ id: text(declaration.unit),
172
+ card: card === null
173
+ ? null
174
+ : {
175
+ titleKey: text(card.titleKey),
176
+ order: card.order,
177
+ },
178
+ tools: declaration.tools.map((tool) => ({
179
+ name: text(tool.name),
180
+ writes: tool.writes,
181
+ })),
182
+ host: {
183
+ inject: [...(declaration.host.inject ?? [])],
184
+ ...(declaration.host.optional === undefined ? {} : { optional: [...declaration.host.optional] }),
185
+ },
186
+ ...(declaration.env === undefined ? {} : { env: declaration.env }),
187
+ },
188
+ };
189
+ }
@@ -1,7 +1,6 @@
1
1
  export interface UnitCard {
2
2
  readonly titleKey: string;
3
3
  readonly order: number;
4
- readonly enabledByDefault: boolean;
5
4
  }
6
5
  export interface UnitTool {
7
6
  readonly name: string;
@@ -46,7 +45,6 @@ export interface PluginRelease {
46
45
  }
47
46
  export interface UnitManifest {
48
47
  readonly id: string;
49
- readonly kind: 'board' | 'ui';
50
48
  readonly card: UnitCard | null;
51
49
  readonly tools: readonly UnitTool[];
52
50
  readonly host: UnitHostNeeds;
@@ -1,5 +1,5 @@
1
1
  import type { PluginRelease } from '../api/unit.js';
2
- export type ProvisionAction = 'install' | 'update' | 'uninstall' | 'update-dependency' | 'install-bundle' | 'update-bundle' | 'remove-bundle' | 'cancel-install';
2
+ export type ProvisionAction = 'install' | 'update' | 'uninstall' | 'update-dependency' | 'install-bundle' | 'update-bundle' | 'remove-bundle' | 'cancel-install' | 'update-plugin' | 'uninstall-plugin';
3
3
  export interface PluginStatePayload {
4
4
  readonly name: string;
5
5
  readonly status: 'ok' | 'outdated' | 'missing';
@@ -32,7 +32,7 @@ export function ApprovalDetail(props) {
32
32
  const owner = (props.units ?? []).find((face) => face.approval?.tools.includes(input.tool) === true);
33
33
  if (owner?.approval !== undefined) {
34
34
  const Render = owner.approval.render;
35
- return (h(CardBoundary, { unit: owner.manifest.id, resetKey: props.callId ?? '', t: t },
35
+ return (h(CardBoundary, { unit: owner.unit, resetKey: props.callId ?? '', t: t },
36
36
  h(Render, { input: input, t: t })));
37
37
  }
38
38
  if (command === '')
@@ -40,12 +40,11 @@ export interface BoardModule {
40
40
  readonly id: string;
41
41
  readonly titleKey: string;
42
42
  readonly order: number;
43
- readonly enabledByDefault: boolean;
44
43
  readonly View: (props: BoardModuleProps) => ReactNode;
45
44
  readonly steps?: Readonly<Record<string, UnitStep>>;
46
45
  }
47
46
  export interface UnitClientFace {
48
- readonly manifest: UnitManifest;
47
+ readonly unit: string;
49
48
  readonly card: (props: BoardModuleProps) => ReactNode;
50
49
  readonly steps?: Readonly<Record<string, UnitStep>>;
51
50
  readonly messages?: Dictionaries;
@@ -57,6 +56,9 @@ export interface UnitClientFace {
57
56
  }) => ReactNode;
58
57
  } | undefined;
59
58
  }
59
+ export interface UnitOwnedClientFace extends UnitClientFace {
60
+ readonly manifest: UnitManifest;
61
+ }
60
62
  export interface ApprovalInput {
61
63
  readonly tool: string;
62
64
  readonly args: Readonly<Record<string, unknown>>;
@@ -0,0 +1,32 @@
1
+ import type { ModuleStore } from './workspace-store.js';
2
+ export declare const PLUGIN_LEDGER_FILE = "plugins.json";
3
+ export interface PluginLedgerEntry {
4
+ readonly opencliHome: string;
5
+ readonly package: string;
6
+ readonly name?: string;
7
+ readonly registry?: string;
8
+ readonly local?: boolean;
9
+ readonly installedAt: string;
10
+ readonly releasedAt?: string;
11
+ }
12
+ export declare function ledgerKeyOf(entry: {
13
+ readonly opencliHome: string;
14
+ readonly package: string;
15
+ }): string;
16
+ export declare function parsePluginLedger(raw: unknown): PluginLedgerEntry[];
17
+ export declare function readPluginLedger(store: ModuleStore): PluginLedgerEntry[];
18
+ export declare function writePluginLedger(store: ModuleStore, entries: readonly PluginLedgerEntry[]): void;
19
+ export declare function pluginEntriesOfHome(entries: readonly PluginLedgerEntry[], opencliHome: string): PluginLedgerEntry[];
20
+ export declare function isHeldEntry(entry: PluginLedgerEntry): boolean;
21
+ export declare function heldEntriesOfHome(entries: readonly PluginLedgerEntry[], opencliHome: string): PluginLedgerEntry[];
22
+ export declare function heldEntryOfPlugin(entries: readonly PluginLedgerEntry[], opencliHome: string, plugin: string): PluginLedgerEntry | undefined;
23
+ export declare function withReleasedEntry(entries: readonly PluginLedgerEntry[], opencliHome: string, pkg: string, releasedAt: string): PluginLedgerEntry[];
24
+ export type PluginRetention = 'remove' | 'keep-held' | 'keep-claimed' | 'keep-unsure';
25
+ export declare function pluginRetentionOf(options: {
26
+ readonly held: boolean;
27
+ readonly claimedBy: readonly string[];
28
+ readonly trustworthy: boolean;
29
+ }): PluginRetention;
30
+ export declare function pluginEntryOf(entries: readonly PluginLedgerEntry[], opencliHome: string, pkg: string): PluginLedgerEntry | undefined;
31
+ export declare function withPluginEntry(entries: readonly PluginLedgerEntry[], entry: PluginLedgerEntry): PluginLedgerEntry[];
32
+ export declare function withoutPluginEntry(entries: readonly PluginLedgerEntry[], opencliHome: string, pkg: string): PluginLedgerEntry[];
@@ -0,0 +1,99 @@
1
+ export const PLUGIN_LEDGER_FILE = 'plugins.json';
2
+ export function ledgerKeyOf(entry) {
3
+ return `${entry.opencliHome}\u0000${entry.package}`;
4
+ }
5
+ function textOf(value) {
6
+ if (typeof value !== 'string')
7
+ return undefined;
8
+ const trimmed = value.trim();
9
+ return trimmed === '' ? undefined : trimmed;
10
+ }
11
+ function entryOf(value) {
12
+ if (typeof value !== 'object' || value === null)
13
+ return undefined;
14
+ const box = value;
15
+ const opencliHome = textOf(box['opencliHome']);
16
+ const pkg = textOf(box['package']);
17
+ const installedAt = textOf(box['installedAt']);
18
+ if (opencliHome === undefined || pkg === undefined || installedAt === undefined)
19
+ return undefined;
20
+ const name = textOf(box['name']);
21
+ const registry = textOf(box['registry']);
22
+ const releasedAt = textOf(box['releasedAt']);
23
+ return {
24
+ opencliHome,
25
+ package: pkg,
26
+ installedAt,
27
+ ...(name === undefined ? {} : { name }),
28
+ ...(registry === undefined ? {} : { registry }),
29
+ ...(releasedAt === undefined ? {} : { releasedAt }),
30
+ ...(box['local'] === true ? { local: true } : {}),
31
+ };
32
+ }
33
+ export function parsePluginLedger(raw) {
34
+ if (typeof raw !== 'object' || raw === null)
35
+ return [];
36
+ const list = raw['entries'];
37
+ if (!Array.isArray(list))
38
+ return [];
39
+ const entries = [];
40
+ for (const item of list) {
41
+ const entry = entryOf(item);
42
+ if (entry === undefined)
43
+ continue;
44
+ if (entries.some((known) => ledgerKeyOf(known) === ledgerKeyOf(entry)))
45
+ continue;
46
+ entries.push(entry);
47
+ }
48
+ return entries;
49
+ }
50
+ export function readPluginLedger(store) {
51
+ return parsePluginLedger(store.read(PLUGIN_LEDGER_FILE));
52
+ }
53
+ export function writePluginLedger(store, entries) {
54
+ if (entries.length === 0) {
55
+ store.remove(PLUGIN_LEDGER_FILE);
56
+ return;
57
+ }
58
+ store.write(PLUGIN_LEDGER_FILE, { entries });
59
+ }
60
+ export function pluginEntriesOfHome(entries, opencliHome) {
61
+ return entries.filter((entry) => entry.opencliHome === opencliHome);
62
+ }
63
+ export function isHeldEntry(entry) {
64
+ return entry.releasedAt === undefined;
65
+ }
66
+ export function heldEntriesOfHome(entries, opencliHome) {
67
+ return pluginEntriesOfHome(entries, opencliHome).filter(isHeldEntry);
68
+ }
69
+ export function heldEntryOfPlugin(entries, opencliHome, plugin) {
70
+ const key = plugin.trim();
71
+ if (key === '')
72
+ return undefined;
73
+ return heldEntriesOfHome(entries, opencliHome).find((entry) => entry.name === key || entry.package === key);
74
+ }
75
+ export function withReleasedEntry(entries, opencliHome, pkg, releasedAt) {
76
+ const entry = pluginEntryOf(entries, opencliHome, pkg);
77
+ if (entry === undefined || entry.releasedAt !== undefined)
78
+ return [...entries];
79
+ return withPluginEntry(entries, { ...entry, releasedAt });
80
+ }
81
+ export function pluginRetentionOf(options) {
82
+ if (options.held)
83
+ return 'keep-held';
84
+ if (!options.trustworthy)
85
+ return 'keep-unsure';
86
+ return options.claimedBy.length > 0 ? 'keep-claimed' : 'remove';
87
+ }
88
+ export function pluginEntryOf(entries, opencliHome, pkg) {
89
+ const key = ledgerKeyOf({ opencliHome, package: pkg });
90
+ return entries.find((entry) => ledgerKeyOf(entry) === key);
91
+ }
92
+ export function withPluginEntry(entries, entry) {
93
+ const key = ledgerKeyOf(entry);
94
+ return [...entries.filter((item) => ledgerKeyOf(item) !== key), entry];
95
+ }
96
+ export function withoutPluginEntry(entries, opencliHome, pkg) {
97
+ const key = ledgerKeyOf({ opencliHome, package: pkg });
98
+ return entries.filter((entry) => ledgerKeyOf(entry) !== key);
99
+ }
@@ -1,6 +1,6 @@
1
1
  import type { ProvisionAction } from '../api/workbench-contract.js';
2
2
  export type { ProvisionAction };
3
- export declare const PROVISION_ACTIONS: readonly ["install", "update", "uninstall", "install-bundle", "update-bundle", "remove-bundle", "cancel-install", "update-dependency"];
3
+ export declare const PROVISION_ACTIONS: readonly ["install", "update", "uninstall", "install-bundle", "update-bundle", "remove-bundle", "cancel-install", "update-dependency", "update-plugin", "uninstall-plugin"];
4
4
  export declare const DEPENDENCY_ACTIONS: readonly ["update-dependency"];
5
5
  export declare function isDependencyAction(action: ProvisionAction): action is (typeof DEPENDENCY_ACTIONS)[number];
6
6
  export type ProvisionTarget = 'unit' | 'id' | 'spec' | 'name' | 'requestId';
@@ -7,6 +7,8 @@ export const PROVISION_ACTIONS = [
7
7
  'remove-bundle',
8
8
  'cancel-install',
9
9
  'update-dependency',
10
+ 'update-plugin',
11
+ 'uninstall-plugin',
10
12
  ];
11
13
  export const DEPENDENCY_ACTIONS = ['update-dependency'];
12
14
  export function isDependencyAction(action) {
@@ -21,6 +23,8 @@ export function provisionTargetOf(action) {
21
23
  return 'spec';
22
24
  if (action === 'remove-bundle')
23
25
  return 'name';
26
+ if (action === 'update-plugin' || action === 'uninstall-plugin')
27
+ return 'name';
24
28
  if (action === 'cancel-install')
25
29
  return 'requestId';
26
30
  return 'unit';
@@ -48,6 +52,8 @@ function requestOf(action, value) {
48
52
  return { action, spec: value };
49
53
  if (action === 'remove-bundle')
50
54
  return { action, name: value };
55
+ if (action === 'update-plugin' || action === 'uninstall-plugin')
56
+ return { action, name: value };
51
57
  if (action === 'cancel-install')
52
58
  return { action, requestId: value };
53
59
  return { action, unit: value };
@@ -8,12 +8,14 @@ import type { ApprovalCall, ApprovalFacts, ApprovalPolicy } from '../host/tool-a
8
8
  import type { ValueHandoff } from '../host/value-handoff.js';
9
9
  import type { WorkspaceStore } from '../host/workspace-store.js';
10
10
  export interface UnitMountDeps {
11
+ readonly manifest: UnitManifest;
11
12
  readonly ctx: DshHostContext;
12
13
  readonly logger?: {
13
14
  info?(message: string): void;
14
15
  warn?(message: string): void;
15
16
  } | undefined;
16
17
  readonly manifests: () => readonly UnitManifest[];
18
+ readonly ownedUnits: () => readonly string[];
17
19
  readonly opencliCommand?: string;
18
20
  readonly store: WorkspaceStore;
19
21
  readonly values?: ValueHandoff | undefined;
@@ -61,7 +63,10 @@ export interface UnitMountResult {
61
63
  readonly approvalDetail?: ((call: ApprovalCall) => ApprovalFacts | undefined | Promise<ApprovalFacts | undefined>) | undefined;
62
64
  readonly needsApproval?: ApprovalPolicy | undefined;
63
65
  }
64
- export interface UnitHostFace {
65
- readonly manifest: UnitManifest;
66
+ export interface UnitModuleFace {
67
+ readonly unit: string;
66
68
  mount(deps: UnitMountDeps): UnitMountResult;
67
69
  }
70
+ export interface UnitOwnedFace extends UnitModuleFace {
71
+ readonly manifest: UnitManifest;
72
+ }
package/dist/host.d.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/dist/host.js 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/dist/i18n/en.js CHANGED
@@ -65,7 +65,7 @@ export const en = {
65
65
  'panels.market.missing': 'not installed',
66
66
  'panels.market.unchecked': 'unknown',
67
67
  'panels.market.checking': 'checking',
68
- 'panels.market.sectionInstalled': 'Installed',
68
+ 'panels.market.sectionInstalled': 'Installed modules',
69
69
  'panels.market.sectionPending': 'Not installed',
70
70
  'panels.market.sectionShared': 'Shared dependencies',
71
71
  'panels.market.sharedUsedBy': 'Used by {modules}',
@@ -93,11 +93,19 @@ export const en = {
93
93
  'panels.market.noSource': 'This module declares no install method — install it yourself',
94
94
  'panels.market.badSource': 'The install method is incomplete — install it yourself',
95
95
  'panels.market.empty': 'No modules available',
96
- 'panels.market.installedSection': 'Installed',
97
- 'panels.market.sectionLocal': 'Linked locally',
96
+ 'panels.market.sectionLocal': 'Local modules',
98
97
  'panels.market.sectionUnlisted': 'Not from the market',
98
+ 'panels.market.sectionPlugins': 'Installed opencli plugins',
99
99
  'panels.market.moduleOutdated': 'update available',
100
+ 'panels.market.moduleRefused': 'This module was built for an older version of the workbench, so it does not work here. Update it, or uninstall.',
101
+ 'panels.market.moduleRefusedDetail': 'Why it cannot be used: {reason}',
100
102
  'panels.market.moduleRestartHint': 'installed but not active — restart dsh once to bring it up',
103
+ 'panels.market.pluginKeptInUse': 'Files kept for now: {names} still uses it.',
104
+ '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.',
105
+ 'panels.market.pluginKeptHeld': 'Files kept: this one is yours, and a module letting go will not remove it.',
106
+ 'panels.market.pluginKeptUnnamed': 'Files kept for now: this one cannot be identified as a plugin, so it is unclear who needs it.',
107
+ 'panels.market.pluginAlsoKnownAs': 'Its package is {pkg} (updates and uninstall use that)',
108
+ 'panels.market.pluginBroken': 'This install is broken: it cannot be identified as a plugin. Uninstall it, or install it again by package name above.',
101
109
  'panels.market.restartNeeded': 'The new version is installed — restart once to pick it up.',
102
110
  'panels.market.foot': 'After installing, enable it in the panel page of that project.',
103
111
  'panels.market.recheck': 'Check for updates',
@@ -145,7 +153,11 @@ export const en = {
145
153
  'panels.market.add.problemInstalled': 'This module is already installed',
146
154
  'panels.market.add.problemNotFound': 'No such module found',
147
155
  'panels.market.add.problemNotPackage': 'That path does not exist or is not a valid module package',
148
- 'panels.market.add.problemNotBundle': 'This package declares no bundle, so it cannot be installed as a module: {reason}',
156
+ 'panels.market.add.problemNotBundle': '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.',
157
+ '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.',
158
+ '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.',
159
+ 'panels.market.add.failureNotModuleOrPlugin': 'This package is neither a workbench module nor an opencli plugin, so installing it would do nothing',
160
+ 'panels.market.add.installedPlugin': 'Installed: the opencli plugin {name}',
149
161
  'panels.market.add.problemNetwork': 'Cannot reach the module source; check the network and retry',
150
162
  'panels.market.add.problemUnknown': 'Cannot fetch module information: {reason}',
151
163
  'panels.market.add.failureNetwork': 'The network connection failed',
@@ -176,6 +188,7 @@ export const en = {
176
188
  'panels.market.add.registryPnpm': "pnpm's own registry",
177
189
  'panels.market.add.check': 'Check',
178
190
  'panels.market.add.checking': 'Checking…',
191
+ 'panels.market.add.classifying': 'Working out what it is…',
179
192
  'panels.market.add.change': 'Edit',
180
193
  'panels.market.add.previewTitle': 'About to install',
181
194
  'panels.market.add.fieldName': 'Package',