@tomflow/proflow-platform-cli 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +24 -3
- package/dist/deployment/adapter.d.ts +5 -5
- package/dist/deployment/descriptor.d.ts +1 -1
- package/dist/deployment/descriptor.js +1 -1
- package/dist/src/apply/apply.d.ts +2 -0
- package/dist/src/apply/apply.js +47 -4
- package/dist/src/apply/current.js +14 -0
- package/dist/src/apply/driver.d.ts +1 -0
- package/dist/src/apply/driver.js +40 -12
- package/dist/src/apply/execute.js +28 -0
- package/dist/src/binding/global-binding.d.ts +69 -0
- package/dist/src/binding/global-binding.js +349 -0
- package/dist/src/binding/production-bindings.d.ts +2 -12
- package/dist/src/binding/production-bindings.js +18 -4
- package/dist/src/cli.d.ts +13 -1
- package/dist/src/cli.js +661 -40
- package/dist/src/errors.d.ts +1 -1
- package/dist/src/errors.js +13 -0
- package/dist/src/install/environment.d.ts +2 -1
- package/dist/src/install/environment.js +34 -30
- package/dist/src/install/install.js +41 -0
- package/dist/src/install/package-manager.d.ts +4 -2
- package/dist/src/install/package-manager.js +73 -17
- package/dist/src/lifecycle/dispatch.js +52 -1
- package/dist/src/lifecycle/service-process.js +32 -2
- package/dist/src/persistence/guards.js +1 -0
- package/dist/src/planner/check.js +12 -1
- package/dist/src/planner/plan.d.ts +1 -0
- package/dist/src/planner/plan.js +14 -26
- package/dist/src/preflight/config.d.ts +1 -0
- package/dist/src/preflight/config.js +43 -0
- package/dist/src/preflight/preflight.d.ts +2 -0
- package/dist/src/preflight/preflight.js +18 -2
- package/dist/src/preflight/requirements.d.ts +2 -2
- package/dist/src/preflight/requirements.js +16 -9
- package/dist/src/registry/npm-registry.d.ts +1 -1
- package/dist/src/registry/npm-registry.js +2 -1
- package/dist/src/security/lock.js +40 -2
- package/package.json +4 -4
- package/proflow.module.json +1 -1
package/dist/src/cli.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { readFile, rm } from "node:fs/promises";
|
|
3
3
|
import { parseModuleDescriptor, } from "@tomflow/proflow-module-contract";
|
|
4
4
|
import { descriptor as platformCliDescriptor } from "../deployment/descriptor.js";
|
|
5
5
|
import { applyPlan } from "./apply/apply.js";
|
|
6
6
|
import { rebuildCurrentAssumptions } from "./apply/current.js";
|
|
7
7
|
import { createWorkspacePackageManagerDriver } from "./apply/driver.js";
|
|
8
|
+
import { acquireGlobalOperationLock, canonicalizeWorkspace, claimWorkspaceBinding, clearGlobalBinding, forgetMissingWorkspaceBinding, loadGlobalBinding, observeBoundWorkspace, requireBoundWorkspace, updateGlobalBindingState, } from "./binding/global-binding.js";
|
|
8
9
|
import { buildProductionBindings, importRawAdapter, } from "./binding/production-bindings.js";
|
|
9
10
|
import { AutoModuleCatalog, discoverModules } from "./discovery/discover.js";
|
|
10
11
|
import { describeModule, readModuleDocument } from "./docs/docs.js";
|
|
@@ -52,6 +53,7 @@ function parseArgs(argv) {
|
|
|
52
53
|
workspace: undefined,
|
|
53
54
|
intent: undefined,
|
|
54
55
|
configFile: undefined,
|
|
56
|
+
forget: false,
|
|
55
57
|
positional,
|
|
56
58
|
};
|
|
57
59
|
for (let index = 1; index < argv.length; index += 1) {
|
|
@@ -59,18 +61,22 @@ function parseArgs(argv) {
|
|
|
59
61
|
if (token === undefined)
|
|
60
62
|
continue;
|
|
61
63
|
if (token === "--workspace") {
|
|
62
|
-
parsed.workspace = argv
|
|
64
|
+
parsed.workspace = requiredOptionValue(argv, index, token);
|
|
63
65
|
index += 1;
|
|
64
66
|
}
|
|
65
67
|
else if (token === "--intent") {
|
|
66
|
-
parsed.intent = argv
|
|
68
|
+
parsed.intent = requiredOptionValue(argv, index, token);
|
|
67
69
|
index += 1;
|
|
68
70
|
}
|
|
69
71
|
else if (token === "--config") {
|
|
70
|
-
parsed.configFile = argv
|
|
72
|
+
parsed.configFile = requiredOptionValue(argv, index, token);
|
|
71
73
|
index += 1;
|
|
72
74
|
}
|
|
75
|
+
else if (token === "--forget") {
|
|
76
|
+
parsed.forget = true;
|
|
77
|
+
}
|
|
73
78
|
else if (token.startsWith("-")) {
|
|
79
|
+
throw new PlatformError("INVALID_REQUEST", `unknown option: ${token}`);
|
|
74
80
|
}
|
|
75
81
|
else {
|
|
76
82
|
positional.push(token);
|
|
@@ -78,12 +84,29 @@ function parseArgs(argv) {
|
|
|
78
84
|
}
|
|
79
85
|
return parsed;
|
|
80
86
|
}
|
|
87
|
+
function requiredOptionValue(argv, index, option) {
|
|
88
|
+
const value = argv[index + 1];
|
|
89
|
+
if (value === undefined || value === "" || value.startsWith("-")) {
|
|
90
|
+
throw new PlatformError("INVALID_REQUEST", `${option} requires a value`);
|
|
91
|
+
}
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
81
94
|
function outcome(command, status, data) {
|
|
82
95
|
const result = { command, status };
|
|
83
96
|
if (data !== undefined)
|
|
84
97
|
result.data = data;
|
|
85
98
|
return result;
|
|
86
99
|
}
|
|
100
|
+
function workspaceSummary(binding) {
|
|
101
|
+
return {
|
|
102
|
+
boundWorkspace: binding.workspaceRealPath,
|
|
103
|
+
workspaceInstanceId: binding.workspaceInstanceId,
|
|
104
|
+
bindingState: binding.state,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function withWorkspace(result, binding) {
|
|
108
|
+
return { ...result, workspace: workspaceSummary(binding) };
|
|
109
|
+
}
|
|
87
110
|
function aggregateStatus(statuses) {
|
|
88
111
|
if (statuses.includes("FAILED"))
|
|
89
112
|
return "FAILED";
|
|
@@ -166,8 +189,7 @@ async function loadConfigFile(path) {
|
|
|
166
189
|
}
|
|
167
190
|
return result;
|
|
168
191
|
}
|
|
169
|
-
async function buildContext(
|
|
170
|
-
const root = workspace ?? process.cwd();
|
|
192
|
+
async function buildContext(root) {
|
|
171
193
|
const paths = workspacePaths(root);
|
|
172
194
|
const catalog = await buildCatalogWithProductionBindings(root, paths);
|
|
173
195
|
return { catalog, paths };
|
|
@@ -196,7 +218,7 @@ async function buildCatalogWithProductionBindings(root, paths) {
|
|
|
196
218
|
workspaceRoot: root,
|
|
197
219
|
modules,
|
|
198
220
|
configByModuleRef,
|
|
199
|
-
importAdapter: (packageName, source) => importRawAdapter(packageName, source),
|
|
221
|
+
importAdapter: (packageName, source) => importRawAdapter(packageName, source, root),
|
|
200
222
|
});
|
|
201
223
|
return new AutoModuleCatalog(root, bindings);
|
|
202
224
|
}
|
|
@@ -287,14 +309,41 @@ async function handleInstallerPreflight(root) {
|
|
|
287
309
|
: "BLOCKED";
|
|
288
310
|
return outcome("preflight", status, result);
|
|
289
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
|
+
}
|
|
290
332
|
async function handlePreflight(ctx, args) {
|
|
291
333
|
const modules = await discoverModules({ catalog: ctx.catalog });
|
|
292
334
|
const selected = selectModules(modules, args.positional[0]);
|
|
293
|
-
const config = await
|
|
294
|
-
const result = await runPreflight(selected, {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
:
|
|
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);
|
|
298
347
|
}
|
|
299
348
|
async function handlePlan(ctx, args) {
|
|
300
349
|
const intent = args.intent;
|
|
@@ -401,6 +450,7 @@ async function handlePlan(ctx, args) {
|
|
|
401
450
|
currentDescriptors,
|
|
402
451
|
targetDescriptors,
|
|
403
452
|
targets,
|
|
453
|
+
config,
|
|
404
454
|
});
|
|
405
455
|
await savePlan(ctx.paths, plan);
|
|
406
456
|
return outcome("plan", "SUCCEEDED", {
|
|
@@ -409,9 +459,15 @@ async function handlePlan(ctx, args) {
|
|
|
409
459
|
plan,
|
|
410
460
|
});
|
|
411
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
|
+
}
|
|
412
467
|
const preflight = await runPreflight(selected, {
|
|
413
468
|
config,
|
|
414
469
|
catalog: ctx.catalog,
|
|
470
|
+
paths: ctx.paths,
|
|
415
471
|
});
|
|
416
472
|
if (preflight.status === "NOT_READY" ||
|
|
417
473
|
preflight.status === "ACTION_REQUIRED") {
|
|
@@ -450,6 +506,11 @@ async function handleApply(ctx, args) {
|
|
|
450
506
|
driver: createWorkspacePackageManagerDriver({
|
|
451
507
|
workspaceRoot: ctx.paths.root,
|
|
452
508
|
}),
|
|
509
|
+
...(plan.intent === "upgrade"
|
|
510
|
+
? {
|
|
511
|
+
refreshCatalog: async () => (await buildContext(ctx.paths.root)).catalog,
|
|
512
|
+
}
|
|
513
|
+
: {}),
|
|
453
514
|
});
|
|
454
515
|
const status = result.outcome === "COMPLETE"
|
|
455
516
|
? "SUCCEEDED"
|
|
@@ -477,6 +538,23 @@ async function handleManagedMutation(ctx, args, intent) {
|
|
|
477
538
|
if (planned.status !== "SUCCEEDED") {
|
|
478
539
|
return { ...planned, command: intent };
|
|
479
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
|
+
}
|
|
480
558
|
const planRef = planRefFromOutcome(planned);
|
|
481
559
|
const applied = await handleApply(ctx, {
|
|
482
560
|
...args,
|
|
@@ -493,7 +571,7 @@ async function handleManagedMutation(ctx, args, intent) {
|
|
|
493
571
|
// Package mutations change Workspace package reality. Rebuild the catalog
|
|
494
572
|
// before reporting the managed set so the result can never be sourced from
|
|
495
573
|
// the pre-mutation catalog.
|
|
496
|
-
const refreshed = await buildContext(
|
|
574
|
+
const refreshed = await buildContext(ctx.paths.root);
|
|
497
575
|
const managed = await handleModules(refreshed, {
|
|
498
576
|
...args,
|
|
499
577
|
command: "modules",
|
|
@@ -506,6 +584,315 @@ async function handleManagedMutation(ctx, args, intent) {
|
|
|
506
584
|
managedModules: managed.data,
|
|
507
585
|
});
|
|
508
586
|
}
|
|
587
|
+
async function resolveRequestedInstallWorkspace(args, runtime) {
|
|
588
|
+
const requested = args.workspace ?? runtime.cwd ?? process.cwd();
|
|
589
|
+
return (await canonicalizeWorkspace(requested)).workspaceRealPath;
|
|
590
|
+
}
|
|
591
|
+
async function handleInstallPlanBeforeBinding(args, runtime) {
|
|
592
|
+
const requestedWorkspace = await resolveRequestedInstallWorkspace(args, runtime);
|
|
593
|
+
const current = await loadGlobalBinding(runtime.globalRoot);
|
|
594
|
+
if (current !== undefined &&
|
|
595
|
+
current.workspaceRealPath !== requestedWorkspace) {
|
|
596
|
+
throw new PlatformError("WORKSPACE_ALREADY_BOUND", `ProFlow is already bound to ${current.workspaceRealPath}; uninstall it before planning an install for ${requestedWorkspace}`);
|
|
597
|
+
}
|
|
598
|
+
const ctx = await buildContext(current?.workspaceRealPath ?? requestedWorkspace);
|
|
599
|
+
const result = await handlePlan(ctx, args);
|
|
600
|
+
return current === undefined ? result : withWorkspace(result, current);
|
|
601
|
+
}
|
|
602
|
+
async function handleApplyWithGlobalBinding(args, runtime) {
|
|
603
|
+
const planRef = args.positional[0];
|
|
604
|
+
if (planRef === undefined) {
|
|
605
|
+
throw new PlatformError("INVALID_REQUEST", "apply requires <planRef>");
|
|
606
|
+
}
|
|
607
|
+
const operationLock = await acquireGlobalOperationLock(runtime.globalRoot);
|
|
608
|
+
let binding = await loadGlobalBinding(runtime.globalRoot);
|
|
609
|
+
try {
|
|
610
|
+
let ctx;
|
|
611
|
+
if (binding === undefined) {
|
|
612
|
+
const requestedWorkspace = await resolveRequestedInstallWorkspace(args, runtime);
|
|
613
|
+
ctx = await buildContext(requestedWorkspace);
|
|
614
|
+
const plan = await loadPlan(ctx.paths, planRef);
|
|
615
|
+
if (plan === undefined) {
|
|
616
|
+
throw new PlatformError("PLAN_NOT_FOUND", `plan ${planRef} not found`);
|
|
617
|
+
}
|
|
618
|
+
if (plan.intent !== "install") {
|
|
619
|
+
throw new PlatformError("WORKSPACE_NOT_BOUND", "only an install plan may establish the first global Workspace binding");
|
|
620
|
+
}
|
|
621
|
+
binding = (await claimWorkspaceBinding({
|
|
622
|
+
workspace: requestedWorkspace,
|
|
623
|
+
globalRoot: runtime.globalRoot,
|
|
624
|
+
})).binding;
|
|
625
|
+
}
|
|
626
|
+
else {
|
|
627
|
+
if (args.workspace !== undefined) {
|
|
628
|
+
const requested = await canonicalizeWorkspace(args.workspace);
|
|
629
|
+
if (requested.workspaceRealPath !== binding.workspaceRealPath) {
|
|
630
|
+
throw new PlatformError("WORKSPACE_ALREADY_BOUND", `command targets ${requested.workspaceRealPath}, but the global Platform Instance is bound to ${binding.workspaceRealPath}`);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
ctx = await buildContext(binding.workspaceRealPath);
|
|
634
|
+
}
|
|
635
|
+
const plan = await loadPlan(ctx.paths, planRef);
|
|
636
|
+
if (plan === undefined) {
|
|
637
|
+
throw new PlatformError("PLAN_NOT_FOUND", `plan ${planRef} not found`);
|
|
638
|
+
}
|
|
639
|
+
const controlsPlatformState = plan.intent === "install" && binding.state !== "INSTALLED";
|
|
640
|
+
if (binding.state === "UNINSTALLING") {
|
|
641
|
+
throw new PlatformError("GLOBAL_OPERATION_LOCKED", "the bound Platform Instance is marked UNINSTALLING; finish recovery before apply");
|
|
642
|
+
}
|
|
643
|
+
if (controlsPlatformState && binding.state !== "INSTALLING") {
|
|
644
|
+
binding = await updateGlobalBindingState({
|
|
645
|
+
workspaceInstanceId: binding.workspaceInstanceId,
|
|
646
|
+
state: "INSTALLING",
|
|
647
|
+
globalRoot: runtime.globalRoot,
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
try {
|
|
651
|
+
const result = await handleApply(ctx, args);
|
|
652
|
+
if (controlsPlatformState) {
|
|
653
|
+
if (result.status === "SUCCEEDED") {
|
|
654
|
+
binding = await updateGlobalBindingState({
|
|
655
|
+
workspaceInstanceId: binding.workspaceInstanceId,
|
|
656
|
+
state: "INSTALLED",
|
|
657
|
+
globalRoot: runtime.globalRoot,
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
else if (result.status === "FAILED" || result.status === "BLOCKED") {
|
|
661
|
+
binding = await updateGlobalBindingState({
|
|
662
|
+
workspaceInstanceId: binding.workspaceInstanceId,
|
|
663
|
+
state: "BROKEN",
|
|
664
|
+
globalRoot: runtime.globalRoot,
|
|
665
|
+
failure: {
|
|
666
|
+
code: result.error?.code ?? result.status,
|
|
667
|
+
message: result.error?.message ??
|
|
668
|
+
"install apply did not reach a successful postcondition",
|
|
669
|
+
},
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
return withWorkspace(result, binding);
|
|
674
|
+
}
|
|
675
|
+
catch (error) {
|
|
676
|
+
if (controlsPlatformState) {
|
|
677
|
+
binding = await updateGlobalBindingState({
|
|
678
|
+
workspaceInstanceId: binding.workspaceInstanceId,
|
|
679
|
+
state: "BROKEN",
|
|
680
|
+
globalRoot: runtime.globalRoot,
|
|
681
|
+
failure: {
|
|
682
|
+
code: error instanceof PlatformError ? error.code : "COMMAND_FAILED",
|
|
683
|
+
message: error instanceof Error ? error.message : String(error),
|
|
684
|
+
},
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
throw error;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
finally {
|
|
691
|
+
await operationLock.release();
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
async function handleInstallWithGlobalBinding(args, runtime) {
|
|
695
|
+
const operationLock = await acquireGlobalOperationLock(runtime.globalRoot);
|
|
696
|
+
try {
|
|
697
|
+
const requestedWorkspace = await resolveRequestedInstallWorkspace(args, runtime);
|
|
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) {
|
|
704
|
+
return withWorkspace(outcome("install", "SUCCEEDED", {
|
|
705
|
+
alreadyInstalled: true,
|
|
706
|
+
changed: false,
|
|
707
|
+
boundWorkspace: existing.workspaceRealPath,
|
|
708
|
+
}), existing);
|
|
709
|
+
}
|
|
710
|
+
if (existing?.state === "UNINSTALLING") {
|
|
711
|
+
throw new PlatformError("GLOBAL_OPERATION_LOCKED", "the bound Platform Instance is marked UNINSTALLING; finish recovery before installing");
|
|
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;
|
|
729
|
+
let activeBinding = initialBinding;
|
|
730
|
+
const controlsPlatformState = !claimed.alreadyBound || initialBinding.state !== "INSTALLED";
|
|
731
|
+
if (controlsPlatformState && initialBinding.state !== "INSTALLING") {
|
|
732
|
+
activeBinding = await updateGlobalBindingState({
|
|
733
|
+
workspaceInstanceId: initialBinding.workspaceInstanceId,
|
|
734
|
+
state: "INSTALLING",
|
|
735
|
+
globalRoot: runtime.globalRoot,
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
try {
|
|
739
|
+
const ctx = await buildContext(activeBinding.workspaceRealPath);
|
|
740
|
+
const result = await handleManagedMutation(ctx, args, "install");
|
|
741
|
+
if (controlsPlatformState) {
|
|
742
|
+
if (result.status === "SUCCEEDED") {
|
|
743
|
+
activeBinding = await updateGlobalBindingState({
|
|
744
|
+
workspaceInstanceId: activeBinding.workspaceInstanceId,
|
|
745
|
+
state: "INSTALLED",
|
|
746
|
+
globalRoot: runtime.globalRoot,
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
else if (result.status === "FAILED" || result.status === "BLOCKED") {
|
|
750
|
+
activeBinding = await updateGlobalBindingState({
|
|
751
|
+
workspaceInstanceId: activeBinding.workspaceInstanceId,
|
|
752
|
+
state: "BROKEN",
|
|
753
|
+
globalRoot: runtime.globalRoot,
|
|
754
|
+
failure: {
|
|
755
|
+
code: result.error?.code ?? result.status,
|
|
756
|
+
message: result.error?.message ??
|
|
757
|
+
"Platform install did not reach a successful postcondition",
|
|
758
|
+
},
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
return withWorkspace(result, activeBinding);
|
|
763
|
+
}
|
|
764
|
+
catch (error) {
|
|
765
|
+
if (controlsPlatformState) {
|
|
766
|
+
activeBinding = await updateGlobalBindingState({
|
|
767
|
+
workspaceInstanceId: activeBinding.workspaceInstanceId,
|
|
768
|
+
state: "BROKEN",
|
|
769
|
+
globalRoot: runtime.globalRoot,
|
|
770
|
+
failure: {
|
|
771
|
+
code: error instanceof PlatformError ? error.code : "COMMAND_FAILED",
|
|
772
|
+
message: error instanceof Error ? error.message : String(error),
|
|
773
|
+
},
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
throw error;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
finally {
|
|
780
|
+
await operationLock.release();
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
async function resolveBoundContext(runtime, requestedWorkspace) {
|
|
784
|
+
const binding = await requireBoundWorkspace(runtime.globalRoot);
|
|
785
|
+
if (requestedWorkspace !== undefined) {
|
|
786
|
+
const requested = await canonicalizeWorkspace(requestedWorkspace);
|
|
787
|
+
if (requested.workspaceRealPath !== binding.workspaceRealPath) {
|
|
788
|
+
throw new PlatformError("WORKSPACE_ALREADY_BOUND", `command targets ${requested.workspaceRealPath}, but the global Platform Instance is bound to ${binding.workspaceRealPath}`);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
return {
|
|
792
|
+
ctx: await buildContext(binding.workspaceRealPath),
|
|
793
|
+
binding,
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
async function handleBoundMutatingCommand(args, runtime) {
|
|
797
|
+
const operationLock = await acquireGlobalOperationLock(runtime.globalRoot);
|
|
798
|
+
try {
|
|
799
|
+
const { ctx, binding } = await resolveBoundContext(runtime, args.workspace);
|
|
800
|
+
if (binding.state === "UNINSTALLING") {
|
|
801
|
+
throw new PlatformError("GLOBAL_OPERATION_LOCKED", "the bound Platform Instance is marked UNINSTALLING; finish uninstall recovery before another mutation");
|
|
802
|
+
}
|
|
803
|
+
let result;
|
|
804
|
+
if (args.command === "upgrade" || args.command === "uninstall") {
|
|
805
|
+
result = await handleManagedMutation(ctx, args, args.command);
|
|
806
|
+
}
|
|
807
|
+
else {
|
|
808
|
+
result = await handleLifecycle(ctx, args, args.command);
|
|
809
|
+
}
|
|
810
|
+
return withWorkspace(result, binding);
|
|
811
|
+
}
|
|
812
|
+
finally {
|
|
813
|
+
await operationLock.release();
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
async function handlePlatformInstanceUninstall(args, runtime) {
|
|
817
|
+
const operationLock = await acquireGlobalOperationLock(runtime.globalRoot);
|
|
818
|
+
let binding;
|
|
819
|
+
try {
|
|
820
|
+
binding = await requireBoundWorkspace(runtime.globalRoot);
|
|
821
|
+
if (args.workspace !== undefined) {
|
|
822
|
+
const requested = await canonicalizeWorkspace(args.workspace);
|
|
823
|
+
if (requested.workspaceRealPath !== binding.workspaceRealPath) {
|
|
824
|
+
throw new PlatformError("WORKSPACE_ALREADY_BOUND", `uninstall targets ${requested.workspaceRealPath}, but the global Platform Instance is bound to ${binding.workspaceRealPath}`);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
binding = await updateGlobalBindingState({
|
|
828
|
+
workspaceInstanceId: binding.workspaceInstanceId,
|
|
829
|
+
state: "UNINSTALLING",
|
|
830
|
+
globalRoot: runtime.globalRoot,
|
|
831
|
+
});
|
|
832
|
+
const ctx = await buildContext(binding.workspaceRealPath);
|
|
833
|
+
const modules = await discoverModules({ catalog: ctx.catalog });
|
|
834
|
+
let removedModules = [];
|
|
835
|
+
if (modules.length > 0) {
|
|
836
|
+
const plan = planDeployment({
|
|
837
|
+
intent: "uninstall",
|
|
838
|
+
modules,
|
|
839
|
+
uninstallScope: "platform-instance",
|
|
840
|
+
});
|
|
841
|
+
await savePlan(ctx.paths, plan);
|
|
842
|
+
const current = await rebuildCurrentAssumptions(ctx.catalog, plan);
|
|
843
|
+
const applied = await applyPlan({
|
|
844
|
+
paths: ctx.paths,
|
|
845
|
+
planRef: plan.planRef,
|
|
846
|
+
catalog: ctx.catalog,
|
|
847
|
+
current,
|
|
848
|
+
driver: createWorkspacePackageManagerDriver({
|
|
849
|
+
workspaceRoot: ctx.paths.root,
|
|
850
|
+
}),
|
|
851
|
+
});
|
|
852
|
+
if (applied.outcome !== "COMPLETE") {
|
|
853
|
+
throw new PlatformError("UNINSTALL_FAILED", `Platform Instance uninstall stopped with ${applied.outcome}`);
|
|
854
|
+
}
|
|
855
|
+
removedModules = modules.map((module) => module.moduleRef);
|
|
856
|
+
}
|
|
857
|
+
const refreshed = await buildContext(binding.workspaceRealPath);
|
|
858
|
+
const remaining = await discoverModules({ catalog: refreshed.catalog });
|
|
859
|
+
if (remaining.length > 0) {
|
|
860
|
+
throw new PlatformError("UNINSTALL_FAILED", `Platform Instance uninstall left managed modules: ${remaining.map((module) => module.moduleRef).join(", ")}`);
|
|
861
|
+
}
|
|
862
|
+
// Deployment-owned plans/state/verification/instance identity must not leak
|
|
863
|
+
// into a future install of the same directory. Business/domain data outside
|
|
864
|
+
// `.proflow/deployment` is intentionally preserved.
|
|
865
|
+
await rm(ctx.paths.deployment, { recursive: true, force: true });
|
|
866
|
+
const completed = binding;
|
|
867
|
+
await clearGlobalBinding({
|
|
868
|
+
workspaceInstanceId: binding.workspaceInstanceId,
|
|
869
|
+
globalRoot: runtime.globalRoot,
|
|
870
|
+
});
|
|
871
|
+
binding = undefined;
|
|
872
|
+
return outcome("uninstall", "SUCCEEDED", {
|
|
873
|
+
uninstalledWorkspace: completed.workspaceRealPath,
|
|
874
|
+
removedModules,
|
|
875
|
+
bindingCleared: true,
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
catch (error) {
|
|
879
|
+
if (binding !== undefined) {
|
|
880
|
+
await updateGlobalBindingState({
|
|
881
|
+
workspaceInstanceId: binding.workspaceInstanceId,
|
|
882
|
+
state: "BROKEN",
|
|
883
|
+
globalRoot: runtime.globalRoot,
|
|
884
|
+
failure: {
|
|
885
|
+
code: error instanceof PlatformError ? error.code : "UNINSTALL_FAILED",
|
|
886
|
+
message: error instanceof Error ? error.message : String(error),
|
|
887
|
+
},
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
throw error;
|
|
891
|
+
}
|
|
892
|
+
finally {
|
|
893
|
+
await operationLock.release();
|
|
894
|
+
}
|
|
895
|
+
}
|
|
509
896
|
async function handleLifecycle(ctx, args, primitive) {
|
|
510
897
|
const modules = await discoverModules({ catalog: ctx.catalog });
|
|
511
898
|
const selected = selectModules(modules, args.positional[0]);
|
|
@@ -572,7 +959,7 @@ async function handleDoctor(ctx, args) {
|
|
|
572
959
|
async function handleManifest(ctx, args) {
|
|
573
960
|
const modules = await discoverModules({ catalog: ctx.catalog });
|
|
574
961
|
const selected = selectModules(modules, args.positional[0]);
|
|
575
|
-
const config = await
|
|
962
|
+
const config = await loadEffectiveConfig(ctx, selected, args.configFile);
|
|
576
963
|
const manifest = await buildManifest({
|
|
577
964
|
catalog: ctx.catalog,
|
|
578
965
|
modules: selected,
|
|
@@ -591,8 +978,14 @@ async function handleManifest(ctx, args) {
|
|
|
591
978
|
return outcome("manifest", "FAILED", manifest);
|
|
592
979
|
}
|
|
593
980
|
}
|
|
594
|
-
async function dispatchCommand(argv) {
|
|
595
|
-
|
|
981
|
+
async function dispatchCommand(argv, runtime = {}) {
|
|
982
|
+
let args;
|
|
983
|
+
try {
|
|
984
|
+
args = parseArgs(argv);
|
|
985
|
+
}
|
|
986
|
+
catch (error) {
|
|
987
|
+
return failure(argv[0] ?? "", error);
|
|
988
|
+
}
|
|
596
989
|
if (!COMMANDS.includes(args.command)) {
|
|
597
990
|
return {
|
|
598
991
|
command: args.command,
|
|
@@ -604,47 +997,143 @@ async function dispatchCommand(argv) {
|
|
|
604
997
|
};
|
|
605
998
|
}
|
|
606
999
|
try {
|
|
607
|
-
const
|
|
608
|
-
if (args.command === "search")
|
|
1000
|
+
const cwd = runtime.cwd ?? process.cwd();
|
|
1001
|
+
if (args.command === "search") {
|
|
1002
|
+
const current = await loadGlobalBinding(runtime.globalRoot);
|
|
1003
|
+
const root = args.workspace ?? current?.workspaceRealPath ?? cwd;
|
|
609
1004
|
return await handleSearch(root, args);
|
|
1005
|
+
}
|
|
610
1006
|
if (args.command === "preflight" && args.intent === "install") {
|
|
1007
|
+
const root = await resolveRequestedInstallWorkspace(args, runtime);
|
|
1008
|
+
const current = await loadGlobalBinding(runtime.globalRoot);
|
|
1009
|
+
if (current !== undefined && current.workspaceRealPath !== root) {
|
|
1010
|
+
throw new PlatformError("WORKSPACE_ALREADY_BOUND", `ProFlow is already bound to ${current.workspaceRealPath}; uninstall it before installing ${root}`);
|
|
1011
|
+
}
|
|
611
1012
|
return await handleInstallerPreflight(root);
|
|
612
1013
|
}
|
|
613
|
-
|
|
1014
|
+
if (args.command === "plan") {
|
|
1015
|
+
if (args.intent === "install") {
|
|
1016
|
+
return await handleInstallPlanBeforeBinding(args, runtime);
|
|
1017
|
+
}
|
|
1018
|
+
if (args.intent === undefined ||
|
|
1019
|
+
!["configure", "upgrade", "uninstall", "repair"].includes(args.intent)) {
|
|
1020
|
+
const current = await loadGlobalBinding(runtime.globalRoot);
|
|
1021
|
+
const root = args.workspace ??
|
|
1022
|
+
current?.workspaceRealPath ??
|
|
1023
|
+
runtime.cwd ??
|
|
1024
|
+
process.cwd();
|
|
1025
|
+
return await handlePlan(await buildContext(root), args);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
if (args.command === "apply") {
|
|
1029
|
+
return await handleApplyWithGlobalBinding(args, runtime);
|
|
1030
|
+
}
|
|
1031
|
+
if (args.command === "install") {
|
|
1032
|
+
return await handleInstallWithGlobalBinding(args, runtime);
|
|
1033
|
+
}
|
|
1034
|
+
if (args.command === "status") {
|
|
1035
|
+
const observation = await observeBoundWorkspace(runtime.globalRoot);
|
|
1036
|
+
if (observation === undefined) {
|
|
1037
|
+
return outcome("status", "SUCCEEDED", {
|
|
1038
|
+
installed: false,
|
|
1039
|
+
bindingState: "UNBOUND",
|
|
1040
|
+
boundWorkspace: null,
|
|
1041
|
+
nextAction: "Run platform install [--workspace <path>]",
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
if (!observation.workspaceExists) {
|
|
1045
|
+
return withWorkspace(outcome("status", "BLOCKED", {
|
|
1046
|
+
installed: false,
|
|
1047
|
+
code: "BOUND_WORKSPACE_MISSING",
|
|
1048
|
+
boundWorkspace: observation.binding.workspaceRealPath,
|
|
1049
|
+
nextAction: "Restore the Workspace or run platform uninstall --forget to clear only the stale binding",
|
|
1050
|
+
}), observation.binding);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
if (args.command === "uninstall" && args.forget) {
|
|
1054
|
+
if (args.positional.length > 0 || args.workspace !== undefined) {
|
|
1055
|
+
throw new PlatformError("INVALID_REQUEST", "platform uninstall --forget only clears the current stale global binding; do not combine it with a module/package or --workspace");
|
|
1056
|
+
}
|
|
1057
|
+
const operationLock = await acquireGlobalOperationLock(runtime.globalRoot);
|
|
1058
|
+
try {
|
|
1059
|
+
const forgotten = await forgetMissingWorkspaceBinding({
|
|
1060
|
+
globalRoot: runtime.globalRoot,
|
|
1061
|
+
});
|
|
1062
|
+
return outcome("uninstall", "SUCCEEDED", {
|
|
1063
|
+
forgottenWorkspace: forgotten.workspaceRealPath,
|
|
1064
|
+
bindingCleared: true,
|
|
1065
|
+
resourcesCleaned: false,
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
1068
|
+
finally {
|
|
1069
|
+
await operationLock.release();
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
if (args.command === "uninstall" && args.positional.length === 0) {
|
|
1073
|
+
const current = await loadGlobalBinding(runtime.globalRoot);
|
|
1074
|
+
if (current === undefined) {
|
|
1075
|
+
return outcome("uninstall", "SUCCEEDED", {
|
|
1076
|
+
alreadyUninstalled: true,
|
|
1077
|
+
bindingCleared: true,
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
return await handlePlatformInstanceUninstall(args, runtime);
|
|
1081
|
+
}
|
|
1082
|
+
if (args.command === "upgrade" ||
|
|
1083
|
+
(args.command === "uninstall" && args.positional.length > 0) ||
|
|
1084
|
+
args.command === "start" ||
|
|
1085
|
+
args.command === "stop" ||
|
|
1086
|
+
args.command === "restart") {
|
|
1087
|
+
return await handleBoundMutatingCommand(args, runtime);
|
|
1088
|
+
}
|
|
1089
|
+
const { ctx, binding } = await resolveBoundContext(runtime, args.workspace);
|
|
1090
|
+
let result;
|
|
614
1091
|
switch (args.command) {
|
|
615
1092
|
case "search":
|
|
616
|
-
|
|
1093
|
+
case "install":
|
|
1094
|
+
throw new PlatformError("COMMAND_FAILED", "unreachable command routing");
|
|
617
1095
|
case "modules":
|
|
618
|
-
|
|
1096
|
+
result = await handleModules(ctx, args);
|
|
1097
|
+
break;
|
|
619
1098
|
case "docs":
|
|
620
|
-
|
|
621
|
-
|
|
1099
|
+
result = await handleDocs(ctx, args);
|
|
1100
|
+
break;
|
|
622
1101
|
case "uninstall":
|
|
623
1102
|
case "upgrade":
|
|
624
|
-
return await handleManagedMutation(ctx, args, args.command);
|
|
625
|
-
case "preflight":
|
|
626
|
-
return await handlePreflight(ctx, args);
|
|
627
|
-
case "plan":
|
|
628
|
-
return await handlePlan(ctx, args);
|
|
629
|
-
case "apply":
|
|
630
|
-
return await handleApply(ctx, args);
|
|
631
1103
|
case "start":
|
|
632
1104
|
case "stop":
|
|
633
1105
|
case "restart":
|
|
1106
|
+
throw new PlatformError("COMMAND_FAILED", "unreachable mutating command routing");
|
|
1107
|
+
case "preflight":
|
|
1108
|
+
result = await handlePreflight(ctx, args);
|
|
1109
|
+
break;
|
|
1110
|
+
case "plan":
|
|
1111
|
+
result = await handlePlan(ctx, args);
|
|
1112
|
+
break;
|
|
1113
|
+
case "apply":
|
|
1114
|
+
result = await handleApply(ctx, args);
|
|
1115
|
+
break;
|
|
634
1116
|
case "status":
|
|
635
|
-
|
|
1117
|
+
result = await handleLifecycle(ctx, args, "status");
|
|
1118
|
+
break;
|
|
636
1119
|
case "verify":
|
|
637
|
-
|
|
1120
|
+
result = await handleVerify(ctx, args);
|
|
1121
|
+
break;
|
|
638
1122
|
case "doctor":
|
|
639
|
-
|
|
1123
|
+
result = await handleDoctor(ctx, args);
|
|
1124
|
+
break;
|
|
640
1125
|
case "manifest":
|
|
641
|
-
|
|
1126
|
+
result = await handleManifest(ctx, args);
|
|
1127
|
+
break;
|
|
1128
|
+
}
|
|
1129
|
+
if (result === undefined) {
|
|
1130
|
+
throw new PlatformError("COMMAND_FAILED", "unreachable command routing");
|
|
642
1131
|
}
|
|
1132
|
+
return withWorkspace(result, binding);
|
|
643
1133
|
}
|
|
644
1134
|
catch (error) {
|
|
645
1135
|
return failure(args.command, error);
|
|
646
1136
|
}
|
|
647
|
-
return failure(args.command, new PlatformError("COMMAND_FAILED", "unreachable"));
|
|
648
1137
|
}
|
|
649
1138
|
function toMachineResult(outcomeResult) {
|
|
650
1139
|
const ok = outcomeResult.status === "SUCCEEDED";
|
|
@@ -654,6 +1143,9 @@ function toMachineResult(outcomeResult) {
|
|
|
654
1143
|
status: outcomeResult.status,
|
|
655
1144
|
moduleRef: MODULE_REF,
|
|
656
1145
|
moduleVersion: MODULE_VERSION,
|
|
1146
|
+
...(outcomeResult.workspace !== undefined
|
|
1147
|
+
? { workspace: outcomeResult.workspace }
|
|
1148
|
+
: {}),
|
|
657
1149
|
...(outcomeResult.data !== undefined ? { data: outcomeResult.data } : {}),
|
|
658
1150
|
...(outcomeResult.status === "ACTION_REQUIRED"
|
|
659
1151
|
? {
|
|
@@ -674,7 +1166,125 @@ function toMachineResult(outcomeResult) {
|
|
|
674
1166
|
: {}),
|
|
675
1167
|
});
|
|
676
1168
|
}
|
|
677
|
-
|
|
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
|
+
}
|
|
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
|
+
}
|
|
678
1288
|
if (argv.includes("--help") || argv.includes("-h")) {
|
|
679
1289
|
return JSON.stringify({
|
|
680
1290
|
contract: "deployment.result.v1",
|
|
@@ -685,10 +1295,19 @@ export async function runCli(argv) {
|
|
|
685
1295
|
data: {
|
|
686
1296
|
usage: "platform <command> [module|package] [--workspace <path>] [--json]",
|
|
687
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
|
+
],
|
|
688
1308
|
},
|
|
689
1309
|
});
|
|
690
1310
|
}
|
|
691
|
-
const filtered = argv.filter((argument) => argument !== "--json");
|
|
692
1311
|
if (filtered.length === 0) {
|
|
693
1312
|
return JSON.stringify({
|
|
694
1313
|
contract: "deployment.result.v1",
|
|
@@ -698,11 +1317,13 @@ export async function runCli(argv) {
|
|
|
698
1317
|
moduleVersion: MODULE_VERSION,
|
|
699
1318
|
});
|
|
700
1319
|
}
|
|
701
|
-
return toMachineResult(await dispatchCommand(filtered));
|
|
1320
|
+
return toMachineResult(await dispatchCommand(filtered, runtime));
|
|
702
1321
|
}
|
|
703
1322
|
if (import.meta.main) {
|
|
704
|
-
const
|
|
705
|
-
|
|
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`);
|
|
706
1327
|
const parsed = JSON.parse(output);
|
|
707
1328
|
if (typeof parsed === "object" &&
|
|
708
1329
|
parsed !== null &&
|