@tyyyho/treg 0.1.16 → 0.1.17
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 +25 -37
- package/README.npm.md +25 -37
- package/README.zh-hant.md +56 -58
- package/dist/init-project/cli.js +18 -52
- package/dist/init-project/index.js +25 -7
- package/dist/init-project/init-prompts.js +221 -0
- package/dist/init-project/mrm-rules/ai-skills.js +13 -6
- package/dist/init-project/mrm-rules/husky.js +4 -0
- package/dist/init-project/package-manager.js +13 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# @tyyyho/treg
|
|
2
2
|
|
|
3
|
-
`treg`
|
|
3
|
+
`treg` applies tooling standards to existing repositories.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Scope:
|
|
6
6
|
|
|
7
7
|
- lint
|
|
8
8
|
- format
|
|
@@ -11,71 +11,59 @@ It sets up infrastructure only:
|
|
|
11
11
|
- husky
|
|
12
12
|
- AI skill guidance
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
Quick start:
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
17
|
npx @tyyyho/treg init
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
Commands:
|
|
21
21
|
|
|
22
22
|
- `init`
|
|
23
23
|
- `add`
|
|
24
24
|
- `list`
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
`init` interactive questions:
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
1. package manager (`pnpm|npm|yarn|bun`)
|
|
29
|
+
2. features (default: `all`)
|
|
30
|
+
3. test runner (if `test` is selected, supports `skip`)
|
|
31
|
+
4. formatter (if `format` is selected)
|
|
32
|
+
5. ai tools (`Claude|Codex|Gemini`, multi-select, if AI skill guidance is selected)
|
|
29
33
|
|
|
30
|
-
|
|
31
|
-
npx @tyyyho/treg init
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
Initialize with explicit framework:
|
|
35
|
-
|
|
36
|
-
```bash
|
|
37
|
-
npx @tyyyho/treg init --framework react
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
Apply selected features:
|
|
34
|
+
`add` examples:
|
|
41
35
|
|
|
42
36
|
```bash
|
|
43
37
|
npx @tyyyho/treg add --features lint,format
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
Use `oxfmt`:
|
|
47
|
-
|
|
48
|
-
```bash
|
|
49
38
|
npx @tyyyho/treg add --features format --formatter oxfmt
|
|
39
|
+
npx @tyyyho/treg add --features test --test-runner vitest
|
|
50
40
|
```
|
|
51
41
|
|
|
52
|
-
|
|
42
|
+
Options:
|
|
53
43
|
|
|
54
|
-
|
|
55
|
-
npx @tyyyho/treg add --no-format --no-test-runner
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
Preview only:
|
|
44
|
+
`init`:
|
|
59
45
|
|
|
60
|
-
```
|
|
61
|
-
|
|
46
|
+
```text
|
|
47
|
+
--dry-run
|
|
48
|
+
--help
|
|
62
49
|
```
|
|
63
50
|
|
|
64
|
-
|
|
51
|
+
`add`:
|
|
65
52
|
|
|
66
53
|
```text
|
|
67
54
|
--framework <node|react|next|vue|svelte|nuxt>
|
|
68
55
|
--features <lint,format,typescript,test,husky>
|
|
69
|
-
--
|
|
70
|
-
--no-test-runner
|
|
56
|
+
--dir <path>
|
|
71
57
|
--formatter <prettier|oxfmt>
|
|
72
58
|
--test-runner <jest|vitest>
|
|
73
|
-
--pm <pnpm|npm|yarn|auto>
|
|
74
|
-
--dir <path>
|
|
75
59
|
--force
|
|
76
60
|
--dry-run
|
|
77
61
|
--skip-husky-install
|
|
78
|
-
--skills
|
|
79
|
-
--no-skills
|
|
80
62
|
--help
|
|
81
63
|
```
|
|
64
|
+
|
|
65
|
+
Defaults:
|
|
66
|
+
|
|
67
|
+
- framework detect order: `nuxt -> next -> react -> vue -> svelte -> node`
|
|
68
|
+
- test runner: `vue/nuxt = vitest`, others = `jest`
|
|
69
|
+
- formatter: `prettier`
|
package/README.npm.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# @tyyyho/treg
|
|
2
2
|
|
|
3
|
-
`treg`
|
|
3
|
+
`treg` applies tooling standards to existing repositories.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Scope:
|
|
6
6
|
|
|
7
7
|
- lint
|
|
8
8
|
- format
|
|
@@ -11,71 +11,59 @@ It sets up infrastructure only:
|
|
|
11
11
|
- husky
|
|
12
12
|
- AI skill guidance
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
Quick start:
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
17
|
npx @tyyyho/treg init
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
Commands:
|
|
21
21
|
|
|
22
22
|
- `init`
|
|
23
23
|
- `add`
|
|
24
24
|
- `list`
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
`init` interactive questions:
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
1. package manager (`pnpm|npm|yarn|bun`)
|
|
29
|
+
2. features (default: `all`)
|
|
30
|
+
3. test runner (if `test` is selected, supports `skip`)
|
|
31
|
+
4. formatter (if `format` is selected)
|
|
32
|
+
5. ai tools (`Claude|Codex|Gemini`, multi-select, if AI skill guidance is selected)
|
|
29
33
|
|
|
30
|
-
|
|
31
|
-
npx @tyyyho/treg init
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
Initialize with explicit framework:
|
|
35
|
-
|
|
36
|
-
```bash
|
|
37
|
-
npx @tyyyho/treg init --framework react
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
Apply selected features:
|
|
34
|
+
`add` examples:
|
|
41
35
|
|
|
42
36
|
```bash
|
|
43
37
|
npx @tyyyho/treg add --features lint,format
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
Use `oxfmt`:
|
|
47
|
-
|
|
48
|
-
```bash
|
|
49
38
|
npx @tyyyho/treg add --features format --formatter oxfmt
|
|
39
|
+
npx @tyyyho/treg add --features test --test-runner vitest
|
|
50
40
|
```
|
|
51
41
|
|
|
52
|
-
|
|
42
|
+
Options:
|
|
53
43
|
|
|
54
|
-
|
|
55
|
-
npx @tyyyho/treg add --no-format --no-test-runner
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
Preview only:
|
|
44
|
+
`init`:
|
|
59
45
|
|
|
60
|
-
```
|
|
61
|
-
|
|
46
|
+
```text
|
|
47
|
+
--dry-run
|
|
48
|
+
--help
|
|
62
49
|
```
|
|
63
50
|
|
|
64
|
-
|
|
51
|
+
`add`:
|
|
65
52
|
|
|
66
53
|
```text
|
|
67
54
|
--framework <node|react|next|vue|svelte|nuxt>
|
|
68
55
|
--features <lint,format,typescript,test,husky>
|
|
69
|
-
--
|
|
70
|
-
--no-test-runner
|
|
56
|
+
--dir <path>
|
|
71
57
|
--formatter <prettier|oxfmt>
|
|
72
58
|
--test-runner <jest|vitest>
|
|
73
|
-
--pm <pnpm|npm|yarn|auto>
|
|
74
|
-
--dir <path>
|
|
75
59
|
--force
|
|
76
60
|
--dry-run
|
|
77
61
|
--skip-husky-install
|
|
78
|
-
--skills
|
|
79
|
-
--no-skills
|
|
80
62
|
--help
|
|
81
63
|
```
|
|
64
|
+
|
|
65
|
+
Defaults:
|
|
66
|
+
|
|
67
|
+
- framework detect order: `nuxt -> next -> react -> vue -> svelte -> node`
|
|
68
|
+
- test runner: `vue/nuxt = vitest`, others = `jest`
|
|
69
|
+
- formatter: `prettier`
|
package/README.zh-hant.md
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
[English README](./README.md)
|
|
4
4
|
|
|
5
5
|
`treg` 是一個用於既有專案的 CLI,可快速套用一致的工具鏈規範。
|
|
6
|
-
|
|
7
6
|
它只處理基礎設施設定:
|
|
8
7
|
|
|
9
8
|
- lint
|
|
@@ -13,78 +12,93 @@
|
|
|
13
12
|
- husky
|
|
14
13
|
- AI skill 指引
|
|
15
14
|
|
|
16
|
-
## 為什麼用 treg
|
|
17
|
-
|
|
18
|
-
`treg` 可以在既有 repo 中快速建立一致的開發基線,避免每次手動重接工具。
|
|
19
|
-
|
|
20
|
-
適合用在:
|
|
21
|
-
|
|
22
|
-
- 快速補齊專案工具鏈
|
|
23
|
-
- 只套用部分 feature
|
|
24
|
-
- 需要可重跑且不破壞設定(`idempotent`)
|
|
25
|
-
- 先看完整計畫再寫檔(`--dry-run`)
|
|
26
|
-
|
|
27
15
|
## 快速開始
|
|
28
16
|
|
|
29
17
|
```bash
|
|
30
18
|
npx @tyyyho/treg init
|
|
31
19
|
```
|
|
32
20
|
|
|
33
|
-
```bash
|
|
34
|
-
pnpm dlx @tyyyho/treg init
|
|
35
|
-
```
|
|
36
|
-
|
|
37
21
|
## 指令總覽
|
|
38
22
|
|
|
39
|
-
- `init
|
|
40
|
-
- `add
|
|
41
|
-
- `list`:列出支援的 framework
|
|
23
|
+
- `init`:初始化功能。
|
|
24
|
+
- `add`:只為既有專案加入指定功能。
|
|
25
|
+
- `list`:列出支援的 framework/feature/formatter/test runner。
|
|
26
|
+
|
|
27
|
+
## Init 互動流程
|
|
28
|
+
|
|
29
|
+
執行 `init` 後,`treg` 會依序詢問:
|
|
30
|
+
|
|
31
|
+
1. 套件管理器(`pnpm|npm|yarn|bun`)
|
|
32
|
+
2. 要加入的功能(預設勾選 `all`)
|
|
33
|
+
3. 測試工具(僅在選到 `test` 時詢問,支援 `skip`)
|
|
34
|
+
4. Formatter(僅在選到 `format` 時詢問)
|
|
35
|
+
5. AI 工具(`Claude|Codex|Gemini` 可複選,僅在選到 AI skill guidance 時詢問)
|
|
36
|
+
|
|
37
|
+
預設 `all` 內容:
|
|
38
|
+
|
|
39
|
+
- lint
|
|
40
|
+
- format
|
|
41
|
+
- TypeScript
|
|
42
|
+
- test
|
|
43
|
+
- husky
|
|
44
|
+
- AI skill guidance
|
|
42
45
|
|
|
43
46
|
## 常見用法
|
|
44
47
|
|
|
45
|
-
|
|
48
|
+
初始化:
|
|
46
49
|
|
|
47
50
|
```bash
|
|
48
51
|
npx @tyyyho/treg init
|
|
49
52
|
```
|
|
50
53
|
|
|
51
|
-
|
|
54
|
+
只預覽 init 計畫:
|
|
52
55
|
|
|
53
56
|
```bash
|
|
54
|
-
npx @tyyyho/treg init --
|
|
57
|
+
npx @tyyyho/treg init --dry-run
|
|
55
58
|
```
|
|
56
59
|
|
|
57
|
-
|
|
60
|
+
只加入 lint + format:
|
|
58
61
|
|
|
59
62
|
```bash
|
|
60
63
|
npx @tyyyho/treg add --features lint,format
|
|
61
64
|
```
|
|
62
65
|
|
|
63
|
-
format
|
|
66
|
+
format 使用 `oxfmt`:
|
|
64
67
|
|
|
65
68
|
```bash
|
|
66
69
|
npx @tyyyho/treg add --features format --formatter oxfmt
|
|
67
70
|
```
|
|
68
71
|
|
|
69
|
-
|
|
72
|
+
test 使用 `vitest`:
|
|
70
73
|
|
|
71
74
|
```bash
|
|
72
|
-
npx @tyyyho/treg add --
|
|
75
|
+
npx @tyyyho/treg add --features test --test-runner vitest
|
|
73
76
|
```
|
|
74
77
|
|
|
75
|
-
|
|
78
|
+
## CLI 參數
|
|
79
|
+
|
|
80
|
+
`init` 可用參數:
|
|
76
81
|
|
|
77
|
-
```
|
|
78
|
-
|
|
82
|
+
```text
|
|
83
|
+
--dry-run
|
|
84
|
+
--help
|
|
79
85
|
```
|
|
80
86
|
|
|
81
|
-
|
|
87
|
+
`add` 可用參數:
|
|
82
88
|
|
|
83
|
-
```
|
|
84
|
-
|
|
89
|
+
```text
|
|
90
|
+
--framework <node|react|next|vue|svelte|nuxt>
|
|
91
|
+
--features <lint,format,typescript,test,husky>
|
|
92
|
+
--dir <path>
|
|
93
|
+
--formatter <prettier|oxfmt>
|
|
94
|
+
--test-runner <jest|vitest>
|
|
95
|
+
--force
|
|
96
|
+
--dry-run
|
|
97
|
+
--skip-husky-install
|
|
98
|
+
--help
|
|
85
99
|
```
|
|
86
100
|
|
|
87
|
-
##
|
|
101
|
+
## 預設行為
|
|
88
102
|
|
|
89
103
|
framework 偵測順序:
|
|
90
104
|
|
|
@@ -97,38 +111,22 @@ framework 偵測順序:
|
|
|
97
111
|
|
|
98
112
|
formatter 預設:
|
|
99
113
|
|
|
100
|
-
- `prettier
|
|
101
|
-
|
|
102
|
-
## CLI 參數
|
|
103
|
-
|
|
104
|
-
```text
|
|
105
|
-
--framework <node|react|next|vue|svelte|nuxt>
|
|
106
|
-
--features <lint,format,typescript,test,husky>
|
|
107
|
-
--no-format
|
|
108
|
-
--no-test-runner
|
|
109
|
-
--dir <path>
|
|
110
|
-
--formatter <prettier|oxfmt>
|
|
111
|
-
--test-runner <jest|vitest>
|
|
112
|
-
--pm <pnpm|npm|yarn|auto>
|
|
113
|
-
--force
|
|
114
|
-
--dry-run
|
|
115
|
-
--skip-husky-install
|
|
116
|
-
--skills
|
|
117
|
-
--no-skills
|
|
118
|
-
--help
|
|
119
|
-
```
|
|
114
|
+
- `prettier`
|
|
120
115
|
|
|
121
116
|
## AI Skills 行為
|
|
122
117
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
-
|
|
126
|
-
-
|
|
118
|
+
- 只會更新選擇的 AI 工具對應檔案:
|
|
119
|
+
- `Claude -> CLAUDE.md`
|
|
120
|
+
- `Codex -> AGENTS.md`
|
|
121
|
+
- `Gemini -> GEMINI.md`
|
|
122
|
+
- 僅更新 repo root 已存在的檔案。
|
|
123
|
+
- 不存在的檔案會跳過,不會自動建立。
|
|
124
|
+
- 每個啟用功能的 skill 檔只會建立一次。
|
|
127
125
|
|
|
128
126
|
## 發布
|
|
129
127
|
|
|
130
128
|
```bash
|
|
131
|
-
|
|
129
|
+
npm run release -- patch
|
|
132
130
|
```
|
|
133
131
|
|
|
134
132
|
支援目標:
|
package/dist/init-project/cli.js
CHANGED
|
@@ -1,10 +1,4 @@
|
|
|
1
1
|
const ALLOWED_COMMANDS = ["init", "add", "list"];
|
|
2
|
-
const ALLOWED_PACKAGE_MANAGERS = [
|
|
3
|
-
"pnpm",
|
|
4
|
-
"npm",
|
|
5
|
-
"yarn",
|
|
6
|
-
"auto",
|
|
7
|
-
];
|
|
8
2
|
const ALLOWED_FRAMEWORKS = [
|
|
9
3
|
"node",
|
|
10
4
|
"react",
|
|
@@ -22,30 +16,28 @@ const ALLOWED_FEATURES = [
|
|
|
22
16
|
];
|
|
23
17
|
const ALLOWED_TEST_RUNNERS = ["jest", "vitest"];
|
|
24
18
|
const ALLOWED_FORMATTERS = ["prettier", "oxfmt"];
|
|
19
|
+
const DEFAULT_AI_TOOLS = ["claude", "codex", "gemini"];
|
|
25
20
|
export const USAGE = `Usage: treg <command> [options]
|
|
26
21
|
|
|
27
22
|
Commands:
|
|
28
|
-
init Initialize infra rules in a project (
|
|
23
|
+
init Initialize infra rules in a project (interactive setup)
|
|
29
24
|
add Add selected infra features to an existing project
|
|
30
25
|
list List supported frameworks, features, formatters, and test runners
|
|
31
26
|
|
|
32
27
|
Options:
|
|
28
|
+
--dry-run Print planned changes without writing files
|
|
29
|
+
-h, --help Show help
|
|
30
|
+
|
|
31
|
+
Add command options:
|
|
33
32
|
--framework <node|react|next|vue|svelte|nuxt>
|
|
34
33
|
Optional framework override (default: auto-detected)
|
|
35
34
|
--features <lint,format,typescript,test,husky>
|
|
36
35
|
Features to install (all selected by default)
|
|
37
|
-
--no-format Skip format feature setup and avoid changing format config/scripts
|
|
38
|
-
--no-test-runner Skip test feature setup and avoid changing test runner/config
|
|
39
36
|
--dir <path> Target directory (defaults to current directory)
|
|
40
37
|
--formatter <prettier|oxfmt> Formatter for format feature (default: prettier)
|
|
41
38
|
--test-runner <jest|vitest> Optional test runner override (default: vue/nuxt=vitest, others=jest)
|
|
42
|
-
--pm <pnpm|npm|yarn|auto> Package manager (auto-detected if omitted)
|
|
43
39
|
--force Overwrite existing config files
|
|
44
|
-
--dry-run Print planned changes without writing files
|
|
45
40
|
--skip-husky-install Do not run husky install
|
|
46
|
-
--skills Update CLAUDE.md/AGENTS.md/GEMINI.md with feature skill guidance (default: enabled)
|
|
47
|
-
--no-skills Disable CLAUDE.md/AGENTS.md/GEMINI.md skill guidance updates
|
|
48
|
-
-h, --help Show help
|
|
49
41
|
`;
|
|
50
42
|
function includes(allowed, value) {
|
|
51
43
|
return allowed.includes(value);
|
|
@@ -53,9 +45,6 @@ function includes(allowed, value) {
|
|
|
53
45
|
function isCommandName(value) {
|
|
54
46
|
return includes(ALLOWED_COMMANDS, value);
|
|
55
47
|
}
|
|
56
|
-
function isPackageManagerOption(value) {
|
|
57
|
-
return includes(ALLOWED_PACKAGE_MANAGERS, value);
|
|
58
|
-
}
|
|
59
48
|
function isFrameworkId(value) {
|
|
60
49
|
return includes(ALLOWED_FRAMEWORKS, value);
|
|
61
50
|
}
|
|
@@ -76,13 +65,11 @@ export function parseArgs(argv) {
|
|
|
76
65
|
formatter: "prettier",
|
|
77
66
|
features: [],
|
|
78
67
|
testRunner: null,
|
|
79
|
-
pm: null,
|
|
80
68
|
force: false,
|
|
81
69
|
dryRun: false,
|
|
82
|
-
noFormat: false,
|
|
83
|
-
noTestRunner: false,
|
|
84
70
|
skipHuskyInstall: false,
|
|
85
71
|
skills: true,
|
|
72
|
+
aiTools: [...DEFAULT_AI_TOOLS],
|
|
86
73
|
help: false,
|
|
87
74
|
};
|
|
88
75
|
let cursor = 0;
|
|
@@ -98,8 +85,16 @@ export function parseArgs(argv) {
|
|
|
98
85
|
}
|
|
99
86
|
if (arg === "-h" || arg === "--help") {
|
|
100
87
|
options.help = true;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (arg === "--dry-run") {
|
|
91
|
+
options.dryRun = true;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (options.command !== "add") {
|
|
95
|
+
throw new Error(`Unsupported option for ${options.command}: ${arg}. Only --dry-run and --help are allowed.`);
|
|
101
96
|
}
|
|
102
|
-
|
|
97
|
+
if (arg === "--framework") {
|
|
103
98
|
options.framework = readFlagValue(argv, i, "--framework");
|
|
104
99
|
i += 1;
|
|
105
100
|
}
|
|
@@ -134,34 +129,12 @@ export function parseArgs(argv) {
|
|
|
134
129
|
else if (arg.startsWith("--test-runner=")) {
|
|
135
130
|
options.testRunner = readInlineFlagValue(arg, "--test-runner");
|
|
136
131
|
}
|
|
137
|
-
else if (arg === "--pm") {
|
|
138
|
-
options.pm = readFlagValue(argv, i, "--pm");
|
|
139
|
-
i += 1;
|
|
140
|
-
}
|
|
141
|
-
else if (arg.startsWith("--pm=")) {
|
|
142
|
-
options.pm = readInlineFlagValue(arg, "--pm");
|
|
143
|
-
}
|
|
144
132
|
else if (arg === "--force") {
|
|
145
133
|
options.force = true;
|
|
146
134
|
}
|
|
147
|
-
else if (arg === "--dry-run") {
|
|
148
|
-
options.dryRun = true;
|
|
149
|
-
}
|
|
150
|
-
else if (arg === "--no-format") {
|
|
151
|
-
options.noFormat = true;
|
|
152
|
-
}
|
|
153
|
-
else if (arg === "--no-test-runner") {
|
|
154
|
-
options.noTestRunner = true;
|
|
155
|
-
}
|
|
156
135
|
else if (arg === "--skip-husky-install") {
|
|
157
136
|
options.skipHuskyInstall = true;
|
|
158
137
|
}
|
|
159
|
-
else if (arg === "--skills") {
|
|
160
|
-
options.skills = true;
|
|
161
|
-
}
|
|
162
|
-
else if (arg === "--no-skills") {
|
|
163
|
-
options.skills = false;
|
|
164
|
-
}
|
|
165
138
|
else {
|
|
166
139
|
throw new Error(`Unknown argument: ${arg}`);
|
|
167
140
|
}
|
|
@@ -199,9 +172,6 @@ function validateParsedOptions(options) {
|
|
|
199
172
|
if (options.projectDir === "") {
|
|
200
173
|
throw new Error("Missing value for --dir");
|
|
201
174
|
}
|
|
202
|
-
if (options.pm && !isPackageManagerOption(options.pm)) {
|
|
203
|
-
throw new Error(`Unsupported package manager: ${options.pm}`);
|
|
204
|
-
}
|
|
205
175
|
if (options.framework && !isFrameworkId(options.framework)) {
|
|
206
176
|
throw new Error(`Unsupported framework: ${options.framework}`);
|
|
207
177
|
}
|
|
@@ -219,12 +189,6 @@ function validateParsedOptions(options) {
|
|
|
219
189
|
}
|
|
220
190
|
export function resolveFeatures(options) {
|
|
221
191
|
const selected = new Set(options.features.length > 0 ? options.features : ALLOWED_FEATURES);
|
|
222
|
-
if (options.noFormat) {
|
|
223
|
-
selected.delete("format");
|
|
224
|
-
}
|
|
225
|
-
if (options.noTestRunner) {
|
|
226
|
-
selected.delete("test");
|
|
227
|
-
}
|
|
228
192
|
return {
|
|
229
193
|
lint: selected.has("lint"),
|
|
230
194
|
format: selected.has("format"),
|
|
@@ -238,6 +202,8 @@ export function printSupportedTargets() {
|
|
|
238
202
|
console.log("Features: lint, format, typescript, test, husky");
|
|
239
203
|
console.log("Formatters: prettier, oxfmt");
|
|
240
204
|
console.log("Test runners: jest, vitest");
|
|
205
|
+
console.log("Package managers: pnpm, npm, yarn, bun");
|
|
206
|
+
console.log("AI tools: Claude, Codex, Gemini");
|
|
241
207
|
}
|
|
242
208
|
export function resolveTestRunner(frameworkId, testRunner) {
|
|
243
209
|
if (testRunner) {
|
|
@@ -3,6 +3,7 @@ import { promises as fs } from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { parseArgs, printSupportedTargets, resolveFeatures, resolveTestRunner, USAGE, } from "./cli.js";
|
|
5
5
|
import { resolveFramework } from "./frameworks/index.js";
|
|
6
|
+
import { collectInitPrompts } from "./init-prompts.js";
|
|
6
7
|
import { runFeatureRules } from "./mrm-rules/index.js";
|
|
7
8
|
import { detectPackageManager, runScript } from "./package-manager.js";
|
|
8
9
|
import { formatStep } from "./utils.js";
|
|
@@ -34,25 +35,42 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
34
35
|
return;
|
|
35
36
|
}
|
|
36
37
|
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf8"));
|
|
37
|
-
const pm = !options.pm || options.pm === "auto"
|
|
38
|
-
? detectPackageManager(projectDir)
|
|
39
|
-
: options.pm;
|
|
40
38
|
const framework = resolveFramework(options.framework, packageJson);
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
let pm = detectPackageManager(projectDir);
|
|
40
|
+
let formatter = options.formatter;
|
|
41
|
+
let testRunner = resolveTestRunner(framework.id, options.testRunner);
|
|
42
|
+
let enabledFeatures = resolveFeatures(options);
|
|
43
|
+
let skills = options.skills;
|
|
44
|
+
let aiTools = [...options.aiTools];
|
|
45
|
+
if (options.command === "init") {
|
|
46
|
+
const prompted = await collectInitPrompts({
|
|
47
|
+
pm,
|
|
48
|
+
formatter,
|
|
49
|
+
testRunner: resolveTestRunner(framework.id, null),
|
|
50
|
+
});
|
|
51
|
+
pm = prompted.pm;
|
|
52
|
+
formatter = prompted.formatter;
|
|
53
|
+
testRunner = prompted.testRunner;
|
|
54
|
+
enabledFeatures = prompted.enabledFeatures;
|
|
55
|
+
skills = prompted.skills;
|
|
56
|
+
aiTools = prompted.aiTools;
|
|
57
|
+
}
|
|
43
58
|
const context = {
|
|
44
59
|
...options,
|
|
60
|
+
formatter,
|
|
45
61
|
testRunner,
|
|
46
62
|
projectDir,
|
|
47
63
|
pm,
|
|
48
64
|
framework,
|
|
49
65
|
enabledFeatures,
|
|
66
|
+
skills,
|
|
67
|
+
aiTools,
|
|
50
68
|
};
|
|
51
69
|
console.log(formatStep(1, TOTAL_STEPS, "Resolve plan", options.dryRun));
|
|
52
|
-
console.log(`${options.dryRun ? "[dry-run] " : ""}Framework=${framework.id}, features=${Object.entries(enabledFeatures)
|
|
70
|
+
console.log(`${options.dryRun ? "[dry-run] " : ""}Framework=${framework.id}, pm=${pm}, features=${Object.entries(enabledFeatures)
|
|
53
71
|
.filter(([, enabled]) => enabled)
|
|
54
72
|
.map(([name]) => name)
|
|
55
|
-
.join(", ")}, formatter=${
|
|
73
|
+
.join(", ")}, formatter=${formatter}, testRunner=${testRunner}, aiTools=${skills ? aiTools.join(", ") : "disabled"}`);
|
|
56
74
|
console.log(formatStep(2, TOTAL_STEPS, "Run mrm rules", options.dryRun));
|
|
57
75
|
await runFeatureRules(context);
|
|
58
76
|
console.log(formatStep(3, TOTAL_STEPS, "Finalize", options.dryRun));
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
2
|
+
import { createInterface } from "node:readline/promises";
|
|
3
|
+
const DEFAULT_AI_TOOLS = ["claude", "codex", "gemini"];
|
|
4
|
+
const PACKAGE_MANAGER_CHOICES = [
|
|
5
|
+
{ value: "pnpm", label: "pnpm" },
|
|
6
|
+
{ value: "npm", label: "npm" },
|
|
7
|
+
{ value: "yarn", label: "yarn" },
|
|
8
|
+
{ value: "bun", label: "bun" },
|
|
9
|
+
];
|
|
10
|
+
const TEST_RUNNER_CHOICES = [
|
|
11
|
+
{ value: "jest", label: "jest" },
|
|
12
|
+
{ value: "vitest", label: "vitest" },
|
|
13
|
+
{ value: "skip", label: "skip (disable test feature)" },
|
|
14
|
+
];
|
|
15
|
+
const FORMATTER_CHOICES = [
|
|
16
|
+
{ value: "prettier", label: "prettier" },
|
|
17
|
+
{ value: "oxfmt", label: "oxfmt" },
|
|
18
|
+
];
|
|
19
|
+
const AI_TOOL_CHOICES = [
|
|
20
|
+
{ value: "claude", label: "Claude" },
|
|
21
|
+
{ value: "codex", label: "Codex" },
|
|
22
|
+
{ value: "gemini", label: "Gemini" },
|
|
23
|
+
];
|
|
24
|
+
const FEATURE_CHOICES = [
|
|
25
|
+
{
|
|
26
|
+
value: "all",
|
|
27
|
+
label: "all (lint, format, TypeScript, test, husky, AI skill guidance)",
|
|
28
|
+
},
|
|
29
|
+
{ value: "lint", label: "lint" },
|
|
30
|
+
{ value: "format", label: "format" },
|
|
31
|
+
{ value: "typescript", label: "TypeScript" },
|
|
32
|
+
{ value: "test", label: "test" },
|
|
33
|
+
{ value: "husky", label: "husky" },
|
|
34
|
+
{ value: "skills", label: "AI skill guidance" },
|
|
35
|
+
];
|
|
36
|
+
function formatChoices(choices) {
|
|
37
|
+
return choices
|
|
38
|
+
.map((choice, index) => ` ${index + 1}. ${choice.label}`)
|
|
39
|
+
.join("\n");
|
|
40
|
+
}
|
|
41
|
+
function parseSingleChoice(rawInput, choices, defaultValue) {
|
|
42
|
+
const normalized = rawInput.trim().toLowerCase();
|
|
43
|
+
if (!normalized) {
|
|
44
|
+
return { ok: true, value: defaultValue };
|
|
45
|
+
}
|
|
46
|
+
const byIndex = Number.parseInt(normalized, 10);
|
|
47
|
+
if (!Number.isNaN(byIndex) && String(byIndex) === normalized) {
|
|
48
|
+
const selected = choices[byIndex - 1];
|
|
49
|
+
if (selected) {
|
|
50
|
+
return { ok: true, value: selected.value };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const byValue = choices.find(choice => choice.value === normalized);
|
|
54
|
+
if (byValue) {
|
|
55
|
+
return { ok: true, value: byValue.value };
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
ok: false,
|
|
59
|
+
error: `Invalid input: ${rawInput}. Please enter a listed number or option name.`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function parseMultiChoice(rawInput, choices, defaultValues) {
|
|
63
|
+
const normalized = rawInput.trim().toLowerCase();
|
|
64
|
+
if (!normalized) {
|
|
65
|
+
return { ok: true, value: [...defaultValues] };
|
|
66
|
+
}
|
|
67
|
+
if (normalized === "skip" || normalized === "none") {
|
|
68
|
+
return { ok: true, value: [] };
|
|
69
|
+
}
|
|
70
|
+
const tokens = normalized
|
|
71
|
+
.split(",")
|
|
72
|
+
.map(item => item.trim())
|
|
73
|
+
.filter(Boolean);
|
|
74
|
+
if (tokens.length === 0) {
|
|
75
|
+
return { ok: false, error: "Please enter at least one option." };
|
|
76
|
+
}
|
|
77
|
+
const selected = new Set();
|
|
78
|
+
for (const token of tokens) {
|
|
79
|
+
const byIndex = Number.parseInt(token, 10);
|
|
80
|
+
if (!Number.isNaN(byIndex) && String(byIndex) === token) {
|
|
81
|
+
const item = choices[byIndex - 1];
|
|
82
|
+
if (!item) {
|
|
83
|
+
return {
|
|
84
|
+
ok: false,
|
|
85
|
+
error: `Invalid index: ${token}. Please choose from listed options.`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
selected.add(item.value);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const byValue = choices.find(choice => choice.value === token);
|
|
92
|
+
if (!byValue) {
|
|
93
|
+
return {
|
|
94
|
+
ok: false,
|
|
95
|
+
error: `Invalid option: ${token}. Please choose from listed options.`,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
selected.add(byValue.value);
|
|
99
|
+
}
|
|
100
|
+
return { ok: true, value: [...selected] };
|
|
101
|
+
}
|
|
102
|
+
function toFeatureSelection(selected) {
|
|
103
|
+
if (selected.includes("all")) {
|
|
104
|
+
return {
|
|
105
|
+
enabledFeatures: {
|
|
106
|
+
lint: true,
|
|
107
|
+
format: true,
|
|
108
|
+
typescript: true,
|
|
109
|
+
test: true,
|
|
110
|
+
husky: true,
|
|
111
|
+
},
|
|
112
|
+
skills: true,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
enabledFeatures: {
|
|
117
|
+
lint: selected.includes("lint"),
|
|
118
|
+
format: selected.includes("format"),
|
|
119
|
+
typescript: selected.includes("typescript"),
|
|
120
|
+
test: selected.includes("test"),
|
|
121
|
+
husky: selected.includes("husky"),
|
|
122
|
+
},
|
|
123
|
+
skills: selected.includes("skills"),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
async function askUntilValid(ask, prompt, parser) {
|
|
127
|
+
while (true) {
|
|
128
|
+
const raw = await ask(prompt);
|
|
129
|
+
const parsed = parser(raw);
|
|
130
|
+
if (parsed.ok && parsed.value !== undefined) {
|
|
131
|
+
return parsed.value;
|
|
132
|
+
}
|
|
133
|
+
console.log(parsed.error ?? "Invalid input");
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
export async function collectInitPrompts(defaults) {
|
|
137
|
+
if (!input.isTTY || !output.isTTY) {
|
|
138
|
+
console.log("Non-interactive shell detected. Use init defaults.");
|
|
139
|
+
return {
|
|
140
|
+
pm: defaults.pm,
|
|
141
|
+
formatter: defaults.formatter,
|
|
142
|
+
testRunner: defaults.testRunner,
|
|
143
|
+
enabledFeatures: {
|
|
144
|
+
lint: true,
|
|
145
|
+
format: true,
|
|
146
|
+
typescript: true,
|
|
147
|
+
test: true,
|
|
148
|
+
husky: true,
|
|
149
|
+
},
|
|
150
|
+
skills: true,
|
|
151
|
+
aiTools: [...DEFAULT_AI_TOOLS],
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
const rl = createInterface({ input, output });
|
|
155
|
+
try {
|
|
156
|
+
console.log("\nInit setup");
|
|
157
|
+
console.log("\n1) Package manager");
|
|
158
|
+
console.log(formatChoices(PACKAGE_MANAGER_CHOICES));
|
|
159
|
+
const pm = await askUntilValid(rl.question.bind(rl), `Select package manager [default: ${defaults.pm}]: `, answer => parseSingleChoice(answer, PACKAGE_MANAGER_CHOICES, defaults.pm));
|
|
160
|
+
console.log("\n2) Features");
|
|
161
|
+
console.log(formatChoices(FEATURE_CHOICES));
|
|
162
|
+
const featureAnswers = await askUntilValid(rl.question.bind(rl), "Select features (comma-separated, default: all): ", answer => parseMultiChoice(answer, FEATURE_CHOICES, ["all"]));
|
|
163
|
+
const featureSelection = toFeatureSelection(featureAnswers);
|
|
164
|
+
let testRunner = defaults.testRunner;
|
|
165
|
+
const enabledFeatures = { ...featureSelection.enabledFeatures };
|
|
166
|
+
if (featureSelection.enabledFeatures.test) {
|
|
167
|
+
console.log("\n3) Test runner");
|
|
168
|
+
console.log(formatChoices(TEST_RUNNER_CHOICES));
|
|
169
|
+
const selectedTestRunner = await askUntilValid(rl.question.bind(rl), `Select test runner [default: ${defaults.testRunner}, or skip]: `, answer => parseSingleChoice(answer, TEST_RUNNER_CHOICES, defaults.testRunner));
|
|
170
|
+
if (selectedTestRunner === "skip") {
|
|
171
|
+
enabledFeatures.test = false;
|
|
172
|
+
console.log("Test feature disabled by selection: skip");
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
testRunner = selectedTestRunner;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
console.log("\n3) Test runner skipped (test feature not selected)");
|
|
180
|
+
}
|
|
181
|
+
let formatter = defaults.formatter;
|
|
182
|
+
if (featureSelection.enabledFeatures.format) {
|
|
183
|
+
console.log("\n4) Formatter");
|
|
184
|
+
console.log(formatChoices(FORMATTER_CHOICES));
|
|
185
|
+
formatter = await askUntilValid(rl.question.bind(rl), `Select formatter [default: ${defaults.formatter}]: `, answer => parseSingleChoice(answer, FORMATTER_CHOICES, defaults.formatter));
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
console.log("\n4) Formatter skipped (format feature not selected)");
|
|
189
|
+
}
|
|
190
|
+
let aiTools = [];
|
|
191
|
+
let skills = featureSelection.skills;
|
|
192
|
+
if (skills) {
|
|
193
|
+
console.log("\n5) AI tools");
|
|
194
|
+
console.log(formatChoices(AI_TOOL_CHOICES));
|
|
195
|
+
console.log("Type 'skip' to disable AI skill guidance.");
|
|
196
|
+
aiTools = await askUntilValid(rl.question.bind(rl), "Select AI tools (comma-separated, default: all): ", answer => parseMultiChoice(answer, AI_TOOL_CHOICES, DEFAULT_AI_TOOLS));
|
|
197
|
+
if (aiTools.length === 0) {
|
|
198
|
+
skills = false;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
console.log("\n5) AI tools skipped (AI skill guidance not selected)");
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
pm,
|
|
206
|
+
formatter,
|
|
207
|
+
testRunner,
|
|
208
|
+
enabledFeatures,
|
|
209
|
+
skills,
|
|
210
|
+
aiTools,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
finally {
|
|
214
|
+
rl.close();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
export const __testables__ = {
|
|
218
|
+
parseSingleChoice,
|
|
219
|
+
parseMultiChoice,
|
|
220
|
+
toFeatureSelection,
|
|
221
|
+
};
|
|
@@ -5,7 +5,11 @@ const START_MARKER = "<!-- treg:skills:start -->";
|
|
|
5
5
|
const END_MARKER = "<!-- treg:skills:end -->";
|
|
6
6
|
const SKILL_SECTION_HEADING = "## treg AI Skills";
|
|
7
7
|
const SKILLS_BASE_DIR = "skills";
|
|
8
|
-
const
|
|
8
|
+
const AI_TOOL_DOCS = {
|
|
9
|
+
claude: "CLAUDE.md",
|
|
10
|
+
codex: "AGENTS.md",
|
|
11
|
+
gemini: "GEMINI.md",
|
|
12
|
+
};
|
|
9
13
|
const FEATURE_SKILLS = {
|
|
10
14
|
format: {
|
|
11
15
|
name: "treg/format",
|
|
@@ -65,8 +69,11 @@ const FEATURE_STEP_LABELS = {
|
|
|
65
69
|
test: "Test Configuration",
|
|
66
70
|
typescript: "TypeScript Settings",
|
|
67
71
|
};
|
|
68
|
-
function resolveSkillsDocs(projectDir) {
|
|
69
|
-
|
|
72
|
+
function resolveSkillsDocs(projectDir, aiTools) {
|
|
73
|
+
const docFiles = [...new Set(aiTools.map(tool => AI_TOOL_DOCS[tool]))];
|
|
74
|
+
return docFiles
|
|
75
|
+
.map(fileName => path.join(projectDir, fileName))
|
|
76
|
+
.filter(existsSync);
|
|
70
77
|
}
|
|
71
78
|
function getEnabledFeatures(enabledFeatures) {
|
|
72
79
|
return Object.entries(enabledFeatures)
|
|
@@ -168,10 +175,10 @@ function upsertSkillSection(content, nextSection) {
|
|
|
168
175
|
return `${content.trimEnd()}\n\n${nextSection.trim()}\n`;
|
|
169
176
|
}
|
|
170
177
|
export async function runAiSkillsRule(context) {
|
|
171
|
-
const { projectDir, dryRun } = context;
|
|
172
|
-
const targetFiles = resolveSkillsDocs(projectDir);
|
|
178
|
+
const { projectDir, dryRun, aiTools } = context;
|
|
179
|
+
const targetFiles = resolveSkillsDocs(projectDir, aiTools);
|
|
173
180
|
if (targetFiles.length === 0) {
|
|
174
|
-
console.log("Skip ai-skills (
|
|
181
|
+
console.log("Skip ai-skills (selected AI docs not found)");
|
|
175
182
|
return;
|
|
176
183
|
}
|
|
177
184
|
const enabled = getEnabledFeatures(context.enabledFeatures);
|
|
@@ -53,6 +53,10 @@ function runHuskyInstallCommand(pm, projectDir) {
|
|
|
53
53
|
runCommand("pnpm exec husky", projectDir, false);
|
|
54
54
|
return;
|
|
55
55
|
}
|
|
56
|
+
if (pm === "bun") {
|
|
57
|
+
runCommand("bunx husky", projectDir, false);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
56
60
|
if (pm === "yarn") {
|
|
57
61
|
runCommand("yarn husky", projectDir, false);
|
|
58
62
|
return;
|
|
@@ -5,6 +5,10 @@ export function detectPackageManager(projectDir) {
|
|
|
5
5
|
if (existsSync(path.join(projectDir, "pnpm-lock.yaml"))) {
|
|
6
6
|
return "pnpm";
|
|
7
7
|
}
|
|
8
|
+
if (existsSync(path.join(projectDir, "bun.lockb")) ||
|
|
9
|
+
existsSync(path.join(projectDir, "bun.lock"))) {
|
|
10
|
+
return "bun";
|
|
11
|
+
}
|
|
8
12
|
if (existsSync(path.join(projectDir, "yarn.lock"))) {
|
|
9
13
|
return "yarn";
|
|
10
14
|
}
|
|
@@ -16,6 +20,8 @@ export function detectPackageManager(projectDir) {
|
|
|
16
20
|
export function getRunCommand(pm) {
|
|
17
21
|
if (pm === "pnpm")
|
|
18
22
|
return "pnpm";
|
|
23
|
+
if (pm === "bun")
|
|
24
|
+
return "bun run";
|
|
19
25
|
if (pm === "yarn")
|
|
20
26
|
return "yarn";
|
|
21
27
|
return "npm run";
|
|
@@ -32,6 +38,10 @@ export function runScript(pm, scriptName, cwd, dryRun = false) {
|
|
|
32
38
|
runCommand(`pnpm ${scriptName}`, cwd, dryRun);
|
|
33
39
|
return;
|
|
34
40
|
}
|
|
41
|
+
if (pm === "bun") {
|
|
42
|
+
runCommand(`bun run ${scriptName}`, cwd, dryRun);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
35
45
|
if (pm === "yarn") {
|
|
36
46
|
runCommand(`yarn ${scriptName}`, cwd, dryRun);
|
|
37
47
|
return;
|
|
@@ -46,6 +56,9 @@ export function installPackages(pm, projectDir, packages, isDev, dryRun = false)
|
|
|
46
56
|
if (pm === "pnpm") {
|
|
47
57
|
command = `pnpm add ${isDev ? "-D " : ""}${list}`;
|
|
48
58
|
}
|
|
59
|
+
else if (pm === "bun") {
|
|
60
|
+
command = `bun add ${isDev ? "-d " : ""}${list}`;
|
|
61
|
+
}
|
|
49
62
|
else if (pm === "yarn") {
|
|
50
63
|
command = `yarn add ${isDev ? "-D " : ""}${list}`;
|
|
51
64
|
}
|