agency-rules 0.5.0 → 0.6.1

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 CHANGED
@@ -5,6 +5,28 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.6.1](https://github.com/deuriib/agency-rules/compare/v0.6.0...v0.6.1) (2026-10-31)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * **pkg:** move runtime dependencies out of devDependencies ([b39c1c8](https://github.com/deuriib/agency-rules/commit/b39c1c8e1f4e46c36c5854f8887b64c335cce01a))
14
+
15
+ ## [0.6.0](https://github.com/deuriib/agency-rules/compare/v0.5.0...v0.6.0) (2026-10-31)
16
+
17
+
18
+ ### Features
19
+
20
+ * **rules:** add Agentmemory MCP guidelines ([0d68b32](https://github.com/deuriib/agency-rules/commit/0d68b32e91b5385087b34d9dc93b45576bf88ceb))
21
+ * **rules:** add Agentmemory MCP guidelines ([1c846f5](https://github.com/deuriib/agency-rules/commit/1c846f5715137ea0d98d5365b99e4589a129a92b)), closes [#14](https://github.com/deuriib/agency-rules/issues/14)
22
+ * **rules:** add subagent delegation guidelines ([4527e1f](https://github.com/deuriib/agency-rules/commit/4527e1f7a16fb131a09406083562d59ff9f0eb8b))
23
+ * **rules:** add subagent delegation guidelines ([1042370](https://github.com/deuriib/agency-rules/commit/1042370beb15a15e9f9a57831a754da4ee05edbc)), closes [#15](https://github.com/deuriib/agency-rules/issues/15)
24
+
25
+
26
+ ### Bug Fixes
27
+
28
+ * unused variable via biome ([7bd25b1](https://github.com/deuriib/agency-rules/commit/7bd25b1c8c1b28521d6aaee57e1d5179e0cfafab))
29
+
8
30
  ## [0.5.0](https://github.com/deuriib/agency-rules/compare/v0.4.1...v0.5.0) (2026-10-31)
9
31
 
10
32
 
@@ -2,137 +2,25 @@
2
2
 
3
3
  import { readFile } from 'node:fs/promises';
4
4
  import { resolve } from 'node:path';
5
- import { pathToFileURL } from 'node:url';
6
-
7
- const command = process.argv[2];
8
-
9
- // ── Read version dynamically ──────────────────────────────────────────
10
- const getVersion = async () => {
11
- try {
12
- const pkgPath = resolve(import.meta.dirname, '..', 'package.json');
13
- const pkg = JSON.parse(await readFile(pkgPath, 'utf8'));
14
- return pkg.version || '0.0.0';
15
- } catch {
16
- return '0.0.0';
17
- }
18
- };
19
-
20
- const version = await getVersion();
21
-
22
- // ── Help Menus ────────────────────────────────────────────────────────
23
- const showHelp = () => {
24
- console.log(`
25
- \x1b[36magency-rules\x1b[0m v${version}
26
- Write rules once. Run them in every harness.
27
-
28
- \x1b[33mUsage:\x1b[0m
29
- $ agency-rules <command> [options]
30
-
31
- \x1b[33mCommands:\x1b[0m
32
- \x1b[32mlist\x1b[0m List all available rules
33
- \x1b[32mconvert\x1b[0m Compile rules/ into each harness's native format
34
- \x1b[32minstall\x1b[0m Install compiled artifacts into detected harnesses
35
- \x1b[32mvalidate\x1b[0m Validate each rule's frontmatter against the schema
36
-
37
- \x1b[33mGlobal Options:\x1b[0m
38
- -h, --help Show this help message
39
- -v, --version Show version number
40
-
41
- \x1b[33mExamples:\x1b[0m
42
- $ agency-rules list
43
- $ agency-rules validate
44
- $ agency-rules convert --dry-run
45
- $ agency-rules install --scope user --harness opencode
46
- `);
47
- };
48
-
49
- const showCommandHelp = (cmd) => {
50
- const helps = {
51
- list: `
52
- \x1b[36magency-rules list\x1b[0m
53
- List all available rules and their descriptions.
54
-
55
- \x1b[33mOptions:\x1b[0m
56
- --json Output the list in JSON format (useful for scripting)
57
-
58
- \x1b[33mExamples:\x1b[0m
59
- $ agency-rules list
60
- $ agency-rules list --json | jq '.'
61
- `,
62
- convert: `
63
- \x1b[36magency-rules convert\x1b[0m
64
- Compile rules/ into each harness's native format.
65
-
66
- \x1b[33mOptions:\x1b[0m
67
- --dry-run Preview which files would be written (no writes)
68
- --harness <id> Limit compilation to a specific harness (e.g., antigravity)
69
-
70
- \x1b[33mExamples:\x1b[0m
71
- $ agency-rules convert
72
- $ agency-rules convert --dry-run
73
- $ agency-rules convert --harness pi-agent
74
- `,
75
- install: `
76
- \x1b[36magency-rules install\x1b[0m
77
- Install compiled artifacts into detected harnesses.
78
-
79
- \x1b[33mOptions:\x1b[0m
80
- --dry-run Preview which files would be copied (no writes)
81
- --scope <project|user> Install local to project or global to user (default: project)
82
- --harness <id> Install into a specific harness
83
- --all Force installation for all harnesses, ignoring auto-detection
84
-
85
- \x1b[33mExamples:\x1b[0m
86
- $ agency-rules install
87
- $ agency-rules install --dry-run
88
- $ agency-rules install --scope user
89
- $ agency-rules install --harness opencode --scope project
90
- `,
91
- validate: `
92
- \x1b[36magency-rules validate\x1b[0m
93
- Validate each rule's frontmatter against the schema.
94
-
95
- \x1b[33mOptions:\x1b[0m
96
- (No specific options)
97
-
98
- \x1b[33mExamples:\x1b[0m
99
- $ agency-rules validate
100
- `,
101
- };
102
- console.log(helps[cmd] || '');
103
- };
104
-
105
- // ── Global Flags ──────────────────────────────────────────────────────
106
- if (process.argv.includes('--version') || process.argv.includes('-v')) {
107
- console.log(version);
108
- process.exit(0);
109
- }
110
-
111
- if (!command || command === 'help' || command === '--help' || command === '-h') {
112
- showHelp();
113
- process.exit(0);
114
- }
115
-
116
- if (!['list', 'convert', 'install', 'validate'].includes(command)) {
117
- console.error(`\x1b[31mError:\x1b[0m Unknown command '${command}'`);
118
- showHelp();
119
- process.exit(1);
120
- }
121
-
122
- // ── Command-specific Help ─────────────────────────────────────────────
123
- if (process.argv.includes('--help') || process.argv.includes('-h')) {
124
- showCommandHelp(command);
125
- process.exit(0);
126
- }
127
-
128
- // ── Execute ───────────────────────────────────────────────────────────
129
- process.argv.splice(2, 1);
130
-
131
- const scriptPath = resolve(import.meta.dirname, '..', 'scripts', `${command}.mjs`);
132
-
133
- try {
134
- await import(pathToFileURL(scriptPath).href);
135
- } catch (err) {
136
- console.error(`\x1b[31m❌ Failed to execute '${command}':\x1b[0m`, err.message);
137
- process.exit(1);
138
- }
5
+ import { defineCommand, runMain } from 'citty';
6
+
7
+ // Read version dynamically from package.json
8
+ const pkgPath = resolve(import.meta.dirname, '..', 'package.json');
9
+ const pkg = JSON.parse(await readFile(pkgPath, 'utf8'));
10
+
11
+ const main = defineCommand({
12
+ meta: {
13
+ name: 'agency-rules',
14
+ version: pkg.version,
15
+ description: 'Write rules once. Run them in every harness.',
16
+ },
17
+ subCommands: {
18
+ // Lazy load each command only when invoked
19
+ list: () => import('../scripts/list.mjs').then((m) => m.default),
20
+ convert: () => import('../scripts/convert.mjs').then((m) => m.default),
21
+ install: () => import('../scripts/install.mjs').then((m) => m.default),
22
+ validate: () => import('../scripts/validate.mjs').then((m) => m.default),
23
+ },
24
+ });
25
+
26
+ runMain(main);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agency-rules",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "description": "Write rules once. Run them in every harness.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -42,7 +42,8 @@
42
42
  "access": "public",
43
43
  "provenance": true
44
44
  },
45
- "devDependencies": {
45
+ "dependencies": {
46
+ "citty": "^0.2.2",
46
47
  "ajv": "^8.17.1",
47
48
  "gray-matter": "^4.0.3",
48
49
  "yaml": "^2.5.0"
@@ -0,0 +1,15 @@
1
+ ---
2
+ name: agentmemory
3
+ description: Guidelines and strict rules for interacting with the Agentmemory MCP server.
4
+ alwaysApply: true
5
+ tags: [mcp, memory, context]
6
+ ---
7
+
8
+ # Agentmemory MCP Rules
9
+
10
+ - **MEM-01** — Recall Before Asking. At the START of any task, call `memory_recall` (or `memory_smart_search`) with the task context before asking the user to repeat anything.
11
+ - **MEM-02** — Save at Decision Points. When you learn something durable (a decision, a fix, a gotcha, a user preference, a project convention), call `memory_save` in the same session — do not batch it for later.
12
+ - **MEM-03** — Lessons From Corrections. Signals this applies: explicit "always/never", "learn this", "from now on", a repeated user fix to the same behavior, "I told you before". Call `memory_save` with a confidence score; re-saving duplicate content strengthens the existing lesson instead of forking it. Before similar work, call `memory_lesson_recall` — check what was learned before deciding. One concept per lesson; state when and where it applies via context and tags.
13
+ - **MEM-04** — Resume, Don't Restart. Resume / recap / "where were we" → `memory_sessions` plus `memory_timeline` around the anchor before doing anything else. Lead with any unanswered question from the prior session. Use `memory_timeline` for chronological context and `memory_smart_search` with `expandIds` for detail — never re-derive what history already holds.
14
+ - **MEM-05** — Track Multi-Step Work as Actions. Work with 3+ steps → `memory_action_create` with typed dependencies; pull from `memory_frontier` / `memory_next`, not vibes. Memory lease acquire before starting an action; release or renew explicitly — leases prevent double-work across agents. Update status in real time; resolving to done unblocks dependents. Hand off via `memory_signal_send`, pick up via `memory_signal_read`.
15
+ - **MEM-06** — Slots Are Small, Pinned State. Durable user or project state (persona, pending items) → memory slots: `memory_slot_get` / `memory_slot_list` first, then `memory_slot_replace`. Appends fail past `sizeLimit` — compact via replace first, never loop blind appends. Seeded defaults are starting values, not gospel; replace them when the user corrects them.
@@ -0,0 +1,17 @@
1
+ ---
2
+ name: subagent-delegation
3
+ description: Guidelines for delegating tasks to subagents using spawn, dispatch, or invoke.
4
+ trigger: model_decision
5
+ tags: [workflow, subagents, context-management]
6
+ ---
7
+
8
+ # Subagent Delegation Rules
9
+
10
+ - **SUB-01** — Trigger Words. When the user says `spawn`, `dispatch`, or `invoke`, they are explicitly asking you to delegate the task to a subagent. Do not attempt to execute the task in the main thread.
11
+ - **SUB-02** — When to Delegate. Use subagents for:
12
+ - **Context-heavy tasks:** Searching large codebases, reading multiple files, or exploring documentation.
13
+ - **Parallelizable work:** Multiple independent fixes or searches that can happen concurrently.
14
+ - **Specialized tasks:** Writing tests, refactoring specific modules, or applying strict formatting.
15
+ - **SUB-03** — Clear Prompts. When spawning a subagent, provide a self-contained prompt. Do not assume the subagent has access to the current conversation history. Pass all necessary context, file paths, and constraints explicitly.
16
+ - **SUB-04** — Await and Synthesize. Wait for the subagent to complete and return its result. Verify the output and synthesize it back into the main workflow before presenting the final answer to the user.
17
+ - **SUB-05** — Fail Safely. If a subagent fails or hits a limit, do not silently retry in the main thread. Report the failure, analyze the error, and decide whether to retry the subagent with a better prompt or escalate to the user.
@@ -1,114 +1,123 @@
1
- import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
2
  import { dirname, join, resolve } from 'node:path';
3
- import { parseArgs } from 'node:util';
3
+ import { defineCommand } from 'citty';
4
4
  import { readRules } from './lib/frontmatter.mjs';
5
5
  import { renderers } from './lib/renderers/index.mjs';
6
6
 
7
- const { values: args } = parseArgs({
8
- options: {
9
- 'dry-run': { type: 'boolean', default: false },
10
- harness: { type: 'string' },
7
+ export default defineCommand({
8
+ meta: {
9
+ name: 'convert',
10
+ description: "Compile rules/ into each harness's native format",
11
11
  },
12
- });
13
-
14
- const dryRun = args['dry-run'];
15
- const prefix = dryRun ? ' [dry-run]' : ' ';
16
-
17
- // Resolve paths: Use local if exists, otherwise fallback to the package's bundled files
18
- const PACKAGE_ROOT = resolve(import.meta.dirname, '..');
19
- const localRules = resolve(process.cwd(), 'rules');
20
- const localTools = resolve(process.cwd(), 'tools.json');
21
-
22
- const RULES_DIR =
23
- process.env.RULES_DIR ||
24
- ((await import('node:fs').then((fs) => fs.existsSync(localRules)))
25
- ? localRules
26
- : resolve(PACKAGE_ROOT, 'rules'));
27
- const TOOLS_PATH = (await import('node:fs').then((fs) => fs.existsSync(localTools)))
28
- ? localTools
29
- : resolve(PACKAGE_ROOT, 'tools.json');
30
-
31
- const INTEGRATIONS_DIR = process.env.INTEGRATIONS_DIR || 'integrations';
32
-
33
- const tools = JSON.parse(await readFile(TOOLS_PATH, 'utf8'));
34
- const rules = await readRules(RULES_DIR);
35
-
36
- console.log(
37
- `📚 Found ${rules.length} rule(s) in ${RULES_DIR}${dryRun ? ' (dry-run — nothing will be written)' : ''}\n`,
38
- );
39
-
40
- const harnessIds = args.harness ? [args.harness] : Object.keys(tools.harnesses);
41
-
42
- for (const id of harnessIds) {
43
- if (!tools.harnesses[id]) {
44
- console.error(`❌ Unknown harness: ${id}`);
45
- process.exit(1);
46
- }
47
- }
48
-
49
- let totalFiles = 0;
50
-
51
- for (const id of harnessIds) {
52
- const harness = tools.harnesses[id];
53
- const render = renderers[harness.format];
54
- if (!render) {
55
- console.error(`❌ No renderer for format "${harness.format}" (harness: ${id})`);
56
- process.exit(1);
57
- }
58
-
59
- if (!harness.integrationPath) {
60
- console.error(`❌ Missing "integrationPath" for harness "${id}" in tools.json`);
61
- process.exit(1);
62
- }
63
-
64
- const harnessDir = join(INTEGRATIONS_DIR, id);
65
- const manifest = [];
66
-
67
- for (const rule of rules) {
68
- let output;
69
- try {
70
- output = render(rule, harness);
71
- } catch (err) {
72
- console.error(`❌ ${id}: failed to render "${rule.slug}": ${err.message}`);
12
+ args: {
13
+ 'dry-run': {
14
+ type: 'boolean',
15
+ description: 'Preview which files would be written (no writes)',
16
+ },
17
+ harness: { type: 'string', description: 'Limit compilation to a specific harness' },
18
+ },
19
+ async run({ args }) {
20
+ const dryRun = args['dry-run'];
21
+ const prefix = dryRun ? ' [dry-run]' : ' ';
22
+
23
+ const PACKAGE_ROOT = resolve(import.meta.dirname, '..');
24
+ const localRules = resolve(process.cwd(), 'rules');
25
+ const localTools = resolve(process.cwd(), 'tools.json');
26
+
27
+ const RULES_DIR =
28
+ process.env.RULES_DIR ||
29
+ ((await import('node:fs').then((fs) => fs.existsSync(localRules)))
30
+ ? localRules
31
+ : resolve(PACKAGE_ROOT, 'rules'));
32
+ const TOOLS_PATH = (await import('node:fs').then((fs) => fs.existsSync(localTools)))
33
+ ? localTools
34
+ : resolve(PACKAGE_ROOT, 'tools.json');
35
+
36
+ const INTEGRATIONS_DIR = process.env.INTEGRATIONS_DIR || 'integrations';
37
+
38
+ const tools = JSON.parse(await readFile(TOOLS_PATH, 'utf8'));
39
+ const rules = await readRules(RULES_DIR);
40
+
41
+ if (rules.length === 0) {
42
+ console.error(`❌ No rules found in ${RULES_DIR}`);
73
43
  process.exit(1);
74
44
  }
75
45
 
76
- const relPath = harness.integrationPath.replace('{{slug}}', rule.slug);
77
- const outPath = join(harnessDir, relPath);
78
- manifest.push({ slug: rule.slug, path: relPath, size: output.length });
79
- totalFiles++;
46
+ const harnessIds = args.harness ? [args.harness] : Object.keys(tools.harnesses);
80
47
 
81
- if (!dryRun) {
82
- await mkdir(dirname(outPath), { recursive: true });
83
- await writeFile(outPath, output);
48
+ for (const id of harnessIds) {
49
+ if (!tools.harnesses[id]) {
50
+ console.error(`❌ Unknown harness: ${id}`);
51
+ process.exit(1);
52
+ }
84
53
  }
85
- }
86
-
87
- if (!dryRun) {
88
- await writeFile(
89
- join(harnessDir, '_manifest.json'),
90
- JSON.stringify(
91
- {
92
- harness: id,
93
- format: harness.format,
94
- files: manifest.map(({ slug, path }) => ({ slug, path })),
95
- },
96
- null,
97
- 2,
98
- ),
54
+
55
+ console.log(
56
+ `📚 Found ${rules.length} rule(s) in ${RULES_DIR}${dryRun ? ' (dry-run — nothing will be written)' : ''}\n`,
99
57
  );
100
- }
101
58
 
102
- console.log(`✅ ${id.padEnd(12)} → ${manifest.length} file(s)`);
103
- if (dryRun) {
104
- for (const f of manifest) {
105
- console.log(`${prefix} would write ${join(harnessDir, f.path)} (${f.size} bytes)`);
59
+ let totalFiles = 0;
60
+
61
+ for (const id of harnessIds) {
62
+ const harness = tools.harnesses[id];
63
+ const render = renderers[harness.format];
64
+ if (!render) {
65
+ console.error(`❌ No renderer for format "${harness.format}" (harness: ${id})`);
66
+ process.exit(1);
67
+ }
68
+
69
+ const harnessDir = join(INTEGRATIONS_DIR, id);
70
+ const manifest = [];
71
+
72
+ for (const rule of rules) {
73
+ let output;
74
+ try {
75
+ output = render(rule, harness);
76
+ } catch (err) {
77
+ console.error(`❌ ${id}: failed to render "${rule.slug}": ${err.message}`);
78
+ process.exit(1);
79
+ }
80
+
81
+ const relPath = harness.integrationPath.replace('{{slug}}', rule.slug);
82
+ const outPath = join(harnessDir, relPath);
83
+ manifest.push({ slug: rule.slug, path: relPath, size: output.length });
84
+ totalFiles++;
85
+
86
+ if (!dryRun) {
87
+ await mkdir(dirname(outPath), { recursive: true });
88
+ await writeFile(outPath, output);
89
+ }
90
+ }
91
+
92
+ if (!dryRun) {
93
+ await writeFile(
94
+ join(harnessDir, '_manifest.json'),
95
+ JSON.stringify(
96
+ {
97
+ harness: id,
98
+ format: harness.format,
99
+ files: manifest.map(({ slug, path }) => ({ slug, path })),
100
+ },
101
+ null,
102
+ 2,
103
+ ),
104
+ );
105
+ }
106
+
107
+ console.log(`✅ ${id.padEnd(12)} → ${manifest.length} file(s)`);
108
+ if (dryRun) {
109
+ for (const f of manifest) {
110
+ console.log(
111
+ `${prefix} would write ${join(harnessDir, f.path)} (${f.size} bytes)`,
112
+ );
113
+ }
114
+ }
106
115
  }
107
- }
108
- }
109
116
 
110
- const summary = dryRun
111
- ? `\n🔎 Dry-run: would write ${totalFiles} file(s) across ${harnessIds.length} harness(es). Nothing was written.`
112
- : `\n🎉 Wrote ${totalFiles} file(s) across ${harnessIds.length} harness(es).`;
117
+ const summary = dryRun
118
+ ? `\n🔎 Dry-run: would write ${totalFiles} file(s) across ${harnessIds.length} harness(es). Nothing was written.`
119
+ : `\n🎉 Wrote ${totalFiles} file(s) across ${harnessIds.length} harness(es).`;
113
120
 
114
- console.log(summary);
121
+ console.log(summary);
122
+ },
123
+ });
@@ -3,159 +3,176 @@ import { copyFile, mkdir, readFile, writeFile } from 'node:fs/promises';
3
3
  import { dirname, join, resolve } from 'node:path';
4
4
  import { stdin as input, stdout as output } from 'node:process';
5
5
  import { createInterface } from 'node:readline/promises';
6
- import { parseArgs } from 'node:util';
6
+ import { defineCommand } from 'citty';
7
7
  import { detectHarnesses, expandHome, pathExists } from './lib/detect.mjs';
8
8
 
9
- const { values: args } = parseArgs({
10
- options: {
11
- scope: { type: 'string' },
12
- harness: { type: 'string' },
13
- all: { type: 'boolean', default: false },
14
- 'dry-run': { type: 'boolean', default: false },
9
+ export default defineCommand({
10
+ meta: {
11
+ name: 'install',
12
+ description: 'Install compiled artifacts into detected harnesses',
15
13
  },
16
- });
17
-
18
- const dryRun = args['dry-run'];
19
- const tag = dryRun ? '[dry-run]' : ' ';
20
- const log = (msg) => console.log(` ${tag} ${msg}`);
21
-
22
- const INTEGRATIONS_DIR = process.env.INTEGRATIONS_DIR || 'integrations';
23
-
24
- const PACKAGE_ROOT = resolve(import.meta.dirname, '..');
25
- const localTools = resolve(process.cwd(), 'tools.json');
26
- const TOOLS_PATH = existsSync(localTools) ? localTools : resolve(PACKAGE_ROOT, 'tools.json');
27
-
28
- const tools = JSON.parse(await readFile(TOOLS_PATH, 'utf8'));
29
-
30
- let finalScope = args.scope;
31
- let finalHarness = args.harness;
32
-
33
- // ── Interactive Mode (si no se pasan parámetros y es una terminal) ─────
34
- if (input.isTTY && ((!finalHarness && !args.all) || !finalScope)) {
35
- const rl = createInterface({ input, output });
36
-
37
- if (!finalHarness && !args.all) {
38
- console.log('\n🎯 Select a harness to install into:');
39
- const keys = Object.keys(tools.harnesses);
40
- keys.forEach((k, i) => {
41
- console.log(` ${i + 1}) ${k} (${tools.harnesses[k].label})`);
42
- });
43
- console.log(` ${keys.length + 1}) Detect automatically`);
44
- console.log(` ${keys.length + 2}) All harnesses`);
45
-
46
- const answer = await rl.question(`\nChoice [1-${keys.length + 2}] (default: detect): `);
47
- const num = parseInt(answer.trim(), 10);
48
-
49
- if (!Number.isNaN(num) && num >= 1 && num <= keys.length) {
50
- finalHarness = keys[num - 1];
51
- } else if (num === keys.length + 2 || answer.trim().toLowerCase() === 'all') {
52
- args.all = true;
14
+ args: {
15
+ scope: {
16
+ type: 'string',
17
+ description: 'Install local to project or global to user (default: project)',
18
+ },
19
+ harness: { type: 'string', description: 'Install into a specific harness' },
20
+ all: {
21
+ type: 'boolean',
22
+ description: 'Force installation for all harnesses, ignoring auto-detection',
23
+ },
24
+ 'dry-run': {
25
+ type: 'boolean',
26
+ description: 'Preview which files would be copied (no writes)',
27
+ },
28
+ },
29
+ async run({ args }) {
30
+ const dryRun = args['dry-run'];
31
+ const tag = dryRun ? '[dry-run]' : ' ';
32
+ const log = (msg) => console.log(` ${tag} ${msg}`);
33
+
34
+ const INTEGRATIONS_DIR = process.env.INTEGRATIONS_DIR || 'integrations';
35
+
36
+ const PACKAGE_ROOT = resolve(import.meta.dirname, '..');
37
+ const localTools = resolve(process.cwd(), 'tools.json');
38
+ const TOOLS_PATH = existsSync(localTools)
39
+ ? localTools
40
+ : resolve(PACKAGE_ROOT, 'tools.json');
41
+
42
+ const tools = JSON.parse(await readFile(TOOLS_PATH, 'utf8'));
43
+
44
+ let finalScope = args.scope;
45
+ let finalHarness = args.harness;
46
+ let forceAll = args.all;
47
+
48
+ // Interactive Mode
49
+ if (input.isTTY && ((!finalHarness && !forceAll) || !finalScope)) {
50
+ const rl = createInterface({ input, output });
51
+
52
+ if (!finalHarness && !forceAll) {
53
+ console.log('\n🎯 Select a harness to install into:');
54
+ const keys = Object.keys(tools.harnesses);
55
+ keys.forEach((k, i) => {
56
+ console.log(` ${i + 1}) ${k} (${tools.harnesses[k].label})`);
57
+ });
58
+ console.log(` ${keys.length + 1}) Detect automatically`);
59
+ console.log(` ${keys.length + 2}) All harnesses`);
60
+
61
+ const answer = await rl.question(
62
+ `\nChoice [1-${keys.length + 2}] (default: detect): `,
63
+ );
64
+ const num = parseInt(answer.trim(), 10);
65
+
66
+ if (!Number.isNaN(num) && num >= 1 && num <= keys.length) {
67
+ finalHarness = keys[num - 1];
68
+ } else if (num === keys.length + 2 || answer.trim().toLowerCase() === 'all') {
69
+ forceAll = true;
70
+ }
71
+ }
72
+
73
+ if (!finalScope) {
74
+ console.log('\n🌍 Select installation scope:');
75
+ console.log(' 1) project (local to this repo)');
76
+ console.log(' 2) user (global for your user)');
77
+
78
+ const answer = await rl.question('\nChoice [1-2] (default: 1): ');
79
+ finalScope =
80
+ answer.trim() === '2' || answer.trim().toLowerCase() === 'user'
81
+ ? 'user'
82
+ : 'project';
83
+ }
84
+
85
+ rl.close();
86
+ console.log('');
53
87
  }
54
- }
55
-
56
- if (!finalScope) {
57
- console.log('\n🌍 Select installation scope:');
58
- console.log(' 1) project (local to this repo)');
59
- console.log(' 2) user (global for your user)');
60
-
61
- const answer = await rl.question('\nChoice [1-2] (default: 1): ');
62
- finalScope =
63
- answer.trim() === '2' || answer.trim().toLowerCase() === 'user' ? 'user' : 'project';
64
- }
65
88
 
66
- rl.close();
67
- console.log('');
68
- }
69
-
70
- // Fallback si no se seleccionó nada
71
- if (!finalScope) finalScope = 'project';
89
+ if (!finalScope) finalScope = 'project';
72
90
 
73
- if (!['project', 'user'].includes(finalScope)) {
74
- console.error(`❌ Invalid --scope: ${finalScope} (use "project" or "user")`);
75
- process.exit(1);
76
- }
77
-
78
- let targets;
79
- if (finalHarness) {
80
- if (!tools.harnesses[finalHarness]) {
81
- console.error(`❌ Unknown harness: ${finalHarness}`);
82
- process.exit(1);
83
- }
84
- targets = [finalHarness];
85
- } else if (args.all) {
86
- targets = Object.keys(tools.harnesses);
87
- } else {
88
- targets = await detectHarnesses(tools);
89
- if (targets.length === 0) {
90
- console.warn('⚠️ No harness detected. Pass --harness <id> or --all to force.');
91
- process.exit(0);
92
- }
93
- }
94
-
95
- console.log(
96
- `🔍 Targets: ${targets.join(', ')} (scope: ${finalScope})${
97
- dryRun ? ' — dry-run, no changes will be made' : ''
98
- }\n`,
99
- );
100
-
101
- let copied = 0;
102
- let skippedHarnesses = 0;
103
- let pluginChanges = 0;
91
+ if (!['project', 'user'].includes(finalScope)) {
92
+ console.error(`❌ Invalid --scope: ${finalScope} (use "project" or "user")`);
93
+ process.exit(1);
94
+ }
104
95
 
105
- for (const id of targets) {
106
- const harness = tools.harnesses[id];
107
- const srcDir = join(INTEGRATIONS_DIR, id);
96
+ let targets;
97
+ if (finalHarness) {
98
+ if (!tools.harnesses[finalHarness]) {
99
+ console.error(`❌ Unknown harness: ${finalHarness}`);
100
+ process.exit(1);
101
+ }
102
+ targets = [finalHarness];
103
+ } else if (forceAll) {
104
+ targets = Object.keys(tools.harnesses);
105
+ } else {
106
+ targets = await detectHarnesses(tools);
107
+ if (targets.length === 0) {
108
+ console.warn('⚠️ No harness detected. Pass --harness <id> or --all to force.');
109
+ process.exit(0);
110
+ }
111
+ }
108
112
 
109
- if (!(await pathExists(srcDir))) {
110
- console.warn(
111
- `⚠️ ${id}: no compiled output at ${srcDir}, skipping. Run "mise run convert" first.`,
113
+ console.log(
114
+ `🔍 Targets: ${targets.join(', ')} (scope: ${finalScope})${dryRun ? ' — dry-run, no changes will be made' : ''}\n`,
112
115
  );
113
- skippedHarnesses++;
114
- continue;
115
- }
116
-
117
- const manifest = JSON.parse(await readFile(join(srcDir, '_manifest.json'), 'utf8'));
118
- const destTemplate = harness.dest[finalScope];
119
- if (!destTemplate) {
120
- console.warn(`⚠️ ${id}: no dest for scope "${finalScope}"`);
121
- skippedHarnesses++;
122
- continue;
123
- }
124
116
 
125
- for (const { slug, path: relPath } of manifest.files) {
126
- const destPath = expandHome(destTemplate.replace('{{slug}}', slug));
127
- if (dryRun) {
128
- log(`${id}: would copy ${relPath} → ${destPath}`);
129
- } else {
130
- await mkdir(dirname(destPath), { recursive: true });
131
- await copyFile(join(srcDir, relPath), destPath);
132
- log(`${id}: ${destPath}`);
117
+ let copied = 0;
118
+ let skippedHarnesses = 0;
119
+ let pluginChanges = 0;
120
+
121
+ for (const id of targets) {
122
+ const harness = tools.harnesses[id];
123
+ const srcDir = join(INTEGRATIONS_DIR, id);
124
+
125
+ if (!(await pathExists(srcDir))) {
126
+ console.warn(
127
+ `⚠️ ${id}: no compiled output at ${srcDir}, skipping. Run "mise run convert" first.`,
128
+ );
129
+ skippedHarnesses++;
130
+ continue;
131
+ }
132
+
133
+ const manifest = JSON.parse(await readFile(join(srcDir, '_manifest.json'), 'utf8'));
134
+ const destTemplate = harness.dest[finalScope];
135
+ if (!destTemplate) {
136
+ console.warn(`⚠️ ${id}: no dest for scope "${finalScope}"`);
137
+ skippedHarnesses++;
138
+ continue;
139
+ }
140
+
141
+ for (const { slug, path: relPath } of manifest.files) {
142
+ const destPath = expandHome(destTemplate.replace('{{slug}}', slug));
143
+ if (dryRun) {
144
+ log(`${id}: would copy ${relPath} → ${destPath}`);
145
+ } else {
146
+ await mkdir(dirname(destPath), { recursive: true });
147
+ await copyFile(join(srcDir, relPath), destPath);
148
+ log(`${id}: ${destPath}`);
149
+ }
150
+ copied++;
151
+ }
152
+
153
+ if (harness.plugin) {
154
+ const changed = await handlePlugin(id, harness.plugin, finalScope, dryRun, log);
155
+ if (changed) pluginChanges++;
156
+ }
133
157
  }
134
- copied++;
135
- }
136
158
 
137
- if (harness.plugin) {
138
- const changed = await handlePlugin(id, harness.plugin, finalScope, dryRun, log);
139
- if (changed) pluginChanges++;
140
- }
141
- }
159
+ console.log(
160
+ dryRun
161
+ ? `\n🔎 Dry-run: would copy ${copied} file(s) across ${targets.length - skippedHarnesses} harness(es).` +
162
+ (pluginChanges ? ` Would update ${pluginChanges} plugin config(s).` : '') +
163
+ ' Nothing was written.'
164
+ : `\n🎉 Installed ${copied} file(s) across ${targets.length - skippedHarnesses} harness(es).` +
165
+ (pluginChanges ? ` Updated ${pluginChanges} plugin config(s).` : ''),
166
+ );
142
167
 
143
- console.log(
144
- dryRun
145
- ? `\n🔎 Dry-run: would copy ${copied} file(s) across ${targets.length - skippedHarnesses} harness(es).` +
146
- (pluginChanges ? ` Would update ${pluginChanges} plugin config(s).` : '') +
147
- ' Nothing was written.'
148
- : `\n🎉 Installed ${copied} file(s) across ${targets.length - skippedHarnesses} harness(es).` +
149
- (pluginChanges ? ` Updated ${pluginChanges} plugin config(s).` : ''),
150
- );
151
-
152
- if (dryRun && skippedHarnesses > 0) {
153
- console.log(
154
- `\n⚠️ ${skippedHarnesses} harness(es) skipped due to missing integrations. Run "mise run convert" first.`,
155
- );
156
- }
168
+ if (dryRun && skippedHarnesses > 0) {
169
+ console.log(
170
+ `\n⚠️ ${skippedHarnesses} harness(es) skipped due to missing integrations. Run "mise run convert" first.`,
171
+ );
172
+ }
173
+ },
174
+ });
157
175
 
158
- // ── Plugin registration ──────────────────────────────────────────────
159
176
  async function handlePlugin(id, plugin, scope, dry, logFn) {
160
177
  const configPath =
161
178
  scope === 'user'
package/scripts/list.mjs CHANGED
@@ -1,48 +1,53 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { resolve } from 'node:path';
3
- import { parseArgs } from 'node:util';
3
+ import { defineCommand } from 'citty';
4
4
  import { readRules } from './lib/frontmatter.mjs';
5
5
 
6
- const { values: args } = parseArgs({
7
- options: {
8
- json: { type: 'boolean', default: false },
6
+ export default defineCommand({
7
+ meta: {
8
+ name: 'list',
9
+ description: 'List all available rules',
10
+ },
11
+ args: {
12
+ json: {
13
+ type: 'boolean',
14
+ description: 'Output in JSON format',
15
+ default: false,
16
+ },
17
+ },
18
+ async run({ args }) {
19
+ const PACKAGE_ROOT = resolve(import.meta.dirname, '..');
20
+ const localRules = resolve(process.cwd(), 'rules');
21
+ const RULES_DIR =
22
+ process.env.RULES_DIR ||
23
+ (existsSync(localRules) ? localRules : resolve(PACKAGE_ROOT, 'rules'));
24
+
25
+ const rules = await readRules(RULES_DIR);
26
+
27
+ if (rules.length === 0) {
28
+ console.error(`❌ No rules found in ${RULES_DIR}`);
29
+ process.exit(1);
30
+ }
31
+
32
+ if (args.json) {
33
+ const output = rules.map((r) => ({
34
+ name: r.slug,
35
+ description: r.data.description || 'No description provided',
36
+ source: r.source,
37
+ }));
38
+ console.log(JSON.stringify(output, null, 2));
39
+ return;
40
+ }
41
+
42
+ const maxSlugLength = Math.max(...rules.map((r) => r.slug.length));
43
+ console.log(`\n📚 \x1b[1mAvailable Rules\x1b[0m (${rules.length})\n`);
44
+
45
+ for (const rule of rules) {
46
+ const paddedSlug = rule.slug.padEnd(maxSlugLength);
47
+ console.log(
48
+ ` \x1b[36m${paddedSlug}\x1b[0m ${rule.data.description || 'No description'}`,
49
+ );
50
+ }
51
+ console.log('\n');
9
52
  },
10
53
  });
11
-
12
- // Resolve paths (local vs package fallback)
13
- const PACKAGE_ROOT = resolve(import.meta.dirname, '..');
14
- const localRules = resolve(process.cwd(), 'rules');
15
- const RULES_DIR =
16
- process.env.RULES_DIR || (existsSync(localRules) ? localRules : resolve(PACKAGE_ROOT, 'rules'));
17
-
18
- const rules = await readRules(RULES_DIR);
19
-
20
- if (rules.length === 0) {
21
- console.error(`❌ No rules found in ${RULES_DIR}`);
22
- process.exit(1);
23
- }
24
-
25
- // ── JSON Output ───────────────────────────────────────────────────────
26
- if (args.json) {
27
- const output = rules.map((r) => ({
28
- name: r.slug,
29
- description: r.data.description || 'No description provided',
30
- source: r.source,
31
- }));
32
- console.log(JSON.stringify(output, null, 2));
33
- process.exit(0);
34
- }
35
-
36
- // ── Pretty Terminal Output ────────────────────────────────────────────
37
- // Find the longest name to align the columns perfectly
38
- const maxSlugLength = Math.max(...rules.map((r) => r.slug.length));
39
-
40
- console.log(`\n📚 \x1b[1mAvailable Rules\x1b[0m (${rules.length})\n`);
41
-
42
- for (const rule of rules) {
43
- const paddedSlug = rule.slug.padEnd(maxSlugLength);
44
- // Using Cyan for the rule name
45
- console.log(` \x1b[36m${paddedSlug}\x1b[0m ${rule.data.description || 'No description'}`);
46
- }
47
-
48
- console.log('\n');
@@ -1,52 +1,67 @@
1
1
  import { readFile } from 'node:fs/promises';
2
- import { join } from 'node:path';
2
+ import { join, resolve } from 'node:path';
3
3
  import Ajv from 'ajv';
4
+ import { defineCommand } from 'citty';
4
5
  import { readRules } from './lib/frontmatter.mjs';
5
6
 
6
- const RULES_DIR = process.env.RULES_DIR || 'rules';
7
+ export default defineCommand({
8
+ meta: {
9
+ name: 'validate',
10
+ description: "Validate each rule's frontmatter against the schema",
11
+ },
12
+ async run() {
13
+ const PACKAGE_ROOT = resolve(import.meta.dirname, '..');
14
+ const localRules = resolve(process.cwd(), 'rules');
15
+ const RULES_DIR =
16
+ process.env.RULES_DIR || (localRules ? localRules : resolve(PACKAGE_ROOT, 'rules'));
7
17
 
8
- const schema = JSON.parse(await readFile(join(RULES_DIR, '_schema.json'), 'utf8'));
9
- const ajv = new Ajv({ allErrors: true, strict: false });
10
- const validate = ajv.compile(schema);
18
+ const schema = JSON.parse(await readFile(join(RULES_DIR, '_schema.json'), 'utf8'));
19
+ const ajv = new Ajv({ allErrors: true, strict: false });
20
+ const validate = ajv.compile(schema);
11
21
 
12
- const rules = await readRules(RULES_DIR);
22
+ const rules = await readRules(RULES_DIR);
13
23
 
14
- if (rules.length === 0) {
15
- console.error(`❌ No rules found in ${RULES_DIR}`);
16
- process.exit(1);
17
- }
24
+ if (rules.length === 0) {
25
+ console.error(`❌ No rules found in ${RULES_DIR}`);
26
+ process.exit(1);
27
+ }
18
28
 
19
- const seen = new Set();
20
- let failures = 0;
29
+ const seen = new Set();
30
+ let failures = 0;
21
31
 
22
- for (const rule of rules) {
23
- const errors = [];
32
+ for (const rule of rules) {
33
+ const errors = [];
24
34
 
25
- if (seen.has(rule.slug)) {
26
- errors.push({ instancePath: '/name', message: `duplicate rule name "${rule.slug}"` });
27
- }
28
- seen.add(rule.slug);
35
+ if (seen.has(rule.slug)) {
36
+ errors.push({
37
+ instancePath: '/name',
38
+ message: `duplicate rule name "${rule.slug}"`,
39
+ });
40
+ }
41
+ seen.add(rule.slug);
29
42
 
30
- if (!validate(rule.data)) {
31
- for (const e of validate.errors ?? []) {
32
- errors.push(e);
33
- }
34
- }
43
+ if (!validate(rule.data)) {
44
+ for (const e of validate.errors ?? []) {
45
+ errors.push(e);
46
+ }
47
+ }
35
48
 
36
- if (errors.length > 0) {
37
- failures++;
38
- console.error(`❌ ${rule.source}`);
39
- for (const e of errors) {
40
- console.error(` ${e.instancePath || '/'} ${e.message}`);
49
+ if (errors.length > 0) {
50
+ failures++;
51
+ console.error(`❌ ${rule.source}`);
52
+ for (const e of errors) {
53
+ console.error(` ${e.instancePath || '/'} ${e.message}`);
54
+ }
55
+ } else {
56
+ console.log(`✅ ${rule.source}`);
57
+ }
41
58
  }
42
- } else {
43
- console.log(`✅ ${rule.source}`);
44
- }
45
- }
46
59
 
47
- if (failures > 0) {
48
- console.error(`\n💥 ${failures}/${rules.length} rule(s) failed validation.`);
49
- process.exit(1);
50
- }
60
+ if (failures > 0) {
61
+ console.error(`\n💥 ${failures}/${rules.length} rule(s) failed validation.`);
62
+ process.exit(1);
63
+ }
51
64
 
52
- console.log(`\n🎉 ${rules.length} rule(s) validated successfully.`);
65
+ console.log(`\n🎉 ${rules.length} rule(s) validated successfully.`);
66
+ },
67
+ });