liquidum 1.2.1 → 1.3.1
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.
- checksums.yaml +4 -4
- data/.devcontainer/devcontainer-lock.json +34 -0
- data/.devcontainer/devcontainer.json +46 -0
- data/.devcontainer/docker-compose.yml +102 -0
- data/.devcontainer/env.conf +31 -0
- data/.devcontainer/postStart.sh +16 -0
- data/.gitignore +2 -0
- data/.shipkit.yml +2 -0
- data/.worktreeinclude +2 -0
- data/CHANGELOG.md +11 -0
- data/Gemfile +5 -1
- data/README.md +57 -0
- data/app/controllers/liquidum/application_controller.rb +1 -1
- data/app/controllers/liquidum/variables_controller.rb +17 -0
- data/bin/de +1112 -0
- data/config/routes.rb +1 -1
- data/lib/liquidum/configuration.rb +3 -0
- data/lib/liquidum/drop.rb +84 -7
- data/lib/liquidum/liquid/liquid_helpers.rb +4 -1
- data/lib/liquidum/liquid/tags/render_tag.rb +1 -1
- data/lib/liquidum/variable.rb +12 -0
- data/lib/liquidum/variables.rb +61 -0
- data/lib/liquidum/version.rb +1 -1
- data/lib/liquidum.rb +10 -3
- data/liquidum.gemspec +1 -1
- data/mise.toml +4 -0
- metadata +18 -9
- data/app/controllers/liquidum/completions_controller.rb +0 -15
data/bin/de
ADDED
|
@@ -0,0 +1,1112 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# de: development environments for this repository. Run without arguments
|
|
3
|
+
# for help. Repository specific settings live in .devcontainer/env.conf, this
|
|
4
|
+
# script is generic: it comes from the de repository, whose `de init` copies
|
|
5
|
+
# it into a repository as bin/de, the same everywhere. Run from the de
|
|
6
|
+
# repository itself (e.g. linked onto the PATH as `de`), it sets repositories
|
|
7
|
+
# up and hands every other command to the bin/de of the current checkout.
|
|
8
|
+
#
|
|
9
|
+
# Every checkout (main checkout or git worktree) gets its own environment: a
|
|
10
|
+
# Docker Compose project named after the branch, labelled with
|
|
11
|
+
#
|
|
12
|
+
# dev.env.name the environment (and compose project) name
|
|
13
|
+
# dev.env.kind persistent | ephemeral
|
|
14
|
+
# dev.env.branch the branch, empty for a detached HEAD
|
|
15
|
+
# dev.env.path the checkout on the host
|
|
16
|
+
# dev.env.gitdir the repository's common .git directory on the host
|
|
17
|
+
#
|
|
18
|
+
# Cleanup selects resources by these labels, never by name.
|
|
19
|
+
set -euo pipefail
|
|
20
|
+
|
|
21
|
+
# Through symlinks, e.g. ~/.local/bin/de to the de repository's bin/de
|
|
22
|
+
DE_ROOT="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/.." && pwd -P)"
|
|
23
|
+
# Set when running from the de repository rather than a repository's copy
|
|
24
|
+
DE_TEMPLATE=""
|
|
25
|
+
DE_DOCS="https://github.com/tdegrunt/de#readme"
|
|
26
|
+
# Your own tools for every environment, see env_write_host_feature
|
|
27
|
+
DE_CONFIG="${DE_CONFIG:-${XDG_CONFIG_HOME:-$HOME/.config}/de/config.json}"
|
|
28
|
+
# Where the folders "home" in DE_CONFIG lists are kept, shared by every environment
|
|
29
|
+
DE_HOME_STATE="${XDG_STATE_HOME:-$HOME/.local/state}/de/home"
|
|
30
|
+
# `de exec` passes a SELENIUM_URL from the caller on, see env.conf. Unset it
|
|
31
|
+
# so env.conf's value isn't exported to the commands this script runs.
|
|
32
|
+
ENV_SELENIUM_URL_GIVEN="${SELENIUM_URL:-}"
|
|
33
|
+
unset SELENIUM_URL
|
|
34
|
+
UP_COMMAND=()
|
|
35
|
+
PRIMARY_BRANCHES=(main master)
|
|
36
|
+
if [ -f "$DE_ROOT/.devcontainer/env.conf" ]; then
|
|
37
|
+
# shellcheck source=../.devcontainer/env.conf
|
|
38
|
+
source "$DE_ROOT/.devcontainer/env.conf"
|
|
39
|
+
elif [ -d "$DE_ROOT/template" ]; then
|
|
40
|
+
DE_TEMPLATE="$DE_ROOT/template"
|
|
41
|
+
else
|
|
42
|
+
printf 'error: %s has neither .devcontainer/env.conf nor template/\n' "$DE_ROOT" >&2
|
|
43
|
+
exit 1
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
# The commands of a repository's bin/de, also listed by the de repository's help
|
|
47
|
+
usage_commands() {
|
|
48
|
+
cat <<'EOF'
|
|
49
|
+
new <branch> [folder] [--from <ref>] [--copy-data[=<services>]]
|
|
50
|
+
[--copy-data-from <checkout>] [--yes] [--no-trust]
|
|
51
|
+
Create a worktree for a branch (next to the main checkout
|
|
52
|
+
as <repository>-<branch> unless a folder is given) and
|
|
53
|
+
start its environment. New branches start from the
|
|
54
|
+
default branch, or from --from. --no-trust as for up.
|
|
55
|
+
--copy-data starts it with a copy of the volumes of the
|
|
56
|
+
environment it branches off (an existing branch: this
|
|
57
|
+
checkout's, or --copy-data-from's), of every service but
|
|
58
|
+
app or the listed ones, e.g. --copy-data=postgres,redis.
|
|
59
|
+
Those services are stopped during the copy, after
|
|
60
|
+
confirming, or right away with --yes.
|
|
61
|
+
up [--rebuild] [--no-trust]
|
|
62
|
+
Create or start this checkout's environment and prepare
|
|
63
|
+
its databases. --rebuild recreates the containers after
|
|
64
|
+
changing .devcontainer/, keeping the data. Trusts the
|
|
65
|
+
checkout's mise config on the host, unless --no-trust.
|
|
66
|
+
exec <command...> Run a command in this checkout's devcontainer, in the same
|
|
67
|
+
directory relative to the checkout.
|
|
68
|
+
status Show the environment's containers, health and URLs.
|
|
69
|
+
code [<checkout>] Open a checkout in VS Code inside its devcontainer,
|
|
70
|
+
starting it first if needed. <checkout> is a branch,
|
|
71
|
+
environment name, worktree folder name or a path in
|
|
72
|
+
it, default this one.
|
|
73
|
+
down [--destroy-persistent]
|
|
74
|
+
Remove an ephemeral environment with its data. A
|
|
75
|
+
persistent one is only stopped, unless
|
|
76
|
+
--destroy-persistent is given and its name confirmed.
|
|
77
|
+
prune [--yes] Remove ephemeral environments whose worktree or branch
|
|
78
|
+
is gone. --yes skips the confirmation.
|
|
79
|
+
EOF
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
usage() {
|
|
83
|
+
cat <<EOF
|
|
84
|
+
Usage: bin/de <command> [arguments]
|
|
85
|
+
|
|
86
|
+
Every checkout of this repository, the main one and each git worktree, gets
|
|
87
|
+
its own development environment: a devcontainer with its own services,
|
|
88
|
+
volumes and network. Environments for ${PRIMARY_BRANCHES[*]} ${PERSISTENT_BRANCHES[*]} are
|
|
89
|
+
persistent and keep their data, all others are ephemeral. A primary branch
|
|
90
|
+
(${PRIMARY_BRANCHES[*]}) gets one named $ENV_REPO, the others $ENV_REPO-<branch>.
|
|
91
|
+
|
|
92
|
+
Commands:
|
|
93
|
+
$(usage_commands)
|
|
94
|
+
help Show this help.
|
|
95
|
+
|
|
96
|
+
Examples:
|
|
97
|
+
bin/de new fix-login
|
|
98
|
+
bin/de exec bin/rails test
|
|
99
|
+
SELENIUM_URL=http://host.docker.internal:4444/ HEADLESS=0 bin/de exec bin/rails test:system
|
|
100
|
+
bin/de code main
|
|
101
|
+
|
|
102
|
+
Settings: .devcontainer/env.conf, your own tools: $DE_CONFIG,
|
|
103
|
+
docs: $DE_DOCS. To update bin/de, run \`de update\` from the de repository.
|
|
104
|
+
EOF
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
# Help when running from the de repository
|
|
108
|
+
usage_source() {
|
|
109
|
+
cat <<EOF
|
|
110
|
+
Usage: de <command> [arguments]
|
|
111
|
+
|
|
112
|
+
Development environments, one devcontainer per checkout. This is the de
|
|
113
|
+
repository's copy ($DE_ROOT): it sets repositories up, and hands every other
|
|
114
|
+
command to the bin/de of the checkout you're in.
|
|
115
|
+
|
|
116
|
+
Commands:
|
|
117
|
+
init [<repository>] [--name <name>] [--force]
|
|
118
|
+
Set up development environments in a repository (default
|
|
119
|
+
the current one). Repository specific files that already
|
|
120
|
+
exist there are kept, unless --force is given. --name
|
|
121
|
+
sets ENV_REPO, the prefix of its environment names,
|
|
122
|
+
default the repository's folder name: needed when two
|
|
123
|
+
repositories have folders of the same name.
|
|
124
|
+
update [<repository>] [--name <name>] [--force]
|
|
125
|
+
Update a repository's bin/de, and add what's new in the
|
|
126
|
+
setup, like init but only where it's set up already.
|
|
127
|
+
upgrade Update this copy of de with a git pull. Then run de
|
|
128
|
+
update in a repository to update its bin/de.
|
|
129
|
+
help Show this help.
|
|
130
|
+
|
|
131
|
+
Run by the current checkout's bin/de (see bin/de help there):
|
|
132
|
+
$(usage_commands)
|
|
133
|
+
|
|
134
|
+
Your own tools for every environment: $DE_CONFIG
|
|
135
|
+
EOF
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
# --- Output -------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
log() { printf '\033[1;34m==>\033[0m %s\n' "$*" >&2; }
|
|
141
|
+
warn() { printf '\033[1;33mwarning:\033[0m %s\n' "$*" >&2; }
|
|
142
|
+
die() { printf '\033[1;31merror:\033[0m %s\n' "$*" >&2; exit 1; }
|
|
143
|
+
|
|
144
|
+
# Prints a command, then runs it
|
|
145
|
+
run() {
|
|
146
|
+
printf '\033[2m+' >&2
|
|
147
|
+
printf ' %q' "$@" >&2
|
|
148
|
+
printf '\033[0m\n' >&2
|
|
149
|
+
"$@"
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
devcontainer_cli() {
|
|
153
|
+
if command -v devcontainer >/dev/null 2>&1; then
|
|
154
|
+
run devcontainer "$@"
|
|
155
|
+
else
|
|
156
|
+
run npx --yes "@devcontainers/cli@$DEVCONTAINER_CLI_VERSION" "$@"
|
|
157
|
+
fi
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
# --- Environment identity -----------------------------------------------
|
|
161
|
+
|
|
162
|
+
# Lowercases and replaces everything but [a-z0-9-] with dashes
|
|
163
|
+
env_sanitize() {
|
|
164
|
+
printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9-' '-' | tr -s '-' | sed 's/^-//; s/-$//'
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
env_is_primary_branch() {
|
|
168
|
+
local branch
|
|
169
|
+
for branch in "${PRIMARY_BRANCHES[@]}"; do
|
|
170
|
+
[ "$1" = "$branch" ] && return 0
|
|
171
|
+
done
|
|
172
|
+
return 1
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
# Primary branches are always persistent
|
|
176
|
+
env_is_persistent_branch() {
|
|
177
|
+
local branch
|
|
178
|
+
env_is_primary_branch "$1" && return 0
|
|
179
|
+
for branch in "${PERSISTENT_BRANCHES[@]}"; do
|
|
180
|
+
[ "$1" = "$branch" ] && return 0
|
|
181
|
+
done
|
|
182
|
+
return 1
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
# Prints the environment name for a checkout directory
|
|
186
|
+
env_name_for() {
|
|
187
|
+
local dir="$1" branch name
|
|
188
|
+
branch="$(git -C "$dir" symbolic-ref --short -q HEAD || true)"
|
|
189
|
+
if [ -n "$branch" ]; then
|
|
190
|
+
env_name_for_branch "$branch"
|
|
191
|
+
return
|
|
192
|
+
fi
|
|
193
|
+
env_name_shorten "$(env_sanitize "$ENV_REPO-detached-$(basename "$dir")")"
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
# Prints the environment name for a checkout of a branch
|
|
197
|
+
env_name_for_branch() {
|
|
198
|
+
if env_is_primary_branch "$1"; then
|
|
199
|
+
env_name_shorten "$(env_sanitize "$ENV_REPO")"
|
|
200
|
+
else
|
|
201
|
+
env_name_shorten "$(env_sanitize "$ENV_REPO-$1")"
|
|
202
|
+
fi
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
# Compose project names and OrbStack domain labels max out at 63 characters
|
|
206
|
+
env_name_shorten() {
|
|
207
|
+
local name="$1"
|
|
208
|
+
if [ "${#name}" -gt 63 ]; then
|
|
209
|
+
name="${name:0:54}-$(printf '%s' "$name" | shasum | cut -c1-8)"
|
|
210
|
+
fi
|
|
211
|
+
printf '%s' "$name"
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
# Sets the ENV_* variables describing the checkout that contains $1
|
|
215
|
+
env_init() {
|
|
216
|
+
local dir="${1:-$PWD}" common
|
|
217
|
+
|
|
218
|
+
ENV_PATH="$(cd "$(git -C "$dir" rev-parse --show-toplevel)" && pwd -P)" ||
|
|
219
|
+
die "$dir is not inside a git checkout"
|
|
220
|
+
common="$(git -C "$ENV_PATH" rev-parse --git-common-dir)"
|
|
221
|
+
ENV_GIT_COMMON_DIR="$(cd "$ENV_PATH" && cd "$common" && pwd -P)"
|
|
222
|
+
ENV_MAIN_CHECKOUT="$(dirname "$ENV_GIT_COMMON_DIR")"
|
|
223
|
+
ENV_BRANCH="$(git -C "$ENV_PATH" symbolic-ref --short -q HEAD || true)"
|
|
224
|
+
ENV_NAME="$(env_name_for "$ENV_PATH")"
|
|
225
|
+
|
|
226
|
+
if [ -n "$ENV_BRANCH" ] && env_is_persistent_branch "$ENV_BRANCH"; then
|
|
227
|
+
ENV_KIND=persistent
|
|
228
|
+
ENV_RESTART=unless-stopped
|
|
229
|
+
else
|
|
230
|
+
ENV_KIND=ephemeral
|
|
231
|
+
ENV_RESTART=no
|
|
232
|
+
fi
|
|
233
|
+
|
|
234
|
+
# Inside the container, both the checkout and the common .git directory are
|
|
235
|
+
# mounted at /workspaces/<host path>, so the relative paths in a worktree's
|
|
236
|
+
# .git file (worktree.useRelativePaths) resolve the same way.
|
|
237
|
+
# devcontainer.json uses the same path as its workspaceFolder.
|
|
238
|
+
ENV_PATH_IN_CONTAINER="/workspaces$ENV_PATH"
|
|
239
|
+
ENV_GIT_COMMON_DIR_IN_CONTAINER="/workspaces$ENV_GIT_COMMON_DIR"
|
|
240
|
+
# A shorter, stable path to the checkout, for interactive use
|
|
241
|
+
ENV_WORKSPACE_LINK="/workspaces/$ENV_REPO"
|
|
242
|
+
|
|
243
|
+
ENV_DOTENV="$ENV_PATH/.devcontainer/.env"
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
# Prints where a browser driven through Selenium at $1 reaches the app's test
|
|
247
|
+
# server: by service name from the environment's own selenium container, and
|
|
248
|
+
# by OrbStack domain from a browser on the Mac
|
|
249
|
+
env_capybara_app_host() {
|
|
250
|
+
case "$1" in
|
|
251
|
+
*//selenium:*) echo "http://app:3100" ;;
|
|
252
|
+
*) echo "http://app.$ENV_NAME.orb.local:3100" ;;
|
|
253
|
+
esac
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
# Writes .devcontainer/.env, which docker compose reads for the project name,
|
|
257
|
+
# labels and mounts
|
|
258
|
+
env_write_dotenv() {
|
|
259
|
+
cat > "$ENV_DOTENV" <<EOF
|
|
260
|
+
# Written by bin/de, don't edit
|
|
261
|
+
COMPOSE_PROJECT_NAME=$ENV_NAME
|
|
262
|
+
ENV_NAME=$ENV_NAME
|
|
263
|
+
ENV_KIND=$ENV_KIND
|
|
264
|
+
ENV_BRANCH=$ENV_BRANCH
|
|
265
|
+
ENV_PATH=$ENV_PATH
|
|
266
|
+
ENV_GIT_COMMON_DIR=$ENV_GIT_COMMON_DIR
|
|
267
|
+
ENV_PATH_IN_CONTAINER=$ENV_PATH_IN_CONTAINER
|
|
268
|
+
ENV_GIT_COMMON_DIR_IN_CONTAINER=$ENV_GIT_COMMON_DIR_IN_CONTAINER
|
|
269
|
+
ENV_WORKSPACE_LINK=$ENV_WORKSPACE_LINK
|
|
270
|
+
ENV_RESTART=$ENV_RESTART
|
|
271
|
+
ENV_GIT_CONFIG_DIR=$HOME/.config/git
|
|
272
|
+
SELENIUM_URL=$SELENIUM_URL
|
|
273
|
+
CAPYBARA_APP_HOST=$(env_capybara_app_host "$SELENIUM_URL")
|
|
274
|
+
EOF
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
# --- Docker resources ---------------------------------------------------
|
|
278
|
+
|
|
279
|
+
# Runs docker compose for this checkout's project
|
|
280
|
+
env_compose() {
|
|
281
|
+
run docker compose --project-directory "$ENV_PATH/.devcontainer" \
|
|
282
|
+
-f "$ENV_PATH/.devcontainer/docker-compose.yml" \
|
|
283
|
+
--env-file "$ENV_DOTENV" -p "$ENV_NAME" "$@"
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
# Succeeds when the app container of an environment is running
|
|
287
|
+
env_running() {
|
|
288
|
+
[ -n "$(docker container ls -q --filter "label=dev.env.name=$1" --filter label=com.docker.compose.service=app)" ]
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
# Prints the value of a label on any container, volume or network of an
|
|
292
|
+
# environment
|
|
293
|
+
env_label() {
|
|
294
|
+
local name="$1" label="$2" kind values
|
|
295
|
+
for kind in container volume network; do
|
|
296
|
+
values="$(docker "$kind" ls $([ "$kind" = container ] && echo -a) --filter "label=dev.env.name=$name" --format "{{.Label \"$label\"}}")"
|
|
297
|
+
if [ -n "$values" ]; then
|
|
298
|
+
head -n1 <<<"$values"
|
|
299
|
+
return
|
|
300
|
+
fi
|
|
301
|
+
done
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
# Prints the names of this repository's environments of a kind, one per line
|
|
305
|
+
env_list() {
|
|
306
|
+
local kind_filter="label=dev.env.kind=$1" repo_filter="label=dev.env.gitdir=$ENV_GIT_COMMON_DIR"
|
|
307
|
+
{
|
|
308
|
+
docker container ls -a --filter "$kind_filter" --filter "$repo_filter" --format '{{.Label "dev.env.name"}}'
|
|
309
|
+
docker volume ls --filter "$kind_filter" --filter "$repo_filter" --format '{{.Label "dev.env.name"}}'
|
|
310
|
+
docker network ls --filter "$kind_filter" --filter "$repo_filter" --format '{{.Label "dev.env.name"}}'
|
|
311
|
+
} | sort -u | sed '/^$/d'
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
# Succeeds when any resource of the environment is labelled persistent
|
|
315
|
+
env_is_persistent() {
|
|
316
|
+
[ -n "$(docker container ls -aq --filter "label=dev.env.name=$1" --filter label=dev.env.kind=persistent)$(docker volume ls -q --filter "label=dev.env.name=$1" --filter label=dev.env.kind=persistent)$(docker network ls -q --filter "label=dev.env.name=$1" --filter label=dev.env.kind=persistent)" ]
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
# Removes every container, volume and network of an environment. Resources are
|
|
320
|
+
# found by the dev.env.name label, plus the compose project label for the ones
|
|
321
|
+
# devcontainer features add without our labels (the docker-in-docker volume).
|
|
322
|
+
env_destroy() {
|
|
323
|
+
local name="$1" ids filter
|
|
324
|
+
for filter in "label=dev.env.name=$name" "label=com.docker.compose.project=$name"; do
|
|
325
|
+
ids="$(docker container ls -aq --filter "$filter")"
|
|
326
|
+
[ -z "$ids" ] || run docker container rm --force --volumes $ids >/dev/null
|
|
327
|
+
done
|
|
328
|
+
for filter in "label=dev.env.name=$name" "label=com.docker.compose.project=$name"; do
|
|
329
|
+
ids="$(docker volume ls -q --filter "$filter")"
|
|
330
|
+
[ -z "$ids" ] || run docker volume rm --force $ids >/dev/null
|
|
331
|
+
ids="$(docker network ls -q --filter "$filter")"
|
|
332
|
+
[ -z "$ids" ] || run docker network rm $ids >/dev/null
|
|
333
|
+
done
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
# Prints "<service> <health>" for every running container of an environment,
|
|
337
|
+
# where health is the healthcheck status, or "running" without a healthcheck
|
|
338
|
+
env_health() {
|
|
339
|
+
local ids
|
|
340
|
+
ids="$(docker container ls -q --filter "label=dev.env.name=$1")"
|
|
341
|
+
[ -z "$ids" ] && return
|
|
342
|
+
# shellcheck disable=SC2086
|
|
343
|
+
docker container inspect $ids --format '{{ index .Config.Labels "com.docker.compose.service" }} {{ if .State.Health }}{{ .State.Health.Status }}{{ else }}{{ .State.Status }}{{ end }}'
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
# Prints "<path>\t<branch>" for every checkout of the repository
|
|
347
|
+
checkouts() {
|
|
348
|
+
git -C "$ENV_PATH" worktree list --porcelain | awk '
|
|
349
|
+
/^worktree / { if (path != "") print path "\t" branch; path = substr($0, 10); branch = "" }
|
|
350
|
+
/^branch / { branch = substr($0, 8); sub("^refs/heads/", "", branch) }
|
|
351
|
+
END { if (path != "") print path "\t" branch }'
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
# Prints the checkout a branch, environment name, worktree folder name or a
|
|
355
|
+
# path in it names, nothing when none does
|
|
356
|
+
checkout_for() {
|
|
357
|
+
local path branch
|
|
358
|
+
while IFS=$'\t' read -r path branch; do
|
|
359
|
+
[ -d "$path" ] || continue
|
|
360
|
+
if [ "$1" = "$branch" ] || [ "$1" = "$(basename "$path")" ] || [ "$1" = "$(env_name_for "$path")" ]; then
|
|
361
|
+
printf '%s' "$path"
|
|
362
|
+
return
|
|
363
|
+
fi
|
|
364
|
+
done < <(checkouts)
|
|
365
|
+
# Or any folder in a checkout, e.g. the absolute path Orca's "Open in" adds
|
|
366
|
+
if [ -d "$1" ]; then
|
|
367
|
+
(cd "$(git -C "$1" rev-parse --show-toplevel)" && pwd -P) || die "$1 is not inside a git checkout"
|
|
368
|
+
fi
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
# Lists the checkouts with their environment names, on stderr
|
|
372
|
+
checkouts_print() {
|
|
373
|
+
local path branch
|
|
374
|
+
while IFS=$'\t' read -r path branch; do
|
|
375
|
+
[ -d "$path" ] && printf ' %-40s %-30s %s\n' "$(env_name_for "$path")" "${branch:-(detached)}" "$path" >&2
|
|
376
|
+
done < <(checkouts)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
# --- Copying data -------------------------------------------------------
|
|
380
|
+
|
|
381
|
+
# Succeeds when service $1 is one of the comma separated services in $2, or
|
|
382
|
+
# with "all", when it's any service but the devcontainer itself (app, whose
|
|
383
|
+
# docker-in-docker volume is a cache)
|
|
384
|
+
copy_data_wants() {
|
|
385
|
+
if [ "$2" = all ]; then
|
|
386
|
+
[ "$1" != app ]
|
|
387
|
+
else
|
|
388
|
+
[[ ",$2," == *",$1,"* ]]
|
|
389
|
+
fi
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
# Finds the named volumes of environment $1 to copy, those of the services $2
|
|
393
|
+
# wants (see copy_data_wants). Sets COPY_VOLUMES ("<volume> <compose volume
|
|
394
|
+
# key> <image>"), and COPY_RUNNING and COPY_RUNNING_SERVICES: the running
|
|
395
|
+
# containers mounting them and their services.
|
|
396
|
+
copy_data_find() {
|
|
397
|
+
local src="$1" wanted="$2" id service image volume key available="" chosen="" found
|
|
398
|
+
COPY_VOLUMES=() COPY_RUNNING="" COPY_RUNNING_SERVICES=""
|
|
399
|
+
for id in $(docker container ls -aq --filter "label=com.docker.compose.project=$src"); do
|
|
400
|
+
service="$(docker container inspect "$id" --format '{{ index .Config.Labels "com.docker.compose.service" }}')"
|
|
401
|
+
image="$(docker container inspect "$id" --format '{{ .Config.Image }}')"
|
|
402
|
+
found=false
|
|
403
|
+
# Named volumes of this environment only, not anonymous or shared ones
|
|
404
|
+
for volume in $(docker container inspect "$id" --format '{{ range .Mounts }}{{ if eq .Type "volume" }}{{ println .Name }}{{ end }}{{ end }}'); do
|
|
405
|
+
[ "$(docker volume inspect "$volume" --format '{{ index .Labels "com.docker.compose.project" }}')" = "$src" ] || continue
|
|
406
|
+
key="$(docker volume inspect "$volume" --format '{{ index .Labels "com.docker.compose.volume" }}')"
|
|
407
|
+
[ -n "$key" ] || continue
|
|
408
|
+
found=true
|
|
409
|
+
copy_data_wants "$service" "$wanted" || continue
|
|
410
|
+
[ "$volume" = "${src}_$key" ] || die "volume $volume of $service has a fixed name, it can't be copied"
|
|
411
|
+
COPY_VOLUMES+=("$volume $key $image")
|
|
412
|
+
done
|
|
413
|
+
$found || continue
|
|
414
|
+
available="$available $service"
|
|
415
|
+
copy_data_wants "$service" "$wanted" || continue
|
|
416
|
+
chosen="$chosen $service "
|
|
417
|
+
if [ "$(docker container inspect "$id" --format '{{ .State.Running }}')" = true ]; then
|
|
418
|
+
COPY_RUNNING="$COPY_RUNNING $id"
|
|
419
|
+
COPY_RUNNING_SERVICES="$COPY_RUNNING_SERVICES $service"
|
|
420
|
+
fi
|
|
421
|
+
done
|
|
422
|
+
|
|
423
|
+
if [ "$wanted" != all ]; then
|
|
424
|
+
for service in ${wanted//,/ }; do
|
|
425
|
+
[[ "$chosen" == *" $service "* ]] ||
|
|
426
|
+
die "$src has no service $service with a named volume, these have one:${available:- none}"
|
|
427
|
+
done
|
|
428
|
+
fi
|
|
429
|
+
[ "${#COPY_VOLUMES[@]}" -gt 0 ] || die "$src has no data to copy, its services with a named volume:${available:- none}"
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
# Copies the COPY_VOLUMES of environment $1 into new volumes for the
|
|
433
|
+
# environment of branch $2 in checkout $3, labelled as compose creates them,
|
|
434
|
+
# so bin/de up adopts them and bin/de down removes them. The services using
|
|
435
|
+
# them are stopped meanwhile, so e.g. postgres's files are consistent, and
|
|
436
|
+
# started again afterwards, also when the copy fails.
|
|
437
|
+
copy_data() {
|
|
438
|
+
local src="$1" branch="$2" path="$3" target kind=ephemeral entry volume key image
|
|
439
|
+
target="$(env_name_for_branch "$branch")"
|
|
440
|
+
env_is_persistent_branch "$branch" && kind=persistent
|
|
441
|
+
|
|
442
|
+
if [ -n "$COPY_RUNNING" ]; then
|
|
443
|
+
log "Stopping$COPY_RUNNING_SERVICES of $src for the copy"
|
|
444
|
+
# shellcheck disable=SC2086
|
|
445
|
+
run docker container stop $COPY_RUNNING >/dev/null
|
|
446
|
+
# shellcheck disable=SC2064
|
|
447
|
+
trap "log 'Starting$COPY_RUNNING_SERVICES of $src again'; run docker container start $COPY_RUNNING >/dev/null" EXIT
|
|
448
|
+
fi
|
|
449
|
+
|
|
450
|
+
for entry in "${COPY_VOLUMES[@]}"; do
|
|
451
|
+
read -r volume key image <<<"$entry"
|
|
452
|
+
log "Copying $volume to ${target}_$key"
|
|
453
|
+
run docker volume create \
|
|
454
|
+
--label "dev.env.name=$target" --label "dev.env.kind=$kind" --label "dev.env.branch=$branch" \
|
|
455
|
+
--label "dev.env.path=$path" --label "dev.env.gitdir=$ENV_GIT_COMMON_DIR" \
|
|
456
|
+
--label "com.docker.compose.project=$target" --label "com.docker.compose.volume=$key" \
|
|
457
|
+
"${target}_$key" >/dev/null
|
|
458
|
+
run docker run --rm --user 0:0 --entrypoint "" \
|
|
459
|
+
-v "$volume:/from:ro" -v "${target}_$key:/to" "$image" cp -a /from/. /to/
|
|
460
|
+
grep -qE "^ $key:" "$path/.devcontainer/docker-compose.yml" ||
|
|
461
|
+
warn "$branch's docker-compose.yml has no volume $key, the copy isn't used"
|
|
462
|
+
done
|
|
463
|
+
|
|
464
|
+
if [ -n "$COPY_RUNNING" ]; then
|
|
465
|
+
trap - EXIT
|
|
466
|
+
log "Starting$COPY_RUNNING_SERVICES of $src again"
|
|
467
|
+
# shellcheck disable=SC2086
|
|
468
|
+
run docker container start $COPY_RUNNING >/dev/null
|
|
469
|
+
fi
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
# --- Commands -----------------------------------------------------------
|
|
473
|
+
|
|
474
|
+
# Writes the local feature devcontainer.json includes as ./de-host, from
|
|
475
|
+
# $DE_CONFIG on the host: your own editor extensions, npm packages and
|
|
476
|
+
# features for every environment of every repository, e.g.
|
|
477
|
+
#
|
|
478
|
+
# {
|
|
479
|
+
# "extensions": ["anthropic.claude-code", "openai.chatgpt"],
|
|
480
|
+
# "npm": ["@anthropic-ai/claude-code", "@openai/codex"],
|
|
481
|
+
# "features": { "ghcr.io/devcontainers/features/python:1": {} },
|
|
482
|
+
# "home": [".claude", ".codex"],
|
|
483
|
+
# "env": { "CLAUDE_CONFIG_DIR": "/home/vscode/.claude" }
|
|
484
|
+
# }
|
|
485
|
+
#
|
|
486
|
+
# "home" lists folders in the container's home directory that every
|
|
487
|
+
# environment shares, kept on the host in $DE_HOME_STATE, e.g. logins. "env"
|
|
488
|
+
# sets environment variables in the container.
|
|
489
|
+
#
|
|
490
|
+
# A feature, so the editor installs its extensions in the container whoever
|
|
491
|
+
# opens it, and its packages are part of the image. Changes apply on the next
|
|
492
|
+
# build, bin/de up --rebuild.
|
|
493
|
+
env_write_host_feature() {
|
|
494
|
+
local dir="$ENV_PATH/.devcontainer/de-host" config='{}' path
|
|
495
|
+
if [ -f "$DE_CONFIG" ]; then
|
|
496
|
+
command -v jq >/dev/null 2>&1 || die "reading $DE_CONFIG needs jq"
|
|
497
|
+
config="$(jq -e 'if type == "object" then . else error("not an object") end' "$DE_CONFIG")" ||
|
|
498
|
+
die "$DE_CONFIG isn't a JSON object"
|
|
499
|
+
fi
|
|
500
|
+
# Docker refuses a missing bind source
|
|
501
|
+
while IFS= read -r path; do
|
|
502
|
+
case "$path" in "" | /* | *..*) die "home in $DE_CONFIG takes paths inside the home directory, not $path" ;; esac
|
|
503
|
+
mkdir -p "$DE_HOME_STATE/$path"
|
|
504
|
+
done < <([ "$config" = '{}' ] || jq -r '.home // [] | .[]' <<<"$config")
|
|
505
|
+
mkdir -p "$dir"
|
|
506
|
+
if [ "$config" = '{}' ]; then
|
|
507
|
+
printf '{ "id": "de-host", "version": "1.0.0", "name": "Nothing from %s" }\n' "$DE_CONFIG" > "$dir/devcontainer-feature.json"
|
|
508
|
+
printf '#!/bin/sh\n# Written by bin/de, %s has nothing to install\n' "$DE_CONFIG" > "$dir/install.sh"
|
|
509
|
+
else
|
|
510
|
+
jq --arg config "$DE_CONFIG" --arg state "$DE_HOME_STATE" '{
|
|
511
|
+
id: "de-host",
|
|
512
|
+
version: "1.0.0",
|
|
513
|
+
name: "Your own tools from \($config), written by bin/de",
|
|
514
|
+
installsAfter: ["ghcr.io/devcontainers/features/node"],
|
|
515
|
+
customizations: { vscode: { extensions: (.extensions // []) } }
|
|
516
|
+
}
|
|
517
|
+
+ (if (.features // {}) != {} then { dependsOn: .features } else {} end)
|
|
518
|
+
+ (if (.home // []) != [] then { mounts: [.home[] | { type: "bind", source: "\($state)/\(.)", target: "/home/vscode/\(.)" }] } else {} end)
|
|
519
|
+
+ (if (.env // {}) != {} then { containerEnv: .env } else {} end)' <<<"$config" > "$dir/devcontainer-feature.json"
|
|
520
|
+
{
|
|
521
|
+
printf '#!/bin/sh\n# Written by bin/de from %s, do not edit\nset -e\n' "$DE_CONFIG"
|
|
522
|
+
jq -r '.npm // [] | if length > 0 then "npm install --global --no-fund --no-audit " + (map(@sh) | join(" ")) else empty end' <<<"$config"
|
|
523
|
+
} > "$dir/install.sh"
|
|
524
|
+
fi
|
|
525
|
+
chmod +x "$dir/install.sh"
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
# Runs on the host before the containers start, see initializeCommand in
|
|
529
|
+
# devcontainer.json, so opening a checkout from an editor works too
|
|
530
|
+
# The Mac's mise refuses a checkout's config until it's trusted, also for
|
|
531
|
+
# commands this script runs there, like npx. Trust is per path, so every
|
|
532
|
+
# worktree needs it, the container trusts its own copy in postStart.sh.
|
|
533
|
+
env_trust_mise() {
|
|
534
|
+
local file
|
|
535
|
+
command -v mise >/dev/null 2>&1 || return 0
|
|
536
|
+
for file in mise.toml mise.local.toml .mise.toml .mise.local.toml; do
|
|
537
|
+
[ -f "$ENV_PATH/$file" ] || continue
|
|
538
|
+
log "Trusting $file on the host (--no-trust to skip)"
|
|
539
|
+
run mise trust --quiet "$ENV_PATH/$file"
|
|
540
|
+
done
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
cmd_initialize() {
|
|
544
|
+
# docker-compose.yml mounts it, and Docker refuses a missing bind source
|
|
545
|
+
mkdir -p "$HOME/.config/git"
|
|
546
|
+
env_write_dotenv
|
|
547
|
+
env_write_host_feature
|
|
548
|
+
log "Environment $ENV_NAME ($ENV_KIND)"
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
# Copies the setup into a repository, from the de repository. bin/de is
|
|
552
|
+
# generic and always updated; the template is repository
|
|
553
|
+
# specific, a starting point that is only copied when missing, or with --force.
|
|
554
|
+
# As de update, the same for a repository that's set up already.
|
|
555
|
+
# The repository's name (ENV_REPO, @REPO@ in the template) is --name, else
|
|
556
|
+
# the ENV_REPO it already has, else its main checkout's folder name.
|
|
557
|
+
cmd_init() {
|
|
558
|
+
local target="" force=false name="" rel src common repo current up_command entry
|
|
559
|
+
local usage_line="usage: de $command [<repository>] [--name <name>] [--force]"
|
|
560
|
+
while [ "$#" -gt 0 ]; do
|
|
561
|
+
case "$1" in
|
|
562
|
+
--force) force=true; shift ;;
|
|
563
|
+
--name) [ "$#" -ge 2 ] || die "$usage_line"; name="$2"; shift 2 ;;
|
|
564
|
+
--name=*) name="${1#--name=}"; shift ;;
|
|
565
|
+
-*) die "$usage_line" ;;
|
|
566
|
+
*) [ -z "$target" ] || die "$usage_line"; target="$1"; shift ;;
|
|
567
|
+
esac
|
|
568
|
+
done
|
|
569
|
+
if [ -n "$name" ] && [ "$(env_sanitize "$name")" != "$name" ]; then
|
|
570
|
+
die "--name $name: use lowercase letters, digits and dashes, e.g. $(env_sanitize "$name")"
|
|
571
|
+
fi
|
|
572
|
+
target="${target:-$PWD}"
|
|
573
|
+
[ -d "$target" ] || die "$target doesn't exist"
|
|
574
|
+
target="$(cd "$(git -C "$target" rev-parse --show-toplevel 2>/dev/null)" && pwd -P)" ||
|
|
575
|
+
die "$target isn't a git checkout"
|
|
576
|
+
[ "$target" != "$DE_ROOT" ] || die "$target is the de repository itself"
|
|
577
|
+
if [ "$command" = update ] && [ ! -f "$target/.devcontainer/env.conf" ]; then
|
|
578
|
+
die "$target isn't set up yet, run: de init"
|
|
579
|
+
fi
|
|
580
|
+
|
|
581
|
+
# In a subshell, env.conf is shell and sets more than ENV_REPO
|
|
582
|
+
current=""
|
|
583
|
+
if [ -f "$target/.devcontainer/env.conf" ]; then
|
|
584
|
+
current="$(ENV_REPO="" && source "$target/.devcontainer/env.conf" && printf '%s' "$ENV_REPO")"
|
|
585
|
+
fi
|
|
586
|
+
# Renaming would leave environments behind under the old name
|
|
587
|
+
if [ -n "$name" ] && [ -n "$current" ] && [ "$name" != "$current" ]; then
|
|
588
|
+
$force || die "$target is named $current already. To rename it, run bin/de down in each of its checkouts, then set ENV_REPO=$name in .devcontainer/env.conf (and \"name\" in devcontainer.json)"
|
|
589
|
+
warn "renaming $current to $name, environments created as $current are left as they are"
|
|
590
|
+
fi
|
|
591
|
+
if [ -n "$name" ]; then
|
|
592
|
+
repo="$name"
|
|
593
|
+
elif [ -n "$current" ]; then
|
|
594
|
+
repo="$current"
|
|
595
|
+
else
|
|
596
|
+
common="$(cd "$target" && cd "$(git rev-parse --git-common-dir)" && pwd -P)"
|
|
597
|
+
repo="$(env_sanitize "$(basename "$(dirname "$common")")")"
|
|
598
|
+
fi
|
|
599
|
+
if [ -d "$target/test/dummy" ] && [ -x "$target/bin/rails" ]; then
|
|
600
|
+
up_command="env RAILS_ENV=test bin/rails app:db:prepare"
|
|
601
|
+
elif [ -x "$target/bin/rails" ]; then
|
|
602
|
+
up_command="env RAILS_ENV=test bin/rails db:prepare"
|
|
603
|
+
else
|
|
604
|
+
up_command=""
|
|
605
|
+
fi
|
|
606
|
+
if [ "$command" = update ]; then
|
|
607
|
+
log "Updating development environments in $target ($repo)"
|
|
608
|
+
else
|
|
609
|
+
log "Setting up development environments in $target ($repo)"
|
|
610
|
+
fi
|
|
611
|
+
|
|
612
|
+
# Generic: always the same as in the de repository
|
|
613
|
+
if cmp -s "$DE_ROOT/bin/de" "$target/bin/de"; then
|
|
614
|
+
printf ' %-36s up to date\n' bin/de >&2
|
|
615
|
+
else
|
|
616
|
+
run mkdir -p "$target/bin"
|
|
617
|
+
run cp -p "$DE_ROOT/bin/de" "$target/bin/de"
|
|
618
|
+
fi
|
|
619
|
+
# The docs used to be copied too, they live in the de repository now
|
|
620
|
+
if [ -f "$target/.devcontainer/README.md" ] && grep -q '^`bin/de` manages them' "$target/.devcontainer/README.md"; then
|
|
621
|
+
log "Removing .devcontainer/README.md, the docs are at $DE_DOCS"
|
|
622
|
+
run rm "$target/.devcontainer/README.md"
|
|
623
|
+
fi
|
|
624
|
+
|
|
625
|
+
# Repository specific: services, image, setup steps and settings
|
|
626
|
+
while IFS= read -r rel; do
|
|
627
|
+
rel="${rel#./}"
|
|
628
|
+
if [ -e "$target/$rel" ] && ! $force; then
|
|
629
|
+
printf ' %-36s exists, kept (--force to replace)\n' "$rel" >&2
|
|
630
|
+
continue
|
|
631
|
+
fi
|
|
632
|
+
run mkdir -p "$(dirname "$target/$rel")"
|
|
633
|
+
run cp -p "$DE_TEMPLATE/$rel" "$target/$rel"
|
|
634
|
+
sed -i '' -e "s|@REPO@|$repo|g" -e "s|@UP_COMMAND@|$up_command|g" "$target/$rel"
|
|
635
|
+
done < <(cd "$DE_TEMPLATE" && find . -type f | sort)
|
|
636
|
+
|
|
637
|
+
if ! grep -q '"\./de-host"' "$target/.devcontainer/devcontainer.json"; then
|
|
638
|
+
warn "add \"./de-host\": {} to the features in .devcontainer/devcontainer.json, for your own tools from $DE_CONFIG"
|
|
639
|
+
fi
|
|
640
|
+
|
|
641
|
+
# Never commit generated files or local secrets
|
|
642
|
+
for entry in .devcontainer/.env .devcontainer/de-host/ mise.local.toml; do
|
|
643
|
+
# A file inside, as a directory entry like de-host/ only matches what's in it
|
|
644
|
+
git -C "$target" check-ignore -q --no-index -- "${entry%/}$([ "${entry%/}" = "$entry" ] || echo /x)" && continue
|
|
645
|
+
log "Adding $entry to .gitignore"
|
|
646
|
+
# Not onto the last line when the file doesn't end in a newline
|
|
647
|
+
if [ -s "$target/.gitignore" ] && [ -n "$(tail -c1 "$target/.gitignore")" ]; then
|
|
648
|
+
echo >> "$target/.gitignore"
|
|
649
|
+
fi
|
|
650
|
+
printf '%s\n' "$entry" >> "$target/.gitignore"
|
|
651
|
+
done
|
|
652
|
+
|
|
653
|
+
if [ "$command" = update ]; then
|
|
654
|
+
log "Done, review the changes in $target and commit them"
|
|
655
|
+
return
|
|
656
|
+
fi
|
|
657
|
+
|
|
658
|
+
cat <<EOF
|
|
659
|
+
|
|
660
|
+
Done. Review the repository specific files for $repo, then start it:
|
|
661
|
+
|
|
662
|
+
.devcontainer/env.conf ENV_REPO, PERSISTENT_BRANCHES, UP_COMMAND
|
|
663
|
+
.devcontainer/docker-compose.yml services, keep the x-env-labels and mounts
|
|
664
|
+
.devcontainer/postStart.sh installing dependencies, the dev database
|
|
665
|
+
.devcontainer/devcontainer.json features, keep ./de-host and the mounts
|
|
666
|
+
.worktreeinclude ignored files new worktrees need
|
|
667
|
+
orca.yaml Orca's worktree hooks
|
|
668
|
+
|
|
669
|
+
cd $target && bin/de up
|
|
670
|
+
|
|
671
|
+
Run de update there later to update its bin/de.
|
|
672
|
+
EOF
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
# Pulls the de repository, as install.sh does when it's already installed.
|
|
676
|
+
# Repositories keep their bin/de until de update runs there.
|
|
677
|
+
cmd_upgrade() {
|
|
678
|
+
local before after
|
|
679
|
+
[ "$#" -eq 0 ] || die "usage: de upgrade"
|
|
680
|
+
git -C "$DE_ROOT" rev-parse --git-dir >/dev/null 2>&1 || die "$DE_ROOT isn't a git checkout"
|
|
681
|
+
before="$(git -C "$DE_ROOT" rev-parse HEAD)"
|
|
682
|
+
log "Updating $DE_ROOT"
|
|
683
|
+
git -C "$DE_ROOT" pull --ff-only --quiet || die "couldn't update $DE_ROOT"
|
|
684
|
+
after="$(git -C "$DE_ROOT" rev-parse HEAD)"
|
|
685
|
+
if [ "$before" = "$after" ]; then
|
|
686
|
+
log "Already up to date"
|
|
687
|
+
return
|
|
688
|
+
fi
|
|
689
|
+
git -C "$DE_ROOT" log --oneline --no-decorate "$before..$after" >&2
|
|
690
|
+
log "Updated, run de update in a repository to update its bin/de"
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
cmd_new() {
|
|
694
|
+
local branch="" folder="" from="" file dest up_args=() existing=false
|
|
695
|
+
local copy="" copy_from="" yes=false source="" source_name="" target_name path checkout_branch answer
|
|
696
|
+
local usage_line="usage: bin/de new <branch> [folder] [--from <ref>] [--copy-data[=<services>]] [--copy-data-from <checkout>] [--yes] [--no-trust]"
|
|
697
|
+
while [ "$#" -gt 0 ]; do
|
|
698
|
+
case "$1" in
|
|
699
|
+
--from) [ "$#" -ge 2 ] || die "$usage_line"; from="$2"; shift 2 ;;
|
|
700
|
+
--copy-data) copy=all; shift ;;
|
|
701
|
+
--copy-data=*) copy="${1#--copy-data=}"; [ -n "$copy" ] || die "$usage_line"; shift ;;
|
|
702
|
+
--copy-data-from) [ "$#" -ge 2 ] || die "$usage_line"; copy_from="$2"; shift 2 ;;
|
|
703
|
+
--copy-data-from=*) copy_from="${1#--copy-data-from=}"; shift ;;
|
|
704
|
+
--yes | -y) yes=true; shift ;;
|
|
705
|
+
--no-trust) up_args+=(--no-trust); shift ;;
|
|
706
|
+
-*) die "$usage_line" ;;
|
|
707
|
+
*)
|
|
708
|
+
if [ -z "$branch" ]; then branch="$1"
|
|
709
|
+
elif [ -z "$folder" ]; then folder="$1"
|
|
710
|
+
else die "$usage_line"
|
|
711
|
+
fi
|
|
712
|
+
shift
|
|
713
|
+
;;
|
|
714
|
+
esac
|
|
715
|
+
done
|
|
716
|
+
[ -n "$branch" ] || die "$usage_line"
|
|
717
|
+
git check-ref-format --branch "$branch" >/dev/null || die "$branch isn't a valid branch name"
|
|
718
|
+
[ -n "$copy_from" ] && [ -z "$copy" ] && copy=all
|
|
719
|
+
|
|
720
|
+
[ -n "$folder" ] || folder="$(dirname "$ENV_MAIN_CHECKOUT")/$(env_sanitize "$ENV_REPO-$branch")"
|
|
721
|
+
[ -e "$folder" ] && die "$folder already exists"
|
|
722
|
+
|
|
723
|
+
if git -C "$ENV_PATH" show-ref --verify --quiet "refs/heads/$branch" ||
|
|
724
|
+
[ -n "$(git -C "$ENV_PATH" for-each-ref --format='%(refname)' "refs/remotes/*/$branch")" ]; then
|
|
725
|
+
existing=true
|
|
726
|
+
elif [ -z "$from" ]; then
|
|
727
|
+
from="$(git -C "$ENV_PATH" symbolic-ref --quiet --short refs/remotes/origin/HEAD || echo HEAD)"
|
|
728
|
+
# Prefer the local branch when there is one, e.g. main over origin/main
|
|
729
|
+
git -C "$ENV_PATH" show-ref --verify --quiet "refs/heads/${from#origin/}" && from="${from#origin/}"
|
|
730
|
+
fi
|
|
731
|
+
|
|
732
|
+
# Everything about the copy is checked before the worktree is created.
|
|
733
|
+
# The data comes from the checkout of the branch the new one starts from,
|
|
734
|
+
# or for an existing branch, from this checkout.
|
|
735
|
+
if [ -n "$copy" ]; then
|
|
736
|
+
if [ -n "$copy_from" ]; then
|
|
737
|
+
source="$(checkout_for "$copy_from")"
|
|
738
|
+
if [ -z "$source" ]; then
|
|
739
|
+
warn "no checkout of $ENV_REPO matches $copy_from, these exist:"
|
|
740
|
+
checkouts_print
|
|
741
|
+
die "nothing created"
|
|
742
|
+
fi
|
|
743
|
+
elif $existing || [ "$from" = HEAD ]; then
|
|
744
|
+
source="$ENV_PATH"
|
|
745
|
+
else
|
|
746
|
+
while IFS=$'\t' read -r path checkout_branch; do
|
|
747
|
+
if [ -d "$path" ] && [ -n "$checkout_branch" ] &&
|
|
748
|
+
{ [ "$from" = "$checkout_branch" ] || [ "$from" = "origin/$checkout_branch" ]; }; then
|
|
749
|
+
source="$path"
|
|
750
|
+
break
|
|
751
|
+
fi
|
|
752
|
+
done < <(checkouts)
|
|
753
|
+
[ -n "$source" ] || die "$from isn't checked out, so it has no environment to copy data from. Pick one with --copy-data-from <checkout>"
|
|
754
|
+
fi
|
|
755
|
+
source_name="$(env_name_for "$source")"
|
|
756
|
+
target_name="$(env_name_for_branch "$branch")"
|
|
757
|
+
[ "$source_name" != "$target_name" ] || die "$branch's environment is $source_name itself, nothing to copy"
|
|
758
|
+
[ -n "$(docker container ls -aq --filter "label=com.docker.compose.project=$source_name")" ] ||
|
|
759
|
+
die "$source_name has no environment to copy data from, start it with bin/de up in $source"
|
|
760
|
+
[ -z "$(docker volume ls -q --filter "label=com.docker.compose.project=$target_name")" ] ||
|
|
761
|
+
die "$target_name has volumes already, remove them first, e.g. with bin/de prune"
|
|
762
|
+
copy_data_find "$source_name" "$copy"
|
|
763
|
+
|
|
764
|
+
log "Copying data from $source_name ($source) to $target_name:"
|
|
765
|
+
printf ' %s\n' "${COPY_VOLUMES[@]%% *}" >&2
|
|
766
|
+
if [ -n "$COPY_RUNNING" ] && ! $yes; then
|
|
767
|
+
[ -t 0 ] || die "pass --yes to stop$COPY_RUNNING_SERVICES of $source_name for the copy without a terminal"
|
|
768
|
+
read -r -p "This stops$COPY_RUNNING_SERVICES of $source_name during the copy, then starts them again. Continue? [y/N] " answer
|
|
769
|
+
[ "$answer" = y ] || [ "$answer" = Y ] || die "nothing created"
|
|
770
|
+
fi
|
|
771
|
+
fi
|
|
772
|
+
|
|
773
|
+
log "Creating worktree $folder for $branch"
|
|
774
|
+
if $existing; then
|
|
775
|
+
# Creates a local branch tracking the remote one, when there's only that
|
|
776
|
+
run git -C "$ENV_PATH" worktree add "$folder" "$branch"
|
|
777
|
+
else
|
|
778
|
+
run git -C "$ENV_PATH" worktree add -b "$branch" "$folder" "$from"
|
|
779
|
+
fi
|
|
780
|
+
folder="$(cd "$folder" && pwd -P)"
|
|
781
|
+
|
|
782
|
+
# Branches that predate the environment setup get it from this checkout,
|
|
783
|
+
# with mise.toml: its SELENIUM_URL would override the environment's
|
|
784
|
+
if [ ! -x "$folder/bin/de" ]; then
|
|
785
|
+
warn "$branch has no environment setup, copying it from $ENV_PATH (uncommitted there)"
|
|
786
|
+
for file in "$ENV_PATH"/.devcontainer/* "$ENV_PATH/bin/de" "$ENV_PATH/mise.toml"; do
|
|
787
|
+
[ -e "$file" ] || continue
|
|
788
|
+
dest="$folder/${file#"$ENV_PATH"/}"
|
|
789
|
+
run mkdir -p "$(dirname "$dest")"
|
|
790
|
+
run cp -p "$file" "$dest"
|
|
791
|
+
done
|
|
792
|
+
fi
|
|
793
|
+
|
|
794
|
+
# A branch's older bin/de may not know --no-trust
|
|
795
|
+
if [ "${#up_args[@]}" -gt 0 ] && ! grep -q -- '--no-trust' "$folder/bin/de"; then
|
|
796
|
+
warn "$branch's bin/de doesn't support --no-trust, it won't trust mise config anyway"
|
|
797
|
+
up_args=()
|
|
798
|
+
fi
|
|
799
|
+
[ -z "$copy" ] || copy_data "$source_name" "$branch" "$folder"
|
|
800
|
+
run "$folder/bin/de" up ${up_args[@]+"${up_args[@]}"}
|
|
801
|
+
|
|
802
|
+
cat <<EOF
|
|
803
|
+
|
|
804
|
+
Worktree $folder is ready:
|
|
805
|
+
|
|
806
|
+
cd $folder
|
|
807
|
+
bin/de exec bin/rails test
|
|
808
|
+
bin/de code # open in VS Code
|
|
809
|
+
bin/de down # when done, then: git worktree remove $folder
|
|
810
|
+
EOF
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
cmd_up() {
|
|
814
|
+
local up_args=() trust=true arg other entry health deadline
|
|
815
|
+
for arg in "$@"; do
|
|
816
|
+
case "$arg" in
|
|
817
|
+
--rebuild) up_args=(--remove-existing-container) ;;
|
|
818
|
+
--no-trust) trust=false ;;
|
|
819
|
+
*) die "usage: bin/de up [--rebuild] [--no-trust]" ;;
|
|
820
|
+
esac
|
|
821
|
+
done
|
|
822
|
+
|
|
823
|
+
log "Environment $ENV_NAME ($ENV_KIND, branch ${ENV_BRANCH:-detached}) for $ENV_PATH"
|
|
824
|
+
|
|
825
|
+
# Share nothing: refuse to adopt another checkout's resources
|
|
826
|
+
other="$(env_label "$ENV_NAME" dev.env.path)"
|
|
827
|
+
if [ -n "$other" ] && [ "$other" != "$ENV_PATH" ]; then
|
|
828
|
+
die "$ENV_NAME already belongs to $other, remove that environment first"
|
|
829
|
+
fi
|
|
830
|
+
|
|
831
|
+
# Worktree .git files point to the main repository by relative path, so the
|
|
832
|
+
# link also resolves inside the container
|
|
833
|
+
if [ "$(git -C "$ENV_PATH" config --get worktree.useRelativePaths || true)" != true ]; then
|
|
834
|
+
log "Enabling relative worktree paths"
|
|
835
|
+
run git -C "$ENV_PATH" config worktree.useRelativePaths true
|
|
836
|
+
fi
|
|
837
|
+
if [ "$ENV_PATH" != "$ENV_MAIN_CHECKOUT" ]; then
|
|
838
|
+
run git -C "$ENV_MAIN_CHECKOUT" worktree repair --relative-paths "$ENV_PATH"
|
|
839
|
+
|
|
840
|
+
# Copy (never link) the ignored local files a worktree needs, from the
|
|
841
|
+
# same list other worktree tools use
|
|
842
|
+
if [ -f "$ENV_MAIN_CHECKOUT/.worktreeinclude" ]; then
|
|
843
|
+
while IFS= read -r entry || [ -n "$entry" ]; do
|
|
844
|
+
case "$entry" in "" | "#"*) continue ;; esac
|
|
845
|
+
[ -e "$ENV_MAIN_CHECKOUT/$entry" ] || continue
|
|
846
|
+
[ -e "$ENV_PATH/$entry" ] && continue
|
|
847
|
+
git -C "$ENV_MAIN_CHECKOUT" check-ignore -q -- "$entry" || continue
|
|
848
|
+
log "Copying $entry from the main checkout"
|
|
849
|
+
run mkdir -p "$(dirname "$ENV_PATH/$entry")"
|
|
850
|
+
run cp -pR "$ENV_MAIN_CHECKOUT/$entry" "$ENV_PATH/$entry"
|
|
851
|
+
done < "$ENV_MAIN_CHECKOUT/.worktreeinclude"
|
|
852
|
+
fi
|
|
853
|
+
fi
|
|
854
|
+
|
|
855
|
+
if $trust; then
|
|
856
|
+
env_trust_mise
|
|
857
|
+
fi
|
|
858
|
+
|
|
859
|
+
log "Writing $ENV_DOTENV"
|
|
860
|
+
env_write_dotenv
|
|
861
|
+
env_write_host_feature
|
|
862
|
+
|
|
863
|
+
log "Building and starting the devcontainer"
|
|
864
|
+
devcontainer_cli up --workspace-folder "$ENV_PATH" ${up_args[@]+"${up_args[@]}"}
|
|
865
|
+
|
|
866
|
+
log "Waiting for health checks"
|
|
867
|
+
deadline=$((SECONDS + 300))
|
|
868
|
+
while :; do
|
|
869
|
+
health="$(env_health "$ENV_NAME")"
|
|
870
|
+
if [ -n "$health" ] && ! grep -qvE ' (healthy|running)$' <<<"$health"; then
|
|
871
|
+
break
|
|
872
|
+
fi
|
|
873
|
+
[ "$SECONDS" -lt "$deadline" ] || die "services not healthy after 5 minutes:"$'\n'"$health"
|
|
874
|
+
sleep 2
|
|
875
|
+
done
|
|
876
|
+
sed 's/^/ /' <<<"$health" >&2
|
|
877
|
+
|
|
878
|
+
if [ "${#UP_COMMAND[@]}" -gt 0 ]; then
|
|
879
|
+
log "Running UP_COMMAND from env.conf"
|
|
880
|
+
(cd "$ENV_PATH" && cmd_exec "${UP_COMMAND[@]}")
|
|
881
|
+
fi
|
|
882
|
+
|
|
883
|
+
cmd_status
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
cmd_exec() {
|
|
887
|
+
local cwd dir remote_env=()
|
|
888
|
+
[ "$#" -gt 0 ] || die "usage: bin/de exec <command> [arguments]"
|
|
889
|
+
env_running "$ENV_NAME" || die "environment $ENV_NAME isn't running, start it with bin/de up"
|
|
890
|
+
|
|
891
|
+
cwd="$(pwd -P)"
|
|
892
|
+
case "$cwd/" in
|
|
893
|
+
"$ENV_PATH/"*) dir="$ENV_PATH_IN_CONTAINER${cwd#"$ENV_PATH"}" ;;
|
|
894
|
+
*) dir="$ENV_PATH_IN_CONTAINER" ;;
|
|
895
|
+
esac
|
|
896
|
+
|
|
897
|
+
if [ -n "$ENV_SELENIUM_URL_GIVEN" ]; then
|
|
898
|
+
log "Using Selenium at $ENV_SELENIUM_URL_GIVEN"
|
|
899
|
+
remote_env+=(--remote-env "SELENIUM_URL=$ENV_SELENIUM_URL_GIVEN")
|
|
900
|
+
remote_env+=(--remote-env "CAPYBARA_APP_HOST=$(env_capybara_app_host "$ENV_SELENIUM_URL_GIVEN")")
|
|
901
|
+
fi
|
|
902
|
+
if [ -n "${HEADLESS:-}" ]; then
|
|
903
|
+
remote_env+=(--remote-env "HEADLESS=$HEADLESS")
|
|
904
|
+
fi
|
|
905
|
+
|
|
906
|
+
# mise on PATH too: its RubyGems plugin runs `mise reshim` after a gem
|
|
907
|
+
# install, and without it Bundler crashes (and prints its config).
|
|
908
|
+
devcontainer_cli exec --workspace-folder "$ENV_PATH" ${remote_env[@]+"${remote_env[@]}"} \
|
|
909
|
+
bash -c 'cd "$1" && shift && PATH="$HOME/.local/bin:$PATH" exec mise exec -- "$@"' de-exec "$dir" "$@"
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
cmd_status() {
|
|
913
|
+
local ids id service port
|
|
914
|
+
[ "$#" -eq 0 ] || die "usage: bin/de status"
|
|
915
|
+
|
|
916
|
+
cat <<EOF
|
|
917
|
+
Environment $ENV_NAME
|
|
918
|
+
Kind $ENV_KIND
|
|
919
|
+
Branch ${ENV_BRANCH:-(detached)}
|
|
920
|
+
Checkout $ENV_PATH
|
|
921
|
+
In container $ENV_WORKSPACE_LINK
|
|
922
|
+
|
|
923
|
+
EOF
|
|
924
|
+
|
|
925
|
+
log "Containers"
|
|
926
|
+
run docker container ls -a --filter "label=dev.env.name=$ENV_NAME" \
|
|
927
|
+
--format 'table {{.Label "com.docker.compose.service"}}\t{{.Names}}\t{{.Status}}\t{{.Ports}}'
|
|
928
|
+
|
|
929
|
+
ids="$(docker container ls -q --filter "label=dev.env.name=$ENV_NAME")"
|
|
930
|
+
if [ -n "$ids" ]; then
|
|
931
|
+
echo
|
|
932
|
+
log "Reachable from the Mac (OrbStack domains)"
|
|
933
|
+
for id in $ids; do
|
|
934
|
+
service="$(docker container inspect "$id" --format '{{ index .Config.Labels "com.docker.compose.service" }}')"
|
|
935
|
+
for port in $(docker container inspect "$id" --format '{{ range $p, $_ := .Config.ExposedPorts }}{{ $p }} {{ end }}'); do
|
|
936
|
+
printf ' %-10s %s.%s.orb.local:%s\n' "$service" "$service" "$ENV_NAME" "${port%/*}"
|
|
937
|
+
done
|
|
938
|
+
# Only when a host port was published, Docker picks the number
|
|
939
|
+
docker port "$id" | sed "s/^/ $service published /"
|
|
940
|
+
done
|
|
941
|
+
fi
|
|
942
|
+
|
|
943
|
+
echo
|
|
944
|
+
log "Volumes"
|
|
945
|
+
run docker volume ls --filter "label=com.docker.compose.project=$ENV_NAME"
|
|
946
|
+
echo
|
|
947
|
+
log "Networks"
|
|
948
|
+
run docker network ls --filter "label=dev.env.name=$ENV_NAME"
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
# Runs `code` (CODE to override, e.g. CODE=cursor) with the dev container URI
|
|
952
|
+
# itself: vscli resolves ${localWorkspaceFolder} in devcontainer.json to
|
|
953
|
+
# /workspaces/<folder name> instead of the host path.
|
|
954
|
+
cmd_code() {
|
|
955
|
+
local target="$ENV_PATH" name host_hex
|
|
956
|
+
if [ "$#" -gt 1 ]; then
|
|
957
|
+
die "usage: bin/de code [branch | environment | folder | path]"
|
|
958
|
+
elif [ "$#" -eq 1 ]; then
|
|
959
|
+
target="$(checkout_for "$1")"
|
|
960
|
+
if [ -z "$target" ]; then
|
|
961
|
+
warn "no checkout of $ENV_REPO matches $1, these exist:"
|
|
962
|
+
checkouts_print
|
|
963
|
+
die "create one with: bin/de new $1"
|
|
964
|
+
fi
|
|
965
|
+
fi
|
|
966
|
+
|
|
967
|
+
[ -x "$target/bin/de" ] || die "$target has no bin/de, it predates the environment setup"
|
|
968
|
+
|
|
969
|
+
name="$(env_name_for "$target")"
|
|
970
|
+
if ! env_running "$name"; then
|
|
971
|
+
log "Environment $name isn't running, starting it"
|
|
972
|
+
if ! run "$target/bin/de" up; then
|
|
973
|
+
# Still open it when only a setup step failed, the editor helps fix it
|
|
974
|
+
env_running "$name" || die "bin/de up failed and $name isn't running"
|
|
975
|
+
warn "bin/de up failed, see above, opening $name anyway"
|
|
976
|
+
fi
|
|
977
|
+
fi
|
|
978
|
+
|
|
979
|
+
log "Opening $target ($name) in VS Code"
|
|
980
|
+
# The authority is the host folder, hex encoded, the path is
|
|
981
|
+
# devcontainer.json's workspaceFolder: /workspaces/<host path>
|
|
982
|
+
host_hex="$(printf '%s' "$target" | xxd -p | tr -d '\n')"
|
|
983
|
+
run "${CODE:-code}" --folder-uri "vscode-remote://dev-container+$host_hex/workspaces$target"
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
cmd_down() {
|
|
987
|
+
local destroy_persistent=false arg answer
|
|
988
|
+
for arg in "$@"; do
|
|
989
|
+
case "$arg" in
|
|
990
|
+
--destroy-persistent) destroy_persistent=true ;;
|
|
991
|
+
*) die "usage: bin/de down [--destroy-persistent]" ;;
|
|
992
|
+
esac
|
|
993
|
+
done
|
|
994
|
+
|
|
995
|
+
env_write_dotenv
|
|
996
|
+
|
|
997
|
+
if [ "$ENV_KIND" = persistent ] || env_is_persistent "$ENV_NAME"; then
|
|
998
|
+
if ! $destroy_persistent; then
|
|
999
|
+
log "Stopping persistent environment $ENV_NAME, its volumes are kept"
|
|
1000
|
+
env_compose stop
|
|
1001
|
+
return
|
|
1002
|
+
fi
|
|
1003
|
+
|
|
1004
|
+
[ -t 0 ] || die "--destroy-persistent needs a terminal to confirm"
|
|
1005
|
+
warn "This deletes $ENV_NAME and all of its data:"
|
|
1006
|
+
docker volume ls --filter "label=com.docker.compose.project=$ENV_NAME" >&2
|
|
1007
|
+
read -r -p "Type the environment name to confirm: " answer
|
|
1008
|
+
[ "$answer" = "$ENV_NAME" ] || die "name didn't match, nothing removed"
|
|
1009
|
+
else
|
|
1010
|
+
log "Removing ephemeral environment $ENV_NAME"
|
|
1011
|
+
fi
|
|
1012
|
+
|
|
1013
|
+
env_compose down --volumes --remove-orphans
|
|
1014
|
+
env_destroy "$ENV_NAME"
|
|
1015
|
+
log "Removed $ENV_NAME"
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
cmd_prune() {
|
|
1019
|
+
local yes=false arg name path branch reason answer orphans=()
|
|
1020
|
+
for arg in "$@"; do
|
|
1021
|
+
case "$arg" in
|
|
1022
|
+
--yes | -y) yes=true ;;
|
|
1023
|
+
*) die "usage: bin/de prune [--yes]" ;;
|
|
1024
|
+
esac
|
|
1025
|
+
done
|
|
1026
|
+
|
|
1027
|
+
log "Looking for ephemeral environments of $ENV_GIT_COMMON_DIR"
|
|
1028
|
+
for name in $(env_list ephemeral); do
|
|
1029
|
+
if env_is_persistent "$name"; then
|
|
1030
|
+
warn "$name has persistent resources, skipping"
|
|
1031
|
+
continue
|
|
1032
|
+
fi
|
|
1033
|
+
|
|
1034
|
+
path="$(env_label "$name" dev.env.path)"
|
|
1035
|
+
branch="$(env_label "$name" dev.env.branch)"
|
|
1036
|
+
reason=""
|
|
1037
|
+
if [ -z "$path" ] || [ ! -d "$path" ]; then
|
|
1038
|
+
reason="checkout ${path:-?} no longer exists"
|
|
1039
|
+
elif ! git -C "$path" rev-parse --git-dir >/dev/null 2>&1; then
|
|
1040
|
+
reason="$path is no longer a git checkout"
|
|
1041
|
+
elif [ -n "$branch" ] && ! git -C "$path" show-ref --verify --quiet "refs/heads/$branch"; then
|
|
1042
|
+
reason="branch $branch is gone"
|
|
1043
|
+
elif [ "$(env_name_for "$path")" != "$name" ]; then
|
|
1044
|
+
reason="$path is now on another branch"
|
|
1045
|
+
fi
|
|
1046
|
+
|
|
1047
|
+
if [ -n "$reason" ]; then
|
|
1048
|
+
orphans+=("$name")
|
|
1049
|
+
printf ' %-50s %s\n' "$name" "$reason"
|
|
1050
|
+
else
|
|
1051
|
+
printf ' %-50s in use by %s, keeping\n' "$name" "$path"
|
|
1052
|
+
fi
|
|
1053
|
+
done
|
|
1054
|
+
|
|
1055
|
+
if [ "${#orphans[@]}" -eq 0 ]; then
|
|
1056
|
+
log "Nothing to prune"
|
|
1057
|
+
return
|
|
1058
|
+
fi
|
|
1059
|
+
|
|
1060
|
+
echo
|
|
1061
|
+
log "These resources will be removed:"
|
|
1062
|
+
for name in "${orphans[@]}"; do
|
|
1063
|
+
docker container ls -a --filter "label=com.docker.compose.project=$name" --format ' container {{.Names}}'
|
|
1064
|
+
docker volume ls --filter "label=com.docker.compose.project=$name" --format ' volume {{.Name}}'
|
|
1065
|
+
docker network ls --filter "label=com.docker.compose.project=$name" --format ' network {{.Name}}'
|
|
1066
|
+
done
|
|
1067
|
+
|
|
1068
|
+
if ! $yes; then
|
|
1069
|
+
[ -t 0 ] || die "pass --yes to prune without a terminal"
|
|
1070
|
+
read -r -p "Remove ${#orphans[@]} environment(s)? [y/N] " answer
|
|
1071
|
+
[ "$answer" = y ] || [ "$answer" = Y ] || die "nothing removed"
|
|
1072
|
+
fi
|
|
1073
|
+
|
|
1074
|
+
for name in "${orphans[@]}"; do
|
|
1075
|
+
log "Removing $name"
|
|
1076
|
+
env_destroy "$name"
|
|
1077
|
+
done
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
# --- Main ---------------------------------------------------------------
|
|
1081
|
+
|
|
1082
|
+
command="${1:-help}"
|
|
1083
|
+
[ "$#" -gt 0 ] && shift
|
|
1084
|
+
|
|
1085
|
+
if [ -n "$DE_TEMPLATE" ]; then
|
|
1086
|
+
case "$command" in
|
|
1087
|
+
help | -h | --help) usage_source; exit ;;
|
|
1088
|
+
init | update) cmd_init "$@"; exit ;;
|
|
1089
|
+
upgrade) cmd_upgrade "$@"; exit ;;
|
|
1090
|
+
esac
|
|
1091
|
+
# `code <folder>` (Orca's "Open in" runs it from /) opens the checkout that
|
|
1092
|
+
# folder is in, from inside it, so a checkout's older bin/de handles it too
|
|
1093
|
+
if [ "$command" = code ] && [ "$#" -eq 1 ] && [ -d "$1" ]; then
|
|
1094
|
+
cd "$1"
|
|
1095
|
+
shift
|
|
1096
|
+
fi
|
|
1097
|
+
# Everything else is for the checkout the current directory is in
|
|
1098
|
+
checkout="$(git rev-parse --show-toplevel 2>/dev/null)" || die "not inside a git checkout"
|
|
1099
|
+
[ -f "$checkout/.devcontainer/env.conf" ] && [ -x "$checkout/bin/de" ] ||
|
|
1100
|
+
die "$checkout has no development environments, set them up with: de init"
|
|
1101
|
+
exec "$checkout/bin/de" "$command" "$@"
|
|
1102
|
+
fi
|
|
1103
|
+
|
|
1104
|
+
case "$command" in
|
|
1105
|
+
help | -h | --help) usage; exit ;;
|
|
1106
|
+
initialize | new | up | exec | status | code | down | prune) ;;
|
|
1107
|
+
init | update | upgrade) die "run de $command from the de repository, which bin/de comes from" ;;
|
|
1108
|
+
*) usage >&2; die "unknown command: $command" ;;
|
|
1109
|
+
esac
|
|
1110
|
+
|
|
1111
|
+
env_init "$DE_ROOT"
|
|
1112
|
+
"cmd_$command" "$@"
|