@delorenj/pjangler 1.2.13 → 1.2.16

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/dist/index.js CHANGED
@@ -2121,6 +2121,7 @@ import { basename as basename3, dirname as dirname6, join as join11, relative, r
2121
2121
  import { fileURLToPath as fileURLToPath3 } from "node:url";
2122
2122
  import { homedir as homedir5 } from "node:os";
2123
2123
  import { spawnSync as spawnSync6 } from "node:child_process";
2124
+ import YAML2 from "yaml";
2124
2125
  var LINK_AGENTFILES_BLOCK = `# This block will handle the linking of
2125
2126
  # agent files to the main AGENTS.md file.
2126
2127
  #
@@ -2523,6 +2524,9 @@ function extractTomlStrings(text3) {
2523
2524
  }
2524
2525
  return values;
2525
2526
  }
2527
+ function stripTomlStringsAndComments(line) {
2528
+ return line.replace(/"(?:\\.|[^"\\])*"/g, '""').replace(/'[^']*'/g, "''").replace(/#.*$/, "");
2529
+ }
2526
2530
  function isManagedHookEntry(value) {
2527
2531
  const trimmed = value.trim();
2528
2532
  return trimmed === "op inject -i .env.op > .env" || /(^|\/)link-agentfiles\.sh$/.test(trimmed);
@@ -2550,12 +2554,17 @@ function upsertLinkAgentfilesHooks(text3) {
2550
2554
  for (let i = hooksStart + 1; i < hooksEnd; i++) {
2551
2555
  if (!/^\s*enter\s*=/.test(lines[i])) continue;
2552
2556
  enterStart = i;
2553
- enterEnd = i + 1;
2554
- const afterEquals = lines[i].slice(lines[i].indexOf("=") + 1);
2555
- if (afterEquals.includes("[") && !afterEquals.includes("]")) {
2556
- while (enterEnd < hooksEnd && !lines[enterEnd].includes("]")) enterEnd++;
2557
- if (enterEnd < hooksEnd) enterEnd++;
2557
+ let depth = 0;
2558
+ let j = i;
2559
+ for (; j < hooksEnd; j++) {
2560
+ for (const ch of stripTomlStringsAndComments(lines[j])) {
2561
+ if (ch === "[") depth++;
2562
+ else if (ch === "]") depth--;
2563
+ }
2564
+ if (depth <= 0) break;
2558
2565
  }
2566
+ enterEnd = Math.min(j, hooksEnd - 1) + 1;
2567
+ while (enterEnd < hooksEnd && /^\s*\]\s*$/.test(lines[enterEnd])) enterEnd++;
2559
2568
  break;
2560
2569
  }
2561
2570
  const existingBlock = enterStart >= 0 ? lines.slice(enterStart, enterEnd).join("\n") : "";
@@ -2890,6 +2899,173 @@ function checkUnit(unit) {
2890
2899
  const active = systemctlUser(["is-active", unit]).ok;
2891
2900
  return { enabled, active };
2892
2901
  }
2902
+ var BMAD_NPM_PACKAGE = "bmad-method";
2903
+ var BMAD_TARGET_CHANNEL = "next";
2904
+ var BMAD_DIST_TAGS_TTL_MS = 60 * 60 * 1e3;
2905
+ var BMAD_INSTALL_TOOLS = [
2906
+ "claude-code",
2907
+ "codex",
2908
+ "cursor",
2909
+ "github-copilot",
2910
+ "adal",
2911
+ "antigravity-cli",
2912
+ "auggie",
2913
+ "goose",
2914
+ "cline",
2915
+ "codebuddy",
2916
+ "codewhale",
2917
+ "command-code",
2918
+ "crush",
2919
+ "droid",
2920
+ "firebender",
2921
+ "gemini",
2922
+ "antigravity",
2923
+ "hermes",
2924
+ "bob",
2925
+ "iflow",
2926
+ "junie",
2927
+ "kilo",
2928
+ "kimi-code",
2929
+ "kiro",
2930
+ "kode",
2931
+ "mistral-vibe",
2932
+ "mux",
2933
+ "neovate",
2934
+ "ona",
2935
+ "openclaw",
2936
+ "opencode",
2937
+ "openhands",
2938
+ "pi",
2939
+ "pochi",
2940
+ "qoder",
2941
+ "qwen",
2942
+ "replit",
2943
+ "roo",
2944
+ "rovo-dev",
2945
+ "cortex",
2946
+ "amp",
2947
+ "trae",
2948
+ "warp",
2949
+ "windsurf",
2950
+ "zencoder"
2951
+ ];
2952
+ function bmadInstallArgs(repoRoot) {
2953
+ return [
2954
+ "-y",
2955
+ `${BMAD_NPM_PACKAGE}@${BMAD_TARGET_CHANNEL}`,
2956
+ "install",
2957
+ "--yes",
2958
+ "--directory",
2959
+ repoRoot,
2960
+ "--modules",
2961
+ "bmm,bmb,cis",
2962
+ "--tools",
2963
+ BMAD_INSTALL_TOOLS.join(",")
2964
+ ];
2965
+ }
2966
+ function runBmadInstall(repoRoot) {
2967
+ const result = spawnSync6("npx", bmadInstallArgs(repoRoot), { encoding: "utf8" });
2968
+ if (result.status !== 0) {
2969
+ return { ok: false, error: result.stderr || result.error?.message || "Unknown error" };
2970
+ }
2971
+ return { ok: true };
2972
+ }
2973
+ function readInstalledBmadVersion(repoRoot) {
2974
+ const raw = safeReadText(join11(repoRoot, "_bmad", "_config", "manifest.yaml"));
2975
+ if (!raw) return void 0;
2976
+ try {
2977
+ const parsed = YAML2.parse(raw);
2978
+ const version = parsed?.installation?.version;
2979
+ return typeof version === "string" && version.trim() ? version.trim() : void 0;
2980
+ } catch {
2981
+ return void 0;
2982
+ }
2983
+ }
2984
+ function bmadCachePath(homeDir) {
2985
+ const cacheRoot = process.env.XDG_CACHE_HOME?.trim() || join11(homeDir, ".cache");
2986
+ return join11(cacheRoot, "pjangler", "bmad-dist-tags.json");
2987
+ }
2988
+ function readBmadDistTagsCache(homeDir) {
2989
+ const raw = safeReadText(bmadCachePath(homeDir));
2990
+ if (!raw) return void 0;
2991
+ try {
2992
+ const parsed = JSON.parse(raw);
2993
+ if (parsed && typeof parsed.fetchedAt === "number" && parsed.distTags && typeof parsed.distTags === "object") {
2994
+ return parsed;
2995
+ }
2996
+ } catch {
2997
+ }
2998
+ return void 0;
2999
+ }
3000
+ function fetchBmadDistTags() {
3001
+ const result = spawnSync6("npm", ["view", BMAD_NPM_PACKAGE, "dist-tags", "--json"], {
3002
+ encoding: "utf8",
3003
+ timeout: 8e3
3004
+ });
3005
+ if (result.status !== 0 || !result.stdout.trim()) return void 0;
3006
+ try {
3007
+ const parsed = JSON.parse(result.stdout);
3008
+ const obj = Array.isArray(parsed) ? parsed[0] : parsed;
3009
+ if (!obj || typeof obj !== "object") return void 0;
3010
+ const tags = {};
3011
+ for (const [key, value] of Object.entries(obj)) {
3012
+ if (typeof value === "string") tags[key] = value;
3013
+ }
3014
+ return Object.keys(tags).length ? tags : void 0;
3015
+ } catch {
3016
+ return void 0;
3017
+ }
3018
+ }
3019
+ function resolveBmadDistTags(homeDir) {
3020
+ const cached = readBmadDistTagsCache(homeDir);
3021
+ if (cached && Date.now() - cached.fetchedAt < BMAD_DIST_TAGS_TTL_MS) {
3022
+ return { distTags: cached.distTags, stale: false };
3023
+ }
3024
+ const fetched = fetchBmadDistTags();
3025
+ if (fetched) {
3026
+ try {
3027
+ const path = bmadCachePath(homeDir);
3028
+ mkdirSync6(dirname6(path), { recursive: true });
3029
+ writeFileSync6(path, JSON.stringify({ fetchedAt: Date.now(), distTags: fetched }, null, 2));
3030
+ } catch {
3031
+ }
3032
+ return { distTags: fetched, stale: false };
3033
+ }
3034
+ if (cached) return { distTags: cached.distTags, stale: true };
3035
+ return void 0;
3036
+ }
3037
+ function compareBmadVersions(a, b) {
3038
+ const parse = (v) => {
3039
+ const [core = "0", pre = ""] = v.replace(/^v/, "").split("-", 2);
3040
+ const parts = core.split(".");
3041
+ const n = (i) => parseInt(parts[i] ?? "0", 10) || 0;
3042
+ return { nums: [n(0), n(1), n(2)], pre };
3043
+ };
3044
+ const pa = parse(a);
3045
+ const pb = parse(b);
3046
+ for (let i = 0; i < 3; i++) {
3047
+ if (pa.nums[i] !== pb.nums[i]) return pa.nums[i] - pb.nums[i];
3048
+ }
3049
+ if (pa.pre === pb.pre) return 0;
3050
+ if (!pa.pre) return 1;
3051
+ if (!pb.pre) return -1;
3052
+ const ida = pa.pre.split(".");
3053
+ const idb = pb.pre.split(".");
3054
+ for (let i = 0; i < Math.max(ida.length, idb.length); i++) {
3055
+ const xa = ida[i];
3056
+ const xb = idb[i];
3057
+ if (xa === void 0) return -1;
3058
+ if (xb === void 0) return 1;
3059
+ const na = Number(xa);
3060
+ const nb = Number(xb);
3061
+ if (!Number.isNaN(na) && !Number.isNaN(nb)) {
3062
+ if (na !== nb) return na - nb;
3063
+ } else if (xa !== xb) {
3064
+ return xa < xb ? -1 : 1;
3065
+ }
3066
+ }
3067
+ return 0;
3068
+ }
2893
3069
  var RULES = [
2894
3070
  {
2895
3071
  id: "mise.config-root",
@@ -3254,15 +3430,14 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3254
3430
  id: "bmad.scaffold",
3255
3431
  title: "BMAD modules/docs scaffold",
3256
3432
  audit: (ctx) => {
3257
- const sourceRoot = join11(ctx.pjanglerRoot, "templates", "commonproject", "_bmad");
3258
3433
  const targetRoot = join11(ctx.repoRoot, "_bmad");
3259
3434
  const sentinels = [
3260
3435
  join11("core", "config.yaml"),
3261
- join11("custom", "config.yaml"),
3262
- join11("custom", "workflows", "ticket-lifecycle", "workflow.yaml"),
3263
- join11("bmm", "workflows", "workflow-status", "workflow.yaml")
3436
+ join11("config.toml"),
3437
+ join11("_config", "manifest.yaml"),
3438
+ join11("bmm", "config.yaml")
3264
3439
  ];
3265
- const missing = sentinels.filter((file) => existsSync9(join11(sourceRoot, file)) && !existsSync9(join11(targetRoot, file)));
3440
+ const missing = sentinels.filter((file) => !existsSync9(join11(targetRoot, file)));
3266
3441
  return {
3267
3442
  id: "bmad.scaffold",
3268
3443
  title: "BMAD modules/docs scaffold",
@@ -3274,17 +3449,148 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3274
3449
  },
3275
3450
  migrate: (ctx, finding) => {
3276
3451
  const changedFiles = [];
3277
- copyMissingRecursive(join11(ctx.pjanglerRoot, "templates", "commonproject", "_bmad"), join11(ctx.repoRoot, "_bmad"), changedFiles, ctx.dryRun);
3452
+ if (ctx.dryRun) {
3453
+ for (const detail of finding.details) {
3454
+ changedFiles.push(join11(ctx.repoRoot, detail));
3455
+ }
3456
+ return {
3457
+ id: finding.id,
3458
+ title: finding.title,
3459
+ status: changedFiles.length ? "applied" : "noop",
3460
+ summary: changedFiles.length ? "Would run non-interactive bmad-method install" : "No changes required",
3461
+ changedFiles,
3462
+ details: [
3463
+ `Would run: npx ${bmadInstallArgs(ctx.repoRoot).join(" ").replace(BMAD_INSTALL_TOOLS.join(","), "...")}`
3464
+ ]
3465
+ };
3466
+ }
3467
+ const install = runBmadInstall(ctx.repoRoot);
3468
+ if (!install.ok) {
3469
+ return {
3470
+ id: finding.id,
3471
+ title: finding.title,
3472
+ status: "blocked",
3473
+ summary: `Failed to run bmad-method install`,
3474
+ changedFiles: [],
3475
+ details: [install.error ?? "Unknown error"]
3476
+ };
3477
+ }
3478
+ for (const detail of finding.details) {
3479
+ if (existsSync9(join11(ctx.repoRoot, detail))) {
3480
+ changedFiles.push(join11(ctx.repoRoot, detail));
3481
+ }
3482
+ }
3278
3483
  return {
3279
3484
  id: finding.id,
3280
3485
  title: finding.title,
3281
3486
  status: changedFiles.length ? "applied" : "noop",
3282
- summary: changedFiles.length ? "Copied missing BMAD scaffold files" : "No changes required",
3487
+ summary: changedFiles.length ? "Installed BMAD scaffold via non-interactive installer" : "No changes required",
3283
3488
  changedFiles,
3284
3489
  details: []
3285
3490
  };
3286
3491
  }
3287
3492
  },
3493
+ {
3494
+ id: "bmad.version",
3495
+ title: "BMAD version currency",
3496
+ audit: (ctx) => {
3497
+ const installed = readInstalledBmadVersion(ctx.repoRoot);
3498
+ if (!installed) {
3499
+ return {
3500
+ id: "bmad.version",
3501
+ title: "BMAD version currency",
3502
+ status: "skip",
3503
+ summary: existsSync9(join11(ctx.repoRoot, "_bmad")) ? "BMAD installed but version manifest unreadable" : "No BMAD install present",
3504
+ details: [],
3505
+ fixable: false
3506
+ };
3507
+ }
3508
+ const resolved = resolveBmadDistTags(ctx.homeDir);
3509
+ const available = resolved?.distTags?.[BMAD_TARGET_CHANNEL];
3510
+ if (!available) {
3511
+ return {
3512
+ id: "bmad.version",
3513
+ title: "BMAD version currency",
3514
+ status: "skip",
3515
+ summary: `BMAD ${installed} installed; latest ${BMAD_TARGET_CHANNEL} version unknown (npm unreachable)`,
3516
+ details: [`Could not resolve ${BMAD_NPM_PACKAGE}@${BMAD_TARGET_CHANNEL} from npm`],
3517
+ fixable: false
3518
+ };
3519
+ }
3520
+ const staleNote = resolved.stale ? ` ${glyph.dot} cached` : "";
3521
+ if (compareBmadVersions(installed, available) >= 0) {
3522
+ return {
3523
+ id: "bmad.version",
3524
+ title: "BMAD version currency",
3525
+ status: "pass",
3526
+ summary: `BMAD ${installed} is current (${BMAD_TARGET_CHANNEL} ${available})${staleNote}`,
3527
+ details: [],
3528
+ fixable: false
3529
+ };
3530
+ }
3531
+ return {
3532
+ id: "bmad.version",
3533
+ title: "BMAD version currency",
3534
+ status: "warn",
3535
+ summary: `BMAD ${installed} is behind ${BMAD_TARGET_CHANNEL} ${available} \u2014 upgrade available`,
3536
+ details: [
3537
+ `installed: ${installed}`,
3538
+ `available: ${available} (${BMAD_NPM_PACKAGE}@${BMAD_TARGET_CHANNEL})`,
3539
+ resolved.distTags.latest ? `stable latest: ${resolved.distTags.latest}` : "",
3540
+ "run `pj migrate bmad.version` to upgrade"
3541
+ ].filter(Boolean),
3542
+ fixable: true
3543
+ };
3544
+ },
3545
+ migrate: (ctx, finding) => {
3546
+ if (finding.status !== "warn") {
3547
+ return {
3548
+ id: finding.id,
3549
+ title: finding.title,
3550
+ status: "noop",
3551
+ summary: finding.status === "skip" ? finding.summary : "BMAD already current",
3552
+ changedFiles: [],
3553
+ details: []
3554
+ };
3555
+ }
3556
+ const installed = readInstalledBmadVersion(ctx.repoRoot);
3557
+ const available = resolveBmadDistTags(ctx.homeDir)?.distTags?.[BMAD_TARGET_CHANNEL];
3558
+ const manifestPath = join11(ctx.repoRoot, "_bmad", "_config", "manifest.yaml");
3559
+ if (ctx.dryRun) {
3560
+ return {
3561
+ id: finding.id,
3562
+ title: finding.title,
3563
+ status: "applied",
3564
+ summary: `Would upgrade BMAD ${installed ?? "?"} -> ${available ?? BMAD_TARGET_CHANNEL}`,
3565
+ changedFiles: [manifestPath],
3566
+ details: [
3567
+ `Would run: npx ${bmadInstallArgs(ctx.repoRoot).join(" ").replace(BMAD_INSTALL_TOOLS.join(","), "...")}`
3568
+ ]
3569
+ };
3570
+ }
3571
+ const install = runBmadInstall(ctx.repoRoot);
3572
+ if (!install.ok) {
3573
+ return {
3574
+ id: finding.id,
3575
+ title: finding.title,
3576
+ status: "blocked",
3577
+ summary: "Failed to upgrade BMAD via installer",
3578
+ changedFiles: [],
3579
+ details: [install.error ?? "Unknown error"]
3580
+ };
3581
+ }
3582
+ const nowInstalled = readInstalledBmadVersion(ctx.repoRoot);
3583
+ const upgraded = Boolean(nowInstalled && installed && compareBmadVersions(nowInstalled, installed) > 0);
3584
+ return {
3585
+ id: finding.id,
3586
+ title: finding.title,
3587
+ status: upgraded ? "applied" : "noop",
3588
+ summary: upgraded ? `Upgraded BMAD ${installed} -> ${nowInstalled}` : `BMAD reinstalled (${nowInstalled ?? "?"})`,
3589
+ changedFiles: upgraded ? [manifestPath] : [],
3590
+ details: []
3591
+ };
3592
+ }
3593
+ },
3288
3594
  {
3289
3595
  id: "hermes.pm-scaffold",
3290
3596
  title: "Hermes PM scaffold parity",
@@ -2048,6 +2048,7 @@ import { basename as basename3, dirname as dirname7, join as join12, relative, r
2048
2048
  import { fileURLToPath as fileURLToPath4 } from "node:url";
2049
2049
  import { homedir as homedir5 } from "node:os";
2050
2050
  import { spawnSync as spawnSync6 } from "node:child_process";
2051
+ import YAML2 from "yaml";
2051
2052
  var LINK_AGENTFILES_BLOCK = `# This block will handle the linking of
2052
2053
  # agent files to the main AGENTS.md file.
2053
2054
  #
@@ -2450,6 +2451,9 @@ function extractTomlStrings(text2) {
2450
2451
  }
2451
2452
  return values;
2452
2453
  }
2454
+ function stripTomlStringsAndComments(line) {
2455
+ return line.replace(/"(?:\\.|[^"\\])*"/g, '""').replace(/'[^']*'/g, "''").replace(/#.*$/, "");
2456
+ }
2453
2457
  function isManagedHookEntry(value) {
2454
2458
  const trimmed = value.trim();
2455
2459
  return trimmed === "op inject -i .env.op > .env" || /(^|\/)link-agentfiles\.sh$/.test(trimmed);
@@ -2477,12 +2481,17 @@ function upsertLinkAgentfilesHooks(text2) {
2477
2481
  for (let i = hooksStart + 1; i < hooksEnd; i++) {
2478
2482
  if (!/^\s*enter\s*=/.test(lines[i])) continue;
2479
2483
  enterStart = i;
2480
- enterEnd = i + 1;
2481
- const afterEquals = lines[i].slice(lines[i].indexOf("=") + 1);
2482
- if (afterEquals.includes("[") && !afterEquals.includes("]")) {
2483
- while (enterEnd < hooksEnd && !lines[enterEnd].includes("]")) enterEnd++;
2484
- if (enterEnd < hooksEnd) enterEnd++;
2484
+ let depth = 0;
2485
+ let j = i;
2486
+ for (; j < hooksEnd; j++) {
2487
+ for (const ch of stripTomlStringsAndComments(lines[j])) {
2488
+ if (ch === "[") depth++;
2489
+ else if (ch === "]") depth--;
2490
+ }
2491
+ if (depth <= 0) break;
2485
2492
  }
2493
+ enterEnd = Math.min(j, hooksEnd - 1) + 1;
2494
+ while (enterEnd < hooksEnd && /^\s*\]\s*$/.test(lines[enterEnd])) enterEnd++;
2486
2495
  break;
2487
2496
  }
2488
2497
  const existingBlock = enterStart >= 0 ? lines.slice(enterStart, enterEnd).join("\n") : "";
@@ -2817,6 +2826,173 @@ function checkUnit(unit) {
2817
2826
  const active = systemctlUser(["is-active", unit]).ok;
2818
2827
  return { enabled, active };
2819
2828
  }
2829
+ var BMAD_NPM_PACKAGE = "bmad-method";
2830
+ var BMAD_TARGET_CHANNEL = "next";
2831
+ var BMAD_DIST_TAGS_TTL_MS = 60 * 60 * 1e3;
2832
+ var BMAD_INSTALL_TOOLS = [
2833
+ "claude-code",
2834
+ "codex",
2835
+ "cursor",
2836
+ "github-copilot",
2837
+ "adal",
2838
+ "antigravity-cli",
2839
+ "auggie",
2840
+ "goose",
2841
+ "cline",
2842
+ "codebuddy",
2843
+ "codewhale",
2844
+ "command-code",
2845
+ "crush",
2846
+ "droid",
2847
+ "firebender",
2848
+ "gemini",
2849
+ "antigravity",
2850
+ "hermes",
2851
+ "bob",
2852
+ "iflow",
2853
+ "junie",
2854
+ "kilo",
2855
+ "kimi-code",
2856
+ "kiro",
2857
+ "kode",
2858
+ "mistral-vibe",
2859
+ "mux",
2860
+ "neovate",
2861
+ "ona",
2862
+ "openclaw",
2863
+ "opencode",
2864
+ "openhands",
2865
+ "pi",
2866
+ "pochi",
2867
+ "qoder",
2868
+ "qwen",
2869
+ "replit",
2870
+ "roo",
2871
+ "rovo-dev",
2872
+ "cortex",
2873
+ "amp",
2874
+ "trae",
2875
+ "warp",
2876
+ "windsurf",
2877
+ "zencoder"
2878
+ ];
2879
+ function bmadInstallArgs(repoRoot) {
2880
+ return [
2881
+ "-y",
2882
+ `${BMAD_NPM_PACKAGE}@${BMAD_TARGET_CHANNEL}`,
2883
+ "install",
2884
+ "--yes",
2885
+ "--directory",
2886
+ repoRoot,
2887
+ "--modules",
2888
+ "bmm,bmb,cis",
2889
+ "--tools",
2890
+ BMAD_INSTALL_TOOLS.join(",")
2891
+ ];
2892
+ }
2893
+ function runBmadInstall(repoRoot) {
2894
+ const result = spawnSync6("npx", bmadInstallArgs(repoRoot), { encoding: "utf8" });
2895
+ if (result.status !== 0) {
2896
+ return { ok: false, error: result.stderr || result.error?.message || "Unknown error" };
2897
+ }
2898
+ return { ok: true };
2899
+ }
2900
+ function readInstalledBmadVersion(repoRoot) {
2901
+ const raw = safeReadText(join12(repoRoot, "_bmad", "_config", "manifest.yaml"));
2902
+ if (!raw) return void 0;
2903
+ try {
2904
+ const parsed = YAML2.parse(raw);
2905
+ const version = parsed?.installation?.version;
2906
+ return typeof version === "string" && version.trim() ? version.trim() : void 0;
2907
+ } catch {
2908
+ return void 0;
2909
+ }
2910
+ }
2911
+ function bmadCachePath(homeDir) {
2912
+ const cacheRoot = process.env.XDG_CACHE_HOME?.trim() || join12(homeDir, ".cache");
2913
+ return join12(cacheRoot, "pjangler", "bmad-dist-tags.json");
2914
+ }
2915
+ function readBmadDistTagsCache(homeDir) {
2916
+ const raw = safeReadText(bmadCachePath(homeDir));
2917
+ if (!raw) return void 0;
2918
+ try {
2919
+ const parsed = JSON.parse(raw);
2920
+ if (parsed && typeof parsed.fetchedAt === "number" && parsed.distTags && typeof parsed.distTags === "object") {
2921
+ return parsed;
2922
+ }
2923
+ } catch {
2924
+ }
2925
+ return void 0;
2926
+ }
2927
+ function fetchBmadDistTags() {
2928
+ const result = spawnSync6("npm", ["view", BMAD_NPM_PACKAGE, "dist-tags", "--json"], {
2929
+ encoding: "utf8",
2930
+ timeout: 8e3
2931
+ });
2932
+ if (result.status !== 0 || !result.stdout.trim()) return void 0;
2933
+ try {
2934
+ const parsed = JSON.parse(result.stdout);
2935
+ const obj = Array.isArray(parsed) ? parsed[0] : parsed;
2936
+ if (!obj || typeof obj !== "object") return void 0;
2937
+ const tags = {};
2938
+ for (const [key, value] of Object.entries(obj)) {
2939
+ if (typeof value === "string") tags[key] = value;
2940
+ }
2941
+ return Object.keys(tags).length ? tags : void 0;
2942
+ } catch {
2943
+ return void 0;
2944
+ }
2945
+ }
2946
+ function resolveBmadDistTags(homeDir) {
2947
+ const cached = readBmadDistTagsCache(homeDir);
2948
+ if (cached && Date.now() - cached.fetchedAt < BMAD_DIST_TAGS_TTL_MS) {
2949
+ return { distTags: cached.distTags, stale: false };
2950
+ }
2951
+ const fetched = fetchBmadDistTags();
2952
+ if (fetched) {
2953
+ try {
2954
+ const path = bmadCachePath(homeDir);
2955
+ mkdirSync6(dirname7(path), { recursive: true });
2956
+ writeFileSync6(path, JSON.stringify({ fetchedAt: Date.now(), distTags: fetched }, null, 2));
2957
+ } catch {
2958
+ }
2959
+ return { distTags: fetched, stale: false };
2960
+ }
2961
+ if (cached) return { distTags: cached.distTags, stale: true };
2962
+ return void 0;
2963
+ }
2964
+ function compareBmadVersions(a, b) {
2965
+ const parse = (v) => {
2966
+ const [core = "0", pre = ""] = v.replace(/^v/, "").split("-", 2);
2967
+ const parts = core.split(".");
2968
+ const n = (i) => parseInt(parts[i] ?? "0", 10) || 0;
2969
+ return { nums: [n(0), n(1), n(2)], pre };
2970
+ };
2971
+ const pa = parse(a);
2972
+ const pb = parse(b);
2973
+ for (let i = 0; i < 3; i++) {
2974
+ if (pa.nums[i] !== pb.nums[i]) return pa.nums[i] - pb.nums[i];
2975
+ }
2976
+ if (pa.pre === pb.pre) return 0;
2977
+ if (!pa.pre) return 1;
2978
+ if (!pb.pre) return -1;
2979
+ const ida = pa.pre.split(".");
2980
+ const idb = pb.pre.split(".");
2981
+ for (let i = 0; i < Math.max(ida.length, idb.length); i++) {
2982
+ const xa = ida[i];
2983
+ const xb = idb[i];
2984
+ if (xa === void 0) return -1;
2985
+ if (xb === void 0) return 1;
2986
+ const na = Number(xa);
2987
+ const nb = Number(xb);
2988
+ if (!Number.isNaN(na) && !Number.isNaN(nb)) {
2989
+ if (na !== nb) return na - nb;
2990
+ } else if (xa !== xb) {
2991
+ return xa < xb ? -1 : 1;
2992
+ }
2993
+ }
2994
+ return 0;
2995
+ }
2820
2996
  var RULES = [
2821
2997
  {
2822
2998
  id: "mise.config-root",
@@ -3181,15 +3357,14 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3181
3357
  id: "bmad.scaffold",
3182
3358
  title: "BMAD modules/docs scaffold",
3183
3359
  audit: (ctx) => {
3184
- const sourceRoot = join12(ctx.pjanglerRoot, "templates", "commonproject", "_bmad");
3185
3360
  const targetRoot = join12(ctx.repoRoot, "_bmad");
3186
3361
  const sentinels = [
3187
3362
  join12("core", "config.yaml"),
3188
- join12("custom", "config.yaml"),
3189
- join12("custom", "workflows", "ticket-lifecycle", "workflow.yaml"),
3190
- join12("bmm", "workflows", "workflow-status", "workflow.yaml")
3363
+ join12("config.toml"),
3364
+ join12("_config", "manifest.yaml"),
3365
+ join12("bmm", "config.yaml")
3191
3366
  ];
3192
- const missing = sentinels.filter((file) => existsSync9(join12(sourceRoot, file)) && !existsSync9(join12(targetRoot, file)));
3367
+ const missing = sentinels.filter((file) => !existsSync9(join12(targetRoot, file)));
3193
3368
  return {
3194
3369
  id: "bmad.scaffold",
3195
3370
  title: "BMAD modules/docs scaffold",
@@ -3201,17 +3376,148 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3201
3376
  },
3202
3377
  migrate: (ctx, finding) => {
3203
3378
  const changedFiles = [];
3204
- copyMissingRecursive(join12(ctx.pjanglerRoot, "templates", "commonproject", "_bmad"), join12(ctx.repoRoot, "_bmad"), changedFiles, ctx.dryRun);
3379
+ if (ctx.dryRun) {
3380
+ for (const detail of finding.details) {
3381
+ changedFiles.push(join12(ctx.repoRoot, detail));
3382
+ }
3383
+ return {
3384
+ id: finding.id,
3385
+ title: finding.title,
3386
+ status: changedFiles.length ? "applied" : "noop",
3387
+ summary: changedFiles.length ? "Would run non-interactive bmad-method install" : "No changes required",
3388
+ changedFiles,
3389
+ details: [
3390
+ `Would run: npx ${bmadInstallArgs(ctx.repoRoot).join(" ").replace(BMAD_INSTALL_TOOLS.join(","), "...")}`
3391
+ ]
3392
+ };
3393
+ }
3394
+ const install = runBmadInstall(ctx.repoRoot);
3395
+ if (!install.ok) {
3396
+ return {
3397
+ id: finding.id,
3398
+ title: finding.title,
3399
+ status: "blocked",
3400
+ summary: `Failed to run bmad-method install`,
3401
+ changedFiles: [],
3402
+ details: [install.error ?? "Unknown error"]
3403
+ };
3404
+ }
3405
+ for (const detail of finding.details) {
3406
+ if (existsSync9(join12(ctx.repoRoot, detail))) {
3407
+ changedFiles.push(join12(ctx.repoRoot, detail));
3408
+ }
3409
+ }
3205
3410
  return {
3206
3411
  id: finding.id,
3207
3412
  title: finding.title,
3208
3413
  status: changedFiles.length ? "applied" : "noop",
3209
- summary: changedFiles.length ? "Copied missing BMAD scaffold files" : "No changes required",
3414
+ summary: changedFiles.length ? "Installed BMAD scaffold via non-interactive installer" : "No changes required",
3210
3415
  changedFiles,
3211
3416
  details: []
3212
3417
  };
3213
3418
  }
3214
3419
  },
3420
+ {
3421
+ id: "bmad.version",
3422
+ title: "BMAD version currency",
3423
+ audit: (ctx) => {
3424
+ const installed = readInstalledBmadVersion(ctx.repoRoot);
3425
+ if (!installed) {
3426
+ return {
3427
+ id: "bmad.version",
3428
+ title: "BMAD version currency",
3429
+ status: "skip",
3430
+ summary: existsSync9(join12(ctx.repoRoot, "_bmad")) ? "BMAD installed but version manifest unreadable" : "No BMAD install present",
3431
+ details: [],
3432
+ fixable: false
3433
+ };
3434
+ }
3435
+ const resolved = resolveBmadDistTags(ctx.homeDir);
3436
+ const available = resolved?.distTags?.[BMAD_TARGET_CHANNEL];
3437
+ if (!available) {
3438
+ return {
3439
+ id: "bmad.version",
3440
+ title: "BMAD version currency",
3441
+ status: "skip",
3442
+ summary: `BMAD ${installed} installed; latest ${BMAD_TARGET_CHANNEL} version unknown (npm unreachable)`,
3443
+ details: [`Could not resolve ${BMAD_NPM_PACKAGE}@${BMAD_TARGET_CHANNEL} from npm`],
3444
+ fixable: false
3445
+ };
3446
+ }
3447
+ const staleNote = resolved.stale ? ` ${glyph.dot} cached` : "";
3448
+ if (compareBmadVersions(installed, available) >= 0) {
3449
+ return {
3450
+ id: "bmad.version",
3451
+ title: "BMAD version currency",
3452
+ status: "pass",
3453
+ summary: `BMAD ${installed} is current (${BMAD_TARGET_CHANNEL} ${available})${staleNote}`,
3454
+ details: [],
3455
+ fixable: false
3456
+ };
3457
+ }
3458
+ return {
3459
+ id: "bmad.version",
3460
+ title: "BMAD version currency",
3461
+ status: "warn",
3462
+ summary: `BMAD ${installed} is behind ${BMAD_TARGET_CHANNEL} ${available} \u2014 upgrade available`,
3463
+ details: [
3464
+ `installed: ${installed}`,
3465
+ `available: ${available} (${BMAD_NPM_PACKAGE}@${BMAD_TARGET_CHANNEL})`,
3466
+ resolved.distTags.latest ? `stable latest: ${resolved.distTags.latest}` : "",
3467
+ "run `pj migrate bmad.version` to upgrade"
3468
+ ].filter(Boolean),
3469
+ fixable: true
3470
+ };
3471
+ },
3472
+ migrate: (ctx, finding) => {
3473
+ if (finding.status !== "warn") {
3474
+ return {
3475
+ id: finding.id,
3476
+ title: finding.title,
3477
+ status: "noop",
3478
+ summary: finding.status === "skip" ? finding.summary : "BMAD already current",
3479
+ changedFiles: [],
3480
+ details: []
3481
+ };
3482
+ }
3483
+ const installed = readInstalledBmadVersion(ctx.repoRoot);
3484
+ const available = resolveBmadDistTags(ctx.homeDir)?.distTags?.[BMAD_TARGET_CHANNEL];
3485
+ const manifestPath = join12(ctx.repoRoot, "_bmad", "_config", "manifest.yaml");
3486
+ if (ctx.dryRun) {
3487
+ return {
3488
+ id: finding.id,
3489
+ title: finding.title,
3490
+ status: "applied",
3491
+ summary: `Would upgrade BMAD ${installed ?? "?"} -> ${available ?? BMAD_TARGET_CHANNEL}`,
3492
+ changedFiles: [manifestPath],
3493
+ details: [
3494
+ `Would run: npx ${bmadInstallArgs(ctx.repoRoot).join(" ").replace(BMAD_INSTALL_TOOLS.join(","), "...")}`
3495
+ ]
3496
+ };
3497
+ }
3498
+ const install = runBmadInstall(ctx.repoRoot);
3499
+ if (!install.ok) {
3500
+ return {
3501
+ id: finding.id,
3502
+ title: finding.title,
3503
+ status: "blocked",
3504
+ summary: "Failed to upgrade BMAD via installer",
3505
+ changedFiles: [],
3506
+ details: [install.error ?? "Unknown error"]
3507
+ };
3508
+ }
3509
+ const nowInstalled = readInstalledBmadVersion(ctx.repoRoot);
3510
+ const upgraded = Boolean(nowInstalled && installed && compareBmadVersions(nowInstalled, installed) > 0);
3511
+ return {
3512
+ id: finding.id,
3513
+ title: finding.title,
3514
+ status: upgraded ? "applied" : "noop",
3515
+ summary: upgraded ? `Upgraded BMAD ${installed} -> ${nowInstalled}` : `BMAD reinstalled (${nowInstalled ?? "?"})`,
3516
+ changedFiles: upgraded ? [manifestPath] : [],
3517
+ details: []
3518
+ };
3519
+ }
3520
+ },
3215
3521
  {
3216
3522
  id: "hermes.pm-scaffold",
3217
3523
  title: "Hermes PM scaffold parity",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@delorenj/pjangler",
3
- "version": "1.2.13",
3
+ "version": "1.2.16",
4
4
  "description": "Project subsystem bootstrapper CLI",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",