@tyyyho/treg 0.1.2 → 0.1.5
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/README.md +39 -40
- package/dist/init-project/cli.js +163 -0
- package/dist/init-project/frameworks/index.js +39 -0
- package/dist/init-project/frameworks/next/index.js +9 -0
- package/dist/init-project/frameworks/node/index.js +8 -0
- package/dist/init-project/frameworks/nuxt/index.js +9 -0
- package/dist/init-project/frameworks/react/index.js +27 -0
- package/dist/init-project/frameworks/react/v18/index.js +6 -0
- package/dist/init-project/frameworks/react/v19/index.js +6 -0
- package/dist/init-project/frameworks/svelte/index.js +9 -0
- package/dist/init-project/frameworks/vue/index.js +9 -0
- package/dist/init-project/index.js +65 -0
- package/dist/init-project/mrm-core.js +3 -0
- package/dist/init-project/mrm-rules/ai-skills.js +188 -0
- package/dist/init-project/mrm-rules/format.js +44 -0
- package/dist/init-project/mrm-rules/husky.js +58 -0
- package/dist/init-project/mrm-rules/index.js +33 -0
- package/dist/init-project/mrm-rules/lint.js +16 -0
- package/dist/init-project/mrm-rules/shared.js +91 -0
- package/dist/init-project/mrm-rules/test-jest.js +48 -0
- package/dist/init-project/mrm-rules/test-vitest.js +46 -0
- package/dist/init-project/mrm-rules/typescript.js +40 -0
- package/dist/init-project/package-manager.js +57 -0
- package/dist/init-project/utils.js +9 -0
- package/dist/init-project.js +6 -0
- package/dist/package.json +3 -0
- package/package.json +10 -5
- package/scripts/init-project/cli.mjs +0 -173
- package/scripts/init-project/cli.test.mjs +0 -116
- package/scripts/init-project/frameworks/index.mjs +0 -48
- package/scripts/init-project/frameworks/next/index.mjs +0 -10
- package/scripts/init-project/frameworks/node/index.mjs +0 -8
- package/scripts/init-project/frameworks/nuxt/index.mjs +0 -10
- package/scripts/init-project/frameworks/react/index.mjs +0 -35
- package/scripts/init-project/frameworks/react/v18/index.mjs +0 -6
- package/scripts/init-project/frameworks/react/v19/index.mjs +0 -6
- package/scripts/init-project/frameworks/svelte/index.mjs +0 -10
- package/scripts/init-project/frameworks/vue/index.mjs +0 -10
- package/scripts/init-project/frameworks.test.mjs +0 -63
- package/scripts/init-project/index.mjs +0 -89
- package/scripts/init-project/mrm-core.mjs +0 -5
- package/scripts/init-project/mrm-rules/ai-skills.mjs +0 -220
- package/scripts/init-project/mrm-rules/ai-skills.test.mjs +0 -91
- package/scripts/init-project/mrm-rules/format.mjs +0 -55
- package/scripts/init-project/mrm-rules/husky.mjs +0 -78
- package/scripts/init-project/mrm-rules/index.mjs +0 -35
- package/scripts/init-project/mrm-rules/lint.mjs +0 -18
- package/scripts/init-project/mrm-rules/shared.mjs +0 -61
- package/scripts/init-project/mrm-rules/test-jest.mjs +0 -75
- package/scripts/init-project/mrm-rules/test-vitest.mjs +0 -64
- package/scripts/init-project/mrm-rules/typescript.mjs +0 -44
- package/scripts/init-project/package-manager.mjs +0 -68
- package/scripts/init-project/package-manager.test.mjs +0 -21
- package/scripts/init-project/utils.mjs +0 -12
- package/scripts/init-project/utils.test.mjs +0 -22
- package/scripts/init-project.mjs +0 -7
package/README.md
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
# @tyyyho/treg
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`treg` is a CLI for quickly setting up project tooling conventions in an existing repository.
|
|
4
|
+
It applies infra setup such as lint, format, TypeScript, test, husky, and AI skill guidance.
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Treg helps both human developers and AI agents work within the same set of expectations, reducing configuration drift and long-term maintenance overhead.
|
|
8
|
-
|
|
9
|
-
## Usage
|
|
6
|
+
## Quick Start
|
|
10
7
|
|
|
11
8
|
```bash
|
|
12
9
|
pnpm dlx @tyyyho/treg init <project-dir> --framework react
|
|
@@ -14,7 +11,34 @@ pnpm dlx @tyyyho/treg init <project-dir> --framework react
|
|
|
14
11
|
npx @tyyyho/treg init <project-dir> --framework react
|
|
15
12
|
```
|
|
16
13
|
|
|
17
|
-
|
|
14
|
+
`init` requires `--framework`.
|
|
15
|
+
|
|
16
|
+
## Commands
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npx @tyyyho/treg <command> [projectDir] [options]
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
- `init`: Initialize infra rules (requires `--framework`)
|
|
23
|
+
- `add`: Add selected infra features to an existing project
|
|
24
|
+
- `list`: List supported frameworks, features, and test runners
|
|
25
|
+
|
|
26
|
+
## Options
|
|
27
|
+
|
|
28
|
+
- `--framework <node|react|next|vue|svelte|nuxt>`: Target framework
|
|
29
|
+
- `--framework-version <major>`: Optional major version hint (react only)
|
|
30
|
+
- `--features <lint,format,typescript,test,husky>`: Features to install (defaults to all)
|
|
31
|
+
- `--test-runner <jest|vitest>`: Test runner when test feature is enabled
|
|
32
|
+
- `--pm <pnpm|npm|yarn|auto>`: Package manager (auto-detected by default)
|
|
33
|
+
- `--force`: Overwrite existing config files
|
|
34
|
+
- `--dry-run`: Print full plan without writing files
|
|
35
|
+
- `--skip-husky-install`: Skip husky install command
|
|
36
|
+
- `--skills`: Update existing `AGENTS.md`/`CLAUDE.md` with skill guidance
|
|
37
|
+
- `--help`: Show help
|
|
38
|
+
|
|
39
|
+
## Features
|
|
40
|
+
|
|
41
|
+
Default feature set:
|
|
18
42
|
|
|
19
43
|
- `husky`
|
|
20
44
|
- `typescript`
|
|
@@ -22,29 +46,6 @@ By default, all features are applied:
|
|
|
22
46
|
- `format`
|
|
23
47
|
- `test`
|
|
24
48
|
|
|
25
|
-
## Options
|
|
26
|
-
|
|
27
|
-
```bash
|
|
28
|
-
npx @tyyyho/treg <command> [projectDir] [options]
|
|
29
|
-
|
|
30
|
-
init Initialize infra rules (requires --framework)
|
|
31
|
-
add Add selected infra features
|
|
32
|
-
list List supported targets
|
|
33
|
-
|
|
34
|
-
--framework <node|react|next|vue|svelte|nuxt>
|
|
35
|
-
Target framework
|
|
36
|
-
--framework-version <major> Optional major version hint (currently react only)
|
|
37
|
-
--pm <pnpm|npm|yarn|auto> Package manager (auto-detected by default)
|
|
38
|
-
--features <lint,format,typescript,test,husky>
|
|
39
|
-
Features to install (all selected by default)
|
|
40
|
-
--test-runner <jest|vitest> Test runner when test feature is enabled
|
|
41
|
-
--force Overwrite existing config files
|
|
42
|
-
--dry-run Show planned changes without writing files
|
|
43
|
-
--skip-husky-install Do not run husky install
|
|
44
|
-
--skills Update AGENTS.md/CLAUDE.md with feature skill guidance
|
|
45
|
-
--help Show help
|
|
46
|
-
```
|
|
47
|
-
|
|
48
49
|
## Examples
|
|
49
50
|
|
|
50
51
|
Initialize a React project:
|
|
@@ -53,19 +54,19 @@ Initialize a React project:
|
|
|
53
54
|
npx @tyyyho/treg init . --framework react
|
|
54
55
|
```
|
|
55
56
|
|
|
56
|
-
Add only lint
|
|
57
|
+
Add only lint + format:
|
|
57
58
|
|
|
58
59
|
```bash
|
|
59
60
|
npx @tyyyho/treg add . --features lint,format
|
|
60
61
|
```
|
|
61
62
|
|
|
62
|
-
Use Vitest:
|
|
63
|
+
Use Vitest for test feature:
|
|
63
64
|
|
|
64
65
|
```bash
|
|
65
66
|
npx @tyyyho/treg init . --framework node --features test --test-runner vitest
|
|
66
67
|
```
|
|
67
68
|
|
|
68
|
-
|
|
69
|
+
Use react major version variant:
|
|
69
70
|
|
|
70
71
|
```bash
|
|
71
72
|
npx @tyyyho/treg init . --framework react --framework-version 18
|
|
@@ -77,16 +78,14 @@ Preview changes only:
|
|
|
77
78
|
npx @tyyyho/treg init . --framework react --dry-run
|
|
78
79
|
```
|
|
79
80
|
|
|
80
|
-
|
|
81
|
+
Update AI skill guidance:
|
|
81
82
|
|
|
82
83
|
```bash
|
|
83
84
|
npx @tyyyho/treg add . --features lint,format,husky --skills
|
|
84
85
|
```
|
|
85
86
|
|
|
86
|
-
##
|
|
87
|
+
## Notes
|
|
87
88
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
npm publish --access public
|
|
92
|
-
```
|
|
89
|
+
- `init` requires `--framework`.
|
|
90
|
+
- `add` lets you install only the features you specify.
|
|
91
|
+
- `--dry-run` prints the full plan and does not write files.
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
const ALLOWED_COMMANDS = ["init", "add", "list"];
|
|
2
|
+
const ALLOWED_PACKAGE_MANAGERS = ["pnpm", "npm", "yarn", "auto"];
|
|
3
|
+
const ALLOWED_FRAMEWORKS = ["node", "react", "next", "vue", "svelte", "nuxt"];
|
|
4
|
+
const ALLOWED_FEATURES = ["lint", "format", "typescript", "test", "husky"];
|
|
5
|
+
const ALLOWED_TEST_RUNNERS = ["jest", "vitest"];
|
|
6
|
+
export const USAGE = `Usage: treg <command> [projectDir] [options]
|
|
7
|
+
|
|
8
|
+
Commands:
|
|
9
|
+
init Initialize infra rules in a project (requires --framework)
|
|
10
|
+
add Add selected infra features to an existing project
|
|
11
|
+
list List supported frameworks, features, and test runners
|
|
12
|
+
|
|
13
|
+
Options:
|
|
14
|
+
--framework <node|react|next|vue|svelte|nuxt>
|
|
15
|
+
Target framework
|
|
16
|
+
--framework-version <major> Optional framework major version hint
|
|
17
|
+
--features <lint,format,typescript,test,husky>
|
|
18
|
+
Features to install (all selected by default)
|
|
19
|
+
--test-runner <jest|vitest> Test runner when test feature is enabled
|
|
20
|
+
--pm <pnpm|npm|yarn|auto> Package manager (auto-detected if omitted)
|
|
21
|
+
--force Overwrite existing config files
|
|
22
|
+
--dry-run Print planned changes without writing files
|
|
23
|
+
--skip-husky-install Do not run husky install
|
|
24
|
+
--skills Update AGENTS.md/CLAUDE.md with feature skill guidance
|
|
25
|
+
-h, --help Show help
|
|
26
|
+
`;
|
|
27
|
+
export function parseArgs(argv) {
|
|
28
|
+
const options = {
|
|
29
|
+
command: "init",
|
|
30
|
+
projectDir: null,
|
|
31
|
+
framework: null,
|
|
32
|
+
frameworkVersion: null,
|
|
33
|
+
features: new Array(),
|
|
34
|
+
testRunner: "jest",
|
|
35
|
+
pm: null,
|
|
36
|
+
force: false,
|
|
37
|
+
dryRun: false,
|
|
38
|
+
skipHuskyInstall: false,
|
|
39
|
+
skills: false,
|
|
40
|
+
help: false,
|
|
41
|
+
};
|
|
42
|
+
let cursor = 0;
|
|
43
|
+
const firstArg = argv[0];
|
|
44
|
+
if (firstArg && ALLOWED_COMMANDS.includes(firstArg)) {
|
|
45
|
+
options.command = firstArg;
|
|
46
|
+
cursor = 1;
|
|
47
|
+
}
|
|
48
|
+
for (let i = cursor; i < argv.length; i += 1) {
|
|
49
|
+
const arg = argv[i];
|
|
50
|
+
if (arg === "-h" || arg === "--help") {
|
|
51
|
+
options.help = true;
|
|
52
|
+
}
|
|
53
|
+
else if (arg === "--framework") {
|
|
54
|
+
options.framework = argv[i + 1];
|
|
55
|
+
i += 1;
|
|
56
|
+
}
|
|
57
|
+
else if (arg.startsWith("--framework=")) {
|
|
58
|
+
options.framework = arg.split("=")[1];
|
|
59
|
+
}
|
|
60
|
+
else if (arg === "--framework-version") {
|
|
61
|
+
options.frameworkVersion = argv[i + 1];
|
|
62
|
+
i += 1;
|
|
63
|
+
}
|
|
64
|
+
else if (arg.startsWith("--framework-version=")) {
|
|
65
|
+
options.frameworkVersion = arg.split("=")[1];
|
|
66
|
+
}
|
|
67
|
+
else if (arg === "--features") {
|
|
68
|
+
options.features.push(...parseCsvValue(argv[i + 1], "--features"));
|
|
69
|
+
i += 1;
|
|
70
|
+
}
|
|
71
|
+
else if (arg.startsWith("--features=")) {
|
|
72
|
+
options.features.push(...parseCsvValue(arg.split("=")[1], "--features"));
|
|
73
|
+
}
|
|
74
|
+
else if (arg === "--test-runner") {
|
|
75
|
+
options.testRunner = argv[i + 1];
|
|
76
|
+
i += 1;
|
|
77
|
+
}
|
|
78
|
+
else if (arg.startsWith("--test-runner=")) {
|
|
79
|
+
options.testRunner = arg.split("=")[1];
|
|
80
|
+
}
|
|
81
|
+
else if (arg === "--pm") {
|
|
82
|
+
options.pm = argv[i + 1];
|
|
83
|
+
i += 1;
|
|
84
|
+
}
|
|
85
|
+
else if (arg.startsWith("--pm=")) {
|
|
86
|
+
options.pm = arg.split("=")[1];
|
|
87
|
+
}
|
|
88
|
+
else if (arg === "--force") {
|
|
89
|
+
options.force = true;
|
|
90
|
+
}
|
|
91
|
+
else if (arg === "--dry-run") {
|
|
92
|
+
options.dryRun = true;
|
|
93
|
+
}
|
|
94
|
+
else if (arg === "--skip-husky-install") {
|
|
95
|
+
options.skipHuskyInstall = true;
|
|
96
|
+
}
|
|
97
|
+
else if (arg === "--skills") {
|
|
98
|
+
options.skills = true;
|
|
99
|
+
}
|
|
100
|
+
else if (!arg.startsWith("-") && !options.projectDir) {
|
|
101
|
+
options.projectDir = arg;
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
validateParsedOptions(options);
|
|
108
|
+
return options;
|
|
109
|
+
}
|
|
110
|
+
function parseCsvValue(rawValue, flagName) {
|
|
111
|
+
if (!rawValue) {
|
|
112
|
+
throw new Error(`Missing value for ${flagName}`);
|
|
113
|
+
}
|
|
114
|
+
return rawValue
|
|
115
|
+
.split(",")
|
|
116
|
+
.map(item => item.trim())
|
|
117
|
+
.filter(Boolean);
|
|
118
|
+
}
|
|
119
|
+
function validateParsedOptions(options) {
|
|
120
|
+
if (!ALLOWED_COMMANDS.includes(options.command)) {
|
|
121
|
+
throw new Error(`Unsupported command: ${options.command}`);
|
|
122
|
+
}
|
|
123
|
+
if (options.pm && !ALLOWED_PACKAGE_MANAGERS.includes(options.pm)) {
|
|
124
|
+
throw new Error(`Unsupported package manager: ${options.pm}`);
|
|
125
|
+
}
|
|
126
|
+
if (options.framework && !ALLOWED_FRAMEWORKS.includes(options.framework)) {
|
|
127
|
+
throw new Error(`Unsupported framework: ${options.framework}`);
|
|
128
|
+
}
|
|
129
|
+
if (options.frameworkVersion && !/^\d+$/.test(options.frameworkVersion)) {
|
|
130
|
+
throw new Error("Invalid --framework-version: major version must be numeric");
|
|
131
|
+
}
|
|
132
|
+
if (options.frameworkVersion &&
|
|
133
|
+
options.framework &&
|
|
134
|
+
options.framework !== "react") {
|
|
135
|
+
throw new Error(`Unsupported --framework-version for framework: ${options.framework}`);
|
|
136
|
+
}
|
|
137
|
+
if (!ALLOWED_TEST_RUNNERS.includes(options.testRunner)) {
|
|
138
|
+
throw new Error(`Unsupported test runner: ${options.testRunner}`);
|
|
139
|
+
}
|
|
140
|
+
for (const feature of options.features) {
|
|
141
|
+
if (!ALLOWED_FEATURES.includes(feature)) {
|
|
142
|
+
throw new Error(`Unsupported feature in --features: ${feature}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (options.command === "init" && !options.help && !options.framework) {
|
|
146
|
+
throw new Error("Missing required option: --framework");
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
export function resolveFeatures(options) {
|
|
150
|
+
const selected = new Set(options.features.length > 0 ? options.features : ALLOWED_FEATURES);
|
|
151
|
+
return {
|
|
152
|
+
lint: selected.has("lint"),
|
|
153
|
+
format: selected.has("format"),
|
|
154
|
+
typescript: selected.has("typescript"),
|
|
155
|
+
test: selected.has("test"),
|
|
156
|
+
husky: selected.has("husky"),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
export function printSupportedTargets() {
|
|
160
|
+
console.log("Frameworks: node, react, next, vue, svelte, nuxt");
|
|
161
|
+
console.log("Features: lint, format, typescript, test, husky");
|
|
162
|
+
console.log("Test runners: jest, vitest");
|
|
163
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { nextFramework } from "./next/index.js";
|
|
2
|
+
import { nodeFramework } from "./node/index.js";
|
|
3
|
+
import { nuxtFramework } from "./nuxt/index.js";
|
|
4
|
+
import { reactFramework, resolveReactFramework } from "./react/index.js";
|
|
5
|
+
import { svelteFramework } from "./svelte/index.js";
|
|
6
|
+
import { vueFramework } from "./vue/index.js";
|
|
7
|
+
const FRAMEWORK_REGISTRY = {
|
|
8
|
+
next: nextFramework,
|
|
9
|
+
node: nodeFramework,
|
|
10
|
+
nuxt: nuxtFramework,
|
|
11
|
+
react: reactFramework,
|
|
12
|
+
svelte: svelteFramework,
|
|
13
|
+
vue: vueFramework,
|
|
14
|
+
};
|
|
15
|
+
const FRAMEWORK_DETECT_ORDER = [
|
|
16
|
+
nuxtFramework,
|
|
17
|
+
nextFramework,
|
|
18
|
+
reactFramework,
|
|
19
|
+
vueFramework,
|
|
20
|
+
svelteFramework,
|
|
21
|
+
nodeFramework,
|
|
22
|
+
];
|
|
23
|
+
export function resolveFramework(frameworkArg, frameworkVersion, packageJson) {
|
|
24
|
+
if (frameworkArg === "react") {
|
|
25
|
+
return resolveReactFramework(packageJson, frameworkVersion);
|
|
26
|
+
}
|
|
27
|
+
if (frameworkArg) {
|
|
28
|
+
return FRAMEWORK_REGISTRY[frameworkArg];
|
|
29
|
+
}
|
|
30
|
+
const detected = detectFramework(packageJson);
|
|
31
|
+
if (detected.id === "react") {
|
|
32
|
+
return resolveReactFramework(packageJson, frameworkVersion);
|
|
33
|
+
}
|
|
34
|
+
return detected;
|
|
35
|
+
}
|
|
36
|
+
export function detectFramework(packageJson) {
|
|
37
|
+
const matched = FRAMEWORK_DETECT_ORDER.find(framework => framework.matches(packageJson));
|
|
38
|
+
return matched ?? nodeFramework;
|
|
39
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { hasPackage } from "../../utils.js";
|
|
2
|
+
export const nextFramework = {
|
|
3
|
+
id: "next",
|
|
4
|
+
testEnvironment: "jsdom",
|
|
5
|
+
tsRequiredExcludes: [".next", "dist", "coverage", "jest.config.js", "public"],
|
|
6
|
+
matches(packageJson) {
|
|
7
|
+
return hasPackage(packageJson, "next");
|
|
8
|
+
},
|
|
9
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { hasPackage } from "../../utils.js";
|
|
2
|
+
export const nuxtFramework = {
|
|
3
|
+
id: "nuxt",
|
|
4
|
+
testEnvironment: "jsdom",
|
|
5
|
+
tsRequiredExcludes: [".nuxt", ".output", "dist", "coverage", "public"],
|
|
6
|
+
matches(packageJson) {
|
|
7
|
+
return hasPackage(packageJson, "nuxt");
|
|
8
|
+
},
|
|
9
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { hasPackage } from "../../utils.js";
|
|
2
|
+
import { reactV18Framework } from "./v18/index.js";
|
|
3
|
+
import { reactV19Framework } from "./v19/index.js";
|
|
4
|
+
export const reactFramework = {
|
|
5
|
+
id: "react",
|
|
6
|
+
variant: "v19",
|
|
7
|
+
testEnvironment: "jsdom",
|
|
8
|
+
tsRequiredExcludes: ["dist", "coverage", "jest.config.js", "public"],
|
|
9
|
+
matches(packageJson) {
|
|
10
|
+
return (hasPackage(packageJson, "react") || hasPackage(packageJson, "react-dom"));
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
const REACT_VARIANTS = {
|
|
14
|
+
18: reactV18Framework,
|
|
15
|
+
19: reactV19Framework,
|
|
16
|
+
};
|
|
17
|
+
export function resolveReactFramework(packageJson, frameworkVersion) {
|
|
18
|
+
if (frameworkVersion && REACT_VARIANTS[frameworkVersion]) {
|
|
19
|
+
return REACT_VARIANTS[frameworkVersion];
|
|
20
|
+
}
|
|
21
|
+
const detected = packageJson?.dependencies?.react ?? packageJson?.devDependencies?.react;
|
|
22
|
+
const major = typeof detected === "string" ? detected.match(/\d+/)?.[0] : null;
|
|
23
|
+
if (major && REACT_VARIANTS[major]) {
|
|
24
|
+
return REACT_VARIANTS[major];
|
|
25
|
+
}
|
|
26
|
+
return reactFramework;
|
|
27
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { promises as fs } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { parseArgs, printSupportedTargets, resolveFeatures, USAGE, } from "./cli.js";
|
|
5
|
+
import { resolveFramework } from "./frameworks/index.js";
|
|
6
|
+
import { runFeatureRules } from "./mrm-rules/index.js";
|
|
7
|
+
import { detectPackageManager, runScript } from "./package-manager.js";
|
|
8
|
+
import { formatStep } from "./utils.js";
|
|
9
|
+
const TOTAL_STEPS = 3;
|
|
10
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
11
|
+
let options;
|
|
12
|
+
try {
|
|
13
|
+
options = parseArgs(argv);
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
console.error(error.message ?? error);
|
|
17
|
+
console.log(USAGE);
|
|
18
|
+
process.exitCode = 1;
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (options.help) {
|
|
22
|
+
console.log(USAGE);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (options.command === "list") {
|
|
26
|
+
printSupportedTargets();
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const projectDir = path.resolve(options.projectDir ?? process.cwd());
|
|
30
|
+
const packageJsonPath = path.join(projectDir, "package.json");
|
|
31
|
+
if (!existsSync(packageJsonPath)) {
|
|
32
|
+
console.error(`package.json not found in ${projectDir}`);
|
|
33
|
+
process.exitCode = 1;
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf8"));
|
|
37
|
+
const pm = !options.pm || options.pm === "auto"
|
|
38
|
+
? detectPackageManager(projectDir)
|
|
39
|
+
: options.pm;
|
|
40
|
+
const framework = resolveFramework(options.framework, options.frameworkVersion, packageJson);
|
|
41
|
+
if (options.frameworkVersion && framework.id !== "react") {
|
|
42
|
+
console.error(`Unsupported --framework-version for framework: ${framework.id}`);
|
|
43
|
+
process.exitCode = 1;
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const enabledFeatures = resolveFeatures(options);
|
|
47
|
+
const context = {
|
|
48
|
+
...options,
|
|
49
|
+
projectDir,
|
|
50
|
+
pm,
|
|
51
|
+
framework,
|
|
52
|
+
enabledFeatures,
|
|
53
|
+
};
|
|
54
|
+
console.log(formatStep(1, TOTAL_STEPS, "Resolve plan", options.dryRun));
|
|
55
|
+
console.log(`${options.dryRun ? "[dry-run] " : ""}Framework=${framework.id}${framework.variant ? `/${framework.variant}` : ""}, features=${Object.entries(enabledFeatures)
|
|
56
|
+
.filter(([, enabled]) => enabled)
|
|
57
|
+
.map(([name]) => name)
|
|
58
|
+
.join(", ")}, testRunner=${options.testRunner}`);
|
|
59
|
+
console.log(formatStep(2, TOTAL_STEPS, "Run mrm rules", options.dryRun));
|
|
60
|
+
await runFeatureRules(context);
|
|
61
|
+
console.log(formatStep(3, TOTAL_STEPS, "Finalize", options.dryRun));
|
|
62
|
+
if (enabledFeatures.format) {
|
|
63
|
+
runScript(pm, "format", projectDir, options.dryRun);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { promises as fs } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
const START_MARKER = "<!-- treg:skills:start -->";
|
|
5
|
+
const END_MARKER = "<!-- treg:skills:end -->";
|
|
6
|
+
const SKILLS_BASE_DIR = "skills";
|
|
7
|
+
const FEATURE_SKILLS = {
|
|
8
|
+
format: {
|
|
9
|
+
name: "treg/format",
|
|
10
|
+
description: "Run and verify formatting rules.",
|
|
11
|
+
when: "在提交前或大範圍改動後,統一格式化程式碼。",
|
|
12
|
+
checklist: ["執行 format", "執行 format:check", "確認未變動非目標檔案"],
|
|
13
|
+
},
|
|
14
|
+
husky: {
|
|
15
|
+
name: "treg/husky",
|
|
16
|
+
description: "Verify and maintain git hook automation.",
|
|
17
|
+
when: "需要保證 pre-commit / pre-push 自動檢查時。",
|
|
18
|
+
checklist: [
|
|
19
|
+
"確認 hooks 可執行",
|
|
20
|
+
"確認含 format:check 與 lint:check",
|
|
21
|
+
"若啟用型別/測試,也要納入 hooks",
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
lint: {
|
|
25
|
+
name: "treg/lint",
|
|
26
|
+
description: "Run and validate lint rules.",
|
|
27
|
+
when: "新增規則或調整工具鏈後,驗證 lint 一致性。",
|
|
28
|
+
checklist: ["執行 lint", "執行 lint:check", "修正 max-warnings 問題"],
|
|
29
|
+
},
|
|
30
|
+
test: {
|
|
31
|
+
name: "treg/test",
|
|
32
|
+
description: "Validate test runner setup and execution.",
|
|
33
|
+
when: "新增測試規則或調整測試設定時。",
|
|
34
|
+
checklist: [
|
|
35
|
+
"確認 test runner 與專案一致",
|
|
36
|
+
"執行 test",
|
|
37
|
+
"視需要執行 test:coverage",
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
typescript: {
|
|
41
|
+
name: "treg/typescript",
|
|
42
|
+
description: "Validate TypeScript strictness and config.",
|
|
43
|
+
when: "調整 tsconfig 或型別嚴格度規則時。",
|
|
44
|
+
checklist: [
|
|
45
|
+
"執行 type-check",
|
|
46
|
+
"確認 strict 相關選項仍生效",
|
|
47
|
+
"檢查 exclude 不含產品邏輯路徑",
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
function resolveSkillsDoc(projectDir) {
|
|
52
|
+
const agentsPath = path.join(projectDir, "AGENTS.md");
|
|
53
|
+
if (existsSync(agentsPath)) {
|
|
54
|
+
return agentsPath;
|
|
55
|
+
}
|
|
56
|
+
const claudePath = path.join(projectDir, "CLAUDE.md");
|
|
57
|
+
if (existsSync(claudePath)) {
|
|
58
|
+
return claudePath;
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
function getEnabledFeatures(enabledFeatures) {
|
|
63
|
+
return Object.entries(enabledFeatures)
|
|
64
|
+
.filter(([, value]) => value)
|
|
65
|
+
.map(([name]) => name)
|
|
66
|
+
.sort((a, b) => a.localeCompare(b));
|
|
67
|
+
}
|
|
68
|
+
function getSkillRelativePath(feature) {
|
|
69
|
+
return `${SKILLS_BASE_DIR}/${feature}/SKILL.md`;
|
|
70
|
+
}
|
|
71
|
+
function buildSkillFile(feature, skill, testRunner) {
|
|
72
|
+
const extra = feature === "test"
|
|
73
|
+
? `\n## Current Test Runner\n\n- \`${testRunner}\`\n`
|
|
74
|
+
: "";
|
|
75
|
+
return `---
|
|
76
|
+
name: ${skill.name}
|
|
77
|
+
description: ${skill.description}
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
# ${skill.name}
|
|
81
|
+
|
|
82
|
+
## When To Use
|
|
83
|
+
|
|
84
|
+
${skill.when}
|
|
85
|
+
|
|
86
|
+
## Validation Checklist
|
|
87
|
+
|
|
88
|
+
- ${skill.checklist.join("\n- ")}
|
|
89
|
+
${extra}`;
|
|
90
|
+
}
|
|
91
|
+
async function ensureSkillFiles(projectDir, enabled, testRunner, dryRun) {
|
|
92
|
+
for (const feature of enabled) {
|
|
93
|
+
const skill = FEATURE_SKILLS[feature];
|
|
94
|
+
if (!skill)
|
|
95
|
+
continue;
|
|
96
|
+
const relativePath = getSkillRelativePath(feature);
|
|
97
|
+
const fullPath = path.join(projectDir, relativePath);
|
|
98
|
+
const content = buildSkillFile(feature, skill, testRunner);
|
|
99
|
+
if (dryRun) {
|
|
100
|
+
console.log(`[dry-run] Would upsert ${relativePath}`);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
|
104
|
+
const current = existsSync(fullPath)
|
|
105
|
+
? await fs.readFile(fullPath, "utf8")
|
|
106
|
+
: null;
|
|
107
|
+
if (current === content) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
await fs.writeFile(fullPath, content, "utf8");
|
|
111
|
+
console.log(`${current === null ? "Created" : "Updated"} ${relativePath}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function buildSkillSection(context) {
|
|
115
|
+
const { enabledFeatures, testRunner } = context;
|
|
116
|
+
const enabled = getEnabledFeatures(enabledFeatures);
|
|
117
|
+
const lines = [
|
|
118
|
+
START_MARKER,
|
|
119
|
+
"## treg AI Skills",
|
|
120
|
+
"",
|
|
121
|
+
"以下 skill 可在 agent 任務完成前引用,確保基礎建設規則正確且可重跑:",
|
|
122
|
+
"",
|
|
123
|
+
];
|
|
124
|
+
for (const feature of enabled) {
|
|
125
|
+
const skill = FEATURE_SKILLS[feature];
|
|
126
|
+
if (!skill)
|
|
127
|
+
continue;
|
|
128
|
+
const skillRelativePath = getSkillRelativePath(feature);
|
|
129
|
+
lines.push(`### ${feature}`);
|
|
130
|
+
lines.push(`- Skill: [${skill.name}](${skillRelativePath})`);
|
|
131
|
+
lines.push(`- 使用時機: ${skill.when}`);
|
|
132
|
+
if (feature === "test") {
|
|
133
|
+
lines.push(`- 目前測試工具: \`${testRunner}\``);
|
|
134
|
+
}
|
|
135
|
+
lines.push(`- 驗證清單: ${skill.checklist.join("、")}`);
|
|
136
|
+
lines.push("");
|
|
137
|
+
}
|
|
138
|
+
lines.push(END_MARKER);
|
|
139
|
+
lines.push("");
|
|
140
|
+
return lines.join("\n");
|
|
141
|
+
}
|
|
142
|
+
function upsertSkillSection(content, nextSection) {
|
|
143
|
+
const start = content.indexOf(START_MARKER);
|
|
144
|
+
const end = content.indexOf(END_MARKER);
|
|
145
|
+
if (start !== -1 && end !== -1 && end > start) {
|
|
146
|
+
const suffixStart = end + END_MARKER.length;
|
|
147
|
+
const before = content.slice(0, start).trimEnd();
|
|
148
|
+
const after = content.slice(suffixStart).trimStart();
|
|
149
|
+
const rebuilt = `${before}\n\n${nextSection.trim()}\n`;
|
|
150
|
+
return after ? `${rebuilt}\n${after}\n` : `${rebuilt}`;
|
|
151
|
+
}
|
|
152
|
+
if (!content.trim()) {
|
|
153
|
+
return `${nextSection.trim()}\n`;
|
|
154
|
+
}
|
|
155
|
+
return `${content.trimEnd()}\n\n${nextSection.trim()}\n`;
|
|
156
|
+
}
|
|
157
|
+
export async function runAiSkillsRule(context) {
|
|
158
|
+
const { projectDir, dryRun } = context;
|
|
159
|
+
const targetFile = resolveSkillsDoc(projectDir);
|
|
160
|
+
if (!targetFile) {
|
|
161
|
+
console.log("Skip ai-skills (AGENTS.md/CLAUDE.md not found)");
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const enabled = getEnabledFeatures(context.enabledFeatures);
|
|
165
|
+
await ensureSkillFiles(projectDir, enabled, context.testRunner, dryRun);
|
|
166
|
+
const section = buildSkillSection(context);
|
|
167
|
+
const current = await fs.readFile(targetFile, "utf8");
|
|
168
|
+
const updated = upsertSkillSection(current, section);
|
|
169
|
+
if (dryRun) {
|
|
170
|
+
console.log(`[dry-run] Would update ${path.basename(targetFile)} with AI skill guidance`);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (updated !== current) {
|
|
174
|
+
await fs.writeFile(targetFile, updated, "utf8");
|
|
175
|
+
console.log(`Updated ${path.basename(targetFile)} with AI skill guidance`);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
console.log(`${path.basename(targetFile)} already contains latest AI skill guidance`);
|
|
179
|
+
}
|
|
180
|
+
export const __testables__ = {
|
|
181
|
+
buildSkillSection,
|
|
182
|
+
buildSkillFile,
|
|
183
|
+
ensureSkillFiles,
|
|
184
|
+
getEnabledFeatures,
|
|
185
|
+
getSkillRelativePath,
|
|
186
|
+
resolveSkillsDoc,
|
|
187
|
+
upsertSkillSection,
|
|
188
|
+
};
|