@tyyyho/treg 0.1.19 → 0.1.21

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.zh-hant.md CHANGED
@@ -29,12 +29,12 @@ npx @tyyyho/treg init
29
29
  執行 `init` 後,`treg` 會依序詢問:
30
30
 
31
31
  1. 套件管理器(`pnpm|npm|yarn|bun`)
32
- 2. 要加入的功能(預設勾選 `all`)
32
+ 2. 要加入的功能(可複選,預設全勾)
33
33
  3. 測試工具(僅在選到 `test` 時詢問,支援 `skip`)
34
34
  4. Formatter(僅在選到 `format` 時詢問)
35
35
  5. AI 工具(`Claude|Codex|Gemini` 可複選,僅在選到 AI skill guidance 時詢問)
36
36
 
37
- 預設 `all` 內容:
37
+ 預設勾選功能:
38
38
 
39
39
  - lint
40
40
  - format
@@ -1,5 +1,4 @@
1
1
  import { stdin as input, stdout as output } from "node:process";
2
- import { createInterface } from "node:readline/promises";
3
2
  const DEFAULT_AI_TOOLS = ["claude", "codex", "gemini"];
4
3
  const PACKAGE_MANAGER_CHOICES = [
5
4
  { value: "pnpm", label: "pnpm" },
@@ -22,10 +21,6 @@ const AI_TOOL_CHOICES = [
22
21
  { value: "gemini", label: "Gemini" },
23
22
  ];
24
23
  const FEATURE_CHOICES = [
25
- {
26
- value: "all",
27
- label: "all (lint, format, TypeScript, test, husky, AI skill guidance)",
28
- },
29
24
  { value: "lint", label: "lint" },
30
25
  { value: "format", label: "format" },
31
26
  { value: "typescript", label: "TypeScript" },
@@ -33,85 +28,8 @@ const FEATURE_CHOICES = [
33
28
  { value: "husky", label: "husky" },
34
29
  { value: "skills", label: "AI skill guidance" },
35
30
  ];
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
- }
31
+ let promptsModulePromise = null;
102
32
  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
33
  return {
116
34
  enabledFeatures: {
117
35
  lint: selected.includes("lint"),
@@ -123,15 +41,41 @@ function toFeatureSelection(selected) {
123
41
  skills: selected.includes("skills"),
124
42
  };
125
43
  }
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");
44
+ function mapChoiceOptions(choices) {
45
+ return choices.map(choice => ({ value: choice, label: choice.label }));
46
+ }
47
+ function unwrapPromptResult(value, prompts) {
48
+ if (prompts.isCancel(value)) {
49
+ prompts.cancel("Prompt cancelled by user");
50
+ throw new Error("Prompt cancelled by user");
51
+ }
52
+ return value;
53
+ }
54
+ async function getPrompts() {
55
+ if (!promptsModulePromise) {
56
+ promptsModulePromise = import("@clack/prompts");
134
57
  }
58
+ return promptsModulePromise;
59
+ }
60
+ async function promptSingleChoice(message, choices, defaultValue) {
61
+ const prompts = await getPrompts();
62
+ const defaultChoice = choices.find(choice => choice.value === defaultValue);
63
+ const options = {
64
+ message,
65
+ options: mapChoiceOptions(choices),
66
+ };
67
+ const result = await prompts.select(defaultChoice ? { ...options, initialValue: defaultChoice } : options);
68
+ return unwrapPromptResult(result, prompts).value;
69
+ }
70
+ async function promptMultiChoice(message, choices, defaultValues) {
71
+ const prompts = await getPrompts();
72
+ const result = await prompts.multiselect({
73
+ message,
74
+ options: mapChoiceOptions(choices),
75
+ initialValues: choices.filter(choice => defaultValues.includes(choice.value)),
76
+ required: false,
77
+ });
78
+ return unwrapPromptResult(result, prompts).map(choice => choice.value);
135
79
  }
136
80
  export async function collectInitPrompts(defaults) {
137
81
  if (!input.isTTY || !output.isTTY) {
@@ -151,71 +95,52 @@ export async function collectInitPrompts(defaults) {
151
95
  aiTools: [...DEFAULT_AI_TOOLS],
152
96
  };
153
97
  }
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
- }
98
+ console.log("\nInit setup");
99
+ const pm = await promptSingleChoice("1) Package manager", PACKAGE_MANAGER_CHOICES, defaults.pm);
100
+ const featureAnswers = await promptMultiChoice("2) Features", FEATURE_CHOICES, FEATURE_CHOICES.map(choice => choice.value));
101
+ const featureSelection = toFeatureSelection(featureAnswers);
102
+ let testRunner = defaults.testRunner;
103
+ const enabledFeatures = { ...featureSelection.enabledFeatures };
104
+ if (featureSelection.enabledFeatures.test) {
105
+ const selectedTestRunner = await promptSingleChoice("3) Test runner", TEST_RUNNER_CHOICES, defaults.testRunner);
106
+ if (selectedTestRunner === "skip") {
107
+ enabledFeatures.test = false;
108
+ console.log("Test feature disabled by selection: skip");
177
109
  }
178
110
  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));
111
+ testRunner = selectedTestRunner;
186
112
  }
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)");
113
+ }
114
+ else {
115
+ console.log("3) Test runner skipped (test feature not selected)");
116
+ }
117
+ let formatter = defaults.formatter;
118
+ if (featureSelection.enabledFeatures.format) {
119
+ formatter = await promptSingleChoice("4) Formatter", FORMATTER_CHOICES, defaults.formatter);
120
+ }
121
+ else {
122
+ console.log("4) Formatter skipped (format feature not selected)");
123
+ }
124
+ let aiTools = [];
125
+ let skills = featureSelection.skills;
126
+ if (skills) {
127
+ aiTools = await promptMultiChoice("5) AI tools", AI_TOOL_CHOICES, DEFAULT_AI_TOOLS);
128
+ if (aiTools.length === 0) {
129
+ skills = false;
203
130
  }
204
- return {
205
- pm,
206
- formatter,
207
- testRunner,
208
- enabledFeatures,
209
- skills,
210
- aiTools,
211
- };
212
131
  }
213
- finally {
214
- rl.close();
132
+ else {
133
+ console.log("5) AI tools skipped (AI skill guidance not selected)");
215
134
  }
135
+ return {
136
+ pm,
137
+ formatter,
138
+ testRunner,
139
+ enabledFeatures,
140
+ skills,
141
+ aiTools,
142
+ };
216
143
  }
217
144
  export const __testables__ = {
218
- parseSingleChoice,
219
- parseMultiChoice,
220
145
  toFeatureSelection,
221
146
  };
@@ -1,8 +1,6 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { promises as fs } from "node:fs";
3
3
  import path from "node:path";
4
- const START_MARKER = "<!-- treg:skills:start -->";
5
- const END_MARKER = "<!-- treg:skills:end -->";
6
4
  const SKILL_SECTION_HEADING = "## treg AI Skills";
7
5
  const SKILLS_BASE_DIR = "skills";
8
6
  const AI_TOOL_DOCS = {
@@ -155,12 +153,6 @@ function upsertSkillSection(content, nextSection) {
155
153
  const rebuilt = `${before}\n\n${nextSection.trim()}\n`;
156
154
  return after ? `${rebuilt}\n${after}\n` : `${rebuilt}`;
157
155
  };
158
- const start = content.indexOf(START_MARKER);
159
- const end = content.indexOf(END_MARKER);
160
- if (start !== -1 && end !== -1 && end > start) {
161
- const suffixStart = end + END_MARKER.length;
162
- return replaceSection(start, suffixStart);
163
- }
164
156
  const headingStart = content.indexOf(SKILL_SECTION_HEADING);
165
157
  if (headingStart !== -1) {
166
158
  const nextHeading = content.indexOf("\n## ", headingStart + 1);
@@ -172,10 +164,6 @@ function upsertSkillSection(content, nextSection) {
172
164
  }
173
165
  return `${content.trimEnd()}\n\n${nextSection.trim()}\n`;
174
166
  }
175
- function buildInitialAiDocContent(fileName) {
176
- const baseName = path.basename(fileName, path.extname(fileName));
177
- return `# ${baseName}\n`;
178
- }
179
167
  export async function runAiSkillsRule(context) {
180
168
  const { projectDir, dryRun, aiTools } = context;
181
169
  const targetFiles = resolveSkillsDocs(projectDir, aiTools);
@@ -189,9 +177,7 @@ export async function runAiSkillsRule(context) {
189
177
  continue;
190
178
  }
191
179
  const exists = existsSync(targetFile);
192
- const current = exists
193
- ? await fs.readFile(targetFile, "utf8")
194
- : buildInitialAiDocContent(targetFile);
180
+ const current = exists ? await fs.readFile(targetFile, "utf8") : "";
195
181
  const updated = upsertSkillSection(current, section);
196
182
  if (updated !== current) {
197
183
  await fs.mkdir(path.dirname(targetFile), { recursive: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tyyyho/treg",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
4
4
  "description": "CLI tool for initializing development conventions in existing projects.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -34,6 +34,7 @@
34
34
  "prepublishOnly": "pnpm format:check && pnpm lint:check && pnpm type:check && pnpm test && pnpm build"
35
35
  },
36
36
  "dependencies": {
37
+ "@clack/prompts": "^1.1.0",
37
38
  "mrm-core": "^7.1.22"
38
39
  },
39
40
  "devDependencies": {