@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.
- package/README.md +42 -6
- package/dist/{chunk-T2UKYK2B.js → chunk-MDJG7IOU.js} +496 -153
- package/dist/chunk-VMN4KGAK.js +43 -0
- package/dist/config/index.d.ts +213 -0
- package/dist/config/index.js +234 -0
- package/dist/{generate-7YPRUSN5.js → generate-KSB3QH7S.js} +2 -1
- package/dist/hub-config-UATYEV6Q.js +10 -0
- package/dist/index.js +2430 -1310
- package/package.json +16 -3
|
@@ -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
|
|
5
|
-
import { join as join3, resolve } from "path";
|
|
6
|
-
import
|
|
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
|
|
23
|
-
import { join
|
|
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 =
|
|
22
|
+
const filePath = join(hubDir, HUB_DIR, CONFIG_FILE);
|
|
29
23
|
if (!existsSync(filePath)) return {};
|
|
30
24
|
try {
|
|
31
|
-
const content = await
|
|
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 =
|
|
32
|
+
const dir = join(hubDir, HUB_DIR);
|
|
39
33
|
await mkdir(dir, { recursive: true });
|
|
40
|
-
await writeFile(
|
|
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 =
|
|
54
|
+
const fullPath = join(dir, entry.name);
|
|
61
55
|
if (entry.isFile() && extensions.some((ext) => entry.name.endsWith(ext))) {
|
|
62
|
-
const content = await
|
|
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
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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(
|
|
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-
|
|
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 ? `` : "";
|
|
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(
|
|
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
|
|
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
|
|
200
|
-
console.log(
|
|
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(
|
|
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 =
|
|
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
|
|
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(
|
|
594
|
+
console.log(chalk3.yellow(` Commands directory ${config.commands_dir} not found, skipping`));
|
|
290
595
|
}
|
|
291
596
|
}
|
|
292
597
|
if (config.commands) {
|
|
293
|
-
await
|
|
598
|
+
await mkdir3(commandsDir, { recursive: true });
|
|
294
599
|
for (const [name, filePath] of Object.entries(config.commands)) {
|
|
295
|
-
const src =
|
|
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(
|
|
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(
|
|
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
|
|
623
|
+
await writeFile3(filePath, before + managedBlock + after, "utf-8");
|
|
319
624
|
return;
|
|
320
625
|
}
|
|
321
|
-
await
|
|
626
|
+
await writeFile3(filePath, managedBlock + "\n\n" + existing, "utf-8");
|
|
322
627
|
return;
|
|
323
628
|
}
|
|
324
|
-
await
|
|
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
|
|
329
|
-
await
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
664
|
+
console.log(chalk3.green(" Generated .cursor/mcp.json"));
|
|
360
665
|
}
|
|
361
666
|
const orchestratorRule = buildOrchestratorRule(config);
|
|
362
|
-
await
|
|
363
|
-
console.log(
|
|
364
|
-
const hubSteeringDirCursor =
|
|
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
|
|
683
|
+
await writeFile3(join3(cursorDir, "rules", mdcName), mdcContent, "utf-8");
|
|
379
684
|
}
|
|
380
685
|
if (mdFiles.length > 0) {
|
|
381
|
-
console.log(
|
|
686
|
+
console.log(chalk3.green(` Copied ${mdFiles.length} steering files to .cursor/rules/`));
|
|
382
687
|
}
|
|
383
688
|
} catch {
|
|
384
689
|
}
|
|
385
|
-
const agentsDir =
|
|
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(
|
|
697
|
+
console.log(chalk3.green(` Copied ${mdFiles.length} agent definitions`));
|
|
393
698
|
} catch {
|
|
394
|
-
console.log(
|
|
699
|
+
console.log(chalk3.yellow(" No agents/ directory found, skipping agent copy"));
|
|
395
700
|
}
|
|
396
|
-
const skillsDir =
|
|
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
|
|
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(
|
|
719
|
+
console.log(chalk3.green(` Copied ${count} skills`));
|
|
415
720
|
}
|
|
416
721
|
} catch {
|
|
417
722
|
}
|
|
418
723
|
const cursorSkillsDirForDocs = join3(cursorDir, "skills");
|
|
419
|
-
await
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
891
|
-
await
|
|
892
|
-
await
|
|
893
|
-
await
|
|
894
|
-
await
|
|
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(
|
|
1219
|
+
console.log(chalk3.green(" Generated .gitignore"));
|
|
898
1220
|
const orchestratorRule = buildOpenCodeOrchestratorRule(config);
|
|
899
|
-
await
|
|
900
|
-
console.log(
|
|
901
|
-
const hubSteeringDirOC =
|
|
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
|
|
1230
|
+
await writeFile3(join3(opencodeDir, "rules", file), content, "utf-8");
|
|
909
1231
|
}
|
|
910
1232
|
if (mdFiles.length > 0) {
|
|
911
|
-
console.log(
|
|
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
|
|
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(
|
|
938
|
-
const agentsDir =
|
|
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
|
|
1268
|
+
await writeFile3(join3(opencodeDir, "agents", file), converted, "utf-8");
|
|
947
1269
|
}
|
|
948
|
-
console.log(
|
|
1270
|
+
console.log(chalk3.green(` Copied ${mdFiles.length} agents to .opencode/agents/`));
|
|
949
1271
|
} catch {
|
|
950
|
-
console.log(
|
|
1272
|
+
console.log(chalk3.yellow(" No agents/ directory found, skipping agent copy"));
|
|
951
1273
|
}
|
|
952
|
-
const skillsDir =
|
|
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(
|
|
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
|
|
976
|
-
console.log(
|
|
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
|
|
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 =
|
|
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(
|
|
1784
|
+
console.log(chalk3.green(` Copied ${mdFiles.length} agents to .claude/agents/`));
|
|
1458
1785
|
} catch {
|
|
1459
|
-
console.log(
|
|
1786
|
+
console.log(chalk3.yellow(" No agents/ directory found, skipping agent copy"));
|
|
1460
1787
|
}
|
|
1461
|
-
const skillsDir =
|
|
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
|
|
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(
|
|
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
|
|
1811
|
+
await mkdir3(claudeSkillsDirForDocs, { recursive: true });
|
|
1485
1812
|
await fetchHubDocsSkill(claudeSkillsDirForDocs);
|
|
1486
|
-
|
|
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(
|
|
1826
|
+
console.log(chalk3.green(` Appended ${mdFiles.length} steering files to CLAUDE.md`));
|
|
1499
1827
|
}
|
|
1500
1828
|
} catch {
|
|
1501
1829
|
}
|
|
1502
|
-
await
|
|
1503
|
-
console.log(
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
1573
|
-
await
|
|
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(
|
|
1917
|
+
console.log(chalk3.dim(` Saved Kiro mode: ${mode}`));
|
|
1590
1918
|
} else {
|
|
1591
|
-
console.log(
|
|
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(
|
|
1923
|
+
console.log(chalk3.green(" Generated .gitignore"));
|
|
1596
1924
|
const kiroRule = buildKiroOrchestratorRule(config);
|
|
1597
1925
|
const kiroOrchestrator = buildKiroSteeringContent(kiroRule, "always", { name: "orchestrator" });
|
|
1598
|
-
await
|
|
1599
|
-
console.log(
|
|
1600
|
-
await
|
|
1601
|
-
console.log(
|
|
1602
|
-
const hubSteeringDir =
|
|
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
|
|
1966
|
+
await writeFile3(destPath, kiroSteering, "utf-8");
|
|
1639
1967
|
}
|
|
1640
1968
|
if (mdFiles.length > 0) {
|
|
1641
|
-
console.log(
|
|
1969
|
+
console.log(chalk3.green(` Copied ${mdFiles.length} steering files to .kiro/steering/`));
|
|
1642
1970
|
}
|
|
1643
1971
|
} catch {
|
|
1644
1972
|
}
|
|
1645
|
-
const agentsDir =
|
|
1973
|
+
const agentsDir = resolve2(hubDir, "agents");
|
|
1646
1974
|
try {
|
|
1647
1975
|
const kiroAgentsDir = join3(kiroDir, "agents");
|
|
1648
|
-
await
|
|
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
|
|
1982
|
+
await writeFile3(join3(kiroAgentsDir, file), kiroAgent, "utf-8");
|
|
1655
1983
|
}
|
|
1656
|
-
console.log(
|
|
1984
|
+
console.log(chalk3.green(` Copied ${mdFiles.length} agents to .kiro/agents/`));
|
|
1657
1985
|
} catch {
|
|
1658
|
-
console.log(
|
|
1986
|
+
console.log(chalk3.yellow(" No agents/ directory found, skipping agent copy"));
|
|
1659
1987
|
}
|
|
1660
|
-
const skillsDir =
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
2029
|
+
await writeFile3(
|
|
1701
2030
|
mcpJsonPath,
|
|
1702
2031
|
JSON.stringify({ mcpServers: mcpConfig }, null, 2) + "\n",
|
|
1703
2032
|
"utf-8"
|
|
1704
2033
|
);
|
|
1705
|
-
console.log(
|
|
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(
|
|
1718
|
-
console.log(
|
|
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(
|
|
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
|
|
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
|
|
1746
|
-
console.log(
|
|
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
|
|
1799
|
-
console.log(
|
|
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
|
-
|
|
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
|
-
|
|
1840
|
-
|
|
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(
|
|
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(
|
|
2266
|
+
console.log(chalk3.yellow("Generated configs are outdated. Run 'hub generate' to update."));
|
|
1932
2267
|
process.exit(1);
|
|
1933
2268
|
}
|
|
1934
|
-
console.log(
|
|
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(
|
|
2278
|
+
console.log(chalk3.red(`
|
|
1944
2279
|
Error: 'memory' is configured but no memory MCP is declared in 'mcps'.
|
|
1945
2280
|
`));
|
|
1946
|
-
console.log(
|
|
2281
|
+
console.log(chalk3.yellow(` Add this to your hub.yaml:
|
|
1947
2282
|
`));
|
|
1948
|
-
console.log(
|
|
1949
|
-
console.log(
|
|
1950
|
-
console.log(
|
|
1951
|
-
console.log(
|
|
1952
|
-
console.log(
|
|
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
|
-
|
|
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(
|
|
2310
|
+
console.log(chalk3.dim(` Saving editor preference: ${generator.name}`));
|
|
1967
2311
|
}
|
|
1968
|
-
console.log(
|
|
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(
|
|
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
|