@orion-agents/orion-code 0.3.7 → 0.3.10

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 CHANGED
@@ -22,6 +22,54 @@ which is **not** a pass.
22
22
 
23
23
  ## [Unreleased]
24
24
 
25
+ ## [0.3.10] — CANDIDATE
26
+
27
+ > **Status: candidate.** Not merged, tagged or published to npm.
28
+
29
+ ### Added
30
+
31
+ - The local Web host defaults to port **4242** (M42, the Orion Nebula) instead of
32
+ the generic 3080; `DEFAULT_WEB_PORT` is the single source shared by
33
+ cli-options, launch and server.
34
+ - `orion web start [--port N]`: idempotent background-host start (prints the URL
35
+ when already running); start/restart/`--background` share one arg builder.
36
+ - README documents the background-host lifecycle (`start|status|stop|restart`,
37
+ pidfile/log locations) and the launchd foreground-supervision recipe.
38
+
39
+ ## [0.3.9] — CANDIDATE
40
+
41
+ > **Status: npm-published.** `@orion-agents/orion-code@0.3.9` is available through the npm
42
+ > registry (latest tag); deterministic-bug-fix release + background host lifecycle
43
+ > (issue #247 S1/S2). Published 2026-09-04; git tag/release may lag.
44
+
45
+ ### Added
46
+
47
+ - `orion web --background`: starts the Web Workbench host detached from the
48
+ terminal, writes a pidfile (`~/.orion-code/web-host.<port>.pid`), redirects
49
+ logs to `~/.orion-code/logs/web-<port>.log` and prints URL/pid.
50
+ - `orion web status|stop|restart [--port <number>]`: pidfile-based host
51
+ management without third-party supervisors. Still binds 127.0.0.1 and keeps
52
+ the fail-closed approval/context/sandbox semantics unchanged (issue #247).
53
+
54
+ ### Fixed
55
+
56
+ - Issue-sweep triage: #222 (blank session rename), #223 (failed create leaves a
57
+ persisted active session), #234 (URL userinfo password starting with `token:`),
58
+ #238 (busy state-write clearing processing), #242 (stale Settings save into the
59
+ newly active workspace) were all already resolved on `main` — closed with
60
+ source evidence.
61
+ - Web redaction now covers uppercase X-prefixed environment assignments
62
+ (`X_CLIENT_SECRET=…`, `CLIENT_SECRET=…`, `AUTH_TOKEN=…`) whose secret suffix
63
+ hides behind underscore word characters (issue #241).
64
+ - Cmd/Ctrl+B can again open the Project Navigator at 761–799px: expanding the
65
+ navigation preference now resolves to a drawer and opens it explicitly instead
66
+ of leaving an invisible drawer (issue #226).
67
+
68
+ ### Tests
69
+
70
+ - `redaction-xprefixed-env` suite: X-prefixed env values, bare uppercase
71
+ assignments, #234 userinfo regression, #241 JSON keys.
72
+
25
73
  ## [0.3.7] — CANDIDATE
26
74
 
27
75
  > **Status: candidate.** This work is isolated on the codex/v0.3.7 worktree (left project
@@ -73,7 +121,6 @@ which is **not** a pass.
73
121
  112 cases green. E2E: `web-v037-session-lifecycle.spec.ts` (archive/restore, delete-confirm,
74
122
  tags round-trip) runs in CI.
75
123
 
76
-
77
124
  ## [0.3.6] — CANDIDATE
78
125
 
79
126
  > **Status: candidate.** This work is isolated on the codex/v0.3.6 worktree (pure web
@@ -111,7 +158,6 @@ which is **not** a pass.
111
158
  - Markdown heading off-by-one: `##` renders as `<h2>` (was `<h3>`), a lone `#` no longer
112
159
  collapses to `<h6>`; >6 hashes stay a plain paragraph per GFM.
113
160
 
114
-
115
161
  ## [0.3.5] — CANDIDATE
116
162
 
117
163
  > **Status: candidate.** This work is isolated on the codex/v0.3.5 worktree. It is not
package/README.md CHANGED
@@ -2,12 +2,10 @@
2
2
 
3
3
  Local-first, goal-driven coding agent for the terminal and browser.
4
4
 
5
- > v0.3.7 candidate — a left project-rail interaction release on top of the v0.3.6 work:
6
- > shortened connection copy, a keyboard-reachable per-Session overflow menu
7
- > (rename / tags / archive / delete with confirmation), persisted Session tags with
8
- > rail badges, an archived-Session section with restore, and a friendlier
9
- > "打开或新增项目" workspace dialog. Candidate source is not an npm publication,
10
- > Git tag, or merge-ready release.
5
+ > v0.3.10 candidate — the local Web host now defaults to port 4242 (M42, the
6
+ > Orion Nebula) with a first-class lifecycle: `orion web start|status|stop|restart`
7
+ > manage a detached host (pidfile + logs), and `--background` stays supported.
8
+ > Candidate source is not an npm publication, Git tag, or merge-ready release.
11
9
 
12
10
  [中文说明](README.zh-CN.md) ·
13
11
  [v0.3.7 plan](docs/plan/v0.3.7-left-rail-improvement-plan.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.7
173
+ npm install -g @orion-agents/orion-code@0.3.10
176
174
  orion --version
177
175
  orion doctor
178
176
  ```
@@ -260,6 +258,30 @@ atomic, revision-guarded Context transition. The left dock is 240–480px or a 4
260
258
  Narrow layouts use drawers without overwriting desktop widths. Files, Git and Review are read-only;
261
259
  terminal creation requires an explicit gesture and its ticket/output never enter Workbench SSE.
262
260
 
261
+ #### Background host & lifecycle (v0.3.10)
262
+
263
+ The Web Workbench binds **127.0.0.1 only**; the default port is **4242** (M42, the
264
+ Orion Nebula) so it never collides with common dev-tool ports.
265
+
266
+ ```bash
267
+ orion web # foreground; open http://127.0.0.1:4242
268
+ orion web --background # detached host (pidfile + logs, survives terminal exit)
269
+ orion web start [--port N] # idempotent background start
270
+ orion web status [--port N] # running pid/URL/workspace or stale report
271
+ orion web restart [--port N] # stop + start
272
+ orion web stop [--port N] # graceful SIGTERM stop
273
+ ```
274
+
275
+ The pidfile lives at `~/.orion-code/web-host.<port>.pid` and logs append to
276
+ `~/.orion-code/logs/web-<port>.log`. Backgrounding is a process-lifecycle feature
277
+ only: approval, context-revision and sandbox rules stay fail-closed, and the host
278
+ never binds beyond loopback.
279
+
280
+ For crash-restart and login autostart on macOS, register the **foreground** command
281
+ (`orion web --no-open --port 4242 --cwd <dir>`) in a launchd LaunchAgent
282
+ (`RunAtLoad` + `KeepAlive`) — do not combine it with `--background`, because
283
+ launchd must supervise the host process itself.
284
+
263
285
  #### Host-managed Settings
264
286
 
265
287
  The Settings dialog reads from the active Host rather than treating browser storage as a second
package/README.zh-CN.md CHANGED
@@ -2,10 +2,10 @@
2
2
 
3
3
  面向终端与浏览器、本地优先的目标驱动 Coding Agent。
4
4
 
5
- > v0.3.7 候选版本:左侧项目栏交互发布(基于 v0.3.6 之上)—— 状态文案精简、会话行键盘可达
6
- > 操作菜单(重命名 / 标签 / 归档 / 删除含确认)、可持久化会话标签与行内徽章、已归档会话区一键还原、
7
- > 以及更友好的「打开或新增项目」工作区对话框。Candidate 源码不代表已创建 npm 发布、Git tag 或达到
8
- > 可合并状态。
5
+ > v0.3.10 候选版本:本地 Web host 默认端口改为 **4242**(M42,猎户座大星云),
6
+ > 并新增一等生命周期管理:`orion web start|status|stop|restart` 管理脱离终端的
7
+ > 后台 host(pidfile + 日志),`--background` 仍可用。Candidate 源码不代表已创建
8
+ > npm 发布、Git tag 或达到可合并状态。
9
9
 
10
10
  [English](README.md) ·
11
11
  [v0.3.7 方案](docs/plan/v0.3.7-left-rail-improvement-plan.md) ·
@@ -102,7 +102,7 @@ Current。Node 20 已结束上游维护,不再属于 v0.3 Runtime 合同。
102
102
  当 npm 已存在不可变的 `0.3.4` 发布凭据后:
103
103
 
104
104
  ```bash
105
- npm install -g @orion-agents/orion-code@0.3.7
105
+ npm install -g @orion-agents/orion-code@0.3.10
106
106
  orion --version
107
107
  orion doctor
108
108
  ```
@@ -186,6 +186,23 @@ Host 时会 abort 并 fail-closed。
186
186
  键盘精细调宽。窄屏自动使用 drawer,且不覆盖桌面宽度偏好。文件、Git 和审阅保持只读;创建终端需要
187
187
  显式 user gesture,短期 ticket 与输出均不进入 Workbench SSE。
188
188
 
189
+ #### 后台运行与管理(v0.3.10)
190
+
191
+ Web Workbench 只绑定 **127.0.0.1**;默认端口为 **4242**(M42,猎户座大星云),避免与常用开发端口冲突。
192
+
193
+ ```bash
194
+ orion web # 前台启动;打开 http://127.0.0.1:4242
195
+ orion web --background # 脱离终端的后台 host(pidfile + 日志)
196
+ orion web start [--port N] # 幂等后台启动
197
+ orion web status [--port N] # 查看运行 pid/URL/workspace
198
+ orion web restart [--port N] # 重启
199
+ orion web stop [--port N] # 优雅停止
200
+ ```
201
+
202
+ pidfile 位于 `~/.orion-code/web-host.<port>.pid`,日志追加到 `~/.orion-code/logs/web-<port>.log`。后台化只涉及进程生命周期:审批/上下文版本/沙箱规则保持 fail-closed,host 绝不绑定局域网。
203
+
204
+ macOS 需要崩溃自动重启与开机自启时,用 launchd LaunchAgent 托管**前台**命令(`orion web --no-open --port 4242 --cwd <dir>`,`RunAtLoad`+`KeepAlive`)——不要与 `--background` 混用,launchd 必须直接监管 host 进程。
205
+
189
206
  #### Host 管理的 Settings
190
207
 
191
208
  Settings dialog 从当前 Host 读取设置,不把浏览器存储当成第二配置真源。主题和减少动效会跨刷新、端口
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,46 @@ 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 parseWebCliOptions(args) {
56
- let port = 3080;
57
- let open = true;
58
- let cwd = process.cwd();
59
- for (let index = 0; index < args.length; index++) {
60
- const argument = args[index];
61
- if (argument === '--no-open') {
62
- open = false;
63
- continue;
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 start [--port <number>] [--cwd <directory>] [--no-open]');
62
+ console.log('orion web status [--port <number>]');
63
+ console.log('orion web stop [--port <number>]');
64
+ console.log('orion web restart [--port <number>]');
65
+ console.log(' Manage a background host started with --background (start starts one).');
66
+ }
67
+ function webBackgroundArgs(options) {
68
+ return [
69
+ ...(options.open ? [] : ['--no-open']),
70
+ '--port',
71
+ String(options.port),
72
+ '--cwd',
73
+ options.cwd,
74
+ ];
75
+ }
76
+ async function manageBackgroundHost(command, args) {
77
+ const options = (0, cli_options_1.parseWebCliOptions)(args);
78
+ const status = (0, host_daemon_1.hostDaemonStatus)(options.port);
79
+ if (command === 'status') {
80
+ if (status.state === 'running') {
81
+ console.log(`orion web is running: ${status.pidfile.url} (pid ${status.pidfile.pid})`);
82
+ console.log(`workspace ${status.pidfile.workspace} · started ${new Date(status.pidfile.startedAt).toISOString()}`);
64
83
  }
65
- if (argument === '--port' || argument.startsWith('--port=')) {
66
- const portValue = argument === '--port' ? args[++index] : argument.slice('--port='.length);
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;
84
+ else if (status.state === 'stale') {
85
+ console.log(`orion web is not running (stale pidfile for pid ${status.pid} on port ${status.port}).`);
75
86
  }
76
- if (argument === '--cwd' || argument.startsWith('--cwd=')) {
77
- const cwdValue = argument === '--cwd' ? args[++index] : argument.slice('--cwd='.length);
78
- if (!cwdValue?.trim())
79
- throw new Error('--cwd requires a directory path.');
80
- cwd = cwdValue;
81
- continue;
87
+ else {
88
+ console.log(`orion web is not running on port ${options.port}.`);
82
89
  }
83
- throw new Error(`Unknown orion web option: ${argument}`);
90
+ return;
84
91
  }
85
- return { port, open, cwd };
86
- }
87
- function showWebHelp() {
88
- console.log('Usage: orion web [--port <number>] [--no-open] [--cwd <directory>]');
89
- console.log('Starts the local-only Web Workbench on 127.0.0.1.');
92
+ const result = await (0, host_daemon_1.stopBackgroundHost)(options.port);
93
+ if (result === 'stopped')
94
+ console.log(`orion web on port ${options.port} stopped.`);
95
+ else
96
+ console.log(`orion web was not running on port ${options.port}.`);
90
97
  }
91
98
  function parseCliOptions(args) {
92
99
  let uiRenderer = config_1.DEFAULT_UI_RENDERER;
@@ -159,7 +166,37 @@ async function main() {
159
166
  showWebHelp();
160
167
  return;
161
168
  }
162
- await (0, web_1.runOrionWeb)(parseWebCliOptions(args.slice(1)));
169
+ const rest = args.slice(1);
170
+ if (rest[0] === 'start') {
171
+ const startOptions = (0, cli_options_1.parseWebCliOptions)(rest.slice(1));
172
+ const current = (0, host_daemon_1.hostDaemonStatus)(startOptions.port);
173
+ if (current.state === 'running') {
174
+ console.log(`orion web is already running: ${current.pidfile.url} (pid ${current.pidfile.pid})`);
175
+ return;
176
+ }
177
+ await (0, web_1.runOrionWeb)({
178
+ ...startOptions,
179
+ background: true,
180
+ backgroundArgs: webBackgroundArgs(startOptions),
181
+ });
182
+ return;
183
+ }
184
+ if (rest[0] === 'status' || rest[0] === 'stop') {
185
+ await manageBackgroundHost(rest[0], rest.slice(1));
186
+ return;
187
+ }
188
+ if (rest[0] === 'restart') {
189
+ await manageBackgroundHost('stop', rest.slice(1));
190
+ const restartOptions = (0, cli_options_1.parseWebCliOptions)(rest.slice(1));
191
+ await (0, web_1.runOrionWeb)({
192
+ ...restartOptions,
193
+ background: true,
194
+ backgroundArgs: webBackgroundArgs(restartOptions),
195
+ });
196
+ return;
197
+ }
198
+ const options = (0, cli_options_1.parseWebCliOptions)(rest);
199
+ await (0, web_1.runOrionWeb)({ ...options, backgroundArgs: webBackgroundArgs(options) });
163
200
  return;
164
201
  }
165
202
  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,17 @@
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
+ /**
6
+ * M42 (Orion Nebula) — the local Web Workbench host binds this by default so
7
+ * `orion web` never collides with common dev-tool ports.
8
+ */
9
+ export declare const DEFAULT_WEB_PORT = 4242;
10
+ export interface WebCliOptions {
11
+ readonly port: number;
12
+ readonly open: boolean;
13
+ readonly cwd: string;
14
+ readonly background: boolean;
15
+ }
16
+ export declare function parseWebCliOptions(args: readonly string[]): WebCliOptions;
17
+ //# sourceMappingURL=cli-options.d.ts.map
@@ -0,0 +1,51 @@
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
+ /**
10
+ * M42 (Orion Nebula) — the local Web Workbench host binds this by default so
11
+ * `orion web` never collides with common dev-tool ports.
12
+ */
13
+ exports.DEFAULT_WEB_PORT = 4242;
14
+ function parseWebCliOptions(args) {
15
+ let port = exports.DEFAULT_WEB_PORT;
16
+ let open = true;
17
+ let cwd = process.cwd();
18
+ let background = false;
19
+ for (let index = 0; index < args.length; index++) {
20
+ const argument = args[index];
21
+ if (argument === '--background' || argument === '--daemon') {
22
+ background = true;
23
+ continue;
24
+ }
25
+ if (argument === '--no-open') {
26
+ open = false;
27
+ continue;
28
+ }
29
+ if (argument === '--port' || argument.startsWith('--port=')) {
30
+ const portValue = argument === '--port' ? args[++index] : argument.slice('--port='.length);
31
+ if (!portValue || !/^\d+$/u.test(portValue)) {
32
+ throw new Error('--port must be an integer from 0 through 65535.');
33
+ }
34
+ port = Number(portValue);
35
+ if (!Number.isSafeInteger(port) || port < 0 || port > 65535) {
36
+ throw new Error('--port must be an integer from 0 through 65535.');
37
+ }
38
+ continue;
39
+ }
40
+ if (argument === '--cwd' || argument.startsWith('--cwd=')) {
41
+ const cwdValue = argument === '--cwd' ? args[++index] : argument.slice('--cwd='.length);
42
+ if (!cwdValue?.trim())
43
+ throw new Error('--cwd requires a directory path.');
44
+ cwd = cwdValue;
45
+ continue;
46
+ }
47
+ throw new Error(`Unknown orion web option: ${argument}`);
48
+ }
49
+ return { port, open, cwd, background };
50
+ }
51
+ //# 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": 4242, "url": "http://127.0.0.1:4242",
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": 4242, "url": "http://127.0.0.1:4242",
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
@@ -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
  }