baldart 3.6.4 → 3.7.1
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/CHANGELOG.md +42 -0
- package/VERSION +1 -1
- package/framework/templates/baldart.config.template.yml +14 -0
- package/package.json +1 -1
- package/src/commands/add.js +7 -2
- package/src/commands/configure.js +31 -0
- package/src/commands/migrate.js +13 -1
- package/src/commands/update.js +58 -11
- package/src/utils/symlinks.js +137 -75
- package/src/utils/tool-adapters/claude.js +41 -0
- package/src/utils/tool-adapters/codex.js +51 -0
- package/src/utils/tool-adapters/index.js +44 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,48 @@ All notable changes to BALDART will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [3.7.1] - 2026-05-22
|
|
9
|
+
|
|
10
|
+
`baldart update` now actively migrates pre-v3.7.0 configs to the new multi-tool layout instead of silently leaving them on `[claude]` only.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **`src/commands/update.js` legacy-config migration** — after the subtree pull, if `baldart.config.yml` exists but `tools.enabled` is missing/empty, the CLI now:
|
|
15
|
+
1. Detects the legacy state.
|
|
16
|
+
2. Surfaces the new capability and shows the autodetected tool list (Claude always, plus Codex if `~/.codex/` exists).
|
|
17
|
+
3. Asks: *"Backfill `tools.enabled: [claude, codex]` into baldart.config.yml?"*.
|
|
18
|
+
4. On accept: writes the array into the config AND re-runs `mergeSkills` for the new tools so `.agents/skills/` is populated immediately — not deferred to the next update.
|
|
19
|
+
|
|
20
|
+
Without this, a pre-v3.7.0 install upgrading to v3.7.x would never get the Codex skill dir populated, because the per-tool dispatch falls back to `['claude']` when `tools.enabled` is absent.
|
|
21
|
+
|
|
22
|
+
## [3.7.0] - 2026-05-22
|
|
23
|
+
|
|
24
|
+
BALDART is now AI-tool agnostic. Every framework skill is installed into both Claude Code (`.claude/skills/`) AND OpenAI Codex CLI (`.agents/skills/`) when both are enabled — same source, two symlinks, zero duplication. Codex skill format is structurally identical to Claude's (`SKILL.md` with `name` + `description` frontmatter + optional `scripts/`/`references/`/`assets/`), so the same file works for both runtimes.
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- **`src/utils/tool-adapters/`** — new adapter registry (mirrors the existing `routine-adapters/` pattern). Each adapter declares the directory its tool reads skills from, plus capability flags (subagents, slash commands, hooks). Currently: `claude.js`, `codex.js`. Adding Cursor / Aider / Cline later is a one-file addition.
|
|
29
|
+
- **`tools.enabled` in `baldart.config.yml`** — array of enabled tool names. Default: `[claude]`, plus `codex` if `~/.codex/` is detected on the user's machine. Honored by `add`, `update`, `migrate`, and `verifySymlinks`.
|
|
30
|
+
- **`baldart configure` AI-tools section** — prompts per available tool with the autodetected default. Refuses to disable Claude (the framework's primary target).
|
|
31
|
+
- **`AUTODETECTED` summary** now shows `AI tools: claude, codex`.
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- **`SymlinkUtils.mergeSkills({ tools })`** — runs the per-item merge once per enabled tool, resolving each tool's target directory via its adapter. The same source under `.framework/framework/.claude/skills/<skill>/` is linked into every enabled tool's expected location.
|
|
36
|
+
- **`SymlinkUtils.createAllSymlinks({ tools })`** — accepts the tool list and passes it through to `mergeSkills`.
|
|
37
|
+
- **`SymlinkUtils.verifySymlinks()`** — reads `tools.enabled` from `baldart.config.yml` and validates each tool's skills dir separately. Output lines are now tagged `[Claude Code]` / `[OpenAI Codex CLI]`.
|
|
38
|
+
- **Per-item skill merge resilience** — `_mergeSkillsForTool` now detects broken symlinks via `fs.lstatSync` and re-links them silently instead of crashing on EEXIST (same fix-pattern as v3.5.1, now applied per-tool).
|
|
39
|
+
|
|
40
|
+
### Why it matters
|
|
41
|
+
|
|
42
|
+
Until now BALDART was Claude-Code-only by construction. A consumer using Codex saw nothing — Codex's discovery walks `.agents/skills/`, and the framework had no awareness of that path. v3.7.0 makes the install universal: declare `tools.enabled: [claude, codex]` (or both, autodetected) and Codex picks up the same 24 skills, indexed by its own discovery, with `AGENTS.md` at the root continuing to work for both as it did before. Adding more tools later (Cursor `.cursor/rules/`, Aider `CONVENTIONS.md`, Cline, etc.) only requires writing a new adapter — the rest of the install pipeline is already tool-pluggable.
|
|
43
|
+
|
|
44
|
+
### Out of scope (Codex-specific limitations)
|
|
45
|
+
|
|
46
|
+
- **Subagents** (Claude's `.claude/agents/<name>.md`) have no Codex equivalent. They remain Claude-only. AGENTS.md sections cover the cross-tool coordination protocol.
|
|
47
|
+
- **Slash commands** (Claude's `.claude/commands/`) — Codex deprecated custom prompts in favor of skills, so `/commandX` patterns stay Claude-only. If you want a workflow callable from Codex, author it as a skill.
|
|
48
|
+
- **Hooks** (Claude's `.claude/hooks/`) — Codex has no equivalent hook system. `framework-edit-gate` remains a Claude-only safety net.
|
|
49
|
+
|
|
8
50
|
## [3.6.4] - 2026-05-22
|
|
9
51
|
|
|
10
52
|
Two configure-flow fixes surfaced by a real-world `mayo` install audit.
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.7.1
|
|
@@ -111,3 +111,17 @@ features:
|
|
|
111
111
|
|
|
112
112
|
# LLM-wiki overlay (paths.wiki_dir + capture/wiki-curator loop).
|
|
113
113
|
has_wiki_overlay: false
|
|
114
|
+
|
|
115
|
+
# ─── TOOLS ───────────────────────────────────────────────────────────────
|
|
116
|
+
# Which AI CLI tools should the framework target on this machine?
|
|
117
|
+
# Each enabled tool gets its own per-item skill symlinks pointing at the
|
|
118
|
+
# SAME source under `.framework/framework/.claude/skills/<skill>/` (no
|
|
119
|
+
# duplication — Claude reads `.claude/skills/`, Codex reads `.agents/skills/`).
|
|
120
|
+
#
|
|
121
|
+
# Adding "codex" exposes every framework skill to OpenAI Codex CLI as well.
|
|
122
|
+
# The set of supported tools is defined by adapters under
|
|
123
|
+
# `src/utils/tool-adapters/`. Currently: claude, codex.
|
|
124
|
+
tools:
|
|
125
|
+
enabled:
|
|
126
|
+
- claude
|
|
127
|
+
# - codex # uncomment to also install for OpenAI Codex CLI
|
package/package.json
CHANGED
package/src/commands/add.js
CHANGED
|
@@ -119,8 +119,13 @@ async function add(repo, options) {
|
|
|
119
119
|
UI.newline();
|
|
120
120
|
// First-time install: 'safe' mode preserves any pre-existing user
|
|
121
121
|
// customisations without prompting (no destructive backups on fresh
|
|
122
|
-
// install paths). Per-skill merge
|
|
123
|
-
|
|
122
|
+
// install paths). Per-skill merge runs for every enabled tool — by
|
|
123
|
+
// default `['claude']`, plus `'codex'` if `~/.codex/` is present on
|
|
124
|
+
// the user's machine (autodetected). Run `npx baldart configure` to
|
|
125
|
+
// change the selection.
|
|
126
|
+
const toolAdapters = require('../utils/tool-adapters');
|
|
127
|
+
const enabledTools = toolAdapters.defaultEnabled();
|
|
128
|
+
await symlinks.createAllSymlinks({ mode: 'safe', tools: enabledTools });
|
|
124
129
|
|
|
125
130
|
UI.newline();
|
|
126
131
|
symlinks.copyCustomizableFiles();
|
|
@@ -2,6 +2,7 @@ const fs = require('fs');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const yaml = require('js-yaml');
|
|
4
4
|
const UI = require('../utils/ui');
|
|
5
|
+
const toolAdapters = require('../utils/tool-adapters');
|
|
5
6
|
|
|
6
7
|
const CONFIG_FILE = 'baldart.config.yml';
|
|
7
8
|
// The subtree pull copies the entire BALDART repo (which itself has a
|
|
@@ -298,6 +299,9 @@ function detect(cwd = process.cwd()) {
|
|
|
298
299
|
has_prd_workflow: exists('docs/prd'),
|
|
299
300
|
has_wiki_overlay: exists('docs/wiki'),
|
|
300
301
|
},
|
|
302
|
+
tools: {
|
|
303
|
+
enabled: toolAdapters.defaultEnabled(cwd)
|
|
304
|
+
},
|
|
301
305
|
};
|
|
302
306
|
|
|
303
307
|
// Reference walkFirst once so the dead-code linter is happy and the helper
|
|
@@ -394,6 +398,32 @@ async function interactivePrompts(merged, detected) {
|
|
|
394
398
|
? segments.split(',').map((s) => s.trim()).filter(Boolean)
|
|
395
399
|
: [];
|
|
396
400
|
|
|
401
|
+
// ---- AI tools (which CLI tools should this install target?) -----------
|
|
402
|
+
UI.section('AI tools (which tools should consume the framework?)');
|
|
403
|
+
merged.tools = merged.tools || {};
|
|
404
|
+
const allTools = toolAdapters.listAdapters();
|
|
405
|
+
const currentEnabled = (merged.tools.enabled && merged.tools.enabled.length)
|
|
406
|
+
? merged.tools.enabled
|
|
407
|
+
: detected.tools.enabled;
|
|
408
|
+
for (const toolName of allTools) {
|
|
409
|
+
const adapter = toolAdapters.getAdapter(toolName);
|
|
410
|
+
const currentlyOn = currentEnabled.includes(toolName);
|
|
411
|
+
const detectedHint = (toolName !== 'claude' && detected.tools.enabled.includes(toolName))
|
|
412
|
+
? ' (detected on this machine)' : '';
|
|
413
|
+
const want = await UI.confirm(`Install for ${adapter.label}?${detectedHint}`, currentlyOn);
|
|
414
|
+
if (want && !currentEnabled.includes(toolName)) currentEnabled.push(toolName);
|
|
415
|
+
if (!want) {
|
|
416
|
+
const idx = currentEnabled.indexOf(toolName);
|
|
417
|
+
if (idx >= 0) currentEnabled.splice(idx, 1);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
// Claude is the framework's primary target — refuse to disable it entirely.
|
|
421
|
+
if (!currentEnabled.includes('claude')) {
|
|
422
|
+
UI.warning('Claude Code cannot be disabled (primary target). Re-enabling.');
|
|
423
|
+
currentEnabled.unshift('claude');
|
|
424
|
+
}
|
|
425
|
+
merged.tools.enabled = currentEnabled;
|
|
426
|
+
|
|
397
427
|
UI.section('Features (explicit yes/no — option A: always ask)');
|
|
398
428
|
for (const flag of [
|
|
399
429
|
['has_design_system', 'Project has a documented design system?'],
|
|
@@ -541,6 +571,7 @@ async function configure(opts = {}) {
|
|
|
541
571
|
|
|
542
572
|
UI.box('AUTODETECTED', [
|
|
543
573
|
`Brand name: ${detected.identity.brand_name || '—'}`,
|
|
574
|
+
`AI tools: ${detected.tools.enabled.join(', ')}`,
|
|
544
575
|
`Design system: ${detected.features.has_design_system ? 'yes' : 'no'}`,
|
|
545
576
|
` └─ signals: ${dsSignalLabels}`,
|
|
546
577
|
`UI guidelines: ${detected.paths.ui_guidelines || '— (none found)'}`,
|
package/src/commands/migrate.js
CHANGED
|
@@ -115,7 +115,19 @@ async function migrate() {
|
|
|
115
115
|
// --- Step 2: per-item framework skill merge ----------------------------
|
|
116
116
|
|
|
117
117
|
UI.section('Step 2: merge framework skills (per-item)');
|
|
118
|
-
|
|
118
|
+
// Read tools from config so migrate honors the user's per-tool selection.
|
|
119
|
+
let enabledTools = ['claude'];
|
|
120
|
+
try {
|
|
121
|
+
const yaml = require('js-yaml');
|
|
122
|
+
const cfgPath = path.join(cwd, 'baldart.config.yml');
|
|
123
|
+
if (fs.existsSync(cfgPath)) {
|
|
124
|
+
const cfg = yaml.load(fs.readFileSync(cfgPath, 'utf8'));
|
|
125
|
+
if (Array.isArray(cfg?.tools?.enabled) && cfg.tools.enabled.length) {
|
|
126
|
+
enabledTools = cfg.tools.enabled;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
} catch (_) { /* keep default */ }
|
|
130
|
+
const mergeResult = symlinks.mergeSkills({ tools: enabledTools });
|
|
119
131
|
UI.info(`Linked ${mergeResult.linked.length} framework skills, kept ${mergeResult.skipped.length} as-is, ${mergeResult.conflicts.length} conflict(s).`);
|
|
120
132
|
|
|
121
133
|
// --- Step 3: restore user skills from .backup --------------------------
|
package/src/commands/update.js
CHANGED
|
@@ -4,6 +4,21 @@ const UI = require('../utils/ui');
|
|
|
4
4
|
const State = require('../utils/state');
|
|
5
5
|
const Hooks = require('../utils/hooks');
|
|
6
6
|
|
|
7
|
+
function readEnabledTools(cwd = process.cwd()) {
|
|
8
|
+
try {
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const yaml = require('js-yaml');
|
|
12
|
+
const cfgPath = path.join(cwd, 'baldart.config.yml');
|
|
13
|
+
if (!fs.existsSync(cfgPath)) return ['claude'];
|
|
14
|
+
const cfg = yaml.load(fs.readFileSync(cfgPath, 'utf8'));
|
|
15
|
+
if (Array.isArray(cfg?.tools?.enabled) && cfg.tools.enabled.length) {
|
|
16
|
+
return cfg.tools.enabled;
|
|
17
|
+
}
|
|
18
|
+
} catch (_) { /* fall through */ }
|
|
19
|
+
return ['claude'];
|
|
20
|
+
}
|
|
21
|
+
|
|
7
22
|
// Path prefixes BALDART itself writes during install/update. Anything outside
|
|
8
23
|
// these patterns is treated as user-owned and never auto-staged.
|
|
9
24
|
const BALDART_MANAGED_PATTERNS = [
|
|
@@ -297,24 +312,26 @@ async function update(options = {}) {
|
|
|
297
312
|
UI.newline();
|
|
298
313
|
UI.section('Verifying Symlinks');
|
|
299
314
|
|
|
315
|
+
// Read enabled tools from baldart.config.yml so updates respect the
|
|
316
|
+
// user's per-tool selection (Claude only / Claude + Codex / …).
|
|
317
|
+
const enabledTools = readEnabledTools();
|
|
318
|
+
|
|
300
319
|
const symlinkValid = symlinks.verifySymlinks();
|
|
301
320
|
if (!symlinkValid) {
|
|
302
321
|
UI.info('Some symlinks are missing or out of date. The framework will:');
|
|
303
322
|
UI.list([
|
|
304
323
|
'Refuse to overwrite any file/dir you customised (you\'ll be asked first).',
|
|
305
324
|
'Convert legacy .claude/skills/ bulk symlink (v2.0.x) into the new per-item layout.',
|
|
306
|
-
|
|
325
|
+
`Merge framework skills into per-tool skill dirs (${enabledTools.join(', ')}) without touching your personal skills.`
|
|
307
326
|
]);
|
|
308
327
|
const recreate = await UI.confirm('Reconcile symlinks now?', true);
|
|
309
328
|
if (recreate) {
|
|
310
|
-
|
|
311
|
-
// confirmation prompt before being moved to .backup.
|
|
312
|
-
await symlinks.createAllSymlinks({ mode: 'prompt' });
|
|
329
|
+
await symlinks.createAllSymlinks({ mode: 'prompt', tools: enabledTools });
|
|
313
330
|
}
|
|
314
331
|
} else {
|
|
315
|
-
//
|
|
316
|
-
//
|
|
317
|
-
symlinks.mergeSkills();
|
|
332
|
+
// Re-run the per-item skill merge for every enabled tool so newly-shipped
|
|
333
|
+
// skills are linked in each tool's expected directory.
|
|
334
|
+
symlinks.mergeSkills({ tools: enabledTools });
|
|
318
335
|
}
|
|
319
336
|
|
|
320
337
|
// Routines wizard (since v2.1.0) — surfaces routines added in the new framework version
|
|
@@ -353,17 +370,47 @@ async function update(options = {}) {
|
|
|
353
370
|
await configureCmd();
|
|
354
371
|
}
|
|
355
372
|
} else {
|
|
356
|
-
//
|
|
373
|
+
// Schema migration: backfill `tools.enabled` for configs created
|
|
374
|
+
// before v3.7.0 (the multi-tool release). Without this, a legacy
|
|
375
|
+
// install that has Codex on the machine would never get .agents/skills/
|
|
376
|
+
// populated, because update would keep defaulting to ['claude'].
|
|
357
377
|
const yaml = require('js-yaml');
|
|
378
|
+
let needsRewrite = false;
|
|
379
|
+
let cur = {};
|
|
380
|
+
try { cur = yaml.load(fs.readFileSync(configPath, 'utf8')) || {}; } catch (_) { cur = null; }
|
|
381
|
+
|
|
382
|
+
if (cur && (!cur.tools || !Array.isArray(cur.tools.enabled) || cur.tools.enabled.length === 0)) {
|
|
383
|
+
const toolAdapters = require('../utils/tool-adapters');
|
|
384
|
+
const suggested = toolAdapters.defaultEnabled();
|
|
385
|
+
UI.newline();
|
|
386
|
+
UI.warning('Pre-v3.7.0 config detected — `tools.enabled` is missing.');
|
|
387
|
+
UI.info(`From v3.7.0, BALDART can install framework skills for multiple AI tools (Claude Code, OpenAI Codex CLI, …) from the same source.`);
|
|
388
|
+
UI.info(`Autodetected on this machine: ${suggested.join(', ')}`);
|
|
389
|
+
const ok = await UI.confirm(`Backfill \`tools.enabled: [${suggested.join(', ')}]\` into baldart.config.yml?`, true);
|
|
390
|
+
if (ok) {
|
|
391
|
+
cur.tools = cur.tools || {};
|
|
392
|
+
cur.tools.enabled = suggested;
|
|
393
|
+
fs.writeFileSync(configPath, yaml.dump(cur, { lineWidth: 100, noRefs: true }));
|
|
394
|
+
UI.success('Backfilled tools.enabled. Re-running the symlink reconcile to install per-tool skill dirs…');
|
|
395
|
+
// Re-run merge so the just-backfilled tools get their skill dirs populated now.
|
|
396
|
+
try { symlinks.mergeSkills({ tools: suggested }); }
|
|
397
|
+
catch (err) { UI.warning(`Skill merge for new tools failed: ${err.message}`); }
|
|
398
|
+
needsRewrite = true;
|
|
399
|
+
} else {
|
|
400
|
+
UI.info('Skipped. You can add it manually under `tools:` in baldart.config.yml or rerun `npx baldart configure`.');
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Detect schema drift on other keys (features.*, paths.*).
|
|
358
405
|
const templatePath = path.join('.framework', 'framework', 'templates', 'baldart.config.template.yml');
|
|
359
406
|
if (fs.existsSync(templatePath)) {
|
|
360
407
|
try {
|
|
361
408
|
const tpl = yaml.load(fs.readFileSync(templatePath, 'utf8')) || {};
|
|
362
|
-
const
|
|
409
|
+
const cur2 = needsRewrite ? cur : (yaml.load(fs.readFileSync(configPath, 'utf8')) || {});
|
|
363
410
|
const missingFeatures = Object.keys(tpl.features || {})
|
|
364
|
-
.filter((k) => !(k in (
|
|
411
|
+
.filter((k) => !(k in (cur2.features || {})));
|
|
365
412
|
const missingPaths = Object.keys(tpl.paths || {})
|
|
366
|
-
.filter((k) => !(k in (
|
|
413
|
+
.filter((k) => !(k in (cur2.paths || {})));
|
|
367
414
|
if (missingFeatures.length || missingPaths.length) {
|
|
368
415
|
UI.newline();
|
|
369
416
|
UI.warning(
|
package/src/utils/symlinks.js
CHANGED
|
@@ -160,93 +160,135 @@ class SymlinkUtils {
|
|
|
160
160
|
// -----------------------------------------------------------------------
|
|
161
161
|
|
|
162
162
|
/**
|
|
163
|
-
* Merge framework skills into the user's
|
|
164
|
-
* without ever touching user-
|
|
163
|
+
* Merge framework skills into the user's per-tool skill directories
|
|
164
|
+
* (.claude/skills/, .agents/skills/, …) without ever touching user-
|
|
165
|
+
* authored skills. The SAME source file under
|
|
166
|
+
* `.framework/framework/.claude/skills/<skill>/` is symlinked into each
|
|
167
|
+
* enabled tool's expected directory — zero duplication, both Claude and
|
|
168
|
+
* Codex read the same content.
|
|
165
169
|
*
|
|
166
|
-
*
|
|
170
|
+
* @param {Object} opts
|
|
171
|
+
* @param {string[]} opts.tools - List of enabled tool names (default: ['claude'])
|
|
172
|
+
* @returns {Object} { linked: [...], skipped: [...], conflicts: [...] }
|
|
167
173
|
*/
|
|
168
|
-
mergeSkills() {
|
|
169
|
-
const
|
|
174
|
+
mergeSkills(opts = {}) {
|
|
175
|
+
const tools = (opts.tools && opts.tools.length) ? opts.tools : ['claude'];
|
|
176
|
+
const aggregate = { linked: [], skipped: [], conflicts: [] };
|
|
170
177
|
|
|
171
|
-
const frameworkSkillsDir = path.join(this.cwd,
|
|
178
|
+
const frameworkSkillsDir = path.join(this.cwd, FRAMEWORK_PAYLOAD, '.claude', 'skills');
|
|
172
179
|
if (!fs.existsSync(frameworkSkillsDir)) {
|
|
173
180
|
UI.warning(`No framework skills found at ${path.relative(this.cwd, frameworkSkillsDir)}. Skipping skill merge.`);
|
|
174
|
-
return
|
|
181
|
+
return aggregate;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const frameworkSkills = fs.readdirSync(frameworkSkillsDir).filter(name => {
|
|
185
|
+
if (name.startsWith('.')) return false;
|
|
186
|
+
const full = path.join(frameworkSkillsDir, name);
|
|
187
|
+
return fs.lstatSync(full).isDirectory();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
for (const tool of tools) {
|
|
191
|
+
const result = this._mergeSkillsForTool(tool, frameworkSkills, frameworkSkillsDir);
|
|
192
|
+
aggregate.linked.push(...result.linked);
|
|
193
|
+
aggregate.skipped.push(...result.skipped);
|
|
194
|
+
aggregate.conflicts.push(...result.conflicts);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (aggregate.conflicts.length > 0) {
|
|
198
|
+
this.ensureDirectory('.baldart');
|
|
199
|
+
const conflictPath = path.join(this.cwd, CONFLICT_LOG);
|
|
200
|
+
let existing = { conflicts: [] };
|
|
201
|
+
if (fs.existsSync(conflictPath)) {
|
|
202
|
+
try { existing = JSON.parse(fs.readFileSync(conflictPath, 'utf8')); }
|
|
203
|
+
catch (_) { /* ignore parse errors, overwrite */ }
|
|
204
|
+
}
|
|
205
|
+
existing.conflicts = aggregate.conflicts;
|
|
206
|
+
existing.last_merge = new Date().toISOString();
|
|
207
|
+
fs.writeFileSync(conflictPath, JSON.stringify(existing, null, 2) + '\n');
|
|
208
|
+
UI.warning(`Recorded ${aggregate.conflicts.length} skill conflict(s) in ${CONFLICT_LOG}`);
|
|
209
|
+
UI.info('Resolve each by renaming your local skill OR confirming the framework version is the one you want, then re-run `npx baldart update`.');
|
|
175
210
|
}
|
|
176
211
|
|
|
177
|
-
|
|
212
|
+
return aggregate;
|
|
213
|
+
}
|
|
178
214
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
215
|
+
/**
|
|
216
|
+
* Per-tool skill merge — internal. Resolves the tool's target directory
|
|
217
|
+
* via the adapter, handles legacy bulk-symlink conversion, then per-item
|
|
218
|
+
* symlinks every framework skill into that directory.
|
|
219
|
+
*/
|
|
220
|
+
_mergeSkillsForTool(toolName, frameworkSkills, frameworkSkillsDir) {
|
|
221
|
+
const { getAdapter } = require('./tool-adapters');
|
|
222
|
+
const adapter = getAdapter(toolName, this.cwd);
|
|
223
|
+
const result = { linked: [], skipped: [], conflicts: [] };
|
|
224
|
+
|
|
225
|
+
const skillsRel = adapter.skillsDir(); // e.g. ".claude/skills" or ".agents/skills"
|
|
226
|
+
const userSkillsDir = path.join(this.cwd, skillsRel);
|
|
227
|
+
|
|
228
|
+
// Legacy: if the user's skills dir is itself a bulk symlink, convert
|
|
229
|
+
// back to a real directory so per-item symlinks can coexist with user skills.
|
|
182
230
|
if (fs.existsSync(userSkillsDir) && fs.lstatSync(userSkillsDir).isSymbolicLink()) {
|
|
183
|
-
UI.warning(
|
|
231
|
+
UI.warning(`[${adapter.label}] Detected legacy bulk skills symlink at ${skillsRel}. Converting to per-item layout…`);
|
|
184
232
|
fs.unlinkSync(userSkillsDir);
|
|
185
233
|
}
|
|
186
234
|
|
|
187
|
-
this.ensureDirectory(
|
|
235
|
+
this.ensureDirectory(skillsRel);
|
|
188
236
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
237
|
+
// Compute the relative path from the user's skills dir to the framework
|
|
238
|
+
// source. For ".claude/skills" or ".agents/skills" both are 2 levels deep,
|
|
239
|
+
// so the prefix is "../.." — but compute it generically to support adapters
|
|
240
|
+
// that pick different depths.
|
|
241
|
+
const skillsDirDepth = skillsRel.split(path.sep).filter(Boolean).length;
|
|
242
|
+
const upPath = path.join(...Array(skillsDirDepth).fill('..'));
|
|
195
243
|
|
|
196
244
|
frameworkSkills.forEach(name => {
|
|
197
245
|
const linkPath = path.join(userSkillsDir, name);
|
|
198
|
-
const target = path.join(
|
|
199
|
-
const targetAbsolute = path.join(this.cwd,
|
|
246
|
+
const target = path.join(upPath, FRAMEWORK_PAYLOAD, '.claude', 'skills', name);
|
|
247
|
+
const targetAbsolute = path.join(this.cwd, FRAMEWORK_PAYLOAD, '.claude', 'skills', name);
|
|
248
|
+
|
|
249
|
+
// Use lstat to detect broken symlinks (fs.existsSync follows links and
|
|
250
|
+
// misclassifies broken ones as "missing", causing EEXIST on symlinkSync).
|
|
251
|
+
let lstat = null;
|
|
252
|
+
try { lstat = fs.lstatSync(linkPath); } catch (_) { /* absent */ }
|
|
200
253
|
|
|
201
|
-
if (!
|
|
254
|
+
if (!lstat) {
|
|
202
255
|
fs.symlinkSync(target, linkPath);
|
|
203
|
-
UI.success(`Skill linked: .
|
|
204
|
-
result.linked.push(name);
|
|
256
|
+
UI.success(`[${adapter.label}] Skill linked: ${path.join(skillsRel, name)}`);
|
|
257
|
+
result.linked.push({ tool: toolName, name });
|
|
205
258
|
return;
|
|
206
259
|
}
|
|
207
260
|
|
|
208
|
-
|
|
209
|
-
if (stat.isSymbolicLink()) {
|
|
261
|
+
if (lstat.isSymbolicLink()) {
|
|
210
262
|
const current = fs.readlinkSync(linkPath);
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
result.skipped.push({ name, reason: 'already-linked' });
|
|
263
|
+
const resolved = path.resolve(path.dirname(linkPath), current);
|
|
264
|
+
if (current === target || resolved === targetAbsolute) {
|
|
265
|
+
result.skipped.push({ tool: toolName, name, reason: 'already-linked' });
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
// Broken-or-misaimed symlink → replace silently with the correct one
|
|
269
|
+
if (!fs.existsSync(linkPath)) {
|
|
270
|
+
fs.unlinkSync(linkPath);
|
|
271
|
+
fs.symlinkSync(target, linkPath);
|
|
272
|
+
UI.success(`[${adapter.label}] Skill re-linked (was broken): ${path.join(skillsRel, name)}`);
|
|
273
|
+
result.linked.push({ tool: toolName, name });
|
|
214
274
|
return;
|
|
215
275
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
result.skipped.push({ name, reason: 'user-symlink-override', target: current });
|
|
276
|
+
UI.info(`[${adapter.label}] Skill kept (user override symlink): ${path.join(skillsRel, name)} → ${current}`);
|
|
277
|
+
result.skipped.push({ tool: toolName, name, reason: 'user-symlink-override', target: current });
|
|
219
278
|
return;
|
|
220
279
|
}
|
|
221
280
|
|
|
222
|
-
|
|
223
|
-
UI.warning(`Skill name conflict: .claude/skills/${name} already exists locally. Framework version NOT installed.`);
|
|
281
|
+
UI.warning(`[${adapter.label}] Skill name conflict: ${path.join(skillsRel, name)} already exists locally. Framework version NOT installed.`);
|
|
224
282
|
result.conflicts.push({
|
|
283
|
+
tool: toolName,
|
|
225
284
|
name,
|
|
226
|
-
local_kind:
|
|
227
|
-
local_path: path.join(
|
|
285
|
+
local_kind: lstat.isDirectory() ? 'directory' : 'file',
|
|
286
|
+
local_path: path.join(skillsRel, name),
|
|
228
287
|
framework_path: path.relative(this.cwd, path.join(frameworkSkillsDir, name)),
|
|
229
288
|
detected_at: new Date().toISOString()
|
|
230
289
|
});
|
|
231
290
|
});
|
|
232
291
|
|
|
233
|
-
// Persist conflicts (only when there's something to record)
|
|
234
|
-
if (result.conflicts.length > 0) {
|
|
235
|
-
this.ensureDirectory('.baldart');
|
|
236
|
-
const conflictPath = path.join(this.cwd, CONFLICT_LOG);
|
|
237
|
-
let existing = { conflicts: [] };
|
|
238
|
-
if (fs.existsSync(conflictPath)) {
|
|
239
|
-
try { existing = JSON.parse(fs.readFileSync(conflictPath, 'utf8')); }
|
|
240
|
-
catch (_) { /* ignore parse errors, overwrite */ }
|
|
241
|
-
}
|
|
242
|
-
// Replace conflicts for this run (most recent wins)
|
|
243
|
-
existing.conflicts = result.conflicts;
|
|
244
|
-
existing.last_merge = new Date().toISOString();
|
|
245
|
-
fs.writeFileSync(conflictPath, JSON.stringify(existing, null, 2) + '\n');
|
|
246
|
-
UI.warning(`Recorded ${result.conflicts.length} skill conflict(s) in ${CONFLICT_LOG}`);
|
|
247
|
-
UI.info('Resolve each by renaming your local skill OR confirming the framework version is the one you want, then re-run `npx baldart update`.');
|
|
248
|
-
}
|
|
249
|
-
|
|
250
292
|
return result;
|
|
251
293
|
}
|
|
252
294
|
|
|
@@ -260,7 +302,7 @@ class SymlinkUtils {
|
|
|
260
302
|
|
|
261
303
|
UI.newline();
|
|
262
304
|
UI.section('Merging Framework Skills');
|
|
263
|
-
this.mergeSkills();
|
|
305
|
+
this.mergeSkills({ tools: opts.tools });
|
|
264
306
|
|
|
265
307
|
UI.newline();
|
|
266
308
|
}
|
|
@@ -298,31 +340,51 @@ class SymlinkUtils {
|
|
|
298
340
|
}
|
|
299
341
|
});
|
|
300
342
|
|
|
301
|
-
//
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
343
|
+
// Per-tool skill directories. Read enabled tools from baldart.config.yml
|
|
344
|
+
// (fallback to ['claude'] if no config yet).
|
|
345
|
+
const { getAdapter } = require('./tool-adapters');
|
|
346
|
+
let enabledTools = ['claude'];
|
|
347
|
+
try {
|
|
348
|
+
const yaml = require('js-yaml');
|
|
349
|
+
const cfgPath = path.join(this.cwd, 'baldart.config.yml');
|
|
350
|
+
if (fs.existsSync(cfgPath)) {
|
|
351
|
+
const cfg = yaml.load(fs.readFileSync(cfgPath, 'utf8'));
|
|
352
|
+
if (Array.isArray(cfg?.tools?.enabled) && cfg.tools.enabled.length) {
|
|
353
|
+
enabledTools = cfg.tools.enabled;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
} catch (_) { /* keep default */ }
|
|
357
|
+
|
|
358
|
+
for (const toolName of enabledTools) {
|
|
359
|
+
let adapter;
|
|
360
|
+
try { adapter = getAdapter(toolName, this.cwd); }
|
|
361
|
+
catch (err) { UI.warning(err.message); allValid = false; continue; }
|
|
362
|
+
|
|
363
|
+
const skillsRel = adapter.skillsDir();
|
|
364
|
+
const skillsDir = path.join(this.cwd, skillsRel);
|
|
365
|
+
if (fs.existsSync(skillsDir)) {
|
|
366
|
+
const stat = fs.lstatSync(skillsDir);
|
|
367
|
+
if (stat.isSymbolicLink()) {
|
|
368
|
+
UI.warning(`[${adapter.label}] Legacy bulk skills symlink at ${skillsRel}. Run \`npx baldart update\` to convert to per-item.`);
|
|
318
369
|
allValid = false;
|
|
319
370
|
} else {
|
|
320
|
-
|
|
371
|
+
const sample = ['skill-creator', 'frontend-design', 'bug', 'prd', 'capture'];
|
|
372
|
+
let frameworkLinks = 0;
|
|
373
|
+
sample.forEach(name => {
|
|
374
|
+
const p = path.join(skillsDir, name);
|
|
375
|
+
if (fs.existsSync(p) && fs.lstatSync(p).isSymbolicLink()) frameworkLinks++;
|
|
376
|
+
});
|
|
377
|
+
if (frameworkLinks === 0) {
|
|
378
|
+
UI.warning(`[${adapter.label}] ${skillsRel}/ has no framework-linked skills. Run \`npx baldart update\`.`);
|
|
379
|
+
allValid = false;
|
|
380
|
+
} else {
|
|
381
|
+
UI.success(`[${adapter.label}] Valid: ${skillsRel}/ (per-item merge, ${frameworkLinks}/${sample.length} sampled framework skills linked)`);
|
|
382
|
+
}
|
|
321
383
|
}
|
|
384
|
+
} else {
|
|
385
|
+
UI.warning(`[${adapter.label}] Missing: ${skillsRel}/ (the framework would merge skills here)`);
|
|
386
|
+
allValid = false;
|
|
322
387
|
}
|
|
323
|
-
} else {
|
|
324
|
-
UI.warning('Missing: .claude/skills/ (the framework would merge skills here)');
|
|
325
|
-
allValid = false;
|
|
326
388
|
}
|
|
327
389
|
|
|
328
390
|
// Project configuration (v3.0.0+)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code tool adapter.
|
|
3
|
+
*
|
|
4
|
+
* Claude Code consumes the framework via:
|
|
5
|
+
* - `.claude/skills/<skill>/SKILL.md` (skills, per-item symlinks)
|
|
6
|
+
* - `.claude/agents/<agent>.md` (subagents, bulk symlink)
|
|
7
|
+
* - `.claude/commands/<command>.md` (slash commands, bulk symlink)
|
|
8
|
+
* - `.claude/settings.json` (hooks, ide config)
|
|
9
|
+
* - `AGENTS.md` (root coordination protocol)
|
|
10
|
+
*
|
|
11
|
+
* All paths are relative to the consumer repo root.
|
|
12
|
+
*/
|
|
13
|
+
class ClaudeAdapter {
|
|
14
|
+
constructor(cwd = process.cwd()) {
|
|
15
|
+
this.cwd = cwd;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
get name() { return 'claude'; }
|
|
19
|
+
get label() { return 'Claude Code'; }
|
|
20
|
+
|
|
21
|
+
/** Where this tool reads skill bundles from. */
|
|
22
|
+
skillsDir() { return '.claude/skills'; }
|
|
23
|
+
|
|
24
|
+
/** Whether this tool consumes subagent definitions (`.claude/agents/`). */
|
|
25
|
+
supportsSubagents() { return true; }
|
|
26
|
+
|
|
27
|
+
/** Whether this tool consumes Claude-style slash commands. */
|
|
28
|
+
supportsSlashCommands() { return true; }
|
|
29
|
+
|
|
30
|
+
/** Whether this tool consumes the Claude PreToolUse hook system. */
|
|
31
|
+
supportsHooks() { return true; }
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Autodetection signal. We assume Claude is always intended (it's the
|
|
35
|
+
* framework's primary target). The opt-out is via `tools.enabled` in
|
|
36
|
+
* baldart.config.yml.
|
|
37
|
+
*/
|
|
38
|
+
static detect(/* cwd */) { return true; }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = ClaudeAdapter;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* OpenAI Codex CLI tool adapter.
|
|
7
|
+
*
|
|
8
|
+
* Codex skill format is structurally identical to Claude's: a directory
|
|
9
|
+
* with `SKILL.md` (YAML frontmatter: name + description), plus optional
|
|
10
|
+
* `scripts/`, `references/`, `assets/`. The ONLY differences from Claude:
|
|
11
|
+
* - Lives at `.agents/skills/<skill>/` (repo-level) or
|
|
12
|
+
* `~/.agents/skills/<skill>/` (user-level).
|
|
13
|
+
* - May carry an optional `agents/openai.yaml` for Codex-specific
|
|
14
|
+
* metadata (display name, icon, brand color, allow_implicit_invocation,
|
|
15
|
+
* dependencies). Not required for the skill to load.
|
|
16
|
+
*
|
|
17
|
+
* Because the format is the same, the install model is "symlink the same
|
|
18
|
+
* source dir into two places" — Claude reads from `.claude/skills/<x>`,
|
|
19
|
+
* Codex reads from `.agents/skills/<x>`, and both follow the symlink to
|
|
20
|
+
* the single source under `.framework/framework/.claude/skills/<x>/`.
|
|
21
|
+
*
|
|
22
|
+
* Codex does NOT have an equivalent of Claude subagents (`.claude/agents/`)
|
|
23
|
+
* or slash commands (custom prompts are deprecated in favor of skills).
|
|
24
|
+
* AGENTS.md at the repo root IS read by Codex — same convention as Claude.
|
|
25
|
+
*/
|
|
26
|
+
class CodexAdapter {
|
|
27
|
+
constructor(cwd = process.cwd()) {
|
|
28
|
+
this.cwd = cwd;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
get name() { return 'codex'; }
|
|
32
|
+
get label() { return 'OpenAI Codex CLI'; }
|
|
33
|
+
|
|
34
|
+
skillsDir() { return '.agents/skills'; }
|
|
35
|
+
|
|
36
|
+
supportsSubagents() { return false; }
|
|
37
|
+
supportsSlashCommands() { return false; }
|
|
38
|
+
supportsHooks() { return false; }
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Heuristic: Codex is present on the user's machine if `~/.codex/` exists
|
|
42
|
+
* (created automatically by `codex` CLI on first run). Pure best-effort
|
|
43
|
+
* for the `configure` autodetect — the user can always toggle the flag.
|
|
44
|
+
*/
|
|
45
|
+
static detect() {
|
|
46
|
+
try { return fs.existsSync(path.join(os.homedir(), '.codex')); }
|
|
47
|
+
catch { return false; }
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = CodexAdapter;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const ClaudeAdapter = require('./claude');
|
|
2
|
+
const CodexAdapter = require('./codex');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Tool adapter registry.
|
|
6
|
+
*
|
|
7
|
+
* Adding a new adapter (Cursor, Aider, Cline, …):
|
|
8
|
+
* 1. Create `src/utils/tool-adapters/<name>.js` exporting a class with
|
|
9
|
+
* the same shape as ClaudeAdapter/CodexAdapter.
|
|
10
|
+
* 2. Add it to the REGISTRY below.
|
|
11
|
+
* 3. (Optional) Implement `static detect(cwd)` so `baldart configure`
|
|
12
|
+
* can autodetect when the user already has the tool installed.
|
|
13
|
+
*
|
|
14
|
+
* Adapters do NOT translate skill content — they just declare WHERE the
|
|
15
|
+
* tool reads from. The same source file under `.framework/framework/.claude/skills/`
|
|
16
|
+
* is symlinked into each enabled tool's expected directory.
|
|
17
|
+
*/
|
|
18
|
+
const REGISTRY = {
|
|
19
|
+
claude: ClaudeAdapter,
|
|
20
|
+
codex: CodexAdapter
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function listAdapters() {
|
|
24
|
+
return Object.keys(REGISTRY);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getAdapter(name, cwd) {
|
|
28
|
+
const Cls = REGISTRY[name];
|
|
29
|
+
if (!Cls) throw new Error(`Unknown tool adapter: ${name}. Available: ${listAdapters().join(', ')}`);
|
|
30
|
+
return new Cls(cwd);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Returns the list of tool names that should be enabled by default for
|
|
35
|
+
* a fresh install. Always includes `claude` (the framework's primary
|
|
36
|
+
* target). Includes `codex` if the user appears to have it installed.
|
|
37
|
+
*/
|
|
38
|
+
function defaultEnabled(cwd) {
|
|
39
|
+
const enabled = ['claude'];
|
|
40
|
+
if (CodexAdapter.detect(cwd)) enabled.push('codex');
|
|
41
|
+
return enabled;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = { REGISTRY, listAdapters, getAdapter, defaultEnabled };
|