Skip to content

feat: add Linux and WSL installers#11

Open
malandr wants to merge 2 commits into
mainfrom
feature/wsl-installer
Open

feat: add Linux and WSL installers#11
malandr wants to merge 2 commits into
mainfrom
feature/wsl-installer

Conversation

@malandr
Copy link
Copy Markdown
Collaborator

@malandr malandr commented Mar 20, 2026

Summary

  • add dedicated Linux and WSL installer scripts plus a Windows PowerShell bootstrapper for WSL installs
  • add guided .env setup, optional user systemd startup, and launcher commands for telecli / telecli-wsl
  • add a tag-driven GitHub release workflow and README install instructions for Linux and Windows via WSL

Test Plan

  • source /home/andrey/projects/telecli/venv/bin/activate && pytest -q tests/test_installers.py tests/test_readme.py tests/test_run_web.py

Notes

  • the full suite still has pre-existing unrelated failures in tests/test_llm_integration.py around AIProxy(... fallback_providers=...); this PR does not change that area

Copilot AI review requested due to automatic review settings March 20, 2026 16:30
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds cross-platform installer entrypoints (Linux + WSL, plus a Windows bootstrapper for WSL) and release/README scaffolding so TeleCLI can be installed via dedicated scripts and shipped as tagged-release assets.

Changes:

  • Add scripts/install-linux.sh and scripts/install-wsl.sh with guided .env seeding, optional user systemd startup, and telecli / telecli-wsl launchers.
  • Add install-windows.ps1 to download and run the WSL installer via wsl.exe.
  • Add a tag-driven GitHub Release workflow and README + tests to validate installer/release/docs scaffolding.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
scripts/install-linux.sh New Linux installer + launcher generation + optional systemd user service.
scripts/install-wsl.sh New WSL installer mirroring Linux flow with telecli-wsl launcher.
install-windows.ps1 Windows bootstrapper to download and run the WSL installer through wsl.exe.
README.md Installation docs for Linux and Windows (WSL2) flows.
.github/workflows/release.yml GitHub Actions workflow to publish installer assets and a source bundle on tags.
tests/test_installers.py Tests for help output, dry-run planning, env seeding, systemd service creation, workflow and README checks.

Comment thread scripts/install-linux.sh
Comment on lines +386 to +391
fi
return 0
fi

cp "${PREFIX}/.env.sample" "${ENV_FILE}"
configure_env_file
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The installer writes ${ENV_FILE} containing secrets (e.g. AUTH_TOKEN, TELEGRAM_BOT_TOKEN) but doesn't set restrictive permissions. On systems with a permissive umask this can leave .env world-readable. Consider explicitly setting permissions (e.g. chmod 600 "${ENV_FILE}") after creating/updating it.

Suggested change
fi
return 0
fi
cp "${PREFIX}/.env.sample" "${ENV_FILE}"
configure_env_file
fi
log "[dry-run] chmod 600 ${ENV_FILE}"
return 0
fi
cp "${PREFIX}/.env.sample" "${ENV_FILE}"
configure_env_file
chmod 600 "${ENV_FILE}"

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ea9b035. ensure_env_file() now applies chmod 600 to ${ENV_FILE} for both existing and newly created env files, and the dry-run path logs the permission step as well. Added execution-level coverage that checks the generated .env ends up with mode 0600.

Comment thread scripts/install-wsl.sh
fi

cp "${PREFIX}/.env.sample" "${ENV_FILE}"
configure_env_file
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The installer writes ${ENV_FILE} containing secrets (e.g. AUTH_TOKEN, TELEGRAM_BOT_TOKEN) but doesn't set restrictive permissions. On systems with a permissive umask this can leave .env world-readable. Consider explicitly setting permissions (e.g. chmod 600 "${ENV_FILE}") after creating/updating it.

Suggested change
configure_env_file
configure_env_file
chmod 600 "${ENV_FILE}"

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ea9b035. The WSL installer now also applies chmod 600 to ${ENV_FILE} after creation and when preserving an existing env file, with matching dry-run output. The new integration-style installer test covers the resulting file mode.

Comment thread tests/test_installers.py
Comment on lines +291 to +296

assert "wsl.exe" in text
assert "Invoke-WebRequest" in text
assert "install-wsl.sh" in text
assert "telecli-wsl start" in text

Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test only asserts that install-windows.ps1 contains certain strings; it doesn't validate any behavior (e.g., that the default --prefix passed to WSL is a valid WSL path and not a literal $HOME string). Consider adding an execution-level test (or at least a stricter assertion) that would catch broken defaults/argument wiring in the bootstrapper.

Suggested change
assert "wsl.exe" in text
assert "Invoke-WebRequest" in text
assert "install-wsl.sh" in text
assert "telecli-wsl start" in text
# Basic wiring: the Windows installer should fetch and invoke the WSL installer.
assert "wsl.exe" in text
assert "Invoke-WebRequest" in text
assert "install-wsl.sh" in text
assert "telecli-wsl start" in text
# Stricter check: ensure the bootstrapper actually passes a --prefix argument
# and does not forward a literal $HOME bash variable into WSL, which would
# indicate broken default/argument wiring.
assert "--prefix" in text
assert "--prefix \"$HOME\"" not in text
assert "--prefix '$HOME'" not in text
assert "$HOME" not in text

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ea9b035. I tightened the Windows bootstrap test to assert the --prefix wiring now uses the WSL-valid ~/.local/share/telecli default and no longer embeds the literal $HOME form. I also added an execution-level installer test for both Linux and WSL that validates generated env permissions, secret-safe token output, and the generated launcher behavior.

Comment thread scripts/install-linux.sh
url
;;
*)
printf 'Usage: ${LAUNCHER_NAME} {start|stop|restart|status|logs|url}\n' >&2
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The generated launcher script prints Usage: ${LAUNCHER_NAME} ... using single quotes, and the launcher script itself never defines LAUNCHER_NAME, so the error output will literally show ${LAUNCHER_NAME} instead of telecli. Consider hardcoding the launcher name into the generated script (or defining LAUNCHER_NAME inside the launcher) and using double quotes so it expands correctly.

Suggested change
printf 'Usage: ${LAUNCHER_NAME} {start|stop|restart|status|logs|url}\n' >&2
printf 'Usage: telecli {start|stop|restart|status|logs|url}\n' >&2

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked this one against the generated launcher rather than only the template. The installer heredoc expands ${LAUNCHER_NAME} at generation time, so the produced launcher already prints Usage: telecli ... on invalid input. I left the production code as-is and added execution-level coverage in ea9b035 that runs the generated launcher and asserts the stderr uses telecli, not ${LAUNCHER_NAME}.

Comment thread scripts/install-wsl.sh
url
;;
*)
printf 'Usage: ${LAUNCHER_NAME} {start|stop|restart|status|logs|url}\n' >&2
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The generated launcher script prints Usage: ${LAUNCHER_NAME} ... using single quotes, and the launcher script itself never defines LAUNCHER_NAME, so the error output will literally show ${LAUNCHER_NAME} instead of telecli-wsl. Consider hardcoding the launcher name into the generated script (or defining LAUNCHER_NAME inside the launcher) and using double quotes so it expands correctly.

Suggested change
printf 'Usage: ${LAUNCHER_NAME} {start|stop|restart|status|logs|url}\n' >&2
printf 'Usage: telecli-wsl {start|stop|restart|status|logs|url}\n' >&2

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same conclusion here: the generated WSL launcher already renders Usage: telecli-wsl ... because ${LAUNCHER_NAME} is expanded by the installer when writing the script. I added execution-level coverage in ea9b035 that invokes the generated telecli-wsl launcher with a bad subcommand and asserts the real stderr output.

Comment thread install-windows.ps1 Outdated
[string]$Distro = "",
[string]$RepoUrl = "https://github.com/malandr/telecli.git",
[string]$Ref = "main",
[string]$Prefix = '$HOME/.local/share/telecli',
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$Prefix defaults to the literal string '$HOME/.local/share/telecli', so the WSL-side bash installer receives an unexpanded $HOME path and will install into a directory literally named $HOME (relative to the working dir). Consider defaulting $Prefix to empty and omitting --prefix unless explicitly provided, or using a WSL-valid default like ~/.local/share/telecli.

Suggested change
[string]$Prefix = '$HOME/.local/share/telecli',
[string]$Prefix = '~/.local/share/telecli',

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ea9b035. The PowerShell bootstrapper now defaults $Prefix to ~/.local/share/telecli, so the WSL-side installer receives a shell-valid home-relative path instead of the literal $HOME/... string.

Comment thread scripts/install-linux.sh Outdated
set_env_value AI_PROXY_PROVIDER "${ai_proxy_provider}"

if [ -n "${generated_auth_token}" ]; then
log "Generated AUTH_TOKEN: ${generated_auth_token}"
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

configure_env_file logs the newly generated AUTH_TOKEN to stdout. This exposes a secret in terminal output (which may be captured in CI logs, shell scrollback, or transcripts). Consider avoiding printing the token value; instead print a message telling the user where it was written (e.g. ${ENV_FILE}) or only show it when explicitly requested via a flag.

Suggested change
log "Generated AUTH_TOKEN: ${generated_auth_token}"
log "Generated AUTH_TOKEN and stored it in ${ENV_FILE}"

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ea9b035. The Linux installer no longer prints the generated AUTH_TOKEN value; it now logs only that a token was generated and written to ${ENV_FILE}. The new execution-level test extracts the token from the generated .env and asserts it never appears in stdout/stderr.

Comment thread scripts/install-wsl.sh Outdated
set_env_value AI_PROXY_PROVIDER "${ai_proxy_provider}"

if [ -n "${generated_auth_token}" ]; then
log "Generated AUTH_TOKEN: ${generated_auth_token}"
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

configure_env_file logs the newly generated AUTH_TOKEN to stdout. This exposes a secret in terminal output (which may be captured in CI logs, shell scrollback, or transcripts). Consider avoiding printing the token value; instead print a message telling the user where it was written (e.g. ${ENV_FILE}) or only show it when explicitly requested via a flag.

Suggested change
log "Generated AUTH_TOKEN: ${generated_auth_token}"
log "Generated AUTH_TOKEN and saved it to ${ENV_FILE}. Keep this token secret and do not share it."

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ea9b035. The WSL installer now mirrors the Linux behavior and logs only that the generated token was written to ${ENV_FILE}, without echoing the secret value. The new integration-style test covers both installers.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 8 comments.

Comment thread install-windows.ps1
$scriptUrl = "https://raw.githubusercontent.com/malandr/telecli/$Ref/scripts/install-wsl.sh"

Write-Step "Downloading WSL installer from $scriptUrl"
$wslInstaller = Invoke-WebRequest -UseBasicParsing -Uri $scriptUrl | Select-Object -ExpandProperty Content
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Invoke-WebRequest -UseBasicParsing is not supported in PowerShell 6+/7 (it was specific to Windows PowerShell 5.1). This will break installs on systems where users run pwsh. Consider removing -UseBasicParsing or conditionally adding it only when it’s available.

Suggested change
$wslInstaller = Invoke-WebRequest -UseBasicParsing -Uri $scriptUrl | Select-Object -ExpandProperty Content
$wslInstaller = Invoke-WebRequest -Uri $scriptUrl | Select-Object -ExpandProperty Content

Copilot uses AI. Check for mistakes.
Comment thread scripts/install-wsl.sh
Comment on lines +213 to +216
--prefix)
PREFIX="$2"
shift 2
;;
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--prefix arguments coming from non-shell callers (notably the Windows bootstrapper) may include a leading ~, but bash won’t perform tilde expansion on values stored in variables. With PREFIX="$2", a value like ~/.local/share/telecli will be treated as a literal path. Consider normalizing PREFIX (and similarly BIN_DIR/STATE_DIR) after parsing options by expanding a leading ~ to $HOME.

Copilot uses AI. Check for mistakes.
Comment thread scripts/install-wsl.sh
if [ -d "${PREFIX}/.git" ]; then
run_cmd git -C "${PREFIX}" fetch --tags origin
run_cmd git -C "${PREFIX}" checkout "${REF}"
if [[ "${REF}" != refs/tags/* ]] && [[ "${REF}" != v* ]]; then
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sync_repo skips git pull when REF matches v*, but that also matches legitimate branch names that start with v (e.g. vNext). That can leave an existing install stuck on an old commit even though it’s meant to track a branch. Consider using an explicit tag check (e.g. verifying refs/tags/$REF) instead of a broad v* heuristic.

Suggested change
if [[ "${REF}" != refs/tags/* ]] && [[ "${REF}" != v* ]]; then
# Only pull when REF is not a tag. Treat both fully-qualified
# refs/tags/* and lightweight tag names as tags.
if ! git -C "${PREFIX}" show-ref --verify --quiet "refs/tags/${REF}" && [[ "${REF}" != refs/tags/* ]]; then

Copilot uses AI. Check for mistakes.
Comment thread scripts/install-wsl.sh
Comment on lines +376 to +406
ensure_env_file() {
if [ -f "${ENV_FILE}" ]; then
log "Keeping existing ${ENV_FILE}"
if is_dry_run; then
log "[dry-run] chmod 600 ${ENV_FILE}"
return 0
fi
chmod 600 "${ENV_FILE}"
return 0
fi

if is_dry_run; then
log "[dry-run] cp ${PREFIX}/.env.sample ${ENV_FILE}"
if auto_config; then
log "[dry-run] apply TELECLI_INSTALL_* overrides to ${ENV_FILE}"
fi
log "[dry-run] chmod 600 ${ENV_FILE}"
return 0
fi

cp "${PREFIX}/.env.sample" "${ENV_FILE}"
configure_env_file
chmod 600 "${ENV_FILE}"
}

install_startup_service() {
local service_content

if [ "${START_AT_STARTUP}" != "true" ]; then
return 0
fi
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TELECLI_INSTALL_START_AT_STARTUP is normalized inside configure_env_file, but ensure_env_file returns early when ${ENV_FILE} already exists and never calls configure_env_file. That means rerunning the installer with TELECLI_INSTALL_START_AT_STARTUP=true won’t actually enable the startup service if a .env is already present. Consider parsing/normalizing START_AT_STARTUP outside of configure_env_file (before ensure_env_file), so startup behavior can be controlled independently of whether .env already exists.

Copilot uses AI. Check for mistakes.
Comment thread scripts/install-linux.sh
if [ -d "${PREFIX}/.git" ]; then
run_cmd git -C "${PREFIX}" fetch --tags origin
run_cmd git -C "${PREFIX}" checkout "${REF}"
if [[ "${REF}" != refs/tags/* ]] && [[ "${REF}" != v* ]]; then
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sync_repo skips git pull when REF matches v*, which will also match branch names starting with v and can prevent expected updates for an existing installation tracking a branch. Consider replacing the v* heuristic with an explicit tag check (e.g. verify refs/tags/$REF) so only real tags skip the pull.

Suggested change
if [[ "${REF}" != refs/tags/* ]] && [[ "${REF}" != v* ]]; then
# Determine if REF refers to a tag; only skip pull for real tags
local is_tag="false"
if [[ "${REF}" == refs/tags/* ]]; then
is_tag="true"
elif git -C "${PREFIX}" show-ref --verify --quiet "refs/tags/${REF}"; then
is_tag="true"
fi
if [ "${is_tag}" != "true" ]; then

Copilot uses AI. Check for mistakes.
Comment thread scripts/install-linux.sh
Comment on lines +403 to +404

if [ "${START_AT_STARTUP}" != "true" ]; then
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TELECLI_INSTALL_START_AT_STARTUP is only applied inside configure_env_file, but ensure_env_file returns early when .env already exists and never calls configure_env_file. As a result, rerunning the installer to enable startup (or to change startup choice) won’t work unless the user deletes .env first. Consider normalizing START_AT_STARTUP outside of env-file configuration so the startup-service decision is independent of whether .env exists.

Suggested change
if [ "${START_AT_STARTUP}" != "true" ]; then
local effective_start_at_startup
# Determine effective START_AT_STARTUP value, preferring TELECLI_INSTALL_START_AT_STARTUP if set.
effective_start_at_startup="${START_AT_STARTUP}"
if [ -n "${TELECLI_INSTALL_START_AT_STARTUP:-}" ]; then
effective_start_at_startup="${TELECLI_INSTALL_START_AT_STARTUP}"
fi
if [ "${effective_start_at_startup}" != "true" ]; then

Copilot uses AI. Check for mistakes.
Comment thread tests/test_installers.py
Comment on lines +368 to +374
assert "[string]$Prefix = '~/.local/share/telecli'" in text
assert "wsl.exe" in text
assert "Invoke-WebRequest" in text
assert "install-wsl.sh" in text
assert "telecli-wsl start" in text
assert "--prefix" in text
assert "$HOME/.local/share/telecli" not in text
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test enforces a default $Prefix of ~/.local/share/telecli in install-windows.ps1, but ~ won’t be expanded when passed as an argv value into the WSL-side bash installer (it will be treated as a literal directory name). After fixing the bootstrapper behavior (e.g. omit --prefix unless explicitly provided, or pass an already-expanded absolute path), update this test to assert the corrected default wiring so it would catch regressions.

Suggested change
assert "[string]$Prefix = '~/.local/share/telecli'" in text
assert "wsl.exe" in text
assert "Invoke-WebRequest" in text
assert "install-wsl.sh" in text
assert "telecli-wsl start" in text
assert "--prefix" in text
assert "$HOME/.local/share/telecli" not in text
# The Windows bootstrapper should not hard-code a tilde- or $HOME-based prefix
# that would be passed directly to the WSL-side installer, since bash would
# treat those as literal paths instead of expanding them.
assert "'~/.local/share/telecli'" not in text
assert "$HOME/.local/share/telecli" not in text
assert "wsl.exe" in text
assert "Invoke-WebRequest" in text
assert "install-wsl.sh" in text
assert "telecli-wsl start" in text
# After fixing the bootstrapper, --prefix should not be unconditionally wired
# into the WSL installer invocation.
assert "--prefix" not in text

Copilot uses AI. Check for mistakes.
Comment thread install-windows.ps1
Comment on lines +3 to +6
[string]$RepoUrl = "https://github.com/malandr/telecli.git",
[string]$Ref = "main",
[string]$Prefix = '~/.local/share/telecli',
[switch]$SkipSystemPackages
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Windows bootstrapper defaults $Prefix to "~/.local/share/telecli" and always passes it through to the bash installer. In this execution path there is no Linux shell to expand ~, so the WSL-side script will treat it as a literal directory name (e.g. install into a ~ folder) rather than the user’s home directory. Consider either omitting --prefix unless the user explicitly supplies one (letting the bash script use its $HOME-based default), or passing an already-expanded absolute WSL path.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants