devlyn-cli 4.0.1 → 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/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
@@ -46,14 +46,38 @@ const PRE_STANDARD_SKILL_MD_SHA256 = new Set([
46
46
  '5f2e8b29609cbb2af72941579823bec598265ebf29cd3a375d6aa47d95b74354', // devlyn-pencil-push
47
47
  ]);
48
48
 
49
- // Skills devlyn-cli shipped and later deleted without listing them, by SHA-256 of their only
50
- // file, SKILL.md (LF endings): a user's own folder of the same name is never removed.
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.
51
52
  const RETIRED_SKILL_MD_SHA256 = {
52
53
  // 0.2.0-1.15.0; deleted in iter-0034, it still routes agents to retired commands.
53
54
  'workflow-routing': new Set([
54
55
  '7fec2bb20d808a873a975d64a6223e404bc5b328c800760bb77257ae6b2f467a',
55
56
  '8bfdaeff2ce48d5a79b5653de3bbe0d56a61a7fd750ed3ad11149fec5a502ecf',
56
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
+ ]),
57
81
  };
58
82
 
59
83
  // Whether `dir` is a real folder whose only file (dotfiles such as .DS_Store aside) is a SKILL.md
@@ -68,63 +92,34 @@ function isShippedCopy(dir, hashes) {
68
92
  .update(fs.readFileSync(skill, 'utf8').replace(/\r\n/g, '\n')).digest('hex'));
69
93
  }
70
94
 
71
- // Cross-agent shared skills directory read by BOTH oh-my-pi and Pi. Verified
72
- // from the omp binary's skill-provider strings ("skills from .agents/skills —
73
- // project walk-up + user home") and Pi's docs ("~/.agents/skills/"). Installing
74
- // the bundle here once covers both agents — the de-dup driver writes it a single
75
- // time even when both are selected.
76
- const SHARED_AGENTS_SKILLS_DIR = path.join(os.homedir(), '.agents', 'skills');
77
-
78
- // Cross-CLI agent installation targets
79
- // Each entry maps a CLI tool to where its agent instructions should be placed
80
- const CLI_TARGETS = {
81
- codex: {
82
- name: 'Codex CLI (OpenAI)',
83
- instructionsFile: 'AGENTS.md',
84
- baseInstructionsFile: 'AGENTS.md',
85
- // Codex auto-loads skills from ~/.codex/skills/ (user-global). Same
86
- // SKILL.md format as Claude Code; descriptions must stay ≤1024 chars.
87
- skillsDir: path.join(os.homedir(), '.codex', 'skills'),
88
- skillsToInstall: DEVLYN_CORE_SKILLS,
89
- detect: () => fs.existsSync(path.join(process.cwd(), 'AGENTS.md')) || fs.existsSync(path.join(process.cwd(), '.codex')),
90
- },
91
- omp: {
92
- name: 'oh-my-pi (omp)',
93
- instructionsFile: 'AGENTS.md',
94
- baseInstructionsFile: 'AGENTS.md',
95
- // omp loads skills from ~/.agents/skills (user home) — shared with Pi.
96
- skillsDir: SHARED_AGENTS_SKILLS_DIR,
97
- skillsToInstall: DEVLYN_CORE_SKILLS,
98
- // Project-scoped only: machine-level ~/.omp must not auto-trigger a project
99
- // AGENTS.md write via `npx devlyn-cli agents` in an unrelated repo.
100
- detect: () => fs.existsSync(path.join(process.cwd(), '.omp')) || fs.existsSync(path.join(process.cwd(), '.agents')),
101
- },
102
- pi: {
103
- name: 'Pi (earendil-works)',
104
- instructionsFile: 'AGENTS.md',
105
- baseInstructionsFile: 'AGENTS.md',
106
- // Pi loads skills from ~/.agents/skills — shared with oh-my-pi.
107
- skillsDir: SHARED_AGENTS_SKILLS_DIR,
108
- skillsToInstall: DEVLYN_CORE_SKILLS,
109
- // Project-scoped only (see omp): machine-level ~/.pi must not auto-trigger.
110
- detect: () => fs.existsSync(path.join(process.cwd(), '.pi')) || fs.existsSync(path.join(process.cwd(), '.agents')),
111
- },
112
- grok: {
113
- name: 'Grok Build CLI (xAI)',
114
- instructionsFile: 'AGENTS.md',
115
- baseInstructionsFile: 'AGENTS.md',
116
- // Grok discovers same-format SKILL.md skills from ./.grok/skills >
117
- // <repo_root>/.grok/skills > ~/.grok/skills > ~/.claude/skills
118
- // (Claude-compatible); skills are slash-invocable in the Grok TUI.
119
- skillsDir: path.join(os.homedir(), '.grok', 'skills'),
120
- skillsToInstall: DEVLYN_CORE_SKILLS,
121
- // Project-scoped only: machine-level ~/.grok must not auto-trigger a project
122
- // AGENTS.md write via `npx devlyn-cli agents` in an unrelated repo.
123
- detect: () => fs.existsSync(path.join(process.cwd(), '.grok')),
124
- },
125
- };
126
-
127
- // 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/.
128
123
  const DEPRECATED_FILES = [
129
124
  'commands/devlyn.handoff.md', // removed in v0.2.0
130
125
  'commands/devlyn.clean.md', // migrated to skills in v0.6.0
@@ -150,9 +145,9 @@ const DEPRECATED_FILES = [
150
145
  // iter-0034 Phase 4 cutover (2026-05-03): 15 user skills deleted and 3 moved
151
146
  // to optional-skills/. Listed here so post-cutover `npx devlyn-cli` upgrades
152
147
  // force-remove stale legacy skill dirs from downstream `~/.claude/skills/`
153
- // even though the source dirs no longer exist (cleanManagedSkillDirs only
154
- // removes target dirs that still exist in source — without this list,
155
- // deleted-from-source skills persist in user installs forever).
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).
156
151
  const DEPRECATED_DIRS = [
157
152
  // v0.7.x rename: devlyn-* → devlyn:*
158
153
  'skills/devlyn-clean',
@@ -183,11 +178,6 @@ const DEPRECATED_DIRS = [
183
178
  'skills/devlyn:team-resolve',
184
179
  'skills/devlyn:team-review',
185
180
  'skills/devlyn:update-docs',
186
- // 0221 Session 1: standards skills moved to optional-skills/ (opt-in).
187
- 'skills/code-health-standards',
188
- 'skills/code-review-standards',
189
- 'skills/root-cause-analysis',
190
- 'skills/ui-implementation-standards',
191
181
  // Deleted entirely on 2026-05-14 (devlyn:team-design-ui merged into
192
182
  // devlyn:design-ui; devlyn:design-system removed outright). Entries kept
193
183
  // so users who previously opted in get their stale copies purged on upgrade.
@@ -195,9 +185,9 @@ const DEPRECATED_DIRS = [
195
185
  'skills/devlyn:design-system',
196
186
  ];
197
187
 
198
- function getTargetDir() {
188
+ function projectDir() {
199
189
  try {
200
- return path.join(process.cwd(), '.claude');
190
+ return process.cwd();
201
191
  } catch {
202
192
  console.error('\n\x1b[33m❌ Current directory no longer exists.\x1b[0m');
203
193
  console.error('\x1b[2m Please cd into a valid directory and try again.\x1b[0m\n');
@@ -237,14 +227,14 @@ ${v} ██║ ██║${p}██╔══╝ ${k}╚██╗ ██╔
237
227
  ${v} ██████╔╝${p}███████╗${k} ╚████╔╝ ${v}███████╗ ${p}██║ ${k}██║ ╚████║${r}
238
228
  ${g} ╚═════╝ ╚══════╝ ╚═══╝ ╚══════╝ ╚═╝ ╚═╝ ╚═══╝${r}
239
229
 
240
- ${COLORS.dim} Claude Code Config Toolkit${r}
230
+ ${COLORS.dim} AI Agent Config Toolkit${r}
241
231
  ${g} v${PKG.version} ${COLORS.dim}· ${k}🍩 by Nocodecat @ Donut Studio${r}
242
232
  `;
243
233
  console.log(logo);
244
234
  }
245
235
 
246
236
  const OPTIONAL_ADDONS = [
247
- // Local optional skills (copied to .claude/skills/)
237
+ // Local optional skills (copied into every selected skill root)
248
238
  { name: 'asset-creator', desc: 'AI pixel art game asset pipeline — generate, chroma-key, catalog', type: 'local' },
249
239
  { name: 'cloudflare-nextjs-setup', desc: 'Cloudflare Workers + Next.js deployment with OpenNext', type: 'local' },
250
240
  { name: 'generate-skill', desc: 'Create well-structured Claude Code skills following Anthropic best practices', type: 'local' },
@@ -362,18 +352,11 @@ function listContents() {
362
352
  log('');
363
353
  }
364
354
 
365
- function cleanupDeprecated(targetDir) {
355
+ // Old and retired skills in one skill root; nothing outside it.
356
+ function cleanupDeprecated(skillsDir) {
366
357
  let removed = 0;
367
358
  const deprecated = DEPRECATED_DIRS.flatMap((relPath) =>
368
- legacySkillPaths(path.join(targetDir, 'skills'), path.basename(relPath)).map((fullPath) => [relPath, fullPath]));
369
- for (const relPath of DEPRECATED_FILES) {
370
- const fullPath = path.join(targetDir, relPath);
371
- if (fs.existsSync(fullPath)) {
372
- fs.unlinkSync(fullPath);
373
- log(` ✕ ${relPath} (deprecated)`, 'dim');
374
- removed++;
375
- }
376
- }
359
+ legacySkillPaths(skillsDir, path.basename(relPath)).map((fullPath) => [relPath, fullPath]));
377
360
  for (const [relPath, fullPath] of deprecated) {
378
361
  if (fs.existsSync(fullPath)) {
379
362
  fs.rmSync(fullPath, { recursive: true });
@@ -382,7 +365,7 @@ function cleanupDeprecated(targetDir) {
382
365
  }
383
366
  }
384
367
  for (const [name, hashes] of Object.entries(RETIRED_SKILL_MD_SHA256)) {
385
- const fullPath = path.join(targetDir, 'skills', name);
368
+ const fullPath = path.join(skillsDir, name);
386
369
  if (isShippedCopy(fullPath, hashes)) {
387
370
  fs.rmSync(fullPath, { recursive: true });
388
371
  log(` ✕ skills/${name}/ (removed)`, 'dim');
@@ -413,8 +396,6 @@ function copyRecursive(src, dest, baseDir) {
413
396
  const stats = fs.statSync(src);
414
397
 
415
398
  if (stats.isDirectory()) {
416
- // Never install dev workspaces, even when running from source repo.
417
- if (UNSHIPPED_SKILL_DIRS.has(path.basename(src))) return;
418
399
  if (!fs.existsSync(dest)) {
419
400
  fs.mkdirSync(dest, { recursive: true });
420
401
  }
@@ -475,38 +456,6 @@ function writeInstallMarker(skillsDir) {
475
456
  }
476
457
  }
477
458
 
478
- // Dev artifacts that live under config/skills/ but must never ship or install.
479
- // Mirrors the `!` exclusions in package.json files[].
480
- const UNSHIPPED_SKILL_DIRS = new Set([
481
- 'devlyn:auto-resolve-workspace',
482
- 'devlyn:ideate-workspace',
483
- 'devlyn-ideate-workspace',
484
- 'preflight-workspace',
485
- 'roadmap-archival-workspace',
486
- ]);
487
-
488
- // Clean managed skill directories before copy to prevent stale-file drift.
489
- // copyRecursive is a pure overlay: if a file was removed or renamed in source,
490
- // the installed mirror keeps the old copy. For each top-level dir under
491
- // config/skills/, remove its counterpart in target/skills/ before the copy so
492
- // each managed skill is fully replaced on every sync. User-installed skills
493
- // (e.g. skill-creator from optional addons) are left alone because they have
494
- // no counterpart in source. Dev workspaces are skipped entirely.
495
- function cleanManagedSkillDirs(sourceSkillsDir, targetSkillsDir) {
496
- if (!fs.existsSync(sourceSkillsDir) || !fs.existsSync(targetSkillsDir)) return 0;
497
- let cleaned = 0;
498
- for (const entry of fs.readdirSync(sourceSkillsDir, { withFileTypes: true })) {
499
- if (!entry.isDirectory()) continue;
500
- if (UNSHIPPED_SKILL_DIRS.has(entry.name)) continue;
501
- const targetPath = path.join(targetSkillsDir, entry.name);
502
- if (fs.existsSync(targetPath)) {
503
- fs.rmSync(targetPath, { recursive: true, force: true });
504
- cleaned++;
505
- }
506
- }
507
- return cleaned;
508
- }
509
-
510
459
  function multiSelect(items, preselectedIndices = []) {
511
460
  return new Promise((resolve) => {
512
461
  const selected = new Set(preselectedIndices.filter((i) => i >= 0 && i < items.length));
@@ -527,10 +476,10 @@ function multiSelect(items, preselectedIndices = []) {
527
476
  const checkbox = selected.has(i) ? `${COLORS.green}◉${COLORS.reset}` : `${COLORS.dim}○${COLORS.reset}`;
528
477
  const pointer = i === cursor ? `${COLORS.cyan}❯${COLORS.reset}` : ' ';
529
478
  const name = i === cursor ? `${COLORS.cyan}${item.name}${COLORS.reset}` : item.name;
530
- const tagLabel = item.type === 'mcp' ? 'mcp' : item.type === 'local' ? 'skill' : item.type === 'cli' ? 'cli' : 'pack';
531
- const tagColor = item.type === 'mcp' ? COLORS.green : item.type === 'local' ? COLORS.magenta : item.type === 'cli' ? COLORS.blue : COLORS.cyan;
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;
532
481
  const tag = `${tagColor}${tagLabel}${COLORS.reset}`;
533
- 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}` : ''}`);
534
483
  console.log(` ${COLORS.dim}${item.desc}${COLORS.reset}`);
535
484
  });
536
485
  };
@@ -600,26 +549,48 @@ function multiSelect(items, preselectedIndices = []) {
600
549
  });
601
550
  }
602
551
 
603
- function installLocalSkill(skillName) {
604
- const src = path.join(OPTIONAL_SKILLS_SOURCE, skillName);
605
- const targetDir = getTargetDir();
606
- const targets = new Set([path.join(targetDir, 'skills'), ...Object.values(CLI_TARGETS)
607
- .map((cli) => cli.skillsDir).filter((dir) => dir && fs.existsSync(dir))]);
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
+ }
608
586
 
609
- if (!fs.existsSync(src)) {
587
+ function installLocalSkill(skillName, roots) {
588
+ if (!fs.existsSync(path.join(OPTIONAL_SKILLS_SOURCE, skillName))) {
610
589
  log(` ⚠️ Skill "${skillName}" not found`, 'yellow');
611
590
  return false;
612
591
  }
613
-
614
592
  log(`\n🛠️ Installing ${skillName}...`, 'cyan');
615
-
616
- // Mirror to every CLI skill-loader directory that already exists so optional
617
- // skills are picked up by Codex/omp/Pi (and any future CLI with a skillsDir)
618
- // the same way required skills are. Existing dir, not new dir — we don't
619
- // create an agent install just because someone opted into a Claude-side skill.
620
- // De-dup by directory: omp and Pi share ~/.agents/skills, so the mirror runs
621
- // once per unique destination.
622
- for (const target of targets) installOptionalSkillInto(target, skillName);
593
+ for (const root of roots) installOptionalSkillInto(root, skillName);
623
594
  return true;
624
595
  }
625
596
 
@@ -673,9 +644,9 @@ function installSkillPack(packName) {
673
644
  }
674
645
  }
675
646
 
676
- function installAddon(addon) {
647
+ function installAddon(addon, roots) {
677
648
  if (addon.type === 'local') {
678
- return installLocalSkill(addon.name);
649
+ return installLocalSkill(addon.name, roots);
679
650
  }
680
651
  if (addon.type === 'mcp') {
681
652
  return installMcpServer(addon.name, addon.command);
@@ -683,168 +654,33 @@ function installAddon(addon) {
683
654
  return installSkillPack(addon.name);
684
655
  }
685
656
 
686
- function detectOtherCLIs() {
687
- const detected = [];
688
- for (const [key, cli] of Object.entries(CLI_TARGETS)) {
689
- if (cli.detect()) {
690
- detected.push(key);
691
- }
692
- }
693
- return detected;
694
- }
695
-
696
- // Install DEVLYN_CORE_SKILLS into a CLI's
697
- // global skills directory (e.g. ~/.codex/skills/). Returns count of skills
698
- // copied. Skipped silently for CLIs without a skillsDir (e.g. cursor, copilot
699
- // at the time of writing — they don't have an analogous skill-loader).
700
- function installSkillsForCLI(cliKey) {
701
- const cli = CLI_TARGETS[cliKey];
702
- if (!cli || !cli.skillsDir || !cli.skillsToInstall) return 0;
703
-
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) {
704
660
  const sourceSkillsDir = path.join(CONFIG_SOURCE, 'skills');
705
- if (!fs.existsSync(cli.skillsDir)) {
706
- fs.mkdirSync(cli.skillsDir, { recursive: true });
707
- }
708
- clearInstallMarker(cli.skillsDir);
709
-
710
- 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);
711
665
  if (removed > 0) {
712
666
  log(`\n🧹 Cleaned up ${removed} deprecated file${removed > 1 ? 's' : ''}`, 'yellow');
713
667
  }
714
-
715
- let copied = 0;
716
- for (const skillName of cli.skillsToInstall) {
668
+ for (const skillName of DEVLYN_CORE_SKILLS) {
717
669
  const src = path.join(sourceSkillsDir, skillName);
718
- const dest = path.join(cli.skillsDir, skillName);
670
+ const dest = path.join(skillsDir, skillName);
719
671
  if (!fs.existsSync(src)) continue;
720
- // Full replace per cleanManagedSkillDirs semantics: stale files in the
721
- // installed mirror would otherwise persist forever.
722
- if (fs.existsSync(dest)) {
723
- fs.rmSync(dest, { recursive: true, force: true });
724
- }
725
- copyRecursive(src, dest, cli.skillsDir);
726
- copied++;
727
- log(` → ${cli.skillsDir.replace(os.homedir(), '~')}/${skillName}`, 'dim');
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);
728
675
  }
729
- assertCompleteSkillInstall(sourceSkillsDir, cli.skillsDir, cli.skillsToInstall);
730
- retireRenamedSkills(cli.skillsDir);
731
- writeInstallMarker(cli.skillsDir);
732
- return copied;
733
- }
734
-
735
- // One-line description of exactly what selecting a CLI target installs, so the
736
- // unified selector stays honest.
737
- function targetDesc(cli) {
738
- if (cli.skillsDir) {
739
- return `${cli.instructionsFile} + devlyn skills → ${cli.skillsDir.replace(os.homedir(), '~')}`;
740
- }
741
- return `${cli.instructionsFile} instructions only`;
742
- }
743
-
744
- // Shared instruction ownership and preservation rules apply to every CLI.
745
- function installInstructionsForCLI(cliKey) {
746
- const cli = CLI_TARGETS[cliKey];
747
- return cli?.baseInstructionsFile ? updateInstructions(cli.baseInstructionsFile) : false;
748
- }
749
-
750
- // Install both instructions and skills for a single CLI. Used by the
751
- // `agents <cli>` command; multi-target paths use installSelectedCLITargets.
752
- function installAgentsForCLI(cliKey) {
753
- const instr = installInstructionsForCLI(cliKey);
754
- const skillsCopied = installSkillsForCLI(cliKey);
755
- if (skillsCopied > 0) {
756
- log(` → ${skillsCopied} skill${skillsCopied > 1 ? 's' : ''} installed (${DEVLYN_CORE_SKILLS.join(' / ')})`, 'dim');
757
- }
758
- return instr || skillsCopied > 0;
759
- }
760
-
761
- // Install instructions + skills for a set of selected CLI targets, writing each
762
- // unique destination exactly once. omp and Pi share project AGENTS.md and the
763
- // ~/.agents/skills dir, so selecting both does not duplicate writes — this is
764
- // the researched "group the common" model: targets are the user's selection,
765
- // destinations are the unit of work.
766
- function installSelectedCLITargets(cliKeys) {
767
- const instrDone = new Set();
768
- const skillsDone = new Set();
769
- let count = 0;
770
- for (const cliKey of cliKeys) {
771
- const cli = CLI_TARGETS[cliKey];
772
- if (!cli) continue;
773
- const instrKey = cli.instructionsFile;
774
- if (!instrDone.has(instrKey)) {
775
- installInstructionsForCLI(cliKey);
776
- instrDone.add(instrKey);
777
- }
778
- if (cli.skillsDir && !skillsDone.has(cli.skillsDir)) {
779
- const copied = installSkillsForCLI(cliKey);
780
- if (copied > 0) {
781
- log(` → ${copied} skill${copied > 1 ? 's' : ''} → ${cli.skillsDir.replace(os.homedir(), '~')}`, 'dim');
782
- }
783
- skillsDone.add(cli.skillsDir);
784
- }
785
- count++;
786
- }
787
- return count;
788
- }
789
-
790
- function installAgentsForAllDetected() {
791
- const detected = detectOtherCLIs();
792
- if (detected.length === 0) return 0;
793
- return installSelectedCLITargets(detected);
794
- }
795
-
796
- // Install the Claude Code core config: project .claude/ (skills, templates,
797
- // settings, CLAUDE.md, .gitignore) plus global ~/.claude/settings.json tweaks.
798
- // Extracted so the unified target selector installs it only when "Claude Code"
799
- // is chosen. Check global settings and instruction conflicts before writes.
800
- function installClaudeCore() {
801
- const globalClaudeDir = path.join(os.homedir(), '.claude');
802
- const globalSettingsPath = path.join(globalClaudeDir, 'settings.json');
803
- function readGlobalSettings() {
804
- let settings = {};
805
- try {
806
- if (fs.lstatSync(globalSettingsPath, { throwIfNoEntry: false })) {
807
- settings = JSON.parse(fs.readFileSync(globalSettingsPath, 'utf8'));
808
- }
809
- } catch (error) {
810
- const reason = error instanceof SyntaxError ? 'invalid JSON' : error.message;
811
- throw new Error(`Cannot merge ${globalSettingsPath}: ${reason}. Original preserved; correct the file or its access and rerun installation.`);
812
- }
813
- if (!settings || typeof settings !== 'object' || Array.isArray(settings)) {
814
- throw new Error(`Cannot merge ${globalSettingsPath}: root must be a JSON object. Original preserved; correct it and rerun installation.`);
815
- }
816
- if (!Object.prototype.hasOwnProperty.call(settings, 'env')) settings.env = {};
817
- if (!settings.env || typeof settings.env !== 'object' || Array.isArray(settings.env)) {
818
- throw new Error(`Cannot merge ${globalSettingsPath}: env must be a JSON object. Original preserved; correct it and rerun installation.`);
819
- }
820
- return settings;
821
- }
822
- readGlobalSettings();
823
- updateInstructions('CLAUDE.md');
824
- const targetDir = getTargetDir();
825
- const skillsDir = path.join(targetDir, 'skills');
826
- log('\n📁 Installing Claude Code config to .claude/', 'green');
827
- if (!fs.existsSync(skillsDir)) fs.mkdirSync(skillsDir, { recursive: true });
828
- clearInstallMarker(skillsDir);
829
- const removed = cleanupDeprecated(targetDir);
830
- if (removed > 0) {
831
- log(`\n🧹 Cleaned up ${removed} deprecated file${removed > 1 ? 's' : ''}`, 'yellow');
832
- }
833
- const refreshed = cleanManagedSkillDirs(
834
- path.join(CONFIG_SOURCE, 'skills'),
835
- skillsDir,
836
- );
837
- if (refreshed > 0) {
838
- log(` 🔄 Refreshing ${refreshed} managed skill director${refreshed === 1 ? 'y' : 'ies'}`, 'dim');
839
- }
840
- copyRecursive(CONFIG_SOURCE, targetDir, targetDir);
841
- assertCompleteSkillInstall(path.join(CONFIG_SOURCE, 'skills'), skillsDir, DEVLYN_CORE_SKILLS);
676
+ assertCompleteSkillInstall(sourceSkillsDir, skillsDir, DEVLYN_CORE_SKILLS);
842
677
  retireRenamedSkills(skillsDir);
843
678
  writeInstallMarker(skillsDir);
679
+ }
844
680
 
845
- // Keep installer-managed pipeline state and install metadata out of git.
846
- const gitignorePath = path.join(process.cwd(), '.gitignore');
847
- const gitignoreEntries = ['.devlyn/', '.claude/skills/.devlyn-install.json'];
681
+ // Keep installer-managed pipeline state and install metadata out of git.
682
+ function ignoreInGit(gitignoreEntries) {
683
+ const gitignorePath = path.join(projectDir(), '.gitignore');
848
684
  let gitignoreContent = fs.existsSync(gitignorePath)
849
685
  ? fs.readFileSync(gitignorePath, 'utf8')
850
686
  : '';
@@ -858,6 +694,55 @@ function installClaudeCore() {
858
694
  fs.writeFileSync(gitignorePath, gitignoreContent + prefix + header + missingGitignoreEntries.join('\n') + '\n');
859
695
  log(` → .gitignore (added ${missingGitignoreEntries.join(', ')})`, 'dim');
860
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']);
861
746
 
862
747
  // Enable agent teams in project settings
863
748
  const settingsPath = path.join(targetDir, 'settings.json');
@@ -911,6 +796,10 @@ function installClaudeCore() {
911
796
  settingsChanged = true;
912
797
  }
913
798
  }
799
+ if (!settings.env.ENABLE_PROMPT_CACHING_1H) {
800
+ settings.env.ENABLE_PROMPT_CACHING_1H = 'true';
801
+ settingsChanged = true;
802
+ }
914
803
  if (!settings.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS) {
915
804
  settings.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS = '1';
916
805
  settingsChanged = true;
@@ -948,23 +837,52 @@ function installClaudeCore() {
948
837
  }
949
838
  if (settingsChanged) {
950
839
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
951
- log(' → settings.json (agent teams + one-hour Bash max + pipeline permissions + Stop hook)', 'dim');
952
- }
953
-
954
- // Configure global Claude Code settings (~/.claude/settings.json)
955
- // Project settings may refer to this same file; merge the latest bytes.
956
- const globalSettings = readGlobalSettings();
957
- if (!globalSettings.env.ENABLE_PROMPT_CACHING_1H) {
958
- globalSettings.env.ENABLE_PROMPT_CACHING_1H = 'true';
959
- if (!fs.existsSync(globalClaudeDir)) fs.mkdirSync(globalClaudeDir, { recursive: true });
960
- fs.writeFileSync(globalSettingsPath, JSON.stringify(globalSettings, null, 2) + '\n');
961
- log(' → ~/.claude/settings.json (enabled 1h prompt caching)', 'dim');
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');
962
882
  }
963
-
964
- log('\n✅ Claude Code config installed!', 'green');
965
883
  }
966
884
 
967
- async function init(skipPrompts = false) {
885
+ async function init({ yes, claude, global }) {
968
886
  showLogo();
969
887
  log('─'.repeat(44), 'dim');
970
888
 
@@ -973,53 +891,47 @@ async function init(skipPrompts = false) {
973
891
  process.exit(1);
974
892
  }
975
893
 
976
- // Non-interactive / -y: preserve historical behavior — install Claude core
977
- // only. Multi-target selection needs a TTY; scripted runs add other agents
978
- // via `npx devlyn-cli agents <cli>`.
979
- if (skipPrompts || !process.stdin.isTTY) {
980
- 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);
981
898
  log('\n💡 Add optional addons later: run `npx devlyn-cli` without -y', 'dim');
982
- log(' Add another agent (Codex / omp / Pi / …) later: `npx devlyn-cli agents <cli>`', 'dim');
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');
983
904
  log(`\n${COLORS.dim} Enjoying devlyn? Star it on GitHub — it helps others find it:${COLORS.reset}`);
984
905
  log(` ${COLORS.purple}→ https://github.com/fysoul17/devlyn-cli${COLORS.reset}\n`);
985
906
  return;
986
907
  }
987
908
 
988
- // Pick every agent to install devlyn into, up front. Claude Code is
989
- // pre-checked (the happy path); every other agent is an explicit opt-in so a
990
- // bare Enter never mutates a project's AGENTS.md or a shared skills dir for an
991
- // agent the user didn't choose.
992
- log('\n🎯 Select the agents to install devlyn into:\n', 'blue');
909
+ log('\n🎯 What to install:\n', 'blue');
993
910
  const targetOptions = [
994
- { key: 'claude', name: 'Claude Code', desc: '.claude/ config — skills, templates, settings, CLAUDE.md', type: 'cli' },
995
- ...Object.entries(CLI_TARGETS).map(([key, cli]) => ({ key, name: cli.name, desc: targetDesc(cli), type: 'cli' })),
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)' },
996
913
  ];
997
- const preselected = [targetOptions.findIndex((o) => o.key === 'claude')];
998
- const selectedKeys = (await multiSelect(targetOptions, preselected)).map((t) => t.key);
914
+ const preselected = claude || hasDevlynClaude(false) || (global && hasDevlynClaude(true)) ? [0, 1] : [0];
915
+ const targets = (await multiSelect(targetOptions, preselected)).map((option) => option.key);
999
916
 
1000
- if (selectedKeys.length === 0) {
1001
- log('\n💡 No agents selected — nothing installed.', 'yellow');
1002
- log(' Run `npx devlyn-cli` again and pick at least one agent.\n', 'dim');
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');
1003
920
  return;
1004
921
  }
1005
922
 
1006
- if (selectedKeys.includes('claude')) {
1007
- installClaudeCore();
1008
- }
1009
- const cliKeys = selectedKeys.filter((k) => k !== 'claude');
1010
- if (cliKeys.length > 0) {
1011
- const installed = installSelectedCLITargets(cliKeys);
1012
- log(`\n ✅ Installed devlyn into ${installed} agent${installed !== 1 ? 's' : ''}`, 'green');
1013
- }
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);
1014
926
 
1015
- // Ask about optional addons (local skills + external packs)
927
+ // Ask about optional addons (local skills + external packs; MCP servers belong to Claude Code)
1016
928
  log('\n📚 Optional skills & packs:\n', 'blue');
1017
929
 
1018
- const selectedAddons = await multiSelect(OPTIONAL_ADDONS);
930
+ const selectedAddons = await multiSelect(OPTIONAL_ADDONS.filter((addon) => addon.type !== 'mcp' || targets.includes('claude')));
1019
931
 
1020
932
  if (selectedAddons.length > 0) {
1021
933
  for (const addon of selectedAddons) {
1022
- installAddon(addon);
934
+ installAddon(addon, roots);
1023
935
  }
1024
936
  } else {
1025
937
  log('💡 No optional addons selected', 'dim');
@@ -1035,12 +947,12 @@ async function init(skipPrompts = false) {
1035
947
  function showHelp() {
1036
948
  showLogo();
1037
949
  log('Usage:', 'green');
1038
- log(' npx devlyn-cli Install/update devlyn (pick agents: Claude / Codex / omp / Pi / …)');
1039
- log(' npx devlyn-cli list List available skills & templates');
1040
- log(' npx devlyn-cli -y Install Claude core without prompts');
1041
- log(' npx devlyn-cli agents Install agents for detected CLIs');
1042
- log(' npx devlyn-cli agents all Install agents for all supported CLIs');
1043
- log(' npx devlyn-cli --help Show this help\n');
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');
1044
956
  log('Optional skills (select during install):', 'green');
1045
957
  OPTIONAL_ADDONS.filter((a) => a.type === 'local').forEach((skill) => {
1046
958
  log(` ${skill.name} ${COLORS.dim}${skill.desc}${COLORS.reset}`);
@@ -1053,16 +965,14 @@ function showHelp() {
1053
965
  OPTIONAL_ADDONS.filter((a) => a.type === 'mcp').forEach((mcp) => {
1054
966
  log(` claude mcp add ${mcp.name} -- ${mcp.command} ${COLORS.dim}${mcp.desc}${COLORS.reset}`);
1055
967
  });
1056
- log('\nSupported CLIs for agent installation:', 'green');
1057
- for (const [key, cli] of Object.entries(CLI_TARGETS)) {
1058
- log(` ${key.padEnd(10)} ${cli.name}`);
1059
- }
1060
968
  log('');
1061
969
  }
1062
970
 
1063
971
  // Main
1064
972
  const args = process.argv.slice(2);
1065
973
 
974
+ const INSTALL_FLAGS = ['-y', '--yes', '--claude', '--global'];
975
+
1066
976
  async function main() {
1067
977
  const command = args[0];
1068
978
 
@@ -1071,56 +981,25 @@ switch (command) {
1071
981
  case '-h':
1072
982
  showHelp();
1073
983
  break;
1074
- case '-y':
1075
- case '--yes':
1076
- await init(true);
1077
- break;
1078
984
  case 'list':
1079
985
  case 'ls':
1080
986
  listContents();
1081
987
  break;
1082
- case 'agents': {
1083
- showLogo();
1084
- log('─'.repeat(44), 'dim');
1085
- const subArg = args[1];
1086
- if (subArg === 'all') {
1087
- // Install for all supported CLIs regardless of detection. De-duped so the
1088
- // shared ~/.agents/skills dir (omp + Pi) and shared AGENTS.md write once.
1089
- log('\n🤖 Installing agents for all supported CLIs...', 'blue');
1090
- const count = installSelectedCLITargets(Object.keys(CLI_TARGETS));
1091
- log(`\n✅ Agents installed for ${count} CLI${count !== 1 ? 's' : ''}`, 'green');
1092
- } else if (Object.keys(CLI_TARGETS).includes(subArg)) {
1093
- // Install for a specific CLI
1094
- installAgentsForCLI(subArg);
1095
- log('\n✅ Done!', 'green');
1096
- } else if (subArg !== undefined) {
1097
- log(`Unknown CLI target: ${JSON.stringify(subArg)}`, 'yellow');
1098
- 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();
1099
999
  process.exit(1);
1100
- } else {
1101
- // Auto-detect and install
1102
- const detected = detectOtherCLIs();
1103
- if (detected.length === 0) {
1104
- log('\n🔍 No other AI CLIs detected in this project.', 'yellow');
1105
- log(' Use `npx devlyn-cli agents all` to install for all supported CLIs', 'dim');
1106
- log(` Supported: ${Object.keys(CLI_TARGETS).join(', ')}`, 'dim');
1107
- } else {
1108
- log(`\n🔍 Detected: ${detected.map((k) => CLI_TARGETS[k].name).join(', ')}`, 'blue');
1109
- const count = installAgentsForAllDetected();
1110
- log(`\n✅ Agents installed for ${count} CLI${count !== 1 ? 's' : ''}`, 'green');
1111
- }
1112
1000
  }
1113
- log('');
1114
- break;
1001
+ await init({ yes: flags.includes('-y') || flags.includes('--yes'), claude: flags.includes('--claude'), global: flags.includes('--global') });
1115
1002
  }
1116
- case 'init':
1117
- case undefined:
1118
- await init(false);
1119
- break;
1120
- default:
1121
- log(`Unknown command: ${command}`, 'yellow');
1122
- showHelp();
1123
- process.exit(1);
1124
1003
  }
1125
1004
  }
1126
1005