@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.
Files changed (97) hide show
  1. package/.claude-plugin/marketplace.json +1 -118
  2. package/.claude-plugin/plugin.json +49 -0
  3. package/AGENTS.md +4 -0
  4. package/CHANGELOG.md +43 -0
  5. package/README.md +53 -39
  6. package/dist/bin/droid.js +525 -212
  7. package/dist/commands/setup.d.ts.map +1 -1
  8. package/dist/commands/tui/components/PlatformBadges.d.ts.map +1 -1
  9. package/dist/commands/tui/components/SettingsDetails.d.ts.map +1 -1
  10. package/dist/commands/tui/hooks/useAppUpdate.d.ts.map +1 -1
  11. package/dist/commands/tui/views/SetupScreen.d.ts.map +1 -1
  12. package/dist/commands/update.d.ts.map +1 -1
  13. package/dist/index.js +345 -186
  14. package/dist/lib/agents.d.ts +4 -2
  15. package/dist/lib/agents.d.ts.map +1 -1
  16. package/dist/lib/migrations.d.ts.map +1 -1
  17. package/dist/lib/platform.codex.d.ts +36 -0
  18. package/dist/lib/platform.codex.d.ts.map +1 -0
  19. package/dist/lib/platforms.d.ts +30 -24
  20. package/dist/lib/platforms.d.ts.map +1 -1
  21. package/dist/lib/skills.d.ts +4 -2
  22. package/dist/lib/skills.d.ts.map +1 -1
  23. package/dist/lib/types.d.ts +2 -1
  24. package/dist/lib/types.d.ts.map +1 -1
  25. package/dist/tools/brain/.claude-plugin/plugin.json +8 -1
  26. package/dist/tools/brain/TOOL.yaml +1 -1
  27. package/dist/tools/brain/skills/brain/SKILL.md +6 -3
  28. package/dist/tools/brain/skills/brain/references/workflows.md +9 -5
  29. package/dist/tools/brain/skills/brain-obsidian/SKILL.md +2 -0
  30. package/dist/tools/coach/.claude-plugin/plugin.json +6 -0
  31. package/dist/tools/coach/skills/coach/SKILL.md +3 -0
  32. package/dist/tools/code-review/.claude-plugin/plugin.json +12 -0
  33. package/dist/tools/code-review/skills/code-review/SKILL.md +2 -0
  34. package/dist/tools/codex/.claude-plugin/plugin.json +9 -0
  35. package/dist/tools/codex/skills/codex/SKILL.md +3 -0
  36. package/dist/tools/comments/.claude-plugin/plugin.json +6 -0
  37. package/dist/tools/comments/skills/comments/SKILL.md +5 -0
  38. package/dist/tools/droid/.claude-plugin/plugin.json +8 -1
  39. package/dist/tools/droid/TOOL.yaml +4 -2
  40. package/dist/tools/droid/commands/setup.md +125 -0
  41. package/dist/tools/droid/skills/droid/SKILL.md +117 -2
  42. package/dist/tools/plan/.claude-plugin/plugin.json +6 -0
  43. package/dist/tools/plan/skills/plan/SKILL.md +2 -0
  44. package/dist/tools/project/.claude-plugin/plugin.json +6 -0
  45. package/dist/tools/project/skills/project/SKILL.md +3 -0
  46. package/dist/tools/tech-design/.claude-plugin/plugin.json +7 -1
  47. package/dist/tools/tech-design/TOOL.yaml +1 -1
  48. package/dist/tools/tech-design/commands/tech-design.md +2 -0
  49. package/dist/tools/tech-design/skills/tech-design/SKILL.md +39 -9
  50. package/dist/tools/tech-design/skills/tech-design/references/publish.md +272 -216
  51. package/dist/tools/tech-design/skills/tech-design/references/start.md +50 -20
  52. package/dist/tools/wrapup/.claude-plugin/plugin.json +6 -0
  53. package/dist/tools/wrapup/skills/wrapup/SKILL.md +2 -0
  54. package/package.json +1 -1
  55. package/scripts/build-plugins.ts +154 -6
  56. package/src/bin/droid.ts +16 -0
  57. package/src/commands/setup.ts +107 -2
  58. package/src/commands/tui/components/PlatformBadges.tsx +1 -0
  59. package/src/commands/tui/components/SettingsDetails.tsx +1 -0
  60. package/src/commands/tui/hooks/useAppUpdate.ts +21 -1
  61. package/src/commands/tui/views/SetupScreen.tsx +10 -1
  62. package/src/commands/update.ts +21 -1
  63. package/src/lib/agents.ts +13 -2
  64. package/src/lib/migrations.ts +81 -9
  65. package/src/lib/platform.codex.ts +131 -0
  66. package/src/lib/platforms.ts +127 -6
  67. package/src/lib/skills.ts +53 -6
  68. package/src/lib/types.ts +1 -0
  69. package/src/tools/brain/.claude-plugin/plugin.json +8 -1
  70. package/src/tools/brain/TOOL.yaml +1 -1
  71. package/src/tools/brain/skills/brain/SKILL.md +6 -3
  72. package/src/tools/brain/skills/brain/references/workflows.md +9 -5
  73. package/src/tools/brain/skills/brain-obsidian/SKILL.md +2 -0
  74. package/src/tools/coach/.claude-plugin/plugin.json +6 -0
  75. package/src/tools/coach/skills/coach/SKILL.md +3 -0
  76. package/src/tools/code-review/.claude-plugin/plugin.json +12 -0
  77. package/src/tools/code-review/skills/code-review/SKILL.md +2 -0
  78. package/src/tools/codex/.claude-plugin/plugin.json +9 -0
  79. package/src/tools/codex/skills/codex/SKILL.md +3 -0
  80. package/src/tools/comments/.claude-plugin/plugin.json +6 -0
  81. package/src/tools/comments/skills/comments/SKILL.md +5 -0
  82. package/src/tools/droid/.claude-plugin/plugin.json +8 -1
  83. package/src/tools/droid/TOOL.yaml +4 -2
  84. package/src/tools/droid/commands/setup.md +125 -0
  85. package/src/tools/droid/skills/droid/SKILL.md +117 -2
  86. package/src/tools/plan/.claude-plugin/plugin.json +6 -0
  87. package/src/tools/plan/skills/plan/SKILL.md +2 -0
  88. package/src/tools/project/.claude-plugin/plugin.json +6 -0
  89. package/src/tools/project/skills/project/SKILL.md +3 -0
  90. package/src/tools/tech-design/.claude-plugin/plugin.json +7 -1
  91. package/src/tools/tech-design/TOOL.yaml +1 -1
  92. package/src/tools/tech-design/commands/tech-design.md +2 -0
  93. package/src/tools/tech-design/skills/tech-design/SKILL.md +39 -9
  94. package/src/tools/tech-design/skills/tech-design/references/publish.md +272 -216
  95. package/src/tools/tech-design/skills/tech-design/references/start.md +50 -20
  96. package/src/tools/wrapup/.claude-plugin/plugin.json +6 -0
  97. package/src/tools/wrapup/skills/wrapup/SKILL.md +2 -0
@@ -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)) {
@@ -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
- if (!existsSync(commandsPath)) {
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 check which commands are aliases
313
+ // Get all bundled tools to determine which commands droid manages
313
314
  const bundledTools = getBundledTools();
314
- const aliasCommands = new Set<string>();
315
+ const deletableDroidCommands = new Set<string>();
315
316
 
316
- // Collect all alias command names across all tools
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 (typeof cmd === 'object' && cmd.is_alias) {
320
- aliasCommands.add(cmd.name);
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-aliases
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
- // Keep aliases, remove everything else
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
+ }
@@ -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
- } as const;
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
- if (existsSync(commandsSource)) {
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)