@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/README.md +237 -60
- package/dist/agents.d.ts +2 -1
- package/dist/agents.d.ts.map +1 -1
- package/dist/agents.js +10 -0
- package/dist/agents.js.map +1 -1
- package/dist/index.js +808 -17
- package/dist/index.js.map +1 -1
- package/dist/prompt.d.ts +4 -0
- package/dist/prompt.d.ts.map +1 -1
- package/dist/prompt.js +22 -1
- package/dist/prompt.js.map +1 -1
- package/dist/ui.d.ts +3 -1
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +38 -5
- package/dist/ui.js.map +1 -1
- package/package.json +3 -4
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 {
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1116
|
+
printUsage();
|
|
326
1117
|
break;
|
|
327
1118
|
case "version":
|
|
328
1119
|
case "--version":
|
|
329
1120
|
case "-v":
|
|
330
|
-
console.log(
|
|
1121
|
+
console.log(`zerocode ${PKG_VERSION}`);
|
|
331
1122
|
break;
|
|
332
1123
|
default:
|
|
333
1124
|
error(`Unknown command: ${command}`);
|
|
334
|
-
|
|
1125
|
+
printUsage();
|
|
335
1126
|
}
|
|
336
1127
|
}
|
|
337
1128
|
main().catch((err) => {
|