@delorenj/pjangler 1.2.1 → 1.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
+
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
5
|
+
import { existsSync as existsSync9, readFileSync as readFileSync6, statSync as statSync2 } from "node:fs";
|
|
6
|
+
import { basename as basename4, join as join12, resolve as resolve3 } from "node:path";
|
|
4
7
|
import { Command as Command3 } from "commander";
|
|
5
8
|
|
|
6
9
|
// src/commands/hermes/types.ts
|
|
@@ -156,6 +159,78 @@ var EnsureTemplateConfig = class extends Command {
|
|
|
156
159
|
}
|
|
157
160
|
};
|
|
158
161
|
|
|
162
|
+
// src/utils/style.ts
|
|
163
|
+
var env = process.env;
|
|
164
|
+
function detectColor() {
|
|
165
|
+
if ("NO_COLOR" in env && env.NO_COLOR !== "") return false;
|
|
166
|
+
const force = env.FORCE_COLOR;
|
|
167
|
+
if (force === "0" || force === "false") return false;
|
|
168
|
+
if (force !== void 0 && force !== "") return true;
|
|
169
|
+
if (env.TERM === "dumb") return false;
|
|
170
|
+
return Boolean(process.stdout.isTTY);
|
|
171
|
+
}
|
|
172
|
+
var colorEnabled = detectColor();
|
|
173
|
+
function sgr(open, close) {
|
|
174
|
+
const prefix = `\x1B[${open}m`;
|
|
175
|
+
const suffix = `\x1B[${close}m`;
|
|
176
|
+
return (value) => colorEnabled ? `${prefix}${value}${suffix}` : String(value);
|
|
177
|
+
}
|
|
178
|
+
var bold = sgr(1, 22);
|
|
179
|
+
var dim = sgr(2, 22);
|
|
180
|
+
var italic = sgr(3, 23);
|
|
181
|
+
var underline = sgr(4, 24);
|
|
182
|
+
var red = sgr(31, 39);
|
|
183
|
+
var green = sgr(32, 39);
|
|
184
|
+
var yellow = sgr(33, 39);
|
|
185
|
+
var blue = sgr(34, 39);
|
|
186
|
+
var magenta = sgr(35, 39);
|
|
187
|
+
var cyan = sgr(36, 39);
|
|
188
|
+
var gray = sgr(90, 39);
|
|
189
|
+
var glyph = {
|
|
190
|
+
pass: "\u2714",
|
|
191
|
+
fail: "\u2716",
|
|
192
|
+
warn: "\u26A0",
|
|
193
|
+
skip: "\u25CB",
|
|
194
|
+
info: "\u2139",
|
|
195
|
+
arrow: "\u21B3",
|
|
196
|
+
bullet: "\u2022",
|
|
197
|
+
dot: "\xB7",
|
|
198
|
+
add: "+",
|
|
199
|
+
chevron: "\u25B8",
|
|
200
|
+
pointer: "\u276F"
|
|
201
|
+
};
|
|
202
|
+
var STATUS_STYLES = {
|
|
203
|
+
pass: { glyph: glyph.pass, color: green, label: "pass" },
|
|
204
|
+
fail: { glyph: glyph.fail, color: red, label: "fail" },
|
|
205
|
+
warn: { glyph: glyph.warn, color: yellow, label: "warn" },
|
|
206
|
+
skip: { glyph: glyph.skip, color: gray, label: "skip" },
|
|
207
|
+
applied: { glyph: glyph.pass, color: green, label: "applied" },
|
|
208
|
+
noop: { glyph: glyph.skip, color: gray, label: "noop" },
|
|
209
|
+
blocked: { glyph: glyph.fail, color: red, label: "blocked" },
|
|
210
|
+
skipped: { glyph: glyph.skip, color: gray, label: "skipped" }
|
|
211
|
+
};
|
|
212
|
+
function statusStyle(status) {
|
|
213
|
+
return STATUS_STYLES[status] ?? { glyph: glyph.dot, color: dim, label: status };
|
|
214
|
+
}
|
|
215
|
+
function projectStatusColor(status) {
|
|
216
|
+
switch (status) {
|
|
217
|
+
case "active":
|
|
218
|
+
return green;
|
|
219
|
+
case "planned":
|
|
220
|
+
return yellow;
|
|
221
|
+
case "archived":
|
|
222
|
+
return gray;
|
|
223
|
+
default:
|
|
224
|
+
return cyan;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
function heading(title, marker = glyph.chevron) {
|
|
228
|
+
return `${cyan(bold(marker))} ${bold(title)}`;
|
|
229
|
+
}
|
|
230
|
+
function joinDot(fragments) {
|
|
231
|
+
return fragments.join(dim(` ${glyph.dot} `));
|
|
232
|
+
}
|
|
233
|
+
|
|
159
234
|
// src/recipes/Recipe.ts
|
|
160
235
|
var Recipe = class {
|
|
161
236
|
context;
|
|
@@ -168,26 +243,22 @@ var Recipe = class {
|
|
|
168
243
|
return this;
|
|
169
244
|
}
|
|
170
245
|
async execute() {
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
246
|
+
const subsystem = this.constructor.name.replace("Recipe", "").toLowerCase();
|
|
247
|
+
const dryRun = this.context.dryRun;
|
|
248
|
+
console.log("");
|
|
249
|
+
console.log(` ${cyan(bold(glyph.chevron))} ${bold(`Initializing ${subsystem} subsystem`)}${dryRun ? ` ${dim(glyph.dot)} ${yellow("dry run")}` : ""}`);
|
|
250
|
+
console.log("");
|
|
177
251
|
for (const command of this.ingredients) {
|
|
178
252
|
const result = await command.invoke();
|
|
179
|
-
|
|
180
|
-
console.log(result.message);
|
|
181
|
-
} else {
|
|
182
|
-
console.log(result.message);
|
|
183
|
-
}
|
|
253
|
+
console.log(result.message.split("\n").map((line) => line ? ` ${line}` : line).join("\n"));
|
|
184
254
|
}
|
|
185
|
-
if (!
|
|
255
|
+
if (!dryRun) {
|
|
186
256
|
this.printNextSteps();
|
|
187
257
|
} else {
|
|
188
258
|
console.log("");
|
|
189
|
-
console.log("
|
|
190
|
-
console.log("
|
|
259
|
+
console.log(` ${green(glyph.pass)} ${dim("Dry-run complete \u2014 no files were modified.")}`);
|
|
260
|
+
console.log(` ${dim("Remove --dry-run to apply changes.")}`);
|
|
261
|
+
console.log("");
|
|
191
262
|
}
|
|
192
263
|
}
|
|
193
264
|
};
|
|
@@ -330,11 +401,111 @@ if __name__ == "__main__":
|
|
|
330
401
|
}
|
|
331
402
|
};
|
|
332
403
|
|
|
404
|
+
// src/commands/AddMiseCodegraphScript.ts
|
|
405
|
+
import { chmodSync } from "fs";
|
|
406
|
+
import { join as join3 } from "path";
|
|
407
|
+
var AddMiseCodegraphScript = class extends Command {
|
|
408
|
+
async invoke() {
|
|
409
|
+
const filePath = ".mise/scripts/codegraph.sh";
|
|
410
|
+
if (this.fileExists(filePath) && !this.context.force) {
|
|
411
|
+
return {
|
|
412
|
+
success: false,
|
|
413
|
+
message: this.formatMessage("\u26A0\uFE0F .mise/scripts/codegraph.sh already exists"),
|
|
414
|
+
filePath
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
const content = `#!/usr/bin/env bash
|
|
418
|
+
# Mise enter hook: ensure the CodeGraph CLI is available and initialize the
|
|
419
|
+
# project index. If \`codegraph\` is not installed, this script installs it
|
|
420
|
+
# non-interactively into the project-local .mise/bin directory and retries.
|
|
421
|
+
#
|
|
422
|
+
# This is intended to run from a mise enter hook so onboarding a new host is
|
|
423
|
+
# fully automatic.
|
|
424
|
+
|
|
425
|
+
set -euo pipefail
|
|
426
|
+
|
|
427
|
+
REPO_ROOT="\${MISE_PROJECT_ROOT:-$(cd "$(dirname "$0")/../.." && pwd)}"
|
|
428
|
+
PROJECT_BIN_DIR="$REPO_ROOT/.mise/bin"
|
|
429
|
+
mkdir -p "$PROJECT_BIN_DIR"
|
|
430
|
+
|
|
431
|
+
# Install the CodeGraph CLI into the project-local bin directory.
|
|
432
|
+
install_codegraph() {
|
|
433
|
+
echo "[mise] codegraph not found. Installing non-interactively..."
|
|
434
|
+
export CODEGRAPH_BIN_DIR="$PROJECT_BIN_DIR"
|
|
435
|
+
curl -fsSL https://raw.githubusercontent.com/colbymchenry/codegraph/main/install.sh | sh
|
|
436
|
+
|
|
437
|
+
if [ -x "$PROJECT_BIN_DIR/codegraph" ]; then
|
|
438
|
+
export PATH="$PROJECT_BIN_DIR:$PATH"
|
|
439
|
+
else
|
|
440
|
+
echo "[mise] codegraph install did not place a binary at $PROJECT_BIN_DIR/codegraph" >&2
|
|
441
|
+
return 1
|
|
442
|
+
fi
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
# Ensure a codegraph binary is available on PATH.
|
|
446
|
+
ensure_codegraph() {
|
|
447
|
+
if command -v codegraph >/dev/null 2>&1; then
|
|
448
|
+
return 0
|
|
449
|
+
fi
|
|
450
|
+
|
|
451
|
+
# Check the project-local bin dir first (previous install from this hook).
|
|
452
|
+
if [ -x "$PROJECT_BIN_DIR/codegraph" ]; then
|
|
453
|
+
export PATH="$PROJECT_BIN_DIR:$PATH"
|
|
454
|
+
return 0
|
|
455
|
+
fi
|
|
456
|
+
|
|
457
|
+
# Check typical user-level install locations before fetching anything.
|
|
458
|
+
for d in "$HOME/.local/bin" "$HOME/.codegraph/current/bin"; do
|
|
459
|
+
if [ -x "$d/codegraph" ]; then
|
|
460
|
+
export PATH="$d:$PATH"
|
|
461
|
+
return 0
|
|
462
|
+
fi
|
|
463
|
+
done
|
|
464
|
+
|
|
465
|
+
install_codegraph
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
# Attempt to initialize the project graph. If the command is missing, install
|
|
469
|
+
# it and retry once.
|
|
470
|
+
init_project() {
|
|
471
|
+
local err_file
|
|
472
|
+
err_file="$(mktemp)"
|
|
473
|
+
trap 'rm -f "$err_file"' RETURN
|
|
474
|
+
|
|
475
|
+
if codegraph init -i "$REPO_ROOT" 2>"$err_file"; then
|
|
476
|
+
return 0
|
|
477
|
+
fi
|
|
478
|
+
|
|
479
|
+
# If the failure looks like a missing binary, install and retry.
|
|
480
|
+
if grep -qiE 'command not found|not installed|No such file|executable file not found' "$err_file" 2>/dev/null; then
|
|
481
|
+
ensure_codegraph
|
|
482
|
+
codegraph init -i "$REPO_ROOT"
|
|
483
|
+
return 0
|
|
484
|
+
fi
|
|
485
|
+
|
|
486
|
+
cat "$err_file" >&2
|
|
487
|
+
return 1
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
init_project
|
|
491
|
+
`;
|
|
492
|
+
this.writeFile(filePath, content);
|
|
493
|
+
if (!this.context.dryRun) {
|
|
494
|
+
chmodSync(join3(this.context.targetDir, filePath), 493);
|
|
495
|
+
}
|
|
496
|
+
return {
|
|
497
|
+
success: true,
|
|
498
|
+
message: this.formatMessage(this.context.dryRun ? "Would create .mise/scripts/codegraph.sh" : "\u2705 Created .mise/scripts/codegraph.sh"),
|
|
499
|
+
filePath
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
|
|
333
504
|
// src/recipes/MiseRecipe.ts
|
|
334
505
|
var MiseRecipe = class extends Recipe {
|
|
335
506
|
constructor(context) {
|
|
336
507
|
super(context);
|
|
337
|
-
this.addIngredient(AddMiseToml).addIngredient(AddDotenv).addIngredient(AddMiseTasksStructure).addIngredient(AddMiseBaseToml).addIngredient(AddMiseBaseScript);
|
|
508
|
+
this.addIngredient(AddMiseToml).addIngredient(AddDotenv).addIngredient(AddMiseTasksStructure).addIngredient(AddMiseBaseToml).addIngredient(AddMiseBaseScript).addIngredient(AddMiseCodegraphScript);
|
|
338
509
|
}
|
|
339
510
|
printNextSteps() {
|
|
340
511
|
console.log("\u{1F389} Mise subsystem initialized successfully!");
|
|
@@ -562,12 +733,12 @@ var NodeRecipe = class extends Recipe {
|
|
|
562
733
|
};
|
|
563
734
|
|
|
564
735
|
// src/commands/hermes/PromptForAgentConfig.ts
|
|
565
|
-
import { basename, join as
|
|
736
|
+
import { basename, join as join4 } from "node:path";
|
|
566
737
|
import { readFileSync } from "node:fs";
|
|
567
738
|
import * as p from "@clack/prompts";
|
|
568
739
|
function detectTicketProvider(targetDir) {
|
|
569
740
|
try {
|
|
570
|
-
const t = JSON.parse(readFileSync(
|
|
741
|
+
const t = JSON.parse(readFileSync(join4(targetDir, ".project.json"), "utf8"))?.ticket_provider?.type;
|
|
571
742
|
return t === "plane" || t === "linear" || t === "trello" ? t : void 0;
|
|
572
743
|
} catch {
|
|
573
744
|
return void 0;
|
|
@@ -703,7 +874,7 @@ var PromptForAgentConfig = class extends Command {
|
|
|
703
874
|
// src/commands/hermes/RunCopierTemplate.ts
|
|
704
875
|
import { spawnSync } from "node:child_process";
|
|
705
876
|
import { homedir as homedir2 } from "node:os";
|
|
706
|
-
import { join as
|
|
877
|
+
import { join as join5, dirname as dirname3 } from "node:path";
|
|
707
878
|
import { existsSync as existsSync3, mkdirSync as mkdirSync3 } from "node:fs";
|
|
708
879
|
import { fileURLToPath } from "node:url";
|
|
709
880
|
import * as p2 from "@clack/prompts";
|
|
@@ -715,8 +886,8 @@ function resolveVendoredTemplate(name) {
|
|
|
715
886
|
return void 0;
|
|
716
887
|
}
|
|
717
888
|
for (let i = 0; i < 8; i++) {
|
|
718
|
-
const candidate =
|
|
719
|
-
if (existsSync3(
|
|
889
|
+
const candidate = join5(dir, "templates", name);
|
|
890
|
+
if (existsSync3(join5(candidate, "copier.yml"))) return candidate;
|
|
720
891
|
const parent = dirname3(dir);
|
|
721
892
|
if (parent === dir) break;
|
|
722
893
|
dir = parent;
|
|
@@ -735,7 +906,7 @@ var RunCopierTemplate = class extends Command {
|
|
|
735
906
|
message: "PromptForAgentConfig must run before RunCopierTemplate (targetRepo/role unset)"
|
|
736
907
|
};
|
|
737
908
|
}
|
|
738
|
-
const roleDir =
|
|
909
|
+
const roleDir = join5(ctx.targetDir, "agents", "hermes", role);
|
|
739
910
|
ctx.roleDir = roleDir;
|
|
740
911
|
ctx.runtimeRepo = `delorenj/agent-hm-${targetRepo}-${role}`;
|
|
741
912
|
const which = spawnSync("which", ["copier"], { encoding: "utf8" });
|
|
@@ -745,7 +916,7 @@ var RunCopierTemplate = class extends Command {
|
|
|
745
916
|
message: "\u2717 copier not found on PATH. Install with: `uv tool install copier` or `pip install copier`"
|
|
746
917
|
};
|
|
747
918
|
}
|
|
748
|
-
if (existsSync3(
|
|
919
|
+
if (existsSync3(join5(roleDir, "role.yaml")) && !ctx.force) {
|
|
749
920
|
if (ctx.yes) {
|
|
750
921
|
ctx.force = true;
|
|
751
922
|
} else {
|
|
@@ -762,7 +933,7 @@ var RunCopierTemplate = class extends Command {
|
|
|
762
933
|
ctx.force = true;
|
|
763
934
|
}
|
|
764
935
|
}
|
|
765
|
-
const
|
|
936
|
+
const env2 = {
|
|
766
937
|
...process.env,
|
|
767
938
|
SKIP_TELEGRAM: "1",
|
|
768
939
|
SKIP_EMAIL: "1",
|
|
@@ -772,9 +943,9 @@ var RunCopierTemplate = class extends Command {
|
|
|
772
943
|
SKIP_BLOODBANK: ctx.skipBloodbank ? "1" : "0",
|
|
773
944
|
SKIP_SYSTEMD: ctx.skipSystemd ? "1" : "0"
|
|
774
945
|
};
|
|
775
|
-
const LOCAL_TEMPLATE =
|
|
946
|
+
const LOCAL_TEMPLATE = join5(homedir2(), "code", "hermes-agent-template");
|
|
776
947
|
const vendored = resolveVendoredTemplate("hermes-agent");
|
|
777
|
-
const templateSrc = process.env.PJANGLER_HERMES_TEMPLATE || vendored || (existsSync3(
|
|
948
|
+
const templateSrc = process.env.PJANGLER_HERMES_TEMPLATE || vendored || (existsSync3(join5(LOCAL_TEMPLATE, "copier.yml")) ? LOCAL_TEMPLATE : HERMES_AGENT_TEMPLATE);
|
|
778
949
|
const args = [
|
|
779
950
|
"copy",
|
|
780
951
|
templateSrc,
|
|
@@ -805,13 +976,13 @@ var RunCopierTemplate = class extends Command {
|
|
|
805
976
|
message: this.formatMessage(`Would run: copier ${args.join(" ")}`)
|
|
806
977
|
};
|
|
807
978
|
}
|
|
808
|
-
mkdirSync3(
|
|
979
|
+
mkdirSync3(join5(ctx.targetDir, "agents", "hermes"), { recursive: true });
|
|
809
980
|
const spinner4 = p2.spinner();
|
|
810
981
|
spinner4.start(`Running copier copy (target: agents/hermes/${role})`);
|
|
811
982
|
const result = spawnSync("copier", args, {
|
|
812
983
|
stdio: "inherit",
|
|
813
984
|
// pass the interactive output through; copier prints its own progress
|
|
814
|
-
env,
|
|
985
|
+
env: env2,
|
|
815
986
|
cwd: ctx.targetDir
|
|
816
987
|
});
|
|
817
988
|
spinner4.stop(result.status === 0 ? "\u2713 copier run complete" : "\u2717 copier failed");
|
|
@@ -830,7 +1001,7 @@ var RunCopierTemplate = class extends Command {
|
|
|
830
1001
|
|
|
831
1002
|
// src/commands/hermes/WireTelegram.ts
|
|
832
1003
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
833
|
-
import { join as
|
|
1004
|
+
import { join as join6 } from "node:path";
|
|
834
1005
|
import { existsSync as existsSync4, unlinkSync } from "node:fs";
|
|
835
1006
|
import * as p3 from "@clack/prompts";
|
|
836
1007
|
var WireTelegram = class extends Command {
|
|
@@ -919,14 +1090,14 @@ var WireTelegram = class extends Command {
|
|
|
919
1090
|
if (p3.isCancel(allowedAnswer)) {
|
|
920
1091
|
return { success: false, message: "\u2717 Aborted; Telegram step deferred." };
|
|
921
1092
|
}
|
|
922
|
-
const script =
|
|
1093
|
+
const script = join6(roleDir, ".scripts", "30-telegram.sh");
|
|
923
1094
|
if (!existsSync4(script)) {
|
|
924
1095
|
return {
|
|
925
1096
|
success: false,
|
|
926
1097
|
message: `\u2717 ${script} not found. Did copier finish? Re-run with --skip-runtime-repo=0 if you skipped it.`
|
|
927
1098
|
};
|
|
928
1099
|
}
|
|
929
|
-
const marker =
|
|
1100
|
+
const marker = join6(roleDir, ".scripts", ".done-30-telegram");
|
|
930
1101
|
if (existsSync4(marker)) unlinkSync(marker);
|
|
931
1102
|
const spinner4 = p3.spinner();
|
|
932
1103
|
spinner4.start("Verifying token + wiring profile");
|
|
@@ -954,7 +1125,7 @@ function cap(s) {
|
|
|
954
1125
|
|
|
955
1126
|
// src/commands/hermes/WireEmail.ts
|
|
956
1127
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
957
|
-
import { join as
|
|
1128
|
+
import { join as join7 } from "node:path";
|
|
958
1129
|
import { existsSync as existsSync5, unlinkSync as unlinkSync2 } from "node:fs";
|
|
959
1130
|
import * as p4 from "@clack/prompts";
|
|
960
1131
|
var WireEmail = class extends Command {
|
|
@@ -970,7 +1141,7 @@ var WireEmail = class extends Command {
|
|
|
970
1141
|
if (!targetRepo || !role || !roleDir) {
|
|
971
1142
|
return { success: false, message: "Cannot wire email: missing target_repo/role/roleDir" };
|
|
972
1143
|
}
|
|
973
|
-
const script =
|
|
1144
|
+
const script = join7(roleDir, ".scripts", "50-email.sh");
|
|
974
1145
|
if (!existsSync5(script)) {
|
|
975
1146
|
return { success: false, message: `\u2717 ${script} not found` };
|
|
976
1147
|
}
|
|
@@ -1033,7 +1204,7 @@ var WireEmail = class extends Command {
|
|
|
1033
1204
|
}
|
|
1034
1205
|
}
|
|
1035
1206
|
}
|
|
1036
|
-
const marker =
|
|
1207
|
+
const marker = join7(roleDir, ".scripts", ".done-50-email");
|
|
1037
1208
|
if (existsSync5(marker)) unlinkSync2(marker);
|
|
1038
1209
|
const spinner4 = p4.spinner();
|
|
1039
1210
|
spinner4.start("Creating Cloudflare Email Routing rule");
|
|
@@ -1122,7 +1293,7 @@ var HermesAgentRecipe = class extends Recipe {
|
|
|
1122
1293
|
|
|
1123
1294
|
// src/commands/AgentHooksCommands.ts
|
|
1124
1295
|
import { homedir as homedir3 } from "node:os";
|
|
1125
|
-
import { join as
|
|
1296
|
+
import { join as join8, dirname as dirname4 } from "node:path";
|
|
1126
1297
|
import { existsSync as existsSync6, cpSync, mkdirSync as mkdirSync4, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
1127
1298
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1128
1299
|
function resolveTemplateRoot() {
|
|
@@ -1133,16 +1304,16 @@ function resolveTemplateRoot() {
|
|
|
1133
1304
|
try {
|
|
1134
1305
|
let dir = dirname4(fileURLToPath2(import.meta.url));
|
|
1135
1306
|
for (let i = 0; i < 8; i++) {
|
|
1136
|
-
candidates.push(
|
|
1307
|
+
candidates.push(join8(dir, "templates", "commonproject", "template"));
|
|
1137
1308
|
const parent = dirname4(dir);
|
|
1138
1309
|
if (parent === dir) break;
|
|
1139
1310
|
dir = parent;
|
|
1140
1311
|
}
|
|
1141
1312
|
} catch {
|
|
1142
1313
|
}
|
|
1143
|
-
candidates.push(
|
|
1314
|
+
candidates.push(join8(homedir3(), "code", "pjangler", "templates", "commonproject", "template"));
|
|
1144
1315
|
for (const c of candidates) {
|
|
1145
|
-
if (existsSync6(
|
|
1316
|
+
if (existsSync6(join8(c, ".agents", "hooks", "hooks.master.json"))) return c;
|
|
1146
1317
|
}
|
|
1147
1318
|
throw new Error(
|
|
1148
1319
|
"Could not locate the CommonProject template. Set PJANGLER_COMMONPROJECT_TEMPLATE to <repo>/templates/commonproject/template."
|
|
@@ -1166,8 +1337,8 @@ var CopyAgentHooksTree = class extends Command {
|
|
|
1166
1337
|
const created = [];
|
|
1167
1338
|
const skipped = [];
|
|
1168
1339
|
for (const { rel, dir } of items) {
|
|
1169
|
-
const src =
|
|
1170
|
-
const dest =
|
|
1340
|
+
const src = join8(templateRoot, rel);
|
|
1341
|
+
const dest = join8(this.context.targetDir, rel);
|
|
1171
1342
|
if (!existsSync6(src)) continue;
|
|
1172
1343
|
if (existsSync6(dest) && !this.context.force) {
|
|
1173
1344
|
skipped.push(rel);
|
|
@@ -1192,7 +1363,7 @@ var WireMiseAgentHooks = class _WireMiseAgentHooks extends Command {
|
|
|
1192
1363
|
static CR = "{{config_root}}";
|
|
1193
1364
|
// mise's own runtime var — emitted literally
|
|
1194
1365
|
async invoke() {
|
|
1195
|
-
const misePath =
|
|
1366
|
+
const misePath = join8(this.context.targetDir, "mise.toml");
|
|
1196
1367
|
if (!existsSync6(misePath)) {
|
|
1197
1368
|
return {
|
|
1198
1369
|
success: false,
|
|
@@ -1313,7 +1484,7 @@ var RECIPE_REGISTRY = {
|
|
|
1313
1484
|
name: "mise",
|
|
1314
1485
|
description: "Mise task runner and environment setup",
|
|
1315
1486
|
class: MiseRecipe,
|
|
1316
|
-
commands: ["AddMiseToml", "AddDotenv", "AddMiseTasksStructure", "AddMiseBaseToml", "AddMiseBaseScript"]
|
|
1487
|
+
commands: ["AddMiseToml", "AddDotenv", "AddMiseTasksStructure", "AddMiseBaseToml", "AddMiseBaseScript", "AddMiseCodegraphScript"]
|
|
1317
1488
|
},
|
|
1318
1489
|
docker: {
|
|
1319
1490
|
name: "docker",
|
|
@@ -1403,6 +1574,12 @@ var COMMAND_REGISTRY = {
|
|
|
1403
1574
|
group: "mise",
|
|
1404
1575
|
class: AddMiseBaseScript
|
|
1405
1576
|
},
|
|
1577
|
+
AddMiseCodegraphScript: {
|
|
1578
|
+
name: "AddMiseCodegraphScript",
|
|
1579
|
+
description: "Create .mise/scripts/codegraph.sh enter hook",
|
|
1580
|
+
group: "mise",
|
|
1581
|
+
class: AddMiseCodegraphScript
|
|
1582
|
+
},
|
|
1406
1583
|
AddDotenv: {
|
|
1407
1584
|
name: "AddDotenv",
|
|
1408
1585
|
description: "Create .env.example file",
|
|
@@ -1438,9 +1615,12 @@ function createRecipe(name, context) {
|
|
|
1438
1615
|
return new info.class(context);
|
|
1439
1616
|
}
|
|
1440
1617
|
|
|
1618
|
+
// src/index.ts
|
|
1619
|
+
import { cancel as cancel2, multiselect, text as text3, isCancel as isCancel5 } from "@clack/prompts";
|
|
1620
|
+
|
|
1441
1621
|
// src/parity/index.ts
|
|
1442
|
-
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";
|
|
1443
|
-
import { dirname as dirname5, join as
|
|
1622
|
+
import { existsSync as existsSync7, lstatSync, mkdirSync as mkdirSync5, readFileSync as readFileSync3, readlinkSync, readdirSync, renameSync, symlinkSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync4, chmodSync as chmodSync2, copyFileSync } from "node:fs";
|
|
1623
|
+
import { basename as basename2, dirname as dirname5, join as join9, relative, resolve } from "node:path";
|
|
1444
1624
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
1445
1625
|
import { homedir as homedir4 } from "node:os";
|
|
1446
1626
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
@@ -1460,6 +1640,28 @@ enter = [
|
|
|
1460
1640
|
patterns = ["AGENTS.md"]
|
|
1461
1641
|
task = "link-agentfiles"
|
|
1462
1642
|
|
|
1643
|
+
[tasks.link-agentfiles]
|
|
1644
|
+
description = "Symlink all agent files to AGENTS.md"
|
|
1645
|
+
run = "{{config_root}}/.mise/scripts/link-agentfiles.sh"`;
|
|
1646
|
+
var LINK_AGENTFILES_HOOK_ENTRIES = [
|
|
1647
|
+
"{{config_root}}/.mise/scripts/link-agentfiles.sh",
|
|
1648
|
+
"op inject -i .env.op > .env"
|
|
1649
|
+
];
|
|
1650
|
+
var LINK_AGENTFILES_HOOKS_BLOCK = `# This block will handle the linking of
|
|
1651
|
+
# agent files to the main AGENTS.md file.
|
|
1652
|
+
#
|
|
1653
|
+
# TODO: Ensure this works for all levels of nesting.
|
|
1654
|
+
# i.e. All linked agent files MUST be siblings at
|
|
1655
|
+
# any given level of nesting.
|
|
1656
|
+
[hooks]
|
|
1657
|
+
enter = [
|
|
1658
|
+
"{{config_root}}/.mise/scripts/link-agentfiles.sh",
|
|
1659
|
+
"op inject -i .env.op > .env",
|
|
1660
|
+
]`;
|
|
1661
|
+
var LINK_AGENTFILES_WATCH_TASK_BLOCK = `[[watch_files]]
|
|
1662
|
+
patterns = ["AGENTS.md"]
|
|
1663
|
+
task = "link-agentfiles"
|
|
1664
|
+
|
|
1463
1665
|
[tasks.link-agentfiles]
|
|
1464
1666
|
description = "Symlink all agent files to AGENTS.md"
|
|
1465
1667
|
run = "{{config_root}}/.mise/scripts/link-agentfiles.sh"`;
|
|
@@ -1492,7 +1694,7 @@ run = "{{config_root}}/.mise/scripts/versioning.sh sync"
|
|
|
1492
1694
|
function resolvePjanglerRoot() {
|
|
1493
1695
|
let dir = dirname5(fileURLToPath3(import.meta.url));
|
|
1494
1696
|
while (dir !== dirname5(dir)) {
|
|
1495
|
-
if (existsSync7(
|
|
1697
|
+
if (existsSync7(join9(dir, "package.json")) && existsSync7(join9(dir, "templates", "commonproject", "copier.yml"))) {
|
|
1496
1698
|
return dir;
|
|
1497
1699
|
}
|
|
1498
1700
|
dir = dirname5(dir);
|
|
@@ -1515,10 +1717,10 @@ function writeText(path, content) {
|
|
|
1515
1717
|
ensureParent(path);
|
|
1516
1718
|
writeFileSync4(path, content);
|
|
1517
1719
|
}
|
|
1518
|
-
function tryParseJson(
|
|
1519
|
-
if (!
|
|
1720
|
+
function tryParseJson(text4) {
|
|
1721
|
+
if (!text4) return null;
|
|
1520
1722
|
try {
|
|
1521
|
-
return JSON.parse(
|
|
1723
|
+
return JSON.parse(text4);
|
|
1522
1724
|
} catch {
|
|
1523
1725
|
return null;
|
|
1524
1726
|
}
|
|
@@ -1555,10 +1757,10 @@ function ensureSymlink(path, target, dryRun) {
|
|
|
1555
1757
|
return { changed: true };
|
|
1556
1758
|
}
|
|
1557
1759
|
function bootstrapAgentsFile(repoRoot, dryRun) {
|
|
1558
|
-
const agentsPath =
|
|
1760
|
+
const agentsPath = join9(repoRoot, "AGENTS.md");
|
|
1559
1761
|
if (existsSync7(agentsPath)) return { changedFiles: [], details: [] };
|
|
1560
1762
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
1561
|
-
const source =
|
|
1763
|
+
const source = join9(repoRoot, file);
|
|
1562
1764
|
if (!existsSync7(source)) continue;
|
|
1563
1765
|
const stat = lstatSync(source);
|
|
1564
1766
|
if (stat.isSymbolicLink()) continue;
|
|
@@ -1568,7 +1770,7 @@ function bootstrapAgentsFile(repoRoot, dryRun) {
|
|
|
1568
1770
|
}
|
|
1569
1771
|
return { changedFiles: [], details: [], blocked: `${file} exists but is not a regular file; cannot promote to AGENTS.md` };
|
|
1570
1772
|
}
|
|
1571
|
-
const readmePath =
|
|
1773
|
+
const readmePath = join9(repoRoot, "README.md");
|
|
1572
1774
|
if (existsSync7(readmePath)) {
|
|
1573
1775
|
const stat = lstatSync(readmePath);
|
|
1574
1776
|
if (!stat.isFile()) return { changedFiles: [], details: [], blocked: "README.md exists but is not a regular file; cannot copy to AGENTS.md" };
|
|
@@ -1577,9 +1779,9 @@ function bootstrapAgentsFile(repoRoot, dryRun) {
|
|
|
1577
1779
|
}
|
|
1578
1780
|
return { changedFiles: [], details: [], blocked: "AGENTS.md missing and no CLAUDE.md, GEMINI.md, or README.md source exists" };
|
|
1579
1781
|
}
|
|
1580
|
-
function yamlGet(
|
|
1782
|
+
function yamlGet(text4, keyPath) {
|
|
1581
1783
|
const parts = keyPath.split(".");
|
|
1582
|
-
const lines =
|
|
1784
|
+
const lines = text4.split("\n");
|
|
1583
1785
|
let start = 0;
|
|
1584
1786
|
let indent = 0;
|
|
1585
1787
|
for (let idx = 0; idx < parts.length; idx += 1) {
|
|
@@ -1608,36 +1810,36 @@ function yamlGet(text3, keyPath) {
|
|
|
1608
1810
|
return "";
|
|
1609
1811
|
}
|
|
1610
1812
|
function discoverRoles(repoRoot) {
|
|
1611
|
-
const rolesDir =
|
|
1813
|
+
const rolesDir = join9(repoRoot, "agents", "hermes");
|
|
1612
1814
|
if (!existsSync7(rolesDir)) return [];
|
|
1613
1815
|
return readdirSync(rolesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => {
|
|
1614
|
-
const roleDir =
|
|
1615
|
-
const roleYamlPath =
|
|
1816
|
+
const roleDir = join9(rolesDir, entry.name);
|
|
1817
|
+
const roleYamlPath = join9(roleDir, "role.yaml");
|
|
1616
1818
|
if (!existsSync7(roleYamlPath)) return null;
|
|
1617
|
-
const
|
|
1618
|
-
const runtimeRepoRaw = yamlGet(
|
|
1819
|
+
const text4 = readText(roleYamlPath);
|
|
1820
|
+
const runtimeRepoRaw = yamlGet(text4, "runtime.github_repo");
|
|
1619
1821
|
return {
|
|
1620
|
-
role: yamlGet(
|
|
1822
|
+
role: yamlGet(text4, "role") || entry.name,
|
|
1621
1823
|
roleDir,
|
|
1622
1824
|
roleYamlPath,
|
|
1623
|
-
repo: yamlGet(
|
|
1624
|
-
agentId: yamlGet(
|
|
1625
|
-
profileName: yamlGet(
|
|
1626
|
-
displayName: yamlGet(
|
|
1627
|
-
purpose: yamlGet(
|
|
1628
|
-
botHandle: yamlGet(
|
|
1825
|
+
repo: yamlGet(text4, "repo"),
|
|
1826
|
+
agentId: yamlGet(text4, "agent_id"),
|
|
1827
|
+
profileName: yamlGet(text4, "profile") || yamlGet(text4, "agent_id"),
|
|
1828
|
+
displayName: yamlGet(text4, "display_name"),
|
|
1829
|
+
purpose: yamlGet(text4, "purpose"),
|
|
1830
|
+
botHandle: yamlGet(text4, "telegram.bot_username"),
|
|
1629
1831
|
runtimeRepo: runtimeRepoRaw.includes("/") ? runtimeRepoRaw.split("/").slice(-1)[0] ?? runtimeRepoRaw : runtimeRepoRaw,
|
|
1630
|
-
runtimeOwner: yamlGet(
|
|
1631
|
-
planeWorkspace: yamlGet(
|
|
1632
|
-
ticketProviderName: yamlGet(
|
|
1633
|
-
ticketProviderBoardId: yamlGet(
|
|
1634
|
-
ticketProviderBoardUrl: yamlGet(
|
|
1635
|
-
ticketProviderIdentifier: yamlGet(
|
|
1832
|
+
runtimeOwner: yamlGet(text4, "runtime.github_owner"),
|
|
1833
|
+
planeWorkspace: yamlGet(text4, "ticket_provider.workspace") || yamlGet(text4, "plane.workspace"),
|
|
1834
|
+
ticketProviderName: yamlGet(text4, "ticket_provider.name"),
|
|
1835
|
+
ticketProviderBoardId: yamlGet(text4, "ticket_provider.board_id"),
|
|
1836
|
+
ticketProviderBoardUrl: yamlGet(text4, "ticket_provider.board_url"),
|
|
1837
|
+
ticketProviderIdentifier: yamlGet(text4, "plane.identifier")
|
|
1636
1838
|
};
|
|
1637
1839
|
}).filter((value) => Boolean(value));
|
|
1638
1840
|
}
|
|
1639
1841
|
function registryPath(homeDir) {
|
|
1640
|
-
return
|
|
1842
|
+
return join9(homeDir, ".hermes", "agents-registry.yaml");
|
|
1641
1843
|
}
|
|
1642
1844
|
function systemctlUser(args) {
|
|
1643
1845
|
const result = spawnSync4("systemctl", ["--user", ...args], { encoding: "utf8" });
|
|
@@ -1648,7 +1850,7 @@ function systemctlUser(args) {
|
|
|
1648
1850
|
};
|
|
1649
1851
|
}
|
|
1650
1852
|
function templateScript(ctx, name) {
|
|
1651
|
-
const source =
|
|
1853
|
+
const source = join9(ctx.pjanglerRoot, ".mise", "scripts", name);
|
|
1652
1854
|
return existsSync7(source) ? readText(source) : void 0;
|
|
1653
1855
|
}
|
|
1654
1856
|
function templateVersioningScript(ctx) {
|
|
@@ -1657,23 +1859,39 @@ function templateVersioningScript(ctx) {
|
|
|
1657
1859
|
function templateLinkAgentfilesScript(ctx) {
|
|
1658
1860
|
return templateScript(ctx, "link-agentfiles.sh");
|
|
1659
1861
|
}
|
|
1862
|
+
function renderGeneratedProjectMiseToml(ctx, template) {
|
|
1863
|
+
const project = readProjectJson(ctx);
|
|
1864
|
+
const projectName = String(project?.project_name ?? basename2(ctx.repoRoot) ?? "project");
|
|
1865
|
+
return template.replace(/\{%\s*raw\s*%\}([\s\S]*?)\{%\s*endraw\s*%\}/g, "$1").replace(/\{\{\s*project_name\s*\}\}/g, projectName);
|
|
1866
|
+
}
|
|
1867
|
+
function ensureMiseTomlFromTemplate(ctx, changedFiles) {
|
|
1868
|
+
const targetPath = join9(ctx.repoRoot, "mise.toml");
|
|
1869
|
+
if (existsSync7(targetPath)) return false;
|
|
1870
|
+
const sourcePath = join9(ctx.pjanglerRoot, "templates", "commonproject", "template", "mise.toml.jinja");
|
|
1871
|
+
if (!existsSync7(sourcePath)) return false;
|
|
1872
|
+
changedFiles.push(targetPath);
|
|
1873
|
+
if (!ctx.dryRun) {
|
|
1874
|
+
writeText(targetPath, renderGeneratedProjectMiseToml(ctx, readText(sourcePath)));
|
|
1875
|
+
}
|
|
1876
|
+
return true;
|
|
1877
|
+
}
|
|
1660
1878
|
function templateVersionFilesConf(ctx, repoRoot) {
|
|
1661
|
-
const packageJson =
|
|
1879
|
+
const packageJson = join9(repoRoot, "package.json");
|
|
1662
1880
|
return existsSync7(packageJson) ? "# mise-versioning manifest: <type> <path>\n# types: json toml cargo csproj gradle plain gittag\njson package.json\ngittag .\n" : "# mise-versioning manifest: <type> <path>\n# types: json toml cargo csproj gradle plain gittag\ngittag .\n";
|
|
1663
1881
|
}
|
|
1664
|
-
function replaceOrAppendManagedBlock(
|
|
1665
|
-
if (startMarker.test(
|
|
1666
|
-
return
|
|
1882
|
+
function replaceOrAppendManagedBlock(text4, startMarker, block, beforePattern) {
|
|
1883
|
+
if (startMarker.test(text4)) {
|
|
1884
|
+
return text4.replace(/# >>> mise-versioning >>>[\s\S]*?# <<< mise-versioning <<</, block);
|
|
1667
1885
|
}
|
|
1668
1886
|
if (beforePattern) {
|
|
1669
|
-
const match =
|
|
1887
|
+
const match = text4.match(beforePattern);
|
|
1670
1888
|
if (match && typeof match.index === "number") {
|
|
1671
|
-
return `${
|
|
1889
|
+
return `${text4.slice(0, match.index).replace(/\s*$/, "\n\n")}${block}
|
|
1672
1890
|
|
|
1673
|
-
${
|
|
1891
|
+
${text4.slice(match.index)}`;
|
|
1674
1892
|
}
|
|
1675
1893
|
}
|
|
1676
|
-
return `${
|
|
1894
|
+
return `${text4.replace(/\s*$/, "")}
|
|
1677
1895
|
|
|
1678
1896
|
${block}
|
|
1679
1897
|
`;
|
|
@@ -1683,22 +1901,22 @@ var CONDITIONAL_HERMES_PATHS = ["agents/hermes/pm/hermes", "agent/hermes/pm/herm
|
|
|
1683
1901
|
function requiredMisePathEntries(ctx) {
|
|
1684
1902
|
const required = [...BASE_MISE_PATH_ENTRIES];
|
|
1685
1903
|
for (const candidate of CONDITIONAL_HERMES_PATHS) {
|
|
1686
|
-
if (existsSync7(
|
|
1904
|
+
if (existsSync7(join9(ctx.repoRoot, candidate)) && !required.includes(candidate)) required.push(candidate);
|
|
1687
1905
|
}
|
|
1688
1906
|
return required;
|
|
1689
1907
|
}
|
|
1690
|
-
function upsertMisePath(
|
|
1908
|
+
function upsertMisePath(text4, required = BASE_MISE_PATH_ENTRIES) {
|
|
1691
1909
|
const render = (values) => `_.path = [${values.map((value) => JSON.stringify(value)).join(", ")}]`;
|
|
1692
|
-
const envMatch =
|
|
1910
|
+
const envMatch = text4.match(/(^|\n)(\[env\][\s\S]*?)(?=\n\[[^\]]+\]|$)/);
|
|
1693
1911
|
if (!envMatch || typeof envMatch.index !== "number") {
|
|
1694
1912
|
return `[env]
|
|
1695
1913
|
${render(required)}
|
|
1696
1914
|
|
|
1697
|
-
${
|
|
1915
|
+
${text4.replace(/^\s+/, "")}`;
|
|
1698
1916
|
}
|
|
1699
|
-
const prefix =
|
|
1917
|
+
const prefix = text4.slice(0, envMatch.index + envMatch[1].length);
|
|
1700
1918
|
const section = envMatch[2];
|
|
1701
|
-
const suffix =
|
|
1919
|
+
const suffix = text4.slice(envMatch.index + envMatch[1].length + section.length);
|
|
1702
1920
|
const pathLine = section.match(/^_\.path\s*=\s*\[([^\]]*)\]\s*$/m);
|
|
1703
1921
|
if (!pathLine) {
|
|
1704
1922
|
return `${prefix}${section.replace(/\n?$/, "\n")}${render(required)}${suffix}`;
|
|
@@ -1709,28 +1927,130 @@ ${text3.replace(/^\s+/, "")}`;
|
|
|
1709
1927
|
if (!merged.includes(value)) merged.push(value);
|
|
1710
1928
|
}
|
|
1711
1929
|
const nextLine = render(merged);
|
|
1712
|
-
if (pathLine[0] === nextLine) return
|
|
1930
|
+
if (pathLine[0] === nextLine) return text4;
|
|
1713
1931
|
return `${prefix}${section.replace(pathLine[0], nextLine)}${suffix}`;
|
|
1714
1932
|
}
|
|
1715
|
-
function
|
|
1716
|
-
const
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1933
|
+
function removeTomlSection(text4, headerPattern, marker, options) {
|
|
1934
|
+
const lines = text4.split("\n");
|
|
1935
|
+
let start = -1;
|
|
1936
|
+
let end = -1;
|
|
1937
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1938
|
+
if (!headerPattern.test(lines[i])) continue;
|
|
1939
|
+
if (marker) {
|
|
1940
|
+
let hasMarker = false;
|
|
1941
|
+
for (let j = i + 1; j < lines.length && !/^\[[^\]]+\]/.test(lines[j]); j++) {
|
|
1942
|
+
if (marker.test(lines[j])) {
|
|
1943
|
+
hasMarker = true;
|
|
1944
|
+
break;
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
if (!hasMarker) continue;
|
|
1948
|
+
}
|
|
1949
|
+
start = i;
|
|
1950
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
1951
|
+
if (/^\[[^\]]+\]/.test(lines[j])) {
|
|
1952
|
+
end = j;
|
|
1953
|
+
break;
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
if (end === -1) end = lines.length;
|
|
1957
|
+
break;
|
|
1958
|
+
}
|
|
1959
|
+
if (start === -1) return text4;
|
|
1960
|
+
if (options?.includePrecedingComments) {
|
|
1961
|
+
while (start > 0 && lines[start - 1].trim().startsWith("#")) {
|
|
1962
|
+
start--;
|
|
1963
|
+
}
|
|
1720
1964
|
}
|
|
1721
|
-
const
|
|
1965
|
+
const result = lines.slice(0, start).concat(lines.slice(end)).join("\n");
|
|
1966
|
+
return result.replace(/\n{3,}/g, "\n\n").replace(/\n+$/, "\n");
|
|
1967
|
+
}
|
|
1968
|
+
function insertTomlBlockBeforeVersioning(text4, block) {
|
|
1969
|
+
const versioningIndex = text4.indexOf("# >>> mise-versioning >>>");
|
|
1722
1970
|
if (versioningIndex >= 0) {
|
|
1723
|
-
return `${
|
|
1971
|
+
return `${text4.slice(0, versioningIndex).replace(/\s*$/, "\n\n")}${block}
|
|
1724
1972
|
|
|
1725
|
-
${
|
|
1973
|
+
${text4.slice(versioningIndex)}`;
|
|
1726
1974
|
}
|
|
1727
|
-
return `${
|
|
1975
|
+
return `${text4.replace(/\s*$/, "")}
|
|
1728
1976
|
|
|
1729
|
-
${
|
|
1977
|
+
${block}
|
|
1730
1978
|
`;
|
|
1731
1979
|
}
|
|
1980
|
+
function extractTomlStrings(text4) {
|
|
1981
|
+
const values = [];
|
|
1982
|
+
const stringPattern = /"((?:\\.|[^"\\])*)"|'([^']*)'/g;
|
|
1983
|
+
for (const match of text4.matchAll(stringPattern)) {
|
|
1984
|
+
if (match[1] !== void 0) {
|
|
1985
|
+
try {
|
|
1986
|
+
values.push(JSON.parse(`"${match[1]}"`));
|
|
1987
|
+
} catch {
|
|
1988
|
+
values.push(match[1]);
|
|
1989
|
+
}
|
|
1990
|
+
} else if (match[2] !== void 0) {
|
|
1991
|
+
values.push(match[2]);
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
return values;
|
|
1995
|
+
}
|
|
1996
|
+
function isManagedHookEntry(value) {
|
|
1997
|
+
const trimmed = value.trim();
|
|
1998
|
+
return trimmed === "op inject -i .env.op > .env" || /(^|\/)link-agentfiles\.sh$/.test(trimmed);
|
|
1999
|
+
}
|
|
2000
|
+
function renderHookEntries(entries, indent = "") {
|
|
2001
|
+
return [
|
|
2002
|
+
`${indent}enter = [`,
|
|
2003
|
+
...entries.map((entry) => `${indent} ${JSON.stringify(entry)},`),
|
|
2004
|
+
`${indent}]`
|
|
2005
|
+
];
|
|
2006
|
+
}
|
|
2007
|
+
function upsertLinkAgentfilesHooks(text4) {
|
|
2008
|
+
const lines = text4.split("\n");
|
|
2009
|
+
const hooksStart = lines.findIndex((line) => /^\[hooks\]$/.test(line.trim()));
|
|
2010
|
+
if (hooksStart === -1) return insertTomlBlockBeforeVersioning(text4, LINK_AGENTFILES_HOOKS_BLOCK);
|
|
2011
|
+
let hooksEnd = lines.length;
|
|
2012
|
+
for (let i = hooksStart + 1; i < lines.length; i++) {
|
|
2013
|
+
if (/^\[[^\]]+\]/.test(lines[i].trim())) {
|
|
2014
|
+
hooksEnd = i;
|
|
2015
|
+
break;
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
let enterStart = -1;
|
|
2019
|
+
let enterEnd = -1;
|
|
2020
|
+
for (let i = hooksStart + 1; i < hooksEnd; i++) {
|
|
2021
|
+
if (!/^\s*enter\s*=/.test(lines[i])) continue;
|
|
2022
|
+
enterStart = i;
|
|
2023
|
+
enterEnd = i + 1;
|
|
2024
|
+
const afterEquals = lines[i].slice(lines[i].indexOf("=") + 1);
|
|
2025
|
+
if (afterEquals.includes("[") && !afterEquals.includes("]")) {
|
|
2026
|
+
while (enterEnd < hooksEnd && !lines[enterEnd].includes("]")) enterEnd++;
|
|
2027
|
+
if (enterEnd < hooksEnd) enterEnd++;
|
|
2028
|
+
}
|
|
2029
|
+
break;
|
|
2030
|
+
}
|
|
2031
|
+
const existingBlock = enterStart >= 0 ? lines.slice(enterStart, enterEnd).join("\n") : "";
|
|
2032
|
+
const preserved = extractTomlStrings(existingBlock).filter((entry) => !isManagedHookEntry(entry));
|
|
2033
|
+
const merged = [...LINK_AGENTFILES_HOOK_ENTRIES];
|
|
2034
|
+
for (const entry of preserved) {
|
|
2035
|
+
if (!merged.includes(entry)) merged.push(entry);
|
|
2036
|
+
}
|
|
2037
|
+
const indent = enterStart >= 0 ? lines[enterStart].match(/^\s*/)?.[0] ?? "" : "";
|
|
2038
|
+
const rendered = renderHookEntries(merged, indent);
|
|
2039
|
+
if (enterStart >= 0) {
|
|
2040
|
+
return lines.slice(0, enterStart).concat(rendered, lines.slice(enterEnd)).join("\n").replace(/\n{3,}/g, "\n\n").replace(/\n+$/, "\n");
|
|
2041
|
+
}
|
|
2042
|
+
return lines.slice(0, hooksStart + 1).concat(rendered, lines.slice(hooksStart + 1)).join("\n").replace(/\n{3,}/g, "\n\n").replace(/\n+$/, "\n");
|
|
2043
|
+
}
|
|
2044
|
+
function upsertLinkAgentfilesBlock(text4, ctx) {
|
|
2045
|
+
const withPath = upsertMisePath(text4, requiredMisePathEntries(ctx));
|
|
2046
|
+
if (withPath.includes(LINK_AGENTFILES_BLOCK)) return withPath;
|
|
2047
|
+
let cleaned = removeTomlSection(withPath, /^\[tasks\.link-agentfiles\]$/, /link-agentfiles/, { includePrecedingComments: false });
|
|
2048
|
+
cleaned = removeTomlSection(cleaned, /^\[\[watch_files\]\]$/, /AGENTS\.md/, { includePrecedingComments: false });
|
|
2049
|
+
cleaned = upsertLinkAgentfilesHooks(cleaned);
|
|
2050
|
+
return insertTomlBlockBeforeVersioning(cleaned, LINK_AGENTFILES_WATCH_TASK_BLOCK);
|
|
2051
|
+
}
|
|
1732
2052
|
function readProjectJson(ctx) {
|
|
1733
|
-
return tryParseJson(safeReadText(
|
|
2053
|
+
return tryParseJson(safeReadText(join9(ctx.repoRoot, ".project.json")));
|
|
1734
2054
|
}
|
|
1735
2055
|
function canonicalProjectJson(ctx) {
|
|
1736
2056
|
const roles = discoverRoles(ctx.repoRoot);
|
|
@@ -1742,28 +2062,40 @@ function canonicalProjectJson(ctx) {
|
|
|
1742
2062
|
workspace: String((existing.ticket_provider?.workspace ?? firstRole?.planeWorkspace ?? "") || ""),
|
|
1743
2063
|
identifier: String((existing.ticket_provider?.identifier ?? firstRole?.ticketProviderIdentifier ?? "") || ""),
|
|
1744
2064
|
board_id: String((existing.ticket_provider?.board_id ?? firstRole?.ticketProviderBoardId ?? "") || ""),
|
|
1745
|
-
board_url: String((existing.ticket_provider?.board_url ?? firstRole?.ticketProviderBoardUrl ?? "") || "")
|
|
2065
|
+
board_url: String((existing.ticket_provider?.board_url ?? firstRole?.ticketProviderBoardUrl ?? "") || ""),
|
|
2066
|
+
state: String((existing.ticket_provider?.state ?? "planned") || "planned")
|
|
1746
2067
|
};
|
|
2068
|
+
const existingAgents = existing.agents ?? {};
|
|
2069
|
+
const discoveredAgents = Object.fromEntries(
|
|
2070
|
+
roles.map((role) => [
|
|
2071
|
+
role.agentId || `${slug}-${role.role}`,
|
|
2072
|
+
{
|
|
2073
|
+
role: role.role,
|
|
2074
|
+
role_dir: relative(ctx.repoRoot, role.roleDir)
|
|
2075
|
+
}
|
|
2076
|
+
])
|
|
2077
|
+
);
|
|
2078
|
+
const agents = { ...existingAgents };
|
|
2079
|
+
for (const [agentId, discovered] of Object.entries(discoveredAgents)) {
|
|
2080
|
+
const existingAgent = existingAgents[agentId] ?? {};
|
|
2081
|
+
agents[agentId] = {
|
|
2082
|
+
role: discovered.role,
|
|
2083
|
+
role_dir: discovered.role_dir,
|
|
2084
|
+
provisioning_state: existingAgent.provisioning_state
|
|
2085
|
+
};
|
|
2086
|
+
}
|
|
1747
2087
|
return {
|
|
1748
2088
|
project_name: String(existing.project_name ?? titleCaseSlug(slug)),
|
|
1749
2089
|
project_description: String(existing.project_description ?? ""),
|
|
1750
2090
|
project_slug: slug,
|
|
1751
2091
|
repo_path: ctx.repoRoot,
|
|
1752
2092
|
ticket_provider: ticketProvider,
|
|
1753
|
-
agents
|
|
1754
|
-
roles.map((role) => [
|
|
1755
|
-
role.agentId || `${slug}-${role.role}`,
|
|
1756
|
-
{
|
|
1757
|
-
role: role.role,
|
|
1758
|
-
role_dir: relative(ctx.repoRoot, role.roleDir)
|
|
1759
|
-
}
|
|
1760
|
-
])
|
|
1761
|
-
)
|
|
2093
|
+
agents
|
|
1762
2094
|
};
|
|
1763
2095
|
}
|
|
1764
2096
|
function projectJsonFinding(ctx) {
|
|
1765
|
-
const projectPath =
|
|
1766
|
-
const planeJsonPath =
|
|
2097
|
+
const projectPath = join9(ctx.repoRoot, ".project.json");
|
|
2098
|
+
const planeJsonPath = join9(ctx.repoRoot, ".plane.json");
|
|
1767
2099
|
const details = [];
|
|
1768
2100
|
const data = readProjectJson(ctx);
|
|
1769
2101
|
const roles = discoverRoles(ctx.repoRoot);
|
|
@@ -1790,7 +2122,7 @@ function projectJsonFinding(ctx) {
|
|
|
1790
2122
|
}
|
|
1791
2123
|
}
|
|
1792
2124
|
const ticketProvider = data.ticket_provider ?? {};
|
|
1793
|
-
for (const key of ["type", "workspace", "identifier", "board_id", "board_url"]) {
|
|
2125
|
+
for (const key of ["type", "workspace", "identifier", "board_id", "board_url", "state"]) {
|
|
1794
2126
|
if (!(key in ticketProvider)) details.push(`ticket_provider.${key} missing`);
|
|
1795
2127
|
}
|
|
1796
2128
|
if (existsSync7(planeJsonPath)) details.push(".plane.json should not exist once .project.json is canonical");
|
|
@@ -1877,9 +2209,9 @@ function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip)
|
|
|
1877
2209
|
if (!existsSync7(sourceDir)) return;
|
|
1878
2210
|
mkdirSync5(targetDir, { recursive: true });
|
|
1879
2211
|
for (const entry of readdirSync(sourceDir, { withFileTypes: true })) {
|
|
1880
|
-
const sourcePath =
|
|
2212
|
+
const sourcePath = join9(sourceDir, entry.name);
|
|
1881
2213
|
if (skip?.(sourcePath)) continue;
|
|
1882
|
-
const targetPath =
|
|
2214
|
+
const targetPath = join9(targetDir, entry.name);
|
|
1883
2215
|
if (entry.isDirectory()) {
|
|
1884
2216
|
copyMissingRecursive(sourcePath, targetPath, changedFiles, dryRun, skip);
|
|
1885
2217
|
continue;
|
|
@@ -1893,7 +2225,7 @@ function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip)
|
|
|
1893
2225
|
}
|
|
1894
2226
|
}
|
|
1895
2227
|
function upsertSubmodule(repoRoot, role, changedFiles, dryRun) {
|
|
1896
|
-
const gitmodulesPath =
|
|
2228
|
+
const gitmodulesPath = join9(repoRoot, ".gitmodules");
|
|
1897
2229
|
const repoName = role.runtimeRepo || `agent-hm-${role.repo}-${role.role}`;
|
|
1898
2230
|
const owner = role.runtimeOwner || "delorenj";
|
|
1899
2231
|
const block = `[submodule "agents/hermes/${role.role}/runtime"]
|
|
@@ -1937,9 +2269,9 @@ ${block}`) : `${current.replace(/\s*$/, "\n")}${block}`;
|
|
|
1937
2269
|
return path;
|
|
1938
2270
|
}
|
|
1939
2271
|
function profileMetaInheritsDefault(path) {
|
|
1940
|
-
const
|
|
2272
|
+
const text4 = safeReadText(path);
|
|
1941
2273
|
return Boolean(
|
|
1942
|
-
|
|
2274
|
+
text4 && /^config:\s*$/m.test(text4) && /^\s+inherit_from:\s*default\s*$/m.test(text4) && /^\s+save_mode:\s*delta\s*$/m.test(text4)
|
|
1943
2275
|
);
|
|
1944
2276
|
}
|
|
1945
2277
|
function upsertInheritedProfileMeta(path, changedFiles, dryRun) {
|
|
@@ -1993,21 +2325,21 @@ var RULES = [
|
|
|
1993
2325
|
id: "mise.config-root",
|
|
1994
2326
|
title: "mise config_root + AGENTS link hooks",
|
|
1995
2327
|
audit: (ctx) => {
|
|
1996
|
-
const misePath =
|
|
2328
|
+
const misePath = join9(ctx.repoRoot, "mise.toml");
|
|
1997
2329
|
if (!existsSync7(misePath)) {
|
|
1998
2330
|
return { id: "mise.config-root", title: "mise config_root + AGENTS link hooks", status: "fail", summary: "mise.toml missing", details: [], fixable: true };
|
|
1999
2331
|
}
|
|
2000
|
-
const
|
|
2332
|
+
const text4 = readText(misePath);
|
|
2001
2333
|
const details = [];
|
|
2002
|
-
const linkAgentfilesPath =
|
|
2334
|
+
const linkAgentfilesPath = join9(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
|
|
2003
2335
|
if (!existsSync7(linkAgentfilesPath)) details.push(".mise/scripts/link-agentfiles.sh missing");
|
|
2004
|
-
const pathValues = [...(
|
|
2336
|
+
const pathValues = [...(text4.match(/^_\.path\s*=\s*\[([^\]]*)\]/m)?.[1] ?? "").matchAll(/"([^"]+)"/g)].map((match) => match[1]);
|
|
2005
2337
|
const missingPathValues = requiredMisePathEntries(ctx).filter((value) => !pathValues.includes(value));
|
|
2006
2338
|
if (missingPathValues.length) details.push(`[env]._.path should include ${missingPathValues.join(", ")}`);
|
|
2007
|
-
if (!
|
|
2008
|
-
if (!
|
|
2009
|
-
if (!
|
|
2010
|
-
if (!
|
|
2339
|
+
if (!text4.includes('"{{config_root}}/.mise/scripts/link-agentfiles.sh"')) details.push("link-agentfiles must use raw {{config_root}} guard");
|
|
2340
|
+
if (!text4.includes("op inject -i .env.op > .env")) details.push("[hooks].enter must materialize .env from .env.op");
|
|
2341
|
+
if (!text4.includes('patterns = ["AGENTS.md"]')) details.push("watch_files must monitor AGENTS.md");
|
|
2342
|
+
if (!text4.includes('task = "link-agentfiles"')) details.push("watch_files must dispatch link-agentfiles task");
|
|
2011
2343
|
return {
|
|
2012
2344
|
id: "mise.config-root",
|
|
2013
2345
|
title: "mise config_root + AGENTS link hooks",
|
|
@@ -2018,19 +2350,26 @@ var RULES = [
|
|
|
2018
2350
|
};
|
|
2019
2351
|
},
|
|
2020
2352
|
migrate: (ctx, finding) => {
|
|
2021
|
-
const path =
|
|
2353
|
+
const path = join9(ctx.repoRoot, "mise.toml");
|
|
2022
2354
|
const changedFiles = [];
|
|
2355
|
+
const details = [];
|
|
2023
2356
|
if (!existsSync7(path)) {
|
|
2024
|
-
|
|
2357
|
+
if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
|
|
2358
|
+
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: [] };
|
|
2359
|
+
}
|
|
2360
|
+
details.push("Initialized mise.toml from generated-project template");
|
|
2361
|
+
if (ctx.dryRun) {
|
|
2362
|
+
return { id: finding.id, title: finding.title, status: "applied", summary: "Would initialize mise.toml from generated-project template", changedFiles, details };
|
|
2363
|
+
}
|
|
2025
2364
|
}
|
|
2026
|
-
let
|
|
2027
|
-
const next = upsertLinkAgentfilesBlock(
|
|
2028
|
-
if (next !==
|
|
2029
|
-
changedFiles.push(path);
|
|
2365
|
+
let text4 = readText(path);
|
|
2366
|
+
const next = upsertLinkAgentfilesBlock(text4, ctx);
|
|
2367
|
+
if (next !== text4) {
|
|
2368
|
+
if (!changedFiles.includes(path)) changedFiles.push(path);
|
|
2030
2369
|
if (!ctx.dryRun) writeText(path, next);
|
|
2031
|
-
|
|
2370
|
+
text4 = next;
|
|
2032
2371
|
}
|
|
2033
|
-
const linkAgentfilesPath =
|
|
2372
|
+
const linkAgentfilesPath = join9(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
|
|
2034
2373
|
const expectedScript = templateLinkAgentfilesScript(ctx);
|
|
2035
2374
|
if (expectedScript === void 0) {
|
|
2036
2375
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "pjangler install is missing .mise/scripts/link-agentfiles.sh \u2014 update @delorenj/pjangler (broken package)", changedFiles, details: [] };
|
|
@@ -2039,7 +2378,7 @@ var RULES = [
|
|
|
2039
2378
|
changedFiles.push(linkAgentfilesPath);
|
|
2040
2379
|
if (!ctx.dryRun) {
|
|
2041
2380
|
writeText(linkAgentfilesPath, expectedScript);
|
|
2042
|
-
|
|
2381
|
+
chmodSync2(linkAgentfilesPath, 493);
|
|
2043
2382
|
}
|
|
2044
2383
|
}
|
|
2045
2384
|
return {
|
|
@@ -2057,11 +2396,11 @@ var RULES = [
|
|
|
2057
2396
|
title: "managed mise versioning block",
|
|
2058
2397
|
audit: (ctx) => {
|
|
2059
2398
|
const details = [];
|
|
2060
|
-
const misePath =
|
|
2061
|
-
const versioningPath =
|
|
2062
|
-
const manifestPath =
|
|
2063
|
-
const
|
|
2064
|
-
if (!
|
|
2399
|
+
const misePath = join9(ctx.repoRoot, "mise.toml");
|
|
2400
|
+
const versioningPath = join9(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
|
|
2401
|
+
const manifestPath = join9(ctx.repoRoot, ".mise", "version-files.conf");
|
|
2402
|
+
const text4 = safeReadText(misePath);
|
|
2403
|
+
if (!text4?.includes("# >>> mise-versioning >>>")) details.push("mise versioning managed block missing");
|
|
2065
2404
|
if (!existsSync7(versioningPath)) details.push(".mise/scripts/versioning.sh missing");
|
|
2066
2405
|
if (!existsSync7(manifestPath)) details.push(".mise/version-files.conf missing");
|
|
2067
2406
|
return {
|
|
@@ -2075,17 +2414,24 @@ var RULES = [
|
|
|
2075
2414
|
},
|
|
2076
2415
|
migrate: (ctx, finding) => {
|
|
2077
2416
|
const changedFiles = [];
|
|
2078
|
-
const
|
|
2417
|
+
const details = [];
|
|
2418
|
+
const misePath = join9(ctx.repoRoot, "mise.toml");
|
|
2079
2419
|
if (!existsSync7(misePath)) {
|
|
2080
|
-
|
|
2420
|
+
if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
|
|
2421
|
+
return { id: finding.id, title: finding.title, status: "blocked", summary: "mise.toml missing and no generated-project mise template available to initialize from", changedFiles, details: [] };
|
|
2422
|
+
}
|
|
2423
|
+
details.push("Initialized mise.toml from generated-project template");
|
|
2424
|
+
if (ctx.dryRun) {
|
|
2425
|
+
return { id: finding.id, title: finding.title, status: "applied", summary: "Would initialize mise.toml from generated-project template", changedFiles, details };
|
|
2426
|
+
}
|
|
2081
2427
|
}
|
|
2082
2428
|
const currentMise = readText(misePath);
|
|
2083
2429
|
const nextMise = replaceOrAppendManagedBlock(currentMise, /# >>> mise-versioning >>>/, VERSIONING_BLOCK, /^\[tasks\.build\]/m);
|
|
2084
2430
|
if (nextMise !== currentMise) {
|
|
2085
|
-
changedFiles.push(misePath);
|
|
2431
|
+
if (!changedFiles.includes(misePath)) changedFiles.push(misePath);
|
|
2086
2432
|
if (!ctx.dryRun) writeText(misePath, nextMise);
|
|
2087
2433
|
}
|
|
2088
|
-
const versioningPath =
|
|
2434
|
+
const versioningPath = join9(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
|
|
2089
2435
|
const expectedScript = templateVersioningScript(ctx);
|
|
2090
2436
|
if (expectedScript === void 0) {
|
|
2091
2437
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "pjangler install is missing .mise/scripts/versioning.sh \u2014 update @delorenj/pjangler (broken package)", changedFiles, details: [] };
|
|
@@ -2094,10 +2440,10 @@ var RULES = [
|
|
|
2094
2440
|
changedFiles.push(versioningPath);
|
|
2095
2441
|
if (!ctx.dryRun) {
|
|
2096
2442
|
writeText(versioningPath, expectedScript);
|
|
2097
|
-
|
|
2443
|
+
chmodSync2(versioningPath, 493);
|
|
2098
2444
|
}
|
|
2099
2445
|
}
|
|
2100
|
-
const manifestPath =
|
|
2446
|
+
const manifestPath = join9(ctx.repoRoot, ".mise", "version-files.conf");
|
|
2101
2447
|
const expectedManifest = templateVersionFilesConf(ctx, ctx.repoRoot);
|
|
2102
2448
|
if (safeReadText(manifestPath) !== expectedManifest) {
|
|
2103
2449
|
changedFiles.push(manifestPath);
|
|
@@ -2117,9 +2463,9 @@ var RULES = [
|
|
|
2117
2463
|
id: "sot.agent-symlinks",
|
|
2118
2464
|
title: "AGENTS/CLAUDE/GEMINI symlink contract",
|
|
2119
2465
|
audit: (ctx) => {
|
|
2120
|
-
const agentsPath =
|
|
2466
|
+
const agentsPath = join9(ctx.repoRoot, "AGENTS.md");
|
|
2121
2467
|
if (!existsSync7(agentsPath)) {
|
|
2122
|
-
const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) => existsSync7(
|
|
2468
|
+
const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) => existsSync7(join9(ctx.repoRoot, file)));
|
|
2123
2469
|
if (fallbackSources.length === 0) {
|
|
2124
2470
|
return { id: "sot.agent-symlinks", title: "AGENTS/CLAUDE/GEMINI symlink contract", status: "skip", summary: "AGENTS.md missing; symlink contract not applicable", details: [], fixable: false };
|
|
2125
2471
|
}
|
|
@@ -2134,7 +2480,7 @@ var RULES = [
|
|
|
2134
2480
|
}
|
|
2135
2481
|
const details = [];
|
|
2136
2482
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
2137
|
-
const full =
|
|
2483
|
+
const full = join9(ctx.repoRoot, file);
|
|
2138
2484
|
const target = readSymlinkTarget(full);
|
|
2139
2485
|
if (target !== "AGENTS.md") details.push(`${file} should be a symlink to AGENTS.md`);
|
|
2140
2486
|
}
|
|
@@ -2158,7 +2504,7 @@ var RULES = [
|
|
|
2158
2504
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "AGENTS.md missing; cannot derive canonical agent file", changedFiles, details: [bootstrap.blocked] };
|
|
2159
2505
|
}
|
|
2160
2506
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
2161
|
-
const full =
|
|
2507
|
+
const full = join9(ctx.repoRoot, file);
|
|
2162
2508
|
const result = ensureSymlink(full, "AGENTS.md", ctx.dryRun);
|
|
2163
2509
|
if (result.blocked) blockedDetails.push(result.blocked);
|
|
2164
2510
|
if (result.changed) changedFiles.push(full);
|
|
@@ -2180,7 +2526,7 @@ var RULES = [
|
|
|
2180
2526
|
migrate: (ctx, finding) => {
|
|
2181
2527
|
const changedFiles = [];
|
|
2182
2528
|
const details = [];
|
|
2183
|
-
const path =
|
|
2529
|
+
const path = join9(ctx.repoRoot, ".project.json");
|
|
2184
2530
|
const existing = readProjectJson(ctx) ?? {};
|
|
2185
2531
|
const canonical = canonicalProjectJson(ctx);
|
|
2186
2532
|
const merged = { ...existing, ...canonical };
|
|
@@ -2190,7 +2536,7 @@ var RULES = [
|
|
|
2190
2536
|
changedFiles.push(path);
|
|
2191
2537
|
if (!ctx.dryRun) writeText(path, expected);
|
|
2192
2538
|
}
|
|
2193
|
-
const planeJson =
|
|
2539
|
+
const planeJson = join9(ctx.repoRoot, ".plane.json");
|
|
2194
2540
|
if (existsSync7(planeJson)) {
|
|
2195
2541
|
const backup = `${planeJson}.migrated-backup`;
|
|
2196
2542
|
if (existsSync7(backup)) {
|
|
@@ -2215,14 +2561,15 @@ var RULES = [
|
|
|
2215
2561
|
title: ".env.op + gitignore secrets contract",
|
|
2216
2562
|
audit: (ctx) => {
|
|
2217
2563
|
const details = [];
|
|
2218
|
-
const envOp = safeReadText(
|
|
2219
|
-
const gitignore = safeReadText(
|
|
2564
|
+
const envOp = safeReadText(join9(ctx.repoRoot, ".env.op"));
|
|
2565
|
+
const gitignore = safeReadText(join9(ctx.repoRoot, ".gitignore"));
|
|
2220
2566
|
if (!envOp) {
|
|
2221
2567
|
details.push(".env.op missing");
|
|
2222
2568
|
} else {
|
|
2223
2569
|
const invalidLines = envOp.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#") && line.includes("=")).filter((line) => {
|
|
2224
2570
|
const value = line.slice(line.indexOf("=") + 1).trim();
|
|
2225
|
-
|
|
2571
|
+
const quotedLiteral = /^"[^"\r\n]*"$/.test(value) || /^'[^'\r\n]*'$/.test(value);
|
|
2572
|
+
return !value.startsWith("op://") && !/^https?:\/\//.test(value) && !/^[A-Za-z0-9_.:-]+$/.test(value) && !quotedLiteral;
|
|
2226
2573
|
});
|
|
2227
2574
|
if (invalidLines.length) details.push(`.env.op has non-reference values that do not look like safe literals: ${invalidLines.join(", ")}`);
|
|
2228
2575
|
}
|
|
@@ -2241,12 +2588,12 @@ var RULES = [
|
|
|
2241
2588
|
migrate: (ctx, finding) => {
|
|
2242
2589
|
const changedFiles = [];
|
|
2243
2590
|
const details = [];
|
|
2244
|
-
const envOpPath =
|
|
2591
|
+
const envOpPath = join9(ctx.repoRoot, ".env.op");
|
|
2245
2592
|
if (!existsSync7(envOpPath)) {
|
|
2246
2593
|
changedFiles.push(envOpPath);
|
|
2247
|
-
if (!ctx.dryRun) writeText(envOpPath, readText(
|
|
2594
|
+
if (!ctx.dryRun) writeText(envOpPath, readText(join9(ctx.pjanglerRoot, "templates", "commonproject", "template", ".env.op")));
|
|
2248
2595
|
}
|
|
2249
|
-
const gitignorePath =
|
|
2596
|
+
const gitignorePath = join9(ctx.repoRoot, ".gitignore");
|
|
2250
2597
|
const gitignore = safeReadText(gitignorePath) ?? "";
|
|
2251
2598
|
const requiredBlock = `# Secrets \u2014 .env is materialized by \`op inject -i .env.op > .env\` on mise enter.
|
|
2252
2599
|
# NEVER commit it. .env.op holds only 1Password references or safe literals and IS committed.
|
|
@@ -2273,20 +2620,20 @@ var RULES = [
|
|
|
2273
2620
|
title: ".copier-answers.yml provenance + drift report",
|
|
2274
2621
|
audit: (ctx) => {
|
|
2275
2622
|
const details = [];
|
|
2276
|
-
const path =
|
|
2277
|
-
const
|
|
2623
|
+
const path = join9(ctx.repoRoot, ".copier-answers.yml");
|
|
2624
|
+
const text4 = safeReadText(path);
|
|
2278
2625
|
const project = readProjectJson(ctx);
|
|
2279
|
-
if (!
|
|
2626
|
+
if (!text4) {
|
|
2280
2627
|
details.push(".copier-answers.yml missing");
|
|
2281
2628
|
} else {
|
|
2282
|
-
if (!
|
|
2283
|
-
if (!
|
|
2629
|
+
if (!text4.startsWith("# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY")) details.push("missing Copier overwrite warning header");
|
|
2630
|
+
if (!text4.includes("_src_path:")) details.push("_src_path missing");
|
|
2284
2631
|
if (project?.project_name) {
|
|
2285
|
-
const nameMatch =
|
|
2632
|
+
const nameMatch = text4.match(/project_name:\s*(.+)/);
|
|
2286
2633
|
if (!nameMatch || nameMatch[1]?.trim() !== String(project.project_name)) details.push("project_name drift between .copier-answers.yml and .project.json");
|
|
2287
2634
|
}
|
|
2288
2635
|
if (project?.project_description) {
|
|
2289
|
-
const descMatch =
|
|
2636
|
+
const descMatch = text4.match(/project_description:\s*([\s\S]*?)(?=\n\w|$)/);
|
|
2290
2637
|
const yamlDesc = descMatch?.[1]?.replace(/\n\s+/g, " ").trim() ?? "";
|
|
2291
2638
|
if (yamlDesc !== String(project.project_description)) details.push("project_description drift between .copier-answers.yml and .project.json");
|
|
2292
2639
|
}
|
|
@@ -2303,16 +2650,16 @@ var RULES = [
|
|
|
2303
2650
|
migrate: (ctx, finding) => {
|
|
2304
2651
|
const changedFiles = [];
|
|
2305
2652
|
const project = canonicalProjectJson(ctx);
|
|
2306
|
-
const
|
|
2307
|
-
_src_path: ${
|
|
2653
|
+
const text4 = `# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
|
|
2654
|
+
_src_path: ${join9(ctx.pjanglerRoot, "templates", "commonproject")}
|
|
2308
2655
|
project_description: ${String(project.project_description)}
|
|
2309
2656
|
project_name: ${String(project.project_name)}
|
|
2310
2657
|
ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
2311
2658
|
`;
|
|
2312
|
-
const path =
|
|
2313
|
-
if (safeReadText(path) !==
|
|
2659
|
+
const path = join9(ctx.repoRoot, ".copier-answers.yml");
|
|
2660
|
+
if (safeReadText(path) !== text4) {
|
|
2314
2661
|
changedFiles.push(path);
|
|
2315
|
-
if (!ctx.dryRun) writeText(path,
|
|
2662
|
+
if (!ctx.dryRun) writeText(path, text4);
|
|
2316
2663
|
}
|
|
2317
2664
|
return {
|
|
2318
2665
|
id: finding.id,
|
|
@@ -2328,15 +2675,15 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
2328
2675
|
id: "bmad.scaffold",
|
|
2329
2676
|
title: "BMAD modules/docs scaffold",
|
|
2330
2677
|
audit: (ctx) => {
|
|
2331
|
-
const sourceRoot =
|
|
2332
|
-
const targetRoot =
|
|
2678
|
+
const sourceRoot = join9(ctx.pjanglerRoot, "templates", "commonproject", "_bmad");
|
|
2679
|
+
const targetRoot = join9(ctx.repoRoot, "_bmad");
|
|
2333
2680
|
const sentinels = [
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2681
|
+
join9("core", "config.yaml"),
|
|
2682
|
+
join9("custom", "config.yaml"),
|
|
2683
|
+
join9("custom", "workflows", "ticket-lifecycle", "workflow.yaml"),
|
|
2684
|
+
join9("bmm", "workflows", "workflow-status", "workflow.yaml")
|
|
2338
2685
|
];
|
|
2339
|
-
const missing = sentinels.filter((file) => existsSync7(
|
|
2686
|
+
const missing = sentinels.filter((file) => existsSync7(join9(sourceRoot, file)) && !existsSync7(join9(targetRoot, file)));
|
|
2340
2687
|
return {
|
|
2341
2688
|
id: "bmad.scaffold",
|
|
2342
2689
|
title: "BMAD modules/docs scaffold",
|
|
@@ -2348,7 +2695,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
2348
2695
|
},
|
|
2349
2696
|
migrate: (ctx, finding) => {
|
|
2350
2697
|
const changedFiles = [];
|
|
2351
|
-
copyMissingRecursive(
|
|
2698
|
+
copyMissingRecursive(join9(ctx.pjanglerRoot, "templates", "commonproject", "_bmad"), join9(ctx.repoRoot, "_bmad"), changedFiles, ctx.dryRun);
|
|
2352
2699
|
return {
|
|
2353
2700
|
id: finding.id,
|
|
2354
2701
|
title: finding.title,
|
|
@@ -2370,11 +2717,11 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
2370
2717
|
}
|
|
2371
2718
|
const details = [];
|
|
2372
2719
|
for (const rel of ["role.yaml", "SOUL.md", "hermes", ".gitignore", ".scripts/70-systemd.sh", ".scripts/heartbeat.sh", ".scripts/checkpoint.sh", ".runtime-scaffold/README.md", "runtime/memories/MEMORY.md", "runtime/bloodbank-consumer.py"]) {
|
|
2373
|
-
if (!existsSync7(
|
|
2720
|
+
if (!existsSync7(join9(role.roleDir, rel))) details.push(`missing ${relative(ctx.repoRoot, join9(role.roleDir, rel))}`);
|
|
2374
2721
|
}
|
|
2375
|
-
const gitmodules = safeReadText(
|
|
2722
|
+
const gitmodules = safeReadText(join9(ctx.repoRoot, ".gitmodules")) ?? "";
|
|
2376
2723
|
if (!gitmodules.includes(`agents/hermes/${role.role}/runtime`)) details.push(".gitmodules missing pm runtime submodule entry");
|
|
2377
|
-
if (!profileMetaInheritsDefault(
|
|
2724
|
+
if (!profileMetaInheritsDefault(join9(role.roleDir, "runtime", "profile.yaml"))) {
|
|
2378
2725
|
details.push("runtime/profile.yaml missing inherited default config metadata");
|
|
2379
2726
|
}
|
|
2380
2727
|
const registry = safeReadText(registryPath(ctx.homeDir));
|
|
@@ -2395,21 +2742,21 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
2395
2742
|
if (!role) {
|
|
2396
2743
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "No pm role present", changedFiles, details: [] };
|
|
2397
2744
|
}
|
|
2398
|
-
const templateRoleDir =
|
|
2399
|
-
writeIfDifferent(
|
|
2400
|
-
writeIfDifferent(
|
|
2401
|
-
writeIfDifferent(
|
|
2402
|
-
copyMissingRecursive(
|
|
2403
|
-
copyMissingRecursive(
|
|
2404
|
-
copyMissingRecursive(
|
|
2405
|
-
const promptSource =
|
|
2406
|
-
const promptTarget =
|
|
2745
|
+
const templateRoleDir = join9(ctx.pjanglerRoot, "templates", "hermes-agent", "template");
|
|
2746
|
+
writeIfDifferent(join9(role.roleDir, "SOUL.md"), renderSoul(role), ctx.dryRun, changedFiles);
|
|
2747
|
+
writeIfDifferent(join9(role.roleDir, "hermes"), renderHermesWrapper(role), ctx.dryRun, changedFiles, 493);
|
|
2748
|
+
writeIfDifferent(join9(role.roleDir, ".gitignore"), readText(join9(templateRoleDir, ".gitignore.jinja")).replace(/\{\{ role \}\}/g, role.role), ctx.dryRun, changedFiles);
|
|
2749
|
+
copyMissingRecursive(join9(templateRoleDir, ".runtime-scaffold"), join9(role.roleDir, ".runtime-scaffold"), changedFiles, ctx.dryRun);
|
|
2750
|
+
copyMissingRecursive(join9(templateRoleDir, ".runtime-scaffold"), join9(role.roleDir, "runtime"), changedFiles, ctx.dryRun);
|
|
2751
|
+
copyMissingRecursive(join9(templateRoleDir, ".scripts"), join9(role.roleDir, ".scripts"), changedFiles, ctx.dryRun, (source) => source.endsWith("sentinel.prompt.md.jinja"));
|
|
2752
|
+
const promptSource = join9(templateRoleDir, ".scripts", "sentinel.prompt.md.jinja");
|
|
2753
|
+
const promptTarget = join9(role.roleDir, ".scripts", "sentinel.prompt.md");
|
|
2407
2754
|
if (existsSync7(promptSource) && !existsSync7(promptTarget)) {
|
|
2408
2755
|
const prompt = readText(promptSource).replace(/\{\{ agent_id \}\}/g, role.agentId).replace(/\{\{ role \}\}/g, role.role).replace(/\{\{ target_repo \}\}/g, role.repo).replace(/\{\{ display_name \}\}/g, role.displayName || role.agentId);
|
|
2409
2756
|
writeIfDifferent(promptTarget, prompt, ctx.dryRun, changedFiles);
|
|
2410
2757
|
}
|
|
2411
2758
|
upsertSubmodule(ctx.repoRoot, role, changedFiles, ctx.dryRun);
|
|
2412
|
-
const profileMetaUpdated = upsertInheritedProfileMeta(
|
|
2759
|
+
const profileMetaUpdated = upsertInheritedProfileMeta(join9(role.roleDir, "runtime", "profile.yaml"), changedFiles, ctx.dryRun);
|
|
2413
2760
|
if (profileMetaUpdated) details.push(`updated ${profileMetaUpdated}`);
|
|
2414
2761
|
const registryUpdated = upsertRegistryEntry(role, ctx.homeDir, changedFiles, ctx.dryRun);
|
|
2415
2762
|
if (registryUpdated) details.push(`updated ${registryUpdated}`);
|
|
@@ -2463,9 +2810,9 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
2463
2810
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "systemd --user unavailable on this host", changedFiles, details };
|
|
2464
2811
|
}
|
|
2465
2812
|
for (const role of roles) {
|
|
2466
|
-
const sysDir =
|
|
2813
|
+
const sysDir = join9(ctx.homeDir, ".config", "systemd", "user");
|
|
2467
2814
|
const units = [`hermes-${role.agentId}-gateway.service`, `hermes-${role.agentId}-consumer.service`, `hermes-${role.agentId}-heartbeat.timer`];
|
|
2468
|
-
const allUnitsPresent = units.every((unit) => existsSync7(
|
|
2815
|
+
const allUnitsPresent = units.every((unit) => existsSync7(join9(sysDir, unit)));
|
|
2469
2816
|
if (allUnitsPresent) {
|
|
2470
2817
|
if (ctx.dryRun) {
|
|
2471
2818
|
details.push(`would run: systemctl --user enable --now ${units.join(" ")}`);
|
|
@@ -2477,7 +2824,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
2477
2824
|
}
|
|
2478
2825
|
continue;
|
|
2479
2826
|
}
|
|
2480
|
-
for (const script of [
|
|
2827
|
+
for (const script of [join9(role.roleDir, ".scripts", "70-systemd.sh")]) {
|
|
2481
2828
|
if (!script || !existsSync7(script)) continue;
|
|
2482
2829
|
if (ctx.dryRun) {
|
|
2483
2830
|
details.push(`would run: bash ${script}`);
|
|
@@ -2505,9 +2852,12 @@ function writeIfDifferent(path, content, dryRun, changedFiles, mode) {
|
|
|
2505
2852
|
changedFiles.push(path);
|
|
2506
2853
|
if (!dryRun) {
|
|
2507
2854
|
writeText(path, normalized);
|
|
2508
|
-
if (mode)
|
|
2855
|
+
if (mode) chmodSync2(path, mode);
|
|
2509
2856
|
}
|
|
2510
2857
|
}
|
|
2858
|
+
function getParityRuleIds() {
|
|
2859
|
+
return RULES.map((rule) => rule.id);
|
|
2860
|
+
}
|
|
2511
2861
|
function runAudit(repoArg) {
|
|
2512
2862
|
const pjanglerRoot = resolvePjanglerRoot();
|
|
2513
2863
|
const ctx = {
|
|
@@ -2524,7 +2874,7 @@ function runAudit(repoArg) {
|
|
|
2524
2874
|
rules
|
|
2525
2875
|
};
|
|
2526
2876
|
}
|
|
2527
|
-
function
|
|
2877
|
+
function runMigrationForRules(ruleIds, repoArg, dryRun) {
|
|
2528
2878
|
const pjanglerRoot = resolvePjanglerRoot();
|
|
2529
2879
|
const ctx = {
|
|
2530
2880
|
repoRoot: resolve(repoArg ?? process.cwd()),
|
|
@@ -2532,9 +2882,9 @@ function runMigration(selector, repoArg, dryRun, all) {
|
|
|
2532
2882
|
pjanglerRoot,
|
|
2533
2883
|
homeDir: homedir4()
|
|
2534
2884
|
};
|
|
2535
|
-
const selected =
|
|
2885
|
+
const selected = RULES.filter((rule) => ruleIds.includes(rule.id));
|
|
2536
2886
|
if (!selected.length) {
|
|
2537
|
-
throw new Error(`Unknown parity
|
|
2887
|
+
throw new Error(`Unknown parity rules: ${ruleIds.join(", ")}`);
|
|
2538
2888
|
}
|
|
2539
2889
|
const results = selected.map((rule) => {
|
|
2540
2890
|
try {
|
|
@@ -2560,43 +2910,490 @@ function runMigration(selector, repoArg, dryRun, all) {
|
|
|
2560
2910
|
changedFiles
|
|
2561
2911
|
};
|
|
2562
2912
|
}
|
|
2913
|
+
function runMigration(selector, repoArg, dryRun, all) {
|
|
2914
|
+
const ruleIds = all ? RULES.map((rule) => rule.id) : selector ? [selector] : [];
|
|
2915
|
+
return runMigrationForRules(ruleIds, repoArg, dryRun);
|
|
2916
|
+
}
|
|
2917
|
+
function prettyTimestamp(iso) {
|
|
2918
|
+
const match = /^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})/.exec(iso);
|
|
2919
|
+
return match ? `${match[1]} ${match[2]} UTC` : iso;
|
|
2920
|
+
}
|
|
2563
2921
|
function formatAuditReport(report) {
|
|
2564
|
-
const
|
|
2922
|
+
const counts = {};
|
|
2923
|
+
for (const rule of report.rules) counts[rule.status] = (counts[rule.status] ?? 0) + 1;
|
|
2924
|
+
const idWidth = report.rules.reduce((width, rule) => Math.max(width, rule.id.length), 0);
|
|
2925
|
+
const tally = [];
|
|
2926
|
+
if (counts.pass) tally.push(green(`${counts.pass} passed`));
|
|
2927
|
+
if (counts.fail) tally.push(red(`${counts.fail} failed`));
|
|
2928
|
+
if (counts.warn) tally.push(yellow(`${counts.warn} warning${counts.warn === 1 ? "" : "s"}`));
|
|
2929
|
+
if (counts.skip) tally.push(gray(`${counts.skip} skipped`));
|
|
2930
|
+
const overall = report.ok ? `${green(glyph.pass)} ${bold("Parity audit passed")}` : `${red(glyph.fail)} ${bold("Parity audit failed")}`;
|
|
2931
|
+
const lines = [""];
|
|
2932
|
+
lines.push(` ${overall}${tally.length ? ` ${dim(glyph.dot)} ${joinDot(tally)}` : ""}`);
|
|
2933
|
+
lines.push(` ${dim(report.repo)} ${dim(glyph.dot)} ${dim(prettyTimestamp(report.auditedAt))}`);
|
|
2934
|
+
lines.push("");
|
|
2565
2935
|
for (const rule of report.rules) {
|
|
2566
|
-
|
|
2567
|
-
|
|
2936
|
+
const style = statusStyle(rule.status);
|
|
2937
|
+
lines.push(` ${style.color(style.glyph)} ${style.color(rule.id.padEnd(idWidth))} ${rule.summary}`);
|
|
2938
|
+
for (const detail of rule.details) lines.push(` ${dim(glyph.arrow)} ${dim(detail)}`);
|
|
2568
2939
|
}
|
|
2569
|
-
|
|
2570
|
-
|
|
2940
|
+
lines.push("");
|
|
2941
|
+
return lines.join("\n");
|
|
2571
2942
|
}
|
|
2572
2943
|
function formatMigrationReport(report) {
|
|
2573
|
-
const
|
|
2944
|
+
const idWidth = report.results.reduce((width, result) => Math.max(width, result.id.length), 0);
|
|
2945
|
+
const overall = report.ok ? `${green(glyph.pass)} ${bold(report.dryRun ? "Migration preview complete" : "Migration complete")}` : `${red(glyph.fail)} ${bold("Migration finished with blockers")}`;
|
|
2946
|
+
const lines = [""];
|
|
2947
|
+
lines.push(` ${overall}${report.dryRun ? ` ${dim(glyph.dot)} ${yellow("dry run")}` : ""}`);
|
|
2948
|
+
lines.push(` ${dim(report.repo)}`);
|
|
2949
|
+
if (report.selectedRules.length) lines.push(` ${dim(`rules: ${report.selectedRules.join(", ")}`)}`);
|
|
2950
|
+
lines.push("");
|
|
2574
2951
|
for (const result of report.results) {
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
for (const
|
|
2952
|
+
const style = statusStyle(result.status);
|
|
2953
|
+
lines.push(` ${style.color(style.glyph)} ${style.color(result.id.padEnd(idWidth))} ${result.summary} ${dim(`[${style.label}]`)}`);
|
|
2954
|
+
for (const detail of result.details) lines.push(` ${dim(glyph.arrow)} ${dim(detail)}`);
|
|
2955
|
+
for (const file of result.changedFiles) lines.push(` ${green(glyph.add)} ${file}`);
|
|
2578
2956
|
}
|
|
2579
2957
|
if (report.changedFiles.length) {
|
|
2580
|
-
lines.push("
|
|
2581
|
-
|
|
2958
|
+
lines.push("");
|
|
2959
|
+
lines.push(` ${bold(`Changed files (${report.changedFiles.length})`)}`);
|
|
2960
|
+
for (const file of report.changedFiles) lines.push(` ${green(glyph.add)} ${file}`);
|
|
2582
2961
|
}
|
|
2583
|
-
|
|
2962
|
+
lines.push("");
|
|
2963
|
+
return lines.join("\n");
|
|
2964
|
+
}
|
|
2965
|
+
|
|
2966
|
+
// src/project/index.ts
|
|
2967
|
+
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
2968
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync4, renameSync as renameSync2, statSync, writeFileSync as writeFileSync5 } from "node:fs";
|
|
2969
|
+
import { homedir as homedir5 } from "node:os";
|
|
2970
|
+
import { basename as basename3, dirname as dirname6, join as join10, resolve as resolve2 } from "node:path";
|
|
2971
|
+
import YAML from "yaml";
|
|
2972
|
+
var PROJECT_REGISTRY_ENV = "PJ_PROJECT_REGISTRY";
|
|
2973
|
+
var PROJECT_REGISTRY_SCHEMA_VERSION = 1;
|
|
2974
|
+
var KNOWN_SKILL_ROOTS = [
|
|
2975
|
+
"/home/delorenj/code/skillex/all-skills",
|
|
2976
|
+
"/home/delorenj/code/CoachingAgentFramework/.agents/skills",
|
|
2977
|
+
"/home/delorenj/code/pjangler/.agents/skills",
|
|
2978
|
+
join10(homedir5(), ".codex", "skills")
|
|
2979
|
+
];
|
|
2980
|
+
function projectRegistryPath(env2 = process.env) {
|
|
2981
|
+
return expandHome(env2[PROJECT_REGISTRY_ENV] || join10(homedir5(), ".config", "pjangler", "projects.yaml"));
|
|
2982
|
+
}
|
|
2983
|
+
function emptyProjectRegistry() {
|
|
2984
|
+
return { schema_version: PROJECT_REGISTRY_SCHEMA_VERSION, projects: {} };
|
|
2985
|
+
}
|
|
2986
|
+
function loadProjectRegistry(path = projectRegistryPath()) {
|
|
2987
|
+
if (!existsSync8(path)) return emptyProjectRegistry();
|
|
2988
|
+
const raw = YAML.parse(readFileSync4(path, "utf8"));
|
|
2989
|
+
if (raw == null) return emptyProjectRegistry();
|
|
2990
|
+
if (!isRecord(raw)) throw new Error(`Project registry must be a mapping: ${path}`);
|
|
2991
|
+
const registry = raw;
|
|
2992
|
+
const normalized = {
|
|
2993
|
+
schema_version: Number(registry.schema_version ?? PROJECT_REGISTRY_SCHEMA_VERSION),
|
|
2994
|
+
projects: isRecord(registry.projects) ? registry.projects : {}
|
|
2995
|
+
};
|
|
2996
|
+
validateProjectRegistry(normalized);
|
|
2997
|
+
return normalized;
|
|
2998
|
+
}
|
|
2999
|
+
function saveProjectRegistry(registry, path = projectRegistryPath()) {
|
|
3000
|
+
validateProjectRegistry(registry);
|
|
3001
|
+
mkdirSync6(dirname6(path), { recursive: true });
|
|
3002
|
+
const temp = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
3003
|
+
writeFileSync5(temp, YAML.stringify(registry, { lineWidth: 0 }), "utf8");
|
|
3004
|
+
renameSync2(temp, path);
|
|
3005
|
+
}
|
|
3006
|
+
function validateProjectRegistry(registry) {
|
|
3007
|
+
if (registry.schema_version !== PROJECT_REGISTRY_SCHEMA_VERSION) {
|
|
3008
|
+
throw new Error(`Unsupported project registry schema_version: ${registry.schema_version}`);
|
|
3009
|
+
}
|
|
3010
|
+
if (!isRecord(registry.projects)) throw new Error("Project registry projects must be a mapping");
|
|
3011
|
+
const slugs = /* @__PURE__ */ new Set();
|
|
3012
|
+
const repoPaths = /* @__PURE__ */ new Map();
|
|
3013
|
+
const identifiers = /* @__PURE__ */ new Map();
|
|
3014
|
+
for (const [slug, project] of Object.entries(registry.projects)) {
|
|
3015
|
+
validateProjectRecord(project, slug);
|
|
3016
|
+
if (slugs.has(project.slug)) throw new Error(`Duplicate project slug: ${project.slug}`);
|
|
3017
|
+
slugs.add(project.slug);
|
|
3018
|
+
const repoKey = resolve2(project.repo_path);
|
|
3019
|
+
const existingRepoSlug = repoPaths.get(repoKey);
|
|
3020
|
+
if (existingRepoSlug && existingRepoSlug !== slug) {
|
|
3021
|
+
throw new Error(`Duplicate project repo_path: ${project.repo_path} used by ${existingRepoSlug} and ${slug}`);
|
|
3022
|
+
}
|
|
3023
|
+
repoPaths.set(repoKey, slug);
|
|
3024
|
+
const identifier = project.ticket_provider.identifier?.toUpperCase();
|
|
3025
|
+
if (identifier) {
|
|
3026
|
+
const existingIdentifierSlug = identifiers.get(identifier);
|
|
3027
|
+
if (existingIdentifierSlug && existingIdentifierSlug !== slug) {
|
|
3028
|
+
throw new Error(`Duplicate project identifier: ${identifier} used by ${existingIdentifierSlug} and ${slug}`);
|
|
3029
|
+
}
|
|
3030
|
+
identifiers.set(identifier, slug);
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
}
|
|
3034
|
+
function slugifyProjectName(value) {
|
|
3035
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "project";
|
|
3036
|
+
}
|
|
3037
|
+
function deriveProjectIdentifier(value) {
|
|
3038
|
+
const compact = value.replace(/[^A-Za-z0-9]/g, "").toUpperCase();
|
|
3039
|
+
const identifier = compact.slice(0, 4) || "PROJ";
|
|
3040
|
+
return identifier.length >= 2 ? identifier : `${identifier}XX`.slice(0, 4);
|
|
3041
|
+
}
|
|
3042
|
+
function normalizeAgentRole(value) {
|
|
3043
|
+
return value?.trim() || "pm";
|
|
3044
|
+
}
|
|
3045
|
+
function jsonStable(value) {
|
|
3046
|
+
return JSON.stringify(value);
|
|
3047
|
+
}
|
|
3048
|
+
function projectRecordEquivalent(a, b) {
|
|
3049
|
+
if (!a) return false;
|
|
3050
|
+
const { created_at: _aCreated, updated_at: _aUpdated, ...aComparable } = a;
|
|
3051
|
+
const { created_at: _bCreated, updated_at: _bUpdated, ...bComparable } = b;
|
|
3052
|
+
return jsonStable(aComparable) === jsonStable(bComparable);
|
|
3053
|
+
}
|
|
3054
|
+
function defaultProjectTargetDir(name, cwd = process.cwd()) {
|
|
3055
|
+
const compactName = name.replace(/[^A-Za-z0-9._-]/g, "") || slugifyProjectName(name);
|
|
3056
|
+
return resolve2(dirname6(resolve2(cwd)), compactName);
|
|
3057
|
+
}
|
|
3058
|
+
function resolveSourceSkillPath(sourceSkill) {
|
|
3059
|
+
if (!sourceSkill) return void 0;
|
|
3060
|
+
const expanded = expandHome(sourceSkill);
|
|
3061
|
+
const direct = resolve2(expanded);
|
|
3062
|
+
if (existsSync8(direct)) return direct;
|
|
3063
|
+
const name = basename3(sourceSkill);
|
|
3064
|
+
for (const root of KNOWN_SKILL_ROOTS) {
|
|
3065
|
+
const candidate = join10(root, name);
|
|
3066
|
+
if (existsSync8(candidate)) return candidate;
|
|
3067
|
+
}
|
|
3068
|
+
const civilWarLetterifier = "/home/delorenj/code/skillex/all-skills/civilwar-letterifier";
|
|
3069
|
+
const hint = existsSync8(civilWarLetterifier) ? ` Did you mean ${civilWarLetterifier}?` : "";
|
|
3070
|
+
throw new Error(`Source skill not found: ${sourceSkill}.${hint}`);
|
|
3071
|
+
}
|
|
3072
|
+
function planProjectInit(input) {
|
|
3073
|
+
if (!input.name.trim()) throw new Error("Project name is required");
|
|
3074
|
+
const registryPath2 = resolve2(projectRegistryPath({ ...process.env, [PROJECT_REGISTRY_ENV]: input.registryPath || process.env[PROJECT_REGISTRY_ENV] }));
|
|
3075
|
+
const registry = loadProjectRegistry(registryPath2);
|
|
3076
|
+
const now = (input.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
3077
|
+
const slug = input.projectSlug ?? slugifyProjectName(input.name);
|
|
3078
|
+
const targetDir = resolve2(input.targetDir ?? defaultProjectTargetDir(input.name, input.cwd));
|
|
3079
|
+
const identifier = (input.projectIdentifier ?? deriveProjectIdentifier(input.name)).toUpperCase();
|
|
3080
|
+
const existing = registry.projects[slug];
|
|
3081
|
+
const sourceSkillPath = resolveSourceSkillPath(input.sourceSkill);
|
|
3082
|
+
const overwrite = input.overwrite ?? input.force ?? false;
|
|
3083
|
+
const agentRole = normalizeAgentRole(input.agentRole);
|
|
3084
|
+
const agents = input.provisionAgent ? {
|
|
3085
|
+
...existing?.agents ?? {},
|
|
3086
|
+
[agentRole]: {
|
|
3087
|
+
role: agentRole,
|
|
3088
|
+
provisioning_state: "planned"
|
|
3089
|
+
}
|
|
3090
|
+
} : existing?.agents ?? {};
|
|
3091
|
+
const scaffold = input.scaffold ?? true;
|
|
3092
|
+
const candidateProject = {
|
|
3093
|
+
name: input.name,
|
|
3094
|
+
slug,
|
|
3095
|
+
repo_path: targetDir,
|
|
3096
|
+
description: input.description ?? "",
|
|
3097
|
+
status: "planned",
|
|
3098
|
+
source_artifacts: sourceSkillPath ? [{ kind: "skill", path: sourceSkillPath, package_name: input.packageName ?? slug }] : [],
|
|
3099
|
+
template: {
|
|
3100
|
+
commonproject: {
|
|
3101
|
+
enabled: true,
|
|
3102
|
+
primary_language: input.primaryLanguage ?? "python"
|
|
3103
|
+
}
|
|
3104
|
+
},
|
|
3105
|
+
ticket_provider: {
|
|
3106
|
+
type: input.ticketProvider ?? "plane",
|
|
3107
|
+
workspace: input.planeWorkspace ?? "33god",
|
|
3108
|
+
identifier,
|
|
3109
|
+
board_id: input.planeProjectId ?? "",
|
|
3110
|
+
board_url: input.planeProjectId ? `https://plane.delo.sh/${input.planeWorkspace ?? "33god"}/projects/${input.planeProjectId}/issues/` : "",
|
|
3111
|
+
state: input.live ? "planned" : "planned"
|
|
3112
|
+
},
|
|
3113
|
+
agents,
|
|
3114
|
+
created_at: existing?.created_at ?? now,
|
|
3115
|
+
updated_at: now
|
|
3116
|
+
};
|
|
3117
|
+
const project = {
|
|
3118
|
+
...candidateProject,
|
|
3119
|
+
updated_at: projectRecordEquivalent(existing, candidateProject) ? existing.updated_at : now
|
|
3120
|
+
};
|
|
3121
|
+
validateNoDuplicateProject(registry, project, overwrite);
|
|
3122
|
+
const pjanglerRoot = resolve2(input.pjanglerRoot ?? resolvePjanglerRoot2());
|
|
3123
|
+
const manifest = projectManifestFromRegistryProject(project);
|
|
3124
|
+
const apply = input.apply ?? false;
|
|
3125
|
+
const live = input.live ?? false;
|
|
3126
|
+
const actions = [
|
|
3127
|
+
{ kind: "registry.upsert", registryPath: registryPath2, slug, project }
|
|
3128
|
+
];
|
|
3129
|
+
if (scaffold) {
|
|
3130
|
+
actions.push(buildCommonProjectCopierAction({
|
|
3131
|
+
pjanglerRoot,
|
|
3132
|
+
targetDir,
|
|
3133
|
+
projectName: project.name,
|
|
3134
|
+
projectDescription: project.description,
|
|
3135
|
+
projectSlug: project.slug,
|
|
3136
|
+
ticketProvider: project.ticket_provider.type,
|
|
3137
|
+
planeWorkspace: project.ticket_provider.workspace ?? "33god",
|
|
3138
|
+
planeProjectId: project.ticket_provider.board_id ?? "",
|
|
3139
|
+
projectIdentifier: identifier,
|
|
3140
|
+
primaryLanguage: project.template.commonproject.primary_language,
|
|
3141
|
+
overwrite
|
|
3142
|
+
}));
|
|
3143
|
+
}
|
|
3144
|
+
actions.push(
|
|
3145
|
+
{ kind: "project.write-manifest", path: join10(targetDir, ".project.json"), manifest },
|
|
3146
|
+
{
|
|
3147
|
+
kind: "plane.create-or-link",
|
|
3148
|
+
enabled: live,
|
|
3149
|
+
live,
|
|
3150
|
+
workspace: project.ticket_provider.workspace ?? "33god",
|
|
3151
|
+
identifier,
|
|
3152
|
+
state: live ? "planned" : "planned",
|
|
3153
|
+
reason: live ? void 0 : "network/cloud actions require --live"
|
|
3154
|
+
},
|
|
3155
|
+
{
|
|
3156
|
+
kind: "hermes.provision-agent",
|
|
3157
|
+
enabled: input.provisionAgent ?? false,
|
|
3158
|
+
local: !live,
|
|
3159
|
+
targetDir,
|
|
3160
|
+
targetRepo: slug,
|
|
3161
|
+
role: agentRole,
|
|
3162
|
+
context: {
|
|
3163
|
+
skipRuntimeRepo: !live,
|
|
3164
|
+
skipPlane: !live,
|
|
3165
|
+
skipBloodbank: !live,
|
|
3166
|
+
skipSystemd: !live || process.platform === "darwin"
|
|
3167
|
+
}
|
|
3168
|
+
}
|
|
3169
|
+
);
|
|
3170
|
+
return { ok: true, apply, dryRun: !apply, live, registryPath: registryPath2, project, manifest, actions };
|
|
3171
|
+
}
|
|
3172
|
+
function executeProjectInitPlan(plan) {
|
|
3173
|
+
const logs = [];
|
|
3174
|
+
const errors = [];
|
|
3175
|
+
const changedFiles = [];
|
|
3176
|
+
if (!plan.apply) return { ok: true, plan, logs, errors, changedFiles };
|
|
3177
|
+
const registry = loadProjectRegistry(plan.registryPath);
|
|
3178
|
+
let pendingRegistryAction;
|
|
3179
|
+
for (const action of plan.actions) {
|
|
3180
|
+
if (action.kind === "copier.copy.commonproject") {
|
|
3181
|
+
mkdirSync6(dirname6(action.targetDir), { recursive: true });
|
|
3182
|
+
const result = spawnSync5(action.command[0], action.command.slice(1), { encoding: "utf8", cwd: action.cwd });
|
|
3183
|
+
if (result.stdout?.trim()) logs.push(result.stdout.trim());
|
|
3184
|
+
if (result.stderr?.trim()) logs.push(result.stderr.trim());
|
|
3185
|
+
if (result.error) {
|
|
3186
|
+
const code = result.error.code;
|
|
3187
|
+
errors.push(
|
|
3188
|
+
code === "ENOENT" ? "copier not found on PATH. Install with: uv tool install copier or pip install copier" : `copier failed: ${result.error.message}`
|
|
3189
|
+
);
|
|
3190
|
+
break;
|
|
3191
|
+
}
|
|
3192
|
+
if (result.status !== 0) {
|
|
3193
|
+
errors.push(`copier exited with status ${result.status ?? "unknown"}`);
|
|
3194
|
+
if (existsSync8(action.targetDir)) changedFiles.push(action.targetDir);
|
|
3195
|
+
break;
|
|
3196
|
+
}
|
|
3197
|
+
changedFiles.push(action.targetDir);
|
|
3198
|
+
} else if (action.kind === "project.write-manifest") {
|
|
3199
|
+
mkdirSync6(dirname6(action.path), { recursive: true });
|
|
3200
|
+
const next = `${JSON.stringify(action.manifest, null, 2)}
|
|
2584
3201
|
`;
|
|
3202
|
+
const current = existsSync8(action.path) ? readFileSync4(action.path, "utf8") : void 0;
|
|
3203
|
+
if (current !== next) {
|
|
3204
|
+
writeFileSync5(action.path, next, "utf8");
|
|
3205
|
+
changedFiles.push(action.path);
|
|
3206
|
+
}
|
|
3207
|
+
} else if (action.kind === "registry.upsert") {
|
|
3208
|
+
pendingRegistryAction = action;
|
|
3209
|
+
} else if (action.kind === "plane.create-or-link") {
|
|
3210
|
+
logs.push(action.enabled ? "plane.create-or-link requires a live provider integration" : "plane.create-or-link skipped (requires --live)");
|
|
3211
|
+
} else if (action.kind === "hermes.provision-agent") {
|
|
3212
|
+
logs.push(action.enabled ? "hermes.provision-agent planned for the caller to execute" : "hermes.provision-agent skipped");
|
|
3213
|
+
}
|
|
3214
|
+
}
|
|
3215
|
+
if (pendingRegistryAction && errors.length === 0) {
|
|
3216
|
+
if (!projectRecordEquivalent(registry.projects[pendingRegistryAction.slug], pendingRegistryAction.project)) {
|
|
3217
|
+
registry.projects[pendingRegistryAction.slug] = pendingRegistryAction.project;
|
|
3218
|
+
saveProjectRegistry(registry, pendingRegistryAction.registryPath);
|
|
3219
|
+
changedFiles.push(pendingRegistryAction.registryPath);
|
|
3220
|
+
}
|
|
3221
|
+
}
|
|
3222
|
+
return { ok: errors.length === 0, plan, logs, errors, changedFiles };
|
|
3223
|
+
}
|
|
3224
|
+
function projectManifestFromRegistryProject(project) {
|
|
3225
|
+
const agents = Object.fromEntries(
|
|
3226
|
+
Object.entries(project.agents).map(([name, agent]) => [
|
|
3227
|
+
`${project.slug}-${name}`,
|
|
3228
|
+
{
|
|
3229
|
+
role: agent.role,
|
|
3230
|
+
role_dir: agent.role_dir,
|
|
3231
|
+
provisioning_state: agent.provisioning_state
|
|
3232
|
+
}
|
|
3233
|
+
])
|
|
3234
|
+
);
|
|
3235
|
+
return {
|
|
3236
|
+
project_name: project.name,
|
|
3237
|
+
project_description: project.description,
|
|
3238
|
+
project_slug: project.slug,
|
|
3239
|
+
repo_path: project.repo_path,
|
|
3240
|
+
ticket_provider: {
|
|
3241
|
+
type: project.ticket_provider.type,
|
|
3242
|
+
workspace: project.ticket_provider.workspace ?? "",
|
|
3243
|
+
identifier: project.ticket_provider.identifier ?? "",
|
|
3244
|
+
board_id: project.ticket_provider.board_id ?? "",
|
|
3245
|
+
board_url: project.ticket_provider.board_url ?? "",
|
|
3246
|
+
state: project.ticket_provider.state
|
|
3247
|
+
},
|
|
3248
|
+
agents
|
|
3249
|
+
};
|
|
3250
|
+
}
|
|
3251
|
+
function formatProjectInitPlan(plan) {
|
|
3252
|
+
const lines = [""];
|
|
3253
|
+
const title = `${bold(plan.project.name)} ${dim(`(${plan.project.slug})`)}`;
|
|
3254
|
+
lines.push(` ${cyan(bold(glyph.chevron))} ${title}${plan.dryRun ? ` ${dim(glyph.dot)} ${yellow("dry run")}` : ""}`);
|
|
3255
|
+
lines.push(` ${dim("registry".padEnd(8))} ${dim(plan.registryPath)}`);
|
|
3256
|
+
lines.push(` ${dim("target".padEnd(8))} ${dim(plan.project.repo_path)}`);
|
|
3257
|
+
lines.push("");
|
|
3258
|
+
lines.push(` ${bold("Actions")} ${dim(`(${plan.actions.length})`)}`);
|
|
3259
|
+
if (!plan.actions.length) lines.push(` ${dim("(nothing to do)")}`);
|
|
3260
|
+
for (const action of plan.actions) {
|
|
3261
|
+
lines.push(` ${cyan(glyph.bullet)} ${action.kind}`);
|
|
3262
|
+
if (action.kind === "copier.copy.commonproject") lines.push(` ${dim(`target: ${action.targetDir}`)}`);
|
|
3263
|
+
if (action.kind === "project.write-manifest") lines.push(` ${dim(`path: ${action.path}`)}`);
|
|
3264
|
+
if (action.kind === "plane.create-or-link" && action.reason) lines.push(` ${dim(`note: ${action.reason}`)}`);
|
|
3265
|
+
}
|
|
3266
|
+
lines.push("");
|
|
3267
|
+
return lines.join("\n");
|
|
3268
|
+
}
|
|
3269
|
+
function formatProjectList(registry) {
|
|
3270
|
+
const projects = Object.values(registry.projects).sort((a, b) => a.slug.localeCompare(b.slug));
|
|
3271
|
+
if (!projects.length) return `
|
|
3272
|
+
${dim("No projects registered.")}
|
|
3273
|
+
`;
|
|
3274
|
+
const slugWidth = projects.reduce((width, project) => Math.max(width, project.slug.length), 0);
|
|
3275
|
+
const idWidth = projects.reduce((width, project) => Math.max(width, String(project.ticket_provider.identifier ?? "").length), 0);
|
|
3276
|
+
const statusWidth = projects.reduce((width, project) => Math.max(width, project.status.length), 0);
|
|
3277
|
+
const lines = ["", ` ${bold("Projects")} ${dim(`(${projects.length})`)}`, ""];
|
|
3278
|
+
for (const project of projects) {
|
|
3279
|
+
const slug = bold(project.slug.padEnd(slugWidth));
|
|
3280
|
+
const identifier = cyan(String(project.ticket_provider.identifier ?? "").padEnd(idWidth));
|
|
3281
|
+
const status = projectStatusColor(project.status)(project.status.padEnd(statusWidth));
|
|
3282
|
+
lines.push(` ${slug} ${identifier} ${status} ${dim(project.repo_path)}`);
|
|
3283
|
+
}
|
|
3284
|
+
lines.push("");
|
|
3285
|
+
return lines.join("\n");
|
|
3286
|
+
}
|
|
3287
|
+
function getProject(registry, slug) {
|
|
3288
|
+
const project = registry.projects[slug];
|
|
3289
|
+
if (!project) throw new Error(`Project not found in registry: ${slug}`);
|
|
3290
|
+
return project;
|
|
3291
|
+
}
|
|
3292
|
+
function doctorProjectRegistry(registryPath2 = projectRegistryPath(), slug) {
|
|
3293
|
+
const issues = [];
|
|
3294
|
+
const registry = loadProjectRegistry(registryPath2);
|
|
3295
|
+
const projects = slug ? [[slug, getProject(registry, slug)]] : Object.entries(registry.projects);
|
|
3296
|
+
for (const [projectSlug, project] of projects) {
|
|
3297
|
+
if (!existsSync8(project.repo_path)) {
|
|
3298
|
+
issues.push({ level: "warn", slug: projectSlug, message: `repo_path does not exist: ${project.repo_path}` });
|
|
3299
|
+
} else if (!statSync(project.repo_path).isDirectory()) {
|
|
3300
|
+
issues.push({ level: "error", slug: projectSlug, message: `repo_path is not a directory: ${project.repo_path}` });
|
|
3301
|
+
} else {
|
|
3302
|
+
const manifestPath = join10(project.repo_path, ".project.json");
|
|
3303
|
+
if (!existsSync8(manifestPath)) issues.push({ level: "warn", slug: projectSlug, message: ".project.json is missing" });
|
|
3304
|
+
}
|
|
3305
|
+
for (const artifact of project.source_artifacts) {
|
|
3306
|
+
if (artifact.path && !existsSync8(artifact.path)) {
|
|
3307
|
+
issues.push({ level: "warn", slug: projectSlug, message: `source artifact missing: ${artifact.path}` });
|
|
3308
|
+
}
|
|
3309
|
+
}
|
|
3310
|
+
}
|
|
3311
|
+
return {
|
|
3312
|
+
ok: !issues.some((issue) => issue.level === "error"),
|
|
3313
|
+
registryPath: registryPath2,
|
|
3314
|
+
checkedProjects: projects.map(([projectSlug]) => projectSlug),
|
|
3315
|
+
issues
|
|
3316
|
+
};
|
|
3317
|
+
}
|
|
3318
|
+
function buildCommonProjectCopierAction(input) {
|
|
3319
|
+
const templateDir = join10(input.pjanglerRoot, "templates", "commonproject");
|
|
3320
|
+
const data = {
|
|
3321
|
+
project_name: input.projectName,
|
|
3322
|
+
project_description: input.projectDescription ?? "",
|
|
3323
|
+
project_slug: input.projectSlug,
|
|
3324
|
+
ticket_provider: input.ticketProvider,
|
|
3325
|
+
plane_workspace: input.planeWorkspace,
|
|
3326
|
+
plane_project_id: input.planeProjectId ?? "",
|
|
3327
|
+
project_identifier: input.projectIdentifier,
|
|
3328
|
+
primary_language: input.primaryLanguage
|
|
3329
|
+
};
|
|
3330
|
+
const command = ["copier", "copy", "--trust", templateDir, input.targetDir, "--defaults"];
|
|
3331
|
+
for (const [key, value] of Object.entries(data)) command.push("--data", `${key}=${value}`);
|
|
3332
|
+
if (input.overwrite) command.push("--overwrite");
|
|
3333
|
+
return {
|
|
3334
|
+
kind: "copier.copy.commonproject",
|
|
3335
|
+
cwd: input.pjanglerRoot,
|
|
3336
|
+
command,
|
|
3337
|
+
targetDir: input.targetDir,
|
|
3338
|
+
data,
|
|
3339
|
+
overwrite: input.overwrite
|
|
3340
|
+
};
|
|
3341
|
+
}
|
|
3342
|
+
function resolvePjanglerRoot2() {
|
|
3343
|
+
let dir = dirname6(new URL(import.meta.url).pathname);
|
|
3344
|
+
while (dir !== dirname6(dir)) {
|
|
3345
|
+
if (existsSync8(join10(dir, "package.json")) && existsSync8(join10(dir, "templates", "commonproject", "copier.yml"))) return dir;
|
|
3346
|
+
dir = dirname6(dir);
|
|
3347
|
+
}
|
|
3348
|
+
return resolve2(process.cwd());
|
|
3349
|
+
}
|
|
3350
|
+
function validateNoDuplicateProject(registry, project, overwrite) {
|
|
3351
|
+
const existingSameSlug = registry.projects[project.slug];
|
|
3352
|
+
if (existingSameSlug && !overwrite && resolve2(existingSameSlug.repo_path) !== resolve2(project.repo_path)) {
|
|
3353
|
+
throw new Error(`Project slug already exists in registry: ${project.slug}`);
|
|
3354
|
+
}
|
|
3355
|
+
for (const [slug, existing] of Object.entries(registry.projects)) {
|
|
3356
|
+
if (slug === project.slug) continue;
|
|
3357
|
+
if (resolve2(existing.repo_path) === resolve2(project.repo_path)) {
|
|
3358
|
+
throw new Error(`Project repo_path already registered by ${slug}: ${project.repo_path}`);
|
|
3359
|
+
}
|
|
3360
|
+
if (existing.ticket_provider.identifier && existing.ticket_provider.identifier.toUpperCase() === project.ticket_provider.identifier?.toUpperCase()) {
|
|
3361
|
+
throw new Error(`Project identifier already registered by ${slug}: ${project.ticket_provider.identifier}`);
|
|
3362
|
+
}
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3365
|
+
function validateProjectRecord(project, key) {
|
|
3366
|
+
if (!isRecord(project)) throw new Error(`Project ${key} must be a mapping`);
|
|
3367
|
+
if (!project.name) throw new Error(`Project ${key} missing name`);
|
|
3368
|
+
if (!project.slug) throw new Error(`Project ${key} missing slug`);
|
|
3369
|
+
if (project.slug !== key) throw new Error(`Project key ${key} does not match slug ${project.slug}`);
|
|
3370
|
+
if (!project.repo_path) throw new Error(`Project ${key} missing repo_path`);
|
|
3371
|
+
if (!Array.isArray(project.source_artifacts)) throw new Error(`Project ${key} source_artifacts must be a list`);
|
|
3372
|
+
if (!isRecord(project.ticket_provider)) throw new Error(`Project ${key} ticket_provider must be a mapping`);
|
|
3373
|
+
if (!isRecord(project.agents)) throw new Error(`Project ${key} agents must be a mapping`);
|
|
3374
|
+
}
|
|
3375
|
+
function expandHome(path) {
|
|
3376
|
+
if (path === "~") return homedir5();
|
|
3377
|
+
if (path.startsWith("~/")) return join10(homedir5(), path.slice(2));
|
|
3378
|
+
return path;
|
|
3379
|
+
}
|
|
3380
|
+
function isRecord(value) {
|
|
3381
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2585
3382
|
}
|
|
2586
3383
|
|
|
2587
3384
|
// src/utils/version.ts
|
|
2588
|
-
import { readFileSync as
|
|
2589
|
-
import { dirname as
|
|
3385
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
3386
|
+
import { dirname as dirname7, join as join11 } from "node:path";
|
|
2590
3387
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
2591
3388
|
var PJANGLER_VERSION = (() => {
|
|
2592
3389
|
try {
|
|
2593
|
-
let dir =
|
|
3390
|
+
let dir = dirname7(fileURLToPath4(import.meta.url));
|
|
2594
3391
|
for (let i = 0; i < 4; i++) {
|
|
2595
3392
|
try {
|
|
2596
|
-
const raw =
|
|
3393
|
+
const raw = readFileSync5(join11(dir, "package.json"), "utf8");
|
|
2597
3394
|
return JSON.parse(raw).version ?? "0.0.0";
|
|
2598
3395
|
} catch {
|
|
2599
|
-
const parent =
|
|
3396
|
+
const parent = dirname7(dir);
|
|
2600
3397
|
if (parent === dir) break;
|
|
2601
3398
|
dir = parent;
|
|
2602
3399
|
}
|
|
@@ -2607,6 +3404,191 @@ var PJANGLER_VERSION = (() => {
|
|
|
2607
3404
|
})();
|
|
2608
3405
|
|
|
2609
3406
|
// src/index.ts
|
|
3407
|
+
var xmark = `${red(glyph.fail)}`;
|
|
3408
|
+
function printMigrationReport(report, asJson) {
|
|
3409
|
+
if (asJson) {
|
|
3410
|
+
console.log(JSON.stringify(report, null, 2));
|
|
3411
|
+
} else {
|
|
3412
|
+
console.log(formatMigrationReport(report));
|
|
3413
|
+
}
|
|
3414
|
+
}
|
|
3415
|
+
async function promptForRuleIds(rules) {
|
|
3416
|
+
const options = rules.filter((rule) => rule.fixable).map((rule) => ({
|
|
3417
|
+
value: rule.id,
|
|
3418
|
+
label: `${rule.id} [${rule.status}] ${rule.title}`,
|
|
3419
|
+
hint: rule.summary
|
|
3420
|
+
}));
|
|
3421
|
+
if (!options.length) {
|
|
3422
|
+
return [];
|
|
3423
|
+
}
|
|
3424
|
+
const initialValues = rules.filter((rule) => rule.fixable && rule.status !== "pass" && rule.status !== "skip").map((rule) => rule.id);
|
|
3425
|
+
const selected = await multiselect({
|
|
3426
|
+
message: "Select parity rules to apply (space to toggle, enter to confirm):",
|
|
3427
|
+
options,
|
|
3428
|
+
initialValues
|
|
3429
|
+
});
|
|
3430
|
+
if (isCancel5(selected)) {
|
|
3431
|
+
return [];
|
|
3432
|
+
}
|
|
3433
|
+
return selected;
|
|
3434
|
+
}
|
|
3435
|
+
function readJson(path) {
|
|
3436
|
+
if (!existsSync9(path)) return void 0;
|
|
3437
|
+
try {
|
|
3438
|
+
const parsed = JSON.parse(readFileSync6(path, "utf8"));
|
|
3439
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
|
|
3440
|
+
} catch {
|
|
3441
|
+
return void 0;
|
|
3442
|
+
}
|
|
3443
|
+
}
|
|
3444
|
+
function findGitRoot(cwd) {
|
|
3445
|
+
const result = spawnSync6("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf8" });
|
|
3446
|
+
if (result.status !== 0) return void 0;
|
|
3447
|
+
return resolve3(result.stdout.trim());
|
|
3448
|
+
}
|
|
3449
|
+
function packageNameToProjectName(value) {
|
|
3450
|
+
if (!value) return void 0;
|
|
3451
|
+
const name = value.split("/").pop() ?? value;
|
|
3452
|
+
return name.replace(/[-_]+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase()).trim();
|
|
3453
|
+
}
|
|
3454
|
+
function deriveProjectDefaults(targetDir) {
|
|
3455
|
+
const manifest = readJson(join12(targetDir, ".project.json"));
|
|
3456
|
+
const pkg = readJson(join12(targetDir, "package.json"));
|
|
3457
|
+
const name = String(manifest?.project_name ?? "").trim() || packageNameToProjectName(typeof pkg?.name === "string" ? pkg.name : void 0) || packageNameToProjectName(basename4(targetDir)) || "Project";
|
|
3458
|
+
const ticketProvider = manifest?.ticket_provider && typeof manifest.ticket_provider === "object" ? manifest.ticket_provider : {};
|
|
3459
|
+
return {
|
|
3460
|
+
name,
|
|
3461
|
+
description: String(manifest?.project_description ?? pkg?.description ?? ""),
|
|
3462
|
+
slug: typeof manifest?.project_slug === "string" ? manifest.project_slug : void 0,
|
|
3463
|
+
identifier: typeof ticketProvider.identifier === "string" ? ticketProvider.identifier : void 0
|
|
3464
|
+
};
|
|
3465
|
+
}
|
|
3466
|
+
function isInteractiveProjectInit(options) {
|
|
3467
|
+
return !options.json && !options.yes && options.tui !== false && Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
3468
|
+
}
|
|
3469
|
+
async function promptTextValue(message, initialValue) {
|
|
3470
|
+
const value = await text3({
|
|
3471
|
+
message,
|
|
3472
|
+
initialValue,
|
|
3473
|
+
validate: (input) => input?.trim() ? void 0 : "Required"
|
|
3474
|
+
});
|
|
3475
|
+
if (isCancel5(value)) {
|
|
3476
|
+
cancel2("project init cancelled");
|
|
3477
|
+
process.exit(1);
|
|
3478
|
+
}
|
|
3479
|
+
return value.trim();
|
|
3480
|
+
}
|
|
3481
|
+
function projectInitActionLabel(kind) {
|
|
3482
|
+
switch (kind) {
|
|
3483
|
+
case "registry.upsert":
|
|
3484
|
+
return "Register/update project registry entry";
|
|
3485
|
+
case "copier.copy.commonproject":
|
|
3486
|
+
return "Render CommonProject scaffold";
|
|
3487
|
+
case "project.write-manifest":
|
|
3488
|
+
return "Write repo-local .project.json projection";
|
|
3489
|
+
case "plane.create-or-link":
|
|
3490
|
+
return "Create/link ticket provider project";
|
|
3491
|
+
case "hermes.provision-agent":
|
|
3492
|
+
return "Provision Hermes agent";
|
|
3493
|
+
default:
|
|
3494
|
+
return kind;
|
|
3495
|
+
}
|
|
3496
|
+
}
|
|
3497
|
+
function registryNeedsUpsert(plan) {
|
|
3498
|
+
const registry = loadProjectRegistry(plan.registryPath);
|
|
3499
|
+
const existing = registry.projects[plan.project.slug];
|
|
3500
|
+
if (!existing) return true;
|
|
3501
|
+
const { created_at: _existingCreated, updated_at: _existingUpdated, ...existingComparable } = existing;
|
|
3502
|
+
const { created_at: _projectCreated, updated_at: _projectUpdated, ...projectComparable } = plan.project;
|
|
3503
|
+
return JSON.stringify(existingComparable) !== JSON.stringify(projectComparable);
|
|
3504
|
+
}
|
|
3505
|
+
function actionNeedsRun(plan, kind, syncMode) {
|
|
3506
|
+
if (kind === "registry.upsert") return registryNeedsUpsert(plan);
|
|
3507
|
+
if (kind === "project.write-manifest") {
|
|
3508
|
+
const action = plan.actions.find((item) => item.kind === "project.write-manifest");
|
|
3509
|
+
if (!action || action.kind !== "project.write-manifest") return false;
|
|
3510
|
+
const next = `${JSON.stringify(action.manifest, null, 2)}
|
|
3511
|
+
`;
|
|
3512
|
+
return !existsSync9(action.path) || readFileSync6(action.path, "utf8") !== next;
|
|
3513
|
+
}
|
|
3514
|
+
if (kind === "copier.copy.commonproject") return true;
|
|
3515
|
+
if (kind === "plane.create-or-link") return plan.actions.some((action) => action.kind === kind && action.enabled);
|
|
3516
|
+
if (kind === "hermes.provision-agent") return plan.actions.some((action) => action.kind === kind && action.enabled);
|
|
3517
|
+
return true;
|
|
3518
|
+
}
|
|
3519
|
+
async function selectProjectInitOperations(input) {
|
|
3520
|
+
const planOperations = input.plan.actions.filter((action) => actionNeedsRun(input.plan, action.kind, input.syncMode)).map((action) => ({
|
|
3521
|
+
value: action.kind,
|
|
3522
|
+
label: projectInitActionLabel(action.kind),
|
|
3523
|
+
hint: action.kind === "registry.upsert" ? input.plan.registryPath : action.kind
|
|
3524
|
+
}));
|
|
3525
|
+
const parityOperations = input.auditRules.filter((rule) => rule.fixable && rule.status !== "pass" && rule.status !== "skip").map((rule) => ({
|
|
3526
|
+
value: `parity:${rule.id}`,
|
|
3527
|
+
label: `${rule.title}`,
|
|
3528
|
+
hint: `${rule.id}: ${rule.summary}`
|
|
3529
|
+
}));
|
|
3530
|
+
const operations = [...planOperations, ...parityOperations];
|
|
3531
|
+
const all = operations.map((operation) => operation.value);
|
|
3532
|
+
if (input.options.yes || input.options.apply && !isInteractiveProjectInit(input.options)) {
|
|
3533
|
+
return {
|
|
3534
|
+
selectedOperations: all,
|
|
3535
|
+
selectedParityRules: parityOperations.map((operation) => operation.value.replace(/^parity:/, ""))
|
|
3536
|
+
};
|
|
3537
|
+
}
|
|
3538
|
+
if (input.options.dryRun || !isInteractiveProjectInit(input.options)) {
|
|
3539
|
+
return { selectedOperations: [], selectedParityRules: [] };
|
|
3540
|
+
}
|
|
3541
|
+
if (!operations.length) return { selectedOperations: [], selectedParityRules: [] };
|
|
3542
|
+
const selected = await multiselect({
|
|
3543
|
+
message: "Select project init operations to run:",
|
|
3544
|
+
options: operations,
|
|
3545
|
+
initialValues: all
|
|
3546
|
+
});
|
|
3547
|
+
if (isCancel5(selected)) {
|
|
3548
|
+
cancel2("project init cancelled");
|
|
3549
|
+
process.exit(1);
|
|
3550
|
+
}
|
|
3551
|
+
return {
|
|
3552
|
+
selectedOperations: selected,
|
|
3553
|
+
selectedParityRules: selected.filter((value) => value.startsWith("parity:")).map((value) => value.replace(/^parity:/, ""))
|
|
3554
|
+
};
|
|
3555
|
+
}
|
|
3556
|
+
async function resolveProjectInitTarget(name, options) {
|
|
3557
|
+
const interactive = isInteractiveProjectInit(options);
|
|
3558
|
+
const cwd = process.cwd();
|
|
3559
|
+
const cwdGitRoot = findGitRoot(cwd);
|
|
3560
|
+
let targetDir = options.targetDir ? resolve3(options.targetDir) : void 0;
|
|
3561
|
+
if (!targetDir && cwdGitRoot) {
|
|
3562
|
+
targetDir = cwdGitRoot;
|
|
3563
|
+
}
|
|
3564
|
+
if (!targetDir && interactive) {
|
|
3565
|
+
const defaultName = name ?? basename4(cwd);
|
|
3566
|
+
const promptedName = name ?? await promptTextValue("Project name", packageNameToProjectName(defaultName));
|
|
3567
|
+
const defaultDir = join12(cwd, promptedName.replace(/[^A-Za-z0-9._-]/g, "") || promptedName.toLowerCase().replace(/[^a-z0-9]+/g, "-"));
|
|
3568
|
+
targetDir = await promptTextValue("Project directory", defaultDir);
|
|
3569
|
+
name = promptedName;
|
|
3570
|
+
}
|
|
3571
|
+
if (!targetDir) {
|
|
3572
|
+
if (!name) throw new Error("Project name or --target-dir is required when project init is not run inside a git repo");
|
|
3573
|
+
targetDir = resolve3(process.cwd(), name.replace(/[^A-Za-z0-9._-]/g, "") || name.toLowerCase().replace(/[^a-z0-9]+/g, "-"));
|
|
3574
|
+
}
|
|
3575
|
+
const targetExists = existsSync9(targetDir);
|
|
3576
|
+
if (targetExists && !statSync2(targetDir).isDirectory()) throw new Error(`Target path is not a directory: ${targetDir}`);
|
|
3577
|
+
const targetGitRoot = targetExists ? findGitRoot(targetDir) : void 0;
|
|
3578
|
+
const syncMode = Boolean(targetGitRoot && resolve3(targetGitRoot) === resolve3(targetDir));
|
|
3579
|
+
const defaults = targetExists ? deriveProjectDefaults(targetDir) : { name: packageNameToProjectName(basename4(targetDir)) ?? "Project", description: "" };
|
|
3580
|
+
if (!name && interactive && !syncMode) {
|
|
3581
|
+
name = await promptTextValue("Project name", defaults.name);
|
|
3582
|
+
}
|
|
3583
|
+
return {
|
|
3584
|
+
name: name ?? defaults.name,
|
|
3585
|
+
targetDir,
|
|
3586
|
+
description: options.description ?? defaults.description,
|
|
3587
|
+
syncMode,
|
|
3588
|
+
slug: options.slug ?? defaults.slug,
|
|
3589
|
+
identifier: options.identifier ?? defaults.identifier
|
|
3590
|
+
};
|
|
3591
|
+
}
|
|
2610
3592
|
var program = new Command3();
|
|
2611
3593
|
program.name("pjangler").description("Project subsystem bootstrapper CLI").version(PJANGLER_VERSION);
|
|
2612
3594
|
program.command("init").argument("<subsystem>", "Subsystem to initialize").description("Initialize a project subsystem").option("--dry-run", "Preview changes without writing files").option("-f, --force", "Overwrite existing files").action(async (subsystem, options) => {
|
|
@@ -2618,61 +3600,219 @@ program.command("init").argument("<subsystem>", "Subsystem to initialize").descr
|
|
|
2618
3600
|
try {
|
|
2619
3601
|
const recipe = createRecipe(subsystem, context);
|
|
2620
3602
|
if (!recipe) {
|
|
2621
|
-
console.error(
|
|
2622
|
-
console.
|
|
3603
|
+
console.error(`${xmark} Unknown subsystem: ${bold(subsystem)}`);
|
|
3604
|
+
console.error(` ${dim("Available:")} ${getRecipeNames().map((available) => cyan(available)).join(dim(", "))}`);
|
|
2623
3605
|
process.exit(1);
|
|
2624
3606
|
}
|
|
2625
3607
|
await recipe.execute();
|
|
2626
3608
|
} catch (error) {
|
|
2627
|
-
console.error(
|
|
3609
|
+
console.error(`${xmark} Error initializing ${bold(subsystem)}:`, error);
|
|
2628
3610
|
process.exit(1);
|
|
2629
3611
|
}
|
|
2630
3612
|
});
|
|
2631
3613
|
program.command("list").description("List available subsystems").action(() => {
|
|
2632
|
-
|
|
3614
|
+
const width = Object.keys(RECIPE_REGISTRY).reduce((max, name) => Math.max(max, name.length), 0);
|
|
3615
|
+
console.log("");
|
|
3616
|
+
console.log(` ${heading("Available subsystems")}`);
|
|
2633
3617
|
console.log("");
|
|
2634
3618
|
for (const [name, info] of Object.entries(RECIPE_REGISTRY)) {
|
|
2635
|
-
console.log(` ${name.padEnd(
|
|
3619
|
+
console.log(` ${cyan(name.padEnd(width))} ${dim(info.description)}`);
|
|
3620
|
+
}
|
|
3621
|
+
console.log("");
|
|
3622
|
+
console.log(` ${dim("Examples")}`);
|
|
3623
|
+
for (const example of ["pj init mise", "pj init docker", "pj init node"]) {
|
|
3624
|
+
console.log(` ${dim(glyph.pointer)} ${dim(example)}`);
|
|
2636
3625
|
}
|
|
2637
3626
|
console.log("");
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
3627
|
+
});
|
|
3628
|
+
var projectCmd = program.command("project").description("Manage the pjangler project registry");
|
|
3629
|
+
projectCmd.command("init").argument("[name]", "Project display name").description("Plan or apply a registry-backed CommonProject initialization or legacy repo sync").option("--description <text>", "Project description").option("--target-dir <path>", "Target repo path").option("--source-skill <path>", "Source skill/template provenance path").option("--primary-language <language>", "Primary language for CommonProject rendering", "python").option("--provision-agent", "Plan local Hermes PM agent provisioning").option("--agent-role <role>", "Hermes agent role to plan when --provision-agent is set", "pm").option("--apply", "Write the registry and render the repo scaffold").option("--dry-run", "Preview changes without writing files (default)").option("--live", "Allow live/network/cloud provisioning actions").option("--slug <slug>", "Project registry slug override").option("--identifier <identifier>", "Ticket identifier override").option("--registry <path>", `Registry path override (default: ${projectRegistryPath()})`).option("-f, --force", "Allow replacing an existing registry entry and re-rendering files").option("-y, --yes", "Apply every proposed operation without prompting").option("--no-tui", "Disable interactive prompts").option("--json", "Output machine-parseable JSON").action(async (name, options) => {
|
|
3630
|
+
try {
|
|
3631
|
+
const target = await resolveProjectInitTarget(name, options);
|
|
3632
|
+
const interactive = isInteractiveProjectInit(options);
|
|
3633
|
+
const apply = Boolean(!options.dryRun && (options.yes || options.apply || interactive));
|
|
3634
|
+
const plan = planProjectInit({
|
|
3635
|
+
name: target.name,
|
|
3636
|
+
description: target.description,
|
|
3637
|
+
targetDir: target.targetDir,
|
|
3638
|
+
sourceSkill: options.sourceSkill,
|
|
3639
|
+
primaryLanguage: options.primaryLanguage,
|
|
3640
|
+
provisionAgent: options.provisionAgent ?? false,
|
|
3641
|
+
agentRole: options.agentRole,
|
|
3642
|
+
apply,
|
|
3643
|
+
live: options.live ?? false,
|
|
3644
|
+
projectSlug: target.slug,
|
|
3645
|
+
projectIdentifier: target.identifier,
|
|
3646
|
+
registryPath: options.registry,
|
|
3647
|
+
force: options.force ?? false,
|
|
3648
|
+
overwrite: options.force ?? false,
|
|
3649
|
+
cwd: process.cwd(),
|
|
3650
|
+
scaffold: !target.syncMode
|
|
3651
|
+
});
|
|
3652
|
+
const audit = target.syncMode ? runAudit(target.targetDir) : void 0;
|
|
3653
|
+
const selection = await selectProjectInitOperations({
|
|
3654
|
+
plan,
|
|
3655
|
+
auditRules: audit?.rules ?? [],
|
|
3656
|
+
syncMode: target.syncMode,
|
|
3657
|
+
options
|
|
3658
|
+
});
|
|
3659
|
+
const selectedPlanActionKinds = new Set(selection.selectedOperations.filter((value) => !value.startsWith("parity:")));
|
|
3660
|
+
const selectedPlan = {
|
|
3661
|
+
...plan,
|
|
3662
|
+
apply,
|
|
3663
|
+
dryRun: !apply,
|
|
3664
|
+
actions: apply ? plan.actions.filter((action) => selectedPlanActionKinds.has(action.kind)) : plan.actions
|
|
3665
|
+
};
|
|
3666
|
+
if (!apply) {
|
|
3667
|
+
const payload = {
|
|
3668
|
+
...plan,
|
|
3669
|
+
mode: target.syncMode ? "sync" : "create",
|
|
3670
|
+
audit,
|
|
3671
|
+
proposedOperations: [
|
|
3672
|
+
...plan.actions.filter((action) => actionNeedsRun(plan, action.kind, target.syncMode)).map((action) => action.kind),
|
|
3673
|
+
...(audit?.rules ?? []).filter((rule) => rule.fixable && rule.status !== "pass" && rule.status !== "skip").map((rule) => `parity:${rule.id}`)
|
|
3674
|
+
]
|
|
3675
|
+
};
|
|
3676
|
+
if (options.json) console.log(JSON.stringify(payload, null, 2));
|
|
3677
|
+
else {
|
|
3678
|
+
console.log(formatProjectInitPlan(plan));
|
|
3679
|
+
if (payload.proposedOperations.length) {
|
|
3680
|
+
console.log(` ${bold("Proposed operations")} ${dim(`(${payload.proposedOperations.length})`)}`);
|
|
3681
|
+
for (const operation of payload.proposedOperations) console.log(` ${cyan(glyph.bullet)} ${operation}`);
|
|
3682
|
+
} else {
|
|
3683
|
+
console.log(` ${green(glyph.pass)} ${dim("Project is already in parity.")}`);
|
|
3684
|
+
}
|
|
3685
|
+
console.log("");
|
|
3686
|
+
}
|
|
3687
|
+
return;
|
|
3688
|
+
}
|
|
3689
|
+
const initResult = selectedPlan.actions.length ? executeProjectInitPlan(selectedPlan) : { ok: true, plan: selectedPlan, logs: [], errors: [], changedFiles: [] };
|
|
3690
|
+
const migrationReport = selection.selectedParityRules.length ? runMigrationForRules(selection.selectedParityRules, target.targetDir, false) : void 0;
|
|
3691
|
+
const migrationErrors = migrationReport?.results.filter((result2) => result2.status === "blocked").map((result2) => `${result2.id}: ${result2.summary}`) ?? [];
|
|
3692
|
+
const changedFiles = Array.from(/* @__PURE__ */ new Set([
|
|
3693
|
+
...initResult.changedFiles,
|
|
3694
|
+
...migrationReport?.changedFiles ?? []
|
|
3695
|
+
])).sort();
|
|
3696
|
+
const result = {
|
|
3697
|
+
ok: initResult.ok && (migrationReport?.ok ?? true),
|
|
3698
|
+
mode: target.syncMode ? "sync" : "create",
|
|
3699
|
+
plan: selectedPlan,
|
|
3700
|
+
audit,
|
|
3701
|
+
selectedOperations: selection.selectedOperations,
|
|
3702
|
+
selectedParityRules: selection.selectedParityRules,
|
|
3703
|
+
logs: initResult.logs,
|
|
3704
|
+
errors: [...initResult.errors, ...migrationErrors],
|
|
3705
|
+
changedFiles,
|
|
3706
|
+
migrationReport
|
|
3707
|
+
};
|
|
3708
|
+
if (options.json) {
|
|
3709
|
+
console.log(JSON.stringify(result, null, 2));
|
|
3710
|
+
} else {
|
|
3711
|
+
console.log(formatProjectInitPlan(selectedPlan));
|
|
3712
|
+
for (const line of result.logs) console.log(line);
|
|
3713
|
+
for (const line of result.errors) console.error(` ${xmark} ${line}`);
|
|
3714
|
+
if (migrationReport) console.log(formatMigrationReport(migrationReport));
|
|
3715
|
+
if (result.ok && changedFiles.length) console.log(` ${green(glyph.pass)} ${bold("Project synchronized")} ${dim(glyph.dot)} ${cyan(plan.project.slug)}
|
|
3716
|
+
`);
|
|
3717
|
+
if (result.ok && changedFiles.length === 0) console.log(` ${green(glyph.pass)} ${dim("Already in parity")} ${dim(glyph.dot)} ${cyan(plan.project.slug)}
|
|
3718
|
+
`);
|
|
3719
|
+
}
|
|
3720
|
+
process.exitCode = result.ok ? 0 : 1;
|
|
3721
|
+
} catch (err) {
|
|
3722
|
+
if (options.json) {
|
|
3723
|
+
console.log(JSON.stringify({ ok: false, error: err instanceof Error ? err.message : String(err) }, null, 2));
|
|
3724
|
+
} else {
|
|
3725
|
+
console.error(`${xmark} project init failed:`, err instanceof Error ? err.message : err);
|
|
3726
|
+
}
|
|
3727
|
+
process.exit(1);
|
|
3728
|
+
}
|
|
3729
|
+
});
|
|
3730
|
+
projectCmd.command("list").description("List projects in the pjangler registry").option("--registry <path>", `Registry path override (default: ${projectRegistryPath()})`).option("--json", "Output machine-parseable JSON").action((options) => {
|
|
3731
|
+
try {
|
|
3732
|
+
const registry = loadProjectRegistry(options.registry ?? projectRegistryPath());
|
|
3733
|
+
if (options.json) console.log(JSON.stringify(registry, null, 2));
|
|
3734
|
+
else console.log(formatProjectList(registry));
|
|
3735
|
+
} catch (err) {
|
|
3736
|
+
console.error(`${xmark} project list failed:`, err instanceof Error ? err.message : err);
|
|
3737
|
+
process.exit(1);
|
|
3738
|
+
}
|
|
3739
|
+
});
|
|
3740
|
+
projectCmd.command("show").argument("<slug>", "Project slug").description("Show one project from the pjangler registry").option("--registry <path>", `Registry path override (default: ${projectRegistryPath()})`).option("--json", "Output machine-parseable JSON").action((slug, options) => {
|
|
3741
|
+
try {
|
|
3742
|
+
const project = getProject(loadProjectRegistry(options.registry ?? projectRegistryPath()), slug);
|
|
3743
|
+
if (options.json) {
|
|
3744
|
+
console.log(JSON.stringify(project, null, 2));
|
|
3745
|
+
} else {
|
|
3746
|
+
console.log("");
|
|
3747
|
+
console.log(` ${heading(project.name)} ${dim(`(${project.slug})`)}`);
|
|
3748
|
+
console.log(` ${dim(project.repo_path)}`);
|
|
3749
|
+
if (project.description) console.log(` ${project.description}`);
|
|
3750
|
+
console.log("");
|
|
3751
|
+
}
|
|
3752
|
+
} catch (err) {
|
|
3753
|
+
console.error(`${xmark} project show failed:`, err instanceof Error ? err.message : err);
|
|
3754
|
+
process.exit(1);
|
|
3755
|
+
}
|
|
3756
|
+
});
|
|
3757
|
+
projectCmd.command("doctor").argument("[slug]", "Optional project slug").description("Validate the project registry and local projections").option("--registry <path>", `Registry path override (default: ${projectRegistryPath()})`).option("--json", "Output machine-parseable JSON").action((slug, options) => {
|
|
3758
|
+
try {
|
|
3759
|
+
const report = doctorProjectRegistry(options.registry ?? projectRegistryPath(), slug);
|
|
3760
|
+
if (options.json) {
|
|
3761
|
+
console.log(JSON.stringify(report, null, 2));
|
|
3762
|
+
} else if (!report.issues.length) {
|
|
3763
|
+
console.log("");
|
|
3764
|
+
console.log(` ${green(glyph.pass)} ${bold("Project registry OK")} ${dim(glyph.dot)} ${dim(report.registryPath)}`);
|
|
3765
|
+
console.log("");
|
|
3766
|
+
} else {
|
|
3767
|
+
console.log("");
|
|
3768
|
+
console.log(` ${red(glyph.fail)} ${bold("Project registry issues")} ${dim(glyph.dot)} ${dim(report.registryPath)}`);
|
|
3769
|
+
console.log("");
|
|
3770
|
+
for (const issue of report.issues) {
|
|
3771
|
+
const mark = issue.level === "error" ? red(glyph.fail) : yellow(glyph.warn);
|
|
3772
|
+
console.log(` ${mark} ${bold(issue.slug ?? "registry")} ${issue.message}`);
|
|
3773
|
+
}
|
|
3774
|
+
console.log("");
|
|
3775
|
+
}
|
|
3776
|
+
process.exit(report.ok ? 0 : 1);
|
|
3777
|
+
} catch (err) {
|
|
3778
|
+
console.error(`${xmark} project doctor failed:`, err instanceof Error ? err.message : err);
|
|
3779
|
+
process.exit(1);
|
|
3780
|
+
}
|
|
2642
3781
|
});
|
|
2643
3782
|
var recipeCmd = program.command("recipe").description("Manage pjangler recipes");
|
|
2644
3783
|
recipeCmd.command("list").description("List all available recipes").action(() => {
|
|
2645
|
-
console.log("
|
|
3784
|
+
console.log("");
|
|
3785
|
+
console.log(` ${heading("Recipes")}`);
|
|
2646
3786
|
console.log("");
|
|
2647
3787
|
for (const [name, info] of Object.entries(RECIPE_REGISTRY)) {
|
|
2648
|
-
console.log(` ${name}`);
|
|
2649
|
-
console.log(`
|
|
2650
|
-
console.log(`
|
|
3788
|
+
console.log(` ${cyan(bold(name))}`);
|
|
3789
|
+
console.log(` ${dim(info.description)}`);
|
|
3790
|
+
console.log(` ${dim("commands")} ${info.commands.map((command) => cyan(command)).join(dim(", "))}`);
|
|
2651
3791
|
console.log("");
|
|
2652
3792
|
}
|
|
2653
|
-
console.log("Usage
|
|
2654
|
-
console.log("
|
|
2655
|
-
console.log("
|
|
3793
|
+
console.log(` ${dim("Usage")}`);
|
|
3794
|
+
console.log(` ${dim(glyph.pointer)} ${dim("pj recipe run <name>")}`);
|
|
3795
|
+
console.log(` ${dim(glyph.pointer)} ${dim("pj recipe describe <name>")}`);
|
|
3796
|
+
console.log("");
|
|
2656
3797
|
});
|
|
2657
3798
|
recipeCmd.command("describe").argument("<name>", "Recipe name").description("Show detailed information about a recipe").action((name) => {
|
|
2658
3799
|
const info = getRecipeInfo(name);
|
|
2659
3800
|
if (!info) {
|
|
2660
|
-
console.error(
|
|
2661
|
-
console.
|
|
3801
|
+
console.error(`${xmark} Recipe not found: ${bold(name)}`);
|
|
3802
|
+
console.error(` ${dim("Available:")} ${getRecipeNames().map((available) => cyan(available)).join(dim(", "))}`);
|
|
2662
3803
|
process.exit(1);
|
|
2663
3804
|
}
|
|
2664
|
-
console.log(`\u{1F4E6} Recipe: ${info.name}`);
|
|
2665
3805
|
console.log("");
|
|
2666
|
-
console.log(`
|
|
3806
|
+
console.log(` ${heading(info.name)}`);
|
|
3807
|
+
console.log(` ${dim(info.description)}`);
|
|
2667
3808
|
console.log("");
|
|
2668
|
-
console.log("Commands
|
|
2669
|
-
for (const
|
|
2670
|
-
|
|
2671
|
-
}
|
|
3809
|
+
console.log(` ${bold("Commands")}`);
|
|
3810
|
+
for (const command of info.commands) console.log(` ${cyan(glyph.bullet)} ${command}`);
|
|
3811
|
+
console.log("");
|
|
3812
|
+
console.log(` ${dim("Usage")}`);
|
|
3813
|
+
console.log(` ${dim(glyph.pointer)} ${dim(`pj recipe run ${name}`)}`);
|
|
3814
|
+
console.log(` ${dim(glyph.pointer)} ${dim(`pj init ${name}`)}`);
|
|
2672
3815
|
console.log("");
|
|
2673
|
-
console.log("Usage:");
|
|
2674
|
-
console.log(` pjangler recipe run ${name}`);
|
|
2675
|
-
console.log(` pjangler init ${name}`);
|
|
2676
3816
|
});
|
|
2677
3817
|
recipeCmd.command("run").argument("<name>", "Recipe name").description("Execute a specific recipe").option("--dry-run", "Preview changes without writing files").option("-f, --force", "Overwrite existing files").action(async (name, options) => {
|
|
2678
3818
|
const context = {
|
|
@@ -2683,80 +3823,73 @@ recipeCmd.command("run").argument("<name>", "Recipe name").description("Execute
|
|
|
2683
3823
|
try {
|
|
2684
3824
|
const recipe = createRecipe(name, context);
|
|
2685
3825
|
if (!recipe) {
|
|
2686
|
-
console.error(
|
|
2687
|
-
console.
|
|
3826
|
+
console.error(`${xmark} Recipe not found: ${bold(name)}`);
|
|
3827
|
+
console.error(` ${dim("Available:")} ${getRecipeNames().map((available) => cyan(available)).join(dim(", "))}`);
|
|
2688
3828
|
process.exit(1);
|
|
2689
3829
|
}
|
|
2690
|
-
const dryRunPrefix = context.dryRun ? "[DRY RUN] " : "";
|
|
2691
|
-
console.log(`${dryRunPrefix}\u{1F680} Running recipe: ${name}`);
|
|
2692
|
-
console.log("");
|
|
2693
3830
|
await recipe.execute();
|
|
2694
3831
|
} catch (error) {
|
|
2695
|
-
console.error(
|
|
3832
|
+
console.error(`${xmark} Error running recipe ${bold(name)}:`, error);
|
|
2696
3833
|
process.exit(1);
|
|
2697
3834
|
}
|
|
2698
3835
|
});
|
|
2699
3836
|
var commandCmd = program.command("command").alias("cmd").description("Manage pjangler commands");
|
|
2700
3837
|
commandCmd.command("list").description("List all available commands").option("-g, --group", "Group commands by category").action((options) => {
|
|
3838
|
+
console.log("");
|
|
2701
3839
|
if (options.group) {
|
|
2702
|
-
console.log("
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
for (const [group, commands] of Object.entries(grouped)) {
|
|
2706
|
-
console.log(` ${group.toUpperCase()}:`);
|
|
2707
|
-
for (const cmd of commands) {
|
|
2708
|
-
console.log(` ${cmd.name.padEnd(30)} - ${cmd.description}`);
|
|
2709
|
-
}
|
|
3840
|
+
console.log(` ${heading("Commands by category")}`);
|
|
3841
|
+
for (const [group, commands] of Object.entries(getCommandsByGroup())) {
|
|
3842
|
+
const width = commands.reduce((max, command) => Math.max(max, command.name.length), 0);
|
|
2710
3843
|
console.log("");
|
|
3844
|
+
console.log(` ${bold(group.toUpperCase())}`);
|
|
3845
|
+
for (const command of commands) {
|
|
3846
|
+
console.log(` ${cyan(command.name.padEnd(width))} ${dim(command.description)}`);
|
|
3847
|
+
}
|
|
2711
3848
|
}
|
|
3849
|
+
console.log("");
|
|
2712
3850
|
} else {
|
|
2713
|
-
|
|
3851
|
+
const width = Object.keys(COMMAND_REGISTRY).reduce((max, name) => Math.max(max, name.length), 0);
|
|
3852
|
+
console.log(` ${heading("Commands")}`);
|
|
2714
3853
|
console.log("");
|
|
2715
3854
|
for (const [name, info] of Object.entries(COMMAND_REGISTRY)) {
|
|
2716
|
-
console.log(` ${name.padEnd(
|
|
3855
|
+
console.log(` ${cyan(name.padEnd(width))} ${dim(info.description)}`);
|
|
2717
3856
|
}
|
|
2718
3857
|
console.log("");
|
|
2719
3858
|
}
|
|
2720
|
-
console.log("Usage
|
|
2721
|
-
console.log("
|
|
2722
|
-
console.log("
|
|
3859
|
+
console.log(` ${dim("Usage")}`);
|
|
3860
|
+
console.log(` ${dim(glyph.pointer)} ${dim("pj command list --group")} ${dim("# group by category")}`);
|
|
3861
|
+
console.log(` ${dim(glyph.pointer)} ${dim("pj command describe <name>")} ${dim("# command details")}`);
|
|
3862
|
+
console.log("");
|
|
2723
3863
|
});
|
|
2724
3864
|
commandCmd.command("describe").argument("<name>", "Command name").description("Show detailed information about a command").action((name) => {
|
|
2725
3865
|
const info = getCommandInfo(name);
|
|
2726
3866
|
if (!info) {
|
|
2727
|
-
console.error(
|
|
2728
|
-
console.
|
|
3867
|
+
console.error(`${xmark} Command not found: ${bold(name)}`);
|
|
3868
|
+
console.error(` ${dim("Available:")} ${getCommandNames().map((available) => cyan(available)).join(dim(", "))}`);
|
|
2729
3869
|
process.exit(1);
|
|
2730
3870
|
}
|
|
2731
|
-
|
|
3871
|
+
const usedIn = Object.entries(RECIPE_REGISTRY).filter(([, recipeInfo]) => recipeInfo.commands.includes(name)).map(([recipeName]) => recipeName);
|
|
2732
3872
|
console.log("");
|
|
2733
|
-
console.log(`
|
|
2734
|
-
console.log(`
|
|
3873
|
+
console.log(` ${heading(info.name)}`);
|
|
3874
|
+
console.log(` ${dim(info.description)}`);
|
|
2735
3875
|
console.log("");
|
|
2736
|
-
console.log("
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
}
|
|
2741
|
-
}
|
|
3876
|
+
console.log(` ${dim("group".padEnd(7))} ${cyan(info.group)}`);
|
|
3877
|
+
console.log(` ${dim("recipes".padEnd(7))} ${usedIn.length ? usedIn.map((recipeName) => cyan(recipeName)).join(dim(", ")) : dim("(none)")}`);
|
|
3878
|
+
console.log("");
|
|
3879
|
+
console.log(` ${dim("Part of recipe execution (not run directly).")}`);
|
|
2742
3880
|
console.log("");
|
|
2743
|
-
console.log("Usage:");
|
|
2744
|
-
console.log(` Part of recipe execution (not run directly)`);
|
|
2745
3881
|
});
|
|
2746
3882
|
commandCmd.command("create").argument("<name>", "Command name").argument("<prompt>", "Description of what the command should do").description("Create a new command from template (placeholder for STORY-005)").option("-t, --template <type>", "Template type (toml, json, yaml, dockerfile)").option("-m, --model <model>", "LLM model to use (OpenRouter)").action((name, prompt, options) => {
|
|
2747
|
-
console.log("\u{1F6A7} Command generation coming in STORY-005!");
|
|
2748
3883
|
console.log("");
|
|
2749
|
-
console.log("
|
|
2750
|
-
console.log(
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
}
|
|
2754
|
-
if (options.model) {
|
|
2755
|
-
|
|
2756
|
-
}
|
|
3884
|
+
console.log(` ${yellow(glyph.warn)} ${bold("Command generation coming in STORY-005")}`);
|
|
3885
|
+
console.log("");
|
|
3886
|
+
console.log(` ${dim("Planned")}`);
|
|
3887
|
+
console.log(` ${cyan(glyph.bullet)} Generate ${bold(name)} from prompt: ${dim(`"${prompt}"`)}`);
|
|
3888
|
+
if (options.template) console.log(` ${cyan(glyph.bullet)} Template type: ${cyan(options.template)}`);
|
|
3889
|
+
if (options.model) console.log(` ${cyan(glyph.bullet)} LLM model: ${cyan(options.model)}`);
|
|
3890
|
+
console.log("");
|
|
3891
|
+
console.log(` ${dim("For now, manually create commands in src/commands/")}`);
|
|
2757
3892
|
console.log("");
|
|
2758
|
-
console.log("This feature will be implemented in the Template Generation System story.");
|
|
2759
|
-
console.log("For now, manually create commands in src/commands/");
|
|
2760
3893
|
});
|
|
2761
3894
|
program.command("audit").argument("[repo]", "Path to repo to audit (default: cwd)").description("Deterministic parity audit against 33god project standard").option("--json", "Output machine-parseable JSON").action((repo, options) => {
|
|
2762
3895
|
try {
|
|
@@ -2768,31 +3901,57 @@ program.command("audit").argument("[repo]", "Path to repo to audit (default: cwd
|
|
|
2768
3901
|
}
|
|
2769
3902
|
process.exit(report.ok ? 0 : 1);
|
|
2770
3903
|
} catch (err) {
|
|
2771
|
-
console.error(
|
|
3904
|
+
console.error(`${xmark} audit failed:`, err);
|
|
2772
3905
|
process.exit(1);
|
|
2773
3906
|
}
|
|
2774
3907
|
});
|
|
2775
|
-
program.command("migrate").argument("[rule-id]", "Rule ID to migrate (omit
|
|
3908
|
+
program.command("migrate").argument("[rule-id]", "Rule ID to migrate (omit to open interactive rule selector)").argument("[repo]", "Path to repo (default: cwd)").description("Idempotent migration recipe for a parity rule (or open the rule selector)").option("--all", "Apply every migration recipe in order").option("--dry-run", "Preview changes without writing files").option("--json", "Output machine-parseable JSON").action(async (ruleId, repo, options) => {
|
|
2776
3909
|
try {
|
|
2777
3910
|
const all = options.all ?? false;
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
3911
|
+
const dryRun = options.dryRun ?? false;
|
|
3912
|
+
if (all) {
|
|
3913
|
+
let actualRepo = repo;
|
|
3914
|
+
if (ruleId && !actualRepo) {
|
|
3915
|
+
actualRepo = ruleId;
|
|
3916
|
+
}
|
|
3917
|
+
const report2 = runMigration(void 0, actualRepo, dryRun, true);
|
|
3918
|
+
printMigrationReport(report2, options.json);
|
|
3919
|
+
process.exit(report2.ok ? 0 : 1);
|
|
3920
|
+
}
|
|
3921
|
+
if (ruleId && repo) {
|
|
3922
|
+
if (!getParityRuleIds().includes(ruleId)) {
|
|
3923
|
+
console.error(`${xmark} Unknown parity rule: ${bold(ruleId)}`);
|
|
3924
|
+
process.exit(1);
|
|
3925
|
+
}
|
|
3926
|
+
const report2 = runMigration(ruleId, repo, dryRun, false);
|
|
3927
|
+
printMigrationReport(report2, options.json);
|
|
3928
|
+
process.exit(report2.ok ? 0 : 1);
|
|
2781
3929
|
}
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
3930
|
+
if (ruleId && getParityRuleIds().includes(ruleId)) {
|
|
3931
|
+
const report2 = runMigration(ruleId, void 0, dryRun, false);
|
|
3932
|
+
printMigrationReport(report2, options.json);
|
|
3933
|
+
process.exit(report2.ok ? 0 : 1);
|
|
2786
3934
|
}
|
|
2787
|
-
const report = runMigration(actualRuleId, actualRepo, options.dryRun ?? false, all);
|
|
2788
3935
|
if (options.json) {
|
|
2789
|
-
console.
|
|
2790
|
-
|
|
2791
|
-
|
|
3936
|
+
console.error(`${xmark} JSON output requires a rule-id or --all`);
|
|
3937
|
+
process.exit(1);
|
|
3938
|
+
}
|
|
3939
|
+
if (!process.stdin.isTTY) {
|
|
3940
|
+
console.error(`${xmark} Provide a rule-id, use --all, or run in an interactive terminal`);
|
|
3941
|
+
process.exit(1);
|
|
2792
3942
|
}
|
|
3943
|
+
const targetRepo = ruleId ?? repo;
|
|
3944
|
+
const audit = runAudit(targetRepo);
|
|
3945
|
+
const ruleIds = await promptForRuleIds(audit.rules);
|
|
3946
|
+
if (!ruleIds.length) {
|
|
3947
|
+
console.log(` ${cyan(glyph.info)} ${dim("No rules selected; nothing to migrate.")}`);
|
|
3948
|
+
process.exit(0);
|
|
3949
|
+
}
|
|
3950
|
+
const report = runMigrationForRules(ruleIds, targetRepo, dryRun);
|
|
3951
|
+
printMigrationReport(report, false);
|
|
2793
3952
|
process.exit(report.ok ? 0 : 1);
|
|
2794
3953
|
} catch (err) {
|
|
2795
|
-
console.error(
|
|
3954
|
+
console.error(`${xmark} migrate failed:`, err);
|
|
2796
3955
|
process.exit(1);
|
|
2797
3956
|
}
|
|
2798
3957
|
});
|
|
@@ -2827,12 +3986,12 @@ program.command("hermes-agent").alias("hermes").description("Provision a Hermes
|
|
|
2827
3986
|
try {
|
|
2828
3987
|
const recipe = createRecipe("hermes-agent", context);
|
|
2829
3988
|
if (!recipe) {
|
|
2830
|
-
console.error(
|
|
3989
|
+
console.error(`${xmark} hermes-agent recipe not registered`);
|
|
2831
3990
|
process.exit(1);
|
|
2832
3991
|
}
|
|
2833
3992
|
await recipe.execute();
|
|
2834
3993
|
} catch (err) {
|
|
2835
|
-
console.error(
|
|
3994
|
+
console.error(`${xmark} hermes-agent failed:`, err);
|
|
2836
3995
|
process.exit(1);
|
|
2837
3996
|
}
|
|
2838
3997
|
});
|
|
@@ -2850,14 +4009,15 @@ configCmd.command("bootstrap").description("Create ~/.config/hermes-agent-templa
|
|
|
2850
4009
|
}
|
|
2851
4010
|
});
|
|
2852
4011
|
program.command("describe").description("Describe the current project (for AI context)").action(() => {
|
|
2853
|
-
console.log("\u{1F50D} Project Description (placeholder for future enhancement)");
|
|
2854
4012
|
console.log("");
|
|
2855
|
-
console.log("
|
|
2856
|
-
console.log("
|
|
2857
|
-
console.log("
|
|
2858
|
-
|
|
2859
|
-
|
|
4013
|
+
console.log(` ${heading("Project description")} ${dim("(placeholder)")}`);
|
|
4014
|
+
console.log("");
|
|
4015
|
+
console.log(` ${dim("Will analyze the project and report:")}`);
|
|
4016
|
+
for (const item of ["Detected project type", "Installed subsystems", "Configuration files present", "Suggested next steps"]) {
|
|
4017
|
+
console.log(` ${cyan(glyph.bullet)} ${item}`);
|
|
4018
|
+
}
|
|
4019
|
+
console.log("");
|
|
4020
|
+
console.log(` ${dim("Coming soon.")}`);
|
|
2860
4021
|
console.log("");
|
|
2861
|
-
console.log("Coming soon!");
|
|
2862
4022
|
});
|
|
2863
4023
|
program.parse();
|