boxdown 1.0.0
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/LICENSE +191 -0
- package/README.md +115 -0
- package/assets/devcontainer/README.md +177 -0
- package/assets/devcontainer/devcontainer.json +83 -0
- package/assets/devcontainer/hooks/initialize.sh +55 -0
- package/assets/devcontainer/hooks/post-create.sh +67 -0
- package/assets/devcontainer/hooks/post-start.sh +34 -0
- package/assets/devcontainer/ssh-config-install.sh +172 -0
- package/assets/devcontainer/start.sh +485 -0
- package/assets/devcontainer/utils/codex-cli-update.sh +9 -0
- package/assets/devcontainer/utils/coding-agent-cli-update.sh +360 -0
- package/assets/devcontainer/utils/deps-install.sh +87 -0
- package/assets/devcontainer/utils/ssh-bootstrap.sh +200 -0
- package/dist/bin/cli.cjs +12 -0
- package/dist/bin/cli.d.cts +1 -0
- package/dist/bin/cli.d.mts +1 -0
- package/dist/bin/cli.mjs +14 -0
- package/dist/bin/cli.mjs.map +1 -0
- package/dist/main-BuEptwlL.cjs +1707 -0
- package/dist/main-ZFTrSVgt.mjs +1685 -0
- package/dist/main-ZFTrSVgt.mjs.map +1 -0
- package/dist/main.cjs +7 -0
- package/dist/main.d.cts +24 -0
- package/dist/main.d.cts.map +1 -0
- package/dist/main.d.mts +24 -0
- package/dist/main.d.mts.map +1 -0
- package/dist/main.mjs +3 -0
- package/docs/README.md +34 -0
- package/docs/architecture.md +75 -0
- package/docs/conventions.md +29 -0
- package/docs/development.md +59 -0
- package/docs/features/README.md +14 -0
- package/docs/features/generated-config-and-state.md +64 -0
- package/docs/features/github-auth-refresh.md +64 -0
- package/docs/features/lifecycle.md +64 -0
- package/docs/features/ssh-config-and-proxy.md +60 -0
- package/docs/features/start-and-shell.md +83 -0
- package/docs/testing.md +63 -0
- package/docs/todo.md +170 -0
- package/package.json +128 -0
- package/src/bin/cli.ts +9 -0
- package/src/coding-agents.ts +22 -0
- package/src/config.ts +81 -0
- package/src/constants.ts +9 -0
- package/src/devcontainer-cli.ts +57 -0
- package/src/devcontainer.ts +394 -0
- package/src/doctor.ts +139 -0
- package/src/github-git-auth.ts +143 -0
- package/src/jsonc.ts +123 -0
- package/src/list.ts +102 -0
- package/src/main.ts +362 -0
- package/src/metadata.ts +84 -0
- package/src/paths.ts +117 -0
- package/src/process.ts +94 -0
- package/src/shell.ts +60 -0
- package/src/ssh-config.ts +113 -0
- package/src/ssh-key.ts +52 -0
- package/src/status.ts +327 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Install or refresh coding-agent CLIs inside the devcontainer.
|
|
4
|
+
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
DEFAULT_AGENTS=(codex opencode claude antigravity)
|
|
8
|
+
UPDATE_INTERVAL_SECONDS="${BOXDOWN_CODING_AGENT_UPDATE_INTERVAL_SECONDS:-3600}"
|
|
9
|
+
UPDATE_LOCK_WAIT_SECONDS="${BOXDOWN_CODING_AGENT_UPDATE_LOCK_WAIT_SECONDS:-120}"
|
|
10
|
+
STATE_DIR="${BOXDOWN_CODING_AGENT_UPDATE_STATE_DIR:-${HOME}/.cache/boxdown/coding-agent-clis}"
|
|
11
|
+
|
|
12
|
+
log() {
|
|
13
|
+
echo "coding-agent-cli-update: $*" >&2
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
usage() {
|
|
17
|
+
cat >&2 <<'EOF'
|
|
18
|
+
Usage: coding-agent-cli-update.sh <install|update-now|maybe-update> [codex|opencode|claude|antigravity...]
|
|
19
|
+
EOF
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
numeric_or_default() {
|
|
23
|
+
local value="$1"
|
|
24
|
+
local default_value="$2"
|
|
25
|
+
|
|
26
|
+
case "${value}" in
|
|
27
|
+
'' | *[!0-9]*) printf '%s\n' "${default_value}" ;;
|
|
28
|
+
*) printf '%s\n' "${value}" ;;
|
|
29
|
+
esac
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
env_value() {
|
|
33
|
+
local name="$1"
|
|
34
|
+
local default_value="$2"
|
|
35
|
+
local value="${!name-}"
|
|
36
|
+
|
|
37
|
+
printf '%s\n' "${value:-${default_value}}"
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
agent_env_prefix() {
|
|
41
|
+
case "$1" in
|
|
42
|
+
codex) printf '%s\n' CODEX ;;
|
|
43
|
+
opencode) printf '%s\n' OPENCODE ;;
|
|
44
|
+
claude) printf '%s\n' CLAUDE ;;
|
|
45
|
+
antigravity) printf '%s\n' ANTIGRAVITY ;;
|
|
46
|
+
*)
|
|
47
|
+
log "unknown coding-agent CLI: $1"
|
|
48
|
+
return 1
|
|
49
|
+
;;
|
|
50
|
+
esac
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
agent_stamp_file() {
|
|
54
|
+
local agent="$1"
|
|
55
|
+
local prefix
|
|
56
|
+
local legacy_value
|
|
57
|
+
|
|
58
|
+
prefix="$(agent_env_prefix "${agent}")" || return 1
|
|
59
|
+
if [ "${agent}" = "codex" ]; then
|
|
60
|
+
legacy_value="${BOXDOWN_CODEX_UPDATE_STAMP_FILE:-}"
|
|
61
|
+
if [ -n "${legacy_value}" ]; then
|
|
62
|
+
printf '%s\n' "${legacy_value}"
|
|
63
|
+
return 0
|
|
64
|
+
fi
|
|
65
|
+
fi
|
|
66
|
+
env_value "BOXDOWN_${prefix}_UPDATE_STAMP_FILE" "${STATE_DIR}/${agent}.stamp"
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
agent_lock_dir() {
|
|
70
|
+
local agent="$1"
|
|
71
|
+
local prefix
|
|
72
|
+
local legacy_value
|
|
73
|
+
|
|
74
|
+
prefix="$(agent_env_prefix "${agent}")" || return 1
|
|
75
|
+
if [ "${agent}" = "codex" ]; then
|
|
76
|
+
legacy_value="${BOXDOWN_CODEX_UPDATE_LOCK_DIR:-}"
|
|
77
|
+
if [ -n "${legacy_value}" ]; then
|
|
78
|
+
printf '%s\n' "${legacy_value}"
|
|
79
|
+
return 0
|
|
80
|
+
fi
|
|
81
|
+
fi
|
|
82
|
+
env_value "BOXDOWN_${prefix}_UPDATE_LOCK_DIR" "${STATE_DIR}/${agent}.lock"
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
agent_interval_seconds() {
|
|
86
|
+
local agent="$1"
|
|
87
|
+
local prefix
|
|
88
|
+
|
|
89
|
+
prefix="$(agent_env_prefix "${agent}")" || return 1
|
|
90
|
+
env_value "BOXDOWN_${prefix}_UPDATE_INTERVAL_SECONDS" "${UPDATE_INTERVAL_SECONDS}"
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
agent_lock_wait_seconds() {
|
|
94
|
+
local agent="$1"
|
|
95
|
+
local prefix
|
|
96
|
+
|
|
97
|
+
prefix="$(agent_env_prefix "${agent}")" || return 1
|
|
98
|
+
env_value "BOXDOWN_${prefix}_UPDATE_LOCK_WAIT_SECONDS" "${UPDATE_LOCK_WAIT_SECONDS}"
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
ensure_state_dir() {
|
|
102
|
+
local stamp_file="$1"
|
|
103
|
+
local lock_dir="$2"
|
|
104
|
+
|
|
105
|
+
mkdir -p "$(dirname "${stamp_file}")" "$(dirname "${lock_dir}")"
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
stamp_fresh() {
|
|
109
|
+
local agent="$1"
|
|
110
|
+
local stamp_file="$2"
|
|
111
|
+
local interval_seconds
|
|
112
|
+
local stamp_time
|
|
113
|
+
local now
|
|
114
|
+
|
|
115
|
+
interval_seconds="$(numeric_or_default "$(agent_interval_seconds "${agent}")" 3600)"
|
|
116
|
+
if [ "${interval_seconds}" = "0" ]; then
|
|
117
|
+
return 1
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
if [ ! -f "${stamp_file}" ]; then
|
|
121
|
+
return 1
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
stamp_time="$(stat -c %Y "${stamp_file}" 2>/dev/null || stat -f %m "${stamp_file}" 2>/dev/null || printf '0')"
|
|
125
|
+
now="$(date +%s)"
|
|
126
|
+
[ "$((now - stamp_time))" -lt "${interval_seconds}" ]
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
wait_for_existing_lock() {
|
|
130
|
+
local agent="$1"
|
|
131
|
+
local lock_dir="$2"
|
|
132
|
+
local wait_seconds
|
|
133
|
+
local deadline
|
|
134
|
+
|
|
135
|
+
wait_seconds="$(numeric_or_default "$(agent_lock_wait_seconds "${agent}")" 120)"
|
|
136
|
+
deadline=$((SECONDS + wait_seconds))
|
|
137
|
+
|
|
138
|
+
while [ -d "${lock_dir}" ]; do
|
|
139
|
+
if [ "${SECONDS}" -ge "${deadline}" ]; then
|
|
140
|
+
return 1
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
sleep 1
|
|
144
|
+
done
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
acquire_lock() {
|
|
148
|
+
local agent="$1"
|
|
149
|
+
local lock_dir="$2"
|
|
150
|
+
|
|
151
|
+
if mkdir "${lock_dir}" 2>/dev/null; then
|
|
152
|
+
ACTIVE_LOCK_DIR="${lock_dir}"
|
|
153
|
+
return 0
|
|
154
|
+
fi
|
|
155
|
+
|
|
156
|
+
log "${agent}: another update is running; waiting for it to finish."
|
|
157
|
+
wait_for_existing_lock "${agent}" "${lock_dir}" || {
|
|
158
|
+
log "${agent}: another update is still running; skipping this preflight."
|
|
159
|
+
return 1
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
mkdir "${lock_dir}" 2>/dev/null || return 1
|
|
163
|
+
ACTIVE_LOCK_DIR="${lock_dir}"
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
release_lock() {
|
|
167
|
+
if [ -n "${ACTIVE_LOCK_DIR:-}" ]; then
|
|
168
|
+
rmdir "${ACTIVE_LOCK_DIR}" 2>/dev/null || true
|
|
169
|
+
ACTIVE_LOCK_DIR=""
|
|
170
|
+
fi
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
run_installer_url() {
|
|
174
|
+
local url="$1"
|
|
175
|
+
shift
|
|
176
|
+
|
|
177
|
+
curl -fsSL "${url}" | "$@"
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
install_codex() {
|
|
181
|
+
local url
|
|
182
|
+
|
|
183
|
+
url="${BOXDOWN_CODEX_INSTALL_URL:-https://chatgpt.com/codex/install.sh}"
|
|
184
|
+
run_installer_url "${url}" env CODEX_NON_INTERACTIVE=1 sh
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
update_codex() {
|
|
188
|
+
if command -v codex >/dev/null 2>&1; then
|
|
189
|
+
if codex update; then
|
|
190
|
+
return 0
|
|
191
|
+
fi
|
|
192
|
+
|
|
193
|
+
log "codex: codex update failed; falling back to installer."
|
|
194
|
+
fi
|
|
195
|
+
|
|
196
|
+
install_codex
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
install_opencode() {
|
|
200
|
+
local url
|
|
201
|
+
|
|
202
|
+
url="${BOXDOWN_OPENCODE_INSTALL_URL:-https://opencode.ai/install}"
|
|
203
|
+
run_installer_url "${url}" bash -s -- --no-modify-path
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
update_opencode() {
|
|
207
|
+
if command -v opencode >/dev/null 2>&1; then
|
|
208
|
+
if opencode upgrade --method curl; then
|
|
209
|
+
return 0
|
|
210
|
+
fi
|
|
211
|
+
|
|
212
|
+
log "opencode: opencode upgrade failed; falling back to installer."
|
|
213
|
+
fi
|
|
214
|
+
|
|
215
|
+
install_opencode
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
install_claude() {
|
|
219
|
+
local url
|
|
220
|
+
|
|
221
|
+
url="${BOXDOWN_CLAUDE_INSTALL_URL:-https://claude.ai/install.sh}"
|
|
222
|
+
run_installer_url "${url}" bash
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
update_claude() {
|
|
226
|
+
if command -v claude >/dev/null 2>&1; then
|
|
227
|
+
if claude update; then
|
|
228
|
+
return 0
|
|
229
|
+
fi
|
|
230
|
+
|
|
231
|
+
log "claude: claude update failed; falling back to installer."
|
|
232
|
+
fi
|
|
233
|
+
|
|
234
|
+
install_claude
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
install_antigravity() {
|
|
238
|
+
local url
|
|
239
|
+
|
|
240
|
+
url="${BOXDOWN_ANTIGRAVITY_INSTALL_URL:-https://antigravity.google/cli/install.sh}"
|
|
241
|
+
run_installer_url "${url}" bash -s -- --skip-path
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
update_antigravity() {
|
|
245
|
+
install_antigravity
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
run_agent_update() {
|
|
249
|
+
case "$1" in
|
|
250
|
+
codex) update_codex ;;
|
|
251
|
+
opencode) update_opencode ;;
|
|
252
|
+
claude) update_claude ;;
|
|
253
|
+
antigravity) update_antigravity ;;
|
|
254
|
+
*)
|
|
255
|
+
log "unknown coding-agent CLI: $1"
|
|
256
|
+
return 1
|
|
257
|
+
;;
|
|
258
|
+
esac
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
touch_stamp() {
|
|
262
|
+
local stamp_file="$1"
|
|
263
|
+
|
|
264
|
+
touch "${stamp_file}"
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
update_now_agent() {
|
|
268
|
+
local agent="$1"
|
|
269
|
+
local stamp_file
|
|
270
|
+
local lock_dir
|
|
271
|
+
local result=0
|
|
272
|
+
|
|
273
|
+
stamp_file="$(agent_stamp_file "${agent}")" || return 1
|
|
274
|
+
lock_dir="$(agent_lock_dir "${agent}")" || return 1
|
|
275
|
+
ensure_state_dir "${stamp_file}" "${lock_dir}"
|
|
276
|
+
acquire_lock "${agent}" "${lock_dir}" || return 1
|
|
277
|
+
if run_agent_update "${agent}"; then
|
|
278
|
+
touch_stamp "${stamp_file}"
|
|
279
|
+
else
|
|
280
|
+
result=$?
|
|
281
|
+
fi
|
|
282
|
+
release_lock
|
|
283
|
+
return "${result}"
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
maybe_update_agent() {
|
|
287
|
+
local agent="$1"
|
|
288
|
+
local stamp_file
|
|
289
|
+
local lock_dir
|
|
290
|
+
local result=0
|
|
291
|
+
|
|
292
|
+
stamp_file="$(agent_stamp_file "${agent}")" || return 1
|
|
293
|
+
lock_dir="$(agent_lock_dir "${agent}")" || return 1
|
|
294
|
+
ensure_state_dir "${stamp_file}" "${lock_dir}"
|
|
295
|
+
if stamp_fresh "${agent}" "${stamp_file}"; then
|
|
296
|
+
return 0
|
|
297
|
+
fi
|
|
298
|
+
|
|
299
|
+
acquire_lock "${agent}" "${lock_dir}" || return 0
|
|
300
|
+
if stamp_fresh "${agent}" "${stamp_file}"; then
|
|
301
|
+
release_lock
|
|
302
|
+
return 0
|
|
303
|
+
fi
|
|
304
|
+
|
|
305
|
+
if run_agent_update "${agent}"; then
|
|
306
|
+
touch_stamp "${stamp_file}"
|
|
307
|
+
else
|
|
308
|
+
result=$?
|
|
309
|
+
fi
|
|
310
|
+
release_lock
|
|
311
|
+
return "${result}"
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
run_action_for_agent() {
|
|
315
|
+
local action="$1"
|
|
316
|
+
local agent="$2"
|
|
317
|
+
|
|
318
|
+
case "${action}" in
|
|
319
|
+
install | update-now)
|
|
320
|
+
update_now_agent "${agent}"
|
|
321
|
+
;;
|
|
322
|
+
maybe-update)
|
|
323
|
+
maybe_update_agent "${agent}"
|
|
324
|
+
;;
|
|
325
|
+
*)
|
|
326
|
+
usage
|
|
327
|
+
return 1
|
|
328
|
+
;;
|
|
329
|
+
esac
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
main() {
|
|
333
|
+
local action="${1:-}"
|
|
334
|
+
local failures=0
|
|
335
|
+
local agents
|
|
336
|
+
|
|
337
|
+
if [ -z "${action}" ]; then
|
|
338
|
+
usage
|
|
339
|
+
return 1
|
|
340
|
+
fi
|
|
341
|
+
shift
|
|
342
|
+
|
|
343
|
+
if [ "$#" -eq 0 ]; then
|
|
344
|
+
agents=("${DEFAULT_AGENTS[@]}")
|
|
345
|
+
else
|
|
346
|
+
agents=("$@")
|
|
347
|
+
fi
|
|
348
|
+
|
|
349
|
+
for agent in "${agents[@]}"; do
|
|
350
|
+
if ! run_action_for_agent "${action}" "${agent}"; then
|
|
351
|
+
log "${agent}: ${action} failed."
|
|
352
|
+
failures=$((failures + 1))
|
|
353
|
+
fi
|
|
354
|
+
done
|
|
355
|
+
|
|
356
|
+
[ "${failures}" -eq 0 ]
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
trap 'release_lock' EXIT
|
|
360
|
+
main "$@"
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Install workspace JS dependencies when package.json exists (non-fatal on failure).
|
|
3
|
+
|
|
4
|
+
main() {
|
|
5
|
+
[[ -f package.json ]] || return 0
|
|
6
|
+
local pm
|
|
7
|
+
pm=$(detect_package_manager)
|
|
8
|
+
install_dependencies "$pm" || true
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
detect_package_manager() {
|
|
12
|
+
local pm
|
|
13
|
+
if pm=$(detect_package_manager_from_lockfiles); then
|
|
14
|
+
echo "$pm"
|
|
15
|
+
return
|
|
16
|
+
fi
|
|
17
|
+
if pm=$(detect_package_manager_from_repo_markers); then
|
|
18
|
+
echo "$pm"
|
|
19
|
+
return
|
|
20
|
+
fi
|
|
21
|
+
if pm=$(detect_package_manager_from_package_json); then
|
|
22
|
+
echo "$pm"
|
|
23
|
+
return
|
|
24
|
+
fi
|
|
25
|
+
echo npm
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
detect_package_manager_from_lockfiles() {
|
|
29
|
+
if [[ -f pnpm-lock.yaml ]]; then
|
|
30
|
+
echo pnpm
|
|
31
|
+
return 0
|
|
32
|
+
fi
|
|
33
|
+
if [[ -f bun.lockb || -f bun.lock ]]; then
|
|
34
|
+
echo bun
|
|
35
|
+
return 0
|
|
36
|
+
fi
|
|
37
|
+
if [[ -f yarn.lock ]]; then
|
|
38
|
+
echo yarn
|
|
39
|
+
return 0
|
|
40
|
+
fi
|
|
41
|
+
if [[ -f package-lock.json ]]; then
|
|
42
|
+
echo npm
|
|
43
|
+
return 0
|
|
44
|
+
fi
|
|
45
|
+
return 1
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
detect_package_manager_from_repo_markers() {
|
|
49
|
+
if [[ -f pnpm-workspace.yaml ]]; then
|
|
50
|
+
echo pnpm
|
|
51
|
+
return 0
|
|
52
|
+
fi
|
|
53
|
+
if [[ -f bunfig.toml ]]; then
|
|
54
|
+
echo bun
|
|
55
|
+
return 0
|
|
56
|
+
fi
|
|
57
|
+
if [[ -f .yarnrc.yml ]]; then
|
|
58
|
+
echo yarn
|
|
59
|
+
return 0
|
|
60
|
+
fi
|
|
61
|
+
return 1
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
detect_package_manager_from_package_json() {
|
|
65
|
+
[[ -f package.json ]] || return 1
|
|
66
|
+
node -e "
|
|
67
|
+
const p = require('./package.json');
|
|
68
|
+
const spec = p.packageManager;
|
|
69
|
+
if (!spec || typeof spec !== 'string') process.exit(1);
|
|
70
|
+
const name = spec.split('@')[0].trim();
|
|
71
|
+
if (!['pnpm', 'npm', 'yarn', 'bun'].includes(name)) process.exit(1);
|
|
72
|
+
console.log(name);
|
|
73
|
+
" 2>/dev/null || return 1
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
install_dependencies() {
|
|
77
|
+
local pm="$1"
|
|
78
|
+
case "$pm" in
|
|
79
|
+
pnpm) pnpm install ;;
|
|
80
|
+
npm) npm install ;;
|
|
81
|
+
yarn) yarn install ;;
|
|
82
|
+
bun) bun install ;;
|
|
83
|
+
*) npm install ;;
|
|
84
|
+
esac
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
main "$@"
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Container-side SSH setup for the repo-local devcontainer workflow.
|
|
4
|
+
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
8
|
+
WORKSPACE_FOLDER="${BOXDOWN_CONTAINER_WORKSPACE_FOLDER:-$(cd "${SCRIPT_DIR}/../.." && pwd -P)}"
|
|
9
|
+
REPO_NAME="${BOXDOWN_WORKSPACE_BASENAME:-$(basename "${WORKSPACE_FOLDER}")}"
|
|
10
|
+
SSH_USER="${DEVCONTAINER_SSH_USER:-node}"
|
|
11
|
+
PUBLIC_KEY_FILE="${DEVCONTAINER_SSH_PUBLIC_KEY_FILE:-${WORKSPACE_FOLDER}/.devcontainer/.ssh/id_ed25519.pub}"
|
|
12
|
+
OPENSSH_CONFIGURE_TIMEOUT_SECONDS="${DEVCONTAINER_OPENSSH_CONFIGURE_TIMEOUT_SECONDS:-180}"
|
|
13
|
+
|
|
14
|
+
as_root() {
|
|
15
|
+
if [ "$(id -u)" -eq 0 ]; then
|
|
16
|
+
"$@"
|
|
17
|
+
else
|
|
18
|
+
sudo "$@"
|
|
19
|
+
fi
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
ssh_user_home() {
|
|
23
|
+
getent passwd "${SSH_USER}" | cut -d: -f6
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
openssh_server_package_configured() {
|
|
27
|
+
local status
|
|
28
|
+
|
|
29
|
+
if ! command -v dpkg-query >/dev/null 2>&1; then
|
|
30
|
+
return 0
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
status="$(dpkg-query -W -f='${Status}' openssh-server 2>/dev/null || true)"
|
|
34
|
+
[ "${status}" = "install ok installed" ]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
openssh_server_configured() {
|
|
38
|
+
[ -x /usr/sbin/sshd ] || return 1
|
|
39
|
+
getent passwd sshd >/dev/null 2>&1 || return 1
|
|
40
|
+
openssh_server_package_configured
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
package_manager_running() {
|
|
44
|
+
if command -v pgrep >/dev/null 2>&1; then
|
|
45
|
+
pgrep -x apt >/dev/null 2>&1 ||
|
|
46
|
+
pgrep -x apt-get >/dev/null 2>&1 ||
|
|
47
|
+
pgrep -x dpkg >/dev/null 2>&1
|
|
48
|
+
return $?
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
ps -eo comm= | grep -Eq '^(apt|apt-get|dpkg)$'
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
configure_pending_dpkg_packages() {
|
|
55
|
+
if ! command -v dpkg >/dev/null 2>&1; then
|
|
56
|
+
return 1
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
# Feed "no" answers so a recovering openssh-server postinst cannot block if
|
|
60
|
+
# another path already generated host keys before dpkg completed.
|
|
61
|
+
printf 'n\nn\nn\nn\nn\n' |
|
|
62
|
+
as_root env DEBIAN_FRONTEND=noninteractive dpkg --configure -a
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
ensure_openssh_server_configured() {
|
|
66
|
+
local deadline
|
|
67
|
+
|
|
68
|
+
if openssh_server_configured; then
|
|
69
|
+
return 0
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
echo "ssh-bootstrap: waiting for openssh-server package configuration before generating SSH host keys." >&2
|
|
73
|
+
deadline=$((SECONDS + OPENSSH_CONFIGURE_TIMEOUT_SECONDS))
|
|
74
|
+
|
|
75
|
+
while ! openssh_server_configured; do
|
|
76
|
+
if ! package_manager_running; then
|
|
77
|
+
configure_pending_dpkg_packages || true
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
if openssh_server_configured; then
|
|
81
|
+
return 0
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
if [ "${SECONDS}" -ge "${deadline}" ]; then
|
|
85
|
+
echo "ssh-bootstrap: openssh-server did not finish configuring within ${OPENSSH_CONFIGURE_TIMEOUT_SECONDS}s." >&2
|
|
86
|
+
if command -v dpkg-query >/dev/null 2>&1; then
|
|
87
|
+
dpkg-query -W -f='ssh-bootstrap: openssh-server status: ${Status}\n' openssh-server >&2 || true
|
|
88
|
+
fi
|
|
89
|
+
if ! getent passwd sshd >/dev/null 2>&1; then
|
|
90
|
+
echo "ssh-bootstrap: missing sshd privilege separation user." >&2
|
|
91
|
+
fi
|
|
92
|
+
return 1
|
|
93
|
+
fi
|
|
94
|
+
|
|
95
|
+
sleep 2
|
|
96
|
+
done
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
install_openssh_server() {
|
|
100
|
+
if [ ! -x /usr/sbin/sshd ]; then
|
|
101
|
+
as_root apt-get update
|
|
102
|
+
as_root env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends openssh-server
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
ensure_openssh_server_configured
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
install_authorized_key_if_present() {
|
|
109
|
+
local user_home
|
|
110
|
+
local authorized_keys
|
|
111
|
+
local public_key
|
|
112
|
+
|
|
113
|
+
if [ ! -r "${PUBLIC_KEY_FILE}" ]; then
|
|
114
|
+
echo "ssh-bootstrap: no public key at ${PUBLIC_KEY_FILE}; skipping authorized_keys setup."
|
|
115
|
+
return 0
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
user_home="$(ssh_user_home)"
|
|
119
|
+
if [ -z "${user_home}" ]; then
|
|
120
|
+
echo "ssh-bootstrap: could not resolve home directory for ${SSH_USER}." >&2
|
|
121
|
+
return 1
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
authorized_keys="${user_home}/.ssh/authorized_keys"
|
|
125
|
+
public_key="$(sed -n '1p' "${PUBLIC_KEY_FILE}")"
|
|
126
|
+
|
|
127
|
+
as_root install -d -m 0700 -o "${SSH_USER}" -g "${SSH_USER}" "${user_home}/.ssh"
|
|
128
|
+
as_root touch "${authorized_keys}"
|
|
129
|
+
as_root chown "${SSH_USER}:${SSH_USER}" "${authorized_keys}"
|
|
130
|
+
as_root chmod 0600 "${authorized_keys}"
|
|
131
|
+
|
|
132
|
+
if ! as_root grep -qxF "${public_key}" "${authorized_keys}"; then
|
|
133
|
+
printf '%s\n' "${public_key}" | as_root tee -a "${authorized_keys}" >/dev/null
|
|
134
|
+
fi
|
|
135
|
+
|
|
136
|
+
as_root chown "${SSH_USER}:${SSH_USER}" "${authorized_keys}"
|
|
137
|
+
as_root chmod 0600 "${authorized_keys}"
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
ensure_workspace_home_symlink() {
|
|
141
|
+
local user_home
|
|
142
|
+
local link_path
|
|
143
|
+
|
|
144
|
+
user_home="$(ssh_user_home)"
|
|
145
|
+
if [ -z "${user_home}" ]; then
|
|
146
|
+
echo "ssh-bootstrap: could not resolve home directory for ${SSH_USER}." >&2
|
|
147
|
+
return 1
|
|
148
|
+
fi
|
|
149
|
+
|
|
150
|
+
link_path="${user_home}/${REPO_NAME}"
|
|
151
|
+
|
|
152
|
+
if [ -e "${link_path}" ] && [ ! -L "${link_path}" ]; then
|
|
153
|
+
echo "ssh-bootstrap: ${link_path} already exists and is not a symlink; leaving it unchanged." >&2
|
|
154
|
+
return 0
|
|
155
|
+
fi
|
|
156
|
+
|
|
157
|
+
as_root ln -sfn "${WORKSPACE_FOLDER}" "${link_path}"
|
|
158
|
+
as_root chown -h "${SSH_USER}:${SSH_USER}" "${link_path}"
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
ensure_runtime_ready() {
|
|
162
|
+
if [ ! -x /usr/sbin/sshd ]; then
|
|
163
|
+
echo "ssh-bootstrap: /usr/sbin/sshd is missing. Recreate the devcontainer so post-create installs openssh-server." >&2
|
|
164
|
+
return 1
|
|
165
|
+
fi
|
|
166
|
+
|
|
167
|
+
ensure_openssh_server_configured
|
|
168
|
+
as_root ssh-keygen -A
|
|
169
|
+
as_root install -d -m 0755 /run/sshd
|
|
170
|
+
install_authorized_key_if_present
|
|
171
|
+
ensure_workspace_home_symlink
|
|
172
|
+
as_root /usr/sbin/sshd -t \
|
|
173
|
+
-o PubkeyAuthentication=yes \
|
|
174
|
+
-o PasswordAuthentication=no \
|
|
175
|
+
-o KbdInteractiveAuthentication=no \
|
|
176
|
+
-o AllowTcpForwarding=yes \
|
|
177
|
+
-o AllowStreamLocalForwarding=yes \
|
|
178
|
+
-o PermitTTY=yes
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
main() {
|
|
182
|
+
case "${1:-runtime}" in
|
|
183
|
+
install)
|
|
184
|
+
install_openssh_server
|
|
185
|
+
;;
|
|
186
|
+
runtime)
|
|
187
|
+
ensure_runtime_ready
|
|
188
|
+
;;
|
|
189
|
+
all)
|
|
190
|
+
install_openssh_server
|
|
191
|
+
ensure_runtime_ready
|
|
192
|
+
;;
|
|
193
|
+
*)
|
|
194
|
+
echo "Usage: $(basename "$0") [install|runtime|all]" >&2
|
|
195
|
+
return 1
|
|
196
|
+
;;
|
|
197
|
+
esac
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
main "$@"
|
package/dist/bin/cli.cjs
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const require_main = require('../main-BuEptwlL.cjs');
|
|
3
|
+
|
|
4
|
+
//#region src/bin/cli.ts
|
|
5
|
+
require_main.runCli().then((exitCode) => {
|
|
6
|
+
process.exitCode = exitCode;
|
|
7
|
+
}, (error) => {
|
|
8
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
9
|
+
process.exitCode = 1;
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
//#endregion
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
package/dist/bin/cli.mjs
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { i as runCli } from "../main-ZFTrSVgt.mjs";
|
|
3
|
+
|
|
4
|
+
//#region src/bin/cli.ts
|
|
5
|
+
runCli().then((exitCode) => {
|
|
6
|
+
process.exitCode = exitCode;
|
|
7
|
+
}, (error) => {
|
|
8
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
9
|
+
process.exitCode = 1;
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
//#endregion
|
|
13
|
+
export { };
|
|
14
|
+
//# sourceMappingURL=cli.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.mjs","names":[],"sources":["../../src/bin/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { runCli } from '../main.ts'\n\nrunCli().then((exitCode) => {\n process.exitCode = exitCode\n}, (error: unknown) => {\n process.stderr.write(`${error instanceof Error ? error.message : String(error)}\\n`)\n process.exitCode = 1\n})\n"],"mappings":";;;;AAGA,QAAQ,CAAC,MAAM,aAAa;AAC1B,SAAQ,WAAW;IACjB,UAAmB;AACrB,SAAQ,OAAO,MAAM,GAAG,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM,CAAC,IAAI;AACnF,SAAQ,WAAW;EACnB"}
|