Repository navigation
Expand file tree
/
Copy pathinstall.sh
More file actions
executable file
·700 lines (623 loc) · 20 KB
/
Copy pathinstall.sh
File metadata and controls
executable file
·700 lines (623 loc) · 20 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
NVM_VERSION="v0.40.3"
NODE_MAJOR="22"
PYTHON_BIN="${PYTHON_BIN:-python3}"
UPDATE_WEBUI=0
UPDATE_TAG=""
UPDATE_BRANCH_PREFIX="cmts-webui-"
WITH_PYPNM_DOCSIS=0
PRODUCT_PROFILE=""
PROFILE_FLAG_PW=0
PROFILE_FLAG_PCW=0
WITH_PYPNM_DOCSIS_PW=0
WITH_PYPNM_DOCSIS_PCW=0
PYPNM_DOCSIS_PIP_PACKAGE=""
PYPNM_DOCSIS_CLI_NAME=""
LOCAL_STACK_SHIM_NAME=""
LOCAL_AGENT_LABEL=""
WEBUI_CLI_NAME="pypnm-cmts-webui"
PYPNM_DOCSIS_PATH=""
PYPNM_DOCSIS_VERSION=""
LOCAL_API_HOST=""
LOCAL_API_PORT=""
RECONFIGURE_LOCAL_AGENT=0
DEVELOPMENT_MODE=0
PROFILE_PW="pypnm-webui"
PROFILE_PCW="pypnm-cmts-webui"
RUNTIME_TEMPLATE_PATH="public/config/pypnm-instances.yaml"
RUNTIME_LOCAL_PATH="public/config/pypnm-instances.local.yaml"
RUNTIME_TEMPLATE_DIR="public/config/templates"
log() {
printf '[install] %s\n' "$1"
}
fail() {
printf '[install][error] %s\n' "$1" >&2
exit 1
}
print_banner() {
local banner_path=""
if [ "${PRODUCT_PROFILE}" = "${PROFILE_PW}" ]; then
banner_path="${ROOT_DIR}/tools/banner-pypnm-webui.txt"
elif [ "${PRODUCT_PROFILE}" = "${PROFILE_PCW}" ]; then
banner_path="${ROOT_DIR}/tools/banner-pypnm-cmts-webui.txt"
else
banner_path="${ROOT_DIR}/tools/banner.txt"
fi
printf '\n'
if [ -f "${banner_path}" ]; then
cat "${banner_path}"
else
printf '[install] PyPNM-CMTS-WebUI installer\n'
fi
printf '\n'
printf '[install] repo: %s\n' "${ROOT_DIR}"
printf '[install] options: profile=%s development=%s backend_addon=%s update_webui=%s\n' \
"${PRODUCT_PROFILE:-unset}" "${DEVELOPMENT_MODE}" "${PYPNM_DOCSIS_PIP_PACKAGE:-none}" "${UPDATE_WEBUI}"
printf '\n'
}
ensure_base_prerequisites() {
ensure_command git "git is required but not found. Install git and re-run."
ensure_command curl "curl is required but not found. Install curl and re-run."
}
ensure_python_venv_support() {
if ! "${PYTHON_BIN}" -m venv --help >/dev/null 2>&1; then
maybe_install_python_venv_packages
if ! "${PYTHON_BIN}" -m venv --help >/dev/null 2>&1; then
fail "Python venv support is required but unavailable for ${PYTHON_BIN}."
fi
fi
if smoke_test_python_venv_creation; then
return
fi
maybe_install_python_venv_packages
if smoke_test_python_venv_creation; then
return
fi
local pyver
pyver="$(python_minor_version)"
fail "Python venv creation failed for ${PYTHON_BIN} (${pyver}). Install Python venv tooling (Ubuntu: sudo apt-get install -y python${pyver}-venv python3-venv python3-pip) and re-run."
}
python_minor_version() {
env -u PYTHONPATH -u PYTHONHOME "${PYTHON_BIN}" -I - <<'EOF'
import sys
print(f"{sys.version_info.major}.{sys.version_info.minor}")
EOF
}
smoke_test_python_venv_creation() {
local tmp_dir
tmp_dir="$(mktemp -d)"
local venv_stderr
venv_stderr="${tmp_dir}/venv.stderr"
if env -u PYTHONPATH -u PYTHONHOME "${PYTHON_BIN}" -I -m venv "${tmp_dir}/check" 2>"${venv_stderr}"; then
rm -rf "${tmp_dir}"
return 0
fi
rm -rf "${tmp_dir}"
return 1
}
maybe_install_python_venv_packages() {
if ! command -v apt-get >/dev/null 2>&1; then
return
fi
local distro_id=""
if [ -f /etc/os-release ]; then
# shellcheck disable=SC1091
. /etc/os-release
distro_id="${ID:-}"
fi
case "${distro_id}" in
ubuntu|debian)
;;
*)
return
;;
esac
local pyver
pyver="$(python_minor_version)"
local packages=("python${pyver}-venv" "python3-venv" "python3-pip")
local apt_runner=()
if [ "${EUID}" -eq 0 ]; then
apt_runner=(apt-get)
else
if ! command -v sudo >/dev/null 2>&1; then
fail "Missing sudo. Install Python venv tooling manually: sudo apt-get install -y ${packages[*]}"
fi
apt_runner=(sudo apt-get)
fi
log "Installing missing Python venv tooling via apt (${packages[*]})"
# shellcheck disable=SC2145
"${apt_runner[@]}" update
if [ "${EUID}" -eq 0 ]; then
DEBIAN_FRONTEND=noninteractive apt-get install -y "${packages[@]}"
else
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y "${packages[@]}"
fi
}
print_help() {
cat <<'EOF'
Usage:
./install.sh
./install.sh --development
./install.sh --with-pypnm-webui [options]
./install.sh --with-pypnm-cmts-webui [options]
./install.sh --update-cmts-webui [tag]
./install.sh --with-pypnm-docsis [options]
./install.sh --with-pypnm-docsis-cmts [options]
Options:
--development Install Python development tooling into .venv.
--with-pypnm-webui, -pw
Select the PyPNM-WebUI product profile.
--with-pypnm-cmts-webui, -pcw
Select the PyPNM-CMTS-WebUI product profile.
--update-cmts-webui [tag]
Update this existing checkout to the latest tag or the
provided tag, then reinstall dependencies and refresh
the local runtime config override.
--with-pypnm-docsis Install a local pypnm-docsis backend add-on.
--with-pypnm-docsis-cmts
Install a local pypnm-docsis-cmts backend alongside WebUI.
--pypnm-docsis-path Install pypnm-docsis-cmts from a local PyPNM checkout.
--pypnm-docsis-version
Install a specific pypnm-docsis-cmts version from pip.
--local-api-host Preselect the local API host without prompting.
--local-api-port Set the local API port for generated runtime config.
--reconfigure-local-agent
Ignore any previously configured Local PyPNM-CMTS Agent host.
-h, --help Show this help.
EOF
}
parse_args() {
while [ "$#" -gt 0 ]; do
case "$1" in
--update-cmts-webui|--update-webui)
if [ "$1" = "--update-webui" ]; then
log "WARN: --update-webui is deprecated. Use --update-cmts-webui."
fi
UPDATE_WEBUI=1
if [ "$#" -gt 1 ] && [[ ! "$2" =~ ^- ]]; then
UPDATE_TAG="$2"
shift
fi
;;
--with-pypnm-docsis-cmts|--with-pypnm-docsis)
if [ "$1" = "--with-pypnm-docsis" ]; then
WITH_PYPNM_DOCSIS_PW=1
else
WITH_PYPNM_DOCSIS_PCW=1
fi
WITH_PYPNM_DOCSIS=1
;;
--with-pypnm-webui|-pw)
PROFILE_FLAG_PW=1
;;
--with-pypnm-cmts-webui|-pcw)
PROFILE_FLAG_PCW=1
;;
--development)
DEVELOPMENT_MODE=1
;;
--pypnm-docsis-path)
shift
PYPNM_DOCSIS_PATH="${1:-}"
;;
--pypnm-docsis-version)
shift
PYPNM_DOCSIS_VERSION="${1:-}"
;;
--local-api-host)
shift
LOCAL_API_HOST="${1:-}"
;;
--local-api-port)
shift
LOCAL_API_PORT="${1:-}"
;;
--reconfigure-local-agent)
RECONFIGURE_LOCAL_AGENT=1
;;
-h|--help)
print_help
exit 0
;;
*)
fail "Unknown argument: $1"
;;
esac
shift
done
}
read_env_value() {
local key="$1"
local env_path="${ROOT_DIR}/.env"
if [ ! -f "${env_path}" ]; then
return
fi
sed -n "s/^${key}=//p" "${env_path}" | tail -n1
}
upsert_env_value() {
local key="$1"
local value="$2"
local env_path="${ROOT_DIR}/.env"
if [ ! -f "${env_path}" ]; then
return
fi
if grep -q "^${key}=" "${env_path}"; then
sed -i "s|^${key}=.*$|${key}=${value}|" "${env_path}"
else
printf '\n%s=%s\n' "${key}" "${value}" >>"${env_path}"
fi
}
fail_profile_usage() {
cat >&2 <<'EOF'
[install][error] Invalid product/backend option combination.
[install][error] Valid examples:
[install][error] ./install.sh --with-pypnm-webui
[install][error] ./install.sh --with-pypnm-webui --with-pypnm-docsis
[install][error] ./install.sh --with-pypnm-cmts-webui
[install][error] ./install.sh --with-pypnm-cmts-webui --with-pypnm-docsis-cmts
EOF
exit 1
}
resolve_install_profile() {
if [ "${PROFILE_FLAG_PW}" -eq 1 ] && [ "${PROFILE_FLAG_PCW}" -eq 1 ]; then
fail_profile_usage
fi
if [ "${WITH_PYPNM_DOCSIS_PW}" -eq 1 ] && [ "${WITH_PYPNM_DOCSIS_PCW}" -eq 1 ]; then
fail_profile_usage
fi
if [ "${PROFILE_FLAG_PW}" -eq 1 ]; then
PRODUCT_PROFILE="${PROFILE_PW}"
elif [ "${PROFILE_FLAG_PCW}" -eq 1 ]; then
PRODUCT_PROFILE="${PROFILE_PCW}"
fi
if [ -z "${PRODUCT_PROFILE}" ]; then
PRODUCT_PROFILE="$(read_env_value "PRODUCT_PROFILE" || true)"
fi
if [ -z "${PRODUCT_PROFILE}" ]; then
if [ "${WITH_PYPNM_DOCSIS_PW}" -eq 1 ]; then
PRODUCT_PROFILE="${PROFILE_PW}"
elif [ "${WITH_PYPNM_DOCSIS_PCW}" -eq 1 ]; then
PRODUCT_PROFILE="${PROFILE_PCW}"
fi
fi
if [ -z "${PRODUCT_PROFILE}" ]; then
fail_profile_usage
fi
case "${PRODUCT_PROFILE}" in
"${PROFILE_PW}")
UPDATE_BRANCH_PREFIX="webui-"
WEBUI_CLI_NAME="pypnm-webui"
LOCAL_STACK_SHIM_NAME="pypnm-webui-local-stack"
LOCAL_AGENT_LABEL="Local PyPNM Agent"
;;
"${PROFILE_PCW}")
UPDATE_BRANCH_PREFIX="cmts-webui-"
WEBUI_CLI_NAME="pypnm-cmts-webui"
LOCAL_STACK_SHIM_NAME="pypnm-cmts-webui-local-stack"
LOCAL_AGENT_LABEL="Local PyPNM-CMTS Agent"
;;
*)
fail "Unsupported PRODUCT_PROFILE value: ${PRODUCT_PROFILE}"
;;
esac
if [ "${WITH_PYPNM_DOCSIS_PW}" -eq 1 ] && [ "${PRODUCT_PROFILE}" != "${PROFILE_PW}" ]; then
fail_profile_usage
fi
if [ "${WITH_PYPNM_DOCSIS_PCW}" -eq 1 ] && [ "${PRODUCT_PROFILE}" != "${PROFILE_PCW}" ]; then
fail_profile_usage
fi
if [ "${WITH_PYPNM_DOCSIS_PW}" -eq 1 ]; then
PYPNM_DOCSIS_PIP_PACKAGE="pypnm-docsis"
PYPNM_DOCSIS_CLI_NAME="pypnm-docsis"
elif [ "${WITH_PYPNM_DOCSIS_PCW}" -eq 1 ]; then
PYPNM_DOCSIS_PIP_PACKAGE="pypnm-docsis-cmts"
PYPNM_DOCSIS_CLI_NAME="pypnm-docsis-cmts"
fi
}
ensure_command() {
local cmd="$1"
local message="$2"
if ! command -v "$cmd" >/dev/null 2>&1; then
fail "$message"
fi
}
prepare_nvm() {
if [ ! -d "$HOME/.nvm" ]; then
log "Installing nvm ${NVM_VERSION}"
curl -fsSL "https://raw.githubusercontent.com/nvm-sh/nvm/${NVM_VERSION}/install.sh" | bash
fi
export NVM_DIR="$HOME/.nvm"
if [ -s "$NVM_DIR/nvm.sh" ]; then
log "Loading nvm from ${NVM_DIR}/nvm.sh"
# nvm.sh is not strict-mode safe under set -u in all environments.
local had_errexit=0
local had_nounset=0
case "$-" in
*e*) had_errexit=1 ;;
esac
case "$-" in
*u*) had_nounset=1 ;;
esac
set +e +u
# shellcheck source=/dev/null
. "$NVM_DIR/nvm.sh"
local nvm_source_status=$?
if [ "${had_errexit}" -eq 1 ]; then
set -e
fi
if [ "${had_nounset}" -eq 1 ]; then
set -u
fi
if ! command -v nvm >/dev/null 2>&1; then
fail "Failed to initialize nvm from ${NVM_DIR}/nvm.sh"
fi
if [ "${nvm_source_status}" -ne 0 ]; then
log "nvm init returned non-zero (${nvm_source_status}) but nvm is available; continuing"
fi
else
fail "nvm init script not found at $NVM_DIR/nvm.sh"
fi
log "Installing Node ${NODE_MAJOR}"
nvm install "$NODE_MAJOR"
nvm use "$NODE_MAJOR"
nvm alias default "$NODE_MAJOR" >/dev/null
log "Node version: $(node -v)"
log "npm version: $(npm -v)"
}
ensure_env_file() {
if [ ! -f .env ]; then
if [ -f .env.example ]; then
cp .env.example .env
log "Created .env from .env.example"
else
fail ".env.example not found"
fi
else
log "Keeping existing .env"
fi
}
sync_runtime_template_for_profile() {
local profile_dir=""
if [ "${PRODUCT_PROFILE}" = "${PROFILE_PW}" ]; then
profile_dir="pw"
else
profile_dir="pcw"
fi
local source_template="${RUNTIME_TEMPLATE_DIR}/${profile_dir}/pypnm-instances.yaml"
[ -f "${source_template}" ] || fail "Missing runtime template for profile '${PRODUCT_PROFILE}': ${source_template}"
if [ ! -f "${RUNTIME_TEMPLATE_PATH}" ] || ! cmp -s "${source_template}" "${RUNTIME_TEMPLATE_PATH}"; then
cp "${source_template}" "${RUNTIME_TEMPLATE_PATH}"
log "Applied runtime template for ${PRODUCT_PROFILE}: ${source_template}"
fi
}
run_isolated_venv_python() {
env -u PYTHONPATH -u PYTHONHOME .venv/bin/python -I "$@"
}
ensure_python_development_tooling() {
if [ ! -d .venv ]; then
log "Creating Python virtual environment (.venv)"
env -u PYTHONPATH -u PYTHONHOME "${PYTHON_BIN}" -I -m venv .venv
else
log "Keeping existing Python virtual environment (.venv)"
fi
log "Installing Python development tooling"
run_isolated_venv_python -m pip install --upgrade pip >/dev/null
run_isolated_venv_python -m pip install -r tools/release/requirements.txt >/dev/null
run_isolated_venv_python -m pip install -r requirements-docs.txt >/dev/null
}
ensure_docs_preview_browser() {
ensure_command npx "npx is required for docs UI preview tooling. Install Node/npm and re-run."
log "Ensuring Playwright Chromium is installed for docs UI previews"
npx playwright install chromium >/dev/null
}
ensure_cli_shim() {
local shim_dirs=("$HOME/.local/bin" "$HOME/bin")
local shell_profiles=("$HOME/.bashrc" "$HOME/.profile" "$HOME/.bash_profile" "$HOME/.zshrc" "$HOME/.zprofile")
local cmts_cli_script="${ROOT_DIR}/tools/cli/pypnm-cmts-webui.js"
local path_export_local='export PATH="$HOME/.local/bin:$PATH"'
local path_export_home='export PATH="$HOME/bin:$PATH"'
local primary_shim_name="${WEBUI_CLI_NAME}"
local compat_shim_name="pypnm-cmts-webui"
if [ "${WEBUI_CLI_NAME}" = "pypnm-cmts-webui" ]; then
compat_shim_name="pypnm-webui"
fi
local shim_dir
for shim_dir in "${shim_dirs[@]}"; do
mkdir -p "${shim_dir}"
local primary_shim_path="${shim_dir}/${primary_shim_name}"
local compat_shim_path="${shim_dir}/${compat_shim_name}"
cat >"${primary_shim_path}" <<EOF
#!/usr/bin/env bash
exec "${cmts_cli_script}" "\$@"
EOF
chmod +x "${primary_shim_path}"
log "Installed CLI shim at ${primary_shim_path}"
cat >"${compat_shim_path}" <<EOF
#!/usr/bin/env bash
exec "${cmts_cli_script}" "\$@"
EOF
chmod +x "${compat_shim_path}"
log "Installed compatibility CLI shim at ${compat_shim_path}"
done
local profile
for profile in "${shell_profiles[@]}"; do
if [ ! -f "${profile}" ]; then
touch "${profile}"
fi
if ! grep -Fqx "${path_export_local}" "${profile}"; then
printf '\n%s\n' "${path_export_local}" >>"${profile}"
log "Added ~/.local/bin PATH export to ${profile}"
fi
if ! grep -Fqx "${path_export_home}" "${profile}"; then
printf '\n%s\n' "${path_export_home}" >>"${profile}"
log "Added ~/bin PATH export to ${profile}"
fi
done
if ! command -v "${WEBUI_CLI_NAME}" >/dev/null 2>&1; then
log "~/.local/bin and ~/bin were configured, but PATH is not updated in this shell yet."
log "Run: source ~/.bashrc (or open a new terminal), then run: ${WEBUI_CLI_NAME} --help"
fi
}
merge_runtime_config_override() {
local source_path
if [ -f "$RUNTIME_LOCAL_PATH" ]; then
source_path="$RUNTIME_LOCAL_PATH"
else
source_path="$RUNTIME_TEMPLATE_PATH"
fi
log "Refreshing local runtime config override"
node --input-type=module <<EOF
import { mergeRuntimeConfigFiles } from "./tools/config-menu/runtime_config_merge.js";
mergeRuntimeConfigFiles({
templatePath: "${RUNTIME_TEMPLATE_PATH}",
sourcePath: "${source_path}",
outputPath: "${RUNTIME_LOCAL_PATH}",
});
EOF
}
validate_runtime_config_for_profile() {
log "Validating runtime config for profile ${PRODUCT_PROFILE}"
node --input-type=module <<EOF
import fs from "node:fs";
import { parse } from "yaml";
import {
normalizeConfig,
resolveProfileContext,
validateConfigForProfile,
} from "./tools/config-menu/config_menu.js";
const profileContext = resolveProfileContext("${ROOT_DIR}");
const files = [
{ path: "${RUNTIME_TEMPLATE_PATH}", label: "runtime template" },
{ path: "${RUNTIME_LOCAL_PATH}", label: "local runtime override" },
];
for (const entry of files) {
if (!fs.existsSync(entry.path)) {
continue;
}
const rawConfig = parse(fs.readFileSync(entry.path, "utf8")) ?? {};
const normalizedConfig = normalizeConfig(rawConfig, profileContext);
const validationError = validateConfigForProfile(normalizedConfig, profileContext);
if (validationError) {
console.error(
\`[install][error] Invalid \${entry.label} for profile \${profileContext.productProfile}: \${entry.path}\\n\`
+ \`[install][error] \${validationError}\`,
);
process.exit(1);
}
}
EOF
}
ensure_git_clean_for_update() {
local status
status="$(git status --porcelain --untracked-files=normal)"
if [ -n "$status" ]; then
fail "Working tree is not clean. Commit or stash tracked changes before --update-cmts-webui."
fi
}
resolve_update_ref() {
if [ -n "$UPDATE_TAG" ]; then
printf '%s' "$UPDATE_TAG"
return
fi
local latest_tag
latest_tag="$(git tag --sort=-v:refname | head -n1)"
if [ -z "$latest_tag" ]; then
fail "No git tags found. Provide one explicitly: ./install.sh --update-cmts-webui <tag>"
fi
printf '%s' "$latest_tag"
}
run_update_checkout() {
ensure_command git "git is required for --update-cmts-webui."
ensure_git_clean_for_update
cd "$ROOT_DIR"
log "Fetching latest tags from origin"
git fetch --tags origin
local target_ref
target_ref="$(resolve_update_ref)"
local target_branch
target_branch="${UPDATE_BRANCH_PREFIX}${target_ref}"
log "Checking out ${target_ref} on branch ${target_branch}"
git checkout -B "$target_branch" "$target_ref"
}
run_with_pypnm_docsis_helper() {
local helper_path="${ROOT_DIR}/tools/install/with-pypnm-docsis.sh"
[ -x "${helper_path}" ] || fail "Combined install helper is not executable: ${helper_path}"
local helper_args=(
--root-dir "${ROOT_DIR}"
--python-bin "${PYTHON_BIN}"
--product-profile "${PRODUCT_PROFILE}"
--backend-pip-package "${PYPNM_DOCSIS_PIP_PACKAGE}"
--backend-cli-name "${PYPNM_DOCSIS_CLI_NAME}"
--webui-cli-name "${WEBUI_CLI_NAME}"
--local-stack-shim-name "${LOCAL_STACK_SHIM_NAME}"
--local-agent-label "${LOCAL_AGENT_LABEL}"
)
if [ -n "${PYPNM_DOCSIS_PATH}" ]; then
helper_args+=(--pypnm-docsis-path "${PYPNM_DOCSIS_PATH}")
fi
if [ -n "${PYPNM_DOCSIS_VERSION}" ]; then
helper_args+=(--pypnm-docsis-version "${PYPNM_DOCSIS_VERSION}")
fi
if [ -n "${LOCAL_API_HOST}" ]; then
helper_args+=(--local-api-host "${LOCAL_API_HOST}")
fi
if [ -n "${LOCAL_API_PORT}" ]; then
helper_args+=(--local-api-port "${LOCAL_API_PORT}")
fi
if [ "${RECONFIGURE_LOCAL_AGENT}" -eq 1 ]; then
helper_args+=(--reconfigure-local-agent)
fi
"${helper_path}" "${helper_args[@]}"
}
main() {
parse_args "$@"
resolve_install_profile
print_banner
ensure_base_prerequisites
cd "$ROOT_DIR"
if [ "$UPDATE_WEBUI" -eq 1 ]; then
run_update_checkout
fi
prepare_nvm
ensure_env_file
upsert_env_value "PRODUCT_PROFILE" "${PRODUCT_PROFILE}"
upsert_env_value "VITE_PRODUCT_PROFILE" "${PRODUCT_PROFILE}"
sync_runtime_template_for_profile
log "Installing npm dependencies"
npm ci
log "Registering pypnm-cmts-webui CLI"
npm link >/dev/null
ensure_cli_shim
merge_runtime_config_override
validate_runtime_config_for_profile
if [ "${DEVELOPMENT_MODE}" -eq 1 ]; then
ensure_command "${PYTHON_BIN}" "${PYTHON_BIN} is required for --development. Install Python 3 and re-run."
ensure_python_venv_support
ensure_python_development_tooling
ensure_docs_preview_browser
fi
if [ "${WITH_PYPNM_DOCSIS}" -eq 1 ]; then
ensure_command "${PYTHON_BIN}" "${PYTHON_BIN} is required for backend add-on install. Install Python 3 and re-run."
ensure_python_venv_support
run_with_pypnm_docsis_helper
validate_runtime_config_for_profile
fi
log "Install complete"
if [ "$UPDATE_WEBUI" -eq 1 ]; then
log "Update complete"
log "Checked out update branch: $(git branch --show-current)"
log "Restart any running WebUI server to load the updated version"
log "If About still shows the prior version, hard refresh the browser"
fi
log "Start dev server with: ${WEBUI_CLI_NAME} serve"
if [ "${WITH_PYPNM_DOCSIS}" -eq 1 ]; then
log "Start local backend + WebUI with: ${WEBUI_CLI_NAME} start-local-stack"
fi
if [ "${DEVELOPMENT_MODE}" -eq 1 ]; then
log "Run release workflow with: .venv/bin/python ./tools/release/release.py --help"
fi
log "Edit runtime config with: ${WEBUI_CLI_NAME} config-menu"
}
main "$@"