valise-sh 0.2.0 → 0.3.0
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.
- package/CHANGELOG.md +152 -2
- package/README.md +99 -146
- package/bin/valise +57 -5376
- package/docs/architecture.md +77 -121
- package/docs/contrib-modules.md +172 -218
- package/docs/installation.md +137 -83
- package/docs/module-abi.md +125 -126
- package/docs/projects.md +249 -0
- package/docs/release-policy.md +68 -93
- package/docs/shared-modules.md +40 -39
- package/examples/README.md +12 -11
- package/examples/e2e/README.md +59 -66
- package/examples/e2e/bundles.tsv +6 -0
- package/examples/e2e/module.d/e2e_aliases.meta +1 -2
- package/examples/e2e/module.d/e2e_completions_bash.meta +0 -1
- package/examples/e2e/module.d/e2e_completions_zsh.meta +0 -1
- package/examples/e2e/module.d/e2e_env_setup.meta +2 -3
- package/examples/e2e/module.d/e2e_fish_features.meta +0 -1
- package/examples/e2e/module.d/e2e_generated_config.meta +1 -2
- package/examples/e2e/module.d/e2e_ksh_features.meta +0 -1
- package/examples/e2e/module.d/e2e_layer_probe.meta +1 -2
- package/examples/e2e/module.d/e2e_optional_network.meta +1 -2
- package/examples/e2e/module.d/e2e_platform_tools.meta +8 -0
- package/examples/e2e/module.d/e2e_platform_tools_bsd.meta +2 -3
- package/examples/e2e/module.d/e2e_platform_tools_darwin.meta +2 -3
- package/examples/e2e/module.d/e2e_platform_tools_linux.meta +2 -3
- package/examples/e2e/valise.conf +4 -0
- package/examples/e2e/variants.tsv +3 -0
- package/examples/local/module.d/hello_valise.meta +1 -2
- package/examples/profile-project/README.md +78 -108
- package/examples/profile-project/bundles.tsv +8 -0
- package/examples/profile-project/{shell/layers → layers}/demo/module.d/contrib_demo_greeting.meta +1 -2
- package/examples/profile-project/{shell/module.d → module.d}/greeting.meta +1 -2
- package/examples/profile-project/{shell/module.d → module.d}/profile_main.meta +0 -1
- package/examples/profile-project/{shell/modules → modules}/greeting.sh +1 -1
- package/examples/profile-project/valise.conf +4 -0
- package/examples/profile-project/variants.tsv +3 -0
- package/examples/shared/README.md +13 -34
- package/examples/shared/module.d/contrib_shared_paths.meta +2 -3
- package/examples/shared/module.d/contrib_shared_umask.meta +1 -2
- package/examples/shared/modules/shared/contrib_shared_paths.sh +2 -2
- package/lib/capabilities.sh +40 -13
- package/lib/cli/commands.sh +278 -0
- package/lib/cli/common.sh +326 -0
- package/lib/cli/devtools.sh +539 -0
- package/lib/cli/fish-hook.sh +986 -0
- package/lib/cli/integration.sh +337 -0
- package/lib/cli/lifecycle.sh +578 -0
- package/lib/cli/project.sh +1807 -0
- package/lib/cli/store.sh +1023 -0
- package/lib/cli/usage.sh +308 -0
- package/lib/compiler.awk +11 -4
- package/lib/fish/loader.fish +69 -9
- package/lib/fish/metadata.fish +49 -5
- package/lib/fish/profile.fish +170 -0
- package/lib/loader.sh +6 -8
- package/lib/metadata.sh +111 -5
- package/manifests/parser-support.tsv +2 -2
- package/module.d/valise_editor.meta +9 -0
- package/module.d/valise_history.meta +9 -0
- package/module.d/valise_pager.meta +9 -0
- package/module.d/valise_path.meta +9 -0
- package/module.d/{platform_bsd.meta → valise_platform_bsd.meta} +3 -4
- package/module.d/{platform_linux.meta → valise_platform_linux.meta} +3 -4
- package/module.d/{platform_macos.meta → valise_platform_macos.meta} +3 -4
- package/module.d/{platform_posix.meta → valise_platform_posix.meta} +2 -3
- package/module.d/valise_xdg.meta +9 -0
- package/modules/system/{contrib_editor.sh → valise_editor.sh} +2 -2
- package/modules/system/{contrib_history.sh → valise_history.sh} +2 -2
- package/modules/system/{contrib_pager.sh → valise_pager.sh} +2 -2
- package/modules/system/{contrib_path.sh → valise_path.sh} +3 -3
- package/modules/system/{contrib_xdg.sh → valise_xdg.sh} +2 -2
- package/package.json +91 -10
- package/docs/variants.md +0 -327
- package/entrypoints/.gitkeep +0 -0
- package/entrypoints/fish/profile.fish +0 -99
- package/examples/e2e/build-manifest.sh +0 -138
- package/examples/local/.gitkeep +0 -0
- package/examples/profile-project/shell/release/bundle-roots +0 -9
- package/examples/profile-project/shell/release/install-variants +0 -3
- package/examples/profile-project/shell/release/module-registry.tsv +0 -4
- package/examples/profile-project/shell/release/project.conf +0 -3
- package/lib/.gitkeep +0 -0
- package/manifests/.gitkeep +0 -0
- package/module.d/.gitkeep +0 -0
- package/module.d/contrib_editor.meta +0 -10
- package/module.d/contrib_history.meta +0 -10
- package/module.d/contrib_pager.meta +0 -10
- package/module.d/contrib_path.meta +0 -10
- package/module.d/contrib_xdg.meta +0 -10
- package/modules/.gitkeep +0 -0
- /package/examples/profile-project/{shell/layers → layers}/demo/modules/shared/contrib_demo_greeting.sh +0 -0
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
# Copyright (c) 2026 Joseph Cumines
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
# valise CLI: lifecycle: the previous-release record and rollback, the install
|
|
4
|
+
# transaction, unmanaged integration detection, uninstall, and the install
|
|
5
|
+
# preview behind apply --dry-run.
|
|
6
|
+
# Sourced by bin/valise; not executable on its own.
|
|
7
|
+
|
|
8
|
+
_prev_tmp=""
|
|
9
|
+
_dry_tmp=""
|
|
10
|
+
_un_tmp=""
|
|
11
|
+
_install_txn=0
|
|
12
|
+
_txn_old_target=""
|
|
13
|
+
_txn_had_previous=0
|
|
14
|
+
_txn_old_previous=""
|
|
15
|
+
_txn_changed=""
|
|
16
|
+
|
|
17
|
+
# The startup files valise may write a managed block into.
|
|
18
|
+
VALISE_STARTUP_NAMES='.profile .bash_profile .bash_login .bashrc .zprofile .zshrc'
|
|
19
|
+
|
|
20
|
+
previous_record_path() {
|
|
21
|
+
printf '%s\n' "$VALISE_XDG_STATE/previous"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
# Startup files install created from nothing, one path per line. Uninstall
|
|
25
|
+
# deletes such a file when removing the block leaves it empty.
|
|
26
|
+
startup_created_path() {
|
|
27
|
+
printf '%s\n' "$VALISE_XDG_STATE/startup-created"
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
fish_hook_path() {
|
|
31
|
+
printf '%s\n' "${XDG_CONFIG_HOME:-$HOME/.config}/fish/conf.d/valise.fish"
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# release_name_valid <name>: a release directory name valise will act on.
|
|
35
|
+
release_name_valid() {
|
|
36
|
+
case "$1" in
|
|
37
|
+
'' | *[!A-Za-z0-9_.-]* | .* | *- | *..*) return 1 ;;
|
|
38
|
+
esac
|
|
39
|
+
return 0
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# write_previous_record <release-id>
|
|
43
|
+
write_previous_record() {
|
|
44
|
+
_wpr_record=$(previous_record_path)
|
|
45
|
+
[ ! -L "$_wpr_record" ] || die "previous-release record must not be a symlink: $_wpr_record"
|
|
46
|
+
_wpr_mkdir=$(trusted_utility_path mkdir) || die "no trusted mkdir utility available"
|
|
47
|
+
"$_wpr_mkdir" -p "$VALISE_XDG_STATE" || die "cannot create state directory: $VALISE_XDG_STATE"
|
|
48
|
+
_prev_tmp=$(trusted_mktemp_at "$VALISE_XDG_STATE" file "$VALISE_XDG_STATE/.previous.XXXXXX") ||
|
|
49
|
+
die "cannot create previous-release staging file"
|
|
50
|
+
printf '%s\n' "$1" >"$_prev_tmp" || die "cannot write previous-release record"
|
|
51
|
+
_wpr_chmod=$(trusted_utility_path chmod) || die "no trusted chmod utility available"
|
|
52
|
+
"$_wpr_chmod" 0644 "$_prev_tmp" || die "cannot set previous-release record mode"
|
|
53
|
+
_wpr_mv=$(trusted_utility_path mv) || die "no trusted mv utility available"
|
|
54
|
+
"$_wpr_mv" -f "$_prev_tmp" "$_wpr_record" || die "cannot publish previous-release record"
|
|
55
|
+
_prev_tmp=""
|
|
56
|
+
unset _wpr_record _wpr_mkdir _wpr_chmod _wpr_mv
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# read_previous_record: print the recorded previous release, or fail.
|
|
60
|
+
read_previous_record() {
|
|
61
|
+
_rpr_record=$(previous_record_path)
|
|
62
|
+
[ -f "$_rpr_record" ] && [ ! -L "$_rpr_record" ] || return 1
|
|
63
|
+
IFS= read -r _rpr_id <"$_rpr_record" || [ -n "${_rpr_id:-}" ] || return 1
|
|
64
|
+
release_name_valid "$_rpr_id" || return 1
|
|
65
|
+
printf '%s\n' "$_rpr_id"
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# note_switch <old-target> <new-target>
|
|
69
|
+
# Record the release the active link named before a switch, when it named an
|
|
70
|
+
# installed release and the switch changed it.
|
|
71
|
+
note_switch() {
|
|
72
|
+
[ -n "$1" ] && [ "$1" != "$2" ] || return 0
|
|
73
|
+
case "$1" in
|
|
74
|
+
"$VALISE_RELEASES_DIR"/*) _ns_id=${1#"$VALISE_RELEASES_DIR"/} ;;
|
|
75
|
+
*) return 0 ;;
|
|
76
|
+
esac
|
|
77
|
+
case "$_ns_id" in */*) unset _ns_id; return 0 ;; esac
|
|
78
|
+
release_name_valid "$_ns_id" || { unset _ns_id; return 0; }
|
|
79
|
+
write_previous_record "$_ns_id"
|
|
80
|
+
unset _ns_id
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# release_activate <release-id>
|
|
84
|
+
# Verify an installed release, make it active, and move the command stubs to
|
|
85
|
+
# it when install published them. Used by release use and release rollback.
|
|
86
|
+
release_activate() {
|
|
87
|
+
_ra_id=$1
|
|
88
|
+
release_name_valid "$_ra_id" || die "invalid release name: $_ra_id"
|
|
89
|
+
_ra_dir="$VALISE_RELEASES_DIR/$_ra_id"
|
|
90
|
+
[ -e "$_ra_dir" ] || die "release not found: $_ra_id"
|
|
91
|
+
[ -d "$_ra_dir" ] || die "release target is not a directory: $_ra_id"
|
|
92
|
+
[ ! -L "$_ra_dir" ] || die "release target must not be a symlink: $_ra_id"
|
|
93
|
+
_ra_root=$(cd "$VALISE_RELEASES_DIR" 2>/dev/null && pwd -P) || die "release root unavailable"
|
|
94
|
+
_ra_parent=$(cd "$(path_parent "$_ra_dir")" 2>/dev/null && pwd -P) || die "release target parent unavailable"
|
|
95
|
+
case "$_ra_parent/" in
|
|
96
|
+
"$_ra_root/"*) : ;;
|
|
97
|
+
*) die "release target escapes release root: $_ra_id" ;;
|
|
98
|
+
esac
|
|
99
|
+
_ra_manifest="$_ra_dir/.valise-manifest"
|
|
100
|
+
[ -f "$_ra_manifest" ] && [ ! -L "$_ra_manifest" ] ||
|
|
101
|
+
die "release manifest snapshot is missing or not a regular file: $_ra_id"
|
|
102
|
+
prepare_manifest_rows "$_ra_manifest" || die "cannot snapshot the release manifest"
|
|
103
|
+
validate_manifest "$_manifest_rows" || die "invalid release manifest snapshot"
|
|
104
|
+
verify_release_dir "$_ra_dir" "$_manifest_rows" "$_ra_id"
|
|
105
|
+
release_commands "$_ra_dir" >/dev/null || die "release $_ra_id lists an invalid command name"
|
|
106
|
+
_ra_config_parent=$(path_parent "$VALISE_ACTIVE_LINK")
|
|
107
|
+
if [ ! -d "$_ra_config_parent" ]; then
|
|
108
|
+
_ra_mkdir=$(trusted_utility_path mkdir) || die "no trusted mkdir utility available"
|
|
109
|
+
"$_ra_mkdir" -p "$_ra_config_parent" || die "cannot create $_ra_config_parent"
|
|
110
|
+
fi
|
|
111
|
+
acquire_lock
|
|
112
|
+
switch_active_link "$_ra_dir"
|
|
113
|
+
# Commands follow the active release only where install published them: a
|
|
114
|
+
# ledger exists once an install ran with integration.
|
|
115
|
+
if [ -f "$(commands_ledger_path)" ]; then
|
|
116
|
+
reconcile_commands "$_ra_dir"
|
|
117
|
+
fi
|
|
118
|
+
release_lock
|
|
119
|
+
unset _ra_id _ra_dir _ra_root _ra_parent _ra_manifest _ra_config_parent _ra_mkdir
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
cmd_release_rollback() {
|
|
123
|
+
case "${1:-}" in
|
|
124
|
+
--help | -h) usage_release; return 0 ;;
|
|
125
|
+
'') ;;
|
|
126
|
+
*) die "unknown release rollback option: $1" ;;
|
|
127
|
+
esac
|
|
128
|
+
_rr_id=$(read_previous_record) ||
|
|
129
|
+
die "no previous release is recorded (one is recorded each time the active release changes)"
|
|
130
|
+
release_activate "$_rr_id"
|
|
131
|
+
info "rolled back to release $_rr_id"
|
|
132
|
+
unset _rr_id
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
# --- Install transaction ---
|
|
136
|
+
|
|
137
|
+
# install_txn_begin: remember what an install may have to restore.
|
|
138
|
+
install_txn_begin() {
|
|
139
|
+
_txn_old_target=
|
|
140
|
+
if [ -L "$VALISE_ACTIVE_LINK" ]; then
|
|
141
|
+
_txn_old_target=$(readlink_target "$VALISE_ACTIVE_LINK" 2>/dev/null || :)
|
|
142
|
+
fi
|
|
143
|
+
_txn_had_previous=0
|
|
144
|
+
_txn_old_previous=
|
|
145
|
+
if _txn_old_previous=$(read_previous_record); then
|
|
146
|
+
_txn_had_previous=1
|
|
147
|
+
fi
|
|
148
|
+
_txn_changed=
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
# install_txn_abort: run from the exit and signal handlers while an install
|
|
152
|
+
# that has switched the active link is unfinished. Puts the previous active
|
|
153
|
+
# release back and says what had already changed. It must not call die.
|
|
154
|
+
install_txn_abort() {
|
|
155
|
+
_install_txn=0
|
|
156
|
+
_ita_ln=$(trusted_utility_path ln 2>/dev/null || :)
|
|
157
|
+
_ita_rm=$(trusted_utility_path rm 2>/dev/null || :)
|
|
158
|
+
if [ -n "$_txn_old_target" ]; then
|
|
159
|
+
if [ -n "$_ita_ln" ] && "$_ita_ln" -sfn "$_txn_old_target" "$VALISE_ACTIVE_LINK" 2>/dev/null; then
|
|
160
|
+
printf 'valise: install failed; the active release is back to %s\n' "${_txn_old_target##*/}" >&2
|
|
161
|
+
else
|
|
162
|
+
printf 'valise: install failed and the previous active release could not be restored: %s\n' "$_txn_old_target" >&2
|
|
163
|
+
fi
|
|
164
|
+
else
|
|
165
|
+
if [ -n "$_ita_rm" ] && "$_ita_rm" -f "$VALISE_ACTIVE_LINK" 2>/dev/null; then
|
|
166
|
+
printf 'valise: install failed; no release is active, as before the install\n' >&2
|
|
167
|
+
else
|
|
168
|
+
printf 'valise: install failed and the new active link could not be removed: %s\n' "$VALISE_ACTIVE_LINK" >&2
|
|
169
|
+
fi
|
|
170
|
+
fi
|
|
171
|
+
if [ "$_txn_had_previous" -eq 1 ]; then
|
|
172
|
+
(
|
|
173
|
+
trap '[ -z "${_prev_tmp:-}" ] || [ -z "$_ita_rm" ] || "$_ita_rm" -f "$_prev_tmp"' EXIT
|
|
174
|
+
write_previous_record "$_txn_old_previous"
|
|
175
|
+
) >/dev/null 2>&1 ||
|
|
176
|
+
printf 'valise: cannot restore the previous-release record\n' >&2
|
|
177
|
+
elif [ -n "$_ita_rm" ]; then
|
|
178
|
+
"$_ita_rm" -f "$(previous_record_path)" 2>/dev/null || :
|
|
179
|
+
fi
|
|
180
|
+
# The subshells remove their own staging files: a die inside them exits
|
|
181
|
+
# the subshell, and this process's lock handler never saw those names.
|
|
182
|
+
if [ -f "$(commands_ledger_path)" ]; then
|
|
183
|
+
if [ -n "$_txn_old_target" ] && [ -d "$_txn_old_target" ]; then
|
|
184
|
+
(
|
|
185
|
+
trap 'for _t in "${_rc_tmp:-}" "${_rc_stub:-}" "${_rc_pub:-}"; do [ -z "$_t" ] || [ -z "$_ita_rm" ] || "$_ita_rm" -f "$_t"; done' EXIT
|
|
186
|
+
reconcile_commands "$_txn_old_target"
|
|
187
|
+
) >/dev/null 2>&1 ||
|
|
188
|
+
printf 'valise: cannot move the command stubs back to the restored release\n' >&2
|
|
189
|
+
elif [ -z "$_txn_old_target" ]; then
|
|
190
|
+
# No release was active before, so no stub may point at one now.
|
|
191
|
+
(remove_owned_commands) >/dev/null 2>&1 ||
|
|
192
|
+
printf 'valise: cannot remove the command stubs this install published\n' >&2
|
|
193
|
+
fi
|
|
194
|
+
fi
|
|
195
|
+
if [ -n "$_txn_changed" ]; then
|
|
196
|
+
printf 'valise: already updated (they load whichever release is active):%s\n' "$_txn_changed" >&2
|
|
197
|
+
fi
|
|
198
|
+
unset _ita_ln _ita_rm
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
# The line a managed block carries when install appended it to a file whose
|
|
202
|
+
# last line had no newline.
|
|
203
|
+
VALISE_NONL_NOTE='# valise: the line before this block had no trailing newline'
|
|
204
|
+
|
|
205
|
+
# --- Integration checks ---
|
|
206
|
+
|
|
207
|
+
# preflight_dir_writable <dir> <what>: fail unless <dir>, or the nearest
|
|
208
|
+
# ancestor that exists when <dir> does not, is a writable directory. Install
|
|
209
|
+
# creates missing directories and stages files beside their targets.
|
|
210
|
+
preflight_dir_writable() {
|
|
211
|
+
_pdw_dir=$1
|
|
212
|
+
while [ ! -e "$_pdw_dir" ] && [ ! -L "$_pdw_dir" ]; do
|
|
213
|
+
_pdw_dir=$(path_parent "$_pdw_dir")
|
|
214
|
+
done
|
|
215
|
+
[ -d "$_pdw_dir" ] || die "$2 directory is not a directory: $_pdw_dir"
|
|
216
|
+
[ -w "$_pdw_dir" ] || die "cannot write the $2 in $_pdw_dir"
|
|
217
|
+
unset _pdw_dir
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
# integration_preflight <release-dir>: fail before anything changes when
|
|
221
|
+
# install could not write the integration or command stubs it is about to
|
|
222
|
+
# write.
|
|
223
|
+
integration_preflight() {
|
|
224
|
+
if [ -n "${HOME:-}" ]; then
|
|
225
|
+
_ipf_home=$(path_normalize "$HOME") || die "cannot resolve HOME"
|
|
226
|
+
_ipf_targets=$(posix_startup_targets "$_ipf_home")
|
|
227
|
+
while IFS= read -r _ipf_target; do
|
|
228
|
+
[ -n "$_ipf_target" ] || continue
|
|
229
|
+
# A symlinked startup file is left for the user; install only warns.
|
|
230
|
+
[ ! -L "$_ipf_target" ] || continue
|
|
231
|
+
if [ -e "$_ipf_target" ]; then
|
|
232
|
+
[ -f "$_ipf_target" ] || die "startup path is not a regular file: $_ipf_target"
|
|
233
|
+
[ -r "$_ipf_target" ] || die "cannot read startup file: $_ipf_target"
|
|
234
|
+
integration_markers_paired "$_ipf_target" ||
|
|
235
|
+
die "unpaired valise managed block markers in $_ipf_target (restore or remove the BEGIN and END lines, then retry)"
|
|
236
|
+
fi
|
|
237
|
+
_ipf_parent=$(path_parent "$_ipf_target")
|
|
238
|
+
[ -d "$_ipf_parent" ] && [ -w "$_ipf_parent" ] ||
|
|
239
|
+
die "cannot write startup files in $_ipf_parent"
|
|
240
|
+
done <<EOF
|
|
241
|
+
$_ipf_targets
|
|
242
|
+
EOF
|
|
243
|
+
fi
|
|
244
|
+
if [ -n "$(trusted_utility_path fish 2>/dev/null || :)" ]; then
|
|
245
|
+
_ipf_hook=$(fish_hook_path)
|
|
246
|
+
[ ! -L "$_ipf_hook" ] || die "Fish hook path must not be a symlink: $_ipf_hook"
|
|
247
|
+
[ ! -d "$_ipf_hook" ] || die "Fish hook path is a directory: $_ipf_hook"
|
|
248
|
+
if [ -e "$_ipf_hook" ] && [ ! -f "$_ipf_hook" ]; then
|
|
249
|
+
die "Fish hook path is not a regular file: $_ipf_hook"
|
|
250
|
+
fi
|
|
251
|
+
preflight_dir_writable "$(path_parent "$_ipf_hook")" "Fish hook"
|
|
252
|
+
fi
|
|
253
|
+
# Stubs are written when the release publishes commands and removed when a
|
|
254
|
+
# ledger lists stubs this release no longer provides.
|
|
255
|
+
if [ -n "$(release_commands "$1")" ] || [ -f "$(commands_ledger_path)" ]; then
|
|
256
|
+
_ipf_bin=$(commands_bin_dir) || die "cannot resolve the command directory: set HOME or VALISE_BIN_DIR"
|
|
257
|
+
preflight_dir_writable "$_ipf_bin" "command stubs"
|
|
258
|
+
fi
|
|
259
|
+
unset _ipf_home _ipf_targets _ipf_target _ipf_parent _ipf_hook _ipf_bin
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
# integration_unmanaged_lines <file>
|
|
263
|
+
# Print file:line: text for each line outside a managed block that loads a
|
|
264
|
+
# valise release by hand: a reference to the active link, a 0.1 generated
|
|
265
|
+
# module list, or a line that sources a compiled plan. Comments are skipped.
|
|
266
|
+
integration_unmanaged_lines() {
|
|
267
|
+
[ -f "$1" ] && [ -r "$1" ] || return 0
|
|
268
|
+
_iul_awk=$(trusted_utility_path awk) || return 1
|
|
269
|
+
"$_iul_awk" '
|
|
270
|
+
/^# BEGIN valise managed block$/ { inside = 1; next }
|
|
271
|
+
/^# END valise managed block$/ { inside = 0; next }
|
|
272
|
+
inside { next }
|
|
273
|
+
/^[[:space:]]*#/ { next }
|
|
274
|
+
index($0, "valise/active") || index($0, "installed-modules.") ||
|
|
275
|
+
($0 ~ /(^|[;&|[:space:]])(\.|source)[[:space:]]/ && $0 ~ /plans\/(sh|bash|zsh|ksh)\.sh/) {
|
|
276
|
+
printf "%s:%d: %s\n", FILENAME, NR, $0
|
|
277
|
+
}
|
|
278
|
+
' "$1"
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
# integration_unmanaged_report: print every unmanaged line in the startup
|
|
282
|
+
# files valise knows about; succeed when there are none.
|
|
283
|
+
integration_unmanaged_report() {
|
|
284
|
+
[ -n "${HOME:-}" ] || return 0
|
|
285
|
+
_iur_home=$(path_normalize "$HOME") || return 0
|
|
286
|
+
_iur_found=0
|
|
287
|
+
for _iur_name in $VALISE_STARTUP_NAMES; do
|
|
288
|
+
_iur_lines=$(integration_unmanaged_lines "$_iur_home/$_iur_name") || _iur_lines=
|
|
289
|
+
[ -n "$_iur_lines" ] || continue
|
|
290
|
+
printf '%s\n' "$_iur_lines"
|
|
291
|
+
_iur_found=1
|
|
292
|
+
done
|
|
293
|
+
_iur_rc=$_iur_found
|
|
294
|
+
unset _iur_home _iur_found _iur_name _iur_lines
|
|
295
|
+
[ "$_iur_rc" -eq 0 ]
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
# startup_created_add <path>: remember that install created this file.
|
|
299
|
+
startup_created_add() {
|
|
300
|
+
_sca_ledger=$(startup_created_path)
|
|
301
|
+
[ ! -L "$_sca_ledger" ] || die "startup ledger must not be a symlink: $_sca_ledger"
|
|
302
|
+
if [ -f "$_sca_ledger" ]; then
|
|
303
|
+
while IFS= read -r _sca_line; do
|
|
304
|
+
if [ "$_sca_line" = "$1" ]; then
|
|
305
|
+
unset _sca_ledger _sca_line
|
|
306
|
+
return 0
|
|
307
|
+
fi
|
|
308
|
+
done <"$_sca_ledger"
|
|
309
|
+
fi
|
|
310
|
+
printf '%s\n' "$1" >>"$_sca_ledger" || die "cannot record created startup file"
|
|
311
|
+
unset _sca_ledger _sca_line
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
startup_created_has() {
|
|
315
|
+
_sch_ledger=$(startup_created_path)
|
|
316
|
+
[ -f "$_sch_ledger" ] && [ ! -L "$_sch_ledger" ] || return 1
|
|
317
|
+
while IFS= read -r _sch_line; do
|
|
318
|
+
[ "$_sch_line" != "$1" ] || { unset _sch_ledger _sch_line; return 0; }
|
|
319
|
+
done <"$_sch_ledger"
|
|
320
|
+
unset _sch_ledger _sch_line
|
|
321
|
+
return 1
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
# --- Uninstall ---
|
|
325
|
+
|
|
326
|
+
# integration_has_posix_block <file>: the file holds a managed block marker.
|
|
327
|
+
integration_has_posix_block() {
|
|
328
|
+
_ihpb_awk=$(trusted_utility_path awk) || return 1
|
|
329
|
+
"$_ihpb_awk" '/^# BEGIN valise managed block$/ { found = 1; exit } END { exit !found }' "$1" 2>/dev/null
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
# uninstall_strip_block <file>: remove the managed block from one startup file,
|
|
333
|
+
# keeping every other byte and the file mode. A file install created that is
|
|
334
|
+
# left empty is deleted.
|
|
335
|
+
uninstall_strip_block() {
|
|
336
|
+
_usb_target=$1
|
|
337
|
+
_usb_awk=$(trusted_utility_path awk) || die "no trusted awk utility available"
|
|
338
|
+
_usb_parent=$(path_parent "$_usb_target")
|
|
339
|
+
_un_tmp=$(trusted_mktemp_at "$_usb_parent" file "$_usb_parent/.valise-uninstall.XXXXXX") ||
|
|
340
|
+
die "cannot create uninstall staging file in $_usb_parent"
|
|
341
|
+
# A block carrying the no-newline note was appended to a last line that
|
|
342
|
+
# had no newline; when nothing follows the block, that newline goes too.
|
|
343
|
+
# Text after the block keeps the file's own ending (nonl: no final newline).
|
|
344
|
+
_usb_tail=$(trusted_utility_path tail) || die "no trusted tail utility available"
|
|
345
|
+
_usb_nonl=0
|
|
346
|
+
[ ! -s "$_usb_target" ] || [ -z "$("$_usb_tail" -c 1 "$_usb_target")" ] || _usb_nonl=1
|
|
347
|
+
_VALISE_NOTE=$VALISE_NONL_NOTE "$_usb_awk" -v nonl="$_usb_nonl" '
|
|
348
|
+
/^# BEGIN valise managed block$/ { inblock = 1; next }
|
|
349
|
+
inblock && $0 == ENVIRON["_VALISE_NOTE"] { had_note = 1; next }
|
|
350
|
+
inblock && /^# END valise managed block$/ { inblock = 0; closed = 1; next }
|
|
351
|
+
inblock { next }
|
|
352
|
+
{ if (closed) after = 1; line[++n] = $0 }
|
|
353
|
+
END {
|
|
354
|
+
for (i = 1; i <= n; i++) {
|
|
355
|
+
if (i == n && ((had_note && !after) || (nonl && after))) printf "%s", line[i]
|
|
356
|
+
else print line[i]
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
' "$_usb_target" >"$_un_tmp" || die "cannot read startup file: $_usb_target"
|
|
360
|
+
_usb_rm=$(trusted_utility_path rm) || die "no trusted rm utility available"
|
|
361
|
+
if [ ! -s "$_un_tmp" ] && startup_created_has "$_usb_target"; then
|
|
362
|
+
"$_usb_rm" -f "$_usb_target" || die "cannot remove startup file: $_usb_target"
|
|
363
|
+
"$_usb_rm" -f "$_un_tmp" || die "cannot remove uninstall staging file"
|
|
364
|
+
_un_tmp=""
|
|
365
|
+
info "removed startup file valise created: $_usb_target"
|
|
366
|
+
else
|
|
367
|
+
_usb_mode=$(file_mode "$_usb_target")
|
|
368
|
+
[ -n "$_usb_mode" ] || die "cannot read startup file mode: $_usb_target"
|
|
369
|
+
_usb_chmod=$(trusted_utility_path chmod) || die "no trusted chmod utility available"
|
|
370
|
+
"$_usb_chmod" "$_usb_mode" "$_un_tmp" || die "cannot set startup file mode"
|
|
371
|
+
_usb_mv=$(trusted_utility_path mv) || die "no trusted mv utility available"
|
|
372
|
+
"$_usb_mv" -f "$_un_tmp" "$_usb_target" || die "cannot rewrite startup file: $_usb_target"
|
|
373
|
+
_un_tmp=""
|
|
374
|
+
info "removed the managed block from $_usb_target"
|
|
375
|
+
fi
|
|
376
|
+
unset _usb_target _usb_awk _usb_tail _usb_nonl _usb_parent _usb_rm _usb_mode _usb_chmod _usb_mv
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
cmd_uninstall() {
|
|
380
|
+
_un_keep=0
|
|
381
|
+
for _un_arg do
|
|
382
|
+
case "$_un_arg" in
|
|
383
|
+
--keep-releases) _un_keep=1 ;;
|
|
384
|
+
--help | -h) usage_uninstall; return 0 ;;
|
|
385
|
+
*) die "unknown uninstall option: $_un_arg (usage: valise uninstall [--keep-releases])" ;;
|
|
386
|
+
esac
|
|
387
|
+
done
|
|
388
|
+
_un_rm=$(trusted_utility_path rm) || die "no trusted rm utility available"
|
|
389
|
+
_un_rmdir=$(trusted_utility_path rmdir) || die "no trusted rmdir utility available"
|
|
390
|
+
_un_mkdir=$(trusted_utility_path mkdir) || die "no trusted mkdir utility available"
|
|
391
|
+
"$_un_mkdir" -p "$VALISE_XDG_STATE" || die "cannot create state directory: $VALISE_XDG_STATE"
|
|
392
|
+
acquire_lock
|
|
393
|
+
|
|
394
|
+
_un_home=
|
|
395
|
+
if [ -n "${HOME:-}" ]; then
|
|
396
|
+
_un_home=$(path_normalize "$HOME") || die "cannot resolve HOME"
|
|
397
|
+
# Check every file before changing any, so a damaged block stops the
|
|
398
|
+
# uninstall with nothing removed.
|
|
399
|
+
for _un_name in $VALISE_STARTUP_NAMES; do
|
|
400
|
+
_un_file="$_un_home/$_un_name"
|
|
401
|
+
[ -f "$_un_file" ] && [ ! -L "$_un_file" ] || continue
|
|
402
|
+
integration_markers_paired "$_un_file" ||
|
|
403
|
+
die "unpaired valise managed block markers in $_un_file (restore or remove the BEGIN and END lines, then retry)"
|
|
404
|
+
done
|
|
405
|
+
for _un_name in $VALISE_STARTUP_NAMES; do
|
|
406
|
+
_un_file="$_un_home/$_un_name"
|
|
407
|
+
if [ -L "$_un_file" ]; then
|
|
408
|
+
if [ -f "$_un_file" ] && integration_has_posix_block "$_un_file"; then
|
|
409
|
+
info "warning: $_un_file is a symlink; remove the valise managed block from it yourself"
|
|
410
|
+
fi
|
|
411
|
+
continue
|
|
412
|
+
fi
|
|
413
|
+
[ -f "$_un_file" ] || continue
|
|
414
|
+
integration_has_posix_block "$_un_file" || continue
|
|
415
|
+
uninstall_strip_block "$_un_file"
|
|
416
|
+
done
|
|
417
|
+
_un_hook=$(fish_hook_path)
|
|
418
|
+
if [ -f "$_un_hook" ] && [ ! -L "$_un_hook" ]; then
|
|
419
|
+
IFS= read -r _un_first <"$_un_hook" || :
|
|
420
|
+
if [ "${_un_first:-}" = '# valise Fish integration. Do not edit.' ]; then
|
|
421
|
+
"$_un_rm" -f "$_un_hook" || die "cannot remove the Fish hook: $_un_hook"
|
|
422
|
+
info "removed the Fish hook: $_un_hook"
|
|
423
|
+
else
|
|
424
|
+
info "warning: leaving $_un_hook: it is not the valise Fish hook"
|
|
425
|
+
fi
|
|
426
|
+
elif [ -e "$_un_hook" ] || [ -L "$_un_hook" ]; then
|
|
427
|
+
info "warning: leaving $_un_hook: it is not a regular file"
|
|
428
|
+
fi
|
|
429
|
+
_un_lines=$(integration_unmanaged_report) || {
|
|
430
|
+
info "warning: these lines load valise outside a managed block; remove them yourself:"
|
|
431
|
+
printf '%s\n' "$_un_lines"
|
|
432
|
+
}
|
|
433
|
+
else
|
|
434
|
+
info "warning: HOME is not set; startup files were not checked"
|
|
435
|
+
fi
|
|
436
|
+
|
|
437
|
+
remove_owned_commands
|
|
438
|
+
|
|
439
|
+
if [ -L "$VALISE_ACTIVE_LINK" ]; then
|
|
440
|
+
"$_un_rm" -f "$VALISE_ACTIVE_LINK" || die "cannot remove the active link: $VALISE_ACTIVE_LINK"
|
|
441
|
+
info "removed the active release link"
|
|
442
|
+
elif [ -e "$VALISE_ACTIVE_LINK" ]; then
|
|
443
|
+
info "warning: leaving $VALISE_ACTIVE_LINK: it is not a symlink"
|
|
444
|
+
fi
|
|
445
|
+
for _un_record in "$(previous_record_path)" "$(startup_created_path)"; do
|
|
446
|
+
if [ -f "$_un_record" ] || [ -L "$_un_record" ]; then
|
|
447
|
+
"$_un_rm" -f "$_un_record" || die "cannot remove $_un_record"
|
|
448
|
+
fi
|
|
449
|
+
done
|
|
450
|
+
if [ "$_un_keep" -eq 0 ]; then
|
|
451
|
+
if [ -d "$VALISE_RELEASES_DIR" ] && [ ! -L "$VALISE_RELEASES_DIR" ]; then
|
|
452
|
+
remove_tree "$VALISE_RELEASES_DIR" || die "cannot remove installed releases: $VALISE_RELEASES_DIR"
|
|
453
|
+
info "removed installed releases"
|
|
454
|
+
fi
|
|
455
|
+
"$_un_rmdir" "$VALISE_XDG_DATA" 2>/dev/null || :
|
|
456
|
+
else
|
|
457
|
+
info "kept installed releases in $VALISE_RELEASES_DIR"
|
|
458
|
+
fi
|
|
459
|
+
"$_un_rmdir" "$VALISE_XDG_CONFIG" 2>/dev/null || :
|
|
460
|
+
release_lock
|
|
461
|
+
"$_un_rmdir" "$VALISE_XDG_STATE" 2>/dev/null || :
|
|
462
|
+
info "valise integration removed; the valise CLI itself is still installed"
|
|
463
|
+
unset _un_keep _un_arg _un_rm _un_rmdir _un_mkdir _un_home _un_name _un_file _un_hook _un_first \
|
|
464
|
+
_un_lines _un_record
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
# --- Install preview ---
|
|
468
|
+
|
|
469
|
+
# _dry_render_compare <target> <render-command...>
|
|
470
|
+
# Print how install would treat one file: current, update, or create.
|
|
471
|
+
_dry_render_compare() {
|
|
472
|
+
_drc_target=$1
|
|
473
|
+
shift
|
|
474
|
+
_dry_tmp=$(trusted_mktemp file "${TMPDIR:-/tmp}/valise-dry-run.XXXXXX") || die "cannot create preview file"
|
|
475
|
+
"$@" >"$_dry_tmp" || die "cannot render $_drc_target"
|
|
476
|
+
_drc_cmp=$(trusted_utility_path cmp) || die "no trusted cmp utility available"
|
|
477
|
+
if [ -f "$_drc_target" ] && "$_drc_cmp" -s "$_dry_tmp" "$_drc_target"; then
|
|
478
|
+
printf ' current: %s\n' "$_drc_target"
|
|
479
|
+
elif [ -e "$_drc_target" ]; then
|
|
480
|
+
printf ' would update: %s\n' "$_drc_target"
|
|
481
|
+
else
|
|
482
|
+
printf ' would create: %s\n' "$_drc_target"
|
|
483
|
+
fi
|
|
484
|
+
_drc_rm=$(trusted_utility_path rm) || die "no trusted rm utility available"
|
|
485
|
+
"$_drc_rm" -f "$_dry_tmp" || die "cannot remove preview file"
|
|
486
|
+
_dry_tmp=""
|
|
487
|
+
unset _drc_target _drc_cmp _drc_rm
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
# install_preview <integration>
|
|
491
|
+
# Describe what valise install would do with the release tree in the current
|
|
492
|
+
# directory, writing nothing outside the temporary directory.
|
|
493
|
+
install_preview() {
|
|
494
|
+
_ip_integration=$1
|
|
495
|
+
_ip_source=$(locate_manifest) || die "cannot find manifest.tsv in the current directory"
|
|
496
|
+
_ip_root=$(cd "$(path_parent "$_ip_source")" && pwd -P) || die "cannot resolve manifest source root"
|
|
497
|
+
prepare_manifest_rows "$_ip_source" || die "cannot normalize manifest.tsv"
|
|
498
|
+
validate_manifest "$_manifest_rows" || die "invalid manifest.tsv schema or row"
|
|
499
|
+
_ip_id=$(file_sha256 "$_manifest_rows") || die "no trusted SHA-256 utility available"
|
|
500
|
+
printf 'release %s\n' "$_ip_id"
|
|
501
|
+
if [ -d "$VALISE_RELEASES_DIR/$_ip_id" ]; then
|
|
502
|
+
printf ' already installed\n'
|
|
503
|
+
else
|
|
504
|
+
printf ' would install to %s\n' "$VALISE_RELEASES_DIR/$_ip_id"
|
|
505
|
+
fi
|
|
506
|
+
_ip_active=$(active_release_id 2>/dev/null || :)
|
|
507
|
+
if [ "$_ip_active" = "$_ip_id" ]; then
|
|
508
|
+
printf ' already active\n'
|
|
509
|
+
else
|
|
510
|
+
printf ' would become active (now: %s)\n' "${_ip_active:-none}"
|
|
511
|
+
fi
|
|
512
|
+
if [ "$_ip_integration" -eq 0 ]; then
|
|
513
|
+
printf 'integration: skipped (--no-integration)\n'
|
|
514
|
+
unset _ip_integration _ip_source _ip_root _ip_id _ip_active
|
|
515
|
+
return 0
|
|
516
|
+
fi
|
|
517
|
+
printf 'startup files\n'
|
|
518
|
+
if [ -n "${HOME:-}" ]; then
|
|
519
|
+
_ip_home=$(path_normalize "$HOME") || die "cannot resolve HOME"
|
|
520
|
+
_ip_snippet=$(print_posix_integration) || die "cannot generate integration block"
|
|
521
|
+
_ip_targets=$(posix_startup_targets "$_ip_home")
|
|
522
|
+
while IFS= read -r _ip_target; do
|
|
523
|
+
[ -n "$_ip_target" ] || continue
|
|
524
|
+
if [ -L "$_ip_target" ]; then
|
|
525
|
+
printf ' left alone (symlink): %s\n' "$_ip_target"
|
|
526
|
+
elif [ -e "$_ip_target" ] && ! integration_markers_paired "$_ip_target"; then
|
|
527
|
+
printf ' would fail (unpaired markers): %s\n' "$_ip_target"
|
|
528
|
+
else
|
|
529
|
+
_dry_render_compare "$_ip_target" integration_render "$_ip_target" "$_ip_snippet"
|
|
530
|
+
fi
|
|
531
|
+
done <<EOF
|
|
532
|
+
$_ip_targets
|
|
533
|
+
EOF
|
|
534
|
+
_ip_lines=$(integration_unmanaged_report) ||
|
|
535
|
+
printf ' warning: valise loaded outside a managed block:\n%s\n' "$_ip_lines"
|
|
536
|
+
else
|
|
537
|
+
printf ' none (HOME is not set)\n'
|
|
538
|
+
fi
|
|
539
|
+
if [ -n "$(trusted_utility_path fish 2>/dev/null || :)" ]; then
|
|
540
|
+
_dry_render_compare "$(fish_hook_path)" print_fish_integration
|
|
541
|
+
fi
|
|
542
|
+
printf 'commands\n'
|
|
543
|
+
_ip_any=0
|
|
544
|
+
_ip_want=$(release_commands "$_ip_root") || die "the release lists an invalid command name"
|
|
545
|
+
if _ip_bin=$(commands_bin_dir); then
|
|
546
|
+
_ip_ledger=$(commands_ledger_path)
|
|
547
|
+
_ip_awk=$(trusted_utility_path awk) || die "no trusted awk utility available"
|
|
548
|
+
if [ -f "$_ip_ledger" ]; then
|
|
549
|
+
while IFS="$(printf '\t')" read -r _ip_name _ip_path _ip_sha; do
|
|
550
|
+
[ -n "$_ip_name" ] || continue
|
|
551
|
+
case " $_ip_want " in *" $_ip_name "*) [ "$_ip_path" != "$_ip_bin/$_ip_name" ] || continue ;; esac
|
|
552
|
+
printf ' would remove: %s\n' "$_ip_path"
|
|
553
|
+
_ip_any=1
|
|
554
|
+
done <"$_ip_ledger"
|
|
555
|
+
fi
|
|
556
|
+
for _ip_name in $_ip_want; do
|
|
557
|
+
_ip_path="$_ip_bin/$_ip_name"
|
|
558
|
+
_ip_sha=
|
|
559
|
+
[ ! -f "$_ip_ledger" ] ||
|
|
560
|
+
_ip_sha=$("$_ip_awk" -F '\t' -v n="$_ip_name" -v p="$_ip_path" '$1 == n && $2 == p { print $3; exit }' "$_ip_ledger")
|
|
561
|
+
if [ -n "$_ip_sha" ] && commands_file_is "$_ip_path" "$_ip_sha"; then
|
|
562
|
+
printf ' current: %s\n' "$_ip_path"
|
|
563
|
+
_ip_any=1
|
|
564
|
+
elif [ -e "$_ip_path" ] || [ -L "$_ip_path" ]; then
|
|
565
|
+
printf ' would skip (exists, not owned by valise): %s\n' "$_ip_path"
|
|
566
|
+
_ip_any=1
|
|
567
|
+
else
|
|
568
|
+
printf ' would publish: %s\n' "$_ip_path"
|
|
569
|
+
_ip_any=1
|
|
570
|
+
fi
|
|
571
|
+
done
|
|
572
|
+
else
|
|
573
|
+
[ -z "$_ip_want" ] || { printf ' none (HOME is not set)\n'; _ip_any=1; }
|
|
574
|
+
fi
|
|
575
|
+
[ "$_ip_any" -eq 1 ] || printf ' none\n'
|
|
576
|
+
unset _ip_integration _ip_source _ip_root _ip_id _ip_active _ip_home _ip_snippet _ip_targets _ip_target \
|
|
577
|
+
_ip_lines _ip_want _ip_bin _ip_ledger _ip_awk _ip_name _ip_path _ip_sha _ip_any
|
|
578
|
+
}
|