@cocorograph/hub-agent 0.6.67 → 0.6.69

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/claude-md.mjs +145 -14
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cocorograph/hub-agent",
3
- "version": "0.6.67",
3
+ "version": "0.6.69",
4
4
  "description": "Hub Hosted Cockpit のローカル常駐 agent。Hub と outbound WSS で接続し、ローカルの tmux/pty を中継する。",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
package/src/claude-md.mjs CHANGED
@@ -51,7 +51,127 @@ async function fetchDirector({ hubUrl, accessToken, dirName, fetchImpl }) {
51
51
  }
52
52
  }
53
53
 
54
- function renderWithDirector(dirName, director) {
54
+ /**
55
+ * dir 名から GitRepository 一覧を取得する。
56
+ *
57
+ * 旧 Hub backend(GitRepository モデル未導入)では 404 を返すので、
58
+ * その場合は空配列扱いで後方互換を維持する。
59
+ *
60
+ * @returns {Promise<Array<object>>}
61
+ */
62
+ async function fetchRepositories({ hubUrl, accessToken, dirName, fetchImpl }) {
63
+ const f = fetchImpl || globalThis.fetch
64
+ if (!f) return []
65
+ const url =
66
+ `${hubUrl.replace(/\/+$/, "")}/api/git-repositories/by-dir/` +
67
+ `${encodeURIComponent(dirName)}/`
68
+ try {
69
+ const res = await f(url, {
70
+ headers: { Authorization: `Bearer ${accessToken}` },
71
+ })
72
+ if (!res.ok) return []
73
+ const payload = await res.json()
74
+ return Array.isArray(payload?.repositories) ? payload.repositories : []
75
+ } catch {
76
+ return []
77
+ }
78
+ }
79
+
80
+ /**
81
+ * GitRepository 配列を CLAUDE.md のクローンレス作業手順セクションに描画する。
82
+ *
83
+ * リポジトリが 1 件もない場合は空文字列を返し、CLAUDE.md には何も追記しない。
84
+ */
85
+ function renderRepositorySection(repositories, dirName = "") {
86
+ if (!Array.isArray(repositories) || repositories.length === 0) return ""
87
+
88
+ const lines = ["## Cockpit クローンレス開発フロー", ""]
89
+ lines.push(
90
+ "この workspace ディレクトリには Git リポジトリを常設しません。",
91
+ "コードを修正するときは、毎回一時ディレクトリにチェックアウトして作業します。",
92
+ "",
93
+ )
94
+
95
+ for (const repo of repositories) {
96
+ const repoUrl = (repo?.repo_url || "").trim()
97
+ if (!repoUrl) continue
98
+ const branch = (repo?.default_branch || "main").trim()
99
+ const deployDisplay = (repo?.deploy_method_display || "").trim()
100
+ const deployNotes = (repo?.deploy_notes || "").trim()
101
+ const stagingName = (repo?.staging_server_name || "").trim()
102
+ const productionName = (repo?.production_server_name || "").trim()
103
+ const repoLabel = (repo?.name || "").trim()
104
+ const heading = repoLabel
105
+ ? `### ${repoUrl} — ${repoLabel}`
106
+ : `### ${repoUrl}`
107
+
108
+ lines.push(heading)
109
+ lines.push("")
110
+ lines.push(`- **デフォルトブランチ**: \`${branch}\``)
111
+ if (deployDisplay) lines.push(`- **デプロイ方式**: ${deployDisplay}`)
112
+ if (stagingName) lines.push(`- **ステージング**: ${stagingName}`)
113
+ if (productionName) lines.push(`- **本番**: ${productionName}`)
114
+ lines.push("")
115
+
116
+ lines.push("**作業手順(クローンレス)**:")
117
+ lines.push("")
118
+ lines.push("```bash")
119
+ lines.push("TMPDIR=$(mktemp -d)")
120
+ lines.push(`gh repo clone ${repoUrl} "$TMPDIR" -- --depth 1 --filter=blob:none --no-checkout`)
121
+ lines.push(`cd "$TMPDIR" && git fetch origin ${branch} && git checkout -b feat/<your-change> origin/${branch}`)
122
+ lines.push("# ファイル編集 → 動作確認")
123
+ lines.push('git commit -am "<message>"')
124
+ lines.push("git push -u origin HEAD")
125
+ lines.push("gh pr create --base " + branch + " --fill")
126
+ lines.push("# 作業終了後")
127
+ lines.push('cd / && rm -rf "$TMPDIR"')
128
+ lines.push("```")
129
+ lines.push("")
130
+
131
+ if (deployNotes) {
132
+ lines.push("**デプロイ手順メモ**:")
133
+ lines.push("")
134
+ lines.push("```")
135
+ lines.push(deployNotes)
136
+ lines.push("```")
137
+ lines.push("")
138
+ }
139
+
140
+ // デプロイ接続情報の安全な取得手順(秘匿値を会話に出さない)。
141
+ if (stagingName || productionName) {
142
+ const envExample = productionName ? "production" : "staging"
143
+ lines.push("**デプロイ接続情報の取得(秘匿値を会話に出さない)**:")
144
+ lines.push("")
145
+ lines.push("```bash")
146
+ lines.push(
147
+ "# 秘密鍵を ~/.ssh/hub_deploy/ に書き出し、ssh コマンドだけ受け取る",
148
+ )
149
+ lines.push(
150
+ `python3 ~/.claude/scripts/hub_helper.py deploy_creds ${dirName || "<dir_name>"} --env ${envExample}`,
151
+ )
152
+ lines.push("```")
153
+ lines.push("")
154
+ lines.push(
155
+ "返り値の `ssh_command` をそのまま使って接続します。`key_file` は",
156
+ "chmod 600 で書き出されます。パスワード認証のサーバーは `password_set: true`",
157
+ "だけ返るので、その場合は Hub の「サーバー情報」タブから人手で取得します。",
158
+ "",
159
+ )
160
+ }
161
+ }
162
+
163
+ lines.push("### デプロイ前の必須ガード", "")
164
+ lines.push(
165
+ "- **本番反映の前に必ず現状バックアップを取る**(DB ダンプ / 対象ディレクトリの退避)。",
166
+ "- ステージングで動作確認してから本番へ。",
167
+ "- 破壊的操作(`reset --hard` / `rm -rf` / DB 操作)の前に対象を確認する。",
168
+ "- Git 認証はローカルの `gh` CLI / SSH 鍵を使用します。",
169
+ "",
170
+ )
171
+ return lines.join("\n")
172
+ }
173
+
174
+ function renderWithDirector(dirName, director, repositorySection = "") {
55
175
  const fm = director?.frontmatter || {}
56
176
  const client = (fm.client || "").trim()
57
177
  const domain = (fm.domain || "").trim()
@@ -94,6 +214,10 @@ function renderWithDirector(dirName, director) {
94
214
  }
95
215
  lines.push("")
96
216
 
217
+ if (repositorySection) {
218
+ lines.push(repositorySection)
219
+ }
220
+
97
221
  lines.push("## 初期化")
98
222
  lines.push("")
99
223
  lines.push(
@@ -109,8 +233,8 @@ function renderWithDirector(dirName, director) {
109
233
  return lines.join("\n")
110
234
  }
111
235
 
112
- function renderPlaceholder(dirName) {
113
- return [
236
+ function renderPlaceholder(dirName, repositorySection = "") {
237
+ const sections = [
114
238
  "# CLAUDE.md",
115
239
  "",
116
240
  "This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.",
@@ -119,12 +243,18 @@ function renderPlaceholder(dirName) {
119
243
  "",
120
244
  `- **Hub Director**: \`[[${dirName}/_director]]\` — 案件の進行・意思決定・直近ログ (未作成)`,
121
245
  "",
246
+ ]
247
+ if (repositorySection) {
248
+ sections.push(repositorySection)
249
+ }
250
+ sections.push(
122
251
  "## 初期化",
123
252
  "",
124
253
  "これは hub-agent の自動生成プレースホルダです。Hub director も未作成です。",
125
254
  "詳細を生成するには、このリポジトリで `claude` を起動して `/init-claude-md` スキルを実行してください。",
126
255
  "",
127
- ].join("\n")
256
+ )
257
+ return sections.join("\n")
128
258
  }
129
259
 
130
260
  /**
@@ -166,18 +296,19 @@ export async function ensureClaudeMd({
166
296
  const token =
167
297
  accessToken === undefined ? await loadAccessToken() : accessToken || null
168
298
  const url = hubUrl || DEFAULT_HUB_API
169
- const director = token
170
- ? await fetchDirector({
171
- hubUrl: url,
172
- accessToken: token,
173
- dirName,
174
- fetchImpl,
175
- })
176
- : null
299
+ // director repositories は独立して取れるので並列フェッチする。
300
+ // どちらも失敗時は null / [] にフォールバックする (token なしの場合も同様)。
301
+ const [director, repositories] = token
302
+ ? await Promise.all([
303
+ fetchDirector({ hubUrl: url, accessToken: token, dirName, fetchImpl }),
304
+ fetchRepositories({ hubUrl: url, accessToken: token, dirName, fetchImpl }),
305
+ ])
306
+ : [null, []]
177
307
 
308
+ const repositorySection = renderRepositorySection(repositories, dirName)
178
309
  const body = director
179
- ? renderWithDirector(dirName, director)
180
- : renderPlaceholder(dirName)
310
+ ? renderWithDirector(dirName, director, repositorySection)
311
+ : renderPlaceholder(dirName, repositorySection)
181
312
  const source = director ? "director" : "placeholder"
182
313
 
183
314
  await fs.writeFile(claudeMdPath, body, "utf-8")