@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.
- package/.next/standalone/apps/web/.next/BUILD_ID +1 -1
- package/.next/standalone/apps/web/.next/build-manifest.json +3 -3
- package/.next/standalone/apps/web/.next/prerender-manifest.json +3 -3
- package/.next/standalone/apps/web/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/apps/web/.next/server/app/_global-error.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/apps/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/apps/web/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/apps/web/.next/server/app/_not-found.rsc +14 -14
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_index.segment.rsc +9 -9
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
- package/.next/standalone/apps/web/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/apps/web/.next/server/app/index.html +1 -1
- package/.next/standalone/apps/web/.next/server/app/index.rsc +16 -16
- package/.next/standalone/apps/web/.next/server/app/index.segments/__PAGE__.segment.rsc +4 -4
- package/.next/standalone/apps/web/.next/server/app/index.segments/_full.segment.rsc +16 -16
- package/.next/standalone/apps/web/.next/server/app/index.segments/_head.segment.rsc +4 -4
- package/.next/standalone/apps/web/.next/server/app/index.segments/_index.segment.rsc +9 -9
- package/.next/standalone/apps/web/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/apps/web/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/apps/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/apps/web/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/apps/web/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/apps/web/.next/server/app/settings.html +1 -1
- package/.next/standalone/apps/web/.next/server/app/settings.rsc +16 -16
- package/.next/standalone/apps/web/.next/server/app/settings.segments/_full.segment.rsc +16 -16
- package/.next/standalone/apps/web/.next/server/app/settings.segments/_head.segment.rsc +4 -4
- package/.next/standalone/apps/web/.next/server/app/settings.segments/_index.segment.rsc +9 -9
- package/.next/standalone/apps/web/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/apps/web/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +4 -4
- package/.next/standalone/apps/web/.next/server/app/settings.segments/settings.segment.rsc +3 -3
- package/.next/standalone/apps/web/.next/server/app/tasks/[id]/page.js.nft.json +1 -1
- package/.next/standalone/apps/web/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0qjafjt._.js +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{_05_5y28._.js → _053072t._.js} +2 -2
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{_0ncpk6b._.js → _07i72iz._.js} +2 -2
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_09eooml._.js +7 -0
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_0bru-z.._.js +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_0i8ojdw._.js +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_0mtw02l._.js +3 -0
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_12aa-fs._.js +7 -0
- package/.next/standalone/apps/web/.next/server/chunks/ssr/apps_web_src_app_settings_page_tsx_0i6itlb._.js +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_09nusj_._.js → node_modules_0-g_j5j._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0axuayq._.js → node_modules_04pfam-._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_10v5lct._.js → node_modules_0_1re~9._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0h_hear._.js → node_modules_0_huoqd._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0c8sp_9._.js → node_modules_0a9pqes._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_09e0q95._.js → node_modules_0bfjoue._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0s3mkxs._.js → node_modules_0e.hm5q._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0la1a4x._.js → node_modules_0krwgny._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0zv4e8x._.js → node_modules_0l23gkf._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0w8sswp._.js → node_modules_0mpkxmv._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_06gyfuv._.js → node_modules_0oip5a1._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_117mm7f._.js → node_modules_0q_xfyl._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0nji2bx._.js → node_modules_0tod6y8._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0o9t~hh._.js → node_modules_0xp9nhx._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0_876x4._.js → node_modules_0zstt1y._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/{node_modules_0mt.8za._.js → node_modules_0~2ubpz._.js} +1 -1
- package/.next/standalone/apps/web/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/apps/web/.next/server/pages/404.html +1 -1
- package/.next/standalone/apps/web/.next/server/pages/500.html +1 -1
- package/.next/standalone/apps/web/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/apps/web/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/apps/web/.next/static/chunks/0.qz1zt1c.y.n.js +5 -0
- package/.next/standalone/apps/web/.next/static/chunks/0ajw1mjy3owa4.js +1 -0
- package/.next/standalone/apps/web/.next/static/chunks/0fe4v5dh3whem.js +5 -0
- package/.next/standalone/apps/web/.next/static/chunks/{0vxoeg5sfua~3.js → 0gzt~9euv6u2y.js} +2 -2
- package/.next/standalone/apps/web/.next/static/chunks/0jcorj3-aambm.js +1 -0
- package/.next/standalone/apps/web/.next/static/chunks/{0p3_2uc39mlse.js → 0s7g4uwmm49t..js} +2 -2
- package/.next/standalone/apps/web/.next/static/chunks/0wnto9g4i0s3h.css +4 -0
- package/.next/standalone/apps/web/.next/static/chunks/{0jc3jwed_8vre.js → 0~_c.mwboavrv.js} +2 -2
- package/dist/app.d.ts +1 -0
- package/dist/app.js +375 -0
- package/dist/cli.js +152 -290
- package/dist/plugin-lifecycle.d.ts +9 -10
- package/dist/plugin-lifecycle.js +77 -65
- package/dist/server/index.js +18 -3
- package/dist/server/lib/auto-update.d.ts +18 -0
- package/dist/server/lib/auto-update.js +208 -0
- package/dist/server/lib/restart.d.ts +8 -0
- package/dist/server/lib/restart.js +13 -0
- package/dist/server/lib/update-check.js +14 -4
- package/dist/server/pty-manager.d.ts +2 -0
- package/dist/server/pty-manager.js +31 -2
- package/dist/server/routes/version.js +16 -0
- package/dist/single-instance-lock.js +3 -17
- package/dist/update-lifecycle.d.ts +19 -0
- package/dist/update-lifecycle.js +93 -0
- package/dist/versions.d.ts +60 -0
- package/dist/versions.js +95 -0
- package/dist/with-plugin.js +6 -8
- package/npm-shrinkwrap.json +8 -8
- package/package.json +1 -1
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_0hqrmyz._.js +0 -3
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_0m.hvms._.js +0 -7
- package/.next/standalone/apps/web/.next/server/chunks/ssr/_0mljs_s._.js +0 -7
- package/.next/standalone/apps/web/.next/static/chunks/08-d-ag2euha9.js +0 -5
- package/.next/standalone/apps/web/.next/static/chunks/0c.dwezn5qjvs.css +0 -4
- package/.next/standalone/apps/web/.next/static/chunks/0gpkw.1e6kcpn.js +0 -1
- package/.next/standalone/apps/web/.next/static/chunks/0v6gxm~ldhmma.js +0 -1
- package/.next/standalone/apps/web/.next/static/chunks/13y6z-vus9yvr.js +0 -5
- /package/.next/standalone/apps/web/.next/static/{EOnxRuFun6cfXk-y9KPq5 → OXWK1FxsZh6lcEQawkL6h}/_buildManifest.js +0 -0
- /package/.next/standalone/apps/web/.next/static/{EOnxRuFun6cfXk-y9KPq5 → OXWK1FxsZh6lcEQawkL6h}/_clientMiddlewareManifest.js +0 -0
- /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
|
|
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
|
-
*
|
|
9
|
+
* Entry: the `tsunagi` bin of @minimalcorp/tsunagi.
|
|
12
10
|
*
|
|
13
|
-
*
|
|
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
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
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
|
-
|
|
89
|
-
|
|
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
|
-
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
144
|
-
|
|
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
|
-
|
|
163
|
-
|
|
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
|
-
|
|
179
|
-
//
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
188
|
-
|
|
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
|
-
|
|
205
|
-
return typeof pkg.version === 'string' ? pkg.version : undefined;
|
|
131
|
+
current.kill('SIGTERM');
|
|
206
132
|
}
|
|
207
133
|
catch {
|
|
208
|
-
|
|
134
|
+
// 既に終了している
|
|
209
135
|
}
|
|
210
136
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
3
|
-
*
|
|
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
|
|
7
|
+
export declare function assertClaudeSupportsPluginDirs(): void;
|
|
11
8
|
/**
|
|
12
|
-
*
|
|
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
|
|
13
|
+
export declare function removeLegacyPluginInstall(): void;
|