learnship 1.9.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/plugin.json +26 -0
- package/.cursor-plugin/plugin.json +26 -0
- package/LICENSE +21 -0
- package/README.md +791 -0
- package/SKILL.md +86 -0
- package/agents/debugger.md +102 -0
- package/agents/executor.md +115 -0
- package/agents/learnship-debugger.md +146 -0
- package/agents/learnship-executor.md +155 -0
- package/agents/learnship-phase-researcher.md +128 -0
- package/agents/learnship-plan-checker.md +119 -0
- package/agents/learnship-planner.md +146 -0
- package/agents/learnship-verifier.md +157 -0
- package/agents/planner.md +109 -0
- package/agents/researcher.md +80 -0
- package/agents/verifier.md +114 -0
- package/bin/install.js +1242 -0
- package/bin/learnship.js +56 -0
- package/commands/learnship/add-phase.md +22 -0
- package/commands/learnship/add-tests.md +24 -0
- package/commands/learnship/add-todo.md +21 -0
- package/commands/learnship/audit-milestone.md +21 -0
- package/commands/learnship/check-todos.md +22 -0
- package/commands/learnship/cleanup.md +22 -0
- package/commands/learnship/complete-milestone.md +22 -0
- package/commands/learnship/debug.md +27 -0
- package/commands/learnship/decision-log.md +22 -0
- package/commands/learnship/diagnose-issues.md +23 -0
- package/commands/learnship/discovery-phase.md +24 -0
- package/commands/learnship/discuss-milestone.md +23 -0
- package/commands/learnship/discuss-phase.md +23 -0
- package/commands/learnship/execute-phase.md +27 -0
- package/commands/learnship/execute-plan.md +26 -0
- package/commands/learnship/health.md +20 -0
- package/commands/learnship/help.md +19 -0
- package/commands/learnship/insert-phase.md +22 -0
- package/commands/learnship/knowledge-base.md +21 -0
- package/commands/learnship/list-phase-assumptions.md +21 -0
- package/commands/learnship/ls.md +20 -0
- package/commands/learnship/map-codebase.md +23 -0
- package/commands/learnship/milestone-retrospective.md +21 -0
- package/commands/learnship/new-milestone.md +23 -0
- package/commands/learnship/new-project.md +24 -0
- package/commands/learnship/next.md +22 -0
- package/commands/learnship/pause-work.md +21 -0
- package/commands/learnship/plan-milestone-gaps.md +22 -0
- package/commands/learnship/plan-phase.md +24 -0
- package/commands/learnship/progress.md +20 -0
- package/commands/learnship/quick.md +27 -0
- package/commands/learnship/reapply-patches.md +21 -0
- package/commands/learnship/release.md +21 -0
- package/commands/learnship/remove-phase.md +23 -0
- package/commands/learnship/research-phase.md +23 -0
- package/commands/learnship/resume-work.md +21 -0
- package/commands/learnship/set-profile.md +21 -0
- package/commands/learnship/settings.md +21 -0
- package/commands/learnship/transition.md +21 -0
- package/commands/learnship/update.md +21 -0
- package/commands/learnship/validate-phase.md +22 -0
- package/commands/learnship/verify-work.md +23 -0
- package/cursor-rules/learnship.mdc +60 -0
- package/gemini-extension.json +10 -0
- package/hooks/hooks-claude.json +15 -0
- package/hooks/hooks-cursor.json +10 -0
- package/hooks/session-start +43 -0
- package/install.sh +254 -0
- package/learnship/references/design-commands.md +119 -0
- package/learnship/references/git-integration.md +249 -0
- package/learnship/references/learning-design.md +142 -0
- package/learnship/references/model-profiles.md +90 -0
- package/learnship/references/planning-config.md +184 -0
- package/learnship/references/questioning.md +162 -0
- package/learnship/references/ui-brand.md +160 -0
- package/learnship/references/verification-patterns.md +608 -0
- package/learnship/templates/agents.md +166 -0
- package/learnship/templates/context.md +72 -0
- package/learnship/templates/plan.md +202 -0
- package/learnship/templates/project.md +184 -0
- package/learnship/templates/requirements.md +231 -0
- package/learnship/templates/state.md +176 -0
- package/learnship/templates/uat.md +80 -0
- package/learnship/workflows/add-phase.md +84 -0
- package/learnship/workflows/add-tests.md +191 -0
- package/learnship/workflows/add-todo.md +108 -0
- package/learnship/workflows/audit-milestone.md +178 -0
- package/learnship/workflows/check-todos.md +138 -0
- package/learnship/workflows/cleanup.md +107 -0
- package/learnship/workflows/complete-milestone.md +191 -0
- package/learnship/workflows/debug.md +245 -0
- package/learnship/workflows/decision-log.md +131 -0
- package/learnship/workflows/diagnose-issues.md +145 -0
- package/learnship/workflows/discovery-phase.md +183 -0
- package/learnship/workflows/discuss-milestone.md +136 -0
- package/learnship/workflows/discuss-phase.md +244 -0
- package/learnship/workflows/execute-phase.md +345 -0
- package/learnship/workflows/execute-plan.md +149 -0
- package/learnship/workflows/health.md +171 -0
- package/learnship/workflows/help.md +153 -0
- package/learnship/workflows/insert-phase.md +106 -0
- package/learnship/workflows/knowledge-base.md +168 -0
- package/learnship/workflows/list-phase-assumptions.md +129 -0
- package/learnship/workflows/ls.md +145 -0
- package/learnship/workflows/map-codebase.md +142 -0
- package/learnship/workflows/milestone-retrospective.md +178 -0
- package/learnship/workflows/new-milestone.md +200 -0
- package/learnship/workflows/new-project.md +340 -0
- package/learnship/workflows/next.md +100 -0
- package/learnship/workflows/pause-work.md +122 -0
- package/learnship/workflows/plan-milestone-gaps.md +160 -0
- package/learnship/workflows/plan-phase.md +288 -0
- package/learnship/workflows/progress.md +118 -0
- package/learnship/workflows/quick.md +256 -0
- package/learnship/workflows/reapply-patches.md +130 -0
- package/learnship/workflows/release.md +217 -0
- package/learnship/workflows/remove-phase.md +128 -0
- package/learnship/workflows/research-phase.md +137 -0
- package/learnship/workflows/resume-work.md +162 -0
- package/learnship/workflows/set-profile.md +78 -0
- package/learnship/workflows/settings.md +204 -0
- package/learnship/workflows/sync-upstream-skills.md +269 -0
- package/learnship/workflows/transition.md +165 -0
- package/learnship/workflows/update.md +166 -0
- package/learnship/workflows/validate-phase.md +174 -0
- package/learnship/workflows/verify-work.md +264 -0
- package/package.json +62 -0
- package/references/design-commands.md +119 -0
- package/references/git-integration.md +249 -0
- package/references/learning-design.md +142 -0
- package/references/model-profiles.md +90 -0
- package/references/planning-config.md +184 -0
- package/references/questioning.md +162 -0
- package/references/ui-brand.md +160 -0
- package/references/verification-patterns.md +608 -0
- package/skills/agentic-learning/SKILL.md +373 -0
- package/skills/agentic-learning/references/either-or-format.md +161 -0
- package/skills/agentic-learning/references/learning-science.md +190 -0
- package/skills/agentic-learning/references/struggle-ladder.md +140 -0
- package/skills/impeccable/SKILL.md +125 -0
- package/skills/impeccable/adapt/SKILL.md +199 -0
- package/skills/impeccable/animate/SKILL.md +190 -0
- package/skills/impeccable/audit/SKILL.md +129 -0
- package/skills/impeccable/bolder/SKILL.md +132 -0
- package/skills/impeccable/clarify/SKILL.md +180 -0
- package/skills/impeccable/colorize/SKILL.md +158 -0
- package/skills/impeccable/critique/SKILL.md +118 -0
- package/skills/impeccable/delight/SKILL.md +317 -0
- package/skills/impeccable/distill/SKILL.md +137 -0
- package/skills/impeccable/extract/SKILL.md +95 -0
- package/skills/impeccable/frontend-design/SKILL.md +127 -0
- package/skills/impeccable/frontend-design/reference/color-and-contrast.md +132 -0
- package/skills/impeccable/frontend-design/reference/interaction-design.md +123 -0
- package/skills/impeccable/frontend-design/reference/motion-design.md +99 -0
- package/skills/impeccable/frontend-design/reference/responsive-design.md +114 -0
- package/skills/impeccable/frontend-design/reference/spatial-design.md +100 -0
- package/skills/impeccable/frontend-design/reference/typography.md +131 -0
- package/skills/impeccable/frontend-design/reference/ux-writing.md +107 -0
- package/skills/impeccable/harden/SKILL.md +358 -0
- package/skills/impeccable/normalize/SKILL.md +67 -0
- package/skills/impeccable/onboard/SKILL.md +243 -0
- package/skills/impeccable/optimize/SKILL.md +269 -0
- package/skills/impeccable/polish/SKILL.md +202 -0
- package/skills/impeccable/quieter/SKILL.md +118 -0
- package/skills/impeccable/teach-impeccable/SKILL.md +69 -0
- package/templates/agents.md +166 -0
- package/templates/config.json +22 -0
- package/templates/context.md +72 -0
- package/templates/plan.md +202 -0
- package/templates/project.md +184 -0
- package/templates/requirements.md +231 -0
- package/templates/state.md +176 -0
- package/templates/uat.md +80 -0
package/bin/install.js
ADDED
|
@@ -0,0 +1,1242 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* learnship multi-platform installer
|
|
5
|
+
*
|
|
6
|
+
* Installs learnship workflows, agents, and commands for:
|
|
7
|
+
* --windsurf ~/.codeium/windsurf/workflows/ (Windsurf — same as install.sh)
|
|
8
|
+
* --claude ~/.claude/ (Claude Code)
|
|
9
|
+
* --opencode ~/.config/opencode/ (OpenCode)
|
|
10
|
+
* --gemini ~/.gemini/ (Gemini CLI)
|
|
11
|
+
* --codex ~/.codex/ (Codex CLI / OpenAI Codex)
|
|
12
|
+
* --all All platforms
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* npx learnship Interactive install
|
|
16
|
+
* npx learnship --claude --global Claude Code, global
|
|
17
|
+
* npx learnship --all --global All platforms, global
|
|
18
|
+
* npx learnship --claude --global --uninstall Remove Claude Code files
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
'use strict';
|
|
22
|
+
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
const path = require('path');
|
|
25
|
+
const os = require('os');
|
|
26
|
+
const readline = require('readline');
|
|
27
|
+
|
|
28
|
+
const pkg = require('../package.json');
|
|
29
|
+
|
|
30
|
+
// Codex config.toml marker — used to identify learnship-managed section
|
|
31
|
+
const LEARNSHIP_CODEX_MARKER = '# learnship Agent Configuration — managed by learnship installer';
|
|
32
|
+
|
|
33
|
+
// Per-agent Codex sandbox modes (read-only for checkers, workspace-write for executors)
|
|
34
|
+
const CODEX_AGENT_SANDBOX = {
|
|
35
|
+
'learnship-executor': 'workspace-write',
|
|
36
|
+
'learnship-planner': 'workspace-write',
|
|
37
|
+
'learnship-phase-researcher': 'workspace-write',
|
|
38
|
+
'learnship-verifier': 'workspace-write',
|
|
39
|
+
'learnship-debugger': 'workspace-write',
|
|
40
|
+
'learnship-plan-checker': 'read-only',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// ─── Colors ────────────────────────────────────────────────────────────────
|
|
44
|
+
const cyan = '\x1b[36m';
|
|
45
|
+
const purple = '\x1b[38;5;135m';
|
|
46
|
+
const green = '\x1b[32m';
|
|
47
|
+
const yellow = '\x1b[33m';
|
|
48
|
+
const dim = '\x1b[2m';
|
|
49
|
+
const reset = '\x1b[0m';
|
|
50
|
+
|
|
51
|
+
// ─── Argument parsing ──────────────────────────────────────────────────────
|
|
52
|
+
const args = process.argv.slice(2);
|
|
53
|
+
const hasWindsurf = args.includes('--windsurf');
|
|
54
|
+
const hasClaude = args.includes('--claude');
|
|
55
|
+
const hasOpencode = args.includes('--opencode');
|
|
56
|
+
const hasGemini = args.includes('--gemini');
|
|
57
|
+
const hasCodex = args.includes('--codex');
|
|
58
|
+
const hasAll = args.includes('--all');
|
|
59
|
+
const hasGlobal = args.includes('--global') || args.includes('-g');
|
|
60
|
+
const hasLocal = args.includes('--local') || args.includes('-l');
|
|
61
|
+
const hasUninstall = args.includes('--uninstall') || args.includes('-u');
|
|
62
|
+
const hasHelp = args.includes('--help') || args.includes('-h');
|
|
63
|
+
|
|
64
|
+
let selectedPlatforms = [];
|
|
65
|
+
if (hasAll) {
|
|
66
|
+
selectedPlatforms = ['windsurf', 'claude', 'opencode', 'gemini', 'codex'];
|
|
67
|
+
} else {
|
|
68
|
+
if (hasWindsurf) selectedPlatforms.push('windsurf');
|
|
69
|
+
if (hasClaude) selectedPlatforms.push('claude');
|
|
70
|
+
if (hasOpencode) selectedPlatforms.push('opencode');
|
|
71
|
+
if (hasGemini) selectedPlatforms.push('gemini');
|
|
72
|
+
if (hasCodex) selectedPlatforms.push('codex');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ─── Banner ────────────────────────────────────────────────────────────────
|
|
76
|
+
const banner = `
|
|
77
|
+
${purple} ██╗ ███████╗ █████╗ ██████╗ ███╗ ██╗███████╗██╗ ██╗██╗██████╗
|
|
78
|
+
██║ ██╔════╝██╔══██╗██╔══██╗████╗ ██║██╔════╝██║ ██║██║██╔══██╗
|
|
79
|
+
██║ █████╗ ███████║██████╔╝██╔██╗ ██║███████╗███████║██║██████╔╝
|
|
80
|
+
██║ ██╔══╝ ██╔══██║██╔══██╗██║╚██╗██║╚════██║██╔══██║██║██╔═══╝
|
|
81
|
+
███████╗███████╗██║ ██║██║ ██║██║ ╚████║███████║██║ ██║██║██║
|
|
82
|
+
╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚═╝ ╚═╝╚═╝╚═╝${reset}
|
|
83
|
+
|
|
84
|
+
${dim}Learn as you build. Build with intent.${reset}
|
|
85
|
+
${dim}v${pkg.version} · Windsurf · Claude Code · OpenCode · Gemini CLI · Codex CLI${reset}
|
|
86
|
+
`;
|
|
87
|
+
|
|
88
|
+
// ─── Help text ─────────────────────────────────────────────────────────────
|
|
89
|
+
const helpText = `
|
|
90
|
+
${yellow}Usage:${reset} npx learnship [platform] [scope] [options]
|
|
91
|
+
|
|
92
|
+
${yellow}Platforms:${reset}
|
|
93
|
+
${cyan}--windsurf${reset} Windsurf (same as install.sh)
|
|
94
|
+
${cyan}--claude${reset} Claude Code (~/.claude/)
|
|
95
|
+
${cyan}--opencode${reset} OpenCode (~/.config/opencode/)
|
|
96
|
+
${cyan}--gemini${reset} Gemini CLI (~/.gemini/)
|
|
97
|
+
${cyan}--codex${reset} Codex CLI (~/.codex/)
|
|
98
|
+
${cyan}--all${reset} All platforms
|
|
99
|
+
|
|
100
|
+
${yellow}Scope:${reset}
|
|
101
|
+
${cyan}-g, --global${reset} Install to global config directory (recommended)
|
|
102
|
+
${cyan}-l, --local${reset} Install to current project directory
|
|
103
|
+
|
|
104
|
+
${yellow}Options:${reset}
|
|
105
|
+
${cyan}-u, --uninstall${reset} Remove learnship files
|
|
106
|
+
${cyan}-h, --help${reset} Show this help
|
|
107
|
+
|
|
108
|
+
${yellow}Examples:${reset}
|
|
109
|
+
${dim}# Interactive install (prompts for platform and scope)${reset}
|
|
110
|
+
npx learnship
|
|
111
|
+
|
|
112
|
+
${dim}# Install for Claude Code globally${reset}
|
|
113
|
+
npx learnship --claude --global
|
|
114
|
+
|
|
115
|
+
${dim}# Install for all platforms globally${reset}
|
|
116
|
+
npx learnship --all --global
|
|
117
|
+
|
|
118
|
+
${dim}# Install for Gemini CLI globally${reset}
|
|
119
|
+
npx learnship --gemini --global
|
|
120
|
+
|
|
121
|
+
${dim}# Install for Codex globally${reset}
|
|
122
|
+
npx learnship --codex --global
|
|
123
|
+
|
|
124
|
+
${dim}# Uninstall from OpenCode${reset}
|
|
125
|
+
npx learnship --opencode --global --uninstall
|
|
126
|
+
|
|
127
|
+
${dim}# Install to current project only${reset}
|
|
128
|
+
npx learnship --claude --local
|
|
129
|
+
`;
|
|
130
|
+
|
|
131
|
+
// ─── Path helpers ──────────────────────────────────────────────────────────
|
|
132
|
+
function expandTilde(p) {
|
|
133
|
+
if (p && p.startsWith('~/')) return path.join(os.homedir(), p.slice(2));
|
|
134
|
+
return p;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function getDirName(platform) {
|
|
138
|
+
if (platform === 'opencode') return '.opencode';
|
|
139
|
+
if (platform === 'gemini') return '.gemini';
|
|
140
|
+
if (platform === 'codex') return '.codex';
|
|
141
|
+
if (platform === 'windsurf') return '.windsurf';
|
|
142
|
+
return '.claude';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function getGlobalDir(platform) {
|
|
146
|
+
switch (platform) {
|
|
147
|
+
case 'opencode': {
|
|
148
|
+
if (process.env.OPENCODE_CONFIG_DIR) return expandTilde(process.env.OPENCODE_CONFIG_DIR);
|
|
149
|
+
if (process.env.XDG_CONFIG_HOME) return path.join(expandTilde(process.env.XDG_CONFIG_HOME), 'opencode');
|
|
150
|
+
return path.join(os.homedir(), '.config', 'opencode');
|
|
151
|
+
}
|
|
152
|
+
case 'gemini': {
|
|
153
|
+
if (process.env.GEMINI_CONFIG_DIR) return expandTilde(process.env.GEMINI_CONFIG_DIR);
|
|
154
|
+
return path.join(os.homedir(), '.gemini');
|
|
155
|
+
}
|
|
156
|
+
case 'codex': {
|
|
157
|
+
if (process.env.CODEX_HOME) return expandTilde(process.env.CODEX_HOME);
|
|
158
|
+
return path.join(os.homedir(), '.codex');
|
|
159
|
+
}
|
|
160
|
+
case 'windsurf': {
|
|
161
|
+
return path.join(os.homedir(), '.codeium', 'windsurf');
|
|
162
|
+
}
|
|
163
|
+
default: { // claude
|
|
164
|
+
if (process.env.CLAUDE_CONFIG_DIR) return expandTilde(process.env.CLAUDE_CONFIG_DIR);
|
|
165
|
+
return path.join(os.homedir(), '.claude');
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function getPlatformLabel(platform) {
|
|
171
|
+
const labels = {
|
|
172
|
+
windsurf: 'Windsurf', claude: 'Claude Code',
|
|
173
|
+
opencode: 'OpenCode', gemini: 'Gemini CLI', codex: 'Codex CLI',
|
|
174
|
+
};
|
|
175
|
+
return labels[platform] || platform;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ─── File helpers ──────────────────────────────────────────────────────────
|
|
179
|
+
function readSettings(p) {
|
|
180
|
+
if (!fs.existsSync(p)) return {};
|
|
181
|
+
try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return {}; }
|
|
182
|
+
}
|
|
183
|
+
function writeSettings(p, obj) {
|
|
184
|
+
fs.writeFileSync(p, JSON.stringify(obj, null, 2) + '\n');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ─── Path helpers (extended) ───────────────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Convert an absolute pathPrefix to $HOME-relative form for bash code blocks.
|
|
191
|
+
* Keeps $HOME as a shell variable so paths remain portable across machines.
|
|
192
|
+
*/
|
|
193
|
+
function toHomePrefix(pathPrefix) {
|
|
194
|
+
const home = os.homedir().replace(/\\/g, '/');
|
|
195
|
+
const normalized = pathPrefix.replace(/\\/g, '/');
|
|
196
|
+
if (normalized.startsWith(home)) return '$HOME' + normalized.slice(home.length);
|
|
197
|
+
return normalized;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ─── Frontmatter conversion ────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
// Color name → hex for OpenCode (color names not supported)
|
|
203
|
+
const colorNameToHex = {
|
|
204
|
+
cyan: '#00FFFF', red: '#FF0000', green: '#00FF00', blue: '#0000FF',
|
|
205
|
+
yellow: '#FFFF00', magenta: '#FF00FF', orange: '#FFA500', purple: '#800080',
|
|
206
|
+
pink: '#FFC0CB', white: '#FFFFFF', black: '#000000', gray: '#808080', grey: '#808080',
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
/** Convert Claude Code tool name → OpenCode tool name */
|
|
210
|
+
function toOpencodeToolName(t) {
|
|
211
|
+
const map = { AskUserQuestion: 'question', SlashCommand: 'skill', TodoWrite: 'todowrite',
|
|
212
|
+
WebFetch: 'webfetch', WebSearch: 'websearch' };
|
|
213
|
+
return map[t] || t.toLowerCase();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Convert Claude Code tool name → Gemini CLI tool name (snake_case) */
|
|
217
|
+
function toGeminiToolName(t) {
|
|
218
|
+
if (t.startsWith('mcp__') || t === 'Task') return null; // auto-discovered
|
|
219
|
+
const map = { Read: 'read_file', Write: 'write_file', Edit: 'replace',
|
|
220
|
+
Bash: 'run_shell_command', Glob: 'glob', Grep: 'search_file_content',
|
|
221
|
+
WebSearch: 'google_web_search', WebFetch: 'web_fetch',
|
|
222
|
+
TodoWrite: 'write_todos', AskUserQuestion: 'ask_user' };
|
|
223
|
+
return map[t] || t.toLowerCase();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Parse YAML frontmatter from a .md file. Returns { frontmatter, body }. */
|
|
227
|
+
function parseFrontmatter(content) {
|
|
228
|
+
if (!content.startsWith('---')) return { frontmatter: null, body: content };
|
|
229
|
+
const end = content.indexOf('---', 3);
|
|
230
|
+
if (end === -1) return { frontmatter: null, body: content };
|
|
231
|
+
return { frontmatter: content.substring(3, end).trim(), body: content.substring(end + 3) };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function getFrontmatterField(fm, field) {
|
|
235
|
+
const m = fm.match(new RegExp(`^${field}:\\s*(.+)$`, 'm'));
|
|
236
|
+
return m ? m[1].trim().replace(/^['"]|['"]$/g, '') : null;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Convert Claude Code command/agent .md → OpenCode format
|
|
241
|
+
* - allowed-tools array → tools object (tool: true)
|
|
242
|
+
* - name: field removed (OpenCode uses filename for commands)
|
|
243
|
+
* - color: name → hex (OpenCode requires hex)
|
|
244
|
+
* - /learnship:cmd → /learnship-cmd
|
|
245
|
+
* - subagent_type="general-purpose" → subagent_type="general"
|
|
246
|
+
* - ~/.claude/ → ~/.config/opencode/
|
|
247
|
+
*/
|
|
248
|
+
function convertToOpencode(content) {
|
|
249
|
+
let c = content
|
|
250
|
+
.replace(/\/learnship:/g, '/learnship-')
|
|
251
|
+
.replace(/~\/\.claude\//g, '~/.config/opencode/')
|
|
252
|
+
.replace(/\$HOME\/\.claude\//g, '$HOME/.config/opencode/')
|
|
253
|
+
.replace(/\bAskUserQuestion\b/g, 'question')
|
|
254
|
+
.replace(/\bSlashCommand\b/g, 'skill')
|
|
255
|
+
.replace(/\bTodoWrite\b/g, 'todowrite')
|
|
256
|
+
.replace(/subagent_type="general-purpose"/g, 'subagent_type="general"');
|
|
257
|
+
|
|
258
|
+
const { frontmatter, body } = parseFrontmatter(c);
|
|
259
|
+
if (!frontmatter) return c;
|
|
260
|
+
|
|
261
|
+
const lines = frontmatter.split('\n');
|
|
262
|
+
const newLines = [];
|
|
263
|
+
let inTools = false;
|
|
264
|
+
const tools = [];
|
|
265
|
+
|
|
266
|
+
for (const line of lines) {
|
|
267
|
+
const t = line.trim();
|
|
268
|
+
if (t.startsWith('name:')) continue; // OpenCode uses filename for command name
|
|
269
|
+
if (t.startsWith('allowed-tools:')) { inTools = true; continue; }
|
|
270
|
+
// Handle inline tools: field (comma-separated string, e.g. agents use 'tools: Read, Write')
|
|
271
|
+
if (t.startsWith('tools:')) {
|
|
272
|
+
const toolsValue = t.substring(6).trim();
|
|
273
|
+
if (toolsValue) {
|
|
274
|
+
// Inline comma-separated: tools: Read, Write, Bash
|
|
275
|
+
for (const tool of toolsValue.split(',').map(s => s.trim()).filter(Boolean)) {
|
|
276
|
+
tools.push(tool);
|
|
277
|
+
}
|
|
278
|
+
} else {
|
|
279
|
+
// YAML array follows
|
|
280
|
+
inTools = true;
|
|
281
|
+
}
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
// Convert color names to hex
|
|
285
|
+
if (t.startsWith('color:')) {
|
|
286
|
+
const colorVal = t.substring(6).trim().toLowerCase();
|
|
287
|
+
const hex = colorNameToHex[colorVal];
|
|
288
|
+
if (hex) { newLines.push(`color: "${hex}"`); }
|
|
289
|
+
else if (colorVal.startsWith('#')) { newLines.push(line); }
|
|
290
|
+
// skip unknown color names entirely
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
if (inTools) {
|
|
294
|
+
if (t.startsWith('- ')) { tools.push(t.slice(2).trim()); continue; }
|
|
295
|
+
if (t && !t.startsWith('-')) inTools = false;
|
|
296
|
+
}
|
|
297
|
+
if (!inTools) newLines.push(line);
|
|
298
|
+
}
|
|
299
|
+
if (tools.length > 0) {
|
|
300
|
+
newLines.push('tools:');
|
|
301
|
+
for (const tool of tools) newLines.push(` ${toOpencodeToolName(tool)}: true`);
|
|
302
|
+
}
|
|
303
|
+
return `---\n${newLines.join('\n').trim()}\n---${body}`;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Convert Claude Code command .md → Gemini CLI .toml
|
|
308
|
+
* Gemini uses TOML: description = "..." and prompt = "..."
|
|
309
|
+
*/
|
|
310
|
+
function convertToGeminiToml(content) {
|
|
311
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
312
|
+
// Strip <sub> tags — terminals can't render them
|
|
313
|
+
const cleanBody = (body || content).replace(/<sub>(.*?)<\/sub>/g, '*($1)*').trim();
|
|
314
|
+
let desc = '';
|
|
315
|
+
if (frontmatter) {
|
|
316
|
+
const d = getFrontmatterField(frontmatter, 'description');
|
|
317
|
+
if (d) desc = d;
|
|
318
|
+
}
|
|
319
|
+
let toml = '';
|
|
320
|
+
if (desc) toml += `description = ${JSON.stringify(desc)}\n`;
|
|
321
|
+
// Escape ${VAR} → $VAR (Gemini treats ${word} as template variables)
|
|
322
|
+
const escapedBody = cleanBody.replace(/\$\{(\w+)\}/g, '$$$1');
|
|
323
|
+
toml += `prompt = ${JSON.stringify(escapedBody)}\n`;
|
|
324
|
+
return toml;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Convert Claude Code command .md → Codex skill SKILL.md
|
|
329
|
+
* - /learnship:cmd → $learnship-cmd
|
|
330
|
+
* - $ARGUMENTS → {{LEARNSHIP_ARGS}}
|
|
331
|
+
* - AskUserQuestion → request_user_input
|
|
332
|
+
* - Adds full <codex_skill_adapter> header (matching GSD's detail level)
|
|
333
|
+
*/
|
|
334
|
+
function convertToCodexSkill(content, skillName) {
|
|
335
|
+
let c = content
|
|
336
|
+
.replace(/\/learnship:([a-z0-9-]+)/gi, '$learnship-$1')
|
|
337
|
+
.replace(/\$ARGUMENTS\b/g, '{{LEARNSHIP_ARGS}}')
|
|
338
|
+
.replace(/\/learnship-help\b/g, '$learnship-help');
|
|
339
|
+
|
|
340
|
+
const { frontmatter, body } = parseFrontmatter(c);
|
|
341
|
+
let description = `Run learnship workflow ${skillName}.`;
|
|
342
|
+
if (frontmatter) {
|
|
343
|
+
const d = getFrontmatterField(frontmatter, 'description');
|
|
344
|
+
if (d) description = d.replace(/\s+/g, ' ').trim();
|
|
345
|
+
}
|
|
346
|
+
const shortDesc = description.length > 180 ? description.slice(0, 177) + '...' : description;
|
|
347
|
+
const invocation = `$${skillName}`;
|
|
348
|
+
|
|
349
|
+
const adapter = `<codex_skill_adapter>
|
|
350
|
+
## A. Skill Invocation
|
|
351
|
+
- This skill is invoked by mentioning \`${invocation}\`.
|
|
352
|
+
- Treat all user text after \`${invocation}\` as \`{{LEARNSHIP_ARGS}}\`.
|
|
353
|
+
- If no arguments are present, treat \`{{LEARNSHIP_ARGS}}\` as empty.
|
|
354
|
+
|
|
355
|
+
## B. AskUserQuestion → request_user_input Mapping
|
|
356
|
+
learnship workflows use \`AskUserQuestion\` (Claude Code syntax). Translate to Codex \`request_user_input\`:
|
|
357
|
+
|
|
358
|
+
Parameter mapping:
|
|
359
|
+
- \`header\` → \`header\`
|
|
360
|
+
- \`question\` → \`question\`
|
|
361
|
+
- Options formatted as \`"Label" — description\` → \`{label: "Label", description: "description"}\`
|
|
362
|
+
- Generate \`id\` from header: lowercase, replace spaces with underscores
|
|
363
|
+
|
|
364
|
+
Multi-select workaround:
|
|
365
|
+
- Codex has no \`multiSelect\`. Use sequential single-selects, or present a numbered freeform list.
|
|
366
|
+
|
|
367
|
+
Execute mode fallback:
|
|
368
|
+
- When \`request_user_input\` is rejected, present a plain-text numbered list and pick a reasonable default.
|
|
369
|
+
|
|
370
|
+
## C. Task() → spawn_agent Mapping
|
|
371
|
+
learnship workflows use \`Task(...)\` (Claude Code syntax). Translate to Codex collaboration tools:
|
|
372
|
+
|
|
373
|
+
Direct mapping:
|
|
374
|
+
- \`Task(subagent_type="X", prompt="Y")\` → \`spawn_agent(agent_type="X", message="Y")\`
|
|
375
|
+
- \`Task(model="...")\` → omit (Codex uses per-role config)
|
|
376
|
+
- \`fork_context: false\` by default — learnship agents load their own context via \`<files_to_read>\` blocks
|
|
377
|
+
|
|
378
|
+
Parallel fan-out:
|
|
379
|
+
- Spawn multiple agents → collect agent IDs → \`wait(ids)\` for all to complete
|
|
380
|
+
|
|
381
|
+
Result parsing:
|
|
382
|
+
- Look for structured markers: \`CHECKPOINT\`, \`PLAN COMPLETE\`, \`SUMMARY\`, etc.
|
|
383
|
+
- \`close_agent(id)\` after collecting results from each agent
|
|
384
|
+
</codex_skill_adapter>`;
|
|
385
|
+
|
|
386
|
+
return `---\nname: ${JSON.stringify(skillName)}\ndescription: ${JSON.stringify(description)}\nmetadata:\n short-description: ${JSON.stringify(shortDesc)}\n---\n\n${adapter}\n\n${body.trimStart()}`;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Convert Claude Code agent .md to Codex agent format.
|
|
391
|
+
* Adds <codex_agent_role> header, cleans frontmatter (removes tools/color).
|
|
392
|
+
*/
|
|
393
|
+
function convertClaudeAgentToCodexAgent(content) {
|
|
394
|
+
// Apply base Codex markdown conversions first
|
|
395
|
+
let c = content
|
|
396
|
+
.replace(/\/learnship:([a-z0-9-]+)/gi, '$learnship-$1')
|
|
397
|
+
.replace(/\$ARGUMENTS\b/g, '{{LEARNSHIP_ARGS}}');
|
|
398
|
+
|
|
399
|
+
const { frontmatter, body } = parseFrontmatter(c);
|
|
400
|
+
if (!frontmatter) return c;
|
|
401
|
+
|
|
402
|
+
const name = getFrontmatterField(frontmatter, 'name') || 'unknown';
|
|
403
|
+
const description = (getFrontmatterField(frontmatter, 'description') || '').replace(/\s+/g, ' ').trim();
|
|
404
|
+
const tools = getFrontmatterField(frontmatter, 'tools') || '';
|
|
405
|
+
|
|
406
|
+
const roleHeader = `<codex_agent_role>\nrole: ${name}\ntools: ${tools}\npurpose: ${description}\n</codex_agent_role>`;
|
|
407
|
+
const cleanFrontmatter = `---\nname: ${JSON.stringify(name)}\ndescription: ${JSON.stringify(description)}\n---`;
|
|
408
|
+
|
|
409
|
+
return `${cleanFrontmatter}\n\n${roleHeader}\n${body}`;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Convert agent .md for Gemini CLI
|
|
414
|
+
* - allowed-tools array → tools YAML array with snake_case names
|
|
415
|
+
* - color: removed (causes validation error in Gemini)
|
|
416
|
+
* - Task: excluded (agents auto-registered as tools in Gemini)
|
|
417
|
+
* - ${VAR} → $VAR in body (Gemini template engine would misparse ${WORD})
|
|
418
|
+
*/
|
|
419
|
+
function convertAgentForGemini(content) {
|
|
420
|
+
if (!content.startsWith('---')) return content;
|
|
421
|
+
const end = content.indexOf('---', 3);
|
|
422
|
+
if (end === -1) return content;
|
|
423
|
+
const frontmatter = content.substring(3, end).trim();
|
|
424
|
+
const body = content.substring(end + 3);
|
|
425
|
+
|
|
426
|
+
const lines = frontmatter.split('\n');
|
|
427
|
+
const newLines = [];
|
|
428
|
+
let inTools = false;
|
|
429
|
+
const tools = [];
|
|
430
|
+
|
|
431
|
+
for (const line of lines) {
|
|
432
|
+
const t = line.trim();
|
|
433
|
+
if (t.startsWith('color:')) continue; // Gemini rejects color field
|
|
434
|
+
if (t.startsWith('allowed-tools:')) { inTools = true; continue; }
|
|
435
|
+
// Handle inline tools: field (comma-separated, used by agent frontmatter)
|
|
436
|
+
if (t.startsWith('tools:')) {
|
|
437
|
+
const toolsValue = t.substring(6).trim();
|
|
438
|
+
if (toolsValue) {
|
|
439
|
+
// Inline: tools: Read, Write, Bash
|
|
440
|
+
for (const tool of toolsValue.split(',').map(s => s.trim()).filter(Boolean)) {
|
|
441
|
+
const mapped = toGeminiToolName(tool);
|
|
442
|
+
if (mapped) tools.push(mapped);
|
|
443
|
+
}
|
|
444
|
+
} else {
|
|
445
|
+
// YAML array follows
|
|
446
|
+
inTools = true;
|
|
447
|
+
}
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
if (inTools) {
|
|
451
|
+
if (t.startsWith('- ')) {
|
|
452
|
+
const mapped = toGeminiToolName(t.slice(2).trim());
|
|
453
|
+
if (mapped) tools.push(mapped);
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
if (t && !t.startsWith('-')) inTools = false;
|
|
457
|
+
}
|
|
458
|
+
if (!inTools) newLines.push(line);
|
|
459
|
+
}
|
|
460
|
+
if (tools.length > 0) {
|
|
461
|
+
newLines.push('tools:');
|
|
462
|
+
for (const tool of tools) newLines.push(` - ${tool}`);
|
|
463
|
+
}
|
|
464
|
+
const escapedBody = body.replace(/\$\{(\w+)\}/g, '$$$1');
|
|
465
|
+
return `---\n${newLines.join('\n').trim()}\n---${escapedBody}`;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// ─── File copy helpers ─────────────────────────────────────────────────────
|
|
469
|
+
|
|
470
|
+
/** Verify a directory exists and has files */
|
|
471
|
+
function verifyInstalled(dirPath, description) {
|
|
472
|
+
if (!fs.existsSync(dirPath)) {
|
|
473
|
+
console.error(` ${yellow}✗${reset} Failed to install ${description}: directory not created`);
|
|
474
|
+
return false;
|
|
475
|
+
}
|
|
476
|
+
try {
|
|
477
|
+
if (fs.readdirSync(dirPath).length === 0) {
|
|
478
|
+
console.error(` ${yellow}✗${reset} Failed to install ${description}: directory is empty`);
|
|
479
|
+
return false;
|
|
480
|
+
}
|
|
481
|
+
} catch (e) {
|
|
482
|
+
console.error(` ${yellow}✗${reset} Failed to install ${description}: ${e.message}`);
|
|
483
|
+
return false;
|
|
484
|
+
}
|
|
485
|
+
return true;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** Recursively copy dir, replacing path references in .md files */
|
|
489
|
+
function copyDir(srcDir, destDir, pathPrefix, platform) {
|
|
490
|
+
if (fs.existsSync(destDir)) fs.rmSync(destDir, { recursive: true });
|
|
491
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
492
|
+
for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
|
|
493
|
+
const src = path.join(srcDir, entry.name);
|
|
494
|
+
const dest = path.join(destDir, entry.name);
|
|
495
|
+
if (entry.isDirectory()) {
|
|
496
|
+
copyDir(src, dest, pathPrefix, platform);
|
|
497
|
+
} else if (entry.name.endsWith('.md')) {
|
|
498
|
+
let c = fs.readFileSync(src, 'utf8');
|
|
499
|
+
c = replacePaths(c, pathPrefix, platform);
|
|
500
|
+
if (platform === 'opencode') c = convertToOpencode(c);
|
|
501
|
+
// gemini agents converted separately; body ${VAR} escaping done there
|
|
502
|
+
fs.writeFileSync(dest, c);
|
|
503
|
+
} else {
|
|
504
|
+
fs.copyFileSync(src, dest);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function replacePaths(content, pathPrefix, platform) {
|
|
510
|
+
const dirName = getDirName(platform);
|
|
511
|
+
let c = content
|
|
512
|
+
// Source files use ~/.claude/ and $HOME/.claude/ as canonical paths
|
|
513
|
+
.replace(/~\/\.claude\//g, pathPrefix)
|
|
514
|
+
.replace(/\$HOME\/\.claude\//g, toHomePrefix(pathPrefix))
|
|
515
|
+
// Local ./.claude/ refs → ./<dirName>/
|
|
516
|
+
.replace(/\.\/.claude\//g, `./${dirName}/`);
|
|
517
|
+
// Also replace platform-specific dir refs that may appear in source
|
|
518
|
+
if (platform === 'opencode') {
|
|
519
|
+
c = c.replace(/~\/\.opencode\//g, pathPrefix);
|
|
520
|
+
} else if (platform === 'gemini') {
|
|
521
|
+
c = c.replace(/~\/\.gemini\//g, pathPrefix);
|
|
522
|
+
} else if (platform === 'codex') {
|
|
523
|
+
c = c.replace(/~\/\.codex\//g, pathPrefix);
|
|
524
|
+
}
|
|
525
|
+
return c;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/** Install Claude Code / Windsurf commands (commands/learnship/ → target/commands/learnship/) */
|
|
529
|
+
function installClaudeCommands(srcDir, targetDir, pathPrefix) {
|
|
530
|
+
const destDir = path.join(targetDir, 'commands', 'learnship');
|
|
531
|
+
if (fs.existsSync(destDir)) fs.rmSync(destDir, { recursive: true });
|
|
532
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
533
|
+
let count = 0;
|
|
534
|
+
for (const f of fs.readdirSync(srcDir)) {
|
|
535
|
+
if (!f.endsWith('.md')) continue;
|
|
536
|
+
let c = fs.readFileSync(path.join(srcDir, f), 'utf8');
|
|
537
|
+
c = replacePaths(c, pathPrefix, 'claude');
|
|
538
|
+
fs.writeFileSync(path.join(destDir, f), c);
|
|
539
|
+
count++;
|
|
540
|
+
}
|
|
541
|
+
return count;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/** Install Claude Code native plugin skills (plugins/learnship/skills/) */
|
|
545
|
+
function installClaudePlugins(skillsSrc, targetDir) {
|
|
546
|
+
const pluginDir = path.join(targetDir, 'plugins', 'learnship');
|
|
547
|
+
const pluginSkillsDir = path.join(pluginDir, 'skills');
|
|
548
|
+
const pluginMetaDir = path.join(pluginDir, '.claude-plugin');
|
|
549
|
+
|
|
550
|
+
// Clean install
|
|
551
|
+
if (fs.existsSync(pluginDir)) fs.rmSync(pluginDir, { recursive: true });
|
|
552
|
+
fs.mkdirSync(pluginSkillsDir, { recursive: true });
|
|
553
|
+
fs.mkdirSync(pluginMetaDir, { recursive: true });
|
|
554
|
+
|
|
555
|
+
// Write plugin manifest
|
|
556
|
+
const manifest = {
|
|
557
|
+
name: 'learnship',
|
|
558
|
+
description: 'Learnship skills — agentic-learning partner and impeccable design system',
|
|
559
|
+
author: { name: 'favio-vazquez' },
|
|
560
|
+
};
|
|
561
|
+
fs.writeFileSync(
|
|
562
|
+
path.join(pluginMetaDir, 'plugin.json'),
|
|
563
|
+
JSON.stringify(manifest, null, 2) + '\n'
|
|
564
|
+
);
|
|
565
|
+
|
|
566
|
+
let count = 0;
|
|
567
|
+
|
|
568
|
+
for (const entry of fs.readdirSync(skillsSrc, { withFileTypes: true })) {
|
|
569
|
+
if (!entry.isDirectory()) continue;
|
|
570
|
+
const skillName = entry.name;
|
|
571
|
+
const srcPath = path.join(skillsSrc, skillName);
|
|
572
|
+
|
|
573
|
+
if (!fs.existsSync(path.join(srcPath, 'SKILL.md'))) continue;
|
|
574
|
+
|
|
575
|
+
const dest = path.join(pluginSkillsDir, skillName);
|
|
576
|
+
|
|
577
|
+
if (skillName === 'impeccable') {
|
|
578
|
+
// impeccable: copy root SKILL.md (rewriting sibling paths → references/ paths),
|
|
579
|
+
// then copy each sub-skill dir into references/
|
|
580
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
581
|
+
let skillMdContent = fs.readFileSync(path.join(srcPath, 'SKILL.md'), 'utf8');
|
|
582
|
+
// Rewrite repo-relative sibling links (e.g. adapt/SKILL.md) to post-install references/ paths
|
|
583
|
+
skillMdContent = skillMdContent.replace(/\]\((?!references\/)([^/)][^)]*\/SKILL\.md)\)/g, '](references/$1)');
|
|
584
|
+
fs.writeFileSync(path.join(dest, 'SKILL.md'), skillMdContent);
|
|
585
|
+
const refsDest = path.join(dest, 'references');
|
|
586
|
+
fs.mkdirSync(refsDest, { recursive: true });
|
|
587
|
+
for (const sub of fs.readdirSync(srcPath, { withFileTypes: true })) {
|
|
588
|
+
if (!sub.isDirectory()) continue;
|
|
589
|
+
const subSrc = path.join(srcPath, sub.name);
|
|
590
|
+
const subDest = path.join(refsDest, sub.name);
|
|
591
|
+
if (fs.existsSync(path.join(subSrc, 'SKILL.md'))) {
|
|
592
|
+
copyDir(subSrc, subDest, '', 'claude');
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
count++;
|
|
596
|
+
} else {
|
|
597
|
+
// agentic-learning and any future top-level skills — copy verbatim
|
|
598
|
+
copyDir(srcPath, dest, '', 'claude');
|
|
599
|
+
count++;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
return count;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/** Install OpenCode commands (flat: learnship-*.md) */
|
|
607
|
+
function installOpencodeCommands(srcDir, targetDir, pathPrefix) {
|
|
608
|
+
const destDir = path.join(targetDir, 'command');
|
|
609
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
610
|
+
// Remove old learnship-*.md
|
|
611
|
+
if (fs.existsSync(destDir)) {
|
|
612
|
+
for (const f of fs.readdirSync(destDir)) {
|
|
613
|
+
if (f.startsWith('learnship-') && f.endsWith('.md')) fs.unlinkSync(path.join(destDir, f));
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
let count = 0;
|
|
617
|
+
for (const f of fs.readdirSync(srcDir)) {
|
|
618
|
+
if (!f.endsWith('.md')) continue;
|
|
619
|
+
const baseName = f.replace('.md', '');
|
|
620
|
+
const destName = `learnship-${baseName}.md`;
|
|
621
|
+
let c = fs.readFileSync(path.join(srcDir, f), 'utf8');
|
|
622
|
+
c = replacePaths(c, pathPrefix, 'opencode');
|
|
623
|
+
c = convertToOpencode(c);
|
|
624
|
+
fs.writeFileSync(path.join(destDir, destName), c);
|
|
625
|
+
count++;
|
|
626
|
+
}
|
|
627
|
+
return count;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/** Install Gemini CLI commands (commands/learnship/*.toml) */
|
|
631
|
+
function installGeminiCommands(srcDir, targetDir, pathPrefix) {
|
|
632
|
+
const destDir = path.join(targetDir, 'commands', 'learnship');
|
|
633
|
+
if (fs.existsSync(destDir)) fs.rmSync(destDir, { recursive: true });
|
|
634
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
635
|
+
let count = 0;
|
|
636
|
+
for (const f of fs.readdirSync(srcDir)) {
|
|
637
|
+
if (!f.endsWith('.md')) continue;
|
|
638
|
+
let c = fs.readFileSync(path.join(srcDir, f), 'utf8');
|
|
639
|
+
c = replacePaths(c, pathPrefix, 'gemini');
|
|
640
|
+
const toml = convertToGeminiToml(c);
|
|
641
|
+
const destName = f.replace('.md', '.toml');
|
|
642
|
+
fs.writeFileSync(path.join(destDir, destName), toml);
|
|
643
|
+
count++;
|
|
644
|
+
}
|
|
645
|
+
return count;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/* Install Codex skills (skills/learnship-NAME/SKILL.md) */
|
|
649
|
+
function installCodexSkills(srcDir, targetDir, pathPrefix) {
|
|
650
|
+
const skillsDir = path.join(targetDir, 'skills');
|
|
651
|
+
fs.mkdirSync(skillsDir, { recursive: true });
|
|
652
|
+
// Remove old learnship-* skill dirs
|
|
653
|
+
for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
|
|
654
|
+
if (entry.isDirectory() && entry.name.startsWith('learnship-')) {
|
|
655
|
+
fs.rmSync(path.join(skillsDir, entry.name), { recursive: true });
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
let count = 0;
|
|
659
|
+
for (const f of fs.readdirSync(srcDir)) {
|
|
660
|
+
if (!f.endsWith('.md')) continue;
|
|
661
|
+
const baseName = f.replace('.md', '');
|
|
662
|
+
const skillName = `learnship-${baseName}`;
|
|
663
|
+
const skillDir = path.join(skillsDir, skillName);
|
|
664
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
665
|
+
let c = fs.readFileSync(path.join(srcDir, f), 'utf8');
|
|
666
|
+
c = replacePaths(c, pathPrefix, 'codex');
|
|
667
|
+
c = convertToCodexSkill(c, skillName);
|
|
668
|
+
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), c);
|
|
669
|
+
count++;
|
|
670
|
+
}
|
|
671
|
+
return count;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Generate the learnship config block for Codex config.toml.
|
|
676
|
+
*/
|
|
677
|
+
function generateCodexConfigBlock(agents) {
|
|
678
|
+
const lines = [
|
|
679
|
+
LEARNSHIP_CODEX_MARKER,
|
|
680
|
+
'[features]',
|
|
681
|
+
'multi_agent = true',
|
|
682
|
+
'default_mode_request_user_input = true',
|
|
683
|
+
'',
|
|
684
|
+
'[agents]',
|
|
685
|
+
'max_threads = 4',
|
|
686
|
+
'max_depth = 2',
|
|
687
|
+
'',
|
|
688
|
+
];
|
|
689
|
+
for (const { name, description } of agents) {
|
|
690
|
+
lines.push(`[agents.${name}]`);
|
|
691
|
+
lines.push(`description = ${JSON.stringify(description)}`);
|
|
692
|
+
lines.push(`config_file = "agents/${name}.toml"`);
|
|
693
|
+
lines.push('');
|
|
694
|
+
}
|
|
695
|
+
return lines.join('\n');
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Strip learnship sections from Codex config.toml content.
|
|
700
|
+
* Returns cleaned content, or null if file would be empty after stripping.
|
|
701
|
+
*/
|
|
702
|
+
function stripLearnshipFromCodexConfig(content) {
|
|
703
|
+
const markerIndex = content.indexOf(LEARNSHIP_CODEX_MARKER);
|
|
704
|
+
if (markerIndex !== -1) {
|
|
705
|
+
let before = content.substring(0, markerIndex).trimEnd();
|
|
706
|
+
before = before.replace(/^multi_agent\s*=\s*true\s*\n?/m, '');
|
|
707
|
+
before = before.replace(/^default_mode_request_user_input\s*=\s*true\s*\n?/m, '');
|
|
708
|
+
before = before.replace(/^\[features\]\s*\n(?=\[|$)/m, '');
|
|
709
|
+
before = before.replace(/\n{3,}/g, '\n\n').trim();
|
|
710
|
+
if (!before) return null;
|
|
711
|
+
return before + '\n';
|
|
712
|
+
}
|
|
713
|
+
// No marker — clean any learnship-injected keys that may have leaked
|
|
714
|
+
let cleaned = content;
|
|
715
|
+
cleaned = cleaned.replace(/^multi_agent\s*=\s*true\s*\n?/m, '');
|
|
716
|
+
cleaned = cleaned.replace(/^default_mode_request_user_input\s*=\s*true\s*\n?/m, '');
|
|
717
|
+
cleaned = cleaned.replace(/^\[agents\.learnship-[^\]]+\]\n(?:(?!\[)[^\n]*\n?)*/gm, '');
|
|
718
|
+
cleaned = cleaned.replace(/^\[features\]\s*\n(?=\[|$)/m, '');
|
|
719
|
+
cleaned = cleaned.replace(/^\[agents\]\s*\n(?=\[|$)/m, '');
|
|
720
|
+
cleaned = cleaned.replace(/\n{3,}/g, '\n\n').trim();
|
|
721
|
+
if (!cleaned) return null;
|
|
722
|
+
return cleaned + '\n';
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* Merge learnship config block into existing or new config.toml.
|
|
727
|
+
* Three cases: new file, existing with learnship marker, existing without marker.
|
|
728
|
+
*/
|
|
729
|
+
function mergeCodexConfig(configPath, learnshipBlock) {
|
|
730
|
+
// Case 1: No config.toml — create fresh
|
|
731
|
+
if (!fs.existsSync(configPath)) {
|
|
732
|
+
fs.writeFileSync(configPath, learnshipBlock + '\n');
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
const existing = fs.readFileSync(configPath, 'utf8');
|
|
736
|
+
const markerIndex = existing.indexOf(LEARNSHIP_CODEX_MARKER);
|
|
737
|
+
|
|
738
|
+
// Case 2: Has learnship marker — truncate and re-append
|
|
739
|
+
if (markerIndex !== -1) {
|
|
740
|
+
let before = existing.substring(0, markerIndex).trimEnd();
|
|
741
|
+
if (before) {
|
|
742
|
+
before = before.replace(/^\[agents\.learnship-[^\]]+\]\n(?:(?!\[)[^\n]*\n?)*/gm, '');
|
|
743
|
+
before = before.replace(/^\[agents\]\n(?:(?!\[)[^\n]*\n?)*/m, '');
|
|
744
|
+
before = before.replace(/\n{3,}/g, '\n\n').trimEnd();
|
|
745
|
+
const hasFeatures = /^\[features\]\s*$/m.test(before);
|
|
746
|
+
if (hasFeatures) {
|
|
747
|
+
if (!before.includes('multi_agent')) before = before.replace(/^\[features\]\s*$/m, '[features]\nmulti_agent = true');
|
|
748
|
+
if (!before.includes('default_mode_request_user_input')) before = before.replace(/^\[features\].*$/m, '$&\ndefault_mode_request_user_input = true');
|
|
749
|
+
const block = LEARNSHIP_CODEX_MARKER + '\n' + learnshipBlock.substring(learnshipBlock.indexOf('[agents]'));
|
|
750
|
+
fs.writeFileSync(configPath, before + '\n\n' + block + '\n');
|
|
751
|
+
} else {
|
|
752
|
+
fs.writeFileSync(configPath, before + '\n\n' + learnshipBlock + '\n');
|
|
753
|
+
}
|
|
754
|
+
} else {
|
|
755
|
+
fs.writeFileSync(configPath, learnshipBlock + '\n');
|
|
756
|
+
}
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// Case 3: No marker — inject features if needed, append agents
|
|
761
|
+
let content = existing;
|
|
762
|
+
const featuresRegex = /^\[features\]\s*$/m;
|
|
763
|
+
const hasFeatures = featuresRegex.test(content);
|
|
764
|
+
if (hasFeatures) {
|
|
765
|
+
if (!content.includes('multi_agent')) content = content.replace(featuresRegex, '[features]\nmulti_agent = true');
|
|
766
|
+
if (!content.includes('default_mode_request_user_input')) content = content.replace(/^\[features\].*$/m, '$&\ndefault_mode_request_user_input = true');
|
|
767
|
+
const agentsBlock = learnshipBlock.substring(learnshipBlock.indexOf('[agents]'));
|
|
768
|
+
content = content.trimEnd() + '\n\n' + LEARNSHIP_CODEX_MARKER + '\n' + agentsBlock + '\n';
|
|
769
|
+
} else {
|
|
770
|
+
content = content.trimEnd() + '\n\n' + learnshipBlock + '\n';
|
|
771
|
+
}
|
|
772
|
+
fs.writeFileSync(configPath, content);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/** Install Codex agent .toml files and update config.toml */
|
|
776
|
+
function installCodexAgents(agentsSrcDir, targetDir, pathPrefix) {
|
|
777
|
+
const agentsDir = path.join(targetDir, 'agents');
|
|
778
|
+
fs.mkdirSync(agentsDir, { recursive: true });
|
|
779
|
+
// Remove stale learnship agent .toml files
|
|
780
|
+
for (const f of fs.readdirSync(agentsDir)) {
|
|
781
|
+
if (f.startsWith('learnship-') && f.endsWith('.toml')) fs.unlinkSync(path.join(agentsDir, f));
|
|
782
|
+
}
|
|
783
|
+
const agents = [];
|
|
784
|
+
for (const f of fs.readdirSync(agentsSrcDir)) {
|
|
785
|
+
if (!f.startsWith('learnship-') || !f.endsWith('.md')) continue;
|
|
786
|
+
let content = fs.readFileSync(path.join(agentsSrcDir, f), 'utf8');
|
|
787
|
+
// Replace ~/.claude/ paths before generating TOML
|
|
788
|
+
content = content.replace(/~\/\.claude\//g, pathPrefix);
|
|
789
|
+
content = content.replace(/\$HOME\/\.claude\//g, toHomePrefix(pathPrefix));
|
|
790
|
+
// Convert to Codex agent format
|
|
791
|
+
content = convertClaudeAgentToCodexAgent(content);
|
|
792
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
793
|
+
const name = frontmatter ? (getFrontmatterField(frontmatter, 'name') || f.replace('.md','')) : f.replace('.md','');
|
|
794
|
+
const description = frontmatter ? (getFrontmatterField(frontmatter, 'description') || '').replace(/\s+/g,' ').trim() : '';
|
|
795
|
+
agents.push({ name, description });
|
|
796
|
+
const sandboxMode = CODEX_AGENT_SANDBOX[name] || 'workspace-write';
|
|
797
|
+
const tomlContent = `sandbox_mode = "${sandboxMode}"\ndeveloper_instructions = """\n${body.trim()}\n"""\n`;
|
|
798
|
+
fs.writeFileSync(path.join(agentsDir, `${name}.toml`), tomlContent);
|
|
799
|
+
}
|
|
800
|
+
const learnshipBlock = generateCodexConfigBlock(agents);
|
|
801
|
+
mergeCodexConfig(path.join(targetDir, 'config.toml'), learnshipBlock);
|
|
802
|
+
return agents.length;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/** Install agent .md files for a platform (not Codex — handled by installCodexAgents) */
|
|
806
|
+
function installAgents(agentsSrcDir, targetDir, pathPrefix, platform) {
|
|
807
|
+
const destDir = path.join(targetDir, 'agents');
|
|
808
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
809
|
+
// Remove stale learnship agent .md files before re-installing
|
|
810
|
+
for (const f of fs.readdirSync(destDir)) {
|
|
811
|
+
if (f.startsWith('learnship-') && f.endsWith('.md')) fs.unlinkSync(path.join(destDir, f));
|
|
812
|
+
}
|
|
813
|
+
let count = 0;
|
|
814
|
+
for (const f of fs.readdirSync(agentsSrcDir)) {
|
|
815
|
+
if (!f.startsWith('learnship-') || !f.endsWith('.md')) continue;
|
|
816
|
+
let c = fs.readFileSync(path.join(agentsSrcDir, f), 'utf8');
|
|
817
|
+
c = replacePaths(c, pathPrefix, platform);
|
|
818
|
+
if (platform === 'gemini') c = convertAgentForGemini(c);
|
|
819
|
+
else if (platform === 'opencode') c = convertToOpencode(c);
|
|
820
|
+
fs.writeFileSync(path.join(destDir, f), c);
|
|
821
|
+
count++;
|
|
822
|
+
}
|
|
823
|
+
return count;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/**
|
|
827
|
+
* Parse JSONC (JSON with Comments) by stripping comments and trailing commas.
|
|
828
|
+
* OpenCode supports JSONC so users may have // comments in opencode.json.
|
|
829
|
+
*/
|
|
830
|
+
function parseJsonc(content) {
|
|
831
|
+
if (content.charCodeAt(0) === 0xFEFF) content = content.slice(1); // strip BOM
|
|
832
|
+
let result = '';
|
|
833
|
+
let inString = false;
|
|
834
|
+
let i = 0;
|
|
835
|
+
while (i < content.length) {
|
|
836
|
+
const char = content[i];
|
|
837
|
+
const next = content[i + 1];
|
|
838
|
+
if (inString) {
|
|
839
|
+
result += char;
|
|
840
|
+
if (char === '\\' && i + 1 < content.length) { result += next; i += 2; continue; }
|
|
841
|
+
if (char === '"') inString = false;
|
|
842
|
+
i++;
|
|
843
|
+
} else {
|
|
844
|
+
if (char === '"') { inString = true; result += char; i++; }
|
|
845
|
+
else if (char === '/' && next === '/') { while (i < content.length && content[i] !== '\n') i++; }
|
|
846
|
+
else if (char === '/' && next === '*') {
|
|
847
|
+
i += 2;
|
|
848
|
+
while (i < content.length - 1 && !(content[i] === '*' && content[i + 1] === '/')) i++;
|
|
849
|
+
i += 2;
|
|
850
|
+
} else { result += char; i++; }
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
result = result.replace(/,(\s*[}\]])/g, '$1'); // remove trailing commas
|
|
854
|
+
return JSON.parse(result);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/** Configure OpenCode permissions to allow reading learnship reference docs */
|
|
858
|
+
function configureOpencodePermissions(targetDir, learnshipDir) {
|
|
859
|
+
const configPath = path.join(targetDir, 'opencode.json');
|
|
860
|
+
let config = {};
|
|
861
|
+
if (fs.existsSync(configPath)) {
|
|
862
|
+
try { config = parseJsonc(fs.readFileSync(configPath, 'utf8')); }
|
|
863
|
+
catch (e) {
|
|
864
|
+
console.log(` ${yellow}⚠${reset} Could not parse opencode.json — skipping permission config`);
|
|
865
|
+
console.log(` ${dim}Reason: ${e.message}${reset}`);
|
|
866
|
+
console.log(` ${dim}Your config was NOT modified. Fix the syntax manually if needed.${reset}`);
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
const defaultDir = path.join(os.homedir(), '.config', 'opencode');
|
|
871
|
+
const learnshipPath = targetDir === defaultDir
|
|
872
|
+
? '~/.config/opencode/learnship/*'
|
|
873
|
+
: `${learnshipDir.replace(/\\/g, '/')}/*`;
|
|
874
|
+
if (!config.permission) config.permission = {};
|
|
875
|
+
if (!config.permission.read) config.permission.read = {};
|
|
876
|
+
if (!config.permission.external_directory) config.permission.external_directory = {};
|
|
877
|
+
if (config.permission.read[learnshipPath] === 'allow' &&
|
|
878
|
+
config.permission.external_directory[learnshipPath] === 'allow') return; // already set
|
|
879
|
+
config.permission.read[learnshipPath] = 'allow';
|
|
880
|
+
config.permission.external_directory[learnshipPath] = 'allow';
|
|
881
|
+
writeSettings(configPath, config);
|
|
882
|
+
console.log(` ${green}✓${reset} Configured read permissions in opencode.json`);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
/**
|
|
886
|
+
* Scan installed files for leaked ~/.claude paths in non-Claude platforms.
|
|
887
|
+
* GSD pattern: warn if any .md/.toml file still contains the source platform path.
|
|
888
|
+
*/
|
|
889
|
+
function scanForLeakedPaths(targetDir, platform) {
|
|
890
|
+
if (platform === 'claude' || platform === 'windsurf') return;
|
|
891
|
+
const leaks = [];
|
|
892
|
+
function scan(dir) {
|
|
893
|
+
if (!fs.existsSync(dir)) return;
|
|
894
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
895
|
+
const full = path.join(dir, entry.name);
|
|
896
|
+
if (entry.isDirectory()) { scan(full); continue; }
|
|
897
|
+
if (!entry.name.endsWith('.md') && !entry.name.endsWith('.toml')) continue;
|
|
898
|
+
if (entry.name === 'CHANGELOG.md') continue;
|
|
899
|
+
const content = fs.readFileSync(full, 'utf8');
|
|
900
|
+
const matches = content.match(/(?:~|\$HOME)\/\.claude\b/g);
|
|
901
|
+
if (matches) leaks.push({ file: full.replace(targetDir + '/', ''), count: matches.length });
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
scan(targetDir);
|
|
905
|
+
if (leaks.length > 0) {
|
|
906
|
+
const total = leaks.reduce((s, l) => s + l.count, 0);
|
|
907
|
+
console.warn(`\n ${yellow}⚠${reset} Found ${total} unreplaced .claude path(s) in ${leaks.length} file(s):`);
|
|
908
|
+
for (const leak of leaks.slice(0, 5)) console.warn(` ${dim}${leak.file}${reset} (${leak.count})`);
|
|
909
|
+
if (leaks.length > 5) console.warn(` ${dim}... and ${leaks.length - 5} more${reset}`);
|
|
910
|
+
console.warn(` ${dim}These paths may not resolve correctly for ${getPlatformLabel(platform)}.${reset}`);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
// ─── Main install function ─────────────────────────────────────────────────
|
|
915
|
+
function install(platform, isGlobal) {
|
|
916
|
+
const src = path.join(__dirname, '..');
|
|
917
|
+
const targetDir = isGlobal ? getGlobalDir(platform) : path.join(process.cwd(), getDirName(platform));
|
|
918
|
+
const pathPrefix = `${targetDir.replace(/\\/g, '/')}/learnship/`;
|
|
919
|
+
const label = getPlatformLabel(platform);
|
|
920
|
+
const locationLabel = targetDir.replace(os.homedir(), '~');
|
|
921
|
+
|
|
922
|
+
console.log(`\n Installing for ${cyan}${label}${reset} → ${cyan}${locationLabel}${reset}\n`);
|
|
923
|
+
|
|
924
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
925
|
+
|
|
926
|
+
const learnshipSrc = path.join(src, 'learnship');
|
|
927
|
+
const commandsSrc = path.join(src, 'commands', 'learnship');
|
|
928
|
+
const agentsSrc = path.join(src, 'agents');
|
|
929
|
+
const skillsSrc = path.join(src, '.windsurf', 'skills');
|
|
930
|
+
const failures = [];
|
|
931
|
+
|
|
932
|
+
// 1. Install learnship/ payload (workflows, references, templates)
|
|
933
|
+
const learnshipDest = path.join(targetDir, 'learnship');
|
|
934
|
+
copyDir(learnshipSrc, learnshipDest, pathPrefix, platform);
|
|
935
|
+
if (verifyInstalled(learnshipDest, 'learnship/')) {
|
|
936
|
+
console.log(` ${green}✓${reset} Installed learnship/ (workflows, references, templates)`);
|
|
937
|
+
} else { failures.push('learnship/'); }
|
|
938
|
+
|
|
939
|
+
// 2. Write VERSION file into learnship/ dir
|
|
940
|
+
fs.writeFileSync(path.join(learnshipDest, 'VERSION'), pkg.version);
|
|
941
|
+
console.log(` ${green}✓${reset} Wrote VERSION (${pkg.version})`);
|
|
942
|
+
|
|
943
|
+
// 2b. Install skills
|
|
944
|
+
// Windsurf: native skill support — copy to targetDir/skills/ (i.e. .windsurf/skills/)
|
|
945
|
+
// Others: copy to learnship/skills/ so the AI loads them as context files
|
|
946
|
+
if (fs.existsSync(skillsSrc)) {
|
|
947
|
+
const skillsDest = platform === 'windsurf'
|
|
948
|
+
? path.join(targetDir, 'skills')
|
|
949
|
+
: path.join(learnshipDest, 'skills');
|
|
950
|
+
fs.mkdirSync(skillsDest, { recursive: true });
|
|
951
|
+
let skillCount = 0;
|
|
952
|
+
for (const entry of fs.readdirSync(skillsSrc, { withFileTypes: true })) {
|
|
953
|
+
if (!entry.isDirectory()) continue;
|
|
954
|
+
copyDir(path.join(skillsSrc, entry.name), path.join(skillsDest, entry.name), pathPrefix, platform);
|
|
955
|
+
skillCount++;
|
|
956
|
+
}
|
|
957
|
+
if (skillCount > 0) {
|
|
958
|
+
const loc = platform === 'windsurf' ? 'skills/' : 'learnship/skills/';
|
|
959
|
+
console.log(` ${green}✓${reset} Installed ${skillCount} skills to ${loc}`);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// 3. Install commands (platform-specific format)
|
|
964
|
+
if (platform === 'windsurf') {
|
|
965
|
+
const wfDest = path.join(targetDir, 'workflows');
|
|
966
|
+
fs.mkdirSync(wfDest, { recursive: true });
|
|
967
|
+
let count = 0;
|
|
968
|
+
for (const f of fs.readdirSync(path.join(learnshipSrc, 'workflows'))) {
|
|
969
|
+
if (!f.endsWith('.md')) continue;
|
|
970
|
+
fs.copyFileSync(path.join(learnshipSrc, 'workflows', f), path.join(wfDest, f));
|
|
971
|
+
count++;
|
|
972
|
+
}
|
|
973
|
+
// Copy templates/ and references/ so @./templates/ and @./references/ resolve in workflows
|
|
974
|
+
for (const subdir of ['templates', 'references']) {
|
|
975
|
+
const srcSub = path.join(learnshipSrc, subdir);
|
|
976
|
+
const destSub = path.join(wfDest, subdir);
|
|
977
|
+
if (fs.existsSync(srcSub)) {
|
|
978
|
+
copyDir(srcSub, destSub, pathPrefix, platform);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
console.log(` ${green}✓${reset} Installed ${count} workflows to workflows/`);
|
|
982
|
+
} else if (platform === 'claude') {
|
|
983
|
+
const count = installClaudeCommands(commandsSrc, targetDir, pathPrefix);
|
|
984
|
+
if (verifyInstalled(path.join(targetDir, 'commands', 'learnship'), 'commands/learnship/')) {
|
|
985
|
+
console.log(` ${green}✓${reset} Installed ${count} commands to commands/learnship/`);
|
|
986
|
+
} else { failures.push('commands/learnship/'); }
|
|
987
|
+
const aCount = installAgents(agentsSrc, targetDir, pathPrefix, 'claude');
|
|
988
|
+
if (aCount > 0) console.log(` ${green}✓${reset} Installed ${aCount} agents to agents/`);
|
|
989
|
+
else failures.push('agents/');
|
|
990
|
+
const pCount = installClaudePlugins(skillsSrc, targetDir);
|
|
991
|
+
if (pCount > 0) {
|
|
992
|
+
console.log(` ${green}✓${reset} Installed ${pCount} skills to plugins/learnship/skills/`);
|
|
993
|
+
} else {
|
|
994
|
+
failures.push('plugins/learnship/skills/');
|
|
995
|
+
}
|
|
996
|
+
} else if (platform === 'opencode') {
|
|
997
|
+
const count = installOpencodeCommands(commandsSrc, targetDir, pathPrefix);
|
|
998
|
+
console.log(` ${green}✓${reset} Installed ${count} commands to command/ (flat)`);
|
|
999
|
+
const aCount = installAgents(agentsSrc, targetDir, pathPrefix, 'opencode');
|
|
1000
|
+
if (aCount > 0) console.log(` ${green}✓${reset} Installed ${aCount} agents to agents/`);
|
|
1001
|
+
configureOpencodePermissions(targetDir, learnshipDest);
|
|
1002
|
+
} else if (platform === 'gemini') {
|
|
1003
|
+
const count = installGeminiCommands(commandsSrc, targetDir, pathPrefix);
|
|
1004
|
+
if (verifyInstalled(path.join(targetDir, 'commands', 'learnship'), 'commands/learnship/')) {
|
|
1005
|
+
console.log(` ${green}✓${reset} Installed ${count} commands to commands/learnship/ (TOML)`);
|
|
1006
|
+
} else { failures.push('commands/learnship/'); }
|
|
1007
|
+
const aCount = installAgents(agentsSrc, targetDir, pathPrefix, 'gemini');
|
|
1008
|
+
if (aCount > 0) console.log(` ${green}✓${reset} Installed ${aCount} agents to agents/`);
|
|
1009
|
+
// Gemini requires experimental agents enabled
|
|
1010
|
+
const settingsPath = path.join(targetDir, 'settings.json');
|
|
1011
|
+
const settings = readSettings(settingsPath);
|
|
1012
|
+
if (!settings.experimental) settings.experimental = {};
|
|
1013
|
+
if (!settings.experimental.enableAgents) {
|
|
1014
|
+
settings.experimental.enableAgents = true;
|
|
1015
|
+
writeSettings(settingsPath, settings);
|
|
1016
|
+
console.log(` ${green}✓${reset} Enabled experimental.enableAgents in settings.json`);
|
|
1017
|
+
}
|
|
1018
|
+
} else if (platform === 'codex') {
|
|
1019
|
+
const count = installCodexSkills(commandsSrc, targetDir, pathPrefix);
|
|
1020
|
+
console.log(` ${green}✓${reset} Installed ${count} skills to skills/`);
|
|
1021
|
+
const aCount = installCodexAgents(agentsSrc, targetDir, pathPrefix);
|
|
1022
|
+
console.log(` ${green}✓${reset} Installed ${aCount} agents + config.toml (sandbox modes: read-only for checkers)`);
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
if (failures.length > 0) {
|
|
1026
|
+
console.error(`\n ${yellow}Installation incomplete!${reset} Failed: ${failures.join(', ')}`);
|
|
1027
|
+
process.exit(1);
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// 4. Scan for leaked .claude paths
|
|
1031
|
+
scanForLeakedPaths(targetDir, platform);
|
|
1032
|
+
|
|
1033
|
+
// 5. Post-install tips
|
|
1034
|
+
const firstCmd = platform === 'windsurf' ? '/ls' :
|
|
1035
|
+
platform === 'claude' ? '/learnship:ls' :
|
|
1036
|
+
platform === 'opencode' ? '/learnship-ls' :
|
|
1037
|
+
platform === 'gemini' ? '/learnship:ls' : '$learnship-ls';
|
|
1038
|
+
console.log(`\n ${green}Done!${reset} Open a project in ${label} and run ${cyan}${firstCmd}${reset}.`);
|
|
1039
|
+
console.log(` ${dim}First time? Run ${cyan}${platform === 'windsurf' ? '/new-project' : platform === 'claude' ? '/learnship:new-project' : platform === 'opencode' ? '/learnship-new-project' : platform === 'gemini' ? '/learnship:new-project' : '$learnship-new-project'}${reset}${dim} to initialize your project and create AGENTS.md.${reset}`);
|
|
1040
|
+
if (platform !== 'windsurf') {
|
|
1041
|
+
console.log(` ${dim}Enable parallel subagents: add ${cyan}"parallelization": true${reset}${dim} to .planning/config.json${reset}`);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// ─── Uninstall function ────────────────────────────────────────────────────
|
|
1046
|
+
function uninstall(platform, isGlobal) {
|
|
1047
|
+
const targetDir = isGlobal ? getGlobalDir(platform) : path.join(process.cwd(), getDirName(platform));
|
|
1048
|
+
const label = getPlatformLabel(platform);
|
|
1049
|
+
const locationLabel = targetDir.replace(os.homedir(), '~');
|
|
1050
|
+
console.log(`\n Uninstalling learnship from ${cyan}${label}${reset} at ${cyan}${locationLabel}${reset}\n`);
|
|
1051
|
+
|
|
1052
|
+
if (!fs.existsSync(targetDir)) {
|
|
1053
|
+
console.log(` ${yellow}⚠${reset} Directory not found — nothing to uninstall.`);
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
let removed = 0;
|
|
1058
|
+
|
|
1059
|
+
// 1. Remove learnship/ payload
|
|
1060
|
+
const learnshipDir = path.join(targetDir, 'learnship');
|
|
1061
|
+
if (fs.existsSync(learnshipDir)) {
|
|
1062
|
+
fs.rmSync(learnshipDir, { recursive: true });
|
|
1063
|
+
console.log(` ${green}✓${reset} Removed learnship/`);
|
|
1064
|
+
removed++;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// 2. Remove platform-specific command files
|
|
1068
|
+
if (platform === 'claude' || platform === 'windsurf') {
|
|
1069
|
+
const commandsDir = path.join(targetDir, 'commands', 'learnship');
|
|
1070
|
+
if (fs.existsSync(commandsDir)) { fs.rmSync(commandsDir, { recursive: true }); removed++; console.log(` ${green}✓${reset} Removed commands/learnship/`); }
|
|
1071
|
+
}
|
|
1072
|
+
if (platform === 'claude') {
|
|
1073
|
+
const pluginDir = path.join(targetDir, 'plugins', 'learnship');
|
|
1074
|
+
if (fs.existsSync(pluginDir)) {
|
|
1075
|
+
fs.rmSync(pluginDir, { recursive: true });
|
|
1076
|
+
removed++;
|
|
1077
|
+
console.log(` ${green}✓${reset} Removed plugins/learnship/`);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
if (platform === 'opencode') {
|
|
1081
|
+
const commandDir = path.join(targetDir, 'command');
|
|
1082
|
+
if (fs.existsSync(commandDir)) {
|
|
1083
|
+
let n = 0;
|
|
1084
|
+
for (const f of fs.readdirSync(commandDir)) {
|
|
1085
|
+
if (f.startsWith('learnship-') && f.endsWith('.md')) { fs.unlinkSync(path.join(commandDir, f)); n++; }
|
|
1086
|
+
}
|
|
1087
|
+
if (n > 0) { removed++; console.log(` ${green}✓${reset} Removed ${n} learnship-*.md from command/`); }
|
|
1088
|
+
}
|
|
1089
|
+
// Clean opencode.json permissions
|
|
1090
|
+
const ocConfig = path.join(targetDir, 'opencode.json');
|
|
1091
|
+
if (fs.existsSync(ocConfig)) {
|
|
1092
|
+
try {
|
|
1093
|
+
const cfg = JSON.parse(fs.readFileSync(ocConfig, 'utf8'));
|
|
1094
|
+
let modified = false;
|
|
1095
|
+
if (cfg.permission) {
|
|
1096
|
+
for (const permType of ['read', 'external_directory']) {
|
|
1097
|
+
if (cfg.permission[permType]) {
|
|
1098
|
+
for (const key of Object.keys(cfg.permission[permType])) {
|
|
1099
|
+
if (key.includes('learnship')) { delete cfg.permission[permType][key]; modified = true; }
|
|
1100
|
+
}
|
|
1101
|
+
if (Object.keys(cfg.permission[permType]).length === 0) delete cfg.permission[permType];
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
if (Object.keys(cfg.permission).length === 0) delete cfg.permission;
|
|
1105
|
+
}
|
|
1106
|
+
if (modified) { fs.writeFileSync(ocConfig, JSON.stringify(cfg, null, 2) + '\n'); removed++; console.log(` ${green}✓${reset} Removed learnship permissions from opencode.json`); }
|
|
1107
|
+
} catch { /* ignore */ }
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
if (platform === 'gemini') {
|
|
1111
|
+
const commandsDir = path.join(targetDir, 'commands', 'learnship');
|
|
1112
|
+
if (fs.existsSync(commandsDir)) { fs.rmSync(commandsDir, { recursive: true }); removed++; console.log(` ${green}✓${reset} Removed commands/learnship/`); }
|
|
1113
|
+
}
|
|
1114
|
+
if (platform === 'codex') {
|
|
1115
|
+
// Remove skill directories
|
|
1116
|
+
const skillsDir = path.join(targetDir, 'skills');
|
|
1117
|
+
if (fs.existsSync(skillsDir)) {
|
|
1118
|
+
let n = 0;
|
|
1119
|
+
for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
|
|
1120
|
+
if (entry.isDirectory() && entry.name.startsWith('learnship-')) {
|
|
1121
|
+
fs.rmSync(path.join(skillsDir, entry.name), { recursive: true }); n++;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
if (n > 0) { removed++; console.log(` ${green}✓${reset} Removed ${n} learnship skill directories`); }
|
|
1125
|
+
}
|
|
1126
|
+
// Remove agent .toml files
|
|
1127
|
+
const agentsDir2 = path.join(targetDir, 'agents');
|
|
1128
|
+
if (fs.existsSync(agentsDir2)) {
|
|
1129
|
+
let n = 0;
|
|
1130
|
+
for (const f of fs.readdirSync(agentsDir2)) {
|
|
1131
|
+
if (f.startsWith('learnship-') && f.endsWith('.toml')) { fs.unlinkSync(path.join(agentsDir2, f)); n++; }
|
|
1132
|
+
}
|
|
1133
|
+
if (n > 0) { removed++; console.log(` ${green}✓${reset} Removed ${n} agent .toml configs`); }
|
|
1134
|
+
}
|
|
1135
|
+
// Clean config.toml
|
|
1136
|
+
const configPath = path.join(targetDir, 'config.toml');
|
|
1137
|
+
if (fs.existsSync(configPath)) {
|
|
1138
|
+
const content = fs.readFileSync(configPath, 'utf8');
|
|
1139
|
+
const cleaned = stripLearnshipFromCodexConfig(content);
|
|
1140
|
+
if (cleaned === null) {
|
|
1141
|
+
fs.unlinkSync(configPath); removed++;
|
|
1142
|
+
console.log(` ${green}✓${reset} Removed config.toml (was learnship-only)`);
|
|
1143
|
+
} else if (cleaned !== content) {
|
|
1144
|
+
fs.writeFileSync(configPath, cleaned); removed++;
|
|
1145
|
+
console.log(` ${green}✓${reset} Cleaned learnship sections from config.toml`);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
// 3. Remove learnship agent .md files
|
|
1151
|
+
const agentsDir = path.join(targetDir, 'agents');
|
|
1152
|
+
if (fs.existsSync(agentsDir)) {
|
|
1153
|
+
let n = 0;
|
|
1154
|
+
for (const f of fs.readdirSync(agentsDir)) {
|
|
1155
|
+
if (f.startsWith('learnship-') && f.endsWith('.md')) { fs.unlinkSync(path.join(agentsDir, f)); n++; }
|
|
1156
|
+
}
|
|
1157
|
+
if (n > 0) { removed++; console.log(` ${green}✓${reset} Removed ${n} learnship agent files`); }
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
if (removed === 0) console.log(` ${yellow}⚠${reset} No learnship files found.`);
|
|
1161
|
+
else console.log(`\n ${green}Done!${reset} learnship uninstalled from ${label}. Your other files and settings were preserved.`);
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
// ─── Interactive prompt ────────────────────────────────────────────────────
|
|
1165
|
+
async function promptUser() {
|
|
1166
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1167
|
+
const ask = (q) => new Promise(resolve => rl.question(q, resolve));
|
|
1168
|
+
|
|
1169
|
+
console.log(` ${yellow}Select platform:${reset}`);
|
|
1170
|
+
console.log(` 1) Claude Code (/learnship:ls)`);
|
|
1171
|
+
console.log(` 2) OpenCode (/learnship-ls)`);
|
|
1172
|
+
console.log(` 3) Gemini CLI (/learnship:ls)`);
|
|
1173
|
+
console.log(` 4) Codex CLI ($learnship-ls)`);
|
|
1174
|
+
console.log(` 5) Windsurf (/ls — same as install.sh)`);
|
|
1175
|
+
console.log(` 6) All platforms`);
|
|
1176
|
+
|
|
1177
|
+
const platformChoice = await ask('\n Platform [1-6]: ');
|
|
1178
|
+
const platformMap = { '1': ['claude'], '2': ['opencode'], '3': ['gemini'], '4': ['codex'], '5': ['windsurf'], '6': ['claude','opencode','gemini','codex','windsurf'] };
|
|
1179
|
+
const platforms = platformMap[platformChoice.trim()] || ['claude'];
|
|
1180
|
+
|
|
1181
|
+
console.log(`\n ${yellow}Install scope:${reset}`);
|
|
1182
|
+
console.log(` 1) Global (recommended) — available in all projects`);
|
|
1183
|
+
console.log(` 2) Local — current project only`);
|
|
1184
|
+
const scopeChoice = await ask('\n Scope [1-2]: ');
|
|
1185
|
+
const isGlobal = scopeChoice.trim() !== '2';
|
|
1186
|
+
|
|
1187
|
+
rl.close();
|
|
1188
|
+
return { platforms, isGlobal };
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
// ─── Entry point ──────────────────────────────────────────────────────────
|
|
1192
|
+
async function main() {
|
|
1193
|
+
console.log(banner);
|
|
1194
|
+
|
|
1195
|
+
if (hasHelp) { console.log(helpText); process.exit(0); }
|
|
1196
|
+
|
|
1197
|
+
let platforms = selectedPlatforms;
|
|
1198
|
+
let isGlobal = hasGlobal || !hasLocal;
|
|
1199
|
+
|
|
1200
|
+
if (platforms.length === 0 && !hasUninstall) {
|
|
1201
|
+
// Interactive
|
|
1202
|
+
const result = await promptUser();
|
|
1203
|
+
platforms = result.platforms;
|
|
1204
|
+
isGlobal = result.isGlobal;
|
|
1205
|
+
} else if (platforms.length === 0 && hasUninstall) {
|
|
1206
|
+
console.error(` ${yellow}Error:${reset} Specify a platform to uninstall from. Example: npx learnship --claude --global --uninstall`);
|
|
1207
|
+
process.exit(1);
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
console.log('');
|
|
1211
|
+
for (const platform of platforms) {
|
|
1212
|
+
if (hasUninstall) uninstall(platform, isGlobal);
|
|
1213
|
+
else install(platform, isGlobal);
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
if (!process.env.LEARNSHIP_TEST_MODE) {
|
|
1218
|
+
main().catch(err => {
|
|
1219
|
+
console.error(` Error: ${err.message}`);
|
|
1220
|
+
process.exit(1);
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
// Test-only exports — allow unit testing without running main install logic
|
|
1225
|
+
if (process.env.LEARNSHIP_TEST_MODE) {
|
|
1226
|
+
module.exports = {
|
|
1227
|
+
convertToOpencode,
|
|
1228
|
+
convertToGeminiToml,
|
|
1229
|
+
convertToCodexSkill,
|
|
1230
|
+
convertClaudeAgentToCodexAgent,
|
|
1231
|
+
convertAgentForGemini,
|
|
1232
|
+
generateCodexConfigBlock,
|
|
1233
|
+
stripLearnshipFromCodexConfig,
|
|
1234
|
+
mergeCodexConfig,
|
|
1235
|
+
installCodexAgents,
|
|
1236
|
+
parseJsonc,
|
|
1237
|
+
replacePaths,
|
|
1238
|
+
toHomePrefix,
|
|
1239
|
+
LEARNSHIP_CODEX_MARKER,
|
|
1240
|
+
CODEX_AGENT_SANDBOX,
|
|
1241
|
+
};
|
|
1242
|
+
}
|