agency-rules 0.6.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 +7 -0
- package/bin/agency-rules.mjs +22 -134
- package/package.json +3 -2
- package/scripts/convert.mjs +107 -98
- package/scripts/install.mjs +156 -139
- package/scripts/list.mjs +47 -42
- package/scripts/validate.mjs +52 -37
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,13 @@ 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
|
+
|
|
8
15
|
## [0.6.0](https://github.com/deuriib/agency-rules/compare/v0.5.0...v0.6.0) (2026-10-31)
|
|
9
16
|
|
|
10
17
|
|
package/bin/agency-rules.mjs
CHANGED
|
@@ -2,137 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
import { readFile } from 'node:fs/promises';
|
|
4
4
|
import { resolve } from 'node:path';
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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.6.
|
|
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
|
-
"
|
|
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"
|
package/scripts/convert.mjs
CHANGED
|
@@ -1,114 +1,123 @@
|
|
|
1
1
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { dirname, join, resolve } from 'node:path';
|
|
3
|
-
import {
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
export default defineCommand({
|
|
8
|
+
meta: {
|
|
9
|
+
name: 'convert',
|
|
10
|
+
description: "Compile rules/ into each harness's native format",
|
|
11
11
|
},
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
for (const
|
|
105
|
-
|
|
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
|
-
|
|
112
|
-
|
|
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
|
+
});
|
package/scripts/install.mjs
CHANGED
|
@@ -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 {
|
|
6
|
+
import { defineCommand } from 'citty';
|
|
7
7
|
import { detectHarnesses, expandHome, pathExists } from './lib/detect.mjs';
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
|
|
75
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
110
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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 {
|
|
3
|
+
import { defineCommand } from 'citty';
|
|
4
4
|
import { readRules } from './lib/frontmatter.mjs';
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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');
|
package/scripts/validate.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
16
|
-
|
|
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
|
-
|
|
32
|
+
for (const rule of rules) {
|
|
33
|
+
const errors = [];
|
|
24
34
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
43
|
+
if (!validate(rule.data)) {
|
|
44
|
+
for (const e of validate.errors ?? []) {
|
|
45
|
+
errors.push(e);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
35
48
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
49
|
-
|
|
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
|
+
});
|