@nalvietnam/avatar-cli 1.3.2 → 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) {
@@ -1120,14 +1120,7 @@ async function writeWithBackup(path, content, mode) {
1120
1120
  await writeTextAtomic(path, content, mode);
1121
1121
  return backup;
1122
1122
  }
1123
- var CLAUDE_SUBDIRS = ["project", "state", "_pending", "_backup"];
1124
- var PROJECT_KNOWLEDGE_TEMPLATES = [
1125
- "project/tech-stack.md",
1126
- "project/conventions.md",
1127
- "project/architecture.md",
1128
- "project/domain.md",
1129
- "project/gotchas.md"
1130
- ];
1123
+ var CLAUDE_SUBDIRS = ["state", "_pending", "_backup"];
1131
1124
  async function createClaudeDirTree(projectRoot) {
1132
1125
  const claudeRoot = join10(projectRoot, ".claude");
1133
1126
  await ensureDir(claudeRoot);
@@ -1137,39 +1130,8 @@ async function createClaudeDirTree(projectRoot) {
1137
1130
  await writeTextAtomic(join10(dir, ".gitkeep"), "");
1138
1131
  }
1139
1132
  }
1140
- async function writeProjectKnowledgeFiles(projectRoot, vars) {
1141
- const baseVars = {
1142
- ...vars,
1143
- primaryLanguage: "(ch\u01B0a scan)",
1144
- frameworks: "(ch\u01B0a scan)",
1145
- databases: "(ch\u01B0a scan)",
1146
- testStack: "(ch\u01B0a scan)",
1147
- buildStack: "(ch\u01B0a scan)",
1148
- toolVersions: "(ch\u01B0a scan)",
1149
- codeStyle: "(ch\u01B0a scan)",
1150
- namingConvention: "(ch\u01B0a scan)",
1151
- folderStructure: "(ch\u01B0a scan)",
1152
- commitConvention: "(ch\u01B0a scan)",
1153
- linterConfig: "(ch\u01B0a scan)",
1154
- architectureOverview: "(ch\u01B0a scan)",
1155
- moduleLayout: "(ch\u01B0a scan)",
1156
- dataFlow: "(ch\u01B0a scan)",
1157
- externalIntegrations: "(ch\u01B0a scan)",
1158
- deploymentTopology: "(ch\u01B0a scan)",
1159
- domainDescription: "(ch\u01B0a scan)",
1160
- coreEntities: "(ch\u01B0a scan)",
1161
- primaryUseCases: "(ch\u01B0a scan)",
1162
- domainGlossary: "(ch\u01B0a scan)"
1163
- };
1164
- const backups = [];
1165
- for (const tpl of PROJECT_KNOWLEDGE_TEMPLATES) {
1166
- const content = await renderTemplateByName(tpl, baseVars);
1167
- const relative4 = tpl.replace(/^project\//, "");
1168
- const outPath = join10(projectRoot, ".claude", "project", relative4);
1169
- const backup = await writeWithBackup(outPath, content);
1170
- if (backup) backups.push(backup);
1171
- }
1172
- return backups;
1133
+ async function writeProjectKnowledgeFiles(_projectRoot, _vars) {
1134
+ return [];
1173
1135
  }
1174
1136
  async function writeRootClaudeMd(projectRoot, vars) {
1175
1137
  const content = await renderTemplateByName("CLAUDE.md", vars);
@@ -1344,14 +1306,196 @@ async function applyFixes(checks) {
1344
1306
  if (count === 0) log.dim("Kh\xF4ng c\xF3 g\xEC \u0111\u1EC3 fix t\u1EF1 \u0111\u1ED9ng.");
1345
1307
  }
1346
1308
 
1347
- // src/commands/init.ts
1348
- import { basename, join as join18, relative as relative2, resolve } from "path";
1349
- import { confirm as confirm3, input as input5, select as select8 } from "@inquirer/prompts";
1350
- 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";
1351
1313
 
1352
- // 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
1353
1407
  import { spawnSync as spawnSync8 } from "child_process";
1354
- 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
+ }
1355
1499
 
1356
1500
  // src/lib/prompt-recovery-action-on-failure.ts
1357
1501
  import { input as input3, select as select3 } from "@inquirer/prompts";
@@ -1377,24 +1521,408 @@ async function promptRetryOrSkip(args) {
1377
1521
  });
1378
1522
  }
1379
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
+
1380
1908
  // src/lib/team-pack-submodule-manager.ts
1381
- import { join as join12 } from "path";
1909
+ import { join as join16 } from "path";
1382
1910
 
1383
1911
  // src/lib/check-team-pack-access-with-retry-loop.ts
1384
- import { spawnSync as spawnSync7 } from "child_process";
1385
- import { confirm as confirm2, select as select4 } from "@inquirer/prompts";
1386
- 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";
1387
1915
  function parseRepoSlugFromGitUrl(url) {
1388
1916
  const httpsMatch = url.match(/github\.com[/:]([^/]+\/[^/]+?)(?:\.git)?$/);
1389
1917
  if (httpsMatch) return httpsMatch[1];
1390
1918
  return null;
1391
1919
  }
1392
1920
  function checkRepoAccess(repoSlug) {
1393
- const r = spawnSync7("gh", ["api", `repos/${repoSlug}`], { stdio: "ignore" });
1921
+ const r = spawnSync12("gh", ["api", `repos/${repoSlug}`], { stdio: "ignore" });
1394
1922
  return r.status === 0;
1395
1923
  }
1396
1924
  function getCurrentGhUser() {
1397
- const r = spawnSync7("gh", ["api", "user", "--jq", ".login"], {
1925
+ const r = spawnSync12("gh", ["api", "user", "--jq", ".login"], {
1398
1926
  encoding: "utf8",
1399
1927
  stdio: ["ignore", "pipe", "pipe"]
1400
1928
  });
@@ -1403,13 +1931,13 @@ function getCurrentGhUser() {
1403
1931
  }
1404
1932
  function triggerGhAuthLoginInteractive() {
1405
1933
  log.info("M\u1EDF browser \u0111\u1EC3 switch GitHub account...");
1406
- const r = spawnSync7("gh", ["auth", "login", "--web"], { stdio: "inherit" });
1934
+ const r = spawnSync12("gh", ["auth", "login", "--web"], { stdio: "inherit" });
1407
1935
  if (r.status !== 0) {
1408
1936
  log.warn(`gh auth login exit ${r.status}. C\xF3 th\u1EC3 ch\u1EA1y tay r\u1ED3i quay l\u1EA1i retry.`);
1409
1937
  }
1410
1938
  }
1411
1939
  async function copyInfoToClipboardWithConsent(info) {
1412
- const ok = await confirm2({
1940
+ const ok = await confirm4({
1413
1941
  message: "Copy th\xF4ng tin (GitHub username + email) v\xE0o clipboard \u0111\u1EC3 d\xE1n v\xE0o Slack/email?",
1414
1942
  default: true
1415
1943
  });
@@ -1438,7 +1966,7 @@ function printAccessWarningBox(repoSlug, ghUser, ssoEmail) {
1438
1966
  `${chalk.dim("Li\xEAn h\u1EC7:")} luke@nal.vn (Slack #avatar-setup)`
1439
1967
  ];
1440
1968
  process.stdout.write(
1441
- `${boxen2(lines.join("\n"), { padding: 1, borderColor: "red", borderStyle: "round" })}
1969
+ `${boxen3(lines.join("\n"), { padding: 1, borderColor: "red", borderStyle: "round" })}
1442
1970
  `
1443
1971
  );
1444
1972
  }
@@ -1543,7 +2071,7 @@ async function addTeamPackSubmodule(projectRoot, tag, ssoEmail) {
1543
2071
  }
1544
2072
  let target = tag ?? null;
1545
2073
  if (!target) {
1546
- target = await latestTag(join12(projectRoot, TEAM_PACK_RELATIVE_PATH));
2074
+ target = await latestTag(join16(projectRoot, TEAM_PACK_RELATIVE_PATH));
1547
2075
  }
1548
2076
  if (target) {
1549
2077
  await checkoutTagInSubmodule(TEAM_PACK_RELATIVE_PATH, target, projectRoot);
@@ -1551,7 +2079,7 @@ async function addTeamPackSubmodule(projectRoot, tag, ssoEmail) {
1551
2079
  return { pinnedTag: target };
1552
2080
  }
1553
2081
  async function readPinnedPackVersion(projectRoot) {
1554
- const submoduleRoot = join12(projectRoot, TEAM_PACK_RELATIVE_PATH);
2082
+ const submoduleRoot = join16(projectRoot, TEAM_PACK_RELATIVE_PATH);
1555
2083
  const tag = await latestTag(submoduleRoot);
1556
2084
  if (tag) return tag;
1557
2085
  const sha = await currentCommitSha(submoduleRoot);
@@ -1565,14 +2093,14 @@ function isSshPermissionError(message) {
1565
2093
  }
1566
2094
  function triggerGhAuthLoginInteractive2() {
1567
2095
  log.info("M\u1EDF browser \u0111\u1EC3 switch GitHub account...");
1568
- const r = spawnSync8("gh", ["auth", "login", "--web"], { stdio: "inherit" });
2096
+ const r = spawnSync13("gh", ["auth", "login", "--web"], { stdio: "inherit" });
1569
2097
  if (r.status !== 0) {
1570
2098
  log.warn(`gh auth login exit ${r.status}. C\xF3 th\u1EC3 ch\u1EA1y tay r\u1ED3i quay l\u1EA1i retry.`);
1571
2099
  }
1572
2100
  }
1573
2101
  function openGithubSshKeysPage() {
1574
2102
  log.info("M\u1EDF trang GitHub Settings \u2192 SSH Keys...");
1575
- const r = spawnSync8("open", ["https://github.com/settings/keys"], { stdio: "ignore" });
2103
+ const r = spawnSync13("open", ["https://github.com/settings/keys"], { stdio: "ignore" });
1576
2104
  if (r.status !== 0) {
1577
2105
  log.info("URL: https://github.com/settings/keys");
1578
2106
  }
@@ -1722,7 +2250,7 @@ ${renderAvatarBanner(opts)}
1722
2250
  }
1723
2251
 
1724
2252
  // src/lib/execute-gh-repo-create.ts
1725
- import { spawnSync as spawnSync9 } from "child_process";
2253
+ import { spawnSync as spawnSync14 } from "child_process";
1726
2254
  var RepoAlreadyExistsError = class extends Error {
1727
2255
  constructor(fullName) {
1728
2256
  super(`Repo "${fullName}" \u0111\xE3 t\u1ED3n t\u1EA1i tr\xEAn GitHub. \u0110\u1ED5i t\xEAn ho\u1EB7c x\xF3a repo c\u0169.`);
@@ -1742,7 +2270,7 @@ function executeGhRepoCreate(input6) {
1742
2270
  "origin",
1743
2271
  "--push"
1744
2272
  ];
1745
- const r = spawnSync9("gh", args, { stdio: "inherit" });
2273
+ const r = spawnSync14("gh", args, { stdio: "inherit" });
1746
2274
  if (r.status !== 0) {
1747
2275
  if (r.status === 1) {
1748
2276
  throw new RepoAlreadyExistsError(fullName);
@@ -1756,9 +2284,9 @@ function executeGhRepoCreate(input6) {
1756
2284
  }
1757
2285
 
1758
2286
  // src/lib/resolve-github-username-default.ts
1759
- import { spawnSync as spawnSync10 } from "child_process";
2287
+ import { spawnSync as spawnSync15 } from "child_process";
1760
2288
  function resolveGithubUsernameDefault() {
1761
- const r = spawnSync10("gh", ["api", "user", "--jq", ".login"], {
2289
+ const r = spawnSync15("gh", ["api", "user", "--jq", ".login"], {
1762
2290
  encoding: "utf8",
1763
2291
  stdio: ["ignore", "pipe", "pipe"]
1764
2292
  });
@@ -1806,12 +2334,12 @@ function createGithubRemoteFromFolder(input6) {
1806
2334
  }
1807
2335
 
1808
2336
  // src/lib/create-workspace-remote-via-gh.ts
1809
- import { spawnSync as spawnSync18 } from "child_process";
2337
+ import { spawnSync as spawnSync23 } from "child_process";
1810
2338
 
1811
2339
  // src/lib/check-gh-cli-auth-status.ts
1812
- import { spawnSync as spawnSync11 } from "child_process";
2340
+ import { spawnSync as spawnSync16 } from "child_process";
1813
2341
  function checkGhCliAuthStatus() {
1814
- const r = spawnSync11("gh", ["auth", "status"], { stdio: "ignore" });
2342
+ const r = spawnSync16("gh", ["auth", "status"], { stdio: "ignore" });
1815
2343
  if (r.error && r.error.code === "ENOENT") {
1816
2344
  return "not-installed";
1817
2345
  }
@@ -1819,12 +2347,12 @@ function checkGhCliAuthStatus() {
1819
2347
  }
1820
2348
 
1821
2349
  // src/lib/detect-package-manager.ts
1822
- import { spawnSync as spawnSync12 } from "child_process";
2350
+ import { spawnSync as spawnSync17 } from "child_process";
1823
2351
  function hasBinary(name) {
1824
2352
  const platform2 = detectHostPlatform();
1825
2353
  const probe = platform2 === "win32" ? "where" : "command";
1826
2354
  const args = platform2 === "win32" ? [name] : ["-v", name];
1827
- const r = spawnSync12(probe, args, {
2355
+ const r = spawnSync17(probe, args, {
1828
2356
  shell: platform2 !== "win32",
1829
2357
  stdio: "ignore"
1830
2358
  });
@@ -1840,11 +2368,11 @@ function detectPackageManager() {
1840
2368
  }
1841
2369
 
1842
2370
  // src/lib/handle-remote-access-failure-with-account-switch.ts
1843
- import { spawnSync as spawnSync14 } from "child_process";
2371
+ import { spawnSync as spawnSync19 } from "child_process";
1844
2372
  import { input as input4, select as select6 } from "@inquirer/prompts";
1845
2373
 
1846
2374
  // src/lib/verify-git-remote-accessible.ts
1847
- import { spawnSync as spawnSync13 } from "child_process";
2375
+ import { spawnSync as spawnSync18 } from "child_process";
1848
2376
  var TIMEOUT_MS = 5e3;
1849
2377
  function classifyRemoteError(stderr) {
1850
2378
  const text = stderr.toLowerCase();
@@ -1860,7 +2388,7 @@ function classifyRemoteError(stderr) {
1860
2388
  return "unknown";
1861
2389
  }
1862
2390
  function tryVerifyGitRemoteAccessible(url) {
1863
- const r = spawnSync13("git", ["ls-remote", "--exit-code", url, "HEAD"], {
2391
+ const r = spawnSync18("git", ["ls-remote", "--exit-code", url, "HEAD"], {
1864
2392
  encoding: "utf8",
1865
2393
  timeout: TIMEOUT_MS,
1866
2394
  stdio: ["ignore", "pipe", "pipe"]
@@ -1882,7 +2410,7 @@ var RemoteAccessAbortedError = class extends Error {
1882
2410
  }
1883
2411
  };
1884
2412
  function getCurrentGhUser2() {
1885
- const r = spawnSync14("gh", ["api", "user", "--jq", ".login"], {
2413
+ const r = spawnSync19("gh", ["api", "user", "--jq", ".login"], {
1886
2414
  encoding: "utf8",
1887
2415
  stdio: ["ignore", "pipe", "pipe"]
1888
2416
  });
@@ -1891,7 +2419,7 @@ function getCurrentGhUser2() {
1891
2419
  }
1892
2420
  function triggerGhAuthLoginInteractive3() {
1893
2421
  log.info("M\u1EDF browser \u0111\u1EC3 switch GitHub account...");
1894
- const r = spawnSync14("gh", ["auth", "login", "--web"], { stdio: "inherit" });
2422
+ const r = spawnSync19("gh", ["auth", "login", "--web"], { stdio: "inherit" });
1895
2423
  if (r.status !== 0) {
1896
2424
  log.warn(`gh auth login exit ${r.status}. B\u1EA1n c\xF3 th\u1EC3 ch\u1EA1y tay r\u1ED3i quay l\u1EA1i retry.`);
1897
2425
  }
@@ -1974,7 +2502,7 @@ async function handleRemoteAccessFailureWithAccountSwitch(args) {
1974
2502
  }
1975
2503
 
1976
2504
  // src/lib/install-gh-cli-via-package-manager.ts
1977
- import { spawnSync as spawnSync15 } from "child_process";
2505
+ import { spawnSync as spawnSync20 } from "child_process";
1978
2506
  var INSTALL_COMMANDS = {
1979
2507
  brew: { cmd: "brew", args: ["install", "gh"] },
1980
2508
  apt: { cmd: "sudo", args: ["apt-get", "install", "-y", "gh"] },
@@ -1985,7 +2513,7 @@ var INSTALL_COMMANDS = {
1985
2513
  function installGhCliViaPackageManager(pm) {
1986
2514
  const spec = INSTALL_COMMANDS[pm];
1987
2515
  log.info(`\u0110ang c\xE0i gh CLI qua ${pm}...`);
1988
- const r = spawnSync15(spec.cmd, spec.args, { stdio: "inherit" });
2516
+ const r = spawnSync20(spec.cmd, spec.args, { stdio: "inherit" });
1989
2517
  if (r.status !== 0) {
1990
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.`);
1991
2519
  }
@@ -1993,9 +2521,9 @@ function installGhCliViaPackageManager(pm) {
1993
2521
  }
1994
2522
 
1995
2523
  // src/lib/setup-git-credential-via-gh.ts
1996
- import { spawnSync as spawnSync16 } from "child_process";
2524
+ import { spawnSync as spawnSync21 } from "child_process";
1997
2525
  function setupGitCredentialViaGh() {
1998
- const r = spawnSync16("gh", ["auth", "setup-git"], { stdio: "ignore" });
2526
+ const r = spawnSync21("gh", ["auth", "setup-git"], { stdio: "ignore" });
1999
2527
  if (r.status !== 0) {
2000
2528
  log.warn("gh auth setup-git fail (non-fatal). N\u1EBFu git clone l\u1ED7i 128 \u2192 ch\u1EA1y th\u1EE7 c\xF4ng.");
2001
2529
  return;
@@ -2004,10 +2532,10 @@ function setupGitCredentialViaGh() {
2004
2532
  }
2005
2533
 
2006
2534
  // src/lib/trigger-gh-cli-auth-login.ts
2007
- import { spawnSync as spawnSync17 } from "child_process";
2535
+ import { spawnSync as spawnSync22 } from "child_process";
2008
2536
  function triggerGhCliAuthLogin() {
2009
2537
  log.info("Kh\u1EDFi \u0111\u1ED9ng \u0111\u0103ng nh\u1EADp GitHub qua gh CLI (browser s\u1EBD m\u1EDF)...");
2010
- const r = spawnSync17(
2538
+ const r = spawnSync22(
2011
2539
  "gh",
2012
2540
  ["auth", "login", "--hostname", "github.com", "--web", "--git-protocol", "ssh"],
2013
2541
  { stdio: "inherit" }
@@ -2125,20 +2653,20 @@ function classifyGhCreateError(stderr) {
2125
2653
  return "unknown";
2126
2654
  }
2127
2655
  function repoExistsOnGitHub(fullName) {
2128
- const r = spawnSync18("gh", ["repo", "view", fullName, "--json", "name"], {
2656
+ const r = spawnSync23("gh", ["repo", "view", fullName, "--json", "name"], {
2129
2657
  stdio: "ignore"
2130
2658
  });
2131
2659
  return r.status === 0;
2132
2660
  }
2133
2661
  function canCreateInNamespace(org, ghUser) {
2134
2662
  if (org.toLowerCase() === ghUser.toLowerCase()) return { ok: true };
2135
- const r = spawnSync18("gh", ["api", `orgs/${org}/members/${ghUser}`, "--silent"], {
2663
+ const r = spawnSync23("gh", ["api", `orgs/${org}/members/${ghUser}`, "--silent"], {
2136
2664
  stdio: "ignore"
2137
2665
  });
2138
2666
  if (r.status === 0) return { ok: true };
2139
- const orgCheck = spawnSync18("gh", ["api", `orgs/${org}`, "--silent"], { stdio: "ignore" });
2667
+ const orgCheck = spawnSync23("gh", ["api", `orgs/${org}`, "--silent"], { stdio: "ignore" });
2140
2668
  if (orgCheck.status !== 0) {
2141
- const userCheck = spawnSync18("gh", ["api", `users/${org}`, "--silent"], { stdio: "ignore" });
2669
+ const userCheck = spawnSync23("gh", ["api", `users/${org}`, "--silent"], { stdio: "ignore" });
2142
2670
  if (userCheck.status === 0) {
2143
2671
  return {
2144
2672
  ok: false,
@@ -2174,7 +2702,7 @@ async function createWorkspaceRemoteViaGh(input6) {
2174
2702
  );
2175
2703
  }
2176
2704
  log.info(`T\u1EA1o GitHub repo cho workspace: ${fullName} (${input6.visibility})...`);
2177
- const r = spawnSync18(
2705
+ const r = spawnSync23(
2178
2706
  "gh",
2179
2707
  [
2180
2708
  "repo",
@@ -2215,7 +2743,7 @@ ${combined}
2215
2743
  function linkExistingRemoteToWorkspace(args) {
2216
2744
  const sshUrl = `git@github.com:${args.fullName}.git`;
2217
2745
  const httpsUrl = `https://github.com/${args.fullName}.git`;
2218
- const addResult = spawnSync18(
2746
+ const addResult = spawnSync23(
2219
2747
  "git",
2220
2748
  ["-C", args.workspacePath, "remote", "add", "origin", sshUrl],
2221
2749
  {
@@ -2224,7 +2752,7 @@ function linkExistingRemoteToWorkspace(args) {
2224
2752
  }
2225
2753
  );
2226
2754
  if (addResult.status !== 0) {
2227
- spawnSync18("git", ["-C", args.workspacePath, "remote", "set-url", "origin", sshUrl], {
2755
+ spawnSync23("git", ["-C", args.workspacePath, "remote", "set-url", "origin", sshUrl], {
2228
2756
  stdio: "ignore"
2229
2757
  });
2230
2758
  }
@@ -2238,11 +2766,11 @@ import { select as select7 } from "@inquirer/prompts";
2238
2766
  import { simpleGit as simpleGit3 } from "simple-git";
2239
2767
 
2240
2768
  // src/lib/check-folder-has-git.ts
2241
- import { existsSync as existsSync4, statSync } from "fs";
2242
- import { join as join13 } from "path";
2769
+ import { existsSync as existsSync6, statSync } from "fs";
2770
+ import { join as join17 } from "path";
2243
2771
  function checkFolderHasGit(folderPath) {
2244
- const gitPath = join13(folderPath, ".git");
2245
- if (!existsSync4(gitPath)) return false;
2772
+ const gitPath = join17(folderPath, ".git");
2773
+ if (!existsSync6(gitPath)) return false;
2246
2774
  const stat = statSync(gitPath);
2247
2775
  return stat.isDirectory() || stat.isFile();
2248
2776
  }
@@ -2272,8 +2800,8 @@ async function createInitialGitCommit(folderPath) {
2272
2800
  }
2273
2801
 
2274
2802
  // src/lib/detect-folder-tech-stack.ts
2275
- import { existsSync as existsSync5 } from "fs";
2276
- import { join as join14 } from "path";
2803
+ import { existsSync as existsSync7 } from "fs";
2804
+ import { join as join18 } from "path";
2277
2805
  var SIGNATURES = {
2278
2806
  node: ["package.json"],
2279
2807
  python: ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile"],
@@ -2285,7 +2813,7 @@ var SIGNATURES = {
2285
2813
  function detectFolderTechStack(folderPath) {
2286
2814
  const matched = [];
2287
2815
  for (const [stack, files] of Object.entries(SIGNATURES)) {
2288
- if (files.some((f) => existsSync5(join14(folderPath, f)))) {
2816
+ if (files.some((f) => existsSync7(join18(folderPath, f)))) {
2289
2817
  matched.push(stack);
2290
2818
  }
2291
2819
  }
@@ -2294,25 +2822,25 @@ function detectFolderTechStack(folderPath) {
2294
2822
 
2295
2823
  // src/lib/gitignore-template-loader.ts
2296
2824
  import { readFileSync as readFileSync3 } from "fs";
2297
- import { dirname as dirname4, join as join15 } from "path";
2825
+ import { dirname as dirname4, join as join19 } from "path";
2298
2826
  import { fileURLToPath as fileURLToPath2 } from "url";
2299
2827
  var __dirname = dirname4(fileURLToPath2(import.meta.url));
2300
2828
  var CANDIDATE_DIRS = [
2301
2829
  // Bundled production: dist/index.js → __dirname = .../dist/, sibling dist/templates
2302
- join15(__dirname, "templates", "gitignore"),
2830
+ join19(__dirname, "templates", "gitignore"),
2303
2831
  // Legacy bundled: nếu file là dist/lib/*.js (sub-bundle), templates ở dist/templates
2304
- join15(__dirname, "..", "templates", "gitignore"),
2832
+ join19(__dirname, "..", "templates", "gitignore"),
2305
2833
  // Dev mode (vitest/tsx run src/ trực tiếp): __dirname = src/lib/
2306
- join15(__dirname, "..", "..", "src", "templates", "gitignore"),
2834
+ join19(__dirname, "..", "..", "src", "templates", "gitignore"),
2307
2835
  // npm-installed alt: __dirname = .../dist/ → package_root/src/templates
2308
- join15(__dirname, "..", "src", "templates", "gitignore")
2836
+ join19(__dirname, "..", "src", "templates", "gitignore")
2309
2837
  ];
2310
2838
  var AVATAR_MARKER_START = "# === avatar ===";
2311
2839
  var AVATAR_MARKER_END = "# === /avatar ===";
2312
2840
  function readTemplate(stack) {
2313
2841
  for (const dir of CANDIDATE_DIRS) {
2314
2842
  try {
2315
- return readFileSync3(join15(dir, `${stack}.txt`), "utf8");
2843
+ return readFileSync3(join19(dir, `${stack}.txt`), "utf8");
2316
2844
  } catch {
2317
2845
  }
2318
2846
  }
@@ -2326,11 +2854,11 @@ ${readTemplate(s).trim()}`);
2326
2854
  }
2327
2855
 
2328
2856
  // src/lib/write-or-merge-gitignore.ts
2329
- import { existsSync as existsSync6, readFileSync as readFileSync4, writeFileSync } from "fs";
2330
- import { join as join16 } from "path";
2857
+ import { existsSync as existsSync8, readFileSync as readFileSync4, writeFileSync } from "fs";
2858
+ import { join as join20 } from "path";
2331
2859
  function writeOrMergeGitignore(folderPath, avatarBlock) {
2332
- const path = join16(folderPath, ".gitignore");
2333
- if (!existsSync6(path)) {
2860
+ const path = join20(folderPath, ".gitignore");
2861
+ if (!existsSync8(path)) {
2334
2862
  writeFileSync(path, avatarBlock, "utf8");
2335
2863
  return;
2336
2864
  }
@@ -2506,7 +3034,7 @@ async function safeBootstrapGitInFolder(folderPath, opts = {}) {
2506
3034
 
2507
3035
  // src/commands/init-conflict-detection-helpers.ts
2508
3036
  import { readdir } from "fs/promises";
2509
- import { join as join17 } from "path";
3037
+ import { join as join21 } from "path";
2510
3038
  async function isEmptyOrMissing(path) {
2511
3039
  if (!await pathExists(path)) return true;
2512
3040
  try {
@@ -2519,7 +3047,7 @@ async function isEmptyOrMissing(path) {
2519
3047
  }
2520
3048
  async function findAlternativeWorkspaceName(parent, desiredName, maxAttempts = 10) {
2521
3049
  for (let i = 2; i < maxAttempts; i++) {
2522
- const candidate = join17(parent, `${desiredName}-${i}`);
3050
+ const candidate = join21(parent, `${desiredName}-${i}`);
2523
3051
  if (await isEmptyOrMissing(candidate)) return candidate;
2524
3052
  }
2525
3053
  return null;
@@ -2535,6 +3063,38 @@ function inferWorkspaceName(repoUrl) {
2535
3063
  const withoutPrefix = baseName.replace(/^avatar-/, "");
2536
3064
  return `avatar-${withoutPrefix}-workspace`;
2537
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
+ }
2538
3098
  function buildScaffoldVariables(args) {
2539
3099
  return {
2540
3100
  projectName: args.projectName,
@@ -2543,12 +3103,13 @@ function buildScaffoldVariables(args) {
2543
3103
  avatarVersion: AVATAR_CLI_VERSION,
2544
3104
  packVersion: args.packVersion,
2545
3105
  lastScan: (/* @__PURE__ */ new Date()).toISOString(),
2546
- mode: args.mode
3106
+ mode: args.mode,
3107
+ gitnexusSection: buildGitnexusSection(args.gitnexusReady ?? false)
2547
3108
  };
2548
3109
  }
2549
3110
 
2550
3111
  // src/commands/login.ts
2551
- import boxen3 from "boxen";
3112
+ import boxen4 from "boxen";
2552
3113
  import open from "open";
2553
3114
 
2554
3115
  // src/lib/google-oauth-device-flow.ts
@@ -2695,7 +3256,7 @@ async function runLogin(opts) {
2695
3256
  "",
2696
3257
  `Ho\u1EB7c Avatar t\u1EF1 m\u1EDF browser, click ${chalk.green("Allow")}...`
2697
3258
  ].join("\n");
2698
- process.stdout.write(`${boxen3(instructions, { padding: 1, borderStyle: "round" })}
3259
+ process.stdout.write(`${boxen4(instructions, { padding: 1, borderStyle: "round" })}
2699
3260
  `);
2700
3261
  void open(verificationUrl).catch(() => {
2701
3262
  log.dim("(Kh\xF4ng m\u1EDF \u0111\u01B0\u1EE3c browser t\u1EF1 \u0111\u1ED9ng \u2014 copy URL \u1EDF tr\xEAn)");
@@ -2751,6 +3312,9 @@ function parseBootstrapStrategyOpts(opts) {
2751
3312
  }
2752
3313
  function registerInitCommand(program2) {
2753
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(
2754
3318
  "--bootstrap-strategy <s>",
2755
3319
  "X\u1EED l\xFD folder dirty: stash | commit-all | skip | branch (default: prompt)"
2756
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) => {
@@ -2856,6 +3420,7 @@ async function runInitFromExistingRemote(opts, ownerEmail) {
2856
3420
  repoOrg: opts.repoOrg,
2857
3421
  flow: "existing-remote",
2858
3422
  aiSkip: opts.aiSkip,
3423
+ gitnexusSkip: opts.gitnexusSkip,
2859
3424
  ssoEmail: ownerEmail
2860
3425
  });
2861
3426
  }
@@ -2892,6 +3457,7 @@ async function runInitFromExistingFolder(opts, ownerEmail) {
2892
3457
  repoOrg: opts.repoOrg,
2893
3458
  flow: "existing-folder",
2894
3459
  aiSkip: opts.aiSkip,
3460
+ gitnexusSkip: opts.gitnexusSkip,
2895
3461
  ssoEmail: ownerEmail
2896
3462
  });
2897
3463
  }
@@ -2911,7 +3477,7 @@ async function runInitFromScratch(opts, ownerEmail) {
2911
3477
  const teamOwner = opts.teamOwner ?? await promptTeamOwner(ownerEmail);
2912
3478
  const workspaceParent = resolve(opts.workspaceParent ?? ".");
2913
3479
  const workspacePath = await resolveWorkspacePath(workspaceParent, projectName, opts.force);
2914
- const srcPath = join18(workspacePath, "src");
3480
+ const srcPath = join22(workspacePath, "src");
2915
3481
  await ensureDir(workspacePath);
2916
3482
  await ensureDir(srcPath);
2917
3483
  await safeBootstrapGitInFolder(srcPath, { autoYes: true });
@@ -2952,7 +3518,8 @@ async function runInitFromScratch(opts, ownerEmail) {
2952
3518
  repoVisibility: opts.repoVisibility,
2953
3519
  repoOrg: opts.repoOrg,
2954
3520
  flow: "new-project",
2955
- aiSkip: opts.aiSkip
3521
+ aiSkip: opts.aiSkip,
3522
+ gitnexusSkip: opts.gitnexusSkip
2956
3523
  });
2957
3524
  } catch (err) {
2958
3525
  sp.fail("Init workspace th\u1EA5t b\u1EA1i");
@@ -2966,7 +3533,7 @@ async function getOrCreateOriginRemote(folderPath, opts) {
2966
3533
  log.success(`Folder \u0111\xE3 c\xF3 remote origin: ${origin.refs.push}`);
2967
3534
  return origin.refs.push;
2968
3535
  }
2969
- const shouldCreate = opts.createRemote ?? await confirm3({
3536
+ const shouldCreate = opts.createRemote ?? await confirm5({
2970
3537
  message: "Folder ch\u01B0a c\xF3 remote. T\u1EA1o GitHub repo ngay \u0111\u1EC3 share team?",
2971
3538
  default: true
2972
3539
  });
@@ -3027,7 +3594,8 @@ async function scaffoldWorkspaceWithSrcSubmodule(args) {
3027
3594
  repoVisibility: args.repoVisibility,
3028
3595
  repoOrg: args.repoOrg,
3029
3596
  flow: args.flow,
3030
- aiSkip: args.aiSkip
3597
+ aiSkip: args.aiSkip,
3598
+ gitnexusSkip: args.gitnexusSkip
3031
3599
  });
3032
3600
  } catch (err) {
3033
3601
  sp.fail("Init workspace th\u1EA5t b\u1EA1i");
@@ -3047,10 +3615,10 @@ async function finalizeWorkspaceScaffold(args) {
3047
3615
  await writeRootClaudeMd(args.workspacePath, vars);
3048
3616
  await writeProjectSettings(args.workspacePath, vars);
3049
3617
  await appendGitignoreEntries(args.workspacePath);
3050
- await ensureDir(join18(args.workspacePath, "notes"));
3051
- await ensureDir(join18(args.workspacePath, "scripts"));
3052
- await installGitHook(join18(args.workspacePath, ".git"), "post-merge");
3053
- 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");
3054
3622
  log.success("C\xE0i post-merge (workspace) + pre-push (src/)");
3055
3623
  await appendAuditEntry("init", `flow=${args.flow},workspace=${args.workspaceName}`);
3056
3624
  await maybeCommitWorkspace(args.workspacePath, args.skipCommit);
@@ -3061,7 +3629,30 @@ async function finalizeWorkspaceScaffold(args) {
3061
3629
  } else {
3062
3630
  aiResult = await runAiSetupPhase({ workspacePath: args.workspacePath });
3063
3631
  }
3064
- 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);
3065
3656
  }
3066
3657
  async function maybeCreateWorkspaceRemote(args) {
3067
3658
  if (args.skipCommit) {
@@ -3071,7 +3662,7 @@ async function maybeCreateWorkspaceRemote(args) {
3071
3662
  let shouldCreate = args.createWorkspaceRemote;
3072
3663
  if (shouldCreate === void 0) {
3073
3664
  if (args.autoYes) return;
3074
- shouldCreate = await confirm3({
3665
+ shouldCreate = await confirm5({
3075
3666
  message: "T\u1EA1o remote GitHub cho workspace \u0111\u1EC3 share team? (Avatar state)",
3076
3667
  default: false
3077
3668
  });
@@ -3150,7 +3741,7 @@ async function maybeCreateWorkspaceRemote(args) {
3150
3741
  }
3151
3742
  }
3152
3743
  async function resolveWorkspacePath(parent, desiredName, force) {
3153
- const desired = join18(parent, desiredName);
3744
+ const desired = join22(parent, desiredName);
3154
3745
  if (await isEmptyOrMissing(desired)) return desired;
3155
3746
  log.warn(`Workspace path "${desired}" \u0111\xE3 c\xF3 n\u1ED9i dung.`);
3156
3747
  while (true) {
@@ -3181,7 +3772,7 @@ async function resolveWorkspacePath(parent, desiredName, force) {
3181
3772
  message: "T\xEAn workspace m\u1EDBi:",
3182
3773
  validate: (v) => v.trim().length > 0 ? true : "T\xEAn kh\xF4ng \u0111\u01B0\u1EE3c r\u1ED7ng"
3183
3774
  });
3184
- const newPath = join18(parent, newName.trim());
3775
+ const newPath = join22(parent, newName.trim());
3185
3776
  if (await isEmptyOrMissing(newPath)) return newPath;
3186
3777
  log.warn(`"${newPath}" c\u0169ng \u0111\xE3 c\xF3 n\u1ED9i dung. Th\u1EED t\xEAn kh\xE1c.`);
3187
3778
  }
@@ -3209,21 +3800,34 @@ function formatAiStatusLine(aiResult) {
3209
3800
  }
3210
3801
  return ` ${chalk.yellow("AI:")} failed (${aiResult.reason.slice(0, 60)}) \xB7 th\u1EED ${chalk.cyan("avatar ai setup")}`;
3211
3802
  }
3212
- 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) {
3213
3817
  const lines = [
3214
3818
  `${chalk.green("\u2713")} Workspace s\u1EB5n s\xE0ng: ${relative2(process.cwd(), rootPath) || rootPath}`,
3215
3819
  ` ${chalk.dim(`(flow: ${flow})`)}`,
3216
3820
  formatAiStatusLine(aiResult),
3821
+ formatGitnexusStatusLine(gitnexusResult),
3217
3822
  "",
3218
3823
  ` ${chalk.cyan(`cd ${rootPath}`)}`,
3219
3824
  ` ${chalk.cyan("claude")} M\u1EDF Claude Code \u1EDF workspace root`,
3220
3825
  "",
3221
- ` ${chalk.cyan("avatar commit --src")} Commit code l\xEAn remote src`,
3222
- ` ${chalk.cyan("avatar commit --avatar")} Commit Avatar state`,
3826
+ ` ${chalk.cyan("avatar commit src")} Commit code l\xEAn client remote`,
3223
3827
  ` ${chalk.cyan("avatar sync")} Pull team-ai-pack m\u1EDBi`,
3224
3828
  ` ${chalk.cyan("avatar uninstall")} G\u1EE1 Avatar (gi\u1EEF code)`
3225
3829
  ];
3226
- process.stdout.write(`${boxen4(lines.join("\n"), { padding: 1, borderStyle: "round" })}
3830
+ process.stdout.write(`${boxen5(lines.join("\n"), { padding: 1, borderStyle: "round" })}
3227
3831
  `);
3228
3832
  }
3229
3833
 
@@ -3274,18 +3878,18 @@ function registerSecretsCommand(program2) {
3274
3878
  }
3275
3879
 
3276
3880
  // src/commands/status.ts
3277
- import { promises as fs8 } from "fs";
3278
- import { join as join20 } from "path";
3279
- import boxen5 from "boxen";
3881
+ import { promises as fs10 } from "fs";
3882
+ import { join as join24 } from "path";
3883
+ import boxen6 from "boxen";
3280
3884
 
3281
3885
  // src/lib/pack-backup-manager.ts
3282
- import { promises as fs7 } from "fs";
3283
- import { join as join19 } from "path";
3886
+ import { promises as fs9 } from "fs";
3887
+ import { join as join23 } from "path";
3284
3888
  var BACKUP_DIR_NAME = "_backup";
3285
3889
  async function listBackups(projectRoot) {
3286
- const dir = join19(projectRoot, ".claude", BACKUP_DIR_NAME);
3890
+ const dir = join23(projectRoot, ".claude", BACKUP_DIR_NAME);
3287
3891
  if (!await pathExists(dir)) return [];
3288
- const entries = await fs7.readdir(dir, { withFileTypes: true });
3892
+ const entries = await fs9.readdir(dir, { withFileTypes: true });
3289
3893
  return entries.filter((e) => e.isDirectory()).map((e) => e.name).sort().reverse();
3290
3894
  }
3291
3895
 
@@ -3309,7 +3913,7 @@ function registerStatusCommand(program2) {
3309
3913
  }
3310
3914
  async function gatherStatus(cwd) {
3311
3915
  const projectName = cwd.split("/").filter(Boolean).pop() ?? "unknown";
3312
- const claudeRoot = join20(cwd, ".claude");
3916
+ const claudeRoot = join24(cwd, ".claude");
3313
3917
  const hasAvatar = await pathExists(claudeRoot);
3314
3918
  if (!hasAvatar) {
3315
3919
  return {
@@ -3322,9 +3926,9 @@ async function gatherStatus(cwd) {
3322
3926
  hasAvatar: false
3323
3927
  };
3324
3928
  }
3325
- const packVersion = await isGitRepo(join20(claudeRoot, "pack")) ? await readPinnedPackVersion(cwd).catch(() => null) : null;
3326
- const pendingDir = join20(claudeRoot, "_pending");
3327
- 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;
3328
3932
  const backupCount = (await listBackups(cwd)).length;
3329
3933
  const techStackSummary = await readTechStackFirstLine(claudeRoot);
3330
3934
  return {
@@ -3338,7 +3942,7 @@ async function gatherStatus(cwd) {
3338
3942
  };
3339
3943
  }
3340
3944
  async function readTechStackFirstLine(claudeRoot) {
3341
- const techStackPath = join20(claudeRoot, "project", "tech-stack.md");
3945
+ const techStackPath = join24(claudeRoot, "project", "tech-stack.md");
3342
3946
  if (!await pathExists(techStackPath)) return "(no tech-stack.md)";
3343
3947
  const content = await readText(techStackPath);
3344
3948
  const firstNonHeaderLine = content.split("\n").find((l) => l.trim() && !l.startsWith("#") && !l.startsWith(">"));
@@ -3354,7 +3958,7 @@ function renderStatusBox(s) {
3354
3958
  `${chalk.dim("Backups:")} ${s.backupCount}`,
3355
3959
  `${chalk.dim("Tech stack:")} ${s.techStackSummary}`
3356
3960
  ];
3357
- process.stdout.write(`${boxen5(lines.join("\n"), { padding: 1, borderStyle: "round" })}
3961
+ process.stdout.write(`${boxen6(lines.join("\n"), { padding: 1, borderStyle: "round" })}
3358
3962
  `);
3359
3963
  }
3360
3964
 
@@ -3373,33 +3977,33 @@ function registerToolsCommand(program2) {
3373
3977
 
3374
3978
  // src/commands/uninstall.ts
3375
3979
  import { relative as relative3 } from "path";
3376
- import { confirm as confirm4 } from "@inquirer/prompts";
3377
- import boxen6 from "boxen";
3980
+ import { confirm as confirm6 } from "@inquirer/prompts";
3981
+ import boxen7 from "boxen";
3378
3982
 
3379
3983
  // src/lib/create-uninstall-backup-snapshot.ts
3380
3984
  import { cp, mkdir, writeFile } from "fs/promises";
3381
- import { homedir as homedir3 } from "os";
3382
- import { basename as basename2, join as join21 } from "path";
3383
- 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");
3384
3988
  async function createUninstallBackupSnapshot(projectRoot, artifacts, avatarVersion) {
3385
3989
  const projectName = basename2(projectRoot);
3386
3990
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
3387
- const backupDir = join21(UNINSTALL_BACKUPS_DIR, `${projectName}-${timestamp}`);
3991
+ const backupDir = join25(UNINSTALL_BACKUPS_DIR, `${projectName}-${timestamp}`);
3388
3992
  await mkdir(backupDir, { recursive: true, mode: 448 });
3389
3993
  if (artifacts.claudeDir) {
3390
- await cp(artifacts.claudeDir, join21(backupDir, ".claude"), { recursive: true });
3994
+ await cp(artifacts.claudeDir, join25(backupDir, ".claude"), { recursive: true });
3391
3995
  }
3392
3996
  if (artifacts.claudeMd) {
3393
- await cp(artifacts.claudeMd, join21(backupDir, "CLAUDE.md"));
3997
+ await cp(artifacts.claudeMd, join25(backupDir, "CLAUDE.md"));
3394
3998
  }
3395
3999
  if (artifacts.postMergeHook || artifacts.prePushHook) {
3396
- const hooksBackupDir = join21(backupDir, "hooks");
4000
+ const hooksBackupDir = join25(backupDir, "hooks");
3397
4001
  await mkdir(hooksBackupDir, { recursive: true });
3398
4002
  if (artifacts.postMergeHook) {
3399
- await cp(artifacts.postMergeHook, join21(hooksBackupDir, "post-merge"));
4003
+ await cp(artifacts.postMergeHook, join25(hooksBackupDir, "post-merge"));
3400
4004
  }
3401
4005
  if (artifacts.prePushHook) {
3402
- await cp(artifacts.prePushHook, join21(hooksBackupDir, "pre-push"));
4006
+ await cp(artifacts.prePushHook, join25(hooksBackupDir, "pre-push"));
3403
4007
  }
3404
4008
  }
3405
4009
  const manifest = {
@@ -3414,27 +4018,27 @@ async function createUninstallBackupSnapshot(projectRoot, artifacts, avatarVersi
3414
4018
  prePushHook: !!artifacts.prePushHook
3415
4019
  }
3416
4020
  };
3417
- 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");
3418
4022
  return backupDir;
3419
4023
  }
3420
4024
 
3421
4025
  // src/lib/detect-avatar-project-artifacts.ts
3422
- import { existsSync as existsSync7 } from "fs";
3423
- import { join as join22 } from "path";
4026
+ import { existsSync as existsSync9 } from "fs";
4027
+ import { join as join26 } from "path";
3424
4028
  function existsOrNull(path) {
3425
- return existsSync7(path) ? path : null;
4029
+ return existsSync9(path) ? path : null;
3426
4030
  }
3427
4031
  function detectAvatarProjectArtifacts(projectRoot) {
3428
- const claudeDir = existsOrNull(join22(projectRoot, ".claude"));
3429
- const claudeMd = existsOrNull(join22(projectRoot, "CLAUDE.md"));
3430
- 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"));
3431
4035
  const prePushHook = existsOrNull(
3432
- join22(projectRoot, ".git", "modules", "src", "hooks", "pre-push")
4036
+ join26(projectRoot, ".git", "modules", "src", "hooks", "pre-push")
3433
4037
  );
3434
- const gitignorePath = existsOrNull(join22(projectRoot, ".gitignore"));
3435
- const gitmodulesPath = existsOrNull(join22(projectRoot, ".gitmodules"));
3436
- const notesDir = existsOrNull(join22(projectRoot, "notes"));
3437
- 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"));
3438
4042
  const hasAnyArtifact = !!(claudeDir || claudeMd || postMergeHook || prePushHook);
3439
4043
  return {
3440
4044
  hasAnyArtifact,
@@ -3455,11 +4059,11 @@ async function executeUninstallDeletion(artifacts, flags) {
3455
4059
  if (artifacts.claudeDir) {
3456
4060
  if (flags.keepSubmodule) {
3457
4061
  const { readdir: readdir2 } = await import("fs/promises");
3458
- const { join: join23 } = await import("path");
4062
+ const { join: join27 } = await import("path");
3459
4063
  const entries = await readdir2(artifacts.claudeDir);
3460
4064
  for (const entry of entries) {
3461
4065
  if (entry === "pack") continue;
3462
- await rm(join23(artifacts.claudeDir, entry), { recursive: true, force: true });
4066
+ await rm(join27(artifacts.claudeDir, entry), { recursive: true, force: true });
3463
4067
  }
3464
4068
  } else {
3465
4069
  await rm(artifacts.claudeDir, { recursive: true, force: true });
@@ -3528,7 +4132,7 @@ async function removeSubmoduleEntry(gitmodulesPath, submodulePath) {
3528
4132
  }
3529
4133
 
3530
4134
  // src/commands/uninstall.ts
3531
- var CLI_VERSION = "1.3.2";
4135
+ var CLI_VERSION = "1.4.0";
3532
4136
  function registerUninstallCommand(program2) {
3533
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) => {
3534
4138
  try {
@@ -3552,7 +4156,7 @@ async function runUninstall(opts) {
3552
4156
  return;
3553
4157
  }
3554
4158
  if (!opts.yes) {
3555
- const ok = await confirm4({
4159
+ const ok = await confirm6({
3556
4160
  message: "Ti\u1EBFp t\u1EE5c g\u1EE1 Avatar?",
3557
4161
  default: false
3558
4162
  });
@@ -3605,12 +4209,12 @@ function printUninstallSuccessBox(backupPath) {
3605
4209
  lines.push(` ${chalk.dim("Backup:")} ${backupPath}`);
3606
4210
  lines.push(` ${chalk.dim("Restore:")} ${chalk.cyan(`cp -r "${backupPath}"/* .`)}`);
3607
4211
  }
3608
- process.stdout.write(`${boxen6(lines.join("\n"), { padding: 1, borderStyle: "round" })}
4212
+ process.stdout.write(`${boxen7(lines.join("\n"), { padding: 1, borderStyle: "round" })}
3609
4213
  `);
3610
4214
  }
3611
4215
 
3612
4216
  // src/index.ts
3613
- var CLI_VERSION2 = "1.3.2";
4217
+ var CLI_VERSION2 = "1.4.0";
3614
4218
  var program = new Command();
3615
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(
3616
4220
  "beforeAll",
@@ -3637,6 +4241,7 @@ registerToolsCommand(program);
3637
4241
  registerSecretsCommand(program);
3638
4242
  registerMcpRunCommand(program);
3639
4243
  registerAiCommand(program);
4244
+ registerGitnexusCommand(program);
3640
4245
  registerUninstallCommand(program);
3641
4246
  program.parseAsync(process.argv).catch((err) => {
3642
4247
  const msg = err instanceof Error ? err.message : String(err);