@logicseek/hello-cc 0.1.8 → 0.1.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 +58 -0
- package/README.md +8 -1
- package/README.zh-CN.md +5 -0
- package/bin/hcc.mjs +43 -9
- package/docs/commands.md +3 -1
- package/docs/commands.zh-CN.md +3 -1
- package/docs/guide.md +14 -2
- package/docs/guide.zh-CN.md +12 -1
- package/lib/core/sessions/launch.mjs +2 -0
- package/lib/integrations/shims/script.mjs +28 -6
- package/lib/runtime/client.mjs +1 -1
- package/lib/runtime/state.mjs +6 -3
- package/lib/web/http.mjs +8 -1
- package/package.json +1 -1
- package/scripts/regression.mjs +253 -3
package/CHANGELOG.md
CHANGED
|
@@ -16,6 +16,64 @@ the release description from the current changelog section. Use
|
|
|
16
16
|
`npm run release:github` with `GH_TOKEN` or `GITHUB_TOKEN` only for local
|
|
17
17
|
backfills.
|
|
18
18
|
|
|
19
|
+
## 0.1.9
|
|
20
|
+
|
|
21
|
+
### Summary
|
|
22
|
+
|
|
23
|
+
hello-cc 0.1.9 publishes the post-0.1.8 Web runtime and provider shim
|
|
24
|
+
hardening work. It tightens Web peer action identity, makes runtime cleanup
|
|
25
|
+
safer, and keeps Claude/Codex shims from attaching unrelated projects to a
|
|
26
|
+
global Web runtime.
|
|
27
|
+
|
|
28
|
+
### Highlights
|
|
29
|
+
|
|
30
|
+
- Hardened Web runtime cleanup so starting `hcc web` from a wrapper shell does
|
|
31
|
+
not terminate the current parent process chain while still removing stale
|
|
32
|
+
orphan runtimes for the same project.
|
|
33
|
+
- Returned structured `BAD_REQUEST` JSON for malformed Web API request bodies
|
|
34
|
+
instead of surfacing raw JSON parse failures.
|
|
35
|
+
- Replaced Web-runtime-token reuse for mutating peer actions with independent
|
|
36
|
+
per-session action tokens.
|
|
37
|
+
- Changed provider shims to fall back to the real Claude/Codex CLI when no
|
|
38
|
+
current-project Web runtime is available.
|
|
39
|
+
- Made provider shims use only the current project's `.hello-cc/runtime.json`
|
|
40
|
+
during managed launches, avoiding accidental attachment through a global Web
|
|
41
|
+
runtime from another project.
|
|
42
|
+
- Kept shim-only environment variables out of restarted provider sessions.
|
|
43
|
+
|
|
44
|
+
### Compatibility Notes
|
|
45
|
+
|
|
46
|
+
- No breaking CLI command changes are intended in this release.
|
|
47
|
+
- `hcc web --local` is still Web mode; use `hcc up` when you only want local
|
|
48
|
+
coordination commands without the Web console or shims.
|
|
49
|
+
- Starting `claude` or `codex` in a directory without a local
|
|
50
|
+
`.hello-cc/runtime.json` now runs the real provider CLI instead of implicitly
|
|
51
|
+
using a global hello-cc Web runtime.
|
|
52
|
+
|
|
53
|
+
### Validation
|
|
54
|
+
|
|
55
|
+
The 0.1.9 release should be validated with:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
git diff --check
|
|
59
|
+
node --check bin/hcc.mjs
|
|
60
|
+
find lib -name '*.mjs' -print0 | xargs -0 -n1 node --check
|
|
61
|
+
node --check scripts/github-release.mjs
|
|
62
|
+
node --check scripts/regression.mjs
|
|
63
|
+
node --check scripts/release-notes.mjs
|
|
64
|
+
npm run release:check
|
|
65
|
+
npm run release:github:dry-run -- --version 0.1.9
|
|
66
|
+
npm pack --dry-run --json
|
|
67
|
+
npm publish --dry-run --registry=https://registry.npmjs.org/ --access public
|
|
68
|
+
npm test
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The expected full regression marker is:
|
|
72
|
+
|
|
73
|
+
```text
|
|
74
|
+
FULL_REGRESSION_OK
|
|
75
|
+
```
|
|
76
|
+
|
|
19
77
|
## 0.1.8
|
|
20
78
|
|
|
21
79
|
### Summary
|
package/README.md
CHANGED
|
@@ -107,6 +107,9 @@ local: http://127.0.0.1:8787/?token=<saved-token>&project=/path/to/project
|
|
|
107
107
|
Use `--local` to bind only to `127.0.0.1`, or `--port N` to request a specific
|
|
108
108
|
port. `hcc web` initializes the project bus, installs Claude/Codex hooks and
|
|
109
109
|
shims, starts or reuses the Web console, and returns the terminal to you.
|
|
110
|
+
`hcc web --local` is still Web mode; it only limits the listener to loopback.
|
|
111
|
+
Use `hcc up` when you want local coordination commands without the Web console
|
|
112
|
+
or shims.
|
|
110
113
|
|
|
111
114
|
After the first shim install, open a new terminal or reload the rc file for
|
|
112
115
|
your shell:
|
|
@@ -125,7 +128,11 @@ codex resume <session-id>
|
|
|
125
128
|
```
|
|
126
129
|
|
|
127
130
|
Those sessions become tmux-backed peers that can be seen and controlled from
|
|
128
|
-
Web while remaining usable from the local terminal.
|
|
131
|
+
Web while remaining usable from the local terminal. The shims only use the
|
|
132
|
+
current project's runtime file. If you start `claude` or `codex` in a directory
|
|
133
|
+
where `hcc web` has not created `.hello-cc/runtime.json`, the shim falls back to
|
|
134
|
+
the real provider CLI instead of using the global Web runtime or creating a new
|
|
135
|
+
project database.
|
|
129
136
|
|
|
130
137
|
## Basic Workflow
|
|
131
138
|
|
package/README.zh-CN.md
CHANGED
|
@@ -94,6 +94,8 @@ local: http://127.0.0.1:8787/?token=<saved-token>&project=/path/to/project
|
|
|
94
94
|
使用 `--local` 可以只绑定 `127.0.0.1`,使用 `--port N` 可以指定请求端口。
|
|
95
95
|
`hcc web` 会初始化项目总线,安装 Claude/Codex hooks 和 shims,启动或复用 Web
|
|
96
96
|
控制台,然后把终端还给你。
|
|
97
|
+
`hcc web --local` 仍然是 Web 模式,只是限制监听在本机 loopback。只想使用本地
|
|
98
|
+
协作命令、不启动 Web 控制台、不安装 shims 时,使用 `hcc up`。
|
|
97
99
|
|
|
98
100
|
第一次安装 shim 后,打开新终端,或根据当前 shell 重新加载配置:
|
|
99
101
|
|
|
@@ -111,6 +113,9 @@ codex resume <session-id>
|
|
|
111
113
|
```
|
|
112
114
|
|
|
113
115
|
这些会话会成为 tmux-backed peer,可以继续在本地终端里使用,也可以被 Web 控制台观察和操作。
|
|
116
|
+
shim 只使用当前项目自己的 runtime 文件。如果你在没有通过 `hcc web` 创建
|
|
117
|
+
`.hello-cc/runtime.json` 的目录里启动 `claude` 或 `codex`,shim 会回退到真实
|
|
118
|
+
provider CLI,不会使用全局 Web runtime,也不会为这个目录新建项目数据库。
|
|
114
119
|
|
|
115
120
|
## 基本流程
|
|
116
121
|
|
package/bin/hcc.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import fs from 'node:fs';
|
|
|
4
4
|
import http from 'node:http';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import process from 'node:process';
|
|
7
|
+
import { randomBytes } from 'node:crypto';
|
|
7
8
|
import { spawn, spawnSync } from 'node:child_process';
|
|
8
9
|
import { setTimeout as sleep } from 'node:timers/promises';
|
|
9
10
|
import { URL, fileURLToPath } from 'node:url';
|
|
@@ -1417,15 +1418,28 @@ async function cmdBroadcast(ctx, args) {
|
|
|
1417
1418
|
touchCurrentPeer(db, ctx, identity, null, 'shell');
|
|
1418
1419
|
const id = sendMessage(db, sender, 'all', taskId, kind, body);
|
|
1419
1420
|
let injected = 0;
|
|
1421
|
+
let skipped = 0;
|
|
1420
1422
|
if (opts.inject) {
|
|
1421
1423
|
const sessions = await runtimeRequest(ctx, 'GET', '/api/sessions', null, runtime);
|
|
1422
1424
|
const running = (sessions.sessions || []).filter((session) => session.status === 'running');
|
|
1423
1425
|
for (const session of running) {
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
+
try {
|
|
1427
|
+
await injectPeer(ctx, session.id, body, !opts['no-enter'], runtime, sender);
|
|
1428
|
+
injected += 1;
|
|
1429
|
+
} catch (err) {
|
|
1430
|
+
if (err instanceof CliError && ['NOT_FOUND', 'SESSION_NOT_RUNNING', 'TMUX_ERROR'].includes(err.code)) {
|
|
1431
|
+
skipped += 1;
|
|
1432
|
+
continue;
|
|
1433
|
+
}
|
|
1434
|
+
throw err;
|
|
1435
|
+
}
|
|
1426
1436
|
}
|
|
1427
1437
|
}
|
|
1428
|
-
printResult(ctx, { id, sender, recipient: 'all', task_id: taskId, kind, body, injected }, (data) =>
|
|
1438
|
+
printResult(ctx, { id, sender, recipient: 'all', task_id: taskId, kind, body, injected, skipped }, (data) => {
|
|
1439
|
+
const injectedText = data.injected ? ` and injected ${data.injected} terminal(s)` : '';
|
|
1440
|
+
const skippedText = data.skipped ? `, skipped ${data.skipped} stale terminal(s)` : '';
|
|
1441
|
+
return `broadcast message #${data.id}${injectedText}${skippedText}`;
|
|
1442
|
+
});
|
|
1429
1443
|
}
|
|
1430
1444
|
|
|
1431
1445
|
async function injectPeer(ctx, peer, text, enter = true, runtime = null, auditActor = null) {
|
|
@@ -2044,11 +2058,21 @@ function hccWebProcessMatches(line, ctx) {
|
|
|
2044
2058
|
sameResolvedPath(global.db, ctx.dbPath);
|
|
2045
2059
|
}
|
|
2046
2060
|
|
|
2061
|
+
function currentProcessAncestorPids(ppidByPid) {
|
|
2062
|
+
const ancestors = new Set();
|
|
2063
|
+
let pid = process.ppid;
|
|
2064
|
+
while (Number.isFinite(pid) && pid > 0 && !ancestors.has(pid)) {
|
|
2065
|
+
ancestors.add(pid);
|
|
2066
|
+
pid = ppidByPid.get(pid);
|
|
2067
|
+
}
|
|
2068
|
+
return ancestors;
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2047
2071
|
async function stopOrphanWebRuntimes(ctx, keepPid = null) {
|
|
2048
2072
|
if (process.platform === 'win32') return;
|
|
2049
2073
|
let output = '';
|
|
2050
2074
|
try {
|
|
2051
|
-
output = spawnSync('ps', ['-eo', 'pid=,args='], {
|
|
2075
|
+
output = spawnSync('ps', ['-eo', 'pid=,ppid=,args='], {
|
|
2052
2076
|
encoding: 'utf8',
|
|
2053
2077
|
stdio: ['ignore', 'pipe', 'ignore']
|
|
2054
2078
|
}).stdout || '';
|
|
@@ -2056,15 +2080,25 @@ async function stopOrphanWebRuntimes(ctx, keepPid = null) {
|
|
|
2056
2080
|
return;
|
|
2057
2081
|
}
|
|
2058
2082
|
|
|
2059
|
-
const
|
|
2083
|
+
const rows = [];
|
|
2084
|
+
const ppidByPid = new Map();
|
|
2060
2085
|
for (const line of output.split('\n')) {
|
|
2061
2086
|
const trimmed = line.trim();
|
|
2062
2087
|
if (!trimmed) continue;
|
|
2063
|
-
const match = trimmed.match(/^(\d+)\s+(.*)$/);
|
|
2088
|
+
const match = trimmed.match(/^(\d+)\s+(\d+)\s+(.*)$/);
|
|
2064
2089
|
if (!match) continue;
|
|
2065
2090
|
const pid = Number.parseInt(match[1], 10);
|
|
2066
|
-
|
|
2067
|
-
if (
|
|
2091
|
+
const ppid = Number.parseInt(match[2], 10);
|
|
2092
|
+
if (!Number.isFinite(pid)) continue;
|
|
2093
|
+
if (Number.isFinite(ppid)) ppidByPid.set(pid, ppid);
|
|
2094
|
+
rows.push({ pid, args: match[3] });
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
const ancestorPids = currentProcessAncestorPids(ppidByPid);
|
|
2098
|
+
const pids = [];
|
|
2099
|
+
for (const row of rows) {
|
|
2100
|
+
if (row.pid === process.pid || row.pid === keepPid || ancestorPids.has(row.pid)) continue;
|
|
2101
|
+
if (hccWebProcessMatches(row.args, ctx)) pids.push(row.pid);
|
|
2068
2102
|
}
|
|
2069
2103
|
if (!pids.length) return;
|
|
2070
2104
|
|
|
@@ -2250,7 +2284,7 @@ async function cmdWeb(ctx, args, startMeta = {}) {
|
|
|
2250
2284
|
const prepared = await prepareLocalBus(ctx, opts);
|
|
2251
2285
|
|
|
2252
2286
|
function newSessionActionToken() {
|
|
2253
|
-
return
|
|
2287
|
+
return randomBytes(32).toString('base64url');
|
|
2254
2288
|
}
|
|
2255
2289
|
|
|
2256
2290
|
function rememberProject(projectCtx) {
|
package/docs/commands.md
CHANGED
|
@@ -29,7 +29,9 @@ shims, starts or reuses the Web console, and returns the terminal to you. Bare
|
|
|
29
29
|
first use. Use `--local` to bind only `127.0.0.1`, `--token` or `HCC_WEB_TOKEN`
|
|
30
30
|
to replace the saved token, and
|
|
31
31
|
`--no-token` only in trusted local/test environments. Use `hcc up` only when you
|
|
32
|
-
want coordination without the Web console or shims.
|
|
32
|
+
want coordination without the Web console or shims. Provider shims only join
|
|
33
|
+
projects with a local `.hello-cc/runtime.json` from `hcc web`; they do not use a
|
|
34
|
+
global runtime to manage arbitrary directories.
|
|
33
35
|
|
|
34
36
|
## Peers And Status
|
|
35
37
|
|
package/docs/commands.zh-CN.md
CHANGED
|
@@ -26,7 +26,9 @@ hcc up [--no-discover] [--no-guidance]
|
|
|
26
26
|
Web 控制台,然后把终端还给你。裸 `hcc web` 会监听 `0.0.0.0` 并使用固定
|
|
27
27
|
URL token;首次使用时自动生成并保存。用 `--local` 可只绑定 `127.0.0.1`,
|
|
28
28
|
用 `--token` 或 `HCC_WEB_TOKEN` 可替换保存的 token,只有在可信本地/测试环境才使用 `--no-token`。
|
|
29
|
-
只想使用本地协作、不需要 Web 或 shims 时,再使用 `hcc up`。
|
|
29
|
+
只想使用本地协作、不需要 Web 或 shims 时,再使用 `hcc up`。provider shim 只会加入
|
|
30
|
+
已经由 `hcc web` 生成本地 `.hello-cc/runtime.json` 的项目,不会使用全局 runtime
|
|
31
|
+
去管理任意目录。
|
|
30
32
|
|
|
31
33
|
## Peers 和状态
|
|
32
34
|
|
package/docs/guide.md
CHANGED
|
@@ -104,6 +104,10 @@ The wrappers create or reuse local persistent tmux terminals:
|
|
|
104
104
|
- The local terminal attaches to the same tmux session and remains usable.
|
|
105
105
|
- The web console controls the same tmux pane, not a temporary browser-only
|
|
106
106
|
terminal.
|
|
107
|
+
- Shims only use the current project's `.hello-cc/runtime.json`. A global Web
|
|
108
|
+
runtime does not make arbitrary directories managed; if the current project
|
|
109
|
+
has not run `hcc web`, `claude` and `codex` fall back to the real provider CLI
|
|
110
|
+
without creating `.hello-cc/mesh.db`.
|
|
107
111
|
- `hcc down` stops only the web runtime. It does not kill tmux sessions.
|
|
108
112
|
- Running `hcc web` again reattaches live recorded tmux panes.
|
|
109
113
|
|
|
@@ -118,11 +122,14 @@ hcc up
|
|
|
118
122
|
```
|
|
119
123
|
|
|
120
124
|
Most users should start with `hcc web`.
|
|
125
|
+
`hcc web --local` is still Web mode; it binds the Web runtime only to
|
|
126
|
+
`127.0.0.1`.
|
|
121
127
|
|
|
122
128
|
## Project Boundary
|
|
123
129
|
|
|
124
|
-
By default, the project boundary is the current working directory.
|
|
125
|
-
|
|
130
|
+
By default, the project boundary is the current working directory. Explicit
|
|
131
|
+
`hcc` commands such as `hcc web`, `hcc up`, `hcc task`, or `hcc peer start`
|
|
132
|
+
create the project database on first use:
|
|
126
133
|
|
|
127
134
|
```text
|
|
128
135
|
/repo-a/.hello-cc/mesh.db
|
|
@@ -132,6 +139,11 @@ current directory has no `.hello-cc/mesh.db`, hello-cc creates one on first use:
|
|
|
132
139
|
Those are two different projects by default. Sessions communicate naturally
|
|
133
140
|
when they start from the same project path.
|
|
134
141
|
|
|
142
|
+
Provider shims are stricter: direct `claude` and `codex` commands only join
|
|
143
|
+
hello-cc when the current project already has a local `.hello-cc/runtime.json`
|
|
144
|
+
from `hcc web`. They do not use `~/.hello-cc/runtime.json` to auto-register or
|
|
145
|
+
create databases for unrelated directories.
|
|
146
|
+
|
|
135
147
|
To share one bus across paths or worktrees, opt in explicitly:
|
|
136
148
|
|
|
137
149
|
```bash
|
package/docs/guide.zh-CN.md
CHANGED
|
@@ -95,6 +95,9 @@ codex resume <session-id>
|
|
|
95
95
|
|
|
96
96
|
- 本地终端会 attach 到同一个 tmux 会话,仍然可以像普通 Claude/Codex 一样交互。
|
|
97
97
|
- Web 控制台看到并操作的是同一个 tmux pane,不是浏览器里临时创建的假终端。
|
|
98
|
+
- shim 只使用当前项目的 `.hello-cc/runtime.json`。全局 Web runtime 不会让任意
|
|
99
|
+
目录自动进入管理;如果当前项目没有运行过 `hcc web`,`claude` 和 `codex`
|
|
100
|
+
会回退到真实 provider CLI,不会创建 `.hello-cc/mesh.db`。
|
|
98
101
|
- `hcc down` 只停止 Web runtime,不会杀掉这些 tmux 会话。
|
|
99
102
|
- 重新运行 `hcc web` 后,只要 tmux pane 还活着,Web 会重新接管。
|
|
100
103
|
|
|
@@ -108,10 +111,14 @@ hcc up
|
|
|
108
111
|
```
|
|
109
112
|
|
|
110
113
|
普通用户优先使用 `hcc web`。
|
|
114
|
+
`hcc web --local` 仍然是 Web 模式,只是把 Web runtime 限制为监听
|
|
115
|
+
`127.0.0.1`。
|
|
111
116
|
|
|
112
117
|
## 项目边界
|
|
113
118
|
|
|
114
|
-
|
|
119
|
+
默认项目边界就是当前工作目录。显式运行 `hcc web`、`hcc up`、`hcc task` 或
|
|
120
|
+
`hcc peer start` 这类 `hcc` 命令时,如果当前目录没有 `.hello-cc/mesh.db`,
|
|
121
|
+
hello-cc 会在首次使用时创建:
|
|
115
122
|
|
|
116
123
|
```text
|
|
117
124
|
/repo-a/.hello-cc/mesh.db
|
|
@@ -120,6 +127,10 @@ hcc up
|
|
|
120
127
|
|
|
121
128
|
这两个目录默认是两个不同项目,不会互相看到任务和消息。想让多个终端自然通信,就在同一个项目目录启动它们。
|
|
122
129
|
|
|
130
|
+
provider shim 更严格:直接运行 `claude` 和 `codex` 时,只有当前项目已经通过
|
|
131
|
+
`hcc web` 生成本地 `.hello-cc/runtime.json` 才会加入 hello-cc。它们不会使用
|
|
132
|
+
`~/.hello-cc/runtime.json` 去自动注册无关目录,也不会为这些目录创建数据库。
|
|
133
|
+
|
|
123
134
|
如果确实要让多个路径或 worktree 共用一个总线,需要显式指定:
|
|
124
135
|
|
|
125
136
|
```bash
|
|
@@ -14,6 +14,7 @@ const LAUNCH_ENV_IGNORED_KEYS = new Set([
|
|
|
14
14
|
'HCC_NO_AUTO_INSTALL_TMUX',
|
|
15
15
|
'HCC_PEER',
|
|
16
16
|
'HCC_ROOT',
|
|
17
|
+
'HCC_RUNTIME_LOCAL_ONLY',
|
|
17
18
|
'HCC_SKIP_SHIM_INSTALL',
|
|
18
19
|
PROVIDER_STATE_ENV,
|
|
19
20
|
'HCC_SHIM_ENSURED',
|
|
@@ -38,6 +39,7 @@ const LAUNCH_ENV_IGNORED_KEYS = new Set([
|
|
|
38
39
|
export function childSessionEnv(extra = {}, baseEnv = process.env) {
|
|
39
40
|
const env = { ...(baseEnv || {}), ...extra };
|
|
40
41
|
delete env[WEB_CHILD_ENV];
|
|
42
|
+
delete env.HCC_RUNTIME_LOCAL_ONLY;
|
|
41
43
|
return env;
|
|
42
44
|
}
|
|
43
45
|
|
|
@@ -139,14 +139,22 @@ detect_current_hcc_pane() {
|
|
|
139
139
|
return 1
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
hcc_runtime_unavailable_start_failure() {
|
|
143
|
+
case "$1" in
|
|
144
|
+
*"No running web runtime found"*|*"No running hello-cc web runtime found"*|*"Runtime is not reachable"*|*"RUNTIME_NOT_RUNNING"*|*"RUNTIME_UNREACHABLE"*|*"RUNTIME_START_FAILED"*|*"RUNTIME_START_TIMEOUT"*)
|
|
145
|
+
return 0
|
|
146
|
+
;;
|
|
147
|
+
esac
|
|
148
|
+
return 1
|
|
149
|
+
}
|
|
150
|
+
|
|
142
151
|
if detect_current_hcc_pane; then
|
|
143
152
|
SESSION_PEER_ID=$(printf '%s' "$CURRENT_HCC_SESSION" | sed 's/^hcc-[^-]*-//')
|
|
144
153
|
if [ -n "$SESSION_PEER_ID" ] && [ "$SESSION_PEER_ID" != "$CURRENT_HCC_SESSION" ]; then
|
|
145
154
|
PEER_ID="$SESSION_PEER_ID"
|
|
146
155
|
fi
|
|
147
156
|
export HCC_PEER="$PEER_ID"
|
|
148
|
-
|
|
149
|
-
"$HCC_BIN" peer attach "$PEER_ID" --kind "${tool.kind}" --pane "$CURRENT_HCC_PANE" --cwd "$PWD" --force >/dev/null 2>&1 || true
|
|
157
|
+
HCC_RUNTIME_LOCAL_ONLY=1 "$HCC_BIN" peer attach "$PEER_ID" --kind "${tool.kind}" --pane "$CURRENT_HCC_PANE" --cwd "$PWD" --force >/dev/null 2>&1 || true
|
|
150
158
|
exec "$REAL_BIN" "$@"
|
|
151
159
|
fi
|
|
152
160
|
|
|
@@ -155,12 +163,15 @@ HCC_PROVIDER_LAUNCH_SCRIPT='REAL_BIN="$1"; shift; HCC_TMUX_BIN="\${HCC_TMUX_BIN:
|
|
|
155
163
|
set +e
|
|
156
164
|
# Wrap in shell so pane_pid stays as bash; Ctrl+C drops back to bash
|
|
157
165
|
START_OUT_FILE=$(mktemp "\${TMPDIR:-/tmp}/hcc-shim-start.XXXXXX")
|
|
158
|
-
(
|
|
166
|
+
(HCC_RUNTIME_LOCAL_ONLY=1 "$HCC_BIN" peer start "$PEER_ID" --kind "${tool.kind}" --restart-env -- bash -lc "$HCC_PROVIDER_LAUNCH_SCRIPT" _ "$REAL_BIN" "$@") >"$START_OUT_FILE" 2>&1
|
|
159
167
|
START_STATUS=$?
|
|
160
168
|
START_OUT=$(cat "$START_OUT_FILE" 2>/dev/null || true)
|
|
161
169
|
rm -f "$START_OUT_FILE"
|
|
162
170
|
set -e
|
|
163
171
|
if [ "$START_STATUS" -ne 0 ]; then
|
|
172
|
+
if hcc_runtime_unavailable_start_failure "$START_OUT"; then
|
|
173
|
+
exec "$REAL_BIN" "$@"
|
|
174
|
+
fi
|
|
164
175
|
printf '%s\\n' "$START_OUT" >&2
|
|
165
176
|
exit "$START_STATUS"
|
|
166
177
|
fi
|
|
@@ -352,14 +363,22 @@ detect_current_hcc_pane() {
|
|
|
352
363
|
return 1
|
|
353
364
|
}
|
|
354
365
|
|
|
366
|
+
hcc_runtime_unavailable_start_failure() {
|
|
367
|
+
case "$1" in
|
|
368
|
+
*"No running web runtime found"*|*"No running hello-cc web runtime found"*|*"Runtime is not reachable"*|*"RUNTIME_NOT_RUNNING"*|*"RUNTIME_UNREACHABLE"*|*"RUNTIME_START_FAILED"*|*"RUNTIME_START_TIMEOUT"*)
|
|
369
|
+
return 0
|
|
370
|
+
;;
|
|
371
|
+
esac
|
|
372
|
+
return 1
|
|
373
|
+
}
|
|
374
|
+
|
|
355
375
|
if detect_current_hcc_pane; then
|
|
356
376
|
SESSION_PEER_ID=$(printf '%s' "$CURRENT_HCC_SESSION" | sed 's/^hcc-[^-]*-//')
|
|
357
377
|
if [ -n "$SESSION_PEER_ID" ] && [ "$SESSION_PEER_ID" != "$CURRENT_HCC_SESSION" ]; then
|
|
358
378
|
PEER_ID="$SESSION_PEER_ID"
|
|
359
379
|
fi
|
|
360
380
|
export HCC_PEER="$PEER_ID"
|
|
361
|
-
|
|
362
|
-
"$HCC_BIN" peer attach "$PEER_ID" --kind "${tool.kind}" --pane "$CURRENT_HCC_PANE" --cwd "$PWD" --force >/dev/null 2>&1 || true
|
|
381
|
+
HCC_RUNTIME_LOCAL_ONLY=1 "$HCC_BIN" peer attach "$PEER_ID" --kind "${tool.kind}" --pane "$CURRENT_HCC_PANE" --cwd "$PWD" --force >/dev/null 2>&1 || true
|
|
363
382
|
exec "$REAL_BIN" "$@"
|
|
364
383
|
fi
|
|
365
384
|
|
|
@@ -368,12 +387,15 @@ HCC_PROVIDER_LAUNCH_SCRIPT='REAL_BIN="$1"; shift; HCC_TMUX_BIN="\${HCC_TMUX_BIN:
|
|
|
368
387
|
set +e
|
|
369
388
|
# Wrap in shell so pane_pid stays as bash; Ctrl+C drops back to bash
|
|
370
389
|
START_OUT_FILE=$(mktemp "\${TMPDIR:-/tmp}/hcc-shim-start.XXXXXX")
|
|
371
|
-
(
|
|
390
|
+
(HCC_RUNTIME_LOCAL_ONLY=1 "$HCC_BIN" peer start "$PEER_ID" --kind "${tool.kind}" --restart-env -- bash -lc "$HCC_PROVIDER_LAUNCH_SCRIPT" _ "$REAL_BIN" "$@") >"$START_OUT_FILE" 2>&1
|
|
372
391
|
START_STATUS=$?
|
|
373
392
|
START_OUT=$(cat "$START_OUT_FILE" 2>/dev/null || true)
|
|
374
393
|
rm -f "$START_OUT_FILE"
|
|
375
394
|
set -e
|
|
376
395
|
if [ "$START_STATUS" -ne 0 ]; then
|
|
396
|
+
if hcc_runtime_unavailable_start_failure "$START_OUT"; then
|
|
397
|
+
exec "$REAL_BIN" "$@"
|
|
398
|
+
fi
|
|
377
399
|
printf '%s\\n' "$START_OUT" >&2
|
|
378
400
|
exit "$START_STATUS"
|
|
379
401
|
fi
|
package/lib/runtime/client.mjs
CHANGED
|
@@ -6,7 +6,7 @@ const DEFAULT_CLI_NAME = 'hcc';
|
|
|
6
6
|
|
|
7
7
|
export async function runtimeRequest(ctx, method, route, body = null, runtime = null, opts = {}) {
|
|
8
8
|
const cliName = opts.cliName || DEFAULT_CLI_NAME;
|
|
9
|
-
const rt = runtime || readRuntime(ctx, { cliName });
|
|
9
|
+
const rt = runtime || readRuntime(ctx, { cliName, localOnly: Boolean(opts.localOnly) });
|
|
10
10
|
const url = runtimeApiUrl(rt, route);
|
|
11
11
|
const headers = { 'Content-Type': 'application/json' };
|
|
12
12
|
headers['X-HCC-Root'] = ctx.root;
|
package/lib/runtime/state.mjs
CHANGED
|
@@ -39,6 +39,7 @@ export function writeRuntime(ctx, runtime) {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
export function readRuntime(ctx, opts = {}) {
|
|
42
|
+
const localOnly = Boolean(opts.localOnly || process.env.HCC_RUNTIME_LOCAL_ONLY === '1');
|
|
42
43
|
if (process.env.HCC_RUNTIME_URL) {
|
|
43
44
|
return {
|
|
44
45
|
base_url: process.env.HCC_RUNTIME_URL,
|
|
@@ -56,9 +57,11 @@ export function readRuntime(ctx, opts = {}) {
|
|
|
56
57
|
try { fs.rmSync(file, { force: true }); } catch {}
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
if (!localOnly) {
|
|
61
|
+
const global = readGlobalRuntimeFile();
|
|
62
|
+
if (global?.base_url) {
|
|
63
|
+
return { ...global, source: globalRuntimePath(), global: true };
|
|
64
|
+
}
|
|
62
65
|
}
|
|
63
66
|
const productName = opts.productName || DEFAULT_PRODUCT_NAME;
|
|
64
67
|
const cliName = opts.cliName || DEFAULT_CLI_NAME;
|
package/lib/web/http.mjs
CHANGED
|
@@ -19,7 +19,14 @@ function readRequestBody(req, limit = 1024 * 1024) {
|
|
|
19
19
|
export async function readJsonRequest(req) {
|
|
20
20
|
const body = await readRequestBody(req);
|
|
21
21
|
if (!body.trim()) return {};
|
|
22
|
-
|
|
22
|
+
try {
|
|
23
|
+
return JSON.parse(body);
|
|
24
|
+
} catch (err) {
|
|
25
|
+
if (err instanceof SyntaxError) {
|
|
26
|
+
throw new CliError('BAD_REQUEST', 'Invalid JSON request body');
|
|
27
|
+
}
|
|
28
|
+
throw err;
|
|
29
|
+
}
|
|
23
30
|
}
|
|
24
31
|
|
|
25
32
|
export function sendHttp(res, status, contentType, body) {
|
package/package.json
CHANGED
package/scripts/regression.mjs
CHANGED
|
@@ -1216,6 +1216,197 @@ function libModuleFiles() {
|
|
|
1216
1216
|
.map((name) => path.join('lib', name));
|
|
1217
1217
|
}
|
|
1218
1218
|
|
|
1219
|
+
function assertShimRuntimeUnavailableFallback(generateShim) {
|
|
1220
|
+
const fallbackDir = fs.mkdtempSync(path.join(os.tmpdir(), `hcc-shim-fallback-${testId}-`));
|
|
1221
|
+
try {
|
|
1222
|
+
const fakeHcc = path.join(fallbackDir, 'hcc');
|
|
1223
|
+
fs.writeFileSync(fakeHcc, `#!/usr/bin/env bash
|
|
1224
|
+
case "\${1:-}" in
|
|
1225
|
+
shim)
|
|
1226
|
+
exit 64
|
|
1227
|
+
;;
|
|
1228
|
+
find-root)
|
|
1229
|
+
if [ -n "\${HCC_FAKE_ROOT:-}" ]; then
|
|
1230
|
+
printf '%s\\n' "$HCC_FAKE_ROOT"
|
|
1231
|
+
exit 0
|
|
1232
|
+
fi
|
|
1233
|
+
exit 1
|
|
1234
|
+
;;
|
|
1235
|
+
web)
|
|
1236
|
+
exit 1
|
|
1237
|
+
;;
|
|
1238
|
+
peer)
|
|
1239
|
+
if [ "\${2:-}" = "start" ]; then
|
|
1240
|
+
printf '%s\\n' "hcc: No running web runtime found. Start hcc web first, then hcc peer start \${3:-peer}" >&2
|
|
1241
|
+
exit 1
|
|
1242
|
+
fi
|
|
1243
|
+
exit 1
|
|
1244
|
+
;;
|
|
1245
|
+
esac
|
|
1246
|
+
exit 1
|
|
1247
|
+
`, { mode: 0o755 });
|
|
1248
|
+
|
|
1249
|
+
const cases = [
|
|
1250
|
+
{
|
|
1251
|
+
tool: { name: 'claude', kind: 'claude', resumeFlag: '--resume' },
|
|
1252
|
+
args: ['--resume', 'missing-web-session'],
|
|
1253
|
+
expected: 'real-claude --resume missing-web-session'
|
|
1254
|
+
},
|
|
1255
|
+
{
|
|
1256
|
+
tool: { name: 'codex', kind: 'codex' },
|
|
1257
|
+
args: ['resume', 'missing-web-session'],
|
|
1258
|
+
expected: 'real-codex resume missing-web-session'
|
|
1259
|
+
}
|
|
1260
|
+
];
|
|
1261
|
+
|
|
1262
|
+
for (const entry of cases) {
|
|
1263
|
+
const realBin = path.join(fallbackDir, `real-${entry.tool.name}`);
|
|
1264
|
+
const shimBin = path.join(fallbackDir, entry.tool.name);
|
|
1265
|
+
const logFile = path.join(fallbackDir, `${entry.tool.name}.log`);
|
|
1266
|
+
fs.writeFileSync(realBin, `#!/usr/bin/env bash
|
|
1267
|
+
printf 'real-${entry.tool.name}'
|
|
1268
|
+
for arg in "$@"; do printf ' %s' "$arg"; done
|
|
1269
|
+
printf '\\n'
|
|
1270
|
+
if [ -n "\${HCC_FAKE_LOG:-}" ]; then
|
|
1271
|
+
printf 'real-${entry.tool.name}' >> "$HCC_FAKE_LOG"
|
|
1272
|
+
for arg in "$@"; do printf ' %s' "$arg" >> "$HCC_FAKE_LOG"; done
|
|
1273
|
+
printf '\\n' >> "$HCC_FAKE_LOG"
|
|
1274
|
+
fi
|
|
1275
|
+
`, { mode: 0o755 });
|
|
1276
|
+
fs.writeFileSync(shimBin, generateShim(fakeHcc, realBin, entry.tool), { mode: 0o755 });
|
|
1277
|
+
|
|
1278
|
+
const result = runMaybe(shimBin, entry.args, {
|
|
1279
|
+
cwd: fallbackDir,
|
|
1280
|
+
env: {
|
|
1281
|
+
...env,
|
|
1282
|
+
HCC_FAKE_ROOT: fallbackDir,
|
|
1283
|
+
HCC_FAKE_LOG: logFile
|
|
1284
|
+
}
|
|
1285
|
+
});
|
|
1286
|
+
if (result.status !== 0) {
|
|
1287
|
+
fail(`${entry.tool.name} shim did not fall back when runtime was unavailable:\nstdout=${result.stdout}\nstderr=${result.stderr}`);
|
|
1288
|
+
}
|
|
1289
|
+
if (!String(result.stdout || '').includes(entry.expected)) {
|
|
1290
|
+
fail(`${entry.tool.name} shim fallback did not launch the real provider:\nstdout=${result.stdout}\nstderr=${result.stderr}`);
|
|
1291
|
+
}
|
|
1292
|
+
const logOutput = fs.existsSync(logFile) ? fs.readFileSync(logFile, 'utf8') : '';
|
|
1293
|
+
if (!logOutput.includes(entry.expected)) {
|
|
1294
|
+
fail(`${entry.tool.name} shim fallback did not preserve provider argv:\n${logOutput}`);
|
|
1295
|
+
}
|
|
1296
|
+
if (String(result.stderr || '').includes('No running web runtime found')) {
|
|
1297
|
+
fail(`${entry.tool.name} shim leaked hcc runtime failure instead of transparent fallback:\n${result.stderr}`);
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
} finally {
|
|
1301
|
+
try { fs.rmSync(fallbackDir, { recursive: true, force: true }); } catch {}
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
async function assertShimIgnoresGlobalRuntime(generateShim) {
|
|
1306
|
+
const testDir = fs.mkdtempSync(path.join(os.tmpdir(), `hcc-shim-global-runtime-${testId}-`));
|
|
1307
|
+
const fakeHome = path.join(testDir, 'home');
|
|
1308
|
+
const projectRoot = path.join(testDir, 'project-without-runtime');
|
|
1309
|
+
const requests = [];
|
|
1310
|
+
let server = null;
|
|
1311
|
+
try {
|
|
1312
|
+
fs.mkdirSync(path.join(fakeHome, '.hello-cc'), { recursive: true });
|
|
1313
|
+
fs.mkdirSync(projectRoot, { recursive: true });
|
|
1314
|
+
|
|
1315
|
+
server = http.createServer((req, res) => {
|
|
1316
|
+
requests.push({ method: req.method, url: req.url });
|
|
1317
|
+
res.setHeader('Content-Type', 'application/json');
|
|
1318
|
+
if (req.method === 'GET' && req.url.startsWith('/api/runtime')) {
|
|
1319
|
+
res.end(JSON.stringify({ ok: true }));
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1322
|
+
if (req.method === 'POST' && req.url.startsWith('/api/sessions')) {
|
|
1323
|
+
res.end(JSON.stringify({ session: {
|
|
1324
|
+
id: 'global-runtime-session',
|
|
1325
|
+
kind: 'claude',
|
|
1326
|
+
role: 'peer',
|
|
1327
|
+
status: 'running',
|
|
1328
|
+
pane: '%900'
|
|
1329
|
+
} }));
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
res.statusCode = 404;
|
|
1333
|
+
res.end(JSON.stringify({ error: { code: 'NOT_FOUND', message: 'not found' } }));
|
|
1334
|
+
});
|
|
1335
|
+
await new Promise((resolve, reject) => {
|
|
1336
|
+
server.once('error', reject);
|
|
1337
|
+
server.listen(0, '127.0.0.1', resolve);
|
|
1338
|
+
});
|
|
1339
|
+
const address = server.address();
|
|
1340
|
+
const globalRuntime = {
|
|
1341
|
+
product: 'hello-cc',
|
|
1342
|
+
version: 'regression',
|
|
1343
|
+
pid: process.pid,
|
|
1344
|
+
root: testDir,
|
|
1345
|
+
db: path.join(testDir, '.hello-cc', 'mesh.db'),
|
|
1346
|
+
host: '127.0.0.1',
|
|
1347
|
+
port: address.port,
|
|
1348
|
+
base_url: `http://127.0.0.1:${address.port}`,
|
|
1349
|
+
token: '',
|
|
1350
|
+
started_at: Math.floor(Date.now() / 1000),
|
|
1351
|
+
global_runtime: true
|
|
1352
|
+
};
|
|
1353
|
+
fs.writeFileSync(path.join(fakeHome, '.hello-cc', 'runtime.json'), JSON.stringify(globalRuntime, null, 2));
|
|
1354
|
+
|
|
1355
|
+
const hccWrapper = path.join(testDir, 'hcc-wrapper');
|
|
1356
|
+
fs.writeFileSync(hccWrapper, `#!/usr/bin/env bash\nexec ${sh(process.execPath)} ${sh(hccBin)} "$@"\n`, { mode: 0o755 });
|
|
1357
|
+
|
|
1358
|
+
const cases = [
|
|
1359
|
+
{
|
|
1360
|
+
tool: { name: 'claude', kind: 'claude', resumeFlag: '--resume' },
|
|
1361
|
+
args: ['--resume', 'global-runtime-session'],
|
|
1362
|
+
expected: 'real-claude --resume global-runtime-session'
|
|
1363
|
+
},
|
|
1364
|
+
{
|
|
1365
|
+
tool: { name: 'codex', kind: 'codex' },
|
|
1366
|
+
args: ['resume', 'global-runtime-session'],
|
|
1367
|
+
expected: 'real-codex resume global-runtime-session'
|
|
1368
|
+
}
|
|
1369
|
+
];
|
|
1370
|
+
|
|
1371
|
+
for (const entry of cases) {
|
|
1372
|
+
requests.length = 0;
|
|
1373
|
+
fs.rmSync(path.join(projectRoot, '.hello-cc'), { recursive: true, force: true });
|
|
1374
|
+
const realBin = path.join(testDir, `real-${entry.tool.name}`);
|
|
1375
|
+
const shimBin = path.join(testDir, entry.tool.name);
|
|
1376
|
+
fs.writeFileSync(realBin, `#!/usr/bin/env bash
|
|
1377
|
+
printf 'real-${entry.tool.name}'
|
|
1378
|
+
for arg in "$@"; do printf ' %s' "$arg"; done
|
|
1379
|
+
printf '\\n'
|
|
1380
|
+
`, { mode: 0o755 });
|
|
1381
|
+
fs.writeFileSync(shimBin, generateShim(hccWrapper, realBin, entry.tool), { mode: 0o755 });
|
|
1382
|
+
const result = runMaybe(shimBin, entry.args, {
|
|
1383
|
+
cwd: projectRoot,
|
|
1384
|
+
env: {
|
|
1385
|
+
...env,
|
|
1386
|
+
HOME: fakeHome,
|
|
1387
|
+
HCC_SHIM_ENSURED: '1',
|
|
1388
|
+
HCC_SHIM_NO_ATTACH: '1'
|
|
1389
|
+
}
|
|
1390
|
+
});
|
|
1391
|
+
if (result.status !== 0) {
|
|
1392
|
+
fail(`${entry.tool.name} shim failed while ignoring global runtime:\nstdout=${result.stdout}\nstderr=${result.stderr}`);
|
|
1393
|
+
}
|
|
1394
|
+
if (!String(result.stdout || '').includes(entry.expected)) {
|
|
1395
|
+
fail(`${entry.tool.name} shim used global runtime instead of falling back:\nstdout=${result.stdout}\nstderr=${result.stderr}`);
|
|
1396
|
+
}
|
|
1397
|
+
if (requests.length) {
|
|
1398
|
+
fail(`${entry.tool.name} shim contacted global runtime despite project-local restriction:\n${JSON.stringify(requests, null, 2)}`);
|
|
1399
|
+
}
|
|
1400
|
+
if (fs.existsSync(path.join(projectRoot, '.hello-cc', 'mesh.db'))) {
|
|
1401
|
+
fail(`${entry.tool.name} shim created a project database while only a global runtime existed`);
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
} finally {
|
|
1405
|
+
if (server?.listening) await new Promise((resolve) => server.close(resolve));
|
|
1406
|
+
try { fs.rmSync(testDir, { recursive: true, force: true }); } catch {}
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1219
1410
|
async function expectBoundedTmuxStream(label) {
|
|
1220
1411
|
let nodes = [];
|
|
1221
1412
|
await waitFor(() => {
|
|
@@ -1260,6 +1451,38 @@ function startRuntime(options = {}) {
|
|
|
1260
1451
|
if (!output.includes('web started in background')) fail(`hcc web did not report background start:\n${output}`);
|
|
1261
1452
|
}
|
|
1262
1453
|
|
|
1454
|
+
async function assertWebWrapperParentSurvives() {
|
|
1455
|
+
const wrapperRoot = fs.mkdtempSync(path.join(os.tmpdir(), `hcc-reg-wrapper-root-${testId}-`));
|
|
1456
|
+
const wrapperPort = port + 101;
|
|
1457
|
+
let wrapperPid = null;
|
|
1458
|
+
try {
|
|
1459
|
+
const script = [
|
|
1460
|
+
'set -e',
|
|
1461
|
+
`out=$(${sh(process.execPath)} ${sh(hccBin)} --root ${sh(wrapperRoot)} web --local --port ${wrapperPort} --no-discover --no-guidance)`,
|
|
1462
|
+
`printf '%s\\n' "$out"`
|
|
1463
|
+
].join('\n');
|
|
1464
|
+
const result = runMaybe('bash', ['-lc', script], { env });
|
|
1465
|
+
if (result.status !== 0) {
|
|
1466
|
+
fail(`hcc web killed or failed inside a wrapper shell:\nstdout=${result.stdout}\nstderr=${result.stderr}\nstatus=${result.status}\nsignal=${result.signal}`);
|
|
1467
|
+
}
|
|
1468
|
+
const match = String(result.stdout || '').match(/^pid:\s*(\d+)/m);
|
|
1469
|
+
if (!match) fail(`wrapper hcc web did not print background pid:\n${result.stdout}`);
|
|
1470
|
+
wrapperPid = Number.parseInt(match[1], 10);
|
|
1471
|
+
ensureFile(path.join(wrapperRoot, '.hello-cc', 'runtime.json'));
|
|
1472
|
+
} finally {
|
|
1473
|
+
runMaybe(process.execPath, [hccBin, '--root', wrapperRoot, 'down'], { env });
|
|
1474
|
+
if (wrapperPid) {
|
|
1475
|
+
try {
|
|
1476
|
+
await waitForProcessExit(wrapperPid, 'wrapper runtime process exit', 5000);
|
|
1477
|
+
} catch {
|
|
1478
|
+
try { process.kill(wrapperPid, 'SIGTERM'); } catch {}
|
|
1479
|
+
try { await waitForProcessExit(wrapperPid, 'wrapper runtime process exit after SIGTERM', 2000); } catch {}
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
try { fs.rmSync(wrapperRoot, { recursive: true, force: true }); } catch {}
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1263
1486
|
async function stopRuntime() {
|
|
1264
1487
|
if (!runtimePid) return;
|
|
1265
1488
|
const pid = runtimePid;
|
|
@@ -1420,6 +1643,7 @@ async function setupRegression() {
|
|
|
1420
1643
|
fail(`shim ensure preserved stale real binary instead of rediscovering PATH binary:\n${rediscoveredShim}`);
|
|
1421
1644
|
}
|
|
1422
1645
|
await stopRuntime();
|
|
1646
|
+
await assertWebWrapperParentSurvives();
|
|
1423
1647
|
assertPeerBindingUniqueConstraints();
|
|
1424
1648
|
assertLegacySchemaMigration();
|
|
1425
1649
|
assertRegisteredProjectDbMigration();
|
|
@@ -1442,6 +1666,18 @@ async function setupRegression() {
|
|
|
1442
1666
|
if (unauthorizedResponse.status !== 401) fail(`default web API allowed missing token: ${unauthorizedResponse.status}`);
|
|
1443
1667
|
const tokenResponse = await runtimeFetch('/api/runtime');
|
|
1444
1668
|
if (!tokenResponse.ok) fail(`default web API rejected runtime token: ${tokenResponse.status}`);
|
|
1669
|
+
const badJsonResponse = await runtimeFetch('/api/projects', {
|
|
1670
|
+
method: 'POST',
|
|
1671
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1672
|
+
body: '{bad json'
|
|
1673
|
+
});
|
|
1674
|
+
if (badJsonResponse.status !== 400) {
|
|
1675
|
+
fail(`bad JSON request returned ${badJsonResponse.status}, expected 400:\n${await badJsonResponse.text()}`);
|
|
1676
|
+
}
|
|
1677
|
+
const badJsonBody = await badJsonResponse.json();
|
|
1678
|
+
if (badJsonBody?.error?.code !== 'BAD_REQUEST') {
|
|
1679
|
+
fail(`bad JSON request did not return BAD_REQUEST:\n${JSON.stringify(badJsonBody, null, 2)}`);
|
|
1680
|
+
}
|
|
1445
1681
|
const tokenFile = path.join(home, '.hello-cc', 'web-token');
|
|
1446
1682
|
ensureFile(tokenFile, tokenRuntime.token);
|
|
1447
1683
|
fs.rmSync(tokenFile, { force: true });
|
|
@@ -2251,6 +2487,10 @@ async function multiProjectWebWorkflow() {
|
|
|
2251
2487
|
if (!managedActionSession.action_token) {
|
|
2252
2488
|
fail(`managed web session did not include action token:\n${JSON.stringify(managedActionSession, null, 2)}`);
|
|
2253
2489
|
}
|
|
2490
|
+
const runtimeActionToken = currentRuntime().token || '';
|
|
2491
|
+
if (managedActionSession.action_token === runtimeActionToken || managedActionSession.action_token.length < 32) {
|
|
2492
|
+
fail(`managed web session action token is not an independent session token:\n${JSON.stringify({ runtimeActionToken, managedActionSession }, null, 2)}`);
|
|
2493
|
+
}
|
|
2254
2494
|
const actionNext = await (await runtimeFetch(`/api/peers/${encodeURIComponent(managedActionSession.peer_id || managedActionSession.id)}/actions/task-next`, {
|
|
2255
2495
|
method: 'POST',
|
|
2256
2496
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -2537,6 +2777,10 @@ async function shimTmuxWorkflow() {
|
|
|
2537
2777
|
}
|
|
2538
2778
|
|
|
2539
2779
|
log('[6/13] shim tmux-backed launch');
|
|
2780
|
+
if (!fs.existsSync(path.join(root, '.hello-cc', 'runtime.json'))) {
|
|
2781
|
+
startRuntime();
|
|
2782
|
+
}
|
|
2783
|
+
await waitRuntime();
|
|
2540
2784
|
const shim = path.join(home, '.hcc-shims', 'claude');
|
|
2541
2785
|
const codexShim = path.join(home, '.hcc-shims', 'codex');
|
|
2542
2786
|
const claudeVersion = run(shim, ['--version'], { cwd: root, env });
|
|
@@ -2608,8 +2852,8 @@ async function shimTmuxWorkflow() {
|
|
|
2608
2852
|
hcc(['inject', peer, `printf '%s\\n' "$HCC_REG_VALUE" > ${firstEnvFile}`]);
|
|
2609
2853
|
await waitForFile(firstEnvFile, 'shim-first', 'shim first env');
|
|
2610
2854
|
const shimInternalEnvFile = path.join(outDir, 'shim-internal-env');
|
|
2611
|
-
hcc(['inject', peer, `printf '%s|%s\\n' "\${HCC_SHIM_ENSURED-unset}" "\${HCC_SKIP_SHIM_INSTALL-unset}" > ${shimInternalEnvFile}`]);
|
|
2612
|
-
await waitForFile(shimInternalEnvFile, 'unset|unset', 'shim internal env cleanup');
|
|
2855
|
+
hcc(['inject', peer, `printf '%s|%s|%s\\n' "\${HCC_SHIM_ENSURED-unset}" "\${HCC_SKIP_SHIM_INSTALL-unset}" "\${HCC_RUNTIME_LOCAL_ONLY-unset}" > ${shimInternalEnvFile}`]);
|
|
2856
|
+
await waitForFile(shimInternalEnvFile, 'unset|unset|unset', 'shim internal env cleanup');
|
|
2613
2857
|
|
|
2614
2858
|
const restarted = run(shim, ['--resume', 'shim-regression-session'], {
|
|
2615
2859
|
cwd: root,
|
|
@@ -3339,9 +3583,13 @@ async function syntaxAndHelp() {
|
|
|
3339
3583
|
if (!integrationShimScriptSource.includes('function generateShim') ||
|
|
3340
3584
|
!integrationShimScriptSource.includes('hello-cc shim for ${tool.name}') ||
|
|
3341
3585
|
!integrationShimScriptSource.includes('HCC_SHIM_ENSURED') ||
|
|
3342
|
-
!integrationShimScriptSource.includes('
|
|
3586
|
+
!integrationShimScriptSource.includes('HCC_RUNTIME_LOCAL_ONLY') ||
|
|
3587
|
+
!integrationShimScriptSource.includes('hcc_runtime_unavailable_start_failure')) {
|
|
3343
3588
|
fail('integrations shim script module is missing expected generateShim wiring');
|
|
3344
3589
|
}
|
|
3590
|
+
if (integrationShimScriptSource.includes('HCC_SKIP_SHIM_INSTALL=1 "$HCC_BIN" web')) {
|
|
3591
|
+
fail('integration shim script still auto-starts hcc web instead of falling back to the real provider');
|
|
3592
|
+
}
|
|
3345
3593
|
if (!shellPathSource.includes('function installPathEntry') ||
|
|
3346
3594
|
!shellPathSource.includes('function uninstallPathEntry') ||
|
|
3347
3595
|
!shellPathSource.includes('.hcc-shims')) {
|
|
@@ -3374,6 +3622,8 @@ async function syntaxAndHelp() {
|
|
|
3374
3622
|
if (setupModule.SHIM_DIR !== integrationShims.SHIM_DIR) fail('setup shim dir export mismatch');
|
|
3375
3623
|
if (typeof integrationShimScript.generateShim !== 'function') fail('integrations shim script module missing generateShim export');
|
|
3376
3624
|
if (shimScriptCompat.generateShim !== integrationShimScript.generateShim) fail('shim script compatibility export mismatch');
|
|
3625
|
+
assertShimRuntimeUnavailableFallback(integrationShimScript.generateShim);
|
|
3626
|
+
await assertShimIgnoresGlobalRuntime(integrationShimScript.generateShim);
|
|
3377
3627
|
for (const name of ['installPathEntry', 'uninstallPathEntry']) {
|
|
3378
3628
|
if (typeof shellPath[name] !== 'function') fail(`shell path module missing export: ${name}`);
|
|
3379
3629
|
if (setupModule[name] !== shellPath[name]) fail(`setup shell path export mismatch: ${name}`);
|