@dsh-workbench/shared 1.0.0-beta.0 → 1.0.0

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 (82) hide show
  1. package/dist/components/FormNodes.js +5 -1
  2. package/dist/components/FormRows.css +1 -0
  3. package/dist/components/FormRows.d.ts +4 -2
  4. package/dist/components/FormRows.js +5 -4
  5. package/dist/config/constants.d.ts +0 -2
  6. package/dist/config/constants.js +0 -5
  7. package/dist/contract/command.d.ts +22 -0
  8. package/dist/contract/command.js +1 -0
  9. package/dist/contract/data.d.ts +2 -1
  10. package/dist/contract/extension.d.ts +14 -0
  11. package/dist/contract/extension.js +1 -0
  12. package/dist/contract/git.d.ts +38 -0
  13. package/dist/contract/git.js +1 -0
  14. package/dist/contract/lib.d.ts +4 -0
  15. package/dist/contract/lib.js +1 -0
  16. package/dist/contract/protocol/config-sync.d.ts +1 -21
  17. package/dist/contract/protocol/market-index.d.ts +1 -0
  18. package/dist/contract/protocol/profile-create.d.ts +0 -3
  19. package/dist/host/adapter-set.js +1 -0
  20. package/dist/host/adapters.d.ts +6 -0
  21. package/dist/host/extension.d.ts +7 -0
  22. package/dist/host/extension.js +39 -0
  23. package/dist/host/git.d.ts +2 -52
  24. package/dist/host/git.js +1 -64
  25. package/dist/host/http.js +1 -1
  26. package/dist/host/market-index.js +3 -44
  27. package/dist/host/port-types.d.ts +1 -0
  28. package/dist/host/release-packages.generated.d.ts +1 -0
  29. package/dist/host/release-packages.generated.js +10 -0
  30. package/dist/host/tool-kit.d.ts +0 -6
  31. package/dist/host/tool-kit.js +0 -22
  32. package/dist/host/unit-face.d.ts +7 -0
  33. package/dist/host/version-range.js +7 -67
  34. package/dist/host/version.d.ts +3 -2
  35. package/dist/host/version.js +17 -19
  36. package/dist/host.d.ts +6 -0
  37. package/dist/host.js +6 -0
  38. package/dist/i18n/en.js +3 -1
  39. package/dist/i18n/index.d.ts +9 -21
  40. package/dist/i18n/settings.d.ts +5 -19
  41. package/dist/i18n/settings.js +7 -35
  42. package/dist/i18n/zh.d.ts +8 -20
  43. package/dist/i18n/zh.js +3 -1
  44. package/dist/index.d.ts +4 -0
  45. package/dist/index.js +4 -0
  46. package/dist/lib/version.d.ts +4 -0
  47. package/dist/lib/version.js +21 -0
  48. package/dist/testing/git.d.ts +3 -0
  49. package/dist/testing/git.js +36 -0
  50. package/package.json +6 -4
  51. package/src/components/Form.test.tsx +6 -0
  52. package/src/components/FormNodes.tsx +6 -1
  53. package/src/components/FormRows.css +1 -0
  54. package/src/components/FormRows.test.tsx +30 -0
  55. package/src/components/FormRows.tsx +9 -5
  56. package/src/config/constants.ts +0 -6
  57. package/src/contract/command.ts +25 -0
  58. package/src/contract/data.ts +2 -1
  59. package/src/contract/extension.ts +20 -0
  60. package/src/contract/git.ts +20 -0
  61. package/src/contract/lib.ts +5 -0
  62. package/src/contract/protocol/config-sync.ts +1 -24
  63. package/src/contract/protocol/market-index.ts +1 -0
  64. package/src/contract/protocol/profile-create.ts +0 -3
  65. package/src/host/adapter-set.ts +1 -0
  66. package/src/host/adapters.ts +6 -0
  67. package/src/host/extension.test.ts +55 -0
  68. package/src/host/extension.ts +57 -0
  69. package/src/host/git.ts +6 -92
  70. package/src/host/http.ts +1 -1
  71. package/src/host/market-index.ts +3 -33
  72. package/src/host/port-types.ts +1 -0
  73. package/src/host/release-packages.generated.ts +10 -0
  74. package/src/host/tool-kit.ts +0 -35
  75. package/src/host/unit-face.ts +7 -0
  76. package/src/host/version-range.ts +6 -64
  77. package/src/host/version.ts +18 -18
  78. package/src/host.ts +6 -0
  79. package/src/i18n/en.ts +3 -1
  80. package/src/i18n/settings.ts +7 -35
  81. package/src/i18n/zh.ts +3 -1
  82. package/src/index.ts +4 -0
@@ -2,35 +2,15 @@ export const CONFIG_SYNC_ROUTE = 'config-sync';
2
2
 
3
3
  export const CONFIG_SYNC_RESOURCE = 'configSync';
4
4
 
5
- export type ConfigSyncState = 'new' | 'differs' | 'same';
6
-
7
- export type ConfigSyncAssetState = 'missing' | 'differs' | 'same' | 'external';
8
-
9
- export interface ConfigSyncAssetFile {
10
- readonly path: string;
11
- readonly state: ConfigSyncAssetState;
12
- readonly bytes: number | null;
13
- }
14
-
15
- export interface ConfigSyncAsset {
16
- readonly key: string;
17
- readonly path: string;
18
- readonly kind: 'file' | 'dir';
19
- readonly state: ConfigSyncAssetState;
20
- readonly files: readonly ConfigSyncAssetFile[];
21
- readonly bytes: number;
22
- readonly reason?: string;
23
- }
5
+ export type ConfigSyncState = 'new' | 'differs';
24
6
 
25
7
  export interface ConfigSyncRow {
26
8
  readonly key: string;
27
9
  readonly id: string;
28
- readonly insert: boolean;
29
10
  readonly state: ConfigSyncState;
30
11
  readonly body: string;
31
12
  readonly ownBody: string | null;
32
13
  readonly missing: readonly string[];
33
- readonly assets: readonly ConfigSyncAsset[];
34
14
  }
35
15
 
36
16
  export interface ConfigSyncProfile {
@@ -52,7 +32,6 @@ export interface ConfigSyncSnapshot {
52
32
  export interface ConfigSyncApplied {
53
33
  readonly written: readonly string[];
54
34
  readonly skipped: readonly string[];
55
- readonly files: readonly string[];
56
35
  readonly verified: boolean;
57
36
  }
58
37
 
@@ -60,6 +39,4 @@ export interface ConfigSyncRequest {
60
39
  readonly source: string;
61
40
  readonly keys: readonly string[];
62
41
  readonly overwrite: readonly string[];
63
- readonly assets?: readonly string[];
64
- readonly overwriteAssets?: readonly string[];
65
42
  }
@@ -217,6 +217,7 @@ export interface PublishChangelog {
217
217
  export interface PublishPreview {
218
218
  readonly name: string;
219
219
  readonly version: string;
220
+ readonly author?: string;
220
221
  readonly registry?: string;
221
222
  readonly market?: MarketPublishDestination;
222
223
  readonly changelog?: PublishChangelog;
@@ -15,9 +15,6 @@ export interface ProfileCreateSnapshot {
15
15
 
16
16
  export interface ProfileCreateCopy {
17
17
  readonly settings: boolean;
18
- readonly plugins: boolean;
19
- readonly projects: boolean;
20
- readonly sessions: boolean;
21
18
  }
22
19
 
23
20
  export interface ProfileCreateRequest {
@@ -265,6 +265,7 @@ export function withDefaultAdapterSet(defaults: AdapterSet, loaded: AdapterSetLo
265
265
  id: set.id ?? defaults.id,
266
266
  version: set.version ?? defaults.version,
267
267
  data: set.data ?? defaults.data,
268
+ libs: set.libs ?? defaults.libs,
268
269
  supply: set.supply ?? defaults.supply,
269
270
  modules: set.modules ?? defaults.modules,
270
271
  profile: set.profile ?? defaults.profile,
@@ -1,5 +1,8 @@
1
1
  import type { DshHostContext } from '../api/dsh.js';
2
2
  import type { DataReader } from '../contract/data.js';
3
+ import type { LibReader } from '../contract/lib.js';
4
+ import type { GitReader } from '../contract/git.js';
5
+ import type { CommandReader } from '../contract/command.js';
3
6
  import type {
4
7
  EnvSupplyPort,
5
8
  HostActions,
@@ -21,6 +24,9 @@ export interface AdapterSet {
21
24
  readonly id: string;
22
25
  readonly version: string;
23
26
  readonly data: DataReader;
27
+ readonly libs?: LibReader | undefined;
28
+ readonly git?: GitReader | undefined;
29
+ readonly commands?: CommandReader | undefined;
24
30
  readonly supply: EnvSupplyPort;
25
31
  readonly modules: ModulePort;
26
32
  readonly profile: ProfilePort;
@@ -0,0 +1,55 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { DataReader } from '../contract/data.js';
3
+ import type { ExtensionNeed } from '../contract/needs.js';
4
+ import { extensionCallerOf } from './extension.js';
5
+
6
+ const NEED: ExtensionNeed = {
7
+ kind: 'extension',
8
+ runtime: 'opencli',
9
+ name: 'coding',
10
+ purpose: '测试',
11
+ };
12
+
13
+ function failingData(): DataReader {
14
+ return {
15
+ read: async () => ({
16
+ ok: false,
17
+ error: {
18
+ code: 'command',
19
+ message: 'opencli 这条命令没跑通(退出码 1)',
20
+ detail: "error: unknown command 'issue-statuses'",
21
+ pageCode: 'cli-unclassified',
22
+ },
23
+ }),
24
+ } as unknown as DataReader;
25
+ }
26
+
27
+ describe('extensionCallerOf', () => {
28
+ it('cli-unclassified 补“升级插件”的 message 与 hint,id 用调用方给的 suiteId', async () => {
29
+ const caller = extensionCallerOf(failingData(), NEED, {
30
+ suiteId: 'coding-issues',
31
+ });
32
+ await expect(caller.json(['issue-statuses'])).rejects.toMatchObject({
33
+ code: 'cli-unclassified',
34
+ message: expect.stringContaining('workbench_modules_update(id=coding-issues)'),
35
+ hint: expect.stringContaining('opencli coding issue-statuses --help'),
36
+ detail: "error: unknown command 'issue-statuses'",
37
+ });
38
+ });
39
+
40
+ it('别的码不补“插件版本旧”', async () => {
41
+ const caller = extensionCallerOf(failingData(), NEED, {
42
+ suiteId: 'coding-issues',
43
+ });
44
+ const data = {
45
+ read: async () => ({
46
+ ok: false,
47
+ error: { code: 'auth', message: '需要登录', pageCode: 'auth-required' },
48
+ }),
49
+ } as unknown as DataReader;
50
+ await expect(
51
+ extensionCallerOf(data, NEED, { suiteId: 'x' }).json(['issue-statuses']),
52
+ ).rejects.toMatchObject({ code: 'auth-required', message: '需要登录' });
53
+ expect(caller).toBeDefined();
54
+ });
55
+ });
@@ -0,0 +1,57 @@
1
+ import type { DataReader } from '../contract/data.js';
2
+ import type { ExtensionCaller, ExtensionCallOptions, ExtensionValue } from '../contract/extension.js';
3
+ import type { ExtensionNeed } from '../contract/needs.js';
4
+ import { WorkbenchDataError } from './data-error.js';
5
+
6
+ function cliCodeOf(code: string): string {
7
+ if (code === 'auth') return 'auth-required';
8
+ if (code === 'timeout') return 'cli-timeout';
9
+ if (code === 'parse') return 'cli-bad-output';
10
+ return 'cli-exec-failed';
11
+ }
12
+
13
+ export interface ExtensionCallerOptions {
14
+ readonly suiteId?: string | undefined;
15
+ }
16
+
17
+ export function extensionCallerOf(
18
+ data: DataReader,
19
+ need: ExtensionNeed,
20
+ callerOptions: ExtensionCallerOptions = {},
21
+ ): ExtensionCaller {
22
+ return {
23
+ async json(
24
+ args: readonly string[],
25
+ options: ExtensionCallOptions = {},
26
+ ): Promise<ExtensionValue> {
27
+ const result = await data.read({
28
+ kind: 'command',
29
+ adapter: need.runtime,
30
+ argv: [need.name, ...args],
31
+ ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
32
+ });
33
+ if (result.ok) return { value: result.data, at: Date.now() };
34
+ const code = result.error.pageCode ?? cliCodeOf(result.error.code);
35
+ const command = [need.name, ...args].join(' ');
36
+ const advice =
37
+ code === 'cli-unclassified'
38
+ ? `这条命令(${command})在这份 ${need.name} 插件里可能不存在(插件版本旧就会缺):用 workbench_modules_update(id=${callerOptions.suiteId ?? need.name})升级,或在面板的模块状态里点更新,再重试。终端确认:opencli ${need.name} ${args[0] ?? ''} --help`
39
+ : undefined;
40
+ const hint =
41
+ advice === undefined
42
+ ? result.error.hint
43
+ : result.error.hint === undefined
44
+ ? advice
45
+ : `${advice}\n${result.error.hint}`;
46
+ throw new WorkbenchDataError(
47
+ code,
48
+ advice === undefined ? result.error.message : `${result.error.message}。${advice}`,
49
+ {
50
+ ...(result.error.detail === undefined ? {} : { detail: result.error.detail }),
51
+ ...(hint === undefined ? {} : { hint }),
52
+ ...(result.error.helpUrl === undefined ? {} : { helpUrl: result.error.helpUrl }),
53
+ },
54
+ );
55
+ },
56
+ };
57
+ }
package/src/host/git.ts CHANGED
@@ -1,99 +1,13 @@
1
- import type { DshShell } from '../api/dsh.js';
2
- import { quoteShellArg, tailLines } from '../host/shell-text.js';
3
- import { runShell } from '../host/shell.js';
4
1
 
5
- export type OriginRead =
6
- | { readonly kind: 'ok'; readonly remote: string }
7
- | { readonly kind: 'no-origin'; readonly detail?: string }
8
- | { readonly kind: 'unknown'; readonly reason: string; readonly detail?: string };
2
+ export type {
3
+ BranchRead,
4
+ HeadRead,
5
+ OriginRead,
6
+ } from '../contract/git.js';
7
+ import type { BranchRead, HeadRead, OriginRead } from '../contract/git.js';
9
8
 
10
9
  export type OriginReader = (path: string) => Promise<OriginRead>;
11
10
 
12
- export type BranchRead =
13
- | { readonly kind: 'ok'; readonly branch: string }
14
- | { readonly kind: 'no-branch'; readonly detail?: string | undefined }
15
- | { readonly kind: 'unknown'; readonly reason: string; readonly detail?: string | undefined };
16
-
17
11
  export type BranchReader = (path: string) => Promise<BranchRead>;
18
12
 
19
- export type HeadRead =
20
- | { readonly kind: 'ok'; readonly commit: string }
21
- | { readonly kind: 'no-head'; readonly detail?: string }
22
- | { readonly kind: 'unknown'; readonly reason: string; readonly detail?: string };
23
-
24
13
  export type HeadReader = (path: string) => Promise<HeadRead>;
25
-
26
- export function createHeadReader(options: {
27
- shell: DshShell;
28
- logger?: { warn?(message: string): void } | undefined;
29
- }): HeadReader {
30
- const { shell, logger } = options;
31
-
32
- return async function readHead(path: string): Promise<HeadRead> {
33
- const key = String(path ?? '').trim();
34
- if (!key) return { kind: 'unknown', reason: 'no-path' };
35
-
36
- let result;
37
- try {
38
- result = await runShell(shell, `git -C ${quoteShellArg(key)} rev-parse HEAD`);
39
- } catch (error) {
40
- logger?.warn?.(`[dsh-workbench] git rev-parse 执行失败:${String(error)}`);
41
- return { kind: 'unknown', reason: 'exec-failed', detail: String(error) };
42
- }
43
-
44
- if (result.exitCode !== 0) {
45
- return { kind: 'no-head', detail: tailLines(result.stderr, 3) };
46
- }
47
- return { kind: 'ok', commit: result.stdout.trim() };
48
- };
49
- }
50
-
51
- export function createBranchReader(options: {
52
- shell: DshShell;
53
- logger?: { warn?(message: string): void } | undefined;
54
- }): BranchReader {
55
- const { shell, logger } = options;
56
-
57
- return async function readBranch(path: string): Promise<BranchRead> {
58
- const key = String(path ?? '').trim();
59
- if (!key) return { kind: 'unknown', reason: 'no-path' };
60
-
61
- let result;
62
- try {
63
- result = await runShell(shell, `git -C ${quoteShellArg(key)} rev-parse --abbrev-ref HEAD`);
64
- } catch (error) {
65
- logger?.warn?.(`[dsh-workbench] git rev-parse --abbrev-ref 执行失败:${String(error)}`);
66
- return { kind: 'unknown', reason: 'exec-failed', detail: String(error) };
67
- }
68
-
69
- const name = result.stdout.trim();
70
- if (result.exitCode !== 0) return { kind: 'no-branch', detail: tailLines(result.stderr, 3) };
71
- if (name === '' || name === 'HEAD') return { kind: 'no-branch', detail: name };
72
- return { kind: 'ok', branch: name };
73
- };
74
- }
75
-
76
- export function createOriginReader(options: {
77
- shell: DshShell;
78
- logger?: { warn?(message: string): void } | undefined;
79
- }): OriginReader {
80
- const { shell, logger } = options;
81
-
82
- return async function readOrigin(path: string): Promise<OriginRead> {
83
- const key = String(path ?? '').trim();
84
- if (!key) return { kind: 'unknown', reason: 'no-path' };
85
-
86
- let result;
87
- try {
88
- result = await runShell(shell, `git -C ${quoteShellArg(key)} remote get-url origin`);
89
- } catch (error) {
90
- logger?.warn?.(`[dsh-workbench] git remote 执行失败:${String(error)}`);
91
- return { kind: 'unknown', reason: 'exec-failed', detail: String(error) };
92
- }
93
-
94
- if (result.exitCode !== 0) {
95
- return { kind: 'no-origin', detail: tailLines(result.stderr, 3) };
96
- }
97
- return { kind: 'ok', remote: result.stdout.trim() };
98
- };
99
- }
package/src/host/http.ts CHANGED
@@ -90,7 +90,7 @@ export function registerApi(ctx: DshHostContext, routes: RouteTable): () => void
90
90
  try {
91
91
  sendJson(res, 200, await handler(search));
92
92
  } catch (error) {
93
- if (error instanceof WorkbenchDataError) {
93
+ if (isDataError(error)) {
94
94
  sendJson(res, 200, { ok: false, error: toErrorPayload(error) });
95
95
  return;
96
96
  }
@@ -13,6 +13,7 @@ import type {
13
13
  } from '../contract/protocol/market-index.js';
14
14
  import { MARKET_INDEX_FORMAT } from '../contract/protocol/market-index.js';
15
15
  import { marketText } from '../lib/market-index.js';
16
+ import { compareVersions, isValidVersion } from './version.js';
16
17
 
17
18
  export { marketBranchOk, marketInstallTarget, marketText } from '../lib/market-index.js';
18
19
 
@@ -39,7 +40,6 @@ export const MARKET_INDEX_README_LINES: readonly string[] = [
39
40
  '',
40
41
  ];
41
42
 
42
- const SEMVER = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+([0-9A-Za-z.-]+))?$/;
43
43
  const NPM_NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
44
44
  const GIT_COMMIT = /^[0-9a-f]{40}$/;
45
45
  const MAX_ICON_CHARS = 32 * 1024;
@@ -73,37 +73,7 @@ function problem(kind: MarketIndexProblem['kind'], detail: string, at?: string):
73
73
  }
74
74
 
75
75
  export function compareSemver(left: string, right: string): number | undefined {
76
- const a = SEMVER.exec(left);
77
- const b = SEMVER.exec(right);
78
- if (a === null || b === null) return undefined;
79
- for (let index = 1; index <= 3; index += 1) {
80
- const diff = Number(a[index]) - Number(b[index]);
81
- if (diff !== 0) return diff > 0 ? 1 : -1;
82
- }
83
- const preA = a[4];
84
- const preB = b[4];
85
- if (preA === undefined && preB === undefined) return 0;
86
- if (preA === undefined) return 1;
87
- if (preB === undefined) return -1;
88
- const leftParts = preA.split('.');
89
- const rightParts = preB.split('.');
90
- for (let index = 0; index < Math.max(leftParts.length, rightParts.length); index += 1) {
91
- const one = leftParts[index];
92
- const other = rightParts[index];
93
- if (one === undefined) return -1;
94
- if (other === undefined) return 1;
95
- const oneNumber = /^\d+$/.test(one);
96
- const otherNumber = /^\d+$/.test(other);
97
- if (oneNumber && otherNumber) {
98
- const diff = Number(one) - Number(other);
99
- if (diff !== 0) return diff > 0 ? 1 : -1;
100
- continue;
101
- }
102
- if (oneNumber) return -1;
103
- if (otherNumber) return 1;
104
- if (one !== other) return one > other ? 1 : -1;
105
- }
106
- return 0;
76
+ return compareVersions(left, right);
107
77
  }
108
78
 
109
79
  function sourceOf(raw: unknown, at: string): MarketIndexSource | MarketIndexProblem {
@@ -138,7 +108,7 @@ function versionOf(raw: unknown, at: string): MarketIndexVersion | MarketIndexPr
138
108
  const record = asRecord(raw);
139
109
  if (record === undefined) return problem('version', '版本必须是一个对象', at);
140
110
  const version = asText(record['version']);
141
- if (!SEMVER.test(version)) return problem('version', `版本号不是合法 SemVer:${JSON.stringify(version)}`, at);
111
+ if (!isValidVersion(version)) return problem('version', `版本号不是合法 SemVer:${JSON.stringify(version)}`, at);
142
112
  const source = sourceOf(record['source'], `${at}.source`);
143
113
  if ('kind' in source) return source;
144
114
  const timestamp = record['publishTimestamp'];
@@ -61,6 +61,7 @@ export interface SupplyLatestOptions {
61
61
 
62
62
  export interface SupplyMissingPartsOptions {
63
63
  readonly shell: DshShell;
64
+ readonly name: string;
64
65
  readonly registry: string;
65
66
  readonly version: string;
66
67
  readonly force?: boolean;
@@ -0,0 +1,10 @@
1
+ export const RELEASE_PACKAGES = [
2
+ '@dsh-workbench/adapter-probe',
3
+ '@dsh-workbench/adapter-profile',
4
+ '@dsh-workbench/adapter-runtime',
5
+ '@dsh-workbench/adapter-supply',
6
+ '@dsh-workbench/adapters-default',
7
+ '@dsh-workbench/core',
8
+ '@dsh-workbench/module-tools',
9
+ '@dsh-workbench/shared',
10
+ ] as const;
@@ -1,5 +1,4 @@
1
1
  import type { DshToolRunContext } from '../api/dsh.js';
2
- import { WorkbenchDataError } from './data-error.js';
3
2
 
4
3
  export function sessionWorkspace(exec: DshToolRunContext | undefined): string | null {
5
4
  const cwd = exec?.agent?.session?.header?.cwd;
@@ -25,37 +24,3 @@ export const TEXT_OUTPUT = { schema: { type: 'object', properties: {}, additiona
25
24
  export function jsonText(value: unknown): string {
26
25
  return JSON.stringify(value);
27
26
  }
28
-
29
- const UNCLASSIFIED = 'cli-unclassified';
30
-
31
- export interface CommandHintNames {
32
- readonly plugin: string;
33
- readonly suiteId: string;
34
- }
35
-
36
- export type CommandHint = <T>(command: string, run: () => Promise<T>) => Promise<T>;
37
-
38
- export function commandHintFor(names: CommandHintNames): CommandHint {
39
- return async (command, run) => {
40
- try {
41
- return await run();
42
- } catch (error) {
43
- if (!(error instanceof WorkbenchDataError) || error.code !== UNCLASSIFIED) throw error;
44
-
45
- const advice =
46
- `这条命令(${command})在这份 ${names.plugin} 插件里可能不存在(插件版本旧就会缺):` +
47
- `用 workbench_modules_update(id=${names.suiteId})升级插件,或在面板的模块状态里点更新,再重试。`;
48
-
49
- throw new WorkbenchDataError(error.code, `${error.message}。${advice}`, {
50
- ...(error.detail === undefined ? {} : { detail: error.detail }),
51
- hint:
52
- `升级:workbench_modules_update(id=${names.suiteId})。` +
53
- `终端确认:opencli ${names.plugin} ${command} --help${
54
- error.hint === undefined ? '' : `\n${error.hint}`
55
- }`,
56
- ...(error.helpUrl === undefined ? {} : { helpUrl: error.helpUrl }),
57
- ...(error.project === undefined ? {} : { project: error.project }),
58
- });
59
- }
60
- };
61
- }
@@ -8,6 +8,9 @@ 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
  import type { DataReader } from '../contract/data.js';
11
+ import type { LibReader } from '../contract/lib.js';
12
+ import type { GitReader } from '../contract/git.js';
13
+ import type { CommandReader } from '../contract/command.js';
11
14
 
12
15
  export interface ToolScopeContext {
13
16
  readonly workspace: string | null;
@@ -22,6 +25,10 @@ export interface UnitMountDeps {
22
25
  readonly ownedUnits: () => readonly string[];
23
26
  readonly installable?: (() => readonly string[]) | undefined;
24
27
  readonly data: DataReader;
28
+ readonly env?: Readonly<Record<string, string | undefined>> | undefined;
29
+ readonly libs?: LibReader | undefined;
30
+ readonly git?: GitReader | undefined;
31
+ readonly commands?: CommandReader | undefined;
25
32
  readonly store: WorkspaceStore;
26
33
  readonly values?: ValueHandoff | undefined;
27
34
  readonly probe?: (
@@ -1,71 +1,13 @@
1
- import { compareVersions } from './version.js';
1
+ import semver from 'semver';
2
2
 
3
3
  export type VersionVerdict = 'satisfied' | 'unsatisfied' | 'malformed';
4
4
 
5
- function readable(version: string): boolean {
6
- return compareVersions(version, '0.0.0') !== undefined;
7
- }
8
-
9
- function segmentsOf(version: string): number[] | undefined {
10
- if (!readable(version)) return undefined;
11
- return version.split('.').map((part) => Number.parseInt(part, 10));
12
- }
13
-
14
- function caretUpper(base: readonly number[]): string {
15
- const [major = 0, minor = 0, patch = 0] = base;
16
- if (major > 0) return `${major + 1}.0.0`;
17
- return minor > 0 ? `0.${minor + 1}.0` : `0.0.${patch + 1}`;
18
- }
19
-
20
- function tildeUpper(base: readonly number[]): string {
21
- const [major = 0, minor = 0] = base;
22
- return `${major}.${minor + 1}.0`;
23
- }
24
-
25
- function comparator(token: string, version: string): boolean | undefined {
26
- const match = /^(>=|<=|>|<|=|\^|~)?\s*(.+)$/.exec(token.trim());
27
- if (match === null) return undefined;
28
- const [, operator = '', raw = ''] = match;
29
- const base = segmentsOf(raw.trim());
30
- if (base === undefined) return undefined;
31
- const at = (other: string): number | undefined => compareVersions(version, other);
32
- switch (operator) {
33
- case '^':
34
- return (at(raw) ?? -1) >= 0 && (at(caretUpper(base)) ?? 1) < 0;
35
- case '~':
36
- return (at(raw) ?? -1) >= 0 && (at(tildeUpper(base)) ?? 1) < 0;
37
- default:
38
- break;
39
- }
40
- const compared = at(raw.trim());
41
- if (compared === undefined) return undefined;
42
- switch (operator) {
43
- case '>=':
44
- return compared >= 0;
45
- case '<=':
46
- return compared <= 0;
47
- case '>':
48
- return compared > 0;
49
- case '<':
50
- return compared < 0;
51
- case '=':
52
- case '':
53
- return compared === 0;
54
- default:
55
- return undefined;
56
- }
57
- }
58
-
59
5
  export function checkVersionRange(contract: string | undefined, version: string): VersionVerdict {
60
6
  const spec = contract?.trim() ?? '';
61
7
  if (spec === '') return 'satisfied';
62
- if (!readable(version)) return 'malformed';
63
- const tokens = spec.split(/\s+/).filter((token) => token !== '');
64
- if (tokens.length === 0) return 'satisfied';
65
- for (const token of tokens) {
66
- const verdict = comparator(token, version);
67
- if (verdict === undefined) return 'malformed';
68
- if (!verdict) return 'unsatisfied';
69
- }
70
- return 'satisfied';
8
+ const range = semver.validRange(spec);
9
+ if (range === null) return 'malformed';
10
+ const parsed = semver.coerce(version);
11
+ if (parsed === null) return 'malformed';
12
+ return semver.satisfies(parsed, range) ? 'satisfied' : 'unsatisfied';
71
13
  }
@@ -1,4 +1,22 @@
1
1
  import { readFileSync } from 'node:fs';
2
+ import semver from 'semver';
3
+
4
+ export function compareVersions(left: string, right: string): number | undefined {
5
+ const a = semver.valid(left.trim());
6
+ const b = semver.valid(right.trim());
7
+ if (a === null || b === null) return undefined;
8
+ return semver.compare(a, b);
9
+ }
10
+
11
+ export function isValidVersion(value: string): boolean {
12
+ return semver.valid(value.trim()) !== null;
13
+ }
14
+
15
+ export function isNewerVersion(latest: string, installed: string): boolean {
16
+ if (latest === installed) return false;
17
+ const compared = compareVersions(latest, installed);
18
+ return compared === undefined ? true : compared > 0;
19
+ }
2
20
 
3
21
  export function runningVersionOf(moduleUrl: string): string | undefined {
4
22
  try {
@@ -10,21 +28,3 @@ export function runningVersionOf(moduleUrl: string): string | undefined {
10
28
  return undefined;
11
29
  }
12
30
  }
13
-
14
- export function compareVersions(a: string, b: string): number | undefined {
15
- const na = a.split('.').map((part) => Number.parseInt(part, 10));
16
- const nb = b.split('.').map((part) => Number.parseInt(part, 10));
17
- if (na.some(Number.isNaN) || nb.some(Number.isNaN)) return undefined;
18
- for (let index = 0; index < Math.max(na.length, nb.length); index += 1) {
19
- const left = na[index] ?? 0;
20
- const right = nb[index] ?? 0;
21
- if (left !== right) return left > right ? 1 : -1;
22
- }
23
- return 0;
24
- }
25
-
26
- export function isNewerVersion(latest: string, installed: string): boolean {
27
- if (latest === installed) return false;
28
- const compared = compareVersions(latest, installed);
29
- return compared === undefined ? true : compared > 0;
30
- }
package/src/host.ts CHANGED
@@ -3,6 +3,10 @@ export * from './api/context-ref.js';
3
3
  export * from './contract/unit.js';
4
4
  export * from './contract/needs.js';
5
5
  export * from './contract/data.js';
6
+ export * from './contract/extension.js';
7
+ export * from './contract/lib.js';
8
+ export * from './contract/git.js';
9
+ export * from './contract/command.js';
6
10
  export * from './contract/unit-board.js';
7
11
  export * from './contract/workbench-contract.js';
8
12
  export * from './contract/blocker.js';
@@ -41,6 +45,7 @@ export * from './host/provision.js';
41
45
  export * from './host/channel-plugin.js';
42
46
  export * from './host/recipe.js';
43
47
  export * from './host/recipe-rules.js';
48
+ export * from './host/release-packages.generated.js';
44
49
  export * from './contract/unit-capability.js';
45
50
  export * from './contract/plugin-source.js';
46
51
  export * from './contract/cli.js';
@@ -63,6 +68,7 @@ export * from './host/patch.js';
63
68
  export * from './host/adapters.js';
64
69
  export * from './host/adapter-set.js';
65
70
  export * from './host/needs.js';
71
+ export * from './host/extension.js';
66
72
  export * from './host/port-types.js';
67
73
  export * from './host/version-range.js';
68
74
  export * from './host/market-index.js';
package/src/i18n/en.ts CHANGED
@@ -277,6 +277,7 @@ export const en: Record<keyof WorkbenchMessages, string> = {
277
277
  'panels.discover.loading': 'Reading the index',
278
278
  'panels.discover.lead': 'Browse modules from an index source; install and go.',
279
279
  'panels.discover.addSource': 'Add',
280
+ 'panels.discover.addingSource': 'Adding…',
280
281
  'panels.discover.addFirstSource': 'Add index source',
281
282
  'panels.discover.removeSource': 'Remove',
282
283
  'panels.discover.removeSourceNamed': 'Remove {url}',
@@ -377,7 +378,6 @@ export const en: Record<keyof WorkbenchMessages, string> = {
377
378
  'panels.discover.tabOverview': 'Overview',
378
379
  'panels.discover.tabReadme': 'Readme',
379
380
  'panels.discover.tabChangelog': 'Changelog',
380
- 'panels.discover.docTitle': '{kind} for {version}',
381
381
  'panels.discover.docReadme': 'readme',
382
382
  'panels.discover.docChangelog': 'changelog',
383
383
  'panels.discover.docLoading': 'Reading…',
@@ -622,6 +622,8 @@ export const en: Record<keyof WorkbenchMessages, string> = {
622
622
  'approval.publish.module': 'Module',
623
623
  'approval.publish.target': 'Target',
624
624
  'approval.publish.version': 'Version',
625
+ 'approval.publish.author': 'Author',
626
+ 'approval.publish.authorMissing': 'unsigned (commits use the local git identity)',
625
627
  'approval.publish.registry': 'Registry',
626
628
  'approval.publish.repo': 'Repository',
627
629
  'approval.publish.branch': 'Branch',