baldart 3.6.2

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.
Files changed (230) hide show
  1. package/CHANGELOG.md +599 -0
  2. package/README.md +566 -0
  3. package/VERSION +1 -0
  4. package/bin/baldart.js +143 -0
  5. package/framework/.claude/agents/REGISTRY.md +169 -0
  6. package/framework/.claude/agents/api-perf-cost-auditor.md +291 -0
  7. package/framework/.claude/agents/code-reviewer.md +350 -0
  8. package/framework/.claude/agents/codebase-architect.md +391 -0
  9. package/framework/.claude/agents/coder.md +291 -0
  10. package/framework/.claude/agents/deep-human-insight.md +198 -0
  11. package/framework/.claude/agents/doc-reviewer.md +440 -0
  12. package/framework/.claude/agents/email-deliverability-architect.md +193 -0
  13. package/framework/.claude/agents/hybrid-ml-architect.md +285 -0
  14. package/framework/.claude/agents/hyper-gamification-designer.md +149 -0
  15. package/framework/.claude/agents/legal-counsel-gdpr.md +179 -0
  16. package/framework/.claude/agents/marketing-conversion-strategist.md +162 -0
  17. package/framework/.claude/agents/motion-expert.md +108 -0
  18. package/framework/.claude/agents/onboarding-architect-lead.md +230 -0
  19. package/framework/.claude/agents/plan-auditor.md +546 -0
  20. package/framework/.claude/agents/prd-card-writer.md +372 -0
  21. package/framework/.claude/agents/prd.md +744 -0
  22. package/framework/.claude/agents/qa-sentinel.md +305 -0
  23. package/framework/.claude/agents/remotion-animator-orchestrator.md +218 -0
  24. package/framework/.claude/agents/security-reviewer.md +276 -0
  25. package/framework/.claude/agents/senior-researcher.md +175 -0
  26. package/framework/.claude/agents/seo-analytics-strategist.md +156 -0
  27. package/framework/.claude/agents/skill-improver.md +61 -0
  28. package/framework/.claude/agents/ui-expert.md +191 -0
  29. package/framework/.claude/agents/visual-designer.md +190 -0
  30. package/framework/.claude/agents/website-orchestrator.md +118 -0
  31. package/framework/.claude/agents/wiki-curator.md +145 -0
  32. package/framework/.claude/commands/baldart-push.md +15 -0
  33. package/framework/.claude/commands/check.md +237 -0
  34. package/framework/.claude/commands/codexreview.md +203 -0
  35. package/framework/.claude/commands/design-review.md +11 -0
  36. package/framework/.claude/commands/issue-review.md +34 -0
  37. package/framework/.claude/commands/new.md +331 -0
  38. package/framework/.claude/commands/qa.md +257 -0
  39. package/framework/.claude/hooks/framework-edit-gate.js +208 -0
  40. package/framework/.claude/hooks/lint-before-commit.sh.template +66 -0
  41. package/framework/.claude/settings.local.json.example +32 -0
  42. package/framework/.claude/skills/api-design-principles/SKILL.md +567 -0
  43. package/framework/.claude/skills/api-design-principles/assets/api-design-checklist.md +155 -0
  44. package/framework/.claude/skills/api-design-principles/assets/rest-api-template.py +182 -0
  45. package/framework/.claude/skills/api-design-principles/references/graphql-schema-design.md +583 -0
  46. package/framework/.claude/skills/api-design-principles/references/rest-best-practices.md +408 -0
  47. package/framework/.claude/skills/baldart-push/SKILL.md +222 -0
  48. package/framework/.claude/skills/bug/SKILL.md +200 -0
  49. package/framework/.claude/skills/bug/references/logging-patterns.md +174 -0
  50. package/framework/.claude/skills/capture/SKILL.md +125 -0
  51. package/framework/.claude/skills/capture/references/synthesis-template.md +42 -0
  52. package/framework/.claude/skills/context-primer/SKILL.md +189 -0
  53. package/framework/.claude/skills/copywriting/SKILL.md +273 -0
  54. package/framework/.claude/skills/copywriting/references/copy-frameworks.md +338 -0
  55. package/framework/.claude/skills/copywriting/references/natural-transitions.md +252 -0
  56. package/framework/.claude/skills/doc-writing-for-rag/SKILL.md +119 -0
  57. package/framework/.claude/skills/doc-writing-for-rag/references/before-after-examples.md +291 -0
  58. package/framework/.claude/skills/doc-writing-for-rag/references/compact-templates.md +183 -0
  59. package/framework/.claude/skills/doc-writing-for-rag/references/frontmatter-minimal.md +112 -0
  60. package/framework/.claude/skills/doc-writing-for-rag/references/line-count-targets.md +110 -0
  61. package/framework/.claude/skills/doc-writing-for-rag/references/schemas-and-errors.md +129 -0
  62. package/framework/.claude/skills/find-skills/SKILL.md +133 -0
  63. package/framework/.claude/skills/frontend-design/LICENSE.txt +177 -0
  64. package/framework/.claude/skills/frontend-design/SKILL.md +84 -0
  65. package/framework/.claude/skills/gamification-design/SKILL.md +130 -0
  66. package/framework/.claude/skills/issue-review/SKILL.md +45 -0
  67. package/framework/.claude/skills/kie-ai/SKILL.md +262 -0
  68. package/framework/.claude/skills/kie-ai/references/models-catalog.md +272 -0
  69. package/framework/.claude/skills/kie-ai/scripts/kie_api.sh +209 -0
  70. package/framework/.claude/skills/kie-ai/scripts/remove_greenscreen.py +69 -0
  71. package/framework/.claude/skills/kie-ai/scripts/setup_api_key.sh +77 -0
  72. package/framework/.claude/skills/motion-design/LICENSE +21 -0
  73. package/framework/.claude/skills/motion-design/README.md +82 -0
  74. package/framework/.claude/skills/motion-design/SKILL.md +336 -0
  75. package/framework/.claude/skills/motion-design/director/choreography.md +93 -0
  76. package/framework/.claude/skills/motion-design/director/context-adaptation.md +83 -0
  77. package/framework/.claude/skills/motion-design/director/core-philosophy.md +53 -0
  78. package/framework/.claude/skills/motion-design/director/decision-framework.md +91 -0
  79. package/framework/.claude/skills/motion-design/director/disney-principles.md +102 -0
  80. package/framework/.claude/skills/motion-design/director/emotion-mapping.md +71 -0
  81. package/framework/.claude/skills/motion-design/director/motion-personality.md +89 -0
  82. package/framework/.claude/skills/motion-design/director/narrative-structure.md +62 -0
  83. package/framework/.claude/skills/motion-design/patterns/ambient-continuous.md +81 -0
  84. package/framework/.claude/skills/motion-design/patterns/entrance-exit.md +82 -0
  85. package/framework/.claude/skills/motion-design/patterns/multi-element.md +69 -0
  86. package/framework/.claude/skills/motion-design/patterns/state-feedback.md +96 -0
  87. package/framework/.claude/skills/motion-design/reference/property-selection.md +95 -0
  88. package/framework/.claude/skills/motion-design/reference/quality-checklist.md +67 -0
  89. package/framework/.claude/skills/motion-design/reference/timing-easing-tables.md +106 -0
  90. package/framework/.claude/skills/motion-design/reference/troubleshooting.md +73 -0
  91. package/framework/.claude/skills/new/SKILL.md +1687 -0
  92. package/framework/.claude/skills/playwright-skill/API_REFERENCE.md +652 -0
  93. package/framework/.claude/skills/playwright-skill/SKILL.md +157 -0
  94. package/framework/.claude/skills/playwright-skill/package.json +26 -0
  95. package/framework/.claude/skills/prd/SKILL.md +228 -0
  96. package/framework/.claude/skills/prd/assets/card-template.yml +232 -0
  97. package/framework/.claude/skills/prd/assets/epic-template.yml +190 -0
  98. package/framework/.claude/skills/prd/assets/prd-template.md +230 -0
  99. package/framework/.claude/skills/prd/assets/state-template.md +78 -0
  100. package/framework/.claude/skills/prd/references/api-perf-gate.md +152 -0
  101. package/framework/.claude/skills/prd/references/audit-phase.md +478 -0
  102. package/framework/.claude/skills/prd/references/backlog-phase.md +145 -0
  103. package/framework/.claude/skills/prd/references/discovery-phase.md +359 -0
  104. package/framework/.claude/skills/prd/references/impact-analysis.md +233 -0
  105. package/framework/.claude/skills/prd/references/prd-add-phase.md +214 -0
  106. package/framework/.claude/skills/prd/references/prd-writing-phase.md +145 -0
  107. package/framework/.claude/skills/prd/references/research-phase.md +216 -0
  108. package/framework/.claude/skills/prd/references/ui-design-phase.md +61 -0
  109. package/framework/.claude/skills/prd/references/validation-phase.md +72 -0
  110. package/framework/.claude/skills/prd-add/SKILL.md +222 -0
  111. package/framework/.claude/skills/prd-add/references/impact-analysis.md +233 -0
  112. package/framework/.claude/skills/remotion-best-practices/SKILL.md +48 -0
  113. package/framework/.claude/skills/remotion-best-practices/rules/3d.md +86 -0
  114. package/framework/.claude/skills/remotion-best-practices/rules/animations.md +29 -0
  115. package/framework/.claude/skills/remotion-best-practices/rules/assets/charts-bar-chart.tsx +173 -0
  116. package/framework/.claude/skills/remotion-best-practices/rules/assets/text-animations-typewriter.tsx +100 -0
  117. package/framework/.claude/skills/remotion-best-practices/rules/assets/text-animations-word-highlight.tsx +108 -0
  118. package/framework/.claude/skills/remotion-best-practices/rules/assets.md +78 -0
  119. package/framework/.claude/skills/remotion-best-practices/rules/audio.md +169 -0
  120. package/framework/.claude/skills/remotion-best-practices/rules/calculate-metadata.md +104 -0
  121. package/framework/.claude/skills/remotion-best-practices/rules/can-decode.md +75 -0
  122. package/framework/.claude/skills/remotion-best-practices/rules/charts.md +58 -0
  123. package/framework/.claude/skills/remotion-best-practices/rules/compositions.md +141 -0
  124. package/framework/.claude/skills/remotion-best-practices/rules/display-captions.md +184 -0
  125. package/framework/.claude/skills/remotion-best-practices/rules/extract-frames.md +229 -0
  126. package/framework/.claude/skills/remotion-best-practices/rules/fonts.md +152 -0
  127. package/framework/.claude/skills/remotion-best-practices/rules/get-audio-duration.md +58 -0
  128. package/framework/.claude/skills/remotion-best-practices/rules/get-video-dimensions.md +68 -0
  129. package/framework/.claude/skills/remotion-best-practices/rules/get-video-duration.md +58 -0
  130. package/framework/.claude/skills/remotion-best-practices/rules/gifs.md +141 -0
  131. package/framework/.claude/skills/remotion-best-practices/rules/images.md +130 -0
  132. package/framework/.claude/skills/remotion-best-practices/rules/import-srt-captions.md +69 -0
  133. package/framework/.claude/skills/remotion-best-practices/rules/light-leaks.md +73 -0
  134. package/framework/.claude/skills/remotion-best-practices/rules/lottie.md +67 -0
  135. package/framework/.claude/skills/remotion-best-practices/rules/maps.md +401 -0
  136. package/framework/.claude/skills/remotion-best-practices/rules/measuring-dom-nodes.md +34 -0
  137. package/framework/.claude/skills/remotion-best-practices/rules/measuring-text.md +143 -0
  138. package/framework/.claude/skills/remotion-best-practices/rules/parameters.md +98 -0
  139. package/framework/.claude/skills/remotion-best-practices/rules/sequencing.md +118 -0
  140. package/framework/.claude/skills/remotion-best-practices/rules/subtitles.md +36 -0
  141. package/framework/.claude/skills/remotion-best-practices/rules/tailwind.md +11 -0
  142. package/framework/.claude/skills/remotion-best-practices/rules/text-animations.md +20 -0
  143. package/framework/.claude/skills/remotion-best-practices/rules/timing.md +179 -0
  144. package/framework/.claude/skills/remotion-best-practices/rules/transcribe-captions.md +70 -0
  145. package/framework/.claude/skills/remotion-best-practices/rules/transitions.md +197 -0
  146. package/framework/.claude/skills/remotion-best-practices/rules/transparent-videos.md +106 -0
  147. package/framework/.claude/skills/remotion-best-practices/rules/trimming.md +52 -0
  148. package/framework/.claude/skills/remotion-best-practices/rules/videos.md +171 -0
  149. package/framework/.claude/skills/seo-audit/SKILL.md +394 -0
  150. package/framework/.claude/skills/seo-audit/references/aeo-geo-patterns.md +279 -0
  151. package/framework/.claude/skills/seo-audit/references/ai-writing-detection.md +190 -0
  152. package/framework/.claude/skills/simplify/SKILL.md +137 -0
  153. package/framework/.claude/skills/skill-creator/LICENSE.txt +202 -0
  154. package/framework/.claude/skills/skill-creator/SKILL.md +356 -0
  155. package/framework/.claude/skills/skill-creator/references/output-patterns.md +82 -0
  156. package/framework/.claude/skills/skill-creator/references/workflows.md +28 -0
  157. package/framework/.claude/skills/skill-creator/scripts/init_skill.py +303 -0
  158. package/framework/.claude/skills/skill-creator/scripts/package_skill.py +110 -0
  159. package/framework/.claude/skills/skill-creator/scripts/quick_validate.py +95 -0
  160. package/framework/.claude/skills/ui-design/SKILL.md +199 -0
  161. package/framework/.claude/skills/ui-design/references/component-discovery.md +54 -0
  162. package/framework/.claude/skills/ui-design/references/evaluation.md +171 -0
  163. package/framework/.claude/skills/ui-design/references/generation.md +109 -0
  164. package/framework/.claude/skills/ui-design/references/inventory.md +59 -0
  165. package/framework/.claude/skills/webapp-testing/LICENSE.txt +202 -0
  166. package/framework/.claude/skills/webapp-testing/SKILL.md +123 -0
  167. package/framework/.claude/skills/webapp-testing/examples/console_logging.py +35 -0
  168. package/framework/.claude/skills/webapp-testing/examples/element_discovery.py +40 -0
  169. package/framework/.claude/skills/webapp-testing/examples/static_html_automation.py +33 -0
  170. package/framework/.claude/skills/webapp-testing/scripts/with_server.py +106 -0
  171. package/framework/.claude/skills/worktree-manager/SKILL.md +680 -0
  172. package/framework/AGENTS.md +240 -0
  173. package/framework/agents/api-contracts.md +137 -0
  174. package/framework/agents/architecture.md +145 -0
  175. package/framework/agents/coding-standards.md +148 -0
  176. package/framework/agents/data-model.md +110 -0
  177. package/framework/agents/deployment-protocol.md +232 -0
  178. package/framework/agents/design-review.md +172 -0
  179. package/framework/agents/env-reference.md +171 -0
  180. package/framework/agents/github-issue-subagent.md +252 -0
  181. package/framework/agents/index.md +261 -0
  182. package/framework/agents/llm-wiki-methodology.md +216 -0
  183. package/framework/agents/maintenance-protocol.md +305 -0
  184. package/framework/agents/observability.md +162 -0
  185. package/framework/agents/performance.md +155 -0
  186. package/framework/agents/project-context.md +145 -0
  187. package/framework/agents/runbook.md +208 -0
  188. package/framework/agents/security.md +168 -0
  189. package/framework/agents/skills-mapping.md +286 -0
  190. package/framework/agents/testing.md +111 -0
  191. package/framework/agents/workflows.md +215 -0
  192. package/framework/docs/PROJECT-CONFIGURATION.md +336 -0
  193. package/framework/docs/references/brand-guidelines.md +170 -0
  194. package/framework/docs/references/ui-guidelines.template.md +182 -0
  195. package/framework/routines/code-review.routine.yml +46 -0
  196. package/framework/routines/doc-review.routine.yml +45 -0
  197. package/framework/routines/ds-drift.routine.yml +52 -0
  198. package/framework/routines/full-sweep.routine.yml +51 -0
  199. package/framework/routines/index.yml +70 -0
  200. package/framework/routines/skill-improve.routine.yml +50 -0
  201. package/framework/routines/wiki-review.routine.yml +45 -0
  202. package/framework/templates/baldart.config.template.yml +113 -0
  203. package/framework/templates/breaking-change-checklist.md +484 -0
  204. package/framework/templates/feature-card.template.yml +125 -0
  205. package/framework/templates/overlays/README.md +44 -0
  206. package/framework/templates/overlays/copywriting.fidelity-example.md +62 -0
  207. package/framework/templates/overlays/ui-design.fidelity-example.md +75 -0
  208. package/framework/templates/skill-project-context.snippet.md +19 -0
  209. package/framework/templates/spec.template.md +208 -0
  210. package/package.json +51 -0
  211. package/src/commands/add.js +229 -0
  212. package/src/commands/configure.js +385 -0
  213. package/src/commands/doctor.js +486 -0
  214. package/src/commands/migrate.js +185 -0
  215. package/src/commands/push.js +0 -0
  216. package/src/commands/routines.js +269 -0
  217. package/src/commands/status.js +130 -0
  218. package/src/commands/update.js +419 -0
  219. package/src/commands/version.js +88 -0
  220. package/src/utils/contamination.js +400 -0
  221. package/src/utils/git.js +181 -0
  222. package/src/utils/hooks.js +152 -0
  223. package/src/utils/routine-adapters/claude-code-cloud.js +78 -0
  224. package/src/utils/routine-adapters/cron.js +138 -0
  225. package/src/utils/routine-adapters/github-actions.js +141 -0
  226. package/src/utils/routine-adapters/index.js +21 -0
  227. package/src/utils/routines.js +166 -0
  228. package/src/utils/state.js +143 -0
  229. package/src/utils/symlinks.js +425 -0
  230. package/src/utils/ui.js +133 -0
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Consumer-side state tracking — `.baldart/state.json`.
3
+ *
4
+ * Centralised view of "what version is installed, when was it installed,
5
+ * when did we last update, when did we last push". Read by `baldart version`
6
+ * and the `/baldart-push` skill so the user always knows which framework
7
+ * version they are working against.
8
+ *
9
+ * The file is committed to the consumer's repo. It is never overwritten by
10
+ * `baldart update` — only mutated by the CLI commands that need to record
11
+ * an event.
12
+ *
13
+ * Schema (v1):
14
+ * {
15
+ * "state_version": 1,
16
+ * "installed_version": "3.1.0",
17
+ * "install_date": "2026-05-21T15:30:00.000Z",
18
+ * "last_update_date": "2026-05-21T16:00:00.000Z",
19
+ * "last_pushed_version": "3.0.0",
20
+ * "last_push_date": "2026-05-21T17:00:00.000Z",
21
+ * "framework_repo": "antbald/BALDART",
22
+ * "history": [
23
+ * { "ts": "...", "event": "install"|"update"|"push", "from": "x", "to": "y", "note": "..." }
24
+ * ]
25
+ * }
26
+ *
27
+ * History keeps the last 20 entries (rolling). Older entries are dropped.
28
+ */
29
+
30
+ const fs = require('fs');
31
+ const path = require('path');
32
+
33
+ const STATE_DIR = '.baldart';
34
+ const STATE_FILE = path.join(STATE_DIR, 'state.json');
35
+ const STATE_VERSION = 1;
36
+ const HISTORY_LIMIT = 20;
37
+
38
+ function defaultState() {
39
+ return {
40
+ state_version: STATE_VERSION,
41
+ installed_version: null,
42
+ install_date: null,
43
+ last_update_date: null,
44
+ last_pushed_version: null,
45
+ last_push_date: null,
46
+ framework_repo: null,
47
+ history: [],
48
+ };
49
+ }
50
+
51
+ function fullPath(cwd = process.cwd()) {
52
+ return path.join(cwd, STATE_FILE);
53
+ }
54
+
55
+ function load(cwd = process.cwd()) {
56
+ const full = fullPath(cwd);
57
+ if (!fs.existsSync(full)) return defaultState();
58
+ try {
59
+ const raw = JSON.parse(fs.readFileSync(full, 'utf8'));
60
+ // Merge with defaults so missing keys get sane values without crashing.
61
+ return { ...defaultState(), ...raw };
62
+ } catch (err) {
63
+ // Corrupt state file — log and fall back to defaults. Do not throw;
64
+ // versioning is auxiliary, never block the user's CLI flow.
65
+ return defaultState();
66
+ }
67
+ }
68
+
69
+ function save(state, cwd = process.cwd()) {
70
+ const dir = path.join(cwd, STATE_DIR);
71
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
72
+ const full = fullPath(cwd);
73
+ fs.writeFileSync(full, JSON.stringify(state, null, 2) + '\n', 'utf8');
74
+ }
75
+
76
+ function appendHistory(state, entry) {
77
+ const next = [...(state.history || []), { ts: new Date().toISOString(), ...entry }];
78
+ return next.slice(-HISTORY_LIMIT);
79
+ }
80
+
81
+ /**
82
+ * Record an install event. Idempotent: calling twice for the same version is a no-op
83
+ * other than the history entry.
84
+ */
85
+ function recordInstall({ version, repo }, cwd = process.cwd()) {
86
+ const state = load(cwd);
87
+ const now = new Date().toISOString();
88
+ const wasFirstInstall = !state.installed_version;
89
+ state.installed_version = version;
90
+ state.framework_repo = repo || state.framework_repo || 'antbald/BALDART';
91
+ if (wasFirstInstall) state.install_date = now;
92
+ state.history = appendHistory(state, {
93
+ event: wasFirstInstall ? 'install' : 'reinstall',
94
+ to: version,
95
+ });
96
+ save(state, cwd);
97
+ return state;
98
+ }
99
+
100
+ function recordUpdate({ from, to }, cwd = process.cwd()) {
101
+ const state = load(cwd);
102
+ state.installed_version = to;
103
+ state.last_update_date = new Date().toISOString();
104
+ state.history = appendHistory(state, { event: 'update', from, to });
105
+ save(state, cwd);
106
+ return state;
107
+ }
108
+
109
+ function recordPush({ from, to, description }, cwd = process.cwd()) {
110
+ const state = load(cwd);
111
+ state.last_pushed_version = to;
112
+ state.last_push_date = new Date().toISOString();
113
+ state.installed_version = to; // a push bumps the local version too
114
+ state.history = appendHistory(state, { event: 'push', from, to, note: description });
115
+ save(state, cwd);
116
+ return state;
117
+ }
118
+
119
+ /**
120
+ * Read the framework's VERSION file from .framework/VERSION. Used by version /
121
+ * status commands to cross-check what the symlinked framework reports vs what
122
+ * the state file remembers.
123
+ */
124
+ function readFrameworkVersion(cwd = process.cwd()) {
125
+ const versionPath = path.join(cwd, '.framework', 'VERSION');
126
+ if (!fs.existsSync(versionPath)) return null;
127
+ try {
128
+ return fs.readFileSync(versionPath, 'utf8').trim();
129
+ } catch (_) {
130
+ return null;
131
+ }
132
+ }
133
+
134
+ module.exports = {
135
+ load,
136
+ save,
137
+ recordInstall,
138
+ recordUpdate,
139
+ recordPush,
140
+ readFrameworkVersion,
141
+ STATE_FILE,
142
+ STATE_VERSION,
143
+ };
@@ -0,0 +1,425 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const UI = require('./ui');
4
+
5
+ const FRAMEWORK_DIR = '.framework';
6
+ // The Git subtree pulls the ENTIRE BALDART repo into `.framework/`, and the
7
+ // repo itself nests its shippable content under a top-level `framework/`
8
+ // directory. So everything we symlink or copy lives at
9
+ // `.framework/framework/<path>`, never directly at `.framework/<path>`.
10
+ // Use FRAMEWORK_PAYLOAD for any source path you want to reach inside the
11
+ // installed framework — never bare FRAMEWORK_DIR.
12
+ const FRAMEWORK_PAYLOAD = path.join(FRAMEWORK_DIR, 'framework');
13
+ const CONFLICT_LOG = path.join('.baldart', 'skill-conflicts.json');
14
+
15
+ /**
16
+ * Symlink management for BALDART.
17
+ *
18
+ * Design notes (v2.1.1+):
19
+ *
20
+ * - `AGENTS.md`, `agents/`, `.claude/agents/`, `.claude/commands/` are
21
+ * **bulk symlinks**. They've worked this way since v1.0.0 and replacing
22
+ * them is safe for users on canonical installations. If a user has
23
+ * replaced one of these with a real file/dir (customisation), we WARN
24
+ * and ask before overwriting — we no longer silently rename to `.backup`.
25
+ *
26
+ * - `.claude/skills/` is a **real directory the user owns**. Each framework
27
+ * skill is symlinked **per-item** inside it. User-authored skills coexist
28
+ * in the same dir. Name collisions are recorded in
29
+ * `.baldart/skill-conflicts.json` and never overwritten.
30
+ */
31
+ class SymlinkUtils {
32
+ constructor(cwd = process.cwd()) {
33
+ this.cwd = cwd;
34
+ }
35
+
36
+ // -----------------------------------------------------------------------
37
+ // Low-level helpers
38
+ // -----------------------------------------------------------------------
39
+
40
+ /**
41
+ * Create a single symlink at linkPath pointing at target.
42
+ * Modes:
43
+ * safe: refuse to overwrite a non-symlink at linkPath (returns false).
44
+ * prompt: ask the user before backing up a non-symlink (default).
45
+ * force: legacy v2.0.x behaviour — backup non-symlink to .backup and replace.
46
+ */
47
+ async createSymlink(target, linkPath, opts = {}) {
48
+ const mode = opts.mode || 'prompt';
49
+ const relative = !!opts.relative;
50
+ const fullLinkPath = path.join(this.cwd, linkPath);
51
+ const fullTarget = relative
52
+ ? path.relative(path.dirname(fullLinkPath), path.join(this.cwd, target))
53
+ : path.join(this.cwd, target);
54
+
55
+ // Use lstat (not exists, which follows symlinks) so we detect broken
56
+ // symlinks — they fail fs.existsSync but still occupy the path and
57
+ // would make fs.symlinkSync below throw EEXIST.
58
+ let lstat = null;
59
+ try { lstat = fs.lstatSync(fullLinkPath); } catch (_) { /* path not present */ }
60
+
61
+ if (lstat) {
62
+ const stat = lstat;
63
+ if (stat.isSymbolicLink()) {
64
+ const current = fs.readlinkSync(fullLinkPath);
65
+ if (current === fullTarget) {
66
+ // Already correct — no-op.
67
+ return true;
68
+ }
69
+ // Existing symlink pointing elsewhere — replace silently
70
+ fs.unlinkSync(fullLinkPath);
71
+ } else {
72
+ // Real file or directory — user customisation
73
+ if (mode === 'safe') {
74
+ UI.warning(`Keeping existing (not a symlink): ${linkPath} — framework will not overwrite.`);
75
+ return false;
76
+ }
77
+ if (mode === 'prompt') {
78
+ UI.warning(`${linkPath} exists and is not a symlink (likely customised).`);
79
+ const ok = await UI.confirm(`Move it to ${linkPath}.backup and install the framework symlink?`, false);
80
+ if (!ok) {
81
+ UI.info(`Skipped ${linkPath}. Your customisation is preserved.`);
82
+ return false;
83
+ }
84
+ }
85
+ const backupPath = `${fullLinkPath}.backup`;
86
+ UI.warning(`Backing up existing: ${linkPath} → ${linkPath}.backup`);
87
+ fs.renameSync(fullLinkPath, backupPath);
88
+ }
89
+ }
90
+
91
+ fs.symlinkSync(fullTarget, fullLinkPath);
92
+ UI.success(`Symlink: ${linkPath} → ${target}`);
93
+ return true;
94
+ }
95
+
96
+ ensureDirectory(dir) {
97
+ const fullPath = path.join(this.cwd, dir);
98
+ if (!fs.existsSync(fullPath)) {
99
+ fs.mkdirSync(fullPath, { recursive: true });
100
+ }
101
+ }
102
+
103
+ copyFile(source, dest) {
104
+ const fullSource = path.join(this.cwd, source);
105
+ const fullDest = path.join(this.cwd, dest);
106
+
107
+ const destDir = path.dirname(fullDest);
108
+ if (!fs.existsSync(destDir)) {
109
+ fs.mkdirSync(destDir, { recursive: true });
110
+ }
111
+
112
+ if (fs.existsSync(fullDest)) {
113
+ UI.warning(`Skipped (already exists): ${dest}`);
114
+ return false;
115
+ }
116
+
117
+ fs.copyFileSync(fullSource, fullDest);
118
+ UI.success(`Copied: ${dest}`);
119
+ return true;
120
+ }
121
+
122
+ // -----------------------------------------------------------------------
123
+ // Bulk symlinks (AGENTS.md, agents/, .claude/agents/, .claude/commands/)
124
+ // -----------------------------------------------------------------------
125
+
126
+ async createBulkSymlinks(opts = {}) {
127
+ const mode = opts.mode || 'prompt';
128
+
129
+ await this.createSymlink(
130
+ path.join(FRAMEWORK_PAYLOAD, 'AGENTS.md'),
131
+ 'AGENTS.md',
132
+ { mode }
133
+ );
134
+
135
+ await this.createSymlink(
136
+ path.join(FRAMEWORK_PAYLOAD, 'agents'),
137
+ 'agents',
138
+ { mode }
139
+ );
140
+
141
+ this.ensureDirectory('.claude');
142
+
143
+ // target is relative to cwd; createSymlink converts it to a path relative
144
+ // to the link's parent directory when `relative: true`.
145
+ await this.createSymlink(
146
+ path.join(FRAMEWORK_PAYLOAD, '.claude', 'agents'),
147
+ path.join('.claude', 'agents'),
148
+ { relative: true, mode }
149
+ );
150
+
151
+ await this.createSymlink(
152
+ path.join(FRAMEWORK_PAYLOAD, '.claude', 'commands'),
153
+ path.join('.claude', 'commands'),
154
+ { relative: true, mode }
155
+ );
156
+ }
157
+
158
+ // -----------------------------------------------------------------------
159
+ // Per-item merge for .claude/skills/
160
+ // -----------------------------------------------------------------------
161
+
162
+ /**
163
+ * Merge framework skills into the user's .claude/skills/ directory
164
+ * without ever touching user-authored skills.
165
+ *
166
+ * Returns { linked: [...], skipped: [...], conflicts: [...] }
167
+ */
168
+ mergeSkills() {
169
+ const result = { linked: [], skipped: [], conflicts: [] };
170
+
171
+ const frameworkSkillsDir = path.join(this.cwd, FRAMEWORK_DIR, 'framework', '.claude', 'skills');
172
+ if (!fs.existsSync(frameworkSkillsDir)) {
173
+ UI.warning(`No framework skills found at ${path.relative(this.cwd, frameworkSkillsDir)}. Skipping skill merge.`);
174
+ return result;
175
+ }
176
+
177
+ const userSkillsDir = path.join(this.cwd, '.claude', 'skills');
178
+
179
+ // Handle the v2.0.x legacy case: .claude/skills is a single symlink to
180
+ // .framework/.../skills. Convert it back to a real directory so we can
181
+ // host per-item symlinks alongside user skills.
182
+ if (fs.existsSync(userSkillsDir) && fs.lstatSync(userSkillsDir).isSymbolicLink()) {
183
+ UI.warning('Detected legacy v2.0.x bulk skills symlink. Converting to per-item layout…');
184
+ fs.unlinkSync(userSkillsDir);
185
+ }
186
+
187
+ this.ensureDirectory('.claude/skills');
188
+
189
+ const frameworkSkills = fs.readdirSync(frameworkSkillsDir).filter(name => {
190
+ // Skip hidden files (.DS_Store, etc.) and any non-skill entries
191
+ if (name.startsWith('.')) return false;
192
+ const full = path.join(frameworkSkillsDir, name);
193
+ return fs.lstatSync(full).isDirectory();
194
+ });
195
+
196
+ frameworkSkills.forEach(name => {
197
+ const linkPath = path.join(userSkillsDir, name);
198
+ const target = path.join('..', '..', FRAMEWORK_DIR, 'framework', '.claude', 'skills', name);
199
+ const targetAbsolute = path.join(this.cwd, FRAMEWORK_DIR, 'framework', '.claude', 'skills', name);
200
+
201
+ if (!fs.existsSync(linkPath)) {
202
+ fs.symlinkSync(target, linkPath);
203
+ UI.success(`Skill linked: .claude/skills/${name}`);
204
+ result.linked.push(name);
205
+ return;
206
+ }
207
+
208
+ const stat = fs.lstatSync(linkPath);
209
+ if (stat.isSymbolicLink()) {
210
+ const current = fs.readlinkSync(linkPath);
211
+ if (current === target || path.resolve(path.dirname(linkPath), current) === targetAbsolute) {
212
+ // Already linked correctly — silent OK
213
+ result.skipped.push({ name, reason: 'already-linked' });
214
+ return;
215
+ }
216
+ // Symlink pointing elsewhere — user override, leave alone
217
+ UI.info(`Skill kept (user override symlink): .claude/skills/${name} → ${current}`);
218
+ result.skipped.push({ name, reason: 'user-symlink-override', target: current });
219
+ return;
220
+ }
221
+
222
+ // Real file or directory under same name — NAME COLLISION
223
+ UI.warning(`Skill name conflict: .claude/skills/${name} already exists locally. Framework version NOT installed.`);
224
+ result.conflicts.push({
225
+ name,
226
+ local_kind: stat.isDirectory() ? 'directory' : 'file',
227
+ local_path: path.join('.claude', 'skills', name),
228
+ framework_path: path.relative(this.cwd, path.join(frameworkSkillsDir, name)),
229
+ detected_at: new Date().toISOString()
230
+ });
231
+ });
232
+
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
+ return result;
251
+ }
252
+
253
+ // -----------------------------------------------------------------------
254
+ // Public composite operations
255
+ // -----------------------------------------------------------------------
256
+
257
+ async createAllSymlinks(opts = {}) {
258
+ UI.section('Creating Symlinks');
259
+ await this.createBulkSymlinks(opts);
260
+
261
+ UI.newline();
262
+ UI.section('Merging Framework Skills');
263
+ this.mergeSkills();
264
+
265
+ UI.newline();
266
+ }
267
+
268
+ verifySymlinks() {
269
+ const bulkSymlinks = [
270
+ 'AGENTS.md',
271
+ 'agents',
272
+ '.claude/agents',
273
+ '.claude/commands'
274
+ ];
275
+
276
+ let allValid = true;
277
+
278
+ bulkSymlinks.forEach(link => {
279
+ const fullPath = path.join(this.cwd, link);
280
+
281
+ // lstat (doesn't follow symlinks) so we can distinguish "absent" from
282
+ // "broken symlink" — the latter fails fs.existsSync but still occupies
283
+ // the path and must be removed before recreating the link.
284
+ let lstat = null;
285
+ try { lstat = fs.lstatSync(fullPath); } catch (_) { /* absent */ }
286
+
287
+ if (!lstat) {
288
+ UI.warning(`Missing: ${link}`);
289
+ allValid = false;
290
+ } else if (!lstat.isSymbolicLink()) {
291
+ UI.warning(`Not a symlink (user customisation?): ${link}`);
292
+ allValid = false;
293
+ } else if (!fs.existsSync(fullPath)) {
294
+ UI.warning(`Broken symlink: ${link} → ${fs.readlinkSync(fullPath)}`);
295
+ allValid = false;
296
+ } else {
297
+ UI.success(`Valid: ${link}`);
298
+ }
299
+ });
300
+
301
+ // .claude/skills/ should be a real directory in v2.1.1+ (or absent).
302
+ const skillsDir = path.join(this.cwd, '.claude', 'skills');
303
+ if (fs.existsSync(skillsDir)) {
304
+ const stat = fs.lstatSync(skillsDir);
305
+ if (stat.isSymbolicLink()) {
306
+ UI.warning('Legacy v2.0.x layout: .claude/skills is a bulk symlink. Run `npx baldart update` (or `npx baldart migrate`) to convert to the per-item layout.');
307
+ allValid = false;
308
+ } else {
309
+ // Spot-check a couple of well-known framework skills
310
+ const sample = ['skill-creator', 'frontend-design', 'bug', 'prd', 'capture'];
311
+ let frameworkLinks = 0;
312
+ sample.forEach(name => {
313
+ const p = path.join(skillsDir, name);
314
+ if (fs.existsSync(p) && fs.lstatSync(p).isSymbolicLink()) frameworkLinks++;
315
+ });
316
+ if (frameworkLinks === 0) {
317
+ UI.warning('.claude/skills/ has no framework-linked skills. Run `npx baldart update` to merge them.');
318
+ allValid = false;
319
+ } else {
320
+ UI.success(`Valid: .claude/skills/ (per-item merge, ${frameworkLinks}/${sample.length} sampled framework skills linked)`);
321
+ }
322
+ }
323
+ } else {
324
+ UI.warning('Missing: .claude/skills/ (the framework would merge skills here)');
325
+ allValid = false;
326
+ }
327
+
328
+ // Project configuration (v3.0.0+)
329
+ const configPath = path.join(this.cwd, 'baldart.config.yml');
330
+ if (!fs.existsSync(configPath)) {
331
+ UI.warning('Missing: baldart.config.yml — skills will prompt for paths on every invocation. Run `npx baldart configure`.');
332
+ allValid = false;
333
+ } else {
334
+ // Check it parses + has a version field — full schema validation is the
335
+ // configure command's job, this is a smoke test.
336
+ try {
337
+ const yaml = require('js-yaml');
338
+ const cfg = yaml.load(fs.readFileSync(configPath, 'utf8'));
339
+ if (!cfg || typeof cfg !== 'object') throw new Error('not an object');
340
+ if (typeof cfg.version !== 'number') throw new Error('missing version');
341
+ UI.success(`Valid: baldart.config.yml (schema v${cfg.version})`);
342
+ } catch (err) {
343
+ UI.warning(`baldart.config.yml is malformed (${err.message}). Re-run \`npx baldart configure\`.`);
344
+ allValid = false;
345
+ }
346
+ }
347
+
348
+ // Overlays dir (v3.0.0+)
349
+ const overlaysDir = path.join(this.cwd, '.baldart', 'overlays');
350
+ if (!fs.existsSync(overlaysDir)) {
351
+ UI.warning('Missing: .baldart/overlays/ — run `npx baldart configure` to create it.');
352
+ allValid = false;
353
+ } else {
354
+ UI.success('Valid: .baldart/overlays/');
355
+ }
356
+
357
+ return allValid;
358
+ }
359
+
360
+ copyCustomizableFiles() {
361
+ UI.section('Copying Customizable Templates');
362
+
363
+ // Hooks
364
+ this.ensureDirectory('.claude/hooks');
365
+ this.copyFile(
366
+ path.join(FRAMEWORK_PAYLOAD, '.claude', 'hooks', 'lint-before-commit.sh.template'),
367
+ path.join('.claude', 'hooks', 'lint-before-commit.sh.template')
368
+ );
369
+
370
+ // UI guidelines
371
+ this.ensureDirectory('docs/references');
372
+ this.copyFile(
373
+ path.join(FRAMEWORK_PAYLOAD, 'docs', 'references', 'ui-guidelines.template.md'),
374
+ path.join('docs', 'references', 'ui-guidelines.template.md')
375
+ );
376
+
377
+ this.copyFile(
378
+ path.join(FRAMEWORK_PAYLOAD, 'docs', 'references', 'brand-guidelines.md'),
379
+ path.join('docs', 'references', 'brand-guidelines.md')
380
+ );
381
+
382
+ // Templates — only user-edit backlog/spec templates.
383
+ // Skip BALDART-internal templates (config schema, snippet, overlay examples)
384
+ // which live in `.framework/templates/` and are consumed by CLI commands,
385
+ // not by humans.
386
+ const FRAMEWORK_INTERNAL = new Set([
387
+ 'baldart.config.template.yml',
388
+ 'skill-project-context.snippet.md',
389
+ 'overlays', // example overlays directory — consumers reference, don't copy wholesale
390
+ ]);
391
+
392
+ this.ensureDirectory('templates');
393
+ const templatesDir = path.join(this.cwd, FRAMEWORK_PAYLOAD, 'templates');
394
+ if (fs.existsSync(templatesDir)) {
395
+ const templateFiles = fs.readdirSync(templatesDir);
396
+ templateFiles.forEach(file => {
397
+ if (FRAMEWORK_INTERNAL.has(file)) return;
398
+ const src = path.join(this.cwd, FRAMEWORK_PAYLOAD, 'templates', file);
399
+ if (fs.lstatSync(src).isDirectory()) return;
400
+ this.copyFile(
401
+ path.join(FRAMEWORK_PAYLOAD, 'templates', file),
402
+ path.join('templates', file)
403
+ );
404
+ });
405
+ }
406
+
407
+ // Ensure consumer-owned overlay directory exists (real dir, never a symlink).
408
+ // The configure command also creates this; this branch covers installs that
409
+ // skip configure.
410
+ const overlaysDir = path.join(this.cwd, '.baldart', 'overlays');
411
+ if (!fs.existsSync(overlaysDir)) {
412
+ fs.mkdirSync(overlaysDir, { recursive: true });
413
+ fs.writeFileSync(
414
+ path.join(overlaysDir, '.gitkeep'),
415
+ '# Skill overlays — see framework/agents/project-context.md § 5\n',
416
+ 'utf8'
417
+ );
418
+ UI.success('Created .baldart/overlays/');
419
+ }
420
+
421
+ UI.newline();
422
+ }
423
+ }
424
+
425
+ module.exports = SymlinkUtils;
@@ -0,0 +1,133 @@
1
+ const chalk = require('chalk');
2
+ const ora = require('ora');
3
+ const inquirer = require('inquirer');
4
+
5
+ class UI {
6
+ static header(text) {
7
+ console.log('');
8
+ console.log(chalk.cyan('━'.repeat(60)));
9
+ console.log(chalk.cyan.bold(text));
10
+ console.log(chalk.cyan('━'.repeat(60)));
11
+ console.log('');
12
+ }
13
+
14
+ static step(number, text) {
15
+ console.log(chalk.green(`✓ STEP ${number}:`) + ` ${text}`);
16
+ }
17
+
18
+ static info(text) {
19
+ console.log(chalk.blue('→') + ` ${text}`);
20
+ }
21
+
22
+ static success(text) {
23
+ console.log(chalk.green('✓') + ` ${text}`);
24
+ }
25
+
26
+ static warning(text) {
27
+ console.log(chalk.yellow('⚠') + ` ${text}`);
28
+ }
29
+
30
+ static error(text) {
31
+ console.log(chalk.red('✗') + ` ${text}`);
32
+ }
33
+
34
+ static spinner(text) {
35
+ return ora({
36
+ text,
37
+ color: 'cyan'
38
+ });
39
+ }
40
+
41
+ static async confirm(message, defaultValue = true) {
42
+ const answers = await inquirer.prompt([
43
+ {
44
+ type: 'confirm',
45
+ name: 'confirmed',
46
+ message,
47
+ default: defaultValue
48
+ }
49
+ ]);
50
+ return answers.confirmed;
51
+ }
52
+
53
+ static async select(message, choices) {
54
+ const answers = await inquirer.prompt([
55
+ {
56
+ type: 'list',
57
+ name: 'selected',
58
+ message,
59
+ choices
60
+ }
61
+ ]);
62
+ return answers.selected;
63
+ }
64
+
65
+ static async input(message, defaultValue = '') {
66
+ const answers = await inquirer.prompt([
67
+ {
68
+ type: 'input',
69
+ name: 'value',
70
+ message,
71
+ default: defaultValue
72
+ }
73
+ ]);
74
+ return answers.value;
75
+ }
76
+
77
+ static box(title, lines) {
78
+ console.log('');
79
+ console.log(chalk.cyan('╔' + '═'.repeat(58) + '╗'));
80
+ console.log(chalk.cyan('║') + chalk.bold(` ${title}`.padEnd(58)) + chalk.cyan('║'));
81
+ console.log(chalk.cyan('╠' + '═'.repeat(58) + '╣'));
82
+
83
+ lines.forEach(line => {
84
+ // Wrap long lines
85
+ const maxWidth = 56;
86
+ const chunks = [];
87
+ let current = '';
88
+
89
+ line.split(' ').forEach(word => {
90
+ if ((current + ' ' + word).length <= maxWidth) {
91
+ current += (current ? ' ' : '') + word;
92
+ } else {
93
+ if (current) chunks.push(current);
94
+ current = word;
95
+ }
96
+ });
97
+ if (current) chunks.push(current);
98
+
99
+ chunks.forEach(chunk => {
100
+ console.log(chalk.cyan('║') + ` ${chunk}`.padEnd(58) + chalk.cyan('║'));
101
+ });
102
+ });
103
+
104
+ console.log(chalk.cyan('╚' + '═'.repeat(58) + '╝'));
105
+ console.log('');
106
+ }
107
+
108
+ static section(title) {
109
+ console.log('');
110
+ console.log(chalk.bold.white(title));
111
+ console.log(chalk.gray('─'.repeat(title.length)));
112
+ }
113
+
114
+ static code(command, description) {
115
+ console.log('');
116
+ console.log(chalk.cyan('Command:'), chalk.white(command));
117
+ if (description) {
118
+ console.log(chalk.gray(description));
119
+ }
120
+ }
121
+
122
+ static list(items, color = 'white') {
123
+ items.forEach(item => {
124
+ console.log(chalk[color](' •'), item);
125
+ });
126
+ }
127
+
128
+ static newline() {
129
+ console.log('');
130
+ }
131
+ }
132
+
133
+ module.exports = UI;