@tanweilabs/daiti 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 TanweiLabs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,92 @@
1
+ # 代体
2
+
3
+ 在你的电脑上运行,通过网页、微信或飞书使用的个人助理。基于 DeepSeek Harness 和 DSH-IM,通过独立插件提供单主会话、助理设置和首次使用流程。
4
+
5
+ **0.0.1 为开发者预览版。** 本包不包含个人记忆、历史会话、密钥或 IM 登录数据。
6
+
7
+ ## 安装
8
+
9
+ 需要 Node.js `^22.19.0` 或 `>=24.0.0`:
10
+
11
+ ```sh
12
+ npm install -g @tanweilabs/daiti@0.0.1
13
+ daiti start
14
+ ```
15
+
16
+ 启动后自动打开本机网页。可用 `daiti status`、`daiti open` 和 `daiti stop` 管理服务。
17
+
18
+ ## 从源码体验
19
+
20
+ 安装 Node.js `^22.19.0` 或 `>=24.0.0`,然后在仓库根目录运行:
21
+
22
+ ```sh
23
+ npm ci
24
+ npm run build
25
+ node bin/daiti.mjs start
26
+ ```
27
+
28
+ 首次启动需要联网安装固定版本的 IM 插件和本地插件包,成功后自动打开已认证的本机网页。不需要克隆或编译 `upstream/deepseek-harness`,也不需要全局安装 pnpm。
29
+
30
+ 首次使用时设置助理名称、确认工作文件夹,然后配置模型。可选择“先看看界面”;此时不会配置模型,实际聊天前仍需在设置里填写有效凭据。模型请求由用户自己的提供商账户承担。
31
+
32
+ ```sh
33
+ node bin/daiti.mjs status
34
+ node bin/daiti.mjs open
35
+ node bin/daiti.mjs stop
36
+ node bin/daiti.mjs start --port 3086 --no-open
37
+ ```
38
+
39
+ 默认端口 3086,只监听本机。关闭浏览器后后台服务继续运行;停止服务或电脑休眠、关机时不能保证 IM 响应。停止前请等待正在执行的任务完成。当前没有开机自启、系统服务或自动升级。
40
+
41
+ ## 数据在哪里
42
+
43
+ - 应用数据:默认 `~/.daiti/`,包含助理设置和私有服务状态;`service.log` 可能包含本机认证链接,请勿公开分享。
44
+ - Harness 运行数据:`~/.daiti/runtime/`,包含聊天历史、凭据、插件和 IM 状态,与已有 `~/.dsh` 分开。
45
+ - 工作文件夹:首次使用时选择,默认建议 `~/Daiti`;用于处理文件和保存成果。只创建缺失的 `AGENTS.md` 和 `library/MEMORY.md` 模板,不覆盖已有内容。
46
+
47
+ 运行目录与工作文件夹不是同一个概念。备份需包含两者。当前不提供已有主会话的工作目录迁移;不要手工改写会话日志或 IM 绑定文件。工作目录不是操作系统权限沙箱,工具权限仍由 Harness 的权限设置控制。
48
+
49
+ 可以通过 `DAITI_HOME` 启动独立测试实例:
50
+
51
+ ```sh
52
+ DAITI_HOME="$HOME/.daiti-preview" node bin/daiti.mjs start --port 3087
53
+ DAITI_HOME="$HOME/.daiti-preview" node bin/daiti.mjs stop
54
+ ```
55
+
56
+ PowerShell 对应设置 `$env:DAITI_HOME = "$HOME\.daiti-preview"`。测试时选择新的工作文件夹;不要在两个运行实例中连接同一个 IM 机器人。
57
+
58
+ ## 连接微信和飞书
59
+
60
+ 在“设置 → IM机器人”里连接。连接完成后,在本人和机器人的私聊中发送页面生成的 `/session …` 命令,收到绑定成功回复后再开始聊天。不同渠道使用同一条命令,才能共用网页主会话;仅使用同一个工作文件夹不等于共用上下文。
61
+
62
+ 现有 DSH-IM 没有公开的扫码激活前绑定接口,因此首版不是自动绑定。如果目标会话工作目录不同,绑定可能重置该机器人的其他会话关联(不删除旧历史);不要将个人主会话绑定到多人群聊。
63
+
64
+ ## 开发和本地打包
65
+
66
+ ```sh
67
+ npm test
68
+ npm run build
69
+ npm run pack:local
70
+ ```
71
+
72
+ 构建将三个插件打包到 `artifacts/`,生成文件名和 SHA-256 清单。`pack:local` 在 `.local/releases/` 生成 CLI 安装包;包含运行入口、插件包和文档,不包含仓库工作目录、上游源码、个人配置或测试数据。
73
+
74
+ 未发布的本地安装包可用 `npm install -g /path/to/tanweilabs-daiti-0.0.1.tgz` 安装,随后使用 `daiti start`。安装仍需联网获取声明的运行依赖。
75
+
76
+ - `bin/`、`lib/`:CLI 和后台进程管理。
77
+ - `packages/assistant-ui/`:界面、首次设置和助理配置。
78
+ - `packages/assistant-preferences/`:仅注入助理名称,记忆由工作区规则引导读取。
79
+ - `packages/loading/`:刷新时的加载状态。
80
+ - `upstream/deepseek-harness/`:固定版本的源码参考子模块,日常安装不依赖它;现有 `scripts/prepare-upstream.sh` / `scripts/start-web.sh` 仅供上游源码实验。
81
+
82
+ 修改后必须验证新用户初始化、刷新/重启恢复同一会话、设置保存与服务停止。没有用户授权,不将个人会话或凭据加入测试,也不自动发布仓库或包。
83
+
84
+ ## 版本和限制
85
+
86
+ 当前固定 Harness `0.1.6-alpha.2`、DSH-IM `4.21.0`。部分界面调整依赖这个版本的插槽、DOM 结构和中文标签;上游升级必须先验收,不能承诺跨版本无改动兼容。
87
+
88
+ 完整验证记录见 [docs/validation.md](docs/validation.md)。Windows/Linux 尚需真实机器安装验证;本轮不宣称模型响应或微信/飞书真实收发已端到端通过。升级机制、异常断电恢复和工作目录迁移仍待完善。
89
+
90
+ ## 许可证
91
+
92
+ 代体代码采用 MIT;依赖保留各自许可证。见 [LICENSE](LICENSE) 和 [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md)。本项目不是 DeepSeek 官方客户端。
@@ -0,0 +1,9 @@
1
+ # Third-party software
2
+
3
+ 代体 is an independent TanweiLabs project, not an official DeepSeek client.
4
+
5
+ - [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness), 0.1.6-alpha.2, MIT, Copyright (c) 2026 DeepSeek. Its original code and license remain in the pinned upstream submodule and installed npm dependencies. Its own third-party notices cover its additional dependencies.
6
+ - [DSH-IM](https://github.com/xmanrui/dsh-im), 4.21.0, MIT. Installed as a separate, unmodified dependency with its original license.
7
+ - pnpm, 11.19.0, MIT; esbuild, 0.25.12, MIT (build-time).
8
+
9
+ The executable package declares the runtime dependencies; setup installs the three local plugin archives and pinned DSH-IM into a separate data directory. No personal memory, session history, model credential or IM login data belongs in this repository or its release artifacts.
@@ -0,0 +1,22 @@
1
+ {
2
+ "plugins": [
3
+ {
4
+ "name": "@tanweilabs/daiti-ui",
5
+ "version": "0.0.1",
6
+ "file": "tanweilabs-daiti-ui-0.0.1.tgz",
7
+ "sha256": "b6354f22f09c1637632210524f40035a7325e10228719647c2ea4c310c43c456"
8
+ },
9
+ {
10
+ "name": "@tanweilabs/daiti-preferences",
11
+ "version": "0.0.1",
12
+ "file": "tanweilabs-daiti-preferences-0.0.1.tgz",
13
+ "sha256": "c458a5895cafe8463433483566793460c9183354b4e3b80a67994972a1f5cf74"
14
+ },
15
+ {
16
+ "name": "@tanweilabs/daiti-loading",
17
+ "version": "0.0.1",
18
+ "file": "tanweilabs-daiti-loading-0.0.1.tgz",
19
+ "sha256": "0914c16b3bd27388004924483263858a7df3ec7c3e7e73b3cae111f48fa957de"
20
+ }
21
+ ]
22
+ }
package/bin/daiti.mjs ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { main } from '../lib/cli.mjs';
3
+ main().catch(error => { console.error(`daiti: ${error.message}`); process.exitCode = 1; });
@@ -0,0 +1,24 @@
1
+ # 本地验收记录
2
+
3
+ 2026-09-21,macOS Apple Silicon,Node.js 22.22.3。以下为真实执行结果,不代表其他系统或真实模型/IM已验收。
4
+
5
+ ## 已通过
6
+
7
+ - `npm test`:9项,覆盖设置持久化、模板不覆盖、非法/过期输入、RPC格式及卸载、独立数据根、私有服务状态、所属服务停止、启动失败清理,以及认证重定向就绪检测。
8
+ - `npm run build`:三个插件包及SHA-256清单生成成功。
9
+ - 全新独立 `DAITI_HOME`:官方CLI安装固定版本IM及三个插件,监听指定端口,正确进入代体首次设置。
10
+ - 浏览器填写名称和独立工作文件夹,选择“先看看界面”,生成通用规则/记忆模板,进入唯一主会话。
11
+ - 重复 `start` 复用同一个服务;`stop` 后端口释放;重新 `start` 后完整助理设置及主会话ID保持不变,网页恢复原助理而不重跑onboarding。
12
+ - 本地 `.tgz` 安装到另一npm前缀,并在不含全局pnpm的PATH中启动;确认实际使用安装前缀中的依赖,没有依赖上游源码目录。
13
+ - 安装包白名单检查:无运行数据、个人资料、会话、密钥、上游源码或开发机绝对路径。
14
+
15
+ ## 未验证或未实现
16
+
17
+ - 没有配置真实模型密钥或发送模型请求;界面/宿主就绪不代表模型请求成功。
18
+ - 没有扫码或执行真实微信/飞书绑定及收发;多渠道仍需本人发送绑定命令。
19
+ - Windows/Linux尚未在真实机器上验证。
20
+ - 未实现开机自启、自动升级、自动跨渠道绑定和工作区迁移。
21
+ - 强制杀死监督进程或断电可能遗留安全锁;当前保守拒绝启动,需确认无残留服务后人工处理。正常停止会清理锁,保留资料。
22
+ - UI适配依赖当前Harness版本的插槽及部分DOM结构,升级前需重复浏览器验收。
23
+
24
+ 现有个人代体与历史测试实例不属于本次验收数据,未被迁移、重置或停止。公开发布仓库或安装包仍需单独确认。
package/lib/cli.mjs ADDED
@@ -0,0 +1,118 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import { spawn } from 'node:child_process';
3
+ import { createRequire } from 'node:module';
4
+ import { homedir } from 'node:os';
5
+ import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { randomBytes } from 'node:crypto';
8
+ export const root = fileURLToPath(new URL('../', import.meta.url));
9
+ export const pause = ms => new Promise(resolve => setTimeout(resolve, ms));
10
+ export function paths(env = process.env) {
11
+ const home = path.resolve(env.DAITI_HOME || path.join(homedir(), '.daiti'));
12
+ return { home, runtime: path.join(home, 'runtime'), state: path.join(home, 'service.json'), log: path.join(home, 'service.log') };
13
+ }
14
+ export async function readJson(file) { try { return JSON.parse(await fs.readFile(file, 'utf8')); } catch (e) { if (e.code === 'ENOENT') return null; throw e; } }
15
+ export async function atomicJson(file, value) {
16
+ const temp = `${file}.${randomBytes(8).toString('hex')}.tmp`;
17
+ await fs.writeFile(temp, JSON.stringify(value, null, 2) + '\n', { mode: 0o600 });
18
+ await fs.rename(temp, file);
19
+ }
20
+ export function parseArgs(args) {
21
+ const command = args[0] || 'help'; let port = 3086, open = true;
22
+ for (let i = 1; i < args.length; i++) {
23
+ if (args[i] === '--no-open') open = false;
24
+ else if (args[i] === '--port') { const value = args[++i]; if (!/^\d+$/.test(value || '') || +value < 1024 || +value > 65535) throw new Error('--port must be between 1024 and 65535'); port = +value; }
25
+ else throw new Error(`Unknown option: ${args[i]}`);
26
+ }
27
+ if (!['setup','start','stop','status','open','help','--help','-h'].includes(command)) throw new Error(`Unknown command: ${command}`);
28
+ return { command, port, open };
29
+ }
30
+ export function runtimeBin() {
31
+ const require = createRequire(path.join(root, 'package.json'));
32
+ const manifest = require.resolve('@deepseek-ai/dsh/package.json');
33
+ const pkg = require(manifest);
34
+ if (pkg.version !== '0.1.6-alpha.2') throw new Error(`Unsupported Harness version ${pkg.version}; run npm ci`);
35
+ return path.resolve(path.dirname(manifest), pkg.bin.dsh);
36
+ }
37
+ export function runtimePath(env = process.env) {
38
+ const require = createRequire(path.join(root, 'package.json'));
39
+ const pnpmDirectory = path.dirname(require.resolve('pnpm'));
40
+ // npm may hoist dependencies outside this scoped package's own directory.
41
+ return [path.resolve(pnpmDirectory, '..', '.bin'), path.join(root, 'node_modules/.bin'), env.PATH || ''].join(path.delimiter);
42
+ }
43
+ function run(args, p) {
44
+ return new Promise((resolve, reject) => {
45
+ const child = spawn(process.execPath, [runtimeBin(), ...args], { env: { ...process.env, DSH_HOME: p.runtime, PATH: runtimePath() }, cwd: p.home, stdio: 'inherit' });
46
+ child.once('error', reject); child.once('exit', code => code === 0 ? resolve() : reject(new Error(`Harness setup exited ${code}`)));
47
+ });
48
+ }
49
+ export async function control(state, action = 'status') {
50
+ if (!state) return null;
51
+ try {
52
+ const response = await fetch(`http://127.0.0.1:${state.controlPort}/${action}`, { method: 'POST', headers: { Authorization: `Bearer ${state.secret}` }, signal: AbortSignal.timeout(1500) });
53
+ if (!response.ok) return null;
54
+ const result = await response.json(); return result.instance === state.instance ? result : null;
55
+ } catch { return null; }
56
+ }
57
+ export async function setup(p) {
58
+ await fs.mkdir(p.home, { recursive: true, mode: 0o700 });
59
+ if (await control(await readJson(p.state))) throw new Error('Stop this Daiti service before setup.');
60
+ const marker = path.join(p.home, 'installation.json');
61
+ const manifest = await readJson(path.join(root, 'artifacts/manifest.json'));
62
+ if (!manifest?.plugins?.length) throw new Error('Plugin artifacts missing. Run npm run build first.');
63
+ const signature = JSON.stringify(manifest);
64
+ if ((await readJson(marker))?.signature === signature) return;
65
+ // Official plugin command initializes the shipped web profile in this isolated home.
66
+ const packageCache = path.join(p.home, 'plugin-packages');
67
+ await fs.mkdir(packageCache, { recursive: true, mode: 0o700 });
68
+ // Persist each version beside the profile: upgrades must not remove the
69
+ // tarballs referenced by its existing pnpm lockfile.
70
+ const packages = await Promise.all(manifest.plugins.map(async plugin => {
71
+ const destination = path.join(packageCache, plugin.file);
72
+ await fs.copyFile(path.resolve(root, 'artifacts', plugin.file), destination);
73
+ return destination;
74
+ }));
75
+ await run(['plugin', '--profile', 'web', 'add', '@xmanrui/dsh-im@4.21.0', ...packages], p);
76
+ const profile = path.join(p.runtime, 'profiles/web');
77
+ const patch = path.join(profile, 'cordis.patch.yml');
78
+ try { await fs.copyFile(patch, `${patch}.before-daiti-${Date.now()}`); } catch (e) { if (e.code !== 'ENOENT') throw e; }
79
+ // Our overlay is separate from the profile's user-owned patch.
80
+ await atomicJson(path.join(p.home, 'daiti.patch.json'), [
81
+ { id: 'ui-sidebar', disabled: true },
82
+ { id: 'daiti-onboarding-preferences', disabled: false, config: { settingsPath: path.join(p.home, 'settings.json') } },
83
+ { id: 'daiti-ui', disabled: false, config: { settingsPath: path.join(p.home, 'settings.json'), defaultWorkspacePath: path.join(homedir(), 'Daiti') } },
84
+ ]);
85
+ await atomicJson(marker, { signature, installedAt: new Date().toISOString() });
86
+ }
87
+ export async function openUrl(url) {
88
+ const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'rundll32' : 'xdg-open';
89
+ const args = process.platform === 'win32' ? ['url.dll,FileProtocolHandler', url] : [url];
90
+ await new Promise((resolve, reject) => { const child = spawn(cmd, args, { stdio: 'ignore' }); child.on('error', reject); child.on('exit', code => code === 0 ? resolve() : reject(new Error('Browser could not be opened.'))); });
91
+ }
92
+ export async function main(argv = process.argv.slice(2)) {
93
+ const options = parseArgs(argv); const p = paths();
94
+ if (['help','--help','-h'].includes(options.command)) { console.log('daiti setup | start [--port 3086] [--no-open] | stop | status | open\nDAITI_HOME selects isolated application data (default ~/.daiti).'); return; }
95
+ if (options.command === 'setup') { await setup(p); console.log(`Daiti prepared: ${p.home}`); return; }
96
+ let current = await control(await readJson(p.state));
97
+ if (options.command === 'status') { console.log(current ? `Daiti ${current.ready ? 'running' : 'starting'}: http://127.0.0.1:${current.port}/` : 'Daiti stopped'); return; }
98
+ if (options.command === 'stop') {
99
+ const state = await readJson(p.state);
100
+ if (!current) { console.log('Daiti stopped (no verified owned service).'); return; }
101
+ await control(state, 'stop');
102
+ for (let i = 0; i < 60; i++) { if (!(await control(state))) { console.log('Daiti stopped.'); return; } await pause(250); }
103
+ throw new Error('Service is still stopping; no unrelated process was signalled.');
104
+ }
105
+ if (options.command === 'open' && !current?.ready) throw new Error('Daiti is not ready. Run daiti start first.');
106
+ if (options.command === 'start' && !current) {
107
+ await setup(p);
108
+ const log = await fs.open(p.log, 'a', 0o600); await log.chmod(0o600);
109
+ const child = spawn(process.execPath, [path.join(root, 'lib/supervisor.mjs'), String(options.port)], { detached: true, stdio: ['ignore', log.fd, log.fd], env: { ...process.env, DAITI_HOME: p.home } });
110
+ await new Promise((resolve, reject) => { child.once('spawn', resolve); child.once('error', reject); });
111
+ child.unref(); await log.close();
112
+ for (let i = 0; i < 240; i++) { current = await control(await readJson(p.state)); if (current?.ready) break; if (child.exitCode !== null) throw new Error(`Daiti exited during startup; inspect ${p.log}`); await pause(250); }
113
+ if (!current?.ready) throw new Error(`Daiti did not become ready; inspect ${p.log}`);
114
+ }
115
+ if (!current?.ready) throw new Error('Daiti is still starting.');
116
+ if (options.open || options.command === 'open') await openUrl(current.url);
117
+ console.log(`Daiti running: http://127.0.0.1:${current.port}/ (use daiti open for authenticated access)`);
118
+ }
@@ -0,0 +1,91 @@
1
+ import { createServer } from 'node:http';
2
+ import { spawn } from 'node:child_process';
3
+ import { promises as fs } from 'node:fs';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { randomBytes, timingSafeEqual } from 'node:crypto';
7
+ import { paths, atomicJson, runtimeBin, runtimePath, root } from './cli.mjs';
8
+
9
+
10
+ // Browser launch authentication exchanges a URL token for a cookie.
11
+ // Node fetch has no cookie jar; retain cookies only across same-origin redirects.
12
+ export async function verifyLaunchUrl(value) {
13
+ const origin = new URL(value).origin;
14
+ let current = value, cookie = '';
15
+ for (let hop = 0; hop < 5; hop++) {
16
+ const response = await fetch(current, {
17
+ redirect: 'manual', signal: AbortSignal.timeout(5000),
18
+ headers: cookie ? { Cookie: cookie } : {},
19
+ });
20
+ const cookies = response.headers.getSetCookie();
21
+ if (cookies.length) cookie = cookies.map(item => item.split(';')[0]).join('; ');
22
+ if ([301, 302, 303, 307, 308].includes(response.status)) {
23
+ const location = response.headers.get('location');
24
+ if (!location) return false;
25
+ const next = new URL(location, current);
26
+ if (next.origin !== origin) return false;
27
+ await response.body?.cancel();
28
+ current = next.href;
29
+ continue;
30
+ }
31
+ const html = await response.text();
32
+ return response.ok && html.includes('__DSH_BOOT__');
33
+ }
34
+ return false;
35
+ }
36
+
37
+ // Only this parent signals its own ChildProcess. CLI clients never signal a PID
38
+ // obtained from disk, so stale state / PID reuse cannot terminate another app.
39
+ export async function supervise({ p = paths(), port, command = process.execPath, args, timeoutMs = 60000 }) {
40
+ const lockPath = path.join(p.home, 'service.lock');
41
+ let lock;
42
+ try { lock = await fs.open(lockPath, 'wx', 0o600); }
43
+ catch (e) { if (e.code === 'EEXIST') throw new Error(`Service lock exists: ${lockPath}. If a previous supervisor crashed, verify no Daiti process remains before removing this lock.`); throw e; }
44
+ const instance = randomBytes(16).toString('hex'), secret = randomBytes(32).toString('hex');
45
+ let child, ready = false, url, stopping = false, exited = false;
46
+ const cleanup = async () => { await lock.close(); await fs.rm(lockPath, { force: true }); await fs.rm(p.state, { force: true }); };
47
+ const server = createServer((req, res) => {
48
+ const actual = Buffer.from(req.headers.authorization || ''); const expected = Buffer.from(`Bearer ${secret}`);
49
+ if (actual.length !== expected.length || !timingSafeEqual(actual, expected)) { res.writeHead(403).end(); return; }
50
+ res.setHeader('Content-Type', 'application/json');
51
+ if (req.method !== 'POST' || !['/status', '/stop'].includes(req.url)) { res.writeHead(404).end('{}'); return; }
52
+ res.end(JSON.stringify({ instance, ready, port, ...(ready ? { url } : {}) }));
53
+ if (req.url === '/stop') stop();
54
+ });
55
+ function stop() {
56
+ if (stopping) return; stopping = true; ready = false;
57
+ if (child && !exited) {
58
+ child.kill('SIGTERM');
59
+ const force = setTimeout(() => { if (!exited) child.kill('SIGKILL'); }, 8000); force.unref();
60
+ } else server.close();
61
+ }
62
+ try {
63
+ await new Promise((resolve, reject) => { server.once('error', reject); server.listen(0, '127.0.0.1', resolve); });
64
+ await atomicJson(p.state, { instance, secret, controlPort: server.address().port, pid: process.pid });
65
+ child = spawn(command, args || [runtimeBin(), '--profile', 'web', '--patch', path.join(p.home, 'daiti.patch.json'), '--host', '127.0.0.1', '--port', String(port), '--no-open'], { env: { ...process.env, DSH_HOME: p.runtime, PATH: runtimePath() }, cwd: p.home, stdio: ['ignore', 'pipe', 'pipe'] });
66
+ let tail = '';
67
+ child.stdout.on('data', chunk => {
68
+ process.stdout.write(chunk);
69
+ tail = (tail + chunk.toString()).slice(-16384);
70
+ const match = tail.match(/dsh web: (http:\/\/127\.0\.0\.1:\d+[^\s]*)/);
71
+ if (match && !url) {
72
+ const candidate = new URL(match[1]);
73
+ if (+candidate.port !== port) return;
74
+ url = candidate.href;
75
+ verifyLaunchUrl(url).then(valid => {
76
+ if (valid && !stopping && !exited) ready = true;
77
+ }).catch(() => {});
78
+ }
79
+ });
80
+ child.stderr.on('data', chunk => process.stderr.write(chunk));
81
+ const timeout = setTimeout(() => { if (!ready) { console.error('Daiti startup timed out.'); stop(); } }, timeoutMs); timeout.unref();
82
+ const terminate = () => stop(); process.on('SIGTERM', terminate); process.on('SIGINT', terminate);
83
+ await new Promise(resolve => { child.once('error', error => { console.error(error.message); resolve(); }); child.once('exit', resolve); });
84
+ exited = true; ready = false; clearTimeout(timeout);
85
+ process.off('SIGTERM', terminate); process.off('SIGINT', terminate);
86
+ await new Promise(resolve => server.close(resolve));
87
+ } finally { server.close(); await cleanup(); }
88
+ }
89
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
90
+ supervise({ port: Number(process.argv[2]) }).catch(error => { console.error(error.message); process.exitCode = 1; });
91
+ }