@principles/install-layout 0.1.0 → 0.2.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 CHANGED
@@ -1,12 +1,20 @@
1
1
  export declare const INSTALL_LAYOUT_VERSION: 1;
2
- export declare const INSTALL_LAYOUT_MODES: readonly ["canonical", "legacy"];
2
+ export declare const INSTALL_LAYOUT_MODES: readonly ['canonical', 'legacy'];
3
3
  export type InstallLayoutMode = (typeof INSTALL_LAYOUT_MODES)[number];
4
- export declare const INSTALL_HOSTS: readonly ["codex", "openclaw"];
4
+ export declare const INSTALL_HOSTS: readonly ['codex', 'openclaw'];
5
5
  export type InstallHost = (typeof INSTALL_HOSTS)[number];
6
6
  export interface InstallManifest {
7
7
  layoutVersion: typeof INSTALL_LAYOUT_VERSION;
8
8
  mode: InstallLayoutMode;
9
9
  hosts: InstallHost[];
10
+ /**
11
+ * Canonical absolute paths of Workspaces the install serves (PRI-624 Slice C).
12
+ * Optional so pre-PRI-624 manifests stay valid; always normalized to
13
+ * resolved absolute paths and deduped. The installer merges the install
14
+ * Workspace in; the Companion worker discovers its per-Workspace workers
15
+ * from this list (SPEC §13 "canonical install manifest").
16
+ */
17
+ workspaces?: readonly string[];
10
18
  }
11
19
  export interface InstallLayoutPaths {
12
20
  homeDir: string;
@@ -37,6 +45,8 @@ export interface ResolveInstallLayoutOptions {
37
45
  canonicalRuntimeExists: boolean;
38
46
  legacyExtensionExists: boolean;
39
47
  }
48
+ /** Merge one Workspace into a manifest's workspace list (installer write path). Idempotent by canonical path. */
49
+ export declare function mergeInstallManifestWorkspaces(current: InstallManifest | undefined, workspaceDir: string): string[];
40
50
  export declare function parseInstallManifest(value: unknown): {
41
51
  manifest?: InstallManifest;
42
52
  error?: string;
package/dist/index.js CHANGED
@@ -8,6 +8,36 @@ function isRecord(value) {
8
8
  function isHost(value) {
9
9
  return typeof value === 'string' && INSTALL_HOSTS.includes(value);
10
10
  }
11
+ /** Workspace entries must be non-empty absolute paths — the Companion spawns per-Workspace workers keyed by canonical path. */
12
+ function parseWorkspaceList(value) {
13
+ if (value === undefined)
14
+ return { workspaces: [] };
15
+ if (!Array.isArray(value))
16
+ return { error: 'install_manifest_malformed: workspaces must be non-empty absolute paths' };
17
+ const resolved = [];
18
+ for (const entry of value) {
19
+ if (typeof entry !== 'string' || entry.length === 0 || !path.isAbsolute(entry)) {
20
+ return { error: 'install_manifest_malformed: workspaces must be non-empty absolute paths' };
21
+ }
22
+ const canonical = path.resolve(entry);
23
+ if (!resolved.includes(canonical))
24
+ resolved.push(canonical);
25
+ }
26
+ return { workspaces: resolved };
27
+ }
28
+ /** Merge one Workspace into a manifest's workspace list (installer write path). Idempotent by canonical path. */
29
+ export function mergeInstallManifestWorkspaces(current, workspaceDir) {
30
+ const canonical = path.resolve(workspaceDir);
31
+ const resolved = [];
32
+ for (const entry of current?.workspaces ?? []) {
33
+ const normalized = path.resolve(entry);
34
+ if (!resolved.includes(normalized))
35
+ resolved.push(normalized);
36
+ }
37
+ if (!resolved.includes(canonical))
38
+ resolved.push(canonical);
39
+ return resolved;
40
+ }
11
41
  export function parseInstallManifest(value) {
12
42
  if (!isRecord(value))
13
43
  return { error: 'install_manifest_malformed: expected an object' };
@@ -21,7 +51,10 @@ export function parseInstallManifest(value) {
21
51
  return { error: 'install_manifest_malformed: hosts must contain codex and/or openclaw' };
22
52
  }
23
53
  const hosts = [...new Set(value.hosts)];
24
- return { manifest: { layoutVersion: INSTALL_LAYOUT_VERSION, mode: value.mode, hosts } };
54
+ const workspaces = parseWorkspaceList(value.workspaces);
55
+ if (workspaces.error)
56
+ return { error: workspaces.error };
57
+ return { manifest: { layoutVersion: INSTALL_LAYOUT_VERSION, mode: value.mode, hosts, workspaces: workspaces.workspaces } };
25
58
  }
26
59
  export function getInstallLayoutPaths(homeDir) {
27
60
  const home = path.resolve(homeDir);
@@ -1,5 +1,6 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { getInstallLayoutPaths, getConsoleServerEntry, parseInstallManifest, resolveInstallLayout } from './index.js';
2
+ import * as path from 'node:path';
3
+ import { getInstallLayoutPaths, getConsoleServerEntry, parseInstallManifest, resolveInstallLayout, mergeInstallManifestWorkspaces, } from './index.js';
3
4
  describe('install layout', () => {
4
5
  it('resolves a canonical Codex-only install without OpenClaw', () => {
5
6
  const paths = getInstallLayoutPaths('C:/Users/alice');
@@ -31,4 +32,38 @@ describe('install layout', () => {
31
32
  error: 'install_manifest_malformed: hosts must contain codex and/or openclaw',
32
33
  });
33
34
  });
35
+ it('accepts a manifest without workspaces (pre-PRI-624 manifests stay valid)', () => {
36
+ const { manifest, error } = parseInstallManifest({ layoutVersion: 1, mode: 'canonical', hosts: ['codex'] });
37
+ expect(error).toBeUndefined();
38
+ expect(manifest?.workspaces).toEqual([]);
39
+ });
40
+ it('parses workspaces as canonical absolute paths, deduped in insertion order', () => {
41
+ const { manifest, error } = parseInstallManifest({
42
+ layoutVersion: 1,
43
+ mode: 'canonical',
44
+ hosts: ['codex'],
45
+ workspaces: ['D:/Code/ws-a', 'D:\\Code\\ws-a', 'C:/work/ws b'],
46
+ });
47
+ expect(error).toBeUndefined();
48
+ expect(manifest?.workspaces?.length).toBe(2);
49
+ expect(manifest?.workspaces?.[0]).toBe(path.resolve('D:/Code/ws-a'));
50
+ expect(manifest?.workspaces).toContain(path.resolve('C:/work/ws b'));
51
+ });
52
+ it('rejects malformed workspace entries loudly', () => {
53
+ expect(parseInstallManifest({ layoutVersion: 1, mode: 'canonical', hosts: ['codex'], workspaces: [''] })).toEqual({
54
+ error: 'install_manifest_malformed: workspaces must be non-empty absolute paths',
55
+ });
56
+ expect(parseInstallManifest({ layoutVersion: 1, mode: 'canonical', hosts: ['codex'], workspaces: ['ws-a'] })).toEqual({
57
+ error: 'install_manifest_malformed: workspaces must be non-empty absolute paths',
58
+ });
59
+ });
60
+ it('merges a workspace into an existing manifest without duplicates', () => {
61
+ const merged = mergeInstallManifestWorkspaces({ layoutVersion: 1, mode: 'canonical', hosts: ['codex'], workspaces: ['D:/Code/ws-a'] }, 'D:\\Code\\ws-a');
62
+ expect(merged.length).toBe(1);
63
+ expect(merged[0]).toBe(path.resolve('D:/Code/ws-a'));
64
+ });
65
+ it('merge accepts unknown current manifests and starts a fresh list', () => {
66
+ const merged = mergeInstallManifestWorkspaces(undefined, 'D:/Code/ws-b');
67
+ expect(merged).toEqual([path.resolve('D:/Code/ws-b')]);
68
+ });
34
69
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@principles/install-layout",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -10,9 +10,17 @@
10
10
  "default": "./dist/index.js"
11
11
  }
12
12
  },
13
- "files": ["dist"],
13
+ "files": [
14
+ "dist"
15
+ ],
14
16
  "license": "MIT",
15
- "publishConfig": { "access": "public" },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/csuzngjh/principles.git"
20
+ },
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
16
24
  "scripts": {
17
25
  "build": "tsc",
18
26
  "test": "npm run build && vitest run"