githits 0.1.3 → 0.1.7

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 CHANGED
@@ -7,31 +7,12 @@ GitHits gives your AI coding assistant access to verified, canonical code exampl
7
7
  ## Quick Start
8
8
 
9
9
  ```sh
10
- npx githits login
10
+ npx githits init
11
11
  ```
12
12
 
13
- This opens your browser to authenticate with your [GitHits](https://githits.com) account. Once logged in, set up your AI assistant:
14
-
15
- **Claude Code**
13
+ `init` authenticates with your [GitHits](https://githits.com) account, then auto-detects your installed coding tools and configures each one with GitHits MCP.
16
14
 
17
- ```sh
18
- claude mcp add githits -- npx -y githits mcp start
19
- ```
20
-
21
- **Cursor / VS Code**
22
-
23
- Add to your MCP settings JSON:
24
-
25
- ```json
26
- {
27
- "mcpServers": {
28
- "githits": {
29
- "command": "npx",
30
- "args": ["-y", "githits", "mcp", "start"]
31
- }
32
- }
33
- }
34
- ```
15
+ Supported tools: Claude Code, Cursor, Windsurf, VS Code / Copilot, Cline, Claude Desktop, Codex CLI, Gemini CLI, and Google Antigravity.
35
16
 
36
17
  That's it. Your assistant now has a `search` tool it will use automatically when it needs code examples.
37
18
 
@@ -84,7 +65,8 @@ export GITHITS_API_TOKEN=ghi-your-token-here
84
65
  ## Commands
85
66
 
86
67
  ```
87
- githits login Authenticate with your GitHits account
68
+ githits init Authenticate and configure your coding tools with GitHits MCP
69
+ githits login Authenticate with your GitHits account (also runs as part of init)
88
70
  githits logout Remove stored credentials
89
71
  githits mcp Show setup instructions in a terminal; starts MCP server when piped
90
72
  githits mcp start Always start MCP server (for use in MCP config files)
@@ -99,9 +81,19 @@ githits auth status Show current authentication status
99
81
  | `GITHITS_MCP_URL` | Override MCP server URL | `https://mcp.githits.com` |
100
82
  | `GITHITS_API_URL` | Override REST API URL | `https://api.githits.com` |
101
83
 
84
+ ## Development
85
+
86
+ ```sh
87
+ bun run build
88
+ npm link
89
+ githits --version
90
+ ```
91
+
92
+ After the initial `npm link`, only `bun run build` is needed for subsequent changes.
93
+
102
94
  ## Requirements
103
95
 
104
- - Node.js 20 or later
96
+ - Node.js 24 or later
105
97
 
106
98
  ## License
107
99
 
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  version
4
- } from "./shared/chunk-8dn0z2ja.js";
4
+ } from "./shared/chunk-hg653b0a.js";
5
5
 
6
6
  // src/cli.ts
7
7
  import { Command } from "commander";
@@ -547,11 +547,40 @@ function getApiUrl() {
547
547
  function getEnvApiToken() {
548
548
  return process.env.GITHITS_API_TOKEN;
549
549
  }
550
+ // src/services/exec-service.ts
551
+ import { spawn } from "node:child_process";
552
+
553
+ class ExecServiceImpl {
554
+ async exec(command, args) {
555
+ return new Promise((resolve, reject) => {
556
+ const child = spawn(command, args, {
557
+ stdio: ["ignore", "pipe", "pipe"],
558
+ env: { ...process.env },
559
+ ...process.platform === "win32" && { shell: true }
560
+ });
561
+ const stdoutChunks = [];
562
+ const stderrChunks = [];
563
+ child.stdout.on("data", (chunk) => stdoutChunks.push(chunk));
564
+ child.stderr.on("data", (chunk) => stderrChunks.push(chunk));
565
+ child.on("error", (error) => {
566
+ reject(error);
567
+ });
568
+ child.on("close", (code) => {
569
+ resolve({
570
+ exitCode: code ?? 1,
571
+ stdout: Buffer.concat(stdoutChunks).toString("utf-8"),
572
+ stderr: Buffer.concat(stderrChunks).toString("utf-8")
573
+ });
574
+ });
575
+ });
576
+ }
577
+ }
550
578
  // src/services/filesystem-service.ts
551
579
  import {
552
580
  mkdir,
553
581
  readdir,
554
582
  readFile,
583
+ rename,
555
584
  stat,
556
585
  unlink,
557
586
  writeFile
@@ -609,6 +638,23 @@ class FileSystemServiceImpl {
609
638
  return false;
610
639
  }
611
640
  }
641
+ async atomicWriteFile(path, contents) {
642
+ const tmpPath = `${path}.${process.pid}.${Date.now()}.tmp`;
643
+ let mode = 384;
644
+ try {
645
+ const existing = await stat(path);
646
+ mode = existing.mode & 511;
647
+ } catch {}
648
+ try {
649
+ await writeFile(tmpPath, contents, { mode });
650
+ await rename(tmpPath, path);
651
+ } catch (error) {
652
+ try {
653
+ await unlink(tmpPath);
654
+ } catch {}
655
+ throw error;
656
+ }
657
+ }
612
658
  }
613
659
  // src/services/githits-service.ts
614
660
  class AuthenticationError extends Error {
@@ -891,6 +937,28 @@ class MigratingAuthStorage {
891
937
  return this.primary.getStorageLocation();
892
938
  }
893
939
  }
940
+ // src/services/prompt-service.ts
941
+ import { checkbox, select } from "@inquirer/prompts";
942
+
943
+ class PromptServiceImpl {
944
+ async checkbox(message, choices) {
945
+ return checkbox({ message, choices });
946
+ }
947
+ async confirm3(message) {
948
+ return select({
949
+ message,
950
+ choices: [
951
+ { value: "yes", name: "Yes" },
952
+ { value: "no", name: "No" },
953
+ {
954
+ value: "always",
955
+ name: "Yes to all",
956
+ description: "Skip confirmation for remaining agents"
957
+ }
958
+ ]
959
+ });
960
+ }
961
+ }
894
962
  // src/services/refreshing-githits-service.ts
895
963
  class RefreshingGitHitsService {
896
964
  apiUrl;
@@ -1239,6 +1307,500 @@ function registerFeedbackCommand(program) {
1239
1307
  }
1240
1308
  });
1241
1309
  }
1310
+ // src/commands/init/setup-handlers.ts
1311
+ function mergeServerConfig(existingContent, serversKey, serverName, serverConfig) {
1312
+ let content = existingContent;
1313
+ if (content.charCodeAt(0) === 65279) {
1314
+ content = content.slice(1);
1315
+ }
1316
+ const trimmed = content.trim();
1317
+ if (trimmed === "") {
1318
+ content = "{}";
1319
+ }
1320
+ let config;
1321
+ try {
1322
+ config = JSON.parse(content);
1323
+ } catch (err) {
1324
+ return {
1325
+ status: "parse_error",
1326
+ error: `Invalid JSON: ${err instanceof Error ? err.message : String(err)}`
1327
+ };
1328
+ }
1329
+ if (typeof config !== "object" || config === null || Array.isArray(config)) {
1330
+ return {
1331
+ status: "parse_error",
1332
+ error: "Config file root is not a JSON object"
1333
+ };
1334
+ }
1335
+ if (!(serversKey in config)) {
1336
+ config[serversKey] = {};
1337
+ }
1338
+ const servers = config[serversKey];
1339
+ if (typeof servers !== "object" || servers === null || Array.isArray(servers)) {
1340
+ return {
1341
+ status: "parse_error",
1342
+ error: `"${serversKey}" is not a JSON object`
1343
+ };
1344
+ }
1345
+ const serversObj = servers;
1346
+ if (serverName in serversObj) {
1347
+ return { status: "already_configured" };
1348
+ }
1349
+ serversObj[serverName] = serverConfig;
1350
+ return {
1351
+ status: "added",
1352
+ content: `${JSON.stringify(config, null, 2)}
1353
+ `
1354
+ };
1355
+ }
1356
+ function formatSetupPreview(config) {
1357
+ if (config.method === "cli") {
1358
+ return config.commands.map((cmd) => `Will run: ${cmd.command} ${cmd.args.join(" ")}`).join(`
1359
+ `);
1360
+ }
1361
+ const snippet = JSON.stringify({ [config.serverName]: config.serverConfig }, null, 2);
1362
+ return `Will add to ${config.configPath}:
1363
+
1364
+ ${snippet}`;
1365
+ }
1366
+ async function isAlreadyConfigured(config, fs) {
1367
+ try {
1368
+ let content;
1369
+ try {
1370
+ content = await fs.readFile(config.configPath);
1371
+ } catch {
1372
+ return false;
1373
+ }
1374
+ if (content.charCodeAt(0) === 65279) {
1375
+ content = content.slice(1);
1376
+ }
1377
+ const trimmed = content.trim();
1378
+ if (trimmed === "") {
1379
+ return false;
1380
+ }
1381
+ const parsed = JSON.parse(trimmed);
1382
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
1383
+ return false;
1384
+ }
1385
+ const servers = parsed[config.serversKey];
1386
+ if (typeof servers !== "object" || servers === null || Array.isArray(servers)) {
1387
+ return false;
1388
+ }
1389
+ return config.serverName in servers;
1390
+ } catch {
1391
+ return false;
1392
+ }
1393
+ }
1394
+ async function isCliAlreadyConfigured(check, execService) {
1395
+ try {
1396
+ const result = await execService.exec(check.command, check.args);
1397
+ const combined = `${result.stdout} ${result.stderr}`;
1398
+ return check.configuredPattern.test(combined);
1399
+ } catch {
1400
+ return false;
1401
+ }
1402
+ }
1403
+ var ALREADY_EXISTS_PATTERNS = [
1404
+ /already exists/i,
1405
+ /already configured/i,
1406
+ /already added/i
1407
+ ];
1408
+ function isAlreadyConfiguredOutput(output) {
1409
+ return ALREADY_EXISTS_PATTERNS.some((pattern) => pattern.test(output));
1410
+ }
1411
+ async function executeCliCommand(cmd, execService) {
1412
+ try {
1413
+ const result = await execService.exec(cmd.command, cmd.args);
1414
+ const combined = `${result.stdout} ${result.stderr}`;
1415
+ if (isAlreadyConfiguredOutput(combined)) {
1416
+ return {
1417
+ status: "already_configured",
1418
+ message: `GitHits already configured via ${cmd.command}`
1419
+ };
1420
+ }
1421
+ if (result.exitCode === 0) {
1422
+ return { status: "success", message: "Configured successfully" };
1423
+ }
1424
+ const detail = result.stderr.trim() || result.stdout.trim();
1425
+ return {
1426
+ status: "failed",
1427
+ message: `Command exited with code ${result.exitCode}${detail ? `: ${detail}` : ""}`
1428
+ };
1429
+ } catch (err) {
1430
+ if (err instanceof Error && "code" in err && err.code === "ENOENT") {
1431
+ return {
1432
+ status: "failed",
1433
+ message: `"${cmd.command}" not found on PATH. Install it or configure manually.`
1434
+ };
1435
+ }
1436
+ return {
1437
+ status: "failed",
1438
+ message: `Failed to run command: ${err instanceof Error ? err.message : String(err)}`
1439
+ };
1440
+ }
1441
+ }
1442
+ async function executeCliSetup(setup, execService) {
1443
+ let anyAlreadyConfigured = false;
1444
+ for (const cmd of setup.commands) {
1445
+ const result = await executeCliCommand(cmd, execService);
1446
+ if (result.status === "failed") {
1447
+ return result;
1448
+ }
1449
+ if (result.status === "already_configured") {
1450
+ anyAlreadyConfigured = true;
1451
+ }
1452
+ }
1453
+ if (anyAlreadyConfigured) {
1454
+ return {
1455
+ status: "already_configured",
1456
+ message: `GitHits already configured via ${setup.commands[0]?.command}`
1457
+ };
1458
+ }
1459
+ return { status: "success", message: "Configured successfully" };
1460
+ }
1461
+ async function executeConfigFileSetup(setup, fs) {
1462
+ try {
1463
+ const parentDir = fs.getDirname(setup.configPath);
1464
+ await fs.ensureDir(parentDir);
1465
+ let existingContent = "";
1466
+ try {
1467
+ existingContent = await fs.readFile(setup.configPath);
1468
+ } catch (err) {
1469
+ if (!(err instanceof Error) || !("code" in err) || err.code !== "ENOENT") {
1470
+ return {
1471
+ status: "failed",
1472
+ message: `Cannot read ${setup.configPath}: ${err instanceof Error ? err.message : String(err)}`
1473
+ };
1474
+ }
1475
+ }
1476
+ const result = mergeServerConfig(existingContent, setup.serversKey, setup.serverName, setup.serverConfig);
1477
+ if (result.status === "already_configured") {
1478
+ return {
1479
+ status: "already_configured",
1480
+ message: `GitHits already configured in ${setup.configPath}`
1481
+ };
1482
+ }
1483
+ if (result.status === "parse_error") {
1484
+ return {
1485
+ status: "failed",
1486
+ message: `Cannot parse ${setup.configPath}: ${result.error}. File left unchanged.`
1487
+ };
1488
+ }
1489
+ await fs.atomicWriteFile(setup.configPath, result.content);
1490
+ return { status: "success", message: "Configured successfully" };
1491
+ } catch (err) {
1492
+ if (err instanceof Error && "code" in err && err.code === "EACCES") {
1493
+ return {
1494
+ status: "failed",
1495
+ message: `Permission denied writing to ${setup.configPath}. Check file permissions.`
1496
+ };
1497
+ }
1498
+ return {
1499
+ status: "failed",
1500
+ message: `Failed to configure: ${err instanceof Error ? err.message : String(err)}`
1501
+ };
1502
+ }
1503
+ }
1504
+
1505
+ // src/commands/init/agent-definitions.ts
1506
+ function getAppDataPath(fs, appName) {
1507
+ const home = fs.getHomeDir();
1508
+ switch (process.platform) {
1509
+ case "win32":
1510
+ return fs.joinPath(process.env.APPDATA ?? fs.joinPath(home, "AppData", "Roaming"), appName);
1511
+ case "darwin":
1512
+ return fs.joinPath(home, "Library", "Application Support", appName);
1513
+ default:
1514
+ return fs.joinPath(home, ".config", appName);
1515
+ }
1516
+ }
1517
+ async function isExecutableAvailable(exec, executable) {
1518
+ try {
1519
+ const lookupCommand = process.platform === "win32" ? "where" : "which";
1520
+ const result = await exec.exec(lookupCommand, [executable]);
1521
+ return result.exitCode === 0;
1522
+ } catch {
1523
+ return false;
1524
+ }
1525
+ }
1526
+ var claudeCode = {
1527
+ name: "Claude Code",
1528
+ id: "claude-code",
1529
+ detectionMethod: "binary",
1530
+ setupMethod: "cli",
1531
+ detectBinary: async (exec) => isExecutableAvailable(exec, "claude"),
1532
+ getSetupConfig: () => ({
1533
+ method: "cli",
1534
+ commands: [
1535
+ {
1536
+ command: "claude",
1537
+ args: [
1538
+ "plugin",
1539
+ "marketplace",
1540
+ "add",
1541
+ "githits-com/githits-claude-code-plugin"
1542
+ ]
1543
+ },
1544
+ {
1545
+ command: "claude",
1546
+ args: ["plugin", "install", "githits@githits-plugins"]
1547
+ }
1548
+ ],
1549
+ checkCommand: {
1550
+ command: "claude",
1551
+ args: ["plugin", "list"],
1552
+ configuredPattern: /githits/i
1553
+ }
1554
+ })
1555
+ };
1556
+ var cursor = {
1557
+ name: "Cursor",
1558
+ id: "cursor",
1559
+ detectionMethod: "path",
1560
+ setupMethod: "config-file",
1561
+ detectPaths: (fs) => [fs.joinPath(fs.getHomeDir(), ".cursor")],
1562
+ getSetupConfig: (fs) => ({
1563
+ method: "config-file",
1564
+ configPath: fs.joinPath(fs.getHomeDir(), ".cursor", "mcp.json"),
1565
+ serversKey: "mcpServers",
1566
+ serverName: "GitHits",
1567
+ serverConfig: { url: getMcpUrl() }
1568
+ })
1569
+ };
1570
+ var windsurf = {
1571
+ name: "Windsurf",
1572
+ id: "windsurf",
1573
+ detectionMethod: "path",
1574
+ setupMethod: "config-file",
1575
+ detectPaths: (fs) => [fs.joinPath(fs.getHomeDir(), ".codeium", "windsurf")],
1576
+ getSetupConfig: (fs) => ({
1577
+ method: "config-file",
1578
+ configPath: fs.joinPath(fs.getHomeDir(), ".codeium", "windsurf", "mcp_config.json"),
1579
+ serversKey: "mcpServers",
1580
+ serverName: "GitHits",
1581
+ serverConfig: { serverUrl: getMcpUrl() }
1582
+ })
1583
+ };
1584
+ var claudeDesktop = {
1585
+ name: "Claude Desktop",
1586
+ id: "claude-desktop",
1587
+ detectionMethod: "path",
1588
+ setupMethod: "config-file",
1589
+ detectPaths: (fs) => {
1590
+ const appData = getAppDataPath(fs, "Claude");
1591
+ if (process.platform === "win32") {
1592
+ const home = fs.getHomeDir();
1593
+ const localAppData = process.env.LOCALAPPDATA ?? fs.joinPath(home, "AppData", "Local");
1594
+ return [
1595
+ appData,
1596
+ fs.joinPath(localAppData, "Claude"),
1597
+ fs.joinPath(localAppData, "Programs", "Claude")
1598
+ ];
1599
+ }
1600
+ return [appData];
1601
+ },
1602
+ getSetupConfig: (fs) => {
1603
+ const appData = getAppDataPath(fs, "Claude");
1604
+ return {
1605
+ method: "config-file",
1606
+ configPath: fs.joinPath(appData, "claude_desktop_config.json"),
1607
+ serversKey: "mcpServers",
1608
+ serverName: "GitHits",
1609
+ serverConfig: {
1610
+ command: "npx",
1611
+ args: ["-y", "mcp-remote", getMcpUrl()]
1612
+ }
1613
+ };
1614
+ }
1615
+ };
1616
+ var codexCli = {
1617
+ name: "Codex CLI",
1618
+ id: "codex-cli",
1619
+ detectionMethod: "binary",
1620
+ setupMethod: "cli",
1621
+ detectBinary: async (exec) => isExecutableAvailable(exec, "codex"),
1622
+ getSetupConfig: () => ({
1623
+ method: "cli",
1624
+ commands: [
1625
+ {
1626
+ command: "codex",
1627
+ args: [
1628
+ "mcp",
1629
+ "add",
1630
+ "githits",
1631
+ "--",
1632
+ "npx",
1633
+ "-y",
1634
+ "githits@latest",
1635
+ "mcp",
1636
+ "start"
1637
+ ]
1638
+ }
1639
+ ],
1640
+ checkCommand: {
1641
+ command: "codex",
1642
+ args: ["mcp", "list"],
1643
+ configuredPattern: /githits/i
1644
+ }
1645
+ })
1646
+ };
1647
+ var vscode = {
1648
+ name: "VS Code / Copilot",
1649
+ id: "vscode",
1650
+ detectionMethod: "path",
1651
+ setupMethod: "config-file",
1652
+ detectPaths: (fs) => {
1653
+ const appData = getAppDataPath(fs, "Code");
1654
+ return [appData];
1655
+ },
1656
+ getSetupConfig: (fs) => {
1657
+ const appData = getAppDataPath(fs, "Code");
1658
+ return {
1659
+ method: "config-file",
1660
+ configPath: fs.joinPath(appData, "User", "mcp.json"),
1661
+ serversKey: "servers",
1662
+ serverName: "GitHits",
1663
+ serverConfig: { url: getMcpUrl(), type: "http" }
1664
+ };
1665
+ }
1666
+ };
1667
+ var cline = {
1668
+ name: "Cline",
1669
+ id: "cline",
1670
+ detectionMethod: "path",
1671
+ setupMethod: "config-file",
1672
+ detectPaths: (fs) => [fs.joinPath(fs.getHomeDir(), ".cline")],
1673
+ getSetupConfig: (fs) => ({
1674
+ method: "config-file",
1675
+ configPath: fs.joinPath(fs.getHomeDir(), ".cline", "data", "settings", "cline_mcp_settings.json"),
1676
+ serversKey: "mcpServers",
1677
+ serverName: "GitHits",
1678
+ serverConfig: { url: getMcpUrl(), type: "streamableHttp" }
1679
+ })
1680
+ };
1681
+ var geminiCli = {
1682
+ name: "Gemini CLI",
1683
+ id: "gemini-cli",
1684
+ detectionMethod: "binary",
1685
+ setupMethod: "cli",
1686
+ detectBinary: async (exec) => isExecutableAvailable(exec, "gemini"),
1687
+ getSetupConfig: () => ({
1688
+ method: "cli",
1689
+ commands: [
1690
+ {
1691
+ command: "gemini",
1692
+ args: [
1693
+ "extensions",
1694
+ "install",
1695
+ "https://github.com/githits-com/githits-gemini-cli"
1696
+ ]
1697
+ }
1698
+ ],
1699
+ checkCommand: {
1700
+ command: "gemini",
1701
+ args: ["extensions", "list"],
1702
+ configuredPattern: /githits/i
1703
+ }
1704
+ })
1705
+ };
1706
+ var googleAntigravity = {
1707
+ name: "Google Antigravity",
1708
+ id: "google-antigravity",
1709
+ detectionMethod: "path",
1710
+ setupMethod: "config-file",
1711
+ detectPaths: (fs) => [fs.joinPath(fs.getHomeDir(), ".gemini", "antigravity")],
1712
+ getSetupConfig: (fs) => ({
1713
+ method: "config-file",
1714
+ configPath: fs.joinPath(fs.getHomeDir(), ".gemini", "antigravity", "mcp_config.json"),
1715
+ serversKey: "mcpServers",
1716
+ serverName: "GitHits",
1717
+ serverConfig: { serverUrl: getMcpUrl() }
1718
+ })
1719
+ };
1720
+ var openCode = {
1721
+ name: "OpenCode",
1722
+ id: "opencode",
1723
+ detectionMethod: "binary",
1724
+ setupMethod: "config-file",
1725
+ detectBinary: async (exec) => isExecutableAvailable(exec, "opencode"),
1726
+ getSetupConfig: (fs) => ({
1727
+ method: "config-file",
1728
+ configPath: process.platform === "win32" ? fs.joinPath(process.env.APPDATA ?? fs.joinPath(fs.getHomeDir(), "AppData", "Roaming"), "opencode", "opencode.json") : fs.joinPath(fs.getHomeDir(), ".config", "opencode", "opencode.json"),
1729
+ serversKey: "mcp",
1730
+ serverName: "GitHits",
1731
+ serverConfig: {
1732
+ type: "local",
1733
+ command: ["npx", "-y", "githits@latest", "mcp", "start"],
1734
+ enabled: true
1735
+ }
1736
+ })
1737
+ };
1738
+ var agentDefinitions = [
1739
+ claudeCode,
1740
+ cursor,
1741
+ windsurf,
1742
+ vscode,
1743
+ cline,
1744
+ claudeDesktop,
1745
+ codexCli,
1746
+ geminiCli,
1747
+ googleAntigravity,
1748
+ openCode
1749
+ ];
1750
+ async function scanAgents(definitions, fs, execService) {
1751
+ const result = {
1752
+ needsSetup: [],
1753
+ alreadyConfigured: [],
1754
+ notDetected: []
1755
+ };
1756
+ for (const agent of definitions) {
1757
+ let detected = false;
1758
+ if (agent.detectionMethod === "binary" && agent.detectBinary) {
1759
+ try {
1760
+ detected = await agent.detectBinary(execService);
1761
+ } catch {
1762
+ detected = false;
1763
+ }
1764
+ } else if (agent.detectionMethod === "path" && agent.detectPaths) {
1765
+ const paths = agent.detectPaths(fs);
1766
+ for (const path of paths) {
1767
+ if (await fs.isDirectory(path)) {
1768
+ detected = true;
1769
+ break;
1770
+ }
1771
+ }
1772
+ }
1773
+ if (!detected) {
1774
+ result.notDetected.push(agent);
1775
+ continue;
1776
+ }
1777
+ if (agent.setupMethod === "config-file") {
1778
+ const config = agent.getSetupConfig(fs);
1779
+ if (config.method === "config-file" && await isAlreadyConfigured(config, fs)) {
1780
+ result.alreadyConfigured.push(agent);
1781
+ } else {
1782
+ result.needsSetup.push(agent);
1783
+ }
1784
+ } else {
1785
+ const config = agent.getSetupConfig(fs);
1786
+ if (config.method === "cli" && config.checkCommand) {
1787
+ const configured = await isCliAlreadyConfigured(config.checkCommand, execService);
1788
+ if (configured) {
1789
+ result.alreadyConfigured.push(agent);
1790
+ } else {
1791
+ result.needsSetup.push(agent);
1792
+ }
1793
+ } else {
1794
+ result.needsSetup.push(agent);
1795
+ }
1796
+ }
1797
+ }
1798
+ return result;
1799
+ }
1800
+ // node_modules/@inquirer/core/dist/lib/errors.js
1801
+ class ExitPromptError extends Error {
1802
+ name = "ExitPromptError";
1803
+ }
1242
1804
  // src/shared/colors.ts
1243
1805
  var colors = {
1244
1806
  reset: "\x1B[0m",
@@ -1263,6 +1825,18 @@ function colorize(text, color, useColors) {
1263
1825
  return text;
1264
1826
  return `${colors[color]}${text}${colors.reset}`;
1265
1827
  }
1828
+ function success(text, useColors) {
1829
+ const checkmark = useColors ? `${colors.green}✓${colors.reset}` : "✓";
1830
+ return `${checkmark} ${text}`;
1831
+ }
1832
+ function error(text, useColors) {
1833
+ const cross = useColors ? `${colors.red}✗${colors.reset}` : "✗";
1834
+ return `${cross} ${text}`;
1835
+ }
1836
+ function warning(text, useColors) {
1837
+ const warn = useColors ? `${colors.yellow}⚠${colors.reset}` : "⚠";
1838
+ return `${warn} ${text}`;
1839
+ }
1266
1840
  function highlight(text, useColors) {
1267
1841
  if (!useColors)
1268
1842
  return text;
@@ -1274,76 +1848,24 @@ function dim(text, useColors) {
1274
1848
  return `${colors.dim}${text}${colors.reset}`;
1275
1849
  }
1276
1850
 
1277
- // src/shared/language-filter.ts
1278
- var DEFAULT_LIMIT = 5;
1279
- function filterLanguages(languages, query, limit = DEFAULT_LIMIT) {
1280
- const lowerQuery = query.toLowerCase();
1281
- return languages.filter((lang) => lang.name.toLowerCase().includes(lowerQuery) || lang.display_name.toLowerCase().includes(lowerQuery) || lang.aliases.some((a) => a.toLowerCase().includes(lowerQuery))).slice(0, limit).map(({ name, display_name }) => ({ name, display_name }));
1282
- }
1283
-
1284
- // src/commands/languages.ts
1285
- async function languagesAction(query, options, deps) {
1286
- requireAuth(deps);
1287
- try {
1288
- const allLanguages = await deps.githitsService.getLanguages();
1289
- const displayList = query ? filterLanguages(allLanguages, query) : allLanguages.map(({ name, display_name }) => ({ name, display_name }));
1290
- if (options.json) {
1291
- console.log(JSON.stringify(displayList));
1292
- } else if (query && displayList.length === 0) {
1293
- console.log(`No languages matching "${query}".`);
1294
- } else {
1295
- const useColors = shouldUseColors();
1296
- for (const lang of displayList) {
1297
- console.log(` ${colorize(lang.name, "cyan", useColors)} ${dim(lang.display_name, useColors)}`);
1298
- }
1299
- }
1300
- } catch (error) {
1301
- console.error(`Failed to list languages: ${error instanceof Error ? error.message : error}`);
1302
- process.exit(1);
1303
- }
1304
- }
1305
- var LANGUAGES_DESCRIPTION = `List supported programming languages.
1306
-
1307
- Without a query, lists all supported languages.
1308
- With a query, filters to the top 5 matches by name, display name, or alias.
1309
-
1310
- Examples:
1311
- githits languages List all languages
1312
- githits languages python Filter by name
1313
- githits languages type --json JSON output for piping`;
1314
- function registerLanguagesCommand(program) {
1315
- program.command("languages").summary("List supported programming languages").description(LANGUAGES_DESCRIPTION).argument("[query]", "Filter by name, display name, or alias").option("--json", "Output as JSON for piping").action(async (query, options) => {
1316
- try {
1317
- const deps = await createContainer();
1318
- await languagesAction(query, options, deps);
1319
- } catch (error) {
1320
- if (error instanceof AuthRequiredError)
1321
- process.exit(1);
1322
- throw error;
1323
- }
1324
- });
1325
- }
1326
1851
  // src/commands/login.ts
1327
1852
  var TIMEOUT_MS = 5 * 60 * 1000;
1328
1853
  function randomPort() {
1329
1854
  return Math.floor(Math.random() * 2000) + 8000;
1330
1855
  }
1331
- async function loginAction(options, deps) {
1856
+ async function loginFlow(options, deps) {
1332
1857
  const { authService, authStorage, browserService, mcpUrl } = deps;
1333
1858
  if (options.port !== undefined && (Number.isNaN(options.port) || options.port < 1 || options.port > 65535)) {
1334
- console.error("Invalid port number. Must be between 1 and 65535.");
1335
- process.exit(1);
1859
+ return {
1860
+ status: "failed",
1861
+ message: "Invalid port number. Must be between 1 and 65535."
1862
+ };
1336
1863
  }
1337
1864
  const existing = await authStorage.loadTokens(mcpUrl);
1338
1865
  if (existing && !options.force) {
1339
1866
  const isExpired = existing.expiresAt && new Date(existing.expiresAt) < new Date;
1340
1867
  if (!isExpired) {
1341
- console.log(`Already logged in.
1342
- `);
1343
- console.log(` Environment: ${mcpUrl}
1344
- `);
1345
- console.log("To re-authenticate, use `githits login --force`.");
1346
- return;
1868
+ return { status: "already_authenticated", message: "Already logged in." };
1347
1869
  }
1348
1870
  console.log(`Token expired. Starting new login...
1349
1871
  `);
@@ -1351,6 +1873,9 @@ async function loginAction(options, deps) {
1351
1873
  console.log(`Re-authenticating (--force flag)...
1352
1874
  `);
1353
1875
  }
1876
+ if (!existing) {
1877
+ await authStorage.clearClient(mcpUrl);
1878
+ }
1354
1879
  console.log("Discovering OAuth endpoints...");
1355
1880
  const metadata = await authService.discoverEndpoints(mcpUrl);
1356
1881
  let client = await authStorage.loadClient(mcpUrl);
@@ -1424,22 +1949,18 @@ async function loginAction(options, deps) {
1424
1949
  callback = await Promise.race([serverPromise, timeoutPromise]);
1425
1950
  if (timeoutId)
1426
1951
  clearTimeout(timeoutId);
1427
- } catch (error) {
1952
+ } catch (error2) {
1428
1953
  if (timeoutId)
1429
1954
  clearTimeout(timeoutId);
1430
- if (error instanceof Error) {
1431
- console.log(`${error.message}.
1432
- `);
1433
- console.log("Run `githits login` to try again.");
1434
- }
1435
- process.exit(1);
1955
+ const msg = error2 instanceof Error ? error2.message : "Authentication failed";
1956
+ return { status: "failed", message: `${msg}.` };
1436
1957
  }
1437
1958
  if (callback.type !== "success") {
1438
- console.log(`${callback.message}
1439
- `);
1440
- console.log("Run `githits login` to try again.");
1441
1959
  await new Promise((r) => setTimeout(r, 2000));
1442
- process.exit(1);
1960
+ return {
1961
+ status: "failed",
1962
+ message: callback.message ?? "Authentication callback failed."
1963
+ };
1443
1964
  }
1444
1965
  let tokenResponse;
1445
1966
  try {
@@ -1451,11 +1972,15 @@ async function loginAction(options, deps) {
1451
1972
  codeVerifier: verifier,
1452
1973
  redirectUri
1453
1974
  });
1454
- } catch (error) {
1455
- console.error(`Failed to complete authentication: ${error instanceof Error ? error.message : error}
1456
- `);
1457
- console.log("Run `githits login` to try again.");
1458
- process.exit(1);
1975
+ } catch (error2) {
1976
+ try {
1977
+ await authStorage.clearClient(mcpUrl);
1978
+ } catch {}
1979
+ const msg = error2 instanceof Error ? error2.message : String(error2);
1980
+ return {
1981
+ status: "failed",
1982
+ message: `Failed to complete authentication: ${msg}`
1983
+ };
1459
1984
  }
1460
1985
  const expiresAt = new Date(Date.now() + tokenResponse.expiresIn * 1000).toISOString();
1461
1986
  await authStorage.saveTokens(mcpUrl, {
@@ -1465,10 +1990,31 @@ async function loginAction(options, deps) {
1465
1990
  createdAt: new Date().toISOString()
1466
1991
  });
1467
1992
  const hours = Math.round(tokenResponse.expiresIn / 3600);
1993
+ return {
1994
+ status: "success",
1995
+ message: `Logged in successfully. Token expires in ${hours} hour${hours !== 1 ? "s" : ""}.`
1996
+ };
1997
+ }
1998
+ async function loginAction(options, deps) {
1999
+ const result = await loginFlow(options, deps);
2000
+ if (result.status === "already_authenticated") {
2001
+ console.log(`Already logged in.
2002
+ `);
2003
+ console.log(` Environment: ${deps.mcpUrl}
2004
+ `);
2005
+ console.log("To re-authenticate, use `githits login --force`.");
2006
+ return;
2007
+ }
2008
+ if (result.status === "failed") {
2009
+ console.error(`${result.message}
2010
+ `);
2011
+ console.log("Run `githits login` to try again.");
2012
+ process.exit(1);
2013
+ }
1468
2014
  console.log(`Logged in successfully.
1469
2015
  `);
1470
- console.log(` Environment: ${mcpUrl}`);
1471
- console.log(` Token expires in: ${hours} hour${hours !== 1 ? "s" : ""}`);
2016
+ console.log(` Environment: ${deps.mcpUrl}`);
2017
+ console.log(result.message.replace("Logged in successfully. ", " "));
1472
2018
  console.log(`
1473
2019
  You're ready to use githits with your AI assistant.`);
1474
2020
  }
@@ -1485,6 +2031,217 @@ function registerLoginCommand(program) {
1485
2031
  await loginAction(options, deps);
1486
2032
  });
1487
2033
  }
2034
+
2035
+ // src/commands/init/init.ts
2036
+ async function initAction(options, deps) {
2037
+ const useColors = shouldUseColors();
2038
+ const { fileSystemService, promptService, execService, createLoginDeps } = deps;
2039
+ console.log(`
2040
+ ${colorize("GitHits", "bold", useColors)} — Set up MCP server for your coding agents
2041
+ `);
2042
+ if (!options.skipLogin && createLoginDeps) {
2043
+ console.log(` Checking authentication...
2044
+ `);
2045
+ let loginResult;
2046
+ try {
2047
+ const loginDeps = await createLoginDeps();
2048
+ loginResult = await loginFlow({}, loginDeps);
2049
+ } catch (error2) {
2050
+ const msg = error2 instanceof Error ? error2.message : String(error2);
2051
+ loginResult = { status: "failed", message: msg };
2052
+ }
2053
+ if (loginResult.status === "already_authenticated") {
2054
+ console.log(` ${success("Already authenticated", useColors)}
2055
+ `);
2056
+ } else if (loginResult.status === "success") {
2057
+ console.log(` ${success("Logged in successfully", useColors)}
2058
+ `);
2059
+ } else {
2060
+ console.log(` ${warning(`Login failed: ${loginResult.message}`, useColors)}
2061
+ `);
2062
+ if (!options.yes) {
2063
+ try {
2064
+ const choice = await promptService.confirm3("Continue without authentication?");
2065
+ if (choice === "no") {
2066
+ console.log("\n Setup cancelled. Run `githits login` to authenticate.\n");
2067
+ return;
2068
+ }
2069
+ } catch (err) {
2070
+ if (err instanceof ExitPromptError) {
2071
+ console.log(`
2072
+ Setup cancelled.
2073
+ `);
2074
+ return;
2075
+ }
2076
+ throw err;
2077
+ }
2078
+ }
2079
+ console.log(` Continuing without authentication...
2080
+ `);
2081
+ }
2082
+ }
2083
+ console.log(` Scanning for available agents...
2084
+ `);
2085
+ const scan = await scanAgents(agentDefinitions, fileSystemService, execService);
2086
+ for (const agent of scan.alreadyConfigured) {
2087
+ console.log(` ${success(`${agent.name} — already configured`, useColors)}`);
2088
+ }
2089
+ for (const agent of scan.needsSetup) {
2090
+ console.log(` ${colorize(`● ${agent.name} — needs setup`, "cyan", useColors)}`);
2091
+ }
2092
+ for (const agent of scan.notDetected) {
2093
+ console.log(` ${colorize(`${agent.name} — not detected`, "dim", useColors)}`);
2094
+ }
2095
+ console.log();
2096
+ if (scan.needsSetup.length === 0 && scan.alreadyConfigured.length === 0) {
2097
+ console.log(` No coding agents detected. Install an agent and try again.
2098
+ `);
2099
+ return;
2100
+ }
2101
+ if (scan.needsSetup.length === 0) {
2102
+ console.log(` All detected agents are already configured. Nothing to do.
2103
+ `);
2104
+ return;
2105
+ }
2106
+ const toSetup = scan.needsSetup;
2107
+ const outcomes = [];
2108
+ let alwaysMode = options.yes ?? false;
2109
+ for (const agent of toSetup) {
2110
+ console.log(` Setting up ${colorize(agent.name, "bold", useColors)}...
2111
+ `);
2112
+ const config = agent.getSetupConfig(fileSystemService);
2113
+ const preview = formatSetupPreview(config);
2114
+ for (const line of preview.split(`
2115
+ `)) {
2116
+ console.log(` ${line}`);
2117
+ }
2118
+ console.log();
2119
+ if (!alwaysMode) {
2120
+ let choice;
2121
+ try {
2122
+ choice = await promptService.confirm3("Proceed?");
2123
+ } catch (err) {
2124
+ if (err instanceof ExitPromptError) {
2125
+ console.log(`
2126
+ Setup cancelled.
2127
+ `);
2128
+ return;
2129
+ }
2130
+ throw err;
2131
+ }
2132
+ if (choice === "no") {
2133
+ outcomes.push({ name: agent.name, status: "skipped" });
2134
+ console.log();
2135
+ continue;
2136
+ }
2137
+ if (choice === "always") {
2138
+ alwaysMode = true;
2139
+ }
2140
+ }
2141
+ const result = config.method === "cli" ? await executeCliSetup(config, execService) : await executeConfigFileSetup(config, fileSystemService);
2142
+ outcomes.push({ name: agent.name, status: result.status });
2143
+ if (result.status === "success") {
2144
+ console.log(` ${success(`${agent.name} configured`, useColors)}
2145
+ `);
2146
+ } else if (result.status === "already_configured") {
2147
+ console.log(` ${warning(`${agent.name} already configured`, useColors)}
2148
+ `);
2149
+ } else {
2150
+ console.log(` ${error(result.message, useColors)}
2151
+ `);
2152
+ }
2153
+ }
2154
+ const configured = outcomes.filter((o) => o.status === "success").length;
2155
+ const alreadyDone = outcomes.filter((o) => o.status === "already_configured").length + scan.alreadyConfigured.length;
2156
+ const failed = outcomes.filter((o) => o.status === "failed").length;
2157
+ const skipped = outcomes.filter((o) => o.status === "skipped").length;
2158
+ if (configured > 0 || alreadyDone > 0) {
2159
+ console.log(" Done! GitHits is ready.");
2160
+ } else if (failed > 0) {
2161
+ console.log(" Setup completed with errors.");
2162
+ } else if (skipped > 0) {
2163
+ console.log(" Setup skipped.");
2164
+ }
2165
+ if (failed > 0) {
2166
+ console.log(` ${failed} agent${failed !== 1 ? "s" : ""} failed to configure.`);
2167
+ }
2168
+ if (skipped > 0) {
2169
+ console.log(` ${skipped} agent${skipped !== 1 ? "s" : ""} skipped.`);
2170
+ }
2171
+ console.log();
2172
+ }
2173
+ var INIT_DESCRIPTION = `Set up GitHits MCP server for your coding agents.
2174
+
2175
+ Authenticates with your GitHits account, then scans for available agents
2176
+ (Claude Code, Cursor, Windsurf, VS Code, Cline, Claude Desktop, Codex CLI,
2177
+ Gemini CLI, Google Antigravity), checks which are already configured,
2178
+ and sets up unconfigured ones with your confirmation.
2179
+
2180
+ Supports CLI-based setup (Claude Code, Codex, Gemini CLI) and config
2181
+ file editing (Cursor, Windsurf, VS Code, Cline, Claude Desktop,
2182
+ Google Antigravity) with atomic writes.`;
2183
+ function registerInitCommand(program) {
2184
+ program.command("init").summary("Set up MCP server for your coding agents").description(INIT_DESCRIPTION).option("-y, --yes", "Skip prompts, configure all detected agents").option("--skip-login", "Skip authentication step").action(async (options) => {
2185
+ const fileSystemService = new FileSystemServiceImpl;
2186
+ const promptService = new PromptServiceImpl;
2187
+ const execService = new ExecServiceImpl;
2188
+ await initAction(options, {
2189
+ fileSystemService,
2190
+ promptService,
2191
+ execService,
2192
+ createLoginDeps: () => createContainer()
2193
+ });
2194
+ });
2195
+ }
2196
+ // src/shared/language-filter.ts
2197
+ var DEFAULT_LIMIT = 5;
2198
+ function filterLanguages(languages, query, limit = DEFAULT_LIMIT) {
2199
+ const lowerQuery = query.toLowerCase();
2200
+ return languages.filter((lang) => lang.name.toLowerCase().includes(lowerQuery) || lang.display_name.toLowerCase().includes(lowerQuery) || lang.aliases.some((a) => a.toLowerCase().includes(lowerQuery))).slice(0, limit).map(({ name, display_name }) => ({ name, display_name }));
2201
+ }
2202
+
2203
+ // src/commands/languages.ts
2204
+ async function languagesAction(query, options, deps) {
2205
+ requireAuth(deps);
2206
+ try {
2207
+ const allLanguages = await deps.githitsService.getLanguages();
2208
+ const displayList = query ? filterLanguages(allLanguages, query) : allLanguages.map(({ name, display_name }) => ({ name, display_name }));
2209
+ if (options.json) {
2210
+ console.log(JSON.stringify(displayList));
2211
+ } else if (query && displayList.length === 0) {
2212
+ console.log(`No languages matching "${query}".`);
2213
+ } else {
2214
+ const useColors = shouldUseColors();
2215
+ for (const lang of displayList) {
2216
+ console.log(` ${colorize(lang.name, "cyan", useColors)} ${dim(lang.display_name, useColors)}`);
2217
+ }
2218
+ }
2219
+ } catch (error2) {
2220
+ console.error(`Failed to list languages: ${error2 instanceof Error ? error2.message : error2}`);
2221
+ process.exit(1);
2222
+ }
2223
+ }
2224
+ var LANGUAGES_DESCRIPTION = `List supported programming languages.
2225
+
2226
+ Without a query, lists all supported languages.
2227
+ With a query, filters to the top 5 matches by name, display name, or alias.
2228
+
2229
+ Examples:
2230
+ githits languages List all languages
2231
+ githits languages python Filter by name
2232
+ githits languages type --json JSON output for piping`;
2233
+ function registerLanguagesCommand(program) {
2234
+ program.command("languages").summary("List supported programming languages").description(LANGUAGES_DESCRIPTION).argument("[query]", "Filter by name, display name, or alias").option("--json", "Output as JSON for piping").action(async (query, options) => {
2235
+ try {
2236
+ const deps = await createContainer();
2237
+ await languagesAction(query, options, deps);
2238
+ } catch (error2) {
2239
+ if (error2 instanceof AuthRequiredError)
2240
+ process.exit(1);
2241
+ throw error2;
2242
+ }
2243
+ });
2244
+ }
1488
2245
  // src/commands/logout.ts
1489
2246
  async function logoutAction(deps) {
1490
2247
  const { authStorage, mcpUrl } = deps;
@@ -1492,13 +2249,13 @@ async function logoutAction(deps) {
1492
2249
  let firstError;
1493
2250
  try {
1494
2251
  await authStorage.clearTokens(mcpUrl);
1495
- } catch (error) {
1496
- firstError = error;
2252
+ } catch (error2) {
2253
+ firstError = error2;
1497
2254
  }
1498
2255
  try {
1499
2256
  await authStorage.clearClient(mcpUrl);
1500
- } catch (error) {
1501
- firstError ??= error;
2257
+ } catch (error2) {
2258
+ firstError ??= error2;
1502
2259
  }
1503
2260
  if (!auth) {
1504
2261
  console.log(`Not currently logged in.
@@ -1547,8 +2304,8 @@ function errorResult(message) {
1547
2304
  async function withErrorHandling(operation, fn) {
1548
2305
  try {
1549
2306
  return await fn();
1550
- } catch (error) {
1551
- const message = error instanceof Error ? error.message : "Unknown error";
2307
+ } catch (error2) {
2308
+ const message = error2 instanceof Error ? error2.message : "Unknown error";
1552
2309
  return errorResult(`Failed to ${operation}: ${message}`);
1553
2310
  }
1554
2311
  }
@@ -1769,8 +2526,8 @@ async function searchAction(query, options, deps) {
1769
2526
  } else {
1770
2527
  console.log(result);
1771
2528
  }
1772
- } catch (error) {
1773
- console.error(`Failed to search: ${error instanceof Error ? error.message : error}`);
2529
+ } catch (error2) {
2530
+ console.error(`Failed to search: ${error2 instanceof Error ? error2.message : error2}`);
1774
2531
  process.exit(1);
1775
2532
  }
1776
2533
  }
@@ -1790,10 +2547,10 @@ function registerSearchCommand(program) {
1790
2547
  try {
1791
2548
  const deps = await createContainer();
1792
2549
  await searchAction(query, options, deps);
1793
- } catch (error) {
1794
- if (error instanceof AuthRequiredError)
2550
+ } catch (error2) {
2551
+ if (error2 instanceof AuthRequiredError)
1795
2552
  process.exit(1);
1796
- throw error;
2553
+ throw error2;
1797
2554
  }
1798
2555
  });
1799
2556
  }
@@ -1805,6 +2562,7 @@ program.name("githits").description("Code examples from global open source for y
1805
2562
  }
1806
2563
  }).addHelpText("after", `
1807
2564
  Getting started:
2565
+ githits init Set up MCP for your coding agents
1808
2566
  githits login Authenticate with your GitHits account
1809
2567
  githits mcp Start MCP server for your AI assistant
1810
2568
  githits search "query" --lang python Search for code examples
@@ -1812,6 +2570,7 @@ Getting started:
1812
2570
  Learn more at https://githits.com
1813
2571
  Docs: https://app.githits.com/docs/
1814
2572
  Support: support@githits.com`);
2573
+ registerInitCommand(program);
1815
2574
  registerLoginCommand(program);
1816
2575
  registerLogoutCommand(program);
1817
2576
  registerMcpCommand(program);
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  version
3
- } from "./shared/chunk-8dn0z2ja.js";
3
+ } from "./shared/chunk-hg653b0a.js";
4
4
  export {
5
5
  version
6
6
  };
@@ -1,4 +1,4 @@
1
1
  // package.json
2
- var version = "0.1.3";
2
+ var version = "0.1.7";
3
3
 
4
4
  export { version };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "githits",
3
3
  "description": "CLI companion for GitHits - code examples from global open source for developers and AI assistants",
4
- "version": "0.1.3",
4
+ "version": "0.1.7",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "dist",
@@ -48,18 +48,19 @@
48
48
  "homepage": "https://githits.com",
49
49
  "repository": {
50
50
  "type": "git",
51
- "url": "git+https://github.com/GitHits-com/githits-cli.git"
51
+ "url": "git+https://github.com/githits-com/githits-cli.git"
52
52
  },
53
53
  "bugs": {
54
- "url": "https://github.com/GitHits-com/githits-cli/issues"
54
+ "url": "https://github.com/githits-com/githits-cli/issues"
55
55
  },
56
56
  "engines": {
57
- "node": ">=20"
57
+ "node": ">=24"
58
58
  },
59
59
  "publishConfig": {
60
60
  "access": "public"
61
61
  },
62
62
  "dependencies": {
63
+ "@inquirer/prompts": "^8.3.2",
63
64
  "@modelcontextprotocol/sdk": "^1.23.0",
64
65
  "@napi-rs/keyring": "^1.2.0",
65
66
  "commander": "^14.0.2",