@nalvietnam/avatar-cli 1.3.3 → 1.4.1

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) {
@@ -183,6 +183,33 @@ function spinner(text) {
183
183
  isEnabled: process.stdout.isTTY ?? false
184
184
  }).start();
185
185
  }
186
+ function spinnerWithElapsed(prefix) {
187
+ const startMs = Date.now();
188
+ const sp = spinner(`${prefix} (0:00)`);
189
+ const formatElapsed = () => {
190
+ const sec = Math.floor((Date.now() - startMs) / 1e3);
191
+ const m = Math.floor(sec / 60);
192
+ const s = sec % 60;
193
+ return `${m}:${String(s).padStart(2, "0")}`;
194
+ };
195
+ const interval = setInterval(() => {
196
+ sp.text = `${prefix} (${formatElapsed()})`;
197
+ }, 1e3);
198
+ return {
199
+ succeed: (text) => {
200
+ clearInterval(interval);
201
+ sp.succeed(`${text} (${formatElapsed()})`);
202
+ },
203
+ fail: (text) => {
204
+ clearInterval(interval);
205
+ sp.fail(`${text} (${formatElapsed()})`);
206
+ },
207
+ stop: () => {
208
+ clearInterval(interval);
209
+ sp.stop();
210
+ }
211
+ };
212
+ }
186
213
 
187
214
  // src/lib/check-claude-code-subscription-and-quota.ts
188
215
  var QUOTA_VERIFY_TIMEOUT_MS = 3e4;
@@ -1306,14 +1333,209 @@ async function applyFixes(checks) {
1306
1333
  if (count === 0) log.dim("Kh\xF4ng c\xF3 g\xEC \u0111\u1EC3 fix t\u1EF1 \u0111\u1ED9ng.");
1307
1334
  }
1308
1335
 
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";
1336
+ // src/commands/gitnexus.ts
1337
+ import { spawnSync as spawnSync11 } from "child_process";
1338
+ import { promises as fs8 } from "fs";
1339
+ import { join as join15 } from "path";
1313
1340
 
1314
- // src/lib/add-team-pack-submodule-with-retry-on-network-fail.ts
1341
+ // src/lib/run-gitnexus-setup-and-analyze.ts
1342
+ import { spawnSync as spawnSync7 } from "child_process";
1343
+ import { existsSync as existsSync4 } from "fs";
1344
+ import { join as join12 } from "path";
1345
+ var SETUP_TIMEOUT_MS = 2 * 60 * 1e3;
1346
+ var ANALYZE_TIMEOUT_MS = 5 * 60 * 1e3;
1347
+ var GitnexusOperationError = class extends Error {
1348
+ operation;
1349
+ reason;
1350
+ exitCode;
1351
+ stderr;
1352
+ constructor(operation, reason, message, exitCode = null, stderr) {
1353
+ super(message);
1354
+ this.name = "GitnexusOperationError";
1355
+ this.operation = operation;
1356
+ this.reason = reason;
1357
+ this.exitCode = exitCode;
1358
+ this.stderr = stderr;
1359
+ }
1360
+ };
1361
+ function classifyOperationFailure(operation, exitCode, signal, stderrSample) {
1362
+ if (signal === "SIGTERM") {
1363
+ return new GitnexusOperationError(
1364
+ operation,
1365
+ "timeout",
1366
+ `gitnexus ${operation} timeout. Check m\u1EA1ng / repo size.`,
1367
+ null,
1368
+ stderrSample
1369
+ );
1370
+ }
1371
+ const stderr = stderrSample.toLowerCase();
1372
+ if (stderr.includes("eacces") || stderr.includes("permission denied")) {
1373
+ return new GitnexusOperationError(
1374
+ operation,
1375
+ "permission",
1376
+ `gitnexus ${operation} fail (permission). Check write access ~/.claude ho\u1EB7c cwd.`,
1377
+ exitCode,
1378
+ stderrSample
1379
+ );
1380
+ }
1381
+ return new GitnexusOperationError(
1382
+ operation,
1383
+ "non-zero-exit",
1384
+ `gitnexus ${operation} exit ${exitCode ?? "null"}. Xem log ph\xEDa tr\xEAn.`,
1385
+ exitCode,
1386
+ stderrSample
1387
+ );
1388
+ }
1389
+ function tailLines(text, n) {
1390
+ const lines = text.split("\n");
1391
+ return lines.slice(-n).join("\n");
1392
+ }
1393
+ function runGitnexusSetup() {
1394
+ const sp = spinnerWithElapsed("Setup GitNexus global skills (~/.claude/skills/gitnexus-*)");
1395
+ const result = spawnSync7("gitnexus", ["setup"], {
1396
+ stdio: ["ignore", "pipe", "pipe"],
1397
+ timeout: SETUP_TIMEOUT_MS,
1398
+ encoding: "utf8"
1399
+ });
1400
+ if (result.status !== 0 || result.signal === "SIGTERM") {
1401
+ sp.fail("GitNexus setup failed");
1402
+ const stderr = (result.stderr || "").trim();
1403
+ const stdout = (result.stdout || "").trim();
1404
+ if (stderr) process.stderr.write(`${tailLines(stderr, 30)}
1405
+ `);
1406
+ else if (stdout) process.stderr.write(`${tailLines(stdout, 30)}
1407
+ `);
1408
+ throw classifyOperationFailure("setup", result.status, result.signal, stderr);
1409
+ }
1410
+ sp.succeed("GitNexus setup OK (global skills installed)");
1411
+ }
1412
+ function runGitnexusAnalyze(workspacePath) {
1413
+ const sp = spinnerWithElapsed(`Analyze workspace ${workspacePath} (1-3 ph\xFAt)`);
1414
+ const result = spawnSync7("gitnexus", ["analyze", "."], {
1415
+ cwd: workspacePath,
1416
+ stdio: ["ignore", "pipe", "pipe"],
1417
+ timeout: ANALYZE_TIMEOUT_MS,
1418
+ encoding: "utf8"
1419
+ });
1420
+ if (result.status !== 0 || result.signal === "SIGTERM") {
1421
+ sp.fail("Analyze failed");
1422
+ const stderr = (result.stderr || "").trim();
1423
+ const stdout = (result.stdout || "").trim();
1424
+ if (stderr) process.stderr.write(`${tailLines(stderr, 30)}
1425
+ `);
1426
+ else if (stdout) process.stderr.write(`${tailLines(stdout, 30)}
1427
+ `);
1428
+ throw classifyOperationFailure("analyze", result.status, result.signal, stderr);
1429
+ }
1430
+ const metaPath = join12(workspacePath, ".gitnexus", "meta.json");
1431
+ if (!existsSync4(metaPath)) {
1432
+ sp.fail("Analyze exit 0 nh\u01B0ng kh\xF4ng th\u1EA5y meta.json");
1433
+ throw new GitnexusOperationError(
1434
+ "analyze",
1435
+ "missing-output",
1436
+ `gitnexus analyze xong nh\u01B0ng kh\xF4ng th\u1EA5y ${metaPath}. Repo c\xF3 th\u1EC3 empty ho\u1EB7c gitnexus fail silent.`
1437
+ );
1438
+ }
1439
+ sp.succeed(`Analyze OK (index t\u1EA1i ${join12(workspacePath, ".gitnexus")})`);
1440
+ }
1441
+
1442
+ // src/lib/run-gitnexus-setup-phase.ts
1443
+ import { confirm as confirm3 } from "@inquirer/prompts";
1444
+ import boxen2 from "boxen";
1445
+
1446
+ // src/lib/detect-gitnexus-installation.ts
1315
1447
  import { spawnSync as spawnSync8 } from "child_process";
1316
- import { select as select5 } from "@inquirer/prompts";
1448
+ var VERSION_PROBE_TIMEOUT_MS2 = 5e3;
1449
+ var SEMVER_REGEX2 = /(\d+\.\d+\.\d+)/;
1450
+ function probeGitnexusBinaryPath() {
1451
+ const isWindows = detectHostPlatform() === "win32";
1452
+ const probeCmd = isWindows ? "where" : "which";
1453
+ const result = spawnSync8(probeCmd, ["gitnexus"], { encoding: "utf8" });
1454
+ if (result.error || result.status !== 0) return null;
1455
+ const out = (result.stdout || "").trim();
1456
+ if (!out) return null;
1457
+ return out.split(/\r?\n/)[0].trim();
1458
+ }
1459
+ function probeGitnexusVersion() {
1460
+ const result = spawnSync8("gitnexus", ["--version"], {
1461
+ encoding: "utf8",
1462
+ timeout: VERSION_PROBE_TIMEOUT_MS2
1463
+ });
1464
+ if (result.error || result.status !== 0) return null;
1465
+ const out = (result.stdout || "").trim();
1466
+ const match = SEMVER_REGEX2.exec(out);
1467
+ return match ? match[1] : null;
1468
+ }
1469
+ function detectGitnexusInstallation() {
1470
+ const path = probeGitnexusBinaryPath();
1471
+ if (!path) {
1472
+ return { installed: false, version: null, path: null };
1473
+ }
1474
+ const version = probeGitnexusVersion();
1475
+ return { installed: true, version, path };
1476
+ }
1477
+
1478
+ // src/lib/install-gitnexus-via-npm.ts
1479
+ import { spawnSync as spawnSync9 } from "child_process";
1480
+ var NPM_INSTALL_TIMEOUT_MS2 = 5 * 60 * 1e3;
1481
+ var GITNEXUS_PACKAGE = "gitnexus";
1482
+ var InstallGitnexusError = class extends Error {
1483
+ reason;
1484
+ exitCode;
1485
+ constructor(reason, message, exitCode = null) {
1486
+ super(message);
1487
+ this.name = "InstallGitnexusError";
1488
+ this.reason = reason;
1489
+ this.exitCode = exitCode;
1490
+ }
1491
+ };
1492
+ function classifyNpmFailure2(exitCode, stderrSample) {
1493
+ const stderr = stderrSample.toLowerCase();
1494
+ if (stderr.includes("eacces") || stderr.includes("permission denied")) {
1495
+ return new InstallGitnexusError(
1496
+ "permission-denied",
1497
+ `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).`,
1498
+ exitCode
1499
+ );
1500
+ }
1501
+ if (stderr.includes("enospc") || stderr.includes("no space")) {
1502
+ return new InstallGitnexusError("disk-full", "\u0110\u0129a \u0111\u1EA7y. Free disk space r\u1ED3i th\u1EED l\u1EA1i.", exitCode);
1503
+ }
1504
+ return new InstallGitnexusError(
1505
+ "generic",
1506
+ `npm install th\u1EA5t b\u1EA1i (exit ${exitCode ?? "null"}). Xem log npm ph\xEDa tr\xEAn.`,
1507
+ exitCode
1508
+ );
1509
+ }
1510
+ function installGitnexusViaNpm() {
1511
+ log.info("\u0110ang c\xE0i GitNexus qua npm (c\xF3 th\u1EC3 m\u1EA5t 1-2 ph\xFAt)...");
1512
+ const result = spawnSync9("npm", ["install", "-g", GITNEXUS_PACKAGE], {
1513
+ stdio: ["inherit", "inherit", "pipe"],
1514
+ timeout: NPM_INSTALL_TIMEOUT_MS2,
1515
+ encoding: "utf8"
1516
+ });
1517
+ if (result.signal === "SIGTERM") {
1518
+ throw new InstallGitnexusError(
1519
+ "timeout",
1520
+ `npm install timeout sau ${NPM_INSTALL_TIMEOUT_MS2 / 1e3}s. Check m\u1EA1ng r\u1ED3i th\u1EED l\u1EA1i.`,
1521
+ null
1522
+ );
1523
+ }
1524
+ if (result.status !== 0) {
1525
+ if (result.stderr) process.stderr.write(result.stderr);
1526
+ throw classifyNpmFailure2(result.status, result.stderr || "");
1527
+ }
1528
+ const probe = detectGitnexusInstallation();
1529
+ if (!probe.installed || !probe.path) {
1530
+ throw new InstallGitnexusError(
1531
+ "binary-not-in-path",
1532
+ "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.",
1533
+ null
1534
+ );
1535
+ }
1536
+ log.success(`\u0110\xE3 c\xE0i GitNexus${probe.version ? ` v${probe.version}` : ""} t\u1EA1i ${probe.path}`);
1537
+ return { version: probe.version, path: probe.path };
1538
+ }
1317
1539
 
1318
1540
  // src/lib/prompt-recovery-action-on-failure.ts
1319
1541
  import { input as input3, select as select3 } from "@inquirer/prompts";
@@ -1339,24 +1561,419 @@ async function promptRetryOrSkip(args) {
1339
1561
  });
1340
1562
  }
1341
1563
 
1564
+ // src/lib/register-gitnexus-mcp-server.ts
1565
+ import { promises as fs7 } from "fs";
1566
+ import { homedir as homedir3 } from "os";
1567
+ import { join as join13 } from "path";
1568
+ var MCP_FILE_MODE = 384;
1569
+ var EXPECTED_GITNEXUS_ENTRY = {
1570
+ command: "gitnexus",
1571
+ args: ["mcp"]
1572
+ };
1573
+ function getMcpServersPath() {
1574
+ return join13(homedir3(), ".claude", "mcp_servers.json");
1575
+ }
1576
+ function isEntryEqual(a, b) {
1577
+ if (a === b) return true;
1578
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
1579
+ return JSON.stringify(a) === JSON.stringify(b);
1580
+ }
1581
+ async function backupExistingFile(path) {
1582
+ const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
1583
+ const backupPath = `${path}.avatar-backup-${ts}`;
1584
+ await fs7.copyFile(path, backupPath);
1585
+ return backupPath;
1586
+ }
1587
+ async function registerGitnexusMcpServer() {
1588
+ const path = getMcpServersPath();
1589
+ let existing = {};
1590
+ let fileExisted = false;
1591
+ if (await pathExists(path)) {
1592
+ fileExisted = true;
1593
+ try {
1594
+ existing = await readJson(path);
1595
+ } catch (err) {
1596
+ throw new Error(
1597
+ `MCP config corrupted (${path}): ${err.message}. Backup + x\xF3a file \u0111\u1EC3 Avatar t\u1EA1o l\u1EA1i.`
1598
+ );
1599
+ }
1600
+ }
1601
+ const existingEntry = existing.mcp_servers?.gitnexus;
1602
+ if (existingEntry && isEntryEqual(existingEntry, EXPECTED_GITNEXUS_ENTRY)) {
1603
+ log.dim(`MCP entry gitnexus \u0111\xE3 \u0111\xFAng t\u1EA1i ${path} (no-op)`);
1604
+ return { path, wasUpdated: false };
1605
+ }
1606
+ let backup;
1607
+ if (fileExisted) {
1608
+ backup = await backupExistingFile(path);
1609
+ log.dim(`Backup ${path} \u2192 ${backup}`);
1610
+ }
1611
+ const merged = {
1612
+ ...existing,
1613
+ mcp_servers: {
1614
+ ...existing.mcp_servers || {},
1615
+ gitnexus: EXPECTED_GITNEXUS_ENTRY
1616
+ }
1617
+ };
1618
+ await writeJsonAtomic(path, merged, MCP_FILE_MODE);
1619
+ try {
1620
+ await fs7.chmod(path, MCP_FILE_MODE);
1621
+ } catch {
1622
+ }
1623
+ log.success(`Registered MCP server: gitnexus \u2192 ${path}`);
1624
+ return { path, wasUpdated: true, backup };
1625
+ }
1626
+
1627
+ // src/lib/run-gitnexus-wiki-conditional.ts
1628
+ import { spawnSync as spawnSync10 } from "child_process";
1629
+ import { existsSync as existsSync5 } from "fs";
1630
+ import { join as join14 } from "path";
1631
+ import { confirm as confirm2 } from "@inquirer/prompts";
1632
+ var WIKI_TIMEOUT_MS = 15 * 60 * 1e3;
1633
+ var DEFAULT_LLMLITE_MODEL = "nal-claude";
1634
+ async function readSettingsForWikiCredentials(workspacePath) {
1635
+ const settingsPath = join14(workspacePath, ".claude", "settings.json");
1636
+ if (!await pathExists(settingsPath)) return null;
1637
+ try {
1638
+ const settings = await readJson(settingsPath);
1639
+ const env = settings.env || {};
1640
+ const apiKey = typeof env.ANTHROPIC_AUTH_TOKEN === "string" ? env.ANTHROPIC_AUTH_TOKEN : null;
1641
+ const baseUrl = typeof env.ANTHROPIC_BASE_URL === "string" ? env.ANTHROPIC_BASE_URL : null;
1642
+ if (!apiKey || !baseUrl) return null;
1643
+ const model = typeof env.ANTHROPIC_MODEL === "string" && env.ANTHROPIC_MODEL.length > 0 ? env.ANTHROPIC_MODEL : DEFAULT_LLMLITE_MODEL;
1644
+ return { apiKey, baseUrl, model };
1645
+ } catch {
1646
+ return null;
1647
+ }
1648
+ }
1649
+ async function confirmWikiGeneration(baseUrl, model) {
1650
+ return await confirm2({
1651
+ message: `Generate wiki cho workspace? (~$0.50 qua ${baseUrl} model=${model}, 2-5 ph\xFAt). Continue?`,
1652
+ default: true
1653
+ });
1654
+ }
1655
+ function tailLines2(text, n) {
1656
+ const lines = text.split("\n");
1657
+ return lines.slice(-n).join("\n");
1658
+ }
1659
+ async function runGitnexusWikiConditional(workspacePath) {
1660
+ const creds = await readSettingsForWikiCredentials(workspacePath);
1661
+ if (!creds) {
1662
+ log.warn("Subscription mode (ho\u1EB7c settings.json kh\xF4ng c\xF3 LLMLite key) \u2192 skip wiki gen.");
1663
+ log.dim("\u0110\u1EC3 gen wiki sau, ch\u1EA1y manual: gitnexus wiki . --api-key <openai-key>");
1664
+ return { ran: false, skipped: true, reason: "subscription-mode" };
1665
+ }
1666
+ const proceed = await confirmWikiGeneration(creds.baseUrl, creds.model);
1667
+ if (!proceed) {
1668
+ log.dim(
1669
+ "User decline wiki gen \u2014 workspace OK kh\xF4ng c\xF3 wiki. Ch\u1EA1y `gitnexus wiki` manual sau khi c\u1EA7n."
1670
+ );
1671
+ return { ran: false, skipped: true, reason: "user-declined" };
1672
+ }
1673
+ const sp = spinnerWithElapsed(`Generating wiki via ${creds.baseUrl} model=${creds.model}`);
1674
+ const result = spawnSync10(
1675
+ "gitnexus",
1676
+ ["wiki", ".", "--api-key", creds.apiKey, "--base-url", creds.baseUrl, "--model", creds.model],
1677
+ {
1678
+ cwd: workspacePath,
1679
+ stdio: ["ignore", "pipe", "pipe"],
1680
+ timeout: WIKI_TIMEOUT_MS,
1681
+ encoding: "utf8"
1682
+ }
1683
+ );
1684
+ if (result.status !== 0 || result.signal === "SIGTERM") {
1685
+ const reason = result.signal === "SIGTERM" ? "timeout" : "non-zero-exit";
1686
+ sp.fail(`Wiki gen ${reason} (exit ${result.status ?? "null"})`);
1687
+ const stderr = (result.stderr || "").trim();
1688
+ const stdout = (result.stdout || "").trim();
1689
+ if (stderr) process.stderr.write(`${tailLines2(stderr, 30)}
1690
+ `);
1691
+ else if (stdout) process.stderr.write(`${tailLines2(stdout, 30)}
1692
+ `);
1693
+ return {
1694
+ ran: false,
1695
+ skipped: true,
1696
+ reason: "fail",
1697
+ detail: `Wiki gen ${reason} (exit ${result.status ?? "null"})`
1698
+ };
1699
+ }
1700
+ const wikiPath = join14(workspacePath, ".gitnexus", "wiki", "index.html");
1701
+ if (!existsSync5(wikiPath)) {
1702
+ sp.fail("Wiki exit 0 nh\u01B0ng kh\xF4ng th\u1EA5y index.html");
1703
+ return {
1704
+ ran: false,
1705
+ skipped: true,
1706
+ reason: "fail",
1707
+ detail: `Wiki exit 0 nh\u01B0ng kh\xF4ng th\u1EA5y ${wikiPath}`
1708
+ };
1709
+ }
1710
+ sp.succeed(`Wiki ready: ${wikiPath}`);
1711
+ return { ran: true, skipped: false, wikiPath };
1712
+ }
1713
+
1714
+ // src/lib/run-gitnexus-setup-phase.ts
1715
+ async function promptInstallGitnexus() {
1716
+ const lines = [
1717
+ chalk.bold("\u{1F9E0} GitNexus ch\u01B0a c\xE0i"),
1718
+ "",
1719
+ "GitNexus = code intelligence layer cho Claude Code:",
1720
+ " \u2022 Architectural awareness (impact analysis)",
1721
+ " \u2022 Call chain debug + blast radius tr\u01B0\u1EDBc refactor",
1722
+ " \u2022 Wiki HTML t\u1EF1 gen m\xF4 t\u1EA3 codebase",
1723
+ "",
1724
+ `S\u1EBD c\xE0i: ${chalk.cyan("npm install -g gitnexus")} (global)`
1725
+ ];
1726
+ process.stdout.write(
1727
+ `${boxen2(lines.join("\n"), { padding: 1, borderStyle: "round", borderColor: "cyan" })}
1728
+ `
1729
+ );
1730
+ return await confirm3({ message: "C\xE0i GitNexus global?", default: true });
1731
+ }
1732
+ async function installWithRecovery() {
1733
+ while (true) {
1734
+ try {
1735
+ installGitnexusViaNpm();
1736
+ return true;
1737
+ } catch (err) {
1738
+ const message = err instanceof Error ? err.message : String(err);
1739
+ 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.";
1740
+ const action = await promptRetryOrSkip({
1741
+ taskName: "C\xE0i GitNexus qua npm",
1742
+ reason: message,
1743
+ allowSkip: true,
1744
+ hint
1745
+ });
1746
+ if (action === "abort") {
1747
+ throw new UserAbortedRecoveryError("User abort t\u1EA1i b\u01B0\u1EDBc c\xE0i GitNexus.");
1748
+ }
1749
+ if (action === "skip") return false;
1750
+ }
1751
+ }
1752
+ }
1753
+ async function analyzeWithRecovery(workspacePath) {
1754
+ while (true) {
1755
+ try {
1756
+ runGitnexusAnalyze(workspacePath);
1757
+ return true;
1758
+ } catch (err) {
1759
+ const message = err instanceof Error ? err.message : String(err);
1760
+ 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.";
1761
+ const action = await promptRetryOrSkip({
1762
+ taskName: "GitNexus analyze workspace",
1763
+ reason: message,
1764
+ allowSkip: true,
1765
+ hint
1766
+ });
1767
+ if (action === "abort") {
1768
+ throw new UserAbortedRecoveryError("User abort t\u1EA1i b\u01B0\u1EDBc GitNexus analyze.");
1769
+ }
1770
+ if (action === "skip") return false;
1771
+ }
1772
+ }
1773
+ }
1774
+ async function runGitnexusSetupPhase(args) {
1775
+ const result = {
1776
+ ok: false,
1777
+ installed: false,
1778
+ analyzed: false,
1779
+ wikiGenerated: false,
1780
+ mcpRegistered: false
1781
+ };
1782
+ try {
1783
+ log.info("=== Phase 10: GitNexus Setup ===");
1784
+ let info = detectGitnexusInstallation();
1785
+ if (!info.installed) {
1786
+ const shouldInstall = await promptInstallGitnexus();
1787
+ if (!shouldInstall) {
1788
+ await appendAuditEntry("gitnexus_setup", "result=skipped,reason=user-declined");
1789
+ log.dim("Skip GitNexus. C\xE0i sau qua `avatar gitnexus install`.");
1790
+ result.reason = "user-declined";
1791
+ return result;
1792
+ }
1793
+ const installed = await installWithRecovery();
1794
+ if (!installed) {
1795
+ await appendAuditEntry("gitnexus_setup", "result=skipped,reason=install-skipped");
1796
+ log.dim("Skip GitNexus install. Workspace OK kh\xF4ng c\xF3 codebase intelligence.");
1797
+ result.reason = "install-skipped";
1798
+ return result;
1799
+ }
1800
+ info = detectGitnexusInstallation();
1801
+ if (!info.installed) {
1802
+ throw new Error("C\xE0i xong nh\u01B0ng kh\xF4ng detect \u0111\u01B0\u1EE3c binary (PATH issue).");
1803
+ }
1804
+ }
1805
+ result.installed = true;
1806
+ log.success(`GitNexus available${info.version ? ` v${info.version}` : ""}`);
1807
+ try {
1808
+ runGitnexusSetup();
1809
+ } catch (err) {
1810
+ log.warn(`gitnexus setup fail: ${err.message}`);
1811
+ log.dim("Skip global skills install. Workspace v\u1EABn d\xF9ng \u0111\u01B0\u1EE3c.");
1812
+ }
1813
+ const analyzed = await analyzeWithRecovery(args.workspacePath);
1814
+ if (!analyzed) {
1815
+ await appendAuditEntry("gitnexus_setup", "result=skipped,reason=analyze-skipped");
1816
+ log.dim("Skip analyze. GitNexus installed nh\u01B0ng ch\u01B0a index.");
1817
+ result.reason = "analyze-skipped";
1818
+ return result;
1819
+ }
1820
+ result.analyzed = true;
1821
+ const wikiResult = await runGitnexusWikiConditional(args.workspacePath);
1822
+ result.wikiGenerated = wikiResult.ran;
1823
+ if (wikiResult.skipped && wikiResult.reason === "fail") {
1824
+ log.warn(`Wiki gen fail (workspace v\u1EABn OK): ${wikiResult.detail ?? "unknown"}`);
1825
+ }
1826
+ try {
1827
+ const mcpResult = await registerGitnexusMcpServer();
1828
+ result.mcpRegistered = true;
1829
+ if (!mcpResult.wasUpdated) {
1830
+ log.dim("MCP server gitnexus \u0111\xE3 registered tr\u01B0\u1EDBc \u0111\xF3.");
1831
+ }
1832
+ } catch (err) {
1833
+ log.warn(`MCP server register fail: ${err.message}`);
1834
+ log.dim(
1835
+ "Workspace OK nh\u01B0ng Claude Code kh\xF4ng t\u1EF1 attach MCP server. Manual add v\xE0o ~/.claude/mcp_servers.json."
1836
+ );
1837
+ }
1838
+ result.ok = true;
1839
+ await appendAuditEntry(
1840
+ "gitnexus_setup",
1841
+ `result=ok,analyzed=${result.analyzed},wiki=${result.wikiGenerated},mcp=${result.mcpRegistered}`
1842
+ );
1843
+ log.success("GitNexus ready");
1844
+ return result;
1845
+ } catch (err) {
1846
+ if (err instanceof UserAbortedRecoveryError) throw err;
1847
+ const message = err instanceof Error ? err.message : String(err);
1848
+ log.warn(`GitNexus setup th\u1EA5t b\u1EA1i: ${message}`);
1849
+ log.dim("Workspace v\u1EABn s\u1EB5n s\xE0ng. Setup sau qua `avatar gitnexus install`.");
1850
+ await appendAuditEntry("gitnexus_setup", `result=failed,error=${message.slice(0, 200)}`);
1851
+ result.reason = message;
1852
+ return result;
1853
+ }
1854
+ }
1855
+
1856
+ // src/commands/gitnexus.ts
1857
+ function ensureWorkspaceCwd2() {
1858
+ const cwd = process.cwd();
1859
+ const workspaceRoot = resolveAvatarWorkspaceRootFromCwd(cwd);
1860
+ if (!workspaceRoot) {
1861
+ log.error(
1862
+ `Kh\xF4ng t\xECm th\u1EA5y Avatar workspace t\u1EEB th\u01B0 m\u1EE5c hi\u1EC7n t\u1EA1i.
1863
+ Avatar workspace c\u1EA7n c\xF3: .claude/ + CLAUDE.md + src/ (ho\u1EB7c .gitmodules).
1864
+ B\u1EA1n \u0111ang \u1EDF: ${cwd}
1865
+ Cd v\xE0o workspace dir r\u1ED3i ch\u1EA1y l\u1EA1i.`
1866
+ );
1867
+ process.exit(1);
1868
+ }
1869
+ if (workspaceRoot !== cwd) {
1870
+ log.dim(`Detected workspace root: ${workspaceRoot}`);
1871
+ }
1872
+ return workspaceRoot;
1873
+ }
1874
+ async function runGitnexusInstall() {
1875
+ const workspacePath = ensureWorkspaceCwd2();
1876
+ const result = await runGitnexusSetupPhase({ workspacePath });
1877
+ if (result.ok) {
1878
+ log.success("GitNexus setup complete");
1879
+ log.dim("Update CLAUDE.md \u0111\u1EC3 re-render section GitNexus: re-run avatar init ho\u1EB7c ch\u1EC9nh tay.");
1880
+ } else {
1881
+ log.warn(`Setup kh\xF4ng complete: ${result.reason ?? "unknown"}`);
1882
+ }
1883
+ }
1884
+ async function runGitnexusStatus() {
1885
+ const workspacePath = ensureWorkspaceCwd2();
1886
+ const metaPath = join15(workspacePath, ".gitnexus", "meta.json");
1887
+ if (!await pathExists(metaPath)) {
1888
+ log.warn(`Ch\u01B0a c\xF3 ${metaPath}. Ch\u1EA1y: avatar gitnexus install`);
1889
+ return;
1890
+ }
1891
+ try {
1892
+ const meta = await readJson(metaPath);
1893
+ log.info(`Project: ${workspacePath}`);
1894
+ log.info(`Last commit: ${meta.lastCommit?.slice(0, 7) ?? "(unknown)"}`);
1895
+ log.info(`Indexed at: ${meta.indexedAt ?? "(unknown)"}`);
1896
+ if (meta.stats) {
1897
+ log.info(
1898
+ `Stats: ${meta.stats.files ?? "?"} files \xB7 ${meta.stats.nodes ?? "?"} nodes \xB7 ${meta.stats.edges ?? "?"} edges`
1899
+ );
1900
+ }
1901
+ if (meta.lastCommit) {
1902
+ const headResult = spawnSync11("git", ["rev-parse", "HEAD"], {
1903
+ cwd: workspacePath,
1904
+ encoding: "utf8"
1905
+ });
1906
+ if (headResult.status === 0) {
1907
+ const currentHead = headResult.stdout.trim();
1908
+ if (currentHead !== meta.lastCommit) {
1909
+ log.warn("\u26A0 Index stale \u2014 HEAD \u0111\xE3 ti\u1EBFn t\u1EEB lastCommit. Ch\u1EA1y: avatar gitnexus analyze");
1910
+ } else {
1911
+ log.dim("Index fresh (HEAD === lastCommit)");
1912
+ }
1913
+ }
1914
+ }
1915
+ const wikiPath = join15(workspacePath, ".gitnexus", "wiki", "index.html");
1916
+ if (await pathExists(wikiPath)) {
1917
+ const stat = await fs8.stat(wikiPath);
1918
+ log.info(`Wiki: ${stat.mtime.toISOString()}`);
1919
+ } else {
1920
+ log.dim("Wiki: ch\u01B0a generate (ch\u1EA1y gitnexus wiki manual)");
1921
+ }
1922
+ } catch (err) {
1923
+ log.error(`Read meta.json fail: ${err.message}`);
1924
+ process.exit(1);
1925
+ }
1926
+ }
1927
+ async function runGitnexusAnalyzeCommand() {
1928
+ const workspacePath = ensureWorkspaceCwd2();
1929
+ try {
1930
+ runGitnexusAnalyze(workspacePath);
1931
+ log.success("Index refreshed. Ch\u1EA1y `avatar gitnexus status` xem chi ti\u1EBFt.");
1932
+ } catch (err) {
1933
+ log.error(`Analyze fail: ${err.message}`);
1934
+ process.exit(1);
1935
+ }
1936
+ }
1937
+ function registerGitnexusCommand(program2) {
1938
+ const gx = program2.command("gitnexus").description("Qu\u1EA3n l\xFD GitNexus code intelligence (M10)");
1939
+ gx.command("install").description("C\xE0i + setup GitNexus cho workspace hi\u1EC7n t\u1EA1i").action(async () => {
1940
+ await runGitnexusInstall();
1941
+ });
1942
+ gx.command("status").description("Show index info + staleness warning").action(async () => {
1943
+ await runGitnexusStatus();
1944
+ });
1945
+ gx.command("analyze").description("Re-run analyze refresh index (no wiki)").action(async () => {
1946
+ await runGitnexusAnalyzeCommand();
1947
+ });
1948
+ }
1949
+
1950
+ // src/commands/init.ts
1951
+ import { basename, join as join22, relative as relative2, resolve } from "path";
1952
+ import { confirm as confirm5, input as input5, select as select8 } from "@inquirer/prompts";
1953
+ import boxen5 from "boxen";
1954
+
1955
+ // src/lib/add-team-pack-submodule-with-retry-on-network-fail.ts
1956
+ import { spawnSync as spawnSync13 } from "child_process";
1957
+ import { select as select5 } from "@inquirer/prompts";
1958
+
1342
1959
  // src/lib/team-pack-submodule-manager.ts
1343
- import { join as join12 } from "path";
1960
+ import { join as join16 } from "path";
1344
1961
 
1345
1962
  // 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";
1963
+ import { spawnSync as spawnSync12 } from "child_process";
1964
+ import { confirm as confirm4, select as select4 } from "@inquirer/prompts";
1965
+ import boxen3 from "boxen";
1349
1966
  function parseRepoSlugFromGitUrl(url) {
1350
1967
  const httpsMatch = url.match(/github\.com[/:]([^/]+\/[^/]+?)(?:\.git)?$/);
1351
1968
  if (httpsMatch) return httpsMatch[1];
1352
1969
  return null;
1353
1970
  }
1354
1971
  function checkRepoAccess(repoSlug) {
1355
- const r = spawnSync7("gh", ["api", `repos/${repoSlug}`], { stdio: "ignore" });
1972
+ const r = spawnSync12("gh", ["api", `repos/${repoSlug}`], { stdio: "ignore" });
1356
1973
  return r.status === 0;
1357
1974
  }
1358
1975
  function getCurrentGhUser() {
1359
- const r = spawnSync7("gh", ["api", "user", "--jq", ".login"], {
1976
+ const r = spawnSync12("gh", ["api", "user", "--jq", ".login"], {
1360
1977
  encoding: "utf8",
1361
1978
  stdio: ["ignore", "pipe", "pipe"]
1362
1979
  });
@@ -1365,13 +1982,13 @@ function getCurrentGhUser() {
1365
1982
  }
1366
1983
  function triggerGhAuthLoginInteractive() {
1367
1984
  log.info("M\u1EDF browser \u0111\u1EC3 switch GitHub account...");
1368
- const r = spawnSync7("gh", ["auth", "login", "--web"], { stdio: "inherit" });
1985
+ const r = spawnSync12("gh", ["auth", "login", "--web"], { stdio: "inherit" });
1369
1986
  if (r.status !== 0) {
1370
1987
  log.warn(`gh auth login exit ${r.status}. C\xF3 th\u1EC3 ch\u1EA1y tay r\u1ED3i quay l\u1EA1i retry.`);
1371
1988
  }
1372
1989
  }
1373
1990
  async function copyInfoToClipboardWithConsent(info) {
1374
- const ok = await confirm2({
1991
+ const ok = await confirm4({
1375
1992
  message: "Copy th\xF4ng tin (GitHub username + email) v\xE0o clipboard \u0111\u1EC3 d\xE1n v\xE0o Slack/email?",
1376
1993
  default: true
1377
1994
  });
@@ -1400,7 +2017,7 @@ function printAccessWarningBox(repoSlug, ghUser, ssoEmail) {
1400
2017
  `${chalk.dim("Li\xEAn h\u1EC7:")} luke@nal.vn (Slack #avatar-setup)`
1401
2018
  ];
1402
2019
  process.stdout.write(
1403
- `${boxen2(lines.join("\n"), { padding: 1, borderColor: "red", borderStyle: "round" })}
2020
+ `${boxen3(lines.join("\n"), { padding: 1, borderColor: "red", borderStyle: "round" })}
1404
2021
  `
1405
2022
  );
1406
2023
  }
@@ -1505,7 +2122,7 @@ async function addTeamPackSubmodule(projectRoot, tag, ssoEmail) {
1505
2122
  }
1506
2123
  let target = tag ?? null;
1507
2124
  if (!target) {
1508
- target = await latestTag(join12(projectRoot, TEAM_PACK_RELATIVE_PATH));
2125
+ target = await latestTag(join16(projectRoot, TEAM_PACK_RELATIVE_PATH));
1509
2126
  }
1510
2127
  if (target) {
1511
2128
  await checkoutTagInSubmodule(TEAM_PACK_RELATIVE_PATH, target, projectRoot);
@@ -1513,7 +2130,7 @@ async function addTeamPackSubmodule(projectRoot, tag, ssoEmail) {
1513
2130
  return { pinnedTag: target };
1514
2131
  }
1515
2132
  async function readPinnedPackVersion(projectRoot) {
1516
- const submoduleRoot = join12(projectRoot, TEAM_PACK_RELATIVE_PATH);
2133
+ const submoduleRoot = join16(projectRoot, TEAM_PACK_RELATIVE_PATH);
1517
2134
  const tag = await latestTag(submoduleRoot);
1518
2135
  if (tag) return tag;
1519
2136
  const sha = await currentCommitSha(submoduleRoot);
@@ -1527,14 +2144,14 @@ function isSshPermissionError(message) {
1527
2144
  }
1528
2145
  function triggerGhAuthLoginInteractive2() {
1529
2146
  log.info("M\u1EDF browser \u0111\u1EC3 switch GitHub account...");
1530
- const r = spawnSync8("gh", ["auth", "login", "--web"], { stdio: "inherit" });
2147
+ const r = spawnSync13("gh", ["auth", "login", "--web"], { stdio: "inherit" });
1531
2148
  if (r.status !== 0) {
1532
2149
  log.warn(`gh auth login exit ${r.status}. C\xF3 th\u1EC3 ch\u1EA1y tay r\u1ED3i quay l\u1EA1i retry.`);
1533
2150
  }
1534
2151
  }
1535
2152
  function openGithubSshKeysPage() {
1536
2153
  log.info("M\u1EDF trang GitHub Settings \u2192 SSH Keys...");
1537
- const r = spawnSync8("open", ["https://github.com/settings/keys"], { stdio: "ignore" });
2154
+ const r = spawnSync13("open", ["https://github.com/settings/keys"], { stdio: "ignore" });
1538
2155
  if (r.status !== 0) {
1539
2156
  log.info("URL: https://github.com/settings/keys");
1540
2157
  }
@@ -1684,7 +2301,7 @@ ${renderAvatarBanner(opts)}
1684
2301
  }
1685
2302
 
1686
2303
  // src/lib/execute-gh-repo-create.ts
1687
- import { spawnSync as spawnSync9 } from "child_process";
2304
+ import { spawnSync as spawnSync14 } from "child_process";
1688
2305
  var RepoAlreadyExistsError = class extends Error {
1689
2306
  constructor(fullName) {
1690
2307
  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 +2321,7 @@ function executeGhRepoCreate(input6) {
1704
2321
  "origin",
1705
2322
  "--push"
1706
2323
  ];
1707
- const r = spawnSync9("gh", args, { stdio: "inherit" });
2324
+ const r = spawnSync14("gh", args, { stdio: "inherit" });
1708
2325
  if (r.status !== 0) {
1709
2326
  if (r.status === 1) {
1710
2327
  throw new RepoAlreadyExistsError(fullName);
@@ -1718,9 +2335,9 @@ function executeGhRepoCreate(input6) {
1718
2335
  }
1719
2336
 
1720
2337
  // src/lib/resolve-github-username-default.ts
1721
- import { spawnSync as spawnSync10 } from "child_process";
2338
+ import { spawnSync as spawnSync15 } from "child_process";
1722
2339
  function resolveGithubUsernameDefault() {
1723
- const r = spawnSync10("gh", ["api", "user", "--jq", ".login"], {
2340
+ const r = spawnSync15("gh", ["api", "user", "--jq", ".login"], {
1724
2341
  encoding: "utf8",
1725
2342
  stdio: ["ignore", "pipe", "pipe"]
1726
2343
  });
@@ -1768,12 +2385,12 @@ function createGithubRemoteFromFolder(input6) {
1768
2385
  }
1769
2386
 
1770
2387
  // src/lib/create-workspace-remote-via-gh.ts
1771
- import { spawnSync as spawnSync18 } from "child_process";
2388
+ import { spawnSync as spawnSync23 } from "child_process";
1772
2389
 
1773
2390
  // src/lib/check-gh-cli-auth-status.ts
1774
- import { spawnSync as spawnSync11 } from "child_process";
2391
+ import { spawnSync as spawnSync16 } from "child_process";
1775
2392
  function checkGhCliAuthStatus() {
1776
- const r = spawnSync11("gh", ["auth", "status"], { stdio: "ignore" });
2393
+ const r = spawnSync16("gh", ["auth", "status"], { stdio: "ignore" });
1777
2394
  if (r.error && r.error.code === "ENOENT") {
1778
2395
  return "not-installed";
1779
2396
  }
@@ -1781,12 +2398,12 @@ function checkGhCliAuthStatus() {
1781
2398
  }
1782
2399
 
1783
2400
  // src/lib/detect-package-manager.ts
1784
- import { spawnSync as spawnSync12 } from "child_process";
2401
+ import { spawnSync as spawnSync17 } from "child_process";
1785
2402
  function hasBinary(name) {
1786
2403
  const platform2 = detectHostPlatform();
1787
2404
  const probe = platform2 === "win32" ? "where" : "command";
1788
2405
  const args = platform2 === "win32" ? [name] : ["-v", name];
1789
- const r = spawnSync12(probe, args, {
2406
+ const r = spawnSync17(probe, args, {
1790
2407
  shell: platform2 !== "win32",
1791
2408
  stdio: "ignore"
1792
2409
  });
@@ -1802,11 +2419,11 @@ function detectPackageManager() {
1802
2419
  }
1803
2420
 
1804
2421
  // src/lib/handle-remote-access-failure-with-account-switch.ts
1805
- import { spawnSync as spawnSync14 } from "child_process";
2422
+ import { spawnSync as spawnSync19 } from "child_process";
1806
2423
  import { input as input4, select as select6 } from "@inquirer/prompts";
1807
2424
 
1808
2425
  // src/lib/verify-git-remote-accessible.ts
1809
- import { spawnSync as spawnSync13 } from "child_process";
2426
+ import { spawnSync as spawnSync18 } from "child_process";
1810
2427
  var TIMEOUT_MS = 5e3;
1811
2428
  function classifyRemoteError(stderr) {
1812
2429
  const text = stderr.toLowerCase();
@@ -1822,7 +2439,7 @@ function classifyRemoteError(stderr) {
1822
2439
  return "unknown";
1823
2440
  }
1824
2441
  function tryVerifyGitRemoteAccessible(url) {
1825
- const r = spawnSync13("git", ["ls-remote", "--exit-code", url, "HEAD"], {
2442
+ const r = spawnSync18("git", ["ls-remote", "--exit-code", url, "HEAD"], {
1826
2443
  encoding: "utf8",
1827
2444
  timeout: TIMEOUT_MS,
1828
2445
  stdio: ["ignore", "pipe", "pipe"]
@@ -1844,7 +2461,7 @@ var RemoteAccessAbortedError = class extends Error {
1844
2461
  }
1845
2462
  };
1846
2463
  function getCurrentGhUser2() {
1847
- const r = spawnSync14("gh", ["api", "user", "--jq", ".login"], {
2464
+ const r = spawnSync19("gh", ["api", "user", "--jq", ".login"], {
1848
2465
  encoding: "utf8",
1849
2466
  stdio: ["ignore", "pipe", "pipe"]
1850
2467
  });
@@ -1853,7 +2470,7 @@ function getCurrentGhUser2() {
1853
2470
  }
1854
2471
  function triggerGhAuthLoginInteractive3() {
1855
2472
  log.info("M\u1EDF browser \u0111\u1EC3 switch GitHub account...");
1856
- const r = spawnSync14("gh", ["auth", "login", "--web"], { stdio: "inherit" });
2473
+ const r = spawnSync19("gh", ["auth", "login", "--web"], { stdio: "inherit" });
1857
2474
  if (r.status !== 0) {
1858
2475
  log.warn(`gh auth login exit ${r.status}. B\u1EA1n c\xF3 th\u1EC3 ch\u1EA1y tay r\u1ED3i quay l\u1EA1i retry.`);
1859
2476
  }
@@ -1936,7 +2553,7 @@ async function handleRemoteAccessFailureWithAccountSwitch(args) {
1936
2553
  }
1937
2554
 
1938
2555
  // src/lib/install-gh-cli-via-package-manager.ts
1939
- import { spawnSync as spawnSync15 } from "child_process";
2556
+ import { spawnSync as spawnSync20 } from "child_process";
1940
2557
  var INSTALL_COMMANDS = {
1941
2558
  brew: { cmd: "brew", args: ["install", "gh"] },
1942
2559
  apt: { cmd: "sudo", args: ["apt-get", "install", "-y", "gh"] },
@@ -1947,7 +2564,7 @@ var INSTALL_COMMANDS = {
1947
2564
  function installGhCliViaPackageManager(pm) {
1948
2565
  const spec = INSTALL_COMMANDS[pm];
1949
2566
  log.info(`\u0110ang c\xE0i gh CLI qua ${pm}...`);
1950
- const r = spawnSync15(spec.cmd, spec.args, { stdio: "inherit" });
2567
+ const r = spawnSync20(spec.cmd, spec.args, { stdio: "inherit" });
1951
2568
  if (r.status !== 0) {
1952
2569
  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
2570
  }
@@ -1955,9 +2572,9 @@ function installGhCliViaPackageManager(pm) {
1955
2572
  }
1956
2573
 
1957
2574
  // src/lib/setup-git-credential-via-gh.ts
1958
- import { spawnSync as spawnSync16 } from "child_process";
2575
+ import { spawnSync as spawnSync21 } from "child_process";
1959
2576
  function setupGitCredentialViaGh() {
1960
- const r = spawnSync16("gh", ["auth", "setup-git"], { stdio: "ignore" });
2577
+ const r = spawnSync21("gh", ["auth", "setup-git"], { stdio: "ignore" });
1961
2578
  if (r.status !== 0) {
1962
2579
  log.warn("gh auth setup-git fail (non-fatal). N\u1EBFu git clone l\u1ED7i 128 \u2192 ch\u1EA1y th\u1EE7 c\xF4ng.");
1963
2580
  return;
@@ -1966,10 +2583,10 @@ function setupGitCredentialViaGh() {
1966
2583
  }
1967
2584
 
1968
2585
  // src/lib/trigger-gh-cli-auth-login.ts
1969
- import { spawnSync as spawnSync17 } from "child_process";
2586
+ import { spawnSync as spawnSync22 } from "child_process";
1970
2587
  function triggerGhCliAuthLogin() {
1971
2588
  log.info("Kh\u1EDFi \u0111\u1ED9ng \u0111\u0103ng nh\u1EADp GitHub qua gh CLI (browser s\u1EBD m\u1EDF)...");
1972
- const r = spawnSync17(
2589
+ const r = spawnSync22(
1973
2590
  "gh",
1974
2591
  ["auth", "login", "--hostname", "github.com", "--web", "--git-protocol", "ssh"],
1975
2592
  { stdio: "inherit" }
@@ -2087,20 +2704,20 @@ function classifyGhCreateError(stderr) {
2087
2704
  return "unknown";
2088
2705
  }
2089
2706
  function repoExistsOnGitHub(fullName) {
2090
- const r = spawnSync18("gh", ["repo", "view", fullName, "--json", "name"], {
2707
+ const r = spawnSync23("gh", ["repo", "view", fullName, "--json", "name"], {
2091
2708
  stdio: "ignore"
2092
2709
  });
2093
2710
  return r.status === 0;
2094
2711
  }
2095
2712
  function canCreateInNamespace(org, ghUser) {
2096
2713
  if (org.toLowerCase() === ghUser.toLowerCase()) return { ok: true };
2097
- const r = spawnSync18("gh", ["api", `orgs/${org}/members/${ghUser}`, "--silent"], {
2714
+ const r = spawnSync23("gh", ["api", `orgs/${org}/members/${ghUser}`, "--silent"], {
2098
2715
  stdio: "ignore"
2099
2716
  });
2100
2717
  if (r.status === 0) return { ok: true };
2101
- const orgCheck = spawnSync18("gh", ["api", `orgs/${org}`, "--silent"], { stdio: "ignore" });
2718
+ const orgCheck = spawnSync23("gh", ["api", `orgs/${org}`, "--silent"], { stdio: "ignore" });
2102
2719
  if (orgCheck.status !== 0) {
2103
- const userCheck = spawnSync18("gh", ["api", `users/${org}`, "--silent"], { stdio: "ignore" });
2720
+ const userCheck = spawnSync23("gh", ["api", `users/${org}`, "--silent"], { stdio: "ignore" });
2104
2721
  if (userCheck.status === 0) {
2105
2722
  return {
2106
2723
  ok: false,
@@ -2136,7 +2753,7 @@ async function createWorkspaceRemoteViaGh(input6) {
2136
2753
  );
2137
2754
  }
2138
2755
  log.info(`T\u1EA1o GitHub repo cho workspace: ${fullName} (${input6.visibility})...`);
2139
- const r = spawnSync18(
2756
+ const r = spawnSync23(
2140
2757
  "gh",
2141
2758
  [
2142
2759
  "repo",
@@ -2177,7 +2794,7 @@ ${combined}
2177
2794
  function linkExistingRemoteToWorkspace(args) {
2178
2795
  const sshUrl = `git@github.com:${args.fullName}.git`;
2179
2796
  const httpsUrl = `https://github.com/${args.fullName}.git`;
2180
- const addResult = spawnSync18(
2797
+ const addResult = spawnSync23(
2181
2798
  "git",
2182
2799
  ["-C", args.workspacePath, "remote", "add", "origin", sshUrl],
2183
2800
  {
@@ -2186,7 +2803,7 @@ function linkExistingRemoteToWorkspace(args) {
2186
2803
  }
2187
2804
  );
2188
2805
  if (addResult.status !== 0) {
2189
- spawnSync18("git", ["-C", args.workspacePath, "remote", "set-url", "origin", sshUrl], {
2806
+ spawnSync23("git", ["-C", args.workspacePath, "remote", "set-url", "origin", sshUrl], {
2190
2807
  stdio: "ignore"
2191
2808
  });
2192
2809
  }
@@ -2200,11 +2817,11 @@ import { select as select7 } from "@inquirer/prompts";
2200
2817
  import { simpleGit as simpleGit3 } from "simple-git";
2201
2818
 
2202
2819
  // src/lib/check-folder-has-git.ts
2203
- import { existsSync as existsSync4, statSync } from "fs";
2204
- import { join as join13 } from "path";
2820
+ import { existsSync as existsSync6, statSync } from "fs";
2821
+ import { join as join17 } from "path";
2205
2822
  function checkFolderHasGit(folderPath) {
2206
- const gitPath = join13(folderPath, ".git");
2207
- if (!existsSync4(gitPath)) return false;
2823
+ const gitPath = join17(folderPath, ".git");
2824
+ if (!existsSync6(gitPath)) return false;
2208
2825
  const stat = statSync(gitPath);
2209
2826
  return stat.isDirectory() || stat.isFile();
2210
2827
  }
@@ -2234,8 +2851,8 @@ async function createInitialGitCommit(folderPath) {
2234
2851
  }
2235
2852
 
2236
2853
  // src/lib/detect-folder-tech-stack.ts
2237
- import { existsSync as existsSync5 } from "fs";
2238
- import { join as join14 } from "path";
2854
+ import { existsSync as existsSync7 } from "fs";
2855
+ import { join as join18 } from "path";
2239
2856
  var SIGNATURES = {
2240
2857
  node: ["package.json"],
2241
2858
  python: ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile"],
@@ -2247,7 +2864,7 @@ var SIGNATURES = {
2247
2864
  function detectFolderTechStack(folderPath) {
2248
2865
  const matched = [];
2249
2866
  for (const [stack, files] of Object.entries(SIGNATURES)) {
2250
- if (files.some((f) => existsSync5(join14(folderPath, f)))) {
2867
+ if (files.some((f) => existsSync7(join18(folderPath, f)))) {
2251
2868
  matched.push(stack);
2252
2869
  }
2253
2870
  }
@@ -2256,25 +2873,25 @@ function detectFolderTechStack(folderPath) {
2256
2873
 
2257
2874
  // src/lib/gitignore-template-loader.ts
2258
2875
  import { readFileSync as readFileSync3 } from "fs";
2259
- import { dirname as dirname4, join as join15 } from "path";
2876
+ import { dirname as dirname4, join as join19 } from "path";
2260
2877
  import { fileURLToPath as fileURLToPath2 } from "url";
2261
2878
  var __dirname = dirname4(fileURLToPath2(import.meta.url));
2262
2879
  var CANDIDATE_DIRS = [
2263
2880
  // Bundled production: dist/index.js → __dirname = .../dist/, sibling dist/templates
2264
- join15(__dirname, "templates", "gitignore"),
2881
+ join19(__dirname, "templates", "gitignore"),
2265
2882
  // Legacy bundled: nếu file là dist/lib/*.js (sub-bundle), templates ở dist/templates
2266
- join15(__dirname, "..", "templates", "gitignore"),
2883
+ join19(__dirname, "..", "templates", "gitignore"),
2267
2884
  // Dev mode (vitest/tsx run src/ trực tiếp): __dirname = src/lib/
2268
- join15(__dirname, "..", "..", "src", "templates", "gitignore"),
2885
+ join19(__dirname, "..", "..", "src", "templates", "gitignore"),
2269
2886
  // npm-installed alt: __dirname = .../dist/ → package_root/src/templates
2270
- join15(__dirname, "..", "src", "templates", "gitignore")
2887
+ join19(__dirname, "..", "src", "templates", "gitignore")
2271
2888
  ];
2272
2889
  var AVATAR_MARKER_START = "# === avatar ===";
2273
2890
  var AVATAR_MARKER_END = "# === /avatar ===";
2274
2891
  function readTemplate(stack) {
2275
2892
  for (const dir of CANDIDATE_DIRS) {
2276
2893
  try {
2277
- return readFileSync3(join15(dir, `${stack}.txt`), "utf8");
2894
+ return readFileSync3(join19(dir, `${stack}.txt`), "utf8");
2278
2895
  } catch {
2279
2896
  }
2280
2897
  }
@@ -2288,11 +2905,11 @@ ${readTemplate(s).trim()}`);
2288
2905
  }
2289
2906
 
2290
2907
  // 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";
2908
+ import { existsSync as existsSync8, readFileSync as readFileSync4, writeFileSync } from "fs";
2909
+ import { join as join20 } from "path";
2293
2910
  function writeOrMergeGitignore(folderPath, avatarBlock) {
2294
- const path = join16(folderPath, ".gitignore");
2295
- if (!existsSync6(path)) {
2911
+ const path = join20(folderPath, ".gitignore");
2912
+ if (!existsSync8(path)) {
2296
2913
  writeFileSync(path, avatarBlock, "utf8");
2297
2914
  return;
2298
2915
  }
@@ -2468,7 +3085,7 @@ async function safeBootstrapGitInFolder(folderPath, opts = {}) {
2468
3085
 
2469
3086
  // src/commands/init-conflict-detection-helpers.ts
2470
3087
  import { readdir } from "fs/promises";
2471
- import { join as join17 } from "path";
3088
+ import { join as join21 } from "path";
2472
3089
  async function isEmptyOrMissing(path) {
2473
3090
  if (!await pathExists(path)) return true;
2474
3091
  try {
@@ -2481,7 +3098,7 @@ async function isEmptyOrMissing(path) {
2481
3098
  }
2482
3099
  async function findAlternativeWorkspaceName(parent, desiredName, maxAttempts = 10) {
2483
3100
  for (let i = 2; i < maxAttempts; i++) {
2484
- const candidate = join17(parent, `${desiredName}-${i}`);
3101
+ const candidate = join21(parent, `${desiredName}-${i}`);
2485
3102
  if (await isEmptyOrMissing(candidate)) return candidate;
2486
3103
  }
2487
3104
  return null;
@@ -2497,6 +3114,38 @@ function inferWorkspaceName(repoUrl) {
2497
3114
  const withoutPrefix = baseName.replace(/^avatar-/, "");
2498
3115
  return `avatar-${withoutPrefix}-workspace`;
2499
3116
  }
3117
+ function buildGitnexusSection(gitnexusReady) {
3118
+ if (!gitnexusReady) return "";
3119
+ return `
3120
+ ### \u{1F9E0} CODEBASE INTELLIGENCE \u2014 GitNexus
3121
+
3122
+ Workspace c\xF3 GitNexus index t\u1EA1i \`.gitnexus/\` cung c\u1EA5p architectural awareness
3123
+ cho Claude Code (impact analysis, call chains, blast radius).
3124
+
3125
+ **H\u01B0\u1EDBng d\u1EABn cho Claude:**
3126
+
3127
+ - Khi c\u1EA7n hi\u1EC3u codebase architecture (vd: tr\u01B0\u1EDBc refactor, debug call chain) \u2192
3128
+ query GitNexus MCP resource \`gitnexus://repo/<name>/context\`
3129
+ - Khi resource tr\u1EA3 staleness hint "\u26A0\uFE0F Index is N commit(s) behind HEAD" \u2192
3130
+ T\u1EF0 ch\u1EA1y \`gitnexus analyze .\` (trong workspace root) tr\u01B0\u1EDBc khi ti\u1EBFp t\u1EE5c task
3131
+ - Sau khi \`gitnexus analyze\` xong: **KH\xD4NG t\u1EF1 ch\u1EA1y \`gitnexus wiki\`**
3132
+ (cost cao, c\xF3 th\u1EC3 block session 2-5 ph\xFAt) \u2014 user s\u1EBD ch\u1EA1y manual khi c\u1EA7n
3133
+
3134
+ **Folders Claude scan auto cho skills:**
3135
+
3136
+ - \`~/.claude/skills/gitnexus-*/\` \u2014 GitNexus global skills (exploring, debugging, ...)
3137
+ - \`.claude/pack/skills/\` \u2014 NAL team-shared skills (qua team-ai-pack submodule)
3138
+ - C\u1EA3 2 \u0111\u1EC1u \u0111\u01B0\u1EE3c scan, kh\xF4ng xung \u0111\u1ED9t (different naming prefix)
3139
+
3140
+ **Manual wiki update:**
3141
+
3142
+ Khi user c\u1EA7n regenerate wiki sau refactor l\u1EDBn \u2014 ch\u1EA1y:
3143
+
3144
+ \`\`\`bash
3145
+ gitnexus wiki . --api-key <key> --base-url <url>
3146
+ \`\`\`
3147
+ `;
3148
+ }
2500
3149
  function buildScaffoldVariables(args) {
2501
3150
  return {
2502
3151
  projectName: args.projectName,
@@ -2505,12 +3154,13 @@ function buildScaffoldVariables(args) {
2505
3154
  avatarVersion: AVATAR_CLI_VERSION,
2506
3155
  packVersion: args.packVersion,
2507
3156
  lastScan: (/* @__PURE__ */ new Date()).toISOString(),
2508
- mode: args.mode
3157
+ mode: args.mode,
3158
+ gitnexusSection: buildGitnexusSection(args.gitnexusReady ?? false)
2509
3159
  };
2510
3160
  }
2511
3161
 
2512
3162
  // src/commands/login.ts
2513
- import boxen3 from "boxen";
3163
+ import boxen4 from "boxen";
2514
3164
  import open from "open";
2515
3165
 
2516
3166
  // src/lib/google-oauth-device-flow.ts
@@ -2657,7 +3307,7 @@ async function runLogin(opts) {
2657
3307
  "",
2658
3308
  `Ho\u1EB7c Avatar t\u1EF1 m\u1EDF browser, click ${chalk.green("Allow")}...`
2659
3309
  ].join("\n");
2660
- process.stdout.write(`${boxen3(instructions, { padding: 1, borderStyle: "round" })}
3310
+ process.stdout.write(`${boxen4(instructions, { padding: 1, borderStyle: "round" })}
2661
3311
  `);
2662
3312
  void open(verificationUrl).catch(() => {
2663
3313
  log.dim("(Kh\xF4ng m\u1EDF \u0111\u01B0\u1EE3c browser t\u1EF1 \u0111\u1ED9ng \u2014 copy URL \u1EDF tr\xEAn)");
@@ -2713,6 +3363,9 @@ function parseBootstrapStrategyOpts(opts) {
2713
3363
  }
2714
3364
  function registerInitCommand(program2) {
2715
3365
  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(
3366
+ "--gitnexus-skip",
3367
+ "B\u1ECF qua phase GitNexus setup (M10 \u2014 ch\u1EA1y `avatar gitnexus install` sau)"
3368
+ ).option(
2716
3369
  "--bootstrap-strategy <s>",
2717
3370
  "X\u1EED l\xFD folder dirty: stash | commit-all | skip | branch (default: prompt)"
2718
3371
  ).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 +3471,7 @@ async function runInitFromExistingRemote(opts, ownerEmail) {
2818
3471
  repoOrg: opts.repoOrg,
2819
3472
  flow: "existing-remote",
2820
3473
  aiSkip: opts.aiSkip,
3474
+ gitnexusSkip: opts.gitnexusSkip,
2821
3475
  ssoEmail: ownerEmail
2822
3476
  });
2823
3477
  }
@@ -2854,6 +3508,7 @@ async function runInitFromExistingFolder(opts, ownerEmail) {
2854
3508
  repoOrg: opts.repoOrg,
2855
3509
  flow: "existing-folder",
2856
3510
  aiSkip: opts.aiSkip,
3511
+ gitnexusSkip: opts.gitnexusSkip,
2857
3512
  ssoEmail: ownerEmail
2858
3513
  });
2859
3514
  }
@@ -2873,7 +3528,7 @@ async function runInitFromScratch(opts, ownerEmail) {
2873
3528
  const teamOwner = opts.teamOwner ?? await promptTeamOwner(ownerEmail);
2874
3529
  const workspaceParent = resolve(opts.workspaceParent ?? ".");
2875
3530
  const workspacePath = await resolveWorkspacePath(workspaceParent, projectName, opts.force);
2876
- const srcPath = join18(workspacePath, "src");
3531
+ const srcPath = join22(workspacePath, "src");
2877
3532
  await ensureDir(workspacePath);
2878
3533
  await ensureDir(srcPath);
2879
3534
  await safeBootstrapGitInFolder(srcPath, { autoYes: true });
@@ -2914,7 +3569,8 @@ async function runInitFromScratch(opts, ownerEmail) {
2914
3569
  repoVisibility: opts.repoVisibility,
2915
3570
  repoOrg: opts.repoOrg,
2916
3571
  flow: "new-project",
2917
- aiSkip: opts.aiSkip
3572
+ aiSkip: opts.aiSkip,
3573
+ gitnexusSkip: opts.gitnexusSkip
2918
3574
  });
2919
3575
  } catch (err) {
2920
3576
  sp.fail("Init workspace th\u1EA5t b\u1EA1i");
@@ -2928,7 +3584,7 @@ async function getOrCreateOriginRemote(folderPath, opts) {
2928
3584
  log.success(`Folder \u0111\xE3 c\xF3 remote origin: ${origin.refs.push}`);
2929
3585
  return origin.refs.push;
2930
3586
  }
2931
- const shouldCreate = opts.createRemote ?? await confirm3({
3587
+ const shouldCreate = opts.createRemote ?? await confirm5({
2932
3588
  message: "Folder ch\u01B0a c\xF3 remote. T\u1EA1o GitHub repo ngay \u0111\u1EC3 share team?",
2933
3589
  default: true
2934
3590
  });
@@ -2989,7 +3645,8 @@ async function scaffoldWorkspaceWithSrcSubmodule(args) {
2989
3645
  repoVisibility: args.repoVisibility,
2990
3646
  repoOrg: args.repoOrg,
2991
3647
  flow: args.flow,
2992
- aiSkip: args.aiSkip
3648
+ aiSkip: args.aiSkip,
3649
+ gitnexusSkip: args.gitnexusSkip
2993
3650
  });
2994
3651
  } catch (err) {
2995
3652
  sp.fail("Init workspace th\u1EA5t b\u1EA1i");
@@ -3009,10 +3666,10 @@ async function finalizeWorkspaceScaffold(args) {
3009
3666
  await writeRootClaudeMd(args.workspacePath, vars);
3010
3667
  await writeProjectSettings(args.workspacePath, vars);
3011
3668
  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");
3669
+ await ensureDir(join22(args.workspacePath, "notes"));
3670
+ await ensureDir(join22(args.workspacePath, "scripts"));
3671
+ await installGitHook(join22(args.workspacePath, ".git"), "post-merge");
3672
+ await installGitHook(join22(args.workspacePath, ".git", "modules", "src"), "pre-push");
3016
3673
  log.success("C\xE0i post-merge (workspace) + pre-push (src/)");
3017
3674
  await appendAuditEntry("init", `flow=${args.flow},workspace=${args.workspaceName}`);
3018
3675
  await maybeCommitWorkspace(args.workspacePath, args.skipCommit);
@@ -3023,7 +3680,30 @@ async function finalizeWorkspaceScaffold(args) {
3023
3680
  } else {
3024
3681
  aiResult = await runAiSetupPhase({ workspacePath: args.workspacePath });
3025
3682
  }
3026
- printInitSuccessBox(args.workspacePath, args.flow, aiResult);
3683
+ let gitnexusResult = null;
3684
+ const skipGitnexus = args.aiSkip || args.gitnexusSkip;
3685
+ if (skipGitnexus) {
3686
+ if (args.gitnexusSkip) {
3687
+ log.dim("B\u1ECF qua GitNexus setup (--gitnexus-skip). Setup sau: avatar gitnexus install");
3688
+ } else {
3689
+ log.dim("B\u1ECF qua GitNexus setup (auto-skip do --ai-skip).");
3690
+ }
3691
+ } else {
3692
+ gitnexusResult = await runGitnexusSetupPhase({ workspacePath: args.workspacePath });
3693
+ }
3694
+ if (gitnexusResult?.ok) {
3695
+ const updatedVars = buildScaffoldVariables({
3696
+ projectName: args.workspaceName,
3697
+ projectDescription: args.description,
3698
+ teamOwner: args.teamOwner,
3699
+ packVersion: args.packVersion,
3700
+ mode: "client",
3701
+ gitnexusReady: true
3702
+ });
3703
+ await writeRootClaudeMd(args.workspacePath, updatedVars);
3704
+ log.dim("Updated CLAUDE.md v\u1EDBi GitNexus section");
3705
+ }
3706
+ printInitSuccessBox(args.workspacePath, args.flow, aiResult, gitnexusResult);
3027
3707
  }
3028
3708
  async function maybeCreateWorkspaceRemote(args) {
3029
3709
  if (args.skipCommit) {
@@ -3033,7 +3713,7 @@ async function maybeCreateWorkspaceRemote(args) {
3033
3713
  let shouldCreate = args.createWorkspaceRemote;
3034
3714
  if (shouldCreate === void 0) {
3035
3715
  if (args.autoYes) return;
3036
- shouldCreate = await confirm3({
3716
+ shouldCreate = await confirm5({
3037
3717
  message: "T\u1EA1o remote GitHub cho workspace \u0111\u1EC3 share team? (Avatar state)",
3038
3718
  default: false
3039
3719
  });
@@ -3112,7 +3792,7 @@ async function maybeCreateWorkspaceRemote(args) {
3112
3792
  }
3113
3793
  }
3114
3794
  async function resolveWorkspacePath(parent, desiredName, force) {
3115
- const desired = join18(parent, desiredName);
3795
+ const desired = join22(parent, desiredName);
3116
3796
  if (await isEmptyOrMissing(desired)) return desired;
3117
3797
  log.warn(`Workspace path "${desired}" \u0111\xE3 c\xF3 n\u1ED9i dung.`);
3118
3798
  while (true) {
@@ -3143,7 +3823,7 @@ async function resolveWorkspacePath(parent, desiredName, force) {
3143
3823
  message: "T\xEAn workspace m\u1EDBi:",
3144
3824
  validate: (v) => v.trim().length > 0 ? true : "T\xEAn kh\xF4ng \u0111\u01B0\u1EE3c r\u1ED7ng"
3145
3825
  });
3146
- const newPath = join18(parent, newName.trim());
3826
+ const newPath = join22(parent, newName.trim());
3147
3827
  if (await isEmptyOrMissing(newPath)) return newPath;
3148
3828
  log.warn(`"${newPath}" c\u0169ng \u0111\xE3 c\xF3 n\u1ED9i dung. Th\u1EED t\xEAn kh\xE1c.`);
3149
3829
  }
@@ -3171,21 +3851,34 @@ function formatAiStatusLine(aiResult) {
3171
3851
  }
3172
3852
  return ` ${chalk.yellow("AI:")} failed (${aiResult.reason.slice(0, 60)}) \xB7 th\u1EED ${chalk.cyan("avatar ai setup")}`;
3173
3853
  }
3174
- function printInitSuccessBox(rootPath, flow, aiResult = null) {
3854
+ function formatGitnexusStatusLine(result) {
3855
+ if (result === null) {
3856
+ return ` ${chalk.yellow("GitNexus:")} skipped \xB7 ${chalk.cyan("avatar gitnexus install")} \u0111\u1EC3 setup sau`;
3857
+ }
3858
+ if (result.ok) {
3859
+ const parts = ["ready"];
3860
+ if (result.analyzed) parts.push("indexed");
3861
+ if (result.wikiGenerated) parts.push("wiki");
3862
+ if (result.mcpRegistered) parts.push("mcp");
3863
+ return ` ${chalk.green("GitNexus:")} ${parts.join(" \xB7 ")}`;
3864
+ }
3865
+ return ` ${chalk.yellow("GitNexus:")} skipped (${(result.reason ?? "unknown").slice(0, 40)}) \xB7 th\u1EED ${chalk.cyan("avatar gitnexus install")}`;
3866
+ }
3867
+ function printInitSuccessBox(rootPath, flow, aiResult = null, gitnexusResult = null) {
3175
3868
  const lines = [
3176
3869
  `${chalk.green("\u2713")} Workspace s\u1EB5n s\xE0ng: ${relative2(process.cwd(), rootPath) || rootPath}`,
3177
3870
  ` ${chalk.dim(`(flow: ${flow})`)}`,
3178
3871
  formatAiStatusLine(aiResult),
3872
+ formatGitnexusStatusLine(gitnexusResult),
3179
3873
  "",
3180
3874
  ` ${chalk.cyan(`cd ${rootPath}`)}`,
3181
3875
  ` ${chalk.cyan("claude")} M\u1EDF Claude Code \u1EDF workspace root`,
3182
3876
  "",
3183
- ` ${chalk.cyan("avatar commit --src")} Commit code l\xEAn remote src`,
3184
- ` ${chalk.cyan("avatar commit --avatar")} Commit Avatar state`,
3877
+ ` ${chalk.cyan("avatar commit src")} Commit code l\xEAn client remote`,
3185
3878
  ` ${chalk.cyan("avatar sync")} Pull team-ai-pack m\u1EDBi`,
3186
3879
  ` ${chalk.cyan("avatar uninstall")} G\u1EE1 Avatar (gi\u1EEF code)`
3187
3880
  ];
3188
- process.stdout.write(`${boxen4(lines.join("\n"), { padding: 1, borderStyle: "round" })}
3881
+ process.stdout.write(`${boxen5(lines.join("\n"), { padding: 1, borderStyle: "round" })}
3189
3882
  `);
3190
3883
  }
3191
3884
 
@@ -3236,18 +3929,18 @@ function registerSecretsCommand(program2) {
3236
3929
  }
3237
3930
 
3238
3931
  // src/commands/status.ts
3239
- import { promises as fs8 } from "fs";
3240
- import { join as join20 } from "path";
3241
- import boxen5 from "boxen";
3932
+ import { promises as fs10 } from "fs";
3933
+ import { join as join24 } from "path";
3934
+ import boxen6 from "boxen";
3242
3935
 
3243
3936
  // src/lib/pack-backup-manager.ts
3244
- import { promises as fs7 } from "fs";
3245
- import { join as join19 } from "path";
3937
+ import { promises as fs9 } from "fs";
3938
+ import { join as join23 } from "path";
3246
3939
  var BACKUP_DIR_NAME = "_backup";
3247
3940
  async function listBackups(projectRoot) {
3248
- const dir = join19(projectRoot, ".claude", BACKUP_DIR_NAME);
3941
+ const dir = join23(projectRoot, ".claude", BACKUP_DIR_NAME);
3249
3942
  if (!await pathExists(dir)) return [];
3250
- const entries = await fs7.readdir(dir, { withFileTypes: true });
3943
+ const entries = await fs9.readdir(dir, { withFileTypes: true });
3251
3944
  return entries.filter((e) => e.isDirectory()).map((e) => e.name).sort().reverse();
3252
3945
  }
3253
3946
 
@@ -3271,7 +3964,7 @@ function registerStatusCommand(program2) {
3271
3964
  }
3272
3965
  async function gatherStatus(cwd) {
3273
3966
  const projectName = cwd.split("/").filter(Boolean).pop() ?? "unknown";
3274
- const claudeRoot = join20(cwd, ".claude");
3967
+ const claudeRoot = join24(cwd, ".claude");
3275
3968
  const hasAvatar = await pathExists(claudeRoot);
3276
3969
  if (!hasAvatar) {
3277
3970
  return {
@@ -3284,9 +3977,9 @@ async function gatherStatus(cwd) {
3284
3977
  hasAvatar: false
3285
3978
  };
3286
3979
  }
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;
3980
+ const packVersion = await isGitRepo(join24(claudeRoot, "pack")) ? await readPinnedPackVersion(cwd).catch(() => null) : null;
3981
+ const pendingDir = join24(claudeRoot, "_pending");
3982
+ const pendingCount = await pathExists(pendingDir) ? (await fs10.readdir(pendingDir)).filter((n) => n.endsWith(".diff.md")).length : 0;
3290
3983
  const backupCount = (await listBackups(cwd)).length;
3291
3984
  const techStackSummary = await readTechStackFirstLine(claudeRoot);
3292
3985
  return {
@@ -3300,7 +3993,7 @@ async function gatherStatus(cwd) {
3300
3993
  };
3301
3994
  }
3302
3995
  async function readTechStackFirstLine(claudeRoot) {
3303
- const techStackPath = join20(claudeRoot, "project", "tech-stack.md");
3996
+ const techStackPath = join24(claudeRoot, "project", "tech-stack.md");
3304
3997
  if (!await pathExists(techStackPath)) return "(no tech-stack.md)";
3305
3998
  const content = await readText(techStackPath);
3306
3999
  const firstNonHeaderLine = content.split("\n").find((l) => l.trim() && !l.startsWith("#") && !l.startsWith(">"));
@@ -3316,7 +4009,7 @@ function renderStatusBox(s) {
3316
4009
  `${chalk.dim("Backups:")} ${s.backupCount}`,
3317
4010
  `${chalk.dim("Tech stack:")} ${s.techStackSummary}`
3318
4011
  ];
3319
- process.stdout.write(`${boxen5(lines.join("\n"), { padding: 1, borderStyle: "round" })}
4012
+ process.stdout.write(`${boxen6(lines.join("\n"), { padding: 1, borderStyle: "round" })}
3320
4013
  `);
3321
4014
  }
3322
4015
 
@@ -3335,33 +4028,33 @@ function registerToolsCommand(program2) {
3335
4028
 
3336
4029
  // src/commands/uninstall.ts
3337
4030
  import { relative as relative3 } from "path";
3338
- import { confirm as confirm4 } from "@inquirer/prompts";
3339
- import boxen6 from "boxen";
4031
+ import { confirm as confirm6 } from "@inquirer/prompts";
4032
+ import boxen7 from "boxen";
3340
4033
 
3341
4034
  // src/lib/create-uninstall-backup-snapshot.ts
3342
4035
  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");
4036
+ import { homedir as homedir4 } from "os";
4037
+ import { basename as basename2, join as join25 } from "path";
4038
+ var UNINSTALL_BACKUPS_DIR = join25(homedir4(), ".avatar", "uninstall-backups");
3346
4039
  async function createUninstallBackupSnapshot(projectRoot, artifacts, avatarVersion) {
3347
4040
  const projectName = basename2(projectRoot);
3348
4041
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
3349
- const backupDir = join21(UNINSTALL_BACKUPS_DIR, `${projectName}-${timestamp}`);
4042
+ const backupDir = join25(UNINSTALL_BACKUPS_DIR, `${projectName}-${timestamp}`);
3350
4043
  await mkdir(backupDir, { recursive: true, mode: 448 });
3351
4044
  if (artifacts.claudeDir) {
3352
- await cp(artifacts.claudeDir, join21(backupDir, ".claude"), { recursive: true });
4045
+ await cp(artifacts.claudeDir, join25(backupDir, ".claude"), { recursive: true });
3353
4046
  }
3354
4047
  if (artifacts.claudeMd) {
3355
- await cp(artifacts.claudeMd, join21(backupDir, "CLAUDE.md"));
4048
+ await cp(artifacts.claudeMd, join25(backupDir, "CLAUDE.md"));
3356
4049
  }
3357
4050
  if (artifacts.postMergeHook || artifacts.prePushHook) {
3358
- const hooksBackupDir = join21(backupDir, "hooks");
4051
+ const hooksBackupDir = join25(backupDir, "hooks");
3359
4052
  await mkdir(hooksBackupDir, { recursive: true });
3360
4053
  if (artifacts.postMergeHook) {
3361
- await cp(artifacts.postMergeHook, join21(hooksBackupDir, "post-merge"));
4054
+ await cp(artifacts.postMergeHook, join25(hooksBackupDir, "post-merge"));
3362
4055
  }
3363
4056
  if (artifacts.prePushHook) {
3364
- await cp(artifacts.prePushHook, join21(hooksBackupDir, "pre-push"));
4057
+ await cp(artifacts.prePushHook, join25(hooksBackupDir, "pre-push"));
3365
4058
  }
3366
4059
  }
3367
4060
  const manifest = {
@@ -3376,27 +4069,27 @@ async function createUninstallBackupSnapshot(projectRoot, artifacts, avatarVersi
3376
4069
  prePushHook: !!artifacts.prePushHook
3377
4070
  }
3378
4071
  };
3379
- await writeFile(join21(backupDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
4072
+ await writeFile(join25(backupDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
3380
4073
  return backupDir;
3381
4074
  }
3382
4075
 
3383
4076
  // src/lib/detect-avatar-project-artifacts.ts
3384
- import { existsSync as existsSync7 } from "fs";
3385
- import { join as join22 } from "path";
4077
+ import { existsSync as existsSync9 } from "fs";
4078
+ import { join as join26 } from "path";
3386
4079
  function existsOrNull(path) {
3387
- return existsSync7(path) ? path : null;
4080
+ return existsSync9(path) ? path : null;
3388
4081
  }
3389
4082
  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"));
4083
+ const claudeDir = existsOrNull(join26(projectRoot, ".claude"));
4084
+ const claudeMd = existsOrNull(join26(projectRoot, "CLAUDE.md"));
4085
+ const postMergeHook = existsOrNull(join26(projectRoot, ".git", "hooks", "post-merge"));
3393
4086
  const prePushHook = existsOrNull(
3394
- join22(projectRoot, ".git", "modules", "src", "hooks", "pre-push")
4087
+ join26(projectRoot, ".git", "modules", "src", "hooks", "pre-push")
3395
4088
  );
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"));
4089
+ const gitignorePath = existsOrNull(join26(projectRoot, ".gitignore"));
4090
+ const gitmodulesPath = existsOrNull(join26(projectRoot, ".gitmodules"));
4091
+ const notesDir = existsOrNull(join26(projectRoot, "notes"));
4092
+ const scriptsDir = existsOrNull(join26(projectRoot, "scripts"));
3400
4093
  const hasAnyArtifact = !!(claudeDir || claudeMd || postMergeHook || prePushHook);
3401
4094
  return {
3402
4095
  hasAnyArtifact,
@@ -3417,11 +4110,11 @@ async function executeUninstallDeletion(artifacts, flags) {
3417
4110
  if (artifacts.claudeDir) {
3418
4111
  if (flags.keepSubmodule) {
3419
4112
  const { readdir: readdir2 } = await import("fs/promises");
3420
- const { join: join23 } = await import("path");
4113
+ const { join: join27 } = await import("path");
3421
4114
  const entries = await readdir2(artifacts.claudeDir);
3422
4115
  for (const entry of entries) {
3423
4116
  if (entry === "pack") continue;
3424
- await rm(join23(artifacts.claudeDir, entry), { recursive: true, force: true });
4117
+ await rm(join27(artifacts.claudeDir, entry), { recursive: true, force: true });
3425
4118
  }
3426
4119
  } else {
3427
4120
  await rm(artifacts.claudeDir, { recursive: true, force: true });
@@ -3490,7 +4183,7 @@ async function removeSubmoduleEntry(gitmodulesPath, submodulePath) {
3490
4183
  }
3491
4184
 
3492
4185
  // src/commands/uninstall.ts
3493
- var CLI_VERSION = "1.3.3";
4186
+ var CLI_VERSION = "1.4.1";
3494
4187
  function registerUninstallCommand(program2) {
3495
4188
  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
4189
  try {
@@ -3514,7 +4207,7 @@ async function runUninstall(opts) {
3514
4207
  return;
3515
4208
  }
3516
4209
  if (!opts.yes) {
3517
- const ok = await confirm4({
4210
+ const ok = await confirm6({
3518
4211
  message: "Ti\u1EBFp t\u1EE5c g\u1EE1 Avatar?",
3519
4212
  default: false
3520
4213
  });
@@ -3567,12 +4260,12 @@ function printUninstallSuccessBox(backupPath) {
3567
4260
  lines.push(` ${chalk.dim("Backup:")} ${backupPath}`);
3568
4261
  lines.push(` ${chalk.dim("Restore:")} ${chalk.cyan(`cp -r "${backupPath}"/* .`)}`);
3569
4262
  }
3570
- process.stdout.write(`${boxen6(lines.join("\n"), { padding: 1, borderStyle: "round" })}
4263
+ process.stdout.write(`${boxen7(lines.join("\n"), { padding: 1, borderStyle: "round" })}
3571
4264
  `);
3572
4265
  }
3573
4266
 
3574
4267
  // src/index.ts
3575
- var CLI_VERSION2 = "1.3.3";
4268
+ var CLI_VERSION2 = "1.4.1";
3576
4269
  var program = new Command();
3577
4270
  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
4271
  "beforeAll",
@@ -3599,6 +4292,7 @@ registerToolsCommand(program);
3599
4292
  registerSecretsCommand(program);
3600
4293
  registerMcpRunCommand(program);
3601
4294
  registerAiCommand(program);
4295
+ registerGitnexusCommand(program);
3602
4296
  registerUninstallCommand(program);
3603
4297
  program.parseAsync(process.argv).catch((err) => {
3604
4298
  const msg = err instanceof Error ? err.message : String(err);