@delorenj/pjangler 1.1.4 → 1.1.5

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.
@@ -0,0 +1,236 @@
1
+ #!/usr/bin/env bash
2
+ # versioning.sh — single source of all repo versioning logic.
3
+ #
4
+ # Installed by the `mise-versioning` skill. All version read/write/bump logic
5
+ # lives here; mise tasks are thin wrappers that call into it.
6
+ #
7
+ # Canonical version = the HIGHEST semver across every file in the manifest.
8
+ # All manifest files are kept in parity on every write (bump / set / sync).
9
+ #
10
+ # Usage:
11
+ # versioning.sh current Print canonical version as vX.Y.Z
12
+ # versioning.sh bump <patch|minor|major>
13
+ # versioning.sh set <X.Y.Z|vX.Y.Z> Force all files to an explicit version
14
+ # versioning.sh check Exit 0 if all files in parity, else list drift
15
+ # versioning.sh sync Force every file up to the canonical (highest) version
16
+ # versioning.sh files List the manifest
17
+ #
18
+ # Manifest: .mise/version-files.conf — lines of "<type> <path>" (see file-types).
19
+ # types: json toml cargo csproj gradle plain gittag
20
+ #
21
+ # Storage format per type:
22
+ # json/toml/cargo/csproj/gradle/plain -> bare "X.Y.Z"
23
+ # gittag -> tag "vX.Y.Z"
24
+ # `current` always prints with the leading "v".
25
+
26
+ set -euo pipefail
27
+
28
+ REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
29
+ MANIFEST="${VERSION_FILES_CONF:-$REPO_ROOT/.mise/version-files.conf}"
30
+
31
+ die() { printf 'versioning: %s\n' "$1" >&2; exit 1; }
32
+
33
+ # Friendly display path for a manifest entry.
34
+ rel() { # $1=type $2=abspath
35
+ [[ "$1" == gittag ]] && { printf '(git tags)'; return; }
36
+ local p="${2#"$REPO_ROOT"/}"; printf '%s' "${p:-$2}"
37
+ }
38
+
39
+ # ---- semver helpers ---------------------------------------------------------
40
+
41
+ # Strip a leading v/V and surrounding whitespace; validate X.Y.Z.
42
+ normalize() {
43
+ local v="${1#[vV]}"
44
+ v="$(printf '%s' "$v" | tr -d '[:space:]')"
45
+ [[ "$v" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || return 1
46
+ printf '%s' "$v"
47
+ }
48
+
49
+ # Return 0 if $1 > $2 (strict), comparing X.Y.Z numerically.
50
+ semver_gt() {
51
+ local a b
52
+ IFS=. read -r a1 a2 a3 <<<"$1"
53
+ IFS=. read -r b1 b2 b3 <<<"$2"
54
+ ((a1 != b1)) && { ((a1 > b1)); return; }
55
+ ((a2 != b2)) && { ((a2 > b2)); return; }
56
+ ((a3 > b3))
57
+ }
58
+
59
+ bump_semver() {
60
+ local ver="$1" part="$2" x y z
61
+ IFS=. read -r x y z <<<"$ver"
62
+ case "$part" in
63
+ major) x=$((x + 1)); y=0; z=0 ;;
64
+ minor) y=$((y + 1)); z=0 ;;
65
+ patch) z=$((z + 1)) ;;
66
+ *) die "unknown bump part: $part (expected patch|minor|major)" ;;
67
+ esac
68
+ printf '%s.%s.%s' "$x" "$y" "$z"
69
+ }
70
+
71
+ # ---- manifest ---------------------------------------------------------------
72
+
73
+ [[ -f "$MANIFEST" ]] || die "manifest not found: $MANIFEST (run the mise-versioning init)"
74
+
75
+ # Emits "type<TAB>abspath" per manifest line, skipping blanks/comments.
76
+ manifest_entries() {
77
+ while read -r type path _rest; do
78
+ [[ -z "${type:-}" || "$type" == \#* ]] && continue
79
+ case "$path" in
80
+ /*) : ;;
81
+ .|"") path="$REPO_ROOT" ;;
82
+ *) path="$REPO_ROOT/$path" ;;
83
+ esac
84
+ printf '%s\t%s\n' "$type" "$path"
85
+ done <"$MANIFEST"
86
+ }
87
+
88
+ # ---- per-type read -----------------------------------------------------------
89
+
90
+ read_version() { # $1=type $2=path -> bare X.Y.Z on stdout, or nothing
91
+ local type="$1" path="$2" raw=""
92
+ case "$type" in
93
+ json)
94
+ [[ -f "$path" ]] || return 0
95
+ raw="$(jq -r '.version // empty' "$path" 2>/dev/null || true)" ;;
96
+ toml|cargo)
97
+ [[ -f "$path" ]] || return 0
98
+ raw="$(grep -m1 -E '^version[[:space:]]*=' "$path" 2>/dev/null \
99
+ | sed -E 's/^version[[:space:]]*=[[:space:]]*["'\'']?([^"'\'' ]+).*/\1/' || true)" ;;
100
+ csproj)
101
+ [[ -f "$path" ]] || return 0
102
+ raw="$(grep -m1 -oE '<Version>[^<]+</Version>' "$path" 2>/dev/null \
103
+ | sed -E 's#</?Version>##g' || true)" ;;
104
+ gradle)
105
+ [[ -f "$path" ]] || return 0
106
+ raw="$(grep -m1 -E '^[[:space:]]*version[[:space:]]*[=]?[[:space:]]*["'\'']' "$path" 2>/dev/null \
107
+ | sed -E 's/.*["'\'']([0-9]+\.[0-9]+\.[0-9]+)["'\''].*/\1/' || true)" ;;
108
+ plain)
109
+ [[ -f "$path" ]] || return 0
110
+ raw="$(head -n1 "$path" 2>/dev/null || true)" ;;
111
+ gittag)
112
+ raw="$(git -C "$REPO_ROOT" tag --list 'v[0-9]*' --sort=-v:refname 2>/dev/null | head -n1 || true)" ;;
113
+ *) die "unknown manifest type: $type" ;;
114
+ esac
115
+ [[ -z "$raw" ]] && return 0
116
+ normalize "$raw" 2>/dev/null || return 0
117
+ }
118
+
119
+ # ---- per-type write ----------------------------------------------------------
120
+
121
+ write_version() { # $1=type $2=path $3=bare-new-version
122
+ local type="$1" path="$2" new="$3" tmp
123
+ case "$type" in
124
+ json)
125
+ [[ -f "$path" ]] || return 0
126
+ tmp="$(mktemp)"
127
+ jq --indent 2 --arg v "$new" '.version = $v' "$path" >"$tmp" && mv "$tmp" "$path" ;;
128
+ toml|cargo)
129
+ [[ -f "$path" ]] || return 0
130
+ # Replace only the first top-level `version = "..."` line.
131
+ sed -i -E "0,/^version[[:space:]]*=/{s/^(version[[:space:]]*=[[:space:]]*[\"']?)[^\"' ]+([\"']?)/\1$new\2/}" "$path" ;;
132
+ csproj)
133
+ [[ -f "$path" ]] || return 0
134
+ sed -i -E "0,/<Version>[^<]+<\/Version>/{s#<Version>[^<]+</Version>#<Version>$new</Version>#}" "$path" ;;
135
+ gradle)
136
+ [[ -f "$path" ]] || return 0
137
+ sed -i -E "0,/^[[:space:]]*version[[:space:]]*[=]?[[:space:]]*[\"']/{s/([\"'])[0-9]+\.[0-9]+\.[0-9]+([\"'])/\1$new\2/}" "$path" ;;
138
+ plain)
139
+ printf '%s\n' "$new" >"$path" ;;
140
+ gittag)
141
+ if git -C "$REPO_ROOT" rev-parse "v$new" >/dev/null 2>&1; then
142
+ printf 'versioning: git tag v%s already exists, skipping\n' "$new" >&2
143
+ else
144
+ git -C "$REPO_ROOT" tag -a "v$new" -m "v$new"
145
+ printf 'versioning: created git tag v%s\n' "$new" >&2
146
+ fi ;;
147
+ *) die "unknown manifest type: $type" ;;
148
+ esac
149
+ }
150
+
151
+ # ---- canonical resolution ----------------------------------------------------
152
+
153
+ canonical() { # highest version across the manifest -> bare X.Y.Z (empty if none)
154
+ local best="" v
155
+ while IFS=$'\t' read -r type path; do
156
+ v="$(read_version "$type" "$path")" || true
157
+ [[ -z "$v" ]] && continue
158
+ if [[ -z "$best" ]] || semver_gt "$v" "$best"; then best="$v"; fi
159
+ done < <(manifest_entries)
160
+ printf '%s' "$best"
161
+ }
162
+
163
+ write_all() { # $1=bare-new-version : write to every manifest file
164
+ local new="$1" type path
165
+ while IFS=$'\t' read -r type path; do
166
+ write_version "$type" "$path" "$new"
167
+ printf ' %-7s %s -> %s\n' "$type" "$(rel "$type" "$path")" "$new" >&2
168
+ done < <(manifest_entries)
169
+ }
170
+
171
+ # ---- commands ----------------------------------------------------------------
172
+
173
+ cmd_current() {
174
+ local v; v="$(canonical)"
175
+ printf 'v%s\n' "${v:-0.0.0}"
176
+ }
177
+
178
+ cmd_bump() {
179
+ local part="${1:-patch}" cur new
180
+ cur="$(canonical)"; [[ -z "$cur" ]] && cur="0.0.0"
181
+ new="$(bump_semver "$cur" "$part")"
182
+ printf 'versioning: %s bump v%s -> v%s\n' "$part" "$cur" "$new" >&2
183
+ write_all "$new"
184
+ printf 'v%s\n' "$new"
185
+ }
186
+
187
+ cmd_set() {
188
+ local new; new="$(normalize "${1:?usage: set <X.Y.Z>}")" || die "invalid version: $1"
189
+ printf 'versioning: set -> v%s\n' "$new" >&2
190
+ write_all "$new"
191
+ printf 'v%s\n' "$new"
192
+ }
193
+
194
+ cmd_check() {
195
+ local canon drift=0 v type path; canon="$(canonical)"
196
+ [[ -z "$canon" ]] && { echo "versioning: no version found in any manifest file" >&2; return 0; }
197
+ while IFS=$'\t' read -r type path; do
198
+ v="$(read_version "$type" "$path")" || true
199
+ [[ -z "$v" ]] && continue
200
+ if [[ "$v" != "$canon" ]]; then
201
+ printf 'DRIFT %-7s %s = v%s (canonical v%s)\n' "$type" "$(rel "$type" "$path")" "$v" "$canon" >&2
202
+ drift=1
203
+ fi
204
+ done < <(manifest_entries)
205
+ if ((drift)); then echo "versioning: files out of parity; run 'versioning.sh sync'" >&2; return 1; fi
206
+ printf 'versioning: all files in parity at v%s\n' "$canon"
207
+ }
208
+
209
+ cmd_sync() {
210
+ local canon; canon="$(canonical)"
211
+ [[ -z "$canon" ]] && die "no version found in any manifest file"
212
+ printf 'versioning: syncing all files to v%s\n' "$canon" >&2
213
+ write_all "$canon"
214
+ printf 'v%s\n' "$canon"
215
+ }
216
+
217
+ cmd_files() {
218
+ while IFS=$'\t' read -r type path; do
219
+ printf '%s\t%s\n' "$type" "$(rel "$type" "$path")"
220
+ done < <(manifest_entries)
221
+ }
222
+
223
+ main() {
224
+ local cmd="${1:-current}"; shift || true
225
+ case "$cmd" in
226
+ current|cur) cmd_current ;;
227
+ bump) cmd_bump "$@" ;;
228
+ set) cmd_set "$@" ;;
229
+ check) cmd_check ;;
230
+ sync) cmd_sync ;;
231
+ files) cmd_files ;;
232
+ *) die "unknown command: $cmd (current|bump|set|check|sync|files)" ;;
233
+ esac
234
+ }
235
+
236
+ main "$@"
package/dist/index.js CHANGED
@@ -1447,7 +1447,8 @@ function systemctlUser(args) {
1447
1447
  };
1448
1448
  }
1449
1449
  function templateVersioningScript(ctx) {
1450
- return readText(join6(ctx.pjanglerRoot, ".mise", "scripts", "versioning.sh"));
1450
+ const source = join6(ctx.pjanglerRoot, ".mise", "scripts", "versioning.sh");
1451
+ return existsSync5(source) ? readText(source) : void 0;
1451
1452
  }
1452
1453
  function templateVersionFilesConf(ctx, repoRoot) {
1453
1454
  const packageJson = join6(repoRoot, "package.json");
@@ -1834,6 +1835,9 @@ var RULES = [
1834
1835
  }
1835
1836
  const versioningPath = join6(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
1836
1837
  const expectedScript = templateVersioningScript(ctx);
1838
+ if (expectedScript === void 0) {
1839
+ return { id: finding.id, title: finding.title, status: "blocked", summary: "pjangler install is missing .mise/scripts/versioning.sh \u2014 update @delorenj/pjangler (broken package)", changedFiles, details: [] };
1840
+ }
1837
1841
  if (safeReadText(versioningPath) !== expectedScript) {
1838
1842
  changedFiles.push(versioningPath);
1839
1843
  if (!ctx.dryRun) {
@@ -2328,7 +2332,20 @@ function runMigration(selector, repoArg, dryRun, all) {
2328
2332
  if (!selected.length) {
2329
2333
  throw new Error(`Unknown parity rule: ${selector}`);
2330
2334
  }
2331
- const results = selected.map((rule) => rule.migrate(ctx, rule.audit(ctx)));
2335
+ const results = selected.map((rule) => {
2336
+ try {
2337
+ return rule.migrate(ctx, rule.audit(ctx));
2338
+ } catch (err) {
2339
+ return {
2340
+ id: rule.id,
2341
+ title: rule.title,
2342
+ status: "blocked",
2343
+ summary: `migrate threw: ${err instanceof Error ? err.message : String(err)}`,
2344
+ changedFiles: [],
2345
+ details: []
2346
+ };
2347
+ }
2348
+ });
2332
2349
  const changedFiles = Array.from(new Set(results.flatMap((result) => result.changedFiles))).sort();
2333
2350
  return {
2334
2351
  repo: ctx.repoRoot,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@delorenj/pjangler",
3
- "version": "1.1.4",
3
+ "version": "1.1.5",
4
4
  "description": "Project subsystem bootstrapper CLI",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -11,7 +11,8 @@
11
11
  },
12
12
  "files": [
13
13
  "dist",
14
- "templates"
14
+ "templates",
15
+ ".mise/scripts/versioning.sh"
15
16
  ],
16
17
  "engines": {
17
18
  "node": ">=20"