@delorenj/pjangler 1.2.14 → 1.2.17

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
@@ -2122,72 +2122,48 @@ 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
2124
  import YAML2 from "yaml";
2125
- var LINK_AGENTFILES_BLOCK = `# This block will handle the linking of
2125
+ var LINK_AGENTFILES_SCRIPT = "'{{config_root}}/.mise/scripts/link-agentfiles.sh'";
2126
+ var OP_INJECT_SCRIPT = "op inject -i .env.op > .env";
2127
+ var CODEGRAPH_SCRIPT = "[ -f '{{config_root}}/.mise/scripts/codegraph.sh' ] && '{{config_root}}/.mise/scripts/codegraph.sh' || true";
2128
+ var HOOKS_COMMENT_HEADER = `# This block will handle the linking of
2126
2129
  # agent files to the main AGENTS.md file.
2127
2130
  #
2128
2131
  # TODO: Ensure this works for all levels of nesting.
2129
2132
  # i.e. All linked agent files MUST be siblings at
2130
- # any given level of nesting.
2131
- [hooks]
2132
- enter = [
2133
- "{{config_root}}/.mise/scripts/link-agentfiles.sh",
2134
- "op inject -i .env.op > .env",
2135
- ]
2136
-
2137
- [[watch_files]]
2138
- patterns = ["AGENTS.md"]
2139
- task = "link-agentfiles"
2140
-
2141
- [tasks.link-agentfiles]
2142
- description = "Symlink all agent files to AGENTS.md"
2143
- run = "{{config_root}}/.mise/scripts/link-agentfiles.sh"`;
2144
- var LINK_AGENTFILES_HOOK_ENTRIES = [
2145
- "{{config_root}}/.mise/scripts/link-agentfiles.sh",
2146
- "op inject -i .env.op > .env"
2147
- ];
2148
- var LINK_AGENTFILES_HOOKS_BLOCK = `# This block will handle the linking of
2149
- # agent files to the main AGENTS.md file.
2150
- #
2151
- # TODO: Ensure this works for all levels of nesting.
2152
- # i.e. All linked agent files MUST be siblings at
2153
- # any given level of nesting.
2154
- [hooks]
2155
- enter = [
2156
- "{{config_root}}/.mise/scripts/link-agentfiles.sh",
2157
- "op inject -i .env.op > .env",
2158
- ]`;
2133
+ # any given level of nesting.`;
2134
+ var LINK_AGENTFILES_HOOK_ENTRIES = [LINK_AGENTFILES_SCRIPT, OP_INJECT_SCRIPT];
2159
2135
  var LINK_AGENTFILES_WATCH_TASK_BLOCK = `[[watch_files]]
2160
2136
  patterns = ["AGENTS.md"]
2161
2137
  task = "link-agentfiles"
2162
2138
 
2163
2139
  [tasks.link-agentfiles]
2164
2140
  description = "Symlink all agent files to AGENTS.md"
2165
- run = "{{config_root}}/.mise/scripts/link-agentfiles.sh"`;
2141
+ run = "'{{config_root}}/.mise/scripts/link-agentfiles.sh'"`;
2166
2142
  var VERSIONING_BLOCK = `# >>> mise-versioning >>> (managed block \u2014 do not edit by hand; re-run init to update)
2167
2143
  [tasks."version"]
2168
2144
  description = "Print the current version (vX.Y.Z)"
2169
- run = "{{config_root}}/.mise/scripts/versioning.sh current"
2145
+ run = "'{{config_root}}/.mise/scripts/versioning.sh' current"
2170
2146
 
2171
2147
  [tasks."version:bump"]
2172
2148
  description = "Bump patch version: vX.Y.Z -> vX.Y.(Z+1)"
2173
2149
  alias = "version:bump-patch"
2174
- run = "{{config_root}}/.mise/scripts/versioning.sh bump patch"
2150
+ run = "'{{config_root}}/.mise/scripts/versioning.sh' bump patch"
2175
2151
 
2176
2152
  [tasks."version:bump-minor"]
2177
2153
  description = "Bump minor version: vX.Y.Z -> vX.(Y+1).0"
2178
- run = "{{config_root}}/.mise/scripts/versioning.sh bump minor"
2154
+ run = "'{{config_root}}/.mise/scripts/versioning.sh' bump minor"
2179
2155
 
2180
2156
  [tasks."version:bump-major"]
2181
2157
  description = "Bump major version: vX.Y.Z -> v(X+1).0.0"
2182
- run = "{{config_root}}/.mise/scripts/versioning.sh bump major"
2158
+ run = "'{{config_root}}/.mise/scripts/versioning.sh' bump major"
2183
2159
 
2184
2160
  [tasks."version:check"]
2185
2161
  description = "Verify every versioned file is in parity"
2186
- run = "{{config_root}}/.mise/scripts/versioning.sh check"
2162
+ run = "'{{config_root}}/.mise/scripts/versioning.sh' check"
2187
2163
 
2188
2164
  [tasks."version:sync"]
2189
2165
  description = "Force every versioned file up to the highest version"
2190
- run = "{{config_root}}/.mise/scripts/versioning.sh sync"
2166
+ run = "'{{config_root}}/.mise/scripts/versioning.sh' sync"
2191
2167
  # <<< mise-versioning <<<`;
2192
2168
  function resolvePjanglerRoot2() {
2193
2169
  let dir = dirname6(fileURLToPath3(import.meta.url));
@@ -2488,6 +2464,9 @@ function removeTomlSection(text3, headerPattern, marker, options) {
2488
2464
  break;
2489
2465
  }
2490
2466
  if (start === -1) return text3;
2467
+ while (end > start + 1 && (lines[end - 1].trim() === "" || lines[end - 1].trim().startsWith("#"))) {
2468
+ end--;
2469
+ }
2491
2470
  if (options?.includePrecedingComments) {
2492
2471
  while (start > 0 && lines[start - 1].trim().startsWith("#")) {
2493
2472
  start--;
@@ -2524,57 +2503,113 @@ function extractTomlStrings(text3) {
2524
2503
  }
2525
2504
  return values;
2526
2505
  }
2506
+ function stripTomlStringsAndComments(line) {
2507
+ return line.replace(/"(?:\\.|[^"\\])*"/g, '""').replace(/'[^']*'/g, "''").replace(/#.*$/, "");
2508
+ }
2527
2509
  function isManagedHookEntry(value) {
2528
2510
  const trimmed = value.trim();
2529
- return trimmed === "op inject -i .env.op > .env" || /(^|\/)link-agentfiles\.sh$/.test(trimmed);
2511
+ if (trimmed === OP_INJECT_SCRIPT) return true;
2512
+ return /link-agentfiles\.sh'?\s*$/.test(trimmed);
2530
2513
  }
2531
- function renderHookEntries(entries, indent = "") {
2532
- return [
2533
- `${indent}enter = [`,
2534
- ...entries.map((entry) => `${indent} ${JSON.stringify(entry)},`),
2535
- `${indent}]`
2536
- ];
2514
+ function normalizeHookScript(script) {
2515
+ const trimmed = script.trim();
2516
+ if (/codegraph\.sh/.test(trimmed)) return CODEGRAPH_SCRIPT;
2517
+ return trimmed;
2537
2518
  }
2538
- function upsertLinkAgentfilesHooks(text3) {
2539
- const lines = text3.split("\n");
2540
- const hooksStart = lines.findIndex((line) => /^\[hooks\]$/.test(line.trim()));
2541
- if (hooksStart === -1) return insertTomlBlockBeforeVersioning(text3, LINK_AGENTFILES_HOOKS_BLOCK);
2542
- let hooksEnd = lines.length;
2543
- for (let i = hooksStart + 1; i < lines.length; i++) {
2544
- if (/^\[[^\]]+\]/.test(lines[i].trim())) {
2545
- hooksEnd = i;
2546
- break;
2519
+ function tomlValueSpanEnd(lines, start, limit) {
2520
+ let depth = 0;
2521
+ let j = start;
2522
+ for (; j < limit; j++) {
2523
+ for (const ch of stripTomlStringsAndComments(lines[j])) {
2524
+ if (ch === "[") depth++;
2525
+ else if (ch === "]") depth--;
2547
2526
  }
2527
+ if (depth <= 0) break;
2548
2528
  }
2549
- let enterStart = -1;
2550
- let enterEnd = -1;
2551
- for (let i = hooksStart + 1; i < hooksEnd; i++) {
2552
- if (!/^\s*enter\s*=/.test(lines[i])) continue;
2553
- enterStart = i;
2554
- enterEnd = i + 1;
2555
- const afterEquals = lines[i].slice(lines[i].indexOf("=") + 1);
2556
- if (afterEquals.includes("[") && !afterEquals.includes("]")) {
2557
- while (enterEnd < hooksEnd && !lines[enterEnd].includes("]")) enterEnd++;
2558
- if (enterEnd < hooksEnd) enterEnd++;
2529
+ return Math.min(j, limit - 1) + 1;
2530
+ }
2531
+ function stripHookBlocks(text3) {
2532
+ const lines = text3.split("\n");
2533
+ const enter = [];
2534
+ const leave = [];
2535
+ const drop = new Array(lines.length).fill(false);
2536
+ const isHeader = (line) => /^\[/.test(line.trim());
2537
+ const dropPrecedingComments = (idx) => {
2538
+ for (let k = idx - 1; k >= 0 && !drop[k] && lines[k].trim().startsWith("#"); k--) drop[k] = true;
2539
+ };
2540
+ for (let i = 0; i < lines.length; i++) {
2541
+ const trimmed = lines[i].trim();
2542
+ const tableMatch = /^\[\[\s*hooks\.(enter|leave)\s*\]\]$/.exec(trimmed);
2543
+ if (tableMatch) {
2544
+ const bucket = tableMatch[1] === "enter" ? enter : leave;
2545
+ dropPrecedingComments(i);
2546
+ drop[i] = true;
2547
+ let j = i + 1;
2548
+ for (; j < lines.length; j++) {
2549
+ const body = lines[j].trim();
2550
+ if (body === "" || body.startsWith("#") || isHeader(lines[j])) break;
2551
+ drop[j] = true;
2552
+ const scriptMatch = /^\s*script\s*=\s*(.+)$/.exec(lines[j]);
2553
+ if (scriptMatch) {
2554
+ const value = extractTomlStrings(scriptMatch[1])[0];
2555
+ if (value !== void 0) bucket.push(value);
2556
+ }
2557
+ }
2558
+ i = j - 1;
2559
+ continue;
2560
+ }
2561
+ if (trimmed === "[hooks]") {
2562
+ dropPrecedingComments(i);
2563
+ let j = i + 1;
2564
+ let lastDrop = i;
2565
+ while (j < lines.length && !isHeader(lines[j])) {
2566
+ const keyMatch = /^\s*(enter|leave)\s*=/.exec(lines[j]);
2567
+ if (keyMatch) {
2568
+ const bucket = keyMatch[1] === "enter" ? enter : leave;
2569
+ const end = tomlValueSpanEnd(lines, j, lines.length);
2570
+ for (const value of extractTomlStrings(lines.slice(j, end).join("\n"))) bucket.push(value);
2571
+ lastDrop = end - 1;
2572
+ j = end;
2573
+ } else if (/^\s*\]\s*$/.test(lines[j])) {
2574
+ lastDrop = j;
2575
+ j++;
2576
+ } else {
2577
+ j++;
2578
+ }
2579
+ }
2580
+ for (let k = i; k <= lastDrop; k++) drop[k] = true;
2581
+ i = j - 1;
2582
+ continue;
2559
2583
  }
2560
- break;
2561
- }
2562
- const existingBlock = enterStart >= 0 ? lines.slice(enterStart, enterEnd).join("\n") : "";
2563
- const preserved = extractTomlStrings(existingBlock).filter((entry) => !isManagedHookEntry(entry));
2564
- const merged = [...LINK_AGENTFILES_HOOK_ENTRIES];
2565
- for (const entry of preserved) {
2566
- if (!merged.includes(entry)) merged.push(entry);
2567
2584
  }
2568
- const indent = enterStart >= 0 ? lines[enterStart].match(/^\s*/)?.[0] ?? "" : "";
2569
- const rendered = renderHookEntries(merged, indent);
2570
- if (enterStart >= 0) {
2571
- return lines.slice(0, enterStart).concat(rendered, lines.slice(enterEnd)).join("\n").replace(/\n{3,}/g, "\n\n").replace(/\n+$/, "\n");
2585
+ const kept = lines.filter((_, idx) => !drop[idx]).join("\n").replace(/\n{3,}/g, "\n\n").replace(/\n+$/, "\n");
2586
+ return { text: kept, enter, leave };
2587
+ }
2588
+ function renderHookTables(scripts, kind) {
2589
+ return scripts.map((script) => `[[hooks.${kind}]]
2590
+ script = ${JSON.stringify(script)}`);
2591
+ }
2592
+ function dedupePreserve(scripts) {
2593
+ const out = [];
2594
+ for (const script of scripts) {
2595
+ if (script && !out.includes(script)) out.push(script);
2572
2596
  }
2573
- return lines.slice(0, hooksStart + 1).concat(rendered, lines.slice(hooksStart + 1)).join("\n").replace(/\n{3,}/g, "\n\n").replace(/\n+$/, "\n");
2597
+ return out;
2598
+ }
2599
+ function upsertLinkAgentfilesHooks(text3) {
2600
+ const { text: stripped, enter, leave } = stripHookBlocks(text3);
2601
+ const preservedEnter = enter.map(normalizeHookScript).filter((script) => !isManagedHookEntry(script));
2602
+ const enterScripts = dedupePreserve([...LINK_AGENTFILES_HOOK_ENTRIES, ...preservedEnter]);
2603
+ const leaveScripts = dedupePreserve(leave.map(normalizeHookScript));
2604
+ const block = [
2605
+ HOOKS_COMMENT_HEADER,
2606
+ ...renderHookTables(enterScripts, "enter"),
2607
+ ...renderHookTables(leaveScripts, "leave")
2608
+ ].join("\n");
2609
+ return insertTomlBlockBeforeVersioning(stripped, block);
2574
2610
  }
2575
2611
  function upsertLinkAgentfilesBlock(text3, ctx) {
2576
2612
  const withPath = upsertMisePath(text3, requiredMisePathEntries(ctx));
2577
- if (withPath.includes(LINK_AGENTFILES_BLOCK)) return withPath;
2578
2613
  let cleaned = removeTomlSection(withPath, /^\[tasks\.link-agentfiles\]$/, /link-agentfiles/, { includePrecedingComments: false });
2579
2614
  cleaned = removeTomlSection(cleaned, /^\[\[watch_files\]\]$/, /AGENTS\.md/, { includePrecedingComments: false });
2580
2615
  cleaned = upsertLinkAgentfilesHooks(cleaned);
@@ -3074,8 +3109,8 @@ var RULES = [
3074
3109
  const pathValues = [...(text3.match(/^_\.path\s*=\s*\[([^\]]*)\]/m)?.[1] ?? "").matchAll(/"([^"]+)"/g)].map((match) => match[1]);
3075
3110
  const missingPathValues = requiredMisePathEntries(ctx).filter((value) => !pathValues.includes(value));
3076
3111
  if (missingPathValues.length) details.push(`[env]._.path should include ${missingPathValues.join(", ")}`);
3077
- if (!text3.includes('"{{config_root}}/.mise/scripts/link-agentfiles.sh"')) details.push("link-agentfiles must use raw {{config_root}} guard");
3078
- if (!text3.includes("op inject -i .env.op > .env")) details.push("[hooks].enter must materialize .env from .env.op");
3112
+ if (!text3.includes("'{{config_root}}/.mise/scripts/link-agentfiles.sh'")) details.push("link-agentfiles hook must use single-quoted {{config_root}} guard");
3113
+ if (!text3.includes("op inject -i .env.op > .env")) details.push("hooks.enter must materialize .env from .env.op");
3079
3114
  if (!text3.includes('patterns = ["AGENTS.md"]')) details.push("watch_files must monitor AGENTS.md");
3080
3115
  if (!text3.includes('task = "link-agentfiles"')) details.push("watch_files must dispatch link-agentfiles task");
3081
3116
  return {
@@ -2049,72 +2049,48 @@ 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
2051
  import YAML2 from "yaml";
2052
- var LINK_AGENTFILES_BLOCK = `# This block will handle the linking of
2052
+ var LINK_AGENTFILES_SCRIPT = "'{{config_root}}/.mise/scripts/link-agentfiles.sh'";
2053
+ var OP_INJECT_SCRIPT = "op inject -i .env.op > .env";
2054
+ var CODEGRAPH_SCRIPT = "[ -f '{{config_root}}/.mise/scripts/codegraph.sh' ] && '{{config_root}}/.mise/scripts/codegraph.sh' || true";
2055
+ var HOOKS_COMMENT_HEADER = `# This block will handle the linking of
2053
2056
  # agent files to the main AGENTS.md file.
2054
2057
  #
2055
2058
  # TODO: Ensure this works for all levels of nesting.
2056
2059
  # i.e. All linked agent files MUST be siblings at
2057
- # any given level of nesting.
2058
- [hooks]
2059
- enter = [
2060
- "{{config_root}}/.mise/scripts/link-agentfiles.sh",
2061
- "op inject -i .env.op > .env",
2062
- ]
2063
-
2064
- [[watch_files]]
2065
- patterns = ["AGENTS.md"]
2066
- task = "link-agentfiles"
2067
-
2068
- [tasks.link-agentfiles]
2069
- description = "Symlink all agent files to AGENTS.md"
2070
- run = "{{config_root}}/.mise/scripts/link-agentfiles.sh"`;
2071
- var LINK_AGENTFILES_HOOK_ENTRIES = [
2072
- "{{config_root}}/.mise/scripts/link-agentfiles.sh",
2073
- "op inject -i .env.op > .env"
2074
- ];
2075
- var LINK_AGENTFILES_HOOKS_BLOCK = `# This block will handle the linking of
2076
- # agent files to the main AGENTS.md file.
2077
- #
2078
- # TODO: Ensure this works for all levels of nesting.
2079
- # i.e. All linked agent files MUST be siblings at
2080
- # any given level of nesting.
2081
- [hooks]
2082
- enter = [
2083
- "{{config_root}}/.mise/scripts/link-agentfiles.sh",
2084
- "op inject -i .env.op > .env",
2085
- ]`;
2060
+ # any given level of nesting.`;
2061
+ var LINK_AGENTFILES_HOOK_ENTRIES = [LINK_AGENTFILES_SCRIPT, OP_INJECT_SCRIPT];
2086
2062
  var LINK_AGENTFILES_WATCH_TASK_BLOCK = `[[watch_files]]
2087
2063
  patterns = ["AGENTS.md"]
2088
2064
  task = "link-agentfiles"
2089
2065
 
2090
2066
  [tasks.link-agentfiles]
2091
2067
  description = "Symlink all agent files to AGENTS.md"
2092
- run = "{{config_root}}/.mise/scripts/link-agentfiles.sh"`;
2068
+ run = "'{{config_root}}/.mise/scripts/link-agentfiles.sh'"`;
2093
2069
  var VERSIONING_BLOCK = `# >>> mise-versioning >>> (managed block \u2014 do not edit by hand; re-run init to update)
2094
2070
  [tasks."version"]
2095
2071
  description = "Print the current version (vX.Y.Z)"
2096
- run = "{{config_root}}/.mise/scripts/versioning.sh current"
2072
+ run = "'{{config_root}}/.mise/scripts/versioning.sh' current"
2097
2073
 
2098
2074
  [tasks."version:bump"]
2099
2075
  description = "Bump patch version: vX.Y.Z -> vX.Y.(Z+1)"
2100
2076
  alias = "version:bump-patch"
2101
- run = "{{config_root}}/.mise/scripts/versioning.sh bump patch"
2077
+ run = "'{{config_root}}/.mise/scripts/versioning.sh' bump patch"
2102
2078
 
2103
2079
  [tasks."version:bump-minor"]
2104
2080
  description = "Bump minor version: vX.Y.Z -> vX.(Y+1).0"
2105
- run = "{{config_root}}/.mise/scripts/versioning.sh bump minor"
2081
+ run = "'{{config_root}}/.mise/scripts/versioning.sh' bump minor"
2106
2082
 
2107
2083
  [tasks."version:bump-major"]
2108
2084
  description = "Bump major version: vX.Y.Z -> v(X+1).0.0"
2109
- run = "{{config_root}}/.mise/scripts/versioning.sh bump major"
2085
+ run = "'{{config_root}}/.mise/scripts/versioning.sh' bump major"
2110
2086
 
2111
2087
  [tasks."version:check"]
2112
2088
  description = "Verify every versioned file is in parity"
2113
- run = "{{config_root}}/.mise/scripts/versioning.sh check"
2089
+ run = "'{{config_root}}/.mise/scripts/versioning.sh' check"
2114
2090
 
2115
2091
  [tasks."version:sync"]
2116
2092
  description = "Force every versioned file up to the highest version"
2117
- run = "{{config_root}}/.mise/scripts/versioning.sh sync"
2093
+ run = "'{{config_root}}/.mise/scripts/versioning.sh' sync"
2118
2094
  # <<< mise-versioning <<<`;
2119
2095
  function resolvePjanglerRoot2() {
2120
2096
  let dir = dirname7(fileURLToPath4(import.meta.url));
@@ -2415,6 +2391,9 @@ function removeTomlSection(text2, headerPattern, marker, options) {
2415
2391
  break;
2416
2392
  }
2417
2393
  if (start === -1) return text2;
2394
+ while (end > start + 1 && (lines[end - 1].trim() === "" || lines[end - 1].trim().startsWith("#"))) {
2395
+ end--;
2396
+ }
2418
2397
  if (options?.includePrecedingComments) {
2419
2398
  while (start > 0 && lines[start - 1].trim().startsWith("#")) {
2420
2399
  start--;
@@ -2451,57 +2430,113 @@ function extractTomlStrings(text2) {
2451
2430
  }
2452
2431
  return values;
2453
2432
  }
2433
+ function stripTomlStringsAndComments(line) {
2434
+ return line.replace(/"(?:\\.|[^"\\])*"/g, '""').replace(/'[^']*'/g, "''").replace(/#.*$/, "");
2435
+ }
2454
2436
  function isManagedHookEntry(value) {
2455
2437
  const trimmed = value.trim();
2456
- return trimmed === "op inject -i .env.op > .env" || /(^|\/)link-agentfiles\.sh$/.test(trimmed);
2438
+ if (trimmed === OP_INJECT_SCRIPT) return true;
2439
+ return /link-agentfiles\.sh'?\s*$/.test(trimmed);
2457
2440
  }
2458
- function renderHookEntries(entries, indent = "") {
2459
- return [
2460
- `${indent}enter = [`,
2461
- ...entries.map((entry) => `${indent} ${JSON.stringify(entry)},`),
2462
- `${indent}]`
2463
- ];
2441
+ function normalizeHookScript(script) {
2442
+ const trimmed = script.trim();
2443
+ if (/codegraph\.sh/.test(trimmed)) return CODEGRAPH_SCRIPT;
2444
+ return trimmed;
2464
2445
  }
2465
- function upsertLinkAgentfilesHooks(text2) {
2466
- const lines = text2.split("\n");
2467
- const hooksStart = lines.findIndex((line) => /^\[hooks\]$/.test(line.trim()));
2468
- if (hooksStart === -1) return insertTomlBlockBeforeVersioning(text2, LINK_AGENTFILES_HOOKS_BLOCK);
2469
- let hooksEnd = lines.length;
2470
- for (let i = hooksStart + 1; i < lines.length; i++) {
2471
- if (/^\[[^\]]+\]/.test(lines[i].trim())) {
2472
- hooksEnd = i;
2473
- break;
2446
+ function tomlValueSpanEnd(lines, start, limit) {
2447
+ let depth = 0;
2448
+ let j = start;
2449
+ for (; j < limit; j++) {
2450
+ for (const ch of stripTomlStringsAndComments(lines[j])) {
2451
+ if (ch === "[") depth++;
2452
+ else if (ch === "]") depth--;
2474
2453
  }
2454
+ if (depth <= 0) break;
2475
2455
  }
2476
- let enterStart = -1;
2477
- let enterEnd = -1;
2478
- for (let i = hooksStart + 1; i < hooksEnd; i++) {
2479
- if (!/^\s*enter\s*=/.test(lines[i])) continue;
2480
- enterStart = i;
2481
- enterEnd = i + 1;
2482
- const afterEquals = lines[i].slice(lines[i].indexOf("=") + 1);
2483
- if (afterEquals.includes("[") && !afterEquals.includes("]")) {
2484
- while (enterEnd < hooksEnd && !lines[enterEnd].includes("]")) enterEnd++;
2485
- if (enterEnd < hooksEnd) enterEnd++;
2456
+ return Math.min(j, limit - 1) + 1;
2457
+ }
2458
+ function stripHookBlocks(text2) {
2459
+ const lines = text2.split("\n");
2460
+ const enter = [];
2461
+ const leave = [];
2462
+ const drop = new Array(lines.length).fill(false);
2463
+ const isHeader = (line) => /^\[/.test(line.trim());
2464
+ const dropPrecedingComments = (idx) => {
2465
+ for (let k = idx - 1; k >= 0 && !drop[k] && lines[k].trim().startsWith("#"); k--) drop[k] = true;
2466
+ };
2467
+ for (let i = 0; i < lines.length; i++) {
2468
+ const trimmed = lines[i].trim();
2469
+ const tableMatch = /^\[\[\s*hooks\.(enter|leave)\s*\]\]$/.exec(trimmed);
2470
+ if (tableMatch) {
2471
+ const bucket = tableMatch[1] === "enter" ? enter : leave;
2472
+ dropPrecedingComments(i);
2473
+ drop[i] = true;
2474
+ let j = i + 1;
2475
+ for (; j < lines.length; j++) {
2476
+ const body = lines[j].trim();
2477
+ if (body === "" || body.startsWith("#") || isHeader(lines[j])) break;
2478
+ drop[j] = true;
2479
+ const scriptMatch = /^\s*script\s*=\s*(.+)$/.exec(lines[j]);
2480
+ if (scriptMatch) {
2481
+ const value = extractTomlStrings(scriptMatch[1])[0];
2482
+ if (value !== void 0) bucket.push(value);
2483
+ }
2484
+ }
2485
+ i = j - 1;
2486
+ continue;
2487
+ }
2488
+ if (trimmed === "[hooks]") {
2489
+ dropPrecedingComments(i);
2490
+ let j = i + 1;
2491
+ let lastDrop = i;
2492
+ while (j < lines.length && !isHeader(lines[j])) {
2493
+ const keyMatch = /^\s*(enter|leave)\s*=/.exec(lines[j]);
2494
+ if (keyMatch) {
2495
+ const bucket = keyMatch[1] === "enter" ? enter : leave;
2496
+ const end = tomlValueSpanEnd(lines, j, lines.length);
2497
+ for (const value of extractTomlStrings(lines.slice(j, end).join("\n"))) bucket.push(value);
2498
+ lastDrop = end - 1;
2499
+ j = end;
2500
+ } else if (/^\s*\]\s*$/.test(lines[j])) {
2501
+ lastDrop = j;
2502
+ j++;
2503
+ } else {
2504
+ j++;
2505
+ }
2506
+ }
2507
+ for (let k = i; k <= lastDrop; k++) drop[k] = true;
2508
+ i = j - 1;
2509
+ continue;
2486
2510
  }
2487
- break;
2488
- }
2489
- const existingBlock = enterStart >= 0 ? lines.slice(enterStart, enterEnd).join("\n") : "";
2490
- const preserved = extractTomlStrings(existingBlock).filter((entry) => !isManagedHookEntry(entry));
2491
- const merged = [...LINK_AGENTFILES_HOOK_ENTRIES];
2492
- for (const entry of preserved) {
2493
- if (!merged.includes(entry)) merged.push(entry);
2494
2511
  }
2495
- const indent = enterStart >= 0 ? lines[enterStart].match(/^\s*/)?.[0] ?? "" : "";
2496
- const rendered = renderHookEntries(merged, indent);
2497
- if (enterStart >= 0) {
2498
- return lines.slice(0, enterStart).concat(rendered, lines.slice(enterEnd)).join("\n").replace(/\n{3,}/g, "\n\n").replace(/\n+$/, "\n");
2512
+ const kept = lines.filter((_, idx) => !drop[idx]).join("\n").replace(/\n{3,}/g, "\n\n").replace(/\n+$/, "\n");
2513
+ return { text: kept, enter, leave };
2514
+ }
2515
+ function renderHookTables(scripts, kind) {
2516
+ return scripts.map((script) => `[[hooks.${kind}]]
2517
+ script = ${JSON.stringify(script)}`);
2518
+ }
2519
+ function dedupePreserve(scripts) {
2520
+ const out = [];
2521
+ for (const script of scripts) {
2522
+ if (script && !out.includes(script)) out.push(script);
2499
2523
  }
2500
- return lines.slice(0, hooksStart + 1).concat(rendered, lines.slice(hooksStart + 1)).join("\n").replace(/\n{3,}/g, "\n\n").replace(/\n+$/, "\n");
2524
+ return out;
2525
+ }
2526
+ function upsertLinkAgentfilesHooks(text2) {
2527
+ const { text: stripped, enter, leave } = stripHookBlocks(text2);
2528
+ const preservedEnter = enter.map(normalizeHookScript).filter((script) => !isManagedHookEntry(script));
2529
+ const enterScripts = dedupePreserve([...LINK_AGENTFILES_HOOK_ENTRIES, ...preservedEnter]);
2530
+ const leaveScripts = dedupePreserve(leave.map(normalizeHookScript));
2531
+ const block = [
2532
+ HOOKS_COMMENT_HEADER,
2533
+ ...renderHookTables(enterScripts, "enter"),
2534
+ ...renderHookTables(leaveScripts, "leave")
2535
+ ].join("\n");
2536
+ return insertTomlBlockBeforeVersioning(stripped, block);
2501
2537
  }
2502
2538
  function upsertLinkAgentfilesBlock(text2, ctx) {
2503
2539
  const withPath = upsertMisePath(text2, requiredMisePathEntries(ctx));
2504
- if (withPath.includes(LINK_AGENTFILES_BLOCK)) return withPath;
2505
2540
  let cleaned = removeTomlSection(withPath, /^\[tasks\.link-agentfiles\]$/, /link-agentfiles/, { includePrecedingComments: false });
2506
2541
  cleaned = removeTomlSection(cleaned, /^\[\[watch_files\]\]$/, /AGENTS\.md/, { includePrecedingComments: false });
2507
2542
  cleaned = upsertLinkAgentfilesHooks(cleaned);
@@ -3001,8 +3036,8 @@ var RULES = [
3001
3036
  const pathValues = [...(text2.match(/^_\.path\s*=\s*\[([^\]]*)\]/m)?.[1] ?? "").matchAll(/"([^"]+)"/g)].map((match) => match[1]);
3002
3037
  const missingPathValues = requiredMisePathEntries(ctx).filter((value) => !pathValues.includes(value));
3003
3038
  if (missingPathValues.length) details.push(`[env]._.path should include ${missingPathValues.join(", ")}`);
3004
- if (!text2.includes('"{{config_root}}/.mise/scripts/link-agentfiles.sh"')) details.push("link-agentfiles must use raw {{config_root}} guard");
3005
- if (!text2.includes("op inject -i .env.op > .env")) details.push("[hooks].enter must materialize .env from .env.op");
3039
+ if (!text2.includes("'{{config_root}}/.mise/scripts/link-agentfiles.sh'")) details.push("link-agentfiles hook must use single-quoted {{config_root}} guard");
3040
+ if (!text2.includes("op inject -i .env.op > .env")) details.push("hooks.enter must materialize .env from .env.op");
3006
3041
  if (!text2.includes('patterns = ["AGENTS.md"]')) details.push("watch_files must monitor AGENTS.md");
3007
3042
  if (!text2.includes('task = "link-agentfiles"')) details.push("watch_files must dispatch link-agentfiles task");
3008
3043
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@delorenj/pjangler",
3
- "version": "1.2.14",
3
+ "version": "1.2.17",
4
4
  "description": "Project subsystem bootstrapper CLI",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -10,7 +10,7 @@ CODEX_HOME = { value = ".codex", path = true }
10
10
  # i.e. All linked agent files MUST be siblings at
11
11
  # any given level of nesting.
12
12
  [hooks]
13
- enter = '{{config_root}}/.mise/scripts/link-agentfiles.sh'
13
+ enter = "'{{config_root}}/.mise/scripts/link-agentfiles.sh'"
14
14
 
15
15
  [[watch_files]]
16
16
  patterns = ["AGENTS.md"]
@@ -11,21 +11,27 @@ _.file = [".env"]
11
11
  # TODO: Ensure this works for all levels of nesting.
12
12
  # i.e. All linked agent files MUST be siblings at
13
13
  # any given level of nesting.
14
- [hooks]
15
- enter = [
16
- "{% raw %}{{config_root}}{% endraw %}/.mise/scripts/link-agentfiles.sh",
17
- "op inject -i .env.op > .env",
18
- "[ -f {% raw %}{{config_root}}{% endraw %}/.mise/scripts/codegraph.sh ] && {% raw %}{{config_root}}{% endraw %}/.mise/scripts/codegraph.sh || true",
14
+ #
15
+ # NOTE: hooks are an array of [[hooks.enter]] tables (NOT enter = [ ... ]) and
16
+ # every {{config_root}} path is single-quoted so it survives a space in the
17
+ # project path mise runs each `script` via `sh -c`.
18
+ [[hooks.enter]]
19
+ script = "'{% raw %}{{config_root}}{% endraw %}/.mise/scripts/link-agentfiles.sh'"
20
+ [[hooks.enter]]
21
+ script = "op inject -i .env.op > .env"
22
+ [[hooks.enter]]
23
+ script = "[ -f '{% raw %}{{config_root}}{% endraw %}/.mise/scripts/codegraph.sh' ] && '{% raw %}{{config_root}}{% endraw %}/.mise/scripts/codegraph.sh' || true"
19
24
  {%- if agent_hooks_layer %}
20
- "{% raw %}{{config_root}}{% endraw %}/.mise/scripts/link-project-skills-to-clis.sh",
21
- "{% raw %}{{config_root}}{% endraw %}/.agents/hooks/sync.py --install --quiet",
25
+ [[hooks.enter]]
26
+ script = "'{% raw %}{{config_root}}{% endraw %}/.mise/scripts/link-project-skills-to-clis.sh'"
27
+ [[hooks.enter]]
28
+ script = "'{% raw %}{{config_root}}{% endraw %}/.agents/hooks/sync.py' --install --quiet"
22
29
  {%- endif %}
23
- ]
24
30
  {%- if agent_hooks_layer %}
25
- leave = [
26
- "{% raw %}{{config_root}}{% endraw %}/.mise/scripts/unlink-project-skills-from-clis.sh",
27
- "{% raw %}{{config_root}}{% endraw %}/.agents/hooks/sync.py --uninstall --quiet",
28
- ]
31
+ [[hooks.leave]]
32
+ script = "'{% raw %}{{config_root}}{% endraw %}/.mise/scripts/unlink-project-skills-from-clis.sh'"
33
+ [[hooks.leave]]
34
+ script = "'{% raw %}{{config_root}}{% endraw %}/.agents/hooks/sync.py' --uninstall --quiet"
29
35
  {%- endif %}
30
36
 
31
37
  [[watch_files]]
@@ -39,34 +45,34 @@ task = "hooks-sync"
39
45
  {% endif %}
40
46
  [tasks.link-agentfiles]
41
47
  description = "Symlink all agent files to AGENTS.md"
42
- run = "{% raw %}{{config_root}}{% endraw %}/.mise/scripts/link-agentfiles.sh"
48
+ run = "'{% raw %}{{config_root}}{% endraw %}/.mise/scripts/link-agentfiles.sh'"
43
49
  {% if agent_hooks_layer %}
44
50
  # --- Project-scoped agent hooks + skill fan-out (see .agents/hooks/README.md) ---
45
51
 
46
52
  [tasks.hooks-sync]
47
53
  description = "Fan out .agents/hooks/hooks.master.json to each agent CLI (claude committed; codex/kimi injected; hermes adapter)"
48
- run = "{% raw %}{{config_root}}{% endraw %}/.agents/hooks/sync.py --install"
54
+ run = "'{% raw %}{{config_root}}{% endraw %}/.agents/hooks/sync.py' --install"
49
55
 
50
56
  [tasks.hooks-check]
51
57
  description = "Drift gate: verify generated hook configs match hooks.master.json (CI-safe, read-only)"
52
- run = "{% raw %}{{config_root}}{% endraw %}/.agents/hooks/sync.py --check"
58
+ run = "'{% raw %}{{config_root}}{% endraw %}/.agents/hooks/sync.py' --check"
53
59
 
54
60
  [tasks.hooks-uninstall]
55
61
  description = "Remove per-user agent-hook injections (codex/kimi/hermes)"
56
- run = "{% raw %}{{config_root}}{% endraw %}/.agents/hooks/sync.py --uninstall"
62
+ run = "'{% raw %}{{config_root}}{% endraw %}/.agents/hooks/sync.py' --uninstall"
57
63
 
58
64
  [tasks.link-project-skills-to-clis]
59
65
  description = "Fan .agents/skills out to each agent CLI; honors local.json defer_to_global + disabled"
60
- run = "{% raw %}{{config_root}}{% endraw %}/.mise/scripts/link-project-skills-to-clis.sh"
66
+ run = "'{% raw %}{{config_root}}{% endraw %}/.mise/scripts/link-project-skills-to-clis.sh'"
61
67
 
62
68
  [tasks.unlink-project-skills-from-clis]
63
69
  description = "Remove project skill symlinks from shared per-CLI dirs"
64
- run = "{% raw %}{{config_root}}{% endraw %}/.mise/scripts/unlink-project-skills-from-clis.sh"
70
+ run = "'{% raw %}{{config_root}}{% endraw %}/.mise/scripts/unlink-project-skills-from-clis.sh'"
65
71
 
66
72
  [tasks.skills-relink]
67
73
  description = "Re-fan the project skill set to all CLIs"
68
- run = "{% raw %}{{config_root}}{% endraw %}/.mise/scripts/link-project-skills-to-clis.sh"
74
+ run = "'{% raw %}{{config_root}}{% endraw %}/.mise/scripts/link-project-skills-to-clis.sh'"
69
75
  {% endif %}
70
76
  [tasks.hindsight-setup]
71
77
  description = "Provision this dev's shared project Hindsight key from 1Password into .env (op inject)"
72
- run = "{% raw %}{{config_root}}{% endraw %}/.mise/scripts/hindsight-setup.sh"
78
+ run = "'{% raw %}{{config_root}}{% endraw %}/.mise/scripts/hindsight-setup.sh'"