@nalvietnam/avatar-cli 1.3.3 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -134,8 +134,8 @@ async function writeUserConfig(config) {
134
134
  }
135
135
  async function clearUserConfig() {
136
136
  if (await pathExists(USER_CONFIG_PATH)) {
137
- const { promises: fs9 } = await import("fs");
138
- await fs9.unlink(USER_CONFIG_PATH);
137
+ const { promises: fs11 } = await import("fs");
138
+ await fs11.unlink(USER_CONFIG_PATH);
139
139
  }
140
140
  }
141
141
  function isTokenExpired(config) {
@@ -1306,14 +1306,196 @@ async function applyFixes(checks) {
1306
1306
  if (count === 0) log.dim("Kh\xF4ng c\xF3 g\xEC \u0111\u1EC3 fix t\u1EF1 \u0111\u1ED9ng.");
1307
1307
  }
1308
1308
 
1309
- // src/commands/init.ts
1310
- import { basename, join as join18, relative as relative2, resolve } from "path";
1311
- import { confirm as confirm3, input as input5, select as select8 } from "@inquirer/prompts";
1312
- import boxen4 from "boxen";
1309
+ // src/commands/gitnexus.ts
1310
+ import { spawnSync as spawnSync11 } from "child_process";
1311
+ import { promises as fs8 } from "fs";
1312
+ import { join as join15 } from "path";
1313
1313
 
1314
- // src/lib/add-team-pack-submodule-with-retry-on-network-fail.ts
1314
+ // src/lib/run-gitnexus-setup-and-analyze.ts
1315
+ import { spawnSync as spawnSync7 } from "child_process";
1316
+ import { existsSync as existsSync4 } from "fs";
1317
+ import { join as join12 } from "path";
1318
+ var SETUP_TIMEOUT_MS = 2 * 60 * 1e3;
1319
+ var ANALYZE_TIMEOUT_MS = 5 * 60 * 1e3;
1320
+ var GitnexusOperationError = class extends Error {
1321
+ operation;
1322
+ reason;
1323
+ exitCode;
1324
+ stderr;
1325
+ constructor(operation, reason, message, exitCode = null, stderr) {
1326
+ super(message);
1327
+ this.name = "GitnexusOperationError";
1328
+ this.operation = operation;
1329
+ this.reason = reason;
1330
+ this.exitCode = exitCode;
1331
+ this.stderr = stderr;
1332
+ }
1333
+ };
1334
+ function classifyOperationFailure(operation, exitCode, signal, stderrSample) {
1335
+ if (signal === "SIGTERM") {
1336
+ return new GitnexusOperationError(
1337
+ operation,
1338
+ "timeout",
1339
+ `gitnexus ${operation} timeout. Check m\u1EA1ng / repo size.`,
1340
+ null,
1341
+ stderrSample
1342
+ );
1343
+ }
1344
+ const stderr = stderrSample.toLowerCase();
1345
+ if (stderr.includes("eacces") || stderr.includes("permission denied")) {
1346
+ return new GitnexusOperationError(
1347
+ operation,
1348
+ "permission",
1349
+ `gitnexus ${operation} fail (permission). Check write access ~/.claude ho\u1EB7c cwd.`,
1350
+ exitCode,
1351
+ stderrSample
1352
+ );
1353
+ }
1354
+ return new GitnexusOperationError(
1355
+ operation,
1356
+ "non-zero-exit",
1357
+ `gitnexus ${operation} exit ${exitCode ?? "null"}. Xem log ph\xEDa tr\xEAn.`,
1358
+ exitCode,
1359
+ stderrSample
1360
+ );
1361
+ }
1362
+ function runGitnexusSetup() {
1363
+ log.info("Setup GitNexus global skills (~/.claude/skills/gitnexus-*)...");
1364
+ const result = spawnSync7("gitnexus", ["setup"], {
1365
+ stdio: ["inherit", "inherit", "pipe"],
1366
+ timeout: SETUP_TIMEOUT_MS,
1367
+ encoding: "utf8"
1368
+ });
1369
+ if (result.status !== 0 || result.signal === "SIGTERM") {
1370
+ const stderr = (result.stderr || "").trim();
1371
+ if (stderr) process.stderr.write(`${stderr}
1372
+ `);
1373
+ throw classifyOperationFailure("setup", result.status, result.signal, stderr);
1374
+ }
1375
+ log.success("GitNexus setup OK (global skills installed)");
1376
+ }
1377
+ function runGitnexusAnalyze(workspacePath) {
1378
+ log.info(`Analyze workspace ${workspacePath} (c\xF3 th\u1EC3 1-3 ph\xFAt)...`);
1379
+ const result = spawnSync7("gitnexus", ["analyze", "."], {
1380
+ cwd: workspacePath,
1381
+ stdio: ["inherit", "inherit", "pipe"],
1382
+ timeout: ANALYZE_TIMEOUT_MS,
1383
+ encoding: "utf8"
1384
+ });
1385
+ if (result.status !== 0 || result.signal === "SIGTERM") {
1386
+ const stderr = (result.stderr || "").trim();
1387
+ if (stderr) process.stderr.write(`${stderr}
1388
+ `);
1389
+ throw classifyOperationFailure("analyze", result.status, result.signal, stderr);
1390
+ }
1391
+ const metaPath = join12(workspacePath, ".gitnexus", "meta.json");
1392
+ if (!existsSync4(metaPath)) {
1393
+ throw new GitnexusOperationError(
1394
+ "analyze",
1395
+ "missing-output",
1396
+ `gitnexus analyze xong nh\u01B0ng kh\xF4ng th\u1EA5y ${metaPath}. Repo c\xF3 th\u1EC3 empty ho\u1EB7c gitnexus fail silent.`
1397
+ );
1398
+ }
1399
+ log.success(`Analyze OK (index t\u1EA1i ${join12(workspacePath, ".gitnexus")})`);
1400
+ }
1401
+
1402
+ // src/lib/run-gitnexus-setup-phase.ts
1403
+ import { confirm as confirm3 } from "@inquirer/prompts";
1404
+ import boxen2 from "boxen";
1405
+
1406
+ // src/lib/detect-gitnexus-installation.ts
1315
1407
  import { spawnSync as spawnSync8 } from "child_process";
1316
- import { select as select5 } from "@inquirer/prompts";
1408
+ var VERSION_PROBE_TIMEOUT_MS2 = 5e3;
1409
+ var SEMVER_REGEX2 = /(\d+\.\d+\.\d+)/;
1410
+ function probeGitnexusBinaryPath() {
1411
+ const isWindows = detectHostPlatform() === "win32";
1412
+ const probeCmd = isWindows ? "where" : "which";
1413
+ const result = spawnSync8(probeCmd, ["gitnexus"], { encoding: "utf8" });
1414
+ if (result.error || result.status !== 0) return null;
1415
+ const out = (result.stdout || "").trim();
1416
+ if (!out) return null;
1417
+ return out.split(/\r?\n/)[0].trim();
1418
+ }
1419
+ function probeGitnexusVersion() {
1420
+ const result = spawnSync8("gitnexus", ["--version"], {
1421
+ encoding: "utf8",
1422
+ timeout: VERSION_PROBE_TIMEOUT_MS2
1423
+ });
1424
+ if (result.error || result.status !== 0) return null;
1425
+ const out = (result.stdout || "").trim();
1426
+ const match = SEMVER_REGEX2.exec(out);
1427
+ return match ? match[1] : null;
1428
+ }
1429
+ function detectGitnexusInstallation() {
1430
+ const path = probeGitnexusBinaryPath();
1431
+ if (!path) {
1432
+ return { installed: false, version: null, path: null };
1433
+ }
1434
+ const version = probeGitnexusVersion();
1435
+ return { installed: true, version, path };
1436
+ }
1437
+
1438
+ // src/lib/install-gitnexus-via-npm.ts
1439
+ import { spawnSync as spawnSync9 } from "child_process";
1440
+ var NPM_INSTALL_TIMEOUT_MS2 = 5 * 60 * 1e3;
1441
+ var GITNEXUS_PACKAGE = "gitnexus";
1442
+ var InstallGitnexusError = class extends Error {
1443
+ reason;
1444
+ exitCode;
1445
+ constructor(reason, message, exitCode = null) {
1446
+ super(message);
1447
+ this.name = "InstallGitnexusError";
1448
+ this.reason = reason;
1449
+ this.exitCode = exitCode;
1450
+ }
1451
+ };
1452
+ function classifyNpmFailure2(exitCode, stderrSample) {
1453
+ const stderr = stderrSample.toLowerCase();
1454
+ if (stderr.includes("eacces") || stderr.includes("permission denied")) {
1455
+ return new InstallGitnexusError(
1456
+ "permission-denied",
1457
+ `npm install -g c\u1EA7n quy\u1EC1n. Th\u1EED: sudo npm install -g ${GITNEXUS_PACKAGE} ho\u1EB7c fix npm prefix (npm config set prefix ~/.npm-global).`,
1458
+ exitCode
1459
+ );
1460
+ }
1461
+ if (stderr.includes("enospc") || stderr.includes("no space")) {
1462
+ return new InstallGitnexusError("disk-full", "\u0110\u0129a \u0111\u1EA7y. Free disk space r\u1ED3i th\u1EED l\u1EA1i.", exitCode);
1463
+ }
1464
+ return new InstallGitnexusError(
1465
+ "generic",
1466
+ `npm install th\u1EA5t b\u1EA1i (exit ${exitCode ?? "null"}). Xem log npm ph\xEDa tr\xEAn.`,
1467
+ exitCode
1468
+ );
1469
+ }
1470
+ function installGitnexusViaNpm() {
1471
+ log.info("\u0110ang c\xE0i GitNexus qua npm (c\xF3 th\u1EC3 m\u1EA5t 1-2 ph\xFAt)...");
1472
+ const result = spawnSync9("npm", ["install", "-g", GITNEXUS_PACKAGE], {
1473
+ stdio: ["inherit", "inherit", "pipe"],
1474
+ timeout: NPM_INSTALL_TIMEOUT_MS2,
1475
+ encoding: "utf8"
1476
+ });
1477
+ if (result.signal === "SIGTERM") {
1478
+ throw new InstallGitnexusError(
1479
+ "timeout",
1480
+ `npm install timeout sau ${NPM_INSTALL_TIMEOUT_MS2 / 1e3}s. Check m\u1EA1ng r\u1ED3i th\u1EED l\u1EA1i.`,
1481
+ null
1482
+ );
1483
+ }
1484
+ if (result.status !== 0) {
1485
+ if (result.stderr) process.stderr.write(result.stderr);
1486
+ throw classifyNpmFailure2(result.status, result.stderr || "");
1487
+ }
1488
+ const probe = detectGitnexusInstallation();
1489
+ if (!probe.installed || !probe.path) {
1490
+ throw new InstallGitnexusError(
1491
+ "binary-not-in-path",
1492
+ "npm c\xE0i xong nh\u01B0ng `gitnexus` kh\xF4ng trong PATH. Reload shell (source ~/.zshrc) ho\u1EB7c th\xEAm npm global bin v\xE0o PATH.",
1493
+ null
1494
+ );
1495
+ }
1496
+ log.success(`\u0110\xE3 c\xE0i GitNexus${probe.version ? ` v${probe.version}` : ""} t\u1EA1i ${probe.path}`);
1497
+ return { version: probe.version, path: probe.path };
1498
+ }
1317
1499
 
1318
1500
  // src/lib/prompt-recovery-action-on-failure.ts
1319
1501
  import { input as input3, select as select3 } from "@inquirer/prompts";
@@ -1339,24 +1521,408 @@ async function promptRetryOrSkip(args) {
1339
1521
  });
1340
1522
  }
1341
1523
 
1524
+ // src/lib/register-gitnexus-mcp-server.ts
1525
+ import { promises as fs7 } from "fs";
1526
+ import { homedir as homedir3 } from "os";
1527
+ import { join as join13 } from "path";
1528
+ var MCP_FILE_MODE = 384;
1529
+ var EXPECTED_GITNEXUS_ENTRY = {
1530
+ command: "gitnexus",
1531
+ args: ["mcp"]
1532
+ };
1533
+ function getMcpServersPath() {
1534
+ return join13(homedir3(), ".claude", "mcp_servers.json");
1535
+ }
1536
+ function isEntryEqual(a, b) {
1537
+ if (a === b) return true;
1538
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
1539
+ return JSON.stringify(a) === JSON.stringify(b);
1540
+ }
1541
+ async function backupExistingFile(path) {
1542
+ const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
1543
+ const backupPath = `${path}.avatar-backup-${ts}`;
1544
+ await fs7.copyFile(path, backupPath);
1545
+ return backupPath;
1546
+ }
1547
+ async function registerGitnexusMcpServer() {
1548
+ const path = getMcpServersPath();
1549
+ let existing = {};
1550
+ let fileExisted = false;
1551
+ if (await pathExists(path)) {
1552
+ fileExisted = true;
1553
+ try {
1554
+ existing = await readJson(path);
1555
+ } catch (err) {
1556
+ throw new Error(
1557
+ `MCP config corrupted (${path}): ${err.message}. Backup + x\xF3a file \u0111\u1EC3 Avatar t\u1EA1o l\u1EA1i.`
1558
+ );
1559
+ }
1560
+ }
1561
+ const existingEntry = existing.mcp_servers?.gitnexus;
1562
+ if (existingEntry && isEntryEqual(existingEntry, EXPECTED_GITNEXUS_ENTRY)) {
1563
+ log.dim(`MCP entry gitnexus \u0111\xE3 \u0111\xFAng t\u1EA1i ${path} (no-op)`);
1564
+ return { path, wasUpdated: false };
1565
+ }
1566
+ let backup;
1567
+ if (fileExisted) {
1568
+ backup = await backupExistingFile(path);
1569
+ log.dim(`Backup ${path} \u2192 ${backup}`);
1570
+ }
1571
+ const merged = {
1572
+ ...existing,
1573
+ mcp_servers: {
1574
+ ...existing.mcp_servers || {},
1575
+ gitnexus: EXPECTED_GITNEXUS_ENTRY
1576
+ }
1577
+ };
1578
+ await writeJsonAtomic(path, merged, MCP_FILE_MODE);
1579
+ try {
1580
+ await fs7.chmod(path, MCP_FILE_MODE);
1581
+ } catch {
1582
+ }
1583
+ log.success(`Registered MCP server: gitnexus \u2192 ${path}`);
1584
+ return { path, wasUpdated: true, backup };
1585
+ }
1586
+
1587
+ // src/lib/run-gitnexus-wiki-conditional.ts
1588
+ import { spawnSync as spawnSync10 } from "child_process";
1589
+ import { existsSync as existsSync5 } from "fs";
1590
+ import { join as join14 } from "path";
1591
+ import { confirm as confirm2 } from "@inquirer/prompts";
1592
+ var WIKI_TIMEOUT_MS = 15 * 60 * 1e3;
1593
+ async function readSettingsForWikiCredentials(workspacePath) {
1594
+ const settingsPath = join14(workspacePath, ".claude", "settings.json");
1595
+ if (!await pathExists(settingsPath)) return null;
1596
+ try {
1597
+ const settings = await readJson(settingsPath);
1598
+ const env = settings.env || {};
1599
+ const apiKey = typeof env.ANTHROPIC_AUTH_TOKEN === "string" ? env.ANTHROPIC_AUTH_TOKEN : null;
1600
+ const baseUrl = typeof env.ANTHROPIC_BASE_URL === "string" ? env.ANTHROPIC_BASE_URL : null;
1601
+ if (!apiKey || !baseUrl) return null;
1602
+ return { apiKey, baseUrl };
1603
+ } catch {
1604
+ return null;
1605
+ }
1606
+ }
1607
+ async function confirmWikiGeneration(baseUrl) {
1608
+ return await confirm2({
1609
+ message: `Generate wiki cho workspace? (~$0.50 qua ${baseUrl}, 2-5 ph\xFAt). Continue?`,
1610
+ default: true
1611
+ });
1612
+ }
1613
+ async function runGitnexusWikiConditional(workspacePath) {
1614
+ const creds = await readSettingsForWikiCredentials(workspacePath);
1615
+ if (!creds) {
1616
+ log.warn("Subscription mode (ho\u1EB7c settings.json kh\xF4ng c\xF3 LLMLite key) \u2192 skip wiki gen.");
1617
+ log.dim("\u0110\u1EC3 gen wiki sau, ch\u1EA1y manual: gitnexus wiki . --api-key <openai-key>");
1618
+ return { ran: false, skipped: true, reason: "subscription-mode" };
1619
+ }
1620
+ const proceed = await confirmWikiGeneration(creds.baseUrl);
1621
+ if (!proceed) {
1622
+ log.dim(
1623
+ "User decline wiki gen \u2014 workspace OK kh\xF4ng c\xF3 wiki. Ch\u1EA1y `gitnexus wiki` manual sau khi c\u1EA7n."
1624
+ );
1625
+ return { ran: false, skipped: true, reason: "user-declined" };
1626
+ }
1627
+ log.info(`Generating wiki via ${creds.baseUrl} (2-5 ph\xFAt)...`);
1628
+ const result = spawnSync10(
1629
+ "gitnexus",
1630
+ ["wiki", ".", "--api-key", creds.apiKey, "--base-url", creds.baseUrl],
1631
+ {
1632
+ cwd: workspacePath,
1633
+ stdio: ["inherit", "inherit", "pipe"],
1634
+ timeout: WIKI_TIMEOUT_MS,
1635
+ encoding: "utf8"
1636
+ }
1637
+ );
1638
+ if (result.status !== 0 || result.signal === "SIGTERM") {
1639
+ const stderr = (result.stderr || "").trim();
1640
+ if (stderr) process.stderr.write(`${stderr}
1641
+ `);
1642
+ const reason = result.signal === "SIGTERM" ? "timeout" : "non-zero-exit";
1643
+ return {
1644
+ ran: false,
1645
+ skipped: true,
1646
+ reason: "fail",
1647
+ detail: `Wiki gen ${reason} (exit ${result.status ?? "null"})`
1648
+ };
1649
+ }
1650
+ const wikiPath = join14(workspacePath, ".gitnexus", "wiki", "index.html");
1651
+ if (!existsSync5(wikiPath)) {
1652
+ return {
1653
+ ran: false,
1654
+ skipped: true,
1655
+ reason: "fail",
1656
+ detail: `Wiki exit 0 nh\u01B0ng kh\xF4ng th\u1EA5y ${wikiPath}`
1657
+ };
1658
+ }
1659
+ log.success(`Wiki ready: ${wikiPath}`);
1660
+ return { ran: true, skipped: false, wikiPath };
1661
+ }
1662
+
1663
+ // src/lib/run-gitnexus-setup-phase.ts
1664
+ async function promptInstallGitnexus() {
1665
+ const lines = [
1666
+ chalk.bold("\u{1F9E0} GitNexus ch\u01B0a c\xE0i"),
1667
+ "",
1668
+ "GitNexus = code intelligence layer cho Claude Code:",
1669
+ " \u2022 Architectural awareness (impact analysis)",
1670
+ " \u2022 Call chain debug + blast radius tr\u01B0\u1EDBc refactor",
1671
+ " \u2022 Wiki HTML t\u1EF1 gen m\xF4 t\u1EA3 codebase",
1672
+ "",
1673
+ `S\u1EBD c\xE0i: ${chalk.cyan("npm install -g gitnexus")} (global)`
1674
+ ];
1675
+ process.stdout.write(
1676
+ `${boxen2(lines.join("\n"), { padding: 1, borderStyle: "round", borderColor: "cyan" })}
1677
+ `
1678
+ );
1679
+ return await confirm3({ message: "C\xE0i GitNexus global?", default: true });
1680
+ }
1681
+ async function installWithRecovery() {
1682
+ while (true) {
1683
+ try {
1684
+ installGitnexusViaNpm();
1685
+ return true;
1686
+ } catch (err) {
1687
+ const message = err instanceof Error ? err.message : String(err);
1688
+ const hint = err instanceof InstallGitnexusError && err.reason === "permission-denied" ? "Th\u1EED l\u1EA1i v\u1EDBi sudo, ho\u1EB7c fix npm prefix: npm config set prefix ~/.npm-global" : "Check log npm ph\xEDa tr\xEAn + th\u1EED l\u1EA1i.";
1689
+ const action = await promptRetryOrSkip({
1690
+ taskName: "C\xE0i GitNexus qua npm",
1691
+ reason: message,
1692
+ allowSkip: true,
1693
+ hint
1694
+ });
1695
+ if (action === "abort") {
1696
+ throw new UserAbortedRecoveryError("User abort t\u1EA1i b\u01B0\u1EDBc c\xE0i GitNexus.");
1697
+ }
1698
+ if (action === "skip") return false;
1699
+ }
1700
+ }
1701
+ }
1702
+ async function analyzeWithRecovery(workspacePath) {
1703
+ while (true) {
1704
+ try {
1705
+ runGitnexusAnalyze(workspacePath);
1706
+ return true;
1707
+ } catch (err) {
1708
+ const message = err instanceof Error ? err.message : String(err);
1709
+ const hint = err instanceof GitnexusOperationError && err.reason === "missing-output" ? "Repo c\xF3 th\u1EC3 empty ho\u1EB7c gitnexus version mismatch. Check `gitnexus --version`." : "Network glitch? Retry th\u01B0\u1EDDng work.";
1710
+ const action = await promptRetryOrSkip({
1711
+ taskName: "GitNexus analyze workspace",
1712
+ reason: message,
1713
+ allowSkip: true,
1714
+ hint
1715
+ });
1716
+ if (action === "abort") {
1717
+ throw new UserAbortedRecoveryError("User abort t\u1EA1i b\u01B0\u1EDBc GitNexus analyze.");
1718
+ }
1719
+ if (action === "skip") return false;
1720
+ }
1721
+ }
1722
+ }
1723
+ async function runGitnexusSetupPhase(args) {
1724
+ const result = {
1725
+ ok: false,
1726
+ installed: false,
1727
+ analyzed: false,
1728
+ wikiGenerated: false,
1729
+ mcpRegistered: false
1730
+ };
1731
+ try {
1732
+ log.info("=== Phase 10: GitNexus Setup ===");
1733
+ let info = detectGitnexusInstallation();
1734
+ if (!info.installed) {
1735
+ const shouldInstall = await promptInstallGitnexus();
1736
+ if (!shouldInstall) {
1737
+ await appendAuditEntry("gitnexus_setup", "result=skipped,reason=user-declined");
1738
+ log.dim("Skip GitNexus. C\xE0i sau qua `avatar gitnexus install`.");
1739
+ result.reason = "user-declined";
1740
+ return result;
1741
+ }
1742
+ const installed = await installWithRecovery();
1743
+ if (!installed) {
1744
+ await appendAuditEntry("gitnexus_setup", "result=skipped,reason=install-skipped");
1745
+ log.dim("Skip GitNexus install. Workspace OK kh\xF4ng c\xF3 codebase intelligence.");
1746
+ result.reason = "install-skipped";
1747
+ return result;
1748
+ }
1749
+ info = detectGitnexusInstallation();
1750
+ if (!info.installed) {
1751
+ throw new Error("C\xE0i xong nh\u01B0ng kh\xF4ng detect \u0111\u01B0\u1EE3c binary (PATH issue).");
1752
+ }
1753
+ }
1754
+ result.installed = true;
1755
+ log.success(`GitNexus available${info.version ? ` v${info.version}` : ""}`);
1756
+ try {
1757
+ runGitnexusSetup();
1758
+ } catch (err) {
1759
+ log.warn(`gitnexus setup fail: ${err.message}`);
1760
+ log.dim("Skip global skills install. Workspace v\u1EABn d\xF9ng \u0111\u01B0\u1EE3c.");
1761
+ }
1762
+ const analyzed = await analyzeWithRecovery(args.workspacePath);
1763
+ if (!analyzed) {
1764
+ await appendAuditEntry("gitnexus_setup", "result=skipped,reason=analyze-skipped");
1765
+ log.dim("Skip analyze. GitNexus installed nh\u01B0ng ch\u01B0a index.");
1766
+ result.reason = "analyze-skipped";
1767
+ return result;
1768
+ }
1769
+ result.analyzed = true;
1770
+ const wikiResult = await runGitnexusWikiConditional(args.workspacePath);
1771
+ result.wikiGenerated = wikiResult.ran;
1772
+ if (wikiResult.skipped && wikiResult.reason === "fail") {
1773
+ log.warn(`Wiki gen fail (workspace v\u1EABn OK): ${wikiResult.detail ?? "unknown"}`);
1774
+ }
1775
+ try {
1776
+ const mcpResult = await registerGitnexusMcpServer();
1777
+ result.mcpRegistered = true;
1778
+ if (!mcpResult.wasUpdated) {
1779
+ log.dim("MCP server gitnexus \u0111\xE3 registered tr\u01B0\u1EDBc \u0111\xF3.");
1780
+ }
1781
+ } catch (err) {
1782
+ log.warn(`MCP server register fail: ${err.message}`);
1783
+ log.dim(
1784
+ "Workspace OK nh\u01B0ng Claude Code kh\xF4ng t\u1EF1 attach MCP server. Manual add v\xE0o ~/.claude/mcp_servers.json."
1785
+ );
1786
+ }
1787
+ result.ok = true;
1788
+ await appendAuditEntry(
1789
+ "gitnexus_setup",
1790
+ `result=ok,analyzed=${result.analyzed},wiki=${result.wikiGenerated},mcp=${result.mcpRegistered}`
1791
+ );
1792
+ log.success("GitNexus ready");
1793
+ return result;
1794
+ } catch (err) {
1795
+ if (err instanceof UserAbortedRecoveryError) throw err;
1796
+ const message = err instanceof Error ? err.message : String(err);
1797
+ log.warn(`GitNexus setup th\u1EA5t b\u1EA1i: ${message}`);
1798
+ log.dim("Workspace v\u1EABn s\u1EB5n s\xE0ng. Setup sau qua `avatar gitnexus install`.");
1799
+ await appendAuditEntry("gitnexus_setup", `result=failed,error=${message.slice(0, 200)}`);
1800
+ result.reason = message;
1801
+ return result;
1802
+ }
1803
+ }
1804
+
1805
+ // src/commands/gitnexus.ts
1806
+ function ensureWorkspaceCwd2() {
1807
+ const cwd = process.cwd();
1808
+ const workspaceRoot = resolveAvatarWorkspaceRootFromCwd(cwd);
1809
+ if (!workspaceRoot) {
1810
+ log.error(
1811
+ `Kh\xF4ng t\xECm th\u1EA5y Avatar workspace t\u1EEB th\u01B0 m\u1EE5c hi\u1EC7n t\u1EA1i.
1812
+ Avatar workspace c\u1EA7n c\xF3: .claude/ + CLAUDE.md + src/ (ho\u1EB7c .gitmodules).
1813
+ B\u1EA1n \u0111ang \u1EDF: ${cwd}
1814
+ Cd v\xE0o workspace dir r\u1ED3i ch\u1EA1y l\u1EA1i.`
1815
+ );
1816
+ process.exit(1);
1817
+ }
1818
+ if (workspaceRoot !== cwd) {
1819
+ log.dim(`Detected workspace root: ${workspaceRoot}`);
1820
+ }
1821
+ return workspaceRoot;
1822
+ }
1823
+ async function runGitnexusInstall() {
1824
+ const workspacePath = ensureWorkspaceCwd2();
1825
+ const result = await runGitnexusSetupPhase({ workspacePath });
1826
+ if (result.ok) {
1827
+ log.success("GitNexus setup complete");
1828
+ log.dim("Update CLAUDE.md \u0111\u1EC3 re-render section GitNexus: re-run avatar init ho\u1EB7c ch\u1EC9nh tay.");
1829
+ } else {
1830
+ log.warn(`Setup kh\xF4ng complete: ${result.reason ?? "unknown"}`);
1831
+ }
1832
+ }
1833
+ async function runGitnexusStatus() {
1834
+ const workspacePath = ensureWorkspaceCwd2();
1835
+ const metaPath = join15(workspacePath, ".gitnexus", "meta.json");
1836
+ if (!await pathExists(metaPath)) {
1837
+ log.warn(`Ch\u01B0a c\xF3 ${metaPath}. Ch\u1EA1y: avatar gitnexus install`);
1838
+ return;
1839
+ }
1840
+ try {
1841
+ const meta = await readJson(metaPath);
1842
+ log.info(`Project: ${workspacePath}`);
1843
+ log.info(`Last commit: ${meta.lastCommit?.slice(0, 7) ?? "(unknown)"}`);
1844
+ log.info(`Indexed at: ${meta.indexedAt ?? "(unknown)"}`);
1845
+ if (meta.stats) {
1846
+ log.info(
1847
+ `Stats: ${meta.stats.files ?? "?"} files \xB7 ${meta.stats.nodes ?? "?"} nodes \xB7 ${meta.stats.edges ?? "?"} edges`
1848
+ );
1849
+ }
1850
+ if (meta.lastCommit) {
1851
+ const headResult = spawnSync11("git", ["rev-parse", "HEAD"], {
1852
+ cwd: workspacePath,
1853
+ encoding: "utf8"
1854
+ });
1855
+ if (headResult.status === 0) {
1856
+ const currentHead = headResult.stdout.trim();
1857
+ if (currentHead !== meta.lastCommit) {
1858
+ log.warn("\u26A0 Index stale \u2014 HEAD \u0111\xE3 ti\u1EBFn t\u1EEB lastCommit. Ch\u1EA1y: avatar gitnexus analyze");
1859
+ } else {
1860
+ log.dim("Index fresh (HEAD === lastCommit)");
1861
+ }
1862
+ }
1863
+ }
1864
+ const wikiPath = join15(workspacePath, ".gitnexus", "wiki", "index.html");
1865
+ if (await pathExists(wikiPath)) {
1866
+ const stat = await fs8.stat(wikiPath);
1867
+ log.info(`Wiki: ${stat.mtime.toISOString()}`);
1868
+ } else {
1869
+ log.dim("Wiki: ch\u01B0a generate (ch\u1EA1y gitnexus wiki manual)");
1870
+ }
1871
+ } catch (err) {
1872
+ log.error(`Read meta.json fail: ${err.message}`);
1873
+ process.exit(1);
1874
+ }
1875
+ }
1876
+ async function runGitnexusAnalyzeCommand() {
1877
+ const workspacePath = ensureWorkspaceCwd2();
1878
+ try {
1879
+ runGitnexusAnalyze(workspacePath);
1880
+ log.success("Index refreshed. Ch\u1EA1y `avatar gitnexus status` xem chi ti\u1EBFt.");
1881
+ } catch (err) {
1882
+ log.error(`Analyze fail: ${err.message}`);
1883
+ process.exit(1);
1884
+ }
1885
+ }
1886
+ function registerGitnexusCommand(program2) {
1887
+ const gx = program2.command("gitnexus").description("Qu\u1EA3n l\xFD GitNexus code intelligence (M10)");
1888
+ gx.command("install").description("C\xE0i + setup GitNexus cho workspace hi\u1EC7n t\u1EA1i").action(async () => {
1889
+ await runGitnexusInstall();
1890
+ });
1891
+ gx.command("status").description("Show index info + staleness warning").action(async () => {
1892
+ await runGitnexusStatus();
1893
+ });
1894
+ gx.command("analyze").description("Re-run analyze refresh index (no wiki)").action(async () => {
1895
+ await runGitnexusAnalyzeCommand();
1896
+ });
1897
+ }
1898
+
1899
+ // src/commands/init.ts
1900
+ import { basename, join as join22, relative as relative2, resolve } from "path";
1901
+ import { confirm as confirm5, input as input5, select as select8 } from "@inquirer/prompts";
1902
+ import boxen5 from "boxen";
1903
+
1904
+ // src/lib/add-team-pack-submodule-with-retry-on-network-fail.ts
1905
+ import { spawnSync as spawnSync13 } from "child_process";
1906
+ import { select as select5 } from "@inquirer/prompts";
1907
+
1342
1908
  // src/lib/team-pack-submodule-manager.ts
1343
- import { join as join12 } from "path";
1909
+ import { join as join16 } from "path";
1344
1910
 
1345
1911
  // src/lib/check-team-pack-access-with-retry-loop.ts
1346
- import { spawnSync as spawnSync7 } from "child_process";
1347
- import { confirm as confirm2, select as select4 } from "@inquirer/prompts";
1348
- import boxen2 from "boxen";
1912
+ import { spawnSync as spawnSync12 } from "child_process";
1913
+ import { confirm as confirm4, select as select4 } from "@inquirer/prompts";
1914
+ import boxen3 from "boxen";
1349
1915
  function parseRepoSlugFromGitUrl(url) {
1350
1916
  const httpsMatch = url.match(/github\.com[/:]([^/]+\/[^/]+?)(?:\.git)?$/);
1351
1917
  if (httpsMatch) return httpsMatch[1];
1352
1918
  return null;
1353
1919
  }
1354
1920
  function checkRepoAccess(repoSlug) {
1355
- const r = spawnSync7("gh", ["api", `repos/${repoSlug}`], { stdio: "ignore" });
1921
+ const r = spawnSync12("gh", ["api", `repos/${repoSlug}`], { stdio: "ignore" });
1356
1922
  return r.status === 0;
1357
1923
  }
1358
1924
  function getCurrentGhUser() {
1359
- const r = spawnSync7("gh", ["api", "user", "--jq", ".login"], {
1925
+ const r = spawnSync12("gh", ["api", "user", "--jq", ".login"], {
1360
1926
  encoding: "utf8",
1361
1927
  stdio: ["ignore", "pipe", "pipe"]
1362
1928
  });
@@ -1365,13 +1931,13 @@ function getCurrentGhUser() {
1365
1931
  }
1366
1932
  function triggerGhAuthLoginInteractive() {
1367
1933
  log.info("M\u1EDF browser \u0111\u1EC3 switch GitHub account...");
1368
- const r = spawnSync7("gh", ["auth", "login", "--web"], { stdio: "inherit" });
1934
+ const r = spawnSync12("gh", ["auth", "login", "--web"], { stdio: "inherit" });
1369
1935
  if (r.status !== 0) {
1370
1936
  log.warn(`gh auth login exit ${r.status}. C\xF3 th\u1EC3 ch\u1EA1y tay r\u1ED3i quay l\u1EA1i retry.`);
1371
1937
  }
1372
1938
  }
1373
1939
  async function copyInfoToClipboardWithConsent(info) {
1374
- const ok = await confirm2({
1940
+ const ok = await confirm4({
1375
1941
  message: "Copy th\xF4ng tin (GitHub username + email) v\xE0o clipboard \u0111\u1EC3 d\xE1n v\xE0o Slack/email?",
1376
1942
  default: true
1377
1943
  });
@@ -1400,7 +1966,7 @@ function printAccessWarningBox(repoSlug, ghUser, ssoEmail) {
1400
1966
  `${chalk.dim("Li\xEAn h\u1EC7:")} luke@nal.vn (Slack #avatar-setup)`
1401
1967
  ];
1402
1968
  process.stdout.write(
1403
- `${boxen2(lines.join("\n"), { padding: 1, borderColor: "red", borderStyle: "round" })}
1969
+ `${boxen3(lines.join("\n"), { padding: 1, borderColor: "red", borderStyle: "round" })}
1404
1970
  `
1405
1971
  );
1406
1972
  }
@@ -1505,7 +2071,7 @@ async function addTeamPackSubmodule(projectRoot, tag, ssoEmail) {
1505
2071
  }
1506
2072
  let target = tag ?? null;
1507
2073
  if (!target) {
1508
- target = await latestTag(join12(projectRoot, TEAM_PACK_RELATIVE_PATH));
2074
+ target = await latestTag(join16(projectRoot, TEAM_PACK_RELATIVE_PATH));
1509
2075
  }
1510
2076
  if (target) {
1511
2077
  await checkoutTagInSubmodule(TEAM_PACK_RELATIVE_PATH, target, projectRoot);
@@ -1513,7 +2079,7 @@ async function addTeamPackSubmodule(projectRoot, tag, ssoEmail) {
1513
2079
  return { pinnedTag: target };
1514
2080
  }
1515
2081
  async function readPinnedPackVersion(projectRoot) {
1516
- const submoduleRoot = join12(projectRoot, TEAM_PACK_RELATIVE_PATH);
2082
+ const submoduleRoot = join16(projectRoot, TEAM_PACK_RELATIVE_PATH);
1517
2083
  const tag = await latestTag(submoduleRoot);
1518
2084
  if (tag) return tag;
1519
2085
  const sha = await currentCommitSha(submoduleRoot);
@@ -1527,14 +2093,14 @@ function isSshPermissionError(message) {
1527
2093
  }
1528
2094
  function triggerGhAuthLoginInteractive2() {
1529
2095
  log.info("M\u1EDF browser \u0111\u1EC3 switch GitHub account...");
1530
- const r = spawnSync8("gh", ["auth", "login", "--web"], { stdio: "inherit" });
2096
+ const r = spawnSync13("gh", ["auth", "login", "--web"], { stdio: "inherit" });
1531
2097
  if (r.status !== 0) {
1532
2098
  log.warn(`gh auth login exit ${r.status}. C\xF3 th\u1EC3 ch\u1EA1y tay r\u1ED3i quay l\u1EA1i retry.`);
1533
2099
  }
1534
2100
  }
1535
2101
  function openGithubSshKeysPage() {
1536
2102
  log.info("M\u1EDF trang GitHub Settings \u2192 SSH Keys...");
1537
- const r = spawnSync8("open", ["https://github.com/settings/keys"], { stdio: "ignore" });
2103
+ const r = spawnSync13("open", ["https://github.com/settings/keys"], { stdio: "ignore" });
1538
2104
  if (r.status !== 0) {
1539
2105
  log.info("URL: https://github.com/settings/keys");
1540
2106
  }
@@ -1684,7 +2250,7 @@ ${renderAvatarBanner(opts)}
1684
2250
  }
1685
2251
 
1686
2252
  // src/lib/execute-gh-repo-create.ts
1687
- import { spawnSync as spawnSync9 } from "child_process";
2253
+ import { spawnSync as spawnSync14 } from "child_process";
1688
2254
  var RepoAlreadyExistsError = class extends Error {
1689
2255
  constructor(fullName) {
1690
2256
  super(`Repo "${fullName}" \u0111\xE3 t\u1ED3n t\u1EA1i tr\xEAn GitHub. \u0110\u1ED5i t\xEAn ho\u1EB7c x\xF3a repo c\u0169.`);
@@ -1704,7 +2270,7 @@ function executeGhRepoCreate(input6) {
1704
2270
  "origin",
1705
2271
  "--push"
1706
2272
  ];
1707
- const r = spawnSync9("gh", args, { stdio: "inherit" });
2273
+ const r = spawnSync14("gh", args, { stdio: "inherit" });
1708
2274
  if (r.status !== 0) {
1709
2275
  if (r.status === 1) {
1710
2276
  throw new RepoAlreadyExistsError(fullName);
@@ -1718,9 +2284,9 @@ function executeGhRepoCreate(input6) {
1718
2284
  }
1719
2285
 
1720
2286
  // src/lib/resolve-github-username-default.ts
1721
- import { spawnSync as spawnSync10 } from "child_process";
2287
+ import { spawnSync as spawnSync15 } from "child_process";
1722
2288
  function resolveGithubUsernameDefault() {
1723
- const r = spawnSync10("gh", ["api", "user", "--jq", ".login"], {
2289
+ const r = spawnSync15("gh", ["api", "user", "--jq", ".login"], {
1724
2290
  encoding: "utf8",
1725
2291
  stdio: ["ignore", "pipe", "pipe"]
1726
2292
  });
@@ -1768,12 +2334,12 @@ function createGithubRemoteFromFolder(input6) {
1768
2334
  }
1769
2335
 
1770
2336
  // src/lib/create-workspace-remote-via-gh.ts
1771
- import { spawnSync as spawnSync18 } from "child_process";
2337
+ import { spawnSync as spawnSync23 } from "child_process";
1772
2338
 
1773
2339
  // src/lib/check-gh-cli-auth-status.ts
1774
- import { spawnSync as spawnSync11 } from "child_process";
2340
+ import { spawnSync as spawnSync16 } from "child_process";
1775
2341
  function checkGhCliAuthStatus() {
1776
- const r = spawnSync11("gh", ["auth", "status"], { stdio: "ignore" });
2342
+ const r = spawnSync16("gh", ["auth", "status"], { stdio: "ignore" });
1777
2343
  if (r.error && r.error.code === "ENOENT") {
1778
2344
  return "not-installed";
1779
2345
  }
@@ -1781,12 +2347,12 @@ function checkGhCliAuthStatus() {
1781
2347
  }
1782
2348
 
1783
2349
  // src/lib/detect-package-manager.ts
1784
- import { spawnSync as spawnSync12 } from "child_process";
2350
+ import { spawnSync as spawnSync17 } from "child_process";
1785
2351
  function hasBinary(name) {
1786
2352
  const platform2 = detectHostPlatform();
1787
2353
  const probe = platform2 === "win32" ? "where" : "command";
1788
2354
  const args = platform2 === "win32" ? [name] : ["-v", name];
1789
- const r = spawnSync12(probe, args, {
2355
+ const r = spawnSync17(probe, args, {
1790
2356
  shell: platform2 !== "win32",
1791
2357
  stdio: "ignore"
1792
2358
  });
@@ -1802,11 +2368,11 @@ function detectPackageManager() {
1802
2368
  }
1803
2369
 
1804
2370
  // src/lib/handle-remote-access-failure-with-account-switch.ts
1805
- import { spawnSync as spawnSync14 } from "child_process";
2371
+ import { spawnSync as spawnSync19 } from "child_process";
1806
2372
  import { input as input4, select as select6 } from "@inquirer/prompts";
1807
2373
 
1808
2374
  // src/lib/verify-git-remote-accessible.ts
1809
- import { spawnSync as spawnSync13 } from "child_process";
2375
+ import { spawnSync as spawnSync18 } from "child_process";
1810
2376
  var TIMEOUT_MS = 5e3;
1811
2377
  function classifyRemoteError(stderr) {
1812
2378
  const text = stderr.toLowerCase();
@@ -1822,7 +2388,7 @@ function classifyRemoteError(stderr) {
1822
2388
  return "unknown";
1823
2389
  }
1824
2390
  function tryVerifyGitRemoteAccessible(url) {
1825
- const r = spawnSync13("git", ["ls-remote", "--exit-code", url, "HEAD"], {
2391
+ const r = spawnSync18("git", ["ls-remote", "--exit-code", url, "HEAD"], {
1826
2392
  encoding: "utf8",
1827
2393
  timeout: TIMEOUT_MS,
1828
2394
  stdio: ["ignore", "pipe", "pipe"]
@@ -1844,7 +2410,7 @@ var RemoteAccessAbortedError = class extends Error {
1844
2410
  }
1845
2411
  };
1846
2412
  function getCurrentGhUser2() {
1847
- const r = spawnSync14("gh", ["api", "user", "--jq", ".login"], {
2413
+ const r = spawnSync19("gh", ["api", "user", "--jq", ".login"], {
1848
2414
  encoding: "utf8",
1849
2415
  stdio: ["ignore", "pipe", "pipe"]
1850
2416
  });
@@ -1853,7 +2419,7 @@ function getCurrentGhUser2() {
1853
2419
  }
1854
2420
  function triggerGhAuthLoginInteractive3() {
1855
2421
  log.info("M\u1EDF browser \u0111\u1EC3 switch GitHub account...");
1856
- const r = spawnSync14("gh", ["auth", "login", "--web"], { stdio: "inherit" });
2422
+ const r = spawnSync19("gh", ["auth", "login", "--web"], { stdio: "inherit" });
1857
2423
  if (r.status !== 0) {
1858
2424
  log.warn(`gh auth login exit ${r.status}. B\u1EA1n c\xF3 th\u1EC3 ch\u1EA1y tay r\u1ED3i quay l\u1EA1i retry.`);
1859
2425
  }
@@ -1936,7 +2502,7 @@ async function handleRemoteAccessFailureWithAccountSwitch(args) {
1936
2502
  }
1937
2503
 
1938
2504
  // src/lib/install-gh-cli-via-package-manager.ts
1939
- import { spawnSync as spawnSync15 } from "child_process";
2505
+ import { spawnSync as spawnSync20 } from "child_process";
1940
2506
  var INSTALL_COMMANDS = {
1941
2507
  brew: { cmd: "brew", args: ["install", "gh"] },
1942
2508
  apt: { cmd: "sudo", args: ["apt-get", "install", "-y", "gh"] },
@@ -1947,7 +2513,7 @@ var INSTALL_COMMANDS = {
1947
2513
  function installGhCliViaPackageManager(pm) {
1948
2514
  const spec = INSTALL_COMMANDS[pm];
1949
2515
  log.info(`\u0110ang c\xE0i gh CLI qua ${pm}...`);
1950
- const r = spawnSync15(spec.cmd, spec.args, { stdio: "inherit" });
2516
+ const r = spawnSync20(spec.cmd, spec.args, { stdio: "inherit" });
1951
2517
  if (r.status !== 0) {
1952
2518
  throw new Error(`C\xE0i gh CLI th\u1EA5t b\u1EA1i qua ${pm} (exit ${r.status}). C\xE0i tay r\u1ED3i ch\u1EA1y l\u1EA1i.`);
1953
2519
  }
@@ -1955,9 +2521,9 @@ function installGhCliViaPackageManager(pm) {
1955
2521
  }
1956
2522
 
1957
2523
  // src/lib/setup-git-credential-via-gh.ts
1958
- import { spawnSync as spawnSync16 } from "child_process";
2524
+ import { spawnSync as spawnSync21 } from "child_process";
1959
2525
  function setupGitCredentialViaGh() {
1960
- const r = spawnSync16("gh", ["auth", "setup-git"], { stdio: "ignore" });
2526
+ const r = spawnSync21("gh", ["auth", "setup-git"], { stdio: "ignore" });
1961
2527
  if (r.status !== 0) {
1962
2528
  log.warn("gh auth setup-git fail (non-fatal). N\u1EBFu git clone l\u1ED7i 128 \u2192 ch\u1EA1y th\u1EE7 c\xF4ng.");
1963
2529
  return;
@@ -1966,10 +2532,10 @@ function setupGitCredentialViaGh() {
1966
2532
  }
1967
2533
 
1968
2534
  // src/lib/trigger-gh-cli-auth-login.ts
1969
- import { spawnSync as spawnSync17 } from "child_process";
2535
+ import { spawnSync as spawnSync22 } from "child_process";
1970
2536
  function triggerGhCliAuthLogin() {
1971
2537
  log.info("Kh\u1EDFi \u0111\u1ED9ng \u0111\u0103ng nh\u1EADp GitHub qua gh CLI (browser s\u1EBD m\u1EDF)...");
1972
- const r = spawnSync17(
2538
+ const r = spawnSync22(
1973
2539
  "gh",
1974
2540
  ["auth", "login", "--hostname", "github.com", "--web", "--git-protocol", "ssh"],
1975
2541
  { stdio: "inherit" }
@@ -2087,20 +2653,20 @@ function classifyGhCreateError(stderr) {
2087
2653
  return "unknown";
2088
2654
  }
2089
2655
  function repoExistsOnGitHub(fullName) {
2090
- const r = spawnSync18("gh", ["repo", "view", fullName, "--json", "name"], {
2656
+ const r = spawnSync23("gh", ["repo", "view", fullName, "--json", "name"], {
2091
2657
  stdio: "ignore"
2092
2658
  });
2093
2659
  return r.status === 0;
2094
2660
  }
2095
2661
  function canCreateInNamespace(org, ghUser) {
2096
2662
  if (org.toLowerCase() === ghUser.toLowerCase()) return { ok: true };
2097
- const r = spawnSync18("gh", ["api", `orgs/${org}/members/${ghUser}`, "--silent"], {
2663
+ const r = spawnSync23("gh", ["api", `orgs/${org}/members/${ghUser}`, "--silent"], {
2098
2664
  stdio: "ignore"
2099
2665
  });
2100
2666
  if (r.status === 0) return { ok: true };
2101
- const orgCheck = spawnSync18("gh", ["api", `orgs/${org}`, "--silent"], { stdio: "ignore" });
2667
+ const orgCheck = spawnSync23("gh", ["api", `orgs/${org}`, "--silent"], { stdio: "ignore" });
2102
2668
  if (orgCheck.status !== 0) {
2103
- const userCheck = spawnSync18("gh", ["api", `users/${org}`, "--silent"], { stdio: "ignore" });
2669
+ const userCheck = spawnSync23("gh", ["api", `users/${org}`, "--silent"], { stdio: "ignore" });
2104
2670
  if (userCheck.status === 0) {
2105
2671
  return {
2106
2672
  ok: false,
@@ -2136,7 +2702,7 @@ async function createWorkspaceRemoteViaGh(input6) {
2136
2702
  );
2137
2703
  }
2138
2704
  log.info(`T\u1EA1o GitHub repo cho workspace: ${fullName} (${input6.visibility})...`);
2139
- const r = spawnSync18(
2705
+ const r = spawnSync23(
2140
2706
  "gh",
2141
2707
  [
2142
2708
  "repo",
@@ -2177,7 +2743,7 @@ ${combined}
2177
2743
  function linkExistingRemoteToWorkspace(args) {
2178
2744
  const sshUrl = `git@github.com:${args.fullName}.git`;
2179
2745
  const httpsUrl = `https://github.com/${args.fullName}.git`;
2180
- const addResult = spawnSync18(
2746
+ const addResult = spawnSync23(
2181
2747
  "git",
2182
2748
  ["-C", args.workspacePath, "remote", "add", "origin", sshUrl],
2183
2749
  {
@@ -2186,7 +2752,7 @@ function linkExistingRemoteToWorkspace(args) {
2186
2752
  }
2187
2753
  );
2188
2754
  if (addResult.status !== 0) {
2189
- spawnSync18("git", ["-C", args.workspacePath, "remote", "set-url", "origin", sshUrl], {
2755
+ spawnSync23("git", ["-C", args.workspacePath, "remote", "set-url", "origin", sshUrl], {
2190
2756
  stdio: "ignore"
2191
2757
  });
2192
2758
  }
@@ -2200,11 +2766,11 @@ import { select as select7 } from "@inquirer/prompts";
2200
2766
  import { simpleGit as simpleGit3 } from "simple-git";
2201
2767
 
2202
2768
  // src/lib/check-folder-has-git.ts
2203
- import { existsSync as existsSync4, statSync } from "fs";
2204
- import { join as join13 } from "path";
2769
+ import { existsSync as existsSync6, statSync } from "fs";
2770
+ import { join as join17 } from "path";
2205
2771
  function checkFolderHasGit(folderPath) {
2206
- const gitPath = join13(folderPath, ".git");
2207
- if (!existsSync4(gitPath)) return false;
2772
+ const gitPath = join17(folderPath, ".git");
2773
+ if (!existsSync6(gitPath)) return false;
2208
2774
  const stat = statSync(gitPath);
2209
2775
  return stat.isDirectory() || stat.isFile();
2210
2776
  }
@@ -2234,8 +2800,8 @@ async function createInitialGitCommit(folderPath) {
2234
2800
  }
2235
2801
 
2236
2802
  // src/lib/detect-folder-tech-stack.ts
2237
- import { existsSync as existsSync5 } from "fs";
2238
- import { join as join14 } from "path";
2803
+ import { existsSync as existsSync7 } from "fs";
2804
+ import { join as join18 } from "path";
2239
2805
  var SIGNATURES = {
2240
2806
  node: ["package.json"],
2241
2807
  python: ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile"],
@@ -2247,7 +2813,7 @@ var SIGNATURES = {
2247
2813
  function detectFolderTechStack(folderPath) {
2248
2814
  const matched = [];
2249
2815
  for (const [stack, files] of Object.entries(SIGNATURES)) {
2250
- if (files.some((f) => existsSync5(join14(folderPath, f)))) {
2816
+ if (files.some((f) => existsSync7(join18(folderPath, f)))) {
2251
2817
  matched.push(stack);
2252
2818
  }
2253
2819
  }
@@ -2256,25 +2822,25 @@ function detectFolderTechStack(folderPath) {
2256
2822
 
2257
2823
  // src/lib/gitignore-template-loader.ts
2258
2824
  import { readFileSync as readFileSync3 } from "fs";
2259
- import { dirname as dirname4, join as join15 } from "path";
2825
+ import { dirname as dirname4, join as join19 } from "path";
2260
2826
  import { fileURLToPath as fileURLToPath2 } from "url";
2261
2827
  var __dirname = dirname4(fileURLToPath2(import.meta.url));
2262
2828
  var CANDIDATE_DIRS = [
2263
2829
  // Bundled production: dist/index.js → __dirname = .../dist/, sibling dist/templates
2264
- join15(__dirname, "templates", "gitignore"),
2830
+ join19(__dirname, "templates", "gitignore"),
2265
2831
  // Legacy bundled: nếu file là dist/lib/*.js (sub-bundle), templates ở dist/templates
2266
- join15(__dirname, "..", "templates", "gitignore"),
2832
+ join19(__dirname, "..", "templates", "gitignore"),
2267
2833
  // Dev mode (vitest/tsx run src/ trực tiếp): __dirname = src/lib/
2268
- join15(__dirname, "..", "..", "src", "templates", "gitignore"),
2834
+ join19(__dirname, "..", "..", "src", "templates", "gitignore"),
2269
2835
  // npm-installed alt: __dirname = .../dist/ → package_root/src/templates
2270
- join15(__dirname, "..", "src", "templates", "gitignore")
2836
+ join19(__dirname, "..", "src", "templates", "gitignore")
2271
2837
  ];
2272
2838
  var AVATAR_MARKER_START = "# === avatar ===";
2273
2839
  var AVATAR_MARKER_END = "# === /avatar ===";
2274
2840
  function readTemplate(stack) {
2275
2841
  for (const dir of CANDIDATE_DIRS) {
2276
2842
  try {
2277
- return readFileSync3(join15(dir, `${stack}.txt`), "utf8");
2843
+ return readFileSync3(join19(dir, `${stack}.txt`), "utf8");
2278
2844
  } catch {
2279
2845
  }
2280
2846
  }
@@ -2288,11 +2854,11 @@ ${readTemplate(s).trim()}`);
2288
2854
  }
2289
2855
 
2290
2856
  // src/lib/write-or-merge-gitignore.ts
2291
- import { existsSync as existsSync6, readFileSync as readFileSync4, writeFileSync } from "fs";
2292
- import { join as join16 } from "path";
2857
+ import { existsSync as existsSync8, readFileSync as readFileSync4, writeFileSync } from "fs";
2858
+ import { join as join20 } from "path";
2293
2859
  function writeOrMergeGitignore(folderPath, avatarBlock) {
2294
- const path = join16(folderPath, ".gitignore");
2295
- if (!existsSync6(path)) {
2860
+ const path = join20(folderPath, ".gitignore");
2861
+ if (!existsSync8(path)) {
2296
2862
  writeFileSync(path, avatarBlock, "utf8");
2297
2863
  return;
2298
2864
  }
@@ -2468,7 +3034,7 @@ async function safeBootstrapGitInFolder(folderPath, opts = {}) {
2468
3034
 
2469
3035
  // src/commands/init-conflict-detection-helpers.ts
2470
3036
  import { readdir } from "fs/promises";
2471
- import { join as join17 } from "path";
3037
+ import { join as join21 } from "path";
2472
3038
  async function isEmptyOrMissing(path) {
2473
3039
  if (!await pathExists(path)) return true;
2474
3040
  try {
@@ -2481,7 +3047,7 @@ async function isEmptyOrMissing(path) {
2481
3047
  }
2482
3048
  async function findAlternativeWorkspaceName(parent, desiredName, maxAttempts = 10) {
2483
3049
  for (let i = 2; i < maxAttempts; i++) {
2484
- const candidate = join17(parent, `${desiredName}-${i}`);
3050
+ const candidate = join21(parent, `${desiredName}-${i}`);
2485
3051
  if (await isEmptyOrMissing(candidate)) return candidate;
2486
3052
  }
2487
3053
  return null;
@@ -2497,6 +3063,38 @@ function inferWorkspaceName(repoUrl) {
2497
3063
  const withoutPrefix = baseName.replace(/^avatar-/, "");
2498
3064
  return `avatar-${withoutPrefix}-workspace`;
2499
3065
  }
3066
+ function buildGitnexusSection(gitnexusReady) {
3067
+ if (!gitnexusReady) return "";
3068
+ return `
3069
+ ### \u{1F9E0} CODEBASE INTELLIGENCE \u2014 GitNexus
3070
+
3071
+ Workspace c\xF3 GitNexus index t\u1EA1i \`.gitnexus/\` cung c\u1EA5p architectural awareness
3072
+ cho Claude Code (impact analysis, call chains, blast radius).
3073
+
3074
+ **H\u01B0\u1EDBng d\u1EABn cho Claude:**
3075
+
3076
+ - Khi c\u1EA7n hi\u1EC3u codebase architecture (vd: tr\u01B0\u1EDBc refactor, debug call chain) \u2192
3077
+ query GitNexus MCP resource \`gitnexus://repo/<name>/context\`
3078
+ - Khi resource tr\u1EA3 staleness hint "\u26A0\uFE0F Index is N commit(s) behind HEAD" \u2192
3079
+ T\u1EF0 ch\u1EA1y \`gitnexus analyze .\` (trong workspace root) tr\u01B0\u1EDBc khi ti\u1EBFp t\u1EE5c task
3080
+ - Sau khi \`gitnexus analyze\` xong: **KH\xD4NG t\u1EF1 ch\u1EA1y \`gitnexus wiki\`**
3081
+ (cost cao, c\xF3 th\u1EC3 block session 2-5 ph\xFAt) \u2014 user s\u1EBD ch\u1EA1y manual khi c\u1EA7n
3082
+
3083
+ **Folders Claude scan auto cho skills:**
3084
+
3085
+ - \`~/.claude/skills/gitnexus-*/\` \u2014 GitNexus global skills (exploring, debugging, ...)
3086
+ - \`.claude/pack/skills/\` \u2014 NAL team-shared skills (qua team-ai-pack submodule)
3087
+ - C\u1EA3 2 \u0111\u1EC1u \u0111\u01B0\u1EE3c scan, kh\xF4ng xung \u0111\u1ED9t (different naming prefix)
3088
+
3089
+ **Manual wiki update:**
3090
+
3091
+ Khi user c\u1EA7n regenerate wiki sau refactor l\u1EDBn \u2014 ch\u1EA1y:
3092
+
3093
+ \`\`\`bash
3094
+ gitnexus wiki . --api-key <key> --base-url <url>
3095
+ \`\`\`
3096
+ `;
3097
+ }
2500
3098
  function buildScaffoldVariables(args) {
2501
3099
  return {
2502
3100
  projectName: args.projectName,
@@ -2505,12 +3103,13 @@ function buildScaffoldVariables(args) {
2505
3103
  avatarVersion: AVATAR_CLI_VERSION,
2506
3104
  packVersion: args.packVersion,
2507
3105
  lastScan: (/* @__PURE__ */ new Date()).toISOString(),
2508
- mode: args.mode
3106
+ mode: args.mode,
3107
+ gitnexusSection: buildGitnexusSection(args.gitnexusReady ?? false)
2509
3108
  };
2510
3109
  }
2511
3110
 
2512
3111
  // src/commands/login.ts
2513
- import boxen3 from "boxen";
3112
+ import boxen4 from "boxen";
2514
3113
  import open from "open";
2515
3114
 
2516
3115
  // src/lib/google-oauth-device-flow.ts
@@ -2657,7 +3256,7 @@ async function runLogin(opts) {
2657
3256
  "",
2658
3257
  `Ho\u1EB7c Avatar t\u1EF1 m\u1EDF browser, click ${chalk.green("Allow")}...`
2659
3258
  ].join("\n");
2660
- process.stdout.write(`${boxen3(instructions, { padding: 1, borderStyle: "round" })}
3259
+ process.stdout.write(`${boxen4(instructions, { padding: 1, borderStyle: "round" })}
2661
3260
  `);
2662
3261
  void open(verificationUrl).catch(() => {
2663
3262
  log.dim("(Kh\xF4ng m\u1EDF \u0111\u01B0\u1EE3c browser t\u1EF1 \u0111\u1ED9ng \u2014 copy URL \u1EDF tr\xEAn)");
@@ -2713,6 +3312,9 @@ function parseBootstrapStrategyOpts(opts) {
2713
3312
  }
2714
3313
  function registerInitCommand(program2) {
2715
3314
  program2.command("init").description("Kh\u1EDFi t\u1EA1o Avatar \u2014 3 flow t\u1EF1 nh\u1EADn di\u1EC7n (repo / folder / new)").option("--project-status <val>", "existing-remote | existing-folder | new-project").option("--folder-path <path>", "\u0110\u01B0\u1EDDng d\u1EABn folder hi\u1EC7n c\xF3 (flow existing-folder)").option("--create-remote", "Force t\u1EA1o remote qua gh (flow existing-folder ho\u1EB7c new-project)").option("--repo-visibility <val>", "private (m\u1EB7c \u0111\u1ECBnh) | public").option("--repo-org <name>", "GitHub org/owner cho repo m\u1EDBi").option("--client-repo <url>", "URL git remote (flow existing-remote)").option("--workspace-name <name>", "T\xEAn workspace").option("--workspace-parent <path>", "Th\u01B0 m\u1EE5c cha t\u1EA1o workspace (m\u1EB7c \u0111\u1ECBnh . \u2014 CWD)").option("--pack-version <tag>", "Pin team-ai-pack v\xE0o tag c\u1EE5 th\u1EC3").option("--team-owner <email>", "Email team owner (b\u1ECF qua prompt)").option("--description <text>", "M\xF4 t\u1EA3 1 d\xF2ng c\u1EE7a d\u1EF1 \xE1n").option("--skip-scan", "B\u1ECF qua project-scanner sau scaffold").option("--skip-team-pack", "B\u1ECF qua submodule team-ai-pack (test mode)").option("--force", "B\u1ECF qua prompt khi workspace path \u0111\xE3 t\u1ED3n t\u1EA1i").option("--yes", "Auto-confirm t\u1EA5t c\u1EA3 prompt").option("--no-commit", "Skip commit workspace initial state (m\u1EB7c \u0111\u1ECBnh LU\xD4N commit)").option("--workspace-remote", "T\u1EA1o GitHub remote cho workspace root (default: prompt)").option("--ai-skip", "B\u1ECF qua phase AI setup (CI/test mode \u2014 ch\u1EA1y `avatar ai setup` sau)").option(
3315
+ "--gitnexus-skip",
3316
+ "B\u1ECF qua phase GitNexus setup (M10 \u2014 ch\u1EA1y `avatar gitnexus install` sau)"
3317
+ ).option(
2716
3318
  "--bootstrap-strategy <s>",
2717
3319
  "X\u1EED l\xFD folder dirty: stash | commit-all | skip | branch (default: prompt)"
2718
3320
  ).option("--preserve-uncommitted", "Alias cho --bootstrap-strategy=stash (gi\u1EEF changes user)").option("--mode <mode>", "[DEPRECATED] D\xF9ng --project-status thay th\u1EBF").action(async (opts) => {
@@ -2818,6 +3420,7 @@ async function runInitFromExistingRemote(opts, ownerEmail) {
2818
3420
  repoOrg: opts.repoOrg,
2819
3421
  flow: "existing-remote",
2820
3422
  aiSkip: opts.aiSkip,
3423
+ gitnexusSkip: opts.gitnexusSkip,
2821
3424
  ssoEmail: ownerEmail
2822
3425
  });
2823
3426
  }
@@ -2854,6 +3457,7 @@ async function runInitFromExistingFolder(opts, ownerEmail) {
2854
3457
  repoOrg: opts.repoOrg,
2855
3458
  flow: "existing-folder",
2856
3459
  aiSkip: opts.aiSkip,
3460
+ gitnexusSkip: opts.gitnexusSkip,
2857
3461
  ssoEmail: ownerEmail
2858
3462
  });
2859
3463
  }
@@ -2873,7 +3477,7 @@ async function runInitFromScratch(opts, ownerEmail) {
2873
3477
  const teamOwner = opts.teamOwner ?? await promptTeamOwner(ownerEmail);
2874
3478
  const workspaceParent = resolve(opts.workspaceParent ?? ".");
2875
3479
  const workspacePath = await resolveWorkspacePath(workspaceParent, projectName, opts.force);
2876
- const srcPath = join18(workspacePath, "src");
3480
+ const srcPath = join22(workspacePath, "src");
2877
3481
  await ensureDir(workspacePath);
2878
3482
  await ensureDir(srcPath);
2879
3483
  await safeBootstrapGitInFolder(srcPath, { autoYes: true });
@@ -2914,7 +3518,8 @@ async function runInitFromScratch(opts, ownerEmail) {
2914
3518
  repoVisibility: opts.repoVisibility,
2915
3519
  repoOrg: opts.repoOrg,
2916
3520
  flow: "new-project",
2917
- aiSkip: opts.aiSkip
3521
+ aiSkip: opts.aiSkip,
3522
+ gitnexusSkip: opts.gitnexusSkip
2918
3523
  });
2919
3524
  } catch (err) {
2920
3525
  sp.fail("Init workspace th\u1EA5t b\u1EA1i");
@@ -2928,7 +3533,7 @@ async function getOrCreateOriginRemote(folderPath, opts) {
2928
3533
  log.success(`Folder \u0111\xE3 c\xF3 remote origin: ${origin.refs.push}`);
2929
3534
  return origin.refs.push;
2930
3535
  }
2931
- const shouldCreate = opts.createRemote ?? await confirm3({
3536
+ const shouldCreate = opts.createRemote ?? await confirm5({
2932
3537
  message: "Folder ch\u01B0a c\xF3 remote. T\u1EA1o GitHub repo ngay \u0111\u1EC3 share team?",
2933
3538
  default: true
2934
3539
  });
@@ -2989,7 +3594,8 @@ async function scaffoldWorkspaceWithSrcSubmodule(args) {
2989
3594
  repoVisibility: args.repoVisibility,
2990
3595
  repoOrg: args.repoOrg,
2991
3596
  flow: args.flow,
2992
- aiSkip: args.aiSkip
3597
+ aiSkip: args.aiSkip,
3598
+ gitnexusSkip: args.gitnexusSkip
2993
3599
  });
2994
3600
  } catch (err) {
2995
3601
  sp.fail("Init workspace th\u1EA5t b\u1EA1i");
@@ -3009,10 +3615,10 @@ async function finalizeWorkspaceScaffold(args) {
3009
3615
  await writeRootClaudeMd(args.workspacePath, vars);
3010
3616
  await writeProjectSettings(args.workspacePath, vars);
3011
3617
  await appendGitignoreEntries(args.workspacePath);
3012
- await ensureDir(join18(args.workspacePath, "notes"));
3013
- await ensureDir(join18(args.workspacePath, "scripts"));
3014
- await installGitHook(join18(args.workspacePath, ".git"), "post-merge");
3015
- await installGitHook(join18(args.workspacePath, ".git", "modules", "src"), "pre-push");
3618
+ await ensureDir(join22(args.workspacePath, "notes"));
3619
+ await ensureDir(join22(args.workspacePath, "scripts"));
3620
+ await installGitHook(join22(args.workspacePath, ".git"), "post-merge");
3621
+ await installGitHook(join22(args.workspacePath, ".git", "modules", "src"), "pre-push");
3016
3622
  log.success("C\xE0i post-merge (workspace) + pre-push (src/)");
3017
3623
  await appendAuditEntry("init", `flow=${args.flow},workspace=${args.workspaceName}`);
3018
3624
  await maybeCommitWorkspace(args.workspacePath, args.skipCommit);
@@ -3023,7 +3629,30 @@ async function finalizeWorkspaceScaffold(args) {
3023
3629
  } else {
3024
3630
  aiResult = await runAiSetupPhase({ workspacePath: args.workspacePath });
3025
3631
  }
3026
- printInitSuccessBox(args.workspacePath, args.flow, aiResult);
3632
+ let gitnexusResult = null;
3633
+ const skipGitnexus = args.aiSkip || args.gitnexusSkip;
3634
+ if (skipGitnexus) {
3635
+ if (args.gitnexusSkip) {
3636
+ log.dim("B\u1ECF qua GitNexus setup (--gitnexus-skip). Setup sau: avatar gitnexus install");
3637
+ } else {
3638
+ log.dim("B\u1ECF qua GitNexus setup (auto-skip do --ai-skip).");
3639
+ }
3640
+ } else {
3641
+ gitnexusResult = await runGitnexusSetupPhase({ workspacePath: args.workspacePath });
3642
+ }
3643
+ if (gitnexusResult?.ok) {
3644
+ const updatedVars = buildScaffoldVariables({
3645
+ projectName: args.workspaceName,
3646
+ projectDescription: args.description,
3647
+ teamOwner: args.teamOwner,
3648
+ packVersion: args.packVersion,
3649
+ mode: "client",
3650
+ gitnexusReady: true
3651
+ });
3652
+ await writeRootClaudeMd(args.workspacePath, updatedVars);
3653
+ log.dim("Updated CLAUDE.md v\u1EDBi GitNexus section");
3654
+ }
3655
+ printInitSuccessBox(args.workspacePath, args.flow, aiResult, gitnexusResult);
3027
3656
  }
3028
3657
  async function maybeCreateWorkspaceRemote(args) {
3029
3658
  if (args.skipCommit) {
@@ -3033,7 +3662,7 @@ async function maybeCreateWorkspaceRemote(args) {
3033
3662
  let shouldCreate = args.createWorkspaceRemote;
3034
3663
  if (shouldCreate === void 0) {
3035
3664
  if (args.autoYes) return;
3036
- shouldCreate = await confirm3({
3665
+ shouldCreate = await confirm5({
3037
3666
  message: "T\u1EA1o remote GitHub cho workspace \u0111\u1EC3 share team? (Avatar state)",
3038
3667
  default: false
3039
3668
  });
@@ -3112,7 +3741,7 @@ async function maybeCreateWorkspaceRemote(args) {
3112
3741
  }
3113
3742
  }
3114
3743
  async function resolveWorkspacePath(parent, desiredName, force) {
3115
- const desired = join18(parent, desiredName);
3744
+ const desired = join22(parent, desiredName);
3116
3745
  if (await isEmptyOrMissing(desired)) return desired;
3117
3746
  log.warn(`Workspace path "${desired}" \u0111\xE3 c\xF3 n\u1ED9i dung.`);
3118
3747
  while (true) {
@@ -3143,7 +3772,7 @@ async function resolveWorkspacePath(parent, desiredName, force) {
3143
3772
  message: "T\xEAn workspace m\u1EDBi:",
3144
3773
  validate: (v) => v.trim().length > 0 ? true : "T\xEAn kh\xF4ng \u0111\u01B0\u1EE3c r\u1ED7ng"
3145
3774
  });
3146
- const newPath = join18(parent, newName.trim());
3775
+ const newPath = join22(parent, newName.trim());
3147
3776
  if (await isEmptyOrMissing(newPath)) return newPath;
3148
3777
  log.warn(`"${newPath}" c\u0169ng \u0111\xE3 c\xF3 n\u1ED9i dung. Th\u1EED t\xEAn kh\xE1c.`);
3149
3778
  }
@@ -3171,21 +3800,34 @@ function formatAiStatusLine(aiResult) {
3171
3800
  }
3172
3801
  return ` ${chalk.yellow("AI:")} failed (${aiResult.reason.slice(0, 60)}) \xB7 th\u1EED ${chalk.cyan("avatar ai setup")}`;
3173
3802
  }
3174
- function printInitSuccessBox(rootPath, flow, aiResult = null) {
3803
+ function formatGitnexusStatusLine(result) {
3804
+ if (result === null) {
3805
+ return ` ${chalk.yellow("GitNexus:")} skipped \xB7 ${chalk.cyan("avatar gitnexus install")} \u0111\u1EC3 setup sau`;
3806
+ }
3807
+ if (result.ok) {
3808
+ const parts = ["ready"];
3809
+ if (result.analyzed) parts.push("indexed");
3810
+ if (result.wikiGenerated) parts.push("wiki");
3811
+ if (result.mcpRegistered) parts.push("mcp");
3812
+ return ` ${chalk.green("GitNexus:")} ${parts.join(" \xB7 ")}`;
3813
+ }
3814
+ return ` ${chalk.yellow("GitNexus:")} skipped (${(result.reason ?? "unknown").slice(0, 40)}) \xB7 th\u1EED ${chalk.cyan("avatar gitnexus install")}`;
3815
+ }
3816
+ function printInitSuccessBox(rootPath, flow, aiResult = null, gitnexusResult = null) {
3175
3817
  const lines = [
3176
3818
  `${chalk.green("\u2713")} Workspace s\u1EB5n s\xE0ng: ${relative2(process.cwd(), rootPath) || rootPath}`,
3177
3819
  ` ${chalk.dim(`(flow: ${flow})`)}`,
3178
3820
  formatAiStatusLine(aiResult),
3821
+ formatGitnexusStatusLine(gitnexusResult),
3179
3822
  "",
3180
3823
  ` ${chalk.cyan(`cd ${rootPath}`)}`,
3181
3824
  ` ${chalk.cyan("claude")} M\u1EDF Claude Code \u1EDF workspace root`,
3182
3825
  "",
3183
- ` ${chalk.cyan("avatar commit --src")} Commit code l\xEAn remote src`,
3184
- ` ${chalk.cyan("avatar commit --avatar")} Commit Avatar state`,
3826
+ ` ${chalk.cyan("avatar commit src")} Commit code l\xEAn client remote`,
3185
3827
  ` ${chalk.cyan("avatar sync")} Pull team-ai-pack m\u1EDBi`,
3186
3828
  ` ${chalk.cyan("avatar uninstall")} G\u1EE1 Avatar (gi\u1EEF code)`
3187
3829
  ];
3188
- process.stdout.write(`${boxen4(lines.join("\n"), { padding: 1, borderStyle: "round" })}
3830
+ process.stdout.write(`${boxen5(lines.join("\n"), { padding: 1, borderStyle: "round" })}
3189
3831
  `);
3190
3832
  }
3191
3833
 
@@ -3236,18 +3878,18 @@ function registerSecretsCommand(program2) {
3236
3878
  }
3237
3879
 
3238
3880
  // src/commands/status.ts
3239
- import { promises as fs8 } from "fs";
3240
- import { join as join20 } from "path";
3241
- import boxen5 from "boxen";
3881
+ import { promises as fs10 } from "fs";
3882
+ import { join as join24 } from "path";
3883
+ import boxen6 from "boxen";
3242
3884
 
3243
3885
  // src/lib/pack-backup-manager.ts
3244
- import { promises as fs7 } from "fs";
3245
- import { join as join19 } from "path";
3886
+ import { promises as fs9 } from "fs";
3887
+ import { join as join23 } from "path";
3246
3888
  var BACKUP_DIR_NAME = "_backup";
3247
3889
  async function listBackups(projectRoot) {
3248
- const dir = join19(projectRoot, ".claude", BACKUP_DIR_NAME);
3890
+ const dir = join23(projectRoot, ".claude", BACKUP_DIR_NAME);
3249
3891
  if (!await pathExists(dir)) return [];
3250
- const entries = await fs7.readdir(dir, { withFileTypes: true });
3892
+ const entries = await fs9.readdir(dir, { withFileTypes: true });
3251
3893
  return entries.filter((e) => e.isDirectory()).map((e) => e.name).sort().reverse();
3252
3894
  }
3253
3895
 
@@ -3271,7 +3913,7 @@ function registerStatusCommand(program2) {
3271
3913
  }
3272
3914
  async function gatherStatus(cwd) {
3273
3915
  const projectName = cwd.split("/").filter(Boolean).pop() ?? "unknown";
3274
- const claudeRoot = join20(cwd, ".claude");
3916
+ const claudeRoot = join24(cwd, ".claude");
3275
3917
  const hasAvatar = await pathExists(claudeRoot);
3276
3918
  if (!hasAvatar) {
3277
3919
  return {
@@ -3284,9 +3926,9 @@ async function gatherStatus(cwd) {
3284
3926
  hasAvatar: false
3285
3927
  };
3286
3928
  }
3287
- const packVersion = await isGitRepo(join20(claudeRoot, "pack")) ? await readPinnedPackVersion(cwd).catch(() => null) : null;
3288
- const pendingDir = join20(claudeRoot, "_pending");
3289
- const pendingCount = await pathExists(pendingDir) ? (await fs8.readdir(pendingDir)).filter((n) => n.endsWith(".diff.md")).length : 0;
3929
+ const packVersion = await isGitRepo(join24(claudeRoot, "pack")) ? await readPinnedPackVersion(cwd).catch(() => null) : null;
3930
+ const pendingDir = join24(claudeRoot, "_pending");
3931
+ const pendingCount = await pathExists(pendingDir) ? (await fs10.readdir(pendingDir)).filter((n) => n.endsWith(".diff.md")).length : 0;
3290
3932
  const backupCount = (await listBackups(cwd)).length;
3291
3933
  const techStackSummary = await readTechStackFirstLine(claudeRoot);
3292
3934
  return {
@@ -3300,7 +3942,7 @@ async function gatherStatus(cwd) {
3300
3942
  };
3301
3943
  }
3302
3944
  async function readTechStackFirstLine(claudeRoot) {
3303
- const techStackPath = join20(claudeRoot, "project", "tech-stack.md");
3945
+ const techStackPath = join24(claudeRoot, "project", "tech-stack.md");
3304
3946
  if (!await pathExists(techStackPath)) return "(no tech-stack.md)";
3305
3947
  const content = await readText(techStackPath);
3306
3948
  const firstNonHeaderLine = content.split("\n").find((l) => l.trim() && !l.startsWith("#") && !l.startsWith(">"));
@@ -3316,7 +3958,7 @@ function renderStatusBox(s) {
3316
3958
  `${chalk.dim("Backups:")} ${s.backupCount}`,
3317
3959
  `${chalk.dim("Tech stack:")} ${s.techStackSummary}`
3318
3960
  ];
3319
- process.stdout.write(`${boxen5(lines.join("\n"), { padding: 1, borderStyle: "round" })}
3961
+ process.stdout.write(`${boxen6(lines.join("\n"), { padding: 1, borderStyle: "round" })}
3320
3962
  `);
3321
3963
  }
3322
3964
 
@@ -3335,33 +3977,33 @@ function registerToolsCommand(program2) {
3335
3977
 
3336
3978
  // src/commands/uninstall.ts
3337
3979
  import { relative as relative3 } from "path";
3338
- import { confirm as confirm4 } from "@inquirer/prompts";
3339
- import boxen6 from "boxen";
3980
+ import { confirm as confirm6 } from "@inquirer/prompts";
3981
+ import boxen7 from "boxen";
3340
3982
 
3341
3983
  // src/lib/create-uninstall-backup-snapshot.ts
3342
3984
  import { cp, mkdir, writeFile } from "fs/promises";
3343
- import { homedir as homedir3 } from "os";
3344
- import { basename as basename2, join as join21 } from "path";
3345
- var UNINSTALL_BACKUPS_DIR = join21(homedir3(), ".avatar", "uninstall-backups");
3985
+ import { homedir as homedir4 } from "os";
3986
+ import { basename as basename2, join as join25 } from "path";
3987
+ var UNINSTALL_BACKUPS_DIR = join25(homedir4(), ".avatar", "uninstall-backups");
3346
3988
  async function createUninstallBackupSnapshot(projectRoot, artifacts, avatarVersion) {
3347
3989
  const projectName = basename2(projectRoot);
3348
3990
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
3349
- const backupDir = join21(UNINSTALL_BACKUPS_DIR, `${projectName}-${timestamp}`);
3991
+ const backupDir = join25(UNINSTALL_BACKUPS_DIR, `${projectName}-${timestamp}`);
3350
3992
  await mkdir(backupDir, { recursive: true, mode: 448 });
3351
3993
  if (artifacts.claudeDir) {
3352
- await cp(artifacts.claudeDir, join21(backupDir, ".claude"), { recursive: true });
3994
+ await cp(artifacts.claudeDir, join25(backupDir, ".claude"), { recursive: true });
3353
3995
  }
3354
3996
  if (artifacts.claudeMd) {
3355
- await cp(artifacts.claudeMd, join21(backupDir, "CLAUDE.md"));
3997
+ await cp(artifacts.claudeMd, join25(backupDir, "CLAUDE.md"));
3356
3998
  }
3357
3999
  if (artifacts.postMergeHook || artifacts.prePushHook) {
3358
- const hooksBackupDir = join21(backupDir, "hooks");
4000
+ const hooksBackupDir = join25(backupDir, "hooks");
3359
4001
  await mkdir(hooksBackupDir, { recursive: true });
3360
4002
  if (artifacts.postMergeHook) {
3361
- await cp(artifacts.postMergeHook, join21(hooksBackupDir, "post-merge"));
4003
+ await cp(artifacts.postMergeHook, join25(hooksBackupDir, "post-merge"));
3362
4004
  }
3363
4005
  if (artifacts.prePushHook) {
3364
- await cp(artifacts.prePushHook, join21(hooksBackupDir, "pre-push"));
4006
+ await cp(artifacts.prePushHook, join25(hooksBackupDir, "pre-push"));
3365
4007
  }
3366
4008
  }
3367
4009
  const manifest = {
@@ -3376,27 +4018,27 @@ async function createUninstallBackupSnapshot(projectRoot, artifacts, avatarVersi
3376
4018
  prePushHook: !!artifacts.prePushHook
3377
4019
  }
3378
4020
  };
3379
- await writeFile(join21(backupDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
4021
+ await writeFile(join25(backupDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
3380
4022
  return backupDir;
3381
4023
  }
3382
4024
 
3383
4025
  // src/lib/detect-avatar-project-artifacts.ts
3384
- import { existsSync as existsSync7 } from "fs";
3385
- import { join as join22 } from "path";
4026
+ import { existsSync as existsSync9 } from "fs";
4027
+ import { join as join26 } from "path";
3386
4028
  function existsOrNull(path) {
3387
- return existsSync7(path) ? path : null;
4029
+ return existsSync9(path) ? path : null;
3388
4030
  }
3389
4031
  function detectAvatarProjectArtifacts(projectRoot) {
3390
- const claudeDir = existsOrNull(join22(projectRoot, ".claude"));
3391
- const claudeMd = existsOrNull(join22(projectRoot, "CLAUDE.md"));
3392
- const postMergeHook = existsOrNull(join22(projectRoot, ".git", "hooks", "post-merge"));
4032
+ const claudeDir = existsOrNull(join26(projectRoot, ".claude"));
4033
+ const claudeMd = existsOrNull(join26(projectRoot, "CLAUDE.md"));
4034
+ const postMergeHook = existsOrNull(join26(projectRoot, ".git", "hooks", "post-merge"));
3393
4035
  const prePushHook = existsOrNull(
3394
- join22(projectRoot, ".git", "modules", "src", "hooks", "pre-push")
4036
+ join26(projectRoot, ".git", "modules", "src", "hooks", "pre-push")
3395
4037
  );
3396
- const gitignorePath = existsOrNull(join22(projectRoot, ".gitignore"));
3397
- const gitmodulesPath = existsOrNull(join22(projectRoot, ".gitmodules"));
3398
- const notesDir = existsOrNull(join22(projectRoot, "notes"));
3399
- const scriptsDir = existsOrNull(join22(projectRoot, "scripts"));
4038
+ const gitignorePath = existsOrNull(join26(projectRoot, ".gitignore"));
4039
+ const gitmodulesPath = existsOrNull(join26(projectRoot, ".gitmodules"));
4040
+ const notesDir = existsOrNull(join26(projectRoot, "notes"));
4041
+ const scriptsDir = existsOrNull(join26(projectRoot, "scripts"));
3400
4042
  const hasAnyArtifact = !!(claudeDir || claudeMd || postMergeHook || prePushHook);
3401
4043
  return {
3402
4044
  hasAnyArtifact,
@@ -3417,11 +4059,11 @@ async function executeUninstallDeletion(artifacts, flags) {
3417
4059
  if (artifacts.claudeDir) {
3418
4060
  if (flags.keepSubmodule) {
3419
4061
  const { readdir: readdir2 } = await import("fs/promises");
3420
- const { join: join23 } = await import("path");
4062
+ const { join: join27 } = await import("path");
3421
4063
  const entries = await readdir2(artifacts.claudeDir);
3422
4064
  for (const entry of entries) {
3423
4065
  if (entry === "pack") continue;
3424
- await rm(join23(artifacts.claudeDir, entry), { recursive: true, force: true });
4066
+ await rm(join27(artifacts.claudeDir, entry), { recursive: true, force: true });
3425
4067
  }
3426
4068
  } else {
3427
4069
  await rm(artifacts.claudeDir, { recursive: true, force: true });
@@ -3490,7 +4132,7 @@ async function removeSubmoduleEntry(gitmodulesPath, submodulePath) {
3490
4132
  }
3491
4133
 
3492
4134
  // src/commands/uninstall.ts
3493
- var CLI_VERSION = "1.3.3";
4135
+ var CLI_VERSION = "1.4.0";
3494
4136
  function registerUninstallCommand(program2) {
3495
4137
  program2.command("uninstall").description("G\u1EE1 Avatar kh\u1ECFi project \u2014 backup t\u1EF1 \u0111\u1ED9ng (M11)").option("--yes", "Skip confirm prompt").option("--no-backup", "Kh\xF4ng t\u1EA1o backup tr\u01B0\u1EDBc khi x\xF3a (nguy hi\u1EC3m)").option("--keep-submodule", "Gi\u1EEF submodule .claude/pack/").option("--keep-hooks", "Gi\u1EEF git hooks post-merge, pre-push").option("--dry-run", "Hi\u1EC3n th\u1ECB danh s\xE1ch s\u1EBD x\xF3a, kh\xF4ng th\u1EF1c thi").action(async (opts) => {
3496
4138
  try {
@@ -3514,7 +4156,7 @@ async function runUninstall(opts) {
3514
4156
  return;
3515
4157
  }
3516
4158
  if (!opts.yes) {
3517
- const ok = await confirm4({
4159
+ const ok = await confirm6({
3518
4160
  message: "Ti\u1EBFp t\u1EE5c g\u1EE1 Avatar?",
3519
4161
  default: false
3520
4162
  });
@@ -3567,12 +4209,12 @@ function printUninstallSuccessBox(backupPath) {
3567
4209
  lines.push(` ${chalk.dim("Backup:")} ${backupPath}`);
3568
4210
  lines.push(` ${chalk.dim("Restore:")} ${chalk.cyan(`cp -r "${backupPath}"/* .`)}`);
3569
4211
  }
3570
- process.stdout.write(`${boxen6(lines.join("\n"), { padding: 1, borderStyle: "round" })}
4212
+ process.stdout.write(`${boxen7(lines.join("\n"), { padding: 1, borderStyle: "round" })}
3571
4213
  `);
3572
4214
  }
3573
4215
 
3574
4216
  // src/index.ts
3575
- var CLI_VERSION2 = "1.3.3";
4217
+ var CLI_VERSION2 = "1.4.0";
3576
4218
  var program = new Command();
3577
4219
  program.name("avatar").description("AI harness CLI for NAL Vietnam engineering").version(CLI_VERSION2, "-v, --version", "Hi\u1EC3n th\u1ECB phi\xEAn b\u1EA3n Avatar CLI").addHelpText(
3578
4220
  "beforeAll",
@@ -3599,6 +4241,7 @@ registerToolsCommand(program);
3599
4241
  registerSecretsCommand(program);
3600
4242
  registerMcpRunCommand(program);
3601
4243
  registerAiCommand(program);
4244
+ registerGitnexusCommand(program);
3602
4245
  registerUninstallCommand(program);
3603
4246
  program.parseAsync(process.argv).catch((err) => {
3604
4247
  const msg = err instanceof Error ? err.message : String(err);