@cocorograph/hub-agent 0.5.12 → 0.5.14
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/hub-agent.mjs +23 -1
- package/package.json +1 -1
- package/scripts/install.sh +47 -7
- package/src/pty-bridge.mjs +58 -4
- package/src/service-install.mjs +41 -0
package/bin/hub-agent.mjs
CHANGED
|
@@ -34,7 +34,11 @@ import {
|
|
|
34
34
|
uninstallPlugin,
|
|
35
35
|
} from "../src/plugin-install.mjs"
|
|
36
36
|
import { loadPlugins } from "../src/plugin-loader.mjs"
|
|
37
|
-
import {
|
|
37
|
+
import {
|
|
38
|
+
installService,
|
|
39
|
+
restartService,
|
|
40
|
+
uninstallService,
|
|
41
|
+
} from "../src/service-install.mjs"
|
|
38
42
|
|
|
39
43
|
const here = path.dirname(fileURLToPath(import.meta.url))
|
|
40
44
|
const pkg = JSON.parse(readFileSync(path.join(here, "..", "package.json"), "utf-8"))
|
|
@@ -164,6 +168,24 @@ program
|
|
|
164
168
|
}
|
|
165
169
|
})
|
|
166
170
|
|
|
171
|
+
program
|
|
172
|
+
.command("restart")
|
|
173
|
+
.description("install-service 済みの daemon を再起動 (macOS=launchctl kickstart / Linux=systemctl --user restart)")
|
|
174
|
+
.action(async () => {
|
|
175
|
+
try {
|
|
176
|
+
const r = await restartService()
|
|
177
|
+
console.log(`restarted: ${r.platform} ${r.path}`)
|
|
178
|
+
if (r.platform === "darwin") {
|
|
179
|
+
console.log(`label: ${r.label}`)
|
|
180
|
+
} else {
|
|
181
|
+
console.log(`unit: ${r.unit}`)
|
|
182
|
+
}
|
|
183
|
+
} catch (err) {
|
|
184
|
+
console.error(`restart failed: ${err.message}`)
|
|
185
|
+
process.exit(1)
|
|
186
|
+
}
|
|
187
|
+
})
|
|
188
|
+
|
|
167
189
|
program
|
|
168
190
|
.command("uninstall-service")
|
|
169
191
|
.description("OS サービス登録を解除")
|
package/package.json
CHANGED
package/scripts/install.sh
CHANGED
|
@@ -30,14 +30,54 @@ present() { command -v "$1" >/dev/null 2>&1; }
|
|
|
30
30
|
|
|
31
31
|
ensure_brew() {
|
|
32
32
|
if [[ "$(uname)" != "Darwin" ]]; then return 0; fi
|
|
33
|
-
if present brew; then
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
if present brew; then
|
|
34
|
+
color_ok "brew already installed"
|
|
35
|
+
else
|
|
36
|
+
color_step "Homebrew をインストール"
|
|
37
|
+
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
|
38
|
+
# Apple Silicon の brew はデフォルト PATH に入らないので追加
|
|
39
|
+
if [[ -d /opt/homebrew/bin ]]; then
|
|
40
|
+
export PATH="/opt/homebrew/bin:$PATH"
|
|
41
|
+
fi
|
|
42
|
+
present brew || { color_err "brew install failed"; exit 1; }
|
|
39
43
|
fi
|
|
40
|
-
|
|
44
|
+
# Apple Silicon Mac は `/opt/homebrew/bin` がデフォルト PATH に入らないので、
|
|
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
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# Apple Silicon Mac で brew のパスを zsh / bash 永続化する。既に追記済みなら no-op。
|
|
52
|
+
# `eval "$(/opt/homebrew/bin/brew shellenv)"` を書くことで、Homebrew が用意する
|
|
53
|
+
# HOMEBREW_PREFIX / PATH / MANPATH / INFOPATH すべてを新規シェルで自動セットする。
|
|
54
|
+
persist_brew_shellenv() {
|
|
55
|
+
local brew_bin=""
|
|
56
|
+
if [[ -x /opt/homebrew/bin/brew ]]; then
|
|
57
|
+
brew_bin="/opt/homebrew/bin/brew"
|
|
58
|
+
elif [[ -x /usr/local/bin/brew ]]; then
|
|
59
|
+
# Intel Mac は `/usr/local/bin` がデフォルト PATH に入るので追記不要
|
|
60
|
+
return 0
|
|
61
|
+
else
|
|
62
|
+
return 0
|
|
63
|
+
fi
|
|
64
|
+
local snippet="eval \"\$(${brew_bin} shellenv)\""
|
|
65
|
+
local marker="# >>> hub-agent: brew shellenv (Apple Silicon PATH) >>>"
|
|
66
|
+
local end_marker="# <<< hub-agent: brew shellenv <<<"
|
|
67
|
+
local target
|
|
68
|
+
for target in "$HOME/.zprofile" "$HOME/.bash_profile"; do
|
|
69
|
+
if [[ -f "$target" ]] && grep -Fq "$snippet" "$target"; then
|
|
70
|
+
color_ok "brew shellenv は既に $target にあります"
|
|
71
|
+
continue
|
|
72
|
+
fi
|
|
73
|
+
color_step "$target に brew shellenv を追記"
|
|
74
|
+
{
|
|
75
|
+
printf '\n%s\n' "$marker"
|
|
76
|
+
printf '%s\n' "$snippet"
|
|
77
|
+
printf '%s\n' "$end_marker"
|
|
78
|
+
} >> "$target"
|
|
79
|
+
color_ok "$target に追記しました (新規シェルから有効)"
|
|
80
|
+
done
|
|
41
81
|
}
|
|
42
82
|
|
|
43
83
|
ensure_pkg() {
|
package/src/pty-bridge.mjs
CHANGED
|
@@ -20,6 +20,14 @@ import { runHookChain } from "./plugin-loader.mjs"
|
|
|
20
20
|
const DEFAULT_COLS = 120
|
|
21
21
|
const DEFAULT_ROWS = 32
|
|
22
22
|
|
|
23
|
+
// pty.onData の chunk を coalesce する待ち時間 (ms)。
|
|
24
|
+
// 0 だと chunk 1 つにつき 1 WebSocket メッセージ + 1 JSON.stringify になり、
|
|
25
|
+
// claude TUI の連続 redraw で秒間 100+ メッセージ → Hub → channels_redis pub/sub
|
|
26
|
+
// → ブラウザ JSON.parse が詰まって瞬断・固まりの原因になる。12ms バッファに
|
|
27
|
+
// 溜めて 80Hz 程度に丸めるとメッセージ数が 1/5〜1/10 に減り、画面は十分滑らか
|
|
28
|
+
// に見える (1 フレーム 16.7ms より短いので体感レイテンシは増えない)。
|
|
29
|
+
const DEFAULT_COALESCE_MS = 12
|
|
30
|
+
|
|
23
31
|
function resolveBin(name) {
|
|
24
32
|
try {
|
|
25
33
|
return execFileSync("/usr/bin/which", [name], { encoding: "utf-8" }).trim()
|
|
@@ -43,8 +51,9 @@ export class PtyBridge extends EventEmitter {
|
|
|
43
51
|
* @param {(args: {sessionName: string, cols: number, rows: number, env: object}) => {command: string, args: string[], env?: object}} [opts.defaultSpawnCommand]
|
|
44
52
|
* - plugin が null を返したときに使うデフォルト spawn 仕様。省略時は
|
|
45
53
|
* `/bin/sh -c "exec tmux attach -t <sessionName>"`
|
|
54
|
+
* @param {number} [opts.coalesceMs] - pty 出力を coalesce する間隔 (ms)。0 で無効。
|
|
46
55
|
*/
|
|
47
|
-
constructor({ ptyModule, logger, plugins = [], defaultSpawnCommand } = {}) {
|
|
56
|
+
constructor({ ptyModule, logger, plugins = [], defaultSpawnCommand, coalesceMs } = {}) {
|
|
48
57
|
super()
|
|
49
58
|
if (!ptyModule || typeof ptyModule.spawn !== "function") {
|
|
50
59
|
throw new TypeError("PtyBridge requires { ptyModule: { spawn } }")
|
|
@@ -59,8 +68,30 @@ export class PtyBridge extends EventEmitter {
|
|
|
59
68
|
args: ["-c", `exec ${tmuxBin()} attach -t ${sessionName}`],
|
|
60
69
|
env: process.env,
|
|
61
70
|
}))
|
|
71
|
+
this.coalesceMs = coalesceMs ?? DEFAULT_COALESCE_MS
|
|
62
72
|
/** @type {Map<string, import('node-pty').IPty>} */
|
|
63
73
|
this.streams = new Map()
|
|
74
|
+
/** @type {Map<string, {buf: string, timer: ReturnType<typeof setTimeout>|null}>} */
|
|
75
|
+
this.coalesceState = new Map()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* stream の coalesce buffer を即時 flush して emit する。
|
|
80
|
+
* detach / exit / shutdown のタイミングで残バッファの取りこぼしを防ぐ。
|
|
81
|
+
* @param {string} stream_id
|
|
82
|
+
*/
|
|
83
|
+
_flushCoalesce(stream_id) {
|
|
84
|
+
const state = this.coalesceState.get(stream_id)
|
|
85
|
+
if (!state) return
|
|
86
|
+
if (state.timer) {
|
|
87
|
+
clearTimeout(state.timer)
|
|
88
|
+
state.timer = null
|
|
89
|
+
}
|
|
90
|
+
if (state.buf) {
|
|
91
|
+
const out = state.buf
|
|
92
|
+
state.buf = ""
|
|
93
|
+
this.emit("output", { stream_id, data: out })
|
|
94
|
+
}
|
|
64
95
|
}
|
|
65
96
|
|
|
66
97
|
/**
|
|
@@ -108,10 +139,30 @@ export class PtyBridge extends EventEmitter {
|
|
|
108
139
|
"pty attached",
|
|
109
140
|
)
|
|
110
141
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
142
|
+
if (this.coalesceMs > 0) {
|
|
143
|
+
const state = { buf: "", timer: null }
|
|
144
|
+
this.coalesceState.set(stream_id, state)
|
|
145
|
+
pty.onData((data) => {
|
|
146
|
+
state.buf += data
|
|
147
|
+
if (!state.timer) {
|
|
148
|
+
state.timer = setTimeout(() => {
|
|
149
|
+
state.timer = null
|
|
150
|
+
const out = state.buf
|
|
151
|
+
if (!out) return
|
|
152
|
+
state.buf = ""
|
|
153
|
+
this.emit("output", { stream_id, data: out })
|
|
154
|
+
}, this.coalesceMs)
|
|
155
|
+
}
|
|
156
|
+
})
|
|
157
|
+
} else {
|
|
158
|
+
pty.onData((data) => {
|
|
159
|
+
this.emit("output", { stream_id, data })
|
|
160
|
+
})
|
|
161
|
+
}
|
|
114
162
|
pty.onExit(({ exitCode }) => {
|
|
163
|
+
// 残バッファを取りこぼさないよう flush してから exit を emit する。
|
|
164
|
+
this._flushCoalesce(stream_id)
|
|
165
|
+
this.coalesceState.delete(stream_id)
|
|
115
166
|
this.streams.delete(stream_id)
|
|
116
167
|
this.emit("exit", { stream_id, code: exitCode })
|
|
117
168
|
})
|
|
@@ -154,6 +205,9 @@ export class PtyBridge extends EventEmitter {
|
|
|
154
205
|
detach({ stream_id }) {
|
|
155
206
|
const pty = this.streams.get(stream_id)
|
|
156
207
|
if (!pty) return false
|
|
208
|
+
// kill 前に残バッファを emit しておく (onExit にも flush はあるが、
|
|
209
|
+
// detach は browser 側都合なので最新を確実に届けたい)。
|
|
210
|
+
this._flushCoalesce(stream_id)
|
|
157
211
|
try {
|
|
158
212
|
pty.kill()
|
|
159
213
|
} catch {
|
package/src/service-install.mjs
CHANGED
|
@@ -105,6 +105,47 @@ export async function installService({ bin } = {}) {
|
|
|
105
105
|
throw new Error(`unsupported platform: ${process.platform}`)
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
/**
|
|
109
|
+
* install-service 済みの hub-agent を再起動する。
|
|
110
|
+
*
|
|
111
|
+
* - macOS: `launchctl kickstart -k gui/<uid>/<label>` で SIGTERM + 再起動
|
|
112
|
+
* - Linux: `systemctl --user restart hub-agent.service`
|
|
113
|
+
*
|
|
114
|
+
* service が install されていない場合は分かりやすい error を投げる。
|
|
115
|
+
* (cockpit web UI から `hub-agent restart` 案内が出ても、enroll 直後の
|
|
116
|
+
* ユーザーが叩くケースを想定)
|
|
117
|
+
*/
|
|
118
|
+
export async function restartService() {
|
|
119
|
+
if (process.platform === "darwin") {
|
|
120
|
+
const dest = macPlistPath()
|
|
121
|
+
try {
|
|
122
|
+
await fs.access(dest)
|
|
123
|
+
} catch {
|
|
124
|
+
throw new Error(
|
|
125
|
+
`service not installed (${dest} not found). run \`hub-agent install-service\` first`,
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
const uid = ensureUnixUid()
|
|
129
|
+
run("launchctl", ["kickstart", "-k", `gui/${uid}/${PLIST_LABEL}`])
|
|
130
|
+
return { platform: "darwin", path: dest, label: PLIST_LABEL }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (process.platform === "linux") {
|
|
134
|
+
const dest = linuxUnitPath()
|
|
135
|
+
try {
|
|
136
|
+
await fs.access(dest)
|
|
137
|
+
} catch {
|
|
138
|
+
throw new Error(
|
|
139
|
+
`service not installed (${dest} not found). run \`hub-agent install-service\` first`,
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
run("systemctl", ["--user", "restart", SYSTEMD_UNIT_NAME])
|
|
143
|
+
return { platform: "linux", path: dest, unit: SYSTEMD_UNIT_NAME }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
throw new Error(`unsupported platform: ${process.platform}`)
|
|
147
|
+
}
|
|
148
|
+
|
|
108
149
|
export async function uninstallService() {
|
|
109
150
|
if (process.platform === "darwin") {
|
|
110
151
|
const uid = ensureUnixUid()
|