@arvoretech/hub 0.6.3 → 0.7.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -43,6 +43,15 @@ async function getSavedEditor(hubDir) {
|
|
|
43
43
|
const cache = await readCache(hubDir);
|
|
44
44
|
return cache.editor;
|
|
45
45
|
}
|
|
46
|
+
async function getKiroMode(hubDir) {
|
|
47
|
+
const cache = await readCache(hubDir);
|
|
48
|
+
return cache.kiroMode;
|
|
49
|
+
}
|
|
50
|
+
async function saveKiroMode(hubDir, mode) {
|
|
51
|
+
const cache = await readCache(hubDir);
|
|
52
|
+
cache.kiroMode = mode;
|
|
53
|
+
await writeCache(hubDir, cache);
|
|
54
|
+
}
|
|
46
55
|
async function collectFileHashes(dir, extensions) {
|
|
47
56
|
if (!existsSync(dir)) return [];
|
|
48
57
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
@@ -112,7 +121,7 @@ async function checkAndAutoRegenerate(hubDir) {
|
|
|
112
121
|
return;
|
|
113
122
|
}
|
|
114
123
|
console.log(chalk.yellow("\n Detected outdated configs, auto-regenerating..."));
|
|
115
|
-
const { generators: generators2 } = await import("./generate-
|
|
124
|
+
const { generators: generators2 } = await import("./generate-75QAGHLE.js");
|
|
116
125
|
const generator = generators2[result.editor];
|
|
117
126
|
if (!generator) {
|
|
118
127
|
console.log(chalk.red(` Unknown editor '${result.editor}' in cache. Run 'hub generate' manually.`));
|
|
@@ -129,6 +138,35 @@ async function checkAndAutoRegenerate(hubDir) {
|
|
|
129
138
|
}
|
|
130
139
|
|
|
131
140
|
// src/commands/generate.ts
|
|
141
|
+
var HUB_DOCS_URL = "https://hub.arvore.com.br/llms-full.txt";
|
|
142
|
+
function stripFrontMatter(content) {
|
|
143
|
+
const match = content.match(/^---\n[\s\S]*?\n---\n*/);
|
|
144
|
+
if (match) return content.slice(match[0].length);
|
|
145
|
+
return content;
|
|
146
|
+
}
|
|
147
|
+
async function fetchHubDocsSkill(skillsDir) {
|
|
148
|
+
try {
|
|
149
|
+
const res = await fetch(HUB_DOCS_URL);
|
|
150
|
+
if (!res.ok) {
|
|
151
|
+
console.log(chalk2.yellow(` Could not fetch hub docs (${res.status}), skipping hub-docs skill`));
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const content = await res.text();
|
|
155
|
+
const hubSkillDir = join3(skillsDir, "hub-docs");
|
|
156
|
+
await mkdir2(hubSkillDir, { recursive: true });
|
|
157
|
+
const skillContent = `---
|
|
158
|
+
name: hub-docs
|
|
159
|
+
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.
|
|
160
|
+
triggers: [hub, rhm, hub.yaml, generate, scan, setup, orchestrator, multi-repo, workspace]
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
${content}`;
|
|
164
|
+
await writeFile2(join3(hubSkillDir, "SKILL.md"), skillContent, "utf-8");
|
|
165
|
+
console.log(chalk2.green(" Fetched hub-docs skill from hub.arvore.com.br"));
|
|
166
|
+
} catch {
|
|
167
|
+
console.log(chalk2.yellow(` Could not fetch hub docs, skipping hub-docs skill`));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
132
170
|
var HUB_MARKER_START = "# >>> hub-managed (do not edit this section)";
|
|
133
171
|
var HUB_MARKER_END = "# <<< hub-managed";
|
|
134
172
|
var HOOK_EVENT_MAP = {
|
|
@@ -269,8 +307,14 @@ async function generateCursor(config, hubDir) {
|
|
|
269
307
|
console.log(chalk2.green(" Generated .cursorignore"));
|
|
270
308
|
if (config.mcps?.length) {
|
|
271
309
|
const mcpConfig = {};
|
|
310
|
+
const upstreamSet = getUpstreamNames(config.mcps);
|
|
272
311
|
for (const mcp of config.mcps) {
|
|
273
|
-
|
|
312
|
+
if (upstreamSet.has(mcp.name)) continue;
|
|
313
|
+
if (mcp.upstreams?.length) {
|
|
314
|
+
mcpConfig[mcp.name] = buildProxyMcpEntry(mcp, config.mcps, buildCursorMcpEntry);
|
|
315
|
+
} else {
|
|
316
|
+
mcpConfig[mcp.name] = buildCursorMcpEntry(mcp);
|
|
317
|
+
}
|
|
274
318
|
}
|
|
275
319
|
await writeFile2(
|
|
276
320
|
join3(cursorDir, "mcp.json"),
|
|
@@ -282,6 +326,27 @@ async function generateCursor(config, hubDir) {
|
|
|
282
326
|
const orchestratorRule = buildOrchestratorRule(config);
|
|
283
327
|
await writeFile2(join3(cursorDir, "rules", "orchestrator.mdc"), orchestratorRule, "utf-8");
|
|
284
328
|
console.log(chalk2.green(" Generated .cursor/rules/orchestrator.mdc"));
|
|
329
|
+
const hubSteeringDirCursor = resolve(hubDir, "steering");
|
|
330
|
+
try {
|
|
331
|
+
const steeringFiles = await readdir2(hubSteeringDirCursor);
|
|
332
|
+
const mdFiles = steeringFiles.filter((f) => f.endsWith(".md"));
|
|
333
|
+
for (const file of mdFiles) {
|
|
334
|
+
const raw = await readFile3(join3(hubSteeringDirCursor, file), "utf-8");
|
|
335
|
+
const content = stripFrontMatter(raw);
|
|
336
|
+
const mdcName = file.replace(/\.md$/, ".mdc");
|
|
337
|
+
const mdcContent = `---
|
|
338
|
+
description: "${file.replace(/\.md$/, "")}"
|
|
339
|
+
alwaysApply: true
|
|
340
|
+
---
|
|
341
|
+
|
|
342
|
+
${content}`;
|
|
343
|
+
await writeFile2(join3(cursorDir, "rules", mdcName), mdcContent, "utf-8");
|
|
344
|
+
}
|
|
345
|
+
if (mdFiles.length > 0) {
|
|
346
|
+
console.log(chalk2.green(` Copied ${mdFiles.length} steering files to .cursor/rules/`));
|
|
347
|
+
}
|
|
348
|
+
} catch {
|
|
349
|
+
}
|
|
285
350
|
const agentsDir = resolve(hubDir, "agents");
|
|
286
351
|
try {
|
|
287
352
|
const agentFiles = await readdir2(agentsDir);
|
|
@@ -315,6 +380,9 @@ async function generateCursor(config, hubDir) {
|
|
|
315
380
|
}
|
|
316
381
|
} catch {
|
|
317
382
|
}
|
|
383
|
+
const cursorSkillsDirForDocs = join3(cursorDir, "skills");
|
|
384
|
+
await mkdir2(cursorSkillsDirForDocs, { recursive: true });
|
|
385
|
+
await fetchHubDocsSkill(cursorSkillsDirForDocs);
|
|
318
386
|
if (config.hooks) {
|
|
319
387
|
const cursorHooks = buildCursorHooks(config.hooks);
|
|
320
388
|
if (cursorHooks) {
|
|
@@ -329,6 +397,62 @@ async function generateCursor(config, hubDir) {
|
|
|
329
397
|
await generateEditorCommands(config, hubDir, cursorDir, ".cursor/commands/");
|
|
330
398
|
await generateVSCodeSettings(config, hubDir);
|
|
331
399
|
}
|
|
400
|
+
function buildProxyUpstreams(proxyMcp, allMcps) {
|
|
401
|
+
const upstreamNames = new Set(proxyMcp.upstreams || []);
|
|
402
|
+
const upstreamEntries = [];
|
|
403
|
+
const collectedEnv = {};
|
|
404
|
+
for (const mcp of allMcps) {
|
|
405
|
+
if (!upstreamNames.has(mcp.name)) continue;
|
|
406
|
+
if (mcp.url || mcp.image) continue;
|
|
407
|
+
const entry = {
|
|
408
|
+
name: mcp.name,
|
|
409
|
+
command: "npx",
|
|
410
|
+
args: ["-y", mcp.package]
|
|
411
|
+
};
|
|
412
|
+
if (mcp.env) {
|
|
413
|
+
entry.env = {};
|
|
414
|
+
for (const [key, value] of Object.entries(mcp.env)) {
|
|
415
|
+
const envRef = value.match(/^\$\{(?:env:)?(\w+)\}$/);
|
|
416
|
+
if (envRef) {
|
|
417
|
+
entry.env[key] = `\${${envRef[1]}}`;
|
|
418
|
+
collectedEnv[envRef[1]] = value;
|
|
419
|
+
} else {
|
|
420
|
+
entry.env[key] = value;
|
|
421
|
+
collectedEnv[key] = value;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
upstreamEntries.push(entry);
|
|
426
|
+
}
|
|
427
|
+
if (proxyMcp.env) {
|
|
428
|
+
for (const [key, value] of Object.entries(proxyMcp.env)) {
|
|
429
|
+
collectedEnv[key] = value;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return {
|
|
433
|
+
upstreamsJson: JSON.stringify(upstreamEntries),
|
|
434
|
+
collectedEnv
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
function buildProxyMcpEntry(proxyMcp, allMcps, buildEntry) {
|
|
438
|
+
const { upstreamsJson, collectedEnv } = buildProxyUpstreams(proxyMcp, allMcps);
|
|
439
|
+
const env = {
|
|
440
|
+
MCP_PROXY_UPSTREAMS: upstreamsJson,
|
|
441
|
+
...collectedEnv
|
|
442
|
+
};
|
|
443
|
+
return buildEntry({ ...proxyMcp, env });
|
|
444
|
+
}
|
|
445
|
+
function getUpstreamNames(mcps) {
|
|
446
|
+
const names = /* @__PURE__ */ new Set();
|
|
447
|
+
for (const mcp of mcps) {
|
|
448
|
+
if (mcp.upstreams) {
|
|
449
|
+
for (const name of mcp.upstreams) {
|
|
450
|
+
names.add(name);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return names;
|
|
455
|
+
}
|
|
332
456
|
function buildCursorMcpEntry(mcp) {
|
|
333
457
|
if (mcp.url) {
|
|
334
458
|
return { url: mcp.url, ...mcp.env && { env: mcp.env } };
|
|
@@ -369,14 +493,22 @@ function buildClaudeCodeMcpEntry(mcp) {
|
|
|
369
493
|
...mcp.env && { env: mcp.env }
|
|
370
494
|
};
|
|
371
495
|
}
|
|
372
|
-
function
|
|
496
|
+
function stripEnvPrefix(env) {
|
|
497
|
+
const result = {};
|
|
498
|
+
for (const [key, value] of Object.entries(env)) {
|
|
499
|
+
result[key] = value.replace(/\$\{env:(\w+)\}/g, "${$1}");
|
|
500
|
+
}
|
|
501
|
+
return result;
|
|
502
|
+
}
|
|
503
|
+
function buildKiroMcpEntry(mcp, mode = "editor") {
|
|
504
|
+
const env = mcp.env ? mode === "editor" ? stripEnvPrefix(mcp.env) : mcp.env : void 0;
|
|
373
505
|
if (mcp.url) {
|
|
374
|
-
return { url: mcp.url, ...
|
|
506
|
+
return { url: mcp.url, ...env && { env } };
|
|
375
507
|
}
|
|
376
508
|
if (mcp.image) {
|
|
377
509
|
const args = ["run", "-i", "--rm"];
|
|
378
|
-
if (
|
|
379
|
-
for (const [key, value] of Object.entries(
|
|
510
|
+
if (env) {
|
|
511
|
+
for (const [key, value] of Object.entries(env)) {
|
|
380
512
|
args.push("-e", `${key}=${value}`);
|
|
381
513
|
}
|
|
382
514
|
}
|
|
@@ -386,9 +518,41 @@ function buildKiroMcpEntry(mcp) {
|
|
|
386
518
|
return {
|
|
387
519
|
command: "npx",
|
|
388
520
|
args: ["-y", mcp.package],
|
|
389
|
-
...
|
|
521
|
+
...env && { env }
|
|
390
522
|
};
|
|
391
523
|
}
|
|
524
|
+
function buildKiroAgentContent(rawContent) {
|
|
525
|
+
const fmMatch = rawContent.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
526
|
+
if (!fmMatch) {
|
|
527
|
+
return `---
|
|
528
|
+
name: agent
|
|
529
|
+
tools: ["@builtin"]
|
|
530
|
+
---
|
|
531
|
+
|
|
532
|
+
${rawContent}`;
|
|
533
|
+
}
|
|
534
|
+
const fmBlock = fmMatch[1];
|
|
535
|
+
const body = fmMatch[2];
|
|
536
|
+
const attrs = {};
|
|
537
|
+
for (const line of fmBlock.split("\n")) {
|
|
538
|
+
const match = line.match(/^(\w+):\s*(.+)$/);
|
|
539
|
+
if (match) attrs[match[1]] = match[2].trim();
|
|
540
|
+
}
|
|
541
|
+
const lines = ["---"];
|
|
542
|
+
if (attrs.name) lines.push(`name: ${attrs.name}`);
|
|
543
|
+
if (attrs.description) lines.push(`description: ${attrs.description}`);
|
|
544
|
+
if (attrs.tools) {
|
|
545
|
+
lines.push(`tools: ${attrs.tools}`);
|
|
546
|
+
} else {
|
|
547
|
+
lines.push(`tools: ["@builtin"]`);
|
|
548
|
+
}
|
|
549
|
+
if (attrs.model && attrs.model !== "inherit") {
|
|
550
|
+
lines.push(`model: ${attrs.model}`);
|
|
551
|
+
}
|
|
552
|
+
lines.push("---");
|
|
553
|
+
return `${lines.join("\n")}
|
|
554
|
+
${body}`;
|
|
555
|
+
}
|
|
392
556
|
function buildOpenCodeMcpEntry(mcp) {
|
|
393
557
|
if (mcp.url) {
|
|
394
558
|
return { type: "remote", url: mcp.url };
|
|
@@ -454,6 +618,43 @@ tools:
|
|
|
454
618
|
${body.trim()}
|
|
455
619
|
`;
|
|
456
620
|
}
|
|
621
|
+
function buildMcpToolsSection(mcps) {
|
|
622
|
+
if (!mcps || mcps.length === 0) return "";
|
|
623
|
+
const proxyMcp = mcps.find((m) => m.upstreams && m.upstreams.length > 0);
|
|
624
|
+
const upstreamNames = getUpstreamNames(mcps);
|
|
625
|
+
const directMcps = mcps.filter((m) => !m.upstreams && !upstreamNames.has(m.name));
|
|
626
|
+
if (!proxyMcp && directMcps.length === 0) return "";
|
|
627
|
+
const lines = [];
|
|
628
|
+
lines.push(`
|
|
629
|
+
## MCP Tools (Model Context Protocol)
|
|
630
|
+
|
|
631
|
+
This workspace has multiple MCP servers available.`);
|
|
632
|
+
if (proxyMcp) {
|
|
633
|
+
lines.push(`
|
|
634
|
+
Some MCPs are aggregated behind a proxy (\`${proxyMcp.name}\`). Their tools are NOT directly visible \u2014 you must use \`mcp_search\` to discover available tools and \`mcp_call\` to execute them.
|
|
635
|
+
|
|
636
|
+
**How to use proxied tools:**
|
|
637
|
+
1. \`mcp_search({ query: "your search term" })\` \u2014 find tools by name or description
|
|
638
|
+
2. \`mcp_call({ ref: "tool-ref-from-search", args: { ... } })\` \u2014 execute the tool
|
|
639
|
+
|
|
640
|
+
**MCPs available via proxy:**`);
|
|
641
|
+
for (const name of proxyMcp.upstreams) {
|
|
642
|
+
lines.push(`- \`${name}\``);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
if (directMcps.length > 0) {
|
|
646
|
+
lines.push(`
|
|
647
|
+
**MCPs available directly:**`);
|
|
648
|
+
for (const mcp of directMcps) {
|
|
649
|
+
lines.push(`- \`${mcp.name}\``);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
if (proxyMcp) {
|
|
653
|
+
lines.push(`
|
|
654
|
+
> When you need a capability and are unsure which tool to use, always try \`mcp_search\` first with relevant keywords. The proxy aggregates tools from all upstream MCPs.`);
|
|
655
|
+
}
|
|
656
|
+
return lines.join("\n");
|
|
657
|
+
}
|
|
457
658
|
function buildOpenCodeOrchestratorRule(config) {
|
|
458
659
|
const taskFolder = config.workflow?.task_folder || "./tasks/<TASK_ID>/";
|
|
459
660
|
const steps = config.workflow?.pipeline || [];
|
|
@@ -525,6 +726,10 @@ ${prompt.sections.after_pipeline.trim()}`);
|
|
|
525
726
|
sections.push(`
|
|
526
727
|
${prompt.sections.after_delivery.trim()}`);
|
|
527
728
|
}
|
|
729
|
+
const mcpToolsSectionOpenCode = buildMcpToolsSection(config.mcps);
|
|
730
|
+
if (mcpToolsSectionOpenCode) {
|
|
731
|
+
sections.push(mcpToolsSectionOpenCode);
|
|
732
|
+
}
|
|
528
733
|
if (config.memory) {
|
|
529
734
|
sections.push(`
|
|
530
735
|
## Team Memory
|
|
@@ -649,13 +854,33 @@ async function generateOpenCode(config, hubDir) {
|
|
|
649
854
|
const orchestratorRule = buildOpenCodeOrchestratorRule(config);
|
|
650
855
|
await writeFile2(join3(opencodeDir, "rules", "orchestrator.md"), orchestratorRule + "\n", "utf-8");
|
|
651
856
|
console.log(chalk2.green(" Generated .opencode/rules/orchestrator.md"));
|
|
857
|
+
const hubSteeringDirOC = resolve(hubDir, "steering");
|
|
858
|
+
try {
|
|
859
|
+
const steeringFiles = await readdir2(hubSteeringDirOC);
|
|
860
|
+
const mdFiles = steeringFiles.filter((f) => f.endsWith(".md"));
|
|
861
|
+
for (const file of mdFiles) {
|
|
862
|
+
const raw = await readFile3(join3(hubSteeringDirOC, file), "utf-8");
|
|
863
|
+
const content = stripFrontMatter(raw);
|
|
864
|
+
await writeFile2(join3(opencodeDir, "rules", file), content, "utf-8");
|
|
865
|
+
}
|
|
866
|
+
if (mdFiles.length > 0) {
|
|
867
|
+
console.log(chalk2.green(` Copied ${mdFiles.length} steering files to .opencode/rules/`));
|
|
868
|
+
}
|
|
869
|
+
} catch {
|
|
870
|
+
}
|
|
652
871
|
const opencodeConfig = {
|
|
653
872
|
$schema: "https://opencode.ai/config.json"
|
|
654
873
|
};
|
|
655
874
|
if (config.mcps?.length) {
|
|
656
875
|
const mcpConfig = {};
|
|
876
|
+
const upstreamSet = getUpstreamNames(config.mcps);
|
|
657
877
|
for (const mcp of config.mcps) {
|
|
658
|
-
|
|
878
|
+
if (upstreamSet.has(mcp.name)) continue;
|
|
879
|
+
if (mcp.upstreams?.length) {
|
|
880
|
+
mcpConfig[mcp.name] = buildProxyMcpEntry(mcp, config.mcps, buildOpenCodeMcpEntry);
|
|
881
|
+
} else {
|
|
882
|
+
mcpConfig[mcp.name] = buildOpenCodeMcpEntry(mcp);
|
|
883
|
+
}
|
|
659
884
|
}
|
|
660
885
|
opencodeConfig.mcp = mcpConfig;
|
|
661
886
|
}
|
|
@@ -698,6 +923,7 @@ async function generateOpenCode(config, hubDir) {
|
|
|
698
923
|
}
|
|
699
924
|
} catch {
|
|
700
925
|
}
|
|
926
|
+
await fetchHubDocsSkill(join3(opencodeDir, "skills"));
|
|
701
927
|
await generateEditorCommands(config, hubDir, opencodeDir, ".opencode/commands/");
|
|
702
928
|
if (config.hooks) {
|
|
703
929
|
const plugin = buildOpenCodeHooksPlugin(config.hooks);
|
|
@@ -727,9 +953,9 @@ function buildKiroOrchestratorRule(config) {
|
|
|
727
953
|
|
|
728
954
|
## Your Main Responsibility
|
|
729
955
|
|
|
730
|
-
You are the development orchestrator. Your job is to ensure that any feature or task requested by the user is completed end-to-end by following a structured pipeline. You
|
|
956
|
+
You are the development orchestrator. Your job is to ensure that any feature or task requested by the user is completed end-to-end by following a structured pipeline. You delegate specialized work to subagents defined in \`.kiro/agents/\`.
|
|
731
957
|
|
|
732
|
-
> **Note:** This workspace
|
|
958
|
+
> **Note:** This workspace has custom subagents in \`.kiro/agents/\`. Each pipeline step delegates to the appropriate subagent. Use \`/agent-name\` or instruct Kiro to "use the X subagent" to invoke them.`);
|
|
733
959
|
if (enforce) {
|
|
734
960
|
sections.push(`
|
|
735
961
|
## STRICT WORKFLOW ENFORCEMENT
|
|
@@ -738,7 +964,7 @@ You are the development orchestrator. Your job is to ensure that any feature or
|
|
|
738
964
|
|
|
739
965
|
- NEVER skip a pipeline step, even if the task seems simple or obvious.
|
|
740
966
|
- ALWAYS execute steps in the exact order defined. Do not reorder, merge, or parallelize steps unless the pipeline explicitly allows it.
|
|
741
|
-
- ALWAYS
|
|
967
|
+
- ALWAYS use the designated subagent for each step. Do not improvise if a subagent is assigned.
|
|
742
968
|
- ALWAYS wait for a step to complete and validate its output before moving to the next step.
|
|
743
969
|
- If a step produces a document, READ the document and confirm it is complete before proceeding.
|
|
744
970
|
- If a step has unanswered questions or validation issues, RESOLVE them before advancing.
|
|
@@ -790,6 +1016,10 @@ ${prompt.sections.after_pipeline.trim()}`);
|
|
|
790
1016
|
sections.push(`
|
|
791
1017
|
${prompt.sections.after_delivery.trim()}`);
|
|
792
1018
|
}
|
|
1019
|
+
const mcpToolsSectionKiro = buildMcpToolsSection(config.mcps);
|
|
1020
|
+
if (mcpToolsSectionKiro) {
|
|
1021
|
+
sections.push(mcpToolsSectionKiro);
|
|
1022
|
+
}
|
|
793
1023
|
if (config.memory) {
|
|
794
1024
|
sections.push(`
|
|
795
1025
|
## Team Memory
|
|
@@ -833,18 +1063,18 @@ function buildKiroPipelineSection(steps) {
|
|
|
833
1063
|
return `
|
|
834
1064
|
## Development Pipeline
|
|
835
1065
|
|
|
836
|
-
|
|
1066
|
+
Follow each step sequentially, delegating to the appropriate subagent:
|
|
837
1067
|
|
|
838
|
-
1. **Refinement** \u2014
|
|
839
|
-
2. **Coding** \u2014
|
|
840
|
-
3. **Review** \u2014
|
|
841
|
-
4. **QA** \u2014
|
|
1068
|
+
1. **Refinement** \u2014 Use the \`refinement\` subagent to collect requirements. Write output to the task document.
|
|
1069
|
+
2. **Coding** \u2014 Use the \`coding-backend\` and \`coding-frontend\` subagents to implement the feature.
|
|
1070
|
+
3. **Review** \u2014 Use the \`code-reviewer\` subagent to review the implementation.
|
|
1071
|
+
4. **QA** \u2014 Use the \`qa-backend\` and/or \`qa-frontend\` subagents to test.
|
|
842
1072
|
5. **Delivery** \u2014 Create PRs and notify the team.`;
|
|
843
1073
|
}
|
|
844
1074
|
const parts = [`
|
|
845
1075
|
## Development Pipeline
|
|
846
1076
|
|
|
847
|
-
Follow each step sequentially,
|
|
1077
|
+
Follow each step sequentially, delegating to the appropriate subagent at each phase.
|
|
848
1078
|
`];
|
|
849
1079
|
for (const step of steps) {
|
|
850
1080
|
if (step.actions) {
|
|
@@ -862,7 +1092,7 @@ Follow each step sequentially, applying the role-specific instructions from the
|
|
|
862
1092
|
parts.push(``);
|
|
863
1093
|
}
|
|
864
1094
|
if (step.agent) {
|
|
865
|
-
parts.push(`
|
|
1095
|
+
parts.push(`Use the \`${step.agent}\` subagent.${step.output ? ` Write output to \`${step.output}\`.` : ""}`);
|
|
866
1096
|
if (step.step === "refinement") {
|
|
867
1097
|
parts.push(`
|
|
868
1098
|
After completing the refinement, validate with the user:
|
|
@@ -876,9 +1106,9 @@ After completing the refinement, validate with the user:
|
|
|
876
1106
|
if (typeof a === "string") return { agent: a };
|
|
877
1107
|
return a;
|
|
878
1108
|
});
|
|
879
|
-
parts.push(`
|
|
1109
|
+
parts.push(`Use these subagents sequentially:`);
|
|
880
1110
|
for (const a of agentList) {
|
|
881
|
-
let line = `-
|
|
1111
|
+
let line = `- \`${a.agent}\``;
|
|
882
1112
|
if (a.output) line += ` \u2192 write to \`${a.output}\``;
|
|
883
1113
|
if (a.when) line += ` (when: ${a.when})`;
|
|
884
1114
|
parts.push(line);
|
|
@@ -972,6 +1202,10 @@ ${prompt.sections.after_pipeline.trim()}`);
|
|
|
972
1202
|
sections.push(`
|
|
973
1203
|
${prompt.sections.after_delivery.trim()}`);
|
|
974
1204
|
}
|
|
1205
|
+
const mcpToolsSectionCursor = buildMcpToolsSection(config.mcps);
|
|
1206
|
+
if (mcpToolsSectionCursor) {
|
|
1207
|
+
sections.push(mcpToolsSectionCursor);
|
|
1208
|
+
}
|
|
975
1209
|
if (config.memory) {
|
|
976
1210
|
sections.push(`
|
|
977
1211
|
## Team Memory
|
|
@@ -1120,8 +1354,9 @@ function buildDeliverySection(config) {
|
|
|
1120
1354
|
`];
|
|
1121
1355
|
if (config.integrations?.github) {
|
|
1122
1356
|
const gh = config.integrations.github;
|
|
1357
|
+
const tool = gh.pr_tool === "mcp" ? "GitHub MCP" : "GitHub CLI";
|
|
1123
1358
|
parts.push(`### Pull Requests`);
|
|
1124
|
-
parts.push(`For each repository with changes, push the branch and create a PR using the
|
|
1359
|
+
parts.push(`For each repository with changes, push the branch and create a PR using the ${tool}.`);
|
|
1125
1360
|
if (gh.pr_branch_pattern) {
|
|
1126
1361
|
parts.push(`Branch naming pattern: \`${gh.pr_branch_pattern}\``);
|
|
1127
1362
|
}
|
|
@@ -1201,12 +1436,37 @@ async function generateClaudeCode(config, hubDir) {
|
|
|
1201
1436
|
}
|
|
1202
1437
|
} catch {
|
|
1203
1438
|
}
|
|
1204
|
-
|
|
1439
|
+
const claudeSkillsDirForDocs = join3(claudeDir, "skills");
|
|
1440
|
+
await mkdir2(claudeSkillsDirForDocs, { recursive: true });
|
|
1441
|
+
await fetchHubDocsSkill(claudeSkillsDirForDocs);
|
|
1442
|
+
const hubSteeringDirClaude = resolve(hubDir, "steering");
|
|
1443
|
+
try {
|
|
1444
|
+
const steeringFiles = await readdir2(hubSteeringDirClaude);
|
|
1445
|
+
const mdFiles = steeringFiles.filter((f) => f.endsWith(".md"));
|
|
1446
|
+
for (const file of mdFiles) {
|
|
1447
|
+
const raw = await readFile3(join3(hubSteeringDirClaude, file), "utf-8");
|
|
1448
|
+
const content = stripFrontMatter(raw).trim();
|
|
1449
|
+
if (content) {
|
|
1450
|
+
claudeMdSections.push(content);
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
if (mdFiles.length > 0) {
|
|
1454
|
+
console.log(chalk2.green(` Appended ${mdFiles.length} steering files to CLAUDE.md`));
|
|
1455
|
+
}
|
|
1456
|
+
} catch {
|
|
1457
|
+
}
|
|
1458
|
+
await writeFile2(join3(hubDir, "CLAUDE.md"), claudeMdSections.join("\n\n"), "utf-8");
|
|
1205
1459
|
console.log(chalk2.green(" Generated CLAUDE.md"));
|
|
1206
1460
|
if (config.mcps?.length) {
|
|
1207
1461
|
const mcpJson = {};
|
|
1462
|
+
const upstreamSet = getUpstreamNames(config.mcps);
|
|
1208
1463
|
for (const mcp of config.mcps) {
|
|
1209
|
-
|
|
1464
|
+
if (upstreamSet.has(mcp.name)) continue;
|
|
1465
|
+
if (mcp.upstreams?.length) {
|
|
1466
|
+
mcpJson[mcp.name] = buildProxyMcpEntry(mcp, config.mcps, buildClaudeCodeMcpEntry);
|
|
1467
|
+
} else {
|
|
1468
|
+
mcpJson[mcp.name] = buildClaudeCodeMcpEntry(mcp);
|
|
1469
|
+
}
|
|
1210
1470
|
}
|
|
1211
1471
|
await writeFile2(
|
|
1212
1472
|
join3(hubDir, ".mcp.json"),
|
|
@@ -1267,6 +1527,25 @@ async function generateKiro(config, hubDir) {
|
|
|
1267
1527
|
const settingsDir = join3(kiroDir, "settings");
|
|
1268
1528
|
await mkdir2(steeringDir, { recursive: true });
|
|
1269
1529
|
await mkdir2(settingsDir, { recursive: true });
|
|
1530
|
+
let mode = await getKiroMode(hubDir);
|
|
1531
|
+
if (!mode) {
|
|
1532
|
+
const { kiroMode } = await inquirer.prompt([
|
|
1533
|
+
{
|
|
1534
|
+
type: "list",
|
|
1535
|
+
name: "kiroMode",
|
|
1536
|
+
message: "How do you use Kiro?",
|
|
1537
|
+
choices: [
|
|
1538
|
+
{ name: "Editor / IDE (e.g. Kiro IDE, VS Code)", value: "editor" },
|
|
1539
|
+
{ name: "CLI (e.g. kiro-cli)", value: "cli" }
|
|
1540
|
+
]
|
|
1541
|
+
}
|
|
1542
|
+
]);
|
|
1543
|
+
mode = kiroMode;
|
|
1544
|
+
await saveKiroMode(hubDir, mode);
|
|
1545
|
+
console.log(chalk2.dim(` Saved Kiro mode: ${mode}`));
|
|
1546
|
+
} else {
|
|
1547
|
+
console.log(chalk2.dim(` Using saved Kiro mode: ${mode}`));
|
|
1548
|
+
}
|
|
1270
1549
|
const gitignoreLines = buildGitignoreLines(config);
|
|
1271
1550
|
await writeManagedFile(join3(hubDir, ".gitignore"), gitignoreLines);
|
|
1272
1551
|
console.log(chalk2.green(" Generated .gitignore"));
|
|
@@ -1276,21 +1555,33 @@ async function generateKiro(config, hubDir) {
|
|
|
1276
1555
|
console.log(chalk2.green(" Generated .kiro/steering/orchestrator.md"));
|
|
1277
1556
|
await writeFile2(join3(hubDir, "AGENTS.md"), kiroRule + "\n", "utf-8");
|
|
1278
1557
|
console.log(chalk2.green(" Generated AGENTS.md"));
|
|
1558
|
+
const hubSteeringDir = resolve(hubDir, "steering");
|
|
1559
|
+
try {
|
|
1560
|
+
const steeringFiles = await readdir2(hubSteeringDir);
|
|
1561
|
+
const mdFiles = steeringFiles.filter((f) => f.endsWith(".md"));
|
|
1562
|
+
for (const file of mdFiles) {
|
|
1563
|
+
const raw = await readFile3(join3(hubSteeringDir, file), "utf-8");
|
|
1564
|
+
const content = stripFrontMatter(raw);
|
|
1565
|
+
const kiroSteering = buildKiroSteeringContent(content);
|
|
1566
|
+
await writeFile2(join3(steeringDir, file), kiroSteering, "utf-8");
|
|
1567
|
+
}
|
|
1568
|
+
if (mdFiles.length > 0) {
|
|
1569
|
+
console.log(chalk2.green(` Copied ${mdFiles.length} steering files to .kiro/steering/`));
|
|
1570
|
+
}
|
|
1571
|
+
} catch {
|
|
1572
|
+
}
|
|
1279
1573
|
const agentsDir = resolve(hubDir, "agents");
|
|
1280
1574
|
try {
|
|
1575
|
+
const kiroAgentsDir = join3(kiroDir, "agents");
|
|
1576
|
+
await mkdir2(kiroAgentsDir, { recursive: true });
|
|
1281
1577
|
const agentFiles = await readdir2(agentsDir);
|
|
1282
1578
|
const mdFiles = agentFiles.filter((f) => f.endsWith(".md"));
|
|
1283
1579
|
for (const file of mdFiles) {
|
|
1284
1580
|
const agentContent = await readFile3(join3(agentsDir, file), "utf-8");
|
|
1285
|
-
const
|
|
1286
|
-
|
|
1287
|
-
name: agentName,
|
|
1288
|
-
description: `Role-specific instructions for the ${agentName} phase. Include when working on ${agentName}-related tasks.`
|
|
1289
|
-
});
|
|
1290
|
-
const steeringName = `agent-${file}`;
|
|
1291
|
-
await writeFile2(join3(steeringDir, steeringName), steeringContent, "utf-8");
|
|
1581
|
+
const kiroAgent = buildKiroAgentContent(agentContent);
|
|
1582
|
+
await writeFile2(join3(kiroAgentsDir, file), kiroAgent, "utf-8");
|
|
1292
1583
|
}
|
|
1293
|
-
console.log(chalk2.green(` Copied ${mdFiles.length} agents
|
|
1584
|
+
console.log(chalk2.green(` Copied ${mdFiles.length} agents to .kiro/agents/`));
|
|
1294
1585
|
} catch {
|
|
1295
1586
|
console.log(chalk2.yellow(" No agents/ directory found, skipping agent copy"));
|
|
1296
1587
|
}
|
|
@@ -1316,10 +1607,20 @@ async function generateKiro(config, hubDir) {
|
|
|
1316
1607
|
}
|
|
1317
1608
|
} catch {
|
|
1318
1609
|
}
|
|
1610
|
+
const kiroSkillsDirForDocs = join3(kiroDir, "skills");
|
|
1611
|
+
await mkdir2(kiroSkillsDirForDocs, { recursive: true });
|
|
1612
|
+
await fetchHubDocsSkill(kiroSkillsDirForDocs);
|
|
1319
1613
|
if (config.mcps?.length) {
|
|
1320
1614
|
const mcpConfig = {};
|
|
1615
|
+
const upstreamSet = getUpstreamNames(config.mcps);
|
|
1616
|
+
const buildEntry = (mcp) => buildKiroMcpEntry(mcp, mode);
|
|
1321
1617
|
for (const mcp of config.mcps) {
|
|
1322
|
-
|
|
1618
|
+
if (upstreamSet.has(mcp.name)) continue;
|
|
1619
|
+
if (mcp.upstreams?.length) {
|
|
1620
|
+
mcpConfig[mcp.name] = buildProxyMcpEntry(mcp, config.mcps, buildEntry);
|
|
1621
|
+
} else {
|
|
1622
|
+
mcpConfig[mcp.name] = buildKiroMcpEntry(mcp, mode);
|
|
1623
|
+
}
|
|
1323
1624
|
}
|
|
1324
1625
|
await writeFile2(
|
|
1325
1626
|
join3(settingsDir, "mcp.json"),
|
|
@@ -1368,6 +1669,59 @@ async function generateVSCodeSettings(config, hubDir) {
|
|
|
1368
1669
|
const merged = { ...existing, ...managed };
|
|
1369
1670
|
await writeFile2(settingsPath, JSON.stringify(merged, null, 2) + "\n", "utf-8");
|
|
1370
1671
|
console.log(chalk2.green(" Generated .vscode/settings.json (git multi-repo detection)"));
|
|
1672
|
+
const workspaceFile = `${config.name}.code-workspace`;
|
|
1673
|
+
const workspacePath = join3(hubDir, workspaceFile);
|
|
1674
|
+
let existingWorkspace = {};
|
|
1675
|
+
if (existsSync2(workspacePath)) {
|
|
1676
|
+
try {
|
|
1677
|
+
const raw = await readFile3(workspacePath, "utf-8");
|
|
1678
|
+
existingWorkspace = JSON.parse(raw);
|
|
1679
|
+
} catch {
|
|
1680
|
+
existingWorkspace = {};
|
|
1681
|
+
}
|
|
1682
|
+
} else {
|
|
1683
|
+
const files = await readdir2(hubDir);
|
|
1684
|
+
const existing2 = files.find((f) => f.endsWith(".code-workspace"));
|
|
1685
|
+
if (existing2) {
|
|
1686
|
+
try {
|
|
1687
|
+
const raw = await readFile3(join3(hubDir, existing2), "utf-8");
|
|
1688
|
+
existingWorkspace = JSON.parse(raw);
|
|
1689
|
+
} catch {
|
|
1690
|
+
existingWorkspace = {};
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
const TECH_LABELS = {
|
|
1695
|
+
nestjs: "NestJS",
|
|
1696
|
+
nextjs: "Next.js",
|
|
1697
|
+
react: "React",
|
|
1698
|
+
elixir: "Elixir",
|
|
1699
|
+
phoenix: "Phoenix",
|
|
1700
|
+
django: "Django",
|
|
1701
|
+
fastapi: "FastAPI",
|
|
1702
|
+
rails: "Rails",
|
|
1703
|
+
spring: "Spring",
|
|
1704
|
+
go: "Go",
|
|
1705
|
+
vue: "Vue",
|
|
1706
|
+
svelte: "Svelte",
|
|
1707
|
+
angular: "Angular",
|
|
1708
|
+
express: "Express",
|
|
1709
|
+
koa: "Koa"
|
|
1710
|
+
};
|
|
1711
|
+
const folders = [
|
|
1712
|
+
{ path: ".", name: "Root" }
|
|
1713
|
+
];
|
|
1714
|
+
for (const repo of config.repos) {
|
|
1715
|
+
const repoPath = repo.path.replace(/^\.\//, "");
|
|
1716
|
+
const displayName = repo.display_name || (repo.tech ? `${repo.name} (${TECH_LABELS[repo.tech] || repo.tech})` : repo.name);
|
|
1717
|
+
folders.push({ path: repoPath, name: displayName });
|
|
1718
|
+
}
|
|
1719
|
+
const workspace = {
|
|
1720
|
+
folders,
|
|
1721
|
+
settings: existingWorkspace.settings || {}
|
|
1722
|
+
};
|
|
1723
|
+
await writeFile2(workspacePath, JSON.stringify(workspace, null, " ") + "\n", "utf-8");
|
|
1724
|
+
console.log(chalk2.green(` Generated ${workspaceFile}`));
|
|
1371
1725
|
}
|
|
1372
1726
|
function buildGitignoreLines(config) {
|
|
1373
1727
|
const lines = [
|
|
@@ -1428,6 +1782,9 @@ async function resolveEditor(opts) {
|
|
|
1428
1782
|
}))
|
|
1429
1783
|
}
|
|
1430
1784
|
]);
|
|
1785
|
+
const cache = await readCache(hubDir);
|
|
1786
|
+
delete cache.kiroMode;
|
|
1787
|
+
await writeCache(hubDir, cache);
|
|
1431
1788
|
return editor2;
|
|
1432
1789
|
}
|
|
1433
1790
|
if (opts.editor) return opts.editor;
|
package/dist/index.js
CHANGED
|
@@ -3,10 +3,10 @@ import {
|
|
|
3
3
|
checkAndAutoRegenerate,
|
|
4
4
|
generateCommand,
|
|
5
5
|
loadHubConfig
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-UIANILFI.js";
|
|
7
7
|
|
|
8
8
|
// src/index.ts
|
|
9
|
-
import { Command as
|
|
9
|
+
import { Command as Command19 } from "commander";
|
|
10
10
|
|
|
11
11
|
// src/commands/init.ts
|
|
12
12
|
import { Command } from "commander";
|
|
@@ -2852,11 +2852,327 @@ var directoryCommand = new Command17("directory").alias("dir").description("Brow
|
|
|
2852
2852
|
console.log(chalk17.dim(" hub commands add <owner>/<repo>\n"));
|
|
2853
2853
|
});
|
|
2854
2854
|
|
|
2855
|
+
// src/commands/scan.ts
|
|
2856
|
+
import { Command as Command18 } from "commander";
|
|
2857
|
+
import { existsSync as existsSync14 } from "fs";
|
|
2858
|
+
import { readdir as readdir6, readFile as readFile8, cp as cp4, mkdir as mkdir9, writeFile as writeFile11 } from "fs/promises";
|
|
2859
|
+
import { execSync as execSync13 } from "child_process";
|
|
2860
|
+
import { join as join17 } from "path";
|
|
2861
|
+
import { parse as parse2 } from "yaml";
|
|
2862
|
+
import chalk18 from "chalk";
|
|
2863
|
+
import inquirer from "inquirer";
|
|
2864
|
+
var EDITOR_DIRS = [".kiro", ".cursor", ".opencode", ".claude"];
|
|
2865
|
+
async function findUnregisteredRepos(hubDir, config) {
|
|
2866
|
+
const registeredPaths = new Set(
|
|
2867
|
+
config.repos.map((r) => r.path.replace(/^\.\//, ""))
|
|
2868
|
+
);
|
|
2869
|
+
const entries = await readdir6(hubDir);
|
|
2870
|
+
const unregistered = [];
|
|
2871
|
+
for (const entry of entries) {
|
|
2872
|
+
if (registeredPaths.has(entry)) continue;
|
|
2873
|
+
const gitDir = join17(hubDir, entry, ".git");
|
|
2874
|
+
if (existsSync14(gitDir)) {
|
|
2875
|
+
unregistered.push(entry);
|
|
2876
|
+
}
|
|
2877
|
+
}
|
|
2878
|
+
return unregistered;
|
|
2879
|
+
}
|
|
2880
|
+
function detectTech(repoDir) {
|
|
2881
|
+
if (existsSync14(join17(repoDir, "mix.exs"))) return "elixir";
|
|
2882
|
+
if (existsSync14(join17(repoDir, "next.config.js")) || existsSync14(join17(repoDir, "next.config.ts")) || existsSync14(join17(repoDir, "next.config.mjs"))) return "nextjs";
|
|
2883
|
+
if (existsSync14(join17(repoDir, "nest-cli.json"))) return "nestjs";
|
|
2884
|
+
if (existsSync14(join17(repoDir, "angular.json"))) return "angular";
|
|
2885
|
+
if (existsSync14(join17(repoDir, "svelte.config.js"))) return "svelte";
|
|
2886
|
+
if (existsSync14(join17(repoDir, "nuxt.config.ts")) || existsSync14(join17(repoDir, "nuxt.config.js"))) return "vue";
|
|
2887
|
+
if (existsSync14(join17(repoDir, "go.mod"))) return "go";
|
|
2888
|
+
if (existsSync14(join17(repoDir, "Gemfile"))) return "rails";
|
|
2889
|
+
if (existsSync14(join17(repoDir, "manage.py"))) return "django";
|
|
2890
|
+
if (existsSync14(join17(repoDir, "package.json"))) return "react";
|
|
2891
|
+
return void 0;
|
|
2892
|
+
}
|
|
2893
|
+
function getGitRemote(repoDir) {
|
|
2894
|
+
try {
|
|
2895
|
+
return execSync13("git remote get-url origin", { cwd: repoDir, encoding: "utf-8" }).trim();
|
|
2896
|
+
} catch {
|
|
2897
|
+
return "";
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2900
|
+
function buildRepoYaml(repo) {
|
|
2901
|
+
const lines = [];
|
|
2902
|
+
lines.push(` - name: ${repo.name}`);
|
|
2903
|
+
lines.push(` path: ${repo.path}`);
|
|
2904
|
+
lines.push(` url: ${repo.url}`);
|
|
2905
|
+
if (repo.tech) lines.push(` tech: ${repo.tech}`);
|
|
2906
|
+
return lines.join("\n");
|
|
2907
|
+
}
|
|
2908
|
+
function findReposInsertionPoint(content) {
|
|
2909
|
+
const lines = content.split("\n");
|
|
2910
|
+
let lastRepoLine = -1;
|
|
2911
|
+
let inRepos = false;
|
|
2912
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2913
|
+
const line = lines[i];
|
|
2914
|
+
if (/^repos:/.test(line)) {
|
|
2915
|
+
inRepos = true;
|
|
2916
|
+
continue;
|
|
2917
|
+
}
|
|
2918
|
+
if (inRepos) {
|
|
2919
|
+
if (/^[a-z]/.test(line) || /^[A-Z]/.test(line)) break;
|
|
2920
|
+
if (line.trim() !== "") lastRepoLine = i;
|
|
2921
|
+
}
|
|
2922
|
+
}
|
|
2923
|
+
if (lastRepoLine === -1) return content.length;
|
|
2924
|
+
let offset = 0;
|
|
2925
|
+
for (let i = 0; i <= lastRepoLine; i++) {
|
|
2926
|
+
offset += lines[i].length + 1;
|
|
2927
|
+
}
|
|
2928
|
+
return offset;
|
|
2929
|
+
}
|
|
2930
|
+
async function findUnsyncedAssets(hubDir) {
|
|
2931
|
+
const unsynced = [];
|
|
2932
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2933
|
+
for (const editor of EDITOR_DIRS) {
|
|
2934
|
+
const editorSkillsDir = join17(hubDir, editor, "skills");
|
|
2935
|
+
if (!existsSync14(editorSkillsDir)) continue;
|
|
2936
|
+
try {
|
|
2937
|
+
const folders = await readdir6(editorSkillsDir);
|
|
2938
|
+
for (const folder of folders) {
|
|
2939
|
+
if (folder === "hub-docs") continue;
|
|
2940
|
+
const skillFile = join17(editorSkillsDir, folder, "SKILL.md");
|
|
2941
|
+
if (!existsSync14(skillFile)) continue;
|
|
2942
|
+
const canonicalSkillFile = join17(hubDir, "skills", folder, "SKILL.md");
|
|
2943
|
+
if (existsSync14(canonicalSkillFile)) continue;
|
|
2944
|
+
const key = `skill:${folder}`;
|
|
2945
|
+
if (seen.has(key)) continue;
|
|
2946
|
+
seen.add(key);
|
|
2947
|
+
unsynced.push({ type: "skill", name: folder, source: editor });
|
|
2948
|
+
}
|
|
2949
|
+
} catch {
|
|
2950
|
+
}
|
|
2951
|
+
}
|
|
2952
|
+
for (const editor of EDITOR_DIRS) {
|
|
2953
|
+
const editorAgentsDir = join17(hubDir, editor, "agents");
|
|
2954
|
+
if (!existsSync14(editorAgentsDir)) continue;
|
|
2955
|
+
try {
|
|
2956
|
+
const files = await readdir6(editorAgentsDir);
|
|
2957
|
+
for (const file of files) {
|
|
2958
|
+
if (!file.endsWith(".md")) continue;
|
|
2959
|
+
const canonicalFile = join17(hubDir, "agents", file);
|
|
2960
|
+
if (existsSync14(canonicalFile)) continue;
|
|
2961
|
+
const key = `agent:${file}`;
|
|
2962
|
+
if (seen.has(key)) continue;
|
|
2963
|
+
seen.add(key);
|
|
2964
|
+
unsynced.push({ type: "agent", name: file, source: editor });
|
|
2965
|
+
}
|
|
2966
|
+
} catch {
|
|
2967
|
+
}
|
|
2968
|
+
}
|
|
2969
|
+
const steeringDir = join17(hubDir, ".kiro", "steering");
|
|
2970
|
+
if (existsSync14(steeringDir)) {
|
|
2971
|
+
const canonicalSteeringDir = join17(hubDir, "steering");
|
|
2972
|
+
try {
|
|
2973
|
+
const files = await readdir6(steeringDir);
|
|
2974
|
+
for (const file of files) {
|
|
2975
|
+
if (!file.endsWith(".md")) continue;
|
|
2976
|
+
if (file === "orchestrator.md") continue;
|
|
2977
|
+
const canonicalFile = join17(canonicalSteeringDir, file);
|
|
2978
|
+
if (existsSync14(canonicalFile)) continue;
|
|
2979
|
+
const key = `steering:${file}`;
|
|
2980
|
+
if (seen.has(key)) continue;
|
|
2981
|
+
seen.add(key);
|
|
2982
|
+
unsynced.push({ type: "steering", name: file, source: ".kiro" });
|
|
2983
|
+
}
|
|
2984
|
+
} catch {
|
|
2985
|
+
}
|
|
2986
|
+
}
|
|
2987
|
+
const cursorRulesDir = join17(hubDir, ".cursor", "rules");
|
|
2988
|
+
if (existsSync14(cursorRulesDir)) {
|
|
2989
|
+
const canonicalSteeringDir = join17(hubDir, "steering");
|
|
2990
|
+
try {
|
|
2991
|
+
const files = await readdir6(cursorRulesDir);
|
|
2992
|
+
for (const file of files) {
|
|
2993
|
+
if (!file.endsWith(".mdc")) continue;
|
|
2994
|
+
if (file === "orchestrator.mdc") continue;
|
|
2995
|
+
const mdName = file.replace(/\.mdc$/, ".md");
|
|
2996
|
+
const canonicalFile = join17(canonicalSteeringDir, mdName);
|
|
2997
|
+
if (existsSync14(canonicalFile)) continue;
|
|
2998
|
+
const key = `steering:${mdName}`;
|
|
2999
|
+
if (seen.has(key)) continue;
|
|
3000
|
+
seen.add(key);
|
|
3001
|
+
unsynced.push({ type: "steering", name: mdName, source: ".cursor" });
|
|
3002
|
+
}
|
|
3003
|
+
} catch {
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
const opencodeRulesDir = join17(hubDir, ".opencode", "rules");
|
|
3007
|
+
if (existsSync14(opencodeRulesDir)) {
|
|
3008
|
+
const canonicalSteeringDir = join17(hubDir, "steering");
|
|
3009
|
+
try {
|
|
3010
|
+
const files = await readdir6(opencodeRulesDir);
|
|
3011
|
+
for (const file of files) {
|
|
3012
|
+
if (!file.endsWith(".md")) continue;
|
|
3013
|
+
if (file === "orchestrator.md") continue;
|
|
3014
|
+
const canonicalFile = join17(canonicalSteeringDir, file);
|
|
3015
|
+
if (existsSync14(canonicalFile)) continue;
|
|
3016
|
+
const key = `steering:${file}`;
|
|
3017
|
+
if (seen.has(key)) continue;
|
|
3018
|
+
seen.add(key);
|
|
3019
|
+
unsynced.push({ type: "steering", name: file, source: ".opencode" });
|
|
3020
|
+
}
|
|
3021
|
+
} catch {
|
|
3022
|
+
}
|
|
3023
|
+
}
|
|
3024
|
+
return unsynced;
|
|
3025
|
+
}
|
|
3026
|
+
function stripFrontMatter(content) {
|
|
3027
|
+
const match = content.match(/^---\n[\s\S]*?\n---\n*/);
|
|
3028
|
+
if (match) return content.slice(match[0].length);
|
|
3029
|
+
return content;
|
|
3030
|
+
}
|
|
3031
|
+
async function syncAssets(hubDir, assets) {
|
|
3032
|
+
for (const asset of assets) {
|
|
3033
|
+
if (asset.type === "skill") {
|
|
3034
|
+
const src = join17(hubDir, asset.source, "skills", asset.name);
|
|
3035
|
+
const dest = join17(hubDir, "skills", asset.name);
|
|
3036
|
+
await mkdir9(join17(hubDir, "skills"), { recursive: true });
|
|
3037
|
+
await cp4(src, dest, { recursive: true });
|
|
3038
|
+
const skillMd = join17(dest, "SKILL.md");
|
|
3039
|
+
if (existsSync14(skillMd)) {
|
|
3040
|
+
const raw = await readFile8(skillMd, "utf-8");
|
|
3041
|
+
const cleaned = stripFrontMatter(raw);
|
|
3042
|
+
if (cleaned !== raw) {
|
|
3043
|
+
await writeFile11(skillMd, cleaned, "utf-8");
|
|
3044
|
+
}
|
|
3045
|
+
}
|
|
3046
|
+
console.log(chalk18.green(` Synced skill: ${asset.name} (from ${asset.source})`));
|
|
3047
|
+
} else if (asset.type === "agent") {
|
|
3048
|
+
const src = join17(hubDir, asset.source, "agents", asset.name);
|
|
3049
|
+
const dest = join17(hubDir, "agents", asset.name);
|
|
3050
|
+
await mkdir9(join17(hubDir, "agents"), { recursive: true });
|
|
3051
|
+
const raw = await readFile8(src, "utf-8");
|
|
3052
|
+
const cleaned = stripFrontMatter(raw);
|
|
3053
|
+
await writeFile11(dest, cleaned, "utf-8");
|
|
3054
|
+
console.log(chalk18.green(` Synced agent: ${asset.name} (from ${asset.source})`));
|
|
3055
|
+
} else if (asset.type === "steering") {
|
|
3056
|
+
const dest = join17(hubDir, "steering", asset.name);
|
|
3057
|
+
await mkdir9(join17(hubDir, "steering"), { recursive: true });
|
|
3058
|
+
let raw;
|
|
3059
|
+
if (asset.source === ".cursor") {
|
|
3060
|
+
const mdcName = asset.name.replace(/\.md$/, ".mdc");
|
|
3061
|
+
raw = await readFile8(join17(hubDir, ".cursor", "rules", mdcName), "utf-8");
|
|
3062
|
+
} else if (asset.source === ".opencode") {
|
|
3063
|
+
raw = await readFile8(join17(hubDir, ".opencode", "rules", asset.name), "utf-8");
|
|
3064
|
+
} else {
|
|
3065
|
+
raw = await readFile8(join17(hubDir, asset.source, "steering", asset.name), "utf-8");
|
|
3066
|
+
}
|
|
3067
|
+
const cleaned = stripFrontMatter(raw);
|
|
3068
|
+
await writeFile11(dest, cleaned, "utf-8");
|
|
3069
|
+
console.log(chalk18.green(` Synced steering: ${asset.name} (from ${asset.source})`));
|
|
3070
|
+
}
|
|
3071
|
+
}
|
|
3072
|
+
}
|
|
3073
|
+
var scanCommand = new Command18("scan").description("Detect git repositories not registered in hub.yaml").option("-y, --yes", "Auto-add all found repos without prompting").action(async (opts) => {
|
|
3074
|
+
const hubDir = process.cwd();
|
|
3075
|
+
const configPath = join17(hubDir, "hub.yaml");
|
|
3076
|
+
if (!existsSync14(configPath)) {
|
|
3077
|
+
console.log(chalk18.red("No hub.yaml found in current directory."));
|
|
3078
|
+
process.exit(1);
|
|
3079
|
+
}
|
|
3080
|
+
const content = await readFile8(configPath, "utf-8");
|
|
3081
|
+
const config = parse2(content);
|
|
3082
|
+
let hasChanges = false;
|
|
3083
|
+
console.log(chalk18.blue("\nScanning for unregistered repositories...\n"));
|
|
3084
|
+
const unregistered = await findUnregisteredRepos(hubDir, config);
|
|
3085
|
+
if (unregistered.length === 0) {
|
|
3086
|
+
console.log(chalk18.green("All repositories are registered in hub.yaml."));
|
|
3087
|
+
} else {
|
|
3088
|
+
console.log(chalk18.yellow(`Found ${unregistered.length} unregistered repo(s):
|
|
3089
|
+
`));
|
|
3090
|
+
const repoDetails = unregistered.map((name) => {
|
|
3091
|
+
const repoDir = join17(hubDir, name);
|
|
3092
|
+
const tech = detectTech(repoDir);
|
|
3093
|
+
const url = getGitRemote(repoDir);
|
|
3094
|
+
return { name, tech, url, path: `./${name}` };
|
|
3095
|
+
});
|
|
3096
|
+
for (const repo of repoDetails) {
|
|
3097
|
+
const techLabel = repo.tech ? chalk18.dim(` (${repo.tech})`) : "";
|
|
3098
|
+
console.log(` ${chalk18.cyan(repo.name)}${techLabel}`);
|
|
3099
|
+
}
|
|
3100
|
+
console.log();
|
|
3101
|
+
let toAdd = repoDetails;
|
|
3102
|
+
if (!opts.yes) {
|
|
3103
|
+
const { selected } = await inquirer.prompt([
|
|
3104
|
+
{
|
|
3105
|
+
type: "checkbox",
|
|
3106
|
+
name: "selected",
|
|
3107
|
+
message: "Select repos to add to hub.yaml:",
|
|
3108
|
+
choices: repoDetails.map((r) => ({
|
|
3109
|
+
name: `${r.name}${r.tech ? ` (${r.tech})` : ""}`,
|
|
3110
|
+
value: r.name,
|
|
3111
|
+
checked: true
|
|
3112
|
+
}))
|
|
3113
|
+
}
|
|
3114
|
+
]);
|
|
3115
|
+
toAdd = repoDetails.filter((r) => selected.includes(r.name));
|
|
3116
|
+
}
|
|
3117
|
+
if (toAdd.length > 0) {
|
|
3118
|
+
const originalContent = await readFile8(configPath, "utf-8");
|
|
3119
|
+
const insertAt = findReposInsertionPoint(originalContent);
|
|
3120
|
+
const before = originalContent.slice(0, insertAt);
|
|
3121
|
+
const after = originalContent.slice(insertAt);
|
|
3122
|
+
const newEntries = toAdd.map(buildRepoYaml).join("\n");
|
|
3123
|
+
const updatedContent = before + newEntries + "\n" + after;
|
|
3124
|
+
await import("fs/promises").then((fs) => fs.writeFile(configPath, updatedContent, "utf-8"));
|
|
3125
|
+
console.log(chalk18.green(`Added ${toAdd.length} repo(s) to hub.yaml.`));
|
|
3126
|
+
hasChanges = true;
|
|
3127
|
+
}
|
|
3128
|
+
}
|
|
3129
|
+
console.log(chalk18.blue("\nScanning for unsynced skills, agents, and steering...\n"));
|
|
3130
|
+
const unsyncedAssets = await findUnsyncedAssets(hubDir);
|
|
3131
|
+
if (unsyncedAssets.length === 0) {
|
|
3132
|
+
console.log(chalk18.green("All assets are synced."));
|
|
3133
|
+
} else {
|
|
3134
|
+
console.log(chalk18.yellow(`Found ${unsyncedAssets.length} unsynced asset(s):
|
|
3135
|
+
`));
|
|
3136
|
+
for (const asset of unsyncedAssets) {
|
|
3137
|
+
console.log(` ${chalk18.cyan(asset.name)} ${chalk18.dim(`(${asset.type} from ${asset.source})`)}`);
|
|
3138
|
+
}
|
|
3139
|
+
console.log();
|
|
3140
|
+
let toSync = unsyncedAssets;
|
|
3141
|
+
if (!opts.yes) {
|
|
3142
|
+
const { selected } = await inquirer.prompt([
|
|
3143
|
+
{
|
|
3144
|
+
type: "checkbox",
|
|
3145
|
+
name: "selected",
|
|
3146
|
+
message: "Select assets to sync to canonical folders:",
|
|
3147
|
+
choices: unsyncedAssets.map((a) => ({
|
|
3148
|
+
name: `${a.name} (${a.type} from ${a.source})`,
|
|
3149
|
+
value: `${a.type}:${a.name}`,
|
|
3150
|
+
checked: true
|
|
3151
|
+
}))
|
|
3152
|
+
}
|
|
3153
|
+
]);
|
|
3154
|
+
toSync = unsyncedAssets.filter((a) => selected.includes(`${a.type}:${a.name}`));
|
|
3155
|
+
}
|
|
3156
|
+
if (toSync.length > 0) {
|
|
3157
|
+
await syncAssets(hubDir, toSync);
|
|
3158
|
+
console.log(chalk18.green(`Synced ${toSync.length} asset(s).`));
|
|
3159
|
+
hasChanges = true;
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
3162
|
+
if (hasChanges) {
|
|
3163
|
+
console.log(chalk18.cyan(`
|
|
3164
|
+
Run ${chalk18.bold("hub generate")} to update editor configs.
|
|
3165
|
+
`));
|
|
3166
|
+
} else {
|
|
3167
|
+
console.log();
|
|
3168
|
+
}
|
|
3169
|
+
});
|
|
3170
|
+
|
|
2855
3171
|
// src/index.ts
|
|
2856
|
-
var program = new
|
|
3172
|
+
var program = new Command19();
|
|
2857
3173
|
program.name("hub").description(
|
|
2858
3174
|
"Give your AI coding assistant the full picture. Multi-repo context, agent orchestration, and end-to-end workflows."
|
|
2859
|
-
).version("0.
|
|
3175
|
+
).version("0.7.3").enablePositionalOptions();
|
|
2860
3176
|
program.addCommand(initCommand);
|
|
2861
3177
|
program.addCommand(addRepoCommand);
|
|
2862
3178
|
program.addCommand(setupCommand);
|
|
@@ -2877,4 +3193,5 @@ program.addCommand(toolsCommand);
|
|
|
2877
3193
|
program.addCommand(memoryCommand);
|
|
2878
3194
|
program.addCommand(updateCommand);
|
|
2879
3195
|
program.addCommand(directoryCommand);
|
|
3196
|
+
program.addCommand(scanCommand);
|
|
2880
3197
|
program.parse();
|