@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,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook preset definitions for `rune hooks install`.
|
|
3
|
+
*
|
|
4
|
+
* Presets:
|
|
5
|
+
* - strict — dispatcher blocks on BLOCK verdict (returns non-zero exit)
|
|
6
|
+
* - gentle — dispatcher warns only (always exits 0), adds --gentle flag
|
|
7
|
+
* - off — no hooks installed (uninstall semantics)
|
|
8
|
+
*
|
|
9
|
+
* Each hook command is `rune hook-dispatch <skill>` so the dispatcher owns
|
|
10
|
+
* skill→command mapping. Commands carry the RUNE_MANAGED signature so we can
|
|
11
|
+
* detect and cleanly replace them without comment markers (settings.json is JSON).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export const RUNE_MANAGED_SIGNATURE = 'rune hook-dispatch';
|
|
15
|
+
|
|
16
|
+
/** Shared relative path to avoid per-file duplication. */
|
|
17
|
+
export const SETTINGS_REL_PATH = '.claude/settings.json';
|
|
18
|
+
|
|
19
|
+
const DISPATCH_CMD = 'npx --yes @rune-kit/rune hook-dispatch';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Regex that matches the exact dispatch invocation Rune writes.
|
|
23
|
+
* Matches: `npx [--yes] @rune-kit/rune hook-dispatch` or
|
|
24
|
+
* `node ... @rune-kit/rune hook-dispatch` as word boundary.
|
|
25
|
+
* Does NOT match arbitrary strings that merely contain those words.
|
|
26
|
+
*/
|
|
27
|
+
const RUNE_DISPATCH_RE = /(^|\s)npx(\s+--yes)?\s+@rune-kit\/rune\s+hook-dispatch\b/;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Regex that matches tier-emitted commands — they substitute a tier env var
|
|
31
|
+
* (e.g. `${RUNE_PRO_ROOT}`, `${RUNE_BUSINESS_ROOT}`). These are Rune-managed
|
|
32
|
+
* because only `rune hooks install --tier <name>` writes them.
|
|
33
|
+
*/
|
|
34
|
+
const RUNE_TIER_RE = /\$\{RUNE_[A-Z][A-Z0-9_]*_ROOT\}/;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Build a preset hooks block for merging into `.claude/settings.json`.
|
|
38
|
+
*
|
|
39
|
+
* @param {'strict'|'gentle'} preset
|
|
40
|
+
* @returns {Object} — { hooks: { PreToolUse: [...], PostToolUse: [...], Stop: [...] } }
|
|
41
|
+
*/
|
|
42
|
+
export function buildPreset(preset) {
|
|
43
|
+
if (preset !== 'strict' && preset !== 'gentle') {
|
|
44
|
+
throw new Error(`Unknown preset: ${preset}. Use 'strict' or 'gentle'.`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const flag = preset === 'gentle' ? ' --gentle' : '';
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
hooks: {
|
|
51
|
+
PreToolUse: [
|
|
52
|
+
{
|
|
53
|
+
matcher: 'Edit|Write',
|
|
54
|
+
hooks: [
|
|
55
|
+
{
|
|
56
|
+
type: 'command',
|
|
57
|
+
command: `${DISPATCH_CMD} preflight${flag}`,
|
|
58
|
+
async: preset === 'gentle',
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
matcher: 'Bash',
|
|
64
|
+
hooks: [
|
|
65
|
+
{
|
|
66
|
+
type: 'command',
|
|
67
|
+
command: `${DISPATCH_CMD} sentinel${flag}`,
|
|
68
|
+
async: false,
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
PostToolUse: [
|
|
74
|
+
{
|
|
75
|
+
matcher: 'Edit|Write',
|
|
76
|
+
hooks: [
|
|
77
|
+
{
|
|
78
|
+
type: 'command',
|
|
79
|
+
command: `${DISPATCH_CMD} dependency-doctor${flag}`,
|
|
80
|
+
async: true,
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
Stop: [
|
|
86
|
+
{
|
|
87
|
+
matcher: '.*',
|
|
88
|
+
hooks: [
|
|
89
|
+
{
|
|
90
|
+
type: 'command',
|
|
91
|
+
command: `${DISPATCH_CMD} completion-gate${flag}`,
|
|
92
|
+
async: false,
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Skills wired by presets — used by `rune hooks status` to verify skill existence.
|
|
103
|
+
*/
|
|
104
|
+
export const WIRED_SKILLS = ['preflight', 'sentinel', 'dependency-doctor', 'completion-gate'];
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Detect if a hook command entry is Rune-managed.
|
|
108
|
+
* Matches only the exact `npx [--yes] @rune-kit/rune hook-dispatch` invocation
|
|
109
|
+
* to avoid false-positives on user commands that merely contain those words.
|
|
110
|
+
*
|
|
111
|
+
* @param {Object} entry — single hook entry { type, command, ... }
|
|
112
|
+
*/
|
|
113
|
+
export function isRuneManaged(entry) {
|
|
114
|
+
if (!entry || typeof entry.command !== 'string') return false;
|
|
115
|
+
return RUNE_DISPATCH_RE.test(entry.command) || RUNE_TIER_RE.test(entry.command);
|
|
116
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `rune hooks status [--platform <name>|all]`
|
|
3
|
+
*
|
|
4
|
+
* For each detected (or requested) platform, reports:
|
|
5
|
+
* - installed (boolean)
|
|
6
|
+
* - preset (gentle | strict | mixed | null)
|
|
7
|
+
* - wired skills
|
|
8
|
+
* - missing skills (present-in-project-but-not-wired)
|
|
9
|
+
* - per-platform notes
|
|
10
|
+
*
|
|
11
|
+
* Claude Code adapter additionally returns `events` for hook-level detail.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { existsSync } from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { ADAPTERS, CAPABILITIES, detectPlatforms, getAdapter, PLATFORM_KEYS } from '../../adapters/hooks/index.js';
|
|
17
|
+
import { WIRED_SKILLS } from './presets.js';
|
|
18
|
+
import { checkManifestRequires, locateTierManifest, resolveTier } from './tiers.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {string} projectRoot
|
|
22
|
+
* @param {string} runeRoot
|
|
23
|
+
* @param {{platform?: string|string[], tier?: string|string[]}} args
|
|
24
|
+
*/
|
|
25
|
+
export async function hookStatus(projectRoot, runeRoot, args = {}) {
|
|
26
|
+
const platforms = resolvePlatforms(projectRoot, args.platform);
|
|
27
|
+
const missingInRepo = findMissingSkills(runeRoot, WIRED_SKILLS);
|
|
28
|
+
const tiers = await resolveRequestedTiers(projectRoot, args.tier);
|
|
29
|
+
|
|
30
|
+
if (platforms.length === 0) {
|
|
31
|
+
return {
|
|
32
|
+
platforms: [],
|
|
33
|
+
results: [],
|
|
34
|
+
tiers,
|
|
35
|
+
missingInRepo,
|
|
36
|
+
notes: [
|
|
37
|
+
'No target platform detected. Create `.claude/`, `.cursor/`, `.windsurf/`, or `.antigravity/` first, or pass `--platform <name>`.',
|
|
38
|
+
],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const results = [];
|
|
43
|
+
for (const id of platforms) {
|
|
44
|
+
const adapter = getAdapter(id);
|
|
45
|
+
const info = await adapter.status(projectRoot);
|
|
46
|
+
results.push({
|
|
47
|
+
platform: id,
|
|
48
|
+
capability: CAPABILITIES[id] ?? null,
|
|
49
|
+
...info,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return { platforms, results, tiers, missingInRepo, notes: [] };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function resolveRequestedTiers(projectRoot, requested) {
|
|
57
|
+
if (!requested) return [];
|
|
58
|
+
const list = Array.isArray(requested) ? requested : [requested];
|
|
59
|
+
const out = [];
|
|
60
|
+
for (const tier of Array.from(new Set(list))) {
|
|
61
|
+
const loc = locateTierManifest(tier, projectRoot);
|
|
62
|
+
if (!loc) {
|
|
63
|
+
out.push({ tier, found: false, manifestPath: null, requiresOk: false, requiresMissing: [], entries: 0 });
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
const manifest = await resolveTier(tier, projectRoot);
|
|
68
|
+
const req = checkManifestRequires(manifest);
|
|
69
|
+
out.push({
|
|
70
|
+
tier,
|
|
71
|
+
found: true,
|
|
72
|
+
manifestPath: loc,
|
|
73
|
+
requiresOk: req.ok,
|
|
74
|
+
requiresMissing: req.missing,
|
|
75
|
+
entries: manifest.entries.length,
|
|
76
|
+
version: manifest.version,
|
|
77
|
+
});
|
|
78
|
+
} catch (err) {
|
|
79
|
+
out.push({
|
|
80
|
+
tier,
|
|
81
|
+
found: false,
|
|
82
|
+
manifestPath: loc,
|
|
83
|
+
error: err.message,
|
|
84
|
+
requiresOk: false,
|
|
85
|
+
requiresMissing: [],
|
|
86
|
+
entries: 0,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function resolvePlatforms(projectRoot, requested) {
|
|
94
|
+
if (!requested) return detectPlatforms(projectRoot);
|
|
95
|
+
const list = Array.isArray(requested) ? requested : [requested];
|
|
96
|
+
const expanded = [];
|
|
97
|
+
for (const item of list) {
|
|
98
|
+
if (item === 'all') {
|
|
99
|
+
expanded.push(...PLATFORM_KEYS);
|
|
100
|
+
} else if (ADAPTERS[item]) {
|
|
101
|
+
expanded.push(item);
|
|
102
|
+
} else {
|
|
103
|
+
throw new Error(`Unknown platform: ${item}. Choose from: ${PLATFORM_KEYS.join(', ')}, all`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return Array.from(new Set(expanded));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function findMissingSkills(runeRoot, skills) {
|
|
110
|
+
const skillsDir = path.join(runeRoot, 'skills');
|
|
111
|
+
return skills.filter((skill) => !existsSync(path.join(skillsDir, skill, 'SKILL.md')));
|
|
112
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tier manifest loader for `rune hooks install/uninstall/status`.
|
|
3
|
+
*
|
|
4
|
+
* The Free compiler is tier-agnostic. Tiers (Pro / Business / custom) ship
|
|
5
|
+
* their own `hooks/manifest.json` files following the schema described in
|
|
6
|
+
* `docs/HOOKS.md`. This module resolves a `--tier` flag (or explicit path)
|
|
7
|
+
* into a loaded manifest.
|
|
8
|
+
*
|
|
9
|
+
* Resolution order for `--tier pro`:
|
|
10
|
+
* 1. `$RUNE_PRO_ROOT/hooks/manifest.json` (env var — primary)
|
|
11
|
+
* 2. `<projectRoot>/../Pro/hooks/manifest.json` (monorepo sibling)
|
|
12
|
+
* 3. Fails with a helpful upgrade message.
|
|
13
|
+
*
|
|
14
|
+
* For `--tier-manifest <path>` we load the file directly. This keeps the
|
|
15
|
+
* compiler side free of hardcoded tier logic — any future tier (Business,
|
|
16
|
+
* third-party) plugs in by shipping a manifest at a known path.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { existsSync } from 'node:fs';
|
|
20
|
+
import { readFile } from 'node:fs/promises';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
|
|
23
|
+
/** Known tier env vars. Adding a new tier = add its env var here. */
|
|
24
|
+
export const TIER_ENV_VARS = Object.freeze({
|
|
25
|
+
pro: 'RUNE_PRO_ROOT',
|
|
26
|
+
business: 'RUNE_BUSINESS_ROOT',
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
/** Valid event names a manifest entry may declare. */
|
|
30
|
+
const VALID_EVENTS = new Set(['UserPromptSubmit', 'PreToolUse', 'PostToolUse', 'Stop', 'statusLine']);
|
|
31
|
+
|
|
32
|
+
/** Tier names must be simple lowercase identifiers. Blocks `../../etc` and similar traversal. */
|
|
33
|
+
const TIER_NAME_RE = /^[a-z][a-z0-9-]{0,31}$/;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Assert a tier name is safe to use as a path segment.
|
|
37
|
+
* @param {string} tier
|
|
38
|
+
*/
|
|
39
|
+
function assertSafeTierName(tier) {
|
|
40
|
+
if (typeof tier !== 'string' || !TIER_NAME_RE.test(tier)) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
`Invalid tier name: ${JSON.stringify(tier)}. Tier must be a lowercase identifier (a-z, 0-9, dash).`,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Locate a tier manifest by tier name.
|
|
49
|
+
*
|
|
50
|
+
* @param {string} tier — 'pro' | 'business' | custom
|
|
51
|
+
* @param {string} projectRoot — the user project root (used for monorepo fallback)
|
|
52
|
+
* @returns {string|null} absolute path if found, else null
|
|
53
|
+
*/
|
|
54
|
+
export function locateTierManifest(tier, projectRoot) {
|
|
55
|
+
assertSafeTierName(tier);
|
|
56
|
+
const envVar = TIER_ENV_VARS[tier];
|
|
57
|
+
if (envVar && process.env[envVar]) {
|
|
58
|
+
// Resolve + re-anchor so that env var values can't traverse out via `..`.
|
|
59
|
+
const root = path.resolve(process.env[envVar]);
|
|
60
|
+
const candidate = path.join(root, 'hooks', 'manifest.json');
|
|
61
|
+
if (existsSync(candidate)) return candidate;
|
|
62
|
+
}
|
|
63
|
+
// Monorepo fallback: <projectRoot>/../<Tier>/hooks/manifest.json (capitalized tier).
|
|
64
|
+
// tier is pre-validated against TIER_NAME_RE so no traversal is possible here.
|
|
65
|
+
const capitalized = tier.charAt(0).toUpperCase() + tier.slice(1);
|
|
66
|
+
const fallback = path.resolve(projectRoot, '..', capitalized, 'hooks', 'manifest.json');
|
|
67
|
+
if (existsSync(fallback)) return fallback;
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Load + validate a tier manifest JSON file.
|
|
73
|
+
*
|
|
74
|
+
* @param {string} manifestPath
|
|
75
|
+
* @returns {Promise<TierManifest>}
|
|
76
|
+
*/
|
|
77
|
+
export async function loadTierManifest(manifestPath) {
|
|
78
|
+
if (!existsSync(manifestPath)) {
|
|
79
|
+
throw new Error(`Tier manifest not found: ${manifestPath}`);
|
|
80
|
+
}
|
|
81
|
+
const raw = await readFile(manifestPath, 'utf-8');
|
|
82
|
+
let parsed;
|
|
83
|
+
try {
|
|
84
|
+
parsed = JSON.parse(raw);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
throw new Error(`Tier manifest ${manifestPath} is not valid JSON: ${err.message}`);
|
|
87
|
+
}
|
|
88
|
+
return validateManifest(parsed, manifestPath);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Validate manifest shape + normalize to internal form.
|
|
93
|
+
* Throws with a specific error on any violation so users get actionable output.
|
|
94
|
+
*/
|
|
95
|
+
export function validateManifest(manifest, source = '<memory>') {
|
|
96
|
+
if (!manifest || typeof manifest !== 'object') {
|
|
97
|
+
throw new Error(`Manifest ${source}: expected top-level object`);
|
|
98
|
+
}
|
|
99
|
+
if (typeof manifest.tier !== 'string' || !manifest.tier) {
|
|
100
|
+
throw new Error(`Manifest ${source}: missing required string field 'tier'`);
|
|
101
|
+
}
|
|
102
|
+
if (!Array.isArray(manifest.entries)) {
|
|
103
|
+
throw new Error(`Manifest ${source}: 'entries' must be an array`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const seenIds = new Set();
|
|
107
|
+
const entries = manifest.entries.map((raw, i) => {
|
|
108
|
+
const where = `${source} entries[${i}]`;
|
|
109
|
+
if (!raw || typeof raw !== 'object') {
|
|
110
|
+
throw new Error(`${where}: expected object`);
|
|
111
|
+
}
|
|
112
|
+
if (typeof raw.id !== 'string' || !raw.id) {
|
|
113
|
+
throw new Error(`${where}: missing required string 'id'`);
|
|
114
|
+
}
|
|
115
|
+
if (seenIds.has(raw.id)) {
|
|
116
|
+
throw new Error(`${where}: duplicate entry id '${raw.id}'`);
|
|
117
|
+
}
|
|
118
|
+
seenIds.add(raw.id);
|
|
119
|
+
if (typeof raw.event !== 'string' || !VALID_EVENTS.has(raw.event)) {
|
|
120
|
+
throw new Error(
|
|
121
|
+
`${where}: 'event' must be one of ${[...VALID_EVENTS].join(', ')} (got ${JSON.stringify(raw.event)})`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
if (typeof raw.command !== 'string' || !raw.command) {
|
|
125
|
+
throw new Error(`${where}: missing required string 'command'`);
|
|
126
|
+
}
|
|
127
|
+
if (raw.matcher !== undefined && typeof raw.matcher !== 'string') {
|
|
128
|
+
throw new Error(`${where}: 'matcher' must be a string if present`);
|
|
129
|
+
}
|
|
130
|
+
if (raw.event !== 'statusLine' && raw.event !== 'Stop' && !raw.matcher) {
|
|
131
|
+
throw new Error(`${where}: '${raw.event}' requires a 'matcher' string (e.g. 'Edit|Write' or '.*')`);
|
|
132
|
+
}
|
|
133
|
+
if (raw.globs !== undefined && !Array.isArray(raw.globs)) {
|
|
134
|
+
throw new Error(`${where}: 'globs' must be an array of strings if present`);
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
id: raw.id,
|
|
138
|
+
skill: typeof raw.skill === 'string' && raw.skill ? raw.skill : raw.id,
|
|
139
|
+
event: raw.event,
|
|
140
|
+
matcher: raw.matcher ?? null,
|
|
141
|
+
command: raw.command,
|
|
142
|
+
async: raw.async === true,
|
|
143
|
+
padding: typeof raw.padding === 'number' ? raw.padding : undefined,
|
|
144
|
+
claudeOnly: raw.claudeOnly === true,
|
|
145
|
+
description: typeof raw.description === 'string' ? raw.description : '',
|
|
146
|
+
globs: Array.isArray(raw.globs) ? [...raw.globs] : null,
|
|
147
|
+
platforms: raw.platforms && typeof raw.platforms === 'object' ? { ...raw.platforms } : {},
|
|
148
|
+
};
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
name: typeof manifest.name === 'string' ? manifest.name : `Rune ${manifest.tier} Hooks`,
|
|
153
|
+
description: typeof manifest.description === 'string' ? manifest.description : '',
|
|
154
|
+
tier: manifest.tier,
|
|
155
|
+
version: typeof manifest.version === 'string' ? manifest.version : '0.0.0',
|
|
156
|
+
requires: Array.isArray(manifest.requires) ? [...manifest.requires] : [],
|
|
157
|
+
entries,
|
|
158
|
+
overrides: manifest.overrides && typeof manifest.overrides === 'object' ? { ...manifest.overrides } : {},
|
|
159
|
+
source,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Check whether a manifest's `requires` list is satisfied by current env.
|
|
165
|
+
* Returns { ok, missing }.
|
|
166
|
+
*/
|
|
167
|
+
export function checkManifestRequires(manifest) {
|
|
168
|
+
const missing = (manifest.requires || []).filter((name) => !process.env[name]);
|
|
169
|
+
return { ok: missing.length === 0, missing };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Resolve a tier request (`--tier pro`) into a loaded, validated manifest,
|
|
174
|
+
* or throw with a helpful upgrade/missing-env message.
|
|
175
|
+
*
|
|
176
|
+
* @param {string} tier
|
|
177
|
+
* @param {string} projectRoot
|
|
178
|
+
* @returns {Promise<TierManifest>}
|
|
179
|
+
*/
|
|
180
|
+
export async function resolveTier(tier, projectRoot) {
|
|
181
|
+
const manifestPath = locateTierManifest(tier, projectRoot);
|
|
182
|
+
if (!manifestPath) {
|
|
183
|
+
const envHint = TIER_ENV_VARS[tier] ? ` Set $${TIER_ENV_VARS[tier]} to your ${tier} install dir.` : '';
|
|
184
|
+
throw new Error(
|
|
185
|
+
`Could not locate '${tier}' tier manifest.${envHint}` +
|
|
186
|
+
` See https://rune.dev/docs/hooks#tiers for install instructions.`,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
const manifest = await loadTierManifest(manifestPath);
|
|
190
|
+
if (manifest.tier !== tier) {
|
|
191
|
+
throw new Error(`Manifest at ${manifestPath} declares tier='${manifest.tier}' but was requested as '${tier}'`);
|
|
192
|
+
}
|
|
193
|
+
return manifest;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* @typedef TierManifestEntry
|
|
198
|
+
* @property {string} id
|
|
199
|
+
* @property {string} skill
|
|
200
|
+
* @property {'UserPromptSubmit'|'PreToolUse'|'PostToolUse'|'Stop'|'statusLine'} event
|
|
201
|
+
* @property {string|null} matcher
|
|
202
|
+
* @property {string} command
|
|
203
|
+
* @property {boolean} async
|
|
204
|
+
* @property {number|undefined} padding
|
|
205
|
+
* @property {boolean} claudeOnly
|
|
206
|
+
* @property {string} description
|
|
207
|
+
* @property {string[]|null} globs
|
|
208
|
+
* @property {Record<string, string>} platforms
|
|
209
|
+
*/
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* @typedef TierManifest
|
|
213
|
+
* @property {string} name
|
|
214
|
+
* @property {string} description
|
|
215
|
+
* @property {string} tier
|
|
216
|
+
* @property {string} version
|
|
217
|
+
* @property {string[]} requires
|
|
218
|
+
* @property {TierManifestEntry[]} entries
|
|
219
|
+
* @property {Record<string, string>} overrides
|
|
220
|
+
* @property {string} source
|
|
221
|
+
*/
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `rune hooks uninstall [--platform <name>|all]`
|
|
3
|
+
*
|
|
4
|
+
* Removes all Rune-managed hook/rule/workflow entries for one or more platforms,
|
|
5
|
+
* leaving user entries intact. If no platform artifacts exist, no-op.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync } from 'node:fs';
|
|
9
|
+
import { unlink, writeFile } from 'node:fs/promises';
|
|
10
|
+
import { ADAPTERS, detectPlatforms, getAdapter, PLATFORM_KEYS } from '../../adapters/hooks/index.js';
|
|
11
|
+
import { resolveTier } from './tiers.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {string} projectRoot
|
|
15
|
+
* @param {{dry?: boolean, platform?: string|string[], tier?: string|string[]}} args
|
|
16
|
+
*/
|
|
17
|
+
export async function uninstallHooks(projectRoot, args = {}) {
|
|
18
|
+
const platforms = resolvePlatforms(projectRoot, args.platform);
|
|
19
|
+
const tiers = await resolveTierManifests(projectRoot, args.tier);
|
|
20
|
+
if (platforms.length === 0) {
|
|
21
|
+
return {
|
|
22
|
+
platforms: [],
|
|
23
|
+
results: [],
|
|
24
|
+
tiers: tiers.map((m) => m.tier),
|
|
25
|
+
written: false,
|
|
26
|
+
notes: ['no target platform detected'],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const results = [];
|
|
31
|
+
let totalWrites = 0;
|
|
32
|
+
for (const id of platforms) {
|
|
33
|
+
const adapter = getAdapter(id);
|
|
34
|
+
const plan = await adapter.uninstall({ projectRoot, tierManifests: tiers });
|
|
35
|
+
let platformWrites = 0;
|
|
36
|
+
if (!args.dry) {
|
|
37
|
+
for (const file of plan.files) {
|
|
38
|
+
if (file.content === null) {
|
|
39
|
+
if (existsSync(file.path)) {
|
|
40
|
+
await unlink(file.path);
|
|
41
|
+
platformWrites += 1;
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
await writeFile(file.path, file.content, 'utf-8');
|
|
45
|
+
platformWrites += 1;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
totalWrites += platformWrites;
|
|
50
|
+
results.push({
|
|
51
|
+
platform: id,
|
|
52
|
+
files: plan.files.map((f) => ({ path: f.path, deleted: f.content === null })),
|
|
53
|
+
notes: plan.notes,
|
|
54
|
+
writes: platformWrites,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
platforms,
|
|
60
|
+
tiers: tiers.map((m) => m.tier),
|
|
61
|
+
results,
|
|
62
|
+
written: !args.dry && totalWrites > 0,
|
|
63
|
+
notes: totalWrites === 0 && !args.dry ? ['no Rune-managed entries found'] : [],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function resolveTierManifests(projectRoot, requested) {
|
|
68
|
+
if (!requested) return [];
|
|
69
|
+
const list = Array.isArray(requested) ? requested : [requested];
|
|
70
|
+
const unique = Array.from(new Set(list.filter((t) => typeof t === 'string' && t.length > 0)));
|
|
71
|
+
const manifests = [];
|
|
72
|
+
for (const tier of unique) {
|
|
73
|
+
manifests.push(await resolveTier(tier, projectRoot));
|
|
74
|
+
}
|
|
75
|
+
return manifests;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function resolvePlatforms(projectRoot, requested) {
|
|
79
|
+
if (!requested) return detectPlatforms(projectRoot);
|
|
80
|
+
const list = Array.isArray(requested) ? requested : [requested];
|
|
81
|
+
const expanded = [];
|
|
82
|
+
for (const item of list) {
|
|
83
|
+
if (item === 'all') {
|
|
84
|
+
// Uninstall `all` walks detected platforms only. Named platforms still work
|
|
85
|
+
// even with no directory — adapter uninstall() returns empty for missing dirs.
|
|
86
|
+
expanded.push(...detectPlatforms(projectRoot));
|
|
87
|
+
} else if (ADAPTERS[item]) {
|
|
88
|
+
expanded.push(item);
|
|
89
|
+
} else {
|
|
90
|
+
throw new Error(`Unknown platform: ${item}. Choose from: ${PLATFORM_KEYS.join(', ')}, all`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return Array.from(new Set(expanded));
|
|
94
|
+
}
|