@logicseek/hello-cc 0.1.3 → 0.1.5
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 +110 -0
- package/bin/hcc.mjs +865 -2430
- package/docs/commands.md +21 -9
- package/docs/commands.zh-CN.md +19 -9
- package/lib/cli-args.mjs +89 -0
- package/lib/db-schema.mjs +324 -0
- package/lib/errors.mjs +7 -0
- package/lib/format.mjs +39 -0
- package/lib/guidance.mjs +123 -0
- package/lib/json-file.mjs +14 -0
- package/lib/package-meta.mjs +24 -0
- package/lib/release-notes.mjs +40 -0
- package/lib/runtime-paths.mjs +45 -0
- package/lib/setup.mjs +3 -474
- package/lib/shell-path.mjs +69 -0
- package/lib/shim-script.mjs +384 -0
- package/lib/web-http.mjs +55 -0
- package/lib/web-runtime.mjs +108 -0
- package/lib/web-ui-template.mjs +1909 -0
- package/package.json +1 -1
- package/scripts/github-release.mjs +11 -49
- package/scripts/regression.mjs +443 -18
- package/scripts/release-notes.mjs +6 -38
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
export function generateShim(hccBin, realBin, tool) {
|
|
2
|
+
if (tool.resumeFlag) {
|
|
3
|
+
// Claude: --resume/--continue/--session-id/--fork-session aware wrapper.
|
|
4
|
+
return `#!/usr/bin/env bash
|
|
5
|
+
# hello-cc shim for ${tool.name} (auto-generated; do not edit manually)
|
|
6
|
+
# Wraps ${tool.name} with hello-cc peer mesh integration.
|
|
7
|
+
# Real binary: ${realBin}
|
|
8
|
+
|
|
9
|
+
set -e
|
|
10
|
+
REAL_BIN="${realBin}"
|
|
11
|
+
HCC_BIN="${hccBin}"
|
|
12
|
+
|
|
13
|
+
if [ "\${HCC_SHIM_ENSURED:-}" != "1" ]; then
|
|
14
|
+
set +e
|
|
15
|
+
"$HCC_BIN" shim ensure "${tool.name}" "$0" >/dev/null 2>&1
|
|
16
|
+
ENSURE_STATUS=$?
|
|
17
|
+
set -e
|
|
18
|
+
if [ "$ENSURE_STATUS" = "75" ]; then
|
|
19
|
+
export HCC_SHIM_ENSURED=1
|
|
20
|
+
exec "$0" "$@"
|
|
21
|
+
fi
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
should_passthrough() {
|
|
25
|
+
for arg in "$@"; do
|
|
26
|
+
case "$arg" in
|
|
27
|
+
-h|--help|-v|--version|-p|--print|--bare|--safe-mode)
|
|
28
|
+
return 0
|
|
29
|
+
;;
|
|
30
|
+
esac
|
|
31
|
+
done
|
|
32
|
+
|
|
33
|
+
case "\${1:-}" in
|
|
34
|
+
agents|auth|auto-mode|doctor|install|mcp|plugin|plugins|project|setup-token|ultrareview|update|upgrade)
|
|
35
|
+
return 0
|
|
36
|
+
;;
|
|
37
|
+
esac
|
|
38
|
+
|
|
39
|
+
return 1
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if should_passthrough "$@"; then
|
|
43
|
+
exec "$REAL_BIN" "$@"
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
# Use the current directory as the hello-cc project root unless HCC_ROOT is set.
|
|
47
|
+
HCC_ROOT=$("$HCC_BIN" find-root 2>/dev/null || true)
|
|
48
|
+
|
|
49
|
+
if [ -z "$HCC_ROOT" ]; then
|
|
50
|
+
exec "$REAL_BIN" "$@"
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
sanitize_peer_part() {
|
|
54
|
+
printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9._-]/-/g; s/^-*//; s/-*$//' | cut -c 1-32
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
tty_peer_part() {
|
|
58
|
+
TTY_NUM=$(tty 2>/dev/null | tr -dc '0-9' | tail -c 4)
|
|
59
|
+
PARENT_PID="\${PPID:-$$}"
|
|
60
|
+
if [ -n "$TTY_NUM" ]; then
|
|
61
|
+
printf '%s-%s' "$TTY_NUM" "$PARENT_PID"
|
|
62
|
+
else
|
|
63
|
+
printf '%s' "$PARENT_PID"
|
|
64
|
+
fi
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
RESUME_ID=""
|
|
68
|
+
SESSION_ID=""
|
|
69
|
+
SESSION_NAME=""
|
|
70
|
+
IS_CONTINUE=0
|
|
71
|
+
IS_FORK=0
|
|
72
|
+
PREV_ARG=""
|
|
73
|
+
for arg in "$@"; do
|
|
74
|
+
case "$PREV_ARG" in
|
|
75
|
+
--resume|-r) RESUME_ID="$arg" ;;
|
|
76
|
+
--session-id) SESSION_ID="$arg" ;;
|
|
77
|
+
--name|-n) SESSION_NAME="$arg" ;;
|
|
78
|
+
esac
|
|
79
|
+
case "$arg" in
|
|
80
|
+
--resume=*) RESUME_ID="\${arg#--resume=}" ;;
|
|
81
|
+
--session-id=*) SESSION_ID="\${arg#--session-id=}" ;;
|
|
82
|
+
--name=*) SESSION_NAME="\${arg#--name=}" ;;
|
|
83
|
+
--continue|-c) IS_CONTINUE=1 ;;
|
|
84
|
+
--fork-session) IS_FORK=1 ;;
|
|
85
|
+
esac
|
|
86
|
+
PREV_ARG="$arg"
|
|
87
|
+
done
|
|
88
|
+
|
|
89
|
+
if [ -n "$HCC_PEER" ]; then
|
|
90
|
+
PEER_ID="$HCC_PEER"
|
|
91
|
+
elif [ -n "$SESSION_ID" ]; then
|
|
92
|
+
PEER_ID="${tool.kind}-\$(sanitize_peer_part "\${SESSION_ID:0:8}")"
|
|
93
|
+
elif [ -n "$RESUME_ID" ] && [ "$IS_FORK" != "1" ]; then
|
|
94
|
+
PEER_ID="${tool.kind}-\$(sanitize_peer_part "\${RESUME_ID:0:8}")"
|
|
95
|
+
elif [ -n "$CLAUDE_CODE_SESSION_ID" ]; then
|
|
96
|
+
PEER_ID="${tool.kind}-\$(sanitize_peer_part "\${CLAUDE_CODE_SESSION_ID:0:8}")"
|
|
97
|
+
elif [ -n "$SESSION_NAME" ] && [ "$IS_FORK" != "1" ] && [ "$IS_CONTINUE" != "1" ]; then
|
|
98
|
+
PEER_ID="${tool.kind}-\$(sanitize_peer_part "$SESSION_NAME")"
|
|
99
|
+
else
|
|
100
|
+
PEER_ID="${tool.kind}-\$(tty_peer_part)"
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
export HCC_PEER="$PEER_ID"
|
|
104
|
+
export HCC_ROOT
|
|
105
|
+
|
|
106
|
+
current_tmux_pane() {
|
|
107
|
+
if [ -n "\${TMUX_PANE:-}" ]; then
|
|
108
|
+
printf '%s\\n' "$TMUX_PANE"
|
|
109
|
+
return 0
|
|
110
|
+
fi
|
|
111
|
+
TTY_PATH=$(tty 2>/dev/null || true)
|
|
112
|
+
if [ -z "$TTY_PATH" ] || [ "$TTY_PATH" = "not a tty" ]; then
|
|
113
|
+
return 1
|
|
114
|
+
fi
|
|
115
|
+
tmux 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
|
+
}
|
|
117
|
+
|
|
118
|
+
CURRENT_HCC_PANE=""
|
|
119
|
+
CURRENT_HCC_SESSION=""
|
|
120
|
+
detect_current_hcc_pane() {
|
|
121
|
+
CURRENT_HCC_SESSION=""
|
|
122
|
+
CURRENT_HCC_PANE=$(current_tmux_pane || true)
|
|
123
|
+
if [ -z "$CURRENT_HCC_PANE" ]; then
|
|
124
|
+
return 1
|
|
125
|
+
fi
|
|
126
|
+
CURRENT_HCC_SESSION=$(tmux display-message -p -t "$CURRENT_HCC_PANE" '#S' 2>/dev/null || true)
|
|
127
|
+
case "$CURRENT_HCC_SESSION" in
|
|
128
|
+
hcc-*-*) return 0 ;;
|
|
129
|
+
esac
|
|
130
|
+
CURRENT_HCC_PANE=""
|
|
131
|
+
CURRENT_HCC_SESSION=""
|
|
132
|
+
return 1
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if detect_current_hcc_pane; then
|
|
136
|
+
SESSION_PEER_ID=$(printf '%s' "$CURRENT_HCC_SESSION" | sed 's/^hcc-[^-]*-//')
|
|
137
|
+
if [ -n "$SESSION_PEER_ID" ] && [ "$SESSION_PEER_ID" != "$CURRENT_HCC_SESSION" ]; then
|
|
138
|
+
PEER_ID="$SESSION_PEER_ID"
|
|
139
|
+
fi
|
|
140
|
+
export HCC_PEER="$PEER_ID"
|
|
141
|
+
"$HCC_BIN" web >/dev/null 2>&1 || true
|
|
142
|
+
"$HCC_BIN" peer attach "$PEER_ID" --kind "${tool.kind}" --pane "$CURRENT_HCC_PANE" --cwd "$PWD" --force >/dev/null 2>&1 || true
|
|
143
|
+
exec "$REAL_BIN" "$@"
|
|
144
|
+
fi
|
|
145
|
+
|
|
146
|
+
set +e
|
|
147
|
+
# Wrap in shell so pane_pid stays as bash; Ctrl+C drops back to bash
|
|
148
|
+
START_OUT=$("$HCC_BIN" web >/dev/null 2>&1 || true; "$HCC_BIN" peer start "$PEER_ID" --kind "${tool.kind}" --restart-env -- bash -lc 'REAL_BIN="$1"; shift; hcc_provider_state() { HCC_TMUX_SESSION=$(tmux display-message -p "#S" 2>/dev/null || true); if [ -n "$HCC_TMUX_SESSION" ]; then tmux set-environment -t "$HCC_TMUX_SESSION" HCC_PROVIDER_STATE "$1" >/dev/null 2>&1 || true; fi; }; hcc_provider_state running; "$REAL_BIN" "$@"; hcc_provider_state exited; exec bash' _ "$REAL_BIN" "$@" 2>&1)
|
|
149
|
+
START_STATUS=$?
|
|
150
|
+
set -e
|
|
151
|
+
if [ "$START_STATUS" -ne 0 ]; then
|
|
152
|
+
printf '%s\\n' "$START_OUT" >&2
|
|
153
|
+
exit "$START_STATUS"
|
|
154
|
+
fi
|
|
155
|
+
|
|
156
|
+
PANE=$(printf '%s\\n' "$START_OUT" | sed -n 's/.*pane=\\(%[0-9][0-9]*\\).*/\\1/p' | tail -n 1)
|
|
157
|
+
if [ -z "$PANE" ]; then
|
|
158
|
+
printf '%s\\n' "$START_OUT"
|
|
159
|
+
exit 0
|
|
160
|
+
fi
|
|
161
|
+
|
|
162
|
+
if [ "$HCC_SHIM_NO_ATTACH" = "1" ] || [ ! -t 0 ]; then
|
|
163
|
+
printf '%s\\n' "$START_OUT"
|
|
164
|
+
exit 0
|
|
165
|
+
fi
|
|
166
|
+
|
|
167
|
+
SESSION=$(tmux display-message -p -t "$PANE" '#S' 2>/dev/null || true)
|
|
168
|
+
if [ -z "$SESSION" ]; then
|
|
169
|
+
printf '%s\\n' "$START_OUT"
|
|
170
|
+
exit 0
|
|
171
|
+
fi
|
|
172
|
+
|
|
173
|
+
if [ -n "$TMUX" ]; then
|
|
174
|
+
exec tmux switch-client -t "$SESSION"
|
|
175
|
+
fi
|
|
176
|
+
|
|
177
|
+
exec tmux attach-session -t "$SESSION"
|
|
178
|
+
`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Codex: resume/fork subcommand aware wrapper.
|
|
182
|
+
return `#!/usr/bin/env bash
|
|
183
|
+
# hello-cc shim for ${tool.name} (auto-generated; do not edit manually)
|
|
184
|
+
# Wraps ${tool.name} with hello-cc peer mesh integration.
|
|
185
|
+
# Real binary: ${realBin}
|
|
186
|
+
|
|
187
|
+
set -e
|
|
188
|
+
REAL_BIN="${realBin}"
|
|
189
|
+
HCC_BIN="${hccBin}"
|
|
190
|
+
|
|
191
|
+
if [ "\${HCC_SHIM_ENSURED:-}" != "1" ]; then
|
|
192
|
+
set +e
|
|
193
|
+
"$HCC_BIN" shim ensure "${tool.name}" "$0" >/dev/null 2>&1
|
|
194
|
+
ENSURE_STATUS=$?
|
|
195
|
+
set -e
|
|
196
|
+
if [ "$ENSURE_STATUS" = "75" ]; then
|
|
197
|
+
export HCC_SHIM_ENSURED=1
|
|
198
|
+
exec "$0" "$@"
|
|
199
|
+
fi
|
|
200
|
+
fi
|
|
201
|
+
|
|
202
|
+
first_non_option() {
|
|
203
|
+
EXPECT_VALUE=0
|
|
204
|
+
for arg in "$@"; do
|
|
205
|
+
if [ "$EXPECT_VALUE" = "1" ]; then
|
|
206
|
+
EXPECT_VALUE=0
|
|
207
|
+
continue
|
|
208
|
+
fi
|
|
209
|
+
|
|
210
|
+
case "$arg" in
|
|
211
|
+
--)
|
|
212
|
+
EXPECT_VALUE=0
|
|
213
|
+
continue
|
|
214
|
+
;;
|
|
215
|
+
-c|--config|-i|--image|-m|--model|-p|--profile|-s|--sandbox|-C|--cd|--add-dir|-a|--ask-for-approval|--remote|--remote-auth-token-env)
|
|
216
|
+
EXPECT_VALUE=1
|
|
217
|
+
continue
|
|
218
|
+
;;
|
|
219
|
+
--config=*|--image=*|--model=*|--profile=*|--sandbox=*|--cd=*|--add-dir=*|--ask-for-approval=*|--remote=*|--remote-auth-token-env=*)
|
|
220
|
+
continue
|
|
221
|
+
;;
|
|
222
|
+
--*)
|
|
223
|
+
continue
|
|
224
|
+
;;
|
|
225
|
+
-*)
|
|
226
|
+
continue
|
|
227
|
+
;;
|
|
228
|
+
*)
|
|
229
|
+
printf '%s' "$arg"
|
|
230
|
+
return 0
|
|
231
|
+
;;
|
|
232
|
+
esac
|
|
233
|
+
done
|
|
234
|
+
return 1
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
should_passthrough() {
|
|
238
|
+
for arg in "$@"; do
|
|
239
|
+
case "$arg" in
|
|
240
|
+
-h|--help|-V|--version)
|
|
241
|
+
return 0
|
|
242
|
+
;;
|
|
243
|
+
esac
|
|
244
|
+
done
|
|
245
|
+
|
|
246
|
+
SUBCOMMAND=$(first_non_option "$@" || true)
|
|
247
|
+
case "$SUBCOMMAND" in
|
|
248
|
+
help|exec|e|review|login|logout|mcp|plugin|mcp-server|app-server|remote-control|completion|update|doctor|sandbox|debug|apply|a|archive|unarchive|cloud|exec-server|features)
|
|
249
|
+
return 0
|
|
250
|
+
;;
|
|
251
|
+
esac
|
|
252
|
+
|
|
253
|
+
return 1
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if should_passthrough "$@"; then
|
|
257
|
+
exec "$REAL_BIN" "$@"
|
|
258
|
+
fi
|
|
259
|
+
|
|
260
|
+
HCC_ROOT=$("$HCC_BIN" find-root 2>/dev/null || true)
|
|
261
|
+
|
|
262
|
+
if [ -z "$HCC_ROOT" ]; then
|
|
263
|
+
exec "$REAL_BIN" "$@"
|
|
264
|
+
fi
|
|
265
|
+
|
|
266
|
+
sanitize_peer_part() {
|
|
267
|
+
printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9._-]/-/g; s/^-*//; s/-*$//' | cut -c 1-32
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
tty_peer_part() {
|
|
271
|
+
TTY_NUM=$(tty 2>/dev/null | tr -dc '0-9' | tail -c 4)
|
|
272
|
+
PARENT_PID="\${PPID:-$$}"
|
|
273
|
+
if [ -n "$TTY_NUM" ]; then
|
|
274
|
+
printf '%s-%s' "$TTY_NUM" "$PARENT_PID"
|
|
275
|
+
else
|
|
276
|
+
printf '%s' "$PARENT_PID"
|
|
277
|
+
fi
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
SUBCMD=""
|
|
281
|
+
RESUME_ID=""
|
|
282
|
+
EXPECT_SESSION=0
|
|
283
|
+
for arg in "$@"; do
|
|
284
|
+
if [ -z "$SUBCMD" ]; then
|
|
285
|
+
if [ "$arg" = "resume" ] || [ "$arg" = "fork" ]; then
|
|
286
|
+
SUBCMD="$arg"
|
|
287
|
+
EXPECT_SESSION=1
|
|
288
|
+
fi
|
|
289
|
+
continue
|
|
290
|
+
fi
|
|
291
|
+
if [ "$EXPECT_SESSION" = "1" ]; then
|
|
292
|
+
case "$arg" in
|
|
293
|
+
--last) EXPECT_SESSION=0 ;;
|
|
294
|
+
-*) ;;
|
|
295
|
+
*) RESUME_ID="$arg"; EXPECT_SESSION=0 ;;
|
|
296
|
+
esac
|
|
297
|
+
fi
|
|
298
|
+
done
|
|
299
|
+
|
|
300
|
+
if [ -n "$HCC_PEER" ]; then
|
|
301
|
+
PEER_ID="$HCC_PEER"
|
|
302
|
+
elif [ "$SUBCMD" = "resume" ] && [ -n "$RESUME_ID" ]; then
|
|
303
|
+
PEER_ID="${tool.kind}-\$(sanitize_peer_part "\${RESUME_ID:0:8}")"
|
|
304
|
+
else
|
|
305
|
+
PEER_ID="${tool.kind}-\$(tty_peer_part)"
|
|
306
|
+
fi
|
|
307
|
+
|
|
308
|
+
export HCC_PEER="$PEER_ID"
|
|
309
|
+
export HCC_ROOT
|
|
310
|
+
|
|
311
|
+
current_tmux_pane() {
|
|
312
|
+
if [ -n "\${TMUX_PANE:-}" ]; then
|
|
313
|
+
printf '%s\\n' "$TMUX_PANE"
|
|
314
|
+
return 0
|
|
315
|
+
fi
|
|
316
|
+
TTY_PATH=$(tty 2>/dev/null || true)
|
|
317
|
+
if [ -z "$TTY_PATH" ] || [ "$TTY_PATH" = "not a tty" ]; then
|
|
318
|
+
return 1
|
|
319
|
+
fi
|
|
320
|
+
tmux 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
|
+
}
|
|
322
|
+
|
|
323
|
+
CURRENT_HCC_PANE=""
|
|
324
|
+
CURRENT_HCC_SESSION=""
|
|
325
|
+
detect_current_hcc_pane() {
|
|
326
|
+
CURRENT_HCC_SESSION=""
|
|
327
|
+
CURRENT_HCC_PANE=$(current_tmux_pane || true)
|
|
328
|
+
if [ -z "$CURRENT_HCC_PANE" ]; then
|
|
329
|
+
return 1
|
|
330
|
+
fi
|
|
331
|
+
CURRENT_HCC_SESSION=$(tmux display-message -p -t "$CURRENT_HCC_PANE" '#S' 2>/dev/null || true)
|
|
332
|
+
case "$CURRENT_HCC_SESSION" in
|
|
333
|
+
hcc-*-*) return 0 ;;
|
|
334
|
+
esac
|
|
335
|
+
CURRENT_HCC_PANE=""
|
|
336
|
+
CURRENT_HCC_SESSION=""
|
|
337
|
+
return 1
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if detect_current_hcc_pane; then
|
|
341
|
+
SESSION_PEER_ID=$(printf '%s' "$CURRENT_HCC_SESSION" | sed 's/^hcc-[^-]*-//')
|
|
342
|
+
if [ -n "$SESSION_PEER_ID" ] && [ "$SESSION_PEER_ID" != "$CURRENT_HCC_SESSION" ]; then
|
|
343
|
+
PEER_ID="$SESSION_PEER_ID"
|
|
344
|
+
fi
|
|
345
|
+
export HCC_PEER="$PEER_ID"
|
|
346
|
+
"$HCC_BIN" web >/dev/null 2>&1 || true
|
|
347
|
+
"$HCC_BIN" peer attach "$PEER_ID" --kind "${tool.kind}" --pane "$CURRENT_HCC_PANE" --cwd "$PWD" --force >/dev/null 2>&1 || true
|
|
348
|
+
exec "$REAL_BIN" "$@"
|
|
349
|
+
fi
|
|
350
|
+
|
|
351
|
+
set +e
|
|
352
|
+
# Wrap in shell so pane_pid stays as bash; Ctrl+C drops back to bash
|
|
353
|
+
START_OUT=$("$HCC_BIN" web >/dev/null 2>&1 || true; "$HCC_BIN" peer start "$PEER_ID" --kind "${tool.kind}" --restart-env -- bash -lc 'REAL_BIN="$1"; shift; hcc_provider_state() { HCC_TMUX_SESSION=$(tmux display-message -p "#S" 2>/dev/null || true); if [ -n "$HCC_TMUX_SESSION" ]; then tmux set-environment -t "$HCC_TMUX_SESSION" HCC_PROVIDER_STATE "$1" >/dev/null 2>&1 || true; fi; }; hcc_provider_state running; "$REAL_BIN" "$@"; hcc_provider_state exited; exec bash' _ "$REAL_BIN" "$@" 2>&1)
|
|
354
|
+
START_STATUS=$?
|
|
355
|
+
set -e
|
|
356
|
+
if [ "$START_STATUS" -ne 0 ]; then
|
|
357
|
+
printf '%s\\n' "$START_OUT" >&2
|
|
358
|
+
exit "$START_STATUS"
|
|
359
|
+
fi
|
|
360
|
+
|
|
361
|
+
PANE=$(printf '%s\\n' "$START_OUT" | sed -n 's/.*pane=\\(%[0-9][0-9]*\\).*/\\1/p' | tail -n 1)
|
|
362
|
+
if [ -z "$PANE" ]; then
|
|
363
|
+
printf '%s\\n' "$START_OUT"
|
|
364
|
+
exit 0
|
|
365
|
+
fi
|
|
366
|
+
|
|
367
|
+
if [ "$HCC_SHIM_NO_ATTACH" = "1" ] || [ ! -t 0 ]; then
|
|
368
|
+
printf '%s\\n' "$START_OUT"
|
|
369
|
+
exit 0
|
|
370
|
+
fi
|
|
371
|
+
|
|
372
|
+
SESSION=$(tmux display-message -p -t "$PANE" '#S' 2>/dev/null || true)
|
|
373
|
+
if [ -z "$SESSION" ]; then
|
|
374
|
+
printf '%s\\n' "$START_OUT"
|
|
375
|
+
exit 0
|
|
376
|
+
fi
|
|
377
|
+
|
|
378
|
+
if [ -n "$TMUX" ]; then
|
|
379
|
+
exec tmux switch-client -t "$SESSION"
|
|
380
|
+
fi
|
|
381
|
+
|
|
382
|
+
exec tmux attach-session -t "$SESSION"
|
|
383
|
+
`;
|
|
384
|
+
}
|
package/lib/web-http.mjs
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { CliError } from './errors.mjs';
|
|
3
|
+
|
|
4
|
+
function readRequestBody(req, limit = 1024 * 1024) {
|
|
5
|
+
return new Promise((resolve, reject) => {
|
|
6
|
+
let body = '';
|
|
7
|
+
req.on('data', (chunk) => {
|
|
8
|
+
body += chunk;
|
|
9
|
+
if (body.length > limit) {
|
|
10
|
+
reject(new CliError('REQUEST_TOO_LARGE', 'Request body is too large'));
|
|
11
|
+
req.destroy();
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
req.on('end', () => resolve(body));
|
|
15
|
+
req.on('error', reject);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function readJsonRequest(req) {
|
|
20
|
+
const body = await readRequestBody(req);
|
|
21
|
+
if (!body.trim()) return {};
|
|
22
|
+
return JSON.parse(body);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function sendHttp(res, status, contentType, body) {
|
|
26
|
+
res.writeHead(status, {
|
|
27
|
+
'Content-Type': contentType,
|
|
28
|
+
'Cache-Control': 'no-store'
|
|
29
|
+
});
|
|
30
|
+
res.end(body);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function sendJson(res, status, body) {
|
|
34
|
+
sendHttp(res, status, 'application/json; charset=utf-8', JSON.stringify(body, null, 2));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function sendFile(res, filePath, contentType) {
|
|
38
|
+
try {
|
|
39
|
+
const body = fs.readFileSync(filePath);
|
|
40
|
+
res.writeHead(200, {
|
|
41
|
+
'Content-Type': contentType,
|
|
42
|
+
'Cache-Control': 'no-store'
|
|
43
|
+
});
|
|
44
|
+
res.end(body);
|
|
45
|
+
} catch {
|
|
46
|
+
sendJson(res, 404, { ok: false, error: { code: 'NOT_FOUND', message: 'Asset not found' } });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function authOk(url, req, token) {
|
|
51
|
+
if (!token) return true;
|
|
52
|
+
const queryToken = url.searchParams.get('token');
|
|
53
|
+
const auth = req.headers.authorization || '';
|
|
54
|
+
return queryToken === token || auth === `Bearer ${token}`;
|
|
55
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import process from 'node:process';
|
|
4
|
+
import { randomBytes } from 'node:crypto';
|
|
5
|
+
import { intOpt } from './cli-args.mjs';
|
|
6
|
+
import { CliError } from './errors.mjs';
|
|
7
|
+
import { globalWebTokenPath } from './runtime-paths.mjs';
|
|
8
|
+
|
|
9
|
+
export function runtimeConnectHost(host) {
|
|
10
|
+
if (host === '0.0.0.0' || host === '::') return '127.0.0.1';
|
|
11
|
+
return host;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function runtimeBaseUrl(host, port) {
|
|
15
|
+
return `http://${runtimeConnectHost(host)}:${port}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function runtimeApiUrl(runtime, route) {
|
|
19
|
+
const baseUrl = typeof runtime === 'string' ? runtime : runtime?.base_url;
|
|
20
|
+
return new URL(route, baseUrl);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function requestUrl(req) {
|
|
24
|
+
return new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function runtimeUrlQuery(runtime, projectRoot = null) {
|
|
28
|
+
const parts = [];
|
|
29
|
+
if (runtime.token) parts.push(`token=${encodeURIComponent(runtime.token)}`);
|
|
30
|
+
if (projectRoot) parts.push(`project=${encodeURIComponent(projectRoot)}`);
|
|
31
|
+
return parts.length ? `?${parts.join('&')}` : '';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function publicRuntimeUrl(runtime, projectRoot = null) {
|
|
35
|
+
const host = runtime.host === '0.0.0.0' || runtime.host === '::'
|
|
36
|
+
? '<machine-ip>'
|
|
37
|
+
: runtime.host || runtimeConnectHost(runtime.host || '127.0.0.1');
|
|
38
|
+
return `http://${host}:${runtime.port}/${runtimeUrlQuery(runtime, projectRoot)}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function localRuntimeUrl(runtime, projectRoot = null) {
|
|
42
|
+
const host = runtimeConnectHost(runtime.host || '127.0.0.1');
|
|
43
|
+
return `http://${host}:${runtime.port}/${runtimeUrlQuery(runtime, projectRoot)}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function readStoredWebToken() {
|
|
47
|
+
const file = globalWebTokenPath();
|
|
48
|
+
if (!fs.existsSync(file)) return '';
|
|
49
|
+
try {
|
|
50
|
+
return fs.readFileSync(file, 'utf8').trim();
|
|
51
|
+
} catch {
|
|
52
|
+
return '';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function writeStoredWebToken(token) {
|
|
57
|
+
if (!token) return '';
|
|
58
|
+
const file = globalWebTokenPath();
|
|
59
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
60
|
+
fs.writeFileSync(file, `${token}\n`, { mode: 0o600 });
|
|
61
|
+
fs.chmodSync(file, 0o600);
|
|
62
|
+
return file;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function validateWebTokenOpts(opts) {
|
|
66
|
+
if (opts['no-token'] && (opts.token || process.env.HCC_WEB_TOKEN)) {
|
|
67
|
+
throw new CliError('BAD_ARGS', '--no-token cannot be combined with --token or HCC_WEB_TOKEN');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function makeWebToken(opts) {
|
|
72
|
+
validateWebTokenOpts(opts);
|
|
73
|
+
if (opts['no-token']) return '';
|
|
74
|
+
const explicitToken = opts.token || process.env.HCC_WEB_TOKEN || '';
|
|
75
|
+
if (explicitToken) {
|
|
76
|
+
writeStoredWebToken(explicitToken);
|
|
77
|
+
return explicitToken;
|
|
78
|
+
}
|
|
79
|
+
const stored = readStoredWebToken();
|
|
80
|
+
if (stored) return stored;
|
|
81
|
+
const generated = randomBytes(24).toString('base64url');
|
|
82
|
+
writeStoredWebToken(generated);
|
|
83
|
+
return generated;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function expectedWebHost(opts) {
|
|
87
|
+
return opts.host || (opts.local ? '127.0.0.1' : '0.0.0.0');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function webRuntimeMatchesRequest(runtime, opts) {
|
|
91
|
+
if (!runtime) return false;
|
|
92
|
+
if (opts['no-token'] && (opts.token || process.env.HCC_WEB_TOKEN)) return false;
|
|
93
|
+
const expectedHost = expectedWebHost(opts);
|
|
94
|
+
if (runtime.host !== expectedHost) return false;
|
|
95
|
+
const expectedPort = intOpt(opts, 'port', 8787);
|
|
96
|
+
if (opts.port !== undefined && runtime.port !== expectedPort) return false;
|
|
97
|
+
const explicitToken = opts.token || process.env.HCC_WEB_TOKEN || '';
|
|
98
|
+
if (opts['no-token']) return !runtime.token;
|
|
99
|
+
if (explicitToken) return runtime.token === explicitToken;
|
|
100
|
+
return Boolean(runtime.token);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function rememberRuntimeToken(runtime, opts) {
|
|
104
|
+
if (!runtime?.token || opts['no-token']) return;
|
|
105
|
+
const explicitToken = opts.token || process.env.HCC_WEB_TOKEN || '';
|
|
106
|
+
if (explicitToken && runtime.token !== explicitToken) return;
|
|
107
|
+
writeStoredWebToken(runtime.token);
|
|
108
|
+
}
|