@tomflow/proflow-platform-cli 0.1.1 → 0.1.2

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.
@@ -6,6 +6,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
6
6
  }
7
7
  return path;
8
8
  };
9
+ import { createRequire } from "node:module";
9
10
  import { join } from "node:path";
10
11
  import { pathToFileURL } from "node:url";
11
12
  import { managedServiceStatus, restartManagedService, startManagedService, stopManagedService, } from "../lifecycle/service-process.js";
@@ -21,15 +22,16 @@ function managedServiceAdapter(workspaceRoot, module, serviceProcess, probeAdapt
21
22
  uninstall: () => stopManagedService(paths, module),
22
23
  };
23
24
  }
24
- export async function importRawAdapter(packageName, source) {
25
+ export async function importRawAdapter(packageName, source, workspaceRoot) {
25
26
  if (source.type === "workspace") {
26
27
  if (source.path === undefined)
27
28
  return {};
28
29
  const url = pathToFileURL(join(source.path, "deployment", "adapter.ts"));
29
30
  return (await /* architecture-allow-local-file-url-import */ import(__rewriteRelativeImportExtension(url.href)));
30
31
  }
31
- const resolved = import.meta.resolve(`${packageName}/deployment/adapter`);
32
- const url = new URL(resolved);
32
+ const workspaceRequire = createRequire(pathToFileURL(join(workspaceRoot, "package.json")));
33
+ const resolved = workspaceRequire.resolve(`${packageName}/deployment/adapter`);
34
+ const url = pathToFileURL(resolved);
33
35
  return (await /* architecture-allow-local-file-url-import */ import(__rewriteRelativeImportExtension(url.href)));
34
36
  }
35
37
  /**
package/dist/src/cli.d.ts CHANGED
@@ -1,12 +1,23 @@
1
1
  #!/usr/bin/env node
2
+ import { type GlobalWorkspaceBinding } from "./binding/global-binding.ts";
2
3
  export type CliStatus = "SUCCEEDED" | "ACTION_REQUIRED" | "BLOCKED" | "FAILED";
4
+ export interface CliWorkspaceSummary {
5
+ boundWorkspace: string;
6
+ workspaceInstanceId: string;
7
+ bindingState: GlobalWorkspaceBinding["state"];
8
+ }
3
9
  export interface CliOutcome {
4
10
  command: string;
5
11
  status: CliStatus;
6
12
  data?: unknown;
13
+ workspace?: CliWorkspaceSummary;
7
14
  error?: {
8
15
  code: string;
9
16
  message: string;
10
17
  };
11
18
  }
12
- export declare function runCli(argv: readonly string[]): Promise<string>;
19
+ export interface CliRuntimeOptions {
20
+ cwd?: string;
21
+ globalRoot?: string;
22
+ }
23
+ export declare function runCli(argv: readonly string[], runtime?: CliRuntimeOptions): Promise<string>;
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[index + 1];
64
+ parsed.workspace = requiredOptionValue(argv, index, token);
63
65
  index += 1;
64
66
  }
65
67
  else if (token === "--intent") {
66
- parsed.intent = argv[index + 1];
68
+ parsed.intent = requiredOptionValue(argv, index, token);
67
69
  index += 1;
68
70
  }
69
71
  else if (token === "--config") {
70
- parsed.configFile = argv[index + 1];
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.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(workspace) {
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
  }
@@ -493,7 +515,7 @@ async function handleManagedMutation(ctx, args, intent) {
493
515
  // Package mutations change Workspace package reality. Rebuild the catalog
494
516
  // before reporting the managed set so the result can never be sourced from
495
517
  // the pre-mutation catalog.
496
- const refreshed = await buildContext(args.workspace);
518
+ const refreshed = await buildContext(ctx.paths.root);
497
519
  const managed = await handleModules(refreshed, {
498
520
  ...args,
499
521
  command: "modules",
@@ -506,6 +528,301 @@ async function handleManagedMutation(ctx, args, intent) {
506
528
  managedModules: managed.data,
507
529
  });
508
530
  }
531
+ async function resolveRequestedInstallWorkspace(args, runtime) {
532
+ const requested = args.workspace ?? runtime.cwd ?? process.cwd();
533
+ return (await canonicalizeWorkspace(requested)).workspaceRealPath;
534
+ }
535
+ async function handleInstallPlanBeforeBinding(args, runtime) {
536
+ const requestedWorkspace = await resolveRequestedInstallWorkspace(args, runtime);
537
+ const current = await loadGlobalBinding(runtime.globalRoot);
538
+ if (current !== undefined &&
539
+ current.workspaceRealPath !== requestedWorkspace) {
540
+ throw new PlatformError("WORKSPACE_ALREADY_BOUND", `ProFlow is already bound to ${current.workspaceRealPath}; uninstall it before planning an install for ${requestedWorkspace}`);
541
+ }
542
+ const ctx = await buildContext(current?.workspaceRealPath ?? requestedWorkspace);
543
+ const result = await handlePlan(ctx, args);
544
+ return current === undefined ? result : withWorkspace(result, current);
545
+ }
546
+ async function handleApplyWithGlobalBinding(args, runtime) {
547
+ const planRef = args.positional[0];
548
+ if (planRef === undefined) {
549
+ throw new PlatformError("INVALID_REQUEST", "apply requires <planRef>");
550
+ }
551
+ const operationLock = await acquireGlobalOperationLock(runtime.globalRoot);
552
+ let binding = await loadGlobalBinding(runtime.globalRoot);
553
+ try {
554
+ let ctx;
555
+ if (binding === undefined) {
556
+ const requestedWorkspace = await resolveRequestedInstallWorkspace(args, runtime);
557
+ ctx = await buildContext(requestedWorkspace);
558
+ const plan = await loadPlan(ctx.paths, planRef);
559
+ if (plan === undefined) {
560
+ throw new PlatformError("PLAN_NOT_FOUND", `plan ${planRef} not found`);
561
+ }
562
+ if (plan.intent !== "install") {
563
+ throw new PlatformError("WORKSPACE_NOT_BOUND", "only an install plan may establish the first global Workspace binding");
564
+ }
565
+ binding = (await claimWorkspaceBinding({
566
+ workspace: requestedWorkspace,
567
+ globalRoot: runtime.globalRoot,
568
+ })).binding;
569
+ }
570
+ else {
571
+ if (args.workspace !== undefined) {
572
+ const requested = await canonicalizeWorkspace(args.workspace);
573
+ if (requested.workspaceRealPath !== binding.workspaceRealPath) {
574
+ throw new PlatformError("WORKSPACE_ALREADY_BOUND", `command targets ${requested.workspaceRealPath}, but the global Platform Instance is bound to ${binding.workspaceRealPath}`);
575
+ }
576
+ }
577
+ ctx = await buildContext(binding.workspaceRealPath);
578
+ }
579
+ const plan = await loadPlan(ctx.paths, planRef);
580
+ if (plan === undefined) {
581
+ throw new PlatformError("PLAN_NOT_FOUND", `plan ${planRef} not found`);
582
+ }
583
+ const controlsPlatformState = plan.intent === "install" && binding.state !== "INSTALLED";
584
+ if (binding.state === "UNINSTALLING") {
585
+ throw new PlatformError("GLOBAL_OPERATION_LOCKED", "the bound Platform Instance is marked UNINSTALLING; finish recovery before apply");
586
+ }
587
+ if (controlsPlatformState && binding.state !== "INSTALLING") {
588
+ binding = await updateGlobalBindingState({
589
+ workspaceInstanceId: binding.workspaceInstanceId,
590
+ state: "INSTALLING",
591
+ globalRoot: runtime.globalRoot,
592
+ });
593
+ }
594
+ try {
595
+ const result = await handleApply(ctx, args);
596
+ if (controlsPlatformState) {
597
+ if (result.status === "SUCCEEDED") {
598
+ binding = await updateGlobalBindingState({
599
+ workspaceInstanceId: binding.workspaceInstanceId,
600
+ state: "INSTALLED",
601
+ globalRoot: runtime.globalRoot,
602
+ });
603
+ }
604
+ else if (result.status === "FAILED" || result.status === "BLOCKED") {
605
+ binding = await updateGlobalBindingState({
606
+ workspaceInstanceId: binding.workspaceInstanceId,
607
+ state: "BROKEN",
608
+ globalRoot: runtime.globalRoot,
609
+ failure: {
610
+ code: result.error?.code ?? result.status,
611
+ message: result.error?.message ??
612
+ "install apply did not reach a successful postcondition",
613
+ },
614
+ });
615
+ }
616
+ }
617
+ return withWorkspace(result, binding);
618
+ }
619
+ catch (error) {
620
+ if (controlsPlatformState) {
621
+ binding = await updateGlobalBindingState({
622
+ workspaceInstanceId: binding.workspaceInstanceId,
623
+ state: "BROKEN",
624
+ globalRoot: runtime.globalRoot,
625
+ failure: {
626
+ code: error instanceof PlatformError ? error.code : "COMMAND_FAILED",
627
+ message: error instanceof Error ? error.message : String(error),
628
+ },
629
+ });
630
+ }
631
+ throw error;
632
+ }
633
+ }
634
+ finally {
635
+ await operationLock.release();
636
+ }
637
+ }
638
+ async function handleInstallWithGlobalBinding(args, runtime) {
639
+ const operationLock = await acquireGlobalOperationLock(runtime.globalRoot);
640
+ try {
641
+ const requestedWorkspace = await resolveRequestedInstallWorkspace(args, runtime);
642
+ const claimed = await claimWorkspaceBinding({
643
+ workspace: requestedWorkspace,
644
+ globalRoot: runtime.globalRoot,
645
+ });
646
+ const initialBinding = claimed.binding;
647
+ if (claimed.alreadyBound &&
648
+ initialBinding.state === "INSTALLED" &&
649
+ args.positional[0] === undefined) {
650
+ return withWorkspace(outcome("install", "SUCCEEDED", {
651
+ alreadyInstalled: true,
652
+ changed: false,
653
+ boundWorkspace: initialBinding.workspaceRealPath,
654
+ }), initialBinding);
655
+ }
656
+ if (initialBinding.state === "UNINSTALLING") {
657
+ throw new PlatformError("GLOBAL_OPERATION_LOCKED", "the bound Platform Instance is marked UNINSTALLING; finish recovery before installing");
658
+ }
659
+ let activeBinding = initialBinding;
660
+ const controlsPlatformState = !claimed.alreadyBound || initialBinding.state !== "INSTALLED";
661
+ if (controlsPlatformState && initialBinding.state !== "INSTALLING") {
662
+ activeBinding = await updateGlobalBindingState({
663
+ workspaceInstanceId: initialBinding.workspaceInstanceId,
664
+ state: "INSTALLING",
665
+ globalRoot: runtime.globalRoot,
666
+ });
667
+ }
668
+ try {
669
+ const ctx = await buildContext(activeBinding.workspaceRealPath);
670
+ const result = await handleManagedMutation(ctx, args, "install");
671
+ if (controlsPlatformState) {
672
+ if (result.status === "SUCCEEDED") {
673
+ activeBinding = await updateGlobalBindingState({
674
+ workspaceInstanceId: activeBinding.workspaceInstanceId,
675
+ state: "INSTALLED",
676
+ globalRoot: runtime.globalRoot,
677
+ });
678
+ }
679
+ else if (result.status === "FAILED" || result.status === "BLOCKED") {
680
+ activeBinding = await updateGlobalBindingState({
681
+ workspaceInstanceId: activeBinding.workspaceInstanceId,
682
+ state: "BROKEN",
683
+ globalRoot: runtime.globalRoot,
684
+ failure: {
685
+ code: result.error?.code ?? result.status,
686
+ message: result.error?.message ??
687
+ "Platform install did not reach a successful postcondition",
688
+ },
689
+ });
690
+ }
691
+ }
692
+ return withWorkspace(result, activeBinding);
693
+ }
694
+ catch (error) {
695
+ if (controlsPlatformState) {
696
+ activeBinding = await updateGlobalBindingState({
697
+ workspaceInstanceId: activeBinding.workspaceInstanceId,
698
+ state: "BROKEN",
699
+ globalRoot: runtime.globalRoot,
700
+ failure: {
701
+ code: error instanceof PlatformError ? error.code : "COMMAND_FAILED",
702
+ message: error instanceof Error ? error.message : String(error),
703
+ },
704
+ });
705
+ }
706
+ throw error;
707
+ }
708
+ }
709
+ finally {
710
+ await operationLock.release();
711
+ }
712
+ }
713
+ async function resolveBoundContext(runtime, requestedWorkspace) {
714
+ const binding = await requireBoundWorkspace(runtime.globalRoot);
715
+ if (requestedWorkspace !== undefined) {
716
+ const requested = await canonicalizeWorkspace(requestedWorkspace);
717
+ if (requested.workspaceRealPath !== binding.workspaceRealPath) {
718
+ throw new PlatformError("WORKSPACE_ALREADY_BOUND", `command targets ${requested.workspaceRealPath}, but the global Platform Instance is bound to ${binding.workspaceRealPath}`);
719
+ }
720
+ }
721
+ return {
722
+ ctx: await buildContext(binding.workspaceRealPath),
723
+ binding,
724
+ };
725
+ }
726
+ async function handleBoundMutatingCommand(args, runtime) {
727
+ const operationLock = await acquireGlobalOperationLock(runtime.globalRoot);
728
+ try {
729
+ const { ctx, binding } = await resolveBoundContext(runtime, args.workspace);
730
+ if (binding.state === "UNINSTALLING") {
731
+ throw new PlatformError("GLOBAL_OPERATION_LOCKED", "the bound Platform Instance is marked UNINSTALLING; finish uninstall recovery before another mutation");
732
+ }
733
+ let result;
734
+ if (args.command === "upgrade" || args.command === "uninstall") {
735
+ result = await handleManagedMutation(ctx, args, args.command);
736
+ }
737
+ else {
738
+ result = await handleLifecycle(ctx, args, args.command);
739
+ }
740
+ return withWorkspace(result, binding);
741
+ }
742
+ finally {
743
+ await operationLock.release();
744
+ }
745
+ }
746
+ async function handlePlatformInstanceUninstall(args, runtime) {
747
+ const operationLock = await acquireGlobalOperationLock(runtime.globalRoot);
748
+ let binding;
749
+ try {
750
+ binding = await requireBoundWorkspace(runtime.globalRoot);
751
+ if (args.workspace !== undefined) {
752
+ const requested = await canonicalizeWorkspace(args.workspace);
753
+ if (requested.workspaceRealPath !== binding.workspaceRealPath) {
754
+ throw new PlatformError("WORKSPACE_ALREADY_BOUND", `uninstall targets ${requested.workspaceRealPath}, but the global Platform Instance is bound to ${binding.workspaceRealPath}`);
755
+ }
756
+ }
757
+ binding = await updateGlobalBindingState({
758
+ workspaceInstanceId: binding.workspaceInstanceId,
759
+ state: "UNINSTALLING",
760
+ globalRoot: runtime.globalRoot,
761
+ });
762
+ const ctx = await buildContext(binding.workspaceRealPath);
763
+ const modules = await discoverModules({ catalog: ctx.catalog });
764
+ let removedModules = [];
765
+ if (modules.length > 0) {
766
+ const plan = planDeployment({
767
+ intent: "uninstall",
768
+ modules,
769
+ uninstallScope: "platform-instance",
770
+ });
771
+ await savePlan(ctx.paths, plan);
772
+ const current = await rebuildCurrentAssumptions(ctx.catalog, plan);
773
+ const applied = await applyPlan({
774
+ paths: ctx.paths,
775
+ planRef: plan.planRef,
776
+ catalog: ctx.catalog,
777
+ current,
778
+ driver: createWorkspacePackageManagerDriver({
779
+ workspaceRoot: ctx.paths.root,
780
+ }),
781
+ });
782
+ if (applied.outcome !== "COMPLETE") {
783
+ throw new PlatformError("UNINSTALL_FAILED", `Platform Instance uninstall stopped with ${applied.outcome}`);
784
+ }
785
+ removedModules = modules.map((module) => module.moduleRef);
786
+ }
787
+ const refreshed = await buildContext(binding.workspaceRealPath);
788
+ const remaining = await discoverModules({ catalog: refreshed.catalog });
789
+ if (remaining.length > 0) {
790
+ throw new PlatformError("UNINSTALL_FAILED", `Platform Instance uninstall left managed modules: ${remaining.map((module) => module.moduleRef).join(", ")}`);
791
+ }
792
+ // Deployment-owned plans/state/verification/instance identity must not leak
793
+ // into a future install of the same directory. Business/domain data outside
794
+ // `.proflow/deployment` is intentionally preserved.
795
+ await rm(ctx.paths.deployment, { recursive: true, force: true });
796
+ const completed = binding;
797
+ await clearGlobalBinding({
798
+ workspaceInstanceId: binding.workspaceInstanceId,
799
+ globalRoot: runtime.globalRoot,
800
+ });
801
+ binding = undefined;
802
+ return outcome("uninstall", "SUCCEEDED", {
803
+ uninstalledWorkspace: completed.workspaceRealPath,
804
+ removedModules,
805
+ bindingCleared: true,
806
+ });
807
+ }
808
+ catch (error) {
809
+ if (binding !== undefined) {
810
+ await updateGlobalBindingState({
811
+ workspaceInstanceId: binding.workspaceInstanceId,
812
+ state: "BROKEN",
813
+ globalRoot: runtime.globalRoot,
814
+ failure: {
815
+ code: error instanceof PlatformError ? error.code : "UNINSTALL_FAILED",
816
+ message: error instanceof Error ? error.message : String(error),
817
+ },
818
+ });
819
+ }
820
+ throw error;
821
+ }
822
+ finally {
823
+ await operationLock.release();
824
+ }
825
+ }
509
826
  async function handleLifecycle(ctx, args, primitive) {
510
827
  const modules = await discoverModules({ catalog: ctx.catalog });
511
828
  const selected = selectModules(modules, args.positional[0]);
@@ -591,8 +908,14 @@ async function handleManifest(ctx, args) {
591
908
  return outcome("manifest", "FAILED", manifest);
592
909
  }
593
910
  }
594
- async function dispatchCommand(argv) {
595
- const args = parseArgs(argv);
911
+ async function dispatchCommand(argv, runtime = {}) {
912
+ let args;
913
+ try {
914
+ args = parseArgs(argv);
915
+ }
916
+ catch (error) {
917
+ return failure(argv[0] ?? "", error);
918
+ }
596
919
  if (!COMMANDS.includes(args.command)) {
597
920
  return {
598
921
  command: args.command,
@@ -604,47 +927,143 @@ async function dispatchCommand(argv) {
604
927
  };
605
928
  }
606
929
  try {
607
- const root = args.workspace ?? process.cwd();
608
- if (args.command === "search")
930
+ const cwd = runtime.cwd ?? process.cwd();
931
+ if (args.command === "search") {
932
+ const current = await loadGlobalBinding(runtime.globalRoot);
933
+ const root = args.workspace ?? current?.workspaceRealPath ?? cwd;
609
934
  return await handleSearch(root, args);
935
+ }
610
936
  if (args.command === "preflight" && args.intent === "install") {
937
+ const root = await resolveRequestedInstallWorkspace(args, runtime);
938
+ const current = await loadGlobalBinding(runtime.globalRoot);
939
+ if (current !== undefined && current.workspaceRealPath !== root) {
940
+ throw new PlatformError("WORKSPACE_ALREADY_BOUND", `ProFlow is already bound to ${current.workspaceRealPath}; uninstall it before installing ${root}`);
941
+ }
611
942
  return await handleInstallerPreflight(root);
612
943
  }
613
- const ctx = await buildContext(args.workspace);
944
+ if (args.command === "plan") {
945
+ if (args.intent === "install") {
946
+ return await handleInstallPlanBeforeBinding(args, runtime);
947
+ }
948
+ if (args.intent === undefined ||
949
+ !["configure", "upgrade", "uninstall", "repair"].includes(args.intent)) {
950
+ const current = await loadGlobalBinding(runtime.globalRoot);
951
+ const root = args.workspace ??
952
+ current?.workspaceRealPath ??
953
+ runtime.cwd ??
954
+ process.cwd();
955
+ return await handlePlan(await buildContext(root), args);
956
+ }
957
+ }
958
+ if (args.command === "apply") {
959
+ return await handleApplyWithGlobalBinding(args, runtime);
960
+ }
961
+ if (args.command === "install") {
962
+ return await handleInstallWithGlobalBinding(args, runtime);
963
+ }
964
+ if (args.command === "status") {
965
+ const observation = await observeBoundWorkspace(runtime.globalRoot);
966
+ if (observation === undefined) {
967
+ return outcome("status", "SUCCEEDED", {
968
+ installed: false,
969
+ bindingState: "UNBOUND",
970
+ boundWorkspace: null,
971
+ nextAction: "Run platform install [--workspace <path>]",
972
+ });
973
+ }
974
+ if (!observation.workspaceExists) {
975
+ return withWorkspace(outcome("status", "BLOCKED", {
976
+ installed: false,
977
+ code: "BOUND_WORKSPACE_MISSING",
978
+ boundWorkspace: observation.binding.workspaceRealPath,
979
+ nextAction: "Restore the Workspace or run platform uninstall --forget to clear only the stale binding",
980
+ }), observation.binding);
981
+ }
982
+ }
983
+ if (args.command === "uninstall" && args.forget) {
984
+ if (args.positional.length > 0 || args.workspace !== undefined) {
985
+ 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");
986
+ }
987
+ const operationLock = await acquireGlobalOperationLock(runtime.globalRoot);
988
+ try {
989
+ const forgotten = await forgetMissingWorkspaceBinding({
990
+ globalRoot: runtime.globalRoot,
991
+ });
992
+ return outcome("uninstall", "SUCCEEDED", {
993
+ forgottenWorkspace: forgotten.workspaceRealPath,
994
+ bindingCleared: true,
995
+ resourcesCleaned: false,
996
+ });
997
+ }
998
+ finally {
999
+ await operationLock.release();
1000
+ }
1001
+ }
1002
+ if (args.command === "uninstall" && args.positional.length === 0) {
1003
+ const current = await loadGlobalBinding(runtime.globalRoot);
1004
+ if (current === undefined) {
1005
+ return outcome("uninstall", "SUCCEEDED", {
1006
+ alreadyUninstalled: true,
1007
+ bindingCleared: true,
1008
+ });
1009
+ }
1010
+ return await handlePlatformInstanceUninstall(args, runtime);
1011
+ }
1012
+ if (args.command === "upgrade" ||
1013
+ (args.command === "uninstall" && args.positional.length > 0) ||
1014
+ args.command === "start" ||
1015
+ args.command === "stop" ||
1016
+ args.command === "restart") {
1017
+ return await handleBoundMutatingCommand(args, runtime);
1018
+ }
1019
+ const { ctx, binding } = await resolveBoundContext(runtime, args.workspace);
1020
+ let result;
614
1021
  switch (args.command) {
615
1022
  case "search":
616
- return await handleSearch(root, args);
1023
+ case "install":
1024
+ throw new PlatformError("COMMAND_FAILED", "unreachable command routing");
617
1025
  case "modules":
618
- return await handleModules(ctx, args);
1026
+ result = await handleModules(ctx, args);
1027
+ break;
619
1028
  case "docs":
620
- return await handleDocs(ctx, args);
621
- case "install":
1029
+ result = await handleDocs(ctx, args);
1030
+ break;
622
1031
  case "uninstall":
623
1032
  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
1033
  case "start":
632
1034
  case "stop":
633
1035
  case "restart":
1036
+ throw new PlatformError("COMMAND_FAILED", "unreachable mutating command routing");
1037
+ case "preflight":
1038
+ result = await handlePreflight(ctx, args);
1039
+ break;
1040
+ case "plan":
1041
+ result = await handlePlan(ctx, args);
1042
+ break;
1043
+ case "apply":
1044
+ result = await handleApply(ctx, args);
1045
+ break;
634
1046
  case "status":
635
- return await handleLifecycle(ctx, args, args.command);
1047
+ result = await handleLifecycle(ctx, args, "status");
1048
+ break;
636
1049
  case "verify":
637
- return await handleVerify(ctx, args);
1050
+ result = await handleVerify(ctx, args);
1051
+ break;
638
1052
  case "doctor":
639
- return await handleDoctor(ctx, args);
1053
+ result = await handleDoctor(ctx, args);
1054
+ break;
640
1055
  case "manifest":
641
- return await handleManifest(ctx, args);
1056
+ result = await handleManifest(ctx, args);
1057
+ break;
1058
+ }
1059
+ if (result === undefined) {
1060
+ throw new PlatformError("COMMAND_FAILED", "unreachable command routing");
642
1061
  }
1062
+ return withWorkspace(result, binding);
643
1063
  }
644
1064
  catch (error) {
645
1065
  return failure(args.command, error);
646
1066
  }
647
- return failure(args.command, new PlatformError("COMMAND_FAILED", "unreachable"));
648
1067
  }
649
1068
  function toMachineResult(outcomeResult) {
650
1069
  const ok = outcomeResult.status === "SUCCEEDED";
@@ -654,6 +1073,9 @@ function toMachineResult(outcomeResult) {
654
1073
  status: outcomeResult.status,
655
1074
  moduleRef: MODULE_REF,
656
1075
  moduleVersion: MODULE_VERSION,
1076
+ ...(outcomeResult.workspace !== undefined
1077
+ ? { workspace: outcomeResult.workspace }
1078
+ : {}),
657
1079
  ...(outcomeResult.data !== undefined ? { data: outcomeResult.data } : {}),
658
1080
  ...(outcomeResult.status === "ACTION_REQUIRED"
659
1081
  ? {
@@ -674,7 +1096,7 @@ function toMachineResult(outcomeResult) {
674
1096
  : {}),
675
1097
  });
676
1098
  }
677
- export async function runCli(argv) {
1099
+ export async function runCli(argv, runtime = {}) {
678
1100
  if (argv.includes("--help") || argv.includes("-h")) {
679
1101
  return JSON.stringify({
680
1102
  contract: "deployment.result.v1",
@@ -698,7 +1120,7 @@ export async function runCli(argv) {
698
1120
  moduleVersion: MODULE_VERSION,
699
1121
  });
700
1122
  }
701
- return toMachineResult(await dispatchCommand(filtered));
1123
+ return toMachineResult(await dispatchCommand(filtered, runtime));
702
1124
  }
703
1125
  if (import.meta.main) {
704
1126
  const output = await runCli(process.argv.slice(2));
@@ -1,4 +1,4 @@
1
- export declare const platformErrorCodes: readonly ["INVALID_REQUEST", "DESCRIPTOR_INVALID", "DUPLICATE_IDENTITY", "DEPENDENCY_UNRESOLVED", "DEPENDENCY_INCOMPATIBLE", "DEPENDENCY_CYCLE", "CONFIG_MISSING", "SECRET_REF_INVALID", "PLAN_STALE", "PLAN_NOT_FOUND", "PLAN_INVALID", "WORKSPACE_LOCKED", "LIFECYCLE_UNSUPPORTED", "SECRET_LEAK", "APPLY_FAILED", "UPGRADE_FAILED", "CORE_PACKAGE_REQUIRED", "UNINSTALL_FAILED", "VERIFY_FAILED", "REGISTRY_UNAVAILABLE", "REGISTRY_AUTH_REQUIRED", "REGISTRY_RESPONSE_INVALID", "PACKAGE_NOT_FOUND", "PACKAGE_NOT_PROFLOW", "COMMAND_FAILED"];
1
+ export declare const platformErrorCodes: readonly ["INVALID_REQUEST", "DESCRIPTOR_INVALID", "DUPLICATE_IDENTITY", "DEPENDENCY_UNRESOLVED", "DEPENDENCY_INCOMPATIBLE", "DEPENDENCY_CYCLE", "CONFIG_MISSING", "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;