@arvoretech/hub 0.6.2 → 0.7.3
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.
|
@@ -43,6 +43,15 @@ async function getSavedEditor(hubDir) {
|
|
|
43
43
|
const cache = await readCache(hubDir);
|
|
44
44
|
return cache.editor;
|
|
45
45
|
}
|
|
46
|
+
async function getKiroMode(hubDir) {
|
|
47
|
+
const cache = await readCache(hubDir);
|
|
48
|
+
return cache.kiroMode;
|
|
49
|
+
}
|
|
50
|
+
async function saveKiroMode(hubDir, mode) {
|
|
51
|
+
const cache = await readCache(hubDir);
|
|
52
|
+
cache.kiroMode = mode;
|
|
53
|
+
await writeCache(hubDir, cache);
|
|
54
|
+
}
|
|
46
55
|
async function collectFileHashes(dir, extensions) {
|
|
47
56
|
if (!existsSync(dir)) return [];
|
|
48
57
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
@@ -112,7 +121,7 @@ async function checkAndAutoRegenerate(hubDir) {
|
|
|
112
121
|
return;
|
|
113
122
|
}
|
|
114
123
|
console.log(chalk.yellow("\n Detected outdated configs, auto-regenerating..."));
|
|
115
|
-
const { generators: generators2 } = await import("./generate-
|
|
124
|
+
const { generators: generators2 } = await import("./generate-BGTLIG4F.js");
|
|
116
125
|
const generator = generators2[result.editor];
|
|
117
126
|
if (!generator) {
|
|
118
127
|
console.log(chalk.red(` Unknown editor '${result.editor}' in cache. Run 'hub generate' manually.`));
|
|
@@ -129,33 +138,62 @@ async function checkAndAutoRegenerate(hubDir) {
|
|
|
129
138
|
}
|
|
130
139
|
|
|
131
140
|
// src/commands/generate.ts
|
|
141
|
+
var HUB_DOCS_URL = "https://hub.arvore.com.br/llms-full.txt";
|
|
142
|
+
function stripFrontMatter(content) {
|
|
143
|
+
const match = content.match(/^---\n[\s\S]*?\n---\n*/);
|
|
144
|
+
if (match) return content.slice(match[0].length);
|
|
145
|
+
return content;
|
|
146
|
+
}
|
|
147
|
+
async function fetchHubDocsSkill(skillsDir) {
|
|
148
|
+
try {
|
|
149
|
+
const res = await fetch(HUB_DOCS_URL);
|
|
150
|
+
if (!res.ok) {
|
|
151
|
+
console.log(chalk2.yellow(` Could not fetch hub docs (${res.status}), skipping hub-docs skill`));
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const content = await res.text();
|
|
155
|
+
const hubSkillDir = join3(skillsDir, "hub-docs");
|
|
156
|
+
await mkdir2(hubSkillDir, { recursive: true });
|
|
157
|
+
const skillContent = `---
|
|
158
|
+
name: hub-docs
|
|
159
|
+
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.
|
|
160
|
+
triggers: [hub, rhm, hub.yaml, generate, scan, setup, orchestrator, multi-repo, workspace]
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
${content}`;
|
|
164
|
+
await writeFile2(join3(hubSkillDir, "SKILL.md"), skillContent, "utf-8");
|
|
165
|
+
console.log(chalk2.green(" Fetched hub-docs skill from hub.arvore.com.br"));
|
|
166
|
+
} catch {
|
|
167
|
+
console.log(chalk2.yellow(` Could not fetch hub docs, skipping hub-docs skill`));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
132
170
|
var HUB_MARKER_START = "# >>> hub-managed (do not edit this section)";
|
|
133
171
|
var HUB_MARKER_END = "# <<< hub-managed";
|
|
134
172
|
var HOOK_EVENT_MAP = {
|
|
135
|
-
session_start: { cursor: "sessionStart", claude: "SessionStart", kiro: void 0 },
|
|
136
|
-
session_end: { cursor: "sessionEnd", claude: "SessionEnd", kiro: void 0 },
|
|
137
|
-
pre_tool_use: { cursor: "preToolUse", claude: "PreToolUse", kiro: "pre_tool_use" },
|
|
138
|
-
post_tool_use: { cursor: "postToolUse", claude: "PostToolUse", kiro: "post_tool_use" },
|
|
139
|
-
post_tool_use_failure: { cursor: void 0, claude: "PostToolUseFailure", kiro: void 0 },
|
|
140
|
-
stop: { cursor: "stop", claude: "Stop", kiro: "agent_stop" },
|
|
141
|
-
subagent_start: { cursor: "subagentStart", claude: "SubagentStart", kiro: void 0 },
|
|
142
|
-
subagent_stop: { cursor: "subagentStop", claude: "SubagentStop", kiro: void 0 },
|
|
143
|
-
pre_compact: { cursor: "preCompact", claude: "PreCompact", kiro: void 0 },
|
|
144
|
-
before_submit_prompt: { cursor: "beforeSubmitPrompt", claude: "UserPromptSubmit", kiro: "prompt_submit" },
|
|
145
|
-
before_shell_execution: { cursor: "beforeShellExecution", claude: void 0, kiro: void 0 },
|
|
146
|
-
after_shell_execution: { cursor: "afterShellExecution", claude: void 0, kiro: void 0 },
|
|
147
|
-
before_mcp_execution: { cursor: "beforeMCPExecution", claude: void 0, kiro: void 0 },
|
|
148
|
-
after_mcp_execution: { cursor: "afterMCPExecution", claude: void 0, kiro: void 0 },
|
|
149
|
-
after_file_edit: { cursor: "afterFileEdit", claude: void 0, kiro: "file_save" },
|
|
150
|
-
before_read_file: { cursor: "beforeReadFile", claude: void 0, kiro: void 0 },
|
|
151
|
-
before_tab_file_read: { cursor: "beforeTabFileRead", claude: void 0, kiro: void 0 },
|
|
152
|
-
after_tab_file_edit: { cursor: "afterTabFileEdit", claude: void 0, kiro: void 0 },
|
|
153
|
-
after_agent_response: { cursor: "afterAgentResponse", claude: void 0, kiro: void 0 },
|
|
154
|
-
after_agent_thought: { cursor: "afterAgentThought", claude: void 0, kiro: void 0 },
|
|
155
|
-
notification: { cursor: void 0, claude: "Notification", kiro: void 0 },
|
|
156
|
-
permission_request: { cursor: void 0, claude: "PermissionRequest", kiro: void 0 },
|
|
157
|
-
task_completed: { cursor: void 0, claude: "TaskCompleted", kiro: void 0 },
|
|
158
|
-
teammate_idle: { cursor: void 0, claude: "TeammateIdle", kiro: void 0 }
|
|
173
|
+
session_start: { cursor: "sessionStart", claude: "SessionStart", kiro: void 0, opencode: "session.created" },
|
|
174
|
+
session_end: { cursor: "sessionEnd", claude: "SessionEnd", kiro: void 0, opencode: "session.idle" },
|
|
175
|
+
pre_tool_use: { cursor: "preToolUse", claude: "PreToolUse", kiro: "pre_tool_use", opencode: "tool.execute.before" },
|
|
176
|
+
post_tool_use: { cursor: "postToolUse", claude: "PostToolUse", kiro: "post_tool_use", opencode: "tool.execute.after" },
|
|
177
|
+
post_tool_use_failure: { cursor: void 0, claude: "PostToolUseFailure", kiro: void 0, opencode: void 0 },
|
|
178
|
+
stop: { cursor: "stop", claude: "Stop", kiro: "agent_stop", opencode: "session.idle" },
|
|
179
|
+
subagent_start: { cursor: "subagentStart", claude: "SubagentStart", kiro: void 0, opencode: void 0 },
|
|
180
|
+
subagent_stop: { cursor: "subagentStop", claude: "SubagentStop", kiro: void 0, opencode: void 0 },
|
|
181
|
+
pre_compact: { cursor: "preCompact", claude: "PreCompact", kiro: void 0, opencode: "session.compacted" },
|
|
182
|
+
before_submit_prompt: { cursor: "beforeSubmitPrompt", claude: "UserPromptSubmit", kiro: "prompt_submit", opencode: void 0 },
|
|
183
|
+
before_shell_execution: { cursor: "beforeShellExecution", claude: void 0, kiro: void 0, opencode: "tool.execute.before" },
|
|
184
|
+
after_shell_execution: { cursor: "afterShellExecution", claude: void 0, kiro: void 0, opencode: "tool.execute.after" },
|
|
185
|
+
before_mcp_execution: { cursor: "beforeMCPExecution", claude: void 0, kiro: void 0, opencode: "tool.execute.before" },
|
|
186
|
+
after_mcp_execution: { cursor: "afterMCPExecution", claude: void 0, kiro: void 0, opencode: "tool.execute.after" },
|
|
187
|
+
after_file_edit: { cursor: "afterFileEdit", claude: void 0, kiro: "file_save", opencode: "file.edited" },
|
|
188
|
+
before_read_file: { cursor: "beforeReadFile", claude: void 0, kiro: void 0, opencode: void 0 },
|
|
189
|
+
before_tab_file_read: { cursor: "beforeTabFileRead", claude: void 0, kiro: void 0, opencode: void 0 },
|
|
190
|
+
after_tab_file_edit: { cursor: "afterTabFileEdit", claude: void 0, kiro: void 0, opencode: "file.edited" },
|
|
191
|
+
after_agent_response: { cursor: "afterAgentResponse", claude: void 0, kiro: void 0, opencode: void 0 },
|
|
192
|
+
after_agent_thought: { cursor: "afterAgentThought", claude: void 0, kiro: void 0, opencode: void 0 },
|
|
193
|
+
notification: { cursor: void 0, claude: "Notification", kiro: void 0, opencode: void 0 },
|
|
194
|
+
permission_request: { cursor: void 0, claude: "PermissionRequest", kiro: void 0, opencode: "permission.asked" },
|
|
195
|
+
task_completed: { cursor: void 0, claude: "TaskCompleted", kiro: void 0, opencode: "session.idle" },
|
|
196
|
+
teammate_idle: { cursor: void 0, claude: "TeammateIdle", kiro: void 0, opencode: void 0 }
|
|
159
197
|
};
|
|
160
198
|
function buildCursorHooks(hooks) {
|
|
161
199
|
const cursorHooks = {};
|
|
@@ -197,8 +235,8 @@ function buildClaudeHooks(hooks) {
|
|
|
197
235
|
if (Object.keys(claudeHooks).length === 0) return null;
|
|
198
236
|
return claudeHooks;
|
|
199
237
|
}
|
|
200
|
-
async function
|
|
201
|
-
const commandsDir = join3(
|
|
238
|
+
async function generateEditorCommands(config, hubDir, targetDir, editorName) {
|
|
239
|
+
const commandsDir = join3(targetDir, "commands");
|
|
202
240
|
let count = 0;
|
|
203
241
|
if (config.commands_dir) {
|
|
204
242
|
const srcDir = resolve(hubDir, config.commands_dir);
|
|
@@ -230,7 +268,7 @@ async function generateCursorCommands(config, hubDir, cursorDir) {
|
|
|
230
268
|
}
|
|
231
269
|
}
|
|
232
270
|
if (count > 0) {
|
|
233
|
-
console.log(chalk2.green(` Copied ${count} commands to
|
|
271
|
+
console.log(chalk2.green(` Copied ${count} commands to ${editorName}`));
|
|
234
272
|
}
|
|
235
273
|
}
|
|
236
274
|
async function writeManagedFile(filePath, managedLines) {
|
|
@@ -269,8 +307,14 @@ async function generateCursor(config, hubDir) {
|
|
|
269
307
|
console.log(chalk2.green(" Generated .cursorignore"));
|
|
270
308
|
if (config.mcps?.length) {
|
|
271
309
|
const mcpConfig = {};
|
|
310
|
+
const upstreamSet = getUpstreamNames(config.mcps);
|
|
272
311
|
for (const mcp of config.mcps) {
|
|
273
|
-
|
|
312
|
+
if (upstreamSet.has(mcp.name)) continue;
|
|
313
|
+
if (mcp.upstreams?.length) {
|
|
314
|
+
mcpConfig[mcp.name] = buildProxyMcpEntry(mcp, config.mcps, buildCursorMcpEntry);
|
|
315
|
+
} else {
|
|
316
|
+
mcpConfig[mcp.name] = buildCursorMcpEntry(mcp);
|
|
317
|
+
}
|
|
274
318
|
}
|
|
275
319
|
await writeFile2(
|
|
276
320
|
join3(cursorDir, "mcp.json"),
|
|
@@ -282,6 +326,27 @@ async function generateCursor(config, hubDir) {
|
|
|
282
326
|
const orchestratorRule = buildOrchestratorRule(config);
|
|
283
327
|
await writeFile2(join3(cursorDir, "rules", "orchestrator.mdc"), orchestratorRule, "utf-8");
|
|
284
328
|
console.log(chalk2.green(" Generated .cursor/rules/orchestrator.mdc"));
|
|
329
|
+
const hubSteeringDirCursor = resolve(hubDir, "steering");
|
|
330
|
+
try {
|
|
331
|
+
const steeringFiles = await readdir2(hubSteeringDirCursor);
|
|
332
|
+
const mdFiles = steeringFiles.filter((f) => f.endsWith(".md"));
|
|
333
|
+
for (const file of mdFiles) {
|
|
334
|
+
const raw = await readFile3(join3(hubSteeringDirCursor, file), "utf-8");
|
|
335
|
+
const content = stripFrontMatter(raw);
|
|
336
|
+
const mdcName = file.replace(/\.md$/, ".mdc");
|
|
337
|
+
const mdcContent = `---
|
|
338
|
+
description: "${file.replace(/\.md$/, "")}"
|
|
339
|
+
alwaysApply: true
|
|
340
|
+
---
|
|
341
|
+
|
|
342
|
+
${content}`;
|
|
343
|
+
await writeFile2(join3(cursorDir, "rules", mdcName), mdcContent, "utf-8");
|
|
344
|
+
}
|
|
345
|
+
if (mdFiles.length > 0) {
|
|
346
|
+
console.log(chalk2.green(` Copied ${mdFiles.length} steering files to .cursor/rules/`));
|
|
347
|
+
}
|
|
348
|
+
} catch {
|
|
349
|
+
}
|
|
285
350
|
const agentsDir = resolve(hubDir, "agents");
|
|
286
351
|
try {
|
|
287
352
|
const agentFiles = await readdir2(agentsDir);
|
|
@@ -315,6 +380,9 @@ async function generateCursor(config, hubDir) {
|
|
|
315
380
|
}
|
|
316
381
|
} catch {
|
|
317
382
|
}
|
|
383
|
+
const cursorSkillsDirForDocs = join3(cursorDir, "skills");
|
|
384
|
+
await mkdir2(cursorSkillsDirForDocs, { recursive: true });
|
|
385
|
+
await fetchHubDocsSkill(cursorSkillsDirForDocs);
|
|
318
386
|
if (config.hooks) {
|
|
319
387
|
const cursorHooks = buildCursorHooks(config.hooks);
|
|
320
388
|
if (cursorHooks) {
|
|
@@ -326,9 +394,65 @@ async function generateCursor(config, hubDir) {
|
|
|
326
394
|
console.log(chalk2.green(" Generated .cursor/hooks.json"));
|
|
327
395
|
}
|
|
328
396
|
}
|
|
329
|
-
await
|
|
397
|
+
await generateEditorCommands(config, hubDir, cursorDir, ".cursor/commands/");
|
|
330
398
|
await generateVSCodeSettings(config, hubDir);
|
|
331
399
|
}
|
|
400
|
+
function buildProxyUpstreams(proxyMcp, allMcps) {
|
|
401
|
+
const upstreamNames = new Set(proxyMcp.upstreams || []);
|
|
402
|
+
const upstreamEntries = [];
|
|
403
|
+
const collectedEnv = {};
|
|
404
|
+
for (const mcp of allMcps) {
|
|
405
|
+
if (!upstreamNames.has(mcp.name)) continue;
|
|
406
|
+
if (mcp.url || mcp.image) continue;
|
|
407
|
+
const entry = {
|
|
408
|
+
name: mcp.name,
|
|
409
|
+
command: "npx",
|
|
410
|
+
args: ["-y", mcp.package]
|
|
411
|
+
};
|
|
412
|
+
if (mcp.env) {
|
|
413
|
+
entry.env = {};
|
|
414
|
+
for (const [key, value] of Object.entries(mcp.env)) {
|
|
415
|
+
const envRef = value.match(/^\$\{(?:env:)?(\w+)\}$/);
|
|
416
|
+
if (envRef) {
|
|
417
|
+
entry.env[key] = `\${${envRef[1]}}`;
|
|
418
|
+
collectedEnv[envRef[1]] = value;
|
|
419
|
+
} else {
|
|
420
|
+
entry.env[key] = value;
|
|
421
|
+
collectedEnv[key] = value;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
upstreamEntries.push(entry);
|
|
426
|
+
}
|
|
427
|
+
if (proxyMcp.env) {
|
|
428
|
+
for (const [key, value] of Object.entries(proxyMcp.env)) {
|
|
429
|
+
collectedEnv[key] = value;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return {
|
|
433
|
+
upstreamsJson: JSON.stringify(upstreamEntries),
|
|
434
|
+
collectedEnv
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
function buildProxyMcpEntry(proxyMcp, allMcps, buildEntry) {
|
|
438
|
+
const { upstreamsJson, collectedEnv } = buildProxyUpstreams(proxyMcp, allMcps);
|
|
439
|
+
const env = {
|
|
440
|
+
MCP_PROXY_UPSTREAMS: upstreamsJson,
|
|
441
|
+
...collectedEnv
|
|
442
|
+
};
|
|
443
|
+
return buildEntry({ ...proxyMcp, env });
|
|
444
|
+
}
|
|
445
|
+
function getUpstreamNames(mcps) {
|
|
446
|
+
const names = /* @__PURE__ */ new Set();
|
|
447
|
+
for (const mcp of mcps) {
|
|
448
|
+
if (mcp.upstreams) {
|
|
449
|
+
for (const name of mcp.upstreams) {
|
|
450
|
+
names.add(name);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return names;
|
|
455
|
+
}
|
|
332
456
|
function buildCursorMcpEntry(mcp) {
|
|
333
457
|
if (mcp.url) {
|
|
334
458
|
return { url: mcp.url, ...mcp.env && { env: mcp.env } };
|
|
@@ -369,14 +493,22 @@ function buildClaudeCodeMcpEntry(mcp) {
|
|
|
369
493
|
...mcp.env && { env: mcp.env }
|
|
370
494
|
};
|
|
371
495
|
}
|
|
372
|
-
function
|
|
496
|
+
function stripEnvPrefix(env) {
|
|
497
|
+
const result = {};
|
|
498
|
+
for (const [key, value] of Object.entries(env)) {
|
|
499
|
+
result[key] = value.replace(/\$\{env:(\w+)\}/g, "${$1}");
|
|
500
|
+
}
|
|
501
|
+
return result;
|
|
502
|
+
}
|
|
503
|
+
function buildKiroMcpEntry(mcp, mode = "editor") {
|
|
504
|
+
const env = mcp.env ? mode === "editor" ? stripEnvPrefix(mcp.env) : mcp.env : void 0;
|
|
373
505
|
if (mcp.url) {
|
|
374
|
-
return { url: mcp.url, ...
|
|
506
|
+
return { url: mcp.url, ...env && { env } };
|
|
375
507
|
}
|
|
376
508
|
if (mcp.image) {
|
|
377
509
|
const args = ["run", "-i", "--rm"];
|
|
378
|
-
if (
|
|
379
|
-
for (const [key, value] of Object.entries(
|
|
510
|
+
if (env) {
|
|
511
|
+
for (const [key, value] of Object.entries(env)) {
|
|
380
512
|
args.push("-e", `${key}=${value}`);
|
|
381
513
|
}
|
|
382
514
|
}
|
|
@@ -386,8 +518,380 @@ function buildKiroMcpEntry(mcp) {
|
|
|
386
518
|
return {
|
|
387
519
|
command: "npx",
|
|
388
520
|
args: ["-y", mcp.package],
|
|
389
|
-
...
|
|
521
|
+
...env && { env }
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
function buildKiroAgentContent(rawContent) {
|
|
525
|
+
const fmMatch = rawContent.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
526
|
+
if (!fmMatch) {
|
|
527
|
+
return `---
|
|
528
|
+
name: agent
|
|
529
|
+
tools: ["@builtin"]
|
|
530
|
+
---
|
|
531
|
+
|
|
532
|
+
${rawContent}`;
|
|
533
|
+
}
|
|
534
|
+
const fmBlock = fmMatch[1];
|
|
535
|
+
const body = fmMatch[2];
|
|
536
|
+
const attrs = {};
|
|
537
|
+
for (const line of fmBlock.split("\n")) {
|
|
538
|
+
const match = line.match(/^(\w+):\s*(.+)$/);
|
|
539
|
+
if (match) attrs[match[1]] = match[2].trim();
|
|
540
|
+
}
|
|
541
|
+
const lines = ["---"];
|
|
542
|
+
if (attrs.name) lines.push(`name: ${attrs.name}`);
|
|
543
|
+
if (attrs.description) lines.push(`description: ${attrs.description}`);
|
|
544
|
+
if (attrs.tools) {
|
|
545
|
+
lines.push(`tools: ${attrs.tools}`);
|
|
546
|
+
} else {
|
|
547
|
+
lines.push(`tools: ["@builtin"]`);
|
|
548
|
+
}
|
|
549
|
+
if (attrs.model && attrs.model !== "inherit") {
|
|
550
|
+
lines.push(`model: ${attrs.model}`);
|
|
551
|
+
}
|
|
552
|
+
lines.push("---");
|
|
553
|
+
return `${lines.join("\n")}
|
|
554
|
+
${body}`;
|
|
555
|
+
}
|
|
556
|
+
function buildOpenCodeMcpEntry(mcp) {
|
|
557
|
+
if (mcp.url) {
|
|
558
|
+
return { type: "remote", url: mcp.url };
|
|
559
|
+
}
|
|
560
|
+
if (mcp.image) {
|
|
561
|
+
const cmd = ["docker", "run", "-i", "--rm"];
|
|
562
|
+
if (mcp.env) {
|
|
563
|
+
for (const [key, value] of Object.entries(mcp.env)) {
|
|
564
|
+
cmd.push("-e", `${key}=${value}`);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
cmd.push(mcp.image);
|
|
568
|
+
return { type: "local", command: cmd, ...mcp.env && { environment: mcp.env } };
|
|
569
|
+
}
|
|
570
|
+
return {
|
|
571
|
+
type: "local",
|
|
572
|
+
command: ["npx", "-y", mcp.package],
|
|
573
|
+
...mcp.env && { environment: mcp.env }
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
function buildOpenCodeHooksPlugin(hooks) {
|
|
577
|
+
const handlers = [];
|
|
578
|
+
const seen = /* @__PURE__ */ new Set();
|
|
579
|
+
for (const [event, entries] of Object.entries(hooks)) {
|
|
580
|
+
const mapped = HOOK_EVENT_MAP[event]?.opencode;
|
|
581
|
+
if (!mapped || seen.has(mapped)) continue;
|
|
582
|
+
seen.add(mapped);
|
|
583
|
+
const commandEntries = entries.filter((e) => e.type === "command" && e.command);
|
|
584
|
+
if (commandEntries.length === 0) continue;
|
|
585
|
+
const cmds = commandEntries.map((e) => JSON.stringify(e.command));
|
|
586
|
+
handlers.push(` "${mapped}": async (input, output) => {
|
|
587
|
+
for (const cmd of [${cmds.join(", ")}]) {
|
|
588
|
+
try { await $\`\${cmd}\`; } catch (e) { console.error("Hook failed:", cmd, e); }
|
|
589
|
+
}
|
|
590
|
+
}`);
|
|
591
|
+
}
|
|
592
|
+
if (handlers.length === 0) return null;
|
|
593
|
+
return `// Auto-generated by hub \u2014 maps hub.yaml hooks to OpenCode plugin events
|
|
594
|
+
export const HubHooksPlugin = async ({ $ }) => {
|
|
595
|
+
return {
|
|
596
|
+
${handlers.join(",\n")}
|
|
597
|
+
};
|
|
598
|
+
};
|
|
599
|
+
`;
|
|
600
|
+
}
|
|
601
|
+
function buildOpenCodeAgentMarkdown(name, content) {
|
|
602
|
+
const existingFrontmatter = content.match(/^---\n([\s\S]*?)\n---/);
|
|
603
|
+
let description = `Specialized agent for ${name} tasks`;
|
|
604
|
+
if (existingFrontmatter) {
|
|
605
|
+
const descMatch = existingFrontmatter[1].match(/^description:\s*["']?(.+?)["']?\s*$/m);
|
|
606
|
+
if (descMatch) description = descMatch[1];
|
|
607
|
+
}
|
|
608
|
+
const body = existingFrontmatter ? content.replace(/^---\n[\s\S]*?\n---\n*/, "") : content;
|
|
609
|
+
return `---
|
|
610
|
+
description: "${description}"
|
|
611
|
+
mode: subagent
|
|
612
|
+
tools:
|
|
613
|
+
write: true
|
|
614
|
+
edit: true
|
|
615
|
+
bash: true
|
|
616
|
+
---
|
|
617
|
+
|
|
618
|
+
${body.trim()}
|
|
619
|
+
`;
|
|
620
|
+
}
|
|
621
|
+
function buildOpenCodeOrchestratorRule(config) {
|
|
622
|
+
const taskFolder = config.workflow?.task_folder || "./tasks/<TASK_ID>/";
|
|
623
|
+
const steps = config.workflow?.pipeline || [];
|
|
624
|
+
const prompt = config.workflow?.prompt;
|
|
625
|
+
const enforce = config.workflow?.enforce_workflow ?? false;
|
|
626
|
+
const sections = [];
|
|
627
|
+
sections.push(`# Orchestrator
|
|
628
|
+
|
|
629
|
+
## Your Main Responsibility
|
|
630
|
+
|
|
631
|
+
You are an agent orchestrator. Your job is to ensure that any feature or task requested by the user is completed end-to-end using specialized sub-agents. Use \`@agent-name\` to invoke sub-agents for each phase of the pipeline.`);
|
|
632
|
+
if (enforce) {
|
|
633
|
+
sections.push(`
|
|
634
|
+
## STRICT WORKFLOW ENFORCEMENT
|
|
635
|
+
|
|
636
|
+
**YOU MUST FOLLOW THE PIPELINE DEFINED BELOW. NO EXCEPTIONS.**
|
|
637
|
+
|
|
638
|
+
- NEVER skip a pipeline step, even if the task seems simple or obvious.
|
|
639
|
+
- ALWAYS execute steps in the exact order defined. Do not reorder, merge, or parallelize steps unless the pipeline explicitly allows it.
|
|
640
|
+
- ALWAYS call the designated sub-agent for each step. Do not attempt to perform a step yourself if an agent is assigned to it.
|
|
641
|
+
- ALWAYS wait for a step to complete and validate its output before moving to the next step.
|
|
642
|
+
- If a step produces a document, READ the document and confirm it is complete before proceeding.
|
|
643
|
+
- If a step has unanswered questions or validation issues, RESOLVE them before advancing.
|
|
644
|
+
- NEVER jump directly to coding without completing refinement first.
|
|
645
|
+
- NEVER skip review or QA steps, even for small changes.
|
|
646
|
+
- If the user asks you to skip a step, explain why the pipeline exists and ask for explicit confirmation before proceeding.`);
|
|
647
|
+
}
|
|
648
|
+
if (prompt?.prepend) {
|
|
649
|
+
sections.push(`
|
|
650
|
+
${prompt.prepend.trim()}`);
|
|
651
|
+
}
|
|
652
|
+
if (config.integrations?.linear) {
|
|
653
|
+
const linear = config.integrations.linear;
|
|
654
|
+
sections.push(`
|
|
655
|
+
## Task Management
|
|
656
|
+
|
|
657
|
+
If the user doesn't have a task in their project management tool, create one using the Linear MCP.${linear.team ? ` Add it to the **${linear.team}** team.` : ""} Provide the link to the user so they can review and modify as needed.`);
|
|
658
|
+
}
|
|
659
|
+
sections.push(`
|
|
660
|
+
## Repositories
|
|
661
|
+
`);
|
|
662
|
+
for (const repo of config.repos) {
|
|
663
|
+
const parts = [`- **${repo.path}**`];
|
|
664
|
+
if (repo.description) parts.push(`\u2014 ${repo.description}`);
|
|
665
|
+
else if (repo.tech) parts.push(`\u2014 ${repo.tech}`);
|
|
666
|
+
if (repo.skills?.length) parts.push(`(skills: ${repo.skills.join(", ")})`);
|
|
667
|
+
sections.push(parts.join(" "));
|
|
668
|
+
if (repo.commands) {
|
|
669
|
+
const cmds = Object.entries(repo.commands).filter(([, v]) => v).map(([k, v]) => `\`${k}\`: \`${v}\``).join(", ");
|
|
670
|
+
if (cmds) sections.push(` Commands: ${cmds}`);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
if (prompt?.sections?.after_repositories) {
|
|
674
|
+
sections.push(`
|
|
675
|
+
${prompt.sections.after_repositories.trim()}`);
|
|
676
|
+
}
|
|
677
|
+
const docStructure = buildDocumentStructure(steps, taskFolder);
|
|
678
|
+
sections.push(docStructure);
|
|
679
|
+
const pipelineSection = buildOpenCodePipelineSection(steps);
|
|
680
|
+
sections.push(pipelineSection);
|
|
681
|
+
if (prompt?.sections?.after_pipeline) {
|
|
682
|
+
sections.push(`
|
|
683
|
+
${prompt.sections.after_pipeline.trim()}`);
|
|
684
|
+
}
|
|
685
|
+
if (config.integrations?.slack || config.integrations?.github) {
|
|
686
|
+
sections.push(buildDeliverySection(config));
|
|
687
|
+
}
|
|
688
|
+
if (prompt?.sections?.after_delivery) {
|
|
689
|
+
sections.push(`
|
|
690
|
+
${prompt.sections.after_delivery.trim()}`);
|
|
691
|
+
}
|
|
692
|
+
if (config.memory) {
|
|
693
|
+
sections.push(`
|
|
694
|
+
## Team Memory
|
|
695
|
+
|
|
696
|
+
This workspace has a team memory knowledge base available via the \`team-memory\` MCP.
|
|
697
|
+
|
|
698
|
+
**Before starting any task**, use \`search_memories\` to find relevant context \u2014 past decisions, conventions, known issues, and domain knowledge. This avoids repeating mistakes and ensures consistency with previous choices.
|
|
699
|
+
|
|
700
|
+
**After completing a task**, if you discovered something valuable (a decision, a gotcha, a convention, domain insight), use \`add_memory\` to capture it for the team.
|
|
701
|
+
|
|
702
|
+
Available tools: \`search_memories\`, \`get_memory\`, \`add_memory\`, \`list_memories\`, \`archive_memory\`, \`remove_memory\`.`);
|
|
703
|
+
}
|
|
704
|
+
sections.push(`
|
|
705
|
+
## Troubleshooting and Debugging
|
|
706
|
+
|
|
707
|
+
For bug reports or unexpected behavior, use the \`@debugger\` agent directly.
|
|
708
|
+
It will:
|
|
709
|
+
1. Collect context (symptoms, environment, timeline)
|
|
710
|
+
2. Analyze logs and stack traces
|
|
711
|
+
3. Form and test hypotheses systematically
|
|
712
|
+
4. Identify the root cause
|
|
713
|
+
5. Propose a solution or call coding agents to implement the fix`);
|
|
714
|
+
if (prompt?.sections) {
|
|
715
|
+
const reservedKeys = /* @__PURE__ */ new Set(["after_repositories", "after_pipeline", "after_delivery"]);
|
|
716
|
+
for (const [name, content] of Object.entries(prompt.sections)) {
|
|
717
|
+
if (reservedKeys.has(name)) continue;
|
|
718
|
+
const title = name.split(/[-_]/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
719
|
+
sections.push(`
|
|
720
|
+
## ${title}
|
|
721
|
+
|
|
722
|
+
${content.trim()}`);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
if (prompt?.append) {
|
|
726
|
+
sections.push(`
|
|
727
|
+
${prompt.append.trim()}`);
|
|
728
|
+
}
|
|
729
|
+
return sections.join("\n");
|
|
730
|
+
}
|
|
731
|
+
function buildOpenCodePipelineSection(steps) {
|
|
732
|
+
if (steps.length === 0) {
|
|
733
|
+
return `
|
|
734
|
+
## Development Pipeline
|
|
735
|
+
|
|
736
|
+
1. Use \`@refinement\` to collect requirements
|
|
737
|
+
2. Use \`@coding-backend\` and/or \`@coding-frontend\` agents to implement
|
|
738
|
+
3. Use \`@code-reviewer\` to review the implementation
|
|
739
|
+
4. Use \`@qa-backend\` and/or \`@qa-frontend\` to test
|
|
740
|
+
5. Create PRs and notify the team`;
|
|
741
|
+
}
|
|
742
|
+
const parts = [`
|
|
743
|
+
## Development Pipeline
|
|
744
|
+
`];
|
|
745
|
+
for (const step of steps) {
|
|
746
|
+
if (step.actions) {
|
|
747
|
+
parts.push(`### Delivery`);
|
|
748
|
+
parts.push(`After all validations pass, execute these actions:`);
|
|
749
|
+
for (const action of step.actions) {
|
|
750
|
+
parts.push(`- ${formatAction(action)}`);
|
|
751
|
+
}
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
const stepTitle = step.step.charAt(0).toUpperCase() + step.step.slice(1);
|
|
755
|
+
parts.push(`### ${stepTitle}`);
|
|
756
|
+
if (step.mode === "plan") {
|
|
757
|
+
parts.push(`**This step is a planning phase.** Switch to the Plan agent (Tab key) for collaborative planning with the user before any implementation begins.`);
|
|
758
|
+
parts.push(``);
|
|
759
|
+
}
|
|
760
|
+
if (step.agent) {
|
|
761
|
+
parts.push(`Call \`@${step.agent}\`.${step.output ? ` It writes to \`${step.output}\`.` : ""}`);
|
|
762
|
+
if (step.step === "refinement") {
|
|
763
|
+
parts.push(`
|
|
764
|
+
After it runs, read the document and validate with the user:
|
|
765
|
+
- If there are unanswered questions, ask the user one at a time
|
|
766
|
+
- If the user requests adjustments, send back to the refinement agent
|
|
767
|
+
- Do not proceed until the document is complete and approved by the user`);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
if (Array.isArray(step.agents)) {
|
|
771
|
+
const agentList = step.agents.map((a) => {
|
|
772
|
+
if (typeof a === "string") return { agent: a };
|
|
773
|
+
return a;
|
|
774
|
+
});
|
|
775
|
+
if (step.parallel) {
|
|
776
|
+
parts.push(`Call these agents in parallel:`);
|
|
777
|
+
} else {
|
|
778
|
+
parts.push(`Call these agents in sequence:`);
|
|
779
|
+
}
|
|
780
|
+
for (const a of agentList) {
|
|
781
|
+
let line = `- \`@${a.agent}\``;
|
|
782
|
+
if (a.output) line += ` \u2192 writes to \`${a.output}\``;
|
|
783
|
+
if (a.when) line += ` (when: ${a.when})`;
|
|
784
|
+
parts.push(line);
|
|
785
|
+
}
|
|
786
|
+
if (step.step === "coding" || step.step === "code" || step.step === "implementation") {
|
|
787
|
+
parts.push(`
|
|
788
|
+
If any coding agent has doubts, they will write questions in their document. Apply the same Q&A logic as refinement \u2014 validate with the user before proceeding.`);
|
|
789
|
+
}
|
|
790
|
+
if (step.step === "validation" || step.step === "review" || step.step === "qa") {
|
|
791
|
+
parts.push(`
|
|
792
|
+
If any validation agent leaves comments requiring fixes, call the relevant coding agents again to address them.`);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
if (step.mode === "plan") {
|
|
796
|
+
parts.push(`
|
|
797
|
+
**After this step is complete and approved**, switch back to Build agent to proceed with the next step.`);
|
|
798
|
+
}
|
|
799
|
+
parts.push("");
|
|
800
|
+
}
|
|
801
|
+
return parts.join("\n");
|
|
802
|
+
}
|
|
803
|
+
async function generateOpenCode(config, hubDir) {
|
|
804
|
+
const opencodeDir = join3(hubDir, ".opencode");
|
|
805
|
+
await mkdir2(join3(opencodeDir, "agents"), { recursive: true });
|
|
806
|
+
await mkdir2(join3(opencodeDir, "rules"), { recursive: true });
|
|
807
|
+
await mkdir2(join3(opencodeDir, "skills"), { recursive: true });
|
|
808
|
+
await mkdir2(join3(opencodeDir, "commands"), { recursive: true });
|
|
809
|
+
await mkdir2(join3(opencodeDir, "plugins"), { recursive: true });
|
|
810
|
+
const gitignoreLines = buildGitignoreLines(config);
|
|
811
|
+
await writeManagedFile(join3(hubDir, ".gitignore"), gitignoreLines);
|
|
812
|
+
console.log(chalk2.green(" Generated .gitignore"));
|
|
813
|
+
const orchestratorRule = buildOpenCodeOrchestratorRule(config);
|
|
814
|
+
await writeFile2(join3(opencodeDir, "rules", "orchestrator.md"), orchestratorRule + "\n", "utf-8");
|
|
815
|
+
console.log(chalk2.green(" Generated .opencode/rules/orchestrator.md"));
|
|
816
|
+
const hubSteeringDirOC = resolve(hubDir, "steering");
|
|
817
|
+
try {
|
|
818
|
+
const steeringFiles = await readdir2(hubSteeringDirOC);
|
|
819
|
+
const mdFiles = steeringFiles.filter((f) => f.endsWith(".md"));
|
|
820
|
+
for (const file of mdFiles) {
|
|
821
|
+
const raw = await readFile3(join3(hubSteeringDirOC, file), "utf-8");
|
|
822
|
+
const content = stripFrontMatter(raw);
|
|
823
|
+
await writeFile2(join3(opencodeDir, "rules", file), content, "utf-8");
|
|
824
|
+
}
|
|
825
|
+
if (mdFiles.length > 0) {
|
|
826
|
+
console.log(chalk2.green(` Copied ${mdFiles.length} steering files to .opencode/rules/`));
|
|
827
|
+
}
|
|
828
|
+
} catch {
|
|
829
|
+
}
|
|
830
|
+
const opencodeConfig = {
|
|
831
|
+
$schema: "https://opencode.ai/config.json"
|
|
390
832
|
};
|
|
833
|
+
if (config.mcps?.length) {
|
|
834
|
+
const mcpConfig = {};
|
|
835
|
+
const upstreamSet = getUpstreamNames(config.mcps);
|
|
836
|
+
for (const mcp of config.mcps) {
|
|
837
|
+
if (upstreamSet.has(mcp.name)) continue;
|
|
838
|
+
if (mcp.upstreams?.length) {
|
|
839
|
+
mcpConfig[mcp.name] = buildProxyMcpEntry(mcp, config.mcps, buildOpenCodeMcpEntry);
|
|
840
|
+
} else {
|
|
841
|
+
mcpConfig[mcp.name] = buildOpenCodeMcpEntry(mcp);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
opencodeConfig.mcp = mcpConfig;
|
|
845
|
+
}
|
|
846
|
+
opencodeConfig.instructions = [".opencode/rules/*.md"];
|
|
847
|
+
await writeFile2(
|
|
848
|
+
join3(hubDir, "opencode.json"),
|
|
849
|
+
JSON.stringify(opencodeConfig, null, 2) + "\n",
|
|
850
|
+
"utf-8"
|
|
851
|
+
);
|
|
852
|
+
console.log(chalk2.green(" Generated opencode.json"));
|
|
853
|
+
const agentsDir = resolve(hubDir, "agents");
|
|
854
|
+
try {
|
|
855
|
+
const agentFiles = await readdir2(agentsDir);
|
|
856
|
+
const mdFiles = agentFiles.filter((f) => f.endsWith(".md"));
|
|
857
|
+
for (const file of mdFiles) {
|
|
858
|
+
const content = await readFile3(join3(agentsDir, file), "utf-8");
|
|
859
|
+
const agentName = file.replace(/\.md$/, "");
|
|
860
|
+
const converted = buildOpenCodeAgentMarkdown(agentName, content);
|
|
861
|
+
await writeFile2(join3(opencodeDir, "agents", file), converted, "utf-8");
|
|
862
|
+
}
|
|
863
|
+
console.log(chalk2.green(` Copied ${mdFiles.length} agents to .opencode/agents/`));
|
|
864
|
+
} catch {
|
|
865
|
+
console.log(chalk2.yellow(" No agents/ directory found, skipping agent copy"));
|
|
866
|
+
}
|
|
867
|
+
const skillsDir = resolve(hubDir, "skills");
|
|
868
|
+
try {
|
|
869
|
+
const skillFolders = await readdir2(skillsDir);
|
|
870
|
+
let count = 0;
|
|
871
|
+
for (const folder of skillFolders) {
|
|
872
|
+
const skillFile = join3(skillsDir, folder, "SKILL.md");
|
|
873
|
+
try {
|
|
874
|
+
await readFile3(skillFile);
|
|
875
|
+
await cp(join3(skillsDir, folder), join3(opencodeDir, "skills", folder), { recursive: true });
|
|
876
|
+
count++;
|
|
877
|
+
} catch {
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
if (count > 0) {
|
|
881
|
+
console.log(chalk2.green(` Copied ${count} skills to .opencode/skills/`));
|
|
882
|
+
}
|
|
883
|
+
} catch {
|
|
884
|
+
}
|
|
885
|
+
await fetchHubDocsSkill(join3(opencodeDir, "skills"));
|
|
886
|
+
await generateEditorCommands(config, hubDir, opencodeDir, ".opencode/commands/");
|
|
887
|
+
if (config.hooks) {
|
|
888
|
+
const plugin = buildOpenCodeHooksPlugin(config.hooks);
|
|
889
|
+
if (plugin) {
|
|
890
|
+
await writeFile2(join3(opencodeDir, "plugins", "hub-hooks.js"), plugin, "utf-8");
|
|
891
|
+
console.log(chalk2.green(" Generated .opencode/plugins/hub-hooks.js"));
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
await generateVSCodeSettings(config, hubDir);
|
|
391
895
|
}
|
|
392
896
|
function buildKiroSteeringContent(content, inclusion = "always", meta) {
|
|
393
897
|
const frontMatter = ["---", `inclusion: ${inclusion}`];
|
|
@@ -408,9 +912,9 @@ function buildKiroOrchestratorRule(config) {
|
|
|
408
912
|
|
|
409
913
|
## Your Main Responsibility
|
|
410
914
|
|
|
411
|
-
You are the development orchestrator. Your job is to ensure that any feature or task requested by the user is completed end-to-end by following a structured pipeline. You
|
|
915
|
+
You are the development orchestrator. Your job is to ensure that any feature or task requested by the user is completed end-to-end by following a structured pipeline. You delegate specialized work to subagents defined in \`.kiro/agents/\`.
|
|
412
916
|
|
|
413
|
-
> **Note:** This workspace
|
|
917
|
+
> **Note:** This workspace has custom subagents in \`.kiro/agents/\`. Each pipeline step delegates to the appropriate subagent. Use \`/agent-name\` or instruct Kiro to "use the X subagent" to invoke them.`);
|
|
414
918
|
if (enforce) {
|
|
415
919
|
sections.push(`
|
|
416
920
|
## STRICT WORKFLOW ENFORCEMENT
|
|
@@ -419,7 +923,7 @@ You are the development orchestrator. Your job is to ensure that any feature or
|
|
|
419
923
|
|
|
420
924
|
- NEVER skip a pipeline step, even if the task seems simple or obvious.
|
|
421
925
|
- ALWAYS execute steps in the exact order defined. Do not reorder, merge, or parallelize steps unless the pipeline explicitly allows it.
|
|
422
|
-
- ALWAYS
|
|
926
|
+
- ALWAYS use the designated subagent for each step. Do not improvise if a subagent is assigned.
|
|
423
927
|
- ALWAYS wait for a step to complete and validate its output before moving to the next step.
|
|
424
928
|
- If a step produces a document, READ the document and confirm it is complete before proceeding.
|
|
425
929
|
- If a step has unanswered questions or validation issues, RESOLVE them before advancing.
|
|
@@ -514,18 +1018,18 @@ function buildKiroPipelineSection(steps) {
|
|
|
514
1018
|
return `
|
|
515
1019
|
## Development Pipeline
|
|
516
1020
|
|
|
517
|
-
|
|
1021
|
+
Follow each step sequentially, delegating to the appropriate subagent:
|
|
518
1022
|
|
|
519
|
-
1. **Refinement** \u2014
|
|
520
|
-
2. **Coding** \u2014
|
|
521
|
-
3. **Review** \u2014
|
|
522
|
-
4. **QA** \u2014
|
|
1023
|
+
1. **Refinement** \u2014 Use the \`refinement\` subagent to collect requirements. Write output to the task document.
|
|
1024
|
+
2. **Coding** \u2014 Use the \`coding-backend\` and \`coding-frontend\` subagents to implement the feature.
|
|
1025
|
+
3. **Review** \u2014 Use the \`code-reviewer\` subagent to review the implementation.
|
|
1026
|
+
4. **QA** \u2014 Use the \`qa-backend\` and/or \`qa-frontend\` subagents to test.
|
|
523
1027
|
5. **Delivery** \u2014 Create PRs and notify the team.`;
|
|
524
1028
|
}
|
|
525
1029
|
const parts = [`
|
|
526
1030
|
## Development Pipeline
|
|
527
1031
|
|
|
528
|
-
Follow each step sequentially,
|
|
1032
|
+
Follow each step sequentially, delegating to the appropriate subagent at each phase.
|
|
529
1033
|
`];
|
|
530
1034
|
for (const step of steps) {
|
|
531
1035
|
if (step.actions) {
|
|
@@ -543,7 +1047,7 @@ Follow each step sequentially, applying the role-specific instructions from the
|
|
|
543
1047
|
parts.push(``);
|
|
544
1048
|
}
|
|
545
1049
|
if (step.agent) {
|
|
546
|
-
parts.push(`
|
|
1050
|
+
parts.push(`Use the \`${step.agent}\` subagent.${step.output ? ` Write output to \`${step.output}\`.` : ""}`);
|
|
547
1051
|
if (step.step === "refinement") {
|
|
548
1052
|
parts.push(`
|
|
549
1053
|
After completing the refinement, validate with the user:
|
|
@@ -557,9 +1061,9 @@ After completing the refinement, validate with the user:
|
|
|
557
1061
|
if (typeof a === "string") return { agent: a };
|
|
558
1062
|
return a;
|
|
559
1063
|
});
|
|
560
|
-
parts.push(`
|
|
1064
|
+
parts.push(`Use these subagents sequentially:`);
|
|
561
1065
|
for (const a of agentList) {
|
|
562
|
-
let line = `-
|
|
1066
|
+
let line = `- \`${a.agent}\``;
|
|
563
1067
|
if (a.output) line += ` \u2192 write to \`${a.output}\``;
|
|
564
1068
|
if (a.when) line += ` (when: ${a.when})`;
|
|
565
1069
|
parts.push(line);
|
|
@@ -801,8 +1305,9 @@ function buildDeliverySection(config) {
|
|
|
801
1305
|
`];
|
|
802
1306
|
if (config.integrations?.github) {
|
|
803
1307
|
const gh = config.integrations.github;
|
|
1308
|
+
const tool = gh.pr_tool === "mcp" ? "GitHub MCP" : "GitHub CLI";
|
|
804
1309
|
parts.push(`### Pull Requests`);
|
|
805
|
-
parts.push(`For each repository with changes, push the branch and create a PR using the
|
|
1310
|
+
parts.push(`For each repository with changes, push the branch and create a PR using the ${tool}.`);
|
|
806
1311
|
if (gh.pr_branch_pattern) {
|
|
807
1312
|
parts.push(`Branch naming pattern: \`${gh.pr_branch_pattern}\``);
|
|
808
1313
|
}
|
|
@@ -882,12 +1387,37 @@ async function generateClaudeCode(config, hubDir) {
|
|
|
882
1387
|
}
|
|
883
1388
|
} catch {
|
|
884
1389
|
}
|
|
885
|
-
|
|
1390
|
+
const claudeSkillsDirForDocs = join3(claudeDir, "skills");
|
|
1391
|
+
await mkdir2(claudeSkillsDirForDocs, { recursive: true });
|
|
1392
|
+
await fetchHubDocsSkill(claudeSkillsDirForDocs);
|
|
1393
|
+
const hubSteeringDirClaude = resolve(hubDir, "steering");
|
|
1394
|
+
try {
|
|
1395
|
+
const steeringFiles = await readdir2(hubSteeringDirClaude);
|
|
1396
|
+
const mdFiles = steeringFiles.filter((f) => f.endsWith(".md"));
|
|
1397
|
+
for (const file of mdFiles) {
|
|
1398
|
+
const raw = await readFile3(join3(hubSteeringDirClaude, file), "utf-8");
|
|
1399
|
+
const content = stripFrontMatter(raw).trim();
|
|
1400
|
+
if (content) {
|
|
1401
|
+
claudeMdSections.push(content);
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
if (mdFiles.length > 0) {
|
|
1405
|
+
console.log(chalk2.green(` Appended ${mdFiles.length} steering files to CLAUDE.md`));
|
|
1406
|
+
}
|
|
1407
|
+
} catch {
|
|
1408
|
+
}
|
|
1409
|
+
await writeFile2(join3(hubDir, "CLAUDE.md"), claudeMdSections.join("\n\n"), "utf-8");
|
|
886
1410
|
console.log(chalk2.green(" Generated CLAUDE.md"));
|
|
887
1411
|
if (config.mcps?.length) {
|
|
888
1412
|
const mcpJson = {};
|
|
1413
|
+
const upstreamSet = getUpstreamNames(config.mcps);
|
|
889
1414
|
for (const mcp of config.mcps) {
|
|
890
|
-
|
|
1415
|
+
if (upstreamSet.has(mcp.name)) continue;
|
|
1416
|
+
if (mcp.upstreams?.length) {
|
|
1417
|
+
mcpJson[mcp.name] = buildProxyMcpEntry(mcp, config.mcps, buildClaudeCodeMcpEntry);
|
|
1418
|
+
} else {
|
|
1419
|
+
mcpJson[mcp.name] = buildClaudeCodeMcpEntry(mcp);
|
|
1420
|
+
}
|
|
891
1421
|
}
|
|
892
1422
|
await writeFile2(
|
|
893
1423
|
join3(hubDir, ".mcp.json"),
|
|
@@ -948,6 +1478,25 @@ async function generateKiro(config, hubDir) {
|
|
|
948
1478
|
const settingsDir = join3(kiroDir, "settings");
|
|
949
1479
|
await mkdir2(steeringDir, { recursive: true });
|
|
950
1480
|
await mkdir2(settingsDir, { recursive: true });
|
|
1481
|
+
let mode = await getKiroMode(hubDir);
|
|
1482
|
+
if (!mode) {
|
|
1483
|
+
const { kiroMode } = await inquirer.prompt([
|
|
1484
|
+
{
|
|
1485
|
+
type: "list",
|
|
1486
|
+
name: "kiroMode",
|
|
1487
|
+
message: "How do you use Kiro?",
|
|
1488
|
+
choices: [
|
|
1489
|
+
{ name: "Editor / IDE (e.g. Kiro IDE, VS Code)", value: "editor" },
|
|
1490
|
+
{ name: "CLI (e.g. kiro-cli)", value: "cli" }
|
|
1491
|
+
]
|
|
1492
|
+
}
|
|
1493
|
+
]);
|
|
1494
|
+
mode = kiroMode;
|
|
1495
|
+
await saveKiroMode(hubDir, mode);
|
|
1496
|
+
console.log(chalk2.dim(` Saved Kiro mode: ${mode}`));
|
|
1497
|
+
} else {
|
|
1498
|
+
console.log(chalk2.dim(` Using saved Kiro mode: ${mode}`));
|
|
1499
|
+
}
|
|
951
1500
|
const gitignoreLines = buildGitignoreLines(config);
|
|
952
1501
|
await writeManagedFile(join3(hubDir, ".gitignore"), gitignoreLines);
|
|
953
1502
|
console.log(chalk2.green(" Generated .gitignore"));
|
|
@@ -957,21 +1506,33 @@ async function generateKiro(config, hubDir) {
|
|
|
957
1506
|
console.log(chalk2.green(" Generated .kiro/steering/orchestrator.md"));
|
|
958
1507
|
await writeFile2(join3(hubDir, "AGENTS.md"), kiroRule + "\n", "utf-8");
|
|
959
1508
|
console.log(chalk2.green(" Generated AGENTS.md"));
|
|
1509
|
+
const hubSteeringDir = resolve(hubDir, "steering");
|
|
1510
|
+
try {
|
|
1511
|
+
const steeringFiles = await readdir2(hubSteeringDir);
|
|
1512
|
+
const mdFiles = steeringFiles.filter((f) => f.endsWith(".md"));
|
|
1513
|
+
for (const file of mdFiles) {
|
|
1514
|
+
const raw = await readFile3(join3(hubSteeringDir, file), "utf-8");
|
|
1515
|
+
const content = stripFrontMatter(raw);
|
|
1516
|
+
const kiroSteering = buildKiroSteeringContent(content);
|
|
1517
|
+
await writeFile2(join3(steeringDir, file), kiroSteering, "utf-8");
|
|
1518
|
+
}
|
|
1519
|
+
if (mdFiles.length > 0) {
|
|
1520
|
+
console.log(chalk2.green(` Copied ${mdFiles.length} steering files to .kiro/steering/`));
|
|
1521
|
+
}
|
|
1522
|
+
} catch {
|
|
1523
|
+
}
|
|
960
1524
|
const agentsDir = resolve(hubDir, "agents");
|
|
961
1525
|
try {
|
|
1526
|
+
const kiroAgentsDir = join3(kiroDir, "agents");
|
|
1527
|
+
await mkdir2(kiroAgentsDir, { recursive: true });
|
|
962
1528
|
const agentFiles = await readdir2(agentsDir);
|
|
963
1529
|
const mdFiles = agentFiles.filter((f) => f.endsWith(".md"));
|
|
964
1530
|
for (const file of mdFiles) {
|
|
965
1531
|
const agentContent = await readFile3(join3(agentsDir, file), "utf-8");
|
|
966
|
-
const
|
|
967
|
-
|
|
968
|
-
name: agentName,
|
|
969
|
-
description: `Role-specific instructions for the ${agentName} phase. Include when working on ${agentName}-related tasks.`
|
|
970
|
-
});
|
|
971
|
-
const steeringName = `agent-${file}`;
|
|
972
|
-
await writeFile2(join3(steeringDir, steeringName), steeringContent, "utf-8");
|
|
1532
|
+
const kiroAgent = buildKiroAgentContent(agentContent);
|
|
1533
|
+
await writeFile2(join3(kiroAgentsDir, file), kiroAgent, "utf-8");
|
|
973
1534
|
}
|
|
974
|
-
console.log(chalk2.green(` Copied ${mdFiles.length} agents
|
|
1535
|
+
console.log(chalk2.green(` Copied ${mdFiles.length} agents to .kiro/agents/`));
|
|
975
1536
|
} catch {
|
|
976
1537
|
console.log(chalk2.yellow(" No agents/ directory found, skipping agent copy"));
|
|
977
1538
|
}
|
|
@@ -997,10 +1558,20 @@ async function generateKiro(config, hubDir) {
|
|
|
997
1558
|
}
|
|
998
1559
|
} catch {
|
|
999
1560
|
}
|
|
1561
|
+
const kiroSkillsDirForDocs = join3(kiroDir, "skills");
|
|
1562
|
+
await mkdir2(kiroSkillsDirForDocs, { recursive: true });
|
|
1563
|
+
await fetchHubDocsSkill(kiroSkillsDirForDocs);
|
|
1000
1564
|
if (config.mcps?.length) {
|
|
1001
1565
|
const mcpConfig = {};
|
|
1566
|
+
const upstreamSet = getUpstreamNames(config.mcps);
|
|
1567
|
+
const buildEntry = (mcp) => buildKiroMcpEntry(mcp, mode);
|
|
1002
1568
|
for (const mcp of config.mcps) {
|
|
1003
|
-
|
|
1569
|
+
if (upstreamSet.has(mcp.name)) continue;
|
|
1570
|
+
if (mcp.upstreams?.length) {
|
|
1571
|
+
mcpConfig[mcp.name] = buildProxyMcpEntry(mcp, config.mcps, buildEntry);
|
|
1572
|
+
} else {
|
|
1573
|
+
mcpConfig[mcp.name] = buildKiroMcpEntry(mcp, mode);
|
|
1574
|
+
}
|
|
1004
1575
|
}
|
|
1005
1576
|
await writeFile2(
|
|
1006
1577
|
join3(settingsDir, "mcp.json"),
|
|
@@ -1049,6 +1620,59 @@ async function generateVSCodeSettings(config, hubDir) {
|
|
|
1049
1620
|
const merged = { ...existing, ...managed };
|
|
1050
1621
|
await writeFile2(settingsPath, JSON.stringify(merged, null, 2) + "\n", "utf-8");
|
|
1051
1622
|
console.log(chalk2.green(" Generated .vscode/settings.json (git multi-repo detection)"));
|
|
1623
|
+
const workspaceFile = `${config.name}.code-workspace`;
|
|
1624
|
+
const workspacePath = join3(hubDir, workspaceFile);
|
|
1625
|
+
let existingWorkspace = {};
|
|
1626
|
+
if (existsSync2(workspacePath)) {
|
|
1627
|
+
try {
|
|
1628
|
+
const raw = await readFile3(workspacePath, "utf-8");
|
|
1629
|
+
existingWorkspace = JSON.parse(raw);
|
|
1630
|
+
} catch {
|
|
1631
|
+
existingWorkspace = {};
|
|
1632
|
+
}
|
|
1633
|
+
} else {
|
|
1634
|
+
const files = await readdir2(hubDir);
|
|
1635
|
+
const existing2 = files.find((f) => f.endsWith(".code-workspace"));
|
|
1636
|
+
if (existing2) {
|
|
1637
|
+
try {
|
|
1638
|
+
const raw = await readFile3(join3(hubDir, existing2), "utf-8");
|
|
1639
|
+
existingWorkspace = JSON.parse(raw);
|
|
1640
|
+
} catch {
|
|
1641
|
+
existingWorkspace = {};
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
const TECH_LABELS = {
|
|
1646
|
+
nestjs: "NestJS",
|
|
1647
|
+
nextjs: "Next.js",
|
|
1648
|
+
react: "React",
|
|
1649
|
+
elixir: "Elixir",
|
|
1650
|
+
phoenix: "Phoenix",
|
|
1651
|
+
django: "Django",
|
|
1652
|
+
fastapi: "FastAPI",
|
|
1653
|
+
rails: "Rails",
|
|
1654
|
+
spring: "Spring",
|
|
1655
|
+
go: "Go",
|
|
1656
|
+
vue: "Vue",
|
|
1657
|
+
svelte: "Svelte",
|
|
1658
|
+
angular: "Angular",
|
|
1659
|
+
express: "Express",
|
|
1660
|
+
koa: "Koa"
|
|
1661
|
+
};
|
|
1662
|
+
const folders = [
|
|
1663
|
+
{ path: ".", name: "Root" }
|
|
1664
|
+
];
|
|
1665
|
+
for (const repo of config.repos) {
|
|
1666
|
+
const repoPath = repo.path.replace(/^\.\//, "");
|
|
1667
|
+
const displayName = repo.display_name || (repo.tech ? `${repo.name} (${TECH_LABELS[repo.tech] || repo.tech})` : repo.name);
|
|
1668
|
+
folders.push({ path: repoPath, name: displayName });
|
|
1669
|
+
}
|
|
1670
|
+
const workspace = {
|
|
1671
|
+
folders,
|
|
1672
|
+
settings: existingWorkspace.settings || {}
|
|
1673
|
+
};
|
|
1674
|
+
await writeFile2(workspacePath, JSON.stringify(workspace, null, " ") + "\n", "utf-8");
|
|
1675
|
+
console.log(chalk2.green(` Generated ${workspaceFile}`));
|
|
1052
1676
|
}
|
|
1053
1677
|
function buildGitignoreLines(config) {
|
|
1054
1678
|
const lines = [
|
|
@@ -1092,7 +1716,8 @@ function buildGitignoreLines(config) {
|
|
|
1092
1716
|
var generators = {
|
|
1093
1717
|
cursor: { name: "Cursor", generate: generateCursor },
|
|
1094
1718
|
"claude-code": { name: "Claude Code", generate: generateClaudeCode },
|
|
1095
|
-
kiro: { name: "Kiro", generate: generateKiro }
|
|
1719
|
+
kiro: { name: "Kiro", generate: generateKiro },
|
|
1720
|
+
opencode: { name: "OpenCode", generate: generateOpenCode }
|
|
1096
1721
|
};
|
|
1097
1722
|
async function resolveEditor(opts) {
|
|
1098
1723
|
const hubDir = process.cwd();
|
|
@@ -1108,6 +1733,9 @@ async function resolveEditor(opts) {
|
|
|
1108
1733
|
}))
|
|
1109
1734
|
}
|
|
1110
1735
|
]);
|
|
1736
|
+
const cache = await readCache(hubDir);
|
|
1737
|
+
delete cache.kiroMode;
|
|
1738
|
+
await writeCache(hubDir, cache);
|
|
1111
1739
|
return editor2;
|
|
1112
1740
|
}
|
|
1113
1741
|
if (opts.editor) return opts.editor;
|
|
@@ -1126,7 +1754,7 @@ async function resolveEditor(opts) {
|
|
|
1126
1754
|
]);
|
|
1127
1755
|
return editor;
|
|
1128
1756
|
}
|
|
1129
|
-
var generateCommand = new Command("generate").description("Generate editor-specific configuration files from hub.yaml").option("-e, --editor <editor>", "Target editor (cursor, claude-code, kiro)").option("--reset-editor", "Reset saved editor preference and choose again").action(async (opts) => {
|
|
1757
|
+
var generateCommand = new Command("generate").description("Generate editor-specific configuration files from hub.yaml").option("-e, --editor <editor>", "Target editor (cursor, claude-code, kiro, opencode)").option("--reset-editor", "Reset saved editor preference and choose again").action(async (opts) => {
|
|
1130
1758
|
const hubDir = process.cwd();
|
|
1131
1759
|
const config = await loadHubConfig(hubDir);
|
|
1132
1760
|
if (config.memory) {
|
package/dist/index.js
CHANGED
|
@@ -3,10 +3,10 @@ import {
|
|
|
3
3
|
checkAndAutoRegenerate,
|
|
4
4
|
generateCommand,
|
|
5
5
|
loadHubConfig
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-2NWNZCYP.js";
|
|
7
7
|
|
|
8
8
|
// src/index.ts
|
|
9
|
-
import { Command as
|
|
9
|
+
import { Command as Command19 } from "commander";
|
|
10
10
|
|
|
11
11
|
// src/commands/init.ts
|
|
12
12
|
import { Command } from "commander";
|
|
@@ -2852,11 +2852,327 @@ var directoryCommand = new Command17("directory").alias("dir").description("Brow
|
|
|
2852
2852
|
console.log(chalk17.dim(" hub commands add <owner>/<repo>\n"));
|
|
2853
2853
|
});
|
|
2854
2854
|
|
|
2855
|
+
// src/commands/scan.ts
|
|
2856
|
+
import { Command as Command18 } from "commander";
|
|
2857
|
+
import { existsSync as existsSync14 } from "fs";
|
|
2858
|
+
import { readdir as readdir6, readFile as readFile8, cp as cp4, mkdir as mkdir9, writeFile as writeFile11 } from "fs/promises";
|
|
2859
|
+
import { execSync as execSync13 } from "child_process";
|
|
2860
|
+
import { join as join17 } from "path";
|
|
2861
|
+
import { parse as parse2 } from "yaml";
|
|
2862
|
+
import chalk18 from "chalk";
|
|
2863
|
+
import inquirer from "inquirer";
|
|
2864
|
+
var EDITOR_DIRS = [".kiro", ".cursor", ".opencode", ".claude"];
|
|
2865
|
+
async function findUnregisteredRepos(hubDir, config) {
|
|
2866
|
+
const registeredPaths = new Set(
|
|
2867
|
+
config.repos.map((r) => r.path.replace(/^\.\//, ""))
|
|
2868
|
+
);
|
|
2869
|
+
const entries = await readdir6(hubDir);
|
|
2870
|
+
const unregistered = [];
|
|
2871
|
+
for (const entry of entries) {
|
|
2872
|
+
if (registeredPaths.has(entry)) continue;
|
|
2873
|
+
const gitDir = join17(hubDir, entry, ".git");
|
|
2874
|
+
if (existsSync14(gitDir)) {
|
|
2875
|
+
unregistered.push(entry);
|
|
2876
|
+
}
|
|
2877
|
+
}
|
|
2878
|
+
return unregistered;
|
|
2879
|
+
}
|
|
2880
|
+
function detectTech(repoDir) {
|
|
2881
|
+
if (existsSync14(join17(repoDir, "mix.exs"))) return "elixir";
|
|
2882
|
+
if (existsSync14(join17(repoDir, "next.config.js")) || existsSync14(join17(repoDir, "next.config.ts")) || existsSync14(join17(repoDir, "next.config.mjs"))) return "nextjs";
|
|
2883
|
+
if (existsSync14(join17(repoDir, "nest-cli.json"))) return "nestjs";
|
|
2884
|
+
if (existsSync14(join17(repoDir, "angular.json"))) return "angular";
|
|
2885
|
+
if (existsSync14(join17(repoDir, "svelte.config.js"))) return "svelte";
|
|
2886
|
+
if (existsSync14(join17(repoDir, "nuxt.config.ts")) || existsSync14(join17(repoDir, "nuxt.config.js"))) return "vue";
|
|
2887
|
+
if (existsSync14(join17(repoDir, "go.mod"))) return "go";
|
|
2888
|
+
if (existsSync14(join17(repoDir, "Gemfile"))) return "rails";
|
|
2889
|
+
if (existsSync14(join17(repoDir, "manage.py"))) return "django";
|
|
2890
|
+
if (existsSync14(join17(repoDir, "package.json"))) return "react";
|
|
2891
|
+
return void 0;
|
|
2892
|
+
}
|
|
2893
|
+
function getGitRemote(repoDir) {
|
|
2894
|
+
try {
|
|
2895
|
+
return execSync13("git remote get-url origin", { cwd: repoDir, encoding: "utf-8" }).trim();
|
|
2896
|
+
} catch {
|
|
2897
|
+
return "";
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2900
|
+
function buildRepoYaml(repo) {
|
|
2901
|
+
const lines = [];
|
|
2902
|
+
lines.push(` - name: ${repo.name}`);
|
|
2903
|
+
lines.push(` path: ${repo.path}`);
|
|
2904
|
+
lines.push(` url: ${repo.url}`);
|
|
2905
|
+
if (repo.tech) lines.push(` tech: ${repo.tech}`);
|
|
2906
|
+
return lines.join("\n");
|
|
2907
|
+
}
|
|
2908
|
+
function findReposInsertionPoint(content) {
|
|
2909
|
+
const lines = content.split("\n");
|
|
2910
|
+
let lastRepoLine = -1;
|
|
2911
|
+
let inRepos = false;
|
|
2912
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2913
|
+
const line = lines[i];
|
|
2914
|
+
if (/^repos:/.test(line)) {
|
|
2915
|
+
inRepos = true;
|
|
2916
|
+
continue;
|
|
2917
|
+
}
|
|
2918
|
+
if (inRepos) {
|
|
2919
|
+
if (/^[a-z]/.test(line) || /^[A-Z]/.test(line)) break;
|
|
2920
|
+
if (line.trim() !== "") lastRepoLine = i;
|
|
2921
|
+
}
|
|
2922
|
+
}
|
|
2923
|
+
if (lastRepoLine === -1) return content.length;
|
|
2924
|
+
let offset = 0;
|
|
2925
|
+
for (let i = 0; i <= lastRepoLine; i++) {
|
|
2926
|
+
offset += lines[i].length + 1;
|
|
2927
|
+
}
|
|
2928
|
+
return offset;
|
|
2929
|
+
}
|
|
2930
|
+
async function findUnsyncedAssets(hubDir) {
|
|
2931
|
+
const unsynced = [];
|
|
2932
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2933
|
+
for (const editor of EDITOR_DIRS) {
|
|
2934
|
+
const editorSkillsDir = join17(hubDir, editor, "skills");
|
|
2935
|
+
if (!existsSync14(editorSkillsDir)) continue;
|
|
2936
|
+
try {
|
|
2937
|
+
const folders = await readdir6(editorSkillsDir);
|
|
2938
|
+
for (const folder of folders) {
|
|
2939
|
+
if (folder === "hub-docs") continue;
|
|
2940
|
+
const skillFile = join17(editorSkillsDir, folder, "SKILL.md");
|
|
2941
|
+
if (!existsSync14(skillFile)) continue;
|
|
2942
|
+
const canonicalSkillFile = join17(hubDir, "skills", folder, "SKILL.md");
|
|
2943
|
+
if (existsSync14(canonicalSkillFile)) continue;
|
|
2944
|
+
const key = `skill:${folder}`;
|
|
2945
|
+
if (seen.has(key)) continue;
|
|
2946
|
+
seen.add(key);
|
|
2947
|
+
unsynced.push({ type: "skill", name: folder, source: editor });
|
|
2948
|
+
}
|
|
2949
|
+
} catch {
|
|
2950
|
+
}
|
|
2951
|
+
}
|
|
2952
|
+
for (const editor of EDITOR_DIRS) {
|
|
2953
|
+
const editorAgentsDir = join17(hubDir, editor, "agents");
|
|
2954
|
+
if (!existsSync14(editorAgentsDir)) continue;
|
|
2955
|
+
try {
|
|
2956
|
+
const files = await readdir6(editorAgentsDir);
|
|
2957
|
+
for (const file of files) {
|
|
2958
|
+
if (!file.endsWith(".md")) continue;
|
|
2959
|
+
const canonicalFile = join17(hubDir, "agents", file);
|
|
2960
|
+
if (existsSync14(canonicalFile)) continue;
|
|
2961
|
+
const key = `agent:${file}`;
|
|
2962
|
+
if (seen.has(key)) continue;
|
|
2963
|
+
seen.add(key);
|
|
2964
|
+
unsynced.push({ type: "agent", name: file, source: editor });
|
|
2965
|
+
}
|
|
2966
|
+
} catch {
|
|
2967
|
+
}
|
|
2968
|
+
}
|
|
2969
|
+
const steeringDir = join17(hubDir, ".kiro", "steering");
|
|
2970
|
+
if (existsSync14(steeringDir)) {
|
|
2971
|
+
const canonicalSteeringDir = join17(hubDir, "steering");
|
|
2972
|
+
try {
|
|
2973
|
+
const files = await readdir6(steeringDir);
|
|
2974
|
+
for (const file of files) {
|
|
2975
|
+
if (!file.endsWith(".md")) continue;
|
|
2976
|
+
if (file === "orchestrator.md") continue;
|
|
2977
|
+
const canonicalFile = join17(canonicalSteeringDir, file);
|
|
2978
|
+
if (existsSync14(canonicalFile)) continue;
|
|
2979
|
+
const key = `steering:${file}`;
|
|
2980
|
+
if (seen.has(key)) continue;
|
|
2981
|
+
seen.add(key);
|
|
2982
|
+
unsynced.push({ type: "steering", name: file, source: ".kiro" });
|
|
2983
|
+
}
|
|
2984
|
+
} catch {
|
|
2985
|
+
}
|
|
2986
|
+
}
|
|
2987
|
+
const cursorRulesDir = join17(hubDir, ".cursor", "rules");
|
|
2988
|
+
if (existsSync14(cursorRulesDir)) {
|
|
2989
|
+
const canonicalSteeringDir = join17(hubDir, "steering");
|
|
2990
|
+
try {
|
|
2991
|
+
const files = await readdir6(cursorRulesDir);
|
|
2992
|
+
for (const file of files) {
|
|
2993
|
+
if (!file.endsWith(".mdc")) continue;
|
|
2994
|
+
if (file === "orchestrator.mdc") continue;
|
|
2995
|
+
const mdName = file.replace(/\.mdc$/, ".md");
|
|
2996
|
+
const canonicalFile = join17(canonicalSteeringDir, mdName);
|
|
2997
|
+
if (existsSync14(canonicalFile)) continue;
|
|
2998
|
+
const key = `steering:${mdName}`;
|
|
2999
|
+
if (seen.has(key)) continue;
|
|
3000
|
+
seen.add(key);
|
|
3001
|
+
unsynced.push({ type: "steering", name: mdName, source: ".cursor" });
|
|
3002
|
+
}
|
|
3003
|
+
} catch {
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
const opencodeRulesDir = join17(hubDir, ".opencode", "rules");
|
|
3007
|
+
if (existsSync14(opencodeRulesDir)) {
|
|
3008
|
+
const canonicalSteeringDir = join17(hubDir, "steering");
|
|
3009
|
+
try {
|
|
3010
|
+
const files = await readdir6(opencodeRulesDir);
|
|
3011
|
+
for (const file of files) {
|
|
3012
|
+
if (!file.endsWith(".md")) continue;
|
|
3013
|
+
if (file === "orchestrator.md") continue;
|
|
3014
|
+
const canonicalFile = join17(canonicalSteeringDir, file);
|
|
3015
|
+
if (existsSync14(canonicalFile)) continue;
|
|
3016
|
+
const key = `steering:${file}`;
|
|
3017
|
+
if (seen.has(key)) continue;
|
|
3018
|
+
seen.add(key);
|
|
3019
|
+
unsynced.push({ type: "steering", name: file, source: ".opencode" });
|
|
3020
|
+
}
|
|
3021
|
+
} catch {
|
|
3022
|
+
}
|
|
3023
|
+
}
|
|
3024
|
+
return unsynced;
|
|
3025
|
+
}
|
|
3026
|
+
function stripFrontMatter(content) {
|
|
3027
|
+
const match = content.match(/^---\n[\s\S]*?\n---\n*/);
|
|
3028
|
+
if (match) return content.slice(match[0].length);
|
|
3029
|
+
return content;
|
|
3030
|
+
}
|
|
3031
|
+
async function syncAssets(hubDir, assets) {
|
|
3032
|
+
for (const asset of assets) {
|
|
3033
|
+
if (asset.type === "skill") {
|
|
3034
|
+
const src = join17(hubDir, asset.source, "skills", asset.name);
|
|
3035
|
+
const dest = join17(hubDir, "skills", asset.name);
|
|
3036
|
+
await mkdir9(join17(hubDir, "skills"), { recursive: true });
|
|
3037
|
+
await cp4(src, dest, { recursive: true });
|
|
3038
|
+
const skillMd = join17(dest, "SKILL.md");
|
|
3039
|
+
if (existsSync14(skillMd)) {
|
|
3040
|
+
const raw = await readFile8(skillMd, "utf-8");
|
|
3041
|
+
const cleaned = stripFrontMatter(raw);
|
|
3042
|
+
if (cleaned !== raw) {
|
|
3043
|
+
await writeFile11(skillMd, cleaned, "utf-8");
|
|
3044
|
+
}
|
|
3045
|
+
}
|
|
3046
|
+
console.log(chalk18.green(` Synced skill: ${asset.name} (from ${asset.source})`));
|
|
3047
|
+
} else if (asset.type === "agent") {
|
|
3048
|
+
const src = join17(hubDir, asset.source, "agents", asset.name);
|
|
3049
|
+
const dest = join17(hubDir, "agents", asset.name);
|
|
3050
|
+
await mkdir9(join17(hubDir, "agents"), { recursive: true });
|
|
3051
|
+
const raw = await readFile8(src, "utf-8");
|
|
3052
|
+
const cleaned = stripFrontMatter(raw);
|
|
3053
|
+
await writeFile11(dest, cleaned, "utf-8");
|
|
3054
|
+
console.log(chalk18.green(` Synced agent: ${asset.name} (from ${asset.source})`));
|
|
3055
|
+
} else if (asset.type === "steering") {
|
|
3056
|
+
const dest = join17(hubDir, "steering", asset.name);
|
|
3057
|
+
await mkdir9(join17(hubDir, "steering"), { recursive: true });
|
|
3058
|
+
let raw;
|
|
3059
|
+
if (asset.source === ".cursor") {
|
|
3060
|
+
const mdcName = asset.name.replace(/\.md$/, ".mdc");
|
|
3061
|
+
raw = await readFile8(join17(hubDir, ".cursor", "rules", mdcName), "utf-8");
|
|
3062
|
+
} else if (asset.source === ".opencode") {
|
|
3063
|
+
raw = await readFile8(join17(hubDir, ".opencode", "rules", asset.name), "utf-8");
|
|
3064
|
+
} else {
|
|
3065
|
+
raw = await readFile8(join17(hubDir, asset.source, "steering", asset.name), "utf-8");
|
|
3066
|
+
}
|
|
3067
|
+
const cleaned = stripFrontMatter(raw);
|
|
3068
|
+
await writeFile11(dest, cleaned, "utf-8");
|
|
3069
|
+
console.log(chalk18.green(` Synced steering: ${asset.name} (from ${asset.source})`));
|
|
3070
|
+
}
|
|
3071
|
+
}
|
|
3072
|
+
}
|
|
3073
|
+
var scanCommand = new Command18("scan").description("Detect git repositories not registered in hub.yaml").option("-y, --yes", "Auto-add all found repos without prompting").action(async (opts) => {
|
|
3074
|
+
const hubDir = process.cwd();
|
|
3075
|
+
const configPath = join17(hubDir, "hub.yaml");
|
|
3076
|
+
if (!existsSync14(configPath)) {
|
|
3077
|
+
console.log(chalk18.red("No hub.yaml found in current directory."));
|
|
3078
|
+
process.exit(1);
|
|
3079
|
+
}
|
|
3080
|
+
const content = await readFile8(configPath, "utf-8");
|
|
3081
|
+
const config = parse2(content);
|
|
3082
|
+
let hasChanges = false;
|
|
3083
|
+
console.log(chalk18.blue("\nScanning for unregistered repositories...\n"));
|
|
3084
|
+
const unregistered = await findUnregisteredRepos(hubDir, config);
|
|
3085
|
+
if (unregistered.length === 0) {
|
|
3086
|
+
console.log(chalk18.green("All repositories are registered in hub.yaml."));
|
|
3087
|
+
} else {
|
|
3088
|
+
console.log(chalk18.yellow(`Found ${unregistered.length} unregistered repo(s):
|
|
3089
|
+
`));
|
|
3090
|
+
const repoDetails = unregistered.map((name) => {
|
|
3091
|
+
const repoDir = join17(hubDir, name);
|
|
3092
|
+
const tech = detectTech(repoDir);
|
|
3093
|
+
const url = getGitRemote(repoDir);
|
|
3094
|
+
return { name, tech, url, path: `./${name}` };
|
|
3095
|
+
});
|
|
3096
|
+
for (const repo of repoDetails) {
|
|
3097
|
+
const techLabel = repo.tech ? chalk18.dim(` (${repo.tech})`) : "";
|
|
3098
|
+
console.log(` ${chalk18.cyan(repo.name)}${techLabel}`);
|
|
3099
|
+
}
|
|
3100
|
+
console.log();
|
|
3101
|
+
let toAdd = repoDetails;
|
|
3102
|
+
if (!opts.yes) {
|
|
3103
|
+
const { selected } = await inquirer.prompt([
|
|
3104
|
+
{
|
|
3105
|
+
type: "checkbox",
|
|
3106
|
+
name: "selected",
|
|
3107
|
+
message: "Select repos to add to hub.yaml:",
|
|
3108
|
+
choices: repoDetails.map((r) => ({
|
|
3109
|
+
name: `${r.name}${r.tech ? ` (${r.tech})` : ""}`,
|
|
3110
|
+
value: r.name,
|
|
3111
|
+
checked: true
|
|
3112
|
+
}))
|
|
3113
|
+
}
|
|
3114
|
+
]);
|
|
3115
|
+
toAdd = repoDetails.filter((r) => selected.includes(r.name));
|
|
3116
|
+
}
|
|
3117
|
+
if (toAdd.length > 0) {
|
|
3118
|
+
const originalContent = await readFile8(configPath, "utf-8");
|
|
3119
|
+
const insertAt = findReposInsertionPoint(originalContent);
|
|
3120
|
+
const before = originalContent.slice(0, insertAt);
|
|
3121
|
+
const after = originalContent.slice(insertAt);
|
|
3122
|
+
const newEntries = toAdd.map(buildRepoYaml).join("\n");
|
|
3123
|
+
const updatedContent = before + newEntries + "\n" + after;
|
|
3124
|
+
await import("fs/promises").then((fs) => fs.writeFile(configPath, updatedContent, "utf-8"));
|
|
3125
|
+
console.log(chalk18.green(`Added ${toAdd.length} repo(s) to hub.yaml.`));
|
|
3126
|
+
hasChanges = true;
|
|
3127
|
+
}
|
|
3128
|
+
}
|
|
3129
|
+
console.log(chalk18.blue("\nScanning for unsynced skills, agents, and steering...\n"));
|
|
3130
|
+
const unsyncedAssets = await findUnsyncedAssets(hubDir);
|
|
3131
|
+
if (unsyncedAssets.length === 0) {
|
|
3132
|
+
console.log(chalk18.green("All assets are synced."));
|
|
3133
|
+
} else {
|
|
3134
|
+
console.log(chalk18.yellow(`Found ${unsyncedAssets.length} unsynced asset(s):
|
|
3135
|
+
`));
|
|
3136
|
+
for (const asset of unsyncedAssets) {
|
|
3137
|
+
console.log(` ${chalk18.cyan(asset.name)} ${chalk18.dim(`(${asset.type} from ${asset.source})`)}`);
|
|
3138
|
+
}
|
|
3139
|
+
console.log();
|
|
3140
|
+
let toSync = unsyncedAssets;
|
|
3141
|
+
if (!opts.yes) {
|
|
3142
|
+
const { selected } = await inquirer.prompt([
|
|
3143
|
+
{
|
|
3144
|
+
type: "checkbox",
|
|
3145
|
+
name: "selected",
|
|
3146
|
+
message: "Select assets to sync to canonical folders:",
|
|
3147
|
+
choices: unsyncedAssets.map((a) => ({
|
|
3148
|
+
name: `${a.name} (${a.type} from ${a.source})`,
|
|
3149
|
+
value: `${a.type}:${a.name}`,
|
|
3150
|
+
checked: true
|
|
3151
|
+
}))
|
|
3152
|
+
}
|
|
3153
|
+
]);
|
|
3154
|
+
toSync = unsyncedAssets.filter((a) => selected.includes(`${a.type}:${a.name}`));
|
|
3155
|
+
}
|
|
3156
|
+
if (toSync.length > 0) {
|
|
3157
|
+
await syncAssets(hubDir, toSync);
|
|
3158
|
+
console.log(chalk18.green(`Synced ${toSync.length} asset(s).`));
|
|
3159
|
+
hasChanges = true;
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
3162
|
+
if (hasChanges) {
|
|
3163
|
+
console.log(chalk18.cyan(`
|
|
3164
|
+
Run ${chalk18.bold("hub generate")} to update editor configs.
|
|
3165
|
+
`));
|
|
3166
|
+
} else {
|
|
3167
|
+
console.log();
|
|
3168
|
+
}
|
|
3169
|
+
});
|
|
3170
|
+
|
|
2855
3171
|
// src/index.ts
|
|
2856
|
-
var program = new
|
|
3172
|
+
var program = new Command19();
|
|
2857
3173
|
program.name("hub").description(
|
|
2858
3174
|
"Give your AI coding assistant the full picture. Multi-repo context, agent orchestration, and end-to-end workflows."
|
|
2859
|
-
).version("0.
|
|
3175
|
+
).version("0.7.3").enablePositionalOptions();
|
|
2860
3176
|
program.addCommand(initCommand);
|
|
2861
3177
|
program.addCommand(addRepoCommand);
|
|
2862
3178
|
program.addCommand(setupCommand);
|
|
@@ -2877,4 +3193,5 @@ program.addCommand(toolsCommand);
|
|
|
2877
3193
|
program.addCommand(memoryCommand);
|
|
2878
3194
|
program.addCommand(updateCommand);
|
|
2879
3195
|
program.addCommand(directoryCommand);
|
|
3196
|
+
program.addCommand(scanCommand);
|
|
2880
3197
|
program.parse();
|