@rune-kit/rune 2.10.0 → 2.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +65 -6
- package/commands/rune.md +168 -168
- package/compiler/__tests__/detect-invariants.test.js +136 -0
- package/compiler/__tests__/doctor-mesh.test.js +229 -0
- package/compiler/__tests__/hook-dispatch.test.js +91 -0
- package/compiler/__tests__/hooks-antigravity.test.js +118 -0
- package/compiler/__tests__/hooks-cursor.test.js +139 -0
- package/compiler/__tests__/hooks-install.test.js +305 -0
- package/compiler/__tests__/hooks-merge.test.js +204 -0
- package/compiler/__tests__/hooks-tiers.test.js +519 -0
- package/compiler/__tests__/hooks-windsurf.test.js +115 -0
- package/compiler/__tests__/inject-claude-md.test.js +152 -0
- package/compiler/__tests__/load-invariants.test.js +408 -0
- package/compiler/__tests__/onboard-invariants.test.js +240 -0
- package/compiler/adapters/hooks/antigravity.js +140 -0
- package/compiler/adapters/hooks/claude.js +166 -0
- package/compiler/adapters/hooks/cursor.js +191 -0
- package/compiler/adapters/hooks/index.js +82 -0
- package/compiler/adapters/hooks/tier-emitter.js +182 -0
- package/compiler/adapters/hooks/windsurf.js +202 -0
- package/compiler/bin/rune.js +196 -6
- package/compiler/commands/hook-dispatch.js +87 -0
- package/compiler/commands/hooks/install.js +120 -0
- package/compiler/commands/hooks/merge.js +211 -0
- package/compiler/commands/hooks/presets.js +116 -0
- package/compiler/commands/hooks/status.js +112 -0
- package/compiler/commands/hooks/tiers.js +221 -0
- package/compiler/commands/hooks/uninstall.js +94 -0
- package/compiler/doctor.js +236 -0
- package/contexts/dev.md +34 -34
- package/contexts/research.md +43 -43
- package/contexts/review.md +55 -55
- package/extensions/ai-ml/PACK.md +88 -88
- package/extensions/ai-ml/skills/ai-agents.md +172 -172
- package/extensions/ai-ml/skills/code-sandbox.md +187 -187
- package/extensions/ai-ml/skills/deep-research.md +146 -146
- package/extensions/ai-ml/skills/embedding-search.md +66 -66
- package/extensions/ai-ml/skills/fine-tuning-guide.md +74 -74
- package/extensions/ai-ml/skills/llm-architect.md +125 -125
- package/extensions/ai-ml/skills/llm-integration.md +64 -64
- package/extensions/ai-ml/skills/prompt-patterns.md +72 -72
- package/extensions/ai-ml/skills/rag-patterns.md +66 -66
- package/extensions/ai-ml/skills/web-extraction.md +114 -114
- package/extensions/analytics/PACK.md +92 -92
- package/extensions/analytics/skills/ab-testing.md +72 -72
- package/extensions/analytics/skills/dashboard-patterns.md +83 -83
- package/extensions/analytics/skills/data-validation.md +68 -68
- package/extensions/analytics/skills/funnel-analysis.md +81 -81
- package/extensions/analytics/skills/sql-patterns.md +57 -57
- package/extensions/analytics/skills/statistical-analysis.md +79 -79
- package/extensions/analytics/skills/tracking-setup.md +71 -71
- package/extensions/backend/PACK.md +104 -104
- package/extensions/backend/skills/api-patterns.md +84 -84
- package/extensions/backend/skills/async-pipeline.md +193 -193
- package/extensions/backend/skills/auth-patterns.md +97 -97
- package/extensions/backend/skills/background-jobs.md +133 -133
- package/extensions/backend/skills/caching-patterns.md +108 -108
- package/extensions/backend/skills/cli-generation.md +133 -133
- package/extensions/backend/skills/database-patterns.md +87 -87
- package/extensions/backend/skills/middleware-patterns.md +104 -104
- package/extensions/chrome-ext/PACK.md +93 -93
- package/extensions/chrome-ext/skills/cws-preflight.md +143 -143
- package/extensions/chrome-ext/skills/cws-publish.md +104 -104
- package/extensions/chrome-ext/skills/ext-ai-integration.md +251 -251
- package/extensions/chrome-ext/skills/ext-messaging.md +139 -139
- package/extensions/chrome-ext/skills/ext-storage.md +133 -133
- package/extensions/chrome-ext/skills/mv3-scaffold.md +164 -164
- package/extensions/content/PACK.md +96 -96
- package/extensions/content/skills/blog-patterns.md +88 -88
- package/extensions/content/skills/cms-integration.md +131 -131
- package/extensions/content/skills/content-scoring.md +107 -107
- package/extensions/content/skills/i18n.md +83 -83
- package/extensions/content/skills/mdx-authoring.md +137 -137
- package/extensions/content/skills/reference.md +1014 -1014
- package/extensions/content/skills/seo-patterns.md +67 -67
- package/extensions/content/skills/video-repurpose.md +153 -153
- package/extensions/devops/PACK.md +101 -101
- package/extensions/devops/skills/chaos-testing.md +67 -67
- package/extensions/devops/skills/ci-cd.md +75 -75
- package/extensions/devops/skills/docker.md +58 -58
- package/extensions/devops/skills/edge-serverless.md +163 -163
- package/extensions/devops/skills/infra-as-code.md +158 -158
- package/extensions/devops/skills/kubernetes.md +110 -110
- package/extensions/devops/skills/monitoring.md +57 -57
- package/extensions/devops/skills/server-setup.md +64 -64
- package/extensions/devops/skills/ssl-domain.md +42 -42
- package/extensions/ecommerce/PACK.md +116 -116
- package/extensions/ecommerce/skills/cart-system.md +79 -79
- package/extensions/ecommerce/skills/inventory-mgmt.md +102 -102
- package/extensions/ecommerce/skills/order-management.md +126 -126
- package/extensions/ecommerce/skills/payment-integration.md +472 -472
- package/extensions/ecommerce/skills/shopify-dev.md +69 -69
- package/extensions/ecommerce/skills/subscription-billing.md +93 -93
- package/extensions/ecommerce/skills/tax-compliance.md +117 -117
- package/extensions/gamedev/PACK.md +142 -142
- package/extensions/gamedev/skills/asset-pipeline.md +74 -74
- package/extensions/gamedev/skills/audio-system.md +129 -129
- package/extensions/gamedev/skills/camera-system.md +87 -87
- package/extensions/gamedev/skills/ecs.md +98 -98
- package/extensions/gamedev/skills/game-loops.md +72 -72
- package/extensions/gamedev/skills/input-system.md +199 -199
- package/extensions/gamedev/skills/multiplayer.md +180 -180
- package/extensions/gamedev/skills/particles.md +105 -105
- package/extensions/gamedev/skills/physics-engine.md +89 -89
- package/extensions/gamedev/skills/scene-management.md +146 -146
- package/extensions/gamedev/skills/threejs-patterns.md +90 -90
- package/extensions/gamedev/skills/webgl.md +71 -71
- package/extensions/mobile/PACK.md +106 -106
- package/extensions/mobile/skills/app-store-connect.md +152 -152
- package/extensions/mobile/skills/app-store-prep.md +66 -66
- package/extensions/mobile/skills/deep-linking.md +109 -109
- package/extensions/mobile/skills/flutter.md +60 -60
- package/extensions/mobile/skills/ios-build-pipeline.md +142 -142
- package/extensions/mobile/skills/native-bridge.md +66 -66
- package/extensions/mobile/skills/ota-updates.md +97 -97
- package/extensions/mobile/skills/push-notifications.md +111 -111
- package/extensions/mobile/skills/react-native.md +82 -82
- package/extensions/saas/PACK.md +116 -116
- package/extensions/saas/skills/billing-integration.md +200 -200
- package/extensions/saas/skills/feature-flags.md +130 -130
- package/extensions/saas/skills/multi-tenant.md +103 -103
- package/extensions/saas/skills/onboarding-flow.md +139 -139
- package/extensions/saas/skills/subscription-flow.md +95 -95
- package/extensions/saas/skills/team-management.md +144 -144
- package/extensions/security/PACK.md +99 -99
- package/extensions/security/skills/api-security.md +140 -140
- package/extensions/security/skills/compliance.md +68 -68
- package/extensions/security/skills/owasp-audit.md +64 -64
- package/extensions/security/skills/pentest-patterns.md +77 -77
- package/extensions/security/skills/secret-mgmt.md +65 -65
- package/extensions/security/skills/supply-chain.md +65 -65
- package/extensions/trading/PACK.md +80 -80
- package/extensions/trading/skills/chart-components.md +55 -55
- package/extensions/trading/skills/experiment-loop.md +125 -125
- package/extensions/trading/skills/fintech-patterns.md +47 -47
- package/extensions/trading/skills/indicator-library.md +58 -58
- package/extensions/trading/skills/quant-analysis.md +111 -111
- package/extensions/trading/skills/realtime-data.md +58 -58
- package/extensions/trading/skills/trade-logic.md +104 -104
- package/extensions/ui/PACK.md +130 -130
- package/extensions/ui/skills/a11y-audit.md +91 -91
- package/extensions/ui/skills/animation-patterns.md +127 -127
- package/extensions/ui/skills/component-patterns.md +100 -100
- package/extensions/ui/skills/design-decision.md +108 -108
- package/extensions/ui/skills/design-system.md +68 -68
- package/extensions/ui/skills/landing-patterns.md +155 -155
- package/extensions/ui/skills/palette-picker.md +173 -173
- package/extensions/ui/skills/react-health.md +90 -90
- package/extensions/ui/skills/type-system.md +125 -125
- package/extensions/ui/skills/web-vitals.md +153 -153
- package/extensions/zalo/PACK.md +145 -145
- package/extensions/zalo/skills/zalo-oa-mcp.md +317 -317
- package/extensions/zalo/skills/zalo-oa-messaging.md +429 -429
- package/extensions/zalo/skills/zalo-oa-setup.md +236 -236
- package/extensions/zalo/skills/zalo-oa-webhook.md +189 -189
- package/extensions/zalo/skills/zalo-personal-messaging.md +194 -194
- package/extensions/zalo/skills/zalo-personal-setup.md +153 -153
- package/extensions/zalo/skills/zalo-rate-guard.md +219 -219
- package/hooks/auto-format/index.cjs +48 -48
- package/hooks/hooks.json +111 -111
- package/hooks/post-session-reflect/index.cjs +189 -189
- package/hooks/pre-compact/index.cjs +95 -95
- package/hooks/run-hook.cmd +1 -1
- package/hooks/secrets-scan/index.cjs +100 -100
- package/hooks/session-start/index.cjs +71 -71
- package/hooks/typecheck/index.cjs +65 -65
- package/package.json +63 -63
- package/references/ui-pro-max-data/LICENSE-UI-PRO-MAX +21 -21
- package/references/ui-pro-max-data/charts.csv +26 -26
- package/references/ui-pro-max-data/colors.csv +161 -161
- package/references/ui-pro-max-data/styles.csv +68 -68
- package/references/ui-pro-max-data/typography.csv +74 -74
- package/references/ui-pro-max-data/ui-reasoning.csv +162 -162
- package/references/ui-pro-max-data/ux-guidelines.csv +99 -99
- package/skills/adversary/SKILL.md +283 -283
- package/skills/asset-creator/SKILL.md +157 -157
- package/skills/audit/SKILL.md +147 -2
- package/skills/autopsy/SKILL.md +335 -335
- package/skills/ba/SKILL.md +85 -1
- package/skills/brainstorm/SKILL.md +380 -342
- package/skills/browser-pilot/SKILL.md +169 -168
- package/skills/constraint-check/SKILL.md +165 -165
- package/skills/context-engine/SKILL.md +408 -404
- package/skills/cook/SKILL.md +917 -863
- package/skills/db/SKILL.md +273 -273
- package/skills/debug/SKILL.md +465 -465
- package/skills/dependency-doctor/SKILL.md +265 -235
- package/skills/deploy/SKILL.md +274 -231
- package/skills/design/DESIGN-REFERENCE.md +365 -365
- package/skills/design/SKILL.md +590 -589
- package/skills/doc-processor/SKILL.md +254 -254
- package/skills/docs/SKILL.md +374 -374
- package/skills/docs-seeker/SKILL.md +178 -177
- package/skills/fix/SKILL.md +332 -330
- package/skills/git/SKILL.md +339 -339
- package/skills/hallucination-guard/SKILL.md +220 -219
- package/skills/incident/SKILL.md +254 -253
- package/skills/integrity-check/SKILL.md +169 -169
- package/skills/journal/SKILL.md +241 -240
- package/skills/launch/SKILL.md +344 -344
- package/skills/logic-guardian/SKILL.md +269 -251
- package/skills/marketing/SKILL.md +351 -289
- package/skills/mcp-builder/SKILL.md +425 -425
- package/skills/neural-memory/SKILL.md +359 -362
- package/skills/onboard/SKILL.md +432 -403
- package/skills/onboard/references/invariants-template.md +76 -0
- package/skills/onboard/scripts/detect-invariants.js +439 -0
- package/skills/onboard/scripts/inject-claude-md.js +150 -0
- package/skills/onboard/scripts/onboard-invariants.js +194 -0
- package/skills/perf/SKILL.md +347 -346
- package/skills/plan/SKILL.md +435 -428
- package/skills/preflight/SKILL.md +415 -415
- package/skills/problem-solver/SKILL.md +380 -284
- package/skills/rescue/SKILL.md +474 -474
- package/skills/research/SKILL.md +4 -0
- package/skills/retro/SKILL.md +3 -1
- package/skills/review/SKILL.md +614 -588
- package/skills/review-intake/SKILL.md +249 -249
- package/skills/safeguard/SKILL.md +200 -200
- package/skills/sast/SKILL.md +190 -190
- package/skills/scaffold/SKILL.md +328 -287
- package/skills/scope-guard/SKILL.md +183 -180
- package/skills/scout/SKILL.md +269 -263
- package/skills/sentinel/SKILL.md +384 -381
- package/skills/sentinel-env/SKILL.md +254 -254
- package/skills/sequential-thinking/SKILL.md +234 -234
- package/skills/session-bridge/SKILL.md +595 -543
- package/skills/session-bridge/scripts/load-invariants.js +397 -0
- package/skills/skill-forge/SKILL.md +581 -581
- package/skills/skill-router/SKILL.md +3 -0
- package/skills/slides/SKILL.md +19 -0
- package/skills/surgeon/SKILL.md +215 -215
- package/skills/team/SKILL.md +557 -537
- package/skills/test/SKILL.md +620 -614
- package/skills/trend-scout/SKILL.md +145 -145
- package/skills/verification/SKILL.md +334 -326
- package/skills/video-creator/SKILL.md +201 -201
- package/skills/watchdog/SKILL.md +168 -168
- package/skills/worktree/SKILL.md +140 -140
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor hooks adapter.
|
|
3
|
+
*
|
|
4
|
+
* Cursor has no PreToolUse/PostToolUse primitives. The closest analog is the
|
|
5
|
+
* `.cursor/rules/*.mdc` auto-attach system: rules with `alwaysApply: true` or
|
|
6
|
+
* glob-scoped rules inject guidance into the agent's prompt when editing
|
|
7
|
+
* matching files. This adapter emits Rune skill invocation reminders as rules.
|
|
8
|
+
*
|
|
9
|
+
* Fidelity vs Claude:
|
|
10
|
+
* - preflight → rule (alwaysApply, agent sees it before every Edit)
|
|
11
|
+
* - sentinel → rule scoped to commit-related files
|
|
12
|
+
* - dependency-doctor → rule scoped to package.json / lockfiles
|
|
13
|
+
* - completion-gate → no analog (Cursor has no Stop hook) — documented in notes
|
|
14
|
+
*
|
|
15
|
+
* All Rune rule files are prefixed `rune-` for unambiguous detection.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { existsSync } from 'node:fs';
|
|
19
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
import { platformEntries, renderTierRule, unsupportedEntries } from './tier-emitter.js';
|
|
22
|
+
|
|
23
|
+
export const id = 'cursor';
|
|
24
|
+
|
|
25
|
+
const RULES_REL_DIR = '.cursor/rules';
|
|
26
|
+
const RUNE_PREFIX = 'rune-';
|
|
27
|
+
const AUTO_SIGNATURE = '@rune-kit/rune hook-dispatch';
|
|
28
|
+
|
|
29
|
+
export function detect(projectRoot) {
|
|
30
|
+
return existsSync(path.join(projectRoot, '.cursor'));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function emit({ preset, projectRoot, tierManifests = [] }) {
|
|
34
|
+
if (preset === 'off' && tierManifests.length === 0) return uninstall({ projectRoot });
|
|
35
|
+
if (preset !== 'off' && preset !== 'strict' && preset !== 'gentle') {
|
|
36
|
+
throw new Error(`cursor adapter: invalid preset '${preset}'`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const rulesDir = path.join(projectRoot, RULES_REL_DIR);
|
|
40
|
+
const mode = preset === 'strict' ? 'BLOCK' : 'WARN';
|
|
41
|
+
const ruleBase = { mode };
|
|
42
|
+
|
|
43
|
+
const rules = [
|
|
44
|
+
{
|
|
45
|
+
name: 'rune-preflight',
|
|
46
|
+
globs: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx', '**/*.py', '**/*.go', '**/*.rs'],
|
|
47
|
+
alwaysApply: true,
|
|
48
|
+
skill: 'preflight',
|
|
49
|
+
description: 'Run Rune preflight before editing source files.',
|
|
50
|
+
detail: `Before editing any source file, mentally run through Rune's preflight checklist: logic preserved, error handling present, no regressions introduced. ${mode} if any concern surfaces.`,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'rune-sentinel',
|
|
54
|
+
globs: ['**/*.sh', '**/*.Dockerfile', 'Dockerfile', '.github/workflows/*.yml', '.env*'],
|
|
55
|
+
alwaysApply: false,
|
|
56
|
+
skill: 'sentinel',
|
|
57
|
+
description: 'Security review before shell / infra / secret edits.',
|
|
58
|
+
detail: `Before running any Bash or editing infrastructure/env files, audit for secrets, destructive commands, and command injection. ${mode} on any finding.`,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'rune-dependency-doctor',
|
|
62
|
+
globs: [
|
|
63
|
+
'package.json',
|
|
64
|
+
'package-lock.json',
|
|
65
|
+
'pnpm-lock.yaml',
|
|
66
|
+
'yarn.lock',
|
|
67
|
+
'requirements.txt',
|
|
68
|
+
'Cargo.toml',
|
|
69
|
+
'go.mod',
|
|
70
|
+
],
|
|
71
|
+
alwaysApply: false,
|
|
72
|
+
skill: 'dependency-doctor',
|
|
73
|
+
description: 'Dependency health audit after lockfile edits.',
|
|
74
|
+
detail:
|
|
75
|
+
'After modifying any lockfile or manifest, run Rune dependency-doctor to check for outdated packages, CVEs, and breaking change risk.',
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
const files =
|
|
80
|
+
preset === 'off'
|
|
81
|
+
? []
|
|
82
|
+
: rules.map((r) => ({
|
|
83
|
+
path: path.join(rulesDir, `${r.name}.mdc`),
|
|
84
|
+
content: renderMdc({ ...r, ...ruleBase }),
|
|
85
|
+
}));
|
|
86
|
+
|
|
87
|
+
const notes =
|
|
88
|
+
preset === 'off'
|
|
89
|
+
? []
|
|
90
|
+
: [
|
|
91
|
+
'Cursor has no Stop hook equivalent — `completion-gate` must be invoked manually via `/rune completion-gate`.',
|
|
92
|
+
`Auto-attach mode: ${preset}. Rules emit ${mode} guidance to the agent.`,
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
for (const manifest of tierManifests) {
|
|
96
|
+
const tierMode = mode; // share preset mode with tier rules
|
|
97
|
+
for (const entry of platformEntries(manifest, 'cursor')) {
|
|
98
|
+
const content = renderTierRule(entry, 'cursor', { mode: tierMode, tier: manifest.tier });
|
|
99
|
+
if (!content) continue;
|
|
100
|
+
files.push({
|
|
101
|
+
path: path.join(rulesDir, `${RUNE_PREFIX}${manifest.tier}-${entry.id}.mdc`),
|
|
102
|
+
content,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
const skipped = unsupportedEntries(manifest, 'cursor');
|
|
106
|
+
if (skipped.length > 0) {
|
|
107
|
+
notes.push(
|
|
108
|
+
`cursor: ${manifest.tier} skipped ${skipped.length} entr${skipped.length === 1 ? 'y' : 'ies'} (${skipped.map((e) => e.id).join(', ')}) — platform-unsupported or Claude-only.`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return { files, notes };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function uninstall({ projectRoot }) {
|
|
117
|
+
const rulesDir = path.join(projectRoot, RULES_REL_DIR);
|
|
118
|
+
if (!existsSync(rulesDir)) return { files: [], notes: [] };
|
|
119
|
+
|
|
120
|
+
const entries = await readdir(rulesDir, { withFileTypes: true });
|
|
121
|
+
const runeFiles = entries.filter((e) => e.isFile() && e.name.startsWith(RUNE_PREFIX) && e.name.endsWith('.mdc'));
|
|
122
|
+
|
|
123
|
+
const files = [];
|
|
124
|
+
for (const file of runeFiles) {
|
|
125
|
+
const abs = path.join(rulesDir, file.name);
|
|
126
|
+
const content = await readFile(abs, 'utf-8');
|
|
127
|
+
if (content.includes(AUTO_SIGNATURE) || content.includes('rune-managed: true')) {
|
|
128
|
+
files.push({ path: abs, content: null });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return { files, notes: files.length === 0 ? ['no Rune-managed rules found'] : [] };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function status(projectRoot) {
|
|
136
|
+
const rulesDir = path.join(projectRoot, RULES_REL_DIR);
|
|
137
|
+
if (!existsSync(rulesDir)) {
|
|
138
|
+
return {
|
|
139
|
+
installed: false,
|
|
140
|
+
preset: null,
|
|
141
|
+
wired: [],
|
|
142
|
+
missing: ['preflight', 'sentinel', 'dependency-doctor'],
|
|
143
|
+
notes: ['no .cursor/rules directory'],
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
const entries = await readdir(rulesDir, { withFileTypes: true });
|
|
147
|
+
const runeFiles = entries.filter((e) => e.isFile() && e.name.startsWith(RUNE_PREFIX) && e.name.endsWith('.mdc'));
|
|
148
|
+
const wired = runeFiles.map((f) => f.name.replace(RUNE_PREFIX, '').replace('.mdc', ''));
|
|
149
|
+
const expected = ['preflight', 'sentinel', 'dependency-doctor'];
|
|
150
|
+
const missing = expected.filter((s) => !wired.includes(s));
|
|
151
|
+
|
|
152
|
+
let preset = null;
|
|
153
|
+
for (const file of runeFiles) {
|
|
154
|
+
const content = await readFile(path.join(rulesDir, file.name), 'utf-8');
|
|
155
|
+
if (/^mode: BLOCK$/m.test(content)) {
|
|
156
|
+
preset = 'strict';
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
if (/^mode: WARN$/m.test(content)) preset = preset ?? 'gentle';
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
installed: runeFiles.length > 0,
|
|
164
|
+
preset,
|
|
165
|
+
wired,
|
|
166
|
+
missing,
|
|
167
|
+
notes: ['completion-gate not available on Cursor'],
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function renderMdc(rule) {
|
|
172
|
+
const frontmatter = [
|
|
173
|
+
'---',
|
|
174
|
+
`description: ${rule.description}`,
|
|
175
|
+
`globs: ${JSON.stringify(rule.globs)}`,
|
|
176
|
+
`alwaysApply: ${rule.alwaysApply}`,
|
|
177
|
+
'rune-managed: true',
|
|
178
|
+
`rune-skill: ${rule.skill}`,
|
|
179
|
+
`mode: ${rule.mode}`,
|
|
180
|
+
'---',
|
|
181
|
+
].join('\n');
|
|
182
|
+
const body = [
|
|
183
|
+
`# Rune ${rule.skill}`,
|
|
184
|
+
'',
|
|
185
|
+
rule.detail,
|
|
186
|
+
'',
|
|
187
|
+
`_Auto-generated by \`rune hooks install\` (${AUTO_SIGNATURE})._`,
|
|
188
|
+
'_Do not hand-edit — changes will be overwritten. Delete the file to opt out._',
|
|
189
|
+
].join('\n');
|
|
190
|
+
return `${frontmatter}\n\n${body}\n`;
|
|
191
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hooks adapter registry — per-platform emitters for `rune hooks install`.
|
|
3
|
+
*
|
|
4
|
+
* Each adapter exposes:
|
|
5
|
+
* - id: platform key (e.g. 'claude', 'cursor')
|
|
6
|
+
* - detect(projectRoot) → boolean (presence of platform-specific dir)
|
|
7
|
+
* - emit({preset, projectRoot}) → Promise<{ files: [{path, content, mode?}], notes: string[] }>
|
|
8
|
+
* - uninstall({projectRoot}) → Promise<{ files: [{path, content|null, mode?}], notes: string[] }>
|
|
9
|
+
* (content = null means delete the file)
|
|
10
|
+
* - status(projectRoot) → Promise<{ installed: boolean, preset: string|null, wired: string[], missing: string[], notes: string[] }>
|
|
11
|
+
*
|
|
12
|
+
* The install command iterates adapters, calls emit, and writes files.
|
|
13
|
+
* Adapters NEVER write files themselves — they return a plan.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import * as antigravity from './antigravity.js';
|
|
17
|
+
import * as claude from './claude.js';
|
|
18
|
+
import * as cursor from './cursor.js';
|
|
19
|
+
import * as windsurf from './windsurf.js';
|
|
20
|
+
|
|
21
|
+
export const ADAPTERS = Object.freeze({
|
|
22
|
+
claude,
|
|
23
|
+
cursor,
|
|
24
|
+
windsurf,
|
|
25
|
+
antigravity,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export const PLATFORM_KEYS = Object.freeze(Object.keys(ADAPTERS));
|
|
29
|
+
|
|
30
|
+
export function getAdapter(id) {
|
|
31
|
+
const adapter = ADAPTERS[id];
|
|
32
|
+
if (!adapter) {
|
|
33
|
+
throw new Error(`Unknown hooks platform: ${id}. Choose from: ${PLATFORM_KEYS.join(', ')}`);
|
|
34
|
+
}
|
|
35
|
+
return adapter;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Auto-detect platforms present in the project. Returns ids with truthy detect().
|
|
40
|
+
*/
|
|
41
|
+
export function detectPlatforms(projectRoot) {
|
|
42
|
+
return PLATFORM_KEYS.filter((id) => ADAPTERS[id].detect?.(projectRoot));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Platform capability matrix — honest about what each can auto-fire.
|
|
47
|
+
* Consumed by `rune hooks status` and docs.
|
|
48
|
+
*/
|
|
49
|
+
export const CAPABILITIES = Object.freeze({
|
|
50
|
+
claude: {
|
|
51
|
+
maturity: 'stable',
|
|
52
|
+
preToolEdit: true,
|
|
53
|
+
preToolBash: true,
|
|
54
|
+
postToolEdit: true,
|
|
55
|
+
stop: true,
|
|
56
|
+
notes: 'Full hook parity — PreToolUse/PostToolUse/Stop all supported.',
|
|
57
|
+
},
|
|
58
|
+
cursor: {
|
|
59
|
+
maturity: 'beta',
|
|
60
|
+
preToolEdit: 'rule-injection',
|
|
61
|
+
preToolBash: false,
|
|
62
|
+
postToolEdit: false,
|
|
63
|
+
stop: false,
|
|
64
|
+
notes: 'No tool-level hooks. Uses auto-attach `.mdc` rules as best-effort pre-edit guidance.',
|
|
65
|
+
},
|
|
66
|
+
windsurf: {
|
|
67
|
+
maturity: 'beta',
|
|
68
|
+
preToolEdit: 'workflow',
|
|
69
|
+
preToolBash: 'workflow',
|
|
70
|
+
postToolEdit: false,
|
|
71
|
+
stop: false,
|
|
72
|
+
notes: 'Workflows fire on user command, not tool use. Cascade rules approximate auto-fire.',
|
|
73
|
+
},
|
|
74
|
+
antigravity: {
|
|
75
|
+
maturity: 'experimental',
|
|
76
|
+
preToolEdit: 'rule-injection',
|
|
77
|
+
preToolBash: false,
|
|
78
|
+
postToolEdit: false,
|
|
79
|
+
stop: false,
|
|
80
|
+
notes: 'Early platform; uses rule-injection fallback only.',
|
|
81
|
+
},
|
|
82
|
+
});
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-platform translation of tier manifest entries.
|
|
3
|
+
*
|
|
4
|
+
* Given a tier manifest (Pro / Business / future), each adapter asks this
|
|
5
|
+
* module for the platform-specific block it should write. Adapters still own
|
|
6
|
+
* file shape + uninstall semantics; this module only handles manifest → block.
|
|
7
|
+
*
|
|
8
|
+
* Keeping translation here (rather than re-implementing per adapter) means:
|
|
9
|
+
* - one place to update when manifest schema evolves
|
|
10
|
+
* - consistent behavior across adapters (no Cursor-specific drift)
|
|
11
|
+
* - Free compiler stays tier-agnostic — any tier's manifest flows through
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export const AUTO_SIGNATURE = '@rune-kit/rune hook-dispatch';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Build a Claude-format block from a tier manifest.
|
|
18
|
+
*
|
|
19
|
+
* Returns:
|
|
20
|
+
* {
|
|
21
|
+
* hooks: { UserPromptSubmit: [...], PreToolUse: [...], ... },
|
|
22
|
+
* statusLine: { type, command, padding? } | null,
|
|
23
|
+
* notes: string[]
|
|
24
|
+
* }
|
|
25
|
+
*/
|
|
26
|
+
export function buildTierBlock(manifest, platformId) {
|
|
27
|
+
if (platformId !== 'claude') {
|
|
28
|
+
throw new Error(`buildTierBlock is claude-specific. For '${platformId}', use renderTierForPlatform.`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const hooks = {};
|
|
32
|
+
let statusLine = null;
|
|
33
|
+
const notes = [];
|
|
34
|
+
|
|
35
|
+
for (const entry of manifest.entries) {
|
|
36
|
+
if (entry.event === 'statusLine') {
|
|
37
|
+
statusLine = {
|
|
38
|
+
type: 'command',
|
|
39
|
+
command: entry.command,
|
|
40
|
+
...(typeof entry.padding === 'number' ? { padding: entry.padding } : {}),
|
|
41
|
+
};
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
// Explicit per-platform opt-out still honored even when building the Claude block
|
|
45
|
+
// (keeps the check symmetric with platformEntries()).
|
|
46
|
+
if (entry.platforms?.claude === 'unsupported') continue;
|
|
47
|
+
|
|
48
|
+
const event = entry.event;
|
|
49
|
+
const matcher = entry.matcher || '.*';
|
|
50
|
+
if (!hooks[event]) hooks[event] = [];
|
|
51
|
+
|
|
52
|
+
const existingGroup = hooks[event].find((g) => g.matcher === matcher);
|
|
53
|
+
const hookCmd = {
|
|
54
|
+
type: 'command',
|
|
55
|
+
command: entry.command,
|
|
56
|
+
async: entry.async === true,
|
|
57
|
+
};
|
|
58
|
+
if (existingGroup) {
|
|
59
|
+
existingGroup.hooks.push(hookCmd);
|
|
60
|
+
} else {
|
|
61
|
+
hooks[event].push({ matcher, hooks: [hookCmd] });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (manifest.entries.some((e) => e.claudeOnly && e.event !== 'statusLine')) {
|
|
66
|
+
notes.push(`${manifest.tier}: some entries are Claude-only and were skipped on other platforms.`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { hooks, statusLine, notes };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Render a tier manifest entry for a rule-based platform (Cursor / Antigravity).
|
|
74
|
+
* Returns null if the entry does not apply to that platform.
|
|
75
|
+
*/
|
|
76
|
+
export function renderTierRule(entry, platformId, { mode, tier }) {
|
|
77
|
+
const hint = entry.platforms?.[platformId];
|
|
78
|
+
if (hint === 'unsupported' || entry.claudeOnly || entry.event === 'statusLine') return null;
|
|
79
|
+
|
|
80
|
+
const globs = entry.globs ?? defaultGlobsForEvent(entry.event);
|
|
81
|
+
const alwaysApply = hint === 'rule-alwaysApply' || entry.event === 'UserPromptSubmit';
|
|
82
|
+
|
|
83
|
+
const frontmatter = [
|
|
84
|
+
'---',
|
|
85
|
+
`description: Rune ${tier} ${entry.skill} — ${entry.description || 'tier hook'}`,
|
|
86
|
+
`globs: ${JSON.stringify(globs)}`,
|
|
87
|
+
`alwaysApply: ${alwaysApply}`,
|
|
88
|
+
'rune-managed: true',
|
|
89
|
+
`rune-tier: ${tier}`,
|
|
90
|
+
`rune-skill: ${entry.skill}`,
|
|
91
|
+
`rune-entry: ${entry.id}`,
|
|
92
|
+
`mode: ${mode}`,
|
|
93
|
+
'---',
|
|
94
|
+
].join('\n');
|
|
95
|
+
|
|
96
|
+
const body = [
|
|
97
|
+
`# Rune ${tier}: ${entry.skill}`,
|
|
98
|
+
'',
|
|
99
|
+
entry.description || `Auto-invoke ${entry.skill} when the matching edit pattern fires.`,
|
|
100
|
+
'',
|
|
101
|
+
`Trigger: \`${entry.event}\`${entry.matcher ? ` matcher \`${entry.matcher}\`` : ''}.`,
|
|
102
|
+
`Mode: ${mode}.`,
|
|
103
|
+
'',
|
|
104
|
+
"On a matching operation, the agent should mentally run this skill's checklist before proceeding.",
|
|
105
|
+
`See the ${tier} skill \`${entry.skill}\` for the full guidance.`,
|
|
106
|
+
'',
|
|
107
|
+
`_Auto-generated by \`rune hooks install --tier ${tier}\` (${AUTO_SIGNATURE})._`,
|
|
108
|
+
'_Do not hand-edit — changes will be overwritten. Delete the file to opt out._',
|
|
109
|
+
].join('\n');
|
|
110
|
+
|
|
111
|
+
return `${frontmatter}\n\n${body}\n`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Render a Windsurf workflow file for a tier entry.
|
|
116
|
+
*/
|
|
117
|
+
export function renderTierWorkflow(entry, { mode, tier }) {
|
|
118
|
+
if (entry.claudeOnly || entry.event === 'statusLine' || entry.platforms?.windsurf === 'unsupported') return null;
|
|
119
|
+
return [
|
|
120
|
+
'---',
|
|
121
|
+
`description: Rune ${tier} ${entry.skill} — ${entry.description || 'tier hook'}`,
|
|
122
|
+
'rune-managed: true',
|
|
123
|
+
`rune-tier: ${tier}`,
|
|
124
|
+
`rune-skill: ${entry.skill}`,
|
|
125
|
+
`rune-entry: ${entry.id}`,
|
|
126
|
+
`mode: ${mode}`,
|
|
127
|
+
'---',
|
|
128
|
+
'',
|
|
129
|
+
`# /rune-${tier}-${entry.skill}`,
|
|
130
|
+
'',
|
|
131
|
+
entry.description || `Run before ${entry.event} operations.`,
|
|
132
|
+
'',
|
|
133
|
+
'## Steps',
|
|
134
|
+
'',
|
|
135
|
+
`1. Review target context for ${entry.skill} concerns.`,
|
|
136
|
+
`2. Apply Rune ${tier} skill guidance (see \`${tier}/skills/${entry.skill}/SKILL.md\`).`,
|
|
137
|
+
`3. Surface any finding — mode ${mode}.`,
|
|
138
|
+
'',
|
|
139
|
+
`_Auto-generated by \`rune hooks install --tier ${tier}\` (${AUTO_SIGNATURE})._`,
|
|
140
|
+
'',
|
|
141
|
+
].join('\n');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Filter which entries a platform should emit rules/workflows for.
|
|
146
|
+
* Statusline + claudeOnly entries are filtered out centrally.
|
|
147
|
+
*/
|
|
148
|
+
export function platformEntries(manifest, platformId) {
|
|
149
|
+
return manifest.entries.filter((entry) => {
|
|
150
|
+
if (entry.event === 'statusLine') return false;
|
|
151
|
+
if (entry.claudeOnly) return false;
|
|
152
|
+
const hint = entry.platforms?.[platformId];
|
|
153
|
+
if (hint === 'unsupported') return false;
|
|
154
|
+
return true;
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Unsupported / skipped entries — exposed so adapters can report them via notes.
|
|
160
|
+
*/
|
|
161
|
+
export function unsupportedEntries(manifest, platformId) {
|
|
162
|
+
return manifest.entries.filter((entry) => {
|
|
163
|
+
if (entry.claudeOnly && platformId !== 'claude') return true;
|
|
164
|
+
if (entry.event === 'statusLine' && platformId !== 'claude') return true;
|
|
165
|
+
const hint = entry.platforms?.[platformId];
|
|
166
|
+
return hint === 'unsupported';
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function defaultGlobsForEvent(event) {
|
|
171
|
+
switch (event) {
|
|
172
|
+
case 'UserPromptSubmit':
|
|
173
|
+
return ['**/*'];
|
|
174
|
+
case 'PreToolUse':
|
|
175
|
+
case 'PostToolUse':
|
|
176
|
+
return ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx', '**/*.py', '**/*.go', '**/*.rs', '**/*.md'];
|
|
177
|
+
case 'Stop':
|
|
178
|
+
return ['**/*'];
|
|
179
|
+
default:
|
|
180
|
+
return ['**/*'];
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Windsurf hooks adapter.
|
|
3
|
+
*
|
|
4
|
+
* Windsurf exposes `.windsurf/workflows/*.md` — user-invoked step-by-step
|
|
5
|
+
* scripts — and `.windsurf/rules/*.md` — cascade rules that inject context.
|
|
6
|
+
*
|
|
7
|
+
* We emit BOTH:
|
|
8
|
+
* - Workflows: `rune-<skill>.md` so the user can manually run `/rune-preflight`
|
|
9
|
+
* - Rules: `rune-<skill>-rule.md` that cascade-inject "run the workflow first"
|
|
10
|
+
* reminders on matching globs
|
|
11
|
+
*
|
|
12
|
+
* Fidelity vs Claude: workflows are user-triggered, not tool-triggered, so
|
|
13
|
+
* true auto-fire isn't possible. Cascade rules are the closest analog to
|
|
14
|
+
* PreToolUse.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { existsSync } from 'node:fs';
|
|
18
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
import { platformEntries, renderTierRule, renderTierWorkflow, unsupportedEntries } from './tier-emitter.js';
|
|
21
|
+
|
|
22
|
+
export const id = 'windsurf';
|
|
23
|
+
|
|
24
|
+
const WORKFLOWS_REL_DIR = '.windsurf/workflows';
|
|
25
|
+
const RULES_REL_DIR = '.windsurf/rules';
|
|
26
|
+
const RUNE_PREFIX = 'rune-';
|
|
27
|
+
const AUTO_SIGNATURE = '@rune-kit/rune hook-dispatch';
|
|
28
|
+
|
|
29
|
+
export function detect(projectRoot) {
|
|
30
|
+
return existsSync(path.join(projectRoot, '.windsurf'));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function emit({ preset, projectRoot, tierManifests = [] }) {
|
|
34
|
+
if (preset === 'off' && tierManifests.length === 0) return uninstall({ projectRoot });
|
|
35
|
+
if (preset !== 'off' && preset !== 'strict' && preset !== 'gentle') {
|
|
36
|
+
throw new Error(`windsurf adapter: invalid preset '${preset}'`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const mode = preset === 'strict' ? 'BLOCK' : 'WARN';
|
|
40
|
+
const skills = [
|
|
41
|
+
{
|
|
42
|
+
name: 'preflight',
|
|
43
|
+
trigger: 'editing source files',
|
|
44
|
+
globs: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.py', '**/*.go', '**/*.rs'],
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'sentinel',
|
|
48
|
+
trigger: 'editing infra / running shell',
|
|
49
|
+
globs: ['**/*.sh', 'Dockerfile', '.github/workflows/*.yml', '.env*'],
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'dependency-doctor',
|
|
53
|
+
trigger: 'editing manifests / lockfiles',
|
|
54
|
+
globs: ['package.json', '*lock*', 'Cargo.toml', 'go.mod', 'requirements.txt'],
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
const workflowsDir = path.join(projectRoot, WORKFLOWS_REL_DIR);
|
|
59
|
+
const rulesDir = path.join(projectRoot, RULES_REL_DIR);
|
|
60
|
+
|
|
61
|
+
const files = [];
|
|
62
|
+
if (preset !== 'off') {
|
|
63
|
+
for (const skill of skills) {
|
|
64
|
+
files.push({
|
|
65
|
+
path: path.join(workflowsDir, `${RUNE_PREFIX}${skill.name}.md`),
|
|
66
|
+
content: renderWorkflow(skill, mode),
|
|
67
|
+
});
|
|
68
|
+
files.push({
|
|
69
|
+
path: path.join(rulesDir, `${RUNE_PREFIX}${skill.name}-rule.md`),
|
|
70
|
+
content: renderRule(skill, mode),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const notes =
|
|
76
|
+
preset === 'off'
|
|
77
|
+
? []
|
|
78
|
+
: [
|
|
79
|
+
'Windsurf workflows are user-invoked. Auto-fire is emulated via cascade rules only.',
|
|
80
|
+
`Preset: ${preset} (${mode}). No Stop/PostToolUse equivalent — completion-gate is manual.`,
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
for (const manifest of tierManifests) {
|
|
84
|
+
for (const entry of platformEntries(manifest, 'windsurf')) {
|
|
85
|
+
const workflow = renderTierWorkflow(entry, { mode, tier: manifest.tier });
|
|
86
|
+
const rule = renderTierRule(entry, 'windsurf', { mode, tier: manifest.tier });
|
|
87
|
+
if (workflow) {
|
|
88
|
+
files.push({
|
|
89
|
+
path: path.join(workflowsDir, `${RUNE_PREFIX}${manifest.tier}-${entry.id}.md`),
|
|
90
|
+
content: workflow,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
if (rule) {
|
|
94
|
+
files.push({
|
|
95
|
+
path: path.join(rulesDir, `${RUNE_PREFIX}${manifest.tier}-${entry.id}-rule.md`),
|
|
96
|
+
content: rule,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const skipped = unsupportedEntries(manifest, 'windsurf');
|
|
101
|
+
if (skipped.length > 0) {
|
|
102
|
+
notes.push(
|
|
103
|
+
`windsurf: ${manifest.tier} skipped ${skipped.length} entr${skipped.length === 1 ? 'y' : 'ies'} (${skipped.map((e) => e.id).join(', ')}) — platform-unsupported or Claude-only.`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { files, notes };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function uninstall({ projectRoot }) {
|
|
112
|
+
const files = [];
|
|
113
|
+
for (const rel of [WORKFLOWS_REL_DIR, RULES_REL_DIR]) {
|
|
114
|
+
const dir = path.join(projectRoot, rel);
|
|
115
|
+
if (!existsSync(dir)) continue;
|
|
116
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
117
|
+
const runeFiles = entries.filter((e) => e.isFile() && e.name.startsWith(RUNE_PREFIX) && e.name.endsWith('.md'));
|
|
118
|
+
for (const file of runeFiles) {
|
|
119
|
+
const abs = path.join(dir, file.name);
|
|
120
|
+
const content = await readFile(abs, 'utf-8');
|
|
121
|
+
if (content.includes(AUTO_SIGNATURE) || content.includes('rune-managed: true')) {
|
|
122
|
+
files.push({ path: abs, content: null });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return { files, notes: files.length === 0 ? ['no Rune-managed windsurf files found'] : [] };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function status(projectRoot) {
|
|
130
|
+
const dir = path.join(projectRoot, WORKFLOWS_REL_DIR);
|
|
131
|
+
if (!existsSync(dir)) {
|
|
132
|
+
return {
|
|
133
|
+
installed: false,
|
|
134
|
+
preset: null,
|
|
135
|
+
wired: [],
|
|
136
|
+
missing: ['preflight', 'sentinel', 'dependency-doctor'],
|
|
137
|
+
notes: ['no .windsurf/workflows directory'],
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
141
|
+
const runeFiles = entries.filter((e) => e.isFile() && e.name.startsWith(RUNE_PREFIX) && e.name.endsWith('.md'));
|
|
142
|
+
const wired = runeFiles.map((f) => f.name.replace(RUNE_PREFIX, '').replace('.md', ''));
|
|
143
|
+
const expected = ['preflight', 'sentinel', 'dependency-doctor'];
|
|
144
|
+
const missing = expected.filter((s) => !wired.includes(s));
|
|
145
|
+
|
|
146
|
+
let preset = null;
|
|
147
|
+
for (const file of runeFiles) {
|
|
148
|
+
const content = await readFile(path.join(dir, file.name), 'utf-8');
|
|
149
|
+
if (/^mode: BLOCK$/m.test(content)) {
|
|
150
|
+
preset = 'strict';
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
if (/^mode: WARN$/m.test(content)) preset = preset ?? 'gentle';
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return { installed: runeFiles.length > 0, preset, wired, missing, notes: [] };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function renderWorkflow(skill, mode) {
|
|
160
|
+
return [
|
|
161
|
+
'---',
|
|
162
|
+
`description: Rune ${skill.name} — run when ${skill.trigger}`,
|
|
163
|
+
'rune-managed: true',
|
|
164
|
+
`rune-skill: ${skill.name}`,
|
|
165
|
+
`mode: ${mode}`,
|
|
166
|
+
'---',
|
|
167
|
+
'',
|
|
168
|
+
`# /rune-${skill.name}`,
|
|
169
|
+
'',
|
|
170
|
+
`Run before ${skill.trigger}. Mode: ${mode}.`,
|
|
171
|
+
'',
|
|
172
|
+
'## Steps',
|
|
173
|
+
'',
|
|
174
|
+
`1. Review the target files for ${skill.name} concerns.`,
|
|
175
|
+
`2. Apply Rune skill guidance (see \`skills/${skill.name}/SKILL.md\`).`,
|
|
176
|
+
`3. If findings emerge: ${mode} and surface to the user.`,
|
|
177
|
+
'',
|
|
178
|
+
`_Auto-generated by \`rune hooks install\` (${AUTO_SIGNATURE})._`,
|
|
179
|
+
'',
|
|
180
|
+
].join('\n');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function renderRule(skill, mode) {
|
|
184
|
+
return [
|
|
185
|
+
'---',
|
|
186
|
+
`description: Auto-invoke /rune-${skill.name} when ${skill.trigger}`,
|
|
187
|
+
`globs: ${JSON.stringify(skill.globs)}`,
|
|
188
|
+
'alwaysApply: false',
|
|
189
|
+
'rune-managed: true',
|
|
190
|
+
`rune-skill: ${skill.name}`,
|
|
191
|
+
`mode: ${mode}`,
|
|
192
|
+
'---',
|
|
193
|
+
'',
|
|
194
|
+
`# Rune ${skill.name} cascade rule`,
|
|
195
|
+
'',
|
|
196
|
+
`When ${skill.trigger}, invoke \`/rune-${skill.name}\` workflow before committing the change.`,
|
|
197
|
+
`Mode: ${mode}. If the workflow surfaces a finding, ${mode} and report to user.`,
|
|
198
|
+
'',
|
|
199
|
+
`_Auto-generated by \`rune hooks install\` (${AUTO_SIGNATURE})._`,
|
|
200
|
+
'',
|
|
201
|
+
].join('\n');
|
|
202
|
+
}
|