@tomflow/proflow-platform-cli 0.1.21 → 0.1.23

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,17 @@
1
1
  # @tomflow/proflow-platform-cli
2
2
 
3
+ ## 0.1.23
4
+
5
+ ### Patch Changes
6
+
7
+ - Resolve DOCS.md from both source and built deployment adapters, prove all 24 built adapters return the published body, and announce package synchronization before the package manager blocks.
8
+
9
+ ## 0.1.22
10
+
11
+ ### Patch Changes
12
+
13
+ - Replace JSON terminal protocols with typed CLI outcomes, add real-time Chinese progress and lifecycle summaries, publish DOCS content, standardize executable setup plans and package-owned setup commands, and preserve only user-owned pnpm policy during uninstall.
14
+
3
15
  ## 0.1.21
4
16
 
5
17
  ### Patch Changes
package/README.md CHANGED
@@ -51,7 +51,7 @@ There is no Platform-derived configuration status, missing-input list, overall r
51
51
 
52
52
  `platform setup` scans all discovered Modules in dependency order. It skips `READY` Modules, invokes `Module.setup` for every non-ready Module, continues after `ACTION_REQUIRED` or `FAILED`, and returns one complete aggregate.
53
53
 
54
- Targeted `platform setup --module <moduleRef> --input '<json>'` forwards opaque input to the owning Module. Platform neither interprets that input nor creates Module-specific instructions.
54
+ Targeted `platform setup --module <moduleRef>` re-observes one Module. Human or AI input is collected by the package-owned setup command shown by `platform setup`; Platform does not accept raw JSON input.
55
55
 
56
56
  ## Docs
57
57
 
package/SETUP.md CHANGED
@@ -1,27 +1,10 @@
1
1
  # @tomflow/proflow-platform-cli — Module Setup
2
2
 
3
- The Platform CLI owns orchestration only. It never interprets another Module's setup details. Global `platform setup` traverses every discovered Module, skips `READY` Modules, lets each non-ready Module run its own package-owned setup workflow, and aggregates every remaining `ACTION_REQUIRED` or `FAILED` result in one response.
4
-
5
- ## Step 1 — Verify the Platform CLI Module
6
-
7
- **Executable:**
8
-
9
- ```bash
10
- platform setup --module platform-cli
11
- ```
12
-
13
- **Success condition:**
14
-
15
- `platform status` reports `platform-cli setup=READY`. No user/private path, token, loopback endpoint, or cross-Module fact is requested.
16
-
17
- ## Step 2 — Run the full workspace setup guide
18
-
19
- **Executable:**
20
-
21
- ```bash
22
- platform setup
23
- ```
24
-
25
- **Success condition:**
26
-
27
- All automatically solvable Module setup work is completed in the same run, and every remaining human/external action is listed together. Re-running the command re-observes reality and continues until all required Modules report `setupStatus=READY`.
3
+ ## STEP-PLATFORM-CLI-01 完成模块自动配置
4
+
5
+ Responsible: AI
6
+ Interactive executable: `platform setup --module platform-cli`
7
+ Non-interactive executable: `platform setup --module platform-cli`
8
+ Required inputs: none
9
+ Verify: `platform status`
10
+ Success condition: `platform-cli.setupStatus=READY`.
@@ -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.21";
9
+ readonly moduleVersion: "0.1.23";
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.21";
19
+ readonly moduleVersion: "0.1.23";
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.21";
29
+ moduleVersion: "0.1.23";
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.21";
43
+ readonly moduleVersion: "0.1.23";
44
44
  };
45
45
  observedEffects: never[];
46
46
  }>;
@@ -50,10 +50,9 @@ export declare const behaviorAdapter: {
50
50
  ok: true;
51
51
  status: "SUCCEEDED";
52
52
  moduleRef: "platform-cli";
53
- moduleVersion: "0.1.21";
53
+ moduleVersion: "0.1.23";
54
54
  data: {
55
55
  docs: string;
56
- setup: string;
57
56
  };
58
57
  };
59
58
  observedEffects: never[];
@@ -64,7 +63,7 @@ export declare const behaviorAdapter: {
64
63
  readonly ok: true;
65
64
  readonly status: "SUCCEEDED";
66
65
  readonly moduleRef: "platform-cli";
67
- readonly moduleVersion: "0.1.21";
66
+ readonly moduleVersion: "0.1.23";
68
67
  };
69
68
  observedEffects: never[];
70
69
  }>;
@@ -74,7 +73,7 @@ export declare const behaviorAdapter: {
74
73
  readonly ok: true;
75
74
  readonly status: "SUCCEEDED";
76
75
  readonly moduleRef: "platform-cli";
77
- readonly moduleVersion: "0.1.21";
76
+ readonly moduleVersion: "0.1.23";
78
77
  };
79
78
  observedEffects: never[];
80
79
  }>;
@@ -1,3 +1,4 @@
1
+ import { readFileSync } from "node:fs";
1
2
  import { descriptor } from "./descriptor.js";
2
3
  const base = {
3
4
  contract: "deployment.result.v1",
@@ -28,7 +29,12 @@ export const behaviorAdapter = {
28
29
  observedEffects: [],
29
30
  }),
30
31
  docs: async (_context) => ({
31
- result: { ...base, data: { docs: "DOCS.md", setup: "SETUP.md" } },
32
+ result: {
33
+ ...base,
34
+ data: {
35
+ docs: readFileSync(new URL(import.meta.url.includes("/dist/") ? "../../DOCS.md" : "../DOCS.md", import.meta.url), "utf8"),
36
+ },
37
+ },
32
38
  observedEffects: [],
33
39
  }),
34
40
  start: async (_context) => ({
@@ -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.21";
6
+ readonly moduleVersion: "0.1.23";
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.21",
6
+ moduleVersion: "0.1.23",
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
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { type PackageCommandRunner } from "./install/package-manager.ts";
3
+ import { type PlatformProgressReporter } from "./progress.ts";
3
4
  import { type NpmCommandRunner } from "./registry/index.ts";
4
5
  export type CliStatus = "SUCCEEDED" | "ACTION_REQUIRED" | "BLOCKED" | "FAILED";
5
6
  export interface CliOutcome {
@@ -17,6 +18,7 @@ export interface CliRuntimeOptions {
17
18
  registryRunner?: NpmCommandRunner;
18
19
  packageRunner?: PackageCommandRunner;
19
20
  executableAvailable?: (command: string) => boolean;
21
+ onProgress?: PlatformProgressReporter;
20
22
  }
21
- export declare function runCli(argv: readonly string[], runtime?: CliRuntimeOptions): Promise<string>;
23
+ export declare function runCli(argv: readonly string[], runtime?: CliRuntimeOptions): Promise<CliOutcome>;
22
24
  export declare function renderHumanResult(result: CliOutcome): string;
package/dist/src/cli.js CHANGED
@@ -1,15 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  import { readFile, realpath, stat } from "node:fs/promises";
3
3
  import { resolve } from "node:path";
4
- import { moduleStatusObservationSchema } from "@tomflow/proflow-module-contract";
4
+ import { moduleDocsDataSchema, moduleSetupPlanDataSchema, moduleStatusObservationSchema, } from "@tomflow/proflow-module-contract";
5
5
  import { descriptor as platformCliDescriptor } from "../deployment/descriptor.js";
6
6
  import { AutoModuleCatalog, discoverModules } from "./discovery/discover.js";
7
7
  import { InstalledModuleCatalog } from "./discovery/installed.js";
8
8
  import { PlatformError } from "./errors.js";
9
9
  import { observeWorkspaceInstalledVersion, removeWorkspacePackages, syncWorkspacePackages, } from "./install/package-manager.js";
10
+ import { cleanOwnedPnpmPolicy, observeMinimumReleaseAgeExclude, recordPnpmPolicyOwnership, } from "./install/pnpm-policy.js";
10
11
  import { installModulesThin, observeDocs, observeStatuses, setupModulesThin, startModulesThin, stopModulesThin, uninstallModulesThin, } from "./lifecycle/index.js";
11
12
  import { ensureWorkspaceMetadata } from "./persistence/workspace-metadata.js";
13
+ import { reportProgress } from "./progress.js";
12
14
  import { discoverRegistryModules, PRO_FLOW_PACKAGE_PREFIX, } from "./registry/index.js";
15
+ import { createTerminalProgressReporter } from "./terminal.js";
13
16
  const COMMANDS = [
14
17
  "install",
15
18
  "uninstall",
@@ -20,36 +23,23 @@ const COMMANDS = [
20
23
  "stop",
21
24
  ];
22
25
  function parseArgs(argv) {
23
- let json = false, workspace, moduleRef, input, inputSeen = false;
26
+ let workspace, moduleRef;
24
27
  let special;
25
28
  const positional = [];
26
29
  for (let index = 0; index < argv.length; index += 1) {
27
30
  const value = argv[index];
28
31
  if (value === undefined)
29
32
  continue;
30
- if (value === "--json") {
31
- json = true;
32
- continue;
33
- }
34
- if (value === "--workspace" ||
35
- value === "--module" ||
36
- value === "--input") {
33
+ if (value === "--json")
34
+ throw new PlatformError("INVALID_REQUEST", "不支持的选项 --json");
35
+ if (value === "--workspace" || value === "--module") {
37
36
  const next = argv[index + 1];
38
- if (!next || (value !== "--input" && next.startsWith("-")))
37
+ if (!next || next.startsWith("-"))
39
38
  throw new PlatformError("INVALID_REQUEST", `${value} requires a value`);
40
39
  if (value === "--workspace")
41
40
  workspace = next;
42
41
  else if (value === "--module")
43
42
  moduleRef = next;
44
- else {
45
- try {
46
- input = JSON.parse(next);
47
- inputSeen = true;
48
- }
49
- catch (error) {
50
- throw new PlatformError("INVALID_REQUEST", `--input must be valid JSON: ${error instanceof Error ? error.message : String(error)}`);
51
- }
52
- }
53
43
  index += 1;
54
44
  continue;
55
45
  }
@@ -68,29 +58,24 @@ function parseArgs(argv) {
68
58
  if (special !== undefined) {
69
59
  if (workspace !== undefined ||
70
60
  moduleRef !== undefined ||
71
- inputSeen ||
72
61
  positional.length > 0)
73
62
  throw new PlatformError("INVALID_REQUEST", `${special} flag cannot be combined with command options`);
74
- return { command: special, json };
63
+ return { command: special };
75
64
  }
76
65
  if (positional.length === 0)
77
- return { command: "help", json };
66
+ return { command: "help" };
78
67
  if (positional.length !== 1)
79
68
  throw new PlatformError("INVALID_REQUEST", "commands accept no positional arguments");
80
69
  const raw = positional[0] ?? "";
81
70
  if (!COMMANDS.includes(raw))
82
71
  throw new PlatformError("INVALID_REQUEST", `unknown command ${raw}`);
83
72
  const command = raw;
84
- if ((moduleRef !== undefined || inputSeen) && command !== "setup")
85
- throw new PlatformError("INVALID_REQUEST", "--module/--input are only valid with setup");
86
- if (inputSeen && moduleRef === undefined)
87
- throw new PlatformError("INVALID_REQUEST", "--input requires --module");
73
+ if (moduleRef !== undefined && command !== "setup")
74
+ throw new PlatformError("INVALID_REQUEST", "--module is only valid with setup");
88
75
  return {
89
76
  command,
90
- json,
91
77
  ...(workspace === undefined ? {} : { workspace }),
92
78
  ...(moduleRef === undefined ? {} : { moduleRef }),
93
- ...(inputSeen ? { input } : {}),
94
79
  };
95
80
  }
96
81
  function outcome(command, status, workspaceRoot, data) {
@@ -144,9 +129,9 @@ function batchStatus(result) {
144
129
  return "ACTION_REQUIRED";
145
130
  return "FAILED";
146
131
  }
147
- async function handleStatus(root) {
132
+ async function handleStatus(root, runtime) {
148
133
  const { catalog, modules } = await buildContext(root);
149
- const observed = await observeStatuses(catalog, modules, root);
134
+ const observed = await observeStatuses(catalog, modules, root, runtime.onProgress);
150
135
  const byRef = new Map(modules.map((module) => [module.moduleRef, module]));
151
136
  const output = observed.map((item) => {
152
137
  if (item.result.status !== "SUCCEEDED")
@@ -166,16 +151,32 @@ async function handleStatus(root) {
166
151
  });
167
152
  return outcome("status", "SUCCEEDED", root, { modules: output });
168
153
  }
169
- async function handleDocs(root) {
170
- const { catalog, modules } = await buildContext(root);
171
- const docs = await observeDocs(catalog, modules, root);
172
- const byRef = new Map(modules.map((module) => [module.moduleRef, module]));
173
- return outcome("docs", "SUCCEEDED", root, {
174
- modules: docs.map((item) => ({
154
+ async function handleDocs(root, runtime) {
155
+ const { catalog, modules: resolvedModules } = await buildContext(root);
156
+ const docs = await observeDocs(catalog, resolvedModules, root, runtime.onProgress);
157
+ const byRef = new Map(resolvedModules.map((module) => [module.moduleRef, module]));
158
+ const modules = [];
159
+ const errors = [];
160
+ for (const item of docs) {
161
+ const parsed = moduleDocsDataSchema.safeParse(item.result.data);
162
+ if (item.result.status !== "SUCCEEDED" || !parsed.success) {
163
+ errors.push({
164
+ moduleRef: item.moduleRef,
165
+ reason: item.result.status !== "SUCCEEDED"
166
+ ? (item.result.error?.message ?? "文档读取失败")
167
+ : "Module.docs 返回格式无效",
168
+ });
169
+ continue;
170
+ }
171
+ modules.push({
175
172
  moduleRef: item.moduleRef,
176
173
  version: byRef.get(item.moduleRef)?.moduleVersion,
177
- docs: item.result.data,
178
- })),
174
+ docs: parsed.data.docs,
175
+ });
176
+ }
177
+ return outcome("docs", errors.length === 0 ? "SUCCEEDED" : "FAILED", root, {
178
+ modules,
179
+ errors,
179
180
  });
180
181
  }
181
182
  async function validateInstalledPackageSet(root, candidates, previousManaged) {
@@ -203,7 +204,19 @@ async function validateInstalledPackageSet(root, candidates, previousManaged) {
203
204
  }
204
205
  }
205
206
  async function handleInstall(root, runtime) {
207
+ reportProgress(runtime.onProgress, {
208
+ command: "install",
209
+ phase: "workspace",
210
+ status: "STARTED",
211
+ message: "正在校验 Workspace",
212
+ });
206
213
  const metadata = await ensureWorkspaceMetadata(root);
214
+ reportProgress(runtime.onProgress, {
215
+ command: "install",
216
+ phase: "registry",
217
+ status: "STARTED",
218
+ message: "正在发现 Registry 模块",
219
+ });
207
220
  const discovered = await discoverRegistryModules({
208
221
  workspaceRoot: root,
209
222
  ...(runtime.registryRunner === undefined
@@ -215,6 +228,13 @@ async function handleInstall(root, runtime) {
215
228
  if (discovered.candidates.length === 0)
216
229
  throw new PlatformError("PACKAGE_NOT_FOUND", "no ProFlow packages were discovered in the configured scope");
217
230
  const previousManaged = await workspaceProFlowDependencies(root);
231
+ const pnpmPolicyBefore = await observeMinimumReleaseAgeExclude(root);
232
+ reportProgress(runtime.onProgress, {
233
+ command: "install",
234
+ phase: "packages",
235
+ status: "STARTED",
236
+ message: "正在同步依赖",
237
+ });
218
238
  const mutation = await syncWorkspacePackages({
219
239
  workspaceRoot: root,
220
240
  packages: discovered.candidates.map((item) => ({
@@ -228,9 +248,16 @@ async function handleInstall(root, runtime) {
228
248
  ? {}
229
249
  : { executableAvailable: runtime.executableAvailable }),
230
250
  });
251
+ reportProgress(runtime.onProgress, {
252
+ command: "install",
253
+ phase: "packages",
254
+ status: "SUCCEEDED",
255
+ message: "依赖同步完成",
256
+ });
257
+ await recordPnpmPolicyOwnership(root, pnpmPolicyBefore);
231
258
  await validateInstalledPackageSet(root, discovered.candidates, previousManaged);
232
259
  const { catalog, modules } = await buildContext(root);
233
- const moduleInstall = await installModulesThin(catalog, modules, root);
260
+ const moduleInstall = await installModulesThin(catalog, modules, root, runtime.onProgress);
234
261
  return outcome("install", batchStatus(moduleInstall), root, {
235
262
  registry: discovered.registry,
236
263
  packageManager: mutation.packageManager,
@@ -271,7 +298,7 @@ async function workspaceProFlowDependencies(root) {
271
298
  async function handleUninstall(root, runtime) {
272
299
  const packageNames = await workspaceProFlowDependencies(root);
273
300
  const { catalog, modules } = await buildContext(root);
274
- const moduleUninstall = await uninstallModulesThin(catalog, modules, root);
301
+ const moduleUninstall = await uninstallModulesThin(catalog, modules, root, runtime.onProgress);
275
302
  if (!moduleUninstall.completed)
276
303
  return outcome("uninstall", batchStatus(moduleUninstall), root, {
277
304
  modules: moduleUninstall,
@@ -287,40 +314,39 @@ async function handleUninstall(root, runtime) {
287
314
  ? {}
288
315
  : { executableAvailable: runtime.executableAvailable }),
289
316
  });
317
+ const cleanedPnpmPolicy = await cleanOwnedPnpmPolicy(root);
290
318
  return outcome("uninstall", "SUCCEEDED", root, {
291
319
  modules: moduleUninstall,
292
320
  packageManager: mutation.packageManager,
293
321
  removed: packageNames,
322
+ cleanedPnpmPolicy,
294
323
  preserved: [".proflow"],
295
324
  });
296
325
  }
297
- async function handleSetup(root, parsed) {
326
+ async function handleSetup(root, parsed, runtime) {
298
327
  const { catalog, modules } = await buildContext(root);
299
328
  const target = parsed.moduleRef === undefined
300
329
  ? undefined
301
- : {
302
- moduleRef: parsed.moduleRef,
303
- ...(Object.hasOwn(parsed, "input") ? { input: parsed.input } : {}),
304
- };
305
- const result = await setupModulesThin(catalog, modules, root, target);
330
+ : { moduleRef: parsed.moduleRef };
331
+ const result = await setupModulesThin(catalog, modules, root, target, runtime.onProgress);
306
332
  return outcome("setup", batchStatus(result), root, result);
307
333
  }
308
- async function handleStart(root) {
334
+ async function handleStart(root, runtime) {
309
335
  const { catalog, modules } = await buildContext(root);
310
- const result = await startModulesThin(catalog, modules, root);
336
+ const result = await startModulesThin(catalog, modules, root, runtime.onProgress);
311
337
  return outcome("start", batchStatus(result), root, result);
312
338
  }
313
- async function handleStop(root) {
339
+ async function handleStop(root, runtime) {
314
340
  const { catalog, modules } = await buildContext(root);
315
- const result = await stopModulesThin(catalog, modules, root);
341
+ const result = await stopModulesThin(catalog, modules, root, runtime.onProgress);
316
342
  return outcome("stop", batchStatus(result), root, result);
317
343
  }
318
344
  function helpOutcome() {
319
345
  return outcome("help", "SUCCEEDED", undefined, {
320
- usage: "platform <install|uninstall|status|setup|docs|start|stop> [--workspace <path>] [--json]",
346
+ usage: "platform <install|uninstall|status|setup|docs|start|stop> [--workspace <path>]",
321
347
  commands: [...COMMANDS],
322
348
  install: "platform install [--workspace <path>]",
323
- setup: "platform setup [--workspace <path>] [--module <moduleRef> --input '<json>']",
349
+ setup: "platform setup [--workspace <path>] [--module <moduleRef>]",
324
350
  });
325
351
  }
326
352
  export async function runCli(argv, runtime = {}) {
@@ -329,35 +355,35 @@ export async function runCli(argv, runtime = {}) {
329
355
  parsed = parseArgs(argv);
330
356
  }
331
357
  catch (error) {
332
- return JSON.stringify(errorOutcome("unknown", error));
358
+ return errorOutcome("unknown", error);
333
359
  }
334
360
  try {
335
361
  if (parsed.command === "help")
336
- return JSON.stringify(helpOutcome());
362
+ return helpOutcome();
337
363
  if (parsed.command === "version")
338
- return JSON.stringify(outcome("version", "SUCCEEDED", undefined, {
364
+ return outcome("version", "SUCCEEDED", undefined, {
339
365
  version: platformCliDescriptor.moduleVersion,
340
- }));
366
+ });
341
367
  const root = await canonicalWorkspace(runtime.cwd ?? process.cwd(), parsed.workspace);
342
368
  switch (parsed.command) {
343
369
  case "install":
344
- return JSON.stringify(await handleInstall(root, runtime));
370
+ return await handleInstall(root, runtime);
345
371
  case "uninstall":
346
- return JSON.stringify(await handleUninstall(root, runtime));
372
+ return await handleUninstall(root, runtime);
347
373
  case "status":
348
- return JSON.stringify(await handleStatus(root));
374
+ return await handleStatus(root, runtime);
349
375
  case "setup":
350
- return JSON.stringify(await handleSetup(root, parsed));
376
+ return await handleSetup(root, parsed, runtime);
351
377
  case "docs":
352
- return JSON.stringify(await handleDocs(root));
378
+ return await handleDocs(root, runtime);
353
379
  case "start":
354
- return JSON.stringify(await handleStart(root));
380
+ return await handleStart(root, runtime);
355
381
  case "stop":
356
- return JSON.stringify(await handleStop(root));
382
+ return await handleStop(root, runtime);
357
383
  }
358
384
  }
359
385
  catch (error) {
360
- return JSON.stringify(errorOutcome(parsed.command, error));
386
+ return errorOutcome(parsed.command, error);
361
387
  }
362
388
  }
363
389
  function errorOutcome(command, error) {
@@ -379,28 +405,81 @@ function errorOutcome(command, error) {
379
405
  const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
380
406
  function renderStatus(data) {
381
407
  if (!isRecord(data) || !Array.isArray(data.modules))
382
- return "No modules.";
408
+ return "未发现模块。";
409
+ const setupLabels = {
410
+ READY: "已就绪",
411
+ ACTION_REQUIRED: "需要操作",
412
+ FAILED: "失败",
413
+ };
414
+ const runtimeLabels = {
415
+ RUNNING: "运行中",
416
+ STOPPED: "已停止",
417
+ FAILED: "运行失败",
418
+ NOT_APPLICABLE: "无独立进程",
419
+ };
383
420
  return [
384
- "ProFlow Modules",
421
+ "ProFlow 模块状态",
385
422
  "",
423
+ "模块 版本 配置状态 运行状态",
386
424
  ...data.modules
387
425
  .filter(isRecord)
388
- .map((raw) => `${String(raw.moduleRef)} ${String(raw.version)} setup=${String(raw.setupStatus)} runtime=${String(raw.runtimeStatus)}`),
426
+ .map((raw) => `${String(raw.moduleRef).padEnd(28)} ${String(raw.version).padEnd(10)} ${(setupLabels[String(raw.setupStatus)] ?? "未知").padEnd(10)} ${runtimeLabels[String(raw.runtimeStatus)] ?? "未知"}`),
427
+ "",
428
+ "需要操作:运行 platform setup",
429
+ "失败:运行 platform setup 查看原因和修复步骤",
389
430
  ].join("\n");
390
431
  }
391
432
  function renderDocs(data) {
392
433
  if (!isRecord(data) || !Array.isArray(data.modules))
393
- return "No module docs.";
394
- const lines = ["ProFlow Docs"];
434
+ return "未发现模块文档。";
435
+ const lines = ["ProFlow 模块文档"];
395
436
  for (const raw of data.modules)
396
437
  if (isRecord(raw))
397
- lines.push("", `## ${String(raw.moduleRef)} @ ${String(raw.version)}`, JSON.stringify(raw.docs ?? {}, null, 2));
438
+ lines.push("", `## ${String(raw.moduleRef)} @ ${String(raw.version)}`, String(raw.docs ?? ""));
439
+ if (Array.isArray(data.errors) && data.errors.length > 0)
440
+ lines.push("", "文档读取失败:", ...data.errors
441
+ .filter(isRecord)
442
+ .map((item) => `- ${String(item.moduleRef)}:${String(item.reason)}`));
398
443
  return lines.join("\n");
399
444
  }
445
+ const setupCommands = {
446
+ "chatgpt-carrier": {
447
+ ai: "proflow-chatgpt-carrier setup --carrier-url <url>",
448
+ inputs: "Custom GPT URL",
449
+ },
450
+ "dev-tunnel": {
451
+ ai: "proflow-dev-tunnel setup --tunnel-id <id> --public-base-url <url>",
452
+ inputs: "Tunnel ID、公开 HTTPS URL",
453
+ },
454
+ "model-provider-api": {
455
+ ai: "proflow-model-provider-api setup --provider-base-url <url>",
456
+ inputs: "模型服务 Base URL",
457
+ },
458
+ "model-runtime": {
459
+ ai: "proflow-model-runtime setup --fast-model <id> --reason-model <id>",
460
+ inputs: "FAST 模型 ID、REASON 模型 ID",
461
+ },
462
+ "execution-browser-extension": {
463
+ ai: "proflow-execution-browser-extension setup --extension-id <id>",
464
+ inputs: "Chrome Extension ID",
465
+ },
466
+ "agent-controller-dev": {
467
+ ai: "proflow-agent-controller-dev setup --carrier-url <url>",
468
+ inputs: "Custom GPT URL",
469
+ },
470
+ "agent-product": {
471
+ ai: "proflow-agent-product setup --carrier-url <url>",
472
+ inputs: "Custom GPT URL",
473
+ },
474
+ "agent-test-ops": {
475
+ ai: "proflow-agent-test-ops setup --carrier-url <url>",
476
+ inputs: "Custom GPT URL",
477
+ },
478
+ };
400
479
  function renderSetup(data) {
401
480
  if (!isRecord(data) || !Array.isArray(data.results))
402
- return "No setup actions are required.";
403
- const lines = ["ProFlow Setup", ""];
481
+ return "没有需要执行的配置步骤。";
482
+ const lines = ["ProFlow 配置", ""];
404
483
  let automatic = 0;
405
484
  let pending = 0;
406
485
  for (const raw of data.results) {
@@ -413,31 +492,52 @@ function renderSetup(data) {
413
492
  continue;
414
493
  }
415
494
  pending += 1;
416
- lines.push(`## ${moduleRef} — ${status}`);
417
- const actionRequired = raw.result.actionRequired;
495
+ lines.push(moduleRef);
496
+ const plan = moduleSetupPlanDataSchema.safeParse(raw.result.data);
497
+ if (plan.success) {
498
+ for (const [index, step] of plan.data.steps.entries()) {
499
+ lines.push(` [${step.state} ${index + 1}/${plan.data.steps.length}] ${step.title}`);
500
+ lines.push(` 人工执行:${step.execution.interactive}`);
501
+ lines.push(` AI 执行:${step.execution.nonInteractive}`);
502
+ if (step.requiredInputs.length > 0)
503
+ lines.push(` 需要输入:${step.requiredInputs.map((item) => item.description).join("、")}`);
504
+ lines.push(` 验证:${step.verify}`);
505
+ lines.push(` 完成条件:${step.successCondition}`);
506
+ if (step.blockedReason)
507
+ lines.push(` 原因:${step.blockedReason}`);
508
+ }
509
+ lines.push("");
510
+ continue;
511
+ }
512
+ lines.push(` [${status === "FAILED" ? "BLOCKED" : "TODO"} 1/1] ${status === "FAILED" ? "等待上游服务信息" : "完成模块配置"}`);
418
513
  const error = raw.result.error;
419
- if (isRecord(actionRequired)) {
420
- if (actionRequired.action !== undefined)
421
- lines.push(`Action: ${String(actionRequired.action)}`);
422
- if (actionRequired.description !== undefined)
423
- lines.push(String(actionRequired.description));
514
+ if (status !== "FAILED") {
515
+ const commands = setupCommands[moduleRef] ?? {
516
+ ai: `proflow-${moduleRef} setup`,
517
+ inputs: "按命令提示提供",
518
+ };
519
+ lines.push(` 人工执行:proflow-${moduleRef} setup`);
520
+ lines.push(` AI 执行:${commands.ai}`);
521
+ lines.push(` 需要输入:${commands.inputs}`);
522
+ lines.push(` 验证:proflow-${moduleRef} verify`);
523
+ lines.push(" 完成条件:配置状态变为“已就绪”");
424
524
  }
425
525
  else if (isRecord(error)) {
426
526
  const code = error.code === undefined ? "FAILED" : String(error.code);
427
527
  const message = error.message === undefined
428
528
  ? "Module setup failed."
429
529
  : String(error.message);
430
- lines.push(`Error: ${code} — ${message}`);
431
- }
432
- else if (raw.result.data !== undefined) {
433
- lines.push(JSON.stringify(raw.result.data, null, 2));
530
+ lines.push(` 原因:${code} — ${message}`);
531
+ lines.push(` 执行:platform setup --module ${moduleRef}`);
532
+ lines.push(" 完成条件:配置状态变为“已就绪”");
434
533
  }
435
534
  lines.push("");
436
535
  }
437
536
  if (pending === 0)
438
- lines.push("All discovered Modules are setup-ready.");
537
+ lines.push("全部模块均已就绪。 ");
439
538
  if (automatic > 0)
440
- lines.push(`Automatically completed in this run: ${automatic}`);
539
+ lines.push(`本次自动完成:${automatic} 个模块`);
540
+ lines.push(`汇总:${automatic} 个已就绪,${pending} 个待处理`);
441
541
  return lines.join("\n").trimEnd();
442
542
  }
443
543
  export function renderHumanResult(result) {
@@ -445,17 +545,38 @@ export function renderHumanResult(result) {
445
545
  isRecord(result.data) &&
446
546
  Array.isArray(result.data.results))
447
547
  return renderSetup(result.data);
448
- if (result.status === "FAILED")
449
- return `${result.command.toUpperCase()} FAILED${result.error ? ` [${result.error.code}] ${result.error.message}` : ""}`;
548
+ if (result.command === "start" && isRecord(result.data)) {
549
+ const blockers = Array.isArray(result.data.blockers)
550
+ ? result.data.blockers.filter(isRecord)
551
+ : [];
552
+ if (blockers.length > 0)
553
+ return [
554
+ "平台未启动:存在未就绪模块",
555
+ "",
556
+ ...blockers.map((item) => `${String(item.moduleRef).padEnd(28)} ${String(item.setupStatus) === "FAILED" ? "失败" : "需要操作"} 配置尚未完成`),
557
+ "",
558
+ `处理方式:platform setup${result.workspaceRoot ? ` --workspace "${result.workspaceRoot}"` : ""}`,
559
+ ].join("\n");
560
+ }
561
+ if (result.status === "FAILED" && result.error)
562
+ return `${result.command} 失败:${result.error.message}`;
450
563
  if (result.command === "help")
451
564
  return [
452
- "ProFlow Platform CLI",
565
+ "ProFlow 平台命令行",
453
566
  "",
454
- ...COMMANDS.map((command) => `platform ${command}`),
567
+ "platform install 安装并初始化全部 ProFlow 模块",
568
+ "platform uninstall 卸载模块包,保留 Workspace 数据",
569
+ "platform status 查看模块配置与运行状态",
570
+ "platform setup 自动配置并列出全部剩余步骤",
571
+ "platform docs 阅读模块能力文档",
572
+ "platform start 完成全量检查后启动平台",
573
+ "platform stop 按逆依赖顺序停止平台",
455
574
  "",
456
- "platform install --workspace <path>",
457
- "platform setup --module <moduleRef> --input '<json>'",
458
- "append --json for machine-readable output",
575
+ "常用参数:--workspace <路径>;setup 还支持 --module <模块名>",
576
+ "推荐流程:install → status → docs → setup start status → stop",
577
+ "人工配置示例:proflow-chatgpt-carrier setup",
578
+ "AI 配置示例:proflow-chatgpt-carrier setup --carrier-url <url>",
579
+ "状态图例:已就绪=配置完成;需要操作=需运行 setup;失败=存在系统阻塞;无独立进程=无需启动",
459
580
  ].join("\n");
460
581
  if (result.command === "version" && isRecord(result.data))
461
582
  return String(result.data.version ?? platformCliDescriptor.moduleVersion);
@@ -465,19 +586,36 @@ export function renderHumanResult(result) {
465
586
  return renderSetup(result.data);
466
587
  if (result.command === "docs")
467
588
  return renderDocs(result.data);
468
- return [
469
- `${result.command.toUpperCase()} ${result.status}`,
470
- ...(result.workspaceRoot ? [`Workspace: ${result.workspaceRoot}`] : []),
471
- ...(result.data === undefined
472
- ? []
473
- : [JSON.stringify(result.data, null, 2)]),
474
- ].join("\n");
589
+ if ((result.command === "start" || result.command === "stop") &&
590
+ isRecord(result.data)) {
591
+ const skipped = Array.isArray(result.data.skipped)
592
+ ? result.data.skipped.length
593
+ : 0;
594
+ const results = Array.isArray(result.data.results)
595
+ ? result.data.results.filter(isRecord)
596
+ : [];
597
+ const operations = results.filter((item) => item.command === result.command);
598
+ const succeeded = operations.filter((item) => isRecord(item.result) && item.result.status === "SUCCEEDED").length;
599
+ const failed = operations.find((item) => isRecord(item.result) && item.result.status !== "SUCCEEDED");
600
+ return [
601
+ `平台${result.command === "start" ? "启动" : "停止"}${failed ? "未完成" : "完成"}`,
602
+ `成功:${succeeded},跳过:${skipped}${failed ? `,失败:${String(failed.moduleRef)}` : ",失败:0"}`,
603
+ ].join("\n");
604
+ }
605
+ const labels = {
606
+ install: "安装",
607
+ uninstall: "卸载",
608
+ start: "启动",
609
+ stop: "停止",
610
+ };
611
+ return `${labels[result.command] ?? result.command}${result.status === "SUCCEEDED" ? "成功" : "未完成"}${result.workspaceRoot ? `\nWorkspace:${result.workspaceRoot}` : ""}`;
475
612
  }
476
613
  if (import.meta.main) {
477
614
  const argv = process.argv.slice(2);
478
- const output = await runCli(argv);
479
- const result = JSON.parse(output);
480
- process.stdout.write(`${argv.includes("--json") ? output : renderHumanResult(result)}\n`);
615
+ const result = await runCli(argv, {
616
+ onProgress: createTerminalProgressReporter(),
617
+ });
618
+ process.stdout.write(`${renderHumanResult(result)}\n`);
481
619
  if (result.status !== "SUCCEEDED")
482
620
  process.exitCode = 1;
483
621
  }
@@ -0,0 +1 @@
1
+ export { type CliOutcome, type CliRuntimeOptions, type CliStatus, runCli, } from "./cli.ts";
@@ -0,0 +1,3 @@
1
+ // Stable programmatic command surface. Terminal entrypoints must consume this
2
+ // typed outcome instead of parsing serialized stdout.
3
+ export { runCli, } from "./cli.js";
@@ -0,0 +1,3 @@
1
+ export declare function observeMinimumReleaseAgeExclude(root: string): Promise<string[]>;
2
+ export declare function recordPnpmPolicyOwnership(root: string, before: readonly string[]): Promise<void>;
3
+ export declare function cleanOwnedPnpmPolicy(root: string): Promise<string[]>;
@@ -0,0 +1,68 @@
1
+ import { mkdir, readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { atomicWrite } from "../paths.js";
4
+ const policyFileName = "pnpm-workspace.yaml";
5
+ const ownershipFile = (root) => join(root, ".proflow", "deployment", "pnpm-policy-ownership.json");
6
+ async function text(root) {
7
+ try {
8
+ return await readFile(join(root, policyFileName), "utf8");
9
+ }
10
+ catch {
11
+ return undefined;
12
+ }
13
+ }
14
+ export async function observeMinimumReleaseAgeExclude(root) {
15
+ const source = await text(root);
16
+ if (!source)
17
+ return [];
18
+ const lines = source.split(/\r?\n/);
19
+ const start = lines.findIndex((line) => /^minimumReleaseAgeExclude:\s*$/.test(line));
20
+ if (start < 0)
21
+ return [];
22
+ const values = [];
23
+ for (let index = start + 1; index < lines.length; index += 1) {
24
+ const line = lines[index] ?? "";
25
+ if (/^\S/.test(line) && line.trim() !== "")
26
+ break;
27
+ const match = line.match(/^\s+-\s+['"]?([^'"]+)['"]?\s*$/);
28
+ if (match?.[1])
29
+ values.push(match[1]);
30
+ }
31
+ return values;
32
+ }
33
+ export async function recordPnpmPolicyOwnership(root, before) {
34
+ const after = await observeMinimumReleaseAgeExclude(root);
35
+ const introduced = after.filter((value) => !before.includes(value));
36
+ if (introduced.length === 0)
37
+ return;
38
+ await mkdir(join(root, ".proflow", "deployment"), { recursive: true });
39
+ await atomicWrite(ownershipFile(root), `${JSON.stringify({ contract: "proflow.pnpm-policy-ownership.v1", introduced }, null, 2)}\n`);
40
+ }
41
+ export async function cleanOwnedPnpmPolicy(root) {
42
+ let introduced;
43
+ try {
44
+ const parsed = JSON.parse(await readFile(ownershipFile(root), "utf8"));
45
+ introduced =
46
+ typeof parsed === "object" &&
47
+ parsed !== null &&
48
+ Array.isArray(Reflect.get(parsed, "introduced"))
49
+ ? Reflect.get(parsed, "introduced").filter((value) => typeof value === "string")
50
+ : [];
51
+ }
52
+ catch {
53
+ return [];
54
+ }
55
+ if (introduced.length === 0)
56
+ return [];
57
+ const source = await text(root);
58
+ if (!source)
59
+ return [];
60
+ const owned = new Set(introduced);
61
+ const lines = source.split(/\r?\n/);
62
+ const next = lines.filter((line) => {
63
+ const match = line.match(/^\s+-\s+['"]?([^'"]+)['"]?\s*$/);
64
+ return !match?.[1] || !owned.has(match[1]);
65
+ });
66
+ await atomicWrite(join(root, policyFileName), next.join("\n"));
67
+ return introduced;
68
+ }
@@ -1,6 +1,7 @@
1
1
  import { type ModuleSetupStatus } from "@tomflow/proflow-module-contract";
2
2
  import type { ResolvedModule } from "../contracts.ts";
3
3
  import type { ModuleCatalog } from "../modules.ts";
4
+ import { type PlatformProgressReporter } from "../progress.ts";
4
5
  import { type ModuleDispatchResult } from "./dispatch.ts";
5
6
  export interface ModuleBatchResult {
6
7
  phase: "install" | "uninstall" | "setup" | "start" | "stop";
@@ -10,14 +11,22 @@ export interface ModuleBatchResult {
10
11
  moduleRef: string;
11
12
  setupStatus: ModuleSetupStatus;
12
13
  };
14
+ blockers?: Array<{
15
+ moduleRef: string;
16
+ setupStatus: ModuleSetupStatus;
17
+ }>;
18
+ skipped?: Array<{
19
+ moduleRef: string;
20
+ reason: "RUNNING" | "STOPPED" | "NOT_APPLICABLE";
21
+ }>;
13
22
  }
14
- export declare function observeStatuses(catalog: ModuleCatalog, modules: readonly ResolvedModule[], workspaceRoot: string): Promise<ModuleDispatchResult[]>;
15
- export declare function observeDocs(catalog: ModuleCatalog, modules: readonly ResolvedModule[], workspaceRoot: string): Promise<ModuleDispatchResult[]>;
16
- export declare const installModulesThin: (catalog: ModuleCatalog, modules: readonly ResolvedModule[], workspaceRoot: string) => Promise<ModuleBatchResult>;
17
- export declare const uninstallModulesThin: (catalog: ModuleCatalog, modules: readonly ResolvedModule[], workspaceRoot: string) => Promise<ModuleBatchResult>;
18
- export declare const stopModulesThin: (catalog: ModuleCatalog, modules: readonly ResolvedModule[], workspaceRoot: string) => Promise<ModuleBatchResult>;
23
+ export declare function observeStatuses(catalog: ModuleCatalog, modules: readonly ResolvedModule[], workspaceRoot: string, reporter?: PlatformProgressReporter): Promise<ModuleDispatchResult[]>;
24
+ export declare function observeDocs(catalog: ModuleCatalog, modules: readonly ResolvedModule[], workspaceRoot: string, reporter?: PlatformProgressReporter): Promise<ModuleDispatchResult[]>;
25
+ export declare const installModulesThin: (catalog: ModuleCatalog, modules: readonly ResolvedModule[], workspaceRoot: string, reporter?: PlatformProgressReporter) => Promise<ModuleBatchResult>;
26
+ export declare const uninstallModulesThin: (catalog: ModuleCatalog, modules: readonly ResolvedModule[], workspaceRoot: string, reporter?: PlatformProgressReporter) => Promise<ModuleBatchResult>;
27
+ export declare function stopModulesThin(catalog: ModuleCatalog, modules: readonly ResolvedModule[], workspaceRoot: string, reporter?: PlatformProgressReporter): Promise<ModuleBatchResult>;
19
28
  export declare function setupModulesThin(catalog: ModuleCatalog, modules: readonly ResolvedModule[], workspaceRoot: string, target?: {
20
29
  moduleRef: string;
21
30
  input?: unknown;
22
- }): Promise<ModuleBatchResult>;
23
- export declare function startModulesThin(catalog: ModuleCatalog, modules: readonly ResolvedModule[], workspaceRoot: string): Promise<ModuleBatchResult>;
31
+ }, reporter?: PlatformProgressReporter): Promise<ModuleBatchResult>;
32
+ export declare function startModulesThin(catalog: ModuleCatalog, modules: readonly ResolvedModule[], workspaceRoot: string, reporter?: PlatformProgressReporter): Promise<ModuleBatchResult>;
@@ -1,6 +1,7 @@
1
1
  import { moduleStatusObservationSchema, } from "@tomflow/proflow-module-contract";
2
2
  import { PlatformError } from "../errors.js";
3
3
  import { buildDependencyGraph } from "../graph/graph.js";
4
+ import { reportProgress } from "../progress.js";
4
5
  import { dispatchModuleCommand, } from "./dispatch.js";
5
6
  const succeeded = (result) => result.status === "SUCCEEDED";
6
7
  const context = (workspaceRoot, input) => input === undefined ? { workspaceRoot } : { workspaceRoot, input };
@@ -12,38 +13,160 @@ function ordered(modules, reverse = false) {
12
13
  .map((ref) => byRef.get(ref))
13
14
  .filter((item) => item !== undefined);
14
15
  }
15
- export async function observeStatuses(catalog, modules, workspaceRoot) {
16
+ export async function observeStatuses(catalog, modules, workspaceRoot, reporter) {
16
17
  const results = [];
17
- for (const module of [...modules].sort((a, b) => a.moduleRef.localeCompare(b.moduleRef)))
18
- results.push(await dispatchModuleCommand(catalog, module, "status", context(workspaceRoot)));
18
+ const modulesInOrder = [...modules].sort((a, b) => a.moduleRef.localeCompare(b.moduleRef));
19
+ for (const [index, module] of modulesInOrder.entries()) {
20
+ reportProgress(reporter, {
21
+ command: "status",
22
+ phase: "status",
23
+ current: index + 1,
24
+ total: modulesInOrder.length,
25
+ moduleRef: module.moduleRef,
26
+ status: "STARTED",
27
+ message: module.moduleRef,
28
+ });
29
+ const result = await dispatchModuleCommand(catalog, module, "status", context(workspaceRoot));
30
+ results.push(result);
31
+ reportProgress(reporter, {
32
+ command: "status",
33
+ phase: "status",
34
+ current: index + 1,
35
+ total: modulesInOrder.length,
36
+ moduleRef: module.moduleRef,
37
+ status: succeeded(result.result) ? "SUCCEEDED" : "FAILED",
38
+ message: module.moduleRef,
39
+ });
40
+ }
19
41
  return results;
20
42
  }
21
- export async function observeDocs(catalog, modules, workspaceRoot) {
43
+ export async function observeDocs(catalog, modules, workspaceRoot, reporter) {
22
44
  const results = [];
23
- for (const module of [...modules].sort((a, b) => a.moduleRef.localeCompare(b.moduleRef)))
24
- results.push(await dispatchModuleCommand(catalog, module, "docs", context(workspaceRoot)));
45
+ const modulesInOrder = [...modules].sort((a, b) => a.moduleRef.localeCompare(b.moduleRef));
46
+ for (const [index, module] of modulesInOrder.entries()) {
47
+ reportProgress(reporter, {
48
+ command: "docs",
49
+ phase: "docs",
50
+ current: index + 1,
51
+ total: modulesInOrder.length,
52
+ moduleRef: module.moduleRef,
53
+ status: "STARTED",
54
+ message: module.moduleRef,
55
+ });
56
+ const result = await dispatchModuleCommand(catalog, module, "docs", context(workspaceRoot));
57
+ results.push(result);
58
+ reportProgress(reporter, {
59
+ command: "docs",
60
+ phase: "docs",
61
+ current: index + 1,
62
+ total: modulesInOrder.length,
63
+ moduleRef: module.moduleRef,
64
+ status: succeeded(result.result) ? "SUCCEEDED" : "FAILED",
65
+ message: module.moduleRef,
66
+ });
67
+ }
25
68
  return results;
26
69
  }
27
- async function runOrdered(catalog, modules, workspaceRoot, command, reverse) {
70
+ async function runOrdered(catalog, modules, workspaceRoot, command, reverse, reporter) {
28
71
  const results = [];
29
- for (const module of ordered(modules, reverse)) {
72
+ const modulesInOrder = ordered(modules, reverse);
73
+ for (const [index, module] of modulesInOrder.entries()) {
74
+ reportProgress(reporter, {
75
+ command,
76
+ phase: command,
77
+ current: index + 1,
78
+ total: modulesInOrder.length,
79
+ moduleRef: module.moduleRef,
80
+ status: "STARTED",
81
+ message: `${module.moduleRef}`,
82
+ });
30
83
  const result = await dispatchModuleCommand(catalog, module, command, context(workspaceRoot));
31
84
  results.push(result);
85
+ reportProgress(reporter, {
86
+ command,
87
+ phase: command,
88
+ current: index + 1,
89
+ total: modulesInOrder.length,
90
+ moduleRef: module.moduleRef,
91
+ status: succeeded(result.result) ? "SUCCEEDED" : "FAILED",
92
+ message: `${module.moduleRef}`,
93
+ });
32
94
  if (!succeeded(result.result))
33
95
  return { phase: command, results, completed: false };
34
96
  }
35
97
  return { phase: command, results, completed: true };
36
98
  }
37
- export const installModulesThin = (catalog, modules, workspaceRoot) => runOrdered(catalog, modules, workspaceRoot, "install", false);
38
- export const uninstallModulesThin = (catalog, modules, workspaceRoot) => runOrdered(catalog, modules, workspaceRoot, "uninstall", true);
39
- export const stopModulesThin = (catalog, modules, workspaceRoot) => runOrdered(catalog, modules, workspaceRoot, "stop", true);
40
- export async function setupModulesThin(catalog, modules, workspaceRoot, target) {
99
+ export const installModulesThin = (catalog, modules, workspaceRoot, reporter) => runOrdered(catalog, modules, workspaceRoot, "install", false, reporter);
100
+ export const uninstallModulesThin = (catalog, modules, workspaceRoot, reporter) => runOrdered(catalog, modules, workspaceRoot, "uninstall", true, reporter);
101
+ export async function stopModulesThin(catalog, modules, workspaceRoot, reporter) {
102
+ const results = [];
103
+ const skipped = [];
104
+ const modulesInOrder = ordered(modules, true);
105
+ for (const [index, module] of modulesInOrder.entries()) {
106
+ reportProgress(reporter, {
107
+ command: "stop",
108
+ phase: "status",
109
+ current: index + 1,
110
+ total: modulesInOrder.length,
111
+ moduleRef: module.moduleRef,
112
+ status: "STARTED",
113
+ message: `${module.moduleRef}`,
114
+ });
115
+ const status = await dispatchModuleCommand(catalog, module, "status", context(workspaceRoot));
116
+ results.push(status);
117
+ if (!succeeded(status.result))
118
+ return { phase: "stop", results, completed: false, skipped };
119
+ const observed = moduleStatusObservationSchema.parse(status.result.data);
120
+ if (observed.runtimeStatus === "STOPPED" ||
121
+ observed.runtimeStatus === "NOT_APPLICABLE") {
122
+ skipped.push({
123
+ moduleRef: module.moduleRef,
124
+ reason: observed.runtimeStatus,
125
+ });
126
+ reportProgress(reporter, {
127
+ command: "stop",
128
+ phase: "stop",
129
+ current: index + 1,
130
+ total: modulesInOrder.length,
131
+ moduleRef: module.moduleRef,
132
+ status: "SKIPPED",
133
+ message: `${module.moduleRef}`,
134
+ });
135
+ continue;
136
+ }
137
+ const stopped = await dispatchModuleCommand(catalog, module, "stop", context(workspaceRoot));
138
+ results.push(stopped);
139
+ reportProgress(reporter, {
140
+ command: "stop",
141
+ phase: "stop",
142
+ current: index + 1,
143
+ total: modulesInOrder.length,
144
+ moduleRef: module.moduleRef,
145
+ status: succeeded(stopped.result) ? "SUCCEEDED" : "FAILED",
146
+ message: `${module.moduleRef}`,
147
+ });
148
+ if (!succeeded(stopped.result))
149
+ return { phase: "stop", results, completed: false, skipped };
150
+ }
151
+ return { phase: "stop", results, completed: true, skipped };
152
+ }
153
+ export async function setupModulesThin(catalog, modules, workspaceRoot, target, reporter) {
41
154
  const results = [];
42
155
  let matched = target === undefined;
43
156
  let completed = true;
44
- for (const module of ordered(modules)) {
157
+ const modulesInOrder = ordered(modules);
158
+ for (const [index, module] of modulesInOrder.entries()) {
45
159
  if (target !== undefined && module.moduleRef !== target.moduleRef)
46
160
  continue;
161
+ reportProgress(reporter, {
162
+ command: "setup",
163
+ phase: "setup",
164
+ current: index + 1,
165
+ total: modulesInOrder.length,
166
+ moduleRef: module.moduleRef,
167
+ status: "STARTED",
168
+ message: module.moduleRef,
169
+ });
47
170
  matched = true;
48
171
  const status = await dispatchModuleCommand(catalog, module, "status", context(workspaceRoot));
49
172
  if (!succeeded(status.result)) {
@@ -54,10 +177,33 @@ export async function setupModulesThin(catalog, modules, workspaceRoot, target)
54
177
  continue;
55
178
  }
56
179
  const observed = moduleStatusObservationSchema.parse(status.result.data);
57
- if (observed.setupStatus === "READY" && target?.input === undefined)
180
+ if (observed.setupStatus === "READY" && target?.input === undefined) {
181
+ reportProgress(reporter, {
182
+ command: "setup",
183
+ phase: "setup",
184
+ current: index + 1,
185
+ total: modulesInOrder.length,
186
+ moduleRef: module.moduleRef,
187
+ status: "SKIPPED",
188
+ message: module.moduleRef,
189
+ });
58
190
  continue;
191
+ }
59
192
  const setup = await dispatchModuleCommand(catalog, module, "setup", context(workspaceRoot, target?.input));
60
193
  results.push(setup);
194
+ reportProgress(reporter, {
195
+ command: "setup",
196
+ phase: "setup",
197
+ current: index + 1,
198
+ total: modulesInOrder.length,
199
+ moduleRef: module.moduleRef,
200
+ status: succeeded(setup.result)
201
+ ? "SUCCEEDED"
202
+ : setup.result.status === "ACTION_REQUIRED"
203
+ ? "SKIPPED"
204
+ : "FAILED",
205
+ message: module.moduleRef,
206
+ });
61
207
  if (!succeeded(setup.result)) {
62
208
  completed = false;
63
209
  if (target !== undefined)
@@ -68,34 +214,71 @@ export async function setupModulesThin(catalog, modules, workspaceRoot, target)
68
214
  throw new PlatformError("INVALID_REQUEST", `setup target module ${target?.moduleRef ?? ""} was not discovered`);
69
215
  return { phase: "setup", results, completed };
70
216
  }
71
- export async function startModulesThin(catalog, modules, workspaceRoot) {
217
+ export async function startModulesThin(catalog, modules, workspaceRoot, reporter) {
72
218
  const results = [];
73
219
  const modulesInOrder = ordered(modules);
74
- for (const module of modulesInOrder) {
220
+ const blockers = [];
221
+ const runtimeByRef = new Map();
222
+ for (const [index, module] of modulesInOrder.entries()) {
223
+ reportProgress(reporter, {
224
+ command: "start",
225
+ phase: "status",
226
+ current: index + 1,
227
+ total: modulesInOrder.length,
228
+ moduleRef: module.moduleRef,
229
+ status: "STARTED",
230
+ message: `${module.moduleRef}`,
231
+ });
75
232
  const status = await dispatchModuleCommand(catalog, module, "status", context(workspaceRoot));
76
- if (!succeeded(status.result))
77
- return {
78
- phase: "start",
79
- results: [...results, status],
80
- completed: false,
81
- };
233
+ results.push(status);
234
+ if (!succeeded(status.result)) {
235
+ blockers.push({ moduleRef: module.moduleRef, setupStatus: "FAILED" });
236
+ continue;
237
+ }
82
238
  const observed = moduleStatusObservationSchema.parse(status.result.data);
239
+ runtimeByRef.set(module.moduleRef, observed.runtimeStatus);
83
240
  if (observed.setupStatus !== "READY")
84
- return {
85
- phase: "start",
86
- results: [...results, status],
87
- completed: false,
88
- blockedBy: {
89
- moduleRef: module.moduleRef,
90
- setupStatus: observed.setupStatus,
91
- },
92
- };
241
+ blockers.push({
242
+ moduleRef: module.moduleRef,
243
+ setupStatus: observed.setupStatus,
244
+ });
93
245
  }
94
- for (const module of modulesInOrder) {
246
+ if (results.some((item) => !succeeded(item.result)) || blockers.length > 0)
247
+ return {
248
+ phase: "start",
249
+ results,
250
+ completed: false,
251
+ ...(blockers[0] ? { blockedBy: blockers[0], blockers } : {}),
252
+ };
253
+ const skipped = [];
254
+ for (const [index, module] of modulesInOrder.entries()) {
255
+ const runtimeStatus = runtimeByRef.get(module.moduleRef);
256
+ if (runtimeStatus === "RUNNING" || runtimeStatus === "NOT_APPLICABLE") {
257
+ skipped.push({ moduleRef: module.moduleRef, reason: runtimeStatus });
258
+ reportProgress(reporter, {
259
+ command: "start",
260
+ phase: "start",
261
+ current: index + 1,
262
+ total: modulesInOrder.length,
263
+ moduleRef: module.moduleRef,
264
+ status: "SKIPPED",
265
+ message: `${module.moduleRef}`,
266
+ });
267
+ continue;
268
+ }
95
269
  const started = await dispatchModuleCommand(catalog, module, "start", context(workspaceRoot));
96
270
  results.push(started);
271
+ reportProgress(reporter, {
272
+ command: "start",
273
+ phase: "start",
274
+ current: index + 1,
275
+ total: modulesInOrder.length,
276
+ moduleRef: module.moduleRef,
277
+ status: succeeded(started.result) ? "SUCCEEDED" : "FAILED",
278
+ message: `${module.moduleRef}`,
279
+ });
97
280
  if (!succeeded(started.result))
98
- return { phase: "start", results, completed: false };
281
+ return { phase: "start", results, completed: false, skipped };
99
282
  }
100
- return { phase: "start", results, completed: true };
283
+ return { phase: "start", results, completed: true, skipped };
101
284
  }
@@ -0,0 +1,12 @@
1
+ export type PlatformProgressStatus = "STARTED" | "SUCCEEDED" | "ACTION_REQUIRED" | "FAILED" | "SKIPPED";
2
+ export interface PlatformProgressEvent {
3
+ command: string;
4
+ phase: string;
5
+ current?: number;
6
+ total?: number;
7
+ moduleRef?: string;
8
+ status: PlatformProgressStatus;
9
+ message: string;
10
+ }
11
+ export type PlatformProgressReporter = (event: PlatformProgressEvent) => void;
12
+ export declare const reportProgress: (reporter: PlatformProgressReporter | undefined, event: PlatformProgressEvent) => void | undefined;
@@ -0,0 +1 @@
1
+ export const reportProgress = (reporter, event) => reporter?.(event);
@@ -0,0 +1,2 @@
1
+ import type { PlatformProgressReporter } from "./progress.ts";
2
+ export declare function createTerminalProgressReporter(stream?: NodeJS.WriteStream): PlatformProgressReporter;
@@ -0,0 +1,44 @@
1
+ import { clearLine, cursorTo } from "node:readline";
2
+ const colors = {
3
+ green: "\u001b[32m",
4
+ red: "\u001b[31m",
5
+ yellow: "\u001b[33m",
6
+ dim: "\u001b[2m",
7
+ reset: "\u001b[0m",
8
+ };
9
+ function progressLine(event, color) {
10
+ const prefix = event.current && event.total
11
+ ? `[${String(event.current).padStart(2, "0")}/${event.total}] `
12
+ : "";
13
+ const suffix = event.status === "SUCCEEDED"
14
+ ? "完成"
15
+ : event.status === "FAILED"
16
+ ? "失败"
17
+ : event.status === "SKIPPED"
18
+ ? "跳过"
19
+ : "";
20
+ if (!color || suffix === "")
21
+ return `${prefix}${event.message}${suffix ? `… ${suffix}` : "…"}`;
22
+ const tone = event.status === "SUCCEEDED"
23
+ ? colors.green
24
+ : event.status === "FAILED"
25
+ ? colors.red
26
+ : colors.yellow;
27
+ return `${colors.dim}${prefix}${colors.reset}${event.message}… ${tone}${suffix}${colors.reset}`;
28
+ }
29
+ export function createTerminalProgressReporter(stream = process.stderr) {
30
+ const interactive = stream.isTTY === true;
31
+ const color = interactive && !("NO_COLOR" in process.env);
32
+ return (event) => {
33
+ const line = progressLine(event, color);
34
+ if (!interactive) {
35
+ stream.write(`${line}\n`);
36
+ return;
37
+ }
38
+ cursorTo(stream, 0);
39
+ clearLine(stream, 0);
40
+ stream.write(line);
41
+ if (event.status !== "STARTED")
42
+ stream.write("\n");
43
+ };
44
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tomflow/proflow-platform-cli",
3
- "version": "0.1.21",
3
+ "version": "0.1.23",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -8,6 +8,7 @@
8
8
  "exports": {
9
9
  ".": "./dist/src/index.js",
10
10
  "./cli": "./dist/src/cli.js",
11
+ "./command": "./dist/src/command.js",
11
12
  "./deployment/adapter": "./dist/deployment/adapter.js",
12
13
  "./deployment/descriptor": "./dist/deployment/descriptor.js"
13
14
  },
@@ -23,11 +24,11 @@
23
24
  "SETUP.md"
24
25
  ],
25
26
  "dependencies": {
26
- "@tomflow/proflow-module-contract": "^0.1.7"
27
+ "@tomflow/proflow-module-contract": "^0.1.9"
27
28
  },
28
29
  "devDependencies": {
29
- "@tomflow/proflow-deployment-conformance": "^0.1.5",
30
- "@tomflow/proflow-module-template": "^0.1.5"
30
+ "@tomflow/proflow-deployment-conformance": "^0.1.7",
31
+ "@tomflow/proflow-module-template": "^0.1.7"
31
32
  },
32
33
  "description": "Thin Platform CLI for Module discovery, documentation, package synchronization and lifecycle orchestration.",
33
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.21",
6
+ "moduleVersion": "0.1.23",
7
7
  "kind": "cli",
8
8
  "templateVersion": "1.0.0",
9
9
  "platformCompatibility": ">=1.0.0 <2.0.0",