@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.
Files changed (86) hide show
  1. package/.changeset/README.md +30 -0
  2. package/.changeset/config.json +14 -0
  3. package/.eslintrc.json +20 -0
  4. package/.github/PULL_REQUEST_TEMPLATE.md +9 -0
  5. package/.github/workflows/ci.yml +47 -0
  6. package/.github/workflows/release.yml +45 -0
  7. package/CHANGELOG.md +11 -0
  8. package/README.md +153 -0
  9. package/bun.lock +571 -0
  10. package/dist/bin/droid.d.ts +3 -0
  11. package/dist/bin/droid.d.ts.map +1 -0
  12. package/dist/bin/droid.js +48 -0
  13. package/dist/bin/droid.js.map +1 -0
  14. package/dist/commands/config.d.ts +8 -0
  15. package/dist/commands/config.d.ts.map +1 -0
  16. package/dist/commands/config.js +67 -0
  17. package/dist/commands/config.js.map +1 -0
  18. package/dist/commands/install.d.ts +2 -0
  19. package/dist/commands/install.d.ts.map +1 -0
  20. package/dist/commands/install.js +42 -0
  21. package/dist/commands/install.js.map +1 -0
  22. package/dist/commands/setup.d.ts +2 -0
  23. package/dist/commands/setup.d.ts.map +1 -0
  24. package/dist/commands/setup.js +132 -0
  25. package/dist/commands/setup.js.map +1 -0
  26. package/dist/commands/skills.d.ts +2 -0
  27. package/dist/commands/skills.d.ts.map +1 -0
  28. package/dist/commands/skills.js +135 -0
  29. package/dist/commands/skills.js.map +1 -0
  30. package/dist/commands/uninstall.d.ts +2 -0
  31. package/dist/commands/uninstall.d.ts.map +1 -0
  32. package/dist/commands/uninstall.js +17 -0
  33. package/dist/commands/uninstall.js.map +1 -0
  34. package/dist/commands/update.d.ts +7 -0
  35. package/dist/commands/update.d.ts.map +1 -0
  36. package/dist/commands/update.js +45 -0
  37. package/dist/commands/update.js.map +1 -0
  38. package/dist/index.d.ts +5 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/index.js +6 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/lib/config.d.ts +46 -0
  43. package/dist/lib/config.d.ts.map +1 -0
  44. package/dist/lib/config.js +133 -0
  45. package/dist/lib/config.js.map +1 -0
  46. package/dist/lib/skill-config.d.ts +6 -0
  47. package/dist/lib/skill-config.d.ts.map +1 -0
  48. package/dist/lib/skill-config.js +80 -0
  49. package/dist/lib/skill-config.js.map +1 -0
  50. package/dist/lib/skills.d.ts +56 -0
  51. package/dist/lib/skills.d.ts.map +1 -0
  52. package/dist/lib/skills.js +245 -0
  53. package/dist/lib/skills.js.map +1 -0
  54. package/dist/lib/types.d.ts +54 -0
  55. package/dist/lib/types.d.ts.map +1 -0
  56. package/dist/lib/types.js +31 -0
  57. package/dist/lib/types.js.map +1 -0
  58. package/dist/lib/version.d.ts +10 -0
  59. package/dist/lib/version.d.ts.map +1 -0
  60. package/dist/lib/version.js +41 -0
  61. package/dist/lib/version.js.map +1 -0
  62. package/dist/skills/comments/SKILL.md +65 -0
  63. package/dist/skills/comments/SKILL.yaml +18 -0
  64. package/dist/skills/comments/commands/comments.md +48 -0
  65. package/package.json +58 -0
  66. package/src/bin/droid.ts +58 -0
  67. package/src/commands/config.ts +86 -0
  68. package/src/commands/install.ts +48 -0
  69. package/src/commands/setup.ts +149 -0
  70. package/src/commands/skills.ts +159 -0
  71. package/src/commands/uninstall.ts +18 -0
  72. package/src/commands/update.ts +58 -0
  73. package/src/index.ts +5 -0
  74. package/src/lib/config.test.ts +99 -0
  75. package/src/lib/config.ts +154 -0
  76. package/src/lib/skill-config.ts +93 -0
  77. package/src/lib/skills.test.ts +138 -0
  78. package/src/lib/skills.ts +285 -0
  79. package/src/lib/types.test.ts +65 -0
  80. package/src/lib/types.ts +68 -0
  81. package/src/lib/version.test.ts +23 -0
  82. package/src/lib/version.ts +47 -0
  83. package/src/skills/comments/SKILL.md +65 -0
  84. package/src/skills/comments/SKILL.yaml +18 -0
  85. package/src/skills/comments/commands/comments.md +48 -0
  86. 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,5 @@
1
+ // Re-export types and utilities for potential programmatic usage
2
+ export * from './lib/types.js';
3
+ export * from './lib/config.js';
4
+ export * from './lib/skills.js';
5
+ export * from './lib/version.js';
@@ -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
+ }