@orderful/droid 0.18.0 → 0.20.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 +28 -0
- package/dist/bin/droid.js +189 -82
- package/dist/index.js +175 -72
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/migrations.d.ts +30 -0
- package/dist/lib/migrations.d.ts.map +1 -0
- package/dist/lib/skill-config.d.ts.map +1 -1
- package/dist/lib/skills.d.ts.map +1 -1
- package/dist/lib/types.d.ts +10 -0
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/tools/brain/TOOL.yaml +1 -1
- package/dist/tools/coach/TOOL.yaml +1 -1
- package/dist/tools/codex/TOOL.yaml +32 -0
- package/dist/tools/codex/commands/codex.md +70 -0
- package/dist/tools/codex/skills/droid-codex/SKILL.md +266 -0
- package/dist/tools/codex/skills/droid-codex/references/creating.md +150 -0
- package/dist/tools/codex/skills/droid-codex/references/decisions.md +128 -0
- package/dist/tools/codex/skills/droid-codex/references/loading.md +163 -0
- package/dist/tools/codex/skills/droid-codex/references/topics.md +213 -0
- package/dist/tools/comments/TOOL.yaml +1 -1
- package/dist/tools/droid/skills/droid/SKILL.md +8 -0
- package/dist/tools/project/TOOL.yaml +1 -1
- package/package.json +1 -1
- package/src/lib/config.ts +13 -2
- package/src/lib/migrations.test.ts +163 -0
- package/src/lib/migrations.ts +182 -0
- package/src/lib/skill-config.ts +3 -1
- package/src/lib/skills.ts +10 -1
- package/src/lib/types.ts +11 -0
- package/src/tools/brain/TOOL.yaml +1 -1
- package/src/tools/coach/TOOL.yaml +1 -1
- package/src/tools/codex/TOOL.yaml +32 -0
- package/src/tools/codex/commands/codex.md +70 -0
- package/src/tools/codex/skills/droid-codex/SKILL.md +266 -0
- package/src/tools/codex/skills/droid-codex/references/creating.md +150 -0
- package/src/tools/codex/skills/droid-codex/references/decisions.md +128 -0
- package/src/tools/codex/skills/droid-codex/references/loading.md +163 -0
- package/src/tools/codex/skills/droid-codex/references/topics.md +213 -0
- package/src/tools/comments/TOOL.yaml +1 -1
- package/src/tools/droid/skills/droid/SKILL.md +8 -0
- package/src/tools/project/TOOL.yaml +1 -1
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# Adding Topics
|
|
2
|
+
|
|
3
|
+
Detailed procedure for capturing explored codebase areas as institutional knowledge.
|
|
4
|
+
|
|
5
|
+
## When to Use
|
|
6
|
+
|
|
7
|
+
- After a deep exploration of a codebase area
|
|
8
|
+
- User asks to save/capture exploration
|
|
9
|
+
- `/codex add topic {name}` command
|
|
10
|
+
- Converting ad-hoc research into persistent knowledge
|
|
11
|
+
|
|
12
|
+
## The Value
|
|
13
|
+
|
|
14
|
+
Topics turn ephemeral exploration into institutional memory:
|
|
15
|
+
- Next time someone needs to understand this area, context is ready
|
|
16
|
+
- AI can load it instantly instead of re-exploring
|
|
17
|
+
- Freshness tracking ensures it stays current
|
|
18
|
+
|
|
19
|
+
## Procedure
|
|
20
|
+
|
|
21
|
+
### 1. Determine Topic Name
|
|
22
|
+
|
|
23
|
+
From user input or infer from exploration:
|
|
24
|
+
- Use kebab-case: `organization-hierarchy`, `webhook-processing`
|
|
25
|
+
- Be specific but concise
|
|
26
|
+
|
|
27
|
+
### 2. Gather Content
|
|
28
|
+
|
|
29
|
+
From the current conversation/exploration, extract:
|
|
30
|
+
- **Overview:** What is this area of the codebase?
|
|
31
|
+
- **Key Concepts:** Important terms, patterns, relationships
|
|
32
|
+
- **Architecture:** How components fit together
|
|
33
|
+
- **Key Files:** Important files and their purposes
|
|
34
|
+
- **Common Patterns:** Recurring patterns in this area
|
|
35
|
+
- **Gotchas:** Things to watch out for
|
|
36
|
+
|
|
37
|
+
### 3. Determine Metadata
|
|
38
|
+
|
|
39
|
+
Ask user or infer:
|
|
40
|
+
- **Confidence:** How thorough was the exploration?
|
|
41
|
+
- `high` - Comprehensive, covered most aspects
|
|
42
|
+
- `medium` - Good coverage but may have gaps
|
|
43
|
+
- `low` - Quick exploration, definitely incomplete
|
|
44
|
+
- **Codebase paths:** Which directories/files were explored?
|
|
45
|
+
|
|
46
|
+
### 4. Read Template
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
cat {codex_repo}/templates/TOPIC.md
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### 5. Create Topic File
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# Create the file
|
|
56
|
+
touch {codex_repo}/topics/{name}.md
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Fill in content:
|
|
60
|
+
|
|
61
|
+
```markdown
|
|
62
|
+
---
|
|
63
|
+
title: {Topic Title}
|
|
64
|
+
type: topic
|
|
65
|
+
created: {today}
|
|
66
|
+
updated: {today}
|
|
67
|
+
confidence: {high|medium|low}
|
|
68
|
+
source: exploration
|
|
69
|
+
codebase_paths:
|
|
70
|
+
- {path/explored/1}
|
|
71
|
+
- {path/explored/2}
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
# {Topic Title}
|
|
75
|
+
|
|
76
|
+
## Overview
|
|
77
|
+
|
|
78
|
+
{Brief description of this area of the codebase}
|
|
79
|
+
|
|
80
|
+
## Key Concepts
|
|
81
|
+
|
|
82
|
+
### {Concept 1}
|
|
83
|
+
|
|
84
|
+
{Explanation}
|
|
85
|
+
|
|
86
|
+
### {Concept 2}
|
|
87
|
+
|
|
88
|
+
{Explanation}
|
|
89
|
+
|
|
90
|
+
## Architecture
|
|
91
|
+
|
|
92
|
+
{How components fit together}
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
{Diagram or structure if helpful}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Key Files
|
|
99
|
+
|
|
100
|
+
| File | Purpose |
|
|
101
|
+
|------|---------|
|
|
102
|
+
| `{path/to/file}` | {What it does} |
|
|
103
|
+
|
|
104
|
+
## Common Patterns
|
|
105
|
+
|
|
106
|
+
{Patterns used in this area}
|
|
107
|
+
|
|
108
|
+
## Gotchas
|
|
109
|
+
|
|
110
|
+
- {Thing to watch out for}
|
|
111
|
+
- {Another gotcha}
|
|
112
|
+
|
|
113
|
+
## Related
|
|
114
|
+
|
|
115
|
+
- {Links to related topics, PRDs, or external docs}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### 6. Create Branch and PR
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
cd {codex_repo}
|
|
122
|
+
git checkout -b codex/topic-{name}
|
|
123
|
+
git add topics/{name}.md
|
|
124
|
+
git commit -F - <<EOF
|
|
125
|
+
topic: add {name}
|
|
126
|
+
EOF
|
|
127
|
+
git push -u origin codex/topic-{name}
|
|
128
|
+
gh pr create --title "Topic: {name}" --body "New topic from codebase exploration"
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### 7. Confirm to User
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
✅ Added topic: {name}
|
|
135
|
+
|
|
136
|
+
📚 {codex_repo}/topics/{name}.md
|
|
137
|
+
Confidence: {confidence}
|
|
138
|
+
Paths covered: {codebase_paths}
|
|
139
|
+
|
|
140
|
+
PR: {pr_url}
|
|
141
|
+
(Auto-merges for new files)
|
|
142
|
+
|
|
143
|
+
Next time someone asks about {topic}, I can load this context instantly.
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Example
|
|
147
|
+
|
|
148
|
+
**User:** (after exploring organization hierarchy) `/codex add topic organization-hierarchy`
|
|
149
|
+
|
|
150
|
+
**Result:**
|
|
151
|
+
|
|
152
|
+
```markdown
|
|
153
|
+
---
|
|
154
|
+
title: Organization Hierarchy
|
|
155
|
+
type: topic
|
|
156
|
+
created: 2026-01-07
|
|
157
|
+
updated: 2026-01-07
|
|
158
|
+
confidence: high
|
|
159
|
+
source: exploration
|
|
160
|
+
codebase_paths:
|
|
161
|
+
- apps/platform-api/src/modules/organization/
|
|
162
|
+
- libs/shared/src/types/organization.ts
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
# Organization Hierarchy
|
|
166
|
+
|
|
167
|
+
## Overview
|
|
168
|
+
|
|
169
|
+
Orderful's multi-tenant organization structure with parent-child relationships, enabling enterprise customers to manage multiple business units under a single account.
|
|
170
|
+
|
|
171
|
+
## Key Concepts
|
|
172
|
+
|
|
173
|
+
### Organization
|
|
174
|
+
|
|
175
|
+
The top-level entity. Has settings, billing, users.
|
|
176
|
+
|
|
177
|
+
### Child Organizations
|
|
178
|
+
|
|
179
|
+
Organizations can have children, creating a hierarchy. Children inherit some settings from parents.
|
|
180
|
+
|
|
181
|
+
### Organization Context
|
|
182
|
+
|
|
183
|
+
Request-scoped context determining which org's data to access.
|
|
184
|
+
|
|
185
|
+
## Architecture
|
|
186
|
+
|
|
187
|
+
```
|
|
188
|
+
Organization (parent)
|
|
189
|
+
├── Organization (child 1)
|
|
190
|
+
│ └── Organization (grandchild)
|
|
191
|
+
└── Organization (child 2)
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Key Files
|
|
195
|
+
|
|
196
|
+
| File | Purpose |
|
|
197
|
+
|------|---------|
|
|
198
|
+
| `organization.entity.ts` | TypeORM entity with parent/child relations |
|
|
199
|
+
| `organization.service.ts` | Business logic for org operations |
|
|
200
|
+
| `organization-context.middleware.ts` | Sets org context from request |
|
|
201
|
+
|
|
202
|
+
## Common Patterns
|
|
203
|
+
|
|
204
|
+
- Always check org context before data access
|
|
205
|
+
- Use `organizationId` foreign key on tenant-scoped entities
|
|
206
|
+
- Hierarchy queries use recursive CTEs
|
|
207
|
+
|
|
208
|
+
## Gotchas
|
|
209
|
+
|
|
210
|
+
- Child orgs don't automatically inherit all parent settings
|
|
211
|
+
- Deleting a parent doesn't cascade to children (soft delete)
|
|
212
|
+
- API keys are scoped to single org, not hierarchy
|
|
213
|
+
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
name: comments
|
|
2
2
|
description: "Enable inline conversations using @droid/@user markers. Tag @droid to ask the AI, AI responds with @{your-name}. Use /comments check to address markers, /comments cleanup to remove resolved threads. Ideal for code review notes and async collaboration."
|
|
3
|
-
version: 0.2.
|
|
3
|
+
version: 0.2.6
|
|
4
4
|
status: beta
|
|
5
5
|
|
|
6
6
|
includes:
|
|
@@ -147,6 +147,7 @@ for f in $(npm root -g)/@orderful/droid/dist/tools/*/TOOL.yaml; do echo "---"; c
|
|
|
147
147
|
| **brain** | Collaborative scratchpad for planning and research |
|
|
148
148
|
| **coach** | Learning-mode AI - scaffolds don't implement, questions don't fix |
|
|
149
149
|
| **code-review** | Code review with specialized agents and confidence scoring |
|
|
150
|
+
| **codex** | Shared organizational knowledge - PRDs, tech designs, patterns, topics |
|
|
150
151
|
| **comments** | Inline conversations using @droid/@user markers |
|
|
151
152
|
| **project** | Project context for persistent AI memory across sessions |
|
|
152
153
|
|
|
@@ -170,6 +171,12 @@ Comprehensive code review using specialized agents. Reviews PRs, staged changes,
|
|
|
170
171
|
**Commands:** `/code-review`
|
|
171
172
|
**Agents:** edi-standards-reviewer, error-handling-reviewer, test-coverage-analyzer, type-reviewer
|
|
172
173
|
|
|
174
|
+
#### codex
|
|
175
|
+
Shared organizational knowledge - PRDs, tech designs, patterns, and explored topics. Load project context, search across the codex, capture decisions during implementation.
|
|
176
|
+
|
|
177
|
+
**Commands:** `/codex`, `/codex projects`, `/codex search`, `/codex decision`, `/codex add topic`
|
|
178
|
+
**Requires:** gh CLI, orderful-codex repo cloned locally
|
|
179
|
+
|
|
173
180
|
#### comments
|
|
174
181
|
Enable inline conversations using @droid/@user markers. Tag @droid to ask the AI, AI responds with @{your-name}. Ideal for code review notes and async collaboration.
|
|
175
182
|
|
|
@@ -215,6 +222,7 @@ Run `droid` to see all available tools or install new ones.
|
|
|
215
222
|
| brain | Planning and research scratchpad with `/brain` commands |
|
|
216
223
|
| coach | Learning-mode AI that asks questions instead of giving answers |
|
|
217
224
|
| code-review | PR review with specialized agents |
|
|
225
|
+
| codex | Shared knowledge - PRDs, tech designs, patterns, topics |
|
|
218
226
|
| comments | Inline @droid/@user conversations in any file |
|
|
219
227
|
| project | Persistent project context across sessions |
|
|
220
228
|
|
package/package.json
CHANGED
package/src/lib/config.ts
CHANGED
|
@@ -160,11 +160,21 @@ export function getConfigDir(): string {
|
|
|
160
160
|
return CONFIG_DIR;
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Normalize skill name for config paths.
|
|
165
|
+
* Strips 'droid-' prefix since that was added as a workaround for Claude Code bug.
|
|
166
|
+
* Config paths should stay intuitive (e.g., ~/.droid/skills/brain/ not ~/.droid/skills/droid-brain/)
|
|
167
|
+
*/
|
|
168
|
+
function normalizeSkillNameForConfig(skillName: string): string {
|
|
169
|
+
return skillName.replace(/^droid-/, '');
|
|
170
|
+
}
|
|
171
|
+
|
|
163
172
|
/**
|
|
164
173
|
* Get skill-specific overrides path
|
|
165
174
|
*/
|
|
166
175
|
export function getSkillOverridesPath(skillName: string): string {
|
|
167
|
-
|
|
176
|
+
const normalizedName = normalizeSkillNameForConfig(skillName);
|
|
177
|
+
return join(CONFIG_DIR, 'skills', normalizedName, 'overrides.yaml');
|
|
168
178
|
}
|
|
169
179
|
|
|
170
180
|
/**
|
|
@@ -189,8 +199,9 @@ export function loadSkillOverrides(skillName: string): SkillOverrides {
|
|
|
189
199
|
* Save skill-specific overrides
|
|
190
200
|
*/
|
|
191
201
|
export function saveSkillOverrides(skillName: string, overrides: SkillOverrides): void {
|
|
202
|
+
const normalizedName = normalizeSkillNameForConfig(skillName);
|
|
192
203
|
const overridesPath = getSkillOverridesPath(skillName);
|
|
193
|
-
const skillDir = join(CONFIG_DIR, 'skills',
|
|
204
|
+
const skillDir = join(CONFIG_DIR, 'skills', normalizedName);
|
|
194
205
|
|
|
195
206
|
if (!existsSync(skillDir)) {
|
|
196
207
|
mkdirSync(skillDir, { recursive: true });
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
|
|
2
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync, readFileSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { tmpdir } from 'os';
|
|
5
|
+
import { getToolMigrations } from './migrations';
|
|
6
|
+
|
|
7
|
+
describe('migrations', () => {
|
|
8
|
+
let testDir: string;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
testDir = join(tmpdir(), `droid-migration-test-${Date.now()}`);
|
|
12
|
+
mkdirSync(testDir, { recursive: true });
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
if (existsSync(testDir)) {
|
|
17
|
+
rmSync(testDir, { recursive: true });
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe('getToolMigrations', () => {
|
|
22
|
+
it('should return migrations for known tools', () => {
|
|
23
|
+
const brainMigrations = getToolMigrations('brain');
|
|
24
|
+
expect(brainMigrations.length).toBeGreaterThan(0);
|
|
25
|
+
expect(brainMigrations[0].version).toBe('0.2.3');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should return empty array for unknown tools', () => {
|
|
29
|
+
const migrations = getToolMigrations('unknown-tool');
|
|
30
|
+
expect(migrations).toEqual([]);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should have migrations for all renamed tools', () => {
|
|
34
|
+
const tools = ['brain', 'comments', 'project', 'coach'];
|
|
35
|
+
for (const tool of tools) {
|
|
36
|
+
const migrations = getToolMigrations(tool);
|
|
37
|
+
expect(migrations.length).toBeGreaterThan(0);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('config directory migration', () => {
|
|
43
|
+
it('should move old prefixed directory to unprefixed', () => {
|
|
44
|
+
// Setup: create old prefixed directory with config
|
|
45
|
+
const skillsDir = join(testDir, 'skills');
|
|
46
|
+
const oldDir = join(skillsDir, 'droid-brain');
|
|
47
|
+
const newDir = join(skillsDir, 'brain');
|
|
48
|
+
|
|
49
|
+
mkdirSync(oldDir, { recursive: true });
|
|
50
|
+
writeFileSync(join(oldDir, 'overrides.yaml'), 'brain_dir: /test/path\n');
|
|
51
|
+
|
|
52
|
+
// Get the migration and run it
|
|
53
|
+
const migrations = getToolMigrations('brain');
|
|
54
|
+
const migration = migrations[0];
|
|
55
|
+
migration.up(testDir);
|
|
56
|
+
|
|
57
|
+
// Verify
|
|
58
|
+
expect(existsSync(oldDir)).toBe(false);
|
|
59
|
+
expect(existsSync(newDir)).toBe(true);
|
|
60
|
+
expect(readFileSync(join(newDir, 'overrides.yaml'), 'utf-8')).toBe('brain_dir: /test/path\n');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should be idempotent - no error if old directory already moved', () => {
|
|
64
|
+
// Setup: only new directory exists (already migrated)
|
|
65
|
+
const skillsDir = join(testDir, 'skills');
|
|
66
|
+
const newDir = join(skillsDir, 'brain');
|
|
67
|
+
|
|
68
|
+
mkdirSync(newDir, { recursive: true });
|
|
69
|
+
writeFileSync(join(newDir, 'overrides.yaml'), 'brain_dir: /test/path\n');
|
|
70
|
+
|
|
71
|
+
// Get the migration and run it - should not throw
|
|
72
|
+
const migrations = getToolMigrations('brain');
|
|
73
|
+
const migration = migrations[0];
|
|
74
|
+
|
|
75
|
+
expect(() => migration.up(testDir)).not.toThrow();
|
|
76
|
+
expect(existsSync(newDir)).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should handle collision by removing old directory', () => {
|
|
80
|
+
// Setup: both old and new directories exist
|
|
81
|
+
const skillsDir = join(testDir, 'skills');
|
|
82
|
+
const oldDir = join(skillsDir, 'droid-brain');
|
|
83
|
+
const newDir = join(skillsDir, 'brain');
|
|
84
|
+
|
|
85
|
+
mkdirSync(oldDir, { recursive: true });
|
|
86
|
+
mkdirSync(newDir, { recursive: true });
|
|
87
|
+
writeFileSync(join(oldDir, 'overrides.yaml'), 'old: true\n');
|
|
88
|
+
writeFileSync(join(newDir, 'overrides.yaml'), 'new: true\n');
|
|
89
|
+
|
|
90
|
+
// Run migration
|
|
91
|
+
const migrations = getToolMigrations('brain');
|
|
92
|
+
const migration = migrations[0];
|
|
93
|
+
migration.up(testDir);
|
|
94
|
+
|
|
95
|
+
// Old should be removed, new should be preserved
|
|
96
|
+
expect(existsSync(oldDir)).toBe(false);
|
|
97
|
+
expect(existsSync(newDir)).toBe(true);
|
|
98
|
+
expect(readFileSync(join(newDir, 'overrides.yaml'), 'utf-8')).toBe('new: true\n');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should do nothing if neither directory exists', () => {
|
|
102
|
+
// Setup: empty skills directory
|
|
103
|
+
const skillsDir = join(testDir, 'skills');
|
|
104
|
+
mkdirSync(skillsDir, { recursive: true });
|
|
105
|
+
|
|
106
|
+
// Run migration - should not throw
|
|
107
|
+
const migrations = getToolMigrations('brain');
|
|
108
|
+
const migration = migrations[0];
|
|
109
|
+
|
|
110
|
+
expect(() => migration.up(testDir)).not.toThrow();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should create parent directory if needed', () => {
|
|
114
|
+
// Setup: old directory exists but skills/ parent doesn't exist for new
|
|
115
|
+
const oldDir = join(testDir, 'skills', 'droid-brain');
|
|
116
|
+
mkdirSync(oldDir, { recursive: true });
|
|
117
|
+
writeFileSync(join(oldDir, 'overrides.yaml'), 'test: true\n');
|
|
118
|
+
|
|
119
|
+
// Run migration
|
|
120
|
+
const migrations = getToolMigrations('brain');
|
|
121
|
+
const migration = migrations[0];
|
|
122
|
+
migration.up(testDir);
|
|
123
|
+
|
|
124
|
+
// Should have moved successfully
|
|
125
|
+
const newDir = join(testDir, 'skills', 'brain');
|
|
126
|
+
expect(existsSync(newDir)).toBe(true);
|
|
127
|
+
expect(existsSync(oldDir)).toBe(false);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe('migration versions', () => {
|
|
132
|
+
it('brain migration should be version 0.2.3', () => {
|
|
133
|
+
const migrations = getToolMigrations('brain');
|
|
134
|
+
expect(migrations[0].version).toBe('0.2.3');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('comments migration should be version 0.2.6', () => {
|
|
138
|
+
const migrations = getToolMigrations('comments');
|
|
139
|
+
expect(migrations[0].version).toBe('0.2.6');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('project migration should be version 0.1.5', () => {
|
|
143
|
+
const migrations = getToolMigrations('project');
|
|
144
|
+
expect(migrations[0].version).toBe('0.1.5');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('coach migration should be version 0.1.3', () => {
|
|
148
|
+
const migrations = getToolMigrations('coach');
|
|
149
|
+
expect(migrations[0].version).toBe('0.1.3');
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe('migration descriptions', () => {
|
|
154
|
+
it('should have descriptive migration descriptions', () => {
|
|
155
|
+
const tools = ['brain', 'comments', 'project', 'coach'];
|
|
156
|
+
for (const tool of tools) {
|
|
157
|
+
const migrations = getToolMigrations(tool);
|
|
158
|
+
expect(migrations[0].description).toContain('config');
|
|
159
|
+
expect(migrations[0].description).toContain('unprefixed');
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
});
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { existsSync, appendFileSync, mkdirSync, renameSync, rmSync } from 'fs';
|
|
2
|
+
import { join, dirname } from 'path';
|
|
3
|
+
import { loadConfig, saveConfig, getConfigDir } from './config';
|
|
4
|
+
import { type Migration } from './types';
|
|
5
|
+
import { compareSemver } from './version';
|
|
6
|
+
|
|
7
|
+
const MIGRATIONS_LOG_FILE = '.migrations.log';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Get the path to the migrations log file
|
|
11
|
+
*/
|
|
12
|
+
function getMigrationsLogPath(): string {
|
|
13
|
+
return join(getConfigDir(), MIGRATIONS_LOG_FILE);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Log a migration event to the hidden log file
|
|
18
|
+
*/
|
|
19
|
+
function logMigration(
|
|
20
|
+
toolName: string,
|
|
21
|
+
fromVersion: string,
|
|
22
|
+
toVersion: string,
|
|
23
|
+
status: 'OK' | 'FAILED',
|
|
24
|
+
error?: string
|
|
25
|
+
): void {
|
|
26
|
+
const timestamp = new Date().toISOString();
|
|
27
|
+
const logEntry = error
|
|
28
|
+
? `${timestamp} ${toolName} ${fromVersion} → ${toVersion} ${status}: ${error}\n`
|
|
29
|
+
: `${timestamp} ${toolName} ${fromVersion} → ${toVersion} ${status}\n`;
|
|
30
|
+
|
|
31
|
+
const logPath = getMigrationsLogPath();
|
|
32
|
+
const logDir = dirname(logPath);
|
|
33
|
+
|
|
34
|
+
if (!existsSync(logDir)) {
|
|
35
|
+
mkdirSync(logDir, { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
appendFileSync(logPath, logEntry);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ============================================================================
|
|
42
|
+
// Tool Migrations Registry
|
|
43
|
+
// Define migrations here, keyed by tool name
|
|
44
|
+
// ============================================================================
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Migration: Move droid-prefixed config directories to unprefixed names
|
|
48
|
+
* e.g., ~/.droid/skills/droid-brain/ → ~/.droid/skills/brain/
|
|
49
|
+
*
|
|
50
|
+
* This fixes a bug where config was saved to prefixed paths but SKILL.md
|
|
51
|
+
* tells Claude to read from unprefixed paths.
|
|
52
|
+
*/
|
|
53
|
+
function createConfigDirMigration(skillName: string, version: string): Migration {
|
|
54
|
+
const unprefixedName = skillName.replace(/^droid-/, '');
|
|
55
|
+
return {
|
|
56
|
+
version,
|
|
57
|
+
description: `Move ${skillName} config to unprefixed location`,
|
|
58
|
+
up: (configDir: string) => {
|
|
59
|
+
const oldDir = join(configDir, 'skills', skillName);
|
|
60
|
+
const newDir = join(configDir, 'skills', unprefixedName);
|
|
61
|
+
|
|
62
|
+
// Only migrate if old exists and new doesn't
|
|
63
|
+
if (existsSync(oldDir) && !existsSync(newDir)) {
|
|
64
|
+
// Ensure parent directory exists
|
|
65
|
+
const parentDir = dirname(newDir);
|
|
66
|
+
if (!existsSync(parentDir)) {
|
|
67
|
+
mkdirSync(parentDir, { recursive: true });
|
|
68
|
+
}
|
|
69
|
+
renameSync(oldDir, newDir);
|
|
70
|
+
} else if (existsSync(oldDir) && existsSync(newDir)) {
|
|
71
|
+
// Both exist - remove the old one (new takes precedence)
|
|
72
|
+
rmSync(oldDir, { recursive: true });
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Registry of all tool migrations
|
|
80
|
+
* Key: tool name (e.g., "brain", "comments")
|
|
81
|
+
* Value: array of migrations sorted by version
|
|
82
|
+
*
|
|
83
|
+
* Migration versions are set to the next version after each tool's current version:
|
|
84
|
+
* - brain: 0.2.2 → 0.2.3
|
|
85
|
+
* - comments: 0.2.5 → 0.2.6
|
|
86
|
+
* - project: 0.1.4 → 0.1.5
|
|
87
|
+
* - coach: 0.1.2 → 0.1.3
|
|
88
|
+
*/
|
|
89
|
+
const TOOL_MIGRATIONS: Record<string, Migration[]> = {
|
|
90
|
+
brain: [createConfigDirMigration('droid-brain', '0.2.3')],
|
|
91
|
+
comments: [createConfigDirMigration('droid-comments', '0.2.6')],
|
|
92
|
+
project: [createConfigDirMigration('droid-project', '0.1.5')],
|
|
93
|
+
coach: [createConfigDirMigration('droid-coach', '0.1.3')],
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get migrations for a tool
|
|
98
|
+
*/
|
|
99
|
+
export function getToolMigrations(toolName: string): Migration[] {
|
|
100
|
+
return TOOL_MIGRATIONS[toolName] ?? [];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Get the last migrated version for a tool
|
|
105
|
+
*/
|
|
106
|
+
export function getLastMigratedVersion(toolName: string): string {
|
|
107
|
+
const config = loadConfig();
|
|
108
|
+
return config.migrations?.[toolName] ?? '0.0.0';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Update the last migrated version for a tool
|
|
113
|
+
*/
|
|
114
|
+
export function setLastMigratedVersion(toolName: string, version: string): void {
|
|
115
|
+
const config = loadConfig();
|
|
116
|
+
if (!config.migrations) {
|
|
117
|
+
config.migrations = {};
|
|
118
|
+
}
|
|
119
|
+
config.migrations[toolName] = version;
|
|
120
|
+
saveConfig(config);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Run migrations for a tool between two versions
|
|
125
|
+
* Returns true if all migrations succeeded, false otherwise
|
|
126
|
+
*/
|
|
127
|
+
export function runMigrations(
|
|
128
|
+
toolName: string,
|
|
129
|
+
fromVersion: string,
|
|
130
|
+
toVersion: string
|
|
131
|
+
): { success: boolean; error?: string } {
|
|
132
|
+
const migrations = getToolMigrations(toolName);
|
|
133
|
+
|
|
134
|
+
// Filter migrations that need to run (version > fromVersion && version <= toVersion)
|
|
135
|
+
const pendingMigrations = migrations.filter((m) => {
|
|
136
|
+
const afterFrom = compareSemver(m.version, fromVersion) > 0;
|
|
137
|
+
const beforeOrAtTo = compareSemver(m.version, toVersion) <= 0;
|
|
138
|
+
return afterFrom && beforeOrAtTo;
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
if (pendingMigrations.length === 0) {
|
|
142
|
+
// No migrations to run, but still update the version marker
|
|
143
|
+
setLastMigratedVersion(toolName, toVersion);
|
|
144
|
+
return { success: true };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const configDir = getConfigDir();
|
|
148
|
+
|
|
149
|
+
for (const migration of pendingMigrations) {
|
|
150
|
+
try {
|
|
151
|
+
migration.up(configDir);
|
|
152
|
+
logMigration(toolName, fromVersion, migration.version, 'OK');
|
|
153
|
+
} catch (error) {
|
|
154
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
155
|
+
logMigration(toolName, fromVersion, migration.version, 'FAILED', errorMessage);
|
|
156
|
+
// Don't update version marker on failure - will retry next time
|
|
157
|
+
return { success: false, error: `Migration ${migration.version} failed: ${errorMessage}` };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// All migrations succeeded, update version marker
|
|
162
|
+
setLastMigratedVersion(toolName, toVersion);
|
|
163
|
+
return { success: true };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Run migrations for a tool after install/update
|
|
168
|
+
* Call this after installSkill() completes
|
|
169
|
+
*/
|
|
170
|
+
export function runToolMigrations(
|
|
171
|
+
toolName: string,
|
|
172
|
+
installedVersion: string
|
|
173
|
+
): { success: boolean; error?: string } {
|
|
174
|
+
const lastMigrated = getLastMigratedVersion(toolName);
|
|
175
|
+
|
|
176
|
+
// Only run if the installed version is newer than last migrated
|
|
177
|
+
if (compareSemver(installedVersion, lastMigrated) <= 0) {
|
|
178
|
+
return { success: true };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return runMigrations(toolName, lastMigrated, installedVersion);
|
|
182
|
+
}
|
package/src/lib/skill-config.ts
CHANGED
|
@@ -86,7 +86,9 @@ export async function promptForSkillConfig(
|
|
|
86
86
|
|
|
87
87
|
if (Object.keys(overrides).length > 0) {
|
|
88
88
|
saveSkillOverrides(skillName, overrides);
|
|
89
|
-
|
|
89
|
+
// Strip droid- prefix for display since that's where the file actually goes
|
|
90
|
+
const displayName = skillName.replace(/^droid-/, '');
|
|
91
|
+
console.log(chalk.green(`\n✓ Configuration saved to ~/.droid/skills/${displayName}/overrides.yaml`));
|
|
90
92
|
} else {
|
|
91
93
|
console.log(chalk.gray('\nNo custom configuration set (using defaults).'));
|
|
92
94
|
}
|
package/src/lib/skills.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync, mkdirSync, writeFileSync, rmSync } from 'fs';
|
|
2
|
-
import { join, dirname } from 'path';
|
|
2
|
+
import { join, dirname, basename } from 'path';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
4
|
import YAML from 'yaml';
|
|
5
5
|
import { loadConfig, saveConfig } from './config';
|
|
@@ -7,6 +7,7 @@ import { Platform, SkillStatus, type SkillManifest, type InstalledSkill, getPlat
|
|
|
7
7
|
import { getInstalledAgentsDir, installAgentFromPath, uninstallAgent, isAgentInstalled } from './agents';
|
|
8
8
|
import { getSkillsPath, getCommandsPath, getConfigPath } from './platforms';
|
|
9
9
|
import { loadToolManifest } from './tools';
|
|
10
|
+
import { runToolMigrations } from './migrations';
|
|
10
11
|
|
|
11
12
|
// Marker comments for CLAUDE.md skill registration
|
|
12
13
|
const DROID_SKILLS_START = '<!-- droid-skills-start -->';
|
|
@@ -542,6 +543,14 @@ export function installSkill(skillName: string): { success: boolean; message: st
|
|
|
542
543
|
const installedSkillNames = Object.keys(updatedTools);
|
|
543
544
|
updatePlatformConfigSkills(config.platform, installedSkillNames);
|
|
544
545
|
|
|
546
|
+
// Run tool migrations (keyed by tool name, not skill name)
|
|
547
|
+
const toolName = basename(toolDir);
|
|
548
|
+
const migrationResult = runToolMigrations(toolName, manifest.version);
|
|
549
|
+
if (!migrationResult.success) {
|
|
550
|
+
// Log warning but don't fail the install - migration will retry next time
|
|
551
|
+
console.warn(`Warning: Migration failed for ${toolName}: ${migrationResult.error}`);
|
|
552
|
+
}
|
|
553
|
+
|
|
545
554
|
return { success: true, message: `Installed ${skillName} v${manifest.version}` };
|
|
546
555
|
}
|
|
547
556
|
|