@arvoretech/hub 0.16.0 → 0.17.0
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.
|
@@ -3,10 +3,10 @@ import {
|
|
|
3
3
|
} from "./chunk-VMN4KGAK.js";
|
|
4
4
|
|
|
5
5
|
// src/commands/generate.ts
|
|
6
|
-
import { Command } from "commander";
|
|
7
|
-
import { existsSync as
|
|
8
|
-
import { mkdir as
|
|
9
|
-
import { join as
|
|
6
|
+
import { Command as Command2 } from "commander";
|
|
7
|
+
import { existsSync as existsSync3 } from "fs";
|
|
8
|
+
import { mkdir as mkdir4, writeFile as writeFile4, readdir as readdir2, copyFile, readFile as readFile4, cp, rm } from "fs/promises";
|
|
9
|
+
import { join as join4, resolve as resolve2 } from "path";
|
|
10
10
|
import chalk3 from "chalk";
|
|
11
11
|
import inquirer from "inquirer";
|
|
12
12
|
|
|
@@ -117,7 +117,7 @@ async function checkAndAutoRegenerate(hubDir) {
|
|
|
117
117
|
return;
|
|
118
118
|
}
|
|
119
119
|
console.log(chalk.yellow("\n Detected outdated configs, auto-regenerating..."));
|
|
120
|
-
const { generators: generators2 } = await import("./generate-
|
|
120
|
+
const { generators: generators2 } = await import("./generate-TBAEF7R5.js");
|
|
121
121
|
const generator = generators2[result.editor];
|
|
122
122
|
if (!generator) {
|
|
123
123
|
console.log(chalk.red(` Unknown editor '${result.editor}' in cache. Run 'hub generate' manually.`));
|
|
@@ -390,6 +390,337 @@ async function fetchRemoteSources(sources, hubDir, skillsDir, steeringDir) {
|
|
|
390
390
|
return { skills: skillCount, steering: steeringCount, errors };
|
|
391
391
|
}
|
|
392
392
|
|
|
393
|
+
// src/commands/persona.ts
|
|
394
|
+
import { Command } from "commander";
|
|
395
|
+
import { join as join3 } from "path";
|
|
396
|
+
import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
|
|
397
|
+
import { existsSync as existsSync2 } from "fs";
|
|
398
|
+
import React from "react";
|
|
399
|
+
import { render } from "ink";
|
|
400
|
+
|
|
401
|
+
// src/tui/PersonaApp.tsx
|
|
402
|
+
import { useState, useCallback } from "react";
|
|
403
|
+
import { Box, Text, useInput, useStdout, useApp } from "ink";
|
|
404
|
+
import TextInput from "ink-text-input";
|
|
405
|
+
|
|
406
|
+
// src/tui/theme.ts
|
|
407
|
+
var colors = {
|
|
408
|
+
brand: "#22c55e",
|
|
409
|
+
brandDim: "#16a34a",
|
|
410
|
+
blue: "#3b82f6",
|
|
411
|
+
purple: "#a78bfa",
|
|
412
|
+
muted: "#6b7280",
|
|
413
|
+
dim: "#4b5563",
|
|
414
|
+
warning: "#eab308",
|
|
415
|
+
error: "#ef4444",
|
|
416
|
+
white: "#ffffff"
|
|
417
|
+
};
|
|
418
|
+
var symbols = {
|
|
419
|
+
check: "\u2713",
|
|
420
|
+
cross: "\u2717",
|
|
421
|
+
arrow: "\u276F",
|
|
422
|
+
dot: "\u25CF",
|
|
423
|
+
circle: "\u25CB",
|
|
424
|
+
tree: "\u{1F333}",
|
|
425
|
+
line: "\u2500",
|
|
426
|
+
corner: "\u256D",
|
|
427
|
+
cornerEnd: "\u2570",
|
|
428
|
+
vertical: "\u2502",
|
|
429
|
+
cornerRight: "\u256E",
|
|
430
|
+
cornerEndRight: "\u256F"
|
|
431
|
+
};
|
|
432
|
+
function horizontalLine(width) {
|
|
433
|
+
return symbols.line.repeat(width);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// src/tui/PersonaApp.tsx
|
|
437
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
438
|
+
var TECHNICAL_LEVELS = [
|
|
439
|
+
{ value: "non-technical", label: "Non-technical", description: "No coding experience \u2014 CEO, PM, designer, etc." },
|
|
440
|
+
{ value: "beginner", label: "Beginner", description: "Learning to code or just started working with devs" },
|
|
441
|
+
{ value: "intermediate", label: "Intermediate", description: "Comfortable with code, still learning some tools" },
|
|
442
|
+
{ value: "advanced", label: "Advanced", description: "Experienced developer, knows the stack well" }
|
|
443
|
+
];
|
|
444
|
+
var STEP_INFO = {
|
|
445
|
+
name: { title: "What's your name?", subtitle: "So the AI knows who it's talking to." },
|
|
446
|
+
role: { title: "What's your role?", subtitle: "e.g. CEO, Product Manager, Designer, Backend Dev, QA Engineer..." },
|
|
447
|
+
technical_level: { title: "How technical are you?", subtitle: "This changes how the AI explains things to you." },
|
|
448
|
+
context: { title: "Anything else the AI should know about you?", subtitle: `e.g. "I focus on business metrics", "I only work on the mobile app", "I review PRs but don't code"` },
|
|
449
|
+
language: { title: "What language should the AI use?", subtitle: "e.g. English, Portugu\xEAs, Espa\xF1ol..." }
|
|
450
|
+
};
|
|
451
|
+
function PersonaApp({ existing, onComplete }) {
|
|
452
|
+
const { stdout } = useStdout();
|
|
453
|
+
const { exit } = useApp();
|
|
454
|
+
const width = Math.min(stdout?.columns || 80, 80);
|
|
455
|
+
const CLEAR2 = "\x1B[2J\x1B[H";
|
|
456
|
+
const [step, setStep] = useState("name");
|
|
457
|
+
const [data, setData] = useState({
|
|
458
|
+
name: existing?.name || "",
|
|
459
|
+
role: existing?.role || "",
|
|
460
|
+
context: existing?.context || "",
|
|
461
|
+
technical_level: existing?.technical_level || "intermediate",
|
|
462
|
+
language: existing?.language || "English"
|
|
463
|
+
});
|
|
464
|
+
const [inputValue, setInputValue] = useState(existing?.name || "");
|
|
465
|
+
const [levelCursor, setLevelCursor] = useState(
|
|
466
|
+
TECHNICAL_LEVELS.findIndex((l) => l.value === (existing?.technical_level || "intermediate"))
|
|
467
|
+
);
|
|
468
|
+
const goTo = useCallback((next) => {
|
|
469
|
+
stdout?.write(CLEAR2);
|
|
470
|
+
setStep(next);
|
|
471
|
+
}, [stdout]);
|
|
472
|
+
const handleTextSubmit = useCallback((value, field, next) => {
|
|
473
|
+
if (!value.trim()) return;
|
|
474
|
+
setData((prev) => ({ ...prev, [field]: value.trim() }));
|
|
475
|
+
setInputValue("");
|
|
476
|
+
goTo(next);
|
|
477
|
+
}, [goTo]);
|
|
478
|
+
useInput((input, key) => {
|
|
479
|
+
if (step === "technical_level") {
|
|
480
|
+
if (key.upArrow) setLevelCursor((p) => p > 0 ? p - 1 : TECHNICAL_LEVELS.length - 1);
|
|
481
|
+
if (key.downArrow) setLevelCursor((p) => p < TECHNICAL_LEVELS.length - 1 ? p + 1 : 0);
|
|
482
|
+
if (key.return) {
|
|
483
|
+
setData((prev) => ({ ...prev, technical_level: TECHNICAL_LEVELS[levelCursor].value }));
|
|
484
|
+
setInputValue(data.context || "");
|
|
485
|
+
goTo("context");
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
if (step === "review") {
|
|
489
|
+
if (key.return || input === "y") {
|
|
490
|
+
onComplete(data);
|
|
491
|
+
goTo("done");
|
|
492
|
+
}
|
|
493
|
+
if (input === "b" || key.leftArrow) {
|
|
494
|
+
setInputValue(data.name);
|
|
495
|
+
goTo("name");
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
if (step === "done" && key.return) {
|
|
499
|
+
exit();
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
const info = STEP_INFO[step];
|
|
503
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width, children: [
|
|
504
|
+
/* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
|
|
505
|
+
/* @__PURE__ */ jsxs(Text, { color: colors.brand, bold: true, children: [
|
|
506
|
+
symbols.tree,
|
|
507
|
+
" Repo Hub"
|
|
508
|
+
] }),
|
|
509
|
+
/* @__PURE__ */ jsxs(Text, { color: colors.dim, children: [
|
|
510
|
+
" ",
|
|
511
|
+
symbols.line,
|
|
512
|
+
" Persona Setup"
|
|
513
|
+
] })
|
|
514
|
+
] }),
|
|
515
|
+
info && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, paddingLeft: 1, children: [
|
|
516
|
+
/* @__PURE__ */ jsx(Text, { color: colors.white, bold: true, children: info.title }),
|
|
517
|
+
/* @__PURE__ */ jsx(Text, { color: colors.dim, children: info.subtitle })
|
|
518
|
+
] }),
|
|
519
|
+
/* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingLeft: 2, children: [
|
|
520
|
+
step === "name" && /* @__PURE__ */ jsxs(Box, { children: [
|
|
521
|
+
/* @__PURE__ */ jsx(Text, { color: colors.brand, bold: true, children: "\u276F " }),
|
|
522
|
+
/* @__PURE__ */ jsx(
|
|
523
|
+
TextInput,
|
|
524
|
+
{
|
|
525
|
+
value: inputValue,
|
|
526
|
+
onChange: setInputValue,
|
|
527
|
+
onSubmit: (v) => {
|
|
528
|
+
handleTextSubmit(v, "name", "role");
|
|
529
|
+
setInputValue(data.role || "");
|
|
530
|
+
},
|
|
531
|
+
placeholder: "Jo\xE3o"
|
|
532
|
+
}
|
|
533
|
+
)
|
|
534
|
+
] }),
|
|
535
|
+
step === "role" && /* @__PURE__ */ jsxs(Box, { children: [
|
|
536
|
+
/* @__PURE__ */ jsx(Text, { color: colors.brand, bold: true, children: "\u276F " }),
|
|
537
|
+
/* @__PURE__ */ jsx(
|
|
538
|
+
TextInput,
|
|
539
|
+
{
|
|
540
|
+
value: inputValue,
|
|
541
|
+
onChange: setInputValue,
|
|
542
|
+
onSubmit: (v) => {
|
|
543
|
+
handleTextSubmit(v, "role", "technical_level");
|
|
544
|
+
},
|
|
545
|
+
placeholder: "CEO"
|
|
546
|
+
}
|
|
547
|
+
)
|
|
548
|
+
] }),
|
|
549
|
+
step === "technical_level" && /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: TECHNICAL_LEVELS.map((level, i) => {
|
|
550
|
+
const active = i === levelCursor;
|
|
551
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
|
|
552
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
553
|
+
/* @__PURE__ */ jsx(Text, { color: active ? colors.brand : colors.dim, children: active ? `${symbols.arrow} ` : " " }),
|
|
554
|
+
/* @__PURE__ */ jsx(Text, { color: active ? colors.white : colors.muted, bold: active, children: level.label })
|
|
555
|
+
] }),
|
|
556
|
+
/* @__PURE__ */ jsx(Box, { paddingLeft: 4, children: /* @__PURE__ */ jsx(Text, { color: colors.dim, children: level.description }) })
|
|
557
|
+
] }, level.value);
|
|
558
|
+
}) }),
|
|
559
|
+
step === "context" && /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: /* @__PURE__ */ jsxs(Box, { children: [
|
|
560
|
+
/* @__PURE__ */ jsx(Text, { color: colors.brand, bold: true, children: "\u276F " }),
|
|
561
|
+
/* @__PURE__ */ jsx(
|
|
562
|
+
TextInput,
|
|
563
|
+
{
|
|
564
|
+
value: inputValue,
|
|
565
|
+
onChange: setInputValue,
|
|
566
|
+
onSubmit: (v) => {
|
|
567
|
+
setData((prev) => ({ ...prev, context: v.trim() }));
|
|
568
|
+
setInputValue(data.language || "English");
|
|
569
|
+
goTo("language");
|
|
570
|
+
},
|
|
571
|
+
placeholder: "(optional \u2014 press Enter to skip)"
|
|
572
|
+
}
|
|
573
|
+
)
|
|
574
|
+
] }) }),
|
|
575
|
+
step === "language" && /* @__PURE__ */ jsxs(Box, { children: [
|
|
576
|
+
/* @__PURE__ */ jsx(Text, { color: colors.brand, bold: true, children: "\u276F " }),
|
|
577
|
+
/* @__PURE__ */ jsx(
|
|
578
|
+
TextInput,
|
|
579
|
+
{
|
|
580
|
+
value: inputValue,
|
|
581
|
+
onChange: setInputValue,
|
|
582
|
+
onSubmit: (v) => {
|
|
583
|
+
handleTextSubmit(v || "English", "language", "review");
|
|
584
|
+
},
|
|
585
|
+
placeholder: "English"
|
|
586
|
+
}
|
|
587
|
+
)
|
|
588
|
+
] }),
|
|
589
|
+
step === "review" && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
590
|
+
/* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingLeft: 1, borderStyle: "round", borderColor: colors.dim, paddingRight: 1, children: [
|
|
591
|
+
/* @__PURE__ */ jsx(ReviewRow, { label: "Name", value: data.name }),
|
|
592
|
+
/* @__PURE__ */ jsx(ReviewRow, { label: "Role", value: data.role }),
|
|
593
|
+
/* @__PURE__ */ jsx(ReviewRow, { label: "Level", value: TECHNICAL_LEVELS.find((l) => l.value === data.technical_level)?.label || data.technical_level }),
|
|
594
|
+
data.context && /* @__PURE__ */ jsx(ReviewRow, { label: "Context", value: data.context }),
|
|
595
|
+
/* @__PURE__ */ jsx(ReviewRow, { label: "Language", value: data.language })
|
|
596
|
+
] }),
|
|
597
|
+
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsxs(Text, { color: colors.dim, children: [
|
|
598
|
+
/* @__PURE__ */ jsx(Text, { color: colors.brand, children: "enter" }),
|
|
599
|
+
" save ",
|
|
600
|
+
/* @__PURE__ */ jsx(Text, { color: colors.muted, children: "b" }),
|
|
601
|
+
" start over"
|
|
602
|
+
] }) })
|
|
603
|
+
] }),
|
|
604
|
+
step === "done" && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", alignItems: "center", children: [
|
|
605
|
+
/* @__PURE__ */ jsxs(Text, { color: colors.brand, bold: true, children: [
|
|
606
|
+
symbols.check,
|
|
607
|
+
" Persona saved"
|
|
608
|
+
] }),
|
|
609
|
+
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsxs(Text, { color: colors.dim, children: [
|
|
610
|
+
"Run ",
|
|
611
|
+
/* @__PURE__ */ jsx(Text, { color: colors.white, bold: true, children: "hub generate" }),
|
|
612
|
+
" to apply it to your AI agent."
|
|
613
|
+
] }) }),
|
|
614
|
+
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsxs(Text, { color: colors.dim, children: [
|
|
615
|
+
"Press ",
|
|
616
|
+
/* @__PURE__ */ jsx(Text, { color: colors.brand, bold: true, children: "Enter" }),
|
|
617
|
+
" to exit"
|
|
618
|
+
] }) })
|
|
619
|
+
] })
|
|
620
|
+
] })
|
|
621
|
+
] });
|
|
622
|
+
}
|
|
623
|
+
function ReviewRow({ label, value }) {
|
|
624
|
+
return /* @__PURE__ */ jsxs(Box, { children: [
|
|
625
|
+
/* @__PURE__ */ jsx(Box, { width: 10, children: /* @__PURE__ */ jsx(Text, { color: colors.muted, children: label }) }),
|
|
626
|
+
/* @__PURE__ */ jsx(Text, { color: colors.white, bold: true, children: value })
|
|
627
|
+
] });
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// src/commands/persona.ts
|
|
631
|
+
import { stringify } from "yaml";
|
|
632
|
+
var ENTER_ALT_SCREEN = "\x1B[?1049h";
|
|
633
|
+
var EXIT_ALT_SCREEN = "\x1B[?1049l";
|
|
634
|
+
var CLEAR = "\x1B[2J\x1B[H";
|
|
635
|
+
var HIDE_CURSOR = "\x1B[?25l";
|
|
636
|
+
var SHOW_CURSOR = "\x1B[?25h";
|
|
637
|
+
function getPersonaPath(hubDir) {
|
|
638
|
+
return join3(hubDir, ".hub", "persona.yaml");
|
|
639
|
+
}
|
|
640
|
+
async function loadPersona(hubDir) {
|
|
641
|
+
const personaPath = getPersonaPath(hubDir);
|
|
642
|
+
if (!existsSync2(personaPath)) return null;
|
|
643
|
+
try {
|
|
644
|
+
const { parse } = await import("yaml");
|
|
645
|
+
const content = await readFile3(personaPath, "utf-8");
|
|
646
|
+
return parse(content);
|
|
647
|
+
} catch {
|
|
648
|
+
return null;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
function buildPersonaSection(persona) {
|
|
652
|
+
const lines = [];
|
|
653
|
+
lines.push(`
|
|
654
|
+
## User Persona
|
|
655
|
+
`);
|
|
656
|
+
lines.push(`You are talking to **${persona.name}**, who is a **${persona.role}**.`);
|
|
657
|
+
if (persona.technical_level === "non-technical") {
|
|
658
|
+
lines.push(`
|
|
659
|
+
${persona.name} is not technical. Adapt your communication:
|
|
660
|
+
- Never use jargon, acronyms, or technical terms without explaining them in plain language first.
|
|
661
|
+
- Explain decisions in terms of business impact, user experience, and outcomes \u2014 not implementation details.
|
|
662
|
+
- When showing progress, focus on what changed for the user/product, not what code was modified.
|
|
663
|
+
- If you need to mention something technical, use analogies and simple language.
|
|
664
|
+
- Keep responses short and focused on what matters to them.
|
|
665
|
+
- When asking questions, frame them as business/product decisions, not technical choices.
|
|
666
|
+
- Never show code snippets, terminal output, or file paths unless explicitly asked.`);
|
|
667
|
+
} else if (persona.technical_level === "beginner") {
|
|
668
|
+
lines.push(`
|
|
669
|
+
${persona.name} is learning and not deeply technical yet. Adapt your communication:
|
|
670
|
+
- Explain technical concepts briefly when you first mention them.
|
|
671
|
+
- Avoid deep implementation details unless asked.
|
|
672
|
+
- Use simple language but don't shy away from introducing technical terms with context.
|
|
673
|
+
- When showing code or commands, briefly explain what they do.
|
|
674
|
+
- Be encouraging and patient \u2014 frame things as learning opportunities.`);
|
|
675
|
+
} else if (persona.technical_level === "intermediate") {
|
|
676
|
+
lines.push(`
|
|
677
|
+
${persona.name} is comfortable with code but may not know every tool or pattern. Adapt your communication:
|
|
678
|
+
- Use technical language normally but explain niche or advanced concepts when relevant.
|
|
679
|
+
- Show code and commands without excessive explanation, but add context for non-obvious decisions.
|
|
680
|
+
- Focus on the "why" behind architectural choices.`);
|
|
681
|
+
} else {
|
|
682
|
+
lines.push(`
|
|
683
|
+
${persona.name} is an experienced developer. Communicate directly:
|
|
684
|
+
- Be concise and technical. Skip basic explanations.
|
|
685
|
+
- Focus on trade-offs, edge cases, and non-obvious implications.
|
|
686
|
+
- Show code directly without hand-holding.`);
|
|
687
|
+
}
|
|
688
|
+
if (persona.context) {
|
|
689
|
+
lines.push(`
|
|
690
|
+
Additional context about ${persona.name}: ${persona.context}`);
|
|
691
|
+
}
|
|
692
|
+
if (persona.language && persona.language.toLowerCase() !== "english") {
|
|
693
|
+
lines.push(`
|
|
694
|
+
Always communicate with ${persona.name} in **${persona.language}**.`);
|
|
695
|
+
}
|
|
696
|
+
return lines.join("\n");
|
|
697
|
+
}
|
|
698
|
+
var personaCommand = new Command("persona").description("Set up your personal AI profile \u2014 adapts how the agent communicates with you").action(async () => {
|
|
699
|
+
const hubDir = process.cwd();
|
|
700
|
+
const hubPath = join3(hubDir, ".hub");
|
|
701
|
+
await mkdir3(hubPath, { recursive: true });
|
|
702
|
+
const existing = await loadPersona(hubDir);
|
|
703
|
+
process.stdout.write(ENTER_ALT_SCREEN + CLEAR + HIDE_CURSOR);
|
|
704
|
+
const cleanup = () => {
|
|
705
|
+
process.stdout.write(SHOW_CURSOR + EXIT_ALT_SCREEN);
|
|
706
|
+
};
|
|
707
|
+
process.on("SIGINT", () => {
|
|
708
|
+
cleanup();
|
|
709
|
+
process.exit(0);
|
|
710
|
+
});
|
|
711
|
+
const { waitUntilExit } = render(
|
|
712
|
+
React.createElement(PersonaApp, {
|
|
713
|
+
existing: existing ?? void 0,
|
|
714
|
+
onComplete: async (persona) => {
|
|
715
|
+
const personaPath = getPersonaPath(hubDir);
|
|
716
|
+
await writeFile3(personaPath, stringify(persona), "utf-8");
|
|
717
|
+
}
|
|
718
|
+
})
|
|
719
|
+
);
|
|
720
|
+
await waitUntilExit();
|
|
721
|
+
cleanup();
|
|
722
|
+
});
|
|
723
|
+
|
|
393
724
|
// src/commands/generate.ts
|
|
394
725
|
var HUB_DOCS_URL = "https://hub.arvore.com.br/llms-full.txt";
|
|
395
726
|
async function syncRemoteSources(config, hubDir, skillsDir, steeringDir) {
|
|
@@ -464,9 +795,9 @@ function parseFrontMatter(content) {
|
|
|
464
795
|
}
|
|
465
796
|
async function readExistingMcpDisabledState(mcpJsonPath) {
|
|
466
797
|
const disabledState = {};
|
|
467
|
-
if (!
|
|
798
|
+
if (!existsSync3(mcpJsonPath)) return disabledState;
|
|
468
799
|
try {
|
|
469
|
-
const content = JSON.parse(await
|
|
800
|
+
const content = JSON.parse(await readFile4(mcpJsonPath, "utf-8"));
|
|
470
801
|
const servers = content.mcpServers || content.mcp || {};
|
|
471
802
|
for (const [name, config] of Object.entries(servers)) {
|
|
472
803
|
if (typeof config.disabled === "boolean") {
|
|
@@ -492,8 +823,8 @@ async function fetchHubDocsSkill(skillsDir) {
|
|
|
492
823
|
return;
|
|
493
824
|
}
|
|
494
825
|
const content = await res.text();
|
|
495
|
-
const hubSkillDir =
|
|
496
|
-
await
|
|
826
|
+
const hubSkillDir = join4(skillsDir, "hub-docs");
|
|
827
|
+
await mkdir4(hubSkillDir, { recursive: true });
|
|
497
828
|
const skillContent = `---
|
|
498
829
|
name: hub-docs
|
|
499
830
|
description: Repo Hub (rhm) documentation. Use when working with hub.yaml, hub CLI commands, agent orchestration, MCP configuration, skills, workflows, or multi-repo workspace setup.
|
|
@@ -501,7 +832,7 @@ triggers: [hub, rhm, hub.yaml, generate, scan, setup, orchestrator, multi-repo,
|
|
|
501
832
|
---
|
|
502
833
|
|
|
503
834
|
${content}`;
|
|
504
|
-
await
|
|
835
|
+
await writeFile4(join4(hubSkillDir, "SKILL.md"), skillContent, "utf-8");
|
|
505
836
|
console.log(chalk3.green(" Fetched hub-docs skill from hub.arvore.com.br"));
|
|
506
837
|
} catch {
|
|
507
838
|
console.log(chalk3.yellow(` Could not fetch hub docs, skipping hub-docs skill`));
|
|
@@ -576,7 +907,7 @@ function buildClaudeHooks(hooks) {
|
|
|
576
907
|
return claudeHooks;
|
|
577
908
|
}
|
|
578
909
|
async function generateEditorCommands(config, hubDir, targetDir, editorName) {
|
|
579
|
-
const commandsDir =
|
|
910
|
+
const commandsDir = join4(targetDir, "commands");
|
|
580
911
|
let count = 0;
|
|
581
912
|
if (config.commands_dir) {
|
|
582
913
|
const srcDir = resolve2(hubDir, config.commands_dir);
|
|
@@ -584,9 +915,9 @@ async function generateEditorCommands(config, hubDir, targetDir, editorName) {
|
|
|
584
915
|
const files = await readdir2(srcDir);
|
|
585
916
|
const mdFiles = files.filter((f) => f.endsWith(".md"));
|
|
586
917
|
if (mdFiles.length > 0) {
|
|
587
|
-
await
|
|
918
|
+
await mkdir4(commandsDir, { recursive: true });
|
|
588
919
|
for (const file of mdFiles) {
|
|
589
|
-
await copyFile(
|
|
920
|
+
await copyFile(join4(srcDir, file), join4(commandsDir, file));
|
|
590
921
|
count++;
|
|
591
922
|
}
|
|
592
923
|
}
|
|
@@ -595,10 +926,10 @@ async function generateEditorCommands(config, hubDir, targetDir, editorName) {
|
|
|
595
926
|
}
|
|
596
927
|
}
|
|
597
928
|
if (config.commands) {
|
|
598
|
-
await
|
|
929
|
+
await mkdir4(commandsDir, { recursive: true });
|
|
599
930
|
for (const [name, filePath] of Object.entries(config.commands)) {
|
|
600
931
|
const src = resolve2(hubDir, filePath);
|
|
601
|
-
const dest =
|
|
932
|
+
const dest = join4(commandsDir, name.endsWith(".md") ? name : `${name}.md`);
|
|
602
933
|
try {
|
|
603
934
|
await copyFile(src, dest);
|
|
604
935
|
count++;
|
|
@@ -613,27 +944,27 @@ async function generateEditorCommands(config, hubDir, targetDir, editorName) {
|
|
|
613
944
|
}
|
|
614
945
|
async function writeManagedFile(filePath, managedLines) {
|
|
615
946
|
const managedBlock = [HUB_MARKER_START, ...managedLines, HUB_MARKER_END].join("\n");
|
|
616
|
-
if (
|
|
617
|
-
const existing = await
|
|
947
|
+
if (existsSync3(filePath)) {
|
|
948
|
+
const existing = await readFile4(filePath, "utf-8");
|
|
618
949
|
const startIdx = existing.indexOf(HUB_MARKER_START);
|
|
619
950
|
const endIdx = existing.indexOf(HUB_MARKER_END);
|
|
620
951
|
if (startIdx !== -1 && endIdx !== -1) {
|
|
621
952
|
const before = existing.substring(0, startIdx);
|
|
622
953
|
const after = existing.substring(endIdx + HUB_MARKER_END.length);
|
|
623
|
-
await
|
|
954
|
+
await writeFile4(filePath, before + managedBlock + after, "utf-8");
|
|
624
955
|
return;
|
|
625
956
|
}
|
|
626
|
-
await
|
|
957
|
+
await writeFile4(filePath, managedBlock + "\n\n" + existing, "utf-8");
|
|
627
958
|
return;
|
|
628
959
|
}
|
|
629
|
-
await
|
|
960
|
+
await writeFile4(filePath, managedBlock + "\n", "utf-8");
|
|
630
961
|
}
|
|
631
962
|
async function generateCursor(config, hubDir) {
|
|
632
|
-
const cursorDir =
|
|
633
|
-
await
|
|
634
|
-
await
|
|
963
|
+
const cursorDir = join4(hubDir, ".cursor");
|
|
964
|
+
await mkdir4(join4(cursorDir, "rules"), { recursive: true });
|
|
965
|
+
await mkdir4(join4(cursorDir, "agents"), { recursive: true });
|
|
635
966
|
const gitignoreLines = buildGitignoreLines(config);
|
|
636
|
-
await writeManagedFile(
|
|
967
|
+
await writeManagedFile(join4(hubDir, ".gitignore"), gitignoreLines);
|
|
637
968
|
console.log(chalk3.green(" Generated .gitignore"));
|
|
638
969
|
const cursorignoreLines = [
|
|
639
970
|
"# Re-include repositories for AI context"
|
|
@@ -643,7 +974,7 @@ async function generateCursor(config, hubDir) {
|
|
|
643
974
|
cursorignoreLines.push(`!${repoDir}/`);
|
|
644
975
|
}
|
|
645
976
|
cursorignoreLines.push("", "# Re-include tasks for agent collaboration", "!tasks/");
|
|
646
|
-
await writeManagedFile(
|
|
977
|
+
await writeManagedFile(join4(hubDir, ".cursorignore"), cursorignoreLines);
|
|
647
978
|
console.log(chalk3.green(" Generated .cursorignore"));
|
|
648
979
|
if (config.mcps?.length) {
|
|
649
980
|
const mcpConfig = {};
|
|
@@ -656,27 +987,36 @@ async function generateCursor(config, hubDir) {
|
|
|
656
987
|
mcpConfig[mcp.name] = buildCursorMcpEntry(mcp);
|
|
657
988
|
}
|
|
658
989
|
}
|
|
659
|
-
|
|
660
|
-
|
|
990
|
+
const sandbox = getSandboxService(config);
|
|
991
|
+
if (sandbox && !mcpConfig["sandbox"]) {
|
|
992
|
+
mcpConfig["sandbox"] = buildSandboxMcpEntry(sandbox.port);
|
|
993
|
+
}
|
|
994
|
+
await writeFile4(
|
|
995
|
+
join4(cursorDir, "mcp.json"),
|
|
661
996
|
JSON.stringify({ mcpServers: mcpConfig }, null, 2) + "\n",
|
|
662
997
|
"utf-8"
|
|
663
998
|
);
|
|
664
999
|
console.log(chalk3.green(" Generated .cursor/mcp.json"));
|
|
665
1000
|
}
|
|
666
1001
|
const orchestratorRule = buildOrchestratorRule(config);
|
|
667
|
-
await
|
|
1002
|
+
await writeFile4(join4(cursorDir, "rules", "orchestrator.mdc"), orchestratorRule, "utf-8");
|
|
668
1003
|
console.log(chalk3.green(" Generated .cursor/rules/orchestrator.mdc"));
|
|
669
1004
|
const cleanedOrchestratorForAgents = orchestratorRule.replace(/^---[\s\S]*?---\n/m, "").trim();
|
|
670
1005
|
const skillsSectionCursor = await buildSkillsSection(hubDir, config);
|
|
671
|
-
const
|
|
672
|
-
|
|
1006
|
+
const personaCursor = await loadPersona(hubDir);
|
|
1007
|
+
const personaSectionCursor = personaCursor ? buildPersonaSection(personaCursor) : "";
|
|
1008
|
+
const agentsMdCursor = [cleanedOrchestratorForAgents, skillsSectionCursor, personaSectionCursor].filter(Boolean).join("\n");
|
|
1009
|
+
await writeFile4(join4(hubDir, "AGENTS.md"), agentsMdCursor + "\n", "utf-8");
|
|
673
1010
|
console.log(chalk3.green(" Generated AGENTS.md"));
|
|
1011
|
+
if (personaCursor) {
|
|
1012
|
+
console.log(chalk3.green(` Applied persona: ${personaCursor.name} (${personaCursor.role})`));
|
|
1013
|
+
}
|
|
674
1014
|
const hubSteeringDirCursor = resolve2(hubDir, "steering");
|
|
675
1015
|
try {
|
|
676
1016
|
const steeringFiles = await readdir2(hubSteeringDirCursor);
|
|
677
1017
|
const mdFiles = steeringFiles.filter((f) => f.endsWith(".md"));
|
|
678
1018
|
for (const file of mdFiles) {
|
|
679
|
-
const raw = await
|
|
1019
|
+
const raw = await readFile4(join4(hubSteeringDirCursor, file), "utf-8");
|
|
680
1020
|
const content = stripFrontMatter(raw);
|
|
681
1021
|
const mdcName = file.replace(/\.md$/, ".mdc");
|
|
682
1022
|
const mdcContent = `---
|
|
@@ -685,7 +1025,7 @@ alwaysApply: true
|
|
|
685
1025
|
---
|
|
686
1026
|
|
|
687
1027
|
${content}`;
|
|
688
|
-
await
|
|
1028
|
+
await writeFile4(join4(cursorDir, "rules", mdcName), mdcContent, "utf-8");
|
|
689
1029
|
}
|
|
690
1030
|
if (mdFiles.length > 0) {
|
|
691
1031
|
console.log(chalk3.green(` Copied ${mdFiles.length} steering files to .cursor/rules/`));
|
|
@@ -696,8 +1036,16 @@ ${content}`;
|
|
|
696
1036
|
try {
|
|
697
1037
|
const agentFiles = await readdir2(agentsDir);
|
|
698
1038
|
const mdFiles = agentFiles.filter((f) => f.endsWith(".md"));
|
|
1039
|
+
const sandboxSvc = getSandboxService(config);
|
|
699
1040
|
for (const file of mdFiles) {
|
|
700
|
-
|
|
1041
|
+
if (sandboxSvc) {
|
|
1042
|
+
const agentName = file.replace(/\.md$/, "");
|
|
1043
|
+
const agentContent = await readFile4(join4(agentsDir, file), "utf-8");
|
|
1044
|
+
const withSandbox = injectSandboxContext(agentName, agentContent, sandboxSvc.port);
|
|
1045
|
+
await writeFile4(join4(cursorDir, "agents", file), withSandbox, "utf-8");
|
|
1046
|
+
} else {
|
|
1047
|
+
await copyFile(join4(agentsDir, file), join4(cursorDir, "agents", file));
|
|
1048
|
+
}
|
|
701
1049
|
}
|
|
702
1050
|
console.log(chalk3.green(` Copied ${mdFiles.length} agent definitions`));
|
|
703
1051
|
} catch {
|
|
@@ -706,15 +1054,15 @@ ${content}`;
|
|
|
706
1054
|
const skillsDir = resolve2(hubDir, "skills");
|
|
707
1055
|
try {
|
|
708
1056
|
const skillFolders = await readdir2(skillsDir);
|
|
709
|
-
const cursorSkillsDir =
|
|
710
|
-
await
|
|
1057
|
+
const cursorSkillsDir = join4(cursorDir, "skills");
|
|
1058
|
+
await mkdir4(cursorSkillsDir, { recursive: true });
|
|
711
1059
|
let count = 0;
|
|
712
1060
|
for (const folder of skillFolders) {
|
|
713
|
-
const skillFile =
|
|
1061
|
+
const skillFile = join4(skillsDir, folder, "SKILL.md");
|
|
714
1062
|
try {
|
|
715
|
-
await
|
|
716
|
-
const srcDir =
|
|
717
|
-
const targetDir =
|
|
1063
|
+
await readFile4(skillFile);
|
|
1064
|
+
const srcDir = join4(skillsDir, folder);
|
|
1065
|
+
const targetDir = join4(cursorSkillsDir, folder);
|
|
718
1066
|
await cp(srcDir, targetDir, { recursive: true });
|
|
719
1067
|
count++;
|
|
720
1068
|
} catch {
|
|
@@ -725,15 +1073,15 @@ ${content}`;
|
|
|
725
1073
|
}
|
|
726
1074
|
} catch {
|
|
727
1075
|
}
|
|
728
|
-
const cursorSkillsDirForDocs =
|
|
729
|
-
await
|
|
1076
|
+
const cursorSkillsDirForDocs = join4(cursorDir, "skills");
|
|
1077
|
+
await mkdir4(cursorSkillsDirForDocs, { recursive: true });
|
|
730
1078
|
await fetchHubDocsSkill(cursorSkillsDirForDocs);
|
|
731
|
-
await syncRemoteSources(config, hubDir,
|
|
1079
|
+
await syncRemoteSources(config, hubDir, join4(cursorDir, "skills"), join4(cursorDir, "rules"));
|
|
732
1080
|
if (config.hooks) {
|
|
733
1081
|
const cursorHooks = buildCursorHooks(config.hooks);
|
|
734
1082
|
if (cursorHooks) {
|
|
735
|
-
await
|
|
736
|
-
|
|
1083
|
+
await writeFile4(
|
|
1084
|
+
join4(cursorDir, "hooks.json"),
|
|
737
1085
|
JSON.stringify(cursorHooks, null, 2) + "\n",
|
|
738
1086
|
"utf-8"
|
|
739
1087
|
);
|
|
@@ -743,6 +1091,14 @@ ${content}`;
|
|
|
743
1091
|
await generateEditorCommands(config, hubDir, cursorDir, ".cursor/commands/");
|
|
744
1092
|
await generateVSCodeSettings(config, hubDir);
|
|
745
1093
|
}
|
|
1094
|
+
function buildSandboxMcpEntry(port) {
|
|
1095
|
+
return { url: `http://localhost:${port}/mcp` };
|
|
1096
|
+
}
|
|
1097
|
+
function getSandboxService(config) {
|
|
1098
|
+
const svc = config.services?.find((s) => s.type === "sandbox");
|
|
1099
|
+
if (!svc) return null;
|
|
1100
|
+
return { port: svc.port ?? 8080 };
|
|
1101
|
+
}
|
|
746
1102
|
function buildProxyUpstreams(proxyMcp, allMcps) {
|
|
747
1103
|
const upstreamNames = new Set(proxyMcp.upstreams || []);
|
|
748
1104
|
const upstreamEntries = [];
|
|
@@ -876,6 +1232,24 @@ function buildKiroMcpEntry(mcp, mode = "editor") {
|
|
|
876
1232
|
...autoApprove && { autoApprove }
|
|
877
1233
|
};
|
|
878
1234
|
}
|
|
1235
|
+
var SANDBOX_AGENT_TARGETS = /* @__PURE__ */ new Set(["qa-frontend", "qa-backend", "coding-frontend", "coding-backend"]);
|
|
1236
|
+
function injectSandboxContext(agentName, content, sandboxPort) {
|
|
1237
|
+
if (!SANDBOX_AGENT_TARGETS.has(agentName)) return content;
|
|
1238
|
+
const section = `
|
|
1239
|
+
## Sandbox Environment
|
|
1240
|
+
|
|
1241
|
+
A sandboxed execution environment is available via the \`sandbox\` MCP (http://localhost:${sandboxPort}/mcp).
|
|
1242
|
+
|
|
1243
|
+
Use it to:
|
|
1244
|
+
- Run shell commands: \`shell.exec\`
|
|
1245
|
+
- Read/write files: \`file.read\`, \`file.write\`
|
|
1246
|
+
- Control a real browser: \`browser.navigate\`, \`browser.screenshot\`, \`browser.click\`
|
|
1247
|
+
- Execute code: \`jupyter.execute\`
|
|
1248
|
+
|
|
1249
|
+
The sandbox workspace is mounted at \`/home/gem/workspace\`. Prefer running builds, tests, and browser interactions inside the sandbox rather than on the host machine.
|
|
1250
|
+
`;
|
|
1251
|
+
return content.trimEnd() + "\n" + section;
|
|
1252
|
+
}
|
|
879
1253
|
function buildKiroAgentContent(rawContent) {
|
|
880
1254
|
const fmMatch = rawContent.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
881
1255
|
if (!fmMatch) {
|
|
@@ -1179,9 +1553,9 @@ async function buildSkillsSection(hubDir, config) {
|
|
|
1179
1553
|
try {
|
|
1180
1554
|
const folders = await readdir2(skillsDir);
|
|
1181
1555
|
for (const folder of folders) {
|
|
1182
|
-
const skillPath =
|
|
1556
|
+
const skillPath = join4(skillsDir, folder, "SKILL.md");
|
|
1183
1557
|
try {
|
|
1184
|
-
const content = await
|
|
1558
|
+
const content = await readFile4(skillPath, "utf-8");
|
|
1185
1559
|
const fm = parseFrontMatter(content);
|
|
1186
1560
|
if (fm?.name) {
|
|
1187
1561
|
skillEntries.push({
|
|
@@ -1504,18 +1878,18 @@ If any validation agent leaves comments requiring fixes, call the relevant codin
|
|
|
1504
1878
|
return parts.join("\n");
|
|
1505
1879
|
}
|
|
1506
1880
|
async function generateOpenCode(config, hubDir) {
|
|
1507
|
-
const opencodeDir =
|
|
1508
|
-
await
|
|
1509
|
-
await
|
|
1510
|
-
await
|
|
1511
|
-
await
|
|
1512
|
-
await
|
|
1881
|
+
const opencodeDir = join4(hubDir, ".opencode");
|
|
1882
|
+
await mkdir4(join4(opencodeDir, "agents"), { recursive: true });
|
|
1883
|
+
await mkdir4(join4(opencodeDir, "rules"), { recursive: true });
|
|
1884
|
+
await mkdir4(join4(opencodeDir, "skills"), { recursive: true });
|
|
1885
|
+
await mkdir4(join4(opencodeDir, "commands"), { recursive: true });
|
|
1886
|
+
await mkdir4(join4(opencodeDir, "plugins"), { recursive: true });
|
|
1513
1887
|
const gitignoreLines = buildGitignoreLines(config);
|
|
1514
|
-
await writeManagedFile(
|
|
1888
|
+
await writeManagedFile(join4(hubDir, ".gitignore"), gitignoreLines);
|
|
1515
1889
|
console.log(chalk3.green(" Generated .gitignore"));
|
|
1516
1890
|
if (config.repos.length > 0) {
|
|
1517
1891
|
const ignoreContent = config.repos.map((r) => `!${r.name}`).join("\n") + "\n";
|
|
1518
|
-
await
|
|
1892
|
+
await writeFile4(join4(hubDir, ".ignore"), ignoreContent, "utf-8");
|
|
1519
1893
|
console.log(chalk3.green(" Generated .ignore"));
|
|
1520
1894
|
}
|
|
1521
1895
|
const orchestratorContent = buildOpenCodeOrchestratorRule(config);
|
|
@@ -1523,22 +1897,27 @@ async function generateOpenCode(config, hubDir) {
|
|
|
1523
1897
|
"Development orchestrator. Delegates specialized work to subagents following a structured pipeline: refinement, coding, review, QA, and delivery.",
|
|
1524
1898
|
orchestratorContent
|
|
1525
1899
|
);
|
|
1526
|
-
await
|
|
1900
|
+
await writeFile4(join4(opencodeDir, "agents", "orchestrator.md"), orchestratorAgent, "utf-8");
|
|
1527
1901
|
console.log(chalk3.green(" Generated .opencode/agents/orchestrator.md (primary agent)"));
|
|
1528
|
-
await rm(
|
|
1902
|
+
await rm(join4(opencodeDir, "rules", "orchestrator.md")).catch(() => {
|
|
1529
1903
|
});
|
|
1530
1904
|
const skillsSectionOC = await buildSkillsSection(hubDir, config);
|
|
1531
|
-
const
|
|
1532
|
-
|
|
1905
|
+
const personaOC = await loadPersona(hubDir);
|
|
1906
|
+
const personaSectionOC = personaOC ? buildPersonaSection(personaOC) : "";
|
|
1907
|
+
const agentsMdOC = [orchestratorContent, skillsSectionOC, personaSectionOC].filter(Boolean).join("\n");
|
|
1908
|
+
await writeFile4(join4(hubDir, "AGENTS.md"), agentsMdOC + "\n", "utf-8");
|
|
1533
1909
|
console.log(chalk3.green(" Generated AGENTS.md"));
|
|
1910
|
+
if (personaOC) {
|
|
1911
|
+
console.log(chalk3.green(` Applied persona: ${personaOC.name} (${personaOC.role})`));
|
|
1912
|
+
}
|
|
1534
1913
|
const hubSteeringDirOC = resolve2(hubDir, "steering");
|
|
1535
1914
|
try {
|
|
1536
1915
|
const steeringFiles = await readdir2(hubSteeringDirOC);
|
|
1537
1916
|
const mdFiles = steeringFiles.filter((f) => f.endsWith(".md"));
|
|
1538
1917
|
for (const file of mdFiles) {
|
|
1539
|
-
const raw = await
|
|
1918
|
+
const raw = await readFile4(join4(hubSteeringDirOC, file), "utf-8");
|
|
1540
1919
|
const content = stripFrontMatter(raw);
|
|
1541
|
-
await
|
|
1920
|
+
await writeFile4(join4(opencodeDir, "rules", file), content, "utf-8");
|
|
1542
1921
|
}
|
|
1543
1922
|
if (mdFiles.length > 0) {
|
|
1544
1923
|
console.log(chalk3.green(` Copied ${mdFiles.length} steering files to .opencode/rules/`));
|
|
@@ -1563,8 +1942,8 @@ async function generateOpenCode(config, hubDir) {
|
|
|
1563
1942
|
opencodeConfig.mcp = mcpConfig;
|
|
1564
1943
|
}
|
|
1565
1944
|
opencodeConfig.instructions = [".opencode/rules/*.md"];
|
|
1566
|
-
await
|
|
1567
|
-
|
|
1945
|
+
await writeFile4(
|
|
1946
|
+
join4(hubDir, "opencode.json"),
|
|
1568
1947
|
JSON.stringify(opencodeConfig, null, 2) + "\n",
|
|
1569
1948
|
"utf-8"
|
|
1570
1949
|
);
|
|
@@ -1575,10 +1954,10 @@ async function generateOpenCode(config, hubDir) {
|
|
|
1575
1954
|
const mdFiles = agentFiles.filter((f) => f.endsWith(".md"));
|
|
1576
1955
|
for (const file of mdFiles) {
|
|
1577
1956
|
if (file === "orchestrator.md") continue;
|
|
1578
|
-
const content = await
|
|
1957
|
+
const content = await readFile4(join4(agentsDir, file), "utf-8");
|
|
1579
1958
|
const agentName = file.replace(/\.md$/, "");
|
|
1580
1959
|
const converted = buildOpenCodeAgentMarkdown(agentName, content);
|
|
1581
|
-
await
|
|
1960
|
+
await writeFile4(join4(opencodeDir, "agents", file), converted, "utf-8");
|
|
1582
1961
|
}
|
|
1583
1962
|
console.log(chalk3.green(` Copied ${mdFiles.length} agents to .opencode/agents/`));
|
|
1584
1963
|
} catch {
|
|
@@ -1589,10 +1968,10 @@ async function generateOpenCode(config, hubDir) {
|
|
|
1589
1968
|
const skillFolders = await readdir2(skillsDir);
|
|
1590
1969
|
let count = 0;
|
|
1591
1970
|
for (const folder of skillFolders) {
|
|
1592
|
-
const skillFile =
|
|
1971
|
+
const skillFile = join4(skillsDir, folder, "SKILL.md");
|
|
1593
1972
|
try {
|
|
1594
|
-
await
|
|
1595
|
-
await cp(
|
|
1973
|
+
await readFile4(skillFile);
|
|
1974
|
+
await cp(join4(skillsDir, folder), join4(opencodeDir, "skills", folder), { recursive: true });
|
|
1596
1975
|
count++;
|
|
1597
1976
|
} catch {
|
|
1598
1977
|
}
|
|
@@ -1602,13 +1981,13 @@ async function generateOpenCode(config, hubDir) {
|
|
|
1602
1981
|
}
|
|
1603
1982
|
} catch {
|
|
1604
1983
|
}
|
|
1605
|
-
await fetchHubDocsSkill(
|
|
1606
|
-
await syncRemoteSources(config, hubDir,
|
|
1984
|
+
await fetchHubDocsSkill(join4(opencodeDir, "skills"));
|
|
1985
|
+
await syncRemoteSources(config, hubDir, join4(opencodeDir, "skills"), join4(opencodeDir, "rules"));
|
|
1607
1986
|
await generateEditorCommands(config, hubDir, opencodeDir, ".opencode/commands/");
|
|
1608
1987
|
if (config.hooks) {
|
|
1609
1988
|
const plugin = buildOpenCodeHooksPlugin(config.hooks);
|
|
1610
1989
|
if (plugin) {
|
|
1611
|
-
await
|
|
1990
|
+
await writeFile4(join4(opencodeDir, "plugins", "hub-hooks.js"), plugin, "utf-8");
|
|
1612
1991
|
console.log(chalk3.green(" Generated .opencode/plugins/hub-hooks.js"));
|
|
1613
1992
|
}
|
|
1614
1993
|
}
|
|
@@ -2095,14 +2474,19 @@ function formatAction(action) {
|
|
|
2095
2474
|
return map[action] || action;
|
|
2096
2475
|
}
|
|
2097
2476
|
async function generateClaudeCode(config, hubDir) {
|
|
2098
|
-
const claudeDir =
|
|
2099
|
-
await
|
|
2477
|
+
const claudeDir = join4(hubDir, ".claude");
|
|
2478
|
+
await mkdir4(join4(claudeDir, "agents"), { recursive: true });
|
|
2100
2479
|
const orchestratorRule = buildOrchestratorRule(config);
|
|
2101
2480
|
const cleanedOrchestrator = orchestratorRule.replace(/^---[\s\S]*?---\n/m, "").trim();
|
|
2102
2481
|
const skillsSectionClaude = await buildSkillsSection(hubDir, config);
|
|
2103
|
-
const
|
|
2104
|
-
|
|
2482
|
+
const personaClaude = await loadPersona(hubDir);
|
|
2483
|
+
const personaSectionClaude = personaClaude ? buildPersonaSection(personaClaude) : "";
|
|
2484
|
+
const agentsMdClaude = [cleanedOrchestrator, skillsSectionClaude, personaSectionClaude].filter(Boolean).join("\n");
|
|
2485
|
+
await writeFile4(join4(hubDir, "AGENTS.md"), agentsMdClaude + "\n", "utf-8");
|
|
2105
2486
|
console.log(chalk3.green(" Generated AGENTS.md"));
|
|
2487
|
+
if (personaClaude) {
|
|
2488
|
+
console.log(chalk3.green(` Applied persona: ${personaClaude.name} (${personaClaude.role})`));
|
|
2489
|
+
}
|
|
2106
2490
|
const claudeMdSections = [];
|
|
2107
2491
|
claudeMdSections.push(cleanedOrchestrator);
|
|
2108
2492
|
const agentsDir = resolve2(hubDir, "agents");
|
|
@@ -2110,7 +2494,7 @@ async function generateClaudeCode(config, hubDir) {
|
|
|
2110
2494
|
const agentFiles = await readdir2(agentsDir);
|
|
2111
2495
|
const mdFiles = agentFiles.filter((f) => f.endsWith(".md"));
|
|
2112
2496
|
for (const file of mdFiles) {
|
|
2113
|
-
await copyFile(
|
|
2497
|
+
await copyFile(join4(agentsDir, file), join4(claudeDir, "agents", file));
|
|
2114
2498
|
}
|
|
2115
2499
|
console.log(chalk3.green(` Copied ${mdFiles.length} agents to .claude/agents/`));
|
|
2116
2500
|
} catch {
|
|
@@ -2119,15 +2503,15 @@ async function generateClaudeCode(config, hubDir) {
|
|
|
2119
2503
|
const skillsDir = resolve2(hubDir, "skills");
|
|
2120
2504
|
try {
|
|
2121
2505
|
const skillFolders = await readdir2(skillsDir);
|
|
2122
|
-
const claudeSkillsDir =
|
|
2123
|
-
await
|
|
2506
|
+
const claudeSkillsDir = join4(claudeDir, "skills");
|
|
2507
|
+
await mkdir4(claudeSkillsDir, { recursive: true });
|
|
2124
2508
|
let count = 0;
|
|
2125
2509
|
for (const folder of skillFolders) {
|
|
2126
|
-
const skillFile =
|
|
2510
|
+
const skillFile = join4(skillsDir, folder, "SKILL.md");
|
|
2127
2511
|
try {
|
|
2128
|
-
await
|
|
2129
|
-
const srcDir =
|
|
2130
|
-
const targetDir =
|
|
2512
|
+
await readFile4(skillFile);
|
|
2513
|
+
const srcDir = join4(skillsDir, folder);
|
|
2514
|
+
const targetDir = join4(claudeSkillsDir, folder);
|
|
2131
2515
|
await cp(srcDir, targetDir, { recursive: true });
|
|
2132
2516
|
count++;
|
|
2133
2517
|
} catch {
|
|
@@ -2138,16 +2522,16 @@ async function generateClaudeCode(config, hubDir) {
|
|
|
2138
2522
|
}
|
|
2139
2523
|
} catch {
|
|
2140
2524
|
}
|
|
2141
|
-
const claudeSkillsDirForDocs =
|
|
2142
|
-
await
|
|
2525
|
+
const claudeSkillsDirForDocs = join4(claudeDir, "skills");
|
|
2526
|
+
await mkdir4(claudeSkillsDirForDocs, { recursive: true });
|
|
2143
2527
|
await fetchHubDocsSkill(claudeSkillsDirForDocs);
|
|
2144
|
-
await syncRemoteSources(config, hubDir,
|
|
2528
|
+
await syncRemoteSources(config, hubDir, join4(claudeDir, "skills"), join4(claudeDir, "steering"));
|
|
2145
2529
|
const hubSteeringDirClaude = resolve2(hubDir, "steering");
|
|
2146
2530
|
try {
|
|
2147
2531
|
const steeringFiles = await readdir2(hubSteeringDirClaude);
|
|
2148
2532
|
const mdFiles = steeringFiles.filter((f) => f.endsWith(".md"));
|
|
2149
2533
|
for (const file of mdFiles) {
|
|
2150
|
-
const raw = await
|
|
2534
|
+
const raw = await readFile4(join4(hubSteeringDirClaude, file), "utf-8");
|
|
2151
2535
|
const content = stripFrontMatter(raw).trim();
|
|
2152
2536
|
if (content) {
|
|
2153
2537
|
claudeMdSections.push(content);
|
|
@@ -2158,7 +2542,7 @@ async function generateClaudeCode(config, hubDir) {
|
|
|
2158
2542
|
}
|
|
2159
2543
|
} catch {
|
|
2160
2544
|
}
|
|
2161
|
-
await
|
|
2545
|
+
await writeFile4(join4(hubDir, "CLAUDE.md"), claudeMdSections.join("\n\n"), "utf-8");
|
|
2162
2546
|
console.log(chalk3.green(" Generated CLAUDE.md"));
|
|
2163
2547
|
if (config.mcps?.length) {
|
|
2164
2548
|
const mcpJson = {};
|
|
@@ -2171,8 +2555,8 @@ async function generateClaudeCode(config, hubDir) {
|
|
|
2171
2555
|
mcpJson[mcp.name] = buildClaudeCodeMcpEntry(mcp);
|
|
2172
2556
|
}
|
|
2173
2557
|
}
|
|
2174
|
-
await
|
|
2175
|
-
|
|
2558
|
+
await writeFile4(
|
|
2559
|
+
join4(hubDir, ".mcp.json"),
|
|
2176
2560
|
JSON.stringify({ mcpServers: mcpJson }, null, 2) + "\n",
|
|
2177
2561
|
"utf-8"
|
|
2178
2562
|
);
|
|
@@ -2214,22 +2598,22 @@ async function generateClaudeCode(config, hubDir) {
|
|
|
2214
2598
|
claudeSettings.hooks = claudeHooks;
|
|
2215
2599
|
}
|
|
2216
2600
|
}
|
|
2217
|
-
await
|
|
2218
|
-
|
|
2601
|
+
await writeFile4(
|
|
2602
|
+
join4(claudeDir, "settings.json"),
|
|
2219
2603
|
JSON.stringify(claudeSettings, null, 2) + "\n",
|
|
2220
2604
|
"utf-8"
|
|
2221
2605
|
);
|
|
2222
2606
|
console.log(chalk3.green(" Generated .claude/settings.json"));
|
|
2223
2607
|
const gitignoreLines = buildGitignoreLines(config);
|
|
2224
|
-
await writeManagedFile(
|
|
2608
|
+
await writeManagedFile(join4(hubDir, ".gitignore"), gitignoreLines);
|
|
2225
2609
|
console.log(chalk3.green(" Generated .gitignore"));
|
|
2226
2610
|
}
|
|
2227
2611
|
async function generateKiro(config, hubDir) {
|
|
2228
|
-
const kiroDir =
|
|
2229
|
-
const steeringDir =
|
|
2230
|
-
const settingsDir =
|
|
2231
|
-
await
|
|
2232
|
-
await
|
|
2612
|
+
const kiroDir = join4(hubDir, ".kiro");
|
|
2613
|
+
const steeringDir = join4(kiroDir, "steering");
|
|
2614
|
+
const settingsDir = join4(kiroDir, "settings");
|
|
2615
|
+
await mkdir4(steeringDir, { recursive: true });
|
|
2616
|
+
await mkdir4(settingsDir, { recursive: true });
|
|
2233
2617
|
let mode = await getKiroMode(hubDir);
|
|
2234
2618
|
if (!mode) {
|
|
2235
2619
|
const { kiroMode } = await inquirer.prompt([
|
|
@@ -2250,25 +2634,32 @@ async function generateKiro(config, hubDir) {
|
|
|
2250
2634
|
console.log(chalk3.dim(` Using saved Kiro mode: ${mode}`));
|
|
2251
2635
|
}
|
|
2252
2636
|
const gitignoreLines = buildGitignoreLines(config);
|
|
2253
|
-
await writeManagedFile(
|
|
2637
|
+
await writeManagedFile(join4(hubDir, ".gitignore"), gitignoreLines);
|
|
2254
2638
|
console.log(chalk3.green(" Generated .gitignore"));
|
|
2255
2639
|
const kiroRule = buildKiroOrchestratorRule(config);
|
|
2256
2640
|
const skillsSection = await buildSkillsSection(hubDir, config);
|
|
2257
|
-
const
|
|
2258
|
-
|
|
2641
|
+
const personaKiro = await loadPersona(hubDir);
|
|
2642
|
+
const personaSectionKiro = personaKiro ? buildPersonaSection(personaKiro) : "";
|
|
2643
|
+
const kiroRuleWithSkills = [kiroRule, skillsSection, personaSectionKiro].filter(Boolean).join("\n");
|
|
2644
|
+
await writeFile4(join4(hubDir, "AGENTS.md"), kiroRuleWithSkills + "\n", "utf-8");
|
|
2259
2645
|
console.log(chalk3.green(" Generated AGENTS.md"));
|
|
2646
|
+
if (personaKiro) {
|
|
2647
|
+
console.log(chalk3.green(` Applied persona: ${personaKiro.name} (${personaKiro.role})`));
|
|
2648
|
+
}
|
|
2649
|
+
await rm(join4(steeringDir, "orchestrator.md")).catch(() => {
|
|
2650
|
+
});
|
|
2260
2651
|
const hubSteeringDir = resolve2(hubDir, "steering");
|
|
2261
2652
|
try {
|
|
2262
2653
|
const steeringFiles = await readdir2(hubSteeringDir);
|
|
2263
2654
|
const mdFiles = steeringFiles.filter((f) => f.endsWith(".md"));
|
|
2264
2655
|
for (const file of mdFiles) {
|
|
2265
|
-
const raw = await
|
|
2656
|
+
const raw = await readFile4(join4(hubSteeringDir, file), "utf-8");
|
|
2266
2657
|
const content = stripFrontMatter(raw);
|
|
2267
|
-
const destPath =
|
|
2658
|
+
const destPath = join4(steeringDir, file);
|
|
2268
2659
|
let inclusion = "always";
|
|
2269
2660
|
let meta;
|
|
2270
|
-
if (
|
|
2271
|
-
const existingContent = await
|
|
2661
|
+
if (existsSync3(destPath)) {
|
|
2662
|
+
const existingContent = await readFile4(destPath, "utf-8");
|
|
2272
2663
|
const existingFm = parseFrontMatter(existingContent);
|
|
2273
2664
|
if (existingFm) {
|
|
2274
2665
|
if (existingFm.inclusion === "auto" || existingFm.inclusion === "manual" || existingFm.inclusion === "fileMatch") {
|
|
@@ -2293,7 +2684,7 @@ async function generateKiro(config, hubDir) {
|
|
|
2293
2684
|
}
|
|
2294
2685
|
}
|
|
2295
2686
|
const kiroSteering = buildKiroSteeringContent(content, inclusion, meta);
|
|
2296
|
-
await
|
|
2687
|
+
await writeFile4(destPath, kiroSteering, "utf-8");
|
|
2297
2688
|
}
|
|
2298
2689
|
if (mdFiles.length > 0) {
|
|
2299
2690
|
console.log(chalk3.green(` Copied ${mdFiles.length} steering files to .kiro/steering/`));
|
|
@@ -2302,14 +2693,17 @@ async function generateKiro(config, hubDir) {
|
|
|
2302
2693
|
}
|
|
2303
2694
|
const agentsDir = resolve2(hubDir, "agents");
|
|
2304
2695
|
try {
|
|
2305
|
-
const kiroAgentsDir =
|
|
2306
|
-
await
|
|
2696
|
+
const kiroAgentsDir = join4(kiroDir, "agents");
|
|
2697
|
+
await mkdir4(kiroAgentsDir, { recursive: true });
|
|
2307
2698
|
const agentFiles = await readdir2(agentsDir);
|
|
2308
2699
|
const mdFiles = agentFiles.filter((f) => f.endsWith(".md"));
|
|
2309
2700
|
for (const file of mdFiles) {
|
|
2310
|
-
const agentContent = await
|
|
2311
|
-
const
|
|
2312
|
-
|
|
2701
|
+
const agentContent = await readFile4(join4(agentsDir, file), "utf-8");
|
|
2702
|
+
const agentName = file.replace(/\.md$/, "");
|
|
2703
|
+
const sandboxSvc = getSandboxService(config);
|
|
2704
|
+
const withSandbox = sandboxSvc ? injectSandboxContext(agentName, agentContent, sandboxSvc.port) : agentContent;
|
|
2705
|
+
const kiroAgent = buildKiroAgentContent(withSandbox);
|
|
2706
|
+
await writeFile4(join4(kiroAgentsDir, file), kiroAgent, "utf-8");
|
|
2313
2707
|
}
|
|
2314
2708
|
console.log(chalk3.green(` Copied ${mdFiles.length} agents to .kiro/agents/`));
|
|
2315
2709
|
} catch {
|
|
@@ -2318,15 +2712,15 @@ async function generateKiro(config, hubDir) {
|
|
|
2318
2712
|
const skillsDir = resolve2(hubDir, "skills");
|
|
2319
2713
|
try {
|
|
2320
2714
|
const skillFolders = await readdir2(skillsDir);
|
|
2321
|
-
const kiroSkillsDir =
|
|
2322
|
-
await
|
|
2715
|
+
const kiroSkillsDir = join4(kiroDir, "skills");
|
|
2716
|
+
await mkdir4(kiroSkillsDir, { recursive: true });
|
|
2323
2717
|
let count = 0;
|
|
2324
2718
|
for (const folder of skillFolders) {
|
|
2325
|
-
const skillFile =
|
|
2719
|
+
const skillFile = join4(skillsDir, folder, "SKILL.md");
|
|
2326
2720
|
try {
|
|
2327
|
-
await
|
|
2328
|
-
const srcDir =
|
|
2329
|
-
const targetDir =
|
|
2721
|
+
await readFile4(skillFile);
|
|
2722
|
+
const srcDir = join4(skillsDir, folder);
|
|
2723
|
+
const targetDir = join4(kiroSkillsDir, folder);
|
|
2330
2724
|
await cp(srcDir, targetDir, { recursive: true });
|
|
2331
2725
|
count++;
|
|
2332
2726
|
} catch {
|
|
@@ -2337,10 +2731,10 @@ async function generateKiro(config, hubDir) {
|
|
|
2337
2731
|
}
|
|
2338
2732
|
} catch {
|
|
2339
2733
|
}
|
|
2340
|
-
const kiroSkillsDirForDocs =
|
|
2341
|
-
await
|
|
2734
|
+
const kiroSkillsDirForDocs = join4(kiroDir, "skills");
|
|
2735
|
+
await mkdir4(kiroSkillsDirForDocs, { recursive: true });
|
|
2342
2736
|
await fetchHubDocsSkill(kiroSkillsDirForDocs);
|
|
2343
|
-
await syncRemoteSources(config, hubDir,
|
|
2737
|
+
await syncRemoteSources(config, hubDir, join4(kiroDir, "skills"), steeringDir);
|
|
2344
2738
|
if (config.mcps?.length) {
|
|
2345
2739
|
const mcpConfig = {};
|
|
2346
2740
|
const upstreamSet = getUpstreamNames(config.mcps);
|
|
@@ -2353,10 +2747,14 @@ async function generateKiro(config, hubDir) {
|
|
|
2353
2747
|
mcpConfig[mcp.name] = buildKiroMcpEntry(mcp, mode);
|
|
2354
2748
|
}
|
|
2355
2749
|
}
|
|
2356
|
-
const
|
|
2750
|
+
const sandbox = getSandboxService(config);
|
|
2751
|
+
if (sandbox && !mcpConfig["sandbox"]) {
|
|
2752
|
+
mcpConfig["sandbox"] = buildSandboxMcpEntry(sandbox.port);
|
|
2753
|
+
}
|
|
2754
|
+
const mcpJsonPath = join4(settingsDir, "mcp.json");
|
|
2357
2755
|
const disabledState = await readExistingMcpDisabledState(mcpJsonPath);
|
|
2358
2756
|
applyDisabledState(mcpConfig, disabledState);
|
|
2359
|
-
await
|
|
2757
|
+
await writeFile4(
|
|
2360
2758
|
mcpJsonPath,
|
|
2361
2759
|
JSON.stringify({ mcpServers: mcpConfig }, null, 2) + "\n",
|
|
2362
2760
|
"utf-8"
|
|
@@ -2383,13 +2781,13 @@ async function generateKiro(config, hubDir) {
|
|
|
2383
2781
|
await generateVSCodeSettings(config, hubDir);
|
|
2384
2782
|
}
|
|
2385
2783
|
async function generateVSCodeSettings(config, hubDir) {
|
|
2386
|
-
const vscodeDir =
|
|
2387
|
-
await
|
|
2388
|
-
const settingsPath =
|
|
2784
|
+
const vscodeDir = join4(hubDir, ".vscode");
|
|
2785
|
+
await mkdir4(vscodeDir, { recursive: true });
|
|
2786
|
+
const settingsPath = join4(vscodeDir, "settings.json");
|
|
2389
2787
|
let existing = {};
|
|
2390
|
-
if (
|
|
2788
|
+
if (existsSync3(settingsPath)) {
|
|
2391
2789
|
try {
|
|
2392
|
-
const raw = await
|
|
2790
|
+
const raw = await readFile4(settingsPath, "utf-8");
|
|
2393
2791
|
existing = JSON.parse(raw);
|
|
2394
2792
|
} catch {
|
|
2395
2793
|
existing = {};
|
|
@@ -2401,14 +2799,14 @@ async function generateVSCodeSettings(config, hubDir) {
|
|
|
2401
2799
|
"git.detectSubmodulesLimit": Math.max(config.repos.length * 2, 20)
|
|
2402
2800
|
};
|
|
2403
2801
|
const merged = { ...existing, ...managed };
|
|
2404
|
-
await
|
|
2802
|
+
await writeFile4(settingsPath, JSON.stringify(merged, null, 2) + "\n", "utf-8");
|
|
2405
2803
|
console.log(chalk3.green(" Generated .vscode/settings.json (git multi-repo detection)"));
|
|
2406
2804
|
const workspaceFile = `${config.name}.code-workspace`;
|
|
2407
|
-
const workspacePath =
|
|
2805
|
+
const workspacePath = join4(hubDir, workspaceFile);
|
|
2408
2806
|
let existingWorkspace = {};
|
|
2409
|
-
if (
|
|
2807
|
+
if (existsSync3(workspacePath)) {
|
|
2410
2808
|
try {
|
|
2411
|
-
const raw = await
|
|
2809
|
+
const raw = await readFile4(workspacePath, "utf-8");
|
|
2412
2810
|
existingWorkspace = JSON.parse(raw);
|
|
2413
2811
|
} catch {
|
|
2414
2812
|
existingWorkspace = {};
|
|
@@ -2418,7 +2816,7 @@ async function generateVSCodeSettings(config, hubDir) {
|
|
|
2418
2816
|
const existing2 = files.find((f) => f.endsWith(".code-workspace"));
|
|
2419
2817
|
if (existing2) {
|
|
2420
2818
|
try {
|
|
2421
|
-
const raw = await
|
|
2819
|
+
const raw = await readFile4(join4(hubDir, existing2), "utf-8");
|
|
2422
2820
|
existingWorkspace = JSON.parse(raw);
|
|
2423
2821
|
} catch {
|
|
2424
2822
|
existingWorkspace = {};
|
|
@@ -2454,7 +2852,7 @@ async function generateVSCodeSettings(config, hubDir) {
|
|
|
2454
2852
|
folders,
|
|
2455
2853
|
settings: existingWorkspace.settings || {}
|
|
2456
2854
|
};
|
|
2457
|
-
await
|
|
2855
|
+
await writeFile4(workspacePath, JSON.stringify(workspace, null, " ") + "\n", "utf-8");
|
|
2458
2856
|
console.log(chalk3.green(` Generated ${workspaceFile}`));
|
|
2459
2857
|
}
|
|
2460
2858
|
function extractEnvVarsByMcp(mcps) {
|
|
@@ -2501,7 +2899,7 @@ async function generateEnvExample(config, hubDir) {
|
|
|
2501
2899
|
totalVars++;
|
|
2502
2900
|
}
|
|
2503
2901
|
if (totalVars === 0) return;
|
|
2504
|
-
await
|
|
2902
|
+
await writeFile4(join4(hubDir, ".env.example"), lines.join("\n") + "\n", "utf-8");
|
|
2505
2903
|
console.log(chalk3.green(` Generated .env.example (${totalVars} vars)`));
|
|
2506
2904
|
}
|
|
2507
2905
|
function buildGitignoreLines(config) {
|
|
@@ -2591,7 +2989,7 @@ async function resolveEditor(opts) {
|
|
|
2591
2989
|
]);
|
|
2592
2990
|
return editor;
|
|
2593
2991
|
}
|
|
2594
|
-
var generateCommand = new
|
|
2992
|
+
var generateCommand = new Command2("generate").description("Generate editor-specific configuration files from hub.yaml").option("-e, --editor <editor>", "Target editor (cursor, claude-code, kiro, opencode)").option("--reset-editor", "Reset saved editor preference and choose again").option("--check", "Check if generated configs are outdated (exit code 1 if outdated)").action(async (opts) => {
|
|
2595
2993
|
const hubDir = process.cwd();
|
|
2596
2994
|
if (opts.check) {
|
|
2597
2995
|
const result = await checkOutdated(hubDir);
|
|
@@ -2656,6 +3054,10 @@ Generating ${generator.name} configuration
|
|
|
2656
3054
|
});
|
|
2657
3055
|
|
|
2658
3056
|
export {
|
|
3057
|
+
colors,
|
|
3058
|
+
symbols,
|
|
3059
|
+
horizontalLine,
|
|
3060
|
+
personaCommand,
|
|
2659
3061
|
generators,
|
|
2660
3062
|
generateCommand,
|
|
2661
3063
|
checkAndAutoRegenerate
|