@tomflow/proflow-platform-cli 0.1.33 → 0.1.34

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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @tomflow/proflow-platform-cli
2
2
 
3
+ ## 0.1.34
4
+
5
+ ### Patch Changes
6
+
7
+ - Upgrade the ProFlow terminal experience, stream Registry and package-manager progress, publish structured Chinese Module documentation, and provide numbered package-owned Setup assistants.
8
+
3
9
  ## 0.1.33
4
10
 
5
11
  ### Patch Changes
package/DOCS.md CHANGED
@@ -1,11 +1,39 @@
1
- # platform-cli
1
+ # platform-cli — ProFlow 管理命令行
2
2
 
3
- Platform CLI(平台命令行)是 ProFlow 的统一管理入口,负责发现模块、安装依赖、汇总状态和按依赖顺序启动或停止模块。
3
+ ## 模块定位与作用
4
4
 
5
- ## 推荐流程
5
+ ProFlow 的薄管理面,负责从 Registry 发现 Module、同步 npm 包、解析依赖顺序,并转发和聚合七标准命令。
6
6
 
7
- `platform install` → `platform status` → `platform docs` → `platform setup` → `platform start`
7
+ ## 主要能力
8
8
 
9
- ## 边界
9
+ - 安装、卸载并验证完整 Module 包集合。
10
+ - 聚合 status、docs 和 setup,不解释 Module 私有业务。
11
+ - 按依赖顺序启动、按逆依赖顺序停止,并支持幂等重试。
10
12
 
11
- Platform CLI 只负责调用和汇总,不读取模块私有配置,也不替模块解释外部服务。具体配置由对应模块的 Setup(配置向导)完成。
13
+ ## 提供的 API 与 Public Contract
14
+
15
+ - 不提供领域业务 Contract。
16
+ - 提供 `platform` CLI;`runCli()` 返回强类型 `CliOutcome`,终端层负责中文渲染。
17
+
18
+ ## 依赖的 Module、Contract 和外部资源
19
+
20
+ - 依赖 npm Registry、当前 Workspace package manager 和所有 discovered Module 的公开 adapter。
21
+ - 不 deep import Module 私有代码,不读取 Module 私有配置。
22
+
23
+ ## 运行形态与生命周期
24
+
25
+ 类型为 CLI,命令执行结束后退出;不是业务 Runtime 或长期守护进程。
26
+
27
+ ## 使用方式
28
+
29
+ 推荐顺序为 `platform install`、`status`、`docs`、`setup`、`start`、`stop`、`uninstall`。
30
+
31
+ ## 职责边界与限制
32
+
33
+ 不代理业务 API、不保存 Module setup 输入、不建立 preflight/doctor 第二真源,也不提供 JSON 输出选项。
34
+
35
+ ## 术语
36
+
37
+ - Discovery(发现):从 Registry 与 Workspace 识别 governed Module。
38
+ - Aggregation(聚合):保留各 Module 结果并以统一终端形式展示。
39
+ - Dependency Order(依赖顺序):Provider 先于 Consumer 的生命周期顺序。
@@ -6,7 +6,7 @@ export declare const behaviorAdapter: {
6
6
  readonly ok: true;
7
7
  readonly status: "SUCCEEDED";
8
8
  readonly moduleRef: "platform-cli";
9
- readonly moduleVersion: "0.1.33";
9
+ readonly moduleVersion: "0.1.34";
10
10
  };
11
11
  observedEffects: never[];
12
12
  }>;
@@ -16,7 +16,7 @@ export declare const behaviorAdapter: {
16
16
  readonly ok: true;
17
17
  readonly status: "SUCCEEDED";
18
18
  readonly moduleRef: "platform-cli";
19
- readonly moduleVersion: "0.1.33";
19
+ readonly moduleVersion: "0.1.34";
20
20
  };
21
21
  observedEffects: never[];
22
22
  }>;
@@ -26,7 +26,7 @@ export declare const behaviorAdapter: {
26
26
  ok: true;
27
27
  status: "SUCCEEDED";
28
28
  moduleRef: "platform-cli";
29
- moduleVersion: "0.1.33";
29
+ moduleVersion: "0.1.34";
30
30
  data: {
31
31
  readonly setupStatus: "READY";
32
32
  readonly runtimeStatus: "NOT_APPLICABLE";
@@ -40,7 +40,7 @@ export declare const behaviorAdapter: {
40
40
  readonly ok: true;
41
41
  readonly status: "SUCCEEDED";
42
42
  readonly moduleRef: "platform-cli";
43
- readonly moduleVersion: "0.1.33";
43
+ readonly moduleVersion: "0.1.34";
44
44
  };
45
45
  observedEffects: never[];
46
46
  }>;
@@ -50,7 +50,7 @@ export declare const behaviorAdapter: {
50
50
  ok: true;
51
51
  status: "SUCCEEDED";
52
52
  moduleRef: "platform-cli";
53
- moduleVersion: "0.1.33";
53
+ moduleVersion: "0.1.34";
54
54
  data: {
55
55
  docs: string;
56
56
  };
@@ -63,7 +63,7 @@ export declare const behaviorAdapter: {
63
63
  readonly ok: true;
64
64
  readonly status: "SUCCEEDED";
65
65
  readonly moduleRef: "platform-cli";
66
- readonly moduleVersion: "0.1.33";
66
+ readonly moduleVersion: "0.1.34";
67
67
  };
68
68
  observedEffects: never[];
69
69
  }>;
@@ -73,7 +73,7 @@ export declare const behaviorAdapter: {
73
73
  readonly ok: true;
74
74
  readonly status: "SUCCEEDED";
75
75
  readonly moduleRef: "platform-cli";
76
- readonly moduleVersion: "0.1.33";
76
+ readonly moduleVersion: "0.1.34";
77
77
  };
78
78
  observedEffects: never[];
79
79
  }>;
@@ -3,7 +3,7 @@ export declare const descriptor: {
3
3
  readonly contractVersion: "1.0.0";
4
4
  readonly moduleRef: "platform-cli";
5
5
  readonly packageName: "@tomflow/proflow-platform-cli";
6
- readonly moduleVersion: "0.1.33";
6
+ readonly moduleVersion: "0.1.34";
7
7
  readonly kind: "cli";
8
8
  readonly templateVersion: "1.0.0";
9
9
  readonly platformCompatibility: ">=1.0.0 <2.0.0";
@@ -3,7 +3,7 @@ export const descriptor = {
3
3
  contractVersion: "1.0.0",
4
4
  moduleRef: "platform-cli",
5
5
  packageName: "@tomflow/proflow-platform-cli",
6
- moduleVersion: "0.1.33",
6
+ moduleVersion: "0.1.34",
7
7
  kind: "cli",
8
8
  templateVersion: "1.0.0",
9
9
  platformCompatibility: ">=1.0.0 <2.0.0",
package/dist/src/cli.d.ts CHANGED
@@ -21,4 +21,8 @@ export interface CliRuntimeOptions {
21
21
  onProgress?: PlatformProgressReporter;
22
22
  }
23
23
  export declare function runCli(argv: readonly string[], runtime?: CliRuntimeOptions): Promise<CliOutcome>;
24
- export declare function renderHumanResult(result: CliOutcome): string;
24
+ export interface HumanRenderOptions {
25
+ color?: boolean;
26
+ width?: number;
27
+ }
28
+ export declare function renderHumanResult(result: CliOutcome, options?: HumanRenderOptions): string;
package/dist/src/cli.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
2
3
  import { readFile, realpath, stat } from "node:fs/promises";
3
4
  import { resolve } from "node:path";
4
5
  import { moduleDocsDataSchema, moduleSetupPlanDataSchema, moduleStatusObservationSchema, } from "@tomflow/proflow-module-contract";
@@ -230,21 +231,50 @@ async function validateInstalledPackageSet(root, candidates, previousManaged) {
230
231
  }
231
232
  }
232
233
  async function handleInstall(root, runtime) {
234
+ const startedAt = Date.now();
233
235
  reportProgress(runtime.onProgress, {
234
236
  command: "install",
235
237
  phase: "workspace",
238
+ kind: "phase",
236
239
  status: "STARTED",
237
240
  message: "正在校验 Workspace",
238
241
  });
239
242
  const metadata = await ensureWorkspaceMetadata(root);
243
+ reportProgress(runtime.onProgress, {
244
+ command: "install",
245
+ phase: "workspace",
246
+ kind: "phase",
247
+ status: "SUCCEEDED",
248
+ message: "Workspace 校验完成",
249
+ elapsedMs: Date.now() - startedAt,
250
+ });
240
251
  reportProgress(runtime.onProgress, {
241
252
  command: "install",
242
253
  phase: "registry",
254
+ kind: "phase",
243
255
  status: "STARTED",
244
- message: "正在发现 Registry 模块",
256
+ message: "正在连接 Registry",
245
257
  });
246
258
  const discovered = await discoverRegistryModules({
247
259
  workspaceRoot: root,
260
+ onSearchComplete: (total) => reportProgress(runtime.onProgress, {
261
+ command: "install",
262
+ phase: "registry",
263
+ kind: "detail",
264
+ status: "STARTED",
265
+ total,
266
+ message: `Registry 返回 ${total} 个候选模块,正在核验`,
267
+ }),
268
+ onPackageChecked: ({ current, total, packageName }) => reportProgress(runtime.onProgress, {
269
+ command: "install",
270
+ phase: "registry",
271
+ kind: "detail",
272
+ status: "SUCCEEDED",
273
+ current,
274
+ total,
275
+ moduleRef: packageName.replace(PRO_FLOW_PACKAGE_PREFIX, ""),
276
+ message: `核验 ${packageName.replace(PRO_FLOW_PACKAGE_PREFIX, "")}`,
277
+ }),
248
278
  ...(runtime.registryRunner === undefined
249
279
  ? {}
250
280
  : { runner: runtime.registryRunner }),
@@ -252,6 +282,7 @@ async function handleInstall(root, runtime) {
252
282
  reportProgress(runtime.onProgress, {
253
283
  command: "install",
254
284
  phase: "registry",
285
+ kind: "phase",
255
286
  status: "SUCCEEDED",
256
287
  message: `已发现 ${discovered.candidates.length} 个注册模块`,
257
288
  });
@@ -264,6 +295,7 @@ async function handleInstall(root, runtime) {
264
295
  reportProgress(runtime.onProgress, {
265
296
  command: "install",
266
297
  phase: "packages",
298
+ kind: "phase",
267
299
  status: "STARTED",
268
300
  message: "正在同步依赖",
269
301
  });
@@ -273,6 +305,7 @@ async function handleInstall(root, runtime) {
273
305
  packageName: item.packageName,
274
306
  version: item.moduleVersion,
275
307
  })),
308
+ onOutput: (line) => reportPackageManagerOutput(runtime, "install", line),
276
309
  ...(runtime.packageRunner === undefined
277
310
  ? {}
278
311
  : { runner: runtime.packageRunner }),
@@ -283,12 +316,20 @@ async function handleInstall(root, runtime) {
283
316
  reportProgress(runtime.onProgress, {
284
317
  command: "install",
285
318
  phase: "packages",
319
+ kind: "phase",
286
320
  status: "SUCCEEDED",
287
321
  message: "依赖同步完成",
288
322
  });
289
323
  await recordPnpmPolicyOwnership(root, pnpmPolicyBefore);
290
324
  await validateInstalledPackageSet(root, discovered.candidates, previousManaged);
291
325
  const { catalog, modules } = await buildContext(root);
326
+ reportProgress(runtime.onProgress, {
327
+ command: "install",
328
+ phase: "validation",
329
+ kind: "phase",
330
+ status: "SUCCEEDED",
331
+ message: `已验证 ${modules.length} 个已安装模块`,
332
+ });
292
333
  const moduleInstall = await installModulesThin(catalog, modules, root, runtime.onProgress);
293
334
  return outcome("install", batchStatus(moduleInstall), root, {
294
335
  registry: discovered.registry,
@@ -302,6 +343,18 @@ async function handleInstall(root, runtime) {
302
343
  next: "platform status",
303
344
  });
304
345
  }
346
+ const usefulPackageOutput = /(?:resolved|downloaded|reused|linked|added|removed|packages:|progress:|warn|warning|error|ERR_|done|completed|audited|changed)/i;
347
+ function reportPackageManagerOutput(runtime, command, output) {
348
+ if (!usefulPackageOutput.test(output.line))
349
+ return;
350
+ reportProgress(runtime.onProgress, {
351
+ command,
352
+ phase: "packages",
353
+ kind: "subprocess",
354
+ status: /(?:error|ERR_)/i.test(output.line) ? "FAILED" : "STARTED",
355
+ message: output.line,
356
+ });
357
+ }
305
358
  async function workspaceProFlowDependencies(root) {
306
359
  try {
307
360
  const parsed = JSON.parse(await readFile(resolve(root, "package.json"), "utf8"));
@@ -339,6 +392,7 @@ async function handleUninstall(root, runtime) {
339
392
  const mutation = await removeWorkspacePackages({
340
393
  workspaceRoot: root,
341
394
  packageNames,
395
+ onOutput: (line) => reportPackageManagerOutput(runtime, "uninstall", line),
342
396
  ...(runtime.packageRunner === undefined
343
397
  ? {}
344
398
  : { runner: runtime.packageRunner }),
@@ -447,7 +501,32 @@ function errorOutcome(command, error) {
447
501
  };
448
502
  }
449
503
  const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
450
- function renderStatus(data) {
504
+ const ansi = {
505
+ reset: "\u001b[0m",
506
+ bold: "\u001b[1m",
507
+ dim: "\u001b[2m",
508
+ cyan: "\u001b[36m",
509
+ blue: "\u001b[34m",
510
+ green: "\u001b[32m",
511
+ yellow: "\u001b[33m",
512
+ red: "\u001b[31m",
513
+ };
514
+ function humanTheme(options = {}) {
515
+ const enabled = options.color === true;
516
+ const paint = (tone, value) => enabled ? `${ansi[tone]}${value}${ansi.reset}` : value;
517
+ return {
518
+ width: Math.max(60, options.width ?? 100),
519
+ paint,
520
+ title: (value) => paint("bold", paint("cyan", value)),
521
+ section: (value) => paint("bold", value),
522
+ command: (value) => paint("blue", value),
523
+ muted: (value) => paint("dim", value),
524
+ success: (value) => paint("green", value),
525
+ warning: (value) => paint("yellow", value),
526
+ failure: (value) => paint("red", value),
527
+ };
528
+ }
529
+ function renderStatus(data, theme) {
451
530
  if (!isRecord(data) || !Array.isArray(data.modules))
452
531
  return "未发现模块。";
453
532
  const setupLabels = {
@@ -462,8 +541,15 @@ function renderStatus(data) {
462
541
  NOT_APPLICABLE: "无独立进程",
463
542
  };
464
543
  const modules = data.modules.filter(isRecord);
465
- const lines = ["ProFlow 状态", ""];
466
- for (const raw of modules) {
544
+ const unhealthy = modules.filter((item) => item.setupStatus !== "READY" || item.runtimeStatus === "FAILED");
545
+ const running = modules.filter((item) => item.setupStatus === "READY" && item.runtimeStatus === "RUNNING");
546
+ const healthy = modules.filter((item) => item.setupStatus === "READY" &&
547
+ item.runtimeStatus !== "RUNNING" &&
548
+ item.runtimeStatus !== "FAILED");
549
+ const lines = [theme.title("ProFlow 状态"), ""];
550
+ if (unhealthy.length > 0)
551
+ lines.push(theme.section("需要处理"), "");
552
+ for (const raw of unhealthy) {
467
553
  const moduleRef = String(raw.moduleRef);
468
554
  const setupStatus = String(raw.setupStatus);
469
555
  const runtimeStatus = String(raw.runtimeStatus);
@@ -478,43 +564,69 @@ function renderStatus(data) {
478
564
  : runtimeStatus === "NOT_APPLICABLE"
479
565
  ? "○"
480
566
  : "✓";
481
- lines.push(`${icon} ${moduleRef.padEnd(30)} ${String(raw.version).padEnd(9)} ${setupLabels[setupStatus] ?? "未知"} · ${runtimeLabels[runtimeStatus] ?? "未知"}`);
567
+ const tone = failed ? theme.failure : theme.warning;
568
+ lines.push(`${tone(icon)} ${theme.section(moduleRef)} ${theme.muted(String(raw.version))}`);
569
+ lines.push(` ${tone(setupLabels[setupStatus] ?? "未知")} · ${runtimeLabels[runtimeStatus] ?? "未知"}`);
482
570
  if (failed) {
483
- lines.push(` 原因:${setupStatus === "FAILED" ? "模块配置检查失败" : "模块运行状态异常"}`, ` 下一步:platform setup --module ${moduleRef}`);
571
+ lines.push(` 原因:${setupStatus === "FAILED" ? "模块配置检查失败" : "模块运行状态异常"}`, ` 下一步:${theme.command(`platform setup --module ${moduleRef}`)}`);
484
572
  }
485
573
  else if (actionRequired) {
486
- lines.push(" 原因:模块尚未完成配置", ` 下一步:platform setup --module ${moduleRef}`);
574
+ lines.push(" 原因:模块尚未完成配置", ` 下一步:${theme.command(`platform setup --module ${moduleRef}`)}`);
487
575
  }
576
+ lines.push("");
488
577
  }
578
+ const compact = (title, entries, symbol) => {
579
+ if (entries.length === 0)
580
+ return;
581
+ lines.push(theme.section(title), "");
582
+ for (const item of entries) {
583
+ const runtime = runtimeLabels[String(item.runtimeStatus)] ?? "未知";
584
+ lines.push(`${theme.success(symbol)} ${String(item.moduleRef).padEnd(32)} ${theme.muted(String(item.version).padEnd(9))} ${runtime}`);
585
+ }
586
+ lines.push("");
587
+ };
588
+ compact("运行中", running, "●");
589
+ compact("已就绪", healthy, "○");
489
590
  const ready = modules.filter((item) => item.setupStatus === "READY").length;
490
591
  const action = modules.filter((item) => item.setupStatus === "ACTION_REQUIRED").length;
491
592
  const failed = modules.filter((item) => item.setupStatus === "FAILED").length;
492
- lines.push("", `汇总:${ready} 已就绪 · ${action} 需要操作 · ${failed} 失败`);
593
+ lines.push(theme.section("汇总"), `${theme.success(`${ready} 已就绪`)} · ${theme.warning(`${action} 需要操作`)} · ${failed > 0 ? theme.failure(`${failed} 失败`) : `${failed} 失败`}`);
493
594
  return lines.join("\n");
494
595
  }
495
- function renderTerminalMarkdown(source) {
596
+ function renderTerminalMarkdown(source, theme) {
597
+ let inCode = false;
496
598
  return source
497
599
  .split(/\r?\n/)
498
600
  .map((line) => {
499
- const heading = line.match(/^#{1,6}\s+(.+)$/);
500
- if (heading?.[1])
501
- return heading[1].replaceAll("`", "");
502
- const bullet = line.match(/^\s*-\s+(.+)$/);
601
+ if (line.trim().startsWith("```")) {
602
+ inCode = !inCode;
603
+ return "";
604
+ }
605
+ if (inCode)
606
+ return ` ${theme.command(line)}`;
607
+ const heading = line.match(/^(#{1,6})\s+(.+)$/);
608
+ if (heading?.[2])
609
+ return heading[1]?.length === 1
610
+ ? theme.title(heading[2].replaceAll("`", ""))
611
+ : theme.section(heading[2].replaceAll("`", ""));
612
+ const bullet = line.match(/^\s*[-*]\s+(.+)$/);
503
613
  if (bullet?.[1])
504
- return ` ${bullet[1].replaceAll("`", "")}`;
505
- return line.replaceAll("`", "");
614
+ return ` ${theme.paint("cyan", "")} ${bullet[1]}`;
615
+ if (/^\|.*\|$/.test(line))
616
+ return theme.muted(line);
617
+ return line.replace(/`([^`]+)`/g, (_match, code) => theme.command(code));
506
618
  })
507
619
  .join("\n")
508
620
  .replace(/\n{3,}/g, "\n\n")
509
621
  .trim();
510
622
  }
511
- function renderDocs(data) {
623
+ function renderDocs(data, theme) {
512
624
  if (!isRecord(data) || !Array.isArray(data.modules))
513
625
  return "未发现模块文档。";
514
- const lines = ["ProFlow 模块文档"];
626
+ const lines = [theme.title("ProFlow 模块文档")];
515
627
  for (const raw of data.modules)
516
628
  if (isRecord(raw))
517
- lines.push("", "─".repeat(72), `${String(raw.moduleRef)} ${String(raw.version)}`, "", renderTerminalMarkdown(String(raw.docs ?? "")));
629
+ lines.push("", theme.muted("─".repeat(Math.min(theme.width, 96))), `${theme.section(String(raw.moduleRef))} ${theme.muted(String(raw.version))}`, "", renderTerminalMarkdown(String(raw.docs ?? ""), theme));
518
630
  if (Array.isArray(data.errors) && data.errors.length > 0)
519
631
  lines.push("", "文档读取失败:", ...data.errors
520
632
  .filter(isRecord)
@@ -555,10 +667,10 @@ const setupCommands = {
555
667
  inputs: "Custom GPT URL",
556
668
  },
557
669
  };
558
- function renderSetup(data) {
670
+ function renderSetup(data, theme) {
559
671
  if (!isRecord(data) || !Array.isArray(data.results))
560
672
  return "没有需要执行的配置步骤。";
561
- const lines = ["ProFlow 配置", ""];
673
+ const lines = [theme.title("ProFlow 配置"), ""];
562
674
  const skipped = Array.isArray(data.skipped) ? data.skipped : [];
563
675
  let ready = skipped.filter((item) => isRecord(item) && item.reason === "READY").length;
564
676
  let needsAction = 0;
@@ -576,19 +688,31 @@ function renderSetup(data) {
576
688
  blocked += 1;
577
689
  else
578
690
  needsAction += 1;
579
- lines.push(`${status === "FAILED" ? "✕" : "◆"} ${moduleRef}`);
691
+ lines.push(`${status === "FAILED" ? theme.failure("✕") : theme.warning("◆")} ${theme.section(moduleRef)}`);
580
692
  const plan = moduleSetupPlanDataSchema.safeParse(raw.result.data);
581
693
  if (plan.success) {
582
694
  for (const [index, step] of plan.data.steps.entries()) {
583
- lines.push(` ${index + 1}. ${step.title}`);
584
- lines.push(` 运行:${step.execution.interactive}`);
585
- lines.push(` 自动执行:${step.execution.nonInteractive}`);
695
+ const marker = step.state === "DONE"
696
+ ? theme.success("✓")
697
+ : step.state === "BLOCKED"
698
+ ? theme.failure("✕")
699
+ : index ===
700
+ plan.data.steps.findIndex((item) => item.state === "TODO")
701
+ ? theme.warning("→")
702
+ : theme.muted("○");
703
+ lines.push(` ${marker} ${String(index + 1).padStart(2, "0")} ${step.title}`);
704
+ if (step.description)
705
+ lines.push(` ${step.description}`);
706
+ lines.push(` 运行:${theme.command(step.execution.interactive)}`);
707
+ lines.push(` AI:${theme.command(step.execution.nonInteractive)}`);
586
708
  if (step.requiredInputs.length > 0)
587
- lines.push(` 需要:${step.requiredInputs.map((item) => item.description).join("、")}`);
588
- lines.push(` 验证:${step.verify}`);
589
- lines.push(` 完成:${step.successCondition}`);
709
+ lines.push(` 需要:${step.requiredInputs.map((item) => item.description).join("、")}`);
710
+ if (step.humanAction)
711
+ lines.push(` 人工操作:${step.humanAction}`);
712
+ lines.push(` 验证:${theme.command(step.verify)}`);
713
+ lines.push(` 完成:${step.successCondition}`);
590
714
  if (step.blockedReason)
591
- lines.push(` 原因:${step.blockedReason}`);
715
+ lines.push(` 原因:${theme.failure(step.blockedReason)}`);
592
716
  }
593
717
  lines.push("");
594
718
  continue;
@@ -619,34 +743,49 @@ function renderSetup(data) {
619
743
  }
620
744
  if (needsAction === 0 && blocked === 0)
621
745
  lines.push("全部模块均已就绪。");
622
- lines.push(`汇总:${ready} 个已就绪,${needsAction} 个需要操作,${blocked} 个系统阻塞`);
746
+ lines.push(theme.section("汇总"), `汇总:${theme.success(`${ready} 个已就绪`)},${theme.warning(`${needsAction} 个需要操作`)},${blocked > 0 ? theme.failure(`${blocked} 个系统阻塞`) : `${blocked} 个系统阻塞`}`);
623
747
  return lines.join("\n").trimEnd();
624
748
  }
625
- export function renderHumanResult(result) {
749
+ export function renderHumanResult(result, options = {}) {
750
+ const theme = humanTheme(options);
626
751
  if (result.command === "setup" &&
627
752
  isRecord(result.data) &&
628
753
  Array.isArray(result.data.results))
629
- return renderSetup(result.data);
754
+ return renderSetup(result.data, theme);
630
755
  if (result.command === "start" && isRecord(result.data)) {
631
756
  const blockers = Array.isArray(result.data.blockers)
632
757
  ? result.data.blockers.filter(isRecord)
633
758
  : [];
634
759
  if (blockers.length > 0)
635
760
  return [
636
- "平台未启动:存在未就绪模块",
761
+ theme.failure("平台未启动:存在未就绪模块"),
637
762
  "",
638
- ...blockers.map((item) => `${String(item.moduleRef).padEnd(28)} ${String(item.setupStatus) === "FAILED" ? "失败" : "需要操作"} 配置尚未完成`),
763
+ ...blockers.flatMap((item) => [
764
+ `${String(item.setupStatus) === "FAILED" ? theme.failure("✕") : theme.warning("◆")} ${theme.section(String(item.moduleRef))} ${String(item.setupStatus) === "FAILED" ? theme.failure("失败") : theme.warning("需要操作")}`,
765
+ ` 原因:${typeof item.reason === "string" ? item.reason : "模块配置尚未完成"}`,
766
+ ` 下一步:${theme.command(`platform setup --module ${String(item.moduleRef)}`)}`,
767
+ ]),
639
768
  "",
640
- `处理方式:platform setup${result.workspaceRoot ? ` --workspace "${result.workspaceRoot}"` : ""}`,
769
+ `处理方式:${theme.command(`platform setup${result.workspaceRoot ? ` --workspace "${result.workspaceRoot}"` : ""}`)}`,
641
770
  ].join("\n");
642
771
  }
643
772
  if (result.status === "FAILED" && result.error)
644
- return `${result.command} 失败:${result.error.message}`;
773
+ return [
774
+ theme.failure(`╭─ ${result.command} 执行失败`),
775
+ `${theme.failure("│")} ${theme.section(result.error.code)}`,
776
+ `${theme.failure("│")} ${result.error.message}`,
777
+ theme.failure("╰─ 请根据提示修复后重试"),
778
+ ].join("\n");
645
779
  if (result.command === "help")
646
780
  return [
647
- "ProFlow 平台命令行",
781
+ theme.title("ProFlow 平台命令行"),
782
+ "管理 ProFlow 模块的安装、配置、文档和运行生命周期。",
648
783
  "",
649
- "platform install 安装并初始化全部 ProFlow 模块",
784
+ theme.section("用法"),
785
+ ` ${theme.command("platform <command> [--workspace <路径>]")}`,
786
+ "",
787
+ theme.section("命令"),
788
+ `${theme.command("platform install")} 安装并初始化全部 ProFlow 模块`,
650
789
  "platform uninstall 卸载模块包,保留 Workspace 数据",
651
790
  "platform status 查看模块配置与运行状态",
652
791
  "platform setup 自动配置并列出全部剩余步骤",
@@ -654,20 +793,32 @@ export function renderHumanResult(result) {
654
793
  "platform start 完成全量检查后启动平台",
655
794
  "platform stop 按逆依赖顺序停止平台",
656
795
  "",
657
- "常用参数:--workspace <路径>;setup 还支持 --module <模块名>",
658
- "推荐流程:install status → docs → setup → start → status → stop",
659
- "人工配置示例:pnpm exec -- proflow-chatgpt-carrier setup",
660
- "AI 配置示例:pnpm exec -- proflow-chatgpt-carrier setup --carrier-url <url>",
661
- "状态图例:已就绪=配置完成;需要操作=需运行 setup;失败=存在系统阻塞;无独立进程=无需启动",
796
+ theme.section("公共参数"),
797
+ " --workspace <路径> 指定 ProFlow Workspace",
798
+ " setup --module <模块名> 只查看指定模块的配置步骤",
799
+ "",
800
+ theme.section("推荐流程"),
801
+ ` ${theme.command("install → status → docs → setup → start → status → stop")}`,
802
+ "",
803
+ theme.section("人工配置示例"),
804
+ ` 人工:${theme.command("pnpm exec -- proflow-chatgpt-carrier setup")}`,
805
+ ` AI: ${theme.command("pnpm exec -- proflow-chatgpt-carrier setup --carrier-url <url>")}`,
806
+ "",
807
+ theme.section("状态图例"),
808
+ ` ${theme.success("已就绪")} 配置完成 ${theme.warning("需要操作")} 运行 setup`,
809
+ ` ${theme.failure("失败")} 存在阻塞 无独立进程 无需启动`,
810
+ "",
811
+ theme.section("遇到问题"),
812
+ ` 先运行 ${theme.command("platform status")};配置问题运行 ${theme.command("platform setup")}。`,
662
813
  ].join("\n");
663
814
  if (result.command === "version" && isRecord(result.data))
664
815
  return String(result.data.version ?? platformCliDescriptor.moduleVersion);
665
816
  if (result.command === "status")
666
- return renderStatus(result.data);
817
+ return renderStatus(result.data, theme);
667
818
  if (result.command === "setup")
668
- return renderSetup(result.data);
819
+ return renderSetup(result.data, theme);
669
820
  if (result.command === "docs")
670
- return renderDocs(result.data);
821
+ return renderDocs(result.data, theme);
671
822
  if ((result.command === "start" || result.command === "stop") &&
672
823
  isRecord(result.data)) {
673
824
  const skippedItems = Array.isArray(result.data.skipped)
@@ -684,8 +835,10 @@ export function renderHumanResult(result) {
684
835
  !skippedRefs.has(String(item.moduleRef))).length;
685
836
  const failed = operations.find((item) => isRecord(item.result) && item.result.status !== "SUCCEEDED");
686
837
  return [
687
- `平台${result.command === "start" ? "启动" : "停止"}${failed ? "未完成" : "完成"}`,
688
- `成功:${succeeded},跳过:${skipped}${failed ? `,失败:${String(failed.moduleRef)}` : ",失败:0"}`,
838
+ failed
839
+ ? theme.failure(`平台${result.command === "start" ? "启动" : "停止"}未完成`)
840
+ : theme.success(`✓ 平台${result.command === "start" ? "启动" : "停止"}完成`),
841
+ `${theme.success(`成功:${succeeded}`)} ${theme.muted(`跳过:${skipped}`)} ${failed ? theme.failure(`失败:${String(failed.moduleRef)}`) : theme.muted("失败:0")}`,
689
842
  ].join("\n");
690
843
  }
691
844
  const labels = {
@@ -694,14 +847,40 @@ export function renderHumanResult(result) {
694
847
  start: "启动",
695
848
  stop: "停止",
696
849
  };
697
- return `${labels[result.command] ?? result.command}${result.status === "SUCCEEDED" ? "成功" : "未完成"}${result.workspaceRoot ? `\nWorkspace:${result.workspaceRoot}` : ""}`;
850
+ return `${result.status === "SUCCEEDED" ? theme.success("✓") : theme.failure("✕")} ${labels[result.command] ?? result.command}${result.status === "SUCCEEDED" ? "成功" : "未完成"}${result.workspaceRoot ? `\n${theme.muted("Workspace")} ${result.workspaceRoot}` : ""}`;
698
851
  }
699
852
  if (import.meta.main) {
700
853
  const argv = process.argv.slice(2);
701
854
  const reporter = createTerminalProgressReporter();
702
855
  const result = await runCli(argv, { onProgress: reporter });
703
856
  reporter.close();
704
- process.stdout.write(`${renderHumanResult(result)}\n`);
857
+ const color = process.stdout.isTTY === true &&
858
+ process.env.NO_COLOR === undefined &&
859
+ process.env.TERM !== "dumb";
860
+ const rendered = `${renderHumanResult(result, {
861
+ color,
862
+ width: process.stdout.columns,
863
+ })}\n`;
864
+ if (result.command === "docs" &&
865
+ process.stdout.isTTY === true &&
866
+ rendered.split("\n").length > (process.stdout.rows ?? 24)) {
867
+ await writeThroughPager(rendered);
868
+ }
869
+ else
870
+ process.stdout.write(rendered);
705
871
  if (result.status !== "SUCCEEDED")
706
872
  process.exitCode = 1;
707
873
  }
874
+ async function writeThroughPager(content) {
875
+ await new Promise((resolvePromise) => {
876
+ const pager = spawn("less", ["-FIRX"], {
877
+ stdio: ["pipe", "inherit", "inherit"],
878
+ });
879
+ pager.once("error", () => {
880
+ process.stdout.write(content);
881
+ resolvePromise();
882
+ });
883
+ pager.once("close", () => resolvePromise());
884
+ pager.stdin.end(content);
885
+ });
886
+ }
@@ -6,7 +6,11 @@ export interface WorkspacePackageManagerSelection {
6
6
  declared?: string;
7
7
  }
8
8
  export interface PackageCommandRunner {
9
- run(command: string, args: readonly string[], cwd: string): Promise<string>;
9
+ run(command: string, args: readonly string[], cwd: string, onOutput?: (line: PackageCommandOutput) => void): Promise<string>;
10
+ }
11
+ export interface PackageCommandOutput {
12
+ stream: "stdout" | "stderr";
13
+ line: string;
10
14
  }
11
15
  export declare function systemPackageCommandRunner(): PackageCommandRunner;
12
16
  export declare function readWorkspacePackageManagerSelection(workspaceRoot: string): Promise<WorkspacePackageManagerSelection>;
@@ -24,11 +28,13 @@ export declare function syncWorkspacePackages(options: {
24
28
  packages: readonly WorkspacePackageTarget[];
25
29
  runner?: PackageCommandRunner;
26
30
  executableAvailable?: (command: string) => boolean;
31
+ onOutput?: (line: PackageCommandOutput) => void;
27
32
  }): Promise<WorkspacePackageMutationResult>;
28
33
  export declare function removeWorkspacePackages(options: {
29
34
  workspaceRoot: string;
30
35
  packageNames: readonly string[];
31
36
  runner?: PackageCommandRunner;
32
37
  executableAvailable?: (command: string) => boolean;
38
+ onOutput?: (line: PackageCommandOutput) => void;
33
39
  }): Promise<WorkspacePackageMutationResult>;
34
40
  export declare function observeWorkspaceInstalledVersion(workspaceRoot: string, packageName: string): Promise<string | undefined>;
@@ -1,20 +1,58 @@
1
- import { execFile } from "node:child_process";
1
+ import { spawn } from "node:child_process";
2
2
  import { accessSync, constants } from "node:fs";
3
3
  import { access, readFile } from "node:fs/promises";
4
4
  import { delimiter, join } from "node:path";
5
- import { promisify } from "node:util";
6
5
  import { PlatformError } from "../errors.js";
7
6
  import { atomicWrite } from "../paths.js";
8
- const execFileAsync = promisify(execFile);
9
7
  export function systemPackageCommandRunner() {
10
8
  return {
11
- async run(command, args, cwd) {
12
- const result = await execFileAsync(command, [...args], {
13
- cwd,
14
- encoding: "utf8",
15
- maxBuffer: 4 * 1024 * 1024,
9
+ async run(command, args, cwd, onOutput) {
10
+ return new Promise((resolvePromise, rejectPromise) => {
11
+ const child = spawn(command, [...args], {
12
+ cwd,
13
+ stdio: ["ignore", "pipe", "pipe"],
14
+ });
15
+ let stdout = "";
16
+ let stderr = "";
17
+ const pending = {
18
+ stdout: "",
19
+ stderr: "",
20
+ };
21
+ const consume = (stream, chunk) => {
22
+ const text = chunk.toString("utf8");
23
+ if (stream === "stdout")
24
+ stdout += text;
25
+ else
26
+ stderr += text;
27
+ const parts = `${pending[stream]}${text}`.split(/\r?\n|\r/);
28
+ pending[stream] = parts.pop() ?? "";
29
+ for (const line of parts) {
30
+ const normalized = line.trim();
31
+ if (normalized)
32
+ onOutput?.({ stream, line: normalized });
33
+ }
34
+ };
35
+ const flush = () => {
36
+ for (const stream of ["stdout", "stderr"]) {
37
+ const line = pending[stream].trim();
38
+ if (line)
39
+ onOutput?.({ stream, line });
40
+ pending[stream] = "";
41
+ }
42
+ };
43
+ child.stdout.on("data", (chunk) => consume("stdout", chunk));
44
+ child.stderr.on("data", (chunk) => consume("stderr", chunk));
45
+ child.once("error", rejectPromise);
46
+ child.once("close", (code, signal) => {
47
+ flush();
48
+ if (code === 0)
49
+ resolvePromise(stdout);
50
+ else {
51
+ const detail = stderr.trim() || stdout.trim();
52
+ rejectPromise(new Error(`${command} exited with ${code ?? signal ?? "unknown"}${detail ? `: ${detail}` : ""}`));
53
+ }
54
+ });
16
55
  });
17
- return result.stdout;
18
56
  },
19
57
  };
20
58
  }
@@ -125,7 +163,7 @@ export async function syncWorkspacePackages(options) {
125
163
  await writeManagedPackageSet(options.workspaceRoot, targets);
126
164
  const args = await packageManagerInstallArgs(runner, manager, options.workspaceRoot);
127
165
  try {
128
- await runner.run(manager.name, args, options.workspaceRoot);
166
+ await runner.run(manager.name, args, options.workspaceRoot, options.onOutput);
129
167
  }
130
168
  catch (error) {
131
169
  throw new PlatformError("COMMAND_FAILED", `${manager.name} install failed: ${packageMutationError(error)}`);
@@ -177,7 +215,7 @@ export async function removeWorkspacePackages(options) {
177
215
  const manager = await requireWorkspacePackageManager(options.workspaceRoot, options.executableAvailable ?? findExecutable);
178
216
  const args = await batchPackageManagerArgs(runner, manager, options.workspaceRoot, "remove", packageNames);
179
217
  try {
180
- await runner.run(manager.name, args, options.workspaceRoot);
218
+ await runner.run(manager.name, args, options.workspaceRoot, options.onOutput);
181
219
  }
182
220
  catch (error) {
183
221
  throw new PlatformError("UNINSTALL_FAILED", `${manager.name} remove failed: ${packageMutationError(error)}`);
@@ -14,6 +14,7 @@ export interface ModuleBatchResult {
14
14
  blockers?: Array<{
15
15
  moduleRef: string;
16
16
  setupStatus: ModuleSetupStatus;
17
+ reason?: string;
17
18
  }>;
18
19
  skipped?: Array<{
19
20
  moduleRef: string;
@@ -250,7 +250,11 @@ export async function startModulesThin(catalog, modules, workspaceRoot, reporter
250
250
  message: `${module.moduleRef}`,
251
251
  });
252
252
  if (!succeeded(status.result)) {
253
- blockers.push({ moduleRef: module.moduleRef, setupStatus: "FAILED" });
253
+ blockers.push({
254
+ moduleRef: module.moduleRef,
255
+ setupStatus: "FAILED",
256
+ reason: status.result.error?.message ?? "模块状态检查失败",
257
+ });
254
258
  continue;
255
259
  }
256
260
  const observed = moduleStatusObservationSchema.parse(status.result.data);
@@ -1,10 +1,13 @@
1
1
  export type PlatformProgressStatus = "STARTED" | "SUCCEEDED" | "ACTION_REQUIRED" | "FAILED" | "SKIPPED";
2
+ export type PlatformProgressKind = "phase" | "detail" | "subprocess";
2
3
  export interface PlatformProgressEvent {
3
4
  command: string;
4
5
  phase: string;
6
+ kind?: PlatformProgressKind;
5
7
  current?: number;
6
8
  total?: number;
7
9
  moduleRef?: string;
10
+ elapsedMs?: number;
8
11
  status: PlatformProgressStatus;
9
12
  message: string;
10
13
  }
@@ -34,4 +34,10 @@ export declare function discoverRegistryModules(options: {
34
34
  packageName?: string;
35
35
  runner?: NpmCommandRunner;
36
36
  nodeVersion?: string;
37
+ onSearchComplete?: (total: number) => void;
38
+ onPackageChecked?: (input: {
39
+ current: number;
40
+ total: number;
41
+ packageName: string;
42
+ }) => void;
37
43
  }): Promise<RegistryDiscoveryResult>;
@@ -42,16 +42,32 @@ export async function discoverRegistryModules(options) {
42
42
  const names = options.packageName === undefined
43
43
  ? await searchPackageNames(options.workspaceRoot, registry, runner)
44
44
  : [validateRequestedPackageName(options.packageName)];
45
+ options.onSearchComplete?.(names.length);
45
46
  const candidates = [];
46
47
  const rejected = [];
47
- for (const packageName of names) {
48
- const manifest = await viewManifest(options.workspaceRoot, registry, packageName, runner);
49
- const assessed = assessManifest(manifest, registry, options.nodeVersion ?? process.versions.node);
50
- if ("candidate" in assessed)
51
- candidates.push(assessed.candidate);
52
- else
53
- rejected.push(assessed.rejected);
54
- }
48
+ let nextIndex = 0;
49
+ let completed = 0;
50
+ const worker = async () => {
51
+ while (nextIndex < names.length) {
52
+ const index = nextIndex++;
53
+ const packageName = names[index];
54
+ if (packageName === undefined)
55
+ return;
56
+ const manifest = await viewManifest(options.workspaceRoot, registry, packageName, runner);
57
+ const assessed = assessManifest(manifest, registry, options.nodeVersion ?? process.versions.node);
58
+ if ("candidate" in assessed)
59
+ candidates.push(assessed.candidate);
60
+ else
61
+ rejected.push(assessed.rejected);
62
+ completed += 1;
63
+ options.onPackageChecked?.({
64
+ current: completed,
65
+ total: names.length,
66
+ packageName,
67
+ });
68
+ }
69
+ };
70
+ await Promise.all(Array.from({ length: Math.min(4, names.length) }, () => worker()));
55
71
  candidates.sort((left, right) => left.packageName.localeCompare(right.packageName));
56
72
  rejected.sort((left, right) => left.packageName.localeCompare(right.packageName));
57
73
  return { registry, candidates, rejected };
@@ -1,14 +1,22 @@
1
1
  import { clearLine, cursorTo } from "node:readline";
2
2
  const colors = {
3
+ cyan: "\u001b[36m",
4
+ blue: "\u001b[34m",
3
5
  green: "\u001b[32m",
4
6
  red: "\u001b[31m",
5
7
  yellow: "\u001b[33m",
8
+ bold: "\u001b[1m",
6
9
  dim: "\u001b[2m",
7
10
  reset: "\u001b[0m",
8
11
  };
9
12
  const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
10
13
  function progressLine(event, color, spinner = "") {
11
- const prefix = event.current && event.total ? `${event.current}/${event.total} · ` : "";
14
+ const prefix = event.current && event.total
15
+ ? `[${String(event.current).padStart(String(event.total).length, "0")}/${event.total}] `
16
+ : "";
17
+ const elapsed = event.elapsedMs !== undefined
18
+ ? ` · ${(event.elapsedMs / 1000).toFixed(1)}s`
19
+ : "";
12
20
  const suffix = event.status === "SUCCEEDED"
13
21
  ? "完成"
14
22
  : event.status === "ACTION_REQUIRED"
@@ -18,23 +26,27 @@ function progressLine(event, color, spinner = "") {
18
26
  : event.status === "SKIPPED"
19
27
  ? "跳过"
20
28
  : "";
21
- const symbol = event.status === "STARTED"
22
- ? spinner
23
- : event.status === "SUCCEEDED"
24
- ? "✓"
25
- : event.status === "FAILED"
26
- ? ""
27
- : event.status === "ACTION_REQUIRED"
28
- ? ""
29
- : "";
29
+ const symbol = event.kind === "subprocess"
30
+ ? "│"
31
+ : event.status === "STARTED"
32
+ ? spinner
33
+ : event.status === "SUCCEEDED"
34
+ ? ""
35
+ : event.status === "FAILED"
36
+ ? ""
37
+ : event.status === "ACTION_REQUIRED"
38
+ ? "◆"
39
+ : "○";
30
40
  if (!color)
31
- return `${symbol ? `${symbol} ` : ""}${prefix}${event.message}${suffix ? ` · ${suffix}` : ""}`;
41
+ return `${symbol ? `${symbol} ` : ""}${prefix}${event.message}${suffix ? ` · ${suffix}` : ""}${elapsed}`;
32
42
  const tone = event.status === "SUCCEEDED"
33
43
  ? colors.green
34
44
  : event.status === "FAILED"
35
45
  ? colors.red
36
- : colors.yellow;
37
- return `${tone}${symbol}${colors.reset} ${colors.dim}${prefix}${colors.reset}${event.message}${suffix ? ` · ${tone}${suffix}${colors.reset}` : ""}`;
46
+ : event.kind === "subprocess"
47
+ ? colors.blue
48
+ : colors.yellow;
49
+ return `${tone}${symbol}${colors.reset} ${colors.dim}${prefix}${colors.reset}${event.message}${suffix ? ` · ${tone}${suffix}${colors.reset}` : ""}${event.elapsedMs === undefined ? "" : `${colors.dim}${elapsed}${colors.reset}`}`;
38
50
  }
39
51
  export function createTerminalProgressReporter(stream = process.stderr) {
40
52
  const interactive = stream.isTTY === true;
@@ -58,7 +70,10 @@ export function createTerminalProgressReporter(stream = process.stderr) {
58
70
  };
59
71
  const reporter = ((event) => {
60
72
  clearSpinner();
61
- active = event.status === "STARTED" ? event : undefined;
73
+ const previousActive = active;
74
+ if (event.kind !== "subprocess") {
75
+ active = event.status === "STARTED" ? event : undefined;
76
+ }
62
77
  const line = progressLine(event, color, interactive ? spinnerFrames[frame++ % spinnerFrames.length] : "›");
63
78
  if (!interactive) {
64
79
  stream.write(`${line}\n`);
@@ -67,18 +82,22 @@ export function createTerminalProgressReporter(stream = process.stderr) {
67
82
  cursorTo(stream, 0);
68
83
  clearLine(stream, 0);
69
84
  stream.write(line);
85
+ if (event.kind === "subprocess") {
86
+ stream.write("\n");
87
+ active = previousActive;
88
+ if (active) {
89
+ paint();
90
+ timer = setInterval(paint, 80);
91
+ timer.unref();
92
+ }
93
+ return;
94
+ }
70
95
  if (event.status === "STARTED") {
71
96
  timer = setInterval(paint, 80);
72
97
  timer.unref();
73
98
  return;
74
99
  }
75
- const compactModuleProgress = event.current !== undefined &&
76
- event.total !== undefined &&
77
- event.current < event.total &&
78
- event.status !== "FAILED" &&
79
- event.status !== "ACTION_REQUIRED";
80
- if (!compactModuleProgress)
81
- stream.write("\n");
100
+ stream.write("\n");
82
101
  });
83
102
  reporter.close = () => {
84
103
  clearSpinner();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tomflow/proflow-platform-cli",
3
- "version": "0.1.33",
3
+ "version": "0.1.34",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -24,11 +24,11 @@
24
24
  "SETUP.md"
25
25
  ],
26
26
  "dependencies": {
27
- "@tomflow/proflow-module-contract": "^0.1.10"
27
+ "@tomflow/proflow-module-contract": "^0.1.11"
28
28
  },
29
29
  "devDependencies": {
30
- "@tomflow/proflow-module-template": "^0.1.8",
31
- "@tomflow/proflow-deployment-conformance": "^0.1.8"
30
+ "@tomflow/proflow-deployment-conformance": "^0.1.9",
31
+ "@tomflow/proflow-module-template": "^0.1.9"
32
32
  },
33
33
  "description": "Thin Platform CLI for Module discovery, documentation, package synchronization and lifecycle orchestration.",
34
34
  "keywords": [
@@ -3,7 +3,7 @@
3
3
  "contractVersion": "1.0.0",
4
4
  "moduleRef": "platform-cli",
5
5
  "packageName": "@tomflow/proflow-platform-cli",
6
- "moduleVersion": "0.1.33",
6
+ "moduleVersion": "0.1.34",
7
7
  "kind": "cli",
8
8
  "templateVersion": "1.0.0",
9
9
  "platformCompatibility": ">=1.0.0 <2.0.0",