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.
- package/CHANGELOG.md +599 -0
- package/README.md +566 -0
- package/VERSION +1 -0
- package/bin/baldart.js +143 -0
- package/framework/.claude/agents/REGISTRY.md +169 -0
- package/framework/.claude/agents/api-perf-cost-auditor.md +291 -0
- package/framework/.claude/agents/code-reviewer.md +350 -0
- package/framework/.claude/agents/codebase-architect.md +391 -0
- package/framework/.claude/agents/coder.md +291 -0
- package/framework/.claude/agents/deep-human-insight.md +198 -0
- package/framework/.claude/agents/doc-reviewer.md +440 -0
- package/framework/.claude/agents/email-deliverability-architect.md +193 -0
- package/framework/.claude/agents/hybrid-ml-architect.md +285 -0
- package/framework/.claude/agents/hyper-gamification-designer.md +149 -0
- package/framework/.claude/agents/legal-counsel-gdpr.md +179 -0
- package/framework/.claude/agents/marketing-conversion-strategist.md +162 -0
- package/framework/.claude/agents/motion-expert.md +108 -0
- package/framework/.claude/agents/onboarding-architect-lead.md +230 -0
- package/framework/.claude/agents/plan-auditor.md +546 -0
- package/framework/.claude/agents/prd-card-writer.md +372 -0
- package/framework/.claude/agents/prd.md +744 -0
- package/framework/.claude/agents/qa-sentinel.md +305 -0
- package/framework/.claude/agents/remotion-animator-orchestrator.md +218 -0
- package/framework/.claude/agents/security-reviewer.md +276 -0
- package/framework/.claude/agents/senior-researcher.md +175 -0
- package/framework/.claude/agents/seo-analytics-strategist.md +156 -0
- package/framework/.claude/agents/skill-improver.md +61 -0
- package/framework/.claude/agents/ui-expert.md +191 -0
- package/framework/.claude/agents/visual-designer.md +190 -0
- package/framework/.claude/agents/website-orchestrator.md +118 -0
- package/framework/.claude/agents/wiki-curator.md +145 -0
- package/framework/.claude/commands/baldart-push.md +15 -0
- package/framework/.claude/commands/check.md +237 -0
- package/framework/.claude/commands/codexreview.md +203 -0
- package/framework/.claude/commands/design-review.md +11 -0
- package/framework/.claude/commands/issue-review.md +34 -0
- package/framework/.claude/commands/new.md +331 -0
- package/framework/.claude/commands/qa.md +257 -0
- package/framework/.claude/hooks/framework-edit-gate.js +208 -0
- package/framework/.claude/hooks/lint-before-commit.sh.template +66 -0
- package/framework/.claude/settings.local.json.example +32 -0
- package/framework/.claude/skills/api-design-principles/SKILL.md +567 -0
- package/framework/.claude/skills/api-design-principles/assets/api-design-checklist.md +155 -0
- package/framework/.claude/skills/api-design-principles/assets/rest-api-template.py +182 -0
- package/framework/.claude/skills/api-design-principles/references/graphql-schema-design.md +583 -0
- package/framework/.claude/skills/api-design-principles/references/rest-best-practices.md +408 -0
- package/framework/.claude/skills/baldart-push/SKILL.md +222 -0
- package/framework/.claude/skills/bug/SKILL.md +200 -0
- package/framework/.claude/skills/bug/references/logging-patterns.md +174 -0
- package/framework/.claude/skills/capture/SKILL.md +125 -0
- package/framework/.claude/skills/capture/references/synthesis-template.md +42 -0
- package/framework/.claude/skills/context-primer/SKILL.md +189 -0
- package/framework/.claude/skills/copywriting/SKILL.md +273 -0
- package/framework/.claude/skills/copywriting/references/copy-frameworks.md +338 -0
- package/framework/.claude/skills/copywriting/references/natural-transitions.md +252 -0
- package/framework/.claude/skills/doc-writing-for-rag/SKILL.md +119 -0
- package/framework/.claude/skills/doc-writing-for-rag/references/before-after-examples.md +291 -0
- package/framework/.claude/skills/doc-writing-for-rag/references/compact-templates.md +183 -0
- package/framework/.claude/skills/doc-writing-for-rag/references/frontmatter-minimal.md +112 -0
- package/framework/.claude/skills/doc-writing-for-rag/references/line-count-targets.md +110 -0
- package/framework/.claude/skills/doc-writing-for-rag/references/schemas-and-errors.md +129 -0
- package/framework/.claude/skills/find-skills/SKILL.md +133 -0
- package/framework/.claude/skills/frontend-design/LICENSE.txt +177 -0
- package/framework/.claude/skills/frontend-design/SKILL.md +84 -0
- package/framework/.claude/skills/gamification-design/SKILL.md +130 -0
- package/framework/.claude/skills/issue-review/SKILL.md +45 -0
- package/framework/.claude/skills/kie-ai/SKILL.md +262 -0
- package/framework/.claude/skills/kie-ai/references/models-catalog.md +272 -0
- package/framework/.claude/skills/kie-ai/scripts/kie_api.sh +209 -0
- package/framework/.claude/skills/kie-ai/scripts/remove_greenscreen.py +69 -0
- package/framework/.claude/skills/kie-ai/scripts/setup_api_key.sh +77 -0
- package/framework/.claude/skills/motion-design/LICENSE +21 -0
- package/framework/.claude/skills/motion-design/README.md +82 -0
- package/framework/.claude/skills/motion-design/SKILL.md +336 -0
- package/framework/.claude/skills/motion-design/director/choreography.md +93 -0
- package/framework/.claude/skills/motion-design/director/context-adaptation.md +83 -0
- package/framework/.claude/skills/motion-design/director/core-philosophy.md +53 -0
- package/framework/.claude/skills/motion-design/director/decision-framework.md +91 -0
- package/framework/.claude/skills/motion-design/director/disney-principles.md +102 -0
- package/framework/.claude/skills/motion-design/director/emotion-mapping.md +71 -0
- package/framework/.claude/skills/motion-design/director/motion-personality.md +89 -0
- package/framework/.claude/skills/motion-design/director/narrative-structure.md +62 -0
- package/framework/.claude/skills/motion-design/patterns/ambient-continuous.md +81 -0
- package/framework/.claude/skills/motion-design/patterns/entrance-exit.md +82 -0
- package/framework/.claude/skills/motion-design/patterns/multi-element.md +69 -0
- package/framework/.claude/skills/motion-design/patterns/state-feedback.md +96 -0
- package/framework/.claude/skills/motion-design/reference/property-selection.md +95 -0
- package/framework/.claude/skills/motion-design/reference/quality-checklist.md +67 -0
- package/framework/.claude/skills/motion-design/reference/timing-easing-tables.md +106 -0
- package/framework/.claude/skills/motion-design/reference/troubleshooting.md +73 -0
- package/framework/.claude/skills/new/SKILL.md +1687 -0
- package/framework/.claude/skills/playwright-skill/API_REFERENCE.md +652 -0
- package/framework/.claude/skills/playwright-skill/SKILL.md +157 -0
- package/framework/.claude/skills/playwright-skill/package.json +26 -0
- package/framework/.claude/skills/prd/SKILL.md +228 -0
- package/framework/.claude/skills/prd/assets/card-template.yml +232 -0
- package/framework/.claude/skills/prd/assets/epic-template.yml +190 -0
- package/framework/.claude/skills/prd/assets/prd-template.md +230 -0
- package/framework/.claude/skills/prd/assets/state-template.md +78 -0
- package/framework/.claude/skills/prd/references/api-perf-gate.md +152 -0
- package/framework/.claude/skills/prd/references/audit-phase.md +478 -0
- package/framework/.claude/skills/prd/references/backlog-phase.md +145 -0
- package/framework/.claude/skills/prd/references/discovery-phase.md +359 -0
- package/framework/.claude/skills/prd/references/impact-analysis.md +233 -0
- package/framework/.claude/skills/prd/references/prd-add-phase.md +214 -0
- package/framework/.claude/skills/prd/references/prd-writing-phase.md +145 -0
- package/framework/.claude/skills/prd/references/research-phase.md +216 -0
- package/framework/.claude/skills/prd/references/ui-design-phase.md +61 -0
- package/framework/.claude/skills/prd/references/validation-phase.md +72 -0
- package/framework/.claude/skills/prd-add/SKILL.md +222 -0
- package/framework/.claude/skills/prd-add/references/impact-analysis.md +233 -0
- package/framework/.claude/skills/remotion-best-practices/SKILL.md +48 -0
- package/framework/.claude/skills/remotion-best-practices/rules/3d.md +86 -0
- package/framework/.claude/skills/remotion-best-practices/rules/animations.md +29 -0
- package/framework/.claude/skills/remotion-best-practices/rules/assets/charts-bar-chart.tsx +173 -0
- package/framework/.claude/skills/remotion-best-practices/rules/assets/text-animations-typewriter.tsx +100 -0
- package/framework/.claude/skills/remotion-best-practices/rules/assets/text-animations-word-highlight.tsx +108 -0
- package/framework/.claude/skills/remotion-best-practices/rules/assets.md +78 -0
- package/framework/.claude/skills/remotion-best-practices/rules/audio.md +169 -0
- package/framework/.claude/skills/remotion-best-practices/rules/calculate-metadata.md +104 -0
- package/framework/.claude/skills/remotion-best-practices/rules/can-decode.md +75 -0
- package/framework/.claude/skills/remotion-best-practices/rules/charts.md +58 -0
- package/framework/.claude/skills/remotion-best-practices/rules/compositions.md +141 -0
- package/framework/.claude/skills/remotion-best-practices/rules/display-captions.md +184 -0
- package/framework/.claude/skills/remotion-best-practices/rules/extract-frames.md +229 -0
- package/framework/.claude/skills/remotion-best-practices/rules/fonts.md +152 -0
- package/framework/.claude/skills/remotion-best-practices/rules/get-audio-duration.md +58 -0
- package/framework/.claude/skills/remotion-best-practices/rules/get-video-dimensions.md +68 -0
- package/framework/.claude/skills/remotion-best-practices/rules/get-video-duration.md +58 -0
- package/framework/.claude/skills/remotion-best-practices/rules/gifs.md +141 -0
- package/framework/.claude/skills/remotion-best-practices/rules/images.md +130 -0
- package/framework/.claude/skills/remotion-best-practices/rules/import-srt-captions.md +69 -0
- package/framework/.claude/skills/remotion-best-practices/rules/light-leaks.md +73 -0
- package/framework/.claude/skills/remotion-best-practices/rules/lottie.md +67 -0
- package/framework/.claude/skills/remotion-best-practices/rules/maps.md +401 -0
- package/framework/.claude/skills/remotion-best-practices/rules/measuring-dom-nodes.md +34 -0
- package/framework/.claude/skills/remotion-best-practices/rules/measuring-text.md +143 -0
- package/framework/.claude/skills/remotion-best-practices/rules/parameters.md +98 -0
- package/framework/.claude/skills/remotion-best-practices/rules/sequencing.md +118 -0
- package/framework/.claude/skills/remotion-best-practices/rules/subtitles.md +36 -0
- package/framework/.claude/skills/remotion-best-practices/rules/tailwind.md +11 -0
- package/framework/.claude/skills/remotion-best-practices/rules/text-animations.md +20 -0
- package/framework/.claude/skills/remotion-best-practices/rules/timing.md +179 -0
- package/framework/.claude/skills/remotion-best-practices/rules/transcribe-captions.md +70 -0
- package/framework/.claude/skills/remotion-best-practices/rules/transitions.md +197 -0
- package/framework/.claude/skills/remotion-best-practices/rules/transparent-videos.md +106 -0
- package/framework/.claude/skills/remotion-best-practices/rules/trimming.md +52 -0
- package/framework/.claude/skills/remotion-best-practices/rules/videos.md +171 -0
- package/framework/.claude/skills/seo-audit/SKILL.md +394 -0
- package/framework/.claude/skills/seo-audit/references/aeo-geo-patterns.md +279 -0
- package/framework/.claude/skills/seo-audit/references/ai-writing-detection.md +190 -0
- package/framework/.claude/skills/simplify/SKILL.md +137 -0
- package/framework/.claude/skills/skill-creator/LICENSE.txt +202 -0
- package/framework/.claude/skills/skill-creator/SKILL.md +356 -0
- package/framework/.claude/skills/skill-creator/references/output-patterns.md +82 -0
- package/framework/.claude/skills/skill-creator/references/workflows.md +28 -0
- package/framework/.claude/skills/skill-creator/scripts/init_skill.py +303 -0
- package/framework/.claude/skills/skill-creator/scripts/package_skill.py +110 -0
- package/framework/.claude/skills/skill-creator/scripts/quick_validate.py +95 -0
- package/framework/.claude/skills/ui-design/SKILL.md +199 -0
- package/framework/.claude/skills/ui-design/references/component-discovery.md +54 -0
- package/framework/.claude/skills/ui-design/references/evaluation.md +171 -0
- package/framework/.claude/skills/ui-design/references/generation.md +109 -0
- package/framework/.claude/skills/ui-design/references/inventory.md +59 -0
- package/framework/.claude/skills/webapp-testing/LICENSE.txt +202 -0
- package/framework/.claude/skills/webapp-testing/SKILL.md +123 -0
- package/framework/.claude/skills/webapp-testing/examples/console_logging.py +35 -0
- package/framework/.claude/skills/webapp-testing/examples/element_discovery.py +40 -0
- package/framework/.claude/skills/webapp-testing/examples/static_html_automation.py +33 -0
- package/framework/.claude/skills/webapp-testing/scripts/with_server.py +106 -0
- package/framework/.claude/skills/worktree-manager/SKILL.md +680 -0
- package/framework/AGENTS.md +240 -0
- package/framework/agents/api-contracts.md +137 -0
- package/framework/agents/architecture.md +145 -0
- package/framework/agents/coding-standards.md +148 -0
- package/framework/agents/data-model.md +110 -0
- package/framework/agents/deployment-protocol.md +232 -0
- package/framework/agents/design-review.md +172 -0
- package/framework/agents/env-reference.md +171 -0
- package/framework/agents/github-issue-subagent.md +252 -0
- package/framework/agents/index.md +261 -0
- package/framework/agents/llm-wiki-methodology.md +216 -0
- package/framework/agents/maintenance-protocol.md +305 -0
- package/framework/agents/observability.md +162 -0
- package/framework/agents/performance.md +155 -0
- package/framework/agents/project-context.md +145 -0
- package/framework/agents/runbook.md +208 -0
- package/framework/agents/security.md +168 -0
- package/framework/agents/skills-mapping.md +286 -0
- package/framework/agents/testing.md +111 -0
- package/framework/agents/workflows.md +215 -0
- package/framework/docs/PROJECT-CONFIGURATION.md +336 -0
- package/framework/docs/references/brand-guidelines.md +170 -0
- package/framework/docs/references/ui-guidelines.template.md +182 -0
- package/framework/routines/code-review.routine.yml +46 -0
- package/framework/routines/doc-review.routine.yml +45 -0
- package/framework/routines/ds-drift.routine.yml +52 -0
- package/framework/routines/full-sweep.routine.yml +51 -0
- package/framework/routines/index.yml +70 -0
- package/framework/routines/skill-improve.routine.yml +50 -0
- package/framework/routines/wiki-review.routine.yml +45 -0
- package/framework/templates/baldart.config.template.yml +113 -0
- package/framework/templates/breaking-change-checklist.md +484 -0
- package/framework/templates/feature-card.template.yml +125 -0
- package/framework/templates/overlays/README.md +44 -0
- package/framework/templates/overlays/copywriting.fidelity-example.md +62 -0
- package/framework/templates/overlays/ui-design.fidelity-example.md +75 -0
- package/framework/templates/skill-project-context.snippet.md +19 -0
- package/framework/templates/spec.template.md +208 -0
- package/package.json +51 -0
- package/src/commands/add.js +229 -0
- package/src/commands/configure.js +385 -0
- package/src/commands/doctor.js +486 -0
- package/src/commands/migrate.js +185 -0
- package/src/commands/push.js +0 -0
- package/src/commands/routines.js +269 -0
- package/src/commands/status.js +130 -0
- package/src/commands/update.js +419 -0
- package/src/commands/version.js +88 -0
- package/src/utils/contamination.js +400 -0
- package/src/utils/git.js +181 -0
- package/src/utils/hooks.js +152 -0
- package/src/utils/routine-adapters/claude-code-cloud.js +78 -0
- package/src/utils/routine-adapters/cron.js +138 -0
- package/src/utils/routine-adapters/github-actions.js +141 -0
- package/src/utils/routine-adapters/index.js +21 -0
- package/src/utils/routines.js +166 -0
- package/src/utils/state.js +143 -0
- package/src/utils/symlinks.js +425 -0
- 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;
|
package/src/utils/ui.js
ADDED
|
@@ -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;
|