@principles/pd-cli 1.142.3 → 1.143.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.
Files changed (57) hide show
  1. package/dist/commands/__tests__/telemetry-flag-wiring.test.d.ts +9 -0
  2. package/dist/commands/__tests__/telemetry-flag-wiring.test.d.ts.map +1 -0
  3. package/dist/commands/__tests__/telemetry-flag-wiring.test.js +76 -0
  4. package/dist/commands/__tests__/telemetry-flag-wiring.test.js.map +1 -0
  5. package/dist/commands/__tests__/version.test.d.ts +2 -0
  6. package/dist/commands/__tests__/version.test.d.ts.map +1 -0
  7. package/dist/commands/__tests__/version.test.js +134 -0
  8. package/dist/commands/__tests__/version.test.js.map +1 -0
  9. package/dist/commands/console.d.ts.map +1 -1
  10. package/dist/commands/console.js +29 -10
  11. package/dist/commands/console.js.map +1 -1
  12. package/dist/commands/pain-record.d.ts.map +1 -1
  13. package/dist/commands/pain-record.js +6 -15
  14. package/dist/commands/pain-record.js.map +1 -1
  15. package/dist/commands/runtime-activation.d.ts +29 -0
  16. package/dist/commands/runtime-activation.d.ts.map +1 -1
  17. package/dist/commands/runtime-activation.js +70 -31
  18. package/dist/commands/runtime-activation.js.map +1 -1
  19. package/dist/commands/runtime-internalization-enqueue-successors.d.ts.map +1 -1
  20. package/dist/commands/runtime-internalization-enqueue-successors.js +2 -2
  21. package/dist/commands/runtime-internalization-enqueue-successors.js.map +1 -1
  22. package/dist/commands/runtime-internalization-retry.d.ts.map +1 -1
  23. package/dist/commands/runtime-internalization-retry.js +14 -0
  24. package/dist/commands/runtime-internalization-retry.js.map +1 -1
  25. package/dist/commands/telemetry.d.ts +82 -0
  26. package/dist/commands/telemetry.d.ts.map +1 -0
  27. package/dist/commands/telemetry.js +268 -0
  28. package/dist/commands/telemetry.js.map +1 -0
  29. package/dist/commands/version.d.ts +11 -0
  30. package/dist/commands/version.d.ts.map +1 -0
  31. package/dist/commands/version.js +48 -0
  32. package/dist/commands/version.js.map +1 -0
  33. package/dist/index.js +35 -2
  34. package/dist/index.js.map +1 -1
  35. package/dist/services/version-report.d.ts +44 -0
  36. package/dist/services/version-report.d.ts.map +1 -0
  37. package/dist/services/version-report.js +152 -0
  38. package/dist/services/version-report.js.map +1 -0
  39. package/package.json +2 -1
  40. package/src/commands/__tests__/telemetry-flag-wiring.test.ts +84 -0
  41. package/src/commands/__tests__/version.test.ts +141 -0
  42. package/src/commands/console.ts +34 -11
  43. package/src/commands/pain-record.ts +6 -13
  44. package/src/commands/runtime-activation.ts +74 -22
  45. package/src/commands/runtime-internalization-enqueue-successors.ts +3 -2
  46. package/src/commands/runtime-internalization-retry.ts +14 -0
  47. package/src/commands/telemetry.ts +349 -0
  48. package/src/commands/version.ts +49 -0
  49. package/src/index.ts +37 -2
  50. package/src/services/version-report.ts +203 -0
  51. package/tests/commands/console-open.test.ts +55 -5
  52. package/tests/commands/pain-record.test.ts +26 -0
  53. package/tests/commands/runtime-activation-shadow-telemetry.test.ts +130 -0
  54. package/tests/commands/runtime-activation.test.ts +67 -1
  55. package/tests/commands/runtime-internalization-enqueue-successors.test.ts +6 -1
  56. package/tests/commands/telemetry.test.ts +190 -0
  57. package/tests/e2e/cli-full-flow.test.ts +38 -3
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Canonical version report builder (SPEC §12).
3
+ *
4
+ * `pd --version` prints one stable short text line; `pd version --json`
5
+ * exposes the full canonical report: productVersion, releaseId, components,
6
+ * bootstrapVersion, channel, source, generation, health, and the last
7
+ * transaction. The canonical product identity comes from the installation
8
+ * state under ~/.pd — never from a checkout's package.json.
9
+ *
10
+ * The record shapes here mirror the canonical contracts owned by
11
+ * create-principles-disciple/src/update/ (the deep ReleaseManager module);
12
+ * this reader is deliberately thin: it READS installation state and never
13
+ * performs update logic.
14
+ */
15
+ import * as fs from 'node:fs';
16
+ import * as os from 'node:os';
17
+ import * as path from 'node:path';
18
+ export class VersionReportError extends Error {
19
+ reason;
20
+ nextAction;
21
+ constructor(reason, message, nextAction) {
22
+ super(message);
23
+ this.name = 'VersionReportError';
24
+ this.reason = reason;
25
+ this.nextAction = nextAction;
26
+ }
27
+ }
28
+ function isPlainObject(value) {
29
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
30
+ }
31
+ function readJsonIfPresent(filePath) {
32
+ if (!fs.existsSync(filePath))
33
+ return null;
34
+ let value;
35
+ try {
36
+ value = JSON.parse(fs.readFileSync(filePath, 'utf8'));
37
+ }
38
+ catch (error) {
39
+ throw new VersionReportError('state_corrupt', `Installation state file is not valid JSON: ${filePath} (${error instanceof Error ? error.message : String(error)})`, 'Run the official installer recovery, or re-run the official installer to repair the installation record.');
40
+ }
41
+ if (!isPlainObject(value)) {
42
+ throw new VersionReportError('state_corrupt', `Installation state file is not a JSON object: ${filePath}`, 'Run the official installer recovery, or re-run the official installer to repair the installation record.');
43
+ }
44
+ return value;
45
+ }
46
+ /** `pd --version` short stable text contract. */
47
+ export function formatShortVersion(report) {
48
+ return `Principles Disciple ${report.productVersion} (${report.releaseId.slice(0, 12)})`;
49
+ }
50
+ function buildLegacyOverlayReport(overlayDir, bootstrap) {
51
+ // The current official installer copies the plugin package directly into
52
+ // the OpenClaw extension root. Older overlay bundles kept it under plugin/;
53
+ // accept that layout only when the canonical root manifest is absent.
54
+ const rootManifest = readJsonIfPresent(path.join(overlayDir, 'package.json'));
55
+ const pluginManifest = rootManifest ?? readJsonIfPresent(path.join(overlayDir, 'plugin', 'package.json'));
56
+ const version = pluginManifest?.version;
57
+ if (typeof version !== 'string' || version.length === 0) {
58
+ throw new VersionReportError('legacy_overlay_manifest_invalid', `The legacy overlay at ${overlayDir} has no readable plugin version.`, 'Re-install PD with the official installer to migrate this installation into the supported layout.');
59
+ }
60
+ const bootstrapVersion = bootstrap?.bootstrapVersion;
61
+ return {
62
+ productVersion: version,
63
+ releaseId: '0'.repeat(64),
64
+ components: { plugin: version },
65
+ bootstrapVersion: typeof bootstrapVersion === 'string' ? bootstrapVersion : 'unknown',
66
+ channel: 'stable',
67
+ source: 'official-legacy-overlay',
68
+ generation: 0,
69
+ health: 'degraded',
70
+ lastTransaction: null,
71
+ };
72
+ }
73
+ function readLastTransaction(historyPath) {
74
+ if (!fs.existsSync(historyPath))
75
+ return null;
76
+ const lines = fs.readFileSync(historyPath, 'utf8').split('\n').filter((line) => line.trim().length > 0);
77
+ const lastLine = lines[lines.length - 1];
78
+ if (lastLine === undefined)
79
+ return null;
80
+ let value;
81
+ try {
82
+ value = JSON.parse(lastLine);
83
+ }
84
+ catch (error) {
85
+ throw new VersionReportError('state_corrupt', `The last transaction record is not valid JSON: ${historyPath} (${error instanceof Error ? error.message : String(error)})`, 'Run the official installer recovery to reconcile the transaction journal before trusting the installed version.');
86
+ }
87
+ if (!isPlainObject(value))
88
+ return null;
89
+ const { transactionId, kind, outcome } = value;
90
+ if (typeof transactionId !== 'string' || typeof kind !== 'string' || typeof outcome !== 'string') {
91
+ return null;
92
+ }
93
+ return { id: transactionId, kind, outcome };
94
+ }
95
+ /**
96
+ * Builds the canonical version report from the installation state. Throws
97
+ * VersionReportError with an installer next-action when no installation
98
+ * exists at all.
99
+ */
100
+ export function buildVersionReport(homeDir = os.homedir()) {
101
+ const pdHome = path.join(homeDir, '.pd');
102
+ const active = readJsonIfPresent(path.join(pdHome, 'active.json'));
103
+ const bootstrap = readJsonIfPresent(path.join(pdHome, 'bootstrap', 'bootstrap.json'));
104
+ const installConfig = readJsonIfPresent(path.join(pdHome, 'install.json'));
105
+ const overlayDir = path.join(homeDir, '.openclaw', 'extensions', 'principles-disciple');
106
+ if (active === null && fs.existsSync(overlayDir)) {
107
+ return buildLegacyOverlayReport(overlayDir, bootstrap);
108
+ }
109
+ if (active === null && !fs.existsSync(pdHome)) {
110
+ throw new VersionReportError('not_installed', 'No PD installation was found under ~/.pd or the legacy overlay location.', 'Install PD with the official installer (npx create-principles-disciple), then run pd version again.');
111
+ }
112
+ if (active === null) {
113
+ throw new VersionReportError('active_record_missing', 'The ~/.pd installation exists but has no active release record.', 'Run the official installer to complete the installation, or re-run it to repair the record.');
114
+ }
115
+ const { generation, releaseId, productVersion } = active;
116
+ if (typeof generation !== 'number' || !Number.isSafeInteger(generation) || generation < 1
117
+ || typeof releaseId !== 'string' || releaseId.length === 0
118
+ || typeof productVersion !== 'string' || productVersion.length === 0) {
119
+ throw new VersionReportError('active_record_corrupt', 'The active release record under ~/.pd is malformed.', 'Run the official installer recovery, or re-run the official installer to repair the installation record.');
120
+ }
121
+ const releaseDir = path.join(pdHome, 'releases', releaseId);
122
+ const releaseManifest = readJsonIfPresent(path.join(releaseDir, 'metadata.json'));
123
+ const releaseMetadataMatchesActive = releaseManifest !== null
124
+ && releaseManifest.productVersion === productVersion
125
+ && releaseManifest.releaseId === releaseId
126
+ && releaseManifest.metadataDigest === active.releaseMetadataDigest;
127
+ const health = releaseManifest === null
128
+ ? 'degraded'
129
+ : releaseMetadataMatchesActive ? 'healthy' : 'corrupt';
130
+ const components = {};
131
+ for (const component of ['plugin', 'console', 'core', 'pd-cli', 'host-runtime', 'install-layout']) {
132
+ const manifest = readJsonIfPresent(path.join(releaseDir, component, 'package.json'));
133
+ const version = manifest?.version;
134
+ if (typeof version === 'string') {
135
+ components[component] = version;
136
+ }
137
+ }
138
+ const bootstrapVersion = bootstrap?.bootstrapVersion;
139
+ const channelValue = installConfig?.channel;
140
+ return {
141
+ productVersion,
142
+ releaseId,
143
+ components,
144
+ bootstrapVersion: typeof bootstrapVersion === 'string' ? bootstrapVersion : 'unknown',
145
+ channel: channelValue === 'candidate' ? 'candidate' : 'stable',
146
+ source: 'official-installer',
147
+ generation,
148
+ health,
149
+ lastTransaction: readLastTransaction(path.join(pdHome, 'logs', 'history.jsonl')),
150
+ };
151
+ }
152
+ //# sourceMappingURL=version-report.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version-report.js","sourceRoot":"","sources":["../../src/services/version-report.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@principles/pd-cli",
3
- "version": "1.142.3",
3
+ "version": "1.143.0",
4
4
  "description": "PD CLI — Pain recording, sample management, and governance tasks for Principles Disciple",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,6 +16,7 @@
16
16
  "@principles/core": "^1.74.1",
17
17
  "@principles/codex-adapter": "^0.1.0",
18
18
  "@principles/host-runtime": "^0.1.0",
19
+ "@principles/install-layout": "0.1.0",
19
20
  "principles-disciple": "^1.74.1",
20
21
  "better-sqlite3": "^13.0.3",
21
22
  "commander": "^12.0.0",
@@ -0,0 +1,84 @@
1
+ /**
2
+ * pd telemetry — parser-level flag wiring tests (PRI-597, CLI Operator Gate
3
+ * rule 7 / EP-04).
4
+ *
5
+ * Exercises the real Commander tree via registerTelemetryCommand; actions are
6
+ * captured so no handler logic runs.
7
+ */
8
+
9
+ import { describe, it, expect } from 'vitest';
10
+ import { Command } from 'commander';
11
+ import { registerTelemetryCommand } from '../telemetry.js';
12
+
13
+ function freshProgram(): Command {
14
+ const program = new Command();
15
+ program.name('pd').exitOverride();
16
+ return program;
17
+ }
18
+
19
+ function requireCmd(cmd: Command | undefined, name: string): Command {
20
+ if (cmd === undefined) {
21
+ throw new Error(`Command '${name}' not found in tree`);
22
+ }
23
+ return cmd;
24
+ }
25
+
26
+ function telemetrySub(name: string): Command {
27
+ const program = freshProgram();
28
+ registerTelemetryCommand(program);
29
+ const telemetry = requireCmd(program.commands.find((c) => c.name() === 'telemetry'), 'telemetry');
30
+ return requireCmd(telemetry.commands.find((c) => c.name() === name), `telemetry ${name}`);
31
+ }
32
+
33
+ describe('pd telemetry — command registration', () => {
34
+ it('registers the telemetry group with all five subcommands', () => {
35
+ const program = freshProgram();
36
+ registerTelemetryCommand(program);
37
+ const telemetry = requireCmd(program.commands.find((c) => c.name() === 'telemetry'), 'telemetry');
38
+ const subNames = telemetry.commands.map((c) => c.name()).sort();
39
+ expect(subNames).toEqual(['disable', 'enable', 'preview', 'reset', 'status']);
40
+ });
41
+
42
+ it('status registers --workspace (-w) and --json', () => {
43
+ const status = telemetrySub('status');
44
+ expect(status.options.find((o) => o.long === '--workspace')?.short).toBe('-w');
45
+ expect(status.options.find((o) => o.long === '--json')).toBeDefined();
46
+ });
47
+
48
+ it('enable/disable/reset register the dry-run/confirm pair (cli-4) plus --workspace and --json', () => {
49
+ for (const name of ['enable', 'disable', 'reset']) {
50
+ const cmd = telemetrySub(name);
51
+ expect(cmd.options.find((o) => o.long === '--dry-run'), name).toBeDefined();
52
+ expect(cmd.options.find((o) => o.long === '--confirm'), name).toBeDefined();
53
+ expect(cmd.options.find((o) => o.long === '--workspace'), name).toBeDefined();
54
+ expect(cmd.options.find((o) => o.long === '--json'), name).toBeDefined();
55
+ }
56
+ });
57
+
58
+ it('preview registers --workspace (-w) and --json', () => {
59
+ const preview = telemetrySub('preview');
60
+ expect(preview.options.find((o) => o.long === '--workspace')?.short).toBe('-w');
61
+ expect(preview.options.find((o) => o.long === '--json')).toBeDefined();
62
+ });
63
+
64
+ it('parser dispatches telemetry enable with --confirm as opts.confirm === true', async () => {
65
+ const program = freshProgram();
66
+ registerTelemetryCommand(program);
67
+ const holder: { opts: Record<string, unknown> | null } = { opts: null };
68
+ const telemetry = requireCmd(program.commands.find((c) => c.name() === 'telemetry'), 'telemetry');
69
+ const enable = requireCmd(telemetry.commands.find((c) => c.name() === 'enable'), 'enable');
70
+ enable.action((...args: unknown[]) => {
71
+ for (let i = args.length - 1; i >= 0; i--) {
72
+ const arg = args[i];
73
+ if (arg !== null && typeof arg === 'object' && !(arg instanceof Command)) {
74
+ holder.opts = arg as Record<string, unknown>;
75
+ break;
76
+ }
77
+ }
78
+ });
79
+ await program.parseAsync(['node', 'pd', 'telemetry', 'enable', '--confirm', '--json']);
80
+ expect(holder.opts).not.toBeNull();
81
+ expect(holder.opts?.confirm).toBe(true);
82
+ expect(holder.opts?.json).toBe(true);
83
+ });
84
+ });
@@ -0,0 +1,141 @@
1
+ import * as fs from 'node:fs';
2
+ import * as os from 'node:os';
3
+ import * as path from 'node:path';
4
+ import { afterEach, describe, expect, it } from 'vitest';
5
+ import { buildVersionReport, formatShortVersion, VersionReportError } from '../../services/version-report.js';
6
+
7
+ const temporaryDirectories: string[] = [];
8
+
9
+ function tempHome(): string {
10
+ const root = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), 'pd-version-'));
11
+ temporaryDirectories.push(root);
12
+ return root;
13
+ }
14
+
15
+ function writeDualSlotHome(home: string): void {
16
+ const pdHome = path.join(home, '.pd');
17
+ fs.mkdirSync(path.join(pdHome, 'releases', 'b'.repeat(64), 'plugin'), { recursive: true });
18
+ fs.mkdirSync(path.join(pdHome, 'bootstrap'), { recursive: true });
19
+ fs.mkdirSync(path.join(pdHome, 'logs'), { recursive: true });
20
+ fs.writeFileSync(path.join(pdHome, 'active.json'), JSON.stringify({
21
+ schemaVersion: 1,
22
+ generation: 4,
23
+ releaseId: 'b'.repeat(64),
24
+ releaseMetadataDigest: '2'.repeat(64),
25
+ previousReleaseId: 'a'.repeat(64),
26
+ transactionId: 'txn-9',
27
+ productVersion: '1.223.0',
28
+ }));
29
+ fs.writeFileSync(path.join(pdHome, 'bootstrap', 'bootstrap.json'), JSON.stringify({
30
+ bootstrapVersion: '1.1.0',
31
+ installedAt: '2026-08-25T00:00:00Z',
32
+ }));
33
+ fs.writeFileSync(path.join(pdHome, 'install.json'), JSON.stringify({ channel: 'candidate', autoCheck: true }));
34
+ fs.writeFileSync(path.join(pdHome, 'releases', 'b'.repeat(64), 'metadata.json'), JSON.stringify({
35
+ productVersion: '1.223.0',
36
+ releaseId: 'b'.repeat(64),
37
+ metadataDigest: '2'.repeat(64),
38
+ }));
39
+ fs.writeFileSync(path.join(pdHome, 'releases', 'b'.repeat(64), 'plugin', 'package.json'), JSON.stringify({ version: '1.76.1' }));
40
+ fs.writeFileSync(path.join(pdHome, 'logs', 'history.jsonl'), [
41
+ JSON.stringify({ at: '2026-08-24T00:00:00Z', kind: 'update', outcome: 'succeeded', transactionId: 'txn-8' }),
42
+ JSON.stringify({ at: '2026-08-25T00:00:00Z', kind: 'recovery', outcome: 'recovered', transactionId: 'txn-9' }),
43
+ ].join('\n') + '\n');
44
+ }
45
+
46
+ afterEach(() => {
47
+ while (temporaryDirectories.length > 0) {
48
+ const directory = temporaryDirectories.pop();
49
+ if (directory) fs.rmSync(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 200 });
50
+ }
51
+ });
52
+
53
+ describe('canonical version report (SPEC 12 / 18-1, 18-10)', () => {
54
+ it('reports the canonical product identity from ~/.pd, not package.json', () => {
55
+ const home = tempHome();
56
+ writeDualSlotHome(home);
57
+ const report = buildVersionReport(home);
58
+ expect(report).toMatchObject({
59
+ productVersion: '1.223.0',
60
+ releaseId: 'b'.repeat(64),
61
+ bootstrapVersion: '1.1.0',
62
+ channel: 'candidate',
63
+ source: 'official-installer',
64
+ generation: 4,
65
+ health: 'healthy',
66
+ });
67
+ expect(report.components.plugin).toBe('1.76.1');
68
+ expect(report.lastTransaction).toEqual({ id: 'txn-9', kind: 'recovery', outcome: 'recovered' });
69
+ expect(formatShortVersion(report)).toBe(`Principles Disciple 1.223.0 (${'b'.repeat(12)})`);
70
+ });
71
+
72
+ it('classifies a legacy overlay installation with an installer next action', () => {
73
+ const home = tempHome();
74
+ // The current official installer creates ~/.pd/bin before the transactional
75
+ // dual-slot layout is activated. That support directory must not make a
76
+ // valid legacy overlay look like a corrupt partial dual-slot install.
77
+ fs.mkdirSync(path.join(home, '.pd', 'bin'), { recursive: true });
78
+ const overlay = path.join(home, '.openclaw', 'extensions', 'principles-disciple');
79
+ fs.mkdirSync(overlay, { recursive: true });
80
+ fs.writeFileSync(path.join(overlay, 'package.json'), JSON.stringify({ version: '1.218.0' }));
81
+ const report = buildVersionReport(home);
82
+ expect(report).toMatchObject({
83
+ productVersion: '1.218.0',
84
+ source: 'official-legacy-overlay',
85
+ health: 'degraded',
86
+ generation: 0,
87
+ });
88
+ });
89
+
90
+ it('retains compatibility with the older nested legacy overlay manifest', () => {
91
+ const home = tempHome();
92
+ const overlay = path.join(home, '.openclaw', 'extensions', 'principles-disciple', 'plugin');
93
+ fs.mkdirSync(overlay, { recursive: true });
94
+ fs.writeFileSync(path.join(overlay, 'package.json'), JSON.stringify({ version: '1.202.0' }));
95
+ expect(buildVersionReport(home)).toMatchObject({
96
+ productVersion: '1.202.0', source: 'official-legacy-overlay', health: 'degraded',
97
+ });
98
+ });
99
+
100
+ it('refuses with a structured reason and next action when nothing is installed', () => {
101
+ const home = tempHome();
102
+ try {
103
+ buildVersionReport(home);
104
+ throw new Error('expected VersionReportError');
105
+ } catch (error) {
106
+ expect(error).toBeInstanceOf(VersionReportError);
107
+ const refusal = error as VersionReportError;
108
+ expect(refusal.reason).toBe('not_installed');
109
+ expect(refusal.nextAction).toMatch(/official installer/i);
110
+ }
111
+ });
112
+
113
+ it('reports degraded health when the active release directory is incomplete', () => {
114
+ const home = tempHome();
115
+ writeDualSlotHome(home);
116
+ fs.rmSync(path.join(home, '.pd', 'releases', 'b'.repeat(64), 'metadata.json'), { force: true });
117
+ const report = buildVersionReport(home);
118
+ expect(report.health).toBe('degraded');
119
+ });
120
+
121
+ it('reports corrupt health when release metadata exists but disagrees with active.json', () => {
122
+ const home = tempHome();
123
+ writeDualSlotHome(home);
124
+ fs.writeFileSync(path.join(home, '.pd', 'releases', 'b'.repeat(64), 'metadata.json'), '{}');
125
+ const report = buildVersionReport(home);
126
+ expect(report.health).toBe('corrupt');
127
+ });
128
+
129
+ it('refuses a malformed active record loudly instead of guessing', () => {
130
+ const home = tempHome();
131
+ fs.mkdirSync(path.join(home, '.pd'), { recursive: true });
132
+ fs.writeFileSync(path.join(home, '.pd', 'active.json'), JSON.stringify({ generation: 'four' }));
133
+ try {
134
+ buildVersionReport(home);
135
+ throw new Error('expected VersionReportError');
136
+ } catch (error) {
137
+ expect(error).toBeInstanceOf(VersionReportError);
138
+ expect((error as VersionReportError).reason).toBe('active_record_corrupt');
139
+ }
140
+ });
141
+ });
@@ -2,6 +2,13 @@ import * as path from 'path';
2
2
  import * as fs from 'fs';
3
3
  import { spawn, type ChildProcess } from 'child_process';
4
4
  import { resolveWorkspaceDir } from '../resolve-workspace.js';
5
+ import {
6
+ getConsoleServerEntry,
7
+ getConsoleWebIndex,
8
+ getInstallLayoutPaths,
9
+ resolveInstallLayout,
10
+ type InstallLayoutMode,
11
+ } from '@principles/install-layout';
5
12
  import {
6
13
  planConsoleLaunch,
7
14
  openBrowser,
@@ -36,11 +43,25 @@ interface ConsoleOptions {
36
43
  json?: boolean;
37
44
  }
38
45
 
39
- function getConsoleDir(): string | null {
46
+ function getConsoleDir(): { dir: string; mode: InstallLayoutMode } | null {
40
47
  const homeDir = process.env.HOME || process.env.USERPROFILE;
41
48
  if (!homeDir) return null;
42
- const consoleDir = path.join(homeDir, '.openclaw', 'extensions', 'principles-disciple', 'console');
43
- return fs.existsSync(consoleDir) ? consoleDir : null;
49
+ const paths = getInstallLayoutPaths(homeDir);
50
+ let manifest: unknown;
51
+ try {
52
+ manifest = JSON.parse(fs.readFileSync(paths.manifest, 'utf8')) as unknown;
53
+ } catch {
54
+ manifest = undefined;
55
+ }
56
+ const resolved = resolveInstallLayout({
57
+ homeDir,
58
+ manifest,
59
+ canonicalRuntimeExists: fs.existsSync(paths.runtimeDir),
60
+ legacyExtensionExists: fs.existsSync(paths.openClawExtensionDir),
61
+ });
62
+ if (resolved.mode === 'missing') return null;
63
+ const dir = resolved.mode === 'canonical' ? paths.consoleDir : path.join(paths.openClawExtensionDir, 'console');
64
+ return { dir, mode: resolved.mode };
44
65
  }
45
66
 
46
67
  export async function handleConsole(opts: ConsoleOptions = {}): Promise<void> {
@@ -48,8 +69,8 @@ export async function handleConsole(opts: ConsoleOptions = {}): Promise<void> {
48
69
  ? path.resolve(opts.workspace)
49
70
  : resolveWorkspaceDir();
50
71
 
51
- const consoleDir = getConsoleDir();
52
- if (!consoleDir) {
72
+ const consoleLocation = getConsoleDir();
73
+ if (!consoleLocation) {
53
74
  const msg = 'pd-console is not installed. Run: npx create-principles-disciple to install.';
54
75
  if (opts.json) {
55
76
  console.log(JSON.stringify({ success: false, reason: msg, nextAction: 'npx create-principles-disciple' }));
@@ -60,7 +81,8 @@ export async function handleConsole(opts: ConsoleOptions = {}): Promise<void> {
60
81
  return;
61
82
  }
62
83
 
63
- const serverEntry = path.join(consoleDir, 'dist', 'server.js');
84
+ const paths = getInstallLayoutPaths(process.env.HOME || process.env.USERPROFILE || '.');
85
+ const serverEntry = getConsoleServerEntry(paths, consoleLocation.mode);
64
86
  if (!fs.existsSync(serverEntry)) {
65
87
  const msg = `Console server entry not found at ${serverEntry}. Re-run installer.`;
66
88
  if (opts.json) {
@@ -74,7 +96,7 @@ export async function handleConsole(opts: ConsoleOptions = {}): Promise<void> {
74
96
 
75
97
  // EP-06 regression guard (PR #1169): verify web UI bundle exists before launch.
76
98
  // Without dist/web/index.html the server returns 404 "Run npm run build:ui first".
77
- const webIndex = path.join(consoleDir, 'dist', 'web', 'index.html');
99
+ const webIndex = getConsoleWebIndex(paths, consoleLocation.mode);
78
100
  if (!fs.existsSync(webIndex)) {
79
101
  const msg = `Console web UI not found at ${webIndex}. The console bundle is corrupted. Re-run installer.`;
80
102
  if (opts.json) {
@@ -274,8 +296,8 @@ export async function handleConsoleOpen(opts: ConsoleOpenOptions = {}): Promise<
274
296
  }
275
297
 
276
298
  // 3) Check that the console runtime is installed (ERR-040: fail loud if missing)
277
- const consoleDir = getConsoleDir();
278
- if (!consoleDir) {
299
+ const consoleLocation = getConsoleDir();
300
+ if (!consoleLocation) {
279
301
  const result: ConsoleLaunchResult = {
280
302
  status: 'failed',
281
303
  url: '',
@@ -296,7 +318,8 @@ export async function handleConsoleOpen(opts: ConsoleOpenOptions = {}): Promise<
296
318
  process.exit(1);
297
319
  return;
298
320
  }
299
- const serverEntry = path.join(consoleDir, 'dist', 'server.js');
321
+ const paths = getInstallLayoutPaths(process.env.HOME || process.env.USERPROFILE || '.');
322
+ const serverEntry = getConsoleServerEntry(paths, consoleLocation.mode);
300
323
  if (!fs.existsSync(serverEntry)) {
301
324
  const result: ConsoleLaunchResult = {
302
325
  status: 'failed',
@@ -322,7 +345,7 @@ export async function handleConsoleOpen(opts: ConsoleOpenOptions = {}): Promise<
322
345
  // EP-06 regression guard (PR #1169): verify web UI bundle exists before launch.
323
346
  // Without dist/web/index.html the server returns 404 "Run npm run build:ui first"
324
347
  // — a fatal first-impression bug for new users.
325
- const webIndex = path.join(consoleDir, 'dist', 'web', 'index.html');
348
+ const webIndex = getConsoleWebIndex(paths, consoleLocation.mode);
326
349
  if (!fs.existsSync(webIndex)) {
327
350
  const result: ConsoleLaunchResult = {
328
351
  status: 'failed',
@@ -12,6 +12,7 @@ import {
12
12
  resolveRuntimeConfig,
13
13
  isRuntimeConfigError,
14
14
  isFeatureEnabled,
15
+ isBuiltinPiAiProvider,
15
16
  } from '@principles/core/runtime-v2';
16
17
  import { resolveWorkspaceDir } from '../resolve-workspace.js';
17
18
  import { loadPdConfig, computeFlagsFromLoadResult } from '../services/pd-config-loader.js';
@@ -141,19 +142,11 @@ export async function handlePainRecord(opts: RecordOptions): Promise<void> {
141
142
  if (!config.model) missing.push('model');
142
143
  if (!config.apiKeyEnv) missing.push('apiKeyEnv');
143
144
  if (config.provider) {
144
- try {
145
- // pi-ai is an optional peer; the package may or may not resolve depending on
146
- // the package manager (pnpm local vs npm in CI). @ts-ignore (not @ts-expect-error)
147
- // is intentional: it suppresses TS2307 when absent and is silently ignored when present.
148
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
149
- // @ts-ignore
150
- const { getProviders } = await import('@mariozechner/pi-ai');
151
- const knownProviders = getProviders() as readonly string[];
152
- if (!knownProviders.includes(config.provider) && !config.baseUrl) {
153
- missing.push('baseUrl');
154
- }
155
- } catch {
156
- // pi-ai may not be available
145
+ // PRI-621 PR2 review: the pi-ai catalog lookup is a core capability —
146
+ // the CLI queries it through @principles/core and must not depend on
147
+ // pi-ai directly (EP-06).
148
+ if (!isBuiltinPiAiProvider(config.provider) && !config.baseUrl) {
149
+ missing.push('baseUrl');
157
150
  }
158
151
  }
159
152