@logicseek/hello-cc 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +104 -0
- package/README.md +33 -9
- package/README.zh-CN.md +31 -9
- package/assets/screenshots/web-console.png +0 -0
- package/bin/hcc.mjs +749 -149
- package/docs/commands.md +13 -6
- package/docs/commands.zh-CN.md +11 -6
- package/docs/guide.md +10 -5
- package/docs/guide.zh-CN.md +8 -5
- package/lib/cli-runtime.mjs +2 -1
- package/lib/core/coordination/tasks.mjs +17 -1
- package/lib/core/coordination/timeline.mjs +11 -1
- package/lib/core/peers/identity.mjs +10 -2
- package/lib/core/sessions/launch.mjs +1 -0
- package/lib/integrations/shims/script.mjs +42 -22
- package/lib/integrations/shims.mjs +45 -6
- package/lib/setup.mjs +1 -0
- package/lib/shell-path.mjs +31 -16
- package/lib/ui/help.mjs +36 -1
- package/lib/web/peer-actions.mjs +42 -6
- package/lib/web/ui-template.mjs +5 -3
- package/package.json +1 -1
- package/scripts/regression.mjs +821 -97
package/docs/commands.md
CHANGED
|
@@ -12,7 +12,7 @@ hcc uninstall [--purge --yes]
|
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
`hcc update` updates the global npm install. `hcc uninstall` removes local
|
|
15
|
-
hooks and
|
|
15
|
+
hooks, shims, and the shell PATH entry; add `--purge --yes` only when you also want to remove the
|
|
16
16
|
current project's `.hello-cc` data and guidance blocks.
|
|
17
17
|
|
|
18
18
|
## Start And Stop
|
|
@@ -93,6 +93,8 @@ sender by default. Use `msg thread` to inspect the full thread for one message.
|
|
|
93
93
|
|
|
94
94
|
```text
|
|
95
95
|
hcc task create --title TEXT [--body TEXT] [--from ID] [--to ID] [--priority N]
|
|
96
|
+
hcc task dispatch --to ID --title TEXT [--body TEXT] [--from ID] [--message TEXT] [--no-inject] [--force]
|
|
97
|
+
hcc task dispatch --to ID --id N [--from ID] [--message TEXT] [--no-inject] [--force]
|
|
96
98
|
hcc task list [--status S] [--peer ID] [--all]
|
|
97
99
|
hcc task claim [--peer ID] --id N[,N] [--id N] [--ids N,N] [--force]
|
|
98
100
|
hcc task takeover [--peer ID] --id N --reason TEXT [--policy any|blocked|stale|blocked-or-stale] [--stale-after SECONDS]
|
|
@@ -107,11 +109,16 @@ Tasks are shared project facts. They remain visible until marked `done` or
|
|
|
107
109
|
task before claiming a new pending task; use `--force` only when intentionally
|
|
108
110
|
taking additional pending tasks, and combine it with `--count N` for explicit
|
|
109
111
|
batch claims. `task claim` also accepts repeated `--id` values and comma-separated
|
|
110
|
-
`--id` or `--ids` lists.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
112
|
+
`--id` or `--ids` lists. `task dispatch` is the explicit one-step form for
|
|
113
|
+
assigning a new or existing task to one peer, sending the durable task message,
|
|
114
|
+
and injecting the startup prompt only when that peer has a running managed
|
|
115
|
+
Claude/Codex terminal. Use `--no-inject` for message-only dispatch, and use
|
|
116
|
+
`--force` only when intentionally injecting while the target already owns another
|
|
117
|
+
active task. Use `task takeover` when explicitly taking a non-complete task from
|
|
118
|
+
another owner; it records the previous owner, requires a reason, and notifies
|
|
119
|
+
them. Add `--policy blocked`, `stale`, or `blocked-or-stale` when takeover
|
|
120
|
+
should only proceed under that auditable precondition. The default policy is
|
|
121
|
+
`any` for compatibility.
|
|
115
122
|
|
|
116
123
|
## Teams
|
|
117
124
|
|
package/docs/commands.zh-CN.md
CHANGED
|
@@ -10,7 +10,7 @@ hcc update [--tag TAG] [--registry URL] [--dry-run]
|
|
|
10
10
|
hcc uninstall [--purge --yes]
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
`hcc update` 更新全局 npm 安装。`hcc uninstall` 移除本机 hooks 和
|
|
13
|
+
`hcc update` 更新全局 npm 安装。`hcc uninstall` 移除本机 hooks、shims 和 shell PATH 配置;
|
|
14
14
|
只有在确定也要删除当前项目的 `.hello-cc` 数据和指导块时,才加
|
|
15
15
|
`--purge --yes`。
|
|
16
16
|
|
|
@@ -86,6 +86,8 @@ hcc broadcast MESSAGE [--from ID] [--task N] [--inject]
|
|
|
86
86
|
|
|
87
87
|
```text
|
|
88
88
|
hcc task create --title TEXT [--body TEXT] [--from ID] [--to ID] [--priority N]
|
|
89
|
+
hcc task dispatch --to ID --title TEXT [--body TEXT] [--from ID] [--message TEXT] [--no-inject] [--force]
|
|
90
|
+
hcc task dispatch --to ID --id N [--from ID] [--message TEXT] [--no-inject] [--force]
|
|
89
91
|
hcc task list [--status S] [--peer ID] [--all]
|
|
90
92
|
hcc task claim [--peer ID] --id N[,N] [--id N] [--ids N,N] [--force]
|
|
91
93
|
hcc task takeover [--peer ID] --id N --reason TEXT [--policy any|blocked|stale|blocked-or-stale] [--stale-after SECONDS]
|
|
@@ -98,11 +100,14 @@ hcc task done [--peer ID] --id N --summary TEXT
|
|
|
98
100
|
任务是项目共享事实。任务会一直可见,直到被标记为 `done` 或 `abandoned`。
|
|
99
101
|
`task next` 会优先返回当前 peer 已经认领、运行、审查或阻塞中的任务;只有明确
|
|
100
102
|
要再接 pending 任务时才使用 `--force`,并可结合 `--count N` 做显式批量认领。
|
|
101
|
-
`task claim` 也支持重复 `--id`、逗号分隔的 `--id` 或 `--ids`
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
103
|
+
`task claim` 也支持重复 `--id`、逗号分隔的 `--id` 或 `--ids` 列表。`task
|
|
104
|
+
dispatch` 是显式的一步分发:创建或指定已有任务并分配给目标 peer、发送持久任务
|
|
105
|
+
消息,并且只在目标 peer 有正在运行的托管 Claude/Codex 终端时注入启动提示。只想
|
|
106
|
+
发消息时使用 `--no-inject`;目标已经拥有其他活动任务但仍要注入时,才使用
|
|
107
|
+
`--force`。明确要从其他 owner 手里接管未完成任务时,使用 `task takeover`;它要求
|
|
108
|
+
填写 reason,会记录原 owner 并通知对方。需要更保守时,可加 `--policy blocked`、
|
|
109
|
+
`stale` 或 `blocked-or-stale`,要求任务符合可审计的阻塞/陈旧条件后才允许接管。
|
|
110
|
+
默认 policy 仍是 `any`,以保持兼容。
|
|
106
111
|
|
|
107
112
|
## 团队
|
|
108
113
|
|
package/docs/guide.md
CHANGED
|
@@ -21,7 +21,7 @@ hcc update
|
|
|
21
21
|
`hcc update --help` to see the available update options. `hcc task update`
|
|
22
22
|
updates a coordination task, not the installed package.
|
|
23
23
|
|
|
24
|
-
Remove hooks and
|
|
24
|
+
Remove hooks, shims, and the shell PATH entry from this machine:
|
|
25
25
|
|
|
26
26
|
```bash
|
|
27
27
|
hcc uninstall
|
|
@@ -83,11 +83,12 @@ with the same web console. The frontend Project selector switches between
|
|
|
83
83
|
registered roots. Each project still keeps its own `.hello-cc/mesh.db`, so
|
|
84
84
|
tasks, messages, locks, and peers stay isolated.
|
|
85
85
|
|
|
86
|
-
After the first shim install, open a new terminal or
|
|
86
|
+
After the first shim install, open a new terminal or reload the rc file for
|
|
87
|
+
your shell:
|
|
87
88
|
|
|
88
|
-
|
|
89
|
-
source ~/.
|
|
90
|
-
|
|
89
|
+
- bash: `source ~/.bashrc`
|
|
90
|
+
- zsh: `source ~/.zshrc`
|
|
91
|
+
- fish: `source ~/.config/fish/config.fish`
|
|
91
92
|
|
|
92
93
|
Then direct project-local commands are wrapped automatically:
|
|
93
94
|
|
|
@@ -106,6 +107,10 @@ The wrappers create or reuse local persistent tmux terminals:
|
|
|
106
107
|
- `hcc down` stops only the web runtime. It does not kill tmux sessions.
|
|
107
108
|
- Running `hcc web` again reattaches live recorded tmux panes.
|
|
108
109
|
|
|
110
|
+
Linux also has richer process auto-discovery through `/proc`. On macOS, prefer
|
|
111
|
+
shim-launched or `hcc peer start` sessions when you want reliable terminal
|
|
112
|
+
control and stable peer identity.
|
|
113
|
+
|
|
109
114
|
For local coordination without Web or shims, use:
|
|
110
115
|
|
|
111
116
|
```bash
|
package/docs/guide.zh-CN.md
CHANGED
|
@@ -20,7 +20,7 @@ hcc update
|
|
|
20
20
|
`hcc update` 会执行 `npm install -g @logicseek/hello-cc@latest`。使用
|
|
21
21
|
`hcc update --help` 可以查看更新选项。`hcc task update` 更新的是协作任务状态,不是安装包版本。
|
|
22
22
|
|
|
23
|
-
移除本机 hooks 和
|
|
23
|
+
移除本机 hooks、shims 和 shell PATH 配置:
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
26
|
hcc uninstall
|
|
@@ -76,11 +76,11 @@ stop: hcc down
|
|
|
76
76
|
|
|
77
77
|
`hcc web` 使用单个全局 Web runtime。你在另一个项目目录再次运行 `hcc web` 时,不会再开一个端口,而是把那个项目注册到同一个 Web 控制台。前端 Project 下拉框可以在不同项目 root 之间切换;每个项目仍然使用自己的 `.hello-cc/mesh.db`,任务、消息、锁和 peers 不会混在一起。
|
|
78
78
|
|
|
79
|
-
第一次安装 shim
|
|
79
|
+
第一次安装 shim 后,打开新终端,或根据当前 shell 重新加载配置:
|
|
80
80
|
|
|
81
|
-
|
|
82
|
-
source ~/.
|
|
83
|
-
|
|
81
|
+
- bash: `source ~/.bashrc`
|
|
82
|
+
- zsh: `source ~/.zshrc`
|
|
83
|
+
- fish: `source ~/.config/fish/config.fish`
|
|
84
84
|
|
|
85
85
|
之后在这个项目目录中直接输入:
|
|
86
86
|
|
|
@@ -98,6 +98,9 @@ codex resume <session-id>
|
|
|
98
98
|
- `hcc down` 只停止 Web runtime,不会杀掉这些 tmux 会话。
|
|
99
99
|
- 重新运行 `hcc web` 后,只要 tmux pane 还活着,Web 会重新接管。
|
|
100
100
|
|
|
101
|
+
Linux 还可以通过 `/proc` 做更完整的进程自动发现。macOS 上如果需要可靠的终端
|
|
102
|
+
控制和稳定 peer 身份,建议使用 shim 启动的会话或 `hcc peer start`。
|
|
103
|
+
|
|
101
104
|
如果只想初始化本地通信、不启动 Web、不安装 shim,可使用:
|
|
102
105
|
|
|
103
106
|
```bash
|
package/lib/cli-runtime.mjs
CHANGED
|
@@ -8,7 +8,8 @@ export function createContext(global = {}, deps = {}) {
|
|
|
8
8
|
const detectRoot = deps.detectRoot;
|
|
9
9
|
if (typeof detectRoot !== 'function') throw new TypeError('createContext requires deps.detectRoot');
|
|
10
10
|
const root = detectRoot(cwd, global.root);
|
|
11
|
-
const
|
|
11
|
+
const envDb = global.root ? null : process.env.HCC_DB;
|
|
12
|
+
const dbPath = path.resolve(global.db || envDb || projectDbPath(root));
|
|
12
13
|
return { cwd, root, dbPath, json: global.json, explicitRoot: Boolean(global.root || process.env.HCC_ROOT) };
|
|
13
14
|
}
|
|
14
15
|
|
|
@@ -19,6 +19,16 @@ export function createTaskStore(deps = {}) {
|
|
|
19
19
|
const sendMessage = typeof deps.sendMessage === 'function' ? deps.sendMessage : noopMessage;
|
|
20
20
|
const activePeerTtl = Number(deps.activePeerTtl || DEFAULT_ACTIVE_PEER_TTL);
|
|
21
21
|
|
|
22
|
+
function taskOwnedError(peer, row, action) {
|
|
23
|
+
return new CliError('TASK_OWNED', `Task #${row.id} is owned by ${row.owner}`, {
|
|
24
|
+
owner: row.owner,
|
|
25
|
+
task_id: row.id,
|
|
26
|
+
attempted_by: peer,
|
|
27
|
+
action,
|
|
28
|
+
notify_owner: true
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
22
32
|
function claimTaskRowsForPeer(db, peer, ids, { force = false, source = null } = {}) {
|
|
23
33
|
return tx(db, () => {
|
|
24
34
|
const tasks = [];
|
|
@@ -26,7 +36,7 @@ export function createTaskStore(deps = {}) {
|
|
|
26
36
|
const row = db.prepare('SELECT * FROM tasks WHERE id = ?').get(id);
|
|
27
37
|
if (!row) throw new CliError('NOT_FOUND', `Task #${id} does not exist`);
|
|
28
38
|
if (row.owner && row.owner !== peer && !force) {
|
|
29
|
-
throw
|
|
39
|
+
throw taskOwnedError(peer, row, 'claim');
|
|
30
40
|
}
|
|
31
41
|
if (row.assignee && row.assignee !== peer && !force) {
|
|
32
42
|
throw new CliError('TASK_ASSIGNED', `Task #${id} is assigned to ${row.assignee}`, { assignee: row.assignee });
|
|
@@ -211,7 +221,13 @@ export function createTaskStore(deps = {}) {
|
|
|
211
221
|
});
|
|
212
222
|
}
|
|
213
223
|
|
|
224
|
+
function assertTaskOwnerForMutation(db, peer, row, action) {
|
|
225
|
+
if (!row.owner || row.owner === peer) return;
|
|
226
|
+
throw taskOwnedError(peer, row, action);
|
|
227
|
+
}
|
|
228
|
+
|
|
214
229
|
return {
|
|
230
|
+
assertTaskOwnerForMutation,
|
|
215
231
|
claimNextTasksForPeer,
|
|
216
232
|
claimTaskRowsForPeer,
|
|
217
233
|
queryOpenTasks,
|
|
@@ -25,7 +25,7 @@ export function taskParticipants(task) {
|
|
|
25
25
|
|
|
26
26
|
export function payloadParticipants(payload) {
|
|
27
27
|
const values = [];
|
|
28
|
-
for (const key of ['peer', 'from_peer', 'to_peer', 'assignee', 'owner', 'recipient', 'kept_peer', 'removed_peer', 'previous_owner']) {
|
|
28
|
+
for (const key of ['peer', 'actor_peer', 'target_peer', 'from_peer', 'to_peer', 'assignee', 'owner', 'recipient', 'kept_peer', 'removed_peer', 'previous_owner']) {
|
|
29
29
|
if (payload?.[key] && payload[key] !== 'all') values.push(payload[key]);
|
|
30
30
|
}
|
|
31
31
|
return uniqueList(values);
|
|
@@ -53,19 +53,29 @@ const TIMELINE_EVENT_ALLOW = new Set([
|
|
|
53
53
|
'task.done',
|
|
54
54
|
'task.abandoned',
|
|
55
55
|
'task.takeover',
|
|
56
|
+
'task.dispatched',
|
|
56
57
|
'team.started',
|
|
57
58
|
'lock.acquired',
|
|
58
59
|
'lock.released',
|
|
59
60
|
'peer.registered',
|
|
60
61
|
'peer.joined',
|
|
61
62
|
'peer.auto_joined',
|
|
63
|
+
'peer.start.requested',
|
|
64
|
+
'peer.attach.requested',
|
|
65
|
+
'peer.stop.requested',
|
|
62
66
|
'peer.stopped',
|
|
67
|
+
'peer.restarted',
|
|
63
68
|
'provider.session.merged',
|
|
69
|
+
'provider.session.rebound',
|
|
64
70
|
'web.session.started',
|
|
65
71
|
'web.session.exited',
|
|
72
|
+
'web.session.stop_requested',
|
|
66
73
|
'tmux.session.attached',
|
|
67
74
|
'tmux.session.detached',
|
|
68
75
|
'tmux.session.exited',
|
|
76
|
+
'tmux.session.restarted',
|
|
77
|
+
'tmux.session.rebound',
|
|
78
|
+
'tmux.session.gc',
|
|
69
79
|
'run.session.started',
|
|
70
80
|
'run.session.exited'
|
|
71
81
|
]);
|
|
@@ -43,8 +43,16 @@ export function autoPeerId(ctx, kindHint = 'shell', observation = {}) {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
export function resolveCurrentPeer(ctx, opts = {}, key = 'peer', kindHint = 'shell', observation = {}) {
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
const explicit = opts[key] || '';
|
|
47
|
+
const envPeer = observation.env?.HCC_PEER || '';
|
|
48
|
+
if (envPeer) {
|
|
49
|
+
return {
|
|
50
|
+
id: envPeer,
|
|
51
|
+
auto: false,
|
|
52
|
+
corrected_from: explicit && explicit !== envPeer ? explicit : null
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
if (explicit) return { id: explicit, auto: false, corrected_from: null };
|
|
48
56
|
return { id: autoPeerId(ctx, kindHint, observation), auto: true };
|
|
49
57
|
}
|
|
50
58
|
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
+
import { shellQuoteArg } from '../../format.mjs';
|
|
2
|
+
|
|
1
3
|
export function generateShim(hccBin, realBin, tool) {
|
|
4
|
+
const hccBinLiteral = shellQuoteArg(hccBin);
|
|
5
|
+
const realBinLiteral = shellQuoteArg(realBin);
|
|
2
6
|
if (tool.resumeFlag) {
|
|
3
7
|
// Claude: --resume/--continue/--session-id/--fork-session aware wrapper.
|
|
4
8
|
return `#!/usr/bin/env bash
|
|
@@ -7,19 +11,22 @@ export function generateShim(hccBin, realBin, tool) {
|
|
|
7
11
|
# Real binary: ${realBin}
|
|
8
12
|
|
|
9
13
|
set -e
|
|
10
|
-
REAL_BIN
|
|
11
|
-
HCC_BIN
|
|
14
|
+
REAL_BIN=${realBinLiteral}
|
|
15
|
+
HCC_BIN=${hccBinLiteral}
|
|
16
|
+
HCC_TMUX_BIN="\${HCC_TMUX_BIN:-$(command -v tmux 2>/dev/null || printf 'tmux')}"
|
|
17
|
+
export HCC_TMUX_BIN
|
|
12
18
|
|
|
13
19
|
if [ "\${HCC_SHIM_ENSURED:-}" != "1" ]; then
|
|
14
20
|
set +e
|
|
15
|
-
"$HCC_BIN" shim ensure "${tool.name}" "$0" >/dev/null 2>&1
|
|
21
|
+
"$HCC_BIN" shim ensure "${tool.name}" "$0" "$REAL_BIN" >/dev/null 2>&1
|
|
16
22
|
ENSURE_STATUS=$?
|
|
17
23
|
set -e
|
|
18
|
-
if [ "$ENSURE_STATUS" = "75" ]; then
|
|
24
|
+
if [ "$ENSURE_STATUS" = "0" ] || [ "$ENSURE_STATUS" = "75" ]; then
|
|
19
25
|
export HCC_SHIM_ENSURED=1
|
|
20
26
|
exec "$0" "$@"
|
|
21
27
|
fi
|
|
22
28
|
fi
|
|
29
|
+
unset HCC_SHIM_ENSURED
|
|
23
30
|
|
|
24
31
|
should_passthrough() {
|
|
25
32
|
for arg in "$@"; do
|
|
@@ -112,7 +119,7 @@ current_tmux_pane() {
|
|
|
112
119
|
if [ -z "$TTY_PATH" ] || [ "$TTY_PATH" = "not a tty" ]; then
|
|
113
120
|
return 1
|
|
114
121
|
fi
|
|
115
|
-
|
|
122
|
+
"$HCC_TMUX_BIN" list-panes -a -F '#{pane_id} #{pane_tty}' 2>/dev/null | awk -v tty="$TTY_PATH" '$2 == tty { print $1; found = 1; exit } END { exit found ? 0 : 1 }'
|
|
116
123
|
}
|
|
117
124
|
|
|
118
125
|
CURRENT_HCC_PANE=""
|
|
@@ -123,7 +130,7 @@ detect_current_hcc_pane() {
|
|
|
123
130
|
if [ -z "$CURRENT_HCC_PANE" ]; then
|
|
124
131
|
return 1
|
|
125
132
|
fi
|
|
126
|
-
CURRENT_HCC_SESSION=$(
|
|
133
|
+
CURRENT_HCC_SESSION=$(command "$HCC_TMUX_BIN" display-message -p -t "$CURRENT_HCC_PANE" '#S' 2>/dev/null || true)
|
|
127
134
|
case "$CURRENT_HCC_SESSION" in
|
|
128
135
|
hcc-*-*) return 0 ;;
|
|
129
136
|
esac
|
|
@@ -138,15 +145,20 @@ if detect_current_hcc_pane; then
|
|
|
138
145
|
PEER_ID="$SESSION_PEER_ID"
|
|
139
146
|
fi
|
|
140
147
|
export HCC_PEER="$PEER_ID"
|
|
141
|
-
"$HCC_BIN" web >/dev/null 2>&1 || true
|
|
148
|
+
HCC_SKIP_SHIM_INSTALL=1 "$HCC_BIN" web >/dev/null 2>&1 || true
|
|
142
149
|
"$HCC_BIN" peer attach "$PEER_ID" --kind "${tool.kind}" --pane "$CURRENT_HCC_PANE" --cwd "$PWD" --force >/dev/null 2>&1 || true
|
|
143
150
|
exec "$REAL_BIN" "$@"
|
|
144
151
|
fi
|
|
145
152
|
|
|
153
|
+
HCC_PROVIDER_LAUNCH_SCRIPT='REAL_BIN="$1"; shift; HCC_TMUX_BIN="\${HCC_TMUX_BIN:-tmux}"; command "$HCC_TMUX_BIN" set-environment HCC_PROVIDER_STATE running >/dev/null 2>&1 || true; "$REAL_BIN" "$@"; command "$HCC_TMUX_BIN" set-environment HCC_PROVIDER_STATE exited >/dev/null 2>&1 || true; exec bash --noprofile --norc'
|
|
154
|
+
|
|
146
155
|
set +e
|
|
147
156
|
# Wrap in shell so pane_pid stays as bash; Ctrl+C drops back to bash
|
|
148
|
-
|
|
157
|
+
START_OUT_FILE=$(mktemp "\${TMPDIR:-/tmp}/hcc-shim-start.XXXXXX")
|
|
158
|
+
(HCC_SKIP_SHIM_INSTALL=1 "$HCC_BIN" web >/dev/null 2>&1 || true; "$HCC_BIN" peer start "$PEER_ID" --kind "${tool.kind}" --restart-env -- bash -lc "$HCC_PROVIDER_LAUNCH_SCRIPT" _ "$REAL_BIN" "$@") >"$START_OUT_FILE" 2>&1
|
|
149
159
|
START_STATUS=$?
|
|
160
|
+
START_OUT=$(cat "$START_OUT_FILE" 2>/dev/null || true)
|
|
161
|
+
rm -f "$START_OUT_FILE"
|
|
150
162
|
set -e
|
|
151
163
|
if [ "$START_STATUS" -ne 0 ]; then
|
|
152
164
|
printf '%s\\n' "$START_OUT" >&2
|
|
@@ -164,17 +176,17 @@ if [ "$HCC_SHIM_NO_ATTACH" = "1" ] || [ ! -t 0 ]; then
|
|
|
164
176
|
exit 0
|
|
165
177
|
fi
|
|
166
178
|
|
|
167
|
-
SESSION=$(
|
|
179
|
+
SESSION=$(command "$HCC_TMUX_BIN" display-message -p -t "$PANE" '#S' 2>/dev/null || true)
|
|
168
180
|
if [ -z "$SESSION" ]; then
|
|
169
181
|
printf '%s\\n' "$START_OUT"
|
|
170
182
|
exit 0
|
|
171
183
|
fi
|
|
172
184
|
|
|
173
185
|
if [ -n "$TMUX" ]; then
|
|
174
|
-
exec
|
|
186
|
+
exec "$HCC_TMUX_BIN" switch-client -t "$SESSION"
|
|
175
187
|
fi
|
|
176
188
|
|
|
177
|
-
exec
|
|
189
|
+
exec "$HCC_TMUX_BIN" attach-session -t "$SESSION"
|
|
178
190
|
`;
|
|
179
191
|
}
|
|
180
192
|
|
|
@@ -185,19 +197,22 @@ exec tmux attach-session -t "$SESSION"
|
|
|
185
197
|
# Real binary: ${realBin}
|
|
186
198
|
|
|
187
199
|
set -e
|
|
188
|
-
REAL_BIN
|
|
189
|
-
HCC_BIN
|
|
200
|
+
REAL_BIN=${realBinLiteral}
|
|
201
|
+
HCC_BIN=${hccBinLiteral}
|
|
202
|
+
HCC_TMUX_BIN="\${HCC_TMUX_BIN:-$(command -v tmux 2>/dev/null || printf 'tmux')}"
|
|
203
|
+
export HCC_TMUX_BIN
|
|
190
204
|
|
|
191
205
|
if [ "\${HCC_SHIM_ENSURED:-}" != "1" ]; then
|
|
192
206
|
set +e
|
|
193
|
-
"$HCC_BIN" shim ensure "${tool.name}" "$0" >/dev/null 2>&1
|
|
207
|
+
"$HCC_BIN" shim ensure "${tool.name}" "$0" "$REAL_BIN" >/dev/null 2>&1
|
|
194
208
|
ENSURE_STATUS=$?
|
|
195
209
|
set -e
|
|
196
|
-
if [ "$ENSURE_STATUS" = "75" ]; then
|
|
210
|
+
if [ "$ENSURE_STATUS" = "0" ] || [ "$ENSURE_STATUS" = "75" ]; then
|
|
197
211
|
export HCC_SHIM_ENSURED=1
|
|
198
212
|
exec "$0" "$@"
|
|
199
213
|
fi
|
|
200
214
|
fi
|
|
215
|
+
unset HCC_SHIM_ENSURED
|
|
201
216
|
|
|
202
217
|
first_non_option() {
|
|
203
218
|
EXPECT_VALUE=0
|
|
@@ -317,7 +332,7 @@ current_tmux_pane() {
|
|
|
317
332
|
if [ -z "$TTY_PATH" ] || [ "$TTY_PATH" = "not a tty" ]; then
|
|
318
333
|
return 1
|
|
319
334
|
fi
|
|
320
|
-
|
|
335
|
+
"$HCC_TMUX_BIN" list-panes -a -F '#{pane_id} #{pane_tty}' 2>/dev/null | awk -v tty="$TTY_PATH" '$2 == tty { print $1; found = 1; exit } END { exit found ? 0 : 1 }'
|
|
321
336
|
}
|
|
322
337
|
|
|
323
338
|
CURRENT_HCC_PANE=""
|
|
@@ -328,7 +343,7 @@ detect_current_hcc_pane() {
|
|
|
328
343
|
if [ -z "$CURRENT_HCC_PANE" ]; then
|
|
329
344
|
return 1
|
|
330
345
|
fi
|
|
331
|
-
CURRENT_HCC_SESSION=$(
|
|
346
|
+
CURRENT_HCC_SESSION=$(command "$HCC_TMUX_BIN" display-message -p -t "$CURRENT_HCC_PANE" '#S' 2>/dev/null || true)
|
|
332
347
|
case "$CURRENT_HCC_SESSION" in
|
|
333
348
|
hcc-*-*) return 0 ;;
|
|
334
349
|
esac
|
|
@@ -343,15 +358,20 @@ if detect_current_hcc_pane; then
|
|
|
343
358
|
PEER_ID="$SESSION_PEER_ID"
|
|
344
359
|
fi
|
|
345
360
|
export HCC_PEER="$PEER_ID"
|
|
346
|
-
"$HCC_BIN" web >/dev/null 2>&1 || true
|
|
361
|
+
HCC_SKIP_SHIM_INSTALL=1 "$HCC_BIN" web >/dev/null 2>&1 || true
|
|
347
362
|
"$HCC_BIN" peer attach "$PEER_ID" --kind "${tool.kind}" --pane "$CURRENT_HCC_PANE" --cwd "$PWD" --force >/dev/null 2>&1 || true
|
|
348
363
|
exec "$REAL_BIN" "$@"
|
|
349
364
|
fi
|
|
350
365
|
|
|
366
|
+
HCC_PROVIDER_LAUNCH_SCRIPT='REAL_BIN="$1"; shift; HCC_TMUX_BIN="\${HCC_TMUX_BIN:-tmux}"; command "$HCC_TMUX_BIN" set-environment HCC_PROVIDER_STATE running >/dev/null 2>&1 || true; "$REAL_BIN" "$@"; command "$HCC_TMUX_BIN" set-environment HCC_PROVIDER_STATE exited >/dev/null 2>&1 || true; exec bash --noprofile --norc'
|
|
367
|
+
|
|
351
368
|
set +e
|
|
352
369
|
# Wrap in shell so pane_pid stays as bash; Ctrl+C drops back to bash
|
|
353
|
-
|
|
370
|
+
START_OUT_FILE=$(mktemp "\${TMPDIR:-/tmp}/hcc-shim-start.XXXXXX")
|
|
371
|
+
(HCC_SKIP_SHIM_INSTALL=1 "$HCC_BIN" web >/dev/null 2>&1 || true; "$HCC_BIN" peer start "$PEER_ID" --kind "${tool.kind}" --restart-env -- bash -lc "$HCC_PROVIDER_LAUNCH_SCRIPT" _ "$REAL_BIN" "$@") >"$START_OUT_FILE" 2>&1
|
|
354
372
|
START_STATUS=$?
|
|
373
|
+
START_OUT=$(cat "$START_OUT_FILE" 2>/dev/null || true)
|
|
374
|
+
rm -f "$START_OUT_FILE"
|
|
355
375
|
set -e
|
|
356
376
|
if [ "$START_STATUS" -ne 0 ]; then
|
|
357
377
|
printf '%s\\n' "$START_OUT" >&2
|
|
@@ -369,16 +389,16 @@ if [ "$HCC_SHIM_NO_ATTACH" = "1" ] || [ ! -t 0 ]; then
|
|
|
369
389
|
exit 0
|
|
370
390
|
fi
|
|
371
391
|
|
|
372
|
-
SESSION=$(
|
|
392
|
+
SESSION=$(command "$HCC_TMUX_BIN" display-message -p -t "$PANE" '#S' 2>/dev/null || true)
|
|
373
393
|
if [ -z "$SESSION" ]; then
|
|
374
394
|
printf '%s\\n' "$START_OUT"
|
|
375
395
|
exit 0
|
|
376
396
|
fi
|
|
377
397
|
|
|
378
398
|
if [ -n "$TMUX" ]; then
|
|
379
|
-
exec
|
|
399
|
+
exec "$HCC_TMUX_BIN" switch-client -t "$SESSION"
|
|
380
400
|
fi
|
|
381
401
|
|
|
382
|
-
exec
|
|
402
|
+
exec "$HCC_TMUX_BIN" attach-session -t "$SESSION"
|
|
383
403
|
`;
|
|
384
404
|
}
|
|
@@ -10,8 +10,9 @@ export const SHIM_DIR = path.join(os.homedir(), '.hcc-shims');
|
|
|
10
10
|
* Install shim scripts for `claude` and `codex` into SHIM_DIR.
|
|
11
11
|
* Returns { shimDir, installed: [paths], changed: [paths], skipped: [names] }.
|
|
12
12
|
*/
|
|
13
|
-
export function installShims(hccBin) {
|
|
13
|
+
export function installShims(hccBin, opts = {}) {
|
|
14
14
|
fs.mkdirSync(SHIM_DIR, { recursive: true });
|
|
15
|
+
const realBins = opts.realBins && typeof opts.realBins === 'object' ? opts.realBins : {};
|
|
15
16
|
|
|
16
17
|
const tools = [
|
|
17
18
|
{ name: 'claude', kind: 'claude', resumeFlag: '--resume' },
|
|
@@ -23,17 +24,23 @@ export function installShims(hccBin) {
|
|
|
23
24
|
const skipped = [];
|
|
24
25
|
|
|
25
26
|
for (const tool of tools) {
|
|
26
|
-
const
|
|
27
|
+
const shimPath = path.join(SHIM_DIR, tool.name);
|
|
28
|
+
let previous = null;
|
|
29
|
+
try { previous = fs.readFileSync(shimPath, 'utf8'); } catch {}
|
|
30
|
+
const requestedRealBin = realBins[tool.name] || null;
|
|
31
|
+
const existingRealBin = previous ? shimRealBinFromContent(previous) : null;
|
|
32
|
+
const realBin = existingRealBin && fsExists(existingRealBin)
|
|
33
|
+
? existingRealBin
|
|
34
|
+
: requestedRealBin && fsExists(requestedRealBin)
|
|
35
|
+
? requestedRealBin
|
|
36
|
+
: findRealBinary(tool.name);
|
|
27
37
|
if (!realBin) {
|
|
28
38
|
skipped.push(`${tool.name} (not found on PATH)`);
|
|
29
39
|
continue;
|
|
30
40
|
}
|
|
31
|
-
const shimPath = path.join(SHIM_DIR, tool.name);
|
|
32
41
|
const content = generateShim(hccBin, realBin, tool);
|
|
33
|
-
let previous = null;
|
|
34
|
-
try { previous = fs.readFileSync(shimPath, 'utf8'); } catch {}
|
|
35
42
|
if (previous !== content) {
|
|
36
|
-
|
|
43
|
+
writeExecutableAtomic(shimPath, content);
|
|
37
44
|
changed.push(shimPath);
|
|
38
45
|
} else {
|
|
39
46
|
try { fs.chmodSync(shimPath, 0o755); } catch {}
|
|
@@ -66,6 +73,20 @@ export function verifyShims() {
|
|
|
66
73
|
);
|
|
67
74
|
}
|
|
68
75
|
|
|
76
|
+
export function shimStatus() {
|
|
77
|
+
const tools = {};
|
|
78
|
+
for (const name of ['claude', 'codex']) {
|
|
79
|
+
const shimPath = path.join(SHIM_DIR, name);
|
|
80
|
+
tools[name] = { installed: fsExists(shimPath), path: shimPath };
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
shimDir: SHIM_DIR,
|
|
84
|
+
tools,
|
|
85
|
+
installed: Object.values(tools).some((tool) => tool.installed),
|
|
86
|
+
complete: Object.values(tools).every((tool) => tool.installed)
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
69
90
|
/**
|
|
70
91
|
* Find the real binary for `name`, skipping the SHIM_DIR.
|
|
71
92
|
* Checks PATH, then npm global bin, then common install locations.
|
|
@@ -104,3 +125,21 @@ export function findRealBinary(name) {
|
|
|
104
125
|
function fsExists(p) {
|
|
105
126
|
try { fs.accessSync(p); return true; } catch { return false; }
|
|
106
127
|
}
|
|
128
|
+
|
|
129
|
+
function shimRealBinFromContent(content) {
|
|
130
|
+
const match = String(content || '').match(/^# Real binary: ([^\r\n]+)$/m);
|
|
131
|
+
return match ? match[1] : null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function writeExecutableAtomic(file, content) {
|
|
135
|
+
const dir = path.dirname(file);
|
|
136
|
+
const base = path.basename(file);
|
|
137
|
+
const tmp = path.join(dir, `.${base}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`);
|
|
138
|
+
try {
|
|
139
|
+
fs.writeFileSync(tmp, content, { mode: 0o755 });
|
|
140
|
+
fs.renameSync(tmp, file);
|
|
141
|
+
} catch (err) {
|
|
142
|
+
try { fs.rmSync(tmp, { force: true }); } catch {}
|
|
143
|
+
throw err;
|
|
144
|
+
}
|
|
145
|
+
}
|
package/lib/setup.mjs
CHANGED
package/lib/shell-path.mjs
CHANGED
|
@@ -2,26 +2,28 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
|
|
5
|
-
export function pathEntryLine(shellType) {
|
|
6
|
-
|
|
7
|
-
return
|
|
5
|
+
export function pathEntryLine(shellType, position = 'final') {
|
|
6
|
+
const suffix = position === 'early' ? 'early' : 'final';
|
|
7
|
+
if (shellType === 'fish') return `set -gx PATH $HOME/.hcc-shims $PATH # hello-cc shims (${suffix})`;
|
|
8
|
+
return `_hcc_shims="$HOME/.hcc-shims"; PATH="\${PATH//:\${_hcc_shims}:/:}"; PATH="\${PATH#\${_hcc_shims}:}"; PATH="\${PATH%:\${_hcc_shims}}"; export PATH="\${_hcc_shims}:$PATH"; unset _hcc_shims # hello-cc shims (${suffix})`;
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
export function removePathEntryLines(content) {
|
|
11
12
|
return String(content || '')
|
|
12
13
|
.split('\n')
|
|
13
|
-
.filter(line => !line
|
|
14
|
+
.filter(line => !isHelloCcShimPathLine(line))
|
|
14
15
|
.join('\n')
|
|
15
16
|
.replace(/\n{3,}/g, '\n\n')
|
|
16
17
|
.replace(/\s+$/, '');
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
export function insertPathEntry(content, shellType) {
|
|
20
|
-
const line = pathEntryLine(shellType);
|
|
21
21
|
const cleaned = removePathEntryLines(content);
|
|
22
|
-
|
|
22
|
+
const finalLine = pathEntryLine(shellType, 'final');
|
|
23
|
+
if (shellType === 'fish' || shellType === 'zsh') return `${cleaned}${cleaned ? '\n' : ''}${finalLine}\n`;
|
|
23
24
|
|
|
24
25
|
const lines = cleaned ? cleaned.split('\n') : [];
|
|
26
|
+
const earlyLine = pathEntryLine(shellType, 'early');
|
|
25
27
|
let insertAt = 0;
|
|
26
28
|
if (lines[0]?.startsWith('#!')) insertAt = 1;
|
|
27
29
|
for (let i = insertAt; i < lines.length; i += 1) {
|
|
@@ -30,9 +32,9 @@ export function insertPathEntry(content, shellType) {
|
|
|
30
32
|
insertAt = i;
|
|
31
33
|
break;
|
|
32
34
|
}
|
|
33
|
-
lines.splice(insertAt, 0,
|
|
35
|
+
lines.splice(insertAt, 0, earlyLine);
|
|
34
36
|
const withEarlyEntry = lines.join('\n').replace(/\s+$/, '');
|
|
35
|
-
return `${withEarlyEntry}${withEarlyEntry ? '\n' : ''}${
|
|
37
|
+
return `${withEarlyEntry}${withEarlyEntry ? '\n' : ''}${finalLine}\n`;
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
export function detectShellRc() {
|
|
@@ -49,21 +51,34 @@ export function installPathEntry() {
|
|
|
49
51
|
|
|
50
52
|
const next = insertPathEntry(content, shellType);
|
|
51
53
|
const alreadyPresent = next === (content.endsWith('\n') ? content : `${content}\n`);
|
|
52
|
-
if (!alreadyPresent)
|
|
54
|
+
if (!alreadyPresent) {
|
|
55
|
+
fs.mkdirSync(path.dirname(rcFile), { recursive: true });
|
|
56
|
+
fs.writeFileSync(rcFile, next);
|
|
57
|
+
}
|
|
53
58
|
return { rcFile, shellType, alreadyPresent };
|
|
54
59
|
}
|
|
55
60
|
|
|
56
61
|
export function uninstallPathEntry() {
|
|
57
62
|
const { path: rcFile } = detectShellRc();
|
|
63
|
+
let content = '';
|
|
64
|
+
try {
|
|
65
|
+
content = fs.readFileSync(rcFile, 'utf8');
|
|
66
|
+
} catch (err) {
|
|
67
|
+
if (err?.code === 'ENOENT') return { rcFile, removed: false, missing: true };
|
|
68
|
+
return { rcFile, removed: false, error: 'could not read shell rc' };
|
|
69
|
+
}
|
|
70
|
+
const filtered = removePathEntryLines(content);
|
|
71
|
+
if (filtered === content.replace(/\s+$/, '')) {
|
|
72
|
+
return { rcFile, removed: false };
|
|
73
|
+
}
|
|
58
74
|
try {
|
|
59
|
-
const content = fs.readFileSync(rcFile, 'utf8');
|
|
60
|
-
const filtered = content
|
|
61
|
-
.split('\n')
|
|
62
|
-
.filter(line => !line.includes('.hcc-shims'))
|
|
63
|
-
.join('\n');
|
|
64
75
|
fs.writeFileSync(rcFile, filtered);
|
|
65
|
-
return { rcFile };
|
|
76
|
+
return { rcFile, removed: true };
|
|
66
77
|
} catch {
|
|
67
|
-
return { rcFile, error: 'could not modify shell rc' };
|
|
78
|
+
return { rcFile, removed: false, error: 'could not modify shell rc' };
|
|
68
79
|
}
|
|
69
80
|
}
|
|
81
|
+
|
|
82
|
+
function isHelloCcShimPathLine(line) {
|
|
83
|
+
return String(line || '').includes('# hello-cc shims');
|
|
84
|
+
}
|