@pennyfarthing/shared 7.0.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.
Files changed (46) hide show
  1. package/dist/generate-skill-docs.d.ts +35 -0
  2. package/dist/generate-skill-docs.d.ts.map +1 -0
  3. package/dist/generate-skill-docs.js +442 -0
  4. package/dist/generate-skill-docs.js.map +1 -0
  5. package/dist/generate-skill-docs.test.d.ts +13 -0
  6. package/dist/generate-skill-docs.test.d.ts.map +1 -0
  7. package/dist/generate-skill-docs.test.js +519 -0
  8. package/dist/generate-skill-docs.test.js.map +1 -0
  9. package/dist/index.d.ts +10 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +10 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/portrait-resolver.d.ts +32 -0
  14. package/dist/portrait-resolver.d.ts.map +1 -0
  15. package/dist/portrait-resolver.js +147 -0
  16. package/dist/portrait-resolver.js.map +1 -0
  17. package/dist/portrait-resolver.test.d.ts +2 -0
  18. package/dist/portrait-resolver.test.d.ts.map +1 -0
  19. package/dist/portrait-resolver.test.js +156 -0
  20. package/dist/portrait-resolver.test.js.map +1 -0
  21. package/dist/skill-search.d.ts +36 -0
  22. package/dist/skill-search.d.ts.map +1 -0
  23. package/dist/skill-search.js +300 -0
  24. package/dist/skill-search.js.map +1 -0
  25. package/dist/skill-search.sh +41 -0
  26. package/dist/skill-search.test.d.ts +16 -0
  27. package/dist/skill-search.test.d.ts.map +1 -0
  28. package/dist/skill-search.test.js +193 -0
  29. package/dist/skill-search.test.js.map +1 -0
  30. package/dist/skill-suggest.d.ts +76 -0
  31. package/dist/skill-suggest.d.ts.map +1 -0
  32. package/dist/skill-suggest.js +256 -0
  33. package/dist/skill-suggest.js.map +1 -0
  34. package/dist/skill-suggest.test.d.ts +12 -0
  35. package/dist/skill-suggest.test.d.ts.map +1 -0
  36. package/dist/skill-suggest.test.js +257 -0
  37. package/dist/skill-suggest.test.js.map +1 -0
  38. package/dist/theme-loader.d.ts +35 -0
  39. package/dist/theme-loader.d.ts.map +1 -0
  40. package/dist/theme-loader.js +170 -0
  41. package/dist/theme-loader.js.map +1 -0
  42. package/dist/theme-loader.test.d.ts +2 -0
  43. package/dist/theme-loader.test.d.ts.map +1 -0
  44. package/dist/theme-loader.test.js +72 -0
  45. package/dist/theme-loader.test.js.map +1 -0
  46. package/package.json +38 -0
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Portrait Resolver - Smart path resolution for Pennyfarthing portraits
3
+ *
4
+ * Checks paths in priority order:
5
+ * 1. PENNYFARTHING_DIST env var (explicit override)
6
+ * 2. Monorepo root (pennyfarthing-dist/ at repo root for dogfooding)
7
+ * 3. Sibling directory (for dev scenarios)
8
+ * 4. Scoped npm (node_modules/@pennyfarthing/core/pennyfarthing-dist/)
9
+ * 5. Legacy npm (node_modules/pennyfarthing/pennyfarthing-dist/)
10
+ */
11
+ import { existsSync, readdirSync } from 'node:fs';
12
+ import { join, dirname, resolve } from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
14
+ // Get the directory where this module is located
15
+ const __filename = fileURLToPath(import.meta.url);
16
+ const __dirname = dirname(__filename);
17
+ /**
18
+ * Resolve the pennyfarthing-dist directory path
19
+ * Checks multiple locations in priority order
20
+ */
21
+ export function resolvePennyfarthingDist() {
22
+ // 1. PENNYFARTHING_DIST env var (explicit override)
23
+ const envPath = process.env.PENNYFARTHING_DIST;
24
+ if (envPath) {
25
+ if (existsSync(envPath)) {
26
+ return envPath;
27
+ }
28
+ // Env var set but path doesn't exist - fall through to other checks
29
+ }
30
+ // 2. Monorepo root (pennyfarthing-dist/ at repo root for dogfooding)
31
+ // Walk up from current directory looking for pennyfarthing-dist/
32
+ let currentDir = __dirname;
33
+ for (let i = 0; i < 10; i++) {
34
+ const monorepoPath = join(currentDir, 'pennyfarthing-dist');
35
+ if (existsSync(monorepoPath)) {
36
+ return monorepoPath;
37
+ }
38
+ const parentDir = dirname(currentDir);
39
+ if (parentDir === currentDir)
40
+ break; // Reached root
41
+ currentDir = parentDir;
42
+ }
43
+ // 3. Sibling directory (for dev scenarios)
44
+ // Check ../pennyfarthing-dist, ../../pennyfarthing-dist from module location
45
+ currentDir = __dirname;
46
+ for (let i = 0; i < 5; i++) {
47
+ currentDir = dirname(currentDir);
48
+ const siblingPath = join(currentDir, 'pennyfarthing-dist');
49
+ if (existsSync(siblingPath)) {
50
+ return siblingPath;
51
+ }
52
+ }
53
+ // 4. Scoped npm (node_modules/@pennyfarthing/core/pennyfarthing-dist/)
54
+ const scopedNpmPath = resolve(__dirname, '..', '..', '..', '@pennyfarthing', 'core', 'pennyfarthing-dist');
55
+ if (existsSync(scopedNpmPath)) {
56
+ return scopedNpmPath;
57
+ }
58
+ // 5. Legacy npm (node_modules/pennyfarthing/pennyfarthing-dist/)
59
+ const legacyNpmPath = resolve(__dirname, '..', '..', '..', 'pennyfarthing', 'pennyfarthing-dist');
60
+ if (existsSync(legacyNpmPath)) {
61
+ return legacyNpmPath;
62
+ }
63
+ // No valid path found
64
+ return null;
65
+ }
66
+ /**
67
+ * Resolve the full path to a portrait image
68
+ * @param theme - Theme name (e.g., 'shakespeare', 'norse-mythology')
69
+ * @param agent - Agent name (e.g., 'sm', 'tea', 'dev')
70
+ * @returns Full path to portrait file, or null if not found
71
+ */
72
+ export function resolvePortraitPath(theme, agent) {
73
+ const distPath = resolvePennyfarthingDist();
74
+ if (!distPath) {
75
+ return null;
76
+ }
77
+ const paths = getPortraitPaths(distPath);
78
+ const portraitsThemeDir = join(paths.portraitsDir, theme);
79
+ if (!existsSync(portraitsThemeDir)) {
80
+ return null;
81
+ }
82
+ // Look for portrait file matching the agent
83
+ // Portraits are in size subdirectories: large/, medium/, small/, original/
84
+ // Portraits follow pattern: {shortName}-{ocean}.png
85
+ // Agent names in tests may be short names (sm, tea, dev) or need mapping
86
+ try {
87
+ // Check size subdirectories in preference order
88
+ const sizeDirectories = ['large', 'medium', 'small', 'original'];
89
+ let files = [];
90
+ let searchDir = portraitsThemeDir;
91
+ for (const sizeDir of sizeDirectories) {
92
+ const sizedPath = join(portraitsThemeDir, sizeDir);
93
+ if (existsSync(sizedPath)) {
94
+ files = readdirSync(sizedPath);
95
+ searchDir = sizedPath;
96
+ break;
97
+ }
98
+ }
99
+ // Fallback to root directory if no size subdirectories
100
+ if (files.length === 0) {
101
+ files = readdirSync(portraitsThemeDir);
102
+ searchDir = portraitsThemeDir;
103
+ }
104
+ // Map agent names to portrait file prefixes based on theme conventions
105
+ // For most themes, portrait names use character short names
106
+ // We need to find a file that contains the agent name or its mapping
107
+ // Note: This includes characters from multiple themes (shakespeare, norse, a-team)
108
+ const agentMappings = {
109
+ 'sm': ['prospero', 'baldur', 'face', 'faceman', 'sm'],
110
+ 'tea': ['hamlet', 'tyr', 'murdock', 'tea'],
111
+ 'dev': ['puck', 'loki', 'ba', 'dev'],
112
+ 'reviewer': ['portia', 'heimdall', 'lynch', 'decker', 'reviewer'],
113
+ 'architect': ['oberon', 'mimir', 'hannibal', 'architect'],
114
+ 'pm': ['henry', 'thor', 'amy', 'pm'],
115
+ 'tech-writer': ['horatio', 'bragi', 'tech-writer'],
116
+ 'ux-designer': ['viola', 'idunn', 'ux-designer'],
117
+ 'devops': ['caliban', 'norns', 'devops'],
118
+ 'orchestrator': ['chorus', 'odin', 'orchestrator'],
119
+ };
120
+ const possiblePrefixes = agentMappings[agent] || [agent];
121
+ for (const file of files) {
122
+ for (const prefix of possiblePrefixes) {
123
+ if (file.toLowerCase().startsWith(prefix.toLowerCase()) &&
124
+ (file.endsWith('.png') || file.endsWith('.jpg'))) {
125
+ return join(searchDir, file);
126
+ }
127
+ }
128
+ }
129
+ }
130
+ catch {
131
+ return null;
132
+ }
133
+ return null;
134
+ }
135
+ /**
136
+ * Get all portrait-related paths for a resolved dist directory
137
+ */
138
+ export function getPortraitPaths(distPath) {
139
+ // Normalize path to remove trailing slashes
140
+ const normalizedPath = distPath.replace(/\/+$/, '');
141
+ return {
142
+ portraitsDir: join(normalizedPath, 'personas', 'portraits'),
143
+ themesDir: join(normalizedPath, 'personas'),
144
+ agentsDir: join(normalizedPath, 'agents'),
145
+ };
146
+ }
147
+ //# sourceMappingURL=portrait-resolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"portrait-resolver.js","sourceRoot":"","sources":["../src/portrait-resolver.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=portrait-resolver.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"portrait-resolver.test.d.ts","sourceRoot":"","sources":["../src/portrait-resolver.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,156 @@
1
+ import { describe, it, afterEach } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import path from 'node:path';
4
+ import { resolvePennyfarthingDist, resolvePortraitPath, getPortraitPaths, } from './portrait-resolver.js';
5
+ describe('portrait-resolver', () => {
6
+ describe('resolvePennyfarthingDist', () => {
7
+ const originalEnv = process.env.PENNYFARTHING_DIST;
8
+ afterEach(() => {
9
+ // Restore original env
10
+ if (originalEnv !== undefined) {
11
+ process.env.PENNYFARTHING_DIST = originalEnv;
12
+ }
13
+ else {
14
+ delete process.env.PENNYFARTHING_DIST;
15
+ }
16
+ });
17
+ it('should return PENNYFARTHING_DIST env var when set and path exists', () => {
18
+ // Scenario 1: Explicit env var override with existing path
19
+ // Use the actual pennyfarthing-dist path that exists
20
+ const actualDistPath = resolvePennyfarthingDist();
21
+ assert.ok(actualDistPath !== null, 'Should have a valid dist path');
22
+ process.env.PENNYFARTHING_DIST = actualDistPath;
23
+ const result = resolvePennyfarthingDist();
24
+ // When env var is set to valid existing path, should return it
25
+ assert.strictEqual(result, actualDistPath);
26
+ });
27
+ it('should fall through when PENNYFARTHING_DIST is set but path does not exist', () => {
28
+ // Scenario 1b: Env var set but invalid - should fall through to other checks
29
+ process.env.PENNYFARTHING_DIST = '/nonexistent/path/pennyfarthing-dist';
30
+ const result = resolvePennyfarthingDist();
31
+ // Should fall through and find monorepo path (since we're in monorepo)
32
+ // The key behavior is it doesn't return the invalid env var path
33
+ assert.ok(result === null || !result.includes('/nonexistent/'));
34
+ });
35
+ it('should find monorepo root pennyfarthing-dist directory', () => {
36
+ // Scenario 2: Monorepo root detection (dogfooding)
37
+ // When running from within pennyfarthing repo, find pennyfarthing-dist/ at root
38
+ delete process.env.PENNYFARTHING_DIST;
39
+ const result = resolvePennyfarthingDist();
40
+ // Should find the monorepo root path
41
+ assert.ok(result !== null, 'Should find monorepo root');
42
+ assert.ok(result.endsWith('pennyfarthing-dist'), 'Path should end with pennyfarthing-dist');
43
+ });
44
+ it('should find sibling pennyfarthing-dist directory', () => {
45
+ // Scenario 3: Sibling directory for dev scenarios
46
+ // e.g., project/pennyfarthing-dist when called from project/packages/shared
47
+ delete process.env.PENNYFARTHING_DIST;
48
+ const result = resolvePennyfarthingDist();
49
+ // Implementation should check ../pennyfarthing-dist, ../../pennyfarthing-dist, etc.
50
+ assert.ok(result === null || result.includes('pennyfarthing-dist'));
51
+ });
52
+ it('should find scoped npm package path', () => {
53
+ // Scenario 4: Scoped npm install
54
+ // node_modules/@pennyfarthing/core/pennyfarthing-dist/
55
+ delete process.env.PENNYFARTHING_DIST;
56
+ const result = resolvePennyfarthingDist();
57
+ // When installed via @pennyfarthing/core, should find that path
58
+ assert.ok(result === null || result.includes('pennyfarthing-dist'));
59
+ });
60
+ it('should find legacy npm package path', () => {
61
+ // Scenario 5: Legacy npm install
62
+ // node_modules/pennyfarthing/pennyfarthing-dist/
63
+ delete process.env.PENNYFARTHING_DIST;
64
+ const result = resolvePennyfarthingDist();
65
+ // When installed via pennyfarthing, should find that path
66
+ assert.ok(result === null || result.includes('pennyfarthing-dist'));
67
+ });
68
+ it('should return a valid path or null', () => {
69
+ // This tests that the function returns a valid result
70
+ delete process.env.PENNYFARTHING_DIST;
71
+ const result = resolvePennyfarthingDist();
72
+ // Result should be either null or a valid pennyfarthing-dist path
73
+ if (result !== null) {
74
+ assert.ok(result.includes('pennyfarthing-dist'));
75
+ }
76
+ });
77
+ it('should check paths in priority order - env var takes precedence', () => {
78
+ // When multiple paths exist, env var should return first if it exists
79
+ const actualDistPath = resolvePennyfarthingDist();
80
+ assert.ok(actualDistPath !== null, 'Should have a valid dist path');
81
+ // Set env var to actual path
82
+ process.env.PENNYFARTHING_DIST = actualDistPath;
83
+ const result = resolvePennyfarthingDist();
84
+ // Env var should take precedence (returns same path since it exists)
85
+ assert.strictEqual(result, actualDistPath);
86
+ });
87
+ });
88
+ describe('resolvePortraitPath', () => {
89
+ it('should resolve portrait path for valid theme and agent', () => {
90
+ // Use a-team which has actual portraits (shakespeare has only .gitkeep)
91
+ const result = resolvePortraitPath('a-team', 'sm');
92
+ assert.ok(result !== null, 'Should find portrait');
93
+ assert.ok(result.includes('a-team'), 'Path should include theme');
94
+ // Portrait files use character names (faceman) not agent names (sm)
95
+ assert.ok(result.includes('face'), 'Path should include character name');
96
+ assert.ok(result.endsWith('.png') || result.endsWith('.jpg'), 'Should be image file');
97
+ });
98
+ it('should return null for invalid theme', () => {
99
+ const result = resolvePortraitPath('nonexistent-theme', 'sm');
100
+ assert.strictEqual(result, null);
101
+ });
102
+ it('should return null for invalid agent', () => {
103
+ const result = resolvePortraitPath('a-team', 'nonexistent-agent');
104
+ assert.strictEqual(result, null);
105
+ });
106
+ it('should handle theme with special characters in name', () => {
107
+ // Themes like 'star-trek-tos' have hyphens
108
+ const result = resolvePortraitPath('star-trek-tos', 'sm');
109
+ assert.ok(result === null || result.includes('star-trek-tos'));
110
+ });
111
+ it('should handle portrait resolution when dist is found', () => {
112
+ // When resolvePennyfarthingDist returns a valid path, we should be able
113
+ // to resolve portraits for known themes
114
+ delete process.env.PENNYFARTHING_DIST;
115
+ const result = resolvePortraitPath('a-team', 'dev');
116
+ // If dist is found (which it is in monorepo), should find portrait
117
+ // dev maps to ba in a-team theme
118
+ if (result !== null) {
119
+ assert.ok(result.includes('a-team'));
120
+ assert.ok(result.includes('ba'));
121
+ }
122
+ });
123
+ });
124
+ describe('getPortraitPaths', () => {
125
+ it('should return correct paths structure', () => {
126
+ const distPath = '/test/pennyfarthing-dist';
127
+ const result = getPortraitPaths(distPath);
128
+ assert.ok('portraitsDir' in result, 'Should have portraitsDir');
129
+ assert.ok('themesDir' in result, 'Should have themesDir');
130
+ assert.ok('agentsDir' in result, 'Should have agentsDir');
131
+ });
132
+ it('should build portraitsDir correctly', () => {
133
+ const distPath = '/test/pennyfarthing-dist';
134
+ const result = getPortraitPaths(distPath);
135
+ // Actual structure: pennyfarthing-dist/personas/portraits/
136
+ assert.strictEqual(result.portraitsDir, path.join(distPath, 'personas', 'portraits'), 'portraitsDir should be distPath/personas/portraits');
137
+ });
138
+ it('should build themesDir correctly', () => {
139
+ const distPath = '/test/pennyfarthing-dist';
140
+ const result = getPortraitPaths(distPath);
141
+ assert.strictEqual(result.themesDir, path.join(distPath, 'personas'), 'themesDir should be distPath/personas');
142
+ });
143
+ it('should build agentsDir correctly', () => {
144
+ const distPath = '/test/pennyfarthing-dist';
145
+ const result = getPortraitPaths(distPath);
146
+ assert.strictEqual(result.agentsDir, path.join(distPath, 'agents'), 'agentsDir should be distPath/agents');
147
+ });
148
+ it('should handle paths with trailing slashes', () => {
149
+ const distPath = '/test/pennyfarthing-dist/';
150
+ const result = getPortraitPaths(distPath);
151
+ // Should normalize paths correctly
152
+ assert.ok(!result.portraitsDir.includes('//'), 'Should not have double slashes');
153
+ });
154
+ });
155
+ });
156
+ //# sourceMappingURL=portrait-resolver.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"portrait-resolver.test.js","sourceRoot":"","sources":["../src/portrait-resolver.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Skill Search Utility - Story 9-2
3
+ *
4
+ * Searches the skill registry by tag, keyword, category, or description query.
5
+ * Returns matching skills with metadata for discovery and suggestions.
6
+ */
7
+ export interface SearchOptions {
8
+ /** Filter by tag (e.g., "tdd", "quality") */
9
+ tag?: string;
10
+ /** Filter by keyword (e.g., "jest", "vitest") */
11
+ keyword?: string;
12
+ /** Search description text */
13
+ query?: string;
14
+ /** Filter by category (e.g., "development", "tools") */
15
+ category?: string;
16
+ /** Custom path to registry file (for testing) */
17
+ registryPath?: string;
18
+ }
19
+ export interface SkillResult {
20
+ name: string;
21
+ description: string;
22
+ category: string;
23
+ tags: string[];
24
+ keywords?: string[];
25
+ version?: string;
26
+ related_skills?: string[];
27
+ }
28
+ /**
29
+ * Search skills in the registry based on provided options.
30
+ *
31
+ * @param options - Search options (tag, keyword, query, category)
32
+ * @returns Promise resolving to array of matching skills
33
+ * @throws Error if registry file not found or invalid category
34
+ */
35
+ export declare function searchSkills(options: SearchOptions): Promise<SkillResult[]>;
36
+ //# sourceMappingURL=skill-search.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skill-search.d.ts","sourceRoot":"","sources":["../src/skill-search.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,MAAM,WAAW,aAAa;IAC5B,6CAA6C;IAC7C,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,iDAAiD;IACjD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,8BAA8B;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,wDAAwD;IACxD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iDAAiD;IACjD,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;CAC3B;AAmLD;;;;;;GAMG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CAoEjF"}