@delorenj/pjangler 1.1.3 → 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
@@ -55,8 +55,8 @@ var Command = class {
55
55
  }
56
56
  fileExists(filePath) {
57
57
  const { existsSync: existsSync6 } = __require("fs");
58
- const { join: join7 } = __require("path");
59
- const fullPath = join7(this.context.targetDir, filePath);
58
+ const { join: join8 } = __require("path");
59
+ const fullPath = join8(this.context.targetDir, filePath);
60
60
  return existsSync6(fullPath);
61
61
  }
62
62
  writeFile(filePath, content) {
@@ -64,9 +64,9 @@ var Command = class {
64
64
  return;
65
65
  }
66
66
  const { writeFileSync: writeFileSync3, mkdirSync: mkdirSync4 } = __require("fs");
67
- const { join: join7, dirname: dirname4 } = __require("path");
68
- const fullPath = join7(this.context.targetDir, filePath);
69
- const dir = dirname4(fullPath);
67
+ const { join: join8, dirname: dirname5 } = __require("path");
68
+ const fullPath = join8(this.context.targetDir, filePath);
69
+ const dir = dirname5(fullPath);
70
70
  mkdirSync4(dir, { recursive: true });
71
71
  writeFileSync3(fullPath, content);
72
72
  }
@@ -75,8 +75,8 @@ var Command = class {
75
75
  return;
76
76
  }
77
77
  const { mkdirSync: mkdirSync4 } = __require("fs");
78
- const { join: join7 } = __require("path");
79
- const fullPath = join7(this.context.targetDir, dirPath);
78
+ const { join: join8 } = __require("path");
79
+ const fullPath = join8(this.context.targetDir, dirPath);
80
80
  mkdirSync4(fullPath, { recursive: true });
81
81
  }
82
82
  };
@@ -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,
@@ -2363,9 +2380,31 @@ function formatMigrationReport(report) {
2363
2380
  `;
2364
2381
  }
2365
2382
 
2383
+ // src/utils/version.ts
2384
+ import { readFileSync as readFileSync3 } from "node:fs";
2385
+ import { dirname as dirname4, join as join7 } from "node:path";
2386
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
2387
+ var PJANGLER_VERSION = (() => {
2388
+ try {
2389
+ let dir = dirname4(fileURLToPath3(import.meta.url));
2390
+ for (let i = 0; i < 4; i++) {
2391
+ try {
2392
+ const raw = readFileSync3(join7(dir, "package.json"), "utf8");
2393
+ return JSON.parse(raw).version ?? "0.0.0";
2394
+ } catch {
2395
+ const parent = dirname4(dir);
2396
+ if (parent === dir) break;
2397
+ dir = parent;
2398
+ }
2399
+ }
2400
+ } catch {
2401
+ }
2402
+ return "0.0.0";
2403
+ })();
2404
+
2366
2405
  // src/index.ts
2367
2406
  var program = new Command3();
2368
- program.name("pjangler").description("Project subsystem bootstrapper CLI").version("1.0.0");
2407
+ program.name("pjangler").description("Project subsystem bootstrapper CLI").version(PJANGLER_VERSION);
2369
2408
  program.command("init").argument("<subsystem>", "Subsystem to initialize").description("Initialize a project subsystem").option("--dry-run", "Preview changes without writing files").option("-f, --force", "Overwrite existing files").action(async (subsystem, options) => {
2370
2409
  const context = {
2371
2410
  targetDir: process.cwd(),
@@ -27,8 +27,8 @@ var Command = class {
27
27
  }
28
28
  fileExists(filePath) {
29
29
  const { existsSync: existsSync6 } = __require("fs");
30
- const { join: join6 } = __require("path");
31
- const fullPath = join6(this.context.targetDir, filePath);
30
+ const { join: join7 } = __require("path");
31
+ const fullPath = join7(this.context.targetDir, filePath);
32
32
  return existsSync6(fullPath);
33
33
  }
34
34
  writeFile(filePath, content) {
@@ -36,9 +36,9 @@ var Command = class {
36
36
  return;
37
37
  }
38
38
  const { writeFileSync: writeFileSync2, mkdirSync: mkdirSync3 } = __require("fs");
39
- const { join: join6, dirname: dirname3 } = __require("path");
40
- const fullPath = join6(this.context.targetDir, filePath);
41
- const dir = dirname3(fullPath);
39
+ const { join: join7, dirname: dirname4 } = __require("path");
40
+ const fullPath = join7(this.context.targetDir, filePath);
41
+ const dir = dirname4(fullPath);
42
42
  mkdirSync3(dir, { recursive: true });
43
43
  writeFileSync2(fullPath, content);
44
44
  }
@@ -47,8 +47,8 @@ var Command = class {
47
47
  return;
48
48
  }
49
49
  const { mkdirSync: mkdirSync3 } = __require("fs");
50
- const { join: join6 } = __require("path");
51
- const fullPath = join6(this.context.targetDir, dirPath);
50
+ const { join: join7 } = __require("path");
51
+ const fullPath = join7(this.context.targetDir, dirPath);
52
52
  mkdirSync3(fullPath, { recursive: true });
53
53
  }
54
54
  };
@@ -1248,10 +1248,32 @@ function createRecipe(name, context) {
1248
1248
  return new info.class(context);
1249
1249
  }
1250
1250
 
1251
+ // src/utils/version.ts
1252
+ import { readFileSync as readFileSync2 } from "node:fs";
1253
+ import { dirname as dirname3, join as join6 } from "node:path";
1254
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
1255
+ var PJANGLER_VERSION = (() => {
1256
+ try {
1257
+ let dir = dirname3(fileURLToPath2(import.meta.url));
1258
+ for (let i = 0; i < 4; i++) {
1259
+ try {
1260
+ const raw = readFileSync2(join6(dir, "package.json"), "utf8");
1261
+ return JSON.parse(raw).version ?? "0.0.0";
1262
+ } catch {
1263
+ const parent = dirname3(dir);
1264
+ if (parent === dir) break;
1265
+ dir = parent;
1266
+ }
1267
+ }
1268
+ } catch {
1269
+ }
1270
+ return "0.0.0";
1271
+ })();
1272
+
1251
1273
  // src/mcp-server.ts
1252
1274
  var server = new McpServer({
1253
1275
  name: "pjangler-mcp",
1254
- version: "1.0.0"
1276
+ version: PJANGLER_VERSION
1255
1277
  });
1256
1278
  function resolveTargetDir(targetDir) {
1257
1279
  const dir = resolve(targetDir ?? process.cwd());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@delorenj/pjangler",
3
- "version": "1.1.3",
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"