@tomflow/proflow-platform-cli 0.1.13 → 0.1.15

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.
Files changed (142) hide show
  1. package/README.md +48 -43
  2. package/dist/deployment/adapter.d.ts +5 -5
  3. package/dist/deployment/adapter.js +2 -1
  4. package/dist/deployment/descriptor.d.ts +2 -3
  5. package/dist/deployment/descriptor.js +2 -3
  6. package/dist/src/binding/production-bindings.d.ts +17 -15
  7. package/dist/src/binding/production-bindings.js +18 -59
  8. package/dist/src/cli.d.ts +7 -9
  9. package/dist/src/cli.js +329 -1237
  10. package/dist/src/contracts.d.ts +4 -97
  11. package/dist/src/discovery/discover.js +0 -1
  12. package/dist/src/docs/aggregate.d.ts +18 -0
  13. package/dist/src/docs/aggregate.js +44 -0
  14. package/dist/src/docs/docs.d.ts +0 -31
  15. package/dist/src/docs/docs.js +0 -61
  16. package/dist/src/docs/index.d.ts +2 -1
  17. package/dist/src/docs/index.js +1 -1
  18. package/dist/src/errors.d.ts +1 -1
  19. package/dist/src/errors.js +0 -18
  20. package/dist/src/graph/graph.d.ts +1 -4
  21. package/dist/src/graph/graph.js +1 -13
  22. package/dist/src/graph/index.d.ts +2 -2
  23. package/dist/src/graph/index.js +1 -1
  24. package/dist/src/index.d.ts +4 -24
  25. package/dist/src/index.js +2 -16
  26. package/dist/src/install/package-manager.d.ts +21 -0
  27. package/dist/src/install/package-manager.js +153 -0
  28. package/dist/src/lifecycle/dispatch.d.ts +0 -30
  29. package/dist/src/lifecycle/dispatch.js +0 -120
  30. package/dist/src/lifecycle/index.d.ts +4 -3
  31. package/dist/src/lifecycle/index.js +2 -2
  32. package/dist/src/lifecycle/thin.d.ts +11 -0
  33. package/dist/src/lifecycle/thin.js +68 -0
  34. package/dist/src/paths.d.ts +0 -14
  35. package/dist/src/paths.js +0 -39
  36. package/dist/src/persistence/config.d.ts +0 -6
  37. package/dist/src/persistence/config.js +0 -14
  38. package/dist/src/persistence/guards.d.ts +0 -6
  39. package/dist/src/persistence/guards.js +0 -114
  40. package/dist/src/persistence/index.d.ts +4 -6
  41. package/dist/src/persistence/index.js +2 -5
  42. package/dist/src/persistence/workspace-metadata.d.ts +7 -0
  43. package/dist/src/persistence/workspace-metadata.js +39 -0
  44. package/dist/src/registry/index.d.ts +1 -1
  45. package/dist/src/registry/index.js +1 -1
  46. package/dist/src/registry/npm-registry.d.ts +0 -14
  47. package/dist/src/registry/npm-registry.js +1 -132
  48. package/package.json +2 -6
  49. package/proflow.module.json +2 -3
  50. package/CHANGELOG.md +0 -79
  51. package/dist/src/apply/apply.d.ts +0 -26
  52. package/dist/src/apply/apply.js +0 -314
  53. package/dist/src/apply/cleanup.d.ts +0 -12
  54. package/dist/src/apply/cleanup.js +0 -37
  55. package/dist/src/apply/current.d.ts +0 -5
  56. package/dist/src/apply/current.js +0 -95
  57. package/dist/src/apply/driver.d.ts +0 -28
  58. package/dist/src/apply/driver.js +0 -239
  59. package/dist/src/apply/execute.d.ts +0 -36
  60. package/dist/src/apply/execute.js +0 -152
  61. package/dist/src/apply/index.d.ts +0 -8
  62. package/dist/src/apply/index.js +0 -4
  63. package/dist/src/apply/reality.d.ts +0 -20
  64. package/dist/src/apply/reality.js +0 -105
  65. package/dist/src/binding/global-binding.d.ts +0 -69
  66. package/dist/src/binding/global-binding.js +0 -349
  67. package/dist/src/doctor/doctor.d.ts +0 -41
  68. package/dist/src/doctor/doctor.js +0 -80
  69. package/dist/src/doctor/index.d.ts +0 -2
  70. package/dist/src/doctor/index.js +0 -1
  71. package/dist/src/install/bootstrap.d.ts +0 -10
  72. package/dist/src/install/bootstrap.js +0 -79
  73. package/dist/src/install/environment.d.ts +0 -30
  74. package/dist/src/install/environment.js +0 -230
  75. package/dist/src/install/index.d.ts +0 -6
  76. package/dist/src/install/index.js +0 -3
  77. package/dist/src/install/install.d.ts +0 -19
  78. package/dist/src/install/install.js +0 -172
  79. package/dist/src/lifecycle/service-process.d.ts +0 -11
  80. package/dist/src/lifecycle/service-process.js +0 -356
  81. package/dist/src/manifest/index.d.ts +0 -2
  82. package/dist/src/manifest/index.js +0 -1
  83. package/dist/src/manifest/manifest.d.ts +0 -61
  84. package/dist/src/manifest/manifest.js +0 -167
  85. package/dist/src/observer/deployment-summary.d.ts +0 -31
  86. package/dist/src/observer/deployment-summary.js +0 -34
  87. package/dist/src/persistence/atomic.d.ts +0 -2
  88. package/dist/src/persistence/atomic.js +0 -16
  89. package/dist/src/persistence/effective-config.d.ts +0 -3
  90. package/dist/src/persistence/effective-config.js +0 -20
  91. package/dist/src/persistence/generated.d.ts +0 -2
  92. package/dist/src/persistence/generated.js +0 -4
  93. package/dist/src/persistence/plans.d.ts +0 -5
  94. package/dist/src/persistence/plans.js +0 -34
  95. package/dist/src/persistence/state.d.ts +0 -7
  96. package/dist/src/persistence/state.js +0 -34
  97. package/dist/src/persistence/verification.d.ts +0 -5
  98. package/dist/src/persistence/verification.js +0 -47
  99. package/dist/src/planner/assemble.d.ts +0 -16
  100. package/dist/src/planner/assemble.js +0 -107
  101. package/dist/src/planner/check.d.ts +0 -20
  102. package/dist/src/planner/check.js +0 -107
  103. package/dist/src/planner/fingerprint.d.ts +0 -12
  104. package/dist/src/planner/fingerprint.js +0 -164
  105. package/dist/src/planner/index.d.ts +0 -13
  106. package/dist/src/planner/index.js +0 -7
  107. package/dist/src/planner/plan.d.ts +0 -15
  108. package/dist/src/planner/plan.js +0 -144
  109. package/dist/src/planner/repair.d.ts +0 -28
  110. package/dist/src/planner/repair.js +0 -131
  111. package/dist/src/planner/stale.d.ts +0 -7
  112. package/dist/src/planner/stale.js +0 -23
  113. package/dist/src/planner/steps.d.ts +0 -37
  114. package/dist/src/planner/steps.js +0 -165
  115. package/dist/src/planner/target-catalog.d.ts +0 -13
  116. package/dist/src/planner/target-catalog.js +0 -48
  117. package/dist/src/planner/upgrade.d.ts +0 -18
  118. package/dist/src/planner/upgrade.js +0 -114
  119. package/dist/src/preflight/config.d.ts +0 -11
  120. package/dist/src/preflight/config.js +0 -88
  121. package/dist/src/preflight/findings.d.ts +0 -26
  122. package/dist/src/preflight/findings.js +0 -1
  123. package/dist/src/preflight/index.d.ts +0 -7
  124. package/dist/src/preflight/index.js +0 -3
  125. package/dist/src/preflight/preflight.d.ts +0 -10
  126. package/dist/src/preflight/preflight.js +0 -190
  127. package/dist/src/preflight/requirements.d.ts +0 -11
  128. package/dist/src/preflight/requirements.js +0 -231
  129. package/dist/src/ready/index.d.ts +0 -2
  130. package/dist/src/ready/index.js +0 -1
  131. package/dist/src/ready/ready.d.ts +0 -40
  132. package/dist/src/ready/ready.js +0 -201
  133. package/dist/src/security/index.d.ts +0 -3
  134. package/dist/src/security/index.js +0 -2
  135. package/dist/src/security/lock.d.ts +0 -9
  136. package/dist/src/security/lock.js +0 -127
  137. package/dist/src/security/redact.d.ts +0 -6
  138. package/dist/src/security/redact.js +0 -48
  139. package/dist/src/verification/index.d.ts +0 -2
  140. package/dist/src/verification/index.js +0 -1
  141. package/dist/src/verification/verify.d.ts +0 -33
  142. package/dist/src/verification/verify.js +0 -114
package/dist/src/cli.js CHANGED
@@ -1,1346 +1,438 @@
1
1
  #!/usr/bin/env node
2
- import { readFile, rm, rmdir } from "node:fs/promises";
3
- import { parseModuleDescriptor, } from "@tomflow/proflow-module-contract";
2
+ import { readFile, realpath, stat } from "node:fs/promises";
3
+ import { resolve } from "node:path";
4
+ import { moduleStatusObservationSchema } from "@tomflow/proflow-module-contract";
4
5
  import { descriptor as platformCliDescriptor } from "../deployment/descriptor.js";
5
- import { applyPlan } from "./apply/apply.js";
6
- import { rebuildCurrentAssumptions } from "./apply/current.js";
7
- import { cleanupWorkspacePackageManagerArtifacts, createWorkspacePackageManagerDriver, } from "./apply/driver.js";
8
- import { acquireGlobalOperationLock, canonicalizeWorkspace, claimWorkspaceBinding, clearGlobalBinding, forgetMissingWorkspaceBinding, loadGlobalBinding, observeBoundWorkspace, requireBoundWorkspace, updateGlobalBindingState, } from "./binding/global-binding.js";
9
6
  import { buildProductionBindings, importRawAdapter, } from "./binding/production-bindings.js";
10
7
  import { AutoModuleCatalog, discoverModules } from "./discovery/discover.js";
11
- import { describeModule, readModuleDocument } from "./docs/docs.js";
12
- import { doctorModules } from "./doctor/doctor.js";
8
+ import { InstalledModuleCatalog } from "./discovery/installed.js";
9
+ import { aggregateModuleDocs } from "./docs/aggregate.js";
13
10
  import { PlatformError } from "./errors.js";
14
- import { buildDependencyGraph } from "./graph/graph.js";
15
- import { selectBootstrapModules } from "./install/bootstrap.js";
16
- import { preflightInstallerEnvironment } from "./install/environment.js";
17
- import { generateInstallDoc } from "./install/install.js";
18
- import { restartModules, startModules, statusModules, stopModules, } from "./lifecycle/dispatch.js";
19
- import { buildManifest } from "./manifest/manifest.js";
20
- import { writeDeploymentObserverSummary } from "./observer/deployment-summary.js";
11
+ import { observeWorkspaceInstalledVersion, removeWorkspacePackages, syncWorkspacePackages, } from "./install/package-manager.js";
12
+ import { observeStatuses, preflightAndStartModules, stopModulesThin, } from "./lifecycle/thin.js";
21
13
  import { workspacePaths } from "./paths.js";
22
14
  import { loadConfig } from "./persistence/config.js";
23
- import { mergeEffectiveConfig } from "./persistence/effective-config.js";
24
- import { loadPlan, savePlan } from "./persistence/plans.js";
25
- import { planDeployment } from "./planner/plan.js";
26
- import { diagnoseRepair } from "./planner/repair.js";
27
- import { runPreflight } from "./preflight/preflight.js";
28
- import { discoverRegistryInstallClosure, discoverRegistryModules, loadRegistryModuleDescriptor, } from "./registry/index.js";
29
- import { verifyModules } from "./verification/verify.js";
30
- const MODULE_REF = platformCliDescriptor.moduleRef;
31
- const MODULE_VERSION = platformCliDescriptor.moduleVersion;
15
+ import { ensureWorkspaceMetadata } from "./persistence/workspace-metadata.js";
16
+ import { discoverRegistryModules, PRO_FLOW_PACKAGE_PREFIX, } from "./registry/index.js";
32
17
  const COMMANDS = [
33
- "search",
34
18
  "modules",
35
19
  "docs",
36
20
  "install",
37
21
  "uninstall",
38
- "upgrade",
39
- "preflight",
40
- "plan",
41
- "apply",
42
22
  "start",
43
23
  "stop",
44
- "restart",
45
- "status",
46
- "verify",
47
- "doctor",
48
- "manifest",
49
24
  ];
50
25
  function parseArgs(argv) {
26
+ let json = false;
27
+ let workspace;
28
+ let special;
51
29
  const positional = [];
52
- const parsed = {
53
- command: argv[0] ?? "",
54
- workspace: undefined,
55
- intent: undefined,
56
- configFile: undefined,
57
- forget: false,
58
- positional,
59
- };
60
- for (let index = 1; index < argv.length; index += 1) {
61
- const token = argv[index];
62
- if (token === undefined)
30
+ for (let index = 0; index < argv.length; index += 1) {
31
+ const value = argv[index];
32
+ if (value === undefined)
33
+ continue;
34
+ if (value === "--json") {
35
+ json = true;
63
36
  continue;
64
- if (token === "--workspace") {
65
- parsed.workspace = requiredOptionValue(argv, index, token);
66
- index += 1;
67
- }
68
- else if (token === "--intent") {
69
- parsed.intent = requiredOptionValue(argv, index, token);
70
- index += 1;
71
37
  }
72
- else if (token === "--config") {
73
- parsed.configFile = requiredOptionValue(argv, index, token);
38
+ if (value === "--workspace") {
39
+ const next = argv[index + 1];
40
+ if (!next || next.startsWith("-")) {
41
+ throw new PlatformError("INVALID_REQUEST", "--workspace requires a path");
42
+ }
43
+ workspace = next;
74
44
  index += 1;
45
+ continue;
75
46
  }
76
- else if (token === "--forget") {
77
- parsed.forget = true;
78
- }
79
- else if (token.startsWith("-")) {
80
- throw new PlatformError("INVALID_REQUEST", `unknown option: ${token}`);
47
+ if (value === "--help" || value === "-h") {
48
+ if (special !== undefined && special !== "help")
49
+ throw new PlatformError("INVALID_REQUEST", "help and version flags cannot be combined");
50
+ special = "help";
51
+ continue;
81
52
  }
82
- else {
83
- positional.push(token);
53
+ if (value === "--version" || value === "-v") {
54
+ if (special !== undefined && special !== "version")
55
+ throw new PlatformError("INVALID_REQUEST", "help and version flags cannot be combined");
56
+ special = "version";
57
+ continue;
84
58
  }
85
- }
86
- return parsed;
87
- }
88
- function requiredOptionValue(argv, index, option) {
89
- const value = argv[index + 1];
90
- if (value === undefined || value === "" || value.startsWith("-")) {
91
- throw new PlatformError("INVALID_REQUEST", `${option} requires a value`);
92
- }
93
- return value;
94
- }
95
- function outcome(command, status, data) {
96
- const result = { command, status };
97
- if (data !== undefined)
98
- result.data = data;
99
- return result;
100
- }
101
- function workspaceSummary(binding) {
102
- return {
103
- boundWorkspace: binding.workspaceRealPath,
104
- workspaceInstanceId: binding.workspaceInstanceId,
105
- bindingState: binding.state,
106
- };
107
- }
108
- function withWorkspace(result, binding) {
109
- return { ...result, workspace: workspaceSummary(binding) };
110
- }
111
- function aggregateStatus(statuses) {
112
- if (statuses.includes("FAILED"))
113
- return "FAILED";
114
- if (statuses.includes("BLOCKED"))
115
- return "BLOCKED";
116
- if (statuses.includes("ACTION_REQUIRED"))
117
- return "ACTION_REQUIRED";
118
- return "SUCCEEDED";
119
- }
120
- function failure(command, error) {
121
- if (error instanceof PlatformError) {
122
- return {
123
- command,
124
- status: "FAILED",
125
- error: { code: error.code, message: error.message },
126
- };
127
- }
128
- const message = error instanceof Error ? error.message : String(error);
59
+ if (value.startsWith("-"))
60
+ throw new PlatformError("INVALID_REQUEST", `unknown option ${value}`);
61
+ positional.push(value);
62
+ }
63
+ if (special !== undefined) {
64
+ if (workspace !== undefined || positional.length > 0)
65
+ throw new PlatformError("INVALID_REQUEST", `${special} flag cannot be combined with a command or --workspace`);
66
+ return { command: special, json };
67
+ }
68
+ if (positional.length === 0) {
69
+ if (workspace !== undefined)
70
+ throw new PlatformError("INVALID_REQUEST", "--workspace is only valid with install");
71
+ return { command: "help", json };
72
+ }
73
+ if (positional.length !== 1)
74
+ throw new PlatformError("INVALID_REQUEST", "commands accept no positional arguments");
75
+ const raw = positional[0] ?? "";
76
+ if (!COMMANDS.includes(raw))
77
+ throw new PlatformError("INVALID_REQUEST", `unknown command ${raw}`);
78
+ const command = raw;
79
+ if (workspace !== undefined && command !== "install")
80
+ throw new PlatformError("INVALID_REQUEST", "--workspace is only valid with install");
81
+ return workspace === undefined
82
+ ? { command, json }
83
+ : { command, workspace, json };
84
+ }
85
+ function outcome(command, status, workspaceRoot, data) {
129
86
  return {
130
87
  command,
131
- status: "FAILED",
132
- error: { code: "COMMAND_FAILED", message },
88
+ status,
89
+ ...(workspaceRoot === undefined ? {} : { workspaceRoot }),
90
+ ...(data === undefined ? {} : { data }),
133
91
  };
134
92
  }
135
- function selectModules(modules, filter) {
136
- if (filter === undefined)
137
- return modules;
138
- const selected = modules.filter((module) => module.moduleRef === filter || module.packageName === filter);
139
- if (selected.length === 0) {
140
- throw new PlatformError("INVALID_REQUEST", `module or package "${filter}" not found`);
141
- }
142
- return selected;
143
- }
144
- function moduleSourceOf(module) {
145
- if (module.source.type === "registry") {
146
- throw new PlatformError("DESCRIPTOR_INVALID", `registry bootstrap target ${module.packageName} has no local descriptor`);
147
- }
148
- const source = {
149
- type: module.source.type,
150
- packageName: module.packageName,
151
- };
152
- if (module.source.path !== undefined)
153
- source.path = module.source.path;
154
- return source;
155
- }
156
- async function loadDescriptors(catalog, modules) {
157
- const descriptors = [];
158
- for (const module of modules) {
159
- const raw = await catalog.loadDescriptor(moduleSourceOf(module));
160
- descriptors.push(parseModuleDescriptor(raw));
161
- }
162
- return descriptors;
163
- }
164
- async function loadConfigFile(path) {
165
- if (path === undefined)
166
- return {};
167
- const raw = await readFile(path, "utf8");
168
- const value = JSON.parse(raw);
169
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
170
- throw new PlatformError("INVALID_REQUEST", `config file ${path} is not an object`);
93
+ async function canonicalWorkspace(cwd, explicit) {
94
+ const candidate = explicit === undefined ? cwd : resolve(cwd, explicit);
95
+ let info;
96
+ try {
97
+ info = await stat(candidate);
171
98
  }
172
- const modules = value.modules;
173
- if (modules === undefined)
174
- return {};
175
- if (typeof modules !== "object" ||
176
- modules === null ||
177
- Array.isArray(modules)) {
178
- throw new PlatformError("INVALID_REQUEST", `config file ${path} "modules" is not an object`);
99
+ catch (error) {
100
+ throw new PlatformError("WORKSPACE_NOT_FOUND", `Workspace does not exist: ${candidate} (${error instanceof Error ? error.message : String(error)})`);
179
101
  }
180
- const result = {};
181
- for (const [ref, slots] of Object.entries(modules)) {
182
- if (typeof slots !== "object" || slots === null || Array.isArray(slots))
183
- continue;
184
- const values = {};
185
- for (const [key, value] of Object.entries(slots)) {
186
- if (typeof value === "string")
187
- values[key] = value;
188
- }
189
- result[ref] = values;
102
+ if (!info.isDirectory()) {
103
+ throw new PlatformError("WORKSPACE_NOT_FOUND", `Workspace is not a directory: ${candidate}`);
190
104
  }
191
- return result;
105
+ return realpath(candidate);
192
106
  }
193
- async function buildContext(root) {
107
+ async function loadConfigMap(root, modules) {
194
108
  const paths = workspacePaths(root);
195
- const catalog = await buildCatalogWithProductionBindings(root, paths);
196
- return { catalog, paths };
197
- }
198
- /**
199
- * The shipped CLI production composition root. Instead of constructing an empty
200
- * `AutoModuleCatalog` (whose binding seam would otherwise be used only by tests),
201
- * it discovers the governed modules, loads their materialized config, runs the
202
- * production binding factory against each module's own deployment adapter, and
203
- * passes the resulting real bindings into the catalog. Modules without a
204
- * production factory stay unbound and fail closed when operated on.
205
- */
206
- async function buildCatalogWithProductionBindings(root, paths) {
207
- const modules = await discoverModules({ workspaceRoot: root });
208
109
  const configByModuleRef = new Map();
209
110
  for (const module of modules) {
210
111
  const config = await loadConfig(paths, module.moduleRef);
211
112
  if (config === undefined)
212
113
  continue;
213
- configByModuleRef.set(module.moduleRef, {
214
- ...config.publicValues,
215
- ...config.secretValues,
216
- });
114
+ configByModuleRef.set(module.moduleRef, config);
217
115
  }
116
+ return configByModuleRef;
117
+ }
118
+ async function buildContext(root) {
119
+ const discovered = await discoverModules({ workspaceRoot: root });
120
+ const configByModuleRef = await loadConfigMap(root, discovered);
218
121
  const bindings = await buildProductionBindings({
219
122
  workspaceRoot: root,
220
- modules,
123
+ modules: discovered,
221
124
  configByModuleRef,
222
125
  importAdapter: (packageName, source) => importRawAdapter(packageName, source, root),
223
126
  });
224
- return new AutoModuleCatalog(root, bindings);
225
- }
226
- async function handleSearch(root, args) {
227
- const result = await discoverRegistryModules({
228
- workspaceRoot: root,
229
- ...(args.positional[0] === undefined
230
- ? {}
231
- : { packageName: args.positional[0] }),
232
- });
233
- if (args.positional[0] !== undefined && result.candidates.length === 0) {
234
- return outcome("search", "BLOCKED", result);
235
- }
236
- return outcome("search", "SUCCEEDED", result);
237
- }
238
- async function handleModules(ctx, args) {
239
- const modules = await discoverModules({ catalog: ctx.catalog });
240
- const selected = selectModules(modules, args.positional[0]);
241
- const descriptors = await loadDescriptors(ctx.catalog, selected);
242
- return outcome("modules", "SUCCEEDED", descriptors.map((descriptor) => ({
243
- moduleRef: descriptor.moduleRef,
244
- packageName: descriptor.packageName,
245
- moduleVersion: descriptor.moduleVersion,
246
- kind: descriptor.kind,
247
- installClass: descriptor.installClass,
248
- identity: descriptor.identity,
249
- lifecycle: descriptor.lifecycle.supported,
250
- documentation: descriptor.documentation,
251
- })));
127
+ const catalog = new AutoModuleCatalog(root, bindings);
128
+ const modules = await discoverModules({ workspaceRoot: root, catalog });
129
+ return { catalog, modules };
252
130
  }
253
- async function handleDocs(ctx, args) {
254
- if (args.positional.length > 2) {
255
- throw new PlatformError("INVALID_REQUEST", "docs accepts at most <moduleRef|packageName> [documentId]");
256
- }
257
- const modules = await discoverModules({ catalog: ctx.catalog });
258
- const moduleFilter = args.positional[0];
259
- const documentId = args.positional[1];
260
- if (documentId !== undefined && moduleFilter === undefined) {
261
- throw new PlatformError("INVALID_REQUEST", "docs document read requires a module");
262
- }
263
- const selected = selectModules(modules, moduleFilter);
264
- const descriptors = await loadDescriptors(ctx.catalog, selected);
265
- if (documentId !== undefined) {
266
- const module = selected[0];
267
- const descriptor = descriptors[0];
268
- if (module === undefined || descriptor === undefined) {
269
- throw new PlatformError("COMMAND_FAILED", "docs module selection invariant failed");
270
- }
271
- return outcome("docs", "SUCCEEDED", await readModuleDocument({
272
- workspaceRoot: ctx.paths.root,
273
- source: moduleSourceOf(module),
274
- descriptor,
275
- documentId,
276
- }));
277
- }
278
- const views = await Promise.all(selected.map((module, index) => describeModule({
279
- workspaceRoot: ctx.paths.root,
280
- source: moduleSourceOf(module),
281
- descriptor: descriptors[index] ??
282
- (() => {
283
- throw new Error(`Missing module descriptor for ${module.packageName}`);
284
- })(),
285
- })));
286
- if (moduleFilter !== undefined) {
287
- return outcome("docs", "SUCCEEDED", views[0]);
288
- }
289
- return outcome("docs", "SUCCEEDED", views.map((view) => ({
290
- moduleRef: view.moduleRef,
291
- packageName: view.packageName,
292
- moduleVersion: view.moduleVersion,
293
- kind: view.kind,
294
- installClass: view.installClass,
295
- identity: view.identity,
296
- commands: view.commands,
297
- publicApiEntries: view.publicApiEntries,
298
- provides: view.provides,
299
- requires: view.requires,
300
- lifecycle: view.lifecycle,
301
- documentation: view.documentation,
302
- })));
303
- }
304
- async function handleInstallerPreflight(root) {
305
- const result = await preflightInstallerEnvironment({ workspaceRoot: root });
306
- const status = result.status === "READY"
131
+ function statusFromModule(value) {
132
+ return value === "SUCCEEDED"
307
133
  ? "SUCCEEDED"
308
- : result.status === "ACTION_REQUIRED"
134
+ : value === "ACTION_REQUIRED"
309
135
  ? "ACTION_REQUIRED"
310
- : "BLOCKED";
311
- return outcome("preflight", status, result);
312
- }
313
- async function loadEffectiveConfig(paths, modules, configFile) {
314
- return mergeEffectiveConfig(paths, modules, await loadConfigFile(configFile));
315
- }
316
- async function handlePreflight(ctx, args) {
317
- const modules = await discoverModules({ catalog: ctx.catalog });
318
- const selected = selectModules(modules, args.positional[0]);
319
- const config = await loadEffectiveConfig(ctx.paths, selected, args.configFile);
320
- const result = await runPreflight(selected, {
321
- config,
322
- catalog: ctx.catalog,
323
- paths: ctx.paths,
324
- });
325
- const status = result.status === "READY"
326
- ? "SUCCEEDED"
327
- : result.status === "ACTION_REQUIRED"
328
- ? "ACTION_REQUIRED"
329
- : "BLOCKED";
330
- return outcome("preflight", status, result);
331
- }
332
- async function handlePlan(ctx, args) {
333
- const intent = args.intent;
334
- if (intent !== "install" &&
335
- intent !== "configure" &&
336
- intent !== "upgrade" &&
337
- intent !== "uninstall" &&
338
- intent !== "repair") {
339
- throw new PlatformError("INVALID_REQUEST", "plan requires --intent install|configure|upgrade|uninstall|repair");
340
- }
341
- if (intent === "install") {
342
- const installer = await preflightInstallerEnvironment({
343
- workspaceRoot: ctx.paths.root,
344
- });
345
- if (installer.status !== "READY") {
346
- return outcome("plan", installer.status === "ACTION_REQUIRED" ? "ACTION_REQUIRED" : "BLOCKED", installer);
347
- }
348
- const registry = args.positional[0] === undefined
349
- ? await discoverRegistryModules({ workspaceRoot: ctx.paths.root })
350
- : await discoverRegistryInstallClosure({
351
- workspaceRoot: ctx.paths.root,
352
- packageName: args.positional[0],
353
- });
354
- const bootstrapModules = selectBootstrapModules(registry.candidates, args.positional[0]);
355
- const plan = planDeployment({
356
- intent: "install",
357
- modules: bootstrapModules,
358
- });
359
- await savePlan(ctx.paths, plan);
360
- return outcome("plan", "SUCCEEDED", {
361
- planRef: plan.planRef,
362
- registry: registry.registry,
363
- plan,
364
- });
365
- }
366
- const modules = await discoverModules({ catalog: ctx.catalog });
367
- const selected = selectModules(modules, args.positional[0]);
368
- const config = args.intent === "upgrade"
369
- ? await loadEffectiveConfig(ctx.paths, selected, args.configFile)
370
- : await loadConfigFile(args.configFile);
371
- if (intent === "uninstall") {
372
- if (args.positional[0] === undefined) {
373
- throw new PlatformError("INVALID_REQUEST", "uninstall plan requires <moduleRef|packageName>");
374
- }
375
- if (selected.every((module) => module.installClass !== "core")) {
376
- const removing = new Set(selected.map((module) => module.moduleRef));
377
- const remaining = modules.filter((module) => !removing.has(module.moduleRef));
378
- // Rebuilding the remaining graph is the uninstall dependency gate: if a
379
- // required contract or moduleRef binding would disappear, graph
380
- // construction fails closed before any removal plan is persisted.
381
- buildDependencyGraph(remaining);
382
- }
383
- const plan = planDeployment({ intent, modules: selected, config });
384
- await savePlan(ctx.paths, plan);
385
- return outcome("plan", "SUCCEEDED", { planRef: plan.planRef, plan });
386
- }
387
- if (intent === "upgrade") {
388
- const installer = await preflightInstallerEnvironment({
389
- workspaceRoot: ctx.paths.root,
390
- });
391
- if (installer.status !== "READY") {
392
- return outcome("plan", installer.status === "ACTION_REQUIRED" ? "ACTION_REQUIRED" : "BLOCKED", installer);
393
- }
394
- const currentDescriptors = await loadDescriptors(ctx.catalog, modules);
395
- const currentByRef = new Map(currentDescriptors.map((descriptor) => [
396
- descriptor.moduleRef,
397
- descriptor,
398
- ]));
399
- const targetByRef = new Map(currentByRef);
400
- const targets = [];
401
- let registryUrl;
402
- for (const module of selected) {
403
- const discovered = await discoverRegistryModules({
404
- workspaceRoot: ctx.paths.root,
405
- packageName: module.packageName,
406
- });
407
- registryUrl ??= discovered.registry;
408
- const candidate = discovered.candidates[0];
409
- if (candidate === undefined) {
410
- throw new PlatformError("PACKAGE_NOT_FOUND", `no installable Registry target found for managed package ${module.packageName}`);
411
- }
412
- targets.push({
413
- moduleRef: module.moduleRef,
414
- targetVersion: candidate.moduleVersion,
415
- });
416
- if (candidate.moduleVersion === module.moduleVersion)
417
- continue;
418
- const targetDescriptor = await loadRegistryModuleDescriptor({
419
- workspaceRoot: ctx.paths.root,
420
- candidate,
421
- });
422
- if (targetDescriptor.moduleRef !== module.moduleRef) {
423
- throw new PlatformError("REGISTRY_RESPONSE_INVALID", `Registry target ${module.packageName}@${candidate.moduleVersion} changed moduleRef from ${module.moduleRef} to ${targetDescriptor.moduleRef}`);
424
- }
425
- targetByRef.set(module.moduleRef, targetDescriptor);
426
- }
427
- const targetDescriptors = modules.map((module) => {
428
- const descriptor = targetByRef.get(module.moduleRef);
429
- if (descriptor === undefined) {
430
- throw new PlatformError("COMMAND_FAILED", `upgrade target descriptor invariant failed for ${module.moduleRef}`);
431
- }
432
- return descriptor;
433
- });
434
- const plan = planDeployment({
435
- intent,
436
- currentDescriptors,
437
- targetDescriptors,
438
- targets,
439
- config,
440
- });
441
- await savePlan(ctx.paths, plan);
442
- return outcome("plan", "SUCCEEDED", {
443
- planRef: plan.planRef,
444
- registry: registryUrl,
445
- plan,
446
- });
447
- }
448
- if (intent === "configure") {
449
- const plan = planDeployment({ intent, modules: selected, config });
450
- await savePlan(ctx.paths, plan);
451
- return outcome("plan", "SUCCEEDED", { planRef: plan.planRef, plan });
452
- }
453
- const preflight = await runPreflight(selected, {
454
- config,
455
- catalog: ctx.catalog,
456
- paths: ctx.paths,
457
- });
458
- if (preflight.status === "NOT_READY" ||
459
- preflight.status === "ACTION_REQUIRED") {
460
- return outcome("plan", preflight.status === "ACTION_REQUIRED" ? "ACTION_REQUIRED" : "BLOCKED", preflight);
461
- }
462
- let input;
463
- if (intent === "repair") {
464
- const diagnosis = diagnoseRepair(await doctorModules(ctx.catalog, selected));
465
- if (diagnosis.blocked.length > 0) {
466
- return outcome("plan", "BLOCKED", diagnosis);
467
- }
468
- input = { intent, modules: selected, facts: diagnosis.facts, config };
469
- }
470
- else {
471
- input = { intent, modules: selected, config };
472
- }
473
- const plan = planDeployment(input);
474
- await savePlan(ctx.paths, plan);
475
- return outcome("plan", "SUCCEEDED", { planRef: plan.planRef, plan });
476
- }
477
- async function handleApply(ctx, args) {
478
- const planRef = args.positional[0];
479
- if (planRef === undefined) {
480
- throw new PlatformError("INVALID_REQUEST", "apply requires <planRef>");
481
- }
482
- const plan = await loadPlan(ctx.paths, planRef);
483
- if (plan === undefined) {
484
- throw new PlatformError("PLAN_NOT_FOUND", `plan ${planRef} not found`);
485
- }
486
- const current = await rebuildCurrentAssumptions(ctx.catalog, plan, ctx.paths);
487
- const result = await applyPlan({
488
- paths: ctx.paths,
489
- planRef,
490
- catalog: ctx.catalog,
491
- current,
492
- driver: createWorkspacePackageManagerDriver({
493
- workspaceRoot: ctx.paths.root,
494
- }),
495
- ...(plan.intent === "upgrade"
496
- ? {
497
- refreshCatalog: async () => (await buildContext(ctx.paths.root)).catalog,
498
- }
499
- : {}),
500
- });
501
- const status = result.outcome === "COMPLETE"
502
- ? "SUCCEEDED"
503
- : result.outcome === "ACTION_REQUIRED"
504
- ? "ACTION_REQUIRED"
505
- : result.outcome === "BLOCKED"
136
+ : value === "BLOCKED"
506
137
  ? "BLOCKED"
507
138
  : "FAILED";
508
- return outcome("apply", status, result);
509
139
  }
510
- function planRefFromOutcome(result) {
511
- if (typeof result.data !== "object" ||
512
- result.data === null ||
513
- Array.isArray(result.data)) {
514
- throw new PlatformError("PLAN_INVALID", "planned mutation returned no plan data");
515
- }
516
- const planRef = Reflect.get(result.data, "planRef");
517
- if (typeof planRef !== "string" || planRef === "") {
518
- throw new PlatformError("PLAN_INVALID", "planned mutation returned no planRef");
519
- }
520
- return planRef;
521
- }
522
- async function handleManagedMutation(ctx, args, intent) {
523
- const planned = await handlePlan(ctx, { ...args, command: "plan", intent });
524
- if (planned.status !== "SUCCEEDED") {
525
- return { ...planned, command: intent };
526
- }
527
- if (intent === "upgrade") {
528
- const data = planned.data;
529
- if (typeof data === "object" && data !== null && !Array.isArray(data)) {
530
- const plan = Reflect.get(data, "plan");
531
- if (typeof plan === "object" &&
532
- plan !== null &&
533
- !Array.isArray(plan) &&
534
- Array.isArray(Reflect.get(plan, "steps")) &&
535
- Reflect.get(plan, "steps").length === 0) {
536
- return outcome("upgrade", "SUCCEEDED", {
537
- ...data,
538
- alreadyLatest: true,
539
- changed: false,
540
- });
541
- }
542
- }
543
- }
544
- const planRef = planRefFromOutcome(planned);
545
- const applied = await handleApply(ctx, {
546
- ...args,
547
- command: "apply",
548
- intent: undefined,
549
- positional: [planRef],
550
- });
551
- if (applied.status !== "SUCCEEDED") {
552
- return outcome(intent, applied.status, {
553
- plan: planned.data,
554
- apply: applied.data,
555
- });
556
- }
557
- // Package mutations change Workspace package reality. Rebuild the catalog
558
- // before reporting the managed set so the result can never be sourced from
559
- // the pre-mutation catalog.
560
- const refreshed = await buildContext(ctx.paths.root);
561
- const managed = await handleModules(refreshed, {
562
- ...args,
563
- command: "modules",
564
- intent: undefined,
565
- positional: [],
566
- });
567
- return outcome(intent, "SUCCEEDED", {
568
- plan: planned.data,
569
- apply: applied.data,
570
- managedModules: managed.data,
140
+ function failedLifecycleStatus(results) {
141
+ const last = results.at(-1);
142
+ return last === undefined
143
+ ? "SUCCEEDED"
144
+ : statusFromModule(last.result.status);
145
+ }
146
+ async function handleModules(root) {
147
+ const { catalog, modules } = await buildContext(root);
148
+ const observed = await observeStatuses(catalog, modules);
149
+ const byRef = new Map(modules.map((module) => [module.moduleRef, module]));
150
+ const output = observed.map((item) => {
151
+ if (item.result.status !== "SUCCEEDED") {
152
+ throw new PlatformError("COMMAND_FAILED", `module ${item.moduleRef} status observation did not return SUCCEEDED`);
153
+ }
154
+ const parsed = moduleStatusObservationSchema.safeParse(item.result.data);
155
+ if (!parsed.success) {
156
+ throw new PlatformError("COMMAND_FAILED", `module ${item.moduleRef} returned an invalid status observation: ${parsed.error.message}`);
157
+ }
158
+ const module = byRef.get(item.moduleRef);
159
+ if (module === undefined)
160
+ throw new PlatformError("COMMAND_FAILED", `unknown module ${item.moduleRef}`);
161
+ return {
162
+ moduleRef: item.moduleRef,
163
+ version: module.moduleVersion,
164
+ configStatus: parsed.data.configStatus,
165
+ ...(parsed.data.missingConfig === undefined
166
+ ? {}
167
+ : { missingConfig: parsed.data.missingConfig }),
168
+ runtimeStatus: parsed.data.runtimeStatus,
169
+ };
571
170
  });
171
+ return outcome("modules", "SUCCEEDED", root, { modules: output });
572
172
  }
573
- async function resolveRequestedInstallWorkspace(args, runtime) {
574
- const requested = args.workspace ?? runtime.cwd ?? process.cwd();
575
- return (await canonicalizeWorkspace(requested)).workspaceRealPath;
576
- }
577
- async function handleInstallPlanBeforeBinding(args, runtime) {
578
- const requestedWorkspace = await resolveRequestedInstallWorkspace(args, runtime);
579
- const current = await loadGlobalBinding(runtime.globalRoot);
580
- if (current !== undefined &&
581
- current.workspaceRealPath !== requestedWorkspace) {
582
- throw new PlatformError("WORKSPACE_ALREADY_BOUND", `ProFlow is already bound to ${current.workspaceRealPath}; uninstall it before planning an install for ${requestedWorkspace}`);
583
- }
584
- const ctx = await buildContext(current?.workspaceRealPath ?? requestedWorkspace);
585
- const result = await handlePlan(ctx, args);
586
- return current === undefined ? result : withWorkspace(result, current);
173
+ async function handleDocs(root) {
174
+ const catalog = new AutoModuleCatalog(root);
175
+ const modules = await discoverModules({ workspaceRoot: root, catalog });
176
+ const docs = await aggregateModuleDocs(root, catalog, modules);
177
+ return outcome("docs", "SUCCEEDED", root, { modules: docs });
587
178
  }
588
- async function handleApplyWithGlobalBinding(args, runtime) {
589
- const planRef = args.positional[0];
590
- if (planRef === undefined) {
591
- throw new PlatformError("INVALID_REQUEST", "apply requires <planRef>");
179
+ async function validateInstalledPackageSet(root, candidates, previousManaged) {
180
+ const expectedNames = candidates.map((item) => item.packageName).sort();
181
+ const declaredNames = await workspaceProFlowDependencies(root);
182
+ if (JSON.stringify(declaredNames) !== JSON.stringify(expectedNames)) {
183
+ throw new PlatformError("COMMAND_FAILED", `managed dependency set mismatch: expected ${expectedNames.join(", ")}, observed ${declaredNames.join(", ")}`);
592
184
  }
593
- const operationLock = await acquireGlobalOperationLock(runtime.globalRoot);
594
- let binding = await loadGlobalBinding(runtime.globalRoot);
595
- try {
596
- let ctx;
597
- if (binding === undefined) {
598
- const requestedWorkspace = await resolveRequestedInstallWorkspace(args, runtime);
599
- ctx = await buildContext(requestedWorkspace);
600
- const plan = await loadPlan(ctx.paths, planRef);
601
- if (plan === undefined) {
602
- throw new PlatformError("PLAN_NOT_FOUND", `plan ${planRef} not found`);
603
- }
604
- if (plan.intent !== "install") {
605
- throw new PlatformError("WORKSPACE_NOT_BOUND", "only an install plan may establish the first global Workspace binding");
606
- }
607
- binding = (await claimWorkspaceBinding({
608
- workspace: requestedWorkspace,
609
- globalRoot: runtime.globalRoot,
610
- })).binding;
611
- }
612
- else {
613
- if (args.workspace !== undefined) {
614
- const requested = await canonicalizeWorkspace(args.workspace);
615
- if (requested.workspaceRealPath !== binding.workspaceRealPath) {
616
- throw new PlatformError("WORKSPACE_ALREADY_BOUND", `command targets ${requested.workspaceRealPath}, but the global Platform Instance is bound to ${binding.workspaceRealPath}`);
617
- }
618
- }
619
- ctx = await buildContext(binding.workspaceRealPath);
620
- }
621
- const plan = await loadPlan(ctx.paths, planRef);
622
- if (plan === undefined) {
623
- throw new PlatformError("PLAN_NOT_FOUND", `plan ${planRef} not found`);
624
- }
625
- const controlsPlatformState = plan.intent === "install" && binding.state !== "INSTALLED";
626
- if (binding.state === "UNINSTALLING") {
627
- throw new PlatformError("GLOBAL_OPERATION_LOCKED", "the bound Platform Instance is marked UNINSTALLING; finish recovery before apply");
628
- }
629
- if (controlsPlatformState && binding.state !== "INSTALLING") {
630
- binding = await updateGlobalBindingState({
631
- workspaceInstanceId: binding.workspaceInstanceId,
632
- state: "INSTALLING",
633
- globalRoot: runtime.globalRoot,
634
- });
635
- }
636
- try {
637
- const result = await handleApply(ctx, args);
638
- if (controlsPlatformState) {
639
- if (result.status === "SUCCEEDED") {
640
- binding = await updateGlobalBindingState({
641
- workspaceInstanceId: binding.workspaceInstanceId,
642
- state: "INSTALLED",
643
- globalRoot: runtime.globalRoot,
644
- });
645
- }
646
- else if (result.status === "FAILED" || result.status === "BLOCKED") {
647
- binding = await updateGlobalBindingState({
648
- workspaceInstanceId: binding.workspaceInstanceId,
649
- state: "BROKEN",
650
- globalRoot: runtime.globalRoot,
651
- failure: {
652
- code: result.error?.code ?? result.status,
653
- message: result.error?.message ??
654
- "install apply did not reach a successful postcondition",
655
- },
656
- });
657
- }
658
- }
659
- return withWorkspace(result, binding);
660
- }
661
- catch (error) {
662
- if (controlsPlatformState) {
663
- binding = await updateGlobalBindingState({
664
- workspaceInstanceId: binding.workspaceInstanceId,
665
- state: "BROKEN",
666
- globalRoot: runtime.globalRoot,
667
- failure: {
668
- code: error instanceof PlatformError ? error.code : "COMMAND_FAILED",
669
- message: error instanceof Error ? error.message : String(error),
670
- },
671
- });
672
- }
673
- throw error;
185
+ for (const candidate of candidates) {
186
+ const observed = await observeWorkspaceInstalledVersion(root, candidate.packageName);
187
+ if (observed !== candidate.moduleVersion) {
188
+ throw new PlatformError("COMMAND_FAILED", `installed version mismatch for ${candidate.packageName}: expected ${candidate.moduleVersion}, observed ${observed ?? "missing"}`);
674
189
  }
675
190
  }
676
- finally {
677
- await operationLock.release();
678
- }
679
- }
680
- async function handleInstallWithGlobalBinding(args, runtime) {
681
- const operationLock = await acquireGlobalOperationLock(runtime.globalRoot);
682
- try {
683
- const requestedWorkspace = await resolveRequestedInstallWorkspace(args, runtime);
684
- const existing = await loadGlobalBinding(runtime.globalRoot);
685
- if (existing !== undefined &&
686
- existing.workspaceRealPath !== requestedWorkspace) {
687
- throw new PlatformError("WORKSPACE_ALREADY_BOUND", `ProFlow is already bound to ${existing.workspaceRealPath}; uninstall it before installing ${requestedWorkspace}`);
688
- }
689
- if (existing?.state === "INSTALLED" && args.positional[0] === undefined) {
690
- return withWorkspace(outcome("install", "SUCCEEDED", {
691
- alreadyInstalled: true,
692
- changed: false,
693
- boundWorkspace: existing.workspaceRealPath,
694
- }), existing);
695
- }
696
- if (existing?.state === "UNINSTALLING") {
697
- throw new PlatformError("GLOBAL_OPERATION_LOCKED", "the bound Platform Instance is marked UNINSTALLING; finish recovery before installing");
698
- }
699
- // Gate A runs before claiming a fresh global binding. Deterministic local
700
- // failures (package-manager conflict, missing executable, registry down,
701
- // unwritable Workspace) therefore do not leave a fake BROKEN installation.
702
- // The global operation lock keeps this preflight + claim sequence atomic
703
- // against other Platform mutations.
704
- const installer = await preflightInstallerEnvironment({
705
- workspaceRoot: requestedWorkspace,
706
- });
707
- if (installer.status !== "READY") {
708
- return outcome("install", installer.status === "ACTION_REQUIRED" ? "ACTION_REQUIRED" : "BLOCKED", { requestedWorkspace, preflight: installer });
191
+ for (const stale of previousManaged.filter((name) => !expectedNames.includes(name))) {
192
+ if ((await observeWorkspaceInstalledVersion(root, stale)) !== undefined) {
193
+ throw new PlatformError("COMMAND_FAILED", `stale managed package remains installed after synchronization: ${stale}`);
709
194
  }
710
- const claimed = await claimWorkspaceBinding({
711
- workspace: requestedWorkspace,
712
- globalRoot: runtime.globalRoot,
713
- });
714
- const initialBinding = claimed.binding;
715
- let activeBinding = initialBinding;
716
- const controlsPlatformState = !claimed.alreadyBound || initialBinding.state !== "INSTALLED";
717
- if (controlsPlatformState && initialBinding.state !== "INSTALLING") {
718
- activeBinding = await updateGlobalBindingState({
719
- workspaceInstanceId: initialBinding.workspaceInstanceId,
720
- state: "INSTALLING",
721
- globalRoot: runtime.globalRoot,
722
- });
723
- }
724
- try {
725
- const ctx = await buildContext(activeBinding.workspaceRealPath);
726
- const result = await handleManagedMutation(ctx, args, "install");
727
- if (controlsPlatformState) {
728
- if (result.status === "SUCCEEDED") {
729
- activeBinding = await updateGlobalBindingState({
730
- workspaceInstanceId: activeBinding.workspaceInstanceId,
731
- state: "INSTALLED",
732
- globalRoot: runtime.globalRoot,
733
- });
734
- }
735
- else if (result.status === "FAILED" || result.status === "BLOCKED") {
736
- activeBinding = await updateGlobalBindingState({
737
- workspaceInstanceId: activeBinding.workspaceInstanceId,
738
- state: "BROKEN",
739
- globalRoot: runtime.globalRoot,
740
- failure: {
741
- code: result.error?.code ?? result.status,
742
- message: result.error?.message ??
743
- "Platform install did not reach a successful postcondition",
744
- },
745
- });
746
- }
747
- }
748
- return withWorkspace(result, activeBinding);
749
- }
750
- catch (error) {
751
- if (controlsPlatformState) {
752
- activeBinding = await updateGlobalBindingState({
753
- workspaceInstanceId: activeBinding.workspaceInstanceId,
754
- state: "BROKEN",
755
- globalRoot: runtime.globalRoot,
756
- failure: {
757
- code: error instanceof PlatformError ? error.code : "COMMAND_FAILED",
758
- message: error instanceof Error ? error.message : String(error),
759
- },
760
- });
761
- }
762
- throw error;
763
- }
764
- }
765
- finally {
766
- await operationLock.release();
767
195
  }
768
- }
769
- async function resolveBoundContext(runtime, requestedWorkspace) {
770
- const binding = await requireBoundWorkspace(runtime.globalRoot);
771
- if (requestedWorkspace !== undefined) {
772
- const requested = await canonicalizeWorkspace(requestedWorkspace);
773
- if (requested.workspaceRealPath !== binding.workspaceRealPath) {
774
- throw new PlatformError("WORKSPACE_ALREADY_BOUND", `command targets ${requested.workspaceRealPath}, but the global Platform Instance is bound to ${binding.workspaceRealPath}`);
196
+ const catalog = new InstalledModuleCatalog(root);
197
+ const sources = await catalog.sources();
198
+ const modules = await discoverModules({ catalog, sources });
199
+ const byPackage = new Map(modules.map((module) => [module.packageName, module]));
200
+ for (const candidate of candidates) {
201
+ const module = byPackage.get(candidate.packageName);
202
+ if (module === undefined ||
203
+ module.moduleVersion !== candidate.moduleVersion) {
204
+ throw new PlatformError("DESCRIPTOR_INVALID", `installed descriptor mismatch for ${candidate.packageName}@${candidate.moduleVersion}`);
775
205
  }
776
206
  }
777
- return {
778
- ctx: await buildContext(binding.workspaceRealPath),
779
- binding,
780
- };
781
207
  }
782
- async function handleBoundMutatingCommand(args, runtime) {
783
- const operationLock = await acquireGlobalOperationLock(runtime.globalRoot);
784
- try {
785
- const { ctx, binding } = await resolveBoundContext(runtime, args.workspace);
786
- if (binding.state === "UNINSTALLING") {
787
- throw new PlatformError("GLOBAL_OPERATION_LOCKED", "the bound Platform Instance is marked UNINSTALLING; finish uninstall recovery before another mutation");
788
- }
789
- let result;
790
- if (args.command === "upgrade" || args.command === "uninstall") {
791
- result = await handleManagedMutation(ctx, args, args.command);
792
- }
793
- else {
794
- result = await handleLifecycle(ctx, args, args.command);
795
- }
796
- return withWorkspace(result, binding);
208
+ async function handleInstall(root, runtime) {
209
+ const discovered = await discoverRegistryModules({
210
+ workspaceRoot: root,
211
+ ...(runtime.registryRunner === undefined
212
+ ? {}
213
+ : { runner: runtime.registryRunner }),
214
+ });
215
+ if (discovered.rejected.length > 0) {
216
+ throw new PlatformError("REGISTRY_RESPONSE_INVALID", `registry contains rejected ProFlow packages: ${discovered.rejected.map((item) => `${item.packageName}:${item.reason}`).join(", ")}`);
797
217
  }
798
- finally {
799
- await operationLock.release();
218
+ if (discovered.candidates.length === 0) {
219
+ throw new PlatformError("PACKAGE_NOT_FOUND", "no ProFlow packages were discovered in the configured scope");
800
220
  }
221
+ const previousManaged = await workspaceProFlowDependencies(root);
222
+ const mutation = await syncWorkspacePackages({
223
+ workspaceRoot: root,
224
+ packages: discovered.candidates.map((item) => ({
225
+ packageName: item.packageName,
226
+ version: item.moduleVersion,
227
+ })),
228
+ ...(runtime.packageRunner === undefined
229
+ ? {}
230
+ : { runner: runtime.packageRunner }),
231
+ ...(runtime.executableAvailable === undefined
232
+ ? {}
233
+ : { executableAvailable: runtime.executableAvailable }),
234
+ });
235
+ await validateInstalledPackageSet(root, discovered.candidates, previousManaged);
236
+ const metadata = await ensureWorkspaceMetadata(root);
237
+ return outcome("install", "SUCCEEDED", root, {
238
+ registry: discovered.registry,
239
+ packageManager: mutation.packageManager,
240
+ packages: discovered.candidates.map((item) => ({
241
+ packageName: item.packageName,
242
+ version: item.moduleVersion,
243
+ })),
244
+ workspace: metadata,
245
+ next: "platform modules",
246
+ });
801
247
  }
802
- async function handlePlatformInstanceUninstall(args, runtime) {
803
- const operationLock = await acquireGlobalOperationLock(runtime.globalRoot);
804
- let binding;
248
+ async function workspaceProFlowDependencies(root) {
805
249
  try {
806
- binding = await requireBoundWorkspace(runtime.globalRoot);
807
- if (args.workspace !== undefined) {
808
- const requested = await canonicalizeWorkspace(args.workspace);
809
- if (requested.workspaceRealPath !== binding.workspaceRealPath) {
810
- throw new PlatformError("WORKSPACE_ALREADY_BOUND", `uninstall targets ${requested.workspaceRealPath}, but the global Platform Instance is bound to ${binding.workspaceRealPath}`);
811
- }
812
- }
813
- binding = await updateGlobalBindingState({
814
- workspaceInstanceId: binding.workspaceInstanceId,
815
- state: "UNINSTALLING",
816
- globalRoot: runtime.globalRoot,
817
- });
818
- const ctx = await buildContext(binding.workspaceRealPath);
819
- const modules = await discoverModules({ catalog: ctx.catalog });
820
- let removedModules = [];
821
- if (modules.length > 0) {
822
- const plan = planDeployment({
823
- intent: "uninstall",
824
- modules,
825
- uninstallScope: "platform-instance",
826
- });
827
- await savePlan(ctx.paths, plan);
828
- const current = await rebuildCurrentAssumptions(ctx.catalog, plan, ctx.paths);
829
- const applied = await applyPlan({
830
- paths: ctx.paths,
831
- planRef: plan.planRef,
832
- catalog: ctx.catalog,
833
- current,
834
- driver: createWorkspacePackageManagerDriver({
835
- workspaceRoot: ctx.paths.root,
836
- }),
837
- });
838
- if (applied.outcome !== "COMPLETE") {
839
- throw new PlatformError("UNINSTALL_FAILED", `Platform Instance uninstall stopped with ${applied.outcome}`);
840
- }
841
- removedModules = modules.map((module) => module.moduleRef);
842
- }
843
- const refreshed = await buildContext(binding.workspaceRealPath);
844
- const remaining = await discoverModules({ catalog: refreshed.catalog });
845
- if (remaining.length > 0) {
846
- throw new PlatformError("UNINSTALL_FAILED", `Platform Instance uninstall left managed modules: ${remaining.map((module) => module.moduleRef).join(", ")}`);
847
- }
848
- if (modules.length > 0) {
849
- await cleanupWorkspacePackageManagerArtifacts({
850
- workspaceRoot: binding.workspaceRealPath,
851
- removedModules: modules,
852
- });
853
- }
854
- // Deployment-owned plans/state/verification/instance identity must not leak
855
- // into a future install of the same directory. Business/domain data outside
856
- // `.proflow/deployment` is intentionally preserved.
857
- await rm(ctx.paths.deployment, { recursive: true, force: true });
858
- for (const path of [
859
- ctx.paths.logsDeployment,
860
- ctx.paths.logs,
861
- ctx.paths.config,
862
- ctx.paths.data,
863
- ctx.paths.runtime,
864
- ctx.paths.cache,
865
- ctx.paths.tmp,
866
- ctx.paths.proflow,
867
- ]) {
868
- try {
869
- await rmdir(path);
870
- }
871
- catch (error) {
872
- const code = typeof error === "object" && error !== null
873
- ? Reflect.get(error, "code")
874
- : undefined;
875
- if (code !== "ENOENT" && code !== "ENOTEMPTY")
876
- throw error;
250
+ const parsed = JSON.parse(await readFile(resolve(root, "package.json"), "utf8"));
251
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
252
+ return [];
253
+ const record = parsed;
254
+ const names = new Set();
255
+ for (const field of ["dependencies", "devDependencies"]) {
256
+ const value = record[field];
257
+ if (typeof value !== "object" || value === null || Array.isArray(value))
258
+ continue;
259
+ for (const name of Object.keys(value)) {
260
+ if (name.startsWith(PRO_FLOW_PACKAGE_PREFIX))
261
+ names.add(name);
877
262
  }
878
263
  }
879
- const completed = binding;
880
- await clearGlobalBinding({
881
- workspaceInstanceId: binding.workspaceInstanceId,
882
- globalRoot: runtime.globalRoot,
883
- });
884
- binding = undefined;
885
- return outcome("uninstall", "SUCCEEDED", {
886
- uninstalledWorkspace: completed.workspaceRealPath,
887
- removedModules,
888
- bindingCleared: true,
889
- });
264
+ return [...names].sort();
890
265
  }
891
266
  catch (error) {
892
- if (binding !== undefined) {
893
- await updateGlobalBindingState({
894
- workspaceInstanceId: binding.workspaceInstanceId,
895
- state: "BROKEN",
896
- globalRoot: runtime.globalRoot,
897
- failure: {
898
- code: error instanceof PlatformError ? error.code : "UNINSTALL_FAILED",
899
- message: error instanceof Error ? error.message : String(error),
900
- },
901
- });
902
- }
903
- throw error;
904
- }
905
- finally {
906
- await operationLock.release();
907
- }
908
- }
909
- async function handleLifecycle(ctx, args, primitive) {
910
- const modules = await discoverModules({ catalog: ctx.catalog });
911
- const selected = selectModules(modules, args.positional[0]);
912
- const result = primitive === "start"
913
- ? await startModules(ctx.catalog, selected)
914
- : primitive === "stop"
915
- ? await stopModules(ctx.catalog, selected)
916
- : primitive === "restart"
917
- ? await restartModules(ctx.catalog, selected)
918
- : await statusModules(ctx.catalog, selected);
919
- const statuses = result.flatMap((item) => item.result ? [item.result.status] : []);
920
- const status = aggregateStatus(statuses);
921
- if (primitive === "status" && !args.positional[0]) {
922
- await writeDeploymentObserverSummary({
923
- paths: ctx.paths,
924
- source: "status",
925
- selectedModuleCount: selected.length,
926
- totalModuleCount: modules.length,
927
- modules: result.flatMap((item) => item.result
928
- ? [{ moduleRef: item.moduleRef, status: item.result.status }]
929
- : []),
930
- });
931
- }
932
- return outcome(primitive, status, result);
933
- }
934
- async function handleVerify(ctx, args) {
935
- const modules = await discoverModules({ catalog: ctx.catalog });
936
- const selected = selectModules(modules, args.positional[0]);
937
- const result = await verifyModules(ctx.catalog, selected, ctx.paths);
938
- const status = aggregateStatus(result.map((item) => item.result.status));
939
- if (!args.positional[0]) {
940
- await writeDeploymentObserverSummary({
941
- paths: ctx.paths,
942
- source: "verify",
943
- selectedModuleCount: selected.length,
944
- totalModuleCount: modules.length,
945
- modules: result.map((item) => ({
946
- moduleRef: item.moduleRef,
947
- status: item.result.status,
948
- })),
949
- });
267
+ if (typeof error === "object" &&
268
+ error !== null &&
269
+ Reflect.get(error, "code") === "ENOENT")
270
+ return [];
271
+ throw new PlatformError("INVALID_REQUEST", `cannot read Workspace package.json: ${error instanceof Error ? error.message : String(error)}`);
950
272
  }
951
- return outcome("verify", status, result);
952
273
  }
953
- async function handleDoctor(ctx, args) {
954
- const modules = await discoverModules({ catalog: ctx.catalog });
955
- const selected = selectModules(modules, args.positional[0]);
956
- const result = await doctorModules(ctx.catalog, selected);
957
- const status = aggregateStatus(result.map((item) => item.status));
958
- if (!args.positional[0]) {
959
- await writeDeploymentObserverSummary({
960
- paths: ctx.paths,
961
- source: "doctor",
962
- selectedModuleCount: selected.length,
963
- totalModuleCount: modules.length,
964
- modules: result.map((item) => ({
965
- moduleRef: item.moduleRef,
966
- status: item.status,
967
- })),
968
- });
969
- }
970
- return outcome("doctor", status, result);
274
+ async function handleUninstall(root, runtime) {
275
+ const packageNames = await workspaceProFlowDependencies(root);
276
+ const mutation = await removeWorkspacePackages({
277
+ workspaceRoot: root,
278
+ packageNames,
279
+ ...(runtime.packageRunner === undefined
280
+ ? {}
281
+ : { runner: runtime.packageRunner }),
282
+ ...(runtime.executableAvailable === undefined
283
+ ? {}
284
+ : { executableAvailable: runtime.executableAvailable }),
285
+ });
286
+ return outcome("uninstall", "SUCCEEDED", root, {
287
+ packageManager: mutation.packageManager,
288
+ removed: packageNames,
289
+ preserved: [".proflow"],
290
+ });
971
291
  }
972
- async function handleManifest(ctx, args) {
973
- const modules = await discoverModules({ catalog: ctx.catalog });
974
- const selected = selectModules(modules, args.positional[0]);
975
- const config = await loadEffectiveConfig(ctx.paths, selected, args.configFile);
976
- const manifest = await buildManifest({
977
- catalog: ctx.catalog,
978
- modules: selected,
979
- paths: ctx.paths,
980
- config,
292
+ async function handleStart(root) {
293
+ const { catalog, modules } = await buildContext(root);
294
+ const result = await preflightAndStartModules(catalog, modules);
295
+ return outcome("start", result.completed ? "SUCCEEDED" : failedLifecycleStatus(result.results), root, result);
296
+ }
297
+ async function handleStop(root) {
298
+ const { catalog, modules } = await buildContext(root);
299
+ const result = await stopModulesThin(catalog, modules);
300
+ return outcome("stop", result.completed ? "SUCCEEDED" : failedLifecycleStatus(result.results), root, result);
301
+ }
302
+ function helpOutcome() {
303
+ return outcome("help", "SUCCEEDED", undefined, {
304
+ usage: "platform <modules|docs|install|uninstall|start|stop> [--json]",
305
+ commands: [...COMMANDS],
306
+ install: "platform install [--workspace <path>]",
981
307
  });
982
- await generateInstallDoc({ modules: selected, config, paths: ctx.paths });
983
- switch (manifest.status) {
984
- case "READY":
985
- return outcome("manifest", "SUCCEEDED", manifest);
986
- case "ACTION_REQUIRED":
987
- return outcome("manifest", "ACTION_REQUIRED", manifest);
988
- case "DEGRADED":
989
- return outcome("manifest", "BLOCKED", manifest);
990
- case "NOT_READY":
991
- return outcome("manifest", "FAILED", manifest);
992
- }
993
308
  }
994
- async function dispatchCommand(argv, runtime = {}) {
995
- let args;
309
+ export async function runCli(argv, runtime = {}) {
310
+ let parsed;
996
311
  try {
997
- args = parseArgs(argv);
312
+ parsed = parseArgs(argv);
998
313
  }
999
314
  catch (error) {
1000
- return failure(argv[0] ?? "", error);
1001
- }
1002
- if (!COMMANDS.includes(args.command)) {
1003
- return {
1004
- command: args.command,
1005
- status: "FAILED",
1006
- error: {
1007
- code: "INVALID_REQUEST",
1008
- message: `unknown command: ${args.command}`,
1009
- },
1010
- };
315
+ return JSON.stringify(errorOutcome("unknown", error));
1011
316
  }
1012
317
  try {
1013
- const cwd = runtime.cwd ?? process.cwd();
1014
- if (args.command === "search") {
1015
- const current = await loadGlobalBinding(runtime.globalRoot);
1016
- const root = args.workspace ?? current?.workspaceRealPath ?? cwd;
1017
- return await handleSearch(root, args);
1018
- }
1019
- if (args.command === "preflight" && args.intent === "install") {
1020
- const root = await resolveRequestedInstallWorkspace(args, runtime);
1021
- const current = await loadGlobalBinding(runtime.globalRoot);
1022
- if (current !== undefined && current.workspaceRealPath !== root) {
1023
- throw new PlatformError("WORKSPACE_ALREADY_BOUND", `ProFlow is already bound to ${current.workspaceRealPath}; uninstall it before installing ${root}`);
1024
- }
1025
- return await handleInstallerPreflight(root);
1026
- }
1027
- if (args.command === "plan") {
1028
- if (args.intent === "install") {
1029
- return await handleInstallPlanBeforeBinding(args, runtime);
1030
- }
1031
- if (args.intent === undefined ||
1032
- !["configure", "upgrade", "uninstall", "repair"].includes(args.intent)) {
1033
- const current = await loadGlobalBinding(runtime.globalRoot);
1034
- const root = args.workspace ??
1035
- current?.workspaceRealPath ??
1036
- runtime.cwd ??
1037
- process.cwd();
1038
- return await handlePlan(await buildContext(root), args);
1039
- }
1040
- }
1041
- if (args.command === "apply") {
1042
- return await handleApplyWithGlobalBinding(args, runtime);
1043
- }
1044
- if (args.command === "install") {
1045
- return await handleInstallWithGlobalBinding(args, runtime);
1046
- }
1047
- if (args.command === "status") {
1048
- const observation = await observeBoundWorkspace(runtime.globalRoot);
1049
- if (observation === undefined) {
1050
- return outcome("status", "SUCCEEDED", {
1051
- installed: false,
1052
- bindingState: "UNBOUND",
1053
- boundWorkspace: null,
1054
- nextAction: "Run platform install [--workspace <path>]",
1055
- });
1056
- }
1057
- if (!observation.workspaceExists) {
1058
- return withWorkspace(outcome("status", "BLOCKED", {
1059
- installed: false,
1060
- code: "BOUND_WORKSPACE_MISSING",
1061
- boundWorkspace: observation.binding.workspaceRealPath,
1062
- nextAction: "Restore the Workspace or run platform uninstall --forget to clear only the stale binding",
1063
- }), observation.binding);
1064
- }
1065
- }
1066
- if (args.command === "uninstall" && args.forget) {
1067
- if (args.positional.length > 0 || args.workspace !== undefined) {
1068
- throw new PlatformError("INVALID_REQUEST", "platform uninstall --forget only clears the current stale global binding; do not combine it with a module/package or --workspace");
1069
- }
1070
- const operationLock = await acquireGlobalOperationLock(runtime.globalRoot);
1071
- try {
1072
- const forgotten = await forgetMissingWorkspaceBinding({
1073
- globalRoot: runtime.globalRoot,
1074
- });
1075
- return outcome("uninstall", "SUCCEEDED", {
1076
- forgottenWorkspace: forgotten.workspaceRealPath,
1077
- bindingCleared: true,
1078
- resourcesCleaned: false,
1079
- });
1080
- }
1081
- finally {
1082
- await operationLock.release();
1083
- }
1084
- }
1085
- if (args.command === "uninstall" && args.positional.length === 0) {
1086
- const current = await loadGlobalBinding(runtime.globalRoot);
1087
- if (current === undefined) {
1088
- return outcome("uninstall", "SUCCEEDED", {
1089
- alreadyUninstalled: true,
1090
- bindingCleared: true,
1091
- });
1092
- }
1093
- return await handlePlatformInstanceUninstall(args, runtime);
1094
- }
1095
- if (args.command === "upgrade" ||
1096
- (args.command === "uninstall" && args.positional.length > 0) ||
1097
- args.command === "start" ||
1098
- args.command === "stop" ||
1099
- args.command === "restart") {
1100
- return await handleBoundMutatingCommand(args, runtime);
1101
- }
1102
- const { ctx, binding } = await resolveBoundContext(runtime, args.workspace);
1103
- let result;
1104
- switch (args.command) {
1105
- case "search":
1106
- case "install":
1107
- throw new PlatformError("COMMAND_FAILED", "unreachable command routing");
318
+ if (parsed.command === "help")
319
+ return JSON.stringify(helpOutcome());
320
+ if (parsed.command === "version") {
321
+ return JSON.stringify(outcome("version", "SUCCEEDED", undefined, {
322
+ version: platformCliDescriptor.moduleVersion,
323
+ }));
324
+ }
325
+ const cwd = await canonicalWorkspace(runtime.cwd ?? process.cwd());
326
+ const root = parsed.command === "install" && parsed.workspace !== undefined
327
+ ? await canonicalWorkspace(cwd, parsed.workspace)
328
+ : cwd;
329
+ switch (parsed.command) {
1108
330
  case "modules":
1109
- result = await handleModules(ctx, args);
1110
- break;
331
+ return JSON.stringify(await handleModules(root));
1111
332
  case "docs":
1112
- result = await handleDocs(ctx, args);
1113
- break;
333
+ return JSON.stringify(await handleDocs(root));
334
+ case "install":
335
+ return JSON.stringify(await handleInstall(root, runtime));
1114
336
  case "uninstall":
1115
- case "upgrade":
337
+ return JSON.stringify(await handleUninstall(root, runtime));
1116
338
  case "start":
339
+ return JSON.stringify(await handleStart(root));
1117
340
  case "stop":
1118
- case "restart":
1119
- throw new PlatformError("COMMAND_FAILED", "unreachable mutating command routing");
1120
- case "preflight":
1121
- result = await handlePreflight(ctx, args);
1122
- break;
1123
- case "plan":
1124
- result = await handlePlan(ctx, args);
1125
- break;
1126
- case "apply":
1127
- result = await handleApply(ctx, args);
1128
- break;
1129
- case "status":
1130
- result = await handleLifecycle(ctx, args, "status");
1131
- break;
1132
- case "verify":
1133
- result = await handleVerify(ctx, args);
1134
- break;
1135
- case "doctor":
1136
- result = await handleDoctor(ctx, args);
1137
- break;
1138
- case "manifest":
1139
- result = await handleManifest(ctx, args);
1140
- break;
341
+ return JSON.stringify(await handleStop(root));
1141
342
  }
1142
- if (result === undefined) {
1143
- throw new PlatformError("COMMAND_FAILED", "unreachable command routing");
1144
- }
1145
- return withWorkspace(result, binding);
1146
343
  }
1147
344
  catch (error) {
1148
- return failure(args.command, error);
345
+ return JSON.stringify(errorOutcome(parsed.command, error));
1149
346
  }
1150
347
  }
1151
- function toMachineResult(outcomeResult) {
1152
- const ok = outcomeResult.status === "SUCCEEDED";
1153
- return JSON.stringify({
1154
- contract: "deployment.result.v1",
1155
- ok,
1156
- status: outcomeResult.status,
1157
- moduleRef: MODULE_REF,
1158
- moduleVersion: MODULE_VERSION,
1159
- ...(outcomeResult.workspace !== undefined
1160
- ? { workspace: outcomeResult.workspace }
1161
- : {}),
1162
- ...(outcomeResult.data !== undefined ? { data: outcomeResult.data } : {}),
1163
- ...(outcomeResult.status === "ACTION_REQUIRED"
1164
- ? {
1165
- actionRequired: {
1166
- action: "resolve-blocking-action",
1167
- description: "one or more steps require a human action",
1168
- },
1169
- }
1170
- : {}),
1171
- ...(outcomeResult.error !== undefined
1172
- ? {
1173
- error: {
1174
- code: outcomeResult.error.code,
1175
- message: outcomeResult.error.message,
1176
- retryable: false,
1177
- },
1178
- }
1179
- : {}),
1180
- });
348
+ function errorOutcome(command, error) {
349
+ if (error instanceof PlatformError) {
350
+ return {
351
+ command,
352
+ status: "FAILED",
353
+ error: { code: error.code, message: error.message },
354
+ };
355
+ }
356
+ return {
357
+ command,
358
+ status: "FAILED",
359
+ error: {
360
+ code: "COMMAND_FAILED",
361
+ message: error instanceof Error ? error.message : String(error),
362
+ },
363
+ };
1181
364
  }
1182
365
  function isRecord(value) {
1183
366
  return typeof value === "object" && value !== null && !Array.isArray(value);
1184
367
  }
1185
- function humanDetailLines(data) {
1186
- if (!isRecord(data)) {
1187
- return data === undefined ? [] : [String(data)];
1188
- }
1189
- const lines = [];
1190
- for (const key of [
1191
- "installed",
1192
- "bindingState",
1193
- "boundWorkspace",
1194
- "requestedWorkspace",
1195
- "alreadyInstalled",
1196
- "alreadyLatest",
1197
- "changed",
1198
- "bindingCleared",
1199
- "resourcesCleaned",
1200
- "planRef",
1201
- "outcome",
1202
- ]) {
1203
- const value = data[key];
1204
- if (typeof value === "string" ||
1205
- typeof value === "number" ||
1206
- typeof value === "boolean") {
1207
- lines.push(`${key}: ${String(value)}`);
1208
- }
1209
- }
1210
- if (typeof data.nextAction === "string" && data.nextAction !== "") {
1211
- lines.push(`Next: ${data.nextAction}`);
368
+ function renderModules(data) {
369
+ if (!isRecord(data) || !Array.isArray(data.modules))
370
+ return "No modules.";
371
+ const lines = ["ProFlow Modules", ""];
372
+ for (const raw of data.modules) {
373
+ if (!isRecord(raw))
374
+ continue;
375
+ const missing = Array.isArray(raw.missingConfig)
376
+ ? raw.missingConfig
377
+ .filter((item) => typeof item === "string")
378
+ .join(",")
379
+ : "-";
380
+ lines.push(`${String(raw.moduleRef)} ${String(raw.version)} config=${String(raw.configStatus)} runtime=${String(raw.runtimeStatus)} missing=${missing}`);
1212
381
  }
1213
- const findings = data.findings;
1214
- let hasMissingConfig = false;
1215
- if (Array.isArray(findings) && findings.length > 0) {
1216
- lines.push("Findings:");
1217
- for (const finding of findings) {
1218
- if (!isRecord(finding))
382
+ return lines.join("\n");
383
+ }
384
+ function renderDocs(data) {
385
+ if (!isRecord(data) || !Array.isArray(data.modules))
386
+ return "No module docs.";
387
+ const lines = ["ProFlow Docs"];
388
+ for (const raw of data.modules) {
389
+ if (!isRecord(raw))
390
+ continue;
391
+ lines.push("", `## ${String(raw.moduleRef)} @ ${String(raw.version)}`);
392
+ if (!Array.isArray(raw.documents))
393
+ continue;
394
+ for (const document of raw.documents) {
395
+ if (!isRecord(document))
1219
396
  continue;
1220
- const severity = typeof finding.severity === "string"
1221
- ? finding.severity.toUpperCase()
1222
- : "INFO";
1223
- const code = typeof finding.code === "string" ? finding.code : "CHECK";
1224
- if (code === "CONFIG_MISSING")
1225
- hasMissingConfig = true;
1226
- const moduleRef = typeof finding.moduleRef === "string" ? ` ${finding.moduleRef}` : "";
1227
- const message = typeof finding.message === "string" ? finding.message : "";
1228
- lines.push(`- [${severity}] ${code}${moduleRef}${message === "" ? "" : `: ${message}`}`);
1229
- }
1230
- }
1231
- if (hasMissingConfig) {
1232
- lines.push("Next: review .proflow/deployment/generated/INSTALL.md, create the required config file, then run platform plan --intent configure --config <file> and platform apply <planRef>.");
1233
- }
1234
- const preflight = data.preflight;
1235
- if (isRecord(preflight)) {
1236
- const nested = humanDetailLines(preflight);
1237
- if (nested.length > 0) {
1238
- lines.push("Preflight:", ...nested.map((line) => ` ${line}`));
397
+ lines.push("", `### ${String(document.id)}`, String(document.content ?? ""));
1239
398
  }
1240
399
  }
1241
- return lines;
400
+ return lines.join("\n");
1242
401
  }
1243
- export function renderHumanResult(machineOutput) {
1244
- const parsed = JSON.parse(machineOutput);
1245
- if (!isRecord(parsed))
1246
- return machineOutput;
1247
- const data = parsed.data;
1248
- if (isRecord(data) && typeof data.version === "string") {
1249
- return `ProFlow Platform CLI ${data.version}`;
402
+ export function renderHumanResult(result) {
403
+ if (result.status === "FAILED") {
404
+ return `${result.command.toUpperCase()} FAILED${result.error ? ` [${result.error.code}] ${result.error.message}` : ""}`;
1250
405
  }
1251
- if (isRecord(data) && typeof data.usage === "string") {
1252
- const commands = Array.isArray(data.commands)
1253
- ? data.commands.filter((item) => typeof item === "string")
1254
- : [];
1255
- const examples = Array.isArray(data.examples)
1256
- ? data.examples.filter((item) => typeof item === "string")
1257
- : [];
406
+ if (result.command === "help") {
1258
407
  return [
1259
408
  "ProFlow Platform CLI",
1260
409
  "",
1261
- `Usage: ${data.usage}`,
1262
- ...(commands.length === 0
1263
- ? []
1264
- : ["", `Commands: ${commands.join(", ")}`]),
1265
- ...(examples.length === 0
1266
- ? []
1267
- : ["", "Common flows:", ...examples.map((item) => ` ${item}`)]),
410
+ ...COMMANDS.map((command) => `platform ${command}`),
411
+ "",
412
+ "platform install --workspace <path>",
413
+ "append --json for machine-readable output",
1268
414
  ].join("\n");
1269
415
  }
1270
- const status = typeof parsed.status === "string" ? parsed.status : "UNKNOWN";
1271
- const lines = [`ProFlow: ${status}`];
1272
- const workspace = parsed.workspace;
1273
- if (isRecord(workspace) && typeof workspace.boundWorkspace === "string") {
1274
- lines.push(`Workspace: ${workspace.boundWorkspace}`);
1275
- }
1276
- const error = parsed.error;
1277
- if (isRecord(error)) {
1278
- const code = typeof error.code === "string" ? error.code : "COMMAND_FAILED";
1279
- const message = typeof error.message === "string" ? error.message : "Unknown error";
1280
- lines.push(`Error: ${code}: ${message}`);
1281
- }
1282
- lines.push(...humanDetailLines(data));
1283
- if (lines.length === 1 && status === "SUCCEEDED") {
1284
- lines.push(`Version: ${MODULE_VERSION}`, "Next: run platform --help");
1285
- }
1286
- return lines.join("\n");
1287
- }
1288
- export async function runCli(argv, runtime = {}) {
1289
- const filtered = argv.filter((argument) => argument !== "--json");
1290
- if (filtered.length === 1 &&
1291
- (filtered[0] === "--version" || filtered[0] === "-v")) {
1292
- return JSON.stringify({
1293
- contract: "deployment.result.v1",
1294
- ok: true,
1295
- status: "SUCCEEDED",
1296
- moduleRef: MODULE_REF,
1297
- moduleVersion: MODULE_VERSION,
1298
- data: { version: MODULE_VERSION },
1299
- });
1300
- }
1301
- if (argv.includes("--help") || argv.includes("-h")) {
1302
- return JSON.stringify({
1303
- contract: "deployment.result.v1",
1304
- ok: true,
1305
- status: "SUCCEEDED",
1306
- moduleRef: MODULE_REF,
1307
- moduleVersion: MODULE_VERSION,
1308
- data: {
1309
- usage: "platform <command> [module|package] [--workspace <path>] [--json]",
1310
- commands: [...COMMANDS],
1311
- examples: [
1312
- "platform preflight --intent install --workspace <path>",
1313
- "platform install [module|package] [--workspace <path>]",
1314
- "platform preflight # startup/runtime readiness for the bound Workspace",
1315
- "platform plan --intent configure --config <file>",
1316
- "platform apply <planRef>",
1317
- "platform start | status | verify | doctor | stop | restart",
1318
- "platform uninstall # remove managed modules and .proflow/deployment state; preserve business/domain data outside .proflow/deployment; clear binding; keep the global CLI; v1 has no purge mode",
1319
- "append --json for the stable machine-readable contract",
1320
- ],
1321
- },
1322
- });
1323
- }
1324
- if (filtered.length === 0) {
1325
- return JSON.stringify({
1326
- contract: "deployment.result.v1",
1327
- ok: true,
1328
- status: "SUCCEEDED",
1329
- moduleRef: MODULE_REF,
1330
- moduleVersion: MODULE_VERSION,
1331
- });
1332
- }
1333
- return toMachineResult(await dispatchCommand(filtered, runtime));
416
+ if (result.command === "version" && isRecord(result.data)) {
417
+ return String(result.data.version ?? platformCliDescriptor.moduleVersion);
418
+ }
419
+ if (result.command === "modules")
420
+ return renderModules(result.data);
421
+ if (result.command === "docs")
422
+ return renderDocs(result.data);
423
+ return [
424
+ `${result.command.toUpperCase()} ${result.status}`,
425
+ ...(result.workspaceRoot ? [`Workspace: ${result.workspaceRoot}`] : []),
426
+ ...(result.data === undefined
427
+ ? []
428
+ : [JSON.stringify(result.data, null, 2)]),
429
+ ].join("\n");
1334
430
  }
1335
431
  if (import.meta.main) {
1336
432
  const argv = process.argv.slice(2);
1337
433
  const output = await runCli(argv);
1338
- const rendered = argv.includes("--json") ? output : renderHumanResult(output);
1339
- process.stdout.write(`${rendered}\n`);
1340
- const parsed = JSON.parse(output);
1341
- if (typeof parsed === "object" &&
1342
- parsed !== null &&
1343
- Reflect.get(parsed, "ok") === false) {
434
+ const result = JSON.parse(output);
435
+ process.stdout.write(`${argv.includes("--json") ? output : renderHumanResult(result)}\n`);
436
+ if (result.status !== "SUCCEEDED")
1344
437
  process.exitCode = 1;
1345
- }
1346
438
  }