@tomflow/proflow-platform-cli 0.1.21 → 0.1.22

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.22
4
+
5
+ ### Patch Changes
6
+
7
+ - 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.
8
+
3
9
  ## 0.1.21
4
10
 
5
11
  ### 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.22";
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.22";
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.22";
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.22";
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.22";
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.22";
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.22";
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("../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.22";
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.22",
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,7 @@ 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);
218
232
  const mutation = await syncWorkspacePackages({
219
233
  workspaceRoot: root,
220
234
  packages: discovered.candidates.map((item) => ({
@@ -228,9 +242,16 @@ async function handleInstall(root, runtime) {
228
242
  ? {}
229
243
  : { executableAvailable: runtime.executableAvailable }),
230
244
  });
245
+ reportProgress(runtime.onProgress, {
246
+ command: "install",
247
+ phase: "packages",
248
+ status: "SUCCEEDED",
249
+ message: "依赖同步完成",
250
+ });
251
+ await recordPnpmPolicyOwnership(root, pnpmPolicyBefore);
231
252
  await validateInstalledPackageSet(root, discovered.candidates, previousManaged);
232
253
  const { catalog, modules } = await buildContext(root);
233
- const moduleInstall = await installModulesThin(catalog, modules, root);
254
+ const moduleInstall = await installModulesThin(catalog, modules, root, runtime.onProgress);
234
255
  return outcome("install", batchStatus(moduleInstall), root, {
235
256
  registry: discovered.registry,
236
257
  packageManager: mutation.packageManager,
@@ -271,7 +292,7 @@ async function workspaceProFlowDependencies(root) {
271
292
  async function handleUninstall(root, runtime) {
272
293
  const packageNames = await workspaceProFlowDependencies(root);
273
294
  const { catalog, modules } = await buildContext(root);
274
- const moduleUninstall = await uninstallModulesThin(catalog, modules, root);
295
+ const moduleUninstall = await uninstallModulesThin(catalog, modules, root, runtime.onProgress);
275
296
  if (!moduleUninstall.completed)
276
297
  return outcome("uninstall", batchStatus(moduleUninstall), root, {
277
298
  modules: moduleUninstall,
@@ -287,40 +308,39 @@ async function handleUninstall(root, runtime) {
287
308
  ? {}
288
309
  : { executableAvailable: runtime.executableAvailable }),
289
310
  });
311
+ const cleanedPnpmPolicy = await cleanOwnedPnpmPolicy(root);
290
312
  return outcome("uninstall", "SUCCEEDED", root, {
291
313
  modules: moduleUninstall,
292
314
  packageManager: mutation.packageManager,
293
315
  removed: packageNames,
316
+ cleanedPnpmPolicy,
294
317
  preserved: [".proflow"],
295
318
  });
296
319
  }
297
- async function handleSetup(root, parsed) {
320
+ async function handleSetup(root, parsed, runtime) {
298
321
  const { catalog, modules } = await buildContext(root);
299
322
  const target = parsed.moduleRef === undefined
300
323
  ? undefined
301
- : {
302
- moduleRef: parsed.moduleRef,
303
- ...(Object.hasOwn(parsed, "input") ? { input: parsed.input } : {}),
304
- };
305
- const result = await setupModulesThin(catalog, modules, root, target);
324
+ : { moduleRef: parsed.moduleRef };
325
+ const result = await setupModulesThin(catalog, modules, root, target, runtime.onProgress);
306
326
  return outcome("setup", batchStatus(result), root, result);
307
327
  }
308
- async function handleStart(root) {
328
+ async function handleStart(root, runtime) {
309
329
  const { catalog, modules } = await buildContext(root);
310
- const result = await startModulesThin(catalog, modules, root);
330
+ const result = await startModulesThin(catalog, modules, root, runtime.onProgress);
311
331
  return outcome("start", batchStatus(result), root, result);
312
332
  }
313
- async function handleStop(root) {
333
+ async function handleStop(root, runtime) {
314
334
  const { catalog, modules } = await buildContext(root);
315
- const result = await stopModulesThin(catalog, modules, root);
335
+ const result = await stopModulesThin(catalog, modules, root, runtime.onProgress);
316
336
  return outcome("stop", batchStatus(result), root, result);
317
337
  }
318
338
  function helpOutcome() {
319
339
  return outcome("help", "SUCCEEDED", undefined, {
320
- usage: "platform <install|uninstall|status|setup|docs|start|stop> [--workspace <path>] [--json]",
340
+ usage: "platform <install|uninstall|status|setup|docs|start|stop> [--workspace <path>]",
321
341
  commands: [...COMMANDS],
322
342
  install: "platform install [--workspace <path>]",
323
- setup: "platform setup [--workspace <path>] [--module <moduleRef> --input '<json>']",
343
+ setup: "platform setup [--workspace <path>] [--module <moduleRef>]",
324
344
  });
325
345
  }
326
346
  export async function runCli(argv, runtime = {}) {
@@ -329,35 +349,35 @@ export async function runCli(argv, runtime = {}) {
329
349
  parsed = parseArgs(argv);
330
350
  }
331
351
  catch (error) {
332
- return JSON.stringify(errorOutcome("unknown", error));
352
+ return errorOutcome("unknown", error);
333
353
  }
334
354
  try {
335
355
  if (parsed.command === "help")
336
- return JSON.stringify(helpOutcome());
356
+ return helpOutcome();
337
357
  if (parsed.command === "version")
338
- return JSON.stringify(outcome("version", "SUCCEEDED", undefined, {
358
+ return outcome("version", "SUCCEEDED", undefined, {
339
359
  version: platformCliDescriptor.moduleVersion,
340
- }));
360
+ });
341
361
  const root = await canonicalWorkspace(runtime.cwd ?? process.cwd(), parsed.workspace);
342
362
  switch (parsed.command) {
343
363
  case "install":
344
- return JSON.stringify(await handleInstall(root, runtime));
364
+ return await handleInstall(root, runtime);
345
365
  case "uninstall":
346
- return JSON.stringify(await handleUninstall(root, runtime));
366
+ return await handleUninstall(root, runtime);
347
367
  case "status":
348
- return JSON.stringify(await handleStatus(root));
368
+ return await handleStatus(root, runtime);
349
369
  case "setup":
350
- return JSON.stringify(await handleSetup(root, parsed));
370
+ return await handleSetup(root, parsed, runtime);
351
371
  case "docs":
352
- return JSON.stringify(await handleDocs(root));
372
+ return await handleDocs(root, runtime);
353
373
  case "start":
354
- return JSON.stringify(await handleStart(root));
374
+ return await handleStart(root, runtime);
355
375
  case "stop":
356
- return JSON.stringify(await handleStop(root));
376
+ return await handleStop(root, runtime);
357
377
  }
358
378
  }
359
379
  catch (error) {
360
- return JSON.stringify(errorOutcome(parsed.command, error));
380
+ return errorOutcome(parsed.command, error);
361
381
  }
362
382
  }
363
383
  function errorOutcome(command, error) {
@@ -379,28 +399,81 @@ function errorOutcome(command, error) {
379
399
  const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
380
400
  function renderStatus(data) {
381
401
  if (!isRecord(data) || !Array.isArray(data.modules))
382
- return "No modules.";
402
+ return "未发现模块。";
403
+ const setupLabels = {
404
+ READY: "已就绪",
405
+ ACTION_REQUIRED: "需要操作",
406
+ FAILED: "失败",
407
+ };
408
+ const runtimeLabels = {
409
+ RUNNING: "运行中",
410
+ STOPPED: "已停止",
411
+ FAILED: "运行失败",
412
+ NOT_APPLICABLE: "无独立进程",
413
+ };
383
414
  return [
384
- "ProFlow Modules",
415
+ "ProFlow 模块状态",
385
416
  "",
417
+ "模块 版本 配置状态 运行状态",
386
418
  ...data.modules
387
419
  .filter(isRecord)
388
- .map((raw) => `${String(raw.moduleRef)} ${String(raw.version)} setup=${String(raw.setupStatus)} runtime=${String(raw.runtimeStatus)}`),
420
+ .map((raw) => `${String(raw.moduleRef).padEnd(28)} ${String(raw.version).padEnd(10)} ${(setupLabels[String(raw.setupStatus)] ?? "未知").padEnd(10)} ${runtimeLabels[String(raw.runtimeStatus)] ?? "未知"}`),
421
+ "",
422
+ "需要操作:运行 platform setup",
423
+ "失败:运行 platform setup 查看原因和修复步骤",
389
424
  ].join("\n");
390
425
  }
391
426
  function renderDocs(data) {
392
427
  if (!isRecord(data) || !Array.isArray(data.modules))
393
- return "No module docs.";
394
- const lines = ["ProFlow Docs"];
428
+ return "未发现模块文档。";
429
+ const lines = ["ProFlow 模块文档"];
395
430
  for (const raw of data.modules)
396
431
  if (isRecord(raw))
397
- lines.push("", `## ${String(raw.moduleRef)} @ ${String(raw.version)}`, JSON.stringify(raw.docs ?? {}, null, 2));
432
+ lines.push("", `## ${String(raw.moduleRef)} @ ${String(raw.version)}`, String(raw.docs ?? ""));
433
+ if (Array.isArray(data.errors) && data.errors.length > 0)
434
+ lines.push("", "文档读取失败:", ...data.errors
435
+ .filter(isRecord)
436
+ .map((item) => `- ${String(item.moduleRef)}:${String(item.reason)}`));
398
437
  return lines.join("\n");
399
438
  }
439
+ const setupCommands = {
440
+ "chatgpt-carrier": {
441
+ ai: "proflow-chatgpt-carrier setup --carrier-url <url>",
442
+ inputs: "Custom GPT URL",
443
+ },
444
+ "dev-tunnel": {
445
+ ai: "proflow-dev-tunnel setup --tunnel-id <id> --public-base-url <url>",
446
+ inputs: "Tunnel ID、公开 HTTPS URL",
447
+ },
448
+ "model-provider-api": {
449
+ ai: "proflow-model-provider-api setup --provider-base-url <url>",
450
+ inputs: "模型服务 Base URL",
451
+ },
452
+ "model-runtime": {
453
+ ai: "proflow-model-runtime setup --fast-model <id> --reason-model <id>",
454
+ inputs: "FAST 模型 ID、REASON 模型 ID",
455
+ },
456
+ "execution-browser-extension": {
457
+ ai: "proflow-execution-browser-extension setup --extension-id <id>",
458
+ inputs: "Chrome Extension ID",
459
+ },
460
+ "agent-controller-dev": {
461
+ ai: "proflow-agent-controller-dev setup --carrier-url <url>",
462
+ inputs: "Custom GPT URL",
463
+ },
464
+ "agent-product": {
465
+ ai: "proflow-agent-product setup --carrier-url <url>",
466
+ inputs: "Custom GPT URL",
467
+ },
468
+ "agent-test-ops": {
469
+ ai: "proflow-agent-test-ops setup --carrier-url <url>",
470
+ inputs: "Custom GPT URL",
471
+ },
472
+ };
400
473
  function renderSetup(data) {
401
474
  if (!isRecord(data) || !Array.isArray(data.results))
402
- return "No setup actions are required.";
403
- const lines = ["ProFlow Setup", ""];
475
+ return "没有需要执行的配置步骤。";
476
+ const lines = ["ProFlow 配置", ""];
404
477
  let automatic = 0;
405
478
  let pending = 0;
406
479
  for (const raw of data.results) {
@@ -413,31 +486,52 @@ function renderSetup(data) {
413
486
  continue;
414
487
  }
415
488
  pending += 1;
416
- lines.push(`## ${moduleRef} — ${status}`);
417
- const actionRequired = raw.result.actionRequired;
489
+ lines.push(moduleRef);
490
+ const plan = moduleSetupPlanDataSchema.safeParse(raw.result.data);
491
+ if (plan.success) {
492
+ for (const [index, step] of plan.data.steps.entries()) {
493
+ lines.push(` [${step.state} ${index + 1}/${plan.data.steps.length}] ${step.title}`);
494
+ lines.push(` 人工执行:${step.execution.interactive}`);
495
+ lines.push(` AI 执行:${step.execution.nonInteractive}`);
496
+ if (step.requiredInputs.length > 0)
497
+ lines.push(` 需要输入:${step.requiredInputs.map((item) => item.description).join("、")}`);
498
+ lines.push(` 验证:${step.verify}`);
499
+ lines.push(` 完成条件:${step.successCondition}`);
500
+ if (step.blockedReason)
501
+ lines.push(` 原因:${step.blockedReason}`);
502
+ }
503
+ lines.push("");
504
+ continue;
505
+ }
506
+ lines.push(` [${status === "FAILED" ? "BLOCKED" : "TODO"} 1/1] ${status === "FAILED" ? "等待上游服务信息" : "完成模块配置"}`);
418
507
  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));
508
+ if (status !== "FAILED") {
509
+ const commands = setupCommands[moduleRef] ?? {
510
+ ai: `proflow-${moduleRef} setup`,
511
+ inputs: "按命令提示提供",
512
+ };
513
+ lines.push(` 人工执行:proflow-${moduleRef} setup`);
514
+ lines.push(` AI 执行:${commands.ai}`);
515
+ lines.push(` 需要输入:${commands.inputs}`);
516
+ lines.push(` 验证:proflow-${moduleRef} verify`);
517
+ lines.push(" 完成条件:配置状态变为“已就绪”");
424
518
  }
425
519
  else if (isRecord(error)) {
426
520
  const code = error.code === undefined ? "FAILED" : String(error.code);
427
521
  const message = error.message === undefined
428
522
  ? "Module setup failed."
429
523
  : 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));
524
+ lines.push(` 原因:${code} — ${message}`);
525
+ lines.push(` 执行:platform setup --module ${moduleRef}`);
526
+ lines.push(" 完成条件:配置状态变为“已就绪”");
434
527
  }
435
528
  lines.push("");
436
529
  }
437
530
  if (pending === 0)
438
- lines.push("All discovered Modules are setup-ready.");
531
+ lines.push("全部模块均已就绪。 ");
439
532
  if (automatic > 0)
440
- lines.push(`Automatically completed in this run: ${automatic}`);
533
+ lines.push(`本次自动完成:${automatic} 个模块`);
534
+ lines.push(`汇总:${automatic} 个已就绪,${pending} 个待处理`);
441
535
  return lines.join("\n").trimEnd();
442
536
  }
443
537
  export function renderHumanResult(result) {
@@ -445,17 +539,38 @@ export function renderHumanResult(result) {
445
539
  isRecord(result.data) &&
446
540
  Array.isArray(result.data.results))
447
541
  return renderSetup(result.data);
448
- if (result.status === "FAILED")
449
- return `${result.command.toUpperCase()} FAILED${result.error ? ` [${result.error.code}] ${result.error.message}` : ""}`;
542
+ if (result.command === "start" && isRecord(result.data)) {
543
+ const blockers = Array.isArray(result.data.blockers)
544
+ ? result.data.blockers.filter(isRecord)
545
+ : [];
546
+ if (blockers.length > 0)
547
+ return [
548
+ "平台未启动:存在未就绪模块",
549
+ "",
550
+ ...blockers.map((item) => `${String(item.moduleRef).padEnd(28)} ${String(item.setupStatus) === "FAILED" ? "失败" : "需要操作"} 配置尚未完成`),
551
+ "",
552
+ `处理方式:platform setup${result.workspaceRoot ? ` --workspace "${result.workspaceRoot}"` : ""}`,
553
+ ].join("\n");
554
+ }
555
+ if (result.status === "FAILED" && result.error)
556
+ return `${result.command} 失败:${result.error.message}`;
450
557
  if (result.command === "help")
451
558
  return [
452
- "ProFlow Platform CLI",
559
+ "ProFlow 平台命令行",
453
560
  "",
454
- ...COMMANDS.map((command) => `platform ${command}`),
561
+ "platform install 安装并初始化全部 ProFlow 模块",
562
+ "platform uninstall 卸载模块包,保留 Workspace 数据",
563
+ "platform status 查看模块配置与运行状态",
564
+ "platform setup 自动配置并列出全部剩余步骤",
565
+ "platform docs 阅读模块能力文档",
566
+ "platform start 完成全量检查后启动平台",
567
+ "platform stop 按逆依赖顺序停止平台",
455
568
  "",
456
- "platform install --workspace <path>",
457
- "platform setup --module <moduleRef> --input '<json>'",
458
- "append --json for machine-readable output",
569
+ "常用参数:--workspace <路径>;setup 还支持 --module <模块名>",
570
+ "推荐流程:install → status → docs → setup start status → stop",
571
+ "人工配置示例:proflow-chatgpt-carrier setup",
572
+ "AI 配置示例:proflow-chatgpt-carrier setup --carrier-url <url>",
573
+ "状态图例:已就绪=配置完成;需要操作=需运行 setup;失败=存在系统阻塞;无独立进程=无需启动",
459
574
  ].join("\n");
460
575
  if (result.command === "version" && isRecord(result.data))
461
576
  return String(result.data.version ?? platformCliDescriptor.moduleVersion);
@@ -465,19 +580,36 @@ export function renderHumanResult(result) {
465
580
  return renderSetup(result.data);
466
581
  if (result.command === "docs")
467
582
  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");
583
+ if ((result.command === "start" || result.command === "stop") &&
584
+ isRecord(result.data)) {
585
+ const skipped = Array.isArray(result.data.skipped)
586
+ ? result.data.skipped.length
587
+ : 0;
588
+ const results = Array.isArray(result.data.results)
589
+ ? result.data.results.filter(isRecord)
590
+ : [];
591
+ const operations = results.filter((item) => item.command === result.command);
592
+ const succeeded = operations.filter((item) => isRecord(item.result) && item.result.status === "SUCCEEDED").length;
593
+ const failed = operations.find((item) => isRecord(item.result) && item.result.status !== "SUCCEEDED");
594
+ return [
595
+ `平台${result.command === "start" ? "启动" : "停止"}${failed ? "未完成" : "完成"}`,
596
+ `成功:${succeeded},跳过:${skipped}${failed ? `,失败:${String(failed.moduleRef)}` : ",失败:0"}`,
597
+ ].join("\n");
598
+ }
599
+ const labels = {
600
+ install: "安装",
601
+ uninstall: "卸载",
602
+ start: "启动",
603
+ stop: "停止",
604
+ };
605
+ return `${labels[result.command] ?? result.command}${result.status === "SUCCEEDED" ? "成功" : "未完成"}${result.workspaceRoot ? `\nWorkspace:${result.workspaceRoot}` : ""}`;
475
606
  }
476
607
  if (import.meta.main) {
477
608
  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`);
609
+ const result = await runCli(argv, {
610
+ onProgress: createTerminalProgressReporter(),
611
+ });
612
+ process.stdout.write(`${renderHumanResult(result)}\n`);
481
613
  if (result.status !== "SUCCEEDED")
482
614
  process.exitCode = 1;
483
615
  }
@@ -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.22",
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.8"
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.6",
31
+ "@tomflow/proflow-module-template": "^0.1.6"
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.22",
7
7
  "kind": "cli",
8
8
  "templateVersion": "1.0.0",
9
9
  "platformCompatibility": ">=1.0.0 <2.0.0",