@arvoretech/hub 0.13.4 → 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,22 +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"));
|
|
1004
|
+
const cleanedOrchestratorForAgents = orchestratorRule.replace(/^---[\s\S]*?---\n/m, "").trim();
|
|
1005
|
+
const skillsSectionCursor = await buildSkillsSection(hubDir, config);
|
|
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");
|
|
1010
|
+
console.log(chalk3.green(" Generated AGENTS.md"));
|
|
1011
|
+
if (personaCursor) {
|
|
1012
|
+
console.log(chalk3.green(` Applied persona: ${personaCursor.name} (${personaCursor.role})`));
|
|
1013
|
+
}
|
|
669
1014
|
const hubSteeringDirCursor = resolve2(hubDir, "steering");
|
|
670
1015
|
try {
|
|
671
1016
|
const steeringFiles = await readdir2(hubSteeringDirCursor);
|
|
672
1017
|
const mdFiles = steeringFiles.filter((f) => f.endsWith(".md"));
|
|
673
1018
|
for (const file of mdFiles) {
|
|
674
|
-
const raw = await
|
|
1019
|
+
const raw = await readFile4(join4(hubSteeringDirCursor, file), "utf-8");
|
|
675
1020
|
const content = stripFrontMatter(raw);
|
|
676
1021
|
const mdcName = file.replace(/\.md$/, ".mdc");
|
|
677
1022
|
const mdcContent = `---
|
|
@@ -680,7 +1025,7 @@ alwaysApply: true
|
|
|
680
1025
|
---
|
|
681
1026
|
|
|
682
1027
|
${content}`;
|
|
683
|
-
await
|
|
1028
|
+
await writeFile4(join4(cursorDir, "rules", mdcName), mdcContent, "utf-8");
|
|
684
1029
|
}
|
|
685
1030
|
if (mdFiles.length > 0) {
|
|
686
1031
|
console.log(chalk3.green(` Copied ${mdFiles.length} steering files to .cursor/rules/`));
|
|
@@ -691,8 +1036,16 @@ ${content}`;
|
|
|
691
1036
|
try {
|
|
692
1037
|
const agentFiles = await readdir2(agentsDir);
|
|
693
1038
|
const mdFiles = agentFiles.filter((f) => f.endsWith(".md"));
|
|
1039
|
+
const sandboxSvc = getSandboxService(config);
|
|
694
1040
|
for (const file of mdFiles) {
|
|
695
|
-
|
|
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
|
+
}
|
|
696
1049
|
}
|
|
697
1050
|
console.log(chalk3.green(` Copied ${mdFiles.length} agent definitions`));
|
|
698
1051
|
} catch {
|
|
@@ -701,15 +1054,15 @@ ${content}`;
|
|
|
701
1054
|
const skillsDir = resolve2(hubDir, "skills");
|
|
702
1055
|
try {
|
|
703
1056
|
const skillFolders = await readdir2(skillsDir);
|
|
704
|
-
const cursorSkillsDir =
|
|
705
|
-
await
|
|
1057
|
+
const cursorSkillsDir = join4(cursorDir, "skills");
|
|
1058
|
+
await mkdir4(cursorSkillsDir, { recursive: true });
|
|
706
1059
|
let count = 0;
|
|
707
1060
|
for (const folder of skillFolders) {
|
|
708
|
-
const skillFile =
|
|
1061
|
+
const skillFile = join4(skillsDir, folder, "SKILL.md");
|
|
709
1062
|
try {
|
|
710
|
-
await
|
|
711
|
-
const srcDir =
|
|
712
|
-
const targetDir =
|
|
1063
|
+
await readFile4(skillFile);
|
|
1064
|
+
const srcDir = join4(skillsDir, folder);
|
|
1065
|
+
const targetDir = join4(cursorSkillsDir, folder);
|
|
713
1066
|
await cp(srcDir, targetDir, { recursive: true });
|
|
714
1067
|
count++;
|
|
715
1068
|
} catch {
|
|
@@ -720,15 +1073,15 @@ ${content}`;
|
|
|
720
1073
|
}
|
|
721
1074
|
} catch {
|
|
722
1075
|
}
|
|
723
|
-
const cursorSkillsDirForDocs =
|
|
724
|
-
await
|
|
1076
|
+
const cursorSkillsDirForDocs = join4(cursorDir, "skills");
|
|
1077
|
+
await mkdir4(cursorSkillsDirForDocs, { recursive: true });
|
|
725
1078
|
await fetchHubDocsSkill(cursorSkillsDirForDocs);
|
|
726
|
-
await syncRemoteSources(config, hubDir,
|
|
1079
|
+
await syncRemoteSources(config, hubDir, join4(cursorDir, "skills"), join4(cursorDir, "rules"));
|
|
727
1080
|
if (config.hooks) {
|
|
728
1081
|
const cursorHooks = buildCursorHooks(config.hooks);
|
|
729
1082
|
if (cursorHooks) {
|
|
730
|
-
await
|
|
731
|
-
|
|
1083
|
+
await writeFile4(
|
|
1084
|
+
join4(cursorDir, "hooks.json"),
|
|
732
1085
|
JSON.stringify(cursorHooks, null, 2) + "\n",
|
|
733
1086
|
"utf-8"
|
|
734
1087
|
);
|
|
@@ -738,6 +1091,14 @@ ${content}`;
|
|
|
738
1091
|
await generateEditorCommands(config, hubDir, cursorDir, ".cursor/commands/");
|
|
739
1092
|
await generateVSCodeSettings(config, hubDir);
|
|
740
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
|
+
}
|
|
741
1102
|
function buildProxyUpstreams(proxyMcp, allMcps) {
|
|
742
1103
|
const upstreamNames = new Set(proxyMcp.upstreams || []);
|
|
743
1104
|
const upstreamEntries = [];
|
|
@@ -871,6 +1232,24 @@ function buildKiroMcpEntry(mcp, mode = "editor") {
|
|
|
871
1232
|
...autoApprove && { autoApprove }
|
|
872
1233
|
};
|
|
873
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
|
+
}
|
|
874
1253
|
function buildKiroAgentContent(rawContent) {
|
|
875
1254
|
const fmMatch = rawContent.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
876
1255
|
if (!fmMatch) {
|
|
@@ -1074,6 +1453,49 @@ You can communicate with agents from other developers on the team via the \`agen
|
|
|
1074
1453
|
- Read the thread before replying to avoid repeating what others said
|
|
1075
1454
|
- When starting a task that touches shared code, check recent threads for relevant context`;
|
|
1076
1455
|
}
|
|
1456
|
+
function hasKanbanMcp(mcps) {
|
|
1457
|
+
if (!mcps) return false;
|
|
1458
|
+
const proxyMcp = mcps.find((m) => m.upstreams && m.upstreams.length > 0);
|
|
1459
|
+
const directMatch = mcps.some((m) => m.name === "kanban" || m.package === "@arvoretech/kanban-mcp");
|
|
1460
|
+
const upstreamMatch = proxyMcp?.upstreams?.includes("kanban") ?? false;
|
|
1461
|
+
return directMatch || upstreamMatch;
|
|
1462
|
+
}
|
|
1463
|
+
function buildKanbanSection(mcps) {
|
|
1464
|
+
if (!hasKanbanMcp(mcps)) return "";
|
|
1465
|
+
return `
|
|
1466
|
+
## Kanban Board
|
|
1467
|
+
|
|
1468
|
+
This workspace has a persistent kanban board via the \`kanban\` MCP. Use it to organize work, track progress across sessions, and coordinate with other chats.
|
|
1469
|
+
|
|
1470
|
+
**When to use the kanban:**
|
|
1471
|
+
- At the start of a task, check the board for existing cards and active sessions
|
|
1472
|
+
- Break complex features into cards before starting implementation
|
|
1473
|
+
- Claim cards you're working on so other sessions can see
|
|
1474
|
+
- Release cards when done (default status: review)
|
|
1475
|
+
- Search for related cards before creating duplicates
|
|
1476
|
+
|
|
1477
|
+
**Workflow:**
|
|
1478
|
+
1. \`list_boards\` / \`get_board\` \u2014 See what's on the board and who's working on what
|
|
1479
|
+
2. \`create_card\` \u2014 Add new tasks to the appropriate column
|
|
1480
|
+
3. \`claim_card\` \u2014 Mark a card as being worked on by this session
|
|
1481
|
+
4. \`move_card\` \u2014 Move cards between columns as work progresses
|
|
1482
|
+
5. \`release_card\` \u2014 Release when done, with status and detail (e.g. "PR #123 created")
|
|
1483
|
+
6. \`search_cards\` \u2014 Find cards by meaning (semantic search)
|
|
1484
|
+
|
|
1485
|
+
**Available tools:** \`list_boards\`, \`create_board\`, \`get_board\`, \`get_card\`, \`create_card\`, \`update_card\`, \`move_card\`, \`claim_card\`, \`release_card\`, \`search_cards\`, \`archive_card\`, \`delete_card\`.
|
|
1486
|
+
|
|
1487
|
+
**Multi-session coordination:**
|
|
1488
|
+
- Always \`claim_card\` before starting work \u2014 other sessions will see it's taken
|
|
1489
|
+
- If a card is already claimed, pick another or use \`force: true\` to override stale sessions
|
|
1490
|
+
- Use \`get_board\` to see active sessions with duration (helps identify abandoned claims)
|
|
1491
|
+
- When finishing, \`release_card\` with a meaningful detail so the next session has context
|
|
1492
|
+
|
|
1493
|
+
**Best practices:**
|
|
1494
|
+
- Use subtasks (\`parent_card_id\`) to break down large cards
|
|
1495
|
+
- Tag cards consistently for easy filtering
|
|
1496
|
+
- Set priority to help triage (urgent > high > medium > low)
|
|
1497
|
+
- Check the board at the start of every session \u2014 don't start from zero`;
|
|
1498
|
+
}
|
|
1077
1499
|
function buildMcpToolsSection(mcps) {
|
|
1078
1500
|
if (!mcps || mcps.length === 0) return "";
|
|
1079
1501
|
const proxyMcp = mcps.find((m) => m.upstreams && m.upstreams.length > 0);
|
|
@@ -1125,6 +1547,141 @@ ${mcp.instructions.trim()}`);
|
|
|
1125
1547
|
}
|
|
1126
1548
|
return lines.join("\n");
|
|
1127
1549
|
}
|
|
1550
|
+
async function buildSkillsSection(hubDir, config) {
|
|
1551
|
+
const skillsDir = resolve2(hubDir, "skills");
|
|
1552
|
+
const skillEntries = [];
|
|
1553
|
+
try {
|
|
1554
|
+
const folders = await readdir2(skillsDir);
|
|
1555
|
+
for (const folder of folders) {
|
|
1556
|
+
const skillPath = join4(skillsDir, folder, "SKILL.md");
|
|
1557
|
+
try {
|
|
1558
|
+
const content = await readFile4(skillPath, "utf-8");
|
|
1559
|
+
const fm = parseFrontMatter(content);
|
|
1560
|
+
if (fm?.name) {
|
|
1561
|
+
skillEntries.push({
|
|
1562
|
+
name: fm.name,
|
|
1563
|
+
description: fm.description || ""
|
|
1564
|
+
});
|
|
1565
|
+
}
|
|
1566
|
+
} catch {
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
} catch {
|
|
1570
|
+
return null;
|
|
1571
|
+
}
|
|
1572
|
+
if (skillEntries.length === 0) return null;
|
|
1573
|
+
const repoSkillMap = /* @__PURE__ */ new Map();
|
|
1574
|
+
for (const repo of config.repos) {
|
|
1575
|
+
if (repo.skills?.length) {
|
|
1576
|
+
for (const skill of repo.skills) {
|
|
1577
|
+
const repos = repoSkillMap.get(skill) || [];
|
|
1578
|
+
repos.push(repo.path);
|
|
1579
|
+
repoSkillMap.set(skill, repos);
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
const parts = [];
|
|
1584
|
+
parts.push(`
|
|
1585
|
+
## Skills
|
|
1586
|
+
|
|
1587
|
+
This workspace has skills that provide specialized knowledge for specific domains and repositories.
|
|
1588
|
+
Consult the relevant skill before working in an unfamiliar area \u2014 they contain patterns, conventions, and project-specific guidance.
|
|
1589
|
+
|
|
1590
|
+
| Skill | Description | Repositories |
|
|
1591
|
+
|-------|-------------|--------------|`);
|
|
1592
|
+
for (const entry of skillEntries) {
|
|
1593
|
+
const repos = repoSkillMap.get(entry.name);
|
|
1594
|
+
const repoCol = repos ? repos.map((r) => `\`${r}\``).join(", ") : "\u2014";
|
|
1595
|
+
const desc = entry.description.replace(/\|/g, "\\|").split(".")[0].trim();
|
|
1596
|
+
parts.push(`| \`${entry.name}\` | ${desc} | ${repoCol} |`);
|
|
1597
|
+
}
|
|
1598
|
+
parts.push(`
|
|
1599
|
+
When to consult a skill:
|
|
1600
|
+
- Before writing code in a repository that has an associated skill
|
|
1601
|
+
- When making architecture or pattern decisions in a specific domain
|
|
1602
|
+
- When unsure about project conventions, libraries, or testing approaches
|
|
1603
|
+
- When the user's request touches a domain covered by an available skill
|
|
1604
|
+
|
|
1605
|
+
Additional context sources:
|
|
1606
|
+
- Use documentation MCPs to check library and framework docs before implementing
|
|
1607
|
+
- Use database MCPs to understand schema, query data, and verify state
|
|
1608
|
+
- Use package registry MCPs to verify security and versions before installing dependencies
|
|
1609
|
+
- Use the repository CLI commands (build, test, lint) to validate changes after implementation
|
|
1610
|
+
- Use monitoring MCPs for production debugging and log analysis when available`);
|
|
1611
|
+
return parts.join("\n");
|
|
1612
|
+
}
|
|
1613
|
+
function buildCoreBehaviorSections() {
|
|
1614
|
+
const sections = [];
|
|
1615
|
+
sections.push(`
|
|
1616
|
+
## Core Behavior
|
|
1617
|
+
|
|
1618
|
+
Be concise, clear, direct, and useful.
|
|
1619
|
+
Prefer technical accuracy over reassurance.
|
|
1620
|
+
Do not use hype, flattery, or exaggerated validation.
|
|
1621
|
+
Do not repeatedly apologize when something unexpected happens \u2014 explain what happened and continue.
|
|
1622
|
+
Do not claim actions were performed unless they were actually performed.
|
|
1623
|
+
Never invent facts, code behavior, file contents, tool capabilities, or execution outcomes.
|
|
1624
|
+
Focus on completing the user's task, not on narrating unnecessary process.`);
|
|
1625
|
+
sections.push(`
|
|
1626
|
+
## Working Style
|
|
1627
|
+
|
|
1628
|
+
Prefer the simplest solution that fully satisfies the request.
|
|
1629
|
+
Avoid over-engineering, speculative abstractions, premature generalization, and cleanup outside the requested scope.
|
|
1630
|
+
Prefer editing existing files over creating new files.
|
|
1631
|
+
Prefer minimal, reversible changes over broad rewrites unless the task explicitly requires a rewrite.
|
|
1632
|
+
Ask the user questions only when a real ambiguity materially affects the solution.
|
|
1633
|
+
Bias toward finding the answer yourself when the available context and tools are sufficient.`);
|
|
1634
|
+
sections.push(`
|
|
1635
|
+
## Search, Reading, and Investigation
|
|
1636
|
+
|
|
1637
|
+
If you are unsure how to satisfy the user's request, gather more information before answering.
|
|
1638
|
+
Prefer discovering answers yourself over asking the user for information that is likely available in the workspace, files, memories, or tools.
|
|
1639
|
+
|
|
1640
|
+
When reading code or documents:
|
|
1641
|
+
- Read enough surrounding context to avoid missing critical behavior
|
|
1642
|
+
- Do not propose modifications to code you have not inspected
|
|
1643
|
+
- If partial views may hide important logic, continue reading before deciding
|
|
1644
|
+
|
|
1645
|
+
For broader exploration:
|
|
1646
|
+
- Use lightweight search first
|
|
1647
|
+
- Escalate to deeper exploration or subagents only when the task is broad, ambiguous, or likely to require several search passes`);
|
|
1648
|
+
sections.push(`
|
|
1649
|
+
## Code Changes
|
|
1650
|
+
|
|
1651
|
+
When making code changes:
|
|
1652
|
+
- Ensure the produced code is runnable and internally consistent
|
|
1653
|
+
- Add required imports, wiring, dependencies, and integration points
|
|
1654
|
+
- Preserve the project's existing patterns unless there is a strong reason to change them
|
|
1655
|
+
- Read the relevant files or sections before modifying existing code
|
|
1656
|
+
- Understand the surrounding code paths and conventions
|
|
1657
|
+
- Prefer small, precise edits
|
|
1658
|
+
|
|
1659
|
+
If you introduce errors:
|
|
1660
|
+
- Try to fix them
|
|
1661
|
+
- Do not get stuck in unbounded retry loops (max 3 attempts on the same issue)
|
|
1662
|
+
- If repeated fixes fail, explain the remaining problem clearly
|
|
1663
|
+
|
|
1664
|
+
Never assume a library is available \u2014 check the dependency file or neighboring code first.
|
|
1665
|
+
When creating a new component, look at existing components to understand conventions.`);
|
|
1666
|
+
sections.push(`
|
|
1667
|
+
## Security and Safety
|
|
1668
|
+
|
|
1669
|
+
Never hardcode secrets, credentials, tokens, or API keys.
|
|
1670
|
+
Flag security risks when noticed.
|
|
1671
|
+
Avoid introducing vulnerabilities such as command injection, SQL injection, XSS, insecure secret handling, broken auth flows, unsafe deserialization, SSRF, or privilege escalation.
|
|
1672
|
+
Do not expose secrets in code, tests, examples, or logs.`);
|
|
1673
|
+
sections.push(`
|
|
1674
|
+
## Git and Operational Discipline
|
|
1675
|
+
|
|
1676
|
+
Do not commit, push, open pull requests, or notify external systems unless the user asked for it or the workspace flow explicitly requires it.
|
|
1677
|
+
|
|
1678
|
+
When handling git work:
|
|
1679
|
+
- Inspect status and diff before committing
|
|
1680
|
+
- Follow existing repository commit conventions
|
|
1681
|
+
- Prefer specific file staging over indiscriminate staging
|
|
1682
|
+
- Do not use destructive git commands without explicit user authorization`);
|
|
1683
|
+
return sections;
|
|
1684
|
+
}
|
|
1128
1685
|
function buildOpenCodeOrchestratorRule(config) {
|
|
1129
1686
|
const taskFolder = config.workflow?.task_folder || "./tasks/<TASK_ID>/";
|
|
1130
1687
|
const steps = config.workflow?.pipeline || [];
|
|
@@ -1218,6 +1775,8 @@ Available tools: \`search_memories\`, \`get_memory\`, \`add_memory\`, \`list_mem
|
|
|
1218
1775
|
if (agentTeamsSectionOpenCode) sections.push(agentTeamsSectionOpenCode);
|
|
1219
1776
|
const agentTeamsChatSectionOpenCode = buildAgentTeamsChatSection(config.mcps);
|
|
1220
1777
|
if (agentTeamsChatSectionOpenCode) sections.push(agentTeamsChatSectionOpenCode);
|
|
1778
|
+
const kanbanSectionOpenCode = buildKanbanSection(config.mcps);
|
|
1779
|
+
if (kanbanSectionOpenCode) sections.push(kanbanSectionOpenCode);
|
|
1221
1780
|
sections.push(`
|
|
1222
1781
|
## Troubleshooting and Debugging
|
|
1223
1782
|
|
|
@@ -1228,6 +1787,7 @@ It will:
|
|
|
1228
1787
|
3. Form and test hypotheses systematically
|
|
1229
1788
|
4. Identify the root cause
|
|
1230
1789
|
5. Propose a solution or call coding agents to implement the fix`);
|
|
1790
|
+
sections.push(...buildCoreBehaviorSections());
|
|
1231
1791
|
if (prompt?.sections) {
|
|
1232
1792
|
const reservedKeys = /* @__PURE__ */ new Set(["after_repositories", "after_pipeline", "after_delivery"]);
|
|
1233
1793
|
for (const [name, content] of Object.entries(prompt.sections)) {
|
|
@@ -1318,18 +1878,18 @@ If any validation agent leaves comments requiring fixes, call the relevant codin
|
|
|
1318
1878
|
return parts.join("\n");
|
|
1319
1879
|
}
|
|
1320
1880
|
async function generateOpenCode(config, hubDir) {
|
|
1321
|
-
const opencodeDir =
|
|
1322
|
-
await
|
|
1323
|
-
await
|
|
1324
|
-
await
|
|
1325
|
-
await
|
|
1326
|
-
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 });
|
|
1327
1887
|
const gitignoreLines = buildGitignoreLines(config);
|
|
1328
|
-
await writeManagedFile(
|
|
1888
|
+
await writeManagedFile(join4(hubDir, ".gitignore"), gitignoreLines);
|
|
1329
1889
|
console.log(chalk3.green(" Generated .gitignore"));
|
|
1330
1890
|
if (config.repos.length > 0) {
|
|
1331
1891
|
const ignoreContent = config.repos.map((r) => `!${r.name}`).join("\n") + "\n";
|
|
1332
|
-
await
|
|
1892
|
+
await writeFile4(join4(hubDir, ".ignore"), ignoreContent, "utf-8");
|
|
1333
1893
|
console.log(chalk3.green(" Generated .ignore"));
|
|
1334
1894
|
}
|
|
1335
1895
|
const orchestratorContent = buildOpenCodeOrchestratorRule(config);
|
|
@@ -1337,18 +1897,27 @@ async function generateOpenCode(config, hubDir) {
|
|
|
1337
1897
|
"Development orchestrator. Delegates specialized work to subagents following a structured pipeline: refinement, coding, review, QA, and delivery.",
|
|
1338
1898
|
orchestratorContent
|
|
1339
1899
|
);
|
|
1340
|
-
await
|
|
1900
|
+
await writeFile4(join4(opencodeDir, "agents", "orchestrator.md"), orchestratorAgent, "utf-8");
|
|
1341
1901
|
console.log(chalk3.green(" Generated .opencode/agents/orchestrator.md (primary agent)"));
|
|
1342
|
-
await rm(
|
|
1902
|
+
await rm(join4(opencodeDir, "rules", "orchestrator.md")).catch(() => {
|
|
1343
1903
|
});
|
|
1904
|
+
const skillsSectionOC = await buildSkillsSection(hubDir, config);
|
|
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");
|
|
1909
|
+
console.log(chalk3.green(" Generated AGENTS.md"));
|
|
1910
|
+
if (personaOC) {
|
|
1911
|
+
console.log(chalk3.green(` Applied persona: ${personaOC.name} (${personaOC.role})`));
|
|
1912
|
+
}
|
|
1344
1913
|
const hubSteeringDirOC = resolve2(hubDir, "steering");
|
|
1345
1914
|
try {
|
|
1346
1915
|
const steeringFiles = await readdir2(hubSteeringDirOC);
|
|
1347
1916
|
const mdFiles = steeringFiles.filter((f) => f.endsWith(".md"));
|
|
1348
1917
|
for (const file of mdFiles) {
|
|
1349
|
-
const raw = await
|
|
1918
|
+
const raw = await readFile4(join4(hubSteeringDirOC, file), "utf-8");
|
|
1350
1919
|
const content = stripFrontMatter(raw);
|
|
1351
|
-
await
|
|
1920
|
+
await writeFile4(join4(opencodeDir, "rules", file), content, "utf-8");
|
|
1352
1921
|
}
|
|
1353
1922
|
if (mdFiles.length > 0) {
|
|
1354
1923
|
console.log(chalk3.green(` Copied ${mdFiles.length} steering files to .opencode/rules/`));
|
|
@@ -1373,8 +1942,8 @@ async function generateOpenCode(config, hubDir) {
|
|
|
1373
1942
|
opencodeConfig.mcp = mcpConfig;
|
|
1374
1943
|
}
|
|
1375
1944
|
opencodeConfig.instructions = [".opencode/rules/*.md"];
|
|
1376
|
-
await
|
|
1377
|
-
|
|
1945
|
+
await writeFile4(
|
|
1946
|
+
join4(hubDir, "opencode.json"),
|
|
1378
1947
|
JSON.stringify(opencodeConfig, null, 2) + "\n",
|
|
1379
1948
|
"utf-8"
|
|
1380
1949
|
);
|
|
@@ -1385,10 +1954,10 @@ async function generateOpenCode(config, hubDir) {
|
|
|
1385
1954
|
const mdFiles = agentFiles.filter((f) => f.endsWith(".md"));
|
|
1386
1955
|
for (const file of mdFiles) {
|
|
1387
1956
|
if (file === "orchestrator.md") continue;
|
|
1388
|
-
const content = await
|
|
1957
|
+
const content = await readFile4(join4(agentsDir, file), "utf-8");
|
|
1389
1958
|
const agentName = file.replace(/\.md$/, "");
|
|
1390
1959
|
const converted = buildOpenCodeAgentMarkdown(agentName, content);
|
|
1391
|
-
await
|
|
1960
|
+
await writeFile4(join4(opencodeDir, "agents", file), converted, "utf-8");
|
|
1392
1961
|
}
|
|
1393
1962
|
console.log(chalk3.green(` Copied ${mdFiles.length} agents to .opencode/agents/`));
|
|
1394
1963
|
} catch {
|
|
@@ -1399,10 +1968,10 @@ async function generateOpenCode(config, hubDir) {
|
|
|
1399
1968
|
const skillFolders = await readdir2(skillsDir);
|
|
1400
1969
|
let count = 0;
|
|
1401
1970
|
for (const folder of skillFolders) {
|
|
1402
|
-
const skillFile =
|
|
1971
|
+
const skillFile = join4(skillsDir, folder, "SKILL.md");
|
|
1403
1972
|
try {
|
|
1404
|
-
await
|
|
1405
|
-
await cp(
|
|
1973
|
+
await readFile4(skillFile);
|
|
1974
|
+
await cp(join4(skillsDir, folder), join4(opencodeDir, "skills", folder), { recursive: true });
|
|
1406
1975
|
count++;
|
|
1407
1976
|
} catch {
|
|
1408
1977
|
}
|
|
@@ -1412,13 +1981,13 @@ async function generateOpenCode(config, hubDir) {
|
|
|
1412
1981
|
}
|
|
1413
1982
|
} catch {
|
|
1414
1983
|
}
|
|
1415
|
-
await fetchHubDocsSkill(
|
|
1416
|
-
await syncRemoteSources(config, hubDir,
|
|
1984
|
+
await fetchHubDocsSkill(join4(opencodeDir, "skills"));
|
|
1985
|
+
await syncRemoteSources(config, hubDir, join4(opencodeDir, "skills"), join4(opencodeDir, "rules"));
|
|
1417
1986
|
await generateEditorCommands(config, hubDir, opencodeDir, ".opencode/commands/");
|
|
1418
1987
|
if (config.hooks) {
|
|
1419
1988
|
const plugin = buildOpenCodeHooksPlugin(config.hooks);
|
|
1420
1989
|
if (plugin) {
|
|
1421
|
-
await
|
|
1990
|
+
await writeFile4(join4(opencodeDir, "plugins", "hub-hooks.js"), plugin, "utf-8");
|
|
1422
1991
|
console.log(chalk3.green(" Generated .opencode/plugins/hub-hooks.js"));
|
|
1423
1992
|
}
|
|
1424
1993
|
}
|
|
@@ -1528,6 +2097,8 @@ Available tools: \`search_memories\`, \`get_memory\`, \`add_memory\`, \`list_mem
|
|
|
1528
2097
|
if (agentTeamsSectionKiro) sections.push(agentTeamsSectionKiro);
|
|
1529
2098
|
const agentTeamsChatSectionKiro = buildAgentTeamsChatSection(config.mcps);
|
|
1530
2099
|
if (agentTeamsChatSectionKiro) sections.push(agentTeamsChatSectionKiro);
|
|
2100
|
+
const kanbanSectionKiro = buildKanbanSection(config.mcps);
|
|
2101
|
+
if (kanbanSectionKiro) sections.push(kanbanSectionKiro);
|
|
1531
2102
|
sections.push(`
|
|
1532
2103
|
## Troubleshooting and Debugging
|
|
1533
2104
|
|
|
@@ -1537,6 +2108,7 @@ For bug reports or unexpected behavior, follow the debugging process from the \`
|
|
|
1537
2108
|
3. Form and test hypotheses systematically
|
|
1538
2109
|
4. Identify the root cause
|
|
1539
2110
|
5. Propose and implement the fix`);
|
|
2111
|
+
sections.push(...buildCoreBehaviorSections());
|
|
1540
2112
|
if (prompt?.sections) {
|
|
1541
2113
|
const reservedKeys = /* @__PURE__ */ new Set(["after_repositories", "after_pipeline", "after_delivery"]);
|
|
1542
2114
|
for (const [name, content] of Object.entries(prompt.sections)) {
|
|
@@ -1720,6 +2292,8 @@ Available tools: \`search_memories\`, \`get_memory\`, \`add_memory\`, \`list_mem
|
|
|
1720
2292
|
if (agentTeamsSectionCursor) sections.push(agentTeamsSectionCursor);
|
|
1721
2293
|
const agentTeamsChatSectionCursor = buildAgentTeamsChatSection(config.mcps);
|
|
1722
2294
|
if (agentTeamsChatSectionCursor) sections.push(agentTeamsChatSectionCursor);
|
|
2295
|
+
const kanbanSectionCursor = buildKanbanSection(config.mcps);
|
|
2296
|
+
if (kanbanSectionCursor) sections.push(kanbanSectionCursor);
|
|
1723
2297
|
sections.push(`
|
|
1724
2298
|
## Troubleshooting and Debugging
|
|
1725
2299
|
|
|
@@ -1730,6 +2304,7 @@ It will:
|
|
|
1730
2304
|
3. Form and test hypotheses systematically
|
|
1731
2305
|
4. Identify the root cause
|
|
1732
2306
|
5. Propose a solution or call coding agents to implement the fix`);
|
|
2307
|
+
sections.push(...buildCoreBehaviorSections());
|
|
1733
2308
|
if (prompt?.sections) {
|
|
1734
2309
|
const reservedKeys = /* @__PURE__ */ new Set(["after_repositories", "after_pipeline", "after_delivery"]);
|
|
1735
2310
|
for (const [name, content] of Object.entries(prompt.sections)) {
|
|
@@ -1899,10 +2474,19 @@ function formatAction(action) {
|
|
|
1899
2474
|
return map[action] || action;
|
|
1900
2475
|
}
|
|
1901
2476
|
async function generateClaudeCode(config, hubDir) {
|
|
1902
|
-
const claudeDir =
|
|
1903
|
-
await
|
|
2477
|
+
const claudeDir = join4(hubDir, ".claude");
|
|
2478
|
+
await mkdir4(join4(claudeDir, "agents"), { recursive: true });
|
|
1904
2479
|
const orchestratorRule = buildOrchestratorRule(config);
|
|
1905
2480
|
const cleanedOrchestrator = orchestratorRule.replace(/^---[\s\S]*?---\n/m, "").trim();
|
|
2481
|
+
const skillsSectionClaude = await buildSkillsSection(hubDir, config);
|
|
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");
|
|
2486
|
+
console.log(chalk3.green(" Generated AGENTS.md"));
|
|
2487
|
+
if (personaClaude) {
|
|
2488
|
+
console.log(chalk3.green(` Applied persona: ${personaClaude.name} (${personaClaude.role})`));
|
|
2489
|
+
}
|
|
1906
2490
|
const claudeMdSections = [];
|
|
1907
2491
|
claudeMdSections.push(cleanedOrchestrator);
|
|
1908
2492
|
const agentsDir = resolve2(hubDir, "agents");
|
|
@@ -1910,7 +2494,7 @@ async function generateClaudeCode(config, hubDir) {
|
|
|
1910
2494
|
const agentFiles = await readdir2(agentsDir);
|
|
1911
2495
|
const mdFiles = agentFiles.filter((f) => f.endsWith(".md"));
|
|
1912
2496
|
for (const file of mdFiles) {
|
|
1913
|
-
await copyFile(
|
|
2497
|
+
await copyFile(join4(agentsDir, file), join4(claudeDir, "agents", file));
|
|
1914
2498
|
}
|
|
1915
2499
|
console.log(chalk3.green(` Copied ${mdFiles.length} agents to .claude/agents/`));
|
|
1916
2500
|
} catch {
|
|
@@ -1919,15 +2503,15 @@ async function generateClaudeCode(config, hubDir) {
|
|
|
1919
2503
|
const skillsDir = resolve2(hubDir, "skills");
|
|
1920
2504
|
try {
|
|
1921
2505
|
const skillFolders = await readdir2(skillsDir);
|
|
1922
|
-
const claudeSkillsDir =
|
|
1923
|
-
await
|
|
2506
|
+
const claudeSkillsDir = join4(claudeDir, "skills");
|
|
2507
|
+
await mkdir4(claudeSkillsDir, { recursive: true });
|
|
1924
2508
|
let count = 0;
|
|
1925
2509
|
for (const folder of skillFolders) {
|
|
1926
|
-
const skillFile =
|
|
2510
|
+
const skillFile = join4(skillsDir, folder, "SKILL.md");
|
|
1927
2511
|
try {
|
|
1928
|
-
await
|
|
1929
|
-
const srcDir =
|
|
1930
|
-
const targetDir =
|
|
2512
|
+
await readFile4(skillFile);
|
|
2513
|
+
const srcDir = join4(skillsDir, folder);
|
|
2514
|
+
const targetDir = join4(claudeSkillsDir, folder);
|
|
1931
2515
|
await cp(srcDir, targetDir, { recursive: true });
|
|
1932
2516
|
count++;
|
|
1933
2517
|
} catch {
|
|
@@ -1938,16 +2522,16 @@ async function generateClaudeCode(config, hubDir) {
|
|
|
1938
2522
|
}
|
|
1939
2523
|
} catch {
|
|
1940
2524
|
}
|
|
1941
|
-
const claudeSkillsDirForDocs =
|
|
1942
|
-
await
|
|
2525
|
+
const claudeSkillsDirForDocs = join4(claudeDir, "skills");
|
|
2526
|
+
await mkdir4(claudeSkillsDirForDocs, { recursive: true });
|
|
1943
2527
|
await fetchHubDocsSkill(claudeSkillsDirForDocs);
|
|
1944
|
-
await syncRemoteSources(config, hubDir,
|
|
2528
|
+
await syncRemoteSources(config, hubDir, join4(claudeDir, "skills"), join4(claudeDir, "steering"));
|
|
1945
2529
|
const hubSteeringDirClaude = resolve2(hubDir, "steering");
|
|
1946
2530
|
try {
|
|
1947
2531
|
const steeringFiles = await readdir2(hubSteeringDirClaude);
|
|
1948
2532
|
const mdFiles = steeringFiles.filter((f) => f.endsWith(".md"));
|
|
1949
2533
|
for (const file of mdFiles) {
|
|
1950
|
-
const raw = await
|
|
2534
|
+
const raw = await readFile4(join4(hubSteeringDirClaude, file), "utf-8");
|
|
1951
2535
|
const content = stripFrontMatter(raw).trim();
|
|
1952
2536
|
if (content) {
|
|
1953
2537
|
claudeMdSections.push(content);
|
|
@@ -1958,7 +2542,7 @@ async function generateClaudeCode(config, hubDir) {
|
|
|
1958
2542
|
}
|
|
1959
2543
|
} catch {
|
|
1960
2544
|
}
|
|
1961
|
-
await
|
|
2545
|
+
await writeFile4(join4(hubDir, "CLAUDE.md"), claudeMdSections.join("\n\n"), "utf-8");
|
|
1962
2546
|
console.log(chalk3.green(" Generated CLAUDE.md"));
|
|
1963
2547
|
if (config.mcps?.length) {
|
|
1964
2548
|
const mcpJson = {};
|
|
@@ -1971,8 +2555,8 @@ async function generateClaudeCode(config, hubDir) {
|
|
|
1971
2555
|
mcpJson[mcp.name] = buildClaudeCodeMcpEntry(mcp);
|
|
1972
2556
|
}
|
|
1973
2557
|
}
|
|
1974
|
-
await
|
|
1975
|
-
|
|
2558
|
+
await writeFile4(
|
|
2559
|
+
join4(hubDir, ".mcp.json"),
|
|
1976
2560
|
JSON.stringify({ mcpServers: mcpJson }, null, 2) + "\n",
|
|
1977
2561
|
"utf-8"
|
|
1978
2562
|
);
|
|
@@ -2014,22 +2598,22 @@ async function generateClaudeCode(config, hubDir) {
|
|
|
2014
2598
|
claudeSettings.hooks = claudeHooks;
|
|
2015
2599
|
}
|
|
2016
2600
|
}
|
|
2017
|
-
await
|
|
2018
|
-
|
|
2601
|
+
await writeFile4(
|
|
2602
|
+
join4(claudeDir, "settings.json"),
|
|
2019
2603
|
JSON.stringify(claudeSettings, null, 2) + "\n",
|
|
2020
2604
|
"utf-8"
|
|
2021
2605
|
);
|
|
2022
2606
|
console.log(chalk3.green(" Generated .claude/settings.json"));
|
|
2023
2607
|
const gitignoreLines = buildGitignoreLines(config);
|
|
2024
|
-
await writeManagedFile(
|
|
2608
|
+
await writeManagedFile(join4(hubDir, ".gitignore"), gitignoreLines);
|
|
2025
2609
|
console.log(chalk3.green(" Generated .gitignore"));
|
|
2026
2610
|
}
|
|
2027
2611
|
async function generateKiro(config, hubDir) {
|
|
2028
|
-
const kiroDir =
|
|
2029
|
-
const steeringDir =
|
|
2030
|
-
const settingsDir =
|
|
2031
|
-
await
|
|
2032
|
-
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 });
|
|
2033
2617
|
let mode = await getKiroMode(hubDir);
|
|
2034
2618
|
if (!mode) {
|
|
2035
2619
|
const { kiroMode } = await inquirer.prompt([
|
|
@@ -2050,26 +2634,32 @@ async function generateKiro(config, hubDir) {
|
|
|
2050
2634
|
console.log(chalk3.dim(` Using saved Kiro mode: ${mode}`));
|
|
2051
2635
|
}
|
|
2052
2636
|
const gitignoreLines = buildGitignoreLines(config);
|
|
2053
|
-
await writeManagedFile(
|
|
2637
|
+
await writeManagedFile(join4(hubDir, ".gitignore"), gitignoreLines);
|
|
2054
2638
|
console.log(chalk3.green(" Generated .gitignore"));
|
|
2055
2639
|
const kiroRule = buildKiroOrchestratorRule(config);
|
|
2056
|
-
const
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2640
|
+
const skillsSection = await buildSkillsSection(hubDir, config);
|
|
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");
|
|
2060
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
|
+
});
|
|
2061
2651
|
const hubSteeringDir = resolve2(hubDir, "steering");
|
|
2062
2652
|
try {
|
|
2063
2653
|
const steeringFiles = await readdir2(hubSteeringDir);
|
|
2064
2654
|
const mdFiles = steeringFiles.filter((f) => f.endsWith(".md"));
|
|
2065
2655
|
for (const file of mdFiles) {
|
|
2066
|
-
const raw = await
|
|
2656
|
+
const raw = await readFile4(join4(hubSteeringDir, file), "utf-8");
|
|
2067
2657
|
const content = stripFrontMatter(raw);
|
|
2068
|
-
const destPath =
|
|
2658
|
+
const destPath = join4(steeringDir, file);
|
|
2069
2659
|
let inclusion = "always";
|
|
2070
2660
|
let meta;
|
|
2071
|
-
if (
|
|
2072
|
-
const existingContent = await
|
|
2661
|
+
if (existsSync3(destPath)) {
|
|
2662
|
+
const existingContent = await readFile4(destPath, "utf-8");
|
|
2073
2663
|
const existingFm = parseFrontMatter(existingContent);
|
|
2074
2664
|
if (existingFm) {
|
|
2075
2665
|
if (existingFm.inclusion === "auto" || existingFm.inclusion === "manual" || existingFm.inclusion === "fileMatch") {
|
|
@@ -2094,7 +2684,7 @@ async function generateKiro(config, hubDir) {
|
|
|
2094
2684
|
}
|
|
2095
2685
|
}
|
|
2096
2686
|
const kiroSteering = buildKiroSteeringContent(content, inclusion, meta);
|
|
2097
|
-
await
|
|
2687
|
+
await writeFile4(destPath, kiroSteering, "utf-8");
|
|
2098
2688
|
}
|
|
2099
2689
|
if (mdFiles.length > 0) {
|
|
2100
2690
|
console.log(chalk3.green(` Copied ${mdFiles.length} steering files to .kiro/steering/`));
|
|
@@ -2103,14 +2693,17 @@ async function generateKiro(config, hubDir) {
|
|
|
2103
2693
|
}
|
|
2104
2694
|
const agentsDir = resolve2(hubDir, "agents");
|
|
2105
2695
|
try {
|
|
2106
|
-
const kiroAgentsDir =
|
|
2107
|
-
await
|
|
2696
|
+
const kiroAgentsDir = join4(kiroDir, "agents");
|
|
2697
|
+
await mkdir4(kiroAgentsDir, { recursive: true });
|
|
2108
2698
|
const agentFiles = await readdir2(agentsDir);
|
|
2109
2699
|
const mdFiles = agentFiles.filter((f) => f.endsWith(".md"));
|
|
2110
2700
|
for (const file of mdFiles) {
|
|
2111
|
-
const agentContent = await
|
|
2112
|
-
const
|
|
2113
|
-
|
|
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");
|
|
2114
2707
|
}
|
|
2115
2708
|
console.log(chalk3.green(` Copied ${mdFiles.length} agents to .kiro/agents/`));
|
|
2116
2709
|
} catch {
|
|
@@ -2119,15 +2712,15 @@ async function generateKiro(config, hubDir) {
|
|
|
2119
2712
|
const skillsDir = resolve2(hubDir, "skills");
|
|
2120
2713
|
try {
|
|
2121
2714
|
const skillFolders = await readdir2(skillsDir);
|
|
2122
|
-
const kiroSkillsDir =
|
|
2123
|
-
await
|
|
2715
|
+
const kiroSkillsDir = join4(kiroDir, "skills");
|
|
2716
|
+
await mkdir4(kiroSkillsDir, { recursive: true });
|
|
2124
2717
|
let count = 0;
|
|
2125
2718
|
for (const folder of skillFolders) {
|
|
2126
|
-
const skillFile =
|
|
2719
|
+
const skillFile = join4(skillsDir, folder, "SKILL.md");
|
|
2127
2720
|
try {
|
|
2128
|
-
await
|
|
2129
|
-
const srcDir =
|
|
2130
|
-
const targetDir =
|
|
2721
|
+
await readFile4(skillFile);
|
|
2722
|
+
const srcDir = join4(skillsDir, folder);
|
|
2723
|
+
const targetDir = join4(kiroSkillsDir, folder);
|
|
2131
2724
|
await cp(srcDir, targetDir, { recursive: true });
|
|
2132
2725
|
count++;
|
|
2133
2726
|
} catch {
|
|
@@ -2138,10 +2731,10 @@ async function generateKiro(config, hubDir) {
|
|
|
2138
2731
|
}
|
|
2139
2732
|
} catch {
|
|
2140
2733
|
}
|
|
2141
|
-
const kiroSkillsDirForDocs =
|
|
2142
|
-
await
|
|
2734
|
+
const kiroSkillsDirForDocs = join4(kiroDir, "skills");
|
|
2735
|
+
await mkdir4(kiroSkillsDirForDocs, { recursive: true });
|
|
2143
2736
|
await fetchHubDocsSkill(kiroSkillsDirForDocs);
|
|
2144
|
-
await syncRemoteSources(config, hubDir,
|
|
2737
|
+
await syncRemoteSources(config, hubDir, join4(kiroDir, "skills"), steeringDir);
|
|
2145
2738
|
if (config.mcps?.length) {
|
|
2146
2739
|
const mcpConfig = {};
|
|
2147
2740
|
const upstreamSet = getUpstreamNames(config.mcps);
|
|
@@ -2154,10 +2747,14 @@ async function generateKiro(config, hubDir) {
|
|
|
2154
2747
|
mcpConfig[mcp.name] = buildKiroMcpEntry(mcp, mode);
|
|
2155
2748
|
}
|
|
2156
2749
|
}
|
|
2157
|
-
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");
|
|
2158
2755
|
const disabledState = await readExistingMcpDisabledState(mcpJsonPath);
|
|
2159
2756
|
applyDisabledState(mcpConfig, disabledState);
|
|
2160
|
-
await
|
|
2757
|
+
await writeFile4(
|
|
2161
2758
|
mcpJsonPath,
|
|
2162
2759
|
JSON.stringify({ mcpServers: mcpConfig }, null, 2) + "\n",
|
|
2163
2760
|
"utf-8"
|
|
@@ -2184,13 +2781,13 @@ async function generateKiro(config, hubDir) {
|
|
|
2184
2781
|
await generateVSCodeSettings(config, hubDir);
|
|
2185
2782
|
}
|
|
2186
2783
|
async function generateVSCodeSettings(config, hubDir) {
|
|
2187
|
-
const vscodeDir =
|
|
2188
|
-
await
|
|
2189
|
-
const settingsPath =
|
|
2784
|
+
const vscodeDir = join4(hubDir, ".vscode");
|
|
2785
|
+
await mkdir4(vscodeDir, { recursive: true });
|
|
2786
|
+
const settingsPath = join4(vscodeDir, "settings.json");
|
|
2190
2787
|
let existing = {};
|
|
2191
|
-
if (
|
|
2788
|
+
if (existsSync3(settingsPath)) {
|
|
2192
2789
|
try {
|
|
2193
|
-
const raw = await
|
|
2790
|
+
const raw = await readFile4(settingsPath, "utf-8");
|
|
2194
2791
|
existing = JSON.parse(raw);
|
|
2195
2792
|
} catch {
|
|
2196
2793
|
existing = {};
|
|
@@ -2202,14 +2799,14 @@ async function generateVSCodeSettings(config, hubDir) {
|
|
|
2202
2799
|
"git.detectSubmodulesLimit": Math.max(config.repos.length * 2, 20)
|
|
2203
2800
|
};
|
|
2204
2801
|
const merged = { ...existing, ...managed };
|
|
2205
|
-
await
|
|
2802
|
+
await writeFile4(settingsPath, JSON.stringify(merged, null, 2) + "\n", "utf-8");
|
|
2206
2803
|
console.log(chalk3.green(" Generated .vscode/settings.json (git multi-repo detection)"));
|
|
2207
2804
|
const workspaceFile = `${config.name}.code-workspace`;
|
|
2208
|
-
const workspacePath =
|
|
2805
|
+
const workspacePath = join4(hubDir, workspaceFile);
|
|
2209
2806
|
let existingWorkspace = {};
|
|
2210
|
-
if (
|
|
2807
|
+
if (existsSync3(workspacePath)) {
|
|
2211
2808
|
try {
|
|
2212
|
-
const raw = await
|
|
2809
|
+
const raw = await readFile4(workspacePath, "utf-8");
|
|
2213
2810
|
existingWorkspace = JSON.parse(raw);
|
|
2214
2811
|
} catch {
|
|
2215
2812
|
existingWorkspace = {};
|
|
@@ -2219,7 +2816,7 @@ async function generateVSCodeSettings(config, hubDir) {
|
|
|
2219
2816
|
const existing2 = files.find((f) => f.endsWith(".code-workspace"));
|
|
2220
2817
|
if (existing2) {
|
|
2221
2818
|
try {
|
|
2222
|
-
const raw = await
|
|
2819
|
+
const raw = await readFile4(join4(hubDir, existing2), "utf-8");
|
|
2223
2820
|
existingWorkspace = JSON.parse(raw);
|
|
2224
2821
|
} catch {
|
|
2225
2822
|
existingWorkspace = {};
|
|
@@ -2255,7 +2852,7 @@ async function generateVSCodeSettings(config, hubDir) {
|
|
|
2255
2852
|
folders,
|
|
2256
2853
|
settings: existingWorkspace.settings || {}
|
|
2257
2854
|
};
|
|
2258
|
-
await
|
|
2855
|
+
await writeFile4(workspacePath, JSON.stringify(workspace, null, " ") + "\n", "utf-8");
|
|
2259
2856
|
console.log(chalk3.green(` Generated ${workspaceFile}`));
|
|
2260
2857
|
}
|
|
2261
2858
|
function extractEnvVarsByMcp(mcps) {
|
|
@@ -2302,7 +2899,7 @@ async function generateEnvExample(config, hubDir) {
|
|
|
2302
2899
|
totalVars++;
|
|
2303
2900
|
}
|
|
2304
2901
|
if (totalVars === 0) return;
|
|
2305
|
-
await
|
|
2902
|
+
await writeFile4(join4(hubDir, ".env.example"), lines.join("\n") + "\n", "utf-8");
|
|
2306
2903
|
console.log(chalk3.green(` Generated .env.example (${totalVars} vars)`));
|
|
2307
2904
|
}
|
|
2308
2905
|
function buildGitignoreLines(config) {
|
|
@@ -2392,7 +2989,7 @@ async function resolveEditor(opts) {
|
|
|
2392
2989
|
]);
|
|
2393
2990
|
return editor;
|
|
2394
2991
|
}
|
|
2395
|
-
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) => {
|
|
2396
2993
|
const hubDir = process.cwd();
|
|
2397
2994
|
if (opts.check) {
|
|
2398
2995
|
const result = await checkOutdated(hubDir);
|
|
@@ -2457,6 +3054,10 @@ Generating ${generator.name} configuration
|
|
|
2457
3054
|
});
|
|
2458
3055
|
|
|
2459
3056
|
export {
|
|
3057
|
+
colors,
|
|
3058
|
+
symbols,
|
|
3059
|
+
horizontalLine,
|
|
3060
|
+
personaCommand,
|
|
2460
3061
|
generators,
|
|
2461
3062
|
generateCommand,
|
|
2462
3063
|
checkAndAutoRegenerate
|