dantelabs-agentic-school 1.0.0 → 1.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.
Files changed (53) hide show
  1. package/.claude-plugin/marketplace.json +11 -53
  2. package/cli/bin/cli.js +31 -8
  3. package/cli/src/commands/info.js +15 -11
  4. package/cli/src/commands/install.js +53 -29
  5. package/cli/src/commands/list.js +28 -22
  6. package/cli/src/commands/uninstall.js +23 -16
  7. package/cli/src/i18n/index.js +96 -0
  8. package/cli/src/i18n/locales/en.js +107 -0
  9. package/cli/src/i18n/locales/ko.js +107 -0
  10. package/cli/src/lib/config.js +116 -1
  11. package/cli/src/lib/installer.js +106 -3
  12. package/package.json +1 -1
  13. package/plugins/brand-analytics/plugin.json +9 -0
  14. package/plugins/campaign-orchestration/plugin.json +9 -0
  15. package/plugins/common/plugin.json +9 -0
  16. package/plugins/common/skills/kie-image-generator/.env.example +4 -0
  17. package/plugins/common/skills/kie-image-generator/SKILL.md +281 -0
  18. package/plugins/common/skills/kie-image-generator/references/api_docs.md +358 -0
  19. package/plugins/common/skills/kie-image-generator/scripts/__pycache__/utils.cpython-313.pyc +0 -0
  20. package/plugins/common/skills/kie-image-generator/scripts/generate_image.py +285 -0
  21. package/plugins/common/skills/kie-image-generator/scripts/models/__init__.py +19 -0
  22. package/plugins/common/skills/kie-image-generator/scripts/models/__pycache__/__init__.cpython-313.pyc +0 -0
  23. package/plugins/common/skills/kie-image-generator/scripts/models/__pycache__/flux_kontext.cpython-313.pyc +0 -0
  24. package/plugins/common/skills/kie-image-generator/scripts/models/__pycache__/gpt4o.cpython-313.pyc +0 -0
  25. package/plugins/common/skills/kie-image-generator/scripts/models/__pycache__/ideogram.cpython-313.pyc +0 -0
  26. package/plugins/common/skills/kie-image-generator/scripts/models/__pycache__/imagen.cpython-313.pyc +0 -0
  27. package/plugins/common/skills/kie-image-generator/scripts/models/__pycache__/nano_banana.cpython-313.pyc +0 -0
  28. package/plugins/common/skills/kie-image-generator/scripts/models/__pycache__/nano_banana_edit.cpython-313.pyc +0 -0
  29. package/plugins/common/skills/kie-image-generator/scripts/models/__pycache__/nano_banana_pro.cpython-313.pyc +0 -0
  30. package/plugins/common/skills/kie-image-generator/scripts/models/__pycache__/seedream.cpython-313.pyc +0 -0
  31. package/plugins/common/skills/kie-image-generator/scripts/models/__pycache__/seedream_edit.cpython-313.pyc +0 -0
  32. package/plugins/common/skills/kie-image-generator/scripts/models/flux_kontext.py +36 -0
  33. package/plugins/common/skills/kie-image-generator/scripts/models/gpt4o.py +36 -0
  34. package/plugins/common/skills/kie-image-generator/scripts/models/ideogram.py +85 -0
  35. package/plugins/common/skills/kie-image-generator/scripts/models/imagen.py +48 -0
  36. package/plugins/common/skills/kie-image-generator/scripts/models/nano_banana.py +40 -0
  37. package/plugins/common/skills/kie-image-generator/scripts/models/nano_banana_edit.py +55 -0
  38. package/plugins/common/skills/kie-image-generator/scripts/models/nano_banana_pro.py +47 -0
  39. package/plugins/common/skills/kie-image-generator/scripts/models/seedream.py +51 -0
  40. package/plugins/common/skills/kie-image-generator/scripts/models/seedream_edit.py +66 -0
  41. package/plugins/common/skills/kie-image-generator/scripts/utils.py +706 -0
  42. package/plugins/common/skills/kie-video-generator/SKILL.md +258 -0
  43. package/plugins/common/skills/kie-video-generator/references/api_docs.md +202 -0
  44. package/plugins/common/skills/kie-video-generator/scripts/__pycache__/utils.cpython-313.pyc +0 -0
  45. package/plugins/common/skills/kie-video-generator/scripts/generate_video.py +356 -0
  46. package/plugins/common/skills/kie-video-generator/scripts/models/__init__.py +4 -0
  47. package/plugins/common/skills/kie-video-generator/scripts/utils.py +617 -0
  48. package/plugins/content-creation/plugin.json +9 -0
  49. package/plugins/creative-production/plugin.json +9 -0
  50. package/plugins/customer-segmentation/plugin.json +9 -0
  51. package/plugins/market-research/plugin.json +9 -0
  52. package/plugins/persona-builder/plugin.json +9 -0
  53. package/plugins/social-strategy/plugin.json +9 -0
@@ -8,14 +8,15 @@ import { getMarketplaceConfig } from '../lib/config.js';
8
8
  import { uninstallPlugin } from '../lib/installer.js';
9
9
  import logger from '../utils/logger.js';
10
10
  import { resolvePath } from '../utils/fs-utils.js';
11
+ import { t } from '../i18n/index.js';
11
12
 
12
13
  export default function uninstallCommand(program) {
13
14
  program
14
15
  .command('uninstall <plugin>')
15
16
  .alias('rm')
16
- .description('Uninstall a plugin from your project')
17
- .option('-p, --path <path>', 'Project path (default: current directory)')
18
- .option('-y, --yes', 'Skip confirmation prompt')
17
+ .description(t('uninstall.description'))
18
+ .option('-p, --path <path>', t('uninstall.optionPath'))
19
+ .option('-y, --yes', t('uninstall.optionYes'))
19
20
  .action(async (pluginName, options) => {
20
21
  const spinner = ora();
21
22
 
@@ -28,12 +29,12 @@ export default function uninstallCommand(program) {
28
29
 
29
30
  // Check if .claude directory exists
30
31
  if (!existsSync(claudeDir)) {
31
- logger.error(`No .claude directory found at ${targetPath}`);
32
+ logger.error(t('uninstall.noClaudeDir', { path: targetPath }));
32
33
  process.exit(1);
33
34
  }
34
35
 
35
36
  // Load marketplace config to get plugin info
36
- spinner.start('Loading plugin registry...');
37
+ spinner.start(t('uninstall.loadingRegistry'));
37
38
  const config = await getMarketplaceConfig();
38
39
  spinner.stop();
39
40
 
@@ -41,9 +42,9 @@ export default function uninstallCommand(program) {
41
42
  const plugin = config.plugins.find((p) => p.name === pluginName);
42
43
 
43
44
  if (!plugin) {
44
- logger.error(`Plugin '${pluginName}' not found in registry`);
45
+ logger.error(t('uninstall.pluginNotFound', { name: pluginName }));
45
46
  console.log();
46
- console.log('Available plugins:');
47
+ console.log(`${t('common.availablePlugins')}:`);
47
48
  config.plugins.forEach((p) => {
48
49
  console.log(` - ${chalk.cyan(p.name)}`);
49
50
  });
@@ -52,22 +53,24 @@ export default function uninstallCommand(program) {
52
53
 
53
54
  // Show what will be removed
54
55
  console.log();
55
- console.log(chalk.bold(`Will remove ${chalk.cyan(plugin.name)}:`));
56
+ console.log(
57
+ chalk.bold(t('uninstall.willRemove', { name: chalk.cyan(plugin.name) }))
58
+ );
56
59
  const components = plugin.components || {};
57
60
 
58
61
  if (components.agents?.length) {
59
62
  console.log(
60
- chalk.gray(` Agents: ${components.agents.join(', ')}`)
63
+ chalk.gray(` ${t('common.agents')}: ${components.agents.join(', ')}`)
61
64
  );
62
65
  }
63
66
  if (components.commands?.length) {
64
67
  console.log(
65
- chalk.gray(` Commands: /${components.commands.join(', /')}`)
68
+ chalk.gray(` ${t('common.commands')}: /${components.commands.join(', /')}`)
66
69
  );
67
70
  }
68
71
  if (components.skills?.length) {
69
72
  console.log(
70
- chalk.gray(` Skills: ${components.skills.join(', ')}`)
73
+ chalk.gray(` ${t('common.skills')}: ${components.skills.join(', ')}`)
71
74
  );
72
75
  }
73
76
  console.log();
@@ -78,29 +81,33 @@ export default function uninstallCommand(program) {
78
81
  {
79
82
  type: 'confirm',
80
83
  name: 'confirm',
81
- message: `Are you sure you want to uninstall ${plugin.name}?`,
84
+ message: t('uninstall.confirmUninstall', { name: plugin.name }),
82
85
  default: false
83
86
  }
84
87
  ]);
85
88
 
86
89
  if (!confirm) {
87
- logger.info('Uninstall cancelled');
90
+ logger.info(t('uninstall.uninstallCancelled'));
88
91
  return;
89
92
  }
90
93
  }
91
94
 
92
95
  // Uninstall
93
- spinner.start(`Uninstalling ${chalk.cyan(plugin.name)}...`);
96
+ spinner.start(t('uninstall.uninstalling', { name: chalk.cyan(plugin.name) }));
94
97
 
95
98
  const results = await uninstallPlugin(plugin, claudeDir);
96
99
 
97
- spinner.succeed(`Uninstalled ${chalk.cyan(plugin.name)}`);
100
+ spinner.succeed(t('uninstall.uninstalled', { name: chalk.cyan(plugin.name) }));
98
101
 
99
102
  // Summary
100
103
  console.log();
101
104
  console.log(
102
105
  chalk.gray(
103
- `Removed: ${results.agents} agents, ${results.commands} commands, ${results.skills} skills`
106
+ t('uninstall.removedSummary', {
107
+ agents: results.agents,
108
+ commands: results.commands,
109
+ skills: results.skills
110
+ })
104
111
  )
105
112
  );
106
113
  } catch (error) {
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Internationalization (i18n) module
3
+ * Default language: English (en)
4
+ * Supported languages: en, ko
5
+ */
6
+
7
+ import en from './locales/en.js';
8
+ import ko from './locales/ko.js';
9
+
10
+ const locales = { en, ko };
11
+
12
+ // Default language
13
+ let currentLocale = 'en';
14
+
15
+ /**
16
+ * Set the current locale
17
+ * @param {string} locale - Locale code (en, ko)
18
+ */
19
+ export function setLocale(locale) {
20
+ if (locales[locale]) {
21
+ currentLocale = locale;
22
+ } else {
23
+ console.warn(`Locale '${locale}' not supported. Using 'en' as default.`);
24
+ currentLocale = 'en';
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Get the current locale
30
+ * @returns {string} Current locale code
31
+ */
32
+ export function getLocale() {
33
+ return currentLocale;
34
+ }
35
+
36
+ /**
37
+ * Get available locales
38
+ * @returns {string[]} Array of locale codes
39
+ */
40
+ export function getAvailableLocales() {
41
+ return Object.keys(locales);
42
+ }
43
+
44
+ /**
45
+ * Get translation for a key path
46
+ * @param {string} keyPath - Dot-separated key path (e.g., 'install.description')
47
+ * @param {object} params - Parameters to interpolate
48
+ * @returns {string} Translated string
49
+ */
50
+ export function t(keyPath, params = {}) {
51
+ const keys = keyPath.split('.');
52
+ let value = locales[currentLocale];
53
+
54
+ for (const key of keys) {
55
+ if (value && typeof value === 'object' && key in value) {
56
+ value = value[key];
57
+ } else {
58
+ // Fallback to English
59
+ value = locales.en;
60
+ for (const k of keys) {
61
+ if (value && typeof value === 'object' && k in value) {
62
+ value = value[k];
63
+ } else {
64
+ return keyPath; // Return key if not found
65
+ }
66
+ }
67
+ break;
68
+ }
69
+ }
70
+
71
+ if (typeof value !== 'string') {
72
+ return keyPath;
73
+ }
74
+
75
+ // Interpolate parameters: {name} -> value
76
+ return value.replace(/\{(\w+)\}/g, (_, key) => {
77
+ return params[key] !== undefined ? params[key] : `{${key}}`;
78
+ });
79
+ }
80
+
81
+ /**
82
+ * Get all translations for a section
83
+ * @param {string} section - Section name (e.g., 'install', 'list')
84
+ * @returns {object} Section translations
85
+ */
86
+ export function getSection(section) {
87
+ return locales[currentLocale][section] || locales.en[section] || {};
88
+ }
89
+
90
+ export default {
91
+ setLocale,
92
+ getLocale,
93
+ getAvailableLocales,
94
+ t,
95
+ getSection
96
+ };
@@ -0,0 +1,107 @@
1
+ /**
2
+ * English translations (default)
3
+ */
4
+ export default {
5
+ // Common
6
+ common: {
7
+ plugins: 'plugins',
8
+ agents: 'agents',
9
+ commands: 'commands',
10
+ skills: 'skills',
11
+ version: 'version',
12
+ install: 'Install',
13
+ summary: 'Summary',
14
+ location: 'Location',
15
+ external: 'External',
16
+ availablePlugins: 'Available plugins',
17
+ yes: 'yes',
18
+ no: 'no'
19
+ },
20
+
21
+ // CLI descriptions
22
+ cli: {
23
+ description: 'Dante Labs Agentic School - Claude Code Plugin Installer',
24
+ examples: 'Examples',
25
+ moreInfo: 'More info',
26
+ installAllPlugins: '# Install all plugins',
27
+ installSpecificPlugin: '# Install specific plugin',
28
+ installCustomPath: '# Install to custom path',
29
+ listPlugins: '# List available plugins',
30
+ showPluginInfo: '# Show plugin info'
31
+ },
32
+
33
+ // Install command
34
+ install: {
35
+ description: 'Install plugins to your project',
36
+ optionPath: 'Installation path (default: current directory)',
37
+ optionForce: 'Force overwrite existing files',
38
+ optionAll: 'Install all available plugins',
39
+ optionNoCommon: 'Skip common utilities',
40
+ optionDryRun: 'Show what would be installed without making changes',
41
+ installTarget: 'Installation target',
42
+ loadingRegistry: 'Loading plugin registry...',
43
+ registryLoaded: 'Plugin registry loaded',
44
+ confirmInstallAll: 'Install all {count} plugins?',
45
+ installCancelled: 'Installation cancelled',
46
+ pluginNotFound: "Plugin '{name}' not found",
47
+ dryRunTitle: 'Dry run - would install:',
48
+ dryRunFooter: 'Run without --dry-run to install.',
49
+ installing: 'Installing {name}...',
50
+ installingComponent: 'Installing {plugin}: {type} {name}',
51
+ installed: 'Installed {name}',
52
+ failedToInstall: 'Failed to install {name}: {error}',
53
+ successMessage: 'Successfully installed {count} plugin(s)',
54
+ componentSummary: '{agents} agents, {commands} commands, {skills} skills',
55
+ nextSteps: 'Next steps',
56
+ nextStep1: 'Run {command} to see available commands',
57
+ nextStep2: 'Try {command}',
58
+ externalSkillsRequired: 'External skills required'
59
+ },
60
+
61
+ // List command
62
+ list: {
63
+ description: 'List all available plugins',
64
+ optionJson: 'Output as JSON',
65
+ optionVerbose: 'Show detailed information',
66
+ title: 'Dante Labs Agentic School - Available Plugins',
67
+ summaryText: '{plugins} plugins, {agents} agents, {commands} commands, {skills} skills',
68
+ installHint: 'Install: npx dantelabs-agentic-school install [plugin-name]'
69
+ },
70
+
71
+ // Info command
72
+ info: {
73
+ description: 'Show detailed information about a plugin',
74
+ optionJson: 'Output as JSON',
75
+ pluginNotFound: "Plugin '{name}' not found",
76
+ agents: 'Agents',
77
+ commands: 'Commands',
78
+ skills: 'Skills',
79
+ externalSkillsRequired: 'External Skills Required',
80
+ installHint: 'Install'
81
+ },
82
+
83
+ // Uninstall command
84
+ uninstall: {
85
+ description: 'Uninstall a plugin from your project',
86
+ optionPath: 'Project path (default: current directory)',
87
+ optionYes: 'Skip confirmation prompt',
88
+ noClaudeDir: 'No .claude directory found at {path}',
89
+ loadingRegistry: 'Loading plugin registry...',
90
+ pluginNotFound: "Plugin '{name}' not found in registry",
91
+ willRemove: 'Will remove {name}:',
92
+ confirmUninstall: 'Are you sure you want to uninstall {name}?',
93
+ uninstallCancelled: 'Uninstall cancelled',
94
+ uninstalling: 'Uninstalling {name}...',
95
+ uninstalled: 'Uninstalled {name}',
96
+ removedSummary: 'Removed: {agents} agents, {commands} commands, {skills} skills'
97
+ },
98
+
99
+ // Logger
100
+ logger: {
101
+ info: 'info',
102
+ success: 'success',
103
+ warn: 'warn',
104
+ error: 'error',
105
+ debug: 'debug'
106
+ }
107
+ };
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Korean translations
3
+ */
4
+ export default {
5
+ // Common
6
+ common: {
7
+ plugins: '플러그인',
8
+ agents: '에이전트',
9
+ commands: '명령어',
10
+ skills: '스킬',
11
+ version: '버전',
12
+ install: '설치',
13
+ summary: '요약',
14
+ location: '위치',
15
+ external: '외부',
16
+ availablePlugins: '사용 가능한 플러그인',
17
+ yes: '예',
18
+ no: '아니오'
19
+ },
20
+
21
+ // CLI descriptions
22
+ cli: {
23
+ description: 'Dante Labs Agentic School - Claude Code 플러그인 설치 도구',
24
+ examples: '사용 예시',
25
+ moreInfo: '자세한 정보',
26
+ installAllPlugins: '# 전체 플러그인 설치',
27
+ installSpecificPlugin: '# 특정 플러그인 설치',
28
+ installCustomPath: '# 경로 지정 설치',
29
+ listPlugins: '# 플러그인 목록 조회',
30
+ showPluginInfo: '# 플러그인 상세 정보'
31
+ },
32
+
33
+ // Install command
34
+ install: {
35
+ description: '프로젝트에 플러그인 설치',
36
+ optionPath: '설치 경로 (기본: 현재 디렉토리)',
37
+ optionForce: '기존 파일 강제 덮어쓰기',
38
+ optionAll: '모든 플러그인 설치',
39
+ optionNoCommon: 'common 유틸리티 제외',
40
+ optionDryRun: '실제 설치 없이 미리보기',
41
+ installTarget: '설치 대상 경로',
42
+ loadingRegistry: '플러그인 목록 불러오는 중...',
43
+ registryLoaded: '플러그인 목록 로드 완료',
44
+ confirmInstallAll: '전체 {count}개 플러그인을 설치할까요?',
45
+ installCancelled: '설치가 취소되었습니다',
46
+ pluginNotFound: "플러그인 '{name}'을(를) 찾을 수 없습니다",
47
+ dryRunTitle: '미리보기 - 설치 예정:',
48
+ dryRunFooter: '--dry-run 없이 실행하면 설치됩니다.',
49
+ installing: '{name} 설치 중...',
50
+ installingComponent: '{plugin} 설치 중: {type} {name}',
51
+ installed: '{name} 설치 완료',
52
+ failedToInstall: '{name} 설치 실패: {error}',
53
+ successMessage: '{count}개 플러그인 설치 완료',
54
+ componentSummary: '에이전트 {agents}개, 명령어 {commands}개, 스킬 {skills}개',
55
+ nextSteps: '다음 단계',
56
+ nextStep1: '{command} 실행하여 사용 가능한 명령어 확인',
57
+ nextStep2: '{command} 명령어 사용해보기',
58
+ externalSkillsRequired: '필요한 외부 스킬'
59
+ },
60
+
61
+ // List command
62
+ list: {
63
+ description: '사용 가능한 플러그인 목록 조회',
64
+ optionJson: 'JSON 형식으로 출력',
65
+ optionVerbose: '상세 정보 표시',
66
+ title: 'Dante Labs Agentic School - 플러그인 목록',
67
+ summaryText: '플러그인 {plugins}개, 에이전트 {agents}개, 명령어 {commands}개, 스킬 {skills}개',
68
+ installHint: '설치: npx dantelabs-agentic-school install [플러그인명]'
69
+ },
70
+
71
+ // Info command
72
+ info: {
73
+ description: '플러그인 상세 정보 조회',
74
+ optionJson: 'JSON 형식으로 출력',
75
+ pluginNotFound: "플러그인 '{name}'을(를) 찾을 수 없습니다",
76
+ agents: '에이전트',
77
+ commands: '명령어',
78
+ skills: '스킬',
79
+ externalSkillsRequired: '필요한 외부 스킬',
80
+ installHint: '설치 명령어'
81
+ },
82
+
83
+ // Uninstall command
84
+ uninstall: {
85
+ description: '프로젝트에서 플러그인 제거',
86
+ optionPath: '프로젝트 경로 (기본: 현재 디렉토리)',
87
+ optionYes: '확인 프롬프트 건너뛰기',
88
+ noClaudeDir: '{path}에 .claude 디렉토리가 없습니다',
89
+ loadingRegistry: '플러그인 목록 불러오는 중...',
90
+ pluginNotFound: "플러그인 '{name}'이(가) 레지스트리에 없습니다",
91
+ willRemove: '{name} 제거 예정:',
92
+ confirmUninstall: '{name}을(를) 정말 삭제하시겠습니까?',
93
+ uninstallCancelled: '삭제가 취소되었습니다',
94
+ uninstalling: '{name} 삭제 중...',
95
+ uninstalled: '{name} 삭제 완료',
96
+ removedSummary: '삭제됨: 에이전트 {agents}개, 명령어 {commands}개, 스킬 {skills}개'
97
+ },
98
+
99
+ // Logger
100
+ logger: {
101
+ info: '정보',
102
+ success: '성공',
103
+ warn: '경고',
104
+ error: '오류',
105
+ debug: '디버그'
106
+ }
107
+ };
@@ -1,4 +1,4 @@
1
- import { readFile, writeFile } from 'fs/promises';
1
+ import { readFile, writeFile, readdir } from 'fs/promises';
2
2
  import { join } from 'path';
3
3
  import { existsSync } from 'fs';
4
4
  import { fileURLToPath } from 'url';
@@ -7,6 +7,9 @@ import { dirname } from 'path';
7
7
  const __filename = fileURLToPath(import.meta.url);
8
8
  const __dirname = dirname(__filename);
9
9
 
10
+ // Package root directory
11
+ const PACKAGE_ROOT = join(__dirname, '../../..');
12
+
10
13
  const CONFIG_CACHE_FILE = '.dantelabs-cache.json';
11
14
  const CACHE_TTL = 1000 * 60 * 60; // 1 hour
12
15
 
@@ -124,6 +127,118 @@ async function saveCachedConfig(config) {
124
127
  }
125
128
  }
126
129
 
130
+ /**
131
+ * Normalize source path (handle both 'path' and 'source' fields)
132
+ */
133
+ export function getSourcePath(plugin) {
134
+ const source = plugin.source || plugin.path;
135
+ if (!source) return `plugins/${plugin.name}`;
136
+ return source.replace(/^\.\//, '');
137
+ }
138
+
139
+ /**
140
+ * Discover components by scanning local plugin directory
141
+ */
142
+ async function discoverLocalComponents(pluginPath) {
143
+ const components = { agents: [], commands: [], skills: [] };
144
+
145
+ // Scan agents directory
146
+ const agentsDir = join(pluginPath, 'agents');
147
+ if (existsSync(agentsDir)) {
148
+ const files = await readdir(agentsDir);
149
+ components.agents = files
150
+ .filter(f => f.endsWith('.md'))
151
+ .map(f => f.replace('.md', ''));
152
+ }
153
+
154
+ // Scan commands directory
155
+ const commandsDir = join(pluginPath, 'commands');
156
+ if (existsSync(commandsDir)) {
157
+ const files = await readdir(commandsDir);
158
+ components.commands = files
159
+ .filter(f => f.endsWith('.md'))
160
+ .map(f => f.replace('.md', ''));
161
+ }
162
+
163
+ // Scan skills directory
164
+ const skillsDir = join(pluginPath, 'skills');
165
+ if (existsSync(skillsDir)) {
166
+ const entries = await readdir(skillsDir, { withFileTypes: true });
167
+ components.skills = entries
168
+ .filter(e => e.isDirectory())
169
+ .map(e => e.name);
170
+ }
171
+
172
+ return components;
173
+ }
174
+
175
+ /**
176
+ * Discover components from remote GitHub directory
177
+ */
178
+ async function discoverRemoteComponents(remotePath) {
179
+ const components = { agents: [], commands: [], skills: [] };
180
+
181
+ const fetchDir = async (path) => {
182
+ const url = `${GITHUB_CONFIG.apiBase}/contents/${path}?ref=${GITHUB_CONFIG.branch}`;
183
+ const response = await fetch(url, {
184
+ headers: {
185
+ 'Accept': 'application/vnd.github.v3+json',
186
+ 'User-Agent': 'dantelabs-agentic-school-cli'
187
+ }
188
+ });
189
+ if (!response.ok) return [];
190
+ return response.json();
191
+ };
192
+
193
+ // Scan agents directory
194
+ try {
195
+ const agentsContents = await fetchDir(`${remotePath}/agents`);
196
+ components.agents = agentsContents
197
+ .filter(f => f.type === 'file' && f.name.endsWith('.md'))
198
+ .map(f => f.name.replace('.md', ''));
199
+ } catch (e) { /* no agents dir */ }
200
+
201
+ // Scan commands directory
202
+ try {
203
+ const commandsContents = await fetchDir(`${remotePath}/commands`);
204
+ components.commands = commandsContents
205
+ .filter(f => f.type === 'file' && f.name.endsWith('.md'))
206
+ .map(f => f.name.replace('.md', ''));
207
+ } catch (e) { /* no commands dir */ }
208
+
209
+ // Scan skills directory
210
+ try {
211
+ const skillsContents = await fetchDir(`${remotePath}/skills`);
212
+ components.skills = skillsContents
213
+ .filter(f => f.type === 'dir')
214
+ .map(f => f.name);
215
+ } catch (e) { /* no skills dir */ }
216
+
217
+ return components;
218
+ }
219
+
220
+ /**
221
+ * Enrich plugin with discovered components
222
+ */
223
+ export async function enrichPluginWithComponents(plugin) {
224
+ // If components already exist, return as is
225
+ if (plugin.components && Object.keys(plugin.components).length > 0) {
226
+ return plugin;
227
+ }
228
+
229
+ const sourcePath = getSourcePath(plugin);
230
+ const localPath = join(PACKAGE_ROOT, sourcePath);
231
+
232
+ let components;
233
+ if (existsSync(localPath)) {
234
+ components = await discoverLocalComponents(localPath);
235
+ } else {
236
+ components = await discoverRemoteComponents(sourcePath);
237
+ }
238
+
239
+ return { ...plugin, components };
240
+ }
241
+
127
242
  /**
128
243
  * Validate plugin name
129
244
  */
@@ -46,6 +46,87 @@ async function copyDirectory(src, dest) {
46
46
  }
47
47
  }
48
48
 
49
+ /**
50
+ * Discover components by scanning plugin directory
51
+ */
52
+ async function discoverComponents(localPath) {
53
+ const components = { agents: [], commands: [], skills: [] };
54
+
55
+ // Scan agents directory
56
+ const agentsDir = join(localPath, 'agents');
57
+ if (existsSync(agentsDir)) {
58
+ const files = await readdir(agentsDir);
59
+ components.agents = files
60
+ .filter(f => f.endsWith('.md'))
61
+ .map(f => f.replace('.md', ''));
62
+ }
63
+
64
+ // Scan commands directory
65
+ const commandsDir = join(localPath, 'commands');
66
+ if (existsSync(commandsDir)) {
67
+ const files = await readdir(commandsDir);
68
+ components.commands = files
69
+ .filter(f => f.endsWith('.md'))
70
+ .map(f => f.replace('.md', ''));
71
+ }
72
+
73
+ // Scan skills directory
74
+ const skillsDir = join(localPath, 'skills');
75
+ if (existsSync(skillsDir)) {
76
+ const entries = await readdir(skillsDir, { withFileTypes: true });
77
+ components.skills = entries
78
+ .filter(e => e.isDirectory())
79
+ .map(e => e.name);
80
+ }
81
+
82
+ return components;
83
+ }
84
+
85
+ /**
86
+ * Discover components from remote GitHub directory
87
+ */
88
+ async function discoverRemoteComponents(remotePath) {
89
+ const { getDirectoryContents } = await import('./downloader.js');
90
+ const components = { agents: [], commands: [], skills: [] };
91
+
92
+ // Scan agents directory
93
+ try {
94
+ const agentsContents = await getDirectoryContents(`${remotePath}/agents`);
95
+ components.agents = agentsContents
96
+ .filter(f => f.type === 'file' && f.name.endsWith('.md'))
97
+ .map(f => f.name.replace('.md', ''));
98
+ } catch (e) { /* no agents dir */ }
99
+
100
+ // Scan commands directory
101
+ try {
102
+ const commandsContents = await getDirectoryContents(`${remotePath}/commands`);
103
+ components.commands = commandsContents
104
+ .filter(f => f.type === 'file' && f.name.endsWith('.md'))
105
+ .map(f => f.name.replace('.md', ''));
106
+ } catch (e) { /* no commands dir */ }
107
+
108
+ // Scan skills directory
109
+ try {
110
+ const skillsContents = await getDirectoryContents(`${remotePath}/skills`);
111
+ components.skills = skillsContents
112
+ .filter(f => f.type === 'dir')
113
+ .map(f => f.name);
114
+ } catch (e) { /* no skills dir */ }
115
+
116
+ return components;
117
+ }
118
+
119
+ /**
120
+ * Normalize source path (handle both 'path' and 'source' fields)
121
+ */
122
+ function getSourcePath(plugin) {
123
+ // Support both old 'path' format and new 'source' format
124
+ const source = plugin.source || plugin.path;
125
+ if (!source) return `plugins/${plugin.name}`;
126
+ // Remove leading './' if present
127
+ return source.replace(/^\.\//, '');
128
+ }
129
+
49
130
  /**
50
131
  * Install a single plugin to .claude directory
51
132
  *
@@ -64,13 +145,22 @@ async function copyDirectory(src, dest) {
64
145
  export async function installPlugin(plugin, claudeDir, options = {}) {
65
146
  const { force = false, onProgress } = options;
66
147
  const pluginName = plugin.name;
67
- const components = plugin.components || {};
68
- const sourcePath = plugin.path; // e.g., "plugins/brand-analytics"
148
+ const sourcePath = getSourcePath(plugin); // e.g., "plugins/brand-analytics"
69
149
 
70
150
  // Determine if we should use local or remote source
71
151
  const useLocalSource = hasLocalSource(sourcePath);
72
152
  const localSourcePath = join(PACKAGE_ROOT, sourcePath);
73
153
 
154
+ // Discover components dynamically (or use provided components for backward compatibility)
155
+ let components = plugin.components;
156
+ if (!components || Object.keys(components).length === 0) {
157
+ if (useLocalSource) {
158
+ components = await discoverComponents(localSourcePath);
159
+ } else {
160
+ components = await discoverRemoteComponents(sourcePath);
161
+ }
162
+ }
163
+
74
164
  // Create base directories
75
165
  await ensureDir(join(claudeDir, 'agents'));
76
166
  await ensureDir(join(claudeDir, 'commands'));
@@ -194,7 +284,20 @@ export async function installPlugin(plugin, claudeDir, options = {}) {
194
284
  */
195
285
  export async function uninstallPlugin(plugin, claudeDir) {
196
286
  const pluginName = plugin.name;
197
- const components = plugin.components || {};
287
+ const sourcePath = getSourcePath(plugin);
288
+
289
+ // Discover components dynamically (or use provided components for backward compatibility)
290
+ let components = plugin.components;
291
+ if (!components || Object.keys(components).length === 0) {
292
+ const useLocalSource = hasLocalSource(sourcePath);
293
+ const localSourcePath = join(PACKAGE_ROOT, sourcePath);
294
+
295
+ if (useLocalSource) {
296
+ components = await discoverComponents(localSourcePath);
297
+ } else {
298
+ components = await discoverRemoteComponents(sourcePath);
299
+ }
300
+ }
198
301
 
199
302
  const results = {
200
303
  plugin: pluginName,