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