@house365/comate 0.2.8 → 0.2.10
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 +88 -9
- package/bin/init.mjs +9 -3
- package/package.json +1 -1
- package/skills/comate/scripts/comate.sh +18 -0
package/bin/init-run.mjs
CHANGED
|
@@ -3,8 +3,10 @@ import { spawn } from 'node:child_process';
|
|
|
3
3
|
import { existsSync, readFileSync } from 'node:fs';
|
|
4
4
|
import { join, resolve } from 'node:path';
|
|
5
5
|
|
|
6
|
+
import { findBash } from './doctor.mjs';
|
|
6
7
|
import {
|
|
7
8
|
NPM_TIMEOUT_MS,
|
|
9
|
+
capture,
|
|
8
10
|
SKILLS_TIMEOUT_MS,
|
|
9
11
|
npmInvocation,
|
|
10
12
|
openSpecState,
|
|
@@ -55,6 +57,19 @@ export async function init(pkgRoot) {
|
|
|
55
57
|
return 1;
|
|
56
58
|
}
|
|
57
59
|
|
|
60
|
+
// comate's skills are bash scripts needing associative arrays (bash 4+), and
|
|
61
|
+
// macOS ships 3.2. Fail here rather than installing everything and letting the
|
|
62
|
+
// first /comate call die inside a sourced library.
|
|
63
|
+
const bash = bashVersion();
|
|
64
|
+
if (bash !== null && bash < 4) {
|
|
65
|
+
console.error(
|
|
66
|
+
`\ncomate 的脚本需要 bash >= 4,当前 ${bash === 0 ? '未知版本' : `bash ${bash}.x`}。\n` +
|
|
67
|
+
'macOS 自带 bash 3.2(授权原因),装一个新的:\n\n brew install bash\n\n' +
|
|
68
|
+
'装好后重跑 comate init。',
|
|
69
|
+
);
|
|
70
|
+
return 3;
|
|
71
|
+
}
|
|
72
|
+
|
|
58
73
|
const stopSpinner = startSpinner('Initializing…');
|
|
59
74
|
|
|
60
75
|
// 1. OpenSpec CLI — a PATH command, so always global.
|
|
@@ -220,22 +235,86 @@ export function banner(version, { ci = isCI() } = {}) {
|
|
|
220
235
|
'╚██████╗╚██████╔╝██║ ╚═╝ ██║██║ ██║ ██║ ███████╗',
|
|
221
236
|
' ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚══════╝',
|
|
222
237
|
];
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
238
|
+
// Trailing padding keeps the source rows aligned; strip it so a colour run
|
|
239
|
+
// doesn't sit behind spaces the terminal may highlight on select.
|
|
240
|
+
const trim = (line) => line.replace(/\s+$/, '');
|
|
241
|
+
// Centred under the art, not under the terminal: the wordmark is fixed-width,
|
|
242
|
+
// so centring against art[0] holds at any window size. Computed rather than
|
|
243
|
+
// hardcoded because the version string changes length on every release.
|
|
244
|
+
const tagline = `${tag} - AI coding almost there!`;
|
|
245
|
+
const indent = ' '.repeat(2 + Math.max(0, Math.round((art[0].length - tagline.length) / 2)));
|
|
246
|
+
if (process.env.NO_COLOR) {
|
|
247
|
+
return ['', ...art.map((l) => ` ${trim(l)}`), '', `${indent}${tagline}`, ''].join('\n');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// 24-bit SGR. Truecolor is not universal, but the two terminals that matter
|
|
251
|
+
// here (Windows Terminal, VS Code) both support it, and one that doesn't
|
|
252
|
+
// degrades to its nearest palette entry rather than breaking.
|
|
253
|
+
const sgr = ([r, g, b]) => `\x1B[38;2;${r};${g};${b}m`;
|
|
254
|
+
const RESET = '\x1B[0m';
|
|
255
|
+
|
|
256
|
+
// Polished chrome under a cyan→magenta rim light: [face, edge] per row. The
|
|
257
|
+
// ramp shifts hue rather than dropping brightness — every row stays high
|
|
258
|
+
// luminance, which is what keeps it reading as lit metal instead of fading
|
|
259
|
+
// out. An earlier darkening ramp went muddy across the bottom half.
|
|
260
|
+
const ramp = [
|
|
261
|
+
[[255, 255, 255], [150, 240, 255]],
|
|
262
|
+
[[214, 248, 255], [ 56, 214, 255]],
|
|
263
|
+
[[168, 214, 255], [ 92, 148, 255]],
|
|
264
|
+
[[186, 160, 255], [148, 100, 255]],
|
|
265
|
+
[[233, 150, 255], [200, 76, 255]],
|
|
266
|
+
[[255, 150, 224], [255, 72, 178]],
|
|
267
|
+
];
|
|
268
|
+
|
|
269
|
+
// Blocks are the lit face, box-drawing pieces are the bevelled edge. Two
|
|
270
|
+
// shades per row is the whole trick: it gives every glyph a rim, which is
|
|
271
|
+
// what separates brushed metal from a flat decal.
|
|
272
|
+
const emboss = (line, [face, edge]) => {
|
|
273
|
+
let out = '';
|
|
274
|
+
let cur = null;
|
|
275
|
+
for (const ch of line) {
|
|
276
|
+
const want = ch === ' ' ? null : ch === '█' ? face : edge;
|
|
277
|
+
if (want !== cur) {
|
|
278
|
+
out += want ? sgr(want) : RESET;
|
|
279
|
+
cur = want;
|
|
280
|
+
}
|
|
281
|
+
out += ch;
|
|
282
|
+
}
|
|
283
|
+
return out + RESET;
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
// The tagline picks up the ramp instead of going dim grey: the name in the
|
|
287
|
+
// white highlight so it stays the thing you read first, the strapline in the
|
|
288
|
+
// magenta rim so the line closes the gradient the art opened. Dropping \x1B[2m
|
|
289
|
+
// matters — dim on a saturated colour is what made this read as washed out.
|
|
290
|
+
const nameLit = `${sgr(ramp[0][0])}${tag}${RESET}`;
|
|
291
|
+
const strap = `${sgr(ramp[5][1])} - AI coding almost there!${RESET}`;
|
|
292
|
+
|
|
228
293
|
return [
|
|
229
294
|
'',
|
|
230
|
-
|
|
231
|
-
// reset doesn't sit behind spaces the terminal may highlight on select.
|
|
232
|
-
...art.map((line) => ` ${brand(line.replace(/\s+$/, ''))}`),
|
|
295
|
+
...art.map((line, i) => ` ${emboss(trim(line), ramp[i])}`),
|
|
233
296
|
'',
|
|
234
|
-
|
|
297
|
+
`${indent}${nameLit}${strap}`,
|
|
235
298
|
'',
|
|
236
299
|
].join('\n');
|
|
237
300
|
}
|
|
238
301
|
|
|
302
|
+
// Major version of the bash that comate's scripts will actually run under, or
|
|
303
|
+
// null when there is no bash at all — that is doctor's diagnosis to deliver, not
|
|
304
|
+
// a reason to block the install. Windows resolves through findBash so a Git Bash
|
|
305
|
+
// install is not mistaken for the WSL launcher.
|
|
306
|
+
// Parsed from --version rather than `-c 'echo $BASH_VERSINFO'`: capture() runs
|
|
307
|
+
// through a shell on Windows, which swallows the variable before bash sees it.
|
|
308
|
+
export function bashVersion(probe = (bin) => capture(bin, ['--version'])) {
|
|
309
|
+
const bin = process.platform === 'win32' ? findBash() : 'bash';
|
|
310
|
+
if (!bin) return null;
|
|
311
|
+
const out = probe(bin);
|
|
312
|
+
if (out === null) return null;
|
|
313
|
+
// "GNU bash, version 5.2.26(1)-release ..." / macOS: "... version 3.2.57(1)"
|
|
314
|
+
const major = out.match(/version (\d+)\./)?.[1];
|
|
315
|
+
return major === undefined ? 0 : Number(major);
|
|
316
|
+
}
|
|
317
|
+
|
|
239
318
|
// Every major CI sets CI=true; GITHUB_ACTIONS covers the Actions runner even in
|
|
240
319
|
// the odd config that doesn't.
|
|
241
320
|
function isCI() {
|
package/bin/init.mjs
CHANGED
|
@@ -98,12 +98,16 @@ function run({ command, args, shell }, timeout, { quiet = false } = {}) {
|
|
|
98
98
|
return { ok: true };
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
|
|
101
|
+
// shell defaults to off: passing args with shell:true is deprecated in Node 24
|
|
102
|
+
// and is an injection surface, since the args are concatenated rather than
|
|
103
|
+
// escaped. Callers that genuinely need cmd.exe resolution (a bare `openspec`
|
|
104
|
+
// that only exists as openspec.cmd) opt in explicitly.
|
|
105
|
+
function capture(command, args, { shell = false } = {}) {
|
|
102
106
|
const { status, stdout: out } = spawnSync(command, args, {
|
|
103
107
|
encoding: 'utf8',
|
|
104
108
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
105
109
|
timeout: 15_000,
|
|
106
|
-
shell:
|
|
110
|
+
...(shell ? { shell: true } : {}),
|
|
107
111
|
});
|
|
108
112
|
return status === 0 ? out.trim() : null;
|
|
109
113
|
}
|
|
@@ -138,7 +142,9 @@ function skillsInvocation(pkgRoot, args) {
|
|
|
138
142
|
}
|
|
139
143
|
|
|
140
144
|
function openSpecState() {
|
|
141
|
-
|
|
145
|
+
// shell:true on Windows: npm installs this as openspec.cmd, which cannot be
|
|
146
|
+
// spawned directly. bashVersion needs no shell — it has a real executable path.
|
|
147
|
+
const version = capture('openspec', ['--version'], { shell: process.platform === 'win32' });
|
|
142
148
|
if (version === null) return { status: 'missing' };
|
|
143
149
|
return { status: atLeast(version, MIN_OPENSPEC) ? 'ready' : 'outdated', version };
|
|
144
150
|
}
|
package/package.json
CHANGED
|
@@ -25,6 +25,24 @@
|
|
|
25
25
|
|
|
26
26
|
set -euo pipefail
|
|
27
27
|
|
|
28
|
+
# Must come before sourcing the lib. comate-lib.sh declares associative arrays
|
|
29
|
+
# at load time, which bash 3.2 (what macOS ships as /bin/bash) parses as an
|
|
30
|
+
# ordinary array assignment — the subscript is then evaluated as arithmetic and
|
|
31
|
+
# `set -u` aborts with "schema_version: unbound variable". doctor has its own
|
|
32
|
+
# bash>=4 check, but it never got to run: the source above it already died.
|
|
33
|
+
if [ "${BASH_VERSINFO[0]:-0}" -lt 4 ]; then
|
|
34
|
+
cat >&2 <<EOF
|
|
35
|
+
comate needs bash >= 4 (associative arrays), found ${BASH_VERSION:-unknown}.
|
|
36
|
+
|
|
37
|
+
macOS ships bash 3.2 for licensing reasons. Install a current one:
|
|
38
|
+
brew install bash
|
|
39
|
+
|
|
40
|
+
Then re-run through it, e.g.:
|
|
41
|
+
$(command -v brew >/dev/null 2>&1 && echo "\$(brew --prefix)/bin/bash" || echo /opt/homebrew/bin/bash) $0 $*
|
|
42
|
+
EOF
|
|
43
|
+
exit 3
|
|
44
|
+
fi
|
|
45
|
+
|
|
28
46
|
SCRIPT_DIR="$(cd "$(dirname "$(readlink -f "$0" 2>/dev/null || echo "$0")")" && pwd)"
|
|
29
47
|
# shellcheck source=comate-lib.sh
|
|
30
48
|
. "$SCRIPT_DIR/comate-lib.sh"
|