@arvoretech/hub 0.7.7 → 0.8.1

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.
@@ -121,7 +121,7 @@ async function checkAndAutoRegenerate(hubDir) {
121
121
  return;
122
122
  }
123
123
  console.log(chalk.yellow("\n Detected outdated configs, auto-regenerating..."));
124
- const { generators: generators2 } = await import("./generate-PLZD6GZN.js");
124
+ const { generators: generators2 } = await import("./generate-BYB47MCP.js");
125
125
  const generator = generators2[result.editor];
126
126
  if (!generator) {
127
127
  console.log(chalk.red(` Unknown editor '${result.editor}' in cache. Run 'hub generate' manually.`));
@@ -144,6 +144,41 @@ function stripFrontMatter(content) {
144
144
  if (match) return content.slice(match[0].length);
145
145
  return content;
146
146
  }
147
+ function parseFrontMatter(content) {
148
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
149
+ if (!match) return null;
150
+ const result = {};
151
+ for (const line of match[1].split("\n")) {
152
+ const colonIdx = line.indexOf(":");
153
+ if (colonIdx === -1) continue;
154
+ const key = line.slice(0, colonIdx).trim();
155
+ const value = line.slice(colonIdx + 1).trim();
156
+ if (key) result[key] = value;
157
+ }
158
+ return result;
159
+ }
160
+ async function readExistingMcpDisabledState(mcpJsonPath) {
161
+ const disabledState = {};
162
+ if (!existsSync2(mcpJsonPath)) return disabledState;
163
+ try {
164
+ const content = JSON.parse(await readFile3(mcpJsonPath, "utf-8"));
165
+ const servers = content.mcpServers || content.mcp || {};
166
+ for (const [name, config] of Object.entries(servers)) {
167
+ if (typeof config.disabled === "boolean") {
168
+ disabledState[name] = config.disabled;
169
+ }
170
+ }
171
+ } catch {
172
+ }
173
+ return disabledState;
174
+ }
175
+ function applyDisabledState(mcpConfig, disabledState) {
176
+ for (const [name, entry] of Object.entries(mcpConfig)) {
177
+ if (name in disabledState) {
178
+ entry.disabled = disabledState[name];
179
+ }
180
+ }
181
+ }
147
182
  async function fetchHubDocsSkill(skillsDir) {
148
183
  try {
149
184
  const res = await fetch(HUB_DOCS_URL);
@@ -1559,7 +1594,7 @@ async function generateKiro(config, hubDir) {
1559
1594
  await writeManagedFile(join3(hubDir, ".gitignore"), gitignoreLines);
1560
1595
  console.log(chalk2.green(" Generated .gitignore"));
1561
1596
  const kiroRule = buildKiroOrchestratorRule(config);
1562
- const kiroOrchestrator = buildKiroSteeringContent(kiroRule);
1597
+ const kiroOrchestrator = buildKiroSteeringContent(kiroRule, "always", { name: "orchestrator" });
1563
1598
  await writeFile2(join3(steeringDir, "orchestrator.md"), kiroOrchestrator, "utf-8");
1564
1599
  console.log(chalk2.green(" Generated .kiro/steering/orchestrator.md"));
1565
1600
  await writeFile2(join3(hubDir, "AGENTS.md"), kiroRule + "\n", "utf-8");
@@ -1571,8 +1606,36 @@ async function generateKiro(config, hubDir) {
1571
1606
  for (const file of mdFiles) {
1572
1607
  const raw = await readFile3(join3(hubSteeringDir, file), "utf-8");
1573
1608
  const content = stripFrontMatter(raw);
1574
- const kiroSteering = buildKiroSteeringContent(content);
1575
- await writeFile2(join3(steeringDir, file), kiroSteering, "utf-8");
1609
+ const destPath = join3(steeringDir, file);
1610
+ let inclusion = "always";
1611
+ let meta;
1612
+ if (existsSync2(destPath)) {
1613
+ const existingContent = await readFile3(destPath, "utf-8");
1614
+ const existingFm = parseFrontMatter(existingContent);
1615
+ if (existingFm) {
1616
+ if (existingFm.inclusion === "auto" || existingFm.inclusion === "manual" || existingFm.inclusion === "fileMatch") {
1617
+ inclusion = "auto";
1618
+ }
1619
+ if (existingFm.name || existingFm.description) {
1620
+ meta = {};
1621
+ if (existingFm.name) meta.name = existingFm.name;
1622
+ if (existingFm.description) meta.description = existingFm.description;
1623
+ }
1624
+ }
1625
+ }
1626
+ const sourceFm = parseFrontMatter(raw);
1627
+ if (sourceFm) {
1628
+ if (sourceFm.inclusion === "auto" || sourceFm.inclusion === "manual" || sourceFm.inclusion === "fileMatch") {
1629
+ inclusion = "auto";
1630
+ }
1631
+ if (sourceFm.name || sourceFm.description) {
1632
+ meta = meta || {};
1633
+ if (sourceFm.name) meta.name = sourceFm.name;
1634
+ if (sourceFm.description) meta.description = sourceFm.description;
1635
+ }
1636
+ }
1637
+ const kiroSteering = buildKiroSteeringContent(content, inclusion, meta);
1638
+ await writeFile2(destPath, kiroSteering, "utf-8");
1576
1639
  }
1577
1640
  if (mdFiles.length > 0) {
1578
1641
  console.log(chalk2.green(` Copied ${mdFiles.length} steering files to .kiro/steering/`));
@@ -1631,8 +1694,11 @@ async function generateKiro(config, hubDir) {
1631
1694
  mcpConfig[mcp.name] = buildKiroMcpEntry(mcp, mode);
1632
1695
  }
1633
1696
  }
1697
+ const mcpJsonPath = join3(settingsDir, "mcp.json");
1698
+ const disabledState = await readExistingMcpDisabledState(mcpJsonPath);
1699
+ applyDisabledState(mcpConfig, disabledState);
1634
1700
  await writeFile2(
1635
- join3(settingsDir, "mcp.json"),
1701
+ mcpJsonPath,
1636
1702
  JSON.stringify({ mcpServers: mcpConfig }, null, 2) + "\n",
1637
1703
  "utf-8"
1638
1704
  );
@@ -1812,8 +1878,21 @@ async function resolveEditor(opts) {
1812
1878
  ]);
1813
1879
  return editor;
1814
1880
  }
1815
- var generateCommand = new Command("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").action(async (opts) => {
1881
+ var generateCommand = new Command("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) => {
1816
1882
  const hubDir = process.cwd();
1883
+ if (opts.check) {
1884
+ const result = await checkOutdated(hubDir);
1885
+ if (result.reason === "no-previous-generate") {
1886
+ console.log(chalk2.yellow("No previous generate found. Run 'hub generate' first."));
1887
+ process.exit(1);
1888
+ }
1889
+ if (result.outdated) {
1890
+ console.log(chalk2.yellow("Generated configs are outdated. Run 'hub generate' to update."));
1891
+ process.exit(1);
1892
+ }
1893
+ console.log(chalk2.green("Generated configs are up to date."));
1894
+ return;
1895
+ }
1817
1896
  const config = await loadHubConfig(hubDir);
1818
1897
  if (config.memory) {
1819
1898
  const hasMemoryMcp = config.mcps?.some(
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  generateCommand,
3
3
  generators
4
- } from "./chunk-5N4KDPKV.js";
4
+ } from "./chunk-VMSQC56H.js";
5
5
  export {
6
6
  generateCommand,
7
7
  generators
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  checkAndAutoRegenerate,
4
4
  generateCommand,
5
5
  loadHubConfig
6
- } from "./chunk-5N4KDPKV.js";
6
+ } from "./chunk-VMSQC56H.js";
7
7
 
8
8
  // src/index.ts
9
9
  import { Command as Command19 } from "commander";
@@ -3021,6 +3021,34 @@ async function findUnsyncedAssets(hubDir) {
3021
3021
  } catch {
3022
3022
  }
3023
3023
  }
3024
+ const hubYamlPath = join17(hubDir, "hub.yaml");
3025
+ if (existsSync14(hubYamlPath)) {
3026
+ const hubContent = await readFile8(hubYamlPath, "utf-8");
3027
+ const hubConfig = parse2(hubContent);
3028
+ const hubMcpNames = new Set((hubConfig.mcps || []).map((m) => m.name));
3029
+ const mcpConfigPaths = [
3030
+ { path: join17(hubDir, ".cursor", "mcp.json"), source: ".cursor", key: "mcpServers" },
3031
+ { path: join17(hubDir, ".kiro", "settings", "mcp.json"), source: ".kiro", key: "mcpServers" },
3032
+ { path: join17(hubDir, ".mcp.json"), source: ".mcp.json", key: "mcpServers" },
3033
+ { path: join17(hubDir, "opencode.json"), source: "opencode.json", key: "mcp" }
3034
+ ];
3035
+ for (const { path: mcpPath, source, key } of mcpConfigPaths) {
3036
+ if (!existsSync14(mcpPath)) continue;
3037
+ try {
3038
+ const content = JSON.parse(await readFile8(mcpPath, "utf-8"));
3039
+ const servers = content[key];
3040
+ if (!servers) continue;
3041
+ for (const serverName of Object.keys(servers)) {
3042
+ if (hubMcpNames.has(serverName)) continue;
3043
+ const mcpKey = `mcp:${serverName}`;
3044
+ if (seen.has(mcpKey)) continue;
3045
+ seen.add(mcpKey);
3046
+ unsynced.push({ type: "mcp", name: serverName, source });
3047
+ }
3048
+ } catch {
3049
+ }
3050
+ }
3051
+ }
3024
3052
  return unsynced;
3025
3053
  }
3026
3054
  function stripFrontMatter(content) {
@@ -3030,6 +3058,10 @@ function stripFrontMatter(content) {
3030
3058
  }
3031
3059
  async function syncAssets(hubDir, assets) {
3032
3060
  for (const asset of assets) {
3061
+ if (asset.type === "mcp") {
3062
+ console.log(chalk18.yellow(` MCP '${asset.name}' found in ${asset.source} but not in hub.yaml \u2014 add it manually.`));
3063
+ continue;
3064
+ }
3033
3065
  if (asset.type === "skill") {
3034
3066
  const src = join17(hubDir, asset.source, "skills", asset.name);
3035
3067
  const dest = join17(hubDir, "skills", asset.name);
@@ -3070,7 +3102,7 @@ async function syncAssets(hubDir, assets) {
3070
3102
  }
3071
3103
  }
3072
3104
  }
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) => {
3105
+ var scanCommand = new Command18("scan").description("Detect git repositories not registered in hub.yaml").option("-y, --yes", "Auto-add all found repos without prompting").option("--check", "Check for unsynced assets without prompting (exit code 1 if found)").action(async (opts) => {
3074
3106
  const hubDir = process.cwd();
3075
3107
  const configPath = join17(hubDir, "hub.yaml");
3076
3108
  if (!existsSync14(configPath)) {
@@ -3079,6 +3111,23 @@ var scanCommand = new Command18("scan").description("Detect git repositories not
3079
3111
  }
3080
3112
  const content = await readFile8(configPath, "utf-8");
3081
3113
  const config = parse2(content);
3114
+ if (opts.check) {
3115
+ const unregistered2 = await findUnregisteredRepos(hubDir, config);
3116
+ const unsyncedAssets2 = await findUnsyncedAssets(hubDir);
3117
+ const total = unregistered2.length + unsyncedAssets2.length;
3118
+ if (total > 0) {
3119
+ if (unregistered2.length > 0) {
3120
+ console.log(chalk18.yellow(`Found ${unregistered2.length} unregistered repo(s): ${unregistered2.join(", ")}`));
3121
+ }
3122
+ if (unsyncedAssets2.length > 0) {
3123
+ console.log(chalk18.yellow(`Found ${unsyncedAssets2.length} unsynced asset(s): ${unsyncedAssets2.map((a) => a.name).join(", ")}`));
3124
+ }
3125
+ console.log(chalk18.yellow("Run 'hub scan' to sync."));
3126
+ process.exit(1);
3127
+ }
3128
+ console.log(chalk18.green("All repos and assets are synced."));
3129
+ return;
3130
+ }
3082
3131
  let hasChanges = false;
3083
3132
  console.log(chalk18.blue("\nScanning for unregistered repositories...\n"));
3084
3133
  const unregistered = await findUnregisteredRepos(hubDir, config);
@@ -3126,7 +3175,7 @@ var scanCommand = new Command18("scan").description("Detect git repositories not
3126
3175
  hasChanges = true;
3127
3176
  }
3128
3177
  }
3129
- console.log(chalk18.blue("\nScanning for unsynced skills, agents, and steering...\n"));
3178
+ console.log(chalk18.blue("\nScanning for unsynced skills, agents, steering, and MCPs...\n"));
3130
3179
  const unsyncedAssets = await findUnsyncedAssets(hubDir);
3131
3180
  if (unsyncedAssets.length === 0) {
3132
3181
  console.log(chalk18.green("All assets are synced."));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arvoretech/hub",
3
- "version": "0.7.7",
3
+ "version": "0.8.1",
4
4
  "description": "CLI for managing AI-aware multi-repository workspaces",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",