@orderful/droid 0.2.0 → 0.3.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.
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
2
- import { existsSync, mkdirSync, rmSync, writeFileSync, readFileSync } from 'fs';
2
+ import { existsSync, mkdirSync, rmSync, writeFileSync, readFileSync, readdirSync } from 'fs';
3
3
  import { join } from 'path';
4
4
  import { tmpdir } from 'os';
5
5
  import { homedir } from 'os';
@@ -13,6 +13,19 @@ import {
13
13
  getBundledSkillsDir,
14
14
  } from './skills.js';
15
15
 
16
+ /**
17
+ * Parse YAML frontmatter from markdown content
18
+ */
19
+ function parseFrontmatter(content: string): Record<string, unknown> | null {
20
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
21
+ if (!match) return null;
22
+ try {
23
+ return YAML.parse(match[1]);
24
+ } catch {
25
+ return null;
26
+ }
27
+ }
28
+
16
29
  describe('getSkillsInstallPath', () => {
17
30
  it('should return Claude Code path', () => {
18
31
  const path = getSkillsInstallPath(AITool.ClaudeCode);
@@ -136,3 +149,64 @@ describe('skill manifest parsing', () => {
136
149
  expect(parsed.provides_output).toBeUndefined();
137
150
  });
138
151
  });
152
+
153
+ describe('bundled skills validation', () => {
154
+ it('all skills should have SKILL.md with valid frontmatter', () => {
155
+ const skillsDir = getBundledSkillsDir();
156
+ const skillDirs = readdirSync(skillsDir, { withFileTypes: true })
157
+ .filter((d) => d.isDirectory())
158
+ .map((d) => d.name);
159
+
160
+ for (const skillName of skillDirs) {
161
+ const skillMdPath = join(skillsDir, skillName, 'SKILL.md');
162
+ expect(existsSync(skillMdPath)).toBe(true);
163
+
164
+ const content = readFileSync(skillMdPath, 'utf-8');
165
+ const frontmatter = parseFrontmatter(content);
166
+
167
+ expect(frontmatter).not.toBeNull();
168
+ expect(frontmatter?.name).toBe(skillName);
169
+ expect(typeof frontmatter?.description).toBe('string');
170
+ }
171
+ });
172
+
173
+ it('all skills should have SKILL.yaml manifest', () => {
174
+ const skillsDir = getBundledSkillsDir();
175
+ const skillDirs = readdirSync(skillsDir, { withFileTypes: true })
176
+ .filter((d) => d.isDirectory())
177
+ .map((d) => d.name);
178
+
179
+ for (const skillName of skillDirs) {
180
+ const yamlPath = join(skillsDir, skillName, 'SKILL.yaml');
181
+ expect(existsSync(yamlPath)).toBe(true);
182
+
183
+ const content = readFileSync(yamlPath, 'utf-8');
184
+ const manifest = YAML.parse(content);
185
+
186
+ expect(manifest.name).toBe(skillName);
187
+ expect(typeof manifest.description).toBe('string');
188
+ expect(typeof manifest.version).toBe('string');
189
+ }
190
+ });
191
+
192
+ it('SKILL.md frontmatter should match SKILL.yaml', () => {
193
+ const skillsDir = getBundledSkillsDir();
194
+ const skillDirs = readdirSync(skillsDir, { withFileTypes: true })
195
+ .filter((d) => d.isDirectory())
196
+ .map((d) => d.name);
197
+
198
+ for (const skillName of skillDirs) {
199
+ const mdPath = join(skillsDir, skillName, 'SKILL.md');
200
+ const yamlPath = join(skillsDir, skillName, 'SKILL.yaml');
201
+
202
+ const mdContent = readFileSync(mdPath, 'utf-8');
203
+ const yamlContent = readFileSync(yamlPath, 'utf-8');
204
+
205
+ const frontmatter = parseFrontmatter(mdContent);
206
+ const manifest = YAML.parse(yamlContent);
207
+
208
+ expect(frontmatter?.name).toBe(manifest.name);
209
+ expect(frontmatter?.description).toBe(manifest.description);
210
+ }
211
+ });
212
+ });
package/src/lib/types.ts CHANGED
@@ -45,6 +45,11 @@ export interface InstalledSkill {
45
45
  installed_at: string;
46
46
  }
47
47
 
48
+ export interface SkillExample {
49
+ title: string;
50
+ code: string;
51
+ }
52
+
48
53
  export interface SkillManifest {
49
54
  name: string;
50
55
  description: string;
@@ -54,6 +59,8 @@ export interface SkillManifest {
54
59
  config_schema?: Record<string, ConfigOption>;
55
60
  // Skills can declare they provide an output target
56
61
  provides_output?: boolean;
62
+ // Usage examples to show in the TUI
63
+ examples?: SkillExample[];
57
64
  }
58
65
 
59
66
  export interface ConfigOption {
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from 'bun:test';
2
- import { getVersion } from './version.js';
2
+ import { getVersion, compareSemver } from './version.js';
3
3
 
4
4
  describe('getVersion', () => {
5
5
  it('should return a version string', () => {
@@ -21,3 +21,22 @@ describe('getVersion', () => {
21
21
  expect(version).not.toBe('0.0.0');
22
22
  });
23
23
  });
24
+
25
+ describe('compareSemver', () => {
26
+ it('should return 1 when first version is greater', () => {
27
+ expect(compareSemver('1.0.0', '0.9.9')).toBe(1);
28
+ expect(compareSemver('0.2.1', '0.2.0')).toBe(1);
29
+ expect(compareSemver('2.0.0', '1.9.9')).toBe(1);
30
+ });
31
+
32
+ it('should return -1 when first version is lesser', () => {
33
+ expect(compareSemver('0.9.9', '1.0.0')).toBe(-1);
34
+ expect(compareSemver('0.2.0', '0.2.1')).toBe(-1);
35
+ expect(compareSemver('1.9.9', '2.0.0')).toBe(-1);
36
+ });
37
+
38
+ it('should return 0 when versions are equal', () => {
39
+ expect(compareSemver('1.0.0', '1.0.0')).toBe(0);
40
+ expect(compareSemver('0.2.1', '0.2.1')).toBe(0);
41
+ });
42
+ });
@@ -19,6 +19,23 @@ export function getVersion(): string {
19
19
  }
20
20
  }
21
21
 
22
+ /**
23
+ * Compare two semver versions
24
+ * Returns: 1 if a > b, -1 if a < b, 0 if equal
25
+ */
26
+ export function compareSemver(a: string, b: string): number {
27
+ const partsA = a.split('.').map(Number);
28
+ const partsB = b.split('.').map(Number);
29
+
30
+ for (let i = 0; i < 3; i++) {
31
+ const numA = partsA[i] || 0;
32
+ const numB = partsB[i] || 0;
33
+ if (numA > numB) return 1;
34
+ if (numA < numB) return -1;
35
+ }
36
+ return 0;
37
+ }
38
+
22
39
  /**
23
40
  * Check for updates (non-blocking)
24
41
  * Shows a message if a new version is available
@@ -33,7 +50,8 @@ export async function checkForUpdates(): Promise<void> {
33
50
  timeout: 3000,
34
51
  }).trim();
35
52
 
36
- if (latestVersion && latestVersion !== currentVersion) {
53
+ // Only show update message if latest is actually newer
54
+ if (latestVersion && compareSemver(latestVersion, currentVersion) > 0) {
37
55
  console.log(
38
56
  chalk.yellow(
39
57
  `\n⚠️ A new version of droid is available (${currentVersion} → ${latestVersion})`
@@ -1,3 +1,11 @@
1
+ ---
2
+ name: comments
3
+ description: Inline conversation in any file via @droid/@user markers
4
+ globs:
5
+ - "**/*"
6
+ alwaysApply: false
7
+ ---
8
+
1
9
  # Comments Skill
2
10
 
3
11
  Enable inline conversation in any file using `> @droid` and `> @{user}` markers.
@@ -16,3 +16,35 @@ config_schema:
16
16
  type: boolean
17
17
  description: Keep original comments after addressing (vs removing them)
18
18
  default: false
19
+ examples:
20
+ - title: "Action request"
21
+ code: |
22
+ // @droid use PascalCase for enum keys
23
+ enum Status {
24
+ PENDING = 'pending'
25
+ }
26
+ - title: "Ask a question"
27
+ code: |
28
+ > @droid Should we cache this?
29
+
30
+ > @fry Yes, add Redis caching here...
31
+ - title: "TODO with context"
32
+ code: |
33
+ // @droid TODO: refactor to async/await
34
+ // The callback pattern is unwieldy
35
+ function fetchData(callback) {
36
+ api.get('/data', callback);
37
+ }
38
+ - title: "Code review note"
39
+ code: |
40
+ // @droid potential memory leak here
41
+ // Clear interval on unmount?
42
+ useEffect(() => {
43
+ setInterval(poll, 1000);
44
+ }, []);
45
+ - title: "Multi-line discussion"
46
+ code: |
47
+ > @droid Best approach for pagination?
48
+ > We have ~10k records.
49
+
50
+ > @fry Use cursor-based, limit 50.
package/tsconfig.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "compilerOptions": {
3
3
  "target": "ES2022",
4
- "module": "NodeNext",
5
- "moduleResolution": "NodeNext",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
6
  "outDir": "./dist",
7
7
  "rootDir": "./src",
8
8
  "strict": true,
@@ -12,7 +12,9 @@
12
12
  "declaration": true,
13
13
  "declarationMap": true,
14
14
  "sourceMap": true,
15
- "resolveJsonModule": true
15
+ "resolveJsonModule": true,
16
+ "jsx": "react-jsx",
17
+ "jsxImportSource": "react"
16
18
  },
17
19
  "include": ["src/**/*"],
18
20
  "exclude": ["node_modules", "dist", "**/*.test.ts"]