@orion-agents/orion-code 0.3.7 → 0.3.9
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/CHANGELOG.md +35 -2
- package/README.md +4 -6
- package/README.zh-CN.md +4 -5
- package/dist/cli.js +58 -32
- package/dist/services/redaction.js +8 -0
- package/dist/web/cli-options.d.ts +13 -0
- package/dist/web/cli-options.js +47 -0
- package/dist/web/host-daemon.d.ts +61 -0
- package/dist/web/host-daemon.js +193 -0
- package/dist/web/launch.d.ts +3 -0
- package/dist/web/launch.js +24 -0
- package/dist/web/review-service.js +41 -13
- package/dist/web-client/assets/{DiffViewer-DqbZ02RN.js → DiffViewer-MMhvMif-.js} +1 -1
- package/dist/web-client/assets/{FilesPanel-Bu1Ab2kJ.js → FilesPanel-4litEN1k.js} +1 -1
- package/dist/web-client/assets/{GitPanel-BixIWb4k.js → GitPanel-BVwlDRVo.js} +1 -1
- package/dist/web-client/assets/{ReviewPanel-DsqJpku_.js → ReviewPanel-ChA0Zd2g.js} +1 -1
- package/dist/web-client/assets/{TerminalPanel-DdjQP2mp.js → TerminalPanel-D8C1uybv.js} +1 -1
- package/dist/web-client/assets/{index-30GT9Vnx.js → index-Dv_uE9dk.js} +7 -7
- package/dist/web-client/index.html +1 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -22,6 +22,41 @@ which is **not** a pass.
|
|
|
22
22
|
|
|
23
23
|
## [Unreleased]
|
|
24
24
|
|
|
25
|
+
## [0.3.9] — CANDIDATE
|
|
26
|
+
|
|
27
|
+
## [0.3.9] — CANDIDATE
|
|
28
|
+
|
|
29
|
+
> **Status: candidate.** Deterministic-bug-fix release + background host lifecycle
|
|
30
|
+
> (issue #247 S1/S2). Not merged, tagged or published to npm.
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- `orion web --background`: starts the Web Workbench host detached from the
|
|
35
|
+
terminal, writes a pidfile (`~/.orion-code/web-host.<port>.pid`), redirects
|
|
36
|
+
logs to `~/.orion-code/logs/web-<port>.log` and prints URL/pid.
|
|
37
|
+
- `orion web status|stop|restart [--port <number>]`: pidfile-based host
|
|
38
|
+
management without third-party supervisors. Still binds 127.0.0.1 and keeps
|
|
39
|
+
the fail-closed approval/context/sandbox semantics unchanged (issue #247).
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- Issue-sweep triage: #222 (blank session rename), #223 (failed create leaves a
|
|
44
|
+
persisted active session), #234 (URL userinfo password starting with `token:`),
|
|
45
|
+
#238 (busy state-write clearing processing), #242 (stale Settings save into the
|
|
46
|
+
newly active workspace) were all already resolved on `main` — closed with
|
|
47
|
+
source evidence.
|
|
48
|
+
- Web redaction now covers uppercase X-prefixed environment assignments
|
|
49
|
+
(`X_CLIENT_SECRET=…`, `CLIENT_SECRET=…`, `AUTH_TOKEN=…`) whose secret suffix
|
|
50
|
+
hides behind underscore word characters (issue #241).
|
|
51
|
+
- Cmd/Ctrl+B can again open the Project Navigator at 761–799px: expanding the
|
|
52
|
+
navigation preference now resolves to a drawer and opens it explicitly instead
|
|
53
|
+
of leaving an invisible drawer (issue #226).
|
|
54
|
+
|
|
55
|
+
### Tests
|
|
56
|
+
|
|
57
|
+
- `redaction-xprefixed-env` suite: X-prefixed env values, bare uppercase
|
|
58
|
+
assignments, #234 userinfo regression, #241 JSON keys.
|
|
59
|
+
|
|
25
60
|
## [0.3.7] — CANDIDATE
|
|
26
61
|
|
|
27
62
|
> **Status: candidate.** This work is isolated on the codex/v0.3.7 worktree (left project
|
|
@@ -73,7 +108,6 @@ which is **not** a pass.
|
|
|
73
108
|
112 cases green. E2E: `web-v037-session-lifecycle.spec.ts` (archive/restore, delete-confirm,
|
|
74
109
|
tags round-trip) runs in CI.
|
|
75
110
|
|
|
76
|
-
|
|
77
111
|
## [0.3.6] — CANDIDATE
|
|
78
112
|
|
|
79
113
|
> **Status: candidate.** This work is isolated on the codex/v0.3.6 worktree (pure web
|
|
@@ -111,7 +145,6 @@ which is **not** a pass.
|
|
|
111
145
|
- Markdown heading off-by-one: `##` renders as `<h2>` (was `<h3>`), a lone `#` no longer
|
|
112
146
|
collapses to `<h6>`; >6 hashes stay a plain paragraph per GFM.
|
|
113
147
|
|
|
114
|
-
|
|
115
148
|
## [0.3.5] — CANDIDATE
|
|
116
149
|
|
|
117
150
|
> **Status: candidate.** This work is isolated on the codex/v0.3.5 worktree. It is not
|
package/README.md
CHANGED
|
@@ -2,11 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
Local-first, goal-driven coding agent for the terminal and browser.
|
|
4
4
|
|
|
5
|
-
> v0.3.
|
|
6
|
-
>
|
|
7
|
-
> (
|
|
8
|
-
> rail badges, an archived-Session section with restore, and a friendlier
|
|
9
|
-
> "打开或新增项目" workspace dialog. Candidate source is not an npm publication,
|
|
5
|
+
> v0.3.9 candidate — deterministic-bug sweep over the open issue set plus a native
|
|
6
|
+
> background-host lifecycle: `orion web --background` with pidfile/log management
|
|
7
|
+
> (`orion web status|stop|restart`). Candidate source is not an npm publication,
|
|
10
8
|
> Git tag, or merge-ready release.
|
|
11
9
|
|
|
12
10
|
[中文说明](README.zh-CN.md) ·
|
|
@@ -172,7 +170,7 @@ current development environments. Node 20 is upstream EOL and is no longer a v0.
|
|
|
172
170
|
After the immutable `0.3.5` npm receipt exists:
|
|
173
171
|
|
|
174
172
|
```bash
|
|
175
|
-
npm install -g @orion-agents/orion-code@0.3.
|
|
173
|
+
npm install -g @orion-agents/orion-code@0.3.9
|
|
176
174
|
orion --version
|
|
177
175
|
orion doctor
|
|
178
176
|
```
|
package/README.zh-CN.md
CHANGED
|
@@ -2,10 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
面向终端与浏览器、本地优先的目标驱动 Coding Agent。
|
|
4
4
|
|
|
5
|
-
> v0.3.
|
|
6
|
-
>
|
|
7
|
-
>
|
|
8
|
-
> 可合并状态。
|
|
5
|
+
> v0.3.9 候选版本:对 OPEN issue 集的确定性 bug 清扫 + 原生后台 host 生命周期 ——
|
|
6
|
+
> `orion web --background`(pidfile/日志)与 `orion web status|stop|restart` 管理。
|
|
7
|
+
> Candidate 源码不代表已创建 npm 发布、Git tag 或达到可合并状态。
|
|
9
8
|
|
|
10
9
|
[English](README.md) ·
|
|
11
10
|
[v0.3.7 方案](docs/plan/v0.3.7-left-rail-improvement-plan.md) ·
|
|
@@ -102,7 +101,7 @@ Current。Node 20 已结束上游维护,不再属于 v0.3 Runtime 合同。
|
|
|
102
101
|
当 npm 已存在不可变的 `0.3.4` 发布凭据后:
|
|
103
102
|
|
|
104
103
|
```bash
|
|
105
|
-
npm install -g @orion-agents/orion-code@0.3.
|
|
104
|
+
npm install -g @orion-agents/orion-code@0.3.9
|
|
106
105
|
orion --version
|
|
107
106
|
orion doctor
|
|
108
107
|
```
|
package/dist/cli.js
CHANGED
|
@@ -18,6 +18,8 @@ const product_bootstrap_1 = require("./runtime/product-bootstrap");
|
|
|
18
18
|
const command_1 = require("./migration/command");
|
|
19
19
|
const version_1 = require("./product/version");
|
|
20
20
|
const web_1 = require("./web");
|
|
21
|
+
const host_daemon_1 = require("./web/host-daemon");
|
|
22
|
+
const cli_options_1 = require("./web/cli-options");
|
|
21
23
|
const BRAND = chalk_1.default.hex('#FF6B35');
|
|
22
24
|
const ACCENT = chalk_1.default.hex('#00D4AA');
|
|
23
25
|
const DIM = chalk_1.default.dim;
|
|
@@ -52,41 +54,36 @@ function showCliHelp() {
|
|
|
52
54
|
console.log(DIM('tui is the default product UI. terminal is the technical fallback; print is experimental.'));
|
|
53
55
|
console.log();
|
|
54
56
|
}
|
|
55
|
-
function
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
57
|
+
function showWebHelp() {
|
|
58
|
+
console.log('Usage: orion web [--port <number>] [--no-open] [--cwd <directory>] [--background]');
|
|
59
|
+
console.log('Starts the local-only Web Workbench on 127.0.0.1.');
|
|
60
|
+
console.log(' --background run detached in the background (pidfile + logs under ~/.orion-code)');
|
|
61
|
+
console.log('orion web status [--port <number>]');
|
|
62
|
+
console.log('orion web stop [--port <number>]');
|
|
63
|
+
console.log('orion web restart [--port <number>]');
|
|
64
|
+
console.log(' Manage a background host started with --background.');
|
|
65
|
+
}
|
|
66
|
+
async function manageBackgroundHost(command, args) {
|
|
67
|
+
const options = (0, cli_options_1.parseWebCliOptions)(args);
|
|
68
|
+
const status = (0, host_daemon_1.hostDaemonStatus)(options.port);
|
|
69
|
+
if (command === 'status') {
|
|
70
|
+
if (status.state === 'running') {
|
|
71
|
+
console.log(`orion web is running: ${status.pidfile.url} (pid ${status.pidfile.pid})`);
|
|
72
|
+
console.log(`workspace ${status.pidfile.workspace} · started ${new Date(status.pidfile.startedAt).toISOString()}`);
|
|
64
73
|
}
|
|
65
|
-
if (
|
|
66
|
-
|
|
67
|
-
if (!portValue || !/^\d+$/u.test(portValue)) {
|
|
68
|
-
throw new Error('--port must be an integer from 0 through 65535.');
|
|
69
|
-
}
|
|
70
|
-
port = Number(portValue);
|
|
71
|
-
if (!Number.isSafeInteger(port) || port < 0 || port > 65535) {
|
|
72
|
-
throw new Error('--port must be an integer from 0 through 65535.');
|
|
73
|
-
}
|
|
74
|
-
continue;
|
|
74
|
+
else if (status.state === 'stale') {
|
|
75
|
+
console.log(`orion web is not running (stale pidfile for pid ${status.pid} on port ${status.port}).`);
|
|
75
76
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (!cwdValue?.trim())
|
|
79
|
-
throw new Error('--cwd requires a directory path.');
|
|
80
|
-
cwd = cwdValue;
|
|
81
|
-
continue;
|
|
77
|
+
else {
|
|
78
|
+
console.log(`orion web is not running on port ${options.port}.`);
|
|
82
79
|
}
|
|
83
|
-
|
|
80
|
+
return;
|
|
84
81
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
82
|
+
const result = await (0, host_daemon_1.stopBackgroundHost)(options.port);
|
|
83
|
+
if (result === 'stopped')
|
|
84
|
+
console.log(`orion web on port ${options.port} stopped.`);
|
|
85
|
+
else
|
|
86
|
+
console.log(`orion web was not running on port ${options.port}.`);
|
|
90
87
|
}
|
|
91
88
|
function parseCliOptions(args) {
|
|
92
89
|
let uiRenderer = config_1.DEFAULT_UI_RENDERER;
|
|
@@ -159,7 +156,36 @@ async function main() {
|
|
|
159
156
|
showWebHelp();
|
|
160
157
|
return;
|
|
161
158
|
}
|
|
162
|
-
|
|
159
|
+
const rest = args.slice(1);
|
|
160
|
+
if (rest[0] === 'status' || rest[0] === 'stop') {
|
|
161
|
+
await manageBackgroundHost(rest[0], rest.slice(1));
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (rest[0] === 'restart') {
|
|
165
|
+
await manageBackgroundHost('stop', rest.slice(1));
|
|
166
|
+
const restartOptions = (0, cli_options_1.parseWebCliOptions)(rest.slice(1));
|
|
167
|
+
await (0, web_1.runOrionWeb)({
|
|
168
|
+
...restartOptions,
|
|
169
|
+
background: true,
|
|
170
|
+
backgroundArgs: [
|
|
171
|
+
...(restartOptions.open ? [] : ['--no-open']),
|
|
172
|
+
'--port',
|
|
173
|
+
String(restartOptions.port),
|
|
174
|
+
'--cwd',
|
|
175
|
+
restartOptions.cwd,
|
|
176
|
+
],
|
|
177
|
+
});
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const options = (0, cli_options_1.parseWebCliOptions)(rest);
|
|
181
|
+
const backgroundArgs = [
|
|
182
|
+
...(options.open ? [] : ['--no-open']),
|
|
183
|
+
'--port',
|
|
184
|
+
String(options.port),
|
|
185
|
+
'--cwd',
|
|
186
|
+
options.cwd,
|
|
187
|
+
];
|
|
188
|
+
await (0, web_1.runOrionWeb)({ ...options, backgroundArgs });
|
|
163
189
|
return;
|
|
164
190
|
}
|
|
165
191
|
if (args[0] === 'migrate') {
|
|
@@ -69,6 +69,14 @@ const SECRET_PATTERNS = [
|
|
|
69
69
|
// Anthropic / OpenAI-style long-lived keys that do not start with `sk-`.
|
|
70
70
|
[/\bxai-[A-Za-z0-9]{16,}\b/g, '[REDACTED_SECRET]'],
|
|
71
71
|
[/\bgsk_[A-Za-z0-9]{16,}\b/g, '[REDACTED_SECRET]'],
|
|
72
|
+
[
|
|
73
|
+
// Uppercase environment assignments with a secret-shaped suffix, e.g.
|
|
74
|
+
// `export X_CLIENT_SECRET=…` / `CLIENT_SECRET=…`. The generic lower-case
|
|
75
|
+
// field rules cannot match these because the underscore is a word
|
|
76
|
+
// character, so no boundary exists right before `client_secret` (issue #241).
|
|
77
|
+
/\b((?:[A-Z][A-Z0-9_]*_)?(?:CLIENT_SECRET|ACCESS_TOKEN|AUTH_TOKEN|SECRET_KEY|PRIVATE_KEY|SESSION_KEY|ENCRYPTION_KEY|DB_PASSWORD|API_KEY|API_TOKEN|CREDENTIALS?|SECRETS?)\s*=\s*)(["']?)[^\s"',;]+/gi,
|
|
78
|
+
'$1$2[REDACTED_SECRET]',
|
|
79
|
+
],
|
|
72
80
|
[/\bBearer\s+[A-Za-z0-9._~+/=-]{8,}/gi, 'Bearer [REDACTED_SECRET]'],
|
|
73
81
|
];
|
|
74
82
|
/** Field-name guard shared by browser/event and approval snapshot projections. */
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI option parsing for `orion web` (extracted from the CLI entry so the
|
|
3
|
+
* grammar has a unit-testable surface — issue #218 regression coverage).
|
|
4
|
+
*/
|
|
5
|
+
export declare const DEFAULT_WEB_PORT = 3080;
|
|
6
|
+
export interface WebCliOptions {
|
|
7
|
+
readonly port: number;
|
|
8
|
+
readonly open: boolean;
|
|
9
|
+
readonly cwd: string;
|
|
10
|
+
readonly background: boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare function parseWebCliOptions(args: readonly string[]): WebCliOptions;
|
|
13
|
+
//# sourceMappingURL=cli-options.d.ts.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* CLI option parsing for `orion web` (extracted from the CLI entry so the
|
|
4
|
+
* grammar has a unit-testable surface — issue #218 regression coverage).
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.DEFAULT_WEB_PORT = void 0;
|
|
8
|
+
exports.parseWebCliOptions = parseWebCliOptions;
|
|
9
|
+
exports.DEFAULT_WEB_PORT = 3080;
|
|
10
|
+
function parseWebCliOptions(args) {
|
|
11
|
+
let port = exports.DEFAULT_WEB_PORT;
|
|
12
|
+
let open = true;
|
|
13
|
+
let cwd = process.cwd();
|
|
14
|
+
let background = false;
|
|
15
|
+
for (let index = 0; index < args.length; index++) {
|
|
16
|
+
const argument = args[index];
|
|
17
|
+
if (argument === '--background' || argument === '--daemon') {
|
|
18
|
+
background = true;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (argument === '--no-open') {
|
|
22
|
+
open = false;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (argument === '--port' || argument.startsWith('--port=')) {
|
|
26
|
+
const portValue = argument === '--port' ? args[++index] : argument.slice('--port='.length);
|
|
27
|
+
if (!portValue || !/^\d+$/u.test(portValue)) {
|
|
28
|
+
throw new Error('--port must be an integer from 0 through 65535.');
|
|
29
|
+
}
|
|
30
|
+
port = Number(portValue);
|
|
31
|
+
if (!Number.isSafeInteger(port) || port < 0 || port > 65535) {
|
|
32
|
+
throw new Error('--port must be an integer from 0 through 65535.');
|
|
33
|
+
}
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (argument === '--cwd' || argument.startsWith('--cwd=')) {
|
|
37
|
+
const cwdValue = argument === '--cwd' ? args[++index] : argument.slice('--cwd='.length);
|
|
38
|
+
if (!cwdValue?.trim())
|
|
39
|
+
throw new Error('--cwd requires a directory path.');
|
|
40
|
+
cwd = cwdValue;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
throw new Error(`Unknown orion web option: ${argument}`);
|
|
44
|
+
}
|
|
45
|
+
return { port, open, cwd, background };
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=cli-options.js.map
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native background/daemon lifecycle for `orion web` (issue #247, S1+S2).
|
|
3
|
+
*
|
|
4
|
+
* A foreground `orion web --background` parent spawns the real host detached,
|
|
5
|
+
* waits for it to publish a pidfile under the config home, then returns. The
|
|
6
|
+
* detached child redirects its stdio to per-host log files and keeps the
|
|
7
|
+
* fail-closed approval / context / sandbox semantics untouched — this is a
|
|
8
|
+
* process-lifecycle concern only, still bound to 127.0.0.1.
|
|
9
|
+
*
|
|
10
|
+
* The pidfile shape is intentionally tiny and human-readable:
|
|
11
|
+
*
|
|
12
|
+
* { "pid": 12345, "port": 3080, "url": "http://127.0.0.1:3080",
|
|
13
|
+
* "startedAt": 1725000000000, "workspace": "/abs/path" }
|
|
14
|
+
*/
|
|
15
|
+
import { type ChildProcess } from 'child_process';
|
|
16
|
+
export declare const HOST_PIDFILE_BASE = "web-host";
|
|
17
|
+
export declare const HOST_DAEMON_CHILD_ENV = "ORION_WEB_DAEMON_CHILD";
|
|
18
|
+
export declare const DAEMON_READY_TIMEOUT_MS = 8000;
|
|
19
|
+
export declare const DAEMON_POLL_MS = 120;
|
|
20
|
+
export interface HostPidfile {
|
|
21
|
+
readonly pid: number;
|
|
22
|
+
readonly port: number;
|
|
23
|
+
readonly url: string;
|
|
24
|
+
readonly workspace: string;
|
|
25
|
+
readonly startedAt: number;
|
|
26
|
+
}
|
|
27
|
+
export type HostDaemonStatus = {
|
|
28
|
+
readonly state: 'running';
|
|
29
|
+
readonly pidfile: HostPidfile;
|
|
30
|
+
} | {
|
|
31
|
+
readonly state: 'stopped';
|
|
32
|
+
} | {
|
|
33
|
+
readonly state: 'stale';
|
|
34
|
+
readonly pid: number;
|
|
35
|
+
readonly port: number;
|
|
36
|
+
};
|
|
37
|
+
export declare function hostLogsDirectory(): string;
|
|
38
|
+
export declare function hostPidfilePath(port: number): string;
|
|
39
|
+
export declare function parseHostPidfile(raw: string): HostPidfile | null;
|
|
40
|
+
export declare function readHostPidfile(port: number): HostPidfile | null;
|
|
41
|
+
export declare function writeHostPidfile(pidfile: HostPidfile): void;
|
|
42
|
+
export declare function clearHostPidfile(port: number): void;
|
|
43
|
+
/** Cross-platform process-liveness probe used by `status`. */
|
|
44
|
+
export declare function isProcessAlive(pid: number): boolean;
|
|
45
|
+
export declare function hostDaemonStatus(port: number): HostDaemonStatus;
|
|
46
|
+
/**
|
|
47
|
+
* Parent-side entry for `--background`: spawn the host detached, redirect its
|
|
48
|
+
* stdio to `~/.orion-code/logs/web-<port>.log`, and wait for the pidfile so the
|
|
49
|
+
* foreground command can print the URL and exit cleanly.
|
|
50
|
+
*/
|
|
51
|
+
export declare function spawnBackgroundHost(options: {
|
|
52
|
+
readonly cwd: string;
|
|
53
|
+
readonly port: number;
|
|
54
|
+
readonly webArgs: readonly string[];
|
|
55
|
+
}): Promise<{
|
|
56
|
+
readonly child: ChildProcess;
|
|
57
|
+
readonly pidfile: HostPidfile;
|
|
58
|
+
}>;
|
|
59
|
+
/** Terminate a background host by pidfile (SIGTERM, then escalate to SIGKILL). */
|
|
60
|
+
export declare function stopBackgroundHost(port: number): Promise<'stopped' | 'not_running'>;
|
|
61
|
+
//# sourceMappingURL=host-daemon.d.ts.map
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DAEMON_POLL_MS = exports.DAEMON_READY_TIMEOUT_MS = exports.HOST_DAEMON_CHILD_ENV = exports.HOST_PIDFILE_BASE = void 0;
|
|
4
|
+
exports.hostLogsDirectory = hostLogsDirectory;
|
|
5
|
+
exports.hostPidfilePath = hostPidfilePath;
|
|
6
|
+
exports.parseHostPidfile = parseHostPidfile;
|
|
7
|
+
exports.readHostPidfile = readHostPidfile;
|
|
8
|
+
exports.writeHostPidfile = writeHostPidfile;
|
|
9
|
+
exports.clearHostPidfile = clearHostPidfile;
|
|
10
|
+
exports.isProcessAlive = isProcessAlive;
|
|
11
|
+
exports.hostDaemonStatus = hostDaemonStatus;
|
|
12
|
+
exports.spawnBackgroundHost = spawnBackgroundHost;
|
|
13
|
+
exports.stopBackgroundHost = stopBackgroundHost;
|
|
14
|
+
/**
|
|
15
|
+
* Native background/daemon lifecycle for `orion web` (issue #247, S1+S2).
|
|
16
|
+
*
|
|
17
|
+
* A foreground `orion web --background` parent spawns the real host detached,
|
|
18
|
+
* waits for it to publish a pidfile under the config home, then returns. The
|
|
19
|
+
* detached child redirects its stdio to per-host log files and keeps the
|
|
20
|
+
* fail-closed approval / context / sandbox semantics untouched — this is a
|
|
21
|
+
* process-lifecycle concern only, still bound to 127.0.0.1.
|
|
22
|
+
*
|
|
23
|
+
* The pidfile shape is intentionally tiny and human-readable:
|
|
24
|
+
*
|
|
25
|
+
* { "pid": 12345, "port": 3080, "url": "http://127.0.0.1:3080",
|
|
26
|
+
* "startedAt": 1725000000000, "workspace": "/abs/path" }
|
|
27
|
+
*/
|
|
28
|
+
const child_process_1 = require("child_process");
|
|
29
|
+
const fs_1 = require("fs");
|
|
30
|
+
const path_1 = require("path");
|
|
31
|
+
const paths_1 = require("../product/paths");
|
|
32
|
+
exports.HOST_PIDFILE_BASE = 'web-host';
|
|
33
|
+
exports.HOST_DAEMON_CHILD_ENV = 'ORION_WEB_DAEMON_CHILD';
|
|
34
|
+
exports.DAEMON_READY_TIMEOUT_MS = 8000;
|
|
35
|
+
exports.DAEMON_POLL_MS = 120;
|
|
36
|
+
function hostLogsDirectory() {
|
|
37
|
+
return (0, path_1.join)((0, paths_1.getConfigHome)(), 'logs');
|
|
38
|
+
}
|
|
39
|
+
function hostPidfilePath(port) {
|
|
40
|
+
return (0, path_1.join)((0, paths_1.getConfigHome)(), `${exports.HOST_PIDFILE_BASE}.${port}.pid`);
|
|
41
|
+
}
|
|
42
|
+
function parseHostPidfile(raw) {
|
|
43
|
+
try {
|
|
44
|
+
const value = JSON.parse(raw);
|
|
45
|
+
if (!value || typeof value !== 'object')
|
|
46
|
+
return null;
|
|
47
|
+
const record = value;
|
|
48
|
+
if (typeof record.pid !== 'number' ||
|
|
49
|
+
typeof record.port !== 'number' ||
|
|
50
|
+
typeof record.url !== 'string' ||
|
|
51
|
+
typeof record.workspace !== 'string' ||
|
|
52
|
+
typeof record.startedAt !== 'number') {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
return Object.freeze({
|
|
56
|
+
pid: record.pid,
|
|
57
|
+
port: record.port,
|
|
58
|
+
url: record.url,
|
|
59
|
+
workspace: record.workspace,
|
|
60
|
+
startedAt: record.startedAt,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function readHostPidfile(port) {
|
|
68
|
+
const path = hostPidfilePath(port);
|
|
69
|
+
if (!(0, fs_1.existsSync)(path))
|
|
70
|
+
return null;
|
|
71
|
+
try {
|
|
72
|
+
return parseHostPidfile((0, fs_1.readFileSync)(path, 'utf8'));
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function writeHostPidfile(pidfile) {
|
|
79
|
+
(0, fs_1.mkdirSync)((0, paths_1.getConfigHome)(), { recursive: true });
|
|
80
|
+
(0, fs_1.writeFileSync)(hostPidfilePath(pidfile.port), JSON.stringify(pidfile, null, 2), 'utf8');
|
|
81
|
+
}
|
|
82
|
+
function clearHostPidfile(port) {
|
|
83
|
+
try {
|
|
84
|
+
(0, fs_1.rmSync)(hostPidfilePath(port), { force: true });
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// Pidfile removal is best-effort: a locked/restricted filesystem must not
|
|
88
|
+
// turn `orion web stop` into a failure after the process is already gone.
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/** Cross-platform process-liveness probe used by `status`. */
|
|
92
|
+
function isProcessAlive(pid) {
|
|
93
|
+
try {
|
|
94
|
+
process.kill(pid, 0);
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
return error.code === 'EPERM';
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function hostDaemonStatus(port) {
|
|
102
|
+
const pidfile = readHostPidfile(port);
|
|
103
|
+
if (!pidfile)
|
|
104
|
+
return { state: 'stopped' };
|
|
105
|
+
if (isProcessAlive(pidfile.pid)) {
|
|
106
|
+
return { state: 'running', pidfile };
|
|
107
|
+
}
|
|
108
|
+
return { state: 'stale', pid: pidfile.pid, port };
|
|
109
|
+
}
|
|
110
|
+
/** Build the argument vector used to re-launch the current CLI detached. */
|
|
111
|
+
function childArgs(webArgs) {
|
|
112
|
+
const entry = process.argv[1];
|
|
113
|
+
if (!entry)
|
|
114
|
+
throw new Error('Unable to determine the CLI entry to relaunch.');
|
|
115
|
+
return [entry, 'web', ...webArgs];
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Parent-side entry for `--background`: spawn the host detached, redirect its
|
|
119
|
+
* stdio to `~/.orion-code/logs/web-<port>.log`, and wait for the pidfile so the
|
|
120
|
+
* foreground command can print the URL and exit cleanly.
|
|
121
|
+
*/
|
|
122
|
+
function spawnBackgroundHost(options) {
|
|
123
|
+
const logsDirectory = hostLogsDirectory();
|
|
124
|
+
(0, fs_1.mkdirSync)(logsDirectory, { recursive: true });
|
|
125
|
+
const logPath = (0, path_1.join)(logsDirectory, `web-${options.port}.log`);
|
|
126
|
+
const output = (0, fs_1.openSync)(logPath, 'a');
|
|
127
|
+
const child = (0, child_process_1.spawn)(process.execPath, childArgs(options.webArgs), {
|
|
128
|
+
cwd: options.cwd,
|
|
129
|
+
detached: true,
|
|
130
|
+
stdio: ['ignore', output, output],
|
|
131
|
+
env: { ...process.env, [exports.HOST_DAEMON_CHILD_ENV]: '1' },
|
|
132
|
+
});
|
|
133
|
+
child.unref();
|
|
134
|
+
return new Promise((resolve, reject) => {
|
|
135
|
+
const deadline = Date.now() + exports.DAEMON_READY_TIMEOUT_MS;
|
|
136
|
+
const poll = () => {
|
|
137
|
+
const pidfile = readHostPidfile(options.port);
|
|
138
|
+
if (pidfile && pidfile.pid === child.pid) {
|
|
139
|
+
resolve({ child, pidfile });
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
143
|
+
reject(new Error(`Background host exited before readiness (code ${child.exitCode ?? 'signal'}); see ${logPath}`));
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (Date.now() > deadline) {
|
|
147
|
+
reject(new Error(`Background host did not become ready within ${exports.DAEMON_READY_TIMEOUT_MS}ms; see ${logPath}`));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
setTimeout(poll, exports.DAEMON_POLL_MS);
|
|
151
|
+
};
|
|
152
|
+
poll();
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
/** Terminate a background host by pidfile (SIGTERM, then escalate to SIGKILL). */
|
|
156
|
+
function stopBackgroundHost(port) {
|
|
157
|
+
const status = hostDaemonStatus(port);
|
|
158
|
+
if (status.state !== 'running')
|
|
159
|
+
return Promise.resolve('not_running');
|
|
160
|
+
const { pidfile } = status;
|
|
161
|
+
return new Promise(resolve => {
|
|
162
|
+
try {
|
|
163
|
+
process.kill(pidfile.pid, 'SIGTERM');
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
clearHostPidfile(port);
|
|
167
|
+
resolve('stopped');
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const deadline = Date.now() + 4000;
|
|
171
|
+
const poll = () => {
|
|
172
|
+
if (!isProcessAlive(pidfile.pid)) {
|
|
173
|
+
clearHostPidfile(port);
|
|
174
|
+
resolve('stopped');
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (Date.now() > deadline) {
|
|
178
|
+
try {
|
|
179
|
+
process.kill(pidfile.pid, 'SIGKILL');
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
// Already gone.
|
|
183
|
+
}
|
|
184
|
+
clearHostPidfile(port);
|
|
185
|
+
resolve('stopped');
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
setTimeout(poll, 100);
|
|
189
|
+
};
|
|
190
|
+
poll();
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
//# sourceMappingURL=host-daemon.js.map
|
package/dist/web/launch.d.ts
CHANGED
|
@@ -2,6 +2,9 @@ export interface RunOrionWebOptions {
|
|
|
2
2
|
readonly cwd: string;
|
|
3
3
|
readonly port?: number;
|
|
4
4
|
readonly open?: boolean;
|
|
5
|
+
readonly background?: boolean;
|
|
6
|
+
/** Full `orion web` arguments of the relaunched child (excludes --background). */
|
|
7
|
+
readonly backgroundArgs?: readonly string[];
|
|
5
8
|
readonly stdout?: Pick<NodeJS.WriteStream, 'write'>;
|
|
6
9
|
readonly stderr?: Pick<NodeJS.WriteStream, 'write'>;
|
|
7
10
|
}
|
package/dist/web/launch.js
CHANGED
|
@@ -3,10 +3,34 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.runOrionWeb = runOrionWeb;
|
|
4
4
|
const child_process_1 = require("child_process");
|
|
5
5
|
const server_1 = require("./server");
|
|
6
|
+
const host_daemon_1 = require("./host-daemon");
|
|
6
7
|
async function runOrionWeb(options) {
|
|
7
8
|
const stdout = options.stdout ?? process.stdout;
|
|
8
9
|
const stderr = options.stderr ?? process.stderr;
|
|
10
|
+
const isDaemonChild = process.env[host_daemon_1.HOST_DAEMON_CHILD_ENV] === '1';
|
|
11
|
+
const port = options.port ?? 3080;
|
|
12
|
+
if (options.background && !isDaemonChild) {
|
|
13
|
+
// Parent side: launch detached and wait for its pidfile, then print the URL.
|
|
14
|
+
const { pidfile } = await (0, host_daemon_1.spawnBackgroundHost)({
|
|
15
|
+
cwd: options.cwd,
|
|
16
|
+
port,
|
|
17
|
+
webArgs: options.backgroundArgs ?? [],
|
|
18
|
+
});
|
|
19
|
+
stdout.write(`Orion Code Web Workbench running in the background: ${pidfile.url}\n`);
|
|
20
|
+
stdout.write(`pid ${pidfile.pid} · logs ~/.orion-code/logs/web-${port}.log\n`);
|
|
21
|
+
stdout.write(`Manage it with: orion web status|stop [--port ${port}]\n`);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
9
24
|
const handle = await (0, server_1.startOrionWebServer)({ cwd: options.cwd, port: options.port });
|
|
25
|
+
if (isDaemonChild) {
|
|
26
|
+
(0, host_daemon_1.writeHostPidfile)({
|
|
27
|
+
pid: process.pid,
|
|
28
|
+
port: handle.port,
|
|
29
|
+
url: handle.url,
|
|
30
|
+
workspace: handle.workbench.workspace,
|
|
31
|
+
startedAt: Date.now(),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
10
34
|
stdout.write(`Orion Code Web Workbench: ${handle.url}\n`);
|
|
11
35
|
stdout.write(`Workspace: ${handle.workbench.workspace}\n`);
|
|
12
36
|
if (options.open !== false) {
|