@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.
Files changed (164) hide show
  1. package/.next/standalone/apps/web/.next/BUILD_ID +1 -1
  2. package/.next/standalone/apps/web/.next/build-manifest.json +3 -3
  3. package/.next/standalone/apps/web/.next/prerender-manifest.json +3 -3
  4. package/.next/standalone/apps/web/.next/server/app/_global-error.html +1 -1
  5. package/.next/standalone/apps/web/.next/server/app/_global-error.rsc +1 -1
  6. package/.next/standalone/apps/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  7. package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  8. package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  9. package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  10. package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  11. package/.next/standalone/apps/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  12. package/.next/standalone/apps/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  13. package/.next/standalone/apps/web/.next/server/app/_not-found.html +1 -1
  14. package/.next/standalone/apps/web/.next/server/app/_not-found.rsc +14 -14
  15. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
  16. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  17. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_index.segment.rsc +9 -9
  18. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  19. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  20. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  21. package/.next/standalone/apps/web/.next/server/app/index.html +1 -1
  22. package/.next/standalone/apps/web/.next/server/app/index.rsc +16 -16
  23. package/.next/standalone/apps/web/.next/server/app/index.segments/__PAGE__.segment.rsc +4 -4
  24. package/.next/standalone/apps/web/.next/server/app/index.segments/_full.segment.rsc +16 -16
  25. package/.next/standalone/apps/web/.next/server/app/index.segments/_head.segment.rsc +4 -4
  26. package/.next/standalone/apps/web/.next/server/app/index.segments/_index.segment.rsc +9 -9
  27. package/.next/standalone/apps/web/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  28. package/.next/standalone/apps/web/.next/server/app/page.js.nft.json +1 -1
  29. package/.next/standalone/apps/web/.next/server/app/page_client-reference-manifest.js +1 -1
  30. package/.next/standalone/apps/web/.next/server/app/settings/page.js.nft.json +1 -1
  31. package/.next/standalone/apps/web/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  32. package/.next/standalone/apps/web/.next/server/app/settings.html +1 -1
  33. package/.next/standalone/apps/web/.next/server/app/settings.rsc +17 -16
  34. package/.next/standalone/apps/web/.next/server/app/settings.segments/_full.segment.rsc +17 -16
  35. package/.next/standalone/apps/web/.next/server/app/settings.segments/_head.segment.rsc +4 -4
  36. package/.next/standalone/apps/web/.next/server/app/settings.segments/_index.segment.rsc +9 -9
  37. package/.next/standalone/apps/web/.next/server/app/settings.segments/_tree.segment.rsc +3 -2
  38. package/.next/standalone/apps/web/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +5 -4
  39. package/.next/standalone/apps/web/.next/server/app/settings.segments/settings.segment.rsc +3 -3
  40. package/.next/standalone/apps/web/.next/server/app/tasks/[id]/page.js.nft.json +1 -1
  41. package/.next/standalone/apps/web/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
  42. package/.next/standalone/apps/web/.next/server/chunks/ssr/[root-of-the-server]__00qk65u._.js +3 -0
  43. package/.next/standalone/apps/web/.next/server/chunks/ssr/[root-of-the-server]__02o_btn._.js +2 -2
  44. package/.next/standalone/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0qjafjt._.js +1 -1
  45. package/.next/standalone/apps/web/.next/server/chunks/ssr/_03vftq7._.js +7 -0
  46. package/.next/standalone/apps/web/.next/server/chunks/ssr/_0835zmv._.js +14 -0
  47. package/.next/standalone/apps/web/.next/server/chunks/ssr/_0g.cd6a._.js +1 -1
  48. package/.next/standalone/apps/web/.next/server/chunks/ssr/_0g79jqo._.js +20 -0
  49. package/.next/standalone/apps/web/.next/server/chunks/ssr/_0kflgfj._.js +20 -0
  50. package/.next/standalone/apps/web/.next/server/chunks/ssr/_0w2dm3r._.js +3 -0
  51. package/.next/standalone/apps/web/.next/server/chunks/ssr/_10gk93.._.js +3 -0
  52. package/.next/standalone/apps/web/.next/server/chunks/ssr/{_05_5y28._.js → _139_l.u._.js} +2 -2
  53. package/.next/standalone/apps/web/.next/server/chunks/ssr/_13w5ytx._.js +7 -0
  54. package/.next/standalone/apps/web/.next/server/chunks/ssr/apps_web_src_app_settings_page_tsx_0i6itlb._.js +2 -2
  55. package/.next/standalone/apps/web/.next/server/chunks/ssr/apps_web_src_app_tasks_[id]_page_tsx_0jshsm2._.js +4 -21
  56. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0c8sp_9._.js → node_modules_04pfam-._.js} +1 -1
  57. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0zv4e8x._.js → node_modules_09d2oqj._.js} +1 -1
  58. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0h_hear._.js → node_modules_0_huoqd._.js} +1 -1
  59. package/.next/standalone/apps/web/.next/server/chunks/ssr/node_modules_0a57phw._.js +3 -0
  60. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0axuayq._.js → node_modules_0ak_3rk._.js} +1 -1
  61. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0la1a4x._.js → node_modules_0cv326~._.js} +1 -1
  62. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0_876x4._.js → node_modules_0h.vgyn._.js} +1 -1
  63. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0w8sswp._.js → node_modules_0krwgny._.js} +1 -1
  64. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_09e0q95._.js → node_modules_0l23gkf._.js} +1 -1
  65. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0o9t~hh._.js → node_modules_0npujym._.js} +1 -1
  66. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_06gyfuv._.js → node_modules_0oip5a1._.js} +1 -1
  67. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_10v5lct._.js → node_modules_0q_5qo~._.js} +1 -1
  68. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_117mm7f._.js → node_modules_0q_xfyl._.js} +1 -1
  69. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0s3mkxs._.js → node_modules_0sn0lqb._.js} +1 -1
  70. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0nji2bx._.js → node_modules_0tod6y8._.js} +1 -1
  71. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0mt.8za._.js → node_modules_0xp9nhx._.js} +1 -1
  72. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_09nusj_._.js → node_modules_11-z65a._.js} +1 -1
  73. package/.next/standalone/apps/web/.next/server/middleware-build-manifest.js +3 -3
  74. package/.next/standalone/apps/web/.next/server/pages/404.html +1 -1
  75. package/.next/standalone/apps/web/.next/server/pages/500.html +1 -1
  76. package/.next/standalone/apps/web/.next/server/server-reference-manifest.js +1 -1
  77. package/.next/standalone/apps/web/.next/server/server-reference-manifest.json +1 -1
  78. package/.next/standalone/apps/web/.next/static/chunks/0.gcmzq82~36i.js +1 -0
  79. package/.next/standalone/apps/web/.next/static/chunks/06gfh91..uydi.js +5 -0
  80. package/.next/standalone/apps/web/.next/static/chunks/08e.dbdz18nd2.js +18 -0
  81. package/.next/standalone/apps/web/.next/static/chunks/0b90ys7wq-p.4.css +4 -0
  82. package/.next/standalone/apps/web/.next/static/chunks/0crn03y2tj0lu.js +6 -0
  83. package/.next/standalone/apps/web/.next/static/chunks/0l83q95s~tih4.js +5 -0
  84. package/.next/standalone/apps/web/.next/static/chunks/0lvl3ec93cab_.js +7 -0
  85. package/.next/standalone/apps/web/.next/static/chunks/0lxvbkhh7amha.js +1 -0
  86. package/.next/standalone/apps/web/.next/static/chunks/0mqupo29wme26.js +7 -0
  87. package/.next/standalone/apps/web/.next/static/chunks/0rn1kcmsfzjmz.js +12 -0
  88. package/.next/standalone/apps/web/.next/static/chunks/0whl1y16cvn5q.js +1 -0
  89. package/.next/standalone/apps/web/.next/static/chunks/0z60ewb40i2ce.js +1 -0
  90. package/.next/standalone/apps/web/.next/static/chunks/0~qk-51yuw4mm.js +18 -0
  91. package/.next/standalone/apps/web/.next/static/chunks/{0v3hfoggxwsov.js → 149sia28cskw9.js} +1 -1
  92. package/.next/standalone/apps/web/.next/static/chunks/17~93fal1fw_d.js +5 -0
  93. package/dist/app.d.ts +1 -0
  94. package/dist/app.js +375 -0
  95. package/dist/cli.js +152 -290
  96. package/dist/plugin-lifecycle.d.ts +9 -10
  97. package/dist/plugin-lifecycle.js +77 -65
  98. package/dist/prisma/migrations/20261007000000_add_claude_profiles/migration.sql +10 -0
  99. package/dist/prisma/schema.prisma +9 -0
  100. package/dist/server/generated/prisma/browser.d.ts +5 -0
  101. package/dist/server/generated/prisma/client.d.ts +5 -0
  102. package/dist/server/generated/prisma/internal/class.d.ts +11 -0
  103. package/dist/server/generated/prisma/internal/class.js +4 -4
  104. package/dist/server/generated/prisma/internal/prismaNamespace.d.ts +83 -1
  105. package/dist/server/generated/prisma/internal/prismaNamespace.js +6 -0
  106. package/dist/server/generated/prisma/internal/prismaNamespaceBrowser.d.ts +7 -0
  107. package/dist/server/generated/prisma/internal/prismaNamespaceBrowser.js +6 -0
  108. package/dist/server/generated/prisma/models/ClaudeProfile.d.ts +939 -0
  109. package/dist/server/generated/prisma/models/ClaudeProfile.js +1 -0
  110. package/dist/server/generated/prisma/models.d.ts +1 -0
  111. package/dist/server/index.js +20 -3
  112. package/dist/server/lib/auto-update.d.ts +18 -0
  113. package/dist/server/lib/auto-update.js +208 -0
  114. package/dist/server/lib/claude-config-guard.d.ts +7 -5
  115. package/dist/server/lib/claude-config-guard.js +31 -12
  116. package/dist/server/lib/claude-profiles.d.ts +49 -0
  117. package/dist/server/lib/claude-profiles.js +168 -0
  118. package/dist/server/lib/pty-env-sync.d.ts +12 -0
  119. package/dist/server/lib/pty-env-sync.js +139 -0
  120. package/dist/server/lib/pty-launch.d.ts +36 -0
  121. package/dist/server/lib/pty-launch.js +141 -0
  122. package/dist/server/lib/repositories/environment.js +8 -34
  123. package/dist/server/lib/restart.d.ts +8 -0
  124. package/dist/server/lib/restart.js +13 -0
  125. package/dist/server/lib/update-check.js +14 -4
  126. package/dist/server/pty-manager.d.ts +25 -1
  127. package/dist/server/pty-manager.js +38 -3
  128. package/dist/server/routes/claude-profiles.d.ts +2 -0
  129. package/dist/server/routes/claude-profiles.js +138 -0
  130. package/dist/server/routes/env.js +25 -1
  131. package/dist/server/routes/onboarding.js +15 -6
  132. package/dist/server/routes/terminal.js +34 -120
  133. package/dist/server/routes/version.js +16 -0
  134. package/dist/single-instance-lock.js +3 -17
  135. package/dist/update-lifecycle.d.ts +19 -0
  136. package/dist/update-lifecycle.js +93 -0
  137. package/dist/versions.d.ts +60 -0
  138. package/dist/versions.js +95 -0
  139. package/dist/with-plugin.js +6 -8
  140. package/npm-shrinkwrap.json +8 -8
  141. package/package.json +1 -1
  142. package/.next/standalone/apps/web/.next/server/chunks/ssr/[root-of-the-server]__03~.ax1._.js +0 -3
  143. package/.next/standalone/apps/web/.next/server/chunks/ssr/_0bru-z.._.js +0 -3
  144. package/.next/standalone/apps/web/.next/server/chunks/ssr/_0hqrmyz._.js +0 -3
  145. package/.next/standalone/apps/web/.next/server/chunks/ssr/_0i8ojdw._.js +0 -14
  146. package/.next/standalone/apps/web/.next/server/chunks/ssr/_0m.hvms._.js +0 -7
  147. package/.next/standalone/apps/web/.next/server/chunks/ssr/_0mljs_s._.js +0 -7
  148. package/.next/standalone/apps/web/.next/server/chunks/ssr/_0ncpk6b._.js +0 -3
  149. package/.next/standalone/apps/web/.next/server/chunks/ssr/_109pmo2._.js +0 -3
  150. package/.next/standalone/apps/web/.next/static/chunks/01mfp5lo0gy.q.js +0 -23
  151. package/.next/standalone/apps/web/.next/static/chunks/08-d-ag2euha9.js +0 -5
  152. package/.next/standalone/apps/web/.next/static/chunks/0_by0zpp68~r7.js +0 -1
  153. package/.next/standalone/apps/web/.next/static/chunks/0c.dwezn5qjvs.css +0 -4
  154. package/.next/standalone/apps/web/.next/static/chunks/0gpkw.1e6kcpn.js +0 -1
  155. package/.next/standalone/apps/web/.next/static/chunks/0jc3jwed_8vre.js +0 -7
  156. package/.next/standalone/apps/web/.next/static/chunks/0p3_2uc39mlse.js +0 -5
  157. package/.next/standalone/apps/web/.next/static/chunks/0v6gxm~ldhmma.js +0 -1
  158. package/.next/standalone/apps/web/.next/static/chunks/0vxoeg5sfua~3.js +0 -12
  159. package/.next/standalone/apps/web/.next/static/chunks/0wzu~340lh7o~.js +0 -7
  160. package/.next/standalone/apps/web/.next/static/chunks/0xa_n_zp.qw-b.js +0 -1
  161. package/.next/standalone/apps/web/.next/static/chunks/13y6z-vus9yvr.js +0 -5
  162. /package/.next/standalone/apps/web/.next/static/{EOnxRuFun6cfXk-y9KPq5 → jgWo5G4-W0asjpQwCqJWQ}/_buildManifest.js +0 -0
  163. /package/.next/standalone/apps/web/.next/static/{EOnxRuFun6cfXk-y9KPq5 → jgWo5G4-W0asjpQwCqJWQ}/_clientMiddlewareManifest.js +0 -0
  164. /package/.next/standalone/apps/web/.next/static/{EOnxRuFun6cfXk-y9KPq5 → jgWo5G4-W0asjpQwCqJWQ}/_ssgManifest.js +0 -0
@@ -2,5 +2,6 @@ export type * from './models/Repository.js';
2
2
  export type * from './models/Task.js';
3
3
  export type * from './models/Tab.js';
4
4
  export type * from './models/EnvironmentVariable.js';
5
+ export type * from './models/ClaudeProfile.js';
5
6
  export type * from './models/AppSetting.js';
6
7
  export type * from './commonInputTypes.js';
@@ -10,6 +10,7 @@ import { envRoutes } from './routes/env.js';
10
10
  import { worktreesRoutes } from './routes/worktrees.js';
11
11
  import { commandsRoutes } from './routes/commands.js';
12
12
  import { onboardingRoutes } from './routes/onboarding.js';
13
+ import { claudeProfilesRoutes } from './routes/claude-profiles.js';
13
14
  import { internalRoutes } from './routes/internal.js';
14
15
  import { hooksRoutes } from './routes/hooks.js';
15
16
  import { mcpRoutes } from './routes/mcp.js';
@@ -25,6 +26,9 @@ import { stopWhisperServerOnExit } from './lib/whisper-process.js';
25
26
  import { settingsRoutes } from './routes/settings.js';
26
27
  import { versionRoutes } from './routes/version.js';
27
28
  import { startUpdateCheck, stopUpdateCheck } from './lib/update-check.js';
29
+ import { stopAutoUpdate } from './lib/auto-update.js';
30
+ import { RESTART_EXIT_CODE, onRestartRequested } from './lib/restart.js';
31
+ import { ptyManager } from './pty-manager.js';
28
32
  import { stopSearxngOnExit, syncSearxng } from './lib/searxng.js';
29
33
  import { createBasicAuth } from './basic-auth.js';
30
34
  // Fastify は単一の公開エンドポイント。Next.js は内部ポートで動かしプロキシする。
@@ -97,6 +101,7 @@ async function start() {
97
101
  await fastify.register(worktreesRoutes, { prefix: '/api' });
98
102
  await fastify.register(commandsRoutes, { prefix: '/api' });
99
103
  await fastify.register(onboardingRoutes, { prefix: '/api' });
104
+ await fastify.register(claudeProfilesRoutes, { prefix: '/api' });
100
105
  await fastify.register(internalRoutes, { prefix: '/api' });
101
106
  await fastify.register(hooksRoutes, { prefix: '/api' });
102
107
  await fastify.register(mcpRoutes, { prefix: '/api' });
@@ -162,21 +167,33 @@ async function start() {
162
167
  watchUserSettingsForLocalModel(LOCAL_MODEL_ALIAS);
163
168
  // npm に新しいバージョンが公開されたかを定期確認し、Web に通知する
164
169
  startUpdateCheck(io);
165
- const shutdown = async (signal) => {
170
+ let shuttingDown = false;
171
+ const shutdown = async (signal, exitCode = 0) => {
172
+ if (shuttingDown)
173
+ return;
174
+ shuttingDown = true;
166
175
  console.log(`[server] Received ${signal}, shutting down...`);
176
+ stopUpdateCheck();
177
+ stopAutoUpdate();
178
+ // PTY(claude を含む)は SIGHUP 任せにせず、ここで止める
179
+ ptyManager.deleteAllSessions();
167
180
  stopWhisperServerOnExit();
168
181
  stopSearxngOnExit();
169
- stopUpdateCheck();
170
182
  // 設定で有効なら、ローカルLLMのモデルをメモリから外す(バッテリー・メモリ節約)。
171
183
  // 開発時の tsx watch はファイル変更のたびに SIGTERM で再起動するため、その場合は外さない
172
184
  if (signal === 'SIGINT' || process.env.NODE_ENV === 'production') {
173
185
  await unloadOnShutdown();
174
186
  }
175
187
  await fastify.close();
176
- process.exit(0);
188
+ process.exit(exitCode);
177
189
  };
178
190
  process.on('SIGINT', () => void shutdown('SIGINT'));
179
191
  process.on('SIGTERM', () => void shutdown('SIGTERM'));
192
+ // 更新の適用(POST /api/version/restart)。apps/cli がこの終了コードを見て新しいバージョンで起動し直す
193
+ onRestartRequested(() => {
194
+ io.emit('version:restarting');
195
+ void shutdown('restart', RESTART_EXIT_CODE);
196
+ });
180
197
  // SIGINT/SIGTERM を経由しない異常終了時の最後の砦('exit' は同期処理のみ可能)。
181
198
  process.on('exit', () => {
182
199
  stopWhisperServerOnExit();
@@ -0,0 +1,18 @@
1
+ import type { AutoUpdateStatus, RestartBlocker } from '@minimalcorp/tsunagi-shared';
2
+ /** entry が古く、再インストールしてほしいか(apps/cli/src/update-lifecycle.ts が判定する) */
3
+ export declare function isEntryOutdated(): boolean;
4
+ export declare function getAutoUpdateStatus(): AutoUpdateStatus;
5
+ /** 状態が変わったときに呼ぶ(update-check が Web へ通知する) */
6
+ export declare function onAutoUpdateChange(listener: () => void): void;
7
+ /**
8
+ * version を再起動で適用できるよう用意する(update-check が新しいバージョンを見つけるたびに呼ぶ)。
9
+ * インストール中は何もしない。より新しいバージョンは次回の確認で用意する。
10
+ */
11
+ export declare function prepareUpdate(version: string): void;
12
+ /** サーバー終了時にインストールを止める。途中のディレクトリは次回の install で消す */
13
+ export declare function stopAutoUpdate(): void;
14
+ /**
15
+ * 再起動すると止まってしまう実行中(running / waiting)のタスク。
16
+ * PTY が残っていないタブは、前回終了時の status が残っているだけなので除く。
17
+ */
18
+ export declare function findRestartBlockers(): Promise<RestartBlocker[]>;
@@ -0,0 +1,208 @@
1
+ import { spawn } from 'child_process';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import { ptyManager } from '../pty-manager.js';
5
+ import { getTsunagiDataDir } from './data-path.js';
6
+ import { prisma } from './db.js';
7
+ /**
8
+ * 新しいバージョンを ~/.tsunagi/versions/<version>/ に `npm install --prefix` で用意し、
9
+ * state.json の next に書く。再起動(requestRestart)で entry が next の版を起動する。
10
+ * ディレクトリ構成と entry との取り決めは apps/cli/src/versions.ts と合わせること。
11
+ */
12
+ const PACKAGE_NAME = '@minimalcorp/tsunagi';
13
+ const COMPLETE_MARKER = '.complete';
14
+ const STDERR_TAIL_MAX_CHARS = 2000;
15
+ // entry 経由かつ npm でインストールされた tsunagi のときだけ apps/cli が設定する
16
+ const enabled = process.env.TSUNAGI_AUTO_UPDATE === '1' && !!process.env.TSUNAGI_VERSION;
17
+ /** entry が古く、再インストールしてほしいか(apps/cli/src/update-lifecycle.ts が判定する) */
18
+ export function isEntryOutdated() {
19
+ return process.env.TSUNAGI_ENTRY_OUTDATED === '1';
20
+ }
21
+ let status = { state: enabled ? 'idle' : 'disabled', version: null, error: null };
22
+ let installer = null;
23
+ let onChange = null;
24
+ export function getAutoUpdateStatus() {
25
+ return { ...status };
26
+ }
27
+ /** 状態が変わったときに呼ぶ(update-check が Web へ通知する) */
28
+ export function onAutoUpdateChange(listener) {
29
+ onChange = listener;
30
+ }
31
+ function setStatus(next) {
32
+ status = next;
33
+ onChange?.();
34
+ }
35
+ function getVersionDir(version) {
36
+ return path.join(getTsunagiDataDir(), 'versions', version);
37
+ }
38
+ function getPackageRoot(version) {
39
+ return path.join(getVersionDir(version), 'node_modules', ...PACKAGE_NAME.split('/'));
40
+ }
41
+ function getStatePath() {
42
+ return path.join(getTsunagiDataDir(), 'versions', 'state.json');
43
+ }
44
+ function readState() {
45
+ try {
46
+ const state = JSON.parse(fs.readFileSync(getStatePath(), 'utf-8'));
47
+ return state && typeof state === 'object' ? state : {};
48
+ }
49
+ catch {
50
+ return {};
51
+ }
52
+ }
53
+ /** 起動に失敗して entry がロールバックしたバージョン */
54
+ function readBrokenVersions() {
55
+ const { broken } = readState();
56
+ return Array.isArray(broken) ? broken.filter((v) => typeof v === 'string') : [];
57
+ }
58
+ /** 次の再起動で起動するバージョンを entry に伝える。他のフィールドは残し、rename で置き換える */
59
+ function setNextVersion(version) {
60
+ const statePath = getStatePath();
61
+ const tmpPath = `${statePath}.${process.pid}.tmp`;
62
+ fs.writeFileSync(tmpPath, JSON.stringify({ ...readState(), next: version }, null, 2), 'utf-8');
63
+ fs.renameSync(tmpPath, statePath);
64
+ }
65
+ /** インストール結果が起動できる形か(entry が起動する dist/app.js があるか) */
66
+ function verifyInstall(version) {
67
+ const root = getPackageRoot(version);
68
+ try {
69
+ const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf-8'));
70
+ return (pkg.version === version &&
71
+ fs.existsSync(path.join(root, 'dist', 'cli.js')) &&
72
+ fs.existsSync(path.join(root, 'dist', 'app.js')));
73
+ }
74
+ catch {
75
+ return false;
76
+ }
77
+ }
78
+ /** 実行中の node と同じ場所の npm を優先する(nvm 等で PATH の npm と食い違わないように) */
79
+ function npmCommand() {
80
+ const candidate = path.join(path.dirname(process.execPath), 'npm');
81
+ return fs.existsSync(candidate) ? candidate : 'npm';
82
+ }
83
+ /** npx 経由で起動した場合の npm_config_* 等が install に影響しないよう取り除く */
84
+ function installEnv() {
85
+ const env = {};
86
+ for (const [key, value] of Object.entries(process.env)) {
87
+ if (/^npm_/i.test(key) || key === 'NODE_ENV' || key.startsWith('TSUNAGI_'))
88
+ continue;
89
+ env[key] = value;
90
+ }
91
+ return env;
92
+ }
93
+ function install(version) {
94
+ const dir = getVersionDir(version);
95
+ // 前回途中で止まったインストールが残っていれば消してからやり直す
96
+ fs.rmSync(dir, { recursive: true, force: true });
97
+ fs.mkdirSync(dir, { recursive: true });
98
+ setStatus({ state: 'installing', version, error: null });
99
+ const child = spawn(npmCommand(), [
100
+ 'install',
101
+ '--prefix',
102
+ dir,
103
+ '--no-audit',
104
+ '--no-fund',
105
+ '--no-update-notifier',
106
+ '--loglevel=error',
107
+ ...(process.env.TSUNAGI_NPM_REGISTRY ? ['--registry', process.env.TSUNAGI_NPM_REGISTRY] : []),
108
+ `${PACKAGE_NAME}@${version}`,
109
+ ], { cwd: dir, env: installEnv(), stdio: ['ignore', 'ignore', 'pipe'] });
110
+ installer = child;
111
+ let stderrTail = '';
112
+ child.stderr?.on('data', (chunk) => {
113
+ stderrTail = (stderrTail + chunk.toString()).slice(-STDERR_TAIL_MAX_CHARS);
114
+ });
115
+ let finished = false;
116
+ const finish = (error) => {
117
+ if (finished)
118
+ return;
119
+ finished = true;
120
+ if (installer === child)
121
+ installer = null;
122
+ // stopAutoUpdate で止めた場合は状態を変えない(サーバー終了中)
123
+ if (status.state !== 'installing' || status.version !== version)
124
+ return;
125
+ if (!error && verifyInstall(version)) {
126
+ fs.writeFileSync(path.join(dir, COMPLETE_MARKER), new Date().toISOString(), 'utf-8');
127
+ setNextVersion(version);
128
+ setStatus({ state: 'ready', version, error: null });
129
+ return;
130
+ }
131
+ fs.rmSync(dir, { recursive: true, force: true });
132
+ setStatus({
133
+ state: 'error',
134
+ version,
135
+ error: error ?? 'The installed package is incomplete',
136
+ });
137
+ };
138
+ child.on('error', (err) => finish(`Failed to run npm: ${err.message}`));
139
+ child.on('exit', (code, signal) => {
140
+ if (code === 0)
141
+ finish(null);
142
+ else
143
+ finish(`npm install failed (${signal ?? `code ${code}`})${stderrTail ? `\n${stderrTail}` : ''}`);
144
+ });
145
+ }
146
+ /**
147
+ * version を再起動で適用できるよう用意する(update-check が新しいバージョンを見つけるたびに呼ぶ)。
148
+ * インストール中は何もしない。より新しいバージョンは次回の確認で用意する。
149
+ */
150
+ export function prepareUpdate(version) {
151
+ if (!enabled || installer)
152
+ return;
153
+ if (status.state === 'ready' && status.version === version)
154
+ return;
155
+ if (readBrokenVersions().includes(version)) {
156
+ fs.rmSync(getVersionDir(version), { recursive: true, force: true });
157
+ setStatus({
158
+ state: 'error',
159
+ version,
160
+ error: `v${version} failed to start and was rolled back`,
161
+ });
162
+ return;
163
+ }
164
+ if (fs.existsSync(path.join(getVersionDir(version), COMPLETE_MARKER))) {
165
+ setNextVersion(version);
166
+ setStatus({ state: 'ready', version, error: null });
167
+ return;
168
+ }
169
+ try {
170
+ install(version);
171
+ }
172
+ catch (err) {
173
+ setStatus({
174
+ state: 'error',
175
+ version,
176
+ error: err instanceof Error ? err.message : String(err),
177
+ });
178
+ }
179
+ }
180
+ /** サーバー終了時にインストールを止める。途中のディレクトリは次回の install で消す */
181
+ export function stopAutoUpdate() {
182
+ if (!installer)
183
+ return;
184
+ try {
185
+ installer.kill('SIGTERM');
186
+ }
187
+ catch {
188
+ // 既に終了している
189
+ }
190
+ installer = null;
191
+ }
192
+ /**
193
+ * 再起動すると止まってしまう実行中(running / waiting)のタスク。
194
+ * PTY が残っていないタブは、前回終了時の status が残っているだけなので除く。
195
+ */
196
+ export async function findRestartBlockers() {
197
+ const tabs = await prisma.tab.findMany({
198
+ where: { status: { in: ['running', 'waiting'] } },
199
+ include: { task: true },
200
+ });
201
+ const blockers = new Map();
202
+ for (const tab of tabs) {
203
+ if (!ptyManager.getSession(tab.tabId))
204
+ continue;
205
+ blockers.set(tab.taskId, { taskId: tab.taskId, title: tab.task.title });
206
+ }
207
+ return [...blockers.values()];
208
+ }
@@ -5,17 +5,19 @@
5
5
  * どこか1セッションで logout すると以降全タブでオンボーディングウィザード
6
6
  * (対話UIではなく初回セットアップ画面)が起動してしまい、--resume/--session-id
7
7
  * を前提にした自動起動フローが止まる。claude 起動直前にこのフラグだけ補正する。
8
+ * configDir はタブが使う Claude プロファイルの CLAUDE_CONFIG_DIR(未指定は ~/.claude.json)。
8
9
  */
9
- export declare function ensureClaudeOnboardingCompleted(): Promise<void>;
10
+ export declare function ensureClaudeOnboardingCompleted(configDir?: string): Promise<void>;
10
11
  /**
11
12
  * ローカルLLMタブで /model の一覧から「ローカルLLM」を Enter で選ぶと、Claude Code は同じモデルへの
12
13
  * 切り替えとみなして PreModelSwitch フックを通さずに ~/.claude/settings.json の model に
13
14
  * 中継口の名前(localModelAlias)を保存する。そのままだと通常の Claude タブがその名前で起動して
14
- * 壊れるため取り除く。
15
+ * 壊れるため取り除く。configDir はタブが使う Claude プロファイルの CLAUDE_CONFIG_DIR。
15
16
  */
16
- export declare function removeLocalModelFromUserSettings(localModelAlias: string): Promise<void>;
17
+ export declare function removeLocalModelFromUserSettings(localModelAlias: string, configDir?: string): Promise<void>;
17
18
  /**
18
- * ~/.claude/settings.json の変更を監視し、中継口の名前が保存されたらすぐ取り除く。
19
+ * settings.json の変更を監視し、中継口の名前が保存されたらすぐ取り除く。
19
20
  * Claude Code は tmp ファイル経由で置き換えることがあるため、ファイルではなくディレクトリを監視する。
21
+ * configDir は Claude プロファイルの CLAUDE_CONFIG_DIR(未指定は外側 Terminal の設定 or ~/.claude)。
20
22
  */
21
- export declare function watchUserSettingsForLocalModel(localModelAlias: string): void;
23
+ export declare function watchUserSettingsForLocalModel(localModelAlias: string, configDir?: string): void;
@@ -2,6 +2,13 @@ import { watch } from 'fs';
2
2
  import * as fs from 'fs/promises';
3
3
  import * as os from 'os';
4
4
  import * as path from 'path';
5
+ /** Claude Code の設定ファイルの場所。CLAUDE_CONFIG_DIR 指定時は .claude.json もその中に置かれる */
6
+ function claudeJsonPath(configDir) {
7
+ return configDir ? path.join(configDir, '.claude.json') : path.join(os.homedir(), '.claude.json');
8
+ }
9
+ function userSettingsDir(configDir) {
10
+ return configDir ?? path.join(os.homedir(), '.claude');
11
+ }
5
12
  /**
6
13
  * `claude logout` は ~/.claude.json の hasCompletedOnboarding を含む
7
14
  * first-launch setup state をリセットする。Tsunagi は全タブ/タスクの
@@ -9,9 +16,10 @@ import * as path from 'path';
9
16
  * どこか1セッションで logout すると以降全タブでオンボーディングウィザード
10
17
  * (対話UIではなく初回セットアップ画面)が起動してしまい、--resume/--session-id
11
18
  * を前提にした自動起動フローが止まる。claude 起動直前にこのフラグだけ補正する。
19
+ * configDir はタブが使う Claude プロファイルの CLAUDE_CONFIG_DIR(未指定は ~/.claude.json)。
12
20
  */
13
- export async function ensureClaudeOnboardingCompleted() {
14
- const configPath = path.join(os.homedir(), '.claude.json');
21
+ export async function ensureClaudeOnboardingCompleted(configDir) {
22
+ const configPath = claudeJsonPath(configDir);
15
23
  let raw;
16
24
  try {
17
25
  raw = await fs.readFile(configPath, 'utf-8');
@@ -42,16 +50,17 @@ export async function ensureClaudeOnboardingCompleted() {
42
50
  await fs.rm(tmpPath, { force: true }).catch(() => { });
43
51
  }
44
52
  }
45
- const USER_SETTINGS_DIR = path.join(os.homedir(), '.claude');
46
53
  const USER_SETTINGS_FILE = 'settings.json';
54
+ /** 監視中の設定ディレクトリ(同じディレクトリを二重に監視しない) */
55
+ const watchedSettingsDirs = new Set();
47
56
  /**
48
57
  * ローカルLLMタブで /model の一覧から「ローカルLLM」を Enter で選ぶと、Claude Code は同じモデルへの
49
58
  * 切り替えとみなして PreModelSwitch フックを通さずに ~/.claude/settings.json の model に
50
59
  * 中継口の名前(localModelAlias)を保存する。そのままだと通常の Claude タブがその名前で起動して
51
- * 壊れるため取り除く。
60
+ * 壊れるため取り除く。configDir はタブが使う Claude プロファイルの CLAUDE_CONFIG_DIR。
52
61
  */
53
- export async function removeLocalModelFromUserSettings(localModelAlias) {
54
- const settingsPath = path.join(USER_SETTINGS_DIR, USER_SETTINGS_FILE);
62
+ export async function removeLocalModelFromUserSettings(localModelAlias, configDir) {
63
+ const settingsPath = path.join(userSettingsDir(configDir), USER_SETTINGS_FILE);
55
64
  let settings;
56
65
  try {
57
66
  settings = JSON.parse(await fs.readFile(settingsPath, 'utf-8'));
@@ -72,21 +81,31 @@ export async function removeLocalModelFromUserSettings(localModelAlias) {
72
81
  }
73
82
  }
74
83
  /**
75
- * ~/.claude/settings.json の変更を監視し、中継口の名前が保存されたらすぐ取り除く。
84
+ * settings.json の変更を監視し、中継口の名前が保存されたらすぐ取り除く。
76
85
  * Claude Code は tmp ファイル経由で置き換えることがあるため、ファイルではなくディレクトリを監視する。
86
+ * configDir は Claude プロファイルの CLAUDE_CONFIG_DIR(未指定は外側 Terminal の設定 or ~/.claude)。
77
87
  */
78
- export function watchUserSettingsForLocalModel(localModelAlias) {
88
+ export function watchUserSettingsForLocalModel(localModelAlias, configDir) {
89
+ const effectiveDir = configDir ?? process.env.CLAUDE_CONFIG_DIR;
90
+ const dir = userSettingsDir(effectiveDir);
91
+ if (watchedSettingsDirs.has(dir))
92
+ return;
79
93
  let watcher;
80
94
  try {
81
- watcher = watch(USER_SETTINGS_DIR, (_event, filename) => {
82
- if (filename === USER_SETTINGS_FILE)
83
- void removeLocalModelFromUserSettings(localModelAlias);
95
+ watcher = watch(dir, (_event, filename) => {
96
+ if (filename === USER_SETTINGS_FILE) {
97
+ void removeLocalModelFromUserSettings(localModelAlias, effectiveDir);
98
+ }
84
99
  });
85
100
  }
86
101
  catch {
87
102
  // ~/.claude がまだない(claude 未実行)場合は監視しない。起動前の補正で対応する
88
103
  return;
89
104
  }
90
- watcher.on('error', () => watcher.close());
105
+ watchedSettingsDirs.add(dir);
106
+ watcher.on('error', () => {
107
+ watcher.close();
108
+ watchedSettingsDirs.delete(dir);
109
+ });
91
110
  watcher.unref();
92
111
  }
@@ -0,0 +1,49 @@
1
+ import type { ClaudeAuthStatus, ClaudeProfile } from '@minimalcorp/tsunagi-shared';
2
+ /**
3
+ * タブで使う Claude プロファイルの割り当てを保存する予約キー。値はプロファイルの slug か
4
+ * DEFAULT_CLAUDE_PROFILE。環境変数と同じ global → owner → repo の継承で解決し、PTY 起動時に
5
+ * CLAUDE_CONFIG_DIR へ変換する(resolveClaudeProfileEnv)。
6
+ */
7
+ export declare const CLAUDE_PROFILE_ENV_KEY = "TSUNAGI_CLAUDE_PROFILE";
8
+ /** システム既定(CLAUDE_CONFIG_DIR を付けず ~/.claude を使う) */
9
+ export declare const DEFAULT_CLAUDE_PROFILE = "default";
10
+ /**
11
+ * Claude Code が /login のログインより優先する認証情報の環境変数。
12
+ * プロファイルを選んだタブでは、外側 Terminal から継承した値も取り除く(残るとプロファイルの
13
+ * ログインではなくこちらが使われるため)。
14
+ */
15
+ export declare const CLAUDE_AUTH_ENV_KEYS: string[];
16
+ /** 汎用の環境変数として登録させないキー(認証は Claude プロファイルで扱う) */
17
+ export declare const RESERVED_ENV_KEYS: string[];
18
+ export declare function getClaudeProfilesDir(): string;
19
+ /** プロファイルの CLAUDE_CONFIG_DIR。システム既定は undefined(~/.claude) */
20
+ export declare function getClaudeProfileConfigDir(slug: string): string | undefined;
21
+ /** 空白を `-` にし、小文字にする(macOS のファイルシステムは大文字小文字を区別しないため) */
22
+ export declare function toClaudeProfileSlug(name: string): string;
23
+ /** プロファイル名の検証。問題があればエラーメッセージを返す */
24
+ export declare function validateClaudeProfileName(name: string): string | null;
25
+ export declare function listClaudeProfiles(): Promise<ClaudeProfile[]>;
26
+ export declare function claudeProfileExists(slug: string): Promise<boolean>;
27
+ export declare function createClaudeProfile(name: string): Promise<ClaudeProfile>;
28
+ /**
29
+ * プロファイルを削除する。ログイン情報(macOS は Keychain)を残さないよう先に logout し、
30
+ * このプロファイルを割り当てていたスコープは割り当てを外して親の設定を継承させる。
31
+ */
32
+ export declare function deleteClaudeProfile(slug: string): Promise<void>;
33
+ /** `claude auth status --json` でプロファイルのログイン状態を取得する */
34
+ export declare function getClaudeAuthStatus(slug: string): Promise<ClaudeAuthStatus>;
35
+ /** ログイン用 PTY に書き込むコマンド */
36
+ export declare function buildClaudeLoginCommand(): string;
37
+ /** ログイン用 PTY の環境変数(PtyManager.createSession の env / unsetKeys) */
38
+ export declare function claudeLoginPtyEnv(slug: string): {
39
+ env: Record<string, string>;
40
+ unsetKeys: string[];
41
+ };
42
+ /**
43
+ * 継承解決済みの環境変数に含まれるプロファイル割り当てを CLAUDE_CONFIG_DIR に変換する。
44
+ * 予約キー自体は PTY に渡さない。
45
+ */
46
+ export declare function resolveClaudeProfileEnv(env: Record<string, string>): {
47
+ env: Record<string, string>;
48
+ unsetKeys: string[];
49
+ };
@@ -0,0 +1,168 @@
1
+ import { execFile } from 'child_process';
2
+ import * as fs from 'fs/promises';
3
+ import * as path from 'path';
4
+ import { promisify } from 'util';
5
+ import { prisma } from './db.js';
6
+ import { getTsunagiDataDir } from './data-path.js';
7
+ const execFileAsync = promisify(execFile);
8
+ /**
9
+ * タブで使う Claude プロファイルの割り当てを保存する予約キー。値はプロファイルの slug か
10
+ * DEFAULT_CLAUDE_PROFILE。環境変数と同じ global → owner → repo の継承で解決し、PTY 起動時に
11
+ * CLAUDE_CONFIG_DIR へ変換する(resolveClaudeProfileEnv)。
12
+ */
13
+ export const CLAUDE_PROFILE_ENV_KEY = 'TSUNAGI_CLAUDE_PROFILE';
14
+ /** システム既定(CLAUDE_CONFIG_DIR を付けず ~/.claude を使う) */
15
+ export const DEFAULT_CLAUDE_PROFILE = 'default';
16
+ /**
17
+ * Claude Code が /login のログインより優先する認証情報の環境変数。
18
+ * プロファイルを選んだタブでは、外側 Terminal から継承した値も取り除く(残るとプロファイルの
19
+ * ログインではなくこちらが使われるため)。
20
+ */
21
+ export const CLAUDE_AUTH_ENV_KEYS = ['ANTHROPIC_API_KEY', 'CLAUDE_CODE_OAUTH_TOKEN'];
22
+ /** 汎用の環境変数として登録させないキー(認証は Claude プロファイルで扱う) */
23
+ export const RESERVED_ENV_KEYS = [
24
+ CLAUDE_PROFILE_ENV_KEY,
25
+ 'CLAUDE_CONFIG_DIR',
26
+ ...CLAUDE_AUTH_ENV_KEYS,
27
+ ];
28
+ const PROFILE_NAME_PATTERN = /^[A-Za-z0-9 _-]+$/;
29
+ export function getClaudeProfilesDir() {
30
+ return path.join(getTsunagiDataDir(), 'claude-profiles');
31
+ }
32
+ /** プロファイルの CLAUDE_CONFIG_DIR。システム既定は undefined(~/.claude) */
33
+ export function getClaudeProfileConfigDir(slug) {
34
+ if (slug === DEFAULT_CLAUDE_PROFILE)
35
+ return undefined;
36
+ return path.join(getClaudeProfilesDir(), slug);
37
+ }
38
+ /** 空白を `-` にし、小文字にする(macOS のファイルシステムは大文字小文字を区別しないため) */
39
+ export function toClaudeProfileSlug(name) {
40
+ return name.trim().replace(/\s+/g, '-').toLowerCase();
41
+ }
42
+ /** プロファイル名の検証。問題があればエラーメッセージを返す */
43
+ export function validateClaudeProfileName(name) {
44
+ const trimmed = name.trim();
45
+ if (!trimmed)
46
+ return 'Name is required';
47
+ if (!PROFILE_NAME_PATTERN.test(trimmed)) {
48
+ return 'Use only letters, numbers, spaces, "-" and "_"';
49
+ }
50
+ if (toClaudeProfileSlug(trimmed) === DEFAULT_CLAUDE_PROFILE) {
51
+ return `"${DEFAULT_CLAUDE_PROFILE}" is reserved`;
52
+ }
53
+ return null;
54
+ }
55
+ export async function listClaudeProfiles() {
56
+ const rows = await prisma.claudeProfile.findMany({ orderBy: { createdAt: 'asc' } });
57
+ return rows.map((row) => ({
58
+ slug: row.slug,
59
+ name: row.name,
60
+ configDir: path.join(getClaudeProfilesDir(), row.slug),
61
+ }));
62
+ }
63
+ export async function claudeProfileExists(slug) {
64
+ if (slug === DEFAULT_CLAUDE_PROFILE)
65
+ return true;
66
+ return (await prisma.claudeProfile.count({ where: { slug } })) > 0;
67
+ }
68
+ export async function createClaudeProfile(name) {
69
+ const error = validateClaudeProfileName(name);
70
+ if (error)
71
+ throw new Error(error);
72
+ const trimmed = name.trim();
73
+ const slug = toClaudeProfileSlug(trimmed);
74
+ if (await claudeProfileExists(slug)) {
75
+ throw new Error(`Profile "${slug}" already exists`);
76
+ }
77
+ const configDir = path.join(getClaudeProfilesDir(), slug);
78
+ await fs.mkdir(configDir, { recursive: true });
79
+ await prisma.claudeProfile.create({ data: { slug, name: trimmed } });
80
+ return { slug, name: trimmed, configDir };
81
+ }
82
+ /**
83
+ * プロファイルを削除する。ログイン情報(macOS は Keychain)を残さないよう先に logout し、
84
+ * このプロファイルを割り当てていたスコープは割り当てを外して親の設定を継承させる。
85
+ */
86
+ export async function deleteClaudeProfile(slug) {
87
+ const configDir = getClaudeProfileConfigDir(slug);
88
+ if (!configDir)
89
+ throw new Error('The system default profile cannot be deleted');
90
+ await runClaude(['auth', 'logout'], configDir).catch(() => undefined);
91
+ await prisma.environmentVariable.deleteMany({
92
+ where: { key: CLAUDE_PROFILE_ENV_KEY, value: slug },
93
+ });
94
+ await prisma.claudeProfile.delete({ where: { slug } });
95
+ await fs.rm(configDir, { recursive: true, force: true });
96
+ }
97
+ /**
98
+ * claude を実行するときの環境変数。プロファイルのログインを見るよう CLAUDE_CONFIG_DIR を付け、
99
+ * ログインより優先される認証情報の環境変数は取り除く。システム既定は外側 Terminal の環境のまま
100
+ * (普段ターミナルで claude を使うときと同じ)。
101
+ */
102
+ function claudeEnv(configDir) {
103
+ const env = { ...process.env };
104
+ if (configDir) {
105
+ env.CLAUDE_CONFIG_DIR = configDir;
106
+ for (const key of CLAUDE_AUTH_ENV_KEYS)
107
+ delete env[key];
108
+ }
109
+ return env;
110
+ }
111
+ async function runClaude(args, configDir) {
112
+ const { stdout } = await execFileAsync('claude', args, {
113
+ env: claudeEnv(configDir),
114
+ timeout: 15_000,
115
+ });
116
+ return stdout;
117
+ }
118
+ /** `claude auth status --json` でプロファイルのログイン状態を取得する */
119
+ export async function getClaudeAuthStatus(slug) {
120
+ try {
121
+ const raw = JSON.parse(await runClaude(['auth', 'status', '--json'], getClaudeProfileConfigDir(slug)));
122
+ const str = (v) => (typeof v === 'string' ? v : undefined);
123
+ return {
124
+ loggedIn: raw.loggedIn === true,
125
+ authMethod: str(raw.authMethod),
126
+ email: str(raw.email),
127
+ orgName: str(raw.orgName),
128
+ subscriptionType: str(raw.subscriptionType),
129
+ };
130
+ }
131
+ catch (err) {
132
+ // claude auth status は未ログイン時に非 0 で終了するが、stdout には JSON が出る
133
+ const stdout = err.stdout;
134
+ if (stdout) {
135
+ try {
136
+ const raw = JSON.parse(stdout);
137
+ return { loggedIn: raw.loggedIn === true, authMethod: raw.authMethod };
138
+ }
139
+ catch {
140
+ // fallthrough
141
+ }
142
+ }
143
+ return { loggedIn: false, error: err instanceof Error ? err.message : String(err) };
144
+ }
145
+ }
146
+ /** ログイン用 PTY に書き込むコマンド */
147
+ export function buildClaudeLoginCommand() {
148
+ return 'claude auth login';
149
+ }
150
+ /** ログイン用 PTY の環境変数(PtyManager.createSession の env / unsetKeys) */
151
+ export function claudeLoginPtyEnv(slug) {
152
+ const configDir = getClaudeProfileConfigDir(slug);
153
+ if (!configDir)
154
+ return { env: {}, unsetKeys: [] };
155
+ return { env: { CLAUDE_CONFIG_DIR: configDir }, unsetKeys: CLAUDE_AUTH_ENV_KEYS };
156
+ }
157
+ /**
158
+ * 継承解決済みの環境変数に含まれるプロファイル割り当てを CLAUDE_CONFIG_DIR に変換する。
159
+ * 予約キー自体は PTY に渡さない。
160
+ */
161
+ export function resolveClaudeProfileEnv(env) {
162
+ const { [CLAUDE_PROFILE_ENV_KEY]: slug, ...rest } = env;
163
+ const configDir = slug ? getClaudeProfileConfigDir(slug) : undefined;
164
+ // システム既定: 外側 Terminal の環境のまま(普段ターミナルで claude を使うときと同じ)
165
+ if (!configDir)
166
+ return { env: rest, unsetKeys: [] };
167
+ return { env: { ...rest, CLAUDE_CONFIG_DIR: configDir }, unsetKeys: CLAUDE_AUTH_ENV_KEYS };
168
+ }
@@ -0,0 +1,12 @@
1
+ import type { Server as SocketIOServer } from 'socket.io';
2
+ /** 起動時に一度呼ぶ。反映待ちの PTY を定期的に確認する */
3
+ export declare function initPtyEnvSync(server: SocketIOServer): void;
4
+ /**
5
+ * tsunagi の環境変数(Claude プロファイルの割り当て・プロファイル削除を含む)を変更した後に呼ぶ。
6
+ * 起動中の PTY ごとに環境変数を解決し直し、作成時と違えば反映待ちにする。
7
+ */
8
+ export declare function notifyEnvChanged(): Promise<void>;
9
+ /** 反映待ちの PTY を、ユーザー操作で今すぐ作り直す */
10
+ export declare function respawnSessionNow(sessionId: string): Promise<boolean>;
11
+ /** join したクライアントに現在の反映待ち状態を伝える */
12
+ export declare function getEnvPending(sessionId: string): boolean;