@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/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,12 @@ 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";
|
|
2124
|
+
import YAML2 from "yaml";
|
|
2034
2125
|
var LINK_AGENTFILES_BLOCK = `# This block will handle the linking of
|
|
2035
2126
|
# agent files to the main AGENTS.md file.
|
|
2036
2127
|
#
|
|
@@ -2101,7 +2192,7 @@ run = "{{config_root}}/.mise/scripts/versioning.sh sync"
|
|
|
2101
2192
|
function resolvePjanglerRoot2() {
|
|
2102
2193
|
let dir = dirname6(fileURLToPath3(import.meta.url));
|
|
2103
2194
|
while (dir !== dirname6(dir)) {
|
|
2104
|
-
if (
|
|
2195
|
+
if (existsSync9(join11(dir, "package.json")) && existsSync9(join11(dir, "templates", "commonproject", "copier.yml"))) {
|
|
2105
2196
|
return dir;
|
|
2106
2197
|
}
|
|
2107
2198
|
dir = dirname6(dir);
|
|
@@ -2112,17 +2203,17 @@ function normalizeNewlines(value) {
|
|
|
2112
2203
|
return value.replace(/\r\n/g, "\n");
|
|
2113
2204
|
}
|
|
2114
2205
|
function readText(path) {
|
|
2115
|
-
return normalizeNewlines(
|
|
2206
|
+
return normalizeNewlines(readFileSync5(path, "utf8"));
|
|
2116
2207
|
}
|
|
2117
2208
|
function safeReadText(path) {
|
|
2118
|
-
return
|
|
2209
|
+
return existsSync9(path) ? readText(path) : null;
|
|
2119
2210
|
}
|
|
2120
2211
|
function ensureParent(path) {
|
|
2121
2212
|
mkdirSync6(dirname6(path), { recursive: true });
|
|
2122
2213
|
}
|
|
2123
2214
|
function writeText(path, content) {
|
|
2124
2215
|
ensureParent(path);
|
|
2125
|
-
|
|
2216
|
+
writeFileSync6(path, content);
|
|
2126
2217
|
}
|
|
2127
2218
|
function tryParseJson(text3) {
|
|
2128
2219
|
if (!text3) return null;
|
|
@@ -2139,7 +2230,7 @@ function titleCaseSlug(slug) {
|
|
|
2139
2230
|
return slug.split(/[-_]/g).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
2140
2231
|
}
|
|
2141
2232
|
function readSymlinkTarget(path) {
|
|
2142
|
-
if (!
|
|
2233
|
+
if (!existsSync9(path)) return null;
|
|
2143
2234
|
try {
|
|
2144
2235
|
return readlinkSync(path);
|
|
2145
2236
|
} catch {
|
|
@@ -2147,7 +2238,7 @@ function readSymlinkTarget(path) {
|
|
|
2147
2238
|
}
|
|
2148
2239
|
}
|
|
2149
2240
|
function ensureSymlink(path, target, dryRun) {
|
|
2150
|
-
if (
|
|
2241
|
+
if (existsSync9(path)) {
|
|
2151
2242
|
const stat = lstatSync(path);
|
|
2152
2243
|
if (stat.isSymbolicLink()) {
|
|
2153
2244
|
const current = readSymlinkTarget(path);
|
|
@@ -2164,11 +2255,11 @@ function ensureSymlink(path, target, dryRun) {
|
|
|
2164
2255
|
return { changed: true };
|
|
2165
2256
|
}
|
|
2166
2257
|
function bootstrapAgentsFile(repoRoot, dryRun) {
|
|
2167
|
-
const agentsPath =
|
|
2168
|
-
if (
|
|
2258
|
+
const agentsPath = join11(repoRoot, "AGENTS.md");
|
|
2259
|
+
if (existsSync9(agentsPath)) return { changedFiles: [], details: [] };
|
|
2169
2260
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
2170
|
-
const source =
|
|
2171
|
-
if (!
|
|
2261
|
+
const source = join11(repoRoot, file);
|
|
2262
|
+
if (!existsSync9(source)) continue;
|
|
2172
2263
|
const stat = lstatSync(source);
|
|
2173
2264
|
if (stat.isSymbolicLink()) continue;
|
|
2174
2265
|
if (stat.isFile()) {
|
|
@@ -2177,8 +2268,8 @@ function bootstrapAgentsFile(repoRoot, dryRun) {
|
|
|
2177
2268
|
}
|
|
2178
2269
|
return { changedFiles: [], details: [], blocked: `${file} exists but is not a regular file; cannot promote to AGENTS.md` };
|
|
2179
2270
|
}
|
|
2180
|
-
const readmePath =
|
|
2181
|
-
if (
|
|
2271
|
+
const readmePath = join11(repoRoot, "README.md");
|
|
2272
|
+
if (existsSync9(readmePath)) {
|
|
2182
2273
|
const stat = lstatSync(readmePath);
|
|
2183
2274
|
if (!stat.isFile()) return { changedFiles: [], details: [], blocked: "README.md exists but is not a regular file; cannot copy to AGENTS.md" };
|
|
2184
2275
|
if (!dryRun) copyFileSync(readmePath, agentsPath);
|
|
@@ -2217,12 +2308,12 @@ function yamlGet(text3, keyPath) {
|
|
|
2217
2308
|
return "";
|
|
2218
2309
|
}
|
|
2219
2310
|
function discoverRoles(repoRoot) {
|
|
2220
|
-
const rolesDir =
|
|
2221
|
-
if (!
|
|
2222
|
-
return
|
|
2223
|
-
const roleDir =
|
|
2224
|
-
const roleYamlPath =
|
|
2225
|
-
if (!
|
|
2311
|
+
const rolesDir = join11(repoRoot, "agents", "hermes");
|
|
2312
|
+
if (!existsSync9(rolesDir)) return [];
|
|
2313
|
+
return readdirSync2(rolesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => {
|
|
2314
|
+
const roleDir = join11(rolesDir, entry.name);
|
|
2315
|
+
const roleYamlPath = join11(roleDir, "role.yaml");
|
|
2316
|
+
if (!existsSync9(roleYamlPath)) return null;
|
|
2226
2317
|
const text3 = readText(roleYamlPath);
|
|
2227
2318
|
const runtimeRepoRaw = yamlGet(text3, "runtime.github_repo");
|
|
2228
2319
|
return {
|
|
@@ -2250,10 +2341,10 @@ function discoverRoles(repoRoot) {
|
|
|
2250
2341
|
}).filter((value) => Boolean(value));
|
|
2251
2342
|
}
|
|
2252
2343
|
function registryPath(homeDir) {
|
|
2253
|
-
return
|
|
2344
|
+
return join11(homeDir, ".hermes", "agents-registry.yaml");
|
|
2254
2345
|
}
|
|
2255
2346
|
function systemctlUser(args) {
|
|
2256
|
-
const result =
|
|
2347
|
+
const result = spawnSync6("systemctl", ["--user", ...args], { encoding: "utf8" });
|
|
2257
2348
|
return {
|
|
2258
2349
|
ok: result.status === 0,
|
|
2259
2350
|
stdout: result.stdout.trim(),
|
|
@@ -2261,8 +2352,8 @@ function systemctlUser(args) {
|
|
|
2261
2352
|
};
|
|
2262
2353
|
}
|
|
2263
2354
|
function templateScript(ctx, name) {
|
|
2264
|
-
const source =
|
|
2265
|
-
return
|
|
2355
|
+
const source = join11(ctx.pjanglerRoot, ".mise", "scripts", name);
|
|
2356
|
+
return existsSync9(source) ? readText(source) : void 0;
|
|
2266
2357
|
}
|
|
2267
2358
|
function templateVersioningScript(ctx) {
|
|
2268
2359
|
return templateScript(ctx, "versioning.sh");
|
|
@@ -2274,8 +2365,8 @@ function resolveAgentHooksLayer2(ctx) {
|
|
|
2274
2365
|
const override = process.env.PJ_AGENT_HOOKS_LAYER;
|
|
2275
2366
|
if (override === "0" || override === "false") return false;
|
|
2276
2367
|
if (override === "1" || override === "true") return true;
|
|
2277
|
-
if (
|
|
2278
|
-
return !
|
|
2368
|
+
if (existsSync9(join11(ctx.repoRoot, ".agents", "hooks", "sync.py"))) return true;
|
|
2369
|
+
return !existsSync9(join11(ctx.homeDir, ".agents", "hooks"));
|
|
2279
2370
|
}
|
|
2280
2371
|
function evaluateMiseConditionals(template, agentHooksLayer) {
|
|
2281
2372
|
const out = [];
|
|
@@ -2305,10 +2396,10 @@ function renderGeneratedProjectMiseToml(ctx, template) {
|
|
|
2305
2396
|
return evaluateMiseConditionals(template, resolveAgentHooksLayer2(ctx)).replace(/\{%\s*raw\s*%\}([\s\S]*?)\{%\s*endraw\s*%\}/g, "$1").replace(/\{\{\s*project_name\s*\}\}/g, projectName);
|
|
2306
2397
|
}
|
|
2307
2398
|
function ensureMiseTomlFromTemplate(ctx, changedFiles) {
|
|
2308
|
-
const targetPath =
|
|
2309
|
-
if (
|
|
2310
|
-
const sourcePath =
|
|
2311
|
-
if (!
|
|
2399
|
+
const targetPath = join11(ctx.repoRoot, "mise.toml");
|
|
2400
|
+
if (existsSync9(targetPath)) return false;
|
|
2401
|
+
const sourcePath = join11(ctx.pjanglerRoot, "templates", "commonproject", "template", "mise.toml.jinja");
|
|
2402
|
+
if (!existsSync9(sourcePath)) return false;
|
|
2312
2403
|
changedFiles.push(targetPath);
|
|
2313
2404
|
if (!ctx.dryRun) {
|
|
2314
2405
|
writeText(targetPath, renderGeneratedProjectMiseToml(ctx, readText(sourcePath)));
|
|
@@ -2316,8 +2407,8 @@ function ensureMiseTomlFromTemplate(ctx, changedFiles) {
|
|
|
2316
2407
|
return true;
|
|
2317
2408
|
}
|
|
2318
2409
|
function templateVersionFilesConf(ctx, repoRoot) {
|
|
2319
|
-
const packageJson =
|
|
2320
|
-
return
|
|
2410
|
+
const packageJson = join11(repoRoot, "package.json");
|
|
2411
|
+
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
2412
|
}
|
|
2322
2413
|
function replaceOrAppendManagedBlock(text3, startMarker, block, beforePattern) {
|
|
2323
2414
|
if (startMarker.test(text3)) {
|
|
@@ -2341,7 +2432,7 @@ var CONDITIONAL_HERMES_PATHS = ["agents/hermes/pm/hermes", "agent/hermes/pm/herm
|
|
|
2341
2432
|
function requiredMisePathEntries(ctx) {
|
|
2342
2433
|
const required = [...BASE_MISE_PATH_ENTRIES];
|
|
2343
2434
|
for (const candidate of CONDITIONAL_HERMES_PATHS) {
|
|
2344
|
-
if (
|
|
2435
|
+
if (existsSync9(join11(ctx.repoRoot, candidate)) && !required.includes(candidate)) required.push(candidate);
|
|
2345
2436
|
}
|
|
2346
2437
|
return required;
|
|
2347
2438
|
}
|
|
@@ -2490,7 +2581,7 @@ function upsertLinkAgentfilesBlock(text3, ctx) {
|
|
|
2490
2581
|
return insertTomlBlockBeforeVersioning(cleaned, LINK_AGENTFILES_WATCH_TASK_BLOCK);
|
|
2491
2582
|
}
|
|
2492
2583
|
function readProjectJson(ctx) {
|
|
2493
|
-
return tryParseJson(safeReadText(
|
|
2584
|
+
return tryParseJson(safeReadText(join11(ctx.repoRoot, ".project.json")));
|
|
2494
2585
|
}
|
|
2495
2586
|
function boolSetting(value, fallback) {
|
|
2496
2587
|
if (typeof value === "boolean") return value;
|
|
@@ -2565,12 +2656,12 @@ function canonicalProjectJson(ctx) {
|
|
|
2565
2656
|
};
|
|
2566
2657
|
}
|
|
2567
2658
|
function projectJsonFinding(ctx) {
|
|
2568
|
-
const projectPath =
|
|
2569
|
-
const planeJsonPath =
|
|
2659
|
+
const projectPath = join11(ctx.repoRoot, ".project.json");
|
|
2660
|
+
const planeJsonPath = join11(ctx.repoRoot, ".plane.json");
|
|
2570
2661
|
const details = [];
|
|
2571
2662
|
const data = readProjectJson(ctx);
|
|
2572
2663
|
const roles = discoverRoles(ctx.repoRoot);
|
|
2573
|
-
if (!
|
|
2664
|
+
if (!existsSync9(projectPath)) {
|
|
2574
2665
|
return { id: "sot.project-json", title: "Canonical .project.json", status: "fail", summary: ".project.json missing", details: [], fixable: true };
|
|
2575
2666
|
}
|
|
2576
2667
|
if (!data) {
|
|
@@ -2605,7 +2696,7 @@ function projectJsonFinding(ctx) {
|
|
|
2605
2696
|
for (const key of ["enabled", "grace_hours", "auto_review"]) {
|
|
2606
2697
|
if (!(key in reconcile)) details.push(`automation.reconcile.${key} missing`);
|
|
2607
2698
|
}
|
|
2608
|
-
if (
|
|
2699
|
+
if (existsSync9(planeJsonPath)) details.push(".plane.json should not exist once .project.json is canonical");
|
|
2609
2700
|
return {
|
|
2610
2701
|
id: "sot.project-json",
|
|
2611
2702
|
title: "Canonical .project.json",
|
|
@@ -2686,17 +2777,17 @@ exec env HERMES_HOME="$HERMES_HOME" HERMES_FLEET_ENV="$FLEET_ENV" HERMES_OAUTH
|
|
|
2686
2777
|
`.replace(/\u0010/g, "$");
|
|
2687
2778
|
}
|
|
2688
2779
|
function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip) {
|
|
2689
|
-
if (!
|
|
2780
|
+
if (!existsSync9(sourceDir)) return;
|
|
2690
2781
|
mkdirSync6(targetDir, { recursive: true });
|
|
2691
|
-
for (const entry of
|
|
2692
|
-
const sourcePath =
|
|
2782
|
+
for (const entry of readdirSync2(sourceDir, { withFileTypes: true })) {
|
|
2783
|
+
const sourcePath = join11(sourceDir, entry.name);
|
|
2693
2784
|
if (skip?.(sourcePath)) continue;
|
|
2694
|
-
const targetPath =
|
|
2785
|
+
const targetPath = join11(targetDir, entry.name);
|
|
2695
2786
|
if (entry.isDirectory()) {
|
|
2696
2787
|
copyMissingRecursive(sourcePath, targetPath, changedFiles, dryRun, skip);
|
|
2697
2788
|
continue;
|
|
2698
2789
|
}
|
|
2699
|
-
if (
|
|
2790
|
+
if (existsSync9(targetPath)) continue;
|
|
2700
2791
|
changedFiles.push(targetPath);
|
|
2701
2792
|
if (!dryRun) {
|
|
2702
2793
|
ensureParent(targetPath);
|
|
@@ -2705,7 +2796,7 @@ function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip)
|
|
|
2705
2796
|
}
|
|
2706
2797
|
}
|
|
2707
2798
|
function upsertSubmodule(repoRoot, role, changedFiles, dryRun) {
|
|
2708
|
-
const gitmodulesPath =
|
|
2799
|
+
const gitmodulesPath = join11(repoRoot, ".gitmodules");
|
|
2709
2800
|
const repoName = role.runtimeRepo || `agent-hm-${role.repo}-${role.role}`;
|
|
2710
2801
|
const owner = role.runtimeOwner || "delorenj";
|
|
2711
2802
|
const block = `[submodule "agents/hermes/${role.role}/runtime"]
|
|
@@ -2800,19 +2891,186 @@ function checkUnit(unit) {
|
|
|
2800
2891
|
const active = systemctlUser(["is-active", unit]).ok;
|
|
2801
2892
|
return { enabled, active };
|
|
2802
2893
|
}
|
|
2894
|
+
var BMAD_NPM_PACKAGE = "bmad-method";
|
|
2895
|
+
var BMAD_TARGET_CHANNEL = "next";
|
|
2896
|
+
var BMAD_DIST_TAGS_TTL_MS = 60 * 60 * 1e3;
|
|
2897
|
+
var BMAD_INSTALL_TOOLS = [
|
|
2898
|
+
"claude-code",
|
|
2899
|
+
"codex",
|
|
2900
|
+
"cursor",
|
|
2901
|
+
"github-copilot",
|
|
2902
|
+
"adal",
|
|
2903
|
+
"antigravity-cli",
|
|
2904
|
+
"auggie",
|
|
2905
|
+
"goose",
|
|
2906
|
+
"cline",
|
|
2907
|
+
"codebuddy",
|
|
2908
|
+
"codewhale",
|
|
2909
|
+
"command-code",
|
|
2910
|
+
"crush",
|
|
2911
|
+
"droid",
|
|
2912
|
+
"firebender",
|
|
2913
|
+
"gemini",
|
|
2914
|
+
"antigravity",
|
|
2915
|
+
"hermes",
|
|
2916
|
+
"bob",
|
|
2917
|
+
"iflow",
|
|
2918
|
+
"junie",
|
|
2919
|
+
"kilo",
|
|
2920
|
+
"kimi-code",
|
|
2921
|
+
"kiro",
|
|
2922
|
+
"kode",
|
|
2923
|
+
"mistral-vibe",
|
|
2924
|
+
"mux",
|
|
2925
|
+
"neovate",
|
|
2926
|
+
"ona",
|
|
2927
|
+
"openclaw",
|
|
2928
|
+
"opencode",
|
|
2929
|
+
"openhands",
|
|
2930
|
+
"pi",
|
|
2931
|
+
"pochi",
|
|
2932
|
+
"qoder",
|
|
2933
|
+
"qwen",
|
|
2934
|
+
"replit",
|
|
2935
|
+
"roo",
|
|
2936
|
+
"rovo-dev",
|
|
2937
|
+
"cortex",
|
|
2938
|
+
"amp",
|
|
2939
|
+
"trae",
|
|
2940
|
+
"warp",
|
|
2941
|
+
"windsurf",
|
|
2942
|
+
"zencoder"
|
|
2943
|
+
];
|
|
2944
|
+
function bmadInstallArgs(repoRoot) {
|
|
2945
|
+
return [
|
|
2946
|
+
"-y",
|
|
2947
|
+
`${BMAD_NPM_PACKAGE}@${BMAD_TARGET_CHANNEL}`,
|
|
2948
|
+
"install",
|
|
2949
|
+
"--yes",
|
|
2950
|
+
"--directory",
|
|
2951
|
+
repoRoot,
|
|
2952
|
+
"--modules",
|
|
2953
|
+
"bmm,bmb,cis",
|
|
2954
|
+
"--tools",
|
|
2955
|
+
BMAD_INSTALL_TOOLS.join(",")
|
|
2956
|
+
];
|
|
2957
|
+
}
|
|
2958
|
+
function runBmadInstall(repoRoot) {
|
|
2959
|
+
const result = spawnSync6("npx", bmadInstallArgs(repoRoot), { encoding: "utf8" });
|
|
2960
|
+
if (result.status !== 0) {
|
|
2961
|
+
return { ok: false, error: result.stderr || result.error?.message || "Unknown error" };
|
|
2962
|
+
}
|
|
2963
|
+
return { ok: true };
|
|
2964
|
+
}
|
|
2965
|
+
function readInstalledBmadVersion(repoRoot) {
|
|
2966
|
+
const raw = safeReadText(join11(repoRoot, "_bmad", "_config", "manifest.yaml"));
|
|
2967
|
+
if (!raw) return void 0;
|
|
2968
|
+
try {
|
|
2969
|
+
const parsed = YAML2.parse(raw);
|
|
2970
|
+
const version = parsed?.installation?.version;
|
|
2971
|
+
return typeof version === "string" && version.trim() ? version.trim() : void 0;
|
|
2972
|
+
} catch {
|
|
2973
|
+
return void 0;
|
|
2974
|
+
}
|
|
2975
|
+
}
|
|
2976
|
+
function bmadCachePath(homeDir) {
|
|
2977
|
+
const cacheRoot = process.env.XDG_CACHE_HOME?.trim() || join11(homeDir, ".cache");
|
|
2978
|
+
return join11(cacheRoot, "pjangler", "bmad-dist-tags.json");
|
|
2979
|
+
}
|
|
2980
|
+
function readBmadDistTagsCache(homeDir) {
|
|
2981
|
+
const raw = safeReadText(bmadCachePath(homeDir));
|
|
2982
|
+
if (!raw) return void 0;
|
|
2983
|
+
try {
|
|
2984
|
+
const parsed = JSON.parse(raw);
|
|
2985
|
+
if (parsed && typeof parsed.fetchedAt === "number" && parsed.distTags && typeof parsed.distTags === "object") {
|
|
2986
|
+
return parsed;
|
|
2987
|
+
}
|
|
2988
|
+
} catch {
|
|
2989
|
+
}
|
|
2990
|
+
return void 0;
|
|
2991
|
+
}
|
|
2992
|
+
function fetchBmadDistTags() {
|
|
2993
|
+
const result = spawnSync6("npm", ["view", BMAD_NPM_PACKAGE, "dist-tags", "--json"], {
|
|
2994
|
+
encoding: "utf8",
|
|
2995
|
+
timeout: 8e3
|
|
2996
|
+
});
|
|
2997
|
+
if (result.status !== 0 || !result.stdout.trim()) return void 0;
|
|
2998
|
+
try {
|
|
2999
|
+
const parsed = JSON.parse(result.stdout);
|
|
3000
|
+
const obj = Array.isArray(parsed) ? parsed[0] : parsed;
|
|
3001
|
+
if (!obj || typeof obj !== "object") return void 0;
|
|
3002
|
+
const tags = {};
|
|
3003
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
3004
|
+
if (typeof value === "string") tags[key] = value;
|
|
3005
|
+
}
|
|
3006
|
+
return Object.keys(tags).length ? tags : void 0;
|
|
3007
|
+
} catch {
|
|
3008
|
+
return void 0;
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
function resolveBmadDistTags(homeDir) {
|
|
3012
|
+
const cached = readBmadDistTagsCache(homeDir);
|
|
3013
|
+
if (cached && Date.now() - cached.fetchedAt < BMAD_DIST_TAGS_TTL_MS) {
|
|
3014
|
+
return { distTags: cached.distTags, stale: false };
|
|
3015
|
+
}
|
|
3016
|
+
const fetched = fetchBmadDistTags();
|
|
3017
|
+
if (fetched) {
|
|
3018
|
+
try {
|
|
3019
|
+
const path = bmadCachePath(homeDir);
|
|
3020
|
+
mkdirSync6(dirname6(path), { recursive: true });
|
|
3021
|
+
writeFileSync6(path, JSON.stringify({ fetchedAt: Date.now(), distTags: fetched }, null, 2));
|
|
3022
|
+
} catch {
|
|
3023
|
+
}
|
|
3024
|
+
return { distTags: fetched, stale: false };
|
|
3025
|
+
}
|
|
3026
|
+
if (cached) return { distTags: cached.distTags, stale: true };
|
|
3027
|
+
return void 0;
|
|
3028
|
+
}
|
|
3029
|
+
function compareBmadVersions(a, b) {
|
|
3030
|
+
const parse = (v) => {
|
|
3031
|
+
const [core = "0", pre = ""] = v.replace(/^v/, "").split("-", 2);
|
|
3032
|
+
const parts = core.split(".");
|
|
3033
|
+
const n = (i) => parseInt(parts[i] ?? "0", 10) || 0;
|
|
3034
|
+
return { nums: [n(0), n(1), n(2)], pre };
|
|
3035
|
+
};
|
|
3036
|
+
const pa = parse(a);
|
|
3037
|
+
const pb = parse(b);
|
|
3038
|
+
for (let i = 0; i < 3; i++) {
|
|
3039
|
+
if (pa.nums[i] !== pb.nums[i]) return pa.nums[i] - pb.nums[i];
|
|
3040
|
+
}
|
|
3041
|
+
if (pa.pre === pb.pre) return 0;
|
|
3042
|
+
if (!pa.pre) return 1;
|
|
3043
|
+
if (!pb.pre) return -1;
|
|
3044
|
+
const ida = pa.pre.split(".");
|
|
3045
|
+
const idb = pb.pre.split(".");
|
|
3046
|
+
for (let i = 0; i < Math.max(ida.length, idb.length); i++) {
|
|
3047
|
+
const xa = ida[i];
|
|
3048
|
+
const xb = idb[i];
|
|
3049
|
+
if (xa === void 0) return -1;
|
|
3050
|
+
if (xb === void 0) return 1;
|
|
3051
|
+
const na = Number(xa);
|
|
3052
|
+
const nb = Number(xb);
|
|
3053
|
+
if (!Number.isNaN(na) && !Number.isNaN(nb)) {
|
|
3054
|
+
if (na !== nb) return na - nb;
|
|
3055
|
+
} else if (xa !== xb) {
|
|
3056
|
+
return xa < xb ? -1 : 1;
|
|
3057
|
+
}
|
|
3058
|
+
}
|
|
3059
|
+
return 0;
|
|
3060
|
+
}
|
|
2803
3061
|
var RULES = [
|
|
2804
3062
|
{
|
|
2805
3063
|
id: "mise.config-root",
|
|
2806
3064
|
title: "mise config_root + AGENTS link hooks",
|
|
2807
3065
|
audit: (ctx) => {
|
|
2808
|
-
const misePath =
|
|
2809
|
-
if (!
|
|
3066
|
+
const misePath = join11(ctx.repoRoot, "mise.toml");
|
|
3067
|
+
if (!existsSync9(misePath)) {
|
|
2810
3068
|
return { id: "mise.config-root", title: "mise config_root + AGENTS link hooks", status: "fail", summary: "mise.toml missing", details: [], fixable: true };
|
|
2811
3069
|
}
|
|
2812
3070
|
const text3 = readText(misePath);
|
|
2813
3071
|
const details = [];
|
|
2814
|
-
const linkAgentfilesPath =
|
|
2815
|
-
if (!
|
|
3072
|
+
const linkAgentfilesPath = join11(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
|
|
3073
|
+
if (!existsSync9(linkAgentfilesPath)) details.push(".mise/scripts/link-agentfiles.sh missing");
|
|
2816
3074
|
const pathValues = [...(text3.match(/^_\.path\s*=\s*\[([^\]]*)\]/m)?.[1] ?? "").matchAll(/"([^"]+)"/g)].map((match) => match[1]);
|
|
2817
3075
|
const missingPathValues = requiredMisePathEntries(ctx).filter((value) => !pathValues.includes(value));
|
|
2818
3076
|
if (missingPathValues.length) details.push(`[env]._.path should include ${missingPathValues.join(", ")}`);
|
|
@@ -2830,10 +3088,10 @@ var RULES = [
|
|
|
2830
3088
|
};
|
|
2831
3089
|
},
|
|
2832
3090
|
migrate: (ctx, finding) => {
|
|
2833
|
-
const path =
|
|
3091
|
+
const path = join11(ctx.repoRoot, "mise.toml");
|
|
2834
3092
|
const changedFiles = [];
|
|
2835
3093
|
const details = [];
|
|
2836
|
-
if (!
|
|
3094
|
+
if (!existsSync9(path)) {
|
|
2837
3095
|
if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
|
|
2838
3096
|
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
3097
|
}
|
|
@@ -2849,7 +3107,7 @@ var RULES = [
|
|
|
2849
3107
|
if (!ctx.dryRun) writeText(path, next);
|
|
2850
3108
|
text3 = next;
|
|
2851
3109
|
}
|
|
2852
|
-
const linkAgentfilesPath =
|
|
3110
|
+
const linkAgentfilesPath = join11(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
|
|
2853
3111
|
const expectedScript = templateLinkAgentfilesScript(ctx);
|
|
2854
3112
|
if (expectedScript === void 0) {
|
|
2855
3113
|
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 +3134,13 @@ var RULES = [
|
|
|
2876
3134
|
title: "managed mise versioning block",
|
|
2877
3135
|
audit: (ctx) => {
|
|
2878
3136
|
const details = [];
|
|
2879
|
-
const misePath =
|
|
2880
|
-
const versioningPath =
|
|
2881
|
-
const manifestPath =
|
|
3137
|
+
const misePath = join11(ctx.repoRoot, "mise.toml");
|
|
3138
|
+
const versioningPath = join11(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
|
|
3139
|
+
const manifestPath = join11(ctx.repoRoot, ".mise", "version-files.conf");
|
|
2882
3140
|
const text3 = safeReadText(misePath);
|
|
2883
3141
|
if (!text3?.includes("# >>> mise-versioning >>>")) details.push("mise versioning managed block missing");
|
|
2884
|
-
if (!
|
|
2885
|
-
if (!
|
|
3142
|
+
if (!existsSync9(versioningPath)) details.push(".mise/scripts/versioning.sh missing");
|
|
3143
|
+
if (!existsSync9(manifestPath)) details.push(".mise/version-files.conf missing");
|
|
2886
3144
|
return {
|
|
2887
3145
|
id: "mise.versioning",
|
|
2888
3146
|
title: "managed mise versioning block",
|
|
@@ -2895,8 +3153,8 @@ var RULES = [
|
|
|
2895
3153
|
migrate: (ctx, finding) => {
|
|
2896
3154
|
const changedFiles = [];
|
|
2897
3155
|
const details = [];
|
|
2898
|
-
const misePath =
|
|
2899
|
-
if (!
|
|
3156
|
+
const misePath = join11(ctx.repoRoot, "mise.toml");
|
|
3157
|
+
if (!existsSync9(misePath)) {
|
|
2900
3158
|
if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
|
|
2901
3159
|
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
3160
|
}
|
|
@@ -2906,12 +3164,21 @@ var RULES = [
|
|
|
2906
3164
|
}
|
|
2907
3165
|
}
|
|
2908
3166
|
const currentMise = readText(misePath);
|
|
2909
|
-
|
|
3167
|
+
let cleanedMise = currentMise;
|
|
3168
|
+
if (!currentMise.includes("# >>> mise-versioning >>>")) {
|
|
3169
|
+
const taskNames = ["version", "version:bump", "version:bump-patch", "version:bump-minor", "version:bump-major", "version:check", "version:sync"];
|
|
3170
|
+
for (const taskName of taskNames) {
|
|
3171
|
+
const escaped = taskName.replace(/:/g, "\\:");
|
|
3172
|
+
const headerPattern = new RegExp(`^\\[tasks\\.(?:"${escaped}"|'${escaped}'|${escaped})\\]$`);
|
|
3173
|
+
cleanedMise = removeTomlSection(cleanedMise, headerPattern);
|
|
3174
|
+
}
|
|
3175
|
+
}
|
|
3176
|
+
const nextMise = replaceOrAppendManagedBlock(cleanedMise, /# >>> mise-versioning >>>/, VERSIONING_BLOCK, /^\[tasks\.build\]/m);
|
|
2910
3177
|
if (nextMise !== currentMise) {
|
|
2911
3178
|
if (!changedFiles.includes(misePath)) changedFiles.push(misePath);
|
|
2912
3179
|
if (!ctx.dryRun) writeText(misePath, nextMise);
|
|
2913
3180
|
}
|
|
2914
|
-
const versioningPath =
|
|
3181
|
+
const versioningPath = join11(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
|
|
2915
3182
|
const expectedScript = templateVersioningScript(ctx);
|
|
2916
3183
|
if (expectedScript === void 0) {
|
|
2917
3184
|
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 +3190,7 @@ var RULES = [
|
|
|
2923
3190
|
chmodSync2(versioningPath, 493);
|
|
2924
3191
|
}
|
|
2925
3192
|
}
|
|
2926
|
-
const manifestPath =
|
|
3193
|
+
const manifestPath = join11(ctx.repoRoot, ".mise", "version-files.conf");
|
|
2927
3194
|
const expectedManifest = templateVersionFilesConf(ctx, ctx.repoRoot);
|
|
2928
3195
|
if (safeReadText(manifestPath) !== expectedManifest) {
|
|
2929
3196
|
changedFiles.push(manifestPath);
|
|
@@ -2943,9 +3210,9 @@ var RULES = [
|
|
|
2943
3210
|
id: "sot.agent-symlinks",
|
|
2944
3211
|
title: "AGENTS/CLAUDE/GEMINI symlink contract",
|
|
2945
3212
|
audit: (ctx) => {
|
|
2946
|
-
const agentsPath =
|
|
2947
|
-
if (!
|
|
2948
|
-
const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) =>
|
|
3213
|
+
const agentsPath = join11(ctx.repoRoot, "AGENTS.md");
|
|
3214
|
+
if (!existsSync9(agentsPath)) {
|
|
3215
|
+
const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) => existsSync9(join11(ctx.repoRoot, file)));
|
|
2949
3216
|
if (fallbackSources.length === 0) {
|
|
2950
3217
|
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
3218
|
}
|
|
@@ -2960,7 +3227,7 @@ var RULES = [
|
|
|
2960
3227
|
}
|
|
2961
3228
|
const details = [];
|
|
2962
3229
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
2963
|
-
const full =
|
|
3230
|
+
const full = join11(ctx.repoRoot, file);
|
|
2964
3231
|
const target = readSymlinkTarget(full);
|
|
2965
3232
|
if (target !== "AGENTS.md") details.push(`${file} should be a symlink to AGENTS.md`);
|
|
2966
3233
|
}
|
|
@@ -2984,7 +3251,7 @@ var RULES = [
|
|
|
2984
3251
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "AGENTS.md missing; cannot derive canonical agent file", changedFiles, details: [bootstrap.blocked] };
|
|
2985
3252
|
}
|
|
2986
3253
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
2987
|
-
const full =
|
|
3254
|
+
const full = join11(ctx.repoRoot, file);
|
|
2988
3255
|
const result = ensureSymlink(full, "AGENTS.md", ctx.dryRun);
|
|
2989
3256
|
if (result.blocked) blockedDetails.push(result.blocked);
|
|
2990
3257
|
if (result.changed) changedFiles.push(full);
|
|
@@ -3006,7 +3273,7 @@ var RULES = [
|
|
|
3006
3273
|
migrate: (ctx, finding) => {
|
|
3007
3274
|
const changedFiles = [];
|
|
3008
3275
|
const details = [];
|
|
3009
|
-
const path =
|
|
3276
|
+
const path = join11(ctx.repoRoot, ".project.json");
|
|
3010
3277
|
const existing = readProjectJson(ctx) ?? {};
|
|
3011
3278
|
const canonical = canonicalProjectJson(ctx);
|
|
3012
3279
|
const merged = { ...existing, ...canonical };
|
|
@@ -3016,10 +3283,10 @@ var RULES = [
|
|
|
3016
3283
|
changedFiles.push(path);
|
|
3017
3284
|
if (!ctx.dryRun) writeText(path, expected);
|
|
3018
3285
|
}
|
|
3019
|
-
const planeJson =
|
|
3020
|
-
if (
|
|
3286
|
+
const planeJson = join11(ctx.repoRoot, ".plane.json");
|
|
3287
|
+
if (existsSync9(planeJson)) {
|
|
3021
3288
|
const backup = `${planeJson}.migrated-backup`;
|
|
3022
|
-
if (
|
|
3289
|
+
if (existsSync9(backup)) {
|
|
3023
3290
|
details.push(`cannot back up .plane.json because ${relative(ctx.repoRoot, backup)} already exists`);
|
|
3024
3291
|
} else {
|
|
3025
3292
|
changedFiles.push(backup);
|
|
@@ -3041,8 +3308,8 @@ var RULES = [
|
|
|
3041
3308
|
title: ".env.op + gitignore secrets contract",
|
|
3042
3309
|
audit: (ctx) => {
|
|
3043
3310
|
const details = [];
|
|
3044
|
-
const envOp = safeReadText(
|
|
3045
|
-
const gitignore = safeReadText(
|
|
3311
|
+
const envOp = safeReadText(join11(ctx.repoRoot, ".env.op"));
|
|
3312
|
+
const gitignore = safeReadText(join11(ctx.repoRoot, ".gitignore"));
|
|
3046
3313
|
if (!envOp) {
|
|
3047
3314
|
details.push(".env.op missing");
|
|
3048
3315
|
} else {
|
|
@@ -3068,12 +3335,12 @@ var RULES = [
|
|
|
3068
3335
|
migrate: (ctx, finding) => {
|
|
3069
3336
|
const changedFiles = [];
|
|
3070
3337
|
const details = [];
|
|
3071
|
-
const envOpPath =
|
|
3072
|
-
if (!
|
|
3338
|
+
const envOpPath = join11(ctx.repoRoot, ".env.op");
|
|
3339
|
+
if (!existsSync9(envOpPath)) {
|
|
3073
3340
|
changedFiles.push(envOpPath);
|
|
3074
|
-
if (!ctx.dryRun) writeText(envOpPath, readText(
|
|
3341
|
+
if (!ctx.dryRun) writeText(envOpPath, readText(join11(ctx.pjanglerRoot, "templates", "commonproject", "template", ".env.op")));
|
|
3075
3342
|
}
|
|
3076
|
-
const gitignorePath =
|
|
3343
|
+
const gitignorePath = join11(ctx.repoRoot, ".gitignore");
|
|
3077
3344
|
const gitignore = safeReadText(gitignorePath) ?? "";
|
|
3078
3345
|
const requiredBlock = `# Secrets \u2014 .env is materialized by \`op inject -i .env.op > .env\` on mise enter.
|
|
3079
3346
|
# NEVER commit it. .env.op holds only 1Password references or safe literals and IS committed.
|
|
@@ -3100,7 +3367,7 @@ var RULES = [
|
|
|
3100
3367
|
title: ".copier-answers.yml provenance + drift report",
|
|
3101
3368
|
audit: (ctx) => {
|
|
3102
3369
|
const details = [];
|
|
3103
|
-
const path =
|
|
3370
|
+
const path = join11(ctx.repoRoot, ".copier-answers.yml");
|
|
3104
3371
|
const text3 = safeReadText(path);
|
|
3105
3372
|
const project = readProjectJson(ctx);
|
|
3106
3373
|
if (!text3) {
|
|
@@ -3131,12 +3398,12 @@ var RULES = [
|
|
|
3131
3398
|
const changedFiles = [];
|
|
3132
3399
|
const project = canonicalProjectJson(ctx);
|
|
3133
3400
|
const text3 = `# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
|
|
3134
|
-
_src_path: ${
|
|
3401
|
+
_src_path: ${join11(ctx.pjanglerRoot, "templates", "commonproject")}
|
|
3135
3402
|
project_description: ${String(project.project_description)}
|
|
3136
3403
|
project_name: ${String(project.project_name)}
|
|
3137
3404
|
ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
3138
3405
|
`;
|
|
3139
|
-
const path =
|
|
3406
|
+
const path = join11(ctx.repoRoot, ".copier-answers.yml");
|
|
3140
3407
|
if (safeReadText(path) !== text3) {
|
|
3141
3408
|
changedFiles.push(path);
|
|
3142
3409
|
if (!ctx.dryRun) writeText(path, text3);
|
|
@@ -3155,15 +3422,14 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3155
3422
|
id: "bmad.scaffold",
|
|
3156
3423
|
title: "BMAD modules/docs scaffold",
|
|
3157
3424
|
audit: (ctx) => {
|
|
3158
|
-
const
|
|
3159
|
-
const targetRoot = join10(ctx.repoRoot, "_bmad");
|
|
3425
|
+
const targetRoot = join11(ctx.repoRoot, "_bmad");
|
|
3160
3426
|
const sentinels = [
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3427
|
+
join11("core", "config.yaml"),
|
|
3428
|
+
join11("config.toml"),
|
|
3429
|
+
join11("_config", "manifest.yaml"),
|
|
3430
|
+
join11("bmm", "config.yaml")
|
|
3165
3431
|
];
|
|
3166
|
-
const missing = sentinels.filter((file) =>
|
|
3432
|
+
const missing = sentinels.filter((file) => !existsSync9(join11(targetRoot, file)));
|
|
3167
3433
|
return {
|
|
3168
3434
|
id: "bmad.scaffold",
|
|
3169
3435
|
title: "BMAD modules/docs scaffold",
|
|
@@ -3175,17 +3441,148 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3175
3441
|
},
|
|
3176
3442
|
migrate: (ctx, finding) => {
|
|
3177
3443
|
const changedFiles = [];
|
|
3178
|
-
|
|
3444
|
+
if (ctx.dryRun) {
|
|
3445
|
+
for (const detail of finding.details) {
|
|
3446
|
+
changedFiles.push(join11(ctx.repoRoot, detail));
|
|
3447
|
+
}
|
|
3448
|
+
return {
|
|
3449
|
+
id: finding.id,
|
|
3450
|
+
title: finding.title,
|
|
3451
|
+
status: changedFiles.length ? "applied" : "noop",
|
|
3452
|
+
summary: changedFiles.length ? "Would run non-interactive bmad-method install" : "No changes required",
|
|
3453
|
+
changedFiles,
|
|
3454
|
+
details: [
|
|
3455
|
+
`Would run: npx ${bmadInstallArgs(ctx.repoRoot).join(" ").replace(BMAD_INSTALL_TOOLS.join(","), "...")}`
|
|
3456
|
+
]
|
|
3457
|
+
};
|
|
3458
|
+
}
|
|
3459
|
+
const install = runBmadInstall(ctx.repoRoot);
|
|
3460
|
+
if (!install.ok) {
|
|
3461
|
+
return {
|
|
3462
|
+
id: finding.id,
|
|
3463
|
+
title: finding.title,
|
|
3464
|
+
status: "blocked",
|
|
3465
|
+
summary: `Failed to run bmad-method install`,
|
|
3466
|
+
changedFiles: [],
|
|
3467
|
+
details: [install.error ?? "Unknown error"]
|
|
3468
|
+
};
|
|
3469
|
+
}
|
|
3470
|
+
for (const detail of finding.details) {
|
|
3471
|
+
if (existsSync9(join11(ctx.repoRoot, detail))) {
|
|
3472
|
+
changedFiles.push(join11(ctx.repoRoot, detail));
|
|
3473
|
+
}
|
|
3474
|
+
}
|
|
3179
3475
|
return {
|
|
3180
3476
|
id: finding.id,
|
|
3181
3477
|
title: finding.title,
|
|
3182
3478
|
status: changedFiles.length ? "applied" : "noop",
|
|
3183
|
-
summary: changedFiles.length ? "
|
|
3479
|
+
summary: changedFiles.length ? "Installed BMAD scaffold via non-interactive installer" : "No changes required",
|
|
3184
3480
|
changedFiles,
|
|
3185
3481
|
details: []
|
|
3186
3482
|
};
|
|
3187
3483
|
}
|
|
3188
3484
|
},
|
|
3485
|
+
{
|
|
3486
|
+
id: "bmad.version",
|
|
3487
|
+
title: "BMAD version currency",
|
|
3488
|
+
audit: (ctx) => {
|
|
3489
|
+
const installed = readInstalledBmadVersion(ctx.repoRoot);
|
|
3490
|
+
if (!installed) {
|
|
3491
|
+
return {
|
|
3492
|
+
id: "bmad.version",
|
|
3493
|
+
title: "BMAD version currency",
|
|
3494
|
+
status: "skip",
|
|
3495
|
+
summary: existsSync9(join11(ctx.repoRoot, "_bmad")) ? "BMAD installed but version manifest unreadable" : "No BMAD install present",
|
|
3496
|
+
details: [],
|
|
3497
|
+
fixable: false
|
|
3498
|
+
};
|
|
3499
|
+
}
|
|
3500
|
+
const resolved = resolveBmadDistTags(ctx.homeDir);
|
|
3501
|
+
const available = resolved?.distTags?.[BMAD_TARGET_CHANNEL];
|
|
3502
|
+
if (!available) {
|
|
3503
|
+
return {
|
|
3504
|
+
id: "bmad.version",
|
|
3505
|
+
title: "BMAD version currency",
|
|
3506
|
+
status: "skip",
|
|
3507
|
+
summary: `BMAD ${installed} installed; latest ${BMAD_TARGET_CHANNEL} version unknown (npm unreachable)`,
|
|
3508
|
+
details: [`Could not resolve ${BMAD_NPM_PACKAGE}@${BMAD_TARGET_CHANNEL} from npm`],
|
|
3509
|
+
fixable: false
|
|
3510
|
+
};
|
|
3511
|
+
}
|
|
3512
|
+
const staleNote = resolved.stale ? ` ${glyph.dot} cached` : "";
|
|
3513
|
+
if (compareBmadVersions(installed, available) >= 0) {
|
|
3514
|
+
return {
|
|
3515
|
+
id: "bmad.version",
|
|
3516
|
+
title: "BMAD version currency",
|
|
3517
|
+
status: "pass",
|
|
3518
|
+
summary: `BMAD ${installed} is current (${BMAD_TARGET_CHANNEL} ${available})${staleNote}`,
|
|
3519
|
+
details: [],
|
|
3520
|
+
fixable: false
|
|
3521
|
+
};
|
|
3522
|
+
}
|
|
3523
|
+
return {
|
|
3524
|
+
id: "bmad.version",
|
|
3525
|
+
title: "BMAD version currency",
|
|
3526
|
+
status: "warn",
|
|
3527
|
+
summary: `BMAD ${installed} is behind ${BMAD_TARGET_CHANNEL} ${available} \u2014 upgrade available`,
|
|
3528
|
+
details: [
|
|
3529
|
+
`installed: ${installed}`,
|
|
3530
|
+
`available: ${available} (${BMAD_NPM_PACKAGE}@${BMAD_TARGET_CHANNEL})`,
|
|
3531
|
+
resolved.distTags.latest ? `stable latest: ${resolved.distTags.latest}` : "",
|
|
3532
|
+
"run `pj migrate bmad.version` to upgrade"
|
|
3533
|
+
].filter(Boolean),
|
|
3534
|
+
fixable: true
|
|
3535
|
+
};
|
|
3536
|
+
},
|
|
3537
|
+
migrate: (ctx, finding) => {
|
|
3538
|
+
if (finding.status !== "warn") {
|
|
3539
|
+
return {
|
|
3540
|
+
id: finding.id,
|
|
3541
|
+
title: finding.title,
|
|
3542
|
+
status: "noop",
|
|
3543
|
+
summary: finding.status === "skip" ? finding.summary : "BMAD already current",
|
|
3544
|
+
changedFiles: [],
|
|
3545
|
+
details: []
|
|
3546
|
+
};
|
|
3547
|
+
}
|
|
3548
|
+
const installed = readInstalledBmadVersion(ctx.repoRoot);
|
|
3549
|
+
const available = resolveBmadDistTags(ctx.homeDir)?.distTags?.[BMAD_TARGET_CHANNEL];
|
|
3550
|
+
const manifestPath = join11(ctx.repoRoot, "_bmad", "_config", "manifest.yaml");
|
|
3551
|
+
if (ctx.dryRun) {
|
|
3552
|
+
return {
|
|
3553
|
+
id: finding.id,
|
|
3554
|
+
title: finding.title,
|
|
3555
|
+
status: "applied",
|
|
3556
|
+
summary: `Would upgrade BMAD ${installed ?? "?"} -> ${available ?? BMAD_TARGET_CHANNEL}`,
|
|
3557
|
+
changedFiles: [manifestPath],
|
|
3558
|
+
details: [
|
|
3559
|
+
`Would run: npx ${bmadInstallArgs(ctx.repoRoot).join(" ").replace(BMAD_INSTALL_TOOLS.join(","), "...")}`
|
|
3560
|
+
]
|
|
3561
|
+
};
|
|
3562
|
+
}
|
|
3563
|
+
const install = runBmadInstall(ctx.repoRoot);
|
|
3564
|
+
if (!install.ok) {
|
|
3565
|
+
return {
|
|
3566
|
+
id: finding.id,
|
|
3567
|
+
title: finding.title,
|
|
3568
|
+
status: "blocked",
|
|
3569
|
+
summary: "Failed to upgrade BMAD via installer",
|
|
3570
|
+
changedFiles: [],
|
|
3571
|
+
details: [install.error ?? "Unknown error"]
|
|
3572
|
+
};
|
|
3573
|
+
}
|
|
3574
|
+
const nowInstalled = readInstalledBmadVersion(ctx.repoRoot);
|
|
3575
|
+
const upgraded = Boolean(nowInstalled && installed && compareBmadVersions(nowInstalled, installed) > 0);
|
|
3576
|
+
return {
|
|
3577
|
+
id: finding.id,
|
|
3578
|
+
title: finding.title,
|
|
3579
|
+
status: upgraded ? "applied" : "noop",
|
|
3580
|
+
summary: upgraded ? `Upgraded BMAD ${installed} -> ${nowInstalled}` : `BMAD reinstalled (${nowInstalled ?? "?"})`,
|
|
3581
|
+
changedFiles: upgraded ? [manifestPath] : [],
|
|
3582
|
+
details: []
|
|
3583
|
+
};
|
|
3584
|
+
}
|
|
3585
|
+
},
|
|
3189
3586
|
{
|
|
3190
3587
|
id: "hermes.pm-scaffold",
|
|
3191
3588
|
title: "Hermes PM scaffold parity",
|
|
@@ -3197,11 +3594,11 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3197
3594
|
}
|
|
3198
3595
|
const details = [];
|
|
3199
3596
|
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 (!
|
|
3597
|
+
if (!existsSync9(join11(role.roleDir, rel))) details.push(`missing ${relative(ctx.repoRoot, join11(role.roleDir, rel))}`);
|
|
3201
3598
|
}
|
|
3202
|
-
const gitmodules = safeReadText(
|
|
3599
|
+
const gitmodules = safeReadText(join11(ctx.repoRoot, ".gitmodules")) ?? "";
|
|
3203
3600
|
if (!gitmodules.includes(`agents/hermes/${role.role}/runtime`)) details.push(".gitmodules missing pm runtime submodule entry");
|
|
3204
|
-
if (!profileMetaInheritsDefault(
|
|
3601
|
+
if (!profileMetaInheritsDefault(join11(role.roleDir, "runtime", "profile.yaml"))) {
|
|
3205
3602
|
details.push("runtime/profile.yaml missing inherited default config metadata");
|
|
3206
3603
|
}
|
|
3207
3604
|
const registry = safeReadText(registryPath(ctx.homeDir));
|
|
@@ -3222,21 +3619,21 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3222
3619
|
if (!role) {
|
|
3223
3620
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "No pm role present", changedFiles, details: [] };
|
|
3224
3621
|
}
|
|
3225
|
-
const templateRoleDir =
|
|
3226
|
-
writeIfDifferent(
|
|
3227
|
-
writeIfDifferent(
|
|
3228
|
-
writeIfDifferent(
|
|
3229
|
-
copyMissingRecursive(
|
|
3230
|
-
copyMissingRecursive(
|
|
3231
|
-
copyMissingRecursive(
|
|
3232
|
-
const promptSource =
|
|
3233
|
-
const promptTarget =
|
|
3234
|
-
if (
|
|
3622
|
+
const templateRoleDir = join11(ctx.pjanglerRoot, "templates", "hermes-agent", "template");
|
|
3623
|
+
writeIfDifferent(join11(role.roleDir, "SOUL.md"), renderSoul(role), ctx.dryRun, changedFiles);
|
|
3624
|
+
writeIfDifferent(join11(role.roleDir, "hermes"), renderHermesWrapper(role), ctx.dryRun, changedFiles, 493);
|
|
3625
|
+
writeIfDifferent(join11(role.roleDir, ".gitignore"), readText(join11(templateRoleDir, ".gitignore.jinja")).replace(/\{\{ role \}\}/g, role.role), ctx.dryRun, changedFiles);
|
|
3626
|
+
copyMissingRecursive(join11(templateRoleDir, ".runtime-scaffold"), join11(role.roleDir, ".runtime-scaffold"), changedFiles, ctx.dryRun);
|
|
3627
|
+
copyMissingRecursive(join11(templateRoleDir, ".runtime-scaffold"), join11(role.roleDir, "runtime"), changedFiles, ctx.dryRun);
|
|
3628
|
+
copyMissingRecursive(join11(templateRoleDir, ".scripts"), join11(role.roleDir, ".scripts"), changedFiles, ctx.dryRun, (source) => source.endsWith("sentinel.prompt.md.jinja"));
|
|
3629
|
+
const promptSource = join11(templateRoleDir, ".scripts", "sentinel.prompt.md.jinja");
|
|
3630
|
+
const promptTarget = join11(role.roleDir, ".scripts", "sentinel.prompt.md");
|
|
3631
|
+
if (existsSync9(promptSource) && !existsSync9(promptTarget)) {
|
|
3235
3632
|
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
3633
|
writeIfDifferent(promptTarget, prompt, ctx.dryRun, changedFiles);
|
|
3237
3634
|
}
|
|
3238
3635
|
upsertSubmodule(ctx.repoRoot, role, changedFiles, ctx.dryRun);
|
|
3239
|
-
const profileMetaUpdated = upsertInheritedProfileMeta(
|
|
3636
|
+
const profileMetaUpdated = upsertInheritedProfileMeta(join11(role.roleDir, "runtime", "profile.yaml"), changedFiles, ctx.dryRun);
|
|
3240
3637
|
if (profileMetaUpdated) details.push(`updated ${profileMetaUpdated}`);
|
|
3241
3638
|
const registryUpdated = upsertRegistryEntry(role, ctx.homeDir, changedFiles, ctx.dryRun);
|
|
3242
3639
|
if (registryUpdated) details.push(`updated ${registryUpdated}`);
|
|
@@ -3250,6 +3647,103 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3250
3647
|
};
|
|
3251
3648
|
}
|
|
3252
3649
|
},
|
|
3650
|
+
{
|
|
3651
|
+
id: "hermes.untracked-runtimes",
|
|
3652
|
+
title: "Hermes agent runtimes untracked + gitignored",
|
|
3653
|
+
audit: (ctx) => {
|
|
3654
|
+
const roles = discoverRoles(ctx.repoRoot);
|
|
3655
|
+
if (roles.length === 0) {
|
|
3656
|
+
return {
|
|
3657
|
+
id: "hermes.untracked-runtimes",
|
|
3658
|
+
title: "Hermes agent runtimes untracked + gitignored",
|
|
3659
|
+
status: "skip",
|
|
3660
|
+
summary: "No Hermes roles present",
|
|
3661
|
+
details: [],
|
|
3662
|
+
fixable: false
|
|
3663
|
+
};
|
|
3664
|
+
}
|
|
3665
|
+
const details = [];
|
|
3666
|
+
for (const role of roles) {
|
|
3667
|
+
const roleRelDir = relative(ctx.repoRoot, role.roleDir);
|
|
3668
|
+
const runtimeRelPath = join11(roleRelDir, "runtime");
|
|
3669
|
+
const lsResult = spawnSync6("git", ["ls-files", "--stage", runtimeRelPath], {
|
|
3670
|
+
cwd: ctx.repoRoot,
|
|
3671
|
+
encoding: "utf8"
|
|
3672
|
+
});
|
|
3673
|
+
if (lsResult.status === 0 && lsResult.stdout.trim().length > 0) {
|
|
3674
|
+
details.push(`submodule runtime is tracked in Git index at ${runtimeRelPath}`);
|
|
3675
|
+
}
|
|
3676
|
+
const gitignorePath = join11(role.roleDir, ".gitignore");
|
|
3677
|
+
if (existsSync9(gitignorePath)) {
|
|
3678
|
+
const content = safeReadText(gitignorePath) ?? "";
|
|
3679
|
+
const lines = content.split(/\r?\n/).map((line) => line.trim());
|
|
3680
|
+
if (!lines.includes("runtime/") && !lines.includes("runtime")) {
|
|
3681
|
+
details.push(`.gitignore missing runtime/ ignore entry in ${relative(ctx.repoRoot, gitignorePath)}`);
|
|
3682
|
+
}
|
|
3683
|
+
} else {
|
|
3684
|
+
details.push(`.gitignore is missing in ${relative(ctx.repoRoot, gitignorePath)}`);
|
|
3685
|
+
}
|
|
3686
|
+
}
|
|
3687
|
+
return {
|
|
3688
|
+
id: "hermes.untracked-runtimes",
|
|
3689
|
+
title: "Hermes agent runtimes untracked + gitignored",
|
|
3690
|
+
status: details.length === 0 ? "pass" : "fail",
|
|
3691
|
+
summary: details.length === 0 ? "All Hermes agent runtimes are untracked and gitignored" : `${details.length} issue(s) with untracked/ignored runtimes detected`,
|
|
3692
|
+
details,
|
|
3693
|
+
fixable: true
|
|
3694
|
+
};
|
|
3695
|
+
},
|
|
3696
|
+
migrate: (ctx, finding) => {
|
|
3697
|
+
const roles = discoverRoles(ctx.repoRoot);
|
|
3698
|
+
const changedFiles = [];
|
|
3699
|
+
const details = [];
|
|
3700
|
+
for (const role of roles) {
|
|
3701
|
+
const roleRelDir = relative(ctx.repoRoot, role.roleDir);
|
|
3702
|
+
const runtimeRelPath = join11(roleRelDir, "runtime");
|
|
3703
|
+
const lsResult = spawnSync6("git", ["ls-files", "--stage", runtimeRelPath], {
|
|
3704
|
+
cwd: ctx.repoRoot,
|
|
3705
|
+
encoding: "utf8"
|
|
3706
|
+
});
|
|
3707
|
+
if (lsResult.status === 0 && lsResult.stdout.trim().length > 0) {
|
|
3708
|
+
details.push(`untrack ${runtimeRelPath}`);
|
|
3709
|
+
changedFiles.push(runtimeRelPath);
|
|
3710
|
+
if (!ctx.dryRun) {
|
|
3711
|
+
spawnSync6("git", ["rm", "--cached", "-r", runtimeRelPath], {
|
|
3712
|
+
cwd: ctx.repoRoot,
|
|
3713
|
+
encoding: "utf8"
|
|
3714
|
+
});
|
|
3715
|
+
}
|
|
3716
|
+
}
|
|
3717
|
+
const gitignorePath = join11(role.roleDir, ".gitignore");
|
|
3718
|
+
let content = "";
|
|
3719
|
+
let isIgnored = false;
|
|
3720
|
+
if (existsSync9(gitignorePath)) {
|
|
3721
|
+
content = safeReadText(gitignorePath) ?? "";
|
|
3722
|
+
const lines = content.split(/\r?\n/).map((line) => line.trim());
|
|
3723
|
+
isIgnored = lines.includes("runtime/") || lines.includes("runtime");
|
|
3724
|
+
}
|
|
3725
|
+
if (!isIgnored) {
|
|
3726
|
+
details.push(`ignore runtime/ in ${relative(ctx.repoRoot, gitignorePath)}`);
|
|
3727
|
+
changedFiles.push(gitignorePath);
|
|
3728
|
+
if (!ctx.dryRun) {
|
|
3729
|
+
if (content && !content.endsWith("\n")) {
|
|
3730
|
+
content += "\n";
|
|
3731
|
+
}
|
|
3732
|
+
content += "runtime/\n";
|
|
3733
|
+
writeText(gitignorePath, content);
|
|
3734
|
+
}
|
|
3735
|
+
}
|
|
3736
|
+
}
|
|
3737
|
+
return {
|
|
3738
|
+
id: finding.id,
|
|
3739
|
+
title: finding.title,
|
|
3740
|
+
status: changedFiles.length ? "applied" : "noop",
|
|
3741
|
+
summary: changedFiles.length ? "Hermes agent runtimes made untracked and ignored" : "No changes required",
|
|
3742
|
+
changedFiles,
|
|
3743
|
+
details
|
|
3744
|
+
};
|
|
3745
|
+
}
|
|
3746
|
+
},
|
|
3253
3747
|
{
|
|
3254
3748
|
id: "systemd.sentinel",
|
|
3255
3749
|
title: "Hermes systemd/sentinel units enabled + active",
|
|
@@ -3290,9 +3784,9 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3290
3784
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "systemd --user unavailable on this host", changedFiles, details };
|
|
3291
3785
|
}
|
|
3292
3786
|
for (const role of roles) {
|
|
3293
|
-
const sysDir =
|
|
3787
|
+
const sysDir = join11(ctx.homeDir, ".config", "systemd", "user");
|
|
3294
3788
|
const units = [`hermes-${role.agentId}-gateway.service`, `hermes-${role.agentId}-consumer.service`, `hermes-${role.agentId}-heartbeat.timer`];
|
|
3295
|
-
const allUnitsPresent = units.every((unit) =>
|
|
3789
|
+
const allUnitsPresent = units.every((unit) => existsSync9(join11(sysDir, unit)));
|
|
3296
3790
|
if (allUnitsPresent) {
|
|
3297
3791
|
if (ctx.dryRun) {
|
|
3298
3792
|
details.push(`would run: systemctl --user enable --now ${units.join(" ")}`);
|
|
@@ -3304,12 +3798,12 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3304
3798
|
}
|
|
3305
3799
|
continue;
|
|
3306
3800
|
}
|
|
3307
|
-
for (const script of [
|
|
3308
|
-
if (!script || !
|
|
3801
|
+
for (const script of [join11(role.roleDir, ".scripts", "70-systemd.sh")]) {
|
|
3802
|
+
if (!script || !existsSync9(script)) continue;
|
|
3309
3803
|
if (ctx.dryRun) {
|
|
3310
3804
|
details.push(`would run: bash ${script}`);
|
|
3311
3805
|
} else {
|
|
3312
|
-
const result =
|
|
3806
|
+
const result = spawnSync6("bash", [script], { cwd: role.roleDir, encoding: "utf8" });
|
|
3313
3807
|
if (result.status !== 0) details.push(`script failed: ${script}: ${result.stderr.trim() || result.stdout.trim()}`);
|
|
3314
3808
|
}
|
|
3315
3809
|
}
|
|
@@ -3444,15 +3938,15 @@ function formatMigrationReport(report) {
|
|
|
3444
3938
|
}
|
|
3445
3939
|
|
|
3446
3940
|
// src/utils/version.ts
|
|
3447
|
-
import { readFileSync as
|
|
3448
|
-
import { dirname as dirname7, join as
|
|
3941
|
+
import { readFileSync as readFileSync6 } from "node:fs";
|
|
3942
|
+
import { dirname as dirname7, join as join12 } from "node:path";
|
|
3449
3943
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
3450
3944
|
var PJANGLER_VERSION = (() => {
|
|
3451
3945
|
try {
|
|
3452
3946
|
let dir = dirname7(fileURLToPath4(import.meta.url));
|
|
3453
3947
|
for (let i = 0; i < 4; i++) {
|
|
3454
3948
|
try {
|
|
3455
|
-
const raw =
|
|
3949
|
+
const raw = readFileSync6(join12(dir, "package.json"), "utf8");
|
|
3456
3950
|
return JSON.parse(raw).version ?? "0.0.0";
|
|
3457
3951
|
} catch {
|
|
3458
3952
|
const parent = dirname7(dir);
|
|
@@ -3495,16 +3989,16 @@ async function promptForRuleIds(rules) {
|
|
|
3495
3989
|
return selected;
|
|
3496
3990
|
}
|
|
3497
3991
|
function readJson(path) {
|
|
3498
|
-
if (!
|
|
3992
|
+
if (!existsSync10(path)) return void 0;
|
|
3499
3993
|
try {
|
|
3500
|
-
const parsed = JSON.parse(
|
|
3994
|
+
const parsed = JSON.parse(readFileSync7(path, "utf8"));
|
|
3501
3995
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
|
|
3502
3996
|
} catch {
|
|
3503
3997
|
return void 0;
|
|
3504
3998
|
}
|
|
3505
3999
|
}
|
|
3506
4000
|
function findGitRoot(cwd) {
|
|
3507
|
-
const result =
|
|
4001
|
+
const result = spawnSync7("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf8" });
|
|
3508
4002
|
if (result.status !== 0) return void 0;
|
|
3509
4003
|
return resolve3(result.stdout.trim());
|
|
3510
4004
|
}
|
|
@@ -3514,8 +4008,8 @@ function packageNameToProjectName(value) {
|
|
|
3514
4008
|
return name.replace(/[-_]+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase()).trim();
|
|
3515
4009
|
}
|
|
3516
4010
|
function deriveProjectDefaults(targetDir) {
|
|
3517
|
-
const manifest = readJson(
|
|
3518
|
-
const pkg = readJson(
|
|
4011
|
+
const manifest = readJson(join13(targetDir, ".project.json"));
|
|
4012
|
+
const pkg = readJson(join13(targetDir, "package.json"));
|
|
3519
4013
|
const name = String(manifest?.project_name ?? "").trim() || packageNameToProjectName(typeof pkg?.name === "string" ? pkg.name : void 0) || packageNameToProjectName(basename4(targetDir)) || "Project";
|
|
3520
4014
|
const ticketProvider = manifest?.ticket_provider && typeof manifest.ticket_provider === "object" ? manifest.ticket_provider : {};
|
|
3521
4015
|
return {
|
|
@@ -3571,7 +4065,7 @@ function actionNeedsRun(plan, kind, syncMode) {
|
|
|
3571
4065
|
if (!action || action.kind !== "project.write-manifest") return false;
|
|
3572
4066
|
const next = `${JSON.stringify(action.manifest, null, 2)}
|
|
3573
4067
|
`;
|
|
3574
|
-
return !
|
|
4068
|
+
return !existsSync10(action.path) || readFileSync7(action.path, "utf8") !== next;
|
|
3575
4069
|
}
|
|
3576
4070
|
if (kind === "copier.copy.commonproject") return true;
|
|
3577
4071
|
if (kind === "ticket-provider.create-or-link") return plan.actions.some((action) => action.kind === kind && action.enabled);
|
|
@@ -3626,7 +4120,7 @@ async function resolveProjectInitTarget(name, options) {
|
|
|
3626
4120
|
if (!targetDir && interactive) {
|
|
3627
4121
|
const defaultName = name ?? basename4(cwd);
|
|
3628
4122
|
const promptedName = name ?? await promptTextValue("Project name", packageNameToProjectName(defaultName));
|
|
3629
|
-
const defaultDir =
|
|
4123
|
+
const defaultDir = join13(cwd, promptedName.replace(/[^A-Za-z0-9._-]/g, "") || promptedName.toLowerCase().replace(/[^a-z0-9]+/g, "-"));
|
|
3630
4124
|
targetDir = await promptTextValue("Project directory", defaultDir);
|
|
3631
4125
|
name = promptedName;
|
|
3632
4126
|
}
|
|
@@ -3634,7 +4128,7 @@ async function resolveProjectInitTarget(name, options) {
|
|
|
3634
4128
|
if (!name) throw new Error("Project name or --target-dir is required when project init is not run inside a git repo");
|
|
3635
4129
|
targetDir = resolve3(process.cwd(), name.replace(/[^A-Za-z0-9._-]/g, "") || name.toLowerCase().replace(/[^a-z0-9]+/g, "-"));
|
|
3636
4130
|
}
|
|
3637
|
-
const targetExists =
|
|
4131
|
+
const targetExists = existsSync10(targetDir);
|
|
3638
4132
|
if (targetExists && !statSync2(targetDir).isDirectory()) throw new Error(`Target path is not a directory: ${targetDir}`);
|
|
3639
4133
|
const targetGitRoot = targetExists ? findGitRoot(targetDir) : void 0;
|
|
3640
4134
|
const syncMode = Boolean(targetGitRoot && resolve3(targetGitRoot) === resolve3(targetDir));
|