@delorenj/pjangler 1.2.12 → 1.2.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { spawnSync as
|
|
5
|
-
import { existsSync as
|
|
6
|
-
import { basename as basename4, join as
|
|
4
|
+
import { spawnSync as spawnSync7 } from "node:child_process";
|
|
5
|
+
import { existsSync as existsSync10, readFileSync as readFileSync7, statSync as statSync2 } from "node:fs";
|
|
6
|
+
import { basename as basename4, join as join13, resolve as resolve3 } from "node:path";
|
|
7
7
|
import { Command as Command3 } from "commander";
|
|
8
8
|
|
|
9
9
|
// src/commands/hermes/types.ts
|
|
@@ -916,10 +916,100 @@ var RunCopierTemplate = class extends Command {
|
|
|
916
916
|
}
|
|
917
917
|
};
|
|
918
918
|
|
|
919
|
-
// src/commands/hermes/
|
|
919
|
+
// src/commands/hermes/UntrackHermesRuntimes.ts
|
|
920
|
+
import { existsSync as existsSync4, readFileSync as readFileSync2, writeFileSync as writeFileSync3, readdirSync } from "fs";
|
|
921
|
+
import { join as join6 } from "path";
|
|
920
922
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
921
|
-
|
|
922
|
-
|
|
923
|
+
var UntrackHermesRuntimes = class extends Command {
|
|
924
|
+
async invoke() {
|
|
925
|
+
const targetDir = this.context.targetDir;
|
|
926
|
+
const rolesDir = join6(targetDir, "agents", "hermes");
|
|
927
|
+
if (!existsSync4(rolesDir)) {
|
|
928
|
+
return {
|
|
929
|
+
success: true,
|
|
930
|
+
message: "No Hermes agents found (no agents/hermes directory)."
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
const roles = readdirSync(rolesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
934
|
+
if (roles.length === 0) {
|
|
935
|
+
return {
|
|
936
|
+
success: true,
|
|
937
|
+
message: "No Hermes agents found."
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
let modifiedAny = false;
|
|
941
|
+
const details = [];
|
|
942
|
+
for (const role of roles) {
|
|
943
|
+
const roleDir = join6("agents", "hermes", role);
|
|
944
|
+
const runtimePath = join6(roleDir, "runtime");
|
|
945
|
+
const gitignorePath = join6(roleDir, ".gitignore");
|
|
946
|
+
let isTracked = false;
|
|
947
|
+
const lsResult = spawnSync2("git", ["ls-files", "--stage", runtimePath], {
|
|
948
|
+
cwd: targetDir,
|
|
949
|
+
encoding: "utf8"
|
|
950
|
+
});
|
|
951
|
+
if (lsResult.status === 0 && lsResult.stdout.trim().length > 0) {
|
|
952
|
+
isTracked = true;
|
|
953
|
+
}
|
|
954
|
+
let isIgnored = false;
|
|
955
|
+
const fullGitignorePath = join6(targetDir, gitignorePath);
|
|
956
|
+
if (existsSync4(fullGitignorePath)) {
|
|
957
|
+
const content = readFileSync2(fullGitignorePath, "utf8");
|
|
958
|
+
const lines = content.split(/\r?\n/).map((line) => line.trim());
|
|
959
|
+
isIgnored = lines.includes("runtime/") || lines.includes("runtime");
|
|
960
|
+
}
|
|
961
|
+
if (isTracked || !isIgnored) {
|
|
962
|
+
modifiedAny = true;
|
|
963
|
+
if (isTracked) {
|
|
964
|
+
details.push(`untrack agents/hermes/${role}/runtime`);
|
|
965
|
+
if (!this.context.dryRun) {
|
|
966
|
+
const rmResult = spawnSync2("git", ["rm", "--cached", "-r", runtimePath], {
|
|
967
|
+
cwd: targetDir,
|
|
968
|
+
encoding: "utf8"
|
|
969
|
+
});
|
|
970
|
+
if (rmResult.status !== 0) {
|
|
971
|
+
return {
|
|
972
|
+
success: false,
|
|
973
|
+
message: `Failed to untrack agents/hermes/${role}/runtime: ${rmResult.stderr}`
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
if (!isIgnored) {
|
|
979
|
+
details.push(`ignore runtime/ in agents/hermes/${role}/.gitignore`);
|
|
980
|
+
if (!this.context.dryRun) {
|
|
981
|
+
let content = "";
|
|
982
|
+
if (existsSync4(fullGitignorePath)) {
|
|
983
|
+
content = readFileSync2(fullGitignorePath, "utf8");
|
|
984
|
+
}
|
|
985
|
+
if (content && !content.endsWith("\n")) {
|
|
986
|
+
content += "\n";
|
|
987
|
+
}
|
|
988
|
+
content += "runtime/\n";
|
|
989
|
+
writeFileSync3(fullGitignorePath, content, "utf8");
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
if (!modifiedAny) {
|
|
995
|
+
return {
|
|
996
|
+
success: true,
|
|
997
|
+
message: "\u2705 All Hermes agent runtimes are already untracked and gitignored."
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
const actionText = this.context.dryRun ? "Would make" : "Made";
|
|
1001
|
+
return {
|
|
1002
|
+
success: true,
|
|
1003
|
+
message: `${actionText} Hermes agent runtimes untracked and gitignored:
|
|
1004
|
+
${details.map((d) => ` - ${d}`).join("\n")}`
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
};
|
|
1008
|
+
|
|
1009
|
+
// src/commands/hermes/WireTelegram.ts
|
|
1010
|
+
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
1011
|
+
import { join as join7 } from "node:path";
|
|
1012
|
+
import { existsSync as existsSync5, unlinkSync } from "node:fs";
|
|
923
1013
|
import * as p3 from "@clack/prompts";
|
|
924
1014
|
var WireTelegram = class extends Command {
|
|
925
1015
|
async invoke() {
|
|
@@ -941,7 +1031,7 @@ var WireTelegram = class extends Command {
|
|
|
941
1031
|
let token = process.env.TELEGRAM_BOT_TOKEN;
|
|
942
1032
|
let source = token ? "env" : null;
|
|
943
1033
|
if (!token) {
|
|
944
|
-
const tryOp =
|
|
1034
|
+
const tryOp = spawnSync3("op", ["read", vaultRef], { encoding: "utf8" });
|
|
945
1035
|
if (tryOp.status === 0) {
|
|
946
1036
|
token = tryOp.stdout.trim();
|
|
947
1037
|
source = "op";
|
|
@@ -980,7 +1070,7 @@ var WireTelegram = class extends Command {
|
|
|
980
1070
|
initialValue: true
|
|
981
1071
|
});
|
|
982
1072
|
if (!p3.isCancel(persist) && persist) {
|
|
983
|
-
const create =
|
|
1073
|
+
const create = spawnSync3(
|
|
984
1074
|
"op",
|
|
985
1075
|
[
|
|
986
1076
|
"item",
|
|
@@ -1007,18 +1097,18 @@ var WireTelegram = class extends Command {
|
|
|
1007
1097
|
if (p3.isCancel(allowedAnswer)) {
|
|
1008
1098
|
return { success: false, message: "\u2717 Aborted; Telegram step deferred." };
|
|
1009
1099
|
}
|
|
1010
|
-
const script =
|
|
1011
|
-
if (!
|
|
1100
|
+
const script = join7(roleDir, ".scripts", "30-telegram.sh");
|
|
1101
|
+
if (!existsSync5(script)) {
|
|
1012
1102
|
return {
|
|
1013
1103
|
success: false,
|
|
1014
1104
|
message: `\u2717 ${script} not found. Did copier finish? Re-run with --skip-runtime-repo=0 if you skipped it.`
|
|
1015
1105
|
};
|
|
1016
1106
|
}
|
|
1017
|
-
const marker =
|
|
1018
|
-
if (
|
|
1107
|
+
const marker = join7(roleDir, ".scripts", ".done-30-telegram");
|
|
1108
|
+
if (existsSync5(marker)) unlinkSync(marker);
|
|
1019
1109
|
const spinner4 = p3.spinner();
|
|
1020
1110
|
spinner4.start("Verifying token + wiring profile");
|
|
1021
|
-
const result =
|
|
1111
|
+
const result = spawnSync3("bash", [script], {
|
|
1022
1112
|
stdio: "inherit",
|
|
1023
1113
|
env: {
|
|
1024
1114
|
...process.env,
|
|
@@ -1041,9 +1131,9 @@ function cap(s) {
|
|
|
1041
1131
|
}
|
|
1042
1132
|
|
|
1043
1133
|
// src/commands/hermes/WireEmail.ts
|
|
1044
|
-
import { spawnSync as
|
|
1045
|
-
import { join as
|
|
1046
|
-
import { existsSync as
|
|
1134
|
+
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
1135
|
+
import { join as join8 } from "node:path";
|
|
1136
|
+
import { existsSync as existsSync6, unlinkSync as unlinkSync2 } from "node:fs";
|
|
1047
1137
|
import * as p4 from "@clack/prompts";
|
|
1048
1138
|
var WireEmail = class extends Command {
|
|
1049
1139
|
async invoke() {
|
|
@@ -1058,13 +1148,13 @@ var WireEmail = class extends Command {
|
|
|
1058
1148
|
if (!targetRepo || !role || !roleDir) {
|
|
1059
1149
|
return { success: false, message: "Cannot wire email: missing target_repo/role/roleDir" };
|
|
1060
1150
|
}
|
|
1061
|
-
const script =
|
|
1062
|
-
if (!
|
|
1151
|
+
const script = join8(roleDir, ".scripts", "50-email.sh");
|
|
1152
|
+
if (!existsSync6(script)) {
|
|
1063
1153
|
return { success: false, message: `\u2717 ${script} not found` };
|
|
1064
1154
|
}
|
|
1065
1155
|
let token = process.env.CF_EMAIL_ROUTING_TOKEN;
|
|
1066
1156
|
if (!token) {
|
|
1067
|
-
const tryOp =
|
|
1157
|
+
const tryOp = spawnSync4(
|
|
1068
1158
|
"op",
|
|
1069
1159
|
["read", "op://DeLoSecrets/Cloudflare-EmailRouting/token"],
|
|
1070
1160
|
{ encoding: "utf8" }
|
|
@@ -1104,7 +1194,7 @@ var WireEmail = class extends Command {
|
|
|
1104
1194
|
initialValue: true
|
|
1105
1195
|
});
|
|
1106
1196
|
if (!p4.isCancel(persist) && persist) {
|
|
1107
|
-
const create =
|
|
1197
|
+
const create = spawnSync4(
|
|
1108
1198
|
"op",
|
|
1109
1199
|
[
|
|
1110
1200
|
"item",
|
|
@@ -1121,11 +1211,11 @@ var WireEmail = class extends Command {
|
|
|
1121
1211
|
}
|
|
1122
1212
|
}
|
|
1123
1213
|
}
|
|
1124
|
-
const marker =
|
|
1125
|
-
if (
|
|
1214
|
+
const marker = join8(roleDir, ".scripts", ".done-50-email");
|
|
1215
|
+
if (existsSync6(marker)) unlinkSync2(marker);
|
|
1126
1216
|
const spinner4 = p4.spinner();
|
|
1127
1217
|
spinner4.start("Creating Cloudflare Email Routing rule");
|
|
1128
|
-
const result =
|
|
1218
|
+
const result = spawnSync4("bash", [script], {
|
|
1129
1219
|
stdio: "inherit",
|
|
1130
1220
|
env: { ...process.env, SKIP_EMAIL: "0", CF_EMAIL_ROUTING_TOKEN: token },
|
|
1131
1221
|
cwd: roleDir
|
|
@@ -1185,7 +1275,7 @@ var PrintHermesSummary = class extends Command {
|
|
|
1185
1275
|
var HermesAgentRecipe = class extends Recipe {
|
|
1186
1276
|
constructor(context) {
|
|
1187
1277
|
super(context);
|
|
1188
|
-
this.addIngredient(EnsureTemplateConfig).addIngredient(PromptForAgentConfig).addIngredient(RunCopierTemplate).addIngredient(WireTelegram).addIngredient(WireEmail).addIngredient(PrintHermesSummary);
|
|
1278
|
+
this.addIngredient(EnsureTemplateConfig).addIngredient(PromptForAgentConfig).addIngredient(RunCopierTemplate).addIngredient(UntrackHermesRuntimes).addIngredient(WireTelegram).addIngredient(WireEmail).addIngredient(PrintHermesSummary);
|
|
1189
1279
|
}
|
|
1190
1280
|
// Override execute() to suppress the base class's per-command logging since
|
|
1191
1281
|
// our commands already render their own UI via @clack/prompts.
|
|
@@ -1209,33 +1299,33 @@ var HermesAgentRecipe = class extends Recipe {
|
|
|
1209
1299
|
|
|
1210
1300
|
// src/commands/AgentHooksCommands.ts
|
|
1211
1301
|
import { homedir as homedir4 } from "node:os";
|
|
1212
|
-
import { join as
|
|
1213
|
-
import { existsSync as
|
|
1302
|
+
import { join as join10, dirname as dirname5 } from "node:path";
|
|
1303
|
+
import { existsSync as existsSync8, cpSync, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "node:fs";
|
|
1214
1304
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1215
1305
|
|
|
1216
1306
|
// src/project/index.ts
|
|
1217
|
-
import { spawnSync as
|
|
1218
|
-
import { existsSync as
|
|
1307
|
+
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
1308
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync3, renameSync, statSync, writeFileSync as writeFileSync4 } from "node:fs";
|
|
1219
1309
|
import { homedir as homedir3 } from "node:os";
|
|
1220
|
-
import { basename as basename2, delimiter, dirname as dirname4, join as
|
|
1310
|
+
import { basename as basename2, delimiter, dirname as dirname4, join as join9, resolve } from "node:path";
|
|
1221
1311
|
import YAML from "yaml";
|
|
1222
1312
|
var PROJECT_REGISTRY_ENV = "PJ_PROJECT_REGISTRY";
|
|
1223
1313
|
var PROJECT_SOURCE_SKILL_ROOTS_ENV = "PJ_SOURCE_SKILL_ROOTS";
|
|
1224
1314
|
var PROJECT_REGISTRY_SCHEMA_VERSION = 1;
|
|
1225
1315
|
var DEFAULT_SOURCE_SKILL_ROOTS = [
|
|
1226
1316
|
"/home/delorenj/code/skillex/all-skills",
|
|
1227
|
-
|
|
1228
|
-
|
|
1317
|
+
join9(homedir3(), ".agents", "skills"),
|
|
1318
|
+
join9(homedir3(), ".codex", "skills")
|
|
1229
1319
|
];
|
|
1230
1320
|
function projectRegistryPath(env2 = process.env) {
|
|
1231
|
-
return expandHome(env2[PROJECT_REGISTRY_ENV] ||
|
|
1321
|
+
return expandHome(env2[PROJECT_REGISTRY_ENV] || join9(homedir3(), ".config", "pjangler", "projects.yaml"));
|
|
1232
1322
|
}
|
|
1233
1323
|
function emptyProjectRegistry() {
|
|
1234
1324
|
return { schema_version: PROJECT_REGISTRY_SCHEMA_VERSION, projects: {} };
|
|
1235
1325
|
}
|
|
1236
1326
|
function loadProjectRegistry(path = projectRegistryPath()) {
|
|
1237
|
-
if (!
|
|
1238
|
-
const raw = YAML.parse(
|
|
1327
|
+
if (!existsSync7(path)) return emptyProjectRegistry();
|
|
1328
|
+
const raw = YAML.parse(readFileSync3(path, "utf8"));
|
|
1239
1329
|
if (raw == null) return emptyProjectRegistry();
|
|
1240
1330
|
if (!isRecord(raw)) throw new Error(`Project registry must be a mapping: ${path}`);
|
|
1241
1331
|
const registry = raw;
|
|
@@ -1250,7 +1340,7 @@ function saveProjectRegistry(registry, path = projectRegistryPath()) {
|
|
|
1250
1340
|
validateProjectRegistry(registry);
|
|
1251
1341
|
mkdirSync4(dirname4(path), { recursive: true });
|
|
1252
1342
|
const temp = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
1253
|
-
|
|
1343
|
+
writeFileSync4(temp, YAML.stringify(registry, { lineWidth: 0 }), "utf8");
|
|
1254
1344
|
renameSync(temp, path);
|
|
1255
1345
|
}
|
|
1256
1346
|
function validateProjectRegistry(registry) {
|
|
@@ -1332,7 +1422,7 @@ function resolveAgentHooksLayer(input, env2 = process.env) {
|
|
|
1332
1422
|
const override = env2.PJ_AGENT_HOOKS_LAYER;
|
|
1333
1423
|
if (override === "0" || override === "false") return false;
|
|
1334
1424
|
if (override === "1" || override === "true") return true;
|
|
1335
|
-
return !
|
|
1425
|
+
return !existsSync7(join9(homedir3(), ".agents", "hooks"));
|
|
1336
1426
|
}
|
|
1337
1427
|
function jsonStable(value) {
|
|
1338
1428
|
return JSON.stringify(value);
|
|
@@ -1363,12 +1453,12 @@ function resolveSourceSkillPath(sourceSkill, env2 = process.env) {
|
|
|
1363
1453
|
if (!sourceSkill) return void 0;
|
|
1364
1454
|
const expanded = expandHome(sourceSkill);
|
|
1365
1455
|
const direct = resolve(expanded);
|
|
1366
|
-
if (
|
|
1456
|
+
if (existsSync7(direct)) return direct;
|
|
1367
1457
|
const name = basename2(sourceSkill);
|
|
1368
1458
|
const roots = sourceSkillRoots(env2);
|
|
1369
1459
|
for (const root of roots) {
|
|
1370
|
-
const candidate =
|
|
1371
|
-
if (
|
|
1460
|
+
const candidate = join9(root, name);
|
|
1461
|
+
if (existsSync7(candidate)) return candidate;
|
|
1372
1462
|
}
|
|
1373
1463
|
const searched = roots.length ? ` Searched roots: ${roots.join(", ")}.` : "";
|
|
1374
1464
|
const hint = `${searched} Add project-specific roots with ${PROJECT_SOURCE_SKILL_ROOTS_ENV}.`;
|
|
@@ -1449,7 +1539,7 @@ function planProjectInit(input) {
|
|
|
1449
1539
|
}));
|
|
1450
1540
|
}
|
|
1451
1541
|
actions.push(
|
|
1452
|
-
{ kind: "project.write-manifest", path:
|
|
1542
|
+
{ kind: "project.write-manifest", path: join9(targetDir, ".project.json"), manifest },
|
|
1453
1543
|
{
|
|
1454
1544
|
kind: "ticket-provider.create-or-link",
|
|
1455
1545
|
enabled: live,
|
|
@@ -1490,7 +1580,7 @@ function executeProjectInitPlan(plan) {
|
|
|
1490
1580
|
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"
|
|
1491
1581
|
);
|
|
1492
1582
|
mkdirSync4(dirname4(action.targetDir), { recursive: true });
|
|
1493
|
-
const result =
|
|
1583
|
+
const result = spawnSync5(action.command[0], action.command.slice(1), { encoding: "utf8", cwd: action.cwd });
|
|
1494
1584
|
if (result.stdout?.trim()) logs.push(result.stdout.trim());
|
|
1495
1585
|
if (result.stderr?.trim()) logs.push(result.stderr.trim());
|
|
1496
1586
|
if (result.error) {
|
|
@@ -1502,7 +1592,7 @@ function executeProjectInitPlan(plan) {
|
|
|
1502
1592
|
}
|
|
1503
1593
|
if (result.status !== 0) {
|
|
1504
1594
|
errors.push(`copier exited with status ${result.status ?? "unknown"}`);
|
|
1505
|
-
if (
|
|
1595
|
+
if (existsSync7(action.targetDir)) changedFiles.push(action.targetDir);
|
|
1506
1596
|
break;
|
|
1507
1597
|
}
|
|
1508
1598
|
changedFiles.push(action.targetDir);
|
|
@@ -1510,9 +1600,9 @@ function executeProjectInitPlan(plan) {
|
|
|
1510
1600
|
mkdirSync4(dirname4(action.path), { recursive: true });
|
|
1511
1601
|
const next = `${JSON.stringify(action.manifest, null, 2)}
|
|
1512
1602
|
`;
|
|
1513
|
-
const current =
|
|
1603
|
+
const current = existsSync7(action.path) ? readFileSync3(action.path, "utf8") : void 0;
|
|
1514
1604
|
if (current !== next) {
|
|
1515
|
-
|
|
1605
|
+
writeFileSync4(action.path, next, "utf8");
|
|
1516
1606
|
changedFiles.push(action.path);
|
|
1517
1607
|
}
|
|
1518
1608
|
} else if (action.kind === "registry.upsert") {
|
|
@@ -1605,16 +1695,16 @@ function doctorProjectRegistry(registryPath2 = projectRegistryPath(), slug) {
|
|
|
1605
1695
|
const registry = loadProjectRegistry(registryPath2);
|
|
1606
1696
|
const projects = slug ? [[slug, getProject(registry, slug)]] : Object.entries(registry.projects);
|
|
1607
1697
|
for (const [projectSlug, project] of projects) {
|
|
1608
|
-
if (!
|
|
1698
|
+
if (!existsSync7(project.repo_path)) {
|
|
1609
1699
|
issues.push({ level: "warn", slug: projectSlug, message: `repo_path does not exist: ${project.repo_path}` });
|
|
1610
1700
|
} else if (!statSync(project.repo_path).isDirectory()) {
|
|
1611
1701
|
issues.push({ level: "error", slug: projectSlug, message: `repo_path is not a directory: ${project.repo_path}` });
|
|
1612
1702
|
} else {
|
|
1613
|
-
const manifestPath =
|
|
1614
|
-
if (!
|
|
1703
|
+
const manifestPath = join9(project.repo_path, ".project.json");
|
|
1704
|
+
if (!existsSync7(manifestPath)) issues.push({ level: "warn", slug: projectSlug, message: ".project.json is missing" });
|
|
1615
1705
|
}
|
|
1616
1706
|
for (const artifact of project.source_artifacts) {
|
|
1617
|
-
if (artifact.path && !
|
|
1707
|
+
if (artifact.path && !existsSync7(artifact.path)) {
|
|
1618
1708
|
issues.push({ level: "warn", slug: projectSlug, message: `source artifact missing: ${artifact.path}` });
|
|
1619
1709
|
}
|
|
1620
1710
|
}
|
|
@@ -1627,7 +1717,7 @@ function doctorProjectRegistry(registryPath2 = projectRegistryPath(), slug) {
|
|
|
1627
1717
|
};
|
|
1628
1718
|
}
|
|
1629
1719
|
function buildCommonProjectCopierAction(input) {
|
|
1630
|
-
const templateDir =
|
|
1720
|
+
const templateDir = join9(input.pjanglerRoot, "templates", "commonproject");
|
|
1631
1721
|
const data = {
|
|
1632
1722
|
project_name: input.projectName,
|
|
1633
1723
|
project_description: input.projectDescription ?? "",
|
|
@@ -1656,7 +1746,7 @@ function buildCommonProjectCopierAction(input) {
|
|
|
1656
1746
|
function resolvePjanglerRoot() {
|
|
1657
1747
|
let dir = dirname4(new URL(import.meta.url).pathname);
|
|
1658
1748
|
while (dir !== dirname4(dir)) {
|
|
1659
|
-
if (
|
|
1749
|
+
if (existsSync7(join9(dir, "package.json")) && existsSync7(join9(dir, "templates", "commonproject", "copier.yml"))) return dir;
|
|
1660
1750
|
dir = dirname4(dir);
|
|
1661
1751
|
}
|
|
1662
1752
|
return resolve(process.cwd());
|
|
@@ -1688,7 +1778,7 @@ function validateProjectRecord(project, key) {
|
|
|
1688
1778
|
}
|
|
1689
1779
|
function expandHome(path) {
|
|
1690
1780
|
if (path === "~") return homedir3();
|
|
1691
|
-
if (path.startsWith("~/")) return
|
|
1781
|
+
if (path.startsWith("~/")) return join9(homedir3(), path.slice(2));
|
|
1692
1782
|
return path;
|
|
1693
1783
|
}
|
|
1694
1784
|
function isRecord(value) {
|
|
@@ -1705,16 +1795,16 @@ function resolveTemplateRoot() {
|
|
|
1705
1795
|
try {
|
|
1706
1796
|
let dir = dirname5(fileURLToPath2(import.meta.url));
|
|
1707
1797
|
for (let i = 0; i < 8; i++) {
|
|
1708
|
-
candidates.push(
|
|
1798
|
+
candidates.push(join10(dir, "templates", "commonproject", "template"));
|
|
1709
1799
|
const parent = dirname5(dir);
|
|
1710
1800
|
if (parent === dir) break;
|
|
1711
1801
|
dir = parent;
|
|
1712
1802
|
}
|
|
1713
1803
|
} catch {
|
|
1714
1804
|
}
|
|
1715
|
-
candidates.push(
|
|
1805
|
+
candidates.push(join10(homedir4(), "code", "pjangler", "templates", "commonproject", "template"));
|
|
1716
1806
|
for (const c of candidates) {
|
|
1717
|
-
if (
|
|
1807
|
+
if (existsSync8(join10(c, ".agents", "hooks", "hooks.master.json"))) return c;
|
|
1718
1808
|
}
|
|
1719
1809
|
throw new Error(
|
|
1720
1810
|
"Could not locate the CommonProject template. Set PJANGLER_COMMONPROJECT_TEMPLATE to <repo>/templates/commonproject/template."
|
|
@@ -1741,10 +1831,10 @@ var CopyAgentHooksTree = class extends Command {
|
|
|
1741
1831
|
const created = [];
|
|
1742
1832
|
const skipped = [];
|
|
1743
1833
|
for (const { rel, dir } of items) {
|
|
1744
|
-
const src =
|
|
1745
|
-
const dest =
|
|
1746
|
-
if (!
|
|
1747
|
-
if (
|
|
1834
|
+
const src = join10(templateRoot, rel);
|
|
1835
|
+
const dest = join10(this.context.targetDir, rel);
|
|
1836
|
+
if (!existsSync8(src)) continue;
|
|
1837
|
+
if (existsSync8(dest) && !this.context.force) {
|
|
1748
1838
|
skipped.push(rel);
|
|
1749
1839
|
continue;
|
|
1750
1840
|
}
|
|
@@ -1770,14 +1860,14 @@ var WireMiseAgentHooks = class _WireMiseAgentHooks extends Command {
|
|
|
1770
1860
|
if (!resolveAgentHooksLayer()) {
|
|
1771
1861
|
return { success: true, message: this.formatMessage(AGENT_HOOKS_SKIP_MESSAGE) };
|
|
1772
1862
|
}
|
|
1773
|
-
const misePath =
|
|
1774
|
-
if (!
|
|
1863
|
+
const misePath = join10(this.context.targetDir, "mise.toml");
|
|
1864
|
+
if (!existsSync8(misePath)) {
|
|
1775
1865
|
return {
|
|
1776
1866
|
success: false,
|
|
1777
1867
|
message: "\u26A0\uFE0F No mise.toml found \u2014 run `pjangler init mise` first, then re-run."
|
|
1778
1868
|
};
|
|
1779
1869
|
}
|
|
1780
|
-
let content =
|
|
1870
|
+
let content = readFileSync4(misePath, "utf8");
|
|
1781
1871
|
if (content.includes(_WireMiseAgentHooks.MARKER)) {
|
|
1782
1872
|
return { success: true, message: this.formatMessage("\u2713 mise.toml already wired for agent-hooks") };
|
|
1783
1873
|
}
|
|
@@ -1853,7 +1943,7 @@ ${leaveBlock}`);
|
|
|
1853
1943
|
""
|
|
1854
1944
|
].join("\n");
|
|
1855
1945
|
content = content.replace(/\n*$/, "\n") + appended;
|
|
1856
|
-
if (!this.context.dryRun)
|
|
1946
|
+
if (!this.context.dryRun) writeFileSync5(misePath, content);
|
|
1857
1947
|
if (wiredHooks) {
|
|
1858
1948
|
return { success: true, message: this.formatMessage("\u2705 Wired mise.toml ([hooks] enter/leave + tasks)") };
|
|
1859
1949
|
}
|
|
@@ -2026,11 +2116,11 @@ function createRecipe(name, context) {
|
|
|
2026
2116
|
import { cancel as cancel2, multiselect, text as text2, isCancel as isCancel5 } from "@clack/prompts";
|
|
2027
2117
|
|
|
2028
2118
|
// src/parity/index.ts
|
|
2029
|
-
import { existsSync as
|
|
2030
|
-
import { basename as basename3, dirname as dirname6, join as
|
|
2119
|
+
import { existsSync as existsSync9, lstatSync, mkdirSync as mkdirSync6, readFileSync as readFileSync5, readlinkSync, readdirSync as readdirSync2, renameSync as renameSync2, symlinkSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync6, chmodSync as chmodSync2, copyFileSync } from "node:fs";
|
|
2120
|
+
import { basename as basename3, dirname as dirname6, join as join11, relative, resolve as resolve2 } from "node:path";
|
|
2031
2121
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
2032
2122
|
import { homedir as homedir5 } from "node:os";
|
|
2033
|
-
import { spawnSync as
|
|
2123
|
+
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
2034
2124
|
var LINK_AGENTFILES_BLOCK = `# This block will handle the linking of
|
|
2035
2125
|
# agent files to the main AGENTS.md file.
|
|
2036
2126
|
#
|
|
@@ -2101,7 +2191,7 @@ run = "{{config_root}}/.mise/scripts/versioning.sh sync"
|
|
|
2101
2191
|
function resolvePjanglerRoot2() {
|
|
2102
2192
|
let dir = dirname6(fileURLToPath3(import.meta.url));
|
|
2103
2193
|
while (dir !== dirname6(dir)) {
|
|
2104
|
-
if (
|
|
2194
|
+
if (existsSync9(join11(dir, "package.json")) && existsSync9(join11(dir, "templates", "commonproject", "copier.yml"))) {
|
|
2105
2195
|
return dir;
|
|
2106
2196
|
}
|
|
2107
2197
|
dir = dirname6(dir);
|
|
@@ -2112,17 +2202,17 @@ function normalizeNewlines(value) {
|
|
|
2112
2202
|
return value.replace(/\r\n/g, "\n");
|
|
2113
2203
|
}
|
|
2114
2204
|
function readText(path) {
|
|
2115
|
-
return normalizeNewlines(
|
|
2205
|
+
return normalizeNewlines(readFileSync5(path, "utf8"));
|
|
2116
2206
|
}
|
|
2117
2207
|
function safeReadText(path) {
|
|
2118
|
-
return
|
|
2208
|
+
return existsSync9(path) ? readText(path) : null;
|
|
2119
2209
|
}
|
|
2120
2210
|
function ensureParent(path) {
|
|
2121
2211
|
mkdirSync6(dirname6(path), { recursive: true });
|
|
2122
2212
|
}
|
|
2123
2213
|
function writeText(path, content) {
|
|
2124
2214
|
ensureParent(path);
|
|
2125
|
-
|
|
2215
|
+
writeFileSync6(path, content);
|
|
2126
2216
|
}
|
|
2127
2217
|
function tryParseJson(text3) {
|
|
2128
2218
|
if (!text3) return null;
|
|
@@ -2139,7 +2229,7 @@ function titleCaseSlug(slug) {
|
|
|
2139
2229
|
return slug.split(/[-_]/g).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
2140
2230
|
}
|
|
2141
2231
|
function readSymlinkTarget(path) {
|
|
2142
|
-
if (!
|
|
2232
|
+
if (!existsSync9(path)) return null;
|
|
2143
2233
|
try {
|
|
2144
2234
|
return readlinkSync(path);
|
|
2145
2235
|
} catch {
|
|
@@ -2147,7 +2237,7 @@ function readSymlinkTarget(path) {
|
|
|
2147
2237
|
}
|
|
2148
2238
|
}
|
|
2149
2239
|
function ensureSymlink(path, target, dryRun) {
|
|
2150
|
-
if (
|
|
2240
|
+
if (existsSync9(path)) {
|
|
2151
2241
|
const stat = lstatSync(path);
|
|
2152
2242
|
if (stat.isSymbolicLink()) {
|
|
2153
2243
|
const current = readSymlinkTarget(path);
|
|
@@ -2164,11 +2254,11 @@ function ensureSymlink(path, target, dryRun) {
|
|
|
2164
2254
|
return { changed: true };
|
|
2165
2255
|
}
|
|
2166
2256
|
function bootstrapAgentsFile(repoRoot, dryRun) {
|
|
2167
|
-
const agentsPath =
|
|
2168
|
-
if (
|
|
2257
|
+
const agentsPath = join11(repoRoot, "AGENTS.md");
|
|
2258
|
+
if (existsSync9(agentsPath)) return { changedFiles: [], details: [] };
|
|
2169
2259
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
2170
|
-
const source =
|
|
2171
|
-
if (!
|
|
2260
|
+
const source = join11(repoRoot, file);
|
|
2261
|
+
if (!existsSync9(source)) continue;
|
|
2172
2262
|
const stat = lstatSync(source);
|
|
2173
2263
|
if (stat.isSymbolicLink()) continue;
|
|
2174
2264
|
if (stat.isFile()) {
|
|
@@ -2177,8 +2267,8 @@ function bootstrapAgentsFile(repoRoot, dryRun) {
|
|
|
2177
2267
|
}
|
|
2178
2268
|
return { changedFiles: [], details: [], blocked: `${file} exists but is not a regular file; cannot promote to AGENTS.md` };
|
|
2179
2269
|
}
|
|
2180
|
-
const readmePath =
|
|
2181
|
-
if (
|
|
2270
|
+
const readmePath = join11(repoRoot, "README.md");
|
|
2271
|
+
if (existsSync9(readmePath)) {
|
|
2182
2272
|
const stat = lstatSync(readmePath);
|
|
2183
2273
|
if (!stat.isFile()) return { changedFiles: [], details: [], blocked: "README.md exists but is not a regular file; cannot copy to AGENTS.md" };
|
|
2184
2274
|
if (!dryRun) copyFileSync(readmePath, agentsPath);
|
|
@@ -2217,12 +2307,12 @@ function yamlGet(text3, keyPath) {
|
|
|
2217
2307
|
return "";
|
|
2218
2308
|
}
|
|
2219
2309
|
function discoverRoles(repoRoot) {
|
|
2220
|
-
const rolesDir =
|
|
2221
|
-
if (!
|
|
2222
|
-
return
|
|
2223
|
-
const roleDir =
|
|
2224
|
-
const roleYamlPath =
|
|
2225
|
-
if (!
|
|
2310
|
+
const rolesDir = join11(repoRoot, "agents", "hermes");
|
|
2311
|
+
if (!existsSync9(rolesDir)) return [];
|
|
2312
|
+
return readdirSync2(rolesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => {
|
|
2313
|
+
const roleDir = join11(rolesDir, entry.name);
|
|
2314
|
+
const roleYamlPath = join11(roleDir, "role.yaml");
|
|
2315
|
+
if (!existsSync9(roleYamlPath)) return null;
|
|
2226
2316
|
const text3 = readText(roleYamlPath);
|
|
2227
2317
|
const runtimeRepoRaw = yamlGet(text3, "runtime.github_repo");
|
|
2228
2318
|
return {
|
|
@@ -2250,10 +2340,10 @@ function discoverRoles(repoRoot) {
|
|
|
2250
2340
|
}).filter((value) => Boolean(value));
|
|
2251
2341
|
}
|
|
2252
2342
|
function registryPath(homeDir) {
|
|
2253
|
-
return
|
|
2343
|
+
return join11(homeDir, ".hermes", "agents-registry.yaml");
|
|
2254
2344
|
}
|
|
2255
2345
|
function systemctlUser(args) {
|
|
2256
|
-
const result =
|
|
2346
|
+
const result = spawnSync6("systemctl", ["--user", ...args], { encoding: "utf8" });
|
|
2257
2347
|
return {
|
|
2258
2348
|
ok: result.status === 0,
|
|
2259
2349
|
stdout: result.stdout.trim(),
|
|
@@ -2261,8 +2351,8 @@ function systemctlUser(args) {
|
|
|
2261
2351
|
};
|
|
2262
2352
|
}
|
|
2263
2353
|
function templateScript(ctx, name) {
|
|
2264
|
-
const source =
|
|
2265
|
-
return
|
|
2354
|
+
const source = join11(ctx.pjanglerRoot, ".mise", "scripts", name);
|
|
2355
|
+
return existsSync9(source) ? readText(source) : void 0;
|
|
2266
2356
|
}
|
|
2267
2357
|
function templateVersioningScript(ctx) {
|
|
2268
2358
|
return templateScript(ctx, "versioning.sh");
|
|
@@ -2274,8 +2364,8 @@ function resolveAgentHooksLayer2(ctx) {
|
|
|
2274
2364
|
const override = process.env.PJ_AGENT_HOOKS_LAYER;
|
|
2275
2365
|
if (override === "0" || override === "false") return false;
|
|
2276
2366
|
if (override === "1" || override === "true") return true;
|
|
2277
|
-
if (
|
|
2278
|
-
return !
|
|
2367
|
+
if (existsSync9(join11(ctx.repoRoot, ".agents", "hooks", "sync.py"))) return true;
|
|
2368
|
+
return !existsSync9(join11(ctx.homeDir, ".agents", "hooks"));
|
|
2279
2369
|
}
|
|
2280
2370
|
function evaluateMiseConditionals(template, agentHooksLayer) {
|
|
2281
2371
|
const out = [];
|
|
@@ -2305,10 +2395,10 @@ function renderGeneratedProjectMiseToml(ctx, template) {
|
|
|
2305
2395
|
return evaluateMiseConditionals(template, resolveAgentHooksLayer2(ctx)).replace(/\{%\s*raw\s*%\}([\s\S]*?)\{%\s*endraw\s*%\}/g, "$1").replace(/\{\{\s*project_name\s*\}\}/g, projectName);
|
|
2306
2396
|
}
|
|
2307
2397
|
function ensureMiseTomlFromTemplate(ctx, changedFiles) {
|
|
2308
|
-
const targetPath =
|
|
2309
|
-
if (
|
|
2310
|
-
const sourcePath =
|
|
2311
|
-
if (!
|
|
2398
|
+
const targetPath = join11(ctx.repoRoot, "mise.toml");
|
|
2399
|
+
if (existsSync9(targetPath)) return false;
|
|
2400
|
+
const sourcePath = join11(ctx.pjanglerRoot, "templates", "commonproject", "template", "mise.toml.jinja");
|
|
2401
|
+
if (!existsSync9(sourcePath)) return false;
|
|
2312
2402
|
changedFiles.push(targetPath);
|
|
2313
2403
|
if (!ctx.dryRun) {
|
|
2314
2404
|
writeText(targetPath, renderGeneratedProjectMiseToml(ctx, readText(sourcePath)));
|
|
@@ -2316,8 +2406,8 @@ function ensureMiseTomlFromTemplate(ctx, changedFiles) {
|
|
|
2316
2406
|
return true;
|
|
2317
2407
|
}
|
|
2318
2408
|
function templateVersionFilesConf(ctx, repoRoot) {
|
|
2319
|
-
const packageJson =
|
|
2320
|
-
return
|
|
2409
|
+
const packageJson = join11(repoRoot, "package.json");
|
|
2410
|
+
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";
|
|
2321
2411
|
}
|
|
2322
2412
|
function replaceOrAppendManagedBlock(text3, startMarker, block, beforePattern) {
|
|
2323
2413
|
if (startMarker.test(text3)) {
|
|
@@ -2341,7 +2431,7 @@ var CONDITIONAL_HERMES_PATHS = ["agents/hermes/pm/hermes", "agent/hermes/pm/herm
|
|
|
2341
2431
|
function requiredMisePathEntries(ctx) {
|
|
2342
2432
|
const required = [...BASE_MISE_PATH_ENTRIES];
|
|
2343
2433
|
for (const candidate of CONDITIONAL_HERMES_PATHS) {
|
|
2344
|
-
if (
|
|
2434
|
+
if (existsSync9(join11(ctx.repoRoot, candidate)) && !required.includes(candidate)) required.push(candidate);
|
|
2345
2435
|
}
|
|
2346
2436
|
return required;
|
|
2347
2437
|
}
|
|
@@ -2490,7 +2580,7 @@ function upsertLinkAgentfilesBlock(text3, ctx) {
|
|
|
2490
2580
|
return insertTomlBlockBeforeVersioning(cleaned, LINK_AGENTFILES_WATCH_TASK_BLOCK);
|
|
2491
2581
|
}
|
|
2492
2582
|
function readProjectJson(ctx) {
|
|
2493
|
-
return tryParseJson(safeReadText(
|
|
2583
|
+
return tryParseJson(safeReadText(join11(ctx.repoRoot, ".project.json")));
|
|
2494
2584
|
}
|
|
2495
2585
|
function boolSetting(value, fallback) {
|
|
2496
2586
|
if (typeof value === "boolean") return value;
|
|
@@ -2565,12 +2655,12 @@ function canonicalProjectJson(ctx) {
|
|
|
2565
2655
|
};
|
|
2566
2656
|
}
|
|
2567
2657
|
function projectJsonFinding(ctx) {
|
|
2568
|
-
const projectPath =
|
|
2569
|
-
const planeJsonPath =
|
|
2658
|
+
const projectPath = join11(ctx.repoRoot, ".project.json");
|
|
2659
|
+
const planeJsonPath = join11(ctx.repoRoot, ".plane.json");
|
|
2570
2660
|
const details = [];
|
|
2571
2661
|
const data = readProjectJson(ctx);
|
|
2572
2662
|
const roles = discoverRoles(ctx.repoRoot);
|
|
2573
|
-
if (!
|
|
2663
|
+
if (!existsSync9(projectPath)) {
|
|
2574
2664
|
return { id: "sot.project-json", title: "Canonical .project.json", status: "fail", summary: ".project.json missing", details: [], fixable: true };
|
|
2575
2665
|
}
|
|
2576
2666
|
if (!data) {
|
|
@@ -2605,7 +2695,7 @@ function projectJsonFinding(ctx) {
|
|
|
2605
2695
|
for (const key of ["enabled", "grace_hours", "auto_review"]) {
|
|
2606
2696
|
if (!(key in reconcile)) details.push(`automation.reconcile.${key} missing`);
|
|
2607
2697
|
}
|
|
2608
|
-
if (
|
|
2698
|
+
if (existsSync9(planeJsonPath)) details.push(".plane.json should not exist once .project.json is canonical");
|
|
2609
2699
|
return {
|
|
2610
2700
|
id: "sot.project-json",
|
|
2611
2701
|
title: "Canonical .project.json",
|
|
@@ -2686,17 +2776,17 @@ exec env HERMES_HOME="$HERMES_HOME" HERMES_FLEET_ENV="$FLEET_ENV" HERMES_OAUTH
|
|
|
2686
2776
|
`.replace(/\u0010/g, "$");
|
|
2687
2777
|
}
|
|
2688
2778
|
function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip) {
|
|
2689
|
-
if (!
|
|
2779
|
+
if (!existsSync9(sourceDir)) return;
|
|
2690
2780
|
mkdirSync6(targetDir, { recursive: true });
|
|
2691
|
-
for (const entry of
|
|
2692
|
-
const sourcePath =
|
|
2781
|
+
for (const entry of readdirSync2(sourceDir, { withFileTypes: true })) {
|
|
2782
|
+
const sourcePath = join11(sourceDir, entry.name);
|
|
2693
2783
|
if (skip?.(sourcePath)) continue;
|
|
2694
|
-
const targetPath =
|
|
2784
|
+
const targetPath = join11(targetDir, entry.name);
|
|
2695
2785
|
if (entry.isDirectory()) {
|
|
2696
2786
|
copyMissingRecursive(sourcePath, targetPath, changedFiles, dryRun, skip);
|
|
2697
2787
|
continue;
|
|
2698
2788
|
}
|
|
2699
|
-
if (
|
|
2789
|
+
if (existsSync9(targetPath)) continue;
|
|
2700
2790
|
changedFiles.push(targetPath);
|
|
2701
2791
|
if (!dryRun) {
|
|
2702
2792
|
ensureParent(targetPath);
|
|
@@ -2705,7 +2795,7 @@ function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip)
|
|
|
2705
2795
|
}
|
|
2706
2796
|
}
|
|
2707
2797
|
function upsertSubmodule(repoRoot, role, changedFiles, dryRun) {
|
|
2708
|
-
const gitmodulesPath =
|
|
2798
|
+
const gitmodulesPath = join11(repoRoot, ".gitmodules");
|
|
2709
2799
|
const repoName = role.runtimeRepo || `agent-hm-${role.repo}-${role.role}`;
|
|
2710
2800
|
const owner = role.runtimeOwner || "delorenj";
|
|
2711
2801
|
const block = `[submodule "agents/hermes/${role.role}/runtime"]
|
|
@@ -2805,14 +2895,14 @@ var RULES = [
|
|
|
2805
2895
|
id: "mise.config-root",
|
|
2806
2896
|
title: "mise config_root + AGENTS link hooks",
|
|
2807
2897
|
audit: (ctx) => {
|
|
2808
|
-
const misePath =
|
|
2809
|
-
if (!
|
|
2898
|
+
const misePath = join11(ctx.repoRoot, "mise.toml");
|
|
2899
|
+
if (!existsSync9(misePath)) {
|
|
2810
2900
|
return { id: "mise.config-root", title: "mise config_root + AGENTS link hooks", status: "fail", summary: "mise.toml missing", details: [], fixable: true };
|
|
2811
2901
|
}
|
|
2812
2902
|
const text3 = readText(misePath);
|
|
2813
2903
|
const details = [];
|
|
2814
|
-
const linkAgentfilesPath =
|
|
2815
|
-
if (!
|
|
2904
|
+
const linkAgentfilesPath = join11(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
|
|
2905
|
+
if (!existsSync9(linkAgentfilesPath)) details.push(".mise/scripts/link-agentfiles.sh missing");
|
|
2816
2906
|
const pathValues = [...(text3.match(/^_\.path\s*=\s*\[([^\]]*)\]/m)?.[1] ?? "").matchAll(/"([^"]+)"/g)].map((match) => match[1]);
|
|
2817
2907
|
const missingPathValues = requiredMisePathEntries(ctx).filter((value) => !pathValues.includes(value));
|
|
2818
2908
|
if (missingPathValues.length) details.push(`[env]._.path should include ${missingPathValues.join(", ")}`);
|
|
@@ -2830,10 +2920,10 @@ var RULES = [
|
|
|
2830
2920
|
};
|
|
2831
2921
|
},
|
|
2832
2922
|
migrate: (ctx, finding) => {
|
|
2833
|
-
const path =
|
|
2923
|
+
const path = join11(ctx.repoRoot, "mise.toml");
|
|
2834
2924
|
const changedFiles = [];
|
|
2835
2925
|
const details = [];
|
|
2836
|
-
if (!
|
|
2926
|
+
if (!existsSync9(path)) {
|
|
2837
2927
|
if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
|
|
2838
2928
|
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: [] };
|
|
2839
2929
|
}
|
|
@@ -2849,7 +2939,7 @@ var RULES = [
|
|
|
2849
2939
|
if (!ctx.dryRun) writeText(path, next);
|
|
2850
2940
|
text3 = next;
|
|
2851
2941
|
}
|
|
2852
|
-
const linkAgentfilesPath =
|
|
2942
|
+
const linkAgentfilesPath = join11(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
|
|
2853
2943
|
const expectedScript = templateLinkAgentfilesScript(ctx);
|
|
2854
2944
|
if (expectedScript === void 0) {
|
|
2855
2945
|
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: [] };
|
|
@@ -2876,13 +2966,13 @@ var RULES = [
|
|
|
2876
2966
|
title: "managed mise versioning block",
|
|
2877
2967
|
audit: (ctx) => {
|
|
2878
2968
|
const details = [];
|
|
2879
|
-
const misePath =
|
|
2880
|
-
const versioningPath =
|
|
2881
|
-
const manifestPath =
|
|
2969
|
+
const misePath = join11(ctx.repoRoot, "mise.toml");
|
|
2970
|
+
const versioningPath = join11(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
|
|
2971
|
+
const manifestPath = join11(ctx.repoRoot, ".mise", "version-files.conf");
|
|
2882
2972
|
const text3 = safeReadText(misePath);
|
|
2883
2973
|
if (!text3?.includes("# >>> mise-versioning >>>")) details.push("mise versioning managed block missing");
|
|
2884
|
-
if (!
|
|
2885
|
-
if (!
|
|
2974
|
+
if (!existsSync9(versioningPath)) details.push(".mise/scripts/versioning.sh missing");
|
|
2975
|
+
if (!existsSync9(manifestPath)) details.push(".mise/version-files.conf missing");
|
|
2886
2976
|
return {
|
|
2887
2977
|
id: "mise.versioning",
|
|
2888
2978
|
title: "managed mise versioning block",
|
|
@@ -2895,8 +2985,8 @@ var RULES = [
|
|
|
2895
2985
|
migrate: (ctx, finding) => {
|
|
2896
2986
|
const changedFiles = [];
|
|
2897
2987
|
const details = [];
|
|
2898
|
-
const misePath =
|
|
2899
|
-
if (!
|
|
2988
|
+
const misePath = join11(ctx.repoRoot, "mise.toml");
|
|
2989
|
+
if (!existsSync9(misePath)) {
|
|
2900
2990
|
if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
|
|
2901
2991
|
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: [] };
|
|
2902
2992
|
}
|
|
@@ -2906,12 +2996,21 @@ var RULES = [
|
|
|
2906
2996
|
}
|
|
2907
2997
|
}
|
|
2908
2998
|
const currentMise = readText(misePath);
|
|
2909
|
-
|
|
2999
|
+
let cleanedMise = currentMise;
|
|
3000
|
+
if (!currentMise.includes("# >>> mise-versioning >>>")) {
|
|
3001
|
+
const taskNames = ["version", "version:bump", "version:bump-patch", "version:bump-minor", "version:bump-major", "version:check", "version:sync"];
|
|
3002
|
+
for (const taskName of taskNames) {
|
|
3003
|
+
const escaped = taskName.replace(/:/g, "\\:");
|
|
3004
|
+
const headerPattern = new RegExp(`^\\[tasks\\.(?:"${escaped}"|'${escaped}'|${escaped})\\]$`);
|
|
3005
|
+
cleanedMise = removeTomlSection(cleanedMise, headerPattern);
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
3008
|
+
const nextMise = replaceOrAppendManagedBlock(cleanedMise, /# >>> mise-versioning >>>/, VERSIONING_BLOCK, /^\[tasks\.build\]/m);
|
|
2910
3009
|
if (nextMise !== currentMise) {
|
|
2911
3010
|
if (!changedFiles.includes(misePath)) changedFiles.push(misePath);
|
|
2912
3011
|
if (!ctx.dryRun) writeText(misePath, nextMise);
|
|
2913
3012
|
}
|
|
2914
|
-
const versioningPath =
|
|
3013
|
+
const versioningPath = join11(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
|
|
2915
3014
|
const expectedScript = templateVersioningScript(ctx);
|
|
2916
3015
|
if (expectedScript === void 0) {
|
|
2917
3016
|
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: [] };
|
|
@@ -2923,7 +3022,7 @@ var RULES = [
|
|
|
2923
3022
|
chmodSync2(versioningPath, 493);
|
|
2924
3023
|
}
|
|
2925
3024
|
}
|
|
2926
|
-
const manifestPath =
|
|
3025
|
+
const manifestPath = join11(ctx.repoRoot, ".mise", "version-files.conf");
|
|
2927
3026
|
const expectedManifest = templateVersionFilesConf(ctx, ctx.repoRoot);
|
|
2928
3027
|
if (safeReadText(manifestPath) !== expectedManifest) {
|
|
2929
3028
|
changedFiles.push(manifestPath);
|
|
@@ -2943,9 +3042,9 @@ var RULES = [
|
|
|
2943
3042
|
id: "sot.agent-symlinks",
|
|
2944
3043
|
title: "AGENTS/CLAUDE/GEMINI symlink contract",
|
|
2945
3044
|
audit: (ctx) => {
|
|
2946
|
-
const agentsPath =
|
|
2947
|
-
if (!
|
|
2948
|
-
const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) =>
|
|
3045
|
+
const agentsPath = join11(ctx.repoRoot, "AGENTS.md");
|
|
3046
|
+
if (!existsSync9(agentsPath)) {
|
|
3047
|
+
const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) => existsSync9(join11(ctx.repoRoot, file)));
|
|
2949
3048
|
if (fallbackSources.length === 0) {
|
|
2950
3049
|
return { id: "sot.agent-symlinks", title: "AGENTS/CLAUDE/GEMINI symlink contract", status: "skip", summary: "AGENTS.md missing; symlink contract not applicable", details: [], fixable: false };
|
|
2951
3050
|
}
|
|
@@ -2960,7 +3059,7 @@ var RULES = [
|
|
|
2960
3059
|
}
|
|
2961
3060
|
const details = [];
|
|
2962
3061
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
2963
|
-
const full =
|
|
3062
|
+
const full = join11(ctx.repoRoot, file);
|
|
2964
3063
|
const target = readSymlinkTarget(full);
|
|
2965
3064
|
if (target !== "AGENTS.md") details.push(`${file} should be a symlink to AGENTS.md`);
|
|
2966
3065
|
}
|
|
@@ -2984,7 +3083,7 @@ var RULES = [
|
|
|
2984
3083
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "AGENTS.md missing; cannot derive canonical agent file", changedFiles, details: [bootstrap.blocked] };
|
|
2985
3084
|
}
|
|
2986
3085
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
2987
|
-
const full =
|
|
3086
|
+
const full = join11(ctx.repoRoot, file);
|
|
2988
3087
|
const result = ensureSymlink(full, "AGENTS.md", ctx.dryRun);
|
|
2989
3088
|
if (result.blocked) blockedDetails.push(result.blocked);
|
|
2990
3089
|
if (result.changed) changedFiles.push(full);
|
|
@@ -3006,7 +3105,7 @@ var RULES = [
|
|
|
3006
3105
|
migrate: (ctx, finding) => {
|
|
3007
3106
|
const changedFiles = [];
|
|
3008
3107
|
const details = [];
|
|
3009
|
-
const path =
|
|
3108
|
+
const path = join11(ctx.repoRoot, ".project.json");
|
|
3010
3109
|
const existing = readProjectJson(ctx) ?? {};
|
|
3011
3110
|
const canonical = canonicalProjectJson(ctx);
|
|
3012
3111
|
const merged = { ...existing, ...canonical };
|
|
@@ -3016,10 +3115,10 @@ var RULES = [
|
|
|
3016
3115
|
changedFiles.push(path);
|
|
3017
3116
|
if (!ctx.dryRun) writeText(path, expected);
|
|
3018
3117
|
}
|
|
3019
|
-
const planeJson =
|
|
3020
|
-
if (
|
|
3118
|
+
const planeJson = join11(ctx.repoRoot, ".plane.json");
|
|
3119
|
+
if (existsSync9(planeJson)) {
|
|
3021
3120
|
const backup = `${planeJson}.migrated-backup`;
|
|
3022
|
-
if (
|
|
3121
|
+
if (existsSync9(backup)) {
|
|
3023
3122
|
details.push(`cannot back up .plane.json because ${relative(ctx.repoRoot, backup)} already exists`);
|
|
3024
3123
|
} else {
|
|
3025
3124
|
changedFiles.push(backup);
|
|
@@ -3041,8 +3140,8 @@ var RULES = [
|
|
|
3041
3140
|
title: ".env.op + gitignore secrets contract",
|
|
3042
3141
|
audit: (ctx) => {
|
|
3043
3142
|
const details = [];
|
|
3044
|
-
const envOp = safeReadText(
|
|
3045
|
-
const gitignore = safeReadText(
|
|
3143
|
+
const envOp = safeReadText(join11(ctx.repoRoot, ".env.op"));
|
|
3144
|
+
const gitignore = safeReadText(join11(ctx.repoRoot, ".gitignore"));
|
|
3046
3145
|
if (!envOp) {
|
|
3047
3146
|
details.push(".env.op missing");
|
|
3048
3147
|
} else {
|
|
@@ -3068,12 +3167,12 @@ var RULES = [
|
|
|
3068
3167
|
migrate: (ctx, finding) => {
|
|
3069
3168
|
const changedFiles = [];
|
|
3070
3169
|
const details = [];
|
|
3071
|
-
const envOpPath =
|
|
3072
|
-
if (!
|
|
3170
|
+
const envOpPath = join11(ctx.repoRoot, ".env.op");
|
|
3171
|
+
if (!existsSync9(envOpPath)) {
|
|
3073
3172
|
changedFiles.push(envOpPath);
|
|
3074
|
-
if (!ctx.dryRun) writeText(envOpPath, readText(
|
|
3173
|
+
if (!ctx.dryRun) writeText(envOpPath, readText(join11(ctx.pjanglerRoot, "templates", "commonproject", "template", ".env.op")));
|
|
3075
3174
|
}
|
|
3076
|
-
const gitignorePath =
|
|
3175
|
+
const gitignorePath = join11(ctx.repoRoot, ".gitignore");
|
|
3077
3176
|
const gitignore = safeReadText(gitignorePath) ?? "";
|
|
3078
3177
|
const requiredBlock = `# Secrets \u2014 .env is materialized by \`op inject -i .env.op > .env\` on mise enter.
|
|
3079
3178
|
# NEVER commit it. .env.op holds only 1Password references or safe literals and IS committed.
|
|
@@ -3100,7 +3199,7 @@ var RULES = [
|
|
|
3100
3199
|
title: ".copier-answers.yml provenance + drift report",
|
|
3101
3200
|
audit: (ctx) => {
|
|
3102
3201
|
const details = [];
|
|
3103
|
-
const path =
|
|
3202
|
+
const path = join11(ctx.repoRoot, ".copier-answers.yml");
|
|
3104
3203
|
const text3 = safeReadText(path);
|
|
3105
3204
|
const project = readProjectJson(ctx);
|
|
3106
3205
|
if (!text3) {
|
|
@@ -3131,12 +3230,12 @@ var RULES = [
|
|
|
3131
3230
|
const changedFiles = [];
|
|
3132
3231
|
const project = canonicalProjectJson(ctx);
|
|
3133
3232
|
const text3 = `# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
|
|
3134
|
-
_src_path: ${
|
|
3233
|
+
_src_path: ${join11(ctx.pjanglerRoot, "templates", "commonproject")}
|
|
3135
3234
|
project_description: ${String(project.project_description)}
|
|
3136
3235
|
project_name: ${String(project.project_name)}
|
|
3137
3236
|
ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
3138
3237
|
`;
|
|
3139
|
-
const path =
|
|
3238
|
+
const path = join11(ctx.repoRoot, ".copier-answers.yml");
|
|
3140
3239
|
if (safeReadText(path) !== text3) {
|
|
3141
3240
|
changedFiles.push(path);
|
|
3142
3241
|
if (!ctx.dryRun) writeText(path, text3);
|
|
@@ -3155,15 +3254,15 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3155
3254
|
id: "bmad.scaffold",
|
|
3156
3255
|
title: "BMAD modules/docs scaffold",
|
|
3157
3256
|
audit: (ctx) => {
|
|
3158
|
-
const sourceRoot =
|
|
3159
|
-
const targetRoot =
|
|
3257
|
+
const sourceRoot = join11(ctx.pjanglerRoot, "templates", "commonproject", "_bmad");
|
|
3258
|
+
const targetRoot = join11(ctx.repoRoot, "_bmad");
|
|
3160
3259
|
const sentinels = [
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3260
|
+
join11("core", "config.yaml"),
|
|
3261
|
+
join11("custom", "config.yaml"),
|
|
3262
|
+
join11("custom", "workflows", "ticket-lifecycle", "workflow.yaml"),
|
|
3263
|
+
join11("bmm", "workflows", "workflow-status", "workflow.yaml")
|
|
3165
3264
|
];
|
|
3166
|
-
const missing = sentinels.filter((file) =>
|
|
3265
|
+
const missing = sentinels.filter((file) => existsSync9(join11(sourceRoot, file)) && !existsSync9(join11(targetRoot, file)));
|
|
3167
3266
|
return {
|
|
3168
3267
|
id: "bmad.scaffold",
|
|
3169
3268
|
title: "BMAD modules/docs scaffold",
|
|
@@ -3175,7 +3274,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3175
3274
|
},
|
|
3176
3275
|
migrate: (ctx, finding) => {
|
|
3177
3276
|
const changedFiles = [];
|
|
3178
|
-
copyMissingRecursive(
|
|
3277
|
+
copyMissingRecursive(join11(ctx.pjanglerRoot, "templates", "commonproject", "_bmad"), join11(ctx.repoRoot, "_bmad"), changedFiles, ctx.dryRun);
|
|
3179
3278
|
return {
|
|
3180
3279
|
id: finding.id,
|
|
3181
3280
|
title: finding.title,
|
|
@@ -3197,11 +3296,11 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3197
3296
|
}
|
|
3198
3297
|
const details = [];
|
|
3199
3298
|
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"]) {
|
|
3200
|
-
if (!
|
|
3299
|
+
if (!existsSync9(join11(role.roleDir, rel))) details.push(`missing ${relative(ctx.repoRoot, join11(role.roleDir, rel))}`);
|
|
3201
3300
|
}
|
|
3202
|
-
const gitmodules = safeReadText(
|
|
3301
|
+
const gitmodules = safeReadText(join11(ctx.repoRoot, ".gitmodules")) ?? "";
|
|
3203
3302
|
if (!gitmodules.includes(`agents/hermes/${role.role}/runtime`)) details.push(".gitmodules missing pm runtime submodule entry");
|
|
3204
|
-
if (!profileMetaInheritsDefault(
|
|
3303
|
+
if (!profileMetaInheritsDefault(join11(role.roleDir, "runtime", "profile.yaml"))) {
|
|
3205
3304
|
details.push("runtime/profile.yaml missing inherited default config metadata");
|
|
3206
3305
|
}
|
|
3207
3306
|
const registry = safeReadText(registryPath(ctx.homeDir));
|
|
@@ -3222,21 +3321,21 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3222
3321
|
if (!role) {
|
|
3223
3322
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "No pm role present", changedFiles, details: [] };
|
|
3224
3323
|
}
|
|
3225
|
-
const templateRoleDir =
|
|
3226
|
-
writeIfDifferent(
|
|
3227
|
-
writeIfDifferent(
|
|
3228
|
-
writeIfDifferent(
|
|
3229
|
-
copyMissingRecursive(
|
|
3230
|
-
copyMissingRecursive(
|
|
3231
|
-
copyMissingRecursive(
|
|
3232
|
-
const promptSource =
|
|
3233
|
-
const promptTarget =
|
|
3234
|
-
if (
|
|
3324
|
+
const templateRoleDir = join11(ctx.pjanglerRoot, "templates", "hermes-agent", "template");
|
|
3325
|
+
writeIfDifferent(join11(role.roleDir, "SOUL.md"), renderSoul(role), ctx.dryRun, changedFiles);
|
|
3326
|
+
writeIfDifferent(join11(role.roleDir, "hermes"), renderHermesWrapper(role), ctx.dryRun, changedFiles, 493);
|
|
3327
|
+
writeIfDifferent(join11(role.roleDir, ".gitignore"), readText(join11(templateRoleDir, ".gitignore.jinja")).replace(/\{\{ role \}\}/g, role.role), ctx.dryRun, changedFiles);
|
|
3328
|
+
copyMissingRecursive(join11(templateRoleDir, ".runtime-scaffold"), join11(role.roleDir, ".runtime-scaffold"), changedFiles, ctx.dryRun);
|
|
3329
|
+
copyMissingRecursive(join11(templateRoleDir, ".runtime-scaffold"), join11(role.roleDir, "runtime"), changedFiles, ctx.dryRun);
|
|
3330
|
+
copyMissingRecursive(join11(templateRoleDir, ".scripts"), join11(role.roleDir, ".scripts"), changedFiles, ctx.dryRun, (source) => source.endsWith("sentinel.prompt.md.jinja"));
|
|
3331
|
+
const promptSource = join11(templateRoleDir, ".scripts", "sentinel.prompt.md.jinja");
|
|
3332
|
+
const promptTarget = join11(role.roleDir, ".scripts", "sentinel.prompt.md");
|
|
3333
|
+
if (existsSync9(promptSource) && !existsSync9(promptTarget)) {
|
|
3235
3334
|
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);
|
|
3236
3335
|
writeIfDifferent(promptTarget, prompt, ctx.dryRun, changedFiles);
|
|
3237
3336
|
}
|
|
3238
3337
|
upsertSubmodule(ctx.repoRoot, role, changedFiles, ctx.dryRun);
|
|
3239
|
-
const profileMetaUpdated = upsertInheritedProfileMeta(
|
|
3338
|
+
const profileMetaUpdated = upsertInheritedProfileMeta(join11(role.roleDir, "runtime", "profile.yaml"), changedFiles, ctx.dryRun);
|
|
3240
3339
|
if (profileMetaUpdated) details.push(`updated ${profileMetaUpdated}`);
|
|
3241
3340
|
const registryUpdated = upsertRegistryEntry(role, ctx.homeDir, changedFiles, ctx.dryRun);
|
|
3242
3341
|
if (registryUpdated) details.push(`updated ${registryUpdated}`);
|
|
@@ -3250,6 +3349,103 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3250
3349
|
};
|
|
3251
3350
|
}
|
|
3252
3351
|
},
|
|
3352
|
+
{
|
|
3353
|
+
id: "hermes.untracked-runtimes",
|
|
3354
|
+
title: "Hermes agent runtimes untracked + gitignored",
|
|
3355
|
+
audit: (ctx) => {
|
|
3356
|
+
const roles = discoverRoles(ctx.repoRoot);
|
|
3357
|
+
if (roles.length === 0) {
|
|
3358
|
+
return {
|
|
3359
|
+
id: "hermes.untracked-runtimes",
|
|
3360
|
+
title: "Hermes agent runtimes untracked + gitignored",
|
|
3361
|
+
status: "skip",
|
|
3362
|
+
summary: "No Hermes roles present",
|
|
3363
|
+
details: [],
|
|
3364
|
+
fixable: false
|
|
3365
|
+
};
|
|
3366
|
+
}
|
|
3367
|
+
const details = [];
|
|
3368
|
+
for (const role of roles) {
|
|
3369
|
+
const roleRelDir = relative(ctx.repoRoot, role.roleDir);
|
|
3370
|
+
const runtimeRelPath = join11(roleRelDir, "runtime");
|
|
3371
|
+
const lsResult = spawnSync6("git", ["ls-files", "--stage", runtimeRelPath], {
|
|
3372
|
+
cwd: ctx.repoRoot,
|
|
3373
|
+
encoding: "utf8"
|
|
3374
|
+
});
|
|
3375
|
+
if (lsResult.status === 0 && lsResult.stdout.trim().length > 0) {
|
|
3376
|
+
details.push(`submodule runtime is tracked in Git index at ${runtimeRelPath}`);
|
|
3377
|
+
}
|
|
3378
|
+
const gitignorePath = join11(role.roleDir, ".gitignore");
|
|
3379
|
+
if (existsSync9(gitignorePath)) {
|
|
3380
|
+
const content = safeReadText(gitignorePath) ?? "";
|
|
3381
|
+
const lines = content.split(/\r?\n/).map((line) => line.trim());
|
|
3382
|
+
if (!lines.includes("runtime/") && !lines.includes("runtime")) {
|
|
3383
|
+
details.push(`.gitignore missing runtime/ ignore entry in ${relative(ctx.repoRoot, gitignorePath)}`);
|
|
3384
|
+
}
|
|
3385
|
+
} else {
|
|
3386
|
+
details.push(`.gitignore is missing in ${relative(ctx.repoRoot, gitignorePath)}`);
|
|
3387
|
+
}
|
|
3388
|
+
}
|
|
3389
|
+
return {
|
|
3390
|
+
id: "hermes.untracked-runtimes",
|
|
3391
|
+
title: "Hermes agent runtimes untracked + gitignored",
|
|
3392
|
+
status: details.length === 0 ? "pass" : "fail",
|
|
3393
|
+
summary: details.length === 0 ? "All Hermes agent runtimes are untracked and gitignored" : `${details.length} issue(s) with untracked/ignored runtimes detected`,
|
|
3394
|
+
details,
|
|
3395
|
+
fixable: true
|
|
3396
|
+
};
|
|
3397
|
+
},
|
|
3398
|
+
migrate: (ctx, finding) => {
|
|
3399
|
+
const roles = discoverRoles(ctx.repoRoot);
|
|
3400
|
+
const changedFiles = [];
|
|
3401
|
+
const details = [];
|
|
3402
|
+
for (const role of roles) {
|
|
3403
|
+
const roleRelDir = relative(ctx.repoRoot, role.roleDir);
|
|
3404
|
+
const runtimeRelPath = join11(roleRelDir, "runtime");
|
|
3405
|
+
const lsResult = spawnSync6("git", ["ls-files", "--stage", runtimeRelPath], {
|
|
3406
|
+
cwd: ctx.repoRoot,
|
|
3407
|
+
encoding: "utf8"
|
|
3408
|
+
});
|
|
3409
|
+
if (lsResult.status === 0 && lsResult.stdout.trim().length > 0) {
|
|
3410
|
+
details.push(`untrack ${runtimeRelPath}`);
|
|
3411
|
+
changedFiles.push(runtimeRelPath);
|
|
3412
|
+
if (!ctx.dryRun) {
|
|
3413
|
+
spawnSync6("git", ["rm", "--cached", "-r", runtimeRelPath], {
|
|
3414
|
+
cwd: ctx.repoRoot,
|
|
3415
|
+
encoding: "utf8"
|
|
3416
|
+
});
|
|
3417
|
+
}
|
|
3418
|
+
}
|
|
3419
|
+
const gitignorePath = join11(role.roleDir, ".gitignore");
|
|
3420
|
+
let content = "";
|
|
3421
|
+
let isIgnored = false;
|
|
3422
|
+
if (existsSync9(gitignorePath)) {
|
|
3423
|
+
content = safeReadText(gitignorePath) ?? "";
|
|
3424
|
+
const lines = content.split(/\r?\n/).map((line) => line.trim());
|
|
3425
|
+
isIgnored = lines.includes("runtime/") || lines.includes("runtime");
|
|
3426
|
+
}
|
|
3427
|
+
if (!isIgnored) {
|
|
3428
|
+
details.push(`ignore runtime/ in ${relative(ctx.repoRoot, gitignorePath)}`);
|
|
3429
|
+
changedFiles.push(gitignorePath);
|
|
3430
|
+
if (!ctx.dryRun) {
|
|
3431
|
+
if (content && !content.endsWith("\n")) {
|
|
3432
|
+
content += "\n";
|
|
3433
|
+
}
|
|
3434
|
+
content += "runtime/\n";
|
|
3435
|
+
writeText(gitignorePath, content);
|
|
3436
|
+
}
|
|
3437
|
+
}
|
|
3438
|
+
}
|
|
3439
|
+
return {
|
|
3440
|
+
id: finding.id,
|
|
3441
|
+
title: finding.title,
|
|
3442
|
+
status: changedFiles.length ? "applied" : "noop",
|
|
3443
|
+
summary: changedFiles.length ? "Hermes agent runtimes made untracked and ignored" : "No changes required",
|
|
3444
|
+
changedFiles,
|
|
3445
|
+
details
|
|
3446
|
+
};
|
|
3447
|
+
}
|
|
3448
|
+
},
|
|
3253
3449
|
{
|
|
3254
3450
|
id: "systemd.sentinel",
|
|
3255
3451
|
title: "Hermes systemd/sentinel units enabled + active",
|
|
@@ -3290,9 +3486,9 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3290
3486
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "systemd --user unavailable on this host", changedFiles, details };
|
|
3291
3487
|
}
|
|
3292
3488
|
for (const role of roles) {
|
|
3293
|
-
const sysDir =
|
|
3489
|
+
const sysDir = join11(ctx.homeDir, ".config", "systemd", "user");
|
|
3294
3490
|
const units = [`hermes-${role.agentId}-gateway.service`, `hermes-${role.agentId}-consumer.service`, `hermes-${role.agentId}-heartbeat.timer`];
|
|
3295
|
-
const allUnitsPresent = units.every((unit) =>
|
|
3491
|
+
const allUnitsPresent = units.every((unit) => existsSync9(join11(sysDir, unit)));
|
|
3296
3492
|
if (allUnitsPresent) {
|
|
3297
3493
|
if (ctx.dryRun) {
|
|
3298
3494
|
details.push(`would run: systemctl --user enable --now ${units.join(" ")}`);
|
|
@@ -3304,12 +3500,12 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3304
3500
|
}
|
|
3305
3501
|
continue;
|
|
3306
3502
|
}
|
|
3307
|
-
for (const script of [
|
|
3308
|
-
if (!script || !
|
|
3503
|
+
for (const script of [join11(role.roleDir, ".scripts", "70-systemd.sh")]) {
|
|
3504
|
+
if (!script || !existsSync9(script)) continue;
|
|
3309
3505
|
if (ctx.dryRun) {
|
|
3310
3506
|
details.push(`would run: bash ${script}`);
|
|
3311
3507
|
} else {
|
|
3312
|
-
const result =
|
|
3508
|
+
const result = spawnSync6("bash", [script], { cwd: role.roleDir, encoding: "utf8" });
|
|
3313
3509
|
if (result.status !== 0) details.push(`script failed: ${script}: ${result.stderr.trim() || result.stdout.trim()}`);
|
|
3314
3510
|
}
|
|
3315
3511
|
}
|
|
@@ -3444,15 +3640,15 @@ function formatMigrationReport(report) {
|
|
|
3444
3640
|
}
|
|
3445
3641
|
|
|
3446
3642
|
// src/utils/version.ts
|
|
3447
|
-
import { readFileSync as
|
|
3448
|
-
import { dirname as dirname7, join as
|
|
3643
|
+
import { readFileSync as readFileSync6 } from "node:fs";
|
|
3644
|
+
import { dirname as dirname7, join as join12 } from "node:path";
|
|
3449
3645
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
3450
3646
|
var PJANGLER_VERSION = (() => {
|
|
3451
3647
|
try {
|
|
3452
3648
|
let dir = dirname7(fileURLToPath4(import.meta.url));
|
|
3453
3649
|
for (let i = 0; i < 4; i++) {
|
|
3454
3650
|
try {
|
|
3455
|
-
const raw =
|
|
3651
|
+
const raw = readFileSync6(join12(dir, "package.json"), "utf8");
|
|
3456
3652
|
return JSON.parse(raw).version ?? "0.0.0";
|
|
3457
3653
|
} catch {
|
|
3458
3654
|
const parent = dirname7(dir);
|
|
@@ -3495,16 +3691,16 @@ async function promptForRuleIds(rules) {
|
|
|
3495
3691
|
return selected;
|
|
3496
3692
|
}
|
|
3497
3693
|
function readJson(path) {
|
|
3498
|
-
if (!
|
|
3694
|
+
if (!existsSync10(path)) return void 0;
|
|
3499
3695
|
try {
|
|
3500
|
-
const parsed = JSON.parse(
|
|
3696
|
+
const parsed = JSON.parse(readFileSync7(path, "utf8"));
|
|
3501
3697
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
|
|
3502
3698
|
} catch {
|
|
3503
3699
|
return void 0;
|
|
3504
3700
|
}
|
|
3505
3701
|
}
|
|
3506
3702
|
function findGitRoot(cwd) {
|
|
3507
|
-
const result =
|
|
3703
|
+
const result = spawnSync7("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf8" });
|
|
3508
3704
|
if (result.status !== 0) return void 0;
|
|
3509
3705
|
return resolve3(result.stdout.trim());
|
|
3510
3706
|
}
|
|
@@ -3514,8 +3710,8 @@ function packageNameToProjectName(value) {
|
|
|
3514
3710
|
return name.replace(/[-_]+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase()).trim();
|
|
3515
3711
|
}
|
|
3516
3712
|
function deriveProjectDefaults(targetDir) {
|
|
3517
|
-
const manifest = readJson(
|
|
3518
|
-
const pkg = readJson(
|
|
3713
|
+
const manifest = readJson(join13(targetDir, ".project.json"));
|
|
3714
|
+
const pkg = readJson(join13(targetDir, "package.json"));
|
|
3519
3715
|
const name = String(manifest?.project_name ?? "").trim() || packageNameToProjectName(typeof pkg?.name === "string" ? pkg.name : void 0) || packageNameToProjectName(basename4(targetDir)) || "Project";
|
|
3520
3716
|
const ticketProvider = manifest?.ticket_provider && typeof manifest.ticket_provider === "object" ? manifest.ticket_provider : {};
|
|
3521
3717
|
return {
|
|
@@ -3571,7 +3767,7 @@ function actionNeedsRun(plan, kind, syncMode) {
|
|
|
3571
3767
|
if (!action || action.kind !== "project.write-manifest") return false;
|
|
3572
3768
|
const next = `${JSON.stringify(action.manifest, null, 2)}
|
|
3573
3769
|
`;
|
|
3574
|
-
return !
|
|
3770
|
+
return !existsSync10(action.path) || readFileSync7(action.path, "utf8") !== next;
|
|
3575
3771
|
}
|
|
3576
3772
|
if (kind === "copier.copy.commonproject") return true;
|
|
3577
3773
|
if (kind === "ticket-provider.create-or-link") return plan.actions.some((action) => action.kind === kind && action.enabled);
|
|
@@ -3626,7 +3822,7 @@ async function resolveProjectInitTarget(name, options) {
|
|
|
3626
3822
|
if (!targetDir && interactive) {
|
|
3627
3823
|
const defaultName = name ?? basename4(cwd);
|
|
3628
3824
|
const promptedName = name ?? await promptTextValue("Project name", packageNameToProjectName(defaultName));
|
|
3629
|
-
const defaultDir =
|
|
3825
|
+
const defaultDir = join13(cwd, promptedName.replace(/[^A-Za-z0-9._-]/g, "") || promptedName.toLowerCase().replace(/[^a-z0-9]+/g, "-"));
|
|
3630
3826
|
targetDir = await promptTextValue("Project directory", defaultDir);
|
|
3631
3827
|
name = promptedName;
|
|
3632
3828
|
}
|
|
@@ -3634,7 +3830,7 @@ async function resolveProjectInitTarget(name, options) {
|
|
|
3634
3830
|
if (!name) throw new Error("Project name or --target-dir is required when project init is not run inside a git repo");
|
|
3635
3831
|
targetDir = resolve3(process.cwd(), name.replace(/[^A-Za-z0-9._-]/g, "") || name.toLowerCase().replace(/[^a-z0-9]+/g, "-"));
|
|
3636
3832
|
}
|
|
3637
|
-
const targetExists =
|
|
3833
|
+
const targetExists = existsSync10(targetDir);
|
|
3638
3834
|
if (targetExists && !statSync2(targetDir).isDirectory()) throw new Error(`Target path is not a directory: ${targetDir}`);
|
|
3639
3835
|
const targetGitRoot = targetExists ? findGitRoot(targetDir) : void 0;
|
|
3640
3836
|
const syncMode = Boolean(targetGitRoot && resolve3(targetGitRoot) === resolve3(targetDir));
|