@orderful/droid 0.37.0 → 0.38.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/.claude-plugin/marketplace.json +1 -118
- package/.claude-plugin/plugin.json +49 -0
- package/AGENTS.md +4 -0
- package/CHANGELOG.md +43 -0
- package/README.md +53 -39
- package/dist/bin/droid.js +525 -212
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/tui/components/PlatformBadges.d.ts.map +1 -1
- package/dist/commands/tui/components/SettingsDetails.d.ts.map +1 -1
- package/dist/commands/tui/hooks/useAppUpdate.d.ts.map +1 -1
- package/dist/commands/tui/views/SetupScreen.d.ts.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/index.js +345 -186
- package/dist/lib/agents.d.ts +4 -2
- package/dist/lib/agents.d.ts.map +1 -1
- package/dist/lib/migrations.d.ts.map +1 -1
- package/dist/lib/platform.codex.d.ts +36 -0
- package/dist/lib/platform.codex.d.ts.map +1 -0
- package/dist/lib/platforms.d.ts +30 -24
- package/dist/lib/platforms.d.ts.map +1 -1
- package/dist/lib/skills.d.ts +4 -2
- package/dist/lib/skills.d.ts.map +1 -1
- package/dist/lib/types.d.ts +2 -1
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/tools/brain/.claude-plugin/plugin.json +8 -1
- package/dist/tools/brain/TOOL.yaml +1 -1
- package/dist/tools/brain/skills/brain/SKILL.md +6 -3
- package/dist/tools/brain/skills/brain/references/workflows.md +9 -5
- package/dist/tools/brain/skills/brain-obsidian/SKILL.md +2 -0
- package/dist/tools/coach/.claude-plugin/plugin.json +6 -0
- package/dist/tools/coach/skills/coach/SKILL.md +3 -0
- package/dist/tools/code-review/.claude-plugin/plugin.json +12 -0
- package/dist/tools/code-review/skills/code-review/SKILL.md +2 -0
- package/dist/tools/codex/.claude-plugin/plugin.json +9 -0
- package/dist/tools/codex/skills/codex/SKILL.md +3 -0
- package/dist/tools/comments/.claude-plugin/plugin.json +6 -0
- package/dist/tools/comments/skills/comments/SKILL.md +5 -0
- package/dist/tools/droid/.claude-plugin/plugin.json +8 -1
- package/dist/tools/droid/TOOL.yaml +4 -2
- package/dist/tools/droid/commands/setup.md +125 -0
- package/dist/tools/droid/skills/droid/SKILL.md +117 -2
- package/dist/tools/plan/.claude-plugin/plugin.json +6 -0
- package/dist/tools/plan/skills/plan/SKILL.md +2 -0
- package/dist/tools/project/.claude-plugin/plugin.json +6 -0
- package/dist/tools/project/skills/project/SKILL.md +3 -0
- package/dist/tools/tech-design/.claude-plugin/plugin.json +7 -1
- package/dist/tools/tech-design/TOOL.yaml +1 -1
- package/dist/tools/tech-design/commands/tech-design.md +2 -0
- package/dist/tools/tech-design/skills/tech-design/SKILL.md +39 -9
- package/dist/tools/tech-design/skills/tech-design/references/publish.md +272 -216
- package/dist/tools/tech-design/skills/tech-design/references/start.md +50 -20
- package/dist/tools/wrapup/.claude-plugin/plugin.json +6 -0
- package/dist/tools/wrapup/skills/wrapup/SKILL.md +2 -0
- package/package.json +1 -1
- package/scripts/build-plugins.ts +154 -6
- package/src/bin/droid.ts +16 -0
- package/src/commands/setup.ts +107 -2
- package/src/commands/tui/components/PlatformBadges.tsx +1 -0
- package/src/commands/tui/components/SettingsDetails.tsx +1 -0
- package/src/commands/tui/hooks/useAppUpdate.ts +21 -1
- package/src/commands/tui/views/SetupScreen.tsx +10 -1
- package/src/commands/update.ts +21 -1
- package/src/lib/agents.ts +13 -2
- package/src/lib/migrations.ts +81 -9
- package/src/lib/platform.codex.ts +131 -0
- package/src/lib/platforms.ts +127 -6
- package/src/lib/skills.ts +53 -6
- package/src/lib/types.ts +1 -0
- package/src/tools/brain/.claude-plugin/plugin.json +8 -1
- package/src/tools/brain/TOOL.yaml +1 -1
- package/src/tools/brain/skills/brain/SKILL.md +6 -3
- package/src/tools/brain/skills/brain/references/workflows.md +9 -5
- package/src/tools/brain/skills/brain-obsidian/SKILL.md +2 -0
- package/src/tools/coach/.claude-plugin/plugin.json +6 -0
- package/src/tools/coach/skills/coach/SKILL.md +3 -0
- package/src/tools/code-review/.claude-plugin/plugin.json +12 -0
- package/src/tools/code-review/skills/code-review/SKILL.md +2 -0
- package/src/tools/codex/.claude-plugin/plugin.json +9 -0
- package/src/tools/codex/skills/codex/SKILL.md +3 -0
- package/src/tools/comments/.claude-plugin/plugin.json +6 -0
- package/src/tools/comments/skills/comments/SKILL.md +5 -0
- package/src/tools/droid/.claude-plugin/plugin.json +8 -1
- package/src/tools/droid/TOOL.yaml +4 -2
- package/src/tools/droid/commands/setup.md +125 -0
- package/src/tools/droid/skills/droid/SKILL.md +117 -2
- package/src/tools/plan/.claude-plugin/plugin.json +6 -0
- package/src/tools/plan/skills/plan/SKILL.md +2 -0
- package/src/tools/project/.claude-plugin/plugin.json +6 -0
- package/src/tools/project/skills/project/SKILL.md +3 -0
- package/src/tools/tech-design/.claude-plugin/plugin.json +7 -1
- package/src/tools/tech-design/TOOL.yaml +1 -1
- package/src/tools/tech-design/commands/tech-design.md +2 -0
- package/src/tools/tech-design/skills/tech-design/SKILL.md +39 -9
- package/src/tools/tech-design/skills/tech-design/references/publish.md +272 -216
- package/src/tools/tech-design/skills/tech-design/references/start.md +50 -20
- package/src/tools/wrapup/.claude-plugin/plugin.json +6 -0
- package/src/tools/wrapup/skills/wrapup/SKILL.md +2 -0
package/src/commands/update.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { execSync } from 'child_process';
|
|
2
|
+
import { execSync, spawnSync } from 'child_process';
|
|
3
3
|
import { getVersion } from '../lib/version';
|
|
4
4
|
import { updateAllSkills } from '../lib/skills';
|
|
5
|
+
import { getAutoUpdateConfig } from '../lib/config';
|
|
5
6
|
|
|
6
7
|
interface UpdateOptions {
|
|
7
8
|
tools?: boolean;
|
|
@@ -83,6 +84,25 @@ export async function updateCommand(
|
|
|
83
84
|
execSync('npm install -g @orderful/droid@latest', { stdio: 'inherit' });
|
|
84
85
|
|
|
85
86
|
console.log(chalk.green(`\n✓ Updated to v${latestVersion}`));
|
|
87
|
+
|
|
88
|
+
// Spawn NEW binary to sync tools (has new bundled manifests)
|
|
89
|
+
// Only if auto-update tools is enabled
|
|
90
|
+
const autoUpdateConfig = getAutoUpdateConfig();
|
|
91
|
+
if (autoUpdateConfig.tools) {
|
|
92
|
+
console.log(chalk.bold('\n🤖 Syncing tools...\n'));
|
|
93
|
+
const toolResult = spawnSync('droid', ['update', '--tools'], {
|
|
94
|
+
stdio: 'inherit',
|
|
95
|
+
timeout: 60000, // 60s timeout for tool sync
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
if (toolResult.error || toolResult.status !== 0) {
|
|
99
|
+
console.log(
|
|
100
|
+
chalk.yellow(
|
|
101
|
+
'Tool sync incomplete - run `droid update --tools` to retry',
|
|
102
|
+
),
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
86
106
|
} catch {
|
|
87
107
|
// Package might not be published yet
|
|
88
108
|
console.log(chalk.yellow('\n⚠ Could not check for updates'));
|
package/src/lib/agents.ts
CHANGED
|
@@ -12,8 +12,9 @@ const BUNDLED_TOOLS_DIR = join(__dirname, '../tools');
|
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Get the installation path for agents based on platform
|
|
15
|
+
* Returns null for platforms that don't support agents (e.g., OpenAI Codex)
|
|
15
16
|
*/
|
|
16
|
-
export function getAgentsInstallPath(platform: Platform): string {
|
|
17
|
+
export function getAgentsInstallPath(platform: Platform): string | null {
|
|
17
18
|
return getAgentsPath(platform);
|
|
18
19
|
}
|
|
19
20
|
|
|
@@ -140,8 +141,9 @@ export function getAgentStatusDisplay(status?: string): string {
|
|
|
140
141
|
|
|
141
142
|
/**
|
|
142
143
|
* Get installed agents directory for the configured platform
|
|
144
|
+
* Returns null if platform doesn't support agents
|
|
143
145
|
*/
|
|
144
|
-
export function getInstalledAgentsDir(): string {
|
|
146
|
+
export function getInstalledAgentsDir(): string | null {
|
|
145
147
|
const config = loadConfig();
|
|
146
148
|
return getAgentsInstallPath(config.platform);
|
|
147
149
|
}
|
|
@@ -152,6 +154,7 @@ export function getInstalledAgentsDir(): string {
|
|
|
152
154
|
export function isAgentInstalled(agentName: string): boolean {
|
|
153
155
|
const config = loadConfig();
|
|
154
156
|
const agentsDir = getAgentsInstallPath(config.platform);
|
|
157
|
+
if (!agentsDir) return false; // Platform doesn't support agents
|
|
155
158
|
const agentPath = join(agentsDir, `${agentName}.md`);
|
|
156
159
|
return existsSync(agentPath);
|
|
157
160
|
}
|
|
@@ -235,6 +238,9 @@ export function installAgentFromPath(
|
|
|
235
238
|
|
|
236
239
|
// Ensure agents directory exists
|
|
237
240
|
const agentsDir = getAgentsInstallPath(targetPlatform);
|
|
241
|
+
if (!agentsDir) {
|
|
242
|
+
return { success: false, message: `Platform ${targetPlatform} does not support agents` };
|
|
243
|
+
}
|
|
238
244
|
if (!existsSync(agentsDir)) {
|
|
239
245
|
mkdirSync(agentsDir, { recursive: true });
|
|
240
246
|
}
|
|
@@ -300,6 +306,11 @@ export function uninstallAgent(
|
|
|
300
306
|
const config = loadConfig();
|
|
301
307
|
const targetPlatform = platform ?? config.platform;
|
|
302
308
|
const agentsDir = getAgentsInstallPath(targetPlatform);
|
|
309
|
+
|
|
310
|
+
if (!agentsDir) {
|
|
311
|
+
return { success: true, message: `Platform ${targetPlatform} does not support agents` };
|
|
312
|
+
}
|
|
313
|
+
|
|
303
314
|
const agentPath = join(agentsDir, `${agentName}.md`);
|
|
304
315
|
|
|
305
316
|
if (!existsSync(agentPath)) {
|
package/src/lib/migrations.ts
CHANGED
|
@@ -305,24 +305,26 @@ function createClaudeCodeCommandCleanupMigration(version: string): Migration {
|
|
|
305
305
|
// Clean up Claude Code commands directory regardless of current platform
|
|
306
306
|
// Users may have switched platforms, leaving orphaned commands
|
|
307
307
|
const commandsPath = getCommandsPath(Platform.ClaudeCode);
|
|
308
|
-
|
|
308
|
+
// ClaudeCode always has a commands path, but check for null to satisfy TypeScript
|
|
309
|
+
if (!commandsPath || !existsSync(commandsPath)) {
|
|
309
310
|
return;
|
|
310
311
|
}
|
|
311
312
|
|
|
312
|
-
// Get all bundled tools to
|
|
313
|
+
// Get all bundled tools to determine which commands droid manages
|
|
313
314
|
const bundledTools = getBundledTools();
|
|
314
|
-
const
|
|
315
|
+
const deletableDroidCommands = new Set<string>();
|
|
315
316
|
|
|
316
|
-
// Collect
|
|
317
|
+
// Collect non-alias commands that droid manages (safe to delete)
|
|
317
318
|
for (const tool of bundledTools) {
|
|
318
319
|
for (const cmd of tool.includes.commands) {
|
|
319
|
-
if (
|
|
320
|
-
|
|
320
|
+
if (!cmd.is_alias) {
|
|
321
|
+
deletableDroidCommands.add(cmd.name);
|
|
321
322
|
}
|
|
322
323
|
}
|
|
323
324
|
}
|
|
324
325
|
|
|
325
|
-
// Check each command file and remove non-
|
|
326
|
+
// Check each command file and remove only droid's non-alias commands
|
|
327
|
+
// User-created commands (not in deletableDroidCommands) are preserved
|
|
326
328
|
const commandFiles = readdirSync(commandsPath, { withFileTypes: true })
|
|
327
329
|
.filter((dirent) => dirent.isFile() && dirent.name.endsWith('.md'))
|
|
328
330
|
.map((dirent) => dirent.name);
|
|
@@ -330,8 +332,7 @@ function createClaudeCodeCommandCleanupMigration(version: string): Migration {
|
|
|
330
332
|
for (const file of commandFiles) {
|
|
331
333
|
const commandName = file.replace('.md', '');
|
|
332
334
|
|
|
333
|
-
|
|
334
|
-
if (!aliasCommands.has(commandName)) {
|
|
335
|
+
if (deletableDroidCommands.has(commandName)) {
|
|
335
336
|
const commandFilePath = join(commandsPath, file);
|
|
336
337
|
try {
|
|
337
338
|
rmSync(commandFilePath);
|
|
@@ -491,6 +492,76 @@ function copyDirRecursive(src: string, dest: string): void {
|
|
|
491
492
|
}
|
|
492
493
|
}
|
|
493
494
|
|
|
495
|
+
/**
|
|
496
|
+
* Migration: Clean up droid-managed skills from OpenCode's platform-specific directory
|
|
497
|
+
*
|
|
498
|
+
* OpenCode checks ~/.config/opencode/skill/ before ~/.claude/skills/, so old skills
|
|
499
|
+
* there shadow the unified location. This migration removes ONLY droid-managed skills
|
|
500
|
+
* from OpenCode's directory, preserving user-authored skills.
|
|
501
|
+
*
|
|
502
|
+
* Safety: Only deletes skills that are bundled with droid. User-created skills are preserved.
|
|
503
|
+
*/
|
|
504
|
+
function createOpenCodeSkillsCleanupMigration(version: string): Migration {
|
|
505
|
+
return {
|
|
506
|
+
version,
|
|
507
|
+
description: 'Remove droid-managed skills from OpenCode platform-specific directory',
|
|
508
|
+
up: () => {
|
|
509
|
+
const oldOpenCodeSkillsPath = join(
|
|
510
|
+
homedir(),
|
|
511
|
+
'.config',
|
|
512
|
+
'opencode',
|
|
513
|
+
'skill',
|
|
514
|
+
);
|
|
515
|
+
const unifiedSkillsPath = join(homedir(), '.claude', 'skills');
|
|
516
|
+
|
|
517
|
+
// Skip if OpenCode skills directory doesn't exist
|
|
518
|
+
if (!existsSync(oldOpenCodeSkillsPath)) {
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Get all bundled tools to determine which skills droid manages
|
|
523
|
+
const bundledTools = getBundledTools();
|
|
524
|
+
const droidManagedSkills = new Set<string>();
|
|
525
|
+
|
|
526
|
+
// Collect skill names that droid manages (safe to delete)
|
|
527
|
+
for (const tool of bundledTools) {
|
|
528
|
+
for (const skill of tool.includes.skills) {
|
|
529
|
+
droidManagedSkills.add(skill.name);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Get all skill directories in OpenCode's location
|
|
534
|
+
const skillDirs = readdirSync(oldOpenCodeSkillsPath, { withFileTypes: true })
|
|
535
|
+
.filter((dirent) => dirent.isDirectory())
|
|
536
|
+
.map((dirent) => dirent.name);
|
|
537
|
+
|
|
538
|
+
for (const skillName of skillDirs) {
|
|
539
|
+
// SAFETY: Only delete if this is a droid-managed skill
|
|
540
|
+
if (!droidManagedSkills.has(skillName)) {
|
|
541
|
+
continue; // Preserve user-authored skills
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Only delete if unified version exists (unified is source of truth)
|
|
545
|
+
const unifiedPath = join(unifiedSkillsPath, skillName);
|
|
546
|
+
if (!existsSync(unifiedPath)) {
|
|
547
|
+
continue; // Keep OpenCode version if no unified version exists
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Delete the OpenCode-specific copy (unified version will be used)
|
|
551
|
+
const oldPath = join(oldOpenCodeSkillsPath, skillName);
|
|
552
|
+
try {
|
|
553
|
+
rmSync(oldPath, { recursive: true });
|
|
554
|
+
} catch (error) {
|
|
555
|
+
// Non-fatal: Log warning but continue with other skills
|
|
556
|
+
console.warn(
|
|
557
|
+
`Warning: Could not remove OpenCode skill ${skillName}: ${error}`,
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
},
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
|
|
494
565
|
/**
|
|
495
566
|
* Registry of package-level migrations
|
|
496
567
|
* These run when the @orderful/droid npm package updates
|
|
@@ -506,6 +577,7 @@ const PACKAGE_MIGRATIONS: Migration[] = [
|
|
|
506
577
|
createClaudeCodeCommandCleanupMigration('0.28.1'),
|
|
507
578
|
createOpenCodePluginCleanupMigration('0.29.2'),
|
|
508
579
|
createUnifiedSkillsPathMigration('0.30.0'),
|
|
580
|
+
createOpenCodeSkillsCleanupMigration('0.37.1'),
|
|
509
581
|
];
|
|
510
582
|
|
|
511
583
|
/**
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI Codex platform-specific logic
|
|
3
|
+
*
|
|
4
|
+
* Codex reads skills from ~/.codex/skills/ but droid installs to ~/.claude/skills/ (unified path).
|
|
5
|
+
* We create symlinks from Codex's location to the unified path.
|
|
6
|
+
*
|
|
7
|
+
* Key differences from other platforms:
|
|
8
|
+
* - Skills only (no commands or agents)
|
|
9
|
+
* - Uses symlinks instead of copying files
|
|
10
|
+
* - Must preserve user's own Codex skills (not managed by droid)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { join, dirname } from 'path';
|
|
14
|
+
import { homedir } from 'os';
|
|
15
|
+
import {
|
|
16
|
+
existsSync,
|
|
17
|
+
mkdirSync,
|
|
18
|
+
rmSync,
|
|
19
|
+
symlinkSync,
|
|
20
|
+
lstatSync,
|
|
21
|
+
readdirSync,
|
|
22
|
+
readlinkSync,
|
|
23
|
+
} from 'fs';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Unified skills path - where droid installs skills
|
|
27
|
+
*/
|
|
28
|
+
const UNIFIED_SKILLS_PATH = join(homedir(), '.claude', 'skills');
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Codex skills path - where Codex reads skills from
|
|
32
|
+
*/
|
|
33
|
+
export const CODEX_SKILLS_PATH = join(homedir(), '.codex', 'skills');
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Create a symlink for a skill in the Codex skills directory
|
|
37
|
+
* Symlinks from ~/.codex/skills/{name} → ~/.claude/skills/{name}
|
|
38
|
+
*
|
|
39
|
+
* Safety guarantees:
|
|
40
|
+
* - Only creates symlinks for skills that exist in unified path
|
|
41
|
+
* - Never deletes real directories (only replaces existing symlinks)
|
|
42
|
+
* - Skips if symlink already points to correct location
|
|
43
|
+
*/
|
|
44
|
+
export function createCodexSymlink(skillName: string): void {
|
|
45
|
+
const source = join(UNIFIED_SKILLS_PATH, skillName);
|
|
46
|
+
const target = join(CODEX_SKILLS_PATH, skillName);
|
|
47
|
+
|
|
48
|
+
// Ensure source exists
|
|
49
|
+
if (!existsSync(source)) {
|
|
50
|
+
console.warn(`Warning: Cannot create Codex symlink - source skill not found: ${source}`);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Ensure ~/.codex/skills/ directory exists
|
|
55
|
+
if (!existsSync(dirname(target))) {
|
|
56
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Only touch symlinks we manage (don't delete user's real directories)
|
|
60
|
+
if (existsSync(target)) {
|
|
61
|
+
try {
|
|
62
|
+
const stat = lstatSync(target);
|
|
63
|
+
if (stat.isSymbolicLink()) {
|
|
64
|
+
// Check if it already points to the correct location
|
|
65
|
+
const currentTarget = readlinkSync(target);
|
|
66
|
+
if (currentTarget === source) {
|
|
67
|
+
return; // Already correct, nothing to do
|
|
68
|
+
}
|
|
69
|
+
rmSync(target); // Wrong target, replace it
|
|
70
|
+
} else {
|
|
71
|
+
// Real file or directory - don't touch it
|
|
72
|
+
console.warn(`Warning: ${target} exists and is not a symlink - skipping to preserve user content`);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.warn(`Warning: Could not check ${target}: ${error}`);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Create symlink
|
|
82
|
+
try {
|
|
83
|
+
symlinkSync(source, target);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.warn(`Warning: Could not create Codex symlink ${target} → ${source}: ${error}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Remove a symlink for a skill from the Codex skills directory
|
|
91
|
+
* Only removes if it's a symlink (preserves user's real directories)
|
|
92
|
+
*/
|
|
93
|
+
export function removeCodexSymlink(skillName: string): void {
|
|
94
|
+
const target = join(CODEX_SKILLS_PATH, skillName);
|
|
95
|
+
|
|
96
|
+
if (!existsSync(target)) {
|
|
97
|
+
return; // Nothing to remove
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const stat = lstatSync(target);
|
|
102
|
+
if (stat.isSymbolicLink()) {
|
|
103
|
+
rmSync(target);
|
|
104
|
+
}
|
|
105
|
+
} catch (error) {
|
|
106
|
+
console.warn(`Warning: Could not remove Codex symlink ${target}: ${error}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Remove all droid-managed Codex symlinks (when Codex is removed or ignored)
|
|
112
|
+
* Only removes symlinks, preserves user's real directories and .system folder
|
|
113
|
+
*/
|
|
114
|
+
export function removeAllCodexSymlinks(): void {
|
|
115
|
+
if (!existsSync(CODEX_SKILLS_PATH)) {
|
|
116
|
+
return; // Nothing to clean up
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const entries = readdirSync(CODEX_SKILLS_PATH, { withFileTypes: true });
|
|
120
|
+
for (const entry of entries) {
|
|
121
|
+
// Only remove symlinks, not the .system directory or other files
|
|
122
|
+
const entryPath = join(CODEX_SKILLS_PATH, entry.name);
|
|
123
|
+
try {
|
|
124
|
+
if (lstatSync(entryPath).isSymbolicLink()) {
|
|
125
|
+
rmSync(entryPath);
|
|
126
|
+
}
|
|
127
|
+
} catch (error) {
|
|
128
|
+
console.warn(`Warning: Could not remove Codex symlink ${entryPath}: ${error}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
package/src/lib/platforms.ts
CHANGED
|
@@ -4,9 +4,20 @@ import { execSync } from 'child_process';
|
|
|
4
4
|
import { existsSync } from 'fs';
|
|
5
5
|
import { Platform, type DroidConfig } from './types';
|
|
6
6
|
|
|
7
|
+
// Re-export Codex-specific functions from platform.codex.ts
|
|
8
|
+
export {
|
|
9
|
+
CODEX_SKILLS_PATH,
|
|
10
|
+
createCodexSymlink,
|
|
11
|
+
removeCodexSymlink,
|
|
12
|
+
removeAllCodexSymlinks,
|
|
13
|
+
} from './platform.codex';
|
|
14
|
+
|
|
15
|
+
import { createCodexSymlink } from './platform.codex';
|
|
16
|
+
|
|
7
17
|
/**
|
|
8
18
|
* Unified skills path - all platforms read from ~/.claude/skills/
|
|
9
19
|
* This is the Agent Skills standard location supported by Claude Code, OpenCode, and Cursor.
|
|
20
|
+
* OpenAI Codex reads from ~/.codex/skills/, so we create symlinks there (see platform.codex.ts).
|
|
10
21
|
*/
|
|
11
22
|
const UNIFIED_SKILLS_PATH = join(homedir(), '.claude', 'skills');
|
|
12
23
|
|
|
@@ -16,8 +27,17 @@ const UNIFIED_SKILLS_PATH = join(homedir(), '.claude', 'skills');
|
|
|
16
27
|
*
|
|
17
28
|
* Note: Skills are unified to ~/.claude/skills/ (all platforms read this location).
|
|
18
29
|
* Commands and agents remain platform-specific as there's no cross-platform compatibility.
|
|
30
|
+
* OpenAI Codex only supports skills (via symlinks) - no commands, agents, or config integration.
|
|
19
31
|
*/
|
|
20
|
-
export const PLATFORM_PATHS
|
|
32
|
+
export const PLATFORM_PATHS: Record<
|
|
33
|
+
Platform,
|
|
34
|
+
{
|
|
35
|
+
skills: string;
|
|
36
|
+
commands: string | null;
|
|
37
|
+
agents: string | null;
|
|
38
|
+
config: string | null;
|
|
39
|
+
}
|
|
40
|
+
> = {
|
|
21
41
|
[Platform.ClaudeCode]: {
|
|
22
42
|
skills: UNIFIED_SKILLS_PATH,
|
|
23
43
|
commands: join(homedir(), '.claude', 'commands'),
|
|
@@ -38,9 +58,16 @@ export const PLATFORM_PATHS = {
|
|
|
38
58
|
agents: join(homedir(), '.cursor', 'agents'),
|
|
39
59
|
config: join(homedir(), '.cursor', 'rules'),
|
|
40
60
|
},
|
|
41
|
-
|
|
61
|
+
[Platform.OpenAICodex]: {
|
|
62
|
+
// Codex reads from ~/.codex/skills/ but we install to unified path and symlink
|
|
63
|
+
skills: UNIFIED_SKILLS_PATH,
|
|
64
|
+
commands: null, // Not supported - Codex has built-in commands only
|
|
65
|
+
agents: null, // Not supported - Codex is single-agent
|
|
66
|
+
config: null, // No config file integration
|
|
67
|
+
},
|
|
68
|
+
};
|
|
42
69
|
|
|
43
|
-
export type PlatformPaths = typeof PLATFORM_PATHS[Platform];
|
|
70
|
+
export type PlatformPaths = (typeof PLATFORM_PATHS)[Platform];
|
|
44
71
|
|
|
45
72
|
/**
|
|
46
73
|
* Get all paths for a platform
|
|
@@ -58,25 +85,42 @@ export function getSkillsPath(platform: Platform): string {
|
|
|
58
85
|
|
|
59
86
|
/**
|
|
60
87
|
* Get commands install path for a platform
|
|
88
|
+
* Returns null for platforms that don't support custom commands (e.g., OpenAI Codex)
|
|
61
89
|
*/
|
|
62
|
-
export function getCommandsPath(platform: Platform): string {
|
|
90
|
+
export function getCommandsPath(platform: Platform): string | null {
|
|
63
91
|
return PLATFORM_PATHS[platform].commands;
|
|
64
92
|
}
|
|
65
93
|
|
|
66
94
|
/**
|
|
67
95
|
* Get agents install path for a platform
|
|
96
|
+
* Returns null for platforms that don't support custom agents (e.g., OpenAI Codex)
|
|
68
97
|
*/
|
|
69
|
-
export function getAgentsPath(platform: Platform): string {
|
|
98
|
+
export function getAgentsPath(platform: Platform): string | null {
|
|
70
99
|
return PLATFORM_PATHS[platform].agents;
|
|
71
100
|
}
|
|
72
101
|
|
|
73
102
|
/**
|
|
74
103
|
* Get platform config file path (CLAUDE.md or AGENTS.md)
|
|
104
|
+
* Returns null for platforms without config file integration (e.g., OpenAI Codex)
|
|
75
105
|
*/
|
|
76
|
-
export function getConfigPath(platform: Platform): string {
|
|
106
|
+
export function getConfigPath(platform: Platform): string | null {
|
|
77
107
|
return PLATFORM_PATHS[platform].config;
|
|
78
108
|
}
|
|
79
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Check if a platform supports custom commands
|
|
112
|
+
*/
|
|
113
|
+
export function platformSupportsCommands(platform: Platform): boolean {
|
|
114
|
+
return PLATFORM_PATHS[platform].commands !== null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Check if a platform supports custom agents
|
|
119
|
+
*/
|
|
120
|
+
export function platformSupportsAgents(platform: Platform): boolean {
|
|
121
|
+
return PLATFORM_PATHS[platform].agents !== null;
|
|
122
|
+
}
|
|
123
|
+
|
|
80
124
|
/**
|
|
81
125
|
* Detect ALL installed platforms
|
|
82
126
|
* Returns array of all platforms that are installed on the system
|
|
@@ -106,6 +150,12 @@ export function detectAllPlatforms(): Platform[] {
|
|
|
106
150
|
// OpenCode not found
|
|
107
151
|
}
|
|
108
152
|
|
|
153
|
+
// OpenAI Codex: check if .codex directory exists
|
|
154
|
+
const codexDir = join(homedir(), '.codex');
|
|
155
|
+
if (existsSync(codexDir)) {
|
|
156
|
+
detected.push(Platform.OpenAICodex);
|
|
157
|
+
}
|
|
158
|
+
|
|
109
159
|
return detected;
|
|
110
160
|
}
|
|
111
161
|
|
|
@@ -117,3 +167,74 @@ export function getActivePlatforms(config: DroidConfig): Platform[] {
|
|
|
117
167
|
const ignored = config.ignored_platforms ?? [];
|
|
118
168
|
return detected.filter(p => !ignored.includes(p));
|
|
119
169
|
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Sync installed tools to newly detected platforms.
|
|
173
|
+
* Called early in common commands to handle platforms added after initial setup.
|
|
174
|
+
*
|
|
175
|
+
* Returns array of newly synced platforms (empty if none).
|
|
176
|
+
*/
|
|
177
|
+
export function syncNewPlatforms(config: DroidConfig): Platform[] {
|
|
178
|
+
const detected = detectAllPlatforms();
|
|
179
|
+
const ignored = config.ignored_platforms ?? [];
|
|
180
|
+
const active = detected.filter(p => !ignored.includes(p));
|
|
181
|
+
|
|
182
|
+
// Platforms already in config (have been initialized)
|
|
183
|
+
const initializedPlatforms = Object.keys(config.platforms ?? {}) as Platform[];
|
|
184
|
+
|
|
185
|
+
// Find newly detected platforms that aren't in config yet
|
|
186
|
+
const newPlatforms = active.filter(p => !initializedPlatforms.includes(p));
|
|
187
|
+
|
|
188
|
+
if (newPlatforms.length === 0) {
|
|
189
|
+
return [];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Get installed skills from primary platform (claude-code)
|
|
193
|
+
const primaryTools = config.platforms?.['claude-code']?.tools ?? {};
|
|
194
|
+
|
|
195
|
+
// Initialize each new platform
|
|
196
|
+
for (const platform of newPlatforms) {
|
|
197
|
+
initializePlatformTools(platform, primaryTools, config);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return newPlatforms;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Initialize a newly detected platform with installed tools.
|
|
205
|
+
*
|
|
206
|
+
* For Codex: creates symlinks for tracked skills only (commands/agents not supported)
|
|
207
|
+
* For other platforms: would install commands/agents (not yet implemented)
|
|
208
|
+
*/
|
|
209
|
+
function initializePlatformTools(
|
|
210
|
+
platform: Platform,
|
|
211
|
+
primaryTools: Record<string, { version: string; installed_at: string; bundled_agents?: string[] }>,
|
|
212
|
+
config: DroidConfig,
|
|
213
|
+
): void {
|
|
214
|
+
if (platform === Platform.OpenAICodex) {
|
|
215
|
+
// Codex: create symlinks only for skills tracked in config (not all skills in directory)
|
|
216
|
+
// This avoids syncing personal skills that weren't installed via droid
|
|
217
|
+
for (const skillName of Object.keys(primaryTools)) {
|
|
218
|
+
createCodexSymlink(skillName);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Initialize platform section in config (mirrors primary platform's tools)
|
|
222
|
+
if (!config.platforms) {
|
|
223
|
+
config.platforms = {};
|
|
224
|
+
}
|
|
225
|
+
config.platforms[platform] = {
|
|
226
|
+
tools: { ...primaryTools },
|
|
227
|
+
};
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// For other platforms (Cursor, OpenCode): would need to install commands/agents
|
|
232
|
+
// This is a future enhancement - for now, just mark as initialized
|
|
233
|
+
if (!config.platforms) {
|
|
234
|
+
config.platforms = {};
|
|
235
|
+
}
|
|
236
|
+
config.platforms[platform] = {
|
|
237
|
+
tools: { ...primaryTools },
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
package/src/lib/skills.ts
CHANGED
|
@@ -29,6 +29,8 @@ import {
|
|
|
29
29
|
getCommandsPath,
|
|
30
30
|
getConfigPath,
|
|
31
31
|
getActivePlatforms,
|
|
32
|
+
createCodexSymlink,
|
|
33
|
+
removeCodexSymlink,
|
|
32
34
|
} from './platforms';
|
|
33
35
|
import { loadToolManifest, isToolInstalled } from './tools';
|
|
34
36
|
import { runToolMigrations } from './migrations';
|
|
@@ -56,15 +58,17 @@ export function getSkillsInstallPath(platform: Platform): string {
|
|
|
56
58
|
|
|
57
59
|
/**
|
|
58
60
|
* Get the commands installation path based on platform
|
|
61
|
+
* Returns null for platforms that don't support commands (e.g., OpenAI Codex)
|
|
59
62
|
*/
|
|
60
|
-
export function getCommandsInstallPath(platform: Platform): string {
|
|
63
|
+
export function getCommandsInstallPath(platform: Platform): string | null {
|
|
61
64
|
return getCommandsPath(platform);
|
|
62
65
|
}
|
|
63
66
|
|
|
64
67
|
/**
|
|
65
68
|
* Get the path to the platform's main config markdown file
|
|
69
|
+
* Returns null for platforms without config file integration (e.g., OpenAI Codex)
|
|
66
70
|
*/
|
|
67
|
-
export function getPlatformConfigPath(platform: Platform): string {
|
|
71
|
+
export function getPlatformConfigPath(platform: Platform): string | null {
|
|
68
72
|
return getConfigPath(platform);
|
|
69
73
|
}
|
|
70
74
|
|
|
@@ -77,6 +81,11 @@ export function updatePlatformConfigSkills(
|
|
|
77
81
|
): void {
|
|
78
82
|
const configPath = getPlatformConfigPath(platform);
|
|
79
83
|
|
|
84
|
+
// Skip platforms without config file integration (e.g., OpenAI Codex)
|
|
85
|
+
if (!configPath) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
80
89
|
let content = '';
|
|
81
90
|
if (existsSync(configPath)) {
|
|
82
91
|
content = readFileSync(configPath, 'utf-8');
|
|
@@ -472,7 +481,7 @@ export function installSkill(skillName: string): {
|
|
|
472
481
|
|
|
473
482
|
const skillsPath = getSkillsInstallPath(config.platform);
|
|
474
483
|
const targetSkillDir = join(skillsPath, skillName);
|
|
475
|
-
const commandsPath = getCommandsInstallPath(config.platform);
|
|
484
|
+
const commandsPath = getCommandsInstallPath(config.platform); // null for platforms without commands
|
|
476
485
|
const tools = getPlatformTools(config);
|
|
477
486
|
|
|
478
487
|
// Clean up old droid- prefixed directories from v0.18.x (Claude Code bug workaround)
|
|
@@ -527,7 +536,8 @@ export function installSkill(skillName: string): {
|
|
|
527
536
|
const toolName = basename(toolDir);
|
|
528
537
|
|
|
529
538
|
// Check command file collisions (these could conflict with other skills)
|
|
530
|
-
|
|
539
|
+
// Skip for platforms that don't support commands (e.g., OpenAI Codex)
|
|
540
|
+
if (commandsPath && existsSync(commandsSource)) {
|
|
531
541
|
const commandFiles = readdirSync(commandsSource).filter(
|
|
532
542
|
(f) => f.endsWith('.md') && f.toLowerCase() !== 'readme.md',
|
|
533
543
|
);
|
|
@@ -608,8 +618,14 @@ export function installSkill(skillName: string): {
|
|
|
608
618
|
? activePlatforms
|
|
609
619
|
: [config.platform]; // Fallback to primary platform
|
|
610
620
|
|
|
621
|
+
// Create Codex symlink if Codex is an active platform
|
|
622
|
+
// Codex reads from ~/.codex/skills/ but skills are installed to ~/.claude/skills/
|
|
623
|
+
if (targetPlatforms.includes(Platform.OpenAICodex)) {
|
|
624
|
+
createCodexSymlink(skillName);
|
|
625
|
+
}
|
|
626
|
+
|
|
611
627
|
// Copy commands if present (from tool level)
|
|
612
|
-
// Install to EACH active platform
|
|
628
|
+
// Install to EACH active platform that supports commands
|
|
613
629
|
if (existsSync(commandsSource)) {
|
|
614
630
|
const commandFiles = readdirSync(commandsSource).filter(
|
|
615
631
|
(f) => f.endsWith('.md') && f.toLowerCase() !== 'readme.md',
|
|
@@ -617,6 +633,10 @@ export function installSkill(skillName: string): {
|
|
|
617
633
|
|
|
618
634
|
for (const platform of targetPlatforms) {
|
|
619
635
|
const platformCommandsPath = getCommandsInstallPath(platform);
|
|
636
|
+
// Skip platforms that don't support commands (e.g., OpenAI Codex)
|
|
637
|
+
if (!platformCommandsPath) {
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
620
640
|
if (!existsSync(platformCommandsPath)) {
|
|
621
641
|
mkdirSync(platformCommandsPath, { recursive: true });
|
|
622
642
|
}
|
|
@@ -725,6 +745,9 @@ export function uninstallSkill(skillName: string): {
|
|
|
725
745
|
? activePlatforms
|
|
726
746
|
: [config.platform]; // Fallback to primary platform
|
|
727
747
|
|
|
748
|
+
// Remove Codex symlink if it exists (before removing the actual skill)
|
|
749
|
+
removeCodexSymlink(skillName);
|
|
750
|
+
|
|
728
751
|
// Remove skill files from unified location
|
|
729
752
|
const skillsPath = getSkillsInstallPath(config.platform);
|
|
730
753
|
const skillDir = join(skillsPath, skillName);
|
|
@@ -732,7 +755,7 @@ export function uninstallSkill(skillName: string): {
|
|
|
732
755
|
rmSync(skillDir, { recursive: true });
|
|
733
756
|
}
|
|
734
757
|
|
|
735
|
-
// Remove command files from EACH active platform
|
|
758
|
+
// Remove command files from EACH active platform that supports commands
|
|
736
759
|
const skillPath = findSkillPath(skillName);
|
|
737
760
|
const commandsSource = skillPath ? join(skillPath.toolDir, 'commands') : null;
|
|
738
761
|
if (commandsSource && existsSync(commandsSource)) {
|
|
@@ -742,6 +765,10 @@ export function uninstallSkill(skillName: string): {
|
|
|
742
765
|
|
|
743
766
|
for (const platform of targetPlatforms) {
|
|
744
767
|
const platformCommandsPath = getCommandsInstallPath(platform);
|
|
768
|
+
// Skip platforms that don't support commands (e.g., OpenAI Codex)
|
|
769
|
+
if (!platformCommandsPath) {
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
745
772
|
for (const file of commandFiles) {
|
|
746
773
|
const commandPath = join(platformCommandsPath, file);
|
|
747
774
|
if (existsSync(commandPath)) {
|
|
@@ -829,6 +856,11 @@ export function isCommandInstalled(
|
|
|
829
856
|
const config = loadConfig();
|
|
830
857
|
const commandsPath = getCommandsInstallPath(config.platform);
|
|
831
858
|
|
|
859
|
+
// Platform doesn't support commands (e.g., OpenAI Codex)
|
|
860
|
+
if (!commandsPath) {
|
|
861
|
+
return false;
|
|
862
|
+
}
|
|
863
|
+
|
|
832
864
|
// Command filename is derived from the command name after the skill prefix
|
|
833
865
|
// e.g., "comments check" from skill "comments" → "check.md"
|
|
834
866
|
const cmdPart = commandName.startsWith(skillName + ' ')
|
|
@@ -884,6 +916,13 @@ export function installCommand(
|
|
|
884
916
|
|
|
885
917
|
// Ensure commands directory exists
|
|
886
918
|
const commandsPath = getCommandsInstallPath(config.platform);
|
|
919
|
+
// Platform doesn't support commands (e.g., OpenAI Codex)
|
|
920
|
+
if (!commandsPath) {
|
|
921
|
+
return {
|
|
922
|
+
success: false,
|
|
923
|
+
message: `Platform ${config.platform} does not support custom commands`,
|
|
924
|
+
};
|
|
925
|
+
}
|
|
887
926
|
if (!existsSync(commandsPath)) {
|
|
888
927
|
mkdirSync(commandsPath, { recursive: true });
|
|
889
928
|
}
|
|
@@ -919,6 +958,14 @@ export function uninstallCommand(
|
|
|
919
958
|
|
|
920
959
|
const commandsPath = getCommandsInstallPath(config.platform);
|
|
921
960
|
|
|
961
|
+
// Platform doesn't support commands (e.g., OpenAI Codex)
|
|
962
|
+
if (!commandsPath) {
|
|
963
|
+
return {
|
|
964
|
+
success: false,
|
|
965
|
+
message: `Platform ${config.platform} does not support custom commands`,
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
|
|
922
969
|
// Find the installed command file
|
|
923
970
|
const cmdPart = commandName.startsWith(skillName + ' ')
|
|
924
971
|
? commandName.slice(skillName.length + 1)
|