@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.
Files changed (56) hide show
  1. package/README.md +39 -40
  2. package/dist/init-project/cli.js +163 -0
  3. package/dist/init-project/frameworks/index.js +39 -0
  4. package/dist/init-project/frameworks/next/index.js +9 -0
  5. package/dist/init-project/frameworks/node/index.js +8 -0
  6. package/dist/init-project/frameworks/nuxt/index.js +9 -0
  7. package/dist/init-project/frameworks/react/index.js +27 -0
  8. package/dist/init-project/frameworks/react/v18/index.js +6 -0
  9. package/dist/init-project/frameworks/react/v19/index.js +6 -0
  10. package/dist/init-project/frameworks/svelte/index.js +9 -0
  11. package/dist/init-project/frameworks/vue/index.js +9 -0
  12. package/dist/init-project/index.js +65 -0
  13. package/dist/init-project/mrm-core.js +3 -0
  14. package/dist/init-project/mrm-rules/ai-skills.js +188 -0
  15. package/dist/init-project/mrm-rules/format.js +44 -0
  16. package/dist/init-project/mrm-rules/husky.js +58 -0
  17. package/dist/init-project/mrm-rules/index.js +33 -0
  18. package/dist/init-project/mrm-rules/lint.js +16 -0
  19. package/dist/init-project/mrm-rules/shared.js +91 -0
  20. package/dist/init-project/mrm-rules/test-jest.js +48 -0
  21. package/dist/init-project/mrm-rules/test-vitest.js +46 -0
  22. package/dist/init-project/mrm-rules/typescript.js +40 -0
  23. package/dist/init-project/package-manager.js +57 -0
  24. package/dist/init-project/utils.js +9 -0
  25. package/dist/init-project.js +6 -0
  26. package/dist/package.json +3 -0
  27. package/package.json +10 -5
  28. package/scripts/init-project/cli.mjs +0 -173
  29. package/scripts/init-project/cli.test.mjs +0 -116
  30. package/scripts/init-project/frameworks/index.mjs +0 -48
  31. package/scripts/init-project/frameworks/next/index.mjs +0 -10
  32. package/scripts/init-project/frameworks/node/index.mjs +0 -8
  33. package/scripts/init-project/frameworks/nuxt/index.mjs +0 -10
  34. package/scripts/init-project/frameworks/react/index.mjs +0 -35
  35. package/scripts/init-project/frameworks/react/v18/index.mjs +0 -6
  36. package/scripts/init-project/frameworks/react/v19/index.mjs +0 -6
  37. package/scripts/init-project/frameworks/svelte/index.mjs +0 -10
  38. package/scripts/init-project/frameworks/vue/index.mjs +0 -10
  39. package/scripts/init-project/frameworks.test.mjs +0 -63
  40. package/scripts/init-project/index.mjs +0 -89
  41. package/scripts/init-project/mrm-core.mjs +0 -5
  42. package/scripts/init-project/mrm-rules/ai-skills.mjs +0 -220
  43. package/scripts/init-project/mrm-rules/ai-skills.test.mjs +0 -91
  44. package/scripts/init-project/mrm-rules/format.mjs +0 -55
  45. package/scripts/init-project/mrm-rules/husky.mjs +0 -78
  46. package/scripts/init-project/mrm-rules/index.mjs +0 -35
  47. package/scripts/init-project/mrm-rules/lint.mjs +0 -18
  48. package/scripts/init-project/mrm-rules/shared.mjs +0 -61
  49. package/scripts/init-project/mrm-rules/test-jest.mjs +0 -75
  50. package/scripts/init-project/mrm-rules/test-vitest.mjs +0 -64
  51. package/scripts/init-project/mrm-rules/typescript.mjs +0 -44
  52. package/scripts/init-project/package-manager.mjs +0 -68
  53. package/scripts/init-project/package-manager.test.mjs +0 -21
  54. package/scripts/init-project/utils.mjs +0 -12
  55. package/scripts/init-project/utils.test.mjs +0 -22
  56. package/scripts/init-project.mjs +0 -7
package/README.md CHANGED
@@ -1,12 +1,9 @@
1
1
  # @tyyyho/treg
2
2
 
3
- Treg is a CLI tool for initializing development conventions in existing projects.
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
- It installs and configures tools and records clear usage guidelines as skills.
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
- By default, all features are applied:
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 and format:
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
- Set framework major version explicitly (for variant rules):
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
- Enable AI skill guidance update:
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
- ## Publish
87
+ ## Notes
87
88
 
88
- ```bash
89
- pnpm install
90
- pnpm run prepublishOnly
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,8 @@
1
+ export const nodeFramework = {
2
+ id: "node",
3
+ testEnvironment: "node",
4
+ tsRequiredExcludes: ["dist", "coverage"],
5
+ matches() {
6
+ return true;
7
+ },
8
+ };
@@ -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,6 @@
1
+ export const reactV18Framework = {
2
+ id: "react",
3
+ variant: "v18",
4
+ testEnvironment: "jsdom",
5
+ tsRequiredExcludes: ["dist", "coverage", "jest.config.js", "public"],
6
+ };
@@ -0,0 +1,6 @@
1
+ export const reactV19Framework = {
2
+ id: "react",
3
+ variant: "v19",
4
+ testEnvironment: "jsdom",
5
+ tsRequiredExcludes: ["dist", "coverage", "jest.config.js", "public"],
6
+ };
@@ -0,0 +1,9 @@
1
+ import { hasPackage } from "../../utils.js";
2
+ export const svelteFramework = {
3
+ id: "svelte",
4
+ testEnvironment: "jsdom",
5
+ tsRequiredExcludes: ["dist", "coverage", ".svelte-kit"],
6
+ matches(packageJson) {
7
+ return hasPackage(packageJson, "svelte");
8
+ },
9
+ };
@@ -0,0 +1,9 @@
1
+ import { hasPackage } from "../../utils.js";
2
+ export const vueFramework = {
3
+ id: "vue",
4
+ testEnvironment: "jsdom",
5
+ tsRequiredExcludes: ["dist", "coverage"],
6
+ matches(packageJson) {
7
+ return hasPackage(packageJson, "vue");
8
+ },
9
+ };
@@ -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,3 @@
1
+ import { createRequire } from "node:module";
2
+ const require = createRequire(import.meta.url);
3
+ export const { file, json, lines, packageJson, install } = require("mrm-core");
@@ -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
+ };