@minimalcorp/tsunagi 0.1.54 → 0.2.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 (113) 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_client-reference-manifest.js +1 -1
  12. package/.next/standalone/apps/web/.next/server/app/_not-found.html +1 -1
  13. package/.next/standalone/apps/web/.next/server/app/_not-found.rsc +14 -14
  14. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
  15. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  16. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_index.segment.rsc +9 -9
  17. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  18. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  19. package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  20. package/.next/standalone/apps/web/.next/server/app/index.html +1 -1
  21. package/.next/standalone/apps/web/.next/server/app/index.rsc +16 -16
  22. package/.next/standalone/apps/web/.next/server/app/index.segments/__PAGE__.segment.rsc +4 -4
  23. package/.next/standalone/apps/web/.next/server/app/index.segments/_full.segment.rsc +16 -16
  24. package/.next/standalone/apps/web/.next/server/app/index.segments/_head.segment.rsc +4 -4
  25. package/.next/standalone/apps/web/.next/server/app/index.segments/_index.segment.rsc +9 -9
  26. package/.next/standalone/apps/web/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  27. package/.next/standalone/apps/web/.next/server/app/page.js.nft.json +1 -1
  28. package/.next/standalone/apps/web/.next/server/app/page_client-reference-manifest.js +1 -1
  29. package/.next/standalone/apps/web/.next/server/app/settings/page.js.nft.json +1 -1
  30. package/.next/standalone/apps/web/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  31. package/.next/standalone/apps/web/.next/server/app/settings.html +1 -1
  32. package/.next/standalone/apps/web/.next/server/app/settings.rsc +16 -16
  33. package/.next/standalone/apps/web/.next/server/app/settings.segments/_full.segment.rsc +16 -16
  34. package/.next/standalone/apps/web/.next/server/app/settings.segments/_head.segment.rsc +4 -4
  35. package/.next/standalone/apps/web/.next/server/app/settings.segments/_index.segment.rsc +9 -9
  36. package/.next/standalone/apps/web/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  37. package/.next/standalone/apps/web/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +4 -4
  38. package/.next/standalone/apps/web/.next/server/app/settings.segments/settings.segment.rsc +3 -3
  39. package/.next/standalone/apps/web/.next/server/app/tasks/[id]/page.js.nft.json +1 -1
  40. package/.next/standalone/apps/web/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
  41. package/.next/standalone/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0qjafjt._.js +1 -1
  42. package/.next/standalone/apps/web/.next/server/chunks/ssr/{_05_5y28._.js → _053072t._.js} +2 -2
  43. package/.next/standalone/apps/web/.next/server/chunks/ssr/{_0ncpk6b._.js → _07i72iz._.js} +2 -2
  44. package/.next/standalone/apps/web/.next/server/chunks/ssr/_09eooml._.js +7 -0
  45. package/.next/standalone/apps/web/.next/server/chunks/ssr/_0bru-z.._.js +1 -1
  46. package/.next/standalone/apps/web/.next/server/chunks/ssr/_0hnxatt._.js +1 -1
  47. package/.next/standalone/apps/web/.next/server/chunks/ssr/_0i8ojdw._.js +1 -1
  48. package/.next/standalone/apps/web/.next/server/chunks/ssr/_0mtw02l._.js +3 -0
  49. package/.next/standalone/apps/web/.next/server/chunks/ssr/_12aa-fs._.js +7 -0
  50. package/.next/standalone/apps/web/.next/server/chunks/ssr/apps_web_src_app_settings_page_tsx_0i6itlb._.js +1 -1
  51. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_09nusj_._.js → node_modules_0-g_j5j._.js} +1 -1
  52. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0axuayq._.js → node_modules_04pfam-._.js} +1 -1
  53. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_10v5lct._.js → node_modules_0_1re~9._.js} +1 -1
  54. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0h_hear._.js → node_modules_0_huoqd._.js} +1 -1
  55. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0c8sp_9._.js → node_modules_0a9pqes._.js} +1 -1
  56. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_09e0q95._.js → node_modules_0bfjoue._.js} +1 -1
  57. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0s3mkxs._.js → node_modules_0e.hm5q._.js} +1 -1
  58. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0la1a4x._.js → node_modules_0krwgny._.js} +1 -1
  59. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0zv4e8x._.js → node_modules_0l23gkf._.js} +1 -1
  60. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0w8sswp._.js → node_modules_0mpkxmv._.js} +1 -1
  61. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_06gyfuv._.js → node_modules_0oip5a1._.js} +1 -1
  62. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_117mm7f._.js → node_modules_0q_xfyl._.js} +1 -1
  63. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0nji2bx._.js → node_modules_0tod6y8._.js} +1 -1
  64. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0o9t~hh._.js → node_modules_0xp9nhx._.js} +1 -1
  65. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0_876x4._.js → node_modules_0zstt1y._.js} +1 -1
  66. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0mt.8za._.js → node_modules_0~2ubpz._.js} +1 -1
  67. package/.next/standalone/apps/web/.next/server/middleware-build-manifest.js +3 -3
  68. package/.next/standalone/apps/web/.next/server/pages/404.html +1 -1
  69. package/.next/standalone/apps/web/.next/server/pages/500.html +1 -1
  70. package/.next/standalone/apps/web/.next/server/server-reference-manifest.js +1 -1
  71. package/.next/standalone/apps/web/.next/server/server-reference-manifest.json +1 -1
  72. package/.next/standalone/apps/web/.next/static/chunks/0.qz1zt1c.y.n.js +5 -0
  73. package/.next/standalone/apps/web/.next/static/chunks/0ajw1mjy3owa4.js +1 -0
  74. package/.next/standalone/apps/web/.next/static/chunks/0fe4v5dh3whem.js +5 -0
  75. package/.next/standalone/apps/web/.next/static/chunks/{0vxoeg5sfua~3.js → 0gzt~9euv6u2y.js} +2 -2
  76. package/.next/standalone/apps/web/.next/static/chunks/0jcorj3-aambm.js +1 -0
  77. package/.next/standalone/apps/web/.next/static/chunks/{0pavejkn3rkj5.js → 0o6ggm7jrim25.js} +1 -1
  78. package/.next/standalone/apps/web/.next/static/chunks/{0p3_2uc39mlse.js → 0s7g4uwmm49t..js} +2 -2
  79. package/.next/standalone/apps/web/.next/static/chunks/0wnto9g4i0s3h.css +4 -0
  80. package/.next/standalone/apps/web/.next/static/chunks/{0jc3jwed_8vre.js → 0~_c.mwboavrv.js} +2 -2
  81. package/dist/app.d.ts +1 -0
  82. package/dist/app.js +375 -0
  83. package/dist/cli.js +152 -290
  84. package/dist/plugin-lifecycle.d.ts +9 -10
  85. package/dist/plugin-lifecycle.js +77 -65
  86. package/dist/server/index.js +18 -3
  87. package/dist/server/lib/auto-update.d.ts +18 -0
  88. package/dist/server/lib/auto-update.js +208 -0
  89. package/dist/server/lib/restart.d.ts +8 -0
  90. package/dist/server/lib/restart.js +13 -0
  91. package/dist/server/lib/update-check.js +14 -4
  92. package/dist/server/pty-manager.d.ts +2 -0
  93. package/dist/server/pty-manager.js +31 -2
  94. package/dist/server/routes/version.js +16 -0
  95. package/dist/single-instance-lock.js +3 -17
  96. package/dist/update-lifecycle.d.ts +19 -0
  97. package/dist/update-lifecycle.js +93 -0
  98. package/dist/versions.d.ts +60 -0
  99. package/dist/versions.js +95 -0
  100. package/dist/with-plugin.js +6 -8
  101. package/npm-shrinkwrap.json +35 -56
  102. package/package.json +1 -1
  103. package/.next/standalone/apps/web/.next/server/chunks/ssr/_0hqrmyz._.js +0 -3
  104. package/.next/standalone/apps/web/.next/server/chunks/ssr/_0m.hvms._.js +0 -7
  105. package/.next/standalone/apps/web/.next/server/chunks/ssr/_0mljs_s._.js +0 -7
  106. package/.next/standalone/apps/web/.next/static/chunks/00b2u96xq7qxl.css +0 -4
  107. package/.next/standalone/apps/web/.next/static/chunks/08-d-ag2euha9.js +0 -5
  108. package/.next/standalone/apps/web/.next/static/chunks/0gpkw.1e6kcpn.js +0 -1
  109. package/.next/standalone/apps/web/.next/static/chunks/0v6gxm~ldhmma.js +0 -1
  110. package/.next/standalone/apps/web/.next/static/chunks/13y6z-vus9yvr.js +0 -5
  111. /package/.next/standalone/apps/web/.next/static/{wiMenzJBT5A0KltHNQWHr → OXWK1FxsZh6lcEQawkL6h}/_buildManifest.js +0 -0
  112. /package/.next/standalone/apps/web/.next/static/{wiMenzJBT5A0KltHNQWHr → OXWK1FxsZh6lcEQawkL6h}/_clientMiddlewareManifest.js +0 -0
  113. /package/.next/standalone/apps/web/.next/static/{wiMenzJBT5A0KltHNQWHr → OXWK1FxsZh6lcEQawkL6h}/_ssgManifest.js +0 -0
package/dist/app.js ADDED
@@ -0,0 +1,375 @@
1
+ import { spawn, spawnSync } from 'node:child_process';
2
+ import * as fs from 'node:fs';
3
+ import * as http from 'node:http';
4
+ import * as path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import openBrowser from 'open';
7
+ import { assertClaudeSupportsPluginDirs, getPluginDir, removeLegacyPluginInstall, } from './plugin-lifecycle.js';
8
+ import { isAutoUpdateEnabled, isEntryOutdated, removeVersionsOlderThan, snapshotDbBeforeFirstStart, waitForPortsFree, } from './update-lifecycle.js';
9
+ import { RESTART_EXIT_CODE } from './versions.js';
10
+ /**
11
+ * tsunagi app: spawned by the entry (dist/cli.js), runs the servers.
12
+ *
13
+ * - Exits only after every child process has exited, so the entry can
14
+ * start the next version right after this process exits.
15
+ * - Fastify exits with RESTART_EXIT_CODE when the user requests a restart to
16
+ * apply an update; this process then stops the rest and exits with the same code.
17
+ * - Sends `{ type: 'ready' }` to the entry over IPC once the servers respond.
18
+ * - Handles the version-specific parts of the update (update-lifecycle.ts).
19
+ *
20
+ * Layout when installed via npm:
21
+ *
22
+ * <pkg>/dist/cli.js ← entry (bin)
23
+ * <pkg>/dist/app.js ← this file
24
+ * <pkg>/dist/update-lifecycle.js
25
+ * <pkg>/dist/versions.js
26
+ * <pkg>/dist/auto-migrate.js
27
+ * <pkg>/dist/plugin-lifecycle.js
28
+ * <pkg>/dist/single-instance-lock.js
29
+ * <pkg>/dist/server/index.js ← Fastify entry (bundled from apps/server/dist)
30
+ * <pkg>/dist/server/lib/**
31
+ * <pkg>/dist/server/generated/prisma/**
32
+ * <pkg>/.next/standalone/apps/web/server.js ← Next.js standalone entry (bundled from apps/web/.next/standalone)
33
+ * <pkg>/.next/standalone/apps/web/.next/static/
34
+ * <pkg>/.next/standalone/node_modules/
35
+ * <pkg>/dist/prisma/schema.prisma ← bundled from apps/server/prisma
36
+ * <pkg>/dist/prisma/migrations/**
37
+ * <pkg>/dist/prisma.config.ts ← passed via --config by dist/auto-migrate.js
38
+ * <pkg>/dist/scripts/monaco-editor.sh ← $EDITOR for Ctrl+G (resolved by dist/server/routes/terminal.js)
39
+ * <pkg>/dist/docs/ ← bundled from apps/docs/out (served on DOCS_PORT)
40
+ * <pkg>/dist/whisper-server/ ← bundled from apps/whisper-server
41
+ * <pkg>/tsunagi-marketplace/plugins/tsunagi-plugin/.claude-plugin/plugin.json
42
+ */
43
+ // ---------------------------------------------------------------------------
44
+ // Braille-dots spinner
45
+ // ---------------------------------------------------------------------------
46
+ const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
47
+ function createSpinner(message) {
48
+ let i = 0;
49
+ // Write first frame immediately so it shows even during spawnSync/execSync blocking
50
+ process.stdout.write(`\r${SPINNER_FRAMES[0]} ${message}`);
51
+ i++;
52
+ const timer = setInterval(() => {
53
+ process.stdout.write(`\r${SPINNER_FRAMES[i % SPINNER_FRAMES.length]} ${message}`);
54
+ i++;
55
+ }, 80);
56
+ return {
57
+ stop() {
58
+ clearInterval(timer);
59
+ // Clear the spinner line
60
+ process.stdout.write('\r' + ' '.repeat(message.length + 4) + '\r');
61
+ },
62
+ };
63
+ }
64
+ let spinner = createSpinner('Initializing...');
65
+ const DIST_DIR = path.dirname(fileURLToPath(import.meta.url));
66
+ const PACKAGE_ROOT = path.resolve(DIST_DIR, '..');
67
+ const AUTO_MIGRATE_JS = path.join(DIST_DIR, 'auto-migrate.js');
68
+ const FASTIFY_ENTRY_JS = path.join(DIST_DIR, 'server', 'index.js');
69
+ const NEXT_STANDALONE_ENTRY = path.join(PACKAGE_ROOT, '.next', 'standalone', 'apps', 'web', 'server.js');
70
+ const DOCS_DIR = path.join(DIST_DIR, 'docs');
71
+ const DOCS_PORT = 2793;
72
+ const isDebug = !!process.env.TSUNAGI_DEBUG;
73
+ const isDocker = fs.existsSync('/.dockerenv');
74
+ // 更新適用のための再起動ではブラウザを開き直さない(既存のタブが再接続する)
75
+ const isRestart = process.env.TSUNAGI_RESTARTED === '1';
76
+ // server(apps/server/src/lib/auto-update.ts)に自動更新の可否と entry の古さを伝える
77
+ if (!isAutoUpdateEnabled())
78
+ delete process.env.TSUNAGI_AUTO_UPDATE;
79
+ if (isEntryOutdated())
80
+ process.env.TSUNAGI_ENTRY_OUTDATED = '1';
81
+ // サーバー側の更新確認(apps/server/src/lib/update-check.ts)に実行中のバージョンを伝える。
82
+ function readPackageVersion() {
83
+ try {
84
+ const pkg = JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, 'package.json'), 'utf8'));
85
+ return typeof pkg.version === 'string' ? pkg.version : undefined;
86
+ }
87
+ catch {
88
+ return undefined;
89
+ }
90
+ }
91
+ const packageVersion = readPackageVersion();
92
+ // ---------------------------------------------------------------------------
93
+ // ASCII art
94
+ // ---------------------------------------------------------------------------
95
+ const TSUNAGI_AA = `
96
+ __ _
97
+ / /____ __ _____ ___ ____ _(_)
98
+ / __(_-</ // / _ \\/ _ \`/ _ \`/ /
99
+ \\__/___/\\_,_/_//_/\\_,_/\\_, /_/
100
+ /___/
101
+ `;
102
+ // ---------------------------------------------------------------------------
103
+ // Phase 1: Auto-migrate (synchronous child process)
104
+ // ---------------------------------------------------------------------------
105
+ function runAutoMigrate() {
106
+ if (!fs.existsSync(AUTO_MIGRATE_JS)) {
107
+ console.error(`[tsunagi] Missing build artifact: ${AUTO_MIGRATE_JS}`);
108
+ process.exit(1);
109
+ }
110
+ const result = spawnSync(process.execPath, [AUTO_MIGRATE_JS], {
111
+ stdio: isDebug ? 'inherit' : ['inherit', 'pipe', 'pipe'],
112
+ cwd: PACKAGE_ROOT,
113
+ });
114
+ if (result.status !== 0) {
115
+ const stderr = result.stderr?.toString().trim();
116
+ if (stderr)
117
+ console.error(stderr);
118
+ console.error('[tsunagi] Database migration failed.');
119
+ process.exit(result.status ?? 1);
120
+ }
121
+ // Display migration result (e.g. "1 migration applied.")
122
+ const stdout = result.stdout?.toString().trim();
123
+ if (stdout)
124
+ console.log(stdout);
125
+ }
126
+ // 初めて起動する版なら、起動に失敗したときに戻せるようマイグレーション前に DB を退避する
127
+ if (packageVersion)
128
+ snapshotDbBeforeFirstStart(packageVersion);
129
+ runAutoMigrate();
130
+ // ---------------------------------------------------------------------------
131
+ // Phase 2: Plugin lifecycle
132
+ // ---------------------------------------------------------------------------
133
+ assertClaudeSupportsPluginDirs();
134
+ removeLegacyPluginInstall();
135
+ // ---------------------------------------------------------------------------
136
+ // Docs static file server
137
+ // ---------------------------------------------------------------------------
138
+ const MIME_TYPES = {
139
+ '.html': 'text/html; charset=utf-8',
140
+ '.css': 'text/css',
141
+ '.js': 'application/javascript',
142
+ '.json': 'application/json',
143
+ '.png': 'image/png',
144
+ '.jpg': 'image/jpeg',
145
+ '.svg': 'image/svg+xml',
146
+ '.ico': 'image/x-icon',
147
+ '.wasm': 'application/wasm',
148
+ '.woff': 'font/woff',
149
+ '.woff2': 'font/woff2',
150
+ '.ttf': 'font/ttf',
151
+ '.txt': 'text/plain',
152
+ '.xml': 'application/xml',
153
+ };
154
+ function startDocsServer() {
155
+ if (!fs.existsSync(DOCS_DIR))
156
+ return null;
157
+ const serveFile = (filePath, res) => {
158
+ const ext = path.extname(filePath);
159
+ const contentType = MIME_TYPES[ext] || 'application/octet-stream';
160
+ fs.readFile(filePath, (err, data) => {
161
+ if (err) {
162
+ res.writeHead(404).end();
163
+ return;
164
+ }
165
+ res.writeHead(200, { 'Content-Type': contentType });
166
+ res.end(data);
167
+ });
168
+ };
169
+ const server = http.createServer((req, res) => {
170
+ let urlPath;
171
+ try {
172
+ urlPath = decodeURIComponent(new URL(req.url ?? '/', 'http://localhost').pathname);
173
+ }
174
+ catch {
175
+ res.writeHead(400).end();
176
+ return;
177
+ }
178
+ const filePath = path.resolve(DOCS_DIR, '.' + urlPath);
179
+ if (!filePath.startsWith(DOCS_DIR)) {
180
+ res.writeHead(403).end();
181
+ return;
182
+ }
183
+ fs.stat(filePath, (err, stat) => {
184
+ if (err) {
185
+ res.writeHead(404).end();
186
+ return;
187
+ }
188
+ if (stat.isDirectory()) {
189
+ if (!urlPath.endsWith('/')) {
190
+ res.writeHead(301, { Location: urlPath + '/' }).end();
191
+ return;
192
+ }
193
+ serveFile(path.join(filePath, 'index.html'), res);
194
+ }
195
+ else {
196
+ serveFile(filePath, res);
197
+ }
198
+ });
199
+ });
200
+ server.listen(DOCS_PORT, '0.0.0.0');
201
+ return server;
202
+ }
203
+ // ---------------------------------------------------------------------------
204
+ // Phase 3: Verify build artifacts & spawn servers
205
+ // ---------------------------------------------------------------------------
206
+ function verifyArtifact(p, label) {
207
+ if (!fs.existsSync(p)) {
208
+ console.error(`[tsunagi] Missing ${label}: ${p}`);
209
+ console.error('[tsunagi] The package appears to be incomplete. Please reinstall.');
210
+ process.exit(1);
211
+ }
212
+ }
213
+ verifyArtifact(FASTIFY_ENTRY_JS, 'Fastify server artifact');
214
+ verifyArtifact(NEXT_STANDALONE_ENTRY, 'Next.js standalone artifact');
215
+ // Fastify が単一の公開ポート。Next.js は内部ポートで動かし Fastify からプロキシする。
216
+ const PORT = process.env.PORT ?? '2791';
217
+ const NEXT_PORT = '2792';
218
+ // 更新を適用するための再起動では、前の版が使っていたポートが空くのを待つ
219
+ if (isRestart)
220
+ await waitForPortsFree([Number(PORT), Number(NEXT_PORT), DOCS_PORT]);
221
+ // Fastify サーバー自身の待受ポートは TSUNAGI_SERVER_PORT で伝える(generic な PORT は上書きしない)。
222
+ // PORT を直接上書きすると、ユーザーが Terminal で明示的に PORT=xxxx と指定していた場合でも
223
+ // tsunagi 内部ターミナル側でその値と tsunagi 自身の値を区別できなくなるため。
224
+ //
225
+ // NODE_ENV は Fastify/Next 等が直接参照するため PORT と同じ専用キー方式が使えず、サーバー
226
+ // プロセス自身には強制的に 'production' を渡す必要がある。上書きする前に外側 Terminal 由来の
227
+ // 元の値(未設定なら undefined)を TSUNAGI_OUTER_NODE_ENV に退避し、tsunagi 内部ターミナル側
228
+ // (apps/server/src/pty-manager.ts)で復元できるようにする。
229
+ const outerNodeEnv = process.env.NODE_ENV;
230
+ const fastifyChild = spawn(process.execPath, [FASTIFY_ENTRY_JS], {
231
+ stdio: ['inherit', 'pipe', 'pipe'],
232
+ cwd: PACKAGE_ROOT,
233
+ env: {
234
+ ...process.env,
235
+ NODE_ENV: 'production',
236
+ ...(outerNodeEnv !== undefined ? { TSUNAGI_OUTER_NODE_ENV: outerNodeEnv } : {}),
237
+ TSUNAGI_SERVER_PORT: PORT,
238
+ TSUNAGI_NEXT_PORT: NEXT_PORT,
239
+ // PTY の CLAUDE_CODE_PLUGIN_DIRS に渡す(apps/server/src/pty-manager.ts)
240
+ TSUNAGI_PLUGIN_DIR: getPluginDir(),
241
+ ...(packageVersion ? { TSUNAGI_VERSION: packageVersion } : {}),
242
+ },
243
+ });
244
+ const nextChild = spawn(process.execPath, [NEXT_STANDALONE_ENTRY], {
245
+ stdio: ['inherit', 'pipe', 'pipe'],
246
+ cwd: path.dirname(NEXT_STANDALONE_ENTRY),
247
+ env: { ...process.env, PORT: NEXT_PORT, NODE_ENV: 'production', HOSTNAME: '0.0.0.0' },
248
+ });
249
+ const docsServer = startDocsServer();
250
+ // ---------------------------------------------------------------------------
251
+ // Child process output forwarding
252
+ // ---------------------------------------------------------------------------
253
+ fastifyChild.stdout?.on('data', (data) => {
254
+ if (isDebug)
255
+ process.stdout.write(data);
256
+ });
257
+ fastifyChild.stderr?.on('data', (data) => {
258
+ process.stderr.write(data);
259
+ });
260
+ nextChild.stdout?.on('data', (data) => {
261
+ if (isDebug)
262
+ process.stdout.write(data);
263
+ });
264
+ nextChild.stderr?.on('data', (data) => {
265
+ process.stderr.write(data);
266
+ });
267
+ // ---------------------------------------------------------------------------
268
+ // Ready detection via /health polling
269
+ // ---------------------------------------------------------------------------
270
+ const POLL_INTERVAL_MS = 300;
271
+ function pollHealth(port) {
272
+ return new Promise((resolve) => {
273
+ const poll = () => {
274
+ const req = http.get(`http://localhost:${port}/health`, (res) => {
275
+ if (res.statusCode === 200) {
276
+ res.resume();
277
+ resolve();
278
+ }
279
+ else {
280
+ res.resume();
281
+ setTimeout(poll, POLL_INTERVAL_MS);
282
+ }
283
+ });
284
+ req.on('error', () => {
285
+ setTimeout(poll, POLL_INTERVAL_MS);
286
+ });
287
+ };
288
+ poll();
289
+ });
290
+ }
291
+ // Fastify(PORT) と Next(NEXT_PORT) は各々 /health を持つ。各プロセスを直接叩く。
292
+ Promise.all([pollHealth(Number(PORT)), pollHealth(Number(NEXT_PORT))]).then(() => {
293
+ spinner.stop();
294
+ console.log(TSUNAGI_AA);
295
+ const url = `http://localhost:${PORT}`;
296
+ // entry に起動完了を伝える(起動失敗時のロールバック判定に使う)
297
+ process.send?.({ type: 'ready' });
298
+ if (packageVersion && isAutoUpdateEnabled())
299
+ removeVersionsOlderThan(packageVersion);
300
+ if (!isDocker && !isRestart) {
301
+ // macOS / Linux / WSL(Windows 側ブラウザ)の差異は open パッケージが吸収する。
302
+ // ブラウザを開けなくてもサーバーは継続する(URL は下で案内する)
303
+ openBrowser(url).catch(() => { });
304
+ }
305
+ console.log(`Open ${url}`);
306
+ if (docsServer) {
307
+ console.log(`Docs http://localhost:${DOCS_PORT}`);
308
+ }
309
+ });
310
+ // ---------------------------------------------------------------------------
311
+ // Shutdown
312
+ // ---------------------------------------------------------------------------
313
+ // Fastify は PTY・SearXNG(docker stop)・ローカルLLM の解放を待ってから終了するため長めに待つ
314
+ const CHILD_EXIT_TIMEOUT_MS = 30_000;
315
+ let shuttingDown = false;
316
+ /** 子プロセスの終了を待つ。timeoutMs 以内に終わらなければ SIGKILL する */
317
+ function stopChild(child, timeoutMs) {
318
+ if (child.exitCode !== null || child.signalCode !== null)
319
+ return Promise.resolve();
320
+ return new Promise((resolve) => {
321
+ const timer = setTimeout(() => {
322
+ try {
323
+ child.kill('SIGKILL');
324
+ }
325
+ catch {
326
+ // ignore
327
+ }
328
+ }, timeoutMs);
329
+ child.once('exit', () => {
330
+ clearTimeout(timer);
331
+ resolve();
332
+ });
333
+ try {
334
+ child.kill('SIGTERM');
335
+ }
336
+ catch {
337
+ // ignore
338
+ }
339
+ });
340
+ }
341
+ async function shutdown(code) {
342
+ if (shuttingDown)
343
+ return;
344
+ shuttingDown = true;
345
+ spinner.stop();
346
+ const cleanupSpinner = createSpinner(code === RESTART_EXIT_CODE ? 'Restarting to apply the update...' : 'Cleaning up...');
347
+ await Promise.all([
348
+ stopChild(fastifyChild, CHILD_EXIT_TIMEOUT_MS),
349
+ stopChild(nextChild, CHILD_EXIT_TIMEOUT_MS),
350
+ new Promise((resolve) => (docsServer ? docsServer.close(() => resolve()) : resolve())),
351
+ ]);
352
+ cleanupSpinner.stop();
353
+ process.exit(code);
354
+ }
355
+ process.on('SIGINT', () => void shutdown(0));
356
+ process.on('SIGTERM', () => void shutdown(0));
357
+ process.on('SIGHUP', () => void shutdown(0));
358
+ // entry が異常終了した場合も取り残されないよう終了する
359
+ process.on('disconnect', () => void shutdown(0));
360
+ fastifyChild.on('exit', (code) => {
361
+ if (shuttingDown)
362
+ return;
363
+ if (code === RESTART_EXIT_CODE) {
364
+ void shutdown(RESTART_EXIT_CODE);
365
+ return;
366
+ }
367
+ console.error(`[tsunagi] Fastify server exited unexpectedly (code ${code})`);
368
+ void shutdown(code ?? 1);
369
+ });
370
+ nextChild.on('exit', (code) => {
371
+ if (!shuttingDown) {
372
+ console.error(`[tsunagi] Next.js server exited unexpectedly (code ${code})`);
373
+ void shutdown(code ?? 1);
374
+ }
375
+ });