6aspec 2.0.0-dev.2 → 2.0.0-dev.3

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 (2) hide show
  1. package/lib/cli.js +177 -44
  2. package/package.json +1 -1
package/lib/cli.js CHANGED
@@ -19,20 +19,58 @@ const TOOLS = [
19
19
  { id: 'claude', name: 'Claude Code', desc: 'Claude Code CLI 配置' },
20
20
  ];
21
21
 
22
- // ─── Welcome Screen ──────────────────────────────────────────────────
23
-
24
- function buildWelcomeScreen() {
25
- const logo = [
26
- `${CYAN} ████ ${RESET}`,
27
- `${CYAN} ░░ ░░ ${RESET}`,
28
- `${CYAN} ████ ${RESET}`,
29
- `${CYAN} ████ ${RESET}`,
30
- `${CYAN} ████ ${RESET}`,
31
- `${CYAN} ░░ ░░ ${RESET}`,
32
- `${CYAN} ████ ${RESET}`,
22
+ // Only the 6aspec-owned subdirectory is removed — user's own commands are untouched
23
+ const TOOL_DIRS = {
24
+ cursor: ['.cursor/commands/6aspec'],
25
+ claude: ['.claude/commands/6aspec'],
26
+ };
27
+
28
+ // ─── Welcome Screen with Animation ──────────────────────────────────
29
+
30
+ // Braille spinner frames
31
+ const SPINNER_CHARS = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
32
+
33
+ // 4-phase ornament pulse: dim → mid → bright → mid
34
+ const ORN_FRAMES = [
35
+ { ch: '░░', c: DIM + CYAN },
36
+ { ch: '▒▒', c: CYAN },
37
+ { ch: '██', c: BOLD + CYAN },
38
+ { ch: '▒▒', c: CYAN },
39
+ ];
40
+
41
+ // Logo: 18 lines tall to match right-side content height
42
+ // Visible width of each line = 20 chars (" ████ ")
43
+ function buildLogoLines(ornIdx, spinIdx) {
44
+ const { ch, c } = ORN_FRAMES[ornIdx % ORN_FRAMES.length];
45
+ const sp = SPINNER_CHARS[spinIdx % SPINNER_CHARS.length];
46
+ const B = BOLD + CYAN;
47
+ const R = RESET;
48
+ const _ = ' '; // 20-char blank line
49
+
50
+ return [
51
+ _,
52
+ ` ${B}████${R} `,
53
+ ` ${B}████${R} `,
54
+ ` ${c}${ch}${R} ${c}${ch}${R} `,
55
+ ` ${c}${ch}${R} ${c}${ch}${R} `,
56
+ ` ${B}████${R} `,
57
+ ` ${B}████${R} `,
58
+ ` ${B}████${R} `,
59
+ ` ${B}████${R} `,
60
+ ` ${c}${ch}${R} ${c}${ch}${R} `,
61
+ ` ${c}${ch}${R} ${c}${ch}${R} `,
62
+ ` ${B}████${R} `,
63
+ ` ${B}████${R} `,
64
+ _,
65
+ ` ${CYAN}${sp}${R} `,
66
+ _,
67
+ _,
68
+ _,
33
69
  ];
70
+ }
34
71
 
35
- const right = [
72
+ function buildRightLines() {
73
+ return [
36
74
  `${BOLD}${WHITE}Welcome to 6Aspec${RESET}`,
37
75
  `${DIM}A lightweight spec-driven framework${RESET}`,
38
76
  ``,
@@ -41,51 +79,89 @@ function buildWelcomeScreen() {
41
79
  ` ${GREEN}•${RESET} /6aspec:* slash commands`,
42
80
  ``,
43
81
  `${YELLOW}Quick start after setup:${RESET}`,
44
- ];
45
-
46
- const brownCmds = [
82
+ ``,
83
+ ` ${MAGENTA}Brownfield (existing project):${RESET}`,
47
84
  ` ${GREEN}/6aspec:brown:new${RESET} Create a change`,
48
85
  ` ${GREEN}/6aspec:brown:continue${RESET} Next artifact`,
49
86
  ` ${GREEN}/6aspec:brown:implement${RESET} Implement tasks`,
50
- ];
51
-
52
- const greenCmds = [
87
+ ``,
88
+ ` ${MAGENTA}Greenfield (new project):${RESET}`,
53
89
  ` ${GREEN}/6aspec:green:new${RESET} Create a module`,
54
90
  ` ${GREEN}/6aspec:green:continue${RESET} Next artifact`,
55
91
  ` ${GREEN}/6aspec:green:execute-task${RESET} Execute tasks`,
56
92
  ];
93
+ }
57
94
 
58
- const footer = `${DIM}Press Enter to select tools...${RESET}`;
59
-
95
+ function buildScreenLines(logoLines, rightLines) {
60
96
  const lines = [];
61
97
  lines.push('');
62
-
63
- const totalRight = [...right, ``, ` ${MAGENTA}Brownfield (existing project):${RESET}`, ...brownCmds, ``, ` ${MAGENTA}Greenfield (new project):${RESET}`, ...greenCmds];
64
-
65
- const maxLines = Math.max(logo.length, totalRight.length);
66
-
67
- for (let i = 0; i < maxLines; i++) {
68
- const l = i < logo.length ? logo[i] : ' ';
69
- const r = i < totalRight.length ? totalRight[i] : '';
98
+ const len = Math.max(logoLines.length, rightLines.length);
99
+ for (let i = 0; i < len; i++) {
100
+ const l = i < logoLines.length ? logoLines[i] : ' ';
101
+ const r = i < rightLines.length ? rightLines[i] : '';
70
102
  lines.push(` ${l} ${r}`);
71
103
  }
72
-
73
104
  lines.push('');
74
- lines.push(` ${' '.repeat(20)} ${footer}`);
105
+ lines.push(` ${' '} ${DIM}Press Enter to select tools...${RESET}`);
75
106
  lines.push('');
76
-
77
- return lines.join('\n');
107
+ return lines;
78
108
  }
79
109
 
80
- // ─── Interactive Tool Selection (pure readline, no deps) ────────────
110
+ // ─── Animated welcome: renders in-place until Enter is pressed ───────
81
111
 
82
- function waitForEnter() {
112
+ function waitForEnterAnimated() {
83
113
  return new Promise((resolve) => {
84
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
85
- rl.question('', () => {
86
- rl.close();
87
- resolve();
88
- });
114
+ const rightLines = buildRightLines();
115
+
116
+ let ornIdx = 0;
117
+ let spinIdx = 0;
118
+ let screenLineCount = 0;
119
+ let firstRender = true;
120
+
121
+ function render() {
122
+ const logoLines = buildLogoLines(ornIdx, spinIdx);
123
+ const screenLines = buildScreenLines(logoLines, rightLines);
124
+ const output = screenLines.join('\n') + '\n';
125
+
126
+ if (firstRender) {
127
+ process.stdout.write(output);
128
+ screenLineCount = screenLines.length;
129
+ firstRender = false;
130
+ } else {
131
+ // Move cursor up to start of screen, overwrite in-place
132
+ process.stdout.write(`\x1b[${screenLineCount}A${output}`);
133
+ }
134
+ }
135
+
136
+ render();
137
+
138
+ const interval = setInterval(() => {
139
+ ornIdx = (ornIdx + 1) % ORN_FRAMES.length;
140
+ spinIdx = (spinIdx + 1) % SPINNER_CHARS.length;
141
+ render();
142
+ }, 150);
143
+
144
+ const isRaw = process.stdin.isTTY;
145
+ if (isRaw) process.stdin.setRawMode(true);
146
+ process.stdin.resume();
147
+ process.stdin.setEncoding('utf8');
148
+
149
+ function onKey(key) {
150
+ if (key === '\r' || key === '\n' || key === ' ') {
151
+ clearInterval(interval);
152
+ process.stdin.removeListener('data', onKey);
153
+ if (isRaw) process.stdin.setRawMode(false);
154
+ process.stdin.pause();
155
+ // Clear welcome screen
156
+ process.stdout.write(`\x1b[${screenLineCount}A\x1b[J`);
157
+ resolve();
158
+ } else if (key === '\u0003') {
159
+ clearInterval(interval);
160
+ process.exit(0);
161
+ }
162
+ }
163
+
164
+ process.stdin.on('data', onKey);
89
165
  });
90
166
  }
91
167
 
@@ -115,8 +191,13 @@ function selectTools(preSelected) {
115
191
  TOOLS.forEach((tool, i) => {
116
192
  const check = selected[i] ? `${GREEN}◉${RESET}` : `○`;
117
193
  const pointer = i === cursor ? `${CYAN}❯${RESET}` : ' ';
118
- const locked = preSelected.includes(tool.id) && selected[i] ? ` ${DIM}(installed)${RESET}` : '';
119
- console.log(` ${pointer} ${check} ${tool.name} ${DIM}- ${tool.desc}${RESET}${locked}`);
194
+ let hint = '';
195
+ if (preSelected.includes(tool.id)) {
196
+ hint = selected[i]
197
+ ? ` ${DIM}(installed)${RESET}`
198
+ : ` ${YELLOW}(will remove)${RESET}`;
199
+ }
200
+ console.log(` ${pointer} ${check} ${tool.name} ${DIM}- ${tool.desc}${RESET}${hint}`);
120
201
  });
121
202
  console.log('');
122
203
  }
@@ -189,6 +270,43 @@ function writeConfig(targetDir, tools) {
189
270
  return config;
190
271
  }
191
272
 
273
+ // ─── Tool removal ───────────────────────────────────────────────────
274
+
275
+ function removeDirRecursive(dirPath) {
276
+ if (!fs.existsSync(dirPath)) return;
277
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
278
+ for (const entry of entries) {
279
+ const full = path.join(dirPath, entry.name);
280
+ if (entry.isDirectory()) removeDirRecursive(full);
281
+ else fs.unlinkSync(full);
282
+ }
283
+ fs.rmdirSync(dirPath);
284
+ }
285
+
286
+ function removeToolFiles(targetDir, tools) {
287
+ for (const tool of tools) {
288
+ const dirs = TOOL_DIRS[tool] || [];
289
+ for (const rel of dirs) {
290
+ const full = path.join(targetDir, rel);
291
+ if (fs.existsSync(full)) {
292
+ removeDirRecursive(full);
293
+ console.log(`${GREEN}[INFO]${RESET} 已删除 ${rel}`);
294
+ }
295
+ }
296
+ }
297
+ }
298
+
299
+ // Simple y/N prompt (readline, no raw mode needed)
300
+ function promptConfirm(question) {
301
+ return new Promise((resolve) => {
302
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
303
+ rl.question(`${question} ${DIM}[y/N]${RESET} `, (answer) => {
304
+ rl.close();
305
+ resolve(answer.trim().toLowerCase() === 'y');
306
+ });
307
+ });
308
+ }
309
+
192
310
  // ─── Parse non-interactive --tool flag ──────────────────────────────
193
311
 
194
312
  function parseToolFlag(args) {
@@ -221,8 +339,7 @@ async function init(args) {
221
339
  console.log(`\n${GREEN}[INFO]${RESET} 非交互模式:初始化 ${selectedTools.join(', ')}`);
222
340
  } else {
223
341
  // Interactive mode
224
- process.stdout.write(buildWelcomeScreen());
225
- await waitForEnter();
342
+ await waitForEnterAnimated();
226
343
  selectedTools = await selectTools(preSelected);
227
344
 
228
345
  if (selectedTools.length === 0) {
@@ -266,10 +383,26 @@ async function init(args) {
266
383
  }
267
384
  }
268
385
 
386
+ // Handle deselected tools (were installed, now unchecked)
387
+ const removedTools = preSelected.filter((t) => !selectedTools.includes(t));
388
+ if (removedTools.length > 0) {
389
+ console.log(`\n${YELLOW}[WARN]${RESET} 以下工具已取消选择: ${removedTools.join(', ')}`);
390
+ for (const tool of removedTools) {
391
+ const dirs = TOOL_DIRS[tool] || [];
392
+ dirs.forEach((d) => console.log(` ${DIM}${d}${RESET}`));
393
+ }
394
+ const confirm = await promptConfirm(' 是否同时删除这些文件?');
395
+ if (confirm) {
396
+ removeToolFiles(targetDir, removedTools);
397
+ } else {
398
+ console.log(`${DIM} 已跳过文件删除,文件保留在磁盘上。${RESET}`);
399
+ }
400
+ }
401
+
269
402
  // Save config
270
403
  const savedConfig = writeConfig(targetDir, selectedTools);
271
404
  console.log(`\n${GREEN}[SUCCESS]${RESET} 初始化完成!配置已保存到 ${CONFIG_FILE}`);
272
- console.log(`${DIM} 工具: ${selectedTools.join(', ')}${RESET}`);
405
+ console.log(`${DIM} 工具: ${selectedTools.length > 0 ? selectedTools.join(', ') : '(none)'}${RESET}`);
273
406
  console.log(`${DIM} 版本: ${savedConfig.version}${RESET}\n`);
274
407
  }
275
408
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "6aspec",
3
- "version": "2.0.0-dev.2",
3
+ "version": "2.0.0-dev.3",
4
4
  "description": "6Aspec - 轻量级 spec 驱动开发框架,支持 Cursor 和 Claude Code",
5
5
  "main": "lib/installer.js",
6
6
  "bin": {