@house365/comate 0.2.5 → 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 +38 -13
- package/bin/spinner.mjs +28 -0
- package/package.json +1 -1
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,10 +55,7 @@ export async function init(pkgRoot) {
|
|
|
54
55
|
return 1;
|
|
55
56
|
}
|
|
56
57
|
|
|
57
|
-
|
|
58
|
-
// and through npm's .ps1 shim stdout is a pipe where \r leaves the padding
|
|
59
|
-
// visible — the failed cleanup looked worse than just keeping the line.
|
|
60
|
-
console.log('\n安装中…');
|
|
58
|
+
const stopSpinner = startSpinner('Initializing…');
|
|
61
59
|
|
|
62
60
|
// 1. OpenSpec CLI — a PATH command, so always global.
|
|
63
61
|
const os = openSpecState();
|
|
@@ -115,7 +113,7 @@ export async function init(pkgRoot) {
|
|
|
115
113
|
// phases read it, so an install without it cannot actually run. An existing
|
|
116
114
|
// one is left alone, which covers projects that already keep it elsewhere.
|
|
117
115
|
if (existsSync('openspec')) {
|
|
118
|
-
results.push(['openspec
|
|
116
|
+
results.push(['openspec/', 'skipped', '已存在']);
|
|
119
117
|
} else {
|
|
120
118
|
// --tools is not optional in practice: openspec derives the tool list by
|
|
121
119
|
// sniffing existing agent config dirs, so a fresh project (no .claude etc.)
|
|
@@ -131,16 +129,42 @@ export async function init(pkgRoot) {
|
|
|
131
129
|
NPM_TIMEOUT_MS,
|
|
132
130
|
{ quiet: true },
|
|
133
131
|
);
|
|
134
|
-
results.push(['openspec
|
|
132
|
+
results.push(['openspec/', r2.ok ? 'installed' : 'failed', r2.ok ? '' : r2.reason, r2.output]);
|
|
135
133
|
}
|
|
136
134
|
|
|
137
|
-
|
|
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
|
+
};
|
|
138
162
|
}
|
|
139
163
|
|
|
140
164
|
// Success is one screen: the wordmark and where things landed. The per-item
|
|
141
165
|
// breakdown only earns its space when something failed, and then it carries the
|
|
142
166
|
// reason — a bare "失败" would leave the user with nowhere to go.
|
|
143
|
-
function report(results
|
|
167
|
+
function report(results) {
|
|
144
168
|
const failed = results.filter(([, s]) => s === 'failed');
|
|
145
169
|
if (failed.length > 0) {
|
|
146
170
|
console.error('\n安装失败\n');
|
|
@@ -158,9 +182,7 @@ function report(results, { home, cwd }) {
|
|
|
158
182
|
}
|
|
159
183
|
|
|
160
184
|
console.log(banner(pkgVersion()));
|
|
161
|
-
console.log(
|
|
162
|
-
console.log(` openspec ${join(cwd, 'openspec')}`);
|
|
163
|
-
console.log('\n 输入 /comate 开始使用,comate doctor 可自检\n');
|
|
185
|
+
console.log(' Type /comate in your agent to start, or run comate doctor\n');
|
|
164
186
|
return 0;
|
|
165
187
|
}
|
|
166
188
|
|
|
@@ -198,13 +220,16 @@ export function banner(version, { ci = isCI() } = {}) {
|
|
|
198
220
|
'╚██████╗╚██████╔╝██║ ╚═╝ ██║██║ ██║ ██║ ███████╗',
|
|
199
221
|
' ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚══════╝',
|
|
200
222
|
];
|
|
201
|
-
|
|
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`);
|
|
202
227
|
const dim = (s) => (process.env.NO_COLOR ? s : `\x1B[2m${s}\x1B[0m`);
|
|
203
228
|
return [
|
|
204
229
|
'',
|
|
205
230
|
// Trailing padding keeps the source rows aligned; strip it so the colour
|
|
206
231
|
// reset doesn't sit behind spaces the terminal may highlight on select.
|
|
207
|
-
...art.map((line) => ` ${
|
|
232
|
+
...art.map((line) => ` ${brand(line.replace(/\s+$/, ''))}`),
|
|
208
233
|
'',
|
|
209
234
|
` ${dim(`${tag} - AI coding almost there!`)}`,
|
|
210
235
|
'',
|
package/bin/spinner.mjs
ADDED
|
@@ -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;
|