@cocorograph/hub-agent 0.4.1

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/LICENSE ADDED
@@ -0,0 +1,5 @@
1
+ Copyright (c) 2026 Cocorograph Inc.
2
+
3
+ All rights reserved. This package is proprietary and may be used only by
4
+ authorized members of Cocorograph Inc. Redistribution or modification
5
+ without prior written permission is prohibited.
package/README.md ADDED
@@ -0,0 +1,108 @@
1
+ # @cocorograph/hub-agent
2
+
3
+ Hub Hosted Cockpit のローカル常駐 agent。`hub.cocorograph.com` から outbound WSS で接続を受け、ローカルマシンの tmux / Claude Code セッションを Hub のブラウザ UI に中継する。
4
+
5
+ 仕様書: [`ナレッジ/インフラ/cockpit-hub-hosted-integration-spec`](https://hub.cocorograph.com/knowledge/spec/cockpit-hub-hosted-integration-spec) (Hub knowledge id=6080)
6
+
7
+ ## インストール
8
+
9
+ ### ワンライナー (推奨)
10
+
11
+ Hub Cockpit Agents ページ (`https://hub.cocorograph.com/user/cockpit/agents`) で
12
+ 「**ワンライナーセットアップ**」をクリックして表示される curl コマンドをコピー → ターミナルで実行:
13
+
14
+ ```bash
15
+ curl -fsSL https://api.hub.cocorograph.com/api/cockpit/agents/install-script | bash
16
+ ```
17
+
18
+ このスクリプトは以下を **全自動** で実行します:
19
+ - Homebrew (macOS で無ければ install)
20
+ - tmux + node 20+ (無ければ install)
21
+ - `npm i -g @cocorograph/hub-agent`
22
+ - `hub-agent enroll <token>` (token は Hub session 経由で埋め込み済、5 分有効)
23
+ - `hub-agent install-service` で OS サービス登録
24
+
25
+ ### 手動インストール (デバッグ用)
26
+
27
+ ```bash
28
+ npm i -g @cocorograph/hub-agent
29
+ hub-agent enroll <token> --hub-url https://api.hub.cocorograph.com
30
+ hub-agent install-service
31
+ ```
32
+
33
+ 詳細は `scripts/install.sh` を参照。
34
+
35
+ ## CLI
36
+
37
+ ```bash
38
+ hub-agent enroll <enrollment_token> [--hub-url URL] [--hostname NAME] [--force]
39
+ hub-agent start # 前景 daemon (install-service 不使用時)
40
+ hub-agent status # 現在の設定 + 接続状態
41
+ hub-agent stop # 停止方法のヒント表示
42
+ hub-agent install-service [--bin PATH]
43
+ hub-agent uninstall-service
44
+ hub-agent plugins list # ~/.hub/plugins/ から読み込み済プラグイン
45
+ hub-agent plugins examples # repo 同梱の install 可能 example
46
+ hub-agent plugins install <name> [--force]
47
+ hub-agent plugins uninstall <name>
48
+ ```
49
+
50
+ ## 設定ファイル
51
+
52
+ - `~/.hub/agent.json` — `{ agent_id, agent_token, hub_url }` (chmod 600)
53
+ - `~/.hub/plugins/<prio>-<name>/plugin.mjs` — プラグイン
54
+ - `~/.hub/plugins/<prio>-<name>/config.json` — プラグイン個別設定
55
+ - `~/.hub/agent.log` — install-service 経由起動時のログ
56
+ - `~/.hub/usage/latest.json` — Claude Code statusLine cache (使用量集計の元データ、任意)
57
+
58
+ ## 構成
59
+
60
+ ```
61
+ hub-agent/
62
+ ├── bin/
63
+ │ └── hub-agent.mjs # CLI entry (commander)
64
+ ├── src/
65
+ │ ├── main.mjs # daemon 本体 + dispatcher
66
+ │ ├── config.mjs # ~/.hub/agent.json 管理
67
+ │ ├── enroll.mjs # enrollment フロー
68
+ │ ├── ws-client.mjs # outbound WSS + reconnect with jitter
69
+ │ ├── pty-bridge.mjs # node-pty 多重化
70
+ │ ├── tmux.mjs # tmux exec/list/create/kill
71
+ │ ├── state.mjs # session status / context_pct 検知
72
+ │ ├── skills.mjs # ~/.claude/skills 集計
73
+ │ ├── usage.mjs # Claude Code 使用量集計
74
+ │ ├── plugin-loader.mjs # plugin discovery + hook chain
75
+ │ ├── hooks.mjs # hook 名・型定義
76
+ │ └── service-install.mjs # launchd / systemd install
77
+ ├── templates/
78
+ │ ├── co.cocorograph.hub-agent.plist
79
+ │ └── hub-agent.service
80
+ ├── scripts/
81
+ │ └── fix-node-pty-perms.mjs # node-pty spawn-helper +x 修復 (postinstall)
82
+ └── test/
83
+ ```
84
+
85
+ ## 開発状況
86
+
87
+ - [x] Sprint A: 仕様書 (Hub knowledge id=6080)
88
+ - [x] Sprint B: Hub 側 enrollment API + WS endpoint 雛形
89
+ - [x] Sprint C: 本リポジトリの雛形 + outbound WSS + hook system
90
+ - [x] Sprint D: pty bridge
91
+ - [x] Sprint E: tmux exec/list/create/kill 移植 + auto list UI
92
+ - [x] Sprint F: Hub Browser↔Agent multiplex 中継
93
+ - [x] Sprint G: Hub frontend /user/cockpit/{agents,terminal} ページ
94
+ - [x] Sprint H: skills / state dot / usage badge リレー
95
+ - [x] Sprint I: 接続復旧 (jitter + force reconnect + Celery stale 検知)
96
+ - [x] Sprint J: 本リポジトリ publish 準備 + launchd/systemd 連携
97
+ - [x] Sprint K: remote-ops プラグイン例 (`plugins/10-tailscale-remote/`)
98
+
99
+ ## 同梱プラグイン
100
+
101
+ - [`plugins/10-tailscale-remote`](./plugins/10-tailscale-remote/README.md) — 自宅
102
+ Mac (hub-agent host) を会社 Mac (`remote_host`) の tmux に対する透過プロキシ化。
103
+ `hub-agent plugins install 10-tailscale-remote` で `~/.hub/plugins/` にコピー、
104
+ `config.json` を編集して restart で有効化
105
+
106
+ ## ライセンス
107
+
108
+ UNLICENSED. Cocorograph Inc. 社内専用。
@@ -0,0 +1,201 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @cocorograph/hub-agent CLI entry。
4
+ *
5
+ * 仕様書: ナレッジ/インフラ/cockpit-hub-hosted-integration-spec (id=6080)
6
+ *
7
+ * Sprint C 時点で実装する subcommand:
8
+ * - enroll <token> : 初回 enrollment
9
+ * - start : daemon 起動
10
+ * - status : 現在の設定 / 接続状態
11
+ * - stop : daemon 停止 (Sprint J で launchd / systemd と併用)
12
+ * - plugins list : プラグイン一覧
13
+ */
14
+ import { readFileSync } from "node:fs"
15
+ import { fileURLToPath } from "node:url"
16
+ import path from "node:path"
17
+
18
+ import { Command } from "commander"
19
+
20
+ import { hasConfig, readConfig } from "../src/config.mjs"
21
+ import { enroll } from "../src/enroll.mjs"
22
+ import {
23
+ installPlugin,
24
+ listInstallableExamples,
25
+ uninstallPlugin,
26
+ } from "../src/plugin-install.mjs"
27
+ import { loadPlugins } from "../src/plugin-loader.mjs"
28
+ import { installService, uninstallService } from "../src/service-install.mjs"
29
+
30
+ const here = path.dirname(fileURLToPath(import.meta.url))
31
+ const pkg = JSON.parse(readFileSync(path.join(here, "..", "package.json"), "utf-8"))
32
+
33
+ const program = new Command()
34
+ program
35
+ .name("hub-agent")
36
+ .description("Hub Hosted Cockpit のローカル常駐 agent")
37
+ .version(pkg.version)
38
+
39
+ program
40
+ .command("enroll <enrollment_token>")
41
+ .description("Hub から発行された enrollment token で agent を登録する")
42
+ .option("--hub-url <url>", "Hub の base URL (default: $HUB_URL or https://hub.cocorograph.com)")
43
+ .option("--hostname <name>", "Hub に表示するホスト名 (default: os.hostname())")
44
+ .option("--force", "既存設定を上書きする")
45
+ .action(async (token, opts) => {
46
+ try {
47
+ const r = await enroll(token, {
48
+ hubUrl: opts.hubUrl,
49
+ hostname: opts.hostname,
50
+ version: pkg.version,
51
+ force: !!opts.force,
52
+ })
53
+ console.log(`enroll succeeded: agent_id=${r.agent_id} primary=${r.is_primary} hub=${r.hub_url}`)
54
+ } catch (err) {
55
+ console.error(`enroll failed: ${err.message}`)
56
+ process.exit(1)
57
+ }
58
+ })
59
+
60
+ program
61
+ .command("start")
62
+ .description("daemon を起動して Hub と WSS 接続する")
63
+ .action(async () => {
64
+ const { startDaemon } = await import("../src/main.mjs")
65
+ try {
66
+ await startDaemon({ version: pkg.version })
67
+ } catch (err) {
68
+ console.error(`start failed: ${err.message}`)
69
+ process.exit(1)
70
+ }
71
+ })
72
+
73
+ program
74
+ .command("status")
75
+ .description("設定と接続状態を表示する")
76
+ .action(async () => {
77
+ const exists = await hasConfig()
78
+ if (!exists) {
79
+ console.log("not enrolled (run `hub-agent enroll <token>`)")
80
+ process.exit(2)
81
+ }
82
+ const cfg = await readConfig()
83
+ console.log(`agent_id: ${cfg.agent_id}`)
84
+ console.log(`hub_url : ${cfg.hub_url}`)
85
+ console.log("(use ps/launchctl/systemctl to check if daemon is running)")
86
+ })
87
+
88
+ program
89
+ .command("stop")
90
+ .description("daemon を停止する (install-service してある場合は OS サービスを停止)")
91
+ .action(() => {
92
+ if (process.platform === "darwin") {
93
+ console.log("launchctl kill SIGTERM gui/$UID/co.cocorograph.hub-agent")
94
+ console.log("or `hub-agent uninstall-service` で常駐解除")
95
+ } else if (process.platform === "linux") {
96
+ console.log("systemctl --user stop hub-agent.service")
97
+ console.log("or `hub-agent uninstall-service` で常駐解除")
98
+ } else {
99
+ console.log("pkill -INT -f hub-agent でプロセス停止してください")
100
+ }
101
+ })
102
+
103
+ program
104
+ .command("install-service")
105
+ .description("OS サービスとして自動起動を有効化 (macOS=launchd / Linux=systemd --user)")
106
+ .option("--bin <path>", "明示的に hub-agent CLI のパスを指定")
107
+ .action(async (opts) => {
108
+ try {
109
+ const r = await installService({ bin: opts.bin })
110
+ console.log(`installed: ${r.platform} ${r.path}`)
111
+ console.log(`bin: ${r.bin}`)
112
+ if (r.platform === "darwin") {
113
+ console.log(`label: ${r.label}`)
114
+ console.log("確認: launchctl list | grep co.cocorograph.hub-agent")
115
+ } else {
116
+ console.log(`unit: ${r.unit}`)
117
+ console.log("確認: systemctl --user status hub-agent.service")
118
+ }
119
+ console.log("ログ: ~/.hub/agent.log")
120
+ } catch (err) {
121
+ console.error(`install-service failed: ${err.message}`)
122
+ process.exit(1)
123
+ }
124
+ })
125
+
126
+ program
127
+ .command("uninstall-service")
128
+ .description("OS サービス登録を解除")
129
+ .action(async () => {
130
+ try {
131
+ const r = await uninstallService()
132
+ console.log(`uninstalled: ${r.platform} ${r.path}`)
133
+ } catch (err) {
134
+ console.error(`uninstall-service failed: ${err.message}`)
135
+ process.exit(1)
136
+ }
137
+ })
138
+
139
+ const plugins = program.command("plugins").description("プラグイン管理")
140
+ plugins
141
+ .command("list")
142
+ .description("~/.hub/plugins/ から読み込み可能なプラグインを表示する")
143
+ .action(async () => {
144
+ const list = await loadPlugins(null)
145
+ if (list.length === 0) {
146
+ console.log("(no plugins)")
147
+ return
148
+ }
149
+ for (const p of list) {
150
+ console.log(`- ${p._prio}: ${p.name}`)
151
+ }
152
+ })
153
+
154
+ plugins
155
+ .command("examples")
156
+ .description("リポ同梱の install 可能 example プラグイン一覧")
157
+ .action(async () => {
158
+ const list = await listInstallableExamples()
159
+ if (list.length === 0) {
160
+ console.log("(no examples bundled)")
161
+ return
162
+ }
163
+ for (const name of list) {
164
+ console.log(`- ${name}`)
165
+ }
166
+ })
167
+
168
+ plugins
169
+ .command("install <name>")
170
+ .description("repo plugins/<name>/ を ~/.hub/plugins/<name>/ にコピー (config.json は温存)")
171
+ .option("--force", "既存ディレクトリを上書きする")
172
+ .action(async (name, opts) => {
173
+ try {
174
+ const r = await installPlugin(name, { force: !!opts.force })
175
+ console.log(`installed: ${r.dest}`)
176
+ console.log("次のステップ:")
177
+ console.log(` $EDITOR ${r.dest}/config.json # 既存なら温存、新規なら example をコピー`)
178
+ console.log(` hub-agent restart (launchctl kickstart / systemctl restart)`)
179
+ } catch (err) {
180
+ console.error(`plugins install failed: ${err.message}`)
181
+ process.exit(1)
182
+ }
183
+ })
184
+
185
+ plugins
186
+ .command("uninstall <name>")
187
+ .description("~/.hub/plugins/<name>/ を削除")
188
+ .action(async (name) => {
189
+ try {
190
+ const r = await uninstallPlugin(name)
191
+ console.log(`uninstalled: ${r.dest}`)
192
+ } catch (err) {
193
+ console.error(`plugins uninstall failed: ${err.message}`)
194
+ process.exit(1)
195
+ }
196
+ })
197
+
198
+ program.parseAsync(process.argv).catch((err) => {
199
+ console.error(err)
200
+ process.exit(1)
201
+ })
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@cocorograph/hub-agent",
3
+ "version": "0.4.1",
4
+ "description": "Hub Hosted Cockpit のローカル常駐 agent。Hub と outbound WSS で接続し、ローカルの tmux/pty を中継する。",
5
+ "type": "module",
6
+ "license": "UNLICENSED",
7
+ "private": false,
8
+ "publishConfig": {
9
+ "access": "public",
10
+ "registry": "https://registry.npmjs.org/"
11
+ },
12
+ "bin": {
13
+ "hub-agent": "bin/hub-agent.mjs"
14
+ },
15
+ "main": "src/main.mjs",
16
+ "engines": {
17
+ "node": ">=20"
18
+ },
19
+ "scripts": {
20
+ "start": "node bin/hub-agent.mjs start",
21
+ "test": "node --test test/*.test.mjs",
22
+ "postinstall": "node scripts/fix-node-pty-perms.mjs",
23
+ "prepublishOnly": "npm test"
24
+ },
25
+ "files": [
26
+ "bin/",
27
+ "src/",
28
+ "scripts/",
29
+ "templates/",
30
+ "plugins/",
31
+ "README.md",
32
+ "LICENSE"
33
+ ],
34
+ "dependencies": {
35
+ "commander": "^12.1.0",
36
+ "node-pty": "^1.0.0",
37
+ "pino": "^9.0.0",
38
+ "ws": "^8.18.0"
39
+ },
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "git+https://github.com/cocorograph/D00000_hub-agent.git"
43
+ },
44
+ "homepage": "https://github.com/cocorograph/D00000_hub-agent#readme",
45
+ "bugs": {
46
+ "url": "https://github.com/cocorograph/D00000_hub-agent/issues"
47
+ },
48
+ "keywords": [
49
+ "cockpit",
50
+ "hub",
51
+ "tmux",
52
+ "remote",
53
+ "agent",
54
+ "cocorograph"
55
+ ]
56
+ }
@@ -0,0 +1,71 @@
1
+ # 10-tailscale-remote
2
+
3
+ 自宅 Mac (hub-agent host) を、Tailscale 等で繋がった会社 Mac (`remote_host`) の
4
+ tmux に対する透過プロキシ化するサンプル plugin。
5
+
6
+ ## 何が起きるか
7
+
8
+ | Browser action | 通常 (plugin 無し) | このプラグイン有効時 |
9
+ |---|---|---|
10
+ | `pty.attach` | ローカル `tmux attach -t SESSION` | `mosh remote_host -- tmux attach -t SESSION` |
11
+ | `tmux.exec [...]` | ローカル `tmux ...` | `ssh remote_host tmux ...` |
12
+
13
+ つまり、`hub.cocorograph.com/user/cockpit/terminal` から開くセッションが、
14
+ あなたの手元 Mac ではなく `remote_host` の tmux に直接繋がります。Cockpit UI の
15
+ `session.state` / `tmux.list_sessions` も全て `remote_host` 側の値です。
16
+
17
+ ## セットアップ
18
+
19
+ 1. ディレクトリを `~/.hub/plugins/10-tailscale-remote/` にコピー
20
+ ```bash
21
+ # CLI 経由 (推奨)
22
+ hub-agent plugins install 10-tailscale-remote
23
+
24
+ # 手動
25
+ mkdir -p ~/.hub/plugins/10-tailscale-remote
26
+ cp -r $(npm root -g)/@cocorograph/hub-agent/plugins/10-tailscale-remote/* \
27
+ ~/.hub/plugins/10-tailscale-remote/
28
+ ```
29
+
30
+ 2. `config.json` を編集
31
+ ```bash
32
+ cp ~/.hub/plugins/10-tailscale-remote/config.example.json \
33
+ ~/.hub/plugins/10-tailscale-remote/config.json
34
+ $EDITOR ~/.hub/plugins/10-tailscale-remote/config.json
35
+ ```
36
+
37
+ ```jsonc
38
+ {
39
+ "remote_host": "mbpm5-office", // ssh ホスト名
40
+ "ssh_key": "/Users/kaz/.ssh/mbpm5-office/id_ed25519",
41
+ "mosh_server": "/opt/homebrew/bin/mosh-server" // remote_host 側のパス
42
+ }
43
+ ```
44
+
45
+ 3. remote 側の準備
46
+ - 同じユーザーで ssh 鍵ログイン可能であること
47
+ - `tmux` + `mosh-server` がインストール済みであること
48
+ - tmux session が既に走っていること (or `hub-agent` の `tmux.create_session` 経由で作る)
49
+
50
+ 4. hub-agent restart
51
+ ```bash
52
+ launchctl kickstart -k gui/$UID/co.cocorograph.hub-agent # macOS
53
+ # or
54
+ systemctl --user restart hub-agent.service # Linux
55
+ ```
56
+
57
+ 5. agent log で `tailscale-remote-ops 有効` の表示を確認
58
+ ```bash
59
+ tail -f ~/.hub/agent.log
60
+ ```
61
+
62
+ ## トラブルシューティング
63
+
64
+ - `config.json` 無し → plugin は no-op で読み込まれます (`agent.log` に WARN)
65
+ - ssh が「Host key verification failed」→ `ssh remote_host` で 1 度手動接続して known_hosts に追加
66
+ - mosh が `Did not find mosh server startup message` → remote_host の `mosh_server` パスを確認 (`which mosh-server`)
67
+ - Tailscale が落ちて remote_host に届かない → cockpit terminal を開くと WS 接続自体は成功するが pty が即 exit する
68
+
69
+ ## 仕様書
70
+
71
+ [`ナレッジ/インフラ/cockpit-hub-hosted-integration-spec`](https://hub.cocorograph.com/knowledge/spec/cockpit-hub-hosted-integration-spec) (id=6080)
@@ -0,0 +1,6 @@
1
+ {
2
+ "_comment": "このファイルを config.json にコピーしてから編集してください。",
3
+ "remote_host": "mbpm5-office",
4
+ "ssh_key": "/Users/kaz/.ssh/mbpm5-office/id_ed25519",
5
+ "mosh_server": "/opt/homebrew/bin/mosh-server"
6
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * tailscale-remote: 自宅 Mac (hub-agent host) を 会社 Mac (remote_host) の
3
+ * tmux に対する透過プロキシ化するプラグイン (Sprint K サンプル)。
4
+ *
5
+ * - `interceptPtySpawn`: ブラウザの attach を mosh + ssh + tmux attach に差し替え
6
+ * - `interceptTmuxExec`: tmux.* RPC (list/create/kill 等) を ssh 越しに発行
7
+ *
8
+ * 使い方:
9
+ * 1. `~/.hub/plugins/10-tailscale-remote/` にこのファイル + config.json を置く
10
+ * (`hub-agent plugins install 10-tailscale-remote` で自動コピー可)
11
+ * 2. config.json の remote_host / ssh_key / mosh_server を埋める
12
+ * 3. hub-agent restart (`hub-agent install-service` してあれば
13
+ * `launchctl kickstart -k gui/$UID/co.cocorograph.hub-agent`)
14
+ *
15
+ * 仕様書: ナレッジ/インフラ/cockpit-hub-hosted-integration-spec (id=6080)
16
+ */
17
+ import { readFileSync } from "node:fs"
18
+
19
+ const cfgUrl = new URL("./config.json", import.meta.url)
20
+ let cfg = { remote_host: "", ssh_key: "", mosh_server: "/opt/homebrew/bin/mosh-server" }
21
+ try {
22
+ cfg = { ...cfg, ...JSON.parse(readFileSync(cfgUrl, "utf-8")) }
23
+ } catch (err) {
24
+ // config.json が無い場合は plugin を no-op で読み込む (hook は何も上書きしない)
25
+ // user が編集してから restart する想定
26
+ cfg.__error = err.message
27
+ }
28
+
29
+ function isConfigured() {
30
+ return Boolean(cfg.remote_host && cfg.ssh_key && !cfg.__error)
31
+ }
32
+
33
+ function sshOpts() {
34
+ return ["-o", "IdentityAgent=none", "-i", cfg.ssh_key]
35
+ }
36
+
37
+ export default {
38
+ name: "tailscale-remote-ops",
39
+ hooks: {
40
+ async onAgentStart({ logger }) {
41
+ if (cfg.__error) {
42
+ logger?.warn(
43
+ { err: cfg.__error },
44
+ "tailscale-remote-ops: config.json が読めません。plugin は no-op で動作します",
45
+ )
46
+ return
47
+ }
48
+ if (!isConfigured()) {
49
+ logger?.warn(
50
+ { config: cfg },
51
+ "tailscale-remote-ops: remote_host / ssh_key が未設定。plugin は no-op",
52
+ )
53
+ return
54
+ }
55
+ logger?.info(
56
+ { remote_host: cfg.remote_host },
57
+ "tailscale-remote-ops 有効: pty.attach / tmux.exec を ssh 越しに発行します",
58
+ )
59
+ },
60
+
61
+ /**
62
+ * Browser からの attach を mosh + ssh + tmux attach に差し替える。
63
+ * pty bridge は通常通り node-pty で /bin/sh を spawn するが、その中身が
64
+ * remote_host への mosh ssh セッションになる。
65
+ */
66
+ async interceptPtySpawn({ sessionName }) {
67
+ if (!isConfigured()) return null
68
+ const sshOpt = sshOpts().join(" ")
69
+ const command = "/bin/sh"
70
+ const inner = [
71
+ `exec mosh`,
72
+ `--predict=adaptive`,
73
+ `--server=${cfg.mosh_server}`,
74
+ `--ssh="ssh ${sshOpt}"`,
75
+ cfg.remote_host,
76
+ `--`,
77
+ `bash -l -c "tmux attach -t ${sessionName}"`,
78
+ ].join(" ")
79
+ return {
80
+ command,
81
+ args: ["-c", inner],
82
+ }
83
+ },
84
+
85
+ /**
86
+ * tmux.* RPC (list-sessions / new-session / kill-session 等) を
87
+ * ssh 越しに remote_host で実行する。
88
+ */
89
+ async interceptTmuxExec({ args }) {
90
+ if (!isConfigured()) return null
91
+ return {
92
+ command: "ssh",
93
+ args: [...sshOpts(), cfg.remote_host, "tmux", ...args],
94
+ }
95
+ },
96
+ },
97
+ }
98
+
99
+ export const _internal = { cfg, isConfigured, sshOpts }
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env node
2
+ // pnpm 経由インストールだと node-pty/prebuilds/*/spawn-helper の実行権限が
3
+ // 剥がれて posix_spawnp が失敗する事象への workaround.
4
+ // postinstall で確実に +x を付与する。
5
+ import { readdirSync, statSync, chmodSync } from "node:fs";
6
+ import { join } from "node:path";
7
+
8
+ const PNPM_GLOB = "node_modules/.pnpm";
9
+ const FALLBACK = "node_modules/node-pty/prebuilds";
10
+
11
+ function fixDir(dir) {
12
+ let count = 0;
13
+ for (const platform of readdirSync(dir)) {
14
+ const helper = join(dir, platform, "spawn-helper");
15
+ try {
16
+ const st = statSync(helper);
17
+ if (!(st.mode & 0o111)) {
18
+ chmodSync(helper, st.mode | 0o755);
19
+ console.log(`[fix-perms] chmod +x ${helper}`);
20
+ count++;
21
+ }
22
+ } catch {
23
+ // not present (e.g. win32 dir has no spawn-helper)
24
+ }
25
+ }
26
+ return count;
27
+ }
28
+
29
+ function findPnpmPrebuilds() {
30
+ const out = [];
31
+ try {
32
+ for (const entry of readdirSync(PNPM_GLOB)) {
33
+ if (!entry.startsWith("node-pty@")) continue;
34
+ const p = join(PNPM_GLOB, entry, "node_modules/node-pty/prebuilds");
35
+ try {
36
+ statSync(p);
37
+ out.push(p);
38
+ } catch { /* skip */ }
39
+ }
40
+ } catch { /* no pnpm dir */ }
41
+ return out;
42
+ }
43
+
44
+ let fixed = 0;
45
+ for (const dir of findPnpmPrebuilds()) {
46
+ fixed += fixDir(dir);
47
+ }
48
+ try {
49
+ statSync(FALLBACK);
50
+ fixed += fixDir(FALLBACK);
51
+ } catch { /* not hoisted */ }
52
+
53
+ if (fixed === 0) {
54
+ // 何も修正しなかった = 既に OK or node-pty 未インストール。サイレント終了。
55
+ }