@arvoretech/hub 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,3271 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command as Command16 } from "commander";
5
+
6
+ // src/commands/init.ts
7
+ import { Command } from "commander";
8
+ import { mkdir, writeFile } from "fs/promises";
9
+ import { join } from "path";
10
+ import { stringify } from "yaml";
11
+ import chalk from "chalk";
12
+ var SCHEMA_COMMENT = "# yaml-language-server: $schema=https://raw.githubusercontent.com/arvoreeducacao/rhm/main/schemas/hub.schema.json\n";
13
+ var DEFAULT_HUB_CONFIG = {
14
+ name: "",
15
+ description: "",
16
+ repos: [],
17
+ services: [],
18
+ mcps: [],
19
+ integrations: {
20
+ github: {
21
+ pr_branch_pattern: "{linear_id}-{slug}"
22
+ },
23
+ slack: {
24
+ channels: {
25
+ prs: "#eng-prs"
26
+ }
27
+ }
28
+ },
29
+ workflow: {
30
+ task_folder: "./tasks/{task_id}/",
31
+ pipeline: [
32
+ { step: "refinement", agent: "refinement", output: "refinement.md" },
33
+ {
34
+ step: "coding",
35
+ agents: ["coding-backend", "coding-frontend"],
36
+ parallel: true
37
+ },
38
+ { step: "review", agent: "code-reviewer", output: "code-review.md" },
39
+ {
40
+ step: "qa",
41
+ agents: ["qa-backend", "qa-frontend"],
42
+ parallel: true,
43
+ tools: ["playwright"]
44
+ },
45
+ {
46
+ step: "deliver",
47
+ actions: ["create-pr", "notify-slack"]
48
+ }
49
+ ]
50
+ }
51
+ };
52
+ var initCommand = new Command("init").description("Initialize a new Repo Hub workspace").argument("[name]", "Hub name", "my-hub").action(async (name) => {
53
+ const hubDir = join(process.cwd(), name);
54
+ console.log(chalk.blue(`
55
+ Initializing Repo Hub: ${name}
56
+ `));
57
+ await mkdir(hubDir, { recursive: true });
58
+ await mkdir(join(hubDir, "tasks"), { recursive: true });
59
+ const config = { ...DEFAULT_HUB_CONFIG, name };
60
+ await writeFile(
61
+ join(hubDir, "hub.yaml"),
62
+ SCHEMA_COMMENT + stringify(config),
63
+ "utf-8"
64
+ );
65
+ const gitignore = [
66
+ "node_modules/",
67
+ ".DS_Store",
68
+ "",
69
+ "# Repositories (managed by hub)",
70
+ "# Add repos here as you add them with: hub add-repo",
71
+ "",
72
+ "# Docker volumes",
73
+ "*_data/",
74
+ "",
75
+ "# Environment files",
76
+ "*.env",
77
+ "*.env.local",
78
+ "!.env.example",
79
+ "",
80
+ "# Task documents",
81
+ "tasks/",
82
+ ""
83
+ ].join("\n");
84
+ await writeFile(join(hubDir, ".gitignore"), gitignore, "utf-8");
85
+ const readme = [
86
+ `# ${name}`,
87
+ "",
88
+ `Powered by [Repo Hub](https://github.com/arvoreeducacao/rhm).`,
89
+ "",
90
+ "## Getting Started",
91
+ "",
92
+ "```bash",
93
+ "# Add repositories",
94
+ "npx @arvoretech/hub add-repo git@github.com:org/api.git",
95
+ "npx @arvoretech/hub add-repo git@github.com:org/frontend.git",
96
+ "",
97
+ "# Setup workspace",
98
+ "npx @arvoretech/hub setup",
99
+ "",
100
+ "# Generate editor configs",
101
+ "npx @arvoretech/hub generate --editor cursor",
102
+ "```",
103
+ ""
104
+ ].join("\n");
105
+ await writeFile(join(hubDir, "README.md"), readme, "utf-8");
106
+ console.log(chalk.green(" Created hub.yaml"));
107
+ console.log(chalk.green(" Created .gitignore"));
108
+ console.log(chalk.green(" Created README.md"));
109
+ console.log();
110
+ console.log(chalk.cyan("Next steps:"));
111
+ console.log(` cd ${name}`);
112
+ console.log(" npx @arvoretech/hub add-repo <git-url>");
113
+ console.log(" npx @arvoretech/hub setup");
114
+ console.log(" npx @arvoretech/hub generate --editor cursor");
115
+ console.log();
116
+ });
117
+
118
+ // src/commands/add-repo.ts
119
+ import { Command as Command2 } from "commander";
120
+ import { existsSync } from "fs";
121
+ import { readFile, writeFile as writeFile2, appendFile } from "fs/promises";
122
+ import { join as join2, basename } from "path";
123
+ import { parse, stringify as stringify2 } from "yaml";
124
+ import chalk2 from "chalk";
125
+ var SCHEMA_COMMENT2 = "# yaml-language-server: $schema=https://raw.githubusercontent.com/arvoreeducacao/rhm/main/schemas/hub.schema.json\n";
126
+ var addRepoCommand = new Command2("add-repo").description("Add a repository to the hub").argument("<url>", "Git repository URL").option("-n, --name <name>", "Repository name (defaults to repo name from URL)").option("-t, --tech <tech>", "Technology (nestjs, nextjs, elixir, react, etc)").action(async (url, opts) => {
127
+ const hubDir = process.cwd();
128
+ const configPath = join2(hubDir, "hub.yaml");
129
+ const content = await readFile(configPath, "utf-8");
130
+ const config = parse(content);
131
+ const repoName = opts.name || basename(url).replace(/\.git$/, "");
132
+ const repoPath = `./${repoName}`;
133
+ if (config.repos.some((r) => r.name === repoName)) {
134
+ console.log(chalk2.yellow(`Repository ${repoName} already exists in hub.yaml`));
135
+ return;
136
+ }
137
+ config.repos.push({
138
+ name: repoName,
139
+ path: repoPath,
140
+ url,
141
+ ...opts.tech && { tech: opts.tech }
142
+ });
143
+ const header = content.startsWith("# yaml-language-server") ? "" : SCHEMA_COMMENT2;
144
+ await writeFile2(configPath, header + stringify2(config), "utf-8");
145
+ const gitignorePath = join2(hubDir, ".gitignore");
146
+ await appendFile(gitignorePath, `${repoName}
147
+ `);
148
+ const cursorignorePath = join2(hubDir, ".cursorignore");
149
+ if (existsSync(cursorignorePath)) {
150
+ await appendFile(cursorignorePath, `!${repoName}/
151
+ `);
152
+ console.log(chalk2.cyan(" Updated: .cursorignore"));
153
+ }
154
+ console.log(chalk2.green(`
155
+ Added ${repoName} to hub`));
156
+ console.log(chalk2.cyan(" Updated: hub.yaml"));
157
+ console.log(chalk2.cyan(" Updated: .gitignore"));
158
+ console.log();
159
+ console.log(`Run ${chalk2.bold("npx @arvoretech/hub setup")} to clone and install.`);
160
+ console.log();
161
+ });
162
+
163
+ // src/commands/setup.ts
164
+ import { Command as Command3 } from "commander";
165
+ import { existsSync as existsSync2 } from "fs";
166
+ import { writeFile as writeFile3 } from "fs/promises";
167
+ import { join as join4 } from "path";
168
+ import { execSync } from "child_process";
169
+ import chalk3 from "chalk";
170
+
171
+ // src/core/hub-config.ts
172
+ import { readFile as readFile2 } from "fs/promises";
173
+ import { join as join3 } from "path";
174
+ import { parse as parse2 } from "yaml";
175
+ async function loadHubConfig(dir) {
176
+ const configPath = join3(dir, "hub.yaml");
177
+ const content = await readFile2(configPath, "utf-8");
178
+ return parse2(content);
179
+ }
180
+
181
+ // src/core/docker-compose.ts
182
+ import { stringify as stringify3 } from "yaml";
183
+ function generateDockerCompose(services) {
184
+ const compose = {
185
+ services: {}
186
+ };
187
+ const svcMap = compose.services;
188
+ for (const svc of services) {
189
+ const entry = {
190
+ image: svc.image,
191
+ restart: "unless-stopped"
192
+ };
193
+ if (svc.port) {
194
+ entry.ports = [`${svc.port}:${svc.port}`];
195
+ } else if (svc.ports?.length) {
196
+ entry.ports = svc.ports.map((p) => `${p}:${p}`);
197
+ }
198
+ if (svc.env) {
199
+ entry.environment = svc.env;
200
+ }
201
+ entry.volumes = [`${svc.name}_data:/var/lib/${guessDataDir(svc.image)}`];
202
+ svcMap[svc.name] = entry;
203
+ }
204
+ const volumes = {};
205
+ for (const svc of services) {
206
+ volumes[`${svc.name}_data`] = null;
207
+ }
208
+ compose.volumes = volumes;
209
+ return stringify3(compose, { lineWidth: 120 });
210
+ }
211
+ function guessDataDir(image) {
212
+ const img = image.split(":")[0].split("/").pop() || "";
213
+ if (img.includes("mysql") || img.includes("mariadb")) return "mysql";
214
+ if (img.includes("postgres")) return "postgresql/data";
215
+ if (img.includes("redis")) return "redis";
216
+ if (img.includes("elasticsearch") || img.includes("opensearch")) return "elasticsearch";
217
+ if (img.includes("qdrant")) return "qdrant";
218
+ if (img.includes("mongo")) return "mongo";
219
+ return img;
220
+ }
221
+
222
+ // src/commands/setup.ts
223
+ function run(cmd, cwd) {
224
+ execSync(cmd, { stdio: "inherit", cwd });
225
+ }
226
+ function runSilent(cmd, _cwd) {
227
+ return execSync(cmd, { encoding: "utf-8", cwd: _cwd, stdio: ["pipe", "pipe", "pipe"] }).trim();
228
+ }
229
+ function canSsh() {
230
+ try {
231
+ const out = runSilent("ssh -T git@github.com 2>&1 || true");
232
+ return out.includes("successfully authenticated");
233
+ } catch {
234
+ return false;
235
+ }
236
+ }
237
+ function hasGh() {
238
+ try {
239
+ runSilent("gh auth status");
240
+ return true;
241
+ } catch {
242
+ return false;
243
+ }
244
+ }
245
+ function hasMise() {
246
+ try {
247
+ runSilent("mise --version");
248
+ return true;
249
+ } catch {
250
+ return false;
251
+ }
252
+ }
253
+ function generateMiseToml(tools, settings) {
254
+ const lines = ["[tools]"];
255
+ for (const [tool, version] of Object.entries(tools)) {
256
+ lines.push(`${tool} = "${version}"`);
257
+ }
258
+ if (settings && Object.keys(settings).length > 0) {
259
+ lines.push("", "[settings]");
260
+ for (const [key, value] of Object.entries(settings)) {
261
+ if (typeof value === "boolean") lines.push(`${key} = ${value}`);
262
+ else if (typeof value === "string") lines.push(`${key} = "${value}"`);
263
+ else if (typeof value === "number") lines.push(`${key} = ${value}`);
264
+ }
265
+ }
266
+ return lines.join("\n") + "\n";
267
+ }
268
+ var setupCommand = new Command3("setup").description("Clone repos, start services, install tools, and install dependencies").option("--skip-services", "Skip Docker services").option("--skip-install", "Skip dependency installation").option("--skip-tools", "Skip tool installation via mise").action(async (opts) => {
269
+ const hubDir = process.cwd();
270
+ const config = await loadHubConfig(hubDir);
271
+ const useGh = !canSsh() && hasGh();
272
+ const hasTools = config.tools || config.repos.some((r) => r.tools);
273
+ const totalSteps = hasTools && !opts.skipTools ? 5 : 4;
274
+ let step = 1;
275
+ console.log(chalk3.blue(`
276
+ \u2501\u2501\u2501 Step ${step}/${totalSteps}: Cloning repositories \u2501\u2501\u2501
277
+ `));
278
+ for (const repo of config.repos) {
279
+ const fullPath = join4(hubDir, repo.path);
280
+ console.log(chalk3.yellow(`\u25B8 ${repo.name}`));
281
+ if (existsSync2(fullPath)) {
282
+ console.log(chalk3.green(" Already exists, skipping"));
283
+ continue;
284
+ }
285
+ if (useGh) {
286
+ const slug = repo.url.replace("git@github.com:", "").replace(".git", "");
287
+ run(`gh repo clone ${slug} ${fullPath}`);
288
+ } else {
289
+ run(`git clone ${repo.url} ${fullPath}`);
290
+ }
291
+ console.log(chalk3.green(" Cloned"));
292
+ }
293
+ step++;
294
+ if (!opts.skipServices && config.services?.length) {
295
+ console.log(chalk3.blue(`
296
+ \u2501\u2501\u2501 Step ${step}/${totalSteps}: Starting services \u2501\u2501\u2501
297
+ `));
298
+ const composePath = join4(hubDir, "docker-compose.yml");
299
+ const composeContent = generateDockerCompose(config.services);
300
+ await writeFile3(composePath, composeContent, "utf-8");
301
+ console.log(chalk3.green(" Generated docker-compose.yml"));
302
+ run("docker compose up -d", hubDir);
303
+ console.log(chalk3.green(" Services started"));
304
+ for (const svc of config.services) {
305
+ const port = svc.port || svc.ports?.[0];
306
+ if (port) {
307
+ console.log(chalk3.cyan(` ${svc.name}: localhost:${port}`));
308
+ }
309
+ }
310
+ } else {
311
+ console.log(chalk3.blue(`
312
+ \u2501\u2501\u2501 Step ${step}/${totalSteps}: Services (skipped) \u2501\u2501\u2501
313
+ `));
314
+ }
315
+ step++;
316
+ if (hasTools && !opts.skipTools) {
317
+ console.log(chalk3.blue(`
318
+ \u2501\u2501\u2501 Step ${step}/${totalSteps}: Installing tools \u2501\u2501\u2501
319
+ `));
320
+ if (!hasMise()) {
321
+ console.log(chalk3.yellow(" mise not installed, skipping tool installation"));
322
+ console.log(chalk3.cyan(" Install mise: curl https://mise.run | sh"));
323
+ } else {
324
+ if (config.tools && Object.keys(config.tools).length > 0) {
325
+ const content = generateMiseToml(config.tools, config.mise_settings);
326
+ await writeFile3(join4(hubDir, ".mise.toml"), content, "utf-8");
327
+ console.log(chalk3.green(` Generated .mise.toml (${Object.keys(config.tools).length} tools)`));
328
+ try {
329
+ run("mise trust && mise install", hubDir);
330
+ console.log(chalk3.green(" Hub tools installed"));
331
+ } catch {
332
+ console.log(chalk3.red(" Failed to install hub tools"));
333
+ }
334
+ }
335
+ for (const repo of config.repos) {
336
+ if (!repo.tools || Object.keys(repo.tools).length === 0) continue;
337
+ const repoDir = join4(hubDir, repo.path);
338
+ if (!existsSync2(repoDir)) continue;
339
+ const merged = { ...config.tools || {}, ...repo.tools };
340
+ const content = generateMiseToml(merged);
341
+ await writeFile3(join4(repoDir, ".mise.toml"), content, "utf-8");
342
+ console.log(chalk3.yellow(`\u25B8 ${repo.name}`));
343
+ try {
344
+ run("mise trust 2>/dev/null; mise install", repoDir);
345
+ console.log(chalk3.green(" Tools installed"));
346
+ } catch {
347
+ console.log(chalk3.red(" Failed to install tools"));
348
+ }
349
+ }
350
+ }
351
+ step++;
352
+ }
353
+ console.log(chalk3.blue(`
354
+ \u2501\u2501\u2501 Step ${step}/${totalSteps}: Generating environment files \u2501\u2501\u2501
355
+ `));
356
+ for (const repo of config.repos) {
357
+ if (!repo.env_file) continue;
358
+ const envPath = join4(hubDir, repo.path, repo.env_file);
359
+ const overrides = config.env?.overrides?.["local"]?.[repo.name];
360
+ if (overrides) {
361
+ const dir = join4(hubDir, repo.path);
362
+ if (existsSync2(dir)) {
363
+ const lines = Object.entries(overrides).map(([k, v]) => `${k}=${v}`);
364
+ await writeFile3(envPath, lines.join("\n") + "\n", "utf-8");
365
+ console.log(chalk3.green(` ${repo.name}: Created ${repo.env_file} (${lines.length} vars)`));
366
+ }
367
+ } else {
368
+ console.log(chalk3.dim(` ${repo.name}: No local overrides`));
369
+ }
370
+ }
371
+ step++;
372
+ if (!opts.skipInstall) {
373
+ console.log(chalk3.blue(`
374
+ \u2501\u2501\u2501 Step ${step}/${totalSteps}: Installing dependencies \u2501\u2501\u2501
375
+ `));
376
+ for (const repo of config.repos) {
377
+ const fullPath = join4(hubDir, repo.path);
378
+ if (!existsSync2(fullPath)) continue;
379
+ const installCmd = repo.commands?.install;
380
+ if (!installCmd) {
381
+ console.log(chalk3.dim(` ${repo.name}: No install command`));
382
+ continue;
383
+ }
384
+ console.log(chalk3.yellow(`\u25B8 ${repo.name}`));
385
+ try {
386
+ run(installCmd, fullPath);
387
+ console.log(chalk3.green(" Installed"));
388
+ } catch {
389
+ console.log(chalk3.red(` Failed: ${installCmd}`));
390
+ }
391
+ }
392
+ } else {
393
+ console.log(chalk3.blue(`
394
+ \u2501\u2501\u2501 Step ${step}/${totalSteps}: Install (skipped) \u2501\u2501\u2501
395
+ `));
396
+ }
397
+ console.log(chalk3.blue("\n\u2501\u2501\u2501 Setup complete \u2501\u2501\u2501\n"));
398
+ console.log("Next steps:");
399
+ console.log(` npx @arvoretech/hub generate --editor cursor`);
400
+ console.log(` Open the project in Cursor and start building`);
401
+ console.log();
402
+ });
403
+
404
+ // src/commands/generate.ts
405
+ import { Command as Command4 } from "commander";
406
+ import { existsSync as existsSync3 } from "fs";
407
+ import { mkdir as mkdir2, writeFile as writeFile4, readdir, copyFile, readFile as readFile3, cp } from "fs/promises";
408
+ import { join as join5, resolve } from "path";
409
+ import chalk4 from "chalk";
410
+ var HUB_MARKER_START = "# >>> hub-managed (do not edit this section)";
411
+ var HUB_MARKER_END = "# <<< hub-managed";
412
+ var HOOK_EVENT_MAP = {
413
+ session_start: { cursor: "sessionStart", claude: "SessionStart", kiro: void 0 },
414
+ session_end: { cursor: "sessionEnd", claude: "SessionEnd", kiro: void 0 },
415
+ pre_tool_use: { cursor: "preToolUse", claude: "PreToolUse", kiro: "pre_tool_use" },
416
+ post_tool_use: { cursor: "postToolUse", claude: "PostToolUse", kiro: "post_tool_use" },
417
+ post_tool_use_failure: { cursor: void 0, claude: "PostToolUseFailure", kiro: void 0 },
418
+ stop: { cursor: "stop", claude: "Stop", kiro: "agent_stop" },
419
+ subagent_start: { cursor: "subagentStart", claude: "SubagentStart", kiro: void 0 },
420
+ subagent_stop: { cursor: "subagentStop", claude: "SubagentStop", kiro: void 0 },
421
+ pre_compact: { cursor: "preCompact", claude: "PreCompact", kiro: void 0 },
422
+ before_submit_prompt: { cursor: "beforeSubmitPrompt", claude: "UserPromptSubmit", kiro: "prompt_submit" },
423
+ before_shell_execution: { cursor: "beforeShellExecution", claude: void 0, kiro: void 0 },
424
+ after_shell_execution: { cursor: "afterShellExecution", claude: void 0, kiro: void 0 },
425
+ before_mcp_execution: { cursor: "beforeMCPExecution", claude: void 0, kiro: void 0 },
426
+ after_mcp_execution: { cursor: "afterMCPExecution", claude: void 0, kiro: void 0 },
427
+ after_file_edit: { cursor: "afterFileEdit", claude: void 0, kiro: "file_save" },
428
+ before_read_file: { cursor: "beforeReadFile", claude: void 0, kiro: void 0 },
429
+ before_tab_file_read: { cursor: "beforeTabFileRead", claude: void 0, kiro: void 0 },
430
+ after_tab_file_edit: { cursor: "afterTabFileEdit", claude: void 0, kiro: void 0 },
431
+ after_agent_response: { cursor: "afterAgentResponse", claude: void 0, kiro: void 0 },
432
+ after_agent_thought: { cursor: "afterAgentThought", claude: void 0, kiro: void 0 },
433
+ notification: { cursor: void 0, claude: "Notification", kiro: void 0 },
434
+ permission_request: { cursor: void 0, claude: "PermissionRequest", kiro: void 0 },
435
+ task_completed: { cursor: void 0, claude: "TaskCompleted", kiro: void 0 },
436
+ teammate_idle: { cursor: void 0, claude: "TeammateIdle", kiro: void 0 }
437
+ };
438
+ function buildCursorHooks(hooks) {
439
+ const cursorHooks = {};
440
+ for (const [event, entries] of Object.entries(hooks)) {
441
+ const mapped = HOOK_EVENT_MAP[event]?.cursor;
442
+ if (!mapped) continue;
443
+ const cursorEntries = entries.map((entry) => {
444
+ const obj = { type: entry.type };
445
+ if (entry.type === "command" && entry.command) obj.command = entry.command;
446
+ if (entry.type === "prompt" && entry.prompt) obj.prompt = entry.prompt;
447
+ if (entry.matcher) obj.matcher = entry.matcher;
448
+ if (entry.timeout_ms) obj.timeout = entry.timeout_ms;
449
+ return obj;
450
+ });
451
+ if (cursorEntries.length > 0) {
452
+ cursorHooks[mapped] = cursorEntries;
453
+ }
454
+ }
455
+ if (Object.keys(cursorHooks).length === 0) return null;
456
+ return { version: 1, hooks: cursorHooks };
457
+ }
458
+ function buildClaudeHooks(hooks) {
459
+ const claudeHooks = {};
460
+ for (const [event, entries] of Object.entries(hooks)) {
461
+ const mapped = HOOK_EVENT_MAP[event]?.claude;
462
+ if (!mapped) continue;
463
+ const claudeEntries = entries.map((entry) => {
464
+ const obj = { type: entry.type };
465
+ if (entry.type === "command" && entry.command) obj.command = entry.command;
466
+ if (entry.type === "prompt" && entry.prompt) obj.prompt = entry.prompt;
467
+ if (entry.matcher) obj.matcher = entry.matcher;
468
+ if (entry.timeout_ms) obj.timeout = entry.timeout_ms;
469
+ return obj;
470
+ });
471
+ if (claudeEntries.length > 0) {
472
+ claudeHooks[mapped] = claudeEntries;
473
+ }
474
+ }
475
+ if (Object.keys(claudeHooks).length === 0) return null;
476
+ return claudeHooks;
477
+ }
478
+ async function generateCursorCommands(config, hubDir, cursorDir) {
479
+ const commandsDir = join5(cursorDir, "commands");
480
+ let count = 0;
481
+ if (config.commands_dir) {
482
+ const srcDir = resolve(hubDir, config.commands_dir);
483
+ try {
484
+ const files = await readdir(srcDir);
485
+ const mdFiles = files.filter((f) => f.endsWith(".md"));
486
+ if (mdFiles.length > 0) {
487
+ await mkdir2(commandsDir, { recursive: true });
488
+ for (const file of mdFiles) {
489
+ await copyFile(join5(srcDir, file), join5(commandsDir, file));
490
+ count++;
491
+ }
492
+ }
493
+ } catch {
494
+ console.log(chalk4.yellow(` Commands directory ${config.commands_dir} not found, skipping`));
495
+ }
496
+ }
497
+ if (config.commands) {
498
+ await mkdir2(commandsDir, { recursive: true });
499
+ for (const [name, filePath] of Object.entries(config.commands)) {
500
+ const src = resolve(hubDir, filePath);
501
+ const dest = join5(commandsDir, name.endsWith(".md") ? name : `${name}.md`);
502
+ try {
503
+ await copyFile(src, dest);
504
+ count++;
505
+ } catch {
506
+ console.log(chalk4.yellow(` Command file ${filePath} not found, skipping`));
507
+ }
508
+ }
509
+ }
510
+ if (count > 0) {
511
+ console.log(chalk4.green(` Copied ${count} commands to .cursor/commands/`));
512
+ }
513
+ }
514
+ async function writeManagedFile(filePath, managedLines) {
515
+ const managedBlock = [HUB_MARKER_START, ...managedLines, HUB_MARKER_END].join("\n");
516
+ if (existsSync3(filePath)) {
517
+ const existing = await readFile3(filePath, "utf-8");
518
+ const startIdx = existing.indexOf(HUB_MARKER_START);
519
+ const endIdx = existing.indexOf(HUB_MARKER_END);
520
+ if (startIdx !== -1 && endIdx !== -1) {
521
+ const before = existing.substring(0, startIdx);
522
+ const after = existing.substring(endIdx + HUB_MARKER_END.length);
523
+ await writeFile4(filePath, before + managedBlock + after, "utf-8");
524
+ return;
525
+ }
526
+ await writeFile4(filePath, managedBlock + "\n\n" + existing, "utf-8");
527
+ return;
528
+ }
529
+ await writeFile4(filePath, managedBlock + "\n", "utf-8");
530
+ }
531
+ async function generateCursor(config, hubDir) {
532
+ const cursorDir = join5(hubDir, ".cursor");
533
+ await mkdir2(join5(cursorDir, "rules"), { recursive: true });
534
+ await mkdir2(join5(cursorDir, "agents"), { recursive: true });
535
+ const gitignoreLines = buildGitignoreLines(config);
536
+ await writeManagedFile(join5(hubDir, ".gitignore"), gitignoreLines);
537
+ console.log(chalk4.green(" Generated .gitignore"));
538
+ const cursorignoreLines = [
539
+ "# Re-include repositories for AI context"
540
+ ];
541
+ for (const repo of config.repos) {
542
+ const repoDir = repo.path.replace("./", "");
543
+ cursorignoreLines.push(`!${repoDir}/`);
544
+ }
545
+ cursorignoreLines.push("", "# Re-include tasks for agent collaboration", "!tasks/");
546
+ await writeManagedFile(join5(hubDir, ".cursorignore"), cursorignoreLines);
547
+ console.log(chalk4.green(" Generated .cursorignore"));
548
+ if (config.mcps?.length) {
549
+ const mcpConfig = {};
550
+ for (const mcp of config.mcps) {
551
+ mcpConfig[mcp.name] = buildCursorMcpEntry(mcp);
552
+ }
553
+ await writeFile4(
554
+ join5(cursorDir, "mcp.json"),
555
+ JSON.stringify({ mcpServers: mcpConfig }, null, 2) + "\n",
556
+ "utf-8"
557
+ );
558
+ console.log(chalk4.green(" Generated .cursor/mcp.json"));
559
+ }
560
+ const orchestratorRule = buildOrchestratorRule(config);
561
+ await writeFile4(join5(cursorDir, "rules", "orchestrator.mdc"), orchestratorRule, "utf-8");
562
+ console.log(chalk4.green(" Generated .cursor/rules/orchestrator.mdc"));
563
+ const agentsDir = resolve(hubDir, "agents");
564
+ try {
565
+ const agentFiles = await readdir(agentsDir);
566
+ const mdFiles = agentFiles.filter((f) => f.endsWith(".md"));
567
+ for (const file of mdFiles) {
568
+ await copyFile(join5(agentsDir, file), join5(cursorDir, "agents", file));
569
+ }
570
+ console.log(chalk4.green(` Copied ${mdFiles.length} agent definitions`));
571
+ } catch {
572
+ console.log(chalk4.yellow(" No agents/ directory found, skipping agent copy"));
573
+ }
574
+ const skillsDir = resolve(hubDir, "skills");
575
+ try {
576
+ const skillFolders = await readdir(skillsDir);
577
+ const cursorSkillsDir = join5(cursorDir, "skills");
578
+ await mkdir2(cursorSkillsDir, { recursive: true });
579
+ let count = 0;
580
+ for (const folder of skillFolders) {
581
+ const skillFile = join5(skillsDir, folder, "SKILL.md");
582
+ try {
583
+ await readFile3(skillFile);
584
+ const srcDir = join5(skillsDir, folder);
585
+ const targetDir = join5(cursorSkillsDir, folder);
586
+ await cp(srcDir, targetDir, { recursive: true });
587
+ count++;
588
+ } catch {
589
+ }
590
+ }
591
+ if (count > 0) {
592
+ console.log(chalk4.green(` Copied ${count} skills`));
593
+ }
594
+ } catch {
595
+ }
596
+ if (config.hooks) {
597
+ const cursorHooks = buildCursorHooks(config.hooks);
598
+ if (cursorHooks) {
599
+ await writeFile4(
600
+ join5(cursorDir, "hooks.json"),
601
+ JSON.stringify(cursorHooks, null, 2) + "\n",
602
+ "utf-8"
603
+ );
604
+ console.log(chalk4.green(" Generated .cursor/hooks.json"));
605
+ }
606
+ }
607
+ await generateCursorCommands(config, hubDir, cursorDir);
608
+ }
609
+ function buildCursorMcpEntry(mcp) {
610
+ if (mcp.url) {
611
+ return { url: mcp.url, ...mcp.env && { env: mcp.env } };
612
+ }
613
+ if (mcp.image) {
614
+ const args = ["run", "-i", "--rm"];
615
+ if (mcp.env) {
616
+ for (const [key, value] of Object.entries(mcp.env)) {
617
+ args.push("-e", `${key}=${value}`);
618
+ }
619
+ }
620
+ args.push(mcp.image);
621
+ return { command: "docker", args };
622
+ }
623
+ return {
624
+ command: "npx",
625
+ args: ["-y", mcp.package],
626
+ ...mcp.env && { env: mcp.env }
627
+ };
628
+ }
629
+ function buildClaudeCodeMcpEntry(mcp) {
630
+ if (mcp.url) {
631
+ return { type: "http", url: mcp.url };
632
+ }
633
+ if (mcp.image) {
634
+ const args = ["run", "-i", "--rm"];
635
+ if (mcp.env) {
636
+ for (const [key, value] of Object.entries(mcp.env)) {
637
+ args.push("-e", `${key}=${value}`);
638
+ }
639
+ }
640
+ args.push(mcp.image);
641
+ return { command: "docker", args };
642
+ }
643
+ return {
644
+ command: "npx",
645
+ args: ["-y", mcp.package],
646
+ ...mcp.env && { env: mcp.env }
647
+ };
648
+ }
649
+ function buildKiroMcpEntry(mcp) {
650
+ if (mcp.url) {
651
+ return { url: mcp.url, ...mcp.env && { env: mcp.env } };
652
+ }
653
+ if (mcp.image) {
654
+ const args = ["run", "-i", "--rm"];
655
+ if (mcp.env) {
656
+ for (const [key, value] of Object.entries(mcp.env)) {
657
+ args.push("-e", `${key}=${value}`);
658
+ }
659
+ }
660
+ args.push(mcp.image);
661
+ return { command: "docker", args };
662
+ }
663
+ return {
664
+ command: "npx",
665
+ args: ["-y", mcp.package],
666
+ ...mcp.env && { env: mcp.env }
667
+ };
668
+ }
669
+ function buildKiroSteeringContent(content, inclusion = "always", meta) {
670
+ const frontMatter = ["---", `inclusion: ${inclusion}`];
671
+ if (meta?.name) frontMatter.push(`name: ${meta.name}`);
672
+ if (meta?.description) frontMatter.push(`description: ${meta.description}`);
673
+ frontMatter.push("---");
674
+ return `${frontMatter.join("\n")}
675
+
676
+ ${content}`;
677
+ }
678
+ function buildKiroOrchestratorRule(config) {
679
+ const taskFolder = config.workflow?.task_folder || "./tasks/<TASK_ID>/";
680
+ const steps = config.workflow?.pipeline || [];
681
+ const prompt = config.workflow?.prompt;
682
+ const sections = [];
683
+ sections.push(`# Orchestrator
684
+
685
+ ## Your Main Responsibility
686
+
687
+ 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 work as a single agent but follow specialized instructions from steering files for each phase of development.
688
+
689
+ > **Note:** This workspace uses steering files in \`.kiro/steering/\` to provide role-specific instructions for each pipeline step. When a step says "follow the instructions from steering file X", read that file and apply its guidelines to the current task.`);
690
+ if (prompt?.prepend) {
691
+ sections.push(`
692
+ ${prompt.prepend.trim()}`);
693
+ }
694
+ if (config.integrations?.linear) {
695
+ const linear = config.integrations.linear;
696
+ sections.push(`
697
+ ## Task Management
698
+
699
+ 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.`);
700
+ }
701
+ sections.push(`
702
+ ## Repositories
703
+ `);
704
+ for (const repo of config.repos) {
705
+ const parts = [`- **${repo.path}**`];
706
+ if (repo.description) parts.push(`\u2014 ${repo.description}`);
707
+ else if (repo.tech) parts.push(`\u2014 ${repo.tech}`);
708
+ if (repo.skills?.length) parts.push(`(skills: ${repo.skills.join(", ")})`);
709
+ sections.push(parts.join(" "));
710
+ if (repo.commands) {
711
+ const cmds = Object.entries(repo.commands).filter(([, v]) => v).map(([k, v]) => `\`${k}\`: \`${v}\``).join(", ");
712
+ if (cmds) sections.push(` Commands: ${cmds}`);
713
+ }
714
+ }
715
+ if (prompt?.sections?.after_repositories) {
716
+ sections.push(`
717
+ ${prompt.sections.after_repositories.trim()}`);
718
+ }
719
+ const docStructure = buildDocumentStructure(steps, taskFolder);
720
+ sections.push(docStructure);
721
+ const pipelineSection = buildKiroPipelineSection(steps);
722
+ sections.push(pipelineSection);
723
+ if (prompt?.sections?.after_pipeline) {
724
+ sections.push(`
725
+ ${prompt.sections.after_pipeline.trim()}`);
726
+ }
727
+ if (config.integrations?.slack || config.integrations?.github) {
728
+ sections.push(buildDeliverySection(config));
729
+ }
730
+ if (prompt?.sections?.after_delivery) {
731
+ sections.push(`
732
+ ${prompt.sections.after_delivery.trim()}`);
733
+ }
734
+ sections.push(`
735
+ ## Troubleshooting and Debugging
736
+
737
+ For bug reports or unexpected behavior, follow the debugging process from the \`agent-debugger.md\` steering file (if available), or:
738
+ 1. Collect context (symptoms, environment, timeline)
739
+ 2. Analyze logs and stack traces
740
+ 3. Form and test hypotheses systematically
741
+ 4. Identify the root cause
742
+ 5. Propose and implement the fix`);
743
+ if (prompt?.sections) {
744
+ const reservedKeys = /* @__PURE__ */ new Set(["after_repositories", "after_pipeline", "after_delivery"]);
745
+ for (const [name, content] of Object.entries(prompt.sections)) {
746
+ if (reservedKeys.has(name)) continue;
747
+ const title = name.split(/[-_]/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
748
+ sections.push(`
749
+ ## ${title}
750
+
751
+ ${content.trim()}`);
752
+ }
753
+ }
754
+ if (prompt?.append) {
755
+ sections.push(`
756
+ ${prompt.append.trim()}`);
757
+ }
758
+ return sections.join("\n");
759
+ }
760
+ function buildKiroPipelineSection(steps) {
761
+ if (steps.length === 0) {
762
+ return `
763
+ ## Development Pipeline
764
+
765
+ Since Kiro does not support sub-agents, follow each step sequentially, applying the guidelines from the corresponding steering file:
766
+
767
+ 1. **Refinement** \u2014 Read and follow \`agent-refinement.md\` steering file to collect requirements. Write output to the task document.
768
+ 2. **Coding** \u2014 Follow the coding steering files (\`agent-coding-backend.md\`, \`agent-coding-frontend.md\`) to implement the feature.
769
+ 3. **Review** \u2014 Follow \`agent-code-reviewer.md\` to review the implementation.
770
+ 4. **QA** \u2014 Follow \`agent-qa-backend.md\` and/or \`agent-qa-frontend.md\` to test.
771
+ 5. **Delivery** \u2014 Create PRs and notify the team.`;
772
+ }
773
+ const parts = [`
774
+ ## Development Pipeline
775
+
776
+ Follow each step sequentially, applying the role-specific instructions from the corresponding steering file at each phase.
777
+ `];
778
+ for (const step of steps) {
779
+ if (step.actions) {
780
+ parts.push(`### Delivery`);
781
+ parts.push(`After all validations pass, execute these actions:`);
782
+ for (const action of step.actions) {
783
+ parts.push(`- ${formatAction(action)}`);
784
+ }
785
+ continue;
786
+ }
787
+ const stepTitle = step.step.charAt(0).toUpperCase() + step.step.slice(1);
788
+ parts.push(`### ${stepTitle}`);
789
+ if (step.agent) {
790
+ parts.push(`Follow the instructions from the \`agent-${step.agent}.md\` steering file.${step.output ? ` Write output to \`${step.output}\`.` : ""}`);
791
+ if (step.step === "refinement") {
792
+ parts.push(`
793
+ After completing the refinement, validate with the user:
794
+ - If there are unanswered questions, ask the user one at a time
795
+ - If the user requests adjustments, revisit the refinement
796
+ - Do not proceed until the document is complete and approved by the user`);
797
+ }
798
+ }
799
+ if (Array.isArray(step.agents)) {
800
+ const agentList = step.agents.map((a) => {
801
+ if (typeof a === "string") return { agent: a };
802
+ return a;
803
+ });
804
+ parts.push(`Follow the instructions from these steering files sequentially:`);
805
+ for (const a of agentList) {
806
+ let line = `- \`agent-${a.agent}.md\``;
807
+ if (a.output) line += ` \u2192 write to \`${a.output}\``;
808
+ if (a.when) line += ` (when: ${a.when})`;
809
+ parts.push(line);
810
+ }
811
+ if (step.step === "coding" || step.step === "code" || step.step === "implementation") {
812
+ parts.push(`
813
+ If you encounter doubts during coding, write questions in the task document and validate with the user before proceeding.`);
814
+ }
815
+ if (step.step === "validation" || step.step === "review" || step.step === "qa") {
816
+ parts.push(`
817
+ If any validation step reveals issues requiring fixes, go back to the relevant coding step to address them.`);
818
+ }
819
+ }
820
+ parts.push("");
821
+ }
822
+ return parts.join("\n");
823
+ }
824
+ function buildOrchestratorRule(config) {
825
+ const taskFolder = config.workflow?.task_folder || "./tasks/<TASK_ID>/";
826
+ const steps = config.workflow?.pipeline || [];
827
+ const prompt = config.workflow?.prompt;
828
+ const sections = [];
829
+ sections.push(`---
830
+ description: "Orchestrator agent \u2014 coordinates sub-agents through the development pipeline"
831
+ alwaysApply: true
832
+ ---
833
+
834
+ # Orchestrator
835
+
836
+ ## Your Main Responsibility
837
+
838
+ 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.`);
839
+ if (prompt?.prepend) {
840
+ sections.push(`
841
+ ${prompt.prepend.trim()}`);
842
+ }
843
+ if (config.integrations?.linear) {
844
+ const linear = config.integrations.linear;
845
+ sections.push(`
846
+ ## Task Management
847
+
848
+ 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.`);
849
+ }
850
+ sections.push(`
851
+ ## Repositories
852
+ `);
853
+ for (const repo of config.repos) {
854
+ const parts = [`- **${repo.path}**`];
855
+ if (repo.description) parts.push(`\u2014 ${repo.description}`);
856
+ else if (repo.tech) parts.push(`\u2014 ${repo.tech}`);
857
+ if (repo.skills?.length) parts.push(`(skills: ${repo.skills.join(", ")})`);
858
+ sections.push(parts.join(" "));
859
+ if (repo.commands) {
860
+ const cmds = Object.entries(repo.commands).filter(([, v]) => v).map(([k, v]) => `\`${k}\`: \`${v}\``).join(", ");
861
+ if (cmds) sections.push(` Commands: ${cmds}`);
862
+ }
863
+ }
864
+ if (prompt?.sections?.after_repositories) {
865
+ sections.push(`
866
+ ${prompt.sections.after_repositories.trim()}`);
867
+ }
868
+ const docStructure = buildDocumentStructure(steps, taskFolder);
869
+ sections.push(docStructure);
870
+ const pipelineSection = buildPipelineSection(steps);
871
+ sections.push(pipelineSection);
872
+ if (prompt?.sections?.after_pipeline) {
873
+ sections.push(`
874
+ ${prompt.sections.after_pipeline.trim()}`);
875
+ }
876
+ if (config.integrations?.slack || config.integrations?.github) {
877
+ sections.push(buildDeliverySection(config));
878
+ }
879
+ if (prompt?.sections?.after_delivery) {
880
+ sections.push(`
881
+ ${prompt.sections.after_delivery.trim()}`);
882
+ }
883
+ sections.push(`
884
+ ## Troubleshooting and Debugging
885
+
886
+ For bug reports or unexpected behavior, use the \`debugger\` agent directly.
887
+ It will:
888
+ 1. Collect context (symptoms, environment, timeline)
889
+ 2. Analyze logs and stack traces
890
+ 3. Form and test hypotheses systematically
891
+ 4. Identify the root cause
892
+ 5. Propose a solution or call coding agents to implement the fix`);
893
+ if (prompt?.sections) {
894
+ const reservedKeys = /* @__PURE__ */ new Set(["after_repositories", "after_pipeline", "after_delivery"]);
895
+ for (const [name, content] of Object.entries(prompt.sections)) {
896
+ if (reservedKeys.has(name)) continue;
897
+ const title = name.split(/[-_]/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
898
+ sections.push(`
899
+ ## ${title}
900
+
901
+ ${content.trim()}`);
902
+ }
903
+ }
904
+ if (prompt?.append) {
905
+ sections.push(`
906
+ ${prompt.append.trim()}`);
907
+ }
908
+ return sections.join("\n");
909
+ }
910
+ function buildDocumentStructure(steps, taskFolder) {
911
+ const outputs = [];
912
+ for (const step of steps) {
913
+ if (step.output) {
914
+ outputs.push(step.output);
915
+ }
916
+ if (Array.isArray(step.agents)) {
917
+ for (const a of step.agents) {
918
+ if (typeof a === "object" && a.output) {
919
+ outputs.push(a.output);
920
+ }
921
+ }
922
+ }
923
+ }
924
+ if (outputs.length === 0) {
925
+ outputs.push("refinement.md", "code-backend.md", "code-frontend.md", "code-review.md", "qa-backend.md", "qa-frontend.md");
926
+ }
927
+ const tree = outputs.map((o) => `\u251C\u2500\u2500 ${o}`);
928
+ if (tree.length > 0) {
929
+ tree[tree.length - 1] = tree[tree.length - 1].replace("\u251C\u2500\u2500", "\u2514\u2500\u2500");
930
+ }
931
+ return `
932
+ ## Document Structure
933
+
934
+ All task documents are stored in \`${taskFolder}\`:
935
+
936
+ \`\`\`
937
+ ${taskFolder}
938
+ ${tree.join("\n")}
939
+ \`\`\``;
940
+ }
941
+ function buildPipelineSection(steps) {
942
+ if (steps.length === 0) {
943
+ return `
944
+ ## Development Pipeline
945
+
946
+ 1. Use the \`refinement\` agent to collect requirements
947
+ 2. Use \`coding-backend\` and/or \`coding-frontend\` agents to implement
948
+ 3. Use \`code-reviewer\` to review the implementation
949
+ 4. Use \`qa-backend\` and/or \`qa-frontend\` to test
950
+ 5. Create PRs and notify the team`;
951
+ }
952
+ const parts = [`
953
+ ## Development Pipeline
954
+ `];
955
+ for (const step of steps) {
956
+ if (step.actions) {
957
+ parts.push(`### Delivery`);
958
+ parts.push(`After all validations pass, execute these actions:`);
959
+ for (const action of step.actions) {
960
+ parts.push(`- ${formatAction(action)}`);
961
+ }
962
+ continue;
963
+ }
964
+ const stepTitle = step.step.charAt(0).toUpperCase() + step.step.slice(1);
965
+ parts.push(`### ${stepTitle}`);
966
+ if (step.agent) {
967
+ parts.push(`Call the \`${step.agent}\` agent.${step.output ? ` It writes to \`${step.output}\`.` : ""}`);
968
+ if (step.step === "refinement") {
969
+ parts.push(`
970
+ After it runs, read the document and validate with the user:
971
+ - If there are unanswered questions, ask the user one at a time
972
+ - If the user requests adjustments, send back to the refinement agent
973
+ - Do not proceed until the document is complete and approved by the user`);
974
+ }
975
+ }
976
+ if (Array.isArray(step.agents)) {
977
+ const agentList = step.agents.map((a) => {
978
+ if (typeof a === "string") return { agent: a };
979
+ return a;
980
+ });
981
+ if (step.parallel) {
982
+ parts.push(`Call these agents${step.parallel ? " in parallel" : ""}:`);
983
+ } else {
984
+ parts.push(`Call these agents in sequence:`);
985
+ }
986
+ for (const a of agentList) {
987
+ let line = `- \`${a.agent}\``;
988
+ if (a.output) line += ` \u2192 writes to \`${a.output}\``;
989
+ if (a.when) line += ` (when: ${a.when})`;
990
+ parts.push(line);
991
+ }
992
+ if (step.step === "coding" || step.step === "code" || step.step === "implementation") {
993
+ parts.push(`
994
+ 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.`);
995
+ }
996
+ if (step.step === "validation" || step.step === "review" || step.step === "qa") {
997
+ parts.push(`
998
+ If any validation agent leaves comments requiring fixes, call the relevant coding agents again to address them.`);
999
+ }
1000
+ }
1001
+ parts.push("");
1002
+ }
1003
+ return parts.join("\n");
1004
+ }
1005
+ function buildDeliverySection(config) {
1006
+ const parts = [`
1007
+ ## Delivery Details
1008
+ `];
1009
+ if (config.integrations?.github) {
1010
+ const gh = config.integrations.github;
1011
+ parts.push(`### Pull Requests`);
1012
+ parts.push(`For each repository with changes, push the branch and create a PR using the GitHub MCP.`);
1013
+ if (gh.pr_branch_pattern) {
1014
+ parts.push(`Branch naming pattern: \`${gh.pr_branch_pattern}\``);
1015
+ }
1016
+ }
1017
+ if (config.integrations?.slack) {
1018
+ const slack = config.integrations.slack;
1019
+ if (slack.channels) {
1020
+ parts.push(`
1021
+ ### Slack Notifications`);
1022
+ for (const [purpose, channel] of Object.entries(slack.channels)) {
1023
+ parts.push(`- **${purpose}**: Post to \`${channel}\``);
1024
+ }
1025
+ }
1026
+ if (slack.templates) {
1027
+ parts.push(`
1028
+ Message templates:`);
1029
+ for (const [name, template] of Object.entries(slack.templates)) {
1030
+ parts.push(`- **${name}**: \`${template}\``);
1031
+ }
1032
+ }
1033
+ }
1034
+ if (config.integrations?.linear) {
1035
+ parts.push(`
1036
+ ### Task Management`);
1037
+ parts.push(`Update the Linear task status after PR creation.`);
1038
+ }
1039
+ return parts.join("\n");
1040
+ }
1041
+ function formatAction(action) {
1042
+ const map = {
1043
+ "create-pr": "Create pull requests for each repository with changes",
1044
+ "notify-slack": "Send notification to the configured Slack channel",
1045
+ "notify-slack-prs": "Send PR notification to the Slack PRs channel",
1046
+ "update-linear": "Update the Linear task status",
1047
+ "update-linear-status": "Update the Linear task status to Review",
1048
+ "update-jira": "Update the Jira task status"
1049
+ };
1050
+ return map[action] || action;
1051
+ }
1052
+ async function generateClaudeCode(config, hubDir) {
1053
+ const claudeDir = join5(hubDir, ".claude");
1054
+ await mkdir2(join5(claudeDir, "agents"), { recursive: true });
1055
+ const orchestratorRule = buildOrchestratorRule(config);
1056
+ const cleanedOrchestrator = orchestratorRule.replace(/^---[\s\S]*?---\n/m, "").trim();
1057
+ const claudeMdSections = [];
1058
+ claudeMdSections.push(cleanedOrchestrator);
1059
+ const agentsDir = resolve(hubDir, "agents");
1060
+ try {
1061
+ const agentFiles = await readdir(agentsDir);
1062
+ const mdFiles = agentFiles.filter((f) => f.endsWith(".md"));
1063
+ for (const file of mdFiles) {
1064
+ await copyFile(join5(agentsDir, file), join5(claudeDir, "agents", file));
1065
+ }
1066
+ console.log(chalk4.green(` Copied ${mdFiles.length} agents to .claude/agents/`));
1067
+ } catch {
1068
+ console.log(chalk4.yellow(" No agents/ directory found, skipping agent copy"));
1069
+ }
1070
+ const skillsDir = resolve(hubDir, "skills");
1071
+ try {
1072
+ const skillFolders = await readdir(skillsDir);
1073
+ const claudeSkillsDir = join5(claudeDir, "skills");
1074
+ await mkdir2(claudeSkillsDir, { recursive: true });
1075
+ let count = 0;
1076
+ for (const folder of skillFolders) {
1077
+ const skillFile = join5(skillsDir, folder, "SKILL.md");
1078
+ try {
1079
+ await readFile3(skillFile);
1080
+ const srcDir = join5(skillsDir, folder);
1081
+ const targetDir = join5(claudeSkillsDir, folder);
1082
+ await cp(srcDir, targetDir, { recursive: true });
1083
+ count++;
1084
+ } catch {
1085
+ }
1086
+ }
1087
+ if (count > 0) {
1088
+ console.log(chalk4.green(` Copied ${count} skills to .claude/skills/`));
1089
+ }
1090
+ } catch {
1091
+ }
1092
+ await writeFile4(join5(hubDir, "CLAUDE.md"), claudeMdSections.join("\n"), "utf-8");
1093
+ console.log(chalk4.green(" Generated CLAUDE.md"));
1094
+ if (config.mcps?.length) {
1095
+ const mcpJson = {};
1096
+ for (const mcp of config.mcps) {
1097
+ mcpJson[mcp.name] = buildClaudeCodeMcpEntry(mcp);
1098
+ }
1099
+ await writeFile4(
1100
+ join5(hubDir, ".mcp.json"),
1101
+ JSON.stringify({ mcpServers: mcpJson }, null, 2) + "\n",
1102
+ "utf-8"
1103
+ );
1104
+ console.log(chalk4.green(" Generated .mcp.json"));
1105
+ }
1106
+ const mcpServerNames = config.mcps?.map((m) => m.name) || [];
1107
+ const claudeSettings = {
1108
+ $schema: "https://json.schemastore.org/claude-code-settings.json",
1109
+ permissions: {
1110
+ allow: [
1111
+ "Read(*)",
1112
+ "Edit(*)",
1113
+ "Write(*)",
1114
+ "Bash(git *)",
1115
+ "Bash(npm *)",
1116
+ "Bash(pnpm *)",
1117
+ "Bash(npx *)",
1118
+ "Bash(ls *)",
1119
+ "Bash(echo *)",
1120
+ "Bash(grep *)",
1121
+ ...mcpServerNames.map((name) => `mcp__${name}__*`)
1122
+ ],
1123
+ deny: [
1124
+ "Read(.env)",
1125
+ "Read(.env.*)",
1126
+ "Read(**/.env)",
1127
+ "Read(**/.env.*)",
1128
+ "Read(**/credentials*)",
1129
+ "Read(**/secrets*)",
1130
+ "Read(**/*.pem)",
1131
+ "Read(**/*.key)"
1132
+ ]
1133
+ },
1134
+ enableAllProjectMcpServers: true
1135
+ };
1136
+ if (config.hooks) {
1137
+ const claudeHooks = buildClaudeHooks(config.hooks);
1138
+ if (claudeHooks) {
1139
+ claudeSettings.hooks = claudeHooks;
1140
+ }
1141
+ }
1142
+ await writeFile4(
1143
+ join5(claudeDir, "settings.json"),
1144
+ JSON.stringify(claudeSettings, null, 2) + "\n",
1145
+ "utf-8"
1146
+ );
1147
+ console.log(chalk4.green(" Generated .claude/settings.json"));
1148
+ const gitignoreLines = buildGitignoreLines(config);
1149
+ await writeManagedFile(join5(hubDir, ".gitignore"), gitignoreLines);
1150
+ console.log(chalk4.green(" Generated .gitignore"));
1151
+ }
1152
+ async function generateKiro(config, hubDir) {
1153
+ const kiroDir = join5(hubDir, ".kiro");
1154
+ const steeringDir = join5(kiroDir, "steering");
1155
+ const settingsDir = join5(kiroDir, "settings");
1156
+ await mkdir2(steeringDir, { recursive: true });
1157
+ await mkdir2(settingsDir, { recursive: true });
1158
+ const gitignoreLines = buildGitignoreLines(config);
1159
+ await writeManagedFile(join5(hubDir, ".gitignore"), gitignoreLines);
1160
+ console.log(chalk4.green(" Generated .gitignore"));
1161
+ const kiroRule = buildKiroOrchestratorRule(config);
1162
+ const kiroOrchestrator = buildKiroSteeringContent(kiroRule);
1163
+ await writeFile4(join5(steeringDir, "orchestrator.md"), kiroOrchestrator, "utf-8");
1164
+ console.log(chalk4.green(" Generated .kiro/steering/orchestrator.md"));
1165
+ await writeFile4(join5(hubDir, "AGENTS.md"), kiroRule + "\n", "utf-8");
1166
+ console.log(chalk4.green(" Generated AGENTS.md"));
1167
+ const agentsDir = resolve(hubDir, "agents");
1168
+ try {
1169
+ const agentFiles = await readdir(agentsDir);
1170
+ const mdFiles = agentFiles.filter((f) => f.endsWith(".md"));
1171
+ for (const file of mdFiles) {
1172
+ const agentContent = await readFile3(join5(agentsDir, file), "utf-8");
1173
+ const agentName = file.replace(/\.md$/, "");
1174
+ const steeringContent = buildKiroSteeringContent(agentContent, "auto", {
1175
+ name: agentName,
1176
+ description: `Role-specific instructions for the ${agentName} phase. Include when working on ${agentName}-related tasks.`
1177
+ });
1178
+ const steeringName = `agent-${file}`;
1179
+ await writeFile4(join5(steeringDir, steeringName), steeringContent, "utf-8");
1180
+ }
1181
+ console.log(chalk4.green(` Copied ${mdFiles.length} agents as steering files`));
1182
+ } catch {
1183
+ console.log(chalk4.yellow(" No agents/ directory found, skipping agent copy"));
1184
+ }
1185
+ const skillsDir = resolve(hubDir, "skills");
1186
+ try {
1187
+ const skillFolders = await readdir(skillsDir);
1188
+ const kiroSkillsDir = join5(kiroDir, "skills");
1189
+ await mkdir2(kiroSkillsDir, { recursive: true });
1190
+ let count = 0;
1191
+ for (const folder of skillFolders) {
1192
+ const skillFile = join5(skillsDir, folder, "SKILL.md");
1193
+ try {
1194
+ await readFile3(skillFile);
1195
+ const srcDir = join5(skillsDir, folder);
1196
+ const targetDir = join5(kiroSkillsDir, folder);
1197
+ await cp(srcDir, targetDir, { recursive: true });
1198
+ count++;
1199
+ } catch {
1200
+ }
1201
+ }
1202
+ if (count > 0) {
1203
+ console.log(chalk4.green(` Copied ${count} skills to .kiro/skills/`));
1204
+ }
1205
+ } catch {
1206
+ }
1207
+ if (config.mcps?.length) {
1208
+ const mcpConfig = {};
1209
+ for (const mcp of config.mcps) {
1210
+ mcpConfig[mcp.name] = buildKiroMcpEntry(mcp);
1211
+ }
1212
+ await writeFile4(
1213
+ join5(settingsDir, "mcp.json"),
1214
+ JSON.stringify({ mcpServers: mcpConfig }, null, 2) + "\n",
1215
+ "utf-8"
1216
+ );
1217
+ console.log(chalk4.green(" Generated .kiro/settings/mcp.json"));
1218
+ }
1219
+ if (config.hooks) {
1220
+ const hookNotes = [];
1221
+ for (const [event, entries] of Object.entries(config.hooks)) {
1222
+ const mapped = HOOK_EVENT_MAP[event]?.kiro;
1223
+ if (!mapped) continue;
1224
+ for (const entry of entries) {
1225
+ hookNotes.push(`- **${mapped}**: ${entry.type === "command" ? entry.command : entry.prompt}`);
1226
+ }
1227
+ }
1228
+ if (hookNotes.length > 0) {
1229
+ console.log(chalk4.yellow(` Note: Kiro hooks are managed via the Kiro panel UI.`));
1230
+ console.log(chalk4.yellow(` The following hooks should be configured manually:`));
1231
+ for (const note of hookNotes) {
1232
+ console.log(chalk4.yellow(` ${note}`));
1233
+ }
1234
+ }
1235
+ }
1236
+ }
1237
+ function buildGitignoreLines(config) {
1238
+ const lines = [
1239
+ "node_modules/",
1240
+ ".DS_Store",
1241
+ "",
1242
+ "# Repositories (managed by hub)"
1243
+ ];
1244
+ for (const repo of config.repos) {
1245
+ lines.push(repo.path.replace("./", ""));
1246
+ }
1247
+ lines.push(
1248
+ "",
1249
+ "# Docker volumes",
1250
+ "*_data/",
1251
+ "",
1252
+ "# Environment files",
1253
+ "*.env",
1254
+ "*.env.local",
1255
+ "!.env.example",
1256
+ "",
1257
+ "# Generated files",
1258
+ "docker-compose.yml",
1259
+ "",
1260
+ "# Task documents",
1261
+ "tasks/"
1262
+ );
1263
+ return lines;
1264
+ }
1265
+ var generators = {
1266
+ cursor: { name: "Cursor", generate: generateCursor },
1267
+ "claude-code": { name: "Claude Code", generate: generateClaudeCode },
1268
+ kiro: { name: "Kiro", generate: generateKiro }
1269
+ };
1270
+ var generateCommand = new Command4("generate").description("Generate editor-specific configuration files from hub.yaml").option("-e, --editor <editor>", "Target editor (cursor, claude-code, kiro)", "cursor").action(async (opts) => {
1271
+ const hubDir = process.cwd();
1272
+ const config = await loadHubConfig(hubDir);
1273
+ const generator = generators[opts.editor];
1274
+ if (!generator) {
1275
+ console.log(
1276
+ chalk4.red(`Unknown editor: ${opts.editor}. Available: ${Object.keys(generators).join(", ")}`)
1277
+ );
1278
+ return;
1279
+ }
1280
+ console.log(chalk4.blue(`
1281
+ Generating ${generator.name} configuration
1282
+ `));
1283
+ await generator.generate(config, hubDir);
1284
+ console.log(chalk4.green("\nDone!\n"));
1285
+ });
1286
+
1287
+ // src/commands/env.ts
1288
+ import { Command as Command5 } from "commander";
1289
+ import { existsSync as existsSync4 } from "fs";
1290
+ import { writeFile as writeFile5, readFile as readFile4 } from "fs/promises";
1291
+ import { join as join6 } from "path";
1292
+ import { execSync as execSync2 } from "child_process";
1293
+ import chalk5 from "chalk";
1294
+ var authenticatedProfiles = /* @__PURE__ */ new Set();
1295
+ function awsAuthenticated(profile) {
1296
+ if (authenticatedProfiles.has(profile)) return true;
1297
+ try {
1298
+ execSync2(`AWS_PROFILE=${profile} aws sts get-caller-identity`, {
1299
+ stdio: ["pipe", "pipe", "pipe"]
1300
+ });
1301
+ authenticatedProfiles.add(profile);
1302
+ return true;
1303
+ } catch {
1304
+ return false;
1305
+ }
1306
+ }
1307
+ function fetchSecret(secretName, profile) {
1308
+ try {
1309
+ const raw = execSync2(
1310
+ `AWS_PROFILE=${profile} aws secretsmanager get-secret-value --secret-id "${secretName}" --query SecretString --output text`,
1311
+ { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
1312
+ ).trim();
1313
+ return JSON.parse(raw);
1314
+ } catch {
1315
+ return null;
1316
+ }
1317
+ }
1318
+ function resolveSecret(secretConfig, defaultProfile) {
1319
+ if (typeof secretConfig === "string") {
1320
+ return { secretName: secretConfig, profile: defaultProfile };
1321
+ }
1322
+ return {
1323
+ secretName: secretConfig.secret,
1324
+ profile: secretConfig.profile || defaultProfile
1325
+ };
1326
+ }
1327
+ function buildDatabaseUrlFromSecret(secretData, buildConfig) {
1328
+ const vars = buildConfig.vars || {};
1329
+ const user = secretData[vars.user || "DB_USERNAME"];
1330
+ const password = secretData[vars.password || "DB_PASSWORD"];
1331
+ const host = secretData[vars.host || "DB_HOSTNAME"];
1332
+ const port = secretData[vars.port || "DB_PORT"] || "3306";
1333
+ const database = secretData[vars.database || "DB_NAME"];
1334
+ if (!user || !host || !database) return null;
1335
+ if (buildConfig.template) {
1336
+ return buildConfig.template.replace("{user}", user).replace("{password}", password || "").replace("{host}", host).replace("{port}", port).replace("{database}", database);
1337
+ }
1338
+ return `mysql://${user}:${password || ""}@${host}:${port}/${database}`;
1339
+ }
1340
+ var envCommand = new Command5("env").description("Generate environment files from hub.yaml profiles").argument("[profile]", "Environment profile (local, staging, prod)", "local").action(async (profile) => {
1341
+ const hubDir = process.cwd();
1342
+ const config = await loadHubConfig(hubDir);
1343
+ const profiles = config.env?.profiles;
1344
+ if (!profiles) {
1345
+ console.log(chalk5.yellow("\nNo env.profiles defined in hub.yaml\n"));
1346
+ return;
1347
+ }
1348
+ const profileConfig = profiles[profile];
1349
+ if (!profileConfig) {
1350
+ const available = Object.keys(profiles).join(", ");
1351
+ console.log(chalk5.red(`
1352
+ Unknown profile: ${profile}. Available: ${available}
1353
+ `));
1354
+ return;
1355
+ }
1356
+ console.log(chalk5.blue(`
1357
+ \u2501\u2501\u2501 Generating environment files (${profile}) \u2501\u2501\u2501
1358
+ `));
1359
+ const defaultAwsProfile = profileConfig.aws_profile || "";
1360
+ const secrets = profileConfig.secrets || {};
1361
+ const buildDbConfigs = profileConfig.build_database_url || {};
1362
+ if (profile !== "local" && defaultAwsProfile) {
1363
+ console.log(chalk5.cyan(` Default AWS profile: ${defaultAwsProfile}`));
1364
+ if (!awsAuthenticated(defaultAwsProfile)) {
1365
+ console.log(chalk5.red(` AWS profile ${defaultAwsProfile} not authenticated`));
1366
+ console.log(chalk5.cyan(` Run: aws sso login --profile ${defaultAwsProfile}`));
1367
+ return;
1368
+ }
1369
+ console.log(chalk5.green(` AWS authenticated`));
1370
+ }
1371
+ for (const repo of config.repos) {
1372
+ if (!repo.env_file) continue;
1373
+ const repoDir = join6(hubDir, repo.path);
1374
+ if (!existsSync4(repoDir)) {
1375
+ console.log(chalk5.dim(` ${repo.name}: repo not cloned, skipping`));
1376
+ continue;
1377
+ }
1378
+ console.log(chalk5.yellow(`\u25B8 ${repo.name}`));
1379
+ const envVars = {};
1380
+ if (profile !== "local" && secrets[repo.name]) {
1381
+ const { secretName, profile: secretProfile } = resolveSecret(
1382
+ secrets[repo.name],
1383
+ defaultAwsProfile
1384
+ );
1385
+ if (!secretProfile) {
1386
+ console.log(chalk5.red(` No AWS profile available for secret: ${secretName}`));
1387
+ } else {
1388
+ if (!awsAuthenticated(secretProfile)) {
1389
+ console.log(chalk5.red(` AWS profile ${secretProfile} not authenticated`));
1390
+ console.log(chalk5.cyan(` Run: aws sso login --profile ${secretProfile}`));
1391
+ continue;
1392
+ }
1393
+ console.log(chalk5.cyan(` Fetching: ${secretName} (profile: ${secretProfile})`));
1394
+ const secretData = fetchSecret(secretName, secretProfile);
1395
+ if (secretData) {
1396
+ Object.assign(envVars, secretData);
1397
+ console.log(chalk5.green(` Loaded ${Object.keys(secretData).length} vars from AWS`));
1398
+ } else {
1399
+ console.log(chalk5.red(` Failed to fetch secret: ${secretName}`));
1400
+ }
1401
+ }
1402
+ }
1403
+ if (profile !== "local" && buildDbConfigs[repo.name]) {
1404
+ const buildConfig = buildDbConfigs[repo.name];
1405
+ const dbProfile = buildConfig.profile || defaultAwsProfile;
1406
+ if (!dbProfile) {
1407
+ console.log(chalk5.red(` No AWS profile available for build_database_url`));
1408
+ } else {
1409
+ if (!awsAuthenticated(dbProfile)) {
1410
+ console.log(chalk5.red(` AWS profile ${dbProfile} not authenticated`));
1411
+ console.log(chalk5.cyan(` Run: aws sso login --profile ${dbProfile}`));
1412
+ } else {
1413
+ console.log(chalk5.cyan(` Building DATABASE_URL from ${buildConfig.from_secret} (profile: ${dbProfile})`));
1414
+ const dbSecretData = fetchSecret(buildConfig.from_secret, dbProfile);
1415
+ if (dbSecretData) {
1416
+ const dbUrl = buildDatabaseUrlFromSecret(dbSecretData, buildConfig);
1417
+ if (dbUrl) {
1418
+ envVars["DATABASE_URL"] = dbUrl;
1419
+ envVars["IDENTITY_DATABASE_URL"] = dbUrl;
1420
+ console.log(chalk5.green(` Built DATABASE_URL from ${buildConfig.from_secret}`));
1421
+ } else {
1422
+ console.log(chalk5.red(` Could not build DATABASE_URL - missing required fields`));
1423
+ }
1424
+ } else {
1425
+ console.log(chalk5.red(` Failed to fetch secret: ${buildConfig.from_secret}`));
1426
+ }
1427
+ }
1428
+ }
1429
+ }
1430
+ const overrides = config.env?.overrides?.[profile]?.[repo.name];
1431
+ if (overrides) {
1432
+ for (const [key, value] of Object.entries(overrides)) {
1433
+ envVars[key] = value;
1434
+ }
1435
+ console.log(chalk5.cyan(` Applied ${Object.keys(overrides).length} overrides`));
1436
+ }
1437
+ const envPath = join6(repoDir, repo.env_file);
1438
+ const existingVars = {};
1439
+ try {
1440
+ const existing = await readFile4(envPath, "utf-8");
1441
+ for (const line of existing.split("\n")) {
1442
+ const trimmed = line.trim();
1443
+ if (!trimmed || trimmed.startsWith("#")) continue;
1444
+ const eqIdx = trimmed.indexOf("=");
1445
+ if (eqIdx > 0) {
1446
+ existingVars[trimmed.slice(0, eqIdx)] = trimmed.slice(eqIdx + 1);
1447
+ }
1448
+ }
1449
+ } catch {
1450
+ }
1451
+ const merged = { ...existingVars, ...envVars };
1452
+ const lines = Object.entries(merged).map(([k, v]) => `${k}=${v}`);
1453
+ await writeFile5(envPath, lines.join("\n") + "\n", "utf-8");
1454
+ console.log(chalk5.green(` Created ${repo.env_file} (${lines.length} vars)`));
1455
+ }
1456
+ console.log();
1457
+ if (profile === "local") {
1458
+ console.log(chalk5.cyan("Using local Docker services only"));
1459
+ console.log(chalk5.cyan("Run: npx @arvoretech/hub services up"));
1460
+ } else if (profile === "prod") {
1461
+ console.log(chalk5.red("WARNING: Using PRODUCTION database!"));
1462
+ console.log(chalk5.red("Be careful with write operations!"));
1463
+ } else {
1464
+ console.log(chalk5.cyan(`Using ${profile} environment`));
1465
+ }
1466
+ console.log();
1467
+ });
1468
+
1469
+ // src/commands/services.ts
1470
+ import { Command as Command6 } from "commander";
1471
+ import { existsSync as existsSync5 } from "fs";
1472
+ import { writeFile as writeFile6 } from "fs/promises";
1473
+ import { join as join7 } from "path";
1474
+ import { execSync as execSync3 } from "child_process";
1475
+ import chalk6 from "chalk";
1476
+ async function ensureCompose(hubDir) {
1477
+ const composePath = join7(hubDir, "docker-compose.yml");
1478
+ if (!existsSync5(composePath)) {
1479
+ const config = await loadHubConfig(hubDir);
1480
+ if (!config.services?.length) {
1481
+ console.log(chalk6.yellow("No services defined in hub.yaml"));
1482
+ process.exit(1);
1483
+ }
1484
+ const content = generateDockerCompose(config.services);
1485
+ await writeFile6(composePath, content, "utf-8");
1486
+ console.log(chalk6.green("Generated docker-compose.yml"));
1487
+ }
1488
+ return composePath;
1489
+ }
1490
+ function isDockerRunning() {
1491
+ try {
1492
+ execSync3("docker info", { stdio: ["pipe", "pipe", "pipe"] });
1493
+ return true;
1494
+ } catch {
1495
+ return false;
1496
+ }
1497
+ }
1498
+ var servicesCommand = new Command6("services").description("Manage Docker development services").argument("[action]", "up, down, restart, ps, logs, clean", "up").argument("[service...]", "Specific services (for logs)").action(async (action, serviceNames) => {
1499
+ if (!isDockerRunning()) {
1500
+ console.log(chalk6.red("\nDocker daemon is not running."));
1501
+ console.log(chalk6.dim("Start Docker Desktop or the Docker daemon and try again.\n"));
1502
+ return;
1503
+ }
1504
+ const hubDir = process.cwd();
1505
+ const composePath = await ensureCompose(hubDir);
1506
+ const compose = `docker compose -f ${composePath}`;
1507
+ try {
1508
+ switch (action) {
1509
+ case "up":
1510
+ case "start": {
1511
+ console.log(chalk6.blue("\nStarting services\n"));
1512
+ execSync3(`${compose} up -d`, { stdio: "inherit", cwd: hubDir });
1513
+ const config = await loadHubConfig(hubDir);
1514
+ console.log(chalk6.green("\nServices running:"));
1515
+ for (const svc of config.services || []) {
1516
+ const port = svc.port || svc.ports?.[0];
1517
+ if (port) console.log(chalk6.cyan(` ${svc.name}: localhost:${port}`));
1518
+ }
1519
+ console.log();
1520
+ break;
1521
+ }
1522
+ case "down":
1523
+ case "stop":
1524
+ console.log(chalk6.blue("\nStopping services\n"));
1525
+ execSync3(`${compose} down`, { stdio: "inherit", cwd: hubDir });
1526
+ console.log(chalk6.green("\nServices stopped\n"));
1527
+ break;
1528
+ case "restart":
1529
+ execSync3(`${compose} restart`, { stdio: "inherit", cwd: hubDir });
1530
+ console.log(chalk6.green("\nServices restarted\n"));
1531
+ break;
1532
+ case "ps":
1533
+ case "status":
1534
+ execSync3(`${compose} ps`, { stdio: "inherit", cwd: hubDir });
1535
+ break;
1536
+ case "logs":
1537
+ if (serviceNames.length) {
1538
+ execSync3(`${compose} logs -f ${serviceNames.join(" ")}`, { stdio: "inherit", cwd: hubDir });
1539
+ } else {
1540
+ execSync3(`${compose} logs -f`, { stdio: "inherit", cwd: hubDir });
1541
+ }
1542
+ break;
1543
+ case "clean":
1544
+ console.log(chalk6.blue("\nCleaning services (removing volumes)\n"));
1545
+ execSync3(`${compose} down -v`, { stdio: "inherit", cwd: hubDir });
1546
+ console.log(chalk6.green("\nServices and volumes removed\n"));
1547
+ break;
1548
+ default:
1549
+ console.log(chalk6.red(`Unknown action: ${action}`));
1550
+ console.log("Usage: hub services [up|down|restart|ps|logs|clean]");
1551
+ process.exit(1);
1552
+ }
1553
+ } catch {
1554
+ console.log(chalk6.red("\nFailed to execute docker compose command."));
1555
+ console.log(chalk6.dim("Check if Docker is running and the docker-compose.yml is valid.\n"));
1556
+ }
1557
+ });
1558
+
1559
+ // src/commands/skills.ts
1560
+ import { Command as Command8 } from "commander";
1561
+ import { existsSync as existsSync6, statSync } from "fs";
1562
+ import { mkdir as mkdir4, readdir as readdir2, readFile as readFile5, rm, cp as cp2 } from "fs/promises";
1563
+ import { join as join9, resolve as resolve2 } from "path";
1564
+ import { execSync as execSync4 } from "child_process";
1565
+ import chalk8 from "chalk";
1566
+
1567
+ // src/commands/registry.ts
1568
+ import { Command as Command7 } from "commander";
1569
+ import { mkdir as mkdir3, writeFile as writeFile7 } from "fs/promises";
1570
+ import { join as join8 } from "path";
1571
+ import chalk7 from "chalk";
1572
+ var DEFAULT_REGISTRY_REPO = process.env.HUB_REGISTRY || "arvoreeducacao/rhm";
1573
+ var DEFAULT_BRANCH = "main";
1574
+ async function downloadDirFromGitHub(repo, remotePath, destDir, branch = DEFAULT_BRANCH) {
1575
+ const apiUrl = `https://api.github.com/repos/${repo}/contents/${remotePath}?ref=${branch}`;
1576
+ const res = await fetch(apiUrl, {
1577
+ headers: { Accept: "application/vnd.github.v3+json" }
1578
+ });
1579
+ if (!res.ok) {
1580
+ if (res.status === 404) {
1581
+ throw new Error(`Not found: ${remotePath} in ${repo}`);
1582
+ }
1583
+ throw new Error(`GitHub API error: ${res.status} ${res.statusText}`);
1584
+ }
1585
+ const items = await res.json();
1586
+ await mkdir3(destDir, { recursive: true });
1587
+ for (const item of items) {
1588
+ if (item.type === "file" && item.download_url) {
1589
+ const fileRes = await fetch(item.download_url);
1590
+ if (!fileRes.ok) continue;
1591
+ const content = await fileRes.text();
1592
+ await writeFile7(join8(destDir, item.name), content, "utf-8");
1593
+ } else if (item.type === "dir") {
1594
+ await downloadDirFromGitHub(repo, item.path, join8(destDir, item.name), branch);
1595
+ }
1596
+ }
1597
+ }
1598
+ async function listRegistryDir(repo, remotePath, branch = DEFAULT_BRANCH) {
1599
+ const apiUrl = `https://api.github.com/repos/${repo}/contents/${remotePath}?ref=${branch}`;
1600
+ const res = await fetch(apiUrl, {
1601
+ headers: { Accept: "application/vnd.github.v3+json" }
1602
+ });
1603
+ if (!res.ok) return [];
1604
+ return await res.json();
1605
+ }
1606
+ async function listRegistrySkills(repo) {
1607
+ const items = await listRegistryDir(repo, "skills");
1608
+ const skills = [];
1609
+ for (const item of items) {
1610
+ if (item.type !== "dir") continue;
1611
+ const skillUrl = `https://raw.githubusercontent.com/${repo}/${DEFAULT_BRANCH}/skills/${item.name}/SKILL.md`;
1612
+ try {
1613
+ const res = await fetch(skillUrl);
1614
+ if (!res.ok) continue;
1615
+ const content = await res.text();
1616
+ const descMatch = content.match(/^description:\s*(.+)$/m);
1617
+ skills.push({
1618
+ name: item.name,
1619
+ description: descMatch?.[1]?.replace(/^["']|["']$/g, "") || ""
1620
+ });
1621
+ } catch {
1622
+ skills.push({ name: item.name, description: "" });
1623
+ }
1624
+ }
1625
+ return skills;
1626
+ }
1627
+ async function listRegistryAgents(repo) {
1628
+ const items = await listRegistryDir(repo, "agents");
1629
+ const agents = [];
1630
+ for (const item of items) {
1631
+ if (item.type !== "file" || !item.name.endsWith(".md")) continue;
1632
+ const agentUrl = `https://raw.githubusercontent.com/${repo}/${DEFAULT_BRANCH}/agents/${item.name}`;
1633
+ try {
1634
+ const res = await fetch(agentUrl);
1635
+ if (!res.ok) continue;
1636
+ const content = await res.text();
1637
+ const descMatch = content.match(/^description:\s*(.+)$/m);
1638
+ const name = item.name.replace(/\.md$/, "");
1639
+ agents.push({
1640
+ name,
1641
+ description: descMatch?.[1]?.replace(/^["']|["']$/g, "") || ""
1642
+ });
1643
+ } catch {
1644
+ agents.push({ name: item.name.replace(/\.md$/, ""), description: "" });
1645
+ }
1646
+ }
1647
+ return agents;
1648
+ }
1649
+ async function listRegistryHooks(repo) {
1650
+ const items = await listRegistryDir(repo, "hooks");
1651
+ const hooks = [];
1652
+ for (const item of items) {
1653
+ if (item.type !== "dir") continue;
1654
+ const readmeUrl = `https://raw.githubusercontent.com/${repo}/${DEFAULT_BRANCH}/hooks/${item.name}/README.md`;
1655
+ try {
1656
+ const res = await fetch(readmeUrl);
1657
+ if (!res.ok) {
1658
+ hooks.push({ name: item.name, description: "" });
1659
+ continue;
1660
+ }
1661
+ const content = await res.text();
1662
+ const firstLine = content.split("\n").find((l) => l.trim() && !l.startsWith("#"));
1663
+ hooks.push({
1664
+ name: item.name,
1665
+ description: firstLine?.trim() || ""
1666
+ });
1667
+ } catch {
1668
+ hooks.push({ name: item.name, description: "" });
1669
+ }
1670
+ }
1671
+ return hooks;
1672
+ }
1673
+ async function listRegistryCommands(repo) {
1674
+ const items = await listRegistryDir(repo, "commands");
1675
+ const commands = [];
1676
+ for (const item of items) {
1677
+ if (item.type !== "file" || !item.name.endsWith(".md")) continue;
1678
+ const cmdUrl = `https://raw.githubusercontent.com/${repo}/${DEFAULT_BRANCH}/commands/${item.name}`;
1679
+ try {
1680
+ const res = await fetch(cmdUrl);
1681
+ if (!res.ok) continue;
1682
+ const content = await res.text();
1683
+ const firstLine = content.split("\n").find((l) => l.trim() && !l.startsWith("#"));
1684
+ const name = item.name.replace(/\.md$/, "");
1685
+ commands.push({
1686
+ name,
1687
+ description: firstLine?.trim() || ""
1688
+ });
1689
+ } catch {
1690
+ commands.push({ name: item.name.replace(/\.md$/, ""), description: "" });
1691
+ }
1692
+ }
1693
+ return commands;
1694
+ }
1695
+ var TYPE_LABELS = {
1696
+ skill: (t) => chalk7.green(t),
1697
+ agent: (t) => chalk7.blue(t),
1698
+ hook: (t) => chalk7.magenta(t),
1699
+ command: (t) => chalk7.cyan(t)
1700
+ };
1701
+ var INSTALL_HINTS = {
1702
+ skill: "hub skills add <name>",
1703
+ agent: "hub agents add <name>",
1704
+ hook: "hub hooks add <name>",
1705
+ command: "hub commands add <name>"
1706
+ };
1707
+ var registryCommand = new Command7("registry").description("Browse and install skills, agents, hooks, and commands from the registry").option("-r, --repo <repo>", "Registry repository (owner/repo)", DEFAULT_REGISTRY_REPO).addCommand(
1708
+ new Command7("search").description("Search the registry").argument("[query]", "Search term").option("-t, --type <type>", "Filter by type (skill, agent, hook, command)").action(async (query, opts) => {
1709
+ const repo = registryCommand.opts().repo || DEFAULT_REGISTRY_REPO;
1710
+ console.log(chalk7.blue(`
1711
+ \u2501\u2501\u2501 Hub Registry (${repo}) \u2501\u2501\u2501
1712
+ `));
1713
+ const results = [];
1714
+ const typeFilter = opts?.type;
1715
+ if (!typeFilter || typeFilter === "skill") {
1716
+ try {
1717
+ const skills = await listRegistrySkills(repo);
1718
+ for (const s of skills) results.push({ type: "skill", ...s });
1719
+ } catch {
1720
+ console.log(chalk7.yellow(" Could not fetch skills from registry."));
1721
+ }
1722
+ }
1723
+ if (!typeFilter || typeFilter === "agent") {
1724
+ try {
1725
+ const agents = await listRegistryAgents(repo);
1726
+ for (const a of agents) results.push({ type: "agent", ...a });
1727
+ } catch {
1728
+ console.log(chalk7.yellow(" Could not fetch agents from registry."));
1729
+ }
1730
+ }
1731
+ if (!typeFilter || typeFilter === "hook") {
1732
+ try {
1733
+ const hooks = await listRegistryHooks(repo);
1734
+ for (const h of hooks) results.push({ type: "hook", ...h });
1735
+ } catch {
1736
+ console.log(chalk7.yellow(" Could not fetch hooks from registry."));
1737
+ }
1738
+ }
1739
+ if (!typeFilter || typeFilter === "command") {
1740
+ try {
1741
+ const commands = await listRegistryCommands(repo);
1742
+ for (const c of commands) results.push({ type: "command", ...c });
1743
+ } catch {
1744
+ console.log(chalk7.yellow(" Could not fetch commands from registry."));
1745
+ }
1746
+ }
1747
+ let filtered = results;
1748
+ if (query) {
1749
+ const q = query.toLowerCase();
1750
+ filtered = results.filter(
1751
+ (e) => e.name.toLowerCase().includes(q) || e.description.toLowerCase().includes(q)
1752
+ );
1753
+ }
1754
+ if (filtered.length === 0) {
1755
+ console.log(chalk7.dim(" No results found.\n"));
1756
+ return;
1757
+ }
1758
+ for (const entry of filtered) {
1759
+ const labelFn = TYPE_LABELS[entry.type] || ((t) => t);
1760
+ console.log(` ${labelFn(`[${entry.type}]`)} ${chalk7.yellow(entry.name)}`);
1761
+ if (entry.description) console.log(` ${entry.description}`);
1762
+ console.log();
1763
+ }
1764
+ console.log(chalk7.cyan(` ${filtered.length} result(s)
1765
+ `));
1766
+ const types = [...new Set(filtered.map((e) => e.type))];
1767
+ for (const type of types) {
1768
+ const hint = INSTALL_HINTS[type];
1769
+ if (hint) console.log(chalk7.dim(` Install ${type}: ${hint}`));
1770
+ }
1771
+ console.log();
1772
+ })
1773
+ ).addCommand(
1774
+ new Command7("list").description("List everything in the registry").option("-t, --type <type>", "Filter by type (skill, agent, hook, command)").action(async (opts) => {
1775
+ const repo = registryCommand.opts().repo || DEFAULT_REGISTRY_REPO;
1776
+ console.log(chalk7.blue(`
1777
+ \u2501\u2501\u2501 Hub Registry (${repo}) \u2501\u2501\u2501
1778
+ `));
1779
+ const typeFilter = opts?.type;
1780
+ if (!typeFilter || typeFilter === "skill") {
1781
+ try {
1782
+ const skills = await listRegistrySkills(repo);
1783
+ if (skills.length) {
1784
+ console.log(chalk7.green(`Skills (${skills.length}):`));
1785
+ for (const s of skills) {
1786
+ console.log(` ${chalk7.yellow(s.name)}${s.description ? ` \u2014 ${s.description}` : ""}`);
1787
+ }
1788
+ console.log();
1789
+ }
1790
+ } catch {
1791
+ console.log(chalk7.yellow(" Could not fetch skills.\n"));
1792
+ }
1793
+ }
1794
+ if (!typeFilter || typeFilter === "agent") {
1795
+ try {
1796
+ const agents = await listRegistryAgents(repo);
1797
+ if (agents.length) {
1798
+ console.log(chalk7.blue(`Agents (${agents.length}):`));
1799
+ for (const a of agents) {
1800
+ console.log(` ${chalk7.yellow(a.name)}${a.description ? ` \u2014 ${a.description}` : ""}`);
1801
+ }
1802
+ console.log();
1803
+ }
1804
+ } catch {
1805
+ console.log(chalk7.yellow(" Could not fetch agents.\n"));
1806
+ }
1807
+ }
1808
+ if (!typeFilter || typeFilter === "hook") {
1809
+ try {
1810
+ const hooks = await listRegistryHooks(repo);
1811
+ if (hooks.length) {
1812
+ console.log(chalk7.magenta(`Hooks (${hooks.length}):`));
1813
+ for (const h of hooks) {
1814
+ console.log(` ${chalk7.yellow(h.name)}${h.description ? ` \u2014 ${h.description}` : ""}`);
1815
+ }
1816
+ console.log();
1817
+ }
1818
+ } catch {
1819
+ console.log(chalk7.yellow(" Could not fetch hooks.\n"));
1820
+ }
1821
+ }
1822
+ if (!typeFilter || typeFilter === "command") {
1823
+ try {
1824
+ const commands = await listRegistryCommands(repo);
1825
+ if (commands.length) {
1826
+ console.log(chalk7.cyan(`Commands (${commands.length}):`));
1827
+ for (const c of commands) {
1828
+ console.log(` ${chalk7.yellow(c.name)}${c.description ? ` \u2014 ${c.description}` : ""}`);
1829
+ }
1830
+ console.log();
1831
+ }
1832
+ } catch {
1833
+ console.log(chalk7.yellow(" Could not fetch commands.\n"));
1834
+ }
1835
+ }
1836
+ })
1837
+ );
1838
+
1839
+ // src/commands/skills.ts
1840
+ var DEFAULT_REGISTRY_REPO2 = process.env.HUB_REGISTRY || "arvoreeducacao/rhm";
1841
+ function tmpDir() {
1842
+ return join9(process.env.TMPDIR || "/tmp", `hub-skills-${Date.now()}`);
1843
+ }
1844
+ async function listLocalSkills(hubDir) {
1845
+ const skillsDir = join9(hubDir, "skills");
1846
+ const skills = [];
1847
+ if (!existsSync6(skillsDir)) return skills;
1848
+ const folders = await readdir2(skillsDir);
1849
+ for (const folder of folders) {
1850
+ const skillFile = join9(skillsDir, folder, "SKILL.md");
1851
+ if (!existsSync6(skillFile)) continue;
1852
+ const content = await readFile5(skillFile, "utf-8");
1853
+ const descMatch = content.match(/^description:\s*(.+)$/m);
1854
+ skills.push({
1855
+ name: folder,
1856
+ description: descMatch?.[1] || ""
1857
+ });
1858
+ }
1859
+ return skills;
1860
+ }
1861
+ async function installSkillsFromDir(sourceSkillsDir, hubDir, opts) {
1862
+ if (!existsSync6(sourceSkillsDir)) {
1863
+ console.log(chalk8.red(" No skills/ directory found in source"));
1864
+ return;
1865
+ }
1866
+ const available = await readdir2(sourceSkillsDir);
1867
+ const skillFolders = available.filter(
1868
+ (f) => existsSync6(join9(sourceSkillsDir, f, "SKILL.md"))
1869
+ );
1870
+ if (skillFolders.length === 0) {
1871
+ console.log(chalk8.red(" No skills found (looking for skills/*/SKILL.md)"));
1872
+ return;
1873
+ }
1874
+ const toInstall = opts.skill ? skillFolders.filter((s) => s === opts.skill) : skillFolders;
1875
+ if (opts.skill && toInstall.length === 0) {
1876
+ console.log(chalk8.red(` Skill '${opts.skill}' not found. Available: ${skillFolders.join(", ")}`));
1877
+ return;
1878
+ }
1879
+ const targetBase = opts.global ? join9(process.env.HOME || "~", ".cursor", "skills") : join9(hubDir, "skills");
1880
+ await mkdir4(targetBase, { recursive: true });
1881
+ for (const skill of toInstall) {
1882
+ const src = join9(sourceSkillsDir, skill);
1883
+ const dest = join9(targetBase, skill);
1884
+ await cp2(src, dest, { recursive: true });
1885
+ console.log(chalk8.green(` Installed: ${skill}`));
1886
+ }
1887
+ console.log(
1888
+ chalk8.green(
1889
+ `
1890
+ ${toInstall.length} skill(s) installed to ${opts.global ? "global" : "project"}
1891
+ `
1892
+ )
1893
+ );
1894
+ }
1895
+ async function addFromRegistry(skillName, hubDir, opts) {
1896
+ const repo = opts.repo || DEFAULT_REGISTRY_REPO2;
1897
+ const remotePath = `skills/${skillName}`;
1898
+ const targetBase = opts.global ? join9(process.env.HOME || "~", ".cursor", "skills") : join9(hubDir, "skills");
1899
+ const dest = join9(targetBase, skillName);
1900
+ console.log(chalk8.cyan(` Downloading ${skillName} from ${repo}...`));
1901
+ try {
1902
+ await downloadDirFromGitHub(repo, remotePath, dest);
1903
+ if (!existsSync6(join9(dest, "SKILL.md"))) {
1904
+ await rm(dest, { recursive: true }).catch(() => {
1905
+ });
1906
+ console.log(chalk8.red(` Skill '${skillName}' not found in registry (${repo})`));
1907
+ console.log(chalk8.dim(" Run 'hub registry list' to see available skills."));
1908
+ return;
1909
+ }
1910
+ console.log(chalk8.green(` Installed: ${skillName}`));
1911
+ console.log(chalk8.green(`
1912
+ 1 skill(s) installed to ${opts.global ? "global" : "project"}
1913
+ `));
1914
+ } catch (err) {
1915
+ console.log(chalk8.red(` Failed to download skill '${skillName}': ${err.message}`));
1916
+ console.log(chalk8.dim(" Run 'hub registry list' to see available skills."));
1917
+ }
1918
+ }
1919
+ async function addFromGitHubSkill(owner, repo, skillName, hubDir, opts) {
1920
+ const fullRepo = `${owner}/${repo}`;
1921
+ const remotePath = `skills/${skillName}`;
1922
+ const targetBase = opts.global ? join9(process.env.HOME || "~", ".cursor", "skills") : join9(hubDir, "skills");
1923
+ const dest = join9(targetBase, skillName);
1924
+ console.log(chalk8.cyan(` Downloading ${skillName} from ${fullRepo} via GitHub API...`));
1925
+ try {
1926
+ await downloadDirFromGitHub(fullRepo, remotePath, dest);
1927
+ if (!existsSync6(join9(dest, "SKILL.md"))) {
1928
+ await rm(dest, { recursive: true }).catch(() => {
1929
+ });
1930
+ console.log(chalk8.red(` Skill '${skillName}' not found in ${fullRepo}/skills/`));
1931
+ console.log(chalk8.dim(` Check available skills: hub skills add ${fullRepo} --list`));
1932
+ return;
1933
+ }
1934
+ console.log(chalk8.green(` Installed: ${skillName} (from ${fullRepo})`));
1935
+ console.log(chalk8.green(`
1936
+ 1 skill(s) installed to ${opts.global ? "global" : "project"}
1937
+ `));
1938
+ } catch (err) {
1939
+ console.log(chalk8.red(` Failed to download: ${err.message}`));
1940
+ }
1941
+ }
1942
+ async function listRemoteSkills(owner, repo) {
1943
+ const fullRepo = `${owner}/${repo}`;
1944
+ console.log(chalk8.cyan(` Fetching skills from ${fullRepo}...
1945
+ `));
1946
+ try {
1947
+ const apiUrl = `https://api.github.com/repos/${fullRepo}/contents/skills`;
1948
+ const res = await fetch(apiUrl, {
1949
+ headers: { Accept: "application/vnd.github.v3+json" }
1950
+ });
1951
+ if (!res.ok) {
1952
+ console.log(chalk8.red(` Could not list skills from ${fullRepo}`));
1953
+ return;
1954
+ }
1955
+ const items = await res.json();
1956
+ const dirs = items.filter((i) => i.type === "dir");
1957
+ if (dirs.length === 0) {
1958
+ console.log(chalk8.dim(" No skills found."));
1959
+ return;
1960
+ }
1961
+ console.log(chalk8.green(` Available skills (${dirs.length}):
1962
+ `));
1963
+ for (const dir of dirs) {
1964
+ console.log(` ${chalk8.yellow(dir.name)}`);
1965
+ }
1966
+ console.log(chalk8.dim(`
1967
+ Install with: hub skills add ${fullRepo}/<skill-name>`));
1968
+ console.log(chalk8.dim(` Install all: hub skills add ${fullRepo}
1969
+ `));
1970
+ } catch {
1971
+ console.log(chalk8.red(` Failed to fetch skill list from ${fullRepo}`));
1972
+ }
1973
+ }
1974
+ async function addFromLocalPath(localPath, hubDir, opts) {
1975
+ const absPath = resolve2(localPath);
1976
+ if (!existsSync6(absPath)) {
1977
+ console.log(chalk8.red(` Path not found: ${absPath}`));
1978
+ return;
1979
+ }
1980
+ if (!statSync(absPath).isDirectory()) {
1981
+ console.log(chalk8.red(` Path is not a directory: ${absPath}`));
1982
+ return;
1983
+ }
1984
+ const sourceSkillsDir = join9(absPath, "skills");
1985
+ await installSkillsFromDir(sourceSkillsDir, hubDir, opts);
1986
+ }
1987
+ async function addFromGitRepo(source, hubDir, opts) {
1988
+ const tmp = tmpDir();
1989
+ try {
1990
+ console.log(chalk8.cyan(` Cloning ${source}...`));
1991
+ try {
1992
+ execSync4(`git clone --depth 1 ${source} ${tmp}`, {
1993
+ stdio: ["pipe", "pipe", "pipe"]
1994
+ });
1995
+ } catch {
1996
+ console.log(chalk8.red(` Repository not found or not accessible: ${source}`));
1997
+ console.log(chalk8.dim(" Make sure the URL is correct and you have access to the repository."));
1998
+ return;
1999
+ }
2000
+ const sourceSkillsDir = join9(tmp, "skills");
2001
+ await installSkillsFromDir(sourceSkillsDir, hubDir, opts);
2002
+ } finally {
2003
+ if (existsSync6(tmp)) {
2004
+ await rm(tmp, { recursive: true });
2005
+ }
2006
+ }
2007
+ }
2008
+ function isLocalPath(source) {
2009
+ return source.startsWith("/") || source.startsWith("./") || source.startsWith("../") || source.startsWith("~");
2010
+ }
2011
+ function parseGitHubSource(source) {
2012
+ const parts = source.split("/");
2013
+ if (parts.length === 2) return { owner: parts[0], repo: parts[1] };
2014
+ if (parts.length === 3) return { owner: parts[0], repo: parts[1], skill: parts[2] };
2015
+ return null;
2016
+ }
2017
+ var skillsCommand = new Command8("skills").description("Manage agent skills").addCommand(
2018
+ new Command8("add").description("Install skills from registry, GitHub (skills.sh compatible), git URL, or local path").argument("<source>", "Skill name, owner/repo, owner/repo/skill, git URL, or local path").option("-s, --skill <name>", "Install a specific skill only (for repo sources)").option("-g, --global", "Install to global ~/.cursor/skills/").option("-r, --repo <repo>", "Registry repository (owner/repo)").option("-l, --list", "List available skills without installing").action(async (source, opts) => {
2019
+ const hubDir = process.cwd();
2020
+ if (isLocalPath(source)) {
2021
+ console.log(chalk8.blue(`
2022
+ Installing skills from ${source}
2023
+ `));
2024
+ await addFromLocalPath(source, hubDir, opts);
2025
+ return;
2026
+ }
2027
+ if (source.startsWith("git@") || source.startsWith("https://")) {
2028
+ console.log(chalk8.blue(`
2029
+ Installing skills from ${source}
2030
+ `));
2031
+ await addFromGitRepo(source, hubDir, opts);
2032
+ return;
2033
+ }
2034
+ const parsed = parseGitHubSource(source);
2035
+ if (parsed && opts.list) {
2036
+ console.log(chalk8.blue(`
2037
+ Listing skills from ${parsed.owner}/${parsed.repo}
2038
+ `));
2039
+ await listRemoteSkills(parsed.owner, parsed.repo);
2040
+ return;
2041
+ }
2042
+ if (parsed?.skill) {
2043
+ console.log(chalk8.blue(`
2044
+ Installing skill ${parsed.skill} from ${parsed.owner}/${parsed.repo}
2045
+ `));
2046
+ await addFromGitHubSkill(parsed.owner, parsed.repo, parsed.skill, hubDir, opts);
2047
+ return;
2048
+ }
2049
+ if (parsed && !parsed.skill) {
2050
+ console.log(chalk8.blue(`
2051
+ Installing skills from ${source}
2052
+ `));
2053
+ const url = `https://github.com/${source}.git`;
2054
+ await addFromGitRepo(url, hubDir, opts);
2055
+ return;
2056
+ }
2057
+ console.log(chalk8.blue(`
2058
+ Installing skill ${source} from registry
2059
+ `));
2060
+ await addFromRegistry(source, hubDir, opts);
2061
+ })
2062
+ ).addCommand(
2063
+ new Command8("find").description("Browse community skills on skills.sh").argument("[query]", "Search term (opens skills.sh)").action(async (query) => {
2064
+ const url = query ? `https://skills.sh/?q=${encodeURIComponent(query)}` : "https://skills.sh";
2065
+ console.log(chalk8.blue("\n Browse community skills at:\n"));
2066
+ console.log(chalk8.cyan(` ${url}
2067
+ `));
2068
+ console.log(chalk8.dim(" Install with: hub skills add <owner>/<repo>/<skill-name>"));
2069
+ console.log(chalk8.dim(" Example: hub skills add vercel-labs/agent-skills/react-best-practices\n"));
2070
+ })
2071
+ ).addCommand(
2072
+ new Command8("list").description("List installed skills").action(async () => {
2073
+ const hubDir = process.cwd();
2074
+ console.log(chalk8.blue("\nInstalled skills\n"));
2075
+ const projectSkills = await listLocalSkills(hubDir);
2076
+ if (projectSkills.length > 0) {
2077
+ console.log(chalk8.cyan("Project:"));
2078
+ for (const s of projectSkills) {
2079
+ console.log(` ${chalk8.yellow(s.name)} \u2014 ${s.description}`);
2080
+ }
2081
+ } else {
2082
+ console.log(chalk8.dim(" No project skills (skills/)"));
2083
+ }
2084
+ const globalDir = join9(process.env.HOME || "~", ".cursor", "skills");
2085
+ const globalSkills = await listLocalSkills(join9(globalDir, ".."));
2086
+ console.log();
2087
+ if (globalSkills.length > 0) {
2088
+ console.log(chalk8.cyan("Global:"));
2089
+ for (const s of globalSkills) {
2090
+ console.log(` ${chalk8.yellow(s.name)} \u2014 ${s.description}`);
2091
+ }
2092
+ } else {
2093
+ console.log(chalk8.dim(" No global skills (~/.cursor/skills/)"));
2094
+ }
2095
+ console.log();
2096
+ })
2097
+ ).addCommand(
2098
+ new Command8("remove").description("Remove a skill").argument("<name>", "Skill name to remove").option("-g, --global", "Remove from global skills").action(async (name, opts) => {
2099
+ const hubDir = process.cwd();
2100
+ const base = opts.global ? join9(process.env.HOME || "~", ".cursor", "skills") : join9(hubDir, "skills");
2101
+ const target = join9(base, name);
2102
+ if (!existsSync6(target)) {
2103
+ console.log(chalk8.red(`
2104
+ Skill '${name}' not found in ${opts.global ? "global" : "project"}
2105
+ `));
2106
+ return;
2107
+ }
2108
+ await rm(target, { recursive: true });
2109
+ console.log(chalk8.green(`
2110
+ Removed skill: ${name}
2111
+ `));
2112
+ })
2113
+ );
2114
+
2115
+ // src/commands/agents.ts
2116
+ import { Command as Command9 } from "commander";
2117
+ import { existsSync as existsSync7, statSync as statSync2 } from "fs";
2118
+ import { mkdir as mkdir5, readdir as readdir3, readFile as readFile6, rm as rm2, copyFile as copyFile2, writeFile as writeFile8 } from "fs/promises";
2119
+ import { join as join10, resolve as resolve3 } from "path";
2120
+ import { execSync as execSync5 } from "child_process";
2121
+ import chalk9 from "chalk";
2122
+ var DEFAULT_REGISTRY_REPO3 = process.env.HUB_REGISTRY || "arvoreeducacao/rhm";
2123
+ var DEFAULT_BRANCH2 = "main";
2124
+ function tmpDir2() {
2125
+ return join10(process.env.TMPDIR || "/tmp", `hub-agents-${Date.now()}`);
2126
+ }
2127
+ async function listLocalAgents(agentsDir) {
2128
+ const agents = [];
2129
+ if (!existsSync7(agentsDir)) return agents;
2130
+ const files = await readdir3(agentsDir);
2131
+ for (const file of files) {
2132
+ if (!file.endsWith(".md")) continue;
2133
+ const content = await readFile6(join10(agentsDir, file), "utf-8");
2134
+ const descMatch = content.match(/^description:\s*(.+)$/m);
2135
+ agents.push({
2136
+ name: file.replace(/\.md$/, ""),
2137
+ description: descMatch?.[1]?.replace(/^["']|["']$/g, "") || ""
2138
+ });
2139
+ }
2140
+ return agents;
2141
+ }
2142
+ async function addFromRegistry2(agentName, hubDir, opts) {
2143
+ const repo = opts.repo || DEFAULT_REGISTRY_REPO3;
2144
+ const fileName = agentName.endsWith(".md") ? agentName : `${agentName}.md`;
2145
+ const rawUrl = `https://raw.githubusercontent.com/${repo}/${DEFAULT_BRANCH2}/agents/${fileName}`;
2146
+ console.log(chalk9.cyan(` Downloading ${agentName} from ${repo}...`));
2147
+ try {
2148
+ const res = await fetch(rawUrl);
2149
+ if (!res.ok) {
2150
+ console.log(chalk9.red(` Agent '${agentName}' not found in registry (${repo})`));
2151
+ console.log(chalk9.dim(" Run 'hub registry list' to see available agents."));
2152
+ return;
2153
+ }
2154
+ const content = await res.text();
2155
+ const targetBase = opts.global ? join10(process.env.HOME || "~", ".cursor", "agents") : join10(hubDir, "agents");
2156
+ await mkdir5(targetBase, { recursive: true });
2157
+ await writeFile8(join10(targetBase, fileName), content, "utf-8");
2158
+ console.log(chalk9.green(` Installed: ${agentName}`));
2159
+ console.log(chalk9.green(`
2160
+ 1 agent(s) installed to ${opts.global ? "global" : "project"}
2161
+ `));
2162
+ } catch (err) {
2163
+ console.log(chalk9.red(` Failed to download agent '${agentName}': ${err.message}`));
2164
+ }
2165
+ }
2166
+ async function addFromLocalPath2(localPath, hubDir, opts) {
2167
+ const absPath = resolve3(localPath);
2168
+ if (!existsSync7(absPath)) {
2169
+ console.log(chalk9.red(` Path not found: ${absPath}`));
2170
+ return;
2171
+ }
2172
+ const sourceAgentsDir = statSync2(absPath).isDirectory() ? join10(absPath, "agents") : absPath;
2173
+ await installAgentsFromDir(sourceAgentsDir, hubDir, opts);
2174
+ }
2175
+ async function installAgentsFromDir(sourceAgentsDir, hubDir, opts) {
2176
+ if (!existsSync7(sourceAgentsDir)) {
2177
+ console.log(chalk9.red(" No agents/ directory found in source"));
2178
+ return;
2179
+ }
2180
+ const files = await readdir3(sourceAgentsDir);
2181
+ const mdFiles = files.filter((f) => f.endsWith(".md"));
2182
+ if (mdFiles.length === 0) {
2183
+ console.log(chalk9.red(" No agent files found (looking for agents/*.md)"));
2184
+ return;
2185
+ }
2186
+ const toInstall = opts.agent ? mdFiles.filter((f) => f === `${opts.agent}.md` || f === opts.agent) : mdFiles;
2187
+ if (opts.agent && toInstall.length === 0) {
2188
+ const available = mdFiles.map((f) => f.replace(/\.md$/, "")).join(", ");
2189
+ console.log(chalk9.red(` Agent '${opts.agent}' not found. Available: ${available}`));
2190
+ return;
2191
+ }
2192
+ const targetBase = opts.global ? join10(process.env.HOME || "~", ".cursor", "agents") : join10(hubDir, "agents");
2193
+ await mkdir5(targetBase, { recursive: true });
2194
+ for (const file of toInstall) {
2195
+ await copyFile2(join10(sourceAgentsDir, file), join10(targetBase, file));
2196
+ console.log(chalk9.green(` Installed: ${file.replace(/\.md$/, "")}`));
2197
+ }
2198
+ console.log(
2199
+ chalk9.green(
2200
+ `
2201
+ ${toInstall.length} agent(s) installed to ${opts.global ? "global" : "project"}
2202
+ `
2203
+ )
2204
+ );
2205
+ }
2206
+ async function addFromGitRepo2(source, hubDir, opts) {
2207
+ const tmp = tmpDir2();
2208
+ try {
2209
+ console.log(chalk9.cyan(` Cloning ${source}...`));
2210
+ try {
2211
+ execSync5(`git clone --depth 1 ${source} ${tmp}`, {
2212
+ stdio: ["pipe", "pipe", "pipe"]
2213
+ });
2214
+ } catch {
2215
+ console.log(chalk9.red(` Repository not found or not accessible: ${source}`));
2216
+ return;
2217
+ }
2218
+ const sourceAgentsDir = join10(tmp, "agents");
2219
+ await installAgentsFromDir(sourceAgentsDir, hubDir, opts);
2220
+ } finally {
2221
+ if (existsSync7(tmp)) {
2222
+ await rm2(tmp, { recursive: true });
2223
+ }
2224
+ }
2225
+ }
2226
+ function isLocalPath2(source) {
2227
+ return source.startsWith("/") || source.startsWith("./") || source.startsWith("../") || source.startsWith("~");
2228
+ }
2229
+ function isRepoReference(source) {
2230
+ return source.startsWith("git@") || source.startsWith("https://") || source.includes("/");
2231
+ }
2232
+ var agentsCommand = new Command9("agents").description("Manage agent definitions").addCommand(
2233
+ new Command9("add").description("Install agents from the registry, a git repository, or local path").argument("<source>", "Agent name (from registry), GitHub shorthand (org/repo), git URL, or local path").option("-a, --agent <name>", "Install a specific agent only (for repo sources)").option("-g, --global", "Install to global ~/.cursor/agents/").option("-r, --repo <repo>", "Registry repository (owner/repo)").action(async (source, opts) => {
2234
+ const hubDir = process.cwd();
2235
+ console.log(chalk9.blue(`
2236
+ Installing agents from ${source}
2237
+ `));
2238
+ if (isLocalPath2(source)) {
2239
+ await addFromLocalPath2(source, hubDir, opts);
2240
+ } else if (isRepoReference(source)) {
2241
+ if (source.startsWith("git@") || source.startsWith("https://")) {
2242
+ await addFromGitRepo2(source, hubDir, opts);
2243
+ } else {
2244
+ const url = `https://github.com/${source}.git`;
2245
+ await addFromGitRepo2(url, hubDir, opts);
2246
+ }
2247
+ } else {
2248
+ await addFromRegistry2(source, hubDir, opts);
2249
+ }
2250
+ })
2251
+ ).addCommand(
2252
+ new Command9("list").description("List installed agents").action(async () => {
2253
+ const hubDir = process.cwd();
2254
+ console.log(chalk9.blue("\nInstalled agents\n"));
2255
+ const projectAgents = await listLocalAgents(join10(hubDir, "agents"));
2256
+ if (projectAgents.length > 0) {
2257
+ console.log(chalk9.cyan("Project:"));
2258
+ for (const a of projectAgents) {
2259
+ console.log(` ${chalk9.yellow(a.name)}${a.description ? ` \u2014 ${a.description}` : ""}`);
2260
+ }
2261
+ } else {
2262
+ console.log(chalk9.dim(" No project agents (agents/)"));
2263
+ }
2264
+ const globalDir = join10(process.env.HOME || "~", ".cursor", "agents");
2265
+ const globalAgents = await listLocalAgents(globalDir);
2266
+ console.log();
2267
+ if (globalAgents.length > 0) {
2268
+ console.log(chalk9.cyan("Global:"));
2269
+ for (const a of globalAgents) {
2270
+ console.log(` ${chalk9.yellow(a.name)}${a.description ? ` \u2014 ${a.description}` : ""}`);
2271
+ }
2272
+ } else {
2273
+ console.log(chalk9.dim(" No global agents (~/.cursor/agents/)"));
2274
+ }
2275
+ console.log();
2276
+ })
2277
+ ).addCommand(
2278
+ new Command9("remove").description("Remove an agent").argument("<name>", "Agent name to remove").option("-g, --global", "Remove from global agents").action(async (name, opts) => {
2279
+ const hubDir = process.cwd();
2280
+ const base = opts.global ? join10(process.env.HOME || "~", ".cursor", "agents") : join10(hubDir, "agents");
2281
+ const fileName = name.endsWith(".md") ? name : `${name}.md`;
2282
+ const target = join10(base, fileName);
2283
+ if (!existsSync7(target)) {
2284
+ console.log(chalk9.red(`
2285
+ Agent '${name}' not found in ${opts.global ? "global" : "project"}
2286
+ `));
2287
+ return;
2288
+ }
2289
+ await rm2(target);
2290
+ console.log(chalk9.green(`
2291
+ Removed agent: ${name}
2292
+ `));
2293
+ })
2294
+ );
2295
+
2296
+ // src/commands/hooks.ts
2297
+ import { Command as Command10 } from "commander";
2298
+ import { existsSync as existsSync8, statSync as statSync3 } from "fs";
2299
+ import { mkdir as mkdir6, readdir as readdir4, readFile as readFile7, rm as rm3, copyFile as copyFile3, cp as cp3 } from "fs/promises";
2300
+ import { join as join11, resolve as resolve4 } from "path";
2301
+ import { execSync as execSync6 } from "child_process";
2302
+ import chalk10 from "chalk";
2303
+ var DEFAULT_REGISTRY_REPO4 = process.env.HUB_REGISTRY || "arvoreeducacao/rhm";
2304
+ function tmpDir3() {
2305
+ return join11(process.env.TMPDIR || "/tmp", `hub-hooks-${Date.now()}`);
2306
+ }
2307
+ async function listLocalHooks(hooksDir) {
2308
+ const hooks = [];
2309
+ if (!existsSync8(hooksDir)) return hooks;
2310
+ const entries = await readdir4(hooksDir);
2311
+ for (const entry of entries) {
2312
+ const entryPath = join11(hooksDir, entry);
2313
+ const stat = statSync3(entryPath);
2314
+ if (stat.isFile() && entry.endsWith(".sh")) {
2315
+ hooks.push({ name: entry.replace(/\.sh$/, ""), description: "" });
2316
+ } else if (stat.isDirectory()) {
2317
+ const readmePath = join11(entryPath, "README.md");
2318
+ let description = "";
2319
+ if (existsSync8(readmePath)) {
2320
+ const content = await readFile7(readmePath, "utf-8");
2321
+ const firstLine = content.split("\n").find((l) => l.trim() && !l.startsWith("#"));
2322
+ description = firstLine?.trim() || "";
2323
+ }
2324
+ hooks.push({ name: entry, description });
2325
+ }
2326
+ }
2327
+ return hooks;
2328
+ }
2329
+ async function addFromRegistry3(hookName, hubDir, opts) {
2330
+ const repo = opts.repo || DEFAULT_REGISTRY_REPO4;
2331
+ const targetDir = join11(hubDir, "hooks", hookName);
2332
+ console.log(chalk10.cyan(` Downloading hook '${hookName}' from ${repo}...`));
2333
+ try {
2334
+ await downloadDirFromGitHub(repo, `hooks/${hookName}`, targetDir);
2335
+ console.log(chalk10.green(` Installed: ${hookName}`));
2336
+ console.log(chalk10.dim("\n Add the hook to hub.yaml to bind it to an event. Example:"));
2337
+ console.log(chalk10.dim(` hooks:`));
2338
+ console.log(chalk10.dim(` after_file_edit:`));
2339
+ console.log(chalk10.dim(` - type: command`));
2340
+ console.log(chalk10.dim(` command: "./hooks/${hookName}/hook.sh"
2341
+ `));
2342
+ } catch {
2343
+ console.log(chalk10.red(` Hook '${hookName}' not found in registry (${repo})`));
2344
+ console.log(chalk10.dim(" Run 'hub registry list --type hook' to see available hooks."));
2345
+ }
2346
+ }
2347
+ async function addFromLocalPath3(localPath, hubDir, opts) {
2348
+ const absPath = resolve4(localPath);
2349
+ if (!existsSync8(absPath)) {
2350
+ console.log(chalk10.red(` Path not found: ${absPath}`));
2351
+ return;
2352
+ }
2353
+ const stat = statSync3(absPath);
2354
+ const sourceHooksDir = stat.isDirectory() ? existsSync8(join11(absPath, "hooks")) ? join11(absPath, "hooks") : absPath : absPath;
2355
+ await installHooksFromDir(sourceHooksDir, hubDir, opts);
2356
+ }
2357
+ async function installHooksFromDir(sourceDir, hubDir, opts) {
2358
+ if (!existsSync8(sourceDir)) {
2359
+ console.log(chalk10.red(" No hooks directory found in source"));
2360
+ return;
2361
+ }
2362
+ const entries = await readdir4(sourceDir);
2363
+ const hookEntries = entries.filter((e) => {
2364
+ const p = join11(sourceDir, e);
2365
+ return e.endsWith(".sh") || statSync3(p).isDirectory();
2366
+ });
2367
+ if (hookEntries.length === 0) {
2368
+ console.log(chalk10.red(" No hook files found"));
2369
+ return;
2370
+ }
2371
+ const toInstall = opts.hook ? hookEntries.filter((e) => e === opts.hook || e === `${opts.hook}.sh` || e.replace(/\.sh$/, "") === opts.hook) : hookEntries;
2372
+ if (opts.hook && toInstall.length === 0) {
2373
+ const available = hookEntries.map((e) => e.replace(/\.sh$/, "")).join(", ");
2374
+ console.log(chalk10.red(` Hook '${opts.hook}' not found. Available: ${available}`));
2375
+ return;
2376
+ }
2377
+ const targetBase = join11(hubDir, "hooks");
2378
+ await mkdir6(targetBase, { recursive: true });
2379
+ for (const entry of toInstall) {
2380
+ const src = join11(sourceDir, entry);
2381
+ const stat = statSync3(src);
2382
+ if (stat.isDirectory()) {
2383
+ await cp3(src, join11(targetBase, entry), { recursive: true });
2384
+ } else {
2385
+ await copyFile3(src, join11(targetBase, entry));
2386
+ }
2387
+ console.log(chalk10.green(` Installed: ${entry.replace(/\.sh$/, "")}`));
2388
+ }
2389
+ console.log(chalk10.green(`
2390
+ ${toInstall.length} hook(s) installed
2391
+ `));
2392
+ }
2393
+ async function addFromGitRepo3(source, hubDir, opts) {
2394
+ const tmp = tmpDir3();
2395
+ try {
2396
+ console.log(chalk10.cyan(` Cloning ${source}...`));
2397
+ try {
2398
+ execSync6(`git clone --depth 1 ${source} ${tmp}`, {
2399
+ stdio: ["pipe", "pipe", "pipe"]
2400
+ });
2401
+ } catch {
2402
+ console.log(chalk10.red(` Repository not found or not accessible: ${source}`));
2403
+ return;
2404
+ }
2405
+ const sourceHooksDir = join11(tmp, "hooks");
2406
+ await installHooksFromDir(sourceHooksDir, hubDir, opts);
2407
+ } finally {
2408
+ if (existsSync8(tmp)) {
2409
+ await rm3(tmp, { recursive: true });
2410
+ }
2411
+ }
2412
+ }
2413
+ function isLocalPath3(source) {
2414
+ return source.startsWith("/") || source.startsWith("./") || source.startsWith("../") || source.startsWith("~");
2415
+ }
2416
+ function isRepoReference2(source) {
2417
+ return source.startsWith("git@") || source.startsWith("https://") || source.includes("/");
2418
+ }
2419
+ var hooksCommand = new Command10("hooks").description("Manage editor hooks").addCommand(
2420
+ new Command10("add").description("Install hooks from the registry, a git repository, or local path").argument("<source>", "Hook name (from registry), GitHub shorthand (org/repo), git URL, or local path").option("--hook <name>", "Install a specific hook only (for repo sources)").option("-r, --repo <repo>", "Registry repository (owner/repo)").action(async (source, opts) => {
2421
+ const hubDir = process.cwd();
2422
+ console.log(chalk10.blue(`
2423
+ Installing hooks from ${source}
2424
+ `));
2425
+ if (isLocalPath3(source)) {
2426
+ await addFromLocalPath3(source, hubDir, opts);
2427
+ } else if (isRepoReference2(source)) {
2428
+ if (source.startsWith("git@") || source.startsWith("https://")) {
2429
+ await addFromGitRepo3(source, hubDir, opts);
2430
+ } else {
2431
+ const url = `https://github.com/${source}.git`;
2432
+ await addFromGitRepo3(url, hubDir, opts);
2433
+ }
2434
+ } else {
2435
+ await addFromRegistry3(source, hubDir, opts);
2436
+ }
2437
+ })
2438
+ ).addCommand(
2439
+ new Command10("list").description("List installed hooks").action(async () => {
2440
+ const hubDir = process.cwd();
2441
+ const hooksDir = join11(hubDir, "hooks");
2442
+ console.log(chalk10.blue("\nInstalled hooks\n"));
2443
+ const hooks = await listLocalHooks(hooksDir);
2444
+ if (hooks.length > 0) {
2445
+ for (const h of hooks) {
2446
+ console.log(` ${chalk10.yellow(h.name)}${h.description ? ` \u2014 ${h.description}` : ""}`);
2447
+ }
2448
+ } else {
2449
+ console.log(chalk10.dim(" No hooks installed (hooks/)"));
2450
+ }
2451
+ console.log();
2452
+ })
2453
+ ).addCommand(
2454
+ new Command10("remove").description("Remove a hook").argument("<name>", "Hook name to remove").action(async (name) => {
2455
+ const hubDir = process.cwd();
2456
+ const hooksDir = join11(hubDir, "hooks");
2457
+ const shFile = join11(hooksDir, `${name}.sh`);
2458
+ const dirPath = join11(hooksDir, name);
2459
+ if (existsSync8(dirPath) && statSync3(dirPath).isDirectory()) {
2460
+ await rm3(dirPath, { recursive: true });
2461
+ console.log(chalk10.green(`
2462
+ Removed hook: ${name}
2463
+ `));
2464
+ } else if (existsSync8(shFile)) {
2465
+ await rm3(shFile);
2466
+ console.log(chalk10.green(`
2467
+ Removed hook: ${name}
2468
+ `));
2469
+ } else {
2470
+ console.log(chalk10.red(`
2471
+ Hook '${name}' not found in hooks/
2472
+ `));
2473
+ }
2474
+ })
2475
+ );
2476
+
2477
+ // src/commands/commands.ts
2478
+ import { Command as Command11 } from "commander";
2479
+ import { existsSync as existsSync9, statSync as statSync4 } from "fs";
2480
+ import { mkdir as mkdir7, readdir as readdir5, readFile as readFile8, rm as rm4, copyFile as copyFile4, writeFile as writeFile9 } from "fs/promises";
2481
+ import { join as join12, resolve as resolve5 } from "path";
2482
+ import { execSync as execSync7 } from "child_process";
2483
+ import chalk11 from "chalk";
2484
+ var DEFAULT_REGISTRY_REPO5 = process.env.HUB_REGISTRY || "arvoreeducacao/rhm";
2485
+ var DEFAULT_BRANCH3 = "main";
2486
+ function tmpDir4() {
2487
+ return join12(process.env.TMPDIR || "/tmp", `hub-commands-${Date.now()}`);
2488
+ }
2489
+ async function listLocalCommands(commandsDir) {
2490
+ const commands = [];
2491
+ if (!existsSync9(commandsDir)) return commands;
2492
+ const files = await readdir5(commandsDir);
2493
+ for (const file of files) {
2494
+ if (!file.endsWith(".md")) continue;
2495
+ const content = await readFile8(join12(commandsDir, file), "utf-8");
2496
+ const firstLine = content.split("\n").find((l) => l.trim() && !l.startsWith("#"));
2497
+ commands.push({
2498
+ name: file.replace(/\.md$/, ""),
2499
+ description: firstLine?.trim() || ""
2500
+ });
2501
+ }
2502
+ return commands;
2503
+ }
2504
+ async function addFromRegistry4(commandName, hubDir, opts) {
2505
+ const repo = opts.repo || DEFAULT_REGISTRY_REPO5;
2506
+ const fileName = commandName.endsWith(".md") ? commandName : `${commandName}.md`;
2507
+ const rawUrl = `https://raw.githubusercontent.com/${repo}/${DEFAULT_BRANCH3}/commands/${fileName}`;
2508
+ console.log(chalk11.cyan(` Downloading command '${commandName}' from ${repo}...`));
2509
+ try {
2510
+ const res = await fetch(rawUrl);
2511
+ if (!res.ok) {
2512
+ console.log(chalk11.red(` Command '${commandName}' not found in registry (${repo})`));
2513
+ console.log(chalk11.dim(" Run 'hub registry list --type command' to see available commands."));
2514
+ return;
2515
+ }
2516
+ const content = await res.text();
2517
+ const targetDir = join12(hubDir, "commands");
2518
+ await mkdir7(targetDir, { recursive: true });
2519
+ await writeFile9(join12(targetDir, fileName), content, "utf-8");
2520
+ console.log(chalk11.green(` Installed: ${commandName}`));
2521
+ console.log(chalk11.dim(`
2522
+ Use it in Cursor with /${commandName}`));
2523
+ console.log(chalk11.dim(` Make sure hub.yaml has: commands_dir: ./commands
2524
+ `));
2525
+ } catch (err) {
2526
+ console.log(chalk11.red(` Failed to download command '${commandName}': ${err.message}`));
2527
+ }
2528
+ }
2529
+ async function addFromLocalPath4(localPath, hubDir, opts) {
2530
+ const absPath = resolve5(localPath);
2531
+ if (!existsSync9(absPath)) {
2532
+ console.log(chalk11.red(` Path not found: ${absPath}`));
2533
+ return;
2534
+ }
2535
+ const stat = statSync4(absPath);
2536
+ if (stat.isFile() && absPath.endsWith(".md")) {
2537
+ const targetDir = join12(hubDir, "commands");
2538
+ await mkdir7(targetDir, { recursive: true });
2539
+ const fileName = absPath.split("/").pop();
2540
+ await copyFile4(absPath, join12(targetDir, fileName));
2541
+ console.log(chalk11.green(` Installed: ${fileName.replace(/\.md$/, "")}`));
2542
+ console.log(chalk11.green(`
2543
+ 1 command(s) installed
2544
+ `));
2545
+ return;
2546
+ }
2547
+ const sourceDir = stat.isDirectory() ? existsSync9(join12(absPath, "commands")) ? join12(absPath, "commands") : absPath : absPath;
2548
+ await installCommandsFromDir(sourceDir, hubDir, opts);
2549
+ }
2550
+ async function installCommandsFromDir(sourceDir, hubDir, opts) {
2551
+ if (!existsSync9(sourceDir)) {
2552
+ console.log(chalk11.red(" No commands directory found in source"));
2553
+ return;
2554
+ }
2555
+ const files = await readdir5(sourceDir);
2556
+ const mdFiles = files.filter((f) => f.endsWith(".md"));
2557
+ if (mdFiles.length === 0) {
2558
+ console.log(chalk11.red(" No command files found (looking for *.md)"));
2559
+ return;
2560
+ }
2561
+ const toInstall = opts.command ? mdFiles.filter((f) => f === `${opts.command}.md` || f === opts.command) : mdFiles;
2562
+ if (opts.command && toInstall.length === 0) {
2563
+ const available = mdFiles.map((f) => f.replace(/\.md$/, "")).join(", ");
2564
+ console.log(chalk11.red(` Command '${opts.command}' not found. Available: ${available}`));
2565
+ return;
2566
+ }
2567
+ const targetDir = join12(hubDir, "commands");
2568
+ await mkdir7(targetDir, { recursive: true });
2569
+ for (const file of toInstall) {
2570
+ await copyFile4(join12(sourceDir, file), join12(targetDir, file));
2571
+ console.log(chalk11.green(` Installed: ${file.replace(/\.md$/, "")}`));
2572
+ }
2573
+ console.log(chalk11.green(`
2574
+ ${toInstall.length} command(s) installed
2575
+ `));
2576
+ }
2577
+ async function addFromGitRepo4(source, hubDir, opts) {
2578
+ const tmp = tmpDir4();
2579
+ try {
2580
+ console.log(chalk11.cyan(` Cloning ${source}...`));
2581
+ try {
2582
+ execSync7(`git clone --depth 1 ${source} ${tmp}`, {
2583
+ stdio: ["pipe", "pipe", "pipe"]
2584
+ });
2585
+ } catch {
2586
+ console.log(chalk11.red(` Repository not found or not accessible: ${source}`));
2587
+ return;
2588
+ }
2589
+ const sourceCommandsDir = join12(tmp, "commands");
2590
+ await installCommandsFromDir(sourceCommandsDir, hubDir, opts);
2591
+ } finally {
2592
+ if (existsSync9(tmp)) {
2593
+ await rm4(tmp, { recursive: true });
2594
+ }
2595
+ }
2596
+ }
2597
+ function isLocalPath4(source) {
2598
+ return source.startsWith("/") || source.startsWith("./") || source.startsWith("../") || source.startsWith("~");
2599
+ }
2600
+ function isRepoReference3(source) {
2601
+ return source.startsWith("git@") || source.startsWith("https://") || source.includes("/");
2602
+ }
2603
+ var commandsCommand = new Command11("commands").description("Manage slash commands (Cursor)").addCommand(
2604
+ new Command11("add").description("Install commands from the registry, a git repository, or local path").argument("<source>", "Command name (from registry), GitHub shorthand (org/repo), git URL, or local path").option("-c, --command <name>", "Install a specific command only (for repo sources)").option("-r, --repo <repo>", "Registry repository (owner/repo)").action(async (source, opts) => {
2605
+ const hubDir = process.cwd();
2606
+ console.log(chalk11.blue(`
2607
+ Installing commands from ${source}
2608
+ `));
2609
+ if (isLocalPath4(source)) {
2610
+ await addFromLocalPath4(source, hubDir, opts);
2611
+ } else if (isRepoReference3(source)) {
2612
+ if (source.startsWith("git@") || source.startsWith("https://")) {
2613
+ await addFromGitRepo4(source, hubDir, opts);
2614
+ } else {
2615
+ const url = `https://github.com/${source}.git`;
2616
+ await addFromGitRepo4(url, hubDir, opts);
2617
+ }
2618
+ } else {
2619
+ await addFromRegistry4(source, hubDir, opts);
2620
+ }
2621
+ })
2622
+ ).addCommand(
2623
+ new Command11("list").description("List installed commands").action(async () => {
2624
+ const hubDir = process.cwd();
2625
+ const commandsDir = join12(hubDir, "commands");
2626
+ console.log(chalk11.blue("\nInstalled commands\n"));
2627
+ const commands = await listLocalCommands(commandsDir);
2628
+ if (commands.length > 0) {
2629
+ for (const c of commands) {
2630
+ console.log(` ${chalk11.yellow(`/${c.name}`)}${c.description ? ` \u2014 ${c.description}` : ""}`);
2631
+ }
2632
+ } else {
2633
+ console.log(chalk11.dim(" No commands installed (commands/)"));
2634
+ }
2635
+ console.log();
2636
+ })
2637
+ ).addCommand(
2638
+ new Command11("remove").description("Remove a command").argument("<name>", "Command name to remove").action(async (name) => {
2639
+ const hubDir = process.cwd();
2640
+ const commandsDir = join12(hubDir, "commands");
2641
+ const fileName = name.endsWith(".md") ? name : `${name}.md`;
2642
+ const target = join12(commandsDir, fileName);
2643
+ if (!existsSync9(target)) {
2644
+ console.log(chalk11.red(`
2645
+ Command '${name}' not found in commands/
2646
+ `));
2647
+ return;
2648
+ }
2649
+ await rm4(target);
2650
+ console.log(chalk11.green(`
2651
+ Removed command: ${name}
2652
+ `));
2653
+ })
2654
+ );
2655
+
2656
+ // src/commands/repos.ts
2657
+ import { Command as Command12 } from "commander";
2658
+ import { existsSync as existsSync10 } from "fs";
2659
+ import { join as join13 } from "path";
2660
+ import { execSync as execSync8 } from "child_process";
2661
+ import chalk12 from "chalk";
2662
+ var pullCommand = new Command12("pull").description("Pull latest changes in all repositories").action(async () => {
2663
+ const hubDir = process.cwd();
2664
+ const config = await loadHubConfig(hubDir);
2665
+ console.log(chalk12.blue("\n\u2501\u2501\u2501 Pulling latest changes \u2501\u2501\u2501\n"));
2666
+ for (const repo of config.repos) {
2667
+ const fullPath = join13(hubDir, repo.path);
2668
+ if (!existsSync10(fullPath)) {
2669
+ console.log(chalk12.red(` ${repo.name}: not cloned`));
2670
+ continue;
2671
+ }
2672
+ console.log(chalk12.yellow(`\u25B8 ${repo.name}`));
2673
+ try {
2674
+ execSync8("git pull --rebase", { cwd: fullPath, stdio: "inherit" });
2675
+ console.log(chalk12.green(" Updated"));
2676
+ } catch {
2677
+ console.log(chalk12.red(" Failed to pull"));
2678
+ }
2679
+ }
2680
+ console.log();
2681
+ });
2682
+ var statusCommand = new Command12("status").description("Show git status for all repositories").action(async () => {
2683
+ const hubDir = process.cwd();
2684
+ const config = await loadHubConfig(hubDir);
2685
+ console.log(chalk12.blue("\n\u2501\u2501\u2501 Git status \u2501\u2501\u2501\n"));
2686
+ for (const repo of config.repos) {
2687
+ const fullPath = join13(hubDir, repo.path);
2688
+ if (!existsSync10(fullPath)) {
2689
+ console.log(chalk12.red(` ${repo.name}: not cloned`));
2690
+ continue;
2691
+ }
2692
+ console.log(chalk12.yellow(`\u25B8 ${repo.name}`));
2693
+ try {
2694
+ const branch = execSync8("git branch --show-current", {
2695
+ cwd: fullPath,
2696
+ encoding: "utf-8",
2697
+ stdio: ["pipe", "pipe", "pipe"]
2698
+ }).trim();
2699
+ const changes = execSync8("git status --porcelain", {
2700
+ cwd: fullPath,
2701
+ encoding: "utf-8",
2702
+ stdio: ["pipe", "pipe", "pipe"]
2703
+ }).trim().split("\n").filter(Boolean).length;
2704
+ let ahead = "0";
2705
+ let behind = "0";
2706
+ try {
2707
+ ahead = execSync8("git rev-list --count @{u}..HEAD", {
2708
+ cwd: fullPath,
2709
+ encoding: "utf-8",
2710
+ stdio: ["pipe", "pipe", "pipe"]
2711
+ }).trim();
2712
+ behind = execSync8("git rev-list --count HEAD..@{u}", {
2713
+ cwd: fullPath,
2714
+ encoding: "utf-8",
2715
+ stdio: ["pipe", "pipe", "pipe"]
2716
+ }).trim();
2717
+ } catch {
2718
+ }
2719
+ console.log(` Branch: ${branch}`);
2720
+ console.log(` Changes: ${changes} file(s)`);
2721
+ console.log(` Ahead: ${ahead} | Behind: ${behind}`);
2722
+ } catch {
2723
+ console.log(chalk12.red(" Failed to get status"));
2724
+ }
2725
+ console.log();
2726
+ }
2727
+ });
2728
+ var execCommand = new Command12("exec").description("Execute a command in all repositories").argument("<cmd...>", "Command to execute").passThroughOptions().allowUnknownOption().action(async (cmd) => {
2729
+ const hubDir = process.cwd();
2730
+ const config = await loadHubConfig(hubDir);
2731
+ const command = cmd.join(" ");
2732
+ console.log(chalk12.blue(`
2733
+ \u2501\u2501\u2501 Executing: ${command} \u2501\u2501\u2501
2734
+ `));
2735
+ for (const repo of config.repos) {
2736
+ const fullPath = join13(hubDir, repo.path);
2737
+ if (!existsSync10(fullPath)) {
2738
+ console.log(chalk12.red(` ${repo.name}: not cloned`));
2739
+ continue;
2740
+ }
2741
+ console.log(chalk12.yellow(`\u25B8 ${repo.name}`));
2742
+ try {
2743
+ execSync8(command, { cwd: fullPath, stdio: "inherit" });
2744
+ } catch {
2745
+ console.log(chalk12.red(` Command failed in ${repo.name}`));
2746
+ }
2747
+ }
2748
+ console.log();
2749
+ });
2750
+
2751
+ // src/commands/worktree.ts
2752
+ import { Command as Command13 } from "commander";
2753
+ import { existsSync as existsSync11 } from "fs";
2754
+ import { cp as cp4, mkdir as mkdir8 } from "fs/promises";
2755
+ import { join as join14 } from "path";
2756
+ import { execSync as execSync9 } from "child_process";
2757
+ import chalk13 from "chalk";
2758
+ function getWorktreeBase() {
2759
+ return join14(process.env.HOME || "~", ".cursor", "worktrees", "repo-hub");
2760
+ }
2761
+ function isGitRepo(dir) {
2762
+ try {
2763
+ execSync9("git rev-parse --is-inside-work-tree", {
2764
+ cwd: dir,
2765
+ stdio: ["pipe", "pipe", "pipe"]
2766
+ });
2767
+ return true;
2768
+ } catch {
2769
+ return false;
2770
+ }
2771
+ }
2772
+ var worktreeCommand = new Command13("worktree").description("Manage git worktrees for parallel work").addCommand(
2773
+ new Command13("add").description("Create a new worktree with environment files copied").argument("<name>", "Worktree name").action(async (name) => {
2774
+ const hubDir = process.cwd();
2775
+ if (!isGitRepo(hubDir)) {
2776
+ console.log(chalk13.red("\nThis directory is not a git repository."));
2777
+ console.log(chalk13.dim("Run 'git init' first or use worktrees from a repo directory.\n"));
2778
+ return;
2779
+ }
2780
+ const config = await loadHubConfig(hubDir);
2781
+ const worktreeBase = getWorktreeBase();
2782
+ const worktreePath = join14(worktreeBase, name);
2783
+ console.log(chalk13.blue(`
2784
+ \u2501\u2501\u2501 Creating worktree: ${name} \u2501\u2501\u2501
2785
+ `));
2786
+ await mkdir8(worktreeBase, { recursive: true });
2787
+ console.log(chalk13.cyan(" Creating git worktree..."));
2788
+ try {
2789
+ execSync9(`git worktree add "${worktreePath}" --detach`, {
2790
+ cwd: hubDir,
2791
+ stdio: "inherit"
2792
+ });
2793
+ } catch {
2794
+ console.log(chalk13.red(" Failed to create worktree. Make sure you have commits in this repo.\n"));
2795
+ return;
2796
+ }
2797
+ console.log(chalk13.cyan(" Copying environment files..."));
2798
+ for (const repo of config.repos) {
2799
+ if (!repo.env_file) continue;
2800
+ const srcEnv = join14(hubDir, repo.path, repo.env_file);
2801
+ if (!existsSync11(srcEnv)) continue;
2802
+ const destDir = join14(worktreePath, repo.path);
2803
+ if (existsSync11(destDir)) {
2804
+ const destEnv = join14(destDir, repo.env_file);
2805
+ try {
2806
+ await cp4(srcEnv, destEnv);
2807
+ console.log(chalk13.green(` ${repo.name}: Copied ${repo.env_file}`));
2808
+ } catch {
2809
+ console.log(chalk13.dim(` ${repo.name}: Could not copy env file`));
2810
+ }
2811
+ }
2812
+ }
2813
+ console.log(chalk13.green(`
2814
+ Worktree created at: ${worktreePath}`));
2815
+ console.log(chalk13.cyan(`Open in Cursor: cursor ${worktreePath}
2816
+ `));
2817
+ })
2818
+ ).addCommand(
2819
+ new Command13("list").description("List all worktrees").action(async () => {
2820
+ const hubDir = process.cwd();
2821
+ if (!isGitRepo(hubDir)) {
2822
+ console.log(chalk13.red("\nThis directory is not a git repository."));
2823
+ console.log(chalk13.dim("Worktrees require a git repository.\n"));
2824
+ return;
2825
+ }
2826
+ console.log(chalk13.blue("\n\u2501\u2501\u2501 Git Worktrees \u2501\u2501\u2501\n"));
2827
+ execSync9("git worktree list", { cwd: hubDir, stdio: "inherit" });
2828
+ console.log();
2829
+ })
2830
+ ).addCommand(
2831
+ new Command13("remove").description("Remove a worktree").argument("<name>", "Worktree name").action(async (name) => {
2832
+ const hubDir = process.cwd();
2833
+ if (!isGitRepo(hubDir)) {
2834
+ console.log(chalk13.red("\nThis directory is not a git repository."));
2835
+ console.log(chalk13.dim("Worktrees require a git repository.\n"));
2836
+ return;
2837
+ }
2838
+ const worktreePath = join14(getWorktreeBase(), name);
2839
+ console.log(chalk13.blue(`
2840
+ \u2501\u2501\u2501 Removing worktree: ${name} \u2501\u2501\u2501
2841
+ `));
2842
+ try {
2843
+ execSync9(`git worktree remove "${worktreePath}" --force`, {
2844
+ cwd: hubDir,
2845
+ stdio: "inherit"
2846
+ });
2847
+ console.log(chalk13.green(" Worktree removed\n"));
2848
+ } catch {
2849
+ console.log(chalk13.red(` Failed to remove worktree '${name}'.
2850
+ `));
2851
+ }
2852
+ })
2853
+ ).addCommand(
2854
+ new Command13("copy-envs").description("Copy environment files to a worktree").argument("[name]", "Worktree name (copies to that worktree)").action(async (name) => {
2855
+ const hubDir = process.cwd();
2856
+ const config = await loadHubConfig(hubDir);
2857
+ const targetDir = name ? join14(getWorktreeBase(), name) : hubDir;
2858
+ if (!existsSync11(targetDir)) {
2859
+ console.log(chalk13.red(`
2860
+ Worktree '${name}' not found at ${targetDir}
2861
+ `));
2862
+ return;
2863
+ }
2864
+ console.log(chalk13.blue("\n\u2501\u2501\u2501 Copying environment files \u2501\u2501\u2501\n"));
2865
+ console.log(chalk13.cyan(` Source: ${hubDir}`));
2866
+ console.log(chalk13.cyan(` Target: ${targetDir}
2867
+ `));
2868
+ for (const repo of config.repos) {
2869
+ if (!repo.env_file) continue;
2870
+ const srcEnv = join14(hubDir, repo.path, repo.env_file);
2871
+ if (!existsSync11(srcEnv)) continue;
2872
+ const destDir = join14(targetDir, repo.path);
2873
+ if (!existsSync11(destDir)) continue;
2874
+ const destEnv = join14(destDir, repo.env_file);
2875
+ try {
2876
+ await cp4(srcEnv, destEnv);
2877
+ console.log(chalk13.green(` ${repo.name}: Copied ${repo.env_file}`));
2878
+ } catch {
2879
+ console.log(chalk13.red(` ${repo.name}: Failed to copy`));
2880
+ }
2881
+ }
2882
+ console.log();
2883
+ })
2884
+ );
2885
+
2886
+ // src/commands/doctor.ts
2887
+ import { Command as Command14 } from "commander";
2888
+ import { existsSync as existsSync12 } from "fs";
2889
+ import { join as join15 } from "path";
2890
+ import { execSync as execSync10 } from "child_process";
2891
+ import chalk14 from "chalk";
2892
+ var CHECKS = [
2893
+ { name: "git", command: "git", versionFlag: "--version", required: true },
2894
+ { name: "docker", command: "docker", versionFlag: "--version", required: true },
2895
+ { name: "node", command: "node", versionFlag: "--version", required: true },
2896
+ { name: "pnpm", command: "pnpm", versionFlag: "--version", required: false },
2897
+ { name: "mise", command: "mise", versionFlag: "--version", required: false },
2898
+ { name: "gh", command: "gh", versionFlag: "--version", required: false },
2899
+ { name: "aws", command: "aws", versionFlag: "--version", required: false }
2900
+ ];
2901
+ function checkCommand(check) {
2902
+ try {
2903
+ const output = execSync10(`${check.command} ${check.versionFlag || "--version"}`, {
2904
+ stdio: ["pipe", "pipe", "pipe"],
2905
+ encoding: "utf-8"
2906
+ }).trim();
2907
+ const version = output.split("\n")[0];
2908
+ return { found: true, version };
2909
+ } catch {
2910
+ return { found: false };
2911
+ }
2912
+ }
2913
+ function getToolVersion(tool) {
2914
+ const versionFlags = {
2915
+ node: "--version",
2916
+ pnpm: "--version",
2917
+ yarn: "--version",
2918
+ erlang: "+V",
2919
+ elixir: "--version",
2920
+ ruby: "--version",
2921
+ python: "--version",
2922
+ go: "version",
2923
+ rust: "--version",
2924
+ direnv: "--version"
2925
+ };
2926
+ const flag = versionFlags[tool] || "--version";
2927
+ try {
2928
+ const cmd = tool === "erlang" ? "erl" : tool;
2929
+ const output = execSync10(`${cmd} ${flag}`, {
2930
+ stdio: ["pipe", "pipe", "pipe"],
2931
+ encoding: "utf-8"
2932
+ }).trim();
2933
+ return output.split("\n")[0];
2934
+ } catch {
2935
+ return null;
2936
+ }
2937
+ }
2938
+ function versionMatches(actual, expected) {
2939
+ const extractVersion2 = (s) => {
2940
+ const match = s.match(/(\d+\.\d+[.\d]*)/);
2941
+ return match?.[1] || s;
2942
+ };
2943
+ const actualClean = extractVersion2(actual);
2944
+ return actualClean.startsWith(expected) || expected.startsWith(actualClean);
2945
+ }
2946
+ var doctorCommand = new Command14("doctor").description("Check required dependencies and tool versions from hub.yaml").action(async () => {
2947
+ const hubDir = process.cwd();
2948
+ let config;
2949
+ try {
2950
+ config = await loadHubConfig(hubDir);
2951
+ } catch {
2952
+ config = null;
2953
+ }
2954
+ console.log(chalk14.blue("\nChecking dependencies\n"));
2955
+ let allOk = true;
2956
+ const required = CHECKS.filter((c) => c.required);
2957
+ const recommended = CHECKS.filter((c) => !c.required);
2958
+ console.log(chalk14.cyan("Required:"));
2959
+ for (const check of required) {
2960
+ const result = checkCommand(check);
2961
+ if (result.found) {
2962
+ console.log(chalk14.green(` \u2713 ${check.name}: ${result.version}`));
2963
+ } else {
2964
+ console.log(chalk14.red(` \u2717 ${check.name}: not found`));
2965
+ allOk = false;
2966
+ }
2967
+ }
2968
+ console.log();
2969
+ console.log(chalk14.cyan("Recommended:"));
2970
+ for (const check of recommended) {
2971
+ const result = checkCommand(check);
2972
+ if (result.found) {
2973
+ console.log(chalk14.green(` \u2713 ${check.name}: ${result.version}`));
2974
+ } else {
2975
+ console.log(chalk14.dim(` - ${check.name}: not found`));
2976
+ }
2977
+ }
2978
+ if (config?.tools && Object.keys(config.tools).length > 0) {
2979
+ console.log();
2980
+ console.log(chalk14.cyan("Hub tools (from hub.yaml):"));
2981
+ for (const [tool, expected] of Object.entries(config.tools)) {
2982
+ const actual = getToolVersion(tool);
2983
+ if (!actual) {
2984
+ console.log(chalk14.red(` \u2717 ${tool}: not found (expected ${expected})`));
2985
+ allOk = false;
2986
+ } else if (versionMatches(actual, expected)) {
2987
+ console.log(chalk14.green(` \u2713 ${tool}: ${actual} (expected ${expected})`));
2988
+ } else {
2989
+ console.log(chalk14.yellow(` \u26A0 ${tool}: ${actual} (expected ${expected})`));
2990
+ }
2991
+ }
2992
+ }
2993
+ if (config?.repos) {
2994
+ const reposWithTools = config.repos.filter(
2995
+ (r) => r.tools && Object.keys(r.tools).length > 0
2996
+ );
2997
+ if (reposWithTools.length > 0) {
2998
+ console.log();
2999
+ console.log(chalk14.cyan("Repo-specific tools:"));
3000
+ for (const repo of reposWithTools) {
3001
+ const repoDir = join15(hubDir, repo.path);
3002
+ if (!existsSync12(repoDir)) {
3003
+ console.log(chalk14.dim(` ${repo.name}: not cloned, skipping`));
3004
+ continue;
3005
+ }
3006
+ console.log(chalk14.yellow(` \u25B8 ${repo.name}`));
3007
+ for (const [tool, expected] of Object.entries(repo.tools)) {
3008
+ const actual = getToolVersion(tool);
3009
+ if (!actual) {
3010
+ console.log(chalk14.red(` \u2717 ${tool}: not found (expected ${expected})`));
3011
+ } else if (versionMatches(actual, expected)) {
3012
+ console.log(chalk14.green(` \u2713 ${tool}: ${actual} (expected ${expected})`));
3013
+ } else {
3014
+ console.log(chalk14.yellow(` \u26A0 ${tool}: ${actual} (expected ${expected})`));
3015
+ }
3016
+ }
3017
+ }
3018
+ }
3019
+ }
3020
+ console.log();
3021
+ if (allOk) {
3022
+ console.log(chalk14.green("All checks passed!\n"));
3023
+ } else {
3024
+ console.log(chalk14.red("Some checks failed.\n"));
3025
+ if (config?.tools) {
3026
+ console.log(chalk14.cyan("Fix with: hub tools install\n"));
3027
+ }
3028
+ process.exit(1);
3029
+ }
3030
+ });
3031
+
3032
+ // src/commands/tools.ts
3033
+ import { Command as Command15 } from "commander";
3034
+ import { existsSync as existsSync13 } from "fs";
3035
+ import { writeFile as writeFile10 } from "fs/promises";
3036
+ import { join as join16 } from "path";
3037
+ import { execSync as execSync11 } from "child_process";
3038
+ import chalk15 from "chalk";
3039
+ function hasMise2() {
3040
+ try {
3041
+ execSync11("mise --version", { stdio: ["pipe", "pipe", "pipe"] });
3042
+ return true;
3043
+ } catch {
3044
+ return false;
3045
+ }
3046
+ }
3047
+ function generateMiseToml2(tools, settings) {
3048
+ const lines = [];
3049
+ lines.push("[tools]");
3050
+ for (const [tool, version] of Object.entries(tools)) {
3051
+ lines.push(`${tool} = "${version}"`);
3052
+ }
3053
+ if (settings && Object.keys(settings).length > 0) {
3054
+ lines.push("");
3055
+ lines.push("[settings]");
3056
+ for (const [key, value] of Object.entries(settings)) {
3057
+ if (typeof value === "boolean") {
3058
+ lines.push(`${key} = ${value}`);
3059
+ } else if (typeof value === "string") {
3060
+ lines.push(`${key} = "${value}"`);
3061
+ } else if (typeof value === "number") {
3062
+ lines.push(`${key} = ${value}`);
3063
+ }
3064
+ }
3065
+ }
3066
+ return lines.join("\n") + "\n";
3067
+ }
3068
+ function mergeTools(global, local) {
3069
+ return { ...global, ...local };
3070
+ }
3071
+ async function generateMiseFiles(config, hubDir) {
3072
+ let count = 0;
3073
+ if (config.tools && Object.keys(config.tools).length > 0) {
3074
+ const content = generateMiseToml2(config.tools, config.mise_settings);
3075
+ await writeFile10(join16(hubDir, ".mise.toml"), content, "utf-8");
3076
+ console.log(chalk15.green(` Generated .mise.toml (${Object.keys(config.tools).length} tools)`));
3077
+ count++;
3078
+ }
3079
+ for (const repo of config.repos) {
3080
+ if (!repo.tools || Object.keys(repo.tools).length === 0) continue;
3081
+ const repoDir = join16(hubDir, repo.path);
3082
+ if (!existsSync13(repoDir)) {
3083
+ console.log(chalk15.dim(` ${repo.name}: not cloned, skipping`));
3084
+ continue;
3085
+ }
3086
+ const merged = mergeTools(config.tools || {}, repo.tools);
3087
+ const content = generateMiseToml2(merged);
3088
+ await writeFile10(join16(repoDir, ".mise.toml"), content, "utf-8");
3089
+ console.log(chalk15.green(` ${repo.name}: Generated .mise.toml (${Object.keys(merged).length} tools)`));
3090
+ count++;
3091
+ }
3092
+ return count;
3093
+ }
3094
+ var toolsCommand = new Command15("tools").description("Manage development tool versions via mise").addCommand(
3095
+ new Command15("install").description("Install all tools defined in hub.yaml using mise").option("--generate", "Generate .mise.toml files before installing").action(async (opts) => {
3096
+ const hubDir = process.cwd();
3097
+ const config = await loadHubConfig(hubDir);
3098
+ if (!hasMise2()) {
3099
+ console.log(chalk15.red("\nmise is not installed."));
3100
+ console.log(chalk15.cyan("Install with: curl https://mise.run | sh"));
3101
+ console.log(chalk15.cyan("Or: brew install mise\n"));
3102
+ return;
3103
+ }
3104
+ if (!config.tools && !config.repos.some((r) => r.tools)) {
3105
+ console.log(chalk15.yellow("\nNo tools defined in hub.yaml\n"));
3106
+ return;
3107
+ }
3108
+ console.log(chalk15.blue("\n\u2501\u2501\u2501 Installing tools \u2501\u2501\u2501\n"));
3109
+ if (opts.generate) {
3110
+ await generateMiseFiles(config, hubDir);
3111
+ console.log();
3112
+ }
3113
+ if (config.tools && Object.keys(config.tools).length > 0) {
3114
+ console.log(chalk15.cyan("Installing hub-level tools..."));
3115
+ try {
3116
+ execSync11("mise trust && mise install", {
3117
+ cwd: hubDir,
3118
+ stdio: "inherit"
3119
+ });
3120
+ console.log(chalk15.green(" Hub tools installed\n"));
3121
+ } catch {
3122
+ console.log(chalk15.red(" Failed to install hub tools\n"));
3123
+ }
3124
+ }
3125
+ for (const repo of config.repos) {
3126
+ if (!repo.tools || Object.keys(repo.tools).length === 0) continue;
3127
+ const repoDir = join16(hubDir, repo.path);
3128
+ if (!existsSync13(repoDir)) {
3129
+ console.log(chalk15.dim(` ${repo.name}: not cloned, skipping`));
3130
+ continue;
3131
+ }
3132
+ console.log(chalk15.yellow(`\u25B8 ${repo.name}`));
3133
+ try {
3134
+ execSync11("mise trust 2>/dev/null; mise install", {
3135
+ cwd: repoDir,
3136
+ stdio: "inherit"
3137
+ });
3138
+ console.log(chalk15.green(" Tools installed"));
3139
+ } catch {
3140
+ console.log(chalk15.red(" Failed to install tools"));
3141
+ }
3142
+ }
3143
+ console.log(chalk15.green("\nAll tools installed!\n"));
3144
+ console.log(chalk15.cyan("Make sure mise is activated in your shell:"));
3145
+ console.log(' eval "$(mise activate zsh)" # zsh');
3146
+ console.log(' eval "$(mise activate bash)" # bash\n');
3147
+ })
3148
+ ).addCommand(
3149
+ new Command15("generate").description("Generate .mise.toml files from hub.yaml").action(async () => {
3150
+ const hubDir = process.cwd();
3151
+ const config = await loadHubConfig(hubDir);
3152
+ if (!config.tools && !config.repos.some((r) => r.tools)) {
3153
+ console.log(chalk15.yellow("\nNo tools defined in hub.yaml\n"));
3154
+ return;
3155
+ }
3156
+ console.log(chalk15.blue("\n\u2501\u2501\u2501 Generating .mise.toml files \u2501\u2501\u2501\n"));
3157
+ const count = await generateMiseFiles(config, hubDir);
3158
+ console.log(chalk15.green(`
3159
+ Generated ${count} .mise.toml file(s)
3160
+ `));
3161
+ console.log(chalk15.cyan("Install with: hub tools install\n"));
3162
+ })
3163
+ ).addCommand(
3164
+ new Command15("check").description("Verify installed tool versions match hub.yaml").action(async () => {
3165
+ const hubDir = process.cwd();
3166
+ const config = await loadHubConfig(hubDir);
3167
+ if (!config.tools && !config.repos.some((r) => r.tools)) {
3168
+ console.log(chalk15.yellow("\nNo tools defined in hub.yaml\n"));
3169
+ return;
3170
+ }
3171
+ console.log(chalk15.blue("\n\u2501\u2501\u2501 Checking tool versions \u2501\u2501\u2501\n"));
3172
+ let allOk = true;
3173
+ if (config.tools) {
3174
+ console.log(chalk15.cyan("Hub tools:"));
3175
+ for (const [tool, expected] of Object.entries(config.tools)) {
3176
+ const actual = getInstalledVersion(tool);
3177
+ if (!actual) {
3178
+ console.log(chalk15.red(` \u2717 ${tool}: not found (expected ${expected})`));
3179
+ allOk = false;
3180
+ } else if (actual.includes(expected) || expected.includes(extractVersion(actual))) {
3181
+ console.log(chalk15.green(` \u2713 ${tool} ${expected}`));
3182
+ } else {
3183
+ console.log(chalk15.yellow(` \u26A0 ${tool}: ${extractVersion(actual)} (expected ${expected})`));
3184
+ allOk = false;
3185
+ }
3186
+ }
3187
+ }
3188
+ for (const repo of config.repos) {
3189
+ if (!repo.tools || Object.keys(repo.tools).length === 0) continue;
3190
+ const repoDir = join16(hubDir, repo.path);
3191
+ if (!existsSync13(repoDir)) {
3192
+ console.log(chalk15.dim(`
3193
+ ${repo.name}: not cloned`));
3194
+ continue;
3195
+ }
3196
+ console.log(chalk15.yellow(`
3197
+ \u25B8 ${repo.name}`));
3198
+ for (const [tool, expected] of Object.entries(repo.tools)) {
3199
+ const actual = getInstalledVersion(tool);
3200
+ if (!actual) {
3201
+ console.log(chalk15.red(` \u2717 ${tool}: not found (expected ${expected})`));
3202
+ allOk = false;
3203
+ } else if (actual.includes(expected) || expected.includes(extractVersion(actual))) {
3204
+ console.log(chalk15.green(` \u2713 ${tool} ${expected}`));
3205
+ } else {
3206
+ console.log(chalk15.yellow(` \u26A0 ${tool}: ${extractVersion(actual)} (expected ${expected})`));
3207
+ allOk = false;
3208
+ }
3209
+ }
3210
+ }
3211
+ console.log();
3212
+ if (allOk) {
3213
+ console.log(chalk15.green("All tool versions match!\n"));
3214
+ } else {
3215
+ console.log(chalk15.red("Some tools are missing or have wrong versions.\n"));
3216
+ console.log(chalk15.cyan("Fix with: hub tools install --generate\n"));
3217
+ }
3218
+ })
3219
+ );
3220
+ function getInstalledVersion(tool) {
3221
+ const cmds = {
3222
+ node: "node --version",
3223
+ pnpm: "pnpm --version",
3224
+ yarn: "yarn --version",
3225
+ erlang: "erl +V 2>&1",
3226
+ elixir: "elixir --version",
3227
+ ruby: "ruby --version",
3228
+ python: "python3 --version",
3229
+ go: "go version",
3230
+ rust: "rustc --version",
3231
+ direnv: "direnv --version",
3232
+ java: "java --version"
3233
+ };
3234
+ const cmd = cmds[tool] || `${tool} --version`;
3235
+ try {
3236
+ return execSync11(cmd, {
3237
+ stdio: ["pipe", "pipe", "pipe"],
3238
+ encoding: "utf-8"
3239
+ }).trim();
3240
+ } catch {
3241
+ return null;
3242
+ }
3243
+ }
3244
+ function extractVersion(s) {
3245
+ const match = s.match(/(\d+\.\d+[.\d]*)/);
3246
+ return match?.[1] || s;
3247
+ }
3248
+
3249
+ // src/index.ts
3250
+ var program = new Command16();
3251
+ program.name("hub").description(
3252
+ "Give your AI coding assistant the full picture. Multi-repo context, agent orchestration, and end-to-end workflows."
3253
+ ).version("0.1.0");
3254
+ program.addCommand(initCommand);
3255
+ program.addCommand(addRepoCommand);
3256
+ program.addCommand(setupCommand);
3257
+ program.addCommand(generateCommand);
3258
+ program.addCommand(envCommand);
3259
+ program.addCommand(servicesCommand);
3260
+ program.addCommand(skillsCommand);
3261
+ program.addCommand(agentsCommand);
3262
+ program.addCommand(hooksCommand);
3263
+ program.addCommand(commandsCommand);
3264
+ program.addCommand(registryCommand);
3265
+ program.addCommand(pullCommand);
3266
+ program.addCommand(statusCommand);
3267
+ program.addCommand(execCommand);
3268
+ program.addCommand(worktreeCommand);
3269
+ program.addCommand(doctorCommand);
3270
+ program.addCommand(toolsCommand);
3271
+ program.parse();