@dsh-workbench/module-tools 0.3.10 → 1.0.0-beta.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.
- package/LICENSE +21 -0
- package/README.md +223 -177
- package/bin/dsh-wb-module.mjs +89 -34
- package/package.json +18 -3
- package/src/arch.mjs +158 -129
- package/src/build.mjs +6 -30
- package/src/changelog.mjs +167 -0
- package/src/check-flags.mjs +0 -7
- package/src/check.mjs +115 -53
- package/src/checks/alias.mjs +0 -9
- package/src/checks/arch.mjs +3 -25
- package/src/checks/bundles.mjs +2 -34
- package/src/checks/cards.mjs +26 -30
- package/src/checks/contracts.mjs +61 -222
- package/src/checks/kit.mjs +2 -42
- package/src/checks/lint.mjs +9 -82
- package/src/checks/markers.mjs +48 -17
- package/src/checks/primitives.mjs +0 -39
- package/src/checks/protocol.mjs +0 -18
- package/src/checks/source.mjs +163 -29
- package/src/checks/standalone.mjs +4 -14
- package/src/checks/style.mjs +71 -241
- package/src/css-injection.mjs +0 -11
- package/src/css-shell.mjs +3 -31
- package/src/datasource-file.mjs +104 -0
- package/src/declared-tools.mjs +0 -24
- package/src/design.mjs +116 -38
- package/src/discover.mjs +2 -10
- package/src/font-scale.mjs +1 -61
- package/src/init.mjs +2 -17
- package/src/lint.mjs +24 -144
- package/src/market-index.mjs +569 -0
- package/src/market-init.mjs +80 -0
- package/src/primitives-surface.mjs +2 -28
- package/src/publish.mjs +6 -24
- package/src/release-changelog.mjs +29 -0
- package/src/release-journal.mjs +131 -0
- package/src/release-report.mjs +99 -0
- package/src/release-util.mjs +80 -0
- package/src/release.mjs +1251 -0
- package/src/seed.mjs +0 -25
- package/src/shell-type-files.mjs +3 -30
- package/src/sync.mjs +41 -24
- package/template/AGENTS.md.tpl +33 -0
- package/template/CHANGELOG.md.tpl +7 -0
- package/template/README.md.tpl +6 -33
- package/template/package.json.tpl +2 -2
- package/template/src/client.tsx.tpl +1 -18
- package/template/src/host.ts.tpl +2 -15
- package/template/types/globals.d.ts +1 -14
- package/template-diy/AGENTS.md.tpl +33 -0
- package/template-diy/CHANGELOG.md.tpl +7 -0
- package/template-diy/README.md.tpl +4 -133
- package/template-diy/package.json.tpl +2 -2
- package/template-diy/src/client.tsx.tpl +1 -29
- package/template-diy/src/host.ts.tpl +1 -15
- package/template-diy/src/messages.ts.tpl +4 -4
- package/template-diy/src/plugin.test.ts.tpl +1 -1
- package/template-diy/types/globals.d.ts +1 -14
- package/template-diy/types/styles.d.ts +0 -10
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
import { cpSync, copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join, resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
MARKET_INDEX_README_LINES,
|
|
6
|
+
MARKET_INDEX_SEED,
|
|
7
|
+
gitUrlOk,
|
|
8
|
+
marketSourceKey,
|
|
9
|
+
parseMarketIndexJson,
|
|
10
|
+
upsertMarketEntry,
|
|
11
|
+
} from '@dsh-workbench/shared/host';
|
|
12
|
+
|
|
13
|
+
const MARKET_FILE = 'market.json';
|
|
14
|
+
const PACKAGES_DIR = 'packages';
|
|
15
|
+
const WORKBENCH_LABEL = 'workbench';
|
|
16
|
+
const WORKSPACE_PREFIX = 'workspace:';
|
|
17
|
+
const ARTIFACT_DIRS = ['dist'];
|
|
18
|
+
const ARTIFACT_FILES = ['client.js', 'cordis.patch.yml', 'README.md', 'CHANGELOG.md'];
|
|
19
|
+
const DEP_FIELDS = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];
|
|
20
|
+
|
|
21
|
+
export class MarketIndexError extends Error {
|
|
22
|
+
constructor(message, step, command) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.name = 'MarketIndexError';
|
|
25
|
+
this.step = step;
|
|
26
|
+
this.command = command;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function argValue(args, flag) {
|
|
31
|
+
const at = args.indexOf(flag);
|
|
32
|
+
return at === -1 ? undefined : args[at + 1];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function messageOf(error) {
|
|
36
|
+
return error instanceof Error ? error.message : String(error);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function tailOf(text) {
|
|
40
|
+
return String(text ?? '')
|
|
41
|
+
.trim()
|
|
42
|
+
.split('\n')
|
|
43
|
+
.filter((one) => one.trim() !== '')
|
|
44
|
+
.slice(-4)
|
|
45
|
+
.join(' | ');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function workbenchRootOf(home) {
|
|
49
|
+
const explicit = process.env.DSH_WORKBENCH_HOME?.trim();
|
|
50
|
+
return explicit === undefined || explicit === '' ? join(home, 'workbench') : explicit;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function normalizedPolicy(value, fallback) {
|
|
54
|
+
if (value === 'artifacts-in-repo' || value === 'catalog-only') return value;
|
|
55
|
+
return fallback;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function sanitizeManifest(manifest) {
|
|
59
|
+
const next = globalThis.structuredClone(manifest);
|
|
60
|
+
delete next.scripts;
|
|
61
|
+
for (const field of DEP_FIELDS) {
|
|
62
|
+
const deps = next[field];
|
|
63
|
+
if (deps === null || typeof deps !== 'object' || Array.isArray(deps)) continue;
|
|
64
|
+
for (const [name, spec] of Object.entries(deps)) {
|
|
65
|
+
if (typeof spec === 'string' && spec.trim().startsWith(WORKSPACE_PREFIX)) delete deps[name];
|
|
66
|
+
}
|
|
67
|
+
if (Object.keys(deps).length === 0) delete next[field];
|
|
68
|
+
}
|
|
69
|
+
return next;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function authorOf(manifest) {
|
|
73
|
+
const author = manifest.author;
|
|
74
|
+
if (typeof author === 'string') return author.trim();
|
|
75
|
+
if (author !== null && typeof author === 'object' && typeof author.name === 'string') return author.name.trim();
|
|
76
|
+
return '';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function descriptionOf(manifest) {
|
|
80
|
+
const description = typeof manifest.description === 'string' ? manifest.description.trim() : '';
|
|
81
|
+
return description === '' ? undefined : { zh: description };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function indexEntryOf(input) {
|
|
85
|
+
const manifest = input.manifest;
|
|
86
|
+
const author = authorOf(manifest);
|
|
87
|
+
const homepage = typeof manifest.homepage === 'string' ? manifest.homepage.trim() : '';
|
|
88
|
+
const keywords = Array.isArray(manifest.keywords)
|
|
89
|
+
? manifest.keywords.flatMap((one) => (typeof one === 'string' && one.trim() !== '' ? [one.trim()] : []))
|
|
90
|
+
: [];
|
|
91
|
+
const order = manifest.dsh?.workbench?.card?.order;
|
|
92
|
+
const dsh = manifest.engines?.dsh;
|
|
93
|
+
const description = descriptionOf(manifest);
|
|
94
|
+
return {
|
|
95
|
+
name: manifest.name,
|
|
96
|
+
...(description === undefined ? {} : { description }),
|
|
97
|
+
...(author === '' ? {} : { author }),
|
|
98
|
+
...(homepage === '' ? {} : { homepage }),
|
|
99
|
+
metadata: {
|
|
100
|
+
category: WORKBENCH_LABEL,
|
|
101
|
+
labels: [WORKBENCH_LABEL],
|
|
102
|
+
...(keywords.length === 0 ? {} : { keywords }),
|
|
103
|
+
...(typeof order === 'number' ? { sortKeys: { order } } : {}),
|
|
104
|
+
},
|
|
105
|
+
versions: [
|
|
106
|
+
{
|
|
107
|
+
version: input.version,
|
|
108
|
+
publishTimestamp: Math.floor(input.at / 1000),
|
|
109
|
+
source: input.source,
|
|
110
|
+
...(typeof dsh === 'string' && dsh.trim() !== '' ? { engines: { dsh: dsh.trim() } } : {}),
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function mergeIndexText(text, entry) {
|
|
117
|
+
const raw = String(text ?? '').trim();
|
|
118
|
+
const parsed = raw === '' ? { ok: true, index: { format: 1, plugins: [] } } : parseMarketIndexJson(raw);
|
|
119
|
+
if (!parsed.ok) return { ok: false, problems: parsed.problems };
|
|
120
|
+
const merged = upsertMarketEntry(parsed.index, entry);
|
|
121
|
+
const checked = parseMarketIndexJson(JSON.stringify(merged));
|
|
122
|
+
if (!checked.ok) return { ok: false, problems: checked.problems };
|
|
123
|
+
return { ok: true, text: `${JSON.stringify(checked.index, null, 2)}\n` };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function readSources(file) {
|
|
127
|
+
try {
|
|
128
|
+
const parsed = JSON.parse(readFileSync(file, 'utf8'));
|
|
129
|
+
const record = parsed !== null && typeof parsed === 'object' ? parsed : {};
|
|
130
|
+
const sources = Array.isArray(record.sources) ? record.sources : [];
|
|
131
|
+
const selected = typeof record.selected === 'string' ? record.selected : null;
|
|
132
|
+
return { sources, selected };
|
|
133
|
+
} catch {
|
|
134
|
+
return { sources: [], selected: null };
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function planOf(raw, args, fallbackPolicy) {
|
|
139
|
+
if (raw === null || typeof raw !== 'object') return undefined;
|
|
140
|
+
if (raw.kind !== 'git') return undefined;
|
|
141
|
+
const url = typeof raw.url === 'string' ? raw.url.trim() : '';
|
|
142
|
+
const localPath = typeof raw.localPath === 'string' && raw.localPath.trim() !== '' ? resolve(raw.localPath.trim()) : '';
|
|
143
|
+
if (url === '' && localPath === '') return undefined;
|
|
144
|
+
if (url !== '' && !gitUrlOk(url)) return undefined;
|
|
145
|
+
const ref = (argValue(args, '--index-ref') ?? (typeof raw.ref === 'string' ? raw.ref : '')).trim();
|
|
146
|
+
const path = (argValue(args, '--index-path') ?? (typeof raw.path === 'string' ? raw.path : '')).trim();
|
|
147
|
+
const policy = normalizedPolicy(argValue(args, '--index-policy'), normalizedPolicy(raw.policy, fallbackPolicy));
|
|
148
|
+
return {
|
|
149
|
+
source: {
|
|
150
|
+
kind: 'git',
|
|
151
|
+
url,
|
|
152
|
+
...(ref === '' ? {} : { ref }),
|
|
153
|
+
...(path === '' ? {} : { path }),
|
|
154
|
+
...(localPath === '' ? {} : { localPath }),
|
|
155
|
+
},
|
|
156
|
+
policy,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function resolveIndexPlan({ args = [], home, exec }) {
|
|
161
|
+
const asked = (argValue(args, '--index') ?? 'auto').trim();
|
|
162
|
+
if (asked === 'none') return undefined;
|
|
163
|
+
const plan =
|
|
164
|
+
asked !== 'auto' && asked !== ''
|
|
165
|
+
? planFromArgument(asked, args, exec)
|
|
166
|
+
: planFromSources(join(workbenchRootOf(home), 'market-index', 'sources.json'), args);
|
|
167
|
+
if (plan === undefined) {
|
|
168
|
+
if (asked === 'auto' || asked === '') return undefined;
|
|
169
|
+
throw new MarketIndexError(
|
|
170
|
+
`这个工坊还没配可写的 git 索引源。给我一个能推的 git 仓库(或它在本机的 clone 路径),我配成索引源并建一份 ${MARKET_FILE}。`,
|
|
171
|
+
'index-write',
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
if (plan.source.url === '' && plan.source.localPath !== undefined) {
|
|
175
|
+
const remote = exec('git', ['-C', plan.source.localPath, 'remote', 'get-url', 'origin'], {
|
|
176
|
+
capture: true,
|
|
177
|
+
allowFailure: true,
|
|
178
|
+
});
|
|
179
|
+
const url = remote.status === 0 ? remote.stdout.trim() : '';
|
|
180
|
+
if (url === '') {
|
|
181
|
+
throw new MarketIndexError(`索引目录没有 origin 远端:${plan.source.localPath}`, 'index-write');
|
|
182
|
+
}
|
|
183
|
+
plan.source.url = url;
|
|
184
|
+
}
|
|
185
|
+
return plan;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function planFromArgument(asked, args, exec) {
|
|
189
|
+
const asDir = resolve(asked);
|
|
190
|
+
if (existsSync(asDir)) {
|
|
191
|
+
const remote = exec('git', ['-C', asDir, 'remote', 'get-url', 'origin'], { capture: true, allowFailure: true });
|
|
192
|
+
const url = remote.status === 0 ? remote.stdout.trim() : '';
|
|
193
|
+
const plan = planOf({ kind: 'git', url, localPath: asDir }, args, 'artifacts-in-repo');
|
|
194
|
+
if (plan === undefined) {
|
|
195
|
+
throw new MarketIndexError(`--index 指的目录不是 git 仓库或没有 origin:${asDir}`, 'index-write');
|
|
196
|
+
}
|
|
197
|
+
return plan;
|
|
198
|
+
}
|
|
199
|
+
if (!gitUrlOk(asked)) {
|
|
200
|
+
throw new MarketIndexError(`--index 只认 none / 目录 / git 地址(收到「${asked}」)`, 'index-write');
|
|
201
|
+
}
|
|
202
|
+
return planOf({ kind: 'git', url: asked }, args, 'catalog-only');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function planFromSources(file, args) {
|
|
206
|
+
const payload = readSources(file);
|
|
207
|
+
const found = payload.sources.find((one) => one?.id === payload.selected);
|
|
208
|
+
return planOf(found ?? null, args, 'catalog-only');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function runGit(exec, cwd, argv, { allowFailure = false } = {}) {
|
|
212
|
+
return exec('git', ['-C', cwd, ...argv], { capture: true, allowFailure });
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function currentBranch(exec, cwd) {
|
|
216
|
+
const result = runGit(exec, cwd, ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
217
|
+
const branch = result.status === 0 ? result.stdout.trim() : '';
|
|
218
|
+
if (branch === '' || branch === 'HEAD') {
|
|
219
|
+
throw new MarketIndexError('索引仓库不在一个分支上(detached HEAD)——切回分支再发', 'index-write');
|
|
220
|
+
}
|
|
221
|
+
return branch;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function worktreeOf(plan, exec, home) {
|
|
225
|
+
const source = plan.source;
|
|
226
|
+
if (source.localPath !== undefined) return source.localPath;
|
|
227
|
+
const base = join(workbenchRootOf(home), 'market-index', marketSourceKey(source));
|
|
228
|
+
const workdir = join(base, 'worktree');
|
|
229
|
+
if (existsSync(workdir)) return workdir;
|
|
230
|
+
mkdirSync(base, { recursive: true });
|
|
231
|
+
const branch = source.ref === undefined ? [] : ['--branch', source.ref];
|
|
232
|
+
const cloned = exec('git', ['clone', ...branch, source.url, workdir], { capture: true, allowFailure: true });
|
|
233
|
+
if (cloned.status !== 0) {
|
|
234
|
+
rmSync(workdir, { recursive: true, force: true });
|
|
235
|
+
throw new MarketIndexError(
|
|
236
|
+
`索引仓库克隆不下来:${tailOf(`${cloned.stdout}\n${cloned.stderr}`)}`,
|
|
237
|
+
'index-write',
|
|
238
|
+
`git clone ${source.url} ${workdir}`,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
return workdir;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function assertClean(exec, workdir) {
|
|
245
|
+
const status = runGit(exec, workdir, ['status', '--porcelain', '--untracked-files=all']);
|
|
246
|
+
if (status.status !== 0) throw new MarketIndexError(`读不了索引仓库状态:${tailOf(status.stderr)}`, 'index-write');
|
|
247
|
+
const dirty = status.stdout.trim();
|
|
248
|
+
if (dirty !== '') {
|
|
249
|
+
throw new MarketIndexError(
|
|
250
|
+
`索引仓库的工作区不干净(${dirty.split('\n').slice(0, 3).join('、')})——先提交/暂存,再发`,
|
|
251
|
+
'index-write',
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function commitOf(exec, workdir, message) {
|
|
257
|
+
const added = runGit(exec, workdir, ['add', '-A'], { allowFailure: true });
|
|
258
|
+
if (added.status !== 0) throw new MarketIndexError(`git add 没过:${tailOf(`${added.stdout}\n${added.stderr}`)}`);
|
|
259
|
+
const committed = runGit(exec, workdir, ['commit', '-m', message], { allowFailure: true });
|
|
260
|
+
const text = `${committed.stdout}\n${committed.stderr}`;
|
|
261
|
+
if (committed.status !== 0 && !/nothing to commit|no changes added|无文件要提交/i.test(text)) {
|
|
262
|
+
throw new MarketIndexError(`git commit 没过:${tailOf(text)}`);
|
|
263
|
+
}
|
|
264
|
+
const head = runGit(exec, workdir, ['rev-parse', 'HEAD']);
|
|
265
|
+
return head.status === 0 ? head.stdout.trim() : '';
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function pushOf(exec, workdir, branch, remote = 'origin') {
|
|
269
|
+
return runGit(exec, workdir, ['push', remote, `HEAD:refs/heads/${branch}`], { allowFailure: true });
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function shortOf(value) {
|
|
273
|
+
return typeof value === 'string' && value.length >= 7 ? value.slice(0, 7) : String(value ?? '');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function remoteHeadOf(exec, workdir, branch) {
|
|
277
|
+
const result = runGit(exec, workdir, ['ls-remote', 'origin', `refs/heads/${branch}`], { allowFailure: true });
|
|
278
|
+
if (result.status !== 0) {
|
|
279
|
+
throw new MarketIndexError(`核不了远端分支 ${branch}:${tailOf(result.stderr)}`, 'index-write');
|
|
280
|
+
}
|
|
281
|
+
const line = result.stdout.trim().split('\n')[0] ?? '';
|
|
282
|
+
return line.split(/\s+/)[0] ?? '';
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function copyArtifacts(root, workdir, unit, manifest) {
|
|
286
|
+
const dest = join(workdir, PACKAGES_DIR, unit);
|
|
287
|
+
rmSync(dest, { recursive: true, force: true });
|
|
288
|
+
mkdirSync(dest, { recursive: true });
|
|
289
|
+
for (const dir of ARTIFACT_DIRS) {
|
|
290
|
+
const src = join(root, dir);
|
|
291
|
+
if (existsSync(src)) cpSync(src, join(dest, dir), { recursive: true });
|
|
292
|
+
}
|
|
293
|
+
for (const name of ARTIFACT_FILES) {
|
|
294
|
+
const src = join(root, name);
|
|
295
|
+
if (existsSync(src)) copyFileSync(src, join(dest, name));
|
|
296
|
+
}
|
|
297
|
+
writeFileSync(join(dest, 'package.json'), `${JSON.stringify(sanitizeManifest(manifest), null, 2)}\n`);
|
|
298
|
+
return `${PACKAGES_DIR}/${unit}`;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function writeEntry(workdir, indexPath, entry) {
|
|
302
|
+
const file = join(workdir, indexPath);
|
|
303
|
+
const current = existsSync(file) ? readFileSync(file, 'utf8') : '';
|
|
304
|
+
const merged = mergeIndexText(current, entry);
|
|
305
|
+
if (!merged.ok) {
|
|
306
|
+
throw new MarketIndexError(
|
|
307
|
+
`${indexPath} 不合 format 1:${merged.problems.map((one) => one.detail).join(';')}`,
|
|
308
|
+
'index-write',
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
312
|
+
writeFileSync(file, merged.text);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function resetToRemote(exec, workdir, branch) {
|
|
316
|
+
const fetched = runGit(exec, workdir, ['fetch', 'origin', branch], { allowFailure: true });
|
|
317
|
+
if (fetched.status !== 0) return false;
|
|
318
|
+
const reset = runGit(exec, workdir, ['reset', '--hard', `origin/${branch}`], { allowFailure: true });
|
|
319
|
+
return reset.status === 0;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function publishSourceOf(publish) {
|
|
323
|
+
if (publish.git !== undefined) {
|
|
324
|
+
return {
|
|
325
|
+
type: 'git',
|
|
326
|
+
url: publish.git.url,
|
|
327
|
+
commit: publish.git.commit,
|
|
328
|
+
...(publish.git.path === undefined || publish.git.path === '' || publish.git.path === '.'
|
|
329
|
+
? {}
|
|
330
|
+
: { path: publish.git.path }),
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
if (publish.registry !== undefined && publish.registry !== '') {
|
|
334
|
+
return { type: 'npm', registry: publish.registry };
|
|
335
|
+
}
|
|
336
|
+
return undefined;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export function updateMarketIndex(options) {
|
|
340
|
+
const { plan, exec, home, root, manifest, unit, version, publish, now, resume, onStep } = options;
|
|
341
|
+
const entryName = `${manifest.name}@${version}`;
|
|
342
|
+
let base = { kind: 'git', url: plan.source.url, entry: entryName };
|
|
343
|
+
let phase = 'start';
|
|
344
|
+
const report = (step) => {
|
|
345
|
+
if (typeof step.phase === 'string') phase = step.phase;
|
|
346
|
+
try {
|
|
347
|
+
onStep?.(step);
|
|
348
|
+
} catch {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
try {
|
|
353
|
+
const workdir = worktreeOf(plan, exec, home);
|
|
354
|
+
const indexPath = plan.source.path ?? MARKET_FILE;
|
|
355
|
+
if (resume !== undefined && plan.source.localPath === undefined) {
|
|
356
|
+
runGit(exec, workdir, ['reset', '--hard', 'HEAD'], { allowFailure: true });
|
|
357
|
+
runGit(exec, workdir, ['clean', '-fd', '--', `${PACKAGES_DIR}/${unit}`, indexPath], { allowFailure: true });
|
|
358
|
+
} else {
|
|
359
|
+
assertClean(exec, workdir);
|
|
360
|
+
}
|
|
361
|
+
const branch = plan.source.ref ?? resume?.branch ?? currentBranch(exec, workdir);
|
|
362
|
+
base = { ...base, branch };
|
|
363
|
+
if (typeof resume?.indexCommit === 'string' && resume.indexCommit !== '' && resume.indexPushed === true) {
|
|
364
|
+
const remote = remoteHeadOf(exec, workdir, branch);
|
|
365
|
+
if (remote === resume.indexCommit) {
|
|
366
|
+
report({ phase: 'remote-checked', indexCommit: resume.indexCommit, indexPushed: true });
|
|
367
|
+
return {
|
|
368
|
+
...base,
|
|
369
|
+
...(typeof resume.path === 'string' ? { path: resume.path } : {}),
|
|
370
|
+
commit: typeof resume.artifactCommit === 'string' ? resume.artifactCommit : resume.indexCommit,
|
|
371
|
+
updated: true,
|
|
372
|
+
resumed: true,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
let source;
|
|
377
|
+
let artifactPath;
|
|
378
|
+
if (plan.policy === 'artifacts-in-repo') {
|
|
379
|
+
report({ phase: 'artifacts' });
|
|
380
|
+
artifactPath = copyArtifacts(root, workdir, unit, manifest);
|
|
381
|
+
report({ phase: 'artifacts-copied' });
|
|
382
|
+
const sha = commitOf(exec, workdir, `chore(release): ${entryName}`);
|
|
383
|
+
report({ phase: 'artifacts-committed', artifactCommit: sha });
|
|
384
|
+
const pushed = pushOf(exec, workdir, branch);
|
|
385
|
+
if (pushed.status !== 0) {
|
|
386
|
+
phase = 'artifacts';
|
|
387
|
+
throw new MarketIndexError(
|
|
388
|
+
`产物提交推不上去(分支保护或权限):本地 commit ${sha};去合并/给权限后跟我说,我补索引。${tailOf(`${pushed.stdout}\n${pushed.stderr}`)}`,
|
|
389
|
+
'git-market-protected',
|
|
390
|
+
`git -C ${workdir} push origin HEAD:refs/heads/${branch}`,
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
report({ phase: 'artifacts-pushed', artifactCommit: sha, artifactPushed: true });
|
|
394
|
+
source = { type: 'git', url: plan.source.url, commit: sha, path: artifactPath };
|
|
395
|
+
} else {
|
|
396
|
+
source = publishSourceOf(publish ?? {});
|
|
397
|
+
if (source === undefined) throw new MarketIndexError('这个出口没有可写进索引的 source(tarball 不进索引)', 'index-write');
|
|
398
|
+
if (source.type === 'npm' && source.registry === 'https://registry.npmjs.org/') delete source.registry;
|
|
399
|
+
}
|
|
400
|
+
const entry = indexEntryOf({ manifest, version, source, at: now() });
|
|
401
|
+
report({ phase: 'index' });
|
|
402
|
+
writeEntry(workdir, indexPath, entry);
|
|
403
|
+
let indexSha = commitOf(exec, workdir, `chore(market): ${entryName}`);
|
|
404
|
+
report({ phase: 'index-committed', indexCommit: indexSha });
|
|
405
|
+
const pushed = pushOf(exec, workdir, branch);
|
|
406
|
+
if (pushed.status !== 0) {
|
|
407
|
+
const replayed = resetToRemote(exec, workdir, branch);
|
|
408
|
+
if (!replayed) {
|
|
409
|
+
phase = 'index';
|
|
410
|
+
throw new MarketIndexError(
|
|
411
|
+
`模块已进仓库、索引没更新(本地 commit ${indexSha}):${tailOf(`${pushed.stdout}\n${pushed.stderr}`)}`,
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
writeEntry(workdir, indexPath, entry);
|
|
415
|
+
indexSha = commitOf(exec, workdir, `chore(market): ${entryName}`);
|
|
416
|
+
report({ phase: 'index-committed', indexCommit: indexSha });
|
|
417
|
+
const retried = pushOf(exec, workdir, branch);
|
|
418
|
+
if (retried.status !== 0) {
|
|
419
|
+
phase = 'index';
|
|
420
|
+
throw new MarketIndexError(`模块已进仓库、索引没更新:${tailOf(`${retried.stdout}\n${retried.stderr}`)}`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
report({ phase: 'index-pushed', indexCommit: indexSha, indexPushed: true });
|
|
424
|
+
const local = runGit(exec, workdir, ['rev-parse', 'HEAD']);
|
|
425
|
+
const headSha = local.status === 0 ? local.stdout.trim() : '';
|
|
426
|
+
const remoteHead = remoteHeadOf(exec, workdir, branch);
|
|
427
|
+
if (headSha === '' || remoteHead !== headSha) {
|
|
428
|
+
phase = 'remote-check';
|
|
429
|
+
throw new MarketIndexError(
|
|
430
|
+
`${plan.policy === 'artifacts-in-repo' ? '两个 commit 没都推上去' : '索引提交没推上去'}:` +
|
|
431
|
+
`远端 ${branch} 是 ${shortOf(remoteHead)},本地是 ${shortOf(headSha)}`,
|
|
432
|
+
'git-market-pushed',
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
report({ phase: 'remote-checked' });
|
|
436
|
+
return {
|
|
437
|
+
...base,
|
|
438
|
+
...(artifactPath === undefined ? {} : { path: artifactPath }),
|
|
439
|
+
commit: source.type === 'git' ? source.commit : indexSha,
|
|
440
|
+
updated: true,
|
|
441
|
+
phase: 'done',
|
|
442
|
+
};
|
|
443
|
+
} catch (error) {
|
|
444
|
+
return {
|
|
445
|
+
...base,
|
|
446
|
+
updated: false,
|
|
447
|
+
phase,
|
|
448
|
+
code: error instanceof MarketIndexError ? error.step : undefined,
|
|
449
|
+
reason: messageOf(error),
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function hasHead(exec, workdir) {
|
|
455
|
+
return runGit(exec, workdir, ['rev-parse', '--verify', 'HEAD'], { allowFailure: true }).status === 0;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function isGithub(url) {
|
|
459
|
+
try {
|
|
460
|
+
return new URL(url).hostname.toLowerCase() === 'github.com';
|
|
461
|
+
} catch {
|
|
462
|
+
return /^[\w.-]+@github\.com:/i.test(url);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function defaultBranchOf(exec, workdir, url, ref) {
|
|
467
|
+
const asked = String(ref ?? '').trim();
|
|
468
|
+
if (asked !== '') return asked;
|
|
469
|
+
const symref = runGit(exec, workdir, ['symbolic-ref', '--short', 'HEAD'], { allowFailure: true });
|
|
470
|
+
const branch = symref.status === 0 ? symref.stdout.trim() : '';
|
|
471
|
+
if (branch !== '') return branch;
|
|
472
|
+
if (isGithub(url)) return 'main';
|
|
473
|
+
throw new MarketIndexError(
|
|
474
|
+
'空仓库要先知道建哪个分支:用 --ref <分支> 指定(例如 main / master)。',
|
|
475
|
+
'index-init',
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
export function initializeMarketRepo(options) {
|
|
480
|
+
const { exec, home, url, dir, ref, indexPath = MARKET_FILE, readme = true } = options;
|
|
481
|
+
let workdir;
|
|
482
|
+
let remote = String(url ?? '').trim();
|
|
483
|
+
if (dir !== undefined) {
|
|
484
|
+
workdir = resolve(dir);
|
|
485
|
+
if (!existsSync(join(workdir, '.git'))) {
|
|
486
|
+
throw new MarketIndexError(`不是一个 git 工作区:${workdir}`, 'index-init');
|
|
487
|
+
}
|
|
488
|
+
const origin = runGit(exec, workdir, ['remote', 'get-url', 'origin'], { allowFailure: true });
|
|
489
|
+
if (origin.status !== 0 || origin.stdout.trim() === '') {
|
|
490
|
+
throw new MarketIndexError(`索引目录没有 origin 远端:${workdir}`, 'index-init');
|
|
491
|
+
}
|
|
492
|
+
remote = origin.stdout.trim();
|
|
493
|
+
} else {
|
|
494
|
+
if (!gitUrlOk(remote)) throw new MarketIndexError(`不是可用的 git 地址:${remote}`, 'index-init');
|
|
495
|
+
workdir = worktreeOf({ source: { kind: 'git', url: remote } }, exec, home);
|
|
496
|
+
}
|
|
497
|
+
assertClean(exec, workdir);
|
|
498
|
+
const head = hasHead(exec, workdir);
|
|
499
|
+
const branch = head ? currentBranch(exec, workdir) : defaultBranchOf(exec, workdir, remote, ref);
|
|
500
|
+
if (!head) {
|
|
501
|
+
const switched = runGit(exec, workdir, ['checkout', '-B', branch], { allowFailure: true });
|
|
502
|
+
if (switched.status !== 0) throw new MarketIndexError(`建分支 ${branch} 没过:${tailOf(switched.stderr)}`, 'index-init');
|
|
503
|
+
}
|
|
504
|
+
const created = [];
|
|
505
|
+
const file = join(workdir, indexPath);
|
|
506
|
+
if (!existsSync(file)) {
|
|
507
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
508
|
+
writeFileSync(file, MARKET_INDEX_SEED);
|
|
509
|
+
created.push(indexPath);
|
|
510
|
+
}
|
|
511
|
+
const readmeFile = join(workdir, 'README.md');
|
|
512
|
+
if (readme && !existsSync(readmeFile)) {
|
|
513
|
+
writeFileSync(readmeFile, `${MARKET_INDEX_README_LINES.join('\n')}`);
|
|
514
|
+
created.push('README.md');
|
|
515
|
+
}
|
|
516
|
+
let commit;
|
|
517
|
+
if (created.length > 0) {
|
|
518
|
+
const added = runGit(exec, workdir, ['add', ...created], { allowFailure: true });
|
|
519
|
+
if (added.status !== 0) throw new MarketIndexError(`git add 没过:${tailOf(added.stderr)}`, 'index-init');
|
|
520
|
+
commit = commitOf(exec, workdir, 'chore: init market index');
|
|
521
|
+
const pushed = pushOf(exec, workdir, branch);
|
|
522
|
+
if (pushed.status !== 0) {
|
|
523
|
+
throw new MarketIndexError(
|
|
524
|
+
`初始化提交推不上去:${tailOf(`${pushed.stdout}\n${pushed.stderr}`)}`,
|
|
525
|
+
'index-init',
|
|
526
|
+
`git -C ${workdir} push origin HEAD:refs/heads/${branch}`,
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
} else {
|
|
530
|
+
const current = runGit(exec, workdir, ['rev-parse', 'HEAD'], { allowFailure: true });
|
|
531
|
+
commit = current.status === 0 ? current.stdout.trim() : '';
|
|
532
|
+
}
|
|
533
|
+
return { ok: true, url: remote, branch, commit, created, changed: created.length > 0, firstBranch: !head };
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
export function marketVersionExists({ plan, exec, home, name, version }) {
|
|
537
|
+
try {
|
|
538
|
+
const workdir = worktreeOf(plan, exec, home);
|
|
539
|
+
const file = join(workdir, plan.source.path ?? MARKET_FILE);
|
|
540
|
+
if (!existsSync(file)) return false;
|
|
541
|
+
const parsed = parseMarketIndexJson(readFileSync(file, 'utf8'));
|
|
542
|
+
if (!parsed.ok) return false;
|
|
543
|
+
const entry = parsed.index.plugins.find((one) => one.name === name);
|
|
544
|
+
return entry?.versions.some((one) => one.version === version) === true;
|
|
545
|
+
} catch {
|
|
546
|
+
return false;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
export function verifyMarketArtifact({ plan, exec, home, unit, commit }) {
|
|
551
|
+
const workdir = worktreeOf(plan, exec, home);
|
|
552
|
+
const found = runGit(exec, workdir, ['cat-file', '-e', `${commit}^{commit}`], { allowFailure: true });
|
|
553
|
+
if (found.status !== 0) {
|
|
554
|
+
throw new MarketIndexError(`索引仓库里找不到 commit ${commit}`, 'market-verify');
|
|
555
|
+
}
|
|
556
|
+
const tree = runGit(exec, workdir, ['ls-tree', '-r', '--name-only', commit], { allowFailure: true });
|
|
557
|
+
if (tree.status !== 0) {
|
|
558
|
+
throw new MarketIndexError(`读不了 commit ${commit} 的内容:${tailOf(tree.stderr)}`, 'market-verify');
|
|
559
|
+
}
|
|
560
|
+
const prefix = `${PACKAGES_DIR}/${unit}/`;
|
|
561
|
+
const files = tree.stdout
|
|
562
|
+
.split('\n')
|
|
563
|
+
.map((one) => one.trim())
|
|
564
|
+
.filter((one) => one.startsWith(prefix))
|
|
565
|
+
.map((one) => one.slice(prefix.length));
|
|
566
|
+
return { path: `${PACKAGES_DIR}/${unit}`, files };
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
export { MARKET_FILE };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import { execaSync } from 'execa';
|
|
5
|
+
|
|
6
|
+
import { MarketIndexError, initializeMarketRepo } from './market-index.mjs';
|
|
7
|
+
|
|
8
|
+
function argValue(args, flag) {
|
|
9
|
+
const at = args.indexOf(flag);
|
|
10
|
+
return at === -1 ? undefined : args[at + 1];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function targetOf(args) {
|
|
14
|
+
const takesValue = new Set(['--ref', '--path']);
|
|
15
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
16
|
+
const one = args[index];
|
|
17
|
+
if (takesValue.has(one)) {
|
|
18
|
+
index += 1;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (!one.startsWith('--')) return one;
|
|
22
|
+
}
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function exec(command, argv, options = {}) {
|
|
27
|
+
const result = execaSync(command, argv, { cwd: options.cwd, reject: false, stdio: 'pipe' });
|
|
28
|
+
return { status: result.exitCode ?? 0, stdout: result.stdout ?? '', stderr: result.stderr ?? '' };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function shortSha(sha) {
|
|
32
|
+
return typeof sha === 'string' && sha.length >= 7 ? sha.slice(0, 7) : sha;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function marketInit({ args }) {
|
|
36
|
+
const target = targetOf(args);
|
|
37
|
+
if (target === undefined) {
|
|
38
|
+
console.error('✖ 用法:dsh-wb-module market-init <git 地址|目录> [--ref <分支>] [--path <索引路径>] [--no-readme] [--json]');
|
|
39
|
+
process.exit(2);
|
|
40
|
+
}
|
|
41
|
+
const json = args.includes('--json');
|
|
42
|
+
const ref = argValue(args, '--ref');
|
|
43
|
+
const indexPath = argValue(args, '--path');
|
|
44
|
+
const isDir = existsSync(resolve(target));
|
|
45
|
+
const configured = process.env.DSH_HOME?.trim() ?? '';
|
|
46
|
+
const home = configured === '' ? join(homedir(), '.dsh') : configured;
|
|
47
|
+
try {
|
|
48
|
+
const result = initializeMarketRepo({
|
|
49
|
+
exec,
|
|
50
|
+
home,
|
|
51
|
+
...(isDir ? { dir: resolve(target) } : { url: target }),
|
|
52
|
+
...(ref === undefined ? {} : { ref }),
|
|
53
|
+
...(indexPath === undefined ? {} : { indexPath }),
|
|
54
|
+
readme: !args.includes('--no-readme'),
|
|
55
|
+
});
|
|
56
|
+
if (json) {
|
|
57
|
+
console.log(JSON.stringify(result));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (result.changed) {
|
|
61
|
+
console.log(`✔ 市场索引已初始化:${result.url}`);
|
|
62
|
+
console.log(` 分支:${result.branch} · commit:${shortSha(result.commit)}`);
|
|
63
|
+
console.log(` 写了:${result.created.join('、')}`);
|
|
64
|
+
console.log(' 下一步:在工作台「模块市场 → 发现」里添加这个地址');
|
|
65
|
+
if (result.firstBranch) {
|
|
66
|
+
console.log(` 提示:这是空仓库的第一个分支;如果 ${result.branch} 不是仓库默认分支,去仓库设置里把默认分支改成它`);
|
|
67
|
+
}
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
console.log(`✔ 市场索引已就绪:${result.url}`);
|
|
71
|
+
console.log(` 分支:${result.branch} · HEAD:${shortSha(result.commit)} —— 没改动`);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (error instanceof MarketIndexError) {
|
|
74
|
+
console.error(`✖ ${error.message}`);
|
|
75
|
+
if (error.command !== undefined) console.error(` ${error.command}`);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
}
|