@house365/comate 0.2.6 → 0.2.7

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/bin/init-run.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  // The `init` flow: install whatever the current project is missing.
2
+ import { spawn } from 'node:child_process';
2
3
  import { existsSync, readFileSync } from 'node:fs';
3
4
  import { join, resolve } from 'node:path';
4
5
 
@@ -54,19 +55,13 @@ export async function init(pkgRoot) {
54
55
  return 1;
55
56
  }
56
57
 
57
- // ponytail: printed steps, not a \r spinner. npm's Windows global install
58
- // routes `comate` through a comate.ps1 shim that makes stdout a pipe, where
59
- // \r does not overwrite — every frame would stay on screen. A line per step
60
- // shows progress in both a terminal and a pipe.
61
- console.log('\nInitializing…');
62
- const step = (name) => process.stdout.write(` ${name}\n`);
58
+ const stopSpinner = startSpinner('Initializing…');
63
59
 
64
60
  // 1. OpenSpec CLI — a PATH command, so always global.
65
61
  const os = openSpecState();
66
62
  if (os.status === 'ready') {
67
63
  results.push(['OpenSpec CLI', 'skipped', `已安装 ${os.version}`]);
68
64
  } else {
69
- step('OpenSpec CLI');
70
65
  const r = run(
71
66
  npmInvocation(['install', '-g', '@fission-ai/openspec@latest']),
72
67
  NPM_TIMEOUT_MS,
@@ -89,7 +84,6 @@ export async function init(pkgRoot) {
89
84
  if (superpowersInstalled('global') || superpowersInstalled('project')) {
90
85
  results.push(['Superpowers', 'skipped', '已安装']);
91
86
  } else {
92
- step('Superpowers');
93
87
  const r = run(
94
88
  skillsInvocation(pkgRoot, ['add', 'obra/superpowers', '-g', '-y']),
95
89
  SKILLS_TIMEOUT_MS,
@@ -106,7 +100,6 @@ export async function init(pkgRoot) {
106
100
  // 3. comate itself. The skills CLI reports per-agent failures without ever
107
101
  // setting a non-zero exit code, and we swallow its output — so a zero status
108
102
  // is not evidence the skill landed. Check the file.
109
- step('comate skills');
110
103
  const comateArgs = ['add', pkgRoot, '--skill', '*', '-g', '-y'];
111
104
  const r = run(skillsInvocation(pkgRoot, comateArgs), SKILLS_TIMEOUT_MS, { quiet: true });
112
105
  const landed = existsSync(join(home, '.agents', 'skills', 'comate', 'SKILL.md'));
@@ -122,7 +115,6 @@ export async function init(pkgRoot) {
122
115
  if (existsSync('openspec')) {
123
116
  results.push(['openspec/', 'skipped', '已存在']);
124
117
  } else {
125
- step('openspec/ structure');
126
118
  // --tools is not optional in practice: openspec derives the tool list by
127
119
  // sniffing existing agent config dirs, so a fresh project (no .claude etc.)
128
120
  // aborts with "No tools detected and no --tools flag provided". comate's
@@ -140,13 +132,39 @@ export async function init(pkgRoot) {
140
132
  results.push(['openspec/', r2.ok ? 'installed' : 'failed', r2.ok ? '' : r2.reason, r2.output]);
141
133
  }
142
134
 
143
- return report(results, { home, cwd });
135
+ stopSpinner();
136
+ return report(results);
137
+ }
138
+
139
+ // The spinner runs in its own process because every step above is a synchronous
140
+ // spawnSync — an interval on this thread would never fire. In a pipe \r does not
141
+ // overwrite, so animating there would leave every frame on screen; print one
142
+ // static line instead.
143
+ function startSpinner(label) {
144
+ if (!process.stdout.isTTY) {
145
+ console.log(`\n${label}`);
146
+ return () => {};
147
+ }
148
+
149
+ const child = spawn(process.execPath, [join(import.meta.dirname, 'spinner.mjs'), label], {
150
+ stdio: ['ignore', 'inherit', 'ignore'],
151
+ });
152
+ let stopped = false;
153
+ return () => {
154
+ if (stopped) return;
155
+ stopped = true;
156
+ child.kill();
157
+ // The parent erases the line, not the child: on Windows SIGTERM is not a
158
+ // real signal — Node terminates the process outright, so a handler in the
159
+ // child never runs and its last frame would sit under the logo.
160
+ process.stdout.write(`\r${' '.repeat(label.length + 6)}\r`);
161
+ };
144
162
  }
145
163
 
146
164
  // Success is one screen: the wordmark and where things landed. The per-item
147
165
  // breakdown only earns its space when something failed, and then it carries the
148
166
  // reason — a bare "失败" would leave the user with nowhere to go.
149
- function report(results, { home, cwd }) {
167
+ function report(results) {
150
168
  const failed = results.filter(([, s]) => s === 'failed');
151
169
  if (failed.length > 0) {
152
170
  console.error('\n安装失败\n');
@@ -164,9 +182,7 @@ function report(results, { home, cwd }) {
164
182
  }
165
183
 
166
184
  console.log(banner(pkgVersion()));
167
- console.log(` skills ${join(home, '.agents', 'skills')}`);
168
- console.log(` openspec ${join(cwd, 'openspec')}`);
169
- console.log('\n 输入 /comate 开始使用,comate doctor 可自检\n');
185
+ console.log(' Type /comate in your agent to start, or run comate doctor\n');
170
186
  return 0;
171
187
  }
172
188
 
@@ -204,13 +220,16 @@ export function banner(version, { ci = isCI() } = {}) {
204
220
  '╚██████╗╚██████╔╝██║ ╚═╝ ██║██║ ██║ ██║ ███████╗',
205
221
  ' ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚══════╝',
206
222
  ];
207
- const cyan = (s) => (process.env.NO_COLOR ? s : `\x1B[36m${s}\x1B[0m`);
223
+ // #ed6d00 as a 24-bit SGR sequence. Truecolor is not universal, but the two
224
+ // terminals that matter here (Windows Terminal, VS Code) both support it, and
225
+ // one that doesn't degrades to its nearest palette entry rather than breaking.
226
+ const brand = (s) => (process.env.NO_COLOR ? s : `\x1B[38;2;237;109;0m${s}\x1B[0m`);
208
227
  const dim = (s) => (process.env.NO_COLOR ? s : `\x1B[2m${s}\x1B[0m`);
209
228
  return [
210
229
  '',
211
230
  // Trailing padding keeps the source rows aligned; strip it so the colour
212
231
  // reset doesn't sit behind spaces the terminal may highlight on select.
213
- ...art.map((line) => ` ${cyan(line.replace(/\s+$/, ''))}`),
232
+ ...art.map((line) => ` ${brand(line.replace(/\s+$/, ''))}`),
214
233
  '',
215
234
  ` ${dim(`${tag} - AI coding almost there!`)}`,
216
235
  '',
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+ // The init spinner, as its own process.
3
+ //
4
+ // It cannot run on init's main thread: every install step is a synchronous
5
+ // spawnSync, which blocks the event loop, so an interval would not fire and the
6
+ // frame would sit frozen for the whole install — worse than plain text.
7
+ //
8
+ // Animating means redrawing with \r, which only overwrites on a real terminal.
9
+ // In a pipe every frame would stay on screen, so there the parent prints a
10
+ // static line instead and never starts this process.
11
+ const FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
12
+ const INTERVAL_MS = 80;
13
+
14
+ const label = process.argv[2] ?? 'Initializing…';
15
+ let i = 0;
16
+
17
+ process.stdout.write('\n');
18
+ const timer = setInterval(() => {
19
+ process.stdout.write(`\r ${FRAMES[i++ % FRAMES.length]} ${label}`);
20
+ }, INTERVAL_MS);
21
+
22
+ // No cleanup handler here on purpose: on Windows SIGTERM is not a real signal,
23
+ // Node terminates the process outright and a handler would never run. The parent
24
+ // erases the line after killing us, which works on every platform.
25
+ //
26
+ // timer is the only thing holding the loop open, so nothing else is needed to
27
+ // keep animating until that kill arrives.
28
+ void timer;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@house365/comate",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "description": "comate —— OpenSpec + Superpowers 规约驱动的 AI 开发编排器(Agent Skills)",
5
5
  "type": "module",
6
6
  "license": "MIT",