@tomflow/proflow-platform-cli 0.1.2 → 0.1.4
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/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/execute.js +28 -0
- package/dist/src/binding/production-bindings.d.ts +1 -11
- package/dist/src/binding/production-bindings.js +13 -1
- package/dist/src/cli.d.ts +1 -0
- package/dist/src/cli.js +220 -21
- package/dist/src/errors.d.ts +1 -1
- package/dist/src/errors.js +1 -0
- package/dist/src/install/environment.js +1 -1
- package/dist/src/install/install.js +41 -0
- 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.js +13 -25
- package/dist/src/planner/upgrade.js +10 -1
- 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 +3 -3
- package/proflow.module.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @tomflow/proflow-platform-cli
|
|
2
2
|
|
|
3
|
+
## 0.1.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Make upgrade planning emit configuration writes only when a real config target exists, and fail before package mutation when new required configuration is missing.
|
|
8
|
+
|
|
9
|
+
## 0.1.3
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- Real-1 final remediation and black-box release closure
|
|
14
|
+
|
|
3
15
|
## 0.1.2
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
|
@@ -5,7 +5,7 @@ export declare const behaviorAdapter: {
|
|
|
5
5
|
ok: boolean;
|
|
6
6
|
status: "SUCCEEDED";
|
|
7
7
|
moduleRef: "platform-cli";
|
|
8
|
-
moduleVersion: "0.1.
|
|
8
|
+
moduleVersion: "0.1.4";
|
|
9
9
|
data?: {} | null;
|
|
10
10
|
};
|
|
11
11
|
observedEffects: never[];
|
|
@@ -16,7 +16,7 @@ export declare const behaviorAdapter: {
|
|
|
16
16
|
ok: boolean;
|
|
17
17
|
status: "SUCCEEDED";
|
|
18
18
|
moduleRef: "platform-cli";
|
|
19
|
-
moduleVersion: "0.1.
|
|
19
|
+
moduleVersion: "0.1.4";
|
|
20
20
|
data?: {} | null;
|
|
21
21
|
};
|
|
22
22
|
observedEffects: never[];
|
|
@@ -27,7 +27,7 @@ export declare const behaviorAdapter: {
|
|
|
27
27
|
ok: boolean;
|
|
28
28
|
status: "SUCCEEDED";
|
|
29
29
|
moduleRef: "platform-cli";
|
|
30
|
-
moduleVersion: "0.1.
|
|
30
|
+
moduleVersion: "0.1.4";
|
|
31
31
|
data?: {} | null;
|
|
32
32
|
};
|
|
33
33
|
observedEffects: never[];
|
|
@@ -38,7 +38,7 @@ export declare const behaviorAdapter: {
|
|
|
38
38
|
ok: boolean;
|
|
39
39
|
status: "SUCCEEDED";
|
|
40
40
|
moduleRef: "platform-cli";
|
|
41
|
-
moduleVersion: "0.1.
|
|
41
|
+
moduleVersion: "0.1.4";
|
|
42
42
|
data?: {} | null;
|
|
43
43
|
};
|
|
44
44
|
observedEffects: never[];
|
|
@@ -49,7 +49,7 @@ export declare const behaviorAdapter: {
|
|
|
49
49
|
ok: boolean;
|
|
50
50
|
status: "SUCCEEDED";
|
|
51
51
|
moduleRef: "platform-cli";
|
|
52
|
-
moduleVersion: "0.1.
|
|
52
|
+
moduleVersion: "0.1.4";
|
|
53
53
|
data?: {} | null;
|
|
54
54
|
};
|
|
55
55
|
observedEffects: never[];
|
|
@@ -3,7 +3,7 @@ export declare const descriptor: {
|
|
|
3
3
|
readonly contractVersion: "1.0.0";
|
|
4
4
|
readonly moduleRef: "platform-cli";
|
|
5
5
|
readonly packageName: "@tomflow/proflow-platform-cli";
|
|
6
|
-
readonly moduleVersion: "0.1.
|
|
6
|
+
readonly moduleVersion: "0.1.4";
|
|
7
7
|
readonly kind: "cli";
|
|
8
8
|
readonly templateVersion: "1.0.0";
|
|
9
9
|
readonly platformCompatibility: ">=1.0.0 <2.0.0";
|
|
@@ -3,7 +3,7 @@ export const descriptor = {
|
|
|
3
3
|
contractVersion: "1.0.0",
|
|
4
4
|
moduleRef: "platform-cli",
|
|
5
5
|
packageName: "@tomflow/proflow-platform-cli",
|
|
6
|
-
moduleVersion: "0.1.
|
|
6
|
+
moduleVersion: "0.1.4",
|
|
7
7
|
kind: "cli",
|
|
8
8
|
templateVersion: "1.0.0",
|
|
9
9
|
platformCompatibility: ">=1.0.0 <2.0.0",
|
|
@@ -12,6 +12,8 @@ export interface ApplyContext {
|
|
|
12
12
|
current: PlanInput;
|
|
13
13
|
driver: PackageManagerDriver;
|
|
14
14
|
observer?: RealityObserver;
|
|
15
|
+
/** Rebuilds installed descriptors/bindings after a package mutation. */
|
|
16
|
+
refreshCatalog?: () => Promise<ModuleCatalog>;
|
|
15
17
|
}
|
|
16
18
|
/**
|
|
17
19
|
* Applies a frozen deployment plan deterministically. Every step is re-checked
|
package/dist/src/apply/apply.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { PlatformError } from "../errors.js";
|
|
2
2
|
import { ensureLayout } from "../paths.js";
|
|
3
3
|
import { emptyDeploymentState, loadDeploymentState, loadPlan, saveDeploymentState, } from "../persistence/index.js";
|
|
4
|
-
import { checkPlanStale, evaluateStepCheck, } from "../planner/index.js";
|
|
4
|
+
import { checkPlanStale, ExecuteStrategy, evaluateStepCheck, } from "../planner/index.js";
|
|
5
5
|
import { acquireWorkspaceLock } from "../security/index.js";
|
|
6
6
|
import { executeStep } from "./execute.js";
|
|
7
7
|
import { createRealityObserver } from "./reality.js";
|
|
@@ -21,10 +21,11 @@ function failureMessage(error) {
|
|
|
21
21
|
*/
|
|
22
22
|
export async function applyPlan(context) {
|
|
23
23
|
const driver = context.driver;
|
|
24
|
-
|
|
24
|
+
let catalog = context.catalog;
|
|
25
|
+
let observer = context.observer ??
|
|
25
26
|
createRealityObserver({
|
|
26
27
|
paths: context.paths,
|
|
27
|
-
catalog
|
|
28
|
+
catalog,
|
|
28
29
|
driver,
|
|
29
30
|
});
|
|
30
31
|
await ensureLayout(context.paths);
|
|
@@ -105,7 +106,7 @@ export async function applyPlan(context) {
|
|
|
105
106
|
}
|
|
106
107
|
let outcome;
|
|
107
108
|
try {
|
|
108
|
-
outcome = await executeStep({ paths: context.paths, catalog
|
|
109
|
+
outcome = await executeStep({ paths: context.paths, catalog, driver }, step, plan);
|
|
109
110
|
}
|
|
110
111
|
catch (error) {
|
|
111
112
|
stepResults.push({
|
|
@@ -125,6 +126,20 @@ export async function applyPlan(context) {
|
|
|
125
126
|
}
|
|
126
127
|
switch (outcome.kind) {
|
|
127
128
|
case "SUCCEEDED": {
|
|
129
|
+
if (step.executeStrategy === ExecuteStrategy.lifecycleUninstall) {
|
|
130
|
+
// A lifecycle uninstall is owned by the module adapter. Its typed
|
|
131
|
+
// SUCCEEDED result is the authoritative teardown confirmation. An
|
|
132
|
+
// idempotent uninstall can legitimately leave status UNBOUND/UNKNOWN,
|
|
133
|
+
// so a second status probe must not reinterpret that successful
|
|
134
|
+
// teardown as a failed lifecycle:stopped postcondition.
|
|
135
|
+
stepResults.push({
|
|
136
|
+
stepRef: step.stepRef,
|
|
137
|
+
moduleRef: step.moduleRef,
|
|
138
|
+
status: "EXECUTED",
|
|
139
|
+
message: `${step.moduleRef} owner confirmed lifecycle uninstall`,
|
|
140
|
+
});
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
128
143
|
// Postcondition re-check: only a genuinely successful mutation is
|
|
129
144
|
// confirmed; an effect that cannot be confirmed stops the apply
|
|
130
145
|
// rather than blindly repeating it.
|
|
@@ -168,6 +183,34 @@ export async function applyPlan(context) {
|
|
|
168
183
|
status: "EXECUTED",
|
|
169
184
|
message: post.reason,
|
|
170
185
|
});
|
|
186
|
+
if (step.kind === "package" && context.refreshCatalog !== undefined) {
|
|
187
|
+
try {
|
|
188
|
+
catalog = await context.refreshCatalog();
|
|
189
|
+
if (context.observer === undefined) {
|
|
190
|
+
observer = createRealityObserver({
|
|
191
|
+
paths: context.paths,
|
|
192
|
+
catalog,
|
|
193
|
+
driver,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
stepResults.push({
|
|
199
|
+
stepRef: step.stepRef,
|
|
200
|
+
moduleRef: step.moduleRef,
|
|
201
|
+
status: "FAILED",
|
|
202
|
+
message: `post-package catalog refresh failed: ${failureMessage(error)}`,
|
|
203
|
+
});
|
|
204
|
+
state.updatedAt = nowIso();
|
|
205
|
+
await saveDeploymentState(context.paths, state);
|
|
206
|
+
return {
|
|
207
|
+
planRef: plan.planRef,
|
|
208
|
+
outcome: "FAILED",
|
|
209
|
+
stepResults,
|
|
210
|
+
completedAt: nowIso(),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
}
|
|
171
214
|
break;
|
|
172
215
|
}
|
|
173
216
|
case "ACTION_REQUIRED": {
|
|
@@ -65,6 +65,20 @@ export async function rebuildCurrentAssumptions(catalog, plan) {
|
|
|
65
65
|
facts: diagnosis.facts,
|
|
66
66
|
};
|
|
67
67
|
}
|
|
68
|
+
if (plan.intent === "uninstall") {
|
|
69
|
+
// A persisted uninstall plan does not store uninstallScope. Recover the only
|
|
70
|
+
// legal scope from its module set for the staleness re-plan: any core module
|
|
71
|
+
// can only have entered the plan through whole-instance uninstall.
|
|
72
|
+
const uninstallScope = plan.resolvedModules.some((module) => module.installClass === "core")
|
|
73
|
+
? "platform-instance"
|
|
74
|
+
: "module";
|
|
75
|
+
return {
|
|
76
|
+
intent: plan.intent,
|
|
77
|
+
modules,
|
|
78
|
+
targets: plan.moduleTargets,
|
|
79
|
+
uninstallScope,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
68
82
|
return {
|
|
69
83
|
intent: plan.intent,
|
|
70
84
|
modules,
|
|
@@ -6,6 +6,29 @@ import { cleanupRemovableFilesystemEffects } from "./cleanup.js";
|
|
|
6
6
|
function moduleOf(plan, step) {
|
|
7
7
|
return plan.resolvedModules.find((module) => module.moduleRef === step.moduleRef);
|
|
8
8
|
}
|
|
9
|
+
function moduleSourceForMaterialization(module) {
|
|
10
|
+
if (module.source.type === "registry") {
|
|
11
|
+
throw new PlatformError("APPLY_FAILED", `registry bootstrap target ${module.packageName} has no local config materializer`);
|
|
12
|
+
}
|
|
13
|
+
return {
|
|
14
|
+
type: module.source.type,
|
|
15
|
+
packageName: module.packageName,
|
|
16
|
+
...(module.source.path === undefined ? {} : { path: module.source.path }),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
async function materializeModuleOwnedConfig(deps, module, config) {
|
|
20
|
+
const namespace = await deps.catalog.loadAdapter(moduleSourceForMaterialization(module));
|
|
21
|
+
if (typeof namespace !== "object" || namespace === null)
|
|
22
|
+
return;
|
|
23
|
+
const materializer = Reflect.get(namespace, "materializeProductionConfig");
|
|
24
|
+
if (typeof materializer !== "function")
|
|
25
|
+
return;
|
|
26
|
+
await materializer({
|
|
27
|
+
moduleRef: module.moduleRef,
|
|
28
|
+
config: config.values,
|
|
29
|
+
workspaceRoot: deps.paths.root,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
9
32
|
function configForStep(plan, step, module) {
|
|
10
33
|
const target = plan.moduleTargets.find((entry) => entry.moduleRef === step.moduleRef);
|
|
11
34
|
if (target?.config === undefined)
|
|
@@ -98,6 +121,11 @@ export async function executeStep(deps, step, plan) {
|
|
|
98
121
|
if (config === undefined) {
|
|
99
122
|
throw new PlatformError("APPLY_FAILED", `module ${step.moduleRef} has no config target to materialize`);
|
|
100
123
|
}
|
|
124
|
+
// The Platform-owned public config is authoritative apply reality. Do not
|
|
125
|
+
// commit it until the module-owned materializer has accepted the target;
|
|
126
|
+
// otherwise a failed materializer can leave a false-satisfied config step
|
|
127
|
+
// that a same-plan resume would incorrectly SKIP.
|
|
128
|
+
await materializeModuleOwnedConfig(deps, module, config);
|
|
101
129
|
await materializeConfig(deps.paths, config);
|
|
102
130
|
return { kind: "SUCCEEDED" };
|
|
103
131
|
}
|
|
@@ -2,6 +2,7 @@ import type { ResolvedModule } from "../contracts.ts";
|
|
|
2
2
|
type ResolvedSource = ResolvedModule["source"];
|
|
3
3
|
export interface DeploymentAdapterBinding {
|
|
4
4
|
behaviorAdapter: Record<string, unknown>;
|
|
5
|
+
materializeProductionConfig?: (...args: unknown[]) => unknown;
|
|
5
6
|
}
|
|
6
7
|
export type ProductionBindingFactory = (input: {
|
|
7
8
|
moduleRef: string;
|
|
@@ -30,16 +31,5 @@ export interface ProductionBindingOptions {
|
|
|
30
31
|
importAdapter: (packageName: string, source: ResolvedSource) => Promise<Record<string, unknown>>;
|
|
31
32
|
}
|
|
32
33
|
export declare function importRawAdapter(packageName: string, source: ResolvedSource, workspaceRoot: string): Promise<Record<string, unknown>>;
|
|
33
|
-
/**
|
|
34
|
-
* The shipped Platform CLI production binding factory. For every discovered
|
|
35
|
-
* module it imports the module's own `deployment/adapter.ts` and, when that
|
|
36
|
-
* adapter exposes a `createProductionBinding` factory, invokes it with the
|
|
37
|
-
* module's materialized config to obtain a real bound adapter. A module without
|
|
38
|
-
* a production factory — or whose import/factory fails, or which has no
|
|
39
|
-
* materialized config the adapter accepts — is left out of the map, so the
|
|
40
|
-
* catalog falls back to the module's unbound default, which must fail-closed
|
|
41
|
-
* (ACTION_REQUIRED / NOT_READY). Platform CLI never invents a service or
|
|
42
|
-
* resource reality: it only relays the adapter's own current reality.
|
|
43
|
-
*/
|
|
44
34
|
export declare function buildProductionBindings(options: ProductionBindingOptions): Promise<ReadonlyMap<string, DeploymentAdapterBinding>>;
|
|
45
35
|
export {};
|
|
@@ -45,6 +45,14 @@ export async function importRawAdapter(packageName, source, workspaceRoot) {
|
|
|
45
45
|
* (ACTION_REQUIRED / NOT_READY). Platform CLI never invents a service or
|
|
46
46
|
* resource reality: it only relays the adapter's own current reality.
|
|
47
47
|
*/
|
|
48
|
+
function materializerBinding(namespace) {
|
|
49
|
+
const materializer = namespace.materializeProductionConfig;
|
|
50
|
+
return typeof materializer === "function"
|
|
51
|
+
? {
|
|
52
|
+
materializeProductionConfig: materializer,
|
|
53
|
+
}
|
|
54
|
+
: {};
|
|
55
|
+
}
|
|
48
56
|
export async function buildProductionBindings(options) {
|
|
49
57
|
const bindings = new Map();
|
|
50
58
|
for (const module of options.modules) {
|
|
@@ -69,6 +77,7 @@ export async function buildProductionBindings(options) {
|
|
|
69
77
|
: {});
|
|
70
78
|
bindings.set(module.packageName, {
|
|
71
79
|
behaviorAdapter: managedServiceAdapter(options.workspaceRoot, module, processBinding.serviceProcess, probeAdapter),
|
|
80
|
+
...materializerBinding(namespace),
|
|
72
81
|
});
|
|
73
82
|
}
|
|
74
83
|
// A formal service package that exposes the process seam must never fall
|
|
@@ -92,7 +101,10 @@ export async function buildProductionBindings(options) {
|
|
|
92
101
|
binding !== null &&
|
|
93
102
|
typeof binding.behaviorAdapter === "object" &&
|
|
94
103
|
binding.behaviorAdapter !== null) {
|
|
95
|
-
bindings.set(module.packageName,
|
|
104
|
+
bindings.set(module.packageName, {
|
|
105
|
+
...binding,
|
|
106
|
+
...materializerBinding(namespace),
|
|
107
|
+
});
|
|
96
108
|
}
|
|
97
109
|
}
|
|
98
110
|
catch {
|
package/dist/src/cli.d.ts
CHANGED
package/dist/src/cli.js
CHANGED
|
@@ -86,7 +86,7 @@ function parseArgs(argv) {
|
|
|
86
86
|
}
|
|
87
87
|
function requiredOptionValue(argv, index, option) {
|
|
88
88
|
const value = argv[index + 1];
|
|
89
|
-
if (value === undefined || value.startsWith("-")) {
|
|
89
|
+
if (value === undefined || value === "" || value.startsWith("-")) {
|
|
90
90
|
throw new PlatformError("INVALID_REQUEST", `${option} requires a value`);
|
|
91
91
|
}
|
|
92
92
|
return value;
|
|
@@ -309,14 +309,41 @@ async function handleInstallerPreflight(root) {
|
|
|
309
309
|
: "BLOCKED";
|
|
310
310
|
return outcome("preflight", status, result);
|
|
311
311
|
}
|
|
312
|
+
async function loadEffectiveConfig(ctx, modules, configFile) {
|
|
313
|
+
const effective = {};
|
|
314
|
+
for (const module of modules) {
|
|
315
|
+
const stored = await loadConfig(ctx.paths, module.moduleRef);
|
|
316
|
+
if (stored === undefined)
|
|
317
|
+
continue;
|
|
318
|
+
effective[module.moduleRef] = {
|
|
319
|
+
...stored.publicValues,
|
|
320
|
+
...stored.secretValues,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
const provided = await loadConfigFile(configFile);
|
|
324
|
+
for (const [moduleRef, values] of Object.entries(provided)) {
|
|
325
|
+
effective[moduleRef] = {
|
|
326
|
+
...(effective[moduleRef] ?? {}),
|
|
327
|
+
...values,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
return effective;
|
|
331
|
+
}
|
|
312
332
|
async function handlePreflight(ctx, args) {
|
|
313
333
|
const modules = await discoverModules({ catalog: ctx.catalog });
|
|
314
334
|
const selected = selectModules(modules, args.positional[0]);
|
|
315
|
-
const config = await
|
|
316
|
-
const result = await runPreflight(selected, {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
:
|
|
335
|
+
const config = await loadEffectiveConfig(ctx, selected, args.configFile);
|
|
336
|
+
const result = await runPreflight(selected, {
|
|
337
|
+
config,
|
|
338
|
+
catalog: ctx.catalog,
|
|
339
|
+
paths: ctx.paths,
|
|
340
|
+
});
|
|
341
|
+
const status = result.status === "READY"
|
|
342
|
+
? "SUCCEEDED"
|
|
343
|
+
: result.status === "ACTION_REQUIRED"
|
|
344
|
+
? "ACTION_REQUIRED"
|
|
345
|
+
: "BLOCKED";
|
|
346
|
+
return outcome("preflight", status, result);
|
|
320
347
|
}
|
|
321
348
|
async function handlePlan(ctx, args) {
|
|
322
349
|
const intent = args.intent;
|
|
@@ -423,6 +450,7 @@ async function handlePlan(ctx, args) {
|
|
|
423
450
|
currentDescriptors,
|
|
424
451
|
targetDescriptors,
|
|
425
452
|
targets,
|
|
453
|
+
config,
|
|
426
454
|
});
|
|
427
455
|
await savePlan(ctx.paths, plan);
|
|
428
456
|
return outcome("plan", "SUCCEEDED", {
|
|
@@ -431,9 +459,15 @@ async function handlePlan(ctx, args) {
|
|
|
431
459
|
plan,
|
|
432
460
|
});
|
|
433
461
|
}
|
|
462
|
+
if (intent === "configure") {
|
|
463
|
+
const plan = planDeployment({ intent, modules: selected, config });
|
|
464
|
+
await savePlan(ctx.paths, plan);
|
|
465
|
+
return outcome("plan", "SUCCEEDED", { planRef: plan.planRef, plan });
|
|
466
|
+
}
|
|
434
467
|
const preflight = await runPreflight(selected, {
|
|
435
468
|
config,
|
|
436
469
|
catalog: ctx.catalog,
|
|
470
|
+
paths: ctx.paths,
|
|
437
471
|
});
|
|
438
472
|
if (preflight.status === "NOT_READY" ||
|
|
439
473
|
preflight.status === "ACTION_REQUIRED") {
|
|
@@ -472,6 +506,11 @@ async function handleApply(ctx, args) {
|
|
|
472
506
|
driver: createWorkspacePackageManagerDriver({
|
|
473
507
|
workspaceRoot: ctx.paths.root,
|
|
474
508
|
}),
|
|
509
|
+
...(plan.intent === "upgrade"
|
|
510
|
+
? {
|
|
511
|
+
refreshCatalog: async () => (await buildContext(ctx.paths.root)).catalog,
|
|
512
|
+
}
|
|
513
|
+
: {}),
|
|
475
514
|
});
|
|
476
515
|
const status = result.outcome === "COMPLETE"
|
|
477
516
|
? "SUCCEEDED"
|
|
@@ -499,6 +538,23 @@ async function handleManagedMutation(ctx, args, intent) {
|
|
|
499
538
|
if (planned.status !== "SUCCEEDED") {
|
|
500
539
|
return { ...planned, command: intent };
|
|
501
540
|
}
|
|
541
|
+
if (intent === "upgrade") {
|
|
542
|
+
const data = planned.data;
|
|
543
|
+
if (typeof data === "object" && data !== null && !Array.isArray(data)) {
|
|
544
|
+
const plan = Reflect.get(data, "plan");
|
|
545
|
+
if (typeof plan === "object" &&
|
|
546
|
+
plan !== null &&
|
|
547
|
+
!Array.isArray(plan) &&
|
|
548
|
+
Array.isArray(Reflect.get(plan, "steps")) &&
|
|
549
|
+
Reflect.get(plan, "steps").length === 0) {
|
|
550
|
+
return outcome("upgrade", "SUCCEEDED", {
|
|
551
|
+
...data,
|
|
552
|
+
alreadyLatest: true,
|
|
553
|
+
changed: false,
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
502
558
|
const planRef = planRefFromOutcome(planned);
|
|
503
559
|
const applied = await handleApply(ctx, {
|
|
504
560
|
...args,
|
|
@@ -639,23 +695,37 @@ async function handleInstallWithGlobalBinding(args, runtime) {
|
|
|
639
695
|
const operationLock = await acquireGlobalOperationLock(runtime.globalRoot);
|
|
640
696
|
try {
|
|
641
697
|
const requestedWorkspace = await resolveRequestedInstallWorkspace(args, runtime);
|
|
642
|
-
const
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
if (
|
|
648
|
-
initialBinding.state === "INSTALLED" &&
|
|
649
|
-
args.positional[0] === undefined) {
|
|
698
|
+
const existing = await loadGlobalBinding(runtime.globalRoot);
|
|
699
|
+
if (existing !== undefined &&
|
|
700
|
+
existing.workspaceRealPath !== requestedWorkspace) {
|
|
701
|
+
throw new PlatformError("WORKSPACE_ALREADY_BOUND", `ProFlow is already bound to ${existing.workspaceRealPath}; uninstall it before installing ${requestedWorkspace}`);
|
|
702
|
+
}
|
|
703
|
+
if (existing?.state === "INSTALLED" && args.positional[0] === undefined) {
|
|
650
704
|
return withWorkspace(outcome("install", "SUCCEEDED", {
|
|
651
705
|
alreadyInstalled: true,
|
|
652
706
|
changed: false,
|
|
653
|
-
boundWorkspace:
|
|
654
|
-
}),
|
|
707
|
+
boundWorkspace: existing.workspaceRealPath,
|
|
708
|
+
}), existing);
|
|
655
709
|
}
|
|
656
|
-
if (
|
|
710
|
+
if (existing?.state === "UNINSTALLING") {
|
|
657
711
|
throw new PlatformError("GLOBAL_OPERATION_LOCKED", "the bound Platform Instance is marked UNINSTALLING; finish recovery before installing");
|
|
658
712
|
}
|
|
713
|
+
// Gate A runs before claiming a fresh global binding. Deterministic local
|
|
714
|
+
// failures (package-manager conflict, missing executable, registry down,
|
|
715
|
+
// unwritable Workspace) therefore do not leave a fake BROKEN installation.
|
|
716
|
+
// The global operation lock keeps this preflight + claim sequence atomic
|
|
717
|
+
// against other Platform mutations.
|
|
718
|
+
const installer = await preflightInstallerEnvironment({
|
|
719
|
+
workspaceRoot: requestedWorkspace,
|
|
720
|
+
});
|
|
721
|
+
if (installer.status !== "READY") {
|
|
722
|
+
return outcome("install", installer.status === "ACTION_REQUIRED" ? "ACTION_REQUIRED" : "BLOCKED", { requestedWorkspace, preflight: installer });
|
|
723
|
+
}
|
|
724
|
+
const claimed = await claimWorkspaceBinding({
|
|
725
|
+
workspace: requestedWorkspace,
|
|
726
|
+
globalRoot: runtime.globalRoot,
|
|
727
|
+
});
|
|
728
|
+
const initialBinding = claimed.binding;
|
|
659
729
|
let activeBinding = initialBinding;
|
|
660
730
|
const controlsPlatformState = !claimed.alreadyBound || initialBinding.state !== "INSTALLED";
|
|
661
731
|
if (controlsPlatformState && initialBinding.state !== "INSTALLING") {
|
|
@@ -889,7 +959,7 @@ async function handleDoctor(ctx, args) {
|
|
|
889
959
|
async function handleManifest(ctx, args) {
|
|
890
960
|
const modules = await discoverModules({ catalog: ctx.catalog });
|
|
891
961
|
const selected = selectModules(modules, args.positional[0]);
|
|
892
|
-
const config = await
|
|
962
|
+
const config = await loadEffectiveConfig(ctx, selected, args.configFile);
|
|
893
963
|
const manifest = await buildManifest({
|
|
894
964
|
catalog: ctx.catalog,
|
|
895
965
|
modules: selected,
|
|
@@ -1096,7 +1166,125 @@ function toMachineResult(outcomeResult) {
|
|
|
1096
1166
|
: {}),
|
|
1097
1167
|
});
|
|
1098
1168
|
}
|
|
1169
|
+
function isRecord(value) {
|
|
1170
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1171
|
+
}
|
|
1172
|
+
function humanDetailLines(data) {
|
|
1173
|
+
if (!isRecord(data)) {
|
|
1174
|
+
return data === undefined ? [] : [String(data)];
|
|
1175
|
+
}
|
|
1176
|
+
const lines = [];
|
|
1177
|
+
for (const key of [
|
|
1178
|
+
"installed",
|
|
1179
|
+
"bindingState",
|
|
1180
|
+
"boundWorkspace",
|
|
1181
|
+
"requestedWorkspace",
|
|
1182
|
+
"alreadyInstalled",
|
|
1183
|
+
"alreadyLatest",
|
|
1184
|
+
"changed",
|
|
1185
|
+
"bindingCleared",
|
|
1186
|
+
"resourcesCleaned",
|
|
1187
|
+
"planRef",
|
|
1188
|
+
"outcome",
|
|
1189
|
+
]) {
|
|
1190
|
+
const value = data[key];
|
|
1191
|
+
if (typeof value === "string" ||
|
|
1192
|
+
typeof value === "number" ||
|
|
1193
|
+
typeof value === "boolean") {
|
|
1194
|
+
lines.push(`${key}: ${String(value)}`);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
if (typeof data.nextAction === "string" && data.nextAction !== "") {
|
|
1198
|
+
lines.push(`Next: ${data.nextAction}`);
|
|
1199
|
+
}
|
|
1200
|
+
const findings = data.findings;
|
|
1201
|
+
let hasMissingConfig = false;
|
|
1202
|
+
if (Array.isArray(findings) && findings.length > 0) {
|
|
1203
|
+
lines.push("Findings:");
|
|
1204
|
+
for (const finding of findings) {
|
|
1205
|
+
if (!isRecord(finding))
|
|
1206
|
+
continue;
|
|
1207
|
+
const severity = typeof finding.severity === "string"
|
|
1208
|
+
? finding.severity.toUpperCase()
|
|
1209
|
+
: "INFO";
|
|
1210
|
+
const code = typeof finding.code === "string" ? finding.code : "CHECK";
|
|
1211
|
+
if (code === "CONFIG_MISSING")
|
|
1212
|
+
hasMissingConfig = true;
|
|
1213
|
+
const moduleRef = typeof finding.moduleRef === "string" ? ` ${finding.moduleRef}` : "";
|
|
1214
|
+
const message = typeof finding.message === "string" ? finding.message : "";
|
|
1215
|
+
lines.push(`- [${severity}] ${code}${moduleRef}${message === "" ? "" : `: ${message}`}`);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
if (hasMissingConfig) {
|
|
1219
|
+
lines.push("Next: review .proflow/deployment/generated/INSTALL.md, create the required config file, then run platform plan --intent configure --config <file> and platform apply <planRef>.");
|
|
1220
|
+
}
|
|
1221
|
+
const preflight = data.preflight;
|
|
1222
|
+
if (isRecord(preflight)) {
|
|
1223
|
+
const nested = humanDetailLines(preflight);
|
|
1224
|
+
if (nested.length > 0) {
|
|
1225
|
+
lines.push("Preflight:", ...nested.map((line) => ` ${line}`));
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
return lines;
|
|
1229
|
+
}
|
|
1230
|
+
export function renderHumanResult(machineOutput) {
|
|
1231
|
+
const parsed = JSON.parse(machineOutput);
|
|
1232
|
+
if (!isRecord(parsed))
|
|
1233
|
+
return machineOutput;
|
|
1234
|
+
const data = parsed.data;
|
|
1235
|
+
if (isRecord(data) && typeof data.version === "string") {
|
|
1236
|
+
return `ProFlow Platform CLI ${data.version}`;
|
|
1237
|
+
}
|
|
1238
|
+
if (isRecord(data) && typeof data.usage === "string") {
|
|
1239
|
+
const commands = Array.isArray(data.commands)
|
|
1240
|
+
? data.commands.filter((item) => typeof item === "string")
|
|
1241
|
+
: [];
|
|
1242
|
+
const examples = Array.isArray(data.examples)
|
|
1243
|
+
? data.examples.filter((item) => typeof item === "string")
|
|
1244
|
+
: [];
|
|
1245
|
+
return [
|
|
1246
|
+
"ProFlow Platform CLI",
|
|
1247
|
+
"",
|
|
1248
|
+
`Usage: ${data.usage}`,
|
|
1249
|
+
...(commands.length === 0
|
|
1250
|
+
? []
|
|
1251
|
+
: ["", `Commands: ${commands.join(", ")}`]),
|
|
1252
|
+
...(examples.length === 0
|
|
1253
|
+
? []
|
|
1254
|
+
: ["", "Common flows:", ...examples.map((item) => ` ${item}`)]),
|
|
1255
|
+
].join("\n");
|
|
1256
|
+
}
|
|
1257
|
+
const status = typeof parsed.status === "string" ? parsed.status : "UNKNOWN";
|
|
1258
|
+
const lines = [`ProFlow: ${status}`];
|
|
1259
|
+
const workspace = parsed.workspace;
|
|
1260
|
+
if (isRecord(workspace) && typeof workspace.boundWorkspace === "string") {
|
|
1261
|
+
lines.push(`Workspace: ${workspace.boundWorkspace}`);
|
|
1262
|
+
}
|
|
1263
|
+
const error = parsed.error;
|
|
1264
|
+
if (isRecord(error)) {
|
|
1265
|
+
const code = typeof error.code === "string" ? error.code : "COMMAND_FAILED";
|
|
1266
|
+
const message = typeof error.message === "string" ? error.message : "Unknown error";
|
|
1267
|
+
lines.push(`Error: ${code}: ${message}`);
|
|
1268
|
+
}
|
|
1269
|
+
lines.push(...humanDetailLines(data));
|
|
1270
|
+
if (lines.length === 1 && status === "SUCCEEDED") {
|
|
1271
|
+
lines.push(`Version: ${MODULE_VERSION}`, "Next: run platform --help");
|
|
1272
|
+
}
|
|
1273
|
+
return lines.join("\n");
|
|
1274
|
+
}
|
|
1099
1275
|
export async function runCli(argv, runtime = {}) {
|
|
1276
|
+
const filtered = argv.filter((argument) => argument !== "--json");
|
|
1277
|
+
if (filtered.length === 1 &&
|
|
1278
|
+
(filtered[0] === "--version" || filtered[0] === "-v")) {
|
|
1279
|
+
return JSON.stringify({
|
|
1280
|
+
contract: "deployment.result.v1",
|
|
1281
|
+
ok: true,
|
|
1282
|
+
status: "SUCCEEDED",
|
|
1283
|
+
moduleRef: MODULE_REF,
|
|
1284
|
+
moduleVersion: MODULE_VERSION,
|
|
1285
|
+
data: { version: MODULE_VERSION },
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1100
1288
|
if (argv.includes("--help") || argv.includes("-h")) {
|
|
1101
1289
|
return JSON.stringify({
|
|
1102
1290
|
contract: "deployment.result.v1",
|
|
@@ -1107,10 +1295,19 @@ export async function runCli(argv, runtime = {}) {
|
|
|
1107
1295
|
data: {
|
|
1108
1296
|
usage: "platform <command> [module|package] [--workspace <path>] [--json]",
|
|
1109
1297
|
commands: [...COMMANDS],
|
|
1298
|
+
examples: [
|
|
1299
|
+
"platform preflight --intent install --workspace <path>",
|
|
1300
|
+
"platform install [module|package] [--workspace <path>]",
|
|
1301
|
+
"platform preflight # startup/runtime readiness for the bound Workspace",
|
|
1302
|
+
"platform plan --intent configure --config <file>",
|
|
1303
|
+
"platform apply <planRef>",
|
|
1304
|
+
"platform start | status | verify | doctor | stop | restart",
|
|
1305
|
+
"platform uninstall # uninstall the bound Platform Instance, not the global CLI",
|
|
1306
|
+
"append --json for the stable machine-readable contract",
|
|
1307
|
+
],
|
|
1110
1308
|
},
|
|
1111
1309
|
});
|
|
1112
1310
|
}
|
|
1113
|
-
const filtered = argv.filter((argument) => argument !== "--json");
|
|
1114
1311
|
if (filtered.length === 0) {
|
|
1115
1312
|
return JSON.stringify({
|
|
1116
1313
|
contract: "deployment.result.v1",
|
|
@@ -1123,8 +1320,10 @@ export async function runCli(argv, runtime = {}) {
|
|
|
1123
1320
|
return toMachineResult(await dispatchCommand(filtered, runtime));
|
|
1124
1321
|
}
|
|
1125
1322
|
if (import.meta.main) {
|
|
1126
|
-
const
|
|
1127
|
-
|
|
1323
|
+
const argv = process.argv.slice(2);
|
|
1324
|
+
const output = await runCli(argv);
|
|
1325
|
+
const rendered = argv.includes("--json") ? output : renderHumanResult(output);
|
|
1326
|
+
process.stdout.write(`${rendered}\n`);
|
|
1128
1327
|
const parsed = JSON.parse(output);
|
|
1129
1328
|
if (typeof parsed === "object" &&
|
|
1130
1329
|
parsed !== null &&
|
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", "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"];
|
|
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
|
@@ -102,7 +102,7 @@ export async function preflightInstallerEnvironment(options) {
|
|
|
102
102
|
if (npmVersion !== undefined) {
|
|
103
103
|
try {
|
|
104
104
|
registry = await resolveScopeRegistry(options.workspaceRoot, runner);
|
|
105
|
-
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);
|
|
106
106
|
findings.push({
|
|
107
107
|
code: "REGISTRY_READY",
|
|
108
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) {
|
|
@@ -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);
|
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
|
}
|
|
@@ -71,8 +71,17 @@ export function planUpgrade(input) {
|
|
|
71
71
|
throw new PlatformError("UPGRADE_FAILED", `upgrade ${ref} has breaking changes but declares no migrate lifecycle: ${assessment.breakingChanges.join("; ")}`);
|
|
72
72
|
}
|
|
73
73
|
const dependencies = deps.get(ref) ?? [];
|
|
74
|
+
const configTarget = input.config?.[ref];
|
|
75
|
+
const missingRequiredConfig = module.configSlots
|
|
76
|
+
.filter((slot) => slot.required &&
|
|
77
|
+
slot.default === undefined &&
|
|
78
|
+
configTarget?.[slot.key] === undefined)
|
|
79
|
+
.map((slot) => slot.key);
|
|
80
|
+
if (missingRequiredConfig.length > 0) {
|
|
81
|
+
throw new PlatformError("UPGRADE_FAILED", `upgrade ${ref} requires configuration before package mutation: ${missingRequiredConfig.join(", ")}`);
|
|
82
|
+
}
|
|
74
83
|
steps.push(packageStep(seq, module, "upgrade", dependencies));
|
|
75
|
-
if (
|
|
84
|
+
if (configTarget !== undefined) {
|
|
76
85
|
steps.push(configStep(seq, module));
|
|
77
86
|
}
|
|
78
87
|
if (assessment?.migrationRequired === true) {
|
|
@@ -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>;
|
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
import { PlatformError } from "../errors.js";
|
|
2
2
|
import { buildDependencyGraph, ModuleRefUnresolvedError, } from "../graph/graph.js";
|
|
3
3
|
import { dispatchLifecycle } from "../lifecycle/index.js";
|
|
4
|
-
import {
|
|
4
|
+
import { loadLatestVerification } from "../persistence/index.js";
|
|
5
|
+
import { checkConfigReadiness, checkConfigReality } from "./config.js";
|
|
5
6
|
import { probeAllRequirements } from "./requirements.js";
|
|
6
7
|
function compareRef(a, b) {
|
|
7
8
|
return a < b ? -1 : a > b ? 1 : 0;
|
|
8
9
|
}
|
|
10
|
+
async function humanVerifiedModules(modules, paths) {
|
|
11
|
+
const verified = new Set();
|
|
12
|
+
if (paths === undefined)
|
|
13
|
+
return verified;
|
|
14
|
+
for (const module of modules) {
|
|
15
|
+
const latest = await loadLatestVerification(paths, module.moduleRef);
|
|
16
|
+
if (latest?.result === "PASS" &&
|
|
17
|
+
latest.moduleVersion === module.moduleVersion) {
|
|
18
|
+
verified.add(module.moduleRef);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return verified;
|
|
22
|
+
}
|
|
9
23
|
export async function runPreflight(modules, options = {}) {
|
|
10
24
|
const findings = [];
|
|
11
25
|
let dependency;
|
|
@@ -33,7 +47,9 @@ export async function runPreflight(modules, options = {}) {
|
|
|
33
47
|
}
|
|
34
48
|
}
|
|
35
49
|
findings.push(...checkConfigReadiness(modules, options.config));
|
|
36
|
-
|
|
50
|
+
findings.push(...(await checkConfigReality(modules, options.config)));
|
|
51
|
+
const verifiedHumanModules = await humanVerifiedModules(modules, options.paths);
|
|
52
|
+
const requirementProbes = await probeAllRequirements(modules, verifiedHumanModules);
|
|
37
53
|
for (const probe of requirementProbes) {
|
|
38
54
|
if (probe.status === "ACTION_REQUIRED") {
|
|
39
55
|
findings.push({
|
|
@@ -7,5 +7,5 @@ export interface RequirementProbe {
|
|
|
7
7
|
status: ProbeStatus;
|
|
8
8
|
message: string;
|
|
9
9
|
}
|
|
10
|
-
export declare function probeAllRequirements(modules: readonly ResolvedModule[]): Promise<RequirementProbe[]>;
|
|
11
|
-
export declare function probeRequirement(moduleRef: string, requirement: ModuleRequirement, modules: readonly ResolvedModule[]): Promise<RequirementProbe>;
|
|
10
|
+
export declare function probeAllRequirements(modules: readonly ResolvedModule[], humanVerifiedModuleRefs?: ReadonlySet<string>): Promise<RequirementProbe[]>;
|
|
11
|
+
export declare function probeRequirement(moduleRef: string, requirement: ModuleRequirement, modules: readonly ResolvedModule[], humanVerifiedModuleRefs?: ReadonlySet<string>): Promise<RequirementProbe>;
|
|
@@ -5,17 +5,17 @@ import { versionSatisfies } from "../modules.js";
|
|
|
5
5
|
function compareRef(a, b) {
|
|
6
6
|
return a < b ? -1 : a > b ? 1 : 0;
|
|
7
7
|
}
|
|
8
|
-
export async function probeAllRequirements(modules) {
|
|
8
|
+
export async function probeAllRequirements(modules, humanVerifiedModuleRefs = new Set()) {
|
|
9
9
|
const sorted = [...modules].sort((a, b) => compareRef(a.moduleRef, b.moduleRef));
|
|
10
10
|
const results = [];
|
|
11
11
|
for (const module of sorted) {
|
|
12
12
|
for (const requirement of module.requirements) {
|
|
13
|
-
results.push(await probeRequirement(module.moduleRef, requirement, modules));
|
|
13
|
+
results.push(await probeRequirement(module.moduleRef, requirement, modules, humanVerifiedModuleRefs));
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
return results;
|
|
17
17
|
}
|
|
18
|
-
export async function probeRequirement(moduleRef, requirement, modules) {
|
|
18
|
+
export async function probeRequirement(moduleRef, requirement, modules, humanVerifiedModuleRefs = new Set()) {
|
|
19
19
|
switch (requirement.kind) {
|
|
20
20
|
case "runtime":
|
|
21
21
|
return probeRuntime(moduleRef, requirement);
|
|
@@ -30,12 +30,19 @@ export async function probeRequirement(moduleRef, requirement, modules) {
|
|
|
30
30
|
case "module-contract":
|
|
31
31
|
return probeModuleContract(moduleRef, requirement, modules);
|
|
32
32
|
case "human":
|
|
33
|
-
return
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
return humanVerifiedModuleRefs.has(moduleRef)
|
|
34
|
+
? {
|
|
35
|
+
moduleRef,
|
|
36
|
+
requirement,
|
|
37
|
+
status: "PASS",
|
|
38
|
+
message: `human prerequisite verified for ${moduleRef}`,
|
|
39
|
+
}
|
|
40
|
+
: {
|
|
41
|
+
moduleRef,
|
|
42
|
+
requirement,
|
|
43
|
+
status: "ACTION_REQUIRED",
|
|
44
|
+
message: requirement.action,
|
|
45
|
+
};
|
|
39
46
|
}
|
|
40
47
|
}
|
|
41
48
|
function probeRuntime(moduleRef, requirement) {
|
|
@@ -6,7 +6,7 @@ export interface NpmCommandResult {
|
|
|
6
6
|
stderr: string;
|
|
7
7
|
}
|
|
8
8
|
export interface NpmCommandRunner {
|
|
9
|
-
run(args: readonly string[], cwd: string): Promise<NpmCommandResult>;
|
|
9
|
+
run(args: readonly string[], cwd: string, timeoutMs?: number): Promise<NpmCommandResult>;
|
|
10
10
|
}
|
|
11
11
|
export interface RegistryModuleCandidate {
|
|
12
12
|
packageName: string;
|
|
@@ -12,12 +12,13 @@ export const PRO_FLOW_SCOPE = "@tomflow";
|
|
|
12
12
|
export const PRO_FLOW_PACKAGE_PREFIX = "@tomflow/proflow-";
|
|
13
13
|
export function systemNpmRunner() {
|
|
14
14
|
return {
|
|
15
|
-
async run(args, cwd) {
|
|
15
|
+
async run(args, cwd, timeoutMs) {
|
|
16
16
|
try {
|
|
17
17
|
const result = await execFileAsync("npm", [...args], {
|
|
18
18
|
cwd,
|
|
19
19
|
encoding: "utf8",
|
|
20
20
|
maxBuffer: 4 * 1024 * 1024,
|
|
21
|
+
...(timeoutMs === undefined ? {} : { timeout: timeoutMs }),
|
|
21
22
|
});
|
|
22
23
|
return { stdout: result.stdout, stderr: result.stderr };
|
|
23
24
|
}
|
|
@@ -45,6 +45,32 @@ function lockedError(existing) {
|
|
|
45
45
|
: `pid=${existing.pid} planRef=${existing.planRef} since=${existing.createdAt} fingerprint=${existing.workspaceFingerprint}`;
|
|
46
46
|
return new PlatformError("WORKSPACE_LOCKED", `workspace apply already in progress (${details})`);
|
|
47
47
|
}
|
|
48
|
+
function isProvablyDeadPid(pid) {
|
|
49
|
+
try {
|
|
50
|
+
process.kill(pid, 0);
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
return errorHasCode(error, "ESRCH");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async function reclaimProvablyStaleLock(paths, expectedFingerprint) {
|
|
58
|
+
const existing = await readWorkspaceLock(paths);
|
|
59
|
+
if (existing === undefined ||
|
|
60
|
+
existing.workspaceFingerprint !== expectedFingerprint ||
|
|
61
|
+
!isProvablyDeadPid(existing.pid)) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
await unlink(workspaceLockPath(paths));
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (errorHasCode(error, "ENOENT"))
|
|
70
|
+
return true;
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
48
74
|
// v1 single-process exclusive lock: O_EXCL create, no distributed coordination,
|
|
49
75
|
// and no reclaim of a lock that cannot be proven stale.
|
|
50
76
|
export async function acquireWorkspaceLock(paths, planRef) {
|
|
@@ -61,10 +87,22 @@ export async function acquireWorkspaceLock(paths, planRef) {
|
|
|
61
87
|
handle = await open(file, "wx", 0o644);
|
|
62
88
|
}
|
|
63
89
|
catch (error) {
|
|
64
|
-
if (errorHasCode(error, "EEXIST"))
|
|
90
|
+
if (!errorHasCode(error, "EEXIST"))
|
|
91
|
+
throw error;
|
|
92
|
+
if (await reclaimProvablyStaleLock(paths, record.workspaceFingerprint)) {
|
|
93
|
+
try {
|
|
94
|
+
handle = await open(file, "wx", 0o644);
|
|
95
|
+
}
|
|
96
|
+
catch (retryError) {
|
|
97
|
+
if (errorHasCode(retryError, "EEXIST")) {
|
|
98
|
+
throw lockedError(await readWorkspaceLock(paths));
|
|
99
|
+
}
|
|
100
|
+
throw retryError;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
65
104
|
throw lockedError(await readWorkspaceLock(paths));
|
|
66
105
|
}
|
|
67
|
-
throw error;
|
|
68
106
|
}
|
|
69
107
|
try {
|
|
70
108
|
await handle.writeFile(`${JSON.stringify(record, null, 2)}\n`, {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tomflow/proflow-platform-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
"@tomflow/proflow-module-contract": "^0.1.1"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
|
-
"@tomflow/proflow-
|
|
28
|
-
"@tomflow/proflow-
|
|
27
|
+
"@tomflow/proflow-deployment-conformance": "^0.1.2",
|
|
28
|
+
"@tomflow/proflow-module-template": "^0.1.2"
|
|
29
29
|
},
|
|
30
30
|
"description": "Deterministic platform-level deployment discovery, planning, lifecycle and verification CLI.",
|
|
31
31
|
"keywords": [
|
package/proflow.module.json
CHANGED