@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.
@@ -0,0 +1,130 @@
1
+ #!/usr/bin/env bash
2
+ # =============================================================================
3
+ # hub-agent ワンライナーインストーラ (Sprint L)
4
+ #
5
+ # Hub の `GET /api/cockpit/agents/install-script` が、このスクリプトに
6
+ # enrollment token + hub url を埋め込んだ personalized 版を返す。
7
+ #
8
+ # 単独使用 (token なし):
9
+ # curl -fsSL https://raw.githubusercontent.com/cocorograph/D00000_hub-agent/main/scripts/install.sh | bash
10
+ #
11
+ # 内容:
12
+ # 1. macOS なら Homebrew (なければ install)
13
+ # 2. tmux + node (なければ install via brew / apt)
14
+ # 3. npm i -g @cocorograph/hub-agent
15
+ # 4. HUB_AGENT_TOKEN が設定されていれば hub-agent enroll を自動実行
16
+ # 5. hub-agent install-service で OS サービス化
17
+ # =============================================================================
18
+
19
+ set -euo pipefail
20
+
21
+ NODE_MIN_MAJOR=20
22
+ PACKAGE_NAME="@cocorograph/hub-agent"
23
+
24
+ color_step() { printf "\033[1;34m==> %s\033[0m\n" "$1"; }
25
+ color_ok() { printf "\033[1;32m✓ %s\033[0m\n" "$1"; }
26
+ color_warn() { printf "\033[1;33m! %s\033[0m\n" "$1"; }
27
+ color_err() { printf "\033[1;31m✗ %s\033[0m\n" "$1" >&2; }
28
+
29
+ present() { command -v "$1" >/dev/null 2>&1; }
30
+
31
+ ensure_brew() {
32
+ if [[ "$(uname)" != "Darwin" ]]; then return 0; fi
33
+ if present brew; then color_ok "brew already installed"; return 0; fi
34
+ color_step "Homebrew をインストール"
35
+ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
36
+ # Apple Silicon の brew はデフォルト PATH に入らないので追加
37
+ if [[ -d /opt/homebrew/bin ]]; then
38
+ export PATH="/opt/homebrew/bin:$PATH"
39
+ fi
40
+ present brew || { color_err "brew install failed"; exit 1; }
41
+ }
42
+
43
+ ensure_pkg() {
44
+ local cmd="$1"
45
+ local brew_pkg="$2"
46
+ local apt_pkg="${3:-$brew_pkg}"
47
+ if present "$cmd"; then color_ok "$cmd already installed"; return 0; fi
48
+ color_step "$cmd をインストール"
49
+ if [[ "$(uname)" == "Darwin" ]]; then
50
+ brew install "$brew_pkg"
51
+ elif present apt-get; then
52
+ sudo apt-get update -y
53
+ sudo apt-get install -y "$apt_pkg"
54
+ elif present dnf; then
55
+ sudo dnf install -y "$apt_pkg"
56
+ elif present pacman; then
57
+ sudo pacman -S --noconfirm "$apt_pkg"
58
+ else
59
+ color_err "対応するパッケージマネージャ (brew/apt/dnf/pacman) が見つかりません。$cmd を手動で install してください"
60
+ exit 1
61
+ fi
62
+ }
63
+
64
+ ensure_node_version() {
65
+ local v
66
+ v=$(node --version 2>/dev/null | sed 's/^v//' | cut -d. -f1)
67
+ if [[ -z "$v" ]]; then
68
+ ensure_pkg node node nodejs
69
+ return
70
+ fi
71
+ if (( v < NODE_MIN_MAJOR )); then
72
+ color_warn "node $v は古いです (>=${NODE_MIN_MAJOR} 必須)。brew で更新を試みます"
73
+ if [[ "$(uname)" == "Darwin" ]]; then
74
+ brew upgrade node || brew install node
75
+ else
76
+ color_err "Node ${NODE_MIN_MAJOR}+ を手動で install してください (nvm 推奨)"
77
+ exit 1
78
+ fi
79
+ else
80
+ color_ok "node $v"
81
+ fi
82
+ }
83
+
84
+ ensure_global_install() {
85
+ if present hub-agent; then
86
+ local cur
87
+ cur=$(hub-agent --version 2>/dev/null || echo "unknown")
88
+ color_step "hub-agent (現在 $cur) を最新版にアップデート"
89
+ npm install -g "$PACKAGE_NAME"
90
+ else
91
+ color_step "$PACKAGE_NAME を install"
92
+ npm install -g "$PACKAGE_NAME"
93
+ fi
94
+ color_ok "hub-agent $(hub-agent --version)"
95
+ }
96
+
97
+ do_enroll() {
98
+ if [[ -z "${HUB_AGENT_TOKEN:-}" ]]; then
99
+ color_warn "HUB_AGENT_TOKEN が未設定。enroll は skip します"
100
+ color_warn " 手動で実行: hub-agent enroll <token> --hub-url ${HUB_AGENT_URL:-https://api.hub.cocorograph.com}"
101
+ return 0
102
+ fi
103
+ if [[ -f "$HOME/.hub/agent.json" ]]; then
104
+ color_warn "~/.hub/agent.json が既にあります。--force で上書きします"
105
+ hub-agent enroll "$HUB_AGENT_TOKEN" --hub-url "${HUB_AGENT_URL:-https://api.hub.cocorograph.com}" --force
106
+ else
107
+ hub-agent enroll "$HUB_AGENT_TOKEN" --hub-url "${HUB_AGENT_URL:-https://api.hub.cocorograph.com}"
108
+ fi
109
+ }
110
+
111
+ do_install_service() {
112
+ color_step "OS サービスとして自動起動を登録"
113
+ hub-agent install-service
114
+ color_ok "install-service 完了。ログ: ~/.hub/agent.log"
115
+ }
116
+
117
+ main() {
118
+ color_step "hub-agent ワンライナーセットアップを開始"
119
+ ensure_brew
120
+ ensure_pkg tmux tmux tmux
121
+ ensure_node_version
122
+ ensure_global_install
123
+ do_enroll
124
+ do_install_service
125
+ echo ""
126
+ color_ok "セットアップ完了。Hub UI で online 表示を確認してください"
127
+ echo " https://hub.cocorograph.com/user/cockpit/agents"
128
+ }
129
+
130
+ main "$@"
package/src/config.mjs ADDED
@@ -0,0 +1,76 @@
1
+ /**
2
+ * `~/.hub/agent.json` の読み書き。
3
+ *
4
+ * 仕様書: ナレッジ/インフラ/cockpit-hub-hosted-integration-spec (id=6080)
5
+ *
6
+ * パスは毎呼び出しで `os.homedir()` を解決する (テストで HOME を差し替え
7
+ * られるように)。`$HUB_AGENT_CONFIG_DIR` を export しておくと、その値が
8
+ * 優先される (mosh / 専用ユーザー用)。
9
+ */
10
+ import { promises as fs, constants as fsConsts } from "node:fs"
11
+ import path from "node:path"
12
+ import os from "node:os"
13
+
14
+ function resolveConfigDir() {
15
+ if (process.env.HUB_AGENT_CONFIG_DIR) return process.env.HUB_AGENT_CONFIG_DIR
16
+ return path.join(os.homedir(), ".hub")
17
+ }
18
+
19
+ export const paths = {
20
+ get configDir() {
21
+ return resolveConfigDir()
22
+ },
23
+ get configFile() {
24
+ return path.join(resolveConfigDir(), "agent.json")
25
+ },
26
+ get pluginsDir() {
27
+ return path.join(resolveConfigDir(), "plugins")
28
+ },
29
+ }
30
+
31
+ /** ~/.hub を作成する (chmod 700)。 */
32
+ export async function ensureConfigDir() {
33
+ await fs.mkdir(paths.configDir, { recursive: true, mode: 0o700 })
34
+ await fs.mkdir(paths.pluginsDir, { recursive: true, mode: 0o700 })
35
+ }
36
+
37
+ /** 設定が存在するかどうか。 */
38
+ export async function hasConfig() {
39
+ try {
40
+ await fs.access(paths.configFile, fsConsts.R_OK)
41
+ return true
42
+ } catch {
43
+ return false
44
+ }
45
+ }
46
+
47
+ /** 設定を読み込む。無い場合は null を返す。 */
48
+ export async function readConfig() {
49
+ try {
50
+ const raw = await fs.readFile(paths.configFile, "utf-8")
51
+ return JSON.parse(raw)
52
+ } catch (err) {
53
+ if (err.code === "ENOENT") return null
54
+ throw err
55
+ }
56
+ }
57
+
58
+ /**
59
+ * 設定を保存する (chmod 600)。
60
+ *
61
+ * @param {object} config - `{ agent_id, agent_token, hub_url }`
62
+ */
63
+ export async function writeConfig(config) {
64
+ await ensureConfigDir()
65
+ const payload = JSON.stringify(config, null, 2)
66
+ await fs.writeFile(paths.configFile, payload, { mode: 0o600 })
67
+ }
68
+
69
+ /** 設定をクリアする (agent unenroll や revoke 後)。 */
70
+ export async function clearConfig() {
71
+ try {
72
+ await fs.unlink(paths.configFile)
73
+ } catch (err) {
74
+ if (err.code !== "ENOENT") throw err
75
+ }
76
+ }
package/src/enroll.mjs ADDED
@@ -0,0 +1,75 @@
1
+ /**
2
+ * `hub-agent enroll <enrollment_token>` の処理。
3
+ *
4
+ * Hub の POST /api/cockpit/agents/enroll-complete/ に hostname と token を
5
+ * 送って agent_id + agent_token を取得し、~/.hub/agent.json に保存する。
6
+ *
7
+ * 仕様書: ナレッジ/インフラ/cockpit-hub-hosted-integration-spec (id=6080)
8
+ */
9
+ import os from "node:os"
10
+
11
+ import { hasConfig, writeConfig } from "./config.mjs"
12
+
13
+ const DEFAULT_HUB_URL = process.env.HUB_URL || "https://hub.cocorograph.com"
14
+
15
+ /**
16
+ * @param {string} enrollmentToken
17
+ * @param {{ hubUrl?: string, hostname?: string, version?: string, force?: boolean, fetchImpl?: typeof fetch }} [opts]
18
+ */
19
+ export async function enroll(enrollmentToken, opts = {}) {
20
+ if (!enrollmentToken) {
21
+ throw new Error("enrollment_token が指定されていません。")
22
+ }
23
+ if (!opts.force && (await hasConfig())) {
24
+ throw new Error(
25
+ "既に enroll 済みです。再 enroll するには --force を付けてください (旧 agent は Hub 側で revoke してから実行)。"
26
+ )
27
+ }
28
+ const hubUrl = (opts.hubUrl || DEFAULT_HUB_URL).replace(/\/+$/, "")
29
+ const hostname = opts.hostname || os.hostname()
30
+ const version = opts.version || "0.1.0"
31
+
32
+ const fetchImpl = opts.fetchImpl || globalThis.fetch
33
+ if (typeof fetchImpl !== "function") {
34
+ throw new Error("global fetch がありません。Node 20+ で実行してください。")
35
+ }
36
+
37
+ const res = await fetchImpl(`${hubUrl}/api/cockpit/agents/enroll-complete/`, {
38
+ method: "POST",
39
+ headers: { "content-type": "application/json" },
40
+ body: JSON.stringify({
41
+ enrollment_token: enrollmentToken,
42
+ hostname,
43
+ version,
44
+ }),
45
+ })
46
+
47
+ const data = await safeJson(res)
48
+ if (!res.ok) {
49
+ const detail = data?.detail || JSON.stringify(data) || `HTTP ${res.status}`
50
+ throw new Error(`enroll failed: ${detail}`)
51
+ }
52
+ if (!data?.agent_id || !data?.agent_token) {
53
+ throw new Error(`enroll response 不正: ${JSON.stringify(data)}`)
54
+ }
55
+
56
+ await writeConfig({
57
+ agent_id: data.agent_id,
58
+ agent_token: data.agent_token,
59
+ hub_url: hubUrl,
60
+ })
61
+
62
+ return {
63
+ agent_id: data.agent_id,
64
+ is_primary: !!data.is_primary,
65
+ hub_url: hubUrl,
66
+ }
67
+ }
68
+
69
+ async function safeJson(res) {
70
+ try {
71
+ return await res.json()
72
+ } catch {
73
+ return null
74
+ }
75
+ }
package/src/hooks.mjs ADDED
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Plugin hook 名と signatures の型定義 (JSDoc)。
3
+ *
4
+ * 仕様書: ナレッジ/インフラ/cockpit-hub-hosted-integration-spec (id=6080)
5
+ * の「プラグイン機構 > Hook 一覧」を参照。
6
+ *
7
+ * すべての hook は async で、ハンドラ毎に optional。最初に non-null を
8
+ * 返した plugin が採用される (`plugin-loader.mjs` 側で chain 実行)。
9
+ */
10
+
11
+ /**
12
+ * @typedef {Object} HookContext
13
+ * @property {import('pino').Logger} logger
14
+ * @property {Object} config - 全体設定 (~/.hub/agent.json)
15
+ */
16
+
17
+ export const HOOK_NAMES = Object.freeze([
18
+ "onAgentStart",
19
+ "onAgentStop",
20
+ "interceptPtySpawn",
21
+ "interceptTmuxExec",
22
+ "interceptSessionCreate",
23
+ "interceptSessionKill",
24
+ "filterSessions",
25
+ "transformChatInput",
26
+ "transformStatusDetection",
27
+ ])
28
+
29
+ /**
30
+ * @typedef {Object} PtySpawnArgs
31
+ * @property {string} sessionName
32
+ * @property {number} cols
33
+ * @property {number} rows
34
+ * @property {Record<string, string>} env
35
+ */
36
+
37
+ /**
38
+ * @typedef {Object} PtySpawnDecision
39
+ * @property {string} command
40
+ * @property {string[]} args
41
+ * @property {Record<string, string>} [env]
42
+ */
43
+
44
+ /**
45
+ * @typedef {Object} TmuxExecArgs
46
+ * @property {string[]} args
47
+ * @property {Record<string, string>} env
48
+ */
49
+
50
+ /**
51
+ * @typedef {Object} TmuxExecDecision
52
+ * @property {string} command
53
+ * @property {string[]} args
54
+ * @property {Record<string, string>} [env]
55
+ */
56
+
57
+ /**
58
+ * @typedef {Object} Plugin
59
+ * @property {string} name
60
+ * @property {{
61
+ * onAgentStart?: (ctx: HookContext) => Promise<void>,
62
+ * onAgentStop?: (ctx: HookContext) => Promise<void>,
63
+ * interceptPtySpawn?: (ctx: HookContext & PtySpawnArgs) => Promise<PtySpawnDecision | null>,
64
+ * interceptTmuxExec?: (ctx: HookContext & TmuxExecArgs) => Promise<TmuxExecDecision | null>,
65
+ * }} hooks
66
+ */
67
+
68
+ export default { HOOK_NAMES }