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,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consumer-side hook registration for `.claude/settings.json`.
|
|
3
|
+
*
|
|
4
|
+
* BALDART ships a Claude Code PreToolUse hook (`framework-edit-gate`) that
|
|
5
|
+
* intercepts Edit / Write / MultiEdit calls targeting files inside
|
|
6
|
+
* `.framework/` and blocks them with an informative reason if the new
|
|
7
|
+
* content contains project-specific tokens (Neo-Brutalism, merchant,
|
|
8
|
+
* Recharts, secrets, hardcoded paths, …).
|
|
9
|
+
*
|
|
10
|
+
* Registration is auto-handled by `baldart add` and re-checked by
|
|
11
|
+
* `baldart update` / `baldart doctor`. The user can remove the entry from
|
|
12
|
+
* `.claude/settings.json` if they want to disable it.
|
|
13
|
+
*
|
|
14
|
+
* Schema: the hook config under `hooks.PreToolUse[]` follows Claude Code's
|
|
15
|
+
* documented hook format. Each entry has a `matcher` (tool name regex) and
|
|
16
|
+
* an inner `hooks[]` array of `{ type: "command", command: <shell> }`.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
|
|
22
|
+
const SETTINGS_FILE = path.join('.claude', 'settings.json');
|
|
23
|
+
const HOOK_ID = 'baldart-framework-edit-gate';
|
|
24
|
+
const HOOK_COMMAND = 'node .framework/framework/.claude/hooks/framework-edit-gate.js';
|
|
25
|
+
const HOOK_MATCHER = 'Edit|Write|MultiEdit|NotebookEdit';
|
|
26
|
+
|
|
27
|
+
function readSettings(cwd = process.cwd()) {
|
|
28
|
+
const full = path.join(cwd, SETTINGS_FILE);
|
|
29
|
+
if (!fs.existsSync(full)) return { path: full, settings: null, malformed: false };
|
|
30
|
+
try {
|
|
31
|
+
const raw = fs.readFileSync(full, 'utf8');
|
|
32
|
+
if (!raw.trim()) return { path: full, settings: {}, malformed: false };
|
|
33
|
+
return { path: full, settings: JSON.parse(raw), malformed: false };
|
|
34
|
+
} catch (_) {
|
|
35
|
+
return { path: full, settings: null, malformed: true };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function writeSettings(settingsPath, settings) {
|
|
40
|
+
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
|
|
41
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Returns true iff the framework-edit-gate hook is already registered in
|
|
46
|
+
* `.claude/settings.json` (matching either by command string or by the
|
|
47
|
+
* HOOK_ID marker we plant in a sibling property).
|
|
48
|
+
*/
|
|
49
|
+
function isRegistered(cwd = process.cwd()) {
|
|
50
|
+
const { settings } = readSettings(cwd);
|
|
51
|
+
if (!settings || !settings.hooks || !Array.isArray(settings.hooks.PreToolUse)) return false;
|
|
52
|
+
for (const entry of settings.hooks.PreToolUse) {
|
|
53
|
+
if (!entry || !Array.isArray(entry.hooks)) continue;
|
|
54
|
+
for (const h of entry.hooks) {
|
|
55
|
+
if (!h) continue;
|
|
56
|
+
if (h.id === HOOK_ID) return true;
|
|
57
|
+
if (h.type === 'command' && typeof h.command === 'string') {
|
|
58
|
+
if (h.command.includes('framework-edit-gate.js')) return true;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Register the hook idempotently. Returns:
|
|
67
|
+
* { status: 'created', path } — settings.json created from scratch
|
|
68
|
+
* { status: 'updated', path } — existing settings.json merged
|
|
69
|
+
* { status: 'already', path } — hook was already registered, no-op
|
|
70
|
+
* { status: 'malformed', path } — existing settings.json is invalid JSON
|
|
71
|
+
*/
|
|
72
|
+
function register(cwd = process.cwd()) {
|
|
73
|
+
const { path: settingsPath, settings: existing, malformed } = readSettings(cwd);
|
|
74
|
+
if (malformed) return { status: 'malformed', path: settingsPath };
|
|
75
|
+
|
|
76
|
+
const settings = existing && typeof existing === 'object' ? { ...existing } : {};
|
|
77
|
+
settings.hooks = settings.hooks && typeof settings.hooks === 'object'
|
|
78
|
+
? { ...settings.hooks }
|
|
79
|
+
: {};
|
|
80
|
+
|
|
81
|
+
const list = Array.isArray(settings.hooks.PreToolUse) ? [...settings.hooks.PreToolUse] : [];
|
|
82
|
+
|
|
83
|
+
// Already present?
|
|
84
|
+
for (const entry of list) {
|
|
85
|
+
if (!entry || !Array.isArray(entry.hooks)) continue;
|
|
86
|
+
for (const h of entry.hooks) {
|
|
87
|
+
if (!h) continue;
|
|
88
|
+
if (h.id === HOOK_ID) return { status: 'already', path: settingsPath };
|
|
89
|
+
if (h.type === 'command' && typeof h.command === 'string'
|
|
90
|
+
&& h.command.includes('framework-edit-gate.js')) {
|
|
91
|
+
return { status: 'already', path: settingsPath };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
list.push({
|
|
97
|
+
matcher: HOOK_MATCHER,
|
|
98
|
+
hooks: [{
|
|
99
|
+
id: HOOK_ID,
|
|
100
|
+
type: 'command',
|
|
101
|
+
command: HOOK_COMMAND,
|
|
102
|
+
}],
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
settings.hooks.PreToolUse = list;
|
|
106
|
+
writeSettings(settingsPath, settings);
|
|
107
|
+
return { status: existing ? 'updated' : 'created', path: settingsPath };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Remove the hook from settings.json. Used by tests and as an undo helper.
|
|
112
|
+
*/
|
|
113
|
+
function unregister(cwd = process.cwd()) {
|
|
114
|
+
const { path: settingsPath, settings, malformed } = readSettings(cwd);
|
|
115
|
+
if (malformed || !settings || !settings.hooks || !Array.isArray(settings.hooks.PreToolUse)) {
|
|
116
|
+
return { status: 'not-found', path: settingsPath };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const filtered = settings.hooks.PreToolUse
|
|
120
|
+
.map((entry) => {
|
|
121
|
+
if (!entry || !Array.isArray(entry.hooks)) return entry;
|
|
122
|
+
const innerKept = entry.hooks.filter((h) => {
|
|
123
|
+
if (!h) return true;
|
|
124
|
+
if (h.id === HOOK_ID) return false;
|
|
125
|
+
if (h.type === 'command' && typeof h.command === 'string'
|
|
126
|
+
&& h.command.includes('framework-edit-gate.js')) return false;
|
|
127
|
+
return true;
|
|
128
|
+
});
|
|
129
|
+
if (innerKept.length === 0) return null;
|
|
130
|
+
return { ...entry, hooks: innerKept };
|
|
131
|
+
})
|
|
132
|
+
.filter(Boolean);
|
|
133
|
+
|
|
134
|
+
if (filtered.length === settings.hooks.PreToolUse.length) {
|
|
135
|
+
return { status: 'not-found', path: settingsPath };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const next = { ...settings, hooks: { ...settings.hooks, PreToolUse: filtered } };
|
|
139
|
+
if (next.hooks.PreToolUse.length === 0) delete next.hooks.PreToolUse;
|
|
140
|
+
if (Object.keys(next.hooks).length === 0) delete next.hooks;
|
|
141
|
+
writeSettings(settingsPath, next);
|
|
142
|
+
return { status: 'removed', path: settingsPath };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
module.exports = {
|
|
146
|
+
isRegistered,
|
|
147
|
+
register,
|
|
148
|
+
unregister,
|
|
149
|
+
HOOK_ID,
|
|
150
|
+
HOOK_COMMAND,
|
|
151
|
+
SETTINGS_FILE,
|
|
152
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Claude Code Cloud adapter.
|
|
6
|
+
*
|
|
7
|
+
* Generates a routine config file under `.claude/routines/<name>.json` that
|
|
8
|
+
* Claude Code Cloud can pick up via `/schedule` or the RemoteTrigger mechanism.
|
|
9
|
+
*
|
|
10
|
+
* The actual remote trigger creation is interactive (user must run `/schedule`
|
|
11
|
+
* inside Claude Code). The adapter writes the config and prints the exact
|
|
12
|
+
* command the user needs to run.
|
|
13
|
+
*/
|
|
14
|
+
class ClaudeCodeCloudAdapter {
|
|
15
|
+
constructor(cwd = process.cwd()) {
|
|
16
|
+
this.cwd = cwd;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
get name() { return 'claude-code-cloud'; }
|
|
20
|
+
get label() { return 'Claude Code Cloud (RemoteTrigger)'; }
|
|
21
|
+
get description() {
|
|
22
|
+
return 'Schedules the agent via Claude Code Cloud RemoteTrigger. Best when you already use Claude Code Cloud.';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
configDir() {
|
|
26
|
+
return path.join(this.cwd, '.claude', 'routines');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
configPath(name) {
|
|
30
|
+
return path.join(this.configDir(), `${name}.json`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
install(spec) {
|
|
34
|
+
if (!fs.existsSync(this.configDir())) {
|
|
35
|
+
fs.mkdirSync(this.configDir(), { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
const config = {
|
|
38
|
+
$schema: 'baldart-routine-config-v1',
|
|
39
|
+
name: spec.name,
|
|
40
|
+
description: spec.description,
|
|
41
|
+
schedule: spec.schedule,
|
|
42
|
+
agent: spec.agent,
|
|
43
|
+
prompt: spec.prompt,
|
|
44
|
+
output: spec.output,
|
|
45
|
+
backend: this.name,
|
|
46
|
+
installed_at: new Date().toISOString()
|
|
47
|
+
};
|
|
48
|
+
fs.writeFileSync(this.configPath(spec.name), JSON.stringify(config, null, 2) + '\n');
|
|
49
|
+
return {
|
|
50
|
+
backend: this.name,
|
|
51
|
+
artifacts: [path.relative(this.cwd, this.configPath(spec.name))],
|
|
52
|
+
followup: [
|
|
53
|
+
`Open Claude Code in this repo and run:`,
|
|
54
|
+
` /schedule create --config .claude/routines/${spec.name}.json`,
|
|
55
|
+
`(or invoke the agent once with /loop ${spec.schedule.cron} <command> to test the cadence)`
|
|
56
|
+
]
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
uninstall(name) {
|
|
61
|
+
const p = this.configPath(name);
|
|
62
|
+
if (fs.existsSync(p)) fs.unlinkSync(p);
|
|
63
|
+
return {
|
|
64
|
+
backend: this.name,
|
|
65
|
+
followup: [
|
|
66
|
+
`Open Claude Code and run:`,
|
|
67
|
+
` /schedule delete ${name}`,
|
|
68
|
+
`to remove the remote trigger.`
|
|
69
|
+
]
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
detect(name) {
|
|
74
|
+
return fs.existsSync(this.configPath(name));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = ClaudeCodeCloudAdapter;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Local cron adapter.
|
|
6
|
+
*
|
|
7
|
+
* Generates `scripts/routines/<name>.sh` (a self-contained wrapper) and prints
|
|
8
|
+
* the exact crontab line the user must install. Does NOT touch the user's
|
|
9
|
+
* crontab directly — that would be too invasive.
|
|
10
|
+
*
|
|
11
|
+
* Requires the `claude` CLI to be available on the user's PATH.
|
|
12
|
+
*/
|
|
13
|
+
class CronAdapter {
|
|
14
|
+
constructor(cwd = process.cwd()) {
|
|
15
|
+
this.cwd = cwd;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
get name() { return 'cron'; }
|
|
19
|
+
get label() { return 'Local cron (shell wrapper + crontab line)'; }
|
|
20
|
+
get description() {
|
|
21
|
+
return 'Generates a shell wrapper. You add a single line to your crontab. Best for self-hosted runs.';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
scriptDir() {
|
|
25
|
+
return path.join(this.cwd, 'scripts', 'routines');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
scriptPath(name) {
|
|
29
|
+
return path.join(this.scriptDir(), `${name}.sh`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
install(spec) {
|
|
33
|
+
if (!fs.existsSync(this.scriptDir())) {
|
|
34
|
+
fs.mkdirSync(this.scriptDir(), { recursive: true });
|
|
35
|
+
}
|
|
36
|
+
const sh = this._renderShellWrapper(spec);
|
|
37
|
+
fs.writeFileSync(this.scriptPath(spec.name), sh);
|
|
38
|
+
fs.chmodSync(this.scriptPath(spec.name), 0o755);
|
|
39
|
+
|
|
40
|
+
const absPath = path.resolve(this.cwd, this.scriptPath(spec.name));
|
|
41
|
+
const crontabLine = `${spec.schedule.cron} ${absPath} >> ${path.dirname(absPath)}/${spec.name}.log 2>&1`;
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
backend: this.name,
|
|
45
|
+
artifacts: [path.relative(this.cwd, this.scriptPath(spec.name))],
|
|
46
|
+
followup: [
|
|
47
|
+
`Add this line to your crontab (\`crontab -e\`):`,
|
|
48
|
+
``,
|
|
49
|
+
` ${crontabLine}`,
|
|
50
|
+
``,
|
|
51
|
+
`Make sure the \`claude\` CLI is on PATH for the cron user and that ANTHROPIC_API_KEY is exported in the script env (edit the wrapper to suit).`,
|
|
52
|
+
`Manual test: ${absPath}`
|
|
53
|
+
],
|
|
54
|
+
crontab_line: crontabLine
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
uninstall(name) {
|
|
59
|
+
const p = this.scriptPath(name);
|
|
60
|
+
if (fs.existsSync(p)) fs.unlinkSync(p);
|
|
61
|
+
return {
|
|
62
|
+
backend: this.name,
|
|
63
|
+
followup: [
|
|
64
|
+
`Remove the crontab line that referenced scripts/routines/${name}.sh.`,
|
|
65
|
+
`Run \`crontab -e\` and delete the matching entry.`
|
|
66
|
+
]
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
detect(name) {
|
|
71
|
+
return fs.existsSync(this.scriptPath(name));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
_renderShellWrapper(spec) {
|
|
75
|
+
const outputPath = (spec.output && spec.output.path) || `docs/reports/{{YYYYMMDD}}-${spec.name}.md`;
|
|
76
|
+
const commitEnabled = spec.output && spec.output.commit && spec.output.commit.enabled;
|
|
77
|
+
const commitPrefix = (spec.output && spec.output.commit && spec.output.commit.prefix) || `[${spec.name.toUpperCase()}]`;
|
|
78
|
+
const promptHeredoc = spec.prompt;
|
|
79
|
+
const repoRoot = '"$(cd "$(dirname "$0")/../.." && pwd)"';
|
|
80
|
+
|
|
81
|
+
return `#!/usr/bin/env bash
|
|
82
|
+
# Generated by BALDART (v${require('../../../package.json').version}) — npx baldart routines
|
|
83
|
+
# Routine: ${spec.name}
|
|
84
|
+
# ${spec.description}
|
|
85
|
+
#
|
|
86
|
+
# Schedule: ${spec.schedule.cron} (${spec.schedule.cadence_label || 'scheduled'} ${spec.schedule.timezone || 'UTC'})
|
|
87
|
+
#
|
|
88
|
+
# Add to crontab (\`crontab -e\`):
|
|
89
|
+
# ${spec.schedule.cron} ${'$(pwd)'}/scripts/routines/${spec.name}.sh >> ${'$(pwd)'}/scripts/routines/${spec.name}.log 2>&1
|
|
90
|
+
|
|
91
|
+
set -euo pipefail
|
|
92
|
+
|
|
93
|
+
# Resolve repo root and move into it so relative paths in the prompt work.
|
|
94
|
+
REPO_ROOT=${repoRoot}
|
|
95
|
+
cd "$REPO_ROOT"
|
|
96
|
+
|
|
97
|
+
# Required: \`claude\` CLI on PATH and ANTHROPIC_API_KEY set.
|
|
98
|
+
if ! command -v claude >/dev/null 2>&1; then
|
|
99
|
+
echo "ERROR: claude CLI not found on PATH" >&2
|
|
100
|
+
exit 1
|
|
101
|
+
fi
|
|
102
|
+
: "\${ANTHROPIC_API_KEY:?ANTHROPIC_API_KEY must be set (export it from the cron env or your shell profile)}"
|
|
103
|
+
|
|
104
|
+
DATE=$(date -u +%Y%m%d)
|
|
105
|
+
OUT_PATH="${outputPath}"
|
|
106
|
+
OUT_PATH="\${OUT_PATH//\\{\\{YYYYMMDD\\}\\}/$DATE}"
|
|
107
|
+
mkdir -p "$(dirname "$OUT_PATH")"
|
|
108
|
+
|
|
109
|
+
claude --print --output-format json --mode bypassPermissions \\
|
|
110
|
+
--agent ${spec.agent} \\
|
|
111
|
+
> /tmp/baldart-${spec.name}.json <<'PROMPT'
|
|
112
|
+
${promptHeredoc}
|
|
113
|
+
PROMPT
|
|
114
|
+
|
|
115
|
+
# Persist the output if the agent did not already write to disk
|
|
116
|
+
if [ ! -s "$OUT_PATH" ]; then
|
|
117
|
+
jq -r '.result // .' /tmp/baldart-${spec.name}.json > "$OUT_PATH" 2>/dev/null \\
|
|
118
|
+
|| cp /tmp/baldart-${spec.name}.json "$OUT_PATH"
|
|
119
|
+
fi
|
|
120
|
+
${commitEnabled ? `
|
|
121
|
+
# Auto-commit the report and any inline fixes
|
|
122
|
+
if git diff --quiet && git diff --staged --quiet; then
|
|
123
|
+
echo "No changes to commit."
|
|
124
|
+
else
|
|
125
|
+
git add -A
|
|
126
|
+
git commit -m "${commitPrefix} routine $(date -u +%Y-%m-%d)"
|
|
127
|
+
# Push only when on a branch with a configured upstream
|
|
128
|
+
if git rev-parse --abbrev-ref --symbolic-full-name '@{upstream}' >/dev/null 2>&1; then
|
|
129
|
+
git push
|
|
130
|
+
fi
|
|
131
|
+
fi
|
|
132
|
+
` : ''}
|
|
133
|
+
echo "OK: ${spec.name} finished at $(date -u)"
|
|
134
|
+
`;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
module.exports = CronAdapter;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* GitHub Actions adapter.
|
|
6
|
+
*
|
|
7
|
+
* Generates `.github/workflows/baldart-<name>.yml` with a `schedule` trigger
|
|
8
|
+
* and a `workflow_dispatch` trigger for manual runs. The workflow checks out
|
|
9
|
+
* the repo, sets up Node, installs Claude Code, and invokes the agent via
|
|
10
|
+
* the headless Claude CLI.
|
|
11
|
+
*
|
|
12
|
+
* Requires the user to set the `ANTHROPIC_API_KEY` secret in the repo.
|
|
13
|
+
*/
|
|
14
|
+
class GitHubActionsAdapter {
|
|
15
|
+
constructor(cwd = process.cwd()) {
|
|
16
|
+
this.cwd = cwd;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
get name() { return 'github-actions'; }
|
|
20
|
+
get label() { return 'GitHub Actions'; }
|
|
21
|
+
get description() {
|
|
22
|
+
return 'Schedules via a workflow with cron + workflow_dispatch. Requires the ANTHROPIC_API_KEY secret.';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
workflowDir() {
|
|
26
|
+
return path.join(this.cwd, '.github', 'workflows');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
workflowPath(name) {
|
|
30
|
+
return path.join(this.workflowDir(), `baldart-${name}.yml`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
install(spec) {
|
|
34
|
+
if (!fs.existsSync(this.workflowDir())) {
|
|
35
|
+
fs.mkdirSync(this.workflowDir(), { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
const yml = this._renderWorkflow(spec);
|
|
38
|
+
fs.writeFileSync(this.workflowPath(spec.name), yml);
|
|
39
|
+
return {
|
|
40
|
+
backend: this.name,
|
|
41
|
+
artifacts: [path.relative(this.cwd, this.workflowPath(spec.name))],
|
|
42
|
+
followup: [
|
|
43
|
+
`Set the repository secret ANTHROPIC_API_KEY in GitHub.`,
|
|
44
|
+
`Commit and push .github/workflows/baldart-${spec.name}.yml to enable the schedule.`,
|
|
45
|
+
`Manual test: gh workflow run baldart-${spec.name}.yml`
|
|
46
|
+
]
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
uninstall(name) {
|
|
51
|
+
const p = this.workflowPath(name);
|
|
52
|
+
if (fs.existsSync(p)) fs.unlinkSync(p);
|
|
53
|
+
return {
|
|
54
|
+
backend: this.name,
|
|
55
|
+
followup: [
|
|
56
|
+
`Commit the removal of .github/workflows/baldart-${name}.yml to disable the schedule.`
|
|
57
|
+
]
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
detect(name) {
|
|
62
|
+
return fs.existsSync(this.workflowPath(name));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
_renderWorkflow(spec) {
|
|
66
|
+
const cron = spec.schedule.cron;
|
|
67
|
+
const promptIndented = spec.prompt.split('\n').map(l => ' ' + l).join('\n');
|
|
68
|
+
const outputPath = (spec.output && spec.output.path) || `docs/reports/{{YYYYMMDD}}-${spec.name}.md`;
|
|
69
|
+
const commitEnabled = spec.output && spec.output.commit && spec.output.commit.enabled;
|
|
70
|
+
const commitPrefix = (spec.output && spec.output.commit && spec.output.commit.prefix) || `[${spec.name.toUpperCase()}]`;
|
|
71
|
+
|
|
72
|
+
return `# Generated by BALDART (v${require('../../../package.json').version}) — npx baldart routines
|
|
73
|
+
# Routine: ${spec.name}
|
|
74
|
+
# ${spec.description}
|
|
75
|
+
|
|
76
|
+
name: BALDART ${spec.name}
|
|
77
|
+
|
|
78
|
+
on:
|
|
79
|
+
schedule:
|
|
80
|
+
- cron: '${cron}' # ${spec.schedule.cadence_label || 'scheduled'}
|
|
81
|
+
workflow_dispatch: # manual trigger from the Actions tab
|
|
82
|
+
|
|
83
|
+
permissions:
|
|
84
|
+
contents: write
|
|
85
|
+
pull-requests: write
|
|
86
|
+
|
|
87
|
+
jobs:
|
|
88
|
+
run:
|
|
89
|
+
runs-on: ubuntu-latest
|
|
90
|
+
timeout-minutes: 60
|
|
91
|
+
steps:
|
|
92
|
+
- name: Checkout
|
|
93
|
+
uses: actions/checkout@v4
|
|
94
|
+
with:
|
|
95
|
+
fetch-depth: 0
|
|
96
|
+
|
|
97
|
+
- name: Setup Node
|
|
98
|
+
uses: actions/setup-node@v4
|
|
99
|
+
with:
|
|
100
|
+
node-version: '20'
|
|
101
|
+
|
|
102
|
+
- name: Install Claude Code CLI
|
|
103
|
+
run: npm install -g @anthropic-ai/claude-code
|
|
104
|
+
|
|
105
|
+
- name: Run ${spec.name}
|
|
106
|
+
env:
|
|
107
|
+
ANTHROPIC_API_KEY: \${{ secrets.ANTHROPIC_API_KEY }}
|
|
108
|
+
run: |
|
|
109
|
+
DATE=$(date -u +%Y%m%d)
|
|
110
|
+
OUT_PATH="${outputPath}"
|
|
111
|
+
OUT_PATH="\${OUT_PATH//\\{\\{YYYYMMDD\\}\\}/$DATE}"
|
|
112
|
+
mkdir -p "$(dirname "$OUT_PATH")"
|
|
113
|
+
|
|
114
|
+
claude --print --output-format json --mode bypassPermissions \\
|
|
115
|
+
--agent ${spec.agent} \\
|
|
116
|
+
> /tmp/claude-output.json <<'PROMPT'
|
|
117
|
+
${promptIndented}
|
|
118
|
+
PROMPT
|
|
119
|
+
|
|
120
|
+
# Persist the output if the agent did not already write to disk
|
|
121
|
+
if [ ! -s "$OUT_PATH" ]; then
|
|
122
|
+
jq -r '.result // .' /tmp/claude-output.json > "$OUT_PATH" || \\
|
|
123
|
+
cp /tmp/claude-output.json "$OUT_PATH"
|
|
124
|
+
fi
|
|
125
|
+
${commitEnabled ? `
|
|
126
|
+
- name: Commit report
|
|
127
|
+
run: |
|
|
128
|
+
git config user.name "baldart-routine"
|
|
129
|
+
git config user.email "baldart-routine@users.noreply.github.com"
|
|
130
|
+
git add -A
|
|
131
|
+
if ! git diff --staged --quiet; then
|
|
132
|
+
git commit -m "${commitPrefix} \${{ github.workflow }} run \$(date -u +%Y-%m-%d)"
|
|
133
|
+
git push
|
|
134
|
+
else
|
|
135
|
+
echo "No changes to commit."
|
|
136
|
+
fi
|
|
137
|
+
` : ''}`;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
module.exports = GitHubActionsAdapter;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const ClaudeCodeCloudAdapter = require('./claude-code-cloud');
|
|
2
|
+
const GitHubActionsAdapter = require('./github-actions');
|
|
3
|
+
const CronAdapter = require('./cron');
|
|
4
|
+
|
|
5
|
+
const REGISTRY = {
|
|
6
|
+
'claude-code-cloud': ClaudeCodeCloudAdapter,
|
|
7
|
+
'github-actions': GitHubActionsAdapter,
|
|
8
|
+
'cron': CronAdapter
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function listAdapters() {
|
|
12
|
+
return Object.keys(REGISTRY);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getAdapter(name, cwd) {
|
|
16
|
+
const Cls = REGISTRY[name];
|
|
17
|
+
if (!Cls) throw new Error(`Unknown adapter: ${name}. Available: ${listAdapters().join(', ')}`);
|
|
18
|
+
return new Cls(cwd);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = { listAdapters, getAdapter, REGISTRY };
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const yaml = require('js-yaml');
|
|
4
|
+
|
|
5
|
+
const FRAMEWORK_DIR = '.framework';
|
|
6
|
+
const ROUTINES_SUBPATH = path.join('framework', 'routines');
|
|
7
|
+
const LOCK_DIR = '.baldart';
|
|
8
|
+
const LOCK_FILE = 'routines.lock.json';
|
|
9
|
+
|
|
10
|
+
class Routines {
|
|
11
|
+
constructor(cwd = process.cwd()) {
|
|
12
|
+
this.cwd = cwd;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// -------- Catalog reading ------------------------------------------------
|
|
16
|
+
|
|
17
|
+
routinesDir() {
|
|
18
|
+
return path.join(this.cwd, FRAMEWORK_DIR, ROUTINES_SUBPATH);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
catalogPath() {
|
|
22
|
+
return path.join(this.routinesDir(), 'index.yml');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Read the routines catalog (index.yml). Returns {version, routines: []} or null. */
|
|
26
|
+
readCatalog() {
|
|
27
|
+
const p = this.catalogPath();
|
|
28
|
+
if (!fs.existsSync(p)) return null;
|
|
29
|
+
try {
|
|
30
|
+
return yaml.load(fs.readFileSync(p, 'utf8'));
|
|
31
|
+
} catch (err) {
|
|
32
|
+
throw new Error(`Failed to parse ${p}: ${err.message}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Read a single routine spec by name. Throws if missing or malformed. */
|
|
37
|
+
readSpec(name) {
|
|
38
|
+
const catalog = this.readCatalog();
|
|
39
|
+
if (!catalog) throw new Error('Routines catalog not found. Is BALDART installed?');
|
|
40
|
+
const entry = (catalog.routines || []).find(r => r.name === name);
|
|
41
|
+
if (!entry) throw new Error(`Unknown routine: ${name}`);
|
|
42
|
+
const specPath = path.join(this.routinesDir(), entry.file);
|
|
43
|
+
if (!fs.existsSync(specPath)) {
|
|
44
|
+
throw new Error(`Routine spec missing on disk: ${specPath}`);
|
|
45
|
+
}
|
|
46
|
+
const spec = yaml.load(fs.readFileSync(specPath, 'utf8'));
|
|
47
|
+
spec._catalogEntry = entry;
|
|
48
|
+
spec._specPath = specPath;
|
|
49
|
+
return spec;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Return the full list of available routines (catalog entries enriched with spec). */
|
|
53
|
+
list() {
|
|
54
|
+
const catalog = this.readCatalog();
|
|
55
|
+
if (!catalog) return [];
|
|
56
|
+
return (catalog.routines || []).map(entry => {
|
|
57
|
+
try {
|
|
58
|
+
return this.readSpec(entry.name);
|
|
59
|
+
} catch (err) {
|
|
60
|
+
return Object.assign({}, entry, { _error: err.message });
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// -------- Lock file ------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
lockDir() {
|
|
68
|
+
return path.join(this.cwd, LOCK_DIR);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
lockPath() {
|
|
72
|
+
return path.join(this.lockDir(), LOCK_FILE);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
readLock() {
|
|
76
|
+
const p = this.lockPath();
|
|
77
|
+
if (!fs.existsSync(p)) return { version: 1, routines: {} };
|
|
78
|
+
try {
|
|
79
|
+
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
80
|
+
} catch (err) {
|
|
81
|
+
throw new Error(`Failed to parse ${p}: ${err.message}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
writeLock(lock) {
|
|
86
|
+
if (!fs.existsSync(this.lockDir())) {
|
|
87
|
+
fs.mkdirSync(this.lockDir(), { recursive: true });
|
|
88
|
+
}
|
|
89
|
+
fs.writeFileSync(this.lockPath(), JSON.stringify(lock, null, 2) + '\n');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Update the lock entry for a routine. Status is "installed" | "skipped" | "disabled". */
|
|
93
|
+
setLockEntry(name, entry) {
|
|
94
|
+
const lock = this.readLock();
|
|
95
|
+
lock.routines = lock.routines || {};
|
|
96
|
+
lock.routines[name] = Object.assign({}, lock.routines[name] || {}, entry, {
|
|
97
|
+
updated_at: new Date().toISOString()
|
|
98
|
+
});
|
|
99
|
+
this.writeLock(lock);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
removeLockEntry(name) {
|
|
103
|
+
const lock = this.readLock();
|
|
104
|
+
if (lock.routines && lock.routines[name]) {
|
|
105
|
+
delete lock.routines[name];
|
|
106
|
+
this.writeLock(lock);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// -------- Status computation --------------------------------------------
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Compute status for every routine in the catalog.
|
|
114
|
+
* Returns array of { spec, lockEntry, status, requiredArtifacts: {present, missing} }
|
|
115
|
+
*/
|
|
116
|
+
computeStatus() {
|
|
117
|
+
const lock = this.readLock();
|
|
118
|
+
return this.list().map(spec => {
|
|
119
|
+
if (spec._error) {
|
|
120
|
+
return { spec, lockEntry: null, status: 'error', error: spec._error };
|
|
121
|
+
}
|
|
122
|
+
const lockEntry = (lock.routines || {})[spec.name] || null;
|
|
123
|
+
const required = (spec.required_artifacts || []).map(p => {
|
|
124
|
+
const full = path.join(this.cwd, p);
|
|
125
|
+
return { path: p, exists: fs.existsSync(full) };
|
|
126
|
+
});
|
|
127
|
+
const missingRequired = required.filter(r => !r.exists);
|
|
128
|
+
const isOptional = !!spec.optional;
|
|
129
|
+
|
|
130
|
+
let status;
|
|
131
|
+
if (lockEntry && lockEntry.status === 'installed') {
|
|
132
|
+
status = 'installed';
|
|
133
|
+
} else if (lockEntry && lockEntry.status === 'skipped') {
|
|
134
|
+
status = 'skipped';
|
|
135
|
+
} else if (lockEntry && lockEntry.status === 'disabled') {
|
|
136
|
+
status = 'disabled';
|
|
137
|
+
} else if (missingRequired.length > 0 && isOptional) {
|
|
138
|
+
status = 'unavailable';
|
|
139
|
+
} else if (missingRequired.length > 0) {
|
|
140
|
+
status = 'blocked';
|
|
141
|
+
} else {
|
|
142
|
+
status = 'available';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
spec,
|
|
147
|
+
lockEntry,
|
|
148
|
+
status,
|
|
149
|
+
requiredArtifacts: { present: required.filter(r => r.exists), missing: missingRequired },
|
|
150
|
+
optional: isOptional
|
|
151
|
+
};
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// -------- New routines since last seen ----------------------------------
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Returns routines that exist in the catalog but have no lock entry — i.e.
|
|
159
|
+
* routines the user has never been prompted about. Used by add.js/update.js.
|
|
160
|
+
*/
|
|
161
|
+
newRoutines() {
|
|
162
|
+
return this.computeStatus().filter(r => r.status === 'available' && !r.lockEntry);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
module.exports = Routines;
|