@orderful/droid 0.6.0 → 0.8.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 (89) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +80 -89
  3. package/assets/droid+claude.png +0 -0
  4. package/bun.lock +1 -1
  5. package/dist/commands/setup.d.ts +1 -0
  6. package/dist/commands/setup.d.ts.map +1 -1
  7. package/dist/commands/setup.js +77 -9
  8. package/dist/commands/setup.js.map +1 -1
  9. package/dist/commands/tui.d.ts.map +1 -1
  10. package/dist/commands/tui.js +111 -70
  11. package/dist/commands/tui.js.map +1 -1
  12. package/dist/lib/agents.d.ts +19 -4
  13. package/dist/lib/agents.d.ts.map +1 -1
  14. package/dist/lib/agents.js +121 -42
  15. package/dist/lib/agents.js.map +1 -1
  16. package/dist/lib/skills.d.ts.map +1 -1
  17. package/dist/lib/skills.js +55 -0
  18. package/dist/lib/skills.js.map +1 -1
  19. package/dist/lib/types.d.ts +1 -0
  20. package/dist/lib/types.d.ts.map +1 -1
  21. package/dist/skills/brain/SKILL.md +146 -0
  22. package/dist/skills/brain/SKILL.yaml +31 -0
  23. package/dist/skills/brain/commands/README.md +7 -0
  24. package/dist/skills/brain/commands/brain.md +50 -0
  25. package/dist/skills/brain/references/metadata.md +59 -0
  26. package/dist/skills/brain/references/naming.md +48 -0
  27. package/dist/skills/brain/references/templates.md +102 -0
  28. package/dist/skills/brain/references/workflows.md +198 -0
  29. package/dist/skills/brain-obsidian/SKILL.md +108 -0
  30. package/dist/skills/brain-obsidian/SKILL.yaml +45 -0
  31. package/dist/skills/brain-obsidian/references/templates.md +144 -0
  32. package/dist/skills/brain-obsidian/references/workflows.md +192 -0
  33. package/dist/skills/code-review/SKILL.md +57 -0
  34. package/dist/skills/code-review/SKILL.yaml +22 -0
  35. package/dist/skills/code-review/agents/edi-standards-reviewer/AGENT.md +39 -0
  36. package/dist/skills/code-review/agents/edi-standards-reviewer/AGENT.yaml +14 -0
  37. package/dist/skills/code-review/agents/error-handling-reviewer/AGENT.md +51 -0
  38. package/dist/skills/code-review/agents/error-handling-reviewer/AGENT.yaml +14 -0
  39. package/dist/skills/code-review/agents/test-coverage-analyzer/AGENT.md +53 -0
  40. package/dist/skills/code-review/agents/test-coverage-analyzer/AGENT.yaml +14 -0
  41. package/dist/skills/code-review/agents/type-reviewer/AGENT.md +50 -0
  42. package/dist/skills/code-review/agents/type-reviewer/AGENT.yaml +13 -0
  43. package/dist/skills/code-review/commands/code-review.md +91 -0
  44. package/dist/skills/comments/SKILL.md +20 -5
  45. package/dist/skills/comments/SKILL.yaml +1 -1
  46. package/dist/skills/comments/commands/comments.md +1 -1
  47. package/dist/skills/project/SKILL.md +9 -7
  48. package/dist/skills/project/SKILL.yaml +1 -1
  49. package/dist/skills/project/commands/project.md +9 -4
  50. package/dist/skills/project/references/creating.md +9 -4
  51. package/dist/skills/project/references/loading.md +11 -5
  52. package/package.json +1 -1
  53. package/src/commands/setup.test.ts +276 -0
  54. package/src/commands/setup.ts +80 -10
  55. package/src/commands/tui.tsx +149 -82
  56. package/src/lib/agents.ts +134 -44
  57. package/src/lib/skills.ts +60 -0
  58. package/src/lib/types.ts +1 -0
  59. package/src/skills/brain/SKILL.md +146 -0
  60. package/src/skills/brain/SKILL.yaml +31 -0
  61. package/src/skills/brain/commands/README.md +7 -0
  62. package/src/skills/brain/commands/brain.md +50 -0
  63. package/src/skills/brain/references/metadata.md +59 -0
  64. package/src/skills/brain/references/naming.md +48 -0
  65. package/src/skills/brain/references/templates.md +102 -0
  66. package/src/skills/brain/references/workflows.md +198 -0
  67. package/src/skills/brain-obsidian/SKILL.md +108 -0
  68. package/src/skills/brain-obsidian/SKILL.yaml +45 -0
  69. package/src/skills/brain-obsidian/references/templates.md +144 -0
  70. package/src/skills/brain-obsidian/references/workflows.md +192 -0
  71. package/src/skills/code-review/SKILL.md +57 -0
  72. package/src/skills/code-review/SKILL.yaml +22 -0
  73. package/src/skills/code-review/agents/edi-standards-reviewer/AGENT.md +39 -0
  74. package/src/skills/code-review/agents/edi-standards-reviewer/AGENT.yaml +14 -0
  75. package/src/skills/code-review/agents/error-handling-reviewer/AGENT.md +51 -0
  76. package/src/skills/code-review/agents/error-handling-reviewer/AGENT.yaml +14 -0
  77. package/src/skills/code-review/agents/test-coverage-analyzer/AGENT.md +53 -0
  78. package/src/skills/code-review/agents/test-coverage-analyzer/AGENT.yaml +14 -0
  79. package/src/skills/code-review/agents/type-reviewer/AGENT.md +50 -0
  80. package/src/skills/code-review/agents/type-reviewer/AGENT.yaml +13 -0
  81. package/src/skills/code-review/commands/code-review.md +91 -0
  82. package/src/skills/comments/SKILL.md +20 -5
  83. package/src/skills/comments/SKILL.yaml +1 -1
  84. package/src/skills/comments/commands/comments.md +1 -1
  85. package/src/skills/project/SKILL.md +9 -7
  86. package/src/skills/project/SKILL.yaml +1 -1
  87. package/src/skills/project/commands/project.md +9 -4
  88. package/src/skills/project/references/creating.md +9 -4
  89. package/src/skills/project/references/loading.md +11 -5
@@ -0,0 +1,276 @@
1
+ import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from 'bun:test';
2
+ import { existsSync, mkdirSync, rmSync, readFileSync, writeFileSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { tmpdir } from 'os';
5
+ import { AITool } from '../lib/types.js';
6
+
7
+ // We need to mock homedir() before importing the module
8
+ // Create a test directory that will act as our fake home
9
+ let testHomeDir: string;
10
+
11
+ // Mock the os module's homedir function
12
+ const originalHomedir = await import('os').then(m => m.homedir);
13
+
14
+ describe('configureAIToolPermissions', () => {
15
+ beforeEach(() => {
16
+ testHomeDir = join(tmpdir(), `droid-setup-test-${Date.now()}`);
17
+ mkdirSync(testHomeDir, { recursive: true });
18
+ });
19
+
20
+ afterEach(() => {
21
+ if (existsSync(testHomeDir)) {
22
+ rmSync(testHomeDir, { recursive: true });
23
+ }
24
+ });
25
+
26
+ describe('OpenCode plugin configuration', () => {
27
+ it('should add opencode-skills plugin to empty config', () => {
28
+ const configDir = join(testHomeDir, '.config', 'opencode');
29
+ const configPath = join(configDir, 'opencode.json');
30
+
31
+ mkdirSync(configDir, { recursive: true });
32
+
33
+ const config: { plugin?: string[] } = {};
34
+ if (!Array.isArray(config.plugin)) {
35
+ config.plugin = [];
36
+ }
37
+ config.plugin.push('opencode-skills');
38
+
39
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
40
+
41
+ const read = JSON.parse(readFileSync(configPath, 'utf-8'));
42
+ expect(read.plugin).toContain('opencode-skills');
43
+ });
44
+
45
+ it('should add opencode-skills plugin to config with existing plugins', () => {
46
+ const configDir = join(testHomeDir, '.config', 'opencode');
47
+ const configPath = join(configDir, 'opencode.json');
48
+
49
+ mkdirSync(configDir, { recursive: true });
50
+
51
+ const existingConfig = {
52
+ plugin: ['some-other-plugin'],
53
+ theme: 'dark',
54
+ };
55
+ writeFileSync(configPath, JSON.stringify(existingConfig, null, 2), 'utf-8');
56
+
57
+ // Read and modify (simulating what the function does)
58
+ const config = JSON.parse(readFileSync(configPath, 'utf-8'));
59
+ if (!config.plugin.includes('opencode-skills')) {
60
+ config.plugin.push('opencode-skills');
61
+ }
62
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
63
+
64
+ const read = JSON.parse(readFileSync(configPath, 'utf-8'));
65
+ expect(read.plugin).toContain('opencode-skills');
66
+ expect(read.plugin).toContain('some-other-plugin');
67
+ expect(read.theme).toBe('dark');
68
+ });
69
+
70
+ it('should not duplicate opencode-skills if already present', () => {
71
+ const configDir = join(testHomeDir, '.config', 'opencode');
72
+ const configPath = join(configDir, 'opencode.json');
73
+
74
+ mkdirSync(configDir, { recursive: true });
75
+
76
+ const existingConfig = {
77
+ plugin: ['opencode-skills'],
78
+ };
79
+ writeFileSync(configPath, JSON.stringify(existingConfig, null, 2), 'utf-8');
80
+
81
+ const config = JSON.parse(readFileSync(configPath, 'utf-8'));
82
+ const alreadyPresent = config.plugin.includes('opencode-skills');
83
+
84
+ expect(alreadyPresent).toBe(true);
85
+ expect(config.plugin.length).toBe(1);
86
+ });
87
+
88
+ it('should handle config without plugin key', () => {
89
+ const configDir = join(testHomeDir, '.config', 'opencode');
90
+ const configPath = join(configDir, 'opencode.json');
91
+
92
+ mkdirSync(configDir, { recursive: true });
93
+
94
+ const existingConfig = {
95
+ theme: 'dark',
96
+ model: 'claude-sonnet',
97
+ };
98
+ writeFileSync(configPath, JSON.stringify(existingConfig, null, 2), 'utf-8');
99
+
100
+ const config = JSON.parse(readFileSync(configPath, 'utf-8'));
101
+ if (!Array.isArray(config.plugin)) {
102
+ config.plugin = [];
103
+ }
104
+ config.plugin.push('opencode-skills');
105
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
106
+
107
+ const read = JSON.parse(readFileSync(configPath, 'utf-8'));
108
+ expect(read.plugin).toContain('opencode-skills');
109
+ expect(read.theme).toBe('dark');
110
+ expect(read.model).toBe('claude-sonnet');
111
+ });
112
+
113
+ it('should create config directory if it does not exist', () => {
114
+ const nestedDir = join(testHomeDir, '.config', 'opencode');
115
+ expect(existsSync(nestedDir)).toBe(false);
116
+
117
+ mkdirSync(nestedDir, { recursive: true });
118
+ expect(existsSync(nestedDir)).toBe(true);
119
+
120
+ const configPath = join(nestedDir, 'opencode.json');
121
+ const config = { plugin: ['opencode-skills'] };
122
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
123
+
124
+ expect(existsSync(configPath)).toBe(true);
125
+ });
126
+
127
+ it('should handle corrupted JSON gracefully', () => {
128
+ const configDir = join(testHomeDir, '.config', 'opencode');
129
+ const configPath = join(configDir, 'opencode.json');
130
+
131
+ mkdirSync(configDir, { recursive: true });
132
+ writeFileSync(configPath, '{ invalid json }}}', 'utf-8');
133
+
134
+ let config: { plugin?: string[] } = {};
135
+ try {
136
+ config = JSON.parse(readFileSync(configPath, 'utf-8'));
137
+ } catch {
138
+ // Invalid JSON, start fresh
139
+ config = {};
140
+ }
141
+
142
+ if (!Array.isArray(config.plugin)) {
143
+ config.plugin = [];
144
+ }
145
+ config.plugin.push('opencode-skills');
146
+
147
+ expect(config.plugin).toContain('opencode-skills');
148
+ });
149
+ });
150
+
151
+ describe('Claude Code permissions configuration', () => {
152
+ it('should add permissions to empty settings', () => {
153
+ const claudeDir = join(testHomeDir, '.claude');
154
+ const settingsPath = join(claudeDir, 'settings.json');
155
+
156
+ mkdirSync(claudeDir, { recursive: true });
157
+
158
+ const settings: { permissions?: { allow?: string[] } } = {};
159
+ if (!settings.permissions) {
160
+ settings.permissions = {};
161
+ }
162
+ if (!Array.isArray(settings.permissions.allow)) {
163
+ settings.permissions.allow = [];
164
+ }
165
+
166
+ const permissions = ['Read(~/.droid/**)', 'Write(~/.droid/**)'];
167
+ for (const perm of permissions) {
168
+ if (!settings.permissions.allow.includes(perm)) {
169
+ settings.permissions.allow.push(perm);
170
+ }
171
+ }
172
+
173
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
174
+
175
+ const read = JSON.parse(readFileSync(settingsPath, 'utf-8'));
176
+ expect(read.permissions.allow).toContain('Read(~/.droid/**)');
177
+ expect(read.permissions.allow).toContain('Write(~/.droid/**)');
178
+ });
179
+
180
+ it('should preserve existing settings when adding permissions', () => {
181
+ const claudeDir = join(testHomeDir, '.claude');
182
+ const settingsPath = join(claudeDir, 'settings.json');
183
+
184
+ mkdirSync(claudeDir, { recursive: true });
185
+
186
+ const existingSettings = {
187
+ permissions: {
188
+ allow: ['Bash(git:*)'],
189
+ deny: ['Bash(rm -rf /*)'],
190
+ },
191
+ other_setting: true,
192
+ };
193
+ writeFileSync(settingsPath, JSON.stringify(existingSettings, null, 2), 'utf-8');
194
+
195
+ const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
196
+ settings.permissions.allow.push('Read(~/.droid/**)');
197
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
198
+
199
+ const read = JSON.parse(readFileSync(settingsPath, 'utf-8'));
200
+ expect(read.permissions.allow).toContain('Bash(git:*)');
201
+ expect(read.permissions.allow).toContain('Read(~/.droid/**)');
202
+ expect(read.permissions.deny).toContain('Bash(rm -rf /*)');
203
+ expect(read.other_setting).toBe(true);
204
+ });
205
+
206
+ it('should handle corrupted JSON gracefully', () => {
207
+ const claudeDir = join(testHomeDir, '.claude');
208
+ const settingsPath = join(claudeDir, 'settings.json');
209
+
210
+ mkdirSync(claudeDir, { recursive: true });
211
+ writeFileSync(settingsPath, '{ not valid json {{{{', 'utf-8');
212
+
213
+ let settings: { permissions?: { allow?: string[] } } = {};
214
+ try {
215
+ settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
216
+ } catch {
217
+ // Invalid JSON, start fresh
218
+ settings = {};
219
+ }
220
+
221
+ if (!settings.permissions) {
222
+ settings.permissions = {};
223
+ }
224
+ if (!Array.isArray(settings.permissions.allow)) {
225
+ settings.permissions.allow = [];
226
+ }
227
+ settings.permissions.allow.push('Read(~/.droid/**)');
228
+
229
+ expect(settings.permissions.allow).toContain('Read(~/.droid/**)');
230
+ });
231
+ });
232
+
233
+ describe('Return value validation', () => {
234
+ it('should return added items when new permissions are added', () => {
235
+ const added: string[] = [];
236
+ const permissions = ['Read(~/.droid/**)', 'Write(~/.droid/**)'];
237
+ const existingAllow: string[] = [];
238
+
239
+ for (const perm of permissions) {
240
+ if (!existingAllow.includes(perm)) {
241
+ existingAllow.push(perm);
242
+ added.push(perm);
243
+ }
244
+ }
245
+
246
+ expect(added).toHaveLength(2);
247
+ expect(added).toContain('Read(~/.droid/**)');
248
+ expect(added).toContain('Write(~/.droid/**)');
249
+ });
250
+
251
+ it('should return alreadyPresent=true when no new permissions needed', () => {
252
+ const added: string[] = [];
253
+ const permissions = ['Read(~/.droid/**)'];
254
+ const existingAllow = ['Read(~/.droid/**)', 'Write(~/.droid/**)'];
255
+
256
+ for (const perm of permissions) {
257
+ if (!existingAllow.includes(perm)) {
258
+ existingAllow.push(perm);
259
+ added.push(perm);
260
+ }
261
+ }
262
+
263
+ const alreadyPresent = added.length === 0;
264
+ expect(alreadyPresent).toBe(true);
265
+ });
266
+
267
+ it('should return error when write fails', () => {
268
+ // Simulate what the function returns on write error
269
+ const result = { added: [], alreadyPresent: false, error: 'Failed to update OpenCode config: EACCES' };
270
+
271
+ expect(result.error).toBeDefined();
272
+ expect(result.error).toContain('Failed to update');
273
+ expect(result.added).toHaveLength(0);
274
+ });
275
+ });
276
+ });
@@ -51,10 +51,17 @@ function detectGitUsername(): string {
51
51
  }
52
52
  }
53
53
 
54
+ /**
55
+ * The opencode-skills plugin name for OpenCode
56
+ * This plugin enables Claude Code-style skills in OpenCode
57
+ * @see https://github.com/malhashemi/opencode-skills
58
+ */
59
+ const OPENCODE_SKILLS_PLUGIN = 'opencode-skills';
60
+
54
61
  /**
55
62
  * Configure AI tool permissions for droid
56
63
  */
57
- export function configureAIToolPermissions(aiTool: AITool): { added: string[]; alreadyPresent: boolean } {
64
+ export function configureAIToolPermissions(aiTool: AITool): { added: string[]; alreadyPresent: boolean; error?: string } {
58
65
  const added: string[] = [];
59
66
 
60
67
  if (aiTool === AITool.ClaudeCode) {
@@ -72,7 +79,7 @@ export function configureAIToolPermissions(aiTool: AITool): { added: string[]; a
72
79
  try {
73
80
  settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
74
81
  } catch {
75
- // Invalid JSON, start fresh
82
+ console.warn(chalk.yellow('⚠ Claude Code settings.json appears corrupted, resetting permissions'));
76
83
  settings = {};
77
84
  }
78
85
  }
@@ -95,13 +102,63 @@ export function configureAIToolPermissions(aiTool: AITool): { added: string[]; a
95
102
 
96
103
  // Save if we added anything
97
104
  if (added.length > 0) {
98
- writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
105
+ try {
106
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
107
+ } catch (e) {
108
+ const message = e instanceof Error ? e.message : 'Unknown error';
109
+ return { added: [], alreadyPresent: false, error: `Failed to update Claude Code settings: ${message}` };
110
+ }
111
+ }
112
+
113
+ return { added, alreadyPresent: added.length === 0 };
114
+ }
115
+
116
+ if (aiTool === AITool.OpenCode) {
117
+ // OpenCode uses opencode.json for config
118
+ // Check global config location: ~/.config/opencode/opencode.json
119
+ const globalConfigDir = join(homedir(), '.config', 'opencode');
120
+ const globalConfigPath = join(globalConfigDir, 'opencode.json');
121
+
122
+ // Ensure config directory exists
123
+ if (!existsSync(globalConfigDir)) {
124
+ mkdirSync(globalConfigDir, { recursive: true });
125
+ }
126
+
127
+ // Load or create config
128
+ let config: { plugin?: string[] } = {};
129
+ if (existsSync(globalConfigPath)) {
130
+ try {
131
+ config = JSON.parse(readFileSync(globalConfigPath, 'utf-8'));
132
+ } catch {
133
+ console.warn(chalk.yellow('⚠ OpenCode config appears corrupted, resetting'));
134
+ config = {};
135
+ }
136
+ }
137
+
138
+ // Ensure plugin array exists
139
+ if (!Array.isArray(config.plugin)) {
140
+ config.plugin = [];
141
+ }
142
+
143
+ // Add opencode-skills plugin if not present
144
+ if (!config.plugin.includes(OPENCODE_SKILLS_PLUGIN)) {
145
+ config.plugin.push(OPENCODE_SKILLS_PLUGIN);
146
+ added.push(OPENCODE_SKILLS_PLUGIN);
147
+ }
148
+
149
+ // Save if we added anything
150
+ if (added.length > 0) {
151
+ try {
152
+ writeFileSync(globalConfigPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
153
+ } catch (e) {
154
+ const message = e instanceof Error ? e.message : 'Unknown error';
155
+ return { added: [], alreadyPresent: false, error: `Failed to update OpenCode config: ${message}` };
156
+ }
99
157
  }
100
158
 
101
159
  return { added, alreadyPresent: added.length === 0 };
102
160
  }
103
161
 
104
- // OpenCode - TODO: implement when we know the settings path
105
162
  return { added: [], alreadyPresent: true };
106
163
  }
107
164
 
@@ -214,12 +271,25 @@ export async function setupCommand(): Promise<void> {
214
271
 
215
272
  console.log(chalk.green('\n✓ Config saved to ~/.droid/config.yaml'));
216
273
 
217
- // Configure AI tool permissions
218
- const { added, alreadyPresent } = configureAIToolPermissions(answers.ai_tool);
219
- if (added.length > 0) {
220
- console.log(chalk.green(`✓ Added droid permissions to ${answers.ai_tool} settings`));
221
- } else if (alreadyPresent) {
222
- console.log(chalk.gray(` Droid permissions already configured in ${answers.ai_tool}`));
274
+ // Configure AI tool permissions/plugins
275
+ const { added, alreadyPresent, error } = configureAIToolPermissions(answers.ai_tool);
276
+ if (error) {
277
+ console.log(chalk.red(`✗ ${error}`));
278
+ console.log(chalk.yellow(' You may need to manually configure your AI tool'));
279
+ } else if (answers.ai_tool === AITool.ClaudeCode) {
280
+ if (added.length > 0) {
281
+ console.log(chalk.green(`✓ Added droid permissions to Claude Code settings`));
282
+ } else if (alreadyPresent) {
283
+ console.log(chalk.gray(` Droid permissions already configured in Claude Code`));
284
+ }
285
+ } else if (answers.ai_tool === AITool.OpenCode) {
286
+ if (added.length > 0) {
287
+ console.log(chalk.green(`✓ Added opencode-skills plugin to OpenCode config`));
288
+ console.log(chalk.gray(` This enables Claude Code-style skills in OpenCode`));
289
+ console.log(chalk.gray(` Restart OpenCode to activate the plugin`));
290
+ } else if (alreadyPresent) {
291
+ console.log(chalk.gray(` opencode-skills plugin already configured in OpenCode`));
292
+ }
223
293
  }
224
294
 
225
295
  console.log(chalk.gray('\nRun `droid skills` to browse and install skills.'));