devlyn-cli 4.0.0 → 4.1.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/AGENTS.md +3 -3
- package/CLAUDE.md +1 -1
- package/README.md +23 -9
- package/bin/devlyn.js +285 -425
- package/bin/instruction-templates.json +79 -0
- package/bin/instructions.js +7 -1
- package/config/skills/_shared/codex-config.md +12 -12
- package/config/skills/devlyn-engines/SKILL.md +35 -9
- package/config/skills/devlyn-ideate/SKILL.md +32 -14
- package/config/skills/devlyn-queue/SKILL.md +36 -13
- package/config/skills/devlyn-queue/scripts/append.py +94 -0
- package/config/skills/devlyn-resolve/SKILL.md +35 -16
- package/config/skills/devlyn-resolve/references/phases/probe-derive.md +1 -1
- package/config/skills/devlyn-resolve/references/phases/verify.md +1 -1
- package/config/skills/devlyn-resolve/references/task-completion.md +9 -2
- package/optional-skills/better-auth-setup/SKILL.md +24 -18
- package/optional-skills/devlyn-reap/SKILL.md +34 -3
- package/optional-skills/dokkit/COMMANDS.md +4 -4
- package/optional-skills/dokkit/EXPORT.md +3 -3
- package/optional-skills/dokkit/FILLING.md +4 -4
- package/optional-skills/dokkit/IMAGE-SOURCING.md +2 -2
- package/optional-skills/dokkit/INGESTION.md +3 -3
- package/optional-skills/dokkit/PIPELINE.md +5 -4
- package/optional-skills/dokkit/SKILL.md +35 -5
- package/optional-skills/dokkit/STATE.md +1 -1
- package/optional-skills/dokkit/references/image-opportunity-heuristics.md +1 -1
- package/optional-skills/generate-skill/REFERENCE.md +10 -13
- package/optional-skills/generate-skill/SKILL.md +13 -5
- package/package.json +1 -1
package/bin/devlyn.js
CHANGED
|
@@ -10,7 +10,7 @@ const { execSync } = require('child_process');
|
|
|
10
10
|
const CONFIG_SOURCE = path.join(__dirname, '..', 'config');
|
|
11
11
|
const OPTIONAL_SKILLS_SOURCE = path.join(__dirname, '..', 'optional-skills');
|
|
12
12
|
const PKG = require('../package.json');
|
|
13
|
-
const { updateInstructions, InstructionError } = require('./instructions');
|
|
13
|
+
const { updateInstructions, InstructionError, holdsDevlynDefaults } = require('./instructions');
|
|
14
14
|
|
|
15
15
|
// The devlyn skill bundle installed into every skill-capable agent's loader
|
|
16
16
|
// directory. Single source of truth so codex/omp/pi stay in lockstep — adding a
|
|
@@ -31,7 +31,6 @@ const RENAMED_SKILLS = {
|
|
|
31
31
|
'devlyn:pencil-push': 'devlyn-pencil-push',
|
|
32
32
|
'devlyn:reap': 'devlyn-reap',
|
|
33
33
|
};
|
|
34
|
-
const DEVLYN_SKILL_DIR_STAMP = '__DEVLYN_SKILL_DIR__';
|
|
35
34
|
const DEVLYN_INSTALL_MARKER = '.devlyn-install.json';
|
|
36
35
|
|
|
37
36
|
// Every spelling a colon name can have on disk: its own and npm's U+F03A extraction alias.
|
|
@@ -47,71 +46,80 @@ const PRE_STANDARD_SKILL_MD_SHA256 = new Set([
|
|
|
47
46
|
'5f2e8b29609cbb2af72941579823bec598265ebf29cd3a375d6aa47d95b74354', // devlyn-pencil-push
|
|
48
47
|
]);
|
|
49
48
|
|
|
50
|
-
|
|
49
|
+
// Skills devlyn-cli installed by default and later deleted or made opt-in, by SHA-256 of their
|
|
50
|
+
// only file, SKILL.md (LF endings): an opted-in copy or a user's own folder of the same name is
|
|
51
|
+
// never removed.
|
|
52
|
+
const RETIRED_SKILL_MD_SHA256 = {
|
|
53
|
+
// 0.2.0-1.15.0; deleted in iter-0034, it still routes agents to retired commands.
|
|
54
|
+
'workflow-routing': new Set([
|
|
55
|
+
'7fec2bb20d808a873a975d64a6223e404bc5b328c800760bb77257ae6b2f467a',
|
|
56
|
+
'8bfdaeff2ce48d5a79b5653de3bbe0d56a61a7fd750ed3ad11149fec5a502ecf',
|
|
57
|
+
]),
|
|
58
|
+
// The standards skills, 0.1.0-3.2.1; 3.3.0 (0221) made them optional addons.
|
|
59
|
+
'code-health-standards': new Set([
|
|
60
|
+
'df2838904c938aa0306ed333cc460cfc5c2cf0ffa20bb5ade57067c6e0acf9be',
|
|
61
|
+
'962a82b5ebeafac515f81f5c68494c67dbf31c3e3e453f23c4452d4725e21355',
|
|
62
|
+
]),
|
|
63
|
+
'code-review-standards': new Set([
|
|
64
|
+
'9c906f2b2a88c2d0aaeec9714405945da5177de6ec433f1acf520c6c4385b875',
|
|
65
|
+
'3bc8587a9143487328be2470dec8df17a19cb14f94e6ec916ed58de660d0e1c4',
|
|
66
|
+
'56abe5736f505c53ca9ce14087a9891cc8661be783abaec7b5b924e3f339cc2a',
|
|
67
|
+
'586dde5014f9ac2f9a13d43fa1c8c257a3ae998a3fc1b867b23b3b2e44f77eca',
|
|
68
|
+
]),
|
|
69
|
+
'root-cause-analysis': new Set([
|
|
70
|
+
'e200ddd04196435af3162a9e13595f8eb5ac68b06465026e0155e453b1bd1c93',
|
|
71
|
+
'1793dcb0f72621c78cc20c905fe5dd7a0af3115e904bf737a6a983a7b9d2c2b6',
|
|
72
|
+
'aab22d1bddc66c6a798f08cdf7be9b45fbe80ccfc928b068ef5d9e2ad951bdda',
|
|
73
|
+
'0eac104bfcde8bdf14d4c284be6f3f4cb243219a679bf569433c2465df84c6c3',
|
|
74
|
+
'e66d15a9585c07e819acfd89c1f6d030517130f6d8f8924bda3120307949fb2e',
|
|
75
|
+
]),
|
|
76
|
+
'ui-implementation-standards': new Set([
|
|
77
|
+
'e2473e35474e78f2b581e225cb33a1e711bb79519a02e9780fc9606e582b878f',
|
|
78
|
+
'1ff64816454171b3f879e9e5cf3c15e3d9c5f2ffc81b73262155f6bc93a1b054',
|
|
79
|
+
'13c709c0e2b01ae94997c271043a685200e969192d2056534bd787c5682874e8',
|
|
80
|
+
]),
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// Whether `dir` is a real folder whose only file (dotfiles such as .DS_Store aside) is a SKILL.md
|
|
84
|
+
// with an LF-normalized SHA-256 in `hashes`: an unedited copy devlyn-cli shipped, not something
|
|
85
|
+
// the user added to or wrote.
|
|
86
|
+
function isShippedCopy(dir, hashes) {
|
|
51
87
|
const skill = path.join(dir, 'SKILL.md');
|
|
52
88
|
return fs.lstatSync(dir, { throwIfNoEntry: false })?.isDirectory() === true
|
|
89
|
+
&& fs.readdirSync(dir).filter((name) => !name.startsWith('.')).length === 1
|
|
53
90
|
&& fs.lstatSync(skill, { throwIfNoEntry: false })?.isFile() === true
|
|
54
|
-
&&
|
|
91
|
+
&& hashes.has(crypto.createHash('sha256')
|
|
55
92
|
.update(fs.readFileSync(skill, 'utf8').replace(/\r\n/g, '\n')).digest('hex'));
|
|
56
93
|
}
|
|
57
94
|
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
// AGENTS.md write via `npx devlyn-cli agents` in an unrelated repo.
|
|
87
|
-
detect: () => fs.existsSync(path.join(process.cwd(), '.omp')) || fs.existsSync(path.join(process.cwd(), '.agents')),
|
|
88
|
-
},
|
|
89
|
-
pi: {
|
|
90
|
-
name: 'Pi (earendil-works)',
|
|
91
|
-
instructionsFile: 'AGENTS.md',
|
|
92
|
-
baseInstructionsFile: 'AGENTS.md',
|
|
93
|
-
// Pi loads skills from ~/.agents/skills — shared with oh-my-pi.
|
|
94
|
-
skillsDir: SHARED_AGENTS_SKILLS_DIR,
|
|
95
|
-
skillsToInstall: DEVLYN_CORE_SKILLS,
|
|
96
|
-
// Project-scoped only (see omp): machine-level ~/.pi must not auto-trigger.
|
|
97
|
-
detect: () => fs.existsSync(path.join(process.cwd(), '.pi')) || fs.existsSync(path.join(process.cwd(), '.agents')),
|
|
98
|
-
},
|
|
99
|
-
grok: {
|
|
100
|
-
name: 'Grok Build CLI (xAI)',
|
|
101
|
-
instructionsFile: 'AGENTS.md',
|
|
102
|
-
baseInstructionsFile: 'AGENTS.md',
|
|
103
|
-
// Grok discovers same-format SKILL.md skills from ./.grok/skills >
|
|
104
|
-
// <repo_root>/.grok/skills > ~/.grok/skills > ~/.claude/skills
|
|
105
|
-
// (Claude-compatible); skills are slash-invocable in the Grok TUI.
|
|
106
|
-
skillsDir: path.join(os.homedir(), '.grok', 'skills'),
|
|
107
|
-
skillsToInstall: DEVLYN_CORE_SKILLS,
|
|
108
|
-
// Project-scoped only: machine-level ~/.grok must not auto-trigger a project
|
|
109
|
-
// AGENTS.md write via `npx devlyn-cli agents` in an unrelated repo.
|
|
110
|
-
detect: () => fs.existsSync(path.join(process.cwd(), '.grok')),
|
|
111
|
-
},
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
// Files removed in previous versions that should be cleaned up on upgrade
|
|
95
|
+
// Skill roots of each target. In a project, `.agents/skills` serves Codex, omp, Pi and Grok,
|
|
96
|
+
// and Claude Code loads only `.claude/skills`. Globally Codex reads ~/.codex/skills and omp,
|
|
97
|
+
// Pi and Grok read ~/.agents/skills.
|
|
98
|
+
function skillRoots(target, global) {
|
|
99
|
+
const dirs = target === 'claude' ? ['.claude'] : global ? ['.agents', '.codex'] : ['.agents'];
|
|
100
|
+
return dirs.map((dir) => path.join(global ? os.homedir() : projectDir(), dir, 'skills'));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// A devlyn Claude install in this scope. Globally only the marker a `--global --claude` run
|
|
104
|
+
// writes counts; in a project also a devlyn skill (0.6.0 and later), command (0.2-0.5) or
|
|
105
|
+
// CLAUDE.md with devlyn defaults, which a team may commit while ignoring .claude/. An optional
|
|
106
|
+
// devlyn skill is not one: 4.0.1 put those into .claude/skills without the Claude target. A
|
|
107
|
+
// CLAUDE.md link (often to AGENTS.md) is not the Claude target's file: updateInstructions
|
|
108
|
+
// refuses links.
|
|
109
|
+
function hasDevlynClaude(global) {
|
|
110
|
+
const claudeDir = path.dirname(skillRoots('claude', global)[0]);
|
|
111
|
+
if (global) return fs.existsSync(path.join(claudeDir, 'skills', DEVLYN_INSTALL_MARKER));
|
|
112
|
+
const names = (dir) => (fs.existsSync(dir) ? fs.readdirSync(dir) : []);
|
|
113
|
+
const optional = new Set(OPTIONAL_ADDONS.map((addon) => addon.name));
|
|
114
|
+
const instructions = path.join(path.dirname(claudeDir), 'CLAUDE.md');
|
|
115
|
+
return names(path.join(claudeDir, 'skills')).some((name) => name === DEVLYN_INSTALL_MARKER
|
|
116
|
+
|| (name.startsWith('devlyn') && !optional.has(name.replace(/[:\uF03A]/g, '-'))))
|
|
117
|
+
|| names(path.join(claudeDir, 'commands')).some((name) => name.startsWith('devlyn.'))
|
|
118
|
+
|| (fs.lstatSync(instructions, { throwIfNoEntry: false })?.isFile() === true
|
|
119
|
+
&& holdsDevlynDefaults('CLAUDE.md', fs.readFileSync(instructions, 'utf8')));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Commands removed in previous versions; the project Claude install deletes them from .claude/.
|
|
115
123
|
const DEPRECATED_FILES = [
|
|
116
124
|
'commands/devlyn.handoff.md', // removed in v0.2.0
|
|
117
125
|
'commands/devlyn.clean.md', // migrated to skills in v0.6.0
|
|
@@ -137,9 +145,9 @@ const DEPRECATED_FILES = [
|
|
|
137
145
|
// iter-0034 Phase 4 cutover (2026-05-03): 15 user skills deleted and 3 moved
|
|
138
146
|
// to optional-skills/. Listed here so post-cutover `npx devlyn-cli` upgrades
|
|
139
147
|
// force-remove stale legacy skill dirs from downstream `~/.claude/skills/`
|
|
140
|
-
// even though the source dirs no longer exist (
|
|
141
|
-
//
|
|
142
|
-
//
|
|
148
|
+
// even though the source dirs no longer exist (the installer replaces only
|
|
149
|
+
// the skills it ships — without this list, deleted-from-source skills persist
|
|
150
|
+
// in user installs forever).
|
|
143
151
|
const DEPRECATED_DIRS = [
|
|
144
152
|
// v0.7.x rename: devlyn-* → devlyn:*
|
|
145
153
|
'skills/devlyn-clean',
|
|
@@ -170,11 +178,6 @@ const DEPRECATED_DIRS = [
|
|
|
170
178
|
'skills/devlyn:team-resolve',
|
|
171
179
|
'skills/devlyn:team-review',
|
|
172
180
|
'skills/devlyn:update-docs',
|
|
173
|
-
// 0221 Session 1: standards skills moved to optional-skills/ (opt-in).
|
|
174
|
-
'skills/code-health-standards',
|
|
175
|
-
'skills/code-review-standards',
|
|
176
|
-
'skills/root-cause-analysis',
|
|
177
|
-
'skills/ui-implementation-standards',
|
|
178
181
|
// Deleted entirely on 2026-05-14 (devlyn:team-design-ui merged into
|
|
179
182
|
// devlyn:design-ui; devlyn:design-system removed outright). Entries kept
|
|
180
183
|
// so users who previously opted in get their stale copies purged on upgrade.
|
|
@@ -182,9 +185,9 @@ const DEPRECATED_DIRS = [
|
|
|
182
185
|
'skills/devlyn:design-system',
|
|
183
186
|
];
|
|
184
187
|
|
|
185
|
-
function
|
|
188
|
+
function projectDir() {
|
|
186
189
|
try {
|
|
187
|
-
return
|
|
190
|
+
return process.cwd();
|
|
188
191
|
} catch {
|
|
189
192
|
console.error('\n\x1b[33m❌ Current directory no longer exists.\x1b[0m');
|
|
190
193
|
console.error('\x1b[2m Please cd into a valid directory and try again.\x1b[0m\n');
|
|
@@ -224,14 +227,14 @@ ${v} ██║ ██║${p}██╔══╝ ${k}╚██╗ ██╔
|
|
|
224
227
|
${v} ██████╔╝${p}███████╗${k} ╚████╔╝ ${v}███████╗ ${p}██║ ${k}██║ ╚████║${r}
|
|
225
228
|
${g} ╚═════╝ ╚══════╝ ╚═══╝ ╚══════╝ ╚═╝ ╚═╝ ╚═══╝${r}
|
|
226
229
|
|
|
227
|
-
${COLORS.dim}
|
|
230
|
+
${COLORS.dim} AI Agent Config Toolkit${r}
|
|
228
231
|
${g} v${PKG.version} ${COLORS.dim}· ${k}🍩 by Nocodecat @ Donut Studio${r}
|
|
229
232
|
`;
|
|
230
233
|
console.log(logo);
|
|
231
234
|
}
|
|
232
235
|
|
|
233
236
|
const OPTIONAL_ADDONS = [
|
|
234
|
-
// Local optional skills (copied
|
|
237
|
+
// Local optional skills (copied into every selected skill root)
|
|
235
238
|
{ name: 'asset-creator', desc: 'AI pixel art game asset pipeline — generate, chroma-key, catalog', type: 'local' },
|
|
236
239
|
{ name: 'cloudflare-nextjs-setup', desc: 'Cloudflare Workers + Next.js deployment with OpenNext', type: 'local' },
|
|
237
240
|
{ name: 'generate-skill', desc: 'Create well-structured Claude Code skills following Anthropic best practices', type: 'local' },
|
|
@@ -349,22 +352,23 @@ function listContents() {
|
|
|
349
352
|
log('');
|
|
350
353
|
}
|
|
351
354
|
|
|
352
|
-
|
|
355
|
+
// Old and retired skills in one skill root; nothing outside it.
|
|
356
|
+
function cleanupDeprecated(skillsDir) {
|
|
353
357
|
let removed = 0;
|
|
354
358
|
const deprecated = DEPRECATED_DIRS.flatMap((relPath) =>
|
|
355
|
-
legacySkillPaths(
|
|
356
|
-
for (const relPath of
|
|
357
|
-
const fullPath = path.join(targetDir, relPath);
|
|
359
|
+
legacySkillPaths(skillsDir, path.basename(relPath)).map((fullPath) => [relPath, fullPath]));
|
|
360
|
+
for (const [relPath, fullPath] of deprecated) {
|
|
358
361
|
if (fs.existsSync(fullPath)) {
|
|
359
|
-
fs.
|
|
360
|
-
log(` ✕ ${relPath} (
|
|
362
|
+
fs.rmSync(fullPath, { recursive: true });
|
|
363
|
+
log(` ✕ ${relPath}/ (removed)`, 'dim');
|
|
361
364
|
removed++;
|
|
362
365
|
}
|
|
363
366
|
}
|
|
364
|
-
for (const [
|
|
365
|
-
|
|
367
|
+
for (const [name, hashes] of Object.entries(RETIRED_SKILL_MD_SHA256)) {
|
|
368
|
+
const fullPath = path.join(skillsDir, name);
|
|
369
|
+
if (isShippedCopy(fullPath, hashes)) {
|
|
366
370
|
fs.rmSync(fullPath, { recursive: true });
|
|
367
|
-
log(` ✕
|
|
371
|
+
log(` ✕ skills/${name}/ (removed)`, 'dim');
|
|
368
372
|
removed++;
|
|
369
373
|
}
|
|
370
374
|
}
|
|
@@ -380,7 +384,7 @@ function retireRenamedSkills(skillsDir) {
|
|
|
380
384
|
for (const [oldName, newName] of Object.entries(RENAMED_SKILLS)) {
|
|
381
385
|
const found = legacySkillPaths(skillsDir, oldName).filter((fullPath) => fs.existsSync(fullPath));
|
|
382
386
|
if (found.length > 0 && optional.has(newName)) installOptionalSkillInto(skillsDir, newName);
|
|
383
|
-
else if (
|
|
387
|
+
else if (isShippedCopy(path.join(skillsDir, newName), PRE_STANDARD_SKILL_MD_SHA256)) refreshPreStandardCopy(skillsDir, newName);
|
|
384
388
|
for (const fullPath of found) {
|
|
385
389
|
fs.rmSync(fullPath, { recursive: true, force: true });
|
|
386
390
|
log(` ✕ ${path.basename(fullPath)}/ (renamed to ${newName})`, 'dim');
|
|
@@ -392,8 +396,6 @@ function copyRecursive(src, dest, baseDir) {
|
|
|
392
396
|
const stats = fs.statSync(src);
|
|
393
397
|
|
|
394
398
|
if (stats.isDirectory()) {
|
|
395
|
-
// Never install dev workspaces, even when running from source repo.
|
|
396
|
-
if (UNSHIPPED_SKILL_DIRS.has(path.basename(src))) return;
|
|
397
399
|
if (!fs.existsSync(dest)) {
|
|
398
400
|
fs.mkdirSync(dest, { recursive: true });
|
|
399
401
|
}
|
|
@@ -454,72 +456,6 @@ function writeInstallMarker(skillsDir) {
|
|
|
454
456
|
}
|
|
455
457
|
}
|
|
456
458
|
|
|
457
|
-
function shellDoubleQuoteDefault(value) {
|
|
458
|
-
if (value.includes('\n')) {
|
|
459
|
-
throw new Error(`Cannot stamp skill path with newline: ${value}`);
|
|
460
|
-
}
|
|
461
|
-
return value.replace(/[\\$"`}]/g, '\\$&');
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
function stampInstalledSkillDir(rootDir, skillDir) {
|
|
465
|
-
// Stamp ONLY the assignment-default occurrence. The sentinel comparison
|
|
466
|
-
// literal on the guard line must stay intact: stamping it makes the guard
|
|
467
|
-
// compare the stamped default to itself and false-positive
|
|
468
|
-
// BLOCKED:shared-dir-unresolved on every codex/omp run that executes the
|
|
469
|
-
// block (CLAUDE_SKILL_DIR unset) — iter-0040 R3 latent finding.
|
|
470
|
-
const assignmentStamp = '${CLAUDE_SKILL_DIR:-' + DEVLYN_SKILL_DIR_STAMP + '}';
|
|
471
|
-
const stampedAssignment = '${CLAUDE_SKILL_DIR:-' + shellDoubleQuoteDefault(skillDir) + '}';
|
|
472
|
-
|
|
473
|
-
function visit(current) {
|
|
474
|
-
const stats = fs.statSync(current);
|
|
475
|
-
if (stats.isDirectory()) {
|
|
476
|
-
for (const item of fs.readdirSync(current)) {
|
|
477
|
-
visit(path.join(current, item));
|
|
478
|
-
}
|
|
479
|
-
return;
|
|
480
|
-
}
|
|
481
|
-
if (path.extname(current) !== '.md') return;
|
|
482
|
-
|
|
483
|
-
const content = fs.readFileSync(current, 'utf8');
|
|
484
|
-
if (!content.includes(assignmentStamp)) return;
|
|
485
|
-
fs.writeFileSync(current, content.split(assignmentStamp).join(stampedAssignment));
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
visit(rootDir);
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
// Dev artifacts that live under config/skills/ but must never ship or install.
|
|
492
|
-
// Mirrors the `!` exclusions in package.json files[].
|
|
493
|
-
const UNSHIPPED_SKILL_DIRS = new Set([
|
|
494
|
-
'devlyn:auto-resolve-workspace',
|
|
495
|
-
'devlyn:ideate-workspace',
|
|
496
|
-
'devlyn-ideate-workspace',
|
|
497
|
-
'preflight-workspace',
|
|
498
|
-
'roadmap-archival-workspace',
|
|
499
|
-
]);
|
|
500
|
-
|
|
501
|
-
// Clean managed skill directories before copy to prevent stale-file drift.
|
|
502
|
-
// copyRecursive is a pure overlay: if a file was removed or renamed in source,
|
|
503
|
-
// the installed mirror keeps the old copy. For each top-level dir under
|
|
504
|
-
// config/skills/, remove its counterpart in target/skills/ before the copy so
|
|
505
|
-
// each managed skill is fully replaced on every sync. User-installed skills
|
|
506
|
-
// (e.g. skill-creator from optional addons) are left alone because they have
|
|
507
|
-
// no counterpart in source. Dev workspaces are skipped entirely.
|
|
508
|
-
function cleanManagedSkillDirs(sourceSkillsDir, targetSkillsDir) {
|
|
509
|
-
if (!fs.existsSync(sourceSkillsDir) || !fs.existsSync(targetSkillsDir)) return 0;
|
|
510
|
-
let cleaned = 0;
|
|
511
|
-
for (const entry of fs.readdirSync(sourceSkillsDir, { withFileTypes: true })) {
|
|
512
|
-
if (!entry.isDirectory()) continue;
|
|
513
|
-
if (UNSHIPPED_SKILL_DIRS.has(entry.name)) continue;
|
|
514
|
-
const targetPath = path.join(targetSkillsDir, entry.name);
|
|
515
|
-
if (fs.existsSync(targetPath)) {
|
|
516
|
-
fs.rmSync(targetPath, { recursive: true, force: true });
|
|
517
|
-
cleaned++;
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
return cleaned;
|
|
521
|
-
}
|
|
522
|
-
|
|
523
459
|
function multiSelect(items, preselectedIndices = []) {
|
|
524
460
|
return new Promise((resolve) => {
|
|
525
461
|
const selected = new Set(preselectedIndices.filter((i) => i >= 0 && i < items.length));
|
|
@@ -540,10 +476,10 @@ function multiSelect(items, preselectedIndices = []) {
|
|
|
540
476
|
const checkbox = selected.has(i) ? `${COLORS.green}◉${COLORS.reset}` : `${COLORS.dim}○${COLORS.reset}`;
|
|
541
477
|
const pointer = i === cursor ? `${COLORS.cyan}❯${COLORS.reset}` : ' ';
|
|
542
478
|
const name = i === cursor ? `${COLORS.cyan}${item.name}${COLORS.reset}` : item.name;
|
|
543
|
-
const tagLabel = item.type === 'mcp' ? 'mcp' : item.type === 'local' ? 'skill' :
|
|
544
|
-
const tagColor = item.type === 'mcp' ? COLORS.green : item.type === 'local' ? COLORS.magenta :
|
|
479
|
+
const tagLabel = item.type === 'mcp' ? 'mcp' : item.type === 'local' ? 'skill' : 'pack';
|
|
480
|
+
const tagColor = item.type === 'mcp' ? COLORS.green : item.type === 'local' ? COLORS.magenta : COLORS.cyan;
|
|
545
481
|
const tag = `${tagColor}${tagLabel}${COLORS.reset}`;
|
|
546
|
-
console.log(`${pointer} ${checkbox} ${name} ${COLORS.dim}[${tag}${COLORS.dim}]${COLORS.reset}`);
|
|
482
|
+
console.log(`${pointer} ${checkbox} ${name}${item.type ? ` ${COLORS.dim}[${tag}${COLORS.dim}]${COLORS.reset}` : ''}`);
|
|
547
483
|
console.log(` ${COLORS.dim}${item.desc}${COLORS.reset}`);
|
|
548
484
|
});
|
|
549
485
|
};
|
|
@@ -613,26 +549,48 @@ function multiSelect(items, preselectedIndices = []) {
|
|
|
613
549
|
});
|
|
614
550
|
}
|
|
615
551
|
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
552
|
+
// One of `items` (↑↓ move, Enter confirms); resolves to the chosen index.
|
|
553
|
+
function singleSelect(items, initial) {
|
|
554
|
+
return new Promise((resolve) => {
|
|
555
|
+
let cursor = initial;
|
|
556
|
+
let drawn = false;
|
|
557
|
+
const render = () => {
|
|
558
|
+
if (drawn) process.stdout.write(`\x1b[${items.length + 2}A\x1b[0J`);
|
|
559
|
+
drawn = true;
|
|
560
|
+
console.log(`${COLORS.dim}(↑↓ navigate, enter confirm)${COLORS.reset}\n`);
|
|
561
|
+
items.forEach((item, i) => console.log(i === cursor ? `${COLORS.cyan}❯ ${item}${COLORS.reset}` : ` ${item}`));
|
|
562
|
+
};
|
|
563
|
+
render();
|
|
564
|
+
process.stdin.setRawMode(true);
|
|
565
|
+
process.stdin.resume();
|
|
566
|
+
process.stdin.setEncoding('utf8');
|
|
567
|
+
const onKeypress = (key) => {
|
|
568
|
+
if (key === '\u0003') {
|
|
569
|
+
process.stdin.setRawMode(false);
|
|
570
|
+
process.exit();
|
|
571
|
+
}
|
|
572
|
+
if (key === '\r' || key === '\n') {
|
|
573
|
+
process.stdin.setRawMode(false);
|
|
574
|
+
process.stdin.removeListener('data', onKeypress);
|
|
575
|
+
process.stdin.pause();
|
|
576
|
+
console.log('');
|
|
577
|
+
resolve(cursor);
|
|
578
|
+
} else if (['\x1b[A', 'k', '\x1b[B', 'j'].includes(key)) {
|
|
579
|
+
cursor = (cursor + (key === '\x1b[A' || key === 'k' ? items.length - 1 : 1)) % items.length;
|
|
580
|
+
render();
|
|
581
|
+
}
|
|
582
|
+
};
|
|
583
|
+
process.stdin.on('data', onKeypress);
|
|
584
|
+
});
|
|
585
|
+
}
|
|
621
586
|
|
|
622
|
-
|
|
587
|
+
function installLocalSkill(skillName, roots) {
|
|
588
|
+
if (!fs.existsSync(path.join(OPTIONAL_SKILLS_SOURCE, skillName))) {
|
|
623
589
|
log(` ⚠️ Skill "${skillName}" not found`, 'yellow');
|
|
624
590
|
return false;
|
|
625
591
|
}
|
|
626
|
-
|
|
627
592
|
log(`\n🛠️ Installing ${skillName}...`, 'cyan');
|
|
628
|
-
|
|
629
|
-
// Mirror to every CLI skill-loader directory that already exists so optional
|
|
630
|
-
// skills are picked up by Codex/omp/Pi (and any future CLI with a skillsDir)
|
|
631
|
-
// the same way required skills are. Existing dir, not new dir — we don't
|
|
632
|
-
// create an agent install just because someone opted into a Claude-side skill.
|
|
633
|
-
// De-dup by directory: omp and Pi share ~/.agents/skills, so the mirror runs
|
|
634
|
-
// once per unique destination.
|
|
635
|
-
for (const target of targets) installOptionalSkillInto(target, skillName);
|
|
593
|
+
for (const root of roots) installOptionalSkillInto(root, skillName);
|
|
636
594
|
return true;
|
|
637
595
|
}
|
|
638
596
|
|
|
@@ -642,7 +600,6 @@ function installOptionalSkillInto(target, skillName) {
|
|
|
642
600
|
const dest = path.join(target, skillName);
|
|
643
601
|
fs.rmSync(dest, { recursive: true, force: true });
|
|
644
602
|
copyRecursive(path.join(OPTIONAL_SKILLS_SOURCE, skillName), dest, target);
|
|
645
|
-
stampInstalledSkillDir(dest, dest);
|
|
646
603
|
assertCompleteSkillInstall(OPTIONAL_SKILLS_SOURCE, target, [skillName]);
|
|
647
604
|
const oldName = Object.keys(RENAMED_SKILLS).find((name) => RENAMED_SKILLS[name] === skillName);
|
|
648
605
|
for (const fullPath of oldName ? legacySkillPaths(target, oldName) : []) {
|
|
@@ -687,9 +644,9 @@ function installSkillPack(packName) {
|
|
|
687
644
|
}
|
|
688
645
|
}
|
|
689
646
|
|
|
690
|
-
function installAddon(addon) {
|
|
647
|
+
function installAddon(addon, roots) {
|
|
691
648
|
if (addon.type === 'local') {
|
|
692
|
-
return installLocalSkill(addon.name);
|
|
649
|
+
return installLocalSkill(addon.name, roots);
|
|
693
650
|
}
|
|
694
651
|
if (addon.type === 'mcp') {
|
|
695
652
|
return installMcpServer(addon.name, addon.command);
|
|
@@ -697,173 +654,33 @@ function installAddon(addon) {
|
|
|
697
654
|
return installSkillPack(addon.name);
|
|
698
655
|
}
|
|
699
656
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
if (cli.detect()) {
|
|
704
|
-
detected.push(key);
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
return detected;
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
// Install DEVLYN_CORE_SKILLS into a CLI's
|
|
711
|
-
// global skills directory (e.g. ~/.codex/skills/). Returns count of skills
|
|
712
|
-
// copied. Skipped silently for CLIs without a skillsDir (e.g. cursor, copilot
|
|
713
|
-
// at the time of writing — they don't have an analogous skill-loader).
|
|
714
|
-
function installSkillsForCLI(cliKey) {
|
|
715
|
-
const cli = CLI_TARGETS[cliKey];
|
|
716
|
-
if (!cli || !cli.skillsDir || !cli.skillsToInstall) return 0;
|
|
717
|
-
|
|
657
|
+
// The core skills into one skill root, replacing older copies and retired skills, then the
|
|
658
|
+
// marker that records a complete install.
|
|
659
|
+
function installCoreSkills(skillsDir) {
|
|
718
660
|
const sourceSkillsDir = path.join(CONFIG_SOURCE, 'skills');
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
const removed = cleanupDeprecated(path.dirname(cli.skillsDir));
|
|
661
|
+
log(`\n📁 Installing devlyn skills to ${skillsDir.replace(os.homedir(), '~')}`, 'green');
|
|
662
|
+
fs.mkdirSync(skillsDir, { recursive: true });
|
|
663
|
+
clearInstallMarker(skillsDir);
|
|
664
|
+
const removed = cleanupDeprecated(skillsDir);
|
|
725
665
|
if (removed > 0) {
|
|
726
666
|
log(`\n🧹 Cleaned up ${removed} deprecated file${removed > 1 ? 's' : ''}`, 'yellow');
|
|
727
667
|
}
|
|
728
|
-
|
|
729
|
-
let copied = 0;
|
|
730
|
-
for (const skillName of cli.skillsToInstall) {
|
|
668
|
+
for (const skillName of DEVLYN_CORE_SKILLS) {
|
|
731
669
|
const src = path.join(sourceSkillsDir, skillName);
|
|
732
|
-
const dest = path.join(
|
|
670
|
+
const dest = path.join(skillsDir, skillName);
|
|
733
671
|
if (!fs.existsSync(src)) continue;
|
|
734
|
-
// Full replace
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
fs.rmSync(dest, { recursive: true, force: true });
|
|
738
|
-
}
|
|
739
|
-
copyRecursive(src, dest, cli.skillsDir);
|
|
740
|
-
stampInstalledSkillDir(dest, dest);
|
|
741
|
-
copied++;
|
|
742
|
-
log(` → ${cli.skillsDir.replace(os.homedir(), '~')}/${skillName}`, 'dim');
|
|
743
|
-
}
|
|
744
|
-
assertCompleteSkillInstall(sourceSkillsDir, cli.skillsDir, cli.skillsToInstall);
|
|
745
|
-
retireRenamedSkills(cli.skillsDir);
|
|
746
|
-
writeInstallMarker(cli.skillsDir);
|
|
747
|
-
return copied;
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
// One-line description of exactly what selecting a CLI target installs, so the
|
|
751
|
-
// unified selector stays honest.
|
|
752
|
-
function targetDesc(cli) {
|
|
753
|
-
if (cli.skillsDir) {
|
|
754
|
-
return `${cli.instructionsFile} + devlyn skills → ${cli.skillsDir.replace(os.homedir(), '~')}`;
|
|
755
|
-
}
|
|
756
|
-
return `${cli.instructionsFile} instructions only`;
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
// Shared instruction ownership and preservation rules apply to every CLI.
|
|
760
|
-
function installInstructionsForCLI(cliKey) {
|
|
761
|
-
const cli = CLI_TARGETS[cliKey];
|
|
762
|
-
return cli?.baseInstructionsFile ? updateInstructions(cli.baseInstructionsFile) : false;
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
// Install both instructions and skills for a single CLI. Used by the
|
|
766
|
-
// `agents <cli>` command; multi-target paths use installSelectedCLITargets.
|
|
767
|
-
function installAgentsForCLI(cliKey) {
|
|
768
|
-
const instr = installInstructionsForCLI(cliKey);
|
|
769
|
-
const skillsCopied = installSkillsForCLI(cliKey);
|
|
770
|
-
if (skillsCopied > 0) {
|
|
771
|
-
log(` → ${skillsCopied} skill${skillsCopied > 1 ? 's' : ''} installed (${DEVLYN_CORE_SKILLS.join(' / ')})`, 'dim');
|
|
772
|
-
}
|
|
773
|
-
return instr || skillsCopied > 0;
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
// Install instructions + skills for a set of selected CLI targets, writing each
|
|
777
|
-
// unique destination exactly once. omp and Pi share project AGENTS.md and the
|
|
778
|
-
// ~/.agents/skills dir, so selecting both does not duplicate writes — this is
|
|
779
|
-
// the researched "group the common" model: targets are the user's selection,
|
|
780
|
-
// destinations are the unit of work.
|
|
781
|
-
function installSelectedCLITargets(cliKeys) {
|
|
782
|
-
const instrDone = new Set();
|
|
783
|
-
const skillsDone = new Set();
|
|
784
|
-
let count = 0;
|
|
785
|
-
for (const cliKey of cliKeys) {
|
|
786
|
-
const cli = CLI_TARGETS[cliKey];
|
|
787
|
-
if (!cli) continue;
|
|
788
|
-
const instrKey = cli.instructionsFile;
|
|
789
|
-
if (!instrDone.has(instrKey)) {
|
|
790
|
-
installInstructionsForCLI(cliKey);
|
|
791
|
-
instrDone.add(instrKey);
|
|
792
|
-
}
|
|
793
|
-
if (cli.skillsDir && !skillsDone.has(cli.skillsDir)) {
|
|
794
|
-
const copied = installSkillsForCLI(cliKey);
|
|
795
|
-
if (copied > 0) {
|
|
796
|
-
log(` → ${copied} skill${copied > 1 ? 's' : ''} → ${cli.skillsDir.replace(os.homedir(), '~')}`, 'dim');
|
|
797
|
-
}
|
|
798
|
-
skillsDone.add(cli.skillsDir);
|
|
799
|
-
}
|
|
800
|
-
count++;
|
|
801
|
-
}
|
|
802
|
-
return count;
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
function installAgentsForAllDetected() {
|
|
806
|
-
const detected = detectOtherCLIs();
|
|
807
|
-
if (detected.length === 0) return 0;
|
|
808
|
-
return installSelectedCLITargets(detected);
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
// Install the Claude Code core config: project .claude/ (skills, templates,
|
|
812
|
-
// settings, CLAUDE.md, .gitignore) plus global ~/.claude/settings.json tweaks.
|
|
813
|
-
// Extracted so the unified target selector installs it only when "Claude Code"
|
|
814
|
-
// is chosen. Check global settings and instruction conflicts before writes.
|
|
815
|
-
function installClaudeCore() {
|
|
816
|
-
const globalClaudeDir = path.join(os.homedir(), '.claude');
|
|
817
|
-
const globalSettingsPath = path.join(globalClaudeDir, 'settings.json');
|
|
818
|
-
function readGlobalSettings() {
|
|
819
|
-
let settings = {};
|
|
820
|
-
try {
|
|
821
|
-
if (fs.lstatSync(globalSettingsPath, { throwIfNoEntry: false })) {
|
|
822
|
-
settings = JSON.parse(fs.readFileSync(globalSettingsPath, 'utf8'));
|
|
823
|
-
}
|
|
824
|
-
} catch (error) {
|
|
825
|
-
const reason = error instanceof SyntaxError ? 'invalid JSON' : error.message;
|
|
826
|
-
throw new Error(`Cannot merge ${globalSettingsPath}: ${reason}. Original preserved; correct the file or its access and rerun installation.`);
|
|
827
|
-
}
|
|
828
|
-
if (!settings || typeof settings !== 'object' || Array.isArray(settings)) {
|
|
829
|
-
throw new Error(`Cannot merge ${globalSettingsPath}: root must be a JSON object. Original preserved; correct it and rerun installation.`);
|
|
830
|
-
}
|
|
831
|
-
if (!Object.prototype.hasOwnProperty.call(settings, 'env')) settings.env = {};
|
|
832
|
-
if (!settings.env || typeof settings.env !== 'object' || Array.isArray(settings.env)) {
|
|
833
|
-
throw new Error(`Cannot merge ${globalSettingsPath}: env must be a JSON object. Original preserved; correct it and rerun installation.`);
|
|
834
|
-
}
|
|
835
|
-
return settings;
|
|
836
|
-
}
|
|
837
|
-
readGlobalSettings();
|
|
838
|
-
updateInstructions('CLAUDE.md');
|
|
839
|
-
const targetDir = getTargetDir();
|
|
840
|
-
const skillsDir = path.join(targetDir, 'skills');
|
|
841
|
-
log('\n📁 Installing Claude Code config to .claude/', 'green');
|
|
842
|
-
if (!fs.existsSync(skillsDir)) fs.mkdirSync(skillsDir, { recursive: true });
|
|
843
|
-
clearInstallMarker(skillsDir);
|
|
844
|
-
const removed = cleanupDeprecated(targetDir);
|
|
845
|
-
if (removed > 0) {
|
|
846
|
-
log(`\n🧹 Cleaned up ${removed} deprecated file${removed > 1 ? 's' : ''}`, 'yellow');
|
|
847
|
-
}
|
|
848
|
-
const refreshed = cleanManagedSkillDirs(
|
|
849
|
-
path.join(CONFIG_SOURCE, 'skills'),
|
|
850
|
-
skillsDir,
|
|
851
|
-
);
|
|
852
|
-
if (refreshed > 0) {
|
|
853
|
-
log(` 🔄 Refreshing ${refreshed} managed skill director${refreshed === 1 ? 'y' : 'ies'}`, 'dim');
|
|
854
|
-
}
|
|
855
|
-
copyRecursive(CONFIG_SOURCE, targetDir, targetDir);
|
|
856
|
-
assertCompleteSkillInstall(path.join(CONFIG_SOURCE, 'skills'), skillsDir, DEVLYN_CORE_SKILLS);
|
|
857
|
-
for (const name of DEVLYN_CORE_SKILLS) {
|
|
858
|
-
const dest = path.join(skillsDir, name);
|
|
859
|
-
stampInstalledSkillDir(dest, dest);
|
|
672
|
+
// Full replace: copyRecursive is an overlay, so stale files would otherwise persist.
|
|
673
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
674
|
+
copyRecursive(src, dest, skillsDir);
|
|
860
675
|
}
|
|
676
|
+
assertCompleteSkillInstall(sourceSkillsDir, skillsDir, DEVLYN_CORE_SKILLS);
|
|
861
677
|
retireRenamedSkills(skillsDir);
|
|
862
678
|
writeInstallMarker(skillsDir);
|
|
679
|
+
}
|
|
863
680
|
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
const
|
|
681
|
+
// Keep installer-managed pipeline state and install metadata out of git.
|
|
682
|
+
function ignoreInGit(gitignoreEntries) {
|
|
683
|
+
const gitignorePath = path.join(projectDir(), '.gitignore');
|
|
867
684
|
let gitignoreContent = fs.existsSync(gitignorePath)
|
|
868
685
|
? fs.readFileSync(gitignorePath, 'utf8')
|
|
869
686
|
: '';
|
|
@@ -877,6 +694,55 @@ function installClaudeCore() {
|
|
|
877
694
|
fs.writeFileSync(gitignorePath, gitignoreContent + prefix + header + missingGitignoreEntries.join('\n') + '\n');
|
|
878
695
|
log(` → .gitignore (added ${missingGitignoreEntries.join(', ')})`, 'dim');
|
|
879
696
|
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// With the Claude target in the same run, an AGENTS.md that links to this project's CLAUDE.md (a
|
|
700
|
+
// common Claude-first setup) gets its devlyn block through CLAUDE.md. Any other link is refused.
|
|
701
|
+
// AGENTS.md that is this project's CLAUDE.md under another name: a symlink to it, or the plain
|
|
702
|
+
// file Git for Windows checks out in a symlink's place when core.symlinks is off (its whole
|
|
703
|
+
// content is the link target). Compared by real path: a link to another hard link of CLAUDE.md
|
|
704
|
+
// would go stale once CLAUDE.md is replaced.
|
|
705
|
+
function agentsMdIsClaudeMd() {
|
|
706
|
+
const agents = path.join(projectDir(), 'AGENTS.md');
|
|
707
|
+
const claude = path.join(projectDir(), 'CLAUDE.md');
|
|
708
|
+
const stat = fs.lstatSync(agents, { throwIfNoEntry: false });
|
|
709
|
+
if (!stat || !fs.lstatSync(claude, { throwIfNoEntry: false })?.isFile()) return false;
|
|
710
|
+
const target = stat.isSymbolicLink() ? agents
|
|
711
|
+
: stat.isFile() && stat.size < 256 ? path.resolve(projectDir(), fs.readFileSync(agents, 'utf8'))
|
|
712
|
+
: null;
|
|
713
|
+
return target !== null && fs.existsSync(target) && fs.realpathSync.native(target) === fs.realpathSync.native(claude);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function installAgentsProject(withClaude) {
|
|
717
|
+
if (!agentsMdIsClaudeMd()) {
|
|
718
|
+
updateInstructions('AGENTS.md');
|
|
719
|
+
} else if (withClaude) {
|
|
720
|
+
log(' → AGENTS.md is CLAUDE.md here, so it gets the devlyn block written there', 'dim');
|
|
721
|
+
} else {
|
|
722
|
+
throw new InstructionError('AGENTS.md is CLAUDE.md under another name here. Choose CLAUDE.md as well '
|
|
723
|
+
+ '(npx devlyn-cli -y --claude) so the devlyn block is written once, into CLAUDE.md.');
|
|
724
|
+
}
|
|
725
|
+
installCoreSkills(skillRoots('agents', false)[0]);
|
|
726
|
+
ignoreInGit(['.devlyn/', '.agents/skills/.devlyn-install.json']);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// Project CLAUDE.md and .claude/: skills, templates, commit conventions and settings.
|
|
730
|
+
function installClaudeCore() {
|
|
731
|
+
updateInstructions('CLAUDE.md');
|
|
732
|
+
const skillsDir = skillRoots('claude', false)[0];
|
|
733
|
+
const targetDir = path.dirname(skillsDir);
|
|
734
|
+
for (const entry of fs.readdirSync(CONFIG_SOURCE)) {
|
|
735
|
+
if (entry !== 'skills') copyRecursive(path.join(CONFIG_SOURCE, entry), path.join(targetDir, entry), targetDir);
|
|
736
|
+
}
|
|
737
|
+
for (const relPath of DEPRECATED_FILES) {
|
|
738
|
+
const fullPath = path.join(targetDir, relPath);
|
|
739
|
+
if (fs.existsSync(fullPath)) {
|
|
740
|
+
fs.unlinkSync(fullPath);
|
|
741
|
+
log(` ✕ ${relPath} (deprecated)`, 'dim');
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
installCoreSkills(skillsDir);
|
|
745
|
+
ignoreInGit(['.devlyn/', '.claude/skills/.devlyn-install.json']);
|
|
880
746
|
|
|
881
747
|
// Enable agent teams in project settings
|
|
882
748
|
const settingsPath = path.join(targetDir, 'settings.json');
|
|
@@ -930,6 +796,10 @@ function installClaudeCore() {
|
|
|
930
796
|
settingsChanged = true;
|
|
931
797
|
}
|
|
932
798
|
}
|
|
799
|
+
if (!settings.env.ENABLE_PROMPT_CACHING_1H) {
|
|
800
|
+
settings.env.ENABLE_PROMPT_CACHING_1H = 'true';
|
|
801
|
+
settingsChanged = true;
|
|
802
|
+
}
|
|
933
803
|
if (!settings.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS) {
|
|
934
804
|
settings.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS = '1';
|
|
935
805
|
settingsChanged = true;
|
|
@@ -967,23 +837,52 @@ function installClaudeCore() {
|
|
|
967
837
|
}
|
|
968
838
|
if (settingsChanged) {
|
|
969
839
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
970
|
-
log(' → settings.json (agent teams + one-hour Bash max + pipeline permissions + Stop hook)', 'dim');
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
840
|
+
log(' → settings.json (agent teams + one-hour Bash max + 1h prompt caching + pipeline permissions + Stop hook)', 'dim');
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// Installs the targets in one scope; returns the skill roots written.
|
|
845
|
+
function install(targets, global) {
|
|
846
|
+
// In the home folder CLAUDE.md, AGENTS.md (agents read a parent folder's too) and
|
|
847
|
+
// .claude/settings.json (permissions, Stop hook) would apply to every project. Same folder by
|
|
848
|
+
// identity, so a link or another spelling of the path is caught too.
|
|
849
|
+
const home = fs.statSync(os.homedir(), { bigint: true, throwIfNoEntry: false });
|
|
850
|
+
const here = fs.statSync(projectDir(), { bigint: true });
|
|
851
|
+
if (!global && home?.dev === here.dev && home?.ino === here.ino) {
|
|
852
|
+
throw new InstructionError('This project is your home folder, so its CLAUDE.md, AGENTS.md and .claude/settings.json '
|
|
853
|
+
+ 'would apply to every project. Run from a project folder, or use --global for skills only.');
|
|
854
|
+
}
|
|
855
|
+
const roots = targets.flatMap((target) => skillRoots(target, global));
|
|
856
|
+
for (const target of targets) {
|
|
857
|
+
if (global) skillRoots(target, true).forEach((root) => installCoreSkills(root));
|
|
858
|
+
else if (target === 'agents') installAgentsProject(targets.includes('claude'));
|
|
859
|
+
else installClaudeCore();
|
|
860
|
+
}
|
|
861
|
+
log(`\n✅ devlyn ${PKG.version} installed`, 'green');
|
|
862
|
+
noticeGlobalDrift(roots);
|
|
863
|
+
return roots;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// A devlyn install in a user root this run does not refresh drifts and can shadow the
|
|
867
|
+
// project's copy; name it without touching it.
|
|
868
|
+
function noticeGlobalDrift(installed) {
|
|
869
|
+
for (const dir of ['.agents', '.codex', '.claude', '.grok']) {
|
|
870
|
+
const root = path.join(os.homedir(), dir, 'skills');
|
|
871
|
+
const marker = path.join(root, DEVLYN_INSTALL_MARKER);
|
|
872
|
+
if (installed.includes(root) || !fs.existsSync(marker)) continue;
|
|
873
|
+
let version;
|
|
874
|
+
try {
|
|
875
|
+
version = JSON.parse(fs.readFileSync(marker, 'utf8')).version;
|
|
876
|
+
} catch (error) {
|
|
877
|
+
version = `(unreadable marker: ${error.message})`;
|
|
878
|
+
}
|
|
879
|
+
// 4.0's Grok root; Grok now reads ~/.agents/skills, so --global never refreshes it.
|
|
880
|
+
const advice = dir === '.grok' ? 'delete it' : 'refresh it with --global, or delete it';
|
|
881
|
+
log(`Global devlyn ${version} in ${root.replace(os.homedir(), '~')} — ${advice}.`, 'yellow');
|
|
981
882
|
}
|
|
982
|
-
|
|
983
|
-
log('\n✅ Claude Code config installed!', 'green');
|
|
984
883
|
}
|
|
985
884
|
|
|
986
|
-
async function init(
|
|
885
|
+
async function init({ yes, claude, global }) {
|
|
987
886
|
showLogo();
|
|
988
887
|
log('─'.repeat(44), 'dim');
|
|
989
888
|
|
|
@@ -992,53 +891,47 @@ async function init(skipPrompts = false) {
|
|
|
992
891
|
process.exit(1);
|
|
993
892
|
}
|
|
994
893
|
|
|
995
|
-
//
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
installClaudeCore();
|
|
894
|
+
// Without prompts: AGENTS plus every target already installed in the chosen scope.
|
|
895
|
+
if (yes || !process.stdin.isTTY) {
|
|
896
|
+
const targets = ['agents', ...(claude || hasDevlynClaude(global) ? ['claude'] : [])];
|
|
897
|
+
install(targets, global);
|
|
1000
898
|
log('\n💡 Add optional addons later: run `npx devlyn-cli` without -y', 'dim');
|
|
1001
|
-
|
|
899
|
+
const hints = [
|
|
900
|
+
...(targets.includes('claude') ? [] : [`${global ? '~/.claude/skills' : 'CLAUDE.md + .claude/'} for Claude Code: add --claude`]),
|
|
901
|
+
...(global ? [] : ['every project on this machine: add --global']),
|
|
902
|
+
];
|
|
903
|
+
if (hints.length > 0) log(` ${hints.join(' · ')}`, 'dim');
|
|
1002
904
|
log(`\n${COLORS.dim} Enjoying devlyn? Star it on GitHub — it helps others find it:${COLORS.reset}`);
|
|
1003
905
|
log(` ${COLORS.purple}→ https://github.com/fysoul17/devlyn-cli${COLORS.reset}\n`);
|
|
1004
906
|
return;
|
|
1005
907
|
}
|
|
1006
908
|
|
|
1007
|
-
|
|
1008
|
-
// pre-checked (the happy path); every other agent is an explicit opt-in so a
|
|
1009
|
-
// bare Enter never mutates a project's AGENTS.md or a shared skills dir for an
|
|
1010
|
-
// agent the user didn't choose.
|
|
1011
|
-
log('\n🎯 Select the agents to install devlyn into:\n', 'blue');
|
|
909
|
+
log('\n🎯 What to install:\n', 'blue');
|
|
1012
910
|
const targetOptions = [
|
|
1013
|
-
{ key: '
|
|
1014
|
-
|
|
911
|
+
{ key: 'agents', name: 'AGENTS.md — Codex · omp · Pi · Grok', desc: 'AGENTS.md + .agents/skills' },
|
|
912
|
+
{ key: 'claude', name: 'CLAUDE.md — Claude Code', desc: 'CLAUDE.md + .claude/ (skills, templates, settings)' },
|
|
1015
913
|
];
|
|
1016
|
-
const preselected =
|
|
1017
|
-
const
|
|
914
|
+
const preselected = claude || hasDevlynClaude(false) || (global && hasDevlynClaude(true)) ? [0, 1] : [0];
|
|
915
|
+
const targets = (await multiSelect(targetOptions, preselected)).map((option) => option.key);
|
|
1018
916
|
|
|
1019
|
-
if (
|
|
1020
|
-
log('\n💡
|
|
1021
|
-
log(' Run `npx devlyn-cli` again and pick at least one
|
|
917
|
+
if (targets.length === 0) {
|
|
918
|
+
log('\n💡 Nothing selected — nothing installed.', 'yellow');
|
|
919
|
+
log(' Run `npx devlyn-cli` again and pick at least one.\n', 'dim');
|
|
1022
920
|
return;
|
|
1023
921
|
}
|
|
1024
922
|
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
const cliKeys = selectedKeys.filter((k) => k !== 'claude');
|
|
1029
|
-
if (cliKeys.length > 0) {
|
|
1030
|
-
const installed = installSelectedCLITargets(cliKeys);
|
|
1031
|
-
log(`\n ✅ Installed devlyn into ${installed} agent${installed !== 1 ? 's' : ''}`, 'green');
|
|
1032
|
-
}
|
|
923
|
+
log('📍 Where:\n', 'blue');
|
|
924
|
+
const scope = await singleSelect(['This project', 'Global — every project on this machine'], global ? 1 : 0);
|
|
925
|
+
const roots = install(targets, scope === 1);
|
|
1033
926
|
|
|
1034
|
-
// Ask about optional addons (local skills + external packs)
|
|
927
|
+
// Ask about optional addons (local skills + external packs; MCP servers belong to Claude Code)
|
|
1035
928
|
log('\n📚 Optional skills & packs:\n', 'blue');
|
|
1036
929
|
|
|
1037
|
-
const selectedAddons = await multiSelect(OPTIONAL_ADDONS);
|
|
930
|
+
const selectedAddons = await multiSelect(OPTIONAL_ADDONS.filter((addon) => addon.type !== 'mcp' || targets.includes('claude')));
|
|
1038
931
|
|
|
1039
932
|
if (selectedAddons.length > 0) {
|
|
1040
933
|
for (const addon of selectedAddons) {
|
|
1041
|
-
installAddon(addon);
|
|
934
|
+
installAddon(addon, roots);
|
|
1042
935
|
}
|
|
1043
936
|
} else {
|
|
1044
937
|
log('💡 No optional addons selected', 'dim');
|
|
@@ -1054,12 +947,12 @@ async function init(skipPrompts = false) {
|
|
|
1054
947
|
function showHelp() {
|
|
1055
948
|
showLogo();
|
|
1056
949
|
log('Usage:', 'green');
|
|
1057
|
-
log(' npx devlyn-cli
|
|
1058
|
-
log(' npx devlyn-cli
|
|
1059
|
-
log(' npx devlyn-cli -y
|
|
1060
|
-
log(' npx devlyn-cli
|
|
1061
|
-
log(' npx devlyn-cli
|
|
1062
|
-
log(' npx devlyn-cli --help
|
|
950
|
+
log(' npx devlyn-cli Install/update devlyn: choose what (AGENTS.md, CLAUDE.md) and where (this project or global)');
|
|
951
|
+
log(' npx devlyn-cli -y Without prompts: AGENTS.md + .agents/skills, plus CLAUDE.md + .claude/ if this project has them');
|
|
952
|
+
log(' npx devlyn-cli -y --claude Also install CLAUDE.md + .claude/ for Claude Code');
|
|
953
|
+
log(' npx devlyn-cli -y --global Skills only, for every project on this machine (~/.agents/skills, ~/.codex/skills; ~/.claude/skills with --claude, or when it already has a devlyn install)');
|
|
954
|
+
log(' npx devlyn-cli list List available skills & templates');
|
|
955
|
+
log(' npx devlyn-cli --help Show this help\n');
|
|
1063
956
|
log('Optional skills (select during install):', 'green');
|
|
1064
957
|
OPTIONAL_ADDONS.filter((a) => a.type === 'local').forEach((skill) => {
|
|
1065
958
|
log(` ${skill.name} ${COLORS.dim}${skill.desc}${COLORS.reset}`);
|
|
@@ -1072,16 +965,14 @@ function showHelp() {
|
|
|
1072
965
|
OPTIONAL_ADDONS.filter((a) => a.type === 'mcp').forEach((mcp) => {
|
|
1073
966
|
log(` claude mcp add ${mcp.name} -- ${mcp.command} ${COLORS.dim}${mcp.desc}${COLORS.reset}`);
|
|
1074
967
|
});
|
|
1075
|
-
log('\nSupported CLIs for agent installation:', 'green');
|
|
1076
|
-
for (const [key, cli] of Object.entries(CLI_TARGETS)) {
|
|
1077
|
-
log(` ${key.padEnd(10)} ${cli.name}`);
|
|
1078
|
-
}
|
|
1079
968
|
log('');
|
|
1080
969
|
}
|
|
1081
970
|
|
|
1082
971
|
// Main
|
|
1083
972
|
const args = process.argv.slice(2);
|
|
1084
973
|
|
|
974
|
+
const INSTALL_FLAGS = ['-y', '--yes', '--claude', '--global'];
|
|
975
|
+
|
|
1085
976
|
async function main() {
|
|
1086
977
|
const command = args[0];
|
|
1087
978
|
|
|
@@ -1090,56 +981,25 @@ switch (command) {
|
|
|
1090
981
|
case '-h':
|
|
1091
982
|
showHelp();
|
|
1092
983
|
break;
|
|
1093
|
-
case '-y':
|
|
1094
|
-
case '--yes':
|
|
1095
|
-
await init(true);
|
|
1096
|
-
break;
|
|
1097
984
|
case 'list':
|
|
1098
985
|
case 'ls':
|
|
1099
986
|
listContents();
|
|
1100
987
|
break;
|
|
1101
|
-
case 'agents':
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
log(
|
|
1111
|
-
|
|
1112
|
-
// Install for a specific CLI
|
|
1113
|
-
installAgentsForCLI(subArg);
|
|
1114
|
-
log('\n✅ Done!', 'green');
|
|
1115
|
-
} else if (subArg !== undefined) {
|
|
1116
|
-
log(`Unknown CLI target: ${JSON.stringify(subArg)}`, 'yellow');
|
|
1117
|
-
log(`Supported: ${Object.keys(CLI_TARGETS).join(', ')}, all`, 'dim');
|
|
988
|
+
case 'agents':
|
|
989
|
+
console.error('`npx devlyn-cli agents` was removed in 4.1.0. Run `npx devlyn-cli` to choose what and where,');
|
|
990
|
+
console.error('or `npx devlyn-cli -y [--claude] [--global]` without prompts.');
|
|
991
|
+
process.exitCode = 1;
|
|
992
|
+
break;
|
|
993
|
+
default: {
|
|
994
|
+
const flags = command === 'init' ? args.slice(1) : args;
|
|
995
|
+
const unknown = flags.find((flag) => !INSTALL_FLAGS.includes(flag));
|
|
996
|
+
if (unknown !== undefined) {
|
|
997
|
+
log(`Unknown command: ${unknown}`, 'yellow');
|
|
998
|
+
showHelp();
|
|
1118
999
|
process.exit(1);
|
|
1119
|
-
} else {
|
|
1120
|
-
// Auto-detect and install
|
|
1121
|
-
const detected = detectOtherCLIs();
|
|
1122
|
-
if (detected.length === 0) {
|
|
1123
|
-
log('\n🔍 No other AI CLIs detected in this project.', 'yellow');
|
|
1124
|
-
log(' Use `npx devlyn-cli agents all` to install for all supported CLIs', 'dim');
|
|
1125
|
-
log(` Supported: ${Object.keys(CLI_TARGETS).join(', ')}`, 'dim');
|
|
1126
|
-
} else {
|
|
1127
|
-
log(`\n🔍 Detected: ${detected.map((k) => CLI_TARGETS[k].name).join(', ')}`, 'blue');
|
|
1128
|
-
const count = installAgentsForAllDetected();
|
|
1129
|
-
log(`\n✅ Agents installed for ${count} CLI${count !== 1 ? 's' : ''}`, 'green');
|
|
1130
|
-
}
|
|
1131
1000
|
}
|
|
1132
|
-
|
|
1133
|
-
break;
|
|
1001
|
+
await init({ yes: flags.includes('-y') || flags.includes('--yes'), claude: flags.includes('--claude'), global: flags.includes('--global') });
|
|
1134
1002
|
}
|
|
1135
|
-
case 'init':
|
|
1136
|
-
case undefined:
|
|
1137
|
-
await init(false);
|
|
1138
|
-
break;
|
|
1139
|
-
default:
|
|
1140
|
-
log(`Unknown command: ${command}`, 'yellow');
|
|
1141
|
-
showHelp();
|
|
1142
|
-
process.exit(1);
|
|
1143
1003
|
}
|
|
1144
1004
|
}
|
|
1145
1005
|
|