@tomflow/proflow-platform-cli 0.1.1 → 0.1.3
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/CHANGELOG.md +12 -0
- package/README.md +24 -3
- package/dist/deployment/adapter.d.ts +5 -5
- package/dist/deployment/descriptor.d.ts +1 -1
- package/dist/deployment/descriptor.js +1 -1
- package/dist/src/apply/apply.d.ts +2 -0
- package/dist/src/apply/apply.js +47 -4
- package/dist/src/apply/current.js +14 -0
- package/dist/src/apply/driver.d.ts +1 -0
- package/dist/src/apply/driver.js +40 -12
- package/dist/src/apply/execute.js +28 -0
- package/dist/src/binding/global-binding.d.ts +69 -0
- package/dist/src/binding/global-binding.js +349 -0
- package/dist/src/binding/production-bindings.d.ts +2 -12
- package/dist/src/binding/production-bindings.js +18 -4
- package/dist/src/cli.d.ts +13 -1
- package/dist/src/cli.js +661 -40
- package/dist/src/errors.d.ts +1 -1
- package/dist/src/errors.js +13 -0
- package/dist/src/install/environment.d.ts +2 -1
- package/dist/src/install/environment.js +34 -30
- package/dist/src/install/install.js +41 -0
- package/dist/src/install/package-manager.d.ts +4 -2
- package/dist/src/install/package-manager.js +73 -17
- package/dist/src/lifecycle/dispatch.js +52 -1
- package/dist/src/lifecycle/service-process.js +32 -2
- package/dist/src/persistence/guards.js +1 -0
- package/dist/src/planner/check.js +12 -1
- package/dist/src/planner/plan.d.ts +1 -0
- package/dist/src/planner/plan.js +14 -26
- package/dist/src/preflight/config.d.ts +1 -0
- package/dist/src/preflight/config.js +43 -0
- package/dist/src/preflight/preflight.d.ts +2 -0
- package/dist/src/preflight/preflight.js +18 -2
- package/dist/src/preflight/requirements.d.ts +2 -2
- package/dist/src/preflight/requirements.js +16 -9
- package/dist/src/registry/npm-registry.d.ts +1 -1
- package/dist/src/registry/npm-registry.js +2 -1
- package/dist/src/security/lock.js +40 -2
- package/package.json +4 -4
- package/proflow.module.json +1 -1
package/dist/src/errors.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const platformErrorCodes: readonly ["INVALID_REQUEST", "DESCRIPTOR_INVALID", "DUPLICATE_IDENTITY", "DEPENDENCY_UNRESOLVED", "DEPENDENCY_INCOMPATIBLE", "DEPENDENCY_CYCLE", "CONFIG_MISSING", "SECRET_REF_INVALID", "PLAN_STALE", "PLAN_NOT_FOUND", "PLAN_INVALID", "WORKSPACE_LOCKED", "LIFECYCLE_UNSUPPORTED", "SECRET_LEAK", "APPLY_FAILED", "UPGRADE_FAILED", "CORE_PACKAGE_REQUIRED", "UNINSTALL_FAILED", "VERIFY_FAILED", "REGISTRY_UNAVAILABLE", "REGISTRY_AUTH_REQUIRED", "REGISTRY_RESPONSE_INVALID", "PACKAGE_NOT_FOUND", "PACKAGE_NOT_PROFLOW", "COMMAND_FAILED"];
|
|
1
|
+
export declare const platformErrorCodes: readonly ["INVALID_REQUEST", "DESCRIPTOR_INVALID", "DUPLICATE_IDENTITY", "DEPENDENCY_UNRESOLVED", "DEPENDENCY_INCOMPATIBLE", "DEPENDENCY_CYCLE", "CONFIG_MISSING", "CONFIG_INVALID", "SECRET_REF_INVALID", "PLAN_STALE", "PLAN_NOT_FOUND", "PLAN_INVALID", "WORKSPACE_LOCKED", "WORKSPACE_NOT_FOUND", "WORKSPACE_NOT_WRITABLE", "WORKSPACE_NOT_BOUND", "WORKSPACE_ALREADY_BOUND", "BOUND_WORKSPACE_MISSING", "WORKSPACE_INSTANCE_INVALID", "GLOBAL_BINDING_INVALID", "GLOBAL_BINDING_LOCKED", "GLOBAL_OPERATION_LOCKED", "PACKAGE_MANAGER_UNSUPPORTED", "PACKAGE_MANAGER_CONFLICT", "PACKAGE_MANAGER_UNAVAILABLE", "LIFECYCLE_UNSUPPORTED", "SECRET_LEAK", "APPLY_FAILED", "UPGRADE_FAILED", "CORE_PACKAGE_REQUIRED", "UNINSTALL_FAILED", "VERIFY_FAILED", "REGISTRY_UNAVAILABLE", "REGISTRY_AUTH_REQUIRED", "REGISTRY_RESPONSE_INVALID", "PACKAGE_NOT_FOUND", "PACKAGE_NOT_PROFLOW", "COMMAND_FAILED"];
|
|
2
2
|
export type PlatformErrorCode = (typeof platformErrorCodes)[number];
|
|
3
3
|
export declare class PlatformError extends Error {
|
|
4
4
|
readonly code: PlatformErrorCode;
|
package/dist/src/errors.js
CHANGED
|
@@ -6,11 +6,24 @@ export const platformErrorCodes = [
|
|
|
6
6
|
"DEPENDENCY_INCOMPATIBLE",
|
|
7
7
|
"DEPENDENCY_CYCLE",
|
|
8
8
|
"CONFIG_MISSING",
|
|
9
|
+
"CONFIG_INVALID",
|
|
9
10
|
"SECRET_REF_INVALID",
|
|
10
11
|
"PLAN_STALE",
|
|
11
12
|
"PLAN_NOT_FOUND",
|
|
12
13
|
"PLAN_INVALID",
|
|
13
14
|
"WORKSPACE_LOCKED",
|
|
15
|
+
"WORKSPACE_NOT_FOUND",
|
|
16
|
+
"WORKSPACE_NOT_WRITABLE",
|
|
17
|
+
"WORKSPACE_NOT_BOUND",
|
|
18
|
+
"WORKSPACE_ALREADY_BOUND",
|
|
19
|
+
"BOUND_WORKSPACE_MISSING",
|
|
20
|
+
"WORKSPACE_INSTANCE_INVALID",
|
|
21
|
+
"GLOBAL_BINDING_INVALID",
|
|
22
|
+
"GLOBAL_BINDING_LOCKED",
|
|
23
|
+
"GLOBAL_OPERATION_LOCKED",
|
|
24
|
+
"PACKAGE_MANAGER_UNSUPPORTED",
|
|
25
|
+
"PACKAGE_MANAGER_CONFLICT",
|
|
26
|
+
"PACKAGE_MANAGER_UNAVAILABLE",
|
|
14
27
|
"LIFECYCLE_UNSUPPORTED",
|
|
15
28
|
"SECRET_LEAK",
|
|
16
29
|
"APPLY_FAILED",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type NpmCommandRunner } from "../registry/index.ts";
|
|
2
|
-
import { type SupportedWorkspacePackageManager } from "./package-manager.ts";
|
|
2
|
+
import { type PackageManagerSelectionSource, type SupportedWorkspacePackageManager } from "./package-manager.ts";
|
|
3
3
|
export declare const PLATFORM_INSTALL_NODE_RANGE = ">=24.19.0";
|
|
4
4
|
export type InstallerPreflightStatus = "READY" | "ACTION_REQUIRED" | "BLOCKED";
|
|
5
5
|
export type InstallerFindingSeverity = "info" | "action" | "error";
|
|
@@ -17,6 +17,7 @@ export interface InstallerEnvironmentPreflightResult {
|
|
|
17
17
|
npmVersion?: string;
|
|
18
18
|
packageManager?: {
|
|
19
19
|
name: SupportedWorkspacePackageManager;
|
|
20
|
+
source: PackageManagerSelectionSource;
|
|
20
21
|
declared?: string;
|
|
21
22
|
version?: string;
|
|
22
23
|
};
|
|
@@ -48,57 +48,61 @@ export async function preflightInstallerEnvironment(options) {
|
|
|
48
48
|
findings.push(platformErrorFinding(error, "NPM_UNAVAILABLE"));
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
-
|
|
51
|
+
let packageManager;
|
|
52
52
|
let packageManagerVersion;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
severity: "action",
|
|
57
|
-
message: `unsupported packageManager declaration: ${workspace.packageManager ?? "<unknown>"}`,
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
else if (packageManager.name === "pnpm") {
|
|
61
|
-
if (!findExecutable("pnpm")) {
|
|
53
|
+
try {
|
|
54
|
+
packageManager = await readWorkspacePackageManagerSelection(options.workspaceRoot);
|
|
55
|
+
if (!findExecutable(packageManager.name)) {
|
|
62
56
|
findings.push({
|
|
63
|
-
code: "
|
|
57
|
+
code: "PACKAGE_MANAGER_UNAVAILABLE",
|
|
64
58
|
severity: "action",
|
|
65
|
-
message: `workspace
|
|
59
|
+
message: `workspace selects ${packageManager.name}, but that executable is not available on PATH`,
|
|
66
60
|
});
|
|
67
61
|
}
|
|
62
|
+
else if (packageManager.name === "npm" && npmVersion !== undefined) {
|
|
63
|
+
packageManagerVersion = npmVersion;
|
|
64
|
+
}
|
|
68
65
|
else {
|
|
69
66
|
try {
|
|
70
|
-
|
|
71
|
-
packageManagerVersion = result;
|
|
72
|
-
findings.push({
|
|
73
|
-
code: "PNPM_READY",
|
|
74
|
-
severity: "info",
|
|
75
|
-
message: `pnpm ${result} is available for this workspace`,
|
|
76
|
-
});
|
|
67
|
+
packageManagerVersion = (await systemPackageCommandRunner().run(packageManager.name, ["--version"], options.workspaceRoot)).trim();
|
|
77
68
|
}
|
|
78
69
|
catch (error) {
|
|
79
70
|
findings.push({
|
|
80
|
-
code: "
|
|
71
|
+
code: "PACKAGE_MANAGER_UNAVAILABLE",
|
|
81
72
|
severity: "action",
|
|
82
73
|
message: error instanceof Error ? error.message : String(error),
|
|
83
74
|
});
|
|
84
75
|
}
|
|
85
76
|
}
|
|
77
|
+
if (packageManagerVersion !== undefined) {
|
|
78
|
+
findings.push({
|
|
79
|
+
code: "PACKAGE_MANAGER_READY",
|
|
80
|
+
severity: "info",
|
|
81
|
+
message: `${packageManager.name} ${packageManagerVersion} is selected via ${packageManager.source}`,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
86
84
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
85
|
+
catch (error) {
|
|
86
|
+
if (error instanceof PlatformError) {
|
|
87
|
+
findings.push({
|
|
88
|
+
code: error.code,
|
|
89
|
+
severity: error.code === "PACKAGE_MANAGER_CONFLICT" ? "error" : "action",
|
|
90
|
+
message: error.message,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
findings.push({
|
|
95
|
+
code: "PACKAGE_MANAGER_UNAVAILABLE",
|
|
96
|
+
severity: "action",
|
|
97
|
+
message: error instanceof Error ? error.message : String(error),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
96
100
|
}
|
|
97
101
|
let registry;
|
|
98
102
|
if (npmVersion !== undefined) {
|
|
99
103
|
try {
|
|
100
104
|
registry = await resolveScopeRegistry(options.workspaceRoot, runner);
|
|
101
|
-
await runner.run(["ping", "--json", "--prefer-online", `--registry=${registry}`], options.workspaceRoot);
|
|
105
|
+
await runner.run(["ping", "--json", "--prefer-online", `--registry=${registry}`], options.workspaceRoot, 10_000);
|
|
102
106
|
findings.push({
|
|
103
107
|
code: "REGISTRY_READY",
|
|
104
108
|
severity: "info",
|
|
@@ -111,6 +111,47 @@ export function renderInstallDoc(input) {
|
|
|
111
111
|
}
|
|
112
112
|
lines.push("");
|
|
113
113
|
}
|
|
114
|
+
const missingRequiredConfig = {};
|
|
115
|
+
for (const module of modules) {
|
|
116
|
+
const resolved = resolveModuleConfig(module, input.config?.[module.moduleRef]);
|
|
117
|
+
for (const slot of module.configSlots) {
|
|
118
|
+
if (!slot.required || resolved.values[slot.key] !== undefined)
|
|
119
|
+
continue;
|
|
120
|
+
const placeholder = slot.type === "secretRef"
|
|
121
|
+
? "secret://provider/name"
|
|
122
|
+
: slot.type === "url"
|
|
123
|
+
? "https://example.invalid"
|
|
124
|
+
: slot.type === "path"
|
|
125
|
+
? "/absolute/path"
|
|
126
|
+
: slot.type === "moduleRef"
|
|
127
|
+
? "module-ref"
|
|
128
|
+
: "<value>";
|
|
129
|
+
const moduleConfig = missingRequiredConfig[module.moduleRef] ?? {};
|
|
130
|
+
moduleConfig[slot.key] = placeholder;
|
|
131
|
+
missingRequiredConfig[module.moduleRef] = moduleConfig;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
lines.push("## Configure Before Start");
|
|
135
|
+
lines.push("");
|
|
136
|
+
if (Object.keys(missingRequiredConfig).length === 0) {
|
|
137
|
+
lines.push("No required configuration is currently missing. Re-run `platform preflight --intent start` before starting to confirm current runtime readiness.");
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
lines.push("Create a JSON config file (for example `./proflow-config.json`) using the required missing slots below. Replace placeholders with real environment-specific values. `secretRef` values must remain opaque references such as `secret://provider/name`; do not place raw secrets in this file.");
|
|
141
|
+
lines.push("");
|
|
142
|
+
lines.push("```json");
|
|
143
|
+
lines.push(JSON.stringify({ modules: missingRequiredConfig }, null, 2));
|
|
144
|
+
lines.push("```");
|
|
145
|
+
lines.push("");
|
|
146
|
+
lines.push("Apply and re-check the configuration:");
|
|
147
|
+
lines.push("");
|
|
148
|
+
lines.push("1. `platform plan --intent configure --config ./proflow-config.json`");
|
|
149
|
+
lines.push("2. Copy the returned `planRef`.");
|
|
150
|
+
lines.push("3. `platform apply <planRef>`");
|
|
151
|
+
lines.push("4. `platform preflight --intent start`");
|
|
152
|
+
lines.push("5. Resolve any remaining human or external-resource actions reported by preflight, then run the same preflight command again until the Platform reports the expected readiness state.");
|
|
153
|
+
}
|
|
154
|
+
lines.push("");
|
|
114
155
|
lines.push("## Verification Plan");
|
|
115
156
|
lines.push("");
|
|
116
157
|
for (const module of modules) {
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
export type SupportedWorkspacePackageManager = "npm" | "pnpm";
|
|
1
|
+
export type SupportedWorkspacePackageManager = "npm" | "yarn" | "pnpm";
|
|
2
|
+
export type PackageManagerSelectionSource = "declared" | "lockfile" | "bootstrap-default";
|
|
2
3
|
export interface WorkspacePackageManagerSelection {
|
|
3
4
|
name: SupportedWorkspacePackageManager;
|
|
5
|
+
source: PackageManagerSelectionSource;
|
|
4
6
|
declared?: string;
|
|
5
7
|
}
|
|
6
8
|
export interface PackageCommandRunner {
|
|
7
9
|
run(command: string, args: readonly string[], cwd: string): Promise<string>;
|
|
8
10
|
}
|
|
9
11
|
export declare function systemPackageCommandRunner(): PackageCommandRunner;
|
|
10
|
-
export declare function readWorkspacePackageManagerSelection(workspaceRoot: string): Promise<WorkspacePackageManagerSelection
|
|
12
|
+
export declare function readWorkspacePackageManagerSelection(workspaceRoot: string): Promise<WorkspacePackageManagerSelection>;
|
|
11
13
|
export declare function findExecutable(command: string): boolean;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { accessSync, constants } from "node:fs";
|
|
3
|
-
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { access, readFile } from "node:fs/promises";
|
|
4
4
|
import { delimiter, join } from "node:path";
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
|
+
import { PlatformError } from "../errors.js";
|
|
6
7
|
const execFileAsync = promisify(execFile);
|
|
7
8
|
export function systemPackageCommandRunner() {
|
|
8
9
|
return {
|
|
@@ -17,25 +18,27 @@ export function systemPackageCommandRunner() {
|
|
|
17
18
|
};
|
|
18
19
|
}
|
|
19
20
|
export async function readWorkspacePackageManagerSelection(workspaceRoot) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
if (
|
|
25
|
-
|
|
21
|
+
const declared = await readDeclaredPackageManager(workspaceRoot);
|
|
22
|
+
const lockManagers = await detectLockfileManagers(workspaceRoot);
|
|
23
|
+
if (declared !== undefined) {
|
|
24
|
+
const name = packageManagerName(declared);
|
|
25
|
+
if (name === undefined) {
|
|
26
|
+
throw new PlatformError("PACKAGE_MANAGER_UNSUPPORTED", `unsupported packageManager declaration: ${declared}; expected npm, yarn, or pnpm`);
|
|
27
|
+
}
|
|
28
|
+
const conflicts = [...lockManagers].filter((manager) => manager !== name);
|
|
29
|
+
if (conflicts.length > 0) {
|
|
30
|
+
throw new PlatformError("PACKAGE_MANAGER_CONFLICT", `packageManager declares ${declared}, but conflicting lockfiles indicate ${conflicts.join(", ")}`);
|
|
26
31
|
}
|
|
32
|
+
return { name, source: "declared", declared };
|
|
27
33
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
// package.json is rejected earlier by installer preflight.
|
|
34
|
+
if (lockManagers.size > 1) {
|
|
35
|
+
throw new PlatformError("PACKAGE_MANAGER_CONFLICT", `multiple package-manager lockfiles are present: ${[...lockManagers].sort().join(", ")}`);
|
|
31
36
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
return undefined;
|
|
38
|
-
return { name, declared };
|
|
37
|
+
const [fromLockfile] = lockManagers;
|
|
38
|
+
if (fromLockfile !== undefined) {
|
|
39
|
+
return { name: fromLockfile, source: "lockfile" };
|
|
40
|
+
}
|
|
41
|
+
return { name: "npm", source: "bootstrap-default" };
|
|
39
42
|
}
|
|
40
43
|
export function findExecutable(command) {
|
|
41
44
|
const path = process.env.PATH ?? "";
|
|
@@ -53,6 +56,59 @@ export function findExecutable(command) {
|
|
|
53
56
|
}
|
|
54
57
|
return false;
|
|
55
58
|
}
|
|
59
|
+
async function readDeclaredPackageManager(workspaceRoot) {
|
|
60
|
+
try {
|
|
61
|
+
const raw = await readFile(join(workspaceRoot, "package.json"), "utf8");
|
|
62
|
+
const parsed = JSON.parse(raw);
|
|
63
|
+
if (!isRecord(parsed)) {
|
|
64
|
+
throw new PlatformError("PACKAGE_MANAGER_CONFLICT", "workspace package.json root must be an object");
|
|
65
|
+
}
|
|
66
|
+
return typeof parsed.packageManager === "string"
|
|
67
|
+
? parsed.packageManager
|
|
68
|
+
: undefined;
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (isMissingFile(error))
|
|
72
|
+
return undefined;
|
|
73
|
+
if (error instanceof PlatformError)
|
|
74
|
+
throw error;
|
|
75
|
+
throw new PlatformError("PACKAGE_MANAGER_CONFLICT", `cannot inspect workspace package.json: ${errorMessage(error)}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function detectLockfileManagers(workspaceRoot) {
|
|
79
|
+
const managers = new Set();
|
|
80
|
+
if ((await pathExists(join(workspaceRoot, "package-lock.json"))) ||
|
|
81
|
+
(await pathExists(join(workspaceRoot, "npm-shrinkwrap.json")))) {
|
|
82
|
+
managers.add("npm");
|
|
83
|
+
}
|
|
84
|
+
if (await pathExists(join(workspaceRoot, "yarn.lock")))
|
|
85
|
+
managers.add("yarn");
|
|
86
|
+
if (await pathExists(join(workspaceRoot, "pnpm-lock.yaml")))
|
|
87
|
+
managers.add("pnpm");
|
|
88
|
+
return managers;
|
|
89
|
+
}
|
|
90
|
+
function packageManagerName(declared) {
|
|
91
|
+
const separator = declared.lastIndexOf("@");
|
|
92
|
+
const name = separator > 0 ? declared.slice(0, separator) : declared;
|
|
93
|
+
return name === "npm" || name === "yarn" || name === "pnpm"
|
|
94
|
+
? name
|
|
95
|
+
: undefined;
|
|
96
|
+
}
|
|
97
|
+
async function pathExists(path) {
|
|
98
|
+
try {
|
|
99
|
+
await access(path);
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
56
106
|
function isRecord(value) {
|
|
57
107
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
58
108
|
}
|
|
109
|
+
function isMissingFile(error) {
|
|
110
|
+
return isRecord(error) && error.code === "ENOENT";
|
|
111
|
+
}
|
|
112
|
+
function errorMessage(error) {
|
|
113
|
+
return error instanceof Error ? error.message : String(error);
|
|
114
|
+
}
|
|
@@ -110,7 +110,58 @@ async function runInOrder(catalog, modules, order, primitive) {
|
|
|
110
110
|
* `start` primitive only to modules that declare it.
|
|
111
111
|
*/
|
|
112
112
|
export async function startModules(catalog, modules) {
|
|
113
|
-
|
|
113
|
+
const graph = buildDependencyGraph(modules);
|
|
114
|
+
const byRef = new Map(modules.map((module) => [module.moduleRef, module]));
|
|
115
|
+
const resultByRef = new Map();
|
|
116
|
+
const results = [];
|
|
117
|
+
for (const moduleRef of graph.order) {
|
|
118
|
+
const module = byRef.get(moduleRef);
|
|
119
|
+
if (module === undefined)
|
|
120
|
+
continue;
|
|
121
|
+
if (!module.lifecycle.includes("start")) {
|
|
122
|
+
const skipped = await runOne(catalog, module, "start");
|
|
123
|
+
results.push(skipped);
|
|
124
|
+
resultByRef.set(moduleRef, skipped);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
const blockingDependency = graph.edges
|
|
128
|
+
.filter((edge) => edge.from === moduleRef)
|
|
129
|
+
.map((edge) => edge.to)
|
|
130
|
+
.find((dependencyRef) => {
|
|
131
|
+
const dependencyModule = byRef.get(dependencyRef);
|
|
132
|
+
if (!dependencyModule?.lifecycle.includes("start"))
|
|
133
|
+
return false;
|
|
134
|
+
const dependencyResult = resultByRef.get(dependencyRef);
|
|
135
|
+
return dependencyResult?.result?.status !== "SUCCEEDED";
|
|
136
|
+
});
|
|
137
|
+
if (blockingDependency !== undefined) {
|
|
138
|
+
const blocked = {
|
|
139
|
+
moduleRef,
|
|
140
|
+
primitive: "start",
|
|
141
|
+
status: "EXECUTED",
|
|
142
|
+
result: {
|
|
143
|
+
contract: "deployment.result.v1",
|
|
144
|
+
ok: false,
|
|
145
|
+
status: "BLOCKED",
|
|
146
|
+
moduleRef: module.moduleRef,
|
|
147
|
+
moduleVersion: module.moduleVersion,
|
|
148
|
+
error: {
|
|
149
|
+
code: "COMMAND_FAILED",
|
|
150
|
+
message: `dependency ${blockingDependency} did not start successfully; ${moduleRef} was not started`,
|
|
151
|
+
retryable: true,
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
observedEffects: [],
|
|
155
|
+
};
|
|
156
|
+
results.push(blocked);
|
|
157
|
+
resultByRef.set(moduleRef, blocked);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
const result = await runOne(catalog, module, "start");
|
|
161
|
+
results.push(result);
|
|
162
|
+
resultByRef.set(moduleRef, result);
|
|
163
|
+
}
|
|
164
|
+
return results;
|
|
114
165
|
}
|
|
115
166
|
/**
|
|
116
167
|
* Stops a module set in reverse dependency topological order, dispatching the
|
|
@@ -195,6 +195,15 @@ export async function managedServiceStatus(paths, module) {
|
|
|
195
195
|
observedEffects: [],
|
|
196
196
|
};
|
|
197
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
|
+
}
|
|
198
207
|
export async function startManagedService(paths, module, rawBinding) {
|
|
199
208
|
const existing = await readRecord(paths, module.moduleRef);
|
|
200
209
|
if (existing && (await isOwnedProcess(existing))) {
|
|
@@ -243,13 +252,20 @@ export async function startManagedService(paths, module, rawBinding) {
|
|
|
243
252
|
result: failed(module, "service process spawned without pid"),
|
|
244
253
|
observedEffects: [],
|
|
245
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
|
+
}
|
|
246
262
|
child.unref();
|
|
247
263
|
const record = {
|
|
248
264
|
contract: "deployment.service-process-state.v1",
|
|
249
265
|
moduleRef: module.moduleRef,
|
|
250
266
|
packageName: module.packageName,
|
|
251
267
|
moduleVersion: module.moduleVersion,
|
|
252
|
-
pid
|
|
268
|
+
pid,
|
|
253
269
|
startedAt: new Date().toISOString(),
|
|
254
270
|
binPath,
|
|
255
271
|
configPath,
|
|
@@ -257,8 +273,22 @@ export async function startManagedService(paths, module, rawBinding) {
|
|
|
257
273
|
stderrPath,
|
|
258
274
|
};
|
|
259
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
|
+
}
|
|
260
290
|
return {
|
|
261
|
-
|
|
291
|
+
...observed,
|
|
262
292
|
observedEffects: ["Manage the declared service process"],
|
|
263
293
|
};
|
|
264
294
|
}
|
|
@@ -76,7 +76,18 @@ function checkConfig(step, plan, reality) {
|
|
|
76
76
|
return notSatisfied(`required config ${slot.key} is missing`);
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
|
-
|
|
79
|
+
// A configure step is satisfied only when current persisted reality matches
|
|
80
|
+
// the immutable target carried by this plan. Required-key presence alone is
|
|
81
|
+
// insufficient: a failed/partial prior apply can leave every key present but
|
|
82
|
+
// with values that the module-owned materializer rejected.
|
|
83
|
+
const target = plan.moduleTargets.find((entry) => entry.moduleRef === step.moduleRef)?.config;
|
|
84
|
+
for (const [key, expected] of Object.entries(target ?? {})) {
|
|
85
|
+
const observed = reality.configValues[key];
|
|
86
|
+
if (observed !== expected) {
|
|
87
|
+
return notSatisfied(`config ${key} for ${step.moduleRef} is ${observed === undefined ? "not observed" : "not at the planned target"}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return satisfied(`planned config materialized for ${step.moduleRef}`);
|
|
80
91
|
}
|
|
81
92
|
function checkVerify(step, plan, reality) {
|
|
82
93
|
const module = moduleOf(plan, step);
|
|
@@ -9,6 +9,7 @@ export interface PlanInput {
|
|
|
9
9
|
currentDescriptors?: readonly ModuleDescriptor[];
|
|
10
10
|
targetDescriptors?: readonly ModuleDescriptor[];
|
|
11
11
|
facts?: readonly RepairFact[];
|
|
12
|
+
uninstallScope?: "module" | "platform-instance";
|
|
12
13
|
now?: Date;
|
|
13
14
|
}
|
|
14
15
|
export declare function planDeployment(input: PlanInput): DeploymentPlan;
|
package/dist/src/planner/plan.js
CHANGED
|
@@ -52,14 +52,14 @@ function planInstallOrConfigure(input) {
|
|
|
52
52
|
const graph = buildDependencyGraph(modules);
|
|
53
53
|
const steps = input.intent === "install"
|
|
54
54
|
? installSteps(modules, graph)
|
|
55
|
-
: configureSteps(modules, graph);
|
|
55
|
+
: configureSteps(modules, graph, input.targets ?? [], input.config);
|
|
56
56
|
return assemblePlan({
|
|
57
57
|
intent: input.intent,
|
|
58
58
|
modules,
|
|
59
59
|
targets: input.targets ?? [],
|
|
60
60
|
config: input.config,
|
|
61
61
|
steps,
|
|
62
|
-
humanActions: humanActionsFromModules(modules),
|
|
62
|
+
humanActions: input.intent === "install" ? humanActionsFromModules(modules) : [],
|
|
63
63
|
now: input.now ?? new Date(),
|
|
64
64
|
});
|
|
65
65
|
}
|
|
@@ -90,35 +90,23 @@ function installSteps(modules, graph) {
|
|
|
90
90
|
}
|
|
91
91
|
return steps;
|
|
92
92
|
}
|
|
93
|
-
function configureSteps(modules, graph) {
|
|
93
|
+
function configureSteps(modules, graph, targets, config) {
|
|
94
94
|
const seq = createSequencer();
|
|
95
95
|
const byRef = new Map(modules.map((module) => [module.moduleRef, module]));
|
|
96
|
+
const targetByRef = new Map(targets.map((target) => [target.moduleRef, target]));
|
|
96
97
|
const steps = [];
|
|
97
98
|
for (const ref of graph.order) {
|
|
98
99
|
const module = byRef.get(ref);
|
|
99
|
-
if (module === undefined)
|
|
100
|
+
if (module === undefined || module.configSlots.length === 0)
|
|
100
101
|
continue;
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if (module.kind === "service") {
|
|
110
|
-
if (module.lifecycle.includes("restart")) {
|
|
111
|
-
steps.push(lifecycleStep(seq, module, "restart"));
|
|
112
|
-
}
|
|
113
|
-
else if (module.lifecycle.includes("start")) {
|
|
114
|
-
steps.push(lifecycleStep(seq, module, "start"));
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
for (const requirement of module.requirements) {
|
|
118
|
-
if (requirement.kind === "human") {
|
|
119
|
-
steps.push(humanStep(seq, module, requirement.action));
|
|
120
|
-
}
|
|
121
|
-
}
|
|
102
|
+
const explicitConfig = targetByRef.get(ref)?.config;
|
|
103
|
+
const providedConfig = config?.[ref];
|
|
104
|
+
const hasMaterializationTarget = (explicitConfig !== undefined &&
|
|
105
|
+
Object.keys(explicitConfig).length > 0) ||
|
|
106
|
+
(providedConfig !== undefined && Object.keys(providedConfig).length > 0);
|
|
107
|
+
if (!hasMaterializationTarget)
|
|
108
|
+
continue;
|
|
109
|
+
steps.push(configStep(seq, module));
|
|
122
110
|
}
|
|
123
111
|
return steps;
|
|
124
112
|
}
|
|
@@ -128,7 +116,7 @@ function planUninstall(input) {
|
|
|
128
116
|
throw new PlatformError("INVALID_REQUEST", "uninstall requires modules");
|
|
129
117
|
}
|
|
130
118
|
const core = modules.find((module) => module.installClass === "core");
|
|
131
|
-
if (core !== undefined) {
|
|
119
|
+
if (core !== undefined && input.uninstallScope !== "platform-instance") {
|
|
132
120
|
throw new PlatformError("CORE_PACKAGE_REQUIRED", `core module ${core.moduleRef} cannot be individually uninstalled`);
|
|
133
121
|
}
|
|
134
122
|
const graph = buildDependencyGraph(modules);
|
|
@@ -8,3 +8,4 @@ export interface ResolvedModuleConfig {
|
|
|
8
8
|
}
|
|
9
9
|
export declare function resolveModuleConfig(module: ResolvedModule, provided: Record<string, string> | undefined): ResolvedModuleConfig;
|
|
10
10
|
export declare function checkConfigReadiness(modules: readonly ResolvedModule[], config: Record<string, Record<string, string>> | undefined): PreflightFinding[];
|
|
11
|
+
export declare function checkConfigReality(modules: readonly ResolvedModule[], config: Record<string, Record<string, string>> | undefined): Promise<PreflightFinding[]>;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { access, readFile, stat } from "node:fs/promises";
|
|
1
3
|
import { PlatformError } from "../errors.js";
|
|
2
4
|
import { isValidSecretRef } from "../security/redact.js";
|
|
3
5
|
function compareRef(a, b) {
|
|
@@ -43,3 +45,44 @@ export function checkConfigReadiness(modules, config) {
|
|
|
43
45
|
}
|
|
44
46
|
return findings;
|
|
45
47
|
}
|
|
48
|
+
function requiresExistingInputFile(key, sensitive) {
|
|
49
|
+
return (sensitive === true ||
|
|
50
|
+
/(?:ConfigPath|ProfilesFile|CredentialFile|TokenFile|tokenFile|credentialFile)$/.test(key));
|
|
51
|
+
}
|
|
52
|
+
function requiresJsonFile(key) {
|
|
53
|
+
return /(?:ConfigPath|ProfilesFile)$/.test(key);
|
|
54
|
+
}
|
|
55
|
+
export async function checkConfigReality(modules, config) {
|
|
56
|
+
const findings = [];
|
|
57
|
+
for (const module of [...modules].sort((a, b) => compareRef(a.moduleRef, b.moduleRef))) {
|
|
58
|
+
for (const slot of module.configSlots) {
|
|
59
|
+
if (slot.type !== "path")
|
|
60
|
+
continue;
|
|
61
|
+
const value = config?.[module.moduleRef]?.[slot.key] ??
|
|
62
|
+
(slot.default === undefined ? undefined : String(slot.default));
|
|
63
|
+
if (!value || !requiresExistingInputFile(slot.key, slot.sensitive))
|
|
64
|
+
continue;
|
|
65
|
+
try {
|
|
66
|
+
await access(value, constants.R_OK);
|
|
67
|
+
const info = await stat(value);
|
|
68
|
+
if (!info.isFile())
|
|
69
|
+
throw new Error("not a file");
|
|
70
|
+
const raw = await readFile(value, "utf8");
|
|
71
|
+
if (slot.sensitive === true && raw.trim().length < 32) {
|
|
72
|
+
throw new Error("credential/token file is shorter than 32 characters");
|
|
73
|
+
}
|
|
74
|
+
if (requiresJsonFile(slot.key))
|
|
75
|
+
JSON.parse(raw);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
findings.push({
|
|
79
|
+
code: "CONFIG_INVALID",
|
|
80
|
+
severity: "error",
|
|
81
|
+
moduleRef: module.moduleRef,
|
|
82
|
+
message: `config path "${slot.key}" for ${module.moduleRef} is not runtime-ready: ${error instanceof Error ? error.message : String(error)}`,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return findings;
|
|
88
|
+
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { ResolvedModule } from "../contracts.ts";
|
|
2
2
|
import type { ModuleCatalog } from "../modules.ts";
|
|
3
|
+
import type { WorkspacePaths } from "../paths.ts";
|
|
3
4
|
import type { PreflightResult } from "./findings.ts";
|
|
4
5
|
export interface PreflightOptions {
|
|
5
6
|
config?: Record<string, Record<string, string>>;
|
|
6
7
|
catalog?: ModuleCatalog;
|
|
8
|
+
paths?: WorkspacePaths;
|
|
7
9
|
}
|
|
8
10
|
export declare function runPreflight(modules: readonly ResolvedModule[], options?: PreflightOptions): Promise<PreflightResult>;
|