@orderful/droid 0.7.0 → 0.9.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/CHANGELOG.md +57 -0
- package/README.md +80 -89
- package/assets/droid+claude.png +0 -0
- package/dist/commands/setup.d.ts +1 -0
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +77 -9
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/tui.d.ts.map +1 -1
- package/dist/commands/tui.js +111 -70
- package/dist/commands/tui.js.map +1 -1
- package/dist/lib/agents.d.ts +19 -4
- package/dist/lib/agents.d.ts.map +1 -1
- package/dist/lib/agents.js +121 -42
- package/dist/lib/agents.js.map +1 -1
- package/dist/lib/skills.d.ts.map +1 -1
- package/dist/lib/skills.js +56 -1
- package/dist/lib/skills.js.map +1 -1
- package/dist/lib/types.d.ts +2 -0
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/skills/brain/SKILL.md +16 -16
- package/dist/skills/brain/SKILL.yaml +4 -6
- package/dist/skills/brain/commands/brain.md +12 -5
- package/dist/skills/brain/commands/scratchpad.md +52 -0
- package/dist/skills/brain/references/workflows.md +14 -4
- package/dist/skills/brain-obsidian/SKILL.md +1 -4
- package/dist/skills/brain-obsidian/SKILL.yaml +1 -4
- package/dist/skills/code-review/SKILL.md +54 -0
- package/dist/skills/code-review/SKILL.yaml +19 -0
- package/dist/skills/code-review/agents/edi-standards-reviewer/AGENT.md +39 -0
- package/dist/skills/code-review/agents/edi-standards-reviewer/AGENT.yaml +14 -0
- package/dist/skills/code-review/agents/error-handling-reviewer/AGENT.md +51 -0
- package/dist/skills/code-review/agents/error-handling-reviewer/AGENT.yaml +14 -0
- package/dist/skills/code-review/agents/test-coverage-analyzer/AGENT.md +53 -0
- package/dist/skills/code-review/agents/test-coverage-analyzer/AGENT.yaml +14 -0
- package/dist/skills/code-review/agents/type-reviewer/AGENT.md +50 -0
- package/dist/skills/code-review/agents/type-reviewer/AGENT.yaml +13 -0
- package/dist/skills/code-review/commands/code-review.md +91 -0
- package/dist/skills/comments/SKILL.md +21 -9
- package/dist/skills/comments/SKILL.yaml +2 -5
- package/dist/skills/comments/commands/comments.md +1 -1
- package/dist/skills/project/SKILL.md +10 -13
- package/dist/skills/project/SKILL.yaml +2 -7
- package/dist/skills/project/commands/project.md +9 -4
- package/dist/skills/project/references/creating.md +9 -4
- package/dist/skills/project/references/loading.md +11 -5
- package/package.json +1 -1
- package/src/commands/setup.test.ts +276 -0
- package/src/commands/setup.ts +80 -10
- package/src/commands/tui.tsx +149 -82
- package/src/lib/agents.ts +134 -44
- package/src/lib/skills.ts +61 -1
- package/src/lib/types.ts +4 -0
- package/src/skills/brain/SKILL.md +16 -16
- package/src/skills/brain/SKILL.yaml +4 -6
- package/src/skills/brain/commands/brain.md +12 -5
- package/src/skills/brain/commands/scratchpad.md +52 -0
- package/src/skills/brain/references/workflows.md +14 -4
- package/src/skills/brain-obsidian/SKILL.md +1 -4
- package/src/skills/brain-obsidian/SKILL.yaml +1 -4
- package/src/skills/code-review/SKILL.md +54 -0
- package/src/skills/code-review/SKILL.yaml +19 -0
- package/src/skills/code-review/agents/edi-standards-reviewer/AGENT.md +39 -0
- package/src/skills/code-review/agents/edi-standards-reviewer/AGENT.yaml +14 -0
- package/src/skills/code-review/agents/error-handling-reviewer/AGENT.md +51 -0
- package/src/skills/code-review/agents/error-handling-reviewer/AGENT.yaml +14 -0
- package/src/skills/code-review/agents/test-coverage-analyzer/AGENT.md +53 -0
- package/src/skills/code-review/agents/test-coverage-analyzer/AGENT.yaml +14 -0
- package/src/skills/code-review/agents/type-reviewer/AGENT.md +50 -0
- package/src/skills/code-review/agents/type-reviewer/AGENT.yaml +13 -0
- package/src/skills/code-review/commands/code-review.md +91 -0
- package/src/skills/comments/SKILL.md +21 -9
- package/src/skills/comments/SKILL.yaml +2 -5
- package/src/skills/comments/commands/comments.md +1 -1
- package/src/skills/project/SKILL.md +10 -13
- package/src/skills/project/SKILL.yaml +2 -7
- package/src/skills/project/commands/project.md +9 -4
- package/src/skills/project/references/creating.md +9 -4
- package/src/skills/project/references/loading.md +11 -5
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: project
|
|
3
|
-
description:
|
|
4
|
-
Manage project context files for persistent AI memory across sessions.
|
|
5
|
-
Load project context before working (/project {name}), update with
|
|
6
|
-
new learnings (/project update), or create new projects (/project create).
|
|
7
|
-
Use when working on multi-session features, refactors, or any work that
|
|
8
|
-
benefits from accumulated context.
|
|
3
|
+
description: "Manage project context files for persistent AI memory across sessions. Load project context before working (/project {name}), update with new learnings (/project update), or create new projects (/project create). Use when working on multi-session features, refactors, or any work that benefits from accumulated context."
|
|
9
4
|
globs:
|
|
10
5
|
- "**/PROJECT.md"
|
|
11
6
|
alwaysApply: false
|
|
@@ -32,31 +27,33 @@ Chat history disappears. Projects persist.
|
|
|
32
27
|
|
|
33
28
|
## Configuration
|
|
34
29
|
|
|
35
|
-
|
|
30
|
+
**IMPORTANT:** Before using any default paths, ALWAYS read `~/.droid/skills/project/overrides.yaml` first. If `projects_dir` is configured there, use that path. Only fall back to defaults if the file doesn't exist or lacks a `projects_dir` setting.
|
|
36
31
|
|
|
37
32
|
| Setting | Default | Description |
|
|
38
33
|
|---------|---------|-------------|
|
|
39
|
-
| `projects_dir` |
|
|
34
|
+
| `projects_dir` | (see below) | Where projects are stored |
|
|
40
35
|
| `preset` | `markdown` | Output format: `markdown` or `obsidian` |
|
|
41
36
|
|
|
42
|
-
Default `projects_dir` by AI tool:
|
|
37
|
+
Default `projects_dir` by AI tool (only if not configured):
|
|
43
38
|
- **claude-code**: `~/.claude/projects`
|
|
44
|
-
- **opencode**: `~/.opencode/projects`
|
|
39
|
+
- **opencode**: `~/.config/opencode/projects`
|
|
45
40
|
|
|
46
41
|
## Commands
|
|
47
42
|
|
|
48
43
|
| Command | Action |
|
|
49
44
|
|---------|--------|
|
|
50
45
|
| `/project` | List and select a project |
|
|
51
|
-
| `/project {keywords}` |
|
|
52
|
-
| `/project create {name}` | Create new project |
|
|
46
|
+
| `/project {keywords}` | **Search** for existing project (fuzzy-match and load) |
|
|
47
|
+
| `/project create {name}` | Create new project (requires `create` keyword) |
|
|
53
48
|
| `/project update` | Update from conversation context |
|
|
54
49
|
|
|
50
|
+
**IMPORTANT:** The default action for `/project {keywords}` is to **SEARCH** for existing projects, NOT create. Only use `/project create {name}` when the user explicitly wants to create a new project.
|
|
51
|
+
|
|
55
52
|
## Loading a Project
|
|
56
53
|
|
|
57
54
|
**Trigger:** `/project {keywords}` or user asks to load/open a project
|
|
58
55
|
|
|
59
|
-
**TLDR:** Fuzzy-match keywords against project folders, read PROJECT.md, summarize context.
|
|
56
|
+
**TLDR:** Search for and load an existing project. Fuzzy-match keywords against project folders, read PROJECT.md, summarize context.
|
|
60
57
|
|
|
61
58
|
Full procedure: `references/loading.md`
|
|
62
59
|
|
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
name: project
|
|
2
|
-
description:
|
|
3
|
-
|
|
4
|
-
Load project context before working (/project {name}), update with
|
|
5
|
-
new learnings (/project update), or create new projects (/project create).
|
|
6
|
-
Use when working on multi-session features, refactors, or any work that
|
|
7
|
-
benefits from accumulated context.
|
|
8
|
-
version: 0.1.0
|
|
2
|
+
description: "Manage project context files for persistent AI memory across sessions. Load project context before working (/project {name}), update with new learnings (/project update), or create new projects (/project create). Use when working on multi-session features, refactors, or any work that benefits from accumulated context."
|
|
3
|
+
version: 0.1.1
|
|
9
4
|
status: beta
|
|
10
5
|
dependencies: []
|
|
11
6
|
provides_output: false
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Manage project context files for persistent AI memory across sessions
|
|
3
|
-
argument-hint: [
|
|
3
|
+
argument-hint: "[{keywords} | update | create {name}]"
|
|
4
4
|
allowed-tools: Read, Write, Edit, Glob, Bash(mkdir:*), Bash(ls:*)
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -12,20 +12,25 @@ Entry point for project context management. See the **project skill** for full b
|
|
|
12
12
|
|
|
13
13
|
$ARGUMENTS
|
|
14
14
|
|
|
15
|
+
## Default Behavior
|
|
16
|
+
|
|
17
|
+
**IMPORTANT:** When given keywords (e.g., `/project audit-log`), the default action is to **SEARCH** for existing projects, NOT create. Only create a project when the `create` keyword is explicitly used.
|
|
18
|
+
|
|
15
19
|
## Usage
|
|
16
20
|
|
|
17
21
|
```
|
|
18
22
|
/project # List and select a project
|
|
19
|
-
/project {keywords} # Fuzzy-match and load
|
|
23
|
+
/project {keywords} # SEARCH: Fuzzy-match and load existing project
|
|
20
24
|
/project update # Update from conversation context
|
|
21
25
|
/project update {name} # Update specific project
|
|
22
26
|
/project create # Create new project interactively
|
|
23
|
-
/project create {name} # Create with name
|
|
27
|
+
/project create {name} # Create with name (requires "create" keyword)
|
|
24
28
|
```
|
|
25
29
|
|
|
26
30
|
## Configuration
|
|
27
31
|
|
|
28
|
-
|
|
32
|
+
**ALWAYS read `~/.droid/skills/project/overrides.yaml` first.** Use configured values if present, only fall back to defaults if missing.
|
|
33
|
+
|
|
29
34
|
- `projects_dir` - Where projects live (default varies by AI tool)
|
|
30
35
|
- `preset` - Template format: `markdown` or `obsidian`
|
|
31
36
|
|
|
@@ -4,21 +4,26 @@
|
|
|
4
4
|
|
|
5
5
|
## Procedure
|
|
6
6
|
|
|
7
|
-
1. **
|
|
7
|
+
1. **Read config first**
|
|
8
|
+
- Read `~/.droid/skills/project/overrides.yaml`
|
|
9
|
+
- Use `projects_dir` if configured, otherwise use default for current AI tool
|
|
10
|
+
- Use `preset` if configured (markdown or obsidian)
|
|
11
|
+
|
|
12
|
+
2. **Get project name**
|
|
8
13
|
- Use provided name, or ask if not provided
|
|
9
14
|
- Convert to kebab-case for folder name
|
|
10
15
|
- Convert to Title Case for display name
|
|
11
16
|
|
|
12
|
-
|
|
17
|
+
3. **Create project folder**
|
|
13
18
|
- Path: `{projects_dir}/{kebab-case-name}/`
|
|
14
19
|
- Verify folder doesn't already exist
|
|
15
20
|
|
|
16
|
-
|
|
21
|
+
4. **Create files from templates** (see `templates.md`)
|
|
17
22
|
- `PROJECT.md` - Main context file
|
|
18
23
|
- `CHANGELOG.md` - Version history
|
|
19
24
|
- Format varies by `preset` config (markdown vs obsidian)
|
|
20
25
|
|
|
21
|
-
|
|
26
|
+
5. **Confirm creation**
|
|
22
27
|
- Show created folder path
|
|
23
28
|
- Offer to help fill in sections (Overview, Goals, Technical Details)
|
|
24
29
|
|
|
@@ -2,25 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
**Trigger:** `/project {keywords}` or user asks to load/open a project
|
|
4
4
|
|
|
5
|
+
**IMPORTANT:** This is a SEARCH operation. When keywords are provided, search for existing projects - do NOT create a new project. Creating requires the explicit `/project create {name}` command.
|
|
6
|
+
|
|
5
7
|
## Procedure
|
|
6
8
|
|
|
7
|
-
1. **
|
|
9
|
+
1. **Read config first**
|
|
10
|
+
- Read `~/.droid/skills/project/overrides.yaml`
|
|
11
|
+
- Use `projects_dir` if configured, otherwise use default for current AI tool
|
|
12
|
+
|
|
13
|
+
2. **List projects** in configured `projects_dir`
|
|
8
14
|
- Each subfolder with a `PROJECT.md` is a project
|
|
9
15
|
|
|
10
|
-
|
|
16
|
+
3. **If no name provided:**
|
|
11
17
|
- Use AskUserQuestion to present available projects
|
|
12
18
|
- Let user select which to load
|
|
13
19
|
|
|
14
|
-
|
|
20
|
+
4. **If name/keywords provided:**
|
|
15
21
|
- Parse as space-separated keywords (e.g., "transaction templates" → `["transaction", "templates"]`)
|
|
16
22
|
- Find folders where name contains ALL keywords (case-insensitive, hyphens as word separators)
|
|
17
23
|
|
|
18
|
-
|
|
24
|
+
5. **Based on matches:**
|
|
19
25
|
- **No matches**: List available projects, ask user to select
|
|
20
26
|
- **One match**: Read `{folder}/PROJECT.md`
|
|
21
27
|
- **Multiple matches**: Use AskUserQuestion to select from matches
|
|
22
28
|
|
|
23
|
-
|
|
29
|
+
6. **After loading:**
|
|
24
30
|
- Confirm which project was loaded
|
|
25
31
|
- Summarize key context (2-3 sentences)
|
|
26
32
|
- Use project contents for all subsequent work in the session
|
package/package.json
CHANGED
|
@@ -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
|
+
});
|
package/src/commands/setup.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 (
|
|
220
|
-
console.log(chalk.
|
|
221
|
-
|
|
222
|
-
|
|
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.'));
|