@principles/install-layout 0.3.1 → 0.4.0
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 +40 -1
- package/dist/index.js +88 -6
- package/package.json +1 -1
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;
|
|
@@ -58,6 +79,8 @@ export interface LayoutResolution {
|
|
|
58
79
|
manifest?: InstallManifest;
|
|
59
80
|
reason?: string;
|
|
60
81
|
nextAction?: string;
|
|
82
|
+
/** Forward-compatibility warnings surfaced while parsing the manifest. */
|
|
83
|
+
warnings?: readonly InstallManifestWarning[];
|
|
61
84
|
}
|
|
62
85
|
export interface ResolveInstallLayoutOptions {
|
|
63
86
|
homeDir: string;
|
|
@@ -65,10 +88,26 @@ export interface ResolveInstallLayoutOptions {
|
|
|
65
88
|
canonicalRuntimeExists: boolean;
|
|
66
89
|
legacyExtensionExists: boolean;
|
|
67
90
|
}
|
|
68
|
-
/**
|
|
91
|
+
/**
|
|
92
|
+
* Merge one Workspace into a manifest's workspace list (installer write path). Idempotent by canonical path. */
|
|
69
93
|
export declare function mergeInstallManifestWorkspaces(current: InstallManifest | undefined, workspaceDir: string): string[];
|
|
94
|
+
/**
|
|
95
|
+
* The `hosts` array a manifest WRITER must persist: every registration the
|
|
96
|
+
* reader recognized plus every unknown one, deduped, known hosts first.
|
|
97
|
+
* Writers that rebuild `hosts` from `manifest.hosts` alone silently DELETE a
|
|
98
|
+
* newer host's registration — the write-side twin of the read-side
|
|
99
|
+
* unknown-host brick (2026-10-16). `undefined` yields `[]` so the installer's
|
|
100
|
+
* fresh-install path keeps working unchanged.
|
|
101
|
+
*/
|
|
102
|
+
export declare function installManifestHostsForPersist(manifest: InstallManifest | undefined): string[];
|
|
103
|
+
/**
|
|
104
|
+
* Parse the install manifest. Structural damage fails closed with `error`;
|
|
105
|
+
* a host string this reader version does not know is a `warnings` entry and
|
|
106
|
+
* the manifest still parses (forward compatibility — see `InstallManifest.unknownHosts`).
|
|
107
|
+
*/
|
|
70
108
|
export declare function parseInstallManifest(value: unknown): {
|
|
71
109
|
manifest?: InstallManifest;
|
|
110
|
+
warnings?: readonly InstallManifestWarning[];
|
|
72
111
|
error?: string;
|
|
73
112
|
};
|
|
74
113
|
export declare function getInstallLayoutPaths(homeDir: string): InstallLayoutPaths;
|
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);
|
|
@@ -96,8 +157,28 @@ export function getWorkbuddyWorkspaceDir(homeDir) {
|
|
|
96
157
|
export function resolveInstallLayout(options) {
|
|
97
158
|
const paths = getInstallLayoutPaths(options.homeDir);
|
|
98
159
|
const parsed = parseInstallManifest(options.manifest);
|
|
160
|
+
const { warnings } = parsed;
|
|
99
161
|
if (parsed.manifest && parsed.manifest.mode === 'canonical' && options.canonicalRuntimeExists) {
|
|
100
|
-
return { mode: 'canonical', paths, manifest: parsed.manifest };
|
|
162
|
+
return { mode: 'canonical', paths, manifest: parsed.manifest, ...(warnings !== undefined ? { warnings } : {}) };
|
|
163
|
+
}
|
|
164
|
+
// RF-01 (review 2026-10-16): when a canonical runtime EXISTS, the manifest
|
|
165
|
+
// is the install-state authority. A manifest that was READ but cannot be
|
|
166
|
+
// honored (newer layoutVersion, structural damage) must refuse the whole
|
|
167
|
+
// resolution — falling through to the legacy extension dir would launch a
|
|
168
|
+
// runtime that does not match the installed protocol. Legacy fallback stays
|
|
169
|
+
// legitimate only when the reader genuinely saw NO manifest (absent file →
|
|
170
|
+
// `options.manifest === undefined`), i.e. a real legacy-only install, or
|
|
171
|
+
// when there is no canonical runtime to bypass.
|
|
172
|
+
if (options.canonicalRuntimeExists && options.manifest !== undefined && parsed.manifest === undefined) {
|
|
173
|
+
return {
|
|
174
|
+
mode: 'missing',
|
|
175
|
+
paths,
|
|
176
|
+
reason: parsed.error ?? 'install_manifest_malformed',
|
|
177
|
+
nextAction: parsed.error?.startsWith('install_manifest_unsupported_version')
|
|
178
|
+
? 'Update PD Companion from https://github.com/csuzngjh/principles/releases (companion-v* Windows installer); do not reinstall the healthy PD runtime.'
|
|
179
|
+
: 'Run the PD installer/updater to repair the structurally damaged installation manifest.',
|
|
180
|
+
...(warnings !== undefined ? { warnings } : {}),
|
|
181
|
+
};
|
|
101
182
|
}
|
|
102
183
|
if (options.legacyExtensionExists) {
|
|
103
184
|
return {
|
|
@@ -106,6 +187,7 @@ export function resolveInstallLayout(options) {
|
|
|
106
187
|
...(parsed.manifest ? { manifest: parsed.manifest } : {}),
|
|
107
188
|
reason: parsed.error ?? 'install_layout_legacy_fallback',
|
|
108
189
|
nextAction: 'Run the installer update to migrate the shared runtime to ~/.pd/runtime.',
|
|
190
|
+
...(warnings !== undefined ? { warnings } : {}),
|
|
109
191
|
};
|
|
110
192
|
}
|
|
111
193
|
return {
|