@international-iot-association/plugin-cli 1.0.0-rc.1

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.js ADDED
@@ -0,0 +1,961 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
5
+ import { basename, join as join5, resolve as resolve2 } from "path";
6
+
7
+ // src/check.mjs
8
+ import { existsSync, readFileSync, readdirSync } from "fs";
9
+ import { join } from "path";
10
+ import { assertPluginUiSingleReact } from "@international-iot-association/plugin-vite-config";
11
+
12
+ // src/manifest-schema.mjs
13
+ var SEMVER_RE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
14
+ var PLUGIN_ID_RE = /^[a-z][a-z0-9]*(\.[a-z0-9][a-z0-9-]*)+$/;
15
+ var PERMISSION_RE = /^[a-z][a-z0-9]*([.:][a-z0-9][a-z0-9-]*)*$/;
16
+ var DEP_RANGE_RE = /^[0-9a-zA-Z.^~*<>=| -]+$/;
17
+ var WINDOWS_RESERVED_NAMES = /* @__PURE__ */ new Set([
18
+ "con",
19
+ "prn",
20
+ "aux",
21
+ "nul",
22
+ ...Array.from({ length: 9 }, (_, i) => `com${i + 1}`),
23
+ ...Array.from({ length: 9 }, (_, i) => `lpt${i + 1}`)
24
+ ]);
25
+ var KNOWN_FIELDS = /* @__PURE__ */ new Set([
26
+ "id",
27
+ "name",
28
+ "version",
29
+ "description",
30
+ "kind",
31
+ "stationTypes",
32
+ "models",
33
+ "agentApi",
34
+ "uiEntry",
35
+ "runtimeEntry",
36
+ "permissions",
37
+ "autoStart",
38
+ "dependencies",
39
+ "resultSchema",
40
+ "steps",
41
+ "checksum",
42
+ "storage",
43
+ "readinessTimeoutMs"
44
+ ]);
45
+ function isStrictSemver(value) {
46
+ return typeof value === "string" && SEMVER_RE.test(value);
47
+ }
48
+ function isValidPluginId(value) {
49
+ if (typeof value !== "string" || value.length === 0 || value.length > 128) return false;
50
+ if (!PLUGIN_ID_RE.test(value)) return false;
51
+ const firstSegment = value.split(".", 1)[0];
52
+ if (WINDOWS_RESERVED_NAMES.has(firstSegment.toLowerCase())) return false;
53
+ return true;
54
+ }
55
+ function isSafeRelativeEntry(value) {
56
+ if (typeof value !== "string" || value.length === 0 || value.length > 512) return false;
57
+ if (value.includes("\\") || value.includes("\0")) return false;
58
+ if (value.startsWith("/") || /^[a-zA-Z]:/.test(value) || value.startsWith("//")) return false;
59
+ const segments = value.split("/");
60
+ for (const segment of segments) {
61
+ if (segment === "" || segment === "." || segment === "..") return false;
62
+ if (segment !== segment.trim() || segment.endsWith(".")) return false;
63
+ const stem = segment.split(".", 1)[0];
64
+ if (WINDOWS_RESERVED_NAMES.has(stem.toLowerCase())) return false;
65
+ }
66
+ return true;
67
+ }
68
+ function isStringArray(value) {
69
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
70
+ }
71
+ function validatePluginManifest(manifest) {
72
+ const errors = [];
73
+ const push = (msg) => errors.push(msg);
74
+ if (manifest === null || typeof manifest !== "object" || Array.isArray(manifest)) {
75
+ return { ok: false, errors: ["manifest \u5FC5\u987B\u662F JSON object"] };
76
+ }
77
+ const m = (
78
+ /** @type {Record<string, unknown>} */
79
+ manifest
80
+ );
81
+ for (const key of Object.keys(m)) {
82
+ if (!KNOWN_FIELDS.has(key))
83
+ push(
84
+ `\u672A\u77E5\u5B57\u6BB5 "${key}"\uFF08fail-closed\uFF1A\u65B0\u5B57\u6BB5\u9700\u540C\u6B65\u66F4\u65B0 manifest-schema \u4E0E plugin-contracts\uFF09`
85
+ );
86
+ }
87
+ if (!isValidPluginId(m.id))
88
+ push(`id \u975E\u6CD5\uFF1A"${m.id}"\uFF08\u8981\u6C42\u5168\u5C0F\u5199\u3001\u81F3\u5C11\u4E24\u6BB5\u3001\u65E0\u4FDD\u7559\u540D\uFF0C\u5982 rti.demo.example\uFF09`);
89
+ if (typeof m.name !== "string" || m.name.trim() === "") push("name \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32");
90
+ if (!isStrictSemver(m.version)) push(`version \u5FC5\u987B\u662F strict SemVer\uFF1A"${m.version}"`);
91
+ if (!isStrictSemver(m.agentApi)) push(`agentApi \u5FC5\u987B\u662F strict SemVer\uFF1A"${m.agentApi}"`);
92
+ const kind = m.kind ?? "station";
93
+ if (kind !== "station" && kind !== "global") push(`kind \u53EA\u80FD\u662F station/global\uFF1A"${m.kind}"`);
94
+ if (!isStringArray(m.stationTypes)) push("stationTypes \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u6570\u7EC4\uFF08\u53EF\u4E3A\u7A7A\u6570\u7EC4\uFF09");
95
+ if (m.models !== void 0 && !isStringArray(m.models))
96
+ push("models \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u6570\u7EC4\uFF08\u53EF\u7701\u7565\uFF1B\u7701\u7565\u7B49\u4EF7\u4E8E\u7A7A\u6570\u7EC4\uFF09");
97
+ if (!isSafeRelativeEntry(m.runtimeEntry)) push(`runtimeEntry \u975E\u6CD5\uFF1A"${m.runtimeEntry}"`);
98
+ if (kind === "station") {
99
+ if (!isSafeRelativeEntry(m.uiEntry)) push(`station \u63D2\u4EF6\u5FC5\u987B\u63D0\u4F9B\u5408\u6CD5 uiEntry\uFF1A"${m.uiEntry}"`);
100
+ } else if (m.uiEntry !== void 0 && !isSafeRelativeEntry(m.uiEntry)) {
101
+ push(`uiEntry \u975E\u6CD5\uFF1A"${m.uiEntry}"`);
102
+ }
103
+ if (!isStringArray(m.permissions)) {
104
+ push("permissions \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u6570\u7EC4\uFF08\u53EF\u4E3A\u7A7A\u6570\u7EC4\uFF09");
105
+ } else {
106
+ for (const perm of m.permissions) {
107
+ if (!PERMISSION_RE.test(perm)) push(`permission \u975E\u6CD5\uFF1A"${perm}"`);
108
+ }
109
+ }
110
+ if (m.autoStart !== void 0 && typeof m.autoStart !== "boolean")
111
+ push("autoStart \u5FC5\u987B\u662F boolean");
112
+ if (m.description !== void 0 && typeof m.description !== "string")
113
+ push("description \u5FC5\u987B\u662F\u5B57\u7B26\u4E32");
114
+ if (m.checksum !== void 0 && typeof m.checksum !== "string") push("checksum \u5FC5\u987B\u662F\u5B57\u7B26\u4E32");
115
+ if (m.storage !== void 0) {
116
+ if (m.storage === null || typeof m.storage !== "object" || Array.isArray(m.storage)) {
117
+ push("storage \u5FC5\u987B\u662F { scope?: 'version' | 'shared', migrateLegacy?: boolean } \u5BF9\u8C61");
118
+ } else {
119
+ for (const key of Object.keys(m.storage)) {
120
+ if (key !== "scope" && key !== "migrateLegacy") {
121
+ push(`storage \u672A\u77E5\u5B57\u6BB5 "${key}"\uFF08\u53EA\u5141\u8BB8 scope/migrateLegacy\uFF09`);
122
+ }
123
+ }
124
+ const scope = m.storage.scope;
125
+ if (scope !== void 0 && scope !== "version" && scope !== "shared") {
126
+ push(`storage.scope \u53EA\u80FD\u662F version/shared\uFF1A"${scope}"`);
127
+ }
128
+ if (m.storage.migrateLegacy !== void 0 && typeof m.storage.migrateLegacy !== "boolean") {
129
+ push(`storage.migrateLegacy \u5FC5\u987B\u662F boolean\uFF1A"${m.storage.migrateLegacy}"`);
130
+ }
131
+ }
132
+ }
133
+ if (m.readinessTimeoutMs !== void 0) {
134
+ if (typeof m.readinessTimeoutMs !== "number" || !Number.isInteger(m.readinessTimeoutMs) || m.readinessTimeoutMs <= 0) {
135
+ push(`readinessTimeoutMs \u5FC5\u987B\u662F\u6B63\u6574\u6570\u6BEB\u79D2\u6570\uFF1A"${m.readinessTimeoutMs}"`);
136
+ }
137
+ }
138
+ if (m.resultSchema !== void 0 && !isSafeRelativeEntry(m.resultSchema)) {
139
+ push(`resultSchema \u975E\u6CD5\uFF1A"${m.resultSchema}"`);
140
+ }
141
+ if (m.dependencies !== void 0) {
142
+ if (m.dependencies === null || typeof m.dependencies !== "object" || Array.isArray(m.dependencies)) {
143
+ push("dependencies \u5FC5\u987B\u662F { pluginId: versionRange } \u5BF9\u8C61");
144
+ } else {
145
+ for (const [depId, range] of Object.entries(m.dependencies)) {
146
+ if (!isValidPluginId(depId)) push(`dependencies \u952E\u975E\u6CD5\uFF1A"${depId}"`);
147
+ if (typeof range !== "string" || range.trim() === "" || !DEP_RANGE_RE.test(range)) {
148
+ push(`dependencies["${depId}"] \u7248\u672C\u8303\u56F4\u975E\u6CD5\uFF1A"${range}"`);
149
+ }
150
+ }
151
+ }
152
+ }
153
+ if (m.steps !== void 0) {
154
+ if (!Array.isArray(m.steps)) {
155
+ push("steps \u5FC5\u987B\u662F\u6570\u7EC4");
156
+ } else {
157
+ m.steps.forEach((step, i) => {
158
+ if (step === null || typeof step !== "object" || Array.isArray(step)) {
159
+ push(`steps[${i}] \u5FC5\u987B\u662F\u5BF9\u8C61`);
160
+ return;
161
+ }
162
+ if (typeof step.key !== "string" || step.key.trim() === "")
163
+ push(`steps[${i}].key \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32`);
164
+ if (typeof step.label !== "string" || step.label.trim() === "")
165
+ push(`steps[${i}].label \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32`);
166
+ if (step.description !== void 0 && typeof step.description !== "string") {
167
+ push(`steps[${i}].description \u5FC5\u987B\u662F\u5B57\u7B26\u4E32`);
168
+ }
169
+ });
170
+ }
171
+ }
172
+ return { ok: errors.length === 0, errors };
173
+ }
174
+
175
+ // src/check.mjs
176
+ function isEmptyTestScript(script) {
177
+ if (typeof script !== "string") return true;
178
+ const trimmed = script.trim();
179
+ if (trimmed.length === 0) return true;
180
+ return /^(echo\b|true$|exit\s+0$|:$)/.test(trimmed);
181
+ }
182
+ function checkPlugin(pluginDir) {
183
+ const errors = [];
184
+ const manifestPath = join(pluginDir, "manifest.json");
185
+ if (!existsSync(manifestPath)) {
186
+ return { ok: false, errors: [`missing manifest.json in ${pluginDir}`] };
187
+ }
188
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
189
+ const { ok: manifestOk, errors: manifestErrors } = validatePluginManifest(manifest);
190
+ if (!manifestOk) {
191
+ for (const e of manifestErrors) errors.push(`manifest: ${e}`);
192
+ }
193
+ const pkgPath = join(pluginDir, "package.json");
194
+ if (!existsSync(pkgPath)) {
195
+ errors.push("missing package.json");
196
+ } else {
197
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
198
+ if (manifestOk && pkg.name !== manifest.id) {
199
+ errors.push(`package.json.name (${pkg.name}) !== manifest.id (${manifest.id})`);
200
+ }
201
+ if (isEmptyTestScript(pkg.scripts?.test)) {
202
+ errors.push("package.json.scripts.test \u7F3A\u5931\u6216\u662F\u7A7A\u58F3");
203
+ }
204
+ }
205
+ const testDir = join(pluginDir, "test");
206
+ const hasTestFile = existsSync(testDir) && readdirSync(testDir).some((f) => f.endsWith(".test.mjs"));
207
+ if (!hasTestFile) errors.push("\u7F3A\u5C11 test/*.test.mjs\uFF08\u81F3\u5C11 1 \u4E2A\uFF09");
208
+ if (!existsSync(join(pluginDir, "smoke.mjs"))) {
209
+ errors.push("\u7F3A\u5C11 smoke.mjs");
210
+ }
211
+ const reactGate = assertPluginUiSingleReact(pluginDir);
212
+ if (!reactGate.ok) errors.push(reactGate.error);
213
+ return { ok: errors.length === 0, errors };
214
+ }
215
+
216
+ // src/new.ts
217
+ import { execFileSync } from "child_process";
218
+ import {
219
+ cpSync,
220
+ existsSync as existsSync2,
221
+ mkdtempSync,
222
+ readFileSync as readFileSync2,
223
+ readdirSync as readdirSync2,
224
+ rmSync,
225
+ statSync,
226
+ writeFileSync
227
+ } from "fs";
228
+ import { tmpdir } from "os";
229
+ import { dirname, join as join2, resolve } from "path";
230
+ import { fileURLToPath } from "url";
231
+ var DEFAULT_TEMPLATE_PACKAGE = "@international-iot-association/plugin-template";
232
+ var SKIP_ENTRIES = /* @__PURE__ */ new Set(["node_modules", "ui-dist", "runtime-dist", ".turbo"]);
233
+ var TEXT_FILE_RE = /\.(ts|tsx|json|css|html|mjs|md)$/;
234
+ var DIR_NAME_RE = /^[a-z0-9][a-z0-9._-]*$/;
235
+ function lastPathSegment(path) {
236
+ const parts = path.split(/[\\/]/);
237
+ return parts[parts.length - 1] ?? "";
238
+ }
239
+ function shouldSkipCopy(src) {
240
+ return SKIP_ENTRIES.has(lastPathSegment(src));
241
+ }
242
+ function assertSafeDirName(dirName) {
243
+ if (!DIR_NAME_RE.test(dirName)) {
244
+ throw new Error(
245
+ `\u76EE\u5F55\u540D\u975E\u6CD5\uFF1A"${dirName}"\uFF08\u53EA\u5141\u8BB8\u5C0F\u5199\u5B57\u6BCD/\u6570\u5B57/._-\uFF0C\u4E14\u4E0D\u80FD\u4EE5 . \u6216 - \u5F00\u5934\uFF09`
246
+ );
247
+ }
248
+ }
249
+ function extractTarball(tgzPath) {
250
+ const extractDir = mkdtempSync(join2(tmpdir(), "plugin-cli-tpl-extract-"));
251
+ execFileSync("tar", ["-xzf", tgzPath, "-C", extractDir], { stdio: "pipe" });
252
+ const packageDir = join2(extractDir, "package");
253
+ if (!existsSync2(packageDir)) {
254
+ throw new Error(`\u6A21\u677F\u538B\u7F29\u5305\u89E3\u538B\u540E\u672A\u627E\u5230 package/ \u76EE\u5F55\uFF08${tgzPath}\uFF09\uFF1A\u4E0D\u662F\u5408\u6CD5\u7684 npm \u5305\u4EA7\u7269`);
255
+ }
256
+ return packageDir;
257
+ }
258
+ function readOwnVersion() {
259
+ try {
260
+ const here = dirname(fileURLToPath(import.meta.url));
261
+ const pkg = JSON.parse(readFileSync2(join2(here, "..", "package.json"), "utf8"));
262
+ return typeof pkg.version === "string" ? pkg.version : "";
263
+ } catch {
264
+ return "";
265
+ }
266
+ }
267
+ function resolveDefaultTemplateSpec(cliVersion, override) {
268
+ if (override && override.trim()) return override.trim();
269
+ return cliVersion.includes("-") ? "next" : "latest";
270
+ }
271
+ function fetchDefaultTemplate(templateVersion) {
272
+ const spec = resolveDefaultTemplateSpec(readOwnVersion(), templateVersion);
273
+ const packDestDir = mkdtempSync(join2(tmpdir(), "plugin-cli-tpl-pack-"));
274
+ let stdout;
275
+ try {
276
+ stdout = execFileSync(
277
+ "npm",
278
+ ["pack", `${DEFAULT_TEMPLATE_PACKAGE}@${spec}`, "--pack-destination", packDestDir],
279
+ {
280
+ stdio: ["ignore", "pipe", "pipe"],
281
+ encoding: "utf8",
282
+ // Windows 上 npm 实际是 npm.cmd,不带 shell 直接 spawn 会 EINVAL/ENOENT
283
+ // (Node ≥18.20.2/20.12.2 的 CVE-2024-27980 修复后行为);
284
+ // 与 test-runner.mjs 里 pnpm 的处理保持一致。
285
+ shell: process.platform === "win32"
286
+ }
287
+ );
288
+ } catch (err) {
289
+ throw new Error(
290
+ `\u62C9\u53D6\u9ED8\u8BA4\u6A21\u677F\u5305 ${DEFAULT_TEMPLATE_PACKAGE}@${spec} \u5931\u8D25\uFF1A\u53EF\u80FD\u662F\u7F51\u7EDC / registry \u4E0D\u53EF\u8FBE\uFF0C\u6216\u8BE5\u7248\u672C / dist-tag \u4E0B\u6CA1\u6709\u53D1\u5E03\u8FC7\u6A21\u677F\u5305\u3002\u53EF\u7528 --template-version <\u7248\u672C\u6216 dist-tag> \u6307\u5B9A\uFF0C\u6216\u6539\u7528 --template <\u672C\u5730\u6A21\u677F\u76EE\u5F55\u6216 .tgz>\uFF08\u4ED3\u5185\u4E3A --template templates/plugin\uFF09\u3002
291
+ \u539F\u59CB\u9519\u8BEF\uFF1A${err.message}`
292
+ );
293
+ }
294
+ const lastLine = stdout.trim().split("\n").pop() ?? "";
295
+ const tgzPath = join2(packDestDir, lastLine.trim());
296
+ if (!lastLine || !existsSync2(tgzPath)) {
297
+ throw new Error(`npm pack ${DEFAULT_TEMPLATE_PACKAGE} \u672A\u4EA7\u51FA\u53EF\u8BC6\u522B\u7684 .tgz \u6587\u4EF6`);
298
+ }
299
+ return extractTarball(tgzPath);
300
+ }
301
+ function resolveTemplateDir(template, templateVersion) {
302
+ if (!template) return fetchDefaultTemplate(templateVersion);
303
+ const resolved = resolve(template);
304
+ if (resolved.endsWith(".tgz")) {
305
+ if (!existsSync2(resolved)) throw new Error(`\u6A21\u677F\u538B\u7F29\u5305\u4E0D\u5B58\u5728\uFF1A${resolved}`);
306
+ return extractTarball(resolved);
307
+ }
308
+ if (!existsSync2(resolved) || !statSync(resolved).isDirectory()) {
309
+ throw new Error(`--template \u6307\u5411\u7684\u76EE\u5F55\u4E0D\u5B58\u5728\u6216\u4E0D\u662F\u76EE\u5F55\uFF1A${resolved}`);
310
+ }
311
+ return resolved;
312
+ }
313
+ function replacePlaceholders(dest, id, displayName) {
314
+ const walk = (d) => {
315
+ for (const e of readdirSync2(d, { withFileTypes: true })) {
316
+ const p = join2(d, e.name);
317
+ if (e.isDirectory()) {
318
+ walk(p);
319
+ } else if (TEXT_FILE_RE.test(e.name)) {
320
+ const s = readFileSync2(p, "utf8");
321
+ const next = s.split("__PLUGIN_ID__").join(id).split("__PLUGIN_NAME__").join(displayName);
322
+ if (next !== s) writeFileSync(p, next);
323
+ }
324
+ }
325
+ };
326
+ walk(dest);
327
+ }
328
+ function scaffoldPlugin(opts) {
329
+ assertSafeDirName(opts.dirName);
330
+ if (!isValidPluginId(opts.id)) {
331
+ throw new Error(
332
+ `\u63D2\u4EF6 id \u975E\u6CD5\uFF1A"${opts.id}"\uFF08\u8981\u6C42\u5168\u5C0F\u5199\u3001\u81F3\u5C11\u4E24\u6BB5\u3001\u65E0 Windows \u4FDD\u7559\u540D\uFF0C\u5982 rti.demo.water-sensor\uFF09`
333
+ );
334
+ }
335
+ const dest = join2(resolve(opts.outParentDir), opts.dirName);
336
+ if (existsSync2(dest)) {
337
+ throw new Error(`\u76EE\u6807\u5DF2\u5B58\u5728\uFF0C\u62D2\u7EDD\u8986\u76D6\uFF1A${dest}`);
338
+ }
339
+ const templateDir = resolveTemplateDir(opts.template, opts.templateVersion);
340
+ cpSync(templateDir, dest, {
341
+ recursive: true,
342
+ filter: (src) => !shouldSkipCopy(src)
343
+ });
344
+ replacePlaceholders(dest, opts.id, opts.displayName);
345
+ const pkgPath = join2(dest, "package.json");
346
+ if (existsSync2(pkgPath)) {
347
+ const pkg = JSON.parse(readFileSync2(pkgPath, "utf8"));
348
+ pkg.name = opts.id;
349
+ writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}
350
+ `);
351
+ }
352
+ const manifestPath = join2(dest, "manifest.json");
353
+ if (!existsSync2(manifestPath)) {
354
+ rmSync(dest, { recursive: true, force: true });
355
+ throw new Error(`\u6A21\u677F\u7F3A\u5C11 manifest.json\uFF08\u6A21\u677F\u76EE\u5F55\uFF1A${templateDir}\uFF09`);
356
+ }
357
+ const generatedManifest = JSON.parse(readFileSync2(manifestPath, "utf8"));
358
+ const { ok, errors } = validatePluginManifest(generatedManifest);
359
+ if (!ok) {
360
+ rmSync(dest, { recursive: true, force: true });
361
+ throw new Error(`\u751F\u6210\u7684 manifest \u672A\u901A\u8FC7\u6821\u9A8C\uFF08\u6A21\u677F\u4E0E schema \u4E0D\u540C\u6B65\uFF09\uFF1A
362
+ - ${errors.join("\n - ")}`);
363
+ }
364
+ return dest;
365
+ }
366
+
367
+ // src/registry.ts
368
+ function buildRegistryEntry(manifest, fileName, sha256, sizeBytes) {
369
+ return {
370
+ id: manifest.id,
371
+ name: manifest.name,
372
+ version: manifest.version,
373
+ description: manifest.description,
374
+ kind: manifest.kind,
375
+ stationTypes: manifest.stationTypes,
376
+ models: manifest.models ?? [],
377
+ agentApi: manifest.agentApi,
378
+ permissions: manifest.permissions,
379
+ autoStart: manifest.autoStart,
380
+ dependencies: manifest.dependencies,
381
+ fileName,
382
+ sha256,
383
+ sizeBytes
384
+ };
385
+ }
386
+
387
+ // src/smoke.mjs
388
+ import assert from "assert/strict";
389
+ import { readFileSync as readFileSync3 } from "fs";
390
+ import { join as join3 } from "path";
391
+ import { pathToFileURL } from "url";
392
+ import { createAPIClient } from "@international-iot-association/api-bridge";
393
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
394
+ function isRetiredGlobalLoggerCompatibilityShell(manifest) {
395
+ return manifest.id === "rti.global.logger" && manifest.kind === "global" && manifest.autoStart === false && Array.isArray(manifest.permissions) && manifest.permissions.length === 0;
396
+ }
397
+ function assertSmokeMinimums(manifest, counters, activated) {
398
+ assert.ok(activated, `${manifest.id} smoke \u672A\u8C03\u7528 toolkit.activate()`);
399
+ assert.ok(
400
+ counters.roundtrips >= 1 || isRetiredGlobalLoggerCompatibilityShell(manifest),
401
+ `${manifest.id} smoke \u672A\u5B8C\u6210\u4EFB\u4F55 API \u5F80\u8FD4`
402
+ );
403
+ const kind = manifest.kind ?? "station";
404
+ if (kind === "station") {
405
+ assert.ok(counters.reports >= 1, `${manifest.id} smoke \u672A\u4EA7\u751F\u4EFB\u4F55 ctx.report.result`);
406
+ }
407
+ }
408
+ function hasChannelPermission(permissions, logicalName) {
409
+ return permissions.includes(logicalName) || permissions.includes(`${logicalName}.writeRead`) || permissions.includes(`channel.${logicalName}.writeRead`);
410
+ }
411
+ function hasChannelControlPermission(permissions, logicalName) {
412
+ return permissions.includes(`${logicalName}.control`) || permissions.includes(`channel.${logicalName}.control`);
413
+ }
414
+ function hasSerialDiscoveryPermission(permissions) {
415
+ return permissions.some(
416
+ (perm) => perm === "serial" || perm === "serial.discovery" || perm === "channel.serial.discovery" || perm === "channel.serial.discovery.read"
417
+ );
418
+ }
419
+ function makeMemoryStorage() {
420
+ const files = /* @__PURE__ */ new Map();
421
+ const normalize = (p) => p.replace(/^\.\//, "");
422
+ return {
423
+ dir: "/mem",
424
+ async read(p) {
425
+ const key = normalize(p);
426
+ return files.has(key) ? files.get(key) : null;
427
+ },
428
+ async write(p, c) {
429
+ files.set(normalize(p), c);
430
+ },
431
+ async append(p, c) {
432
+ const key = normalize(p);
433
+ files.set(key, (files.get(key) ?? "") + c);
434
+ },
435
+ async list(dir = ".") {
436
+ const prefix = dir === "." ? "" : `${dir.replace(/\/+$/, "")}/`;
437
+ const names = [];
438
+ for (const key of files.keys()) {
439
+ if (!key.startsWith(prefix)) continue;
440
+ const rest = key.slice(prefix.length);
441
+ if (rest && !rest.includes("/")) names.push(rest);
442
+ }
443
+ return names;
444
+ },
445
+ async remove(p) {
446
+ files.delete(normalize(p));
447
+ },
448
+ _files: files
449
+ };
450
+ }
451
+ function makeBusRouter() {
452
+ const nodes = /* @__PURE__ */ new Map();
453
+ return {
454
+ register(pluginId) {
455
+ const handlers = [];
456
+ nodes.set(pluginId, handlers);
457
+ return {
458
+ postMessage(to, envelope) {
459
+ const target = nodes.get(to);
460
+ if (!target) return;
461
+ queueMicrotask(() => {
462
+ for (const h of target) h(pluginId, envelope);
463
+ });
464
+ },
465
+ onMessage(handler) {
466
+ handlers.push(handler);
467
+ }
468
+ };
469
+ }
470
+ };
471
+ }
472
+ function makeBusConsumer(router, consumerId, providerId, counters) {
473
+ const endpoint = router.register(consumerId);
474
+ const { client, clientMessageHandler, serverEventHandler } = createAPIClient({
475
+ requestServerFunc: async (req) => {
476
+ endpoint.postMessage(providerId, req);
477
+ }
478
+ });
479
+ endpoint.onMessage((_from, envelope) => {
480
+ if (envelope.type === "response") {
481
+ counters?.roundtrips !== void 0 && (counters.roundtrips += 1);
482
+ clientMessageHandler(envelope);
483
+ } else if (envelope.type === "event") {
484
+ serverEventHandler(envelope);
485
+ }
486
+ });
487
+ return client;
488
+ }
489
+ function makeGatedHarness(manifest, opts = {}, counters = { roundtrips: 0 }) {
490
+ const permissions = manifest.permissions ?? [];
491
+ const events = [];
492
+ const results = [];
493
+ const subscriptions = /* @__PURE__ */ new Map();
494
+ let runtimeOnMessage = null;
495
+ const deny = (message) => {
496
+ const err = new Error(message);
497
+ counters.permissionDenials = (counters.permissionDenials ?? 0) + 1;
498
+ throw err;
499
+ };
500
+ const defaultControl = async (request) => {
501
+ if (request.action === "configure") {
502
+ return {
503
+ logicalName: request.config.logicalName,
504
+ provider: request.config.provider,
505
+ state: "closed",
506
+ metrics: {}
507
+ };
508
+ }
509
+ if (request.action === "open") {
510
+ return { logicalName: request.logicalName, provider: "mock", state: "open", metrics: {} };
511
+ }
512
+ if (request.action === "close") {
513
+ return { logicalName: request.logicalName, provider: "mock", state: "closed", metrics: {} };
514
+ }
515
+ if (request.action === "status") {
516
+ return { logicalName: request.logicalName, provider: "mock", state: "open", metrics: {} };
517
+ }
518
+ if (request.action === "listPorts" || request.action === "detectDevices") return [];
519
+ throw new Error(`unexpected control action: ${request.action}`);
520
+ };
521
+ const ctx = {
522
+ logger: { info() {
523
+ }, warn() {
524
+ }, error() {
525
+ }, debug() {
526
+ } },
527
+ channels: {
528
+ async writeReadMock(logicalName, payload) {
529
+ return {
530
+ logicalName,
531
+ request: payload,
532
+ response: `MOCK<${logicalName}>:${payload}:OK`,
533
+ latencyMs: 1
534
+ };
535
+ },
536
+ async writeRead(logicalName, payload, options) {
537
+ if (!hasChannelPermission(permissions, logicalName)) {
538
+ deny(
539
+ `permission denied: plugin "${manifest.id}" has no permission for channel "${logicalName}"`
540
+ );
541
+ }
542
+ if (!opts.writeRead) throw new Error("no device configured");
543
+ return opts.writeRead(logicalName, payload, options);
544
+ },
545
+ async control(request) {
546
+ if (request.action === "listPorts" || request.action === "detectDevices") {
547
+ if (!hasSerialDiscoveryPermission(permissions)) {
548
+ deny(`plugin "${manifest.id}" has no serial discovery permission for ${request.action}`);
549
+ }
550
+ } else {
551
+ const logicalName = request.action === "configure" ? request.config.logicalName : request.logicalName;
552
+ if (!hasChannelControlPermission(permissions, logicalName)) {
553
+ deny(`plugin ${manifest.id} does not have permission for channel ${logicalName}`);
554
+ }
555
+ }
556
+ return (opts.controlOverride ?? defaultControl)(request);
557
+ },
558
+ subscribe(logicalName, handler) {
559
+ if (!hasChannelPermission(permissions, logicalName)) {
560
+ deny(
561
+ `permission denied: plugin "${manifest.id}" has no permission for channel "${logicalName}"`
562
+ );
563
+ }
564
+ const list = subscriptions.get(logicalName) ?? [];
565
+ list.push(handler);
566
+ subscriptions.set(logicalName, list);
567
+ return () => {
568
+ const current = subscriptions.get(logicalName);
569
+ if (!current) return;
570
+ const idx = current.indexOf(handler);
571
+ if (idx >= 0) current.splice(idx, 1);
572
+ };
573
+ }
574
+ },
575
+ report: {
576
+ stepEvent: (e) => events.push(e),
577
+ result: (r) => {
578
+ counters.reports = (counters.reports ?? 0) + 1;
579
+ results.push(r);
580
+ }
581
+ },
582
+ ui: {
583
+ postMessage: (env) => {
584
+ if (env.type === "response") {
585
+ counters.roundtrips += 1;
586
+ clientMessageHandler(env);
587
+ } else if (env.type === "event") {
588
+ serverEventHandler(env);
589
+ }
590
+ },
591
+ onMessage: (h) => {
592
+ runtimeOnMessage = h;
593
+ }
594
+ },
595
+ bus: opts.bus ?? { postMessage() {
596
+ }, onMessage() {
597
+ } }
598
+ };
599
+ if (permissions.includes("storage")) {
600
+ ctx.storage = opts.storage ?? makeMemoryStorage();
601
+ }
602
+ if (permissions.includes("ui:file-dialog")) {
603
+ ctx.dialog = opts.dialog ?? {
604
+ async openFile() {
605
+ return { canceled: true, files: [] };
606
+ }
607
+ };
608
+ }
609
+ if (permissions.includes("platform:object-upload")) {
610
+ ctx.platform = {
611
+ ...ctx.platform ?? {},
612
+ upload: opts.upload ?? {
613
+ async submit() {
614
+ return {
615
+ ok: false,
616
+ code: "internal",
617
+ faultCode: "UP-SMOKE-STUB",
618
+ retryable: false,
619
+ detail: "smoke \u7F3A\u7701\u4E0A\u4F20\u6869\uFF1A\u672A\u6CE8\u5165 opts.upload",
620
+ attempts: 1,
621
+ uploadedBeforeFailure: 0
622
+ };
623
+ }
624
+ }
625
+ };
626
+ }
627
+ if (permissions.includes("devtools:virtual-serial")) {
628
+ const denyVirtualSerial = async () => {
629
+ throw new Error(
630
+ "\u865A\u62DF\u4E32\u53E3\u80FD\u529B\u672A\u542F\u7528\uFF1Asmoke \u7F3A\u7701\u6869\u4E0D\u94F8\u9020 PTY\uFF08\u8981\u6D4B\u6210\u529F\u8DEF\u5F84\u8BF7\u6CE8\u5165 opts.virtualSerial\uFF09"
631
+ );
632
+ };
633
+ ctx.platform = {
634
+ ...ctx.platform ?? {},
635
+ virtualSerial: opts.virtualSerial ?? {
636
+ create: denyVirtualSerial,
637
+ release: denyVirtualSerial,
638
+ list: denyVirtualSerial
639
+ }
640
+ };
641
+ }
642
+ const { client, clientMessageHandler, serverEventHandler } = createAPIClient({
643
+ requestServerFunc: async (req) => {
644
+ runtimeOnMessage?.(req);
645
+ }
646
+ });
647
+ const emitChannelEvent = (logicalName, event) => {
648
+ for (const handler of subscriptions.get(logicalName) ?? []) handler(event);
649
+ };
650
+ return { ctx, client, events, results, subscriptions, emitChannelEvent, storage: ctx.storage };
651
+ }
652
+ async function runPluginSmoke(pluginDir) {
653
+ const manifestRaw = readFileSync3(join3(pluginDir, "manifest.json"), "utf8");
654
+ const manifest = JSON.parse(manifestRaw);
655
+ const { ok, errors } = validatePluginManifest(manifest);
656
+ if (!ok) throw new Error(`manifest \u6821\u9A8C\u5931\u8D25\uFF08${pluginDir}\uFF09\uFF1A${errors.join("; ")}`);
657
+ const counters = { roundtrips: 0, reports: 0, permissionDenials: 0 };
658
+ let activated = false;
659
+ const toolkit = {
660
+ assert,
661
+ sleep,
662
+ manifest,
663
+ pluginDir,
664
+ counters,
665
+ makeBusRouter,
666
+ makeMemoryStorage,
667
+ makeBusConsumer: (router, consumerId, providerId) => makeBusConsumer(router, consumerId, providerId, counters),
668
+ createHarness: (opts = {}) => makeGatedHarness(manifest, opts, counters),
669
+ /**
670
+ * 激活已构建 runtime 并登记 smoke 最小契约。
671
+ * override 只供需要测试专用 assembly 的 smoke 使用;默认仍加载插件导出的生产 activate。
672
+ */
673
+ async activate(ctx, override) {
674
+ let runtimeActivate = override;
675
+ if (runtimeActivate === void 0) {
676
+ const entry = pathToFileURL(join3(pluginDir, "runtime-dist", "main.mjs")).href;
677
+ const mod = await import(entry);
678
+ runtimeActivate = mod.activate;
679
+ }
680
+ if (typeof runtimeActivate !== "function") {
681
+ throw new Error(`runtime-dist/main.mjs \u672A\u63D0\u4F9B\u53EF\u8C03\u7528\u7684 activate()\uFF08${manifest.id}\uFF09`);
682
+ }
683
+ const handle = await runtimeActivate(ctx);
684
+ activated = true;
685
+ return handle;
686
+ }
687
+ };
688
+ const smokeUrl = pathToFileURL(join3(pluginDir, "smoke.mjs")).href;
689
+ const smokeMod = await import(smokeUrl);
690
+ if (typeof smokeMod.default !== "function") {
691
+ throw new Error(`smoke.mjs \u5FC5\u987B\u9ED8\u8BA4\u5BFC\u51FA async \u51FD\u6570\uFF08${manifest.id}\uFF09`);
692
+ }
693
+ await smokeMod.default(toolkit);
694
+ assertSmokeMinimums(manifest, counters, activated);
695
+ return { pluginId: manifest.id, roundtrips: counters.roundtrips, reports: counters.reports };
696
+ }
697
+
698
+ // src/stage.ts
699
+ import { cpSync as cpSync2, existsSync as existsSync3, mkdirSync, readFileSync as readFileSync4, rmSync as rmSync2 } from "fs";
700
+ import { join as join4 } from "path";
701
+ import { assertPluginUiSingleReact as assertPluginUiSingleReact2 } from "@international-iot-association/plugin-vite-config";
702
+ function assertExists(path, label) {
703
+ if (!existsSync3(path)) throw new Error(`${label} not found: ${path}`);
704
+ }
705
+ function readPluginManifest(pluginDir) {
706
+ const manifestPath = join4(pluginDir, "manifest.json");
707
+ if (!existsSync3(manifestPath)) {
708
+ throw new Error(`missing manifest.json in ${pluginDir}`);
709
+ }
710
+ const manifest = JSON.parse(readFileSync4(manifestPath, "utf8"));
711
+ const { ok, errors } = validatePluginManifest(manifest);
712
+ if (!ok) {
713
+ throw new Error(`manifest \u6821\u9A8C\u5931\u8D25\uFF08${manifestPath}\uFF09\uFF1A
714
+ - ${errors.join("\n - ")}`);
715
+ }
716
+ return manifest;
717
+ }
718
+ function stagePlugin(pluginDir, stagingDir) {
719
+ const manifest = readPluginManifest(pluginDir);
720
+ rmSync2(stagingDir, { recursive: true, force: true });
721
+ mkdirSync(stagingDir, { recursive: true });
722
+ cpSync2(join4(pluginDir, "manifest.json"), join4(stagingDir, "manifest.json"));
723
+ const schemas = join4(pluginDir, "schemas");
724
+ if (existsSync3(schemas)) cpSync2(schemas, join4(stagingDir, "schemas"), { recursive: true });
725
+ if (manifest.uiEntry) {
726
+ const uiDist = join4(pluginDir, "ui-dist");
727
+ assertExists(uiDist, `ui build output (run "pnpm --filter ${manifest.id} build")`);
728
+ const reactGate = assertPluginUiSingleReact2(pluginDir);
729
+ if (!reactGate.ok) {
730
+ throw new Error(reactGate.error);
731
+ }
732
+ cpSync2(uiDist, join4(stagingDir, "ui"), { recursive: true });
733
+ }
734
+ const runtimeDist = join4(pluginDir, "runtime-dist");
735
+ assertExists(runtimeDist, `runtime build output (run "pnpm --filter ${manifest.id} build")`);
736
+ cpSync2(runtimeDist, join4(stagingDir, "runtime"), { recursive: true });
737
+ if (manifest.uiEntry) assertExists(join4(stagingDir, manifest.uiEntry), `uiEntry "${manifest.uiEntry}"`);
738
+ assertExists(join4(stagingDir, manifest.runtimeEntry), `runtimeEntry "${manifest.runtimeEntry}"`);
739
+ return manifest;
740
+ }
741
+
742
+ // src/test-runner.mjs
743
+ import { spawnSync } from "child_process";
744
+ function parseTapTestCount(output) {
745
+ const matches = [...output.matchAll(/^# tests (\d+)$/gm)];
746
+ const last = matches.at(-1);
747
+ return last ? Number(last[1]) : null;
748
+ }
749
+ function parseTapFailureCount(output) {
750
+ const fail = [...output.matchAll(/^# fail (\d+)$/gm)].at(-1);
751
+ if (!fail) return null;
752
+ const cancelled = [...output.matchAll(/^# cancelled (\d+)$/gm)].at(-1);
753
+ return Number(fail[1]) + (cancelled ? Number(cancelled[1]) : 0);
754
+ }
755
+ function runPluginTestSuite(pluginDir) {
756
+ const result = spawnSync("pnpm", ["test"], {
757
+ cwd: pluginDir,
758
+ encoding: "utf8",
759
+ // Windows 上 pnpm 是 pnpm.cmd,不带 shell 直接 spawn 会 ENOENT;
760
+ // 与仓内 tools/verify.mjs 的既定写法(run()/runCaptured())保持一致。
761
+ shell: process.platform === "win32",
762
+ env: {
763
+ ...process.env,
764
+ NODE_OPTIONS: `${process.env.NODE_OPTIONS ?? ""} --test-reporter=tap`.trim()
765
+ }
766
+ });
767
+ if (result.error) {
768
+ return {
769
+ ok: false,
770
+ testCount: null,
771
+ output: `${result.stdout ?? ""}${result.stderr ?? ""}`,
772
+ error: `\u65E0\u6CD5\u542F\u52A8\u6D4B\u8BD5\u8FDB\u7A0B\uFF1A${result.error.message}`
773
+ };
774
+ }
775
+ const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
776
+ const testCount = parseTapTestCount(output);
777
+ if (testCount === null) {
778
+ return {
779
+ ok: false,
780
+ testCount: null,
781
+ output,
782
+ error: "\u6D4B\u8BD5\u8F93\u51FA\u4E2D\u6CA1\u6709 node --test TAP \u6C47\u603B\uFF08\u811A\u672C\u6CA1\u6709\u771F\u6B63\u8DD1\u6D4B\u8BD5\uFF0C\u6216\u672A\u4F7F\u7528 node --test\uFF09"
783
+ };
784
+ }
785
+ if (testCount === 0) {
786
+ return { ok: false, testCount, output, error: "0 \u4E2A\u6D4B\u8BD5\u7528\u4F8B" };
787
+ }
788
+ const failCount = parseTapFailureCount(output);
789
+ if (failCount !== null && failCount > 0) {
790
+ return { ok: false, testCount, output, error: `TAP \u6C47\u603B\u6709 ${failCount} \u4E2A\u5931\u8D25/\u53D6\u6D88\u7528\u4F8B` };
791
+ }
792
+ if (result.status !== 0) {
793
+ return { ok: false, testCount, output, error: `\u6D4B\u8BD5\u8FDB\u7A0B\u9000\u51FA\u7801 ${result.status}` };
794
+ }
795
+ return { ok: true, testCount, output };
796
+ }
797
+
798
+ // src/zip.ts
799
+ import { createHash } from "crypto";
800
+ import { readFileSync as readFileSync5, statSync as statSync2 } from "fs";
801
+ import AdmZip from "adm-zip";
802
+ function zipStaging(stagingDir, outZip) {
803
+ const zip = new AdmZip();
804
+ zip.addLocalFolder(stagingDir, "", (entryName) => !entryName.includes(".DS_Store"));
805
+ zip.writeZip(outZip);
806
+ const bytes = readFileSync5(outZip);
807
+ const sha256 = createHash("sha256").update(bytes).digest("hex");
808
+ const sizeBytes = statSync2(outZip).size;
809
+ return { zipPath: outZip, sha256, sizeBytes };
810
+ }
811
+
812
+ // src/cli.ts
813
+ var USAGE_LINES = [
814
+ "\u7528\u6CD5\uFF1Aplugin-cli check <\u63D2\u4EF6\u76EE\u5F55>",
815
+ " plugin-cli pack <\u63D2\u4EF6\u76EE\u5F55> --out <\u8F93\u51FA\u76EE\u5F55>",
816
+ ' plugin-cli new <\u76EE\u5F55\u540D> "<\u663E\u793A\u540D>" <\u63D2\u4EF6id> [--template <\u76EE\u5F55\u6216.tgz>] [--template-version <\u7248\u672C\u6216dist-tag>] [--out <\u7236\u76EE\u5F55>]',
817
+ " plugin-cli smoke <\u63D2\u4EF6\u76EE\u5F55>",
818
+ " plugin-cli --help"
819
+ ];
820
+ function printUsage(stream) {
821
+ const write = stream === "stdout" ? console.log : console.error;
822
+ for (const line of USAGE_LINES) write(line);
823
+ }
824
+ function help() {
825
+ printUsage("stdout");
826
+ process.exit(0);
827
+ }
828
+ function usage() {
829
+ printUsage("stderr");
830
+ process.exit(1);
831
+ }
832
+ function parseArgs(argv) {
833
+ const positional = [];
834
+ let out;
835
+ for (let i = 0; i < argv.length; i += 1) {
836
+ if (argv[i] === "--out") {
837
+ out = argv[i + 1];
838
+ i += 1;
839
+ } else {
840
+ positional.push(argv[i]);
841
+ }
842
+ }
843
+ const pluginDir = positional[0];
844
+ if (!pluginDir) usage();
845
+ return { pluginDir: resolve2(pluginDir), out };
846
+ }
847
+ function runCheck(argv) {
848
+ const { pluginDir } = parseArgs(argv);
849
+ const result = checkPlugin(pluginDir);
850
+ for (const e of result.errors) console.error(`- ${e}`);
851
+ const testResult = runPluginTestSuite(pluginDir);
852
+ if (!testResult.ok) console.error(`- \u63D2\u4EF6\u81EA\u8EAB\u6D4B\u8BD5\u672A\u901A\u8FC7\uFF1A${testResult.error}`);
853
+ const ok = result.ok && testResult.ok;
854
+ console.log(ok ? `check ok: ${pluginDir}` : `check failed: ${pluginDir}`);
855
+ console.log(
856
+ "\u6CE8\u610F\uFF1A\u672C\u547D\u4EE4\u4E0D\u542B\u8DE8\u63D2\u4EF6 id \u5BF9\u8D26\u3001registry \u5BF9\u8D26\u3001\u5168\u4ED3 secret-scan\uFF08\u8FD9\u4E9B\u53EA\u5728\u4ED3\u5185 ./dev verify \u505A\uFF09\u3002"
857
+ );
858
+ if (!ok) process.exitCode = 1;
859
+ }
860
+ function runPack(argv) {
861
+ const { pluginDir, out } = parseArgs(argv);
862
+ if (!out) usage();
863
+ const outDir = resolve2(out);
864
+ mkdirSync2(outDir, { recursive: true });
865
+ const stagingDir = join5(outDir, ".staging", basename(pluginDir));
866
+ const manifest = stagePlugin(pluginDir, stagingDir);
867
+ const fileName = `${manifest.id}-${manifest.version}.zip`;
868
+ const zipPath = join5(outDir, fileName);
869
+ const { sha256, sizeBytes } = zipStaging(stagingDir, zipPath);
870
+ const entry = buildRegistryEntry(manifest, fileName, sha256, sizeBytes);
871
+ const registry = { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), plugins: [entry] };
872
+ const registryPath = join5(outDir, `${manifest.id}-registry.json`);
873
+ writeFileSync2(registryPath, JSON.stringify(registry, null, 2));
874
+ console.log(`packed ${fileName} sha256=${sha256.slice(0, 16)}\u2026 ${sizeBytes}B (unsigned)`);
875
+ console.log(`wrote ${registryPath}`);
876
+ }
877
+ function parseNewArgs(argv) {
878
+ const positional = [];
879
+ let template;
880
+ let templateVersion;
881
+ let out;
882
+ for (let i = 0; i < argv.length; i += 1) {
883
+ if (argv[i] === "--template") {
884
+ template = argv[i + 1];
885
+ i += 1;
886
+ } else if (argv[i] === "--template-version") {
887
+ templateVersion = argv[i + 1];
888
+ i += 1;
889
+ } else if (argv[i] === "--out") {
890
+ out = argv[i + 1];
891
+ i += 1;
892
+ } else {
893
+ positional.push(argv[i]);
894
+ }
895
+ }
896
+ const [dirName, displayName, id] = positional;
897
+ if (!dirName || !displayName || !id) usage();
898
+ return { dirName, displayName, id, template, templateVersion, out };
899
+ }
900
+ function runNew(argv) {
901
+ const { dirName, displayName, id, template, templateVersion, out } = parseNewArgs(argv);
902
+ const outParentDir = out ? resolve2(out) : join5(process.cwd(), "plugins");
903
+ try {
904
+ const dest = scaffoldPlugin({ dirName, displayName, id, template, templateVersion, outParentDir });
905
+ console.log(`\u2713 \u5DF2\u751F\u6210 ${dest} (id=${id}, name=${displayName})`);
906
+ console.log("\u4E0B\u4E00\u6B65\uFF1A");
907
+ console.log(` cd ${dest}`);
908
+ console.log(" pnpm install");
909
+ console.log(" pnpm run build && pnpm run pack # \u4ED3\u5185 monorepo \u5219\u6539\u7528 pnpm --filter <id> build");
910
+ console.log(dest);
911
+ } catch (err) {
912
+ console.error(err.message);
913
+ process.exit(1);
914
+ }
915
+ }
916
+ async function runSmoke(argv) {
917
+ const { pluginDir } = parseArgs(argv);
918
+ let smokeResult;
919
+ try {
920
+ smokeResult = await runPluginSmoke(pluginDir);
921
+ } catch (err) {
922
+ console.error(err.message);
923
+ process.exitCode = 1;
924
+ return;
925
+ }
926
+ const testResult = runPluginTestSuite(pluginDir);
927
+ if (!testResult.ok) {
928
+ console.error(`- \u63D2\u4EF6\u81EA\u8EAB\u6D4B\u8BD5\u672A\u901A\u8FC7\uFF1A${testResult.error}`);
929
+ console.log(`smoke failed: ${pluginDir}`);
930
+ process.exitCode = 1;
931
+ return;
932
+ }
933
+ console.log(
934
+ `smoke ok: ${pluginDir} roundtrips=${smokeResult.roundtrips} reports=${smokeResult.reports} tests=${testResult.testCount}`
935
+ );
936
+ }
937
+ async function main() {
938
+ const [sub, ...rest] = process.argv.slice(2);
939
+ switch (sub) {
940
+ case "check":
941
+ runCheck(rest);
942
+ break;
943
+ case "pack":
944
+ runPack(rest);
945
+ break;
946
+ case "new":
947
+ runNew(rest);
948
+ break;
949
+ case "smoke":
950
+ await runSmoke(rest);
951
+ break;
952
+ case "--help":
953
+ case "-h":
954
+ case "help":
955
+ help();
956
+ // eslint-disable-next-line no-fallthrough -- help() 的返回类型是 never,不会落到下一分支
957
+ default:
958
+ usage();
959
+ }
960
+ }
961
+ main();