@tomflow/proflow-platform-cli 0.1.13 → 0.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -43
- package/dist/deployment/adapter.d.ts +5 -5
- package/dist/deployment/adapter.js +2 -1
- package/dist/deployment/descriptor.d.ts +2 -3
- package/dist/deployment/descriptor.js +2 -3
- package/dist/src/binding/production-bindings.d.ts +17 -15
- package/dist/src/binding/production-bindings.js +18 -59
- package/dist/src/cli.d.ts +7 -9
- package/dist/src/cli.js +329 -1237
- package/dist/src/contracts.d.ts +4 -97
- package/dist/src/discovery/discover.js +0 -1
- package/dist/src/docs/aggregate.d.ts +18 -0
- package/dist/src/docs/aggregate.js +44 -0
- package/dist/src/docs/docs.d.ts +0 -31
- package/dist/src/docs/docs.js +0 -61
- package/dist/src/docs/index.d.ts +2 -1
- package/dist/src/docs/index.js +1 -1
- package/dist/src/errors.d.ts +1 -1
- package/dist/src/errors.js +0 -18
- package/dist/src/graph/graph.d.ts +1 -4
- package/dist/src/graph/graph.js +1 -13
- package/dist/src/graph/index.d.ts +2 -2
- package/dist/src/graph/index.js +1 -1
- package/dist/src/index.d.ts +4 -24
- package/dist/src/index.js +2 -16
- package/dist/src/install/package-manager.d.ts +21 -0
- package/dist/src/install/package-manager.js +153 -0
- package/dist/src/lifecycle/dispatch.d.ts +0 -30
- package/dist/src/lifecycle/dispatch.js +0 -120
- package/dist/src/lifecycle/index.d.ts +4 -3
- package/dist/src/lifecycle/index.js +2 -2
- package/dist/src/lifecycle/thin.d.ts +11 -0
- package/dist/src/lifecycle/thin.js +68 -0
- package/dist/src/paths.d.ts +0 -14
- package/dist/src/paths.js +0 -39
- package/dist/src/persistence/config.d.ts +0 -6
- package/dist/src/persistence/config.js +0 -14
- package/dist/src/persistence/guards.d.ts +0 -6
- package/dist/src/persistence/guards.js +0 -114
- package/dist/src/persistence/index.d.ts +4 -6
- package/dist/src/persistence/index.js +2 -5
- package/dist/src/persistence/workspace-metadata.d.ts +7 -0
- package/dist/src/persistence/workspace-metadata.js +39 -0
- package/dist/src/registry/index.d.ts +1 -1
- package/dist/src/registry/index.js +1 -1
- package/dist/src/registry/npm-registry.d.ts +0 -14
- package/dist/src/registry/npm-registry.js +1 -132
- package/package.json +2 -6
- package/proflow.module.json +2 -3
- package/CHANGELOG.md +0 -79
- package/dist/src/apply/apply.d.ts +0 -26
- package/dist/src/apply/apply.js +0 -314
- package/dist/src/apply/cleanup.d.ts +0 -12
- package/dist/src/apply/cleanup.js +0 -37
- package/dist/src/apply/current.d.ts +0 -5
- package/dist/src/apply/current.js +0 -95
- package/dist/src/apply/driver.d.ts +0 -28
- package/dist/src/apply/driver.js +0 -239
- package/dist/src/apply/execute.d.ts +0 -36
- package/dist/src/apply/execute.js +0 -152
- package/dist/src/apply/index.d.ts +0 -8
- package/dist/src/apply/index.js +0 -4
- package/dist/src/apply/reality.d.ts +0 -20
- package/dist/src/apply/reality.js +0 -105
- package/dist/src/binding/global-binding.d.ts +0 -69
- package/dist/src/binding/global-binding.js +0 -349
- package/dist/src/doctor/doctor.d.ts +0 -41
- package/dist/src/doctor/doctor.js +0 -80
- package/dist/src/doctor/index.d.ts +0 -2
- package/dist/src/doctor/index.js +0 -1
- package/dist/src/install/bootstrap.d.ts +0 -10
- package/dist/src/install/bootstrap.js +0 -79
- package/dist/src/install/environment.d.ts +0 -30
- package/dist/src/install/environment.js +0 -230
- package/dist/src/install/index.d.ts +0 -6
- package/dist/src/install/index.js +0 -3
- package/dist/src/install/install.d.ts +0 -19
- package/dist/src/install/install.js +0 -172
- package/dist/src/lifecycle/service-process.d.ts +0 -11
- package/dist/src/lifecycle/service-process.js +0 -356
- package/dist/src/manifest/index.d.ts +0 -2
- package/dist/src/manifest/index.js +0 -1
- package/dist/src/manifest/manifest.d.ts +0 -61
- package/dist/src/manifest/manifest.js +0 -167
- package/dist/src/observer/deployment-summary.d.ts +0 -31
- package/dist/src/observer/deployment-summary.js +0 -34
- package/dist/src/persistence/atomic.d.ts +0 -2
- package/dist/src/persistence/atomic.js +0 -16
- package/dist/src/persistence/effective-config.d.ts +0 -3
- package/dist/src/persistence/effective-config.js +0 -20
- package/dist/src/persistence/generated.d.ts +0 -2
- package/dist/src/persistence/generated.js +0 -4
- package/dist/src/persistence/plans.d.ts +0 -5
- package/dist/src/persistence/plans.js +0 -34
- package/dist/src/persistence/state.d.ts +0 -7
- package/dist/src/persistence/state.js +0 -34
- package/dist/src/persistence/verification.d.ts +0 -5
- package/dist/src/persistence/verification.js +0 -47
- package/dist/src/planner/assemble.d.ts +0 -16
- package/dist/src/planner/assemble.js +0 -107
- package/dist/src/planner/check.d.ts +0 -20
- package/dist/src/planner/check.js +0 -107
- package/dist/src/planner/fingerprint.d.ts +0 -12
- package/dist/src/planner/fingerprint.js +0 -164
- package/dist/src/planner/index.d.ts +0 -13
- package/dist/src/planner/index.js +0 -7
- package/dist/src/planner/plan.d.ts +0 -15
- package/dist/src/planner/plan.js +0 -144
- package/dist/src/planner/repair.d.ts +0 -28
- package/dist/src/planner/repair.js +0 -131
- package/dist/src/planner/stale.d.ts +0 -7
- package/dist/src/planner/stale.js +0 -23
- package/dist/src/planner/steps.d.ts +0 -37
- package/dist/src/planner/steps.js +0 -165
- package/dist/src/planner/target-catalog.d.ts +0 -13
- package/dist/src/planner/target-catalog.js +0 -48
- package/dist/src/planner/upgrade.d.ts +0 -18
- package/dist/src/planner/upgrade.js +0 -114
- package/dist/src/preflight/config.d.ts +0 -11
- package/dist/src/preflight/config.js +0 -88
- package/dist/src/preflight/findings.d.ts +0 -26
- package/dist/src/preflight/findings.js +0 -1
- package/dist/src/preflight/index.d.ts +0 -7
- package/dist/src/preflight/index.js +0 -3
- package/dist/src/preflight/preflight.d.ts +0 -10
- package/dist/src/preflight/preflight.js +0 -190
- package/dist/src/preflight/requirements.d.ts +0 -11
- package/dist/src/preflight/requirements.js +0 -231
- package/dist/src/ready/index.d.ts +0 -2
- package/dist/src/ready/index.js +0 -1
- package/dist/src/ready/ready.d.ts +0 -40
- package/dist/src/ready/ready.js +0 -201
- package/dist/src/security/index.d.ts +0 -3
- package/dist/src/security/index.js +0 -2
- package/dist/src/security/lock.d.ts +0 -9
- package/dist/src/security/lock.js +0 -127
- package/dist/src/security/redact.d.ts +0 -6
- package/dist/src/security/redact.js +0 -48
- package/dist/src/verification/index.d.ts +0 -2
- package/dist/src/verification/index.js +0 -1
- package/dist/src/verification/verify.d.ts +0 -33
- package/dist/src/verification/verify.js +0 -114
|
@@ -1,356 +0,0 @@
|
|
|
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
|
-
async function waitForStartupStability(pid, milliseconds = 500) {
|
|
199
|
-
const deadline = Date.now() + milliseconds;
|
|
200
|
-
while (Date.now() < deadline) {
|
|
201
|
-
if (!isAlive(pid))
|
|
202
|
-
return false;
|
|
203
|
-
await new Promise((resolveWait) => setTimeout(resolveWait, 50));
|
|
204
|
-
}
|
|
205
|
-
return isAlive(pid);
|
|
206
|
-
}
|
|
207
|
-
export async function startManagedService(paths, module, rawBinding) {
|
|
208
|
-
const existing = await readRecord(paths, module.moduleRef);
|
|
209
|
-
if (existing && (await isOwnedProcess(existing))) {
|
|
210
|
-
if (!recordIdentityMatchesModule(existing, module)) {
|
|
211
|
-
return {
|
|
212
|
-
result: failed(module, "managed service state identity does not match the current module"),
|
|
213
|
-
observedEffects: [],
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
return managedServiceStatus(paths, module);
|
|
217
|
-
}
|
|
218
|
-
if (existing)
|
|
219
|
-
await removeRecord(paths, module.moduleRef);
|
|
220
|
-
const binding = serviceProcessBindingSchema.parse(rawBinding);
|
|
221
|
-
let binPath;
|
|
222
|
-
try {
|
|
223
|
-
binPath = await resolveOwnedBin(paths.root, module, binding);
|
|
224
|
-
}
|
|
225
|
-
catch (error) {
|
|
226
|
-
return {
|
|
227
|
-
result: failed(module, error instanceof Error ? error.message : String(error)),
|
|
228
|
-
observedEffects: [],
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
|
-
const dir = serviceDir(paths, module.moduleRef);
|
|
232
|
-
const configPath = join(dir, "config.json");
|
|
233
|
-
const stdoutPath = join(paths.logsDeployment, `${module.moduleRef}.stdout.log`);
|
|
234
|
-
const stderrPath = join(paths.logsDeployment, `${module.moduleRef}.stderr.log`);
|
|
235
|
-
await writeJsonAtomic(configPath, binding.config, 0o600);
|
|
236
|
-
await mkdir(paths.logsDeployment, { recursive: true });
|
|
237
|
-
const stdout = await open(stdoutPath, "a");
|
|
238
|
-
const stderr = await open(stderrPath, "a");
|
|
239
|
-
try {
|
|
240
|
-
const child = spawn(process.execPath, [binPath, binding.startCommand, configPath], {
|
|
241
|
-
cwd: paths.root,
|
|
242
|
-
detached: true,
|
|
243
|
-
env: process.env,
|
|
244
|
-
stdio: ["ignore", stdout.fd, stderr.fd],
|
|
245
|
-
});
|
|
246
|
-
await new Promise((resolveSpawn, rejectSpawn) => {
|
|
247
|
-
child.once("spawn", resolveSpawn);
|
|
248
|
-
child.once("error", rejectSpawn);
|
|
249
|
-
});
|
|
250
|
-
if (child.pid === undefined)
|
|
251
|
-
return {
|
|
252
|
-
result: failed(module, "service process spawned without pid"),
|
|
253
|
-
observedEffects: [],
|
|
254
|
-
};
|
|
255
|
-
const pid = child.pid;
|
|
256
|
-
if (!(await waitForStartupStability(pid))) {
|
|
257
|
-
return {
|
|
258
|
-
result: failed(module, `service process ${pid} exited during startup stabilization`),
|
|
259
|
-
observedEffects: [],
|
|
260
|
-
};
|
|
261
|
-
}
|
|
262
|
-
child.unref();
|
|
263
|
-
const record = {
|
|
264
|
-
contract: "deployment.service-process-state.v1",
|
|
265
|
-
moduleRef: module.moduleRef,
|
|
266
|
-
packageName: module.packageName,
|
|
267
|
-
moduleVersion: module.moduleVersion,
|
|
268
|
-
pid,
|
|
269
|
-
startedAt: new Date().toISOString(),
|
|
270
|
-
binPath,
|
|
271
|
-
configPath,
|
|
272
|
-
stdoutPath,
|
|
273
|
-
stderrPath,
|
|
274
|
-
};
|
|
275
|
-
await writeJsonAtomic(recordPath(paths, module.moduleRef), record, 0o600);
|
|
276
|
-
const observed = await managedServiceStatus(paths, module);
|
|
277
|
-
if (observed.result.status !== "SUCCEEDED") {
|
|
278
|
-
await removeRecord(paths, module.moduleRef);
|
|
279
|
-
try {
|
|
280
|
-
process.kill(pid, "SIGTERM");
|
|
281
|
-
}
|
|
282
|
-
catch {
|
|
283
|
-
// process already exited between observations
|
|
284
|
-
}
|
|
285
|
-
return {
|
|
286
|
-
result: failed(module, `service process ${pid} could not be observed as the owned RUNNING process after spawn`),
|
|
287
|
-
observedEffects: [],
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
return {
|
|
291
|
-
...observed,
|
|
292
|
-
observedEffects: ["Manage the declared service process"],
|
|
293
|
-
};
|
|
294
|
-
}
|
|
295
|
-
catch (error) {
|
|
296
|
-
return {
|
|
297
|
-
result: failed(module, error instanceof Error ? error.message : String(error)),
|
|
298
|
-
observedEffects: [],
|
|
299
|
-
};
|
|
300
|
-
}
|
|
301
|
-
finally {
|
|
302
|
-
await Promise.all([stdout.close(), stderr.close()]);
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
async function waitForExit(pid, timeoutMs = 2_000) {
|
|
306
|
-
const deadline = Date.now() + timeoutMs;
|
|
307
|
-
while (Date.now() < deadline) {
|
|
308
|
-
if (!isAlive(pid))
|
|
309
|
-
return true;
|
|
310
|
-
await new Promise((resolveWait) => setTimeout(resolveWait, 50));
|
|
311
|
-
}
|
|
312
|
-
return !isAlive(pid);
|
|
313
|
-
}
|
|
314
|
-
export async function stopManagedService(paths, module) {
|
|
315
|
-
const record = await readRecord(paths, module.moduleRef);
|
|
316
|
-
if (!record || !(await isOwnedProcess(record))) {
|
|
317
|
-
if (record)
|
|
318
|
-
await removeRecord(paths, module.moduleRef);
|
|
319
|
-
return {
|
|
320
|
-
result: { ...base(module), data: { state: "STOPPED" } },
|
|
321
|
-
observedEffects: [],
|
|
322
|
-
};
|
|
323
|
-
}
|
|
324
|
-
if (!recordIdentityMatchesModule(record, module)) {
|
|
325
|
-
return {
|
|
326
|
-
result: failed(module, "managed service state identity does not match the current module"),
|
|
327
|
-
observedEffects: [],
|
|
328
|
-
};
|
|
329
|
-
}
|
|
330
|
-
try {
|
|
331
|
-
process.kill(record.pid, "SIGTERM");
|
|
332
|
-
if (!(await waitForExit(record.pid))) {
|
|
333
|
-
return {
|
|
334
|
-
result: failed(module, `service process ${record.pid} did not stop after SIGTERM`),
|
|
335
|
-
observedEffects: [],
|
|
336
|
-
};
|
|
337
|
-
}
|
|
338
|
-
await removeRecord(paths, module.moduleRef);
|
|
339
|
-
return {
|
|
340
|
-
result: { ...base(module), data: { state: "STOPPED", pid: record.pid } },
|
|
341
|
-
observedEffects: ["Manage the declared service process"],
|
|
342
|
-
};
|
|
343
|
-
}
|
|
344
|
-
catch (error) {
|
|
345
|
-
return {
|
|
346
|
-
result: failed(module, error instanceof Error ? error.message : String(error)),
|
|
347
|
-
observedEffects: [],
|
|
348
|
-
};
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
export async function restartManagedService(paths, module, binding) {
|
|
352
|
-
const stopped = await stopManagedService(paths, module);
|
|
353
|
-
if (stopped.result.status === "FAILED")
|
|
354
|
-
return stopped;
|
|
355
|
-
return startManagedService(paths, module, binding);
|
|
356
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { buildManifest, MANIFEST_CONTRACT } from "./manifest.js";
|
|
@@ -1,61 +0,0 @@
|
|
|
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>;
|
|
@@ -1,167 +0,0 @@
|
|
|
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 currentVerifiedModuleRefs = new Set();
|
|
42
|
-
const materializedConfig = {};
|
|
43
|
-
for (const module of modules) {
|
|
44
|
-
// materialized config current reality (never caller-supplied intent)
|
|
45
|
-
const persisted = await loadConfig(deps.paths, module.moduleRef);
|
|
46
|
-
if (persisted !== undefined) {
|
|
47
|
-
materializedConfig[module.moduleRef] = {
|
|
48
|
-
...persisted.publicValues,
|
|
49
|
-
...persisted.secretValues,
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
// live runtime status — current reality, not persisted state
|
|
53
|
-
const run = statusByRef.get(module.moduleRef);
|
|
54
|
-
let runtimeObserved = false;
|
|
55
|
-
let runtimeStatus;
|
|
56
|
-
let runtimeResourceVersion;
|
|
57
|
-
if (run !== undefined &&
|
|
58
|
-
run.status === "EXECUTED" &&
|
|
59
|
-
run.result !== undefined) {
|
|
60
|
-
runtimeObserved = true;
|
|
61
|
-
runtimeStatus = run.result.status;
|
|
62
|
-
if (run.result.resourceVersion !== undefined) {
|
|
63
|
-
runtimeResourceVersion = run.result.resourceVersion;
|
|
64
|
-
}
|
|
65
|
-
if (run.result.status === "ACTION_REQUIRED" &&
|
|
66
|
-
run.result.actionRequired !== undefined) {
|
|
67
|
-
const action = {
|
|
68
|
-
moduleRef: module.moduleRef,
|
|
69
|
-
action: run.result.actionRequired.action,
|
|
70
|
-
description: run.result.actionRequired.description,
|
|
71
|
-
};
|
|
72
|
-
pendingActions.push(action);
|
|
73
|
-
blockingActions.push(action);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
// external resource identity/version
|
|
77
|
-
let resourceIdentity;
|
|
78
|
-
if (module.kind === "external-resource") {
|
|
79
|
-
const secretRefs = module.configSlots
|
|
80
|
-
.filter((slot) => slot.type === "secretRef")
|
|
81
|
-
.map((slot) => slot.key);
|
|
82
|
-
resourceIdentity = configFingerprint(persisted?.publicValues ?? {}, secretRefs);
|
|
83
|
-
resources.push({
|
|
84
|
-
moduleRef: module.moduleRef,
|
|
85
|
-
...(resourceIdentity !== undefined ? { resourceIdentity } : {}),
|
|
86
|
-
...(runtimeResourceVersion !== undefined
|
|
87
|
-
? { resourceVersion: runtimeResourceVersion }
|
|
88
|
-
: {}),
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
manifestModules.push({
|
|
92
|
-
moduleRef: module.moduleRef,
|
|
93
|
-
packageName: module.packageName,
|
|
94
|
-
moduleVersion: module.moduleVersion,
|
|
95
|
-
kind: module.kind,
|
|
96
|
-
source: module.source,
|
|
97
|
-
provides: module.provides,
|
|
98
|
-
requires: module.requires,
|
|
99
|
-
runtimeObserved,
|
|
100
|
-
...(runtimeStatus !== undefined ? { runtimeStatus } : {}),
|
|
101
|
-
...(runtimeResourceVersion !== undefined
|
|
102
|
-
? { runtimeResourceVersion }
|
|
103
|
-
: {}),
|
|
104
|
-
...(resourceIdentity !== undefined ? { resourceIdentity } : {}),
|
|
105
|
-
});
|
|
106
|
-
// verification history summary
|
|
107
|
-
const history = await loadVerificationHistory(deps.paths, module.moduleRef);
|
|
108
|
-
allRecords.push(...history);
|
|
109
|
-
const latest = history[history.length - 1];
|
|
110
|
-
if (latest?.result === "PASS" &&
|
|
111
|
-
latest.moduleVersion === module.moduleVersion) {
|
|
112
|
-
currentVerifiedModuleRefs.add(module.moduleRef);
|
|
113
|
-
}
|
|
114
|
-
const lastPassAt = lastVerifiedAt(history, "PASS");
|
|
115
|
-
const lastFailAt = lastVerifiedAt(history, "FAIL");
|
|
116
|
-
verification.push({
|
|
117
|
-
moduleRef: module.moduleRef,
|
|
118
|
-
historyCount: history.length,
|
|
119
|
-
...(latest !== undefined ? { latest } : {}),
|
|
120
|
-
...(lastPassAt !== undefined ? { lastPassAt } : {}),
|
|
121
|
-
...(lastFailAt !== undefined ? { lastFailAt } : {}),
|
|
122
|
-
});
|
|
123
|
-
// config readiness, secret-redacted, from materialized current reality
|
|
124
|
-
const resolved = resolveModuleConfig(module, materializedConfig[module.moduleRef]);
|
|
125
|
-
configEntries.push({
|
|
126
|
-
moduleRef: module.moduleRef,
|
|
127
|
-
values: redactSecretValues(resolved.values, resolved.secretRefs),
|
|
128
|
-
secretRefs: [...resolved.secretRefs],
|
|
129
|
-
missing: resolved.missing,
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
// persisted pending ACTION_REQUIRED blocks readiness unless its plan is COMPLETE
|
|
133
|
-
const deploymentState = await loadDeploymentState(deps.paths);
|
|
134
|
-
if (deploymentState !== undefined) {
|
|
135
|
-
for (const pending of clearCompletedPendingActions(deploymentState)
|
|
136
|
-
.pendingActions) {
|
|
137
|
-
if (currentVerifiedModuleRefs.has(pending.moduleRef))
|
|
138
|
-
continue;
|
|
139
|
-
const action = {
|
|
140
|
-
moduleRef: pending.moduleRef,
|
|
141
|
-
action: pending.action,
|
|
142
|
-
...(pending.description !== undefined
|
|
143
|
-
? { description: pending.description }
|
|
144
|
-
: {}),
|
|
145
|
-
};
|
|
146
|
-
blockingActions.push(action);
|
|
147
|
-
pendingActions.push(action);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
const { state } = assessPlatformReady({
|
|
151
|
-
modules,
|
|
152
|
-
status: statusResults,
|
|
153
|
-
verification: allRecords,
|
|
154
|
-
config: materializedConfig,
|
|
155
|
-
blockingActions,
|
|
156
|
-
resources,
|
|
157
|
-
});
|
|
158
|
-
return {
|
|
159
|
-
contract: MANIFEST_CONTRACT,
|
|
160
|
-
observedAt,
|
|
161
|
-
status: state,
|
|
162
|
-
modules: manifestModules,
|
|
163
|
-
verification,
|
|
164
|
-
config: configEntries,
|
|
165
|
-
pendingActions,
|
|
166
|
-
};
|
|
167
|
-
}
|
|
@@ -1,31 +0,0 @@
|
|
|
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>;
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { join } from "node:path";
|
|
2
|
-
import { atomicWrite } from "../paths.js";
|
|
3
|
-
function stateOf(statuses) {
|
|
4
|
-
if (statuses.includes("FAILED") || statuses.includes("BLOCKED"))
|
|
5
|
-
return "NOT_READY";
|
|
6
|
-
if (statuses.includes("ACTION_REQUIRED"))
|
|
7
|
-
return "ACTION_REQUIRED";
|
|
8
|
-
return statuses.length === 0 ? "NOT_READY" : "READY";
|
|
9
|
-
}
|
|
10
|
-
export const DEPLOYMENT_OBSERVER_SUMMARY_TTL_MS = 60_000;
|
|
11
|
-
export async function writeDeploymentObserverSummary(input) {
|
|
12
|
-
const modules = [...input.modules].sort((a, b) => a.moduleRef.localeCompare(b.moduleRef));
|
|
13
|
-
const observedAt = new Date();
|
|
14
|
-
const isCompletePlatformObservation = input.selectedModuleCount === input.totalModuleCount &&
|
|
15
|
-
modules.length === input.totalModuleCount;
|
|
16
|
-
const summary = {
|
|
17
|
-
contract: "proflow.deployment-observer-summary.v1",
|
|
18
|
-
scope: "PLATFORM",
|
|
19
|
-
source: input.source,
|
|
20
|
-
state: isCompletePlatformObservation
|
|
21
|
-
? stateOf(modules.map((item) => item.status))
|
|
22
|
-
: "NOT_READY",
|
|
23
|
-
selectedModuleCount: input.selectedModuleCount,
|
|
24
|
-
totalModuleCount: input.totalModuleCount,
|
|
25
|
-
observedModuleCount: modules.length,
|
|
26
|
-
blockingModuleCount: modules.filter((item) => item.status !== "SUCCEEDED")
|
|
27
|
-
.length,
|
|
28
|
-
modules,
|
|
29
|
-
observedAt: observedAt.toISOString(),
|
|
30
|
-
freshUntil: new Date(observedAt.getTime() + DEPLOYMENT_OBSERVER_SUMMARY_TTL_MS).toISOString(),
|
|
31
|
-
};
|
|
32
|
-
await atomicWrite(join(input.paths.deployment, "observer-summary.json"), `${JSON.stringify(summary, null, 2)}\n`);
|
|
33
|
-
return summary;
|
|
34
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { randomBytes } from "node:crypto";
|
|
2
|
-
import { chmod, mkdir, rename, writeFile } from "node:fs/promises";
|
|
3
|
-
import { dirname } from "node:path";
|
|
4
|
-
// Mode-aware atomic write: bounded write to a temp sibling, then rename into
|
|
5
|
-
// place. This is an enhancement of paths.atomicWrite that pins an explicit file
|
|
6
|
-
// mode (e.g. 0o600 for secret-bearing materialization) independent of umask.
|
|
7
|
-
export async function writeFileAtomic(file, content, mode = 0o644) {
|
|
8
|
-
await mkdir(dirname(file), { recursive: true });
|
|
9
|
-
const tmp = `${file}.tmp-${randomBytes(6).toString("hex")}`;
|
|
10
|
-
await writeFile(tmp, content, { encoding: "utf8", mode });
|
|
11
|
-
await chmod(tmp, mode);
|
|
12
|
-
await rename(tmp, file);
|
|
13
|
-
}
|
|
14
|
-
export async function writeJsonAtomic(file, value, mode = 0o644) {
|
|
15
|
-
await writeFileAtomic(file, `${JSON.stringify(value, null, 2)}\n`, mode);
|
|
16
|
-
}
|
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
import type { ResolvedModule } from "../contracts.ts";
|
|
2
|
-
import type { WorkspacePaths } from "../paths.ts";
|
|
3
|
-
export declare function mergeEffectiveConfig(paths: WorkspacePaths, modules: readonly ResolvedModule[], provided: Record<string, Record<string, string>>): Promise<Record<string, Record<string, string>>>;
|