@tyyyho/treg 0.1.19 → 0.1.20

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,5 @@
1
1
  import { stdin as input, stdout as output } from "node:process";
2
- import { createInterface } from "node:readline/promises";
2
+ import { clearScreenDown, cursorTo, emitKeypressEvents, moveCursor as moveTerminalCursor, } from "node:readline";
3
3
  const DEFAULT_AI_TOOLS = ["claude", "codex", "gemini"];
4
4
  const PACKAGE_MANAGER_CHOICES = [
5
5
  { value: "pnpm", label: "pnpm" },
@@ -22,10 +22,6 @@ const AI_TOOL_CHOICES = [
22
22
  { value: "gemini", label: "Gemini" },
23
23
  ];
24
24
  const FEATURE_CHOICES = [
25
- {
26
- value: "all",
27
- label: "all (lint, format, TypeScript, test, husky, AI skill guidance)",
28
- },
29
25
  { value: "lint", label: "lint" },
30
26
  { value: "format", label: "format" },
31
27
  { value: "typescript", label: "TypeScript" },
@@ -33,85 +29,30 @@ const FEATURE_CHOICES = [
33
29
  { value: "husky", label: "husky" },
34
30
  { value: "skills", label: "AI skill guidance" },
35
31
  ];
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
- }
32
+ function moveCursorIndex(currentIndex, delta, size) {
33
+ if (size <= 0) {
34
+ return 0;
52
35
  }
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
- };
36
+ return (currentIndex + delta + size) % size;
61
37
  }
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: [] };
38
+ function toggleSelectedValue(selectedValues, value) {
39
+ const next = new Set(selectedValues);
40
+ if (next.has(value)) {
41
+ next.delete(value);
42
+ return next;
69
43
  }
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] };
44
+ next.add(value);
45
+ return next;
46
+ }
47
+ function selectAllValues(choices) {
48
+ return new Set(choices.map(choice => choice.value));
49
+ }
50
+ function getSelectedValuesInOrder(choices, selectedValues) {
51
+ return choices
52
+ .filter(choice => selectedValues.has(choice.value))
53
+ .map(choice => choice.value);
101
54
  }
102
55
  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
56
  return {
116
57
  enabledFeatures: {
117
58
  lint: selected.includes("lint"),
@@ -123,14 +64,170 @@ function toFeatureSelection(selected) {
123
64
  skills: selected.includes("skills"),
124
65
  };
125
66
  }
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");
67
+ function renderFrame(lines, previousLineCount) {
68
+ if (previousLineCount > 0) {
69
+ moveTerminalCursor(output, 0, -previousLineCount);
70
+ cursorTo(output, 0);
71
+ clearScreenDown(output);
72
+ }
73
+ output.write(lines.join("\n"));
74
+ output.write("\n");
75
+ return lines.length;
76
+ }
77
+ function isConfirmKey(key) {
78
+ return key.name === "return" || key.name === "enter";
79
+ }
80
+ function isSelectAllKey(key) {
81
+ return key.name === "a" && !key.ctrl && !key.meta;
82
+ }
83
+ function ensureRawMode() {
84
+ emitKeypressEvents(input);
85
+ input.setRawMode(true);
86
+ input.resume();
87
+ output.write("\x1b[?25l");
88
+ return () => {
89
+ input.setRawMode(false);
90
+ output.write("\x1b[?25h");
91
+ };
92
+ }
93
+ function buildSingleSelectLines(title, choices, highlightedIndex) {
94
+ return [
95
+ title,
96
+ ...choices.map((choice, index) => {
97
+ const cursor = index === highlightedIndex ? ">" : " ";
98
+ return `${cursor} ${choice.label}`;
99
+ }),
100
+ "Use up/down arrows to move, Enter to confirm.",
101
+ ];
102
+ }
103
+ function buildMultiSelectLines(title, choices, selectedValues, highlightedIndex, allowSelectAll) {
104
+ const controls = allowSelectAll
105
+ ? "Use up/down arrows to move, Space to toggle, A to select all, Enter to confirm."
106
+ : "Use up/down arrows to move, Space to toggle, Enter to confirm.";
107
+ return [
108
+ title,
109
+ ...choices.map((choice, index) => {
110
+ const cursor = index === highlightedIndex ? ">" : " ";
111
+ const mark = selectedValues.has(choice.value) ? "x" : " ";
112
+ return `${cursor} [${mark}] ${choice.label}`;
113
+ }),
114
+ controls,
115
+ ];
116
+ }
117
+ async function promptSingleChoice(title, choices, defaultValue) {
118
+ const defaultIndex = choices.findIndex(choice => choice.value === defaultValue);
119
+ let highlightedIndex = defaultIndex >= 0 ? defaultIndex : 0;
120
+ let renderedLineCount = 0;
121
+ const restore = ensureRawMode();
122
+ try {
123
+ renderedLineCount = renderFrame(buildSingleSelectLines(title, choices, highlightedIndex), renderedLineCount);
124
+ return await new Promise((resolve, reject) => {
125
+ let settled = false;
126
+ const onKeypress = (_, key) => {
127
+ if (key.ctrl && key.name === "c") {
128
+ if (settled) {
129
+ return;
130
+ }
131
+ settled = true;
132
+ cleanup();
133
+ reject(new Error("Prompt cancelled by user"));
134
+ return;
135
+ }
136
+ if (key.name === "up") {
137
+ highlightedIndex = moveCursorIndex(highlightedIndex, -1, choices.length);
138
+ renderedLineCount = renderFrame(buildSingleSelectLines(title, choices, highlightedIndex), renderedLineCount);
139
+ return;
140
+ }
141
+ if (key.name === "down") {
142
+ highlightedIndex = moveCursorIndex(highlightedIndex, 1, choices.length);
143
+ renderedLineCount = renderFrame(buildSingleSelectLines(title, choices, highlightedIndex), renderedLineCount);
144
+ return;
145
+ }
146
+ if (isConfirmKey(key)) {
147
+ const selected = choices[highlightedIndex];
148
+ if (selected) {
149
+ if (settled) {
150
+ return;
151
+ }
152
+ settled = true;
153
+ cleanup();
154
+ resolve(selected.value);
155
+ }
156
+ }
157
+ };
158
+ const cleanup = () => {
159
+ input.off("keypress", onKeypress);
160
+ };
161
+ input.on("keypress", onKeypress);
162
+ });
163
+ }
164
+ finally {
165
+ restore();
166
+ output.write("\n");
167
+ }
168
+ }
169
+ async function promptMultiChoice(title, choices, options) {
170
+ let highlightedIndex = 0;
171
+ let selectedValues = new Set(options.defaultValues);
172
+ let renderedLineCount = 0;
173
+ const allowSelectAll = options.allowSelectAll ?? false;
174
+ const restore = ensureRawMode();
175
+ try {
176
+ renderedLineCount = renderFrame(buildMultiSelectLines(title, choices, selectedValues, highlightedIndex, allowSelectAll), renderedLineCount);
177
+ return await new Promise((resolve, reject) => {
178
+ let settled = false;
179
+ const onKeypress = (_, key) => {
180
+ if (key.ctrl && key.name === "c") {
181
+ if (settled) {
182
+ return;
183
+ }
184
+ settled = true;
185
+ cleanup();
186
+ reject(new Error("Prompt cancelled by user"));
187
+ return;
188
+ }
189
+ if (key.name === "up") {
190
+ highlightedIndex = moveCursorIndex(highlightedIndex, -1, choices.length);
191
+ renderedLineCount = renderFrame(buildMultiSelectLines(title, choices, selectedValues, highlightedIndex, allowSelectAll), renderedLineCount);
192
+ return;
193
+ }
194
+ if (key.name === "down") {
195
+ highlightedIndex = moveCursorIndex(highlightedIndex, 1, choices.length);
196
+ renderedLineCount = renderFrame(buildMultiSelectLines(title, choices, selectedValues, highlightedIndex, allowSelectAll), renderedLineCount);
197
+ return;
198
+ }
199
+ if (key.name === "space") {
200
+ const highlighted = choices[highlightedIndex];
201
+ if (!highlighted) {
202
+ return;
203
+ }
204
+ selectedValues = toggleSelectedValue(selectedValues, highlighted.value);
205
+ renderedLineCount = renderFrame(buildMultiSelectLines(title, choices, selectedValues, highlightedIndex, allowSelectAll), renderedLineCount);
206
+ return;
207
+ }
208
+ if (allowSelectAll && isSelectAllKey(key)) {
209
+ selectedValues = selectAllValues(choices);
210
+ renderedLineCount = renderFrame(buildMultiSelectLines(title, choices, selectedValues, highlightedIndex, allowSelectAll), renderedLineCount);
211
+ return;
212
+ }
213
+ if (isConfirmKey(key)) {
214
+ if (settled) {
215
+ return;
216
+ }
217
+ settled = true;
218
+ cleanup();
219
+ resolve(getSelectedValuesInOrder(choices, selectedValues));
220
+ }
221
+ };
222
+ const cleanup = () => {
223
+ input.off("keypress", onKeypress);
224
+ };
225
+ input.on("keypress", onKeypress);
226
+ });
227
+ }
228
+ finally {
229
+ restore();
230
+ output.write("\n");
134
231
  }
135
232
  }
136
233
  export async function collectInitPrompts(defaults) {
@@ -151,71 +248,67 @@ export async function collectInitPrompts(defaults) {
151
248
  aiTools: [...DEFAULT_AI_TOOLS],
152
249
  };
153
250
  }
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));
251
+ console.log("\nInit setup");
252
+ console.log("\n1) Package manager");
253
+ const pm = await promptSingleChoice("Select package manager:", PACKAGE_MANAGER_CHOICES, defaults.pm);
254
+ console.log("\n2) Features");
255
+ const featureAnswers = await promptMultiChoice("Select features:", FEATURE_CHOICES, {
256
+ defaultValues: FEATURE_CHOICES.map(choice => choice.value),
257
+ allowSelectAll: true,
258
+ });
259
+ const featureSelection = toFeatureSelection(featureAnswers);
260
+ let testRunner = defaults.testRunner;
261
+ const enabledFeatures = { ...featureSelection.enabledFeatures };
262
+ if (featureSelection.enabledFeatures.test) {
263
+ console.log("\n3) Test runner");
264
+ const selectedTestRunner = await promptSingleChoice("Select test runner:", TEST_RUNNER_CHOICES, defaults.testRunner);
265
+ if (selectedTestRunner === "skip") {
266
+ enabledFeatures.test = false;
267
+ console.log("Test feature disabled by selection: skip");
186
268
  }
187
269
  else {
188
- console.log("\n4) Formatter skipped (format feature not selected)");
270
+ testRunner = selectedTestRunner;
189
271
  }
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)");
272
+ }
273
+ else {
274
+ console.log("\n3) Test runner skipped (test feature not selected)");
275
+ }
276
+ let formatter = defaults.formatter;
277
+ if (featureSelection.enabledFeatures.format) {
278
+ console.log("\n4) Formatter");
279
+ formatter = await promptSingleChoice("Select formatter:", FORMATTER_CHOICES, defaults.formatter);
280
+ }
281
+ else {
282
+ console.log("\n4) Formatter skipped (format feature not selected)");
283
+ }
284
+ let aiTools = [];
285
+ let skills = featureSelection.skills;
286
+ if (skills) {
287
+ console.log("\n5) AI tools");
288
+ aiTools = await promptMultiChoice("Select AI tools:", AI_TOOL_CHOICES, {
289
+ defaultValues: DEFAULT_AI_TOOLS,
290
+ allowSelectAll: true,
291
+ });
292
+ if (aiTools.length === 0) {
293
+ skills = false;
203
294
  }
204
- return {
205
- pm,
206
- formatter,
207
- testRunner,
208
- enabledFeatures,
209
- skills,
210
- aiTools,
211
- };
212
295
  }
213
- finally {
214
- rl.close();
296
+ else {
297
+ console.log("\n5) AI tools skipped (AI skill guidance not selected)");
215
298
  }
299
+ return {
300
+ pm,
301
+ formatter,
302
+ testRunner,
303
+ enabledFeatures,
304
+ skills,
305
+ aiTools,
306
+ };
216
307
  }
217
308
  export const __testables__ = {
218
- parseSingleChoice,
219
- parseMultiChoice,
309
+ moveCursorIndex,
310
+ toggleSelectedValue,
311
+ selectAllValues,
312
+ getSelectedValuesInOrder,
220
313
  toFeatureSelection,
221
314
  };
@@ -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.20",
4
4
  "description": "CLI tool for initializing development conventions in existing projects.",
5
5
  "license": "MIT",
6
6
  "repository": {