@cocorograph/hub-agent 0.8.23 → 0.8.25

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.
@@ -1,71 +0,0 @@
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)
@@ -1,6 +0,0 @@
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
- }
@@ -1,99 +0,0 @@
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 }
@@ -1,89 +0,0 @@
1
- #!/usr/bin/env node
2
- // pnpm 経由インストールだと prebuilds/*/spawn-helper の実行権限が剥がれて
3
- // posix_spawnp が失敗する事象への workaround. postinstall で確実に +x を付与する。
4
- //
5
- // PTY backend は @lydell/node-pty (Microsoft 公式の fd-leak 修正フォーク) に移行済み。
6
- // prebuild は @lydell/node-pty-<platform>/prebuilds/<platform>/spawn-helper に入る
7
- // (npm install では +x 付きで展開されるが、pnpm 等で剥がれるケースに備えて修復する)。
8
- // 旧 node-pty の配置も後方互換で走査する。
9
- import { readdirSync, statSync, chmodSync } from "node:fs";
10
- import { join } from "node:path";
11
-
12
- const PNPM_GLOB = "node_modules/.pnpm";
13
- // prebuilds ディレクトリの候補 (node-pty 旧配置 + @lydell platform パッケージ)。
14
- const FALLBACK_DIRS = ["node_modules/node-pty/prebuilds"];
15
-
16
- function fixDir(dir) {
17
- let count = 0;
18
- for (const platform of readdirSync(dir)) {
19
- const helper = join(dir, platform, "spawn-helper");
20
- try {
21
- const st = statSync(helper);
22
- if (!(st.mode & 0o111)) {
23
- chmodSync(helper, st.mode | 0o755);
24
- console.log(`[fix-perms] chmod +x ${helper}`);
25
- count++;
26
- }
27
- } catch {
28
- // not present (e.g. win32 dir has no spawn-helper)
29
- }
30
- }
31
- return count;
32
- }
33
-
34
- // @lydell の platform パッケージ (@lydell/node-pty-darwin-arm64 等) を走査する。
35
- function findLydellPrebuilds() {
36
- const out = [];
37
- const scopeDir = "node_modules/@lydell";
38
- try {
39
- for (const entry of readdirSync(scopeDir)) {
40
- if (!entry.startsWith("node-pty-")) continue; // platform バイナリパッケージのみ
41
- const p = join(scopeDir, entry, "prebuilds");
42
- try {
43
- statSync(p);
44
- out.push(p);
45
- } catch { /* prebuilds 無し */ }
46
- }
47
- } catch { /* @lydell scope 無し */ }
48
- return out;
49
- }
50
-
51
- function findPnpmPrebuilds() {
52
- const out = [];
53
- try {
54
- for (const entry of readdirSync(PNPM_GLOB)) {
55
- let inner = null;
56
- if (entry.startsWith("node-pty@")) {
57
- // 旧 node-pty: node_modules/.pnpm/node-pty@x/node_modules/node-pty/prebuilds
58
- inner = "node_modules/node-pty/prebuilds";
59
- } else if (entry.startsWith("@lydell+node-pty-")) {
60
- // @lydell platform: @lydell+node-pty-darwin-arm64@x → pkg 名 = node-pty-darwin-arm64
61
- const pkg = entry.slice("@lydell+".length).split("@")[0];
62
- inner = join("node_modules/@lydell", pkg, "prebuilds");
63
- } else {
64
- continue;
65
- }
66
- const p = join(PNPM_GLOB, entry, inner);
67
- try {
68
- statSync(p);
69
- out.push(p);
70
- } catch { /* skip */ }
71
- }
72
- } catch { /* no pnpm dir */ }
73
- return out;
74
- }
75
-
76
- let fixed = 0;
77
- for (const dir of [...findPnpmPrebuilds(), ...findLydellPrebuilds()]) {
78
- fixed += fixDir(dir);
79
- }
80
- for (const fallback of FALLBACK_DIRS) {
81
- try {
82
- statSync(fallback);
83
- fixed += fixDir(fallback);
84
- } catch { /* not hoisted */ }
85
- }
86
-
87
- if (fixed === 0) {
88
- // 何も修正しなかった = 既に OK or node-pty 未インストール。サイレント終了。
89
- }
@@ -1,111 +0,0 @@
1
- /**
2
- * `hub-agent plugins install <name>` / `uninstall <name>` の実装 (Sprint K)。
3
- *
4
- * リポ同梱の `plugins/<name>/` を `~/.hub/plugins/<name>/` にコピーする。
5
- * uninstall は `~/.hub/plugins/<name>/` を rm -rf する。
6
- *
7
- * config.json は plugin によって user 編集が必要なため、existing なら上書き
8
- * しない (force オプションで強制可)。
9
- */
10
- import { promises as fs } from "node:fs"
11
- import path from "node:path"
12
- import { fileURLToPath } from "node:url"
13
-
14
- import { paths as configPaths } from "./config.mjs"
15
-
16
- function repoPluginsDir() {
17
- return path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "plugins")
18
- }
19
-
20
- export async function listInstallableExamples() {
21
- const dir = repoPluginsDir()
22
- let entries
23
- try {
24
- entries = await fs.readdir(dir, { withFileTypes: true })
25
- } catch {
26
- return []
27
- }
28
- const out = []
29
- for (const e of entries) {
30
- if (!e.isDirectory()) continue
31
- const pluginFile = path.join(dir, e.name, "plugin.mjs")
32
- try {
33
- await fs.access(pluginFile)
34
- out.push(e.name)
35
- } catch {
36
- /* plugin.mjs 無いディレクトリは skip */
37
- }
38
- }
39
- return out
40
- }
41
-
42
- async function copyDir(src, dest, { skipExisting = [] } = {}) {
43
- await fs.mkdir(dest, { recursive: true, mode: 0o700 })
44
- const entries = await fs.readdir(src, { withFileTypes: true })
45
- for (const e of entries) {
46
- const s = path.join(src, e.name)
47
- const d = path.join(dest, e.name)
48
- if (e.isDirectory()) {
49
- await copyDir(s, d, { skipExisting })
50
- } else {
51
- if (skipExisting.includes(e.name)) {
52
- try {
53
- await fs.access(d)
54
- continue // 既存ならコピーしない (user 編集を尊重)
55
- } catch {
56
- /* fall through to copy */
57
- }
58
- }
59
- await fs.copyFile(s, d)
60
- }
61
- }
62
- }
63
-
64
- /** @returns {Promise<{ source: string, dest: string, copied: boolean }>} */
65
- export async function installPlugin(name, { force = false } = {}) {
66
- if (!name || typeof name !== "string") {
67
- throw new Error("plugin name is required")
68
- }
69
- const src = path.join(repoPluginsDir(), name)
70
- try {
71
- const st = await fs.stat(src)
72
- if (!st.isDirectory()) throw new Error("not a directory")
73
- } catch {
74
- const available = await listInstallableExamples()
75
- const hint = available.length ? ` Available: ${available.join(", ")}` : ""
76
- throw new Error(`plugin "${name}" not found in repo plugins/.${hint}`)
77
- }
78
- const dest = path.join(configPaths.pluginsDir, name)
79
- if (!force) {
80
- try {
81
- await fs.access(dest)
82
- throw new Error(
83
- `${dest} already exists. Pass --force to overwrite (config.json は常に温存)`,
84
- )
85
- } catch (err) {
86
- if (err.code !== "ENOENT") {
87
- // dest exists かつ force でない
88
- if (err.message && err.message.includes("already exists")) throw err
89
- if (err.code !== undefined) throw err
90
- }
91
- }
92
- }
93
- // config.json は既存なら skip して user 編集を温存
94
- await copyDir(src, dest, { skipExisting: ["config.json"] })
95
- return { source: src, dest, copied: true }
96
- }
97
-
98
- export async function uninstallPlugin(name) {
99
- if (!name || typeof name !== "string") {
100
- throw new Error("plugin name is required")
101
- }
102
- const dest = path.join(configPaths.pluginsDir, name)
103
- try {
104
- await fs.rm(dest, { recursive: true, force: true })
105
- } catch (err) {
106
- throw new Error(`failed to remove ${dest}: ${err.message}`)
107
- }
108
- return { dest, removed: true }
109
- }
110
-
111
- export const _internal = { repoPluginsDir, copyDir }