@principles/pd-cli 1.128.0 → 1.128.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.
@@ -0,0 +1,121 @@
1
+ /**
2
+ * PRI-455: SKILL ↔ CLI contract test.
3
+ *
4
+ * Parses SKILL.md templates for `pd <command>` references and verifies
5
+ * that each referenced command path exists in the CLI command tree.
6
+ *
7
+ * This prevents SKILL templates from referencing deleted or renamed commands.
8
+ *
9
+ * Covers BOTH `en` and `zh` skill directories so bilingual template updates
10
+ * are validated. Fails explicitly (does not silently skip) when the expected
11
+ * skills directory is missing — a missing directory is a real contract break.
12
+ */
13
+ import { describe, it, expect } from 'vitest';
14
+ import { execFileSync } from 'node:child_process';
15
+ import { readFileSync, readdirSync, existsSync } from 'node:fs';
16
+ import { join, resolve } from 'node:path';
17
+ import { getBuiltPdCliPath } from '../helpers/pd-cli-path.js';
18
+
19
+ /**
20
+ * Resolve the skills directory for a given language.
21
+ * Tests run from packages/pd-cli, so the plugin templates live one level up.
22
+ */
23
+ function resolveSkillsDir(lang: 'en' | 'zh'): string {
24
+ return resolve(
25
+ process.cwd(),
26
+ '..',
27
+ 'openclaw-plugin',
28
+ 'templates',
29
+ 'langs',
30
+ lang,
31
+ 'skills',
32
+ );
33
+ }
34
+
35
+ /**
36
+ * Extract `pd <command> [subcommand] ...` patterns from SKILL.md content.
37
+ * Matches lines like:
38
+ * pd pain record --reason ...
39
+ * pd runtime probe --json
40
+ * pd candidate list --workspace ...
41
+ * Returns full command paths (e.g. "pain record", "runtime probe").
42
+ */
43
+ function extractPdCommands(markdown: string): string[] {
44
+ const commands: string[] = [];
45
+ // Match `pd <words>` in code blocks and inline code
46
+ const pdPattern = /`pd\s+([\w-]+(?:\s+[\w-]+)*)/g;
47
+ let match: RegExpExecArray | null;
48
+ while ((match = pdPattern.exec(markdown)) !== null) {
49
+ // Take up to 3 path segments (e.g. "runtime internalization queue")
50
+ const parts = match[1].split(/\s+/).slice(0, 3);
51
+ // Filter out flags and options
52
+ const cleanParts = parts.filter((p) => !p.startsWith('--') && !p.startsWith('<'));
53
+ if (cleanParts.length > 0) {
54
+ commands.push(cleanParts.join(' '));
55
+ }
56
+ }
57
+ return [...new Set(commands)];
58
+ }
59
+
60
+ /**
61
+ * Check if a command path exists by running `pd <path> --help`.
62
+ * Returns true if the command is registered (exit code 0 or help output contains "Usage:").
63
+ */
64
+ function commandExists(commandPath: string): boolean {
65
+ const args = commandPath.split(' ');
66
+ try {
67
+ const output = execFileSync('node', [getBuiltPdCliPath(), ...args, '--help'], {
68
+ encoding: 'utf8',
69
+ cwd: process.cwd(),
70
+ timeout: 10000,
71
+ stdio: ['pipe', 'pipe', 'pipe'],
72
+ });
73
+ return output.includes('Usage:') || output.includes('Options:');
74
+ } catch {
75
+ return false;
76
+ }
77
+ }
78
+
79
+ // Collect (lang, skill, command) triples across both en and zh.
80
+ // A missing skills directory is a real contract break — fail explicitly.
81
+ const LANGS = ['en', 'zh'] as const;
82
+ const skillCommandPairs: { lang: string; skill: string; command: string }[] = [];
83
+ const missingLangDirs: string[] = [];
84
+
85
+ for (const lang of LANGS) {
86
+ const skillsDir = resolveSkillsDir(lang);
87
+ if (!existsSync(skillsDir)) {
88
+ missingLangDirs.push(lang);
89
+ continue;
90
+ }
91
+ const skillDirs = readdirSync(skillsDir).filter((dir) =>
92
+ existsSync(join(skillsDir, dir, 'SKILL.md')),
93
+ );
94
+ for (const dir of skillDirs) {
95
+ const skillPath = join(skillsDir, dir, 'SKILL.md');
96
+ const content = readFileSync(skillPath, 'utf8');
97
+ const commands = extractPdCommands(content);
98
+ for (const cmd of commands) {
99
+ skillCommandPairs.push({ lang, skill: dir, command: cmd });
100
+ }
101
+ }
102
+ }
103
+
104
+ describe('PRI-455: SKILL ↔ CLI contract', () => {
105
+ it('both en and zh skills directories exist', () => {
106
+ // Explicit failure instead of silent skip — a missing directory means
107
+ // the contract is no longer being validated for that language.
108
+ expect(missingLangDirs, `missing skills directories: ${missingLangDirs.join(', ')}`).toEqual([]);
109
+ });
110
+
111
+ it('found SKILL files that reference pd commands', () => {
112
+ expect(skillCommandPairs.length).toBeGreaterThan(0);
113
+ });
114
+
115
+ // Test each SKILL-referenced command exists in the CLI
116
+ for (const { lang, skill, command } of skillCommandPairs) {
117
+ it(`[${lang}] SKILL "${skill}" references valid command: pd ${command}`, () => {
118
+ expect(commandExists(command)).toBe(true);
119
+ });
120
+ }
121
+ });
@@ -46,8 +46,8 @@ describe('CLI command tree: pd runtime internalization', () => {
46
46
  expect(output).toContain('--json');
47
47
  });
48
48
 
49
- it('runtime subcommand list includes internalization', () => {
49
+ it('runtime subcommand list does NOT include internalization (PRI-455: hidden from --help)', () => {
50
50
  const output = runPdHelp(['runtime', '--help']);
51
- expect(output).toMatch(/internalization\s/);
51
+ expect(output).not.toMatch(/internalization\s/);
52
52
  });
53
53
  });
@@ -1,10 +0,0 @@
1
- /**
2
- * pd central sync command implementation.
3
- *
4
- * Usage: pd central sync
5
- *
6
- * Triggers a sync cycle via CentralDatabase.syncAll() and reports
7
- * per-workspace sync results with exit code 0 on success, non-zero on failure.
8
- */
9
- export declare function handleCentralSync(): Promise<void>;
10
- //# sourceMappingURL=central-sync.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"central-sync.d.ts","sourceRoot":"","sources":["../../src/commands/central-sync.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAeH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CAqBvD"}
@@ -1,32 +0,0 @@
1
- /**
2
- * pd central sync command implementation.
3
- *
4
- * Usage: pd central sync
5
- *
6
- * Triggers a sync cycle via CentralDatabase.syncAll() and reports
7
- * per-workspace sync results with exit code 0 on success, non-zero on failure.
8
- */
9
- async function loadCentralDatabase() {
10
- const importModule = Function('specifier', 'return import(specifier)');
11
- return importModule('../../../openclaw-plugin/src/service/central-database.js');
12
- }
13
- export async function handleCentralSync() {
14
- try {
15
- const { CentralDatabase } = await loadCentralDatabase();
16
- const centralDb = new CentralDatabase();
17
- const results = centralDb.syncAll();
18
- const totalRecords = Array.from(results.values()).reduce((sum, count) => sum + count, 0);
19
- const workspaceCount = results.size;
20
- console.log(`Sync complete — ${totalRecords} records across ${workspaceCount} workspace(s).`);
21
- for (const [workspaceName, count] of results.entries()) {
22
- console.log(` ${workspaceName}: ${count} records`);
23
- }
24
- centralDb.dispose();
25
- }
26
- catch (err) {
27
- const message = err instanceof Error ? err.message : String(err);
28
- console.error(`Error: Sync failed — ${message}`);
29
- process.exit(1);
30
- }
31
- }
32
- //# sourceMappingURL=central-sync.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"central-sync.js","sourceRoot":"","sources":["../../src/commands/central-sync.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,KAAK,UAAU,mBAAmB;IAIhC,MAAM,YAAY,GAAG,QAAQ,CAAC,WAAW,EAAE,0BAA0B,CAKnE,CAAC;IACH,OAAO,YAAY,CAAC,0DAA0D,CAAC,CAAC;AAClF,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,iBAAiB;IACrC,IAAI,CAAC;QACH,MAAM,EAAE,eAAe,EAAE,GAAG,MAAM,mBAAmB,EAAE,CAAC;QACxD,MAAM,SAAS,GAAG,IAAI,eAAe,EAAE,CAAC;QACxC,MAAM,OAAO,GAAG,SAAS,CAAC,OAAO,EAAE,CAAC;QAEpC,MAAM,YAAY,GAAG,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,CAAC,GAAG,GAAG,KAAK,EAAE,CAAC,CAAC,CAAC;QACzF,MAAM,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;QAEpC,OAAO,CAAC,GAAG,CAAC,mBAAmB,YAAY,mBAAmB,cAAc,gBAAgB,CAAC,CAAC;QAE9F,KAAK,MAAM,CAAC,aAAa,EAAE,KAAK,CAAC,IAAI,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC;YACvD,OAAO,CAAC,GAAG,CAAC,KAAK,aAAa,KAAK,KAAK,UAAU,CAAC,CAAC;QACtD,CAAC;QAED,SAAS,CAAC,OAAO,EAAE,CAAC;IACtB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACjE,OAAO,CAAC,KAAK,CAAC,wBAAwB,OAAO,EAAE,CAAC,CAAC;QACjD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;AACH,CAAC"}
@@ -1,44 +0,0 @@
1
- /**
2
- * pd central sync command implementation.
3
- *
4
- * Usage: pd central sync
5
- *
6
- * Triggers a sync cycle via CentralDatabase.syncAll() and reports
7
- * per-workspace sync results with exit code 0 on success, non-zero on failure.
8
- */
9
-
10
- async function loadCentralDatabase(): Promise<{ CentralDatabase: new () => {
11
- syncAll(): Map<string, number>;
12
- dispose(): void;
13
- } }> {
14
- const importModule = Function('specifier', 'return import(specifier)') as (specifier: string) => Promise<{
15
- CentralDatabase: new () => {
16
- syncAll(): Map<string, number>;
17
- dispose(): void;
18
- };
19
- }>;
20
- return importModule('../../../openclaw-plugin/src/service/central-database.js');
21
- }
22
-
23
- export async function handleCentralSync(): Promise<void> {
24
- try {
25
- const { CentralDatabase } = await loadCentralDatabase();
26
- const centralDb = new CentralDatabase();
27
- const results = centralDb.syncAll();
28
-
29
- const totalRecords = Array.from(results.values()).reduce((sum, count) => sum + count, 0);
30
- const workspaceCount = results.size;
31
-
32
- console.log(`Sync complete — ${totalRecords} records across ${workspaceCount} workspace(s).`);
33
-
34
- for (const [workspaceName, count] of results.entries()) {
35
- console.log(` ${workspaceName}: ${count} records`);
36
- }
37
-
38
- centralDb.dispose();
39
- } catch (err) {
40
- const message = err instanceof Error ? err.message : String(err);
41
- console.error(`Error: Sync failed — ${message}`);
42
- process.exit(1);
43
- }
44
- }