@delorenj/pjangler 1.2.2 → 1.2.4
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,24 +3,12 @@
|
|
|
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
|
|
10
10
|
var HERMES_AGENT_TEMPLATE = "gh:delorenj/hermes-agent-template";
|
|
11
11
|
var SOUL_TONES = ["direct", "playful", "formal", "terse"];
|
|
12
|
-
var ROLE_CHOICES = [
|
|
13
|
-
{ value: "pm", label: "Project Manager (pm)", hint: "triage, planning, ticket authorship, board reconciliation" },
|
|
14
|
-
{ value: "dev", label: "Developer (dev)", hint: "implements tickets" },
|
|
15
|
-
{ value: "review", label: "Reviewer (review)", hint: "adversarial code review" },
|
|
16
|
-
{ value: "ops", label: "Ops (ops)", hint: "deploy / infra" },
|
|
17
|
-
{ value: "qa", label: "QA (qa)", hint: "test authorship + verification" }
|
|
18
|
-
];
|
|
19
|
-
var TICKET_PROVIDERS = [
|
|
20
|
-
{ value: "plane", label: "Plane", hint: "self-hosted at plane.delo.sh (default)" },
|
|
21
|
-
{ value: "linear", label: "Linear", hint: "team board (created in Linear UI)" },
|
|
22
|
-
{ value: "trello", label: "Trello", hint: "board = project" }
|
|
23
|
-
];
|
|
24
12
|
function deriveAgentId(repo, role) {
|
|
25
13
|
return `${repo}-${role}`.toLowerCase();
|
|
26
14
|
}
|
|
@@ -159,6 +147,78 @@ var EnsureTemplateConfig = class extends Command {
|
|
|
159
147
|
}
|
|
160
148
|
};
|
|
161
149
|
|
|
150
|
+
// src/utils/style.ts
|
|
151
|
+
var env = process.env;
|
|
152
|
+
function detectColor() {
|
|
153
|
+
if ("NO_COLOR" in env && env.NO_COLOR !== "") return false;
|
|
154
|
+
const force = env.FORCE_COLOR;
|
|
155
|
+
if (force === "0" || force === "false") return false;
|
|
156
|
+
if (force !== void 0 && force !== "") return true;
|
|
157
|
+
if (env.TERM === "dumb") return false;
|
|
158
|
+
return Boolean(process.stdout.isTTY);
|
|
159
|
+
}
|
|
160
|
+
var colorEnabled = detectColor();
|
|
161
|
+
function sgr(open, close) {
|
|
162
|
+
const prefix = `\x1B[${open}m`;
|
|
163
|
+
const suffix = `\x1B[${close}m`;
|
|
164
|
+
return (value) => colorEnabled ? `${prefix}${value}${suffix}` : String(value);
|
|
165
|
+
}
|
|
166
|
+
var bold = sgr(1, 22);
|
|
167
|
+
var dim = sgr(2, 22);
|
|
168
|
+
var italic = sgr(3, 23);
|
|
169
|
+
var underline = sgr(4, 24);
|
|
170
|
+
var red = sgr(31, 39);
|
|
171
|
+
var green = sgr(32, 39);
|
|
172
|
+
var yellow = sgr(33, 39);
|
|
173
|
+
var blue = sgr(34, 39);
|
|
174
|
+
var magenta = sgr(35, 39);
|
|
175
|
+
var cyan = sgr(36, 39);
|
|
176
|
+
var gray = sgr(90, 39);
|
|
177
|
+
var glyph = {
|
|
178
|
+
pass: "\u2714",
|
|
179
|
+
fail: "\u2716",
|
|
180
|
+
warn: "\u26A0",
|
|
181
|
+
skip: "\u25CB",
|
|
182
|
+
info: "\u2139",
|
|
183
|
+
arrow: "\u21B3",
|
|
184
|
+
bullet: "\u2022",
|
|
185
|
+
dot: "\xB7",
|
|
186
|
+
add: "+",
|
|
187
|
+
chevron: "\u25B8",
|
|
188
|
+
pointer: "\u276F"
|
|
189
|
+
};
|
|
190
|
+
var STATUS_STYLES = {
|
|
191
|
+
pass: { glyph: glyph.pass, color: green, label: "pass" },
|
|
192
|
+
fail: { glyph: glyph.fail, color: red, label: "fail" },
|
|
193
|
+
warn: { glyph: glyph.warn, color: yellow, label: "warn" },
|
|
194
|
+
skip: { glyph: glyph.skip, color: gray, label: "skip" },
|
|
195
|
+
applied: { glyph: glyph.pass, color: green, label: "applied" },
|
|
196
|
+
noop: { glyph: glyph.skip, color: gray, label: "noop" },
|
|
197
|
+
blocked: { glyph: glyph.fail, color: red, label: "blocked" },
|
|
198
|
+
skipped: { glyph: glyph.skip, color: gray, label: "skipped" }
|
|
199
|
+
};
|
|
200
|
+
function statusStyle(status) {
|
|
201
|
+
return STATUS_STYLES[status] ?? { glyph: glyph.dot, color: dim, label: status };
|
|
202
|
+
}
|
|
203
|
+
function projectStatusColor(status) {
|
|
204
|
+
switch (status) {
|
|
205
|
+
case "active":
|
|
206
|
+
return green;
|
|
207
|
+
case "planned":
|
|
208
|
+
return yellow;
|
|
209
|
+
case "archived":
|
|
210
|
+
return gray;
|
|
211
|
+
default:
|
|
212
|
+
return cyan;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function heading(title, marker = glyph.chevron) {
|
|
216
|
+
return `${cyan(bold(marker))} ${bold(title)}`;
|
|
217
|
+
}
|
|
218
|
+
function joinDot(fragments) {
|
|
219
|
+
return fragments.join(dim(` ${glyph.dot} `));
|
|
220
|
+
}
|
|
221
|
+
|
|
162
222
|
// src/recipes/Recipe.ts
|
|
163
223
|
var Recipe = class {
|
|
164
224
|
context;
|
|
@@ -171,26 +231,22 @@ var Recipe = class {
|
|
|
171
231
|
return this;
|
|
172
232
|
}
|
|
173
233
|
async execute() {
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}
|
|
234
|
+
const subsystem = this.constructor.name.replace("Recipe", "").toLowerCase();
|
|
235
|
+
const dryRun = this.context.dryRun;
|
|
236
|
+
console.log("");
|
|
237
|
+
console.log(` ${cyan(bold(glyph.chevron))} ${bold(`Initializing ${subsystem} subsystem`)}${dryRun ? ` ${dim(glyph.dot)} ${yellow("dry run")}` : ""}`);
|
|
238
|
+
console.log("");
|
|
180
239
|
for (const command of this.ingredients) {
|
|
181
240
|
const result = await command.invoke();
|
|
182
|
-
|
|
183
|
-
console.log(result.message);
|
|
184
|
-
} else {
|
|
185
|
-
console.log(result.message);
|
|
186
|
-
}
|
|
241
|
+
console.log(result.message.split("\n").map((line) => line ? ` ${line}` : line).join("\n"));
|
|
187
242
|
}
|
|
188
|
-
if (!
|
|
243
|
+
if (!dryRun) {
|
|
189
244
|
this.printNextSteps();
|
|
190
245
|
} else {
|
|
191
246
|
console.log("");
|
|
192
|
-
console.log("
|
|
193
|
-
console.log("
|
|
247
|
+
console.log(` ${green(glyph.pass)} ${dim("Dry-run complete \u2014 no files were modified.")}`);
|
|
248
|
+
console.log(` ${dim("Remove --dry-run to apply changes.")}`);
|
|
249
|
+
console.log("");
|
|
194
250
|
}
|
|
195
251
|
}
|
|
196
252
|
};
|
|
@@ -333,11 +389,111 @@ if __name__ == "__main__":
|
|
|
333
389
|
}
|
|
334
390
|
};
|
|
335
391
|
|
|
392
|
+
// src/commands/AddMiseCodegraphScript.ts
|
|
393
|
+
import { chmodSync } from "fs";
|
|
394
|
+
import { join as join3 } from "path";
|
|
395
|
+
var AddMiseCodegraphScript = class extends Command {
|
|
396
|
+
async invoke() {
|
|
397
|
+
const filePath = ".mise/scripts/codegraph.sh";
|
|
398
|
+
if (this.fileExists(filePath) && !this.context.force) {
|
|
399
|
+
return {
|
|
400
|
+
success: false,
|
|
401
|
+
message: this.formatMessage("\u26A0\uFE0F .mise/scripts/codegraph.sh already exists"),
|
|
402
|
+
filePath
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
const content = `#!/usr/bin/env bash
|
|
406
|
+
# Mise enter hook: ensure the CodeGraph CLI is available and initialize the
|
|
407
|
+
# project index. If \`codegraph\` is not installed, this script installs it
|
|
408
|
+
# non-interactively into the project-local .mise/bin directory and retries.
|
|
409
|
+
#
|
|
410
|
+
# This is intended to run from a mise enter hook so onboarding a new host is
|
|
411
|
+
# fully automatic.
|
|
412
|
+
|
|
413
|
+
set -euo pipefail
|
|
414
|
+
|
|
415
|
+
REPO_ROOT="\${MISE_PROJECT_ROOT:-$(cd "$(dirname "$0")/../.." && pwd)}"
|
|
416
|
+
PROJECT_BIN_DIR="$REPO_ROOT/.mise/bin"
|
|
417
|
+
mkdir -p "$PROJECT_BIN_DIR"
|
|
418
|
+
|
|
419
|
+
# Install the CodeGraph CLI into the project-local bin directory.
|
|
420
|
+
install_codegraph() {
|
|
421
|
+
echo "[mise] codegraph not found. Installing non-interactively..."
|
|
422
|
+
export CODEGRAPH_BIN_DIR="$PROJECT_BIN_DIR"
|
|
423
|
+
curl -fsSL https://raw.githubusercontent.com/colbymchenry/codegraph/main/install.sh | sh
|
|
424
|
+
|
|
425
|
+
if [ -x "$PROJECT_BIN_DIR/codegraph" ]; then
|
|
426
|
+
export PATH="$PROJECT_BIN_DIR:$PATH"
|
|
427
|
+
else
|
|
428
|
+
echo "[mise] codegraph install did not place a binary at $PROJECT_BIN_DIR/codegraph" >&2
|
|
429
|
+
return 1
|
|
430
|
+
fi
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
# Ensure a codegraph binary is available on PATH.
|
|
434
|
+
ensure_codegraph() {
|
|
435
|
+
if command -v codegraph >/dev/null 2>&1; then
|
|
436
|
+
return 0
|
|
437
|
+
fi
|
|
438
|
+
|
|
439
|
+
# Check the project-local bin dir first (previous install from this hook).
|
|
440
|
+
if [ -x "$PROJECT_BIN_DIR/codegraph" ]; then
|
|
441
|
+
export PATH="$PROJECT_BIN_DIR:$PATH"
|
|
442
|
+
return 0
|
|
443
|
+
fi
|
|
444
|
+
|
|
445
|
+
# Check typical user-level install locations before fetching anything.
|
|
446
|
+
for d in "$HOME/.local/bin" "$HOME/.codegraph/current/bin"; do
|
|
447
|
+
if [ -x "$d/codegraph" ]; then
|
|
448
|
+
export PATH="$d:$PATH"
|
|
449
|
+
return 0
|
|
450
|
+
fi
|
|
451
|
+
done
|
|
452
|
+
|
|
453
|
+
install_codegraph
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
# Attempt to initialize the project graph. If the command is missing, install
|
|
457
|
+
# it and retry once.
|
|
458
|
+
init_project() {
|
|
459
|
+
local err_file
|
|
460
|
+
err_file="$(mktemp)"
|
|
461
|
+
trap 'rm -f "$err_file"' RETURN
|
|
462
|
+
|
|
463
|
+
if codegraph init -i "$REPO_ROOT" 2>"$err_file"; then
|
|
464
|
+
return 0
|
|
465
|
+
fi
|
|
466
|
+
|
|
467
|
+
# If the failure looks like a missing binary, install and retry.
|
|
468
|
+
if grep -qiE 'command not found|not installed|No such file|executable file not found' "$err_file" 2>/dev/null; then
|
|
469
|
+
ensure_codegraph
|
|
470
|
+
codegraph init -i "$REPO_ROOT"
|
|
471
|
+
return 0
|
|
472
|
+
fi
|
|
473
|
+
|
|
474
|
+
cat "$err_file" >&2
|
|
475
|
+
return 1
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
init_project
|
|
479
|
+
`;
|
|
480
|
+
this.writeFile(filePath, content);
|
|
481
|
+
if (!this.context.dryRun) {
|
|
482
|
+
chmodSync(join3(this.context.targetDir, filePath), 493);
|
|
483
|
+
}
|
|
484
|
+
return {
|
|
485
|
+
success: true,
|
|
486
|
+
message: this.formatMessage(this.context.dryRun ? "Would create .mise/scripts/codegraph.sh" : "\u2705 Created .mise/scripts/codegraph.sh"),
|
|
487
|
+
filePath
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
|
|
336
492
|
// src/recipes/MiseRecipe.ts
|
|
337
493
|
var MiseRecipe = class extends Recipe {
|
|
338
494
|
constructor(context) {
|
|
339
495
|
super(context);
|
|
340
|
-
this.addIngredient(AddMiseToml).addIngredient(AddDotenv).addIngredient(AddMiseTasksStructure).addIngredient(AddMiseBaseToml).addIngredient(AddMiseBaseScript);
|
|
496
|
+
this.addIngredient(AddMiseToml).addIngredient(AddDotenv).addIngredient(AddMiseTasksStructure).addIngredient(AddMiseBaseToml).addIngredient(AddMiseBaseScript).addIngredient(AddMiseCodegraphScript);
|
|
341
497
|
}
|
|
342
498
|
printNextSteps() {
|
|
343
499
|
console.log("\u{1F389} Mise subsystem initialized successfully!");
|
|
@@ -565,12 +721,12 @@ var NodeRecipe = class extends Recipe {
|
|
|
565
721
|
};
|
|
566
722
|
|
|
567
723
|
// src/commands/hermes/PromptForAgentConfig.ts
|
|
568
|
-
import { basename, join as
|
|
724
|
+
import { basename, join as join4 } from "node:path";
|
|
569
725
|
import { readFileSync } from "node:fs";
|
|
570
726
|
import * as p from "@clack/prompts";
|
|
571
727
|
function detectTicketProvider(targetDir) {
|
|
572
728
|
try {
|
|
573
|
-
const t = JSON.parse(readFileSync(
|
|
729
|
+
const t = JSON.parse(readFileSync(join4(targetDir, ".project.json"), "utf8"))?.ticket_provider?.type;
|
|
574
730
|
return t === "plane" || t === "linear" || t === "trello" ? t : void 0;
|
|
575
731
|
} catch {
|
|
576
732
|
return void 0;
|
|
@@ -580,19 +736,18 @@ var PromptForAgentConfig = class extends Command {
|
|
|
580
736
|
async invoke() {
|
|
581
737
|
const ctx = this.context;
|
|
582
738
|
const defaultRepo = basename(ctx.targetDir).toLowerCase();
|
|
583
|
-
|
|
739
|
+
ctx.targetRepo = (ctx.targetRepo ?? defaultRepo).toLowerCase();
|
|
740
|
+
ctx.role ??= "pm";
|
|
741
|
+
ctx.agentPurpose ??= `${ctx.role} agent for ${ctx.targetRepo}`;
|
|
742
|
+
ctx.soulTone ??= "direct";
|
|
743
|
+
ctx.modelProvider ??= "";
|
|
744
|
+
ctx.modelName ??= "";
|
|
745
|
+
ctx.ticketProvider ??= detectTicketProvider(ctx.targetDir) ?? "plane";
|
|
746
|
+
ctx.skipEmail ??= true;
|
|
747
|
+
ctx.agentId = deriveAgentId(ctx.targetRepo, ctx.role);
|
|
748
|
+
ctx.profileName = deriveProfileName(ctx.targetRepo, ctx.role);
|
|
584
749
|
if (ctx.yes) {
|
|
585
|
-
ctx.targetRepo = (ctx.targetRepo ?? defaultRepo).toLowerCase();
|
|
586
|
-
ctx.role ??= defaultRole;
|
|
587
|
-
ctx.agentPurpose ??= `${ctx.role} agent for ${ctx.targetRepo}`;
|
|
588
|
-
ctx.soulTone ??= "direct";
|
|
589
|
-
ctx.modelProvider ??= "";
|
|
590
|
-
ctx.modelName ??= "";
|
|
591
|
-
ctx.ticketProvider ??= detectTicketProvider(ctx.targetDir) ?? "plane";
|
|
592
750
|
ctx.skipTelegram ??= true;
|
|
593
|
-
ctx.skipEmail ??= true;
|
|
594
|
-
ctx.agentId = deriveAgentId(ctx.targetRepo, ctx.role);
|
|
595
|
-
ctx.profileName = deriveProfileName(ctx.targetRepo, ctx.role);
|
|
596
751
|
return {
|
|
597
752
|
success: true,
|
|
598
753
|
message: this.formatMessage(
|
|
@@ -600,96 +755,19 @@ var PromptForAgentConfig = class extends Command {
|
|
|
600
755
|
)
|
|
601
756
|
};
|
|
602
757
|
}
|
|
603
|
-
p.intro("\u2695 hermes-agent \xB7
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
placeholder: defaultRepo,
|
|
608
|
-
initialValue: defaultRepo,
|
|
609
|
-
validate: (v) => v && v.trim() ? void 0 : "required"
|
|
610
|
-
});
|
|
611
|
-
if (p.isCancel(answer)) return this.cancelled();
|
|
612
|
-
ctx.targetRepo = String(answer).trim().toLowerCase();
|
|
613
|
-
}
|
|
614
|
-
if (!ctx.role) {
|
|
615
|
-
const answer = await p.select({
|
|
616
|
-
message: "Role",
|
|
617
|
-
options: ROLE_CHOICES.map((r) => ({ value: r.value, label: r.label, hint: r.hint })),
|
|
618
|
-
initialValue: defaultRole
|
|
619
|
-
});
|
|
620
|
-
if (p.isCancel(answer)) return this.cancelled();
|
|
621
|
-
ctx.role = String(answer).trim();
|
|
622
|
-
}
|
|
623
|
-
if (ctx.ticketProvider === void 0) {
|
|
624
|
-
const detected = detectTicketProvider(ctx.targetDir);
|
|
625
|
-
const answer = await p.select({
|
|
626
|
-
message: "Ticket board provider",
|
|
627
|
-
options: TICKET_PROVIDERS.map((t) => ({
|
|
628
|
-
value: t.value,
|
|
629
|
-
label: t.label,
|
|
630
|
-
hint: t.value === detected ? `${t.hint} \u2014 current .project.json` : t.hint
|
|
631
|
-
})),
|
|
632
|
-
initialValue: detected ?? "plane"
|
|
633
|
-
});
|
|
634
|
-
if (p.isCancel(answer)) return this.cancelled();
|
|
635
|
-
ctx.ticketProvider = answer;
|
|
636
|
-
}
|
|
637
|
-
if (!ctx.agentPurpose) {
|
|
638
|
-
const answer = await p.text({
|
|
639
|
-
message: "One-line purpose",
|
|
640
|
-
placeholder: `${ctx.role} agent for ${ctx.targetRepo}`,
|
|
641
|
-
initialValue: `${ctx.role} agent for ${ctx.targetRepo}`
|
|
642
|
-
});
|
|
643
|
-
if (p.isCancel(answer)) return this.cancelled();
|
|
644
|
-
ctx.agentPurpose = String(answer).trim();
|
|
645
|
-
}
|
|
646
|
-
if (!ctx.soulTone) {
|
|
647
|
-
const answer = await p.select({
|
|
648
|
-
message: "Personality tone",
|
|
649
|
-
options: SOUL_TONES.map((t) => ({
|
|
650
|
-
value: t,
|
|
651
|
-
label: t,
|
|
652
|
-
hint: t === "direct" ? "decision-forward, no preamble (default)" : t === "terse" ? "minimum words, conclusion-first" : t === "playful" ? "warm, mildly funny" : "precise, structured"
|
|
653
|
-
})),
|
|
654
|
-
initialValue: "direct"
|
|
655
|
-
});
|
|
656
|
-
if (p.isCancel(answer)) return this.cancelled();
|
|
657
|
-
ctx.soulTone = answer;
|
|
658
|
-
}
|
|
659
|
-
if (ctx.modelProvider === void 0) {
|
|
660
|
-
const answer = await p.text({
|
|
661
|
-
message: "Provider override (empty = inherit shared default profile)",
|
|
662
|
-
placeholder: ""
|
|
663
|
-
});
|
|
664
|
-
if (p.isCancel(answer)) return this.cancelled();
|
|
665
|
-
ctx.modelProvider = String(answer).trim();
|
|
666
|
-
}
|
|
667
|
-
if (ctx.modelName === void 0) {
|
|
668
|
-
const answer = await p.text({
|
|
669
|
-
message: "Model name override (empty = inherit shared default profile)",
|
|
670
|
-
placeholder: ""
|
|
671
|
-
});
|
|
672
|
-
if (p.isCancel(answer)) return this.cancelled();
|
|
673
|
-
ctx.modelName = String(answer).trim();
|
|
674
|
-
}
|
|
758
|
+
p.intro("\u2695 hermes-agent \xB7 provision the PM agent for this repo");
|
|
759
|
+
p.log.info(
|
|
760
|
+
`agent ${ctx.agentId} \xB7 board ${ctx.ticketProvider} \xB7 tone ${ctx.soulTone}`
|
|
761
|
+
);
|
|
675
762
|
if (ctx.skipTelegram === void 0) {
|
|
763
|
+
const botHandle = `${ctx.targetRepo.replace(/-/g, "_")}_${ctx.role}_bot`;
|
|
676
764
|
const wire = await p.confirm({
|
|
677
|
-
message: `Wire up the Telegram bot (@${
|
|
765
|
+
message: `Wire up the Telegram bot (@${botHandle}) now?`,
|
|
678
766
|
initialValue: true
|
|
679
767
|
});
|
|
680
768
|
if (p.isCancel(wire)) return this.cancelled();
|
|
681
769
|
ctx.skipTelegram = !wire;
|
|
682
770
|
}
|
|
683
|
-
if (ctx.skipEmail === void 0) {
|
|
684
|
-
const wire = await p.confirm({
|
|
685
|
-
message: `Provision the delo.sh email address (${ctx.targetRepo}-${ctx.role}@delo.sh) now?`,
|
|
686
|
-
initialValue: true
|
|
687
|
-
});
|
|
688
|
-
if (p.isCancel(wire)) return this.cancelled();
|
|
689
|
-
ctx.skipEmail = !wire;
|
|
690
|
-
}
|
|
691
|
-
ctx.agentId = deriveAgentId(ctx.targetRepo, ctx.role);
|
|
692
|
-
ctx.profileName = deriveProfileName(ctx.targetRepo, ctx.role);
|
|
693
771
|
return {
|
|
694
772
|
success: true,
|
|
695
773
|
message: this.formatMessage(
|
|
@@ -706,7 +784,7 @@ var PromptForAgentConfig = class extends Command {
|
|
|
706
784
|
// src/commands/hermes/RunCopierTemplate.ts
|
|
707
785
|
import { spawnSync } from "node:child_process";
|
|
708
786
|
import { homedir as homedir2 } from "node:os";
|
|
709
|
-
import { join as
|
|
787
|
+
import { join as join5, dirname as dirname3 } from "node:path";
|
|
710
788
|
import { existsSync as existsSync3, mkdirSync as mkdirSync3 } from "node:fs";
|
|
711
789
|
import { fileURLToPath } from "node:url";
|
|
712
790
|
import * as p2 from "@clack/prompts";
|
|
@@ -718,8 +796,8 @@ function resolveVendoredTemplate(name) {
|
|
|
718
796
|
return void 0;
|
|
719
797
|
}
|
|
720
798
|
for (let i = 0; i < 8; i++) {
|
|
721
|
-
const candidate =
|
|
722
|
-
if (existsSync3(
|
|
799
|
+
const candidate = join5(dir, "templates", name);
|
|
800
|
+
if (existsSync3(join5(candidate, "copier.yml"))) return candidate;
|
|
723
801
|
const parent = dirname3(dir);
|
|
724
802
|
if (parent === dir) break;
|
|
725
803
|
dir = parent;
|
|
@@ -738,7 +816,7 @@ var RunCopierTemplate = class extends Command {
|
|
|
738
816
|
message: "PromptForAgentConfig must run before RunCopierTemplate (targetRepo/role unset)"
|
|
739
817
|
};
|
|
740
818
|
}
|
|
741
|
-
const roleDir =
|
|
819
|
+
const roleDir = join5(ctx.targetDir, "agents", "hermes", role);
|
|
742
820
|
ctx.roleDir = roleDir;
|
|
743
821
|
ctx.runtimeRepo = `delorenj/agent-hm-${targetRepo}-${role}`;
|
|
744
822
|
const which = spawnSync("which", ["copier"], { encoding: "utf8" });
|
|
@@ -748,7 +826,7 @@ var RunCopierTemplate = class extends Command {
|
|
|
748
826
|
message: "\u2717 copier not found on PATH. Install with: `uv tool install copier` or `pip install copier`"
|
|
749
827
|
};
|
|
750
828
|
}
|
|
751
|
-
if (existsSync3(
|
|
829
|
+
if (existsSync3(join5(roleDir, "role.yaml")) && !ctx.force) {
|
|
752
830
|
if (ctx.yes) {
|
|
753
831
|
ctx.force = true;
|
|
754
832
|
} else {
|
|
@@ -765,7 +843,7 @@ var RunCopierTemplate = class extends Command {
|
|
|
765
843
|
ctx.force = true;
|
|
766
844
|
}
|
|
767
845
|
}
|
|
768
|
-
const
|
|
846
|
+
const env2 = {
|
|
769
847
|
...process.env,
|
|
770
848
|
SKIP_TELEGRAM: "1",
|
|
771
849
|
SKIP_EMAIL: "1",
|
|
@@ -775,9 +853,9 @@ var RunCopierTemplate = class extends Command {
|
|
|
775
853
|
SKIP_BLOODBANK: ctx.skipBloodbank ? "1" : "0",
|
|
776
854
|
SKIP_SYSTEMD: ctx.skipSystemd ? "1" : "0"
|
|
777
855
|
};
|
|
778
|
-
const LOCAL_TEMPLATE =
|
|
856
|
+
const LOCAL_TEMPLATE = join5(homedir2(), "code", "hermes-agent-template");
|
|
779
857
|
const vendored = resolveVendoredTemplate("hermes-agent");
|
|
780
|
-
const templateSrc = process.env.PJANGLER_HERMES_TEMPLATE || vendored || (existsSync3(
|
|
858
|
+
const templateSrc = process.env.PJANGLER_HERMES_TEMPLATE || vendored || (existsSync3(join5(LOCAL_TEMPLATE, "copier.yml")) ? LOCAL_TEMPLATE : HERMES_AGENT_TEMPLATE);
|
|
781
859
|
const args = [
|
|
782
860
|
"copy",
|
|
783
861
|
templateSrc,
|
|
@@ -808,13 +886,13 @@ var RunCopierTemplate = class extends Command {
|
|
|
808
886
|
message: this.formatMessage(`Would run: copier ${args.join(" ")}`)
|
|
809
887
|
};
|
|
810
888
|
}
|
|
811
|
-
mkdirSync3(
|
|
889
|
+
mkdirSync3(join5(ctx.targetDir, "agents", "hermes"), { recursive: true });
|
|
812
890
|
const spinner4 = p2.spinner();
|
|
813
891
|
spinner4.start(`Running copier copy (target: agents/hermes/${role})`);
|
|
814
892
|
const result = spawnSync("copier", args, {
|
|
815
893
|
stdio: "inherit",
|
|
816
894
|
// pass the interactive output through; copier prints its own progress
|
|
817
|
-
env,
|
|
895
|
+
env: env2,
|
|
818
896
|
cwd: ctx.targetDir
|
|
819
897
|
});
|
|
820
898
|
spinner4.stop(result.status === 0 ? "\u2713 copier run complete" : "\u2717 copier failed");
|
|
@@ -833,7 +911,7 @@ var RunCopierTemplate = class extends Command {
|
|
|
833
911
|
|
|
834
912
|
// src/commands/hermes/WireTelegram.ts
|
|
835
913
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
836
|
-
import { join as
|
|
914
|
+
import { join as join6 } from "node:path";
|
|
837
915
|
import { existsSync as existsSync4, unlinkSync } from "node:fs";
|
|
838
916
|
import * as p3 from "@clack/prompts";
|
|
839
917
|
var WireTelegram = class extends Command {
|
|
@@ -922,14 +1000,14 @@ var WireTelegram = class extends Command {
|
|
|
922
1000
|
if (p3.isCancel(allowedAnswer)) {
|
|
923
1001
|
return { success: false, message: "\u2717 Aborted; Telegram step deferred." };
|
|
924
1002
|
}
|
|
925
|
-
const script =
|
|
1003
|
+
const script = join6(roleDir, ".scripts", "30-telegram.sh");
|
|
926
1004
|
if (!existsSync4(script)) {
|
|
927
1005
|
return {
|
|
928
1006
|
success: false,
|
|
929
1007
|
message: `\u2717 ${script} not found. Did copier finish? Re-run with --skip-runtime-repo=0 if you skipped it.`
|
|
930
1008
|
};
|
|
931
1009
|
}
|
|
932
|
-
const marker =
|
|
1010
|
+
const marker = join6(roleDir, ".scripts", ".done-30-telegram");
|
|
933
1011
|
if (existsSync4(marker)) unlinkSync(marker);
|
|
934
1012
|
const spinner4 = p3.spinner();
|
|
935
1013
|
spinner4.start("Verifying token + wiring profile");
|
|
@@ -957,14 +1035,14 @@ function cap(s) {
|
|
|
957
1035
|
|
|
958
1036
|
// src/commands/hermes/WireEmail.ts
|
|
959
1037
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
960
|
-
import { join as
|
|
1038
|
+
import { join as join7 } from "node:path";
|
|
961
1039
|
import { existsSync as existsSync5, unlinkSync as unlinkSync2 } from "node:fs";
|
|
962
1040
|
import * as p4 from "@clack/prompts";
|
|
963
1041
|
var WireEmail = class extends Command {
|
|
964
1042
|
async invoke() {
|
|
965
1043
|
const ctx = this.context;
|
|
966
1044
|
if (ctx.skipEmail) {
|
|
967
|
-
return { success: true, message: "
|
|
1045
|
+
return { success: true, message: "" };
|
|
968
1046
|
}
|
|
969
1047
|
if (ctx.dryRun) {
|
|
970
1048
|
return { success: true, message: this.formatMessage("Would create CF Email Routing rule") };
|
|
@@ -973,7 +1051,7 @@ var WireEmail = class extends Command {
|
|
|
973
1051
|
if (!targetRepo || !role || !roleDir) {
|
|
974
1052
|
return { success: false, message: "Cannot wire email: missing target_repo/role/roleDir" };
|
|
975
1053
|
}
|
|
976
|
-
const script =
|
|
1054
|
+
const script = join7(roleDir, ".scripts", "50-email.sh");
|
|
977
1055
|
if (!existsSync5(script)) {
|
|
978
1056
|
return { success: false, message: `\u2717 ${script} not found` };
|
|
979
1057
|
}
|
|
@@ -1036,7 +1114,7 @@ var WireEmail = class extends Command {
|
|
|
1036
1114
|
}
|
|
1037
1115
|
}
|
|
1038
1116
|
}
|
|
1039
|
-
const marker =
|
|
1117
|
+
const marker = join7(roleDir, ".scripts", ".done-50-email");
|
|
1040
1118
|
if (existsSync5(marker)) unlinkSync2(marker);
|
|
1041
1119
|
const spinner4 = p4.spinner();
|
|
1042
1120
|
spinner4.start("Creating Cloudflare Email Routing rule");
|
|
@@ -1072,7 +1150,7 @@ var PrintHermesSummary = class extends Command {
|
|
|
1072
1150
|
lines.push(`role dir ${ctx.roleDir}`);
|
|
1073
1151
|
lines.push(`runtime gh:${runtimeRepo}`);
|
|
1074
1152
|
lines.push(`telegram @${botHandle}${skipTelegram ? " (NOT yet wired)" : ""}`);
|
|
1075
|
-
lines.push(`email ${email}
|
|
1153
|
+
if (!skipEmail) lines.push(`email ${email}`);
|
|
1076
1154
|
lines.push("");
|
|
1077
1155
|
lines.push("Start daemons:");
|
|
1078
1156
|
lines.push(` systemctl --user start ${csm}`);
|
|
@@ -1085,11 +1163,10 @@ var PrintHermesSummary = class extends Command {
|
|
|
1085
1163
|
lines.push("");
|
|
1086
1164
|
lines.push("Talk locally:");
|
|
1087
1165
|
lines.push(` ${ctx.roleDir}/hermes chat "status"`);
|
|
1088
|
-
if (skipTelegram
|
|
1166
|
+
if (skipTelegram) {
|
|
1089
1167
|
lines.push("");
|
|
1090
|
-
lines.push("
|
|
1091
|
-
|
|
1092
|
-
if (skipEmail) lines.push(" pjangler hermes-agent --skip-email=false # wire just email");
|
|
1168
|
+
lines.push("Wire Telegram later:");
|
|
1169
|
+
lines.push(" pjangler hermes-agent # re-run and answer yes when asked");
|
|
1093
1170
|
}
|
|
1094
1171
|
p5.note(lines.join("\n"), `Provisioned ${agentId}`);
|
|
1095
1172
|
p5.outro("Done.");
|
|
@@ -1125,7 +1202,7 @@ var HermesAgentRecipe = class extends Recipe {
|
|
|
1125
1202
|
|
|
1126
1203
|
// src/commands/AgentHooksCommands.ts
|
|
1127
1204
|
import { homedir as homedir3 } from "node:os";
|
|
1128
|
-
import { join as
|
|
1205
|
+
import { join as join8, dirname as dirname4 } from "node:path";
|
|
1129
1206
|
import { existsSync as existsSync6, cpSync, mkdirSync as mkdirSync4, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
1130
1207
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1131
1208
|
function resolveTemplateRoot() {
|
|
@@ -1136,16 +1213,16 @@ function resolveTemplateRoot() {
|
|
|
1136
1213
|
try {
|
|
1137
1214
|
let dir = dirname4(fileURLToPath2(import.meta.url));
|
|
1138
1215
|
for (let i = 0; i < 8; i++) {
|
|
1139
|
-
candidates.push(
|
|
1216
|
+
candidates.push(join8(dir, "templates", "commonproject", "template"));
|
|
1140
1217
|
const parent = dirname4(dir);
|
|
1141
1218
|
if (parent === dir) break;
|
|
1142
1219
|
dir = parent;
|
|
1143
1220
|
}
|
|
1144
1221
|
} catch {
|
|
1145
1222
|
}
|
|
1146
|
-
candidates.push(
|
|
1223
|
+
candidates.push(join8(homedir3(), "code", "pjangler", "templates", "commonproject", "template"));
|
|
1147
1224
|
for (const c of candidates) {
|
|
1148
|
-
if (existsSync6(
|
|
1225
|
+
if (existsSync6(join8(c, ".agents", "hooks", "hooks.master.json"))) return c;
|
|
1149
1226
|
}
|
|
1150
1227
|
throw new Error(
|
|
1151
1228
|
"Could not locate the CommonProject template. Set PJANGLER_COMMONPROJECT_TEMPLATE to <repo>/templates/commonproject/template."
|
|
@@ -1169,8 +1246,8 @@ var CopyAgentHooksTree = class extends Command {
|
|
|
1169
1246
|
const created = [];
|
|
1170
1247
|
const skipped = [];
|
|
1171
1248
|
for (const { rel, dir } of items) {
|
|
1172
|
-
const src =
|
|
1173
|
-
const dest =
|
|
1249
|
+
const src = join8(templateRoot, rel);
|
|
1250
|
+
const dest = join8(this.context.targetDir, rel);
|
|
1174
1251
|
if (!existsSync6(src)) continue;
|
|
1175
1252
|
if (existsSync6(dest) && !this.context.force) {
|
|
1176
1253
|
skipped.push(rel);
|
|
@@ -1195,7 +1272,7 @@ var WireMiseAgentHooks = class _WireMiseAgentHooks extends Command {
|
|
|
1195
1272
|
static CR = "{{config_root}}";
|
|
1196
1273
|
// mise's own runtime var — emitted literally
|
|
1197
1274
|
async invoke() {
|
|
1198
|
-
const misePath =
|
|
1275
|
+
const misePath = join8(this.context.targetDir, "mise.toml");
|
|
1199
1276
|
if (!existsSync6(misePath)) {
|
|
1200
1277
|
return {
|
|
1201
1278
|
success: false,
|
|
@@ -1316,7 +1393,7 @@ var RECIPE_REGISTRY = {
|
|
|
1316
1393
|
name: "mise",
|
|
1317
1394
|
description: "Mise task runner and environment setup",
|
|
1318
1395
|
class: MiseRecipe,
|
|
1319
|
-
commands: ["AddMiseToml", "AddDotenv", "AddMiseTasksStructure", "AddMiseBaseToml", "AddMiseBaseScript"]
|
|
1396
|
+
commands: ["AddMiseToml", "AddDotenv", "AddMiseTasksStructure", "AddMiseBaseToml", "AddMiseBaseScript", "AddMiseCodegraphScript"]
|
|
1320
1397
|
},
|
|
1321
1398
|
docker: {
|
|
1322
1399
|
name: "docker",
|
|
@@ -1406,6 +1483,12 @@ var COMMAND_REGISTRY = {
|
|
|
1406
1483
|
group: "mise",
|
|
1407
1484
|
class: AddMiseBaseScript
|
|
1408
1485
|
},
|
|
1486
|
+
AddMiseCodegraphScript: {
|
|
1487
|
+
name: "AddMiseCodegraphScript",
|
|
1488
|
+
description: "Create .mise/scripts/codegraph.sh enter hook",
|
|
1489
|
+
group: "mise",
|
|
1490
|
+
class: AddMiseCodegraphScript
|
|
1491
|
+
},
|
|
1409
1492
|
AddDotenv: {
|
|
1410
1493
|
name: "AddDotenv",
|
|
1411
1494
|
description: "Create .env.example file",
|
|
@@ -1442,11 +1525,11 @@ function createRecipe(name, context) {
|
|
|
1442
1525
|
}
|
|
1443
1526
|
|
|
1444
1527
|
// src/index.ts
|
|
1445
|
-
import { cancel as cancel2, multiselect, text as
|
|
1528
|
+
import { cancel as cancel2, multiselect, text as text2, isCancel as isCancel5 } from "@clack/prompts";
|
|
1446
1529
|
|
|
1447
1530
|
// 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
|
|
1531
|
+
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";
|
|
1532
|
+
import { basename as basename2, dirname as dirname5, join as join9, relative, resolve } from "node:path";
|
|
1450
1533
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
1451
1534
|
import { homedir as homedir4 } from "node:os";
|
|
1452
1535
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
@@ -1520,7 +1603,7 @@ run = "{{config_root}}/.mise/scripts/versioning.sh sync"
|
|
|
1520
1603
|
function resolvePjanglerRoot() {
|
|
1521
1604
|
let dir = dirname5(fileURLToPath3(import.meta.url));
|
|
1522
1605
|
while (dir !== dirname5(dir)) {
|
|
1523
|
-
if (existsSync7(
|
|
1606
|
+
if (existsSync7(join9(dir, "package.json")) && existsSync7(join9(dir, "templates", "commonproject", "copier.yml"))) {
|
|
1524
1607
|
return dir;
|
|
1525
1608
|
}
|
|
1526
1609
|
dir = dirname5(dir);
|
|
@@ -1543,10 +1626,10 @@ function writeText(path, content) {
|
|
|
1543
1626
|
ensureParent(path);
|
|
1544
1627
|
writeFileSync4(path, content);
|
|
1545
1628
|
}
|
|
1546
|
-
function tryParseJson(
|
|
1547
|
-
if (!
|
|
1629
|
+
function tryParseJson(text3) {
|
|
1630
|
+
if (!text3) return null;
|
|
1548
1631
|
try {
|
|
1549
|
-
return JSON.parse(
|
|
1632
|
+
return JSON.parse(text3);
|
|
1550
1633
|
} catch {
|
|
1551
1634
|
return null;
|
|
1552
1635
|
}
|
|
@@ -1583,10 +1666,10 @@ function ensureSymlink(path, target, dryRun) {
|
|
|
1583
1666
|
return { changed: true };
|
|
1584
1667
|
}
|
|
1585
1668
|
function bootstrapAgentsFile(repoRoot, dryRun) {
|
|
1586
|
-
const agentsPath =
|
|
1669
|
+
const agentsPath = join9(repoRoot, "AGENTS.md");
|
|
1587
1670
|
if (existsSync7(agentsPath)) return { changedFiles: [], details: [] };
|
|
1588
1671
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
1589
|
-
const source =
|
|
1672
|
+
const source = join9(repoRoot, file);
|
|
1590
1673
|
if (!existsSync7(source)) continue;
|
|
1591
1674
|
const stat = lstatSync(source);
|
|
1592
1675
|
if (stat.isSymbolicLink()) continue;
|
|
@@ -1596,7 +1679,7 @@ function bootstrapAgentsFile(repoRoot, dryRun) {
|
|
|
1596
1679
|
}
|
|
1597
1680
|
return { changedFiles: [], details: [], blocked: `${file} exists but is not a regular file; cannot promote to AGENTS.md` };
|
|
1598
1681
|
}
|
|
1599
|
-
const readmePath =
|
|
1682
|
+
const readmePath = join9(repoRoot, "README.md");
|
|
1600
1683
|
if (existsSync7(readmePath)) {
|
|
1601
1684
|
const stat = lstatSync(readmePath);
|
|
1602
1685
|
if (!stat.isFile()) return { changedFiles: [], details: [], blocked: "README.md exists but is not a regular file; cannot copy to AGENTS.md" };
|
|
@@ -1605,9 +1688,9 @@ function bootstrapAgentsFile(repoRoot, dryRun) {
|
|
|
1605
1688
|
}
|
|
1606
1689
|
return { changedFiles: [], details: [], blocked: "AGENTS.md missing and no CLAUDE.md, GEMINI.md, or README.md source exists" };
|
|
1607
1690
|
}
|
|
1608
|
-
function yamlGet(
|
|
1691
|
+
function yamlGet(text3, keyPath) {
|
|
1609
1692
|
const parts = keyPath.split(".");
|
|
1610
|
-
const lines =
|
|
1693
|
+
const lines = text3.split("\n");
|
|
1611
1694
|
let start = 0;
|
|
1612
1695
|
let indent = 0;
|
|
1613
1696
|
for (let idx = 0; idx < parts.length; idx += 1) {
|
|
@@ -1636,36 +1719,36 @@ function yamlGet(text4, keyPath) {
|
|
|
1636
1719
|
return "";
|
|
1637
1720
|
}
|
|
1638
1721
|
function discoverRoles(repoRoot) {
|
|
1639
|
-
const rolesDir =
|
|
1722
|
+
const rolesDir = join9(repoRoot, "agents", "hermes");
|
|
1640
1723
|
if (!existsSync7(rolesDir)) return [];
|
|
1641
1724
|
return readdirSync(rolesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => {
|
|
1642
|
-
const roleDir =
|
|
1643
|
-
const roleYamlPath =
|
|
1725
|
+
const roleDir = join9(rolesDir, entry.name);
|
|
1726
|
+
const roleYamlPath = join9(roleDir, "role.yaml");
|
|
1644
1727
|
if (!existsSync7(roleYamlPath)) return null;
|
|
1645
|
-
const
|
|
1646
|
-
const runtimeRepoRaw = yamlGet(
|
|
1728
|
+
const text3 = readText(roleYamlPath);
|
|
1729
|
+
const runtimeRepoRaw = yamlGet(text3, "runtime.github_repo");
|
|
1647
1730
|
return {
|
|
1648
|
-
role: yamlGet(
|
|
1731
|
+
role: yamlGet(text3, "role") || entry.name,
|
|
1649
1732
|
roleDir,
|
|
1650
1733
|
roleYamlPath,
|
|
1651
|
-
repo: yamlGet(
|
|
1652
|
-
agentId: yamlGet(
|
|
1653
|
-
profileName: yamlGet(
|
|
1654
|
-
displayName: yamlGet(
|
|
1655
|
-
purpose: yamlGet(
|
|
1656
|
-
botHandle: yamlGet(
|
|
1734
|
+
repo: yamlGet(text3, "repo"),
|
|
1735
|
+
agentId: yamlGet(text3, "agent_id"),
|
|
1736
|
+
profileName: yamlGet(text3, "profile") || yamlGet(text3, "agent_id"),
|
|
1737
|
+
displayName: yamlGet(text3, "display_name"),
|
|
1738
|
+
purpose: yamlGet(text3, "purpose"),
|
|
1739
|
+
botHandle: yamlGet(text3, "telegram.bot_username"),
|
|
1657
1740
|
runtimeRepo: runtimeRepoRaw.includes("/") ? runtimeRepoRaw.split("/").slice(-1)[0] ?? runtimeRepoRaw : runtimeRepoRaw,
|
|
1658
|
-
runtimeOwner: yamlGet(
|
|
1659
|
-
planeWorkspace: yamlGet(
|
|
1660
|
-
ticketProviderName: yamlGet(
|
|
1661
|
-
ticketProviderBoardId: yamlGet(
|
|
1662
|
-
ticketProviderBoardUrl: yamlGet(
|
|
1663
|
-
ticketProviderIdentifier: yamlGet(
|
|
1741
|
+
runtimeOwner: yamlGet(text3, "runtime.github_owner"),
|
|
1742
|
+
planeWorkspace: yamlGet(text3, "ticket_provider.workspace") || yamlGet(text3, "plane.workspace"),
|
|
1743
|
+
ticketProviderName: yamlGet(text3, "ticket_provider.name"),
|
|
1744
|
+
ticketProviderBoardId: yamlGet(text3, "ticket_provider.board_id"),
|
|
1745
|
+
ticketProviderBoardUrl: yamlGet(text3, "ticket_provider.board_url"),
|
|
1746
|
+
ticketProviderIdentifier: yamlGet(text3, "plane.identifier")
|
|
1664
1747
|
};
|
|
1665
1748
|
}).filter((value) => Boolean(value));
|
|
1666
1749
|
}
|
|
1667
1750
|
function registryPath(homeDir) {
|
|
1668
|
-
return
|
|
1751
|
+
return join9(homeDir, ".hermes", "agents-registry.yaml");
|
|
1669
1752
|
}
|
|
1670
1753
|
function systemctlUser(args) {
|
|
1671
1754
|
const result = spawnSync4("systemctl", ["--user", ...args], { encoding: "utf8" });
|
|
@@ -1676,7 +1759,7 @@ function systemctlUser(args) {
|
|
|
1676
1759
|
};
|
|
1677
1760
|
}
|
|
1678
1761
|
function templateScript(ctx, name) {
|
|
1679
|
-
const source =
|
|
1762
|
+
const source = join9(ctx.pjanglerRoot, ".mise", "scripts", name);
|
|
1680
1763
|
return existsSync7(source) ? readText(source) : void 0;
|
|
1681
1764
|
}
|
|
1682
1765
|
function templateVersioningScript(ctx) {
|
|
@@ -1691,9 +1774,9 @@ function renderGeneratedProjectMiseToml(ctx, template) {
|
|
|
1691
1774
|
return template.replace(/\{%\s*raw\s*%\}([\s\S]*?)\{%\s*endraw\s*%\}/g, "$1").replace(/\{\{\s*project_name\s*\}\}/g, projectName);
|
|
1692
1775
|
}
|
|
1693
1776
|
function ensureMiseTomlFromTemplate(ctx, changedFiles) {
|
|
1694
|
-
const targetPath =
|
|
1777
|
+
const targetPath = join9(ctx.repoRoot, "mise.toml");
|
|
1695
1778
|
if (existsSync7(targetPath)) return false;
|
|
1696
|
-
const sourcePath =
|
|
1779
|
+
const sourcePath = join9(ctx.pjanglerRoot, "templates", "commonproject", "template", "mise.toml.jinja");
|
|
1697
1780
|
if (!existsSync7(sourcePath)) return false;
|
|
1698
1781
|
changedFiles.push(targetPath);
|
|
1699
1782
|
if (!ctx.dryRun) {
|
|
@@ -1702,22 +1785,22 @@ function ensureMiseTomlFromTemplate(ctx, changedFiles) {
|
|
|
1702
1785
|
return true;
|
|
1703
1786
|
}
|
|
1704
1787
|
function templateVersionFilesConf(ctx, repoRoot) {
|
|
1705
|
-
const packageJson =
|
|
1788
|
+
const packageJson = join9(repoRoot, "package.json");
|
|
1706
1789
|
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
1790
|
}
|
|
1708
|
-
function replaceOrAppendManagedBlock(
|
|
1709
|
-
if (startMarker.test(
|
|
1710
|
-
return
|
|
1791
|
+
function replaceOrAppendManagedBlock(text3, startMarker, block, beforePattern) {
|
|
1792
|
+
if (startMarker.test(text3)) {
|
|
1793
|
+
return text3.replace(/# >>> mise-versioning >>>[\s\S]*?# <<< mise-versioning <<</, block);
|
|
1711
1794
|
}
|
|
1712
1795
|
if (beforePattern) {
|
|
1713
|
-
const match =
|
|
1796
|
+
const match = text3.match(beforePattern);
|
|
1714
1797
|
if (match && typeof match.index === "number") {
|
|
1715
|
-
return `${
|
|
1798
|
+
return `${text3.slice(0, match.index).replace(/\s*$/, "\n\n")}${block}
|
|
1716
1799
|
|
|
1717
|
-
${
|
|
1800
|
+
${text3.slice(match.index)}`;
|
|
1718
1801
|
}
|
|
1719
1802
|
}
|
|
1720
|
-
return `${
|
|
1803
|
+
return `${text3.replace(/\s*$/, "")}
|
|
1721
1804
|
|
|
1722
1805
|
${block}
|
|
1723
1806
|
`;
|
|
@@ -1727,22 +1810,22 @@ var CONDITIONAL_HERMES_PATHS = ["agents/hermes/pm/hermes", "agent/hermes/pm/herm
|
|
|
1727
1810
|
function requiredMisePathEntries(ctx) {
|
|
1728
1811
|
const required = [...BASE_MISE_PATH_ENTRIES];
|
|
1729
1812
|
for (const candidate of CONDITIONAL_HERMES_PATHS) {
|
|
1730
|
-
if (existsSync7(
|
|
1813
|
+
if (existsSync7(join9(ctx.repoRoot, candidate)) && !required.includes(candidate)) required.push(candidate);
|
|
1731
1814
|
}
|
|
1732
1815
|
return required;
|
|
1733
1816
|
}
|
|
1734
|
-
function upsertMisePath(
|
|
1817
|
+
function upsertMisePath(text3, required = BASE_MISE_PATH_ENTRIES) {
|
|
1735
1818
|
const render = (values) => `_.path = [${values.map((value) => JSON.stringify(value)).join(", ")}]`;
|
|
1736
|
-
const envMatch =
|
|
1819
|
+
const envMatch = text3.match(/(^|\n)(\[env\][\s\S]*?)(?=\n\[[^\]]+\]|$)/);
|
|
1737
1820
|
if (!envMatch || typeof envMatch.index !== "number") {
|
|
1738
1821
|
return `[env]
|
|
1739
1822
|
${render(required)}
|
|
1740
1823
|
|
|
1741
|
-
${
|
|
1824
|
+
${text3.replace(/^\s+/, "")}`;
|
|
1742
1825
|
}
|
|
1743
|
-
const prefix =
|
|
1826
|
+
const prefix = text3.slice(0, envMatch.index + envMatch[1].length);
|
|
1744
1827
|
const section = envMatch[2];
|
|
1745
|
-
const suffix =
|
|
1828
|
+
const suffix = text3.slice(envMatch.index + envMatch[1].length + section.length);
|
|
1746
1829
|
const pathLine = section.match(/^_\.path\s*=\s*\[([^\]]*)\]\s*$/m);
|
|
1747
1830
|
if (!pathLine) {
|
|
1748
1831
|
return `${prefix}${section.replace(/\n?$/, "\n")}${render(required)}${suffix}`;
|
|
@@ -1753,11 +1836,11 @@ ${text4.replace(/^\s+/, "")}`;
|
|
|
1753
1836
|
if (!merged.includes(value)) merged.push(value);
|
|
1754
1837
|
}
|
|
1755
1838
|
const nextLine = render(merged);
|
|
1756
|
-
if (pathLine[0] === nextLine) return
|
|
1839
|
+
if (pathLine[0] === nextLine) return text3;
|
|
1757
1840
|
return `${prefix}${section.replace(pathLine[0], nextLine)}${suffix}`;
|
|
1758
1841
|
}
|
|
1759
|
-
function removeTomlSection(
|
|
1760
|
-
const lines =
|
|
1842
|
+
function removeTomlSection(text3, headerPattern, marker, options) {
|
|
1843
|
+
const lines = text3.split("\n");
|
|
1761
1844
|
let start = -1;
|
|
1762
1845
|
let end = -1;
|
|
1763
1846
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -1782,7 +1865,7 @@ function removeTomlSection(text4, headerPattern, marker, options) {
|
|
|
1782
1865
|
if (end === -1) end = lines.length;
|
|
1783
1866
|
break;
|
|
1784
1867
|
}
|
|
1785
|
-
if (start === -1) return
|
|
1868
|
+
if (start === -1) return text3;
|
|
1786
1869
|
if (options?.includePrecedingComments) {
|
|
1787
1870
|
while (start > 0 && lines[start - 1].trim().startsWith("#")) {
|
|
1788
1871
|
start--;
|
|
@@ -1791,22 +1874,22 @@ function removeTomlSection(text4, headerPattern, marker, options) {
|
|
|
1791
1874
|
const result = lines.slice(0, start).concat(lines.slice(end)).join("\n");
|
|
1792
1875
|
return result.replace(/\n{3,}/g, "\n\n").replace(/\n+$/, "\n");
|
|
1793
1876
|
}
|
|
1794
|
-
function insertTomlBlockBeforeVersioning(
|
|
1795
|
-
const versioningIndex =
|
|
1877
|
+
function insertTomlBlockBeforeVersioning(text3, block) {
|
|
1878
|
+
const versioningIndex = text3.indexOf("# >>> mise-versioning >>>");
|
|
1796
1879
|
if (versioningIndex >= 0) {
|
|
1797
|
-
return `${
|
|
1880
|
+
return `${text3.slice(0, versioningIndex).replace(/\s*$/, "\n\n")}${block}
|
|
1798
1881
|
|
|
1799
|
-
${
|
|
1882
|
+
${text3.slice(versioningIndex)}`;
|
|
1800
1883
|
}
|
|
1801
|
-
return `${
|
|
1884
|
+
return `${text3.replace(/\s*$/, "")}
|
|
1802
1885
|
|
|
1803
1886
|
${block}
|
|
1804
1887
|
`;
|
|
1805
1888
|
}
|
|
1806
|
-
function extractTomlStrings(
|
|
1889
|
+
function extractTomlStrings(text3) {
|
|
1807
1890
|
const values = [];
|
|
1808
1891
|
const stringPattern = /"((?:\\.|[^"\\])*)"|'([^']*)'/g;
|
|
1809
|
-
for (const match of
|
|
1892
|
+
for (const match of text3.matchAll(stringPattern)) {
|
|
1810
1893
|
if (match[1] !== void 0) {
|
|
1811
1894
|
try {
|
|
1812
1895
|
values.push(JSON.parse(`"${match[1]}"`));
|
|
@@ -1830,10 +1913,10 @@ function renderHookEntries(entries, indent = "") {
|
|
|
1830
1913
|
`${indent}]`
|
|
1831
1914
|
];
|
|
1832
1915
|
}
|
|
1833
|
-
function upsertLinkAgentfilesHooks(
|
|
1834
|
-
const lines =
|
|
1916
|
+
function upsertLinkAgentfilesHooks(text3) {
|
|
1917
|
+
const lines = text3.split("\n");
|
|
1835
1918
|
const hooksStart = lines.findIndex((line) => /^\[hooks\]$/.test(line.trim()));
|
|
1836
|
-
if (hooksStart === -1) return insertTomlBlockBeforeVersioning(
|
|
1919
|
+
if (hooksStart === -1) return insertTomlBlockBeforeVersioning(text3, LINK_AGENTFILES_HOOKS_BLOCK);
|
|
1837
1920
|
let hooksEnd = lines.length;
|
|
1838
1921
|
for (let i = hooksStart + 1; i < lines.length; i++) {
|
|
1839
1922
|
if (/^\[[^\]]+\]/.test(lines[i].trim())) {
|
|
@@ -1867,8 +1950,8 @@ function upsertLinkAgentfilesHooks(text4) {
|
|
|
1867
1950
|
}
|
|
1868
1951
|
return lines.slice(0, hooksStart + 1).concat(rendered, lines.slice(hooksStart + 1)).join("\n").replace(/\n{3,}/g, "\n\n").replace(/\n+$/, "\n");
|
|
1869
1952
|
}
|
|
1870
|
-
function upsertLinkAgentfilesBlock(
|
|
1871
|
-
const withPath = upsertMisePath(
|
|
1953
|
+
function upsertLinkAgentfilesBlock(text3, ctx) {
|
|
1954
|
+
const withPath = upsertMisePath(text3, requiredMisePathEntries(ctx));
|
|
1872
1955
|
if (withPath.includes(LINK_AGENTFILES_BLOCK)) return withPath;
|
|
1873
1956
|
let cleaned = removeTomlSection(withPath, /^\[tasks\.link-agentfiles\]$/, /link-agentfiles/, { includePrecedingComments: false });
|
|
1874
1957
|
cleaned = removeTomlSection(cleaned, /^\[\[watch_files\]\]$/, /AGENTS\.md/, { includePrecedingComments: false });
|
|
@@ -1876,7 +1959,7 @@ function upsertLinkAgentfilesBlock(text4, ctx) {
|
|
|
1876
1959
|
return insertTomlBlockBeforeVersioning(cleaned, LINK_AGENTFILES_WATCH_TASK_BLOCK);
|
|
1877
1960
|
}
|
|
1878
1961
|
function readProjectJson(ctx) {
|
|
1879
|
-
return tryParseJson(safeReadText(
|
|
1962
|
+
return tryParseJson(safeReadText(join9(ctx.repoRoot, ".project.json")));
|
|
1880
1963
|
}
|
|
1881
1964
|
function canonicalProjectJson(ctx) {
|
|
1882
1965
|
const roles = discoverRoles(ctx.repoRoot);
|
|
@@ -1920,8 +2003,8 @@ function canonicalProjectJson(ctx) {
|
|
|
1920
2003
|
};
|
|
1921
2004
|
}
|
|
1922
2005
|
function projectJsonFinding(ctx) {
|
|
1923
|
-
const projectPath =
|
|
1924
|
-
const planeJsonPath =
|
|
2006
|
+
const projectPath = join9(ctx.repoRoot, ".project.json");
|
|
2007
|
+
const planeJsonPath = join9(ctx.repoRoot, ".plane.json");
|
|
1925
2008
|
const details = [];
|
|
1926
2009
|
const data = readProjectJson(ctx);
|
|
1927
2010
|
const roles = discoverRoles(ctx.repoRoot);
|
|
@@ -2035,9 +2118,9 @@ function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip)
|
|
|
2035
2118
|
if (!existsSync7(sourceDir)) return;
|
|
2036
2119
|
mkdirSync5(targetDir, { recursive: true });
|
|
2037
2120
|
for (const entry of readdirSync(sourceDir, { withFileTypes: true })) {
|
|
2038
|
-
const sourcePath =
|
|
2121
|
+
const sourcePath = join9(sourceDir, entry.name);
|
|
2039
2122
|
if (skip?.(sourcePath)) continue;
|
|
2040
|
-
const targetPath =
|
|
2123
|
+
const targetPath = join9(targetDir, entry.name);
|
|
2041
2124
|
if (entry.isDirectory()) {
|
|
2042
2125
|
copyMissingRecursive(sourcePath, targetPath, changedFiles, dryRun, skip);
|
|
2043
2126
|
continue;
|
|
@@ -2051,7 +2134,7 @@ function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip)
|
|
|
2051
2134
|
}
|
|
2052
2135
|
}
|
|
2053
2136
|
function upsertSubmodule(repoRoot, role, changedFiles, dryRun) {
|
|
2054
|
-
const gitmodulesPath =
|
|
2137
|
+
const gitmodulesPath = join9(repoRoot, ".gitmodules");
|
|
2055
2138
|
const repoName = role.runtimeRepo || `agent-hm-${role.repo}-${role.role}`;
|
|
2056
2139
|
const owner = role.runtimeOwner || "delorenj";
|
|
2057
2140
|
const block = `[submodule "agents/hermes/${role.role}/runtime"]
|
|
@@ -2095,9 +2178,9 @@ ${block}`) : `${current.replace(/\s*$/, "\n")}${block}`;
|
|
|
2095
2178
|
return path;
|
|
2096
2179
|
}
|
|
2097
2180
|
function profileMetaInheritsDefault(path) {
|
|
2098
|
-
const
|
|
2181
|
+
const text3 = safeReadText(path);
|
|
2099
2182
|
return Boolean(
|
|
2100
|
-
|
|
2183
|
+
text3 && /^config:\s*$/m.test(text3) && /^\s+inherit_from:\s*default\s*$/m.test(text3) && /^\s+save_mode:\s*delta\s*$/m.test(text3)
|
|
2101
2184
|
);
|
|
2102
2185
|
}
|
|
2103
2186
|
function upsertInheritedProfileMeta(path, changedFiles, dryRun) {
|
|
@@ -2151,21 +2234,21 @@ var RULES = [
|
|
|
2151
2234
|
id: "mise.config-root",
|
|
2152
2235
|
title: "mise config_root + AGENTS link hooks",
|
|
2153
2236
|
audit: (ctx) => {
|
|
2154
|
-
const misePath =
|
|
2237
|
+
const misePath = join9(ctx.repoRoot, "mise.toml");
|
|
2155
2238
|
if (!existsSync7(misePath)) {
|
|
2156
2239
|
return { id: "mise.config-root", title: "mise config_root + AGENTS link hooks", status: "fail", summary: "mise.toml missing", details: [], fixable: true };
|
|
2157
2240
|
}
|
|
2158
|
-
const
|
|
2241
|
+
const text3 = readText(misePath);
|
|
2159
2242
|
const details = [];
|
|
2160
|
-
const linkAgentfilesPath =
|
|
2243
|
+
const linkAgentfilesPath = join9(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
|
|
2161
2244
|
if (!existsSync7(linkAgentfilesPath)) details.push(".mise/scripts/link-agentfiles.sh missing");
|
|
2162
|
-
const pathValues = [...(
|
|
2245
|
+
const pathValues = [...(text3.match(/^_\.path\s*=\s*\[([^\]]*)\]/m)?.[1] ?? "").matchAll(/"([^"]+)"/g)].map((match) => match[1]);
|
|
2163
2246
|
const missingPathValues = requiredMisePathEntries(ctx).filter((value) => !pathValues.includes(value));
|
|
2164
2247
|
if (missingPathValues.length) details.push(`[env]._.path should include ${missingPathValues.join(", ")}`);
|
|
2165
|
-
if (!
|
|
2166
|
-
if (!
|
|
2167
|
-
if (!
|
|
2168
|
-
if (!
|
|
2248
|
+
if (!text3.includes('"{{config_root}}/.mise/scripts/link-agentfiles.sh"')) details.push("link-agentfiles must use raw {{config_root}} guard");
|
|
2249
|
+
if (!text3.includes("op inject -i .env.op > .env")) details.push("[hooks].enter must materialize .env from .env.op");
|
|
2250
|
+
if (!text3.includes('patterns = ["AGENTS.md"]')) details.push("watch_files must monitor AGENTS.md");
|
|
2251
|
+
if (!text3.includes('task = "link-agentfiles"')) details.push("watch_files must dispatch link-agentfiles task");
|
|
2169
2252
|
return {
|
|
2170
2253
|
id: "mise.config-root",
|
|
2171
2254
|
title: "mise config_root + AGENTS link hooks",
|
|
@@ -2176,7 +2259,7 @@ var RULES = [
|
|
|
2176
2259
|
};
|
|
2177
2260
|
},
|
|
2178
2261
|
migrate: (ctx, finding) => {
|
|
2179
|
-
const path =
|
|
2262
|
+
const path = join9(ctx.repoRoot, "mise.toml");
|
|
2180
2263
|
const changedFiles = [];
|
|
2181
2264
|
const details = [];
|
|
2182
2265
|
if (!existsSync7(path)) {
|
|
@@ -2188,14 +2271,14 @@ var RULES = [
|
|
|
2188
2271
|
return { id: finding.id, title: finding.title, status: "applied", summary: "Would initialize mise.toml from generated-project template", changedFiles, details };
|
|
2189
2272
|
}
|
|
2190
2273
|
}
|
|
2191
|
-
let
|
|
2192
|
-
const next = upsertLinkAgentfilesBlock(
|
|
2193
|
-
if (next !==
|
|
2274
|
+
let text3 = readText(path);
|
|
2275
|
+
const next = upsertLinkAgentfilesBlock(text3, ctx);
|
|
2276
|
+
if (next !== text3) {
|
|
2194
2277
|
if (!changedFiles.includes(path)) changedFiles.push(path);
|
|
2195
2278
|
if (!ctx.dryRun) writeText(path, next);
|
|
2196
|
-
|
|
2279
|
+
text3 = next;
|
|
2197
2280
|
}
|
|
2198
|
-
const linkAgentfilesPath =
|
|
2281
|
+
const linkAgentfilesPath = join9(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
|
|
2199
2282
|
const expectedScript = templateLinkAgentfilesScript(ctx);
|
|
2200
2283
|
if (expectedScript === void 0) {
|
|
2201
2284
|
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 +2287,7 @@ var RULES = [
|
|
|
2204
2287
|
changedFiles.push(linkAgentfilesPath);
|
|
2205
2288
|
if (!ctx.dryRun) {
|
|
2206
2289
|
writeText(linkAgentfilesPath, expectedScript);
|
|
2207
|
-
|
|
2290
|
+
chmodSync2(linkAgentfilesPath, 493);
|
|
2208
2291
|
}
|
|
2209
2292
|
}
|
|
2210
2293
|
return {
|
|
@@ -2222,11 +2305,11 @@ var RULES = [
|
|
|
2222
2305
|
title: "managed mise versioning block",
|
|
2223
2306
|
audit: (ctx) => {
|
|
2224
2307
|
const details = [];
|
|
2225
|
-
const misePath =
|
|
2226
|
-
const versioningPath =
|
|
2227
|
-
const manifestPath =
|
|
2228
|
-
const
|
|
2229
|
-
if (!
|
|
2308
|
+
const misePath = join9(ctx.repoRoot, "mise.toml");
|
|
2309
|
+
const versioningPath = join9(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
|
|
2310
|
+
const manifestPath = join9(ctx.repoRoot, ".mise", "version-files.conf");
|
|
2311
|
+
const text3 = safeReadText(misePath);
|
|
2312
|
+
if (!text3?.includes("# >>> mise-versioning >>>")) details.push("mise versioning managed block missing");
|
|
2230
2313
|
if (!existsSync7(versioningPath)) details.push(".mise/scripts/versioning.sh missing");
|
|
2231
2314
|
if (!existsSync7(manifestPath)) details.push(".mise/version-files.conf missing");
|
|
2232
2315
|
return {
|
|
@@ -2241,7 +2324,7 @@ var RULES = [
|
|
|
2241
2324
|
migrate: (ctx, finding) => {
|
|
2242
2325
|
const changedFiles = [];
|
|
2243
2326
|
const details = [];
|
|
2244
|
-
const misePath =
|
|
2327
|
+
const misePath = join9(ctx.repoRoot, "mise.toml");
|
|
2245
2328
|
if (!existsSync7(misePath)) {
|
|
2246
2329
|
if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
|
|
2247
2330
|
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 +2340,7 @@ var RULES = [
|
|
|
2257
2340
|
if (!changedFiles.includes(misePath)) changedFiles.push(misePath);
|
|
2258
2341
|
if (!ctx.dryRun) writeText(misePath, nextMise);
|
|
2259
2342
|
}
|
|
2260
|
-
const versioningPath =
|
|
2343
|
+
const versioningPath = join9(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
|
|
2261
2344
|
const expectedScript = templateVersioningScript(ctx);
|
|
2262
2345
|
if (expectedScript === void 0) {
|
|
2263
2346
|
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 +2349,10 @@ var RULES = [
|
|
|
2266
2349
|
changedFiles.push(versioningPath);
|
|
2267
2350
|
if (!ctx.dryRun) {
|
|
2268
2351
|
writeText(versioningPath, expectedScript);
|
|
2269
|
-
|
|
2352
|
+
chmodSync2(versioningPath, 493);
|
|
2270
2353
|
}
|
|
2271
2354
|
}
|
|
2272
|
-
const manifestPath =
|
|
2355
|
+
const manifestPath = join9(ctx.repoRoot, ".mise", "version-files.conf");
|
|
2273
2356
|
const expectedManifest = templateVersionFilesConf(ctx, ctx.repoRoot);
|
|
2274
2357
|
if (safeReadText(manifestPath) !== expectedManifest) {
|
|
2275
2358
|
changedFiles.push(manifestPath);
|
|
@@ -2289,9 +2372,9 @@ var RULES = [
|
|
|
2289
2372
|
id: "sot.agent-symlinks",
|
|
2290
2373
|
title: "AGENTS/CLAUDE/GEMINI symlink contract",
|
|
2291
2374
|
audit: (ctx) => {
|
|
2292
|
-
const agentsPath =
|
|
2375
|
+
const agentsPath = join9(ctx.repoRoot, "AGENTS.md");
|
|
2293
2376
|
if (!existsSync7(agentsPath)) {
|
|
2294
|
-
const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) => existsSync7(
|
|
2377
|
+
const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) => existsSync7(join9(ctx.repoRoot, file)));
|
|
2295
2378
|
if (fallbackSources.length === 0) {
|
|
2296
2379
|
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
2380
|
}
|
|
@@ -2306,7 +2389,7 @@ var RULES = [
|
|
|
2306
2389
|
}
|
|
2307
2390
|
const details = [];
|
|
2308
2391
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
2309
|
-
const full =
|
|
2392
|
+
const full = join9(ctx.repoRoot, file);
|
|
2310
2393
|
const target = readSymlinkTarget(full);
|
|
2311
2394
|
if (target !== "AGENTS.md") details.push(`${file} should be a symlink to AGENTS.md`);
|
|
2312
2395
|
}
|
|
@@ -2330,7 +2413,7 @@ var RULES = [
|
|
|
2330
2413
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "AGENTS.md missing; cannot derive canonical agent file", changedFiles, details: [bootstrap.blocked] };
|
|
2331
2414
|
}
|
|
2332
2415
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
2333
|
-
const full =
|
|
2416
|
+
const full = join9(ctx.repoRoot, file);
|
|
2334
2417
|
const result = ensureSymlink(full, "AGENTS.md", ctx.dryRun);
|
|
2335
2418
|
if (result.blocked) blockedDetails.push(result.blocked);
|
|
2336
2419
|
if (result.changed) changedFiles.push(full);
|
|
@@ -2352,7 +2435,7 @@ var RULES = [
|
|
|
2352
2435
|
migrate: (ctx, finding) => {
|
|
2353
2436
|
const changedFiles = [];
|
|
2354
2437
|
const details = [];
|
|
2355
|
-
const path =
|
|
2438
|
+
const path = join9(ctx.repoRoot, ".project.json");
|
|
2356
2439
|
const existing = readProjectJson(ctx) ?? {};
|
|
2357
2440
|
const canonical = canonicalProjectJson(ctx);
|
|
2358
2441
|
const merged = { ...existing, ...canonical };
|
|
@@ -2362,7 +2445,7 @@ var RULES = [
|
|
|
2362
2445
|
changedFiles.push(path);
|
|
2363
2446
|
if (!ctx.dryRun) writeText(path, expected);
|
|
2364
2447
|
}
|
|
2365
|
-
const planeJson =
|
|
2448
|
+
const planeJson = join9(ctx.repoRoot, ".plane.json");
|
|
2366
2449
|
if (existsSync7(planeJson)) {
|
|
2367
2450
|
const backup = `${planeJson}.migrated-backup`;
|
|
2368
2451
|
if (existsSync7(backup)) {
|
|
@@ -2387,8 +2470,8 @@ var RULES = [
|
|
|
2387
2470
|
title: ".env.op + gitignore secrets contract",
|
|
2388
2471
|
audit: (ctx) => {
|
|
2389
2472
|
const details = [];
|
|
2390
|
-
const envOp = safeReadText(
|
|
2391
|
-
const gitignore = safeReadText(
|
|
2473
|
+
const envOp = safeReadText(join9(ctx.repoRoot, ".env.op"));
|
|
2474
|
+
const gitignore = safeReadText(join9(ctx.repoRoot, ".gitignore"));
|
|
2392
2475
|
if (!envOp) {
|
|
2393
2476
|
details.push(".env.op missing");
|
|
2394
2477
|
} else {
|
|
@@ -2414,12 +2497,12 @@ var RULES = [
|
|
|
2414
2497
|
migrate: (ctx, finding) => {
|
|
2415
2498
|
const changedFiles = [];
|
|
2416
2499
|
const details = [];
|
|
2417
|
-
const envOpPath =
|
|
2500
|
+
const envOpPath = join9(ctx.repoRoot, ".env.op");
|
|
2418
2501
|
if (!existsSync7(envOpPath)) {
|
|
2419
2502
|
changedFiles.push(envOpPath);
|
|
2420
|
-
if (!ctx.dryRun) writeText(envOpPath, readText(
|
|
2503
|
+
if (!ctx.dryRun) writeText(envOpPath, readText(join9(ctx.pjanglerRoot, "templates", "commonproject", "template", ".env.op")));
|
|
2421
2504
|
}
|
|
2422
|
-
const gitignorePath =
|
|
2505
|
+
const gitignorePath = join9(ctx.repoRoot, ".gitignore");
|
|
2423
2506
|
const gitignore = safeReadText(gitignorePath) ?? "";
|
|
2424
2507
|
const requiredBlock = `# Secrets \u2014 .env is materialized by \`op inject -i .env.op > .env\` on mise enter.
|
|
2425
2508
|
# NEVER commit it. .env.op holds only 1Password references or safe literals and IS committed.
|
|
@@ -2446,20 +2529,20 @@ var RULES = [
|
|
|
2446
2529
|
title: ".copier-answers.yml provenance + drift report",
|
|
2447
2530
|
audit: (ctx) => {
|
|
2448
2531
|
const details = [];
|
|
2449
|
-
const path =
|
|
2450
|
-
const
|
|
2532
|
+
const path = join9(ctx.repoRoot, ".copier-answers.yml");
|
|
2533
|
+
const text3 = safeReadText(path);
|
|
2451
2534
|
const project = readProjectJson(ctx);
|
|
2452
|
-
if (!
|
|
2535
|
+
if (!text3) {
|
|
2453
2536
|
details.push(".copier-answers.yml missing");
|
|
2454
2537
|
} else {
|
|
2455
|
-
if (!
|
|
2456
|
-
if (!
|
|
2538
|
+
if (!text3.startsWith("# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY")) details.push("missing Copier overwrite warning header");
|
|
2539
|
+
if (!text3.includes("_src_path:")) details.push("_src_path missing");
|
|
2457
2540
|
if (project?.project_name) {
|
|
2458
|
-
const nameMatch =
|
|
2541
|
+
const nameMatch = text3.match(/project_name:\s*(.+)/);
|
|
2459
2542
|
if (!nameMatch || nameMatch[1]?.trim() !== String(project.project_name)) details.push("project_name drift between .copier-answers.yml and .project.json");
|
|
2460
2543
|
}
|
|
2461
2544
|
if (project?.project_description) {
|
|
2462
|
-
const descMatch =
|
|
2545
|
+
const descMatch = text3.match(/project_description:\s*([\s\S]*?)(?=\n\w|$)/);
|
|
2463
2546
|
const yamlDesc = descMatch?.[1]?.replace(/\n\s+/g, " ").trim() ?? "";
|
|
2464
2547
|
if (yamlDesc !== String(project.project_description)) details.push("project_description drift between .copier-answers.yml and .project.json");
|
|
2465
2548
|
}
|
|
@@ -2476,16 +2559,16 @@ var RULES = [
|
|
|
2476
2559
|
migrate: (ctx, finding) => {
|
|
2477
2560
|
const changedFiles = [];
|
|
2478
2561
|
const project = canonicalProjectJson(ctx);
|
|
2479
|
-
const
|
|
2480
|
-
_src_path: ${
|
|
2562
|
+
const text3 = `# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
|
|
2563
|
+
_src_path: ${join9(ctx.pjanglerRoot, "templates", "commonproject")}
|
|
2481
2564
|
project_description: ${String(project.project_description)}
|
|
2482
2565
|
project_name: ${String(project.project_name)}
|
|
2483
2566
|
ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
2484
2567
|
`;
|
|
2485
|
-
const path =
|
|
2486
|
-
if (safeReadText(path) !==
|
|
2568
|
+
const path = join9(ctx.repoRoot, ".copier-answers.yml");
|
|
2569
|
+
if (safeReadText(path) !== text3) {
|
|
2487
2570
|
changedFiles.push(path);
|
|
2488
|
-
if (!ctx.dryRun) writeText(path,
|
|
2571
|
+
if (!ctx.dryRun) writeText(path, text3);
|
|
2489
2572
|
}
|
|
2490
2573
|
return {
|
|
2491
2574
|
id: finding.id,
|
|
@@ -2501,15 +2584,15 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
2501
2584
|
id: "bmad.scaffold",
|
|
2502
2585
|
title: "BMAD modules/docs scaffold",
|
|
2503
2586
|
audit: (ctx) => {
|
|
2504
|
-
const sourceRoot =
|
|
2505
|
-
const targetRoot =
|
|
2587
|
+
const sourceRoot = join9(ctx.pjanglerRoot, "templates", "commonproject", "_bmad");
|
|
2588
|
+
const targetRoot = join9(ctx.repoRoot, "_bmad");
|
|
2506
2589
|
const sentinels = [
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2590
|
+
join9("core", "config.yaml"),
|
|
2591
|
+
join9("custom", "config.yaml"),
|
|
2592
|
+
join9("custom", "workflows", "ticket-lifecycle", "workflow.yaml"),
|
|
2593
|
+
join9("bmm", "workflows", "workflow-status", "workflow.yaml")
|
|
2511
2594
|
];
|
|
2512
|
-
const missing = sentinels.filter((file) => existsSync7(
|
|
2595
|
+
const missing = sentinels.filter((file) => existsSync7(join9(sourceRoot, file)) && !existsSync7(join9(targetRoot, file)));
|
|
2513
2596
|
return {
|
|
2514
2597
|
id: "bmad.scaffold",
|
|
2515
2598
|
title: "BMAD modules/docs scaffold",
|
|
@@ -2521,7 +2604,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
2521
2604
|
},
|
|
2522
2605
|
migrate: (ctx, finding) => {
|
|
2523
2606
|
const changedFiles = [];
|
|
2524
|
-
copyMissingRecursive(
|
|
2607
|
+
copyMissingRecursive(join9(ctx.pjanglerRoot, "templates", "commonproject", "_bmad"), join9(ctx.repoRoot, "_bmad"), changedFiles, ctx.dryRun);
|
|
2525
2608
|
return {
|
|
2526
2609
|
id: finding.id,
|
|
2527
2610
|
title: finding.title,
|
|
@@ -2543,11 +2626,11 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
2543
2626
|
}
|
|
2544
2627
|
const details = [];
|
|
2545
2628
|
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(
|
|
2629
|
+
if (!existsSync7(join9(role.roleDir, rel))) details.push(`missing ${relative(ctx.repoRoot, join9(role.roleDir, rel))}`);
|
|
2547
2630
|
}
|
|
2548
|
-
const gitmodules = safeReadText(
|
|
2631
|
+
const gitmodules = safeReadText(join9(ctx.repoRoot, ".gitmodules")) ?? "";
|
|
2549
2632
|
if (!gitmodules.includes(`agents/hermes/${role.role}/runtime`)) details.push(".gitmodules missing pm runtime submodule entry");
|
|
2550
|
-
if (!profileMetaInheritsDefault(
|
|
2633
|
+
if (!profileMetaInheritsDefault(join9(role.roleDir, "runtime", "profile.yaml"))) {
|
|
2551
2634
|
details.push("runtime/profile.yaml missing inherited default config metadata");
|
|
2552
2635
|
}
|
|
2553
2636
|
const registry = safeReadText(registryPath(ctx.homeDir));
|
|
@@ -2568,21 +2651,21 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
2568
2651
|
if (!role) {
|
|
2569
2652
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "No pm role present", changedFiles, details: [] };
|
|
2570
2653
|
}
|
|
2571
|
-
const templateRoleDir =
|
|
2572
|
-
writeIfDifferent(
|
|
2573
|
-
writeIfDifferent(
|
|
2574
|
-
writeIfDifferent(
|
|
2575
|
-
copyMissingRecursive(
|
|
2576
|
-
copyMissingRecursive(
|
|
2577
|
-
copyMissingRecursive(
|
|
2578
|
-
const promptSource =
|
|
2579
|
-
const promptTarget =
|
|
2654
|
+
const templateRoleDir = join9(ctx.pjanglerRoot, "templates", "hermes-agent", "template");
|
|
2655
|
+
writeIfDifferent(join9(role.roleDir, "SOUL.md"), renderSoul(role), ctx.dryRun, changedFiles);
|
|
2656
|
+
writeIfDifferent(join9(role.roleDir, "hermes"), renderHermesWrapper(role), ctx.dryRun, changedFiles, 493);
|
|
2657
|
+
writeIfDifferent(join9(role.roleDir, ".gitignore"), readText(join9(templateRoleDir, ".gitignore.jinja")).replace(/\{\{ role \}\}/g, role.role), ctx.dryRun, changedFiles);
|
|
2658
|
+
copyMissingRecursive(join9(templateRoleDir, ".runtime-scaffold"), join9(role.roleDir, ".runtime-scaffold"), changedFiles, ctx.dryRun);
|
|
2659
|
+
copyMissingRecursive(join9(templateRoleDir, ".runtime-scaffold"), join9(role.roleDir, "runtime"), changedFiles, ctx.dryRun);
|
|
2660
|
+
copyMissingRecursive(join9(templateRoleDir, ".scripts"), join9(role.roleDir, ".scripts"), changedFiles, ctx.dryRun, (source) => source.endsWith("sentinel.prompt.md.jinja"));
|
|
2661
|
+
const promptSource = join9(templateRoleDir, ".scripts", "sentinel.prompt.md.jinja");
|
|
2662
|
+
const promptTarget = join9(role.roleDir, ".scripts", "sentinel.prompt.md");
|
|
2580
2663
|
if (existsSync7(promptSource) && !existsSync7(promptTarget)) {
|
|
2581
2664
|
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
2665
|
writeIfDifferent(promptTarget, prompt, ctx.dryRun, changedFiles);
|
|
2583
2666
|
}
|
|
2584
2667
|
upsertSubmodule(ctx.repoRoot, role, changedFiles, ctx.dryRun);
|
|
2585
|
-
const profileMetaUpdated = upsertInheritedProfileMeta(
|
|
2668
|
+
const profileMetaUpdated = upsertInheritedProfileMeta(join9(role.roleDir, "runtime", "profile.yaml"), changedFiles, ctx.dryRun);
|
|
2586
2669
|
if (profileMetaUpdated) details.push(`updated ${profileMetaUpdated}`);
|
|
2587
2670
|
const registryUpdated = upsertRegistryEntry(role, ctx.homeDir, changedFiles, ctx.dryRun);
|
|
2588
2671
|
if (registryUpdated) details.push(`updated ${registryUpdated}`);
|
|
@@ -2636,9 +2719,9 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
2636
2719
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "systemd --user unavailable on this host", changedFiles, details };
|
|
2637
2720
|
}
|
|
2638
2721
|
for (const role of roles) {
|
|
2639
|
-
const sysDir =
|
|
2722
|
+
const sysDir = join9(ctx.homeDir, ".config", "systemd", "user");
|
|
2640
2723
|
const units = [`hermes-${role.agentId}-gateway.service`, `hermes-${role.agentId}-consumer.service`, `hermes-${role.agentId}-heartbeat.timer`];
|
|
2641
|
-
const allUnitsPresent = units.every((unit) => existsSync7(
|
|
2724
|
+
const allUnitsPresent = units.every((unit) => existsSync7(join9(sysDir, unit)));
|
|
2642
2725
|
if (allUnitsPresent) {
|
|
2643
2726
|
if (ctx.dryRun) {
|
|
2644
2727
|
details.push(`would run: systemctl --user enable --now ${units.join(" ")}`);
|
|
@@ -2650,7 +2733,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
2650
2733
|
}
|
|
2651
2734
|
continue;
|
|
2652
2735
|
}
|
|
2653
|
-
for (const script of [
|
|
2736
|
+
for (const script of [join9(role.roleDir, ".scripts", "70-systemd.sh")]) {
|
|
2654
2737
|
if (!script || !existsSync7(script)) continue;
|
|
2655
2738
|
if (ctx.dryRun) {
|
|
2656
2739
|
details.push(`would run: bash ${script}`);
|
|
@@ -2678,7 +2761,7 @@ function writeIfDifferent(path, content, dryRun, changedFiles, mode) {
|
|
|
2678
2761
|
changedFiles.push(path);
|
|
2679
2762
|
if (!dryRun) {
|
|
2680
2763
|
writeText(path, normalized);
|
|
2681
|
-
if (mode)
|
|
2764
|
+
if (mode) chmodSync2(path, mode);
|
|
2682
2765
|
}
|
|
2683
2766
|
}
|
|
2684
2767
|
function getParityRuleIds() {
|
|
@@ -2740,35 +2823,60 @@ function runMigration(selector, repoArg, dryRun, all) {
|
|
|
2740
2823
|
const ruleIds = all ? RULES.map((rule) => rule.id) : selector ? [selector] : [];
|
|
2741
2824
|
return runMigrationForRules(ruleIds, repoArg, dryRun);
|
|
2742
2825
|
}
|
|
2826
|
+
function prettyTimestamp(iso) {
|
|
2827
|
+
const match = /^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})/.exec(iso);
|
|
2828
|
+
return match ? `${match[1]} ${match[2]} UTC` : iso;
|
|
2829
|
+
}
|
|
2743
2830
|
function formatAuditReport(report) {
|
|
2744
|
-
const
|
|
2831
|
+
const counts = {};
|
|
2832
|
+
for (const rule of report.rules) counts[rule.status] = (counts[rule.status] ?? 0) + 1;
|
|
2833
|
+
const idWidth = report.rules.reduce((width, rule) => Math.max(width, rule.id.length), 0);
|
|
2834
|
+
const tally = [];
|
|
2835
|
+
if (counts.pass) tally.push(green(`${counts.pass} passed`));
|
|
2836
|
+
if (counts.fail) tally.push(red(`${counts.fail} failed`));
|
|
2837
|
+
if (counts.warn) tally.push(yellow(`${counts.warn} warning${counts.warn === 1 ? "" : "s"}`));
|
|
2838
|
+
if (counts.skip) tally.push(gray(`${counts.skip} skipped`));
|
|
2839
|
+
const overall = report.ok ? `${green(glyph.pass)} ${bold("Parity audit passed")}` : `${red(glyph.fail)} ${bold("Parity audit failed")}`;
|
|
2840
|
+
const lines = [""];
|
|
2841
|
+
lines.push(` ${overall}${tally.length ? ` ${dim(glyph.dot)} ${joinDot(tally)}` : ""}`);
|
|
2842
|
+
lines.push(` ${dim(report.repo)} ${dim(glyph.dot)} ${dim(prettyTimestamp(report.auditedAt))}`);
|
|
2843
|
+
lines.push("");
|
|
2745
2844
|
for (const rule of report.rules) {
|
|
2746
|
-
|
|
2747
|
-
|
|
2845
|
+
const style = statusStyle(rule.status);
|
|
2846
|
+
lines.push(` ${style.color(style.glyph)} ${style.color(rule.id.padEnd(idWidth))} ${rule.summary}`);
|
|
2847
|
+
for (const detail of rule.details) lines.push(` ${dim(glyph.arrow)} ${dim(detail)}`);
|
|
2748
2848
|
}
|
|
2749
|
-
|
|
2750
|
-
|
|
2849
|
+
lines.push("");
|
|
2850
|
+
return lines.join("\n");
|
|
2751
2851
|
}
|
|
2752
2852
|
function formatMigrationReport(report) {
|
|
2753
|
-
const
|
|
2853
|
+
const idWidth = report.results.reduce((width, result) => Math.max(width, result.id.length), 0);
|
|
2854
|
+
const overall = report.ok ? `${green(glyph.pass)} ${bold(report.dryRun ? "Migration preview complete" : "Migration complete")}` : `${red(glyph.fail)} ${bold("Migration finished with blockers")}`;
|
|
2855
|
+
const lines = [""];
|
|
2856
|
+
lines.push(` ${overall}${report.dryRun ? ` ${dim(glyph.dot)} ${yellow("dry run")}` : ""}`);
|
|
2857
|
+
lines.push(` ${dim(report.repo)}`);
|
|
2858
|
+
if (report.selectedRules.length) lines.push(` ${dim(`rules: ${report.selectedRules.join(", ")}`)}`);
|
|
2859
|
+
lines.push("");
|
|
2754
2860
|
for (const result of report.results) {
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
for (const
|
|
2861
|
+
const style = statusStyle(result.status);
|
|
2862
|
+
lines.push(` ${style.color(style.glyph)} ${style.color(result.id.padEnd(idWidth))} ${result.summary} ${dim(`[${style.label}]`)}`);
|
|
2863
|
+
for (const detail of result.details) lines.push(` ${dim(glyph.arrow)} ${dim(detail)}`);
|
|
2864
|
+
for (const file of result.changedFiles) lines.push(` ${green(glyph.add)} ${file}`);
|
|
2758
2865
|
}
|
|
2759
2866
|
if (report.changedFiles.length) {
|
|
2760
|
-
lines.push("
|
|
2761
|
-
|
|
2867
|
+
lines.push("");
|
|
2868
|
+
lines.push(` ${bold(`Changed files (${report.changedFiles.length})`)}`);
|
|
2869
|
+
for (const file of report.changedFiles) lines.push(` ${green(glyph.add)} ${file}`);
|
|
2762
2870
|
}
|
|
2763
|
-
|
|
2764
|
-
|
|
2871
|
+
lines.push("");
|
|
2872
|
+
return lines.join("\n");
|
|
2765
2873
|
}
|
|
2766
2874
|
|
|
2767
2875
|
// src/project/index.ts
|
|
2768
2876
|
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
2769
2877
|
import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync4, renameSync as renameSync2, statSync, writeFileSync as writeFileSync5 } from "node:fs";
|
|
2770
2878
|
import { homedir as homedir5 } from "node:os";
|
|
2771
|
-
import { basename as basename3, dirname as dirname6, join as
|
|
2879
|
+
import { basename as basename3, dirname as dirname6, join as join10, resolve as resolve2 } from "node:path";
|
|
2772
2880
|
import YAML from "yaml";
|
|
2773
2881
|
var PROJECT_REGISTRY_ENV = "PJ_PROJECT_REGISTRY";
|
|
2774
2882
|
var PROJECT_REGISTRY_SCHEMA_VERSION = 1;
|
|
@@ -2776,10 +2884,10 @@ var KNOWN_SKILL_ROOTS = [
|
|
|
2776
2884
|
"/home/delorenj/code/skillex/all-skills",
|
|
2777
2885
|
"/home/delorenj/code/CoachingAgentFramework/.agents/skills",
|
|
2778
2886
|
"/home/delorenj/code/pjangler/.agents/skills",
|
|
2779
|
-
|
|
2887
|
+
join10(homedir5(), ".codex", "skills")
|
|
2780
2888
|
];
|
|
2781
|
-
function projectRegistryPath(
|
|
2782
|
-
return expandHome(
|
|
2889
|
+
function projectRegistryPath(env2 = process.env) {
|
|
2890
|
+
return expandHome(env2[PROJECT_REGISTRY_ENV] || join10(homedir5(), ".config", "pjangler", "projects.yaml"));
|
|
2783
2891
|
}
|
|
2784
2892
|
function emptyProjectRegistry() {
|
|
2785
2893
|
return { schema_version: PROJECT_REGISTRY_SCHEMA_VERSION, projects: {} };
|
|
@@ -2863,7 +2971,7 @@ function resolveSourceSkillPath(sourceSkill) {
|
|
|
2863
2971
|
if (existsSync8(direct)) return direct;
|
|
2864
2972
|
const name = basename3(sourceSkill);
|
|
2865
2973
|
for (const root of KNOWN_SKILL_ROOTS) {
|
|
2866
|
-
const candidate =
|
|
2974
|
+
const candidate = join10(root, name);
|
|
2867
2975
|
if (existsSync8(candidate)) return candidate;
|
|
2868
2976
|
}
|
|
2869
2977
|
const civilWarLetterifier = "/home/delorenj/code/skillex/all-skills/civilwar-letterifier";
|
|
@@ -2943,7 +3051,7 @@ function planProjectInit(input) {
|
|
|
2943
3051
|
}));
|
|
2944
3052
|
}
|
|
2945
3053
|
actions.push(
|
|
2946
|
-
{ kind: "project.write-manifest", path:
|
|
3054
|
+
{ kind: "project.write-manifest", path: join10(targetDir, ".project.json"), manifest },
|
|
2947
3055
|
{
|
|
2948
3056
|
kind: "plane.create-or-link",
|
|
2949
3057
|
enabled: live,
|
|
@@ -3050,24 +3158,40 @@ function projectManifestFromRegistryProject(project) {
|
|
|
3050
3158
|
};
|
|
3051
3159
|
}
|
|
3052
3160
|
function formatProjectInitPlan(plan) {
|
|
3053
|
-
const lines = [
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3161
|
+
const lines = [""];
|
|
3162
|
+
const title = `${bold(plan.project.name)} ${dim(`(${plan.project.slug})`)}`;
|
|
3163
|
+
lines.push(` ${cyan(bold(glyph.chevron))} ${title}${plan.dryRun ? ` ${dim(glyph.dot)} ${yellow("dry run")}` : ""}`);
|
|
3164
|
+
lines.push(` ${dim("registry".padEnd(8))} ${dim(plan.registryPath)}`);
|
|
3165
|
+
lines.push(` ${dim("target".padEnd(8))} ${dim(plan.project.repo_path)}`);
|
|
3166
|
+
lines.push("");
|
|
3167
|
+
lines.push(` ${bold("Actions")} ${dim(`(${plan.actions.length})`)}`);
|
|
3168
|
+
if (!plan.actions.length) lines.push(` ${dim("(nothing to do)")}`);
|
|
3059
3169
|
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(`
|
|
3170
|
+
lines.push(` ${cyan(glyph.bullet)} ${action.kind}`);
|
|
3171
|
+
if (action.kind === "copier.copy.commonproject") lines.push(` ${dim(`target: ${action.targetDir}`)}`);
|
|
3172
|
+
if (action.kind === "project.write-manifest") lines.push(` ${dim(`path: ${action.path}`)}`);
|
|
3173
|
+
if (action.kind === "plane.create-or-link" && action.reason) lines.push(` ${dim(`note: ${action.reason}`)}`);
|
|
3064
3174
|
}
|
|
3175
|
+
lines.push("");
|
|
3065
3176
|
return lines.join("\n");
|
|
3066
3177
|
}
|
|
3067
3178
|
function formatProjectList(registry) {
|
|
3068
3179
|
const projects = Object.values(registry.projects).sort((a, b) => a.slug.localeCompare(b.slug));
|
|
3069
|
-
if (!projects.length) return
|
|
3070
|
-
|
|
3180
|
+
if (!projects.length) return `
|
|
3181
|
+
${dim("No projects registered.")}
|
|
3182
|
+
`;
|
|
3183
|
+
const slugWidth = projects.reduce((width, project) => Math.max(width, project.slug.length), 0);
|
|
3184
|
+
const idWidth = projects.reduce((width, project) => Math.max(width, String(project.ticket_provider.identifier ?? "").length), 0);
|
|
3185
|
+
const statusWidth = projects.reduce((width, project) => Math.max(width, project.status.length), 0);
|
|
3186
|
+
const lines = ["", ` ${bold("Projects")} ${dim(`(${projects.length})`)}`, ""];
|
|
3187
|
+
for (const project of projects) {
|
|
3188
|
+
const slug = bold(project.slug.padEnd(slugWidth));
|
|
3189
|
+
const identifier = cyan(String(project.ticket_provider.identifier ?? "").padEnd(idWidth));
|
|
3190
|
+
const status = projectStatusColor(project.status)(project.status.padEnd(statusWidth));
|
|
3191
|
+
lines.push(` ${slug} ${identifier} ${status} ${dim(project.repo_path)}`);
|
|
3192
|
+
}
|
|
3193
|
+
lines.push("");
|
|
3194
|
+
return lines.join("\n");
|
|
3071
3195
|
}
|
|
3072
3196
|
function getProject(registry, slug) {
|
|
3073
3197
|
const project = registry.projects[slug];
|
|
@@ -3084,7 +3208,7 @@ function doctorProjectRegistry(registryPath2 = projectRegistryPath(), slug) {
|
|
|
3084
3208
|
} else if (!statSync(project.repo_path).isDirectory()) {
|
|
3085
3209
|
issues.push({ level: "error", slug: projectSlug, message: `repo_path is not a directory: ${project.repo_path}` });
|
|
3086
3210
|
} else {
|
|
3087
|
-
const manifestPath =
|
|
3211
|
+
const manifestPath = join10(project.repo_path, ".project.json");
|
|
3088
3212
|
if (!existsSync8(manifestPath)) issues.push({ level: "warn", slug: projectSlug, message: ".project.json is missing" });
|
|
3089
3213
|
}
|
|
3090
3214
|
for (const artifact of project.source_artifacts) {
|
|
@@ -3101,7 +3225,7 @@ function doctorProjectRegistry(registryPath2 = projectRegistryPath(), slug) {
|
|
|
3101
3225
|
};
|
|
3102
3226
|
}
|
|
3103
3227
|
function buildCommonProjectCopierAction(input) {
|
|
3104
|
-
const templateDir =
|
|
3228
|
+
const templateDir = join10(input.pjanglerRoot, "templates", "commonproject");
|
|
3105
3229
|
const data = {
|
|
3106
3230
|
project_name: input.projectName,
|
|
3107
3231
|
project_description: input.projectDescription ?? "",
|
|
@@ -3127,7 +3251,7 @@ function buildCommonProjectCopierAction(input) {
|
|
|
3127
3251
|
function resolvePjanglerRoot2() {
|
|
3128
3252
|
let dir = dirname6(new URL(import.meta.url).pathname);
|
|
3129
3253
|
while (dir !== dirname6(dir)) {
|
|
3130
|
-
if (existsSync8(
|
|
3254
|
+
if (existsSync8(join10(dir, "package.json")) && existsSync8(join10(dir, "templates", "commonproject", "copier.yml"))) return dir;
|
|
3131
3255
|
dir = dirname6(dir);
|
|
3132
3256
|
}
|
|
3133
3257
|
return resolve2(process.cwd());
|
|
@@ -3159,7 +3283,7 @@ function validateProjectRecord(project, key) {
|
|
|
3159
3283
|
}
|
|
3160
3284
|
function expandHome(path) {
|
|
3161
3285
|
if (path === "~") return homedir5();
|
|
3162
|
-
if (path.startsWith("~/")) return
|
|
3286
|
+
if (path.startsWith("~/")) return join10(homedir5(), path.slice(2));
|
|
3163
3287
|
return path;
|
|
3164
3288
|
}
|
|
3165
3289
|
function isRecord(value) {
|
|
@@ -3168,14 +3292,14 @@ function isRecord(value) {
|
|
|
3168
3292
|
|
|
3169
3293
|
// src/utils/version.ts
|
|
3170
3294
|
import { readFileSync as readFileSync5 } from "node:fs";
|
|
3171
|
-
import { dirname as dirname7, join as
|
|
3295
|
+
import { dirname as dirname7, join as join11 } from "node:path";
|
|
3172
3296
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
3173
3297
|
var PJANGLER_VERSION = (() => {
|
|
3174
3298
|
try {
|
|
3175
3299
|
let dir = dirname7(fileURLToPath4(import.meta.url));
|
|
3176
3300
|
for (let i = 0; i < 4; i++) {
|
|
3177
3301
|
try {
|
|
3178
|
-
const raw = readFileSync5(
|
|
3302
|
+
const raw = readFileSync5(join11(dir, "package.json"), "utf8");
|
|
3179
3303
|
return JSON.parse(raw).version ?? "0.0.0";
|
|
3180
3304
|
} catch {
|
|
3181
3305
|
const parent = dirname7(dir);
|
|
@@ -3189,6 +3313,7 @@ var PJANGLER_VERSION = (() => {
|
|
|
3189
3313
|
})();
|
|
3190
3314
|
|
|
3191
3315
|
// src/index.ts
|
|
3316
|
+
var xmark = `${red(glyph.fail)}`;
|
|
3192
3317
|
function printMigrationReport(report, asJson) {
|
|
3193
3318
|
if (asJson) {
|
|
3194
3319
|
console.log(JSON.stringify(report, null, 2));
|
|
@@ -3236,8 +3361,8 @@ function packageNameToProjectName(value) {
|
|
|
3236
3361
|
return name.replace(/[-_]+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase()).trim();
|
|
3237
3362
|
}
|
|
3238
3363
|
function deriveProjectDefaults(targetDir) {
|
|
3239
|
-
const manifest = readJson(
|
|
3240
|
-
const pkg = readJson(
|
|
3364
|
+
const manifest = readJson(join12(targetDir, ".project.json"));
|
|
3365
|
+
const pkg = readJson(join12(targetDir, "package.json"));
|
|
3241
3366
|
const name = String(manifest?.project_name ?? "").trim() || packageNameToProjectName(typeof pkg?.name === "string" ? pkg.name : void 0) || packageNameToProjectName(basename4(targetDir)) || "Project";
|
|
3242
3367
|
const ticketProvider = manifest?.ticket_provider && typeof manifest.ticket_provider === "object" ? manifest.ticket_provider : {};
|
|
3243
3368
|
return {
|
|
@@ -3251,7 +3376,7 @@ function isInteractiveProjectInit(options) {
|
|
|
3251
3376
|
return !options.json && !options.yes && options.tui !== false && Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
3252
3377
|
}
|
|
3253
3378
|
async function promptTextValue(message, initialValue) {
|
|
3254
|
-
const value = await
|
|
3379
|
+
const value = await text2({
|
|
3255
3380
|
message,
|
|
3256
3381
|
initialValue,
|
|
3257
3382
|
validate: (input) => input?.trim() ? void 0 : "Required"
|
|
@@ -3348,7 +3473,7 @@ async function resolveProjectInitTarget(name, options) {
|
|
|
3348
3473
|
if (!targetDir && interactive) {
|
|
3349
3474
|
const defaultName = name ?? basename4(cwd);
|
|
3350
3475
|
const promptedName = name ?? await promptTextValue("Project name", packageNameToProjectName(defaultName));
|
|
3351
|
-
const defaultDir =
|
|
3476
|
+
const defaultDir = join12(cwd, promptedName.replace(/[^A-Za-z0-9._-]/g, "") || promptedName.toLowerCase().replace(/[^a-z0-9]+/g, "-"));
|
|
3352
3477
|
targetDir = await promptTextValue("Project directory", defaultDir);
|
|
3353
3478
|
name = promptedName;
|
|
3354
3479
|
}
|
|
@@ -3384,27 +3509,30 @@ program.command("init").argument("<subsystem>", "Subsystem to initialize").descr
|
|
|
3384
3509
|
try {
|
|
3385
3510
|
const recipe = createRecipe(subsystem, context);
|
|
3386
3511
|
if (!recipe) {
|
|
3387
|
-
console.error(
|
|
3388
|
-
console.
|
|
3512
|
+
console.error(`${xmark} Unknown subsystem: ${bold(subsystem)}`);
|
|
3513
|
+
console.error(` ${dim("Available:")} ${getRecipeNames().map((available) => cyan(available)).join(dim(", "))}`);
|
|
3389
3514
|
process.exit(1);
|
|
3390
3515
|
}
|
|
3391
3516
|
await recipe.execute();
|
|
3392
3517
|
} catch (error) {
|
|
3393
|
-
console.error(
|
|
3518
|
+
console.error(`${xmark} Error initializing ${bold(subsystem)}:`, error);
|
|
3394
3519
|
process.exit(1);
|
|
3395
3520
|
}
|
|
3396
3521
|
});
|
|
3397
3522
|
program.command("list").description("List available subsystems").action(() => {
|
|
3398
|
-
|
|
3523
|
+
const width = Object.keys(RECIPE_REGISTRY).reduce((max, name) => Math.max(max, name.length), 0);
|
|
3524
|
+
console.log("");
|
|
3525
|
+
console.log(` ${heading("Available subsystems")}`);
|
|
3399
3526
|
console.log("");
|
|
3400
3527
|
for (const [name, info] of Object.entries(RECIPE_REGISTRY)) {
|
|
3401
|
-
console.log(` ${name.padEnd(
|
|
3528
|
+
console.log(` ${cyan(name.padEnd(width))} ${dim(info.description)}`);
|
|
3529
|
+
}
|
|
3530
|
+
console.log("");
|
|
3531
|
+
console.log(` ${dim("Examples")}`);
|
|
3532
|
+
for (const example of ["pj init mise", "pj init docker", "pj init node"]) {
|
|
3533
|
+
console.log(` ${dim(glyph.pointer)} ${dim(example)}`);
|
|
3402
3534
|
}
|
|
3403
3535
|
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
3536
|
});
|
|
3409
3537
|
var projectCmd = program.command("project").description("Manage the pjangler project registry");
|
|
3410
3538
|
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 +3586,12 @@ projectCmd.command("init").argument("[name]", "Project display name").descriptio
|
|
|
3458
3586
|
else {
|
|
3459
3587
|
console.log(formatProjectInitPlan(plan));
|
|
3460
3588
|
if (payload.proposedOperations.length) {
|
|
3461
|
-
console.log("Proposed
|
|
3462
|
-
for (const operation of payload.proposedOperations) console.log(`
|
|
3589
|
+
console.log(` ${bold("Proposed operations")} ${dim(`(${payload.proposedOperations.length})`)}`);
|
|
3590
|
+
for (const operation of payload.proposedOperations) console.log(` ${cyan(glyph.bullet)} ${operation}`);
|
|
3463
3591
|
} else {
|
|
3464
|
-
console.log("Project is already in parity.");
|
|
3592
|
+
console.log(` ${green(glyph.pass)} ${dim("Project is already in parity.")}`);
|
|
3465
3593
|
}
|
|
3594
|
+
console.log("");
|
|
3466
3595
|
}
|
|
3467
3596
|
return;
|
|
3468
3597
|
}
|
|
@@ -3490,17 +3619,19 @@ projectCmd.command("init").argument("[name]", "Project display name").descriptio
|
|
|
3490
3619
|
} else {
|
|
3491
3620
|
console.log(formatProjectInitPlan(selectedPlan));
|
|
3492
3621
|
for (const line of result.logs) console.log(line);
|
|
3493
|
-
for (const line of result.errors) console.error(line);
|
|
3622
|
+
for (const line of result.errors) console.error(` ${xmark} ${line}`);
|
|
3494
3623
|
if (migrationReport) console.log(formatMigrationReport(migrationReport));
|
|
3495
|
-
if (result.ok && changedFiles.length) console.log(`Project synchronized
|
|
3496
|
-
|
|
3624
|
+
if (result.ok && changedFiles.length) console.log(` ${green(glyph.pass)} ${bold("Project synchronized")} ${dim(glyph.dot)} ${cyan(plan.project.slug)}
|
|
3625
|
+
`);
|
|
3626
|
+
if (result.ok && changedFiles.length === 0) console.log(` ${green(glyph.pass)} ${dim("Already in parity")} ${dim(glyph.dot)} ${cyan(plan.project.slug)}
|
|
3627
|
+
`);
|
|
3497
3628
|
}
|
|
3498
3629
|
process.exitCode = result.ok ? 0 : 1;
|
|
3499
3630
|
} catch (err) {
|
|
3500
3631
|
if (options.json) {
|
|
3501
3632
|
console.log(JSON.stringify({ ok: false, error: err instanceof Error ? err.message : String(err) }, null, 2));
|
|
3502
3633
|
} else {
|
|
3503
|
-
console.error(
|
|
3634
|
+
console.error(`${xmark} project init failed:`, err instanceof Error ? err.message : err);
|
|
3504
3635
|
}
|
|
3505
3636
|
process.exit(1);
|
|
3506
3637
|
}
|
|
@@ -3511,19 +3642,24 @@ projectCmd.command("list").description("List projects in the pjangler registry")
|
|
|
3511
3642
|
if (options.json) console.log(JSON.stringify(registry, null, 2));
|
|
3512
3643
|
else console.log(formatProjectList(registry));
|
|
3513
3644
|
} catch (err) {
|
|
3514
|
-
console.error(
|
|
3645
|
+
console.error(`${xmark} project list failed:`, err instanceof Error ? err.message : err);
|
|
3515
3646
|
process.exit(1);
|
|
3516
3647
|
}
|
|
3517
3648
|
});
|
|
3518
3649
|
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
3650
|
try {
|
|
3520
3651
|
const project = getProject(loadProjectRegistry(options.registry ?? projectRegistryPath()), slug);
|
|
3521
|
-
if (options.json)
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3652
|
+
if (options.json) {
|
|
3653
|
+
console.log(JSON.stringify(project, null, 2));
|
|
3654
|
+
} else {
|
|
3655
|
+
console.log("");
|
|
3656
|
+
console.log(` ${heading(project.name)} ${dim(`(${project.slug})`)}`);
|
|
3657
|
+
console.log(` ${dim(project.repo_path)}`);
|
|
3658
|
+
if (project.description) console.log(` ${project.description}`);
|
|
3659
|
+
console.log("");
|
|
3660
|
+
}
|
|
3525
3661
|
} catch (err) {
|
|
3526
|
-
console.error(
|
|
3662
|
+
console.error(`${xmark} project show failed:`, err instanceof Error ? err.message : err);
|
|
3527
3663
|
process.exit(1);
|
|
3528
3664
|
}
|
|
3529
3665
|
});
|
|
@@ -3533,50 +3669,59 @@ projectCmd.command("doctor").argument("[slug]", "Optional project slug").descrip
|
|
|
3533
3669
|
if (options.json) {
|
|
3534
3670
|
console.log(JSON.stringify(report, null, 2));
|
|
3535
3671
|
} else if (!report.issues.length) {
|
|
3536
|
-
console.log(
|
|
3672
|
+
console.log("");
|
|
3673
|
+
console.log(` ${green(glyph.pass)} ${bold("Project registry OK")} ${dim(glyph.dot)} ${dim(report.registryPath)}`);
|
|
3674
|
+
console.log("");
|
|
3537
3675
|
} else {
|
|
3538
|
-
console.log(
|
|
3539
|
-
|
|
3676
|
+
console.log("");
|
|
3677
|
+
console.log(` ${red(glyph.fail)} ${bold("Project registry issues")} ${dim(glyph.dot)} ${dim(report.registryPath)}`);
|
|
3678
|
+
console.log("");
|
|
3679
|
+
for (const issue of report.issues) {
|
|
3680
|
+
const mark = issue.level === "error" ? red(glyph.fail) : yellow(glyph.warn);
|
|
3681
|
+
console.log(` ${mark} ${bold(issue.slug ?? "registry")} ${issue.message}`);
|
|
3682
|
+
}
|
|
3683
|
+
console.log("");
|
|
3540
3684
|
}
|
|
3541
3685
|
process.exit(report.ok ? 0 : 1);
|
|
3542
3686
|
} catch (err) {
|
|
3543
|
-
console.error(
|
|
3687
|
+
console.error(`${xmark} project doctor failed:`, err instanceof Error ? err.message : err);
|
|
3544
3688
|
process.exit(1);
|
|
3545
3689
|
}
|
|
3546
3690
|
});
|
|
3547
3691
|
var recipeCmd = program.command("recipe").description("Manage pjangler recipes");
|
|
3548
3692
|
recipeCmd.command("list").description("List all available recipes").action(() => {
|
|
3549
|
-
console.log("
|
|
3693
|
+
console.log("");
|
|
3694
|
+
console.log(` ${heading("Recipes")}`);
|
|
3550
3695
|
console.log("");
|
|
3551
3696
|
for (const [name, info] of Object.entries(RECIPE_REGISTRY)) {
|
|
3552
|
-
console.log(` ${name}`);
|
|
3553
|
-
console.log(`
|
|
3554
|
-
console.log(`
|
|
3697
|
+
console.log(` ${cyan(bold(name))}`);
|
|
3698
|
+
console.log(` ${dim(info.description)}`);
|
|
3699
|
+
console.log(` ${dim("commands")} ${info.commands.map((command) => cyan(command)).join(dim(", "))}`);
|
|
3555
3700
|
console.log("");
|
|
3556
3701
|
}
|
|
3557
|
-
console.log("Usage
|
|
3558
|
-
console.log("
|
|
3559
|
-
console.log("
|
|
3702
|
+
console.log(` ${dim("Usage")}`);
|
|
3703
|
+
console.log(` ${dim(glyph.pointer)} ${dim("pj recipe run <name>")}`);
|
|
3704
|
+
console.log(` ${dim(glyph.pointer)} ${dim("pj recipe describe <name>")}`);
|
|
3705
|
+
console.log("");
|
|
3560
3706
|
});
|
|
3561
3707
|
recipeCmd.command("describe").argument("<name>", "Recipe name").description("Show detailed information about a recipe").action((name) => {
|
|
3562
3708
|
const info = getRecipeInfo(name);
|
|
3563
3709
|
if (!info) {
|
|
3564
|
-
console.error(
|
|
3565
|
-
console.
|
|
3710
|
+
console.error(`${xmark} Recipe not found: ${bold(name)}`);
|
|
3711
|
+
console.error(` ${dim("Available:")} ${getRecipeNames().map((available) => cyan(available)).join(dim(", "))}`);
|
|
3566
3712
|
process.exit(1);
|
|
3567
3713
|
}
|
|
3568
|
-
console.log(`\u{1F4E6} Recipe: ${info.name}`);
|
|
3569
3714
|
console.log("");
|
|
3570
|
-
console.log(`
|
|
3715
|
+
console.log(` ${heading(info.name)}`);
|
|
3716
|
+
console.log(` ${dim(info.description)}`);
|
|
3571
3717
|
console.log("");
|
|
3572
|
-
console.log("Commands
|
|
3573
|
-
for (const
|
|
3574
|
-
|
|
3575
|
-
}
|
|
3718
|
+
console.log(` ${bold("Commands")}`);
|
|
3719
|
+
for (const command of info.commands) console.log(` ${cyan(glyph.bullet)} ${command}`);
|
|
3720
|
+
console.log("");
|
|
3721
|
+
console.log(` ${dim("Usage")}`);
|
|
3722
|
+
console.log(` ${dim(glyph.pointer)} ${dim(`pj recipe run ${name}`)}`);
|
|
3723
|
+
console.log(` ${dim(glyph.pointer)} ${dim(`pj init ${name}`)}`);
|
|
3576
3724
|
console.log("");
|
|
3577
|
-
console.log("Usage:");
|
|
3578
|
-
console.log(` pjangler recipe run ${name}`);
|
|
3579
|
-
console.log(` pjangler init ${name}`);
|
|
3580
3725
|
});
|
|
3581
3726
|
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
3727
|
const context = {
|
|
@@ -3587,80 +3732,73 @@ recipeCmd.command("run").argument("<name>", "Recipe name").description("Execute
|
|
|
3587
3732
|
try {
|
|
3588
3733
|
const recipe = createRecipe(name, context);
|
|
3589
3734
|
if (!recipe) {
|
|
3590
|
-
console.error(
|
|
3591
|
-
console.
|
|
3735
|
+
console.error(`${xmark} Recipe not found: ${bold(name)}`);
|
|
3736
|
+
console.error(` ${dim("Available:")} ${getRecipeNames().map((available) => cyan(available)).join(dim(", "))}`);
|
|
3592
3737
|
process.exit(1);
|
|
3593
3738
|
}
|
|
3594
|
-
const dryRunPrefix = context.dryRun ? "[DRY RUN] " : "";
|
|
3595
|
-
console.log(`${dryRunPrefix}\u{1F680} Running recipe: ${name}`);
|
|
3596
|
-
console.log("");
|
|
3597
3739
|
await recipe.execute();
|
|
3598
3740
|
} catch (error) {
|
|
3599
|
-
console.error(
|
|
3741
|
+
console.error(`${xmark} Error running recipe ${bold(name)}:`, error);
|
|
3600
3742
|
process.exit(1);
|
|
3601
3743
|
}
|
|
3602
3744
|
});
|
|
3603
3745
|
var commandCmd = program.command("command").alias("cmd").description("Manage pjangler commands");
|
|
3604
3746
|
commandCmd.command("list").description("List all available commands").option("-g, --group", "Group commands by category").action((options) => {
|
|
3747
|
+
console.log("");
|
|
3605
3748
|
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
|
-
}
|
|
3749
|
+
console.log(` ${heading("Commands by category")}`);
|
|
3750
|
+
for (const [group, commands] of Object.entries(getCommandsByGroup())) {
|
|
3751
|
+
const width = commands.reduce((max, command) => Math.max(max, command.name.length), 0);
|
|
3614
3752
|
console.log("");
|
|
3753
|
+
console.log(` ${bold(group.toUpperCase())}`);
|
|
3754
|
+
for (const command of commands) {
|
|
3755
|
+
console.log(` ${cyan(command.name.padEnd(width))} ${dim(command.description)}`);
|
|
3756
|
+
}
|
|
3615
3757
|
}
|
|
3758
|
+
console.log("");
|
|
3616
3759
|
} else {
|
|
3617
|
-
|
|
3760
|
+
const width = Object.keys(COMMAND_REGISTRY).reduce((max, name) => Math.max(max, name.length), 0);
|
|
3761
|
+
console.log(` ${heading("Commands")}`);
|
|
3618
3762
|
console.log("");
|
|
3619
3763
|
for (const [name, info] of Object.entries(COMMAND_REGISTRY)) {
|
|
3620
|
-
console.log(` ${name.padEnd(
|
|
3764
|
+
console.log(` ${cyan(name.padEnd(width))} ${dim(info.description)}`);
|
|
3621
3765
|
}
|
|
3622
3766
|
console.log("");
|
|
3623
3767
|
}
|
|
3624
|
-
console.log("Usage
|
|
3625
|
-
console.log("
|
|
3626
|
-
console.log("
|
|
3768
|
+
console.log(` ${dim("Usage")}`);
|
|
3769
|
+
console.log(` ${dim(glyph.pointer)} ${dim("pj command list --group")} ${dim("# group by category")}`);
|
|
3770
|
+
console.log(` ${dim(glyph.pointer)} ${dim("pj command describe <name>")} ${dim("# command details")}`);
|
|
3771
|
+
console.log("");
|
|
3627
3772
|
});
|
|
3628
3773
|
commandCmd.command("describe").argument("<name>", "Command name").description("Show detailed information about a command").action((name) => {
|
|
3629
3774
|
const info = getCommandInfo(name);
|
|
3630
3775
|
if (!info) {
|
|
3631
|
-
console.error(
|
|
3632
|
-
console.
|
|
3776
|
+
console.error(`${xmark} Command not found: ${bold(name)}`);
|
|
3777
|
+
console.error(` ${dim("Available:")} ${getCommandNames().map((available) => cyan(available)).join(dim(", "))}`);
|
|
3633
3778
|
process.exit(1);
|
|
3634
3779
|
}
|
|
3635
|
-
|
|
3780
|
+
const usedIn = Object.entries(RECIPE_REGISTRY).filter(([, recipeInfo]) => recipeInfo.commands.includes(name)).map(([recipeName]) => recipeName);
|
|
3636
3781
|
console.log("");
|
|
3637
|
-
console.log(`
|
|
3638
|
-
console.log(`
|
|
3782
|
+
console.log(` ${heading(info.name)}`);
|
|
3783
|
+
console.log(` ${dim(info.description)}`);
|
|
3639
3784
|
console.log("");
|
|
3640
|
-
console.log("
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
}
|
|
3645
|
-
}
|
|
3785
|
+
console.log(` ${dim("group".padEnd(7))} ${cyan(info.group)}`);
|
|
3786
|
+
console.log(` ${dim("recipes".padEnd(7))} ${usedIn.length ? usedIn.map((recipeName) => cyan(recipeName)).join(dim(", ")) : dim("(none)")}`);
|
|
3787
|
+
console.log("");
|
|
3788
|
+
console.log(` ${dim("Part of recipe execution (not run directly).")}`);
|
|
3646
3789
|
console.log("");
|
|
3647
|
-
console.log("Usage:");
|
|
3648
|
-
console.log(` Part of recipe execution (not run directly)`);
|
|
3649
3790
|
});
|
|
3650
3791
|
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
3792
|
console.log("");
|
|
3653
|
-
console.log("
|
|
3654
|
-
console.log(
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
}
|
|
3658
|
-
if (options.model) {
|
|
3659
|
-
|
|
3660
|
-
}
|
|
3793
|
+
console.log(` ${yellow(glyph.warn)} ${bold("Command generation coming in STORY-005")}`);
|
|
3794
|
+
console.log("");
|
|
3795
|
+
console.log(` ${dim("Planned")}`);
|
|
3796
|
+
console.log(` ${cyan(glyph.bullet)} Generate ${bold(name)} from prompt: ${dim(`"${prompt}"`)}`);
|
|
3797
|
+
if (options.template) console.log(` ${cyan(glyph.bullet)} Template type: ${cyan(options.template)}`);
|
|
3798
|
+
if (options.model) console.log(` ${cyan(glyph.bullet)} LLM model: ${cyan(options.model)}`);
|
|
3799
|
+
console.log("");
|
|
3800
|
+
console.log(` ${dim("For now, manually create commands in src/commands/")}`);
|
|
3661
3801
|
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
3802
|
});
|
|
3665
3803
|
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
3804
|
try {
|
|
@@ -3672,7 +3810,7 @@ program.command("audit").argument("[repo]", "Path to repo to audit (default: cwd
|
|
|
3672
3810
|
}
|
|
3673
3811
|
process.exit(report.ok ? 0 : 1);
|
|
3674
3812
|
} catch (err) {
|
|
3675
|
-
console.error(
|
|
3813
|
+
console.error(`${xmark} audit failed:`, err);
|
|
3676
3814
|
process.exit(1);
|
|
3677
3815
|
}
|
|
3678
3816
|
});
|
|
@@ -3691,7 +3829,7 @@ program.command("migrate").argument("[rule-id]", "Rule ID to migrate (omit to op
|
|
|
3691
3829
|
}
|
|
3692
3830
|
if (ruleId && repo) {
|
|
3693
3831
|
if (!getParityRuleIds().includes(ruleId)) {
|
|
3694
|
-
console.error(
|
|
3832
|
+
console.error(`${xmark} Unknown parity rule: ${bold(ruleId)}`);
|
|
3695
3833
|
process.exit(1);
|
|
3696
3834
|
}
|
|
3697
3835
|
const report2 = runMigration(ruleId, repo, dryRun, false);
|
|
@@ -3704,29 +3842,29 @@ program.command("migrate").argument("[rule-id]", "Rule ID to migrate (omit to op
|
|
|
3704
3842
|
process.exit(report2.ok ? 0 : 1);
|
|
3705
3843
|
}
|
|
3706
3844
|
if (options.json) {
|
|
3707
|
-
console.error(
|
|
3845
|
+
console.error(`${xmark} JSON output requires a rule-id or --all`);
|
|
3708
3846
|
process.exit(1);
|
|
3709
3847
|
}
|
|
3710
3848
|
if (!process.stdin.isTTY) {
|
|
3711
|
-
console.error(
|
|
3849
|
+
console.error(`${xmark} Provide a rule-id, use --all, or run in an interactive terminal`);
|
|
3712
3850
|
process.exit(1);
|
|
3713
3851
|
}
|
|
3714
3852
|
const targetRepo = ruleId ?? repo;
|
|
3715
3853
|
const audit = runAudit(targetRepo);
|
|
3716
3854
|
const ruleIds = await promptForRuleIds(audit.rules);
|
|
3717
3855
|
if (!ruleIds.length) {
|
|
3718
|
-
console.log("No rules selected; nothing to migrate.");
|
|
3856
|
+
console.log(` ${cyan(glyph.info)} ${dim("No rules selected; nothing to migrate.")}`);
|
|
3719
3857
|
process.exit(0);
|
|
3720
3858
|
}
|
|
3721
3859
|
const report = runMigrationForRules(ruleIds, targetRepo, dryRun);
|
|
3722
3860
|
printMigrationReport(report, false);
|
|
3723
3861
|
process.exit(report.ok ? 0 : 1);
|
|
3724
3862
|
} catch (err) {
|
|
3725
|
-
console.error(
|
|
3863
|
+
console.error(`${xmark} migrate failed:`, err);
|
|
3726
3864
|
process.exit(1);
|
|
3727
3865
|
}
|
|
3728
3866
|
});
|
|
3729
|
-
program.command("hermes-agent").alias("hermes").description("Provision
|
|
3867
|
+
program.command("hermes-agent").alias("hermes").description("Provision the PM agent for the current repo (defaults everything; only asks about Telegram)").option("-y, --yes", "Non-interactive: accept all defaults (also skips the Telegram prompt)").option("--target-repo <name>", "Target repo name (default: basename of cwd)").option("--role <role>", "Agent role override (default: pm \u2014 the only role in the fleet)").option("--purpose <text>", 'One-line agent purpose (default: "pm agent for <repo>")').option(`--tone <tone>`, `Personality tone (default: direct; ${SOUL_TONES.join(" | ")})`).option("--model-provider <name>", 'Inference provider override ("" = inherit shared default profile)').option("--model-name <name>", 'Model name override ("" = inherit shared default profile)').option("--skip-telegram", "Skip the Telegram wire-up (no BotFather prompt)").option("--email", "Also provision the delo.sh email address (off by default; never prompted)").option("--skip-runtime-repo", "Skip creating the per-agent runtime GH repo").option("--skip-plane", "Skip creating the Plane project").option("--skip-bloodbank", "Skip installing the Bloodbank NATS consumer").option("--skip-systemd", "Skip installing systemd --user units").option("--local", "Local-only: skip runtime repo, Plane, Bloodbank, and systemd (safe for laptops/macOS/non-technical operators)").option("--force-config", "Regenerate ~/.config/hermes-agent-template/config.toml even if it exists").option("--dry-run", "Preview what would run; don't execute copier").option("-f, --force", "Re-render even if agents/hermes/<role>/role.yaml already exists").action(async (options) => {
|
|
3730
3868
|
const isDarwin = process.platform === "darwin";
|
|
3731
3869
|
const local = options.local ?? false;
|
|
3732
3870
|
const context = {
|
|
@@ -3743,7 +3881,8 @@ program.command("hermes-agent").alias("hermes").description("Provision a Hermes
|
|
|
3743
3881
|
modelProvider: options.modelProvider,
|
|
3744
3882
|
modelName: options.modelName,
|
|
3745
3883
|
skipTelegram: options.skipTelegram,
|
|
3746
|
-
|
|
3884
|
+
// Email is opt-in only: `--email` wires it, otherwise it's never done.
|
|
3885
|
+
skipEmail: options.email ? false : void 0,
|
|
3747
3886
|
// --local (and macOS, for systemd) flip the heavy/irreversible steps off
|
|
3748
3887
|
// by default so a non-technical operator can't accidentally create cloud
|
|
3749
3888
|
// resources under the wrong account or hit systemd on a Mac. An explicit
|
|
@@ -3757,12 +3896,12 @@ program.command("hermes-agent").alias("hermes").description("Provision a Hermes
|
|
|
3757
3896
|
try {
|
|
3758
3897
|
const recipe = createRecipe("hermes-agent", context);
|
|
3759
3898
|
if (!recipe) {
|
|
3760
|
-
console.error(
|
|
3899
|
+
console.error(`${xmark} hermes-agent recipe not registered`);
|
|
3761
3900
|
process.exit(1);
|
|
3762
3901
|
}
|
|
3763
3902
|
await recipe.execute();
|
|
3764
3903
|
} catch (err) {
|
|
3765
|
-
console.error(
|
|
3904
|
+
console.error(`${xmark} hermes-agent failed:`, err);
|
|
3766
3905
|
process.exit(1);
|
|
3767
3906
|
}
|
|
3768
3907
|
});
|
|
@@ -3780,14 +3919,15 @@ configCmd.command("bootstrap").description("Create ~/.config/hermes-agent-templa
|
|
|
3780
3919
|
}
|
|
3781
3920
|
});
|
|
3782
3921
|
program.command("describe").description("Describe the current project (for AI context)").action(() => {
|
|
3783
|
-
console.log("\u{1F50D} Project Description (placeholder for future enhancement)");
|
|
3784
3922
|
console.log("");
|
|
3785
|
-
console.log("
|
|
3786
|
-
console.log("
|
|
3787
|
-
console.log("
|
|
3788
|
-
|
|
3789
|
-
|
|
3923
|
+
console.log(` ${heading("Project description")} ${dim("(placeholder)")}`);
|
|
3924
|
+
console.log("");
|
|
3925
|
+
console.log(` ${dim("Will analyze the project and report:")}`);
|
|
3926
|
+
for (const item of ["Detected project type", "Installed subsystems", "Configuration files present", "Suggested next steps"]) {
|
|
3927
|
+
console.log(` ${cyan(glyph.bullet)} ${item}`);
|
|
3928
|
+
}
|
|
3929
|
+
console.log("");
|
|
3930
|
+
console.log(` ${dim("Coming soon.")}`);
|
|
3790
3931
|
console.log("");
|
|
3791
|
-
console.log("Coming soon!");
|
|
3792
3932
|
});
|
|
3793
3933
|
program.parse();
|