@arvoretech/hub 0.8.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,9 @@
1
1
  // src/commands/generate.ts
2
2
  import { Command } from "commander";
3
3
  import { existsSync as existsSync2 } from "fs";
4
- import { mkdir as mkdir2, writeFile as writeFile2, readdir as readdir2, copyFile, readFile as readFile3, cp } from "fs/promises";
5
- import { join as join3, resolve } from "path";
6
- import chalk2 from "chalk";
4
+ import { mkdir as mkdir3, writeFile as writeFile3, readdir as readdir2, copyFile, readFile as readFile4, cp } from "fs/promises";
5
+ import { join as join4, resolve as resolve2 } from "path";
6
+ import chalk3 from "chalk";
7
7
  import inquirer from "inquirer";
8
8
 
9
9
  // src/core/hub-config.ts
@@ -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-BYB47MCP.js");
124
+ const { generators: generators2 } = await import("./generate-H4YM3WTV.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.`));
@@ -137,8 +137,317 @@ async function checkAndAutoRegenerate(hubDir) {
137
137
  }
138
138
  }
139
139
 
140
+ // src/core/design-sources.ts
141
+ import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
142
+ import { join as join3, relative, resolve } from "path";
143
+ import chalk2 from "chalk";
144
+
145
+ // src/core/notion.ts
146
+ var NOTION_API_VERSION = "2022-06-28";
147
+ function getNotionToken() {
148
+ return process.env.NOTION_API_KEY || process.env.NOTION_TOKEN;
149
+ }
150
+ function extractPageId(pageRef) {
151
+ const cleaned = pageRef.replace(/https?:\/\/(?:www\.)?notion\.(?:so|site)\//, "");
152
+ const withoutSlug = cleaned.split("?")[0].split("#")[0];
153
+ const segments = withoutSlug.split("/");
154
+ const last = segments[segments.length - 1] || "";
155
+ const dashParts = last.split("-");
156
+ const candidate = dashParts[dashParts.length - 1];
157
+ if (candidate && /^[a-f0-9]{32}$/i.test(candidate)) {
158
+ return formatPageId(candidate);
159
+ }
160
+ const noDashes = pageRef.replace(/-/g, "");
161
+ const hexMatch = noDashes.match(/([a-f0-9]{32})/i);
162
+ if (hexMatch) return formatPageId(hexMatch[1]);
163
+ return pageRef;
164
+ }
165
+ function formatPageId(raw) {
166
+ return `${raw.slice(0, 8)}-${raw.slice(8, 12)}-${raw.slice(12, 16)}-${raw.slice(16, 20)}-${raw.slice(20)}`;
167
+ }
168
+ function richTextToMarkdown(richTexts) {
169
+ return richTexts.map((rt) => {
170
+ let text = rt.plain_text;
171
+ if (!text) return "";
172
+ if (rt.annotations?.code) text = `\`${text}\``;
173
+ if (rt.annotations?.bold) text = `**${text}**`;
174
+ if (rt.annotations?.italic) text = `*${text}*`;
175
+ if (rt.annotations?.strikethrough) text = `~~${text}~~`;
176
+ if (rt.href) text = `[${text}](${rt.href})`;
177
+ return text;
178
+ }).join("");
179
+ }
180
+ async function notionFetch(endpoint, token) {
181
+ const res = await fetch(`https://api.notion.com/v1${endpoint}`, {
182
+ headers: {
183
+ Authorization: `Bearer ${token}`,
184
+ "Notion-Version": NOTION_API_VERSION
185
+ }
186
+ });
187
+ if (!res.ok) {
188
+ throw new Error(`Notion API ${res.status}: ${res.statusText}`);
189
+ }
190
+ return res.json();
191
+ }
192
+ async function fetchAllBlocks(pageId, token) {
193
+ const blocks = [];
194
+ let cursor;
195
+ do {
196
+ const qs = cursor ? `?start_cursor=${cursor}` : "";
197
+ const data = await notionFetch(`/blocks/${pageId}/children${qs}`, token);
198
+ blocks.push(...data.results);
199
+ cursor = data.has_more ? data.next_cursor ?? void 0 : void 0;
200
+ } while (cursor);
201
+ return blocks;
202
+ }
203
+ function blockToMarkdown(block, indent = "") {
204
+ const type = block.type;
205
+ const data = block[type];
206
+ if (!data) return "";
207
+ const rt = data.rich_text || [];
208
+ const text = richTextToMarkdown(rt);
209
+ switch (type) {
210
+ case "paragraph":
211
+ return text ? `${indent}${text}` : "";
212
+ case "heading_1":
213
+ return `# ${text}`;
214
+ case "heading_2":
215
+ return `## ${text}`;
216
+ case "heading_3":
217
+ return `### ${text}`;
218
+ case "bulleted_list_item":
219
+ return `${indent}- ${text}`;
220
+ case "numbered_list_item":
221
+ return `${indent}1. ${text}`;
222
+ case "to_do": {
223
+ const checked = data.checked ? "x" : " ";
224
+ return `${indent}- [${checked}] ${text}`;
225
+ }
226
+ case "toggle":
227
+ return `${indent}<details><summary>${text}</summary>`;
228
+ case "code": {
229
+ const lang = data.language || "";
230
+ return `\`\`\`${lang}
231
+ ${text}
232
+ \`\`\``;
233
+ }
234
+ case "quote":
235
+ return `> ${text}`;
236
+ case "callout":
237
+ return `> ${text}`;
238
+ case "divider":
239
+ return "---";
240
+ case "table_row": {
241
+ const cells = data.cells || [];
242
+ return `| ${cells.map((c) => richTextToMarkdown(c)).join(" | ")} |`;
243
+ }
244
+ case "image": {
245
+ const imgData = data;
246
+ const url = imgData.file?.url || imgData.external?.url || "";
247
+ const caption = data.caption || [];
248
+ const alt = richTextToMarkdown(caption) || "image";
249
+ return url ? `![${alt}](${url})` : "";
250
+ }
251
+ case "bookmark": {
252
+ const bUrl = data.url;
253
+ return bUrl ? `[${bUrl}](${bUrl})` : "";
254
+ }
255
+ default:
256
+ return text || "";
257
+ }
258
+ }
259
+ async function blocksToMarkdown(blocks, token, indent = "") {
260
+ const lines = [];
261
+ for (const block of blocks) {
262
+ const type = block.type;
263
+ if (type === "table") {
264
+ if (block.has_children) {
265
+ const children = await fetchAllBlocks(block.id, token);
266
+ const rows = children.map((child) => blockToMarkdown(child, indent));
267
+ if (rows.length > 0) {
268
+ lines.push(rows[0]);
269
+ const colCount = (rows[0].match(/\|/g)?.length || 2) - 1;
270
+ lines.push(`|${" --- |".repeat(colCount)}`);
271
+ lines.push(...rows.slice(1));
272
+ }
273
+ }
274
+ lines.push("");
275
+ continue;
276
+ }
277
+ const line = blockToMarkdown(block, indent);
278
+ if (line !== void 0) lines.push(line);
279
+ if (block.has_children && type !== "table") {
280
+ const children = await fetchAllBlocks(block.id, token);
281
+ const childMd = await blocksToMarkdown(children, token, indent + " ");
282
+ if (childMd) lines.push(childMd);
283
+ if (type === "toggle") lines.push("</details>");
284
+ } else if (type === "toggle") {
285
+ lines.push("</details>");
286
+ }
287
+ }
288
+ return lines.join("\n");
289
+ }
290
+ async function fetchNotionPageAsMarkdown(pageRef) {
291
+ const token = getNotionToken();
292
+ if (!token) {
293
+ throw new Error("NOTION_API_KEY or NOTION_TOKEN env var is required to fetch Notion pages");
294
+ }
295
+ const pageId = extractPageId(pageRef);
296
+ const page = await notionFetch(`/pages/${pageId}`, token);
297
+ let title = "";
298
+ for (const prop of Object.values(page.properties)) {
299
+ if (prop.title?.length) {
300
+ title = prop.title.map((t) => t.plain_text).join("");
301
+ break;
302
+ }
303
+ }
304
+ const blocks = await fetchAllBlocks(pageId, token);
305
+ const content = await blocksToMarkdown(blocks, token);
306
+ return { title: title || "Untitled", content };
307
+ }
308
+
309
+ // src/core/design-sources.ts
310
+ async function fetchFromNotion(source) {
311
+ if (!source.notion_page) throw new Error(`No notion_page for source: ${source.name}`);
312
+ const { content } = await fetchNotionPageAsMarkdown(source.notion_page);
313
+ return content;
314
+ }
315
+ async function fetchFromUrl(source) {
316
+ if (!source.url) throw new Error(`No url for source: ${source.name}`);
317
+ const res = await fetch(source.url, { signal: AbortSignal.timeout(3e4) });
318
+ if (!res.ok) throw new Error(`Failed to fetch ${source.url}: ${res.status}`);
319
+ return res.text();
320
+ }
321
+ async function fetchFromPath(source, hubDir) {
322
+ if (!source.path) throw new Error(`No path for source: ${source.name}`);
323
+ const fullPath = resolve(hubDir, source.path);
324
+ const rel = relative(hubDir, fullPath);
325
+ if (rel.startsWith("..") || resolve(fullPath) === fullPath && !fullPath.startsWith(hubDir)) {
326
+ throw new Error(`Path "${source.path}" escapes the workspace for source: ${source.name}`);
327
+ }
328
+ return readFile3(fullPath, "utf-8");
329
+ }
330
+ async function fetchSourceContent(source, hubDir) {
331
+ if (source.notion_page) return fetchFromNotion(source);
332
+ if (source.url) return fetchFromUrl(source);
333
+ if (source.path) return fetchFromPath(source, hubDir);
334
+ throw new Error(`Source "${source.name}" has no notion_page, url, or path`);
335
+ }
336
+ function buildSkillContent(source) {
337
+ const triggers = source.triggers?.length ? source.triggers : [source.name.replace(/-/g, " ")];
338
+ const parts = [
339
+ "---",
340
+ `name: ${source.name}`,
341
+ `description: ${source.instructions || `Design source: ${source.name}`}`,
342
+ `triggers: [${triggers.join(", ")}]`,
343
+ "---",
344
+ ""
345
+ ];
346
+ if (source.instructions) {
347
+ parts.push(source.instructions, "");
348
+ }
349
+ parts.push(source.content);
350
+ return parts.join("\n");
351
+ }
352
+ function buildSteeringContent(source) {
353
+ const parts = [];
354
+ if (source.instructions) {
355
+ parts.push(source.instructions, "");
356
+ }
357
+ parts.push(source.content);
358
+ return parts.join("\n");
359
+ }
360
+ async function fetchRemoteSources(sources, hubDir, skillsDir, steeringDir) {
361
+ let skillCount = 0;
362
+ let steeringCount = 0;
363
+ const errors = [];
364
+ for (const source of sources) {
365
+ try {
366
+ const rawContent = await fetchSourceContent(source, hubDir);
367
+ const fetched = {
368
+ name: source.name,
369
+ type: source.type,
370
+ content: rawContent,
371
+ triggers: source.triggers,
372
+ instructions: source.instructions
373
+ };
374
+ if (source.type === "skill") {
375
+ const skillDir = join3(skillsDir, source.name);
376
+ await mkdir2(skillDir, { recursive: true });
377
+ const skillContent = buildSkillContent(fetched);
378
+ await writeFile2(join3(skillDir, "SKILL.md"), skillContent, "utf-8");
379
+ skillCount++;
380
+ console.log(chalk2.green(` \u2713 ${source.name} (skill)`));
381
+ } else {
382
+ await mkdir2(steeringDir, { recursive: true });
383
+ const steeringContent = buildSteeringContent(fetched);
384
+ await writeFile2(join3(steeringDir, `${source.name}.md`), steeringContent, "utf-8");
385
+ steeringCount++;
386
+ console.log(chalk2.green(` \u2713 ${source.name} (steering)`));
387
+ }
388
+ } catch (err) {
389
+ const msg = err instanceof Error ? err.message : String(err);
390
+ errors.push(`${source.name}: ${msg}`);
391
+ console.log(chalk2.yellow(` \u2717 ${source.name}: ${msg}`));
392
+ }
393
+ }
394
+ return { skills: skillCount, steering: steeringCount, errors };
395
+ }
396
+
140
397
  // src/commands/generate.ts
141
398
  var HUB_DOCS_URL = "https://hub.arvore.com.br/llms-full.txt";
399
+ async function syncRemoteSources(config, hubDir, skillsDir, steeringDir) {
400
+ if (!config.remote_sources?.length) return;
401
+ console.log(chalk3.blue(" Syncing remote sources..."));
402
+ const result = await fetchRemoteSources(config.remote_sources, hubDir, skillsDir, steeringDir);
403
+ if (result.skills > 0 || result.steering > 0) {
404
+ console.log(chalk3.green(` Synced ${result.skills} skill(s) and ${result.steering} steering file(s) from remote sources`));
405
+ }
406
+ if (result.errors.length > 0) {
407
+ console.log(chalk3.yellow(` ${result.errors.length} remote source(s) failed`));
408
+ }
409
+ }
410
+ function buildDesignSection(config) {
411
+ const design = config.design;
412
+ if (!design) return null;
413
+ const hasContent = design.skills?.length || design.libraries?.length || design.icons || design.instructions;
414
+ if (!hasContent) return null;
415
+ const parts = [];
416
+ parts.push(`
417
+ ## Design System`);
418
+ if (design.instructions) {
419
+ parts.push(`
420
+ ${design.instructions.trim()}`);
421
+ }
422
+ if (design.skills?.length) {
423
+ parts.push(`
424
+ ### Design Skills
425
+ `);
426
+ parts.push(`The following skills contain design guidelines and should be consulted when working on UI:`);
427
+ for (const skill of design.skills) {
428
+ parts.push(`- \`${skill}\``);
429
+ }
430
+ }
431
+ if (design.libraries?.length) {
432
+ parts.push(`
433
+ ### UI Libraries
434
+ `);
435
+ for (const lib of design.libraries) {
436
+ const refs = [];
437
+ if (lib.mcp) refs.push(`docs via \`${lib.mcp}\` MCP`);
438
+ if (lib.url) refs.push(`[docs](${lib.url})`);
439
+ if (lib.path) refs.push(`local docs at \`${lib.path}\``);
440
+ parts.push(`- **${lib.name}**${refs.length ? ` \u2014 ${refs.join(", ")}` : ""}`);
441
+ }
442
+ }
443
+ if (design.icons) {
444
+ parts.push(`
445
+ ### Icons
446
+ `);
447
+ parts.push(`Icon library: **${design.icons}**. Always use this library for icons.`);
448
+ }
449
+ return parts.join("\n");
450
+ }
142
451
  function stripFrontMatter(content) {
143
452
  const match = content.match(/^---\n[\s\S]*?\n---\n*/);
144
453
  if (match) return content.slice(match[0].length);
@@ -161,7 +470,7 @@ async function readExistingMcpDisabledState(mcpJsonPath) {
161
470
  const disabledState = {};
162
471
  if (!existsSync2(mcpJsonPath)) return disabledState;
163
472
  try {
164
- const content = JSON.parse(await readFile3(mcpJsonPath, "utf-8"));
473
+ const content = JSON.parse(await readFile4(mcpJsonPath, "utf-8"));
165
474
  const servers = content.mcpServers || content.mcp || {};
166
475
  for (const [name, config] of Object.entries(servers)) {
167
476
  if (typeof config.disabled === "boolean") {
@@ -183,12 +492,12 @@ async function fetchHubDocsSkill(skillsDir) {
183
492
  try {
184
493
  const res = await fetch(HUB_DOCS_URL);
185
494
  if (!res.ok) {
186
- console.log(chalk2.yellow(` Could not fetch hub docs (${res.status}), skipping hub-docs skill`));
495
+ console.log(chalk3.yellow(` Could not fetch hub docs (${res.status}), skipping hub-docs skill`));
187
496
  return;
188
497
  }
189
498
  const content = await res.text();
190
- const hubSkillDir = join3(skillsDir, "hub-docs");
191
- await mkdir2(hubSkillDir, { recursive: true });
499
+ const hubSkillDir = join4(skillsDir, "hub-docs");
500
+ await mkdir3(hubSkillDir, { recursive: true });
192
501
  const skillContent = `---
193
502
  name: hub-docs
194
503
  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.
@@ -196,10 +505,10 @@ triggers: [hub, rhm, hub.yaml, generate, scan, setup, orchestrator, multi-repo,
196
505
  ---
197
506
 
198
507
  ${content}`;
199
- await writeFile2(join3(hubSkillDir, "SKILL.md"), skillContent, "utf-8");
200
- console.log(chalk2.green(" Fetched hub-docs skill from hub.arvore.com.br"));
508
+ await writeFile3(join4(hubSkillDir, "SKILL.md"), skillContent, "utf-8");
509
+ console.log(chalk3.green(" Fetched hub-docs skill from hub.arvore.com.br"));
201
510
  } catch {
202
- console.log(chalk2.yellow(` Could not fetch hub docs, skipping hub-docs skill`));
511
+ console.log(chalk3.yellow(` Could not fetch hub docs, skipping hub-docs skill`));
203
512
  }
204
513
  }
205
514
  var HUB_MARKER_START = "# >>> hub-managed (do not edit this section)";
@@ -271,65 +580,65 @@ function buildClaudeHooks(hooks) {
271
580
  return claudeHooks;
272
581
  }
273
582
  async function generateEditorCommands(config, hubDir, targetDir, editorName) {
274
- const commandsDir = join3(targetDir, "commands");
583
+ const commandsDir = join4(targetDir, "commands");
275
584
  let count = 0;
276
585
  if (config.commands_dir) {
277
- const srcDir = resolve(hubDir, config.commands_dir);
586
+ const srcDir = resolve2(hubDir, config.commands_dir);
278
587
  try {
279
588
  const files = await readdir2(srcDir);
280
589
  const mdFiles = files.filter((f) => f.endsWith(".md"));
281
590
  if (mdFiles.length > 0) {
282
- await mkdir2(commandsDir, { recursive: true });
591
+ await mkdir3(commandsDir, { recursive: true });
283
592
  for (const file of mdFiles) {
284
- await copyFile(join3(srcDir, file), join3(commandsDir, file));
593
+ await copyFile(join4(srcDir, file), join4(commandsDir, file));
285
594
  count++;
286
595
  }
287
596
  }
288
597
  } catch {
289
- console.log(chalk2.yellow(` Commands directory ${config.commands_dir} not found, skipping`));
598
+ console.log(chalk3.yellow(` Commands directory ${config.commands_dir} not found, skipping`));
290
599
  }
291
600
  }
292
601
  if (config.commands) {
293
- await mkdir2(commandsDir, { recursive: true });
602
+ await mkdir3(commandsDir, { recursive: true });
294
603
  for (const [name, filePath] of Object.entries(config.commands)) {
295
- const src = resolve(hubDir, filePath);
296
- const dest = join3(commandsDir, name.endsWith(".md") ? name : `${name}.md`);
604
+ const src = resolve2(hubDir, filePath);
605
+ const dest = join4(commandsDir, name.endsWith(".md") ? name : `${name}.md`);
297
606
  try {
298
607
  await copyFile(src, dest);
299
608
  count++;
300
609
  } catch {
301
- console.log(chalk2.yellow(` Command file ${filePath} not found, skipping`));
610
+ console.log(chalk3.yellow(` Command file ${filePath} not found, skipping`));
302
611
  }
303
612
  }
304
613
  }
305
614
  if (count > 0) {
306
- console.log(chalk2.green(` Copied ${count} commands to ${editorName}`));
615
+ console.log(chalk3.green(` Copied ${count} commands to ${editorName}`));
307
616
  }
308
617
  }
309
618
  async function writeManagedFile(filePath, managedLines) {
310
619
  const managedBlock = [HUB_MARKER_START, ...managedLines, HUB_MARKER_END].join("\n");
311
620
  if (existsSync2(filePath)) {
312
- const existing = await readFile3(filePath, "utf-8");
621
+ const existing = await readFile4(filePath, "utf-8");
313
622
  const startIdx = existing.indexOf(HUB_MARKER_START);
314
623
  const endIdx = existing.indexOf(HUB_MARKER_END);
315
624
  if (startIdx !== -1 && endIdx !== -1) {
316
625
  const before = existing.substring(0, startIdx);
317
626
  const after = existing.substring(endIdx + HUB_MARKER_END.length);
318
- await writeFile2(filePath, before + managedBlock + after, "utf-8");
627
+ await writeFile3(filePath, before + managedBlock + after, "utf-8");
319
628
  return;
320
629
  }
321
- await writeFile2(filePath, managedBlock + "\n\n" + existing, "utf-8");
630
+ await writeFile3(filePath, managedBlock + "\n\n" + existing, "utf-8");
322
631
  return;
323
632
  }
324
- await writeFile2(filePath, managedBlock + "\n", "utf-8");
633
+ await writeFile3(filePath, managedBlock + "\n", "utf-8");
325
634
  }
326
635
  async function generateCursor(config, hubDir) {
327
- const cursorDir = join3(hubDir, ".cursor");
328
- await mkdir2(join3(cursorDir, "rules"), { recursive: true });
329
- await mkdir2(join3(cursorDir, "agents"), { recursive: true });
636
+ const cursorDir = join4(hubDir, ".cursor");
637
+ await mkdir3(join4(cursorDir, "rules"), { recursive: true });
638
+ await mkdir3(join4(cursorDir, "agents"), { recursive: true });
330
639
  const gitignoreLines = buildGitignoreLines(config);
331
- await writeManagedFile(join3(hubDir, ".gitignore"), gitignoreLines);
332
- console.log(chalk2.green(" Generated .gitignore"));
640
+ await writeManagedFile(join4(hubDir, ".gitignore"), gitignoreLines);
641
+ console.log(chalk3.green(" Generated .gitignore"));
333
642
  const cursorignoreLines = [
334
643
  "# Re-include repositories for AI context"
335
644
  ];
@@ -338,8 +647,8 @@ async function generateCursor(config, hubDir) {
338
647
  cursorignoreLines.push(`!${repoDir}/`);
339
648
  }
340
649
  cursorignoreLines.push("", "# Re-include tasks for agent collaboration", "!tasks/");
341
- await writeManagedFile(join3(hubDir, ".cursorignore"), cursorignoreLines);
342
- console.log(chalk2.green(" Generated .cursorignore"));
650
+ await writeManagedFile(join4(hubDir, ".cursorignore"), cursorignoreLines);
651
+ console.log(chalk3.green(" Generated .cursorignore"));
343
652
  if (config.mcps?.length) {
344
653
  const mcpConfig = {};
345
654
  const upstreamSet = getUpstreamNames(config.mcps);
@@ -351,22 +660,22 @@ async function generateCursor(config, hubDir) {
351
660
  mcpConfig[mcp.name] = buildCursorMcpEntry(mcp);
352
661
  }
353
662
  }
354
- await writeFile2(
355
- join3(cursorDir, "mcp.json"),
663
+ await writeFile3(
664
+ join4(cursorDir, "mcp.json"),
356
665
  JSON.stringify({ mcpServers: mcpConfig }, null, 2) + "\n",
357
666
  "utf-8"
358
667
  );
359
- console.log(chalk2.green(" Generated .cursor/mcp.json"));
668
+ console.log(chalk3.green(" Generated .cursor/mcp.json"));
360
669
  }
361
670
  const orchestratorRule = buildOrchestratorRule(config);
362
- await writeFile2(join3(cursorDir, "rules", "orchestrator.mdc"), orchestratorRule, "utf-8");
363
- console.log(chalk2.green(" Generated .cursor/rules/orchestrator.mdc"));
364
- const hubSteeringDirCursor = resolve(hubDir, "steering");
671
+ await writeFile3(join4(cursorDir, "rules", "orchestrator.mdc"), orchestratorRule, "utf-8");
672
+ console.log(chalk3.green(" Generated .cursor/rules/orchestrator.mdc"));
673
+ const hubSteeringDirCursor = resolve2(hubDir, "steering");
365
674
  try {
366
675
  const steeringFiles = await readdir2(hubSteeringDirCursor);
367
676
  const mdFiles = steeringFiles.filter((f) => f.endsWith(".md"));
368
677
  for (const file of mdFiles) {
369
- const raw = await readFile3(join3(hubSteeringDirCursor, file), "utf-8");
678
+ const raw = await readFile4(join4(hubSteeringDirCursor, file), "utf-8");
370
679
  const content = stripFrontMatter(raw);
371
680
  const mdcName = file.replace(/\.md$/, ".mdc");
372
681
  const mdcContent = `---
@@ -375,58 +684,59 @@ alwaysApply: true
375
684
  ---
376
685
 
377
686
  ${content}`;
378
- await writeFile2(join3(cursorDir, "rules", mdcName), mdcContent, "utf-8");
687
+ await writeFile3(join4(cursorDir, "rules", mdcName), mdcContent, "utf-8");
379
688
  }
380
689
  if (mdFiles.length > 0) {
381
- console.log(chalk2.green(` Copied ${mdFiles.length} steering files to .cursor/rules/`));
690
+ console.log(chalk3.green(` Copied ${mdFiles.length} steering files to .cursor/rules/`));
382
691
  }
383
692
  } catch {
384
693
  }
385
- const agentsDir = resolve(hubDir, "agents");
694
+ const agentsDir = resolve2(hubDir, "agents");
386
695
  try {
387
696
  const agentFiles = await readdir2(agentsDir);
388
697
  const mdFiles = agentFiles.filter((f) => f.endsWith(".md"));
389
698
  for (const file of mdFiles) {
390
- await copyFile(join3(agentsDir, file), join3(cursorDir, "agents", file));
699
+ await copyFile(join4(agentsDir, file), join4(cursorDir, "agents", file));
391
700
  }
392
- console.log(chalk2.green(` Copied ${mdFiles.length} agent definitions`));
701
+ console.log(chalk3.green(` Copied ${mdFiles.length} agent definitions`));
393
702
  } catch {
394
- console.log(chalk2.yellow(" No agents/ directory found, skipping agent copy"));
703
+ console.log(chalk3.yellow(" No agents/ directory found, skipping agent copy"));
395
704
  }
396
- const skillsDir = resolve(hubDir, "skills");
705
+ const skillsDir = resolve2(hubDir, "skills");
397
706
  try {
398
707
  const skillFolders = await readdir2(skillsDir);
399
- const cursorSkillsDir = join3(cursorDir, "skills");
400
- await mkdir2(cursorSkillsDir, { recursive: true });
708
+ const cursorSkillsDir = join4(cursorDir, "skills");
709
+ await mkdir3(cursorSkillsDir, { recursive: true });
401
710
  let count = 0;
402
711
  for (const folder of skillFolders) {
403
- const skillFile = join3(skillsDir, folder, "SKILL.md");
712
+ const skillFile = join4(skillsDir, folder, "SKILL.md");
404
713
  try {
405
- await readFile3(skillFile);
406
- const srcDir = join3(skillsDir, folder);
407
- const targetDir = join3(cursorSkillsDir, folder);
714
+ await readFile4(skillFile);
715
+ const srcDir = join4(skillsDir, folder);
716
+ const targetDir = join4(cursorSkillsDir, folder);
408
717
  await cp(srcDir, targetDir, { recursive: true });
409
718
  count++;
410
719
  } catch {
411
720
  }
412
721
  }
413
722
  if (count > 0) {
414
- console.log(chalk2.green(` Copied ${count} skills`));
723
+ console.log(chalk3.green(` Copied ${count} skills`));
415
724
  }
416
725
  } catch {
417
726
  }
418
- const cursorSkillsDirForDocs = join3(cursorDir, "skills");
419
- await mkdir2(cursorSkillsDirForDocs, { recursive: true });
727
+ const cursorSkillsDirForDocs = join4(cursorDir, "skills");
728
+ await mkdir3(cursorSkillsDirForDocs, { recursive: true });
420
729
  await fetchHubDocsSkill(cursorSkillsDirForDocs);
730
+ await syncRemoteSources(config, hubDir, join4(cursorDir, "skills"), join4(cursorDir, "rules"));
421
731
  if (config.hooks) {
422
732
  const cursorHooks = buildCursorHooks(config.hooks);
423
733
  if (cursorHooks) {
424
- await writeFile2(
425
- join3(cursorDir, "hooks.json"),
734
+ await writeFile3(
735
+ join4(cursorDir, "hooks.json"),
426
736
  JSON.stringify(cursorHooks, null, 2) + "\n",
427
737
  "utf-8"
428
738
  );
429
- console.log(chalk2.green(" Generated .cursor/hooks.json"));
739
+ console.log(chalk3.green(" Generated .cursor/hooks.json"));
430
740
  }
431
741
  }
432
742
  await generateEditorCommands(config, hubDir, cursorDir, ".cursor/commands/");
@@ -786,6 +1096,8 @@ This workspace has a team memory knowledge base available via the \`team-memory\
786
1096
 
787
1097
  Available tools: \`search_memories\`, \`get_memory\`, \`add_memory\`, \`list_memories\`, \`archive_memory\`, \`remove_memory\`.`);
788
1098
  }
1099
+ const designSectionOpenCode = buildDesignSection(config);
1100
+ if (designSectionOpenCode) sections.push(designSectionOpenCode);
789
1101
  sections.push(`
790
1102
  ## Troubleshooting and Debugging
791
1103
 
@@ -886,29 +1198,29 @@ If any validation agent leaves comments requiring fixes, call the relevant codin
886
1198
  return parts.join("\n");
887
1199
  }
888
1200
  async function generateOpenCode(config, hubDir) {
889
- const opencodeDir = join3(hubDir, ".opencode");
890
- await mkdir2(join3(opencodeDir, "agents"), { recursive: true });
891
- await mkdir2(join3(opencodeDir, "rules"), { recursive: true });
892
- await mkdir2(join3(opencodeDir, "skills"), { recursive: true });
893
- await mkdir2(join3(opencodeDir, "commands"), { recursive: true });
894
- await mkdir2(join3(opencodeDir, "plugins"), { recursive: true });
1201
+ const opencodeDir = join4(hubDir, ".opencode");
1202
+ await mkdir3(join4(opencodeDir, "agents"), { recursive: true });
1203
+ await mkdir3(join4(opencodeDir, "rules"), { recursive: true });
1204
+ await mkdir3(join4(opencodeDir, "skills"), { recursive: true });
1205
+ await mkdir3(join4(opencodeDir, "commands"), { recursive: true });
1206
+ await mkdir3(join4(opencodeDir, "plugins"), { recursive: true });
895
1207
  const gitignoreLines = buildGitignoreLines(config);
896
- await writeManagedFile(join3(hubDir, ".gitignore"), gitignoreLines);
897
- console.log(chalk2.green(" Generated .gitignore"));
1208
+ await writeManagedFile(join4(hubDir, ".gitignore"), gitignoreLines);
1209
+ console.log(chalk3.green(" Generated .gitignore"));
898
1210
  const orchestratorRule = buildOpenCodeOrchestratorRule(config);
899
- await writeFile2(join3(opencodeDir, "rules", "orchestrator.md"), orchestratorRule + "\n", "utf-8");
900
- console.log(chalk2.green(" Generated .opencode/rules/orchestrator.md"));
901
- const hubSteeringDirOC = resolve(hubDir, "steering");
1211
+ await writeFile3(join4(opencodeDir, "rules", "orchestrator.md"), orchestratorRule + "\n", "utf-8");
1212
+ console.log(chalk3.green(" Generated .opencode/rules/orchestrator.md"));
1213
+ const hubSteeringDirOC = resolve2(hubDir, "steering");
902
1214
  try {
903
1215
  const steeringFiles = await readdir2(hubSteeringDirOC);
904
1216
  const mdFiles = steeringFiles.filter((f) => f.endsWith(".md"));
905
1217
  for (const file of mdFiles) {
906
- const raw = await readFile3(join3(hubSteeringDirOC, file), "utf-8");
1218
+ const raw = await readFile4(join4(hubSteeringDirOC, file), "utf-8");
907
1219
  const content = stripFrontMatter(raw);
908
- await writeFile2(join3(opencodeDir, "rules", file), content, "utf-8");
1220
+ await writeFile3(join4(opencodeDir, "rules", file), content, "utf-8");
909
1221
  }
910
1222
  if (mdFiles.length > 0) {
911
- console.log(chalk2.green(` Copied ${mdFiles.length} steering files to .opencode/rules/`));
1223
+ console.log(chalk3.green(` Copied ${mdFiles.length} steering files to .opencode/rules/`));
912
1224
  }
913
1225
  } catch {
914
1226
  }
@@ -929,51 +1241,52 @@ async function generateOpenCode(config, hubDir) {
929
1241
  opencodeConfig.mcp = mcpConfig;
930
1242
  }
931
1243
  opencodeConfig.instructions = [".opencode/rules/*.md"];
932
- await writeFile2(
933
- join3(hubDir, "opencode.json"),
1244
+ await writeFile3(
1245
+ join4(hubDir, "opencode.json"),
934
1246
  JSON.stringify(opencodeConfig, null, 2) + "\n",
935
1247
  "utf-8"
936
1248
  );
937
- console.log(chalk2.green(" Generated opencode.json"));
938
- const agentsDir = resolve(hubDir, "agents");
1249
+ console.log(chalk3.green(" Generated opencode.json"));
1250
+ const agentsDir = resolve2(hubDir, "agents");
939
1251
  try {
940
1252
  const agentFiles = await readdir2(agentsDir);
941
1253
  const mdFiles = agentFiles.filter((f) => f.endsWith(".md"));
942
1254
  for (const file of mdFiles) {
943
- const content = await readFile3(join3(agentsDir, file), "utf-8");
1255
+ const content = await readFile4(join4(agentsDir, file), "utf-8");
944
1256
  const agentName = file.replace(/\.md$/, "");
945
1257
  const converted = buildOpenCodeAgentMarkdown(agentName, content);
946
- await writeFile2(join3(opencodeDir, "agents", file), converted, "utf-8");
1258
+ await writeFile3(join4(opencodeDir, "agents", file), converted, "utf-8");
947
1259
  }
948
- console.log(chalk2.green(` Copied ${mdFiles.length} agents to .opencode/agents/`));
1260
+ console.log(chalk3.green(` Copied ${mdFiles.length} agents to .opencode/agents/`));
949
1261
  } catch {
950
- console.log(chalk2.yellow(" No agents/ directory found, skipping agent copy"));
1262
+ console.log(chalk3.yellow(" No agents/ directory found, skipping agent copy"));
951
1263
  }
952
- const skillsDir = resolve(hubDir, "skills");
1264
+ const skillsDir = resolve2(hubDir, "skills");
953
1265
  try {
954
1266
  const skillFolders = await readdir2(skillsDir);
955
1267
  let count = 0;
956
1268
  for (const folder of skillFolders) {
957
- const skillFile = join3(skillsDir, folder, "SKILL.md");
1269
+ const skillFile = join4(skillsDir, folder, "SKILL.md");
958
1270
  try {
959
- await readFile3(skillFile);
960
- await cp(join3(skillsDir, folder), join3(opencodeDir, "skills", folder), { recursive: true });
1271
+ await readFile4(skillFile);
1272
+ await cp(join4(skillsDir, folder), join4(opencodeDir, "skills", folder), { recursive: true });
961
1273
  count++;
962
1274
  } catch {
963
1275
  }
964
1276
  }
965
1277
  if (count > 0) {
966
- console.log(chalk2.green(` Copied ${count} skills to .opencode/skills/`));
1278
+ console.log(chalk3.green(` Copied ${count} skills to .opencode/skills/`));
967
1279
  }
968
1280
  } catch {
969
1281
  }
970
- await fetchHubDocsSkill(join3(opencodeDir, "skills"));
1282
+ await fetchHubDocsSkill(join4(opencodeDir, "skills"));
1283
+ await syncRemoteSources(config, hubDir, join4(opencodeDir, "skills"), join4(opencodeDir, "rules"));
971
1284
  await generateEditorCommands(config, hubDir, opencodeDir, ".opencode/commands/");
972
1285
  if (config.hooks) {
973
1286
  const plugin = buildOpenCodeHooksPlugin(config.hooks);
974
1287
  if (plugin) {
975
- await writeFile2(join3(opencodeDir, "plugins", "hub-hooks.js"), plugin, "utf-8");
976
- console.log(chalk2.green(" Generated .opencode/plugins/hub-hooks.js"));
1288
+ await writeFile3(join4(opencodeDir, "plugins", "hub-hooks.js"), plugin, "utf-8");
1289
+ console.log(chalk3.green(" Generated .opencode/plugins/hub-hooks.js"));
977
1290
  }
978
1291
  }
979
1292
  await generateVSCodeSettings(config, hubDir);
@@ -1076,6 +1389,8 @@ This workspace has a team memory knowledge base available via the \`team-memory\
1076
1389
 
1077
1390
  Available tools: \`search_memories\`, \`get_memory\`, \`add_memory\`, \`list_memories\`, \`archive_memory\`, \`remove_memory\`.`);
1078
1391
  }
1392
+ const designSectionKiro = buildDesignSection(config);
1393
+ if (designSectionKiro) sections.push(designSectionKiro);
1079
1394
  sections.push(`
1080
1395
  ## Troubleshooting and Debugging
1081
1396
 
@@ -1262,6 +1577,8 @@ This workspace has a team memory knowledge base available via the \`team-memory\
1262
1577
 
1263
1578
  Available tools: \`search_memories\`, \`get_memory\`, \`add_memory\`, \`list_memories\`, \`archive_memory\`, \`remove_memory\`.`);
1264
1579
  }
1580
+ const designSectionCursor = buildDesignSection(config);
1581
+ if (designSectionCursor) sections.push(designSectionCursor);
1265
1582
  sections.push(`
1266
1583
  ## Troubleshooting and Debugging
1267
1584
 
@@ -1441,66 +1758,67 @@ function formatAction(action) {
1441
1758
  return map[action] || action;
1442
1759
  }
1443
1760
  async function generateClaudeCode(config, hubDir) {
1444
- const claudeDir = join3(hubDir, ".claude");
1445
- await mkdir2(join3(claudeDir, "agents"), { recursive: true });
1761
+ const claudeDir = join4(hubDir, ".claude");
1762
+ await mkdir3(join4(claudeDir, "agents"), { recursive: true });
1446
1763
  const orchestratorRule = buildOrchestratorRule(config);
1447
1764
  const cleanedOrchestrator = orchestratorRule.replace(/^---[\s\S]*?---\n/m, "").trim();
1448
1765
  const claudeMdSections = [];
1449
1766
  claudeMdSections.push(cleanedOrchestrator);
1450
- const agentsDir = resolve(hubDir, "agents");
1767
+ const agentsDir = resolve2(hubDir, "agents");
1451
1768
  try {
1452
1769
  const agentFiles = await readdir2(agentsDir);
1453
1770
  const mdFiles = agentFiles.filter((f) => f.endsWith(".md"));
1454
1771
  for (const file of mdFiles) {
1455
- await copyFile(join3(agentsDir, file), join3(claudeDir, "agents", file));
1772
+ await copyFile(join4(agentsDir, file), join4(claudeDir, "agents", file));
1456
1773
  }
1457
- console.log(chalk2.green(` Copied ${mdFiles.length} agents to .claude/agents/`));
1774
+ console.log(chalk3.green(` Copied ${mdFiles.length} agents to .claude/agents/`));
1458
1775
  } catch {
1459
- console.log(chalk2.yellow(" No agents/ directory found, skipping agent copy"));
1776
+ console.log(chalk3.yellow(" No agents/ directory found, skipping agent copy"));
1460
1777
  }
1461
- const skillsDir = resolve(hubDir, "skills");
1778
+ const skillsDir = resolve2(hubDir, "skills");
1462
1779
  try {
1463
1780
  const skillFolders = await readdir2(skillsDir);
1464
- const claudeSkillsDir = join3(claudeDir, "skills");
1465
- await mkdir2(claudeSkillsDir, { recursive: true });
1781
+ const claudeSkillsDir = join4(claudeDir, "skills");
1782
+ await mkdir3(claudeSkillsDir, { recursive: true });
1466
1783
  let count = 0;
1467
1784
  for (const folder of skillFolders) {
1468
- const skillFile = join3(skillsDir, folder, "SKILL.md");
1785
+ const skillFile = join4(skillsDir, folder, "SKILL.md");
1469
1786
  try {
1470
- await readFile3(skillFile);
1471
- const srcDir = join3(skillsDir, folder);
1472
- const targetDir = join3(claudeSkillsDir, folder);
1787
+ await readFile4(skillFile);
1788
+ const srcDir = join4(skillsDir, folder);
1789
+ const targetDir = join4(claudeSkillsDir, folder);
1473
1790
  await cp(srcDir, targetDir, { recursive: true });
1474
1791
  count++;
1475
1792
  } catch {
1476
1793
  }
1477
1794
  }
1478
1795
  if (count > 0) {
1479
- console.log(chalk2.green(` Copied ${count} skills to .claude/skills/`));
1796
+ console.log(chalk3.green(` Copied ${count} skills to .claude/skills/`));
1480
1797
  }
1481
1798
  } catch {
1482
1799
  }
1483
- const claudeSkillsDirForDocs = join3(claudeDir, "skills");
1484
- await mkdir2(claudeSkillsDirForDocs, { recursive: true });
1800
+ const claudeSkillsDirForDocs = join4(claudeDir, "skills");
1801
+ await mkdir3(claudeSkillsDirForDocs, { recursive: true });
1485
1802
  await fetchHubDocsSkill(claudeSkillsDirForDocs);
1486
- const hubSteeringDirClaude = resolve(hubDir, "steering");
1803
+ await syncRemoteSources(config, hubDir, join4(claudeDir, "skills"), join4(claudeDir, "steering"));
1804
+ const hubSteeringDirClaude = resolve2(hubDir, "steering");
1487
1805
  try {
1488
1806
  const steeringFiles = await readdir2(hubSteeringDirClaude);
1489
1807
  const mdFiles = steeringFiles.filter((f) => f.endsWith(".md"));
1490
1808
  for (const file of mdFiles) {
1491
- const raw = await readFile3(join3(hubSteeringDirClaude, file), "utf-8");
1809
+ const raw = await readFile4(join4(hubSteeringDirClaude, file), "utf-8");
1492
1810
  const content = stripFrontMatter(raw).trim();
1493
1811
  if (content) {
1494
1812
  claudeMdSections.push(content);
1495
1813
  }
1496
1814
  }
1497
1815
  if (mdFiles.length > 0) {
1498
- console.log(chalk2.green(` Appended ${mdFiles.length} steering files to CLAUDE.md`));
1816
+ console.log(chalk3.green(` Appended ${mdFiles.length} steering files to CLAUDE.md`));
1499
1817
  }
1500
1818
  } catch {
1501
1819
  }
1502
- await writeFile2(join3(hubDir, "CLAUDE.md"), claudeMdSections.join("\n\n"), "utf-8");
1503
- console.log(chalk2.green(" Generated CLAUDE.md"));
1820
+ await writeFile3(join4(hubDir, "CLAUDE.md"), claudeMdSections.join("\n\n"), "utf-8");
1821
+ console.log(chalk3.green(" Generated CLAUDE.md"));
1504
1822
  if (config.mcps?.length) {
1505
1823
  const mcpJson = {};
1506
1824
  const upstreamSet = getUpstreamNames(config.mcps);
@@ -1512,12 +1830,12 @@ async function generateClaudeCode(config, hubDir) {
1512
1830
  mcpJson[mcp.name] = buildClaudeCodeMcpEntry(mcp);
1513
1831
  }
1514
1832
  }
1515
- await writeFile2(
1516
- join3(hubDir, ".mcp.json"),
1833
+ await writeFile3(
1834
+ join4(hubDir, ".mcp.json"),
1517
1835
  JSON.stringify({ mcpServers: mcpJson }, null, 2) + "\n",
1518
1836
  "utf-8"
1519
1837
  );
1520
- console.log(chalk2.green(" Generated .mcp.json"));
1838
+ console.log(chalk3.green(" Generated .mcp.json"));
1521
1839
  }
1522
1840
  const mcpServerNames = config.mcps?.map((m) => m.name) || [];
1523
1841
  const claudeSettings = {
@@ -1555,22 +1873,22 @@ async function generateClaudeCode(config, hubDir) {
1555
1873
  claudeSettings.hooks = claudeHooks;
1556
1874
  }
1557
1875
  }
1558
- await writeFile2(
1559
- join3(claudeDir, "settings.json"),
1876
+ await writeFile3(
1877
+ join4(claudeDir, "settings.json"),
1560
1878
  JSON.stringify(claudeSettings, null, 2) + "\n",
1561
1879
  "utf-8"
1562
1880
  );
1563
- console.log(chalk2.green(" Generated .claude/settings.json"));
1881
+ console.log(chalk3.green(" Generated .claude/settings.json"));
1564
1882
  const gitignoreLines = buildGitignoreLines(config);
1565
- await writeManagedFile(join3(hubDir, ".gitignore"), gitignoreLines);
1566
- console.log(chalk2.green(" Generated .gitignore"));
1883
+ await writeManagedFile(join4(hubDir, ".gitignore"), gitignoreLines);
1884
+ console.log(chalk3.green(" Generated .gitignore"));
1567
1885
  }
1568
1886
  async function generateKiro(config, hubDir) {
1569
- const kiroDir = join3(hubDir, ".kiro");
1570
- const steeringDir = join3(kiroDir, "steering");
1571
- const settingsDir = join3(kiroDir, "settings");
1572
- await mkdir2(steeringDir, { recursive: true });
1573
- await mkdir2(settingsDir, { recursive: true });
1887
+ const kiroDir = join4(hubDir, ".kiro");
1888
+ const steeringDir = join4(kiroDir, "steering");
1889
+ const settingsDir = join4(kiroDir, "settings");
1890
+ await mkdir3(steeringDir, { recursive: true });
1891
+ await mkdir3(settingsDir, { recursive: true });
1574
1892
  let mode = await getKiroMode(hubDir);
1575
1893
  if (!mode) {
1576
1894
  const { kiroMode } = await inquirer.prompt([
@@ -1586,31 +1904,31 @@ async function generateKiro(config, hubDir) {
1586
1904
  ]);
1587
1905
  mode = kiroMode;
1588
1906
  await saveKiroMode(hubDir, mode);
1589
- console.log(chalk2.dim(` Saved Kiro mode: ${mode}`));
1907
+ console.log(chalk3.dim(` Saved Kiro mode: ${mode}`));
1590
1908
  } else {
1591
- console.log(chalk2.dim(` Using saved Kiro mode: ${mode}`));
1909
+ console.log(chalk3.dim(` Using saved Kiro mode: ${mode}`));
1592
1910
  }
1593
1911
  const gitignoreLines = buildGitignoreLines(config);
1594
- await writeManagedFile(join3(hubDir, ".gitignore"), gitignoreLines);
1595
- console.log(chalk2.green(" Generated .gitignore"));
1912
+ await writeManagedFile(join4(hubDir, ".gitignore"), gitignoreLines);
1913
+ console.log(chalk3.green(" Generated .gitignore"));
1596
1914
  const kiroRule = buildKiroOrchestratorRule(config);
1597
1915
  const kiroOrchestrator = buildKiroSteeringContent(kiroRule, "always", { name: "orchestrator" });
1598
- await writeFile2(join3(steeringDir, "orchestrator.md"), kiroOrchestrator, "utf-8");
1599
- console.log(chalk2.green(" Generated .kiro/steering/orchestrator.md"));
1600
- await writeFile2(join3(hubDir, "AGENTS.md"), kiroRule + "\n", "utf-8");
1601
- console.log(chalk2.green(" Generated AGENTS.md"));
1602
- const hubSteeringDir = resolve(hubDir, "steering");
1916
+ await writeFile3(join4(steeringDir, "orchestrator.md"), kiroOrchestrator, "utf-8");
1917
+ console.log(chalk3.green(" Generated .kiro/steering/orchestrator.md"));
1918
+ await writeFile3(join4(hubDir, "AGENTS.md"), kiroRule + "\n", "utf-8");
1919
+ console.log(chalk3.green(" Generated AGENTS.md"));
1920
+ const hubSteeringDir = resolve2(hubDir, "steering");
1603
1921
  try {
1604
1922
  const steeringFiles = await readdir2(hubSteeringDir);
1605
1923
  const mdFiles = steeringFiles.filter((f) => f.endsWith(".md"));
1606
1924
  for (const file of mdFiles) {
1607
- const raw = await readFile3(join3(hubSteeringDir, file), "utf-8");
1925
+ const raw = await readFile4(join4(hubSteeringDir, file), "utf-8");
1608
1926
  const content = stripFrontMatter(raw);
1609
- const destPath = join3(steeringDir, file);
1927
+ const destPath = join4(steeringDir, file);
1610
1928
  let inclusion = "always";
1611
1929
  let meta;
1612
1930
  if (existsSync2(destPath)) {
1613
- const existingContent = await readFile3(destPath, "utf-8");
1931
+ const existingContent = await readFile4(destPath, "utf-8");
1614
1932
  const existingFm = parseFrontMatter(existingContent);
1615
1933
  if (existingFm) {
1616
1934
  if (existingFm.inclusion === "auto" || existingFm.inclusion === "manual" || existingFm.inclusion === "fileMatch") {
@@ -1635,53 +1953,54 @@ async function generateKiro(config, hubDir) {
1635
1953
  }
1636
1954
  }
1637
1955
  const kiroSteering = buildKiroSteeringContent(content, inclusion, meta);
1638
- await writeFile2(destPath, kiroSteering, "utf-8");
1956
+ await writeFile3(destPath, kiroSteering, "utf-8");
1639
1957
  }
1640
1958
  if (mdFiles.length > 0) {
1641
- console.log(chalk2.green(` Copied ${mdFiles.length} steering files to .kiro/steering/`));
1959
+ console.log(chalk3.green(` Copied ${mdFiles.length} steering files to .kiro/steering/`));
1642
1960
  }
1643
1961
  } catch {
1644
1962
  }
1645
- const agentsDir = resolve(hubDir, "agents");
1963
+ const agentsDir = resolve2(hubDir, "agents");
1646
1964
  try {
1647
- const kiroAgentsDir = join3(kiroDir, "agents");
1648
- await mkdir2(kiroAgentsDir, { recursive: true });
1965
+ const kiroAgentsDir = join4(kiroDir, "agents");
1966
+ await mkdir3(kiroAgentsDir, { recursive: true });
1649
1967
  const agentFiles = await readdir2(agentsDir);
1650
1968
  const mdFiles = agentFiles.filter((f) => f.endsWith(".md"));
1651
1969
  for (const file of mdFiles) {
1652
- const agentContent = await readFile3(join3(agentsDir, file), "utf-8");
1970
+ const agentContent = await readFile4(join4(agentsDir, file), "utf-8");
1653
1971
  const kiroAgent = buildKiroAgentContent(agentContent);
1654
- await writeFile2(join3(kiroAgentsDir, file), kiroAgent, "utf-8");
1972
+ await writeFile3(join4(kiroAgentsDir, file), kiroAgent, "utf-8");
1655
1973
  }
1656
- console.log(chalk2.green(` Copied ${mdFiles.length} agents to .kiro/agents/`));
1974
+ console.log(chalk3.green(` Copied ${mdFiles.length} agents to .kiro/agents/`));
1657
1975
  } catch {
1658
- console.log(chalk2.yellow(" No agents/ directory found, skipping agent copy"));
1976
+ console.log(chalk3.yellow(" No agents/ directory found, skipping agent copy"));
1659
1977
  }
1660
- const skillsDir = resolve(hubDir, "skills");
1978
+ const skillsDir = resolve2(hubDir, "skills");
1661
1979
  try {
1662
1980
  const skillFolders = await readdir2(skillsDir);
1663
- const kiroSkillsDir = join3(kiroDir, "skills");
1664
- await mkdir2(kiroSkillsDir, { recursive: true });
1981
+ const kiroSkillsDir = join4(kiroDir, "skills");
1982
+ await mkdir3(kiroSkillsDir, { recursive: true });
1665
1983
  let count = 0;
1666
1984
  for (const folder of skillFolders) {
1667
- const skillFile = join3(skillsDir, folder, "SKILL.md");
1985
+ const skillFile = join4(skillsDir, folder, "SKILL.md");
1668
1986
  try {
1669
- await readFile3(skillFile);
1670
- const srcDir = join3(skillsDir, folder);
1671
- const targetDir = join3(kiroSkillsDir, folder);
1987
+ await readFile4(skillFile);
1988
+ const srcDir = join4(skillsDir, folder);
1989
+ const targetDir = join4(kiroSkillsDir, folder);
1672
1990
  await cp(srcDir, targetDir, { recursive: true });
1673
1991
  count++;
1674
1992
  } catch {
1675
1993
  }
1676
1994
  }
1677
1995
  if (count > 0) {
1678
- console.log(chalk2.green(` Copied ${count} skills to .kiro/skills/`));
1996
+ console.log(chalk3.green(` Copied ${count} skills to .kiro/skills/`));
1679
1997
  }
1680
1998
  } catch {
1681
1999
  }
1682
- const kiroSkillsDirForDocs = join3(kiroDir, "skills");
1683
- await mkdir2(kiroSkillsDirForDocs, { recursive: true });
2000
+ const kiroSkillsDirForDocs = join4(kiroDir, "skills");
2001
+ await mkdir3(kiroSkillsDirForDocs, { recursive: true });
1684
2002
  await fetchHubDocsSkill(kiroSkillsDirForDocs);
2003
+ await syncRemoteSources(config, hubDir, join4(kiroDir, "skills"), steeringDir);
1685
2004
  if (config.mcps?.length) {
1686
2005
  const mcpConfig = {};
1687
2006
  const upstreamSet = getUpstreamNames(config.mcps);
@@ -1694,15 +2013,15 @@ async function generateKiro(config, hubDir) {
1694
2013
  mcpConfig[mcp.name] = buildKiroMcpEntry(mcp, mode);
1695
2014
  }
1696
2015
  }
1697
- const mcpJsonPath = join3(settingsDir, "mcp.json");
2016
+ const mcpJsonPath = join4(settingsDir, "mcp.json");
1698
2017
  const disabledState = await readExistingMcpDisabledState(mcpJsonPath);
1699
2018
  applyDisabledState(mcpConfig, disabledState);
1700
- await writeFile2(
2019
+ await writeFile3(
1701
2020
  mcpJsonPath,
1702
2021
  JSON.stringify({ mcpServers: mcpConfig }, null, 2) + "\n",
1703
2022
  "utf-8"
1704
2023
  );
1705
- console.log(chalk2.green(" Generated .kiro/settings/mcp.json"));
2024
+ console.log(chalk3.green(" Generated .kiro/settings/mcp.json"));
1706
2025
  }
1707
2026
  if (config.hooks) {
1708
2027
  const hookNotes = [];
@@ -1714,23 +2033,23 @@ async function generateKiro(config, hubDir) {
1714
2033
  }
1715
2034
  }
1716
2035
  if (hookNotes.length > 0) {
1717
- console.log(chalk2.yellow(` Note: Kiro hooks are managed via the Kiro panel UI.`));
1718
- console.log(chalk2.yellow(` The following hooks should be configured manually:`));
2036
+ console.log(chalk3.yellow(` Note: Kiro hooks are managed via the Kiro panel UI.`));
2037
+ console.log(chalk3.yellow(` The following hooks should be configured manually:`));
1719
2038
  for (const note of hookNotes) {
1720
- console.log(chalk2.yellow(` ${note}`));
2039
+ console.log(chalk3.yellow(` ${note}`));
1721
2040
  }
1722
2041
  }
1723
2042
  }
1724
2043
  await generateVSCodeSettings(config, hubDir);
1725
2044
  }
1726
2045
  async function generateVSCodeSettings(config, hubDir) {
1727
- const vscodeDir = join3(hubDir, ".vscode");
1728
- await mkdir2(vscodeDir, { recursive: true });
1729
- const settingsPath = join3(vscodeDir, "settings.json");
2046
+ const vscodeDir = join4(hubDir, ".vscode");
2047
+ await mkdir3(vscodeDir, { recursive: true });
2048
+ const settingsPath = join4(vscodeDir, "settings.json");
1730
2049
  let existing = {};
1731
2050
  if (existsSync2(settingsPath)) {
1732
2051
  try {
1733
- const raw = await readFile3(settingsPath, "utf-8");
2052
+ const raw = await readFile4(settingsPath, "utf-8");
1734
2053
  existing = JSON.parse(raw);
1735
2054
  } catch {
1736
2055
  existing = {};
@@ -1742,14 +2061,14 @@ async function generateVSCodeSettings(config, hubDir) {
1742
2061
  "git.detectSubmodulesLimit": Math.max(config.repos.length * 2, 20)
1743
2062
  };
1744
2063
  const merged = { ...existing, ...managed };
1745
- await writeFile2(settingsPath, JSON.stringify(merged, null, 2) + "\n", "utf-8");
1746
- console.log(chalk2.green(" Generated .vscode/settings.json (git multi-repo detection)"));
2064
+ await writeFile3(settingsPath, JSON.stringify(merged, null, 2) + "\n", "utf-8");
2065
+ console.log(chalk3.green(" Generated .vscode/settings.json (git multi-repo detection)"));
1747
2066
  const workspaceFile = `${config.name}.code-workspace`;
1748
- const workspacePath = join3(hubDir, workspaceFile);
2067
+ const workspacePath = join4(hubDir, workspaceFile);
1749
2068
  let existingWorkspace = {};
1750
2069
  if (existsSync2(workspacePath)) {
1751
2070
  try {
1752
- const raw = await readFile3(workspacePath, "utf-8");
2071
+ const raw = await readFile4(workspacePath, "utf-8");
1753
2072
  existingWorkspace = JSON.parse(raw);
1754
2073
  } catch {
1755
2074
  existingWorkspace = {};
@@ -1759,7 +2078,7 @@ async function generateVSCodeSettings(config, hubDir) {
1759
2078
  const existing2 = files.find((f) => f.endsWith(".code-workspace"));
1760
2079
  if (existing2) {
1761
2080
  try {
1762
- const raw = await readFile3(join3(hubDir, existing2), "utf-8");
2081
+ const raw = await readFile4(join4(hubDir, existing2), "utf-8");
1763
2082
  existingWorkspace = JSON.parse(raw);
1764
2083
  } catch {
1765
2084
  existingWorkspace = {};
@@ -1795,8 +2114,55 @@ async function generateVSCodeSettings(config, hubDir) {
1795
2114
  folders,
1796
2115
  settings: existingWorkspace.settings || {}
1797
2116
  };
1798
- await writeFile2(workspacePath, JSON.stringify(workspace, null, " ") + "\n", "utf-8");
1799
- console.log(chalk2.green(` Generated ${workspaceFile}`));
2117
+ await writeFile3(workspacePath, JSON.stringify(workspace, null, " ") + "\n", "utf-8");
2118
+ console.log(chalk3.green(` Generated ${workspaceFile}`));
2119
+ }
2120
+ function extractEnvVarsByMcp(mcps) {
2121
+ const envVarPattern = /\$\{env:([^}]+)\}/;
2122
+ const groups = [];
2123
+ for (const mcp of mcps) {
2124
+ if (!mcp.env) continue;
2125
+ const vars = [];
2126
+ const seenInGroup = /* @__PURE__ */ new Set();
2127
+ for (const value of Object.values(mcp.env)) {
2128
+ const match = envVarPattern.exec(value);
2129
+ if (match && !seenInGroup.has(match[1])) {
2130
+ seenInGroup.add(match[1]);
2131
+ vars.push(match[1]);
2132
+ }
2133
+ }
2134
+ if (vars.length > 0) {
2135
+ groups.push({ name: mcp.name, vars: vars.sort() });
2136
+ }
2137
+ }
2138
+ return groups;
2139
+ }
2140
+ async function generateEnvExample(config, hubDir) {
2141
+ const groups = extractEnvVarsByMcp(config.mcps || []);
2142
+ let totalVars = 0;
2143
+ const lines = [];
2144
+ const seen = /* @__PURE__ */ new Set();
2145
+ for (const group of groups) {
2146
+ const uniqueVars = group.vars.filter((v) => !seen.has(v));
2147
+ if (uniqueVars.length === 0) continue;
2148
+ for (const v of uniqueVars) seen.add(v);
2149
+ if (lines.length > 0) lines.push("");
2150
+ lines.push(`# ${group.name}`);
2151
+ for (const v of uniqueVars) {
2152
+ lines.push(`${v}=`);
2153
+ }
2154
+ totalVars += uniqueVars.length;
2155
+ }
2156
+ const hasNotionSources = config.remote_sources?.some((s) => s.notion_page);
2157
+ if (hasNotionSources && !seen.has("NOTION_API_KEY")) {
2158
+ if (lines.length > 0) lines.push("");
2159
+ lines.push("# Remote Sources (Notion)");
2160
+ lines.push("NOTION_API_KEY=");
2161
+ totalVars++;
2162
+ }
2163
+ if (totalVars === 0) return;
2164
+ await writeFile3(join4(hubDir, ".env.example"), lines.join("\n") + "\n", "utf-8");
2165
+ console.log(chalk3.green(` Generated .env.example (${totalVars} vars)`));
1800
2166
  }
1801
2167
  function buildGitignoreLines(config) {
1802
2168
  const lines = [
@@ -1883,14 +2249,14 @@ var generateCommand = new Command("generate").description("Generate editor-speci
1883
2249
  if (opts.check) {
1884
2250
  const result = await checkOutdated(hubDir);
1885
2251
  if (result.reason === "no-previous-generate") {
1886
- console.log(chalk2.yellow("No previous generate found. Run 'hub generate' first."));
2252
+ console.log(chalk3.yellow("No previous generate found. Run 'hub generate' first."));
1887
2253
  process.exit(1);
1888
2254
  }
1889
2255
  if (result.outdated) {
1890
- console.log(chalk2.yellow("Generated configs are outdated. Run 'hub generate' to update."));
2256
+ console.log(chalk3.yellow("Generated configs are outdated. Run 'hub generate' to update."));
1891
2257
  process.exit(1);
1892
2258
  }
1893
- console.log(chalk2.green("Generated configs are up to date."));
2259
+ console.log(chalk3.green("Generated configs are up to date."));
1894
2260
  return;
1895
2261
  }
1896
2262
  const config = await loadHubConfig(hubDir);
@@ -1899,37 +2265,47 @@ var generateCommand = new Command("generate").description("Generate editor-speci
1899
2265
  (m) => m.name === "team-memory" || m.package === "@arvoretech/memory-mcp"
1900
2266
  );
1901
2267
  if (!hasMemoryMcp) {
1902
- console.log(chalk2.red(`
2268
+ console.log(chalk3.red(`
1903
2269
  Error: 'memory' is configured but no memory MCP is declared in 'mcps'.
1904
2270
  `));
1905
- console.log(chalk2.yellow(` Add this to your hub.yaml:
2271
+ console.log(chalk3.yellow(` Add this to your hub.yaml:
1906
2272
  `));
1907
- console.log(chalk2.dim(` mcps:`));
1908
- console.log(chalk2.dim(` - name: team-memory`));
1909
- console.log(chalk2.dim(` package: "@arvoretech/memory-mcp"`));
1910
- console.log(chalk2.dim(` env:`));
1911
- console.log(chalk2.dim(` MEMORY_PATH: ${config.memory.path || "./memories"}
2273
+ console.log(chalk3.dim(` mcps:`));
2274
+ console.log(chalk3.dim(` - name: team-memory`));
2275
+ console.log(chalk3.dim(` package: "@arvoretech/memory-mcp"`));
2276
+ console.log(chalk3.dim(` env:`));
2277
+ console.log(chalk3.dim(` MEMORY_PATH: ${config.memory.path || "./memories"}
1912
2278
  `));
1913
2279
  process.exit(1);
1914
2280
  }
1915
2281
  }
2282
+ if (config.remote_sources?.length) {
2283
+ const hasNotionSources = config.remote_sources.some((s) => s.notion_page);
2284
+ if (hasNotionSources && !process.env.NOTION_API_KEY && !process.env.NOTION_TOKEN) {
2285
+ console.log(chalk3.yellow(`
2286
+ Warning: remote_sources include Notion pages but NOTION_API_KEY is not set.`));
2287
+ console.log(chalk3.yellow(` Notion sources will be skipped. Set NOTION_API_KEY in your .env or environment.
2288
+ `));
2289
+ }
2290
+ }
1916
2291
  const editorKey = await resolveEditor(opts);
1917
2292
  const generator = generators[editorKey];
1918
2293
  if (!generator) {
1919
2294
  console.log(
1920
- chalk2.red(`Unknown editor: ${editorKey}. Available: ${Object.keys(generators).join(", ")}`)
2295
+ chalk3.red(`Unknown editor: ${editorKey}. Available: ${Object.keys(generators).join(", ")}`)
1921
2296
  );
1922
2297
  return;
1923
2298
  }
1924
2299
  if (opts.editor || opts.resetEditor) {
1925
- console.log(chalk2.dim(` Saving editor preference: ${generator.name}`));
2300
+ console.log(chalk3.dim(` Saving editor preference: ${generator.name}`));
1926
2301
  }
1927
- console.log(chalk2.blue(`
2302
+ console.log(chalk3.blue(`
1928
2303
  Generating ${generator.name} configuration
1929
2304
  `));
1930
2305
  await generator.generate(config, hubDir);
2306
+ await generateEnvExample(config, hubDir);
1931
2307
  await saveGenerateState(hubDir, editorKey);
1932
- console.log(chalk2.green("\nDone!\n"));
2308
+ console.log(chalk3.green("\nDone!\n"));
1933
2309
  });
1934
2310
 
1935
2311
  export {
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  generateCommand,
3
3
  generators
4
- } from "./chunk-VMSQC56H.js";
4
+ } from "./chunk-NUJMJOEK.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-VMSQC56H.js";
6
+ } from "./chunk-NUJMJOEK.js";
7
7
 
8
8
  // src/index.ts
9
9
  import { Command as Command19 } from "commander";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arvoretech/hub",
3
- "version": "0.8.1",
3
+ "version": "0.9.0",
4
4
  "description": "CLI for managing AI-aware multi-repository workspaces",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",