peertable 0.8.54 → 0.8.56
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/README.ja.md +32 -33
- package/README.md +36 -37
- package/package.json +3 -1
- package/room/client.mjs +24 -33
- package/room/server.mjs +1 -1
- package/skill/SKILL.md +69 -94
- package/skill/scripts/aiterm-client.mjs +48 -0
- package/skill/scripts/aiterm-deliver.mjs +12 -1
- package/skill/scripts/change-seat.mjs +86 -0
- package/skill/scripts/change-seat.sh +3 -276
- package/skill/scripts/change-seat.test.mjs +129 -0
- package/skill/scripts/cli.mjs +77 -0
- package/skill/scripts/doctor.mjs +53 -0
- package/skill/scripts/doctor.sh +2 -213
- package/skill/scripts/ensure-all-bridges.mjs +42 -0
- package/skill/scripts/ensure-all-bridges.sh +3 -80
- package/skill/scripts/ensure-bridge.mjs +11 -0
- package/skill/scripts/ensure-bridge.sh +2 -95
- package/skill/scripts/ensure-codex-room-mcp.mjs +1 -1
- package/skill/scripts/ensure-project-runtime.mjs +85 -0
- package/skill/scripts/ensure-project-runtime.sh +2 -11
- package/skill/scripts/ensure-project-runtime.test.mjs +40 -0
- package/skill/scripts/install-skill.mjs +89 -0
- package/skill/scripts/install-skill.test.mjs +84 -0
- package/skill/scripts/launch-seat.mjs +156 -0
- package/skill/scripts/launch-seat.sh +3 -797
- package/skill/scripts/launch-seat.test.mjs +159 -0
- package/skill/scripts/leave-seat.mjs +42 -0
- package/skill/scripts/leave-seat.sh +3 -92
- package/skill/scripts/leave-seat.test.mjs +53 -0
- package/skill/scripts/legacy-entry.mjs +33 -0
- package/skill/scripts/platform/windows/resolve-lattice-command.mjs +12 -1
- package/skill/scripts/project-runtime.mjs +95 -0
- package/skill/scripts/project-runtime.test.mjs +29 -0
- package/skill/scripts/project-scaffold.mjs +179 -0
- package/skill/scripts/project-scaffold.test.mjs +129 -0
- package/skill/scripts/refresh-seat-identity.mjs +11 -8
- package/skill/scripts/remove-managed-room-mcp.mjs +9 -4
- package/skill/scripts/resume.sh +2 -225
- package/skill/scripts/room-api.mjs +28 -0
- package/skill/scripts/room-public-session.test.mjs +73 -0
- package/skill/scripts/runtime-contract.test.mjs +13 -146
- package/skill/scripts/runtime-launch-command.mjs +19 -0
- package/skill/scripts/runtime-launch-command.test.mjs +32 -0
- package/skill/scripts/seat-approval.mjs +24 -0
- package/skill/scripts/seat-approval.test.mjs +43 -0
- package/skill/scripts/seat-identity.mjs +2 -71
- package/skill/scripts/seat-observer.mjs +54 -0
- package/skill/scripts/seat-observer.test.mjs +58 -0
- package/skill/scripts/seat-session.mjs +17 -0
- package/skill/scripts/seat-status-bridge.mjs +33 -206
- package/skill/scripts/seat-usage.mjs +3 -334
- package/skill/scripts/setup.sh +3 -215
- package/skill/scripts/teardown.mjs +113 -0
- package/skill/scripts/teardown.sh +3 -410
- package/skill/scripts/teardown.test.mjs +62 -0
- package/skill/scripts/upgrade-team-assets.mjs +268 -0
- package/skill/scripts/upgrade-team-assets.sh +2 -282
- package/skill/scripts/wakeup-bridge.mjs +24 -32
- package/skill/scripts/wakeup-delivery.mjs +7 -18
- package/skill/templates/charter.md +1 -1
- package/skill/templates/member-standalone.md +1 -1
- package/skill/templates/member.md +3 -3
- package/skill/scripts/agent-pane-status.mjs +0 -6
- package/skill/scripts/aiterm-configure.mjs +0 -31
- package/skill/scripts/aiterm-launch.mjs +0 -64
- package/skill/scripts/aiterm-send.mjs +0 -22
- package/skill/scripts/claude-dialog.mjs +0 -26
- package/skill/scripts/codex-dialog.mjs +0 -67
- package/skill/scripts/platform/windows/build-bridge-command.mjs +0 -52
- package/skill/scripts/seat-input.mjs +0 -36
- package/skill/scripts/tmux-at.bash +0 -22
- package/skill/scripts/tmux-socket.mjs +0 -19
- package/skill/scripts/vendors/grok/pane-status.mjs +0 -30
package/skill/scripts/doctor.sh
CHANGED
|
@@ -1,215 +1,4 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
#
|
|
3
|
-
# usage: doctor.sh <project_dir> [--repair]
|
|
4
|
-
#
|
|
5
|
-
# 判定するのは5点だけ:
|
|
6
|
-
# 1. room サーバー到達性(GET /api/<room>/summary)
|
|
7
|
-
# 2. 台帳(room member 行)の素性・本人性の完全性
|
|
8
|
-
# 3. 各席の tmux セッション(peer-<name>)の存在と、席file の pid+lstart による本人性
|
|
9
|
-
# 4. 2ブリッジ(wakeup / seat-status)の record 生存と、record にある識別情報だけで
|
|
10
|
-
# 判定できる範囲の本人性・鮮度(判定できないものは「判定不能」と正直に出す。偽の生存判定を作らない)
|
|
11
|
-
# 5. Lattice 併用モード(mode=lattice)なら `lattice status --json` の state / active_runs
|
|
12
|
-
#
|
|
13
|
-
# 各行は OK / NG / REPAIRED / 判定不能 のいずれかで始まる。--repair は NG のブリッジだけ
|
|
14
|
-
# ensure-bridge.sh で立て直す。**席の再起動はしない**——席は人の判断が要るので、NG 表示に
|
|
15
|
-
# launch-seat.sh を促す一言を添えるだけに留める。
|
|
16
|
-
#
|
|
17
|
-
# 終了コード: 全 OK(判定不能を含む)= 0、NG が1件でもあれば 1(--repair で全部直れば 0)。
|
|
2
|
+
# 既存の引数を共通の製品入口へ渡す。
|
|
18
3
|
set -euo pipefail
|
|
19
|
-
|
|
20
|
-
proj="${1:-}"
|
|
21
|
-
repair=false
|
|
22
|
-
if [ "${2:-}" = "--repair" ]; then repair=true; fi
|
|
23
|
-
if [ -z "$proj" ]; then
|
|
24
|
-
echo "usage: doctor.sh <project_dir> [--repair]" >&2
|
|
25
|
-
exit 1
|
|
26
|
-
fi
|
|
27
|
-
proj=$(cd "$proj" && pwd)
|
|
28
|
-
script_dir=$(cd "$(dirname "$0")" && pwd)
|
|
29
|
-
setup="$proj/.team/setup-state.json"
|
|
30
|
-
if [ ! -f "$setup" ]; then
|
|
31
|
-
echo "NG .team/setup-state.json が無い(${proj} は peertable setup 済みか確認せよ)" >&2
|
|
32
|
-
exit 1
|
|
33
|
-
fi
|
|
34
|
-
|
|
35
|
-
DOCTOR_PROJ="$proj" DOCTOR_SCRIPT_DIR="$script_dir" DOCTOR_REPAIR="$repair" \
|
|
36
|
-
node --input-type=module <<'NODE'
|
|
37
|
-
import { existsSync, readFileSync, readdirSync } from 'node:fs'
|
|
38
|
-
import { execFileSync, spawnSync } from 'node:child_process'
|
|
39
|
-
import { join } from 'node:path'
|
|
40
|
-
import { pathToFileURL } from 'node:url'
|
|
41
|
-
|
|
42
|
-
const proj = process.env.DOCTOR_PROJ
|
|
43
|
-
const scriptDir = process.env.DOCTOR_SCRIPT_DIR
|
|
44
|
-
const repair = process.env.DOCTOR_REPAIR === 'true'
|
|
45
|
-
const team = join(proj, '.team')
|
|
46
|
-
|
|
47
|
-
const { bridgeRecordLive } = await import(pathToFileURL(join(scriptDir, 'bridge-record-live.mjs')))
|
|
48
|
-
const { observePidCommand } = await import(pathToFileURL(join(scriptDir, 'refresh-seat-identity.mjs')))
|
|
49
|
-
const { resolveLatticeInvocation, resolveTmuxSocket, tmuxArgv } = await import(pathToFileURL(join(scriptDir, 'seat-usage.mjs')))
|
|
50
|
-
|
|
51
|
-
let ng = 0
|
|
52
|
-
function line(level, text) {
|
|
53
|
-
console.log(`${level} ${text}`)
|
|
54
|
-
if (level === 'NG') ng = 1
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const setup = JSON.parse(readFileSync(join(team, 'setup-state.json'), 'utf8'))
|
|
58
|
-
const { room, server_url: url, mode } = setup
|
|
59
|
-
|
|
60
|
-
// 1. room サーバー到達性
|
|
61
|
-
let summary = null
|
|
62
|
-
try {
|
|
63
|
-
const res = await fetch(`${url}/api/${encodeURIComponent(room)}/summary`)
|
|
64
|
-
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
65
|
-
summary = await res.json()
|
|
66
|
-
line('OK', `room 到達: ${url} room=${room} seq=${summary.seq} member_count=${summary.member_count}`)
|
|
67
|
-
} catch (error) {
|
|
68
|
-
line('NG', `room 到達不可: ${url} room=${room}(${error.message})`)
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// members は以降の突合・席チェックにも使う
|
|
72
|
-
let members = []
|
|
73
|
-
let membersOk = false
|
|
74
|
-
try {
|
|
75
|
-
const res = await fetch(`${url}/api/${encodeURIComponent(room)}/members`)
|
|
76
|
-
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
77
|
-
const body = await res.json()
|
|
78
|
-
members = Array.isArray(body.members) ? body.members : []
|
|
79
|
-
membersOk = true
|
|
80
|
-
} catch (error) {
|
|
81
|
-
line('NG', `room members を取得できない(${error.message})`)
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// 2. 台帳の完全性。tmux 席を持つ member(親以外)は本人性欄(pid 等)も台帳に載っている
|
|
85
|
-
// はず(席file は 2026-08-22 廃止・正本は member 行だけ)
|
|
86
|
-
function hasDescriptor(member) {
|
|
87
|
-
return Boolean(member?.observe && typeof member.observe === 'object'
|
|
88
|
-
&& typeof member.observe.tmux_target === 'string' && member.observe.tmux_target.length > 0)
|
|
89
|
-
}
|
|
90
|
-
const seatRows = new Map()
|
|
91
|
-
if (membersOk) {
|
|
92
|
-
const seated = members.filter(hasDescriptor)
|
|
93
|
-
const missingIdentity = seated.filter(m => !Number.isSafeInteger(m.pid) || !m.started_identity || !m.argv_digest)
|
|
94
|
-
for (const m of seated) seatRows.set(m.name, m)
|
|
95
|
-
if (missingIdentity.length === 0) {
|
|
96
|
-
line('OK', `台帳の member 行に素性・本人性が揃っている(${seated.length} 席)`)
|
|
97
|
-
} else {
|
|
98
|
-
line('NG', `台帳の本人性欄が欠けている member: ${missingIdentity.map(m => m.name).join(',')}`)
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// 3. 各席の tmux セッション(peer-<name>)の存在と、台帳の pid+lstart による本人性
|
|
103
|
-
for (const [name, seat] of seatRows) {
|
|
104
|
-
const member = seat
|
|
105
|
-
const socket = member?.observe?.tmux_socket || resolveTmuxSocket(process.env).socket
|
|
106
|
-
const target = member?.observe?.tmux_target || `peer-${name}`
|
|
107
|
-
let sessionExists = false
|
|
108
|
-
try {
|
|
109
|
-
execFileSync('tmux', tmuxArgv(['has-session', '-t', target], { socket }), { stdio: 'ignore' })
|
|
110
|
-
sessionExists = true
|
|
111
|
-
} catch { /* has-session は非ゼロ終了で無しを表す */ }
|
|
112
|
-
if (!sessionExists) {
|
|
113
|
-
line('NG', `席 ${name}: tmux セッション ${target} が無い(launch-seat.sh で立て直す)`)
|
|
114
|
-
continue
|
|
115
|
-
}
|
|
116
|
-
let identity = null
|
|
117
|
-
let identityError = null
|
|
118
|
-
try { identity = observePidCommand(seat.pid) } catch (error) { identityError = error }
|
|
119
|
-
if (identityError) {
|
|
120
|
-
line('NG', `席 ${name}: pid ${seat.pid} を観測できない(${identityError.message})(launch-seat.sh で立て直す)`)
|
|
121
|
-
continue
|
|
122
|
-
}
|
|
123
|
-
if (identity.started_identity !== seat.started_identity) {
|
|
124
|
-
line('NG', `席 ${name}: pid ${seat.pid} は再利用されている(lstart不一致・本人ではない)(launch-seat.sh で立て直す)`)
|
|
125
|
-
continue
|
|
126
|
-
}
|
|
127
|
-
line('OK', `席 ${name}: tmux ${target} 生存・pid ${seat.pid} 本人性確認`)
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// 4. 2ブリッジ(run-bridge は 2026-08-22 退役)。record の形式が違うので判定できる範囲だけ判定する
|
|
131
|
-
function judgeWakeup() {
|
|
132
|
-
const path = join(team, 'wakeup-bridge.json')
|
|
133
|
-
if (!existsSync(path)) return { level: 'NG', text: 'wakeup-bridge: record が無い(起動していない)' }
|
|
134
|
-
const record = JSON.parse(readFileSync(path, 'utf8'))
|
|
135
|
-
if (bridgeRecordLive(record)) {
|
|
136
|
-
const age = Math.round((Date.now() - Date.parse(record.last_progress_at)) / 1000)
|
|
137
|
-
return { level: 'OK', text: `wakeup-bridge: 生存 pid=${record.pid} last_progress ${age}秒前` }
|
|
138
|
-
}
|
|
139
|
-
return { level: 'NG', text: `wakeup-bridge: pid=${record.pid} が死んでいるか last_progress_at が古い` }
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function judgeSeatStatusBridge() {
|
|
143
|
-
const path = join(team, 'seat-status-bridge.json')
|
|
144
|
-
if (!existsSync(path)) return { level: 'NG', text: 'seat-status-bridge: record が無い(起動していない)' }
|
|
145
|
-
const record = JSON.parse(readFileSync(path, 'utf8'))
|
|
146
|
-
let alive = false
|
|
147
|
-
try { process.kill(record.pid, 0); alive = true } catch { /* 死んでいる */ }
|
|
148
|
-
if (!alive) return { level: 'NG', text: `seat-status-bridge: pid=${record.pid} が死んでいる` }
|
|
149
|
-
return { level: '判定不能', text: `seat-status-bridge: pid=${record.pid} は生存しているが record に lstart が無く本人性を確認できない` }
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
for (const [name, judge] of [['wakeup', judgeWakeup], ['seat-status', judgeSeatStatusBridge]]) {
|
|
153
|
-
let result = judge()
|
|
154
|
-
if (result.level === 'NG' && repair) {
|
|
155
|
-
const res = spawnSync(join(scriptDir, 'ensure-bridge.sh'), [proj, name], { encoding: 'utf8' })
|
|
156
|
-
if (res.status === 0) {
|
|
157
|
-
const after = judge()
|
|
158
|
-
if (after.level !== 'NG') { line('REPAIRED', `${name}-bridge: 立て直した(${after.text})`); continue }
|
|
159
|
-
line('NG', `${name}-bridge: 立て直したが依然として不健全(${after.text})`)
|
|
160
|
-
continue
|
|
161
|
-
}
|
|
162
|
-
const detail = (res.stderr || res.stdout || '').trim().split('\n').slice(-3).join(' / ')
|
|
163
|
-
line('NG', `${name}-bridge: 立て直しに失敗(${detail || `exit ${res.status}`})`)
|
|
164
|
-
continue
|
|
165
|
-
}
|
|
166
|
-
line(result.level, result.text)
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// 4.5 端末別の装置可用性(silent absence禁止・2026-08-25 オーナー裁定):
|
|
170
|
-
// この端末で「動かない」なら動かないと表示する。黙ってMacだけで動く状態を作らない。
|
|
171
|
-
{
|
|
172
|
-
// job観測(アクティブランプへの預け仕事合成): tmux list-sessions がこの端末で成立するか
|
|
173
|
-
try {
|
|
174
|
-
const probe = spawnSync('tmux', ['list-sessions', '-F', '#{session_name}'], { encoding: 'utf8' })
|
|
175
|
-
if (probe.error) throw probe.error
|
|
176
|
-
line('OK', `job観測: tmux list-sessions 実行可(ランプ合成は有効。ジョブは席のシェルからtmuxセッションで走らせる——名前は自由)`)
|
|
177
|
-
} catch (error) {
|
|
178
|
-
line('NG', `job観測: tmux list-sessions が失敗——預け仕事のランプ合成はこの端末で無効(${String(error.message ?? error).split('\n')[0]})`)
|
|
179
|
-
}
|
|
180
|
-
// env帰属(2026-08-26 オーナー裁定: 帰属は規約名でなくOSの環境変数継承): この端末のtmux/psmuxが
|
|
181
|
-
// update-environment と show-environment を持つか。持たない実装(psmuxの互換欠け等)では
|
|
182
|
-
// 名前fallback(peer-<name>- 前置)だけで動くので、その事実を機械判定で出す。
|
|
183
|
-
try {
|
|
184
|
-
const opts = execFileSync('tmux', tmuxArgv(['show-options', '-g', 'update-environment'], { socket: resolveTmuxSocket(process.env).socket }), { encoding: 'utf8' })
|
|
185
|
-
if (/PEERTABLE_MEMBER/.test(opts)) line('OK', 'env帰属: update-environment に PEERTABLE_MEMBER が載っている(新規セッションへ持ち主が自動継承される)')
|
|
186
|
-
else line('NG', 'env帰属: update-environment に PEERTABLE_MEMBER が無い——launch-seat をこの版で一度通すと設定される。それまで帰属は peer-<name>- 前置の名前fallbackのみ')
|
|
187
|
-
} catch (error) {
|
|
188
|
-
line('NG', `env帰属: show-options が失敗——この端末のtmux/psmuxはenv帰属を判定できず、名前fallbackのみで動作(${String(error.message ?? error).split('\n')[0]})`)
|
|
189
|
-
}
|
|
190
|
-
// 読了ack: server members に read_seq 欄が観測できるか(席が一度もackしていない間は判定できない)
|
|
191
|
-
try {
|
|
192
|
-
const res = JSON.parse(execFileSync('curl', ['-s', `${url}/api/${encodeURIComponent(room)}/members`], { encoding: 'utf8' }))
|
|
193
|
-
const withAck = (res.members ?? []).filter(m => m.read_seq !== undefined).length
|
|
194
|
-
if (withAck > 0) line('OK', `読了ack: server対応を確認(read_seq保持 ${withAck} 席)`)
|
|
195
|
-
else line('判定不能', '読了ack: read_seq を持つ席がまだ無い(旧serverか、席が一度も read_unread→post していないか区別できない)')
|
|
196
|
-
} catch (error) {
|
|
197
|
-
line('NG', `読了ack: members を読めない(${String(error.message ?? error).split('\n')[0]})`)
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// 5. Lattice 併用モードの工程正本
|
|
202
|
-
if (mode === 'lattice') {
|
|
203
|
-
const latticeCli = process.env.LATTICE_CLI || (typeof setup.lattice_cli === 'string' && setup.lattice_cli) || 'lattice'
|
|
204
|
-
try {
|
|
205
|
-
const invocation = resolveLatticeInvocation(latticeCli, ['status', '--json'])
|
|
206
|
-
const out = execFileSync(invocation.command, invocation.argv, { cwd: proj, encoding: 'utf8', maxBuffer: 8 * 1024 * 1024 })
|
|
207
|
-
const status = JSON.parse(out)
|
|
208
|
-
line('OK', `Lattice: state=${status.state} active_runs=${(status.active_runs ?? []).length}`)
|
|
209
|
-
} catch (error) {
|
|
210
|
-
line('NG', `Lattice status を取得できない(${String(error.message ?? error).split('\n')[0]})`)
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
process.exit(ng)
|
|
215
|
-
NODE
|
|
4
|
+
exec node "$(dirname "$0")/legacy-entry.mjs" doctor "$@"
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// 既存のsetup記録があるprojectだけを巡回する。npm導入からは呼ばない。
|
|
3
|
+
import { existsSync, readdirSync, statSync, writeFileSync, appendFileSync } from 'node:fs'
|
|
4
|
+
import { homedir } from 'node:os'
|
|
5
|
+
import { join, resolve } from 'node:path'
|
|
6
|
+
import { AitermClient } from './aiterm-client.mjs'
|
|
7
|
+
import { RoomApi } from './room-api.mjs'
|
|
8
|
+
import { ensureProjectRuntime } from './ensure-project-runtime.mjs'
|
|
9
|
+
import { resumeProject } from './project-runtime.mjs'
|
|
10
|
+
import { readSetup } from './project-scaffold.mjs'
|
|
11
|
+
import { findSeatSession } from './seat-session.mjs'
|
|
12
|
+
|
|
13
|
+
const root = resolve(process.argv[2] || join(homedir(), 'Developer'))
|
|
14
|
+
const aiterm = new AitermClient()
|
|
15
|
+
let failed = false
|
|
16
|
+
try {
|
|
17
|
+
const sessions = await aiterm.sessions(['PEERTABLE_MEMBER', 'PEERTABLE_ROOM'])
|
|
18
|
+
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
19
|
+
if (!entry.isDirectory()) continue
|
|
20
|
+
const project = join(root, entry.name)
|
|
21
|
+
if (!existsSync(join(project, '.team', 'setup-state.json'))) continue
|
|
22
|
+
try {
|
|
23
|
+
const state = readSetup(project)
|
|
24
|
+
await ensureProjectRuntime(project, { aiterm })
|
|
25
|
+
const members = await new RoomApi(state).members()
|
|
26
|
+
const missing = members.filter(member => member.delivery?.kind !== 'parent_watch' && (member.harness ?? member.vendor)
|
|
27
|
+
&& !findSeatSession(member, sessions, state.room))
|
|
28
|
+
if (!missing.length) continue
|
|
29
|
+
const lock = join(project, '.team', 'seat-revive.lock')
|
|
30
|
+
if (existsSync(lock) && Date.now() - statSync(lock).mtimeMs < 600_000) continue
|
|
31
|
+
writeFileSync(lock, `${new Date().toISOString()}\n`)
|
|
32
|
+
console.error(`席消失を検知: ${project}: ${missing.map(member => member.name).join(', ')}。resumeで再開します`)
|
|
33
|
+
const result = await resumeProject({ project })
|
|
34
|
+
appendFileSync(join(project, '.team', 'seat-revive.log'), `${JSON.stringify(result)}\n`)
|
|
35
|
+
} catch (error) {
|
|
36
|
+
failed = true
|
|
37
|
+
console.error(`PEERTABLE_PATROL_FAILED: ${project}: ${error.message}`)
|
|
38
|
+
appendFileSync(join(project, '.team', 'seat-revive.log'), `${new Date().toISOString()} ${error.message}\n`)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
} finally { await aiterm.close() }
|
|
42
|
+
if (failed) process.exitCode = 1
|
|
@@ -1,81 +1,4 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
#
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
# 対象卓の発見は実物(.team/setup-state.json の存在)だけを根拠にする。登録簿は持たない。
|
|
6
|
-
set -u
|
|
7
|
-
# launchd環境にはTMPDIRが無く、tmux socketの既定解決($TMPDIR/claude-tmux-sockets/…)が
|
|
8
|
-
# /tmp側へ落ちて全ensureが失敗する(実被弾 2026-08-29)。macOSの正規per-user tempを自前導出する。
|
|
9
|
-
if [ -z "${TMPDIR:-}" ] && command -v getconf >/dev/null; then
|
|
10
|
-
TMPDIR="$(getconf DARWIN_USER_TEMP_DIR 2>/dev/null || true)"
|
|
11
|
-
[ -n "$TMPDIR" ] && export TMPDIR
|
|
12
|
-
fi
|
|
13
|
-
# launchd環境には書込tokenが無い。bridgeのreceipt書込とresumeの席起動に必要なので、
|
|
14
|
-
# 標準の置き場から読み込む(無ければ従来どおり進み、書込systemは自分で degrade を報告する)
|
|
15
|
-
[ -f "$HOME/.config/peertable.env" ] && . "$HOME/.config/peertable.env"
|
|
16
|
-
scripts="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
|
|
17
|
-
for state in /Users/kite/Developer/*/.team/setup-state.json; do
|
|
18
|
-
[ -f "$state" ] || continue
|
|
19
|
-
proj="$(dirname "$(dirname "$state")")"
|
|
20
|
-
bash "$scripts/ensure-bridge.sh" "$proj" seat-status >/dev/null 2>&1 \
|
|
21
|
-
|| echo "$(date -u +%FT%TZ) ensure失敗: seat-status $proj"
|
|
22
|
-
bash "$scripts/ensure-bridge.sh" "$proj" alarm >/dev/null 2>&1 \
|
|
23
|
-
|| echo "$(date -u +%FT%TZ) ensure失敗: alarm $proj"
|
|
24
|
-
# wakeup は配達先の席名一覧が要る。room 台帳から現行メンバーを実物で読む
|
|
25
|
-
server="$(python3 -c "import json;print(json.load(open('$state'))['server_url'])" 2>/dev/null)"
|
|
26
|
-
room="$(python3 -c "import json;print(json.load(open('$state'))['room'])" 2>/dev/null)"
|
|
27
|
-
if [ -n "$server" ] && [ -n "$room" ]; then
|
|
28
|
-
seats="$(curl -s --max-time 10 "$server/api/$room/members" | python3 -c "
|
|
29
|
-
import json,sys
|
|
30
|
-
try:
|
|
31
|
-
ms=json.load(sys.stdin).get('members',[])
|
|
32
|
-
except Exception:
|
|
33
|
-
ms=[]
|
|
34
|
-
print(' '.join(m['name'] for m in ms if m.get('observe') or m.get('harness')))" 2>/dev/null)"
|
|
35
|
-
if [ -n "$seats" ]; then
|
|
36
|
-
# shellcheck disable=SC2086
|
|
37
|
-
bash "$scripts/ensure-bridge.sh" "$proj" wakeup $seats >/dev/null 2>&1 \
|
|
38
|
-
|| echo "$(date -u +%FT%TZ) ensure失敗: wakeup $proj ($seats)"
|
|
39
|
-
fi
|
|
40
|
-
fi
|
|
41
|
-
done
|
|
42
|
-
|
|
43
|
-
# ---- 席の自動蘇生(オーナー恒久裁定「死んだら自動で起こせ。毎回手で復活させるな」)----
|
|
44
|
-
# tmux server死で全席が消える事故が反復した(実被弾 2026-08-29〜30に3回・原因未特定)。
|
|
45
|
-
# 台帳に居るのにtmux sessionが無い席を検知し、**工程が残っている時だけ**resumeで再着席させる。
|
|
46
|
-
# 仕事ゼロの時は起こさない(quotaを浪費しない。SEAT_TUI_GONE通知が親へ届く経路は別に生きている)。
|
|
47
|
-
# 二重起動はresume側のlaunch-seatが同名席を処理するが、周期の重複実行はlockで抑止する。
|
|
48
|
-
for state in /Users/kite/Developer/*/.team/setup-state.json; do
|
|
49
|
-
[ -f "$state" ] || continue
|
|
50
|
-
proj="$(dirname "$(dirname "$state")")"
|
|
51
|
-
lock="$proj/.team/seat-revive.lock"
|
|
52
|
-
# 10分cooldown(前回の蘇生が走っていれば触らない)
|
|
53
|
-
if [ -f "$lock" ] && [ -n "$(find "$lock" -mmin -10 2>/dev/null)" ]; then continue; fi
|
|
54
|
-
server="$(python3 -c "import json;print(json.load(open('$state'))['server_url'])" 2>/dev/null)"
|
|
55
|
-
room="$(python3 -c "import json;print(json.load(open('$state'))['room'])" 2>/dev/null)"
|
|
56
|
-
[ -n "$server" ] && [ -n "$room" ] || continue
|
|
57
|
-
sockprefix=$(node "$scripts/tmux-socket.mjs" --prefix 2>/dev/null) || continue
|
|
58
|
-
# shellcheck disable=SC2206
|
|
59
|
-
conn=($sockprefix)
|
|
60
|
-
dead=""
|
|
61
|
-
for seat in $(curl -s --max-time 10 "$server/api/$room/members" | python3 -c "
|
|
62
|
-
import json,sys
|
|
63
|
-
try: ms=json.load(sys.stdin).get('members',[])
|
|
64
|
-
except Exception: ms=[]
|
|
65
|
-
print(' '.join(m['name'] for m in ms if m.get('harness')))" 2>/dev/null); do
|
|
66
|
-
tmux "${conn[@]}" has-session -t "peer-$seat" 2>/dev/null || dead="$dead $seat"
|
|
67
|
-
done
|
|
68
|
-
[ -n "$dead" ] || continue
|
|
69
|
-
work=$(cd "$proj" && lattice todo status --json 2>/dev/null | python3 -c "
|
|
70
|
-
import json,sys
|
|
71
|
-
try: d=json.load(sys.stdin)
|
|
72
|
-
except Exception: print(0); raise SystemExit
|
|
73
|
-
print(len(d.get('next_ready',[]))+len(d.get('active_set',[]))+len(d.get('audit_pending',[])))" 2>/dev/null)
|
|
74
|
-
# 工程ゼロでも起こす(オーナー裁定 2026-08-30「席全滅で待機でもないのは何もログ取れない」)。
|
|
75
|
-
# 空の卓は障害の再現条件と席側の証人(pane log・rollout)を失う。蘇生コストは席あたり
|
|
76
|
-
# 待機宣言1ターンで、観測体制の維持が勝る。
|
|
77
|
-
date -u +%FT%TZ > "$lock"
|
|
78
|
-
echo "$(date -u +%FT%TZ) 席消失を検知($proj:$dead / 残工程 ${work:-0})。resumeで自動蘇生する"
|
|
79
|
-
(cd "$proj" && bash "$scripts/resume.sh" "$proj" >> "$proj/.team/seat-revive.log" 2>&1) \
|
|
80
|
-
|| echo "$(date -u +%FT%TZ) 自動蘇生に失敗: $proj(seat-revive.log参照)"
|
|
81
|
-
done
|
|
2
|
+
# 既存projectの任意巡回。npm導入からは呼ばない。
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
exec node "$(dirname "$0")/ensure-all-bridges.mjs" "$@"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { ensureBridge } from './ensure-project-runtime.mjs'
|
|
3
|
+
const [project, kind, ...args] = process.argv.slice(2)
|
|
4
|
+
try {
|
|
5
|
+
console.log(JSON.stringify(await ensureBridge(project, kind, {
|
|
6
|
+
force: args.includes('--force'), args: args.filter(arg => arg !== '--force'),
|
|
7
|
+
})))
|
|
8
|
+
} catch (error) {
|
|
9
|
+
console.error(`${error.code ?? 'PEERTABLE_RUNTIME_FAILED'}: ${error.message}`)
|
|
10
|
+
process.exitCode = 1
|
|
11
|
+
}
|
|
@@ -1,97 +1,4 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
#
|
|
2
|
+
# 全OS共通の公開API実装へ渡す旧入口。
|
|
3
3
|
set -euo pipefail
|
|
4
|
-
|
|
5
|
-
proj="$1"; name="$2"; shift 2
|
|
6
|
-
case "$name" in seat-status) script="seat-status-bridge.mjs" ;; wakeup) script="wakeup-bridge.mjs" ;; alarm) script="alarm-bridge.mjs" ;; run) echo "ENSURE_BRIDGE_RETIRED: run-bridge は退役した(2026-08-22)。介入は席が自分の Lattice コマンド応答で受け取る" >&2; exit 1 ;; *) echo "usage: ensure-bridge.sh <project> <seat-status|wakeup|alarm> [args...]" >&2; exit 1 ;; esac
|
|
7
|
-
scripts_dir=$(cd "$(dirname "$0")" && pwd -P)
|
|
8
|
-
repo=$(cd "$scripts_dir/../.." && pwd -P)
|
|
9
|
-
peertable_version=$(node -p "require(process.argv[1]).version" "$repo/package.json")
|
|
10
|
-
peertable_runtime_digest=$(node "$scripts_dir/runtime-digest.mjs")
|
|
11
|
-
team="$proj/.team"; record="$team/$name-bridge.json"; log="$team/$name-bridge.log"
|
|
12
|
-
force=false
|
|
13
|
-
if [ "${1:-}" = "--force" ]; then force=true; shift; fi
|
|
14
|
-
if [ $# -eq 0 ] && [ -f "$record" ]; then
|
|
15
|
-
saved=()
|
|
16
|
-
while IFS= read -r arg; do saved+=("$arg"); done < <(node -e 'const x=require(process.argv[1]); for (const a of x.args||[]) console.log(a)' "$record")
|
|
17
|
-
# macOS 標準 bash 3.2 の set -u では、空配列の展開が unbound variable になる。
|
|
18
|
-
# args が空なら現在の「引数なし」を保ち、値がある時だけ復元する。
|
|
19
|
-
if [ "${#saved[@]}" -gt 0 ]; then
|
|
20
|
-
set -- "${saved[@]}"
|
|
21
|
-
fi
|
|
22
|
-
fi
|
|
23
|
-
if [ -f "$record" ]; then
|
|
24
|
-
if node "$scripts_dir/bridge-record-live.mjs" "$record"; then
|
|
25
|
-
record_version=$(node -e 'const x=require(process.argv[1]);process.stdout.write(x.peertable_version||"")' "$record")
|
|
26
|
-
record_digest=$(node -e 'const x=require(process.argv[1]);process.stdout.write(x.peertable_runtime_digest||"")' "$record")
|
|
27
|
-
if [ "$record_version" = "$peertable_version" ] && [ "$record_digest" = "$peertable_runtime_digest" ]; then exit 0; fi
|
|
28
|
-
echo "${name}-bridge: Peertable ${record_version:-unknown} → ${peertable_version} の現行runtimeへ自動更新する" >&2
|
|
29
|
-
node "$scripts_dir/$script" "$proj" --stop || {
|
|
30
|
-
echo "${name}-bridge: 旧版bridgeを停止できないため更新を中止する" >&2
|
|
31
|
-
exit 1
|
|
32
|
-
}
|
|
33
|
-
fi
|
|
34
|
-
if ! "$force" && grep -q 'WRITE_DENIED' "$log" 2>/dev/null; then echo "${name}-bridge: 前回はWRITE_DENIEDで終了。--forceを指定すること" >&2; exit 1; fi
|
|
35
|
-
fi
|
|
36
|
-
# shellcheck disable=SC1091
|
|
37
|
-
. "$(dirname "$0")/tmux-at.bash"
|
|
38
|
-
sock=$(node "$(dirname "$0")/tmux-socket.mjs")
|
|
39
|
-
room=$(node -e 'process.stdout.write(require(process.argv[1]).room)' "$team/setup-state.json")
|
|
40
|
-
session="peertable-${name}-${room}"
|
|
41
|
-
# **session が在るだけでは常駐が生きている証拠にならない**(中の node だけ死んで殻が残る)。
|
|
42
|
-
# pid/進捗の生存確認を通れずここへ来たsessionは、Windowsでlogを開いたままの可能性がある。
|
|
43
|
-
# log切詰めより先に畳み、EBUSYを発生させない。
|
|
44
|
-
tmux_at kill-session -t "$session" 2>/dev/null || true
|
|
45
|
-
# **死んだ記録をここで消す。** 残すと下の loop が前回の `ready_at` を読んで、
|
|
46
|
-
# 新しい bridge が1バイトも動いていない段階で success を返す——「起動していないのに
|
|
47
|
-
# 起動したと言う」=この supervisor が塞ぐはずの穴そのものになる。
|
|
48
|
-
rm -f "$record"
|
|
49
|
-
if ! node -e 'import { writeFileSync } from "node:fs"; writeFileSync(process.argv[1], "")' "$log"; then
|
|
50
|
-
echo "${name}-bridge: log を切り詰められないので追記する" >&2
|
|
51
|
-
: >> "$log" || true
|
|
52
|
-
fi
|
|
53
|
-
# **手渡された env を常駐へ渡す。** 新しい session が継ぐのは tmux *server* の環境であって
|
|
54
|
-
# 呼び出し元 client の環境ではないので、素で起こすと `PEERTABLE_TMUX_SOCKET` の手渡しが黙って消え、
|
|
55
|
-
# 常駐が別の socket(本番の既定)を観測しにいく(2026-08-11 実測)。決定73 と同じ形の裏返しである。
|
|
56
|
-
# **秘密値はコマンドラインへ載せない。** `env PEERTABLE_POST_TOKEN=… node …` の形は
|
|
57
|
-
# tmux のsession起動コマンドと ps の両方に平文で残る(2026-08-30 実測)。トークンは
|
|
58
|
-
# 0600 の credential file へ落とし、常駐へはそのパスだけを渡す(bridge 側の
|
|
59
|
-
# resolvePostToken が PEERTABLE_CREDENTIAL_FILE → ~/.config/peertable.env の順で読む)
|
|
60
|
-
if [ -n "${PEERTABLE_POST_TOKEN:-}" ] && [ -z "${PEERTABLE_CREDENTIAL_FILE:-}" ]; then
|
|
61
|
-
cred_dir="$proj/.team/credentials"
|
|
62
|
-
mkdir -p "$cred_dir" && chmod 700 "$cred_dir"
|
|
63
|
-
( umask 177; printf '%s\n' "$PEERTABLE_POST_TOKEN" > "$cred_dir/bridge.token" )
|
|
64
|
-
PEERTABLE_CREDENTIAL_FILE="$cred_dir/bridge.token"
|
|
65
|
-
export PEERTABLE_CREDENTIAL_FILE
|
|
66
|
-
fi
|
|
67
|
-
env_prefix=""
|
|
68
|
-
for v in PEERTABLE_TMUX_SOCKET PEERTABLE_CREDENTIAL_FILE PEERTABLE_URL PEERTABLE_PARENT_NAME; do
|
|
69
|
-
eval "val=\${$v:-}"
|
|
70
|
-
[ -n "$val" ] && env_prefix="$env_prefix $v=$(printf '%q' "$val")"
|
|
71
|
-
done
|
|
72
|
-
if [ "$(node -p 'process.platform')" = "win32" ]; then
|
|
73
|
-
# psmuxが起動する正規shellはPowerShell 7。POSIXの`env`とMSYS pathを文字列のまま
|
|
74
|
-
# 渡すとnodeへ到達する前に終了するため、Windows nodeに各pathをargvとして変換させ、
|
|
75
|
-
# PowerShell EncodedCommandを一箇所で組み立てる。
|
|
76
|
-
win_command=$(node "$scripts_dir/platform/windows/build-bridge-command.mjs" \
|
|
77
|
-
"$scripts_dir/$script" "$proj" "$log" "$@")
|
|
78
|
-
tmux_at new-session -d -s "$session" "$win_command"
|
|
79
|
-
else
|
|
80
|
-
tmux_at new-session -d -s "$session" "env${env_prefix} node $(dirname "$0")/$script $(printf '%q ' "$proj" "$@") >> $(printf '%q' "$log") 2>&1"
|
|
81
|
-
fi
|
|
82
|
-
for _ in $(seq 1 30); do
|
|
83
|
-
if [ -f "$record" ] && node -e 'process.exit(require(process.argv[1]).ready_at?0:1)' "$record"; then
|
|
84
|
-
# **末尾は本物の改行にする。** `"\\n"` と書くと JS がリテラルの `\`+`n` を足し、
|
|
85
|
-
# record が JSON として壊れる——`--stop` も次回起動も `JSON.parse` で落ちて、
|
|
86
|
-
# 「止められない・建て直せない常駐」ができる(2026-08-11 実測)。一時 file→rename で原子的に。
|
|
87
|
-
node -e 'const fs=require("fs");const p=process.argv[1],version=process.argv[2],digest=process.argv[3],a=process.argv.slice(4);
|
|
88
|
-
const t=`${p}.${process.pid}.tmp`;
|
|
89
|
-
fs.writeFileSync(t,JSON.stringify({...JSON.parse(fs.readFileSync(p,"utf8")),peertable_version:version,peertable_runtime_digest:digest,args:a})+"\n");
|
|
90
|
-
fs.renameSync(t,p)' "$record" "$peertable_version" "$peertable_runtime_digest" "$@"
|
|
91
|
-
exit 0
|
|
92
|
-
fi
|
|
93
|
-
sleep .5
|
|
94
|
-
done
|
|
95
|
-
echo "${name}-bridge: ready_at を待てなかった。ログ末尾:" >&2
|
|
96
|
-
tail -n 20 "$log" >&2 || true
|
|
97
|
-
exit 1
|
|
4
|
+
exec node "$(dirname "$0")/ensure-bridge.mjs" "$@"
|
|
@@ -77,7 +77,7 @@ function expectedBlock(addedNewline) {
|
|
|
77
77
|
'[mcp_servers.room]',
|
|
78
78
|
'command = "node"',
|
|
79
79
|
`args = [${JSON.stringify(client)}]`,
|
|
80
|
-
'env_vars = ["
|
|
80
|
+
'env_vars = ["AITERM_SESSION_ID"]',
|
|
81
81
|
'[mcp_servers.room.env]',
|
|
82
82
|
...explicitEnv,
|
|
83
83
|
endMarker,
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// 3bridgeを順に起動・更新し、実際のready記録まで確認する。
|
|
3
|
+
import { createHash } from 'node:crypto'
|
|
4
|
+
import { existsSync, mkdirSync, renameSync, unlinkSync, writeFileSync } from 'node:fs'
|
|
5
|
+
import { join, resolve } from 'node:path'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
7
|
+
import { setTimeout as delay } from 'node:timers/promises'
|
|
8
|
+
import { AitermClient } from './aiterm-client.mjs'
|
|
9
|
+
import { bridgeRecordLive } from './bridge-record-live.mjs'
|
|
10
|
+
import { packageRoot } from './install-skill.mjs'
|
|
11
|
+
import { projectPath, readSetup, readJson, runScript, fail } from './project-scaffold.mjs'
|
|
12
|
+
import { runtimeLaunchCommand } from './runtime-launch-command.mjs'
|
|
13
|
+
|
|
14
|
+
export const bridgeKinds = Object.freeze(['alarm', 'seat-status', 'wakeup'])
|
|
15
|
+
export const bridgeSession = (project, room, kind) => `peertable-${kind}-${room}-${createHash('sha256').update(project).digest('hex').slice(0, 8)}`
|
|
16
|
+
|
|
17
|
+
export async function ensureBridge(projectArg, kind, { root = packageRoot, env = process.env, aiterm, args = [], force = false } = {}) {
|
|
18
|
+
if (!bridgeKinds.includes(kind)) fail('ENSURE_BRIDGE_KIND_INVALID', kind)
|
|
19
|
+
const project = projectPath(projectArg)
|
|
20
|
+
const state = readSetup(project)
|
|
21
|
+
const team = join(project, '.team')
|
|
22
|
+
const recordPath = join(team, `${kind}-bridge.json`), log = join(team, `${kind}-bridge.log`)
|
|
23
|
+
const script = `${kind}-bridge.mjs`
|
|
24
|
+
const version = readJson(join(root, 'package.json')).version
|
|
25
|
+
const digest = runScript('runtime-digest.mjs', [], { root })
|
|
26
|
+
const previous = existsSync(recordPath) ? readJson(recordPath) : null
|
|
27
|
+
if (bridgeRecordLive(previous) && previous.peertable_version === version && previous.peertable_runtime_digest === digest)
|
|
28
|
+
return { kind, status: 'current', session_id: previous.aiterm_session_id ?? null }
|
|
29
|
+
const ownClient = !aiterm
|
|
30
|
+
aiterm ??= new AitermClient({ env })
|
|
31
|
+
try {
|
|
32
|
+
if (previous && bridgeRecordLive(previous)) runScript(script, [project, '--stop'], { root, env })
|
|
33
|
+
const session = previous?.aiterm_session_id ?? (previous ? `peertable-${kind}-${state.room}` : bridgeSession(project, state.room, kind))
|
|
34
|
+
await aiterm.structured('pty_close', { session_id: session }, 'aiterm.pty-close-result.v1')
|
|
35
|
+
if (existsSync(recordPath)) unlinkSync(recordPath)
|
|
36
|
+
writeFileSync(log, '')
|
|
37
|
+
const launchEnv = { ...env }
|
|
38
|
+
if (launchEnv.PEERTABLE_POST_TOKEN && !launchEnv.PEERTABLE_CREDENTIAL_FILE) {
|
|
39
|
+
const credentials = join(team, 'credentials')
|
|
40
|
+
mkdirSync(credentials, { recursive: true, mode: 0o700 })
|
|
41
|
+
launchEnv.PEERTABLE_CREDENTIAL_FILE = join(credentials, 'bridge.token')
|
|
42
|
+
writeFileSync(launchEnv.PEERTABLE_CREDENTIAL_FILE, `${launchEnv.PEERTABLE_POST_TOKEN}\n`, { mode: 0o600 })
|
|
43
|
+
}
|
|
44
|
+
if (!launchEnv.PEERTABLE_CREDENTIAL_FILE) {
|
|
45
|
+
launchEnv.PEERTABLE_CREDENTIAL_FILE = runScript('seat-credential.mjs', ['prepare', project, state.room, 'runtime'], { env: launchEnv })
|
|
46
|
+
}
|
|
47
|
+
const effectiveArgs = args.length ? args : previous?.args ?? []
|
|
48
|
+
await aiterm.call('pty_open', { name: session })
|
|
49
|
+
await aiterm.structured('pty_send', { session_id: session, text: runtimeLaunchCommand({
|
|
50
|
+
executable: process.execPath, script: join(root, 'skill', 'scripts', script), project,
|
|
51
|
+
args: effectiveArgs, log, env: launchEnv,
|
|
52
|
+
}) }, 'aiterm.pty-send-result.v1')
|
|
53
|
+
const deadline = Date.now() + 15_000
|
|
54
|
+
while (Date.now() < deadline) {
|
|
55
|
+
const record = existsSync(recordPath) ? readJson(recordPath) : null
|
|
56
|
+
if (record?.ready_at && bridgeRecordLive(record)) {
|
|
57
|
+
const temporary = `${recordPath}.${process.pid}.tmp`
|
|
58
|
+
writeFileSync(temporary, `${JSON.stringify({ ...record, peertable_version: version, peertable_runtime_digest: digest,
|
|
59
|
+
args: effectiveArgs, aiterm_session_id: session })}\n`)
|
|
60
|
+
renameSync(temporary, recordPath)
|
|
61
|
+
return { kind, status: previous ? 'updated' : 'started', session_id: session }
|
|
62
|
+
}
|
|
63
|
+
await delay(200)
|
|
64
|
+
}
|
|
65
|
+
fail('PEERTABLE_RUNTIME_NOT_READY', `${kind}のready記録を確認できません。${log}`)
|
|
66
|
+
} finally {
|
|
67
|
+
if (ownClient) await aiterm.close()
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function ensureProjectRuntime(project, options = {}) {
|
|
72
|
+
const aiterm = options.aiterm ?? new AitermClient({ env: options.env })
|
|
73
|
+
try {
|
|
74
|
+
const bridges = []
|
|
75
|
+
for (const kind of bridgeKinds) bridges.push(await ensureBridge(project, kind, { ...options, aiterm }))
|
|
76
|
+
return { schema: 'peertable.runtime-result.v1', status: 'ready', bridges }
|
|
77
|
+
} finally {
|
|
78
|
+
if (!options.aiterm) await aiterm.close()
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
83
|
+
try { console.log(JSON.stringify(await ensureProjectRuntime(process.argv[2], { force: process.argv.includes('--force') }))) }
|
|
84
|
+
catch (error) { console.error(`${error.code ?? 'PEERTABLE_RUNTIME_FAILED'}: ${error.message}`); process.exitCode = 1 }
|
|
85
|
+
}
|
|
@@ -1,13 +1,4 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
#
|
|
2
|
+
# 全OS共通の公開API実装へ渡す旧入口。
|
|
3
3
|
set -euo pipefail
|
|
4
|
-
|
|
5
|
-
proj="${1:-}"
|
|
6
|
-
[ -n "$proj" ] || { echo "usage: ensure-project-runtime.sh <project_dir>" >&2; exit 2; }
|
|
7
|
-
scripts=$(cd "$(dirname "$0")" && pwd -P)
|
|
8
|
-
|
|
9
|
-
for kind in alarm seat-status wakeup; do
|
|
10
|
-
"$scripts/ensure-bridge.sh" "$proj" "$kind"
|
|
11
|
-
done
|
|
12
|
-
|
|
13
|
-
echo "peertable runtime ready: alarm / seat-status / wakeup"
|
|
4
|
+
exec node "$(dirname "$0")/ensure-project-runtime.mjs" "$@"
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { mkdtempSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from 'node:fs'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { tmpdir } from 'node:os'
|
|
6
|
+
import { ensureBridge } from './ensure-project-runtime.mjs'
|
|
7
|
+
|
|
8
|
+
test('新規runtimeは指定した資格元からファイルを準備して公開PTYへ渡す', async t => {
|
|
9
|
+
const project = realpathSync(mkdtempSync(join(tmpdir(), 'peertable-runtime-credential-')))
|
|
10
|
+
t.after(() => rmSync(project, { recursive: true, force: true }))
|
|
11
|
+
const team = join(project, '.team')
|
|
12
|
+
mkdirSync(team)
|
|
13
|
+
writeFileSync(join(team, 'setup-state.json'), JSON.stringify({ room: 'fixture', server_url: 'http://localhost', mode: 'standalone' }))
|
|
14
|
+
const source = join(project, 'source.env')
|
|
15
|
+
writeFileSync(source, 'PEERTABLE_POST_TOKEN=fixture-credential-123\n', { mode: 0o600 })
|
|
16
|
+
const env = { ...process.env, PEERTABLE_TOKEN_SOURCE_FILE: source }
|
|
17
|
+
delete env.PEERTABLE_POST_TOKEN
|
|
18
|
+
delete env.PEERTABLE_CREDENTIAL_FILE
|
|
19
|
+
let sent = false
|
|
20
|
+
const aiterm = {
|
|
21
|
+
call: async tool => { assert.equal(tool, 'pty_open') },
|
|
22
|
+
structured: async (tool, args) => {
|
|
23
|
+
if (tool === 'pty_close') return {}
|
|
24
|
+
assert.equal(tool, 'pty_send')
|
|
25
|
+
const credentials = join(team, 'credentials')
|
|
26
|
+
const files = readdirSync(credentials)
|
|
27
|
+
assert.equal(files.length, 1)
|
|
28
|
+
assert.equal(readFileSync(join(credentials, files[0]), 'utf8'), 'fixture-credential-123\n')
|
|
29
|
+
assert.ok(args.text.includes(files[0]))
|
|
30
|
+
assert.ok(args.text.includes('PEERTABLE_CREDENTIAL_FILE'))
|
|
31
|
+
assert.ok(!args.text.includes('fixture-credential-123'))
|
|
32
|
+
const stamp = new Date().toISOString()
|
|
33
|
+
writeFileSync(join(team, 'alarm-bridge.json'), JSON.stringify({ pid: process.pid, ready_at: stamp, last_progress_at: stamp }))
|
|
34
|
+
sent = true
|
|
35
|
+
return {}
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
assert.equal((await ensureBridge(project, 'alarm', { aiterm, env })).status, 'started')
|
|
39
|
+
assert.equal(sent, true)
|
|
40
|
+
})
|