@delorenj/pjangler 1.2.19 → 1.2.21
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 +642 -291
- package/dist/mcp-server.js +649 -298
- package/package.json +1 -1
- package/templates/commonproject/copier.yml +3 -4
- package/templates/commonproject/template/.agents/hooks/README.md +13 -26
- package/templates/commonproject/template/.agents/hooks/lib/local-config.sh +4 -14
- package/templates/commonproject/template/.agents/hooks/sync.py +5 -6
- package/templates/commonproject/template/.agents/local.example.json +2 -8
- package/templates/commonproject/template/.agents/skills.json +6 -0
- package/templates/commonproject/template/mise.toml.jinja +9 -16
- package/templates/hermes-agent/template/.scripts/heartbeat.sh +11 -0
- package/templates/hermes-agent/template/.scripts/momo-wip-lock.py +137 -0
- package/templates/hermes-agent/template/.scripts/sentinel.prompt.md.jinja +21 -0
- package/templates/hermes-agent/template/SOUL.md.jinja +34 -13
- package/templates/commonproject/template/.mise/scripts/link-project-skills-to-clis.sh +0 -110
- package/templates/commonproject/template/.mise/scripts/unlink-project-skills-from-clis.sh +0 -45
package/dist/index.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { spawnSync as spawnSync7 } from "node:child_process";
|
|
5
|
-
import { existsSync as
|
|
6
|
-
import { basename as basename4, join as
|
|
5
|
+
import { existsSync as existsSync11, readFileSync as readFileSync8, statSync as statSync2 } from "node:fs";
|
|
6
|
+
import { basename as basename4, join as join15, resolve as resolve3 } from "node:path";
|
|
7
7
|
import { Command as Command3 } from "commander";
|
|
8
8
|
|
|
9
9
|
// src/commands/hermes/types.ts
|
|
@@ -410,97 +410,284 @@ var AddMiseCodegraphScript = class extends Command {
|
|
|
410
410
|
};
|
|
411
411
|
}
|
|
412
412
|
const content = `#!/usr/bin/env bash
|
|
413
|
-
#
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
413
|
+
# Auto-generated by pjangler
|
|
414
|
+
|
|
415
|
+
REPO_ROOT="$(pwd)"
|
|
416
|
+
PROJECT_NAME="$(basename "$REPO_ROOT")"
|
|
417
|
+
CONTAINER_NAME="codegraph-mcp-$PROJECT_NAME"
|
|
418
|
+
CACHE_DIR="$REPO_ROOT/.codegraph"
|
|
419
|
+
|
|
420
|
+
# Deterministically generate a port based on the repository path
|
|
421
|
+
PORT=$(echo -n "$REPO_ROOT" | md5sum | awk '{print $1}' | tr -d 'a-f' | cut -c1-4)
|
|
422
|
+
# Ensure port is > 1024
|
|
423
|
+
PORT=$(( (PORT % 60000) + 1025 ))
|
|
424
|
+
|
|
425
|
+
if ! docker ps --format '{{.Names}}' | grep -q "^$CONTAINER_NAME$"; then
|
|
426
|
+
echo "\u{1F680} Starting CodeGraph MCP Server on port $PORT..."
|
|
427
|
+
|
|
428
|
+
# Ensure the container isn't lingering in a stopped state
|
|
429
|
+
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
|
430
|
+
|
|
431
|
+
# Run the true Colby McHenry CodeGraph Docker image
|
|
432
|
+
# which we built locally as colbymchenry-codegraph-mcp:latest
|
|
433
|
+
docker run -d \\
|
|
434
|
+
--name "$CONTAINER_NAME" \\
|
|
435
|
+
--restart unless-stopped \\
|
|
436
|
+
-p "$PORT:8045" \\
|
|
437
|
+
-v "$REPO_ROOT:/repo" \\
|
|
438
|
+
colbymchenry-codegraph-mcp:latest >/dev/null
|
|
439
|
+
|
|
440
|
+
echo "\u2705 CodeGraph MCP running in background. SSE URL: http://localhost:$PORT/sse"
|
|
441
|
+
fi
|
|
442
|
+
|
|
443
|
+
# Run init inside the container to ensure the index is bootstrapped.
|
|
444
|
+
# We run this using the standard codegraph CLI inside the container
|
|
445
|
+
docker exec "$CONTAINER_NAME" codegraph init -i /repo >/dev/null 2>&1 || true
|
|
446
|
+
|
|
447
|
+
# Wire up the MCP server to local agents
|
|
448
|
+
WIRE_SCRIPT="$(dirname "$0")/codegraph-wire.sh"
|
|
449
|
+
if [ -x "$WIRE_SCRIPT" ]; then
|
|
450
|
+
"$WIRE_SCRIPT"
|
|
451
|
+
fi
|
|
452
|
+
`;
|
|
453
|
+
this.writeFile(filePath, content);
|
|
454
|
+
if (!this.context.dryRun) {
|
|
455
|
+
chmodSync(join3(this.context.targetDir, filePath), 493);
|
|
456
|
+
}
|
|
457
|
+
return {
|
|
458
|
+
success: true,
|
|
459
|
+
message: this.formatMessage(this.context.dryRun ? "Would create .mise/scripts/codegraph.sh" : "\u2705 Created .mise/scripts/codegraph.sh"),
|
|
460
|
+
filePath
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
// src/commands/AddMiseCodegraphWireScript.ts
|
|
466
|
+
import { chmodSync as chmodSync2 } from "fs";
|
|
467
|
+
import { join as join4 } from "path";
|
|
468
|
+
var AddMiseCodegraphWireScript = class extends Command {
|
|
469
|
+
async invoke() {
|
|
470
|
+
const filePath = ".mise/scripts/codegraph-wire.sh";
|
|
471
|
+
if (this.fileExists(filePath) && !this.context.force) {
|
|
472
|
+
return {
|
|
473
|
+
success: false,
|
|
474
|
+
message: this.formatMessage("\u26A0\uFE0F .mise/scripts/codegraph-wire.sh already exists"),
|
|
475
|
+
filePath
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
const content = `#!/usr/bin/env bash
|
|
479
|
+
# Mise enter hook: Auto-wire CodeGraph MCP Server to local agents.
|
|
480
|
+
# Detects Claude Code, Codex, Gemini, Kimi, and OpenCode and configures
|
|
481
|
+
# them to use the local SSE endpoint for the current project.
|
|
419
482
|
|
|
420
483
|
set -euo pipefail
|
|
421
484
|
|
|
422
485
|
REPO_ROOT="\${MISE_PROJECT_ROOT:-$(cd "$(dirname "$0")/../.." && pwd)}"
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
486
|
+
|
|
487
|
+
# Compute the same port as the container script
|
|
488
|
+
PORT_HASH=$(echo -n "$REPO_ROOT" | md5sum | awk '{print $1}')
|
|
489
|
+
PORT_DEC=$(printf "%d" "0x\${PORT_HASH:0:4}")
|
|
490
|
+
PORT=$(( 8045 + (PORT_DEC % 955) ))
|
|
491
|
+
|
|
492
|
+
SSE_URL="http://localhost:$PORT/sse"
|
|
493
|
+
|
|
494
|
+
inject_sse() {
|
|
495
|
+
local target="$1"
|
|
496
|
+
local agent="$2"
|
|
497
|
+
|
|
498
|
+
if [ ! -f "$target" ]; then
|
|
499
|
+
mkdir -p "$(dirname "$target")"
|
|
500
|
+
echo '{"mcpServers": {}}' > "$target"
|
|
501
|
+
fi
|
|
502
|
+
|
|
503
|
+
# Ensure the file is valid JSON (fail gracefully if it's garbled)
|
|
504
|
+
if ! jq . "$target" >/dev/null 2>&1; then
|
|
505
|
+
echo "[mise] WARNING: $target is not valid JSON, skipping $agent wiring" >&2
|
|
506
|
+
return
|
|
437
507
|
fi
|
|
508
|
+
|
|
509
|
+
# Inject or update the codegraph server
|
|
510
|
+
jq --arg url "$SSE_URL" '.mcpServers.codegraph = {"type": "sse", "url": $url}' "$target" > "$target.tmp" && mv "$target.tmp" "$target"
|
|
511
|
+
echo "[mise] Wired CodeGraph SSE for $agent -> $target"
|
|
438
512
|
}
|
|
439
513
|
|
|
440
|
-
#
|
|
441
|
-
|
|
442
|
-
if command -v codegraph >/dev/null 2>&1; then
|
|
443
|
-
return 0
|
|
444
|
-
fi
|
|
514
|
+
# 1. Claude Code (Project-scoped)
|
|
515
|
+
inject_sse "$REPO_ROOT/.claude.json" "Claude Code"
|
|
445
516
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
export PATH="$PROJECT_BIN_DIR:$PATH"
|
|
449
|
-
return 0
|
|
450
|
-
fi
|
|
517
|
+
# 2. Codex (Global)
|
|
518
|
+
inject_sse "$HOME/.codex/mcp.json" "Codex"
|
|
451
519
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
if [ -x "$d/codegraph" ]; then
|
|
455
|
-
export PATH="$d:$PATH"
|
|
456
|
-
return 0
|
|
457
|
-
fi
|
|
458
|
-
done
|
|
520
|
+
# 3. Gemini (Global)
|
|
521
|
+
inject_sse "$HOME/.gemini/config/mcp.json" "Gemini"
|
|
459
522
|
|
|
460
|
-
|
|
461
|
-
|
|
523
|
+
# 4. Kimi (Global)
|
|
524
|
+
inject_sse "$HOME/.kimi-code/mcp.json" "Kimi"
|
|
462
525
|
|
|
463
|
-
#
|
|
464
|
-
|
|
465
|
-
init_project() {
|
|
466
|
-
local err_file
|
|
467
|
-
err_file="$(mktemp)"
|
|
468
|
-
trap 'rm -f "$err_file"' RETURN
|
|
526
|
+
# 5. OpenCode (Global)
|
|
527
|
+
inject_sse "$HOME/.opencode/mcp.json" "OpenCode"
|
|
469
528
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
fi
|
|
529
|
+
# 6. Cursor (Global)
|
|
530
|
+
inject_sse "$HOME/.cursor/mcp.json" "Cursor"
|
|
473
531
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
ensure_codegraph
|
|
477
|
-
codegraph init -i "$REPO_ROOT"
|
|
478
|
-
return 0
|
|
479
|
-
fi
|
|
532
|
+
# 7. VSCode native MCP (Global)
|
|
533
|
+
inject_sse "$HOME/.vscode/mcp.json" "VSCode"
|
|
480
534
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
}
|
|
535
|
+
# 8. Cline (VSCode extension)
|
|
536
|
+
inject_sse "$HOME/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json" "Cline"
|
|
484
537
|
|
|
485
|
-
|
|
538
|
+
# 9. Roo (VSCode extension)
|
|
539
|
+
inject_sse "$HOME/.config/Code/User/globalStorage/rooveterinaryinc.roo-cline/settings/mcp_settings.json" "Roo Code"
|
|
486
540
|
`;
|
|
487
541
|
this.writeFile(filePath, content);
|
|
488
542
|
if (!this.context.dryRun) {
|
|
489
|
-
|
|
543
|
+
chmodSync2(join4(this.context.targetDir, filePath), 493);
|
|
490
544
|
}
|
|
491
545
|
return {
|
|
492
546
|
success: true,
|
|
493
|
-
message: this.formatMessage(this.context.dryRun ? "Would create .mise/scripts/codegraph.sh" : "\u2705 Created .mise/scripts/codegraph.sh"),
|
|
547
|
+
message: this.formatMessage(this.context.dryRun ? "Would create .mise/scripts/codegraph-wire.sh" : "\u2705 Created .mise/scripts/codegraph-wire.sh"),
|
|
494
548
|
filePath
|
|
495
549
|
};
|
|
496
550
|
}
|
|
497
551
|
};
|
|
498
552
|
|
|
553
|
+
// src/commands/WireMiseOpInject.ts
|
|
554
|
+
import { join as join5 } from "node:path";
|
|
555
|
+
import { existsSync as existsSync3, readFileSync, writeFileSync as writeFileSync3, chmodSync as chmodSync3 } from "node:fs";
|
|
556
|
+
var WireMiseOpInject = class _WireMiseOpInject extends Command {
|
|
557
|
+
static MARKER = "# pjangler:op-inject";
|
|
558
|
+
static CR = "{{config_root}}";
|
|
559
|
+
async invoke() {
|
|
560
|
+
const scriptPath = ".mise/scripts/inject-op-secrets.sh";
|
|
561
|
+
const fullScriptPath = join5(this.context.targetDir, scriptPath);
|
|
562
|
+
const scriptContent = `#!/usr/bin/env bash
|
|
563
|
+
# Resolve 1Password \`op://\` references in .env.op into .env.secrets, which mise
|
|
564
|
+
# loads via \`_.file\`.
|
|
565
|
+
#
|
|
566
|
+
# Runs from the mise enter hook: fail-open and non-interactive.
|
|
567
|
+
# No .env.op means no 1Password, so this exits before spending anything.
|
|
568
|
+
|
|
569
|
+
set -euo pipefail
|
|
570
|
+
|
|
571
|
+
REPO_ROOT="\${MISE_PROJECT_ROOT:-$(cd "$(dirname "$0")/../.." && pwd)}"
|
|
572
|
+
SRC="$REPO_ROOT/.env.op"
|
|
573
|
+
OUT="$REPO_ROOT/.env.secrets"
|
|
574
|
+
|
|
575
|
+
TTL_HOURS="\${OP_INJECT_TTL_HOURS:-12}"
|
|
576
|
+
|
|
577
|
+
[[ -f "$SRC" ]] || exit 0
|
|
578
|
+
command -v op >/dev/null 2>&1 || exit 0
|
|
579
|
+
|
|
580
|
+
if [[ -f "$OUT" && "$SRC" -ot "$OUT" ]]; then
|
|
581
|
+
if [[ "$TTL_HOURS" != "0" ]] && [[ -z "$(find "$OUT" -mmin "+$((TTL_HOURS * 60))" 2>/dev/null)" ]]; then
|
|
582
|
+
exit 0
|
|
583
|
+
fi
|
|
584
|
+
fi
|
|
585
|
+
|
|
586
|
+
op account list >/dev/null 2>&1 || exit 0
|
|
587
|
+
|
|
588
|
+
tmp="$(mktemp "\${OUT}.XXXXXX")"
|
|
589
|
+
trap 'rm -f "$tmp"' EXIT
|
|
590
|
+
chmod 600 "$tmp"
|
|
591
|
+
|
|
592
|
+
if op inject -i "$SRC" -o "$tmp" --force >/dev/null 2>&1; then
|
|
593
|
+
mv "$tmp" "$OUT"
|
|
594
|
+
trap - EXIT
|
|
595
|
+
else
|
|
596
|
+
echo "mise: op inject from .env.op failed \u2014 $(basename "$OUT") unchanged." >&2
|
|
597
|
+
echo " Check 'op signin', or that .env.op's op:// items are readable." >&2
|
|
598
|
+
echo " To disable: rm .env.op" >&2
|
|
599
|
+
fi
|
|
600
|
+
`;
|
|
601
|
+
this.writeFile(scriptPath, scriptContent);
|
|
602
|
+
if (!this.context.dryRun) {
|
|
603
|
+
chmodSync3(fullScriptPath, 493);
|
|
604
|
+
}
|
|
605
|
+
const misePath = join5(this.context.targetDir, "mise.toml");
|
|
606
|
+
if (!existsSync3(misePath)) {
|
|
607
|
+
return {
|
|
608
|
+
success: false,
|
|
609
|
+
message: "\u26A0\uFE0F No mise.toml found \u2014 run `pj init mise` first, then re-run."
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
let content = readFileSync(misePath, "utf8");
|
|
613
|
+
if (content.includes(_WireMiseOpInject.MARKER)) {
|
|
614
|
+
return { success: true, message: this.formatMessage("\u2713 mise.toml already wired for op-inject") };
|
|
615
|
+
}
|
|
616
|
+
const cr = _WireMiseOpInject.CR;
|
|
617
|
+
const envRe = /\[env\]\s*\n([\s\S]*?(?=\n\[|$))/;
|
|
618
|
+
if (envRe.test(content)) {
|
|
619
|
+
content = content.replace(envRe, (m) => {
|
|
620
|
+
let block = m;
|
|
621
|
+
if (!block.includes("_.file")) {
|
|
622
|
+
block += `
|
|
623
|
+
_.file = ['.env', '.env.secrets']
|
|
624
|
+
`;
|
|
625
|
+
} else if (!block.includes(".env.secrets")) {
|
|
626
|
+
block = block.replace(/(_\.file\s*=\s*\[)([^\]]*?)(\])/, (m2, prefix, files, suffix) => {
|
|
627
|
+
const added = files.trim() ? `${files}, '.env.secrets'` : `'.env.secrets'`;
|
|
628
|
+
return `${prefix}${added}${suffix}`;
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
return block;
|
|
632
|
+
});
|
|
633
|
+
} else {
|
|
634
|
+
const envBlock = `[env]
|
|
635
|
+
_.file = ['.env', '.env.secrets']
|
|
636
|
+
|
|
637
|
+
`;
|
|
638
|
+
if (content.includes("[tools]")) {
|
|
639
|
+
content = content.replace(/(\[tools\][\s\S]*?(?=\n\[|$))/, `$1
|
|
640
|
+
${envBlock}`);
|
|
641
|
+
} else {
|
|
642
|
+
content = envBlock + content;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
const enterAdds = ` '${cr}/.mise/scripts/inject-op-secrets.sh',`;
|
|
646
|
+
const enterRe = /(enter\s*=\s*\[[\s\S]*?)(\n[ \t]*\])/;
|
|
647
|
+
if (enterRe.test(content)) {
|
|
648
|
+
content = content.replace(enterRe, (_m, head, close) => {
|
|
649
|
+
const sep = /[,[]\s*$/.test(head) ? "" : ",";
|
|
650
|
+
return `${head}${sep}
|
|
651
|
+
${enterAdds}${close}`;
|
|
652
|
+
});
|
|
653
|
+
} else {
|
|
654
|
+
if (content.includes("[hooks]")) {
|
|
655
|
+
content = content.replace(/\[hooks\]/, `[hooks]
|
|
656
|
+
enter = [
|
|
657
|
+
${enterAdds}
|
|
658
|
+
]`);
|
|
659
|
+
} else {
|
|
660
|
+
content += `
|
|
661
|
+
[hooks]
|
|
662
|
+
enter = [
|
|
663
|
+
${enterAdds}
|
|
664
|
+
]
|
|
665
|
+
`;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
const appended = [
|
|
669
|
+
"",
|
|
670
|
+
_WireMiseOpInject.MARKER,
|
|
671
|
+
"[tasks.secrets-inject]",
|
|
672
|
+
'description = "Re-resolve .env.op secrets from 1Password into .env.secrets"',
|
|
673
|
+
`run = "OP_INJECT_TTL_HOURS=0 .mise/scripts/inject-op-secrets.sh"`,
|
|
674
|
+
_WireMiseOpInject.MARKER + ":end",
|
|
675
|
+
""
|
|
676
|
+
].join("\n");
|
|
677
|
+
content = content.replace(/\n*$/, "\n") + appended;
|
|
678
|
+
if (!this.context.dryRun) writeFileSync3(misePath, content);
|
|
679
|
+
return {
|
|
680
|
+
success: true,
|
|
681
|
+
message: this.formatMessage("\u2705 Wired mise.toml for op-inject (_.file, [hooks] enter, tasks.secrets-inject)")
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
};
|
|
685
|
+
|
|
499
686
|
// src/recipes/MiseRecipe.ts
|
|
500
687
|
var MiseRecipe = class extends Recipe {
|
|
501
688
|
constructor(context) {
|
|
502
689
|
super(context);
|
|
503
|
-
this.addIngredient(AddMiseToml).addIngredient(AddDotenv).addIngredient(AddMiseTasksStructure).addIngredient(AddMiseBaseToml).addIngredient(AddMiseBaseScript).addIngredient(AddMiseCodegraphScript);
|
|
690
|
+
this.addIngredient(AddMiseToml).addIngredient(AddDotenv).addIngredient(AddMiseTasksStructure).addIngredient(AddMiseBaseToml).addIngredient(AddMiseBaseScript).addIngredient(AddMiseCodegraphScript).addIngredient(AddMiseCodegraphWireScript).addIngredient(WireMiseOpInject);
|
|
504
691
|
}
|
|
505
692
|
printNextSteps() {
|
|
506
693
|
console.log("\u{1F389} Mise subsystem initialized successfully!");
|
|
@@ -728,12 +915,12 @@ var NodeRecipe = class extends Recipe {
|
|
|
728
915
|
};
|
|
729
916
|
|
|
730
917
|
// src/commands/hermes/PromptForAgentConfig.ts
|
|
731
|
-
import { basename, join as
|
|
732
|
-
import { readFileSync } from "node:fs";
|
|
918
|
+
import { basename, join as join6 } from "node:path";
|
|
919
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
733
920
|
import * as p from "@clack/prompts";
|
|
734
921
|
function detectTicketProvider(targetDir) {
|
|
735
922
|
try {
|
|
736
|
-
const t = JSON.parse(
|
|
923
|
+
const t = JSON.parse(readFileSync2(join6(targetDir, ".project.json"), "utf8"))?.ticket_provider?.type;
|
|
737
924
|
return t === "plane" || t === "trello" ? t : void 0;
|
|
738
925
|
} catch {
|
|
739
926
|
return void 0;
|
|
@@ -791,8 +978,8 @@ var PromptForAgentConfig = class extends Command {
|
|
|
791
978
|
// src/commands/hermes/RunCopierTemplate.ts
|
|
792
979
|
import { spawnSync } from "node:child_process";
|
|
793
980
|
import { homedir as homedir2 } from "node:os";
|
|
794
|
-
import { join as
|
|
795
|
-
import { existsSync as
|
|
981
|
+
import { join as join7, dirname as dirname3 } from "node:path";
|
|
982
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3 } from "node:fs";
|
|
796
983
|
import { fileURLToPath } from "node:url";
|
|
797
984
|
import * as p2 from "@clack/prompts";
|
|
798
985
|
function resolveVendoredTemplate(name) {
|
|
@@ -803,8 +990,8 @@ function resolveVendoredTemplate(name) {
|
|
|
803
990
|
return void 0;
|
|
804
991
|
}
|
|
805
992
|
for (let i = 0; i < 8; i++) {
|
|
806
|
-
const candidate =
|
|
807
|
-
if (
|
|
993
|
+
const candidate = join7(dir, "templates", name);
|
|
994
|
+
if (existsSync4(join7(candidate, "copier.yml"))) return candidate;
|
|
808
995
|
const parent = dirname3(dir);
|
|
809
996
|
if (parent === dir) break;
|
|
810
997
|
dir = parent;
|
|
@@ -823,7 +1010,7 @@ var RunCopierTemplate = class extends Command {
|
|
|
823
1010
|
message: "PromptForAgentConfig must run before RunCopierTemplate (targetRepo/role unset)"
|
|
824
1011
|
};
|
|
825
1012
|
}
|
|
826
|
-
const roleDir =
|
|
1013
|
+
const roleDir = join7(ctx.targetDir, "agents", "hermes", role);
|
|
827
1014
|
ctx.roleDir = roleDir;
|
|
828
1015
|
ctx.runtimeRepo = `delorenj/agent-hm-${targetRepo}-${role}`;
|
|
829
1016
|
const which = spawnSync("which", ["copier"], { encoding: "utf8" });
|
|
@@ -833,7 +1020,7 @@ var RunCopierTemplate = class extends Command {
|
|
|
833
1020
|
message: "\u2717 copier not found on PATH. Install with: `uv tool install copier` or `pip install copier`"
|
|
834
1021
|
};
|
|
835
1022
|
}
|
|
836
|
-
if (
|
|
1023
|
+
if (existsSync4(join7(roleDir, "role.yaml")) && !ctx.force) {
|
|
837
1024
|
if (ctx.yes) {
|
|
838
1025
|
ctx.force = true;
|
|
839
1026
|
} else {
|
|
@@ -860,9 +1047,9 @@ var RunCopierTemplate = class extends Command {
|
|
|
860
1047
|
SKIP_BLOODBANK: ctx.skipBloodbank ? "1" : "0",
|
|
861
1048
|
SKIP_SYSTEMD: ctx.skipSystemd ? "1" : "0"
|
|
862
1049
|
};
|
|
863
|
-
const LOCAL_TEMPLATE =
|
|
1050
|
+
const LOCAL_TEMPLATE = join7(homedir2(), "code", "hermes-agent-template");
|
|
864
1051
|
const vendored = resolveVendoredTemplate("hermes-agent");
|
|
865
|
-
const templateSrc = process.env.PJANGLER_HERMES_TEMPLATE || vendored || (
|
|
1052
|
+
const templateSrc = process.env.PJANGLER_HERMES_TEMPLATE || vendored || (existsSync4(join7(LOCAL_TEMPLATE, "copier.yml")) ? LOCAL_TEMPLATE : HERMES_AGENT_TEMPLATE);
|
|
866
1053
|
const args = [
|
|
867
1054
|
"copy",
|
|
868
1055
|
templateSrc,
|
|
@@ -893,7 +1080,7 @@ var RunCopierTemplate = class extends Command {
|
|
|
893
1080
|
message: this.formatMessage(`Would run: copier ${args.join(" ")}`)
|
|
894
1081
|
};
|
|
895
1082
|
}
|
|
896
|
-
mkdirSync3(
|
|
1083
|
+
mkdirSync3(join7(ctx.targetDir, "agents", "hermes"), { recursive: true });
|
|
897
1084
|
const spinner4 = p2.spinner();
|
|
898
1085
|
spinner4.start(`Running copier copy (target: agents/hermes/${role})`);
|
|
899
1086
|
const result = spawnSync("copier", args, {
|
|
@@ -917,14 +1104,14 @@ var RunCopierTemplate = class extends Command {
|
|
|
917
1104
|
};
|
|
918
1105
|
|
|
919
1106
|
// src/commands/hermes/UntrackHermesRuntimes.ts
|
|
920
|
-
import { existsSync as
|
|
921
|
-
import { join as
|
|
1107
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync4, readdirSync } from "fs";
|
|
1108
|
+
import { join as join8 } from "path";
|
|
922
1109
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
923
1110
|
var UntrackHermesRuntimes = class extends Command {
|
|
924
1111
|
async invoke() {
|
|
925
1112
|
const targetDir = this.context.targetDir;
|
|
926
|
-
const rolesDir =
|
|
927
|
-
if (!
|
|
1113
|
+
const rolesDir = join8(targetDir, "agents", "hermes");
|
|
1114
|
+
if (!existsSync5(rolesDir)) {
|
|
928
1115
|
return {
|
|
929
1116
|
success: true,
|
|
930
1117
|
message: "No Hermes agents found (no agents/hermes directory)."
|
|
@@ -940,9 +1127,9 @@ var UntrackHermesRuntimes = class extends Command {
|
|
|
940
1127
|
let modifiedAny = false;
|
|
941
1128
|
const details = [];
|
|
942
1129
|
for (const role of roles) {
|
|
943
|
-
const roleDir =
|
|
944
|
-
const runtimePath =
|
|
945
|
-
const gitignorePath =
|
|
1130
|
+
const roleDir = join8("agents", "hermes", role);
|
|
1131
|
+
const runtimePath = join8(roleDir, "runtime");
|
|
1132
|
+
const gitignorePath = join8(roleDir, ".gitignore");
|
|
946
1133
|
let isTracked = false;
|
|
947
1134
|
const lsResult = spawnSync2("git", ["ls-files", "--stage", runtimePath], {
|
|
948
1135
|
cwd: targetDir,
|
|
@@ -952,9 +1139,9 @@ var UntrackHermesRuntimes = class extends Command {
|
|
|
952
1139
|
isTracked = true;
|
|
953
1140
|
}
|
|
954
1141
|
let isIgnored = false;
|
|
955
|
-
const fullGitignorePath =
|
|
956
|
-
if (
|
|
957
|
-
const content =
|
|
1142
|
+
const fullGitignorePath = join8(targetDir, gitignorePath);
|
|
1143
|
+
if (existsSync5(fullGitignorePath)) {
|
|
1144
|
+
const content = readFileSync3(fullGitignorePath, "utf8");
|
|
958
1145
|
const lines = content.split(/\r?\n/).map((line) => line.trim());
|
|
959
1146
|
isIgnored = lines.includes("runtime/") || lines.includes("runtime");
|
|
960
1147
|
}
|
|
@@ -979,14 +1166,14 @@ var UntrackHermesRuntimes = class extends Command {
|
|
|
979
1166
|
details.push(`ignore runtime/ in agents/hermes/${role}/.gitignore`);
|
|
980
1167
|
if (!this.context.dryRun) {
|
|
981
1168
|
let content = "";
|
|
982
|
-
if (
|
|
983
|
-
content =
|
|
1169
|
+
if (existsSync5(fullGitignorePath)) {
|
|
1170
|
+
content = readFileSync3(fullGitignorePath, "utf8");
|
|
984
1171
|
}
|
|
985
1172
|
if (content && !content.endsWith("\n")) {
|
|
986
1173
|
content += "\n";
|
|
987
1174
|
}
|
|
988
1175
|
content += "runtime/\n";
|
|
989
|
-
|
|
1176
|
+
writeFileSync4(fullGitignorePath, content, "utf8");
|
|
990
1177
|
}
|
|
991
1178
|
}
|
|
992
1179
|
}
|
|
@@ -1008,8 +1195,8 @@ ${details.map((d) => ` - ${d}`).join("\n")}`
|
|
|
1008
1195
|
|
|
1009
1196
|
// src/commands/hermes/WireTelegram.ts
|
|
1010
1197
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
1011
|
-
import { join as
|
|
1012
|
-
import { existsSync as
|
|
1198
|
+
import { join as join9 } from "node:path";
|
|
1199
|
+
import { existsSync as existsSync6, unlinkSync } from "node:fs";
|
|
1013
1200
|
import * as p3 from "@clack/prompts";
|
|
1014
1201
|
var WireTelegram = class extends Command {
|
|
1015
1202
|
async invoke() {
|
|
@@ -1097,15 +1284,15 @@ var WireTelegram = class extends Command {
|
|
|
1097
1284
|
if (p3.isCancel(allowedAnswer)) {
|
|
1098
1285
|
return { success: false, message: "\u2717 Aborted; Telegram step deferred." };
|
|
1099
1286
|
}
|
|
1100
|
-
const script =
|
|
1101
|
-
if (!
|
|
1287
|
+
const script = join9(roleDir, ".scripts", "30-telegram.sh");
|
|
1288
|
+
if (!existsSync6(script)) {
|
|
1102
1289
|
return {
|
|
1103
1290
|
success: false,
|
|
1104
1291
|
message: `\u2717 ${script} not found. Did copier finish? Re-run with --skip-runtime-repo=0 if you skipped it.`
|
|
1105
1292
|
};
|
|
1106
1293
|
}
|
|
1107
|
-
const marker =
|
|
1108
|
-
if (
|
|
1294
|
+
const marker = join9(roleDir, ".scripts", ".done-30-telegram");
|
|
1295
|
+
if (existsSync6(marker)) unlinkSync(marker);
|
|
1109
1296
|
const spinner4 = p3.spinner();
|
|
1110
1297
|
spinner4.start("Verifying token + wiring profile");
|
|
1111
1298
|
const result = spawnSync3("bash", [script], {
|
|
@@ -1132,8 +1319,8 @@ function cap(s) {
|
|
|
1132
1319
|
|
|
1133
1320
|
// src/commands/hermes/WireEmail.ts
|
|
1134
1321
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
1135
|
-
import { join as
|
|
1136
|
-
import { existsSync as
|
|
1322
|
+
import { join as join10 } from "node:path";
|
|
1323
|
+
import { existsSync as existsSync7, unlinkSync as unlinkSync2 } from "node:fs";
|
|
1137
1324
|
import * as p4 from "@clack/prompts";
|
|
1138
1325
|
var WireEmail = class extends Command {
|
|
1139
1326
|
async invoke() {
|
|
@@ -1148,8 +1335,8 @@ var WireEmail = class extends Command {
|
|
|
1148
1335
|
if (!targetRepo || !role || !roleDir) {
|
|
1149
1336
|
return { success: false, message: "Cannot wire email: missing target_repo/role/roleDir" };
|
|
1150
1337
|
}
|
|
1151
|
-
const script =
|
|
1152
|
-
if (!
|
|
1338
|
+
const script = join10(roleDir, ".scripts", "50-email.sh");
|
|
1339
|
+
if (!existsSync7(script)) {
|
|
1153
1340
|
return { success: false, message: `\u2717 ${script} not found` };
|
|
1154
1341
|
}
|
|
1155
1342
|
let token = process.env.CF_EMAIL_ROUTING_TOKEN;
|
|
@@ -1211,8 +1398,8 @@ var WireEmail = class extends Command {
|
|
|
1211
1398
|
}
|
|
1212
1399
|
}
|
|
1213
1400
|
}
|
|
1214
|
-
const marker =
|
|
1215
|
-
if (
|
|
1401
|
+
const marker = join10(roleDir, ".scripts", ".done-50-email");
|
|
1402
|
+
if (existsSync7(marker)) unlinkSync2(marker);
|
|
1216
1403
|
const spinner4 = p4.spinner();
|
|
1217
1404
|
spinner4.start("Creating Cloudflare Email Routing rule");
|
|
1218
1405
|
const result = spawnSync4("bash", [script], {
|
|
@@ -1299,15 +1486,15 @@ var HermesAgentRecipe = class extends Recipe {
|
|
|
1299
1486
|
|
|
1300
1487
|
// src/commands/AgentHooksCommands.ts
|
|
1301
1488
|
import { homedir as homedir4 } from "node:os";
|
|
1302
|
-
import { join as
|
|
1303
|
-
import { existsSync as
|
|
1489
|
+
import { join as join12, dirname as dirname5 } from "node:path";
|
|
1490
|
+
import { existsSync as existsSync9, cpSync, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync6 } from "node:fs";
|
|
1304
1491
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1305
1492
|
|
|
1306
1493
|
// src/project/index.ts
|
|
1307
1494
|
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
1308
|
-
import { existsSync as
|
|
1495
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync4, renameSync, statSync, writeFileSync as writeFileSync5 } from "node:fs";
|
|
1309
1496
|
import { homedir as homedir3 } from "node:os";
|
|
1310
|
-
import { basename as basename2, delimiter, dirname as dirname4, join as
|
|
1497
|
+
import { basename as basename2, delimiter, dirname as dirname4, join as join11, resolve } from "node:path";
|
|
1311
1498
|
import YAML2 from "yaml";
|
|
1312
1499
|
|
|
1313
1500
|
// src/project/RegistryStore.ts
|
|
@@ -1542,18 +1729,18 @@ var PROJECT_SOURCE_SKILL_ROOTS_ENV = "PJ_SOURCE_SKILL_ROOTS";
|
|
|
1542
1729
|
var PROJECT_REGISTRY_SCHEMA_VERSION = 1;
|
|
1543
1730
|
var DEFAULT_SOURCE_SKILL_ROOTS = [
|
|
1544
1731
|
"/home/delorenj/code/skillex/all-skills",
|
|
1545
|
-
|
|
1546
|
-
|
|
1732
|
+
join11(homedir3(), ".agents", "skills"),
|
|
1733
|
+
join11(homedir3(), ".codex", "skills")
|
|
1547
1734
|
];
|
|
1548
1735
|
function projectRegistryPath(env2 = process.env) {
|
|
1549
|
-
return expandHome(env2[PROJECT_REGISTRY_ENV] ||
|
|
1736
|
+
return expandHome(env2[PROJECT_REGISTRY_ENV] || join11(homedir3(), ".config", "pjangler", "projects.yaml"));
|
|
1550
1737
|
}
|
|
1551
1738
|
function emptyProjectRegistry() {
|
|
1552
1739
|
return { schema_version: PROJECT_REGISTRY_SCHEMA_VERSION, projects: {} };
|
|
1553
1740
|
}
|
|
1554
1741
|
function loadProjectRegistry(path = projectRegistryPath()) {
|
|
1555
|
-
if (!
|
|
1556
|
-
const raw = YAML2.parse(
|
|
1742
|
+
if (!existsSync8(path)) return emptyProjectRegistry();
|
|
1743
|
+
const raw = YAML2.parse(readFileSync4(path, "utf8"));
|
|
1557
1744
|
if (raw == null) return emptyProjectRegistry();
|
|
1558
1745
|
if (!isRecord(raw)) throw new Error(`Project registry must be a mapping: ${path}`);
|
|
1559
1746
|
const registry = raw;
|
|
@@ -1568,7 +1755,7 @@ function saveProjectRegistry(registry, path = projectRegistryPath()) {
|
|
|
1568
1755
|
validateProjectRegistry(registry);
|
|
1569
1756
|
mkdirSync4(dirname4(path), { recursive: true });
|
|
1570
1757
|
const temp = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
1571
|
-
|
|
1758
|
+
writeFileSync5(temp, YAML2.stringify(registry, { lineWidth: 0 }), "utf8");
|
|
1572
1759
|
renameSync(temp, path);
|
|
1573
1760
|
}
|
|
1574
1761
|
function validateProjectRegistry(registry) {
|
|
@@ -1650,7 +1837,7 @@ function resolveAgentHooksLayer(input, env2 = process.env) {
|
|
|
1650
1837
|
const override = env2.PJ_AGENT_HOOKS_LAYER;
|
|
1651
1838
|
if (override === "0" || override === "false") return false;
|
|
1652
1839
|
if (override === "1" || override === "true") return true;
|
|
1653
|
-
return !
|
|
1840
|
+
return !existsSync8(join11(homedir3(), ".agents", "hooks"));
|
|
1654
1841
|
}
|
|
1655
1842
|
function jsonStable(value) {
|
|
1656
1843
|
return JSON.stringify(value);
|
|
@@ -1681,12 +1868,12 @@ function resolveSourceSkillPath(sourceSkill, env2 = process.env) {
|
|
|
1681
1868
|
if (!sourceSkill) return void 0;
|
|
1682
1869
|
const expanded = expandHome(sourceSkill);
|
|
1683
1870
|
const direct = resolve(expanded);
|
|
1684
|
-
if (
|
|
1871
|
+
if (existsSync8(direct)) return direct;
|
|
1685
1872
|
const name = basename2(sourceSkill);
|
|
1686
1873
|
const roots = sourceSkillRoots(env2);
|
|
1687
1874
|
for (const root of roots) {
|
|
1688
|
-
const candidate =
|
|
1689
|
-
if (
|
|
1875
|
+
const candidate = join11(root, name);
|
|
1876
|
+
if (existsSync8(candidate)) return candidate;
|
|
1690
1877
|
}
|
|
1691
1878
|
const searched = roots.length ? ` Searched roots: ${roots.join(", ")}.` : "";
|
|
1692
1879
|
const hint = `${searched} Add project-specific roots with ${PROJECT_SOURCE_SKILL_ROOTS_ENV}.`;
|
|
@@ -1767,7 +1954,7 @@ function planProjectInit(input) {
|
|
|
1767
1954
|
}));
|
|
1768
1955
|
}
|
|
1769
1956
|
actions.push(
|
|
1770
|
-
{ kind: "project.write-manifest", path:
|
|
1957
|
+
{ kind: "project.write-manifest", path: join11(targetDir, ".project.json"), manifest },
|
|
1771
1958
|
{
|
|
1772
1959
|
kind: "ticket-provider.create-or-link",
|
|
1773
1960
|
enabled: live,
|
|
@@ -1820,7 +2007,7 @@ async function executeProjectInitPlan(plan) {
|
|
|
1820
2007
|
}
|
|
1821
2008
|
if (result.status !== 0) {
|
|
1822
2009
|
errors.push(`copier exited with status ${result.status ?? "unknown"}`);
|
|
1823
|
-
if (
|
|
2010
|
+
if (existsSync8(action.targetDir)) changedFiles.push(action.targetDir);
|
|
1824
2011
|
break;
|
|
1825
2012
|
}
|
|
1826
2013
|
changedFiles.push(action.targetDir);
|
|
@@ -1828,9 +2015,9 @@ async function executeProjectInitPlan(plan) {
|
|
|
1828
2015
|
mkdirSync4(dirname4(action.path), { recursive: true });
|
|
1829
2016
|
const next = `${JSON.stringify(action.manifest, null, 2)}
|
|
1830
2017
|
`;
|
|
1831
|
-
const current =
|
|
2018
|
+
const current = existsSync8(action.path) ? readFileSync4(action.path, "utf8") : void 0;
|
|
1832
2019
|
if (current !== next) {
|
|
1833
|
-
|
|
2020
|
+
writeFileSync5(action.path, next, "utf8");
|
|
1834
2021
|
changedFiles.push(action.path);
|
|
1835
2022
|
}
|
|
1836
2023
|
} else if (action.kind === "registry.upsert") {
|
|
@@ -1933,16 +2120,16 @@ function doctorProjectRegistry(registryPath2 = projectRegistryPath(), slug) {
|
|
|
1933
2120
|
const registry = loadProjectRegistry(registryPath2);
|
|
1934
2121
|
const projects = slug ? [[slug, getProject(registry, slug)]] : Object.entries(registry.projects);
|
|
1935
2122
|
for (const [projectSlug, project] of projects) {
|
|
1936
|
-
if (!
|
|
2123
|
+
if (!existsSync8(project.repo_path)) {
|
|
1937
2124
|
issues.push({ level: "warn", slug: projectSlug, message: `repo_path does not exist: ${project.repo_path}` });
|
|
1938
2125
|
} else if (!statSync(project.repo_path).isDirectory()) {
|
|
1939
2126
|
issues.push({ level: "error", slug: projectSlug, message: `repo_path is not a directory: ${project.repo_path}` });
|
|
1940
2127
|
} else {
|
|
1941
|
-
const manifestPath =
|
|
1942
|
-
if (!
|
|
2128
|
+
const manifestPath = join11(project.repo_path, ".project.json");
|
|
2129
|
+
if (!existsSync8(manifestPath)) issues.push({ level: "warn", slug: projectSlug, message: ".project.json is missing" });
|
|
1943
2130
|
}
|
|
1944
2131
|
for (const artifact of project.source_artifacts) {
|
|
1945
|
-
if (artifact.path && !
|
|
2132
|
+
if (artifact.path && !existsSync8(artifact.path)) {
|
|
1946
2133
|
issues.push({ level: "warn", slug: projectSlug, message: `source artifact missing: ${artifact.path}` });
|
|
1947
2134
|
}
|
|
1948
2135
|
}
|
|
@@ -1955,7 +2142,7 @@ function doctorProjectRegistry(registryPath2 = projectRegistryPath(), slug) {
|
|
|
1955
2142
|
};
|
|
1956
2143
|
}
|
|
1957
2144
|
function buildCommonProjectCopierAction(input) {
|
|
1958
|
-
const templateDir =
|
|
2145
|
+
const templateDir = join11(input.pjanglerRoot, "templates", "commonproject");
|
|
1959
2146
|
const data = {
|
|
1960
2147
|
project_name: input.projectName,
|
|
1961
2148
|
project_description: input.projectDescription ?? "",
|
|
@@ -1984,7 +2171,7 @@ function buildCommonProjectCopierAction(input) {
|
|
|
1984
2171
|
function resolvePjanglerRoot() {
|
|
1985
2172
|
let dir = dirname4(new URL(import.meta.url).pathname);
|
|
1986
2173
|
while (dir !== dirname4(dir)) {
|
|
1987
|
-
if (
|
|
2174
|
+
if (existsSync8(join11(dir, "package.json")) && existsSync8(join11(dir, "templates", "commonproject", "copier.yml"))) return dir;
|
|
1988
2175
|
dir = dirname4(dir);
|
|
1989
2176
|
}
|
|
1990
2177
|
return resolve(process.cwd());
|
|
@@ -2016,7 +2203,7 @@ function validateProjectRecord(project, key) {
|
|
|
2016
2203
|
}
|
|
2017
2204
|
function expandHome(path) {
|
|
2018
2205
|
if (path === "~") return homedir3();
|
|
2019
|
-
if (path.startsWith("~/")) return
|
|
2206
|
+
if (path.startsWith("~/")) return join11(homedir3(), path.slice(2));
|
|
2020
2207
|
return path;
|
|
2021
2208
|
}
|
|
2022
2209
|
function isRecord(value) {
|
|
@@ -2033,16 +2220,16 @@ function resolveTemplateRoot() {
|
|
|
2033
2220
|
try {
|
|
2034
2221
|
let dir = dirname5(fileURLToPath2(import.meta.url));
|
|
2035
2222
|
for (let i = 0; i < 8; i++) {
|
|
2036
|
-
candidates.push(
|
|
2223
|
+
candidates.push(join12(dir, "templates", "commonproject", "template"));
|
|
2037
2224
|
const parent = dirname5(dir);
|
|
2038
2225
|
if (parent === dir) break;
|
|
2039
2226
|
dir = parent;
|
|
2040
2227
|
}
|
|
2041
2228
|
} catch {
|
|
2042
2229
|
}
|
|
2043
|
-
candidates.push(
|
|
2230
|
+
candidates.push(join12(homedir4(), "code", "pjangler", "templates", "commonproject", "template"));
|
|
2044
2231
|
for (const c of candidates) {
|
|
2045
|
-
if (
|
|
2232
|
+
if (existsSync9(join12(c, ".agents", "hooks", "hooks.master.json"))) return c;
|
|
2046
2233
|
}
|
|
2047
2234
|
throw new Error(
|
|
2048
2235
|
"Could not locate the CommonProject template. Set PJANGLER_COMMONPROJECT_TEMPLATE to <repo>/templates/commonproject/template."
|
|
@@ -2061,18 +2248,17 @@ var CopyAgentHooksTree = class extends Command {
|
|
|
2061
2248
|
}
|
|
2062
2249
|
const items = [
|
|
2063
2250
|
{ rel: ".agents/hooks", dir: true },
|
|
2251
|
+
{ rel: ".agents/skills.json", dir: false },
|
|
2064
2252
|
{ rel: ".agents/local.example.json", dir: false },
|
|
2065
|
-
{ rel: ".mise/scripts/link-project-skills-to-clis.sh", dir: false },
|
|
2066
|
-
{ rel: ".mise/scripts/unlink-project-skills-from-clis.sh", dir: false },
|
|
2067
2253
|
{ rel: ".mise/scripts/hindsight-setup.sh", dir: false }
|
|
2068
2254
|
];
|
|
2069
2255
|
const created = [];
|
|
2070
2256
|
const skipped = [];
|
|
2071
2257
|
for (const { rel, dir } of items) {
|
|
2072
|
-
const src =
|
|
2073
|
-
const dest =
|
|
2074
|
-
if (!
|
|
2075
|
-
if (
|
|
2258
|
+
const src = join12(templateRoot, rel);
|
|
2259
|
+
const dest = join12(this.context.targetDir, rel);
|
|
2260
|
+
if (!existsSync9(src)) continue;
|
|
2261
|
+
if (existsSync9(dest) && !this.context.force) {
|
|
2076
2262
|
skipped.push(rel);
|
|
2077
2263
|
continue;
|
|
2078
2264
|
}
|
|
@@ -2098,25 +2284,21 @@ var WireMiseAgentHooks = class _WireMiseAgentHooks extends Command {
|
|
|
2098
2284
|
if (!resolveAgentHooksLayer()) {
|
|
2099
2285
|
return { success: true, message: this.formatMessage(AGENT_HOOKS_SKIP_MESSAGE) };
|
|
2100
2286
|
}
|
|
2101
|
-
const misePath =
|
|
2102
|
-
if (!
|
|
2287
|
+
const misePath = join12(this.context.targetDir, "mise.toml");
|
|
2288
|
+
if (!existsSync9(misePath)) {
|
|
2103
2289
|
return {
|
|
2104
2290
|
success: false,
|
|
2105
2291
|
message: "\u26A0\uFE0F No mise.toml found \u2014 run `pjangler init mise` first, then re-run."
|
|
2106
2292
|
};
|
|
2107
2293
|
}
|
|
2108
|
-
let content =
|
|
2294
|
+
let content = readFileSync5(misePath, "utf8");
|
|
2109
2295
|
if (content.includes(_WireMiseAgentHooks.MARKER)) {
|
|
2110
2296
|
return { success: true, message: this.formatMessage("\u2713 mise.toml already wired for agent-hooks") };
|
|
2111
2297
|
}
|
|
2112
2298
|
const cr = _WireMiseAgentHooks.CR;
|
|
2113
|
-
const enterAdds = [
|
|
2114
|
-
` "${cr}/.mise/scripts/link-project-skills-to-clis.sh",`,
|
|
2115
|
-
` "${cr}/.agents/hooks/sync.py --install --quiet",`
|
|
2116
|
-
].join("\n");
|
|
2299
|
+
const enterAdds = [` "sync-skills.py --scope project",`, ` "${cr}/.agents/hooks/sync.py --install --quiet",`].join("\n");
|
|
2117
2300
|
const leaveBlock = [
|
|
2118
2301
|
"leave = [",
|
|
2119
|
-
` "${cr}/.mise/scripts/unlink-project-skills-from-clis.sh",`,
|
|
2120
2302
|
` "${cr}/.agents/hooks/sync.py --uninstall --quiet",`,
|
|
2121
2303
|
"]"
|
|
2122
2304
|
].join("\n");
|
|
@@ -2133,7 +2315,6 @@ ${enterAdds}${close}`;
|
|
|
2133
2315
|
content = content.replace(leaveRe, (_m, head, close) => {
|
|
2134
2316
|
const sep = /[,[]\s*$/.test(head) ? "" : ",";
|
|
2135
2317
|
return `${head}${sep}
|
|
2136
|
-
"${cr}/.mise/scripts/unlink-project-skills-from-clis.sh",
|
|
2137
2318
|
"${cr}/.agents/hooks/sync.py --uninstall --quiet",${close}`;
|
|
2138
2319
|
});
|
|
2139
2320
|
} else {
|
|
@@ -2146,6 +2327,14 @@ ${leaveBlock}`);
|
|
|
2146
2327
|
"",
|
|
2147
2328
|
_WireMiseAgentHooks.MARKER + " (generated \u2014 see .agents/hooks/README.md)",
|
|
2148
2329
|
"[[watch_files]]",
|
|
2330
|
+
'patterns = [".agents/skills.json"]',
|
|
2331
|
+
'task = "skills-sync"',
|
|
2332
|
+
"",
|
|
2333
|
+
"[tasks.skills-sync]",
|
|
2334
|
+
'description = "Sync skills from manifest to local CLI dirs"',
|
|
2335
|
+
'run = "sync-skills.py --scope project"',
|
|
2336
|
+
"",
|
|
2337
|
+
"[[watch_files]]",
|
|
2149
2338
|
'patterns = [".agents/hooks/hooks.master.json"]',
|
|
2150
2339
|
'task = "hooks-sync"',
|
|
2151
2340
|
"",
|
|
@@ -2161,18 +2350,6 @@ ${leaveBlock}`);
|
|
|
2161
2350
|
'description = "Remove per-user agent-hook injections (codex/kimi/hermes)"',
|
|
2162
2351
|
`run = "${cr}/.agents/hooks/sync.py --uninstall"`,
|
|
2163
2352
|
"",
|
|
2164
|
-
"[tasks.link-project-skills-to-clis]",
|
|
2165
|
-
'description = "Fan .agents/skills out to each agent CLI (honors local.json)"',
|
|
2166
|
-
`run = "${cr}/.mise/scripts/link-project-skills-to-clis.sh"`,
|
|
2167
|
-
"",
|
|
2168
|
-
"[tasks.unlink-project-skills-from-clis]",
|
|
2169
|
-
'description = "Remove project skill symlinks from shared per-CLI dirs"',
|
|
2170
|
-
`run = "${cr}/.mise/scripts/unlink-project-skills-from-clis.sh"`,
|
|
2171
|
-
"",
|
|
2172
|
-
"[tasks.skills-relink]",
|
|
2173
|
-
'description = "Re-fan the project skill set to all CLIs"',
|
|
2174
|
-
`run = "${cr}/.mise/scripts/link-project-skills-to-clis.sh"`,
|
|
2175
|
-
"",
|
|
2176
2353
|
"[tasks.hindsight-setup]",
|
|
2177
2354
|
`description = "Provision this dev's shared project Hindsight key from 1Password into .env"`,
|
|
2178
2355
|
`run = "${cr}/.mise/scripts/hindsight-setup.sh"`,
|
|
@@ -2181,7 +2358,7 @@ ${leaveBlock}`);
|
|
|
2181
2358
|
""
|
|
2182
2359
|
].join("\n");
|
|
2183
2360
|
content = content.replace(/\n*$/, "\n") + appended;
|
|
2184
|
-
if (!this.context.dryRun)
|
|
2361
|
+
if (!this.context.dryRun) writeFileSync6(misePath, content);
|
|
2185
2362
|
if (wiredHooks) {
|
|
2186
2363
|
return { success: true, message: this.formatMessage("\u2705 Wired mise.toml ([hooks] enter/leave + tasks)") };
|
|
2187
2364
|
}
|
|
@@ -2190,8 +2367,8 @@ ${leaveBlock}`);
|
|
|
2190
2367
|
message: this.formatMessage(
|
|
2191
2368
|
`\u2705 Added agent-hooks tasks to mise.toml.
|
|
2192
2369
|
\u26A0\uFE0F Could not find a [hooks].enter array to extend \u2014 add these to your [hooks] block manually:
|
|
2193
|
-
enter += "
|
|
2194
|
-
leave += "${cr}/.
|
|
2370
|
+
enter += "sync-skills.py --scope project", "${cr}/.agents/hooks/sync.py --install --quiet"
|
|
2371
|
+
leave += "${cr}/.agents/hooks/sync.py --uninstall --quiet"`
|
|
2195
2372
|
)
|
|
2196
2373
|
};
|
|
2197
2374
|
}
|
|
@@ -2206,10 +2383,25 @@ var AgentHooksRecipe = class extends Recipe {
|
|
|
2206
2383
|
printNextSteps() {
|
|
2207
2384
|
console.log("\u{1FA9D} Agent-hooks layer installed!");
|
|
2208
2385
|
console.log(" Next steps:");
|
|
2209
|
-
console.log(" 1. mise run
|
|
2210
|
-
console.log(" 2.
|
|
2211
|
-
console.log(" 3.
|
|
2212
|
-
console.log(
|
|
2386
|
+
console.log(" 1. mise run skills-sync # sync .agents/skills.json into local CLI dirs");
|
|
2387
|
+
console.log(" 2. mise run hooks-sync # generate .claude/settings.json + inject codex/kimi/hermes");
|
|
2388
|
+
console.log(" 3. git add .claude/settings.json .agents/hooks .agents/skills.json && commit (codex/kimi/hermes are per-dev)");
|
|
2389
|
+
console.log(" 4. mise run hindsight-setup # set HINDSIGHT_OP_KEY_REF to your 1Password item first");
|
|
2390
|
+
console.log(" 5. Optional per-dev hook opt-out: copy .agents/local.example.json -> .agents/local.json");
|
|
2391
|
+
}
|
|
2392
|
+
};
|
|
2393
|
+
|
|
2394
|
+
// src/recipes/MiseOpInjectRecipe.ts
|
|
2395
|
+
var MiseOpInjectRecipe = class extends Recipe {
|
|
2396
|
+
constructor(context) {
|
|
2397
|
+
super(context);
|
|
2398
|
+
this.addIngredient(WireMiseOpInject);
|
|
2399
|
+
}
|
|
2400
|
+
printNextSteps() {
|
|
2401
|
+
console.log("\u{1F389} Wired up .env.op 1Password resolution via mise!");
|
|
2402
|
+
console.log(" Next steps:");
|
|
2403
|
+
console.log(" 1. Create .env.op with your op:// secret references");
|
|
2404
|
+
console.log(" 2. Run `mise run secrets-inject` or simply cd out and back in to trigger the hook");
|
|
2213
2405
|
}
|
|
2214
2406
|
};
|
|
2215
2407
|
|
|
@@ -2252,6 +2444,12 @@ var RECIPE_REGISTRY = {
|
|
|
2252
2444
|
description: "Retrofit the project-scoped agent-hooks + skill fan-out layer (Claude/Codex/Kimi/Hermes hooks via mise enter/leave)",
|
|
2253
2445
|
class: AgentHooksRecipe,
|
|
2254
2446
|
commands: ["CopyAgentHooksTree", "WireMiseAgentHooks"]
|
|
2447
|
+
},
|
|
2448
|
+
"mise-op-inject": {
|
|
2449
|
+
name: "mise-op-inject",
|
|
2450
|
+
description: "Wire up op-inject script to mise.toml for 1Password secret resolution",
|
|
2451
|
+
class: MiseOpInjectRecipe,
|
|
2452
|
+
commands: ["WireMiseOpInject"]
|
|
2255
2453
|
}
|
|
2256
2454
|
};
|
|
2257
2455
|
var COMMAND_REGISTRY = {
|
|
@@ -2320,6 +2518,12 @@ var COMMAND_REGISTRY = {
|
|
|
2320
2518
|
description: "Create .env.example file",
|
|
2321
2519
|
group: "environment",
|
|
2322
2520
|
class: AddDotenv
|
|
2521
|
+
},
|
|
2522
|
+
WireMiseOpInject: {
|
|
2523
|
+
name: "WireMiseOpInject",
|
|
2524
|
+
description: "Wire up op-inject script to mise.toml for 1Password secret resolution",
|
|
2525
|
+
group: "mise",
|
|
2526
|
+
class: WireMiseOpInject
|
|
2323
2527
|
}
|
|
2324
2528
|
};
|
|
2325
2529
|
function getRecipeNames() {
|
|
@@ -2354,29 +2558,39 @@ function createRecipe(name, context) {
|
|
|
2354
2558
|
import { cancel as cancel2, multiselect, text as text2, isCancel as isCancel5 } from "@clack/prompts";
|
|
2355
2559
|
|
|
2356
2560
|
// src/parity/index.ts
|
|
2357
|
-
import { existsSync as
|
|
2358
|
-
import { basename as basename3, dirname as dirname6, join as
|
|
2561
|
+
import { existsSync as existsSync10, lstatSync, mkdirSync as mkdirSync6, readFileSync as readFileSync6, readlinkSync, readdirSync as readdirSync2, renameSync as renameSync2, symlinkSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync7, chmodSync as chmodSync4, copyFileSync, rmSync } from "node:fs";
|
|
2562
|
+
import { basename as basename3, dirname as dirname6, join as join13, relative, resolve as resolve2 } from "node:path";
|
|
2359
2563
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
2360
2564
|
import { homedir as homedir5 } from "node:os";
|
|
2361
2565
|
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
2362
2566
|
import YAML3 from "yaml";
|
|
2363
2567
|
var LINK_AGENTFILES_SCRIPT = "'{{config_root}}/.mise/scripts/link-agentfiles.sh'";
|
|
2364
2568
|
var OP_INJECT_SCRIPT = "op inject -i .env.op > .env";
|
|
2569
|
+
var SYNC_SKILLS_SCRIPT = "sync-skills.py --scope project";
|
|
2365
2570
|
var CODEGRAPH_SCRIPT = "[ -f '{{config_root}}/.mise/scripts/codegraph.sh' ] && '{{config_root}}/.mise/scripts/codegraph.sh' || true";
|
|
2571
|
+
var SKILLS_REGISTRY_URL = "https://github.com/delorenj/skillex.git";
|
|
2366
2572
|
var HOOKS_COMMENT_HEADER = `# This block will handle the linking of
|
|
2367
2573
|
# agent files to the main AGENTS.md file.
|
|
2368
2574
|
#
|
|
2369
2575
|
# TODO: Ensure this works for all levels of nesting.
|
|
2370
2576
|
# i.e. All linked agent files MUST be siblings at
|
|
2371
2577
|
# any given level of nesting.`;
|
|
2372
|
-
var LINK_AGENTFILES_HOOK_ENTRIES = [LINK_AGENTFILES_SCRIPT, OP_INJECT_SCRIPT];
|
|
2578
|
+
var LINK_AGENTFILES_HOOK_ENTRIES = [LINK_AGENTFILES_SCRIPT, OP_INJECT_SCRIPT, SYNC_SKILLS_SCRIPT];
|
|
2373
2579
|
var LINK_AGENTFILES_WATCH_TASK_BLOCK = `[[watch_files]]
|
|
2374
2580
|
patterns = ["AGENTS.md"]
|
|
2375
2581
|
task = "link-agentfiles"
|
|
2376
2582
|
|
|
2583
|
+
[[watch_files]]
|
|
2584
|
+
patterns = [".agents/skills.json"]
|
|
2585
|
+
task = "skills-sync"
|
|
2586
|
+
|
|
2377
2587
|
[tasks.link-agentfiles]
|
|
2378
2588
|
description = "Symlink all agent files to AGENTS.md"
|
|
2379
|
-
run = "'{{config_root}}/.mise/scripts/link-agentfiles.sh'"
|
|
2589
|
+
run = "'{{config_root}}/.mise/scripts/link-agentfiles.sh'"
|
|
2590
|
+
|
|
2591
|
+
[tasks.skills-sync]
|
|
2592
|
+
description = "Sync skills from manifest to local CLI dirs"
|
|
2593
|
+
run = "sync-skills.py --scope project"`;
|
|
2380
2594
|
var VERSIONING_BLOCK = `# >>> mise-versioning >>> (managed block \u2014 do not edit by hand; re-run init to update)
|
|
2381
2595
|
[tasks."version"]
|
|
2382
2596
|
description = "Print the current version (vX.Y.Z)"
|
|
@@ -2406,7 +2620,7 @@ run = "'{{config_root}}/.mise/scripts/versioning.sh' sync"
|
|
|
2406
2620
|
function resolvePjanglerRoot2() {
|
|
2407
2621
|
let dir = dirname6(fileURLToPath3(import.meta.url));
|
|
2408
2622
|
while (dir !== dirname6(dir)) {
|
|
2409
|
-
if (
|
|
2623
|
+
if (existsSync10(join13(dir, "package.json")) && existsSync10(join13(dir, "templates", "commonproject", "copier.yml"))) {
|
|
2410
2624
|
return dir;
|
|
2411
2625
|
}
|
|
2412
2626
|
dir = dirname6(dir);
|
|
@@ -2417,17 +2631,17 @@ function normalizeNewlines(value) {
|
|
|
2417
2631
|
return value.replace(/\r\n/g, "\n");
|
|
2418
2632
|
}
|
|
2419
2633
|
function readText(path) {
|
|
2420
|
-
return normalizeNewlines(
|
|
2634
|
+
return normalizeNewlines(readFileSync6(path, "utf8"));
|
|
2421
2635
|
}
|
|
2422
2636
|
function safeReadText(path) {
|
|
2423
|
-
return
|
|
2637
|
+
return existsSync10(path) ? readText(path) : null;
|
|
2424
2638
|
}
|
|
2425
2639
|
function ensureParent(path) {
|
|
2426
2640
|
mkdirSync6(dirname6(path), { recursive: true });
|
|
2427
2641
|
}
|
|
2428
2642
|
function writeText(path, content) {
|
|
2429
2643
|
ensureParent(path);
|
|
2430
|
-
|
|
2644
|
+
writeFileSync7(path, content);
|
|
2431
2645
|
}
|
|
2432
2646
|
function tryParseJson(text3) {
|
|
2433
2647
|
if (!text3) return null;
|
|
@@ -2444,7 +2658,7 @@ function titleCaseSlug(slug) {
|
|
|
2444
2658
|
return slug.split(/[-_]/g).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
2445
2659
|
}
|
|
2446
2660
|
function readSymlinkTarget(path) {
|
|
2447
|
-
if (!
|
|
2661
|
+
if (!existsSync10(path)) return null;
|
|
2448
2662
|
try {
|
|
2449
2663
|
return readlinkSync(path);
|
|
2450
2664
|
} catch {
|
|
@@ -2452,7 +2666,7 @@ function readSymlinkTarget(path) {
|
|
|
2452
2666
|
}
|
|
2453
2667
|
}
|
|
2454
2668
|
function ensureSymlink(path, target, dryRun) {
|
|
2455
|
-
if (
|
|
2669
|
+
if (existsSync10(path)) {
|
|
2456
2670
|
const stat = lstatSync(path);
|
|
2457
2671
|
if (stat.isSymbolicLink()) {
|
|
2458
2672
|
const current = readSymlinkTarget(path);
|
|
@@ -2469,11 +2683,11 @@ function ensureSymlink(path, target, dryRun) {
|
|
|
2469
2683
|
return { changed: true };
|
|
2470
2684
|
}
|
|
2471
2685
|
function bootstrapAgentsFile(repoRoot, dryRun) {
|
|
2472
|
-
const agentsPath =
|
|
2473
|
-
if (
|
|
2686
|
+
const agentsPath = join13(repoRoot, "AGENTS.md");
|
|
2687
|
+
if (existsSync10(agentsPath)) return { changedFiles: [], details: [] };
|
|
2474
2688
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
2475
|
-
const source =
|
|
2476
|
-
if (!
|
|
2689
|
+
const source = join13(repoRoot, file);
|
|
2690
|
+
if (!existsSync10(source)) continue;
|
|
2477
2691
|
const stat = lstatSync(source);
|
|
2478
2692
|
if (stat.isSymbolicLink()) continue;
|
|
2479
2693
|
if (stat.isFile()) {
|
|
@@ -2482,8 +2696,8 @@ function bootstrapAgentsFile(repoRoot, dryRun) {
|
|
|
2482
2696
|
}
|
|
2483
2697
|
return { changedFiles: [], details: [], blocked: `${file} exists but is not a regular file; cannot promote to AGENTS.md` };
|
|
2484
2698
|
}
|
|
2485
|
-
const readmePath =
|
|
2486
|
-
if (
|
|
2699
|
+
const readmePath = join13(repoRoot, "README.md");
|
|
2700
|
+
if (existsSync10(readmePath)) {
|
|
2487
2701
|
const stat = lstatSync(readmePath);
|
|
2488
2702
|
if (!stat.isFile()) return { changedFiles: [], details: [], blocked: "README.md exists but is not a regular file; cannot copy to AGENTS.md" };
|
|
2489
2703
|
if (!dryRun) copyFileSync(readmePath, agentsPath);
|
|
@@ -2522,12 +2736,12 @@ function yamlGet(text3, keyPath) {
|
|
|
2522
2736
|
return "";
|
|
2523
2737
|
}
|
|
2524
2738
|
function discoverRoles(repoRoot) {
|
|
2525
|
-
const rolesDir =
|
|
2526
|
-
if (!
|
|
2739
|
+
const rolesDir = join13(repoRoot, "agents", "hermes");
|
|
2740
|
+
if (!existsSync10(rolesDir)) return [];
|
|
2527
2741
|
return readdirSync2(rolesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => {
|
|
2528
|
-
const roleDir =
|
|
2529
|
-
const roleYamlPath =
|
|
2530
|
-
if (!
|
|
2742
|
+
const roleDir = join13(rolesDir, entry.name);
|
|
2743
|
+
const roleYamlPath = join13(roleDir, "role.yaml");
|
|
2744
|
+
if (!existsSync10(roleYamlPath)) return null;
|
|
2531
2745
|
const text3 = readText(roleYamlPath);
|
|
2532
2746
|
const runtimeRepoRaw = yamlGet(text3, "runtime.github_repo");
|
|
2533
2747
|
return {
|
|
@@ -2555,7 +2769,7 @@ function discoverRoles(repoRoot) {
|
|
|
2555
2769
|
}).filter((value) => Boolean(value));
|
|
2556
2770
|
}
|
|
2557
2771
|
function registryPath(homeDir) {
|
|
2558
|
-
return
|
|
2772
|
+
return join13(homeDir, ".hermes", "agents-registry.yaml");
|
|
2559
2773
|
}
|
|
2560
2774
|
function systemctlUser(args) {
|
|
2561
2775
|
const result = spawnSync6("systemctl", ["--user", ...args], { encoding: "utf8" });
|
|
@@ -2566,8 +2780,8 @@ function systemctlUser(args) {
|
|
|
2566
2780
|
};
|
|
2567
2781
|
}
|
|
2568
2782
|
function templateScript(ctx, name) {
|
|
2569
|
-
const source =
|
|
2570
|
-
return
|
|
2783
|
+
const source = join13(ctx.pjanglerRoot, ".mise", "scripts", name);
|
|
2784
|
+
return existsSync10(source) ? readText(source) : void 0;
|
|
2571
2785
|
}
|
|
2572
2786
|
function templateVersioningScript(ctx) {
|
|
2573
2787
|
return templateScript(ctx, "versioning.sh");
|
|
@@ -2579,8 +2793,8 @@ function resolveAgentHooksLayer2(ctx) {
|
|
|
2579
2793
|
const override = process.env.PJ_AGENT_HOOKS_LAYER;
|
|
2580
2794
|
if (override === "0" || override === "false") return false;
|
|
2581
2795
|
if (override === "1" || override === "true") return true;
|
|
2582
|
-
if (
|
|
2583
|
-
return !
|
|
2796
|
+
if (existsSync10(join13(ctx.repoRoot, ".agents", "hooks", "sync.py"))) return true;
|
|
2797
|
+
return !existsSync10(join13(ctx.homeDir, ".agents", "hooks"));
|
|
2584
2798
|
}
|
|
2585
2799
|
function evaluateMiseConditionals(template, agentHooksLayer) {
|
|
2586
2800
|
const out = [];
|
|
@@ -2610,19 +2824,36 @@ function renderGeneratedProjectMiseToml(ctx, template) {
|
|
|
2610
2824
|
return evaluateMiseConditionals(template, resolveAgentHooksLayer2(ctx)).replace(/\{%\s*raw\s*%\}([\s\S]*?)\{%\s*endraw\s*%\}/g, "$1").replace(/\{\{\s*project_name\s*\}\}/g, projectName);
|
|
2611
2825
|
}
|
|
2612
2826
|
function ensureMiseTomlFromTemplate(ctx, changedFiles) {
|
|
2613
|
-
const targetPath =
|
|
2614
|
-
if (
|
|
2615
|
-
const sourcePath =
|
|
2616
|
-
if (!
|
|
2827
|
+
const targetPath = join13(ctx.repoRoot, "mise.toml");
|
|
2828
|
+
if (existsSync10(targetPath)) return false;
|
|
2829
|
+
const sourcePath = join13(ctx.pjanglerRoot, "templates", "commonproject", "template", "mise.toml.jinja");
|
|
2830
|
+
if (!existsSync10(sourcePath)) return false;
|
|
2617
2831
|
changedFiles.push(targetPath);
|
|
2618
2832
|
if (!ctx.dryRun) {
|
|
2619
2833
|
writeText(targetPath, renderGeneratedProjectMiseToml(ctx, readText(sourcePath)));
|
|
2620
2834
|
}
|
|
2621
2835
|
return true;
|
|
2622
2836
|
}
|
|
2837
|
+
function templateCommonProjectText(ctx, rel) {
|
|
2838
|
+
const path = join13(ctx.pjanglerRoot, "templates", "commonproject", "template", rel);
|
|
2839
|
+
return existsSync10(path) ? readText(path) : void 0;
|
|
2840
|
+
}
|
|
2841
|
+
function canonicalSkillsManifest() {
|
|
2842
|
+
return `${JSON.stringify(
|
|
2843
|
+
{
|
|
2844
|
+
$schema: "https://raw.githubusercontent.com/skillex/schemas/main/skills.schema.json",
|
|
2845
|
+
inherit_global: true,
|
|
2846
|
+
registry: SKILLS_REGISTRY_URL,
|
|
2847
|
+
skills: []
|
|
2848
|
+
},
|
|
2849
|
+
null,
|
|
2850
|
+
2
|
|
2851
|
+
)}
|
|
2852
|
+
`;
|
|
2853
|
+
}
|
|
2623
2854
|
function templateVersionFilesConf(ctx, repoRoot) {
|
|
2624
|
-
const packageJson =
|
|
2625
|
-
return
|
|
2855
|
+
const packageJson = join13(repoRoot, "package.json");
|
|
2856
|
+
return existsSync10(packageJson) ? "# mise-versioning manifest: <type> <path>\n# types: json toml cargo csproj gradle plain gittag\njson package.json\ngittag .\n" : "# mise-versioning manifest: <type> <path>\n# types: json toml cargo csproj gradle plain gittag\ngittag .\n";
|
|
2626
2857
|
}
|
|
2627
2858
|
function replaceOrAppendManagedBlock(text3, startMarker, block, beforePattern) {
|
|
2628
2859
|
if (startMarker.test(text3)) {
|
|
@@ -2646,7 +2877,7 @@ var CONDITIONAL_HERMES_PATHS = ["agents/hermes/pm/hermes", "agent/hermes/pm/herm
|
|
|
2646
2877
|
function requiredMisePathEntries(ctx) {
|
|
2647
2878
|
const required = [...BASE_MISE_PATH_ENTRIES];
|
|
2648
2879
|
for (const candidate of CONDITIONAL_HERMES_PATHS) {
|
|
2649
|
-
if (
|
|
2880
|
+
if (existsSync10(join13(ctx.repoRoot, candidate)) && !required.includes(candidate)) required.push(candidate);
|
|
2650
2881
|
}
|
|
2651
2882
|
return required;
|
|
2652
2883
|
}
|
|
@@ -2747,6 +2978,9 @@ function stripTomlStringsAndComments(line) {
|
|
|
2747
2978
|
function isManagedHookEntry(value) {
|
|
2748
2979
|
const trimmed = value.trim();
|
|
2749
2980
|
if (trimmed === OP_INJECT_SCRIPT) return true;
|
|
2981
|
+
if (trimmed === SYNC_SKILLS_SCRIPT) return true;
|
|
2982
|
+
if (/link-project-skills-to-clis\.sh'?\s*$/.test(trimmed)) return true;
|
|
2983
|
+
if (/unlink-project-skills-from-clis\.sh'?\s*$/.test(trimmed)) return true;
|
|
2750
2984
|
return /link-agentfiles\.sh'?\s*$/.test(trimmed);
|
|
2751
2985
|
}
|
|
2752
2986
|
function normalizeHookScript(script) {
|
|
@@ -2849,12 +3083,17 @@ function upsertLinkAgentfilesHooks(text3) {
|
|
|
2849
3083
|
function upsertLinkAgentfilesBlock(text3, ctx) {
|
|
2850
3084
|
const withPath = upsertMisePath(text3, requiredMisePathEntries(ctx));
|
|
2851
3085
|
let cleaned = removeTomlSection(withPath, /^\[tasks\.link-agentfiles\]$/, /link-agentfiles/, { includePrecedingComments: false });
|
|
3086
|
+
cleaned = removeTomlSection(cleaned, /^\[tasks\.skills-sync\]$/, void 0, { includePrecedingComments: false });
|
|
3087
|
+
cleaned = removeTomlSection(cleaned, /^\[tasks\.link-project-skills-to-clis\]$/, void 0, { includePrecedingComments: false });
|
|
3088
|
+
cleaned = removeTomlSection(cleaned, /^\[tasks\.unlink-project-skills-from-clis\]$/, void 0, { includePrecedingComments: false });
|
|
3089
|
+
cleaned = removeTomlSection(cleaned, /^\[tasks\.skills-relink\]$/, void 0, { includePrecedingComments: false });
|
|
2852
3090
|
cleaned = removeTomlSection(cleaned, /^\[\[watch_files\]\]$/, /AGENTS\.md/, { includePrecedingComments: false });
|
|
3091
|
+
cleaned = removeTomlSection(cleaned, /^\[\[watch_files\]\]$/, /\.agents\/skills\.json/, { includePrecedingComments: false });
|
|
2853
3092
|
cleaned = upsertLinkAgentfilesHooks(cleaned);
|
|
2854
3093
|
return insertTomlBlockBeforeVersioning(cleaned, LINK_AGENTFILES_WATCH_TASK_BLOCK);
|
|
2855
3094
|
}
|
|
2856
3095
|
function readProjectJson(ctx) {
|
|
2857
|
-
return tryParseJson(safeReadText(
|
|
3096
|
+
return tryParseJson(safeReadText(join13(ctx.repoRoot, ".project.json")));
|
|
2858
3097
|
}
|
|
2859
3098
|
function boolSetting(value, fallback) {
|
|
2860
3099
|
if (typeof value === "boolean") return value;
|
|
@@ -2929,12 +3168,12 @@ function canonicalProjectJson(ctx) {
|
|
|
2929
3168
|
};
|
|
2930
3169
|
}
|
|
2931
3170
|
function projectJsonFinding(ctx) {
|
|
2932
|
-
const projectPath =
|
|
2933
|
-
const planeJsonPath =
|
|
3171
|
+
const projectPath = join13(ctx.repoRoot, ".project.json");
|
|
3172
|
+
const planeJsonPath = join13(ctx.repoRoot, ".plane.json");
|
|
2934
3173
|
const details = [];
|
|
2935
3174
|
const data = readProjectJson(ctx);
|
|
2936
3175
|
const roles = discoverRoles(ctx.repoRoot);
|
|
2937
|
-
if (!
|
|
3176
|
+
if (!existsSync10(projectPath)) {
|
|
2938
3177
|
return { id: "sot.project-json", title: "Canonical .project.json", status: "fail", summary: ".project.json missing", details: [], fixable: true };
|
|
2939
3178
|
}
|
|
2940
3179
|
if (!data) {
|
|
@@ -2969,7 +3208,7 @@ function projectJsonFinding(ctx) {
|
|
|
2969
3208
|
for (const key of ["enabled", "grace_hours", "auto_review"]) {
|
|
2970
3209
|
if (!(key in reconcile)) details.push(`automation.reconcile.${key} missing`);
|
|
2971
3210
|
}
|
|
2972
|
-
if (
|
|
3211
|
+
if (existsSync10(planeJsonPath)) details.push(".plane.json should not exist once .project.json is canonical");
|
|
2973
3212
|
return {
|
|
2974
3213
|
id: "sot.project-json",
|
|
2975
3214
|
title: "Canonical .project.json",
|
|
@@ -3050,17 +3289,17 @@ exec env HERMES_HOME="$HERMES_HOME" HERMES_FLEET_ENV="$FLEET_ENV" HERMES_OAUTH
|
|
|
3050
3289
|
`.replace(/\u0010/g, "$");
|
|
3051
3290
|
}
|
|
3052
3291
|
function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip) {
|
|
3053
|
-
if (!
|
|
3292
|
+
if (!existsSync10(sourceDir)) return;
|
|
3054
3293
|
mkdirSync6(targetDir, { recursive: true });
|
|
3055
3294
|
for (const entry of readdirSync2(sourceDir, { withFileTypes: true })) {
|
|
3056
|
-
const sourcePath =
|
|
3295
|
+
const sourcePath = join13(sourceDir, entry.name);
|
|
3057
3296
|
if (skip?.(sourcePath)) continue;
|
|
3058
|
-
const targetPath =
|
|
3297
|
+
const targetPath = join13(targetDir, entry.name);
|
|
3059
3298
|
if (entry.isDirectory()) {
|
|
3060
3299
|
copyMissingRecursive(sourcePath, targetPath, changedFiles, dryRun, skip);
|
|
3061
3300
|
continue;
|
|
3062
3301
|
}
|
|
3063
|
-
if (
|
|
3302
|
+
if (existsSync10(targetPath)) continue;
|
|
3064
3303
|
changedFiles.push(targetPath);
|
|
3065
3304
|
if (!dryRun) {
|
|
3066
3305
|
ensureParent(targetPath);
|
|
@@ -3069,7 +3308,7 @@ function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip)
|
|
|
3069
3308
|
}
|
|
3070
3309
|
}
|
|
3071
3310
|
function upsertSubmodule(repoRoot, role, changedFiles, dryRun) {
|
|
3072
|
-
const gitmodulesPath =
|
|
3311
|
+
const gitmodulesPath = join13(repoRoot, ".gitmodules");
|
|
3073
3312
|
const repoName = role.runtimeRepo || `agent-hm-${role.repo}-${role.role}`;
|
|
3074
3313
|
const owner = role.runtimeOwner || "delorenj";
|
|
3075
3314
|
const block = `[submodule "agents/hermes/${role.role}/runtime"]
|
|
@@ -3236,7 +3475,7 @@ function runBmadInstall(repoRoot) {
|
|
|
3236
3475
|
return { ok: true };
|
|
3237
3476
|
}
|
|
3238
3477
|
function readInstalledBmadVersion(repoRoot) {
|
|
3239
|
-
const raw = safeReadText(
|
|
3478
|
+
const raw = safeReadText(join13(repoRoot, "_bmad", "_config", "manifest.yaml"));
|
|
3240
3479
|
if (!raw) return void 0;
|
|
3241
3480
|
try {
|
|
3242
3481
|
const parsed = YAML3.parse(raw);
|
|
@@ -3247,8 +3486,8 @@ function readInstalledBmadVersion(repoRoot) {
|
|
|
3247
3486
|
}
|
|
3248
3487
|
}
|
|
3249
3488
|
function bmadCachePath(homeDir) {
|
|
3250
|
-
const cacheRoot = process.env.XDG_CACHE_HOME?.trim() ||
|
|
3251
|
-
return
|
|
3489
|
+
const cacheRoot = process.env.XDG_CACHE_HOME?.trim() || join13(homeDir, ".cache");
|
|
3490
|
+
return join13(cacheRoot, "pjangler", "bmad-dist-tags.json");
|
|
3252
3491
|
}
|
|
3253
3492
|
function readBmadDistTagsCache(homeDir) {
|
|
3254
3493
|
const raw = safeReadText(bmadCachePath(homeDir));
|
|
@@ -3291,7 +3530,7 @@ function resolveBmadDistTags(homeDir) {
|
|
|
3291
3530
|
try {
|
|
3292
3531
|
const path = bmadCachePath(homeDir);
|
|
3293
3532
|
mkdirSync6(dirname6(path), { recursive: true });
|
|
3294
|
-
|
|
3533
|
+
writeFileSync7(path, JSON.stringify({ fetchedAt: Date.now(), distTags: fetched }, null, 2));
|
|
3295
3534
|
} catch {
|
|
3296
3535
|
}
|
|
3297
3536
|
return { distTags: fetched, stale: false };
|
|
@@ -3336,14 +3575,14 @@ var RULES = [
|
|
|
3336
3575
|
id: "mise.config-root",
|
|
3337
3576
|
title: "mise config_root + AGENTS link hooks",
|
|
3338
3577
|
audit: (ctx) => {
|
|
3339
|
-
const misePath =
|
|
3340
|
-
if (!
|
|
3578
|
+
const misePath = join13(ctx.repoRoot, "mise.toml");
|
|
3579
|
+
if (!existsSync10(misePath)) {
|
|
3341
3580
|
return { id: "mise.config-root", title: "mise config_root + AGENTS link hooks", status: "fail", summary: "mise.toml missing", details: [], fixable: true };
|
|
3342
3581
|
}
|
|
3343
3582
|
const text3 = readText(misePath);
|
|
3344
3583
|
const details = [];
|
|
3345
|
-
const linkAgentfilesPath =
|
|
3346
|
-
if (!
|
|
3584
|
+
const linkAgentfilesPath = join13(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
|
|
3585
|
+
if (!existsSync10(linkAgentfilesPath)) details.push(".mise/scripts/link-agentfiles.sh missing");
|
|
3347
3586
|
const pathValues = [...(text3.match(/^_\.path\s*=\s*\[([^\]]*)\]/m)?.[1] ?? "").matchAll(/"([^"]+)"/g)].map((match) => match[1]);
|
|
3348
3587
|
const missingPathValues = requiredMisePathEntries(ctx).filter((value) => !pathValues.includes(value));
|
|
3349
3588
|
if (missingPathValues.length) details.push(`[env]._.path should include ${missingPathValues.join(", ")}`);
|
|
@@ -3361,10 +3600,10 @@ var RULES = [
|
|
|
3361
3600
|
};
|
|
3362
3601
|
},
|
|
3363
3602
|
migrate: (ctx, finding) => {
|
|
3364
|
-
const path =
|
|
3603
|
+
const path = join13(ctx.repoRoot, "mise.toml");
|
|
3365
3604
|
const changedFiles = [];
|
|
3366
3605
|
const details = [];
|
|
3367
|
-
if (!
|
|
3606
|
+
if (!existsSync10(path)) {
|
|
3368
3607
|
if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
|
|
3369
3608
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "mise.toml missing and no generated-project mise template available to initialize from", changedFiles, details: [] };
|
|
3370
3609
|
}
|
|
@@ -3380,7 +3619,7 @@ var RULES = [
|
|
|
3380
3619
|
if (!ctx.dryRun) writeText(path, next);
|
|
3381
3620
|
text3 = next;
|
|
3382
3621
|
}
|
|
3383
|
-
const linkAgentfilesPath =
|
|
3622
|
+
const linkAgentfilesPath = join13(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
|
|
3384
3623
|
const expectedScript = templateLinkAgentfilesScript(ctx);
|
|
3385
3624
|
if (expectedScript === void 0) {
|
|
3386
3625
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "pjangler install is missing .mise/scripts/link-agentfiles.sh \u2014 update @delorenj/pjangler (broken package)", changedFiles, details: [] };
|
|
@@ -3389,7 +3628,7 @@ var RULES = [
|
|
|
3389
3628
|
changedFiles.push(linkAgentfilesPath);
|
|
3390
3629
|
if (!ctx.dryRun) {
|
|
3391
3630
|
writeText(linkAgentfilesPath, expectedScript);
|
|
3392
|
-
|
|
3631
|
+
chmodSync4(linkAgentfilesPath, 493);
|
|
3393
3632
|
}
|
|
3394
3633
|
}
|
|
3395
3634
|
return {
|
|
@@ -3407,13 +3646,13 @@ var RULES = [
|
|
|
3407
3646
|
title: "managed mise versioning block",
|
|
3408
3647
|
audit: (ctx) => {
|
|
3409
3648
|
const details = [];
|
|
3410
|
-
const misePath =
|
|
3411
|
-
const versioningPath =
|
|
3412
|
-
const manifestPath =
|
|
3649
|
+
const misePath = join13(ctx.repoRoot, "mise.toml");
|
|
3650
|
+
const versioningPath = join13(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
|
|
3651
|
+
const manifestPath = join13(ctx.repoRoot, ".mise", "version-files.conf");
|
|
3413
3652
|
const text3 = safeReadText(misePath);
|
|
3414
3653
|
if (!text3?.includes("# >>> mise-versioning >>>")) details.push("mise versioning managed block missing");
|
|
3415
|
-
if (!
|
|
3416
|
-
if (!
|
|
3654
|
+
if (!existsSync10(versioningPath)) details.push(".mise/scripts/versioning.sh missing");
|
|
3655
|
+
if (!existsSync10(manifestPath)) details.push(".mise/version-files.conf missing");
|
|
3417
3656
|
return {
|
|
3418
3657
|
id: "mise.versioning",
|
|
3419
3658
|
title: "managed mise versioning block",
|
|
@@ -3426,8 +3665,8 @@ var RULES = [
|
|
|
3426
3665
|
migrate: (ctx, finding) => {
|
|
3427
3666
|
const changedFiles = [];
|
|
3428
3667
|
const details = [];
|
|
3429
|
-
const misePath =
|
|
3430
|
-
if (!
|
|
3668
|
+
const misePath = join13(ctx.repoRoot, "mise.toml");
|
|
3669
|
+
if (!existsSync10(misePath)) {
|
|
3431
3670
|
if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
|
|
3432
3671
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "mise.toml missing and no generated-project mise template available to initialize from", changedFiles, details: [] };
|
|
3433
3672
|
}
|
|
@@ -3451,7 +3690,7 @@ var RULES = [
|
|
|
3451
3690
|
if (!changedFiles.includes(misePath)) changedFiles.push(misePath);
|
|
3452
3691
|
if (!ctx.dryRun) writeText(misePath, nextMise);
|
|
3453
3692
|
}
|
|
3454
|
-
const versioningPath =
|
|
3693
|
+
const versioningPath = join13(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
|
|
3455
3694
|
const expectedScript = templateVersioningScript(ctx);
|
|
3456
3695
|
if (expectedScript === void 0) {
|
|
3457
3696
|
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: [] };
|
|
@@ -3460,10 +3699,10 @@ var RULES = [
|
|
|
3460
3699
|
changedFiles.push(versioningPath);
|
|
3461
3700
|
if (!ctx.dryRun) {
|
|
3462
3701
|
writeText(versioningPath, expectedScript);
|
|
3463
|
-
|
|
3702
|
+
chmodSync4(versioningPath, 493);
|
|
3464
3703
|
}
|
|
3465
3704
|
}
|
|
3466
|
-
const manifestPath =
|
|
3705
|
+
const manifestPath = join13(ctx.repoRoot, ".mise", "version-files.conf");
|
|
3467
3706
|
const expectedManifest = templateVersionFilesConf(ctx, ctx.repoRoot);
|
|
3468
3707
|
if (safeReadText(manifestPath) !== expectedManifest) {
|
|
3469
3708
|
changedFiles.push(manifestPath);
|
|
@@ -3479,13 +3718,125 @@ var RULES = [
|
|
|
3479
3718
|
};
|
|
3480
3719
|
}
|
|
3481
3720
|
},
|
|
3721
|
+
{
|
|
3722
|
+
id: "skills.project-manifest",
|
|
3723
|
+
title: "Skillex project skills manifest",
|
|
3724
|
+
audit: (ctx) => {
|
|
3725
|
+
const details = [];
|
|
3726
|
+
const manifestPath = join13(ctx.repoRoot, ".agents", "skills.json");
|
|
3727
|
+
const legacyDir = join13(ctx.repoRoot, ".agents", "skills");
|
|
3728
|
+
const localExamplePath = join13(ctx.repoRoot, ".agents", "local.example.json");
|
|
3729
|
+
const misePath = join13(ctx.repoRoot, "mise.toml");
|
|
3730
|
+
let fixable = true;
|
|
3731
|
+
const manifest = tryParseJson(safeReadText(manifestPath));
|
|
3732
|
+
if (!manifest) {
|
|
3733
|
+
details.push(".agents/skills.json missing or invalid JSON");
|
|
3734
|
+
} else {
|
|
3735
|
+
if (manifest.inherit_global !== true) details.push(".agents/skills.json should set inherit_global: true");
|
|
3736
|
+
if (manifest.registry !== SKILLS_REGISTRY_URL) details.push(`.agents/skills.json should set registry to ${SKILLS_REGISTRY_URL}`);
|
|
3737
|
+
if (!Array.isArray(manifest.skills)) details.push(".agents/skills.json should define a skills array");
|
|
3738
|
+
}
|
|
3739
|
+
if (existsSync10(legacyDir)) {
|
|
3740
|
+
const entries = readdirSync2(legacyDir);
|
|
3741
|
+
if (entries.length > 0) {
|
|
3742
|
+
details.push(".agents/skills/ still contains legacy committed skills; map them into .agents/skills.json before migrating");
|
|
3743
|
+
fixable = false;
|
|
3744
|
+
} else {
|
|
3745
|
+
details.push(".agents/skills/ legacy directory should be removed");
|
|
3746
|
+
}
|
|
3747
|
+
}
|
|
3748
|
+
for (const rel of [".mise/scripts/link-project-skills-to-clis.sh", ".mise/scripts/unlink-project-skills-from-clis.sh"]) {
|
|
3749
|
+
if (existsSync10(join13(ctx.repoRoot, rel))) details.push(`${rel} is a legacy symlink-era script and should be removed`);
|
|
3750
|
+
}
|
|
3751
|
+
const localExample = tryParseJson(safeReadText(localExamplePath));
|
|
3752
|
+
if (localExample && Object.prototype.hasOwnProperty.call(localExample, "skills")) {
|
|
3753
|
+
details.push(".agents/local.example.json still documents legacy skills overrides; drop the skills section");
|
|
3754
|
+
}
|
|
3755
|
+
const mise = safeReadText(misePath);
|
|
3756
|
+
if (!mise?.includes(SYNC_SKILLS_SCRIPT)) details.push("mise.toml should run sync-skills.py --scope project on enter");
|
|
3757
|
+
if (!mise?.includes('patterns = [".agents/skills.json"]')) details.push("mise.toml should watch .agents/skills.json");
|
|
3758
|
+
if (!mise?.includes("[tasks.skills-sync]")) details.push("mise.toml should define a skills-sync task");
|
|
3759
|
+
if (mise?.includes("link-project-skills-to-clis.sh") || mise?.includes("unlink-project-skills-from-clis.sh") || mise?.includes("[tasks.skills-relink]")) {
|
|
3760
|
+
details.push("mise.toml still contains legacy skill-link wiring");
|
|
3761
|
+
}
|
|
3762
|
+
return {
|
|
3763
|
+
id: "skills.project-manifest",
|
|
3764
|
+
title: "Skillex project skills manifest",
|
|
3765
|
+
status: details.length === 0 ? "pass" : "fail",
|
|
3766
|
+
summary: details.length === 0 ? "Skillex skills manifest parity verified" : `${details.length} Skillex migration issue(s) detected`,
|
|
3767
|
+
details,
|
|
3768
|
+
fixable
|
|
3769
|
+
};
|
|
3770
|
+
},
|
|
3771
|
+
migrate: (ctx, finding) => {
|
|
3772
|
+
const changedFiles = [];
|
|
3773
|
+
const details = [];
|
|
3774
|
+
const manifestPath = join13(ctx.repoRoot, ".agents", "skills.json");
|
|
3775
|
+
const legacyDir = join13(ctx.repoRoot, ".agents", "skills");
|
|
3776
|
+
const localExamplePath = join13(ctx.repoRoot, ".agents", "local.example.json");
|
|
3777
|
+
const misePath = join13(ctx.repoRoot, "mise.toml");
|
|
3778
|
+
if (existsSync10(legacyDir)) {
|
|
3779
|
+
const entries = readdirSync2(legacyDir);
|
|
3780
|
+
if (entries.length > 0) {
|
|
3781
|
+
return {
|
|
3782
|
+
id: finding.id,
|
|
3783
|
+
title: finding.title,
|
|
3784
|
+
status: "blocked",
|
|
3785
|
+
summary: "Legacy .agents/skills/ still contains committed skills; migrate them into .agents/skills.json manually first",
|
|
3786
|
+
changedFiles,
|
|
3787
|
+
details: entries.map((entry) => `.agents/skills/${entry}`)
|
|
3788
|
+
};
|
|
3789
|
+
}
|
|
3790
|
+
changedFiles.push(legacyDir);
|
|
3791
|
+
if (!ctx.dryRun) rmSync(legacyDir, { recursive: true, force: true });
|
|
3792
|
+
}
|
|
3793
|
+
const expectedManifest = canonicalSkillsManifest();
|
|
3794
|
+
if (safeReadText(manifestPath) !== expectedManifest) {
|
|
3795
|
+
changedFiles.push(manifestPath);
|
|
3796
|
+
if (!ctx.dryRun) writeText(manifestPath, expectedManifest);
|
|
3797
|
+
details.push("Wrote canonical .agents/skills.json manifest");
|
|
3798
|
+
}
|
|
3799
|
+
for (const rel of [".mise/scripts/link-project-skills-to-clis.sh", ".mise/scripts/unlink-project-skills-from-clis.sh"]) {
|
|
3800
|
+
const path = join13(ctx.repoRoot, rel);
|
|
3801
|
+
if (existsSync10(path)) {
|
|
3802
|
+
changedFiles.push(path);
|
|
3803
|
+
if (!ctx.dryRun) unlinkSync3(path);
|
|
3804
|
+
}
|
|
3805
|
+
}
|
|
3806
|
+
const templateLocalExample = templateCommonProjectText(ctx, ".agents/local.example.json");
|
|
3807
|
+
const currentLocalExample = safeReadText(localExamplePath);
|
|
3808
|
+
if (templateLocalExample && currentLocalExample && currentLocalExample !== templateLocalExample) {
|
|
3809
|
+
changedFiles.push(localExamplePath);
|
|
3810
|
+
if (!ctx.dryRun) writeText(localExamplePath, templateLocalExample);
|
|
3811
|
+
}
|
|
3812
|
+
if (!existsSync10(misePath)) {
|
|
3813
|
+
if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
|
|
3814
|
+
return { id: finding.id, title: finding.title, status: "blocked", summary: "mise.toml missing and no generated-project mise template available to initialize from", changedFiles, details };
|
|
3815
|
+
}
|
|
3816
|
+
}
|
|
3817
|
+
const currentMise = readText(misePath);
|
|
3818
|
+
const nextMise = upsertLinkAgentfilesBlock(currentMise, ctx);
|
|
3819
|
+
if (nextMise !== currentMise) {
|
|
3820
|
+
if (!changedFiles.includes(misePath)) changedFiles.push(misePath);
|
|
3821
|
+
if (!ctx.dryRun) writeText(misePath, nextMise);
|
|
3822
|
+
}
|
|
3823
|
+
return {
|
|
3824
|
+
id: finding.id,
|
|
3825
|
+
title: finding.title,
|
|
3826
|
+
status: changedFiles.length ? "applied" : "noop",
|
|
3827
|
+
summary: changedFiles.length ? "Skillex skills manifest contract normalized" : "No changes required",
|
|
3828
|
+
changedFiles,
|
|
3829
|
+
details
|
|
3830
|
+
};
|
|
3831
|
+
}
|
|
3832
|
+
},
|
|
3482
3833
|
{
|
|
3483
3834
|
id: "sot.agent-symlinks",
|
|
3484
3835
|
title: "AGENTS/CLAUDE/GEMINI symlink contract",
|
|
3485
3836
|
audit: (ctx) => {
|
|
3486
|
-
const agentsPath =
|
|
3487
|
-
if (!
|
|
3488
|
-
const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) =>
|
|
3837
|
+
const agentsPath = join13(ctx.repoRoot, "AGENTS.md");
|
|
3838
|
+
if (!existsSync10(agentsPath)) {
|
|
3839
|
+
const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) => existsSync10(join13(ctx.repoRoot, file)));
|
|
3489
3840
|
if (fallbackSources.length === 0) {
|
|
3490
3841
|
return { id: "sot.agent-symlinks", title: "AGENTS/CLAUDE/GEMINI symlink contract", status: "skip", summary: "AGENTS.md missing; symlink contract not applicable", details: [], fixable: false };
|
|
3491
3842
|
}
|
|
@@ -3500,7 +3851,7 @@ var RULES = [
|
|
|
3500
3851
|
}
|
|
3501
3852
|
const details = [];
|
|
3502
3853
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
3503
|
-
const full =
|
|
3854
|
+
const full = join13(ctx.repoRoot, file);
|
|
3504
3855
|
const target = readSymlinkTarget(full);
|
|
3505
3856
|
if (target !== "AGENTS.md") details.push(`${file} should be a symlink to AGENTS.md`);
|
|
3506
3857
|
}
|
|
@@ -3524,7 +3875,7 @@ var RULES = [
|
|
|
3524
3875
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "AGENTS.md missing; cannot derive canonical agent file", changedFiles, details: [bootstrap.blocked] };
|
|
3525
3876
|
}
|
|
3526
3877
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
3527
|
-
const full =
|
|
3878
|
+
const full = join13(ctx.repoRoot, file);
|
|
3528
3879
|
const result = ensureSymlink(full, "AGENTS.md", ctx.dryRun);
|
|
3529
3880
|
if (result.blocked) blockedDetails.push(result.blocked);
|
|
3530
3881
|
if (result.changed) changedFiles.push(full);
|
|
@@ -3546,7 +3897,7 @@ var RULES = [
|
|
|
3546
3897
|
migrate: (ctx, finding) => {
|
|
3547
3898
|
const changedFiles = [];
|
|
3548
3899
|
const details = [];
|
|
3549
|
-
const path =
|
|
3900
|
+
const path = join13(ctx.repoRoot, ".project.json");
|
|
3550
3901
|
const existing = readProjectJson(ctx) ?? {};
|
|
3551
3902
|
const canonical = canonicalProjectJson(ctx);
|
|
3552
3903
|
const merged = { ...existing, ...canonical };
|
|
@@ -3556,10 +3907,10 @@ var RULES = [
|
|
|
3556
3907
|
changedFiles.push(path);
|
|
3557
3908
|
if (!ctx.dryRun) writeText(path, expected);
|
|
3558
3909
|
}
|
|
3559
|
-
const planeJson =
|
|
3560
|
-
if (
|
|
3910
|
+
const planeJson = join13(ctx.repoRoot, ".plane.json");
|
|
3911
|
+
if (existsSync10(planeJson)) {
|
|
3561
3912
|
const backup = `${planeJson}.migrated-backup`;
|
|
3562
|
-
if (
|
|
3913
|
+
if (existsSync10(backup)) {
|
|
3563
3914
|
details.push(`cannot back up .plane.json because ${relative(ctx.repoRoot, backup)} already exists`);
|
|
3564
3915
|
} else {
|
|
3565
3916
|
changedFiles.push(backup);
|
|
@@ -3581,8 +3932,8 @@ var RULES = [
|
|
|
3581
3932
|
title: ".env.op + gitignore secrets contract",
|
|
3582
3933
|
audit: (ctx) => {
|
|
3583
3934
|
const details = [];
|
|
3584
|
-
const envOp = safeReadText(
|
|
3585
|
-
const gitignore = safeReadText(
|
|
3935
|
+
const envOp = safeReadText(join13(ctx.repoRoot, ".env.op"));
|
|
3936
|
+
const gitignore = safeReadText(join13(ctx.repoRoot, ".gitignore"));
|
|
3586
3937
|
if (!envOp) {
|
|
3587
3938
|
details.push(".env.op missing");
|
|
3588
3939
|
} else {
|
|
@@ -3608,12 +3959,12 @@ var RULES = [
|
|
|
3608
3959
|
migrate: (ctx, finding) => {
|
|
3609
3960
|
const changedFiles = [];
|
|
3610
3961
|
const details = [];
|
|
3611
|
-
const envOpPath =
|
|
3612
|
-
if (!
|
|
3962
|
+
const envOpPath = join13(ctx.repoRoot, ".env.op");
|
|
3963
|
+
if (!existsSync10(envOpPath)) {
|
|
3613
3964
|
changedFiles.push(envOpPath);
|
|
3614
|
-
if (!ctx.dryRun) writeText(envOpPath, readText(
|
|
3965
|
+
if (!ctx.dryRun) writeText(envOpPath, readText(join13(ctx.pjanglerRoot, "templates", "commonproject", "template", ".env.op")));
|
|
3615
3966
|
}
|
|
3616
|
-
const gitignorePath =
|
|
3967
|
+
const gitignorePath = join13(ctx.repoRoot, ".gitignore");
|
|
3617
3968
|
const gitignore = safeReadText(gitignorePath) ?? "";
|
|
3618
3969
|
const requiredBlock = `# Secrets \u2014 .env is materialized by \`op inject -i .env.op > .env\` on mise enter.
|
|
3619
3970
|
# NEVER commit it. .env.op holds only 1Password references or safe literals and IS committed.
|
|
@@ -3640,7 +3991,7 @@ var RULES = [
|
|
|
3640
3991
|
title: ".copier-answers.yml provenance + drift report",
|
|
3641
3992
|
audit: (ctx) => {
|
|
3642
3993
|
const details = [];
|
|
3643
|
-
const path =
|
|
3994
|
+
const path = join13(ctx.repoRoot, ".copier-answers.yml");
|
|
3644
3995
|
const text3 = safeReadText(path);
|
|
3645
3996
|
const project = readProjectJson(ctx);
|
|
3646
3997
|
if (!text3) {
|
|
@@ -3671,12 +4022,12 @@ var RULES = [
|
|
|
3671
4022
|
const changedFiles = [];
|
|
3672
4023
|
const project = canonicalProjectJson(ctx);
|
|
3673
4024
|
const text3 = `# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
|
|
3674
|
-
_src_path: ${
|
|
4025
|
+
_src_path: ${join13(ctx.pjanglerRoot, "templates", "commonproject")}
|
|
3675
4026
|
project_description: ${String(project.project_description)}
|
|
3676
4027
|
project_name: ${String(project.project_name)}
|
|
3677
4028
|
ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
3678
4029
|
`;
|
|
3679
|
-
const path =
|
|
4030
|
+
const path = join13(ctx.repoRoot, ".copier-answers.yml");
|
|
3680
4031
|
if (safeReadText(path) !== text3) {
|
|
3681
4032
|
changedFiles.push(path);
|
|
3682
4033
|
if (!ctx.dryRun) writeText(path, text3);
|
|
@@ -3695,14 +4046,14 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3695
4046
|
id: "bmad.scaffold",
|
|
3696
4047
|
title: "BMAD modules/docs scaffold",
|
|
3697
4048
|
audit: (ctx) => {
|
|
3698
|
-
const targetRoot =
|
|
4049
|
+
const targetRoot = join13(ctx.repoRoot, "_bmad");
|
|
3699
4050
|
const sentinels = [
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
4051
|
+
join13("core", "config.yaml"),
|
|
4052
|
+
join13("config.toml"),
|
|
4053
|
+
join13("_config", "manifest.yaml"),
|
|
4054
|
+
join13("bmm", "config.yaml")
|
|
3704
4055
|
];
|
|
3705
|
-
const missing = sentinels.filter((file) => !
|
|
4056
|
+
const missing = sentinels.filter((file) => !existsSync10(join13(targetRoot, file)));
|
|
3706
4057
|
return {
|
|
3707
4058
|
id: "bmad.scaffold",
|
|
3708
4059
|
title: "BMAD modules/docs scaffold",
|
|
@@ -3716,7 +4067,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3716
4067
|
const changedFiles = [];
|
|
3717
4068
|
if (ctx.dryRun) {
|
|
3718
4069
|
for (const detail of finding.details) {
|
|
3719
|
-
changedFiles.push(
|
|
4070
|
+
changedFiles.push(join13(ctx.repoRoot, detail));
|
|
3720
4071
|
}
|
|
3721
4072
|
return {
|
|
3722
4073
|
id: finding.id,
|
|
@@ -3741,8 +4092,8 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3741
4092
|
};
|
|
3742
4093
|
}
|
|
3743
4094
|
for (const detail of finding.details) {
|
|
3744
|
-
if (
|
|
3745
|
-
changedFiles.push(
|
|
4095
|
+
if (existsSync10(join13(ctx.repoRoot, detail))) {
|
|
4096
|
+
changedFiles.push(join13(ctx.repoRoot, detail));
|
|
3746
4097
|
}
|
|
3747
4098
|
}
|
|
3748
4099
|
return {
|
|
@@ -3765,7 +4116,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3765
4116
|
id: "bmad.version",
|
|
3766
4117
|
title: "BMAD version currency",
|
|
3767
4118
|
status: "skip",
|
|
3768
|
-
summary:
|
|
4119
|
+
summary: existsSync10(join13(ctx.repoRoot, "_bmad")) ? "BMAD installed but version manifest unreadable" : "No BMAD install present",
|
|
3769
4120
|
details: [],
|
|
3770
4121
|
fixable: false
|
|
3771
4122
|
};
|
|
@@ -3820,7 +4171,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3820
4171
|
}
|
|
3821
4172
|
const installed = readInstalledBmadVersion(ctx.repoRoot);
|
|
3822
4173
|
const available = resolveBmadDistTags(ctx.homeDir)?.distTags?.[BMAD_TARGET_CHANNEL];
|
|
3823
|
-
const manifestPath =
|
|
4174
|
+
const manifestPath = join13(ctx.repoRoot, "_bmad", "_config", "manifest.yaml");
|
|
3824
4175
|
if (ctx.dryRun) {
|
|
3825
4176
|
return {
|
|
3826
4177
|
id: finding.id,
|
|
@@ -3867,11 +4218,11 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3867
4218
|
}
|
|
3868
4219
|
const details = [];
|
|
3869
4220
|
for (const rel of ["role.yaml", "SOUL.md", "hermes", ".gitignore", ".scripts/70-systemd.sh", ".scripts/heartbeat.sh", ".scripts/checkpoint.sh", ".runtime-scaffold/README.md", "runtime/memories/MEMORY.md", "runtime/bloodbank-consumer.py"]) {
|
|
3870
|
-
if (!
|
|
4221
|
+
if (!existsSync10(join13(role.roleDir, rel))) details.push(`missing ${relative(ctx.repoRoot, join13(role.roleDir, rel))}`);
|
|
3871
4222
|
}
|
|
3872
|
-
const gitmodules = safeReadText(
|
|
4223
|
+
const gitmodules = safeReadText(join13(ctx.repoRoot, ".gitmodules")) ?? "";
|
|
3873
4224
|
if (!gitmodules.includes(`agents/hermes/${role.role}/runtime`)) details.push(".gitmodules missing pm runtime submodule entry");
|
|
3874
|
-
if (!profileMetaInheritsDefault(
|
|
4225
|
+
if (!profileMetaInheritsDefault(join13(role.roleDir, "runtime", "profile.yaml"))) {
|
|
3875
4226
|
details.push("runtime/profile.yaml missing inherited default config metadata");
|
|
3876
4227
|
}
|
|
3877
4228
|
const registry = safeReadText(registryPath(ctx.homeDir));
|
|
@@ -3892,21 +4243,21 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3892
4243
|
if (!role) {
|
|
3893
4244
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "No pm role present", changedFiles, details: [] };
|
|
3894
4245
|
}
|
|
3895
|
-
const templateRoleDir =
|
|
3896
|
-
writeIfDifferent(
|
|
3897
|
-
writeIfDifferent(
|
|
3898
|
-
writeIfDifferent(
|
|
3899
|
-
copyMissingRecursive(
|
|
3900
|
-
copyMissingRecursive(
|
|
3901
|
-
copyMissingRecursive(
|
|
3902
|
-
const promptSource =
|
|
3903
|
-
const promptTarget =
|
|
3904
|
-
if (
|
|
4246
|
+
const templateRoleDir = join13(ctx.pjanglerRoot, "templates", "hermes-agent", "template");
|
|
4247
|
+
writeIfDifferent(join13(role.roleDir, "SOUL.md"), renderSoul(role), ctx.dryRun, changedFiles);
|
|
4248
|
+
writeIfDifferent(join13(role.roleDir, "hermes"), renderHermesWrapper(role), ctx.dryRun, changedFiles, 493);
|
|
4249
|
+
writeIfDifferent(join13(role.roleDir, ".gitignore"), readText(join13(templateRoleDir, ".gitignore.jinja")).replace(/\{\{ role \}\}/g, role.role), ctx.dryRun, changedFiles);
|
|
4250
|
+
copyMissingRecursive(join13(templateRoleDir, ".runtime-scaffold"), join13(role.roleDir, ".runtime-scaffold"), changedFiles, ctx.dryRun);
|
|
4251
|
+
copyMissingRecursive(join13(templateRoleDir, ".runtime-scaffold"), join13(role.roleDir, "runtime"), changedFiles, ctx.dryRun);
|
|
4252
|
+
copyMissingRecursive(join13(templateRoleDir, ".scripts"), join13(role.roleDir, ".scripts"), changedFiles, ctx.dryRun, (source) => source.endsWith("sentinel.prompt.md.jinja"));
|
|
4253
|
+
const promptSource = join13(templateRoleDir, ".scripts", "sentinel.prompt.md.jinja");
|
|
4254
|
+
const promptTarget = join13(role.roleDir, ".scripts", "sentinel.prompt.md");
|
|
4255
|
+
if (existsSync10(promptSource) && !existsSync10(promptTarget)) {
|
|
3905
4256
|
const prompt = readText(promptSource).replace(/\{\{ agent_id \}\}/g, role.agentId).replace(/\{\{ role \}\}/g, role.role).replace(/\{\{ target_repo \}\}/g, role.repo).replace(/\{\{ display_name \}\}/g, role.displayName || role.agentId);
|
|
3906
4257
|
writeIfDifferent(promptTarget, prompt, ctx.dryRun, changedFiles);
|
|
3907
4258
|
}
|
|
3908
4259
|
upsertSubmodule(ctx.repoRoot, role, changedFiles, ctx.dryRun);
|
|
3909
|
-
const profileMetaUpdated = upsertInheritedProfileMeta(
|
|
4260
|
+
const profileMetaUpdated = upsertInheritedProfileMeta(join13(role.roleDir, "runtime", "profile.yaml"), changedFiles, ctx.dryRun);
|
|
3910
4261
|
if (profileMetaUpdated) details.push(`updated ${profileMetaUpdated}`);
|
|
3911
4262
|
const registryUpdated = upsertRegistryEntry(role, ctx.homeDir, changedFiles, ctx.dryRun);
|
|
3912
4263
|
if (registryUpdated) details.push(`updated ${registryUpdated}`);
|
|
@@ -3938,7 +4289,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3938
4289
|
const details = [];
|
|
3939
4290
|
for (const role of roles) {
|
|
3940
4291
|
const roleRelDir = relative(ctx.repoRoot, role.roleDir);
|
|
3941
|
-
const runtimeRelPath =
|
|
4292
|
+
const runtimeRelPath = join13(roleRelDir, "runtime");
|
|
3942
4293
|
const lsResult = spawnSync6("git", ["ls-files", "--stage", runtimeRelPath], {
|
|
3943
4294
|
cwd: ctx.repoRoot,
|
|
3944
4295
|
encoding: "utf8"
|
|
@@ -3946,8 +4297,8 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3946
4297
|
if (lsResult.status === 0 && lsResult.stdout.trim().length > 0) {
|
|
3947
4298
|
details.push(`submodule runtime is tracked in Git index at ${runtimeRelPath}`);
|
|
3948
4299
|
}
|
|
3949
|
-
const gitignorePath =
|
|
3950
|
-
if (
|
|
4300
|
+
const gitignorePath = join13(role.roleDir, ".gitignore");
|
|
4301
|
+
if (existsSync10(gitignorePath)) {
|
|
3951
4302
|
const content = safeReadText(gitignorePath) ?? "";
|
|
3952
4303
|
const lines = content.split(/\r?\n/).map((line) => line.trim());
|
|
3953
4304
|
if (!lines.includes("runtime/") && !lines.includes("runtime")) {
|
|
@@ -3972,7 +4323,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3972
4323
|
const details = [];
|
|
3973
4324
|
for (const role of roles) {
|
|
3974
4325
|
const roleRelDir = relative(ctx.repoRoot, role.roleDir);
|
|
3975
|
-
const runtimeRelPath =
|
|
4326
|
+
const runtimeRelPath = join13(roleRelDir, "runtime");
|
|
3976
4327
|
const lsResult = spawnSync6("git", ["ls-files", "--stage", runtimeRelPath], {
|
|
3977
4328
|
cwd: ctx.repoRoot,
|
|
3978
4329
|
encoding: "utf8"
|
|
@@ -3987,10 +4338,10 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3987
4338
|
});
|
|
3988
4339
|
}
|
|
3989
4340
|
}
|
|
3990
|
-
const gitignorePath =
|
|
4341
|
+
const gitignorePath = join13(role.roleDir, ".gitignore");
|
|
3991
4342
|
let content = "";
|
|
3992
4343
|
let isIgnored = false;
|
|
3993
|
-
if (
|
|
4344
|
+
if (existsSync10(gitignorePath)) {
|
|
3994
4345
|
content = safeReadText(gitignorePath) ?? "";
|
|
3995
4346
|
const lines = content.split(/\r?\n/).map((line) => line.trim());
|
|
3996
4347
|
isIgnored = lines.includes("runtime/") || lines.includes("runtime");
|
|
@@ -4057,9 +4408,9 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
4057
4408
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "systemd --user unavailable on this host", changedFiles, details };
|
|
4058
4409
|
}
|
|
4059
4410
|
for (const role of roles) {
|
|
4060
|
-
const sysDir =
|
|
4411
|
+
const sysDir = join13(ctx.homeDir, ".config", "systemd", "user");
|
|
4061
4412
|
const units = [`hermes-${role.agentId}-gateway.service`, `hermes-${role.agentId}-consumer.service`, `hermes-${role.agentId}-heartbeat.timer`];
|
|
4062
|
-
const allUnitsPresent = units.every((unit) =>
|
|
4413
|
+
const allUnitsPresent = units.every((unit) => existsSync10(join13(sysDir, unit)));
|
|
4063
4414
|
if (allUnitsPresent) {
|
|
4064
4415
|
if (ctx.dryRun) {
|
|
4065
4416
|
details.push(`would run: systemctl --user enable --now ${units.join(" ")}`);
|
|
@@ -4071,8 +4422,8 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
4071
4422
|
}
|
|
4072
4423
|
continue;
|
|
4073
4424
|
}
|
|
4074
|
-
for (const script of [
|
|
4075
|
-
if (!script || !
|
|
4425
|
+
for (const script of [join13(role.roleDir, ".scripts", "70-systemd.sh")]) {
|
|
4426
|
+
if (!script || !existsSync10(script)) continue;
|
|
4076
4427
|
if (ctx.dryRun) {
|
|
4077
4428
|
details.push(`would run: bash ${script}`);
|
|
4078
4429
|
} else {
|
|
@@ -4099,7 +4450,7 @@ function writeIfDifferent(path, content, dryRun, changedFiles, mode) {
|
|
|
4099
4450
|
changedFiles.push(path);
|
|
4100
4451
|
if (!dryRun) {
|
|
4101
4452
|
writeText(path, normalized);
|
|
4102
|
-
if (mode)
|
|
4453
|
+
if (mode) chmodSync4(path, mode);
|
|
4103
4454
|
}
|
|
4104
4455
|
}
|
|
4105
4456
|
function getParityRuleIds() {
|
|
@@ -4211,15 +4562,15 @@ function formatMigrationReport(report) {
|
|
|
4211
4562
|
}
|
|
4212
4563
|
|
|
4213
4564
|
// src/utils/version.ts
|
|
4214
|
-
import { readFileSync as
|
|
4215
|
-
import { dirname as dirname7, join as
|
|
4565
|
+
import { readFileSync as readFileSync7 } from "node:fs";
|
|
4566
|
+
import { dirname as dirname7, join as join14 } from "node:path";
|
|
4216
4567
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
4217
4568
|
var PJANGLER_VERSION = (() => {
|
|
4218
4569
|
try {
|
|
4219
4570
|
let dir = dirname7(fileURLToPath4(import.meta.url));
|
|
4220
4571
|
for (let i = 0; i < 4; i++) {
|
|
4221
4572
|
try {
|
|
4222
|
-
const raw =
|
|
4573
|
+
const raw = readFileSync7(join14(dir, "package.json"), "utf8");
|
|
4223
4574
|
return JSON.parse(raw).version ?? "0.0.0";
|
|
4224
4575
|
} catch {
|
|
4225
4576
|
const parent = dirname7(dir);
|
|
@@ -4262,9 +4613,9 @@ async function promptForRuleIds(rules) {
|
|
|
4262
4613
|
return selected;
|
|
4263
4614
|
}
|
|
4264
4615
|
function readJson(path) {
|
|
4265
|
-
if (!
|
|
4616
|
+
if (!existsSync11(path)) return void 0;
|
|
4266
4617
|
try {
|
|
4267
|
-
const parsed = JSON.parse(
|
|
4618
|
+
const parsed = JSON.parse(readFileSync8(path, "utf8"));
|
|
4268
4619
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
|
|
4269
4620
|
} catch {
|
|
4270
4621
|
return void 0;
|
|
@@ -4281,8 +4632,8 @@ function packageNameToProjectName(value) {
|
|
|
4281
4632
|
return name.replace(/[-_]+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase()).trim();
|
|
4282
4633
|
}
|
|
4283
4634
|
function deriveProjectDefaults(targetDir) {
|
|
4284
|
-
const manifest = readJson(
|
|
4285
|
-
const pkg = readJson(
|
|
4635
|
+
const manifest = readJson(join15(targetDir, ".project.json"));
|
|
4636
|
+
const pkg = readJson(join15(targetDir, "package.json"));
|
|
4286
4637
|
const name = String(manifest?.project_name ?? "").trim() || packageNameToProjectName(typeof pkg?.name === "string" ? pkg.name : void 0) || packageNameToProjectName(basename4(targetDir)) || "Project";
|
|
4287
4638
|
const ticketProvider = manifest?.ticket_provider && typeof manifest.ticket_provider === "object" ? manifest.ticket_provider : {};
|
|
4288
4639
|
return {
|
|
@@ -4338,7 +4689,7 @@ function actionNeedsRun(plan, kind, syncMode) {
|
|
|
4338
4689
|
if (!action || action.kind !== "project.write-manifest") return false;
|
|
4339
4690
|
const next = `${JSON.stringify(action.manifest, null, 2)}
|
|
4340
4691
|
`;
|
|
4341
|
-
return !
|
|
4692
|
+
return !existsSync11(action.path) || readFileSync8(action.path, "utf8") !== next;
|
|
4342
4693
|
}
|
|
4343
4694
|
if (kind === "copier.copy.commonproject") return true;
|
|
4344
4695
|
if (kind === "ticket-provider.create-or-link") return plan.actions.some((action) => action.kind === kind && action.enabled);
|
|
@@ -4393,7 +4744,7 @@ async function resolveProjectInitTarget(name, options) {
|
|
|
4393
4744
|
if (!targetDir && interactive) {
|
|
4394
4745
|
const defaultName = name ?? basename4(cwd);
|
|
4395
4746
|
const promptedName = name ?? await promptTextValue("Project name", packageNameToProjectName(defaultName));
|
|
4396
|
-
const defaultDir =
|
|
4747
|
+
const defaultDir = join15(cwd, promptedName.replace(/[^A-Za-z0-9._-]/g, "") || promptedName.toLowerCase().replace(/[^a-z0-9]+/g, "-"));
|
|
4397
4748
|
targetDir = await promptTextValue("Project directory", defaultDir);
|
|
4398
4749
|
name = promptedName;
|
|
4399
4750
|
}
|
|
@@ -4401,7 +4752,7 @@ async function resolveProjectInitTarget(name, options) {
|
|
|
4401
4752
|
if (!name) throw new Error("Project name or --target-dir is required when project init is not run inside a git repo");
|
|
4402
4753
|
targetDir = resolve3(process.cwd(), name.replace(/[^A-Za-z0-9._-]/g, "") || name.toLowerCase().replace(/[^a-z0-9]+/g, "-"));
|
|
4403
4754
|
}
|
|
4404
|
-
const targetExists =
|
|
4755
|
+
const targetExists = existsSync11(targetDir);
|
|
4405
4756
|
if (targetExists && !statSync2(targetDir).isDirectory()) throw new Error(`Target path is not a directory: ${targetDir}`);
|
|
4406
4757
|
const targetGitRoot = targetExists ? findGitRoot(targetDir) : void 0;
|
|
4407
4758
|
const syncMode = Boolean(targetGitRoot && resolve3(targetGitRoot) === resolve3(targetDir));
|