@covro/covro 0.1.1-dev.7 → 0.1.1-dev.8
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/README.md +11 -8
- package/README.zh-CN.md +11 -8
- package/dist/cli/background.js +103 -0
- package/dist/cli/background.js.map +7 -0
- package/dist/cli/daemon.js +6 -1
- package/dist/cli/daemon.js.map +2 -2
- package/dist/cli/doctor.js +92 -0
- package/dist/cli/doctor.js.map +7 -0
- package/dist/cli/launch-environment.js +85 -0
- package/dist/cli/launch-environment.js.map +7 -0
- package/dist/cli/launchd.js +172 -0
- package/dist/cli/launchd.js.map +7 -0
- package/dist/cli/main.js +7 -35
- package/dist/cli/main.js.map +2 -2
- package/dist/cli/service.js +23 -49
- package/dist/cli/service.js.map +2 -2
- package/dist/update-worker.cjs +419 -168
- package/dist/updates/worker.js +7 -61
- package/dist/updates/worker.js.map +2 -2
- package/dist/web/assets/{index-D1Fx5UOj.js → index-CAtxws9I.js} +1 -1
- package/dist/web/index.html +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -69,14 +69,15 @@ covro
|
|
|
69
69
|
|
|
70
70
|
`covro` starts in the background and prints the local address. Open **<http://127.0.0.1:8787>** to get started. Close the terminal and keep working in your browser.
|
|
71
71
|
|
|
72
|
-
| Command | Purpose
|
|
73
|
-
| ------------------------- |
|
|
74
|
-
| `covro status` | Show the running instance and its address
|
|
75
|
-
| `covro
|
|
76
|
-
| `covro
|
|
77
|
-
| `covro
|
|
78
|
-
| `covro
|
|
79
|
-
| `covro service
|
|
72
|
+
| Command | Purpose |
|
|
73
|
+
| ------------------------- | -------------------------------------------------------------------------------------- |
|
|
74
|
+
| `covro status` | Show the running instance and its address |
|
|
75
|
+
| `covro doctor` | Diagnose the running instance's session and GitHub credentials without printing tokens |
|
|
76
|
+
| `covro logs` | Read recent service logs |
|
|
77
|
+
| `covro stop` | Stop the service |
|
|
78
|
+
| `covro run` | Run in the foreground |
|
|
79
|
+
| `covro service install` | Opt in to login auto-start on macOS or Linux |
|
|
80
|
+
| `covro service uninstall` | Remove login auto-start |
|
|
80
81
|
|
|
81
82
|
The npm package is `@covro/covro`; the command is `covro`. Install the development channel with `npm install -g @covro/covro@dev`.
|
|
82
83
|
|
|
@@ -84,6 +85,8 @@ The npm CLI uses `~/.covro/config.json` and a fixed data directory beside it. Re
|
|
|
84
85
|
|
|
85
86
|
Open **Service configuration → Advanced settings** to edit size limits in MiB (1 MiB = 1024 × 1024 bytes). Decimal values must correspond to whole bytes; existing byte values are preserved exactly. JSON files and the API continue to use bytes. Saved changes take effect after restarting the service.
|
|
86
87
|
|
|
88
|
+
On macOS, background startup uses the logged-in user's GUI launchd domain so commands can access existing Keychain credentials when their execution permissions allow it. This does not enable login auto-start; use `covro service install` for that. Without a desktop session, headless startup remains available with a Keychain access notice. After installing this fix, restart existing instances with `covro stop` and `covro start`. See [startup and credential diagnostics](docs/distribution.md#macos-sessions-and-credentials).
|
|
89
|
+
|
|
87
90
|
<a id="run-from-source"></a>
|
|
88
91
|
|
|
89
92
|
### 🛠️ Run from source
|
package/README.zh-CN.md
CHANGED
|
@@ -69,14 +69,15 @@ covro
|
|
|
69
69
|
|
|
70
70
|
`covro` 默认在后台启动,并输出本机访问地址。打开 **<http://127.0.0.1:8787>** 即可开始使用。关掉终端后,仍可继续通过浏览器工作。
|
|
71
71
|
|
|
72
|
-
| 命令 | 用途
|
|
73
|
-
| ------------------------- |
|
|
74
|
-
| `covro status` | 查看运行实例与访问地址
|
|
75
|
-
| `covro
|
|
76
|
-
| `covro
|
|
77
|
-
| `covro
|
|
78
|
-
| `covro
|
|
79
|
-
| `covro service
|
|
72
|
+
| 命令 | 用途 |
|
|
73
|
+
| ------------------------- | -------------------------------------------- |
|
|
74
|
+
| `covro status` | 查看运行实例与访问地址 |
|
|
75
|
+
| `covro doctor` | 诊断运行实例的会话和 GitHub 凭证,不输出令牌 |
|
|
76
|
+
| `covro logs` | 查看最近的服务日志 |
|
|
77
|
+
| `covro stop` | 停止服务 |
|
|
78
|
+
| `covro run` | 在前台运行 |
|
|
79
|
+
| `covro service install` | 在 macOS 或 Linux 上主动启用登录自启 |
|
|
80
|
+
| `covro service uninstall` | 移除登录自启 |
|
|
80
81
|
|
|
81
82
|
npm 包名为 `@covro/covro`,命令为 `covro`。开发渠道使用 `npm install -g @covro/covro@dev` 安装。
|
|
82
83
|
|
|
@@ -84,6 +85,8 @@ npm CLI 默认使用 `~/.covro/config.json`,数据目录固定在配置文件
|
|
|
84
85
|
|
|
85
86
|
在**服务配置 → 高级设置**中,以 MiB 编辑大小限制(1 MiB = 1024 × 1024 字节)。小数必须精确对应整数字节;已有字节值会原样保留。JSON 文件和 API 继续使用字节,保存后需重启服务生效。
|
|
86
87
|
|
|
88
|
+
macOS 后台启动使用已登录用户的 GUI launchd 域,使权限允许的命令能够访问已有钥匙串凭证;这不会开启登录自启,登录自启仍需显式运行 `covro service install`。没有桌面会话时仍支持 headless 启动,并提示钥匙串访问限制。安装此修复后,已有实例需要执行 `covro stop` 和 `covro start` 重启。详见[启动与凭证诊断](docs/distribution.md#macos-sessions-and-credentials)。
|
|
89
|
+
|
|
87
90
|
<a id="从源码运行"></a>
|
|
88
91
|
|
|
89
92
|
### 🛠️ 从源码运行
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { spawn, execFile } from "node:child_process";
|
|
2
|
+
import { mkdir, open } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
import { setTimeout } from "node:timers/promises";
|
|
6
|
+
import lockfile from "proper-lockfile";
|
|
7
|
+
import { control, isNotRunning, stopAndWait } from "./control.js";
|
|
8
|
+
import {
|
|
9
|
+
configureLaunchdLogin,
|
|
10
|
+
guiSessionAvailable,
|
|
11
|
+
loginServiceInstalled,
|
|
12
|
+
startLaunchd,
|
|
13
|
+
unloadLaunchd
|
|
14
|
+
} from "./launchd.js";
|
|
15
|
+
import { readJson } from "../updates/files.js";
|
|
16
|
+
const exec = promisify(execFile);
|
|
17
|
+
async function lifecycle(location, action) {
|
|
18
|
+
await mkdir(location.state, { recursive: true, mode: 448 });
|
|
19
|
+
const release = await lockfile.lock(location.state, {
|
|
20
|
+
lockfilePath: join(location.state, "lifecycle.lock"),
|
|
21
|
+
stale: 1e4,
|
|
22
|
+
update: 2e3,
|
|
23
|
+
retries: { retries: 280, minTimeout: 250, maxTimeout: 250 }
|
|
24
|
+
});
|
|
25
|
+
try {
|
|
26
|
+
return await action();
|
|
27
|
+
} finally {
|
|
28
|
+
await release();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async function start(location, entry) {
|
|
32
|
+
try {
|
|
33
|
+
return await control(location, "status");
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if (!isNotRunning(error)) throw error;
|
|
36
|
+
}
|
|
37
|
+
let child;
|
|
38
|
+
const installed = await readJson(
|
|
39
|
+
join(location.state, "service.json")
|
|
40
|
+
);
|
|
41
|
+
if (process.platform === "darwin" && await startLaunchd(location, entry)) {
|
|
42
|
+
} else if (installed?.platform === "linux") {
|
|
43
|
+
await exec("systemctl", ["--user", "start", `${installed.label}.service`]);
|
|
44
|
+
} else {
|
|
45
|
+
if (process.platform === "darwin")
|
|
46
|
+
console.warn(
|
|
47
|
+
"\u5F53\u524D\u6CA1\u6709 macOS \u684C\u9762\u767B\u5F55\u4F1A\u8BDD\uFF0C\u4F7F\u7528 headless \u540E\u53F0\u542F\u52A8\uFF1B\u7CFB\u7EDF\u94A5\u5319\u4E32\u51ED\u8BC1\u53EF\u80FD\u4E0D\u53EF\u7528\u3002"
|
|
48
|
+
);
|
|
49
|
+
const log = await open(location.log, "a", 384);
|
|
50
|
+
try {
|
|
51
|
+
child = spawn(process.execPath, [entry, "run", "--config", location.config], {
|
|
52
|
+
detached: true,
|
|
53
|
+
stdio: ["ignore", log.fd, log.fd],
|
|
54
|
+
windowsHide: true,
|
|
55
|
+
env: { ...process.env, COVRO_HOME: location.home }
|
|
56
|
+
});
|
|
57
|
+
await new Promise((resolve, reject) => {
|
|
58
|
+
child.once("spawn", resolve);
|
|
59
|
+
child.once("error", reject);
|
|
60
|
+
});
|
|
61
|
+
child.unref();
|
|
62
|
+
} finally {
|
|
63
|
+
await log.close();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
for (let attempt = 0; attempt < 240; attempt++) {
|
|
67
|
+
try {
|
|
68
|
+
return await control(location, "status");
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if (!isNotRunning(error)) throw error;
|
|
71
|
+
}
|
|
72
|
+
if (child && child.exitCode !== null) throw new Error(`Covro \u542F\u52A8\u5931\u8D25\uFF0C\u8BF7\u67E5\u770B ${location.log}`);
|
|
73
|
+
await setTimeout(250);
|
|
74
|
+
}
|
|
75
|
+
throw new Error(`Covro \u5C1A\u672A\u786E\u8BA4\u542F\u52A8\uFF0C\u8BF7\u8FD0\u884C covro status \u5E76\u67E5\u770B ${location.log}`);
|
|
76
|
+
}
|
|
77
|
+
async function stop(location) {
|
|
78
|
+
await stopAndWait(location);
|
|
79
|
+
if (process.platform === "darwin") await unloadLaunchd(location);
|
|
80
|
+
else {
|
|
81
|
+
const installed = await readJson(
|
|
82
|
+
join(location.state, "service.json")
|
|
83
|
+
);
|
|
84
|
+
if (installed?.platform === "linux")
|
|
85
|
+
await exec("systemctl", ["--user", "stop", `${installed.label}.service`]);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const startBackground = (location, entry) => lifecycle(location, () => start(location, entry));
|
|
89
|
+
const stopBackground = (location) => lifecycle(location, () => stop(location));
|
|
90
|
+
const macOSLoginService = (location, entry, install) => lifecycle(location, async () => {
|
|
91
|
+
if (install && !await guiSessionAvailable(location))
|
|
92
|
+
throw new Error("\u542F\u7528\u767B\u5F55\u81EA\u542F\u9700\u8981\u5F53\u524D\u7528\u6237\u5DF2\u767B\u5F55 macOS \u684C\u9762");
|
|
93
|
+
if (!install && !await loginServiceInstalled(location)) return;
|
|
94
|
+
await stop(location);
|
|
95
|
+
await configureLaunchdLogin(location, entry, install);
|
|
96
|
+
if (install) await start(location, entry);
|
|
97
|
+
});
|
|
98
|
+
export {
|
|
99
|
+
macOSLoginService,
|
|
100
|
+
startBackground,
|
|
101
|
+
stopBackground
|
|
102
|
+
};
|
|
103
|
+
//# sourceMappingURL=background.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/cli/background.ts"],
|
|
4
|
+
"sourcesContent": ["import { spawn, execFile } from 'node:child_process';\nimport { mkdir, open } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { promisify } from 'node:util';\nimport { setTimeout } from 'node:timers/promises';\nimport lockfile from 'proper-lockfile';\nimport type { CovroPaths } from './paths.js';\nimport { control, isNotRunning, stopAndWait } from './control.js';\nimport {\n configureLaunchdLogin,\n guiSessionAvailable,\n loginServiceInstalled,\n startLaunchd,\n unloadLaunchd,\n} from './launchd.js';\nimport { readJson } from '../updates/files.js';\n\nconst exec = promisify(execFile);\nasync function lifecycle<T>(location: CovroPaths, action: () => Promise<T>) {\n await mkdir(location.state, { recursive: true, mode: 0o700 });\n const release = await lockfile.lock(location.state, {\n lockfilePath: join(location.state, 'lifecycle.lock'),\n stale: 10_000,\n update: 2_000,\n retries: { retries: 280, minTimeout: 250, maxTimeout: 250 },\n });\n try {\n return await action();\n } finally {\n await release();\n }\n}\n\nasync function start(location: CovroPaths, entry: string) {\n try {\n return await control(location, 'status');\n } catch (error) {\n if (!isNotRunning(error)) throw error;\n }\n let child: ReturnType<typeof spawn> | undefined;\n const installed = await readJson<{ platform: string; label: string }>(\n join(location.state, 'service.json'),\n );\n if (process.platform === 'darwin' && (await startLaunchd(location, entry))) {\n // launchd owns the process; startup is confirmed through the same private control socket.\n } else if (installed?.platform === 'linux') {\n await exec('systemctl', ['--user', 'start', `${installed.label}.service`]);\n } else {\n if (process.platform === 'darwin')\n console.warn(\n '\u5F53\u524D\u6CA1\u6709 macOS \u684C\u9762\u767B\u5F55\u4F1A\u8BDD\uFF0C\u4F7F\u7528 headless \u540E\u53F0\u542F\u52A8\uFF1B\u7CFB\u7EDF\u94A5\u5319\u4E32\u51ED\u8BC1\u53EF\u80FD\u4E0D\u53EF\u7528\u3002',\n );\n const log = await open(location.log, 'a', 0o600);\n try {\n child = spawn(process.execPath, [entry, 'run', '--config', location.config], {\n detached: true,\n stdio: ['ignore', log.fd, log.fd],\n windowsHide: true,\n env: { ...process.env, COVRO_HOME: location.home },\n });\n await new Promise<void>((resolve, reject) => {\n child!.once('spawn', resolve);\n child!.once('error', reject);\n });\n child.unref();\n } finally {\n await log.close();\n }\n }\n for (let attempt = 0; attempt < 240; attempt++) {\n try {\n return await control(location, 'status');\n } catch (error) {\n if (!isNotRunning(error)) throw error;\n }\n if (child && child.exitCode !== null) throw new Error(`Covro \u542F\u52A8\u5931\u8D25\uFF0C\u8BF7\u67E5\u770B ${location.log}`);\n await setTimeout(250);\n }\n throw new Error(`Covro \u5C1A\u672A\u786E\u8BA4\u542F\u52A8\uFF0C\u8BF7\u8FD0\u884C covro status \u5E76\u67E5\u770B ${location.log}`);\n}\n\nasync function stop(location: CovroPaths) {\n await stopAndWait(location);\n if (process.platform === 'darwin') await unloadLaunchd(location);\n else {\n const installed = await readJson<{ platform: string; label: string }>(\n join(location.state, 'service.json'),\n );\n if (installed?.platform === 'linux')\n await exec('systemctl', ['--user', 'stop', `${installed.label}.service`]);\n }\n}\n\nexport const startBackground = (location: CovroPaths, entry: string) =>\n lifecycle(location, () => start(location, entry));\nexport const stopBackground = (location: CovroPaths) => lifecycle(location, () => stop(location));\nexport const macOSLoginService = (location: CovroPaths, entry: string, install: boolean) =>\n lifecycle(location, async () => {\n if (install && !(await guiSessionAvailable(location)))\n throw new Error('\u542F\u7528\u767B\u5F55\u81EA\u542F\u9700\u8981\u5F53\u524D\u7528\u6237\u5DF2\u767B\u5F55 macOS \u684C\u9762');\n if (!install && !(await loginServiceInstalled(location))) return;\n await stop(location);\n await configureLaunchdLogin(location, entry, install);\n if (install) await start(location, entry);\n });\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/cli/daemon.js
CHANGED
|
@@ -6,6 +6,7 @@ import { startGateway } from "../server/main.js";
|
|
|
6
6
|
import { packageInfo, paths } from "./paths.js";
|
|
7
7
|
import { UpdateManager } from "../updates/manager.js";
|
|
8
8
|
import { saveJson } from "../updates/files.js";
|
|
9
|
+
import { doctor, launchContext } from "./doctor.js";
|
|
9
10
|
async function runDaemon(configPath) {
|
|
10
11
|
const location = paths(configPath);
|
|
11
12
|
await mkdir(location.state, { recursive: true, mode: 448 });
|
|
@@ -13,6 +14,7 @@ async function runDaemon(configPath) {
|
|
|
13
14
|
let config;
|
|
14
15
|
let runtime;
|
|
15
16
|
let updates;
|
|
17
|
+
const context = await launchContext();
|
|
16
18
|
const server = createServer((request, response) => {
|
|
17
19
|
void (async () => {
|
|
18
20
|
response.setHeader("content-type", "application/json");
|
|
@@ -33,9 +35,12 @@ async function runDaemon(configPath) {
|
|
|
33
35
|
dataDir: config.dataDir,
|
|
34
36
|
maintenance: runtime?.service.isInMaintenance(),
|
|
35
37
|
codexVersion: runtime?.codex.diagnostics().codexVersion ?? null,
|
|
36
|
-
codexReady: runtime?.codex.ready ?? false
|
|
38
|
+
codexReady: runtime?.codex.ready ?? false,
|
|
39
|
+
launchContext: context
|
|
37
40
|
})
|
|
38
41
|
);
|
|
42
|
+
} else if (request.url === "/doctor") {
|
|
43
|
+
response.end(JSON.stringify(await doctor()));
|
|
39
44
|
} else if (request.url === "/stop") {
|
|
40
45
|
await stop(() => response.end(JSON.stringify({ stopped: true })));
|
|
41
46
|
} else if (request.url === "/prepare-update") {
|
package/dist/cli/daemon.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/cli/daemon.ts"],
|
|
4
|
-
"sourcesContent": ["import { createServer } from 'node:http';\nimport { chmod, mkdir, rm } from 'node:fs/promises';\nimport { acquireInstanceLock } from '../server/instance-lock.js';\nimport { loadConfig } from '../server/config.js';\nimport { startGateway } from '../server/main.js';\nimport { packageInfo, paths } from './paths.js';\nimport { UpdateManager } from '../updates/manager.js';\nimport { saveJson } from '../updates/files.js';\n\nexport async function runDaemon(configPath: string) {\n const location = paths(configPath);\n await mkdir(location.state, { recursive: true, mode: 0o700 });\n const release = await acquireInstanceLock(location.state);\n let config: Awaited<ReturnType<typeof loadConfig>>;\n let runtime: Awaited<ReturnType<typeof startGateway>> | undefined;\n let updates: UpdateManager | undefined;\n const server = createServer((request, response) => {\n void (async () => {\n response.setHeader('content-type', 'application/json');\n if (request.method !== 'POST') throw new Error('Expected POST');\n let body = '';\n for await (const chunk of request) {\n body += String(chunk);\n if (body.length > 8192) throw new Error('Control request too large');\n }\n const input: unknown = body ? JSON.parse(body) : {};\n if (request.url === '/status') {\n response.end(\n JSON.stringify({\n version: packageInfo.version,\n pid: process.pid,\n origin: config.publicOrigin[0],\n configPath,\n dataDir: config.dataDir,\n maintenance: runtime?.service.isInMaintenance(),\n codexVersion: runtime?.codex.diagnostics().codexVersion ?? null,\n codexReady: runtime?.codex.ready ?? false,\n }),\n );\n } else if (request.url === '/stop') {\n await stop(() => response.end(JSON.stringify({ stopped: true })));\n } else if (request.url === '/prepare-update') {\n response.end(JSON.stringify(runtime!.service.prepareUpdate()));\n } else if (request.url === '/cancel-update') {\n runtime!.service.setMaintenance(false);\n response.end(JSON.stringify({ cancelled: true }));\n } else if (request.url === '/activate-update') {\n const id = (input as { id?: unknown }).id;\n if (typeof id !== 'string') throw new Error('Invalid upgrade identity');\n await updates!.commitActivation(id);\n runtime!.service.setMaintenance(false);\n response.end(JSON.stringify({ activated: true }));\n } else if (request.url === '/update-status') {\n response.end(JSON.stringify(await updates!.status()));\n } else if (request.url === '/update-check') {\n const channel = (input as { channel?: unknown }).channel;\n if (channel !== undefined && !['stable', 'beta', 'dev'].includes(String(channel)))\n throw new Error('Invalid channel');\n response.end(\n JSON.stringify(await updates!.check(channel as 'stable' | 'beta' | 'dev' | undefined)),\n );\n } else if (request.url === '/update') {\n response.end(\n JSON.stringify(await updates!.start(input as Parameters<UpdateManager['start']>[0])),\n );\n } else {\n response.statusCode = 404;\n response.end(JSON.stringify({ error: 'Unknown control action' }));\n }\n })().catch((error: unknown) => {\n response.statusCode = 400;\n response.end(\n JSON.stringify({ error: error instanceof Error ? error.message : 'Control failed' }),\n );\n });\n });\n let stopping: Promise<void> | undefined;\n const stop = (respond?: () => void) =>\n (stopping ??= (async () => {\n await runtime?.close();\n respond?.();\n await new Promise<void>((done) => server.close(() => done()));\n if (process.platform !== 'win32') await rm(location.socket, { force: true });\n await release();\n })());\n try {\n config = await loadConfig({ configPath, development: false });\n updates = new UpdateManager(\n location,\n config.dataDir,\n () => runtime?.codex.diagnostics().codexVersion ?? null,\n );\n await updates.initialize();\n const maintenance = await updates.activationPending();\n runtime = await startGateway(config, { updates, maintenance });\n if (process.platform !== 'win32') await rm(location.socket, { force: true });\n await new Promise<void>((done, reject) => {\n server.once('error', reject);\n server.listen(location.socket, done);\n });\n if (process.platform !== 'win32') await chmod(location.socket, 0o600);\n await saveJson(`${location.state}/instance.json`, {\n root: updates.context?.root,\n config: location.config,\n socket: location.socket,\n });\n console.log(`Covro ${packageInfo.version} listening on ${config.publicOrigin[0]}`);\n process.once('SIGINT', () => void stop());\n process.once('SIGTERM', () => void stop());\n } catch (error) {\n updates?.close();\n await runtime?.close();\n await release();\n throw error;\n }\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,OAAO,OAAO,UAAU;AACjC,SAAS,2BAA2B;AACpC,SAAS,kBAAkB;AAC3B,SAAS,oBAAoB;AAC7B,SAAS,aAAa,aAAa;AACnC,SAAS,qBAAqB;AAC9B,SAAS,gBAAgB;
|
|
4
|
+
"sourcesContent": ["import { createServer } from 'node:http';\nimport { chmod, mkdir, rm } from 'node:fs/promises';\nimport { acquireInstanceLock } from '../server/instance-lock.js';\nimport { loadConfig } from '../server/config.js';\nimport { startGateway } from '../server/main.js';\nimport { packageInfo, paths } from './paths.js';\nimport { UpdateManager } from '../updates/manager.js';\nimport { saveJson } from '../updates/files.js';\nimport { doctor, launchContext } from './doctor.js';\n\nexport async function runDaemon(configPath: string) {\n const location = paths(configPath);\n await mkdir(location.state, { recursive: true, mode: 0o700 });\n const release = await acquireInstanceLock(location.state);\n let config: Awaited<ReturnType<typeof loadConfig>>;\n let runtime: Awaited<ReturnType<typeof startGateway>> | undefined;\n let updates: UpdateManager | undefined;\n const context = await launchContext();\n const server = createServer((request, response) => {\n void (async () => {\n response.setHeader('content-type', 'application/json');\n if (request.method !== 'POST') throw new Error('Expected POST');\n let body = '';\n for await (const chunk of request) {\n body += String(chunk);\n if (body.length > 8192) throw new Error('Control request too large');\n }\n const input: unknown = body ? JSON.parse(body) : {};\n if (request.url === '/status') {\n response.end(\n JSON.stringify({\n version: packageInfo.version,\n pid: process.pid,\n origin: config.publicOrigin[0],\n configPath,\n dataDir: config.dataDir,\n maintenance: runtime?.service.isInMaintenance(),\n codexVersion: runtime?.codex.diagnostics().codexVersion ?? null,\n codexReady: runtime?.codex.ready ?? false,\n launchContext: context,\n }),\n );\n } else if (request.url === '/doctor') {\n response.end(JSON.stringify(await doctor()));\n } else if (request.url === '/stop') {\n await stop(() => response.end(JSON.stringify({ stopped: true })));\n } else if (request.url === '/prepare-update') {\n response.end(JSON.stringify(runtime!.service.prepareUpdate()));\n } else if (request.url === '/cancel-update') {\n runtime!.service.setMaintenance(false);\n response.end(JSON.stringify({ cancelled: true }));\n } else if (request.url === '/activate-update') {\n const id = (input as { id?: unknown }).id;\n if (typeof id !== 'string') throw new Error('Invalid upgrade identity');\n await updates!.commitActivation(id);\n runtime!.service.setMaintenance(false);\n response.end(JSON.stringify({ activated: true }));\n } else if (request.url === '/update-status') {\n response.end(JSON.stringify(await updates!.status()));\n } else if (request.url === '/update-check') {\n const channel = (input as { channel?: unknown }).channel;\n if (channel !== undefined && !['stable', 'beta', 'dev'].includes(String(channel)))\n throw new Error('Invalid channel');\n response.end(\n JSON.stringify(await updates!.check(channel as 'stable' | 'beta' | 'dev' | undefined)),\n );\n } else if (request.url === '/update') {\n response.end(\n JSON.stringify(await updates!.start(input as Parameters<UpdateManager['start']>[0])),\n );\n } else {\n response.statusCode = 404;\n response.end(JSON.stringify({ error: 'Unknown control action' }));\n }\n })().catch((error: unknown) => {\n response.statusCode = 400;\n response.end(\n JSON.stringify({ error: error instanceof Error ? error.message : 'Control failed' }),\n );\n });\n });\n let stopping: Promise<void> | undefined;\n const stop = (respond?: () => void) =>\n (stopping ??= (async () => {\n await runtime?.close();\n respond?.();\n await new Promise<void>((done) => server.close(() => done()));\n if (process.platform !== 'win32') await rm(location.socket, { force: true });\n await release();\n })());\n try {\n config = await loadConfig({ configPath, development: false });\n updates = new UpdateManager(\n location,\n config.dataDir,\n () => runtime?.codex.diagnostics().codexVersion ?? null,\n );\n await updates.initialize();\n const maintenance = await updates.activationPending();\n runtime = await startGateway(config, { updates, maintenance });\n if (process.platform !== 'win32') await rm(location.socket, { force: true });\n await new Promise<void>((done, reject) => {\n server.once('error', reject);\n server.listen(location.socket, done);\n });\n if (process.platform !== 'win32') await chmod(location.socket, 0o600);\n await saveJson(`${location.state}/instance.json`, {\n root: updates.context?.root,\n config: location.config,\n socket: location.socket,\n });\n console.log(`Covro ${packageInfo.version} listening on ${config.publicOrigin[0]}`);\n process.once('SIGINT', () => void stop());\n process.once('SIGTERM', () => void stop());\n } catch (error) {\n updates?.close();\n await runtime?.close();\n await release();\n throw error;\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
const exec = promisify(execFile);
|
|
4
|
+
async function probe(command, args) {
|
|
5
|
+
try {
|
|
6
|
+
const result = await exec(command, args, { timeout: 4e3, maxBuffer: 64 * 1024 });
|
|
7
|
+
return { exitCode: 0, ...result, failure: void 0 };
|
|
8
|
+
} catch (error) {
|
|
9
|
+
const result = error;
|
|
10
|
+
return {
|
|
11
|
+
exitCode: typeof result.code === "number" ? result.code : null,
|
|
12
|
+
stdout: result.stdout ?? "",
|
|
13
|
+
stderr: result.stderr ?? "",
|
|
14
|
+
failure: result.killed ? "timeout" : typeof result.code === "string" ? result.code : void 0
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async function launchContext() {
|
|
19
|
+
const manager = process.platform === "darwin" ? await probe("/bin/launchctl", ["managername"]) : void 0;
|
|
20
|
+
return {
|
|
21
|
+
platform: process.platform,
|
|
22
|
+
manager: manager?.exitCode === 0 && /^(Aqua|Background|System|LoginWindow|StandardIO)$/.test(manager.stdout.trim()) ? manager.stdout.trim() : null,
|
|
23
|
+
uid: process.getuid?.() ?? null
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
async function doctor() {
|
|
27
|
+
const context = await launchContext();
|
|
28
|
+
const environmentToken = Boolean(process.env.GH_TOKEN || process.env.GITHUB_TOKEN);
|
|
29
|
+
const credential = await probe("gh", ["auth", "token", "--hostname", "github.com"]);
|
|
30
|
+
const found = credential.exitCode === 0 && Boolean(credential.stdout.trim());
|
|
31
|
+
const credentialSource = environmentToken ? "environment" : "stored";
|
|
32
|
+
const notes = [
|
|
33
|
+
"\u68C0\u67E5\u6765\u81EA\u8FD0\u884C\u4E2D\u7684 Covro \u5B9E\u4F8B\uFF0C\u4E0D\u662F\u8C03\u7528 doctor \u7684\u7EC8\u7AEF\u3002",
|
|
34
|
+
"\u672A\u63A2\u6D4B Agent \u6216 Shell \u7684 Codex \u6C99\u7BB1\uFF1B\u5B9E\u4F8B\u8BA4\u8BC1\u6210\u529F\u4E0D\u4EE3\u8868\u53D7\u9650\u547D\u4EE4\u5177\u6709\u76F8\u540C\u6743\u9650\u3002"
|
|
35
|
+
];
|
|
36
|
+
if (context.platform === "darwin" && context.manager && context.manager !== "Aqua")
|
|
37
|
+
notes.push(
|
|
38
|
+
"\u5B9E\u4F8B\u4E0D\u5728 Aqua \u684C\u9762\u4F1A\u8BDD\uFF0C\u7CFB\u7EDF\u94A5\u5319\u4E32\u53EF\u80FD\u4E0D\u53EF\u8BBF\u95EE\uFF1B\u5728\u684C\u9762\u767B\u5F55\u540E\u7528 covro stop \u548C covro start \u91CD\u542F\uFF0C\u6216\u5B89\u88C5 GUI \u767B\u5F55\u670D\u52A1\u3002"
|
|
39
|
+
);
|
|
40
|
+
if (environmentToken)
|
|
41
|
+
notes.push("GH_TOKEN \u6216 GITHUB_TOKEN \u5DF2\u8BBE\u7F6E\uFF0CGitHub CLI \u4F1A\u4F18\u5148\u4F7F\u7528\u73AF\u5883\u53D8\u91CF\u4E2D\u7684\u51ED\u8BC1\u3002");
|
|
42
|
+
const base = {
|
|
43
|
+
pid: process.pid,
|
|
44
|
+
launchContext: context,
|
|
45
|
+
scope: "daemon-child-process",
|
|
46
|
+
sandbox: "not-tested",
|
|
47
|
+
credentialSource,
|
|
48
|
+
credentialReadable: found
|
|
49
|
+
};
|
|
50
|
+
if (credential.failure === "ENOENT")
|
|
51
|
+
return {
|
|
52
|
+
...base,
|
|
53
|
+
result: "gh-unavailable",
|
|
54
|
+
notes: [...notes, "\u5B9E\u4F8B PATH \u4E2D\u6CA1\u6709 GitHub CLI\u3002"]
|
|
55
|
+
};
|
|
56
|
+
if (!found)
|
|
57
|
+
return {
|
|
58
|
+
...base,
|
|
59
|
+
result: "credential-unavailable",
|
|
60
|
+
lookupExitCode: credential.exitCode,
|
|
61
|
+
notes: [
|
|
62
|
+
...notes,
|
|
63
|
+
"\u672A\u80FD\u8BFB\u53D6\u51ED\u8BC1\uFF0C\u5C1A\u672A\u9A8C\u8BC1\u4EE4\u724C\u662F\u5426\u6709\u6548\uFF1B\u8BF7\u5206\u522B\u68C0\u67E5\u542F\u52A8\u4F1A\u8BDD\u3001\u94A5\u5319\u4E32/\u6C99\u7BB1\u8BBF\u95EE\u548C\u662F\u5426\u5DF2\u914D\u7F6E\u51ED\u8BC1\uFF0C\u4E0D\u80FD\u636E\u6B64\u8BA4\u5B9A\u9700\u8981\u91CD\u65B0\u767B\u5F55\u3002"
|
|
64
|
+
]
|
|
65
|
+
};
|
|
66
|
+
const api = await probe("gh", [
|
|
67
|
+
"api",
|
|
68
|
+
"user",
|
|
69
|
+
"--hostname",
|
|
70
|
+
"github.com",
|
|
71
|
+
"--include",
|
|
72
|
+
"--silent"
|
|
73
|
+
]);
|
|
74
|
+
const status = /HTTP\/[\d.]+\s+(\d{3})/.exec(`${api.stdout}
|
|
75
|
+
${api.stderr}`)?.[1];
|
|
76
|
+
const httpStatus = status ? Number(status) : null;
|
|
77
|
+
const result = api.exitCode === 0 ? "authenticated" : httpStatus === 401 ? "authentication-rejected" : httpStatus === 403 ? "request-forbidden" : "request-failed";
|
|
78
|
+
if (result === "authentication-rejected")
|
|
79
|
+
notes.push(
|
|
80
|
+
"\u5DF2\u8BFB\u53D6\u51ED\u8BC1\uFF0C\u4F46 GitHub \u8FD4\u56DE 401\uFF1B\u8BF7\u68C0\u67E5\u64A4\u9500\u3001\u5931\u6548\u6216\u73AF\u5883\u53D8\u91CF\u8986\u76D6\uFF0C\u800C\u4E0D\u662F\u5C06\u5176\u5F52\u56E0\u4E8E\u94A5\u5319\u4E32\u8BFB\u53D6\u5931\u8D25\u3002"
|
|
81
|
+
);
|
|
82
|
+
else if (result === "request-forbidden")
|
|
83
|
+
notes.push("GitHub \u8FD4\u56DE 403\uFF1B\u8BF7\u68C0\u67E5\u6743\u9650\u3001\u7EC4\u7EC7\u7B56\u7565\u6216\u8BF7\u6C42\u9650\u989D\uFF0C\u4E0D\u80FD\u636E\u6B64\u8BA4\u5B9A\u4EE4\u724C\u5931\u6548\u3002");
|
|
84
|
+
else if (result === "request-failed")
|
|
85
|
+
notes.push("\u5DF2\u8BFB\u53D6\u51ED\u8BC1\uFF0C\u4F46\u8BA4\u8BC1\u8BF7\u6C42\u672A\u6210\u529F\uFF1B\u8BF7\u68C0\u67E5\u7F51\u7EDC\u3001\u4EE3\u7406\u6216\u670D\u52A1\u72B6\u6001\uFF0C\u672A\u786E\u8BA4\u4EE4\u724C\u5931\u6548\u3002");
|
|
86
|
+
return { ...base, result, httpStatus, requestExitCode: api.exitCode, notes };
|
|
87
|
+
}
|
|
88
|
+
export {
|
|
89
|
+
doctor,
|
|
90
|
+
launchContext
|
|
91
|
+
};
|
|
92
|
+
//# sourceMappingURL=doctor.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/cli/doctor.ts"],
|
|
4
|
+
"sourcesContent": ["import { execFile } from 'node:child_process';\nimport { promisify } from 'node:util';\n\nconst exec = promisify(execFile);\nasync function probe(command: string, args: string[]) {\n try {\n const result = await exec(command, args, { timeout: 4000, maxBuffer: 64 * 1024 });\n return { exitCode: 0, ...result, failure: undefined };\n } catch (error) {\n const result = error as {\n code?: string | number;\n stdout?: string;\n stderr?: string;\n killed?: boolean;\n };\n return {\n exitCode: typeof result.code === 'number' ? result.code : null,\n stdout: result.stdout ?? '',\n stderr: result.stderr ?? '',\n failure: result.killed\n ? 'timeout'\n : typeof result.code === 'string'\n ? result.code\n : undefined,\n };\n }\n}\n\nexport async function launchContext() {\n const manager =\n process.platform === 'darwin' ? await probe('/bin/launchctl', ['managername']) : undefined;\n return {\n platform: process.platform,\n manager:\n manager?.exitCode === 0 &&\n /^(Aqua|Background|System|LoginWindow|StandardIO)$/.test(manager.stdout.trim())\n ? manager.stdout.trim()\n : null,\n uid: process.getuid?.() ?? null,\n };\n}\n\n/** Runs inside the daemon. Return classifications only: command errors can contain credentials. */\nexport async function doctor() {\n const context = await launchContext();\n const environmentToken = Boolean(process.env.GH_TOKEN || process.env.GITHUB_TOKEN);\n const credential = await probe('gh', ['auth', 'token', '--hostname', 'github.com']);\n const found = credential.exitCode === 0 && Boolean(credential.stdout.trim());\n const credentialSource = environmentToken ? 'environment' : 'stored';\n const notes = [\n '\u68C0\u67E5\u6765\u81EA\u8FD0\u884C\u4E2D\u7684 Covro \u5B9E\u4F8B\uFF0C\u4E0D\u662F\u8C03\u7528 doctor \u7684\u7EC8\u7AEF\u3002',\n '\u672A\u63A2\u6D4B Agent \u6216 Shell \u7684 Codex \u6C99\u7BB1\uFF1B\u5B9E\u4F8B\u8BA4\u8BC1\u6210\u529F\u4E0D\u4EE3\u8868\u53D7\u9650\u547D\u4EE4\u5177\u6709\u76F8\u540C\u6743\u9650\u3002',\n ];\n if (context.platform === 'darwin' && context.manager && context.manager !== 'Aqua')\n notes.push(\n '\u5B9E\u4F8B\u4E0D\u5728 Aqua \u684C\u9762\u4F1A\u8BDD\uFF0C\u7CFB\u7EDF\u94A5\u5319\u4E32\u53EF\u80FD\u4E0D\u53EF\u8BBF\u95EE\uFF1B\u5728\u684C\u9762\u767B\u5F55\u540E\u7528 covro stop \u548C covro start \u91CD\u542F\uFF0C\u6216\u5B89\u88C5 GUI \u767B\u5F55\u670D\u52A1\u3002',\n );\n if (environmentToken)\n notes.push('GH_TOKEN \u6216 GITHUB_TOKEN \u5DF2\u8BBE\u7F6E\uFF0CGitHub CLI \u4F1A\u4F18\u5148\u4F7F\u7528\u73AF\u5883\u53D8\u91CF\u4E2D\u7684\u51ED\u8BC1\u3002');\n const base = {\n pid: process.pid,\n launchContext: context,\n scope: 'daemon-child-process',\n sandbox: 'not-tested',\n credentialSource,\n credentialReadable: found,\n };\n if (credential.failure === 'ENOENT')\n return {\n ...base,\n result: 'gh-unavailable',\n notes: [...notes, '\u5B9E\u4F8B PATH \u4E2D\u6CA1\u6709 GitHub CLI\u3002'],\n };\n if (!found)\n return {\n ...base,\n result: 'credential-unavailable',\n lookupExitCode: credential.exitCode,\n notes: [\n ...notes,\n '\u672A\u80FD\u8BFB\u53D6\u51ED\u8BC1\uFF0C\u5C1A\u672A\u9A8C\u8BC1\u4EE4\u724C\u662F\u5426\u6709\u6548\uFF1B\u8BF7\u5206\u522B\u68C0\u67E5\u542F\u52A8\u4F1A\u8BDD\u3001\u94A5\u5319\u4E32/\u6C99\u7BB1\u8BBF\u95EE\u548C\u662F\u5426\u5DF2\u914D\u7F6E\u51ED\u8BC1\uFF0C\u4E0D\u80FD\u636E\u6B64\u8BA4\u5B9A\u9700\u8981\u91CD\u65B0\u767B\u5F55\u3002',\n ],\n };\n const api = await probe('gh', [\n 'api',\n 'user',\n '--hostname',\n 'github.com',\n '--include',\n '--silent',\n ]);\n const status = /HTTP\\/[\\d.]+\\s+(\\d{3})/.exec(`${api.stdout}\\n${api.stderr}`)?.[1];\n const httpStatus = status ? Number(status) : null;\n const result =\n api.exitCode === 0\n ? 'authenticated'\n : httpStatus === 401\n ? 'authentication-rejected'\n : httpStatus === 403\n ? 'request-forbidden'\n : 'request-failed';\n if (result === 'authentication-rejected')\n notes.push(\n '\u5DF2\u8BFB\u53D6\u51ED\u8BC1\uFF0C\u4F46 GitHub \u8FD4\u56DE 401\uFF1B\u8BF7\u68C0\u67E5\u64A4\u9500\u3001\u5931\u6548\u6216\u73AF\u5883\u53D8\u91CF\u8986\u76D6\uFF0C\u800C\u4E0D\u662F\u5C06\u5176\u5F52\u56E0\u4E8E\u94A5\u5319\u4E32\u8BFB\u53D6\u5931\u8D25\u3002',\n );\n else if (result === 'request-forbidden')\n notes.push('GitHub \u8FD4\u56DE 403\uFF1B\u8BF7\u68C0\u67E5\u6743\u9650\u3001\u7EC4\u7EC7\u7B56\u7565\u6216\u8BF7\u6C42\u9650\u989D\uFF0C\u4E0D\u80FD\u636E\u6B64\u8BA4\u5B9A\u4EE4\u724C\u5931\u6548\u3002');\n else if (result === 'request-failed')\n notes.push('\u5DF2\u8BFB\u53D6\u51ED\u8BC1\uFF0C\u4F46\u8BA4\u8BC1\u8BF7\u6C42\u672A\u6210\u529F\uFF1B\u8BF7\u68C0\u67E5\u7F51\u7EDC\u3001\u4EE3\u7406\u6216\u670D\u52A1\u72B6\u6001\uFF0C\u672A\u786E\u8BA4\u4EE4\u724C\u5931\u6548\u3002');\n return { ...base, result, httpStatus, requestExitCode: api.exitCode, notes };\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,gBAAgB;AACzB,SAAS,iBAAiB;AAE1B,MAAM,OAAO,UAAU,QAAQ;AAC/B,eAAe,MAAM,SAAiB,MAAgB;AACpD,MAAI;AACF,UAAM,SAAS,MAAM,KAAK,SAAS,MAAM,EAAE,SAAS,KAAM,WAAW,KAAK,KAAK,CAAC;AAChF,WAAO,EAAE,UAAU,GAAG,GAAG,QAAQ,SAAS,OAAU;AAAA,EACtD,SAAS,OAAO;AACd,UAAM,SAAS;AAMf,WAAO;AAAA,MACL,UAAU,OAAO,OAAO,SAAS,WAAW,OAAO,OAAO;AAAA,MAC1D,QAAQ,OAAO,UAAU;AAAA,MACzB,QAAQ,OAAO,UAAU;AAAA,MACzB,SAAS,OAAO,SACZ,YACA,OAAO,OAAO,SAAS,WACrB,OAAO,OACP;AAAA,IACR;AAAA,EACF;AACF;AAEA,eAAsB,gBAAgB;AACpC,QAAM,UACJ,QAAQ,aAAa,WAAW,MAAM,MAAM,kBAAkB,CAAC,aAAa,CAAC,IAAI;AACnF,SAAO;AAAA,IACL,UAAU,QAAQ;AAAA,IAClB,SACE,SAAS,aAAa,KACtB,oDAAoD,KAAK,QAAQ,OAAO,KAAK,CAAC,IAC1E,QAAQ,OAAO,KAAK,IACpB;AAAA,IACN,KAAK,QAAQ,SAAS,KAAK;AAAA,EAC7B;AACF;AAGA,eAAsB,SAAS;AAC7B,QAAM,UAAU,MAAM,cAAc;AACpC,QAAM,mBAAmB,QAAQ,QAAQ,IAAI,YAAY,QAAQ,IAAI,YAAY;AACjF,QAAM,aAAa,MAAM,MAAM,MAAM,CAAC,QAAQ,SAAS,cAAc,YAAY,CAAC;AAClF,QAAM,QAAQ,WAAW,aAAa,KAAK,QAAQ,WAAW,OAAO,KAAK,CAAC;AAC3E,QAAM,mBAAmB,mBAAmB,gBAAgB;AAC5D,QAAM,QAAQ;AAAA,IACZ;AAAA,IACA;AAAA,EACF;AACA,MAAI,QAAQ,aAAa,YAAY,QAAQ,WAAW,QAAQ,YAAY;AAC1E,UAAM;AAAA,MACJ;AAAA,IACF;AACF,MAAI;AACF,UAAM,KAAK,sJAAuD;AACpE,QAAM,OAAO;AAAA,IACX,KAAK,QAAQ;AAAA,IACb,eAAe;AAAA,IACf,OAAO;AAAA,IACP,SAAS;AAAA,IACT;AAAA,IACA,oBAAoB;AAAA,EACtB;AACA,MAAI,WAAW,YAAY;AACzB,WAAO;AAAA,MACL,GAAG;AAAA,MACH,QAAQ;AAAA,MACR,OAAO,CAAC,GAAG,OAAO,uDAAyB;AAAA,IAC7C;AACF,MAAI,CAAC;AACH,WAAO;AAAA,MACL,GAAG;AAAA,MACH,QAAQ;AAAA,MACR,gBAAgB,WAAW;AAAA,MAC3B,OAAO;AAAA,QACL,GAAG;AAAA,QACH;AAAA,MACF;AAAA,IACF;AACF,QAAM,MAAM,MAAM,MAAM,MAAM;AAAA,IAC5B;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AACD,QAAM,SAAS,yBAAyB,KAAK,GAAG,IAAI,MAAM;AAAA,EAAK,IAAI,MAAM,EAAE,IAAI,CAAC;AAChF,QAAM,aAAa,SAAS,OAAO,MAAM,IAAI;AAC7C,QAAM,SACJ,IAAI,aAAa,IACb,kBACA,eAAe,MACb,4BACA,eAAe,MACb,sBACA;AACV,MAAI,WAAW;AACb,UAAM;AAAA,MACJ;AAAA,IACF;AAAA,WACO,WAAW;AAClB,UAAM,KAAK,iMAA2C;AAAA,WAC/C,WAAW;AAClB,UAAM,KAAK,gOAAuC;AACpD,SAAO,EAAE,GAAG,MAAM,QAAQ,YAAY,iBAAiB,IAAI,UAAU,MAAM;AAC7E;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { createServer } from "node:net";
|
|
2
|
+
import { chmod, rm } from "node:fs/promises";
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
async function environmentBridge(home) {
|
|
6
|
+
const path = join(home, `e-${randomBytes(6).toString("hex")}.sock`);
|
|
7
|
+
const clients = /* @__PURE__ */ new Set();
|
|
8
|
+
let accept;
|
|
9
|
+
const consumed = new Promise((resolve) => {
|
|
10
|
+
accept = resolve;
|
|
11
|
+
});
|
|
12
|
+
const payload = JSON.stringify({ env: { ...process.env, COVRO_HOME: home }, cwd: process.cwd() });
|
|
13
|
+
const server = createServer((socket) => {
|
|
14
|
+
clients.add(socket);
|
|
15
|
+
socket.setTimeout(5e3, () => socket.destroy());
|
|
16
|
+
socket.on("error", () => {
|
|
17
|
+
});
|
|
18
|
+
socket.on("close", () => clients.delete(socket));
|
|
19
|
+
socket.end(payload, () => accept?.());
|
|
20
|
+
});
|
|
21
|
+
try {
|
|
22
|
+
await new Promise((resolve, reject) => {
|
|
23
|
+
server.once("error", reject);
|
|
24
|
+
server.listen(path, resolve);
|
|
25
|
+
});
|
|
26
|
+
await chmod(path, 384);
|
|
27
|
+
return {
|
|
28
|
+
path,
|
|
29
|
+
async wait() {
|
|
30
|
+
let timer;
|
|
31
|
+
try {
|
|
32
|
+
await Promise.race([
|
|
33
|
+
consumed,
|
|
34
|
+
new Promise((_, reject) => {
|
|
35
|
+
timer = setTimeout(
|
|
36
|
+
() => reject(new Error("launchd \u542F\u52A8\u73AF\u5883\u4F20\u9012\u8D85\u65F6\uFF0C\u8BF7\u67E5\u770B Covro \u65E5\u5FD7")),
|
|
37
|
+
1e4
|
|
38
|
+
);
|
|
39
|
+
})
|
|
40
|
+
]);
|
|
41
|
+
} finally {
|
|
42
|
+
clearTimeout(timer);
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
async close() {
|
|
46
|
+
for (const socket of clients) socket.destroy();
|
|
47
|
+
await new Promise(
|
|
48
|
+
(resolve, reject) => server.close((error) => error ? reject(error) : resolve())
|
|
49
|
+
);
|
|
50
|
+
await rm(path, { force: true });
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
} catch (error) {
|
|
54
|
+
server.close();
|
|
55
|
+
await rm(path, { force: true });
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const launchRunner = `import { connect } from 'node:net';
|
|
60
|
+
const [socketPath, entry, config] = process.argv.slice(2);
|
|
61
|
+
try {
|
|
62
|
+
const input = await new Promise((resolve, reject) => {
|
|
63
|
+
const socket = connect(socketPath);
|
|
64
|
+
let body = '';
|
|
65
|
+
socket.setTimeout(10000, () => socket.destroy(new Error('launch environment timed out')));
|
|
66
|
+
socket.setEncoding('utf8');
|
|
67
|
+
socket.on('data', chunk => {
|
|
68
|
+
body += chunk;
|
|
69
|
+
if (Buffer.byteLength(body) > 2 * 1024 * 1024) socket.destroy(new Error('launch environment too large'));
|
|
70
|
+
});
|
|
71
|
+
socket.on('error', reject);
|
|
72
|
+
socket.on('end', () => { try { resolve(JSON.parse(body)); } catch { reject(new Error('invalid launch environment')); } });
|
|
73
|
+
});
|
|
74
|
+
process.chdir(input.cwd);
|
|
75
|
+
process.execve(process.execPath, [process.execPath, entry, 'run', '--config', config], input.env);
|
|
76
|
+
} catch {
|
|
77
|
+
console.error('Covro launch environment unavailable; restart with covro start.');
|
|
78
|
+
process.exitCode = 1;
|
|
79
|
+
}
|
|
80
|
+
`;
|
|
81
|
+
export {
|
|
82
|
+
environmentBridge,
|
|
83
|
+
launchRunner
|
|
84
|
+
};
|
|
85
|
+
//# sourceMappingURL=launch-environment.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/cli/launch-environment.ts"],
|
|
4
|
+
"sourcesContent": ["import { createServer, type Socket } from 'node:net';\nimport { chmod, rm } from 'node:fs/promises';\nimport { randomBytes } from 'node:crypto';\nimport { join } from 'node:path';\n\n// The launchd job receives only this socket path. Credentials stay in memory, never in a plist.\nexport async function environmentBridge(home: string) {\n const path = join(home, `e-${randomBytes(6).toString('hex')}.sock`);\n const clients = new Set<Socket>();\n let accept: (() => void) | undefined;\n const consumed = new Promise<void>((resolve) => {\n accept = resolve;\n });\n const payload = JSON.stringify({ env: { ...process.env, COVRO_HOME: home }, cwd: process.cwd() });\n const server = createServer((socket) => {\n clients.add(socket);\n socket.setTimeout(5000, () => socket.destroy());\n socket.on('error', () => {});\n socket.on('close', () => clients.delete(socket));\n socket.end(payload, () => accept?.());\n });\n try {\n await new Promise<void>((resolve, reject) => {\n server.once('error', reject);\n server.listen(path, resolve);\n });\n await chmod(path, 0o600);\n return {\n path,\n async wait() {\n let timer: ReturnType<typeof setTimeout> | undefined;\n try {\n await Promise.race([\n consumed,\n new Promise<never>((_, reject) => {\n timer = setTimeout(\n () => reject(new Error('launchd \u542F\u52A8\u73AF\u5883\u4F20\u9012\u8D85\u65F6\uFF0C\u8BF7\u67E5\u770B Covro \u65E5\u5FD7')),\n 10_000,\n );\n }),\n ]);\n } finally {\n clearTimeout(timer);\n }\n },\n async close() {\n for (const socket of clients) socket.destroy();\n await new Promise<void>((resolve, reject) =>\n server.close((error) => (error ? reject(error) : resolve())),\n );\n await rm(path, { force: true });\n },\n };\n } catch (error) {\n server.close();\n await rm(path, { force: true });\n throw error;\n }\n}\n\n// Standalone Node runner also works when the update worker restores a version predating this fix.\n// execve preserves launchd's process identity and signal handling without an extra supervisor.\nexport const launchRunner = `import { connect } from 'node:net';\nconst [socketPath, entry, config] = process.argv.slice(2);\ntry {\n const input = await new Promise((resolve, reject) => {\n const socket = connect(socketPath);\n let body = '';\n socket.setTimeout(10000, () => socket.destroy(new Error('launch environment timed out')));\n socket.setEncoding('utf8');\n socket.on('data', chunk => {\n body += chunk;\n if (Buffer.byteLength(body) > 2 * 1024 * 1024) socket.destroy(new Error('launch environment too large'));\n });\n socket.on('error', reject);\n socket.on('end', () => { try { resolve(JSON.parse(body)); } catch { reject(new Error('invalid launch environment')); } });\n });\n process.chdir(input.cwd);\n process.execve(process.execPath, [process.execPath, entry, 'run', '--config', config], input.env);\n} catch {\n console.error('Covro launch environment unavailable; restart with covro start.');\n process.exitCode = 1;\n}\n`;\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAiC;AAC1C,SAAS,OAAO,UAAU;AAC1B,SAAS,mBAAmB;AAC5B,SAAS,YAAY;AAGrB,eAAsB,kBAAkB,MAAc;AACpD,QAAM,OAAO,KAAK,MAAM,KAAK,YAAY,CAAC,EAAE,SAAS,KAAK,CAAC,OAAO;AAClE,QAAM,UAAU,oBAAI,IAAY;AAChC,MAAI;AACJ,QAAM,WAAW,IAAI,QAAc,CAAC,YAAY;AAC9C,aAAS;AAAA,EACX,CAAC;AACD,QAAM,UAAU,KAAK,UAAU,EAAE,KAAK,EAAE,GAAG,QAAQ,KAAK,YAAY,KAAK,GAAG,KAAK,QAAQ,IAAI,EAAE,CAAC;AAChG,QAAM,SAAS,aAAa,CAAC,WAAW;AACtC,YAAQ,IAAI,MAAM;AAClB,WAAO,WAAW,KAAM,MAAM,OAAO,QAAQ,CAAC;AAC9C,WAAO,GAAG,SAAS,MAAM;AAAA,IAAC,CAAC;AAC3B,WAAO,GAAG,SAAS,MAAM,QAAQ,OAAO,MAAM,CAAC;AAC/C,WAAO,IAAI,SAAS,MAAM,SAAS,CAAC;AAAA,EACtC,CAAC;AACD,MAAI;AACF,UAAM,IAAI,QAAc,CAAC,SAAS,WAAW;AAC3C,aAAO,KAAK,SAAS,MAAM;AAC3B,aAAO,OAAO,MAAM,OAAO;AAAA,IAC7B,CAAC;AACD,UAAM,MAAM,MAAM,GAAK;AACvB,WAAO;AAAA,MACL;AAAA,MACA,MAAM,OAAO;AACX,YAAI;AACJ,YAAI;AACF,gBAAM,QAAQ,KAAK;AAAA,YACjB;AAAA,YACA,IAAI,QAAe,CAAC,GAAG,WAAW;AAChC,sBAAQ;AAAA,gBACN,MAAM,OAAO,IAAI,MAAM,qGAA+B,CAAC;AAAA,gBACvD;AAAA,cACF;AAAA,YACF,CAAC;AAAA,UACH,CAAC;AAAA,QACH,UAAE;AACA,uBAAa,KAAK;AAAA,QACpB;AAAA,MACF;AAAA,MACA,MAAM,QAAQ;AACZ,mBAAW,UAAU,QAAS,QAAO,QAAQ;AAC7C,cAAM,IAAI;AAAA,UAAc,CAAC,SAAS,WAChC,OAAO,MAAM,CAAC,UAAW,QAAQ,OAAO,KAAK,IAAI,QAAQ,CAAE;AAAA,QAC7D;AACA,cAAM,GAAG,MAAM,EAAE,OAAO,KAAK,CAAC;AAAA,MAChC;AAAA,IACF;AAAA,EACF,SAAS,OAAO;AACd,WAAO,MAAM;AACb,UAAM,GAAG,MAAM,EAAE,OAAO,KAAK,CAAC;AAC9B,UAAM;AAAA,EACR;AACF;AAIO,MAAM,eAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|