@minimalcorp/tsunagi 0.1.55 → 0.3.0
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/.next/standalone/apps/web/.next/BUILD_ID +1 -1
- package/.next/standalone/apps/web/.next/build-manifest.json +3 -3
- package/.next/standalone/apps/web/.next/prerender-manifest.json +3 -3
- package/.next/standalone/apps/web/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/apps/web/.next/server/app/_global-error.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/apps/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/apps/web/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/apps/web/.next/server/app/_not-found.rsc +14 -14
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_index.segment.rsc +9 -9
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/apps/web/.next/server/app/index.html +1 -1
- package/.next/standalone/apps/web/.next/server/app/index.rsc +16 -16
- package/.next/standalone/apps/web/.next/server/app/index.segments/__PAGE__.segment.rsc +4 -4
- package/.next/standalone/apps/web/.next/server/app/index.segments/_full.segment.rsc +16 -16
- package/.next/standalone/apps/web/.next/server/app/index.segments/_head.segment.rsc +4 -4
- package/.next/standalone/apps/web/.next/server/app/index.segments/_index.segment.rsc +9 -9
- package/.next/standalone/apps/web/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/apps/web/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/apps/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/apps/web/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/apps/web/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/apps/web/.next/server/app/settings.html +1 -1
- package/.next/standalone/apps/web/.next/server/app/settings.rsc +17 -16
- package/.next/standalone/apps/web/.next/server/app/settings.segments/_full.segment.rsc +17 -16
- package/.next/standalone/apps/web/.next/server/app/settings.segments/_head.segment.rsc +4 -4
- package/.next/standalone/apps/web/.next/server/app/settings.segments/_index.segment.rsc +9 -9
- package/.next/standalone/apps/web/.next/server/app/settings.segments/_tree.segment.rsc +3 -2
- package/.next/standalone/apps/web/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +5 -4
- package/.next/standalone/apps/web/.next/server/app/settings.segments/settings.segment.rsc +3 -3
- package/.next/standalone/apps/web/.next/server/app/tasks/[id]/page.js.nft.json +1 -1
- package/.next/standalone/apps/web/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/[root-of-the-server]__00qk65u._.js +3 -0
- package/.next/standalone/apps/web/.next/server/chunks/ssr/[root-of-the-server]__02o_btn._.js +2 -2
- package/.next/standalone/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0qjafjt._.js +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_03vftq7._.js +7 -0
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_0835zmv._.js +14 -0
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_0g.cd6a._.js +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_0g79jqo._.js +20 -0
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_0kflgfj._.js +20 -0
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_0w2dm3r._.js +3 -0
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_10gk93.._.js +3 -0
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{_05_5y28._.js → _139_l.u._.js} +2 -2
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_13w5ytx._.js +7 -0
- package/.next/standalone/apps/web/.next/server/chunks/ssr/apps_web_src_app_settings_page_tsx_0i6itlb._.js +2 -2
- package/.next/standalone/apps/web/.next/server/chunks/ssr/apps_web_src_app_tasks_[id]_page_tsx_0jshsm2._.js +4 -21
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0c8sp_9._.js → node_modules_04pfam-._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0zv4e8x._.js → node_modules_09d2oqj._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0h_hear._.js → node_modules_0_huoqd._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/node_modules_0a57phw._.js +3 -0
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0axuayq._.js → node_modules_0ak_3rk._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0la1a4x._.js → node_modules_0cv326~._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0_876x4._.js → node_modules_0h.vgyn._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0w8sswp._.js → node_modules_0krwgny._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_09e0q95._.js → node_modules_0l23gkf._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0o9t~hh._.js → node_modules_0npujym._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_06gyfuv._.js → node_modules_0oip5a1._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_10v5lct._.js → node_modules_0q_5qo~._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_117mm7f._.js → node_modules_0q_xfyl._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0s3mkxs._.js → node_modules_0sn0lqb._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0nji2bx._.js → node_modules_0tod6y8._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0mt.8za._.js → node_modules_0xp9nhx._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_09nusj_._.js → node_modules_11-z65a._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/apps/web/.next/server/pages/404.html +1 -1
- package/.next/standalone/apps/web/.next/server/pages/500.html +1 -1
- package/.next/standalone/apps/web/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/apps/web/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/apps/web/.next/static/chunks/0.gcmzq82~36i.js +1 -0
- package/.next/standalone/apps/web/.next/static/chunks/06gfh91..uydi.js +5 -0
- package/.next/standalone/apps/web/.next/static/chunks/08e.dbdz18nd2.js +18 -0
- package/.next/standalone/apps/web/.next/static/chunks/0b90ys7wq-p.4.css +4 -0
- package/.next/standalone/apps/web/.next/static/chunks/0crn03y2tj0lu.js +6 -0
- package/.next/standalone/apps/web/.next/static/chunks/0l83q95s~tih4.js +5 -0
- package/.next/standalone/apps/web/.next/static/chunks/0lvl3ec93cab_.js +7 -0
- package/.next/standalone/apps/web/.next/static/chunks/0lxvbkhh7amha.js +1 -0
- package/.next/standalone/apps/web/.next/static/chunks/0mqupo29wme26.js +7 -0
- package/.next/standalone/apps/web/.next/static/chunks/0rn1kcmsfzjmz.js +12 -0
- package/.next/standalone/apps/web/.next/static/chunks/0whl1y16cvn5q.js +1 -0
- package/.next/standalone/apps/web/.next/static/chunks/0z60ewb40i2ce.js +1 -0
- package/.next/standalone/apps/web/.next/static/chunks/0~qk-51yuw4mm.js +18 -0
- package/.next/standalone/apps/web/.next/static/chunks/{0v3hfoggxwsov.js → 149sia28cskw9.js} +1 -1
- package/.next/standalone/apps/web/.next/static/chunks/17~93fal1fw_d.js +5 -0
- package/dist/app.d.ts +1 -0
- package/dist/app.js +375 -0
- package/dist/cli.js +152 -290
- package/dist/plugin-lifecycle.d.ts +9 -10
- package/dist/plugin-lifecycle.js +77 -65
- package/dist/prisma/migrations/20261007000000_add_claude_profiles/migration.sql +10 -0
- package/dist/prisma/schema.prisma +9 -0
- package/dist/server/generated/prisma/browser.d.ts +5 -0
- package/dist/server/generated/prisma/client.d.ts +5 -0
- package/dist/server/generated/prisma/internal/class.d.ts +11 -0
- package/dist/server/generated/prisma/internal/class.js +4 -4
- package/dist/server/generated/prisma/internal/prismaNamespace.d.ts +83 -1
- package/dist/server/generated/prisma/internal/prismaNamespace.js +6 -0
- package/dist/server/generated/prisma/internal/prismaNamespaceBrowser.d.ts +7 -0
- package/dist/server/generated/prisma/internal/prismaNamespaceBrowser.js +6 -0
- package/dist/server/generated/prisma/models/ClaudeProfile.d.ts +939 -0
- package/dist/server/generated/prisma/models/ClaudeProfile.js +1 -0
- package/dist/server/generated/prisma/models.d.ts +1 -0
- package/dist/server/index.js +20 -3
- package/dist/server/lib/auto-update.d.ts +18 -0
- package/dist/server/lib/auto-update.js +208 -0
- package/dist/server/lib/claude-config-guard.d.ts +7 -5
- package/dist/server/lib/claude-config-guard.js +31 -12
- package/dist/server/lib/claude-profiles.d.ts +49 -0
- package/dist/server/lib/claude-profiles.js +168 -0
- package/dist/server/lib/pty-env-sync.d.ts +12 -0
- package/dist/server/lib/pty-env-sync.js +139 -0
- package/dist/server/lib/pty-launch.d.ts +36 -0
- package/dist/server/lib/pty-launch.js +141 -0
- package/dist/server/lib/repositories/environment.js +8 -34
- package/dist/server/lib/restart.d.ts +8 -0
- package/dist/server/lib/restart.js +13 -0
- package/dist/server/lib/update-check.js +14 -4
- package/dist/server/pty-manager.d.ts +25 -1
- package/dist/server/pty-manager.js +38 -3
- package/dist/server/routes/claude-profiles.d.ts +2 -0
- package/dist/server/routes/claude-profiles.js +138 -0
- package/dist/server/routes/env.js +25 -1
- package/dist/server/routes/onboarding.js +15 -6
- package/dist/server/routes/terminal.js +34 -120
- package/dist/server/routes/version.js +16 -0
- package/dist/single-instance-lock.js +3 -17
- package/dist/update-lifecycle.d.ts +19 -0
- package/dist/update-lifecycle.js +93 -0
- package/dist/versions.d.ts +60 -0
- package/dist/versions.js +95 -0
- package/dist/with-plugin.js +6 -8
- package/npm-shrinkwrap.json +8 -8
- package/package.json +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/[root-of-the-server]__03~.ax1._.js +0 -3
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_0bru-z.._.js +0 -3
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_0hqrmyz._.js +0 -3
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_0i8ojdw._.js +0 -14
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_0m.hvms._.js +0 -7
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_0mljs_s._.js +0 -7
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_0ncpk6b._.js +0 -3
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_109pmo2._.js +0 -3
- package/.next/standalone/apps/web/.next/static/chunks/01mfp5lo0gy.q.js +0 -23
- package/.next/standalone/apps/web/.next/static/chunks/08-d-ag2euha9.js +0 -5
- package/.next/standalone/apps/web/.next/static/chunks/0_by0zpp68~r7.js +0 -1
- package/.next/standalone/apps/web/.next/static/chunks/0c.dwezn5qjvs.css +0 -4
- package/.next/standalone/apps/web/.next/static/chunks/0gpkw.1e6kcpn.js +0 -1
- package/.next/standalone/apps/web/.next/static/chunks/0jc3jwed_8vre.js +0 -7
- package/.next/standalone/apps/web/.next/static/chunks/0p3_2uc39mlse.js +0 -5
- package/.next/standalone/apps/web/.next/static/chunks/0v6gxm~ldhmma.js +0 -1
- package/.next/standalone/apps/web/.next/static/chunks/0vxoeg5sfua~3.js +0 -12
- package/.next/standalone/apps/web/.next/static/chunks/0wzu~340lh7o~.js +0 -7
- package/.next/standalone/apps/web/.next/static/chunks/0xa_n_zp.qw-b.js +0 -1
- package/.next/standalone/apps/web/.next/static/chunks/13y6z-vus9yvr.js +0 -5
- /package/.next/standalone/apps/web/.next/static/{EOnxRuFun6cfXk-y9KPq5 → jgWo5G4-W0asjpQwCqJWQ}/_buildManifest.js +0 -0
- /package/.next/standalone/apps/web/.next/static/{EOnxRuFun6cfXk-y9KPq5 → jgWo5G4-W0asjpQwCqJWQ}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/apps/web/.next/static/{EOnxRuFun6cfXk-y9KPq5 → jgWo5G4-W0asjpQwCqJWQ}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { execFile } from 'child_process';
|
|
2
|
+
import { ptyManager } from '../pty-manager.js';
|
|
3
|
+
import { prisma } from './db.js';
|
|
4
|
+
import { resolvePtyLaunch, startPtySession } from './pty-launch.js';
|
|
5
|
+
/**
|
|
6
|
+
* tsunagi の環境変数(Claude プロファイルの割り当てを含む)の変更を、起動中の PTY に反映する。
|
|
7
|
+
*
|
|
8
|
+
* 動いているプロセスの環境変数は外から書き換えられないため、PTY を作り直して反映する
|
|
9
|
+
* (claude タブは --resume で同じ会話に戻る)。作業を壊さないよう、claude が応答中・許可待ちの間や
|
|
10
|
+
* シェルでコマンドを実行中の間、直近にユーザー入力があった間は作り直さず「反映待ち」にしておき、
|
|
11
|
+
* 安全になった時点で作り直す。
|
|
12
|
+
*/
|
|
13
|
+
/** 直近の入力からこの時間が経つまでは作り直さない(入力中の文字を消さないため) */
|
|
14
|
+
const INPUT_IDLE_MS = 15_000;
|
|
15
|
+
/** 反映待ちの PTY を作り直せるか確認する間隔 */
|
|
16
|
+
const CHECK_INTERVAL_MS = 5_000;
|
|
17
|
+
/** claude がこの状態の間は作り直さない(hooks が Tab.status に記録する) */
|
|
18
|
+
const BUSY_TAB_STATUSES = new Set(['running', 'waiting']);
|
|
19
|
+
let io = null;
|
|
20
|
+
let checking = false;
|
|
21
|
+
function emitPending(session) {
|
|
22
|
+
io?.to(`tab:${session.sessionId}`).emit('env-pending', {
|
|
23
|
+
sessionId: session.sessionId,
|
|
24
|
+
pending: session.envPending,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
/** 環境変数の比較用キー(キー順に依存しない) */
|
|
28
|
+
function launchKey(env, unsetKeys) {
|
|
29
|
+
const sorted = Object.keys(env)
|
|
30
|
+
.sort()
|
|
31
|
+
.map((key) => [key, env[key]]);
|
|
32
|
+
return JSON.stringify([sorted, [...unsetKeys].sort()]);
|
|
33
|
+
}
|
|
34
|
+
async function resolveFor(session) {
|
|
35
|
+
try {
|
|
36
|
+
return await resolvePtyLaunch(session.sessionId, session.options.request ?? {});
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
console.warn(`[pty-env-sync] Failed to resolve env for ${session.sessionId}:`, err);
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** シェル配下でコマンドが動いているか(pgrep は該当なしのとき exit 1) */
|
|
44
|
+
function hasChildProcess(pid) {
|
|
45
|
+
return new Promise((resolve) => {
|
|
46
|
+
execFile('pgrep', ['-P', String(pid)], (err) => resolve(!err));
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
async function canRespawnNow(session) {
|
|
50
|
+
if (Date.now() - session.lastInputAt < INPUT_IDLE_MS)
|
|
51
|
+
return false;
|
|
52
|
+
const tab = await prisma.tab
|
|
53
|
+
.findUnique({ where: { tabId: session.sessionId }, select: { mode: true, status: true } })
|
|
54
|
+
.catch(() => null);
|
|
55
|
+
if (tab && tab.mode !== 'terminal')
|
|
56
|
+
return !BUSY_TAB_STATUSES.has(tab.status);
|
|
57
|
+
return !(await hasChildProcess(session.pty.pid));
|
|
58
|
+
}
|
|
59
|
+
/** PTY を最新の環境変数で作り直す。クライアントには pty-respawned で再接続させる */
|
|
60
|
+
async function respawn(session) {
|
|
61
|
+
const launch = await resolveFor(session);
|
|
62
|
+
if (!launch)
|
|
63
|
+
return;
|
|
64
|
+
const { sessionId } = session;
|
|
65
|
+
// 判定から作り直しまでの間に別経路で作り直し・削除されていれば何もしない
|
|
66
|
+
if (ptyManager.getSession(sessionId) !== session)
|
|
67
|
+
return;
|
|
68
|
+
session.respawning = true;
|
|
69
|
+
ptyManager.deleteSession(sessionId);
|
|
70
|
+
try {
|
|
71
|
+
await startPtySession(sessionId, launch, {
|
|
72
|
+
request: session.options.request ?? {},
|
|
73
|
+
launchClaude: session.options.launchClaude ?? false,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
console.error(`[pty-env-sync] Failed to respawn ${sessionId}:`, err);
|
|
78
|
+
io?.to(`tab:${sessionId}`).emit('exit', { exitCode: -1 });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
console.log(`[pty-env-sync] Respawned ${sessionId} with updated env`);
|
|
82
|
+
io?.to(`tab:${sessionId}`).emit('pty-respawned', { sessionId });
|
|
83
|
+
}
|
|
84
|
+
/** 反映待ちの PTY のうち、安全なものを作り直す */
|
|
85
|
+
async function respawnPendingSessions() {
|
|
86
|
+
if (checking)
|
|
87
|
+
return;
|
|
88
|
+
checking = true;
|
|
89
|
+
try {
|
|
90
|
+
for (const sessionId of ptyManager.listSessions()) {
|
|
91
|
+
const session = ptyManager.getSession(sessionId);
|
|
92
|
+
if (!session?.envPending || session.respawning)
|
|
93
|
+
continue;
|
|
94
|
+
if (await canRespawnNow(session))
|
|
95
|
+
await respawn(session);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
checking = false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/** 起動時に一度呼ぶ。反映待ちの PTY を定期的に確認する */
|
|
103
|
+
export function initPtyEnvSync(server) {
|
|
104
|
+
io = server;
|
|
105
|
+
setInterval(() => void respawnPendingSessions(), CHECK_INTERVAL_MS).unref();
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* tsunagi の環境変数(Claude プロファイルの割り当て・プロファイル削除を含む)を変更した後に呼ぶ。
|
|
109
|
+
* 起動中の PTY ごとに環境変数を解決し直し、作成時と違えば反映待ちにする。
|
|
110
|
+
*/
|
|
111
|
+
export async function notifyEnvChanged() {
|
|
112
|
+
for (const sessionId of ptyManager.listSessions()) {
|
|
113
|
+
const session = ptyManager.getSession(sessionId);
|
|
114
|
+
if (!session?.options.syncEnv || session.respawning)
|
|
115
|
+
continue;
|
|
116
|
+
const launch = await resolveFor(session);
|
|
117
|
+
if (!launch)
|
|
118
|
+
continue;
|
|
119
|
+
const pending = launchKey(launch.env, launch.unsetKeys) !==
|
|
120
|
+
launchKey(session.appliedEnv, session.appliedUnsetKeys);
|
|
121
|
+
if (pending !== session.envPending) {
|
|
122
|
+
session.envPending = pending;
|
|
123
|
+
emitPending(session);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
await respawnPendingSessions();
|
|
127
|
+
}
|
|
128
|
+
/** 反映待ちの PTY を、ユーザー操作で今すぐ作り直す */
|
|
129
|
+
export async function respawnSessionNow(sessionId) {
|
|
130
|
+
const session = ptyManager.getSession(sessionId);
|
|
131
|
+
if (!session || session.respawning)
|
|
132
|
+
return false;
|
|
133
|
+
await respawn(session);
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
/** join したクライアントに現在の反映待ち状態を伝える */
|
|
137
|
+
export function getEnvPending(sessionId) {
|
|
138
|
+
return ptyManager.getSession(sessionId)?.envPending ?? false;
|
|
139
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { TabMode } from '@minimalcorp/tsunagi-shared';
|
|
2
|
+
import { type PtySession } from '../pty-manager.js';
|
|
3
|
+
/** PTY 起動時にクライアントが指定する値(環境変数の自動反映で作り直すときも同じ値を使う) */
|
|
4
|
+
export interface PtyLaunchRequest {
|
|
5
|
+
cwd?: string;
|
|
6
|
+
env?: Record<string, string>;
|
|
7
|
+
}
|
|
8
|
+
export interface PtyLaunch {
|
|
9
|
+
workingDir: string;
|
|
10
|
+
mode: TabMode | undefined;
|
|
11
|
+
/** タブ(Task)に紐づくか。紐づくタブは worktree が必須 */
|
|
12
|
+
hasTab: boolean;
|
|
13
|
+
env: Record<string, string>;
|
|
14
|
+
unsetKeys: string[];
|
|
15
|
+
}
|
|
16
|
+
export declare class PtyLaunchError extends Error {
|
|
17
|
+
readonly statusCode: number;
|
|
18
|
+
constructor(message: string, statusCode: number);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* sessionId(=tab_id) から PTY の作業ディレクトリと環境変数を解決する。
|
|
22
|
+
* 環境変数の優先順位: リクエストで渡されたenv > ローカルLLMのprovider env >
|
|
23
|
+
* DB env(global/owner/repo、Claude プロファイルを CLAUDE_CONFIG_DIR に変換) > tsunagi独自のEDITOR設定
|
|
24
|
+
*/
|
|
25
|
+
export declare function resolvePtyLaunch(sessionId: string, request: PtyLaunchRequest): Promise<PtyLaunch>;
|
|
26
|
+
/**
|
|
27
|
+
* 解決済みの launch で PTY を作り、claude を起動する。
|
|
28
|
+
* 新規作成(POST /terminal/sessions)と環境変数変更時の作り直し(pty-env-sync)で共用する。
|
|
29
|
+
*/
|
|
30
|
+
export declare function startPtySession(sessionId: string, launch: PtyLaunch, options: {
|
|
31
|
+
request: PtyLaunchRequest;
|
|
32
|
+
launchClaude: boolean;
|
|
33
|
+
command?: string;
|
|
34
|
+
}): Promise<PtySession>;
|
|
35
|
+
/** 既存 PTY で claude を(再)起動する前の補正。PTY 作成時の環境変数のプロファイルを対象にする */
|
|
36
|
+
export declare function prepareClaudeConfigForSession(session: PtySession): Promise<void>;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { ptyManager } from '../pty-manager.js';
|
|
6
|
+
import { prisma } from './db.js';
|
|
7
|
+
import { getWorktreePath } from './worktree-manager.js';
|
|
8
|
+
import { getEnv } from './repositories/environment.js';
|
|
9
|
+
import { ensureClaudeOnboardingCompleted, removeLocalModelFromUserSettings, watchUserSettingsForLocalModel, } from './claude-config-guard.js';
|
|
10
|
+
import { LOCAL_LLM_UNSET_ENV_KEYS, LOCAL_MODEL_ALIAS, buildLocalLlmEnv } from './local-llm-env.js';
|
|
11
|
+
import { ensureActiveLoaded, getLocalLlmSettings } from './local-llm.js';
|
|
12
|
+
import { LOCAL_LLM_PROXY_URL, buildClaudeCommand, isLocalLlmMode } from './claude-command.js';
|
|
13
|
+
import { resolveClaudeProfileEnv } from './claude-profiles.js';
|
|
14
|
+
// このファイル基準で解決する(cwd 非依存):
|
|
15
|
+
// dev: apps/server/src/lib → apps/server/scripts
|
|
16
|
+
// build: apps/server/dist/lib → apps/server/scripts
|
|
17
|
+
// npm bundle: <pkg>/dist/server/lib → <pkg>/dist/scripts
|
|
18
|
+
const TSUNAGI_EDITOR_PATH = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../scripts/monaco-editor.sh');
|
|
19
|
+
// Fastify(API) の公開ポート。index.ts と同じ既定値・同じ環境変数(TSUNAGI_SERVER_PORT)を見る。
|
|
20
|
+
// monaco-editor.sh など PTY 内のプロセスが API を叩く際の同一ホスト向けベース URL に使う。
|
|
21
|
+
const SERVER_PORT = Number(process.env.TSUNAGI_SERVER_PORT) || 2791;
|
|
22
|
+
export class PtyLaunchError extends Error {
|
|
23
|
+
statusCode;
|
|
24
|
+
constructor(message, statusCode) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.statusCode = statusCode;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* sessionId(=tab_id) から PTY の作業ディレクトリと環境変数を解決する。
|
|
31
|
+
* 環境変数の優先順位: リクエストで渡されたenv > ローカルLLMのprovider env >
|
|
32
|
+
* DB env(global/owner/repo、Claude プロファイルを CLAUDE_CONFIG_DIR に変換) > tsunagi独自のEDITOR設定
|
|
33
|
+
*/
|
|
34
|
+
export async function resolvePtyLaunch(sessionId, request) {
|
|
35
|
+
// Taskに紐づかないタブは Tab レコードが存在しないため null になる。
|
|
36
|
+
const tab = await prisma.tab.findUnique({
|
|
37
|
+
where: { tabId: sessionId },
|
|
38
|
+
include: { task: true },
|
|
39
|
+
});
|
|
40
|
+
// cwd は Task があればサーバー側で worktree パスを導出する(クライアントの state に依存しない)。
|
|
41
|
+
// クライアント指定の cwd は Task に紐づかないタブのフォールバックとしてのみ使う。
|
|
42
|
+
const defaultDir = path.join(os.homedir(), '.tsunagi', 'workspaces');
|
|
43
|
+
let workingDir = tab
|
|
44
|
+
? getWorktreePath(tab.task.owner, tab.task.repo, tab.task.branch)
|
|
45
|
+
: (request.cwd ?? defaultDir);
|
|
46
|
+
// cwd が存在するか確認。Task のタブは worktree 必須(無ければ起動しない)、
|
|
47
|
+
// それ以外はデフォルトにフォールバック
|
|
48
|
+
try {
|
|
49
|
+
await fs.access(workingDir);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
if (tab)
|
|
53
|
+
throw new PtyLaunchError(`Worktree not found at ${workingDir}`, 409);
|
|
54
|
+
workingDir = defaultDir;
|
|
55
|
+
}
|
|
56
|
+
// repo スコープまで階層マージした環境変数を取得する(global → owner → repo、後勝ちで上書き)。
|
|
57
|
+
// Taskに紐づかないタブは global のみになる。
|
|
58
|
+
// 読み込みに失敗したら起動しない(空の環境変数で起動・作り直しすると設定が抜け落ちるため)
|
|
59
|
+
const dbEnv = tab ? await getEnv('repo', tab.task.owner, tab.task.repo) : await getEnv('global');
|
|
60
|
+
const profile = resolveClaudeProfileEnv(dbEnv);
|
|
61
|
+
// ローカルLLMタブ: Anthropic の代わりに tsunagi の中継口(→ Ollama / LM Studio)で claude を動かす。
|
|
62
|
+
// DB の環境変数より後に適用し、Anthropic の認証トークンは環境変数ごと取り除く。
|
|
63
|
+
const mode = tab?.mode;
|
|
64
|
+
let providerEnv = {};
|
|
65
|
+
let unsetKeys = profile.unsetKeys;
|
|
66
|
+
if (isLocalLlmMode(mode)) {
|
|
67
|
+
const { active, extraEnv } = await getLocalLlmSettings();
|
|
68
|
+
if (!active) {
|
|
69
|
+
throw new PtyLaunchError('ローカルLLMのモデルが未設定です。Settings で使用するモデルを選んでください', 400);
|
|
70
|
+
}
|
|
71
|
+
providerEnv = buildLocalLlmEnv({
|
|
72
|
+
proxyUrl: LOCAL_LLM_PROXY_URL,
|
|
73
|
+
contextTokens: active.contextTokens,
|
|
74
|
+
extraEnv,
|
|
75
|
+
});
|
|
76
|
+
unsetKeys = [...new Set([...unsetKeys, ...LOCAL_LLM_UNSET_ENV_KEYS])];
|
|
77
|
+
}
|
|
78
|
+
// tsunagi-editor.sh をデフォルトにすることで Ctrl+G が Monaco Modal を開く。
|
|
79
|
+
// DB / リクエストで EDITOR が明示設定されている場合はそちらが優先される。
|
|
80
|
+
const tsunagiDefaultEnv = {
|
|
81
|
+
EDITOR: TSUNAGI_EDITOR_PATH,
|
|
82
|
+
TSUNAGI_SESSION_ID: sessionId,
|
|
83
|
+
// monaco-editor.sh が API(Fastify) を叩くためのベース URL。サーバーと同一
|
|
84
|
+
// ホストなので localhost。単一ポート化で API は SERVER_PORT(既定 2791)。
|
|
85
|
+
TSUNAGI_API_BASE: `http://localhost:${SERVER_PORT}`,
|
|
86
|
+
};
|
|
87
|
+
return {
|
|
88
|
+
workingDir,
|
|
89
|
+
mode,
|
|
90
|
+
hasTab: Boolean(tab),
|
|
91
|
+
env: { ...tsunagiDefaultEnv, ...profile.env, ...providerEnv, ...request.env },
|
|
92
|
+
unsetKeys,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* claude 起動前の補正。claude logout で .claude.json の hasCompletedOnboarding がリセットされて
|
|
97
|
+
* いると、次回 claude 起動時にオンボーディングウィザードが表示され --resume/--session-id を前提にした
|
|
98
|
+
* 自動起動フローが止まるため補正する。監視の取りこぼしに備え、中継口の名前が既定モデルに残って
|
|
99
|
+
* いないかも補正する。対象はタブが使う Claude プロファイルの設定ディレクトリ。
|
|
100
|
+
*/
|
|
101
|
+
async function prepareClaudeConfig(env) {
|
|
102
|
+
// PTY は外側 Terminal の環境を継承するため、プロファイル未指定なら外側の CLAUDE_CONFIG_DIR に従う
|
|
103
|
+
const configDir = env.CLAUDE_CONFIG_DIR ?? process.env.CLAUDE_CONFIG_DIR;
|
|
104
|
+
await ensureClaudeOnboardingCompleted(configDir);
|
|
105
|
+
await removeLocalModelFromUserSettings(LOCAL_MODEL_ALIAS, configDir);
|
|
106
|
+
watchUserSettingsForLocalModel(LOCAL_MODEL_ALIAS, configDir);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* 解決済みの launch で PTY を作り、claude を起動する。
|
|
110
|
+
* 新規作成(POST /terminal/sessions)と環境変数変更時の作り直し(pty-env-sync)で共用する。
|
|
111
|
+
*/
|
|
112
|
+
export async function startPtySession(sessionId, launch, options) {
|
|
113
|
+
await fs.mkdir(launch.workingDir, { recursive: true });
|
|
114
|
+
let command = options.command;
|
|
115
|
+
if (options.launchClaude) {
|
|
116
|
+
if (isLocalLlmMode(launch.mode)) {
|
|
117
|
+
// 最初の発言を待たせないよう読み込みを始めておく(完了は待たない)
|
|
118
|
+
ensureActiveLoaded().catch(() => undefined);
|
|
119
|
+
}
|
|
120
|
+
command = await buildClaudeCommand(sessionId, launch.mode ?? 'claude');
|
|
121
|
+
}
|
|
122
|
+
const session = ptyManager.createSession(sessionId, launch.workingDir, launch.env, launch.unsetKeys, {
|
|
123
|
+
request: options.request,
|
|
124
|
+
launchClaude: options.launchClaude,
|
|
125
|
+
syncEnv: true,
|
|
126
|
+
});
|
|
127
|
+
// コマンドが指定されていればPTY起動後にシェルへ書き込む
|
|
128
|
+
if (command) {
|
|
129
|
+
const cmd = command.endsWith('\n') ? command : command + '\n';
|
|
130
|
+
await prepareClaudeConfig(launch.env);
|
|
131
|
+
// シェルの初期化(プロンプト表示)を待つため少し遅延させて書き込む
|
|
132
|
+
setTimeout(() => {
|
|
133
|
+
session.pty.write(cmd);
|
|
134
|
+
}, 300);
|
|
135
|
+
}
|
|
136
|
+
return session;
|
|
137
|
+
}
|
|
138
|
+
/** 既存 PTY で claude を(再)起動する前の補正。PTY 作成時の環境変数のプロファイルを対象にする */
|
|
139
|
+
export async function prepareClaudeConfigForSession(session) {
|
|
140
|
+
await prepareClaudeConfig(session.appliedEnv);
|
|
141
|
+
}
|
|
@@ -21,48 +21,22 @@ export async function getAllEnv(scope, owner, repo) {
|
|
|
21
21
|
// 環境変数取得(優先順位: repo > owner > global)
|
|
22
22
|
export async function getEnv(scope, owner, repo) {
|
|
23
23
|
const result = {};
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
CLAUDE_TOKEN_KEYS.forEach((key) => delete result[key]);
|
|
29
|
-
};
|
|
30
|
-
// Helper: 環境変数リストにClaude認証トークンが含まれるか
|
|
31
|
-
const hasClaudeToken = (envs) => {
|
|
32
|
-
return envs.some((env) => CLAUDE_TOKEN_KEYS.includes(env.key));
|
|
24
|
+
const apply = (envs) => {
|
|
25
|
+
envs.forEach((env) => {
|
|
26
|
+
result[env.key] = env.value;
|
|
27
|
+
});
|
|
33
28
|
};
|
|
34
29
|
// グローバル変数を先に適用(有効なもののみ)
|
|
35
|
-
|
|
36
|
-
where: { scope: 'global', enabled: true },
|
|
37
|
-
});
|
|
38
|
-
globalEnvs.forEach((env) => {
|
|
39
|
-
result[env.key] = env.value;
|
|
40
|
-
});
|
|
30
|
+
apply(await prisma.environmentVariable.findMany({ where: { scope: 'global', enabled: true } }));
|
|
41
31
|
// owner変数を適用(上書き、有効なもののみ)
|
|
42
32
|
if (scope === 'owner' || scope === 'repo') {
|
|
43
|
-
|
|
44
|
-
where: { scope: 'owner', owner, enabled: true },
|
|
45
|
-
});
|
|
46
|
-
// Ownerスコープで新しいClaude認証トークンがあれば、Globalのものをクリア
|
|
47
|
-
if (hasClaudeToken(ownerEnvs)) {
|
|
48
|
-
clearClaudeTokens();
|
|
49
|
-
}
|
|
50
|
-
ownerEnvs.forEach((env) => {
|
|
51
|
-
result[env.key] = env.value;
|
|
52
|
-
});
|
|
33
|
+
apply(await prisma.environmentVariable.findMany({ where: { scope: 'owner', owner, enabled: true } }));
|
|
53
34
|
}
|
|
54
35
|
// repo変数を適用(上書き、有効なもののみ)
|
|
55
36
|
if (scope === 'repo') {
|
|
56
|
-
|
|
37
|
+
apply(await prisma.environmentVariable.findMany({
|
|
57
38
|
where: { scope: 'repo', owner, repo, enabled: true },
|
|
58
|
-
});
|
|
59
|
-
// Repoスコープで新しいClaude認証トークンがあれば、以前のものをクリア
|
|
60
|
-
if (hasClaudeToken(repoEnvs)) {
|
|
61
|
-
clearClaudeTokens();
|
|
62
|
-
}
|
|
63
|
-
repoEnvs.forEach((env) => {
|
|
64
|
-
result[env.key] = env.value;
|
|
65
|
-
});
|
|
39
|
+
}));
|
|
66
40
|
}
|
|
67
41
|
return result;
|
|
68
42
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 更新を適用するための再起動。Fastify はこの終了コードで終了し、apps/cli の app が残りを止めて
|
|
3
|
+
* 同じコードで終了すると、entry が新しいバージョンで起動し直す(apps/cli/src/versions.ts と合わせる)。
|
|
4
|
+
*/
|
|
5
|
+
export declare const RESTART_EXIT_CODE = 75;
|
|
6
|
+
/** index.ts が終了処理を登録する */
|
|
7
|
+
export declare function onRestartRequested(fn: () => void): void;
|
|
8
|
+
export declare function requestRestart(): void;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 更新を適用するための再起動。Fastify はこの終了コードで終了し、apps/cli の app が残りを止めて
|
|
3
|
+
* 同じコードで終了すると、entry が新しいバージョンで起動し直す(apps/cli/src/versions.ts と合わせる)。
|
|
4
|
+
*/
|
|
5
|
+
export const RESTART_EXIT_CODE = 75;
|
|
6
|
+
let handler = null;
|
|
7
|
+
/** index.ts が終了処理を登録する */
|
|
8
|
+
export function onRestartRequested(fn) {
|
|
9
|
+
handler = fn;
|
|
10
|
+
}
|
|
11
|
+
export function requestRestart() {
|
|
12
|
+
handler?.();
|
|
13
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { getAutoUpdateStatus, isEntryOutdated, onAutoUpdateChange, prepareUpdate, } from './auto-update.js';
|
|
2
|
+
// 公開パッケージの latest dist-tag のみを返す軽量エンドポイント(数KB)。
|
|
3
|
+
// TSUNAGI_NPM_REGISTRY でミラー・検証用 registry に差し替えられる(自動更新の install も同じ registry を使う)
|
|
4
|
+
const REGISTRY_BASE = (process.env.TSUNAGI_NPM_REGISTRY || 'https://registry.npmjs.org').replace(/\/+$/, '');
|
|
5
|
+
const REGISTRY_URL = `${REGISTRY_BASE}/@minimalcorp%2ftsunagi/latest`;
|
|
3
6
|
const CHECK_INTERVAL_MS = 60 * 60 * 1000;
|
|
4
7
|
const FETCH_TIMEOUT_MS = 10_000;
|
|
5
8
|
// 実行中のバージョンは apps/cli が自身の package.json から注入する。
|
|
@@ -29,7 +32,10 @@ export function isNewer(latest, current) {
|
|
|
29
32
|
return false;
|
|
30
33
|
}
|
|
31
34
|
export function getUpdateStatus() {
|
|
32
|
-
return { ...status };
|
|
35
|
+
return { ...status, autoUpdate: getAutoUpdateStatus(), entryOutdated: isEntryOutdated() };
|
|
36
|
+
}
|
|
37
|
+
function emitStatus() {
|
|
38
|
+
socket?.emit('version:status', getUpdateStatus());
|
|
33
39
|
}
|
|
34
40
|
/** registry を確認して status を更新する。確認できなければ false(前回の結果を保持) */
|
|
35
41
|
async function check() {
|
|
@@ -46,7 +52,10 @@ async function check() {
|
|
|
46
52
|
status.updateAvailable = isNewer(version, status.current);
|
|
47
53
|
status.checkedAt = new Date().toISOString();
|
|
48
54
|
// checkedAt も表示しているため、変化の有無に関わらず通知する
|
|
49
|
-
|
|
55
|
+
emitStatus();
|
|
56
|
+
// 新しいバージョンは自動でインストールしておき、再起動のタイミングだけユーザーに任せる
|
|
57
|
+
if (status.updateAvailable)
|
|
58
|
+
prepareUpdate(version);
|
|
50
59
|
return true;
|
|
51
60
|
}
|
|
52
61
|
catch {
|
|
@@ -62,6 +71,7 @@ export function startUpdateCheck(io) {
|
|
|
62
71
|
if (!status.current || timer)
|
|
63
72
|
return;
|
|
64
73
|
socket = io;
|
|
74
|
+
onAutoUpdateChange(emitStatus);
|
|
65
75
|
void check();
|
|
66
76
|
timer = setInterval(() => void check(), CHECK_INTERVAL_MS);
|
|
67
77
|
timer.unref();
|
|
@@ -1,8 +1,30 @@
|
|
|
1
1
|
import * as pty from 'node-pty';
|
|
2
|
+
/** PTY 作成時の付随情報(環境変数の変更を反映するために作り直すときに使う) */
|
|
3
|
+
export interface PtySessionOptions {
|
|
4
|
+
/** クライアントが指定した cwd / env */
|
|
5
|
+
request?: {
|
|
6
|
+
cwd?: string;
|
|
7
|
+
env?: Record<string, string>;
|
|
8
|
+
};
|
|
9
|
+
/** 作成時に claude を起動したか(作り直し後も claude を --resume で起動する) */
|
|
10
|
+
launchClaude?: boolean;
|
|
11
|
+
/** tsunagi の環境変数の変更を自動で反映する対象か(Claude のログイン用 PTY 等は対象外) */
|
|
12
|
+
syncEnv?: boolean;
|
|
13
|
+
}
|
|
2
14
|
export interface PtySession {
|
|
3
15
|
pty: pty.IPty;
|
|
4
16
|
sessionId: string;
|
|
5
17
|
cwd: string;
|
|
18
|
+
/** 作成時に渡した環境変数と取り除いたキー(変更検知の比較元) */
|
|
19
|
+
appliedEnv: Record<string, string>;
|
|
20
|
+
appliedUnsetKeys: string[];
|
|
21
|
+
options: PtySessionOptions;
|
|
22
|
+
/** tsunagi の環境変数が変わり、作り直し待ちの状態 */
|
|
23
|
+
envPending: boolean;
|
|
24
|
+
/** 環境変数の反映のために kill 中。onExit で「セッション終了」として扱わない */
|
|
25
|
+
respawning: boolean;
|
|
26
|
+
/** 最後にユーザー入力があった時刻(入力中の作り直しを避けるため) */
|
|
27
|
+
lastInputAt: number;
|
|
6
28
|
/** PTY出力のリングバッファ(再接続時にまとめて送信) */
|
|
7
29
|
scrollback: string[];
|
|
8
30
|
scrollbackSize: number;
|
|
@@ -24,7 +46,7 @@ declare class PtyManager {
|
|
|
24
46
|
* @param unsetKeys 外側 Terminal や DB 由来でも内部ターミナルに渡さない環境変数キー
|
|
25
47
|
* (例: Ollama タブでは Anthropic の認証トークンを取り除く)
|
|
26
48
|
*/
|
|
27
|
-
createSession(sessionId: string, cwd: string, env?: Record<string, string>, unsetKeys?: string[]): PtySession;
|
|
49
|
+
createSession(sessionId: string, cwd: string, env?: Record<string, string>, unsetKeys?: string[], options?: PtySessionOptions): PtySession;
|
|
28
50
|
getSession(sessionId: string): PtySession | undefined;
|
|
29
51
|
deleteSession(sessionId: string): void;
|
|
30
52
|
/**
|
|
@@ -39,6 +61,8 @@ declare class PtyManager {
|
|
|
39
61
|
*/
|
|
40
62
|
cancelGc(sessionId: string): void;
|
|
41
63
|
listSessions(): string[];
|
|
64
|
+
/** サーバー終了時に全セッションを止める(SIGHUP 任せにせず明示的に kill する) */
|
|
65
|
+
deleteAllSessions(): void;
|
|
42
66
|
}
|
|
43
67
|
export declare const ptyManager: PtyManager;
|
|
44
68
|
export {};
|
|
@@ -3,8 +3,9 @@ import * as os from 'os';
|
|
|
3
3
|
const SCROLLBACK_MAX_BYTES = 256 * 1024; // 256KB
|
|
4
4
|
const SESSION_GC_INTERVAL_MS = 30 * 60 * 1000; // 30分
|
|
5
5
|
/**
|
|
6
|
-
* apps/cli
|
|
7
|
-
* (TSUNAGI_SERVER_PORT, NODE_ENV, TSUNAGI_NEXT_PORT, TSUNAGI_OUTER_NODE_ENV
|
|
6
|
+
* apps/cli(entry・app)が Fastify サーバー起動時に自分で注入する環境変数
|
|
7
|
+
* (TSUNAGI_SERVER_PORT, NODE_ENV, TSUNAGI_NEXT_PORT, TSUNAGI_OUTER_NODE_ENV, TSUNAGI_PLUGIN_DIR,
|
|
8
|
+
* TSUNAGI_ENTRY_*, TSUNAGI_LAST_GOOD, TSUNAGI_AUTO_UPDATE, TSUNAGI_RESTARTED)。
|
|
8
9
|
* サーバープロセス自身の制御用に後から足された値のため、内部ターミナルには継承しない。
|
|
9
10
|
* 外側 Terminal 由来の環境変数はこのリストと無関係に、これまで通り全て継承する
|
|
10
11
|
* (PORT はここでは扱わない。apps/cli は generic な PORT を上書きせず、専用キー
|
|
@@ -21,7 +22,28 @@ const TSUNAGI_INJECTED_ENV_KEYS = [
|
|
|
21
22
|
'NODE_ENV',
|
|
22
23
|
'TSUNAGI_NEXT_PORT',
|
|
23
24
|
'TSUNAGI_OUTER_NODE_ENV',
|
|
25
|
+
'TSUNAGI_PLUGIN_DIR',
|
|
26
|
+
'TSUNAGI_ENTRY_PROTOCOL',
|
|
27
|
+
'TSUNAGI_ENTRY_PACKAGE_ROOT',
|
|
28
|
+
'TSUNAGI_ENTRY_OUTDATED',
|
|
29
|
+
'TSUNAGI_LAST_GOOD',
|
|
30
|
+
'TSUNAGI_AUTO_UPDATE',
|
|
31
|
+
'TSUNAGI_RESTARTED',
|
|
24
32
|
];
|
|
33
|
+
/**
|
|
34
|
+
* tsunagi の Claude Code プラグイン(hooks・MCP・skill)を、内部ターミナルで起動する claude に
|
|
35
|
+
* セッション単位で読み込ませる。ユーザー設定にはインストールしないため、tsunagi 外の claude には
|
|
36
|
+
* 影響しない。外側 Terminal や DB で CLAUDE_CODE_PLUGIN_DIRS が設定済みなら、その後ろに追加する。
|
|
37
|
+
*/
|
|
38
|
+
function addTsunagiPluginDir(env) {
|
|
39
|
+
const pluginDir = process.env.TSUNAGI_PLUGIN_DIR;
|
|
40
|
+
if (!pluginDir)
|
|
41
|
+
return;
|
|
42
|
+
const dirs = (env.CLAUDE_CODE_PLUGIN_DIRS ?? '').split(':').filter(Boolean);
|
|
43
|
+
if (!dirs.includes(pluginDir))
|
|
44
|
+
dirs.push(pluginDir);
|
|
45
|
+
env.CLAUDE_CODE_PLUGIN_DIRS = dirs.join(':');
|
|
46
|
+
}
|
|
25
47
|
class PtyManager {
|
|
26
48
|
sessions = new Map();
|
|
27
49
|
/** sessionId → 最後に input を送信した socket.id */
|
|
@@ -41,7 +63,7 @@ class PtyManager {
|
|
|
41
63
|
* @param unsetKeys 外側 Terminal や DB 由来でも内部ターミナルに渡さない環境変数キー
|
|
42
64
|
* (例: Ollama タブでは Anthropic の認証トークンを取り除く)
|
|
43
65
|
*/
|
|
44
|
-
createSession(sessionId, cwd, env, unsetKeys = []) {
|
|
66
|
+
createSession(sessionId, cwd, env = {}, unsetKeys = [], options = {}) {
|
|
45
67
|
if (this.sessions.has(sessionId)) {
|
|
46
68
|
throw new Error(`Session already exists: ${sessionId}`);
|
|
47
69
|
}
|
|
@@ -67,6 +89,7 @@ class PtyManager {
|
|
|
67
89
|
for (const key of unsetKeys) {
|
|
68
90
|
delete spawnEnv[key];
|
|
69
91
|
}
|
|
92
|
+
addTsunagiPluginDir(spawnEnv);
|
|
70
93
|
const ptyProcess = pty.spawn(shell, [], {
|
|
71
94
|
name: 'xterm-256color',
|
|
72
95
|
cols: 80,
|
|
@@ -79,6 +102,12 @@ class PtyManager {
|
|
|
79
102
|
pty: ptyProcess,
|
|
80
103
|
sessionId,
|
|
81
104
|
cwd,
|
|
105
|
+
appliedEnv: env,
|
|
106
|
+
appliedUnsetKeys: unsetKeys,
|
|
107
|
+
options,
|
|
108
|
+
envPending: false,
|
|
109
|
+
respawning: false,
|
|
110
|
+
lastInputAt: 0,
|
|
82
111
|
scrollback: [],
|
|
83
112
|
scrollbackSize: 0,
|
|
84
113
|
lastOutputAt: now,
|
|
@@ -163,5 +192,11 @@ class PtyManager {
|
|
|
163
192
|
listSessions() {
|
|
164
193
|
return Array.from(this.sessions.keys());
|
|
165
194
|
}
|
|
195
|
+
/** サーバー終了時に全セッションを止める(SIGHUP 任せにせず明示的に kill する) */
|
|
196
|
+
deleteAllSessions() {
|
|
197
|
+
for (const sessionId of Array.from(this.sessions.keys())) {
|
|
198
|
+
this.deleteSession(sessionId);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
166
201
|
}
|
|
167
202
|
export const ptyManager = new PtyManager();
|