webui: Console pinned copy, update step and service hardening (#1571)
Row 77, round 3 (round 2 rebased onto row 95). Rocko built it. This covers row 75's N1-N5 and Vikunja 146 and 147. `install --copy ID` refuses unless every ExecStart path of the bus unit, read from `systemctl --user show` so drop-ins count, lies inside that copy. It also refuses when the unit isn't loaded, no unit is loaded, ExecStart has no path, or systemctl fails. COPY_PATHS gains packages/tasks/src and packages/tasks/fields.json. Every copy directory is 0555. TOOLS.md and the webui README give the removal command and the rollback floor. Candidate manifest d9880f4c92d1d976027562a03cc5dd07512954e86f49f1da0ebb34e642a955f5 (10 paths, 8 applied by the patch, 2 packet files already committed), base2e26fbe1, packet agents/rocko/work/queue-77/r2b/. Approvals on round 3: Darkwing 27979 and Filbert 27985 (Filbert round 2: 27898). Sage's gate rerun 22:32:51Z-22:35:12Z on459c4fc6plus the patch (row 92 included), manifest 10/10: webui 101, control-board 124, and every scripts/test-*.sh exited 0 (test-release 4, Docker cases skipped at a dead socket). Live at the next Console install and restart (decision 109). Mos makes that call. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
+221
-22
@@ -1,18 +1,44 @@
|
||||
#!/usr/bin/env bash
|
||||
# `scripts/webui-service.sh render|install|uninstall|status`: the systemd
|
||||
# user unit that runs the Console, `node packages/webui/src/cli.mjs serve`
|
||||
# (lead decision 94). The template is
|
||||
# `scripts/webui-service.sh copy|render|install|uninstall|status`: the
|
||||
# systemd user unit that runs the Console, `node packages/webui/src/cli.mjs
|
||||
# serve` (lead decision 94), from a read-only copy of one landed commit
|
||||
# (row 77, #1571, lead decision 112). The template is
|
||||
# packages/webui/systemd/mosaic-webui.service.in; the rendered unit is
|
||||
# mosaic-webui.service in the systemd user directory, one instance.
|
||||
#
|
||||
# render [FLAGS] print the rendered unit on stdout
|
||||
# install [--dir DIR] [--no-reload] [FLAGS]
|
||||
# copy COMMIT [--copies DIR] [--allow-worktree]
|
||||
# write `git archive` of the paths the Console
|
||||
# loads at COMMIT into DIR/<full id>/, read-only;
|
||||
# DIR defaults to
|
||||
# ${XDG_DATA_HOME:-~/.local/share}/mosaic-console
|
||||
# render [--copy ID [--copies DIR]] [FLAGS]
|
||||
# print the rendered unit on stdout; without
|
||||
# --copy it runs from this checkout
|
||||
# install --copy ID [--copies DIR] [--dir DIR] [--no-reload] [--allow-worktree] [FLAGS]
|
||||
# write the unit (write to a temp file, then
|
||||
# rename) and run `systemctl --user daemon-reload`
|
||||
# rename) and run `systemctl --user daemon-reload`;
|
||||
# refuses while the unit is running
|
||||
# uninstall [--dir DIR] [--no-reload]
|
||||
# remove the unit; refuses while it is active
|
||||
# status [--dir DIR] unit state, whether it sends, then GET
|
||||
# /healthz on its port
|
||||
# remove the unit; refuses while it is running
|
||||
# status [--dir DIR] unit state, the copy it runs and whether
|
||||
# refactor has moved past it, whether it sends,
|
||||
# then GET /healthz on its port
|
||||
#
|
||||
# With --copy the unit's WorkingDirectory and ExecStart point at the copy,
|
||||
# and ExecStart passes this checkout as serve's --checkout: the queue read
|
||||
# runs the checkout's queue-read.mjs, since the queue store refuses code
|
||||
# outside the canonical checkout; everything else comes from the copy.
|
||||
#
|
||||
# copy and install run only from the canonical checkout, whose root holds a
|
||||
# .git directory (a linked worktree has a .git file). --allow-worktree lifts
|
||||
# that refusal and exists for the tests only, which run in worktrees.
|
||||
#
|
||||
# The update step: `copy COMMIT`; Jason or Mos stops the unit; `install
|
||||
# --copy ID` with the same flags; Jason or Mos starts it. Rollback is the
|
||||
# same with the earlier id. Copies stay until someone removes them. install
|
||||
# refuses unless the bus unit runs from the same copy (bus_on_copy below),
|
||||
# and nothing puts the bus on a copy yet, so the step can't be used until a
|
||||
# later row does.
|
||||
#
|
||||
# FLAGS are serve's: --port N (default 7332; 7330 is the operator's own
|
||||
# Console), --business ID, --board URL, and the preview set --public-host
|
||||
@@ -29,19 +55,27 @@
|
||||
# decision 102, which accepts that sends from a Console Mos started enter
|
||||
# the trail as Jason (human-cli). Any other agent starting it, even detached
|
||||
# through systemd, works around the human proof (lead decisions 62 and 94).
|
||||
# install and uninstall refuse while the unit is running, with or without
|
||||
# --no-reload: a unit rewritten under a running Console would take the new
|
||||
# ExecStart at its next Restart=on-failure, with nobody starting it.
|
||||
#
|
||||
# Exit codes: 0 ok, 1 operation failed, 4 usage.
|
||||
set -euo pipefail
|
||||
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
TEMPLATE="$REPO/packages/webui/systemd/mosaic-webui.service.in"
|
||||
UNIT_NAME="mosaic-webui.service"
|
||||
USAGE="usage: scripts/webui-service.sh render [FLAGS] | install [--dir DIR] [--no-reload] [FLAGS] | uninstall [--dir DIR] [--no-reload] | status [--dir DIR]
|
||||
FLAGS: [--port N] [--business ID] [--board URL] [--public-host NAME --auth-header HEADER --allow-user ID [--send]]"
|
||||
USAGE="usage: scripts/webui-service.sh copy COMMIT [--copies DIR] [--allow-worktree] | render [--copy ID [--copies DIR]] [FLAGS] | install --copy ID [--copies DIR] [--dir DIR] [--no-reload] [--allow-worktree] [FLAGS] | uninstall [--dir DIR] [--no-reload] | status [--dir DIR]
|
||||
FLAGS: [--port N] [--business ID] [--board URL] [--public-host NAME --auth-header HEADER --allow-user ID [--send]]
|
||||
--allow-worktree is for the tests only: copy and install otherwise refuse outside the canonical checkout."
|
||||
|
||||
die() { echo "webui-service: $*" >&2; exit 1; }
|
||||
usage() { echo "$USAGE" >&2; exit 4; }
|
||||
|
||||
UNIT_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
|
||||
COPIES="${XDG_DATA_HOME:-$HOME/.local/share}/mosaic-console"
|
||||
COPY_ID=""
|
||||
COPY_FLAGS=0
|
||||
ALLOW_WORKTREE=0
|
||||
RELOAD=1
|
||||
DIR_FLAGS=0
|
||||
PORT=7332
|
||||
@@ -57,6 +91,9 @@ parse_flags() {
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--dir) [ $# -ge 2 ] || usage; UNIT_DIR="$2"; DIR_FLAGS=1; shift 2 ;;
|
||||
--copy) [ $# -ge 2 ] || usage; COPY_ID="$2"; COPY_FLAGS=1; shift 2 ;;
|
||||
--copies) [ $# -ge 2 ] || usage; COPIES="$2"; COPY_FLAGS=1; shift 2 ;;
|
||||
--allow-worktree) ALLOW_WORKTREE=1; shift ;;
|
||||
--no-reload) RELOAD=0; DIR_FLAGS=1; shift ;;
|
||||
--port) [ $# -ge 2 ] || usage; PORT="$2"; SERVE_SET=1; shift 2 ;;
|
||||
--business) [ $# -ge 2 ] || usage; BUSINESS="$2"; SERVE_SET=1; shift 2 ;;
|
||||
@@ -102,13 +139,69 @@ serve_flags() {
|
||||
case "$PUBLIC_HOST" in *[!A-Za-z0-9.-]*) die "public host must be a hostname, no port or scheme: $PUBLIC_HOST" ;; esac
|
||||
case "$AUTH_HEADER" in [A-Za-z0-9]*) ;; *) die "auth header must be a header name: $AUTH_HEADER" ;; esac
|
||||
case "$AUTH_HEADER" in *[!A-Za-z0-9-]*) die "auth header must be a header name: $AUTH_HEADER" ;; esac
|
||||
case "$ALLOW_USER" in [A-Za-z0-9]*) ;; *) die "allowed user must start with a letter or digit: $ALLOW_USER" ;; esac
|
||||
case "$ALLOW_USER" in *[!A-Za-z0-9._@+-]*) die "allowed user contains a character the unit cannot carry: $ALLOW_USER" ;; esac
|
||||
# Lead decision 101: the uid header carries the Authentik uid, 64
|
||||
# lower-case hex characters, and nothing else is let through on it.
|
||||
if [ "${AUTH_HEADER,,}" = x-authentik-uid ] && ! [[ "$ALLOW_USER" =~ ^[0-9a-f]{64}$ ]]; then
|
||||
die "with --auth-header $AUTH_HEADER the allowed user must be an Authentik uid, 64 lower-case hex characters (^[0-9a-f]{64}\$, lead decision 101): $ALLOW_USER"
|
||||
fi
|
||||
flags="$flags --public-host $PUBLIC_HOST --auth-header $AUTH_HEADER --allow-user $ALLOW_USER"
|
||||
fi
|
||||
[ "$SEND" = 0 ] || flags="$flags --send"
|
||||
echo "$flags"
|
||||
}
|
||||
|
||||
# The paths the Console loads, the only ones a copy holds: serve's import
|
||||
# closure (packages/webui/src/cli.mjs and the human transport child,
|
||||
# packages/bus/src/human-cli.mjs) reaches these nine src directories
|
||||
# (packages/tasks/src since row 61: reads.mjs imports its field table
|
||||
# loader, which reads packages/tasks/fields.json);
|
||||
# serve runs scripts/mosaic-config.mjs as its config check and reads roles/
|
||||
# and RELEASE; the unit's Documentation= names the README. The queue read
|
||||
# runs from the checkout (serve --checkout), so packages/queue is not here.
|
||||
# The pinned-copy test serves from a copy: a missing import fails it at
|
||||
# load, and a missing file read at request time (fields.json, RELEASE,
|
||||
# roles/) fails the route that reads it, which the test also GETs.
|
||||
COPY_PATHS=(
|
||||
packages/bus/src
|
||||
packages/business/src
|
||||
packages/cli/src
|
||||
packages/conversation/src
|
||||
packages/discord/src
|
||||
packages/harness/src
|
||||
packages/seat/src
|
||||
packages/webui/src
|
||||
packages/webui/README.md
|
||||
packages/tasks/src
|
||||
packages/tasks/fields.json
|
||||
scripts/mosaic-config.mjs
|
||||
roles
|
||||
RELEASE
|
||||
)
|
||||
|
||||
# N2 (row 75 review): copy and install read and point at this checkout, so
|
||||
# they run from the canonical one, never a worktree someone may remove.
|
||||
canonical() {
|
||||
[ -d "$REPO/.git" ] || [ "$ALLOW_WORKTREE" = 1 ] || die "refusing: $REPO is not the canonical checkout (its .git is not a directory); run copy and install from the canonical checkout"
|
||||
}
|
||||
|
||||
# The copies directory goes into the unit as part of the copy's path.
|
||||
copies_ok() {
|
||||
case "$COPIES" in /*) ;; *) die "copies directory must be an absolute path: $COPIES" ;; esac
|
||||
case "$COPIES" in *[!A-Za-z0-9/._+-]*) die "copies directory contains a character the unit cannot carry: $COPIES" ;; esac
|
||||
}
|
||||
|
||||
# The root the unit runs from: the copy named by --copy, else this checkout.
|
||||
unit_root() {
|
||||
if [ -z "$COPY_ID" ]; then echo "$REPO"; return 0; fi
|
||||
case "$COPY_ID" in *[!0-9a-f]*) die "--copy must be a full 40-character commit id: $COPY_ID" ;; esac
|
||||
[ "${#COPY_ID}" -eq 40 ] || die "--copy must be a full 40-character commit id: $COPY_ID"
|
||||
copies_ok
|
||||
[ -f "$COPIES/$COPY_ID/packages/webui/src/cli.mjs" ] || die "no copy $COPY_ID in $COPIES; make it with: scripts/webui-service.sh copy $COPY_ID"
|
||||
echo "$COPIES/$COPY_ID"
|
||||
}
|
||||
|
||||
render() {
|
||||
[ -f "$TEMPLATE" ] || die "template missing: $TEMPLATE"
|
||||
local node
|
||||
@@ -119,18 +212,68 @@ render() {
|
||||
case "$node" in /*) ;; *) die "node path must be absolute: $node" ;; esac
|
||||
case "$node" in *[!A-Za-z0-9/._+-]*) die "node path contains a character the unit cannot carry: $node" ;; esac
|
||||
case "$REPO" in *[!A-Za-z0-9/._+-]*) die "repository path contains a character the unit cannot carry: $REPO" ;; esac
|
||||
local root
|
||||
root="$(unit_root)" || exit 1
|
||||
local node_dir path="/usr/local/bin:/usr/bin:/bin"
|
||||
node_dir="$(dirname "$node")"
|
||||
case ":$path:" in *":$node_dir:"*) ;; *) path="$node_dir:$path" ;; esac
|
||||
local flags
|
||||
flags="$(serve_flags)" || exit 1
|
||||
sed -e "s|@REPO@|$REPO|g" -e "s|@PATH@|$path|g" -e "s|@NODE@|$node|g" -e "s|@FLAGS@|$flags|g" "$TEMPLATE"
|
||||
[ -z "$COPY_ID" ] || flags="--checkout $REPO $flags"
|
||||
sed -e "s|@REPO@|$root|g" -e "s|@PATH@|$path|g" -e "s|@NODE@|$node|g" -e "s|@FLAGS@|$flags|g" "$TEMPLATE"
|
||||
}
|
||||
|
||||
# The unit's active state from list-units, printed when it is anything but
|
||||
# inactive or failed (active, activating, deactivating, reloading...). A
|
||||
# unit systemd has not loaded is not listed and prints nothing. N4: a
|
||||
# failed call refuses with one line naming it; the state is unknown, so
|
||||
# nothing is written or removed.
|
||||
running() {
|
||||
local out
|
||||
out="$(systemctl --user list-units --no-legend --plain "$UNIT_NAME" 2>/dev/null)" || die "refusing: systemctl --user list-units --no-legend --plain $UNIT_NAME failed, so the unit's state is unknown; nothing was changed"
|
||||
printf '%s\n' "$out" | awk -v unit="$UNIT_NAME" '$1 == unit && $3 != "inactive" && $3 != "failed" {print $3}'
|
||||
}
|
||||
|
||||
# The bus host proves a human by checking that the calling process runs its
|
||||
# own packages/bus/src/human-cli.mjs (packages/bus/src/human.mjs), and the
|
||||
# Console spawns the one beside its own files. So a Console on a copy works
|
||||
# only with a bus on that same copy: install refuses unless every bus unit
|
||||
# it would call has an ExecStart path inside the copy. With --business the
|
||||
# unit is [email protected]; without it, every mosaic-bus@ unit systemd
|
||||
# has loaded, and none loaded refuses. Like the state read, a failed call
|
||||
# refuses with one line.
|
||||
bus_on_copy() {
|
||||
local copy="$1" units unit out load paths p
|
||||
if [ -n "$BUSINESS" ]; then
|
||||
units="mosaic-bus@$BUSINESS.service"
|
||||
else
|
||||
out="$(systemctl --user list-units --all --no-legend --plain 'mosaic-bus@*.service' 2>/dev/null)" || die "refusing: systemctl --user list-units --all --no-legend --plain mosaic-bus@*.service failed, so the bus unit is unknown; nothing was changed"
|
||||
units="$(printf '%s\n' "$out" | awk '$1 ~ /^mosaic-bus@.+\.service$/ {print $1}')"
|
||||
[ -n "$units" ] || die "refusing: no mosaic-bus@ unit is loaded, and the Console on the copy $copy needs its bus on that copy; name the business with --business ID"
|
||||
fi
|
||||
for unit in $units; do
|
||||
out="$(systemctl --user show "$unit" -p LoadState -p ExecStart 2>/dev/null)" || die "refusing: systemctl --user show $unit failed, so the bus unit is unknown; nothing was changed"
|
||||
load="$(printf '%s\n' "$out" | sed -n 's/^LoadState=//p')"
|
||||
[ "$load" = loaded ] || die "refusing: no bus unit $unit is installed (LoadState ${load:-unknown}); the Console on the copy $copy needs its bus on that copy"
|
||||
paths="$(printf '%s\n' "$out" | sed -n 's/^ExecStart=//p' | grep -o 'path=[^ ;]*' | sed 's/^path=//' || true)"
|
||||
[ -n "$paths" ] || die "refusing: $unit has no ExecStart path, so where its bus runs from is unknown; nothing was changed"
|
||||
for p in $paths; do
|
||||
case "$p" in "$copy"/*) ;; *) die "refusing: $unit runs from $p, not the copy $copy; the bus accepts only its own human-cli.mjs, so every Console bus call would fail human-required until the bus runs from the same copy" ;; esac
|
||||
done
|
||||
done
|
||||
}
|
||||
|
||||
install_unit() {
|
||||
parse_flags "$@"
|
||||
canonical
|
||||
[ -n "$COPY_ID" ] || die "install needs --copy ID, a copy made with: scripts/webui-service.sh copy COMMIT"
|
||||
local unit
|
||||
unit="$(render)" || exit 1
|
||||
bus_on_copy "$COPIES/$COPY_ID"
|
||||
# N1: a running unit would take the new ExecStart at its next restart.
|
||||
local state
|
||||
state="$(running)" || exit 1
|
||||
[ -z "$state" ] || die "refusing: $UNIT_NAME is running ($state); Jason or Mos stops it first (systemctl --user stop mosaic-webui), then install, then start it"
|
||||
mkdir -p "$UNIT_DIR"
|
||||
local target="$UNIT_DIR/$UNIT_NAME" tmp
|
||||
tmp="$(mktemp "$UNIT_DIR/.$UNIT_NAME.XXXXXX")"
|
||||
@@ -163,13 +306,12 @@ MSG
|
||||
|
||||
uninstall_unit() {
|
||||
parse_flags "$@"
|
||||
[ "$SERVE_SET" = 0 ] || usage
|
||||
[ "$SERVE_SET" = 0 ] && [ "$COPY_FLAGS" = 0 ] && [ "$ALLOW_WORKTREE" = 0 ] || usage
|
||||
local target="$UNIT_DIR/$UNIT_NAME"
|
||||
if [ "$RELOAD" = 1 ]; then
|
||||
local active
|
||||
active="$(systemctl --user list-units --no-legend --plain "$UNIT_NAME" 2>/dev/null | awk '$3 == "active" || $3 == "activating" || $3 == "deactivating" {print $1}')"
|
||||
[ -z "$active" ] || die "refusing: $UNIT_NAME is still running; the human stops it first (systemctl --user stop mosaic-webui)"
|
||||
fi
|
||||
# N3: checked with --no-reload too, which skips only daemon-reload.
|
||||
local state
|
||||
state="$(running)" || exit 1
|
||||
[ -z "$state" ] || die "refusing: $UNIT_NAME is still running ($state); Jason or Mos stops it first (systemctl --user stop mosaic-webui)"
|
||||
if [ -f "$target" ]; then
|
||||
rm -f "$target"
|
||||
echo "removed: $target"
|
||||
@@ -184,7 +326,7 @@ uninstall_unit() {
|
||||
|
||||
status_unit() {
|
||||
parse_flags "$@"
|
||||
[ "$SERVE_SET" = 0 ] && [ "$RELOAD" = 1 ] || usage
|
||||
[ "$SERVE_SET" = 0 ] && [ "$RELOAD" = 1 ] && [ "$COPY_FLAGS" = 0 ] && [ "$ALLOW_WORKTREE" = 0 ] || usage
|
||||
local target="$UNIT_DIR/$UNIT_NAME"
|
||||
echo "unit: $UNIT_NAME"
|
||||
echo " enabled: $(systemctl --user is-enabled "$UNIT_NAME" 2>&1 || true)"
|
||||
@@ -194,8 +336,26 @@ status_unit() {
|
||||
echo "healthz: not checked; no unit at $target"
|
||||
return 0
|
||||
fi
|
||||
# render puts --send right after the allowed user, which may itself be
|
||||
# the value --send; a unit that sends ends with both.
|
||||
# The copy is the last part of WorkingDirectory; status only reads.
|
||||
local wd copy head
|
||||
wd="$(sed -n 's/^WorkingDirectory=//p' "$target")"
|
||||
copy="${wd##*/}"
|
||||
if [[ "$copy" =~ ^[0-9a-f]{40}$ ]]; then
|
||||
echo "copy: $copy"
|
||||
if head="$(git -C "$REPO" rev-parse --verify --quiet refs/heads/refactor 2>/dev/null)"; then
|
||||
if [ "$head" = "$copy" ]; then
|
||||
echo "refactor: $head, the copy's commit"
|
||||
else
|
||||
echo "refactor: $head, another commit; the copy is $copy"
|
||||
fi
|
||||
else
|
||||
echo "refactor: unknown; git rev-parse refs/heads/refactor failed in $REPO"
|
||||
fi
|
||||
else
|
||||
echo "copy: none; the unit runs from $wd"
|
||||
fi
|
||||
# render puts --send right after the allowed user. The allowed user
|
||||
# starts with a letter or digit (N5), so it is never the value --send.
|
||||
if grep -q '^ExecStart=.* --allow-user [^ ][^ ]* --send$' "$target"; then
|
||||
echo "send: on (--send; human messages to roles)"
|
||||
else
|
||||
@@ -212,12 +372,51 @@ fetch(process.argv[1], { signal: AbortSignal.timeout(5000) })
|
||||
' "$url")"
|
||||
}
|
||||
|
||||
# Writes the copy into a temp directory beside the target, takes write
|
||||
# permission off every file, then renames it into place, so a copy at its
|
||||
# final path is always whole and read-only. An existing copy is left alone.
|
||||
copy_commit() {
|
||||
[ $# -ge 1 ] || usage
|
||||
local commit="$1"; shift
|
||||
case "$commit" in -*) usage ;; esac
|
||||
parse_flags "$@"
|
||||
[ "$SERVE_SET" = 0 ] && [ "$DIR_FLAGS" = 0 ] && [ -z "$COPY_ID" ] || usage
|
||||
canonical
|
||||
copies_ok
|
||||
case "$commit" in [A-Za-z0-9]*) ;; *) die "commit must be a commit id or branch name: $commit" ;; esac
|
||||
case "$commit" in *[!A-Za-z0-9._/-]*) die "commit must be a commit id or branch name: $commit" ;; esac
|
||||
git -C "$REPO" rev-parse --verify --quiet refs/heads/refactor >/dev/null || die "no refs/heads/refactor in $REPO"
|
||||
local id
|
||||
id="$(git -C "$REPO" rev-parse --verify --quiet "$commit^{commit}")" || die "not a commit: $commit"
|
||||
git -C "$REPO" merge-base --is-ancestor "$id" refs/heads/refactor || die "refusing: $id is not reachable from refs/heads/refactor"
|
||||
local target="$COPIES/$id"
|
||||
if [ -e "$target" ]; then echo "exists: $target"; return 0; fi
|
||||
mkdir -p "$COPIES"
|
||||
COPY_TMP="$(mktemp -d "$COPIES/.$id.XXXXXX")"
|
||||
trap 'chmod -R u+w "$COPY_TMP" 2>/dev/null; rm -rf "$COPY_TMP"' EXIT
|
||||
git -C "$REPO" archive --format=tar "$id" -- "${COPY_PATHS[@]}" | tar -x -f - -C "$COPY_TMP" || die "git archive of $id failed; no copy written"
|
||||
# Read-only files and directories: a writer that replaces by rename
|
||||
# (git, editors, sed -i) can't change a copy either.
|
||||
find "$COPY_TMP" -type f -exec chmod a-w {} +
|
||||
find "$COPY_TMP" -type d -exec chmod 0555 {} +
|
||||
if ! mv -T "$COPY_TMP" "$target" 2>/dev/null; then
|
||||
[ -e "$target" ] || die "could not rename the copy into $target"
|
||||
echo "exists: $target"
|
||||
return 0
|
||||
fi
|
||||
trap - EXIT
|
||||
echo "copied: $target"
|
||||
echo "install it with: scripts/webui-service.sh install --copy $id, with the unit's flags"
|
||||
}
|
||||
|
||||
[ $# -ge 1 ] || usage
|
||||
cmd="$1"; shift
|
||||
case "$cmd" in
|
||||
copy) copy_commit "$@" ;;
|
||||
render)
|
||||
parse_flags "$@"
|
||||
[ "$DIR_FLAGS" = 0 ] || usage
|
||||
[ "$DIR_FLAGS" = 0 ] && [ "$ALLOW_WORKTREE" = 0 ] || usage
|
||||
{ [ -n "$COPY_ID" ] || [ "$COPY_FLAGS" = 0 ]; } || usage
|
||||
render ;;
|
||||
install) install_unit "$@" ;;
|
||||
uninstall) uninstall_unit "$@" ;;
|
||||
|
||||
Reference in New Issue
Block a user