githits 0.1.5 → 0.1.8

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/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  version
4
- } from "./shared/chunk-0fnprry7.js";
4
+ } from "./shared/chunk-6w8an8m7.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,495 @@ 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: ["plugin", "marketplace", "add", "githits-com/githits-cli"]
1538
+ },
1539
+ {
1540
+ command: "claude",
1541
+ args: ["plugin", "install", "githits@githits-plugins"]
1542
+ }
1543
+ ],
1544
+ checkCommand: {
1545
+ command: "claude",
1546
+ args: ["plugin", "list"],
1547
+ configuredPattern: /githits/i
1548
+ }
1549
+ })
1550
+ };
1551
+ var cursor = {
1552
+ name: "Cursor",
1553
+ id: "cursor",
1554
+ detectionMethod: "path",
1555
+ setupMethod: "config-file",
1556
+ detectPaths: (fs) => [fs.joinPath(fs.getHomeDir(), ".cursor")],
1557
+ getSetupConfig: (fs) => ({
1558
+ method: "config-file",
1559
+ configPath: fs.joinPath(fs.getHomeDir(), ".cursor", "mcp.json"),
1560
+ serversKey: "mcpServers",
1561
+ serverName: "GitHits",
1562
+ serverConfig: { url: getMcpUrl() }
1563
+ })
1564
+ };
1565
+ var windsurf = {
1566
+ name: "Windsurf",
1567
+ id: "windsurf",
1568
+ detectionMethod: "path",
1569
+ setupMethod: "config-file",
1570
+ detectPaths: (fs) => [fs.joinPath(fs.getHomeDir(), ".codeium", "windsurf")],
1571
+ getSetupConfig: (fs) => ({
1572
+ method: "config-file",
1573
+ configPath: fs.joinPath(fs.getHomeDir(), ".codeium", "windsurf", "mcp_config.json"),
1574
+ serversKey: "mcpServers",
1575
+ serverName: "GitHits",
1576
+ serverConfig: { serverUrl: getMcpUrl() }
1577
+ })
1578
+ };
1579
+ var claudeDesktop = {
1580
+ name: "Claude Desktop",
1581
+ id: "claude-desktop",
1582
+ detectionMethod: "path",
1583
+ setupMethod: "config-file",
1584
+ detectPaths: (fs) => {
1585
+ const appData = getAppDataPath(fs, "Claude");
1586
+ if (process.platform === "win32") {
1587
+ const home = fs.getHomeDir();
1588
+ const localAppData = process.env.LOCALAPPDATA ?? fs.joinPath(home, "AppData", "Local");
1589
+ return [
1590
+ appData,
1591
+ fs.joinPath(localAppData, "Claude"),
1592
+ fs.joinPath(localAppData, "Programs", "Claude")
1593
+ ];
1594
+ }
1595
+ return [appData];
1596
+ },
1597
+ getSetupConfig: (fs) => {
1598
+ const appData = getAppDataPath(fs, "Claude");
1599
+ return {
1600
+ method: "config-file",
1601
+ configPath: fs.joinPath(appData, "claude_desktop_config.json"),
1602
+ serversKey: "mcpServers",
1603
+ serverName: "GitHits",
1604
+ serverConfig: {
1605
+ command: "npx",
1606
+ args: ["-y", "mcp-remote", getMcpUrl()]
1607
+ }
1608
+ };
1609
+ }
1610
+ };
1611
+ var codexCli = {
1612
+ name: "Codex CLI",
1613
+ id: "codex-cli",
1614
+ detectionMethod: "binary",
1615
+ setupMethod: "cli",
1616
+ detectBinary: async (exec) => isExecutableAvailable(exec, "codex"),
1617
+ getSetupConfig: () => ({
1618
+ method: "cli",
1619
+ commands: [
1620
+ {
1621
+ command: "codex",
1622
+ args: [
1623
+ "mcp",
1624
+ "add",
1625
+ "githits",
1626
+ "--",
1627
+ "npx",
1628
+ "-y",
1629
+ "githits@latest",
1630
+ "mcp",
1631
+ "start"
1632
+ ]
1633
+ }
1634
+ ],
1635
+ checkCommand: {
1636
+ command: "codex",
1637
+ args: ["mcp", "list"],
1638
+ configuredPattern: /githits/i
1639
+ }
1640
+ })
1641
+ };
1642
+ var vscode = {
1643
+ name: "VS Code / Copilot",
1644
+ id: "vscode",
1645
+ detectionMethod: "path",
1646
+ setupMethod: "config-file",
1647
+ detectPaths: (fs) => {
1648
+ const appData = getAppDataPath(fs, "Code");
1649
+ return [appData];
1650
+ },
1651
+ getSetupConfig: (fs) => {
1652
+ const appData = getAppDataPath(fs, "Code");
1653
+ return {
1654
+ method: "config-file",
1655
+ configPath: fs.joinPath(appData, "User", "mcp.json"),
1656
+ serversKey: "servers",
1657
+ serverName: "GitHits",
1658
+ serverConfig: { url: getMcpUrl(), type: "http" }
1659
+ };
1660
+ }
1661
+ };
1662
+ var cline = {
1663
+ name: "Cline",
1664
+ id: "cline",
1665
+ detectionMethod: "path",
1666
+ setupMethod: "config-file",
1667
+ detectPaths: (fs) => [fs.joinPath(fs.getHomeDir(), ".cline")],
1668
+ getSetupConfig: (fs) => ({
1669
+ method: "config-file",
1670
+ configPath: fs.joinPath(fs.getHomeDir(), ".cline", "data", "settings", "cline_mcp_settings.json"),
1671
+ serversKey: "mcpServers",
1672
+ serverName: "GitHits",
1673
+ serverConfig: { url: getMcpUrl(), type: "streamableHttp" }
1674
+ })
1675
+ };
1676
+ var geminiCli = {
1677
+ name: "Gemini CLI",
1678
+ id: "gemini-cli",
1679
+ detectionMethod: "binary",
1680
+ setupMethod: "cli",
1681
+ detectBinary: async (exec) => isExecutableAvailable(exec, "gemini"),
1682
+ getSetupConfig: () => ({
1683
+ method: "cli",
1684
+ commands: [
1685
+ {
1686
+ command: "gemini",
1687
+ args: [
1688
+ "extensions",
1689
+ "install",
1690
+ "https://github.com/githits-com/githits-cli"
1691
+ ]
1692
+ }
1693
+ ],
1694
+ checkCommand: {
1695
+ command: "gemini",
1696
+ args: ["extensions", "list"],
1697
+ configuredPattern: /githits/i
1698
+ }
1699
+ })
1700
+ };
1701
+ var googleAntigravity = {
1702
+ name: "Google Antigravity",
1703
+ id: "google-antigravity",
1704
+ detectionMethod: "path",
1705
+ setupMethod: "config-file",
1706
+ detectPaths: (fs) => [fs.joinPath(fs.getHomeDir(), ".gemini", "antigravity")],
1707
+ getSetupConfig: (fs) => ({
1708
+ method: "config-file",
1709
+ configPath: fs.joinPath(fs.getHomeDir(), ".gemini", "antigravity", "mcp_config.json"),
1710
+ serversKey: "mcpServers",
1711
+ serverName: "GitHits",
1712
+ serverConfig: { serverUrl: getMcpUrl() }
1713
+ })
1714
+ };
1715
+ var openCode = {
1716
+ name: "OpenCode",
1717
+ id: "opencode",
1718
+ detectionMethod: "binary",
1719
+ setupMethod: "config-file",
1720
+ detectBinary: async (exec) => isExecutableAvailable(exec, "opencode"),
1721
+ getSetupConfig: (fs) => ({
1722
+ method: "config-file",
1723
+ 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"),
1724
+ serversKey: "mcp",
1725
+ serverName: "GitHits",
1726
+ serverConfig: {
1727
+ type: "local",
1728
+ command: ["npx", "-y", "githits@latest", "mcp", "start"],
1729
+ enabled: true
1730
+ }
1731
+ })
1732
+ };
1733
+ var agentDefinitions = [
1734
+ claudeCode,
1735
+ cursor,
1736
+ windsurf,
1737
+ vscode,
1738
+ cline,
1739
+ claudeDesktop,
1740
+ codexCli,
1741
+ geminiCli,
1742
+ googleAntigravity,
1743
+ openCode
1744
+ ];
1745
+ async function scanAgents(definitions, fs, execService) {
1746
+ const result = {
1747
+ needsSetup: [],
1748
+ alreadyConfigured: [],
1749
+ notDetected: []
1750
+ };
1751
+ for (const agent of definitions) {
1752
+ let detected = false;
1753
+ if (agent.detectionMethod === "binary" && agent.detectBinary) {
1754
+ try {
1755
+ detected = await agent.detectBinary(execService);
1756
+ } catch {
1757
+ detected = false;
1758
+ }
1759
+ } else if (agent.detectionMethod === "path" && agent.detectPaths) {
1760
+ const paths = agent.detectPaths(fs);
1761
+ for (const path of paths) {
1762
+ if (await fs.isDirectory(path)) {
1763
+ detected = true;
1764
+ break;
1765
+ }
1766
+ }
1767
+ }
1768
+ if (!detected) {
1769
+ result.notDetected.push(agent);
1770
+ continue;
1771
+ }
1772
+ if (agent.setupMethod === "config-file") {
1773
+ const config = agent.getSetupConfig(fs);
1774
+ if (config.method === "config-file" && await isAlreadyConfigured(config, fs)) {
1775
+ result.alreadyConfigured.push(agent);
1776
+ } else {
1777
+ result.needsSetup.push(agent);
1778
+ }
1779
+ } else {
1780
+ const config = agent.getSetupConfig(fs);
1781
+ if (config.method === "cli" && config.checkCommand) {
1782
+ const configured = await isCliAlreadyConfigured(config.checkCommand, execService);
1783
+ if (configured) {
1784
+ result.alreadyConfigured.push(agent);
1785
+ } else {
1786
+ result.needsSetup.push(agent);
1787
+ }
1788
+ } else {
1789
+ result.needsSetup.push(agent);
1790
+ }
1791
+ }
1792
+ }
1793
+ return result;
1794
+ }
1795
+ // node_modules/@inquirer/core/dist/lib/errors.js
1796
+ class ExitPromptError extends Error {
1797
+ name = "ExitPromptError";
1798
+ }
1242
1799
  // src/shared/colors.ts
1243
1800
  var colors = {
1244
1801
  reset: "\x1B[0m",
@@ -1263,6 +1820,18 @@ function colorize(text, color, useColors) {
1263
1820
  return text;
1264
1821
  return `${colors[color]}${text}${colors.reset}`;
1265
1822
  }
1823
+ function success(text, useColors) {
1824
+ const checkmark = useColors ? `${colors.green}✓${colors.reset}` : "✓";
1825
+ return `${checkmark} ${text}`;
1826
+ }
1827
+ function error(text, useColors) {
1828
+ const cross = useColors ? `${colors.red}✗${colors.reset}` : "✗";
1829
+ return `${cross} ${text}`;
1830
+ }
1831
+ function warning(text, useColors) {
1832
+ const warn = useColors ? `${colors.yellow}⚠${colors.reset}` : "⚠";
1833
+ return `${warn} ${text}`;
1834
+ }
1266
1835
  function highlight(text, useColors) {
1267
1836
  if (!useColors)
1268
1837
  return text;
@@ -1274,76 +1843,24 @@ function dim(text, useColors) {
1274
1843
  return `${colors.dim}${text}${colors.reset}`;
1275
1844
  }
1276
1845
 
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
1846
  // src/commands/login.ts
1327
1847
  var TIMEOUT_MS = 5 * 60 * 1000;
1328
1848
  function randomPort() {
1329
1849
  return Math.floor(Math.random() * 2000) + 8000;
1330
1850
  }
1331
- async function loginAction(options, deps) {
1851
+ async function loginFlow(options, deps) {
1332
1852
  const { authService, authStorage, browserService, mcpUrl } = deps;
1333
1853
  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);
1854
+ return {
1855
+ status: "failed",
1856
+ message: "Invalid port number. Must be between 1 and 65535."
1857
+ };
1336
1858
  }
1337
1859
  const existing = await authStorage.loadTokens(mcpUrl);
1338
1860
  if (existing && !options.force) {
1339
1861
  const isExpired = existing.expiresAt && new Date(existing.expiresAt) < new Date;
1340
1862
  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;
1863
+ return { status: "already_authenticated", message: "Already logged in." };
1347
1864
  }
1348
1865
  console.log(`Token expired. Starting new login...
1349
1866
  `);
@@ -1427,22 +1944,18 @@ async function loginAction(options, deps) {
1427
1944
  callback = await Promise.race([serverPromise, timeoutPromise]);
1428
1945
  if (timeoutId)
1429
1946
  clearTimeout(timeoutId);
1430
- } catch (error) {
1947
+ } catch (error2) {
1431
1948
  if (timeoutId)
1432
1949
  clearTimeout(timeoutId);
1433
- if (error instanceof Error) {
1434
- console.log(`${error.message}.
1435
- `);
1436
- console.log("Run `githits login` to try again.");
1437
- }
1438
- process.exit(1);
1950
+ const msg = error2 instanceof Error ? error2.message : "Authentication failed";
1951
+ return { status: "failed", message: `${msg}.` };
1439
1952
  }
1440
1953
  if (callback.type !== "success") {
1441
- console.log(`${callback.message}
1442
- `);
1443
- console.log("Run `githits login` to try again.");
1444
1954
  await new Promise((r) => setTimeout(r, 2000));
1445
- process.exit(1);
1955
+ return {
1956
+ status: "failed",
1957
+ message: callback.message ?? "Authentication callback failed."
1958
+ };
1446
1959
  }
1447
1960
  let tokenResponse;
1448
1961
  try {
@@ -1454,14 +1967,15 @@ async function loginAction(options, deps) {
1454
1967
  codeVerifier: verifier,
1455
1968
  redirectUri
1456
1969
  });
1457
- } catch (error) {
1970
+ } catch (error2) {
1458
1971
  try {
1459
1972
  await authStorage.clearClient(mcpUrl);
1460
1973
  } catch {}
1461
- console.error(`Failed to complete authentication: ${error instanceof Error ? error.message : error}
1462
- `);
1463
- console.log("Run `githits login` to try again.");
1464
- process.exit(1);
1974
+ const msg = error2 instanceof Error ? error2.message : String(error2);
1975
+ return {
1976
+ status: "failed",
1977
+ message: `Failed to complete authentication: ${msg}`
1978
+ };
1465
1979
  }
1466
1980
  const expiresAt = new Date(Date.now() + tokenResponse.expiresIn * 1000).toISOString();
1467
1981
  await authStorage.saveTokens(mcpUrl, {
@@ -1471,10 +1985,31 @@ async function loginAction(options, deps) {
1471
1985
  createdAt: new Date().toISOString()
1472
1986
  });
1473
1987
  const hours = Math.round(tokenResponse.expiresIn / 3600);
1988
+ return {
1989
+ status: "success",
1990
+ message: `Logged in successfully. Token expires in ${hours} hour${hours !== 1 ? "s" : ""}.`
1991
+ };
1992
+ }
1993
+ async function loginAction(options, deps) {
1994
+ const result = await loginFlow(options, deps);
1995
+ if (result.status === "already_authenticated") {
1996
+ console.log(`Already logged in.
1997
+ `);
1998
+ console.log(` Environment: ${deps.mcpUrl}
1999
+ `);
2000
+ console.log("To re-authenticate, use `githits login --force`.");
2001
+ return;
2002
+ }
2003
+ if (result.status === "failed") {
2004
+ console.error(`${result.message}
2005
+ `);
2006
+ console.log("Run `githits login` to try again.");
2007
+ process.exit(1);
2008
+ }
1474
2009
  console.log(`Logged in successfully.
1475
2010
  `);
1476
- console.log(` Environment: ${mcpUrl}`);
1477
- console.log(` Token expires in: ${hours} hour${hours !== 1 ? "s" : ""}`);
2011
+ console.log(` Environment: ${deps.mcpUrl}`);
2012
+ console.log(result.message.replace("Logged in successfully. ", " "));
1478
2013
  console.log(`
1479
2014
  You're ready to use githits with your AI assistant.`);
1480
2015
  }
@@ -1491,6 +2026,217 @@ function registerLoginCommand(program) {
1491
2026
  await loginAction(options, deps);
1492
2027
  });
1493
2028
  }
2029
+
2030
+ // src/commands/init/init.ts
2031
+ async function initAction(options, deps) {
2032
+ const useColors = shouldUseColors();
2033
+ const { fileSystemService, promptService, execService, createLoginDeps } = deps;
2034
+ console.log(`
2035
+ ${colorize("GitHits", "bold", useColors)} — Set up MCP server for your coding agents
2036
+ `);
2037
+ if (!options.skipLogin && createLoginDeps) {
2038
+ console.log(` Checking authentication...
2039
+ `);
2040
+ let loginResult;
2041
+ try {
2042
+ const loginDeps = await createLoginDeps();
2043
+ loginResult = await loginFlow({}, loginDeps);
2044
+ } catch (error2) {
2045
+ const msg = error2 instanceof Error ? error2.message : String(error2);
2046
+ loginResult = { status: "failed", message: msg };
2047
+ }
2048
+ if (loginResult.status === "already_authenticated") {
2049
+ console.log(` ${success("Already authenticated", useColors)}
2050
+ `);
2051
+ } else if (loginResult.status === "success") {
2052
+ console.log(` ${success("Logged in successfully", useColors)}
2053
+ `);
2054
+ } else {
2055
+ console.log(` ${warning(`Login failed: ${loginResult.message}`, useColors)}
2056
+ `);
2057
+ if (!options.yes) {
2058
+ try {
2059
+ const choice = await promptService.confirm3("Continue without authentication?");
2060
+ if (choice === "no") {
2061
+ console.log("\n Setup cancelled. Run `githits login` to authenticate.\n");
2062
+ return;
2063
+ }
2064
+ } catch (err) {
2065
+ if (err instanceof ExitPromptError) {
2066
+ console.log(`
2067
+ Setup cancelled.
2068
+ `);
2069
+ return;
2070
+ }
2071
+ throw err;
2072
+ }
2073
+ }
2074
+ console.log(` Continuing without authentication...
2075
+ `);
2076
+ }
2077
+ }
2078
+ console.log(` Scanning for available agents...
2079
+ `);
2080
+ const scan = await scanAgents(agentDefinitions, fileSystemService, execService);
2081
+ for (const agent of scan.alreadyConfigured) {
2082
+ console.log(` ${success(`${agent.name} — already configured`, useColors)}`);
2083
+ }
2084
+ for (const agent of scan.needsSetup) {
2085
+ console.log(` ${colorize(`● ${agent.name} — needs setup`, "cyan", useColors)}`);
2086
+ }
2087
+ for (const agent of scan.notDetected) {
2088
+ console.log(` ${colorize(`${agent.name} — not detected`, "dim", useColors)}`);
2089
+ }
2090
+ console.log();
2091
+ if (scan.needsSetup.length === 0 && scan.alreadyConfigured.length === 0) {
2092
+ console.log(` No coding agents detected. Install an agent and try again.
2093
+ `);
2094
+ return;
2095
+ }
2096
+ if (scan.needsSetup.length === 0) {
2097
+ console.log(` All detected agents are already configured. Nothing to do.
2098
+ `);
2099
+ return;
2100
+ }
2101
+ const toSetup = scan.needsSetup;
2102
+ const outcomes = [];
2103
+ let alwaysMode = options.yes ?? false;
2104
+ for (const agent of toSetup) {
2105
+ console.log(` Setting up ${colorize(agent.name, "bold", useColors)}...
2106
+ `);
2107
+ const config = agent.getSetupConfig(fileSystemService);
2108
+ const preview = formatSetupPreview(config);
2109
+ for (const line of preview.split(`
2110
+ `)) {
2111
+ console.log(` ${line}`);
2112
+ }
2113
+ console.log();
2114
+ if (!alwaysMode) {
2115
+ let choice;
2116
+ try {
2117
+ choice = await promptService.confirm3("Proceed?");
2118
+ } catch (err) {
2119
+ if (err instanceof ExitPromptError) {
2120
+ console.log(`
2121
+ Setup cancelled.
2122
+ `);
2123
+ return;
2124
+ }
2125
+ throw err;
2126
+ }
2127
+ if (choice === "no") {
2128
+ outcomes.push({ name: agent.name, status: "skipped" });
2129
+ console.log();
2130
+ continue;
2131
+ }
2132
+ if (choice === "always") {
2133
+ alwaysMode = true;
2134
+ }
2135
+ }
2136
+ const result = config.method === "cli" ? await executeCliSetup(config, execService) : await executeConfigFileSetup(config, fileSystemService);
2137
+ outcomes.push({ name: agent.name, status: result.status });
2138
+ if (result.status === "success") {
2139
+ console.log(` ${success(`${agent.name} configured`, useColors)}
2140
+ `);
2141
+ } else if (result.status === "already_configured") {
2142
+ console.log(` ${warning(`${agent.name} already configured`, useColors)}
2143
+ `);
2144
+ } else {
2145
+ console.log(` ${error(result.message, useColors)}
2146
+ `);
2147
+ }
2148
+ }
2149
+ const configured = outcomes.filter((o) => o.status === "success").length;
2150
+ const alreadyDone = outcomes.filter((o) => o.status === "already_configured").length + scan.alreadyConfigured.length;
2151
+ const failed = outcomes.filter((o) => o.status === "failed").length;
2152
+ const skipped = outcomes.filter((o) => o.status === "skipped").length;
2153
+ if (configured > 0 || alreadyDone > 0) {
2154
+ console.log(" Done! GitHits is ready.");
2155
+ } else if (failed > 0) {
2156
+ console.log(" Setup completed with errors.");
2157
+ } else if (skipped > 0) {
2158
+ console.log(" Setup skipped.");
2159
+ }
2160
+ if (failed > 0) {
2161
+ console.log(` ${failed} agent${failed !== 1 ? "s" : ""} failed to configure.`);
2162
+ }
2163
+ if (skipped > 0) {
2164
+ console.log(` ${skipped} agent${skipped !== 1 ? "s" : ""} skipped.`);
2165
+ }
2166
+ console.log();
2167
+ }
2168
+ var INIT_DESCRIPTION = `Set up GitHits MCP server for your coding agents.
2169
+
2170
+ Authenticates with your GitHits account, then scans for available agents
2171
+ (Claude Code, Cursor, Windsurf, VS Code, Cline, Claude Desktop, Codex CLI,
2172
+ Gemini CLI, Google Antigravity), checks which are already configured,
2173
+ and sets up unconfigured ones with your confirmation.
2174
+
2175
+ Supports CLI-based setup (Claude Code, Codex, Gemini CLI) and config
2176
+ file editing (Cursor, Windsurf, VS Code, Cline, Claude Desktop,
2177
+ Google Antigravity) with atomic writes.`;
2178
+ function registerInitCommand(program) {
2179
+ 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) => {
2180
+ const fileSystemService = new FileSystemServiceImpl;
2181
+ const promptService = new PromptServiceImpl;
2182
+ const execService = new ExecServiceImpl;
2183
+ await initAction(options, {
2184
+ fileSystemService,
2185
+ promptService,
2186
+ execService,
2187
+ createLoginDeps: () => createContainer()
2188
+ });
2189
+ });
2190
+ }
2191
+ // src/shared/language-filter.ts
2192
+ var DEFAULT_LIMIT = 5;
2193
+ function filterLanguages(languages, query, limit = DEFAULT_LIMIT) {
2194
+ const lowerQuery = query.toLowerCase();
2195
+ 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 }));
2196
+ }
2197
+
2198
+ // src/commands/languages.ts
2199
+ async function languagesAction(query, options, deps) {
2200
+ requireAuth(deps);
2201
+ try {
2202
+ const allLanguages = await deps.githitsService.getLanguages();
2203
+ const displayList = query ? filterLanguages(allLanguages, query) : allLanguages.map(({ name, display_name }) => ({ name, display_name }));
2204
+ if (options.json) {
2205
+ console.log(JSON.stringify(displayList));
2206
+ } else if (query && displayList.length === 0) {
2207
+ console.log(`No languages matching "${query}".`);
2208
+ } else {
2209
+ const useColors = shouldUseColors();
2210
+ for (const lang of displayList) {
2211
+ console.log(` ${colorize(lang.name, "cyan", useColors)} ${dim(lang.display_name, useColors)}`);
2212
+ }
2213
+ }
2214
+ } catch (error2) {
2215
+ console.error(`Failed to list languages: ${error2 instanceof Error ? error2.message : error2}`);
2216
+ process.exit(1);
2217
+ }
2218
+ }
2219
+ var LANGUAGES_DESCRIPTION = `List supported programming languages.
2220
+
2221
+ Without a query, lists all supported languages.
2222
+ With a query, filters to the top 5 matches by name, display name, or alias.
2223
+
2224
+ Examples:
2225
+ githits languages List all languages
2226
+ githits languages python Filter by name
2227
+ githits languages type --json JSON output for piping`;
2228
+ function registerLanguagesCommand(program) {
2229
+ 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) => {
2230
+ try {
2231
+ const deps = await createContainer();
2232
+ await languagesAction(query, options, deps);
2233
+ } catch (error2) {
2234
+ if (error2 instanceof AuthRequiredError)
2235
+ process.exit(1);
2236
+ throw error2;
2237
+ }
2238
+ });
2239
+ }
1494
2240
  // src/commands/logout.ts
1495
2241
  async function logoutAction(deps) {
1496
2242
  const { authStorage, mcpUrl } = deps;
@@ -1498,13 +2244,13 @@ async function logoutAction(deps) {
1498
2244
  let firstError;
1499
2245
  try {
1500
2246
  await authStorage.clearTokens(mcpUrl);
1501
- } catch (error) {
1502
- firstError = error;
2247
+ } catch (error2) {
2248
+ firstError = error2;
1503
2249
  }
1504
2250
  try {
1505
2251
  await authStorage.clearClient(mcpUrl);
1506
- } catch (error) {
1507
- firstError ??= error;
2252
+ } catch (error2) {
2253
+ firstError ??= error2;
1508
2254
  }
1509
2255
  if (!auth) {
1510
2256
  console.log(`Not currently logged in.
@@ -1553,8 +2299,8 @@ function errorResult(message) {
1553
2299
  async function withErrorHandling(operation, fn) {
1554
2300
  try {
1555
2301
  return await fn();
1556
- } catch (error) {
1557
- const message = error instanceof Error ? error.message : "Unknown error";
2302
+ } catch (error2) {
2303
+ const message = error2 instanceof Error ? error2.message : "Unknown error";
1558
2304
  return errorResult(`Failed to ${operation}: ${message}`);
1559
2305
  }
1560
2306
  }
@@ -1775,8 +2521,8 @@ async function searchAction(query, options, deps) {
1775
2521
  } else {
1776
2522
  console.log(result);
1777
2523
  }
1778
- } catch (error) {
1779
- console.error(`Failed to search: ${error instanceof Error ? error.message : error}`);
2524
+ } catch (error2) {
2525
+ console.error(`Failed to search: ${error2 instanceof Error ? error2.message : error2}`);
1780
2526
  process.exit(1);
1781
2527
  }
1782
2528
  }
@@ -1796,10 +2542,10 @@ function registerSearchCommand(program) {
1796
2542
  try {
1797
2543
  const deps = await createContainer();
1798
2544
  await searchAction(query, options, deps);
1799
- } catch (error) {
1800
- if (error instanceof AuthRequiredError)
2545
+ } catch (error2) {
2546
+ if (error2 instanceof AuthRequiredError)
1801
2547
  process.exit(1);
1802
- throw error;
2548
+ throw error2;
1803
2549
  }
1804
2550
  });
1805
2551
  }
@@ -1811,6 +2557,7 @@ program.name("githits").description("Code examples from global open source for y
1811
2557
  }
1812
2558
  }).addHelpText("after", `
1813
2559
  Getting started:
2560
+ githits init Set up MCP for your coding agents
1814
2561
  githits login Authenticate with your GitHits account
1815
2562
  githits mcp Start MCP server for your AI assistant
1816
2563
  githits search "query" --lang python Search for code examples
@@ -1818,6 +2565,7 @@ Getting started:
1818
2565
  Learn more at https://githits.com
1819
2566
  Docs: https://app.githits.com/docs/
1820
2567
  Support: support@githits.com`);
2568
+ registerInitCommand(program);
1821
2569
  registerLoginCommand(program);
1822
2570
  registerLogoutCommand(program);
1823
2571
  registerMcpCommand(program);