@whylineee/zerocode 0.1.0 → 0.1.2

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 CHANGED
@@ -1,35 +1,54 @@
1
1
  #!/usr/bin/env node
2
- import { writeFileSync, existsSync, mkdirSync } from "node:fs";
3
- import { join } from "node:path";
4
- import { detectAgents, allAgentTargets, addMcpToAgent, removeMcpFromAgent, listInstalledMcp, } from "./agents.js";
2
+ import { writeFileSync, readFileSync, existsSync, mkdirSync, readdirSync } from "node:fs";
3
+ import { join, dirname } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { execSync } from "node:child_process";
6
+ import { detectAgents, allAgentTargets, addMcpToAgent, removeMcpFromAgent, listInstalledMcp, supportsJsonConfig, } from "./agents.js";
5
7
  import { mcpRegistry, skillRegistry, findMcp, findSkill } from "./registry.js";
6
8
  import { banner, heading, success, warn, error, info, item, table, divider } from "./ui.js";
7
- import { choose, confirm, askEnvVars } from "./prompt.js";
9
+ import { choose, chooseMultiple, confirm, ask, askEnvVars } from "./prompt.js";
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = dirname(__filename);
12
+ const PKG_VERSION = (() => {
13
+ try {
14
+ const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
15
+ return pkg.version ?? "0.0.0";
16
+ }
17
+ catch {
18
+ return "0.0.0";
19
+ }
20
+ })();
8
21
  // ── Arg parsing ──────────────────────────────────────────────────
9
22
  const args = process.argv.slice(2);
10
23
  const command = args[0]?.toLowerCase();
11
24
  const subCommand = args[1]?.toLowerCase();
12
25
  const target = args[2]?.toLowerCase();
13
- function usage() {
14
- banner();
26
+ function printUsage() {
15
27
  console.log(" Usage:");
16
28
  console.log();
29
+ console.log(" zerocode init Interactive setup wizard");
17
30
  console.log(" zerocode detect Detect installed agents");
31
+ console.log(" zerocode configure Pick an agent and manage it interactively");
18
32
  console.log(" zerocode list mcp List available MCP servers");
19
33
  console.log(" zerocode list skills List available skills");
20
34
  console.log(" zerocode add mcp <name> Install MCP server to an agent");
21
35
  console.log(" zerocode add skill <name> Install a skill to the project");
22
36
  console.log(" zerocode remove mcp <name> Remove MCP server from an agent");
23
37
  console.log(" zerocode status Show what's installed on each agent");
24
- console.log(" zerocode add mcp <name> --agent <id> Install to a specific agent");
38
+ console.log(" zerocode doctor Check your setup for issues");
39
+ console.log(" zerocode sync Copy MCP servers from one agent to another");
40
+ console.log(" zerocode export Export config to shareable .zerocode.json");
41
+ console.log(" zerocode import [path] Import config from .zerocode.json");
42
+ console.log(" zerocode backup Backup all agent configs");
43
+ console.log(" zerocode restore Restore agent configs from backup");
25
44
  console.log();
26
45
  console.log(" Examples:");
27
46
  console.log();
28
- console.log(" npx zerocode detect");
29
- console.log(" npx zerocode add mcp filesystem-mcp");
30
- console.log(" npx zerocode add mcp github-mcp --agent claude-desktop");
31
- console.log(" npx zerocode add skill pr-reviewer");
32
- console.log(" npx zerocode status");
47
+ console.log(" npx @whylineee/zerocode detect");
48
+ console.log(" npx @whylineee/zerocode add mcp filesystem-mcp");
49
+ console.log(" npx @whylineee/zerocode add mcp github-mcp --agent claude-desktop");
50
+ console.log(" npx @whylineee/zerocode add skill pr-reviewer");
51
+ console.log(" npx @whylineee/zerocode status");
33
52
  console.log();
34
53
  }
35
54
  // ── Helpers ──────────────────────────────────────────────────────
@@ -168,8 +187,13 @@ async function cmdAddMcp() {
168
187
  warn("Cancelled.");
169
188
  return;
170
189
  }
171
- addMcpToAgent(agent, serverName, entry);
190
+ const written = addMcpToAgent(agent, serverName, entry);
172
191
  console.log();
192
+ if (!written) {
193
+ warn(`${agent.name} does not use JSON config — MCP must be configured manually.`);
194
+ info(`Add the server entry to: ${agent.configPath}`);
195
+ return;
196
+ }
173
197
  success(`${mcp.name} installed to ${agent.name}`);
174
198
  info(`Config written to: ${agent.configPath}`);
175
199
  if (mcp.envVars && mcp.envVars.length > 0) {
@@ -232,6 +256,13 @@ async function cmdRemoveMcp() {
232
256
  if (agentFlag) {
233
257
  const all = allAgentTargets();
234
258
  agent = all.find((a) => a.id === agentFlag);
259
+ if (!agent) {
260
+ error(`Agent "${agentFlag}" not found. Available agents:`);
261
+ for (const a of detectAgents()) {
262
+ item(a.id, a.name);
263
+ }
264
+ return;
265
+ }
235
266
  }
236
267
  else {
237
268
  const detected = detectAgents();
@@ -273,10 +304,744 @@ async function cmdStatus() {
273
304
  divider();
274
305
  }
275
306
  }
307
+ // ── Init ─────────────────────────────────────────────────────────
308
+ async function cmdInit() {
309
+ heading("Interactive Setup Wizard");
310
+ info("Let's set up your AI agent environment.\n");
311
+ // Step 1 — Detect agents
312
+ const detected = detectAgents();
313
+ if (detected.length === 0) {
314
+ warn("No agents detected on this machine.");
315
+ info("Install an agent first (Claude Desktop, Cursor, Windsurf, etc.) and re-run.");
316
+ return;
317
+ }
318
+ success(`Found ${detected.length} agent(s) on your machine:`);
319
+ console.log();
320
+ for (const a of detected) {
321
+ item(a.name, a.scope);
322
+ }
323
+ // Step 2 — Pick agents to configure
324
+ const selectedAgents = await chooseMultiple("Which agents do you want to configure?", detected);
325
+ if (selectedAgents.length === 0) {
326
+ warn("No agents selected. Aborting.");
327
+ return;
328
+ }
329
+ // Step 3 — Pick MCP servers to install
330
+ console.log();
331
+ info("Popular MCP starter packs:");
332
+ console.log();
333
+ info(" a) Essential — filesystem, git, memory");
334
+ info(" b) Web — fetch, brave-search, firecrawl");
335
+ info(" c) DevOps — github, docker, sentry");
336
+ info(" d) Custom — pick your own");
337
+ console.log();
338
+ const pack = await ask(" Choose a pack (a/b/c/d): ");
339
+ let mcpSlugs;
340
+ switch (pack.toLowerCase()) {
341
+ case "a":
342
+ mcpSlugs = ["filesystem-mcp", "git-mcp", "memory-mcp"];
343
+ break;
344
+ case "b":
345
+ mcpSlugs = ["fetch-mcp", "brave-search-mcp", "firecrawl-mcp"];
346
+ break;
347
+ case "c":
348
+ mcpSlugs = ["github-mcp", "docker-mcp", "sentry-mcp"];
349
+ break;
350
+ default: {
351
+ const picked = await chooseMultiple("Select MCP servers to install:", mcpRegistry.map((m) => ({ name: m.name, slug: m.slug })));
352
+ mcpSlugs = picked.map((p) => p.slug);
353
+ }
354
+ }
355
+ if (mcpSlugs.length === 0) {
356
+ warn("No MCP servers selected. Skipping MCP setup.");
357
+ }
358
+ // Step 4 — Collect env vars for all selected MCP servers
359
+ const allEnvValues = {};
360
+ for (const slug of mcpSlugs) {
361
+ const mcp = findMcp(slug);
362
+ if (!mcp)
363
+ continue;
364
+ if (mcp.envVars && mcp.envVars.length > 0) {
365
+ console.log();
366
+ info(`${mcp.name} requires: ${mcp.envVars.join(", ")}`);
367
+ const vals = await askEnvVars(mcp.envVars);
368
+ Object.assign(allEnvValues, vals);
369
+ }
370
+ }
371
+ // Step 5 — Install to all selected agents
372
+ console.log();
373
+ heading("Installing...");
374
+ for (const agent of selectedAgents) {
375
+ for (const slug of mcpSlugs) {
376
+ const mcp = findMcp(slug);
377
+ if (!mcp)
378
+ continue;
379
+ const entry = resolveEnvPlaceholders({ ...mcp.entry }, allEnvValues);
380
+ const serverName = slug.replace(/-mcp$/, "");
381
+ addMcpToAgent(agent, serverName, entry);
382
+ success(`${mcp.name} → ${agent.name}`);
383
+ }
384
+ }
385
+ // Step 6 — Optionally install skills
386
+ console.log();
387
+ const wantSkills = await confirm("Install some skills too?");
388
+ if (wantSkills) {
389
+ const picked = await chooseMultiple("Select skills:", skillRegistry.map((s) => ({ name: s.name, slug: s.slug })));
390
+ for (const s of picked) {
391
+ const skill = findSkill(s.slug);
392
+ if (!skill)
393
+ continue;
394
+ const skillDir = join(process.cwd(), ".zerocode", "skills", skill.slug);
395
+ const skillPath = join(skillDir, "SKILL.md");
396
+ if (!existsSync(skillDir))
397
+ mkdirSync(skillDir, { recursive: true });
398
+ writeFileSync(skillPath, skill.skillMd + "\n", "utf-8");
399
+ success(`${skill.name} → ${skillPath}`);
400
+ }
401
+ }
402
+ console.log();
403
+ success("Setup complete!");
404
+ info("Restart your agents to pick up the new MCP servers.");
405
+ info("Run 'zerocode status' to verify everything.");
406
+ }
407
+ // ── Doctor ───────────────────────────────────────────────────────
408
+ function commandExists(cmd) {
409
+ try {
410
+ execSync(`which ${cmd}`, { stdio: "ignore" });
411
+ return true;
412
+ }
413
+ catch {
414
+ return false;
415
+ }
416
+ }
417
+ function isValidJson(filePath) {
418
+ try {
419
+ if (!existsSync(filePath))
420
+ return false;
421
+ JSON.parse(readFileSync(filePath, "utf-8"));
422
+ return true;
423
+ }
424
+ catch {
425
+ return false;
426
+ }
427
+ }
428
+ async function cmdDoctor() {
429
+ heading("Diagnostics");
430
+ let issues = 0;
431
+ // Check runtimes
432
+ info("Checking runtimes...");
433
+ console.log();
434
+ const runtimes = [
435
+ ["node", "Required for npx-based MCP servers"],
436
+ ["npx", "Runs MCP servers from npm"],
437
+ ["uvx", "Runs Python-based MCP servers (git, fetch, sqlite)"],
438
+ ["python3", "Required by some MCP servers"],
439
+ ];
440
+ for (const [cmd, purpose] of runtimes) {
441
+ if (commandExists(cmd)) {
442
+ try {
443
+ const ver = execSync(`${cmd} --version 2>/dev/null`, { encoding: "utf-8" }).trim();
444
+ success(`${cmd} ${ver} — ${purpose}`);
445
+ }
446
+ catch {
447
+ success(`${cmd} found — ${purpose}`);
448
+ }
449
+ }
450
+ else {
451
+ warn(`${cmd} not found — ${purpose}`);
452
+ issues++;
453
+ }
454
+ }
455
+ // Check agents
456
+ console.log();
457
+ info("Checking agents...");
458
+ console.log();
459
+ const detected = detectAgents();
460
+ if (detected.length === 0) {
461
+ warn("No agents detected.");
462
+ issues++;
463
+ }
464
+ else {
465
+ for (const agent of detected) {
466
+ const configExists = existsSync(agent.configPath);
467
+ const usesJson = agent.configFormat !== "claude-code";
468
+ const configValid = usesJson ? isValidJson(agent.configPath) : configExists;
469
+ if (!configExists) {
470
+ item(agent.name, "config not found (will be created on first add)");
471
+ }
472
+ else if (!configValid) {
473
+ error(`${agent.name} — config exists but is INVALID JSON: ${agent.configPath}`);
474
+ issues++;
475
+ }
476
+ else {
477
+ const installed = listInstalledMcp(agent);
478
+ const count = Object.keys(installed).length;
479
+ success(`${agent.name} — ${count} MCP server(s) configured`);
480
+ // Check if configured servers have placeholder env vars
481
+ for (const [key, srv] of Object.entries(installed)) {
482
+ const hasPlaceholder = srv.args.some((a) => /^\{.+\}$/.test(a));
483
+ const envPlaceholders = srv.env
484
+ ? Object.values(srv.env).filter((v) => /^\{.+\}$/.test(v))
485
+ : [];
486
+ if (hasPlaceholder || envPlaceholders.length > 0) {
487
+ warn(` └─ ${key}: has unresolved placeholder values`);
488
+ issues++;
489
+ }
490
+ }
491
+ }
492
+ }
493
+ }
494
+ // Summary
495
+ console.log();
496
+ divider();
497
+ if (issues === 0) {
498
+ success("All checks passed! Your setup looks healthy.");
499
+ }
500
+ else {
501
+ warn(`${issues} issue(s) found. Review the warnings above.`);
502
+ }
503
+ }
504
+ // ── Backup / Restore ─────────────────────────────────────────────
505
+ const BACKUP_DIR = join(process.cwd(), ".zerocode");
506
+ const BACKUP_FILE = join(BACKUP_DIR, "backup.json");
507
+ async function cmdBackup() {
508
+ heading("Backing up agent configs");
509
+ const detected = detectAgents();
510
+ if (detected.length === 0) {
511
+ warn("No agents detected. Nothing to back up.");
512
+ return;
513
+ }
514
+ const backup = {
515
+ timestamp: new Date().toISOString(),
516
+ agents: [],
517
+ };
518
+ for (const agent of detected) {
519
+ if (existsSync(agent.configPath)) {
520
+ const content = readFileSync(agent.configPath, "utf-8");
521
+ backup.agents.push({
522
+ id: agent.id,
523
+ name: agent.name,
524
+ configPath: agent.configPath,
525
+ configFormat: agent.configFormat,
526
+ content,
527
+ });
528
+ success(`${agent.name} — backed up`);
529
+ }
530
+ else {
531
+ info(`${agent.name} — no config file yet, skipping`);
532
+ }
533
+ }
534
+ if (backup.agents.length === 0) {
535
+ warn("No configs found to back up.");
536
+ return;
537
+ }
538
+ if (!existsSync(BACKUP_DIR)) {
539
+ mkdirSync(BACKUP_DIR, { recursive: true });
540
+ }
541
+ writeFileSync(BACKUP_FILE, JSON.stringify(backup, null, 2), "utf-8");
542
+ console.log();
543
+ success(`Backup saved to ${BACKUP_FILE}`);
544
+ info(`${backup.agents.length} agent config(s) stored.`);
545
+ }
546
+ async function cmdRestore() {
547
+ heading("Restoring agent configs");
548
+ if (!existsSync(BACKUP_FILE)) {
549
+ error(`No backup found at ${BACKUP_FILE}`);
550
+ info("Run 'zerocode backup' first.");
551
+ return;
552
+ }
553
+ const raw = readFileSync(BACKUP_FILE, "utf-8");
554
+ let backup;
555
+ try {
556
+ backup = JSON.parse(raw);
557
+ }
558
+ catch {
559
+ error("Backup file is corrupted.");
560
+ return;
561
+ }
562
+ info(`Backup from: ${backup.timestamp}`);
563
+ info(`Contains: ${backup.agents.length} agent config(s)`);
564
+ console.log();
565
+ for (const entry of backup.agents) {
566
+ item(entry.name, entry.configPath);
567
+ }
568
+ console.log();
569
+ const ok = await confirm("Restore all configs? This will overwrite current configs.");
570
+ if (!ok) {
571
+ warn("Cancelled.");
572
+ return;
573
+ }
574
+ for (const entry of backup.agents) {
575
+ const dir = dirname(entry.configPath);
576
+ if (!existsSync(dir)) {
577
+ mkdirSync(dir, { recursive: true });
578
+ }
579
+ writeFileSync(entry.configPath, entry.content, "utf-8");
580
+ success(`${entry.name} — restored`);
581
+ }
582
+ console.log();
583
+ success("All configs restored!");
584
+ info("Restart your agents to apply.");
585
+ }
586
+ // ── Export / Import ──────────────────────────────────────────────
587
+ const EXPORT_FILE = join(process.cwd(), ".zerocode.json");
588
+ async function cmdExport() {
589
+ heading("Exporting configuration");
590
+ const detected = detectAgents();
591
+ if (detected.length === 0) {
592
+ warn("No agents detected. Nothing to export.");
593
+ return;
594
+ }
595
+ // Collect all unique MCP server slugs across agents
596
+ const mcpMap = new Map();
597
+ const agentNames = [];
598
+ for (const agent of detected) {
599
+ const installed = listInstalledMcp(agent);
600
+ const keys = Object.keys(installed);
601
+ if (keys.length > 0) {
602
+ agentNames.push(agent.name);
603
+ }
604
+ for (const key of keys) {
605
+ if (mcpMap.has(key))
606
+ continue;
607
+ // Try to match back to registry for metadata
608
+ const regEntry = mcpRegistry.find((m) => m.slug.replace(/-mcp$/, "") === key);
609
+ mcpMap.set(key, {
610
+ slug: regEntry?.slug ?? key,
611
+ name: regEntry?.name ?? key,
612
+ envVars: regEntry?.envVars,
613
+ });
614
+ }
615
+ }
616
+ // Collect installed skills
617
+ const skillsSlugs = [];
618
+ const skillsDir = join(process.cwd(), ".zerocode", "skills");
619
+ if (existsSync(skillsDir)) {
620
+ try {
621
+ const dirs = readdirSync(skillsDir, { withFileTypes: true });
622
+ for (const d of dirs) {
623
+ if (d.isDirectory() && existsSync(join(skillsDir, d.name, "SKILL.md"))) {
624
+ skillsSlugs.push(d.name);
625
+ }
626
+ }
627
+ }
628
+ catch { /* ignore */ }
629
+ }
630
+ if (mcpMap.size === 0 && skillsSlugs.length === 0) {
631
+ warn("No MCP servers or skills found. Nothing to export.");
632
+ return;
633
+ }
634
+ const manifest = {
635
+ version: 1,
636
+ timestamp: new Date().toISOString(),
637
+ mcpServers: Array.from(mcpMap.values()),
638
+ skills: skillsSlugs,
639
+ source: { agents: agentNames },
640
+ };
641
+ // Show summary
642
+ if (manifest.mcpServers.length > 0) {
643
+ success(`${manifest.mcpServers.length} MCP server(s):`);
644
+ for (const m of manifest.mcpServers) {
645
+ item(m.name, m.slug);
646
+ }
647
+ }
648
+ if (manifest.skills.length > 0) {
649
+ console.log();
650
+ success(`${manifest.skills.length} skill(s):`);
651
+ for (const s of manifest.skills) {
652
+ item(s, "");
653
+ }
654
+ }
655
+ console.log();
656
+ const ok = await confirm(`Export to ${EXPORT_FILE}?`);
657
+ if (!ok) {
658
+ warn("Cancelled.");
659
+ return;
660
+ }
661
+ writeFileSync(EXPORT_FILE, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
662
+ console.log();
663
+ success(`Exported to ${EXPORT_FILE}`);
664
+ info("Share this file with your team or copy it to another machine.");
665
+ info("Import with: zerocode import");
666
+ }
667
+ async function cmdImport() {
668
+ heading("Importing configuration");
669
+ const importPath = args[1] || EXPORT_FILE;
670
+ if (!existsSync(importPath)) {
671
+ error(`No manifest found at ${importPath}`);
672
+ info("Usage: zerocode import [path-to-.zerocode.json]");
673
+ info("Or run 'zerocode export' on the source machine first.");
674
+ return;
675
+ }
676
+ let manifest;
677
+ try {
678
+ manifest = JSON.parse(readFileSync(importPath, "utf-8"));
679
+ }
680
+ catch {
681
+ error("Manifest file is invalid JSON.");
682
+ return;
683
+ }
684
+ if (!manifest.version || !manifest.mcpServers) {
685
+ error("Invalid manifest format.");
686
+ return;
687
+ }
688
+ info(`Manifest from: ${manifest.timestamp}`);
689
+ info(`Source agents: ${manifest.source.agents.join(", ") || "unknown"}`);
690
+ console.log();
691
+ if (manifest.mcpServers.length > 0) {
692
+ success(`${manifest.mcpServers.length} MCP server(s) to install:`);
693
+ for (const m of manifest.mcpServers) {
694
+ item(m.name, m.slug);
695
+ }
696
+ }
697
+ if (manifest.skills.length > 0) {
698
+ console.log();
699
+ success(`${manifest.skills.length} skill(s) to install:`);
700
+ for (const s of manifest.skills) {
701
+ item(s, "");
702
+ }
703
+ }
704
+ // Pick target agents
705
+ console.log();
706
+ const detected = detectAgents();
707
+ if (detected.length === 0) {
708
+ warn("No agents detected on this machine.");
709
+ info("Install an agent first, then re-run.");
710
+ return;
711
+ }
712
+ const selectedAgents = await chooseMultiple("Install to which agents?", detected);
713
+ if (selectedAgents.length === 0) {
714
+ warn("No agents selected. Aborting.");
715
+ return;
716
+ }
717
+ // Collect all required env vars
718
+ const allRequiredEnvVars = [];
719
+ for (const m of manifest.mcpServers) {
720
+ if (m.envVars) {
721
+ for (const v of m.envVars) {
722
+ if (!allRequiredEnvVars.includes(v)) {
723
+ allRequiredEnvVars.push(v);
724
+ }
725
+ }
726
+ }
727
+ }
728
+ let envValues = {};
729
+ if (allRequiredEnvVars.length > 0) {
730
+ console.log();
731
+ info(`Some servers require env vars: ${allRequiredEnvVars.join(", ")}`);
732
+ envValues = await askEnvVars(allRequiredEnvVars);
733
+ }
734
+ // Install MCP servers
735
+ console.log();
736
+ heading("Installing MCP servers...");
737
+ for (const agent of selectedAgents) {
738
+ for (const m of manifest.mcpServers) {
739
+ const regEntry = findMcp(m.slug);
740
+ if (regEntry) {
741
+ const entry = resolveEnvPlaceholders({ ...regEntry.entry }, envValues);
742
+ const serverName = m.slug.replace(/-mcp$/, "");
743
+ addMcpToAgent(agent, serverName, entry);
744
+ success(`${m.name} → ${agent.name}`);
745
+ }
746
+ else {
747
+ warn(`${m.slug} not found in registry — skipped`);
748
+ }
749
+ }
750
+ }
751
+ // Install skills
752
+ if (manifest.skills.length > 0) {
753
+ console.log();
754
+ heading("Installing skills...");
755
+ for (const slug of manifest.skills) {
756
+ const skill = findSkill(slug);
757
+ if (skill) {
758
+ const skillDir = join(process.cwd(), ".zerocode", "skills", skill.slug);
759
+ const skillPath = join(skillDir, "SKILL.md");
760
+ if (!existsSync(skillDir))
761
+ mkdirSync(skillDir, { recursive: true });
762
+ writeFileSync(skillPath, skill.skillMd + "\n", "utf-8");
763
+ success(`${skill.name} → ${skillPath}`);
764
+ }
765
+ else {
766
+ warn(`Skill "${slug}" not found in registry — skipped`);
767
+ }
768
+ }
769
+ }
770
+ console.log();
771
+ success("Import complete!");
772
+ info("Restart your agents to pick up the new MCP servers.");
773
+ }
774
+ // ── Configure (agent-centric interactive) ────────────────────────
775
+ async function cmdConfigure() {
776
+ heading("Agent Configurator");
777
+ // Step 1 — Detect agents
778
+ const detected = detectAgents();
779
+ if (detected.length === 0) {
780
+ warn("No agents detected on this machine.");
781
+ info("Install an agent first (Claude Desktop, Cursor, Windsurf, etc.) and re-run.");
782
+ return;
783
+ }
784
+ success(`Found ${detected.length} agent(s):`);
785
+ console.log();
786
+ for (const a of detected) {
787
+ item(a.name, `${a.scope} · ${a.configPath}`);
788
+ }
789
+ // Step 2 — Pick an agent
790
+ const agent = await choose("Select an agent to configure:", detected);
791
+ if (!agent) {
792
+ warn("No agent selected. Aborting.");
793
+ return;
794
+ }
795
+ console.log();
796
+ success(`Selected: ${agent.name}`);
797
+ // Step 3 — Interactive menu loop
798
+ const actions = [
799
+ { name: "Add MCP server" },
800
+ { name: "Add skill" },
801
+ { name: "View installed MCP servers" },
802
+ { name: "Remove MCP server" },
803
+ { name: "Exit" },
804
+ ];
805
+ while (true) {
806
+ console.log();
807
+ divider();
808
+ const action = await choose(`What do you want to do with ${agent.name}?`, actions);
809
+ if (!action || action.name === "Exit") {
810
+ info("Done configuring.");
811
+ break;
812
+ }
813
+ if (action.name === "Add MCP server") {
814
+ await configureAddMcp(agent);
815
+ }
816
+ else if (action.name === "Add skill") {
817
+ await configureAddSkill();
818
+ }
819
+ else if (action.name === "View installed MCP servers") {
820
+ configureViewInstalled(agent);
821
+ }
822
+ else if (action.name === "Remove MCP server") {
823
+ await configureRemoveMcp(agent);
824
+ }
825
+ }
826
+ }
827
+ async function configureAddMcp(agent) {
828
+ // Let user pick from the full registry
829
+ const items = mcpRegistry.map((m) => ({ name: `${m.name} \x1b[2m${m.slug}\x1b[0m`, slug: m.slug }));
830
+ const picked = await choose("Select MCP server to install:", items);
831
+ if (!picked) {
832
+ warn("Nothing selected.");
833
+ return;
834
+ }
835
+ const mcp = findMcp(picked.slug);
836
+ if (!mcp)
837
+ return;
838
+ info(mcp.description);
839
+ // Collect env vars
840
+ let envValues = {};
841
+ if (mcp.envVars && mcp.envVars.length > 0) {
842
+ console.log();
843
+ info(`Requires: ${mcp.envVars.join(", ")}`);
844
+ envValues = await askEnvVars(mcp.envVars);
845
+ }
846
+ const entry = resolveEnvPlaceholders({ ...mcp.entry }, envValues);
847
+ const serverName = mcp.slug.replace(/-mcp$/, "");
848
+ console.log();
849
+ info(`Server: ${mcp.name}`);
850
+ info(`Agent: ${agent.name}`);
851
+ info(`Key: ${serverName}`);
852
+ console.log();
853
+ const ok = await confirm("Install this MCP server?");
854
+ if (!ok) {
855
+ warn("Cancelled.");
856
+ return;
857
+ }
858
+ const written = addMcpToAgent(agent, serverName, entry);
859
+ if (!written) {
860
+ warn(`${agent.name} does not use JSON config — MCP must be configured manually.`);
861
+ info(`Add the server entry to: ${agent.configPath}`);
862
+ return;
863
+ }
864
+ success(`${mcp.name} installed to ${agent.name}`);
865
+ info("Restart your agent to pick up the new MCP server.");
866
+ }
867
+ async function configureAddSkill() {
868
+ const items = skillRegistry.map((s) => ({ name: `${s.name} \x1b[2m${s.slug}\x1b[0m`, slug: s.slug }));
869
+ const picked = await choose("Select skill to install:", items);
870
+ if (!picked) {
871
+ warn("Nothing selected.");
872
+ return;
873
+ }
874
+ const skill = findSkill(picked.slug);
875
+ if (!skill)
876
+ return;
877
+ info(skill.description);
878
+ const skillDir = join(process.cwd(), ".zerocode", "skills", skill.slug);
879
+ const skillPath = join(skillDir, "SKILL.md");
880
+ if (existsSync(skillPath)) {
881
+ const overwrite = await confirm(`Skill already exists at ${skillPath}. Overwrite?`);
882
+ if (!overwrite) {
883
+ warn("Cancelled.");
884
+ return;
885
+ }
886
+ }
887
+ if (!existsSync(skillDir)) {
888
+ mkdirSync(skillDir, { recursive: true });
889
+ }
890
+ writeFileSync(skillPath, skill.skillMd + "\n", "utf-8");
891
+ success(`${skill.name} installed to ${skillPath}`);
892
+ }
893
+ function configureViewInstalled(agent) {
894
+ const installed = listInstalledMcp(agent);
895
+ const keys = Object.keys(installed);
896
+ console.log();
897
+ if (keys.length === 0) {
898
+ info("No MCP servers configured on this agent.");
899
+ }
900
+ else {
901
+ success(`${keys.length} MCP server(s) on ${agent.name}:`);
902
+ console.log();
903
+ for (const key of keys) {
904
+ const srv = installed[key];
905
+ item(key, `${srv.command} ${srv.args.join(" ")}`);
906
+ }
907
+ }
908
+ }
909
+ async function configureRemoveMcp(agent) {
910
+ const installed = listInstalledMcp(agent);
911
+ const keys = Object.keys(installed);
912
+ if (keys.length === 0) {
913
+ info("No MCP servers to remove.");
914
+ return;
915
+ }
916
+ const items = keys.map((k) => ({ name: k }));
917
+ const picked = await choose("Select MCP server to remove:", items);
918
+ if (!picked) {
919
+ warn("Nothing selected.");
920
+ return;
921
+ }
922
+ const ok = await confirm(`Remove "${picked.name}" from ${agent.name}?`);
923
+ if (!ok) {
924
+ warn("Cancelled.");
925
+ return;
926
+ }
927
+ const removed = removeMcpFromAgent(agent, picked.name);
928
+ if (removed) {
929
+ success(`Removed "${picked.name}" from ${agent.name}`);
930
+ }
931
+ else {
932
+ warn(`"${picked.name}" was not found.`);
933
+ }
934
+ }
935
+ // ── Sync ─────────────────────────────────────────────────────────
936
+ async function cmdSync() {
937
+ heading("Sync MCP servers between agents");
938
+ const detected = detectAgents();
939
+ if (detected.length < 2) {
940
+ warn("Need at least 2 detected agents to sync.");
941
+ if (detected.length === 1) {
942
+ info(`Only found: ${detected[0].name}`);
943
+ }
944
+ info("Install more agents and re-run.");
945
+ return;
946
+ }
947
+ // Filter to agents that support JSON config (can read from)
948
+ const readable = detected.filter((a) => supportsJsonConfig(a));
949
+ if (readable.length === 0) {
950
+ warn("No agents with readable JSON config found.");
951
+ return;
952
+ }
953
+ // Step 1 — Pick source agent
954
+ const source = await choose("Copy FROM which agent?", readable);
955
+ if (!source) {
956
+ warn("No source agent selected. Aborting.");
957
+ return;
958
+ }
959
+ const installed = listInstalledMcp(source);
960
+ const keys = Object.keys(installed);
961
+ if (keys.length === 0) {
962
+ warn(`${source.name} has no MCP servers configured. Nothing to sync.`);
963
+ return;
964
+ }
965
+ console.log();
966
+ success(`${source.name} has ${keys.length} MCP server(s):`);
967
+ console.log();
968
+ for (const key of keys) {
969
+ const srv = installed[key];
970
+ item(key, `${srv.command} ${srv.args.join(" ")}`);
971
+ }
972
+ // Step 2 — Pick which servers to copy
973
+ console.log();
974
+ const copyAll = await confirm("Copy all servers?");
975
+ let serversToCopy;
976
+ if (copyAll) {
977
+ serversToCopy = keys;
978
+ }
979
+ else {
980
+ const picked = await chooseMultiple("Select servers to copy:", keys.map((k) => ({ name: k })));
981
+ serversToCopy = picked.map((p) => p.name);
982
+ if (serversToCopy.length === 0) {
983
+ warn("No servers selected. Aborting.");
984
+ return;
985
+ }
986
+ }
987
+ // Step 3 — Pick target agents
988
+ const targets = detected.filter((a) => a.id !== source.id && supportsJsonConfig(a));
989
+ if (targets.length === 0) {
990
+ warn("No other agents with JSON config available as targets.");
991
+ return;
992
+ }
993
+ console.log();
994
+ const selectedTargets = await chooseMultiple("Copy TO which agents?", targets);
995
+ if (selectedTargets.length === 0) {
996
+ warn("No target agents selected. Aborting.");
997
+ return;
998
+ }
999
+ // Step 4 — Summary and confirm
1000
+ console.log();
1001
+ info(`Copying ${serversToCopy.length} server(s) from ${source.name} to:`);
1002
+ for (const t of selectedTargets) {
1003
+ info(` → ${t.name}`);
1004
+ }
1005
+ console.log();
1006
+ const ok = await confirm("Proceed?");
1007
+ if (!ok) {
1008
+ warn("Cancelled.");
1009
+ return;
1010
+ }
1011
+ // Step 5 — Copy
1012
+ console.log();
1013
+ let copied = 0;
1014
+ let skipped = 0;
1015
+ for (const target of selectedTargets) {
1016
+ for (const serverName of serversToCopy) {
1017
+ const entry = installed[serverName];
1018
+ const written = addMcpToAgent(target, serverName, entry);
1019
+ if (written) {
1020
+ success(`${serverName} → ${target.name}`);
1021
+ copied++;
1022
+ }
1023
+ else {
1024
+ warn(`${serverName} → ${target.name} — skipped (non-JSON config)`);
1025
+ skipped++;
1026
+ }
1027
+ }
1028
+ }
1029
+ console.log();
1030
+ success(`Sync complete! ${copied} server(s) copied.`);
1031
+ if (skipped > 0) {
1032
+ warn(`${skipped} skipped (agents without JSON config support).`);
1033
+ }
1034
+ info("Restart your agents to pick up the changes.");
1035
+ }
276
1036
  // ── Main ─────────────────────────────────────────────────────────
277
1037
  async function main() {
1038
+ // Show splash on every run except version
1039
+ const isVersion = command === "version" || command === "--version" || command === "-v";
1040
+ if (!isVersion) {
1041
+ banner(PKG_VERSION);
1042
+ }
278
1043
  if (!command) {
279
- usage();
1044
+ printUsage();
280
1045
  return;
281
1046
  }
282
1047
  switch (command) {
@@ -319,19 +1084,45 @@ async function main() {
319
1084
  case "status":
320
1085
  await cmdStatus();
321
1086
  break;
1087
+ case "init":
1088
+ await cmdInit();
1089
+ break;
1090
+ case "doctor":
1091
+ await cmdDoctor();
1092
+ break;
1093
+ case "backup":
1094
+ await cmdBackup();
1095
+ break;
1096
+ case "restore":
1097
+ await cmdRestore();
1098
+ break;
1099
+ case "configure":
1100
+ case "config":
1101
+ await cmdConfigure();
1102
+ break;
1103
+ case "sync":
1104
+ case "copy":
1105
+ await cmdSync();
1106
+ break;
1107
+ case "export":
1108
+ await cmdExport();
1109
+ break;
1110
+ case "import":
1111
+ await cmdImport();
1112
+ break;
322
1113
  case "help":
323
1114
  case "--help":
324
1115
  case "-h":
325
- usage();
1116
+ printUsage();
326
1117
  break;
327
1118
  case "version":
328
1119
  case "--version":
329
1120
  case "-v":
330
- console.log("zerocode 0.1.0");
1121
+ console.log(`zerocode ${PKG_VERSION}`);
331
1122
  break;
332
1123
  default:
333
1124
  error(`Unknown command: ${command}`);
334
- usage();
1125
+ printUsage();
335
1126
  }
336
1127
  }
337
1128
  main().catch((err) => {