@tomflow/proflow-platform-cli 0.1.14 → 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 +327 -1316
  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 -31
  29. package/dist/src/lifecycle/dispatch.js +0 -125
  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 -85
  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,1427 +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 },
133
- };
134
- }
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,
88
+ status,
89
+ ...(workspaceRoot === undefined ? {} : { workspaceRoot }),
90
+ ...(data === undefined ? {} : { data }),
151
91
  };
152
- if (module.source.path !== undefined)
153
- source.path = module.source.path;
154
- return source;
155
92
  }
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));
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);
161
98
  }
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`);
99
+ catch (error) {
100
+ throw new PlatformError("WORKSPACE_NOT_FOUND", `Workspace does not exist: ${candidate} (${error instanceof Error ? error.message : String(error)})`);
171
101
  }
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`);
102
+ if (!info.isDirectory()) {
103
+ throw new PlatformError("WORKSPACE_NOT_FOUND", `Workspace is not a directory: ${candidate}`);
179
104
  }
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;
190
- }
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);
127
+ const catalog = new AutoModuleCatalog(root, bindings);
128
+ const modules = await discoverModules({ workspaceRoot: root, catalog });
129
+ return { catalog, modules };
237
130
  }
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
- })));
252
- }
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;
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 });
576
178
  }
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}`);
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(", ")}`);
583
184
  }
584
- const ctx = await buildContext(current?.workspaceRealPath ?? requestedWorkspace);
585
- const result = await handlePlan(ctx, args);
586
- return current === undefined ? result : withWorkspace(result, current);
587
- }
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>");
592
- }
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);
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}`);
695
194
  }
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 });
709
- }
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
- if (primitive === "start") {
911
- const preflight = await handlePreflight(ctx, args);
912
- if (preflight.status !== "SUCCEEDED") {
913
- return outcome("start", preflight.status, preflight.data);
914
- }
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)}`);
915
272
  }
916
- const modules = await discoverModules({ catalog: ctx.catalog });
917
- const selected = selectModules(modules, args.positional[0]);
918
- const result = primitive === "start"
919
- ? await startModules(ctx.catalog, selected)
920
- : primitive === "stop"
921
- ? await stopModules(ctx.catalog, selected)
922
- : primitive === "restart"
923
- ? await restartModules(ctx.catalog, selected)
924
- : await statusModules(ctx.catalog, selected);
925
- const statuses = result.flatMap((item) => item.result ? [item.result.status] : []);
926
- const status = aggregateStatus(statuses);
927
- if (primitive === "status" && !args.positional[0]) {
928
- await writeDeploymentObserverSummary({
929
- paths: ctx.paths,
930
- source: "status",
931
- selectedModuleCount: selected.length,
932
- totalModuleCount: modules.length,
933
- modules: result.flatMap((item) => item.result
934
- ? [{ moduleRef: item.moduleRef, status: item.result.status }]
935
- : []),
936
- });
937
- }
938
- return outcome(primitive, status, result);
939
273
  }
940
- async function handleVerify(ctx, args) {
941
- const modules = await discoverModules({ catalog: ctx.catalog });
942
- const selected = selectModules(modules, args.positional[0]);
943
- const result = await verifyModules(ctx.catalog, selected, ctx.paths);
944
- const status = aggregateStatus(result.map((item) => item.result.status));
945
- if (!args.positional[0]) {
946
- await writeDeploymentObserverSummary({
947
- paths: ctx.paths,
948
- source: "verify",
949
- selectedModuleCount: selected.length,
950
- totalModuleCount: modules.length,
951
- modules: result.map((item) => ({
952
- moduleRef: item.moduleRef,
953
- status: item.result.status,
954
- })),
955
- });
956
- }
957
- return outcome("verify", status, result);
958
- }
959
- async function handleDoctor(ctx, args) {
960
- const modules = await discoverModules({ catalog: ctx.catalog });
961
- const selected = selectModules(modules, args.positional[0]);
962
- const result = await doctorModules(ctx.catalog, selected);
963
- const status = aggregateStatus(result.map((item) => item.status));
964
- if (!args.positional[0]) {
965
- await writeDeploymentObserverSummary({
966
- paths: ctx.paths,
967
- source: "doctor",
968
- selectedModuleCount: selected.length,
969
- totalModuleCount: modules.length,
970
- modules: result.map((item) => ({
971
- moduleRef: item.moduleRef,
972
- status: item.status,
973
- })),
974
- });
975
- }
976
- 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
+ });
977
291
  }
978
- async function handleManifest(ctx, args) {
979
- const modules = await discoverModules({ catalog: ctx.catalog });
980
- const selected = selectModules(modules, args.positional[0]);
981
- const config = await loadEffectiveConfig(ctx.paths, selected, args.configFile);
982
- const manifest = await buildManifest({
983
- catalog: ctx.catalog,
984
- modules: selected,
985
- paths: ctx.paths,
986
- 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>]",
987
307
  });
988
- await generateInstallDoc({ modules: selected, config, paths: ctx.paths });
989
- switch (manifest.status) {
990
- case "READY":
991
- return outcome("manifest", "SUCCEEDED", manifest);
992
- case "ACTION_REQUIRED":
993
- return outcome("manifest", "ACTION_REQUIRED", manifest);
994
- case "DEGRADED":
995
- return outcome("manifest", "BLOCKED", manifest);
996
- case "NOT_READY":
997
- return outcome("manifest", "FAILED", manifest);
998
- }
999
308
  }
1000
- async function dispatchCommand(argv, runtime = {}) {
1001
- let args;
309
+ export async function runCli(argv, runtime = {}) {
310
+ let parsed;
1002
311
  try {
1003
- args = parseArgs(argv);
312
+ parsed = parseArgs(argv);
1004
313
  }
1005
314
  catch (error) {
1006
- return failure(argv[0] ?? "", error);
1007
- }
1008
- if (!COMMANDS.includes(args.command)) {
1009
- return {
1010
- command: args.command,
1011
- status: "FAILED",
1012
- error: {
1013
- code: "INVALID_REQUEST",
1014
- message: `unknown command: ${args.command}`,
1015
- },
1016
- };
315
+ return JSON.stringify(errorOutcome("unknown", error));
1017
316
  }
1018
317
  try {
1019
- const cwd = runtime.cwd ?? process.cwd();
1020
- if (args.command === "search") {
1021
- const current = await loadGlobalBinding(runtime.globalRoot);
1022
- const root = args.workspace ?? current?.workspaceRealPath ?? cwd;
1023
- return await handleSearch(root, args);
1024
- }
1025
- if (args.command === "preflight" && args.intent === "install") {
1026
- const root = await resolveRequestedInstallWorkspace(args, runtime);
1027
- const current = await loadGlobalBinding(runtime.globalRoot);
1028
- if (current !== undefined && current.workspaceRealPath !== root) {
1029
- throw new PlatformError("WORKSPACE_ALREADY_BOUND", `ProFlow is already bound to ${current.workspaceRealPath}; uninstall it before installing ${root}`);
1030
- }
1031
- return await handleInstallerPreflight(root);
1032
- }
1033
- if (args.command === "plan") {
1034
- if (args.intent === "install") {
1035
- return await handleInstallPlanBeforeBinding(args, runtime);
1036
- }
1037
- if (args.intent === undefined ||
1038
- !["configure", "upgrade", "uninstall", "repair"].includes(args.intent)) {
1039
- const current = await loadGlobalBinding(runtime.globalRoot);
1040
- const root = args.workspace ??
1041
- current?.workspaceRealPath ??
1042
- runtime.cwd ??
1043
- process.cwd();
1044
- return await handlePlan(await buildContext(root), args);
1045
- }
1046
- }
1047
- if (args.command === "apply") {
1048
- return await handleApplyWithGlobalBinding(args, runtime);
1049
- }
1050
- if (args.command === "install") {
1051
- return await handleInstallWithGlobalBinding(args, runtime);
1052
- }
1053
- if (args.command === "status") {
1054
- const observation = await observeBoundWorkspace(runtime.globalRoot);
1055
- if (observation === undefined) {
1056
- return outcome("status", "SUCCEEDED", {
1057
- installed: false,
1058
- bindingState: "UNBOUND",
1059
- boundWorkspace: null,
1060
- nextAction: "Run platform install [--workspace <path>]",
1061
- });
1062
- }
1063
- if (!observation.workspaceExists) {
1064
- return withWorkspace(outcome("status", "BLOCKED", {
1065
- installed: false,
1066
- code: "BOUND_WORKSPACE_MISSING",
1067
- boundWorkspace: observation.binding.workspaceRealPath,
1068
- nextAction: "Restore the Workspace or run platform uninstall --forget to clear only the stale binding",
1069
- }), observation.binding);
1070
- }
1071
- }
1072
- if (args.command === "uninstall" && args.forget) {
1073
- if (args.positional.length > 0 || args.workspace !== undefined) {
1074
- 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");
1075
- }
1076
- const operationLock = await acquireGlobalOperationLock(runtime.globalRoot);
1077
- try {
1078
- const forgotten = await forgetMissingWorkspaceBinding({
1079
- globalRoot: runtime.globalRoot,
1080
- });
1081
- return outcome("uninstall", "SUCCEEDED", {
1082
- forgottenWorkspace: forgotten.workspaceRealPath,
1083
- bindingCleared: true,
1084
- resourcesCleaned: false,
1085
- });
1086
- }
1087
- finally {
1088
- await operationLock.release();
1089
- }
1090
- }
1091
- if (args.command === "uninstall" && args.positional.length === 0) {
1092
- const current = await loadGlobalBinding(runtime.globalRoot);
1093
- if (current === undefined) {
1094
- return outcome("uninstall", "SUCCEEDED", {
1095
- alreadyUninstalled: true,
1096
- bindingCleared: true,
1097
- });
1098
- }
1099
- return await handlePlatformInstanceUninstall(args, runtime);
1100
- }
1101
- if (args.command === "upgrade" ||
1102
- (args.command === "uninstall" && args.positional.length > 0) ||
1103
- args.command === "start" ||
1104
- args.command === "stop" ||
1105
- args.command === "restart") {
1106
- return await handleBoundMutatingCommand(args, runtime);
1107
- }
1108
- const { ctx, binding } = await resolveBoundContext(runtime, args.workspace);
1109
- let result;
1110
- switch (args.command) {
1111
- case "search":
1112
- case "install":
1113
- 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) {
1114
330
  case "modules":
1115
- result = await handleModules(ctx, args);
1116
- break;
331
+ return JSON.stringify(await handleModules(root));
1117
332
  case "docs":
1118
- result = await handleDocs(ctx, args);
1119
- break;
333
+ return JSON.stringify(await handleDocs(root));
334
+ case "install":
335
+ return JSON.stringify(await handleInstall(root, runtime));
1120
336
  case "uninstall":
1121
- case "upgrade":
337
+ return JSON.stringify(await handleUninstall(root, runtime));
1122
338
  case "start":
339
+ return JSON.stringify(await handleStart(root));
1123
340
  case "stop":
1124
- case "restart":
1125
- throw new PlatformError("COMMAND_FAILED", "unreachable mutating command routing");
1126
- case "preflight":
1127
- result = await handlePreflight(ctx, args);
1128
- break;
1129
- case "plan":
1130
- result = await handlePlan(ctx, args);
1131
- break;
1132
- case "apply":
1133
- result = await handleApply(ctx, args);
1134
- break;
1135
- case "status":
1136
- result = await handleLifecycle(ctx, args, "status");
1137
- break;
1138
- case "verify":
1139
- result = await handleVerify(ctx, args);
1140
- break;
1141
- case "doctor":
1142
- result = await handleDoctor(ctx, args);
1143
- break;
1144
- case "manifest":
1145
- result = await handleManifest(ctx, args);
1146
- break;
341
+ return JSON.stringify(await handleStop(root));
1147
342
  }
1148
- if (result === undefined) {
1149
- throw new PlatformError("COMMAND_FAILED", "unreachable command routing");
1150
- }
1151
- return withWorkspace(result, binding);
1152
343
  }
1153
344
  catch (error) {
1154
- return failure(args.command, error);
345
+ return JSON.stringify(errorOutcome(parsed.command, error));
1155
346
  }
1156
347
  }
1157
- function toMachineResult(outcomeResult) {
1158
- const ok = outcomeResult.status === "SUCCEEDED";
1159
- return JSON.stringify({
1160
- contract: "deployment.result.v1",
1161
- ok,
1162
- status: outcomeResult.status,
1163
- moduleRef: MODULE_REF,
1164
- moduleVersion: MODULE_VERSION,
1165
- ...(outcomeResult.workspace !== undefined
1166
- ? { workspace: outcomeResult.workspace }
1167
- : {}),
1168
- ...(outcomeResult.data !== undefined ? { data: outcomeResult.data } : {}),
1169
- ...(outcomeResult.status === "ACTION_REQUIRED"
1170
- ? {
1171
- actionRequired: {
1172
- action: "resolve-blocking-action",
1173
- description: "one or more steps require a human action",
1174
- },
1175
- }
1176
- : {}),
1177
- ...(outcomeResult.error !== undefined
1178
- ? {
1179
- error: {
1180
- code: outcomeResult.error.code,
1181
- message: outcomeResult.error.message,
1182
- retryable: false,
1183
- },
1184
- }
1185
- : {}),
1186
- });
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
+ };
1187
364
  }
1188
365
  function isRecord(value) {
1189
366
  return typeof value === "object" && value !== null && !Array.isArray(value);
1190
367
  }
1191
- function humanArrayLines(data) {
1192
- const isVerification = data.some((item) => isRecord(item) && isRecord(item.record));
1193
- const isDoctor = data.some((item) => isRecord(item) && Array.isArray(item.errors) && isRecord(item.nextAction));
1194
- const lines = [
1195
- isVerification ? "Verification:" : isDoctor ? "Doctor:" : "Lifecycle:",
1196
- ];
1197
- let skippedUnsupported = 0;
1198
- for (const item of data) {
1199
- if (!isRecord(item))
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))
1200
374
  continue;
1201
- const moduleRef = typeof item.moduleRef === "string" ? item.moduleRef : "unknown";
1202
- const runStatus = typeof item.status === "string" ? item.status : "UNKNOWN";
1203
- if (runStatus === "SKIP_UNSUPPORTED") {
1204
- skippedUnsupported += 1;
1205
- continue;
1206
- }
1207
- const result = isRecord(item.result) ? item.result : undefined;
1208
- const resultStatus = result !== undefined && typeof result.status === "string"
1209
- ? result.status
1210
- : runStatus;
1211
- let detail = "";
1212
- const actionRequired = result !== undefined && isRecord(result.actionRequired)
1213
- ? result.actionRequired
1214
- : undefined;
1215
- const error = result !== undefined && isRecord(result.error) ? result.error : undefined;
1216
- if (actionRequired !== undefined) {
1217
- const action = typeof actionRequired.action === "string"
1218
- ? actionRequired.action
1219
- : "action-required";
1220
- const description = typeof actionRequired.description === "string"
1221
- ? actionRequired.description
1222
- : "human action required";
1223
- detail = ` — ${action}: ${description}`;
1224
- }
1225
- else if (error !== undefined) {
1226
- const code = typeof error.code === "string" ? error.code : "COMMAND_FAILED";
1227
- const message = typeof error.message === "string" ? error.message : "Unknown error";
1228
- detail = ` — ${code}: ${message}`;
1229
- }
1230
- else if (isRecord(item.record) &&
1231
- typeof item.record.summary === "string") {
1232
- detail = ` — ${item.record.summary}`;
1233
- }
1234
- lines.push(`- ${moduleRef}: ${resultStatus}${detail}`);
1235
- if (Array.isArray(item.errors)) {
1236
- for (const doctorError of item.errors) {
1237
- if (!isRecord(doctorError))
1238
- continue;
1239
- const code = typeof doctorError.code === "string"
1240
- ? doctorError.code
1241
- : "COMMAND_FAILED";
1242
- const message = typeof doctorError.message === "string"
1243
- ? doctorError.message
1244
- : "Unknown error";
1245
- lines.push(` Error: ${code}: ${message}`);
1246
- }
1247
- }
1248
- if (isRecord(item.nextAction)) {
1249
- if (item.nextAction.kind === "human-action" &&
1250
- typeof item.nextAction.action === "string" &&
1251
- typeof item.nextAction.description === "string") {
1252
- lines.push(` Next: ${item.nextAction.action} — ${item.nextAction.description}`);
1253
- }
1254
- else if (item.nextAction.kind === "repair-plan" &&
1255
- typeof item.nextAction.summary === "string") {
1256
- lines.push(` Next: platform plan --intent repair — ${item.nextAction.summary}`);
1257
- }
1258
- }
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}`);
1259
381
  }
1260
- if (skippedUnsupported > 0)
1261
- lines.push(`Skipped unsupported: ${skippedUnsupported}`);
1262
- return lines;
382
+ return lines.join("\n");
1263
383
  }
1264
- function humanDetailLines(data) {
1265
- if (Array.isArray(data))
1266
- return humanArrayLines(data);
1267
- if (!isRecord(data)) {
1268
- return data === undefined ? [] : [String(data)];
1269
- }
1270
- const lines = [];
1271
- for (const key of [
1272
- "installed",
1273
- "bindingState",
1274
- "boundWorkspace",
1275
- "requestedWorkspace",
1276
- "alreadyInstalled",
1277
- "alreadyLatest",
1278
- "changed",
1279
- "bindingCleared",
1280
- "resourcesCleaned",
1281
- "planRef",
1282
- "outcome",
1283
- ]) {
1284
- const value = data[key];
1285
- if (typeof value === "string" ||
1286
- typeof value === "number" ||
1287
- typeof value === "boolean") {
1288
- lines.push(`${key}: ${String(value)}`);
1289
- }
1290
- }
1291
- if (typeof data.nextAction === "string" && data.nextAction !== "") {
1292
- lines.push(`Next: ${data.nextAction}`);
1293
- }
1294
- const findings = data.findings;
1295
- let hasMissingConfig = false;
1296
- if (Array.isArray(findings) && findings.length > 0) {
1297
- lines.push("Findings:");
1298
- for (const finding of findings) {
1299
- if (!isRecord(finding))
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))
1300
396
  continue;
1301
- const severity = typeof finding.severity === "string"
1302
- ? finding.severity.toUpperCase()
1303
- : "INFO";
1304
- const code = typeof finding.code === "string" ? finding.code : "CHECK";
1305
- if (code === "CONFIG_MISSING")
1306
- hasMissingConfig = true;
1307
- const moduleRef = typeof finding.moduleRef === "string" ? ` ${finding.moduleRef}` : "";
1308
- const message = typeof finding.message === "string" ? finding.message : "";
1309
- lines.push(`- [${severity}] ${code}${moduleRef}${message === "" ? "" : `: ${message}`}`);
397
+ lines.push("", `### ${String(document.id)}`, String(document.content ?? ""));
1310
398
  }
1311
399
  }
1312
- if (hasMissingConfig) {
1313
- 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>.");
1314
- }
1315
- const preflight = data.preflight;
1316
- if (isRecord(preflight)) {
1317
- const nested = humanDetailLines(preflight);
1318
- if (nested.length > 0) {
1319
- lines.push("Preflight:", ...nested.map((line) => ` ${line}`));
1320
- }
1321
- }
1322
- return lines;
400
+ return lines.join("\n");
1323
401
  }
1324
- export function renderHumanResult(machineOutput) {
1325
- const parsed = JSON.parse(machineOutput);
1326
- if (!isRecord(parsed))
1327
- return machineOutput;
1328
- const data = parsed.data;
1329
- if (isRecord(data) && typeof data.version === "string") {
1330
- 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}` : ""}`;
1331
405
  }
1332
- if (isRecord(data) && typeof data.usage === "string") {
1333
- const commands = Array.isArray(data.commands)
1334
- ? data.commands.filter((item) => typeof item === "string")
1335
- : [];
1336
- const examples = Array.isArray(data.examples)
1337
- ? data.examples.filter((item) => typeof item === "string")
1338
- : [];
406
+ if (result.command === "help") {
1339
407
  return [
1340
408
  "ProFlow Platform CLI",
1341
409
  "",
1342
- `Usage: ${data.usage}`,
1343
- ...(commands.length === 0
1344
- ? []
1345
- : ["", `Commands: ${commands.join(", ")}`]),
1346
- ...(examples.length === 0
1347
- ? []
1348
- : ["", "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",
1349
414
  ].join("\n");
1350
415
  }
1351
- const status = typeof parsed.status === "string" ? parsed.status : "UNKNOWN";
1352
- const lines = [`ProFlow: ${status}`];
1353
- const workspace = parsed.workspace;
1354
- if (isRecord(workspace) && typeof workspace.boundWorkspace === "string") {
1355
- lines.push(`Workspace: ${workspace.boundWorkspace}`);
1356
- }
1357
- const error = parsed.error;
1358
- if (isRecord(error)) {
1359
- const code = typeof error.code === "string" ? error.code : "COMMAND_FAILED";
1360
- const message = typeof error.message === "string" ? error.message : "Unknown error";
1361
- lines.push(`Error: ${code}: ${message}`);
1362
- }
1363
- lines.push(...humanDetailLines(data));
1364
- if (lines.length === 1 && status === "SUCCEEDED") {
1365
- lines.push(`Version: ${MODULE_VERSION}`, "Next: run platform --help");
1366
- }
1367
- return lines.join("\n");
1368
- }
1369
- export async function runCli(argv, runtime = {}) {
1370
- const filtered = argv.filter((argument) => argument !== "--json");
1371
- if (filtered.length === 1 &&
1372
- (filtered[0] === "--version" || filtered[0] === "-v")) {
1373
- return JSON.stringify({
1374
- contract: "deployment.result.v1",
1375
- ok: true,
1376
- status: "SUCCEEDED",
1377
- moduleRef: MODULE_REF,
1378
- moduleVersion: MODULE_VERSION,
1379
- data: { version: MODULE_VERSION },
1380
- });
1381
- }
1382
- if (argv.includes("--help") || argv.includes("-h")) {
1383
- return JSON.stringify({
1384
- contract: "deployment.result.v1",
1385
- ok: true,
1386
- status: "SUCCEEDED",
1387
- moduleRef: MODULE_REF,
1388
- moduleVersion: MODULE_VERSION,
1389
- data: {
1390
- usage: "platform <command> [module|package] [--workspace <path>] [--json]",
1391
- commands: [...COMMANDS],
1392
- examples: [
1393
- "platform preflight --intent install --workspace <path>",
1394
- "platform install [module|package] [--workspace <path>]",
1395
- "platform preflight # startup/runtime readiness for the bound Workspace",
1396
- "platform plan --intent configure --config <file>",
1397
- "platform apply <planRef>",
1398
- "platform start | status | verify | doctor | stop | restart",
1399
- "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",
1400
- "append --json for the stable machine-readable contract",
1401
- ],
1402
- },
1403
- });
1404
- }
1405
- if (filtered.length === 0) {
1406
- return JSON.stringify({
1407
- contract: "deployment.result.v1",
1408
- ok: true,
1409
- status: "SUCCEEDED",
1410
- moduleRef: MODULE_REF,
1411
- moduleVersion: MODULE_VERSION,
1412
- });
1413
- }
1414
- 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");
1415
430
  }
1416
431
  if (import.meta.main) {
1417
432
  const argv = process.argv.slice(2);
1418
433
  const output = await runCli(argv);
1419
- const rendered = argv.includes("--json") ? output : renderHumanResult(output);
1420
- process.stdout.write(`${rendered}\n`);
1421
- const parsed = JSON.parse(output);
1422
- if (typeof parsed === "object" &&
1423
- parsed !== null &&
1424
- 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")
1425
437
  process.exitCode = 1;
1426
- }
1427
438
  }