@cocorograph/hub-agent 0.5.18 → 0.5.20
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/package.json +1 -1
- package/scripts/install.sh +131 -9
- package/src/main.mjs +28 -2
- package/src/service-install.mjs +52 -2
package/package.json
CHANGED
package/scripts/install.sh
CHANGED
|
@@ -10,16 +10,20 @@
|
|
|
10
10
|
#
|
|
11
11
|
# 内容:
|
|
12
12
|
# 1. macOS なら Homebrew (なければ install)
|
|
13
|
+
# - 管理者権限が無い環境では自動で $HOME/homebrew にユーザーローカル
|
|
14
|
+
# インストールに切り替わる (HUB_AGENT_USER_BREW=1 で強制も可)
|
|
13
15
|
# 2. tmux + node (なければ install via brew / apt)
|
|
14
16
|
# 3. npm i -g @cocorograph/hub-agent
|
|
15
|
-
# 4.
|
|
16
|
-
# 5. hub-agent
|
|
17
|
+
# 4. npm i -g @anthropic-ai/claude-code (既存があれば upgrade のみ)
|
|
18
|
+
# 5. HUB_AGENT_TOKEN が設定されていれば hub-agent enroll を自動実行
|
|
19
|
+
# 6. hub-agent install-service で OS サービス化
|
|
17
20
|
# =============================================================================
|
|
18
21
|
|
|
19
22
|
set -euo pipefail
|
|
20
23
|
|
|
21
24
|
NODE_MIN_MAJOR=20
|
|
22
25
|
PACKAGE_NAME="@cocorograph/hub-agent"
|
|
26
|
+
CLAUDE_CODE_PACKAGE="@anthropic-ai/claude-code"
|
|
23
27
|
|
|
24
28
|
color_step() { printf "\033[1;34m==> %s\033[0m\n" "$1"; }
|
|
25
29
|
color_ok() { printf "\033[1;32m✓ %s\033[0m\n" "$1"; }
|
|
@@ -32,20 +36,39 @@ ensure_brew() {
|
|
|
32
36
|
if [[ "$(uname)" != "Darwin" ]]; then return 0; fi
|
|
33
37
|
if present brew; then
|
|
34
38
|
color_ok "brew already installed"
|
|
39
|
+
persist_brew_shellenv
|
|
40
|
+
return 0
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
# 管理者権限の有無を判定して user-mode に切り替える。
|
|
44
|
+
# - 環境変数 HUB_AGENT_USER_BREW=1 で明示強制
|
|
45
|
+
# - sudo がパスワードなしで通らない = 管理者ではない可能性が高いと判定
|
|
46
|
+
# user-mode では $HOME/homebrew にユーザーローカルインストール(sudo 不要)。
|
|
47
|
+
local use_user_mode=0
|
|
48
|
+
if [[ "${HUB_AGENT_USER_BREW:-0}" == "1" ]]; then
|
|
49
|
+
use_user_mode=1
|
|
50
|
+
color_step "HUB_AGENT_USER_BREW=1 → ユーザーローカル Homebrew を使用"
|
|
51
|
+
elif ! sudo -n true 2>/dev/null; then
|
|
52
|
+
color_warn "管理者権限なしと判定 → \$HOME/homebrew にユーザーローカル Homebrew をインストール"
|
|
53
|
+
use_user_mode=1
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
if (( use_user_mode == 1 )); then
|
|
57
|
+
color_step "Homebrew をユーザーローカルインストール (\$HOME/homebrew)"
|
|
58
|
+
mkdir -p "$HOME/homebrew"
|
|
59
|
+
curl -L https://github.com/Homebrew/brew/tarball/master | tar xz --strip 1 -C "$HOME/homebrew"
|
|
60
|
+
export PATH="$HOME/homebrew/bin:$PATH"
|
|
61
|
+
persist_user_brew_shellenv
|
|
35
62
|
else
|
|
36
|
-
color_step "Homebrew
|
|
63
|
+
color_step "Homebrew をシステムインストール"
|
|
37
64
|
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
|
38
65
|
# Apple Silicon の brew はデフォルト PATH に入らないので追加
|
|
39
66
|
if [[ -d /opt/homebrew/bin ]]; then
|
|
40
67
|
export PATH="/opt/homebrew/bin:$PATH"
|
|
41
68
|
fi
|
|
42
|
-
|
|
69
|
+
persist_brew_shellenv
|
|
43
70
|
fi
|
|
44
|
-
|
|
45
|
-
# `~/.zprofile` / `~/.bash_profile` に `brew shellenv` の eval を **永続化** する。
|
|
46
|
-
# これを入れないと install.sh 終了後の新規ターミナルから `npm` / `node` /
|
|
47
|
-
# `hub-agent` が `command not found` になる (葛西氏 Mac で 0.5.11 リリース直後に踏んだ)。
|
|
48
|
-
persist_brew_shellenv
|
|
71
|
+
present brew || { color_err "brew install failed"; exit 1; }
|
|
49
72
|
}
|
|
50
73
|
|
|
51
74
|
# Apple Silicon Mac で brew のパスを zsh / bash 永続化する。既に追記済みなら no-op。
|
|
@@ -80,6 +103,68 @@ persist_brew_shellenv() {
|
|
|
80
103
|
done
|
|
81
104
|
}
|
|
82
105
|
|
|
106
|
+
# user-mode($HOME/homebrew)用の shellenv 永続化。
|
|
107
|
+
# system 版 persist_brew_shellenv と同じ仕組みだが対象 brew が $HOME 配下。
|
|
108
|
+
persist_user_brew_shellenv() {
|
|
109
|
+
local brew_bin="$HOME/homebrew/bin/brew"
|
|
110
|
+
[[ -x "$brew_bin" ]] || return 0
|
|
111
|
+
local snippet="eval \"\$(${brew_bin} shellenv)\""
|
|
112
|
+
local marker="# >>> hub-agent: brew shellenv (user-local) >>>"
|
|
113
|
+
local end_marker="# <<< hub-agent: brew shellenv <<<"
|
|
114
|
+
local target
|
|
115
|
+
for target in "$HOME/.zprofile" "$HOME/.bash_profile"; do
|
|
116
|
+
if [[ -f "$target" ]] && grep -Fq "$snippet" "$target"; then
|
|
117
|
+
color_ok "user-local brew shellenv は既に $target にあります"
|
|
118
|
+
continue
|
|
119
|
+
fi
|
|
120
|
+
color_step "$target に user-local brew shellenv を追記"
|
|
121
|
+
{
|
|
122
|
+
printf '\n%s\n' "$marker"
|
|
123
|
+
printf '%s\n' "$snippet"
|
|
124
|
+
printf '%s\n' "$end_marker"
|
|
125
|
+
} >> "$target"
|
|
126
|
+
color_ok "$target に追記しました (新規シェルから有効)"
|
|
127
|
+
done
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
# user-mode の場合、npm の global prefix を $HOME/.npm-global に切り替える。
|
|
131
|
+
# brew が $HOME/homebrew にいるとき、その prefix への書き込み権限はあるものの、
|
|
132
|
+
# 後で system 版 node / npm に切り替わる可能性も考えて user 領域に逃がしておく。
|
|
133
|
+
# system mode(/opt/homebrew 等)では何もしない(既存挙動を温存)。
|
|
134
|
+
ensure_npm_user_prefix() {
|
|
135
|
+
[[ "$(uname)" != "Darwin" ]] && return 0
|
|
136
|
+
[[ -d "$HOME/homebrew" ]] || return 0 # user-mode 検知
|
|
137
|
+
|
|
138
|
+
# 既に $HOME/.npm-global が prefix なら何もしない
|
|
139
|
+
local cur_prefix
|
|
140
|
+
cur_prefix="$(npm config get prefix 2>/dev/null || echo '')"
|
|
141
|
+
if [[ "$cur_prefix" == "$HOME/.npm-global" ]]; then
|
|
142
|
+
color_ok "npm global prefix は既に $HOME/.npm-global"
|
|
143
|
+
return 0
|
|
144
|
+
fi
|
|
145
|
+
|
|
146
|
+
color_step "npm global prefix を \$HOME/.npm-global に切替 (user-mode)"
|
|
147
|
+
mkdir -p "$HOME/.npm-global"
|
|
148
|
+
npm config set prefix "$HOME/.npm-global"
|
|
149
|
+
export PATH="$HOME/.npm-global/bin:$PATH"
|
|
150
|
+
|
|
151
|
+
local snippet='export PATH="$HOME/.npm-global/bin:$PATH"'
|
|
152
|
+
local marker="# >>> hub-agent: npm-global PATH (user-mode) >>>"
|
|
153
|
+
local end_marker="# <<< hub-agent: npm-global PATH <<<"
|
|
154
|
+
local target
|
|
155
|
+
for target in "$HOME/.zprofile" "$HOME/.bash_profile"; do
|
|
156
|
+
if [[ -f "$target" ]] && grep -Fq "$snippet" "$target"; then
|
|
157
|
+
continue
|
|
158
|
+
fi
|
|
159
|
+
{
|
|
160
|
+
printf '\n%s\n' "$marker"
|
|
161
|
+
printf '%s\n' "$snippet"
|
|
162
|
+
printf '%s\n' "$end_marker"
|
|
163
|
+
} >> "$target"
|
|
164
|
+
color_ok "$target に npm-global PATH を追記"
|
|
165
|
+
done
|
|
166
|
+
}
|
|
167
|
+
|
|
83
168
|
ensure_pkg() {
|
|
84
169
|
local cmd="$1"
|
|
85
170
|
local brew_pkg="$2"
|
|
@@ -134,6 +219,26 @@ ensure_global_install() {
|
|
|
134
219
|
color_ok "hub-agent $(hub-agent --version)"
|
|
135
220
|
}
|
|
136
221
|
|
|
222
|
+
# Claude Code CLI を同梱でセットアップ。Cockpit 側から claude を呼び出すので、
|
|
223
|
+
# 同じ npm-global prefix に入れておくとパス解決が一貫する。
|
|
224
|
+
# 既存があれば upgrade のみ(破壊しない)。
|
|
225
|
+
ensure_claude_code() {
|
|
226
|
+
if present claude; then
|
|
227
|
+
local cur
|
|
228
|
+
cur=$(claude --version 2>/dev/null || echo "unknown")
|
|
229
|
+
color_step "Claude Code (現在 $cur) を最新版にアップデート"
|
|
230
|
+
npm install -g "$CLAUDE_CODE_PACKAGE" || color_warn "Claude Code upgrade に失敗 (既存版で継続)"
|
|
231
|
+
else
|
|
232
|
+
color_step "$CLAUDE_CODE_PACKAGE を install"
|
|
233
|
+
npm install -g "$CLAUDE_CODE_PACKAGE"
|
|
234
|
+
fi
|
|
235
|
+
if present claude; then
|
|
236
|
+
color_ok "claude $(claude --version 2>/dev/null || echo 'installed')"
|
|
237
|
+
else
|
|
238
|
+
color_warn "claude コマンドが PATH に見つかりません。新規シェルで再確認してください"
|
|
239
|
+
fi
|
|
240
|
+
}
|
|
241
|
+
|
|
137
242
|
do_enroll() {
|
|
138
243
|
if [[ -z "${HUB_AGENT_TOKEN:-}" ]]; then
|
|
139
244
|
color_warn "HUB_AGENT_TOKEN が未設定。enroll は skip します"
|
|
@@ -154,17 +259,34 @@ do_install_service() {
|
|
|
154
259
|
color_ok "install-service 完了。ログ: ~/.hub/agent.log"
|
|
155
260
|
}
|
|
156
261
|
|
|
262
|
+
macos_perm_guidance() {
|
|
263
|
+
[[ "$(uname)" != "Darwin" ]] && return 0
|
|
264
|
+
cat <<'EOF'
|
|
265
|
+
|
|
266
|
+
📣 macOS の権限ダイアログについて
|
|
267
|
+
hub-agent は tmux/pty を中継するため、初回起動時に macOS から
|
|
268
|
+
「node がローカルネットワーク上の機器を検出することを求めています」
|
|
269
|
+
「node がアクセシビリティを制御することを求めています」
|
|
270
|
+
などのダイアログが出る場合があります。すべて「許可」を選んでください。
|
|
271
|
+
許可は「システム設定 > プライバシーとセキュリティ」から後で変更できます。
|
|
272
|
+
|
|
273
|
+
EOF
|
|
274
|
+
}
|
|
275
|
+
|
|
157
276
|
main() {
|
|
158
277
|
color_step "hub-agent ワンライナーセットアップを開始"
|
|
159
278
|
ensure_brew
|
|
160
279
|
ensure_pkg tmux tmux tmux
|
|
161
280
|
ensure_node_version
|
|
281
|
+
ensure_npm_user_prefix
|
|
162
282
|
ensure_global_install
|
|
283
|
+
ensure_claude_code
|
|
163
284
|
do_enroll
|
|
164
285
|
do_install_service
|
|
165
286
|
echo ""
|
|
166
287
|
color_ok "セットアップ完了。Hub UI で online 表示を確認してください"
|
|
167
288
|
echo " https://hub.cocorograph.com/user/cockpit/agents"
|
|
289
|
+
macos_perm_guidance
|
|
168
290
|
}
|
|
169
291
|
|
|
170
292
|
main "$@"
|
package/src/main.mjs
CHANGED
|
@@ -137,8 +137,34 @@ export async function startDaemon({ version, ptyModule } = {}) {
|
|
|
137
137
|
client.send({ type: "pty.exit", stream_id, code })
|
|
138
138
|
})
|
|
139
139
|
|
|
140
|
-
|
|
141
|
-
|
|
140
|
+
// Hub からのメッセージ dispatch は **直列実行** する。
|
|
141
|
+
//
|
|
142
|
+
// `EventEmitter.emit` は async リスナーの完了を待たないため、`async (msg) =>
|
|
143
|
+
// await dispatch(msg, ...)` の形だと複数 WS メッセージが連続して届いたとき
|
|
144
|
+
// それぞれの dispatch が並列実行される。`tmux.exec` の `await execTmux`
|
|
145
|
+
// (subprocess 待ち) 中に後続 `pty.data` の同期 `ptyBridge.write` が先行する
|
|
146
|
+
// と、ブラウザ→Hub では順序が保証されていても agent 内部で逆転する。
|
|
147
|
+
//
|
|
148
|
+
// 典型的な被害: cockpit ChatInput の `await onCancelTmuxMode()` → paste
|
|
149
|
+
// の流れで、ブラウザ側が cancel の応答を待ってから paste を送っても、
|
|
150
|
+
// 別経路の `tmux.exec` (state loop の polling 等) で agent が busy だと、
|
|
151
|
+
// cancel の execTmux が完了する前に paste が ptyBridge.write される。
|
|
152
|
+
// 結果として paste が tmux 内部状態 (copy mode 等) を抜ける前に届き、
|
|
153
|
+
// claude TUI に正しく入らない事象が散発する。
|
|
154
|
+
//
|
|
155
|
+
// Promise chain で前の dispatch 完了を待ってから次を始めることで、WS
|
|
156
|
+
// 受信順 = pane 反映順を保証する。pty 出力 (agent → browser) は別経路
|
|
157
|
+
// (`ptyBridge.on("output")`) なのでこの直列化の影響を受けない。
|
|
158
|
+
let dispatchChain = Promise.resolve()
|
|
159
|
+
client.on("message", (msg) => {
|
|
160
|
+
dispatchChain = dispatchChain
|
|
161
|
+
.then(() => dispatch(msg, { ...ctx, client, ptyBridge }))
|
|
162
|
+
.catch((err) => {
|
|
163
|
+
logger.error(
|
|
164
|
+
{ err: err.message, type: msg?.type },
|
|
165
|
+
"dispatch threw in serial queue",
|
|
166
|
+
)
|
|
167
|
+
})
|
|
142
168
|
})
|
|
143
169
|
|
|
144
170
|
client.connect()
|
package/src/service-install.mjs
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* テンプレ内の __HUB_AGENT_BIN__ / __HOME__ / __PATH__ を実行時に置換する。
|
|
10
10
|
* 仕様書: ナレッジ/インフラ/cockpit-hub-hosted-integration-spec (id=6080)
|
|
11
11
|
*/
|
|
12
|
-
import { promises as fs } from "node:fs"
|
|
12
|
+
import { promises as fs, existsSync, realpathSync } from "node:fs"
|
|
13
13
|
import os from "node:os"
|
|
14
14
|
import path from "node:path"
|
|
15
15
|
import { spawnSync } from "node:child_process"
|
|
@@ -63,10 +63,59 @@ function run(cmd, args, opts = {}) {
|
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
/**
|
|
67
|
+
* `which hub-agent` の結果を、launchd / systemd から長期間 exec 可能な
|
|
68
|
+
* 安定パスに正規化する。pure 関数。`opts` で fs を差し替えてテスト可能。
|
|
69
|
+
*
|
|
70
|
+
* 背景: fnm 利用者の `which hub-agent` は現在のシェル専用の一時パス
|
|
71
|
+
* (`~/.local/state/fnm_multishells/<PID>_<ts>/bin/hub-agent`) を返す。
|
|
72
|
+
* それを plist に書き込むと、install-service を叩いたシェル終了後に
|
|
73
|
+
* symlink が消えて launchd が exec できず、agent が無音で停止する事故が
|
|
74
|
+
* 発生する (2026-05-19, シェル PID 4683 で install したまま放置されて
|
|
75
|
+
* 検知が遅れた)。
|
|
76
|
+
*
|
|
77
|
+
* 優先順:
|
|
78
|
+
* 1. fnm default alias `~/.local/share/fnm/aliases/default/bin/hub-agent`
|
|
79
|
+
* が存在すればそれを返す (fnm 利用者にとって最も安定)
|
|
80
|
+
* 2. whichPath が `/fnm_multishells/` を含むなら realpath で
|
|
81
|
+
* `~/.local/share/fnm/node-versions/vX.Y.Z/.../bin/hub-agent.mjs`
|
|
82
|
+
* に展開 (node アップグレード前提でも install-service 再実行で済む)
|
|
83
|
+
* 3. それ以外 (brew / nvm / 通常 PATH) は whichPath をそのまま返す
|
|
84
|
+
*/
|
|
85
|
+
function normalizeBinPath(whichPath, opts = {}) {
|
|
86
|
+
const home = opts.home ?? os.homedir()
|
|
87
|
+
const fileExists = opts.fileExists ?? existsSync
|
|
88
|
+
const resolveReal = opts.realpath ?? realpathSync
|
|
89
|
+
|
|
90
|
+
const fnmDefaultBin = path.join(
|
|
91
|
+
home,
|
|
92
|
+
".local",
|
|
93
|
+
"share",
|
|
94
|
+
"fnm",
|
|
95
|
+
"aliases",
|
|
96
|
+
"default",
|
|
97
|
+
"bin",
|
|
98
|
+
"hub-agent",
|
|
99
|
+
)
|
|
100
|
+
if (fileExists(fnmDefaultBin)) return fnmDefaultBin
|
|
101
|
+
|
|
102
|
+
if (!whichPath) return null
|
|
103
|
+
if (whichPath.includes("/fnm_multishells/")) {
|
|
104
|
+
try {
|
|
105
|
+
return resolveReal(whichPath)
|
|
106
|
+
} catch {
|
|
107
|
+
// realpath 失敗時は whichPath をそのまま返す (既存挙動の保持)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return whichPath
|
|
111
|
+
}
|
|
112
|
+
|
|
66
113
|
/** インストール先の hub-agent CLI のフルパスを返す。`hub-agent` が PATH にある前提。 */
|
|
67
114
|
function detectHubAgentBin() {
|
|
68
115
|
const r = spawnSync("/usr/bin/which", ["hub-agent"], { encoding: "utf-8" })
|
|
69
|
-
|
|
116
|
+
const whichPath = r.status === 0 && r.stdout ? r.stdout.trim() : null
|
|
117
|
+
const normalized = normalizeBinPath(whichPath)
|
|
118
|
+
if (normalized) return normalized
|
|
70
119
|
// fallback: node $repo/bin/hub-agent.mjs
|
|
71
120
|
return path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "bin", "hub-agent.mjs")
|
|
72
121
|
}
|
|
@@ -179,6 +228,7 @@ export async function uninstallService() {
|
|
|
179
228
|
export const _internal = {
|
|
180
229
|
expandTemplate,
|
|
181
230
|
detectHubAgentBin,
|
|
231
|
+
normalizeBinPath,
|
|
182
232
|
macPlistPath,
|
|
183
233
|
linuxUnitPath,
|
|
184
234
|
repoTemplatesDir,
|