@orderful/droid 0.26.0 → 0.27.1
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/.claude-plugin/marketplace.json +7 -7
- package/AGENTS.md +36 -32
- package/CHANGELOG.md +46 -0
- package/dist/bin/droid.js +140 -24
- package/dist/index.js +97 -25
- package/dist/lib/migrations.d.ts +8 -0
- package/dist/lib/migrations.d.ts.map +1 -1
- package/dist/lib/skills.d.ts.map +1 -1
- package/dist/lib/tools.d.ts.map +1 -1
- package/dist/lib/types.d.ts +10 -2
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/tools/brain/.claude-plugin/plugin.json +1 -1
- package/dist/tools/brain/TOOL.yaml +7 -5
- package/dist/tools/brain/commands/brain.md +17 -49
- package/dist/tools/brain/commands/scratchpad.md +13 -50
- package/{src/tools/brain/skills/droid-brain → dist/tools/brain/skills/brain}/SKILL.md +5 -5
- package/dist/tools/brain/skills/{droid-brain-obsidian → brain-obsidian}/SKILL.md +8 -8
- package/dist/tools/coach/.claude-plugin/plugin.json +1 -1
- package/dist/tools/coach/TOOL.yaml +4 -3
- package/dist/tools/coach/commands/coach.md +14 -54
- package/{src/tools/coach/skills/droid-coach → dist/tools/coach/skills/coach}/SKILL.md +4 -3
- package/dist/tools/code-review/.claude-plugin/plugin.json +1 -1
- package/dist/tools/code-review/TOOL.yaml +4 -3
- package/dist/tools/code-review/commands/code-review.md +18 -102
- package/dist/tools/code-review/skills/code-review/SKILL.md +154 -0
- package/dist/tools/codex/.claude-plugin/plugin.json +1 -1
- package/dist/tools/codex/TOOL.yaml +4 -3
- package/dist/tools/codex/commands/codex.md +18 -65
- package/dist/tools/codex/skills/{droid-codex → codex}/SKILL.md +64 -45
- package/{src/tools/codex/skills/droid-codex → dist/tools/codex/skills/codex}/references/loading.md +94 -55
- package/dist/tools/codex/skills/codex/scripts/git-finish-write.d.ts.map +1 -0
- package/dist/tools/codex/skills/codex/scripts/git-preamble.d.ts.map +1 -0
- package/dist/tools/codex/skills/codex/scripts/git-start-write.d.ts.map +1 -0
- package/dist/tools/comments/.claude-plugin/plugin.json +1 -1
- package/dist/tools/comments/TOOL.yaml +4 -3
- package/dist/tools/comments/commands/comments.md +12 -14
- package/{src/tools/comments/skills/droid-comments → dist/tools/comments/skills/comments}/SKILL.md +3 -1
- package/dist/tools/project/.claude-plugin/plugin.json +1 -1
- package/dist/tools/project/TOOL.yaml +4 -3
- package/dist/tools/project/commands/project.md +12 -27
- package/dist/tools/project/skills/{droid-project → project}/SKILL.md +12 -11
- package/dist/tools/tech-design/.claude-plugin/plugin.json +1 -1
- package/dist/tools/tech-design/TOOL.yaml +4 -3
- package/dist/tools/tech-design/commands/tech-design.md +18 -80
- package/{src/tools/tech-design/skills/droid-tech-design → dist/tools/tech-design/skills/tech-design}/SKILL.md +1 -1
- package/package.json +1 -1
- package/src/commands/tui/components/Badge.test.tsx +10 -4
- package/src/commands/tui.tsx +4 -4
- package/src/lib/migrations.ts +154 -4
- package/src/lib/skills.test.ts +199 -74
- package/src/lib/skills.ts +55 -54
- package/src/lib/tools.ts +19 -12
- package/src/lib/types.ts +20 -5
- package/src/tools/brain/.claude-plugin/plugin.json +1 -1
- package/src/tools/brain/TOOL.yaml +7 -5
- package/src/tools/brain/commands/brain.md +17 -49
- package/src/tools/brain/commands/scratchpad.md +13 -50
- package/{dist/tools/brain/skills/droid-brain → src/tools/brain/skills/brain}/SKILL.md +5 -5
- package/src/tools/brain/skills/{droid-brain-obsidian → brain-obsidian}/SKILL.md +8 -8
- package/src/tools/coach/.claude-plugin/plugin.json +1 -1
- package/src/tools/coach/TOOL.yaml +4 -3
- package/src/tools/coach/commands/coach.md +14 -54
- package/{dist/tools/coach/skills/droid-coach → src/tools/coach/skills/coach}/SKILL.md +4 -3
- package/src/tools/code-review/.claude-plugin/plugin.json +1 -1
- package/src/tools/code-review/TOOL.yaml +4 -3
- package/src/tools/code-review/commands/code-review.md +18 -102
- package/src/tools/code-review/skills/code-review/SKILL.md +154 -0
- package/src/tools/codex/.claude-plugin/plugin.json +1 -1
- package/src/tools/codex/TOOL.yaml +4 -3
- package/src/tools/codex/commands/codex.md +18 -65
- package/src/tools/codex/skills/{droid-codex → codex}/SKILL.md +64 -45
- package/{dist/tools/codex/skills/droid-codex → src/tools/codex/skills/codex}/references/loading.md +94 -55
- package/src/tools/comments/.claude-plugin/plugin.json +1 -1
- package/src/tools/comments/TOOL.yaml +4 -3
- package/src/tools/comments/commands/comments.md +12 -14
- package/{dist/tools/comments/skills/droid-comments → src/tools/comments/skills/comments}/SKILL.md +3 -1
- package/src/tools/project/.claude-plugin/plugin.json +1 -1
- package/src/tools/project/TOOL.yaml +4 -3
- package/src/tools/project/commands/project.md +12 -27
- package/src/tools/project/skills/{droid-project → project}/SKILL.md +12 -11
- package/src/tools/tech-design/.claude-plugin/plugin.json +1 -1
- package/src/tools/tech-design/TOOL.yaml +4 -3
- package/src/tools/tech-design/commands/tech-design.md +18 -80
- package/{dist/tools/tech-design/skills/droid-tech-design → src/tools/tech-design/skills/tech-design}/SKILL.md +1 -1
- package/dist/tools/code-review/skills/droid-code-review/SKILL.md +0 -55
- package/dist/tools/codex/skills/droid-codex/scripts/git-finish-write.d.ts.map +0 -1
- package/dist/tools/codex/skills/droid-codex/scripts/git-preamble.d.ts.map +0 -1
- package/dist/tools/codex/skills/droid-codex/scripts/git-start-write.d.ts.map +0 -1
- package/src/tools/code-review/skills/droid-code-review/SKILL.md +0 -55
- /package/dist/tools/brain/skills/{droid-brain → brain}/references/metadata.md +0 -0
- /package/dist/tools/brain/skills/{droid-brain → brain}/references/naming.md +0 -0
- /package/dist/tools/brain/skills/{droid-brain → brain}/references/templates.md +0 -0
- /package/dist/tools/brain/skills/{droid-brain → brain}/references/workflows.md +0 -0
- /package/dist/tools/brain/skills/{droid-brain-obsidian → brain-obsidian}/references/templates.md +0 -0
- /package/dist/tools/brain/skills/{droid-brain-obsidian → brain-obsidian}/references/workflows.md +0 -0
- /package/dist/tools/codex/skills/{droid-codex → codex}/references/creating.md +0 -0
- /package/dist/tools/codex/skills/{droid-codex → codex}/references/decisions.md +0 -0
- /package/dist/tools/codex/skills/{droid-codex → codex}/references/topics.md +0 -0
- /package/dist/tools/codex/skills/{droid-codex → codex}/scripts/git-finish-write.d.ts +0 -0
- /package/dist/tools/codex/skills/{droid-codex → codex}/scripts/git-finish-write.ts +0 -0
- /package/dist/tools/codex/skills/{droid-codex → codex}/scripts/git-preamble.d.ts +0 -0
- /package/dist/tools/codex/skills/{droid-codex → codex}/scripts/git-preamble.ts +0 -0
- /package/dist/tools/codex/skills/{droid-codex → codex}/scripts/git-scripts.test.ts +0 -0
- /package/dist/tools/codex/skills/{droid-codex → codex}/scripts/git-start-write.d.ts +0 -0
- /package/dist/tools/codex/skills/{droid-codex → codex}/scripts/git-start-write.ts +0 -0
- /package/dist/tools/project/skills/{droid-project → project}/references/changelog.md +0 -0
- /package/dist/tools/project/skills/{droid-project → project}/references/creating.md +0 -0
- /package/dist/tools/project/skills/{droid-project → project}/references/loading.md +0 -0
- /package/dist/tools/project/skills/{droid-project → project}/references/templates.md +0 -0
- /package/dist/tools/project/skills/{droid-project → project}/references/updating.md +0 -0
- /package/dist/tools/project/skills/{droid-project → project}/references/versioning.md +0 -0
- /package/dist/tools/tech-design/skills/{droid-tech-design → tech-design}/references/draft.md +0 -0
- /package/dist/tools/tech-design/skills/{droid-tech-design → tech-design}/references/gaps.md +0 -0
- /package/dist/tools/tech-design/skills/{droid-tech-design → tech-design}/references/publish.md +0 -0
- /package/dist/tools/tech-design/skills/{droid-tech-design → tech-design}/references/research-doc-template.md +0 -0
- /package/dist/tools/tech-design/skills/{droid-tech-design → tech-design}/references/rollup-template.md +0 -0
- /package/dist/tools/tech-design/skills/{droid-tech-design → tech-design}/references/start.md +0 -0
- /package/dist/tools/tech-design/skills/{droid-tech-design → tech-design}/references/think.md +0 -0
- /package/dist/tools/tech-design/skills/{droid-tech-design → tech-design}/references/thought-doc-template.md +0 -0
- /package/src/tools/brain/skills/{droid-brain → brain}/references/metadata.md +0 -0
- /package/src/tools/brain/skills/{droid-brain → brain}/references/naming.md +0 -0
- /package/src/tools/brain/skills/{droid-brain → brain}/references/templates.md +0 -0
- /package/src/tools/brain/skills/{droid-brain → brain}/references/workflows.md +0 -0
- /package/src/tools/brain/skills/{droid-brain-obsidian → brain-obsidian}/references/templates.md +0 -0
- /package/src/tools/brain/skills/{droid-brain-obsidian → brain-obsidian}/references/workflows.md +0 -0
- /package/src/tools/codex/skills/{droid-codex → codex}/references/creating.md +0 -0
- /package/src/tools/codex/skills/{droid-codex → codex}/references/decisions.md +0 -0
- /package/src/tools/codex/skills/{droid-codex → codex}/references/topics.md +0 -0
- /package/src/tools/codex/skills/{droid-codex → codex}/scripts/git-finish-write.ts +0 -0
- /package/src/tools/codex/skills/{droid-codex → codex}/scripts/git-preamble.ts +0 -0
- /package/src/tools/codex/skills/{droid-codex → codex}/scripts/git-scripts.test.ts +0 -0
- /package/src/tools/codex/skills/{droid-codex → codex}/scripts/git-start-write.ts +0 -0
- /package/src/tools/project/skills/{droid-project → project}/references/changelog.md +0 -0
- /package/src/tools/project/skills/{droid-project → project}/references/creating.md +0 -0
- /package/src/tools/project/skills/{droid-project → project}/references/loading.md +0 -0
- /package/src/tools/project/skills/{droid-project → project}/references/templates.md +0 -0
- /package/src/tools/project/skills/{droid-project → project}/references/updating.md +0 -0
- /package/src/tools/project/skills/{droid-project → project}/references/versioning.md +0 -0
- /package/src/tools/tech-design/skills/{droid-tech-design → tech-design}/references/draft.md +0 -0
- /package/src/tools/tech-design/skills/{droid-tech-design → tech-design}/references/gaps.md +0 -0
- /package/src/tools/tech-design/skills/{droid-tech-design → tech-design}/references/publish.md +0 -0
- /package/src/tools/tech-design/skills/{droid-tech-design → tech-design}/references/research-doc-template.md +0 -0
- /package/src/tools/tech-design/skills/{droid-tech-design → tech-design}/references/rollup-template.md +0 -0
- /package/src/tools/tech-design/skills/{droid-tech-design → tech-design}/references/start.md +0 -0
- /package/src/tools/tech-design/skills/{droid-tech-design → tech-design}/references/think.md +0 -0
- /package/src/tools/tech-design/skills/{droid-tech-design → tech-design}/references/thought-doc-template.md +0 -0
package/src/lib/skills.test.ts
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
rmSync,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
readdirSync,
|
|
9
|
+
} from "fs";
|
|
10
|
+
import { join } from "path";
|
|
11
|
+
import { tmpdir } from "os";
|
|
12
|
+
import { homedir } from "os";
|
|
13
|
+
import YAML from "yaml";
|
|
14
|
+
import { Platform, SkillStatus } from "./types";
|
|
8
15
|
import {
|
|
9
16
|
getSkillsInstallPath,
|
|
10
17
|
getCommandsInstallPath,
|
|
@@ -12,7 +19,9 @@ import {
|
|
|
12
19
|
getSkillStatusDisplay,
|
|
13
20
|
getBundledSkillsDir,
|
|
14
21
|
loadSkillManifest,
|
|
15
|
-
|
|
22
|
+
installSkill,
|
|
23
|
+
} from "./skills";
|
|
24
|
+
import { loadConfig, saveConfig } from "./config";
|
|
16
25
|
|
|
17
26
|
/**
|
|
18
27
|
* Parse YAML frontmatter from markdown content
|
|
@@ -27,73 +36,73 @@ function parseFrontmatter(content: string): Record<string, unknown> | null {
|
|
|
27
36
|
}
|
|
28
37
|
}
|
|
29
38
|
|
|
30
|
-
describe(
|
|
31
|
-
it(
|
|
39
|
+
describe("getSkillsInstallPath", () => {
|
|
40
|
+
it("should return Claude Code path", () => {
|
|
32
41
|
const path = getSkillsInstallPath(Platform.ClaudeCode);
|
|
33
|
-
expect(path).toBe(join(homedir(),
|
|
42
|
+
expect(path).toBe(join(homedir(), ".claude", "skills"));
|
|
34
43
|
});
|
|
35
44
|
|
|
36
|
-
it(
|
|
45
|
+
it("should return OpenCode path", () => {
|
|
37
46
|
const path = getSkillsInstallPath(Platform.OpenCode);
|
|
38
|
-
expect(path).toBe(join(homedir(),
|
|
47
|
+
expect(path).toBe(join(homedir(), ".config", "opencode", "skills"));
|
|
39
48
|
});
|
|
40
49
|
});
|
|
41
50
|
|
|
42
|
-
describe(
|
|
43
|
-
it(
|
|
51
|
+
describe("getCommandsInstallPath", () => {
|
|
52
|
+
it("should return Claude Code commands path", () => {
|
|
44
53
|
const path = getCommandsInstallPath(Platform.ClaudeCode);
|
|
45
|
-
expect(path).toBe(join(homedir(),
|
|
54
|
+
expect(path).toBe(join(homedir(), ".claude", "commands"));
|
|
46
55
|
});
|
|
47
56
|
|
|
48
|
-
it(
|
|
57
|
+
it("should return OpenCode commands path", () => {
|
|
49
58
|
const path = getCommandsInstallPath(Platform.OpenCode);
|
|
50
|
-
expect(path).toBe(join(homedir(),
|
|
59
|
+
expect(path).toBe(join(homedir(), ".config", "opencode", "command"));
|
|
51
60
|
});
|
|
52
61
|
});
|
|
53
62
|
|
|
54
|
-
describe(
|
|
55
|
-
it(
|
|
63
|
+
describe("getPlatformConfigPath", () => {
|
|
64
|
+
it("should return Claude Code CLAUDE.md path", () => {
|
|
56
65
|
const path = getPlatformConfigPath(Platform.ClaudeCode);
|
|
57
|
-
expect(path).toBe(join(homedir(),
|
|
66
|
+
expect(path).toBe(join(homedir(), ".claude", "CLAUDE.md"));
|
|
58
67
|
});
|
|
59
68
|
|
|
60
|
-
it(
|
|
69
|
+
it("should return OpenCode AGENTS.md path", () => {
|
|
61
70
|
const path = getPlatformConfigPath(Platform.OpenCode);
|
|
62
|
-
expect(path).toBe(join(homedir(),
|
|
71
|
+
expect(path).toBe(join(homedir(), ".config", "opencode", "AGENTS.md"));
|
|
63
72
|
});
|
|
64
73
|
});
|
|
65
74
|
|
|
66
|
-
describe(
|
|
67
|
-
it(
|
|
75
|
+
describe("getBundledSkillsDir", () => {
|
|
76
|
+
it("should return a path ending in tools", () => {
|
|
68
77
|
const dir = getBundledSkillsDir();
|
|
69
|
-
expect(dir.endsWith(
|
|
78
|
+
expect(dir.endsWith("tools")).toBe(true);
|
|
70
79
|
});
|
|
71
80
|
|
|
72
|
-
it(
|
|
81
|
+
it("should return an existing directory", () => {
|
|
73
82
|
const dir = getBundledSkillsDir();
|
|
74
83
|
expect(existsSync(dir)).toBe(true);
|
|
75
84
|
});
|
|
76
85
|
});
|
|
77
86
|
|
|
78
|
-
describe(
|
|
79
|
-
it(
|
|
80
|
-
expect(getSkillStatusDisplay(SkillStatus.Alpha)).toBe(
|
|
87
|
+
describe("getSkillStatusDisplay", () => {
|
|
88
|
+
it("should return [alpha] for alpha status", () => {
|
|
89
|
+
expect(getSkillStatusDisplay(SkillStatus.Alpha)).toBe("[alpha]");
|
|
81
90
|
});
|
|
82
91
|
|
|
83
|
-
it(
|
|
84
|
-
expect(getSkillStatusDisplay(SkillStatus.Beta)).toBe(
|
|
92
|
+
it("should return [beta] for beta status", () => {
|
|
93
|
+
expect(getSkillStatusDisplay(SkillStatus.Beta)).toBe("[beta]");
|
|
85
94
|
});
|
|
86
95
|
|
|
87
|
-
it(
|
|
88
|
-
expect(getSkillStatusDisplay(SkillStatus.Stable)).toBe(
|
|
96
|
+
it("should return empty string for stable status", () => {
|
|
97
|
+
expect(getSkillStatusDisplay(SkillStatus.Stable)).toBe("");
|
|
89
98
|
});
|
|
90
99
|
|
|
91
|
-
it(
|
|
92
|
-
expect(getSkillStatusDisplay(undefined)).toBe(
|
|
100
|
+
it("should return empty string for undefined status", () => {
|
|
101
|
+
expect(getSkillStatusDisplay(undefined)).toBe("");
|
|
93
102
|
});
|
|
94
103
|
});
|
|
95
104
|
|
|
96
|
-
describe(
|
|
105
|
+
describe("skill manifest parsing", () => {
|
|
97
106
|
let testDir: string;
|
|
98
107
|
|
|
99
108
|
beforeEach(() => {
|
|
@@ -107,44 +116,44 @@ describe('skill manifest parsing', () => {
|
|
|
107
116
|
}
|
|
108
117
|
});
|
|
109
118
|
|
|
110
|
-
it(
|
|
119
|
+
it("should parse valid skill manifest", () => {
|
|
111
120
|
const manifest = {
|
|
112
|
-
name:
|
|
113
|
-
description:
|
|
114
|
-
version:
|
|
115
|
-
status:
|
|
116
|
-
dependencies: [
|
|
121
|
+
name: "test-skill",
|
|
122
|
+
description: "A test skill",
|
|
123
|
+
version: "1.0.0",
|
|
124
|
+
status: "beta",
|
|
125
|
+
dependencies: ["comments"],
|
|
117
126
|
provides_output: true,
|
|
118
127
|
};
|
|
119
128
|
|
|
120
|
-
const manifestPath = join(testDir,
|
|
121
|
-
writeFileSync(manifestPath, YAML.stringify(manifest),
|
|
129
|
+
const manifestPath = join(testDir, "SKILL.yaml");
|
|
130
|
+
writeFileSync(manifestPath, YAML.stringify(manifest), "utf-8");
|
|
122
131
|
|
|
123
|
-
const content = require(
|
|
132
|
+
const content = require("fs").readFileSync(manifestPath, "utf-8");
|
|
124
133
|
const parsed = YAML.parse(content);
|
|
125
134
|
|
|
126
|
-
expect(parsed.name).toBe(
|
|
127
|
-
expect(parsed.description).toBe(
|
|
128
|
-
expect(parsed.version).toBe(
|
|
129
|
-
expect(parsed.status).toBe(
|
|
130
|
-
expect(parsed.dependencies).toEqual([
|
|
135
|
+
expect(parsed.name).toBe("test-skill");
|
|
136
|
+
expect(parsed.description).toBe("A test skill");
|
|
137
|
+
expect(parsed.version).toBe("1.0.0");
|
|
138
|
+
expect(parsed.status).toBe("beta");
|
|
139
|
+
expect(parsed.dependencies).toEqual(["comments"]);
|
|
131
140
|
expect(parsed.provides_output).toBe(true);
|
|
132
141
|
});
|
|
133
142
|
|
|
134
|
-
it(
|
|
143
|
+
it("should handle manifest without optional fields", () => {
|
|
135
144
|
const manifest = {
|
|
136
|
-
name:
|
|
137
|
-
description:
|
|
138
|
-
version:
|
|
145
|
+
name: "minimal-skill",
|
|
146
|
+
description: "Minimal",
|
|
147
|
+
version: "0.1.0",
|
|
139
148
|
};
|
|
140
149
|
|
|
141
|
-
const manifestPath = join(testDir,
|
|
142
|
-
writeFileSync(manifestPath, YAML.stringify(manifest),
|
|
150
|
+
const manifestPath = join(testDir, "SKILL.yaml");
|
|
151
|
+
writeFileSync(manifestPath, YAML.stringify(manifest), "utf-8");
|
|
143
152
|
|
|
144
|
-
const content = require(
|
|
153
|
+
const content = require("fs").readFileSync(manifestPath, "utf-8");
|
|
145
154
|
const parsed = YAML.parse(content);
|
|
146
155
|
|
|
147
|
-
expect(parsed.name).toBe(
|
|
156
|
+
expect(parsed.name).toBe("minimal-skill");
|
|
148
157
|
expect(parsed.status).toBeUndefined();
|
|
149
158
|
expect(parsed.dependencies).toBeUndefined();
|
|
150
159
|
expect(parsed.provides_output).toBeUndefined();
|
|
@@ -154,7 +163,9 @@ describe('skill manifest parsing', () => {
|
|
|
154
163
|
/**
|
|
155
164
|
* Helper to get all skill directories in the new tools/{tool}/skills/{skill} structure
|
|
156
165
|
*/
|
|
157
|
-
function getAllSkillPaths(
|
|
166
|
+
function getAllSkillPaths(
|
|
167
|
+
toolsDir: string,
|
|
168
|
+
): Array<{ skillName: string; skillDir: string }> {
|
|
158
169
|
const skillPaths: Array<{ skillName: string; skillDir: string }> = [];
|
|
159
170
|
|
|
160
171
|
const toolDirs = readdirSync(toolsDir, { withFileTypes: true })
|
|
@@ -162,7 +173,7 @@ function getAllSkillPaths(toolsDir: string): Array<{ skillName: string; skillDir
|
|
|
162
173
|
.map((d) => d.name);
|
|
163
174
|
|
|
164
175
|
for (const toolName of toolDirs) {
|
|
165
|
-
const skillsDir = join(toolsDir, toolName,
|
|
176
|
+
const skillsDir = join(toolsDir, toolName, "skills");
|
|
166
177
|
if (!existsSync(skillsDir)) continue;
|
|
167
178
|
|
|
168
179
|
const skillDirs = readdirSync(skillsDir, { withFileTypes: true })
|
|
@@ -180,43 +191,43 @@ function getAllSkillPaths(toolsDir: string): Array<{ skillName: string; skillDir
|
|
|
180
191
|
return skillPaths;
|
|
181
192
|
}
|
|
182
193
|
|
|
183
|
-
describe(
|
|
184
|
-
it(
|
|
194
|
+
describe("bundled skills validation", () => {
|
|
195
|
+
it("all skills should have SKILL.md", () => {
|
|
185
196
|
const toolsDir = getBundledSkillsDir();
|
|
186
197
|
const skillPaths = getAllSkillPaths(toolsDir);
|
|
187
198
|
|
|
188
199
|
expect(skillPaths.length).toBeGreaterThan(0);
|
|
189
200
|
|
|
190
201
|
for (const { skillDir } of skillPaths) {
|
|
191
|
-
const skillMdPath = join(skillDir,
|
|
202
|
+
const skillMdPath = join(skillDir, "SKILL.md");
|
|
192
203
|
expect(existsSync(skillMdPath)).toBe(true);
|
|
193
204
|
}
|
|
194
205
|
});
|
|
195
206
|
|
|
196
|
-
it(
|
|
207
|
+
it("all skills should have metadata in TOOL.yaml", () => {
|
|
197
208
|
const toolsDir = getBundledSkillsDir();
|
|
198
209
|
const skillPaths = getAllSkillPaths(toolsDir);
|
|
199
210
|
|
|
200
211
|
for (const { skillName, skillDir } of skillPaths) {
|
|
201
212
|
// Get the parent tool directory
|
|
202
|
-
const toolDir = join(skillDir,
|
|
203
|
-
const toolYamlPath = join(toolDir,
|
|
213
|
+
const toolDir = join(skillDir, "..", "..");
|
|
214
|
+
const toolYamlPath = join(toolDir, "TOOL.yaml");
|
|
204
215
|
expect(existsSync(toolYamlPath)).toBe(true);
|
|
205
216
|
|
|
206
|
-
const toolContent = readFileSync(toolYamlPath,
|
|
217
|
+
const toolContent = readFileSync(toolYamlPath, "utf-8");
|
|
207
218
|
const toolManifest = YAML.parse(toolContent);
|
|
208
219
|
|
|
209
220
|
// Find the skill in includes.skills
|
|
210
221
|
const skillInclude = toolManifest.includes?.skills?.find(
|
|
211
|
-
(s: { name: string }) => s.name === skillName
|
|
222
|
+
(s: { name: string }) => s.name === skillName,
|
|
212
223
|
);
|
|
213
224
|
expect(skillInclude).toBeDefined();
|
|
214
|
-
expect(typeof toolManifest.description).toBe(
|
|
215
|
-
expect(typeof toolManifest.version).toBe(
|
|
225
|
+
expect(typeof toolManifest.description).toBe("string");
|
|
226
|
+
expect(typeof toolManifest.version).toBe("string");
|
|
216
227
|
}
|
|
217
228
|
});
|
|
218
229
|
|
|
219
|
-
it(
|
|
230
|
+
it("loadSkillManifest should return valid manifest from TOOL.yaml", () => {
|
|
220
231
|
const toolsDir = getBundledSkillsDir();
|
|
221
232
|
const skillPaths = getAllSkillPaths(toolsDir);
|
|
222
233
|
|
|
@@ -225,8 +236,122 @@ describe('bundled skills validation', () => {
|
|
|
225
236
|
|
|
226
237
|
expect(manifest).not.toBeNull();
|
|
227
238
|
expect(manifest?.name).toBe(skillName);
|
|
228
|
-
expect(typeof manifest?.description).toBe(
|
|
229
|
-
expect(typeof manifest?.version).toBe(
|
|
239
|
+
expect(typeof manifest?.description).toBe("string");
|
|
240
|
+
expect(typeof manifest?.version).toBe("string");
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
describe('platform-specific command installation', () => {
|
|
245
|
+
let testConfigDir: string;
|
|
246
|
+
let testSkillsDir: string;
|
|
247
|
+
let testCommandsDir: string;
|
|
248
|
+
let originalConfig: any;
|
|
249
|
+
|
|
250
|
+
beforeEach(() => {
|
|
251
|
+
// Save original config
|
|
252
|
+
originalConfig = loadConfig();
|
|
253
|
+
|
|
254
|
+
// Create temporary directories for testing
|
|
255
|
+
testConfigDir = join(tmpdir(), `droid-test-config-${Date.now()}`);
|
|
256
|
+
testSkillsDir = join(tmpdir(), `droid-test-skills-${Date.now()}`);
|
|
257
|
+
testCommandsDir = join(tmpdir(), `droid-test-commands-${Date.now()}`);
|
|
258
|
+
|
|
259
|
+
mkdirSync(testConfigDir, { recursive: true });
|
|
260
|
+
mkdirSync(testSkillsDir, { recursive: true });
|
|
261
|
+
mkdirSync(testCommandsDir, { recursive: true });
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
afterEach(() => {
|
|
265
|
+
// Restore original config
|
|
266
|
+
saveConfig(originalConfig);
|
|
267
|
+
|
|
268
|
+
// Cleanup test directories
|
|
269
|
+
if (existsSync(testConfigDir)) {
|
|
270
|
+
rmSync(testConfigDir, { recursive: true });
|
|
271
|
+
}
|
|
272
|
+
if (existsSync(testSkillsDir)) {
|
|
273
|
+
rmSync(testSkillsDir, { recursive: true });
|
|
274
|
+
}
|
|
275
|
+
if (existsSync(testCommandsDir)) {
|
|
276
|
+
rmSync(testCommandsDir, { recursive: true });
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('should install all commands on OpenCode platform', () => {
|
|
281
|
+
// This is an integration test that would require mocking the install paths
|
|
282
|
+
// For now, we verify the logic exists by checking tool manifests
|
|
283
|
+
const toolsDir = getBundledSkillsDir();
|
|
284
|
+
|
|
285
|
+
// Find brain tool which has both primary and alias commands
|
|
286
|
+
const brainToolDir = join(toolsDir, 'brain');
|
|
287
|
+
const toolYamlPath = join(brainToolDir, 'TOOL.yaml');
|
|
288
|
+
|
|
289
|
+
if (!existsSync(toolYamlPath)) {
|
|
290
|
+
// Skip test if brain tool doesn't exist
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const toolContent = readFileSync(toolYamlPath, 'utf-8');
|
|
295
|
+
const toolManifest = YAML.parse(toolContent);
|
|
296
|
+
|
|
297
|
+
// Verify brain has both primary and alias commands
|
|
298
|
+
const commands = toolManifest.includes?.commands || [];
|
|
299
|
+
const brainCommand = commands.find((c: any) => c.name === 'brain');
|
|
300
|
+
const scratchpadCommand = commands.find((c: any) => c.name === 'scratchpad');
|
|
301
|
+
|
|
302
|
+
expect(brainCommand).toBeDefined();
|
|
303
|
+
expect(brainCommand.is_alias).toBe(false);
|
|
304
|
+
|
|
305
|
+
expect(scratchpadCommand).toBeDefined();
|
|
306
|
+
expect(scratchpadCommand.is_alias).toBe(true);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('should identify alias commands correctly', () => {
|
|
310
|
+
const toolsDir = getBundledSkillsDir();
|
|
311
|
+
const brainToolDir = join(toolsDir, 'brain');
|
|
312
|
+
const toolYamlPath = join(brainToolDir, 'TOOL.yaml');
|
|
313
|
+
|
|
314
|
+
if (!existsSync(toolYamlPath)) {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const toolContent = readFileSync(toolYamlPath, 'utf-8');
|
|
319
|
+
const toolManifest = YAML.parse(toolContent);
|
|
320
|
+
const commands = toolManifest.includes?.commands || [];
|
|
321
|
+
|
|
322
|
+
// All alias commands should have is_alias: true
|
|
323
|
+
const aliasCommands = commands.filter((c: any) => c.is_alias === true);
|
|
324
|
+
const primaryCommands = commands.filter((c: any) => c.is_alias === false);
|
|
325
|
+
|
|
326
|
+
// Should have at least one of each type in brain tool
|
|
327
|
+
expect(aliasCommands.length).toBeGreaterThan(0);
|
|
328
|
+
expect(primaryCommands.length).toBeGreaterThan(0);
|
|
329
|
+
|
|
330
|
+
// Verify scratchpad is marked as alias
|
|
331
|
+
const scratchpad = commands.find((c: any) => c.name === 'scratchpad');
|
|
332
|
+
if (scratchpad) {
|
|
333
|
+
expect(scratchpad.is_alias).toBe(true);
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it('should have is_alias property on all command objects', () => {
|
|
338
|
+
const toolsDir = getBundledSkillsDir();
|
|
339
|
+
const toolDirs = readdirSync(toolsDir, { withFileTypes: true })
|
|
340
|
+
.filter(d => d.isDirectory())
|
|
341
|
+
.map(d => join(toolsDir, d.name));
|
|
342
|
+
|
|
343
|
+
for (const toolDir of toolDirs) {
|
|
344
|
+
const toolYamlPath = join(toolDir, 'TOOL.yaml');
|
|
345
|
+
if (!existsSync(toolYamlPath)) continue;
|
|
346
|
+
|
|
347
|
+
const toolContent = readFileSync(toolYamlPath, 'utf-8');
|
|
348
|
+
const toolManifest = YAML.parse(toolContent);
|
|
349
|
+
const commands = toolManifest.includes?.commands || [];
|
|
350
|
+
|
|
351
|
+
// All commands should have is_alias property (boolean)
|
|
352
|
+
for (const cmd of commands) {
|
|
353
|
+
expect(typeof cmd.is_alias).toBe('boolean');
|
|
354
|
+
}
|
|
230
355
|
}
|
|
231
356
|
});
|
|
232
357
|
});
|
package/src/lib/skills.ts
CHANGED
|
@@ -411,6 +411,15 @@ export function installSkill(skillName: string): {
|
|
|
411
411
|
};
|
|
412
412
|
}
|
|
413
413
|
|
|
414
|
+
// Load tool manifest to get command metadata (for is_alias flag)
|
|
415
|
+
const toolManifest = loadToolManifest(toolDir);
|
|
416
|
+
if (!toolManifest) {
|
|
417
|
+
return {
|
|
418
|
+
success: false,
|
|
419
|
+
message: `Invalid tool manifest for tool containing '${skillName}'`,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
414
423
|
// Check dependencies
|
|
415
424
|
if (manifest.dependencies) {
|
|
416
425
|
for (const dep of manifest.dependencies) {
|
|
@@ -428,49 +437,39 @@ export function installSkill(skillName: string): {
|
|
|
428
437
|
const commandsPath = getCommandsInstallPath(config.platform);
|
|
429
438
|
const tools = getPlatformTools(config);
|
|
430
439
|
|
|
431
|
-
// Clean up old
|
|
432
|
-
//
|
|
433
|
-
//
|
|
434
|
-
//
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
// 5. Bump tool versions and create changeset
|
|
452
|
-
// Migration logic: Handle skills renamed from {name} to droid-{name} (v0.18.0 workaround)
|
|
453
|
-
// This handles both fresh installs and updates where old directory/config entries exist
|
|
454
|
-
if (skillName.startsWith('droid-')) {
|
|
455
|
-
const oldSkillName = skillName.replace(/^droid-/, '');
|
|
456
|
-
const oldSkillDir = join(skillsPath, oldSkillName);
|
|
457
|
-
|
|
458
|
-
// Remove or migrate old skill directory
|
|
459
|
-
if (existsSync(oldSkillDir)) {
|
|
440
|
+
// Clean up old droid- prefixed directories from v0.18.x (Claude Code bug workaround)
|
|
441
|
+
// CLEANUP: Remove after v0.22.0 or 2026-04-01 (3 months post-v0.21.0 release)
|
|
442
|
+
// Migration for droid- prefix removal (v0.18.x → v0.21.0)
|
|
443
|
+
// These skills were temporarily renamed with droid- prefix, now back to original names
|
|
444
|
+
const renamedSkills = [
|
|
445
|
+
'comments',
|
|
446
|
+
'brain',
|
|
447
|
+
'project',
|
|
448
|
+
'coach',
|
|
449
|
+
'code-review',
|
|
450
|
+
'codex',
|
|
451
|
+
'tech-design',
|
|
452
|
+
'brain-obsidian',
|
|
453
|
+
];
|
|
454
|
+
if (renamedSkills.includes(skillName)) {
|
|
455
|
+
const droidPrefixedName = `droid-${skillName}`;
|
|
456
|
+
const droidPrefixedDir = join(skillsPath, droidPrefixedName);
|
|
457
|
+
|
|
458
|
+
// Remove old droid- prefixed directory
|
|
459
|
+
if (existsSync(droidPrefixedDir)) {
|
|
460
460
|
try {
|
|
461
|
-
rmSync(
|
|
461
|
+
rmSync(droidPrefixedDir, { recursive: true });
|
|
462
462
|
} catch (error) {
|
|
463
463
|
// Non-fatal: Log warning but continue installation
|
|
464
464
|
console.warn(
|
|
465
|
-
`Warning: Could not remove old skill directory ${
|
|
465
|
+
`Warning: Could not remove old skill directory ${droidPrefixedDir}: ${error}`,
|
|
466
466
|
);
|
|
467
467
|
}
|
|
468
468
|
}
|
|
469
469
|
|
|
470
470
|
// Migrate old config entry if it exists
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
delete tools[oldSkillName];
|
|
471
|
+
if (tools[droidPrefixedName]) {
|
|
472
|
+
delete tools[droidPrefixedName];
|
|
474
473
|
// Save immediately to ensure cleanup is persisted (matches pattern in tools.ts)
|
|
475
474
|
setPlatformTools(config, tools);
|
|
476
475
|
saveConfig(config);
|
|
@@ -484,23 +483,10 @@ export function installSkill(skillName: string): {
|
|
|
484
483
|
// Check for collisions BEFORE installing (only if not already installed by droid)
|
|
485
484
|
// Note: If skill folder exists but skill isn't in config, we allow overwriting (stale state
|
|
486
485
|
// from platform switch or manual cleanup). Command/agent collisions still checked.
|
|
487
|
-
|
|
488
|
-
// Special handling for droid- prefix migration (temporary workaround for Claude Code bug #14945):
|
|
489
|
-
// If installing droid-comments, check if 'comments' is in config (old naming)
|
|
490
|
-
const oldSkillName = skillName.startsWith('droid-')
|
|
491
|
-
? skillName.replace(/^droid-/, '')
|
|
492
|
-
: null;
|
|
493
|
-
const isAlreadyInstalled =
|
|
494
|
-
tools[skillName] || (oldSkillName && tools[oldSkillName]);
|
|
486
|
+
const isAlreadyInstalled = tools[skillName];
|
|
495
487
|
|
|
496
488
|
if (!isAlreadyInstalled) {
|
|
497
|
-
// For collision detection, also check if this is the same tool (with/without droid- prefix)
|
|
498
|
-
// E.g., installing droid-comments should be allowed to overwrite existing comments.md
|
|
499
|
-
//
|
|
500
|
-
// TO REVERSE WHEN BUG #14945 IS FIXED:
|
|
501
|
-
// Remove the normalizedToolName logic - tools will have original names without droid- prefix
|
|
502
489
|
const toolName = basename(toolDir);
|
|
503
|
-
const normalizedToolName = toolName.replace(/^droid-/, '');
|
|
504
490
|
|
|
505
491
|
// Check command file collisions (these could conflict with other skills)
|
|
506
492
|
if (existsSync(commandsSource)) {
|
|
@@ -512,9 +498,9 @@ export function installSkill(skillName: string): {
|
|
|
512
498
|
if (existsSync(targetCommandPath)) {
|
|
513
499
|
const commandName = file.replace('.md', '');
|
|
514
500
|
|
|
515
|
-
// Allow overwrite if the command name matches the tool name
|
|
501
|
+
// Allow overwrite if the command name matches the tool name
|
|
516
502
|
// E.g., installing 'comments' tool can overwrite 'comments.md'
|
|
517
|
-
if (commandName === toolName
|
|
503
|
+
if (commandName === toolName) {
|
|
518
504
|
continue; // Same tool, allow overwrite
|
|
519
505
|
}
|
|
520
506
|
|
|
@@ -595,6 +581,7 @@ export function installSkill(skillName: string): {
|
|
|
595
581
|
}
|
|
596
582
|
|
|
597
583
|
// Copy commands if present (from tool level)
|
|
584
|
+
// Platform-specific: Only install primary commands on OpenCode, always install aliases
|
|
598
585
|
if (existsSync(commandsSource)) {
|
|
599
586
|
if (!existsSync(commandsPath)) {
|
|
600
587
|
mkdirSync(commandsPath, { recursive: true });
|
|
@@ -602,11 +589,25 @@ export function installSkill(skillName: string): {
|
|
|
602
589
|
const commandFiles = readdirSync(commandsSource).filter(
|
|
603
590
|
(f) => f.endsWith('.md') && f.toLowerCase() !== 'readme.md',
|
|
604
591
|
);
|
|
592
|
+
|
|
605
593
|
for (const file of commandFiles) {
|
|
606
|
-
const
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
594
|
+
const commandName = file.replace('.md', '');
|
|
595
|
+
|
|
596
|
+
// Find this command in the tool manifest to check if it's an alias
|
|
597
|
+
const commandMeta = toolManifest.includes.commands.find(
|
|
598
|
+
(cmd) => (typeof cmd === 'string' ? cmd : cmd.name) === commandName,
|
|
599
|
+
);
|
|
600
|
+
const isAlias = typeof commandMeta === 'object' && commandMeta.is_alias;
|
|
601
|
+
|
|
602
|
+
// Install if: OpenCode (all commands) OR alias (both platforms)
|
|
603
|
+
const shouldInstall = config.platform === Platform.OpenCode || isAlias;
|
|
604
|
+
|
|
605
|
+
if (shouldInstall) {
|
|
606
|
+
const sourcePath = join(commandsSource, file);
|
|
607
|
+
const targetPath = join(commandsPath, file);
|
|
608
|
+
const content = readFileSync(sourcePath, 'utf-8');
|
|
609
|
+
writeFileSync(targetPath, content);
|
|
610
|
+
}
|
|
610
611
|
}
|
|
611
612
|
}
|
|
612
613
|
|
package/src/lib/tools.ts
CHANGED
|
@@ -3,7 +3,12 @@ import { join, dirname } from 'path';
|
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
4
|
import YAML from 'yaml';
|
|
5
5
|
import { loadConfig, saveConfig } from './config';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
type ToolManifest,
|
|
8
|
+
type ToolIncludes,
|
|
9
|
+
getPlatformTools,
|
|
10
|
+
setPlatformTools,
|
|
11
|
+
} from './types';
|
|
7
12
|
import { compareSemver } from './version';
|
|
8
13
|
|
|
9
14
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -78,15 +83,15 @@ export function isToolInstalled(toolName: string): boolean {
|
|
|
78
83
|
const installedTools = getPlatformTools(config);
|
|
79
84
|
|
|
80
85
|
// A tool is installed if any of its required skills are in the installed tools
|
|
81
|
-
const tool = getBundledTools().find(t => t.name === toolName);
|
|
86
|
+
const tool = getBundledTools().find((t) => t.name === toolName);
|
|
82
87
|
if (!tool) return false;
|
|
83
88
|
|
|
84
89
|
const requiredSkills = tool.includes.skills
|
|
85
|
-
.filter(s => s.required)
|
|
86
|
-
.map(s => s.name);
|
|
90
|
+
.filter((s) => s.required)
|
|
91
|
+
.map((s) => s.name);
|
|
87
92
|
|
|
88
93
|
// Tool is installed if at least one required skill is installed
|
|
89
|
-
return requiredSkills.some(skillName => skillName in installedTools);
|
|
94
|
+
return requiredSkills.some((skillName) => skillName in installedTools);
|
|
90
95
|
}
|
|
91
96
|
|
|
92
97
|
/**
|
|
@@ -96,13 +101,13 @@ export function getInstalledToolVersion(toolName: string): string | null {
|
|
|
96
101
|
const config = loadConfig();
|
|
97
102
|
const installedTools = getPlatformTools(config);
|
|
98
103
|
|
|
99
|
-
const tool = getBundledTools().find(t => t.name === toolName);
|
|
104
|
+
const tool = getBundledTools().find((t) => t.name === toolName);
|
|
100
105
|
if (!tool) return null;
|
|
101
106
|
|
|
102
107
|
// Get version from the first installed required skill
|
|
103
108
|
const requiredSkills = tool.includes.skills
|
|
104
|
-
.filter(s => s.required)
|
|
105
|
-
.map(s => s.name);
|
|
109
|
+
.filter((s) => s.required)
|
|
110
|
+
.map((s) => s.name);
|
|
106
111
|
|
|
107
112
|
for (const skillName of requiredSkills) {
|
|
108
113
|
if (installedTools[skillName]) {
|
|
@@ -139,7 +144,7 @@ export interface ToolUpdateInfo {
|
|
|
139
144
|
*/
|
|
140
145
|
export function getToolUpdateStatus(toolName: string): ToolUpdateInfo {
|
|
141
146
|
const installedVersion = getInstalledToolVersion(toolName);
|
|
142
|
-
const tool = getBundledTools().find(t => t.name === toolName);
|
|
147
|
+
const tool = getBundledTools().find((t) => t.name === toolName);
|
|
143
148
|
const bundledVersion = tool?.version || null;
|
|
144
149
|
|
|
145
150
|
if (!installedVersion || !bundledVersion) {
|
|
@@ -176,10 +181,12 @@ export function getToolsWithUpdates(): ToolUpdateInfo[] {
|
|
|
176
181
|
for (const tool of bundledTools) {
|
|
177
182
|
// Find if any of the tool's required skills are installed
|
|
178
183
|
const requiredSkills = tool.includes.skills
|
|
179
|
-
.filter(s => s.required)
|
|
180
|
-
.map(s => s.name);
|
|
184
|
+
.filter((s) => s.required)
|
|
185
|
+
.map((s) => s.name);
|
|
181
186
|
|
|
182
|
-
const isInstalled = requiredSkills.some(
|
|
187
|
+
const isInstalled = requiredSkills.some(
|
|
188
|
+
(skillName) => skillName in installedTools,
|
|
189
|
+
);
|
|
183
190
|
|
|
184
191
|
if (isInstalled) {
|
|
185
192
|
const updateStatus = getToolUpdateStatus(tool.name);
|