@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.
- package/CHANGELOG.md +6 -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/driver.d.ts +1 -0
- package/dist/src/apply/driver.js +40 -12
- 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 +1 -1
- package/dist/src/binding/production-bindings.js +5 -3
- package/dist/src/cli.d.ts +12 -1
- package/dist/src/cli.js +453 -31
- package/dist/src/errors.d.ts +1 -1
- package/dist/src/errors.js +12 -0
- package/dist/src/install/environment.d.ts +2 -1
- package/dist/src/install/environment.js +33 -29
- package/dist/src/install/package-manager.d.ts +4 -2
- package/dist/src/install/package-manager.js +73 -17
- package/dist/src/planner/plan.d.ts +1 -0
- package/dist/src/planner/plan.js +1 -1
- package/package.json +4 -4
- package/proflow.module.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @tomflow/proflow-platform-cli
|
|
2
2
|
|
|
3
|
+
## 0.1.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Close the Real-1 global Platform control-plane contract across every published ProFlow package. Platform CLI now owns one durable global Workspace binding with canonical identity, cross-process operation locking, cwd/`--workspace` install targeting, cross-directory instance commands, whole-instance uninstall/rebind, and deterministic npm/yarn/pnpm Workspace package-manager selection. Every package-owned install entry, including newly generated Module Template packages, delegates to the Shell-global `platform` command and fails closed when that global CLI is unavailable; Deployment Conformance and the formal test plans mechanically enforce the same rule across all 24 packages.
|
|
8
|
+
|
|
3
9
|
## 0.1.1
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -8,10 +8,20 @@ Business Fact Owner: none
|
|
|
8
8
|
|
|
9
9
|
## Purpose
|
|
10
10
|
|
|
11
|
-
The single platform-level deterministic Deployment planner/executor. It understands only the Module Contract and public lifecycle/verification contracts; it does not import other domains' business internals.
|
|
11
|
+
The single platform-level deterministic Deployment planner/executor and v1 global `platform` control plane. It understands only the Module Contract and public lifecycle/verification contracts; it does not import other domains' business internals. The global CLI binary is distinct from the single bound Platform Instance.
|
|
12
12
|
|
|
13
13
|
For the v1 Custom GPT carrier it may plan/verify Web-only setup, role-scoped Action auth/schema, File Bridge/Code Interpreter/Web Search requirements, `x-openai-isConsequential:false` + Always Allow target configuration, Chrome/runtime prerequisites and current verification freshness. It never creates/guesses Task `workerRef/c-id`, never uses exact ChatGPT model id as READY truth, and never accepts System Observer assessment as Deployment READY truth.
|
|
14
14
|
|
|
15
|
+
## Global CLI bootstrap
|
|
16
|
+
|
|
17
|
+
The v1 `platform` command is a Shell-global control plane. A straightforward bootstrap is:
|
|
18
|
+
|
|
19
|
+
```text
|
|
20
|
+
npm install --global @tomflow/proflow-platform-cli
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
This choice installs the global CLI only. It does **not** force the managed Workspace to use npm: a bound Workspace may independently use `npm`, `yarn`, or `pnpm`, selected from its own package-manager facts.
|
|
24
|
+
|
|
15
25
|
## Commands
|
|
16
26
|
|
|
17
27
|
```
|
|
@@ -20,8 +30,8 @@ platform modules [module|package]
|
|
|
20
30
|
platform docs [module|package] [documentId]
|
|
21
31
|
platform preflight [module]
|
|
22
32
|
platform preflight --intent install
|
|
23
|
-
platform install [package]
|
|
24
|
-
platform uninstall
|
|
33
|
+
platform install [package] [--workspace <path>]
|
|
34
|
+
platform uninstall [module|package]
|
|
25
35
|
platform upgrade [module|package]
|
|
26
36
|
platform plan --intent install|configure|upgrade|uninstall|repair [options]
|
|
27
37
|
platform apply <planRef>
|
|
@@ -35,3 +45,14 @@ platform manifest [module]
|
|
|
35
45
|
```
|
|
36
46
|
|
|
37
47
|
`install` / `upgrade` / `uninstall` are AI-friendly high-level commands that reuse the same frozen Plan -> Apply engine. `search` reads the npm Registry installable world; `modules` reads the current Workspace managed world; `docs` mechanically aggregates package-owned Descriptor, npm `bin/exports`, and declared documentation without maintaining a second package-knowledge catalog. Package mutation is real Workspace package-manager mutation and is never satisfied by the historical no-op driver seam.
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
## v1 global workspace semantics
|
|
51
|
+
|
|
52
|
+
- The `platform` binary is installed globally and can be invoked from any shell directory.
|
|
53
|
+
- At most one ProFlow Workspace may be bound globally at a time.
|
|
54
|
+
- `platform install` targets the current working directory by default; `platform install --workspace <path>` explicitly selects the requested Workspace for Agent/automation use.
|
|
55
|
+
- Requested Workspace paths are canonicalized before binding comparison. A second distinct Workspace is rejected with `WORKSPACE_ALREADY_BOUND` until the current Platform Instance is uninstalled.
|
|
56
|
+
- `status/start/stop/restart/modules/docs/verify/doctor/upgrade/uninstall` resolve the durable global binding rather than the caller's cwd. `platform status` exposes the bound Workspace path.
|
|
57
|
+
- `platform uninstall` without a module/package removes the current Platform Instance and clears its binding; it does not uninstall the global CLI package.
|
|
58
|
+
- Workspace package mutation is package-manager agnostic for v1: `npm`, `yarn`, and `pnpm` are supported. The CLI installation manager and the bound Workspace manager are independent facts.
|
|
@@ -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.2";
|
|
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.2";
|
|
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.2";
|
|
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.2";
|
|
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.2";
|
|
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.2";
|
|
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.2",
|
|
7
7
|
kind: "cli",
|
|
8
8
|
templateVersion: "1.0.0",
|
|
9
9
|
platformCompatibility: ">=1.0.0 <2.0.0",
|
|
@@ -20,4 +20,5 @@ export declare function workspaceResidentDriver(): PackageManagerDriver;
|
|
|
20
20
|
export declare function createWorkspacePackageManagerDriver(options: {
|
|
21
21
|
workspaceRoot: string;
|
|
22
22
|
runner?: PackageCommandRunner;
|
|
23
|
+
executableAvailable?: (command: string) => boolean;
|
|
23
24
|
}): PackageManagerDriver;
|
package/dist/src/apply/driver.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { PlatformError } from "../errors.js";
|
|
4
|
-
import { readWorkspacePackageManagerSelection, systemPackageCommandRunner, } from "../install/package-manager.js";
|
|
4
|
+
import { findExecutable, readWorkspacePackageManagerSelection, systemPackageCommandRunner, } from "../install/package-manager.js";
|
|
5
5
|
import { atomicWrite } from "../paths.js";
|
|
6
6
|
/**
|
|
7
7
|
* Monorepo/test seam retained for source-resident planning. Production CLI must
|
|
@@ -20,26 +20,27 @@ export function workspaceResidentDriver() {
|
|
|
20
20
|
}
|
|
21
21
|
export function createWorkspacePackageManagerDriver(options) {
|
|
22
22
|
const runner = options.runner ?? systemPackageCommandRunner();
|
|
23
|
+
const executableAvailable = options.executableAvailable ?? findExecutable;
|
|
23
24
|
return {
|
|
24
25
|
observeInstalledVersion: (module) => observeInstalledVersion(options.workspaceRoot, module.packageName),
|
|
25
26
|
install: async (module) => {
|
|
26
27
|
await ensureWorkspacePackageJson(options.workspaceRoot);
|
|
27
|
-
const manager = await requirePackageManager(options.workspaceRoot);
|
|
28
|
+
const manager = await requirePackageManager(options.workspaceRoot, executableAvailable);
|
|
28
29
|
await mutatePackage(runner, manager, options.workspaceRoot, "install", `${module.packageName}@${module.moduleVersion}`);
|
|
29
30
|
},
|
|
30
31
|
upgrade: async (module) => {
|
|
31
32
|
await ensureWorkspacePackageJson(options.workspaceRoot);
|
|
32
|
-
const manager = await requirePackageManager(options.workspaceRoot);
|
|
33
|
+
const manager = await requirePackageManager(options.workspaceRoot, executableAvailable);
|
|
33
34
|
await mutatePackage(runner, manager, options.workspaceRoot, "upgrade", `${module.packageName}@${module.moduleVersion}`);
|
|
34
35
|
},
|
|
35
36
|
remove: async (module) => {
|
|
36
|
-
const manager = await requirePackageManager(options.workspaceRoot);
|
|
37
|
+
const manager = await requirePackageManager(options.workspaceRoot, executableAvailable);
|
|
37
38
|
await mutatePackage(runner, manager, options.workspaceRoot, "remove", module.packageName);
|
|
38
39
|
},
|
|
39
40
|
};
|
|
40
41
|
}
|
|
41
42
|
async function mutatePackage(runner, manager, workspaceRoot, operation, packageSpec) {
|
|
42
|
-
const args = packageManagerArgs(manager
|
|
43
|
+
const args = await packageManagerArgs(runner, manager, workspaceRoot, operation, packageSpec);
|
|
43
44
|
try {
|
|
44
45
|
await runner.run(manager.name, args, workspaceRoot);
|
|
45
46
|
}
|
|
@@ -47,20 +48,47 @@ async function mutatePackage(runner, manager, workspaceRoot, operation, packageS
|
|
|
47
48
|
throw new PlatformError(operation === "upgrade" ? "UPGRADE_FAILED" : "APPLY_FAILED", `${manager.name} ${operation} failed for ${packageSpec}: ${errorMessage(error)}`);
|
|
48
49
|
}
|
|
49
50
|
}
|
|
50
|
-
function packageManagerArgs(manager, operation, packageSpec) {
|
|
51
|
-
if (manager === "pnpm") {
|
|
51
|
+
async function packageManagerArgs(runner, manager, workspaceRoot, operation, packageSpec) {
|
|
52
|
+
if (manager.name === "pnpm") {
|
|
52
53
|
return operation === "remove"
|
|
53
54
|
? ["remove", "--ignore-scripts", packageSpec]
|
|
54
55
|
: ["add", "--save-exact", "--ignore-scripts", packageSpec];
|
|
55
56
|
}
|
|
57
|
+
if (manager.name === "npm") {
|
|
58
|
+
return operation === "remove"
|
|
59
|
+
? ["uninstall", "--ignore-scripts", packageSpec]
|
|
60
|
+
: ["install", "--save-exact", "--ignore-scripts", packageSpec];
|
|
61
|
+
}
|
|
62
|
+
const major = await yarnMajorVersion(runner, manager, workspaceRoot);
|
|
63
|
+
if (major <= 1) {
|
|
64
|
+
return operation === "remove"
|
|
65
|
+
? ["remove", "--ignore-scripts", packageSpec]
|
|
66
|
+
: ["add", "--exact", "--ignore-scripts", packageSpec];
|
|
67
|
+
}
|
|
56
68
|
return operation === "remove"
|
|
57
|
-
? ["
|
|
58
|
-
: ["
|
|
69
|
+
? ["remove", "--mode=skip-build", packageSpec]
|
|
70
|
+
: ["add", "--exact", "--mode=skip-build", packageSpec];
|
|
71
|
+
}
|
|
72
|
+
async function yarnMajorVersion(runner, manager, workspaceRoot) {
|
|
73
|
+
const declaredVersion = declaredPackageManagerVersion(manager.declared);
|
|
74
|
+
const value = declaredVersion ??
|
|
75
|
+
(await runner.run("yarn", ["--version"], workspaceRoot)).trim();
|
|
76
|
+
const major = Number.parseInt(value.split(".")[0] ?? "", 10);
|
|
77
|
+
if (!Number.isInteger(major) || major < 1) {
|
|
78
|
+
throw new PlatformError("PACKAGE_MANAGER_UNAVAILABLE", `cannot determine a supported Yarn major version from ${value || "<empty>"}`);
|
|
79
|
+
}
|
|
80
|
+
return major;
|
|
81
|
+
}
|
|
82
|
+
function declaredPackageManagerVersion(declared) {
|
|
83
|
+
if (declared === undefined)
|
|
84
|
+
return undefined;
|
|
85
|
+
const separator = declared.lastIndexOf("@");
|
|
86
|
+
return separator > 0 ? declared.slice(separator + 1) : undefined;
|
|
59
87
|
}
|
|
60
|
-
async function requirePackageManager(workspaceRoot) {
|
|
88
|
+
async function requirePackageManager(workspaceRoot, executableAvailable) {
|
|
61
89
|
const manager = await readWorkspacePackageManagerSelection(workspaceRoot);
|
|
62
|
-
if (manager
|
|
63
|
-
throw new PlatformError("
|
|
90
|
+
if (!executableAvailable(manager.name)) {
|
|
91
|
+
throw new PlatformError("PACKAGE_MANAGER_UNAVAILABLE", `workspace package manager ${manager.name} is not available on PATH`);
|
|
64
92
|
}
|
|
65
93
|
return manager;
|
|
66
94
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export declare const GLOBAL_BINDING_CONTRACT: "deployment.global-binding.v1";
|
|
2
|
+
export declare const WORKSPACE_INSTANCE_CONTRACT: "deployment.workspace-instance.v1";
|
|
3
|
+
export type GlobalBindingState = "INSTALLING" | "INSTALLED" | "UNINSTALLING" | "BROKEN";
|
|
4
|
+
export interface GlobalWorkspaceBinding {
|
|
5
|
+
contract: typeof GLOBAL_BINDING_CONTRACT;
|
|
6
|
+
state: GlobalBindingState;
|
|
7
|
+
workspacePath: string;
|
|
8
|
+
workspaceRealPath: string;
|
|
9
|
+
workspaceInstanceId: string;
|
|
10
|
+
createdAt: string;
|
|
11
|
+
updatedAt: string;
|
|
12
|
+
failure?: {
|
|
13
|
+
code: string;
|
|
14
|
+
message: string;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export interface WorkspaceInstanceMarker {
|
|
18
|
+
contract: typeof WORKSPACE_INSTANCE_CONTRACT;
|
|
19
|
+
workspaceInstanceId: string;
|
|
20
|
+
workspaceRealPath: string;
|
|
21
|
+
createdAt: string;
|
|
22
|
+
}
|
|
23
|
+
export interface GlobalPlatformPaths {
|
|
24
|
+
root: string;
|
|
25
|
+
bindingJson: string;
|
|
26
|
+
bindingLock: string;
|
|
27
|
+
operationLock: string;
|
|
28
|
+
}
|
|
29
|
+
export interface BoundWorkspaceObservation {
|
|
30
|
+
binding: GlobalWorkspaceBinding;
|
|
31
|
+
workspaceExists: boolean;
|
|
32
|
+
}
|
|
33
|
+
export interface GlobalBindingLockHandle {
|
|
34
|
+
release(): Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
export declare function globalPlatformPaths(globalRoot?: string): GlobalPlatformPaths;
|
|
37
|
+
export declare function canonicalizeWorkspace(workspace: string): Promise<{
|
|
38
|
+
workspacePath: string;
|
|
39
|
+
workspaceRealPath: string;
|
|
40
|
+
}>;
|
|
41
|
+
export declare function loadGlobalBinding(globalRoot?: string): Promise<GlobalWorkspaceBinding | undefined>;
|
|
42
|
+
export declare function observeBoundWorkspace(globalRoot?: string): Promise<BoundWorkspaceObservation | undefined>;
|
|
43
|
+
export declare function requireBoundWorkspace(globalRoot?: string): Promise<GlobalWorkspaceBinding>;
|
|
44
|
+
export declare function claimWorkspaceBinding(options: {
|
|
45
|
+
workspace: string;
|
|
46
|
+
globalRoot?: string | undefined;
|
|
47
|
+
}): Promise<{
|
|
48
|
+
binding: GlobalWorkspaceBinding;
|
|
49
|
+
alreadyBound: boolean;
|
|
50
|
+
}>;
|
|
51
|
+
export declare function updateGlobalBindingState(options: {
|
|
52
|
+
workspaceInstanceId: string;
|
|
53
|
+
state: GlobalBindingState;
|
|
54
|
+
globalRoot?: string | undefined;
|
|
55
|
+
failure?: {
|
|
56
|
+
code: string;
|
|
57
|
+
message: string;
|
|
58
|
+
};
|
|
59
|
+
}): Promise<GlobalWorkspaceBinding>;
|
|
60
|
+
export declare function clearGlobalBinding(options: {
|
|
61
|
+
workspaceInstanceId: string;
|
|
62
|
+
globalRoot?: string | undefined;
|
|
63
|
+
removeWorkspaceMarker?: boolean;
|
|
64
|
+
}): Promise<void>;
|
|
65
|
+
export declare function forgetMissingWorkspaceBinding(options: {
|
|
66
|
+
globalRoot?: string | undefined;
|
|
67
|
+
}): Promise<GlobalWorkspaceBinding>;
|
|
68
|
+
export declare function acquireGlobalBindingLock(paths: GlobalPlatformPaths): Promise<GlobalBindingLockHandle>;
|
|
69
|
+
export declare function acquireGlobalOperationLock(globalRoot?: string): Promise<GlobalBindingLockHandle>;
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { access, chmod, mkdir, open, readFile, realpath, rm, stat, unlink, } from "node:fs/promises";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { dirname, join, resolve } from "node:path";
|
|
6
|
+
import { PlatformError } from "../errors.js";
|
|
7
|
+
import { atomicWrite } from "../paths.js";
|
|
8
|
+
export const GLOBAL_BINDING_CONTRACT = "deployment.global-binding.v1";
|
|
9
|
+
export const WORKSPACE_INSTANCE_CONTRACT = "deployment.workspace-instance.v1";
|
|
10
|
+
export function globalPlatformPaths(globalRoot) {
|
|
11
|
+
const root = globalRoot ??
|
|
12
|
+
process.env.PROFLOW_GLOBAL_HOME ??
|
|
13
|
+
join(homedir(), ".proflow", "platform");
|
|
14
|
+
return {
|
|
15
|
+
root,
|
|
16
|
+
bindingJson: join(root, "binding.json"),
|
|
17
|
+
bindingLock: join(root, "binding.lock"),
|
|
18
|
+
operationLock: join(root, "operation.lock"),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export async function canonicalizeWorkspace(workspace) {
|
|
22
|
+
const workspacePath = resolve(workspace);
|
|
23
|
+
let info;
|
|
24
|
+
try {
|
|
25
|
+
info = await stat(workspacePath);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
throw new PlatformError("WORKSPACE_NOT_FOUND", `workspace does not exist: ${workspacePath}`);
|
|
29
|
+
}
|
|
30
|
+
if (!info.isDirectory()) {
|
|
31
|
+
throw new PlatformError("WORKSPACE_NOT_FOUND", `workspace is not a directory: ${workspacePath}`);
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
await access(workspacePath, constants.R_OK | constants.W_OK);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
throw new PlatformError("WORKSPACE_NOT_WRITABLE", `workspace is not readable and writable: ${workspacePath}`);
|
|
38
|
+
}
|
|
39
|
+
return { workspacePath, workspaceRealPath: await realpath(workspacePath) };
|
|
40
|
+
}
|
|
41
|
+
export async function loadGlobalBinding(globalRoot) {
|
|
42
|
+
const paths = globalPlatformPaths(globalRoot);
|
|
43
|
+
try {
|
|
44
|
+
const raw = await readFile(paths.bindingJson, "utf8");
|
|
45
|
+
const parsed = JSON.parse(raw);
|
|
46
|
+
if (!isGlobalWorkspaceBinding(parsed)) {
|
|
47
|
+
throw new PlatformError("GLOBAL_BINDING_INVALID", `global binding is invalid: ${paths.bindingJson}`);
|
|
48
|
+
}
|
|
49
|
+
return parsed;
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (isMissingFile(error))
|
|
53
|
+
return undefined;
|
|
54
|
+
if (error instanceof PlatformError)
|
|
55
|
+
throw error;
|
|
56
|
+
throw new PlatformError("GLOBAL_BINDING_INVALID", `cannot read global binding ${paths.bindingJson}: ${errorMessage(error)}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export async function observeBoundWorkspace(globalRoot) {
|
|
60
|
+
const binding = await loadGlobalBinding(globalRoot);
|
|
61
|
+
if (binding === undefined)
|
|
62
|
+
return undefined;
|
|
63
|
+
return {
|
|
64
|
+
binding,
|
|
65
|
+
workspaceExists: await directoryExists(binding.workspaceRealPath),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
export async function requireBoundWorkspace(globalRoot) {
|
|
69
|
+
const observation = await observeBoundWorkspace(globalRoot);
|
|
70
|
+
if (observation === undefined) {
|
|
71
|
+
throw new PlatformError("WORKSPACE_NOT_BOUND", "no ProFlow Workspace is currently installed/bound");
|
|
72
|
+
}
|
|
73
|
+
if (!observation.workspaceExists) {
|
|
74
|
+
throw new PlatformError("BOUND_WORKSPACE_MISSING", `bound Workspace no longer exists: ${observation.binding.workspaceRealPath}`);
|
|
75
|
+
}
|
|
76
|
+
return observation.binding;
|
|
77
|
+
}
|
|
78
|
+
export async function claimWorkspaceBinding(options) {
|
|
79
|
+
const requested = await canonicalizeWorkspace(options.workspace);
|
|
80
|
+
const paths = globalPlatformPaths(options.globalRoot);
|
|
81
|
+
const lock = await acquireGlobalBindingLock(paths);
|
|
82
|
+
try {
|
|
83
|
+
const current = await loadGlobalBinding(options.globalRoot);
|
|
84
|
+
if (current !== undefined) {
|
|
85
|
+
if (current.workspaceRealPath !== requested.workspaceRealPath) {
|
|
86
|
+
throw new PlatformError("WORKSPACE_ALREADY_BOUND", `ProFlow is already bound to ${current.workspaceRealPath}; uninstall it before installing ${requested.workspaceRealPath}`);
|
|
87
|
+
}
|
|
88
|
+
return { binding: current, alreadyBound: true };
|
|
89
|
+
}
|
|
90
|
+
const existingMarker = await loadWorkspaceInstanceMarker(requested.workspaceRealPath);
|
|
91
|
+
const now = new Date().toISOString();
|
|
92
|
+
const binding = {
|
|
93
|
+
contract: GLOBAL_BINDING_CONTRACT,
|
|
94
|
+
state: "INSTALLING",
|
|
95
|
+
workspacePath: requested.workspacePath,
|
|
96
|
+
workspaceRealPath: requested.workspaceRealPath,
|
|
97
|
+
workspaceInstanceId: existingMarker?.workspaceInstanceId ?? randomUUID(),
|
|
98
|
+
createdAt: existingMarker?.createdAt ?? now,
|
|
99
|
+
updatedAt: now,
|
|
100
|
+
};
|
|
101
|
+
await saveGlobalBinding(paths, binding);
|
|
102
|
+
if (existingMarker === undefined) {
|
|
103
|
+
try {
|
|
104
|
+
await writeWorkspaceInstanceMarker(binding);
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
const broken = withBindingState(binding, "BROKEN", {
|
|
108
|
+
code: "WORKSPACE_INSTANCE_WRITE_FAILED",
|
|
109
|
+
message: errorMessage(error),
|
|
110
|
+
});
|
|
111
|
+
await saveGlobalBinding(paths, broken);
|
|
112
|
+
throw new PlatformError("GLOBAL_BINDING_INVALID", `failed to materialize Workspace instance identity: ${errorMessage(error)}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return { binding, alreadyBound: false };
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
await lock.release();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
export async function updateGlobalBindingState(options) {
|
|
122
|
+
const paths = globalPlatformPaths(options.globalRoot);
|
|
123
|
+
const lock = await acquireGlobalBindingLock(paths);
|
|
124
|
+
try {
|
|
125
|
+
const current = await loadGlobalBinding(options.globalRoot);
|
|
126
|
+
if (current === undefined) {
|
|
127
|
+
throw new PlatformError("WORKSPACE_NOT_BOUND", "cannot update global binding because no Workspace is bound");
|
|
128
|
+
}
|
|
129
|
+
if (current.workspaceInstanceId !== options.workspaceInstanceId) {
|
|
130
|
+
throw new PlatformError("GLOBAL_BINDING_INVALID", "global binding instance identity changed during the operation");
|
|
131
|
+
}
|
|
132
|
+
const next = withBindingState(current, options.state, options.failure);
|
|
133
|
+
await saveGlobalBinding(paths, next);
|
|
134
|
+
return next;
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
await lock.release();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
export async function clearGlobalBinding(options) {
|
|
141
|
+
const paths = globalPlatformPaths(options.globalRoot);
|
|
142
|
+
const lock = await acquireGlobalBindingLock(paths);
|
|
143
|
+
try {
|
|
144
|
+
const current = await loadGlobalBinding(options.globalRoot);
|
|
145
|
+
if (current === undefined)
|
|
146
|
+
return;
|
|
147
|
+
if (current.workspaceInstanceId !== options.workspaceInstanceId) {
|
|
148
|
+
throw new PlatformError("GLOBAL_BINDING_INVALID", "refusing to clear a different Workspace binding");
|
|
149
|
+
}
|
|
150
|
+
await rm(paths.bindingJson, { force: true });
|
|
151
|
+
if (options.removeWorkspaceMarker === true) {
|
|
152
|
+
await rm(workspaceInstanceMarkerPath(current.workspaceRealPath), {
|
|
153
|
+
force: true,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
finally {
|
|
158
|
+
await lock.release();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
export async function forgetMissingWorkspaceBinding(options) {
|
|
162
|
+
const paths = globalPlatformPaths(options.globalRoot);
|
|
163
|
+
const lock = await acquireGlobalBindingLock(paths);
|
|
164
|
+
try {
|
|
165
|
+
const current = await loadGlobalBinding(options.globalRoot);
|
|
166
|
+
if (current === undefined) {
|
|
167
|
+
throw new PlatformError("WORKSPACE_NOT_BOUND", "no ProFlow Workspace binding exists to forget");
|
|
168
|
+
}
|
|
169
|
+
if (await directoryExists(current.workspaceRealPath)) {
|
|
170
|
+
throw new PlatformError("INVALID_REQUEST", "bound Workspace still exists; use normal platform uninstall instead of forget");
|
|
171
|
+
}
|
|
172
|
+
await rm(paths.bindingJson, { force: true });
|
|
173
|
+
return current;
|
|
174
|
+
}
|
|
175
|
+
finally {
|
|
176
|
+
await lock.release();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
export async function acquireGlobalBindingLock(paths) {
|
|
180
|
+
return acquireExclusiveGlobalLock(paths.root, paths.bindingLock, "GLOBAL_BINDING_LOCKED", "another process is mutating the global Workspace binding");
|
|
181
|
+
}
|
|
182
|
+
export async function acquireGlobalOperationLock(globalRoot) {
|
|
183
|
+
const paths = globalPlatformPaths(globalRoot);
|
|
184
|
+
return acquireExclusiveGlobalLock(paths.root, paths.operationLock, "GLOBAL_OPERATION_LOCKED", "another Platform install/uninstall operation is already running");
|
|
185
|
+
}
|
|
186
|
+
async function acquireExclusiveGlobalLock(root, file, code, message) {
|
|
187
|
+
await mkdir(root, { recursive: true });
|
|
188
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
189
|
+
try {
|
|
190
|
+
const handle = await open(file, "wx", 0o600);
|
|
191
|
+
try {
|
|
192
|
+
await handle.writeFile(`${JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() })}\n`, "utf8");
|
|
193
|
+
}
|
|
194
|
+
finally {
|
|
195
|
+
await handle.close();
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
async release() {
|
|
199
|
+
try {
|
|
200
|
+
await unlink(file);
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
if (!isMissingFile(error))
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
if (!isAlreadyExists(error))
|
|
211
|
+
throw error;
|
|
212
|
+
const record = await readLockRecord(file);
|
|
213
|
+
if (record !== undefined && !pidIsAlive(record.pid)) {
|
|
214
|
+
await rm(file, { force: true });
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
throw new PlatformError(code, `${message}: ${file}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
throw new PlatformError(code, `${message}: ${file}`);
|
|
221
|
+
}
|
|
222
|
+
function workspaceInstanceMarkerPath(workspaceRoot) {
|
|
223
|
+
return join(workspaceRoot, ".proflow", "deployment", "instance.json");
|
|
224
|
+
}
|
|
225
|
+
async function loadWorkspaceInstanceMarker(workspaceRoot) {
|
|
226
|
+
const path = workspaceInstanceMarkerPath(workspaceRoot);
|
|
227
|
+
try {
|
|
228
|
+
const parsed = JSON.parse(await readFile(path, "utf8"));
|
|
229
|
+
if (!isWorkspaceInstanceMarker(parsed)) {
|
|
230
|
+
throw new PlatformError("WORKSPACE_INSTANCE_INVALID", `Workspace instance marker is invalid: ${path}`);
|
|
231
|
+
}
|
|
232
|
+
if (parsed.workspaceRealPath !== workspaceRoot) {
|
|
233
|
+
throw new PlatformError("WORKSPACE_INSTANCE_INVALID", `Workspace instance marker belongs to ${parsed.workspaceRealPath}, not ${workspaceRoot}`);
|
|
234
|
+
}
|
|
235
|
+
return parsed;
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
if (isMissingFile(error))
|
|
239
|
+
return undefined;
|
|
240
|
+
if (error instanceof PlatformError)
|
|
241
|
+
throw error;
|
|
242
|
+
throw new PlatformError("WORKSPACE_INSTANCE_INVALID", `cannot read Workspace instance marker ${path}: ${errorMessage(error)}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
function isWorkspaceInstanceMarker(value) {
|
|
246
|
+
if (!isRecord(value))
|
|
247
|
+
return false;
|
|
248
|
+
return (value.contract === WORKSPACE_INSTANCE_CONTRACT &&
|
|
249
|
+
typeof value.workspaceInstanceId === "string" &&
|
|
250
|
+
value.workspaceInstanceId.length > 0 &&
|
|
251
|
+
typeof value.workspaceRealPath === "string" &&
|
|
252
|
+
typeof value.createdAt === "string");
|
|
253
|
+
}
|
|
254
|
+
async function writeWorkspaceInstanceMarker(binding) {
|
|
255
|
+
const path = workspaceInstanceMarkerPath(binding.workspaceRealPath);
|
|
256
|
+
const marker = {
|
|
257
|
+
contract: WORKSPACE_INSTANCE_CONTRACT,
|
|
258
|
+
workspaceInstanceId: binding.workspaceInstanceId,
|
|
259
|
+
workspaceRealPath: binding.workspaceRealPath,
|
|
260
|
+
createdAt: binding.createdAt,
|
|
261
|
+
};
|
|
262
|
+
await mkdir(dirname(path), { recursive: true });
|
|
263
|
+
await atomicWrite(path, `${JSON.stringify(marker, null, 2)}\n`);
|
|
264
|
+
await chmod(path, 0o600);
|
|
265
|
+
}
|
|
266
|
+
async function saveGlobalBinding(paths, binding) {
|
|
267
|
+
await mkdir(paths.root, { recursive: true });
|
|
268
|
+
await atomicWrite(paths.bindingJson, `${JSON.stringify(binding, null, 2)}\n`);
|
|
269
|
+
await chmod(paths.bindingJson, 0o600);
|
|
270
|
+
}
|
|
271
|
+
function withBindingState(binding, state, failure) {
|
|
272
|
+
const { failure: _previousFailure, ...base } = binding;
|
|
273
|
+
return {
|
|
274
|
+
...base,
|
|
275
|
+
state,
|
|
276
|
+
updatedAt: new Date().toISOString(),
|
|
277
|
+
...(failure === undefined ? {} : { failure }),
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
function isGlobalWorkspaceBinding(value) {
|
|
281
|
+
if (!isRecord(value))
|
|
282
|
+
return false;
|
|
283
|
+
if (value.contract !== GLOBAL_BINDING_CONTRACT)
|
|
284
|
+
return false;
|
|
285
|
+
if (!isBindingState(value.state))
|
|
286
|
+
return false;
|
|
287
|
+
return (typeof value.workspacePath === "string" &&
|
|
288
|
+
typeof value.workspaceRealPath === "string" &&
|
|
289
|
+
typeof value.workspaceInstanceId === "string" &&
|
|
290
|
+
typeof value.createdAt === "string" &&
|
|
291
|
+
typeof value.updatedAt === "string" &&
|
|
292
|
+
(value.failure === undefined ||
|
|
293
|
+
(isRecord(value.failure) &&
|
|
294
|
+
typeof value.failure.code === "string" &&
|
|
295
|
+
typeof value.failure.message === "string")));
|
|
296
|
+
}
|
|
297
|
+
function isBindingState(value) {
|
|
298
|
+
return (value === "INSTALLING" ||
|
|
299
|
+
value === "INSTALLED" ||
|
|
300
|
+
value === "UNINSTALLING" ||
|
|
301
|
+
value === "BROKEN");
|
|
302
|
+
}
|
|
303
|
+
async function directoryExists(path) {
|
|
304
|
+
try {
|
|
305
|
+
return (await stat(path)).isDirectory();
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
async function readLockRecord(path) {
|
|
312
|
+
try {
|
|
313
|
+
const parsed = JSON.parse(await readFile(path, "utf8"));
|
|
314
|
+
if (isRecord(parsed) &&
|
|
315
|
+
typeof parsed.pid === "number" &&
|
|
316
|
+
Number.isInteger(parsed.pid) &&
|
|
317
|
+
parsed.pid > 0) {
|
|
318
|
+
return { pid: parsed.pid };
|
|
319
|
+
}
|
|
320
|
+
return undefined;
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
return undefined;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
function pidIsAlive(pid) {
|
|
327
|
+
try {
|
|
328
|
+
process.kill(pid, 0);
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
catch (error) {
|
|
332
|
+
return isPermissionError(error);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
function isRecord(value) {
|
|
336
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
337
|
+
}
|
|
338
|
+
function isMissingFile(error) {
|
|
339
|
+
return isRecord(error) && error.code === "ENOENT";
|
|
340
|
+
}
|
|
341
|
+
function isAlreadyExists(error) {
|
|
342
|
+
return isRecord(error) && error.code === "EEXIST";
|
|
343
|
+
}
|
|
344
|
+
function isPermissionError(error) {
|
|
345
|
+
return isRecord(error) && error.code === "EPERM";
|
|
346
|
+
}
|
|
347
|
+
function errorMessage(error) {
|
|
348
|
+
return error instanceof Error ? error.message : String(error);
|
|
349
|
+
}
|
|
@@ -29,7 +29,7 @@ export interface ProductionBindingOptions {
|
|
|
29
29
|
configByModuleRef: ReadonlyMap<string, Record<string, string>>;
|
|
30
30
|
importAdapter: (packageName: string, source: ResolvedSource) => Promise<Record<string, unknown>>;
|
|
31
31
|
}
|
|
32
|
-
export declare function importRawAdapter(packageName: string, source: ResolvedSource): Promise<Record<string, unknown>>;
|
|
32
|
+
export declare function importRawAdapter(packageName: string, source: ResolvedSource, workspaceRoot: string): Promise<Record<string, unknown>>;
|
|
33
33
|
/**
|
|
34
34
|
* The shipped Platform CLI production binding factory. For every discovered
|
|
35
35
|
* module it imports the module's own `deployment/adapter.ts` and, when that
|