@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,138 @@
|
|
|
1
|
+
import * as os from 'os';
|
|
2
|
+
import { ptyManager } from '../pty-manager.js';
|
|
3
|
+
import { prisma } from '../lib/db.js';
|
|
4
|
+
import { deleteEnv, getEnv, setEnv } from '../lib/repositories/environment.js';
|
|
5
|
+
import { CLAUDE_PROFILE_ENV_KEY, DEFAULT_CLAUDE_PROFILE, buildClaudeLoginCommand, claudeLoginPtyEnv, claudeProfileExists, createClaudeProfile, deleteClaudeProfile, getClaudeAuthStatus, listClaudeProfiles, } from '../lib/claude-profiles.js';
|
|
6
|
+
import { notifyEnvChanged } from '../lib/pty-env-sync.js';
|
|
7
|
+
/** ログイン用 PTY の sessionId の接頭辞(タブの PTY と区別する) */
|
|
8
|
+
const LOGIN_SESSION_PREFIX = 'claude-login-';
|
|
9
|
+
function validateScope(scope, owner, repo) {
|
|
10
|
+
if (scope !== 'global' && scope !== 'owner' && scope !== 'repo')
|
|
11
|
+
return 'Invalid scope';
|
|
12
|
+
if (scope === 'owner' && !owner)
|
|
13
|
+
return 'owner is required for scope=owner';
|
|
14
|
+
if (scope === 'repo' && (!owner || !repo))
|
|
15
|
+
return 'owner and repo are required for scope=repo';
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
async function getAssignment(scope, owner, repo) {
|
|
19
|
+
const row = await prisma.environmentVariable.findFirst({
|
|
20
|
+
where: {
|
|
21
|
+
key: CLAUDE_PROFILE_ENV_KEY,
|
|
22
|
+
scope,
|
|
23
|
+
...(scope !== 'global' && { owner }),
|
|
24
|
+
...(scope === 'repo' && { repo }),
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
const merged = await getEnv(scope, owner, repo);
|
|
28
|
+
const assignment = {
|
|
29
|
+
assigned: row?.value ?? null,
|
|
30
|
+
effective: merged[CLAUDE_PROFILE_ENV_KEY] ?? DEFAULT_CLAUDE_PROFILE,
|
|
31
|
+
};
|
|
32
|
+
return assignment;
|
|
33
|
+
}
|
|
34
|
+
function syncPtyEnv(fastify) {
|
|
35
|
+
notifyEnvChanged().catch((err) => fastify.log.warn(err, 'Failed to sync PTY env'));
|
|
36
|
+
}
|
|
37
|
+
export async function claudeProfilesRoutes(fastify) {
|
|
38
|
+
// GET /claude-profiles - システム既定 + 登録済みプロファイルとログイン状態
|
|
39
|
+
fastify.get('/claude-profiles', async (_request, reply) => {
|
|
40
|
+
const profiles = await listClaudeProfiles();
|
|
41
|
+
const data = await Promise.all([
|
|
42
|
+
getClaudeAuthStatus(DEFAULT_CLAUDE_PROFILE).then((status) => ({
|
|
43
|
+
slug: DEFAULT_CLAUDE_PROFILE,
|
|
44
|
+
name: 'System default',
|
|
45
|
+
// CLAUDE_CONFIG_DIR を付けない(普段ターミナルで claude を使うときと同じ設定)
|
|
46
|
+
configDir: null,
|
|
47
|
+
status,
|
|
48
|
+
})),
|
|
49
|
+
...profiles.map(async (profile) => ({
|
|
50
|
+
...profile,
|
|
51
|
+
status: await getClaudeAuthStatus(profile.slug),
|
|
52
|
+
})),
|
|
53
|
+
]);
|
|
54
|
+
return reply.send({ data: { profiles: data } });
|
|
55
|
+
});
|
|
56
|
+
// GET /claude-profiles/:slug/status - ログイン状態(ログイン用ダイアログで完了を確認する)
|
|
57
|
+
fastify.get('/claude-profiles/:slug/status', async (request, reply) => {
|
|
58
|
+
const { slug } = request.params;
|
|
59
|
+
if (!(await claudeProfileExists(slug))) {
|
|
60
|
+
return reply.status(404).send({ error: 'Profile not found' });
|
|
61
|
+
}
|
|
62
|
+
return reply.send({ data: { status: await getClaudeAuthStatus(slug) } });
|
|
63
|
+
});
|
|
64
|
+
// POST /claude-profiles - プロファイル作成
|
|
65
|
+
fastify.post('/claude-profiles', async (request, reply) => {
|
|
66
|
+
try {
|
|
67
|
+
const profile = await createClaudeProfile(request.body?.name ?? '');
|
|
68
|
+
return reply.status(201).send({ data: { profile } });
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
return reply.status(400).send({ error: err instanceof Error ? err.message : String(err) });
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
// DELETE /claude-profiles/:slug - プロファイル削除(logout してディレクトリごと消す)
|
|
75
|
+
fastify.delete('/claude-profiles/:slug', async (request, reply) => {
|
|
76
|
+
const { slug } = request.params;
|
|
77
|
+
if (slug === DEFAULT_CLAUDE_PROFILE || !(await claudeProfileExists(slug))) {
|
|
78
|
+
return reply.status(404).send({ error: 'Profile not found' });
|
|
79
|
+
}
|
|
80
|
+
await deleteClaudeProfile(slug);
|
|
81
|
+
syncPtyEnv(fastify);
|
|
82
|
+
return reply.status(204).send();
|
|
83
|
+
});
|
|
84
|
+
// POST /claude-profiles/:slug/login - ログイン用 PTY を起動し `claude auth login` を実行する
|
|
85
|
+
fastify.post('/claude-profiles/:slug/login', async (request, reply) => {
|
|
86
|
+
const { slug } = request.params;
|
|
87
|
+
if (!(await claudeProfileExists(slug))) {
|
|
88
|
+
return reply.status(404).send({ error: 'Profile not found' });
|
|
89
|
+
}
|
|
90
|
+
// 同じプロファイルのログイン用 PTY が残っていれば閉じる
|
|
91
|
+
const prefix = `${LOGIN_SESSION_PREFIX}${slug}-`;
|
|
92
|
+
for (const id of ptyManager.listSessions()) {
|
|
93
|
+
if (id.startsWith(prefix))
|
|
94
|
+
ptyManager.deleteSession(id);
|
|
95
|
+
}
|
|
96
|
+
const sessionId = `${prefix}${crypto.randomUUID()}`;
|
|
97
|
+
const { env, unsetKeys } = claudeLoginPtyEnv(slug);
|
|
98
|
+
const session = ptyManager.createSession(sessionId, os.homedir(), env, unsetKeys);
|
|
99
|
+
// シェルの初期化(プロンプト表示)を待つため少し遅延させて書き込む
|
|
100
|
+
setTimeout(() => session.pty.write(`${buildClaudeLoginCommand()}\n`), 300);
|
|
101
|
+
return reply.status(201).send({ data: { sessionId } });
|
|
102
|
+
});
|
|
103
|
+
// DELETE /claude-profiles/login/:sessionId - ログイン用 PTY を閉じる
|
|
104
|
+
fastify.delete('/claude-profiles/login/:sessionId', async (request, reply) => {
|
|
105
|
+
const { sessionId } = request.params;
|
|
106
|
+
if (!sessionId.startsWith(LOGIN_SESSION_PREFIX)) {
|
|
107
|
+
return reply.status(400).send({ error: 'Not a login session' });
|
|
108
|
+
}
|
|
109
|
+
ptyManager.deleteSession(sessionId);
|
|
110
|
+
return reply.status(204).send();
|
|
111
|
+
});
|
|
112
|
+
// GET /claude-profiles/assignment - スコープのプロファイル割り当て
|
|
113
|
+
fastify.get('/claude-profiles/assignment', async (request, reply) => {
|
|
114
|
+
const { scope, owner, repo } = request.query;
|
|
115
|
+
const error = validateScope(scope, owner, repo);
|
|
116
|
+
if (error)
|
|
117
|
+
return reply.status(400).send({ error });
|
|
118
|
+
return reply.send({ data: { assignment: await getAssignment(scope, owner, repo) } });
|
|
119
|
+
});
|
|
120
|
+
// PUT /claude-profiles/assignment - スコープのプロファイル割り当てを変更する
|
|
121
|
+
fastify.put('/claude-profiles/assignment', async (request, reply) => {
|
|
122
|
+
const { scope, owner, repo, slug } = request.body ?? {};
|
|
123
|
+
const error = validateScope(scope, owner, repo);
|
|
124
|
+
if (error)
|
|
125
|
+
return reply.status(400).send({ error });
|
|
126
|
+
if (slug !== null && !(await claudeProfileExists(slug))) {
|
|
127
|
+
return reply.status(400).send({ error: 'Profile not found' });
|
|
128
|
+
}
|
|
129
|
+
if (slug === null) {
|
|
130
|
+
await deleteEnv(CLAUDE_PROFILE_ENV_KEY, scope, owner, repo);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
await setEnv(CLAUDE_PROFILE_ENV_KEY, slug, scope, owner, repo);
|
|
134
|
+
}
|
|
135
|
+
syncPtyEnv(fastify);
|
|
136
|
+
return reply.send({ data: { assignment: await getAssignment(scope, owner, repo) } });
|
|
137
|
+
});
|
|
138
|
+
}
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import * as envRepo from '../lib/repositories/environment.js';
|
|
2
|
+
import { RESERVED_ENV_KEYS } from '../lib/claude-profiles.js';
|
|
3
|
+
import { notifyEnvChanged } from '../lib/pty-env-sync.js';
|
|
4
|
+
const RESERVED_KEY_ERROR = 'Claude の認証は Settings の Claude Profile で設定してください';
|
|
5
|
+
/** 変更後、起動中のターミナルに反映する(失敗しても API 自体は成功扱い) */
|
|
6
|
+
function syncPtyEnv(fastify) {
|
|
7
|
+
notifyEnvChanged().catch((err) => fastify.log.warn(err, 'Failed to sync PTY env'));
|
|
8
|
+
}
|
|
2
9
|
export async function envRoutes(fastify) {
|
|
3
10
|
// GET /env
|
|
4
11
|
fastify.get('/env', async (request, reply) => {
|
|
@@ -30,6 +37,9 @@ export async function envRoutes(fastify) {
|
|
|
30
37
|
if (!key || !value || !scope) {
|
|
31
38
|
return reply.status(400).send({ error: 'Missing required fields: key, value, scope' });
|
|
32
39
|
}
|
|
40
|
+
if (RESERVED_ENV_KEYS.includes(key)) {
|
|
41
|
+
return reply.status(400).send({ error: RESERVED_KEY_ERROR });
|
|
42
|
+
}
|
|
33
43
|
if (scope === 'owner' && !owner) {
|
|
34
44
|
return reply.status(400).send({ error: 'Missing required field: owner for scope=owner' });
|
|
35
45
|
}
|
|
@@ -39,6 +49,7 @@ export async function envRoutes(fastify) {
|
|
|
39
49
|
});
|
|
40
50
|
}
|
|
41
51
|
await envRepo.setEnv(key, value, scope, owner, repo);
|
|
52
|
+
syncPtyEnv(fastify);
|
|
42
53
|
return reply.status(201).send({ data: { success: true } });
|
|
43
54
|
}
|
|
44
55
|
catch (error) {
|
|
@@ -53,6 +64,9 @@ export async function envRoutes(fastify) {
|
|
|
53
64
|
if (!key || !value || !scope) {
|
|
54
65
|
return reply.status(400).send({ error: 'Missing required fields: key, value, scope' });
|
|
55
66
|
}
|
|
67
|
+
if (RESERVED_ENV_KEYS.includes(key)) {
|
|
68
|
+
return reply.status(400).send({ error: RESERVED_KEY_ERROR });
|
|
69
|
+
}
|
|
56
70
|
if (scope === 'owner' && !owner) {
|
|
57
71
|
return reply.status(400).send({ error: 'Missing required field: owner for scope=owner' });
|
|
58
72
|
}
|
|
@@ -62,6 +76,7 @@ export async function envRoutes(fastify) {
|
|
|
62
76
|
});
|
|
63
77
|
}
|
|
64
78
|
await envRepo.setEnv(key, value, scope, owner, repo);
|
|
79
|
+
syncPtyEnv(fastify);
|
|
65
80
|
return reply.status(200).send({ data: { success: true } });
|
|
66
81
|
}
|
|
67
82
|
catch (error) {
|
|
@@ -80,6 +95,9 @@ export async function envRoutes(fastify) {
|
|
|
80
95
|
error: 'Missing required query parameters: key, scope',
|
|
81
96
|
});
|
|
82
97
|
}
|
|
98
|
+
if (RESERVED_ENV_KEYS.includes(key)) {
|
|
99
|
+
return reply.status(400).send({ error: RESERVED_KEY_ERROR });
|
|
100
|
+
}
|
|
83
101
|
if (scope === 'owner' && !owner) {
|
|
84
102
|
return reply.status(400).send({
|
|
85
103
|
error: 'Missing required query parameter: owner for scope=owner',
|
|
@@ -94,6 +112,7 @@ export async function envRoutes(fastify) {
|
|
|
94
112
|
if (!success) {
|
|
95
113
|
return reply.status(404).send({ error: 'Environment variable not found' });
|
|
96
114
|
}
|
|
115
|
+
syncPtyEnv(fastify);
|
|
97
116
|
return reply.status(200).send({ data: { success: true } });
|
|
98
117
|
}
|
|
99
118
|
catch (error) {
|
|
@@ -116,7 +135,8 @@ export async function envRoutes(fastify) {
|
|
|
116
135
|
error: 'Missing required query parameters: owner, repo for scope=repo',
|
|
117
136
|
});
|
|
118
137
|
}
|
|
119
|
-
|
|
138
|
+
// 予約キー(Claude プロファイルの割り当て)は専用 UI で扱うため一覧に含めない
|
|
139
|
+
const envVars = (await envRepo.getAllEnv(scope, owner || undefined, repo || undefined)).filter((env) => !RESERVED_ENV_KEYS.includes(env.key));
|
|
120
140
|
return reply.status(200).send({ data: { envVars } });
|
|
121
141
|
}
|
|
122
142
|
catch (error) {
|
|
@@ -131,7 +151,11 @@ export async function envRoutes(fastify) {
|
|
|
131
151
|
if (!key || !scope || typeof enabled !== 'boolean') {
|
|
132
152
|
return reply.status(400).send({ error: 'key, scope, and enabled are required' });
|
|
133
153
|
}
|
|
154
|
+
if (RESERVED_ENV_KEYS.includes(key)) {
|
|
155
|
+
return reply.status(400).send({ error: RESERVED_KEY_ERROR });
|
|
156
|
+
}
|
|
134
157
|
await envRepo.toggleEnv(key, scope, enabled, owner, repo);
|
|
158
|
+
syncPtyEnv(fastify);
|
|
135
159
|
return reply.status(200).send({ data: { success: true } });
|
|
136
160
|
}
|
|
137
161
|
catch (error) {
|
|
@@ -1,15 +1,24 @@
|
|
|
1
|
-
import { getEnv } from '../lib/repositories/environment.js';
|
|
2
1
|
import { isLocalLlmReady } from '../lib/local-llm.js';
|
|
2
|
+
import { DEFAULT_CLAUDE_PROFILE, getClaudeAuthStatus, listClaudeProfiles, } from '../lib/claude-profiles.js';
|
|
3
|
+
/** システム既定かいずれかの Claude プロファイルがログイン済みか */
|
|
4
|
+
async function isClaudeLoggedIn() {
|
|
5
|
+
if ((await getClaudeAuthStatus(DEFAULT_CLAUDE_PROFILE)).loggedIn)
|
|
6
|
+
return true;
|
|
7
|
+
const profiles = await listClaudeProfiles();
|
|
8
|
+
const statuses = await Promise.all(profiles.map((p) => getClaudeAuthStatus(p.slug)));
|
|
9
|
+
return statuses.some((status) => status.loggedIn);
|
|
10
|
+
}
|
|
3
11
|
export async function onboardingRoutes(fastify) {
|
|
4
12
|
// GET /onboarding/status
|
|
5
13
|
fastify.get('/onboarding/status', async (_request, reply) => {
|
|
6
14
|
try {
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
15
|
+
const [claudeLoggedIn, localLlmReady] = await Promise.all([
|
|
16
|
+
isClaudeLoggedIn(),
|
|
17
|
+
// 実験的機能のローカルLLM(Ollama / LM Studio)を設定済みなら、Claude にログインしていなくても claude を起動できる
|
|
18
|
+
isLocalLlmReady(),
|
|
19
|
+
]);
|
|
11
20
|
return reply.status(200).send({
|
|
12
|
-
data: { completed:
|
|
21
|
+
data: { completed: claudeLoggedIn || localLlmReady, claudeLoggedIn, localLlmReady },
|
|
13
22
|
});
|
|
14
23
|
}
|
|
15
24
|
catch (error) {
|
|
@@ -1,23 +1,9 @@
|
|
|
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
1
|
import { ptyManager } from '../pty-manager.js';
|
|
6
2
|
import { prisma } from '../lib/db.js';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import { ensureActiveLoaded, getLocalLlmSettings } from '../lib/local-llm.js';
|
|
12
|
-
import { LOCAL_LLM_PROXY_URL, buildClaudeCommand, isLocalLlmMode } from '../lib/claude-command.js';
|
|
13
|
-
// このファイル基準で解決する(cwd 非依存):
|
|
14
|
-
// dev: apps/server/src/routes → apps/server/scripts
|
|
15
|
-
// build: apps/server/dist/routes → apps/server/scripts
|
|
16
|
-
// npm bundle: <pkg>/dist/server/routes → <pkg>/dist/scripts
|
|
17
|
-
const TSUNAGI_EDITOR_PATH = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../scripts/monaco-editor.sh');
|
|
18
|
-
// Fastify(API) の公開ポート。index.ts と同じ既定値・同じ環境変数(TSUNAGI_SERVER_PORT)を見る。
|
|
19
|
-
// monaco-editor.sh など PTY 内のプロセスが API を叩く際の同一ホスト向けベース URL に使う。
|
|
20
|
-
const SERVER_PORT = Number(process.env.TSUNAGI_SERVER_PORT) || 2791;
|
|
3
|
+
import { ensureActiveLoaded } from '../lib/local-llm.js';
|
|
4
|
+
import { buildClaudeCommand, isLocalLlmMode } from '../lib/claude-command.js';
|
|
5
|
+
import { PtyLaunchError, prepareClaudeConfigForSession, resolvePtyLaunch, startPtySession, } from '../lib/pty-launch.js';
|
|
6
|
+
import { getEnvPending, initPtyEnvSync, respawnSessionNow } from '../lib/pty-env-sync.js';
|
|
21
7
|
/**
|
|
22
8
|
* 再接続時の画面復元用 scrollback から「端末への問い合わせ(クエリ)」シーケンスを除去する。
|
|
23
9
|
*
|
|
@@ -41,24 +27,9 @@ function stripTerminalQueries(s) {
|
|
|
41
27
|
.replace(/\x1b\[[?>]?[0-9;]*n/g, '') // DSR: ESC[?6n(主犯) / ESC[6n / ESC[5n など
|
|
42
28
|
.replace(/\x1b\[[?>=]?[0-9;]*c/g, ''); // DA: ESC[c / ESC[0c / ESC[>c / ESC[=c
|
|
43
29
|
}
|
|
44
|
-
/**
|
|
45
|
-
* ローカルLLMタブで注入する環境変数を返す。Claude Code は tsunagi の中継口に接続し、
|
|
46
|
-
* 中継口が使用中のモデルへ転送する。最初の発言を待たせないよう、読み込みはここで始めておく(完了は待たない)
|
|
47
|
-
*/
|
|
48
|
-
async function prepareLocalLlm() {
|
|
49
|
-
const { active, extraEnv } = await getLocalLlmSettings();
|
|
50
|
-
if (!active) {
|
|
51
|
-
throw new Error('ローカルLLMのモデルが未設定です。Settings で使用するモデルを選んでください');
|
|
52
|
-
}
|
|
53
|
-
ensureActiveLoaded().catch(() => undefined);
|
|
54
|
-
return buildLocalLlmEnv({
|
|
55
|
-
proxyUrl: LOCAL_LLM_PROXY_URL,
|
|
56
|
-
contextTokens: active.contextTokens,
|
|
57
|
-
extraEnv,
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
30
|
export async function terminalRoutes(fastify) {
|
|
61
31
|
const io = fastify.io;
|
|
32
|
+
initPtyEnvSync(io);
|
|
62
33
|
// socket.io イベントハンドラ
|
|
63
34
|
io.on('connection', (socket) => {
|
|
64
35
|
let boundSessionId = null;
|
|
@@ -97,6 +68,8 @@ export async function terminalRoutes(fastify) {
|
|
|
97
68
|
socket.removeAllListeners('resize');
|
|
98
69
|
socket.removeAllListeners('input');
|
|
99
70
|
socket.join(room);
|
|
71
|
+
// 環境変数の変更が未反映ならクライアントに伝える(反映待ちバッジ)
|
|
72
|
+
socket.emit('env-pending', { sessionId, pending: getEnvPending(sessionId) });
|
|
100
73
|
// 接続確立 → GCタイマーをキャンセル
|
|
101
74
|
ptyManager.cancelGc(sessionId);
|
|
102
75
|
// この socket を PTY の所有者(active socket)にする。input の書き込みは所有者の
|
|
@@ -133,6 +106,10 @@ export async function terminalRoutes(fastify) {
|
|
|
133
106
|
dataHandlerDispose = () => dataHandler.dispose();
|
|
134
107
|
// PTYプロセス終了 → room全体に通知(全接続クライアントに終了を伝える)+ セッション削除
|
|
135
108
|
const exitHandler = ptyProcess.onExit(({ exitCode }) => {
|
|
109
|
+
// 環境変数の反映のための作り直し(pty-env-sync)。新しい PTY が同じ sessionId で
|
|
110
|
+
// 既に動いているため、終了扱い・セッション削除はしない
|
|
111
|
+
if (session.respawning)
|
|
112
|
+
return;
|
|
136
113
|
io.to(room).emit('exit', { exitCode });
|
|
137
114
|
// Ctrl+C等でClaudeが強制終了した場合にclaudeStatusをidleにリセット
|
|
138
115
|
// todos は残す(Claude 側の Task はセッションに永続化され、--resume 後も続きから使われる)
|
|
@@ -171,6 +148,7 @@ export async function terminalRoutes(fastify) {
|
|
|
171
148
|
return;
|
|
172
149
|
if (ptyManager.getActiveSocket(sessionId) !== socket.id)
|
|
173
150
|
return;
|
|
151
|
+
session.lastInputAt = Date.now();
|
|
174
152
|
ptyProcess.write(data);
|
|
175
153
|
});
|
|
176
154
|
});
|
|
@@ -204,8 +182,7 @@ export async function terminalRoutes(fastify) {
|
|
|
204
182
|
});
|
|
205
183
|
// POST /terminal/sessions - セッション作成(または既存セッション再利用)
|
|
206
184
|
fastify.post('/terminal/sessions', async (request, reply) => {
|
|
207
|
-
const { cwd, env, sessionId: requestedSessionId, claude } = request.body ?? {};
|
|
208
|
-
let { command } = request.body ?? {};
|
|
185
|
+
const { cwd, env, sessionId: requestedSessionId, claude, command } = request.body ?? {};
|
|
209
186
|
const sessionId = requestedSessionId ?? crypto.randomUUID();
|
|
210
187
|
// 既存セッションが生きていれば再利用
|
|
211
188
|
const existing = ptyManager.getSession(sessionId);
|
|
@@ -213,95 +190,22 @@ export async function terminalRoutes(fastify) {
|
|
|
213
190
|
fastify.log.info({ sessionId }, 'Reusing existing PTY session');
|
|
214
191
|
return reply.status(200).send({ sessionId, reused: true });
|
|
215
192
|
}
|
|
216
|
-
|
|
217
|
-
// Taskに紐づかないタブは Tab レコードが存在しないため null になる。
|
|
218
|
-
const tab = await prisma.tab
|
|
219
|
-
.findUnique({ where: { tabId: sessionId }, include: { task: true } })
|
|
220
|
-
.catch((err) => {
|
|
221
|
-
fastify.log.warn({ err, sessionId }, 'Failed to load tab');
|
|
222
|
-
return null;
|
|
223
|
-
});
|
|
224
|
-
// cwd は Task があればサーバー側で worktree パスを導出する(クライアントの state に依存しない)。
|
|
225
|
-
// クライアント指定の cwd は Task に紐づかないタブのフォールバックとしてのみ使う。
|
|
226
|
-
const defaultDir = path.join(os.homedir(), '.tsunagi', 'workspaces');
|
|
227
|
-
let workingDir = tab
|
|
228
|
-
? getWorktreePath(tab.task.owner, tab.task.repo, tab.task.branch)
|
|
229
|
-
: (cwd ?? defaultDir);
|
|
230
|
-
// cwd が存在するか確認。Task のタブは worktree 必須(無ければ起動しない)、
|
|
231
|
-
// それ以外はデフォルトにフォールバック
|
|
193
|
+
const launchRequest = { cwd, env };
|
|
232
194
|
try {
|
|
233
|
-
await
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
}
|
|
240
|
-
fastify.log.warn({ sessionId, workingDir }, 'cwd does not exist, falling back to default');
|
|
241
|
-
workingDir = defaultDir;
|
|
242
|
-
}
|
|
243
|
-
// デフォルトディレクトリが存在しない場合も作成する
|
|
244
|
-
await fs.mkdir(workingDir, { recursive: true });
|
|
245
|
-
try {
|
|
246
|
-
// repo スコープまで階層マージした環境変数を取得する(global → owner → repo、後勝ちで上書き)。
|
|
247
|
-
// Taskに紐づかないタブは global のみになる。
|
|
248
|
-
let dbEnv = {};
|
|
249
|
-
const tabMode = tab?.mode;
|
|
250
|
-
try {
|
|
251
|
-
dbEnv = tab ? await getEnv('repo', tab.task.owner, tab.task.repo) : await getEnv('global');
|
|
252
|
-
fastify.log.info({ count: Object.keys(dbEnv).length, owner: tab?.task.owner, repo: tab?.task.repo }, 'Loaded env vars');
|
|
253
|
-
}
|
|
254
|
-
catch (err) {
|
|
255
|
-
fastify.log.warn({ err }, 'Failed to load env vars');
|
|
256
|
-
}
|
|
257
|
-
// ローカルLLMタブ: Anthropic の代わりに tsunagi の中継口(→ Ollama / LM Studio)で claude を動かす。
|
|
258
|
-
// DB の環境変数より後に適用し、Anthropic の認証トークンは環境変数ごと取り除く。
|
|
259
|
-
let providerEnv = {};
|
|
260
|
-
let unsetKeys = [];
|
|
261
|
-
if (isLocalLlmMode(tabMode)) {
|
|
262
|
-
try {
|
|
263
|
-
providerEnv = await prepareLocalLlm();
|
|
264
|
-
}
|
|
265
|
-
catch (err) {
|
|
266
|
-
return reply
|
|
267
|
-
.status(400)
|
|
268
|
-
.send({ error: err instanceof Error ? err.message : String(err) });
|
|
269
|
-
}
|
|
270
|
-
unsetKeys = LOCAL_LLM_UNSET_ENV_KEYS;
|
|
271
|
-
}
|
|
272
|
-
if (claude) {
|
|
273
|
-
command = await buildClaudeCommand(sessionId, (tabMode ?? 'claude'));
|
|
274
|
-
}
|
|
275
|
-
// 優先順位: リクエストで渡されたenv > ローカルLLMのprovider env > DB env(global/owner/repo) > tsunagi独自のEDITOR設定
|
|
276
|
-
// tsunagi-editor.sh をデフォルトにすることで Ctrl+G が Monaco Modal を開く。
|
|
277
|
-
// DB / リクエストで EDITOR が明示設定されている場合はそちらが優先される。
|
|
278
|
-
const tsunagiDefaultEnv = {
|
|
279
|
-
EDITOR: TSUNAGI_EDITOR_PATH,
|
|
280
|
-
TSUNAGI_SESSION_ID: sessionId,
|
|
281
|
-
// monaco-editor.sh が API(Fastify) を叩くためのベース URL。サーバーと同一
|
|
282
|
-
// ホストなので localhost。単一ポート化で API は SERVER_PORT(既定 2791)。
|
|
283
|
-
TSUNAGI_API_BASE: `http://localhost:${SERVER_PORT}`,
|
|
284
|
-
};
|
|
285
|
-
const mergedEnv = { ...tsunagiDefaultEnv, ...dbEnv, ...providerEnv, ...env };
|
|
286
|
-
const session = ptyManager.createSession(sessionId, workingDir, mergedEnv, unsetKeys);
|
|
287
|
-
// コマンドが指定されていればPTY起動後にシェルへ書き込む
|
|
288
|
-
if (command) {
|
|
289
|
-
const cmd = command.endsWith('\n') ? command : command + '\n';
|
|
290
|
-
// claude logout で ~/.claude.json の hasCompletedOnboarding がリセットされて
|
|
291
|
-
// いると、次回 claude 起動時にオンボーディングウィザードが表示され --resume/
|
|
292
|
-
// --session-id を前提にした自動起動フローが止まるため、起動前に補正する。
|
|
293
|
-
await ensureClaudeOnboardingCompleted();
|
|
294
|
-
// 監視の取りこぼしに備え、起動前にも中継口の名前が既定モデルに残っていないか補正する
|
|
295
|
-
await removeLocalModelFromUserSettings(LOCAL_MODEL_ALIAS);
|
|
296
|
-
// シェルの初期化(プロンプト表示)を待つため少し遅延させて書き込む
|
|
297
|
-
setTimeout(() => {
|
|
298
|
-
session.pty.write(cmd);
|
|
299
|
-
}, 300);
|
|
300
|
-
}
|
|
195
|
+
const launch = await resolvePtyLaunch(sessionId, launchRequest);
|
|
196
|
+
await startPtySession(sessionId, launch, {
|
|
197
|
+
request: launchRequest,
|
|
198
|
+
launchClaude: Boolean(claude),
|
|
199
|
+
command,
|
|
200
|
+
});
|
|
301
201
|
return reply.status(201).send({ sessionId, reused: false });
|
|
302
202
|
}
|
|
303
203
|
catch (err) {
|
|
304
204
|
const message = err instanceof Error ? err.message : String(err);
|
|
205
|
+
if (err instanceof PtyLaunchError) {
|
|
206
|
+
fastify.log.warn({ sessionId, err: message }, 'Failed to launch PTY');
|
|
207
|
+
return reply.status(err.statusCode).send({ error: message });
|
|
208
|
+
}
|
|
305
209
|
return reply.status(500).send({ error: message });
|
|
306
210
|
}
|
|
307
211
|
});
|
|
@@ -326,9 +230,19 @@ export async function terminalRoutes(fastify) {
|
|
|
326
230
|
catch (err) {
|
|
327
231
|
return reply.status(400).send({ error: err instanceof Error ? err.message : String(err) });
|
|
328
232
|
}
|
|
233
|
+
// 環境変数の反映で PTY を作り直したときも claude を起動し直す
|
|
234
|
+
session.options.launchClaude = true;
|
|
235
|
+
await prepareClaudeConfigForSession(session);
|
|
329
236
|
session.pty.write(`${command}\n`);
|
|
330
237
|
return reply.status(204).send();
|
|
331
238
|
});
|
|
239
|
+
// POST /terminal/sessions/:sessionId/respawn - 環境変数の変更を今すぐ反映する(PTY を作り直す)
|
|
240
|
+
fastify.post('/terminal/sessions/:sessionId/respawn', async (request, reply) => {
|
|
241
|
+
const ok = await respawnSessionNow(request.params.sessionId);
|
|
242
|
+
if (!ok)
|
|
243
|
+
return reply.status(404).send({ error: 'Session not found' });
|
|
244
|
+
return reply.status(204).send();
|
|
245
|
+
});
|
|
332
246
|
// DELETE /terminal/sessions/:sessionId - セッション明示削除(PTY kill)
|
|
333
247
|
fastify.delete('/terminal/sessions/:sessionId', async (request, reply) => {
|
|
334
248
|
const { sessionId } = request.params;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { findRestartBlockers, getAutoUpdateStatus } from '../lib/auto-update.js';
|
|
2
|
+
import { requestRestart } from '../lib/restart.js';
|
|
1
3
|
import { checkForUpdateNow, getUpdateStatus } from '../lib/update-check.js';
|
|
2
4
|
export async function versionRoutes(fastify) {
|
|
3
5
|
// GET /version - 実行中のバージョンと npm の最新バージョン
|
|
@@ -15,4 +17,18 @@ export async function versionRoutes(fastify) {
|
|
|
15
17
|
}
|
|
16
18
|
return reply.status(200).send({ data: getUpdateStatus() });
|
|
17
19
|
});
|
|
20
|
+
// POST /version/restart - インストール済みの新しいバージョンを再起動で適用する。
|
|
21
|
+
// 実行中(running / waiting)のタスクがあれば止まってしまうため 409 で拒否する
|
|
22
|
+
fastify.post('/version/restart', async (_request, reply) => {
|
|
23
|
+
if (getAutoUpdateStatus().state !== 'ready') {
|
|
24
|
+
return reply.status(400).send({ error: 'No update is ready to apply' });
|
|
25
|
+
}
|
|
26
|
+
const blockers = await findRestartBlockers();
|
|
27
|
+
if (blockers.length > 0) {
|
|
28
|
+
return reply.status(409).send({ error: 'Tasks are running', data: { blockers } });
|
|
29
|
+
}
|
|
30
|
+
// 応答を返してから終了処理に入る
|
|
31
|
+
reply.raw.once('finish', requestRestart);
|
|
32
|
+
return reply.status(202).send({ data: getUpdateStatus() });
|
|
33
|
+
});
|
|
18
34
|
}
|
|
@@ -7,7 +7,8 @@ import * as path from 'node:path';
|
|
|
7
7
|
* - Writes current PID to `~/.tsunagi/state/tsunagi.lock` on acquire
|
|
8
8
|
* - On startup, if lock file exists and its PID is alive, exits with error
|
|
9
9
|
* - Stale locks (PID not alive) are removed automatically
|
|
10
|
-
* - Lock is released on
|
|
10
|
+
* - Lock is released on process exit. Signal handling is left to the caller
|
|
11
|
+
* (the entry waits for the app to stop before exiting)
|
|
11
12
|
*/
|
|
12
13
|
function getTsunagiDataDir() {
|
|
13
14
|
return process.env.TSUNAGI_DATA_DIR || path.join(os.homedir(), '.tsunagi');
|
|
@@ -79,20 +80,5 @@ export function acquireSingleInstanceLock() {
|
|
|
79
80
|
}
|
|
80
81
|
}
|
|
81
82
|
fs.writeFileSync(lockFilePath, String(process.pid), 'utf-8');
|
|
82
|
-
process.on('
|
|
83
|
-
releaseLock();
|
|
84
|
-
process.exit(0);
|
|
85
|
-
});
|
|
86
|
-
process.on('SIGTERM', () => {
|
|
87
|
-
releaseLock();
|
|
88
|
-
process.exit(0);
|
|
89
|
-
});
|
|
90
|
-
process.on('exit', () => {
|
|
91
|
-
releaseLock();
|
|
92
|
-
});
|
|
93
|
-
process.on('uncaughtException', (err) => {
|
|
94
|
-
console.error('[tsunagi] Uncaught exception:', err);
|
|
95
|
-
releaseLock();
|
|
96
|
-
process.exit(1);
|
|
97
|
-
});
|
|
83
|
+
process.on('exit', releaseLock);
|
|
98
84
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* entry が古く、再インストールで更新してほしいか。
|
|
3
|
+
* 古い entry でも取り決めの範囲内で自動更新は続ける(versions.ts 参照)。
|
|
4
|
+
*/
|
|
5
|
+
export declare function isEntryOutdated(): boolean;
|
|
6
|
+
/** entry が npm でインストールされたパッケージから起動しており、自動更新を使うか */
|
|
7
|
+
export declare function isAutoUpdateEnabled(): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* 前回正常に起動した版と違う版(初めて起動する版)なら、マイグレーション前に DB を退避する。
|
|
10
|
+
* 起動に失敗すると entry がこの退避を戻して前の版で起動し直す。
|
|
11
|
+
*/
|
|
12
|
+
export declare function snapshotDbBeforeFirstStart(version: string): void;
|
|
13
|
+
/** 前の版が使っていたポートが空くまで待つ(上限付き) */
|
|
14
|
+
export declare function waitForPortsFree(ports: number[], timeoutMs?: number): Promise<void>;
|
|
15
|
+
/**
|
|
16
|
+
* 起動完了後、実行中より古いバージョンを消す(インストール済み・途中とも)。
|
|
17
|
+
* 実行中より新しいものはインストール中・再起動待ちの可能性があるため残す。
|
|
18
|
+
*/
|
|
19
|
+
export declare function removeVersionsOlderThan(version: string): void;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as net from 'node:net';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { ENTRY_PROTOCOL, compareVersions, getTsunagiDataDir, getVersionsDir, parseVersion, updateEntryState, } from './versions.js';
|
|
5
|
+
/**
|
|
6
|
+
* App-side part of the auto-update: everything the entry (cli.ts) leaves to
|
|
7
|
+
* the app, so that it can change with each version.
|
|
8
|
+
*/
|
|
9
|
+
/** 初めて起動するバージョンが書き換える可能性のあるファイル(データディレクトリからの相対パス) */
|
|
10
|
+
const DB_FILES = ['state/tsunagi.db', 'state/tsunagi.db-wal', 'state/tsunagi.db-shm'];
|
|
11
|
+
const SNAPSHOT_PREFIX = 'pre-update-';
|
|
12
|
+
const SNAPSHOT_RETENTION = 3;
|
|
13
|
+
/** entry から起動されたか。dev 起動(with-plugin.ts)や直接実行では null */
|
|
14
|
+
function getEntryProtocol() {
|
|
15
|
+
const value = Number(process.env.TSUNAGI_ENTRY_PROTOCOL);
|
|
16
|
+
return Number.isInteger(value) && value > 0 ? value : null;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* entry が古く、再インストールで更新してほしいか。
|
|
20
|
+
* 古い entry でも取り決めの範囲内で自動更新は続ける(versions.ts 参照)。
|
|
21
|
+
*/
|
|
22
|
+
export function isEntryOutdated() {
|
|
23
|
+
const protocol = getEntryProtocol();
|
|
24
|
+
return protocol !== null && protocol < ENTRY_PROTOCOL;
|
|
25
|
+
}
|
|
26
|
+
/** entry が npm でインストールされたパッケージから起動しており、自動更新を使うか */
|
|
27
|
+
export function isAutoUpdateEnabled() {
|
|
28
|
+
return getEntryProtocol() !== null && process.env.TSUNAGI_AUTO_UPDATE === '1';
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* 前回正常に起動した版と違う版(初めて起動する版)なら、マイグレーション前に DB を退避する。
|
|
32
|
+
* 起動に失敗すると entry がこの退避を戻して前の版で起動し直す。
|
|
33
|
+
*/
|
|
34
|
+
export function snapshotDbBeforeFirstStart(version) {
|
|
35
|
+
const lastGood = process.env.TSUNAGI_LAST_GOOD;
|
|
36
|
+
if (!lastGood || lastGood === version || getEntryProtocol() === null)
|
|
37
|
+
return;
|
|
38
|
+
const dataDir = getTsunagiDataDir();
|
|
39
|
+
const backupsDir = path.join(dataDir, 'backups');
|
|
40
|
+
const dir = path.join(backupsDir, `${SNAPSHOT_PREFIX}${Date.now()}-v${version}`);
|
|
41
|
+
const files = {};
|
|
42
|
+
for (const file of DB_FILES) {
|
|
43
|
+
const src = path.join(dataDir, file);
|
|
44
|
+
files[file] = fs.existsSync(src);
|
|
45
|
+
if (!files[file])
|
|
46
|
+
continue;
|
|
47
|
+
fs.mkdirSync(path.dirname(path.join(dir, file)), { recursive: true });
|
|
48
|
+
fs.copyFileSync(src, path.join(dir, file));
|
|
49
|
+
}
|
|
50
|
+
updateEntryState((state) => ({ ...state, snapshot: { version, dir, files } }));
|
|
51
|
+
const snapshots = fs
|
|
52
|
+
.readdirSync(backupsDir)
|
|
53
|
+
.filter((name) => name.startsWith(SNAPSHOT_PREFIX))
|
|
54
|
+
.sort();
|
|
55
|
+
while (snapshots.length > SNAPSHOT_RETENTION) {
|
|
56
|
+
const oldest = snapshots.shift();
|
|
57
|
+
if (oldest)
|
|
58
|
+
fs.rmSync(path.join(backupsDir, oldest), { recursive: true, force: true });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** 前の版が使っていたポートが空くまで待つ(上限付き) */
|
|
62
|
+
export async function waitForPortsFree(ports, timeoutMs = 10_000) {
|
|
63
|
+
const isFree = (port) => new Promise((resolve) => {
|
|
64
|
+
const server = net.createServer();
|
|
65
|
+
server.once('error', () => resolve(false));
|
|
66
|
+
server.listen(port, '0.0.0.0', () => server.close(() => resolve(true)));
|
|
67
|
+
});
|
|
68
|
+
const deadline = Date.now() + timeoutMs;
|
|
69
|
+
while (Date.now() < deadline) {
|
|
70
|
+
const results = await Promise.all(ports.map(isFree));
|
|
71
|
+
if (results.every(Boolean))
|
|
72
|
+
return;
|
|
73
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* 起動完了後、実行中より古いバージョンを消す(インストール済み・途中とも)。
|
|
78
|
+
* 実行中より新しいものはインストール中・再起動待ちの可能性があるため残す。
|
|
79
|
+
*/
|
|
80
|
+
export function removeVersionsOlderThan(version) {
|
|
81
|
+
let entries;
|
|
82
|
+
try {
|
|
83
|
+
entries = fs.readdirSync(getVersionsDir());
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
for (const name of entries) {
|
|
89
|
+
if (!parseVersion(name) || compareVersions(name, version) >= 0)
|
|
90
|
+
continue;
|
|
91
|
+
fs.rmSync(path.join(getVersionsDir(), name), { recursive: true, force: true });
|
|
92
|
+
}
|
|
93
|
+
}
|