@delorenj/pjangler 1.2.2 → 1.2.3
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
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
5
5
|
import { existsSync as existsSync9, readFileSync as readFileSync6, statSync as statSync2 } from "node:fs";
|
|
6
|
-
import { basename as basename4, join as
|
|
6
|
+
import { basename as basename4, join as join12, resolve as resolve3 } from "node:path";
|
|
7
7
|
import { Command as Command3 } from "commander";
|
|
8
8
|
|
|
9
9
|
// src/commands/hermes/types.ts
|
|
@@ -159,6 +159,78 @@ var EnsureTemplateConfig = class extends Command {
|
|
|
159
159
|
}
|
|
160
160
|
};
|
|
161
161
|
|
|
162
|
+
// src/utils/style.ts
|
|
163
|
+
var env = process.env;
|
|
164
|
+
function detectColor() {
|
|
165
|
+
if ("NO_COLOR" in env && env.NO_COLOR !== "") return false;
|
|
166
|
+
const force = env.FORCE_COLOR;
|
|
167
|
+
if (force === "0" || force === "false") return false;
|
|
168
|
+
if (force !== void 0 && force !== "") return true;
|
|
169
|
+
if (env.TERM === "dumb") return false;
|
|
170
|
+
return Boolean(process.stdout.isTTY);
|
|
171
|
+
}
|
|
172
|
+
var colorEnabled = detectColor();
|
|
173
|
+
function sgr(open, close) {
|
|
174
|
+
const prefix = `\x1B[${open}m`;
|
|
175
|
+
const suffix = `\x1B[${close}m`;
|
|
176
|
+
return (value) => colorEnabled ? `${prefix}${value}${suffix}` : String(value);
|
|
177
|
+
}
|
|
178
|
+
var bold = sgr(1, 22);
|
|
179
|
+
var dim = sgr(2, 22);
|
|
180
|
+
var italic = sgr(3, 23);
|
|
181
|
+
var underline = sgr(4, 24);
|
|
182
|
+
var red = sgr(31, 39);
|
|
183
|
+
var green = sgr(32, 39);
|
|
184
|
+
var yellow = sgr(33, 39);
|
|
185
|
+
var blue = sgr(34, 39);
|
|
186
|
+
var magenta = sgr(35, 39);
|
|
187
|
+
var cyan = sgr(36, 39);
|
|
188
|
+
var gray = sgr(90, 39);
|
|
189
|
+
var glyph = {
|
|
190
|
+
pass: "\u2714",
|
|
191
|
+
fail: "\u2716",
|
|
192
|
+
warn: "\u26A0",
|
|
193
|
+
skip: "\u25CB",
|
|
194
|
+
info: "\u2139",
|
|
195
|
+
arrow: "\u21B3",
|
|
196
|
+
bullet: "\u2022",
|
|
197
|
+
dot: "\xB7",
|
|
198
|
+
add: "+",
|
|
199
|
+
chevron: "\u25B8",
|
|
200
|
+
pointer: "\u276F"
|
|
201
|
+
};
|
|
202
|
+
var STATUS_STYLES = {
|
|
203
|
+
pass: { glyph: glyph.pass, color: green, label: "pass" },
|
|
204
|
+
fail: { glyph: glyph.fail, color: red, label: "fail" },
|
|
205
|
+
warn: { glyph: glyph.warn, color: yellow, label: "warn" },
|
|
206
|
+
skip: { glyph: glyph.skip, color: gray, label: "skip" },
|
|
207
|
+
applied: { glyph: glyph.pass, color: green, label: "applied" },
|
|
208
|
+
noop: { glyph: glyph.skip, color: gray, label: "noop" },
|
|
209
|
+
blocked: { glyph: glyph.fail, color: red, label: "blocked" },
|
|
210
|
+
skipped: { glyph: glyph.skip, color: gray, label: "skipped" }
|
|
211
|
+
};
|
|
212
|
+
function statusStyle(status) {
|
|
213
|
+
return STATUS_STYLES[status] ?? { glyph: glyph.dot, color: dim, label: status };
|
|
214
|
+
}
|
|
215
|
+
function projectStatusColor(status) {
|
|
216
|
+
switch (status) {
|
|
217
|
+
case "active":
|
|
218
|
+
return green;
|
|
219
|
+
case "planned":
|
|
220
|
+
return yellow;
|
|
221
|
+
case "archived":
|
|
222
|
+
return gray;
|
|
223
|
+
default:
|
|
224
|
+
return cyan;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
function heading(title, marker = glyph.chevron) {
|
|
228
|
+
return `${cyan(bold(marker))} ${bold(title)}`;
|
|
229
|
+
}
|
|
230
|
+
function joinDot(fragments) {
|
|
231
|
+
return fragments.join(dim(` ${glyph.dot} `));
|
|
232
|
+
}
|
|
233
|
+
|
|
162
234
|
// src/recipes/Recipe.ts
|
|
163
235
|
var Recipe = class {
|
|
164
236
|
context;
|
|
@@ -171,26 +243,22 @@ var Recipe = class {
|
|
|
171
243
|
return this;
|
|
172
244
|
}
|
|
173
245
|
async execute() {
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}
|
|
246
|
+
const subsystem = this.constructor.name.replace("Recipe", "").toLowerCase();
|
|
247
|
+
const dryRun = this.context.dryRun;
|
|
248
|
+
console.log("");
|
|
249
|
+
console.log(` ${cyan(bold(glyph.chevron))} ${bold(`Initializing ${subsystem} subsystem`)}${dryRun ? ` ${dim(glyph.dot)} ${yellow("dry run")}` : ""}`);
|
|
250
|
+
console.log("");
|
|
180
251
|
for (const command of this.ingredients) {
|
|
181
252
|
const result = await command.invoke();
|
|
182
|
-
|
|
183
|
-
console.log(result.message);
|
|
184
|
-
} else {
|
|
185
|
-
console.log(result.message);
|
|
186
|
-
}
|
|
253
|
+
console.log(result.message.split("\n").map((line) => line ? ` ${line}` : line).join("\n"));
|
|
187
254
|
}
|
|
188
|
-
if (!
|
|
255
|
+
if (!dryRun) {
|
|
189
256
|
this.printNextSteps();
|
|
190
257
|
} else {
|
|
191
258
|
console.log("");
|
|
192
|
-
console.log("
|
|
193
|
-
console.log("
|
|
259
|
+
console.log(` ${green(glyph.pass)} ${dim("Dry-run complete \u2014 no files were modified.")}`);
|
|
260
|
+
console.log(` ${dim("Remove --dry-run to apply changes.")}`);
|
|
261
|
+
console.log("");
|
|
194
262
|
}
|
|
195
263
|
}
|
|
196
264
|
};
|
|
@@ -333,11 +401,111 @@ if __name__ == "__main__":
|
|
|
333
401
|
}
|
|
334
402
|
};
|
|
335
403
|
|
|
404
|
+
// src/commands/AddMiseCodegraphScript.ts
|
|
405
|
+
import { chmodSync } from "fs";
|
|
406
|
+
import { join as join3 } from "path";
|
|
407
|
+
var AddMiseCodegraphScript = class extends Command {
|
|
408
|
+
async invoke() {
|
|
409
|
+
const filePath = ".mise/scripts/codegraph.sh";
|
|
410
|
+
if (this.fileExists(filePath) && !this.context.force) {
|
|
411
|
+
return {
|
|
412
|
+
success: false,
|
|
413
|
+
message: this.formatMessage("\u26A0\uFE0F .mise/scripts/codegraph.sh already exists"),
|
|
414
|
+
filePath
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
const content = `#!/usr/bin/env bash
|
|
418
|
+
# Mise enter hook: ensure the CodeGraph CLI is available and initialize the
|
|
419
|
+
# project index. If \`codegraph\` is not installed, this script installs it
|
|
420
|
+
# non-interactively into the project-local .mise/bin directory and retries.
|
|
421
|
+
#
|
|
422
|
+
# This is intended to run from a mise enter hook so onboarding a new host is
|
|
423
|
+
# fully automatic.
|
|
424
|
+
|
|
425
|
+
set -euo pipefail
|
|
426
|
+
|
|
427
|
+
REPO_ROOT="\${MISE_PROJECT_ROOT:-$(cd "$(dirname "$0")/../.." && pwd)}"
|
|
428
|
+
PROJECT_BIN_DIR="$REPO_ROOT/.mise/bin"
|
|
429
|
+
mkdir -p "$PROJECT_BIN_DIR"
|
|
430
|
+
|
|
431
|
+
# Install the CodeGraph CLI into the project-local bin directory.
|
|
432
|
+
install_codegraph() {
|
|
433
|
+
echo "[mise] codegraph not found. Installing non-interactively..."
|
|
434
|
+
export CODEGRAPH_BIN_DIR="$PROJECT_BIN_DIR"
|
|
435
|
+
curl -fsSL https://raw.githubusercontent.com/colbymchenry/codegraph/main/install.sh | sh
|
|
436
|
+
|
|
437
|
+
if [ -x "$PROJECT_BIN_DIR/codegraph" ]; then
|
|
438
|
+
export PATH="$PROJECT_BIN_DIR:$PATH"
|
|
439
|
+
else
|
|
440
|
+
echo "[mise] codegraph install did not place a binary at $PROJECT_BIN_DIR/codegraph" >&2
|
|
441
|
+
return 1
|
|
442
|
+
fi
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
# Ensure a codegraph binary is available on PATH.
|
|
446
|
+
ensure_codegraph() {
|
|
447
|
+
if command -v codegraph >/dev/null 2>&1; then
|
|
448
|
+
return 0
|
|
449
|
+
fi
|
|
450
|
+
|
|
451
|
+
# Check the project-local bin dir first (previous install from this hook).
|
|
452
|
+
if [ -x "$PROJECT_BIN_DIR/codegraph" ]; then
|
|
453
|
+
export PATH="$PROJECT_BIN_DIR:$PATH"
|
|
454
|
+
return 0
|
|
455
|
+
fi
|
|
456
|
+
|
|
457
|
+
# Check typical user-level install locations before fetching anything.
|
|
458
|
+
for d in "$HOME/.local/bin" "$HOME/.codegraph/current/bin"; do
|
|
459
|
+
if [ -x "$d/codegraph" ]; then
|
|
460
|
+
export PATH="$d:$PATH"
|
|
461
|
+
return 0
|
|
462
|
+
fi
|
|
463
|
+
done
|
|
464
|
+
|
|
465
|
+
install_codegraph
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
# Attempt to initialize the project graph. If the command is missing, install
|
|
469
|
+
# it and retry once.
|
|
470
|
+
init_project() {
|
|
471
|
+
local err_file
|
|
472
|
+
err_file="$(mktemp)"
|
|
473
|
+
trap 'rm -f "$err_file"' RETURN
|
|
474
|
+
|
|
475
|
+
if codegraph init -i "$REPO_ROOT" 2>"$err_file"; then
|
|
476
|
+
return 0
|
|
477
|
+
fi
|
|
478
|
+
|
|
479
|
+
# If the failure looks like a missing binary, install and retry.
|
|
480
|
+
if grep -qiE 'command not found|not installed|No such file|executable file not found' "$err_file" 2>/dev/null; then
|
|
481
|
+
ensure_codegraph
|
|
482
|
+
codegraph init -i "$REPO_ROOT"
|
|
483
|
+
return 0
|
|
484
|
+
fi
|
|
485
|
+
|
|
486
|
+
cat "$err_file" >&2
|
|
487
|
+
return 1
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
init_project
|
|
491
|
+
`;
|
|
492
|
+
this.writeFile(filePath, content);
|
|
493
|
+
if (!this.context.dryRun) {
|
|
494
|
+
chmodSync(join3(this.context.targetDir, filePath), 493);
|
|
495
|
+
}
|
|
496
|
+
return {
|
|
497
|
+
success: true,
|
|
498
|
+
message: this.formatMessage(this.context.dryRun ? "Would create .mise/scripts/codegraph.sh" : "\u2705 Created .mise/scripts/codegraph.sh"),
|
|
499
|
+
filePath
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
|
|
336
504
|
// src/recipes/MiseRecipe.ts
|
|
337
505
|
var MiseRecipe = class extends Recipe {
|
|
338
506
|
constructor(context) {
|
|
339
507
|
super(context);
|
|
340
|
-
this.addIngredient(AddMiseToml).addIngredient(AddDotenv).addIngredient(AddMiseTasksStructure).addIngredient(AddMiseBaseToml).addIngredient(AddMiseBaseScript);
|
|
508
|
+
this.addIngredient(AddMiseToml).addIngredient(AddDotenv).addIngredient(AddMiseTasksStructure).addIngredient(AddMiseBaseToml).addIngredient(AddMiseBaseScript).addIngredient(AddMiseCodegraphScript);
|
|
341
509
|
}
|
|
342
510
|
printNextSteps() {
|
|
343
511
|
console.log("\u{1F389} Mise subsystem initialized successfully!");
|
|
@@ -565,12 +733,12 @@ var NodeRecipe = class extends Recipe {
|
|
|
565
733
|
};
|
|
566
734
|
|
|
567
735
|
// src/commands/hermes/PromptForAgentConfig.ts
|
|
568
|
-
import { basename, join as
|
|
736
|
+
import { basename, join as join4 } from "node:path";
|
|
569
737
|
import { readFileSync } from "node:fs";
|
|
570
738
|
import * as p from "@clack/prompts";
|
|
571
739
|
function detectTicketProvider(targetDir) {
|
|
572
740
|
try {
|
|
573
|
-
const t = JSON.parse(readFileSync(
|
|
741
|
+
const t = JSON.parse(readFileSync(join4(targetDir, ".project.json"), "utf8"))?.ticket_provider?.type;
|
|
574
742
|
return t === "plane" || t === "linear" || t === "trello" ? t : void 0;
|
|
575
743
|
} catch {
|
|
576
744
|
return void 0;
|
|
@@ -706,7 +874,7 @@ var PromptForAgentConfig = class extends Command {
|
|
|
706
874
|
// src/commands/hermes/RunCopierTemplate.ts
|
|
707
875
|
import { spawnSync } from "node:child_process";
|
|
708
876
|
import { homedir as homedir2 } from "node:os";
|
|
709
|
-
import { join as
|
|
877
|
+
import { join as join5, dirname as dirname3 } from "node:path";
|
|
710
878
|
import { existsSync as existsSync3, mkdirSync as mkdirSync3 } from "node:fs";
|
|
711
879
|
import { fileURLToPath } from "node:url";
|
|
712
880
|
import * as p2 from "@clack/prompts";
|
|
@@ -718,8 +886,8 @@ function resolveVendoredTemplate(name) {
|
|
|
718
886
|
return void 0;
|
|
719
887
|
}
|
|
720
888
|
for (let i = 0; i < 8; i++) {
|
|
721
|
-
const candidate =
|
|
722
|
-
if (existsSync3(
|
|
889
|
+
const candidate = join5(dir, "templates", name);
|
|
890
|
+
if (existsSync3(join5(candidate, "copier.yml"))) return candidate;
|
|
723
891
|
const parent = dirname3(dir);
|
|
724
892
|
if (parent === dir) break;
|
|
725
893
|
dir = parent;
|
|
@@ -738,7 +906,7 @@ var RunCopierTemplate = class extends Command {
|
|
|
738
906
|
message: "PromptForAgentConfig must run before RunCopierTemplate (targetRepo/role unset)"
|
|
739
907
|
};
|
|
740
908
|
}
|
|
741
|
-
const roleDir =
|
|
909
|
+
const roleDir = join5(ctx.targetDir, "agents", "hermes", role);
|
|
742
910
|
ctx.roleDir = roleDir;
|
|
743
911
|
ctx.runtimeRepo = `delorenj/agent-hm-${targetRepo}-${role}`;
|
|
744
912
|
const which = spawnSync("which", ["copier"], { encoding: "utf8" });
|
|
@@ -748,7 +916,7 @@ var RunCopierTemplate = class extends Command {
|
|
|
748
916
|
message: "\u2717 copier not found on PATH. Install with: `uv tool install copier` or `pip install copier`"
|
|
749
917
|
};
|
|
750
918
|
}
|
|
751
|
-
if (existsSync3(
|
|
919
|
+
if (existsSync3(join5(roleDir, "role.yaml")) && !ctx.force) {
|
|
752
920
|
if (ctx.yes) {
|
|
753
921
|
ctx.force = true;
|
|
754
922
|
} else {
|
|
@@ -765,7 +933,7 @@ var RunCopierTemplate = class extends Command {
|
|
|
765
933
|
ctx.force = true;
|
|
766
934
|
}
|
|
767
935
|
}
|
|
768
|
-
const
|
|
936
|
+
const env2 = {
|
|
769
937
|
...process.env,
|
|
770
938
|
SKIP_TELEGRAM: "1",
|
|
771
939
|
SKIP_EMAIL: "1",
|
|
@@ -775,9 +943,9 @@ var RunCopierTemplate = class extends Command {
|
|
|
775
943
|
SKIP_BLOODBANK: ctx.skipBloodbank ? "1" : "0",
|
|
776
944
|
SKIP_SYSTEMD: ctx.skipSystemd ? "1" : "0"
|
|
777
945
|
};
|
|
778
|
-
const LOCAL_TEMPLATE =
|
|
946
|
+
const LOCAL_TEMPLATE = join5(homedir2(), "code", "hermes-agent-template");
|
|
779
947
|
const vendored = resolveVendoredTemplate("hermes-agent");
|
|
780
|
-
const templateSrc = process.env.PJANGLER_HERMES_TEMPLATE || vendored || (existsSync3(
|
|
948
|
+
const templateSrc = process.env.PJANGLER_HERMES_TEMPLATE || vendored || (existsSync3(join5(LOCAL_TEMPLATE, "copier.yml")) ? LOCAL_TEMPLATE : HERMES_AGENT_TEMPLATE);
|
|
781
949
|
const args = [
|
|
782
950
|
"copy",
|
|
783
951
|
templateSrc,
|
|
@@ -808,13 +976,13 @@ var RunCopierTemplate = class extends Command {
|
|
|
808
976
|
message: this.formatMessage(`Would run: copier ${args.join(" ")}`)
|
|
809
977
|
};
|
|
810
978
|
}
|
|
811
|
-
mkdirSync3(
|
|
979
|
+
mkdirSync3(join5(ctx.targetDir, "agents", "hermes"), { recursive: true });
|
|
812
980
|
const spinner4 = p2.spinner();
|
|
813
981
|
spinner4.start(`Running copier copy (target: agents/hermes/${role})`);
|
|
814
982
|
const result = spawnSync("copier", args, {
|
|
815
983
|
stdio: "inherit",
|
|
816
984
|
// pass the interactive output through; copier prints its own progress
|
|
817
|
-
env,
|
|
985
|
+
env: env2,
|
|
818
986
|
cwd: ctx.targetDir
|
|
819
987
|
});
|
|
820
988
|
spinner4.stop(result.status === 0 ? "\u2713 copier run complete" : "\u2717 copier failed");
|
|
@@ -833,7 +1001,7 @@ var RunCopierTemplate = class extends Command {
|
|
|
833
1001
|
|
|
834
1002
|
// src/commands/hermes/WireTelegram.ts
|
|
835
1003
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
836
|
-
import { join as
|
|
1004
|
+
import { join as join6 } from "node:path";
|
|
837
1005
|
import { existsSync as existsSync4, unlinkSync } from "node:fs";
|
|
838
1006
|
import * as p3 from "@clack/prompts";
|
|
839
1007
|
var WireTelegram = class extends Command {
|
|
@@ -922,14 +1090,14 @@ var WireTelegram = class extends Command {
|
|
|
922
1090
|
if (p3.isCancel(allowedAnswer)) {
|
|
923
1091
|
return { success: false, message: "\u2717 Aborted; Telegram step deferred." };
|
|
924
1092
|
}
|
|
925
|
-
const script =
|
|
1093
|
+
const script = join6(roleDir, ".scripts", "30-telegram.sh");
|
|
926
1094
|
if (!existsSync4(script)) {
|
|
927
1095
|
return {
|
|
928
1096
|
success: false,
|
|
929
1097
|
message: `\u2717 ${script} not found. Did copier finish? Re-run with --skip-runtime-repo=0 if you skipped it.`
|
|
930
1098
|
};
|
|
931
1099
|
}
|
|
932
|
-
const marker =
|
|
1100
|
+
const marker = join6(roleDir, ".scripts", ".done-30-telegram");
|
|
933
1101
|
if (existsSync4(marker)) unlinkSync(marker);
|
|
934
1102
|
const spinner4 = p3.spinner();
|
|
935
1103
|
spinner4.start("Verifying token + wiring profile");
|
|
@@ -957,7 +1125,7 @@ function cap(s) {
|
|
|
957
1125
|
|
|
958
1126
|
// src/commands/hermes/WireEmail.ts
|
|
959
1127
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
960
|
-
import { join as
|
|
1128
|
+
import { join as join7 } from "node:path";
|
|
961
1129
|
import { existsSync as existsSync5, unlinkSync as unlinkSync2 } from "node:fs";
|
|
962
1130
|
import * as p4 from "@clack/prompts";
|
|
963
1131
|
var WireEmail = class extends Command {
|
|
@@ -973,7 +1141,7 @@ var WireEmail = class extends Command {
|
|
|
973
1141
|
if (!targetRepo || !role || !roleDir) {
|
|
974
1142
|
return { success: false, message: "Cannot wire email: missing target_repo/role/roleDir" };
|
|
975
1143
|
}
|
|
976
|
-
const script =
|
|
1144
|
+
const script = join7(roleDir, ".scripts", "50-email.sh");
|
|
977
1145
|
if (!existsSync5(script)) {
|
|
978
1146
|
return { success: false, message: `\u2717 ${script} not found` };
|
|
979
1147
|
}
|
|
@@ -1036,7 +1204,7 @@ var WireEmail = class extends Command {
|
|
|
1036
1204
|
}
|
|
1037
1205
|
}
|
|
1038
1206
|
}
|
|
1039
|
-
const marker =
|
|
1207
|
+
const marker = join7(roleDir, ".scripts", ".done-50-email");
|
|
1040
1208
|
if (existsSync5(marker)) unlinkSync2(marker);
|
|
1041
1209
|
const spinner4 = p4.spinner();
|
|
1042
1210
|
spinner4.start("Creating Cloudflare Email Routing rule");
|
|
@@ -1125,7 +1293,7 @@ var HermesAgentRecipe = class extends Recipe {
|
|
|
1125
1293
|
|
|
1126
1294
|
// src/commands/AgentHooksCommands.ts
|
|
1127
1295
|
import { homedir as homedir3 } from "node:os";
|
|
1128
|
-
import { join as
|
|
1296
|
+
import { join as join8, dirname as dirname4 } from "node:path";
|
|
1129
1297
|
import { existsSync as existsSync6, cpSync, mkdirSync as mkdirSync4, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
1130
1298
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1131
1299
|
function resolveTemplateRoot() {
|
|
@@ -1136,16 +1304,16 @@ function resolveTemplateRoot() {
|
|
|
1136
1304
|
try {
|
|
1137
1305
|
let dir = dirname4(fileURLToPath2(import.meta.url));
|
|
1138
1306
|
for (let i = 0; i < 8; i++) {
|
|
1139
|
-
candidates.push(
|
|
1307
|
+
candidates.push(join8(dir, "templates", "commonproject", "template"));
|
|
1140
1308
|
const parent = dirname4(dir);
|
|
1141
1309
|
if (parent === dir) break;
|
|
1142
1310
|
dir = parent;
|
|
1143
1311
|
}
|
|
1144
1312
|
} catch {
|
|
1145
1313
|
}
|
|
1146
|
-
candidates.push(
|
|
1314
|
+
candidates.push(join8(homedir3(), "code", "pjangler", "templates", "commonproject", "template"));
|
|
1147
1315
|
for (const c of candidates) {
|
|
1148
|
-
if (existsSync6(
|
|
1316
|
+
if (existsSync6(join8(c, ".agents", "hooks", "hooks.master.json"))) return c;
|
|
1149
1317
|
}
|
|
1150
1318
|
throw new Error(
|
|
1151
1319
|
"Could not locate the CommonProject template. Set PJANGLER_COMMONPROJECT_TEMPLATE to <repo>/templates/commonproject/template."
|
|
@@ -1169,8 +1337,8 @@ var CopyAgentHooksTree = class extends Command {
|
|
|
1169
1337
|
const created = [];
|
|
1170
1338
|
const skipped = [];
|
|
1171
1339
|
for (const { rel, dir } of items) {
|
|
1172
|
-
const src =
|
|
1173
|
-
const dest =
|
|
1340
|
+
const src = join8(templateRoot, rel);
|
|
1341
|
+
const dest = join8(this.context.targetDir, rel);
|
|
1174
1342
|
if (!existsSync6(src)) continue;
|
|
1175
1343
|
if (existsSync6(dest) && !this.context.force) {
|
|
1176
1344
|
skipped.push(rel);
|
|
@@ -1195,7 +1363,7 @@ var WireMiseAgentHooks = class _WireMiseAgentHooks extends Command {
|
|
|
1195
1363
|
static CR = "{{config_root}}";
|
|
1196
1364
|
// mise's own runtime var — emitted literally
|
|
1197
1365
|
async invoke() {
|
|
1198
|
-
const misePath =
|
|
1366
|
+
const misePath = join8(this.context.targetDir, "mise.toml");
|
|
1199
1367
|
if (!existsSync6(misePath)) {
|
|
1200
1368
|
return {
|
|
1201
1369
|
success: false,
|
|
@@ -1316,7 +1484,7 @@ var RECIPE_REGISTRY = {
|
|
|
1316
1484
|
name: "mise",
|
|
1317
1485
|
description: "Mise task runner and environment setup",
|
|
1318
1486
|
class: MiseRecipe,
|
|
1319
|
-
commands: ["AddMiseToml", "AddDotenv", "AddMiseTasksStructure", "AddMiseBaseToml", "AddMiseBaseScript"]
|
|
1487
|
+
commands: ["AddMiseToml", "AddDotenv", "AddMiseTasksStructure", "AddMiseBaseToml", "AddMiseBaseScript", "AddMiseCodegraphScript"]
|
|
1320
1488
|
},
|
|
1321
1489
|
docker: {
|
|
1322
1490
|
name: "docker",
|
|
@@ -1406,6 +1574,12 @@ var COMMAND_REGISTRY = {
|
|
|
1406
1574
|
group: "mise",
|
|
1407
1575
|
class: AddMiseBaseScript
|
|
1408
1576
|
},
|
|
1577
|
+
AddMiseCodegraphScript: {
|
|
1578
|
+
name: "AddMiseCodegraphScript",
|
|
1579
|
+
description: "Create .mise/scripts/codegraph.sh enter hook",
|
|
1580
|
+
group: "mise",
|
|
1581
|
+
class: AddMiseCodegraphScript
|
|
1582
|
+
},
|
|
1409
1583
|
AddDotenv: {
|
|
1410
1584
|
name: "AddDotenv",
|
|
1411
1585
|
description: "Create .env.example file",
|
|
@@ -1445,8 +1619,8 @@ function createRecipe(name, context) {
|
|
|
1445
1619
|
import { cancel as cancel2, multiselect, text as text3, isCancel as isCancel5 } from "@clack/prompts";
|
|
1446
1620
|
|
|
1447
1621
|
// src/parity/index.ts
|
|
1448
|
-
import { existsSync as existsSync7, lstatSync, mkdirSync as mkdirSync5, readFileSync as readFileSync3, readlinkSync, readdirSync, renameSync, symlinkSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync4, chmodSync, copyFileSync } from "node:fs";
|
|
1449
|
-
import { basename as basename2, dirname as dirname5, join as
|
|
1622
|
+
import { existsSync as existsSync7, lstatSync, mkdirSync as mkdirSync5, readFileSync as readFileSync3, readlinkSync, readdirSync, renameSync, symlinkSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync4, chmodSync as chmodSync2, copyFileSync } from "node:fs";
|
|
1623
|
+
import { basename as basename2, dirname as dirname5, join as join9, relative, resolve } from "node:path";
|
|
1450
1624
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
1451
1625
|
import { homedir as homedir4 } from "node:os";
|
|
1452
1626
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
@@ -1520,7 +1694,7 @@ run = "{{config_root}}/.mise/scripts/versioning.sh sync"
|
|
|
1520
1694
|
function resolvePjanglerRoot() {
|
|
1521
1695
|
let dir = dirname5(fileURLToPath3(import.meta.url));
|
|
1522
1696
|
while (dir !== dirname5(dir)) {
|
|
1523
|
-
if (existsSync7(
|
|
1697
|
+
if (existsSync7(join9(dir, "package.json")) && existsSync7(join9(dir, "templates", "commonproject", "copier.yml"))) {
|
|
1524
1698
|
return dir;
|
|
1525
1699
|
}
|
|
1526
1700
|
dir = dirname5(dir);
|
|
@@ -1583,10 +1757,10 @@ function ensureSymlink(path, target, dryRun) {
|
|
|
1583
1757
|
return { changed: true };
|
|
1584
1758
|
}
|
|
1585
1759
|
function bootstrapAgentsFile(repoRoot, dryRun) {
|
|
1586
|
-
const agentsPath =
|
|
1760
|
+
const agentsPath = join9(repoRoot, "AGENTS.md");
|
|
1587
1761
|
if (existsSync7(agentsPath)) return { changedFiles: [], details: [] };
|
|
1588
1762
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
1589
|
-
const source =
|
|
1763
|
+
const source = join9(repoRoot, file);
|
|
1590
1764
|
if (!existsSync7(source)) continue;
|
|
1591
1765
|
const stat = lstatSync(source);
|
|
1592
1766
|
if (stat.isSymbolicLink()) continue;
|
|
@@ -1596,7 +1770,7 @@ function bootstrapAgentsFile(repoRoot, dryRun) {
|
|
|
1596
1770
|
}
|
|
1597
1771
|
return { changedFiles: [], details: [], blocked: `${file} exists but is not a regular file; cannot promote to AGENTS.md` };
|
|
1598
1772
|
}
|
|
1599
|
-
const readmePath =
|
|
1773
|
+
const readmePath = join9(repoRoot, "README.md");
|
|
1600
1774
|
if (existsSync7(readmePath)) {
|
|
1601
1775
|
const stat = lstatSync(readmePath);
|
|
1602
1776
|
if (!stat.isFile()) return { changedFiles: [], details: [], blocked: "README.md exists but is not a regular file; cannot copy to AGENTS.md" };
|
|
@@ -1636,11 +1810,11 @@ function yamlGet(text4, keyPath) {
|
|
|
1636
1810
|
return "";
|
|
1637
1811
|
}
|
|
1638
1812
|
function discoverRoles(repoRoot) {
|
|
1639
|
-
const rolesDir =
|
|
1813
|
+
const rolesDir = join9(repoRoot, "agents", "hermes");
|
|
1640
1814
|
if (!existsSync7(rolesDir)) return [];
|
|
1641
1815
|
return readdirSync(rolesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => {
|
|
1642
|
-
const roleDir =
|
|
1643
|
-
const roleYamlPath =
|
|
1816
|
+
const roleDir = join9(rolesDir, entry.name);
|
|
1817
|
+
const roleYamlPath = join9(roleDir, "role.yaml");
|
|
1644
1818
|
if (!existsSync7(roleYamlPath)) return null;
|
|
1645
1819
|
const text4 = readText(roleYamlPath);
|
|
1646
1820
|
const runtimeRepoRaw = yamlGet(text4, "runtime.github_repo");
|
|
@@ -1665,7 +1839,7 @@ function discoverRoles(repoRoot) {
|
|
|
1665
1839
|
}).filter((value) => Boolean(value));
|
|
1666
1840
|
}
|
|
1667
1841
|
function registryPath(homeDir) {
|
|
1668
|
-
return
|
|
1842
|
+
return join9(homeDir, ".hermes", "agents-registry.yaml");
|
|
1669
1843
|
}
|
|
1670
1844
|
function systemctlUser(args) {
|
|
1671
1845
|
const result = spawnSync4("systemctl", ["--user", ...args], { encoding: "utf8" });
|
|
@@ -1676,7 +1850,7 @@ function systemctlUser(args) {
|
|
|
1676
1850
|
};
|
|
1677
1851
|
}
|
|
1678
1852
|
function templateScript(ctx, name) {
|
|
1679
|
-
const source =
|
|
1853
|
+
const source = join9(ctx.pjanglerRoot, ".mise", "scripts", name);
|
|
1680
1854
|
return existsSync7(source) ? readText(source) : void 0;
|
|
1681
1855
|
}
|
|
1682
1856
|
function templateVersioningScript(ctx) {
|
|
@@ -1691,9 +1865,9 @@ function renderGeneratedProjectMiseToml(ctx, template) {
|
|
|
1691
1865
|
return template.replace(/\{%\s*raw\s*%\}([\s\S]*?)\{%\s*endraw\s*%\}/g, "$1").replace(/\{\{\s*project_name\s*\}\}/g, projectName);
|
|
1692
1866
|
}
|
|
1693
1867
|
function ensureMiseTomlFromTemplate(ctx, changedFiles) {
|
|
1694
|
-
const targetPath =
|
|
1868
|
+
const targetPath = join9(ctx.repoRoot, "mise.toml");
|
|
1695
1869
|
if (existsSync7(targetPath)) return false;
|
|
1696
|
-
const sourcePath =
|
|
1870
|
+
const sourcePath = join9(ctx.pjanglerRoot, "templates", "commonproject", "template", "mise.toml.jinja");
|
|
1697
1871
|
if (!existsSync7(sourcePath)) return false;
|
|
1698
1872
|
changedFiles.push(targetPath);
|
|
1699
1873
|
if (!ctx.dryRun) {
|
|
@@ -1702,7 +1876,7 @@ function ensureMiseTomlFromTemplate(ctx, changedFiles) {
|
|
|
1702
1876
|
return true;
|
|
1703
1877
|
}
|
|
1704
1878
|
function templateVersionFilesConf(ctx, repoRoot) {
|
|
1705
|
-
const packageJson =
|
|
1879
|
+
const packageJson = join9(repoRoot, "package.json");
|
|
1706
1880
|
return existsSync7(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";
|
|
1707
1881
|
}
|
|
1708
1882
|
function replaceOrAppendManagedBlock(text4, startMarker, block, beforePattern) {
|
|
@@ -1727,7 +1901,7 @@ var CONDITIONAL_HERMES_PATHS = ["agents/hermes/pm/hermes", "agent/hermes/pm/herm
|
|
|
1727
1901
|
function requiredMisePathEntries(ctx) {
|
|
1728
1902
|
const required = [...BASE_MISE_PATH_ENTRIES];
|
|
1729
1903
|
for (const candidate of CONDITIONAL_HERMES_PATHS) {
|
|
1730
|
-
if (existsSync7(
|
|
1904
|
+
if (existsSync7(join9(ctx.repoRoot, candidate)) && !required.includes(candidate)) required.push(candidate);
|
|
1731
1905
|
}
|
|
1732
1906
|
return required;
|
|
1733
1907
|
}
|
|
@@ -1876,7 +2050,7 @@ function upsertLinkAgentfilesBlock(text4, ctx) {
|
|
|
1876
2050
|
return insertTomlBlockBeforeVersioning(cleaned, LINK_AGENTFILES_WATCH_TASK_BLOCK);
|
|
1877
2051
|
}
|
|
1878
2052
|
function readProjectJson(ctx) {
|
|
1879
|
-
return tryParseJson(safeReadText(
|
|
2053
|
+
return tryParseJson(safeReadText(join9(ctx.repoRoot, ".project.json")));
|
|
1880
2054
|
}
|
|
1881
2055
|
function canonicalProjectJson(ctx) {
|
|
1882
2056
|
const roles = discoverRoles(ctx.repoRoot);
|
|
@@ -1920,8 +2094,8 @@ function canonicalProjectJson(ctx) {
|
|
|
1920
2094
|
};
|
|
1921
2095
|
}
|
|
1922
2096
|
function projectJsonFinding(ctx) {
|
|
1923
|
-
const projectPath =
|
|
1924
|
-
const planeJsonPath =
|
|
2097
|
+
const projectPath = join9(ctx.repoRoot, ".project.json");
|
|
2098
|
+
const planeJsonPath = join9(ctx.repoRoot, ".plane.json");
|
|
1925
2099
|
const details = [];
|
|
1926
2100
|
const data = readProjectJson(ctx);
|
|
1927
2101
|
const roles = discoverRoles(ctx.repoRoot);
|
|
@@ -2035,9 +2209,9 @@ function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip)
|
|
|
2035
2209
|
if (!existsSync7(sourceDir)) return;
|
|
2036
2210
|
mkdirSync5(targetDir, { recursive: true });
|
|
2037
2211
|
for (const entry of readdirSync(sourceDir, { withFileTypes: true })) {
|
|
2038
|
-
const sourcePath =
|
|
2212
|
+
const sourcePath = join9(sourceDir, entry.name);
|
|
2039
2213
|
if (skip?.(sourcePath)) continue;
|
|
2040
|
-
const targetPath =
|
|
2214
|
+
const targetPath = join9(targetDir, entry.name);
|
|
2041
2215
|
if (entry.isDirectory()) {
|
|
2042
2216
|
copyMissingRecursive(sourcePath, targetPath, changedFiles, dryRun, skip);
|
|
2043
2217
|
continue;
|
|
@@ -2051,7 +2225,7 @@ function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip)
|
|
|
2051
2225
|
}
|
|
2052
2226
|
}
|
|
2053
2227
|
function upsertSubmodule(repoRoot, role, changedFiles, dryRun) {
|
|
2054
|
-
const gitmodulesPath =
|
|
2228
|
+
const gitmodulesPath = join9(repoRoot, ".gitmodules");
|
|
2055
2229
|
const repoName = role.runtimeRepo || `agent-hm-${role.repo}-${role.role}`;
|
|
2056
2230
|
const owner = role.runtimeOwner || "delorenj";
|
|
2057
2231
|
const block = `[submodule "agents/hermes/${role.role}/runtime"]
|
|
@@ -2151,13 +2325,13 @@ var RULES = [
|
|
|
2151
2325
|
id: "mise.config-root",
|
|
2152
2326
|
title: "mise config_root + AGENTS link hooks",
|
|
2153
2327
|
audit: (ctx) => {
|
|
2154
|
-
const misePath =
|
|
2328
|
+
const misePath = join9(ctx.repoRoot, "mise.toml");
|
|
2155
2329
|
if (!existsSync7(misePath)) {
|
|
2156
2330
|
return { id: "mise.config-root", title: "mise config_root + AGENTS link hooks", status: "fail", summary: "mise.toml missing", details: [], fixable: true };
|
|
2157
2331
|
}
|
|
2158
2332
|
const text4 = readText(misePath);
|
|
2159
2333
|
const details = [];
|
|
2160
|
-
const linkAgentfilesPath =
|
|
2334
|
+
const linkAgentfilesPath = join9(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
|
|
2161
2335
|
if (!existsSync7(linkAgentfilesPath)) details.push(".mise/scripts/link-agentfiles.sh missing");
|
|
2162
2336
|
const pathValues = [...(text4.match(/^_\.path\s*=\s*\[([^\]]*)\]/m)?.[1] ?? "").matchAll(/"([^"]+)"/g)].map((match) => match[1]);
|
|
2163
2337
|
const missingPathValues = requiredMisePathEntries(ctx).filter((value) => !pathValues.includes(value));
|
|
@@ -2176,7 +2350,7 @@ var RULES = [
|
|
|
2176
2350
|
};
|
|
2177
2351
|
},
|
|
2178
2352
|
migrate: (ctx, finding) => {
|
|
2179
|
-
const path =
|
|
2353
|
+
const path = join9(ctx.repoRoot, "mise.toml");
|
|
2180
2354
|
const changedFiles = [];
|
|
2181
2355
|
const details = [];
|
|
2182
2356
|
if (!existsSync7(path)) {
|
|
@@ -2195,7 +2369,7 @@ var RULES = [
|
|
|
2195
2369
|
if (!ctx.dryRun) writeText(path, next);
|
|
2196
2370
|
text4 = next;
|
|
2197
2371
|
}
|
|
2198
|
-
const linkAgentfilesPath =
|
|
2372
|
+
const linkAgentfilesPath = join9(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
|
|
2199
2373
|
const expectedScript = templateLinkAgentfilesScript(ctx);
|
|
2200
2374
|
if (expectedScript === void 0) {
|
|
2201
2375
|
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: [] };
|
|
@@ -2204,7 +2378,7 @@ var RULES = [
|
|
|
2204
2378
|
changedFiles.push(linkAgentfilesPath);
|
|
2205
2379
|
if (!ctx.dryRun) {
|
|
2206
2380
|
writeText(linkAgentfilesPath, expectedScript);
|
|
2207
|
-
|
|
2381
|
+
chmodSync2(linkAgentfilesPath, 493);
|
|
2208
2382
|
}
|
|
2209
2383
|
}
|
|
2210
2384
|
return {
|
|
@@ -2222,9 +2396,9 @@ var RULES = [
|
|
|
2222
2396
|
title: "managed mise versioning block",
|
|
2223
2397
|
audit: (ctx) => {
|
|
2224
2398
|
const details = [];
|
|
2225
|
-
const misePath =
|
|
2226
|
-
const versioningPath =
|
|
2227
|
-
const manifestPath =
|
|
2399
|
+
const misePath = join9(ctx.repoRoot, "mise.toml");
|
|
2400
|
+
const versioningPath = join9(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
|
|
2401
|
+
const manifestPath = join9(ctx.repoRoot, ".mise", "version-files.conf");
|
|
2228
2402
|
const text4 = safeReadText(misePath);
|
|
2229
2403
|
if (!text4?.includes("# >>> mise-versioning >>>")) details.push("mise versioning managed block missing");
|
|
2230
2404
|
if (!existsSync7(versioningPath)) details.push(".mise/scripts/versioning.sh missing");
|
|
@@ -2241,7 +2415,7 @@ var RULES = [
|
|
|
2241
2415
|
migrate: (ctx, finding) => {
|
|
2242
2416
|
const changedFiles = [];
|
|
2243
2417
|
const details = [];
|
|
2244
|
-
const misePath =
|
|
2418
|
+
const misePath = join9(ctx.repoRoot, "mise.toml");
|
|
2245
2419
|
if (!existsSync7(misePath)) {
|
|
2246
2420
|
if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
|
|
2247
2421
|
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: [] };
|
|
@@ -2257,7 +2431,7 @@ var RULES = [
|
|
|
2257
2431
|
if (!changedFiles.includes(misePath)) changedFiles.push(misePath);
|
|
2258
2432
|
if (!ctx.dryRun) writeText(misePath, nextMise);
|
|
2259
2433
|
}
|
|
2260
|
-
const versioningPath =
|
|
2434
|
+
const versioningPath = join9(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
|
|
2261
2435
|
const expectedScript = templateVersioningScript(ctx);
|
|
2262
2436
|
if (expectedScript === void 0) {
|
|
2263
2437
|
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: [] };
|
|
@@ -2266,10 +2440,10 @@ var RULES = [
|
|
|
2266
2440
|
changedFiles.push(versioningPath);
|
|
2267
2441
|
if (!ctx.dryRun) {
|
|
2268
2442
|
writeText(versioningPath, expectedScript);
|
|
2269
|
-
|
|
2443
|
+
chmodSync2(versioningPath, 493);
|
|
2270
2444
|
}
|
|
2271
2445
|
}
|
|
2272
|
-
const manifestPath =
|
|
2446
|
+
const manifestPath = join9(ctx.repoRoot, ".mise", "version-files.conf");
|
|
2273
2447
|
const expectedManifest = templateVersionFilesConf(ctx, ctx.repoRoot);
|
|
2274
2448
|
if (safeReadText(manifestPath) !== expectedManifest) {
|
|
2275
2449
|
changedFiles.push(manifestPath);
|
|
@@ -2289,9 +2463,9 @@ var RULES = [
|
|
|
2289
2463
|
id: "sot.agent-symlinks",
|
|
2290
2464
|
title: "AGENTS/CLAUDE/GEMINI symlink contract",
|
|
2291
2465
|
audit: (ctx) => {
|
|
2292
|
-
const agentsPath =
|
|
2466
|
+
const agentsPath = join9(ctx.repoRoot, "AGENTS.md");
|
|
2293
2467
|
if (!existsSync7(agentsPath)) {
|
|
2294
|
-
const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) => existsSync7(
|
|
2468
|
+
const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) => existsSync7(join9(ctx.repoRoot, file)));
|
|
2295
2469
|
if (fallbackSources.length === 0) {
|
|
2296
2470
|
return { id: "sot.agent-symlinks", title: "AGENTS/CLAUDE/GEMINI symlink contract", status: "skip", summary: "AGENTS.md missing; symlink contract not applicable", details: [], fixable: false };
|
|
2297
2471
|
}
|
|
@@ -2306,7 +2480,7 @@ var RULES = [
|
|
|
2306
2480
|
}
|
|
2307
2481
|
const details = [];
|
|
2308
2482
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
2309
|
-
const full =
|
|
2483
|
+
const full = join9(ctx.repoRoot, file);
|
|
2310
2484
|
const target = readSymlinkTarget(full);
|
|
2311
2485
|
if (target !== "AGENTS.md") details.push(`${file} should be a symlink to AGENTS.md`);
|
|
2312
2486
|
}
|
|
@@ -2330,7 +2504,7 @@ var RULES = [
|
|
|
2330
2504
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "AGENTS.md missing; cannot derive canonical agent file", changedFiles, details: [bootstrap.blocked] };
|
|
2331
2505
|
}
|
|
2332
2506
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
2333
|
-
const full =
|
|
2507
|
+
const full = join9(ctx.repoRoot, file);
|
|
2334
2508
|
const result = ensureSymlink(full, "AGENTS.md", ctx.dryRun);
|
|
2335
2509
|
if (result.blocked) blockedDetails.push(result.blocked);
|
|
2336
2510
|
if (result.changed) changedFiles.push(full);
|
|
@@ -2352,7 +2526,7 @@ var RULES = [
|
|
|
2352
2526
|
migrate: (ctx, finding) => {
|
|
2353
2527
|
const changedFiles = [];
|
|
2354
2528
|
const details = [];
|
|
2355
|
-
const path =
|
|
2529
|
+
const path = join9(ctx.repoRoot, ".project.json");
|
|
2356
2530
|
const existing = readProjectJson(ctx) ?? {};
|
|
2357
2531
|
const canonical = canonicalProjectJson(ctx);
|
|
2358
2532
|
const merged = { ...existing, ...canonical };
|
|
@@ -2362,7 +2536,7 @@ var RULES = [
|
|
|
2362
2536
|
changedFiles.push(path);
|
|
2363
2537
|
if (!ctx.dryRun) writeText(path, expected);
|
|
2364
2538
|
}
|
|
2365
|
-
const planeJson =
|
|
2539
|
+
const planeJson = join9(ctx.repoRoot, ".plane.json");
|
|
2366
2540
|
if (existsSync7(planeJson)) {
|
|
2367
2541
|
const backup = `${planeJson}.migrated-backup`;
|
|
2368
2542
|
if (existsSync7(backup)) {
|
|
@@ -2387,8 +2561,8 @@ var RULES = [
|
|
|
2387
2561
|
title: ".env.op + gitignore secrets contract",
|
|
2388
2562
|
audit: (ctx) => {
|
|
2389
2563
|
const details = [];
|
|
2390
|
-
const envOp = safeReadText(
|
|
2391
|
-
const gitignore = safeReadText(
|
|
2564
|
+
const envOp = safeReadText(join9(ctx.repoRoot, ".env.op"));
|
|
2565
|
+
const gitignore = safeReadText(join9(ctx.repoRoot, ".gitignore"));
|
|
2392
2566
|
if (!envOp) {
|
|
2393
2567
|
details.push(".env.op missing");
|
|
2394
2568
|
} else {
|
|
@@ -2414,12 +2588,12 @@ var RULES = [
|
|
|
2414
2588
|
migrate: (ctx, finding) => {
|
|
2415
2589
|
const changedFiles = [];
|
|
2416
2590
|
const details = [];
|
|
2417
|
-
const envOpPath =
|
|
2591
|
+
const envOpPath = join9(ctx.repoRoot, ".env.op");
|
|
2418
2592
|
if (!existsSync7(envOpPath)) {
|
|
2419
2593
|
changedFiles.push(envOpPath);
|
|
2420
|
-
if (!ctx.dryRun) writeText(envOpPath, readText(
|
|
2594
|
+
if (!ctx.dryRun) writeText(envOpPath, readText(join9(ctx.pjanglerRoot, "templates", "commonproject", "template", ".env.op")));
|
|
2421
2595
|
}
|
|
2422
|
-
const gitignorePath =
|
|
2596
|
+
const gitignorePath = join9(ctx.repoRoot, ".gitignore");
|
|
2423
2597
|
const gitignore = safeReadText(gitignorePath) ?? "";
|
|
2424
2598
|
const requiredBlock = `# Secrets \u2014 .env is materialized by \`op inject -i .env.op > .env\` on mise enter.
|
|
2425
2599
|
# NEVER commit it. .env.op holds only 1Password references or safe literals and IS committed.
|
|
@@ -2446,7 +2620,7 @@ var RULES = [
|
|
|
2446
2620
|
title: ".copier-answers.yml provenance + drift report",
|
|
2447
2621
|
audit: (ctx) => {
|
|
2448
2622
|
const details = [];
|
|
2449
|
-
const path =
|
|
2623
|
+
const path = join9(ctx.repoRoot, ".copier-answers.yml");
|
|
2450
2624
|
const text4 = safeReadText(path);
|
|
2451
2625
|
const project = readProjectJson(ctx);
|
|
2452
2626
|
if (!text4) {
|
|
@@ -2477,12 +2651,12 @@ var RULES = [
|
|
|
2477
2651
|
const changedFiles = [];
|
|
2478
2652
|
const project = canonicalProjectJson(ctx);
|
|
2479
2653
|
const text4 = `# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
|
|
2480
|
-
_src_path: ${
|
|
2654
|
+
_src_path: ${join9(ctx.pjanglerRoot, "templates", "commonproject")}
|
|
2481
2655
|
project_description: ${String(project.project_description)}
|
|
2482
2656
|
project_name: ${String(project.project_name)}
|
|
2483
2657
|
ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
2484
2658
|
`;
|
|
2485
|
-
const path =
|
|
2659
|
+
const path = join9(ctx.repoRoot, ".copier-answers.yml");
|
|
2486
2660
|
if (safeReadText(path) !== text4) {
|
|
2487
2661
|
changedFiles.push(path);
|
|
2488
2662
|
if (!ctx.dryRun) writeText(path, text4);
|
|
@@ -2501,15 +2675,15 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
2501
2675
|
id: "bmad.scaffold",
|
|
2502
2676
|
title: "BMAD modules/docs scaffold",
|
|
2503
2677
|
audit: (ctx) => {
|
|
2504
|
-
const sourceRoot =
|
|
2505
|
-
const targetRoot =
|
|
2678
|
+
const sourceRoot = join9(ctx.pjanglerRoot, "templates", "commonproject", "_bmad");
|
|
2679
|
+
const targetRoot = join9(ctx.repoRoot, "_bmad");
|
|
2506
2680
|
const sentinels = [
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2681
|
+
join9("core", "config.yaml"),
|
|
2682
|
+
join9("custom", "config.yaml"),
|
|
2683
|
+
join9("custom", "workflows", "ticket-lifecycle", "workflow.yaml"),
|
|
2684
|
+
join9("bmm", "workflows", "workflow-status", "workflow.yaml")
|
|
2511
2685
|
];
|
|
2512
|
-
const missing = sentinels.filter((file) => existsSync7(
|
|
2686
|
+
const missing = sentinels.filter((file) => existsSync7(join9(sourceRoot, file)) && !existsSync7(join9(targetRoot, file)));
|
|
2513
2687
|
return {
|
|
2514
2688
|
id: "bmad.scaffold",
|
|
2515
2689
|
title: "BMAD modules/docs scaffold",
|
|
@@ -2521,7 +2695,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
2521
2695
|
},
|
|
2522
2696
|
migrate: (ctx, finding) => {
|
|
2523
2697
|
const changedFiles = [];
|
|
2524
|
-
copyMissingRecursive(
|
|
2698
|
+
copyMissingRecursive(join9(ctx.pjanglerRoot, "templates", "commonproject", "_bmad"), join9(ctx.repoRoot, "_bmad"), changedFiles, ctx.dryRun);
|
|
2525
2699
|
return {
|
|
2526
2700
|
id: finding.id,
|
|
2527
2701
|
title: finding.title,
|
|
@@ -2543,11 +2717,11 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
2543
2717
|
}
|
|
2544
2718
|
const details = [];
|
|
2545
2719
|
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"]) {
|
|
2546
|
-
if (!existsSync7(
|
|
2720
|
+
if (!existsSync7(join9(role.roleDir, rel))) details.push(`missing ${relative(ctx.repoRoot, join9(role.roleDir, rel))}`);
|
|
2547
2721
|
}
|
|
2548
|
-
const gitmodules = safeReadText(
|
|
2722
|
+
const gitmodules = safeReadText(join9(ctx.repoRoot, ".gitmodules")) ?? "";
|
|
2549
2723
|
if (!gitmodules.includes(`agents/hermes/${role.role}/runtime`)) details.push(".gitmodules missing pm runtime submodule entry");
|
|
2550
|
-
if (!profileMetaInheritsDefault(
|
|
2724
|
+
if (!profileMetaInheritsDefault(join9(role.roleDir, "runtime", "profile.yaml"))) {
|
|
2551
2725
|
details.push("runtime/profile.yaml missing inherited default config metadata");
|
|
2552
2726
|
}
|
|
2553
2727
|
const registry = safeReadText(registryPath(ctx.homeDir));
|
|
@@ -2568,21 +2742,21 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
2568
2742
|
if (!role) {
|
|
2569
2743
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "No pm role present", changedFiles, details: [] };
|
|
2570
2744
|
}
|
|
2571
|
-
const templateRoleDir =
|
|
2572
|
-
writeIfDifferent(
|
|
2573
|
-
writeIfDifferent(
|
|
2574
|
-
writeIfDifferent(
|
|
2575
|
-
copyMissingRecursive(
|
|
2576
|
-
copyMissingRecursive(
|
|
2577
|
-
copyMissingRecursive(
|
|
2578
|
-
const promptSource =
|
|
2579
|
-
const promptTarget =
|
|
2745
|
+
const templateRoleDir = join9(ctx.pjanglerRoot, "templates", "hermes-agent", "template");
|
|
2746
|
+
writeIfDifferent(join9(role.roleDir, "SOUL.md"), renderSoul(role), ctx.dryRun, changedFiles);
|
|
2747
|
+
writeIfDifferent(join9(role.roleDir, "hermes"), renderHermesWrapper(role), ctx.dryRun, changedFiles, 493);
|
|
2748
|
+
writeIfDifferent(join9(role.roleDir, ".gitignore"), readText(join9(templateRoleDir, ".gitignore.jinja")).replace(/\{\{ role \}\}/g, role.role), ctx.dryRun, changedFiles);
|
|
2749
|
+
copyMissingRecursive(join9(templateRoleDir, ".runtime-scaffold"), join9(role.roleDir, ".runtime-scaffold"), changedFiles, ctx.dryRun);
|
|
2750
|
+
copyMissingRecursive(join9(templateRoleDir, ".runtime-scaffold"), join9(role.roleDir, "runtime"), changedFiles, ctx.dryRun);
|
|
2751
|
+
copyMissingRecursive(join9(templateRoleDir, ".scripts"), join9(role.roleDir, ".scripts"), changedFiles, ctx.dryRun, (source) => source.endsWith("sentinel.prompt.md.jinja"));
|
|
2752
|
+
const promptSource = join9(templateRoleDir, ".scripts", "sentinel.prompt.md.jinja");
|
|
2753
|
+
const promptTarget = join9(role.roleDir, ".scripts", "sentinel.prompt.md");
|
|
2580
2754
|
if (existsSync7(promptSource) && !existsSync7(promptTarget)) {
|
|
2581
2755
|
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);
|
|
2582
2756
|
writeIfDifferent(promptTarget, prompt, ctx.dryRun, changedFiles);
|
|
2583
2757
|
}
|
|
2584
2758
|
upsertSubmodule(ctx.repoRoot, role, changedFiles, ctx.dryRun);
|
|
2585
|
-
const profileMetaUpdated = upsertInheritedProfileMeta(
|
|
2759
|
+
const profileMetaUpdated = upsertInheritedProfileMeta(join9(role.roleDir, "runtime", "profile.yaml"), changedFiles, ctx.dryRun);
|
|
2586
2760
|
if (profileMetaUpdated) details.push(`updated ${profileMetaUpdated}`);
|
|
2587
2761
|
const registryUpdated = upsertRegistryEntry(role, ctx.homeDir, changedFiles, ctx.dryRun);
|
|
2588
2762
|
if (registryUpdated) details.push(`updated ${registryUpdated}`);
|
|
@@ -2636,9 +2810,9 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
2636
2810
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "systemd --user unavailable on this host", changedFiles, details };
|
|
2637
2811
|
}
|
|
2638
2812
|
for (const role of roles) {
|
|
2639
|
-
const sysDir =
|
|
2813
|
+
const sysDir = join9(ctx.homeDir, ".config", "systemd", "user");
|
|
2640
2814
|
const units = [`hermes-${role.agentId}-gateway.service`, `hermes-${role.agentId}-consumer.service`, `hermes-${role.agentId}-heartbeat.timer`];
|
|
2641
|
-
const allUnitsPresent = units.every((unit) => existsSync7(
|
|
2815
|
+
const allUnitsPresent = units.every((unit) => existsSync7(join9(sysDir, unit)));
|
|
2642
2816
|
if (allUnitsPresent) {
|
|
2643
2817
|
if (ctx.dryRun) {
|
|
2644
2818
|
details.push(`would run: systemctl --user enable --now ${units.join(" ")}`);
|
|
@@ -2650,7 +2824,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
2650
2824
|
}
|
|
2651
2825
|
continue;
|
|
2652
2826
|
}
|
|
2653
|
-
for (const script of [
|
|
2827
|
+
for (const script of [join9(role.roleDir, ".scripts", "70-systemd.sh")]) {
|
|
2654
2828
|
if (!script || !existsSync7(script)) continue;
|
|
2655
2829
|
if (ctx.dryRun) {
|
|
2656
2830
|
details.push(`would run: bash ${script}`);
|
|
@@ -2678,7 +2852,7 @@ function writeIfDifferent(path, content, dryRun, changedFiles, mode) {
|
|
|
2678
2852
|
changedFiles.push(path);
|
|
2679
2853
|
if (!dryRun) {
|
|
2680
2854
|
writeText(path, normalized);
|
|
2681
|
-
if (mode)
|
|
2855
|
+
if (mode) chmodSync2(path, mode);
|
|
2682
2856
|
}
|
|
2683
2857
|
}
|
|
2684
2858
|
function getParityRuleIds() {
|
|
@@ -2740,35 +2914,60 @@ function runMigration(selector, repoArg, dryRun, all) {
|
|
|
2740
2914
|
const ruleIds = all ? RULES.map((rule) => rule.id) : selector ? [selector] : [];
|
|
2741
2915
|
return runMigrationForRules(ruleIds, repoArg, dryRun);
|
|
2742
2916
|
}
|
|
2917
|
+
function prettyTimestamp(iso) {
|
|
2918
|
+
const match = /^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})/.exec(iso);
|
|
2919
|
+
return match ? `${match[1]} ${match[2]} UTC` : iso;
|
|
2920
|
+
}
|
|
2743
2921
|
function formatAuditReport(report) {
|
|
2744
|
-
const
|
|
2922
|
+
const counts = {};
|
|
2923
|
+
for (const rule of report.rules) counts[rule.status] = (counts[rule.status] ?? 0) + 1;
|
|
2924
|
+
const idWidth = report.rules.reduce((width, rule) => Math.max(width, rule.id.length), 0);
|
|
2925
|
+
const tally = [];
|
|
2926
|
+
if (counts.pass) tally.push(green(`${counts.pass} passed`));
|
|
2927
|
+
if (counts.fail) tally.push(red(`${counts.fail} failed`));
|
|
2928
|
+
if (counts.warn) tally.push(yellow(`${counts.warn} warning${counts.warn === 1 ? "" : "s"}`));
|
|
2929
|
+
if (counts.skip) tally.push(gray(`${counts.skip} skipped`));
|
|
2930
|
+
const overall = report.ok ? `${green(glyph.pass)} ${bold("Parity audit passed")}` : `${red(glyph.fail)} ${bold("Parity audit failed")}`;
|
|
2931
|
+
const lines = [""];
|
|
2932
|
+
lines.push(` ${overall}${tally.length ? ` ${dim(glyph.dot)} ${joinDot(tally)}` : ""}`);
|
|
2933
|
+
lines.push(` ${dim(report.repo)} ${dim(glyph.dot)} ${dim(prettyTimestamp(report.auditedAt))}`);
|
|
2934
|
+
lines.push("");
|
|
2745
2935
|
for (const rule of report.rules) {
|
|
2746
|
-
|
|
2747
|
-
|
|
2936
|
+
const style = statusStyle(rule.status);
|
|
2937
|
+
lines.push(` ${style.color(style.glyph)} ${style.color(rule.id.padEnd(idWidth))} ${rule.summary}`);
|
|
2938
|
+
for (const detail of rule.details) lines.push(` ${dim(glyph.arrow)} ${dim(detail)}`);
|
|
2748
2939
|
}
|
|
2749
|
-
|
|
2750
|
-
|
|
2940
|
+
lines.push("");
|
|
2941
|
+
return lines.join("\n");
|
|
2751
2942
|
}
|
|
2752
2943
|
function formatMigrationReport(report) {
|
|
2753
|
-
const
|
|
2944
|
+
const idWidth = report.results.reduce((width, result) => Math.max(width, result.id.length), 0);
|
|
2945
|
+
const overall = report.ok ? `${green(glyph.pass)} ${bold(report.dryRun ? "Migration preview complete" : "Migration complete")}` : `${red(glyph.fail)} ${bold("Migration finished with blockers")}`;
|
|
2946
|
+
const lines = [""];
|
|
2947
|
+
lines.push(` ${overall}${report.dryRun ? ` ${dim(glyph.dot)} ${yellow("dry run")}` : ""}`);
|
|
2948
|
+
lines.push(` ${dim(report.repo)}`);
|
|
2949
|
+
if (report.selectedRules.length) lines.push(` ${dim(`rules: ${report.selectedRules.join(", ")}`)}`);
|
|
2950
|
+
lines.push("");
|
|
2754
2951
|
for (const result of report.results) {
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
for (const
|
|
2952
|
+
const style = statusStyle(result.status);
|
|
2953
|
+
lines.push(` ${style.color(style.glyph)} ${style.color(result.id.padEnd(idWidth))} ${result.summary} ${dim(`[${style.label}]`)}`);
|
|
2954
|
+
for (const detail of result.details) lines.push(` ${dim(glyph.arrow)} ${dim(detail)}`);
|
|
2955
|
+
for (const file of result.changedFiles) lines.push(` ${green(glyph.add)} ${file}`);
|
|
2758
2956
|
}
|
|
2759
2957
|
if (report.changedFiles.length) {
|
|
2760
|
-
lines.push("
|
|
2761
|
-
|
|
2958
|
+
lines.push("");
|
|
2959
|
+
lines.push(` ${bold(`Changed files (${report.changedFiles.length})`)}`);
|
|
2960
|
+
for (const file of report.changedFiles) lines.push(` ${green(glyph.add)} ${file}`);
|
|
2762
2961
|
}
|
|
2763
|
-
|
|
2764
|
-
|
|
2962
|
+
lines.push("");
|
|
2963
|
+
return lines.join("\n");
|
|
2765
2964
|
}
|
|
2766
2965
|
|
|
2767
2966
|
// src/project/index.ts
|
|
2768
2967
|
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
2769
2968
|
import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync4, renameSync as renameSync2, statSync, writeFileSync as writeFileSync5 } from "node:fs";
|
|
2770
2969
|
import { homedir as homedir5 } from "node:os";
|
|
2771
|
-
import { basename as basename3, dirname as dirname6, join as
|
|
2970
|
+
import { basename as basename3, dirname as dirname6, join as join10, resolve as resolve2 } from "node:path";
|
|
2772
2971
|
import YAML from "yaml";
|
|
2773
2972
|
var PROJECT_REGISTRY_ENV = "PJ_PROJECT_REGISTRY";
|
|
2774
2973
|
var PROJECT_REGISTRY_SCHEMA_VERSION = 1;
|
|
@@ -2776,10 +2975,10 @@ var KNOWN_SKILL_ROOTS = [
|
|
|
2776
2975
|
"/home/delorenj/code/skillex/all-skills",
|
|
2777
2976
|
"/home/delorenj/code/CoachingAgentFramework/.agents/skills",
|
|
2778
2977
|
"/home/delorenj/code/pjangler/.agents/skills",
|
|
2779
|
-
|
|
2978
|
+
join10(homedir5(), ".codex", "skills")
|
|
2780
2979
|
];
|
|
2781
|
-
function projectRegistryPath(
|
|
2782
|
-
return expandHome(
|
|
2980
|
+
function projectRegistryPath(env2 = process.env) {
|
|
2981
|
+
return expandHome(env2[PROJECT_REGISTRY_ENV] || join10(homedir5(), ".config", "pjangler", "projects.yaml"));
|
|
2783
2982
|
}
|
|
2784
2983
|
function emptyProjectRegistry() {
|
|
2785
2984
|
return { schema_version: PROJECT_REGISTRY_SCHEMA_VERSION, projects: {} };
|
|
@@ -2863,7 +3062,7 @@ function resolveSourceSkillPath(sourceSkill) {
|
|
|
2863
3062
|
if (existsSync8(direct)) return direct;
|
|
2864
3063
|
const name = basename3(sourceSkill);
|
|
2865
3064
|
for (const root of KNOWN_SKILL_ROOTS) {
|
|
2866
|
-
const candidate =
|
|
3065
|
+
const candidate = join10(root, name);
|
|
2867
3066
|
if (existsSync8(candidate)) return candidate;
|
|
2868
3067
|
}
|
|
2869
3068
|
const civilWarLetterifier = "/home/delorenj/code/skillex/all-skills/civilwar-letterifier";
|
|
@@ -2943,7 +3142,7 @@ function planProjectInit(input) {
|
|
|
2943
3142
|
}));
|
|
2944
3143
|
}
|
|
2945
3144
|
actions.push(
|
|
2946
|
-
{ kind: "project.write-manifest", path:
|
|
3145
|
+
{ kind: "project.write-manifest", path: join10(targetDir, ".project.json"), manifest },
|
|
2947
3146
|
{
|
|
2948
3147
|
kind: "plane.create-or-link",
|
|
2949
3148
|
enabled: live,
|
|
@@ -3050,24 +3249,40 @@ function projectManifestFromRegistryProject(project) {
|
|
|
3050
3249
|
};
|
|
3051
3250
|
}
|
|
3052
3251
|
function formatProjectInitPlan(plan) {
|
|
3053
|
-
const lines = [
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3252
|
+
const lines = [""];
|
|
3253
|
+
const title = `${bold(plan.project.name)} ${dim(`(${plan.project.slug})`)}`;
|
|
3254
|
+
lines.push(` ${cyan(bold(glyph.chevron))} ${title}${plan.dryRun ? ` ${dim(glyph.dot)} ${yellow("dry run")}` : ""}`);
|
|
3255
|
+
lines.push(` ${dim("registry".padEnd(8))} ${dim(plan.registryPath)}`);
|
|
3256
|
+
lines.push(` ${dim("target".padEnd(8))} ${dim(plan.project.repo_path)}`);
|
|
3257
|
+
lines.push("");
|
|
3258
|
+
lines.push(` ${bold("Actions")} ${dim(`(${plan.actions.length})`)}`);
|
|
3259
|
+
if (!plan.actions.length) lines.push(` ${dim("(nothing to do)")}`);
|
|
3059
3260
|
for (const action of plan.actions) {
|
|
3060
|
-
lines.push(`
|
|
3061
|
-
if (action.kind === "copier.copy.commonproject") lines.push(`
|
|
3062
|
-
if (action.kind === "project.write-manifest") lines.push(`
|
|
3063
|
-
if (action.kind === "plane.create-or-link" && action.reason) lines.push(`
|
|
3261
|
+
lines.push(` ${cyan(glyph.bullet)} ${action.kind}`);
|
|
3262
|
+
if (action.kind === "copier.copy.commonproject") lines.push(` ${dim(`target: ${action.targetDir}`)}`);
|
|
3263
|
+
if (action.kind === "project.write-manifest") lines.push(` ${dim(`path: ${action.path}`)}`);
|
|
3264
|
+
if (action.kind === "plane.create-or-link" && action.reason) lines.push(` ${dim(`note: ${action.reason}`)}`);
|
|
3064
3265
|
}
|
|
3266
|
+
lines.push("");
|
|
3065
3267
|
return lines.join("\n");
|
|
3066
3268
|
}
|
|
3067
3269
|
function formatProjectList(registry) {
|
|
3068
3270
|
const projects = Object.values(registry.projects).sort((a, b) => a.slug.localeCompare(b.slug));
|
|
3069
|
-
if (!projects.length) return
|
|
3070
|
-
|
|
3271
|
+
if (!projects.length) return `
|
|
3272
|
+
${dim("No projects registered.")}
|
|
3273
|
+
`;
|
|
3274
|
+
const slugWidth = projects.reduce((width, project) => Math.max(width, project.slug.length), 0);
|
|
3275
|
+
const idWidth = projects.reduce((width, project) => Math.max(width, String(project.ticket_provider.identifier ?? "").length), 0);
|
|
3276
|
+
const statusWidth = projects.reduce((width, project) => Math.max(width, project.status.length), 0);
|
|
3277
|
+
const lines = ["", ` ${bold("Projects")} ${dim(`(${projects.length})`)}`, ""];
|
|
3278
|
+
for (const project of projects) {
|
|
3279
|
+
const slug = bold(project.slug.padEnd(slugWidth));
|
|
3280
|
+
const identifier = cyan(String(project.ticket_provider.identifier ?? "").padEnd(idWidth));
|
|
3281
|
+
const status = projectStatusColor(project.status)(project.status.padEnd(statusWidth));
|
|
3282
|
+
lines.push(` ${slug} ${identifier} ${status} ${dim(project.repo_path)}`);
|
|
3283
|
+
}
|
|
3284
|
+
lines.push("");
|
|
3285
|
+
return lines.join("\n");
|
|
3071
3286
|
}
|
|
3072
3287
|
function getProject(registry, slug) {
|
|
3073
3288
|
const project = registry.projects[slug];
|
|
@@ -3084,7 +3299,7 @@ function doctorProjectRegistry(registryPath2 = projectRegistryPath(), slug) {
|
|
|
3084
3299
|
} else if (!statSync(project.repo_path).isDirectory()) {
|
|
3085
3300
|
issues.push({ level: "error", slug: projectSlug, message: `repo_path is not a directory: ${project.repo_path}` });
|
|
3086
3301
|
} else {
|
|
3087
|
-
const manifestPath =
|
|
3302
|
+
const manifestPath = join10(project.repo_path, ".project.json");
|
|
3088
3303
|
if (!existsSync8(manifestPath)) issues.push({ level: "warn", slug: projectSlug, message: ".project.json is missing" });
|
|
3089
3304
|
}
|
|
3090
3305
|
for (const artifact of project.source_artifacts) {
|
|
@@ -3101,7 +3316,7 @@ function doctorProjectRegistry(registryPath2 = projectRegistryPath(), slug) {
|
|
|
3101
3316
|
};
|
|
3102
3317
|
}
|
|
3103
3318
|
function buildCommonProjectCopierAction(input) {
|
|
3104
|
-
const templateDir =
|
|
3319
|
+
const templateDir = join10(input.pjanglerRoot, "templates", "commonproject");
|
|
3105
3320
|
const data = {
|
|
3106
3321
|
project_name: input.projectName,
|
|
3107
3322
|
project_description: input.projectDescription ?? "",
|
|
@@ -3127,7 +3342,7 @@ function buildCommonProjectCopierAction(input) {
|
|
|
3127
3342
|
function resolvePjanglerRoot2() {
|
|
3128
3343
|
let dir = dirname6(new URL(import.meta.url).pathname);
|
|
3129
3344
|
while (dir !== dirname6(dir)) {
|
|
3130
|
-
if (existsSync8(
|
|
3345
|
+
if (existsSync8(join10(dir, "package.json")) && existsSync8(join10(dir, "templates", "commonproject", "copier.yml"))) return dir;
|
|
3131
3346
|
dir = dirname6(dir);
|
|
3132
3347
|
}
|
|
3133
3348
|
return resolve2(process.cwd());
|
|
@@ -3159,7 +3374,7 @@ function validateProjectRecord(project, key) {
|
|
|
3159
3374
|
}
|
|
3160
3375
|
function expandHome(path) {
|
|
3161
3376
|
if (path === "~") return homedir5();
|
|
3162
|
-
if (path.startsWith("~/")) return
|
|
3377
|
+
if (path.startsWith("~/")) return join10(homedir5(), path.slice(2));
|
|
3163
3378
|
return path;
|
|
3164
3379
|
}
|
|
3165
3380
|
function isRecord(value) {
|
|
@@ -3168,14 +3383,14 @@ function isRecord(value) {
|
|
|
3168
3383
|
|
|
3169
3384
|
// src/utils/version.ts
|
|
3170
3385
|
import { readFileSync as readFileSync5 } from "node:fs";
|
|
3171
|
-
import { dirname as dirname7, join as
|
|
3386
|
+
import { dirname as dirname7, join as join11 } from "node:path";
|
|
3172
3387
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
3173
3388
|
var PJANGLER_VERSION = (() => {
|
|
3174
3389
|
try {
|
|
3175
3390
|
let dir = dirname7(fileURLToPath4(import.meta.url));
|
|
3176
3391
|
for (let i = 0; i < 4; i++) {
|
|
3177
3392
|
try {
|
|
3178
|
-
const raw = readFileSync5(
|
|
3393
|
+
const raw = readFileSync5(join11(dir, "package.json"), "utf8");
|
|
3179
3394
|
return JSON.parse(raw).version ?? "0.0.0";
|
|
3180
3395
|
} catch {
|
|
3181
3396
|
const parent = dirname7(dir);
|
|
@@ -3189,6 +3404,7 @@ var PJANGLER_VERSION = (() => {
|
|
|
3189
3404
|
})();
|
|
3190
3405
|
|
|
3191
3406
|
// src/index.ts
|
|
3407
|
+
var xmark = `${red(glyph.fail)}`;
|
|
3192
3408
|
function printMigrationReport(report, asJson) {
|
|
3193
3409
|
if (asJson) {
|
|
3194
3410
|
console.log(JSON.stringify(report, null, 2));
|
|
@@ -3236,8 +3452,8 @@ function packageNameToProjectName(value) {
|
|
|
3236
3452
|
return name.replace(/[-_]+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase()).trim();
|
|
3237
3453
|
}
|
|
3238
3454
|
function deriveProjectDefaults(targetDir) {
|
|
3239
|
-
const manifest = readJson(
|
|
3240
|
-
const pkg = readJson(
|
|
3455
|
+
const manifest = readJson(join12(targetDir, ".project.json"));
|
|
3456
|
+
const pkg = readJson(join12(targetDir, "package.json"));
|
|
3241
3457
|
const name = String(manifest?.project_name ?? "").trim() || packageNameToProjectName(typeof pkg?.name === "string" ? pkg.name : void 0) || packageNameToProjectName(basename4(targetDir)) || "Project";
|
|
3242
3458
|
const ticketProvider = manifest?.ticket_provider && typeof manifest.ticket_provider === "object" ? manifest.ticket_provider : {};
|
|
3243
3459
|
return {
|
|
@@ -3348,7 +3564,7 @@ async function resolveProjectInitTarget(name, options) {
|
|
|
3348
3564
|
if (!targetDir && interactive) {
|
|
3349
3565
|
const defaultName = name ?? basename4(cwd);
|
|
3350
3566
|
const promptedName = name ?? await promptTextValue("Project name", packageNameToProjectName(defaultName));
|
|
3351
|
-
const defaultDir =
|
|
3567
|
+
const defaultDir = join12(cwd, promptedName.replace(/[^A-Za-z0-9._-]/g, "") || promptedName.toLowerCase().replace(/[^a-z0-9]+/g, "-"));
|
|
3352
3568
|
targetDir = await promptTextValue("Project directory", defaultDir);
|
|
3353
3569
|
name = promptedName;
|
|
3354
3570
|
}
|
|
@@ -3384,27 +3600,30 @@ program.command("init").argument("<subsystem>", "Subsystem to initialize").descr
|
|
|
3384
3600
|
try {
|
|
3385
3601
|
const recipe = createRecipe(subsystem, context);
|
|
3386
3602
|
if (!recipe) {
|
|
3387
|
-
console.error(
|
|
3388
|
-
console.
|
|
3603
|
+
console.error(`${xmark} Unknown subsystem: ${bold(subsystem)}`);
|
|
3604
|
+
console.error(` ${dim("Available:")} ${getRecipeNames().map((available) => cyan(available)).join(dim(", "))}`);
|
|
3389
3605
|
process.exit(1);
|
|
3390
3606
|
}
|
|
3391
3607
|
await recipe.execute();
|
|
3392
3608
|
} catch (error) {
|
|
3393
|
-
console.error(
|
|
3609
|
+
console.error(`${xmark} Error initializing ${bold(subsystem)}:`, error);
|
|
3394
3610
|
process.exit(1);
|
|
3395
3611
|
}
|
|
3396
3612
|
});
|
|
3397
3613
|
program.command("list").description("List available subsystems").action(() => {
|
|
3398
|
-
|
|
3614
|
+
const width = Object.keys(RECIPE_REGISTRY).reduce((max, name) => Math.max(max, name.length), 0);
|
|
3615
|
+
console.log("");
|
|
3616
|
+
console.log(` ${heading("Available subsystems")}`);
|
|
3399
3617
|
console.log("");
|
|
3400
3618
|
for (const [name, info] of Object.entries(RECIPE_REGISTRY)) {
|
|
3401
|
-
console.log(` ${name.padEnd(
|
|
3619
|
+
console.log(` ${cyan(name.padEnd(width))} ${dim(info.description)}`);
|
|
3620
|
+
}
|
|
3621
|
+
console.log("");
|
|
3622
|
+
console.log(` ${dim("Examples")}`);
|
|
3623
|
+
for (const example of ["pj init mise", "pj init docker", "pj init node"]) {
|
|
3624
|
+
console.log(` ${dim(glyph.pointer)} ${dim(example)}`);
|
|
3402
3625
|
}
|
|
3403
3626
|
console.log("");
|
|
3404
|
-
console.log("Usage examples:");
|
|
3405
|
-
console.log(" pjangler init mise");
|
|
3406
|
-
console.log(" pjangler init docker");
|
|
3407
|
-
console.log(" pjangler init node");
|
|
3408
3627
|
});
|
|
3409
3628
|
var projectCmd = program.command("project").description("Manage the pjangler project registry");
|
|
3410
3629
|
projectCmd.command("init").argument("[name]", "Project display name").description("Plan or apply a registry-backed CommonProject initialization or legacy repo sync").option("--description <text>", "Project description").option("--target-dir <path>", "Target repo path").option("--source-skill <path>", "Source skill/template provenance path").option("--primary-language <language>", "Primary language for CommonProject rendering", "python").option("--provision-agent", "Plan local Hermes PM agent provisioning").option("--agent-role <role>", "Hermes agent role to plan when --provision-agent is set", "pm").option("--apply", "Write the registry and render the repo scaffold").option("--dry-run", "Preview changes without writing files (default)").option("--live", "Allow live/network/cloud provisioning actions").option("--slug <slug>", "Project registry slug override").option("--identifier <identifier>", "Ticket identifier override").option("--registry <path>", `Registry path override (default: ${projectRegistryPath()})`).option("-f, --force", "Allow replacing an existing registry entry and re-rendering files").option("-y, --yes", "Apply every proposed operation without prompting").option("--no-tui", "Disable interactive prompts").option("--json", "Output machine-parseable JSON").action(async (name, options) => {
|
|
@@ -3458,11 +3677,12 @@ projectCmd.command("init").argument("[name]", "Project display name").descriptio
|
|
|
3458
3677
|
else {
|
|
3459
3678
|
console.log(formatProjectInitPlan(plan));
|
|
3460
3679
|
if (payload.proposedOperations.length) {
|
|
3461
|
-
console.log("Proposed
|
|
3462
|
-
for (const operation of payload.proposedOperations) console.log(`
|
|
3680
|
+
console.log(` ${bold("Proposed operations")} ${dim(`(${payload.proposedOperations.length})`)}`);
|
|
3681
|
+
for (const operation of payload.proposedOperations) console.log(` ${cyan(glyph.bullet)} ${operation}`);
|
|
3463
3682
|
} else {
|
|
3464
|
-
console.log("Project is already in parity.");
|
|
3683
|
+
console.log(` ${green(glyph.pass)} ${dim("Project is already in parity.")}`);
|
|
3465
3684
|
}
|
|
3685
|
+
console.log("");
|
|
3466
3686
|
}
|
|
3467
3687
|
return;
|
|
3468
3688
|
}
|
|
@@ -3490,17 +3710,19 @@ projectCmd.command("init").argument("[name]", "Project display name").descriptio
|
|
|
3490
3710
|
} else {
|
|
3491
3711
|
console.log(formatProjectInitPlan(selectedPlan));
|
|
3492
3712
|
for (const line of result.logs) console.log(line);
|
|
3493
|
-
for (const line of result.errors) console.error(line);
|
|
3713
|
+
for (const line of result.errors) console.error(` ${xmark} ${line}`);
|
|
3494
3714
|
if (migrationReport) console.log(formatMigrationReport(migrationReport));
|
|
3495
|
-
if (result.ok && changedFiles.length) console.log(`Project synchronized
|
|
3496
|
-
|
|
3715
|
+
if (result.ok && changedFiles.length) console.log(` ${green(glyph.pass)} ${bold("Project synchronized")} ${dim(glyph.dot)} ${cyan(plan.project.slug)}
|
|
3716
|
+
`);
|
|
3717
|
+
if (result.ok && changedFiles.length === 0) console.log(` ${green(glyph.pass)} ${dim("Already in parity")} ${dim(glyph.dot)} ${cyan(plan.project.slug)}
|
|
3718
|
+
`);
|
|
3497
3719
|
}
|
|
3498
3720
|
process.exitCode = result.ok ? 0 : 1;
|
|
3499
3721
|
} catch (err) {
|
|
3500
3722
|
if (options.json) {
|
|
3501
3723
|
console.log(JSON.stringify({ ok: false, error: err instanceof Error ? err.message : String(err) }, null, 2));
|
|
3502
3724
|
} else {
|
|
3503
|
-
console.error(
|
|
3725
|
+
console.error(`${xmark} project init failed:`, err instanceof Error ? err.message : err);
|
|
3504
3726
|
}
|
|
3505
3727
|
process.exit(1);
|
|
3506
3728
|
}
|
|
@@ -3511,19 +3733,24 @@ projectCmd.command("list").description("List projects in the pjangler registry")
|
|
|
3511
3733
|
if (options.json) console.log(JSON.stringify(registry, null, 2));
|
|
3512
3734
|
else console.log(formatProjectList(registry));
|
|
3513
3735
|
} catch (err) {
|
|
3514
|
-
console.error(
|
|
3736
|
+
console.error(`${xmark} project list failed:`, err instanceof Error ? err.message : err);
|
|
3515
3737
|
process.exit(1);
|
|
3516
3738
|
}
|
|
3517
3739
|
});
|
|
3518
3740
|
projectCmd.command("show").argument("<slug>", "Project slug").description("Show one project from the pjangler registry").option("--registry <path>", `Registry path override (default: ${projectRegistryPath()})`).option("--json", "Output machine-parseable JSON").action((slug, options) => {
|
|
3519
3741
|
try {
|
|
3520
3742
|
const project = getProject(loadProjectRegistry(options.registry ?? projectRegistryPath()), slug);
|
|
3521
|
-
if (options.json)
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3743
|
+
if (options.json) {
|
|
3744
|
+
console.log(JSON.stringify(project, null, 2));
|
|
3745
|
+
} else {
|
|
3746
|
+
console.log("");
|
|
3747
|
+
console.log(` ${heading(project.name)} ${dim(`(${project.slug})`)}`);
|
|
3748
|
+
console.log(` ${dim(project.repo_path)}`);
|
|
3749
|
+
if (project.description) console.log(` ${project.description}`);
|
|
3750
|
+
console.log("");
|
|
3751
|
+
}
|
|
3525
3752
|
} catch (err) {
|
|
3526
|
-
console.error(
|
|
3753
|
+
console.error(`${xmark} project show failed:`, err instanceof Error ? err.message : err);
|
|
3527
3754
|
process.exit(1);
|
|
3528
3755
|
}
|
|
3529
3756
|
});
|
|
@@ -3533,50 +3760,59 @@ projectCmd.command("doctor").argument("[slug]", "Optional project slug").descrip
|
|
|
3533
3760
|
if (options.json) {
|
|
3534
3761
|
console.log(JSON.stringify(report, null, 2));
|
|
3535
3762
|
} else if (!report.issues.length) {
|
|
3536
|
-
console.log(
|
|
3763
|
+
console.log("");
|
|
3764
|
+
console.log(` ${green(glyph.pass)} ${bold("Project registry OK")} ${dim(glyph.dot)} ${dim(report.registryPath)}`);
|
|
3765
|
+
console.log("");
|
|
3537
3766
|
} else {
|
|
3538
|
-
console.log(
|
|
3539
|
-
|
|
3767
|
+
console.log("");
|
|
3768
|
+
console.log(` ${red(glyph.fail)} ${bold("Project registry issues")} ${dim(glyph.dot)} ${dim(report.registryPath)}`);
|
|
3769
|
+
console.log("");
|
|
3770
|
+
for (const issue of report.issues) {
|
|
3771
|
+
const mark = issue.level === "error" ? red(glyph.fail) : yellow(glyph.warn);
|
|
3772
|
+
console.log(` ${mark} ${bold(issue.slug ?? "registry")} ${issue.message}`);
|
|
3773
|
+
}
|
|
3774
|
+
console.log("");
|
|
3540
3775
|
}
|
|
3541
3776
|
process.exit(report.ok ? 0 : 1);
|
|
3542
3777
|
} catch (err) {
|
|
3543
|
-
console.error(
|
|
3778
|
+
console.error(`${xmark} project doctor failed:`, err instanceof Error ? err.message : err);
|
|
3544
3779
|
process.exit(1);
|
|
3545
3780
|
}
|
|
3546
3781
|
});
|
|
3547
3782
|
var recipeCmd = program.command("recipe").description("Manage pjangler recipes");
|
|
3548
3783
|
recipeCmd.command("list").description("List all available recipes").action(() => {
|
|
3549
|
-
console.log("
|
|
3784
|
+
console.log("");
|
|
3785
|
+
console.log(` ${heading("Recipes")}`);
|
|
3550
3786
|
console.log("");
|
|
3551
3787
|
for (const [name, info] of Object.entries(RECIPE_REGISTRY)) {
|
|
3552
|
-
console.log(` ${name}`);
|
|
3553
|
-
console.log(`
|
|
3554
|
-
console.log(`
|
|
3788
|
+
console.log(` ${cyan(bold(name))}`);
|
|
3789
|
+
console.log(` ${dim(info.description)}`);
|
|
3790
|
+
console.log(` ${dim("commands")} ${info.commands.map((command) => cyan(command)).join(dim(", "))}`);
|
|
3555
3791
|
console.log("");
|
|
3556
3792
|
}
|
|
3557
|
-
console.log("Usage
|
|
3558
|
-
console.log("
|
|
3559
|
-
console.log("
|
|
3793
|
+
console.log(` ${dim("Usage")}`);
|
|
3794
|
+
console.log(` ${dim(glyph.pointer)} ${dim("pj recipe run <name>")}`);
|
|
3795
|
+
console.log(` ${dim(glyph.pointer)} ${dim("pj recipe describe <name>")}`);
|
|
3796
|
+
console.log("");
|
|
3560
3797
|
});
|
|
3561
3798
|
recipeCmd.command("describe").argument("<name>", "Recipe name").description("Show detailed information about a recipe").action((name) => {
|
|
3562
3799
|
const info = getRecipeInfo(name);
|
|
3563
3800
|
if (!info) {
|
|
3564
|
-
console.error(
|
|
3565
|
-
console.
|
|
3801
|
+
console.error(`${xmark} Recipe not found: ${bold(name)}`);
|
|
3802
|
+
console.error(` ${dim("Available:")} ${getRecipeNames().map((available) => cyan(available)).join(dim(", "))}`);
|
|
3566
3803
|
process.exit(1);
|
|
3567
3804
|
}
|
|
3568
|
-
console.log(`\u{1F4E6} Recipe: ${info.name}`);
|
|
3569
3805
|
console.log("");
|
|
3570
|
-
console.log(`
|
|
3806
|
+
console.log(` ${heading(info.name)}`);
|
|
3807
|
+
console.log(` ${dim(info.description)}`);
|
|
3571
3808
|
console.log("");
|
|
3572
|
-
console.log("Commands
|
|
3573
|
-
for (const
|
|
3574
|
-
|
|
3575
|
-
}
|
|
3809
|
+
console.log(` ${bold("Commands")}`);
|
|
3810
|
+
for (const command of info.commands) console.log(` ${cyan(glyph.bullet)} ${command}`);
|
|
3811
|
+
console.log("");
|
|
3812
|
+
console.log(` ${dim("Usage")}`);
|
|
3813
|
+
console.log(` ${dim(glyph.pointer)} ${dim(`pj recipe run ${name}`)}`);
|
|
3814
|
+
console.log(` ${dim(glyph.pointer)} ${dim(`pj init ${name}`)}`);
|
|
3576
3815
|
console.log("");
|
|
3577
|
-
console.log("Usage:");
|
|
3578
|
-
console.log(` pjangler recipe run ${name}`);
|
|
3579
|
-
console.log(` pjangler init ${name}`);
|
|
3580
3816
|
});
|
|
3581
3817
|
recipeCmd.command("run").argument("<name>", "Recipe name").description("Execute a specific recipe").option("--dry-run", "Preview changes without writing files").option("-f, --force", "Overwrite existing files").action(async (name, options) => {
|
|
3582
3818
|
const context = {
|
|
@@ -3587,80 +3823,73 @@ recipeCmd.command("run").argument("<name>", "Recipe name").description("Execute
|
|
|
3587
3823
|
try {
|
|
3588
3824
|
const recipe = createRecipe(name, context);
|
|
3589
3825
|
if (!recipe) {
|
|
3590
|
-
console.error(
|
|
3591
|
-
console.
|
|
3826
|
+
console.error(`${xmark} Recipe not found: ${bold(name)}`);
|
|
3827
|
+
console.error(` ${dim("Available:")} ${getRecipeNames().map((available) => cyan(available)).join(dim(", "))}`);
|
|
3592
3828
|
process.exit(1);
|
|
3593
3829
|
}
|
|
3594
|
-
const dryRunPrefix = context.dryRun ? "[DRY RUN] " : "";
|
|
3595
|
-
console.log(`${dryRunPrefix}\u{1F680} Running recipe: ${name}`);
|
|
3596
|
-
console.log("");
|
|
3597
3830
|
await recipe.execute();
|
|
3598
3831
|
} catch (error) {
|
|
3599
|
-
console.error(
|
|
3832
|
+
console.error(`${xmark} Error running recipe ${bold(name)}:`, error);
|
|
3600
3833
|
process.exit(1);
|
|
3601
3834
|
}
|
|
3602
3835
|
});
|
|
3603
3836
|
var commandCmd = program.command("command").alias("cmd").description("Manage pjangler commands");
|
|
3604
3837
|
commandCmd.command("list").description("List all available commands").option("-g, --group", "Group commands by category").action((options) => {
|
|
3838
|
+
console.log("");
|
|
3605
3839
|
if (options.group) {
|
|
3606
|
-
console.log("
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
for (const [group, commands] of Object.entries(grouped)) {
|
|
3610
|
-
console.log(` ${group.toUpperCase()}:`);
|
|
3611
|
-
for (const cmd of commands) {
|
|
3612
|
-
console.log(` ${cmd.name.padEnd(30)} - ${cmd.description}`);
|
|
3613
|
-
}
|
|
3840
|
+
console.log(` ${heading("Commands by category")}`);
|
|
3841
|
+
for (const [group, commands] of Object.entries(getCommandsByGroup())) {
|
|
3842
|
+
const width = commands.reduce((max, command) => Math.max(max, command.name.length), 0);
|
|
3614
3843
|
console.log("");
|
|
3844
|
+
console.log(` ${bold(group.toUpperCase())}`);
|
|
3845
|
+
for (const command of commands) {
|
|
3846
|
+
console.log(` ${cyan(command.name.padEnd(width))} ${dim(command.description)}`);
|
|
3847
|
+
}
|
|
3615
3848
|
}
|
|
3849
|
+
console.log("");
|
|
3616
3850
|
} else {
|
|
3617
|
-
|
|
3851
|
+
const width = Object.keys(COMMAND_REGISTRY).reduce((max, name) => Math.max(max, name.length), 0);
|
|
3852
|
+
console.log(` ${heading("Commands")}`);
|
|
3618
3853
|
console.log("");
|
|
3619
3854
|
for (const [name, info] of Object.entries(COMMAND_REGISTRY)) {
|
|
3620
|
-
console.log(` ${name.padEnd(
|
|
3855
|
+
console.log(` ${cyan(name.padEnd(width))} ${dim(info.description)}`);
|
|
3621
3856
|
}
|
|
3622
3857
|
console.log("");
|
|
3623
3858
|
}
|
|
3624
|
-
console.log("Usage
|
|
3625
|
-
console.log("
|
|
3626
|
-
console.log("
|
|
3859
|
+
console.log(` ${dim("Usage")}`);
|
|
3860
|
+
console.log(` ${dim(glyph.pointer)} ${dim("pj command list --group")} ${dim("# group by category")}`);
|
|
3861
|
+
console.log(` ${dim(glyph.pointer)} ${dim("pj command describe <name>")} ${dim("# command details")}`);
|
|
3862
|
+
console.log("");
|
|
3627
3863
|
});
|
|
3628
3864
|
commandCmd.command("describe").argument("<name>", "Command name").description("Show detailed information about a command").action((name) => {
|
|
3629
3865
|
const info = getCommandInfo(name);
|
|
3630
3866
|
if (!info) {
|
|
3631
|
-
console.error(
|
|
3632
|
-
console.
|
|
3867
|
+
console.error(`${xmark} Command not found: ${bold(name)}`);
|
|
3868
|
+
console.error(` ${dim("Available:")} ${getCommandNames().map((available) => cyan(available)).join(dim(", "))}`);
|
|
3633
3869
|
process.exit(1);
|
|
3634
3870
|
}
|
|
3635
|
-
|
|
3871
|
+
const usedIn = Object.entries(RECIPE_REGISTRY).filter(([, recipeInfo]) => recipeInfo.commands.includes(name)).map(([recipeName]) => recipeName);
|
|
3636
3872
|
console.log("");
|
|
3637
|
-
console.log(`
|
|
3638
|
-
console.log(`
|
|
3873
|
+
console.log(` ${heading(info.name)}`);
|
|
3874
|
+
console.log(` ${dim(info.description)}`);
|
|
3639
3875
|
console.log("");
|
|
3640
|
-
console.log("
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
}
|
|
3645
|
-
}
|
|
3876
|
+
console.log(` ${dim("group".padEnd(7))} ${cyan(info.group)}`);
|
|
3877
|
+
console.log(` ${dim("recipes".padEnd(7))} ${usedIn.length ? usedIn.map((recipeName) => cyan(recipeName)).join(dim(", ")) : dim("(none)")}`);
|
|
3878
|
+
console.log("");
|
|
3879
|
+
console.log(` ${dim("Part of recipe execution (not run directly).")}`);
|
|
3646
3880
|
console.log("");
|
|
3647
|
-
console.log("Usage:");
|
|
3648
|
-
console.log(` Part of recipe execution (not run directly)`);
|
|
3649
3881
|
});
|
|
3650
3882
|
commandCmd.command("create").argument("<name>", "Command name").argument("<prompt>", "Description of what the command should do").description("Create a new command from template (placeholder for STORY-005)").option("-t, --template <type>", "Template type (toml, json, yaml, dockerfile)").option("-m, --model <model>", "LLM model to use (OpenRouter)").action((name, prompt, options) => {
|
|
3651
|
-
console.log("\u{1F6A7} Command generation coming in STORY-005!");
|
|
3652
3883
|
console.log("");
|
|
3653
|
-
console.log("
|
|
3654
|
-
console.log(
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
}
|
|
3658
|
-
if (options.model) {
|
|
3659
|
-
|
|
3660
|
-
}
|
|
3884
|
+
console.log(` ${yellow(glyph.warn)} ${bold("Command generation coming in STORY-005")}`);
|
|
3885
|
+
console.log("");
|
|
3886
|
+
console.log(` ${dim("Planned")}`);
|
|
3887
|
+
console.log(` ${cyan(glyph.bullet)} Generate ${bold(name)} from prompt: ${dim(`"${prompt}"`)}`);
|
|
3888
|
+
if (options.template) console.log(` ${cyan(glyph.bullet)} Template type: ${cyan(options.template)}`);
|
|
3889
|
+
if (options.model) console.log(` ${cyan(glyph.bullet)} LLM model: ${cyan(options.model)}`);
|
|
3890
|
+
console.log("");
|
|
3891
|
+
console.log(` ${dim("For now, manually create commands in src/commands/")}`);
|
|
3661
3892
|
console.log("");
|
|
3662
|
-
console.log("This feature will be implemented in the Template Generation System story.");
|
|
3663
|
-
console.log("For now, manually create commands in src/commands/");
|
|
3664
3893
|
});
|
|
3665
3894
|
program.command("audit").argument("[repo]", "Path to repo to audit (default: cwd)").description("Deterministic parity audit against 33god project standard").option("--json", "Output machine-parseable JSON").action((repo, options) => {
|
|
3666
3895
|
try {
|
|
@@ -3672,7 +3901,7 @@ program.command("audit").argument("[repo]", "Path to repo to audit (default: cwd
|
|
|
3672
3901
|
}
|
|
3673
3902
|
process.exit(report.ok ? 0 : 1);
|
|
3674
3903
|
} catch (err) {
|
|
3675
|
-
console.error(
|
|
3904
|
+
console.error(`${xmark} audit failed:`, err);
|
|
3676
3905
|
process.exit(1);
|
|
3677
3906
|
}
|
|
3678
3907
|
});
|
|
@@ -3691,7 +3920,7 @@ program.command("migrate").argument("[rule-id]", "Rule ID to migrate (omit to op
|
|
|
3691
3920
|
}
|
|
3692
3921
|
if (ruleId && repo) {
|
|
3693
3922
|
if (!getParityRuleIds().includes(ruleId)) {
|
|
3694
|
-
console.error(
|
|
3923
|
+
console.error(`${xmark} Unknown parity rule: ${bold(ruleId)}`);
|
|
3695
3924
|
process.exit(1);
|
|
3696
3925
|
}
|
|
3697
3926
|
const report2 = runMigration(ruleId, repo, dryRun, false);
|
|
@@ -3704,25 +3933,25 @@ program.command("migrate").argument("[rule-id]", "Rule ID to migrate (omit to op
|
|
|
3704
3933
|
process.exit(report2.ok ? 0 : 1);
|
|
3705
3934
|
}
|
|
3706
3935
|
if (options.json) {
|
|
3707
|
-
console.error(
|
|
3936
|
+
console.error(`${xmark} JSON output requires a rule-id or --all`);
|
|
3708
3937
|
process.exit(1);
|
|
3709
3938
|
}
|
|
3710
3939
|
if (!process.stdin.isTTY) {
|
|
3711
|
-
console.error(
|
|
3940
|
+
console.error(`${xmark} Provide a rule-id, use --all, or run in an interactive terminal`);
|
|
3712
3941
|
process.exit(1);
|
|
3713
3942
|
}
|
|
3714
3943
|
const targetRepo = ruleId ?? repo;
|
|
3715
3944
|
const audit = runAudit(targetRepo);
|
|
3716
3945
|
const ruleIds = await promptForRuleIds(audit.rules);
|
|
3717
3946
|
if (!ruleIds.length) {
|
|
3718
|
-
console.log("No rules selected; nothing to migrate.");
|
|
3947
|
+
console.log(` ${cyan(glyph.info)} ${dim("No rules selected; nothing to migrate.")}`);
|
|
3719
3948
|
process.exit(0);
|
|
3720
3949
|
}
|
|
3721
3950
|
const report = runMigrationForRules(ruleIds, targetRepo, dryRun);
|
|
3722
3951
|
printMigrationReport(report, false);
|
|
3723
3952
|
process.exit(report.ok ? 0 : 1);
|
|
3724
3953
|
} catch (err) {
|
|
3725
|
-
console.error(
|
|
3954
|
+
console.error(`${xmark} migrate failed:`, err);
|
|
3726
3955
|
process.exit(1);
|
|
3727
3956
|
}
|
|
3728
3957
|
});
|
|
@@ -3757,12 +3986,12 @@ program.command("hermes-agent").alias("hermes").description("Provision a Hermes
|
|
|
3757
3986
|
try {
|
|
3758
3987
|
const recipe = createRecipe("hermes-agent", context);
|
|
3759
3988
|
if (!recipe) {
|
|
3760
|
-
console.error(
|
|
3989
|
+
console.error(`${xmark} hermes-agent recipe not registered`);
|
|
3761
3990
|
process.exit(1);
|
|
3762
3991
|
}
|
|
3763
3992
|
await recipe.execute();
|
|
3764
3993
|
} catch (err) {
|
|
3765
|
-
console.error(
|
|
3994
|
+
console.error(`${xmark} hermes-agent failed:`, err);
|
|
3766
3995
|
process.exit(1);
|
|
3767
3996
|
}
|
|
3768
3997
|
});
|
|
@@ -3780,14 +4009,15 @@ configCmd.command("bootstrap").description("Create ~/.config/hermes-agent-templa
|
|
|
3780
4009
|
}
|
|
3781
4010
|
});
|
|
3782
4011
|
program.command("describe").description("Describe the current project (for AI context)").action(() => {
|
|
3783
|
-
console.log("\u{1F50D} Project Description (placeholder for future enhancement)");
|
|
3784
4012
|
console.log("");
|
|
3785
|
-
console.log("
|
|
3786
|
-
console.log("
|
|
3787
|
-
console.log("
|
|
3788
|
-
|
|
3789
|
-
|
|
4013
|
+
console.log(` ${heading("Project description")} ${dim("(placeholder)")}`);
|
|
4014
|
+
console.log("");
|
|
4015
|
+
console.log(` ${dim("Will analyze the project and report:")}`);
|
|
4016
|
+
for (const item of ["Detected project type", "Installed subsystems", "Configuration files present", "Suggested next steps"]) {
|
|
4017
|
+
console.log(` ${cyan(glyph.bullet)} ${item}`);
|
|
4018
|
+
}
|
|
4019
|
+
console.log("");
|
|
4020
|
+
console.log(` ${dim("Coming soon.")}`);
|
|
3790
4021
|
console.log("");
|
|
3791
|
-
console.log("Coming soon!");
|
|
3792
4022
|
});
|
|
3793
4023
|
program.parse();
|