@house365/comate 0.2.8 → 0.2.9

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.
@@ -236,6 +251,22 @@ export function banner(version, { ci = isCI() } = {}) {
236
251
  ].join('\n');
237
252
  }
238
253
 
254
+ // Major version of the bash that comate's scripts will actually run under, or
255
+ // null when there is no bash at all — that is doctor's diagnosis to deliver, not
256
+ // a reason to block the install. Windows resolves through findBash so a Git Bash
257
+ // install is not mistaken for the WSL launcher.
258
+ // Parsed from --version rather than `-c 'echo $BASH_VERSINFO'`: capture() runs
259
+ // through a shell on Windows, which swallows the variable before bash sees it.
260
+ export function bashVersion(probe = (bin) => capture(bin, ['--version'])) {
261
+ const bin = process.platform === 'win32' ? findBash() : 'bash';
262
+ if (!bin) return null;
263
+ const out = probe(bin);
264
+ if (out === null) return null;
265
+ // "GNU bash, version 5.2.26(1)-release ..." / macOS: "... version 3.2.57(1)"
266
+ const major = out.match(/version (\d+)\./)?.[1];
267
+ return major === undefined ? 0 : Number(major);
268
+ }
269
+
239
270
  // Every major CI sets CI=true; GITHUB_ACTIONS covers the Actions runner even in
240
271
  // the odd config that doesn't.
241
272
  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.9",
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"