@tomflow/proflow-platform-cli 0.1.0

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 (144) hide show
  1. package/README.md +37 -0
  2. package/conformance.json +1 -0
  3. package/dist/deployment/adapter.d.ts +57 -0
  4. package/dist/deployment/adapter.js +25 -0
  5. package/dist/deployment/descriptor.d.ts +39 -0
  6. package/dist/deployment/descriptor.js +41 -0
  7. package/dist/src/apply/apply.d.ts +24 -0
  8. package/dist/src/apply/apply.js +271 -0
  9. package/dist/src/apply/cleanup.d.ts +12 -0
  10. package/dist/src/apply/cleanup.js +37 -0
  11. package/dist/src/apply/current.d.ts +4 -0
  12. package/dist/src/apply/current.js +73 -0
  13. package/dist/src/apply/driver.d.ts +23 -0
  14. package/dist/src/apply/driver.js +154 -0
  15. package/dist/src/apply/execute.d.ts +36 -0
  16. package/dist/src/apply/execute.js +124 -0
  17. package/dist/src/apply/index.d.ts +8 -0
  18. package/dist/src/apply/index.js +4 -0
  19. package/dist/src/apply/reality.d.ts +20 -0
  20. package/dist/src/apply/reality.js +93 -0
  21. package/dist/src/binding/production-bindings.d.ts +45 -0
  22. package/dist/src/binding/production-bindings.js +102 -0
  23. package/dist/src/cli.d.ts +12 -0
  24. package/dist/src/cli.js +712 -0
  25. package/dist/src/contracts.d.ts +113 -0
  26. package/dist/src/contracts.js +1 -0
  27. package/dist/src/discovery/catalog.d.ts +10 -0
  28. package/dist/src/discovery/catalog.js +76 -0
  29. package/dist/src/discovery/discover.d.ts +19 -0
  30. package/dist/src/discovery/discover.js +124 -0
  31. package/dist/src/discovery/index.d.ts +5 -0
  32. package/dist/src/discovery/index.js +4 -0
  33. package/dist/src/discovery/installed.d.ts +21 -0
  34. package/dist/src/discovery/installed.js +130 -0
  35. package/dist/src/discovery/workspace.d.ts +10 -0
  36. package/dist/src/discovery/workspace.js +155 -0
  37. package/dist/src/docs/docs.d.ts +47 -0
  38. package/dist/src/docs/docs.js +129 -0
  39. package/dist/src/docs/index.d.ts +1 -0
  40. package/dist/src/docs/index.js +1 -0
  41. package/dist/src/doctor/doctor.d.ts +41 -0
  42. package/dist/src/doctor/doctor.js +80 -0
  43. package/dist/src/doctor/index.d.ts +2 -0
  44. package/dist/src/doctor/index.js +1 -0
  45. package/dist/src/errors.d.ts +6 -0
  46. package/dist/src/errors.js +35 -0
  47. package/dist/src/graph/graph.d.ts +23 -0
  48. package/dist/src/graph/graph.js +132 -0
  49. package/dist/src/graph/index.d.ts +2 -0
  50. package/dist/src/graph/index.js +1 -0
  51. package/dist/src/index.d.ts +28 -0
  52. package/dist/src/index.js +18 -0
  53. package/dist/src/install/bootstrap.d.ts +10 -0
  54. package/dist/src/install/bootstrap.js +79 -0
  55. package/dist/src/install/environment.d.ts +29 -0
  56. package/dist/src/install/environment.js +226 -0
  57. package/dist/src/install/index.d.ts +6 -0
  58. package/dist/src/install/index.js +3 -0
  59. package/dist/src/install/install.d.ts +19 -0
  60. package/dist/src/install/install.js +131 -0
  61. package/dist/src/install/package-manager.d.ts +11 -0
  62. package/dist/src/install/package-manager.js +58 -0
  63. package/dist/src/lifecycle/dispatch.d.ts +47 -0
  64. package/dist/src/lifecycle/dispatch.js +142 -0
  65. package/dist/src/lifecycle/index.d.ts +3 -0
  66. package/dist/src/lifecycle/index.js +2 -0
  67. package/dist/src/lifecycle/service-process.d.ts +11 -0
  68. package/dist/src/lifecycle/service-process.js +326 -0
  69. package/dist/src/manifest/index.d.ts +2 -0
  70. package/dist/src/manifest/index.js +1 -0
  71. package/dist/src/manifest/manifest.d.ts +61 -0
  72. package/dist/src/manifest/manifest.js +160 -0
  73. package/dist/src/modules.d.ts +11 -0
  74. package/dist/src/modules.js +55 -0
  75. package/dist/src/observer/deployment-summary.d.ts +31 -0
  76. package/dist/src/observer/deployment-summary.js +34 -0
  77. package/dist/src/paths.d.ts +21 -0
  78. package/dist/src/paths.js +56 -0
  79. package/dist/src/persistence/atomic.d.ts +2 -0
  80. package/dist/src/persistence/atomic.js +16 -0
  81. package/dist/src/persistence/config.d.ts +12 -0
  82. package/dist/src/persistence/config.js +43 -0
  83. package/dist/src/persistence/generated.d.ts +2 -0
  84. package/dist/src/persistence/generated.js +4 -0
  85. package/dist/src/persistence/guards.d.ts +7 -0
  86. package/dist/src/persistence/guards.js +120 -0
  87. package/dist/src/persistence/index.d.ts +6 -0
  88. package/dist/src/persistence/index.js +5 -0
  89. package/dist/src/persistence/plans.d.ts +5 -0
  90. package/dist/src/persistence/plans.js +34 -0
  91. package/dist/src/persistence/state.d.ts +7 -0
  92. package/dist/src/persistence/state.js +34 -0
  93. package/dist/src/persistence/verification.d.ts +5 -0
  94. package/dist/src/persistence/verification.js +47 -0
  95. package/dist/src/planner/assemble.d.ts +16 -0
  96. package/dist/src/planner/assemble.js +107 -0
  97. package/dist/src/planner/check.d.ts +20 -0
  98. package/dist/src/planner/check.js +96 -0
  99. package/dist/src/planner/fingerprint.d.ts +12 -0
  100. package/dist/src/planner/fingerprint.js +164 -0
  101. package/dist/src/planner/index.d.ts +13 -0
  102. package/dist/src/planner/index.js +7 -0
  103. package/dist/src/planner/plan.d.ts +14 -0
  104. package/dist/src/planner/plan.js +156 -0
  105. package/dist/src/planner/repair.d.ts +28 -0
  106. package/dist/src/planner/repair.js +131 -0
  107. package/dist/src/planner/stale.d.ts +7 -0
  108. package/dist/src/planner/stale.js +23 -0
  109. package/dist/src/planner/steps.d.ts +37 -0
  110. package/dist/src/planner/steps.js +165 -0
  111. package/dist/src/planner/target-catalog.d.ts +13 -0
  112. package/dist/src/planner/target-catalog.js +48 -0
  113. package/dist/src/planner/upgrade.d.ts +18 -0
  114. package/dist/src/planner/upgrade.js +105 -0
  115. package/dist/src/preflight/config.d.ts +10 -0
  116. package/dist/src/preflight/config.js +45 -0
  117. package/dist/src/preflight/findings.d.ts +26 -0
  118. package/dist/src/preflight/findings.js +1 -0
  119. package/dist/src/preflight/index.d.ts +7 -0
  120. package/dist/src/preflight/index.js +3 -0
  121. package/dist/src/preflight/preflight.d.ts +8 -0
  122. package/dist/src/preflight/preflight.js +174 -0
  123. package/dist/src/preflight/requirements.d.ts +11 -0
  124. package/dist/src/preflight/requirements.js +224 -0
  125. package/dist/src/ready/index.d.ts +2 -0
  126. package/dist/src/ready/index.js +1 -0
  127. package/dist/src/ready/ready.d.ts +40 -0
  128. package/dist/src/ready/ready.js +201 -0
  129. package/dist/src/registry/index.d.ts +2 -0
  130. package/dist/src/registry/index.js +1 -0
  131. package/dist/src/registry/npm-registry.d.ts +51 -0
  132. package/dist/src/registry/npm-registry.js +341 -0
  133. package/dist/src/security/index.d.ts +3 -0
  134. package/dist/src/security/index.js +2 -0
  135. package/dist/src/security/lock.d.ts +9 -0
  136. package/dist/src/security/lock.js +89 -0
  137. package/dist/src/security/redact.d.ts +6 -0
  138. package/dist/src/security/redact.js +48 -0
  139. package/dist/src/verification/index.d.ts +2 -0
  140. package/dist/src/verification/index.js +1 -0
  141. package/dist/src/verification/verify.d.ts +33 -0
  142. package/dist/src/verification/verify.js +114 -0
  143. package/package.json +49 -0
  144. package/proflow.module.json +45 -0
@@ -0,0 +1,29 @@
1
+ import { type NpmCommandRunner } from "../registry/index.ts";
2
+ import { type SupportedWorkspacePackageManager } from "./package-manager.ts";
3
+ export declare const PLATFORM_INSTALL_NODE_RANGE = ">=24.19.0";
4
+ export type InstallerPreflightStatus = "READY" | "ACTION_REQUIRED" | "BLOCKED";
5
+ export type InstallerFindingSeverity = "info" | "action" | "error";
6
+ export interface InstallerFinding {
7
+ code: string;
8
+ severity: InstallerFindingSeverity;
9
+ message: string;
10
+ }
11
+ export interface InstallerEnvironmentPreflightResult {
12
+ contract: "deployment.installer-preflight.v1";
13
+ ok: boolean;
14
+ status: InstallerPreflightStatus;
15
+ workspaceRoot: string;
16
+ nodeVersion: string;
17
+ npmVersion?: string;
18
+ packageManager?: {
19
+ name: SupportedWorkspacePackageManager;
20
+ declared?: string;
21
+ version?: string;
22
+ };
23
+ registry?: string;
24
+ findings: InstallerFinding[];
25
+ }
26
+ export declare function preflightInstallerEnvironment(options: {
27
+ workspaceRoot: string;
28
+ runner?: NpmCommandRunner;
29
+ }): Promise<InstallerEnvironmentPreflightResult>;
@@ -0,0 +1,226 @@
1
+ import { constants } from "node:fs";
2
+ import { access, readFile, stat } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { PlatformError } from "../errors.js";
5
+ import { versionSatisfies } from "../modules.js";
6
+ import { resolveScopeRegistry, systemNpmRunner, } from "../registry/index.js";
7
+ import { findExecutable, readWorkspacePackageManagerSelection, systemPackageCommandRunner, } from "./package-manager.js";
8
+ export const PLATFORM_INSTALL_NODE_RANGE = ">=24.19.0";
9
+ export async function preflightInstallerEnvironment(options) {
10
+ const runner = options.runner ?? systemNpmRunner();
11
+ const findings = [];
12
+ const nodeVersion = process.versions.node;
13
+ if (!versionSatisfies(nodeVersion, PLATFORM_INSTALL_NODE_RANGE)) {
14
+ findings.push({
15
+ code: "NODE_VERSION_UNSUPPORTED",
16
+ severity: "error",
17
+ message: `node ${nodeVersion} does not satisfy ${PLATFORM_INSTALL_NODE_RANGE}`,
18
+ });
19
+ }
20
+ else {
21
+ findings.push({
22
+ code: "NODE_VERSION_READY",
23
+ severity: "info",
24
+ message: `node ${nodeVersion} satisfies ${PLATFORM_INSTALL_NODE_RANGE}`,
25
+ });
26
+ }
27
+ const workspace = await inspectWorkspace(options.workspaceRoot);
28
+ findings.push(...workspace.findings);
29
+ let npmVersion;
30
+ if (!findExecutable("npm")) {
31
+ findings.push({
32
+ code: "NPM_UNAVAILABLE",
33
+ severity: "error",
34
+ message: "npm executable is not available on PATH",
35
+ });
36
+ }
37
+ else {
38
+ try {
39
+ const npm = await runner.run(["--version"], options.workspaceRoot);
40
+ npmVersion = npm.stdout.trim();
41
+ findings.push({
42
+ code: "NPM_READY",
43
+ severity: "info",
44
+ message: `npm ${npmVersion || "<unknown>"} is available`,
45
+ });
46
+ }
47
+ catch (error) {
48
+ findings.push(platformErrorFinding(error, "NPM_UNAVAILABLE"));
49
+ }
50
+ }
51
+ const packageManager = await readWorkspacePackageManagerSelection(options.workspaceRoot);
52
+ let packageManagerVersion;
53
+ if (packageManager === undefined) {
54
+ findings.push({
55
+ code: "PACKAGE_MANAGER_UNSUPPORTED",
56
+ severity: "action",
57
+ message: `unsupported packageManager declaration: ${workspace.packageManager ?? "<unknown>"}`,
58
+ });
59
+ }
60
+ else if (packageManager.name === "pnpm") {
61
+ if (!findExecutable("pnpm")) {
62
+ findings.push({
63
+ code: "PNPM_REQUIRED",
64
+ severity: "action",
65
+ message: `workspace declares ${packageManager.declared}; install/enable pnpm before applying package changes`,
66
+ });
67
+ }
68
+ else {
69
+ try {
70
+ const result = (await systemPackageCommandRunner().run("pnpm", ["--version"], options.workspaceRoot)).trim();
71
+ packageManagerVersion = result;
72
+ findings.push({
73
+ code: "PNPM_READY",
74
+ severity: "info",
75
+ message: `pnpm ${result} is available for this workspace`,
76
+ });
77
+ }
78
+ catch (error) {
79
+ findings.push({
80
+ code: "PNPM_REQUIRED",
81
+ severity: "action",
82
+ message: error instanceof Error ? error.message : String(error),
83
+ });
84
+ }
85
+ }
86
+ }
87
+ else if (npmVersion !== undefined) {
88
+ packageManagerVersion = npmVersion;
89
+ findings.push({
90
+ code: "PACKAGE_MANAGER_READY",
91
+ severity: "info",
92
+ message: packageManager.declared === undefined
93
+ ? "workspace does not declare packageManager; npm will be used for bootstrap package operations"
94
+ : `workspace declares ${packageManager.declared}`,
95
+ });
96
+ }
97
+ let registry;
98
+ if (npmVersion !== undefined) {
99
+ try {
100
+ registry = await resolveScopeRegistry(options.workspaceRoot, runner);
101
+ await runner.run(["ping", "--json", "--prefer-online", `--registry=${registry}`], options.workspaceRoot);
102
+ findings.push({
103
+ code: "REGISTRY_READY",
104
+ severity: "info",
105
+ message: `npm registry is reachable: ${registry}`,
106
+ });
107
+ }
108
+ catch (error) {
109
+ findings.push(platformErrorFinding(error, "REGISTRY_UNAVAILABLE"));
110
+ }
111
+ }
112
+ const status = aggregateInstallerStatus(findings);
113
+ return {
114
+ contract: "deployment.installer-preflight.v1",
115
+ ok: status === "READY",
116
+ status,
117
+ workspaceRoot: options.workspaceRoot,
118
+ nodeVersion,
119
+ ...(npmVersion === undefined ? {} : { npmVersion }),
120
+ ...(packageManager === undefined
121
+ ? {}
122
+ : {
123
+ packageManager: {
124
+ ...packageManager,
125
+ ...(packageManagerVersion === undefined
126
+ ? {}
127
+ : { version: packageManagerVersion }),
128
+ },
129
+ }),
130
+ ...(registry === undefined ? {} : { registry }),
131
+ findings,
132
+ };
133
+ }
134
+ async function inspectWorkspace(workspaceRoot) {
135
+ const findings = [];
136
+ try {
137
+ const info = await stat(workspaceRoot);
138
+ if (!info.isDirectory()) {
139
+ return {
140
+ findings: [
141
+ {
142
+ code: "WORKSPACE_INVALID",
143
+ severity: "error",
144
+ message: `${workspaceRoot} is not a directory`,
145
+ },
146
+ ],
147
+ };
148
+ }
149
+ await access(workspaceRoot, constants.R_OK | constants.W_OK);
150
+ findings.push({
151
+ code: "WORKSPACE_WRITABLE",
152
+ severity: "info",
153
+ message: `workspace is readable and writable: ${workspaceRoot}`,
154
+ });
155
+ }
156
+ catch {
157
+ return {
158
+ findings: [
159
+ {
160
+ code: "WORKSPACE_NOT_WRITABLE",
161
+ severity: "error",
162
+ message: `workspace is missing or not writable: ${workspaceRoot}`,
163
+ },
164
+ ],
165
+ };
166
+ }
167
+ const manifestPath = join(workspaceRoot, "package.json");
168
+ try {
169
+ const raw = await readFile(manifestPath, "utf8");
170
+ const parsed = JSON.parse(raw);
171
+ if (!isRecord(parsed))
172
+ throw new Error("package.json root is not an object");
173
+ const manifest = parsed;
174
+ findings.push({
175
+ code: "PACKAGE_JSON_READY",
176
+ severity: "info",
177
+ message: "workspace package.json is readable",
178
+ });
179
+ return {
180
+ ...(typeof manifest.packageManager === "string"
181
+ ? { packageManager: manifest.packageManager }
182
+ : {}),
183
+ findings,
184
+ };
185
+ }
186
+ catch (error) {
187
+ if (isMissingFile(error)) {
188
+ findings.push({
189
+ code: "PACKAGE_JSON_CREATABLE",
190
+ severity: "info",
191
+ message: "workspace has no package.json; installer may create one before the first package mutation",
192
+ });
193
+ return { findings };
194
+ }
195
+ findings.push({
196
+ code: "PACKAGE_JSON_INVALID",
197
+ severity: "error",
198
+ message: error instanceof Error ? error.message : String(error),
199
+ });
200
+ return { findings };
201
+ }
202
+ }
203
+ function aggregateInstallerStatus(findings) {
204
+ if (findings.some((finding) => finding.severity === "error"))
205
+ return "BLOCKED";
206
+ if (findings.some((finding) => finding.severity === "action")) {
207
+ return "ACTION_REQUIRED";
208
+ }
209
+ return "READY";
210
+ }
211
+ function platformErrorFinding(error, fallbackCode) {
212
+ if (error instanceof PlatformError) {
213
+ return { code: error.code, severity: "error", message: error.message };
214
+ }
215
+ return {
216
+ code: fallbackCode,
217
+ severity: "error",
218
+ message: error instanceof Error ? error.message : String(error),
219
+ };
220
+ }
221
+ function isRecord(value) {
222
+ return typeof value === "object" && value !== null && !Array.isArray(value);
223
+ }
224
+ function isMissingFile(error) {
225
+ return isRecord(error) && error.code === "ENOENT";
226
+ }
@@ -0,0 +1,6 @@
1
+ export { registryCandidateToBootstrapModule, selectBootstrapModules, } from "./bootstrap.ts";
2
+ export type { InstallerEnvironmentPreflightResult, InstallerFinding, InstallerFindingSeverity, InstallerPreflightStatus, } from "./environment.ts";
3
+ export { PLATFORM_INSTALL_NODE_RANGE, preflightInstallerEnvironment, } from "./environment.ts";
4
+ export type { GenerateInstallDocInput, InstallDocInput, } from "./install.ts";
5
+ export { generateInstallDoc, renderInstallDoc } from "./install.ts";
6
+ export type { SupportedWorkspacePackageManager, WorkspacePackageManagerSelection, } from "./package-manager.ts";
@@ -0,0 +1,3 @@
1
+ export { registryCandidateToBootstrapModule, selectBootstrapModules, } from "./bootstrap.js";
2
+ export { PLATFORM_INSTALL_NODE_RANGE, preflightInstallerEnvironment, } from "./environment.js";
3
+ export { generateInstallDoc, renderInstallDoc } from "./install.js";
@@ -0,0 +1,19 @@
1
+ import type { ResolvedModule } from "../contracts.ts";
2
+ import type { WorkspacePaths } from "../paths.ts";
3
+ export interface InstallDocInput {
4
+ modules: readonly ResolvedModule[];
5
+ config?: Record<string, Record<string, string>>;
6
+ now?: Date;
7
+ }
8
+ export interface GenerateInstallDocInput extends InstallDocInput {
9
+ paths: WorkspacePaths;
10
+ }
11
+ /**
12
+ * Renders the generated INSTALL document from the real selected module set:
13
+ * module refs/versions, machine/runtime requirements, secret-redacted config
14
+ * slots, external resources, effects, human actions, and the verification plan.
15
+ * No raw secret value may survive — config values are redacted by key, and the
16
+ * whole document is deep-redacted against every known raw secret value.
17
+ */
18
+ export declare function renderInstallDoc(input: InstallDocInput): string;
19
+ export declare function generateInstallDoc(input: GenerateInstallDocInput): Promise<string>;
@@ -0,0 +1,131 @@
1
+ import { writeInstallDoc } from "../persistence/index.js";
2
+ import { resolveModuleConfig } from "../preflight/index.js";
3
+ import { redactDeep, redactSecretValues } from "../security/index.js";
4
+ function compareRef(a, b) {
5
+ return a < b ? -1 : a > b ? 1 : 0;
6
+ }
7
+ function requirementLine(moduleRef, requirement) {
8
+ switch (requirement.kind) {
9
+ case "runtime":
10
+ return `- \`${moduleRef}\`: runtime \`${requirement.runtime}\` ${requirement.versionRange}`;
11
+ case "executable":
12
+ return `- \`${moduleRef}\`: executable \`${requirement.command}\`${requirement.versionRange !== undefined
13
+ ? ` ${requirement.versionRange}`
14
+ : ""}`;
15
+ case "filesystem":
16
+ return `- \`${moduleRef}\`: filesystem \`${requirement.path}\` (${requirement.access})`;
17
+ case "port":
18
+ return `- \`${moduleRef}\`: port ${requirement.port}/${requirement.protocol}`;
19
+ case "network":
20
+ return `- \`${moduleRef}\`: network \`${requirement.url}\``;
21
+ case "module-contract":
22
+ return `- \`${moduleRef}\`: module-contract \`${requirement.contractRef}\` ${requirement.versionRange}`;
23
+ case "human":
24
+ return `- \`${moduleRef}\`: human — ${requirement.action}`;
25
+ }
26
+ }
27
+ function collectRawSecrets(modules, config) {
28
+ const secrets = [];
29
+ for (const module of modules) {
30
+ const resolved = resolveModuleConfig(module, config?.[module.moduleRef]);
31
+ const sensitiveKeys = new Set(module.configSlots
32
+ .filter((slot) => slot.sensitive === true && slot.type !== "secretRef")
33
+ .map((slot) => slot.key));
34
+ for (const [key, value] of Object.entries(resolved.values)) {
35
+ if (sensitiveKeys.has(key) && value.length > 0)
36
+ secrets.push(value);
37
+ }
38
+ }
39
+ return secrets;
40
+ }
41
+ /**
42
+ * Renders the generated INSTALL document from the real selected module set:
43
+ * module refs/versions, machine/runtime requirements, secret-redacted config
44
+ * slots, external resources, effects, human actions, and the verification plan.
45
+ * No raw secret value may survive — config values are redacted by key, and the
46
+ * whole document is deep-redacted against every known raw secret value.
47
+ */
48
+ export function renderInstallDoc(input) {
49
+ const generatedAt = (input.now ?? new Date()).toISOString();
50
+ const modules = [...input.modules].sort((a, b) => compareRef(a.moduleRef, b.moduleRef));
51
+ const lines = [];
52
+ lines.push("# ProFlow Deployment — Generated INSTALL");
53
+ lines.push("");
54
+ lines.push(`> Generated at ${generatedAt}. Regenerated by \`platform manifest\`.`);
55
+ lines.push("");
56
+ lines.push("## Selected Module Set");
57
+ lines.push("");
58
+ for (const module of modules) {
59
+ lines.push(`- \`${module.moduleRef}\` (\`${module.packageName}@${module.moduleVersion}\`, kind: \`${module.kind}\`)`);
60
+ }
61
+ lines.push("");
62
+ lines.push("## Requirements");
63
+ lines.push("");
64
+ for (const module of modules) {
65
+ for (const requirement of module.requirements) {
66
+ lines.push(requirementLine(module.moduleRef, requirement));
67
+ }
68
+ }
69
+ lines.push("");
70
+ lines.push("## Config Slots");
71
+ lines.push("");
72
+ for (const module of modules) {
73
+ const resolved = resolveModuleConfig(module, input.config?.[module.moduleRef]);
74
+ const values = redactSecretValues(resolved.values, resolved.secretRefs);
75
+ for (const slot of module.configSlots) {
76
+ const value = values[slot.key];
77
+ const suffix = value !== undefined ? ` = \`${value}\`` : "";
78
+ const required = slot.required ? " (required)" : "";
79
+ lines.push(`- \`${module.moduleRef}\`.\`${slot.key}\` [${slot.type}]${required}${suffix}`);
80
+ }
81
+ }
82
+ lines.push("");
83
+ const external = modules.filter((module) => module.kind === "external-resource");
84
+ if (external.length > 0) {
85
+ lines.push("## External Resources");
86
+ lines.push("");
87
+ for (const module of external) {
88
+ lines.push(`- \`${module.moduleRef}\` (\`${module.packageName}@${module.moduleVersion}\`)`);
89
+ }
90
+ lines.push("");
91
+ }
92
+ lines.push("## Effects");
93
+ lines.push("");
94
+ for (const module of modules) {
95
+ for (const effect of module.effects) {
96
+ lines.push(`- \`${module.moduleRef}\`: [${effect.kind}] ${effect.description}`);
97
+ }
98
+ }
99
+ lines.push("");
100
+ const humanActions = modules.flatMap((module) => module.requirements
101
+ .filter((requirement) => requirement.kind === "human")
102
+ .map((requirement) => ({
103
+ moduleRef: module.moduleRef,
104
+ action: requirement.action,
105
+ })));
106
+ if (humanActions.length > 0) {
107
+ lines.push("## Human Actions");
108
+ lines.push("");
109
+ for (const action of humanActions) {
110
+ lines.push(`- \`${action.moduleRef}\`: ${action.action}`);
111
+ }
112
+ lines.push("");
113
+ }
114
+ lines.push("## Verification Plan");
115
+ lines.push("");
116
+ for (const module of modules) {
117
+ for (const verificationCheck of module.verification.checks) {
118
+ lines.push(`- \`${module.moduleRef}\`: \`${verificationCheck.id}\` (${verificationCheck.lifecycle}) — ${verificationCheck.description}`);
119
+ }
120
+ }
121
+ const content = `${lines.join("\n")}\n`;
122
+ // defense-in-depth: deep-redact every raw secret value wherever it appears
123
+ const rawSecrets = collectRawSecrets(modules, input.config);
124
+ const redacted = redactDeep(content, rawSecrets);
125
+ return typeof redacted === "string" ? redacted : content;
126
+ }
127
+ export async function generateInstallDoc(input) {
128
+ const content = renderInstallDoc(input);
129
+ await writeInstallDoc(input.paths, content);
130
+ return content;
131
+ }
@@ -0,0 +1,11 @@
1
+ export type SupportedWorkspacePackageManager = "npm" | "pnpm";
2
+ export interface WorkspacePackageManagerSelection {
3
+ name: SupportedWorkspacePackageManager;
4
+ declared?: string;
5
+ }
6
+ export interface PackageCommandRunner {
7
+ run(command: string, args: readonly string[], cwd: string): Promise<string>;
8
+ }
9
+ export declare function systemPackageCommandRunner(): PackageCommandRunner;
10
+ export declare function readWorkspacePackageManagerSelection(workspaceRoot: string): Promise<WorkspacePackageManagerSelection | undefined>;
11
+ export declare function findExecutable(command: string): boolean;
@@ -0,0 +1,58 @@
1
+ import { execFile } from "node:child_process";
2
+ import { accessSync, constants } from "node:fs";
3
+ import { readFile } from "node:fs/promises";
4
+ import { delimiter, join } from "node:path";
5
+ import { promisify } from "node:util";
6
+ const execFileAsync = promisify(execFile);
7
+ export function systemPackageCommandRunner() {
8
+ return {
9
+ async run(command, args, cwd) {
10
+ const result = await execFileAsync(command, [...args], {
11
+ cwd,
12
+ encoding: "utf8",
13
+ maxBuffer: 4 * 1024 * 1024,
14
+ });
15
+ return result.stdout;
16
+ },
17
+ };
18
+ }
19
+ export async function readWorkspacePackageManagerSelection(workspaceRoot) {
20
+ let declared;
21
+ try {
22
+ const raw = await readFile(join(workspaceRoot, "package.json"), "utf8");
23
+ const parsed = JSON.parse(raw);
24
+ if (isRecord(parsed) && typeof parsed.packageManager === "string") {
25
+ declared = parsed.packageManager;
26
+ }
27
+ }
28
+ catch {
29
+ // A Fresh Workspace without package.json uses npm for bootstrap. Invalid
30
+ // package.json is rejected earlier by installer preflight.
31
+ }
32
+ if (declared === undefined)
33
+ return { name: "npm" };
34
+ const separator = declared.lastIndexOf("@");
35
+ const name = separator > 0 ? declared.slice(0, separator) : declared;
36
+ if (name !== "npm" && name !== "pnpm")
37
+ return undefined;
38
+ return { name, declared };
39
+ }
40
+ export function findExecutable(command) {
41
+ const path = process.env.PATH ?? "";
42
+ const suffixes = process.platform === "win32" ? [".cmd", ".exe", ""] : [""];
43
+ for (const directory of path.split(delimiter).filter(Boolean)) {
44
+ for (const suffix of suffixes) {
45
+ try {
46
+ accessSync(join(directory, `${command}${suffix}`), constants.X_OK);
47
+ return true;
48
+ }
49
+ catch {
50
+ // keep scanning PATH
51
+ }
52
+ }
53
+ }
54
+ return false;
55
+ }
56
+ function isRecord(value) {
57
+ return typeof value === "object" && value !== null && !Array.isArray(value);
58
+ }
@@ -0,0 +1,47 @@
1
+ import { type LifecyclePrimitive, type ModuleOperationResult } from "@tomflow/proflow-module-contract";
2
+ import type { ResolvedModule } from "../contracts.ts";
3
+ import type { ModuleCatalog } from "../modules.ts";
4
+ export interface LifecycleDispatchResult {
5
+ moduleRef: string;
6
+ primitive: LifecyclePrimitive;
7
+ result: ModuleOperationResult;
8
+ observedEffects: string[];
9
+ }
10
+ export type LifecycleRunStatus = "EXECUTED" | "SKIP_UNSUPPORTED";
11
+ export interface LifecycleRunResult {
12
+ moduleRef: string;
13
+ primitive: LifecyclePrimitive;
14
+ status: LifecycleRunStatus;
15
+ result: ModuleOperationResult | undefined;
16
+ observedEffects: string[];
17
+ }
18
+ /**
19
+ * Dispatches a single lifecycle primitive against one module through its
20
+ * public deployment adapter. The descriptor is the source of truth for what is
21
+ * supported: a primitive not declared in `lifecycle` is rejected with
22
+ * `LIFECYCLE_UNSUPPORTED` rather than faked. The adapter result is always
23
+ * runtime-validated against the Module Operation Result schema.
24
+ */
25
+ export declare function dispatchLifecycle(catalog: ModuleCatalog, module: ResolvedModule, primitive: LifecyclePrimitive): Promise<LifecycleDispatchResult>;
26
+ /**
27
+ * Starts a module set in forward dependency topological order, dispatching the
28
+ * `start` primitive only to modules that declare it.
29
+ */
30
+ export declare function startModules(catalog: ModuleCatalog, modules: readonly ResolvedModule[]): Promise<LifecycleRunResult[]>;
31
+ /**
32
+ * Stops a module set in reverse dependency topological order, dispatching the
33
+ * `stop` primitive only to modules that declare it.
34
+ */
35
+ export declare function stopModules(catalog: ModuleCatalog, modules: readonly ResolvedModule[]): Promise<LifecycleRunResult[]>;
36
+ /**
37
+ * Restarts a module set in forward dependency topological order, dispatching
38
+ * `restart` only to modules that explicitly declare it. Dependencies are
39
+ * settled before the modules that consume them.
40
+ */
41
+ export declare function restartModules(catalog: ModuleCatalog, modules: readonly ResolvedModule[]): Promise<LifecycleRunResult[]>;
42
+ /**
43
+ * Queries current reality by dispatching the `status` primitive to every
44
+ * module that declares it. Status is always read live from the adapter; no
45
+ * persisted or cached value is substituted for current reality.
46
+ */
47
+ export declare function statusModules(catalog: ModuleCatalog, modules: readonly ResolvedModule[]): Promise<LifecycleRunResult[]>;
@@ -0,0 +1,142 @@
1
+ import { moduleOperationResultSchema, } from "@tomflow/proflow-module-contract";
2
+ import { PlatformError } from "../errors.js";
3
+ import { buildDependencyGraph } from "../graph/graph.js";
4
+ function isRecord(value) {
5
+ return typeof value === "object" && value !== null;
6
+ }
7
+ function isPrimitiveFn(value) {
8
+ return typeof value === "function";
9
+ }
10
+ function isWrappedResult(value) {
11
+ return isRecord(value) && "result" in value;
12
+ }
13
+ function moduleSource(module) {
14
+ if (module.source.type === "registry") {
15
+ throw new PlatformError("DESCRIPTOR_INVALID", `registry bootstrap target ${module.packageName} has no local lifecycle adapter`);
16
+ }
17
+ const source = {
18
+ type: module.source.type,
19
+ packageName: module.packageName,
20
+ };
21
+ if (module.source.path !== undefined)
22
+ source.path = module.source.path;
23
+ return source;
24
+ }
25
+ function resolveBehaviorAdapter(namespace) {
26
+ if (!isRecord(namespace)) {
27
+ throw new PlatformError("COMMAND_FAILED", "lifecycle adapter namespace is not an object");
28
+ }
29
+ const behaviorAdapter = namespace.behaviorAdapter;
30
+ if (!isRecord(behaviorAdapter)) {
31
+ throw new PlatformError("COMMAND_FAILED", "lifecycle adapter exposes no behaviorAdapter object");
32
+ }
33
+ return behaviorAdapter;
34
+ }
35
+ function observedEffectsOf(value) {
36
+ if (!Array.isArray(value))
37
+ return [];
38
+ return value.filter((item) => typeof item === "string");
39
+ }
40
+ function normalizeInvocation(raw) {
41
+ if (isWrappedResult(raw)) {
42
+ return {
43
+ result: raw.result,
44
+ observedEffects: observedEffectsOf(raw.observedEffects),
45
+ };
46
+ }
47
+ return { result: raw, observedEffects: [] };
48
+ }
49
+ /**
50
+ * Dispatches a single lifecycle primitive against one module through its
51
+ * public deployment adapter. The descriptor is the source of truth for what is
52
+ * supported: a primitive not declared in `lifecycle` is rejected with
53
+ * `LIFECYCLE_UNSUPPORTED` rather than faked. The adapter result is always
54
+ * runtime-validated against the Module Operation Result schema.
55
+ */
56
+ export async function dispatchLifecycle(catalog, module, primitive) {
57
+ if (!module.lifecycle.includes(primitive)) {
58
+ throw new PlatformError("LIFECYCLE_UNSUPPORTED", `module ${module.moduleRef} does not declare lifecycle primitive "${primitive}"`);
59
+ }
60
+ const namespace = await catalog.loadAdapter(moduleSource(module));
61
+ const adapter = resolveBehaviorAdapter(namespace);
62
+ const invoke = adapter[primitive];
63
+ if (!isPrimitiveFn(invoke)) {
64
+ throw new PlatformError("COMMAND_FAILED", `module ${module.moduleRef} declares "${primitive}" but its adapter does not implement it`);
65
+ }
66
+ const { result, observedEffects } = normalizeInvocation(await invoke());
67
+ const parsed = moduleOperationResultSchema.safeParse(result);
68
+ if (!parsed.success) {
69
+ throw new PlatformError("COMMAND_FAILED", `module ${module.moduleRef} "${primitive}" returned an invalid result: ${parsed.error.message}`);
70
+ }
71
+ return {
72
+ moduleRef: module.moduleRef,
73
+ primitive,
74
+ result: parsed.data,
75
+ observedEffects,
76
+ };
77
+ }
78
+ async function runOne(catalog, module, primitive) {
79
+ if (!module.lifecycle.includes(primitive)) {
80
+ return {
81
+ moduleRef: module.moduleRef,
82
+ primitive,
83
+ status: "SKIP_UNSUPPORTED",
84
+ result: undefined,
85
+ observedEffects: [],
86
+ };
87
+ }
88
+ const dispatched = await dispatchLifecycle(catalog, module, primitive);
89
+ return {
90
+ moduleRef: dispatched.moduleRef,
91
+ primitive,
92
+ status: "EXECUTED",
93
+ result: dispatched.result,
94
+ observedEffects: dispatched.observedEffects,
95
+ };
96
+ }
97
+ async function runInOrder(catalog, modules, order, primitive) {
98
+ const byRef = new Map(modules.map((module) => [module.moduleRef, module]));
99
+ const results = [];
100
+ for (const moduleRef of order) {
101
+ const module = byRef.get(moduleRef);
102
+ if (module === undefined)
103
+ continue;
104
+ results.push(await runOne(catalog, module, primitive));
105
+ }
106
+ return results;
107
+ }
108
+ /**
109
+ * Starts a module set in forward dependency topological order, dispatching the
110
+ * `start` primitive only to modules that declare it.
111
+ */
112
+ export async function startModules(catalog, modules) {
113
+ return runInOrder(catalog, modules, buildDependencyGraph(modules).order, "start");
114
+ }
115
+ /**
116
+ * Stops a module set in reverse dependency topological order, dispatching the
117
+ * `stop` primitive only to modules that declare it.
118
+ */
119
+ export async function stopModules(catalog, modules) {
120
+ const order = [...buildDependencyGraph(modules).order].reverse();
121
+ return runInOrder(catalog, modules, order, "stop");
122
+ }
123
+ /**
124
+ * Restarts a module set in forward dependency topological order, dispatching
125
+ * `restart` only to modules that explicitly declare it. Dependencies are
126
+ * settled before the modules that consume them.
127
+ */
128
+ export async function restartModules(catalog, modules) {
129
+ return runInOrder(catalog, modules, buildDependencyGraph(modules).order, "restart");
130
+ }
131
+ /**
132
+ * Queries current reality by dispatching the `status` primitive to every
133
+ * module that declares it. Status is always read live from the adapter; no
134
+ * persisted or cached value is substituted for current reality.
135
+ */
136
+ export async function statusModules(catalog, modules) {
137
+ const results = [];
138
+ for (const module of modules) {
139
+ results.push(await runOne(catalog, module, "status"));
140
+ }
141
+ return results;
142
+ }