@principles/install-layout 0.3.1 → 0.4.1
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/dist/index.d.ts +55 -1
- package/dist/index.js +98 -6
- package/package.json +3 -2
package/dist/index.d.ts
CHANGED
|
@@ -6,7 +6,23 @@ export type InstallHost = (typeof INSTALL_HOSTS)[number];
|
|
|
6
6
|
export interface InstallManifest {
|
|
7
7
|
layoutVersion: typeof INSTALL_LAYOUT_VERSION;
|
|
8
8
|
mode: InstallLayoutMode;
|
|
9
|
+
/**
|
|
10
|
+
* Hosts THIS reader version recognizes. Host strings the reader does not
|
|
11
|
+
* know are NOT here — they land in `unknownHosts` so an install registered
|
|
12
|
+
* with a newer host keeps resolving instead of being rejected as malformed.
|
|
13
|
+
*/
|
|
9
14
|
hosts: InstallHost[];
|
|
15
|
+
/**
|
|
16
|
+
* Host strings present in the persisted `hosts` array that this reader
|
|
17
|
+
* version does not know (forward compatibility, 2026-10-16). A NEWER
|
|
18
|
+
* installer may register a host an OLDER reader never heard of; that is a
|
|
19
|
+
* compatibility warning, never "manifest malformed" — treating it as one
|
|
20
|
+
* made every pre-WorkBuddy Companion report `console_runtime_not_installed`
|
|
21
|
+
* against a healthy 3-host install and mass-stop its workers. Fail-closed
|
|
22
|
+
* stays reserved for structural damage: wrong layoutVersion, non-array or
|
|
23
|
+
* non-string hosts, bad mode, illegal workspace paths.
|
|
24
|
+
*/
|
|
25
|
+
unknownHosts?: readonly string[];
|
|
10
26
|
/**
|
|
11
27
|
* Canonical absolute paths of Workspaces the install serves (PRI-624 Slice C).
|
|
12
28
|
* Optional so pre-PRI-624 manifests stay valid; always normalized to
|
|
@@ -16,6 +32,11 @@ export interface InstallManifest {
|
|
|
16
32
|
*/
|
|
17
33
|
workspaces?: readonly string[];
|
|
18
34
|
}
|
|
35
|
+
/** Structured forward-compatibility observation from manifest parsing (rc-9). */
|
|
36
|
+
export interface InstallManifestWarning {
|
|
37
|
+
code: 'install_manifest_unknown_host';
|
|
38
|
+
host: string;
|
|
39
|
+
}
|
|
19
40
|
export interface InstallLayoutPaths {
|
|
20
41
|
homeDir: string;
|
|
21
42
|
pdDir: string;
|
|
@@ -51,6 +72,14 @@ export interface InstallLayoutPaths {
|
|
|
51
72
|
* the same way the Codex hook resolves the canonical runtime.
|
|
52
73
|
*/
|
|
53
74
|
workbuddyAdapterDir: string;
|
|
75
|
+
/**
|
|
76
|
+
* Gate 4 (PRI-1071): the shared read-only MCP server component (payload dir
|
|
77
|
+
* pd-mcp-server/, npm name @principles/pd-mcp-server). This is the Install
|
|
78
|
+
* Layout SSoT for the canonical server entry OpenClaw/WorkBuddy launchers
|
|
79
|
+
* bind to; Codex keeps its plugin-data runtime layout and records that dir
|
|
80
|
+
* in its own binding.
|
|
81
|
+
*/
|
|
82
|
+
mcpServerDir: string;
|
|
54
83
|
}
|
|
55
84
|
export interface LayoutResolution {
|
|
56
85
|
mode: InstallLayoutMode | 'missing';
|
|
@@ -58,6 +87,8 @@ export interface LayoutResolution {
|
|
|
58
87
|
manifest?: InstallManifest;
|
|
59
88
|
reason?: string;
|
|
60
89
|
nextAction?: string;
|
|
90
|
+
/** Forward-compatibility warnings surfaced while parsing the manifest. */
|
|
91
|
+
warnings?: readonly InstallManifestWarning[];
|
|
61
92
|
}
|
|
62
93
|
export interface ResolveInstallLayoutOptions {
|
|
63
94
|
homeDir: string;
|
|
@@ -65,13 +96,36 @@ export interface ResolveInstallLayoutOptions {
|
|
|
65
96
|
canonicalRuntimeExists: boolean;
|
|
66
97
|
legacyExtensionExists: boolean;
|
|
67
98
|
}
|
|
68
|
-
/**
|
|
99
|
+
/**
|
|
100
|
+
* Merge one Workspace into a manifest's workspace list (installer write path). Idempotent by canonical path. */
|
|
69
101
|
export declare function mergeInstallManifestWorkspaces(current: InstallManifest | undefined, workspaceDir: string): string[];
|
|
102
|
+
/**
|
|
103
|
+
* The `hosts` array a manifest WRITER must persist: every registration the
|
|
104
|
+
* reader recognized plus every unknown one, deduped, known hosts first.
|
|
105
|
+
* Writers that rebuild `hosts` from `manifest.hosts` alone silently DELETE a
|
|
106
|
+
* newer host's registration — the write-side twin of the read-side
|
|
107
|
+
* unknown-host brick (2026-10-16). `undefined` yields `[]` so the installer's
|
|
108
|
+
* fresh-install path keeps working unchanged.
|
|
109
|
+
*/
|
|
110
|
+
export declare function installManifestHostsForPersist(manifest: InstallManifest | undefined): string[];
|
|
111
|
+
/**
|
|
112
|
+
* Parse the install manifest. Structural damage fails closed with `error`;
|
|
113
|
+
* a host string this reader version does not know is a `warnings` entry and
|
|
114
|
+
* the manifest still parses (forward compatibility — see `InstallManifest.unknownHosts`).
|
|
115
|
+
*/
|
|
70
116
|
export declare function parseInstallManifest(value: unknown): {
|
|
71
117
|
manifest?: InstallManifest;
|
|
118
|
+
warnings?: readonly InstallManifestWarning[];
|
|
72
119
|
error?: string;
|
|
73
120
|
};
|
|
74
121
|
export declare function getInstallLayoutPaths(homeDir: string): InstallLayoutPaths;
|
|
122
|
+
/**
|
|
123
|
+
* Canonical installed MCP server entry for the shared runtime layout
|
|
124
|
+
* (~/.pd/runtime/pd-mcp-server/dist/index.js). Launchers, binding writers and
|
|
125
|
+
* health diagnostics derive the path from this single owner instead of each
|
|
126
|
+
* repeating the segment list.
|
|
127
|
+
*/
|
|
128
|
+
export declare function getMcpServerEntry(paths: InstallLayoutPaths): string;
|
|
75
129
|
/**
|
|
76
130
|
* WorkBuddy governance workspace (SPEC workbuddy-host-adapter §4.1 OD-07,
|
|
77
131
|
* frozen by Phase 0 probe P06 2026-10-12): an explicit user-level directory
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,9 @@ function isRecord(value) {
|
|
|
8
8
|
function isHost(value) {
|
|
9
9
|
return typeof value === 'string' && INSTALL_HOSTS.includes(value);
|
|
10
10
|
}
|
|
11
|
+
function isNonEmptyString(value) {
|
|
12
|
+
return typeof value === 'string' && value.length > 0;
|
|
13
|
+
}
|
|
11
14
|
/** Workspace entries must be non-empty absolute paths — the Companion spawns per-Workspace workers keyed by canonical path. */
|
|
12
15
|
function parseWorkspaceList(value) {
|
|
13
16
|
if (value === undefined)
|
|
@@ -25,7 +28,8 @@ function parseWorkspaceList(value) {
|
|
|
25
28
|
}
|
|
26
29
|
return { workspaces: resolved };
|
|
27
30
|
}
|
|
28
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* Merge one Workspace into a manifest's workspace list (installer write path). Idempotent by canonical path. */
|
|
29
33
|
export function mergeInstallManifestWorkspaces(current, workspaceDir) {
|
|
30
34
|
const canonical = path.resolve(workspaceDir);
|
|
31
35
|
const resolved = [];
|
|
@@ -38,6 +42,22 @@ export function mergeInstallManifestWorkspaces(current, workspaceDir) {
|
|
|
38
42
|
resolved.push(canonical);
|
|
39
43
|
return resolved;
|
|
40
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* The `hosts` array a manifest WRITER must persist: every registration the
|
|
47
|
+
* reader recognized plus every unknown one, deduped, known hosts first.
|
|
48
|
+
* Writers that rebuild `hosts` from `manifest.hosts` alone silently DELETE a
|
|
49
|
+
* newer host's registration — the write-side twin of the read-side
|
|
50
|
+
* unknown-host brick (2026-10-16). `undefined` yields `[]` so the installer's
|
|
51
|
+
* fresh-install path keeps working unchanged.
|
|
52
|
+
*/
|
|
53
|
+
export function installManifestHostsForPersist(manifest) {
|
|
54
|
+
return [...new Set([...(manifest?.hosts ?? []), ...(manifest?.unknownHosts ?? [])])];
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Parse the install manifest. Structural damage fails closed with `error`;
|
|
58
|
+
* a host string this reader version does not know is a `warnings` entry and
|
|
59
|
+
* the manifest still parses (forward compatibility — see `InstallManifest.unknownHosts`).
|
|
60
|
+
*/
|
|
41
61
|
export function parseInstallManifest(value) {
|
|
42
62
|
if (!isRecord(value))
|
|
43
63
|
return { error: 'install_manifest_malformed: expected an object' };
|
|
@@ -47,14 +67,55 @@ export function parseInstallManifest(value) {
|
|
|
47
67
|
if (value.mode !== 'canonical' && value.mode !== 'legacy') {
|
|
48
68
|
return { error: 'install_manifest_malformed: mode must be canonical or legacy' };
|
|
49
69
|
}
|
|
50
|
-
|
|
51
|
-
|
|
70
|
+
// Host entries must be non-empty strings; WHICH strings are valid hosts is
|
|
71
|
+
// version-relative knowledge, so unknown strings warn instead of failing.
|
|
72
|
+
if (!Array.isArray(value.hosts) || !value.hosts.every(isNonEmptyString)) {
|
|
73
|
+
return { error: 'install_manifest_malformed: hosts must be an array of host name strings' };
|
|
74
|
+
}
|
|
75
|
+
// Idempotency: the parser must re-read its own output. An all-unknown-hosts
|
|
76
|
+
// manifest parses to hosts: [] + unknownHosts, so the optional persisted
|
|
77
|
+
// `unknownHosts` field is accepted on input too (installManifestHostsForPersist
|
|
78
|
+
// reunifies them on write; writers persist the union into `hosts`).
|
|
79
|
+
let carriedUnknownHosts = [];
|
|
80
|
+
if (value.unknownHosts !== undefined) {
|
|
81
|
+
if (!Array.isArray(value.unknownHosts) || !value.unknownHosts.every(isNonEmptyString)) {
|
|
82
|
+
return { error: 'install_manifest_malformed: unknownHosts must be an array of host name strings' };
|
|
83
|
+
}
|
|
84
|
+
carriedUnknownHosts = value.unknownHosts.filter((host) => !isHost(host));
|
|
85
|
+
}
|
|
86
|
+
if (value.hosts.length === 0 && carriedUnknownHosts.length === 0) {
|
|
87
|
+
return { error: 'install_manifest_malformed: hosts must contain at least one host' };
|
|
88
|
+
}
|
|
89
|
+
const hosts = [];
|
|
90
|
+
const unknownHosts = [];
|
|
91
|
+
const warnings = [];
|
|
92
|
+
for (const host of [...new Set(value.hosts)]) {
|
|
93
|
+
if (isHost(host))
|
|
94
|
+
hosts.push(host);
|
|
95
|
+
else {
|
|
96
|
+
unknownHosts.push(host);
|
|
97
|
+
warnings.push({ code: 'install_manifest_unknown_host', host });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
for (const host of carriedUnknownHosts) {
|
|
101
|
+
if (!unknownHosts.includes(host)) {
|
|
102
|
+
unknownHosts.push(host);
|
|
103
|
+
warnings.push({ code: 'install_manifest_unknown_host', host });
|
|
104
|
+
}
|
|
52
105
|
}
|
|
53
|
-
const hosts = [...new Set(value.hosts)];
|
|
54
106
|
const workspaces = parseWorkspaceList(value.workspaces);
|
|
55
107
|
if (workspaces.error)
|
|
56
108
|
return { error: workspaces.error };
|
|
57
|
-
return {
|
|
109
|
+
return {
|
|
110
|
+
manifest: {
|
|
111
|
+
layoutVersion: INSTALL_LAYOUT_VERSION,
|
|
112
|
+
mode: value.mode,
|
|
113
|
+
hosts,
|
|
114
|
+
...(unknownHosts.length > 0 ? { unknownHosts } : {}),
|
|
115
|
+
workspaces: workspaces.workspaces,
|
|
116
|
+
},
|
|
117
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
118
|
+
};
|
|
58
119
|
}
|
|
59
120
|
export function getInstallLayoutPaths(homeDir) {
|
|
60
121
|
const home = path.resolve(homeDir);
|
|
@@ -79,8 +140,18 @@ export function getInstallLayoutPaths(homeDir) {
|
|
|
79
140
|
releaseManagerDir: path.join(runtimeDir, 'release-manager'),
|
|
80
141
|
codexAdapterDir: path.join(runtimeDir, 'codex-adapter'),
|
|
81
142
|
workbuddyAdapterDir: path.join(runtimeDir, 'workbuddy-adapter'),
|
|
143
|
+
mcpServerDir: path.join(runtimeDir, 'pd-mcp-server'),
|
|
82
144
|
};
|
|
83
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* Canonical installed MCP server entry for the shared runtime layout
|
|
148
|
+
* (~/.pd/runtime/pd-mcp-server/dist/index.js). Launchers, binding writers and
|
|
149
|
+
* health diagnostics derive the path from this single owner instead of each
|
|
150
|
+
* repeating the segment list.
|
|
151
|
+
*/
|
|
152
|
+
export function getMcpServerEntry(paths) {
|
|
153
|
+
return path.join(paths.mcpServerDir, 'dist', 'index.js');
|
|
154
|
+
}
|
|
84
155
|
/**
|
|
85
156
|
* WorkBuddy governance workspace (SPEC workbuddy-host-adapter §4.1 OD-07,
|
|
86
157
|
* frozen by Phase 0 probe P06 2026-10-12): an explicit user-level directory
|
|
@@ -96,8 +167,28 @@ export function getWorkbuddyWorkspaceDir(homeDir) {
|
|
|
96
167
|
export function resolveInstallLayout(options) {
|
|
97
168
|
const paths = getInstallLayoutPaths(options.homeDir);
|
|
98
169
|
const parsed = parseInstallManifest(options.manifest);
|
|
170
|
+
const { warnings } = parsed;
|
|
99
171
|
if (parsed.manifest && parsed.manifest.mode === 'canonical' && options.canonicalRuntimeExists) {
|
|
100
|
-
return { mode: 'canonical', paths, manifest: parsed.manifest };
|
|
172
|
+
return { mode: 'canonical', paths, manifest: parsed.manifest, ...(warnings !== undefined ? { warnings } : {}) };
|
|
173
|
+
}
|
|
174
|
+
// RF-01 (review 2026-10-16): when a canonical runtime EXISTS, the manifest
|
|
175
|
+
// is the install-state authority. A manifest that was READ but cannot be
|
|
176
|
+
// honored (newer layoutVersion, structural damage) must refuse the whole
|
|
177
|
+
// resolution — falling through to the legacy extension dir would launch a
|
|
178
|
+
// runtime that does not match the installed protocol. Legacy fallback stays
|
|
179
|
+
// legitimate only when the reader genuinely saw NO manifest (absent file →
|
|
180
|
+
// `options.manifest === undefined`), i.e. a real legacy-only install, or
|
|
181
|
+
// when there is no canonical runtime to bypass.
|
|
182
|
+
if (options.canonicalRuntimeExists && options.manifest !== undefined && parsed.manifest === undefined) {
|
|
183
|
+
return {
|
|
184
|
+
mode: 'missing',
|
|
185
|
+
paths,
|
|
186
|
+
reason: parsed.error ?? 'install_manifest_malformed',
|
|
187
|
+
nextAction: parsed.error?.startsWith('install_manifest_unsupported_version')
|
|
188
|
+
? 'Update PD Companion from https://github.com/csuzngjh/principles/releases (companion-v* Windows installer); do not reinstall the healthy PD runtime.'
|
|
189
|
+
: 'Run the PD installer/updater to repair the structurally damaged installation manifest.',
|
|
190
|
+
...(warnings !== undefined ? { warnings } : {}),
|
|
191
|
+
};
|
|
101
192
|
}
|
|
102
193
|
if (options.legacyExtensionExists) {
|
|
103
194
|
return {
|
|
@@ -106,6 +197,7 @@ export function resolveInstallLayout(options) {
|
|
|
106
197
|
...(parsed.manifest ? { manifest: parsed.manifest } : {}),
|
|
107
198
|
reason: parsed.error ?? 'install_layout_legacy_fallback',
|
|
108
199
|
nextAction: 'Run the installer update to migrate the shared runtime to ~/.pd/runtime.',
|
|
200
|
+
...(warnings !== undefined ? { warnings } : {}),
|
|
109
201
|
};
|
|
110
202
|
}
|
|
111
203
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@principles/install-layout",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -33,5 +33,6 @@
|
|
|
33
33
|
"@types/node": "^26.4.1",
|
|
34
34
|
"typescript": "^7.0.2",
|
|
35
35
|
"vitest": "^5.0.0"
|
|
36
|
-
}
|
|
36
|
+
},
|
|
37
|
+
"gitHead": "d3355465b090daf098581cdb4c174634fe1e5279"
|
|
37
38
|
}
|