@dsh-workbench/shared 1.1.6 → 1.1.8

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 (37) hide show
  1. package/dist/contract/needs.d.ts +3 -0
  2. package/dist/contract/needs.js +17 -0
  3. package/dist/contract/protocol/market-index.d.ts +1 -0
  4. package/dist/host/lib-runners.d.ts +20 -0
  5. package/dist/host/lib-runners.js +46 -0
  6. package/dist/host/market-index-claim.d.ts +0 -1
  7. package/dist/host/market-index-claim.js +0 -3
  8. package/dist/host/market-index-doc.d.ts +1 -45
  9. package/dist/host/market-index-parse.d.ts +9 -4
  10. package/dist/host/market-index-parse.js +17 -92
  11. package/dist/host/market-index.d.ts +5 -6
  12. package/dist/host/market-index.js +29 -87
  13. package/dist/host/opencli-runner.d.ts +25 -0
  14. package/dist/host/opencli-runner.js +50 -0
  15. package/dist/host.d.ts +2 -0
  16. package/dist/host.js +2 -0
  17. package/dist/i18n/en.js +3 -1
  18. package/dist/i18n/index.d.ts +4 -2
  19. package/dist/i18n/zh.d.ts +3 -1
  20. package/dist/i18n/zh.js +3 -1
  21. package/package.json +1 -1
  22. package/src/contract/__tests__/needs.test.ts +8 -0
  23. package/src/contract/needs.ts +15 -0
  24. package/src/contract/protocol/market-index.ts +1 -0
  25. package/src/host/__tests__/lib-runners.test.ts +82 -0
  26. package/src/host/__tests__/market-index-assemble.test.ts +26 -0
  27. package/src/host/__tests__/market-index.test.ts +41 -51
  28. package/src/host/__tests__/opencli-runner.test.ts +120 -0
  29. package/src/host/lib-runners.ts +70 -0
  30. package/src/host/market-index-claim.ts +0 -4
  31. package/src/host/market-index-doc.ts +1 -53
  32. package/src/host/market-index-parse.ts +21 -85
  33. package/src/host/market-index.ts +33 -127
  34. package/src/host/opencli-runner.ts +76 -0
  35. package/src/host.ts +2 -0
  36. package/src/i18n/en.ts +3 -1
  37. package/src/i18n/zh.ts +3 -1
@@ -76,12 +76,15 @@ export interface ExtensionNeed extends NeedCommon {
76
76
  }
77
77
  /**
78
78
  * 要在隔离运行时里有一个共用依赖包。
79
+ * `runs` 写它自己会起哪些运行时的命令(如 `["opencli"]`)—— 写了外壳就把那份执行器注入进去
80
+ * (库侧约定 `setOpencliRunner`);不写就按它自己在 PATH 上找。
79
81
  */
80
82
  export interface LibNeed extends NeedCommon {
81
83
  readonly kind: 'lib';
82
84
  readonly name: string;
83
85
  readonly version?: string;
84
86
  readonly sources?: readonly SupplySource[];
87
+ readonly runs?: readonly string[] | undefined;
85
88
  }
86
89
  /**
87
90
  * 跑起来时要有这个环境变量;`secret` 为真时任何地方都不回显它的值。
@@ -22,6 +22,7 @@ export const NEED_KEYS = [
22
22
  'secret',
23
23
  'install',
24
24
  'docs',
25
+ 'runs',
25
26
  ];
26
27
  const SOURCE_KEYS = ['kind', 'package', 'registryEnv', 'registryDefault', 'url', 'ref', 'path', 'env'];
27
28
  const REGISTRY_PATTERN = /^(?:https?|file):\/\//;
@@ -149,6 +150,20 @@ export function binInstallProblemsOf(raw, at) {
149
150
  }
150
151
  return errors;
151
152
  }
153
+ function libRunsProblemsOf(raw, at) {
154
+ if (raw === undefined)
155
+ return [];
156
+ if (!Array.isArray(raw))
157
+ return [`${at}.runs 要是数组`];
158
+ if (raw.length === 0)
159
+ return [`${at}.runs 是空数组 —— 不自己跑命令就整条删掉`];
160
+ const names = raw.map((one) => text(one));
161
+ if (names.some((one) => one === ''))
162
+ return [`${at}.runs 里有空字符串`];
163
+ if (new Set(names).size !== names.length)
164
+ return [`${at}.runs 里有重复的运行时`];
165
+ return [];
166
+ }
152
167
  function perKindProblemsOf(kind, raw, at) {
153
168
  const errors = [];
154
169
  if (kind === 'bin' || kind === 'lib' || kind === 'env') {
@@ -159,6 +174,8 @@ function perKindProblemsOf(kind, raw, at) {
159
174
  errors.push(...binInstallProblemsOf(raw['install'], at));
160
175
  if (raw['docs'] !== undefined && text(raw['docs']) === '')
161
176
  errors.push(`${at}.docs 是空串`);
177
+ if (kind === 'lib')
178
+ errors.push(...libRunsProblemsOf(raw['runs'], at));
162
179
  if (kind === 'extension') {
163
180
  if (text(raw['runtime']) === '')
164
181
  errors.push(`${at}.runtime 没写 —— 扩展属于哪个运行时(如 opencli)`);
@@ -208,6 +208,7 @@ export interface PublishPreview {
208
208
  readonly name: string;
209
209
  readonly version: string;
210
210
  readonly author?: string;
211
+ readonly labels?: readonly string[];
211
212
  readonly registry?: string;
212
213
  readonly market?: MarketPublishDestination;
213
214
  readonly claim?: MarketClaim;
@@ -0,0 +1,20 @@
1
+ import type { LibReader } from '../contract/lib.js';
2
+ import type { OpencliRunner } from './opencli-runner.js';
3
+ export interface LibRuntimes {
4
+ readonly opencli?: OpencliRunner | undefined;
5
+ }
6
+ interface Logger {
7
+ warn?(message: string): void;
8
+ }
9
+ /**
10
+ * 给声明了 `runs` 的 lib 注入「跑这个运行时」的执行器:库侧按约定暴露 `setOpencliRunner`
11
+ * (运行时名字首字母大写 + `Runner`),外壳在每次 `load` 之后把执行器交给它。
12
+ *
13
+ * 没暴露的(版本旧)就保持它自己的行为(按 PATH 找),只记一条 warn;不熟悉的运行时同样只 warn。
14
+ * @param libs 原来的读库口。
15
+ * @param runtimes 外壳手上有的执行器,按运行时名字给。
16
+ * @param logger 记 warn 的地方(默认不记)。
17
+ * @returns 包好的读库口;`load` 的签名与返回不变。
18
+ */
19
+ export declare function withLibRunners(libs: LibReader, runtimes: LibRuntimes, logger?: Logger): LibReader;
20
+ export {};
@@ -0,0 +1,46 @@
1
+ function runnerOf(runtimes, runtime) {
2
+ return runtime === 'opencli' ? runtimes.opencli : undefined;
3
+ }
4
+ function setterNameOf(runtime) {
5
+ return `set${runtime.charAt(0).toUpperCase()}${runtime.slice(1)}Runner`;
6
+ }
7
+ /**
8
+ * 给声明了 `runs` 的 lib 注入「跑这个运行时」的执行器:库侧按约定暴露 `setOpencliRunner`
9
+ * (运行时名字首字母大写 + `Runner`),外壳在每次 `load` 之后把执行器交给它。
10
+ *
11
+ * 没暴露的(版本旧)就保持它自己的行为(按 PATH 找),只记一条 warn;不熟悉的运行时同样只 warn。
12
+ * @param libs 原来的读库口。
13
+ * @param runtimes 外壳手上有的执行器,按运行时名字给。
14
+ * @param logger 记 warn 的地方(默认不记)。
15
+ * @returns 包好的读库口;`load` 的签名与返回不变。
16
+ */
17
+ export function withLibRunners(libs, runtimes, logger) {
18
+ const warned = new Set();
19
+ const warnOnce = (key, message) => {
20
+ if (warned.has(key))
21
+ return;
22
+ warned.add(key);
23
+ logger?.warn?.(message);
24
+ };
25
+ return {
26
+ async load(need, entry) {
27
+ const module = await libs.load(need, entry);
28
+ for (const runtime of need.runs ?? []) {
29
+ const runner = runnerOf(runtimes, runtime);
30
+ if (runner === undefined) {
31
+ warnOnce(`${need.name}:${runtime}:unsupported`, `[dsh-workbench] ${need.name} 声明了自己会跑 ${runtime},但工作台没有这个执行器 —— 它会按自己的方式找`);
32
+ continue;
33
+ }
34
+ const setter = typeof module === 'object' && module !== null
35
+ ? module[setterNameOf(runtime)]
36
+ : undefined;
37
+ if (typeof setter !== 'function') {
38
+ warnOnce(`${need.name}:${entry}:${runtime}:missing`, `[dsh-workbench] ${need.name} 声明了自己会跑 ${runtime},但这一版没有 ${setterNameOf(runtime)} —— 它会按 PATH 找`);
39
+ continue;
40
+ }
41
+ setter.call(module, runner);
42
+ }
43
+ return module;
44
+ },
45
+ };
46
+ }
@@ -15,6 +15,5 @@ export interface MarketClaimInput {
15
15
  }
16
16
  export declare function marketArtifactPath(name: string): string;
17
17
  export declare function marketArtifactRoot(name: string): string;
18
- export declare function marketArtifactPathFor(name: string, version: string): string;
19
18
  export declare function entryUnitOf(entry: MarketIndexEntry): string | undefined;
20
19
  export declare function claimOf(index: MarketIndex, input: MarketClaimInput): MarketClaim;
@@ -45,9 +45,6 @@ export function marketArtifactPath(name) {
45
45
  export function marketArtifactRoot(name) {
46
46
  return `${MARKET_ARTIFACTS_DIR}/${name}`;
47
47
  }
48
- export function marketArtifactPathFor(name, version) {
49
- return `${MARKET_ARTIFACTS_DIR}/${name}/${version}`;
50
- }
51
48
  export function entryUnitOf(entry) {
52
49
  const unit = entry.metadata?.unit?.trim();
53
50
  if (unit !== undefined && unit !== '')
@@ -1,38 +1,4 @@
1
- import type { MarketIndex, MarketIndexEngines, MarketIndexEntryMetadata, MarketIndexProblem, MarketIndexSource, MarketLocalizedText } from '../contract/protocol/market-index.js';
2
- export interface MarketVersionDoc {
3
- readonly name: string;
4
- readonly version: string;
5
- readonly source: MarketIndexSource;
6
- readonly publishTimestamp?: number;
7
- readonly engines?: MarketIndexEngines;
8
- readonly title?: MarketLocalizedText;
9
- readonly description?: MarketLocalizedText;
10
- readonly author?: string;
11
- readonly owner?: string;
12
- readonly homepage?: string;
13
- readonly icon?: string;
14
- readonly metadata?: MarketIndexEntryMetadata;
15
- }
16
- export interface MarketMetaDoc {
17
- readonly name: string;
18
- readonly epoch?: number;
19
- readonly publishTimestamp: number;
20
- readonly title?: MarketLocalizedText;
21
- readonly description?: MarketLocalizedText;
22
- readonly author?: string;
23
- readonly owner?: string;
24
- readonly homepage?: string;
25
- readonly icon?: string;
26
- readonly metadata?: MarketIndexEntryMetadata;
27
- }
28
- export type MarketEntryDoc = MarketVersionDoc | MarketMetaDoc;
29
- export type MarketEntryDocParse = {
30
- readonly ok: true;
31
- readonly doc: MarketEntryDoc;
32
- } | {
33
- readonly ok: false;
34
- readonly problems: readonly MarketIndexProblem[];
35
- };
1
+ import type { MarketIndex, MarketIndexProblem } from '../contract/protocol/market-index.js';
36
2
  export interface MarketEntryFile {
37
3
  readonly path: string;
38
4
  readonly text: string;
@@ -44,13 +10,3 @@ export type MarketIndexAssemble = {
44
10
  readonly ok: false;
45
11
  readonly problems: readonly MarketIndexProblem[];
46
12
  };
47
- export interface MarketDocFields {
48
- readonly name: string;
49
- readonly title?: MarketLocalizedText;
50
- readonly description?: MarketLocalizedText;
51
- readonly author?: string;
52
- readonly owner?: string;
53
- readonly homepage?: string;
54
- readonly icon?: string;
55
- readonly metadata?: MarketIndexEntryMetadata;
56
- }
@@ -1,5 +1,4 @@
1
1
  import type { DiscoverFilter, IndexSourceSpec, MarketIndex, MarketIndexEntry, MarketIndexParse, MarketIndexProblem, MarketIndexVersion, MarketSourceConfig } from '../contract/protocol/market-index.js';
2
- import type { MarketEntryDocParse } from './market-index-doc.js';
3
2
  export declare function compareSemver(left: string, right: string): number | undefined;
4
3
  export declare function httpUrlProblem(url: string): string | undefined;
5
4
  export declare function gitUrlOk(url: string): boolean;
@@ -18,6 +17,12 @@ export type SourceConfigParse = {
18
17
  readonly problem: MarketIndexProblem;
19
18
  };
20
19
  export declare function parseSourceConfig(raw: unknown): SourceConfigParse;
21
- export declare function parseMarketEntryDoc(raw: unknown): MarketEntryDocParse;
22
- export declare function marketEntryPath(dir: string, name: string, version: string): string;
23
- export declare function marketMetaPath(dir: string, name: string, epoch: number, suffix: string): string;
20
+ export declare function marketEntryPath(dir: string, name: string): string;
21
+ export type MarketEntryParse = {
22
+ readonly ok: true;
23
+ readonly entry: MarketIndexEntry;
24
+ } | {
25
+ readonly ok: false;
26
+ readonly problems: readonly MarketIndexProblem[];
27
+ };
28
+ export declare function parseMarketEntryJson(text: string): MarketEntryParse;
@@ -384,102 +384,27 @@ export function parseSourceConfig(raw) {
384
384
  },
385
385
  };
386
386
  }
387
- function docFieldsOf(record, at) {
388
- const name = asText(record['name']);
389
- if (name === '' || name.length > 214 || !NPM_NAME.test(name)) {
390
- return problem('name', `不是合法 npm 包名:${JSON.stringify(name)}`, at);
391
- }
392
- const icon = asText(record['icon']);
393
- if (icon !== '') {
394
- const badIcon = iconProblem(icon);
395
- if (badIcon !== undefined)
396
- return badIcon;
397
- }
398
- const owner = asText(record['owner']);
399
- if (owner !== '') {
400
- const badOwner = ownerProblem(owner);
401
- if (badOwner !== undefined)
402
- return badOwner;
403
- }
404
- const metadata = metadataOf(record['metadata']);
405
- if (metadata?.unit !== undefined && !UNIT_ID.test(metadata.unit)) {
406
- return problem('unit', `metadata.unit 只能是小写字母开头的 a-z0-9-(现在是 ${JSON.stringify(metadata.unit)})`, at);
407
- }
408
- const title = asLocalized(record['title']);
409
- const description = asLocalized(record['description']);
410
- const author = asText(record['author']);
411
- const homepage = asText(record['homepage']);
412
- return {
413
- name,
414
- ...(title === undefined ? {} : { title }),
415
- ...(description === undefined ? {} : { description }),
416
- ...(author === '' ? {} : { author }),
417
- ...(owner === '' ? {} : { owner }),
418
- ...(homepage === '' ? {} : { homepage }),
419
- ...(icon === '' ? {} : { icon }),
420
- ...(metadata === undefined ? {} : { metadata }),
421
- };
422
- }
423
- export function parseMarketEntryDoc(raw) {
424
- const record = asRecord(raw);
425
- if (record === undefined)
426
- return { ok: false, problems: [problem('entry', '条目文件必须是一个 JSON 对象')] };
427
- const fields = docFieldsOf(record, 'entry');
428
- if ('kind' in fields)
429
- return { ok: false, problems: [fields] };
430
- const version = asText(record['version']);
431
- const timestamp = record['publishTimestamp'];
432
- if (timestamp !== undefined && (!Number.isInteger(timestamp) || timestamp < 0)) {
433
- return { ok: false, problems: [problem('timestamp', 'publishTimestamp 必须是 Unix 秒整数', 'entry')] };
434
- }
435
- if (version === '') {
436
- if (timestamp === undefined) {
437
- return { ok: false, problems: [problem('timestamp', '元数据文件要有 publishTimestamp', 'entry')] };
438
- }
439
- const epoch = record['epoch'];
440
- if (epoch !== undefined && (!Number.isInteger(epoch) || epoch < 0)) {
441
- return { ok: false, problems: [problem('timestamp', 'epoch 必须是非负整数', 'entry')] };
442
- }
443
- return {
444
- ok: true,
445
- doc: { ...fields, publishTimestamp: timestamp, ...(epoch === undefined ? {} : { epoch: epoch }) },
446
- };
447
- }
448
- if (!isValidVersion(version)) {
449
- return { ok: false, problems: [problem('version', `版本号不是合法 SemVer:${JSON.stringify(version)}`, 'entry')] };
450
- }
451
- const source = sourceOf(record['source'], 'entry.source');
452
- if ('kind' in source)
453
- return { ok: false, problems: [source] };
454
- const engines = asRecord(record['engines']);
455
- const dsh = engines === undefined ? '' : asText(engines['dsh']);
456
- return {
457
- ok: true,
458
- doc: {
459
- ...fields,
460
- version,
461
- source,
462
- ...(timestamp === undefined ? {} : { publishTimestamp: timestamp }),
463
- ...(dsh === '' ? {} : { engines: { dsh } }),
464
- },
465
- };
466
- }
467
387
  function indexPathBase(dir) {
468
388
  return dir.replace(/^\/+|\/+$/g, '') || MARKET_INDEX_DIR;
469
389
  }
470
- export function marketEntryPath(dir, name, version) {
390
+ export function marketEntryPath(dir, name) {
471
391
  if (!NPM_NAME.test(name))
472
392
  throw new Error(`不是合法包名:${JSON.stringify(name)}`);
473
- if (!isValidVersion(version))
474
- throw new Error(`不是合法版本号:${JSON.stringify(version)}`);
475
- return `${indexPathBase(dir)}/${name}/${version}.json`;
393
+ return `${indexPathBase(dir)}/${name}.json`;
476
394
  }
477
- export function marketMetaPath(dir, name, epoch, suffix) {
478
- if (!NPM_NAME.test(name))
479
- throw new Error(`不是合法包名:${JSON.stringify(name)}`);
480
- if (!Number.isInteger(epoch) || epoch < 0)
481
- throw new Error(`epoch 不合法:${String(epoch)}`);
482
- if (!/^[a-z0-9]{4,16}$/u.test(suffix))
483
- throw new Error(`后缀不合法:${suffix}`);
484
- return `${indexPathBase(dir)}/${name}/meta-${String(epoch)}-${suffix}.json`;
395
+ export function parseMarketEntryJson(text) {
396
+ let raw;
397
+ try {
398
+ raw = JSON.parse(text);
399
+ }
400
+ catch (error) {
401
+ return {
402
+ ok: false,
403
+ problems: [problem('not-json', `JSON 解析失败:${error instanceof Error ? error.message : String(error)}`, 'entry')],
404
+ };
405
+ }
406
+ const parsed = entryOf(raw, 'entry');
407
+ if ('kind' in parsed)
408
+ return { ok: false, problems: [parsed] };
409
+ return { ok: true, entry: parsed };
485
410
  }
@@ -1,14 +1,13 @@
1
- import type { MarketIndex, MarketIndexEntry, MarketIndexVersion } from '../contract/protocol/market-index.js';
2
- import type { MarketEntryFile, MarketIndexAssemble, MarketMetaDoc, MarketVersionDoc } from './market-index-doc.js';
1
+ import type { MarketIndex, MarketIndexEntry } from '../contract/protocol/market-index.js';
2
+ import type { MarketEntryFile, MarketIndexAssemble } from './market-index-doc.js';
3
3
  export declare const MARKET_ENTRIES_README_LINES: readonly string[];
4
4
  export declare const MARKET_INDEX_README_LINES: readonly string[];
5
5
  export { marketBranchOk, marketInstallTarget, marketText } from '../lib/market-index.js';
6
- export type { MarketEntryDoc, MarketEntryDocParse, MarketEntryFile, MarketIndexAssemble, MarketMetaDoc, MarketVersionDoc, } from './market-index-doc.js';
7
- export { compareSemver, filterMarketIndex, gitUrlOk, httpUrlProblem, marketEntryLabels, marketEntryPath, marketMetaPath, marketSourceKey, parseMarketEntryDoc, parseMarketIndex, parseMarketIndexJson, parseSourceConfig, sortMarketVersions, upsertMarketEntry, } from './market-index-parse.js';
6
+ export type { MarketEntryFile, MarketIndexAssemble } from './market-index-doc.js';
7
+ export { compareSemver, filterMarketIndex, gitUrlOk, httpUrlProblem, marketEntryLabels, marketEntryPath, marketSourceKey, parseMarketEntryJson, parseMarketIndex, parseMarketIndexJson, parseSourceConfig, sortMarketVersions, upsertMarketEntry, } from './market-index-parse.js';
8
8
  export type { SourceConfigParse } from './market-index-parse.js';
9
- export declare function marketVersionDocOf(entry: MarketIndexEntry, version: MarketIndexVersion): MarketVersionDoc;
10
- export declare function marketMetaDocOf(entry: MarketIndexEntry, epoch: number, at: number): MarketMetaDoc;
11
9
  export declare function assembleMarketIndex(files: readonly MarketEntryFile[]): MarketIndexAssemble;
10
+ export declare function marketEntryFileOf(entry: MarketIndexEntry, dir?: string): MarketEntryFile;
12
11
  export declare function marketEntryFilesOf(index: MarketIndex, dir?: string): readonly MarketEntryFile[];
13
12
  export declare function collectMarketEntryFiles(dir: string): readonly MarketEntryFile[];
14
13
  export declare function marketEntryFilesOfLegacy(text: string, dir?: string): readonly MarketEntryFile[] | undefined;
@@ -1,18 +1,22 @@
1
1
  import { existsSync, readdirSync, readFileSync } from 'node:fs';
2
2
  import { join, relative, sep } from 'node:path';
3
3
  import { MARKET_INDEX_DIR, MARKET_INDEX_FORMAT } from '../contract/protocol/market-index.js';
4
- import { marketEntryPath, parseMarketEntryDoc, parseMarketIndexJson, sortMarketVersions, } from './market-index-parse.js';
4
+ import { entryUnitOf } from './market-index-claim.js';
5
+ import { marketEntryPath, parseMarketEntryJson, parseMarketIndexJson, sortMarketVersions } from './market-index-parse.js';
5
6
  export const MARKET_ENTRIES_README_LINES = [
6
7
  '# 索引目录',
7
8
  '',
8
- '一个包一个目录,一个版本一个文件:`market/<包名>/<版本>.json`;产物在 `artifacts/<包名>/<版本>/`。',
9
+ '一个包一个文件:`market/<包名>.json`,里面是这一个包的元数据与所有版本;',
9
10
  '',
10
- '`meta-<epoch>-<随机后缀>.json` 是只改标题 / 署名 / 说明时追加的元数据快照,读取时按 epoch 取最新的一份。',
11
+ '产物在 `artifacts/<包名>/`(只放最新一版,老版本按条目里的 commit 取历史)。',
12
+ '',
13
+ '发布时只改自己包的文件;包与包之间不共享文件。',
11
14
  ];
12
15
  export const MARKET_INDEX_README_LINES = [
13
16
  '# 模块市场索引',
14
17
  '',
15
- '这个仓库给 DeepSeek Harness 工作台当模块市场索引:条目在 `market/<包名>/<版本>.json`,产物在 `artifacts/<包名>/<版本>/`。',
18
+ '这个仓库给 DeepSeek Harness 工作台当模块市场索引:一个包一个文件 `market/<包名>.json`,',
19
+ '产物在 `artifacts/<包名>/`;老仓库的 `market.json` 会在第一次发布时自动拆开。',
16
20
  '',
17
21
  '## 发布模块',
18
22
  '',
@@ -22,8 +26,7 @@ export const MARKET_INDEX_README_LINES = [
22
26
  'dsh-wb-module release --index <本仓库地址> --index-policy artifacts-in-repo',
23
27
  '```',
24
28
  '',
25
- '产物会提交到 `artifacts/<包名>/<版本>/`,条目提交到 `market/<包名>/<版本>.json`。',
26
- '发布只新增文件、不改已有文件,两台机器同时发也不会冲突;老条目的 `packages/<包名>/` 路径继续可用。',
29
+ '发布只会改自己包的那一个文件;包与包之间不会碰同一个文件。',
27
30
  '',
28
31
  '一个包名归第一次发布的人(条目文件里的 `owner`)。换一个署名发同名会被拒;要换人,先把这一条的 `owner` 改成新的人。',
29
32
  '同一个市场里,一个模块 id(`metadata.unit`)只能被一个条目使用;另一个包名想用同一个 id / 产物目录也会被拒。',
@@ -34,107 +37,46 @@ export const MARKET_INDEX_README_LINES = [
34
37
  '',
35
38
  ];
36
39
  export { marketBranchOk, marketInstallTarget, marketText } from '../lib/market-index.js';
37
- export { compareSemver, filterMarketIndex, gitUrlOk, httpUrlProblem, marketEntryLabels, marketEntryPath, marketMetaPath, marketSourceKey, parseMarketEntryDoc, parseMarketIndex, parseMarketIndexJson, parseSourceConfig, sortMarketVersions, upsertMarketEntry, } from './market-index-parse.js';
40
+ export { compareSemver, filterMarketIndex, gitUrlOk, httpUrlProblem, marketEntryLabels, marketEntryPath, marketSourceKey, parseMarketEntryJson, parseMarketIndex, parseMarketIndexJson, parseSourceConfig, sortMarketVersions, upsertMarketEntry, } from './market-index-parse.js';
38
41
  function problem(kind, detail, at) {
39
42
  return { kind, detail, ...(at === undefined ? {} : { at }) };
40
43
  }
41
- function docFieldsFromEntry(entry) {
42
- return {
43
- name: entry.name,
44
- ...(entry.title === undefined ? {} : { title: entry.title }),
45
- ...(entry.description === undefined ? {} : { description: entry.description }),
46
- ...(entry.author === undefined ? {} : { author: entry.author }),
47
- ...(entry.owner === undefined ? {} : { owner: entry.owner }),
48
- ...(entry.homepage === undefined ? {} : { homepage: entry.homepage }),
49
- ...(entry.icon === undefined ? {} : { icon: entry.icon }),
50
- ...(entry.metadata === undefined ? {} : { metadata: entry.metadata }),
51
- };
52
- }
53
- export function marketVersionDocOf(entry, version) {
54
- return { ...docFieldsFromEntry(entry), ...version };
55
- }
56
- export function marketMetaDocOf(entry, epoch, at) {
57
- return { ...docFieldsFromEntry(entry), epoch, publishTimestamp: at };
58
- }
59
44
  export function assembleMarketIndex(files) {
60
45
  const problems = [];
61
- const groups = new Map();
46
+ const entries = new Map();
62
47
  const ordered = [...files].sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0));
63
48
  for (const file of ordered) {
64
- let raw;
65
- try {
66
- raw = JSON.parse(file.text);
67
- }
68
- catch (error) {
69
- problems.push(problem('not-json', `JSON 解析失败:${error instanceof Error ? error.message : String(error)}`, file.path));
49
+ if (!file.path.endsWith('.json'))
70
50
  continue;
71
- }
72
- const parsed = parseMarketEntryDoc(raw);
51
+ const parsed = parseMarketEntryJson(file.text);
73
52
  if (!parsed.ok) {
74
53
  for (const one of parsed.problems)
75
54
  problems.push({ ...one, at: file.path });
76
55
  continue;
77
56
  }
78
- const doc = parsed.doc;
79
- const group = groups.get(doc.name) ?? { versions: new Map(), meta: undefined, fallback: undefined };
80
- const at = doc.publishTimestamp ?? 0;
81
- if ('version' in doc) {
82
- if (group.versions.has(doc.version)) {
83
- problems.push(problem('duplicate-version', `版本重复:${doc.version}`, file.path));
84
- }
85
- group.versions.set(doc.version, {
86
- version: doc.version,
87
- source: doc.source,
88
- ...(doc.publishTimestamp === undefined ? {} : { publishTimestamp: doc.publishTimestamp }),
89
- ...(doc.engines === undefined ? {} : { engines: doc.engines }),
90
- });
91
- const newerFallback = group.fallback === undefined ||
92
- at > group.fallback.at ||
93
- (at === group.fallback.at && file.path > group.fallback.path);
94
- if (newerFallback)
95
- group.fallback = { at, path: file.path, doc };
96
- }
97
- else {
98
- const epoch = doc.epoch ?? 0;
99
- const newer = group.meta === undefined ||
100
- epoch > group.meta.epoch ||
101
- (epoch === group.meta.epoch && at > group.meta.at) ||
102
- (epoch === group.meta.epoch && at === group.meta.at && file.path > group.meta.path);
103
- if (newer)
104
- group.meta = { epoch, at, path: file.path, doc };
57
+ const entry = parsed.entry;
58
+ if (entries.has(entry.name)) {
59
+ problems.push(problem('duplicate-name', `插件名重复:${entry.name}`, file.path));
60
+ continue;
105
61
  }
106
- groups.set(doc.name, group);
62
+ entries.set(entry.name, { ...entry, versions: sortMarketVersions(entry.versions) });
107
63
  }
108
64
  if (problems.length > 0)
109
65
  return { ok: false, problems };
110
- const plugins = [...groups.entries()].map(([name, group]) => {
111
- const meta = group.meta?.doc ?? group.fallback?.doc;
112
- return {
113
- name,
114
- versions: sortMarketVersions([...group.versions.values()]),
115
- ...(meta?.title === undefined ? {} : { title: meta.title }),
116
- ...(meta?.description === undefined ? {} : { description: meta.description }),
117
- ...(meta?.author === undefined ? {} : { author: meta.author }),
118
- ...(meta?.owner === undefined ? {} : { owner: meta.owner }),
119
- ...(meta?.homepage === undefined ? {} : { homepage: meta.homepage }),
120
- ...(meta?.icon === undefined ? {} : { icon: meta.icon }),
121
- ...(meta?.metadata === undefined ? {} : { metadata: meta.metadata }),
122
- };
123
- });
124
- plugins.sort((left, right) => (left.name < right.name ? -1 : left.name > right.name ? 1 : 0));
66
+ const plugins = [...entries.values()].sort((left, right) => (left.name < right.name ? -1 : left.name > right.name ? 1 : 0));
125
67
  return { ok: true, index: { format: MARKET_INDEX_FORMAT, plugins } };
126
68
  }
69
+ export function marketEntryFileOf(entry, dir = MARKET_INDEX_DIR) {
70
+ return { path: marketEntryPath(dir, entry.name), text: `${JSON.stringify(entry, null, 2)}\n` };
71
+ }
127
72
  export function marketEntryFilesOf(index, dir = MARKET_INDEX_DIR) {
128
- const files = [];
129
- for (const entry of index.plugins) {
130
- for (const version of entry.versions) {
131
- files.push({
132
- path: marketEntryPath(dir, entry.name, version.version),
133
- text: `${JSON.stringify(marketVersionDocOf(entry, version), null, 2)}\n`,
134
- });
135
- }
136
- }
137
- return files;
73
+ return index.plugins.map((entry) => marketEntryFileOf(healedUnit(entry), dir));
74
+ }
75
+ function healedUnit(entry) {
76
+ if (entry.metadata?.unit !== undefined)
77
+ return entry;
78
+ const unit = entryUnitOf(entry);
79
+ return unit === undefined ? entry : { ...entry, metadata: { ...entry.metadata, unit } };
138
80
  }
139
81
  export function collectMarketEntryFiles(dir) {
140
82
  if (!existsSync(dir))
@@ -0,0 +1,25 @@
1
+ import type { DataRequest, DataResult } from '../contract/data.js';
2
+ /**
3
+ * 一条 opencli 命令的执行器:同一形状的 `execFile('opencli', args)`,结果只留 stdout。
4
+ */
5
+ export type OpencliRunner = (args: readonly string[]) => Promise<{
6
+ readonly stdout: string;
7
+ }>;
8
+ /**
9
+ * 读缝(`DataReader.read`)的形状;模块直接把自己的 `data.read` 传进来即可。
10
+ */
11
+ export type OpencliRead = (request: DataRequest) => Promise<DataResult<unknown>>;
12
+ export interface OpencliRunnerOptions {
13
+ readonly timeoutMs?: number;
14
+ }
15
+ /**
16
+ * 把读缝包成一条 opencli 命令的执行器:给还按 `execFile('opencli', args)` 收结果的编排库用,
17
+ * 让它们跑在面板用的那份 opencli 上(同一套插件、同一份登录态)。
18
+ *
19
+ * 失败按 opencli 自己的约定还原:未登录是 `code: AUTH_REQUIRED` + 退出码 77,编排库的
20
+ * 「等登录再重试」才接得上;其它失败保留 `Command failed: opencli …` 那句外壳。
21
+ * @param read 模块手里的读缝(`data.read`)。
22
+ * @param options `timeoutMs` 透传给读缝;不传就用读缝自己的默认值。
23
+ * @returns 执行器;直接交给编排库的 `setOpencliRunner`。
24
+ */
25
+ export declare function opencliRunnerOf(read: OpencliRead, options?: OpencliRunnerOptions): OpencliRunner;
@@ -0,0 +1,50 @@
1
+ const AUTH_REQUIRED_EXIT_CODE = 77;
2
+ function textOf(value) {
3
+ return typeof value === 'string' ? value : '';
4
+ }
5
+ function failedOf(args, error) {
6
+ const head = `Command failed: opencli ${args.join(' ')}`;
7
+ const auth = error?.pageCode === 'auth-required';
8
+ const helpUrl = textOf(error?.helpUrl);
9
+ const detail = textOf(error?.detail);
10
+ const hint = textOf(error?.hint);
11
+ const failed = new Error([
12
+ head,
13
+ ...(auth
14
+ ? ['code: AUTH_REQUIRED', `message: ${textOf(error?.message)}`]
15
+ : [textOf(error?.message) || 'opencli 这条命令没跑通']),
16
+ ...(detail === '' ? [] : [detail]),
17
+ ...(hint === '' ? [] : [hint]),
18
+ ...(auth && helpUrl !== ''
19
+ ? [`help: Please open Chrome or Chromium and log in to ${helpUrl}`]
20
+ : []),
21
+ ].join('\n'));
22
+ if (auth)
23
+ failed.code = AUTH_REQUIRED_EXIT_CODE;
24
+ return failed;
25
+ }
26
+ /**
27
+ * 把读缝包成一条 opencli 命令的执行器:给还按 `execFile('opencli', args)` 收结果的编排库用,
28
+ * 让它们跑在面板用的那份 opencli 上(同一套插件、同一份登录态)。
29
+ *
30
+ * 失败按 opencli 自己的约定还原:未登录是 `code: AUTH_REQUIRED` + 退出码 77,编排库的
31
+ * 「等登录再重试」才接得上;其它失败保留 `Command failed: opencli …` 那句外壳。
32
+ * @param read 模块手里的读缝(`data.read`)。
33
+ * @param options `timeoutMs` 透传给读缝;不传就用读缝自己的默认值。
34
+ * @returns 执行器;直接交给编排库的 `setOpencliRunner`。
35
+ */
36
+ export function opencliRunnerOf(read, options = {}) {
37
+ return async (args) => {
38
+ const result = await read({
39
+ kind: 'command',
40
+ adapter: 'opencli',
41
+ argv: [...args],
42
+ ...(options.timeoutMs === undefined
43
+ ? {}
44
+ : { timeoutMs: options.timeoutMs }),
45
+ });
46
+ if (result.ok)
47
+ return { stdout: JSON.stringify(result.data) };
48
+ throw failedOf(args, result.error);
49
+ };
50
+ }
package/dist/host.d.ts CHANGED
@@ -34,6 +34,8 @@ export * from './host/tokens.js';
34
34
  export * from './host/tool-approval.js';
35
35
  export * from './host/tool-kit.js';
36
36
  export * from './host/unit-face.js';
37
+ export * from './host/opencli-runner.js';
38
+ export * from './host/lib-runners.js';
37
39
  export * from './host/value-handoff.js';
38
40
  export * from './host/workspace-store.js';
39
41
  export * from './host/write-result.js';
package/dist/host.js CHANGED
@@ -34,6 +34,8 @@ export * from './host/tokens.js';
34
34
  export * from './host/tool-approval.js';
35
35
  export * from './host/tool-kit.js';
36
36
  export * from './host/unit-face.js';
37
+ export * from './host/opencli-runner.js';
38
+ export * from './host/lib-runners.js';
37
39
  export * from './host/value-handoff.js';
38
40
  export * from './host/workspace-store.js';
39
41
  export * from './host/write-result.js';
package/dist/i18n/en.js CHANGED
@@ -372,7 +372,7 @@ export const en = {
372
372
  'panels.discover.missingIndex': 'This repository has no {path} yet.',
373
373
  'panels.discover.initTitle': 'Initialize this repository as a module market index?',
374
374
  'panels.discover.initCta': 'Initialize market index',
375
- 'panels.discover.initWhat': 'It will write the index directory market/ and README.md on {branch}, commit once, and push. Modules you publish from this workbench will commit their build files under artifacts/<package-name>/<version>/ and their entry under market/<package-name>/<version>.json. The repository stays owned by its owner.',
375
+ 'panels.discover.initWhat': 'It will write the index directory market/ and README.md on {branch}, commit once, and push. Modules you publish from this workbench will commit their build files under artifacts/<package-name>/ and their entry in market/<package-name>.json. The repository stays owned by its owner.',
376
376
  'panels.discover.initBranch': 'Branch',
377
377
  'panels.discover.initBranchHint': 'The index and build files go to this branch; an empty repository gets it created.',
378
378
  'panels.discover.initBranchBad': 'Use only letters, digits, dots, underscores, slashes, or dashes in a branch name.',
@@ -703,6 +703,8 @@ export const en = {
703
703
  'approval.publish.version': 'Version',
704
704
  'approval.publish.author': 'Author',
705
705
  'approval.publish.authorMissing': 'unsigned',
706
+ 'approval.publish.labels': 'Labels',
707
+ 'approval.publish.labelsMissing': 'none yet (1–2 picked on publish)',
706
708
  'approval.publish.owner': 'Owner',
707
709
  'approval.publish.ownerUnowned': 'originally unattributed',
708
710
  'approval.publish.ownerTaken': 'Owned by {owner}',