@delorenj/pjangler 1.2.12 → 1.2.14
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/mcp-server.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/mcp-server.ts
|
|
4
|
-
import { existsSync as
|
|
5
|
-
import { basename as basename4, dirname as dirname8, join as
|
|
4
|
+
import { existsSync as existsSync10, statSync as statSync2 } from "node:fs";
|
|
5
|
+
import { basename as basename4, dirname as dirname8, join as join13, resolve as resolve3 } from "node:path";
|
|
6
6
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
7
7
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8
8
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -902,10 +902,100 @@ var RunCopierTemplate = class extends Command {
|
|
|
902
902
|
}
|
|
903
903
|
};
|
|
904
904
|
|
|
905
|
-
// src/commands/hermes/
|
|
905
|
+
// src/commands/hermes/UntrackHermesRuntimes.ts
|
|
906
|
+
import { existsSync as existsSync4, readFileSync as readFileSync2, writeFileSync as writeFileSync3, readdirSync } from "fs";
|
|
907
|
+
import { join as join6 } from "path";
|
|
906
908
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
907
|
-
|
|
908
|
-
|
|
909
|
+
var UntrackHermesRuntimes = class extends Command {
|
|
910
|
+
async invoke() {
|
|
911
|
+
const targetDir = this.context.targetDir;
|
|
912
|
+
const rolesDir = join6(targetDir, "agents", "hermes");
|
|
913
|
+
if (!existsSync4(rolesDir)) {
|
|
914
|
+
return {
|
|
915
|
+
success: true,
|
|
916
|
+
message: "No Hermes agents found (no agents/hermes directory)."
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
const roles = readdirSync(rolesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
920
|
+
if (roles.length === 0) {
|
|
921
|
+
return {
|
|
922
|
+
success: true,
|
|
923
|
+
message: "No Hermes agents found."
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
let modifiedAny = false;
|
|
927
|
+
const details = [];
|
|
928
|
+
for (const role of roles) {
|
|
929
|
+
const roleDir = join6("agents", "hermes", role);
|
|
930
|
+
const runtimePath = join6(roleDir, "runtime");
|
|
931
|
+
const gitignorePath = join6(roleDir, ".gitignore");
|
|
932
|
+
let isTracked = false;
|
|
933
|
+
const lsResult = spawnSync2("git", ["ls-files", "--stage", runtimePath], {
|
|
934
|
+
cwd: targetDir,
|
|
935
|
+
encoding: "utf8"
|
|
936
|
+
});
|
|
937
|
+
if (lsResult.status === 0 && lsResult.stdout.trim().length > 0) {
|
|
938
|
+
isTracked = true;
|
|
939
|
+
}
|
|
940
|
+
let isIgnored = false;
|
|
941
|
+
const fullGitignorePath = join6(targetDir, gitignorePath);
|
|
942
|
+
if (existsSync4(fullGitignorePath)) {
|
|
943
|
+
const content = readFileSync2(fullGitignorePath, "utf8");
|
|
944
|
+
const lines = content.split(/\r?\n/).map((line) => line.trim());
|
|
945
|
+
isIgnored = lines.includes("runtime/") || lines.includes("runtime");
|
|
946
|
+
}
|
|
947
|
+
if (isTracked || !isIgnored) {
|
|
948
|
+
modifiedAny = true;
|
|
949
|
+
if (isTracked) {
|
|
950
|
+
details.push(`untrack agents/hermes/${role}/runtime`);
|
|
951
|
+
if (!this.context.dryRun) {
|
|
952
|
+
const rmResult = spawnSync2("git", ["rm", "--cached", "-r", runtimePath], {
|
|
953
|
+
cwd: targetDir,
|
|
954
|
+
encoding: "utf8"
|
|
955
|
+
});
|
|
956
|
+
if (rmResult.status !== 0) {
|
|
957
|
+
return {
|
|
958
|
+
success: false,
|
|
959
|
+
message: `Failed to untrack agents/hermes/${role}/runtime: ${rmResult.stderr}`
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
if (!isIgnored) {
|
|
965
|
+
details.push(`ignore runtime/ in agents/hermes/${role}/.gitignore`);
|
|
966
|
+
if (!this.context.dryRun) {
|
|
967
|
+
let content = "";
|
|
968
|
+
if (existsSync4(fullGitignorePath)) {
|
|
969
|
+
content = readFileSync2(fullGitignorePath, "utf8");
|
|
970
|
+
}
|
|
971
|
+
if (content && !content.endsWith("\n")) {
|
|
972
|
+
content += "\n";
|
|
973
|
+
}
|
|
974
|
+
content += "runtime/\n";
|
|
975
|
+
writeFileSync3(fullGitignorePath, content, "utf8");
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
if (!modifiedAny) {
|
|
981
|
+
return {
|
|
982
|
+
success: true,
|
|
983
|
+
message: "\u2705 All Hermes agent runtimes are already untracked and gitignored."
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
const actionText = this.context.dryRun ? "Would make" : "Made";
|
|
987
|
+
return {
|
|
988
|
+
success: true,
|
|
989
|
+
message: `${actionText} Hermes agent runtimes untracked and gitignored:
|
|
990
|
+
${details.map((d) => ` - ${d}`).join("\n")}`
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
};
|
|
994
|
+
|
|
995
|
+
// src/commands/hermes/WireTelegram.ts
|
|
996
|
+
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
997
|
+
import { join as join7 } from "node:path";
|
|
998
|
+
import { existsSync as existsSync5, unlinkSync } from "node:fs";
|
|
909
999
|
import * as p3 from "@clack/prompts";
|
|
910
1000
|
var WireTelegram = class extends Command {
|
|
911
1001
|
async invoke() {
|
|
@@ -927,7 +1017,7 @@ var WireTelegram = class extends Command {
|
|
|
927
1017
|
let token = process.env.TELEGRAM_BOT_TOKEN;
|
|
928
1018
|
let source = token ? "env" : null;
|
|
929
1019
|
if (!token) {
|
|
930
|
-
const tryOp =
|
|
1020
|
+
const tryOp = spawnSync3("op", ["read", vaultRef], { encoding: "utf8" });
|
|
931
1021
|
if (tryOp.status === 0) {
|
|
932
1022
|
token = tryOp.stdout.trim();
|
|
933
1023
|
source = "op";
|
|
@@ -966,7 +1056,7 @@ var WireTelegram = class extends Command {
|
|
|
966
1056
|
initialValue: true
|
|
967
1057
|
});
|
|
968
1058
|
if (!p3.isCancel(persist) && persist) {
|
|
969
|
-
const create =
|
|
1059
|
+
const create = spawnSync3(
|
|
970
1060
|
"op",
|
|
971
1061
|
[
|
|
972
1062
|
"item",
|
|
@@ -993,18 +1083,18 @@ var WireTelegram = class extends Command {
|
|
|
993
1083
|
if (p3.isCancel(allowedAnswer)) {
|
|
994
1084
|
return { success: false, message: "\u2717 Aborted; Telegram step deferred." };
|
|
995
1085
|
}
|
|
996
|
-
const script =
|
|
997
|
-
if (!
|
|
1086
|
+
const script = join7(roleDir, ".scripts", "30-telegram.sh");
|
|
1087
|
+
if (!existsSync5(script)) {
|
|
998
1088
|
return {
|
|
999
1089
|
success: false,
|
|
1000
1090
|
message: `\u2717 ${script} not found. Did copier finish? Re-run with --skip-runtime-repo=0 if you skipped it.`
|
|
1001
1091
|
};
|
|
1002
1092
|
}
|
|
1003
|
-
const marker =
|
|
1004
|
-
if (
|
|
1093
|
+
const marker = join7(roleDir, ".scripts", ".done-30-telegram");
|
|
1094
|
+
if (existsSync5(marker)) unlinkSync(marker);
|
|
1005
1095
|
const spinner4 = p3.spinner();
|
|
1006
1096
|
spinner4.start("Verifying token + wiring profile");
|
|
1007
|
-
const result =
|
|
1097
|
+
const result = spawnSync3("bash", [script], {
|
|
1008
1098
|
stdio: "inherit",
|
|
1009
1099
|
env: {
|
|
1010
1100
|
...process.env,
|
|
@@ -1027,9 +1117,9 @@ function cap(s) {
|
|
|
1027
1117
|
}
|
|
1028
1118
|
|
|
1029
1119
|
// src/commands/hermes/WireEmail.ts
|
|
1030
|
-
import { spawnSync as
|
|
1031
|
-
import { join as
|
|
1032
|
-
import { existsSync as
|
|
1120
|
+
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
1121
|
+
import { join as join8 } from "node:path";
|
|
1122
|
+
import { existsSync as existsSync6, unlinkSync as unlinkSync2 } from "node:fs";
|
|
1033
1123
|
import * as p4 from "@clack/prompts";
|
|
1034
1124
|
var WireEmail = class extends Command {
|
|
1035
1125
|
async invoke() {
|
|
@@ -1044,13 +1134,13 @@ var WireEmail = class extends Command {
|
|
|
1044
1134
|
if (!targetRepo || !role || !roleDir) {
|
|
1045
1135
|
return { success: false, message: "Cannot wire email: missing target_repo/role/roleDir" };
|
|
1046
1136
|
}
|
|
1047
|
-
const script =
|
|
1048
|
-
if (!
|
|
1137
|
+
const script = join8(roleDir, ".scripts", "50-email.sh");
|
|
1138
|
+
if (!existsSync6(script)) {
|
|
1049
1139
|
return { success: false, message: `\u2717 ${script} not found` };
|
|
1050
1140
|
}
|
|
1051
1141
|
let token = process.env.CF_EMAIL_ROUTING_TOKEN;
|
|
1052
1142
|
if (!token) {
|
|
1053
|
-
const tryOp =
|
|
1143
|
+
const tryOp = spawnSync4(
|
|
1054
1144
|
"op",
|
|
1055
1145
|
["read", "op://DeLoSecrets/Cloudflare-EmailRouting/token"],
|
|
1056
1146
|
{ encoding: "utf8" }
|
|
@@ -1090,7 +1180,7 @@ var WireEmail = class extends Command {
|
|
|
1090
1180
|
initialValue: true
|
|
1091
1181
|
});
|
|
1092
1182
|
if (!p4.isCancel(persist) && persist) {
|
|
1093
|
-
const create =
|
|
1183
|
+
const create = spawnSync4(
|
|
1094
1184
|
"op",
|
|
1095
1185
|
[
|
|
1096
1186
|
"item",
|
|
@@ -1107,11 +1197,11 @@ var WireEmail = class extends Command {
|
|
|
1107
1197
|
}
|
|
1108
1198
|
}
|
|
1109
1199
|
}
|
|
1110
|
-
const marker =
|
|
1111
|
-
if (
|
|
1200
|
+
const marker = join8(roleDir, ".scripts", ".done-50-email");
|
|
1201
|
+
if (existsSync6(marker)) unlinkSync2(marker);
|
|
1112
1202
|
const spinner4 = p4.spinner();
|
|
1113
1203
|
spinner4.start("Creating Cloudflare Email Routing rule");
|
|
1114
|
-
const result =
|
|
1204
|
+
const result = spawnSync4("bash", [script], {
|
|
1115
1205
|
stdio: "inherit",
|
|
1116
1206
|
env: { ...process.env, SKIP_EMAIL: "0", CF_EMAIL_ROUTING_TOKEN: token },
|
|
1117
1207
|
cwd: roleDir
|
|
@@ -1171,7 +1261,7 @@ var PrintHermesSummary = class extends Command {
|
|
|
1171
1261
|
var HermesAgentRecipe = class extends Recipe {
|
|
1172
1262
|
constructor(context) {
|
|
1173
1263
|
super(context);
|
|
1174
|
-
this.addIngredient(EnsureTemplateConfig).addIngredient(PromptForAgentConfig).addIngredient(RunCopierTemplate).addIngredient(WireTelegram).addIngredient(WireEmail).addIngredient(PrintHermesSummary);
|
|
1264
|
+
this.addIngredient(EnsureTemplateConfig).addIngredient(PromptForAgentConfig).addIngredient(RunCopierTemplate).addIngredient(UntrackHermesRuntimes).addIngredient(WireTelegram).addIngredient(WireEmail).addIngredient(PrintHermesSummary);
|
|
1175
1265
|
}
|
|
1176
1266
|
// Override execute() to suppress the base class's per-command logging since
|
|
1177
1267
|
// our commands already render their own UI via @clack/prompts.
|
|
@@ -1195,33 +1285,33 @@ var HermesAgentRecipe = class extends Recipe {
|
|
|
1195
1285
|
|
|
1196
1286
|
// src/commands/AgentHooksCommands.ts
|
|
1197
1287
|
import { homedir as homedir4 } from "node:os";
|
|
1198
|
-
import { join as
|
|
1199
|
-
import { existsSync as
|
|
1288
|
+
import { join as join10, dirname as dirname5 } from "node:path";
|
|
1289
|
+
import { existsSync as existsSync8, cpSync, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "node:fs";
|
|
1200
1290
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1201
1291
|
|
|
1202
1292
|
// src/project/index.ts
|
|
1203
|
-
import { spawnSync as
|
|
1204
|
-
import { existsSync as
|
|
1293
|
+
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
1294
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync3, renameSync, statSync, writeFileSync as writeFileSync4 } from "node:fs";
|
|
1205
1295
|
import { homedir as homedir3 } from "node:os";
|
|
1206
|
-
import { basename as basename2, delimiter, dirname as dirname4, join as
|
|
1296
|
+
import { basename as basename2, delimiter, dirname as dirname4, join as join9, resolve } from "node:path";
|
|
1207
1297
|
import YAML from "yaml";
|
|
1208
1298
|
var PROJECT_REGISTRY_ENV = "PJ_PROJECT_REGISTRY";
|
|
1209
1299
|
var PROJECT_SOURCE_SKILL_ROOTS_ENV = "PJ_SOURCE_SKILL_ROOTS";
|
|
1210
1300
|
var PROJECT_REGISTRY_SCHEMA_VERSION = 1;
|
|
1211
1301
|
var DEFAULT_SOURCE_SKILL_ROOTS = [
|
|
1212
1302
|
"/home/delorenj/code/skillex/all-skills",
|
|
1213
|
-
|
|
1214
|
-
|
|
1303
|
+
join9(homedir3(), ".agents", "skills"),
|
|
1304
|
+
join9(homedir3(), ".codex", "skills")
|
|
1215
1305
|
];
|
|
1216
1306
|
function projectRegistryPath(env2 = process.env) {
|
|
1217
|
-
return expandHome(env2[PROJECT_REGISTRY_ENV] ||
|
|
1307
|
+
return expandHome(env2[PROJECT_REGISTRY_ENV] || join9(homedir3(), ".config", "pjangler", "projects.yaml"));
|
|
1218
1308
|
}
|
|
1219
1309
|
function emptyProjectRegistry() {
|
|
1220
1310
|
return { schema_version: PROJECT_REGISTRY_SCHEMA_VERSION, projects: {} };
|
|
1221
1311
|
}
|
|
1222
1312
|
function loadProjectRegistry(path = projectRegistryPath()) {
|
|
1223
|
-
if (!
|
|
1224
|
-
const raw = YAML.parse(
|
|
1313
|
+
if (!existsSync7(path)) return emptyProjectRegistry();
|
|
1314
|
+
const raw = YAML.parse(readFileSync3(path, "utf8"));
|
|
1225
1315
|
if (raw == null) return emptyProjectRegistry();
|
|
1226
1316
|
if (!isRecord(raw)) throw new Error(`Project registry must be a mapping: ${path}`);
|
|
1227
1317
|
const registry = raw;
|
|
@@ -1236,7 +1326,7 @@ function saveProjectRegistry(registry, path = projectRegistryPath()) {
|
|
|
1236
1326
|
validateProjectRegistry(registry);
|
|
1237
1327
|
mkdirSync4(dirname4(path), { recursive: true });
|
|
1238
1328
|
const temp = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
1239
|
-
|
|
1329
|
+
writeFileSync4(temp, YAML.stringify(registry, { lineWidth: 0 }), "utf8");
|
|
1240
1330
|
renameSync(temp, path);
|
|
1241
1331
|
}
|
|
1242
1332
|
function validateProjectRegistry(registry) {
|
|
@@ -1318,7 +1408,7 @@ function resolveAgentHooksLayer(input, env2 = process.env) {
|
|
|
1318
1408
|
const override = env2.PJ_AGENT_HOOKS_LAYER;
|
|
1319
1409
|
if (override === "0" || override === "false") return false;
|
|
1320
1410
|
if (override === "1" || override === "true") return true;
|
|
1321
|
-
return !
|
|
1411
|
+
return !existsSync7(join9(homedir3(), ".agents", "hooks"));
|
|
1322
1412
|
}
|
|
1323
1413
|
function jsonStable(value) {
|
|
1324
1414
|
return JSON.stringify(value);
|
|
@@ -1349,12 +1439,12 @@ function resolveSourceSkillPath(sourceSkill, env2 = process.env) {
|
|
|
1349
1439
|
if (!sourceSkill) return void 0;
|
|
1350
1440
|
const expanded = expandHome(sourceSkill);
|
|
1351
1441
|
const direct = resolve(expanded);
|
|
1352
|
-
if (
|
|
1442
|
+
if (existsSync7(direct)) return direct;
|
|
1353
1443
|
const name = basename2(sourceSkill);
|
|
1354
1444
|
const roots = sourceSkillRoots(env2);
|
|
1355
1445
|
for (const root of roots) {
|
|
1356
|
-
const candidate =
|
|
1357
|
-
if (
|
|
1446
|
+
const candidate = join9(root, name);
|
|
1447
|
+
if (existsSync7(candidate)) return candidate;
|
|
1358
1448
|
}
|
|
1359
1449
|
const searched = roots.length ? ` Searched roots: ${roots.join(", ")}.` : "";
|
|
1360
1450
|
const hint = `${searched} Add project-specific roots with ${PROJECT_SOURCE_SKILL_ROOTS_ENV}.`;
|
|
@@ -1435,7 +1525,7 @@ function planProjectInit(input) {
|
|
|
1435
1525
|
}));
|
|
1436
1526
|
}
|
|
1437
1527
|
actions.push(
|
|
1438
|
-
{ kind: "project.write-manifest", path:
|
|
1528
|
+
{ kind: "project.write-manifest", path: join9(targetDir, ".project.json"), manifest },
|
|
1439
1529
|
{
|
|
1440
1530
|
kind: "ticket-provider.create-or-link",
|
|
1441
1531
|
enabled: live,
|
|
@@ -1476,7 +1566,7 @@ function executeProjectInitPlan(plan) {
|
|
|
1476
1566
|
action.data.agent_hooks_layer === "false" ? "commonproject: agent-hooks layer skipped (global ~/.agents/hooks detected \u2014 no per-user CLI injection)" : "commonproject: agent-hooks layer included"
|
|
1477
1567
|
);
|
|
1478
1568
|
mkdirSync4(dirname4(action.targetDir), { recursive: true });
|
|
1479
|
-
const result =
|
|
1569
|
+
const result = spawnSync5(action.command[0], action.command.slice(1), { encoding: "utf8", cwd: action.cwd });
|
|
1480
1570
|
if (result.stdout?.trim()) logs.push(result.stdout.trim());
|
|
1481
1571
|
if (result.stderr?.trim()) logs.push(result.stderr.trim());
|
|
1482
1572
|
if (result.error) {
|
|
@@ -1488,7 +1578,7 @@ function executeProjectInitPlan(plan) {
|
|
|
1488
1578
|
}
|
|
1489
1579
|
if (result.status !== 0) {
|
|
1490
1580
|
errors.push(`copier exited with status ${result.status ?? "unknown"}`);
|
|
1491
|
-
if (
|
|
1581
|
+
if (existsSync7(action.targetDir)) changedFiles.push(action.targetDir);
|
|
1492
1582
|
break;
|
|
1493
1583
|
}
|
|
1494
1584
|
changedFiles.push(action.targetDir);
|
|
@@ -1496,9 +1586,9 @@ function executeProjectInitPlan(plan) {
|
|
|
1496
1586
|
mkdirSync4(dirname4(action.path), { recursive: true });
|
|
1497
1587
|
const next = `${JSON.stringify(action.manifest, null, 2)}
|
|
1498
1588
|
`;
|
|
1499
|
-
const current =
|
|
1589
|
+
const current = existsSync7(action.path) ? readFileSync3(action.path, "utf8") : void 0;
|
|
1500
1590
|
if (current !== next) {
|
|
1501
|
-
|
|
1591
|
+
writeFileSync4(action.path, next, "utf8");
|
|
1502
1592
|
changedFiles.push(action.path);
|
|
1503
1593
|
}
|
|
1504
1594
|
} else if (action.kind === "registry.upsert") {
|
|
@@ -1551,7 +1641,7 @@ function getProject(registry, slug) {
|
|
|
1551
1641
|
return project;
|
|
1552
1642
|
}
|
|
1553
1643
|
function buildCommonProjectCopierAction(input) {
|
|
1554
|
-
const templateDir =
|
|
1644
|
+
const templateDir = join9(input.pjanglerRoot, "templates", "commonproject");
|
|
1555
1645
|
const data = {
|
|
1556
1646
|
project_name: input.projectName,
|
|
1557
1647
|
project_description: input.projectDescription ?? "",
|
|
@@ -1580,7 +1670,7 @@ function buildCommonProjectCopierAction(input) {
|
|
|
1580
1670
|
function resolvePjanglerRoot() {
|
|
1581
1671
|
let dir = dirname4(new URL(import.meta.url).pathname);
|
|
1582
1672
|
while (dir !== dirname4(dir)) {
|
|
1583
|
-
if (
|
|
1673
|
+
if (existsSync7(join9(dir, "package.json")) && existsSync7(join9(dir, "templates", "commonproject", "copier.yml"))) return dir;
|
|
1584
1674
|
dir = dirname4(dir);
|
|
1585
1675
|
}
|
|
1586
1676
|
return resolve(process.cwd());
|
|
@@ -1612,7 +1702,7 @@ function validateProjectRecord(project, key) {
|
|
|
1612
1702
|
}
|
|
1613
1703
|
function expandHome(path) {
|
|
1614
1704
|
if (path === "~") return homedir3();
|
|
1615
|
-
if (path.startsWith("~/")) return
|
|
1705
|
+
if (path.startsWith("~/")) return join9(homedir3(), path.slice(2));
|
|
1616
1706
|
return path;
|
|
1617
1707
|
}
|
|
1618
1708
|
function isRecord(value) {
|
|
@@ -1629,16 +1719,16 @@ function resolveTemplateRoot() {
|
|
|
1629
1719
|
try {
|
|
1630
1720
|
let dir = dirname5(fileURLToPath2(import.meta.url));
|
|
1631
1721
|
for (let i = 0; i < 8; i++) {
|
|
1632
|
-
candidates.push(
|
|
1722
|
+
candidates.push(join10(dir, "templates", "commonproject", "template"));
|
|
1633
1723
|
const parent = dirname5(dir);
|
|
1634
1724
|
if (parent === dir) break;
|
|
1635
1725
|
dir = parent;
|
|
1636
1726
|
}
|
|
1637
1727
|
} catch {
|
|
1638
1728
|
}
|
|
1639
|
-
candidates.push(
|
|
1729
|
+
candidates.push(join10(homedir4(), "code", "pjangler", "templates", "commonproject", "template"));
|
|
1640
1730
|
for (const c of candidates) {
|
|
1641
|
-
if (
|
|
1731
|
+
if (existsSync8(join10(c, ".agents", "hooks", "hooks.master.json"))) return c;
|
|
1642
1732
|
}
|
|
1643
1733
|
throw new Error(
|
|
1644
1734
|
"Could not locate the CommonProject template. Set PJANGLER_COMMONPROJECT_TEMPLATE to <repo>/templates/commonproject/template."
|
|
@@ -1665,10 +1755,10 @@ var CopyAgentHooksTree = class extends Command {
|
|
|
1665
1755
|
const created = [];
|
|
1666
1756
|
const skipped = [];
|
|
1667
1757
|
for (const { rel, dir } of items) {
|
|
1668
|
-
const src =
|
|
1669
|
-
const dest =
|
|
1670
|
-
if (!
|
|
1671
|
-
if (
|
|
1758
|
+
const src = join10(templateRoot, rel);
|
|
1759
|
+
const dest = join10(this.context.targetDir, rel);
|
|
1760
|
+
if (!existsSync8(src)) continue;
|
|
1761
|
+
if (existsSync8(dest) && !this.context.force) {
|
|
1672
1762
|
skipped.push(rel);
|
|
1673
1763
|
continue;
|
|
1674
1764
|
}
|
|
@@ -1694,14 +1784,14 @@ var WireMiseAgentHooks = class _WireMiseAgentHooks extends Command {
|
|
|
1694
1784
|
if (!resolveAgentHooksLayer()) {
|
|
1695
1785
|
return { success: true, message: this.formatMessage(AGENT_HOOKS_SKIP_MESSAGE) };
|
|
1696
1786
|
}
|
|
1697
|
-
const misePath =
|
|
1698
|
-
if (!
|
|
1787
|
+
const misePath = join10(this.context.targetDir, "mise.toml");
|
|
1788
|
+
if (!existsSync8(misePath)) {
|
|
1699
1789
|
return {
|
|
1700
1790
|
success: false,
|
|
1701
1791
|
message: "\u26A0\uFE0F No mise.toml found \u2014 run `pjangler init mise` first, then re-run."
|
|
1702
1792
|
};
|
|
1703
1793
|
}
|
|
1704
|
-
let content =
|
|
1794
|
+
let content = readFileSync4(misePath, "utf8");
|
|
1705
1795
|
if (content.includes(_WireMiseAgentHooks.MARKER)) {
|
|
1706
1796
|
return { success: true, message: this.formatMessage("\u2713 mise.toml already wired for agent-hooks") };
|
|
1707
1797
|
}
|
|
@@ -1777,7 +1867,7 @@ ${leaveBlock}`);
|
|
|
1777
1867
|
""
|
|
1778
1868
|
].join("\n");
|
|
1779
1869
|
content = content.replace(/\n*$/, "\n") + appended;
|
|
1780
|
-
if (!this.context.dryRun)
|
|
1870
|
+
if (!this.context.dryRun) writeFileSync5(misePath, content);
|
|
1781
1871
|
if (wiredHooks) {
|
|
1782
1872
|
return { success: true, message: this.formatMessage("\u2705 Wired mise.toml ([hooks] enter/leave + tasks)") };
|
|
1783
1873
|
}
|
|
@@ -1931,15 +2021,15 @@ function createRecipe(name, context) {
|
|
|
1931
2021
|
}
|
|
1932
2022
|
|
|
1933
2023
|
// src/utils/version.ts
|
|
1934
|
-
import { readFileSync as
|
|
1935
|
-
import { dirname as dirname6, join as
|
|
2024
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
2025
|
+
import { dirname as dirname6, join as join11 } from "node:path";
|
|
1936
2026
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
1937
2027
|
var PJANGLER_VERSION = (() => {
|
|
1938
2028
|
try {
|
|
1939
2029
|
let dir = dirname6(fileURLToPath3(import.meta.url));
|
|
1940
2030
|
for (let i = 0; i < 4; i++) {
|
|
1941
2031
|
try {
|
|
1942
|
-
const raw =
|
|
2032
|
+
const raw = readFileSync5(join11(dir, "package.json"), "utf8");
|
|
1943
2033
|
return JSON.parse(raw).version ?? "0.0.0";
|
|
1944
2034
|
} catch {
|
|
1945
2035
|
const parent = dirname6(dir);
|
|
@@ -1953,11 +2043,12 @@ var PJANGLER_VERSION = (() => {
|
|
|
1953
2043
|
})();
|
|
1954
2044
|
|
|
1955
2045
|
// src/parity/index.ts
|
|
1956
|
-
import { existsSync as
|
|
1957
|
-
import { basename as basename3, dirname as dirname7, join as
|
|
2046
|
+
import { existsSync as existsSync9, lstatSync, mkdirSync as mkdirSync6, readFileSync as readFileSync6, readlinkSync, readdirSync as readdirSync2, renameSync as renameSync2, symlinkSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync6, chmodSync as chmodSync2, copyFileSync } from "node:fs";
|
|
2047
|
+
import { basename as basename3, dirname as dirname7, join as join12, relative, resolve as resolve2 } from "node:path";
|
|
1958
2048
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
1959
2049
|
import { homedir as homedir5 } from "node:os";
|
|
1960
|
-
import { spawnSync as
|
|
2050
|
+
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
2051
|
+
import YAML2 from "yaml";
|
|
1961
2052
|
var LINK_AGENTFILES_BLOCK = `# This block will handle the linking of
|
|
1962
2053
|
# agent files to the main AGENTS.md file.
|
|
1963
2054
|
#
|
|
@@ -2028,7 +2119,7 @@ run = "{{config_root}}/.mise/scripts/versioning.sh sync"
|
|
|
2028
2119
|
function resolvePjanglerRoot2() {
|
|
2029
2120
|
let dir = dirname7(fileURLToPath4(import.meta.url));
|
|
2030
2121
|
while (dir !== dirname7(dir)) {
|
|
2031
|
-
if (
|
|
2122
|
+
if (existsSync9(join12(dir, "package.json")) && existsSync9(join12(dir, "templates", "commonproject", "copier.yml"))) {
|
|
2032
2123
|
return dir;
|
|
2033
2124
|
}
|
|
2034
2125
|
dir = dirname7(dir);
|
|
@@ -2039,17 +2130,17 @@ function normalizeNewlines(value) {
|
|
|
2039
2130
|
return value.replace(/\r\n/g, "\n");
|
|
2040
2131
|
}
|
|
2041
2132
|
function readText(path) {
|
|
2042
|
-
return normalizeNewlines(
|
|
2133
|
+
return normalizeNewlines(readFileSync6(path, "utf8"));
|
|
2043
2134
|
}
|
|
2044
2135
|
function safeReadText(path) {
|
|
2045
|
-
return
|
|
2136
|
+
return existsSync9(path) ? readText(path) : null;
|
|
2046
2137
|
}
|
|
2047
2138
|
function ensureParent(path) {
|
|
2048
2139
|
mkdirSync6(dirname7(path), { recursive: true });
|
|
2049
2140
|
}
|
|
2050
2141
|
function writeText(path, content) {
|
|
2051
2142
|
ensureParent(path);
|
|
2052
|
-
|
|
2143
|
+
writeFileSync6(path, content);
|
|
2053
2144
|
}
|
|
2054
2145
|
function tryParseJson(text2) {
|
|
2055
2146
|
if (!text2) return null;
|
|
@@ -2066,7 +2157,7 @@ function titleCaseSlug(slug) {
|
|
|
2066
2157
|
return slug.split(/[-_]/g).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
2067
2158
|
}
|
|
2068
2159
|
function readSymlinkTarget(path) {
|
|
2069
|
-
if (!
|
|
2160
|
+
if (!existsSync9(path)) return null;
|
|
2070
2161
|
try {
|
|
2071
2162
|
return readlinkSync(path);
|
|
2072
2163
|
} catch {
|
|
@@ -2074,7 +2165,7 @@ function readSymlinkTarget(path) {
|
|
|
2074
2165
|
}
|
|
2075
2166
|
}
|
|
2076
2167
|
function ensureSymlink(path, target, dryRun) {
|
|
2077
|
-
if (
|
|
2168
|
+
if (existsSync9(path)) {
|
|
2078
2169
|
const stat = lstatSync(path);
|
|
2079
2170
|
if (stat.isSymbolicLink()) {
|
|
2080
2171
|
const current = readSymlinkTarget(path);
|
|
@@ -2091,11 +2182,11 @@ function ensureSymlink(path, target, dryRun) {
|
|
|
2091
2182
|
return { changed: true };
|
|
2092
2183
|
}
|
|
2093
2184
|
function bootstrapAgentsFile(repoRoot, dryRun) {
|
|
2094
|
-
const agentsPath =
|
|
2095
|
-
if (
|
|
2185
|
+
const agentsPath = join12(repoRoot, "AGENTS.md");
|
|
2186
|
+
if (existsSync9(agentsPath)) return { changedFiles: [], details: [] };
|
|
2096
2187
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
2097
|
-
const source =
|
|
2098
|
-
if (!
|
|
2188
|
+
const source = join12(repoRoot, file);
|
|
2189
|
+
if (!existsSync9(source)) continue;
|
|
2099
2190
|
const stat = lstatSync(source);
|
|
2100
2191
|
if (stat.isSymbolicLink()) continue;
|
|
2101
2192
|
if (stat.isFile()) {
|
|
@@ -2104,8 +2195,8 @@ function bootstrapAgentsFile(repoRoot, dryRun) {
|
|
|
2104
2195
|
}
|
|
2105
2196
|
return { changedFiles: [], details: [], blocked: `${file} exists but is not a regular file; cannot promote to AGENTS.md` };
|
|
2106
2197
|
}
|
|
2107
|
-
const readmePath =
|
|
2108
|
-
if (
|
|
2198
|
+
const readmePath = join12(repoRoot, "README.md");
|
|
2199
|
+
if (existsSync9(readmePath)) {
|
|
2109
2200
|
const stat = lstatSync(readmePath);
|
|
2110
2201
|
if (!stat.isFile()) return { changedFiles: [], details: [], blocked: "README.md exists but is not a regular file; cannot copy to AGENTS.md" };
|
|
2111
2202
|
if (!dryRun) copyFileSync(readmePath, agentsPath);
|
|
@@ -2144,12 +2235,12 @@ function yamlGet(text2, keyPath) {
|
|
|
2144
2235
|
return "";
|
|
2145
2236
|
}
|
|
2146
2237
|
function discoverRoles(repoRoot) {
|
|
2147
|
-
const rolesDir =
|
|
2148
|
-
if (!
|
|
2149
|
-
return
|
|
2150
|
-
const roleDir =
|
|
2151
|
-
const roleYamlPath =
|
|
2152
|
-
if (!
|
|
2238
|
+
const rolesDir = join12(repoRoot, "agents", "hermes");
|
|
2239
|
+
if (!existsSync9(rolesDir)) return [];
|
|
2240
|
+
return readdirSync2(rolesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => {
|
|
2241
|
+
const roleDir = join12(rolesDir, entry.name);
|
|
2242
|
+
const roleYamlPath = join12(roleDir, "role.yaml");
|
|
2243
|
+
if (!existsSync9(roleYamlPath)) return null;
|
|
2153
2244
|
const text2 = readText(roleYamlPath);
|
|
2154
2245
|
const runtimeRepoRaw = yamlGet(text2, "runtime.github_repo");
|
|
2155
2246
|
return {
|
|
@@ -2177,10 +2268,10 @@ function discoverRoles(repoRoot) {
|
|
|
2177
2268
|
}).filter((value) => Boolean(value));
|
|
2178
2269
|
}
|
|
2179
2270
|
function registryPath(homeDir) {
|
|
2180
|
-
return
|
|
2271
|
+
return join12(homeDir, ".hermes", "agents-registry.yaml");
|
|
2181
2272
|
}
|
|
2182
2273
|
function systemctlUser(args) {
|
|
2183
|
-
const result =
|
|
2274
|
+
const result = spawnSync6("systemctl", ["--user", ...args], { encoding: "utf8" });
|
|
2184
2275
|
return {
|
|
2185
2276
|
ok: result.status === 0,
|
|
2186
2277
|
stdout: result.stdout.trim(),
|
|
@@ -2188,8 +2279,8 @@ function systemctlUser(args) {
|
|
|
2188
2279
|
};
|
|
2189
2280
|
}
|
|
2190
2281
|
function templateScript(ctx, name) {
|
|
2191
|
-
const source =
|
|
2192
|
-
return
|
|
2282
|
+
const source = join12(ctx.pjanglerRoot, ".mise", "scripts", name);
|
|
2283
|
+
return existsSync9(source) ? readText(source) : void 0;
|
|
2193
2284
|
}
|
|
2194
2285
|
function templateVersioningScript(ctx) {
|
|
2195
2286
|
return templateScript(ctx, "versioning.sh");
|
|
@@ -2201,8 +2292,8 @@ function resolveAgentHooksLayer2(ctx) {
|
|
|
2201
2292
|
const override = process.env.PJ_AGENT_HOOKS_LAYER;
|
|
2202
2293
|
if (override === "0" || override === "false") return false;
|
|
2203
2294
|
if (override === "1" || override === "true") return true;
|
|
2204
|
-
if (
|
|
2205
|
-
return !
|
|
2295
|
+
if (existsSync9(join12(ctx.repoRoot, ".agents", "hooks", "sync.py"))) return true;
|
|
2296
|
+
return !existsSync9(join12(ctx.homeDir, ".agents", "hooks"));
|
|
2206
2297
|
}
|
|
2207
2298
|
function evaluateMiseConditionals(template, agentHooksLayer) {
|
|
2208
2299
|
const out = [];
|
|
@@ -2232,10 +2323,10 @@ function renderGeneratedProjectMiseToml(ctx, template) {
|
|
|
2232
2323
|
return evaluateMiseConditionals(template, resolveAgentHooksLayer2(ctx)).replace(/\{%\s*raw\s*%\}([\s\S]*?)\{%\s*endraw\s*%\}/g, "$1").replace(/\{\{\s*project_name\s*\}\}/g, projectName);
|
|
2233
2324
|
}
|
|
2234
2325
|
function ensureMiseTomlFromTemplate(ctx, changedFiles) {
|
|
2235
|
-
const targetPath =
|
|
2236
|
-
if (
|
|
2237
|
-
const sourcePath =
|
|
2238
|
-
if (!
|
|
2326
|
+
const targetPath = join12(ctx.repoRoot, "mise.toml");
|
|
2327
|
+
if (existsSync9(targetPath)) return false;
|
|
2328
|
+
const sourcePath = join12(ctx.pjanglerRoot, "templates", "commonproject", "template", "mise.toml.jinja");
|
|
2329
|
+
if (!existsSync9(sourcePath)) return false;
|
|
2239
2330
|
changedFiles.push(targetPath);
|
|
2240
2331
|
if (!ctx.dryRun) {
|
|
2241
2332
|
writeText(targetPath, renderGeneratedProjectMiseToml(ctx, readText(sourcePath)));
|
|
@@ -2243,8 +2334,8 @@ function ensureMiseTomlFromTemplate(ctx, changedFiles) {
|
|
|
2243
2334
|
return true;
|
|
2244
2335
|
}
|
|
2245
2336
|
function templateVersionFilesConf(ctx, repoRoot) {
|
|
2246
|
-
const packageJson =
|
|
2247
|
-
return
|
|
2337
|
+
const packageJson = join12(repoRoot, "package.json");
|
|
2338
|
+
return existsSync9(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";
|
|
2248
2339
|
}
|
|
2249
2340
|
function replaceOrAppendManagedBlock(text2, startMarker, block, beforePattern) {
|
|
2250
2341
|
if (startMarker.test(text2)) {
|
|
@@ -2268,7 +2359,7 @@ var CONDITIONAL_HERMES_PATHS = ["agents/hermes/pm/hermes", "agent/hermes/pm/herm
|
|
|
2268
2359
|
function requiredMisePathEntries(ctx) {
|
|
2269
2360
|
const required = [...BASE_MISE_PATH_ENTRIES];
|
|
2270
2361
|
for (const candidate of CONDITIONAL_HERMES_PATHS) {
|
|
2271
|
-
if (
|
|
2362
|
+
if (existsSync9(join12(ctx.repoRoot, candidate)) && !required.includes(candidate)) required.push(candidate);
|
|
2272
2363
|
}
|
|
2273
2364
|
return required;
|
|
2274
2365
|
}
|
|
@@ -2417,7 +2508,7 @@ function upsertLinkAgentfilesBlock(text2, ctx) {
|
|
|
2417
2508
|
return insertTomlBlockBeforeVersioning(cleaned, LINK_AGENTFILES_WATCH_TASK_BLOCK);
|
|
2418
2509
|
}
|
|
2419
2510
|
function readProjectJson(ctx) {
|
|
2420
|
-
return tryParseJson(safeReadText(
|
|
2511
|
+
return tryParseJson(safeReadText(join12(ctx.repoRoot, ".project.json")));
|
|
2421
2512
|
}
|
|
2422
2513
|
function boolSetting(value, fallback) {
|
|
2423
2514
|
if (typeof value === "boolean") return value;
|
|
@@ -2492,12 +2583,12 @@ function canonicalProjectJson(ctx) {
|
|
|
2492
2583
|
};
|
|
2493
2584
|
}
|
|
2494
2585
|
function projectJsonFinding(ctx) {
|
|
2495
|
-
const projectPath =
|
|
2496
|
-
const planeJsonPath =
|
|
2586
|
+
const projectPath = join12(ctx.repoRoot, ".project.json");
|
|
2587
|
+
const planeJsonPath = join12(ctx.repoRoot, ".plane.json");
|
|
2497
2588
|
const details = [];
|
|
2498
2589
|
const data = readProjectJson(ctx);
|
|
2499
2590
|
const roles = discoverRoles(ctx.repoRoot);
|
|
2500
|
-
if (!
|
|
2591
|
+
if (!existsSync9(projectPath)) {
|
|
2501
2592
|
return { id: "sot.project-json", title: "Canonical .project.json", status: "fail", summary: ".project.json missing", details: [], fixable: true };
|
|
2502
2593
|
}
|
|
2503
2594
|
if (!data) {
|
|
@@ -2532,7 +2623,7 @@ function projectJsonFinding(ctx) {
|
|
|
2532
2623
|
for (const key of ["enabled", "grace_hours", "auto_review"]) {
|
|
2533
2624
|
if (!(key in reconcile)) details.push(`automation.reconcile.${key} missing`);
|
|
2534
2625
|
}
|
|
2535
|
-
if (
|
|
2626
|
+
if (existsSync9(planeJsonPath)) details.push(".plane.json should not exist once .project.json is canonical");
|
|
2536
2627
|
return {
|
|
2537
2628
|
id: "sot.project-json",
|
|
2538
2629
|
title: "Canonical .project.json",
|
|
@@ -2613,17 +2704,17 @@ exec env HERMES_HOME="$HERMES_HOME" HERMES_FLEET_ENV="$FLEET_ENV" HERMES_OAUTH
|
|
|
2613
2704
|
`.replace(/\u0010/g, "$");
|
|
2614
2705
|
}
|
|
2615
2706
|
function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip) {
|
|
2616
|
-
if (!
|
|
2707
|
+
if (!existsSync9(sourceDir)) return;
|
|
2617
2708
|
mkdirSync6(targetDir, { recursive: true });
|
|
2618
|
-
for (const entry of
|
|
2619
|
-
const sourcePath =
|
|
2709
|
+
for (const entry of readdirSync2(sourceDir, { withFileTypes: true })) {
|
|
2710
|
+
const sourcePath = join12(sourceDir, entry.name);
|
|
2620
2711
|
if (skip?.(sourcePath)) continue;
|
|
2621
|
-
const targetPath =
|
|
2712
|
+
const targetPath = join12(targetDir, entry.name);
|
|
2622
2713
|
if (entry.isDirectory()) {
|
|
2623
2714
|
copyMissingRecursive(sourcePath, targetPath, changedFiles, dryRun, skip);
|
|
2624
2715
|
continue;
|
|
2625
2716
|
}
|
|
2626
|
-
if (
|
|
2717
|
+
if (existsSync9(targetPath)) continue;
|
|
2627
2718
|
changedFiles.push(targetPath);
|
|
2628
2719
|
if (!dryRun) {
|
|
2629
2720
|
ensureParent(targetPath);
|
|
@@ -2632,7 +2723,7 @@ function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip)
|
|
|
2632
2723
|
}
|
|
2633
2724
|
}
|
|
2634
2725
|
function upsertSubmodule(repoRoot, role, changedFiles, dryRun) {
|
|
2635
|
-
const gitmodulesPath =
|
|
2726
|
+
const gitmodulesPath = join12(repoRoot, ".gitmodules");
|
|
2636
2727
|
const repoName = role.runtimeRepo || `agent-hm-${role.repo}-${role.role}`;
|
|
2637
2728
|
const owner = role.runtimeOwner || "delorenj";
|
|
2638
2729
|
const block = `[submodule "agents/hermes/${role.role}/runtime"]
|
|
@@ -2727,19 +2818,186 @@ function checkUnit(unit) {
|
|
|
2727
2818
|
const active = systemctlUser(["is-active", unit]).ok;
|
|
2728
2819
|
return { enabled, active };
|
|
2729
2820
|
}
|
|
2821
|
+
var BMAD_NPM_PACKAGE = "bmad-method";
|
|
2822
|
+
var BMAD_TARGET_CHANNEL = "next";
|
|
2823
|
+
var BMAD_DIST_TAGS_TTL_MS = 60 * 60 * 1e3;
|
|
2824
|
+
var BMAD_INSTALL_TOOLS = [
|
|
2825
|
+
"claude-code",
|
|
2826
|
+
"codex",
|
|
2827
|
+
"cursor",
|
|
2828
|
+
"github-copilot",
|
|
2829
|
+
"adal",
|
|
2830
|
+
"antigravity-cli",
|
|
2831
|
+
"auggie",
|
|
2832
|
+
"goose",
|
|
2833
|
+
"cline",
|
|
2834
|
+
"codebuddy",
|
|
2835
|
+
"codewhale",
|
|
2836
|
+
"command-code",
|
|
2837
|
+
"crush",
|
|
2838
|
+
"droid",
|
|
2839
|
+
"firebender",
|
|
2840
|
+
"gemini",
|
|
2841
|
+
"antigravity",
|
|
2842
|
+
"hermes",
|
|
2843
|
+
"bob",
|
|
2844
|
+
"iflow",
|
|
2845
|
+
"junie",
|
|
2846
|
+
"kilo",
|
|
2847
|
+
"kimi-code",
|
|
2848
|
+
"kiro",
|
|
2849
|
+
"kode",
|
|
2850
|
+
"mistral-vibe",
|
|
2851
|
+
"mux",
|
|
2852
|
+
"neovate",
|
|
2853
|
+
"ona",
|
|
2854
|
+
"openclaw",
|
|
2855
|
+
"opencode",
|
|
2856
|
+
"openhands",
|
|
2857
|
+
"pi",
|
|
2858
|
+
"pochi",
|
|
2859
|
+
"qoder",
|
|
2860
|
+
"qwen",
|
|
2861
|
+
"replit",
|
|
2862
|
+
"roo",
|
|
2863
|
+
"rovo-dev",
|
|
2864
|
+
"cortex",
|
|
2865
|
+
"amp",
|
|
2866
|
+
"trae",
|
|
2867
|
+
"warp",
|
|
2868
|
+
"windsurf",
|
|
2869
|
+
"zencoder"
|
|
2870
|
+
];
|
|
2871
|
+
function bmadInstallArgs(repoRoot) {
|
|
2872
|
+
return [
|
|
2873
|
+
"-y",
|
|
2874
|
+
`${BMAD_NPM_PACKAGE}@${BMAD_TARGET_CHANNEL}`,
|
|
2875
|
+
"install",
|
|
2876
|
+
"--yes",
|
|
2877
|
+
"--directory",
|
|
2878
|
+
repoRoot,
|
|
2879
|
+
"--modules",
|
|
2880
|
+
"bmm,bmb,cis",
|
|
2881
|
+
"--tools",
|
|
2882
|
+
BMAD_INSTALL_TOOLS.join(",")
|
|
2883
|
+
];
|
|
2884
|
+
}
|
|
2885
|
+
function runBmadInstall(repoRoot) {
|
|
2886
|
+
const result = spawnSync6("npx", bmadInstallArgs(repoRoot), { encoding: "utf8" });
|
|
2887
|
+
if (result.status !== 0) {
|
|
2888
|
+
return { ok: false, error: result.stderr || result.error?.message || "Unknown error" };
|
|
2889
|
+
}
|
|
2890
|
+
return { ok: true };
|
|
2891
|
+
}
|
|
2892
|
+
function readInstalledBmadVersion(repoRoot) {
|
|
2893
|
+
const raw = safeReadText(join12(repoRoot, "_bmad", "_config", "manifest.yaml"));
|
|
2894
|
+
if (!raw) return void 0;
|
|
2895
|
+
try {
|
|
2896
|
+
const parsed = YAML2.parse(raw);
|
|
2897
|
+
const version = parsed?.installation?.version;
|
|
2898
|
+
return typeof version === "string" && version.trim() ? version.trim() : void 0;
|
|
2899
|
+
} catch {
|
|
2900
|
+
return void 0;
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
2903
|
+
function bmadCachePath(homeDir) {
|
|
2904
|
+
const cacheRoot = process.env.XDG_CACHE_HOME?.trim() || join12(homeDir, ".cache");
|
|
2905
|
+
return join12(cacheRoot, "pjangler", "bmad-dist-tags.json");
|
|
2906
|
+
}
|
|
2907
|
+
function readBmadDistTagsCache(homeDir) {
|
|
2908
|
+
const raw = safeReadText(bmadCachePath(homeDir));
|
|
2909
|
+
if (!raw) return void 0;
|
|
2910
|
+
try {
|
|
2911
|
+
const parsed = JSON.parse(raw);
|
|
2912
|
+
if (parsed && typeof parsed.fetchedAt === "number" && parsed.distTags && typeof parsed.distTags === "object") {
|
|
2913
|
+
return parsed;
|
|
2914
|
+
}
|
|
2915
|
+
} catch {
|
|
2916
|
+
}
|
|
2917
|
+
return void 0;
|
|
2918
|
+
}
|
|
2919
|
+
function fetchBmadDistTags() {
|
|
2920
|
+
const result = spawnSync6("npm", ["view", BMAD_NPM_PACKAGE, "dist-tags", "--json"], {
|
|
2921
|
+
encoding: "utf8",
|
|
2922
|
+
timeout: 8e3
|
|
2923
|
+
});
|
|
2924
|
+
if (result.status !== 0 || !result.stdout.trim()) return void 0;
|
|
2925
|
+
try {
|
|
2926
|
+
const parsed = JSON.parse(result.stdout);
|
|
2927
|
+
const obj = Array.isArray(parsed) ? parsed[0] : parsed;
|
|
2928
|
+
if (!obj || typeof obj !== "object") return void 0;
|
|
2929
|
+
const tags = {};
|
|
2930
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
2931
|
+
if (typeof value === "string") tags[key] = value;
|
|
2932
|
+
}
|
|
2933
|
+
return Object.keys(tags).length ? tags : void 0;
|
|
2934
|
+
} catch {
|
|
2935
|
+
return void 0;
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
2938
|
+
function resolveBmadDistTags(homeDir) {
|
|
2939
|
+
const cached = readBmadDistTagsCache(homeDir);
|
|
2940
|
+
if (cached && Date.now() - cached.fetchedAt < BMAD_DIST_TAGS_TTL_MS) {
|
|
2941
|
+
return { distTags: cached.distTags, stale: false };
|
|
2942
|
+
}
|
|
2943
|
+
const fetched = fetchBmadDistTags();
|
|
2944
|
+
if (fetched) {
|
|
2945
|
+
try {
|
|
2946
|
+
const path = bmadCachePath(homeDir);
|
|
2947
|
+
mkdirSync6(dirname7(path), { recursive: true });
|
|
2948
|
+
writeFileSync6(path, JSON.stringify({ fetchedAt: Date.now(), distTags: fetched }, null, 2));
|
|
2949
|
+
} catch {
|
|
2950
|
+
}
|
|
2951
|
+
return { distTags: fetched, stale: false };
|
|
2952
|
+
}
|
|
2953
|
+
if (cached) return { distTags: cached.distTags, stale: true };
|
|
2954
|
+
return void 0;
|
|
2955
|
+
}
|
|
2956
|
+
function compareBmadVersions(a, b) {
|
|
2957
|
+
const parse = (v) => {
|
|
2958
|
+
const [core = "0", pre = ""] = v.replace(/^v/, "").split("-", 2);
|
|
2959
|
+
const parts = core.split(".");
|
|
2960
|
+
const n = (i) => parseInt(parts[i] ?? "0", 10) || 0;
|
|
2961
|
+
return { nums: [n(0), n(1), n(2)], pre };
|
|
2962
|
+
};
|
|
2963
|
+
const pa = parse(a);
|
|
2964
|
+
const pb = parse(b);
|
|
2965
|
+
for (let i = 0; i < 3; i++) {
|
|
2966
|
+
if (pa.nums[i] !== pb.nums[i]) return pa.nums[i] - pb.nums[i];
|
|
2967
|
+
}
|
|
2968
|
+
if (pa.pre === pb.pre) return 0;
|
|
2969
|
+
if (!pa.pre) return 1;
|
|
2970
|
+
if (!pb.pre) return -1;
|
|
2971
|
+
const ida = pa.pre.split(".");
|
|
2972
|
+
const idb = pb.pre.split(".");
|
|
2973
|
+
for (let i = 0; i < Math.max(ida.length, idb.length); i++) {
|
|
2974
|
+
const xa = ida[i];
|
|
2975
|
+
const xb = idb[i];
|
|
2976
|
+
if (xa === void 0) return -1;
|
|
2977
|
+
if (xb === void 0) return 1;
|
|
2978
|
+
const na = Number(xa);
|
|
2979
|
+
const nb = Number(xb);
|
|
2980
|
+
if (!Number.isNaN(na) && !Number.isNaN(nb)) {
|
|
2981
|
+
if (na !== nb) return na - nb;
|
|
2982
|
+
} else if (xa !== xb) {
|
|
2983
|
+
return xa < xb ? -1 : 1;
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2986
|
+
return 0;
|
|
2987
|
+
}
|
|
2730
2988
|
var RULES = [
|
|
2731
2989
|
{
|
|
2732
2990
|
id: "mise.config-root",
|
|
2733
2991
|
title: "mise config_root + AGENTS link hooks",
|
|
2734
2992
|
audit: (ctx) => {
|
|
2735
|
-
const misePath =
|
|
2736
|
-
if (!
|
|
2993
|
+
const misePath = join12(ctx.repoRoot, "mise.toml");
|
|
2994
|
+
if (!existsSync9(misePath)) {
|
|
2737
2995
|
return { id: "mise.config-root", title: "mise config_root + AGENTS link hooks", status: "fail", summary: "mise.toml missing", details: [], fixable: true };
|
|
2738
2996
|
}
|
|
2739
2997
|
const text2 = readText(misePath);
|
|
2740
2998
|
const details = [];
|
|
2741
|
-
const linkAgentfilesPath =
|
|
2742
|
-
if (!
|
|
2999
|
+
const linkAgentfilesPath = join12(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
|
|
3000
|
+
if (!existsSync9(linkAgentfilesPath)) details.push(".mise/scripts/link-agentfiles.sh missing");
|
|
2743
3001
|
const pathValues = [...(text2.match(/^_\.path\s*=\s*\[([^\]]*)\]/m)?.[1] ?? "").matchAll(/"([^"]+)"/g)].map((match) => match[1]);
|
|
2744
3002
|
const missingPathValues = requiredMisePathEntries(ctx).filter((value) => !pathValues.includes(value));
|
|
2745
3003
|
if (missingPathValues.length) details.push(`[env]._.path should include ${missingPathValues.join(", ")}`);
|
|
@@ -2757,10 +3015,10 @@ var RULES = [
|
|
|
2757
3015
|
};
|
|
2758
3016
|
},
|
|
2759
3017
|
migrate: (ctx, finding) => {
|
|
2760
|
-
const path =
|
|
3018
|
+
const path = join12(ctx.repoRoot, "mise.toml");
|
|
2761
3019
|
const changedFiles = [];
|
|
2762
3020
|
const details = [];
|
|
2763
|
-
if (!
|
|
3021
|
+
if (!existsSync9(path)) {
|
|
2764
3022
|
if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
|
|
2765
3023
|
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: [] };
|
|
2766
3024
|
}
|
|
@@ -2776,7 +3034,7 @@ var RULES = [
|
|
|
2776
3034
|
if (!ctx.dryRun) writeText(path, next);
|
|
2777
3035
|
text2 = next;
|
|
2778
3036
|
}
|
|
2779
|
-
const linkAgentfilesPath =
|
|
3037
|
+
const linkAgentfilesPath = join12(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
|
|
2780
3038
|
const expectedScript = templateLinkAgentfilesScript(ctx);
|
|
2781
3039
|
if (expectedScript === void 0) {
|
|
2782
3040
|
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: [] };
|
|
@@ -2803,13 +3061,13 @@ var RULES = [
|
|
|
2803
3061
|
title: "managed mise versioning block",
|
|
2804
3062
|
audit: (ctx) => {
|
|
2805
3063
|
const details = [];
|
|
2806
|
-
const misePath =
|
|
2807
|
-
const versioningPath =
|
|
2808
|
-
const manifestPath =
|
|
3064
|
+
const misePath = join12(ctx.repoRoot, "mise.toml");
|
|
3065
|
+
const versioningPath = join12(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
|
|
3066
|
+
const manifestPath = join12(ctx.repoRoot, ".mise", "version-files.conf");
|
|
2809
3067
|
const text2 = safeReadText(misePath);
|
|
2810
3068
|
if (!text2?.includes("# >>> mise-versioning >>>")) details.push("mise versioning managed block missing");
|
|
2811
|
-
if (!
|
|
2812
|
-
if (!
|
|
3069
|
+
if (!existsSync9(versioningPath)) details.push(".mise/scripts/versioning.sh missing");
|
|
3070
|
+
if (!existsSync9(manifestPath)) details.push(".mise/version-files.conf missing");
|
|
2813
3071
|
return {
|
|
2814
3072
|
id: "mise.versioning",
|
|
2815
3073
|
title: "managed mise versioning block",
|
|
@@ -2822,8 +3080,8 @@ var RULES = [
|
|
|
2822
3080
|
migrate: (ctx, finding) => {
|
|
2823
3081
|
const changedFiles = [];
|
|
2824
3082
|
const details = [];
|
|
2825
|
-
const misePath =
|
|
2826
|
-
if (!
|
|
3083
|
+
const misePath = join12(ctx.repoRoot, "mise.toml");
|
|
3084
|
+
if (!existsSync9(misePath)) {
|
|
2827
3085
|
if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
|
|
2828
3086
|
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: [] };
|
|
2829
3087
|
}
|
|
@@ -2833,12 +3091,21 @@ var RULES = [
|
|
|
2833
3091
|
}
|
|
2834
3092
|
}
|
|
2835
3093
|
const currentMise = readText(misePath);
|
|
2836
|
-
|
|
3094
|
+
let cleanedMise = currentMise;
|
|
3095
|
+
if (!currentMise.includes("# >>> mise-versioning >>>")) {
|
|
3096
|
+
const taskNames = ["version", "version:bump", "version:bump-patch", "version:bump-minor", "version:bump-major", "version:check", "version:sync"];
|
|
3097
|
+
for (const taskName of taskNames) {
|
|
3098
|
+
const escaped = taskName.replace(/:/g, "\\:");
|
|
3099
|
+
const headerPattern = new RegExp(`^\\[tasks\\.(?:"${escaped}"|'${escaped}'|${escaped})\\]$`);
|
|
3100
|
+
cleanedMise = removeTomlSection(cleanedMise, headerPattern);
|
|
3101
|
+
}
|
|
3102
|
+
}
|
|
3103
|
+
const nextMise = replaceOrAppendManagedBlock(cleanedMise, /# >>> mise-versioning >>>/, VERSIONING_BLOCK, /^\[tasks\.build\]/m);
|
|
2837
3104
|
if (nextMise !== currentMise) {
|
|
2838
3105
|
if (!changedFiles.includes(misePath)) changedFiles.push(misePath);
|
|
2839
3106
|
if (!ctx.dryRun) writeText(misePath, nextMise);
|
|
2840
3107
|
}
|
|
2841
|
-
const versioningPath =
|
|
3108
|
+
const versioningPath = join12(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
|
|
2842
3109
|
const expectedScript = templateVersioningScript(ctx);
|
|
2843
3110
|
if (expectedScript === void 0) {
|
|
2844
3111
|
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: [] };
|
|
@@ -2850,7 +3117,7 @@ var RULES = [
|
|
|
2850
3117
|
chmodSync2(versioningPath, 493);
|
|
2851
3118
|
}
|
|
2852
3119
|
}
|
|
2853
|
-
const manifestPath =
|
|
3120
|
+
const manifestPath = join12(ctx.repoRoot, ".mise", "version-files.conf");
|
|
2854
3121
|
const expectedManifest = templateVersionFilesConf(ctx, ctx.repoRoot);
|
|
2855
3122
|
if (safeReadText(manifestPath) !== expectedManifest) {
|
|
2856
3123
|
changedFiles.push(manifestPath);
|
|
@@ -2870,9 +3137,9 @@ var RULES = [
|
|
|
2870
3137
|
id: "sot.agent-symlinks",
|
|
2871
3138
|
title: "AGENTS/CLAUDE/GEMINI symlink contract",
|
|
2872
3139
|
audit: (ctx) => {
|
|
2873
|
-
const agentsPath =
|
|
2874
|
-
if (!
|
|
2875
|
-
const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) =>
|
|
3140
|
+
const agentsPath = join12(ctx.repoRoot, "AGENTS.md");
|
|
3141
|
+
if (!existsSync9(agentsPath)) {
|
|
3142
|
+
const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) => existsSync9(join12(ctx.repoRoot, file)));
|
|
2876
3143
|
if (fallbackSources.length === 0) {
|
|
2877
3144
|
return { id: "sot.agent-symlinks", title: "AGENTS/CLAUDE/GEMINI symlink contract", status: "skip", summary: "AGENTS.md missing; symlink contract not applicable", details: [], fixable: false };
|
|
2878
3145
|
}
|
|
@@ -2887,7 +3154,7 @@ var RULES = [
|
|
|
2887
3154
|
}
|
|
2888
3155
|
const details = [];
|
|
2889
3156
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
2890
|
-
const full =
|
|
3157
|
+
const full = join12(ctx.repoRoot, file);
|
|
2891
3158
|
const target = readSymlinkTarget(full);
|
|
2892
3159
|
if (target !== "AGENTS.md") details.push(`${file} should be a symlink to AGENTS.md`);
|
|
2893
3160
|
}
|
|
@@ -2911,7 +3178,7 @@ var RULES = [
|
|
|
2911
3178
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "AGENTS.md missing; cannot derive canonical agent file", changedFiles, details: [bootstrap.blocked] };
|
|
2912
3179
|
}
|
|
2913
3180
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
2914
|
-
const full =
|
|
3181
|
+
const full = join12(ctx.repoRoot, file);
|
|
2915
3182
|
const result = ensureSymlink(full, "AGENTS.md", ctx.dryRun);
|
|
2916
3183
|
if (result.blocked) blockedDetails.push(result.blocked);
|
|
2917
3184
|
if (result.changed) changedFiles.push(full);
|
|
@@ -2933,7 +3200,7 @@ var RULES = [
|
|
|
2933
3200
|
migrate: (ctx, finding) => {
|
|
2934
3201
|
const changedFiles = [];
|
|
2935
3202
|
const details = [];
|
|
2936
|
-
const path =
|
|
3203
|
+
const path = join12(ctx.repoRoot, ".project.json");
|
|
2937
3204
|
const existing = readProjectJson(ctx) ?? {};
|
|
2938
3205
|
const canonical = canonicalProjectJson(ctx);
|
|
2939
3206
|
const merged = { ...existing, ...canonical };
|
|
@@ -2943,10 +3210,10 @@ var RULES = [
|
|
|
2943
3210
|
changedFiles.push(path);
|
|
2944
3211
|
if (!ctx.dryRun) writeText(path, expected);
|
|
2945
3212
|
}
|
|
2946
|
-
const planeJson =
|
|
2947
|
-
if (
|
|
3213
|
+
const planeJson = join12(ctx.repoRoot, ".plane.json");
|
|
3214
|
+
if (existsSync9(planeJson)) {
|
|
2948
3215
|
const backup = `${planeJson}.migrated-backup`;
|
|
2949
|
-
if (
|
|
3216
|
+
if (existsSync9(backup)) {
|
|
2950
3217
|
details.push(`cannot back up .plane.json because ${relative(ctx.repoRoot, backup)} already exists`);
|
|
2951
3218
|
} else {
|
|
2952
3219
|
changedFiles.push(backup);
|
|
@@ -2968,8 +3235,8 @@ var RULES = [
|
|
|
2968
3235
|
title: ".env.op + gitignore secrets contract",
|
|
2969
3236
|
audit: (ctx) => {
|
|
2970
3237
|
const details = [];
|
|
2971
|
-
const envOp = safeReadText(
|
|
2972
|
-
const gitignore = safeReadText(
|
|
3238
|
+
const envOp = safeReadText(join12(ctx.repoRoot, ".env.op"));
|
|
3239
|
+
const gitignore = safeReadText(join12(ctx.repoRoot, ".gitignore"));
|
|
2973
3240
|
if (!envOp) {
|
|
2974
3241
|
details.push(".env.op missing");
|
|
2975
3242
|
} else {
|
|
@@ -2995,12 +3262,12 @@ var RULES = [
|
|
|
2995
3262
|
migrate: (ctx, finding) => {
|
|
2996
3263
|
const changedFiles = [];
|
|
2997
3264
|
const details = [];
|
|
2998
|
-
const envOpPath =
|
|
2999
|
-
if (!
|
|
3265
|
+
const envOpPath = join12(ctx.repoRoot, ".env.op");
|
|
3266
|
+
if (!existsSync9(envOpPath)) {
|
|
3000
3267
|
changedFiles.push(envOpPath);
|
|
3001
|
-
if (!ctx.dryRun) writeText(envOpPath, readText(
|
|
3268
|
+
if (!ctx.dryRun) writeText(envOpPath, readText(join12(ctx.pjanglerRoot, "templates", "commonproject", "template", ".env.op")));
|
|
3002
3269
|
}
|
|
3003
|
-
const gitignorePath =
|
|
3270
|
+
const gitignorePath = join12(ctx.repoRoot, ".gitignore");
|
|
3004
3271
|
const gitignore = safeReadText(gitignorePath) ?? "";
|
|
3005
3272
|
const requiredBlock = `# Secrets \u2014 .env is materialized by \`op inject -i .env.op > .env\` on mise enter.
|
|
3006
3273
|
# NEVER commit it. .env.op holds only 1Password references or safe literals and IS committed.
|
|
@@ -3027,7 +3294,7 @@ var RULES = [
|
|
|
3027
3294
|
title: ".copier-answers.yml provenance + drift report",
|
|
3028
3295
|
audit: (ctx) => {
|
|
3029
3296
|
const details = [];
|
|
3030
|
-
const path =
|
|
3297
|
+
const path = join12(ctx.repoRoot, ".copier-answers.yml");
|
|
3031
3298
|
const text2 = safeReadText(path);
|
|
3032
3299
|
const project = readProjectJson(ctx);
|
|
3033
3300
|
if (!text2) {
|
|
@@ -3058,12 +3325,12 @@ var RULES = [
|
|
|
3058
3325
|
const changedFiles = [];
|
|
3059
3326
|
const project = canonicalProjectJson(ctx);
|
|
3060
3327
|
const text2 = `# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
|
|
3061
|
-
_src_path: ${
|
|
3328
|
+
_src_path: ${join12(ctx.pjanglerRoot, "templates", "commonproject")}
|
|
3062
3329
|
project_description: ${String(project.project_description)}
|
|
3063
3330
|
project_name: ${String(project.project_name)}
|
|
3064
3331
|
ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
3065
3332
|
`;
|
|
3066
|
-
const path =
|
|
3333
|
+
const path = join12(ctx.repoRoot, ".copier-answers.yml");
|
|
3067
3334
|
if (safeReadText(path) !== text2) {
|
|
3068
3335
|
changedFiles.push(path);
|
|
3069
3336
|
if (!ctx.dryRun) writeText(path, text2);
|
|
@@ -3082,15 +3349,14 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3082
3349
|
id: "bmad.scaffold",
|
|
3083
3350
|
title: "BMAD modules/docs scaffold",
|
|
3084
3351
|
audit: (ctx) => {
|
|
3085
|
-
const
|
|
3086
|
-
const targetRoot = join11(ctx.repoRoot, "_bmad");
|
|
3352
|
+
const targetRoot = join12(ctx.repoRoot, "_bmad");
|
|
3087
3353
|
const sentinels = [
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3354
|
+
join12("core", "config.yaml"),
|
|
3355
|
+
join12("config.toml"),
|
|
3356
|
+
join12("_config", "manifest.yaml"),
|
|
3357
|
+
join12("bmm", "config.yaml")
|
|
3092
3358
|
];
|
|
3093
|
-
const missing = sentinels.filter((file) =>
|
|
3359
|
+
const missing = sentinels.filter((file) => !existsSync9(join12(targetRoot, file)));
|
|
3094
3360
|
return {
|
|
3095
3361
|
id: "bmad.scaffold",
|
|
3096
3362
|
title: "BMAD modules/docs scaffold",
|
|
@@ -3102,17 +3368,148 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3102
3368
|
},
|
|
3103
3369
|
migrate: (ctx, finding) => {
|
|
3104
3370
|
const changedFiles = [];
|
|
3105
|
-
|
|
3371
|
+
if (ctx.dryRun) {
|
|
3372
|
+
for (const detail of finding.details) {
|
|
3373
|
+
changedFiles.push(join12(ctx.repoRoot, detail));
|
|
3374
|
+
}
|
|
3375
|
+
return {
|
|
3376
|
+
id: finding.id,
|
|
3377
|
+
title: finding.title,
|
|
3378
|
+
status: changedFiles.length ? "applied" : "noop",
|
|
3379
|
+
summary: changedFiles.length ? "Would run non-interactive bmad-method install" : "No changes required",
|
|
3380
|
+
changedFiles,
|
|
3381
|
+
details: [
|
|
3382
|
+
`Would run: npx ${bmadInstallArgs(ctx.repoRoot).join(" ").replace(BMAD_INSTALL_TOOLS.join(","), "...")}`
|
|
3383
|
+
]
|
|
3384
|
+
};
|
|
3385
|
+
}
|
|
3386
|
+
const install = runBmadInstall(ctx.repoRoot);
|
|
3387
|
+
if (!install.ok) {
|
|
3388
|
+
return {
|
|
3389
|
+
id: finding.id,
|
|
3390
|
+
title: finding.title,
|
|
3391
|
+
status: "blocked",
|
|
3392
|
+
summary: `Failed to run bmad-method install`,
|
|
3393
|
+
changedFiles: [],
|
|
3394
|
+
details: [install.error ?? "Unknown error"]
|
|
3395
|
+
};
|
|
3396
|
+
}
|
|
3397
|
+
for (const detail of finding.details) {
|
|
3398
|
+
if (existsSync9(join12(ctx.repoRoot, detail))) {
|
|
3399
|
+
changedFiles.push(join12(ctx.repoRoot, detail));
|
|
3400
|
+
}
|
|
3401
|
+
}
|
|
3106
3402
|
return {
|
|
3107
3403
|
id: finding.id,
|
|
3108
3404
|
title: finding.title,
|
|
3109
3405
|
status: changedFiles.length ? "applied" : "noop",
|
|
3110
|
-
summary: changedFiles.length ? "
|
|
3406
|
+
summary: changedFiles.length ? "Installed BMAD scaffold via non-interactive installer" : "No changes required",
|
|
3111
3407
|
changedFiles,
|
|
3112
3408
|
details: []
|
|
3113
3409
|
};
|
|
3114
3410
|
}
|
|
3115
3411
|
},
|
|
3412
|
+
{
|
|
3413
|
+
id: "bmad.version",
|
|
3414
|
+
title: "BMAD version currency",
|
|
3415
|
+
audit: (ctx) => {
|
|
3416
|
+
const installed = readInstalledBmadVersion(ctx.repoRoot);
|
|
3417
|
+
if (!installed) {
|
|
3418
|
+
return {
|
|
3419
|
+
id: "bmad.version",
|
|
3420
|
+
title: "BMAD version currency",
|
|
3421
|
+
status: "skip",
|
|
3422
|
+
summary: existsSync9(join12(ctx.repoRoot, "_bmad")) ? "BMAD installed but version manifest unreadable" : "No BMAD install present",
|
|
3423
|
+
details: [],
|
|
3424
|
+
fixable: false
|
|
3425
|
+
};
|
|
3426
|
+
}
|
|
3427
|
+
const resolved = resolveBmadDistTags(ctx.homeDir);
|
|
3428
|
+
const available = resolved?.distTags?.[BMAD_TARGET_CHANNEL];
|
|
3429
|
+
if (!available) {
|
|
3430
|
+
return {
|
|
3431
|
+
id: "bmad.version",
|
|
3432
|
+
title: "BMAD version currency",
|
|
3433
|
+
status: "skip",
|
|
3434
|
+
summary: `BMAD ${installed} installed; latest ${BMAD_TARGET_CHANNEL} version unknown (npm unreachable)`,
|
|
3435
|
+
details: [`Could not resolve ${BMAD_NPM_PACKAGE}@${BMAD_TARGET_CHANNEL} from npm`],
|
|
3436
|
+
fixable: false
|
|
3437
|
+
};
|
|
3438
|
+
}
|
|
3439
|
+
const staleNote = resolved.stale ? ` ${glyph.dot} cached` : "";
|
|
3440
|
+
if (compareBmadVersions(installed, available) >= 0) {
|
|
3441
|
+
return {
|
|
3442
|
+
id: "bmad.version",
|
|
3443
|
+
title: "BMAD version currency",
|
|
3444
|
+
status: "pass",
|
|
3445
|
+
summary: `BMAD ${installed} is current (${BMAD_TARGET_CHANNEL} ${available})${staleNote}`,
|
|
3446
|
+
details: [],
|
|
3447
|
+
fixable: false
|
|
3448
|
+
};
|
|
3449
|
+
}
|
|
3450
|
+
return {
|
|
3451
|
+
id: "bmad.version",
|
|
3452
|
+
title: "BMAD version currency",
|
|
3453
|
+
status: "warn",
|
|
3454
|
+
summary: `BMAD ${installed} is behind ${BMAD_TARGET_CHANNEL} ${available} \u2014 upgrade available`,
|
|
3455
|
+
details: [
|
|
3456
|
+
`installed: ${installed}`,
|
|
3457
|
+
`available: ${available} (${BMAD_NPM_PACKAGE}@${BMAD_TARGET_CHANNEL})`,
|
|
3458
|
+
resolved.distTags.latest ? `stable latest: ${resolved.distTags.latest}` : "",
|
|
3459
|
+
"run `pj migrate bmad.version` to upgrade"
|
|
3460
|
+
].filter(Boolean),
|
|
3461
|
+
fixable: true
|
|
3462
|
+
};
|
|
3463
|
+
},
|
|
3464
|
+
migrate: (ctx, finding) => {
|
|
3465
|
+
if (finding.status !== "warn") {
|
|
3466
|
+
return {
|
|
3467
|
+
id: finding.id,
|
|
3468
|
+
title: finding.title,
|
|
3469
|
+
status: "noop",
|
|
3470
|
+
summary: finding.status === "skip" ? finding.summary : "BMAD already current",
|
|
3471
|
+
changedFiles: [],
|
|
3472
|
+
details: []
|
|
3473
|
+
};
|
|
3474
|
+
}
|
|
3475
|
+
const installed = readInstalledBmadVersion(ctx.repoRoot);
|
|
3476
|
+
const available = resolveBmadDistTags(ctx.homeDir)?.distTags?.[BMAD_TARGET_CHANNEL];
|
|
3477
|
+
const manifestPath = join12(ctx.repoRoot, "_bmad", "_config", "manifest.yaml");
|
|
3478
|
+
if (ctx.dryRun) {
|
|
3479
|
+
return {
|
|
3480
|
+
id: finding.id,
|
|
3481
|
+
title: finding.title,
|
|
3482
|
+
status: "applied",
|
|
3483
|
+
summary: `Would upgrade BMAD ${installed ?? "?"} -> ${available ?? BMAD_TARGET_CHANNEL}`,
|
|
3484
|
+
changedFiles: [manifestPath],
|
|
3485
|
+
details: [
|
|
3486
|
+
`Would run: npx ${bmadInstallArgs(ctx.repoRoot).join(" ").replace(BMAD_INSTALL_TOOLS.join(","), "...")}`
|
|
3487
|
+
]
|
|
3488
|
+
};
|
|
3489
|
+
}
|
|
3490
|
+
const install = runBmadInstall(ctx.repoRoot);
|
|
3491
|
+
if (!install.ok) {
|
|
3492
|
+
return {
|
|
3493
|
+
id: finding.id,
|
|
3494
|
+
title: finding.title,
|
|
3495
|
+
status: "blocked",
|
|
3496
|
+
summary: "Failed to upgrade BMAD via installer",
|
|
3497
|
+
changedFiles: [],
|
|
3498
|
+
details: [install.error ?? "Unknown error"]
|
|
3499
|
+
};
|
|
3500
|
+
}
|
|
3501
|
+
const nowInstalled = readInstalledBmadVersion(ctx.repoRoot);
|
|
3502
|
+
const upgraded = Boolean(nowInstalled && installed && compareBmadVersions(nowInstalled, installed) > 0);
|
|
3503
|
+
return {
|
|
3504
|
+
id: finding.id,
|
|
3505
|
+
title: finding.title,
|
|
3506
|
+
status: upgraded ? "applied" : "noop",
|
|
3507
|
+
summary: upgraded ? `Upgraded BMAD ${installed} -> ${nowInstalled}` : `BMAD reinstalled (${nowInstalled ?? "?"})`,
|
|
3508
|
+
changedFiles: upgraded ? [manifestPath] : [],
|
|
3509
|
+
details: []
|
|
3510
|
+
};
|
|
3511
|
+
}
|
|
3512
|
+
},
|
|
3116
3513
|
{
|
|
3117
3514
|
id: "hermes.pm-scaffold",
|
|
3118
3515
|
title: "Hermes PM scaffold parity",
|
|
@@ -3124,11 +3521,11 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3124
3521
|
}
|
|
3125
3522
|
const details = [];
|
|
3126
3523
|
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"]) {
|
|
3127
|
-
if (!
|
|
3524
|
+
if (!existsSync9(join12(role.roleDir, rel))) details.push(`missing ${relative(ctx.repoRoot, join12(role.roleDir, rel))}`);
|
|
3128
3525
|
}
|
|
3129
|
-
const gitmodules = safeReadText(
|
|
3526
|
+
const gitmodules = safeReadText(join12(ctx.repoRoot, ".gitmodules")) ?? "";
|
|
3130
3527
|
if (!gitmodules.includes(`agents/hermes/${role.role}/runtime`)) details.push(".gitmodules missing pm runtime submodule entry");
|
|
3131
|
-
if (!profileMetaInheritsDefault(
|
|
3528
|
+
if (!profileMetaInheritsDefault(join12(role.roleDir, "runtime", "profile.yaml"))) {
|
|
3132
3529
|
details.push("runtime/profile.yaml missing inherited default config metadata");
|
|
3133
3530
|
}
|
|
3134
3531
|
const registry = safeReadText(registryPath(ctx.homeDir));
|
|
@@ -3149,21 +3546,21 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3149
3546
|
if (!role) {
|
|
3150
3547
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "No pm role present", changedFiles, details: [] };
|
|
3151
3548
|
}
|
|
3152
|
-
const templateRoleDir =
|
|
3153
|
-
writeIfDifferent(
|
|
3154
|
-
writeIfDifferent(
|
|
3155
|
-
writeIfDifferent(
|
|
3156
|
-
copyMissingRecursive(
|
|
3157
|
-
copyMissingRecursive(
|
|
3158
|
-
copyMissingRecursive(
|
|
3159
|
-
const promptSource =
|
|
3160
|
-
const promptTarget =
|
|
3161
|
-
if (
|
|
3549
|
+
const templateRoleDir = join12(ctx.pjanglerRoot, "templates", "hermes-agent", "template");
|
|
3550
|
+
writeIfDifferent(join12(role.roleDir, "SOUL.md"), renderSoul(role), ctx.dryRun, changedFiles);
|
|
3551
|
+
writeIfDifferent(join12(role.roleDir, "hermes"), renderHermesWrapper(role), ctx.dryRun, changedFiles, 493);
|
|
3552
|
+
writeIfDifferent(join12(role.roleDir, ".gitignore"), readText(join12(templateRoleDir, ".gitignore.jinja")).replace(/\{\{ role \}\}/g, role.role), ctx.dryRun, changedFiles);
|
|
3553
|
+
copyMissingRecursive(join12(templateRoleDir, ".runtime-scaffold"), join12(role.roleDir, ".runtime-scaffold"), changedFiles, ctx.dryRun);
|
|
3554
|
+
copyMissingRecursive(join12(templateRoleDir, ".runtime-scaffold"), join12(role.roleDir, "runtime"), changedFiles, ctx.dryRun);
|
|
3555
|
+
copyMissingRecursive(join12(templateRoleDir, ".scripts"), join12(role.roleDir, ".scripts"), changedFiles, ctx.dryRun, (source) => source.endsWith("sentinel.prompt.md.jinja"));
|
|
3556
|
+
const promptSource = join12(templateRoleDir, ".scripts", "sentinel.prompt.md.jinja");
|
|
3557
|
+
const promptTarget = join12(role.roleDir, ".scripts", "sentinel.prompt.md");
|
|
3558
|
+
if (existsSync9(promptSource) && !existsSync9(promptTarget)) {
|
|
3162
3559
|
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);
|
|
3163
3560
|
writeIfDifferent(promptTarget, prompt, ctx.dryRun, changedFiles);
|
|
3164
3561
|
}
|
|
3165
3562
|
upsertSubmodule(ctx.repoRoot, role, changedFiles, ctx.dryRun);
|
|
3166
|
-
const profileMetaUpdated = upsertInheritedProfileMeta(
|
|
3563
|
+
const profileMetaUpdated = upsertInheritedProfileMeta(join12(role.roleDir, "runtime", "profile.yaml"), changedFiles, ctx.dryRun);
|
|
3167
3564
|
if (profileMetaUpdated) details.push(`updated ${profileMetaUpdated}`);
|
|
3168
3565
|
const registryUpdated = upsertRegistryEntry(role, ctx.homeDir, changedFiles, ctx.dryRun);
|
|
3169
3566
|
if (registryUpdated) details.push(`updated ${registryUpdated}`);
|
|
@@ -3177,6 +3574,103 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3177
3574
|
};
|
|
3178
3575
|
}
|
|
3179
3576
|
},
|
|
3577
|
+
{
|
|
3578
|
+
id: "hermes.untracked-runtimes",
|
|
3579
|
+
title: "Hermes agent runtimes untracked + gitignored",
|
|
3580
|
+
audit: (ctx) => {
|
|
3581
|
+
const roles = discoverRoles(ctx.repoRoot);
|
|
3582
|
+
if (roles.length === 0) {
|
|
3583
|
+
return {
|
|
3584
|
+
id: "hermes.untracked-runtimes",
|
|
3585
|
+
title: "Hermes agent runtimes untracked + gitignored",
|
|
3586
|
+
status: "skip",
|
|
3587
|
+
summary: "No Hermes roles present",
|
|
3588
|
+
details: [],
|
|
3589
|
+
fixable: false
|
|
3590
|
+
};
|
|
3591
|
+
}
|
|
3592
|
+
const details = [];
|
|
3593
|
+
for (const role of roles) {
|
|
3594
|
+
const roleRelDir = relative(ctx.repoRoot, role.roleDir);
|
|
3595
|
+
const runtimeRelPath = join12(roleRelDir, "runtime");
|
|
3596
|
+
const lsResult = spawnSync6("git", ["ls-files", "--stage", runtimeRelPath], {
|
|
3597
|
+
cwd: ctx.repoRoot,
|
|
3598
|
+
encoding: "utf8"
|
|
3599
|
+
});
|
|
3600
|
+
if (lsResult.status === 0 && lsResult.stdout.trim().length > 0) {
|
|
3601
|
+
details.push(`submodule runtime is tracked in Git index at ${runtimeRelPath}`);
|
|
3602
|
+
}
|
|
3603
|
+
const gitignorePath = join12(role.roleDir, ".gitignore");
|
|
3604
|
+
if (existsSync9(gitignorePath)) {
|
|
3605
|
+
const content = safeReadText(gitignorePath) ?? "";
|
|
3606
|
+
const lines = content.split(/\r?\n/).map((line) => line.trim());
|
|
3607
|
+
if (!lines.includes("runtime/") && !lines.includes("runtime")) {
|
|
3608
|
+
details.push(`.gitignore missing runtime/ ignore entry in ${relative(ctx.repoRoot, gitignorePath)}`);
|
|
3609
|
+
}
|
|
3610
|
+
} else {
|
|
3611
|
+
details.push(`.gitignore is missing in ${relative(ctx.repoRoot, gitignorePath)}`);
|
|
3612
|
+
}
|
|
3613
|
+
}
|
|
3614
|
+
return {
|
|
3615
|
+
id: "hermes.untracked-runtimes",
|
|
3616
|
+
title: "Hermes agent runtimes untracked + gitignored",
|
|
3617
|
+
status: details.length === 0 ? "pass" : "fail",
|
|
3618
|
+
summary: details.length === 0 ? "All Hermes agent runtimes are untracked and gitignored" : `${details.length} issue(s) with untracked/ignored runtimes detected`,
|
|
3619
|
+
details,
|
|
3620
|
+
fixable: true
|
|
3621
|
+
};
|
|
3622
|
+
},
|
|
3623
|
+
migrate: (ctx, finding) => {
|
|
3624
|
+
const roles = discoverRoles(ctx.repoRoot);
|
|
3625
|
+
const changedFiles = [];
|
|
3626
|
+
const details = [];
|
|
3627
|
+
for (const role of roles) {
|
|
3628
|
+
const roleRelDir = relative(ctx.repoRoot, role.roleDir);
|
|
3629
|
+
const runtimeRelPath = join12(roleRelDir, "runtime");
|
|
3630
|
+
const lsResult = spawnSync6("git", ["ls-files", "--stage", runtimeRelPath], {
|
|
3631
|
+
cwd: ctx.repoRoot,
|
|
3632
|
+
encoding: "utf8"
|
|
3633
|
+
});
|
|
3634
|
+
if (lsResult.status === 0 && lsResult.stdout.trim().length > 0) {
|
|
3635
|
+
details.push(`untrack ${runtimeRelPath}`);
|
|
3636
|
+
changedFiles.push(runtimeRelPath);
|
|
3637
|
+
if (!ctx.dryRun) {
|
|
3638
|
+
spawnSync6("git", ["rm", "--cached", "-r", runtimeRelPath], {
|
|
3639
|
+
cwd: ctx.repoRoot,
|
|
3640
|
+
encoding: "utf8"
|
|
3641
|
+
});
|
|
3642
|
+
}
|
|
3643
|
+
}
|
|
3644
|
+
const gitignorePath = join12(role.roleDir, ".gitignore");
|
|
3645
|
+
let content = "";
|
|
3646
|
+
let isIgnored = false;
|
|
3647
|
+
if (existsSync9(gitignorePath)) {
|
|
3648
|
+
content = safeReadText(gitignorePath) ?? "";
|
|
3649
|
+
const lines = content.split(/\r?\n/).map((line) => line.trim());
|
|
3650
|
+
isIgnored = lines.includes("runtime/") || lines.includes("runtime");
|
|
3651
|
+
}
|
|
3652
|
+
if (!isIgnored) {
|
|
3653
|
+
details.push(`ignore runtime/ in ${relative(ctx.repoRoot, gitignorePath)}`);
|
|
3654
|
+
changedFiles.push(gitignorePath);
|
|
3655
|
+
if (!ctx.dryRun) {
|
|
3656
|
+
if (content && !content.endsWith("\n")) {
|
|
3657
|
+
content += "\n";
|
|
3658
|
+
}
|
|
3659
|
+
content += "runtime/\n";
|
|
3660
|
+
writeText(gitignorePath, content);
|
|
3661
|
+
}
|
|
3662
|
+
}
|
|
3663
|
+
}
|
|
3664
|
+
return {
|
|
3665
|
+
id: finding.id,
|
|
3666
|
+
title: finding.title,
|
|
3667
|
+
status: changedFiles.length ? "applied" : "noop",
|
|
3668
|
+
summary: changedFiles.length ? "Hermes agent runtimes made untracked and ignored" : "No changes required",
|
|
3669
|
+
changedFiles,
|
|
3670
|
+
details
|
|
3671
|
+
};
|
|
3672
|
+
}
|
|
3673
|
+
},
|
|
3180
3674
|
{
|
|
3181
3675
|
id: "systemd.sentinel",
|
|
3182
3676
|
title: "Hermes systemd/sentinel units enabled + active",
|
|
@@ -3217,9 +3711,9 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3217
3711
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "systemd --user unavailable on this host", changedFiles, details };
|
|
3218
3712
|
}
|
|
3219
3713
|
for (const role of roles) {
|
|
3220
|
-
const sysDir =
|
|
3714
|
+
const sysDir = join12(ctx.homeDir, ".config", "systemd", "user");
|
|
3221
3715
|
const units = [`hermes-${role.agentId}-gateway.service`, `hermes-${role.agentId}-consumer.service`, `hermes-${role.agentId}-heartbeat.timer`];
|
|
3222
|
-
const allUnitsPresent = units.every((unit) =>
|
|
3716
|
+
const allUnitsPresent = units.every((unit) => existsSync9(join12(sysDir, unit)));
|
|
3223
3717
|
if (allUnitsPresent) {
|
|
3224
3718
|
if (ctx.dryRun) {
|
|
3225
3719
|
details.push(`would run: systemctl --user enable --now ${units.join(" ")}`);
|
|
@@ -3231,12 +3725,12 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3231
3725
|
}
|
|
3232
3726
|
continue;
|
|
3233
3727
|
}
|
|
3234
|
-
for (const script of [
|
|
3235
|
-
if (!script || !
|
|
3728
|
+
for (const script of [join12(role.roleDir, ".scripts", "70-systemd.sh")]) {
|
|
3729
|
+
if (!script || !existsSync9(script)) continue;
|
|
3236
3730
|
if (ctx.dryRun) {
|
|
3237
3731
|
details.push(`would run: bash ${script}`);
|
|
3238
3732
|
} else {
|
|
3239
|
-
const result =
|
|
3733
|
+
const result = spawnSync6("bash", [script], { cwd: role.roleDir, encoding: "utf8" });
|
|
3240
3734
|
if (result.status !== 0) details.push(`script failed: ${script}: ${result.stderr.trim() || result.stdout.trim()}`);
|
|
3241
3735
|
}
|
|
3242
3736
|
}
|
|
@@ -3356,7 +3850,7 @@ var server = new McpServer({
|
|
|
3356
3850
|
var TICKET_PROVIDER_SCHEMA = z.enum(["plane", "trello"]);
|
|
3357
3851
|
function resolveTargetDir(targetDir) {
|
|
3358
3852
|
const dir = resolve3(targetDir ?? process.cwd());
|
|
3359
|
-
if (!
|
|
3853
|
+
if (!existsSync10(dir)) {
|
|
3360
3854
|
throw new Error(`Target directory does not exist: ${dir}`);
|
|
3361
3855
|
}
|
|
3362
3856
|
if (!statSync2(dir).isDirectory()) {
|
|
@@ -3367,7 +3861,7 @@ function resolveTargetDir(targetDir) {
|
|
|
3367
3861
|
function resolvePjanglerRoot3() {
|
|
3368
3862
|
let dir = dirname8(fileURLToPath5(import.meta.url));
|
|
3369
3863
|
while (dir !== dirname8(dir)) {
|
|
3370
|
-
if (
|
|
3864
|
+
if (existsSync10(join13(dir, "package.json")) && existsSync10(join13(dir, "templates", "commonproject", "copier.yml"))) {
|
|
3371
3865
|
return dir;
|
|
3372
3866
|
}
|
|
3373
3867
|
dir = dirname8(dir);
|
|
@@ -3569,8 +4063,8 @@ server.registerTool(
|
|
|
3569
4063
|
const pjanglerRoot = resolvePjanglerRoot3();
|
|
3570
4064
|
const projectSlug = input.projectSlug ?? slugify(input.projectName);
|
|
3571
4065
|
const parentDir = resolve3(input.parentDir ?? process.cwd());
|
|
3572
|
-
if (!
|
|
3573
|
-
const targetDir = resolve3(input.targetDir ??
|
|
4066
|
+
if (!existsSync10(parentDir) || !statSync2(parentDir).isDirectory()) throw new Error(`Parent directory does not exist: ${parentDir}`);
|
|
4067
|
+
const targetDir = resolve3(input.targetDir ?? join13(parentDir, projectSlug));
|
|
3574
4068
|
const overwrite = input.overwrite ?? input.force ?? false;
|
|
3575
4069
|
const dryRun = input.dryRun ?? true;
|
|
3576
4070
|
const local = input.local ?? true;
|
|
@@ -3580,7 +4074,7 @@ server.registerTool(
|
|
|
3580
4074
|
if (!skipPlane && ticketProvider === "plane" && !boardId) {
|
|
3581
4075
|
throw new Error("boardId or planeProjectId is required when skipPlane=false for Plane; keep skipPlane=true for safe local bootstrap");
|
|
3582
4076
|
}
|
|
3583
|
-
if (!dryRun &&
|
|
4077
|
+
if (!dryRun && existsSync10(targetDir) && !overwrite) throw new Error(`Target already exists: ${targetDir} (set force/overwrite=true to re-render)`);
|
|
3584
4078
|
const plan = planProjectInit({
|
|
3585
4079
|
name: input.projectName,
|
|
3586
4080
|
description: input.projectDescription,
|