devlyn-cli 4.0.1 ā 4.2.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 +11 -95
- package/CLAUDE.md +7 -160
- package/LICENSE +21 -0
- package/README.md +83 -114
- package/bin/devlyn.js +330 -472
- package/bin/instruction-templates.json +120 -2
- package/bin/instructions.js +35 -8
- package/config/skills/_shared/adapters/claude.md +8 -85
- package/config/skills/_shared/adapters/codex.md +14 -34
- package/config/skills/_shared/adapters/omp.md +1 -11
- package/config/skills/_shared/codex-monitored.sh +4 -54
- package/config/skills/_shared/engine-doctor.sh +18 -58
- package/config/skills/_shared/engine-preflight.md +15 -47
- package/config/skills/_shared/expected-contract.py +242 -0
- package/config/skills/_shared/expected.schema.json +10 -95
- package/config/skills/_shared/invocation-receipt.py +26 -757
- package/config/skills/_shared/platform-support.py +37 -28
- package/config/skills/_shared/role-config.py +100 -421
- package/config/skills/_shared/runtime-principles.md +17 -93
- package/config/skills/_shared/task-complete.py +490 -285
- package/config/skills/_shared/task-completion.md +235 -0
- package/config/skills/devlyn-engines/SKILL.md +9 -11
- package/config/skills/devlyn-ideate/SKILL.md +57 -107
- package/config/skills/devlyn-ideate/references/elicitation.md +18 -99
- package/config/skills/devlyn-ideate/references/loop.md +110 -0
- package/config/skills/devlyn-ideate/references/package-format.md +128 -0
- package/config/skills/devlyn-ideate/scripts/acceptance.py +795 -0
- package/config/skills/devlyn-ideate/scripts/queue.py +1780 -0
- package/package.json +2 -3
- package/config/commit-conventions.md +0 -28
- package/config/skills/_shared/adapters/README.md +0 -74
- package/config/skills/_shared/adapters/grok.md +0 -100
- package/config/skills/_shared/archive_run.py +0 -969
- package/config/skills/_shared/codex-config.md +0 -81
- package/config/skills/_shared/collect-codex-findings.py +0 -505
- package/config/skills/_shared/finish-gate.py +0 -521
- package/config/skills/_shared/grok-anchor-guard.py +0 -78
- package/config/skills/_shared/judge-output-parser.py +0 -200
- package/config/skills/_shared/judge-role-evidence.py +0 -320
- package/config/skills/_shared/phase-prompt-render.py +0 -183
- package/config/skills/_shared/process-evidence.py +0 -926
- package/config/skills/_shared/resolve-bootstrap.py +0 -1410
- package/config/skills/_shared/resolve-stop-hook.py +0 -359
- package/config/skills/_shared/run-bounded.py +0 -58
- package/config/skills/_shared/spec-verify-check.py +0 -5083
- package/config/skills/_shared/state-phase-write.py +0 -5989
- package/config/skills/_shared/terminal-claim-check.py +0 -777
- package/config/skills/_shared/verify-merge-findings.py +0 -3528
- package/config/skills/devlyn-design-ui/SKILL.md +0 -650
- package/config/skills/devlyn-ideate/references/from-spec-mode.md +0 -61
- package/config/skills/devlyn-ideate/references/project-mode.md +0 -80
- package/config/skills/devlyn-ideate/references/spec-template.md +0 -121
- package/config/skills/devlyn-ideate/references/templates/decision.md +0 -48
- package/config/skills/devlyn-ideate/references/templates/roadmap.md +0 -50
- package/config/skills/devlyn-ideate/references/templates/vision.md +0 -44
- package/config/skills/devlyn-queue/SKILL.md +0 -66
- package/config/skills/devlyn-queue/scripts/append.py +0 -94
- package/config/skills/devlyn-resolve/SKILL.md +0 -379
- package/config/skills/devlyn-resolve/references/free-form-mode.md +0 -71
- package/config/skills/devlyn-resolve/references/outer-loop.md +0 -23
- package/config/skills/devlyn-resolve/references/phases/build-gate.md +0 -75
- package/config/skills/devlyn-resolve/references/phases/cleanup.md +0 -33
- package/config/skills/devlyn-resolve/references/phases/implement.md +0 -44
- package/config/skills/devlyn-resolve/references/phases/plan.md +0 -39
- package/config/skills/devlyn-resolve/references/phases/probe-derive.md +0 -261
- package/config/skills/devlyn-resolve/references/phases/surface-close.md +0 -34
- package/config/skills/devlyn-resolve/references/phases/verify.md +0 -310
- package/config/skills/devlyn-resolve/references/state-schema.md +0 -152
- package/config/skills/devlyn-resolve/references/task-completion.md +0 -160
- package/config/templates/prompt-templates.md +0 -71
- package/config/templates/template-feature.spec.md +0 -255
- package/config/templates/template-product-spec.md +0 -680
package/bin/devlyn.js
CHANGED
|
@@ -10,28 +10,30 @@ 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, importsAgentsMd } = 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
|
|
17
17
|
// skill here installs it everywhere.
|
|
18
|
-
const DEVLYN_CORE_SKILLS = ['devlyn-
|
|
18
|
+
const DEVLYN_CORE_SKILLS = ['devlyn-ideate', 'devlyn-engines', '_shared'];
|
|
19
19
|
// 4.0.0 renamed the colon-named skills to the Agent Skills standard (`[a-z0-9-]`, name ==
|
|
20
20
|
// folder): Git for Windows cannot check out a folder with ':' in its name. Installs from 3.x
|
|
21
21
|
// and earlier keep the old folder under either spelling (':' or npm's U+F03A extraction
|
|
22
22
|
// alias). Both are removed, and an optional skill that was installed is installed again
|
|
23
23
|
// under its new name in the same place.
|
|
24
24
|
const RENAMED_SKILLS = {
|
|
25
|
-
'devlyn:resolve': 'devlyn-resolve',
|
|
26
25
|
'devlyn:ideate': 'devlyn-ideate',
|
|
27
|
-
'devlyn:design-ui': 'devlyn-design-ui',
|
|
28
26
|
'devlyn:engines': 'devlyn-engines',
|
|
29
|
-
'devlyn:queue': 'devlyn-queue',
|
|
30
27
|
'devlyn:pencil-pull': 'devlyn-pencil-pull',
|
|
31
28
|
'devlyn:pencil-push': 'devlyn-pencil-push',
|
|
32
29
|
'devlyn:reap': 'devlyn-reap',
|
|
33
30
|
};
|
|
34
31
|
const DEVLYN_INSTALL_MARKER = '.devlyn-install.json';
|
|
32
|
+
// Project settings earlier releases added for the retired pipeline.
|
|
33
|
+
const RETIRED_STOP_HOOK = 'python3 "$CLAUDE_PROJECT_DIR/.claude/skills/_shared/resolve-stop-hook.py"';
|
|
34
|
+
const RETIRED_ENV = ['CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS'];
|
|
35
|
+
const RETIRED_PERMISSIONS = ['Write(.devlyn/**)', 'Edit(.devlyn/**)', 'Bash(git add *)', 'Bash(git commit *)',
|
|
36
|
+
'Bash(git diff *)', 'Bash(git status *)', 'Bash(git log *)'];
|
|
35
37
|
|
|
36
38
|
// Every spelling a colon name can have on disk: its own and npm's U+F03A extraction alias.
|
|
37
39
|
function legacySkillPaths(root, name) {
|
|
@@ -46,14 +48,38 @@ const PRE_STANDARD_SKILL_MD_SHA256 = new Set([
|
|
|
46
48
|
'5f2e8b29609cbb2af72941579823bec598265ebf29cd3a375d6aa47d95b74354', // devlyn-pencil-push
|
|
47
49
|
]);
|
|
48
50
|
|
|
49
|
-
// Skills devlyn-cli
|
|
50
|
-
// file, SKILL.md (LF endings): a user's own folder of the same name is
|
|
51
|
+
// Skills devlyn-cli installed by default and later deleted or made opt-in, by SHA-256 of their
|
|
52
|
+
// only file, SKILL.md (LF endings): an opted-in copy or a user's own folder of the same name is
|
|
53
|
+
// never removed.
|
|
51
54
|
const RETIRED_SKILL_MD_SHA256 = {
|
|
52
55
|
// 0.2.0-1.15.0; deleted in iter-0034, it still routes agents to retired commands.
|
|
53
56
|
'workflow-routing': new Set([
|
|
54
57
|
'7fec2bb20d808a873a975d64a6223e404bc5b328c800760bb77257ae6b2f467a',
|
|
55
58
|
'8bfdaeff2ce48d5a79b5653de3bbe0d56a61a7fd750ed3ad11149fec5a502ecf',
|
|
56
59
|
]),
|
|
60
|
+
// The standards skills, 0.1.0-3.2.1; 3.3.0 (0221) made them optional addons.
|
|
61
|
+
'code-health-standards': new Set([
|
|
62
|
+
'df2838904c938aa0306ed333cc460cfc5c2cf0ffa20bb5ade57067c6e0acf9be',
|
|
63
|
+
'962a82b5ebeafac515f81f5c68494c67dbf31c3e3e453f23c4452d4725e21355',
|
|
64
|
+
]),
|
|
65
|
+
'code-review-standards': new Set([
|
|
66
|
+
'9c906f2b2a88c2d0aaeec9714405945da5177de6ec433f1acf520c6c4385b875',
|
|
67
|
+
'3bc8587a9143487328be2470dec8df17a19cb14f94e6ec916ed58de660d0e1c4',
|
|
68
|
+
'56abe5736f505c53ca9ce14087a9891cc8661be783abaec7b5b924e3f339cc2a',
|
|
69
|
+
'586dde5014f9ac2f9a13d43fa1c8c257a3ae998a3fc1b867b23b3b2e44f77eca',
|
|
70
|
+
]),
|
|
71
|
+
'root-cause-analysis': new Set([
|
|
72
|
+
'e200ddd04196435af3162a9e13595f8eb5ac68b06465026e0155e453b1bd1c93',
|
|
73
|
+
'1793dcb0f72621c78cc20c905fe5dd7a0af3115e904bf737a6a983a7b9d2c2b6',
|
|
74
|
+
'aab22d1bddc66c6a798f08cdf7be9b45fbe80ccfc928b068ef5d9e2ad951bdda',
|
|
75
|
+
'0eac104bfcde8bdf14d4c284be6f3f4cb243219a679bf569433c2465df84c6c3',
|
|
76
|
+
'e66d15a9585c07e819acfd89c1f6d030517130f6d8f8924bda3120307949fb2e',
|
|
77
|
+
]),
|
|
78
|
+
'ui-implementation-standards': new Set([
|
|
79
|
+
'e2473e35474e78f2b581e225cb33a1e711bb79519a02e9780fc9606e582b878f',
|
|
80
|
+
'1ff64816454171b3f879e9e5cf3c15e3d9c5f2ffc81b73262155f6bc93a1b054',
|
|
81
|
+
'13c709c0e2b01ae94997c271043a685200e969192d2056534bd787c5682874e8',
|
|
82
|
+
]),
|
|
57
83
|
};
|
|
58
84
|
|
|
59
85
|
// Whether `dir` is a real folder whose only file (dotfiles such as .DS_Store aside) is a SKILL.md
|
|
@@ -68,63 +94,34 @@ function isShippedCopy(dir, hashes) {
|
|
|
68
94
|
.update(fs.readFileSync(skill, 'utf8').replace(/\r\n/g, '\n')).digest('hex'));
|
|
69
95
|
}
|
|
70
96
|
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
|
97
|
+
// Skill roots of each target. In a project, `.agents/skills` serves Codex, omp, Pi and Grok,
|
|
98
|
+
// and Claude Code loads only `.claude/skills`. Globally Codex reads ~/.codex/skills and omp,
|
|
99
|
+
// Pi and Grok read ~/.agents/skills.
|
|
100
|
+
function skillRoots(target, global) {
|
|
101
|
+
const dirs = target === 'claude' ? ['.claude'] : global ? ['.agents', '.codex'] : ['.agents'];
|
|
102
|
+
return dirs.map((dir) => path.join(global ? os.homedir() : projectDir(), dir, 'skills'));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// A devlyn Claude install in this scope. Globally only the marker a `--global --claude` run
|
|
106
|
+
// writes counts; in a project also a devlyn skill (0.6.0 and later), command (0.2-0.5) or
|
|
107
|
+
// CLAUDE.md with devlyn defaults, held or imported from AGENTS.md, which a team may commit
|
|
108
|
+
// while ignoring .claude/. An optional devlyn skill is not one: 4.0.1 put those into
|
|
109
|
+
// .claude/skills without the Claude target. A CLAUDE.md link (often to AGENTS.md) is not the
|
|
110
|
+
// Claude target's file: updateInstructions refuses links.
|
|
111
|
+
function hasDevlynClaude(global) {
|
|
112
|
+
const claudeDir = path.dirname(skillRoots('claude', global)[0]);
|
|
113
|
+
if (global) return fs.existsSync(path.join(claudeDir, 'skills', DEVLYN_INSTALL_MARKER));
|
|
114
|
+
const names = (dir) => (fs.existsSync(dir) ? fs.readdirSync(dir) : []);
|
|
115
|
+
const optional = new Set(OPTIONAL_ADDONS.map((addon) => addon.name));
|
|
116
|
+
const instructions = path.join(path.dirname(claudeDir), 'CLAUDE.md');
|
|
117
|
+
const claude = fs.lstatSync(instructions, { throwIfNoEntry: false })?.isFile() ? fs.readFileSync(instructions, 'utf8') : '';
|
|
118
|
+
return names(path.join(claudeDir, 'skills')).some((name) => name === DEVLYN_INSTALL_MARKER
|
|
119
|
+
|| (name.startsWith('devlyn') && !optional.has(name.replace(/[:\uF03A]/g, '-'))))
|
|
120
|
+
|| names(path.join(claudeDir, 'commands')).some((name) => name.startsWith('devlyn.'))
|
|
121
|
+
|| holdsDevlynDefaults('CLAUDE.md', claude) || importsAgentsDefaults(claude);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Commands removed in previous versions; the project Claude install deletes them from .claude/.
|
|
128
125
|
const DEPRECATED_FILES = [
|
|
129
126
|
'commands/devlyn.handoff.md', // removed in v0.2.0
|
|
130
127
|
'commands/devlyn.clean.md', // migrated to skills in v0.6.0
|
|
@@ -150,9 +147,9 @@ const DEPRECATED_FILES = [
|
|
|
150
147
|
// iter-0034 Phase 4 cutover (2026-05-03): 15 user skills deleted and 3 moved
|
|
151
148
|
// to optional-skills/. Listed here so post-cutover `npx devlyn-cli` upgrades
|
|
152
149
|
// force-remove stale legacy skill dirs from downstream `~/.claude/skills/`
|
|
153
|
-
// even though the source dirs no longer exist (
|
|
154
|
-
//
|
|
155
|
-
//
|
|
150
|
+
// even though the source dirs no longer exist (the installer replaces only
|
|
151
|
+
// the skills it ships ā without this list, deleted-from-source skills persist
|
|
152
|
+
// in user installs forever).
|
|
156
153
|
const DEPRECATED_DIRS = [
|
|
157
154
|
// v0.7.x rename: devlyn-* ā devlyn:*
|
|
158
155
|
'skills/devlyn-clean',
|
|
@@ -183,21 +180,25 @@ const DEPRECATED_DIRS = [
|
|
|
183
180
|
'skills/devlyn:team-resolve',
|
|
184
181
|
'skills/devlyn:team-review',
|
|
185
182
|
'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
183
|
// Deleted entirely on 2026-05-14 (devlyn:team-design-ui merged into
|
|
192
184
|
// devlyn:design-ui; devlyn:design-system removed outright). Entries kept
|
|
193
185
|
// so users who previously opted in get their stale copies purged on upgrade.
|
|
194
186
|
'skills/devlyn:team-design-ui',
|
|
195
187
|
'skills/devlyn:design-system',
|
|
188
|
+
// Retired after 4.1.0 (design-ui deleted; the intent queue moved into
|
|
189
|
+
// devlyn-ideate; resolve retired). All were core skills, so their 3.x and
|
|
190
|
+
// 4.x folders go.
|
|
191
|
+
'skills/devlyn-design-ui',
|
|
192
|
+
'skills/devlyn:design-ui',
|
|
193
|
+
'skills/devlyn-queue',
|
|
194
|
+
'skills/devlyn:queue',
|
|
195
|
+
'skills/devlyn-resolve',
|
|
196
|
+
'skills/devlyn:resolve',
|
|
196
197
|
];
|
|
197
198
|
|
|
198
|
-
function
|
|
199
|
+
function projectDir() {
|
|
199
200
|
try {
|
|
200
|
-
return
|
|
201
|
+
return process.cwd();
|
|
201
202
|
} catch {
|
|
202
203
|
console.error('\n\x1b[33mā Current directory no longer exists.\x1b[0m');
|
|
203
204
|
console.error('\x1b[2m Please cd into a valid directory and try again.\x1b[0m\n');
|
|
@@ -237,14 +238,14 @@ ${v} āāā āāā${p}āāāāāā ${k}āāāā āāā
|
|
|
237
238
|
${v} āāāāāāāā${p}āāāāāāāā${k} āāāāāāā ${v}āāāāāāāā ${p}āāā ${k}āāā āāāāāā${r}
|
|
238
239
|
${g} āāāāāāā āāāāāāāā āāāāā āāāāāāāā āāā āāā āāāāā${r}
|
|
239
240
|
|
|
240
|
-
${COLORS.dim}
|
|
241
|
+
${COLORS.dim} AI Agent Config Toolkit${r}
|
|
241
242
|
${g} v${PKG.version} ${COLORS.dim}Ā· ${k}š© by Nocodecat @ Donut Studio${r}
|
|
242
243
|
`;
|
|
243
244
|
console.log(logo);
|
|
244
245
|
}
|
|
245
246
|
|
|
246
247
|
const OPTIONAL_ADDONS = [
|
|
247
|
-
// Local optional skills (copied
|
|
248
|
+
// Local optional skills (copied into every selected skill root)
|
|
248
249
|
{ name: 'asset-creator', desc: 'AI pixel art game asset pipeline ā generate, chroma-key, catalog', type: 'local' },
|
|
249
250
|
{ name: 'cloudflare-nextjs-setup', desc: 'Cloudflare Workers + Next.js deployment with OpenNext', type: 'local' },
|
|
250
251
|
{ name: 'generate-skill', desc: 'Create well-structured Claude Code skills following Anthropic best practices', type: 'local' },
|
|
@@ -267,10 +268,7 @@ const OPTIONAL_ADDONS = [
|
|
|
267
268
|
{ name: 'anthropics/skills', desc: 'Official Anthropic skill-creator with eval framework and description optimizer', type: 'external' },
|
|
268
269
|
{ name: 'Leonxlnx/taste-skill', desc: 'Premium frontend design skills ā modern layouts, animations, and visual refinement', type: 'external' },
|
|
269
270
|
// MCP servers (installed via claude mcp add)
|
|
270
|
-
|
|
271
|
-
// Install the CLI separately per https://platform.openai.com/docs/codex ā the
|
|
272
|
-
// pair/risk-probe routes fail closed when Codex is required but unavailable.
|
|
273
|
-
{ name: 'playwright', desc: 'Playwright MCP for browser testing ā powers /devlyn-resolve BUILD_GATE browser tier', type: 'mcp', command: 'npx -y @playwright/mcp@latest' },
|
|
271
|
+
{ name: 'playwright', desc: 'Playwright MCP for browser testing', type: 'mcp', command: 'npx -y @playwright/mcp@latest' },
|
|
274
272
|
];
|
|
275
273
|
|
|
276
274
|
function log(msg, color = 'reset') {
|
|
@@ -316,23 +314,8 @@ function listContents() {
|
|
|
316
314
|
showLogo();
|
|
317
315
|
log('ā'.repeat(44), 'dim');
|
|
318
316
|
|
|
319
|
-
const templatesDir = path.join(CONFIG_SOURCE, 'templates');
|
|
320
317
|
const skillsDir = path.join(CONFIG_SOURCE, 'skills');
|
|
321
318
|
|
|
322
|
-
// List templates
|
|
323
|
-
if (fs.existsSync(templatesDir)) {
|
|
324
|
-
const templates = fs.readdirSync(templatesDir).filter((f) => f.endsWith('.md'));
|
|
325
|
-
if (templates.length > 0) {
|
|
326
|
-
log('\nš Templates:', 'blue');
|
|
327
|
-
templates.forEach((file) => {
|
|
328
|
-
const name = file.replace('.md', '');
|
|
329
|
-
const desc = getDescription(path.join(templatesDir, file));
|
|
330
|
-
log(` ${COLORS.green}${name}${COLORS.reset}`);
|
|
331
|
-
if (desc) log(` ${COLORS.dim}${desc}${COLORS.reset}`);
|
|
332
|
-
});
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
|
|
336
319
|
// List skills
|
|
337
320
|
if (fs.existsSync(skillsDir)) {
|
|
338
321
|
const skills = fs.readdirSync(skillsDir).filter((d) => {
|
|
@@ -362,18 +345,11 @@ function listContents() {
|
|
|
362
345
|
log('');
|
|
363
346
|
}
|
|
364
347
|
|
|
365
|
-
|
|
348
|
+
// Old and retired skills in one skill root; nothing outside it.
|
|
349
|
+
function cleanupDeprecated(skillsDir) {
|
|
366
350
|
let removed = 0;
|
|
367
351
|
const deprecated = DEPRECATED_DIRS.flatMap((relPath) =>
|
|
368
|
-
legacySkillPaths(
|
|
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
|
-
}
|
|
352
|
+
legacySkillPaths(skillsDir, path.basename(relPath)).map((fullPath) => [relPath, fullPath]));
|
|
377
353
|
for (const [relPath, fullPath] of deprecated) {
|
|
378
354
|
if (fs.existsSync(fullPath)) {
|
|
379
355
|
fs.rmSync(fullPath, { recursive: true });
|
|
@@ -382,7 +358,7 @@ function cleanupDeprecated(targetDir) {
|
|
|
382
358
|
}
|
|
383
359
|
}
|
|
384
360
|
for (const [name, hashes] of Object.entries(RETIRED_SKILL_MD_SHA256)) {
|
|
385
|
-
const fullPath = path.join(
|
|
361
|
+
const fullPath = path.join(skillsDir, name);
|
|
386
362
|
if (isShippedCopy(fullPath, hashes)) {
|
|
387
363
|
fs.rmSync(fullPath, { recursive: true });
|
|
388
364
|
log(` ā skills/${name}/ (removed)`, 'dim');
|
|
@@ -413,8 +389,6 @@ function copyRecursive(src, dest, baseDir) {
|
|
|
413
389
|
const stats = fs.statSync(src);
|
|
414
390
|
|
|
415
391
|
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
392
|
if (!fs.existsSync(dest)) {
|
|
419
393
|
fs.mkdirSync(dest, { recursive: true });
|
|
420
394
|
}
|
|
@@ -475,38 +449,6 @@ function writeInstallMarker(skillsDir) {
|
|
|
475
449
|
}
|
|
476
450
|
}
|
|
477
451
|
|
|
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
452
|
function multiSelect(items, preselectedIndices = []) {
|
|
511
453
|
return new Promise((resolve) => {
|
|
512
454
|
const selected = new Set(preselectedIndices.filter((i) => i >= 0 && i < items.length));
|
|
@@ -527,10 +469,10 @@ function multiSelect(items, preselectedIndices = []) {
|
|
|
527
469
|
const checkbox = selected.has(i) ? `${COLORS.green}ā${COLORS.reset}` : `${COLORS.dim}ā${COLORS.reset}`;
|
|
528
470
|
const pointer = i === cursor ? `${COLORS.cyan}āÆ${COLORS.reset}` : ' ';
|
|
529
471
|
const name = i === cursor ? `${COLORS.cyan}${item.name}${COLORS.reset}` : item.name;
|
|
530
|
-
const tagLabel = item.type === 'mcp' ? 'mcp' : item.type === 'local' ? 'skill' :
|
|
531
|
-
const tagColor = item.type === 'mcp' ? COLORS.green : item.type === 'local' ? COLORS.magenta :
|
|
472
|
+
const tagLabel = item.type === 'mcp' ? 'mcp' : item.type === 'local' ? 'skill' : 'pack';
|
|
473
|
+
const tagColor = item.type === 'mcp' ? COLORS.green : item.type === 'local' ? COLORS.magenta : COLORS.cyan;
|
|
532
474
|
const tag = `${tagColor}${tagLabel}${COLORS.reset}`;
|
|
533
|
-
console.log(`${pointer} ${checkbox} ${name} ${COLORS.dim}[${tag}${COLORS.dim}]${COLORS.reset}`);
|
|
475
|
+
console.log(`${pointer} ${checkbox} ${name}${item.type ? ` ${COLORS.dim}[${tag}${COLORS.dim}]${COLORS.reset}` : ''}`);
|
|
534
476
|
console.log(` ${COLORS.dim}${item.desc}${COLORS.reset}`);
|
|
535
477
|
});
|
|
536
478
|
};
|
|
@@ -600,26 +542,48 @@ function multiSelect(items, preselectedIndices = []) {
|
|
|
600
542
|
});
|
|
601
543
|
}
|
|
602
544
|
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
545
|
+
// One of `items` (āā move, Enter confirms); resolves to the chosen index.
|
|
546
|
+
function singleSelect(items, initial) {
|
|
547
|
+
return new Promise((resolve) => {
|
|
548
|
+
let cursor = initial;
|
|
549
|
+
let drawn = false;
|
|
550
|
+
const render = () => {
|
|
551
|
+
if (drawn) process.stdout.write(`\x1b[${items.length + 2}A\x1b[0J`);
|
|
552
|
+
drawn = true;
|
|
553
|
+
console.log(`${COLORS.dim}(āā navigate, enter confirm)${COLORS.reset}\n`);
|
|
554
|
+
items.forEach((item, i) => console.log(i === cursor ? `${COLORS.cyan}⯠${item}${COLORS.reset}` : ` ${item}`));
|
|
555
|
+
};
|
|
556
|
+
render();
|
|
557
|
+
process.stdin.setRawMode(true);
|
|
558
|
+
process.stdin.resume();
|
|
559
|
+
process.stdin.setEncoding('utf8');
|
|
560
|
+
const onKeypress = (key) => {
|
|
561
|
+
if (key === '\u0003') {
|
|
562
|
+
process.stdin.setRawMode(false);
|
|
563
|
+
process.exit();
|
|
564
|
+
}
|
|
565
|
+
if (key === '\r' || key === '\n') {
|
|
566
|
+
process.stdin.setRawMode(false);
|
|
567
|
+
process.stdin.removeListener('data', onKeypress);
|
|
568
|
+
process.stdin.pause();
|
|
569
|
+
console.log('');
|
|
570
|
+
resolve(cursor);
|
|
571
|
+
} else if (['\x1b[A', 'k', '\x1b[B', 'j'].includes(key)) {
|
|
572
|
+
cursor = (cursor + (key === '\x1b[A' || key === 'k' ? items.length - 1 : 1)) % items.length;
|
|
573
|
+
render();
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
process.stdin.on('data', onKeypress);
|
|
577
|
+
});
|
|
578
|
+
}
|
|
608
579
|
|
|
609
|
-
|
|
580
|
+
function installLocalSkill(skillName, roots) {
|
|
581
|
+
if (!fs.existsSync(path.join(OPTIONAL_SKILLS_SOURCE, skillName))) {
|
|
610
582
|
log(` ā ļø Skill "${skillName}" not found`, 'yellow');
|
|
611
583
|
return false;
|
|
612
584
|
}
|
|
613
|
-
|
|
614
585
|
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);
|
|
586
|
+
for (const root of roots) installOptionalSkillInto(root, skillName);
|
|
623
587
|
return true;
|
|
624
588
|
}
|
|
625
589
|
|
|
@@ -673,9 +637,9 @@ function installSkillPack(packName) {
|
|
|
673
637
|
}
|
|
674
638
|
}
|
|
675
639
|
|
|
676
|
-
function installAddon(addon) {
|
|
640
|
+
function installAddon(addon, roots) {
|
|
677
641
|
if (addon.type === 'local') {
|
|
678
|
-
return installLocalSkill(addon.name);
|
|
642
|
+
return installLocalSkill(addon.name, roots);
|
|
679
643
|
}
|
|
680
644
|
if (addon.type === 'mcp') {
|
|
681
645
|
return installMcpServer(addon.name, addon.command);
|
|
@@ -683,168 +647,33 @@ function installAddon(addon) {
|
|
|
683
647
|
return installSkillPack(addon.name);
|
|
684
648
|
}
|
|
685
649
|
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
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
|
-
|
|
650
|
+
// The core skills into one skill root, replacing older copies and retired skills, then the
|
|
651
|
+
// marker that records a complete install.
|
|
652
|
+
function installCoreSkills(skillsDir) {
|
|
704
653
|
const sourceSkillsDir = path.join(CONFIG_SOURCE, 'skills');
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
const removed = cleanupDeprecated(path.dirname(cli.skillsDir));
|
|
654
|
+
log(`\nš Installing devlyn skills to ${skillsDir.replace(os.homedir(), '~')}`, 'green');
|
|
655
|
+
fs.mkdirSync(skillsDir, { recursive: true });
|
|
656
|
+
clearInstallMarker(skillsDir);
|
|
657
|
+
const removed = cleanupDeprecated(skillsDir);
|
|
711
658
|
if (removed > 0) {
|
|
712
659
|
log(`\nš§¹ Cleaned up ${removed} deprecated file${removed > 1 ? 's' : ''}`, 'yellow');
|
|
713
660
|
}
|
|
714
|
-
|
|
715
|
-
let copied = 0;
|
|
716
|
-
for (const skillName of cli.skillsToInstall) {
|
|
661
|
+
for (const skillName of DEVLYN_CORE_SKILLS) {
|
|
717
662
|
const src = path.join(sourceSkillsDir, skillName);
|
|
718
|
-
const dest = path.join(
|
|
663
|
+
const dest = path.join(skillsDir, skillName);
|
|
719
664
|
if (!fs.existsSync(src)) continue;
|
|
720
|
-
// Full replace
|
|
721
|
-
|
|
722
|
-
|
|
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');
|
|
728
|
-
}
|
|
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++;
|
|
665
|
+
// Full replace: copyRecursive is an overlay, so stale files would otherwise persist.
|
|
666
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
667
|
+
copyRecursive(src, dest, skillsDir);
|
|
786
668
|
}
|
|
787
|
-
|
|
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);
|
|
669
|
+
assertCompleteSkillInstall(sourceSkillsDir, skillsDir, DEVLYN_CORE_SKILLS);
|
|
842
670
|
retireRenamedSkills(skillsDir);
|
|
843
671
|
writeInstallMarker(skillsDir);
|
|
672
|
+
}
|
|
844
673
|
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
const
|
|
674
|
+
// Keep installer-managed pipeline state and install metadata out of git.
|
|
675
|
+
function ignoreInGit(gitignoreEntries) {
|
|
676
|
+
const gitignorePath = path.join(projectDir(), '.gitignore');
|
|
848
677
|
let gitignoreContent = fs.existsSync(gitignorePath)
|
|
849
678
|
? fs.readFileSync(gitignorePath, 'utf8')
|
|
850
679
|
: '';
|
|
@@ -858,61 +687,106 @@ function installClaudeCore() {
|
|
|
858
687
|
fs.writeFileSync(gitignorePath, gitignoreContent + prefix + header + missingGitignoreEntries.join('\n') + '\n');
|
|
859
688
|
log(` ā .gitignore (added ${missingGitignoreEntries.join(', ')})`, 'dim');
|
|
860
689
|
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// With the Claude target in the same run, an AGENTS.md that links to this project's CLAUDE.md (a
|
|
693
|
+
// common Claude-first setup) gets its devlyn block through CLAUDE.md. Any other link is refused.
|
|
694
|
+
// AGENTS.md that is this project's CLAUDE.md under another name: a symlink to it, or the plain
|
|
695
|
+
// file Git for Windows checks out in a symlink's place when core.symlinks is off (its whole
|
|
696
|
+
// content is the link target). Compared by real path: a link to another hard link of CLAUDE.md
|
|
697
|
+
// would go stale once CLAUDE.md is replaced.
|
|
698
|
+
function agentsMdIsClaudeMd() {
|
|
699
|
+
const agents = path.join(projectDir(), 'AGENTS.md');
|
|
700
|
+
const claude = path.join(projectDir(), 'CLAUDE.md');
|
|
701
|
+
const stat = fs.lstatSync(agents, { throwIfNoEntry: false });
|
|
702
|
+
if (!stat || !fs.lstatSync(claude, { throwIfNoEntry: false })?.isFile()) return false;
|
|
703
|
+
const target = stat.isSymbolicLink() ? agents
|
|
704
|
+
: stat.isFile() && stat.size < 256 ? path.resolve(projectDir(), fs.readFileSync(agents, 'utf8'))
|
|
705
|
+
: null;
|
|
706
|
+
return target !== null && fs.existsSync(target) && fs.realpathSync.native(target) === fs.realpathSync.native(claude);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function installAgentsProject(withClaude) {
|
|
710
|
+
if (!agentsMdIsClaudeMd()) {
|
|
711
|
+
updateInstructions('AGENTS.md');
|
|
712
|
+
} else if (withClaude) {
|
|
713
|
+
log(' ā AGENTS.md is CLAUDE.md here, so it gets the devlyn block written there', 'dim');
|
|
714
|
+
} else {
|
|
715
|
+
throw new InstructionError('AGENTS.md is CLAUDE.md under another name here. Choose CLAUDE.md as well '
|
|
716
|
+
+ '(npx devlyn-cli -y --claude) so the devlyn block is written once, into CLAUDE.md.');
|
|
717
|
+
}
|
|
718
|
+
installCoreSkills(skillRoots('agents', false)[0]);
|
|
719
|
+
ignoreInGit(['.devlyn/', '.agents/skills/.devlyn-install.json']);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// Whether Claude Code reads the devlyn defaults of AGENTS.md through an `@AGENTS.md` import in this CLAUDE.md text.
|
|
723
|
+
// An AGENTS.md that is CLAUDE.md under another name holds CLAUDE.md's own defaults.
|
|
724
|
+
function importsAgentsDefaults(claude) {
|
|
725
|
+
const agents = path.join(projectDir(), 'AGENTS.md');
|
|
726
|
+
return importsAgentsMd(claude) && !agentsMdIsClaudeMd() && fs.statSync(agents, { throwIfNoEntry: false })?.isFile() === true
|
|
727
|
+
&& holdsDevlynDefaults('AGENTS.md', fs.readFileSync(agents, 'utf8'));
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// Claude Code reads AGENTS.md only where no CLAUDE.md exists, or through a CLAUDE.md that imports it. So a new
|
|
731
|
+
// CLAUDE.md imports an existing AGENTS.md, and a CLAUDE.md importing an AGENTS.md that holds devlyn defaults holds no
|
|
732
|
+
// copy of its own, not even one it got while AGENTS.md lacked the block. Only the AGENTS.md target updates those defaults,
|
|
733
|
+
// together with .agents/skills, so the Claude target stops while they need an update.
|
|
734
|
+
function installClaudeInstructions() {
|
|
735
|
+
const file = path.join(projectDir(), 'CLAUDE.md');
|
|
736
|
+
const stat = fs.lstatSync(file, { throwIfNoEntry: false });
|
|
737
|
+
const initial = !stat && fs.statSync(path.join(projectDir(), 'AGENTS.md'), { throwIfNoEntry: false })?.isFile() ? '@AGENTS.md\n' : '';
|
|
738
|
+
const imported = importsAgentsDefaults(stat?.isFile() ? fs.readFileSync(file, 'utf8') : initial);
|
|
739
|
+
if (imported && updateInstructions('AGENTS.md', '', true, false)) {
|
|
740
|
+
throw new InstructionError('Claude Code reads AGENTS.md here, and its devlyn defaults need an update. Codex, omp, Pi and Grok '
|
|
741
|
+
+ 'read them too, with .agents/skills, so choose AGENTS.md as well (npx devlyn-cli -y --claude) to update both together.');
|
|
742
|
+
}
|
|
743
|
+
updateInstructions('CLAUDE.md', initial, !imported);
|
|
744
|
+
if (imported) log(' ā CLAUDE.md imports AGENTS.md, which holds the devlyn block', 'dim');
|
|
745
|
+
}
|
|
861
746
|
|
|
862
|
-
|
|
747
|
+
// Project CLAUDE.md and .claude/: skills and settings.
|
|
748
|
+
function installClaudeCore() {
|
|
749
|
+
const skillsDir = skillRoots('claude', false)[0];
|
|
750
|
+
const targetDir = path.dirname(skillsDir);
|
|
751
|
+
// Read before any write, so settings this install cannot merge leave the Claude target as it was.
|
|
863
752
|
const settingsPath = path.join(targetDir, 'settings.json');
|
|
753
|
+
const unmergeable = (reason) => new InstructionError(`Cannot merge .claude/settings.json: ${reason}. `
|
|
754
|
+
+ 'CLAUDE.md and .claude/ are unchanged; fix the file and rerun the same install command.');
|
|
864
755
|
let settings = {};
|
|
865
756
|
if (fs.existsSync(settingsPath)) {
|
|
866
757
|
try {
|
|
867
|
-
|
|
758
|
+
// Claude Code accepts a leading byte order mark.
|
|
759
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8').replace(/^\uFEFF/, ''));
|
|
868
760
|
} catch (error) {
|
|
869
|
-
throw
|
|
761
|
+
throw unmergeable(error.message);
|
|
870
762
|
}
|
|
871
763
|
}
|
|
872
764
|
if (!settings || typeof settings !== 'object' || Array.isArray(settings)) {
|
|
873
|
-
throw
|
|
765
|
+
throw unmergeable('root must be a JSON object');
|
|
874
766
|
}
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
settings.env = {};
|
|
879
|
-
settingsChanged = true;
|
|
767
|
+
if (Object.prototype.hasOwnProperty.call(settings, 'env')
|
|
768
|
+
&& (!settings.env || typeof settings.env !== 'object' || Array.isArray(settings.env))) {
|
|
769
|
+
throw unmergeable('env must be a JSON object');
|
|
880
770
|
}
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
}
|
|
889
|
-
if (!settings.permissions || typeof settings.permissions !== 'object' || Array.isArray(settings.permissions)) {
|
|
890
|
-
throw new Error('Cannot merge .claude/settings.json: permissions must be a JSON object');
|
|
771
|
+
installClaudeInstructions();
|
|
772
|
+
for (const relPath of DEPRECATED_FILES) {
|
|
773
|
+
const fullPath = path.join(targetDir, relPath);
|
|
774
|
+
if (fs.existsSync(fullPath)) {
|
|
775
|
+
fs.unlinkSync(fullPath);
|
|
776
|
+
log(` ā ${relPath} (deprecated)`, 'dim');
|
|
777
|
+
}
|
|
891
778
|
}
|
|
892
|
-
|
|
893
|
-
|
|
779
|
+
installCoreSkills(skillsDir);
|
|
780
|
+
ignoreInGit(['.devlyn/', '.claude/skills/.devlyn-install.json']);
|
|
781
|
+
|
|
782
|
+
// One-hour foreground Bash calls and one-hour prompt caching serve a drain host that waits in the foreground.
|
|
783
|
+
let settingsChanged = false;
|
|
784
|
+
if (!Object.prototype.hasOwnProperty.call(settings, 'env')) {
|
|
785
|
+
settings.env = {};
|
|
894
786
|
settingsChanged = true;
|
|
895
787
|
}
|
|
896
|
-
if (!
|
|
897
|
-
|
|
898
|
-
}
|
|
899
|
-
const pipelinePermissions = [
|
|
900
|
-
'Write(.devlyn/**)',
|
|
901
|
-
'Edit(.devlyn/**)',
|
|
902
|
-
'Bash(git add *)',
|
|
903
|
-
'Bash(git commit *)',
|
|
904
|
-
'Bash(git diff *)',
|
|
905
|
-
'Bash(git status *)',
|
|
906
|
-
'Bash(git log *)',
|
|
907
|
-
];
|
|
908
|
-
for (const perm of pipelinePermissions) {
|
|
909
|
-
if (!settings.permissions.allow.includes(perm)) {
|
|
910
|
-
settings.permissions.allow.push(perm);
|
|
911
|
-
settingsChanged = true;
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
if (!settings.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS) {
|
|
915
|
-
settings.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS = '1';
|
|
788
|
+
if (!settings.env.ENABLE_PROMPT_CACHING_1H) {
|
|
789
|
+
settings.env.ENABLE_PROMPT_CACHING_1H = 'true';
|
|
916
790
|
settingsChanged = true;
|
|
917
791
|
}
|
|
918
792
|
const bashMaxTimeoutMs = Number.parseInt(settings.env.BASH_MAX_TIMEOUT_MS, 10);
|
|
@@ -920,51 +794,74 @@ function installClaudeCore() {
|
|
|
920
794
|
settings.env.BASH_MAX_TIMEOUT_MS = '3600000';
|
|
921
795
|
settingsChanged = true;
|
|
922
796
|
}
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
settings.
|
|
932
|
-
settingsChanged = true;
|
|
933
|
-
}
|
|
934
|
-
if (!Array.isArray(settings.hooks.Stop)) {
|
|
935
|
-
throw new Error('Cannot merge .claude/settings.json: hooks.Stop must be an array');
|
|
936
|
-
}
|
|
937
|
-
const stopHookCommand = 'python3 "$CLAUDE_PROJECT_DIR/.claude/skills/_shared/resolve-stop-hook.py"';
|
|
938
|
-
const stopHookInstalled = settings.hooks.Stop.some((entry) => (
|
|
939
|
-
entry && Array.isArray(entry.hooks) && entry.hooks.some((hook) => (
|
|
940
|
-
hook && hook.type === 'command' && hook.command === stopHookCommand
|
|
941
|
-
))
|
|
942
|
-
));
|
|
943
|
-
if (!stopHookInstalled) {
|
|
944
|
-
settings.hooks.Stop.push({
|
|
945
|
-
hooks: [{ type: 'command', command: stopHookCommand, timeout: 30 }],
|
|
946
|
-
});
|
|
797
|
+
// Remove exactly the retired Stop hook command, and its entry only when nothing else is left in it.
|
|
798
|
+
const retiredHook = (hook) => hook?.type === 'command' && hook.command === RETIRED_STOP_HOOK;
|
|
799
|
+
const stop = Array.isArray(settings.hooks?.Stop) ? settings.hooks.Stop : [];
|
|
800
|
+
for (let i = stop.length - 1; i >= 0; i--) {
|
|
801
|
+
const hooks = stop[i]?.hooks;
|
|
802
|
+
if (!Array.isArray(hooks) || !hooks.some(retiredHook)) continue;
|
|
803
|
+
stop[i].hooks = hooks.filter((hook) => !retiredHook(hook));
|
|
804
|
+
if (stop[i].hooks.length === 0 && Object.keys(stop[i]).length === 1) stop.splice(i, 1);
|
|
805
|
+
log(' ā settings.json Stop hook of the retired pipeline (removed)', 'dim');
|
|
947
806
|
settingsChanged = true;
|
|
948
807
|
}
|
|
949
808
|
if (settingsChanged) {
|
|
950
809
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
951
|
-
log(' ā settings.json
|
|
810
|
+
log(' ā settings.json', 'dim');
|
|
952
811
|
}
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
812
|
+
// Their ownership cannot be established, so the other retired settings stay; name the ones present.
|
|
813
|
+
const retired = [
|
|
814
|
+
...RETIRED_ENV.filter((key) => Object.prototype.hasOwnProperty.call(settings.env, key)),
|
|
815
|
+
...RETIRED_PERMISSIONS.filter((rule) => Array.isArray(settings.permissions?.allow) && settings.permissions.allow.includes(rule)),
|
|
816
|
+
];
|
|
817
|
+
if (retired.length > 0) {
|
|
818
|
+
log(` Retired devlyn settings may remain in .claude/settings.json: ${retired.join(', ')} ā delete those you did not add yourself.`, 'yellow');
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// Installs the targets in one scope; returns the skill roots written.
|
|
823
|
+
function install(targets, global) {
|
|
824
|
+
// In the home folder CLAUDE.md, AGENTS.md (agents read a parent folder's too) and
|
|
825
|
+
// .claude/settings.json would apply to every project. Same folder by identity, so a link or
|
|
826
|
+
// another spelling of the path is caught too.
|
|
827
|
+
const home = fs.statSync(os.homedir(), { bigint: true, throwIfNoEntry: false });
|
|
828
|
+
const here = fs.statSync(projectDir(), { bigint: true });
|
|
829
|
+
if (!global && home?.dev === here.dev && home?.ino === here.ino) {
|
|
830
|
+
throw new InstructionError('This project is your home folder, so its CLAUDE.md, AGENTS.md and .claude/settings.json '
|
|
831
|
+
+ 'would apply to every project. Run from a project folder, or use --global for skills only.');
|
|
832
|
+
}
|
|
833
|
+
const roots = targets.flatMap((target) => skillRoots(target, global));
|
|
834
|
+
// AGENTS.md first, whatever order the menu gave: a new CLAUDE.md holds the block only when AGENTS.md does not.
|
|
835
|
+
for (const target of ['agents', 'claude'].filter((name) => targets.includes(name))) {
|
|
836
|
+
if (global) skillRoots(target, true).forEach((root) => installCoreSkills(root));
|
|
837
|
+
else if (target === 'agents') installAgentsProject(targets.includes('claude'));
|
|
838
|
+
else installClaudeCore();
|
|
839
|
+
}
|
|
840
|
+
log(`\nā
devlyn ${PKG.version} installed`, 'green');
|
|
841
|
+
noticeGlobalDrift(roots);
|
|
842
|
+
return roots;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// A devlyn install in a user root this run does not refresh drifts and can shadow the
|
|
846
|
+
// project's copy; name it without touching it.
|
|
847
|
+
function noticeGlobalDrift(installed) {
|
|
848
|
+
for (const dir of ['.agents', '.codex', '.claude', '.grok']) {
|
|
849
|
+
const root = path.join(os.homedir(), dir, 'skills');
|
|
850
|
+
const marker = path.join(root, DEVLYN_INSTALL_MARKER);
|
|
851
|
+
if (installed.includes(root) || !fs.existsSync(marker)) continue;
|
|
852
|
+
let version;
|
|
853
|
+
try {
|
|
854
|
+
version = JSON.parse(fs.readFileSync(marker, 'utf8')).version;
|
|
855
|
+
} catch (error) {
|
|
856
|
+
version = `(unreadable marker: ${error.message})`;
|
|
857
|
+
}
|
|
858
|
+
// 4.0's Grok root; Grok now reads ~/.agents/skills, so --global never refreshes it.
|
|
859
|
+
const advice = dir === '.grok' ? 'delete it' : 'refresh it with --global, or delete it';
|
|
860
|
+
log(`Global devlyn ${version} in ${root.replace(os.homedir(), '~')} ā ${advice}.`, 'yellow');
|
|
962
861
|
}
|
|
963
|
-
|
|
964
|
-
log('\nā
Claude Code config installed!', 'green');
|
|
965
862
|
}
|
|
966
863
|
|
|
967
|
-
async function init(
|
|
864
|
+
async function init({ yes, claude, global }) {
|
|
968
865
|
showLogo();
|
|
969
866
|
log('ā'.repeat(44), 'dim');
|
|
970
867
|
|
|
@@ -973,53 +870,47 @@ async function init(skipPrompts = false) {
|
|
|
973
870
|
process.exit(1);
|
|
974
871
|
}
|
|
975
872
|
|
|
976
|
-
//
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
installClaudeCore();
|
|
873
|
+
// Without prompts: AGENTS plus every target already installed in the chosen scope.
|
|
874
|
+
if (yes || !process.stdin.isTTY) {
|
|
875
|
+
const targets = ['agents', ...(claude || hasDevlynClaude(global) ? ['claude'] : [])];
|
|
876
|
+
install(targets, global);
|
|
981
877
|
log('\nš” Add optional addons later: run `npx devlyn-cli` without -y', 'dim');
|
|
982
|
-
|
|
878
|
+
const hints = [
|
|
879
|
+
...(targets.includes('claude') ? [] : [`${global ? '~/.claude/skills' : 'CLAUDE.md + .claude/'} for Claude Code: add --claude`]),
|
|
880
|
+
...(global ? [] : ['every project on this machine: add --global']),
|
|
881
|
+
];
|
|
882
|
+
if (hints.length > 0) log(` ${hints.join(' Ā· ')}`, 'dim');
|
|
983
883
|
log(`\n${COLORS.dim} Enjoying devlyn? Star it on GitHub ā it helps others find it:${COLORS.reset}`);
|
|
984
884
|
log(` ${COLORS.purple}ā https://github.com/fysoul17/devlyn-cli${COLORS.reset}\n`);
|
|
985
885
|
return;
|
|
986
886
|
}
|
|
987
887
|
|
|
988
|
-
|
|
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');
|
|
888
|
+
log('\nšÆ What to install:\n', 'blue');
|
|
993
889
|
const targetOptions = [
|
|
994
|
-
{ key: '
|
|
995
|
-
|
|
890
|
+
{ key: 'agents', name: 'AGENTS.md ā Codex Ā· omp Ā· Pi Ā· Grok', desc: 'AGENTS.md + .agents/skills' },
|
|
891
|
+
{ key: 'claude', name: 'CLAUDE.md ā Claude Code', desc: 'CLAUDE.md + .claude/ (skills, settings)' },
|
|
996
892
|
];
|
|
997
|
-
const preselected =
|
|
998
|
-
const
|
|
893
|
+
const preselected = claude || hasDevlynClaude(false) || (global && hasDevlynClaude(true)) ? [0, 1] : [0];
|
|
894
|
+
const targets = (await multiSelect(targetOptions, preselected)).map((option) => option.key);
|
|
999
895
|
|
|
1000
|
-
if (
|
|
1001
|
-
log('\nš”
|
|
1002
|
-
log(' Run `npx devlyn-cli` again and pick at least one
|
|
896
|
+
if (targets.length === 0) {
|
|
897
|
+
log('\nš” Nothing selected ā nothing installed.', 'yellow');
|
|
898
|
+
log(' Run `npx devlyn-cli` again and pick at least one.\n', 'dim');
|
|
1003
899
|
return;
|
|
1004
900
|
}
|
|
1005
901
|
|
|
1006
|
-
|
|
1007
|
-
|
|
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
|
-
}
|
|
902
|
+
log('š Where:\n', 'blue');
|
|
903
|
+
const scope = await singleSelect(['This project', 'Global ā every project on this machine'], global ? 1 : 0);
|
|
904
|
+
const roots = install(targets, scope === 1);
|
|
1014
905
|
|
|
1015
|
-
// Ask about optional addons (local skills + external packs)
|
|
906
|
+
// Ask about optional addons (local skills + external packs; MCP servers belong to Claude Code)
|
|
1016
907
|
log('\nš Optional skills & packs:\n', 'blue');
|
|
1017
908
|
|
|
1018
|
-
const selectedAddons = await multiSelect(OPTIONAL_ADDONS);
|
|
909
|
+
const selectedAddons = await multiSelect(OPTIONAL_ADDONS.filter((addon) => addon.type !== 'mcp' || targets.includes('claude')));
|
|
1019
910
|
|
|
1020
911
|
if (selectedAddons.length > 0) {
|
|
1021
912
|
for (const addon of selectedAddons) {
|
|
1022
|
-
installAddon(addon);
|
|
913
|
+
installAddon(addon, roots);
|
|
1023
914
|
}
|
|
1024
915
|
} else {
|
|
1025
916
|
log('š” No optional addons selected', 'dim');
|
|
@@ -1035,12 +926,12 @@ async function init(skipPrompts = false) {
|
|
|
1035
926
|
function showHelp() {
|
|
1036
927
|
showLogo();
|
|
1037
928
|
log('Usage:', 'green');
|
|
1038
|
-
log(' npx devlyn-cli
|
|
1039
|
-
log(' npx devlyn-cli
|
|
1040
|
-
log(' npx devlyn-cli -y
|
|
1041
|
-
log(' npx devlyn-cli
|
|
1042
|
-
log(' npx devlyn-cli
|
|
1043
|
-
log(' npx devlyn-cli --help
|
|
929
|
+
log(' npx devlyn-cli Install/update devlyn: choose what (AGENTS.md, CLAUDE.md) and where (this project or global)');
|
|
930
|
+
log(' npx devlyn-cli -y Without prompts: AGENTS.md + .agents/skills, plus CLAUDE.md + .claude/ if this project has them');
|
|
931
|
+
log(' npx devlyn-cli -y --claude Also install CLAUDE.md + .claude/ for Claude Code');
|
|
932
|
+
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)');
|
|
933
|
+
log(' npx devlyn-cli list List available skills');
|
|
934
|
+
log(' npx devlyn-cli --help Show this help\n');
|
|
1044
935
|
log('Optional skills (select during install):', 'green');
|
|
1045
936
|
OPTIONAL_ADDONS.filter((a) => a.type === 'local').forEach((skill) => {
|
|
1046
937
|
log(` ${skill.name} ${COLORS.dim}${skill.desc}${COLORS.reset}`);
|
|
@@ -1053,16 +944,14 @@ function showHelp() {
|
|
|
1053
944
|
OPTIONAL_ADDONS.filter((a) => a.type === 'mcp').forEach((mcp) => {
|
|
1054
945
|
log(` claude mcp add ${mcp.name} -- ${mcp.command} ${COLORS.dim}${mcp.desc}${COLORS.reset}`);
|
|
1055
946
|
});
|
|
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
947
|
log('');
|
|
1061
948
|
}
|
|
1062
949
|
|
|
1063
950
|
// Main
|
|
1064
951
|
const args = process.argv.slice(2);
|
|
1065
952
|
|
|
953
|
+
const INSTALL_FLAGS = ['-y', '--yes', '--claude', '--global'];
|
|
954
|
+
|
|
1066
955
|
async function main() {
|
|
1067
956
|
const command = args[0];
|
|
1068
957
|
|
|
@@ -1071,56 +960,25 @@ switch (command) {
|
|
|
1071
960
|
case '-h':
|
|
1072
961
|
showHelp();
|
|
1073
962
|
break;
|
|
1074
|
-
case '-y':
|
|
1075
|
-
case '--yes':
|
|
1076
|
-
await init(true);
|
|
1077
|
-
break;
|
|
1078
963
|
case 'list':
|
|
1079
964
|
case 'ls':
|
|
1080
965
|
listContents();
|
|
1081
966
|
break;
|
|
1082
|
-
case 'agents':
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
log(
|
|
1092
|
-
|
|
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');
|
|
967
|
+
case 'agents':
|
|
968
|
+
console.error('`npx devlyn-cli agents` was removed in 4.1.0. Run `npx devlyn-cli` to choose what and where,');
|
|
969
|
+
console.error('or `npx devlyn-cli -y [--claude] [--global]` without prompts.');
|
|
970
|
+
process.exitCode = 1;
|
|
971
|
+
break;
|
|
972
|
+
default: {
|
|
973
|
+
const flags = command === 'init' ? args.slice(1) : args;
|
|
974
|
+
const unknown = flags.find((flag) => !INSTALL_FLAGS.includes(flag));
|
|
975
|
+
if (unknown !== undefined) {
|
|
976
|
+
log(`Unknown command: ${unknown}`, 'yellow');
|
|
977
|
+
showHelp();
|
|
1099
978
|
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
979
|
}
|
|
1113
|
-
|
|
1114
|
-
break;
|
|
980
|
+
await init({ yes: flags.includes('-y') || flags.includes('--yes'), claude: flags.includes('--claude'), global: flags.includes('--global') });
|
|
1115
981
|
}
|
|
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
982
|
}
|
|
1125
983
|
}
|
|
1126
984
|
|