@minimalcorp/tsunagi 0.1.55 → 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 (111) 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/_0i8ojdw._.js +1 -1
  47. package/.next/standalone/apps/web/.next/server/chunks/ssr/_0mtw02l._.js +3 -0
  48. package/.next/standalone/apps/web/.next/server/chunks/ssr/_12aa-fs._.js +7 -0
  49. package/.next/standalone/apps/web/.next/server/chunks/ssr/apps_web_src_app_settings_page_tsx_0i6itlb._.js +1 -1
  50. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_09nusj_._.js → node_modules_0-g_j5j._.js} +1 -1
  51. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0axuayq._.js → node_modules_04pfam-._.js} +1 -1
  52. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_10v5lct._.js → node_modules_0_1re~9._.js} +1 -1
  53. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0h_hear._.js → node_modules_0_huoqd._.js} +1 -1
  54. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0c8sp_9._.js → node_modules_0a9pqes._.js} +1 -1
  55. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_09e0q95._.js → node_modules_0bfjoue._.js} +1 -1
  56. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0s3mkxs._.js → node_modules_0e.hm5q._.js} +1 -1
  57. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0la1a4x._.js → node_modules_0krwgny._.js} +1 -1
  58. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0zv4e8x._.js → node_modules_0l23gkf._.js} +1 -1
  59. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0w8sswp._.js → node_modules_0mpkxmv._.js} +1 -1
  60. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_06gyfuv._.js → node_modules_0oip5a1._.js} +1 -1
  61. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_117mm7f._.js → node_modules_0q_xfyl._.js} +1 -1
  62. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0nji2bx._.js → node_modules_0tod6y8._.js} +1 -1
  63. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0o9t~hh._.js → node_modules_0xp9nhx._.js} +1 -1
  64. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0_876x4._.js → node_modules_0zstt1y._.js} +1 -1
  65. package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0mt.8za._.js → node_modules_0~2ubpz._.js} +1 -1
  66. package/.next/standalone/apps/web/.next/server/middleware-build-manifest.js +3 -3
  67. package/.next/standalone/apps/web/.next/server/pages/404.html +1 -1
  68. package/.next/standalone/apps/web/.next/server/pages/500.html +1 -1
  69. package/.next/standalone/apps/web/.next/server/server-reference-manifest.js +1 -1
  70. package/.next/standalone/apps/web/.next/server/server-reference-manifest.json +1 -1
  71. package/.next/standalone/apps/web/.next/static/chunks/0.qz1zt1c.y.n.js +5 -0
  72. package/.next/standalone/apps/web/.next/static/chunks/0ajw1mjy3owa4.js +1 -0
  73. package/.next/standalone/apps/web/.next/static/chunks/0fe4v5dh3whem.js +5 -0
  74. package/.next/standalone/apps/web/.next/static/chunks/{0vxoeg5sfua~3.js → 0gzt~9euv6u2y.js} +2 -2
  75. package/.next/standalone/apps/web/.next/static/chunks/0jcorj3-aambm.js +1 -0
  76. package/.next/standalone/apps/web/.next/static/chunks/{0p3_2uc39mlse.js → 0s7g4uwmm49t..js} +2 -2
  77. package/.next/standalone/apps/web/.next/static/chunks/0wnto9g4i0s3h.css +4 -0
  78. package/.next/standalone/apps/web/.next/static/chunks/{0jc3jwed_8vre.js → 0~_c.mwboavrv.js} +2 -2
  79. package/dist/app.d.ts +1 -0
  80. package/dist/app.js +375 -0
  81. package/dist/cli.js +152 -290
  82. package/dist/plugin-lifecycle.d.ts +9 -10
  83. package/dist/plugin-lifecycle.js +77 -65
  84. package/dist/server/index.js +18 -3
  85. package/dist/server/lib/auto-update.d.ts +18 -0
  86. package/dist/server/lib/auto-update.js +208 -0
  87. package/dist/server/lib/restart.d.ts +8 -0
  88. package/dist/server/lib/restart.js +13 -0
  89. package/dist/server/lib/update-check.js +14 -4
  90. package/dist/server/pty-manager.d.ts +2 -0
  91. package/dist/server/pty-manager.js +31 -2
  92. package/dist/server/routes/version.js +16 -0
  93. package/dist/single-instance-lock.js +3 -17
  94. package/dist/update-lifecycle.d.ts +19 -0
  95. package/dist/update-lifecycle.js +93 -0
  96. package/dist/versions.d.ts +60 -0
  97. package/dist/versions.js +95 -0
  98. package/dist/with-plugin.js +6 -8
  99. package/npm-shrinkwrap.json +8 -8
  100. package/package.json +1 -1
  101. package/.next/standalone/apps/web/.next/server/chunks/ssr/_0hqrmyz._.js +0 -3
  102. package/.next/standalone/apps/web/.next/server/chunks/ssr/_0m.hvms._.js +0 -7
  103. package/.next/standalone/apps/web/.next/server/chunks/ssr/_0mljs_s._.js +0 -7
  104. package/.next/standalone/apps/web/.next/static/chunks/08-d-ag2euha9.js +0 -5
  105. package/.next/standalone/apps/web/.next/static/chunks/0c.dwezn5qjvs.css +0 -4
  106. package/.next/standalone/apps/web/.next/static/chunks/0gpkw.1e6kcpn.js +0 -1
  107. package/.next/standalone/apps/web/.next/static/chunks/0v6gxm~ldhmma.js +0 -1
  108. package/.next/standalone/apps/web/.next/static/chunks/13y6z-vus9yvr.js +0 -5
  109. /package/.next/standalone/apps/web/.next/static/{EOnxRuFun6cfXk-y9KPq5 → OXWK1FxsZh6lcEQawkL6h}/_buildManifest.js +0 -0
  110. /package/.next/standalone/apps/web/.next/static/{EOnxRuFun6cfXk-y9KPq5 → OXWK1FxsZh6lcEQawkL6h}/_clientMiddlewareManifest.js +0 -0
  111. /package/.next/standalone/apps/web/.next/static/{EOnxRuFun6cfXk-y9KPq5 → OXWK1FxsZh6lcEQawkL6h}/_ssgManifest.js +0 -0
package/dist/cli.js CHANGED
@@ -1,328 +1,190 @@
1
1
  #!/usr/bin/env node
2
- import { spawn, spawnSync } from 'node:child_process';
2
+ import { spawn } from 'node:child_process';
3
3
  import * as fs from 'node:fs';
4
- import * as http from 'node:http';
5
4
  import * as path from 'node:path';
6
5
  import { fileURLToPath } from 'node:url';
7
- import openBrowser from 'open';
8
- import { cleanupPluginState, ensureCleanPluginState } from './plugin-lifecycle.js';
9
6
  import { acquireSingleInstanceLock } from './single-instance-lock.js';
7
+ import { ENTRY_PROTOCOL, RESTART_EXIT_CODE, compareVersions, getBrokenVersions, getPackageRootOf, isInstalled, readEntryState, restoreSnapshot, updateEntryState, } from './versions.js';
10
8
  /**
11
- * Production CLI entrypoint shipped as `bin` in @minimalcorp/tsunagi.
9
+ * Entry: the `tsunagi` bin of @minimalcorp/tsunagi.
12
10
  *
13
- * Layout when installed via npm:
11
+ * This file stays on the user's machine until they reinstall the package,
12
+ * while the app (dist/app.js) is replaced by every auto-update. So it only
13
+ * starts the app and starts it again; every other decision belongs to the app.
14
+ * Follow the contract in versions.ts when changing anything here.
14
15
  *
15
- * <pkg>/dist/cli.js ← this file
16
- * <pkg>/dist/auto-migrate.js
17
- * <pkg>/dist/plugin-lifecycle.js
18
- * <pkg>/dist/single-instance-lock.js
19
- * <pkg>/dist/server/index.js ← Fastify entry (bundled from apps/server/dist)
20
- * <pkg>/dist/server/lib/**
21
- * <pkg>/dist/server/generated/prisma/**
22
- * <pkg>/.next/standalone/apps/web/server.js ← Next.js standalone entry (bundled from apps/web/.next/standalone)
23
- * <pkg>/.next/standalone/apps/web/.next/static/
24
- * <pkg>/.next/standalone/node_modules/
25
- * <pkg>/dist/prisma/schema.prisma ← bundled from apps/server/prisma
26
- * <pkg>/dist/prisma/migrations/**
27
- * <pkg>/dist/prisma.config.ts ← passed via --config by dist/auto-migrate.js
28
- * <pkg>/dist/scripts/monaco-editor.sh ← $EDITOR for Ctrl+G (resolved by dist/server/routes/terminal.js)
29
- * <pkg>/dist/docs/ ← bundled from apps/docs/out (served on DOCS_PORT)
30
- * <pkg>/dist/whisper-server/ ← bundled from apps/whisper-server
31
- * <pkg>/tsunagi-marketplace/plugins/tsunagi-plugin/.claude-plugin/plugin.json
16
+ * - Holds the single-instance lock and forwards SIGINT / SIGTERM / SIGHUP.
17
+ * - Runs the app of `next` (or `lastGood`) from ~/.tsunagi/versions/, or of
18
+ * this package when it is newer (reinstalled by hand).
19
+ * - Runs the app in its own process group and kills what is left of the
20
+ * group after the app exits.
21
+ * - Starts the app again when it exits with RESTART_EXIT_CODE.
22
+ * - When a version other than `lastGood` exits before `ready`, marks it broken,
23
+ * restores the DB snapshot the app took, and starts `lastGood` again.
32
24
  */
33
- // ---------------------------------------------------------------------------
34
- // Braille-dots spinner
35
- // ---------------------------------------------------------------------------
36
- const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
37
- function createSpinner(message) {
38
- let i = 0;
39
- // Write first frame immediately so it shows even during spawnSync/execSync blocking
40
- process.stdout.write(`\r${SPINNER_FRAMES[0]} ${message}`);
41
- i++;
42
- const timer = setInterval(() => {
43
- process.stdout.write(`\r${SPINNER_FRAMES[i % SPINNER_FRAMES.length]} ${message}`);
44
- i++;
45
- }, 80);
46
- return {
47
- stop() {
48
- clearInterval(timer);
49
- // Clear the spinner line
50
- process.stdout.write('\r' + ' '.repeat(message.length + 4) + '\r');
51
- },
52
- };
53
- }
54
25
  if (process.platform !== 'darwin' && process.platform !== 'linux') {
55
26
  console.error(`[tsunagi] Unsupported platform: ${process.platform}`);
56
27
  console.error('[tsunagi] Tsunagi currently supports macOS and Linux only.');
57
28
  process.exit(1);
58
29
  }
59
- let spinner = createSpinner('Initializing...');
60
30
  acquireSingleInstanceLock();
61
- const DIST_DIR = path.dirname(fileURLToPath(import.meta.url));
62
- const PACKAGE_ROOT = path.resolve(DIST_DIR, '..');
63
- const AUTO_MIGRATE_JS = path.join(DIST_DIR, 'auto-migrate.js');
64
- const FASTIFY_ENTRY_JS = path.join(DIST_DIR, 'server', 'index.js');
65
- const NEXT_STANDALONE_ENTRY = path.join(PACKAGE_ROOT, '.next', 'standalone', 'apps', 'web', 'server.js');
66
- const DOCS_DIR = path.join(DIST_DIR, 'docs');
67
- const DOCS_PORT = 2793;
68
- const isDebug = !!process.env.TSUNAGI_DEBUG;
69
- const isDocker = fs.existsSync('/.dockerenv');
70
- // ---------------------------------------------------------------------------
71
- // ASCII art
72
- // ---------------------------------------------------------------------------
73
- const TSUNAGI_AA = `
74
- __ _
75
- / /____ __ _____ ___ ____ _(_)
76
- / __(_-</ // / _ \\/ _ \`/ _ \`/ /
77
- \\__/___/\\_,_/_//_/\\_,_/\\_, /_/
78
- /___/
79
- `;
80
- // ---------------------------------------------------------------------------
81
- // Phase 1: Auto-migrate (synchronous child process)
82
- // ---------------------------------------------------------------------------
83
- function runAutoMigrate() {
84
- if (!fs.existsSync(AUTO_MIGRATE_JS)) {
85
- console.error(`[tsunagi] Missing build artifact: ${AUTO_MIGRATE_JS}`);
86
- process.exit(1);
31
+ const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
32
+ function readPackageVersion(root) {
33
+ try {
34
+ const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
35
+ return typeof pkg.version === 'string' ? pkg.version : null;
87
36
  }
88
- const result = spawnSync(process.execPath, [AUTO_MIGRATE_JS], {
89
- stdio: isDebug ? 'inherit' : ['inherit', 'pipe', 'pipe'],
90
- cwd: PACKAGE_ROOT,
91
- });
92
- if (result.status !== 0) {
93
- const stderr = result.stderr?.toString().trim();
94
- if (stderr)
95
- console.error(stderr);
96
- console.error('[tsunagi] Database migration failed.');
97
- process.exit(result.status ?? 1);
37
+ catch {
38
+ return null;
98
39
  }
99
- // Display migration result (e.g. "1 migration applied.")
100
- const stdout = result.stdout?.toString().trim();
101
- if (stdout)
102
- console.log(stdout);
103
40
  }
104
- runAutoMigrate();
105
- // ---------------------------------------------------------------------------
106
- // Phase 2: Plugin lifecycle
107
- // ---------------------------------------------------------------------------
108
- ensureCleanPluginState();
109
- // ---------------------------------------------------------------------------
110
- // Docs static file server
111
- // ---------------------------------------------------------------------------
112
- const MIME_TYPES = {
113
- '.html': 'text/html; charset=utf-8',
114
- '.css': 'text/css',
115
- '.js': 'application/javascript',
116
- '.json': 'application/json',
117
- '.png': 'image/png',
118
- '.jpg': 'image/jpeg',
119
- '.svg': 'image/svg+xml',
120
- '.ico': 'image/x-icon',
121
- '.wasm': 'application/wasm',
122
- '.woff': 'font/woff',
123
- '.woff2': 'font/woff2',
124
- '.ttf': 'font/ttf',
125
- '.txt': 'text/plain',
126
- '.xml': 'application/xml',
127
- };
128
- function startDocsServer() {
129
- if (!fs.existsSync(DOCS_DIR))
130
- return null;
131
- const serveFile = (filePath, res) => {
132
- const ext = path.extname(filePath);
133
- const contentType = MIME_TYPES[ext] || 'application/octet-stream';
134
- fs.readFile(filePath, (err, data) => {
135
- if (err) {
136
- res.writeHead(404).end();
137
- return;
138
- }
139
- res.writeHead(200, { 'Content-Type': contentType });
140
- res.end(data);
41
+ const ownVersion = readPackageVersion(PACKAGE_ROOT) ?? '0.0.0';
42
+ // npm でインストールされた tsunagi だけを自動更新する。
43
+ // リポジトリから build して直接実行している場合(docker/compose.prd.yml 等)はこのパッケージだけを使う
44
+ const useInstalledVersions = PACKAGE_ROOT.split(path.sep).includes('node_modules') &&
45
+ process.env.TSUNAGI_DISABLE_AUTO_UPDATE !== '1';
46
+ function resolveApp(state) {
47
+ const own = { version: ownVersion, root: PACKAGE_ROOT };
48
+ if (!useInstalledVersions)
49
+ return own;
50
+ const broken = getBrokenVersions(state);
51
+ const installed = [state.next, state.lastGood].find((version) => typeof version === 'string' && !broken.includes(version) && isInstalled(version));
52
+ if (!installed)
53
+ return own;
54
+ // 手動で入れ直したこのパッケージの方が新しければ、そちらを使う
55
+ if (!broken.includes(own.version) && compareVersions(own.version, installed) >= 0)
56
+ return own;
57
+ return { version: installed, root: getPackageRootOf(installed) };
58
+ }
59
+ let current = null;
60
+ let stopping = false;
61
+ /** app のプロセスグループに残ったプロセスを止める */
62
+ function killProcessGroup(pid) {
63
+ if (!pid)
64
+ return;
65
+ try {
66
+ process.kill(-pid, 'SIGKILL');
67
+ }
68
+ catch {
69
+ // グループに誰も残っていない
70
+ }
71
+ }
72
+ function runApp(app, options) {
73
+ return new Promise((resolve) => {
74
+ let ready = false;
75
+ const env = {
76
+ ...process.env,
77
+ TSUNAGI_ENTRY_PROTOCOL: String(ENTRY_PROTOCOL),
78
+ TSUNAGI_ENTRY_PACKAGE_ROOT: PACKAGE_ROOT,
79
+ };
80
+ if (useInstalledVersions)
81
+ env.TSUNAGI_AUTO_UPDATE = '1';
82
+ if (options.lastGood)
83
+ env.TSUNAGI_LAST_GOOD = options.lastGood;
84
+ if (options.restarted)
85
+ env.TSUNAGI_RESTARTED = '1';
86
+ // detached: app と子孫を独立したプロセスグループにし、終了後にまとめて止められるようにする。
87
+ // 端末の Ctrl+C は entry だけが受け、app には SIGTERM で伝える
88
+ const child = spawn(process.execPath, [path.join(app.root, 'dist', 'app.js')], {
89
+ stdio: ['ignore', 'inherit', 'inherit', 'ipc'],
90
+ cwd: app.root,
91
+ env,
92
+ detached: true,
141
93
  });
142
- };
143
- const server = http.createServer((req, res) => {
144
- let urlPath;
145
- try {
146
- urlPath = decodeURIComponent(new URL(req.url ?? '/', 'http://localhost').pathname);
147
- }
148
- catch {
149
- res.writeHead(400).end();
150
- return;
151
- }
152
- const filePath = path.resolve(DOCS_DIR, '.' + urlPath);
153
- if (!filePath.startsWith(DOCS_DIR)) {
154
- res.writeHead(403).end();
155
- return;
156
- }
157
- fs.stat(filePath, (err, stat) => {
158
- if (err) {
159
- res.writeHead(404).end();
94
+ current = child;
95
+ child.on('message', (message) => {
96
+ if (message?.type !== 'ready' || ready)
160
97
  return;
98
+ ready = true;
99
+ try {
100
+ updateEntryState((state) => ({
101
+ ...state,
102
+ lastGood: app.version,
103
+ broken: getBrokenVersions(state).filter((version) => version !== app.version),
104
+ snapshot: null,
105
+ }));
161
106
  }
162
- if (stat.isDirectory()) {
163
- if (!urlPath.endsWith('/')) {
164
- res.writeHead(301, { Location: urlPath + '/' }).end();
165
- return;
166
- }
167
- serveFile(path.join(filePath, 'index.html'), res);
168
- }
169
- else {
170
- serveFile(filePath, res);
107
+ catch (error) {
108
+ console.error('[tsunagi] Failed to record the running version:', error);
171
109
  }
172
110
  });
111
+ child.on('error', (error) => {
112
+ console.error('[tsunagi] Failed to start the app:', error);
113
+ });
114
+ child.on('exit', (code, signal) => {
115
+ killProcessGroup(child.pid);
116
+ current = null;
117
+ resolve({ code: code ?? (signal ? 1 : 0), ready });
118
+ });
173
119
  });
174
- server.listen(DOCS_PORT, '0.0.0.0');
175
- return server;
176
120
  }
177
- // ---------------------------------------------------------------------------
178
- // Phase 3: Verify build artifacts & spawn servers
179
- // ---------------------------------------------------------------------------
180
- function verifyArtifact(p, label) {
181
- if (!fs.existsSync(p)) {
182
- console.error(`[tsunagi] Missing ${label}: ${p}`);
183
- console.error('[tsunagi] The package appears to be incomplete. Please reinstall.');
184
- process.exit(1);
121
+ function stop() {
122
+ if (stopping) {
123
+ // 2回目の Ctrl+C: 待たずに止める
124
+ killProcessGroup(current?.pid);
125
+ process.exit(130);
185
126
  }
186
- }
187
- verifyArtifact(FASTIFY_ENTRY_JS, 'Fastify server artifact');
188
- verifyArtifact(NEXT_STANDALONE_ENTRY, 'Next.js standalone artifact');
189
- // Fastify が単一の公開ポート。Next.js は内部ポートで動かし Fastify からプロキシする。
190
- const PORT = process.env.PORT ?? '2791';
191
- const NEXT_PORT = '2792';
192
- // Fastify サーバー自身の待受ポートは TSUNAGI_SERVER_PORT で伝える(generic な PORT は上書きしない)。
193
- // PORT を直接上書きすると、ユーザーが Terminal で明示的に PORT=xxxx と指定していた場合でも
194
- // tsunagi 内部ターミナル側でその値と tsunagi 自身の値を区別できなくなるため。
195
- //
196
- // NODE_ENV は Fastify/Next 等が直接参照するため PORT と同じ専用キー方式が使えず、サーバー
197
- // プロセス自身には強制的に 'production' を渡す必要がある。上書きする前に外側 Terminal 由来の
198
- // 元の値(未設定なら undefined)を TSUNAGI_OUTER_NODE_ENV に退避し、tsunagi 内部ターミナル側
199
- // (apps/server/src/pty-manager.ts)で復元できるようにする。
200
- const outerNodeEnv = process.env.NODE_ENV;
201
- // サーバー側の更新確認(apps/server/src/lib/update-check.ts)に実行中のバージョンを伝える。
202
- function readPackageVersion() {
127
+ stopping = true;
128
+ if (!current)
129
+ process.exit(0);
203
130
  try {
204
- const pkg = JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, 'package.json'), 'utf8'));
205
- return typeof pkg.version === 'string' ? pkg.version : undefined;
131
+ current.kill('SIGTERM');
206
132
  }
207
133
  catch {
208
- return undefined;
134
+ // 既に終了している
209
135
  }
210
136
  }
211
- const packageVersion = readPackageVersion();
212
- const fastifyChild = spawn(process.execPath, [FASTIFY_ENTRY_JS], {
213
- stdio: ['inherit', 'pipe', 'pipe'],
214
- cwd: PACKAGE_ROOT,
215
- env: {
216
- ...process.env,
217
- NODE_ENV: 'production',
218
- ...(outerNodeEnv !== undefined ? { TSUNAGI_OUTER_NODE_ENV: outerNodeEnv } : {}),
219
- TSUNAGI_SERVER_PORT: PORT,
220
- TSUNAGI_NEXT_PORT: NEXT_PORT,
221
- ...(packageVersion ? { TSUNAGI_VERSION: packageVersion } : {}),
222
- },
223
- });
224
- const nextChild = spawn(process.execPath, [NEXT_STANDALONE_ENTRY], {
225
- stdio: ['inherit', 'pipe', 'pipe'],
226
- cwd: path.dirname(NEXT_STANDALONE_ENTRY),
227
- env: { ...process.env, PORT: NEXT_PORT, NODE_ENV: 'production', HOSTNAME: '0.0.0.0' },
228
- });
229
- const docsServer = startDocsServer();
230
- // ---------------------------------------------------------------------------
231
- // Child process output forwarding
232
- // ---------------------------------------------------------------------------
233
- fastifyChild.stdout?.on('data', (data) => {
234
- if (isDebug)
235
- process.stdout.write(data);
236
- });
237
- fastifyChild.stderr?.on('data', (data) => {
238
- process.stderr.write(data);
239
- });
240
- nextChild.stdout?.on('data', (data) => {
241
- if (isDebug)
242
- process.stdout.write(data);
243
- });
244
- nextChild.stderr?.on('data', (data) => {
245
- process.stderr.write(data);
137
+ process.on('SIGINT', stop);
138
+ process.on('SIGTERM', stop);
139
+ process.on('SIGHUP', stop);
140
+ process.on('uncaughtException', (error) => {
141
+ console.error('[tsunagi] Uncaught exception:', error);
142
+ killProcessGroup(current?.pid);
143
+ process.exit(1);
246
144
  });
247
- // ---------------------------------------------------------------------------
248
- // Ready detection via /health polling
249
- // ---------------------------------------------------------------------------
250
- const POLL_INTERVAL_MS = 300;
251
- function pollHealth(port) {
252
- return new Promise((resolve) => {
253
- const poll = () => {
254
- const req = http.get(`http://localhost:${port}/health`, (res) => {
255
- if (res.statusCode === 200) {
256
- res.resume();
257
- resolve();
258
- }
259
- else {
260
- res.resume();
261
- setTimeout(poll, POLL_INTERVAL_MS);
262
- }
263
- });
264
- req.on('error', () => {
265
- setTimeout(poll, POLL_INTERVAL_MS);
266
- });
267
- };
268
- poll();
145
+ function setBroken(version, broken) {
146
+ updateEntryState((state) => {
147
+ const others = getBrokenVersions(state).filter((v) => v !== version);
148
+ return { ...state, broken: broken ? [...others, version] : others };
269
149
  });
270
150
  }
271
- // Fastify(PORT) と Next(NEXT_PORT) は各々 /health を持つ。各プロセスを直接叩く。
272
- Promise.all([pollHealth(Number(PORT)), pollHealth(Number(NEXT_PORT))]).then(() => {
273
- spinner.stop();
274
- console.log(TSUNAGI_AA);
275
- const url = `http://localhost:${PORT}`;
276
- if (!isDocker) {
277
- // macOS / Linux / WSL(Windows 側ブラウザ)の差異は open パッケージが吸収する。
278
- // ブラウザを開けなくてもサーバーは継続する(URL は下で案内する)
279
- openBrowser(url).catch(() => { });
280
- }
281
- console.log(`Open ${url}`);
282
- if (docsServer) {
283
- console.log(`Docs http://localhost:${DOCS_PORT}`);
284
- }
285
- });
286
- // ---------------------------------------------------------------------------
287
- // Shutdown
288
- // ---------------------------------------------------------------------------
289
- let shuttingDown = false;
290
- function shutdown(code) {
291
- if (shuttingDown)
292
- return;
293
- shuttingDown = true;
294
- spinner.stop();
295
- const cleanupSpinner = createSpinner('Cleaning up...');
296
- for (const child of [fastifyChild, nextChild]) {
297
- if (child && !child.killed && child.exitCode === null) {
298
- try {
299
- child.kill('SIGTERM');
300
- }
301
- catch {
302
- // ignore
151
+ async function main() {
152
+ let restarted = false;
153
+ // ロールバックの原因になったバージョン。戻した版も起動できなければ環境側の問題なので記録を取り消す
154
+ let rolledBackFrom = null;
155
+ for (;;) {
156
+ const state = readEntryState();
157
+ const lastGood = typeof state.lastGood === 'string' ? state.lastGood : null;
158
+ const app = resolveApp(state);
159
+ const result = await runApp(app, { restarted, lastGood });
160
+ if (stopping)
161
+ process.exit(0);
162
+ if (result.ready) {
163
+ rolledBackFrom = null;
164
+ }
165
+ else if (rolledBackFrom) {
166
+ setBroken(rolledBackFrom, false);
167
+ process.exit(result.code);
168
+ }
169
+ else if (lastGood && app.version !== lastGood) {
170
+ const { snapshot } = readEntryState();
171
+ setBroken(app.version, true);
172
+ if (resolveApp(readEntryState()).version !== app.version) {
173
+ console.error(`[tsunagi] v${app.version} failed to start (code ${result.code}). Rolling back to v${lastGood}.`);
174
+ if (snapshot?.version === app.version)
175
+ restoreSnapshot(snapshot);
176
+ updateEntryState((s) => ({ ...s, snapshot: null }));
177
+ rolledBackFrom = app.version;
178
+ continue;
303
179
  }
180
+ // 戻せる版がない
181
+ setBroken(app.version, false);
182
+ }
183
+ if (result.code === RESTART_EXIT_CODE) {
184
+ restarted = true;
185
+ continue;
304
186
  }
187
+ process.exit(result.code);
305
188
  }
306
- docsServer?.close();
307
- cleanupPluginState();
308
- cleanupSpinner.stop();
309
- process.exit(code);
310
189
  }
311
- process.on('SIGINT', () => shutdown(0));
312
- process.on('SIGTERM', () => shutdown(0));
313
- process.on('exit', () => {
314
- if (!shuttingDown)
315
- cleanupPluginState();
316
- });
317
- fastifyChild.on('exit', (code) => {
318
- if (!shuttingDown) {
319
- console.error(`[tsunagi] Fastify server exited unexpectedly (code ${code})`);
320
- shutdown(code ?? 1);
321
- }
322
- });
323
- nextChild.on('exit', (code) => {
324
- if (!shuttingDown) {
325
- console.error(`[tsunagi] Next.js server exited unexpectedly (code ${code})`);
326
- shutdown(code ?? 1);
327
- }
328
- });
190
+ void main();
@@ -1,14 +1,13 @@
1
+ /** PTY の CLAUDE_CODE_PLUGIN_DIRS に渡すプラグインのディレクトリ(絶対パス) */
2
+ export declare function getPluginDir(): string;
1
3
  /**
2
- * Ensure a clean plugin state by removing any pre-existing tsunagi plugin /
3
- * marketplace registrations and then installing fresh copies.
4
- *
5
- * Exits the process with code 1 on install failure.
6
- *
7
- * @returns `'clean'` if orphaned state was cleaned up before install,
8
- * `'fresh'` if no prior state existed.
4
+ * Exits the process with code 1 if the `claude` CLI is missing or too old to
5
+ * load plugins from `CLAUDE_CODE_PLUGIN_DIRS`.
9
6
  */
10
- export declare function ensureCleanPluginState(): 'clean' | 'fresh';
7
+ export declare function assertClaudeSupportsPluginDirs(): void;
11
8
  /**
12
- * Best-effort cleanup. Called on process shutdown. Never throws.
9
+ * Remove the user-scope plugin / marketplace registered by older tsunagi
10
+ * versions. Runs once; a marker file in the state dir records completion.
11
+ * Never throws and never blocks startup on failure.
13
12
  */
14
- export declare function cleanupPluginState(): void;
13
+ export declare function removeLegacyPluginInstall(): void;