@dp-bohrium/widget-cli 0.0.0-stage → 0.1.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/dist/cli.mjs ADDED
@@ -0,0 +1,2251 @@
1
+ #!/usr/bin/env node
2
+ import { a as resolveWidgetProjects, i as checkWidgetProject, n as scaffoldDshWidget, o as compileAuthorManifest, r as buildWidgetPackage, t as auditDshPlugin } from "./dsh-DgkNH7L-.mjs";
3
+ import { createRequire } from "node:module";
4
+ import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
5
+ import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync, statSync, watch } from "node:fs";
6
+ import { mkdir, writeFile } from "node:fs/promises";
7
+ import { basename, dirname, join, relative, resolve, sep } from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+ import { spawn, spawnSync } from "node:child_process";
10
+ import { createServer } from "node:http";
11
+ //#region ../../scripts/widget/widget-dev.mjs
12
+ const repoRoot = resolve(fileURLToPath(new URL("../..", import.meta.url)));
13
+ const DEFAULT_PORT = 4300;
14
+ const PROFILE_PATH = "/__widget/profile";
15
+ const STATUS_PATH = "/__widget/status";
16
+ const LAUNCH_PATH = "/__widget/launch.html";
17
+ const V1_ROOT = "/__widget/v1";
18
+ const DESCRIPTOR_PATH = `${V1_ROOT}/descriptor`;
19
+ const CLAIM_PATH = `${V1_ROOT}/claim`;
20
+ const V1_STATUS_PATH = `${V1_ROOT}/status`;
21
+ const V1_PROFILE_PATH = `${V1_ROOT}/profile`;
22
+ const EVENTS_PATH = `${V1_ROOT}/events`;
23
+ const CONTROL_PATH = `${V1_ROOT}/control`;
24
+ const GUEST_ROOT = "/__widget/guest";
25
+ const DEVELOPMENT_PROTOCOL = "bohrium.widget-development-host";
26
+ const CLI_PROTOCOL = "bohrium.widget-development-cli";
27
+ const SESSION_HARD_TTL_MS = 864e5;
28
+ const SSE_HEARTBEAT_INTERVAL_MS = 15e3;
29
+ const RETAINED_GENERATIONS = 2;
30
+ const GENERATION_LIMITS = Object.freeze({
31
+ fileCount: 256,
32
+ singleFileBytes: 25e6,
33
+ totalBytes: 1e8
34
+ });
35
+ const LOCAL_BRIDGE_HOST = "127.0.0.1";
36
+ const LOCAL_BRIDGE_PORTS = [
37
+ 47850,
38
+ 47851,
39
+ 47852,
40
+ 47853,
41
+ 47854
42
+ ];
43
+ const OPEN_WIDGET_DEVELOPMENT_PATH = "/widget-host/desktop/open-widget-development";
44
+ const LOCAL_BRIDGE_TIMEOUT_MS = 350;
45
+ const RESOURCE_BUDGET = {
46
+ version: 1,
47
+ bundle: {
48
+ compressedBytes: 5e7,
49
+ expandedBytes: 1e8,
50
+ fileCount: 256,
51
+ singleFileBytes: 25e6
52
+ },
53
+ bridge: {
54
+ messageBytes: 256e3,
55
+ messagesPerSecond: 60,
56
+ pendingRequests: 32
57
+ },
58
+ files: {
59
+ readBytesPerRequest: 4194304,
60
+ readBytesPerRun: 16777216,
61
+ callsPerMinute: 120,
62
+ listPageSize: 100
63
+ },
64
+ timeouts: {
65
+ bundleLoadMs: 1e4,
66
+ handshakeMs: 1e4,
67
+ firstRenderMs: 1e4,
68
+ updateMs: 1e4,
69
+ capabilityMs: 15e3,
70
+ disposeGraceMs: 1e3
71
+ },
72
+ concurrency: {
73
+ activeIframesPerPage: 4,
74
+ activeIframesPerRun: 2,
75
+ hiddenRecycleMs: 6e4
76
+ },
77
+ retry: {
78
+ maxAttempts: 1,
79
+ initialDelayMs: 0,
80
+ maxDelayMs: 0,
81
+ totalMs: 0
82
+ },
83
+ failure: {
84
+ consecutiveFailures: 3,
85
+ windowMs: 6e4
86
+ }
87
+ };
88
+ const NETWORK_LIMITS = {
89
+ timeoutMs: 15e3,
90
+ maxRedirects: 4,
91
+ maxConcurrentRequests: 4,
92
+ callsPerMinute: 60,
93
+ requestHeaderBytes: 16384,
94
+ responseHeaderBytes: 32768,
95
+ requestBodyBytes: 1048576,
96
+ responseBodyBytes: 8388608
97
+ };
98
+ function usage$1() {
99
+ return `用法:pnpm widget:dev <project-dir|manifest.json> [选项]
100
+
101
+ 选项:
102
+ --open 启动后唤起宿主
103
+ --scheme <scheme> deep link scheme(默认 widget-host;可按宿主配置覆盖)
104
+ --bridge <url> 指定宿主 loopback bridge;未指定时探测标准 bridge 端口
105
+ --json 输出机器可读启动结果和错误
106
+ --watch 监听 src、manifest.json 和 package.json,变化后重新 build
107
+ --no-build 使用现有的 manifest.widgetConfig.entry,不执行 build
108
+ --port <port> 固定 Profile 服务端口(默认 ${DEFAULT_PORT})
109
+ --help 显示帮助
110
+
111
+ 开发流程:
112
+ 1. 在 Widget 工程目录执行此命令。
113
+ 2. 使用 --open(优先连接已运行的 host bridge),或复制命令输出的 <scheme>://widget-dev URL。
114
+ 3. 在生产宿主中触发对应 Tool Result / File Preview / Panel。
115
+ 4. 修改源码;--watch 会重建 Bundle。需要重新加载时,手动打开上方 Host URL。
116
+ `;
117
+ }
118
+ function parseArgs(argv) {
119
+ const positional = [];
120
+ const options = {
121
+ open: false,
122
+ watch: false,
123
+ build: true,
124
+ port: DEFAULT_PORT,
125
+ scheme: "widget-host",
126
+ bridge: null,
127
+ json: false
128
+ };
129
+ for (let index = 0; index < argv.length; index += 1) {
130
+ const argument = argv[index];
131
+ if (argument === "--help" || argument === "-h") return { help: true };
132
+ if (argument === "--json") options.json = true;
133
+ else if (argument === "--open") options.open = true;
134
+ else if (argument === "--watch") options.watch = true;
135
+ else if (argument === "--no-build") options.build = false;
136
+ else if (argument === "--scheme") {
137
+ const value = argv[++index];
138
+ if (!value || !/^[a-z][a-z0-9+.-]*$/u.test(value)) throw new Error("--scheme 必须是合法的 URI scheme");
139
+ options.scheme = value;
140
+ } else if (argument === "--bridge") {
141
+ const value = argv[++index];
142
+ if (!value) throw new Error("--bridge 需要一个 loopback bridge URL");
143
+ let bridge;
144
+ try {
145
+ bridge = new URL(value);
146
+ } catch {
147
+ throw new Error("--bridge 必须是合法的 HTTP(S) URL");
148
+ }
149
+ if (bridge.protocol !== "http:" && bridge.protocol !== "https:") throw new Error("--bridge 必须使用 HTTP(S) 协议");
150
+ if (![
151
+ "localhost",
152
+ "127.0.0.1",
153
+ "[::1]",
154
+ "::1"
155
+ ].includes(bridge.hostname)) throw new Error("--bridge 必须指向 loopback 主机");
156
+ if (bridge.username || bridge.password || bridge.hash || bridge.search) throw new Error("--bridge 不得包含认证信息、query 或 hash");
157
+ const port = Number(bridge.port);
158
+ if (!bridge.port || !Number.isInteger(port) || port < 1024 || port > 65535) throw new Error("--bridge 必须包含 1024 到 65535 之间的显式端口");
159
+ options.bridge = bridge.href;
160
+ } else if (argument === "--port") {
161
+ const value = Number(argv[++index]);
162
+ if (!Number.isInteger(value) || value < 1024 || value > 65535) throw new Error("--port 必须是 1024 到 65535 之间的整数");
163
+ options.port = value;
164
+ } else if (argument.startsWith("-")) throw new Error(`未知选项:${argument}`);
165
+ else positional.push(argument);
166
+ }
167
+ if (positional.length !== 1) throw new Error("需要一个 Widget 工程目录或 manifest.json");
168
+ return {
169
+ help: false,
170
+ project: positional[0],
171
+ ...options
172
+ };
173
+ }
174
+ function createWidgetDevError(error, context = {}) {
175
+ return {
176
+ code: typeof error?.code === "string" && error.code.startsWith("WIDGET_DEV_") ? error.code : "WIDGET_DEV_FAILED",
177
+ message: error instanceof Error ? error.message : String(error),
178
+ context,
179
+ recovery: context.phase === "watch-build" ? "读取 status.lastBuildError,修复工程后重新保存源码或重新执行 build。" : "先读取 error.context 和 build 输出,修复工程或宿主连接后重试。"
180
+ };
181
+ }
182
+ function secureHeaders(contentType) {
183
+ return {
184
+ "cache-control": "no-store",
185
+ "content-type": contentType,
186
+ "referrer-policy": "no-referrer",
187
+ "x-content-type-options": "nosniff"
188
+ };
189
+ }
190
+ function json(response, status, value, extraHeaders = {}) {
191
+ const body = JSON.stringify(value);
192
+ response.writeHead(status, {
193
+ ...secureHeaders("application/json; charset=utf-8"),
194
+ "content-length": Buffer.byteLength(body),
195
+ ...extraHeaders
196
+ });
197
+ response.end(body);
198
+ }
199
+ function token() {
200
+ return randomBytes(32).toString("base64url");
201
+ }
202
+ function equalToken(actual, expected) {
203
+ if (typeof actual !== "string") return false;
204
+ const actualBytes = Buffer.from(actual);
205
+ const expectedBytes = Buffer.from(expected);
206
+ return actualBytes.length === expectedBytes.length && timingSafeEqual(actualBytes, expectedBytes);
207
+ }
208
+ function bearer(header) {
209
+ return /^Bearer ([A-Za-z0-9_-]+)$/u.exec(String(header ?? ""))?.[1] ?? null;
210
+ }
211
+ function formatDevelopmentEvent(type, data) {
212
+ return JSON.stringify({
213
+ protocol: CLI_PROTOCOL,
214
+ version: 1,
215
+ type,
216
+ data
217
+ });
218
+ }
219
+ function createWidgetDevelopmentSession({ origin, sessionId = token(), now = /* @__PURE__ */ new Date() }) {
220
+ const claimToken = token();
221
+ const accessToken = token();
222
+ const issuedAt = now.toISOString();
223
+ const expiresAt = new Date(now.getTime() + SESSION_HARD_TTL_MS).toISOString();
224
+ let claimed = false;
225
+ let disconnected = false;
226
+ const descriptor = Object.freeze({
227
+ protocol: DEVELOPMENT_PROTOCOL,
228
+ protocolVersion: 1,
229
+ sessionId,
230
+ issuedAt,
231
+ expiresAt,
232
+ origin,
233
+ endpoints: Object.freeze({
234
+ claim: CLAIM_PATH,
235
+ status: V1_STATUS_PATH,
236
+ events: EVENTS_PATH,
237
+ control: CONTROL_PATH,
238
+ profile: V1_PROFILE_PATH
239
+ })
240
+ });
241
+ return Object.freeze({
242
+ claimToken,
243
+ accessToken,
244
+ descriptor,
245
+ claim(candidate) {
246
+ if (claimed) throw new Error("Widget development session already claimed(已领取)");
247
+ if (Date.now() >= Date.parse(expiresAt)) throw new Error("claim token expired");
248
+ if (!equalToken(candidate, claimToken)) throw new Error("invalid claim token");
249
+ claimed = true;
250
+ return {
251
+ tokenType: "Bearer",
252
+ accessToken,
253
+ expiresAt
254
+ };
255
+ },
256
+ authorize(header) {
257
+ return !disconnected && Date.now() < Date.parse(expiresAt) && claimed && equalToken(bearer(header), accessToken);
258
+ },
259
+ disconnect() {
260
+ disconnected = true;
261
+ }
262
+ });
263
+ }
264
+ function slug(value) {
265
+ return String(value).toLowerCase().replace(/[^a-z0-9]+/gu, "-").replace(/^-|-$/gu, "") || "widget";
266
+ }
267
+ function requestedGrants(requested = {}) {
268
+ const grants = [];
269
+ const files = requested.files ?? {};
270
+ if (files.list) grants.push({
271
+ name: "files.list",
272
+ limits: {
273
+ pageSize: 100,
274
+ callsPerMinute: 120
275
+ }
276
+ });
277
+ if (files.metadata) grants.push({
278
+ name: "files.metadata",
279
+ limits: { callsPerMinute: 120 }
280
+ });
281
+ if (files.read) grants.push({
282
+ name: "files.read",
283
+ limits: {
284
+ bytesPerRequest: 4194304,
285
+ bytesPerRun: 16777216,
286
+ callsPerMinute: 120
287
+ }
288
+ });
289
+ if (requested.events?.topics?.length) grants.push({
290
+ name: "events.read",
291
+ topics: [...requested.events.topics],
292
+ limits: {
293
+ callsPerMinute: 120,
294
+ pageSize: 100,
295
+ minPollMs: 1e3
296
+ }
297
+ });
298
+ if (requested.actions?.names?.length) grants.push({
299
+ name: "actions.invoke",
300
+ actions: [...requested.actions.names],
301
+ limits: { callsPerMinute: 120 }
302
+ });
303
+ if (requested.network?.fetch) grants.push({
304
+ name: "network.fetch",
305
+ limits: NETWORK_LIMITS
306
+ });
307
+ if (requested.presentation) grants.push({
308
+ name: "ui.presentation",
309
+ surfaces: [...requested.presentation.surfaces],
310
+ requiresHostConfirmation: true
311
+ });
312
+ return grants;
313
+ }
314
+ function createDevelopmentConfig({ manifest, runId, origin, launchUrl, digest, now = /* @__PURE__ */ new Date() }) {
315
+ const issuedAt = now.toISOString();
316
+ const expiresAt = new Date(now.getTime() + 6e5).toISOString();
317
+ const widgetId = slug(manifest.name);
318
+ return {
319
+ schemaVersion: 1,
320
+ hostApiVersion: 1,
321
+ configId: `widget-dev-${digest.slice(-12)}`,
322
+ runId,
323
+ mode: "preview",
324
+ issuedAt,
325
+ expiresAt,
326
+ recheckAt: new Date(now.getTime() + 3e5).toISOString(),
327
+ securityProfile: "runtime-security-profile-v1",
328
+ resourceBudget: RESOURCE_BUDGET,
329
+ runtime: {
330
+ origin,
331
+ catalogRevision: "widget-dev-local"
332
+ },
333
+ network: {
334
+ mode: "unavailable",
335
+ policyVersion: "widget-dev-local",
336
+ limits: NETWORK_LIMITS
337
+ },
338
+ bindings: [{
339
+ status: "loadable",
340
+ bindingId: `widget-dev-binding-${widgetId}`,
341
+ widgetId: `widget-dev-widget-${widgetId}`,
342
+ widgetVersionId: `widget-dev-version-${digest.slice(-12)}`,
343
+ digest,
344
+ manifest,
345
+ launch: {
346
+ url: launchUrl,
347
+ expiresAt
348
+ },
349
+ grantedCapabilities: requestedGrants(manifest.widgetConfig.requestedCapabilities)
350
+ }]
351
+ };
352
+ }
353
+ function createLaunchHtml(entryPath) {
354
+ return `<!doctype html><html><head><meta charset="utf-8"><title>Widget Development Host</title></head><body><script src="/${entryPath.split("/").map(encodeURIComponent).join("/")}"><\/script></body></html>`;
355
+ }
356
+ function guestGenerationRoot(generation) {
357
+ return `${GUEST_ROOT}/${generation.guestCapability}`;
358
+ }
359
+ function panelDevelopmentDescriptor(generation, profileUrl, statusUrl) {
360
+ const contribution = generation.manifest.widgetConfig.contribution;
361
+ const descriptor = {
362
+ profileUrl,
363
+ statusUrl
364
+ };
365
+ if (contribution.type !== "panel") return {
366
+ descriptor,
367
+ presentationRequests: []
368
+ };
369
+ const id = contribution.id;
370
+ const key = `widget-dev-panel-${slug(id)}`;
371
+ const runId = "widget-dev-run";
372
+ const widgetId = `widget-dev-widget-${slug(generation.manifest.name)}`;
373
+ const widgetVersionId = `widget-dev-version-${generation.digest.slice(-12)}`;
374
+ const requestedSurfaces = generation.manifest.widgetConfig.requestedCapabilities?.presentation?.surfaces;
375
+ const allowedSurfaces = ["workspace", ...Array.isArray(requestedSurfaces) ? requestedSurfaces : []].filter((surface, index, surfaces) => surfaces.indexOf(surface) === index);
376
+ const defaultSurface = allowedSurfaces[0];
377
+ const panel = {
378
+ key,
379
+ runId,
380
+ widgetId,
381
+ widgetVersionId,
382
+ snapshot: {
383
+ id,
384
+ title: generation.manifest.name,
385
+ state: "complete",
386
+ data: {}
387
+ },
388
+ presentation: {
389
+ allowedSurfaces,
390
+ defaultSurface,
391
+ movable: true,
392
+ resizable: true
393
+ }
394
+ };
395
+ const presentationRequests = [{
396
+ requestId: `widget-dev-present-${slug(id)}`,
397
+ key,
398
+ surface: defaultSurface
399
+ }];
400
+ return {
401
+ descriptor: {
402
+ ...descriptor,
403
+ panel,
404
+ presentationRequests
405
+ },
406
+ presentationRequests
407
+ };
408
+ }
409
+ function readProject(projectRoot) {
410
+ const sourceManifest = JSON.parse(readFileSync(join(projectRoot, "manifest.json"), "utf8"));
411
+ const manifest = compileAuthorManifest(sourceManifest);
412
+ const entryPath = manifest.widgetConfig.entry;
413
+ const entryFile = join(projectRoot, entryPath);
414
+ if (!existsSync(entryFile) || statSync(entryFile).size === 0) throw new Error(`entry 不存在或为空:${entryPath};先运行 build,或移除 --no-build`);
415
+ return {
416
+ manifest,
417
+ entryPath,
418
+ digest: `sha256:${createHash("sha256").update(readFileSync(entryFile)).digest("hex")}`
419
+ };
420
+ }
421
+ function contentType(path) {
422
+ if (path.endsWith(".js") || path.endsWith(".mjs")) return "text/javascript; charset=utf-8";
423
+ if (path.endsWith(".css")) return "text/css; charset=utf-8";
424
+ if (path.endsWith(".json")) return "application/json; charset=utf-8";
425
+ if (path.endsWith(".svg")) return "image/svg+xml";
426
+ if (path.endsWith(".png")) return "image/png";
427
+ if (path.endsWith(".jpg") || path.endsWith(".jpeg")) return "image/jpeg";
428
+ if (path.endsWith(".woff2")) return "font/woff2";
429
+ return "application/octet-stream";
430
+ }
431
+ function collectGenerationAssets(projectRoot, entryPath, limits) {
432
+ const rootName = entryPath.split("/")[0];
433
+ const assetRoot = resolve(projectRoot, rootName);
434
+ if (assetRoot !== projectRoot && !assetRoot.startsWith(`${projectRoot}${sep}`)) throw new Error(`entry 路径越界:${entryPath}`);
435
+ if (!existsSync(assetRoot) || !statSync(assetRoot).isDirectory()) throw new Error(`entry 资源目录不存在:${rootName}`);
436
+ const files = [];
437
+ const visit = (directory) => {
438
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
439
+ const file = join(directory, entry.name);
440
+ if (entry.isSymbolicLink() || lstatSync(file).isSymbolicLink()) throw new Error(`generation 不允许 symbolic link(符号链接):${relative(projectRoot, file)}`);
441
+ if (entry.isDirectory()) visit(file);
442
+ else if (entry.isFile()) files.push(file);
443
+ }
444
+ };
445
+ visit(assetRoot);
446
+ if (files.length > limits.fileCount) throw new Error(`generation 资源数量超限:${files.length}/${limits.fileCount}`);
447
+ let totalBytes = 0;
448
+ return files.map((file) => {
449
+ const path = relative(projectRoot, file).replaceAll("\\", "/");
450
+ if (path.startsWith("../") || path.split("/").includes("..")) throw new Error(`generation 资源路径越界:${path}`);
451
+ const bytes = readFileSync(file);
452
+ if (bytes.byteLength > limits.singleFileBytes) throw new Error(`generation 单文件超限:${path}`);
453
+ totalBytes += bytes.byteLength;
454
+ if (totalBytes > limits.totalBytes) throw new Error(`generation 总字节数超限:${totalBytes}/${limits.totalBytes}`);
455
+ return {
456
+ path,
457
+ bytes
458
+ };
459
+ }).sort((a, b) => a.path.localeCompare(b.path));
460
+ }
461
+ function createDevelopmentGeneration(projectRoot, number = 0, limits = GENERATION_LIMITS) {
462
+ const { manifest, entryPath } = readProject(projectRoot);
463
+ const collected = collectGenerationAssets(projectRoot, entryPath, limits);
464
+ if (!collected.some(({ path }) => path === entryPath)) throw new Error(`entry 未进入 generation:${entryPath}`);
465
+ const hash = createHash("sha256");
466
+ const assets = [];
467
+ const assetBytes = /* @__PURE__ */ new Map();
468
+ for (const { path, bytes } of collected) {
469
+ const assetDigest = `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
470
+ assets.push(Object.freeze({
471
+ path,
472
+ bytes: bytes.byteLength,
473
+ digest: assetDigest
474
+ }));
475
+ assetBytes.set(path, Buffer.from(bytes));
476
+ hash.update(path).update("\0").update(assetDigest).update("\0");
477
+ }
478
+ const digest = `sha256:${hash.digest("hex")}`;
479
+ return Object.freeze({
480
+ number,
481
+ digest,
482
+ guestCapability: token(),
483
+ manifest: Object.freeze(manifest),
484
+ entryPath,
485
+ assets: Object.freeze(assets),
486
+ getAsset(path) {
487
+ const bytes = assetBytes.get(path);
488
+ return bytes ? Buffer.from(bytes) : null;
489
+ }
490
+ });
491
+ }
492
+ function serveGenerationAsset(generation, path, response) {
493
+ const bytes = generation.getAsset(path);
494
+ if (!bytes) {
495
+ response.writeHead(404, secureHeaders("text/plain; charset=utf-8")).end("not found");
496
+ return;
497
+ }
498
+ response.writeHead(200, {
499
+ ...secureHeaders(contentType(path)),
500
+ "content-length": bytes.byteLength,
501
+ etag: `"${generation.assets.find((asset) => asset.path === path).digest}"`
502
+ });
503
+ response.end(bytes);
504
+ }
505
+ function createWidgetDevelopmentBridgeUrl(profileUrl, bridge) {
506
+ const base = typeof bridge === "number" ? `http://${LOCAL_BRIDGE_HOST}:${bridge}${OPEN_WIDGET_DEVELOPMENT_PATH}` : bridge;
507
+ const url = new URL(base);
508
+ url.searchParams.set("profile", profileUrl);
509
+ return url.href;
510
+ }
511
+ function createWidgetDevelopmentV1BridgeUrl(bridge) {
512
+ return typeof bridge === "number" ? `http://${LOCAL_BRIDGE_HOST}:${bridge}/widget-host/desktop/v1/development-sessions` : bridge;
513
+ }
514
+ function createWidgetDevelopmentBridgeRequest({ descriptorUrl, session, expiresAt }) {
515
+ return {
516
+ protocol: DEVELOPMENT_PROTOCOL,
517
+ protocolVersion: 1,
518
+ sessionId: session.descriptor.sessionId,
519
+ descriptorUrl,
520
+ expiresAt
521
+ };
522
+ }
523
+ async function openViaLocalBridge(profileUrl, bridgeUrl, developmentSession = null, descriptorUrl = null, expiresAt = null) {
524
+ let origin;
525
+ try {
526
+ origin = new URL(profileUrl).origin;
527
+ } catch {
528
+ return {
529
+ ok: false,
530
+ mode: "bridge",
531
+ error: createWidgetDevError(/* @__PURE__ */ new Error("Profile URL 无效"), {
532
+ phase: "bridge",
533
+ profileUrl
534
+ })
535
+ };
536
+ }
537
+ const bridgeBases = bridgeUrl ? [bridgeUrl] : LOCAL_BRIDGE_PORTS.map((port) => `http://${LOCAL_BRIDGE_HOST}:${port}${OPEN_WIDGET_DEVELOPMENT_PATH}`);
538
+ for (const bridgeBase of bridgeBases) {
539
+ if (developmentSession && descriptorUrl) {
540
+ const v1Endpoint = createWidgetDevelopmentV1BridgeUrl(bridgeUrl ?? Number(new URL(bridgeBase).port));
541
+ const controller = new AbortController();
542
+ const timeout = setTimeout(() => controller.abort(), LOCAL_BRIDGE_TIMEOUT_MS);
543
+ try {
544
+ const response = await fetch(v1Endpoint, {
545
+ method: "POST",
546
+ headers: {
547
+ Authorization: `Bearer ${developmentSession.claimToken}`,
548
+ "Content-Type": "application/json",
549
+ Origin: origin
550
+ },
551
+ body: JSON.stringify(createWidgetDevelopmentBridgeRequest({
552
+ descriptorUrl,
553
+ session: developmentSession,
554
+ expiresAt
555
+ })),
556
+ signal: controller.signal
557
+ });
558
+ const body = response.ok ? await response.json() : null;
559
+ if (body?.accepted === true && body.protocolVersion === 1 && body.sessionId === developmentSession.descriptor.sessionId) return {
560
+ ok: true,
561
+ mode: "bridge-v1",
562
+ endpoint: v1Endpoint
563
+ };
564
+ } catch {} finally {
565
+ clearTimeout(timeout);
566
+ }
567
+ }
568
+ const bridge = createWidgetDevelopmentBridgeUrl(profileUrl, bridgeBase);
569
+ const controller = new AbortController();
570
+ const timeout = setTimeout(() => controller.abort(), LOCAL_BRIDGE_TIMEOUT_MS);
571
+ try {
572
+ const response = await fetch(bridge, {
573
+ headers: { Origin: origin },
574
+ signal: controller.signal
575
+ });
576
+ if (!response.ok) continue;
577
+ if ((await response.json())?.opened === true) return {
578
+ ok: true,
579
+ mode: "bridge-legacy",
580
+ endpoint: bridge,
581
+ warning: "宿主不支持 Widget Development Host v1,已回落 legacy profile。"
582
+ };
583
+ } catch {} finally {
584
+ clearTimeout(timeout);
585
+ }
586
+ }
587
+ return {
588
+ ok: false,
589
+ mode: "bridge",
590
+ error: createWidgetDevError(/* @__PURE__ */ new Error("未发现可用的 loopback host bridge"), {
591
+ phase: "bridge",
592
+ profileUrl,
593
+ endpoints: bridgeBases
594
+ })
595
+ };
596
+ }
597
+ async function openHost(url, profileUrl, bridgeUrl, { jsonOutput = false, developmentSession = null, descriptorUrl = null, expiresAt = null } = {}) {
598
+ const bridgeResult = await openViaLocalBridge(profileUrl, bridgeUrl, developmentSession, descriptorUrl, expiresAt);
599
+ if (bridgeResult.ok) {
600
+ if (!jsonOutput) {
601
+ console.log("已通过本地 host bridge 打开 Widget Development Host。\n");
602
+ if (bridgeResult.warning) console.warn(bridgeResult.warning);
603
+ }
604
+ return bridgeResult;
605
+ }
606
+ const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
607
+ const args = process.platform === "win32" ? ["", url] : [url];
608
+ const result = spawnSync(command, args, {
609
+ encoding: "utf8",
610
+ shell: process.platform === "win32"
611
+ });
612
+ if (result.error || result.status !== 0) {
613
+ const detail = result.error?.message || result.stderr?.trim() || `exit ${result.status}`;
614
+ const failure = {
615
+ ok: false,
616
+ mode: "deep-link",
617
+ url,
618
+ error: createWidgetDevError(new Error(detail), {
619
+ phase: "open-host",
620
+ scheme: new URL(url).protocol.slice(0, -1),
621
+ bridge: bridgeResult.error
622
+ })
623
+ };
624
+ if (!jsonOutput) {
625
+ console.error(`无法唤起宿主(scheme=${failure.error.context.scheme}):${detail}`);
626
+ console.error("请确认生产宿主已安装并注册该 scheme,或复制上方 URL 手动打开。");
627
+ }
628
+ return failure;
629
+ }
630
+ return {
631
+ ok: true,
632
+ mode: "deep-link",
633
+ url,
634
+ bridge: bridgeResult.error
635
+ };
636
+ }
637
+ function projectFingerprint(projectRoot) {
638
+ const files = [];
639
+ const visit = (directory) => {
640
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
641
+ if (entry.name === "node_modules" || entry.name === "dist" || entry.name.startsWith(".")) continue;
642
+ const path = join(directory, entry.name);
643
+ if (entry.isDirectory()) visit(path);
644
+ else if (entry.isFile()) {
645
+ const stat = statSync(path);
646
+ files.push(`${path}:${stat.mtimeMs}:${stat.size}`);
647
+ }
648
+ }
649
+ };
650
+ visit(projectRoot);
651
+ return files.sort().join("\n");
652
+ }
653
+ function shouldIgnoreProjectChange(filename, projectRoot = "") {
654
+ const normalized = String(filename).replaceAll("\\", "/");
655
+ const rootName = projectRoot.replaceAll("\\", "/").split("/").at(-1);
656
+ return normalized === "dist" || normalized.startsWith("dist/") || normalized.includes("/dist/") || normalized.includes("node_modules") || normalized.startsWith("_tmp_") || Boolean(rootName && normalized === rootName);
657
+ }
658
+ function createProjectWatcher(projectRoot, onChange) {
659
+ try {
660
+ const watcher = watch(projectRoot, { recursive: true }, (_event, filename) => {
661
+ if (!filename || shouldIgnoreProjectChange(filename, projectRoot)) return;
662
+ onChange();
663
+ });
664
+ return { close: () => watcher.close() };
665
+ } catch (error) {
666
+ console.warn(`递归文件监听不可用,改用轮询:${String(error)}`);
667
+ let previous = projectFingerprint(projectRoot);
668
+ const timer = setInterval(() => {
669
+ const current = projectFingerprint(projectRoot);
670
+ if (current === previous) return;
671
+ previous = current;
672
+ onChange();
673
+ }, 500);
674
+ return { close: () => clearInterval(timer) };
675
+ }
676
+ }
677
+ function runBuild(projectRoot, { capture = false } = {}) {
678
+ return new Promise((resolveBuild, rejectBuild) => {
679
+ const command = process.platform === "win32" ? "pnpm.cmd" : "pnpm";
680
+ const child = spawn(command, [
681
+ "--dir",
682
+ projectRoot,
683
+ "run",
684
+ "build"
685
+ ], { stdio: capture ? [
686
+ "ignore",
687
+ "pipe",
688
+ "pipe"
689
+ ] : "inherit" });
690
+ let stdout = "";
691
+ let stderr = "";
692
+ if (capture) {
693
+ child.stdout.on("data", (chunk) => {
694
+ stdout += String(chunk);
695
+ });
696
+ child.stderr.on("data", (chunk) => {
697
+ stderr += String(chunk);
698
+ });
699
+ }
700
+ child.once("error", (error) => {
701
+ error.code = "WIDGET_DEV_BUILD_FAILED";
702
+ error.stdout = stdout;
703
+ error.stderr = stderr;
704
+ rejectBuild(error);
705
+ });
706
+ child.once("exit", (code, signal) => {
707
+ if (code === 0) {
708
+ resolveBuild({
709
+ stdout,
710
+ stderr
711
+ });
712
+ return;
713
+ }
714
+ const error = /* @__PURE__ */ new Error(`Widget build 失败(exit ${code ?? signal ?? "unknown"})`);
715
+ error.code = "WIDGET_DEV_BUILD_FAILED";
716
+ error.stdout = stdout;
717
+ error.stderr = stderr;
718
+ rejectBuild(error);
719
+ });
720
+ });
721
+ }
722
+ async function startWidgetDevelopment(projectRoot, options) {
723
+ const emit = options.emit ?? ((line, error = false) => error ? console.error(line) : console.log(line));
724
+ if (options.build) await runBuild(projectRoot, { capture: options.json });
725
+ let successfulGeneration = createDevelopmentGeneration(projectRoot, 0);
726
+ const retainedGenerations = /* @__PURE__ */ new Map([[successfulGeneration.guestCapability, successfulGeneration]]);
727
+ const initial = {
728
+ manifest: successfulGeneration.manifest,
729
+ entryPath: successfulGeneration.entryPath,
730
+ digest: successfulGeneration.digest
731
+ };
732
+ const state = {
733
+ phase: "ready",
734
+ generation: successfulGeneration.number,
735
+ watch: options.watch,
736
+ lastBuild: {
737
+ ok: true,
738
+ digest: successfulGeneration.digest,
739
+ assets: successfulGeneration.assets
740
+ },
741
+ lastBuildError: null
742
+ };
743
+ const eventClients = /* @__PURE__ */ new Map();
744
+ let eventId = 0;
745
+ let session;
746
+ const sendEvent = (type, data) => {
747
+ const event = {
748
+ protocolVersion: 1,
749
+ eventId: String(++eventId),
750
+ type,
751
+ data
752
+ };
753
+ const frame = `id: ${event.eventId}\nevent: ${type}\ndata: ${JSON.stringify(event)}\n\n`;
754
+ for (const response of eventClients.keys()) response.write(frame);
755
+ if (options.json) emit(formatDevelopmentEvent(type, data), type === "generation-failed");
756
+ };
757
+ const unauthorized = (response) => json(response, 401, {
758
+ ok: false,
759
+ error: {
760
+ code: "WIDGET_DEV_UNAUTHORIZED",
761
+ message: "Bearer token required"
762
+ }
763
+ });
764
+ const developmentSnapshot = () => {
765
+ const origin = `http://127.0.0.1:${server.address().port}`;
766
+ const development = panelDevelopmentDescriptor(successfulGeneration, `${origin}${V1_PROFILE_PATH}`, `${origin}${V1_STATUS_PATH}`);
767
+ return {
768
+ protocolVersion: 1,
769
+ state: state.phase,
770
+ sessionId: session.descriptor.sessionId,
771
+ revision: state.generation + 1,
772
+ generation: state.generation,
773
+ updateAvailable: false,
774
+ descriptor: development.descriptor,
775
+ presentationRequests: development.presentationRequests,
776
+ digest: successfulGeneration.digest,
777
+ assets: successfulGeneration.assets,
778
+ watch: state.watch,
779
+ lastBuild: state.lastBuild,
780
+ lastBuildError: state.lastBuildError
781
+ };
782
+ };
783
+ const generationReadyData = () => {
784
+ const snapshot = developmentSnapshot();
785
+ return {
786
+ generation: snapshot.generation,
787
+ revision: snapshot.revision,
788
+ digest: snapshot.digest,
789
+ assets: snapshot.assets,
790
+ descriptor: snapshot.descriptor,
791
+ presentationRequests: snapshot.presentationRequests
792
+ };
793
+ };
794
+ const server = createServer((request, response) => {
795
+ const url = new URL(request.url ?? "/", "http://127.0.0.1");
796
+ if (request.method === "OPTIONS") {
797
+ response.writeHead(405, secureHeaders("text/plain; charset=utf-8")).end();
798
+ return;
799
+ }
800
+ if (url.pathname === DESCRIPTOR_PATH && request.method === "GET") {
801
+ json(response, 200, session.descriptor);
802
+ return;
803
+ }
804
+ if (url.pathname === CLAIM_PATH && request.method === "POST") {
805
+ try {
806
+ json(response, 200, session.claim(bearer(request.headers.authorization)));
807
+ } catch (error) {
808
+ json(response, 401, {
809
+ ok: false,
810
+ error: {
811
+ code: "WIDGET_DEV_CLAIM_REJECTED",
812
+ message: String(error)
813
+ }
814
+ });
815
+ }
816
+ return;
817
+ }
818
+ if (url.pathname.startsWith(V1_ROOT) && !session.authorize(request.headers.authorization)) {
819
+ unauthorized(response);
820
+ return;
821
+ }
822
+ if (url.pathname === V1_STATUS_PATH && request.method === "GET") {
823
+ json(response, 200, developmentSnapshot());
824
+ return;
825
+ }
826
+ if (url.pathname === EVENTS_PATH && request.method === "GET") {
827
+ response.writeHead(200, {
828
+ ...secureHeaders("text/event-stream; charset=utf-8"),
829
+ connection: "keep-alive"
830
+ });
831
+ const initialEvent = {
832
+ protocolVersion: 1,
833
+ eventId: String(++eventId),
834
+ type: "generation-ready",
835
+ data: generationReadyData()
836
+ };
837
+ response.write(`id: ${initialEvent.eventId}\nevent: generation-ready\ndata: ${JSON.stringify(initialEvent)}\n\n`);
838
+ const heartbeat = setInterval(() => {
839
+ if (!response.writableEnded) response.write(": heartbeat\n\n");
840
+ }, options.heartbeatIntervalMs ?? SSE_HEARTBEAT_INTERVAL_MS);
841
+ eventClients.set(response, heartbeat);
842
+ const removeEventClient = () => {
843
+ clearInterval(heartbeat);
844
+ eventClients.delete(response);
845
+ };
846
+ request.once("close", removeEventClient);
847
+ response.once("close", removeEventClient);
848
+ return;
849
+ }
850
+ if (url.pathname === CONTROL_PATH && request.method === "POST") {
851
+ let body = "";
852
+ request.on("data", (chunk) => {
853
+ body += String(chunk);
854
+ if (body.length > 16384) request.destroy();
855
+ });
856
+ request.on("end", () => {
857
+ try {
858
+ const control = JSON.parse(body || "{}");
859
+ if (control.protocolVersion !== 1 || !["reload", "disconnect"].includes(control.action)) throw new Error("invalid control action");
860
+ if (control.action === "disconnect") {
861
+ sendEvent("disconnected", { reason: "host-request" });
862
+ session.disconnect();
863
+ } else sendEvent("reload-requested", { generation: state.generation });
864
+ json(response, 200, {
865
+ ok: true,
866
+ protocolVersion: 1,
867
+ action: control.action
868
+ });
869
+ } catch (error) {
870
+ json(response, 400, {
871
+ ok: false,
872
+ error: { message: String(error) }
873
+ });
874
+ }
875
+ });
876
+ return;
877
+ }
878
+ if (url.pathname === V1_PROFILE_PATH && request.method === "GET") {
879
+ const runId = url.searchParams.get("runId") || "widget-dev-run";
880
+ const contribution = url.searchParams.get("contribution");
881
+ const panelId = url.searchParams.get("id");
882
+ const manifestContribution = successfulGeneration.manifest.widgetConfig.contribution;
883
+ if (!(!contribution || contribution === manifestContribution.type && (contribution !== "panel" || !panelId || panelId === manifestContribution.id))) {
884
+ json(response, 404, {
885
+ ok: false,
886
+ error: {
887
+ code: "WIDGET_DEV_PROFILE_NOT_FOUND",
888
+ message: "No development binding matches the requested contribution",
889
+ attempted: {
890
+ contribution,
891
+ id: panelId
892
+ }
893
+ }
894
+ });
895
+ return;
896
+ }
897
+ const origin = `http://127.0.0.1:${server.address().port}`;
898
+ const launchUrl = `${origin}${guestGenerationRoot(successfulGeneration)}/launch.html`;
899
+ json(response, 200, createDevelopmentConfig({
900
+ manifest: successfulGeneration.manifest,
901
+ runId,
902
+ origin,
903
+ launchUrl,
904
+ digest: successfulGeneration.digest
905
+ }));
906
+ return;
907
+ }
908
+ if (url.pathname === PROFILE_PATH) {
909
+ const runId = url.searchParams.get("runId") || "widget-dev-run";
910
+ const origin = `http://127.0.0.1:${server.address().port}`;
911
+ json(response, 200, createDevelopmentConfig({
912
+ manifest: successfulGeneration.manifest,
913
+ runId,
914
+ origin,
915
+ launchUrl: `${origin}${LAUNCH_PATH}`,
916
+ digest: successfulGeneration.digest
917
+ }));
918
+ return;
919
+ }
920
+ if (url.pathname === STATUS_PATH) {
921
+ json(response, 200, {
922
+ ok: state.phase === "ready",
923
+ command: "dev",
924
+ profile: PROFILE_PATH,
925
+ launch: LAUNCH_PATH,
926
+ ...state
927
+ });
928
+ return;
929
+ }
930
+ if (url.pathname === LAUNCH_PATH) {
931
+ response.writeHead(200, secureHeaders("text/html; charset=utf-8"));
932
+ response.end(createLaunchHtml(successfulGeneration.entryPath));
933
+ return;
934
+ }
935
+ if (url.pathname.startsWith(`${GUEST_ROOT}/`)) {
936
+ const parts = url.pathname.slice(`${GUEST_ROOT}/`.length).split("/");
937
+ const capability = parts.shift();
938
+ const generation = retainedGenerations.get(capability);
939
+ if (!generation || parts.length === 0) {
940
+ response.writeHead(404, secureHeaders("text/plain; charset=utf-8")).end("not found");
941
+ return;
942
+ }
943
+ const guestPath = parts.join("/");
944
+ if (guestPath === "launch.html") {
945
+ response.writeHead(200, secureHeaders("text/html; charset=utf-8"));
946
+ response.end(createLaunchHtml(`${GUEST_ROOT.slice(1)}/${capability}/${generation.entryPath}`));
947
+ return;
948
+ }
949
+ if (guestPath.includes("\\") || guestPath.split("/").includes("..")) {
950
+ response.writeHead(403, secureHeaders("text/plain; charset=utf-8")).end("forbidden");
951
+ return;
952
+ }
953
+ serveGenerationAsset(generation, guestPath, response);
954
+ return;
955
+ }
956
+ let assetPath;
957
+ try {
958
+ assetPath = decodeURIComponent(url.pathname.replace(/^\/+/, ""));
959
+ } catch {
960
+ response.writeHead(400, secureHeaders("text/plain; charset=utf-8")).end("bad path");
961
+ return;
962
+ }
963
+ if (assetPath.includes("\\") || assetPath.split("/").includes("..")) {
964
+ response.writeHead(403, secureHeaders("text/plain; charset=utf-8")).end("forbidden");
965
+ return;
966
+ }
967
+ serveGenerationAsset(successfulGeneration, assetPath, response);
968
+ });
969
+ await new Promise((resolveListen, rejectListen) => {
970
+ server.once("error", rejectListen);
971
+ server.listen(options.port, "127.0.0.1", resolveListen);
972
+ });
973
+ const origin = `http://127.0.0.1:${server.address().port}`;
974
+ session = createWidgetDevelopmentSession({ origin });
975
+ const profileUrl = `${origin}${PROFILE_PATH}`;
976
+ const descriptorUrl = `${origin}${DESCRIPTOR_PATH}`;
977
+ const deepLink = `${options.scheme}://widget-dev?profile=${encodeURIComponent(profileUrl)}`;
978
+ const openResult = options.open ? await openHost(deepLink, profileUrl, options.bridge, {
979
+ jsonOutput: options.json,
980
+ developmentSession: session,
981
+ descriptorUrl,
982
+ expiresAt: session.descriptor.expiresAt
983
+ }) : {
984
+ ok: false,
985
+ mode: "manual",
986
+ reason: "not_requested",
987
+ url: deepLink
988
+ };
989
+ const startup = {
990
+ ok: true,
991
+ command: "dev",
992
+ protocolVersion: 1,
993
+ sessionId: session.descriptor.sessionId,
994
+ projectRoot,
995
+ origin,
996
+ descriptorUrl,
997
+ profileUrl,
998
+ statusUrl: `${origin}${STATUS_PATH}`,
999
+ hostUrl: deepLink,
1000
+ digest: successfulGeneration.digest,
1001
+ assets: successfulGeneration.assets,
1002
+ watch: options.watch,
1003
+ open: openResult
1004
+ };
1005
+ if (options.json) emit(formatDevelopmentEvent("session-ready", startup));
1006
+ else {
1007
+ emit(`\nWidget Development Host 已启动:${relative(repoRoot, projectRoot) || projectRoot}`);
1008
+ emit(`Descriptor: ${descriptorUrl}`);
1009
+ emit(`Profile: ${profileUrl}`);
1010
+ emit(`Status: ${startup.statusUrl}`);
1011
+ emit(`Host: ${deepLink}`);
1012
+ emit("\n将生产宿主用于 Widget 开发:复制 Host URL,或加 --open 自动唤起。\n");
1013
+ }
1014
+ let timer;
1015
+ let building = false;
1016
+ let pending = false;
1017
+ const rebuild = async () => {
1018
+ if (building) {
1019
+ pending = true;
1020
+ return;
1021
+ }
1022
+ building = true;
1023
+ try {
1024
+ const build = await runBuild(projectRoot, { capture: options.json });
1025
+ const candidate = createDevelopmentGeneration(projectRoot, successfulGeneration.number + 1);
1026
+ successfulGeneration = candidate;
1027
+ retainedGenerations.set(candidate.guestCapability, candidate);
1028
+ while (retainedGenerations.size > RETAINED_GENERATIONS) retainedGenerations.delete(retainedGenerations.keys().next().value);
1029
+ state.generation = candidate.number;
1030
+ state.lastBuild = {
1031
+ ok: true,
1032
+ digest: candidate.digest,
1033
+ assets: candidate.assets,
1034
+ stdout: build.stdout,
1035
+ stderr: build.stderr
1036
+ };
1037
+ state.lastBuildError = null;
1038
+ sendEvent("generation-ready", generationReadyData());
1039
+ if (!options.json) emit("Widget 已重新构建;宿主可通过事件或 reload 获取新 generation。");
1040
+ } catch (error) {
1041
+ const redactProjectRoot = (value) => String(value ?? "").replaceAll(projectRoot, "<project>");
1042
+ state.lastBuildError = createWidgetDevError(error, {
1043
+ phase: "watch-build",
1044
+ stdout: redactProjectRoot(error?.stdout),
1045
+ stderr: redactProjectRoot(error?.stderr)
1046
+ });
1047
+ sendEvent("generation-failed", {
1048
+ generation: successfulGeneration.number,
1049
+ retainedDigest: successfulGeneration.digest,
1050
+ error: state.lastBuildError
1051
+ });
1052
+ if (!options.json) emit(String(error), true);
1053
+ } finally {
1054
+ building = false;
1055
+ if (pending) {
1056
+ pending = false;
1057
+ rebuild();
1058
+ }
1059
+ }
1060
+ };
1061
+ const watcher = options.watch ? createProjectWatcher(projectRoot, () => {
1062
+ clearTimeout(timer);
1063
+ timer = setTimeout(() => void rebuild(), 150);
1064
+ }) : null;
1065
+ let closed = false;
1066
+ const close = (reason = "cli-stop") => {
1067
+ if (closed) return;
1068
+ closed = true;
1069
+ state.phase = "stopping";
1070
+ sendEvent("session-stopping", { reason });
1071
+ session.disconnect();
1072
+ clearTimeout(timer);
1073
+ watcher?.close();
1074
+ for (const [response, heartbeat] of eventClients) {
1075
+ clearInterval(heartbeat);
1076
+ response.end();
1077
+ }
1078
+ eventClients.clear();
1079
+ server.close();
1080
+ };
1081
+ if (options.registerSignals !== false) {
1082
+ process.once("SIGINT", () => {
1083
+ close("signal");
1084
+ process.exit(0);
1085
+ });
1086
+ process.once("SIGTERM", () => {
1087
+ close("signal");
1088
+ process.exit(0);
1089
+ });
1090
+ }
1091
+ return {
1092
+ server,
1093
+ close,
1094
+ rebuild,
1095
+ initial,
1096
+ startup,
1097
+ state,
1098
+ session
1099
+ };
1100
+ }
1101
+ function emitCliEnvelope(type, data, error = false) {
1102
+ const line = formatDevelopmentEvent(type, data);
1103
+ if (error) console.error(line);
1104
+ else console.log(line);
1105
+ }
1106
+ async function runWidgetDevelopmentCli(argv = process.argv.slice(2)) {
1107
+ try {
1108
+ const options = parseArgs(argv);
1109
+ if (options.help) {
1110
+ console.log(usage$1());
1111
+ return 0;
1112
+ }
1113
+ const [projectRoot] = resolveWidgetProjects(options.project);
1114
+ const check = await checkWidgetProject(projectRoot, { requireBuiltEntry: false });
1115
+ if (!check.ok) {
1116
+ if (options.json) emitCliEnvelope("error", {
1117
+ ok: false,
1118
+ command: "dev",
1119
+ error: createWidgetDevError(/* @__PURE__ */ new Error("Widget Dev 启动前 L0 check 失败"), {
1120
+ phase: "validate",
1121
+ projectRoot,
1122
+ errors: check.errors
1123
+ })
1124
+ });
1125
+ else {
1126
+ console.error("Widget Dev 启动前 L0 check 失败:");
1127
+ check.errors.forEach((error) => {
1128
+ console.error(` - ${error}`);
1129
+ });
1130
+ }
1131
+ return 1;
1132
+ }
1133
+ await startWidgetDevelopment(projectRoot, options);
1134
+ return 0;
1135
+ } catch (error) {
1136
+ if (argv.includes("--json")) emitCliEnvelope("error", {
1137
+ ok: false,
1138
+ command: "dev",
1139
+ error: createWidgetDevError(error, { input: argv[0] ?? null })
1140
+ });
1141
+ else console.error(`${String(error)}\n\n${usage$1()}`);
1142
+ return 1;
1143
+ }
1144
+ }
1145
+ //#endregion
1146
+ //#region src/manifest-lint.mjs
1147
+ /**
1148
+ * 作者清单(VS Code 风格 `contributes` 清单)的本地静态检查。
1149
+ *
1150
+ * `@dp-bohrium/widget-contracts` 的 host schema 只在 `compileAuthorManifest` 之后生效,
1151
+ * 而编译只保留宿主运行时需要的字段:作者的 `version`、`displayName` 和未知顶层键会被
1152
+ * 静默丢弃,于是"清单写错但 check 全绿"。这里只补下游永远看不到的作者侧约束,报错文案
1153
+ * 直接指向作者填的字段名。
1154
+ *
1155
+ * 边界:`contributes` / `capabilities` / `runtime` / `samples` / `description` 的结构
1156
+ * 在编译后仍由 host schema 校验,这里不重复实现,避免与
1157
+ * `widget-plugin-manifest.schema.json` 产生第二份真相。
1158
+ */
1159
+ const VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u;
1160
+ const AUTHOR_MANIFEST_KEYS = Object.freeze([
1161
+ "$schema",
1162
+ "name",
1163
+ "version",
1164
+ "displayName",
1165
+ "description",
1166
+ "entry",
1167
+ "activationEvents",
1168
+ "contributes",
1169
+ "capabilities",
1170
+ "runtime",
1171
+ "samples"
1172
+ ]);
1173
+ const AUTHOR_MANIFEST_SCHEMA_ID = "@dp-bohrium/widget-contracts/schemas/widget-plugin-manifest.schema.json";
1174
+ function isPlainObject(value) {
1175
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
1176
+ }
1177
+ /** 有 `contributes` 就按作者清单处理;这也是 `compileAuthorManifest` 的输入前提。 */
1178
+ function looksLikeAuthorManifest(value) {
1179
+ return isPlainObject(value) && "contributes" in value;
1180
+ }
1181
+ function suggestKey(key) {
1182
+ const lower = key.toLowerCase();
1183
+ const candidate = AUTHOR_MANIFEST_KEYS.find((allowed) => {
1184
+ const normalized = allowed.toLowerCase();
1185
+ return normalized === lower || normalized.startsWith(lower) || lower.startsWith(normalized);
1186
+ });
1187
+ return candidate && candidate !== key ? candidate : void 0;
1188
+ }
1189
+ /**
1190
+ * @returns {{ ok: boolean, errors: string[], warnings: string[] }}
1191
+ * `errors` 是作者必须修的清单错误;`warnings` 是可继续执行但值得提示的问题。
1192
+ */
1193
+ function lintAuthorManifest(manifest, options = {}) {
1194
+ const fileExists = options.fileExists;
1195
+ const errors = [];
1196
+ const warnings = [];
1197
+ if (!isPlainObject(manifest)) return {
1198
+ ok: false,
1199
+ errors: ["manifest.json 必须是 JSON 对象"],
1200
+ warnings
1201
+ };
1202
+ for (const key of Object.keys(manifest)) {
1203
+ if (AUTHOR_MANIFEST_KEYS.includes(key)) continue;
1204
+ const suggestion = suggestKey(key);
1205
+ errors.push(`manifest.json 不允许多余字段 "${key}"${suggestion ? `,是否想写 "${suggestion}"?` : ""}(additionalProperties=false)`);
1206
+ }
1207
+ if (typeof manifest.name !== "string" || manifest.name.length === 0) errors.push("manifest.json 缺少必填字段 \"name\"(非空字符串)");
1208
+ else if (manifest.name.length > 128) errors.push("manifest.json 的 \"name\" 超过 128 个字符");
1209
+ if (manifest.version === void 0) errors.push("manifest.json 缺少必填字段 \"version\"(例如 \"0.1.0\")");
1210
+ else if (typeof manifest.version !== "string" || !VERSION_PATTERN.test(manifest.version)) errors.push(`manifest.json 的 "version" 必须是 semver,当前是 ${JSON.stringify(manifest.version)}`);
1211
+ if (manifest.displayName !== void 0 && typeof manifest.displayName !== "string") errors.push(`manifest.json 的 "displayName" 必须是字符串,当前是 ${JSON.stringify(manifest.displayName)}`);
1212
+ else if (typeof manifest.displayName === "string" && manifest.displayName.length > 128) errors.push("manifest.json 的 \"displayName\" 超过 128 个字符");
1213
+ if (manifest.entry !== void 0) {
1214
+ if (typeof manifest.entry !== "string" || manifest.entry.length === 0) errors.push("manifest.json 的 \"entry\" 必须是非空字符串");
1215
+ else if (fileExists && !fileExists(manifest.entry)) warnings.push(`manifest.json 的 "entry" 指向的源文件不存在:${manifest.entry}(CLI 打包使用构建后的入口,此处仅提示)`);
1216
+ }
1217
+ if (isPlainObject(manifest.widgetConfig)) warnings.push("manifest.json 同时包含作者字段(contributes)和宿主字段(widgetConfig);打包时以编译结果为准,建议只保留作者清单");
1218
+ const contribution = manifest.contributes?.widget;
1219
+ if (contribution?.type === "panel" && contribution.id === "workspace-files") {
1220
+ const files = manifest.capabilities?.files;
1221
+ const missing = [
1222
+ "list",
1223
+ "metadata",
1224
+ "read"
1225
+ ].filter((capability) => files?.[capability] !== true);
1226
+ if (missing.length > 0) errors.push(`contributes.widget.id 为 "workspace-files" 时必须申请 capabilities.files.${missing.join("、capabilities.files.")};否则 Host 文件面板无法加载`);
1227
+ }
1228
+ if (errors.length > 0) errors.push(`作者清单结构以 ${AUTHOR_MANIFEST_SCHEMA_ID} 为准;执行 bohr-widget schema 可查看本地 Schema 路径。`);
1229
+ return {
1230
+ ok: errors.length === 0,
1231
+ errors,
1232
+ warnings
1233
+ };
1234
+ }
1235
+ //#endregion
1236
+ //#region src/templates.mjs
1237
+ function reactSource({ reportSize = false } = {}) {
1238
+ return `import { createRoot } from "react-dom/client";
1239
+ import type { WidgetContext } from "@dp-bohrium/widget-sdk";
1240
+ import type { RenderRequest } from "@dp-bohrium/widget-contracts";
1241
+
1242
+ function WidgetView({ request }: { request?: RenderRequest }) {
1243
+ const data = request?.content ?? { state: "等待 Host 提供数据" };
1244
+ return (
1245
+ <main style={{ fontFamily: "system-ui", padding: 20 }}>
1246
+ <h1>Widget 面板</h1>
1247
+ <pre style={{ whiteSpace: "pre-wrap" }}>
1248
+ {JSON.stringify(data, null, 2)}
1249
+ </pre>
1250
+ </main>
1251
+ );
1252
+ }
1253
+
1254
+ function activate(context: WidgetContext) {
1255
+ const host = document.createElement("main");
1256
+ document.body.replaceChildren(host);
1257
+ const root = createRoot(host);
1258
+ let renderRequest: RenderRequest | undefined;${reportSize ? "\n let resized = false;" : ""}
1259
+ const render = () => root.render(<WidgetView request={renderRequest} />);
1260
+ render();
1261
+ context.render((request) => {
1262
+ renderRequest = request;
1263
+ render();${reportSize ? `
1264
+ if (!resized) {
1265
+ context.resize(640, 360);
1266
+ resized = true;
1267
+ }` : ""}
1268
+ });
1269
+ context.subscriptions.add(() => root.unmount());
1270
+ }
1271
+
1272
+ export { activate };
1273
+ `;
1274
+ }
1275
+ function hostSource({ sourceFile }) {
1276
+ return `import { runWidgetHost } from "@dp-bohrium/widget-sdk";
1277
+ import * as widget from "./${sourceFile.split("/").at(-1)}";
1278
+
1279
+ runWidgetHost(widget);
1280
+ `;
1281
+ }
1282
+ const TEMPLATES = Object.freeze({
1283
+ panel: {
1284
+ manifest: ({ name, panelId = name, ui = "vanilla" }) => ({
1285
+ $schema: "@dp-bohrium/widget-contracts/schemas/widget-plugin-manifest.schema.json",
1286
+ name,
1287
+ version: "0.1.0",
1288
+ displayName: name,
1289
+ entry: ui === "react" ? "src/widget.tsx" : "src/widget.ts",
1290
+ activationEvents: ["session.start"],
1291
+ contributes: { widget: {
1292
+ type: "panel",
1293
+ id: panelId,
1294
+ surfaces: ["workspace"]
1295
+ } },
1296
+ ...panelId === "workspace-files" ? { capabilities: { files: {
1297
+ list: true,
1298
+ metadata: true,
1299
+ read: true
1300
+ } } } : {},
1301
+ samples: [{
1302
+ id: "default",
1303
+ title: "默认样例",
1304
+ request: "samples/default.json"
1305
+ }]
1306
+ }),
1307
+ source: `import type { WidgetContext } from "@dp-bohrium/widget-sdk";
1308
+
1309
+ function activate(context: WidgetContext) {
1310
+ const root = document.createElement("main");
1311
+ root.textContent = "等待 Host 提供面板数据";
1312
+ document.body.replaceChildren(root);
1313
+ context.render((request) => {
1314
+ const content = request.content;
1315
+ root.textContent = JSON.stringify(
1316
+ "data" in content ? content.data : content,
1317
+ null,
1318
+ 2,
1319
+ );
1320
+ });
1321
+ }
1322
+
1323
+ export { activate };
1324
+ `,
1325
+ reactSource,
1326
+ hostSource,
1327
+ sample: ({ name, panelId = name }) => ({
1328
+ schemaVersion: 1,
1329
+ requestId: `${name}-default`,
1330
+ instanceId: name,
1331
+ revision: 1,
1332
+ contribution: "panel",
1333
+ state: "complete",
1334
+ content: {
1335
+ contribution: "panel",
1336
+ id: panelId,
1337
+ title: name,
1338
+ data: panelId === "workspace-files" ? { root: {
1339
+ id: "workspace-root",
1340
+ kind: "directory",
1341
+ name: "workspace"
1342
+ } } : { status: "ready" }
1343
+ },
1344
+ context: {
1345
+ locale: "zh-CN",
1346
+ theme: {
1347
+ mode: "light",
1348
+ tokens: {
1349
+ background: "#fff",
1350
+ foreground: "#111",
1351
+ muted: "#eee",
1352
+ mutedForeground: "#666",
1353
+ border: "#ddd",
1354
+ primary: "#06c",
1355
+ danger: "#c00"
1356
+ }
1357
+ },
1358
+ viewport: {
1359
+ width: 640,
1360
+ height: 360
1361
+ },
1362
+ capabilities: panelId === "workspace-files" ? [
1363
+ {
1364
+ name: "files.list",
1365
+ limits: {
1366
+ pageSize: 100,
1367
+ callsPerMinute: 60
1368
+ }
1369
+ },
1370
+ {
1371
+ name: "files.metadata",
1372
+ limits: { callsPerMinute: 60 }
1373
+ },
1374
+ {
1375
+ name: "files.read",
1376
+ limits: {
1377
+ bytesPerRequest: 65536,
1378
+ bytesPerRun: 262144,
1379
+ callsPerMinute: 60
1380
+ }
1381
+ }
1382
+ ] : [],
1383
+ correlationId: `${name}-default`
1384
+ }
1385
+ })
1386
+ },
1387
+ "file-preview": {
1388
+ manifest: ({ name, ui = "vanilla" }) => ({
1389
+ $schema: "@dp-bohrium/widget-contracts/schemas/widget-plugin-manifest.schema.json",
1390
+ name,
1391
+ version: "0.1.0",
1392
+ displayName: name,
1393
+ entry: ui === "react" ? "src/widget.tsx" : "src/widget.ts",
1394
+ activationEvents: ["workspace-file-opened"],
1395
+ contributes: { widget: {
1396
+ type: "file-preview",
1397
+ extensions: ["txt"]
1398
+ } },
1399
+ samples: [{
1400
+ id: "default",
1401
+ title: "默认样例",
1402
+ request: "samples/default.json"
1403
+ }]
1404
+ }),
1405
+ source: `import type { WidgetContext } from "@dp-bohrium/widget-sdk";
1406
+
1407
+ function activate(context: WidgetContext) {
1408
+ const root = document.createElement("main");
1409
+ document.body.replaceChildren(root);
1410
+ let resized = false;
1411
+ context.render((request) => {
1412
+ root.textContent = JSON.stringify(request.content ?? {}, null, 2);
1413
+ if (!resized) {
1414
+ context.resize(640, 360);
1415
+ resized = true;
1416
+ }
1417
+ });
1418
+ }
1419
+
1420
+ export { activate };
1421
+ `,
1422
+ reactSource: (options) => reactSource({
1423
+ ...options,
1424
+ reportSize: true
1425
+ }),
1426
+ hostSource,
1427
+ sample: () => ({
1428
+ schemaVersion: 1,
1429
+ requestId: "sample-file-preview",
1430
+ instanceId: "sample-file-preview",
1431
+ revision: 1,
1432
+ contribution: "file-preview",
1433
+ state: "complete",
1434
+ content: {
1435
+ contribution: "file-preview",
1436
+ file: {
1437
+ id: "sample",
1438
+ name: "README.txt",
1439
+ extension: "txt",
1440
+ kind: "file"
1441
+ },
1442
+ preview: { format: "text" }
1443
+ },
1444
+ context: {
1445
+ locale: "en-US",
1446
+ theme: {
1447
+ mode: "light",
1448
+ tokens: {
1449
+ background: "#fff",
1450
+ foreground: "#111",
1451
+ muted: "#eee",
1452
+ mutedForeground: "#666",
1453
+ border: "#ddd",
1454
+ primary: "#06c",
1455
+ danger: "#c00"
1456
+ }
1457
+ },
1458
+ viewport: {
1459
+ width: 640,
1460
+ height: 360
1461
+ },
1462
+ capabilities: [],
1463
+ correlationId: "sample-file-preview"
1464
+ }
1465
+ })
1466
+ }
1467
+ });
1468
+ //#endregion
1469
+ //#region src/cli.mjs
1470
+ const require = createRequire(import.meta.url);
1471
+ const packageJson = require("../package.json");
1472
+ const sdkPackage = require("@dp-bohrium/widget-sdk/package.json");
1473
+ const contractsPackage = require("@dp-bohrium/widget-contracts/package.json");
1474
+ const COMMANDS = [
1475
+ "create",
1476
+ "check",
1477
+ "validate",
1478
+ "build",
1479
+ "dev",
1480
+ "pack",
1481
+ "doctor",
1482
+ "schema",
1483
+ "migrate",
1484
+ "version",
1485
+ "help"
1486
+ ];
1487
+ function usage() {
1488
+ return `bohr-widget — Bohrium Widget 插件工具
1489
+
1490
+ 用法:
1491
+ bohr-widget create <目录> [--template panel|file-preview] [--ui vanilla|react] [--panel-id <id>]
1492
+ bohr-widget check <目录|manifest.json|all> [--json]
1493
+ bohr-widget validate <目录|manifest.json|all> [--json]
1494
+ bohr-widget build <目录> [--json]
1495
+ bohr-widget dev <目录> [--open] [--watch] [--no-build] [--json] [--port <端口>] [--scheme <scheme>] [--bridge <url>]
1496
+ bohr-widget pack <目录> [--out <zip>] [--include-preview-fixtures] [--no-build] [--json]
1497
+ bohr-widget doctor [目录] [--json]
1498
+ bohr-widget schema
1499
+ bohr-widget migrate dsh audit <plugin-dir> [--json]
1500
+ bohr-widget migrate dsh scaffold <plugin-dir> --out <widget-dir> [--json]
1501
+ bohr-widget version [--json]
1502
+ bohr-widget help | -h | --help
1503
+
1504
+ --json 只输出一个 JSON 文档;check all --json 输出 checked/passed/failed/results[] 汇总。
1505
+ `;
1506
+ }
1507
+ function parseOptions(argv) {
1508
+ const positional = [];
1509
+ const options = {
1510
+ json: false,
1511
+ template: "panel",
1512
+ ui: "vanilla",
1513
+ includePreviewFixtures: false,
1514
+ output: void 0,
1515
+ open: false,
1516
+ watch: false,
1517
+ build: true,
1518
+ port: 4300,
1519
+ scheme: "widget-host",
1520
+ bridge: null,
1521
+ panelId: void 0
1522
+ };
1523
+ for (let index = 0; index < argv.length; index += 1) {
1524
+ const argument = argv[index];
1525
+ if (argument === "--json") options.json = true;
1526
+ else if (argument === "--include-preview-fixtures") options.includePreviewFixtures = true;
1527
+ else if (argument === "--template") {
1528
+ options.template = argv[++index];
1529
+ if (!options.template) throw new Error("--template 需要模板名");
1530
+ } else if (argument === "--ui") {
1531
+ options.ui = argv[++index];
1532
+ if (!options.ui) throw new Error("--ui 需要 vanilla 或 react");
1533
+ } else if (argument === "--panel-id") {
1534
+ options.panelId = argv[++index];
1535
+ if (!options.panelId) throw new Error("--panel-id 需要稳定的 Panel id");
1536
+ } else if (argument === "--out") {
1537
+ options.output = argv[++index];
1538
+ if (!options.output) throw new Error("--out 需要输出路径");
1539
+ } else if (argument === "--open") options.open = true;
1540
+ else if (argument === "--watch") options.watch = true;
1541
+ else if (argument === "--no-build") options.build = false;
1542
+ else if (argument === "--port") options.port = Number(argv[++index]);
1543
+ else if (argument === "--scheme") options.scheme = argv[++index];
1544
+ else if (argument === "--bridge") options.bridge = argv[++index];
1545
+ else if (argument.startsWith("-")) throw new Error(`未知选项:${argument}`);
1546
+ else positional.push(argument);
1547
+ }
1548
+ return {
1549
+ positional,
1550
+ options
1551
+ };
1552
+ }
1553
+ function printResult(result, json) {
1554
+ if (json) console.log(JSON.stringify(result, null, 2));
1555
+ else {
1556
+ for (const check of result.checks ?? []) console.log(`${check.ok ? "✓" : "✗"} ${check.name}`);
1557
+ for (const error of result.errors ?? []) console.error(`错误:${error}`);
1558
+ console.log(result.ok ? "校验通过" : "校验失败");
1559
+ }
1560
+ }
1561
+ function structuredError({ code, message, context = {}, recovery }) {
1562
+ return {
1563
+ code,
1564
+ message,
1565
+ context,
1566
+ recovery
1567
+ };
1568
+ }
1569
+ function commandRecovery(command, error) {
1570
+ if (!COMMANDS.includes(command)) return "命令不存在或拼写有误:执行 bohr-widget help 查看支持的命令与参数。";
1571
+ if (command === "create") return "确认目标目录不存在,且 --template 与 --ui 取值合法后重新执行 create。";
1572
+ if (command === "build") {
1573
+ if (error?.cliCode === "WIDGET_CLI_BUILD_UNRESOLVED_IMPORT") return "安装或修正 error.message 中的依赖,并确认 bundler 已把浏览器运行时依赖打进 dist/widget.js 后重新执行 build。";
1574
+ return error?.code === 127 ? "工程缺少构建依赖:先在工程目录执行包管理器 install,再重新执行 build。" : "读取 error.stdout / error.stderr 定位构建错误,修复工程后重新执行 build。";
1575
+ }
1576
+ if (command === "pack") return "先执行 bohr-widget check <工程目录>,修复 manifest、entry、sample 或 fixtures 后重新打包。";
1577
+ if (command === "check" || command === "validate") return "根据 errors 修复 Manifest、entry、sample 或 fixtures 后重新校验。";
1578
+ return "根据错误上下文修复后重试;不确定时保留 command、error 和上下文交给上层 CLI。";
1579
+ }
1580
+ /** 命令名要进 error.code,必须净化成标识符片段(unknown-cmd → UNKNOWN_CMD)。 */
1581
+ function commandCode(command) {
1582
+ return String(command ?? "").replace(/[^A-Za-z0-9]+/gu, "_").replace(/^_+|_+$/gu, "").toUpperCase() || "UNKNOWN";
1583
+ }
1584
+ function commandError(command, error, context = {}) {
1585
+ return structuredError({
1586
+ code: typeof error?.cliCode === "string" ? error.cliCode : typeof error?.code === "string" && error.code.startsWith("WIDGET_") ? error.code : `WIDGET_CLI_${commandCode(command)}_FAILED`,
1587
+ message: error instanceof Error ? error.message : String(error),
1588
+ context,
1589
+ recovery: commandRecovery(command, error)
1590
+ });
1591
+ }
1592
+ /**
1593
+ * 目录名 → Widget name(同时用作 panel id)。中文或纯符号目录名必须回退成合法标识符,
1594
+ * 不能净化成 "----" 这类空壳,否则 manifest 的 name/id 会直接违反作者清单契约。
1595
+ */
1596
+ function deriveWidgetName(directoryName) {
1597
+ const slug = String(directoryName ?? "").toLowerCase().replace(/[^a-z0-9]+/gu, "-").replace(/^-+|-+$/gu, "");
1598
+ if (slug.length === 0) return `widget-${createHash("sha256").update(String(directoryName ?? "")).digest("hex").slice(0, 8)}`;
1599
+ return slug.slice(0, 64).replace(/-+$/gu, "");
1600
+ }
1601
+ async function commandCreate(directory, options) {
1602
+ if (!directory) throw new Error("create 需要目标目录");
1603
+ const template = TEMPLATES[options.template];
1604
+ if (!template) throw new Error(`未知模板:${options.template}`);
1605
+ if (!["vanilla", "react"].includes(options.ui)) throw new Error(`未知 UI 模式:${options.ui},可选 vanilla 或 react`);
1606
+ if (options.panelId && options.template !== "panel") throw new Error("--panel-id 只适用于 --template panel");
1607
+ if (options.panelId && !/^[A-Za-z0-9_.:-]{1,128}$/u.test(options.panelId)) throw new Error("--panel-id 必须为 1-128 位,只能包含字母、数字、下划线、点、冒号和连字符");
1608
+ const root = resolve(directory);
1609
+ if (existsSync(root)) throw new Error(`目标目录已存在:${root}`);
1610
+ const directoryName = basename(root);
1611
+ const name = deriveWidgetName(directoryName);
1612
+ const panelId = options.template === "panel" ? options.panelId ?? name : void 0;
1613
+ const templateContext = {
1614
+ name,
1615
+ panelId,
1616
+ sdkVersion: sdkPackage.version,
1617
+ ui: options.ui
1618
+ };
1619
+ const manifest = template.manifest(templateContext);
1620
+ const sample = template.sample(templateContext);
1621
+ await mkdir(join(root, "src"), { recursive: true });
1622
+ await mkdir(join(root, "samples"), { recursive: true });
1623
+ await mkdir(join(root, "scripts"), { recursive: true });
1624
+ await writeFile(join(root, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
1625
+ await writeFile(join(root, "samples/default.json"), `${JSON.stringify(sample, null, 2)}\n`);
1626
+ const sourceFile = options.ui === "react" ? "src/widget.tsx" : "src/widget.ts";
1627
+ const hostFile = "src/widget-host.ts";
1628
+ const source = options.ui === "react" && template.reactSource ? template.reactSource({ name }) : typeof template.source === "function" ? template.source({
1629
+ ui: options.ui,
1630
+ name
1631
+ }) : template.source;
1632
+ await writeFile(join(root, sourceFile), source);
1633
+ await writeFile(join(root, hostFile), template.hostSource({ sourceFile }));
1634
+ await writeFile(join(root, "scripts/rename-iife.mjs"), `import { rename } from "node:fs/promises";\n\nawait rename("dist/widget.iife.js", "dist/widget.js");\n`);
1635
+ await writeFile(join(root, ".gitignore"), "node_modules/\ndist/\n.artifacts/\n*.zip\n");
1636
+ await writeFile(join(root, "README.md"), `# ${name}
1637
+
1638
+ Bohrium Widget ${options.template} 插件工程(${options.ui})。
1639
+
1640
+ \`\`\`bash
1641
+ pnpm install
1642
+ pnpm build
1643
+ bohr-widget check .
1644
+ bohr-widget pack .
1645
+ \`\`\`
1646
+
1647
+ - 源码入口:${sourceFile},由 ${hostFile} 调用 SDK 的 runWidgetHost 启动
1648
+ - manifest.json 是作者清单,打包时由 CLI 编译为宿主清单
1649
+ - 构建产物 dist/widget.js 不提交到版本库
1650
+ `);
1651
+ await writeFile(join(root, "tsconfig.json"), `${JSON.stringify({
1652
+ compilerOptions: {
1653
+ target: "ES2022",
1654
+ module: "ESNext",
1655
+ moduleResolution: "bundler",
1656
+ lib: ["ES2022", "DOM"],
1657
+ strict: true,
1658
+ noEmit: true,
1659
+ allowImportingTsExtensions: true,
1660
+ skipLibCheck: true,
1661
+ ...options.ui === "react" ? { jsx: "react-jsx" } : {}
1662
+ },
1663
+ include: ["src"]
1664
+ }, null, 2)}\n`);
1665
+ await writeFile(join(root, "tsdown.config.ts"), `import { defineConfig } from "tsdown";\n\nexport default defineConfig({\n entry: { widget: "${hostFile}" },\n format: ["iife"],\n platform: "browser",\n target: "es2022",\n ${options.ui === "react" ? "jsx: \"automatic\"," : ""}\n dts: false,\n outDir: "dist",\n outExtensions: () => ({ js: ".js" }),\n clean: true,\n sourcemap: false,\n minify: true,\n deps: {\n alwaysBundle: ["@dp-bohrium/widget-sdk"${options.ui === "react" ? ", \"react\", \"react-dom\", \"react-dom/client\", \"react/jsx-runtime\"" : ""}],\n onlyBundle: false,\n },\n});\n`);
1666
+ await writeFile(join(root, "package.json"), `${JSON.stringify({
1667
+ name: `@dp-bohrium/widget-example-${name}`,
1668
+ private: true,
1669
+ type: "module",
1670
+ scripts: { build: "tsc --noEmit && tsdown && node scripts/rename-iife.mjs" },
1671
+ dependencies: {
1672
+ "@dp-bohrium/widget-sdk": sdkPackage.version,
1673
+ ...options.ui === "react" ? {
1674
+ react: "^19.0.0",
1675
+ "react-dom": "^19.0.0"
1676
+ } : {}
1677
+ },
1678
+ devDependencies: {
1679
+ "@dp-bohrium/widget-contracts": contractsPackage.version,
1680
+ tsdown: "0.22.14",
1681
+ typescript: "5.9.3",
1682
+ ...options.ui === "react" ? {
1683
+ "@types/react": "^19.0.0",
1684
+ "@types/react-dom": "^19.0.0"
1685
+ } : {}
1686
+ }
1687
+ }, null, 2)}\n`);
1688
+ const warnings = [];
1689
+ if (name !== directoryName) warnings.push(`目录名 "${directoryName}" 不是合法标识符,manifest 的 name 已取 "${name}";需要其他取值请直接编辑 manifest.json。`);
1690
+ if (options.template === "panel" && panelId !== "workspace-files") warnings.push(`Panel id "${panelId}" 需要 Host 预先注册并绑定;工作区文件面板请使用 --panel-id workspace-files。`);
1691
+ const result = {
1692
+ ok: true,
1693
+ command: "create",
1694
+ projectRoot: root,
1695
+ name,
1696
+ template: options.template,
1697
+ ui: options.ui,
1698
+ entry: sourceFile,
1699
+ ...panelId ? { panelId } : {},
1700
+ manifestPath: join(root, "manifest.json"),
1701
+ packagePath: join(root, "package.json"),
1702
+ ...warnings.length === 0 ? {} : { warnings }
1703
+ };
1704
+ if (options.json) console.log(JSON.stringify(result, null, 2));
1705
+ else {
1706
+ console.log(`已创建 Widget 工程:${root}(manifest name: ${name})`);
1707
+ for (const warning of warnings) console.warn(`提示:${warning}`);
1708
+ console.log("下一步:pnpm install && pnpm build && bohr-widget check .");
1709
+ }
1710
+ return result;
1711
+ }
1712
+ function projectLabel(projectRoot) {
1713
+ const relativePath = relative(process.cwd(), projectRoot);
1714
+ return !relativePath || relativePath.startsWith("..") ? projectRoot : relativePath;
1715
+ }
1716
+ /**
1717
+ * `check all` 以当前工作目录为根。发布形态下 CLI 位于消费者的 node_modules 内,
1718
+ * 用包自身位置反推仓库根只会得到空集合(旧行为:静默 exit 0,零覆盖)。
1719
+ */
1720
+ function resolveProjectSet(input) {
1721
+ if (input !== "all") return resolveWidgetProjects(input);
1722
+ const base = resolve(process.cwd());
1723
+ const catalogued = resolveWidgetProjects("all", base);
1724
+ if (catalogued.length > 0) return catalogued;
1725
+ return existsSync(join(base, "manifest.json")) ? [base] : [];
1726
+ }
1727
+ /** 作者清单里下游校验看不到的字段,先在这里给出精确错误。 */
1728
+ function lintProjectManifest(projectRoot) {
1729
+ const manifestPath = join(projectRoot, "manifest.json");
1730
+ if (!existsSync(manifestPath)) return null;
1731
+ let manifest;
1732
+ try {
1733
+ manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
1734
+ } catch {
1735
+ return null;
1736
+ }
1737
+ if (!looksLikeAuthorManifest(manifest)) return null;
1738
+ return lintAuthorManifest(manifest, { fileExists: (declared) => existsSync(join(projectRoot, declared)) });
1739
+ }
1740
+ function fixtureContentBytes(value) {
1741
+ if (!value || typeof value.content !== "string") return null;
1742
+ try {
1743
+ return value.encoding === "base64" ? Buffer.from(value.content, "base64").byteLength : Buffer.byteLength(value.content, "utf8");
1744
+ } catch {
1745
+ return null;
1746
+ }
1747
+ }
1748
+ /** CLI 在共享 L0 契约之外补充当前 Host 与预览 fixture 的可运行性检查。 */
1749
+ function applyCliProjectPolicies(result) {
1750
+ const widgetConfig = result.manifest?.widgetConfig;
1751
+ if (!widgetConfig?.contribution) return result;
1752
+ const contribution = widgetConfig.contribution;
1753
+ const files = widgetConfig.requestedCapabilities?.files;
1754
+ if (contribution.type === "panel" && contribution.id === "workspace-files") {
1755
+ const missing = [
1756
+ "list",
1757
+ "metadata",
1758
+ "read"
1759
+ ].filter((capability) => files?.[capability] !== true);
1760
+ const ok = missing.length === 0;
1761
+ result.checks.push({
1762
+ name: "workspace-files 申请完整只读文件能力",
1763
+ ok
1764
+ });
1765
+ if (!ok) result.errors.push(`workspace-files 缺少 capabilities.files.${missing.join("、capabilities.files.")};Host 无法加载文件面板`);
1766
+ }
1767
+ if (result.fixtures && files?.read) {
1768
+ const listedFiles = /* @__PURE__ */ new Map();
1769
+ for (const listing of Object.values(result.fixtures.files?.list ?? {})) for (const item of listing?.items ?? []) if (item?.kind === "file" && typeof item.id === "string") listedFiles.set(item.id, item);
1770
+ const readable = result.fixtures.files?.read ?? {};
1771
+ const metadata = result.fixtures.files?.metadata ?? {};
1772
+ const missing = [...listedFiles.keys()].filter((id) => readable[id] === void 0);
1773
+ const sizeMismatches = [];
1774
+ for (const [id, item] of listedFiles) {
1775
+ const bytes = fixtureContentBytes(readable[id]);
1776
+ if (bytes === null) continue;
1777
+ for (const [source, size] of [["files.list", item.size], ["files.metadata", metadata[id]?.size]]) if (typeof size === "number" && size !== bytes) sizeMismatches.push(`${id} (${source}=${size}, read=${bytes})`);
1778
+ }
1779
+ const ok = missing.length === 0 && sizeMismatches.length === 0;
1780
+ result.checks.push({
1781
+ name: "fixtures 覆盖目录清单中的可点击文件",
1782
+ ok,
1783
+ detail: `${listedFiles.size} listed files`
1784
+ });
1785
+ if (missing.length > 0) result.errors.push(`preview/fixtures.json: files.read 缺少 files.list 中的文件:${missing.join(", ")}`);
1786
+ if (sizeMismatches.length > 0) result.errors.push(`preview/fixtures.json: 文件大小与 read 内容字节数不一致:${sizeMismatches.join(", ")}`);
1787
+ }
1788
+ result.ok = result.errors.length === 0 && result.checks.every((check) => check.ok);
1789
+ return result;
1790
+ }
1791
+ async function commandCheck(input, options, commandName = "check") {
1792
+ if (!input) throw new Error("check 需要工程目录、manifest.json 或 all");
1793
+ const base = resolve(process.cwd());
1794
+ const projects = resolveProjectSet(input);
1795
+ if (projects.length === 0) {
1796
+ const result = {
1797
+ ok: false,
1798
+ command: commandName,
1799
+ checked: 0,
1800
+ base,
1801
+ checks: [],
1802
+ errors: [`${base} 下没有找到 Widget 工程(已查找 examples/sdk、examples/integration 和当前目录的 manifest.json)`],
1803
+ error: structuredError({
1804
+ code: "WIDGET_CLI_NO_WIDGET_PROJECT",
1805
+ message: "check all 没有匹配到任何 Widget 工程,未校验任何文件。",
1806
+ context: {
1807
+ command: commandName,
1808
+ input,
1809
+ base
1810
+ },
1811
+ recovery: "在 Widget 工程目录内执行 bohr-widget check .;或在包含 examples/sdk|examples/integration 的工作区根目录执行 check all。"
1812
+ })
1813
+ };
1814
+ if (options.json) console.log(JSON.stringify(result, null, 2));
1815
+ else {
1816
+ console.error(`错误:${result.errors[0]}`);
1817
+ console.error(`恢复建议:${result.error.recovery}`);
1818
+ }
1819
+ process.exitCode = 1;
1820
+ return result;
1821
+ }
1822
+ const results = [];
1823
+ let passed = true;
1824
+ for (const project of projects) {
1825
+ const label = projectLabel(project);
1826
+ const lint = lintProjectManifest(project);
1827
+ let result;
1828
+ if (lint && !lint.ok) result = {
1829
+ ok: false,
1830
+ label,
1831
+ lintErrors: lint.errors,
1832
+ checks: [{
1833
+ name: "作者清单静态检查",
1834
+ ok: false
1835
+ }],
1836
+ errors: lint.errors,
1837
+ warnings: lint.warnings,
1838
+ manifest: null,
1839
+ sourceManifest: null,
1840
+ manifestKind: "author",
1841
+ projectRoot: project
1842
+ };
1843
+ else {
1844
+ result = applyCliProjectPolicies(await checkWidgetProject(project, { label }));
1845
+ if (lint?.warnings.length) result.warnings = [...lint.warnings, ...result.warnings ?? []];
1846
+ if (!result.ok && result.manifestKind === "host" && looksLikeAuthorManifest(result.sourceManifest)) result.errors = [...result.errors, "manifest.json 含作者字段 contributes,却被按宿主清单校验:请补齐 version,并确认 contributes.widget.type 为 panel 或 file-preview。"];
1847
+ }
1848
+ results.push(result);
1849
+ passed &&= result.ok;
1850
+ if (input !== "all") printResult(options.json ? {
1851
+ command: commandName,
1852
+ ...result,
1853
+ ...result.ok ? {} : { error: structuredError({
1854
+ code: result.lintErrors ? "WIDGET_CLI_MANIFEST_INVALID" : "WIDGET_CLI_VALIDATION_FAILED",
1855
+ message: "Widget 工程校验失败。",
1856
+ context: {
1857
+ projectRoot: result.projectRoot,
1858
+ label
1859
+ },
1860
+ recovery: commandRecovery(commandName)
1861
+ }) }
1862
+ } : result, options.json);
1863
+ }
1864
+ if (input === "all") {
1865
+ const failed = results.filter((result) => !result.ok);
1866
+ const summary = {
1867
+ ok: passed,
1868
+ command: commandName,
1869
+ base,
1870
+ checked: results.length,
1871
+ passed: results.length - failed.length,
1872
+ failed: failed.length,
1873
+ results: results.map((result) => ({
1874
+ label: result.label,
1875
+ projectRoot: result.projectRoot,
1876
+ ok: result.ok,
1877
+ checks: result.checks,
1878
+ errors: result.errors,
1879
+ ...result.warnings?.length ? { warnings: result.warnings } : {}
1880
+ })),
1881
+ ...passed ? {} : { error: structuredError({
1882
+ code: "WIDGET_CLI_VALIDATION_FAILED",
1883
+ message: "部分 Widget 工程校验失败。",
1884
+ context: {
1885
+ base,
1886
+ failedProjects: failed.map((result) => result.projectRoot)
1887
+ },
1888
+ recovery: commandRecovery(commandName)
1889
+ }) }
1890
+ };
1891
+ if (options.json) console.log(JSON.stringify(summary, null, 2));
1892
+ else {
1893
+ for (const result of results) printResult(result, false);
1894
+ console.log(`\n已校验 ${summary.checked} 个工程:${summary.passed} 通过 / ${summary.failed} 失败`);
1895
+ }
1896
+ }
1897
+ if (!passed) process.exitCode = 1;
1898
+ return input === "all" ? void 0 : results[0];
1899
+ }
1900
+ const LOCKFILE_MANAGERS = [
1901
+ ["pnpm-lock.yaml", "pnpm"],
1902
+ ["yarn.lock", "yarn"],
1903
+ ["package-lock.json", "npm"],
1904
+ ["npm-shrinkwrap.json", "npm"]
1905
+ ];
1906
+ /** 用哪个包管理器跑工程构建:packageManager 优先,其次 lockfile,最后回退 npm。 */
1907
+ function detectPackageManager(root, projectPackage) {
1908
+ const declaredManager = String(projectPackage.packageManager ?? "").split("@")[0];
1909
+ if ([
1910
+ "pnpm",
1911
+ "yarn",
1912
+ "npm"
1913
+ ].includes(declaredManager)) return declaredManager;
1914
+ for (const [lockfile, manager] of LOCKFILE_MANAGERS) if (existsSync(join(root, lockfile))) return manager;
1915
+ return "npm";
1916
+ }
1917
+ function firstFailureLine(output) {
1918
+ const lines = String(output ?? "").split("\n").map((item) => item.trim()).filter((item) => item.length > 0);
1919
+ const line = lines.find((item) => !/^(?:npm|pnpm|yarn)\s+(?:warn|warning)\b/iu.test(item)) ?? lines[0];
1920
+ return line ? line.slice(0, 300) : void 0;
1921
+ }
1922
+ const UNRESOLVED_BUILD_PATTERNS = [
1923
+ /\[UNRESOLVED_IMPORT\]/u,
1924
+ /Could not resolve ['"][^'"]+['"]/u,
1925
+ /Module not found, treating it as an external dependency/u,
1926
+ /No name was provided for external module/u
1927
+ ];
1928
+ function unresolvedBuildDiagnostic(stdout, stderr) {
1929
+ return `${stderr ?? ""}\n${stdout ?? ""}`.split("\n").map((line) => line.trim()).filter(Boolean).find((line) => UNRESOLVED_BUILD_PATTERNS.some((pattern) => pattern.test(line)))?.slice(0, 300);
1930
+ }
1931
+ async function runProjectBuild(root, { capture = false } = {}) {
1932
+ const manager = detectPackageManager(root, JSON.parse(readFileSync(join(root, "package.json"), "utf8")));
1933
+ const { spawn } = await import("node:child_process");
1934
+ const command = process.platform === "win32" ? `${manager}.cmd` : manager;
1935
+ return await new Promise((resolvePromise, reject) => {
1936
+ const child = spawn(command, ["run", "build"], {
1937
+ cwd: root,
1938
+ stdio: capture ? [
1939
+ "ignore",
1940
+ "pipe",
1941
+ "pipe"
1942
+ ] : "inherit"
1943
+ });
1944
+ let stdout = "";
1945
+ let stderr = "";
1946
+ if (capture) {
1947
+ child.stdout.on("data", (chunk) => {
1948
+ stdout += String(chunk);
1949
+ });
1950
+ child.stderr.on("data", (chunk) => {
1951
+ stderr += String(chunk);
1952
+ });
1953
+ }
1954
+ child.once("error", (error) => reject(error));
1955
+ child.once("exit", (code, signal) => {
1956
+ const result = {
1957
+ code,
1958
+ signal,
1959
+ stdout,
1960
+ stderr
1961
+ };
1962
+ if (code === 0) {
1963
+ const unresolved = unresolvedBuildDiagnostic(stdout, stderr);
1964
+ if (!unresolved) {
1965
+ resolvePromise(result);
1966
+ return;
1967
+ }
1968
+ const error = /* @__PURE__ */ new Error(`构建器退出码为 0,但存在未解析依赖:${unresolved}`);
1969
+ Object.assign(error, result, { cliCode: "WIDGET_CLI_BUILD_UNRESOLVED_IMPORT" });
1970
+ reject(error);
1971
+ } else {
1972
+ const detail = firstFailureLine(stderr) ?? firstFailureLine(stdout);
1973
+ const error = /* @__PURE__ */ new Error(`构建失败(exit ${code ?? signal ?? "unknown"})${detail ? `:${detail}` : ""}`);
1974
+ Object.assign(error, result);
1975
+ reject(error);
1976
+ }
1977
+ });
1978
+ });
1979
+ }
1980
+ async function commandBuild(directory, options = {}) {
1981
+ if (!directory) throw new Error("build 需要工程目录");
1982
+ const root = resolve(directory);
1983
+ try {
1984
+ const build = await runProjectBuild(root, { capture: true });
1985
+ const result = {
1986
+ ok: true,
1987
+ command: "build",
1988
+ projectRoot: root,
1989
+ stdout: build.stdout,
1990
+ stderr: build.stderr
1991
+ };
1992
+ if (options.json && options.emit !== false) console.log(JSON.stringify(result, null, 2));
1993
+ else if (options.emit !== false) {
1994
+ if (build.stdout) process.stdout.write(build.stdout);
1995
+ if (build.stderr) process.stderr.write(build.stderr);
1996
+ }
1997
+ return result;
1998
+ } catch (error) {
1999
+ const result = {
2000
+ ok: false,
2001
+ command: "build",
2002
+ projectRoot: root,
2003
+ error: commandError("build", error, {
2004
+ projectRoot: root,
2005
+ phase: "build"
2006
+ }),
2007
+ stdout: error?.stdout ?? "",
2008
+ stderr: error?.stderr ?? ""
2009
+ };
2010
+ if (options.json && options.emit !== false) console.log(JSON.stringify(result, null, 2));
2011
+ if (!options.json && options.emit !== false) {
2012
+ if (result.stdout) process.stdout.write(result.stdout);
2013
+ if (result.stderr) process.stderr.write(result.stderr);
2014
+ }
2015
+ process.exitCode = 1;
2016
+ if (!options.json && options.emit !== false) throw error;
2017
+ return result;
2018
+ }
2019
+ }
2020
+ async function commandPack(directory, options) {
2021
+ if (!directory) throw new Error("pack 需要工程目录");
2022
+ if (options.build) {
2023
+ const build = await commandBuild(directory, {
2024
+ ...options,
2025
+ emit: false
2026
+ });
2027
+ if (!options.json) {
2028
+ if (build.stdout) process.stdout.write(build.stdout);
2029
+ if (build.stderr) process.stderr.write(build.stderr);
2030
+ }
2031
+ if (!build.ok) {
2032
+ if (options.json) console.log(JSON.stringify({
2033
+ ...build,
2034
+ command: "pack",
2035
+ phase: "build"
2036
+ }, null, 2));
2037
+ process.exitCode = 1;
2038
+ return build;
2039
+ }
2040
+ }
2041
+ const result = applyCliProjectPolicies(await checkWidgetProject(resolve(directory), { requireBuiltEntry: true }));
2042
+ if (!options.json) printResult(result, false);
2043
+ if (!result.ok) {
2044
+ if (options.json) console.log(JSON.stringify({
2045
+ ok: false,
2046
+ command: "pack",
2047
+ projectRoot: result.projectRoot,
2048
+ error: structuredError({
2049
+ code: "WIDGET_CLI_PACK_VALIDATION_FAILED",
2050
+ message: "Widget 工程校验失败,未生成可交付 Bundle。",
2051
+ context: {
2052
+ projectRoot: result.projectRoot,
2053
+ phase: "validate"
2054
+ },
2055
+ recovery: commandRecovery("pack")
2056
+ }),
2057
+ checks: result.checks,
2058
+ errors: result.errors
2059
+ }, null, 2));
2060
+ process.exitCode = 1;
2061
+ return {
2062
+ ok: false,
2063
+ command: "pack",
2064
+ projectRoot: result.projectRoot
2065
+ };
2066
+ }
2067
+ const destination = resolve(options.output ?? join(result.projectRoot, `${result.manifest.name}.zip`));
2068
+ const fixturesPath = join(result.projectRoot, "preview", "fixtures.json");
2069
+ const hasFixtures = existsSync(fixturesPath);
2070
+ const fixturesIncluded = hasFixtures && Boolean(options.includePreviewFixtures);
2071
+ const warnings = [];
2072
+ if (options.includePreviewFixtures && !hasFixtures) warnings.push("使用了 --include-preview-fixtures,但 preview/fixtures.json 不存在,ZIP 未包含任何 fixtures。");
2073
+ else if (hasFixtures && !options.includePreviewFixtures) warnings.push("工程包含 preview/fixtures.json,未使用 --include-preview-fixtures,ZIP 未包含 fixtures。");
2074
+ const bundle = buildWidgetPackage(result, { includePreviewFixtures: fixturesIncluded });
2075
+ await mkdir(dirname(destination), { recursive: true });
2076
+ await writeFile(destination, bundle);
2077
+ const artifact = {
2078
+ ok: true,
2079
+ command: "pack",
2080
+ projectRoot: result.projectRoot,
2081
+ path: destination,
2082
+ bytes: bundle.byteLength,
2083
+ sha256: `sha256:${createHash("sha256").update(bundle).digest("hex")}`,
2084
+ toolchain: {
2085
+ cli: packageJson.version,
2086
+ sdk: sdkPackage.version,
2087
+ contracts: contractsPackage.version
2088
+ },
2089
+ manifest: {
2090
+ name: result.manifest.name,
2091
+ entry: result.manifest.widgetConfig.entry,
2092
+ contribution: result.manifest.widgetConfig.contribution,
2093
+ sampleIds: (result.manifest.widgetConfig.samples ?? []).map((sample) => sample.id)
2094
+ },
2095
+ checks: result.checks,
2096
+ fixtures: {
2097
+ path: fixturesPath,
2098
+ requested: Boolean(options.includePreviewFixtures),
2099
+ included: fixturesIncluded
2100
+ },
2101
+ ...warnings.length === 0 ? {} : { warnings }
2102
+ };
2103
+ if (options.json) console.log(JSON.stringify(artifact, null, 2));
2104
+ else {
2105
+ console.log(`已生成可加载插件包:${destination}`);
2106
+ for (const warning of warnings) console.warn(`提示:${warning}`);
2107
+ }
2108
+ return artifact;
2109
+ }
2110
+ async function commandMigrate(positional, options) {
2111
+ if (positional[0] !== "dsh") throw new Error("migrate 目前只支持 dsh");
2112
+ const action = positional[1];
2113
+ const input = positional[2];
2114
+ if (!input) throw new Error(`migrate dsh ${action ?? ""} 需要源插件目录`);
2115
+ if (action === "audit") {
2116
+ const report = await auditDshPlugin(input);
2117
+ if (options.json) console.log(JSON.stringify({
2118
+ ok: true,
2119
+ command: "migrate dsh audit",
2120
+ report
2121
+ }, null, 2));
2122
+ else console.log(`${report.migrationStatus}: ${report.source.name ?? "unknown"} (${report.compatibility.status})`);
2123
+ return report;
2124
+ }
2125
+ if (action === "scaffold") {
2126
+ if (!options.output) throw new Error("migrate dsh scaffold 需要 --out <widget-dir>");
2127
+ const result = await scaffoldDshWidget(input, options.output, {
2128
+ sdkVersion: sdkPackage.version,
2129
+ contractsVersion: contractsPackage.version
2130
+ });
2131
+ if (options.json) console.log(JSON.stringify(result, null, 2));
2132
+ else console.log(`已生成迁移 Widget:${result.projectRoot}`);
2133
+ return result;
2134
+ }
2135
+ throw new Error(`未知 migrate dsh 操作:${action ?? ""}`);
2136
+ }
2137
+ async function commandDoctor(directory, options) {
2138
+ const root = resolve(directory ?? ".");
2139
+ const packagePath = join(root, "package.json");
2140
+ const manifestPath = join(root, "manifest.json");
2141
+ const report = {
2142
+ cli: packageJson.version,
2143
+ node: process.versions.node,
2144
+ sdk: sdkPackage.version,
2145
+ contracts: contractsPackage.version,
2146
+ project: root,
2147
+ packageJson: existsSync(packagePath),
2148
+ manifest: existsSync(manifestPath),
2149
+ nodeSupported: Number(process.versions.node.split(".")[0]) >= 20,
2150
+ dependenciesInstalled: existsSync(join(root, "node_modules")),
2151
+ manifestValid: false,
2152
+ warnings: []
2153
+ };
2154
+ if (report.manifest) try {
2155
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
2156
+ report.manifestValid = true;
2157
+ report.manifestKind = manifest.contributes ? "plugin" : "host";
2158
+ report.pluginName = manifest.name;
2159
+ report.entryPath = manifest.widgetConfig?.entry;
2160
+ } catch {
2161
+ report.manifestKind = "invalid";
2162
+ }
2163
+ report.entryPath ??= "dist/widget.js";
2164
+ report.entry = existsSync(join(root, report.entryPath));
2165
+ report.ok = report.nodeSupported && report.packageJson && report.manifest && report.manifestValid;
2166
+ report.ready = report.ok && report.dependenciesInstalled && report.entry;
2167
+ if (report.manifest && !report.manifestValid) report.warnings.push("manifest.json 不是有效 JSON:修复语法后再执行 doctor、check 或 pack。");
2168
+ if (!report.dependenciesInstalled) report.warnings.push("未找到 node_modules:先执行包管理器 install,build/pack 需要工程依赖。");
2169
+ if (!report.entry) report.warnings.push(`未找到构建入口 ${report.entryPath}:先执行 bohr-widget build . 或工程 build 脚本。`);
2170
+ if (options.json) console.log(JSON.stringify(report, null, 2));
2171
+ else {
2172
+ console.log(`${report.ok ? "✓" : "✗"} 环境检查${report.ok ? "通过" : "失败"}:Node ${report.node},SDK ${report.sdk},Contracts ${report.contracts}`);
2173
+ console.log(`${report.ready ? "✓" : "✗"} 工程可直接 check/pack:${report.ready ? "是" : "否"}`);
2174
+ for (const warning of report.warnings) console.warn(`提示:${warning}`);
2175
+ }
2176
+ if (!report.ok) process.exitCode = 1;
2177
+ }
2178
+ function commandSchema() {
2179
+ console.log(require.resolve("@dp-bohrium/widget-contracts/schemas/widget-plugin-manifest.schema.json"));
2180
+ }
2181
+ function commandVersion(options) {
2182
+ const result = {
2183
+ ok: true,
2184
+ command: "version",
2185
+ cli: packageJson.version,
2186
+ sdk: sdkPackage.version,
2187
+ contracts: contractsPackage.version
2188
+ };
2189
+ if (options.json) console.log(JSON.stringify(result, null, 2));
2190
+ else console.log(`${result.cli} (SDK ${result.sdk})`);
2191
+ return result;
2192
+ }
2193
+ async function main(argv) {
2194
+ if (argv.includes("--help") || argv.includes("-h")) return console.log(usage());
2195
+ const command = argv[0] ?? "help";
2196
+ if (command === "dev") {
2197
+ const exitCode = await runWidgetDevelopmentCli(argv.slice(1));
2198
+ if (exitCode !== 0) process.exitCode = exitCode;
2199
+ return exitCode;
2200
+ }
2201
+ const { positional, options } = parseOptions(argv.slice(1));
2202
+ if (!COMMANDS.includes(command)) throw new Error(`未知命令:${command}`);
2203
+ if (command === "help") return console.log(usage());
2204
+ if (command === "version") return commandVersion(options);
2205
+ if (command === "create") return commandCreate(positional[0], options);
2206
+ if (command === "check" || command === "validate") return commandCheck(positional[0], options, command);
2207
+ if (command === "build") return commandBuild(positional[0], options);
2208
+ if (command === "pack") return commandPack(positional[0], options);
2209
+ if (command === "doctor") return commandDoctor(positional[0], options);
2210
+ if (command === "migrate") return commandMigrate(positional, options);
2211
+ if (command === "schema") return commandSchema();
2212
+ }
2213
+ /**
2214
+ * 是否直接执行本文件。npm/pnpm 的 node_modules/.bin/bohr-widget 是符号链接,
2215
+ * import.meta.url 已被 Node 解析为 realpath,而 process.argv[1] 保留调用时的路径;
2216
+ * 只做 resolve() 比较会让 `bohr-widget ...`、`npx bohr-widget ...` 静默空转(exit 0)。
2217
+ */
2218
+ function isDirectInvocation() {
2219
+ const entry = process.argv[1];
2220
+ if (!entry) return false;
2221
+ const normalize = (value) => {
2222
+ let resolved;
2223
+ try {
2224
+ resolved = realpathSync(value);
2225
+ } catch {
2226
+ resolved = resolve(value);
2227
+ }
2228
+ return process.platform === "win32" ? resolved.toLowerCase() : resolved;
2229
+ };
2230
+ return normalize(entry) === normalize(fileURLToPath(import.meta.url));
2231
+ }
2232
+ if (isDirectInvocation()) try {
2233
+ await main(process.argv.slice(2));
2234
+ } catch (error) {
2235
+ const argv = process.argv.slice(2);
2236
+ const operands = argv.filter((argument) => !argument.startsWith("-"));
2237
+ const command = operands[0] ?? "help";
2238
+ if (argv.includes("--json")) console.log(JSON.stringify({
2239
+ ok: false,
2240
+ command,
2241
+ error: commandError(command, error, {
2242
+ input: operands[1] ?? null,
2243
+ stdout: error?.stdout ?? "",
2244
+ stderr: error?.stderr ?? ""
2245
+ })
2246
+ }, null, 2));
2247
+ else console.error(`${error instanceof Error ? error.message : String(error)}\n\n${usage()}`);
2248
+ process.exitCode = 1;
2249
+ }
2250
+ //#endregion
2251
+ export {};