@arvoretech/hub 0.6.3 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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-67HCNILJ.js");
124
+ const { generators: generators2 } = await import("./generate-BGTLIG4F.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
- mcpConfig[mcp.name] = buildCursorMcpEntry(mcp);
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 buildKiroMcpEntry(mcp) {
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, ...mcp.env && { env: mcp.env } };
506
+ return { url: mcp.url, ...env && { env } };
375
507
  }
376
508
  if (mcp.image) {
377
509
  const args = ["run", "-i", "--rm"];
378
- if (mcp.env) {
379
- for (const [key, value] of Object.entries(mcp.env)) {
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
- ...mcp.env && { env: mcp.env }
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 };
@@ -649,13 +813,33 @@ async function generateOpenCode(config, hubDir) {
649
813
  const orchestratorRule = buildOpenCodeOrchestratorRule(config);
650
814
  await writeFile2(join3(opencodeDir, "rules", "orchestrator.md"), orchestratorRule + "\n", "utf-8");
651
815
  console.log(chalk2.green(" Generated .opencode/rules/orchestrator.md"));
816
+ const hubSteeringDirOC = resolve(hubDir, "steering");
817
+ try {
818
+ const steeringFiles = await readdir2(hubSteeringDirOC);
819
+ const mdFiles = steeringFiles.filter((f) => f.endsWith(".md"));
820
+ for (const file of mdFiles) {
821
+ const raw = await readFile3(join3(hubSteeringDirOC, file), "utf-8");
822
+ const content = stripFrontMatter(raw);
823
+ await writeFile2(join3(opencodeDir, "rules", file), content, "utf-8");
824
+ }
825
+ if (mdFiles.length > 0) {
826
+ console.log(chalk2.green(` Copied ${mdFiles.length} steering files to .opencode/rules/`));
827
+ }
828
+ } catch {
829
+ }
652
830
  const opencodeConfig = {
653
831
  $schema: "https://opencode.ai/config.json"
654
832
  };
655
833
  if (config.mcps?.length) {
656
834
  const mcpConfig = {};
835
+ const upstreamSet = getUpstreamNames(config.mcps);
657
836
  for (const mcp of config.mcps) {
658
- mcpConfig[mcp.name] = buildOpenCodeMcpEntry(mcp);
837
+ if (upstreamSet.has(mcp.name)) continue;
838
+ if (mcp.upstreams?.length) {
839
+ mcpConfig[mcp.name] = buildProxyMcpEntry(mcp, config.mcps, buildOpenCodeMcpEntry);
840
+ } else {
841
+ mcpConfig[mcp.name] = buildOpenCodeMcpEntry(mcp);
842
+ }
659
843
  }
660
844
  opencodeConfig.mcp = mcpConfig;
661
845
  }
@@ -698,6 +882,7 @@ async function generateOpenCode(config, hubDir) {
698
882
  }
699
883
  } catch {
700
884
  }
885
+ await fetchHubDocsSkill(join3(opencodeDir, "skills"));
701
886
  await generateEditorCommands(config, hubDir, opencodeDir, ".opencode/commands/");
702
887
  if (config.hooks) {
703
888
  const plugin = buildOpenCodeHooksPlugin(config.hooks);
@@ -727,9 +912,9 @@ function buildKiroOrchestratorRule(config) {
727
912
 
728
913
  ## Your Main Responsibility
729
914
 
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 work as a single agent but follow specialized instructions from steering files for each phase of development.
915
+ 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
916
 
732
- > **Note:** This workspace uses steering files in \`.kiro/steering/\` to provide role-specific instructions for each pipeline step. When a step says "follow the instructions from steering file X", read that file and apply its guidelines to the current task.`);
917
+ > **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
918
  if (enforce) {
734
919
  sections.push(`
735
920
  ## STRICT WORKFLOW ENFORCEMENT
@@ -738,7 +923,7 @@ You are the development orchestrator. Your job is to ensure that any feature or
738
923
 
739
924
  - NEVER skip a pipeline step, even if the task seems simple or obvious.
740
925
  - ALWAYS execute steps in the exact order defined. Do not reorder, merge, or parallelize steps unless the pipeline explicitly allows it.
741
- - ALWAYS follow the designated steering file for each step. Do not improvise if a steering file is assigned.
926
+ - ALWAYS use the designated subagent for each step. Do not improvise if a subagent is assigned.
742
927
  - ALWAYS wait for a step to complete and validate its output before moving to the next step.
743
928
  - If a step produces a document, READ the document and confirm it is complete before proceeding.
744
929
  - If a step has unanswered questions or validation issues, RESOLVE them before advancing.
@@ -833,18 +1018,18 @@ function buildKiroPipelineSection(steps) {
833
1018
  return `
834
1019
  ## Development Pipeline
835
1020
 
836
- Since Kiro does not support sub-agents, follow each step sequentially, applying the guidelines from the corresponding steering file:
1021
+ Follow each step sequentially, delegating to the appropriate subagent:
837
1022
 
838
- 1. **Refinement** \u2014 Read and follow \`agent-refinement.md\` steering file to collect requirements. Write output to the task document.
839
- 2. **Coding** \u2014 Follow the coding steering files (\`agent-coding-backend.md\`, \`agent-coding-frontend.md\`) to implement the feature.
840
- 3. **Review** \u2014 Follow \`agent-code-reviewer.md\` to review the implementation.
841
- 4. **QA** \u2014 Follow \`agent-qa-backend.md\` and/or \`agent-qa-frontend.md\` to test.
1023
+ 1. **Refinement** \u2014 Use the \`refinement\` subagent to collect requirements. Write output to the task document.
1024
+ 2. **Coding** \u2014 Use the \`coding-backend\` and \`coding-frontend\` subagents to implement the feature.
1025
+ 3. **Review** \u2014 Use the \`code-reviewer\` subagent to review the implementation.
1026
+ 4. **QA** \u2014 Use the \`qa-backend\` and/or \`qa-frontend\` subagents to test.
842
1027
  5. **Delivery** \u2014 Create PRs and notify the team.`;
843
1028
  }
844
1029
  const parts = [`
845
1030
  ## Development Pipeline
846
1031
 
847
- Follow each step sequentially, applying the role-specific instructions from the corresponding steering file at each phase.
1032
+ Follow each step sequentially, delegating to the appropriate subagent at each phase.
848
1033
  `];
849
1034
  for (const step of steps) {
850
1035
  if (step.actions) {
@@ -862,7 +1047,7 @@ Follow each step sequentially, applying the role-specific instructions from the
862
1047
  parts.push(``);
863
1048
  }
864
1049
  if (step.agent) {
865
- parts.push(`Follow the instructions from the \`agent-${step.agent}.md\` steering file.${step.output ? ` Write output to \`${step.output}\`.` : ""}`);
1050
+ parts.push(`Use the \`${step.agent}\` subagent.${step.output ? ` Write output to \`${step.output}\`.` : ""}`);
866
1051
  if (step.step === "refinement") {
867
1052
  parts.push(`
868
1053
  After completing the refinement, validate with the user:
@@ -876,9 +1061,9 @@ After completing the refinement, validate with the user:
876
1061
  if (typeof a === "string") return { agent: a };
877
1062
  return a;
878
1063
  });
879
- parts.push(`Follow the instructions from these steering files sequentially:`);
1064
+ parts.push(`Use these subagents sequentially:`);
880
1065
  for (const a of agentList) {
881
- let line = `- \`agent-${a.agent}.md\``;
1066
+ let line = `- \`${a.agent}\``;
882
1067
  if (a.output) line += ` \u2192 write to \`${a.output}\``;
883
1068
  if (a.when) line += ` (when: ${a.when})`;
884
1069
  parts.push(line);
@@ -1120,8 +1305,9 @@ function buildDeliverySection(config) {
1120
1305
  `];
1121
1306
  if (config.integrations?.github) {
1122
1307
  const gh = config.integrations.github;
1308
+ const tool = gh.pr_tool === "mcp" ? "GitHub MCP" : "GitHub CLI";
1123
1309
  parts.push(`### Pull Requests`);
1124
- parts.push(`For each repository with changes, push the branch and create a PR using the GitHub MCP.`);
1310
+ parts.push(`For each repository with changes, push the branch and create a PR using the ${tool}.`);
1125
1311
  if (gh.pr_branch_pattern) {
1126
1312
  parts.push(`Branch naming pattern: \`${gh.pr_branch_pattern}\``);
1127
1313
  }
@@ -1201,12 +1387,37 @@ async function generateClaudeCode(config, hubDir) {
1201
1387
  }
1202
1388
  } catch {
1203
1389
  }
1204
- await writeFile2(join3(hubDir, "CLAUDE.md"), claudeMdSections.join("\n"), "utf-8");
1390
+ const claudeSkillsDirForDocs = join3(claudeDir, "skills");
1391
+ await mkdir2(claudeSkillsDirForDocs, { recursive: true });
1392
+ await fetchHubDocsSkill(claudeSkillsDirForDocs);
1393
+ const hubSteeringDirClaude = resolve(hubDir, "steering");
1394
+ try {
1395
+ const steeringFiles = await readdir2(hubSteeringDirClaude);
1396
+ const mdFiles = steeringFiles.filter((f) => f.endsWith(".md"));
1397
+ for (const file of mdFiles) {
1398
+ const raw = await readFile3(join3(hubSteeringDirClaude, file), "utf-8");
1399
+ const content = stripFrontMatter(raw).trim();
1400
+ if (content) {
1401
+ claudeMdSections.push(content);
1402
+ }
1403
+ }
1404
+ if (mdFiles.length > 0) {
1405
+ console.log(chalk2.green(` Appended ${mdFiles.length} steering files to CLAUDE.md`));
1406
+ }
1407
+ } catch {
1408
+ }
1409
+ await writeFile2(join3(hubDir, "CLAUDE.md"), claudeMdSections.join("\n\n"), "utf-8");
1205
1410
  console.log(chalk2.green(" Generated CLAUDE.md"));
1206
1411
  if (config.mcps?.length) {
1207
1412
  const mcpJson = {};
1413
+ const upstreamSet = getUpstreamNames(config.mcps);
1208
1414
  for (const mcp of config.mcps) {
1209
- mcpJson[mcp.name] = buildClaudeCodeMcpEntry(mcp);
1415
+ if (upstreamSet.has(mcp.name)) continue;
1416
+ if (mcp.upstreams?.length) {
1417
+ mcpJson[mcp.name] = buildProxyMcpEntry(mcp, config.mcps, buildClaudeCodeMcpEntry);
1418
+ } else {
1419
+ mcpJson[mcp.name] = buildClaudeCodeMcpEntry(mcp);
1420
+ }
1210
1421
  }
1211
1422
  await writeFile2(
1212
1423
  join3(hubDir, ".mcp.json"),
@@ -1267,6 +1478,25 @@ async function generateKiro(config, hubDir) {
1267
1478
  const settingsDir = join3(kiroDir, "settings");
1268
1479
  await mkdir2(steeringDir, { recursive: true });
1269
1480
  await mkdir2(settingsDir, { recursive: true });
1481
+ let mode = await getKiroMode(hubDir);
1482
+ if (!mode) {
1483
+ const { kiroMode } = await inquirer.prompt([
1484
+ {
1485
+ type: "list",
1486
+ name: "kiroMode",
1487
+ message: "How do you use Kiro?",
1488
+ choices: [
1489
+ { name: "Editor / IDE (e.g. Kiro IDE, VS Code)", value: "editor" },
1490
+ { name: "CLI (e.g. kiro-cli)", value: "cli" }
1491
+ ]
1492
+ }
1493
+ ]);
1494
+ mode = kiroMode;
1495
+ await saveKiroMode(hubDir, mode);
1496
+ console.log(chalk2.dim(` Saved Kiro mode: ${mode}`));
1497
+ } else {
1498
+ console.log(chalk2.dim(` Using saved Kiro mode: ${mode}`));
1499
+ }
1270
1500
  const gitignoreLines = buildGitignoreLines(config);
1271
1501
  await writeManagedFile(join3(hubDir, ".gitignore"), gitignoreLines);
1272
1502
  console.log(chalk2.green(" Generated .gitignore"));
@@ -1276,21 +1506,33 @@ async function generateKiro(config, hubDir) {
1276
1506
  console.log(chalk2.green(" Generated .kiro/steering/orchestrator.md"));
1277
1507
  await writeFile2(join3(hubDir, "AGENTS.md"), kiroRule + "\n", "utf-8");
1278
1508
  console.log(chalk2.green(" Generated AGENTS.md"));
1509
+ const hubSteeringDir = resolve(hubDir, "steering");
1510
+ try {
1511
+ const steeringFiles = await readdir2(hubSteeringDir);
1512
+ const mdFiles = steeringFiles.filter((f) => f.endsWith(".md"));
1513
+ for (const file of mdFiles) {
1514
+ const raw = await readFile3(join3(hubSteeringDir, file), "utf-8");
1515
+ const content = stripFrontMatter(raw);
1516
+ const kiroSteering = buildKiroSteeringContent(content);
1517
+ await writeFile2(join3(steeringDir, file), kiroSteering, "utf-8");
1518
+ }
1519
+ if (mdFiles.length > 0) {
1520
+ console.log(chalk2.green(` Copied ${mdFiles.length} steering files to .kiro/steering/`));
1521
+ }
1522
+ } catch {
1523
+ }
1279
1524
  const agentsDir = resolve(hubDir, "agents");
1280
1525
  try {
1526
+ const kiroAgentsDir = join3(kiroDir, "agents");
1527
+ await mkdir2(kiroAgentsDir, { recursive: true });
1281
1528
  const agentFiles = await readdir2(agentsDir);
1282
1529
  const mdFiles = agentFiles.filter((f) => f.endsWith(".md"));
1283
1530
  for (const file of mdFiles) {
1284
1531
  const agentContent = await readFile3(join3(agentsDir, file), "utf-8");
1285
- const agentName = file.replace(/\.md$/, "");
1286
- const steeringContent = buildKiroSteeringContent(agentContent, "auto", {
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");
1532
+ const kiroAgent = buildKiroAgentContent(agentContent);
1533
+ await writeFile2(join3(kiroAgentsDir, file), kiroAgent, "utf-8");
1292
1534
  }
1293
- console.log(chalk2.green(` Copied ${mdFiles.length} agents as steering files`));
1535
+ console.log(chalk2.green(` Copied ${mdFiles.length} agents to .kiro/agents/`));
1294
1536
  } catch {
1295
1537
  console.log(chalk2.yellow(" No agents/ directory found, skipping agent copy"));
1296
1538
  }
@@ -1316,10 +1558,20 @@ async function generateKiro(config, hubDir) {
1316
1558
  }
1317
1559
  } catch {
1318
1560
  }
1561
+ const kiroSkillsDirForDocs = join3(kiroDir, "skills");
1562
+ await mkdir2(kiroSkillsDirForDocs, { recursive: true });
1563
+ await fetchHubDocsSkill(kiroSkillsDirForDocs);
1319
1564
  if (config.mcps?.length) {
1320
1565
  const mcpConfig = {};
1566
+ const upstreamSet = getUpstreamNames(config.mcps);
1567
+ const buildEntry = (mcp) => buildKiroMcpEntry(mcp, mode);
1321
1568
  for (const mcp of config.mcps) {
1322
- mcpConfig[mcp.name] = buildKiroMcpEntry(mcp);
1569
+ if (upstreamSet.has(mcp.name)) continue;
1570
+ if (mcp.upstreams?.length) {
1571
+ mcpConfig[mcp.name] = buildProxyMcpEntry(mcp, config.mcps, buildEntry);
1572
+ } else {
1573
+ mcpConfig[mcp.name] = buildKiroMcpEntry(mcp, mode);
1574
+ }
1323
1575
  }
1324
1576
  await writeFile2(
1325
1577
  join3(settingsDir, "mcp.json"),
@@ -1368,6 +1620,59 @@ async function generateVSCodeSettings(config, hubDir) {
1368
1620
  const merged = { ...existing, ...managed };
1369
1621
  await writeFile2(settingsPath, JSON.stringify(merged, null, 2) + "\n", "utf-8");
1370
1622
  console.log(chalk2.green(" Generated .vscode/settings.json (git multi-repo detection)"));
1623
+ const workspaceFile = `${config.name}.code-workspace`;
1624
+ const workspacePath = join3(hubDir, workspaceFile);
1625
+ let existingWorkspace = {};
1626
+ if (existsSync2(workspacePath)) {
1627
+ try {
1628
+ const raw = await readFile3(workspacePath, "utf-8");
1629
+ existingWorkspace = JSON.parse(raw);
1630
+ } catch {
1631
+ existingWorkspace = {};
1632
+ }
1633
+ } else {
1634
+ const files = await readdir2(hubDir);
1635
+ const existing2 = files.find((f) => f.endsWith(".code-workspace"));
1636
+ if (existing2) {
1637
+ try {
1638
+ const raw = await readFile3(join3(hubDir, existing2), "utf-8");
1639
+ existingWorkspace = JSON.parse(raw);
1640
+ } catch {
1641
+ existingWorkspace = {};
1642
+ }
1643
+ }
1644
+ }
1645
+ const TECH_LABELS = {
1646
+ nestjs: "NestJS",
1647
+ nextjs: "Next.js",
1648
+ react: "React",
1649
+ elixir: "Elixir",
1650
+ phoenix: "Phoenix",
1651
+ django: "Django",
1652
+ fastapi: "FastAPI",
1653
+ rails: "Rails",
1654
+ spring: "Spring",
1655
+ go: "Go",
1656
+ vue: "Vue",
1657
+ svelte: "Svelte",
1658
+ angular: "Angular",
1659
+ express: "Express",
1660
+ koa: "Koa"
1661
+ };
1662
+ const folders = [
1663
+ { path: ".", name: "Root" }
1664
+ ];
1665
+ for (const repo of config.repos) {
1666
+ const repoPath = repo.path.replace(/^\.\//, "");
1667
+ const displayName = repo.display_name || (repo.tech ? `${repo.name} (${TECH_LABELS[repo.tech] || repo.tech})` : repo.name);
1668
+ folders.push({ path: repoPath, name: displayName });
1669
+ }
1670
+ const workspace = {
1671
+ folders,
1672
+ settings: existingWorkspace.settings || {}
1673
+ };
1674
+ await writeFile2(workspacePath, JSON.stringify(workspace, null, " ") + "\n", "utf-8");
1675
+ console.log(chalk2.green(` Generated ${workspaceFile}`));
1371
1676
  }
1372
1677
  function buildGitignoreLines(config) {
1373
1678
  const lines = [
@@ -1428,6 +1733,9 @@ async function resolveEditor(opts) {
1428
1733
  }))
1429
1734
  }
1430
1735
  ]);
1736
+ const cache = await readCache(hubDir);
1737
+ delete cache.kiroMode;
1738
+ await writeCache(hubDir, cache);
1431
1739
  return editor2;
1432
1740
  }
1433
1741
  if (opts.editor) return opts.editor;
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  generateCommand,
3
3
  generators
4
- } from "./chunk-U5G47GMH.js";
4
+ } from "./chunk-2NWNZCYP.js";
5
5
  export {
6
6
  generateCommand,
7
7
  generators
package/dist/index.js CHANGED
@@ -3,10 +3,10 @@ import {
3
3
  checkAndAutoRegenerate,
4
4
  generateCommand,
5
5
  loadHubConfig
6
- } from "./chunk-U5G47GMH.js";
6
+ } from "./chunk-2NWNZCYP.js";
7
7
 
8
8
  // src/index.ts
9
- import { Command as Command18 } from "commander";
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 Command18();
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.6.2").enablePositionalOptions();
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arvoretech/hub",
3
- "version": "0.6.3",
3
+ "version": "0.7.3",
4
4
  "description": "CLI for managing AI-aware multi-repository workspaces",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",