@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 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
- // #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`);
227
- const dim = (s) => (process.env.NO_COLOR ? s : `\x1B[2m${s}\x1B[0m`);
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
- // Trailing padding keeps the source rows aligned; strip it so the colour
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
- ` ${dim(`${tag} - AI coding almost there!`)}`,
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
- function capture(command, args) {
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: process.platform === 'win32',
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
- const version = capture('openspec', ['--version']);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@house365/comate",
3
- "version": "0.2.8",
3
+ "version": "0.2.10",
4
4
  "description": "comate —— OpenSpec + Superpowers 规约驱动的 AI 开发编排器(Agent Skills)",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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"