@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,34 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface WorkspacePaths {
|
|
2
|
+
root: string;
|
|
3
|
+
proflow: string;
|
|
4
|
+
config: string;
|
|
5
|
+
data: string;
|
|
6
|
+
logs: string;
|
|
7
|
+
logsDeployment: string;
|
|
8
|
+
runtime: string;
|
|
9
|
+
cache: string;
|
|
10
|
+
tmp: string;
|
|
11
|
+
deployment: string;
|
|
12
|
+
stateJson: string;
|
|
13
|
+
plans: string;
|
|
14
|
+
verification: string;
|
|
15
|
+
generated: string;
|
|
16
|
+
installMd: string;
|
|
17
|
+
}
|
|
18
|
+
export declare function workspacePaths(root: string): WorkspacePaths;
|
|
19
|
+
export declare function ensureLayout(paths: WorkspacePaths): Promise<void>;
|
|
20
|
+
export declare function atomicWrite(file: string, content: string): Promise<void>;
|
|
21
|
+
export declare function readJson<T>(file: string): Promise<T | undefined>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { mkdir, rename, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
export function workspacePaths(root) {
|
|
5
|
+
const proflow = join(root, ".proflow");
|
|
6
|
+
const deployment = join(proflow, "deployment");
|
|
7
|
+
return {
|
|
8
|
+
root,
|
|
9
|
+
proflow,
|
|
10
|
+
config: join(proflow, "config"),
|
|
11
|
+
data: join(proflow, "data"),
|
|
12
|
+
logs: join(proflow, "logs"),
|
|
13
|
+
logsDeployment: join(proflow, "logs", "deployment"),
|
|
14
|
+
runtime: join(proflow, "runtime"),
|
|
15
|
+
cache: join(proflow, "cache"),
|
|
16
|
+
tmp: join(proflow, "tmp"),
|
|
17
|
+
deployment,
|
|
18
|
+
stateJson: join(deployment, "state.json"),
|
|
19
|
+
plans: join(deployment, "plans"),
|
|
20
|
+
verification: join(deployment, "verification"),
|
|
21
|
+
generated: join(deployment, "generated"),
|
|
22
|
+
installMd: join(deployment, "generated", "INSTALL.md"),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export async function ensureLayout(paths) {
|
|
26
|
+
for (const dir of [
|
|
27
|
+
paths.config,
|
|
28
|
+
paths.data,
|
|
29
|
+
paths.logsDeployment,
|
|
30
|
+
paths.runtime,
|
|
31
|
+
paths.cache,
|
|
32
|
+
paths.tmp,
|
|
33
|
+
paths.plans,
|
|
34
|
+
paths.verification,
|
|
35
|
+
paths.generated,
|
|
36
|
+
]) {
|
|
37
|
+
await mkdir(dir, { recursive: true });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// Atomic write: bounded write to a temp sibling then rename into place.
|
|
41
|
+
export async function atomicWrite(file, content) {
|
|
42
|
+
await mkdir(dirname(file), { recursive: true });
|
|
43
|
+
const tmp = `${file}.tmp-${randomBytes(6).toString("hex")}`;
|
|
44
|
+
await writeFile(tmp, content, "utf8");
|
|
45
|
+
await rename(tmp, file);
|
|
46
|
+
}
|
|
47
|
+
export async function readJson(file) {
|
|
48
|
+
try {
|
|
49
|
+
const { readFile } = await import("node:fs/promises");
|
|
50
|
+
const raw = await readFile(file, "utf8");
|
|
51
|
+
return JSON.parse(raw);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type WorkspacePaths } from "../paths.ts";
|
|
2
|
+
export interface MaterializedConfig {
|
|
3
|
+
publicValues: Record<string, string>;
|
|
4
|
+
secretValues: Record<string, string>;
|
|
5
|
+
}
|
|
6
|
+
export interface ModuleConfig {
|
|
7
|
+
moduleRef: string;
|
|
8
|
+
values: Record<string, string>;
|
|
9
|
+
secretRefs: readonly string[];
|
|
10
|
+
}
|
|
11
|
+
export declare function materializeConfig(paths: WorkspacePaths, config: ModuleConfig): Promise<void>;
|
|
12
|
+
export declare function loadConfig(paths: WorkspacePaths, moduleRef: string): Promise<MaterializedConfig | undefined>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { readJson } from "../paths.js";
|
|
3
|
+
import { writeJsonAtomic } from "./atomic.js";
|
|
4
|
+
import { assertSafeFileName } from "./guards.js";
|
|
5
|
+
function configFilePath(paths, moduleRef) {
|
|
6
|
+
assertSafeFileName(moduleRef, "moduleRef");
|
|
7
|
+
return join(paths.config, `${moduleRef}.json`);
|
|
8
|
+
}
|
|
9
|
+
function secretsFilePath(paths, moduleRef) {
|
|
10
|
+
assertSafeFileName(moduleRef, "moduleRef");
|
|
11
|
+
return join(paths.config, `${moduleRef}.secrets.json`);
|
|
12
|
+
}
|
|
13
|
+
function isStringMap(value) {
|
|
14
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
return Object.values(value).every((item) => typeof item === "string");
|
|
18
|
+
}
|
|
19
|
+
// secretRef config values are opaque reference identities (e.g.
|
|
20
|
+
// `secret://model-provider/default`), not raw secrets, so they materialize to
|
|
21
|
+
// the public config file verbatim. A raw secret would go to the separate 0o600
|
|
22
|
+
// secrets file — but v1 planning produces no raw secrets, so it stays empty and
|
|
23
|
+
// must never hold a secretRef reference.
|
|
24
|
+
export async function materializeConfig(paths, config) {
|
|
25
|
+
const publicValues = {};
|
|
26
|
+
for (const [key, value] of Object.entries(config.values)) {
|
|
27
|
+
publicValues[key] = value;
|
|
28
|
+
}
|
|
29
|
+
await writeJsonAtomic(configFilePath(paths, config.moduleRef), publicValues, 0o644);
|
|
30
|
+
await writeJsonAtomic(secretsFilePath(paths, config.moduleRef), {}, 0o600);
|
|
31
|
+
}
|
|
32
|
+
export async function loadConfig(paths, moduleRef) {
|
|
33
|
+
const publicRaw = await readJson(configFilePath(paths, moduleRef));
|
|
34
|
+
const secretRaw = await readJson(secretsFilePath(paths, moduleRef));
|
|
35
|
+
const publicValues = isStringMap(publicRaw) ? publicRaw : undefined;
|
|
36
|
+
const secretValues = isStringMap(secretRaw) ? secretRaw : undefined;
|
|
37
|
+
if (publicValues === undefined && secretValues === undefined)
|
|
38
|
+
return undefined;
|
|
39
|
+
return {
|
|
40
|
+
publicValues: publicValues ?? {},
|
|
41
|
+
secretValues: secretValues ?? {},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { DeploymentPlan, DeploymentState, LockRecord, VerificationRecord } from "../contracts.ts";
|
|
2
|
+
export declare const DEPLOYMENT_STATE_CONTRACT = "proflow.deployment-state.v1";
|
|
3
|
+
export declare function assertSafeFileName(name: string, kind: string): void;
|
|
4
|
+
export declare function isDeploymentState(value: unknown): value is DeploymentState;
|
|
5
|
+
export declare function isVerificationRecord(value: unknown): value is VerificationRecord;
|
|
6
|
+
export declare function isDeploymentPlan(value: unknown): value is DeploymentPlan;
|
|
7
|
+
export declare function isLockRecord(value: unknown): value is LockRecord;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { PlatformError } from "../errors.js";
|
|
2
|
+
export const DEPLOYMENT_STATE_CONTRACT = "proflow.deployment-state.v1";
|
|
3
|
+
const SAFE_FILE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
4
|
+
export function assertSafeFileName(name, kind) {
|
|
5
|
+
if (!SAFE_FILE_NAME.test(name)) {
|
|
6
|
+
throw new PlatformError("INVALID_REQUEST", `invalid ${kind} name: ${name}`);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
function isRecord(value) {
|
|
10
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
11
|
+
}
|
|
12
|
+
function isString(value) {
|
|
13
|
+
return typeof value === "string";
|
|
14
|
+
}
|
|
15
|
+
function isStringArray(value) {
|
|
16
|
+
return Array.isArray(value) && value.every(isString);
|
|
17
|
+
}
|
|
18
|
+
function isDeploymentIntent(value) {
|
|
19
|
+
return (value === "install" ||
|
|
20
|
+
value === "configure" ||
|
|
21
|
+
value === "upgrade" ||
|
|
22
|
+
value === "repair");
|
|
23
|
+
}
|
|
24
|
+
function isSelectedModuleFact(value) {
|
|
25
|
+
return (isRecord(value) &&
|
|
26
|
+
isString(value.moduleRef) &&
|
|
27
|
+
isString(value.moduleVersion));
|
|
28
|
+
}
|
|
29
|
+
function isLastAppliedPlan(value) {
|
|
30
|
+
return (isRecord(value) &&
|
|
31
|
+
isString(value.planRef) &&
|
|
32
|
+
isDeploymentIntent(value.intent) &&
|
|
33
|
+
isString(value.appliedAt));
|
|
34
|
+
}
|
|
35
|
+
function isVerificationIndexEntry(value) {
|
|
36
|
+
return (isRecord(value) && isString(value.moduleRef) && isString(value.latestRef));
|
|
37
|
+
}
|
|
38
|
+
function isPendingAction(value) {
|
|
39
|
+
return (isRecord(value) &&
|
|
40
|
+
isString(value.planRef) &&
|
|
41
|
+
isString(value.stepRef) &&
|
|
42
|
+
isString(value.moduleRef) &&
|
|
43
|
+
isString(value.action) &&
|
|
44
|
+
(value.description === undefined || isString(value.description)) &&
|
|
45
|
+
isString(value.createdAt));
|
|
46
|
+
}
|
|
47
|
+
export function isDeploymentState(value) {
|
|
48
|
+
if (!isRecord(value))
|
|
49
|
+
return false;
|
|
50
|
+
if (value.contract !== DEPLOYMENT_STATE_CONTRACT)
|
|
51
|
+
return false;
|
|
52
|
+
if (!Array.isArray(value.selectedModules))
|
|
53
|
+
return false;
|
|
54
|
+
if (!value.selectedModules.every(isSelectedModuleFact))
|
|
55
|
+
return false;
|
|
56
|
+
if (!Array.isArray(value.lastAppliedPlans))
|
|
57
|
+
return false;
|
|
58
|
+
if (!value.lastAppliedPlans.every(isLastAppliedPlan))
|
|
59
|
+
return false;
|
|
60
|
+
if (!Array.isArray(value.verificationIndex))
|
|
61
|
+
return false;
|
|
62
|
+
if (!value.verificationIndex.every(isVerificationIndexEntry))
|
|
63
|
+
return false;
|
|
64
|
+
if (!Array.isArray(value.pendingActions))
|
|
65
|
+
return false;
|
|
66
|
+
if (!value.pendingActions.every(isPendingAction))
|
|
67
|
+
return false;
|
|
68
|
+
return isString(value.updatedAt);
|
|
69
|
+
}
|
|
70
|
+
export function isVerificationRecord(value) {
|
|
71
|
+
if (!isRecord(value))
|
|
72
|
+
return false;
|
|
73
|
+
if (!isString(value.verificationRef))
|
|
74
|
+
return false;
|
|
75
|
+
if (!isString(value.moduleRef))
|
|
76
|
+
return false;
|
|
77
|
+
if (!isString(value.moduleVersion))
|
|
78
|
+
return false;
|
|
79
|
+
if (value.resourceIdentity !== undefined && !isString(value.resourceIdentity))
|
|
80
|
+
return false;
|
|
81
|
+
if (value.resourceVersion !== undefined && !isString(value.resourceVersion))
|
|
82
|
+
return false;
|
|
83
|
+
if (value.result !== "PASS" && value.result !== "FAIL")
|
|
84
|
+
return false;
|
|
85
|
+
if (!isString(value.summary))
|
|
86
|
+
return false;
|
|
87
|
+
if (!isStringArray(value.evidenceRefs))
|
|
88
|
+
return false;
|
|
89
|
+
return isString(value.verifiedAt);
|
|
90
|
+
}
|
|
91
|
+
export function isDeploymentPlan(value) {
|
|
92
|
+
if (!isRecord(value))
|
|
93
|
+
return false;
|
|
94
|
+
if (!isString(value.planRef))
|
|
95
|
+
return false;
|
|
96
|
+
if (!isDeploymentIntent(value.intent))
|
|
97
|
+
return false;
|
|
98
|
+
if (!Array.isArray(value.moduleTargets))
|
|
99
|
+
return false;
|
|
100
|
+
if (!Array.isArray(value.resolvedModules))
|
|
101
|
+
return false;
|
|
102
|
+
if (!Array.isArray(value.steps))
|
|
103
|
+
return false;
|
|
104
|
+
if (!Array.isArray(value.effects))
|
|
105
|
+
return false;
|
|
106
|
+
if (!Array.isArray(value.humanActions))
|
|
107
|
+
return false;
|
|
108
|
+
if (!Array.isArray(value.verification))
|
|
109
|
+
return false;
|
|
110
|
+
if (!isString(value.fingerprint))
|
|
111
|
+
return false;
|
|
112
|
+
return isString(value.createdAt);
|
|
113
|
+
}
|
|
114
|
+
export function isLockRecord(value) {
|
|
115
|
+
return (isRecord(value) &&
|
|
116
|
+
typeof value.pid === "number" &&
|
|
117
|
+
isString(value.createdAt) &&
|
|
118
|
+
isString(value.planRef) &&
|
|
119
|
+
isString(value.workspaceFingerprint));
|
|
120
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type { MaterializedConfig, ModuleConfig } from "./config.ts";
|
|
2
|
+
export { loadConfig, materializeConfig } from "./config.ts";
|
|
3
|
+
export { writeInstallDoc } from "./generated.ts";
|
|
4
|
+
export { listPlans, loadPlan, savePlan } from "./plans.ts";
|
|
5
|
+
export { emptyDeploymentState, loadDeploymentState, saveDeploymentState, } from "./state.ts";
|
|
6
|
+
export { appendVerification, loadLatestVerification, loadVerificationHistory, } from "./verification.ts";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { loadConfig, materializeConfig } from "./config.js";
|
|
2
|
+
export { writeInstallDoc } from "./generated.js";
|
|
3
|
+
export { listPlans, loadPlan, savePlan } from "./plans.js";
|
|
4
|
+
export { emptyDeploymentState, loadDeploymentState, saveDeploymentState, } from "./state.js";
|
|
5
|
+
export { appendVerification, loadLatestVerification, loadVerificationHistory, } from "./verification.js";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { DeploymentPlan } from "../contracts.ts";
|
|
2
|
+
import { type WorkspacePaths } from "../paths.ts";
|
|
3
|
+
export declare function savePlan(paths: WorkspacePaths, plan: DeploymentPlan): Promise<void>;
|
|
4
|
+
export declare function loadPlan(paths: WorkspacePaths, planRef: string): Promise<DeploymentPlan | undefined>;
|
|
5
|
+
export declare function listPlans(paths: WorkspacePaths): Promise<string[]>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { readdir } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { readJson } from "../paths.js";
|
|
4
|
+
import { writeJsonAtomic } from "./atomic.js";
|
|
5
|
+
import { assertSafeFileName, isDeploymentPlan } from "./guards.js";
|
|
6
|
+
function planFilePath(paths, planRef) {
|
|
7
|
+
assertSafeFileName(planRef, "planRef");
|
|
8
|
+
return join(paths.plans, `${planRef}.json`);
|
|
9
|
+
}
|
|
10
|
+
// Plans are public DTOs: secretRef config values are opaque reference
|
|
11
|
+
// identities (e.g. `secret://model-provider/default`), safe to persist verbatim.
|
|
12
|
+
// No raw secret ever enters a plan, so no redaction is applied on save.
|
|
13
|
+
export async function savePlan(paths, plan) {
|
|
14
|
+
await writeJsonAtomic(planFilePath(paths, plan.planRef), plan);
|
|
15
|
+
}
|
|
16
|
+
export async function loadPlan(paths, planRef) {
|
|
17
|
+
const value = await readJson(planFilePath(paths, planRef));
|
|
18
|
+
if (value === undefined)
|
|
19
|
+
return undefined;
|
|
20
|
+
return isDeploymentPlan(value) ? value : undefined;
|
|
21
|
+
}
|
|
22
|
+
export async function listPlans(paths) {
|
|
23
|
+
let entries;
|
|
24
|
+
try {
|
|
25
|
+
entries = await readdir(paths.plans);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
return entries
|
|
31
|
+
.filter((entry) => entry.endsWith(".json"))
|
|
32
|
+
.map((entry) => entry.slice(0, -".json".length))
|
|
33
|
+
.sort();
|
|
34
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { DeploymentState, PendingActionRecord } from "../contracts.ts";
|
|
2
|
+
import { type WorkspacePaths } from "../paths.ts";
|
|
3
|
+
export declare function emptyDeploymentState(): DeploymentState;
|
|
4
|
+
export declare function clearPendingActions(state: DeploymentState, isResolved: (action: PendingActionRecord) => boolean): DeploymentState;
|
|
5
|
+
export declare function clearCompletedPendingActions(state: DeploymentState): DeploymentState;
|
|
6
|
+
export declare function loadDeploymentState(paths: WorkspacePaths): Promise<DeploymentState | undefined>;
|
|
7
|
+
export declare function saveDeploymentState(paths: WorkspacePaths, state: DeploymentState): Promise<void>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { readJson } from "../paths.js";
|
|
2
|
+
import { writeJsonAtomic } from "./atomic.js";
|
|
3
|
+
import { DEPLOYMENT_STATE_CONTRACT, isDeploymentState } from "./guards.js";
|
|
4
|
+
export function emptyDeploymentState() {
|
|
5
|
+
return {
|
|
6
|
+
contract: DEPLOYMENT_STATE_CONTRACT,
|
|
7
|
+
selectedModules: [],
|
|
8
|
+
lastAppliedPlans: [],
|
|
9
|
+
verificationIndex: [],
|
|
10
|
+
pendingActions: [],
|
|
11
|
+
updatedAt: new Date().toISOString(),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export function clearPendingActions(state, isResolved) {
|
|
15
|
+
return {
|
|
16
|
+
...state,
|
|
17
|
+
pendingActions: state.pendingActions.filter((action) => !isResolved(action)),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export function clearCompletedPendingActions(state) {
|
|
21
|
+
const completed = new Set(state.lastAppliedPlans.map((entry) => entry.planRef));
|
|
22
|
+
return clearPendingActions(state, (action) => completed.has(action.planRef));
|
|
23
|
+
}
|
|
24
|
+
// Loaded state is validated at the boundary; a missing or corrupt file resolves
|
|
25
|
+
// to undefined so stale persisted facts can never impersonate current reality.
|
|
26
|
+
export async function loadDeploymentState(paths) {
|
|
27
|
+
const value = await readJson(paths.stateJson);
|
|
28
|
+
if (value === undefined)
|
|
29
|
+
return undefined;
|
|
30
|
+
return isDeploymentState(value) ? value : undefined;
|
|
31
|
+
}
|
|
32
|
+
export async function saveDeploymentState(paths, state) {
|
|
33
|
+
await writeJsonAtomic(paths.stateJson, state);
|
|
34
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { VerificationRecord } from "../contracts.ts";
|
|
2
|
+
import type { WorkspacePaths } from "../paths.ts";
|
|
3
|
+
export declare function appendVerification(paths: WorkspacePaths, record: VerificationRecord): Promise<void>;
|
|
4
|
+
export declare function loadVerificationHistory(paths: WorkspacePaths, moduleRef: string): Promise<VerificationRecord[]>;
|
|
5
|
+
export declare function loadLatestVerification(paths: WorkspacePaths, moduleRef: string): Promise<VerificationRecord | undefined>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { writeFileAtomic } from "./atomic.js";
|
|
4
|
+
import { assertSafeFileName, isVerificationRecord } from "./guards.js";
|
|
5
|
+
function verificationFilePath(paths, moduleRef) {
|
|
6
|
+
assertSafeFileName(moduleRef, "moduleRef");
|
|
7
|
+
return join(paths.verification, `${moduleRef}.jsonl`);
|
|
8
|
+
}
|
|
9
|
+
async function readJsonlLines(file) {
|
|
10
|
+
let raw;
|
|
11
|
+
try {
|
|
12
|
+
raw = await readFile(file, "utf8");
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
const lines = [];
|
|
18
|
+
for (const line of raw.split("\n")) {
|
|
19
|
+
const trimmed = line.trim();
|
|
20
|
+
if (trimmed.length === 0)
|
|
21
|
+
continue;
|
|
22
|
+
try {
|
|
23
|
+
lines.push(JSON.parse(trimmed));
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// skip a corrupt line rather than discarding otherwise valid history
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return lines;
|
|
30
|
+
}
|
|
31
|
+
// Verification history is append-only: existing records are preserved and the
|
|
32
|
+
// new record is appended, then the whole file is atomically replaced.
|
|
33
|
+
export async function appendVerification(paths, record) {
|
|
34
|
+
const file = verificationFilePath(paths, record.moduleRef);
|
|
35
|
+
const records = (await readJsonlLines(file)).filter(isVerificationRecord);
|
|
36
|
+
records.push(record);
|
|
37
|
+
const content = `${records.map((item) => JSON.stringify(item)).join("\n")}\n`;
|
|
38
|
+
await writeFileAtomic(file, content);
|
|
39
|
+
}
|
|
40
|
+
export async function loadVerificationHistory(paths, moduleRef) {
|
|
41
|
+
const lines = await readJsonlLines(verificationFilePath(paths, moduleRef));
|
|
42
|
+
return lines.filter(isVerificationRecord);
|
|
43
|
+
}
|
|
44
|
+
export async function loadLatestVerification(paths, moduleRef) {
|
|
45
|
+
const history = await loadVerificationHistory(paths, moduleRef);
|
|
46
|
+
return history[history.length - 1];
|
|
47
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { HumanAction, ModuleDescriptor } from "@tomflow/proflow-module-contract";
|
|
2
|
+
import type { DeploymentIntent, DeploymentPlan, DeploymentStep, ModuleTarget, ResolvedModule } from "../contracts.ts";
|
|
3
|
+
export declare function toResolvedModule(descriptor: ModuleDescriptor): ResolvedModule;
|
|
4
|
+
export declare function generatePlanRef(intent: DeploymentIntent, moduleTargets: readonly ModuleTarget[], now: Date): string;
|
|
5
|
+
export declare function deepFreeze<T>(value: T): T;
|
|
6
|
+
export interface AssembleArgs {
|
|
7
|
+
intent: DeploymentIntent;
|
|
8
|
+
modules: readonly ResolvedModule[];
|
|
9
|
+
targets: readonly ModuleTarget[];
|
|
10
|
+
config: Record<string, Record<string, string>> | undefined;
|
|
11
|
+
steps: DeploymentStep[];
|
|
12
|
+
humanActions: HumanAction[];
|
|
13
|
+
now: Date;
|
|
14
|
+
targetDescriptors?: readonly ModuleDescriptor[];
|
|
15
|
+
}
|
|
16
|
+
export declare function assemblePlan(args: AssembleArgs): DeploymentPlan;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { buildDependencyGraph } from "../graph/graph.js";
|
|
3
|
+
import { computeFingerprint } from "./fingerprint.js";
|
|
4
|
+
function compareRef(a, b) {
|
|
5
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
6
|
+
}
|
|
7
|
+
export function toResolvedModule(descriptor) {
|
|
8
|
+
return {
|
|
9
|
+
moduleRef: descriptor.moduleRef,
|
|
10
|
+
packageName: descriptor.packageName,
|
|
11
|
+
moduleVersion: descriptor.moduleVersion,
|
|
12
|
+
kind: descriptor.kind,
|
|
13
|
+
installClass: descriptor.installClass,
|
|
14
|
+
identity: descriptor.identity,
|
|
15
|
+
documentation: descriptor.documentation,
|
|
16
|
+
provides: descriptor.provides,
|
|
17
|
+
requires: descriptor.requires,
|
|
18
|
+
requirements: descriptor.requirements,
|
|
19
|
+
configSlots: descriptor.configSlots,
|
|
20
|
+
lifecycle: descriptor.lifecycle.supported,
|
|
21
|
+
verification: descriptor.verification,
|
|
22
|
+
effects: descriptor.effects,
|
|
23
|
+
source: { type: "installed" },
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export function generatePlanRef(intent, moduleTargets, now) {
|
|
27
|
+
const key = JSON.stringify({
|
|
28
|
+
intent,
|
|
29
|
+
targets: [...moduleTargets]
|
|
30
|
+
.sort((a, b) => compareRef(a.moduleRef, b.moduleRef))
|
|
31
|
+
.map((target) => ({
|
|
32
|
+
moduleRef: target.moduleRef,
|
|
33
|
+
targetVersion: target.targetVersion ?? null,
|
|
34
|
+
})),
|
|
35
|
+
});
|
|
36
|
+
const hash = createHash("sha256").update(key).digest("hex").slice(0, 8);
|
|
37
|
+
return `plan-${intent}-${now.getTime()}-${hash}`;
|
|
38
|
+
}
|
|
39
|
+
export function deepFreeze(value) {
|
|
40
|
+
if (value !== null && typeof value === "object" && !Object.isFrozen(value)) {
|
|
41
|
+
Object.freeze(value);
|
|
42
|
+
for (const key of Object.keys(value)) {
|
|
43
|
+
deepFreeze(value[key]);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
export function assemblePlan(args) {
|
|
49
|
+
const graph = buildDependencyGraph(args.modules);
|
|
50
|
+
const byRef = new Map(args.modules.map((module) => [module.moduleRef, module]));
|
|
51
|
+
const targetByRef = new Map(args.targets.map((target) => [target.moduleRef, target]));
|
|
52
|
+
const moduleTargets = [];
|
|
53
|
+
for (const ref of graph.order) {
|
|
54
|
+
const explicit = targetByRef.get(ref);
|
|
55
|
+
const merged = {
|
|
56
|
+
...(explicit?.config ?? {}),
|
|
57
|
+
...(args.config?.[ref] ?? {}),
|
|
58
|
+
};
|
|
59
|
+
const target = { moduleRef: ref };
|
|
60
|
+
if (explicit?.targetVersion !== undefined) {
|
|
61
|
+
target.targetVersion = explicit.targetVersion;
|
|
62
|
+
}
|
|
63
|
+
if (Object.keys(merged).length > 0) {
|
|
64
|
+
target.config = merged;
|
|
65
|
+
}
|
|
66
|
+
moduleTargets.push(target);
|
|
67
|
+
}
|
|
68
|
+
const verification = graph.order.map((ref) => ({
|
|
69
|
+
moduleRef: ref,
|
|
70
|
+
checks: (byRef.get(ref)?.verification.checks ?? []).map((check) => check.id),
|
|
71
|
+
}));
|
|
72
|
+
const effectMap = new Map();
|
|
73
|
+
for (const module of args.modules) {
|
|
74
|
+
for (const effect of module.effects) {
|
|
75
|
+
const key = `${effect.kind}:${effect.description}`;
|
|
76
|
+
if (!effectMap.has(key))
|
|
77
|
+
effectMap.set(key, effect);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const effects = [...effectMap.values()].sort((a, b) => compareRef(`${a.kind}:${a.description}`, `${b.kind}:${b.description}`));
|
|
81
|
+
const humanByAction = new Map();
|
|
82
|
+
for (const action of args.humanActions) {
|
|
83
|
+
if (!humanByAction.has(action.action)) {
|
|
84
|
+
humanByAction.set(action.action, action);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const humanActions = [...humanByAction.values()].sort((a, b) => compareRef(a.action, b.action));
|
|
88
|
+
const resolvedModules = [...args.modules].sort((a, b) => compareRef(a.moduleRef, b.moduleRef));
|
|
89
|
+
const plan = {
|
|
90
|
+
planRef: "",
|
|
91
|
+
intent: args.intent,
|
|
92
|
+
moduleTargets,
|
|
93
|
+
resolvedModules,
|
|
94
|
+
...(args.targetDescriptors !== undefined
|
|
95
|
+
? { targetDescriptors: [...args.targetDescriptors] }
|
|
96
|
+
: {}),
|
|
97
|
+
steps: args.steps,
|
|
98
|
+
effects,
|
|
99
|
+
humanActions,
|
|
100
|
+
verification,
|
|
101
|
+
fingerprint: "",
|
|
102
|
+
createdAt: args.now.toISOString(),
|
|
103
|
+
};
|
|
104
|
+
plan.planRef = generatePlanRef(args.intent, moduleTargets, args.now);
|
|
105
|
+
plan.fingerprint = computeFingerprint(plan);
|
|
106
|
+
return deepFreeze(plan);
|
|
107
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { DeploymentPlan, DeploymentStep } from "../contracts.ts";
|
|
2
|
+
export interface VerificationCheckResult {
|
|
3
|
+
id: string;
|
|
4
|
+
status: "PASS" | "FAIL";
|
|
5
|
+
}
|
|
6
|
+
export interface StepReality {
|
|
7
|
+
installedVersion?: string;
|
|
8
|
+
configValues?: Record<string, string>;
|
|
9
|
+
processRunning?: boolean;
|
|
10
|
+
resourceConfigured?: boolean;
|
|
11
|
+
humanActionVerified?: boolean;
|
|
12
|
+
verificationChecks?: readonly VerificationCheckResult[];
|
|
13
|
+
migrated?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export type StepCheckStatus = "SATISFIED" | "NOT_SATISFIED" | "UNKNOWN";
|
|
16
|
+
export interface StepCheckResult {
|
|
17
|
+
status: StepCheckStatus;
|
|
18
|
+
reason: string;
|
|
19
|
+
}
|
|
20
|
+
export declare function evaluateStepCheck(step: DeploymentStep, plan: DeploymentPlan, reality: StepReality): StepCheckResult;
|