@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.
- package/README.md +37 -0
- package/conformance.json +1 -0
- package/dist/deployment/adapter.d.ts +57 -0
- package/dist/deployment/adapter.js +25 -0
- package/dist/deployment/descriptor.d.ts +39 -0
- package/dist/deployment/descriptor.js +41 -0
- package/dist/src/apply/apply.d.ts +24 -0
- package/dist/src/apply/apply.js +271 -0
- package/dist/src/apply/cleanup.d.ts +12 -0
- package/dist/src/apply/cleanup.js +37 -0
- package/dist/src/apply/current.d.ts +4 -0
- package/dist/src/apply/current.js +73 -0
- package/dist/src/apply/driver.d.ts +23 -0
- package/dist/src/apply/driver.js +154 -0
- package/dist/src/apply/execute.d.ts +36 -0
- package/dist/src/apply/execute.js +124 -0
- package/dist/src/apply/index.d.ts +8 -0
- package/dist/src/apply/index.js +4 -0
- package/dist/src/apply/reality.d.ts +20 -0
- package/dist/src/apply/reality.js +93 -0
- package/dist/src/binding/production-bindings.d.ts +45 -0
- package/dist/src/binding/production-bindings.js +102 -0
- package/dist/src/cli.d.ts +12 -0
- package/dist/src/cli.js +712 -0
- package/dist/src/contracts.d.ts +113 -0
- package/dist/src/contracts.js +1 -0
- package/dist/src/discovery/catalog.d.ts +10 -0
- package/dist/src/discovery/catalog.js +76 -0
- package/dist/src/discovery/discover.d.ts +19 -0
- package/dist/src/discovery/discover.js +124 -0
- package/dist/src/discovery/index.d.ts +5 -0
- package/dist/src/discovery/index.js +4 -0
- package/dist/src/discovery/installed.d.ts +21 -0
- package/dist/src/discovery/installed.js +130 -0
- package/dist/src/discovery/workspace.d.ts +10 -0
- package/dist/src/discovery/workspace.js +155 -0
- package/dist/src/docs/docs.d.ts +47 -0
- package/dist/src/docs/docs.js +129 -0
- package/dist/src/docs/index.d.ts +1 -0
- package/dist/src/docs/index.js +1 -0
- package/dist/src/doctor/doctor.d.ts +41 -0
- package/dist/src/doctor/doctor.js +80 -0
- package/dist/src/doctor/index.d.ts +2 -0
- package/dist/src/doctor/index.js +1 -0
- package/dist/src/errors.d.ts +6 -0
- package/dist/src/errors.js +35 -0
- package/dist/src/graph/graph.d.ts +23 -0
- package/dist/src/graph/graph.js +132 -0
- package/dist/src/graph/index.d.ts +2 -0
- package/dist/src/graph/index.js +1 -0
- package/dist/src/index.d.ts +28 -0
- package/dist/src/index.js +18 -0
- package/dist/src/install/bootstrap.d.ts +10 -0
- package/dist/src/install/bootstrap.js +79 -0
- package/dist/src/install/environment.d.ts +29 -0
- package/dist/src/install/environment.js +226 -0
- package/dist/src/install/index.d.ts +6 -0
- package/dist/src/install/index.js +3 -0
- package/dist/src/install/install.d.ts +19 -0
- package/dist/src/install/install.js +131 -0
- package/dist/src/install/package-manager.d.ts +11 -0
- package/dist/src/install/package-manager.js +58 -0
- package/dist/src/lifecycle/dispatch.d.ts +47 -0
- package/dist/src/lifecycle/dispatch.js +142 -0
- package/dist/src/lifecycle/index.d.ts +3 -0
- package/dist/src/lifecycle/index.js +2 -0
- package/dist/src/lifecycle/service-process.d.ts +11 -0
- package/dist/src/lifecycle/service-process.js +326 -0
- package/dist/src/manifest/index.d.ts +2 -0
- package/dist/src/manifest/index.js +1 -0
- package/dist/src/manifest/manifest.d.ts +61 -0
- package/dist/src/manifest/manifest.js +160 -0
- package/dist/src/modules.d.ts +11 -0
- package/dist/src/modules.js +55 -0
- package/dist/src/observer/deployment-summary.d.ts +31 -0
- package/dist/src/observer/deployment-summary.js +34 -0
- package/dist/src/paths.d.ts +21 -0
- package/dist/src/paths.js +56 -0
- package/dist/src/persistence/atomic.d.ts +2 -0
- package/dist/src/persistence/atomic.js +16 -0
- package/dist/src/persistence/config.d.ts +12 -0
- package/dist/src/persistence/config.js +43 -0
- package/dist/src/persistence/generated.d.ts +2 -0
- package/dist/src/persistence/generated.js +4 -0
- package/dist/src/persistence/guards.d.ts +7 -0
- package/dist/src/persistence/guards.js +120 -0
- package/dist/src/persistence/index.d.ts +6 -0
- package/dist/src/persistence/index.js +5 -0
- package/dist/src/persistence/plans.d.ts +5 -0
- package/dist/src/persistence/plans.js +34 -0
- package/dist/src/persistence/state.d.ts +7 -0
- package/dist/src/persistence/state.js +34 -0
- package/dist/src/persistence/verification.d.ts +5 -0
- package/dist/src/persistence/verification.js +47 -0
- package/dist/src/planner/assemble.d.ts +16 -0
- package/dist/src/planner/assemble.js +107 -0
- package/dist/src/planner/check.d.ts +20 -0
- package/dist/src/planner/check.js +96 -0
- package/dist/src/planner/fingerprint.d.ts +12 -0
- package/dist/src/planner/fingerprint.js +164 -0
- package/dist/src/planner/index.d.ts +13 -0
- package/dist/src/planner/index.js +7 -0
- package/dist/src/planner/plan.d.ts +14 -0
- package/dist/src/planner/plan.js +156 -0
- package/dist/src/planner/repair.d.ts +28 -0
- package/dist/src/planner/repair.js +131 -0
- package/dist/src/planner/stale.d.ts +7 -0
- package/dist/src/planner/stale.js +23 -0
- package/dist/src/planner/steps.d.ts +37 -0
- package/dist/src/planner/steps.js +165 -0
- package/dist/src/planner/target-catalog.d.ts +13 -0
- package/dist/src/planner/target-catalog.js +48 -0
- package/dist/src/planner/upgrade.d.ts +18 -0
- package/dist/src/planner/upgrade.js +105 -0
- package/dist/src/preflight/config.d.ts +10 -0
- package/dist/src/preflight/config.js +45 -0
- package/dist/src/preflight/findings.d.ts +26 -0
- package/dist/src/preflight/findings.js +1 -0
- package/dist/src/preflight/index.d.ts +7 -0
- package/dist/src/preflight/index.js +3 -0
- package/dist/src/preflight/preflight.d.ts +8 -0
- package/dist/src/preflight/preflight.js +174 -0
- package/dist/src/preflight/requirements.d.ts +11 -0
- package/dist/src/preflight/requirements.js +224 -0
- package/dist/src/ready/index.d.ts +2 -0
- package/dist/src/ready/index.js +1 -0
- package/dist/src/ready/ready.d.ts +40 -0
- package/dist/src/ready/ready.js +201 -0
- package/dist/src/registry/index.d.ts +2 -0
- package/dist/src/registry/index.js +1 -0
- package/dist/src/registry/npm-registry.d.ts +51 -0
- package/dist/src/registry/npm-registry.js +341 -0
- package/dist/src/security/index.d.ts +3 -0
- package/dist/src/security/index.js +2 -0
- package/dist/src/security/lock.d.ts +9 -0
- package/dist/src/security/lock.js +89 -0
- package/dist/src/security/redact.d.ts +6 -0
- package/dist/src/security/redact.js +48 -0
- package/dist/src/verification/index.d.ts +2 -0
- package/dist/src/verification/index.js +1 -0
- package/dist/src/verification/verify.d.ts +33 -0
- package/dist/src/verification/verify.js +114 -0
- package/package.json +49 -0
- package/proflow.module.json +45 -0
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export type { LifecycleDispatchResult, LifecycleRunResult, LifecycleRunStatus, } from "./dispatch.ts";
|
|
2
|
+
export { dispatchLifecycle, restartModules, startModules, statusModules, stopModules, } from "./dispatch.ts";
|
|
3
|
+
export { managedServiceStatus, restartManagedService, startManagedService, stopManagedService, } from "./service-process.ts";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type ModuleOperationResult } from "@tomflow/proflow-module-contract";
|
|
2
|
+
import type { ResolvedModule } from "../contracts.ts";
|
|
3
|
+
import type { WorkspacePaths } from "../paths.ts";
|
|
4
|
+
export interface ManagedServiceInvocation {
|
|
5
|
+
result: ModuleOperationResult;
|
|
6
|
+
observedEffects: string[];
|
|
7
|
+
}
|
|
8
|
+
export declare function managedServiceStatus(paths: WorkspacePaths, module: ResolvedModule): Promise<ManagedServiceInvocation>;
|
|
9
|
+
export declare function startManagedService(paths: WorkspacePaths, module: ResolvedModule, rawBinding: unknown): Promise<ManagedServiceInvocation>;
|
|
10
|
+
export declare function stopManagedService(paths: WorkspacePaths, module: ResolvedModule): Promise<ManagedServiceInvocation>;
|
|
11
|
+
export declare function restartManagedService(paths: WorkspacePaths, module: ResolvedModule, binding: unknown): Promise<ManagedServiceInvocation>;
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { execFile, spawn } from "node:child_process";
|
|
2
|
+
import { mkdir, open, readFile, rm } from "node:fs/promises";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
import { serviceProcessBindingSchema, } from "@tomflow/proflow-module-contract";
|
|
8
|
+
import { writeJsonAtomic } from "../persistence/atomic.js";
|
|
9
|
+
const execFileAsync = promisify(execFile);
|
|
10
|
+
function base(module) {
|
|
11
|
+
return {
|
|
12
|
+
contract: "deployment.result.v1",
|
|
13
|
+
ok: true,
|
|
14
|
+
status: "SUCCEEDED",
|
|
15
|
+
moduleRef: module.moduleRef,
|
|
16
|
+
moduleVersion: module.moduleVersion,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function actionRequired(module, action, description, checks = []) {
|
|
20
|
+
return {
|
|
21
|
+
...base(module),
|
|
22
|
+
ok: false,
|
|
23
|
+
status: "ACTION_REQUIRED",
|
|
24
|
+
actionRequired: { action, description },
|
|
25
|
+
checks,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function failed(module, message) {
|
|
29
|
+
return {
|
|
30
|
+
...base(module),
|
|
31
|
+
ok: false,
|
|
32
|
+
status: "FAILED",
|
|
33
|
+
error: { code: "COMMAND_FAILED", message, retryable: true },
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function serviceDir(paths, moduleRef) {
|
|
37
|
+
return join(paths.runtime, "services", moduleRef);
|
|
38
|
+
}
|
|
39
|
+
function recordPath(paths, moduleRef) {
|
|
40
|
+
return join(serviceDir(paths, moduleRef), "process.json");
|
|
41
|
+
}
|
|
42
|
+
async function readRecord(paths, moduleRef) {
|
|
43
|
+
try {
|
|
44
|
+
const raw = JSON.parse(await readFile(recordPath(paths, moduleRef), "utf8"));
|
|
45
|
+
if (typeof raw !== "object" || raw === null)
|
|
46
|
+
return undefined;
|
|
47
|
+
const value = raw;
|
|
48
|
+
if (value.contract !== "deployment.service-process-state.v1" ||
|
|
49
|
+
typeof value.pid !== "number" ||
|
|
50
|
+
typeof value.moduleRef !== "string" ||
|
|
51
|
+
typeof value.packageName !== "string" ||
|
|
52
|
+
typeof value.moduleVersion !== "string" ||
|
|
53
|
+
typeof value.binPath !== "string" ||
|
|
54
|
+
typeof value.configPath !== "string" ||
|
|
55
|
+
typeof value.stdoutPath !== "string" ||
|
|
56
|
+
typeof value.stderrPath !== "string" ||
|
|
57
|
+
typeof value.startedAt !== "string")
|
|
58
|
+
return undefined;
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function recordIdentityMatchesModule(record, module) {
|
|
66
|
+
return (record.moduleRef === module.moduleRef &&
|
|
67
|
+
record.packageName === module.packageName);
|
|
68
|
+
}
|
|
69
|
+
function recordVersionMatchesModule(record, module) {
|
|
70
|
+
return record.moduleVersion === module.moduleVersion;
|
|
71
|
+
}
|
|
72
|
+
function isAlive(pid) {
|
|
73
|
+
try {
|
|
74
|
+
process.kill(pid, 0);
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async function isOwnedProcess(record) {
|
|
82
|
+
if (!isAlive(record.pid))
|
|
83
|
+
return false;
|
|
84
|
+
try {
|
|
85
|
+
let commandLine;
|
|
86
|
+
if (process.platform === "win32") {
|
|
87
|
+
const result = await execFileAsync("powershell.exe", [
|
|
88
|
+
"-NoProfile",
|
|
89
|
+
"-Command",
|
|
90
|
+
`(Get-CimInstance Win32_Process -Filter "ProcessId = ${record.pid}").CommandLine`,
|
|
91
|
+
], { encoding: "utf8" });
|
|
92
|
+
commandLine = result.stdout;
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
const result = await execFileAsync("ps", ["-p", String(record.pid), "-o", "command="], { encoding: "utf8" });
|
|
96
|
+
commandLine = result.stdout;
|
|
97
|
+
}
|
|
98
|
+
return (commandLine.includes(record.binPath) &&
|
|
99
|
+
commandLine.includes(record.configPath));
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async function removeRecord(paths, moduleRef) {
|
|
106
|
+
await rm(recordPath(paths, moduleRef), { force: true });
|
|
107
|
+
}
|
|
108
|
+
async function resolvePackageRoot(workspaceRoot, module) {
|
|
109
|
+
if (module.source.type === "workspace") {
|
|
110
|
+
if (!module.source.path)
|
|
111
|
+
throw new Error(`workspace source missing path for ${module.packageName}`);
|
|
112
|
+
return module.source.path;
|
|
113
|
+
}
|
|
114
|
+
if (module.source.type !== "installed") {
|
|
115
|
+
throw new Error(`registry bootstrap target ${module.packageName} has no local package root`);
|
|
116
|
+
}
|
|
117
|
+
const require = createRequire(pathToFileURL(join(workspaceRoot, "package.json")));
|
|
118
|
+
const artifact = require.resolve(`${module.packageName}/deployment/descriptor`);
|
|
119
|
+
let current = dirname(artifact);
|
|
120
|
+
for (;;) {
|
|
121
|
+
try {
|
|
122
|
+
const parsed = JSON.parse(await readFile(join(current, "package.json"), "utf8"));
|
|
123
|
+
if (parsed.name === module.packageName)
|
|
124
|
+
return current;
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// keep walking upward
|
|
128
|
+
}
|
|
129
|
+
const parent = dirname(current);
|
|
130
|
+
if (parent === current)
|
|
131
|
+
throw new Error(`package root not found for ${module.packageName}`);
|
|
132
|
+
current = parent;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async function resolveOwnedBin(workspaceRoot, module, binding) {
|
|
136
|
+
const packageRoot = await resolvePackageRoot(workspaceRoot, module);
|
|
137
|
+
const manifest = JSON.parse(await readFile(join(packageRoot, "package.json"), "utf8"));
|
|
138
|
+
const relative = typeof manifest.bin === "string"
|
|
139
|
+
? manifest.bin
|
|
140
|
+
: manifest.bin?.[binding.bin];
|
|
141
|
+
if (typeof relative !== "string")
|
|
142
|
+
throw new Error(`package ${module.packageName} does not own bin ${binding.bin}`);
|
|
143
|
+
const target = resolve(packageRoot, relative);
|
|
144
|
+
const root = resolve(packageRoot);
|
|
145
|
+
if (target !== root && !target.startsWith(`${root}/`))
|
|
146
|
+
throw new Error(`service bin escapes package root: ${relative}`);
|
|
147
|
+
return target;
|
|
148
|
+
}
|
|
149
|
+
export async function managedServiceStatus(paths, module) {
|
|
150
|
+
const record = await readRecord(paths, module.moduleRef);
|
|
151
|
+
if (!record || !(await isOwnedProcess(record))) {
|
|
152
|
+
if (record)
|
|
153
|
+
await removeRecord(paths, module.moduleRef);
|
|
154
|
+
return {
|
|
155
|
+
result: actionRequired(module, "start-service", "Service process is not running", [
|
|
156
|
+
{
|
|
157
|
+
id: "service-process-live",
|
|
158
|
+
status: "FAIL",
|
|
159
|
+
message: "No live managed service process",
|
|
160
|
+
},
|
|
161
|
+
]),
|
|
162
|
+
observedEffects: [],
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
if (!recordIdentityMatchesModule(record, module)) {
|
|
166
|
+
return {
|
|
167
|
+
result: failed(module, "managed service state identity does not match the current module"),
|
|
168
|
+
observedEffects: [],
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
if (!recordVersionMatchesModule(record, module)) {
|
|
172
|
+
return {
|
|
173
|
+
result: actionRequired(module, "restart-service", `Managed service process is still running package version ${record.moduleVersion}; restart is required for ${module.moduleVersion}`, [
|
|
174
|
+
{
|
|
175
|
+
id: "service-process-version",
|
|
176
|
+
status: "FAIL",
|
|
177
|
+
message: `Running ${record.moduleVersion}, installed ${module.moduleVersion}`,
|
|
178
|
+
},
|
|
179
|
+
]),
|
|
180
|
+
observedEffects: [],
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
result: {
|
|
185
|
+
...base(module),
|
|
186
|
+
data: { state: "RUNNING", pid: record.pid, startedAt: record.startedAt },
|
|
187
|
+
checks: [
|
|
188
|
+
{
|
|
189
|
+
id: "service-process-live",
|
|
190
|
+
status: "PASS",
|
|
191
|
+
message: `Managed service process ${record.pid} is alive`,
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
},
|
|
195
|
+
observedEffects: [],
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
export async function startManagedService(paths, module, rawBinding) {
|
|
199
|
+
const existing = await readRecord(paths, module.moduleRef);
|
|
200
|
+
if (existing && (await isOwnedProcess(existing))) {
|
|
201
|
+
if (!recordIdentityMatchesModule(existing, module)) {
|
|
202
|
+
return {
|
|
203
|
+
result: failed(module, "managed service state identity does not match the current module"),
|
|
204
|
+
observedEffects: [],
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
return managedServiceStatus(paths, module);
|
|
208
|
+
}
|
|
209
|
+
if (existing)
|
|
210
|
+
await removeRecord(paths, module.moduleRef);
|
|
211
|
+
const binding = serviceProcessBindingSchema.parse(rawBinding);
|
|
212
|
+
let binPath;
|
|
213
|
+
try {
|
|
214
|
+
binPath = await resolveOwnedBin(paths.root, module, binding);
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
return {
|
|
218
|
+
result: failed(module, error instanceof Error ? error.message : String(error)),
|
|
219
|
+
observedEffects: [],
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
const dir = serviceDir(paths, module.moduleRef);
|
|
223
|
+
const configPath = join(dir, "config.json");
|
|
224
|
+
const stdoutPath = join(paths.logsDeployment, `${module.moduleRef}.stdout.log`);
|
|
225
|
+
const stderrPath = join(paths.logsDeployment, `${module.moduleRef}.stderr.log`);
|
|
226
|
+
await writeJsonAtomic(configPath, binding.config, 0o600);
|
|
227
|
+
await mkdir(paths.logsDeployment, { recursive: true });
|
|
228
|
+
const stdout = await open(stdoutPath, "a");
|
|
229
|
+
const stderr = await open(stderrPath, "a");
|
|
230
|
+
try {
|
|
231
|
+
const child = spawn(process.execPath, [binPath, binding.startCommand, configPath], {
|
|
232
|
+
cwd: paths.root,
|
|
233
|
+
detached: true,
|
|
234
|
+
env: process.env,
|
|
235
|
+
stdio: ["ignore", stdout.fd, stderr.fd],
|
|
236
|
+
});
|
|
237
|
+
await new Promise((resolveSpawn, rejectSpawn) => {
|
|
238
|
+
child.once("spawn", resolveSpawn);
|
|
239
|
+
child.once("error", rejectSpawn);
|
|
240
|
+
});
|
|
241
|
+
if (child.pid === undefined)
|
|
242
|
+
return {
|
|
243
|
+
result: failed(module, "service process spawned without pid"),
|
|
244
|
+
observedEffects: [],
|
|
245
|
+
};
|
|
246
|
+
child.unref();
|
|
247
|
+
const record = {
|
|
248
|
+
contract: "deployment.service-process-state.v1",
|
|
249
|
+
moduleRef: module.moduleRef,
|
|
250
|
+
packageName: module.packageName,
|
|
251
|
+
moduleVersion: module.moduleVersion,
|
|
252
|
+
pid: child.pid,
|
|
253
|
+
startedAt: new Date().toISOString(),
|
|
254
|
+
binPath,
|
|
255
|
+
configPath,
|
|
256
|
+
stdoutPath,
|
|
257
|
+
stderrPath,
|
|
258
|
+
};
|
|
259
|
+
await writeJsonAtomic(recordPath(paths, module.moduleRef), record, 0o600);
|
|
260
|
+
return {
|
|
261
|
+
result: { ...base(module), data: { state: "RUNNING", pid: child.pid } },
|
|
262
|
+
observedEffects: ["Manage the declared service process"],
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
catch (error) {
|
|
266
|
+
return {
|
|
267
|
+
result: failed(module, error instanceof Error ? error.message : String(error)),
|
|
268
|
+
observedEffects: [],
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
finally {
|
|
272
|
+
await Promise.all([stdout.close(), stderr.close()]);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
async function waitForExit(pid, timeoutMs = 2_000) {
|
|
276
|
+
const deadline = Date.now() + timeoutMs;
|
|
277
|
+
while (Date.now() < deadline) {
|
|
278
|
+
if (!isAlive(pid))
|
|
279
|
+
return true;
|
|
280
|
+
await new Promise((resolveWait) => setTimeout(resolveWait, 50));
|
|
281
|
+
}
|
|
282
|
+
return !isAlive(pid);
|
|
283
|
+
}
|
|
284
|
+
export async function stopManagedService(paths, module) {
|
|
285
|
+
const record = await readRecord(paths, module.moduleRef);
|
|
286
|
+
if (!record || !(await isOwnedProcess(record))) {
|
|
287
|
+
if (record)
|
|
288
|
+
await removeRecord(paths, module.moduleRef);
|
|
289
|
+
return {
|
|
290
|
+
result: { ...base(module), data: { state: "STOPPED" } },
|
|
291
|
+
observedEffects: [],
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
if (!recordIdentityMatchesModule(record, module)) {
|
|
295
|
+
return {
|
|
296
|
+
result: failed(module, "managed service state identity does not match the current module"),
|
|
297
|
+
observedEffects: [],
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
try {
|
|
301
|
+
process.kill(record.pid, "SIGTERM");
|
|
302
|
+
if (!(await waitForExit(record.pid))) {
|
|
303
|
+
return {
|
|
304
|
+
result: failed(module, `service process ${record.pid} did not stop after SIGTERM`),
|
|
305
|
+
observedEffects: [],
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
await removeRecord(paths, module.moduleRef);
|
|
309
|
+
return {
|
|
310
|
+
result: { ...base(module), data: { state: "STOPPED", pid: record.pid } },
|
|
311
|
+
observedEffects: ["Manage the declared service process"],
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
catch (error) {
|
|
315
|
+
return {
|
|
316
|
+
result: failed(module, error instanceof Error ? error.message : String(error)),
|
|
317
|
+
observedEffects: [],
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
export async function restartManagedService(paths, module, binding) {
|
|
322
|
+
const stopped = await stopManagedService(paths, module);
|
|
323
|
+
if (stopped.result.status === "FAILED")
|
|
324
|
+
return stopped;
|
|
325
|
+
return startManagedService(paths, module, binding);
|
|
326
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { buildManifest, MANIFEST_CONTRACT } from "./manifest.js";
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { ModuleOperationResult } from "@tomflow/proflow-module-contract";
|
|
2
|
+
import type { PlatformState, ResolvedModule, VerificationRecord } from "../contracts.ts";
|
|
3
|
+
import type { ModuleCatalog } from "../modules.ts";
|
|
4
|
+
import type { WorkspacePaths } from "../paths.ts";
|
|
5
|
+
export declare const MANIFEST_CONTRACT = "proflow.manifest.v1";
|
|
6
|
+
export interface ManifestModule {
|
|
7
|
+
moduleRef: string;
|
|
8
|
+
packageName: string;
|
|
9
|
+
moduleVersion: string;
|
|
10
|
+
kind: ResolvedModule["kind"];
|
|
11
|
+
source: ResolvedModule["source"];
|
|
12
|
+
provides: ResolvedModule["provides"];
|
|
13
|
+
requires: ResolvedModule["requires"];
|
|
14
|
+
runtimeObserved: boolean;
|
|
15
|
+
runtimeStatus?: ModuleOperationResult["status"];
|
|
16
|
+
runtimeResourceVersion?: string;
|
|
17
|
+
resourceIdentity?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface ManifestVerification {
|
|
20
|
+
moduleRef: string;
|
|
21
|
+
historyCount: number;
|
|
22
|
+
latest?: VerificationRecord;
|
|
23
|
+
lastPassAt?: string;
|
|
24
|
+
lastFailAt?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface ManifestConfig {
|
|
27
|
+
moduleRef: string;
|
|
28
|
+
values: Record<string, string>;
|
|
29
|
+
secretRefs: string[];
|
|
30
|
+
missing: string[];
|
|
31
|
+
}
|
|
32
|
+
export interface ManifestPendingAction {
|
|
33
|
+
moduleRef?: string;
|
|
34
|
+
action: string;
|
|
35
|
+
description?: string;
|
|
36
|
+
}
|
|
37
|
+
export interface PlatformManifest {
|
|
38
|
+
contract: typeof MANIFEST_CONTRACT;
|
|
39
|
+
observedAt: string;
|
|
40
|
+
status: PlatformState;
|
|
41
|
+
modules: ManifestModule[];
|
|
42
|
+
verification: ManifestVerification[];
|
|
43
|
+
config: ManifestConfig[];
|
|
44
|
+
pendingActions: ManifestPendingAction[];
|
|
45
|
+
}
|
|
46
|
+
export interface ManifestDeps {
|
|
47
|
+
catalog: ModuleCatalog;
|
|
48
|
+
modules: readonly ResolvedModule[];
|
|
49
|
+
paths: WorkspacePaths;
|
|
50
|
+
config?: Record<string, Record<string, string>>;
|
|
51
|
+
now?: Date;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Dynamically composes the platform manifest from current reality: the resolved
|
|
55
|
+
* module set, module/adapter versions, external resource identity/version, live
|
|
56
|
+
* runtime status (via `statusModules`, never persisted state), provides/requires,
|
|
57
|
+
* secret-redacted config readiness, verification history summary, pending
|
|
58
|
+
* ACTION_REQUIRED, and the aggregated platform status. Every composition carries
|
|
59
|
+
* an explicit `observedAt` freshness timestamp.
|
|
60
|
+
*/
|
|
61
|
+
export declare function buildManifest(deps: ManifestDeps): Promise<PlatformManifest>;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { statusModules } from "../lifecycle/index.js";
|
|
2
|
+
import { loadConfig, loadDeploymentState, loadVerificationHistory, } from "../persistence/index.js";
|
|
3
|
+
import { clearCompletedPendingActions } from "../persistence/state.js";
|
|
4
|
+
import { resolveModuleConfig } from "../preflight/index.js";
|
|
5
|
+
import { assessPlatformReady, } from "../ready/index.js";
|
|
6
|
+
import { redactSecretValues } from "../security/index.js";
|
|
7
|
+
import { configFingerprint } from "../verification/index.js";
|
|
8
|
+
export const MANIFEST_CONTRACT = "proflow.manifest.v1";
|
|
9
|
+
function compareRef(a, b) {
|
|
10
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
11
|
+
}
|
|
12
|
+
function lastVerifiedAt(records, result) {
|
|
13
|
+
for (let i = records.length - 1; i >= 0; i -= 1) {
|
|
14
|
+
const record = records[i];
|
|
15
|
+
if (record !== undefined && record.result === result) {
|
|
16
|
+
return record.verifiedAt;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Dynamically composes the platform manifest from current reality: the resolved
|
|
23
|
+
* module set, module/adapter versions, external resource identity/version, live
|
|
24
|
+
* runtime status (via `statusModules`, never persisted state), provides/requires,
|
|
25
|
+
* secret-redacted config readiness, verification history summary, pending
|
|
26
|
+
* ACTION_REQUIRED, and the aggregated platform status. Every composition carries
|
|
27
|
+
* an explicit `observedAt` freshness timestamp.
|
|
28
|
+
*/
|
|
29
|
+
export async function buildManifest(deps) {
|
|
30
|
+
const observedAt = (deps.now ?? new Date()).toISOString();
|
|
31
|
+
const modules = [...deps.modules].sort((a, b) => compareRef(a.moduleRef, b.moduleRef));
|
|
32
|
+
const statusResults = await statusModules(deps.catalog, modules);
|
|
33
|
+
const statusByRef = new Map(statusResults.map((run) => [run.moduleRef, run]));
|
|
34
|
+
const manifestModules = [];
|
|
35
|
+
const verification = [];
|
|
36
|
+
const configEntries = [];
|
|
37
|
+
const allRecords = [];
|
|
38
|
+
const pendingActions = [];
|
|
39
|
+
const blockingActions = [];
|
|
40
|
+
const resources = [];
|
|
41
|
+
const materializedConfig = {};
|
|
42
|
+
for (const module of modules) {
|
|
43
|
+
// materialized config current reality (never caller-supplied intent)
|
|
44
|
+
const persisted = await loadConfig(deps.paths, module.moduleRef);
|
|
45
|
+
if (persisted !== undefined) {
|
|
46
|
+
materializedConfig[module.moduleRef] = {
|
|
47
|
+
...persisted.publicValues,
|
|
48
|
+
...persisted.secretValues,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
// live runtime status — current reality, not persisted state
|
|
52
|
+
const run = statusByRef.get(module.moduleRef);
|
|
53
|
+
let runtimeObserved = false;
|
|
54
|
+
let runtimeStatus;
|
|
55
|
+
let runtimeResourceVersion;
|
|
56
|
+
if (run !== undefined &&
|
|
57
|
+
run.status === "EXECUTED" &&
|
|
58
|
+
run.result !== undefined) {
|
|
59
|
+
runtimeObserved = true;
|
|
60
|
+
runtimeStatus = run.result.status;
|
|
61
|
+
if (run.result.resourceVersion !== undefined) {
|
|
62
|
+
runtimeResourceVersion = run.result.resourceVersion;
|
|
63
|
+
}
|
|
64
|
+
if (run.result.status === "ACTION_REQUIRED" &&
|
|
65
|
+
run.result.actionRequired !== undefined) {
|
|
66
|
+
const action = {
|
|
67
|
+
moduleRef: module.moduleRef,
|
|
68
|
+
action: run.result.actionRequired.action,
|
|
69
|
+
description: run.result.actionRequired.description,
|
|
70
|
+
};
|
|
71
|
+
pendingActions.push(action);
|
|
72
|
+
blockingActions.push(action);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// external resource identity/version
|
|
76
|
+
let resourceIdentity;
|
|
77
|
+
if (module.kind === "external-resource") {
|
|
78
|
+
const secretRefs = module.configSlots
|
|
79
|
+
.filter((slot) => slot.type === "secretRef")
|
|
80
|
+
.map((slot) => slot.key);
|
|
81
|
+
resourceIdentity = configFingerprint(persisted?.publicValues ?? {}, secretRefs);
|
|
82
|
+
resources.push({
|
|
83
|
+
moduleRef: module.moduleRef,
|
|
84
|
+
...(resourceIdentity !== undefined ? { resourceIdentity } : {}),
|
|
85
|
+
...(runtimeResourceVersion !== undefined
|
|
86
|
+
? { resourceVersion: runtimeResourceVersion }
|
|
87
|
+
: {}),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
manifestModules.push({
|
|
91
|
+
moduleRef: module.moduleRef,
|
|
92
|
+
packageName: module.packageName,
|
|
93
|
+
moduleVersion: module.moduleVersion,
|
|
94
|
+
kind: module.kind,
|
|
95
|
+
source: module.source,
|
|
96
|
+
provides: module.provides,
|
|
97
|
+
requires: module.requires,
|
|
98
|
+
runtimeObserved,
|
|
99
|
+
...(runtimeStatus !== undefined ? { runtimeStatus } : {}),
|
|
100
|
+
...(runtimeResourceVersion !== undefined
|
|
101
|
+
? { runtimeResourceVersion }
|
|
102
|
+
: {}),
|
|
103
|
+
...(resourceIdentity !== undefined ? { resourceIdentity } : {}),
|
|
104
|
+
});
|
|
105
|
+
// verification history summary
|
|
106
|
+
const history = await loadVerificationHistory(deps.paths, module.moduleRef);
|
|
107
|
+
allRecords.push(...history);
|
|
108
|
+
const latest = history[history.length - 1];
|
|
109
|
+
const lastPassAt = lastVerifiedAt(history, "PASS");
|
|
110
|
+
const lastFailAt = lastVerifiedAt(history, "FAIL");
|
|
111
|
+
verification.push({
|
|
112
|
+
moduleRef: module.moduleRef,
|
|
113
|
+
historyCount: history.length,
|
|
114
|
+
...(latest !== undefined ? { latest } : {}),
|
|
115
|
+
...(lastPassAt !== undefined ? { lastPassAt } : {}),
|
|
116
|
+
...(lastFailAt !== undefined ? { lastFailAt } : {}),
|
|
117
|
+
});
|
|
118
|
+
// config readiness, secret-redacted, from materialized current reality
|
|
119
|
+
const resolved = resolveModuleConfig(module, materializedConfig[module.moduleRef]);
|
|
120
|
+
configEntries.push({
|
|
121
|
+
moduleRef: module.moduleRef,
|
|
122
|
+
values: redactSecretValues(resolved.values, resolved.secretRefs),
|
|
123
|
+
secretRefs: [...resolved.secretRefs],
|
|
124
|
+
missing: resolved.missing,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
// persisted pending ACTION_REQUIRED blocks readiness unless its plan is COMPLETE
|
|
128
|
+
const deploymentState = await loadDeploymentState(deps.paths);
|
|
129
|
+
if (deploymentState !== undefined) {
|
|
130
|
+
for (const pending of clearCompletedPendingActions(deploymentState)
|
|
131
|
+
.pendingActions) {
|
|
132
|
+
const action = {
|
|
133
|
+
moduleRef: pending.moduleRef,
|
|
134
|
+
action: pending.action,
|
|
135
|
+
...(pending.description !== undefined
|
|
136
|
+
? { description: pending.description }
|
|
137
|
+
: {}),
|
|
138
|
+
};
|
|
139
|
+
blockingActions.push(action);
|
|
140
|
+
pendingActions.push(action);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const { state } = assessPlatformReady({
|
|
144
|
+
modules,
|
|
145
|
+
status: statusResults,
|
|
146
|
+
verification: allRecords,
|
|
147
|
+
config: materializedConfig,
|
|
148
|
+
blockingActions,
|
|
149
|
+
resources,
|
|
150
|
+
});
|
|
151
|
+
return {
|
|
152
|
+
contract: MANIFEST_CONTRACT,
|
|
153
|
+
observedAt,
|
|
154
|
+
status: state,
|
|
155
|
+
modules: manifestModules,
|
|
156
|
+
verification,
|
|
157
|
+
config: configEntries,
|
|
158
|
+
pendingActions,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface ModuleSource {
|
|
2
|
+
type: "workspace" | "installed";
|
|
3
|
+
packageName: string;
|
|
4
|
+
path?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface ModuleCatalog {
|
|
7
|
+
sources(): Promise<ModuleSource[]>;
|
|
8
|
+
loadDescriptor(source: ModuleSource): Promise<unknown>;
|
|
9
|
+
loadAdapter(source: ModuleSource): Promise<unknown>;
|
|
10
|
+
}
|
|
11
|
+
export declare function versionSatisfies(version: string, range: string): boolean;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Minimal private semver range matcher — supports exact, >=, >, <=, <, and
|
|
2
|
+
// whitespace-separated comparator combinations. Not a general resolver.
|
|
3
|
+
function parseSemver(value) {
|
|
4
|
+
const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(value.trim());
|
|
5
|
+
if (match === null)
|
|
6
|
+
return null;
|
|
7
|
+
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
8
|
+
}
|
|
9
|
+
function compareSemver(a, b) {
|
|
10
|
+
for (let i = 0; i < 3; i += 1) {
|
|
11
|
+
const left = a[i] ?? 0;
|
|
12
|
+
const right = b[i] ?? 0;
|
|
13
|
+
if (left < right)
|
|
14
|
+
return -1;
|
|
15
|
+
if (left > right)
|
|
16
|
+
return 1;
|
|
17
|
+
}
|
|
18
|
+
return 0;
|
|
19
|
+
}
|
|
20
|
+
export function versionSatisfies(version, range) {
|
|
21
|
+
const parsed = parseSemver(version);
|
|
22
|
+
if (parsed === null)
|
|
23
|
+
return false;
|
|
24
|
+
const parts = range
|
|
25
|
+
.trim()
|
|
26
|
+
.split(/\s+/)
|
|
27
|
+
.filter((part) => part.length > 0);
|
|
28
|
+
if (parts.length === 0)
|
|
29
|
+
return false;
|
|
30
|
+
return parts.every((part) => {
|
|
31
|
+
if (part === "*")
|
|
32
|
+
return true;
|
|
33
|
+
const match = /^(>=|<=|>|<|=)?(.+)$/.exec(part);
|
|
34
|
+
const op = match?.[1] ?? "=";
|
|
35
|
+
const rest = match?.[2] ?? "";
|
|
36
|
+
const target = parseSemver(rest);
|
|
37
|
+
if (target === null)
|
|
38
|
+
return false;
|
|
39
|
+
const cmp = compareSemver(parsed, target);
|
|
40
|
+
switch (op) {
|
|
41
|
+
case ">=":
|
|
42
|
+
return cmp >= 0;
|
|
43
|
+
case ">":
|
|
44
|
+
return cmp > 0;
|
|
45
|
+
case "<=":
|
|
46
|
+
return cmp <= 0;
|
|
47
|
+
case "<":
|
|
48
|
+
return cmp < 0;
|
|
49
|
+
case "=":
|
|
50
|
+
return cmp === 0;
|
|
51
|
+
default:
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { PlatformState } from "../contracts.ts";
|
|
2
|
+
import { type WorkspacePaths } from "../paths.ts";
|
|
3
|
+
export type DeploymentObserverSource = "status" | "verify" | "doctor";
|
|
4
|
+
export type DeploymentObserverModuleStatus = "SUCCEEDED" | "BLOCKED" | "ACTION_REQUIRED" | "FAILED";
|
|
5
|
+
export interface DeploymentObserverSummary {
|
|
6
|
+
contract: "proflow.deployment-observer-summary.v1";
|
|
7
|
+
scope: "PLATFORM";
|
|
8
|
+
source: DeploymentObserverSource;
|
|
9
|
+
state: PlatformState;
|
|
10
|
+
selectedModuleCount: number;
|
|
11
|
+
totalModuleCount: number;
|
|
12
|
+
observedModuleCount: number;
|
|
13
|
+
blockingModuleCount: number;
|
|
14
|
+
modules: readonly {
|
|
15
|
+
moduleRef: string;
|
|
16
|
+
status: DeploymentObserverModuleStatus;
|
|
17
|
+
}[];
|
|
18
|
+
observedAt: string;
|
|
19
|
+
freshUntil: string;
|
|
20
|
+
}
|
|
21
|
+
export declare const DEPLOYMENT_OBSERVER_SUMMARY_TTL_MS = 60000;
|
|
22
|
+
export declare function writeDeploymentObserverSummary(input: {
|
|
23
|
+
paths: WorkspacePaths;
|
|
24
|
+
source: DeploymentObserverSource;
|
|
25
|
+
selectedModuleCount: number;
|
|
26
|
+
totalModuleCount: number;
|
|
27
|
+
modules: readonly {
|
|
28
|
+
moduleRef: string;
|
|
29
|
+
status: DeploymentObserverModuleStatus;
|
|
30
|
+
}[];
|
|
31
|
+
}): Promise<DeploymentObserverSummary>;
|