Skip to content
12 changes: 11 additions & 1 deletion packages/app/src/lib/core/templates-entrypoint/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,16 @@ const renderCloneAuthRepoUrl = (): string =>
AUTH_REPO_URL="$(printf "%s" "$REPO_URL" | sed "s#^https://#https://\${RESOLVED_GIT_AUTH_USER}:\${RESOLVED_GIT_AUTH_TOKEN}@#")"
fi`

// CHANGE: refresh shared git mirrors with branch/tag refs only.
// WHY: GitHub exposes refs/pull/* under refs/*; fetching all refs is unbounded for public repos.
// QUOTE(TZ): n/a
// REF: issue-267
// SOURCE: n/a
// FORMAT THEOREM: forall r in fetchedRefs: r in refs/heads/* union refs/tags/*
// PURITY: SHELL
// EFFECT: generated shell performs git fetch through the configured container user
// INVARIANT: mirror refresh excludes refs/pull/* while preserving branch/tag object reuse.
// COMPLEXITY: O(|heads| + |tags|) remote refs
const renderCloneCacheInit = (config: TemplateConfig): string =>
` CLONE_CACHE_ARGS=""
CACHE_REPO_DIR=""
Expand All @@ -135,7 +145,7 @@ const renderCloneCacheInit = (config: TemplateConfig): string =>
chown 1000:1000 "$CACHE_ROOT" || true
if [[ -d "$CACHE_REPO_DIR" ]]; then
if su - ${config.sshUser} -c "git --git-dir '$CACHE_REPO_DIR' rev-parse --is-bare-repository >/dev/null 2>&1"; then
if ! su - ${config.sshUser} -c "GIT_TERMINAL_PROMPT=0 git --git-dir '$CACHE_REPO_DIR' fetch --progress --prune '$AUTH_REPO_URL' '+refs/*:refs/*'"; then
if ! su - ${config.sshUser} -c "GIT_TERMINAL_PROMPT=0 git --git-dir '$CACHE_REPO_DIR' fetch --progress --prune '$AUTH_REPO_URL' '+refs/heads/*:refs/heads/*' '+refs/tags/*:refs/tags/*'"; then
echo "[clone-cache] mirror refresh failed for $REPO_URL"
fi
CLONE_CACHE_ARGS="--reference-if-able '$CACHE_REPO_DIR' --dissociate"
Expand Down
19 changes: 12 additions & 7 deletions packages/app/src/lib/core/templates-prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import { renderZshConfig as renderZshConfigTemplate } from "./templates-zsh.js"
// FORMAT THEOREM: forall s in InteractiveShells: prompt(s) -> includes(time, path, branch|empty)
// PURITY: CORE
// EFFECT: n/a
// INVARIANT: script is deterministic
// INVARIANT: script is deterministic and does not touch TTY state outside interactive shells
// COMPLEXITY: O(1)
const dockerGitTerminalSanitizeShell = String.raw`docker_git_terminal_write_escape() {
if [ -c /dev/tty ]; then
printf "\033[0m\033[?25h\033[?1l\033>\033[?1000l\033[?1002l\033[?1003l\033[?1005l\033[?1006l\033[?1015l\033[?1007l\033[?1004l\033[?2004l\033[>4;0m\033[>4m\033[<u" > /dev/tty 2>/dev/null && return 0
{ printf "\033[0m\033[?25h\033[?1l\033>\033[?1000l\033[?1002l\033[?1003l\033[?1005l\033[?1006l\033[?1015l\033[?1007l\033[?1004l\033[?2004l\033[>4;0m\033[>4m\033[<u" > /dev/tty; } 2>/dev/null && return 0
fi
if [ -t 1 ]; then
printf "\033[0m\033[?25h\033[?1l\033>\033[?1000l\033[?1002l\033[?1003l\033[?1005l\033[?1006l\033[?1015l\033[?1007l\033[?1004l\033[?2004l\033[>4;0m\033[>4m\033[<u"
Expand All @@ -24,14 +24,19 @@ const dockerGitTerminalSanitizeShell = String.raw`docker_git_terminal_write_esca
docker_git_terminal_sanitize() {
# Recover interactive TTY settings after abrupt exits from fullscreen/raw-mode tools.
if [ -c /dev/tty ]; then
stty sane < /dev/tty > /dev/tty 2>/dev/null || stty sane < /dev/tty 2>/dev/null || true
{ stty sane < /dev/tty > /dev/tty; } 2>/dev/null || { stty sane < /dev/tty; } 2>/dev/null || true
elif [ -t 0 ]; then
stty sane 2>/dev/null || true
fi
docker_git_terminal_write_escape || true
}`

const dockerGitPromptScript = `${dockerGitTerminalSanitizeShell}
case "$-" in
*i*) ;;
*) return 0 2>/dev/null || exit 0 ;;
esac

docker_git_branch() { git rev-parse --abbrev-ref HEAD 2>/dev/null; }
docker_git_short_pwd() {
local full_path
Expand Down Expand Up @@ -97,8 +102,8 @@ docker_git_prompt_apply() {
PS1="\${base}> "
fi
}
if [ -n "$PROMPT_COMMAND" ]; then
PROMPT_COMMAND="docker_git_prompt_apply;$PROMPT_COMMAND"
if [ -n "\${PROMPT_COMMAND-}" ]; then
PROMPT_COMMAND="docker_git_prompt_apply;\${PROMPT_COMMAND}"
else
PROMPT_COMMAND="docker_git_prompt_apply"
fi
Expand Down Expand Up @@ -191,7 +196,7 @@ export const renderZshConfig = (): string => renderZshConfigTemplate(dockerGitTe
// FORMAT THEOREM: forall s in InteractiveShells: prompt(s) -> includes(time, path, branch|empty)
// PURITY: CORE
// EFFECT: n/a
// INVARIANT: only interactive shells source /etc/profile.d/zz-prompt.sh
// INVARIANT: only interactive shells mutate prompt or TTY state
// COMPLEXITY: O(1)
export const renderDockerfilePrompt = (): string =>
String.raw`# Shell prompt: show git branch for interactive sessions
Expand Down Expand Up @@ -229,7 +234,7 @@ EOF`
// FORMAT THEOREM: forall s in InteractiveShells: prompt(s) -> includes(time, path, branch|empty)
// PURITY: CORE
// EFFECT: n/a
// INVARIANT: /etc/profile.d/zz-prompt.sh is non-empty after entrypoint
// INVARIANT: /etc/profile.d/zz-prompt.sh is non-empty after entrypoint and inert for non-interactive shells
// COMPLEXITY: O(1)
export const renderEntrypointPrompt = (): string =>
String.raw`# Ensure docker-git prompt is configured for interactive shells
Expand Down
50 changes: 42 additions & 8 deletions packages/app/src/lib/core/templates/dockerfile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,22 @@ import { renderDockerfilePrompt } from "../templates-prompt.js"
import { renderDockerfileGlab } from "./glab.js"
import { renderDockerfileGitleaks, renderDockerfileOpenCode } from "./tools.js"

// CHANGE: use the shared link-foundation JS box as the generated project base image
// WHY: issue #267 asks docker-git to reuse unified box containers instead of maintaining a raw Ubuntu workspace base; the Docker Hub JS alias is public and keeps CI pull size bounded
// QUOTE(ТЗ): "Что бы не зависить только от своих обновлений, а иметь единую инфраструктру есть смысл юзать готовый репозиторий"
// REF: issue-267
// SOURCE: https://github.com/link-foundation/box#docker-hub---combo-boxes
// FORMAT THEOREM: renderDockerfile(config) -> base_image(rendered) = DOCKER_GIT_BASE_IMAGE
// PURITY: CORE
// INVARIANT: the rendered Dockerfile inherits JS/runtime tooling from link-foundation/box while preserving docker-git bootstrap layers
// COMPLEXITY: O(1)/O(1)
const dockerGitBaseImage = "konard/box-js:latest"

const renderDockerfilePrelude = (): string =>
`FROM ubuntu:24.04
`ARG DOCKER_GIT_BASE_IMAGE=${dockerGitBaseImage}
FROM \${DOCKER_GIT_BASE_IMAGE}

USER root
ARG UBUNTU_APT_MIRROR=
ENV DEBIAN_FRONTEND=noninteractive
ENV NVM_DIR=/usr/local/nvm
Expand Down Expand Up @@ -169,7 +182,7 @@ EOF
RUN chmod +x /usr/local/bin/docker-git-playwright-mcp`

const renderDockerfileBunProfile = (): string =>
`RUN printf "export PATH=/usr/local/bun/bin:$PATH\\n" \
`RUN printf "export PATH=/usr/local/bun/bin:\\$PATH\\n" \
> /etc/profile.d/bun.sh && chmod 0644 /etc/profile.d/bun.sh`

const renderDockerfileBun = (config: TemplateConfig): string =>
Expand All @@ -185,21 +198,42 @@ const renderDockerfileBun = (config: TemplateConfig): string =>
.filter((chunk) => chunk.trim().length > 0)
.join("\n")

// CHANGE: normalize inherited box image HOME/PATH/WORKDIR and moved login files after the SSH user rewrite
// WHY: box-js publishes HOME=/home/box and login rc files may contain absolute /home/box references; runtime user paths must be re-bound to the mounted /home/dev volume
// QUOTE(ТЗ): "юзать готовый репозиторий"
// REF: issue-267
// SOURCE: n/a
// FORMAT THEOREM: forall u = config.sshUser: HOME(rendered) = /home/u and forall p in login_rc(u): not contains(p, "/home/box")
// PURITY: CORE
// INVARIANT: tilde-expanded and login-shell runtime paths for the SSH user resolve inside the configured home volume
// COMPLEXITY: O(1)/O(1)
const renderDockerfileUsers = (config: TemplateConfig): string =>
`# Create non-root user for SSH (align UID/GID with host user 1000)
RUN if id -u ubuntu >/dev/null 2>&1; then \
if getent group 1000 >/dev/null 2>&1; then \
EXISTING_GROUP="$(getent group 1000 | cut -d: -f1)"; \
if [ "$EXISTING_GROUP" != "${config.sshUser}" ]; then groupmod -n ${config.sshUser} "$EXISTING_GROUP" || true; fi; \
RUN for BASE_USER in box ubuntu; do \
if [ "$BASE_USER" != "${config.sshUser}" ] && id -u "$BASE_USER" >/dev/null 2>&1; then \
if getent group 1000 >/dev/null 2>&1; then \
EXISTING_GROUP="$(getent group 1000 | cut -d: -f1)"; \
if [ "$EXISTING_GROUP" != "${config.sshUser}" ]; then groupmod -n ${config.sshUser} "$EXISTING_GROUP" || true; fi; \
fi; \
usermod -l ${config.sshUser} -d /home/${config.sshUser} -m -s /usr/bin/zsh "$BASE_USER" || true; \
break; \
fi; \
usermod -l ${config.sshUser} -d /home/${config.sshUser} -m -s /usr/bin/zsh ubuntu || true; \
fi
done
RUN if id -u ${config.sshUser} >/dev/null 2>&1; then \
usermod -u 1000 -g 1000 -o ${config.sshUser}; \
else \
groupadd -g 1000 ${config.sshUser} || true; \
useradd -m -s /usr/bin/zsh -u 1000 -g 1000 -o ${config.sshUser}; \
fi
RUN set -eu; \
if [ -d /home/${config.sshUser} ]; then \
find /home/${config.sshUser} -maxdepth 2 -type f \
\\( -name ".profile" -o -name ".bash_profile" -o -name ".bashrc" -o -name ".zprofile" -o -name ".zshenv" -o -name ".zshrc" \\) \
-exec sed -i -e "s|/home/box|/home/${config.sshUser}|g" -e "s|/home/ubuntu|/home/${config.sshUser}|g" {} +; \
fi
ENV HOME=/home/${config.sshUser}
ENV PATH=/usr/local/bun/bin:/home/${config.sshUser}/.deno/bin:/home/${config.sshUser}/.bun/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
WORKDIR /home/${config.sshUser}
RUN printf "%s\\n" "${config.sshUser} ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/${config.sshUser} \
&& chmod 0440 /etc/sudoers.d/${config.sshUser}

Expand Down
12 changes: 11 additions & 1 deletion packages/lib/src/core/templates-entrypoint/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,16 @@ const renderCloneAuthRepoUrl = (): string =>
AUTH_REPO_URL="$(printf "%s" "$REPO_URL" | sed "s#^https://#https://\${RESOLVED_GIT_AUTH_USER}:\${RESOLVED_GIT_AUTH_TOKEN}@#")"
fi`

// CHANGE: refresh shared git mirrors with branch/tag refs only.
// WHY: GitHub exposes refs/pull/* under refs/*; fetching all refs is unbounded for public repos.
// QUOTE(TZ): n/a
// REF: issue-267
// SOURCE: n/a
// FORMAT THEOREM: forall r in fetchedRefs: r in refs/heads/* union refs/tags/*
// PURITY: SHELL
// EFFECT: generated shell performs git fetch through the configured container user
// INVARIANT: mirror refresh excludes refs/pull/* while preserving branch/tag object reuse.
// COMPLEXITY: O(|heads| + |tags|) remote refs
const renderCloneCacheInit = (config: TemplateConfig): string =>
` CLONE_CACHE_ARGS=""
CACHE_REPO_DIR=""
Expand All @@ -135,7 +145,7 @@ const renderCloneCacheInit = (config: TemplateConfig): string =>
chown 1000:1000 "$CACHE_ROOT" || true
if [[ -d "$CACHE_REPO_DIR" ]]; then
if su - ${config.sshUser} -c "git --git-dir '$CACHE_REPO_DIR' rev-parse --is-bare-repository >/dev/null 2>&1"; then
if ! su - ${config.sshUser} -c "GIT_TERMINAL_PROMPT=0 git --git-dir '$CACHE_REPO_DIR' fetch --progress --prune '$AUTH_REPO_URL' '+refs/*:refs/*'"; then
if ! su - ${config.sshUser} -c "GIT_TERMINAL_PROMPT=0 git --git-dir '$CACHE_REPO_DIR' fetch --progress --prune '$AUTH_REPO_URL' '+refs/heads/*:refs/heads/*' '+refs/tags/*:refs/tags/*'"; then
echo "[clone-cache] mirror refresh failed for $REPO_URL"
fi
CLONE_CACHE_ARGS="--reference-if-able '$CACHE_REPO_DIR' --dissociate"
Expand Down
19 changes: 12 additions & 7 deletions packages/lib/src/core/templates-prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ import { renderZshConfig as renderZshConfigTemplate } from "./templates-zsh.js"
// FORMAT THEOREM: forall s in InteractiveShells: prompt(s) -> includes(time, path, branch|empty)
// PURITY: CORE
// EFFECT: n/a
// INVARIANT: script is deterministic
// INVARIANT: script is deterministic and does not touch TTY state outside interactive shells
// COMPLEXITY: O(1)
const dockerGitTerminalSanitizeShell = String.raw`docker_git_terminal_write_escape() {
if [ -c /dev/tty ]; then
printf "\033[0m\033[?25h\033[?1l\033>\033[?1000l\033[?1002l\033[?1003l\033[?1005l\033[?1006l\033[?1015l\033[?1007l\033[?1004l\033[?2004l\033[>4;0m\033[>4m\033[<u" > /dev/tty 2>/dev/null && return 0
{ printf "\033[0m\033[?25h\033[?1l\033>\033[?1000l\033[?1002l\033[?1003l\033[?1005l\033[?1006l\033[?1015l\033[?1007l\033[?1004l\033[?2004l\033[>4;0m\033[>4m\033[<u" > /dev/tty; } 2>/dev/null && return 0
fi
if [ -t 1 ]; then
printf "\033[0m\033[?25h\033[?1l\033>\033[?1000l\033[?1002l\033[?1003l\033[?1005l\033[?1006l\033[?1015l\033[?1007l\033[?1004l\033[?2004l\033[>4;0m\033[>4m\033[<u"
Expand All @@ -23,14 +23,19 @@ const dockerGitTerminalSanitizeShell = String.raw`docker_git_terminal_write_esca
docker_git_terminal_sanitize() {
# Recover interactive TTY settings after abrupt exits from fullscreen/raw-mode tools.
if [ -c /dev/tty ]; then
stty sane < /dev/tty > /dev/tty 2>/dev/null || stty sane < /dev/tty 2>/dev/null || true
{ stty sane < /dev/tty > /dev/tty; } 2>/dev/null || { stty sane < /dev/tty; } 2>/dev/null || true
elif [ -t 0 ]; then
stty sane 2>/dev/null || true
fi
docker_git_terminal_write_escape || true
}`

const dockerGitPromptScript = `${dockerGitTerminalSanitizeShell}
case "$-" in
*i*) ;;
*) return 0 2>/dev/null || exit 0 ;;
esac

docker_git_branch() { git rev-parse --abbrev-ref HEAD 2>/dev/null; }
docker_git_short_pwd() {
local full_path
Expand Down Expand Up @@ -96,8 +101,8 @@ docker_git_prompt_apply() {
PS1="\${base}> "
fi
}
if [ -n "$PROMPT_COMMAND" ]; then
PROMPT_COMMAND="docker_git_prompt_apply;$PROMPT_COMMAND"
if [ -n "\${PROMPT_COMMAND-}" ]; then
PROMPT_COMMAND="docker_git_prompt_apply;\${PROMPT_COMMAND}"
else
PROMPT_COMMAND="docker_git_prompt_apply"
fi
Expand Down Expand Up @@ -190,7 +195,7 @@ export const renderZshConfig = (): string => renderZshConfigTemplate(dockerGitTe
// FORMAT THEOREM: forall s in InteractiveShells: prompt(s) -> includes(time, path, branch|empty)
// PURITY: CORE
// EFFECT: n/a
// INVARIANT: only interactive shells source /etc/profile.d/zz-prompt.sh
// INVARIANT: only interactive shells mutate prompt or TTY state
// COMPLEXITY: O(1)
export const renderDockerfilePrompt = (): string =>
String.raw`# Shell prompt: show git branch for interactive sessions
Expand Down Expand Up @@ -228,7 +233,7 @@ EOF`
// FORMAT THEOREM: forall s in InteractiveShells: prompt(s) -> includes(time, path, branch|empty)
// PURITY: CORE
// EFFECT: n/a
// INVARIANT: /etc/profile.d/zz-prompt.sh is non-empty after entrypoint
// INVARIANT: /etc/profile.d/zz-prompt.sh is non-empty after entrypoint and inert for non-interactive shells
// COMPLEXITY: O(1)
export const renderEntrypointPrompt = (): string =>
String.raw`# Ensure docker-git prompt is configured for interactive shells
Expand Down
50 changes: 42 additions & 8 deletions packages/lib/src/core/templates/dockerfile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,22 @@ import { renderDockerfilePrompt } from "../templates-prompt.js"
import { renderDockerfileGlab } from "./glab.js"
import { renderDockerfileGitleaks, renderDockerfileOpenCode } from "./tools.js"

// CHANGE: use the shared link-foundation JS box as the generated project base image
// WHY: issue #267 asks docker-git to reuse unified box containers instead of maintaining a raw Ubuntu workspace base; the Docker Hub JS alias is public and keeps CI pull size bounded
// QUOTE(ТЗ): "Что бы не зависить только от своих обновлений, а иметь единую инфраструктру есть смысл юзать готовый репозиторий"
// REF: issue-267
// SOURCE: https://github.com/link-foundation/box#docker-hub---combo-boxes
// FORMAT THEOREM: renderDockerfile(config) -> base_image(rendered) = DOCKER_GIT_BASE_IMAGE
// PURITY: CORE
// INVARIANT: the rendered Dockerfile inherits JS/runtime tooling from link-foundation/box while preserving docker-git bootstrap layers
// COMPLEXITY: O(1)/O(1)
const dockerGitBaseImage = "konard/box-js:latest"

const renderDockerfilePrelude = (): string =>
`FROM ubuntu:24.04
`ARG DOCKER_GIT_BASE_IMAGE=${dockerGitBaseImage}
FROM \${DOCKER_GIT_BASE_IMAGE}

USER root
ARG UBUNTU_APT_MIRROR=
ENV DEBIAN_FRONTEND=noninteractive
ENV NVM_DIR=/usr/local/nvm
Expand Down Expand Up @@ -169,7 +182,7 @@ EOF
RUN chmod +x /usr/local/bin/docker-git-playwright-mcp`

const renderDockerfileBunProfile = (): string =>
`RUN printf "export PATH=/usr/local/bun/bin:$PATH\\n" \
`RUN printf "export PATH=/usr/local/bun/bin:\\$PATH\\n" \
> /etc/profile.d/bun.sh && chmod 0644 /etc/profile.d/bun.sh`

const renderDockerfileBun = (config: TemplateConfig): string =>
Expand All @@ -185,21 +198,42 @@ const renderDockerfileBun = (config: TemplateConfig): string =>
.filter((chunk) => chunk.trim().length > 0)
.join("\n")

// CHANGE: normalize inherited box image HOME/PATH/WORKDIR and moved login files after the SSH user rewrite
// WHY: box-js publishes HOME=/home/box and login rc files may contain absolute /home/box references; runtime user paths must be re-bound to the mounted /home/dev volume
// QUOTE(ТЗ): "юзать готовый репозиторий"
// REF: issue-267
// SOURCE: n/a
// FORMAT THEOREM: forall u = config.sshUser: HOME(rendered) = /home/u and forall p in login_rc(u): not contains(p, "/home/box")
// PURITY: CORE
// INVARIANT: tilde-expanded and login-shell runtime paths for the SSH user resolve inside the configured home volume
// COMPLEXITY: O(1)/O(1)
const renderDockerfileUsers = (config: TemplateConfig): string =>
`# Create non-root user for SSH (align UID/GID with host user 1000)
RUN if id -u ubuntu >/dev/null 2>&1; then \
if getent group 1000 >/dev/null 2>&1; then \
EXISTING_GROUP="$(getent group 1000 | cut -d: -f1)"; \
if [ "$EXISTING_GROUP" != "${config.sshUser}" ]; then groupmod -n ${config.sshUser} "$EXISTING_GROUP" || true; fi; \
RUN for BASE_USER in box ubuntu; do \
if [ "$BASE_USER" != "${config.sshUser}" ] && id -u "$BASE_USER" >/dev/null 2>&1; then \
if getent group 1000 >/dev/null 2>&1; then \
EXISTING_GROUP="$(getent group 1000 | cut -d: -f1)"; \
if [ "$EXISTING_GROUP" != "${config.sshUser}" ]; then groupmod -n ${config.sshUser} "$EXISTING_GROUP" || true; fi; \
fi; \
usermod -l ${config.sshUser} -d /home/${config.sshUser} -m -s /usr/bin/zsh "$BASE_USER" || true; \
break; \
fi; \
usermod -l ${config.sshUser} -d /home/${config.sshUser} -m -s /usr/bin/zsh ubuntu || true; \
fi
done
RUN if id -u ${config.sshUser} >/dev/null 2>&1; then \
usermod -u 1000 -g 1000 -o ${config.sshUser}; \
else \
groupadd -g 1000 ${config.sshUser} || true; \
useradd -m -s /usr/bin/zsh -u 1000 -g 1000 -o ${config.sshUser}; \
fi
RUN set -eu; \
if [ -d /home/${config.sshUser} ]; then \
find /home/${config.sshUser} -maxdepth 2 -type f \
\\( -name ".profile" -o -name ".bash_profile" -o -name ".bashrc" -o -name ".zprofile" -o -name ".zshenv" -o -name ".zshrc" \\) \
-exec sed -i -e "s|/home/box|/home/${config.sshUser}|g" -e "s|/home/ubuntu|/home/${config.sshUser}|g" {} +; \
fi
ENV HOME=/home/${config.sshUser}
ENV PATH=/usr/local/bun/bin:/home/${config.sshUser}/.deno/bin:/home/${config.sshUser}/.bun/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
WORKDIR /home/${config.sshUser}
RUN printf "%s\\n" "${config.sshUser} ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/${config.sshUser} \
&& chmod 0440 /etc/sudoers.d/${config.sshUser}

Expand Down
Loading
Loading