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