@arvoretech/hub 0.8.2 → 0.10.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,43 +1,37 @@
1
+ import {
2
+ loadHubConfig
3
+ } from "./chunk-VMN4KGAK.js";
4
+
1
5
  // src/commands/generate.ts
2
6
  import { Command } from "commander";
3
7
  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";
8
+ import { mkdir as mkdir3, writeFile as writeFile3, readdir as readdir2, copyFile, readFile as readFile3, cp } from "fs/promises";
9
+ import { join as join3, resolve as resolve2 } from "path";
10
+ import chalk3 from "chalk";
7
11
  import inquirer from "inquirer";
8
12
 
9
- // src/core/hub-config.ts
10
- import { readFile } from "fs/promises";
11
- import { join } from "path";
12
- import { parse } from "yaml";
13
- async function loadHubConfig(dir) {
14
- const configPath = join(dir, "hub.yaml");
15
- const content = await readFile(configPath, "utf-8");
16
- return parse(content);
17
- }
18
-
19
13
  // src/core/hub-cache.ts
20
14
  import { createHash } from "crypto";
21
15
  import { existsSync } from "fs";
22
- import { mkdir, readdir, readFile as readFile2, writeFile } from "fs/promises";
23
- import { join as join2 } from "path";
16
+ import { mkdir, readdir, readFile, writeFile } from "fs/promises";
17
+ import { join } from "path";
24
18
  import chalk from "chalk";
25
19
  var HUB_DIR = ".hub";
26
20
  var CONFIG_FILE = "config.json";
27
21
  async function readCache(hubDir) {
28
- const filePath = join2(hubDir, HUB_DIR, CONFIG_FILE);
22
+ const filePath = join(hubDir, HUB_DIR, CONFIG_FILE);
29
23
  if (!existsSync(filePath)) return {};
30
24
  try {
31
- const content = await readFile2(filePath, "utf-8");
25
+ const content = await readFile(filePath, "utf-8");
32
26
  return JSON.parse(content);
33
27
  } catch {
34
28
  return {};
35
29
  }
36
30
  }
37
31
  async function writeCache(hubDir, cache) {
38
- const dir = join2(hubDir, HUB_DIR);
32
+ const dir = join(hubDir, HUB_DIR);
39
33
  await mkdir(dir, { recursive: true });
40
- await writeFile(join2(dir, CONFIG_FILE), JSON.stringify(cache, null, 2) + "\n", "utf-8");
34
+ await writeFile(join(dir, CONFIG_FILE), JSON.stringify(cache, null, 2) + "\n", "utf-8");
41
35
  }
42
36
  async function getSavedEditor(hubDir) {
43
37
  const cache = await readCache(hubDir);
@@ -57,9 +51,9 @@ async function collectFileHashes(dir, extensions) {
57
51
  const entries = await readdir(dir, { withFileTypes: true });
58
52
  const hashes = [];
59
53
  for (const entry of entries) {
60
- const fullPath = join2(dir, entry.name);
54
+ const fullPath = join(dir, entry.name);
61
55
  if (entry.isFile() && extensions.some((ext) => entry.name.endsWith(ext))) {
62
- const content = await readFile2(fullPath, "utf-8");
56
+ const content = await readFile(fullPath, "utf-8");
63
57
  hashes.push(`${entry.name}:${createHash("sha256").update(content).digest("hex")}`);
64
58
  } else if (entry.isDirectory()) {
65
59
  const subHashes = await collectFileHashes(fullPath, extensions);
@@ -70,14 +64,16 @@ async function collectFileHashes(dir, extensions) {
70
64
  }
71
65
  async function computeInputsHash(hubDir) {
72
66
  const parts = [];
73
- const hubYamlPath = join2(hubDir, "hub.yaml");
74
- if (existsSync(hubYamlPath)) {
75
- const content = await readFile2(hubYamlPath, "utf-8");
76
- parts.push(`hub.yaml:${createHash("sha256").update(content).digest("hex")}`);
67
+ const { resolveConfigPath } = await import("./hub-config-UATYEV6Q.js");
68
+ const { path: activeConfigPath, format } = resolveConfigPath(hubDir);
69
+ if (existsSync(activeConfigPath)) {
70
+ const configFile = format === "typescript" ? "hub.config.ts" : "hub.yaml";
71
+ const content = await readFile(activeConfigPath, "utf-8");
72
+ parts.push(`${configFile}:${createHash("sha256").update(content).digest("hex")}`);
77
73
  }
78
74
  const dirs = ["agents", "skills", "hooks", "commands"];
79
75
  for (const dir of dirs) {
80
- const dirHashes = await collectFileHashes(join2(hubDir, dir), [".md", ".sh"]);
76
+ const dirHashes = await collectFileHashes(join(hubDir, dir), [".md", ".sh"]);
81
77
  parts.push(...dirHashes.map((h) => `${dir}/${h}`));
82
78
  }
83
79
  return createHash("sha256").update(parts.join("\n")).digest("hex");
@@ -121,7 +117,7 @@ async function checkAndAutoRegenerate(hubDir) {
121
117
  return;
122
118
  }
123
119
  console.log(chalk.yellow("\n Detected outdated configs, auto-regenerating..."));
124
- const { generators: generators2 } = await import("./generate-7YPRUSN5.js");
120
+ const { generators: generators2 } = await import("./generate-KSB3QH7S.js");
125
121
  const generator = generators2[result.editor];
126
122
  if (!generator) {
127
123
  console.log(chalk.red(` Unknown editor '${result.editor}' in cache. Run 'hub generate' manually.`));
@@ -137,8 +133,317 @@ async function checkAndAutoRegenerate(hubDir) {
137
133
  }
138
134
  }
139
135
 
136
+ // src/core/design-sources.ts
137
+ import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
138
+ import { join as join2, relative, resolve } from "path";
139
+ import chalk2 from "chalk";
140
+
141
+ // src/core/notion.ts
142
+ var NOTION_API_VERSION = "2022-06-28";
143
+ function getNotionToken() {
144
+ return process.env.NOTION_API_KEY || process.env.NOTION_TOKEN;
145
+ }
146
+ function extractPageId(pageRef) {
147
+ const cleaned = pageRef.replace(/https?:\/\/(?:www\.)?notion\.(?:so|site)\//, "");
148
+ const withoutSlug = cleaned.split("?")[0].split("#")[0];
149
+ const segments = withoutSlug.split("/");
150
+ const last = segments[segments.length - 1] || "";
151
+ const dashParts = last.split("-");
152
+ const candidate = dashParts[dashParts.length - 1];
153
+ if (candidate && /^[a-f0-9]{32}$/i.test(candidate)) {
154
+ return formatPageId(candidate);
155
+ }
156
+ const noDashes = pageRef.replace(/-/g, "");
157
+ const hexMatch = noDashes.match(/([a-f0-9]{32})/i);
158
+ if (hexMatch) return formatPageId(hexMatch[1]);
159
+ return pageRef;
160
+ }
161
+ function formatPageId(raw) {
162
+ return `${raw.slice(0, 8)}-${raw.slice(8, 12)}-${raw.slice(12, 16)}-${raw.slice(16, 20)}-${raw.slice(20)}`;
163
+ }
164
+ function richTextToMarkdown(richTexts) {
165
+ return richTexts.map((rt) => {
166
+ let text = rt.plain_text;
167
+ if (!text) return "";
168
+ if (rt.annotations?.code) text = `\`${text}\``;
169
+ if (rt.annotations?.bold) text = `**${text}**`;
170
+ if (rt.annotations?.italic) text = `*${text}*`;
171
+ if (rt.annotations?.strikethrough) text = `~~${text}~~`;
172
+ if (rt.href) text = `[${text}](${rt.href})`;
173
+ return text;
174
+ }).join("");
175
+ }
176
+ async function notionFetch(endpoint, token) {
177
+ const res = await fetch(`https://api.notion.com/v1${endpoint}`, {
178
+ headers: {
179
+ Authorization: `Bearer ${token}`,
180
+ "Notion-Version": NOTION_API_VERSION
181
+ }
182
+ });
183
+ if (!res.ok) {
184
+ throw new Error(`Notion API ${res.status}: ${res.statusText}`);
185
+ }
186
+ return res.json();
187
+ }
188
+ async function fetchAllBlocks(pageId, token) {
189
+ const blocks = [];
190
+ let cursor;
191
+ do {
192
+ const qs = cursor ? `?start_cursor=${cursor}` : "";
193
+ const data = await notionFetch(`/blocks/${pageId}/children${qs}`, token);
194
+ blocks.push(...data.results);
195
+ cursor = data.has_more ? data.next_cursor ?? void 0 : void 0;
196
+ } while (cursor);
197
+ return blocks;
198
+ }
199
+ function blockToMarkdown(block, indent = "") {
200
+ const type = block.type;
201
+ const data = block[type];
202
+ if (!data) return "";
203
+ const rt = data.rich_text || [];
204
+ const text = richTextToMarkdown(rt);
205
+ switch (type) {
206
+ case "paragraph":
207
+ return text ? `${indent}${text}` : "";
208
+ case "heading_1":
209
+ return `# ${text}`;
210
+ case "heading_2":
211
+ return `## ${text}`;
212
+ case "heading_3":
213
+ return `### ${text}`;
214
+ case "bulleted_list_item":
215
+ return `${indent}- ${text}`;
216
+ case "numbered_list_item":
217
+ return `${indent}1. ${text}`;
218
+ case "to_do": {
219
+ const checked = data.checked ? "x" : " ";
220
+ return `${indent}- [${checked}] ${text}`;
221
+ }
222
+ case "toggle":
223
+ return `${indent}<details><summary>${text}</summary>`;
224
+ case "code": {
225
+ const lang = data.language || "";
226
+ return `\`\`\`${lang}
227
+ ${text}
228
+ \`\`\``;
229
+ }
230
+ case "quote":
231
+ return `> ${text}`;
232
+ case "callout":
233
+ return `> ${text}`;
234
+ case "divider":
235
+ return "---";
236
+ case "table_row": {
237
+ const cells = data.cells || [];
238
+ return `| ${cells.map((c) => richTextToMarkdown(c)).join(" | ")} |`;
239
+ }
240
+ case "image": {
241
+ const imgData = data;
242
+ const url = imgData.file?.url || imgData.external?.url || "";
243
+ const caption = data.caption || [];
244
+ const alt = richTextToMarkdown(caption) || "image";
245
+ return url ? `![${alt}](${url})` : "";
246
+ }
247
+ case "bookmark": {
248
+ const bUrl = data.url;
249
+ return bUrl ? `[${bUrl}](${bUrl})` : "";
250
+ }
251
+ default:
252
+ return text || "";
253
+ }
254
+ }
255
+ async function blocksToMarkdown(blocks, token, indent = "") {
256
+ const lines = [];
257
+ for (const block of blocks) {
258
+ const type = block.type;
259
+ if (type === "table") {
260
+ if (block.has_children) {
261
+ const children = await fetchAllBlocks(block.id, token);
262
+ const rows = children.map((child) => blockToMarkdown(child, indent));
263
+ if (rows.length > 0) {
264
+ lines.push(rows[0]);
265
+ const colCount = (rows[0].match(/\|/g)?.length || 2) - 1;
266
+ lines.push(`|${" --- |".repeat(colCount)}`);
267
+ lines.push(...rows.slice(1));
268
+ }
269
+ }
270
+ lines.push("");
271
+ continue;
272
+ }
273
+ const line = blockToMarkdown(block, indent);
274
+ if (line !== void 0) lines.push(line);
275
+ if (block.has_children && type !== "table") {
276
+ const children = await fetchAllBlocks(block.id, token);
277
+ const childMd = await blocksToMarkdown(children, token, indent + " ");
278
+ if (childMd) lines.push(childMd);
279
+ if (type === "toggle") lines.push("</details>");
280
+ } else if (type === "toggle") {
281
+ lines.push("</details>");
282
+ }
283
+ }
284
+ return lines.join("\n");
285
+ }
286
+ async function fetchNotionPageAsMarkdown(pageRef) {
287
+ const token = getNotionToken();
288
+ if (!token) {
289
+ throw new Error("NOTION_API_KEY or NOTION_TOKEN env var is required to fetch Notion pages");
290
+ }
291
+ const pageId = extractPageId(pageRef);
292
+ const page = await notionFetch(`/pages/${pageId}`, token);
293
+ let title = "";
294
+ for (const prop of Object.values(page.properties)) {
295
+ if (prop.title?.length) {
296
+ title = prop.title.map((t) => t.plain_text).join("");
297
+ break;
298
+ }
299
+ }
300
+ const blocks = await fetchAllBlocks(pageId, token);
301
+ const content = await blocksToMarkdown(blocks, token);
302
+ return { title: title || "Untitled", content };
303
+ }
304
+
305
+ // src/core/design-sources.ts
306
+ async function fetchFromNotion(source) {
307
+ if (!source.notion_page) throw new Error(`No notion_page for source: ${source.name}`);
308
+ const { content } = await fetchNotionPageAsMarkdown(source.notion_page);
309
+ return content;
310
+ }
311
+ async function fetchFromUrl(source) {
312
+ if (!source.url) throw new Error(`No url for source: ${source.name}`);
313
+ const res = await fetch(source.url, { signal: AbortSignal.timeout(3e4) });
314
+ if (!res.ok) throw new Error(`Failed to fetch ${source.url}: ${res.status}`);
315
+ return res.text();
316
+ }
317
+ async function fetchFromPath(source, hubDir) {
318
+ if (!source.path) throw new Error(`No path for source: ${source.name}`);
319
+ const fullPath = resolve(hubDir, source.path);
320
+ const rel = relative(hubDir, fullPath);
321
+ if (rel.startsWith("..") || resolve(fullPath) === fullPath && !fullPath.startsWith(hubDir)) {
322
+ throw new Error(`Path "${source.path}" escapes the workspace for source: ${source.name}`);
323
+ }
324
+ return readFile2(fullPath, "utf-8");
325
+ }
326
+ async function fetchSourceContent(source, hubDir) {
327
+ if (source.notion_page) return fetchFromNotion(source);
328
+ if (source.url) return fetchFromUrl(source);
329
+ if (source.path) return fetchFromPath(source, hubDir);
330
+ throw new Error(`Source "${source.name}" has no notion_page, url, or path`);
331
+ }
332
+ function buildSkillContent(source) {
333
+ const triggers = source.triggers?.length ? source.triggers : [source.name.replace(/-/g, " ")];
334
+ const parts = [
335
+ "---",
336
+ `name: ${source.name}`,
337
+ `description: ${source.instructions || `Design source: ${source.name}`}`,
338
+ `triggers: [${triggers.join(", ")}]`,
339
+ "---",
340
+ ""
341
+ ];
342
+ if (source.instructions) {
343
+ parts.push(source.instructions, "");
344
+ }
345
+ parts.push(source.content);
346
+ return parts.join("\n");
347
+ }
348
+ function buildSteeringContent(source) {
349
+ const parts = [];
350
+ if (source.instructions) {
351
+ parts.push(source.instructions, "");
352
+ }
353
+ parts.push(source.content);
354
+ return parts.join("\n");
355
+ }
356
+ async function fetchRemoteSources(sources, hubDir, skillsDir, steeringDir) {
357
+ let skillCount = 0;
358
+ let steeringCount = 0;
359
+ const errors = [];
360
+ for (const source of sources) {
361
+ try {
362
+ const rawContent = await fetchSourceContent(source, hubDir);
363
+ const fetched = {
364
+ name: source.name,
365
+ type: source.type,
366
+ content: rawContent,
367
+ triggers: source.triggers,
368
+ instructions: source.instructions
369
+ };
370
+ if (source.type === "skill") {
371
+ const skillDir = join2(skillsDir, source.name);
372
+ await mkdir2(skillDir, { recursive: true });
373
+ const skillContent = buildSkillContent(fetched);
374
+ await writeFile2(join2(skillDir, "SKILL.md"), skillContent, "utf-8");
375
+ skillCount++;
376
+ console.log(chalk2.green(` \u2713 ${source.name} (skill)`));
377
+ } else {
378
+ await mkdir2(steeringDir, { recursive: true });
379
+ const steeringContent = buildSteeringContent(fetched);
380
+ await writeFile2(join2(steeringDir, `${source.name}.md`), steeringContent, "utf-8");
381
+ steeringCount++;
382
+ console.log(chalk2.green(` \u2713 ${source.name} (steering)`));
383
+ }
384
+ } catch (err) {
385
+ const msg = err instanceof Error ? err.message : String(err);
386
+ errors.push(`${source.name}: ${msg}`);
387
+ console.log(chalk2.yellow(` \u2717 ${source.name}: ${msg}`));
388
+ }
389
+ }
390
+ return { skills: skillCount, steering: steeringCount, errors };
391
+ }
392
+
140
393
  // src/commands/generate.ts
141
394
  var HUB_DOCS_URL = "https://hub.arvore.com.br/llms-full.txt";
395
+ async function syncRemoteSources(config, hubDir, skillsDir, steeringDir) {
396
+ if (!config.remote_sources?.length) return;
397
+ console.log(chalk3.blue(" Syncing remote sources..."));
398
+ const result = await fetchRemoteSources(config.remote_sources, hubDir, skillsDir, steeringDir);
399
+ if (result.skills > 0 || result.steering > 0) {
400
+ console.log(chalk3.green(` Synced ${result.skills} skill(s) and ${result.steering} steering file(s) from remote sources`));
401
+ }
402
+ if (result.errors.length > 0) {
403
+ console.log(chalk3.yellow(` ${result.errors.length} remote source(s) failed`));
404
+ }
405
+ }
406
+ function buildDesignSection(config) {
407
+ const design = config.design;
408
+ if (!design) return null;
409
+ const hasContent = design.skills?.length || design.libraries?.length || design.icons || design.instructions;
410
+ if (!hasContent) return null;
411
+ const parts = [];
412
+ parts.push(`
413
+ ## Design System`);
414
+ if (design.instructions) {
415
+ parts.push(`
416
+ ${design.instructions.trim()}`);
417
+ }
418
+ if (design.skills?.length) {
419
+ parts.push(`
420
+ ### Design Skills
421
+ `);
422
+ parts.push(`The following skills contain design guidelines and should be consulted when working on UI:`);
423
+ for (const skill of design.skills) {
424
+ parts.push(`- \`${skill}\``);
425
+ }
426
+ }
427
+ if (design.libraries?.length) {
428
+ parts.push(`
429
+ ### UI Libraries
430
+ `);
431
+ for (const lib of design.libraries) {
432
+ const refs = [];
433
+ if (lib.mcp) refs.push(`docs via \`${lib.mcp}\` MCP`);
434
+ if (lib.url) refs.push(`[docs](${lib.url})`);
435
+ if (lib.path) refs.push(`local docs at \`${lib.path}\``);
436
+ parts.push(`- **${lib.name}**${refs.length ? ` \u2014 ${refs.join(", ")}` : ""}`);
437
+ }
438
+ }
439
+ if (design.icons) {
440
+ parts.push(`
441
+ ### Icons
442
+ `);
443
+ parts.push(`Icon library: **${design.icons}**. Always use this library for icons.`);
444
+ }
445
+ return parts.join("\n");
446
+ }
142
447
  function stripFrontMatter(content) {
143
448
  const match = content.match(/^---\n[\s\S]*?\n---\n*/);
144
449
  if (match) return content.slice(match[0].length);
@@ -183,12 +488,12 @@ async function fetchHubDocsSkill(skillsDir) {
183
488
  try {
184
489
  const res = await fetch(HUB_DOCS_URL);
185
490
  if (!res.ok) {
186
- console.log(chalk2.yellow(` Could not fetch hub docs (${res.status}), skipping hub-docs skill`));
491
+ console.log(chalk3.yellow(` Could not fetch hub docs (${res.status}), skipping hub-docs skill`));
187
492
  return;
188
493
  }
189
494
  const content = await res.text();
190
495
  const hubSkillDir = join3(skillsDir, "hub-docs");
191
- await mkdir2(hubSkillDir, { recursive: true });
496
+ await mkdir3(hubSkillDir, { recursive: true });
192
497
  const skillContent = `---
193
498
  name: hub-docs
194
499
  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 +501,10 @@ triggers: [hub, rhm, hub.yaml, generate, scan, setup, orchestrator, multi-repo,
196
501
  ---
197
502
 
198
503
  ${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"));
504
+ await writeFile3(join3(hubSkillDir, "SKILL.md"), skillContent, "utf-8");
505
+ console.log(chalk3.green(" Fetched hub-docs skill from hub.arvore.com.br"));
201
506
  } catch {
202
- console.log(chalk2.yellow(` Could not fetch hub docs, skipping hub-docs skill`));
507
+ console.log(chalk3.yellow(` Could not fetch hub docs, skipping hub-docs skill`));
203
508
  }
204
509
  }
205
510
  var HUB_MARKER_START = "# >>> hub-managed (do not edit this section)";
@@ -274,36 +579,36 @@ async function generateEditorCommands(config, hubDir, targetDir, editorName) {
274
579
  const commandsDir = join3(targetDir, "commands");
275
580
  let count = 0;
276
581
  if (config.commands_dir) {
277
- const srcDir = resolve(hubDir, config.commands_dir);
582
+ const srcDir = resolve2(hubDir, config.commands_dir);
278
583
  try {
279
584
  const files = await readdir2(srcDir);
280
585
  const mdFiles = files.filter((f) => f.endsWith(".md"));
281
586
  if (mdFiles.length > 0) {
282
- await mkdir2(commandsDir, { recursive: true });
587
+ await mkdir3(commandsDir, { recursive: true });
283
588
  for (const file of mdFiles) {
284
589
  await copyFile(join3(srcDir, file), join3(commandsDir, file));
285
590
  count++;
286
591
  }
287
592
  }
288
593
  } catch {
289
- console.log(chalk2.yellow(` Commands directory ${config.commands_dir} not found, skipping`));
594
+ console.log(chalk3.yellow(` Commands directory ${config.commands_dir} not found, skipping`));
290
595
  }
291
596
  }
292
597
  if (config.commands) {
293
- await mkdir2(commandsDir, { recursive: true });
598
+ await mkdir3(commandsDir, { recursive: true });
294
599
  for (const [name, filePath] of Object.entries(config.commands)) {
295
- const src = resolve(hubDir, filePath);
600
+ const src = resolve2(hubDir, filePath);
296
601
  const dest = join3(commandsDir, name.endsWith(".md") ? name : `${name}.md`);
297
602
  try {
298
603
  await copyFile(src, dest);
299
604
  count++;
300
605
  } catch {
301
- console.log(chalk2.yellow(` Command file ${filePath} not found, skipping`));
606
+ console.log(chalk3.yellow(` Command file ${filePath} not found, skipping`));
302
607
  }
303
608
  }
304
609
  }
305
610
  if (count > 0) {
306
- console.log(chalk2.green(` Copied ${count} commands to ${editorName}`));
611
+ console.log(chalk3.green(` Copied ${count} commands to ${editorName}`));
307
612
  }
308
613
  }
309
614
  async function writeManagedFile(filePath, managedLines) {
@@ -315,21 +620,21 @@ async function writeManagedFile(filePath, managedLines) {
315
620
  if (startIdx !== -1 && endIdx !== -1) {
316
621
  const before = existing.substring(0, startIdx);
317
622
  const after = existing.substring(endIdx + HUB_MARKER_END.length);
318
- await writeFile2(filePath, before + managedBlock + after, "utf-8");
623
+ await writeFile3(filePath, before + managedBlock + after, "utf-8");
319
624
  return;
320
625
  }
321
- await writeFile2(filePath, managedBlock + "\n\n" + existing, "utf-8");
626
+ await writeFile3(filePath, managedBlock + "\n\n" + existing, "utf-8");
322
627
  return;
323
628
  }
324
- await writeFile2(filePath, managedBlock + "\n", "utf-8");
629
+ await writeFile3(filePath, managedBlock + "\n", "utf-8");
325
630
  }
326
631
  async function generateCursor(config, hubDir) {
327
632
  const cursorDir = join3(hubDir, ".cursor");
328
- await mkdir2(join3(cursorDir, "rules"), { recursive: true });
329
- await mkdir2(join3(cursorDir, "agents"), { recursive: true });
633
+ await mkdir3(join3(cursorDir, "rules"), { recursive: true });
634
+ await mkdir3(join3(cursorDir, "agents"), { recursive: true });
330
635
  const gitignoreLines = buildGitignoreLines(config);
331
636
  await writeManagedFile(join3(hubDir, ".gitignore"), gitignoreLines);
332
- console.log(chalk2.green(" Generated .gitignore"));
637
+ console.log(chalk3.green(" Generated .gitignore"));
333
638
  const cursorignoreLines = [
334
639
  "# Re-include repositories for AI context"
335
640
  ];
@@ -339,7 +644,7 @@ async function generateCursor(config, hubDir) {
339
644
  }
340
645
  cursorignoreLines.push("", "# Re-include tasks for agent collaboration", "!tasks/");
341
646
  await writeManagedFile(join3(hubDir, ".cursorignore"), cursorignoreLines);
342
- console.log(chalk2.green(" Generated .cursorignore"));
647
+ console.log(chalk3.green(" Generated .cursorignore"));
343
648
  if (config.mcps?.length) {
344
649
  const mcpConfig = {};
345
650
  const upstreamSet = getUpstreamNames(config.mcps);
@@ -351,17 +656,17 @@ async function generateCursor(config, hubDir) {
351
656
  mcpConfig[mcp.name] = buildCursorMcpEntry(mcp);
352
657
  }
353
658
  }
354
- await writeFile2(
659
+ await writeFile3(
355
660
  join3(cursorDir, "mcp.json"),
356
661
  JSON.stringify({ mcpServers: mcpConfig }, null, 2) + "\n",
357
662
  "utf-8"
358
663
  );
359
- console.log(chalk2.green(" Generated .cursor/mcp.json"));
664
+ console.log(chalk3.green(" Generated .cursor/mcp.json"));
360
665
  }
361
666
  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");
667
+ await writeFile3(join3(cursorDir, "rules", "orchestrator.mdc"), orchestratorRule, "utf-8");
668
+ console.log(chalk3.green(" Generated .cursor/rules/orchestrator.mdc"));
669
+ const hubSteeringDirCursor = resolve2(hubDir, "steering");
365
670
  try {
366
671
  const steeringFiles = await readdir2(hubSteeringDirCursor);
367
672
  const mdFiles = steeringFiles.filter((f) => f.endsWith(".md"));
@@ -375,29 +680,29 @@ alwaysApply: true
375
680
  ---
376
681
 
377
682
  ${content}`;
378
- await writeFile2(join3(cursorDir, "rules", mdcName), mdcContent, "utf-8");
683
+ await writeFile3(join3(cursorDir, "rules", mdcName), mdcContent, "utf-8");
379
684
  }
380
685
  if (mdFiles.length > 0) {
381
- console.log(chalk2.green(` Copied ${mdFiles.length} steering files to .cursor/rules/`));
686
+ console.log(chalk3.green(` Copied ${mdFiles.length} steering files to .cursor/rules/`));
382
687
  }
383
688
  } catch {
384
689
  }
385
- const agentsDir = resolve(hubDir, "agents");
690
+ const agentsDir = resolve2(hubDir, "agents");
386
691
  try {
387
692
  const agentFiles = await readdir2(agentsDir);
388
693
  const mdFiles = agentFiles.filter((f) => f.endsWith(".md"));
389
694
  for (const file of mdFiles) {
390
695
  await copyFile(join3(agentsDir, file), join3(cursorDir, "agents", file));
391
696
  }
392
- console.log(chalk2.green(` Copied ${mdFiles.length} agent definitions`));
697
+ console.log(chalk3.green(` Copied ${mdFiles.length} agent definitions`));
393
698
  } catch {
394
- console.log(chalk2.yellow(" No agents/ directory found, skipping agent copy"));
699
+ console.log(chalk3.yellow(" No agents/ directory found, skipping agent copy"));
395
700
  }
396
- const skillsDir = resolve(hubDir, "skills");
701
+ const skillsDir = resolve2(hubDir, "skills");
397
702
  try {
398
703
  const skillFolders = await readdir2(skillsDir);
399
704
  const cursorSkillsDir = join3(cursorDir, "skills");
400
- await mkdir2(cursorSkillsDir, { recursive: true });
705
+ await mkdir3(cursorSkillsDir, { recursive: true });
401
706
  let count = 0;
402
707
  for (const folder of skillFolders) {
403
708
  const skillFile = join3(skillsDir, folder, "SKILL.md");
@@ -411,22 +716,23 @@ ${content}`;
411
716
  }
412
717
  }
413
718
  if (count > 0) {
414
- console.log(chalk2.green(` Copied ${count} skills`));
719
+ console.log(chalk3.green(` Copied ${count} skills`));
415
720
  }
416
721
  } catch {
417
722
  }
418
723
  const cursorSkillsDirForDocs = join3(cursorDir, "skills");
419
- await mkdir2(cursorSkillsDirForDocs, { recursive: true });
724
+ await mkdir3(cursorSkillsDirForDocs, { recursive: true });
420
725
  await fetchHubDocsSkill(cursorSkillsDirForDocs);
726
+ await syncRemoteSources(config, hubDir, join3(cursorDir, "skills"), join3(cursorDir, "rules"));
421
727
  if (config.hooks) {
422
728
  const cursorHooks = buildCursorHooks(config.hooks);
423
729
  if (cursorHooks) {
424
- await writeFile2(
730
+ await writeFile3(
425
731
  join3(cursorDir, "hooks.json"),
426
732
  JSON.stringify(cursorHooks, null, 2) + "\n",
427
733
  "utf-8"
428
734
  );
429
- console.log(chalk2.green(" Generated .cursor/hooks.json"));
735
+ console.log(chalk3.green(" Generated .cursor/hooks.json"));
430
736
  }
431
737
  }
432
738
  await generateEditorCommands(config, hubDir, cursorDir, ".cursor/commands/");
@@ -667,6 +973,7 @@ function buildMcpToolsSection(mcps) {
667
973
  const proxyMcp = mcps.find((m) => m.upstreams && m.upstreams.length > 0);
668
974
  const upstreamNames = getUpstreamNames(mcps);
669
975
  const directMcps = mcps.filter((m) => !m.upstreams && !upstreamNames.has(m.name));
976
+ const mcpByName = new Map(mcps.map((m) => [m.name, m]));
670
977
  if (!proxyMcp && directMcps.length === 0) return "";
671
978
  const lines = [];
672
979
  lines.push(`
@@ -683,20 +990,33 @@ Some MCPs are aggregated behind a proxy (\`${proxyMcp.name}\`). Their tools are
683
990
 
684
991
  **MCPs available via proxy:**`);
685
992
  for (const name of proxyMcp.upstreams) {
686
- lines.push(`- \`${name}\``);
993
+ const mcp = mcpByName.get(name);
994
+ const desc = mcp?.description ? ` \u2014 ${mcp.description}` : "";
995
+ lines.push(`- \`${name}\`${desc}`);
687
996
  }
688
997
  }
689
998
  if (directMcps.length > 0) {
690
999
  lines.push(`
691
1000
  **MCPs available directly:**`);
692
1001
  for (const mcp of directMcps) {
693
- lines.push(`- \`${mcp.name}\``);
1002
+ const desc = mcp.description ? ` \u2014 ${mcp.description}` : "";
1003
+ lines.push(`- \`${mcp.name}\`${desc}`);
694
1004
  }
695
1005
  }
696
1006
  if (proxyMcp) {
697
1007
  lines.push(`
698
1008
  > When you need a capability and are unsure which tool to use, always try \`mcp_search\` first with relevant keywords. The proxy aggregates tools from all upstream MCPs.`);
699
1009
  }
1010
+ const mcpsWithInstructions = mcps.filter((m) => m.instructions && !m.upstreams);
1011
+ if (mcpsWithInstructions.length > 0) {
1012
+ lines.push(`
1013
+ ### MCP Instructions`);
1014
+ for (const mcp of mcpsWithInstructions) {
1015
+ lines.push(`
1016
+ #### ${mcp.name}
1017
+ ${mcp.instructions.trim()}`);
1018
+ }
1019
+ }
700
1020
  return lines.join("\n");
701
1021
  }
702
1022
  function buildOpenCodeOrchestratorRule(config) {
@@ -786,6 +1106,8 @@ This workspace has a team memory knowledge base available via the \`team-memory\
786
1106
 
787
1107
  Available tools: \`search_memories\`, \`get_memory\`, \`add_memory\`, \`list_memories\`, \`archive_memory\`, \`remove_memory\`.`);
788
1108
  }
1109
+ const designSectionOpenCode = buildDesignSection(config);
1110
+ if (designSectionOpenCode) sections.push(designSectionOpenCode);
789
1111
  sections.push(`
790
1112
  ## Troubleshooting and Debugging
791
1113
 
@@ -887,28 +1209,28 @@ If any validation agent leaves comments requiring fixes, call the relevant codin
887
1209
  }
888
1210
  async function generateOpenCode(config, hubDir) {
889
1211
  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 });
1212
+ await mkdir3(join3(opencodeDir, "agents"), { recursive: true });
1213
+ await mkdir3(join3(opencodeDir, "rules"), { recursive: true });
1214
+ await mkdir3(join3(opencodeDir, "skills"), { recursive: true });
1215
+ await mkdir3(join3(opencodeDir, "commands"), { recursive: true });
1216
+ await mkdir3(join3(opencodeDir, "plugins"), { recursive: true });
895
1217
  const gitignoreLines = buildGitignoreLines(config);
896
1218
  await writeManagedFile(join3(hubDir, ".gitignore"), gitignoreLines);
897
- console.log(chalk2.green(" Generated .gitignore"));
1219
+ console.log(chalk3.green(" Generated .gitignore"));
898
1220
  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");
1221
+ await writeFile3(join3(opencodeDir, "rules", "orchestrator.md"), orchestratorRule + "\n", "utf-8");
1222
+ console.log(chalk3.green(" Generated .opencode/rules/orchestrator.md"));
1223
+ const hubSteeringDirOC = resolve2(hubDir, "steering");
902
1224
  try {
903
1225
  const steeringFiles = await readdir2(hubSteeringDirOC);
904
1226
  const mdFiles = steeringFiles.filter((f) => f.endsWith(".md"));
905
1227
  for (const file of mdFiles) {
906
1228
  const raw = await readFile3(join3(hubSteeringDirOC, file), "utf-8");
907
1229
  const content = stripFrontMatter(raw);
908
- await writeFile2(join3(opencodeDir, "rules", file), content, "utf-8");
1230
+ await writeFile3(join3(opencodeDir, "rules", file), content, "utf-8");
909
1231
  }
910
1232
  if (mdFiles.length > 0) {
911
- console.log(chalk2.green(` Copied ${mdFiles.length} steering files to .opencode/rules/`));
1233
+ console.log(chalk3.green(` Copied ${mdFiles.length} steering files to .opencode/rules/`));
912
1234
  }
913
1235
  } catch {
914
1236
  }
@@ -929,13 +1251,13 @@ async function generateOpenCode(config, hubDir) {
929
1251
  opencodeConfig.mcp = mcpConfig;
930
1252
  }
931
1253
  opencodeConfig.instructions = [".opencode/rules/*.md"];
932
- await writeFile2(
1254
+ await writeFile3(
933
1255
  join3(hubDir, "opencode.json"),
934
1256
  JSON.stringify(opencodeConfig, null, 2) + "\n",
935
1257
  "utf-8"
936
1258
  );
937
- console.log(chalk2.green(" Generated opencode.json"));
938
- const agentsDir = resolve(hubDir, "agents");
1259
+ console.log(chalk3.green(" Generated opencode.json"));
1260
+ const agentsDir = resolve2(hubDir, "agents");
939
1261
  try {
940
1262
  const agentFiles = await readdir2(agentsDir);
941
1263
  const mdFiles = agentFiles.filter((f) => f.endsWith(".md"));
@@ -943,13 +1265,13 @@ async function generateOpenCode(config, hubDir) {
943
1265
  const content = await readFile3(join3(agentsDir, file), "utf-8");
944
1266
  const agentName = file.replace(/\.md$/, "");
945
1267
  const converted = buildOpenCodeAgentMarkdown(agentName, content);
946
- await writeFile2(join3(opencodeDir, "agents", file), converted, "utf-8");
1268
+ await writeFile3(join3(opencodeDir, "agents", file), converted, "utf-8");
947
1269
  }
948
- console.log(chalk2.green(` Copied ${mdFiles.length} agents to .opencode/agents/`));
1270
+ console.log(chalk3.green(` Copied ${mdFiles.length} agents to .opencode/agents/`));
949
1271
  } catch {
950
- console.log(chalk2.yellow(" No agents/ directory found, skipping agent copy"));
1272
+ console.log(chalk3.yellow(" No agents/ directory found, skipping agent copy"));
951
1273
  }
952
- const skillsDir = resolve(hubDir, "skills");
1274
+ const skillsDir = resolve2(hubDir, "skills");
953
1275
  try {
954
1276
  const skillFolders = await readdir2(skillsDir);
955
1277
  let count = 0;
@@ -963,17 +1285,18 @@ async function generateOpenCode(config, hubDir) {
963
1285
  }
964
1286
  }
965
1287
  if (count > 0) {
966
- console.log(chalk2.green(` Copied ${count} skills to .opencode/skills/`));
1288
+ console.log(chalk3.green(` Copied ${count} skills to .opencode/skills/`));
967
1289
  }
968
1290
  } catch {
969
1291
  }
970
1292
  await fetchHubDocsSkill(join3(opencodeDir, "skills"));
1293
+ await syncRemoteSources(config, hubDir, join3(opencodeDir, "skills"), join3(opencodeDir, "rules"));
971
1294
  await generateEditorCommands(config, hubDir, opencodeDir, ".opencode/commands/");
972
1295
  if (config.hooks) {
973
1296
  const plugin = buildOpenCodeHooksPlugin(config.hooks);
974
1297
  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"));
1298
+ await writeFile3(join3(opencodeDir, "plugins", "hub-hooks.js"), plugin, "utf-8");
1299
+ console.log(chalk3.green(" Generated .opencode/plugins/hub-hooks.js"));
977
1300
  }
978
1301
  }
979
1302
  await generateVSCodeSettings(config, hubDir);
@@ -1076,6 +1399,8 @@ This workspace has a team memory knowledge base available via the \`team-memory\
1076
1399
 
1077
1400
  Available tools: \`search_memories\`, \`get_memory\`, \`add_memory\`, \`list_memories\`, \`archive_memory\`, \`remove_memory\`.`);
1078
1401
  }
1402
+ const designSectionKiro = buildDesignSection(config);
1403
+ if (designSectionKiro) sections.push(designSectionKiro);
1079
1404
  sections.push(`
1080
1405
  ## Troubleshooting and Debugging
1081
1406
 
@@ -1262,6 +1587,8 @@ This workspace has a team memory knowledge base available via the \`team-memory\
1262
1587
 
1263
1588
  Available tools: \`search_memories\`, \`get_memory\`, \`add_memory\`, \`list_memories\`, \`archive_memory\`, \`remove_memory\`.`);
1264
1589
  }
1590
+ const designSectionCursor = buildDesignSection(config);
1591
+ if (designSectionCursor) sections.push(designSectionCursor);
1265
1592
  sections.push(`
1266
1593
  ## Troubleshooting and Debugging
1267
1594
 
@@ -1442,27 +1769,27 @@ function formatAction(action) {
1442
1769
  }
1443
1770
  async function generateClaudeCode(config, hubDir) {
1444
1771
  const claudeDir = join3(hubDir, ".claude");
1445
- await mkdir2(join3(claudeDir, "agents"), { recursive: true });
1772
+ await mkdir3(join3(claudeDir, "agents"), { recursive: true });
1446
1773
  const orchestratorRule = buildOrchestratorRule(config);
1447
1774
  const cleanedOrchestrator = orchestratorRule.replace(/^---[\s\S]*?---\n/m, "").trim();
1448
1775
  const claudeMdSections = [];
1449
1776
  claudeMdSections.push(cleanedOrchestrator);
1450
- const agentsDir = resolve(hubDir, "agents");
1777
+ const agentsDir = resolve2(hubDir, "agents");
1451
1778
  try {
1452
1779
  const agentFiles = await readdir2(agentsDir);
1453
1780
  const mdFiles = agentFiles.filter((f) => f.endsWith(".md"));
1454
1781
  for (const file of mdFiles) {
1455
1782
  await copyFile(join3(agentsDir, file), join3(claudeDir, "agents", file));
1456
1783
  }
1457
- console.log(chalk2.green(` Copied ${mdFiles.length} agents to .claude/agents/`));
1784
+ console.log(chalk3.green(` Copied ${mdFiles.length} agents to .claude/agents/`));
1458
1785
  } catch {
1459
- console.log(chalk2.yellow(" No agents/ directory found, skipping agent copy"));
1786
+ console.log(chalk3.yellow(" No agents/ directory found, skipping agent copy"));
1460
1787
  }
1461
- const skillsDir = resolve(hubDir, "skills");
1788
+ const skillsDir = resolve2(hubDir, "skills");
1462
1789
  try {
1463
1790
  const skillFolders = await readdir2(skillsDir);
1464
1791
  const claudeSkillsDir = join3(claudeDir, "skills");
1465
- await mkdir2(claudeSkillsDir, { recursive: true });
1792
+ await mkdir3(claudeSkillsDir, { recursive: true });
1466
1793
  let count = 0;
1467
1794
  for (const folder of skillFolders) {
1468
1795
  const skillFile = join3(skillsDir, folder, "SKILL.md");
@@ -1476,14 +1803,15 @@ async function generateClaudeCode(config, hubDir) {
1476
1803
  }
1477
1804
  }
1478
1805
  if (count > 0) {
1479
- console.log(chalk2.green(` Copied ${count} skills to .claude/skills/`));
1806
+ console.log(chalk3.green(` Copied ${count} skills to .claude/skills/`));
1480
1807
  }
1481
1808
  } catch {
1482
1809
  }
1483
1810
  const claudeSkillsDirForDocs = join3(claudeDir, "skills");
1484
- await mkdir2(claudeSkillsDirForDocs, { recursive: true });
1811
+ await mkdir3(claudeSkillsDirForDocs, { recursive: true });
1485
1812
  await fetchHubDocsSkill(claudeSkillsDirForDocs);
1486
- const hubSteeringDirClaude = resolve(hubDir, "steering");
1813
+ await syncRemoteSources(config, hubDir, join3(claudeDir, "skills"), join3(claudeDir, "steering"));
1814
+ const hubSteeringDirClaude = resolve2(hubDir, "steering");
1487
1815
  try {
1488
1816
  const steeringFiles = await readdir2(hubSteeringDirClaude);
1489
1817
  const mdFiles = steeringFiles.filter((f) => f.endsWith(".md"));
@@ -1495,12 +1823,12 @@ async function generateClaudeCode(config, hubDir) {
1495
1823
  }
1496
1824
  }
1497
1825
  if (mdFiles.length > 0) {
1498
- console.log(chalk2.green(` Appended ${mdFiles.length} steering files to CLAUDE.md`));
1826
+ console.log(chalk3.green(` Appended ${mdFiles.length} steering files to CLAUDE.md`));
1499
1827
  }
1500
1828
  } catch {
1501
1829
  }
1502
- await writeFile2(join3(hubDir, "CLAUDE.md"), claudeMdSections.join("\n\n"), "utf-8");
1503
- console.log(chalk2.green(" Generated CLAUDE.md"));
1830
+ await writeFile3(join3(hubDir, "CLAUDE.md"), claudeMdSections.join("\n\n"), "utf-8");
1831
+ console.log(chalk3.green(" Generated CLAUDE.md"));
1504
1832
  if (config.mcps?.length) {
1505
1833
  const mcpJson = {};
1506
1834
  const upstreamSet = getUpstreamNames(config.mcps);
@@ -1512,12 +1840,12 @@ async function generateClaudeCode(config, hubDir) {
1512
1840
  mcpJson[mcp.name] = buildClaudeCodeMcpEntry(mcp);
1513
1841
  }
1514
1842
  }
1515
- await writeFile2(
1843
+ await writeFile3(
1516
1844
  join3(hubDir, ".mcp.json"),
1517
1845
  JSON.stringify({ mcpServers: mcpJson }, null, 2) + "\n",
1518
1846
  "utf-8"
1519
1847
  );
1520
- console.log(chalk2.green(" Generated .mcp.json"));
1848
+ console.log(chalk3.green(" Generated .mcp.json"));
1521
1849
  }
1522
1850
  const mcpServerNames = config.mcps?.map((m) => m.name) || [];
1523
1851
  const claudeSettings = {
@@ -1555,22 +1883,22 @@ async function generateClaudeCode(config, hubDir) {
1555
1883
  claudeSettings.hooks = claudeHooks;
1556
1884
  }
1557
1885
  }
1558
- await writeFile2(
1886
+ await writeFile3(
1559
1887
  join3(claudeDir, "settings.json"),
1560
1888
  JSON.stringify(claudeSettings, null, 2) + "\n",
1561
1889
  "utf-8"
1562
1890
  );
1563
- console.log(chalk2.green(" Generated .claude/settings.json"));
1891
+ console.log(chalk3.green(" Generated .claude/settings.json"));
1564
1892
  const gitignoreLines = buildGitignoreLines(config);
1565
1893
  await writeManagedFile(join3(hubDir, ".gitignore"), gitignoreLines);
1566
- console.log(chalk2.green(" Generated .gitignore"));
1894
+ console.log(chalk3.green(" Generated .gitignore"));
1567
1895
  }
1568
1896
  async function generateKiro(config, hubDir) {
1569
1897
  const kiroDir = join3(hubDir, ".kiro");
1570
1898
  const steeringDir = join3(kiroDir, "steering");
1571
1899
  const settingsDir = join3(kiroDir, "settings");
1572
- await mkdir2(steeringDir, { recursive: true });
1573
- await mkdir2(settingsDir, { recursive: true });
1900
+ await mkdir3(steeringDir, { recursive: true });
1901
+ await mkdir3(settingsDir, { recursive: true });
1574
1902
  let mode = await getKiroMode(hubDir);
1575
1903
  if (!mode) {
1576
1904
  const { kiroMode } = await inquirer.prompt([
@@ -1586,20 +1914,20 @@ async function generateKiro(config, hubDir) {
1586
1914
  ]);
1587
1915
  mode = kiroMode;
1588
1916
  await saveKiroMode(hubDir, mode);
1589
- console.log(chalk2.dim(` Saved Kiro mode: ${mode}`));
1917
+ console.log(chalk3.dim(` Saved Kiro mode: ${mode}`));
1590
1918
  } else {
1591
- console.log(chalk2.dim(` Using saved Kiro mode: ${mode}`));
1919
+ console.log(chalk3.dim(` Using saved Kiro mode: ${mode}`));
1592
1920
  }
1593
1921
  const gitignoreLines = buildGitignoreLines(config);
1594
1922
  await writeManagedFile(join3(hubDir, ".gitignore"), gitignoreLines);
1595
- console.log(chalk2.green(" Generated .gitignore"));
1923
+ console.log(chalk3.green(" Generated .gitignore"));
1596
1924
  const kiroRule = buildKiroOrchestratorRule(config);
1597
1925
  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");
1926
+ await writeFile3(join3(steeringDir, "orchestrator.md"), kiroOrchestrator, "utf-8");
1927
+ console.log(chalk3.green(" Generated .kiro/steering/orchestrator.md"));
1928
+ await writeFile3(join3(hubDir, "AGENTS.md"), kiroRule + "\n", "utf-8");
1929
+ console.log(chalk3.green(" Generated AGENTS.md"));
1930
+ const hubSteeringDir = resolve2(hubDir, "steering");
1603
1931
  try {
1604
1932
  const steeringFiles = await readdir2(hubSteeringDir);
1605
1933
  const mdFiles = steeringFiles.filter((f) => f.endsWith(".md"));
@@ -1635,33 +1963,33 @@ async function generateKiro(config, hubDir) {
1635
1963
  }
1636
1964
  }
1637
1965
  const kiroSteering = buildKiroSteeringContent(content, inclusion, meta);
1638
- await writeFile2(destPath, kiroSteering, "utf-8");
1966
+ await writeFile3(destPath, kiroSteering, "utf-8");
1639
1967
  }
1640
1968
  if (mdFiles.length > 0) {
1641
- console.log(chalk2.green(` Copied ${mdFiles.length} steering files to .kiro/steering/`));
1969
+ console.log(chalk3.green(` Copied ${mdFiles.length} steering files to .kiro/steering/`));
1642
1970
  }
1643
1971
  } catch {
1644
1972
  }
1645
- const agentsDir = resolve(hubDir, "agents");
1973
+ const agentsDir = resolve2(hubDir, "agents");
1646
1974
  try {
1647
1975
  const kiroAgentsDir = join3(kiroDir, "agents");
1648
- await mkdir2(kiroAgentsDir, { recursive: true });
1976
+ await mkdir3(kiroAgentsDir, { recursive: true });
1649
1977
  const agentFiles = await readdir2(agentsDir);
1650
1978
  const mdFiles = agentFiles.filter((f) => f.endsWith(".md"));
1651
1979
  for (const file of mdFiles) {
1652
1980
  const agentContent = await readFile3(join3(agentsDir, file), "utf-8");
1653
1981
  const kiroAgent = buildKiroAgentContent(agentContent);
1654
- await writeFile2(join3(kiroAgentsDir, file), kiroAgent, "utf-8");
1982
+ await writeFile3(join3(kiroAgentsDir, file), kiroAgent, "utf-8");
1655
1983
  }
1656
- console.log(chalk2.green(` Copied ${mdFiles.length} agents to .kiro/agents/`));
1984
+ console.log(chalk3.green(` Copied ${mdFiles.length} agents to .kiro/agents/`));
1657
1985
  } catch {
1658
- console.log(chalk2.yellow(" No agents/ directory found, skipping agent copy"));
1986
+ console.log(chalk3.yellow(" No agents/ directory found, skipping agent copy"));
1659
1987
  }
1660
- const skillsDir = resolve(hubDir, "skills");
1988
+ const skillsDir = resolve2(hubDir, "skills");
1661
1989
  try {
1662
1990
  const skillFolders = await readdir2(skillsDir);
1663
1991
  const kiroSkillsDir = join3(kiroDir, "skills");
1664
- await mkdir2(kiroSkillsDir, { recursive: true });
1992
+ await mkdir3(kiroSkillsDir, { recursive: true });
1665
1993
  let count = 0;
1666
1994
  for (const folder of skillFolders) {
1667
1995
  const skillFile = join3(skillsDir, folder, "SKILL.md");
@@ -1675,13 +2003,14 @@ async function generateKiro(config, hubDir) {
1675
2003
  }
1676
2004
  }
1677
2005
  if (count > 0) {
1678
- console.log(chalk2.green(` Copied ${count} skills to .kiro/skills/`));
2006
+ console.log(chalk3.green(` Copied ${count} skills to .kiro/skills/`));
1679
2007
  }
1680
2008
  } catch {
1681
2009
  }
1682
2010
  const kiroSkillsDirForDocs = join3(kiroDir, "skills");
1683
- await mkdir2(kiroSkillsDirForDocs, { recursive: true });
2011
+ await mkdir3(kiroSkillsDirForDocs, { recursive: true });
1684
2012
  await fetchHubDocsSkill(kiroSkillsDirForDocs);
2013
+ await syncRemoteSources(config, hubDir, join3(kiroDir, "skills"), steeringDir);
1685
2014
  if (config.mcps?.length) {
1686
2015
  const mcpConfig = {};
1687
2016
  const upstreamSet = getUpstreamNames(config.mcps);
@@ -1697,12 +2026,12 @@ async function generateKiro(config, hubDir) {
1697
2026
  const mcpJsonPath = join3(settingsDir, "mcp.json");
1698
2027
  const disabledState = await readExistingMcpDisabledState(mcpJsonPath);
1699
2028
  applyDisabledState(mcpConfig, disabledState);
1700
- await writeFile2(
2029
+ await writeFile3(
1701
2030
  mcpJsonPath,
1702
2031
  JSON.stringify({ mcpServers: mcpConfig }, null, 2) + "\n",
1703
2032
  "utf-8"
1704
2033
  );
1705
- console.log(chalk2.green(" Generated .kiro/settings/mcp.json"));
2034
+ console.log(chalk3.green(" Generated .kiro/settings/mcp.json"));
1706
2035
  }
1707
2036
  if (config.hooks) {
1708
2037
  const hookNotes = [];
@@ -1714,10 +2043,10 @@ async function generateKiro(config, hubDir) {
1714
2043
  }
1715
2044
  }
1716
2045
  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:`));
2046
+ console.log(chalk3.yellow(` Note: Kiro hooks are managed via the Kiro panel UI.`));
2047
+ console.log(chalk3.yellow(` The following hooks should be configured manually:`));
1719
2048
  for (const note of hookNotes) {
1720
- console.log(chalk2.yellow(` ${note}`));
2049
+ console.log(chalk3.yellow(` ${note}`));
1721
2050
  }
1722
2051
  }
1723
2052
  }
@@ -1725,7 +2054,7 @@ async function generateKiro(config, hubDir) {
1725
2054
  }
1726
2055
  async function generateVSCodeSettings(config, hubDir) {
1727
2056
  const vscodeDir = join3(hubDir, ".vscode");
1728
- await mkdir2(vscodeDir, { recursive: true });
2057
+ await mkdir3(vscodeDir, { recursive: true });
1729
2058
  const settingsPath = join3(vscodeDir, "settings.json");
1730
2059
  let existing = {};
1731
2060
  if (existsSync2(settingsPath)) {
@@ -1742,8 +2071,8 @@ async function generateVSCodeSettings(config, hubDir) {
1742
2071
  "git.detectSubmodulesLimit": Math.max(config.repos.length * 2, 20)
1743
2072
  };
1744
2073
  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)"));
2074
+ await writeFile3(settingsPath, JSON.stringify(merged, null, 2) + "\n", "utf-8");
2075
+ console.log(chalk3.green(" Generated .vscode/settings.json (git multi-repo detection)"));
1747
2076
  const workspaceFile = `${config.name}.code-workspace`;
1748
2077
  const workspacePath = join3(hubDir, workspaceFile);
1749
2078
  let existingWorkspace = {};
@@ -1795,8 +2124,8 @@ async function generateVSCodeSettings(config, hubDir) {
1795
2124
  folders,
1796
2125
  settings: existingWorkspace.settings || {}
1797
2126
  };
1798
- await writeFile2(workspacePath, JSON.stringify(workspace, null, " ") + "\n", "utf-8");
1799
- console.log(chalk2.green(` Generated ${workspaceFile}`));
2127
+ await writeFile3(workspacePath, JSON.stringify(workspace, null, " ") + "\n", "utf-8");
2128
+ console.log(chalk3.green(` Generated ${workspaceFile}`));
1800
2129
  }
1801
2130
  function extractEnvVarsByMcp(mcps) {
1802
2131
  const envVarPattern = /\$\{env:([^}]+)\}/;
@@ -1819,9 +2148,7 @@ function extractEnvVarsByMcp(mcps) {
1819
2148
  return groups;
1820
2149
  }
1821
2150
  async function generateEnvExample(config, hubDir) {
1822
- if (!config.mcps?.length) return;
1823
- const groups = extractEnvVarsByMcp(config.mcps);
1824
- if (groups.length === 0) return;
2151
+ const groups = extractEnvVarsByMcp(config.mcps || []);
1825
2152
  let totalVars = 0;
1826
2153
  const lines = [];
1827
2154
  const seen = /* @__PURE__ */ new Set();
@@ -1836,8 +2163,16 @@ async function generateEnvExample(config, hubDir) {
1836
2163
  }
1837
2164
  totalVars += uniqueVars.length;
1838
2165
  }
1839
- await writeFile2(join3(hubDir, ".env.example"), lines.join("\n") + "\n", "utf-8");
1840
- console.log(chalk2.green(` Generated .env.example (${totalVars} vars)`));
2166
+ const hasNotionSources = config.remote_sources?.some((s) => s.notion_page);
2167
+ if (hasNotionSources && !seen.has("NOTION_API_KEY")) {
2168
+ if (lines.length > 0) lines.push("");
2169
+ lines.push("# Remote Sources (Notion)");
2170
+ lines.push("NOTION_API_KEY=");
2171
+ totalVars++;
2172
+ }
2173
+ if (totalVars === 0) return;
2174
+ await writeFile3(join3(hubDir, ".env.example"), lines.join("\n") + "\n", "utf-8");
2175
+ console.log(chalk3.green(` Generated .env.example (${totalVars} vars)`));
1841
2176
  }
1842
2177
  function buildGitignoreLines(config) {
1843
2178
  const lines = [
@@ -1924,14 +2259,14 @@ var generateCommand = new Command("generate").description("Generate editor-speci
1924
2259
  if (opts.check) {
1925
2260
  const result = await checkOutdated(hubDir);
1926
2261
  if (result.reason === "no-previous-generate") {
1927
- console.log(chalk2.yellow("No previous generate found. Run 'hub generate' first."));
2262
+ console.log(chalk3.yellow("No previous generate found. Run 'hub generate' first."));
1928
2263
  process.exit(1);
1929
2264
  }
1930
2265
  if (result.outdated) {
1931
- console.log(chalk2.yellow("Generated configs are outdated. Run 'hub generate' to update."));
2266
+ console.log(chalk3.yellow("Generated configs are outdated. Run 'hub generate' to update."));
1932
2267
  process.exit(1);
1933
2268
  }
1934
- console.log(chalk2.green("Generated configs are up to date."));
2269
+ console.log(chalk3.green("Generated configs are up to date."));
1935
2270
  return;
1936
2271
  }
1937
2272
  const config = await loadHubConfig(hubDir);
@@ -1940,42 +2275,50 @@ var generateCommand = new Command("generate").description("Generate editor-speci
1940
2275
  (m) => m.name === "team-memory" || m.package === "@arvoretech/memory-mcp"
1941
2276
  );
1942
2277
  if (!hasMemoryMcp) {
1943
- console.log(chalk2.red(`
2278
+ console.log(chalk3.red(`
1944
2279
  Error: 'memory' is configured but no memory MCP is declared in 'mcps'.
1945
2280
  `));
1946
- console.log(chalk2.yellow(` Add this to your hub.yaml:
2281
+ console.log(chalk3.yellow(` Add this to your hub.yaml:
1947
2282
  `));
1948
- console.log(chalk2.dim(` mcps:`));
1949
- console.log(chalk2.dim(` - name: team-memory`));
1950
- console.log(chalk2.dim(` package: "@arvoretech/memory-mcp"`));
1951
- console.log(chalk2.dim(` env:`));
1952
- console.log(chalk2.dim(` MEMORY_PATH: ${config.memory.path || "./memories"}
2283
+ console.log(chalk3.dim(` mcps:`));
2284
+ console.log(chalk3.dim(` - name: team-memory`));
2285
+ console.log(chalk3.dim(` package: "@arvoretech/memory-mcp"`));
2286
+ console.log(chalk3.dim(` env:`));
2287
+ console.log(chalk3.dim(` MEMORY_PATH: ${config.memory.path || "./memories"}
1953
2288
  `));
1954
2289
  process.exit(1);
1955
2290
  }
1956
2291
  }
2292
+ if (config.remote_sources?.length) {
2293
+ const hasNotionSources = config.remote_sources.some((s) => s.notion_page);
2294
+ if (hasNotionSources && !process.env.NOTION_API_KEY && !process.env.NOTION_TOKEN) {
2295
+ console.log(chalk3.yellow(`
2296
+ Warning: remote_sources include Notion pages but NOTION_API_KEY is not set.`));
2297
+ console.log(chalk3.yellow(` Notion sources will be skipped. Set NOTION_API_KEY in your .env or environment.
2298
+ `));
2299
+ }
2300
+ }
1957
2301
  const editorKey = await resolveEditor(opts);
1958
2302
  const generator = generators[editorKey];
1959
2303
  if (!generator) {
1960
2304
  console.log(
1961
- chalk2.red(`Unknown editor: ${editorKey}. Available: ${Object.keys(generators).join(", ")}`)
2305
+ chalk3.red(`Unknown editor: ${editorKey}. Available: ${Object.keys(generators).join(", ")}`)
1962
2306
  );
1963
2307
  return;
1964
2308
  }
1965
2309
  if (opts.editor || opts.resetEditor) {
1966
- console.log(chalk2.dim(` Saving editor preference: ${generator.name}`));
2310
+ console.log(chalk3.dim(` Saving editor preference: ${generator.name}`));
1967
2311
  }
1968
- console.log(chalk2.blue(`
2312
+ console.log(chalk3.blue(`
1969
2313
  Generating ${generator.name} configuration
1970
2314
  `));
1971
2315
  await generator.generate(config, hubDir);
1972
2316
  await generateEnvExample(config, hubDir);
1973
2317
  await saveGenerateState(hubDir, editorKey);
1974
- console.log(chalk2.green("\nDone!\n"));
2318
+ console.log(chalk3.green("\nDone!\n"));
1975
2319
  });
1976
2320
 
1977
2321
  export {
1978
- loadHubConfig,
1979
2322
  generators,
1980
2323
  generateCommand,
1981
2324
  checkAndAutoRegenerate