@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:
|
|
138
|
-
await
|
|
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 = ["
|
|
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(
|
|
1141
|
-
|
|
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/
|
|
1348
|
-
import {
|
|
1349
|
-
import {
|
|
1350
|
-
import
|
|
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/
|
|
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
|
-
|
|
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
|
|
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
|
|
1385
|
-
import { confirm as
|
|
1386
|
-
import
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
`${
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
2287
|
+
import { spawnSync as spawnSync15 } from "child_process";
|
|
1760
2288
|
function resolveGithubUsernameDefault() {
|
|
1761
|
-
const r =
|
|
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
|
|
2337
|
+
import { spawnSync as spawnSync23 } from "child_process";
|
|
1810
2338
|
|
|
1811
2339
|
// src/lib/check-gh-cli-auth-status.ts
|
|
1812
|
-
import { spawnSync as
|
|
2340
|
+
import { spawnSync as spawnSync16 } from "child_process";
|
|
1813
2341
|
function checkGhCliAuthStatus() {
|
|
1814
|
-
const r =
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
2524
|
+
import { spawnSync as spawnSync21 } from "child_process";
|
|
1997
2525
|
function setupGitCredentialViaGh() {
|
|
1998
|
-
const r =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
2667
|
+
const orgCheck = spawnSync23("gh", ["api", `orgs/${org}`, "--silent"], { stdio: "ignore" });
|
|
2140
2668
|
if (orgCheck.status !== 0) {
|
|
2141
|
-
const userCheck =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
|
2242
|
-
import { join as
|
|
2769
|
+
import { existsSync as existsSync6, statSync } from "fs";
|
|
2770
|
+
import { join as join17 } from "path";
|
|
2243
2771
|
function checkFolderHasGit(folderPath) {
|
|
2244
|
-
const gitPath =
|
|
2245
|
-
if (!
|
|
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
|
|
2276
|
-
import { join as
|
|
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) =>
|
|
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
|
|
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
|
-
|
|
2830
|
+
join19(__dirname, "templates", "gitignore"),
|
|
2303
2831
|
// Legacy bundled: nếu file là dist/lib/*.js (sub-bundle), templates ở dist/templates
|
|
2304
|
-
|
|
2832
|
+
join19(__dirname, "..", "templates", "gitignore"),
|
|
2305
2833
|
// Dev mode (vitest/tsx run src/ trực tiếp): __dirname = src/lib/
|
|
2306
|
-
|
|
2834
|
+
join19(__dirname, "..", "..", "src", "templates", "gitignore"),
|
|
2307
2835
|
// npm-installed alt: __dirname = .../dist/ → package_root/src/templates
|
|
2308
|
-
|
|
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(
|
|
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
|
|
2330
|
-
import { join as
|
|
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 =
|
|
2333
|
-
if (!
|
|
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
|
|
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 =
|
|
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
|
|
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(`${
|
|
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 =
|
|
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
|
|
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(
|
|
3051
|
-
await ensureDir(
|
|
3052
|
-
await installGitHook(
|
|
3053
|
-
await installGitHook(
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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(`${
|
|
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
|
|
3278
|
-
import { join as
|
|
3279
|
-
import
|
|
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
|
|
3283
|
-
import { join as
|
|
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 =
|
|
3890
|
+
const dir = join23(projectRoot, ".claude", BACKUP_DIR_NAME);
|
|
3287
3891
|
if (!await pathExists(dir)) return [];
|
|
3288
|
-
const entries = await
|
|
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 =
|
|
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(
|
|
3326
|
-
const pendingDir =
|
|
3327
|
-
const pendingCount = await pathExists(pendingDir) ? (await
|
|
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 =
|
|
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(`${
|
|
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
|
|
3377
|
-
import
|
|
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
|
|
3382
|
-
import { basename as basename2, join as
|
|
3383
|
-
var UNINSTALL_BACKUPS_DIR =
|
|
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 =
|
|
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,
|
|
3994
|
+
await cp(artifacts.claudeDir, join25(backupDir, ".claude"), { recursive: true });
|
|
3391
3995
|
}
|
|
3392
3996
|
if (artifacts.claudeMd) {
|
|
3393
|
-
await cp(artifacts.claudeMd,
|
|
3997
|
+
await cp(artifacts.claudeMd, join25(backupDir, "CLAUDE.md"));
|
|
3394
3998
|
}
|
|
3395
3999
|
if (artifacts.postMergeHook || artifacts.prePushHook) {
|
|
3396
|
-
const hooksBackupDir =
|
|
4000
|
+
const hooksBackupDir = join25(backupDir, "hooks");
|
|
3397
4001
|
await mkdir(hooksBackupDir, { recursive: true });
|
|
3398
4002
|
if (artifacts.postMergeHook) {
|
|
3399
|
-
await cp(artifacts.postMergeHook,
|
|
4003
|
+
await cp(artifacts.postMergeHook, join25(hooksBackupDir, "post-merge"));
|
|
3400
4004
|
}
|
|
3401
4005
|
if (artifacts.prePushHook) {
|
|
3402
|
-
await cp(artifacts.prePushHook,
|
|
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(
|
|
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
|
|
3423
|
-
import { join as
|
|
4026
|
+
import { existsSync as existsSync9 } from "fs";
|
|
4027
|
+
import { join as join26 } from "path";
|
|
3424
4028
|
function existsOrNull(path) {
|
|
3425
|
-
return
|
|
4029
|
+
return existsSync9(path) ? path : null;
|
|
3426
4030
|
}
|
|
3427
4031
|
function detectAvatarProjectArtifacts(projectRoot) {
|
|
3428
|
-
const claudeDir = existsOrNull(
|
|
3429
|
-
const claudeMd = existsOrNull(
|
|
3430
|
-
const postMergeHook = existsOrNull(
|
|
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
|
-
|
|
4036
|
+
join26(projectRoot, ".git", "modules", "src", "hooks", "pre-push")
|
|
3433
4037
|
);
|
|
3434
|
-
const gitignorePath = existsOrNull(
|
|
3435
|
-
const gitmodulesPath = existsOrNull(
|
|
3436
|
-
const notesDir = existsOrNull(
|
|
3437
|
-
const scriptsDir = existsOrNull(
|
|
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:
|
|
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(
|
|
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.
|
|
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
|
|
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(`${
|
|
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.
|
|
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);
|