@dsh-workbench/shared 1.1.1 → 1.1.3

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.
@@ -2,6 +2,7 @@ export const MARKET_INDEX_FORMAT = 1;
2
2
  export const MARKET_INDEX_PATH = 'market.json';
3
3
  export const MARKET_DISCOVER_ROUTE = 'panels/market-discover';
4
4
  export const MARKET_DOC_ROUTE = 'panels/market-doc';
5
+ export const MARKET_GIT_PROBE_ROUTE = 'panels/git-probe';
5
6
  export const MARKET_PUBLISH_PREVIEW_ROUTE = 'panels/market-publish-preview';
6
7
  export const MARKET_SOURCES_RESOURCE = 'market-sources';
7
8
  export const MARKET_SOURCES_ROUTE = 'panels/market-sources';
@@ -9,6 +10,13 @@ export const MARKET_WORKBENCH_LABEL = 'workbench';
9
10
 
10
11
  export type MarketSourceKind = 'http' | 'git';
11
12
 
13
+ export type GitProbeStatus = 'ok' | 'auth' | 'unreachable';
14
+
15
+ export interface GitProbePayload {
16
+ readonly status: GitProbeStatus;
17
+ readonly detail?: string;
18
+ }
19
+
12
20
  export type MarketSourcePolicy = 'artifacts-in-repo' | 'catalog-only';
13
21
 
14
22
  export interface IndexSourceSpec {
@@ -56,6 +64,8 @@ export interface MarketIndexVersion {
56
64
  }
57
65
 
58
66
  export interface MarketIndexEntryMetadata {
67
+ readonly workbench?: boolean;
68
+ readonly unit?: string;
59
69
  readonly category?: string;
60
70
  readonly labels?: readonly string[];
61
71
  readonly keywords?: readonly string[];
@@ -67,12 +77,22 @@ export interface MarketIndexEntry {
67
77
  readonly title?: MarketLocalizedText;
68
78
  readonly description?: MarketLocalizedText;
69
79
  readonly author?: string;
80
+ readonly owner?: string;
70
81
  readonly homepage?: string;
71
82
  readonly icon?: string;
72
83
  readonly metadata?: MarketIndexEntryMetadata;
73
84
  readonly versions: readonly MarketIndexVersion[];
74
85
  }
75
86
 
87
+ export type MarketClaim =
88
+ | { readonly state: 'new' }
89
+ | { readonly state: 'owned'; readonly owner: string }
90
+ | { readonly state: 'unowned' }
91
+ | { readonly state: 'conflict'; readonly owner: string }
92
+ | { readonly state: 'unit-taken'; readonly entry: string }
93
+ | { readonly state: 'path-taken'; readonly entry: string }
94
+ | { readonly state: 'unknown' };
95
+
76
96
  export interface MarketIndexCategory {
77
97
  readonly title?: MarketLocalizedText;
78
98
  }
@@ -114,7 +134,9 @@ export type MarketIndexProblemKind =
114
134
  | 'source'
115
135
  | 'git-commit'
116
136
  | 'timestamp'
117
- | 'icon';
137
+ | 'icon'
138
+ | 'owner'
139
+ | 'unit';
118
140
 
119
141
  export interface MarketIndexProblem {
120
142
  readonly kind: MarketIndexProblemKind;
@@ -213,6 +235,7 @@ export interface PublishChangelog {
213
235
  readonly version: string;
214
236
  readonly text?: string;
215
237
  readonly missing?: boolean;
238
+ readonly pending?: boolean;
216
239
  }
217
240
 
218
241
  export interface PublishPreview {
@@ -221,6 +244,7 @@ export interface PublishPreview {
221
244
  readonly author?: string;
222
245
  readonly registry?: string;
223
246
  readonly market?: MarketPublishDestination;
247
+ readonly claim?: MarketClaim;
224
248
  readonly changelog?: PublishChangelog;
225
249
  }
226
250
 
@@ -245,4 +269,5 @@ export type MarketSourcesAction =
245
269
  | { readonly action: 'save'; readonly source: MarketSourceConfig; readonly select?: boolean }
246
270
  | { readonly action: 'remove'; readonly id: string }
247
271
  | { readonly action: 'select'; readonly id: string | null }
248
- | { readonly action: 'init'; readonly id: string; readonly ref?: string };
272
+ | { readonly action: 'init'; readonly id: string; readonly ref?: string; readonly stampOnly?: boolean }
273
+ | { readonly action: 'read-only'; readonly id: string };
@@ -6,6 +6,7 @@ import {
6
6
  changelogSectionOf,
7
7
  changelogSkeleton,
8
8
  parseChangelog,
9
+ planChangelog,
9
10
  } from '../changelog.js';
10
11
 
11
12
  const SAMPLE = [
@@ -100,3 +101,35 @@ describe('changelogSkeleton', () => {
100
101
  expect(changelogSkeleton()).toBe('# 更新日志\n');
101
102
  });
102
103
  });
104
+
105
+ describe('planChangelog', () => {
106
+ it('这一版已有内容就 kept,不追加也不改原文', () => {
107
+ const plan = planChangelog(SAMPLE, { version: '1.2.0', kind: 'fixed', note: '又修了一条' });
108
+ expect(plan?.action).toBe('kept');
109
+ expect(plan?.text).toBe(SAMPLE);
110
+ expect(plan?.section).toContain('模块详情能看说明与更新日志');
111
+ expect(plan?.section).not.toContain('又修了一条');
112
+ });
113
+
114
+ it('缺这一版且有 note 就 written:空文件从骨架起,段里就是这条', () => {
115
+ const plan = planChangelog('', { version: '0.1.0', kind: 'added', note: '第一次发布', date: '2026-10-27' });
116
+ expect(plan?.action).toBe('written');
117
+ expect(plan?.version).toBe('0.1.0');
118
+ expect(plan?.text.startsWith('# 更新日志')).toBe(true);
119
+ expect(plan?.section).toContain('- 第一次发布');
120
+ expect(changelogHasEntry(plan?.text ?? '', '0.1.0')).toBe(true);
121
+ });
122
+
123
+ it('缺这一版又没有 note 就 undefined(发布前先要一句话)', () => {
124
+ expect(planChangelog(SAMPLE, { version: '9.9.9', note: ' ' })).toBeUndefined();
125
+ expect(planChangelog('', { version: '9.9.9', note: '' })).toBeUndefined();
126
+ });
127
+
128
+ it('只有版本标题没有条目时算缺,补进去而不是再开一段', () => {
129
+ const empty = '# 更新日志\n\n## v0.2.0\n\n`2026-10-26`\n';
130
+ const plan = planChangelog(empty, { version: '0.2.0', kind: 'fixed', note: '修一条' });
131
+ expect(plan?.action).toBe('written');
132
+ expect(parseChangelog(plan?.text ?? '').filter((one) => one.version === '0.2.0')).toHaveLength(1);
133
+ expect(plan?.section).toContain('- 修一条');
134
+ });
135
+ });
@@ -1,5 +1,6 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
  import type { MarketIndex, MarketIndexEntry } from '../../contract/protocol/market-index.js';
3
+ import { claimOf, claimOfEntry, entryOwnerOf, entryUnitOf, marketArtifactPath, samePublisher } from '../market-index-claim.js';
3
4
  import {
4
5
  filterMarketIndex,
5
6
  marketInstallTarget,
@@ -39,6 +40,13 @@ describe('parseMarketIndex', () => {
39
40
  expect(parsed.index.title).toEqual({ zh: '工作台模块' });
40
41
  });
41
42
 
43
+ it('★ metadata.workbench 是身份标记,labels 可以为空(不再靠 workbench 标签)', () => {
44
+ const parsed = parseMarketIndex(indexOf([entry({ metadata: { workbench: true } })]));
45
+ expect(parsed.ok).toBe(true);
46
+ if (!parsed.ok) return;
47
+ expect(parsed.index.plugins[0]?.metadata).toEqual({ workbench: true });
48
+ });
49
+
42
50
  it('format 不是 1 就整份拒', () => {
43
51
  const parsed = parseMarketIndex({ format: 2, plugins: [] });
44
52
  expect(parsed.ok).toBe(false);
@@ -78,6 +86,31 @@ describe('parseMarketIndex', () => {
78
86
  if (parsed.ok) return;
79
87
  expect(parsed.problems[0]?.kind).toBe('not-json');
80
88
  });
89
+
90
+ it('收下 owner,原样写回', () => {
91
+ const parsed = parseMarketIndex(indexOf([entry({ owner: '陈康 <kang@example.com>' })]));
92
+ expect(parsed.ok).toBe(true);
93
+ if (!parsed.ok) return;
94
+ expect(parsed.index.plugins[0]?.owner).toBe('陈康 <kang@example.com>');
95
+ });
96
+
97
+ it('owner 只收一行:带换行整份拒', () => {
98
+ const parsed = parseMarketIndex(indexOf([entry({ owner: '陈康\n<b>' })]));
99
+ expect(parsed.ok).toBe(false);
100
+ if (parsed.ok) return;
101
+ expect(parsed.problems[0]?.kind).toBe('owner');
102
+ });
103
+
104
+ it('收下 metadata.unit;不是合法模块 id 整份拒', () => {
105
+ const parsed = parseMarketIndex(indexOf([entry({ metadata: { unit: 'inventory' } })]));
106
+ expect(parsed.ok).toBe(true);
107
+ if (!parsed.ok) return;
108
+ expect(parsed.index.plugins[0]?.metadata?.unit).toBe('inventory');
109
+ const bad = parseMarketIndex(indexOf([entry({ metadata: { unit: 'Not A Unit' } })]));
110
+ expect(bad.ok).toBe(false);
111
+ if (bad.ok) return;
112
+ expect(bad.problems[0]?.kind).toBe('unit');
113
+ });
81
114
  });
82
115
 
83
116
  describe('upsertMarketEntry', () => {
@@ -101,6 +134,139 @@ describe('upsertMarketEntry', () => {
101
134
  expect(next.plugins[0]?.versions).toHaveLength(1);
102
135
  expect(next.plugins[0]?.versions[0]?.source.type).toBe('git');
103
136
  });
137
+
138
+ it('已有 owner 就翻不过来:后来的条目只加版本', () => {
139
+ const owned = upsertMarketEntry(
140
+ { format: 1, plugins: [entry({ owner: '甲 <a@example.com>' })] },
141
+ entry({ owner: '乙 <b@example.com>', versions: [{ version: '0.2.0', source: { type: 'npm' } }] }),
142
+ );
143
+ expect(owned.plugins[0]?.owner).toBe('甲 <a@example.com>');
144
+ expect(owned.plugins[0]?.versions[0]?.version).toBe('0.2.0');
145
+ });
146
+ });
147
+
148
+ describe('认领', () => {
149
+ const owned = entry({ owner: '陈康 <kang@example.com>' });
150
+
151
+ it('没有条目是 new,条目没 owner 也没 author 是 unowned', () => {
152
+ expect(claimOfEntry(undefined, '陈康')).toEqual({ state: 'new' });
153
+ expect(claimOfEntry(entry(), '陈康')).toEqual({ state: 'unowned' });
154
+ });
155
+
156
+ it('邮箱相同就算同一人,大小写不影响', () => {
157
+ expect(samePublisher('陈康 <KANG@example.com>', '陈康 <kang@example.com>')).toBe(true);
158
+ });
159
+
160
+ it('两边都有邮箱时邮箱说了算:姓名不同也算同一人', () => {
161
+ expect(samePublisher('陈康 <kang@example.com>', 'Kang Chen <kang@example.com>')).toBe(true);
162
+ });
163
+
164
+ it('只有一边有邮箱就比姓名:格式漂了也认', () => {
165
+ expect(samePublisher('陈康', '陈康 <kang@example.com>')).toBe(true);
166
+ });
167
+
168
+ it('姓名和邮箱都不同就是冲突', () => {
169
+ expect(claimOfEntry(owned, '乙 <yi@example.com>')).toEqual({
170
+ state: 'conflict',
171
+ owner: '陈康 <kang@example.com>',
172
+ });
173
+ });
174
+
175
+ it('原主自己发是 owned;没给身份时是 unknown', () => {
176
+ expect(claimOfEntry(owned, '陈康 <kang@example.com>')).toEqual({
177
+ state: 'owned',
178
+ owner: '陈康 <kang@example.com>',
179
+ });
180
+ expect(claimOfEntry(owned, '')).toEqual({ state: 'unknown' });
181
+ });
182
+
183
+ it('老条目只有 author:把它当 owner 认', () => {
184
+ const legacy = entry({ author: '陈康 <kang@example.com>' });
185
+ expect(entryOwnerOf(legacy)).toBe('陈康 <kang@example.com>');
186
+ expect(claimOf({ format: 1, plugins: [legacy] }, { name: '@scope/module-x', publisher: '陈康' })).toEqual({
187
+ state: 'owned',
188
+ owner: '陈康 <kang@example.com>',
189
+ });
190
+ });
191
+
192
+ it('别的条目用了同一个 unit:unit-taken;老条目从产物目录认出 unit', () => {
193
+ const other = entry({ name: '@scope/other', owner: '乙', metadata: { unit: 'inventory' } });
194
+ expect(claimOf({ format: 1, plugins: [other] }, { name: '@scope/module-x', publisher: '甲', unit: 'inventory' })).toEqual({
195
+ state: 'unit-taken',
196
+ entry: '@scope/other',
197
+ });
198
+ const legacy = entry({
199
+ name: '@scope/legacy',
200
+ versions: [
201
+ {
202
+ version: '0.1.0',
203
+ source: {
204
+ type: 'git',
205
+ url: 'https://git.company.com/wb/market.git',
206
+ commit: 'a'.repeat(40),
207
+ path: 'packages/inventory',
208
+ },
209
+ },
210
+ ],
211
+ });
212
+ expect(entryUnitOf(legacy)).toBe('inventory');
213
+ expect(claimOf({ format: 1, plugins: [legacy] }, { name: '@scope/module-x', publisher: '甲', unit: 'inventory' }).state).toBe(
214
+ 'unit-taken',
215
+ );
216
+ });
217
+
218
+ it('同一个条目自己再用同 unit / 同产物目录,不算撞', () => {
219
+ const mine = entry({
220
+ owner: '甲',
221
+ metadata: { unit: 'inventory' },
222
+ versions: [
223
+ {
224
+ version: '0.1.0',
225
+ source: {
226
+ type: 'git',
227
+ url: 'https://git.company.com/wb/market.git',
228
+ commit: 'a'.repeat(40),
229
+ path: marketArtifactPath('@scope/module-x'),
230
+ },
231
+ },
232
+ ],
233
+ });
234
+ expect(
235
+ claimOf(
236
+ { format: 1, plugins: [mine] },
237
+ {
238
+ name: '@scope/module-x',
239
+ publisher: '甲',
240
+ unit: 'inventory',
241
+ artifactPath: marketArtifactPath('@scope/module-x'),
242
+ },
243
+ ).state,
244
+ ).toBe('owned');
245
+ });
246
+
247
+ it('别的条目占了同一个产物目录:path-taken', () => {
248
+ const other = entry({
249
+ name: '@scope/other',
250
+ owner: '乙',
251
+ versions: [
252
+ {
253
+ version: '0.1.0',
254
+ source: {
255
+ type: 'git',
256
+ url: 'https://git.company.com/wb/market.git',
257
+ commit: 'a'.repeat(40),
258
+ path: marketArtifactPath('@scope/module-x'),
259
+ },
260
+ },
261
+ ],
262
+ });
263
+ expect(
264
+ claimOf(
265
+ { format: 1, plugins: [other] },
266
+ { name: '@scope/module-x', publisher: '甲', unit: 'x', artifactPath: marketArtifactPath('@scope/module-x') },
267
+ ),
268
+ ).toEqual({ state: 'path-taken', entry: '@scope/other' });
269
+ });
104
270
  });
105
271
 
106
272
  describe('filterMarketIndex', () => {
@@ -202,6 +202,25 @@ export function appendChangelogEntry(text: string, options: ChangelogEntryOption
202
202
  return `${normalizeWhitespace(out)}\n`;
203
203
  }
204
204
 
205
+ export interface ChangelogPlan {
206
+ readonly action: 'kept' | 'written';
207
+ readonly version: string;
208
+ readonly text: string;
209
+ readonly section: string;
210
+ }
211
+
212
+ export function planChangelog(text: string, options: ChangelogEntryOptions): ChangelogPlan | undefined {
213
+ const source = normalizeChangelogText(text);
214
+ const version = changelogVersion(options.version);
215
+ const section = changelogSectionOf(source, version);
216
+ if (section !== undefined && changelogHasEntry(source, version)) {
217
+ return { action: 'kept', version, text: source, section };
218
+ }
219
+ if (bulletsOf(options.note).length === 0) return undefined;
220
+ const next = appendChangelogEntry(source, options);
221
+ return { action: 'written', version, text: next, section: changelogSectionOf(next, version) ?? '' };
222
+ }
223
+
205
224
  function normalizeWhitespace(rows: readonly string[]): string {
206
225
  const out: string[] = [];
207
226
  for (const row of rows) {
@@ -0,0 +1,91 @@
1
+ import type { MarketClaim, MarketIndex, MarketIndexEntry } from '../contract/protocol/market-index.js';
2
+
3
+ export interface PublisherIdentity {
4
+ readonly name: string;
5
+ readonly email?: string;
6
+ }
7
+
8
+ function foldedName(name: string): string {
9
+ return name.replace(/\s+/gu, ' ').trim().toLowerCase();
10
+ }
11
+
12
+ export function publisherIdentityOf(text: string | undefined): PublisherIdentity | undefined {
13
+ const raw = (text ?? '').trim();
14
+ if (raw === '') return undefined;
15
+ const match = /^(.*?)\s*<([^<>]*)>\s*$/u.exec(raw);
16
+ const name = (match === null ? raw : match[1]).trim();
17
+ const email = match === null ? '' : match[2].trim().toLowerCase();
18
+ if (name === '') return undefined;
19
+ return email === '' ? { name } : { name, email };
20
+ }
21
+
22
+ export function samePublisher(left: string, right: string): boolean {
23
+ const a = publisherIdentityOf(left);
24
+ const b = publisherIdentityOf(right);
25
+ if (a === undefined || b === undefined) return false;
26
+ if (a.email !== undefined && b.email !== undefined) return a.email === b.email;
27
+ return foldedName(a.name) === foldedName(b.name);
28
+ }
29
+
30
+ export function entryOwnerOf(entry: MarketIndexEntry): string | undefined {
31
+ const owner = (entry.owner ?? '').trim();
32
+ if (owner !== '') return owner;
33
+ const author = (entry.author ?? '').trim();
34
+ return author === '' ? undefined : author;
35
+ }
36
+
37
+ export function claimOfEntry(entry: MarketIndexEntry | undefined, publisher: string | undefined): MarketClaim {
38
+ if (entry === undefined) return { state: 'new' };
39
+ const owner = entryOwnerOf(entry);
40
+ if (owner === undefined) return { state: 'unowned' };
41
+ if (publisher === undefined || publisher.trim() === '') return { state: 'unknown' };
42
+ return samePublisher(owner, publisher) ? { state: 'owned', owner } : { state: 'conflict', owner };
43
+ }
44
+
45
+ export interface MarketClaimInput {
46
+ readonly name: string;
47
+ readonly publisher: string | undefined;
48
+ readonly unit?: string | undefined;
49
+ readonly artifactPath?: string | undefined;
50
+ }
51
+
52
+ export function marketArtifactPath(name: string): string {
53
+ return `packages/${name}`;
54
+ }
55
+
56
+ export function entryUnitOf(entry: MarketIndexEntry): string | undefined {
57
+ const unit = entry.metadata?.unit?.trim();
58
+ if (unit !== undefined && unit !== '') return unit;
59
+ for (const version of entry.versions) {
60
+ if (version.source.type !== 'git') continue;
61
+ const match = /^packages\/([^/]+)$/u.exec(version.source.path ?? '');
62
+ const legacy = match?.[1];
63
+ if (legacy !== undefined) return legacy;
64
+ }
65
+ return undefined;
66
+ }
67
+
68
+ export function claimOf(index: MarketIndex, input: MarketClaimInput): MarketClaim {
69
+ const base = claimOfEntry(
70
+ index.plugins.find((one) => one.name === input.name),
71
+ input.publisher,
72
+ );
73
+ if (base.state === 'conflict' || base.state === 'unknown') return base;
74
+ const unit = (input.unit ?? '').trim();
75
+ if (unit !== '') {
76
+ for (const other of index.plugins) {
77
+ if (other.name === input.name) continue;
78
+ if (entryUnitOf(other) !== unit) continue;
79
+ return { state: 'unit-taken', entry: other.name };
80
+ }
81
+ }
82
+ const path = (input.artifactPath ?? '').trim();
83
+ if (path !== '') {
84
+ for (const other of index.plugins) {
85
+ if (other.name === input.name) continue;
86
+ if (!other.versions.some((one) => one.source.type === 'git' && (one.source.path ?? '') === path)) continue;
87
+ return { state: 'path-taken', entry: other.name };
88
+ }
89
+ }
90
+ return base;
91
+ }
@@ -32,7 +32,10 @@ export const MARKET_INDEX_README_LINES: readonly string[] = [
32
32
  'dsh-wb-module release --index <本仓库地址> --index-policy artifacts-in-repo',
33
33
  '```',
34
34
  '',
35
- '产物会提交到 `packages/<unit>/`,并更新 `market.json`。',
35
+ '产物会提交到 `packages/<包名>/`(带 scope 时是 `packages/@scope/名字/`),并更新 `market.json`。',
36
+ '',
37
+ '一个包名归第一次发布的人(`market.json` 里的 `owner`)。换一个署名发同名会被拒;要换人,先把这一条的 `owner` 改成新的人。',
38
+ '同一个市场里,一个模块 id(`metadata.unit`)只能被一个条目使用;另一个包名想用同一个 id / 产物目录也会被拒。',
36
39
  '',
37
40
  '## 使用这个索引',
38
41
  '',
@@ -152,9 +155,20 @@ function iconProblem(icon: string): MarketIndexProblem | undefined {
152
155
  return problem('icon', 'icon 只能是 base64 data URL 或 http(s) URL');
153
156
  }
154
157
 
158
+ const MAX_OWNER_CHARS = 200;
159
+ const UNIT_ID = /^[a-z][a-z0-9-]*$/;
160
+
161
+ function ownerProblem(owner: string): MarketIndexProblem | undefined {
162
+ if (/[\r\n]/u.test(owner)) return problem('owner', 'owner 只能是一行署名');
163
+ if (owner.length > MAX_OWNER_CHARS) return problem('owner', 'owner 超过 200 字');
164
+ return undefined;
165
+ }
166
+
155
167
  function metadataOf(raw: unknown): MarketIndexEntry['metadata'] {
156
168
  const record = asRecord(raw);
157
169
  if (record === undefined) return undefined;
170
+ const workbench = record['workbench'] === true;
171
+ const unit = asText(record['unit']);
158
172
  const category = asText(record['category']);
159
173
  const labels = asTextArray(record['labels']);
160
174
  const keywords = asTextArray(record['keywords']);
@@ -168,6 +182,8 @@ function metadataOf(raw: unknown): MarketIndexEntry['metadata'] {
168
182
  ),
169
183
  );
170
184
  return {
185
+ ...(workbench ? { workbench: true } : {}),
186
+ ...(unit === '' ? {} : { unit }),
171
187
  ...(category === '' ? {} : { category }),
172
188
  ...(labels.length === 0 ? {} : { labels }),
173
189
  ...(keywords.length === 0 ? {} : { keywords }),
@@ -192,14 +208,23 @@ function entryOf(raw: unknown, at: string): MarketIndexEntry | MarketIndexProble
192
208
  const title = asLocalized(record['title']);
193
209
  const description = asLocalized(record['description']);
194
210
  const author = asText(record['author']);
211
+ const owner = asText(record['owner']);
212
+ if (owner !== '') {
213
+ const badOwner = ownerProblem(owner);
214
+ if (badOwner !== undefined) return badOwner;
215
+ }
195
216
  const homepage = asText(record['homepage']);
196
217
  const metadata = metadataOf(record['metadata']);
218
+ if (metadata?.unit !== undefined && !UNIT_ID.test(metadata.unit)) {
219
+ return problem('unit', `metadata.unit 只能是小写字母开头的 a-z0-9-(现在是 ${JSON.stringify(metadata.unit)})`, at);
220
+ }
197
221
  return {
198
222
  name,
199
223
  versions,
200
224
  ...(title === undefined ? {} : { title }),
201
225
  ...(description === undefined ? {} : { description }),
202
226
  ...(author === '' ? {} : { author }),
227
+ ...(owner === '' ? {} : { owner }),
203
228
  ...(homepage === '' ? {} : { homepage }),
204
229
  ...(icon === '' ? {} : { icon }),
205
230
  ...(metadata === undefined ? {} : { metadata }),
@@ -272,8 +297,14 @@ export function upsertMarketEntry(index: MarketIndex, entry: MarketIndexEntry):
272
297
  const merged = new Map<string, MarketIndexVersion>();
273
298
  for (const one of current.versions) merged.set(one.version, one);
274
299
  for (const one of entry.versions) merged.set(one.version, one);
300
+ const owner = current.owner ?? entry.owner;
275
301
  const plugins = [...index.plugins];
276
- plugins[found] = { ...current, ...entry, versions: sortMarketVersions([...merged.values()]) };
302
+ plugins[found] = {
303
+ ...current,
304
+ ...entry,
305
+ ...(owner === undefined ? {} : { owner }),
306
+ versions: sortMarketVersions([...merged.values()]),
307
+ };
277
308
  return { ...index, plugins };
278
309
  }
279
310
 
package/src/host.ts CHANGED
@@ -15,6 +15,7 @@ export * from './contract/long-task.js';
15
15
  export * from './lib/text.js';
16
16
  export * from './lib/ui.js';
17
17
  export * from './lib/board-fact.js';
18
+ export * from './lib/git-address.js';
18
19
  export * from './api/blocker-table.js';
19
20
  export * from './contract/unit-declaration.js';
20
21
  export * from './host/board-fact.js';
@@ -72,4 +73,5 @@ export * from './host/extension.js';
72
73
  export * from './host/port-types.js';
73
74
  export * from './host/version-range.js';
74
75
  export * from './host/market-index.js';
76
+ export * from './host/market-index-claim.js';
75
77
  export * from './host/changelog.js';
package/src/i18n/en.ts CHANGED
@@ -258,6 +258,7 @@ export const en: Record<keyof WorkbenchMessages, string> = {
258
258
  'panels.market.add.failureInstalled':
259
259
  'It is already installed in this workbench. The same package cannot be installed over itself: uninstall it first, then install again. The uninstall did not succeed this time.',
260
260
  'panels.market.add.failureGeneric': 'Something went wrong during install — see the install details',
261
+ 'panels.market.add.failureCredentials': 'This repository needs credentials: this machine has none for it. Clone it once in a terminal, set up a credential helper, or install from the SSH address.',
261
262
  'panels.market.add.approveTitle': 'Your approval is needed for install scripts',
262
263
  'panels.market.add.approveCaution': 'Only allow packages you trust: install scripts run on this machine with your permissions.',
263
264
  'panels.market.add.approveRun': 'Allow and continue installing',
@@ -299,10 +300,16 @@ export const en: Record<keyof WorkbenchMessages, string> = {
299
300
  'panels.discover.addingSource': 'Adding…',
300
301
  'panels.discover.addFirstSource': 'Add index source',
301
302
  'panels.discover.removeSource': 'Remove',
302
- 'panels.discover.removeSourceNamed': 'Remove {url}',
303
- 'panels.discover.useSource': 'Use',
304
- 'panels.discover.useSourceNamed': 'Use {label}',
305
- 'panels.discover.sourceCurrent': 'In use',
303
+ 'panels.discover.removeTitle': 'Remove this index source?',
304
+ 'panels.discover.removeBody': '"{label}" will be removed from this list; the repository itself is not deleted, and you can add it back later.',
305
+ 'panels.discover.sourceActions': 'Actions for {label}',
306
+ 'panels.discover.sourceStateWritable': 'Publish target',
307
+ 'panels.discover.sourceStateReadOnly': 'Read-only list',
308
+ 'panels.discover.sourceEnable': 'Set up for publishing',
309
+ 'panels.discover.sourceDisable': 'Set to read-only',
310
+ 'panels.discover.sourceWorking': 'Updating…',
311
+ 'panels.discover.sourceEnabled': '"{label}" is ready for publishing',
312
+ 'panels.discover.sourceReadOnly': '"{label}" is now read-only',
306
313
  'panels.discover.refresh': 'Refresh',
307
314
  'panels.discover.search': 'Search modules',
308
315
  'panels.discover.clearSearch': 'Clear search',
@@ -324,12 +331,13 @@ export const en: Record<keyof WorkbenchMessages, string> = {
324
331
  'panels.discover.noSourceHint': 'Add a module market index (http address or git repository) to browse and install its modules here',
325
332
  'panels.discover.sourcesFailed': 'Could not read the index sources.',
326
333
  'panels.discover.unreadable': 'Could not read this index.',
334
+ 'panels.discover.sourceAuth': 'This index source could not be read: this machine has no git credentials for it. Set up credentials (or switch the source to its SSH address), then refresh.',
327
335
  'panels.discover.invalid': 'This index is not in a valid format.',
328
336
  'panels.discover.emptyRepo': 'This repository has no commits yet.',
329
337
  'panels.discover.missingIndex': 'This repository has no {path} yet.',
330
338
  'panels.discover.initTitle': 'Initialize this repository as a module market index?',
331
339
  'panels.discover.initCta': 'Initialize market index',
332
- 'panels.discover.initWhat': 'It will write market.json and README.md on {branch}, commit once, and push. Modules you publish from this workbench will commit their build files under packages/<unit>/. The repository stays owned by its owner.',
340
+ 'panels.discover.initWhat': 'It will write market.json and README.md on {branch}, commit once, and push. Modules you publish from this workbench will commit their build files under packages/<package-name>/. The repository stays owned by its owner.',
333
341
  'panels.discover.initBranch': 'Branch',
334
342
  'panels.discover.initBranchHint': 'The index and build files go to this branch; an empty repository gets it created.',
335
343
  'panels.discover.initBranchBad': 'Use only letters, digits, dots, underscores, slashes, or dashes in a branch name.',
@@ -347,8 +355,23 @@ export const en: Record<keyof WorkbenchMessages, string> = {
347
355
  'panels.discover.badShellDetail': 'This version is not compatible with the running workbench {current}; it requires {peer}',
348
356
  'panels.discover.unverified': 'Workbench unverified',
349
357
  'panels.discover.installing': 'Installing…',
358
+ 'panels.discover.installingNamed': 'Installing {name}…',
359
+ 'panels.discover.cancelInstall': 'Cancel install',
360
+ 'panels.discover.cancelling': 'Cancelling…',
361
+ 'panels.discover.installCancelled': 'Install cancelled: {name}',
350
362
  'panels.discover.installedDone': 'Installed: {name}',
351
363
  'panels.discover.failed': 'Failed: {reason}',
364
+ 'panels.discover.transportTitle': 'Where to read this repository',
365
+ 'panels.discover.transportSsh': 'SSH',
366
+ 'panels.discover.transportHttps': 'HTTPS',
367
+ 'panels.discover.transportOk': 'Readable',
368
+ 'panels.discover.transportAuth': 'Needs credentials',
369
+ 'panels.discover.transportUnreachable': 'Unreachable',
370
+ 'panels.discover.transportUnknown': 'Not probed',
371
+ 'panels.discover.transportChecking': 'Checking…',
372
+ 'panels.discover.transportTest': 'Test this address',
373
+ 'panels.discover.transportPlaceholder': 'Or paste another address (mirror, alias)',
374
+ 'panels.discover.transportNeedsAuth': 'This repository needs credentials: clone it once in a terminal, or set up an SSH key / credential helper, then install again.',
352
375
  'panels.discover.expand': 'Show versions',
353
376
  'panels.discover.collapse': 'Hide versions',
354
377
  'panels.discover.versionCount': '{n} versions',
@@ -647,6 +670,13 @@ export const en: Record<keyof WorkbenchMessages, string> = {
647
670
  'approval.publish.version': 'Version',
648
671
  'approval.publish.author': 'Author',
649
672
  'approval.publish.authorMissing': 'unsigned',
673
+ 'approval.publish.owner': 'Owner',
674
+ 'approval.publish.ownerUnowned': 'originally unattributed',
675
+ 'approval.publish.ownerTaken': 'Owned by {owner}',
676
+ 'approval.publish.unit': 'Module id',
677
+ 'approval.publish.unitTaken': 'Taken by {entry}',
678
+ 'approval.publish.path': 'Artifact dir',
679
+ 'approval.publish.pathTaken': 'Taken by {entry}',
650
680
  'approval.publish.registry': 'Registry',
651
681
  'approval.publish.repo': 'Repository',
652
682
  'approval.publish.branch': 'Branch',
@@ -657,6 +687,7 @@ export const en: Record<keyof WorkbenchMessages, string> = {
657
687
  'approval.publish.twoCommits': 'build files + market.json in 2 commits, one push',
658
688
  'approval.publish.reading': 'Reading…',
659
689
  'approval.publish.changelog': 'Changelog for {version}',
690
+ 'approval.publish.changelogPending': 'This release writes the {version} changelog entry',
660
691
  'approval.publish.changelogMissing': 'No changelog entry for {version}; publishing will be blocked',
661
692
  'approval.publish.targetAuto': 'configured default',
662
693
  'approval.publish.targetTeam': 'team registry',
@@ -665,6 +696,15 @@ export const en: Record<keyof WorkbenchMessages, string> = {
665
696
  'approval.publish.targetGit': 'git repository',
666
697
  'approval.publish.targetMarket': 'module market index',
667
698
  'approval.publish.targetLocal': 'keep local',
699
+ 'approval.market.source': 'Index source',
700
+ 'approval.market.url': 'URL',
701
+ 'approval.market.action': 'This action',
702
+ 'approval.market.policy': 'Current state',
703
+ 'approval.market.enable': 'make it publishable',
704
+ 'approval.market.disable': 'make it read-only',
705
+ 'approval.market.writable': 'publishable',
706
+ 'approval.market.readOnly': 'read-only module list',
707
+ 'approval.market.reading': 'Reading…',
668
708
  'markdown.copy': 'Copy',
669
709
  'markdown.copied': 'Copied',
670
710
  'markdown.footnotes': 'Footnotes',