@orderful/droid 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/.changeset/README.md +30 -0
- package/.changeset/config.json +14 -0
- package/.eslintrc.json +20 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +9 -0
- package/.github/workflows/ci.yml +47 -0
- package/.github/workflows/release.yml +45 -0
- package/CHANGELOG.md +11 -0
- package/README.md +153 -0
- package/bun.lock +571 -0
- package/dist/bin/droid.d.ts +3 -0
- package/dist/bin/droid.d.ts.map +1 -0
- package/dist/bin/droid.js +48 -0
- package/dist/bin/droid.js.map +1 -0
- package/dist/commands/config.d.ts +8 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +67 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/install.d.ts +2 -0
- package/dist/commands/install.d.ts.map +1 -0
- package/dist/commands/install.js +42 -0
- package/dist/commands/install.js.map +1 -0
- package/dist/commands/setup.d.ts +2 -0
- package/dist/commands/setup.d.ts.map +1 -0
- package/dist/commands/setup.js +132 -0
- package/dist/commands/setup.js.map +1 -0
- package/dist/commands/skills.d.ts +2 -0
- package/dist/commands/skills.d.ts.map +1 -0
- package/dist/commands/skills.js +135 -0
- package/dist/commands/skills.js.map +1 -0
- package/dist/commands/uninstall.d.ts +2 -0
- package/dist/commands/uninstall.d.ts.map +1 -0
- package/dist/commands/uninstall.js +17 -0
- package/dist/commands/uninstall.js.map +1 -0
- package/dist/commands/update.d.ts +7 -0
- package/dist/commands/update.d.ts.map +1 -0
- package/dist/commands/update.js +45 -0
- package/dist/commands/update.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/config.d.ts +46 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +133 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/skill-config.d.ts +6 -0
- package/dist/lib/skill-config.d.ts.map +1 -0
- package/dist/lib/skill-config.js +80 -0
- package/dist/lib/skill-config.js.map +1 -0
- package/dist/lib/skills.d.ts +56 -0
- package/dist/lib/skills.d.ts.map +1 -0
- package/dist/lib/skills.js +245 -0
- package/dist/lib/skills.js.map +1 -0
- package/dist/lib/types.d.ts +54 -0
- package/dist/lib/types.d.ts.map +1 -0
- package/dist/lib/types.js +31 -0
- package/dist/lib/types.js.map +1 -0
- package/dist/lib/version.d.ts +10 -0
- package/dist/lib/version.d.ts.map +1 -0
- package/dist/lib/version.js +41 -0
- package/dist/lib/version.js.map +1 -0
- package/dist/skills/comments/SKILL.md +65 -0
- package/dist/skills/comments/SKILL.yaml +18 -0
- package/dist/skills/comments/commands/comments.md +48 -0
- package/package.json +58 -0
- package/src/bin/droid.ts +58 -0
- package/src/commands/config.ts +86 -0
- package/src/commands/install.ts +48 -0
- package/src/commands/setup.ts +149 -0
- package/src/commands/skills.ts +159 -0
- package/src/commands/uninstall.ts +18 -0
- package/src/commands/update.ts +58 -0
- package/src/index.ts +5 -0
- package/src/lib/config.test.ts +99 -0
- package/src/lib/config.ts +154 -0
- package/src/lib/skill-config.ts +93 -0
- package/src/lib/skills.test.ts +138 -0
- package/src/lib/skills.ts +285 -0
- package/src/lib/types.test.ts +65 -0
- package/src/lib/types.ts +68 -0
- package/src/lib/version.test.ts +23 -0
- package/src/lib/version.ts +47 -0
- package/src/skills/comments/SKILL.md +65 -0
- package/src/skills/comments/SKILL.yaml +18 -0
- package/src/skills/comments/commands/comments.md +48 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import {
|
|
5
|
+
getBundledSkills,
|
|
6
|
+
getBundledSkillsDir,
|
|
7
|
+
isSkillInstalled,
|
|
8
|
+
getInstalledSkill,
|
|
9
|
+
getSkillStatusDisplay,
|
|
10
|
+
installSkill,
|
|
11
|
+
uninstallSkill,
|
|
12
|
+
loadSkillManifest,
|
|
13
|
+
} from '../lib/skills.js';
|
|
14
|
+
import { promptForSkillConfig } from '../lib/skill-config.js';
|
|
15
|
+
import { SkillStatus, type SkillManifest } from '../lib/types.js';
|
|
16
|
+
|
|
17
|
+
function formatSkillChoice(skill: SkillManifest): string {
|
|
18
|
+
const installed = isSkillInstalled(skill.name);
|
|
19
|
+
const installedInfo = getInstalledSkill(skill.name);
|
|
20
|
+
const statusDisplay = getSkillStatusDisplay(skill.status);
|
|
21
|
+
|
|
22
|
+
let line = `${skill.name}`;
|
|
23
|
+
|
|
24
|
+
if (installed && installedInfo) {
|
|
25
|
+
line += chalk.green(` [installed] v${installedInfo.version}`);
|
|
26
|
+
} else {
|
|
27
|
+
line += chalk.gray(` v${skill.version}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (statusDisplay) {
|
|
31
|
+
const color =
|
|
32
|
+
skill.status === SkillStatus.Alpha
|
|
33
|
+
? chalk.red
|
|
34
|
+
: skill.status === SkillStatus.Beta
|
|
35
|
+
? chalk.yellow
|
|
36
|
+
: chalk.white;
|
|
37
|
+
line += ` ${color(statusDisplay)}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return line;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function skillsCommand(): Promise<void> {
|
|
44
|
+
const skills = getBundledSkills();
|
|
45
|
+
|
|
46
|
+
if (skills.length === 0) {
|
|
47
|
+
console.log(chalk.yellow('\nNo skills available yet.'));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
console.log(chalk.bold('\n🤖 Available Skills\n'));
|
|
52
|
+
|
|
53
|
+
const skillChoices = skills.map((skill) => ({
|
|
54
|
+
name: formatSkillChoice(skill),
|
|
55
|
+
value: skill.name,
|
|
56
|
+
short: skill.name,
|
|
57
|
+
}));
|
|
58
|
+
|
|
59
|
+
const choices = [
|
|
60
|
+
{
|
|
61
|
+
name: chalk.gray('← Exit'),
|
|
62
|
+
value: '__exit__',
|
|
63
|
+
short: 'Exit',
|
|
64
|
+
},
|
|
65
|
+
...skillChoices,
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
const { selectedSkill } = await inquirer.prompt<{ selectedSkill: string }>([
|
|
69
|
+
{
|
|
70
|
+
type: 'list',
|
|
71
|
+
name: 'selectedSkill',
|
|
72
|
+
message: 'Select a skill to view details or install:',
|
|
73
|
+
choices,
|
|
74
|
+
pageSize: 15,
|
|
75
|
+
},
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
if (selectedSkill === '__exit__') {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const skill = skills.find((s) => s.name === selectedSkill);
|
|
83
|
+
if (!skill) {
|
|
84
|
+
console.error(chalk.red('Skill not found'));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Show skill details
|
|
89
|
+
console.log(chalk.bold(`\n📦 ${skill.name}`));
|
|
90
|
+
console.log(chalk.gray(`Version: ${skill.version}`));
|
|
91
|
+
if (skill.status) {
|
|
92
|
+
console.log(chalk.gray(`Status: ${skill.status}`));
|
|
93
|
+
}
|
|
94
|
+
console.log(`\n${skill.description}`);
|
|
95
|
+
|
|
96
|
+
if (skill.dependencies && skill.dependencies.length > 0) {
|
|
97
|
+
console.log(chalk.gray(`\nDependencies: ${skill.dependencies.join(', ')}`));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const installed = isSkillInstalled(skill.name);
|
|
101
|
+
|
|
102
|
+
const actions = installed
|
|
103
|
+
? [
|
|
104
|
+
{ name: '← Back', value: 'back' },
|
|
105
|
+
{ name: 'Configure', value: 'configure' },
|
|
106
|
+
{ name: 'Uninstall', value: 'uninstall' },
|
|
107
|
+
]
|
|
108
|
+
: [
|
|
109
|
+
{ name: '← Back', value: 'back' },
|
|
110
|
+
{ name: 'Install', value: 'install' },
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
const { action } = await inquirer.prompt<{ action: string }>([
|
|
114
|
+
{
|
|
115
|
+
type: 'list',
|
|
116
|
+
name: 'action',
|
|
117
|
+
message: 'What would you like to do?',
|
|
118
|
+
choices: actions,
|
|
119
|
+
},
|
|
120
|
+
]);
|
|
121
|
+
|
|
122
|
+
switch (action) {
|
|
123
|
+
case 'install': {
|
|
124
|
+
const result = installSkill(skill.name);
|
|
125
|
+
if (result.success) {
|
|
126
|
+
console.log(chalk.green(`\n✓ ${result.message}`));
|
|
127
|
+
// Prompt for config if skill has options
|
|
128
|
+
const manifest = loadSkillManifest(join(getBundledSkillsDir(), skill.name));
|
|
129
|
+
if (manifest?.config_schema && Object.keys(manifest.config_schema).length > 0) {
|
|
130
|
+
await promptForSkillConfig(skill.name, manifest.config_schema, true);
|
|
131
|
+
}
|
|
132
|
+
} else {
|
|
133
|
+
console.log(chalk.red(`\n✗ ${result.message}`));
|
|
134
|
+
}
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
case 'configure': {
|
|
138
|
+
const manifest = loadSkillManifest(join(getBundledSkillsDir(), skill.name));
|
|
139
|
+
if (manifest?.config_schema && Object.keys(manifest.config_schema).length > 0) {
|
|
140
|
+
await promptForSkillConfig(skill.name, manifest.config_schema, false);
|
|
141
|
+
} else {
|
|
142
|
+
console.log(chalk.gray('\nThis skill has no configuration options.'));
|
|
143
|
+
}
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
case 'uninstall': {
|
|
147
|
+
const result = uninstallSkill(skill.name);
|
|
148
|
+
if (result.success) {
|
|
149
|
+
console.log(chalk.green(`\n✓ ${result.message}`));
|
|
150
|
+
} else {
|
|
151
|
+
console.log(chalk.red(`\n✗ ${result.message}`));
|
|
152
|
+
}
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
case 'back':
|
|
156
|
+
await skillsCommand();
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { uninstallSkill, isSkillInstalled } from '../lib/skills.js';
|
|
3
|
+
|
|
4
|
+
export async function uninstallCommand(skillName: string): Promise<void> {
|
|
5
|
+
if (!isSkillInstalled(skillName)) {
|
|
6
|
+
console.error(chalk.red(`\n✗ Skill '${skillName}' is not installed`));
|
|
7
|
+
process.exit(1);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const result = uninstallSkill(skillName);
|
|
11
|
+
|
|
12
|
+
if (result.success) {
|
|
13
|
+
console.log(chalk.green(`\n✓ ${result.message}`));
|
|
14
|
+
} else {
|
|
15
|
+
console.error(chalk.red(`\n✗ ${result.message}`));
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { execSync } from 'child_process';
|
|
3
|
+
import { getVersion } from '../lib/version.js';
|
|
4
|
+
|
|
5
|
+
interface UpdateOptions {
|
|
6
|
+
skills?: boolean;
|
|
7
|
+
cli?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function updateCommand(skill?: string, options?: UpdateOptions): Promise<void> {
|
|
11
|
+
// If specific skill specified, update just that skill
|
|
12
|
+
if (skill) {
|
|
13
|
+
console.log(chalk.yellow('\n⚠ Per-skill updates not implemented yet'));
|
|
14
|
+
console.log(chalk.gray('Skills are bundled with the CLI - run `droid update` to update all.'));
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// If --skills flag, update skills only
|
|
19
|
+
if (options?.skills) {
|
|
20
|
+
console.log(chalk.yellow('\n⚠ Skill-only updates not implemented yet'));
|
|
21
|
+
console.log(chalk.gray('Skills are bundled with the CLI - run `droid update` to update all.'));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Update CLI (and bundled skills with it)
|
|
26
|
+
console.log(chalk.bold('\n🤖 Updating Droid...\n'));
|
|
27
|
+
|
|
28
|
+
const currentVersion = getVersion();
|
|
29
|
+
console.log(chalk.gray(`Current version: ${currentVersion}`));
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
// Check for latest version
|
|
33
|
+
const latestVersion = execSync('npm view @orderful/droid version 2>/dev/null', {
|
|
34
|
+
encoding: 'utf-8',
|
|
35
|
+
}).trim();
|
|
36
|
+
|
|
37
|
+
if (!latestVersion) {
|
|
38
|
+
console.log(chalk.yellow('\n⚠ Could not check for updates'));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (latestVersion === currentVersion) {
|
|
43
|
+
console.log(chalk.green('\n✓ Already on latest version'));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
console.log(chalk.gray(`Latest version: ${latestVersion}`));
|
|
48
|
+
console.log(chalk.gray('\nUpdating...'));
|
|
49
|
+
|
|
50
|
+
execSync('npm install -g @orderful/droid@latest', { stdio: 'inherit' });
|
|
51
|
+
|
|
52
|
+
console.log(chalk.green(`\n✓ Updated to v${latestVersion}`));
|
|
53
|
+
} catch {
|
|
54
|
+
// Package might not be published yet
|
|
55
|
+
console.log(chalk.yellow('\n⚠ Could not check for updates'));
|
|
56
|
+
console.log(chalk.gray('Package may not be published yet.'));
|
|
57
|
+
}
|
|
58
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
|
|
2
|
+
import { existsSync, mkdirSync, rmSync, readFileSync, writeFileSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { tmpdir } from 'os';
|
|
5
|
+
import YAML from 'yaml';
|
|
6
|
+
import { AITool, BuiltInOutput } from './types.js';
|
|
7
|
+
|
|
8
|
+
describe('config types', () => {
|
|
9
|
+
it('should have correct enum values', () => {
|
|
10
|
+
expect(AITool.ClaudeCode).toBe('claude-code');
|
|
11
|
+
expect(AITool.OpenCode).toBe('opencode');
|
|
12
|
+
expect(BuiltInOutput.Terminal).toBe('terminal');
|
|
13
|
+
expect(BuiltInOutput.Editor).toBe('editor');
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe('config value parsing', () => {
|
|
18
|
+
it('should parse dot notation keys correctly', () => {
|
|
19
|
+
const keys = 'skills.comments.version'.split('.');
|
|
20
|
+
expect(keys).toEqual(['skills', 'comments', 'version']);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should handle single key', () => {
|
|
24
|
+
const keys = 'user_tag'.split('.');
|
|
25
|
+
expect(keys).toEqual(['user_tag']);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('YAML serialization', () => {
|
|
30
|
+
it('should serialize config correctly', () => {
|
|
31
|
+
const config = {
|
|
32
|
+
ai_tool: 'claude-code',
|
|
33
|
+
user_tag: '@fry',
|
|
34
|
+
output_preference: 'terminal',
|
|
35
|
+
git_username: 'frytyler',
|
|
36
|
+
skills: {
|
|
37
|
+
comments: {
|
|
38
|
+
version: '0.1.0',
|
|
39
|
+
installed_at: '2025-01-01T00:00:00.000Z',
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const yaml = YAML.stringify(config, { indent: 2 });
|
|
45
|
+
const parsed = YAML.parse(yaml);
|
|
46
|
+
|
|
47
|
+
expect(parsed).toEqual(config);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should handle empty skills object', () => {
|
|
51
|
+
const config = {
|
|
52
|
+
ai_tool: 'claude-code',
|
|
53
|
+
user_tag: '@user',
|
|
54
|
+
skills: {},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const yaml = YAML.stringify(config, { indent: 2 });
|
|
58
|
+
const parsed = YAML.parse(yaml);
|
|
59
|
+
|
|
60
|
+
expect(parsed.skills).toEqual({});
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('config file operations', () => {
|
|
65
|
+
let testDir: string;
|
|
66
|
+
|
|
67
|
+
beforeEach(() => {
|
|
68
|
+
testDir = join(tmpdir(), `droid-test-${Date.now()}`);
|
|
69
|
+
mkdirSync(testDir, { recursive: true });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
afterEach(() => {
|
|
73
|
+
if (existsSync(testDir)) {
|
|
74
|
+
rmSync(testDir, { recursive: true });
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should create directory if it does not exist', () => {
|
|
79
|
+
const nestedDir = join(testDir, 'nested', 'path');
|
|
80
|
+
expect(existsSync(nestedDir)).toBe(false);
|
|
81
|
+
|
|
82
|
+
mkdirSync(nestedDir, { recursive: true });
|
|
83
|
+
expect(existsSync(nestedDir)).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should write and read YAML file', () => {
|
|
87
|
+
const configPath = join(testDir, 'config.yaml');
|
|
88
|
+
const config = { user_mention: '@test', ai_tool: 'claude-code' };
|
|
89
|
+
|
|
90
|
+
const content = YAML.stringify(config, { indent: 2 });
|
|
91
|
+
writeFileSync(configPath, content, 'utf-8');
|
|
92
|
+
|
|
93
|
+
const read = readFileSync(configPath, 'utf-8');
|
|
94
|
+
const parsed = YAML.parse(read);
|
|
95
|
+
|
|
96
|
+
expect(parsed.user_mention).toBe('@test');
|
|
97
|
+
expect(parsed.ai_tool).toBe('claude-code');
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
2
|
+
import { homedir } from 'os';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import YAML from 'yaml';
|
|
5
|
+
import { AITool, BuiltInOutput, type DroidConfig, type SkillOverrides } from './types.js';
|
|
6
|
+
|
|
7
|
+
const CONFIG_DIR = join(homedir(), '.droid');
|
|
8
|
+
const CONFIG_FILE = join(CONFIG_DIR, 'config.yaml');
|
|
9
|
+
|
|
10
|
+
const DEFAULT_CONFIG: DroidConfig = {
|
|
11
|
+
ai_tool: AITool.ClaudeCode,
|
|
12
|
+
user_mention: '@user',
|
|
13
|
+
output_preference: BuiltInOutput.Terminal,
|
|
14
|
+
git_username: '',
|
|
15
|
+
skills: {},
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Ensure the config directory exists
|
|
20
|
+
*/
|
|
21
|
+
export function ensureConfigDir(): void {
|
|
22
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
23
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Check if config file exists
|
|
29
|
+
*/
|
|
30
|
+
export function configExists(): boolean {
|
|
31
|
+
return existsSync(CONFIG_FILE);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Load the global config, or return defaults if none exists
|
|
36
|
+
*/
|
|
37
|
+
export function loadConfig(): DroidConfig {
|
|
38
|
+
ensureConfigDir();
|
|
39
|
+
|
|
40
|
+
if (!existsSync(CONFIG_FILE)) {
|
|
41
|
+
return { ...DEFAULT_CONFIG };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const content = readFileSync(CONFIG_FILE, 'utf-8');
|
|
46
|
+
const config = YAML.parse(content) as Partial<DroidConfig>;
|
|
47
|
+
return { ...DEFAULT_CONFIG, ...config };
|
|
48
|
+
} catch (error) {
|
|
49
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
50
|
+
console.error(`Error reading config: ${message}`);
|
|
51
|
+
return { ...DEFAULT_CONFIG };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Save the global config
|
|
57
|
+
*/
|
|
58
|
+
export function saveConfig(config: DroidConfig): void {
|
|
59
|
+
ensureConfigDir();
|
|
60
|
+
|
|
61
|
+
const content = YAML.stringify(config, { indent: 2 });
|
|
62
|
+
writeFileSync(CONFIG_FILE, content, 'utf-8');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get a specific config value using dot notation
|
|
67
|
+
*/
|
|
68
|
+
export function getConfigValue(key: string): unknown {
|
|
69
|
+
const config = loadConfig();
|
|
70
|
+
const keys = key.split('.');
|
|
71
|
+
let value: unknown = config;
|
|
72
|
+
|
|
73
|
+
for (const k of keys) {
|
|
74
|
+
if (value === undefined || value === null || typeof value !== 'object') {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
value = (value as Record<string, unknown>)[k];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return value;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Set a specific config value using dot notation
|
|
85
|
+
*/
|
|
86
|
+
export function setConfigValue(key: string, value: unknown): void {
|
|
87
|
+
const config = loadConfig();
|
|
88
|
+
const keys = key.split('.');
|
|
89
|
+
let current: Record<string, unknown> = config as unknown as Record<string, unknown>;
|
|
90
|
+
|
|
91
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
92
|
+
if (current[keys[i]] === undefined) {
|
|
93
|
+
current[keys[i]] = {};
|
|
94
|
+
}
|
|
95
|
+
current = current[keys[i]] as Record<string, unknown>;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
current[keys[keys.length - 1]] = value;
|
|
99
|
+
saveConfig(config);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get the path to the config file
|
|
104
|
+
*/
|
|
105
|
+
export function getConfigPath(): string {
|
|
106
|
+
return CONFIG_FILE;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Get the path to the config directory
|
|
111
|
+
*/
|
|
112
|
+
export function getConfigDir(): string {
|
|
113
|
+
return CONFIG_DIR;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Get skill-specific overrides path
|
|
118
|
+
*/
|
|
119
|
+
export function getSkillOverridesPath(skillName: string): string {
|
|
120
|
+
return join(CONFIG_DIR, 'skills', skillName, 'overrides.yaml');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Load skill-specific overrides
|
|
125
|
+
*/
|
|
126
|
+
export function loadSkillOverrides(skillName: string): SkillOverrides {
|
|
127
|
+
const overridesPath = getSkillOverridesPath(skillName);
|
|
128
|
+
|
|
129
|
+
if (!existsSync(overridesPath)) {
|
|
130
|
+
return {};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
const content = readFileSync(overridesPath, 'utf-8');
|
|
135
|
+
return (YAML.parse(content) as SkillOverrides) || {};
|
|
136
|
+
} catch {
|
|
137
|
+
return {};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Save skill-specific overrides
|
|
143
|
+
*/
|
|
144
|
+
export function saveSkillOverrides(skillName: string, overrides: SkillOverrides): void {
|
|
145
|
+
const overridesPath = getSkillOverridesPath(skillName);
|
|
146
|
+
const skillDir = join(CONFIG_DIR, 'skills', skillName);
|
|
147
|
+
|
|
148
|
+
if (!existsSync(skillDir)) {
|
|
149
|
+
mkdirSync(skillDir, { recursive: true });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const content = YAML.stringify(overrides, { indent: 2 });
|
|
153
|
+
writeFileSync(overridesPath, content, 'utf-8');
|
|
154
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import { saveSkillOverrides, loadConfig, loadSkillOverrides } from './config.js';
|
|
4
|
+
import { ConfigOptionType, type SkillOverrides, type ConfigOption } from './types.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Prompt user to configure a skill after install
|
|
8
|
+
*/
|
|
9
|
+
export async function promptForSkillConfig(
|
|
10
|
+
skillName: string,
|
|
11
|
+
configSchema: Record<string, ConfigOption>,
|
|
12
|
+
askFirst: boolean = true
|
|
13
|
+
): Promise<void> {
|
|
14
|
+
const globalConfig = loadConfig();
|
|
15
|
+
|
|
16
|
+
if (askFirst) {
|
|
17
|
+
const { wantsConfigure } = await inquirer.prompt<{ wantsConfigure: boolean }>([
|
|
18
|
+
{
|
|
19
|
+
type: 'confirm',
|
|
20
|
+
name: 'wantsConfigure',
|
|
21
|
+
message: 'Would you like to configure this skill now?',
|
|
22
|
+
default: false,
|
|
23
|
+
},
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
if (!wantsConfigure) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
console.log(chalk.bold(`\n⚙️ Configure ${skillName}\n`));
|
|
32
|
+
|
|
33
|
+
// Load existing overrides to use as defaults
|
|
34
|
+
const existingOverrides = loadSkillOverrides(skillName);
|
|
35
|
+
|
|
36
|
+
const questions = Object.entries(configSchema).map(([key, option]) => {
|
|
37
|
+
const baseQuestion = {
|
|
38
|
+
name: key,
|
|
39
|
+
message: option.description,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Use existing override as default if present
|
|
43
|
+
const existingValue = existingOverrides[key];
|
|
44
|
+
|
|
45
|
+
switch (option.type) {
|
|
46
|
+
case ConfigOptionType.Boolean:
|
|
47
|
+
return {
|
|
48
|
+
...baseQuestion,
|
|
49
|
+
type: 'confirm' as const,
|
|
50
|
+
default: existingValue ?? option.default ?? false,
|
|
51
|
+
};
|
|
52
|
+
case ConfigOptionType.Select:
|
|
53
|
+
return {
|
|
54
|
+
...baseQuestion,
|
|
55
|
+
type: 'list' as const,
|
|
56
|
+
choices: option.options || [],
|
|
57
|
+
default: existingValue ?? option.default,
|
|
58
|
+
};
|
|
59
|
+
case ConfigOptionType.String:
|
|
60
|
+
default: {
|
|
61
|
+
// For user_mention, default to global config value if no existing override
|
|
62
|
+
let defaultValue = existingValue ?? option.default ?? '';
|
|
63
|
+
if (key === 'user_mention' && !existingValue && globalConfig.user_mention) {
|
|
64
|
+
defaultValue = globalConfig.user_mention;
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
...baseQuestion,
|
|
68
|
+
type: 'input' as const,
|
|
69
|
+
default: defaultValue,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const answers = await inquirer.prompt(questions);
|
|
76
|
+
|
|
77
|
+
// Filter out empty/default values to keep overrides minimal
|
|
78
|
+
const overrides: SkillOverrides = {};
|
|
79
|
+
for (const [key, value] of Object.entries(answers)) {
|
|
80
|
+
const schema = configSchema[key];
|
|
81
|
+
// Only save if different from default
|
|
82
|
+
if (value !== schema.default && value !== '' && value !== false) {
|
|
83
|
+
overrides[key] = value as string | boolean | number;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (Object.keys(overrides).length > 0) {
|
|
88
|
+
saveSkillOverrides(skillName, overrides);
|
|
89
|
+
console.log(chalk.green(`\n✓ Configuration saved to ~/.droid/skills/${skillName}/overrides.yaml`));
|
|
90
|
+
} else {
|
|
91
|
+
console.log(chalk.gray('\nNo custom configuration set (using defaults).'));
|
|
92
|
+
}
|
|
93
|
+
}
|